commit 903df52206ae0a4e6b2aab0fd64c5b434af707a8 Author: chelsea Date: Thu Feb 12 17:41:45 2026 -0600 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5656db --- /dev/null +++ b/README.md @@ -0,0 +1,251 @@ +# LLM Bot Framework + +A template for building Discord bots powered by LLMs with natural language command parsing. + +## Features + +- **AI-Powered Parsing**: Uses LLMs to parse natural language into structured JSON with automatic retry and validation +- **Module Registry**: Easily register domain-specific command handlers +- **Flask API**: REST API with JWT authentication +- **PostgreSQL**: Generic CRUD layer for any table +- **Discord Bot**: Session management, login flow, background tasks +- **Notifications**: Discord webhook + ntfy support out of the box +- **Docker Ready**: Full docker-compose setup + +## Quick Start + +```bash +# Copy environment config +cp config/.env.example config/.env + +# Edit with your values +nano config/.env + +# Start everything +docker-compose up +``` + +## Project Structure + +``` +llm-bot-framework/ +├── api/ +│ ├── main.py # Flask app with auth routes +│ └── routes/ +│ └── example.py # Example route module +├── bot/ +│ ├── bot.py # Discord client +│ ├── command_registry.py # Module registration +│ └── commands/ +│ └── example.py # Example command module +├── core/ +│ ├── postgres.py # Generic PostgreSQL CRUD +│ ├── auth.py # JWT + bcrypt +│ ├── users.py # User management +│ └── notifications.py # Multi-channel notifications +├── ai/ +│ ├── parser.py # LLM JSON parser +│ └── ai_config.json # Model + prompts config +├── scheduler/ +│ └── daemon.py # Background polling +├── config/ +│ ├── schema.sql # Database schema +│ └── .env.example # Environment template +├── docker-compose.yml +├── Dockerfile +└── requirements.txt +``` + +## Creating a Domain Module + +### 1. Add Database Schema + +Edit `config/schema.sql`: + +```sql +CREATE TABLE IF NOT EXISTS tasks ( + id UUID PRIMARY KEY, + user_uuid UUID REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + completed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### 2. Create API Routes + +Create `api/routes/tasks.py`: + +```python +import flask +import sys, os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +import core.auth as auth +import core.postgres as postgres +import uuid + +def register(app): + @app.route('/api/tasks', methods=['GET']) + def api_listTasks(): + header = flask.request.headers.get('Authorization', '') + if not header.startswith('Bearer '): + return flask.jsonify({'error': 'missing token'}), 401 + token = header[7:] + + # Get user UUID from token + from core.auth import decodeJwtPayload + import json, base64 + payload = token.split('.')[1] + payload += '=' * (4 - len(payload) % 4) + decoded = json.loads(base64.urlsafe_b64decode(payload)) + user_uuid = decoded['sub'] + + tasks = postgres.select("tasks", {"user_uuid": user_uuid}) + return flask.jsonify(tasks), 200 + + @app.route('/api/tasks', methods=['POST']) + def api_addTask(): + header = flask.request.headers.get('Authorization', '') + if not header.startswith('Bearer '): + return flask.jsonify({'error': 'missing token'}), 401 + token = header[7:] + + data = flask.request.get_json() + task = postgres.insert("tasks", { + 'id': str(uuid.uuid4()), + 'user_uuid': data['user_uuid'], + 'name': data['name'], + }) + return flask.jsonify(task), 201 +``` + +Register it in `api/main.py`: + +```python +import api.routes.tasks as tasks_routes +register_routes(tasks_routes) +``` + +### 3. Create Bot Commands + +Create `bot/commands/tasks.py`: + +```python +import sys, os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from bot.command_registry import register_module +import ai.parser as ai_parser + +async def handle_task(message, session, parsed): + action = parsed.get('action') + token = session['token'] + user_uuid = session['user_uuid'] + + # Make API calls using the bot's apiRequest helper + from bot.bot import apiRequest + + if action == 'list': + result, status = apiRequest('get', f'/api/tasks', token) + if status == 200: + lines = [f"- {t['name']}" for t in result] + await message.channel.send("Your tasks:\n" + "\n".join(lines)) + else: + await message.channel.send("Failed to fetch tasks.") + + elif action == 'add': + task_name = parsed.get('task_name') + result, status = apiRequest('post', '/api/tasks', token, { + 'user_uuid': user_uuid, + 'name': task_name + }) + if status == 201: + await message.channel.send(f"Added task: **{task_name}**") + else: + await message.channel.send("Failed to add task.") + +def validate_task_json(data): + errors = [] + if 'action' not in data: + errors.append('Missing required field: action') + if data.get('action') == 'add' and 'task_name' not in data: + errors.append('Missing required field for add: task_name') + return errors + +register_module('task', handle_task) +ai_parser.register_validator('task', validate_task_json) +``` + +### 4. Add AI Prompts + +Edit `ai/ai_config.json`: + +```json +{ + "prompts": { + "command_parser": { + "system": "You are a helpful assistant...", + "user_template": "..." + }, + "task_parser": { + "system": "You parse task commands...", + "user_template": "Parse: \"{user_input}\"\n\nReturn JSON with action (list/add/complete) and task_name." + } + } +} +``` + +## AI Parser Usage + +```python +import ai.parser as ai_parser + +# Basic usage +parsed = ai_parser.parse(user_input, 'command_parser') + +# With conversation history +history = [("previous message", {"action": "add", "item": "test"})] +parsed = ai_parser.parse(user_input, 'command_parser', history=history) + +# Register custom validator +ai_parser.register_validator('task', validate_task_json) +``` + +## Notification Channels + +```python +import core.notifications as notif + +# Set user notification settings +notif.setNotificationSettings(user_uuid, { + 'discord_webhook': 'https://discord.com/api/webhooks/...', + 'discord_enabled': True, + 'ntfy_topic': 'my-alerts', + 'ntfy_enabled': True +}) + +# Send notification +settings = notif.getNotificationSettings(user_uuid) +notif._sendToEnabledChannels(settings, "Task due: Buy groceries") +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `DISCORD_BOT_TOKEN` | Discord bot token | +| `API_URL` | API URL (default: `http://app:5000`) | +| `DB_HOST` | PostgreSQL host | +| `DB_PORT` | PostgreSQL port | +| `DB_NAME` | Database name | +| `DB_USER` | Database user | +| `DB_PASS` | Database password | +| `JWT_SECRET` | JWT signing secret | +| `OPENROUTER_API_KEY` | OpenRouter API key | +| `OPENROUTER_BASE_URL` | OpenRouter base URL | +| `AI_CONFIG_PATH` | Path to ai_config.json | + +## License + +MIT