252 lines
7.0 KiB
Markdown
252 lines
7.0 KiB
Markdown
# 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
|