# 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