97a166f5aad667646b5446aac5183c74a6287e6a
- Add all 14 missing database tables (medications, med_logs, routines, etc.) - Rewrite medication scheduling: support specific days, every N days, as-needed (PRN) - Fix taken_times matching: match by created_at date, not scheduled_time string - Fix adherence calculation: taken / expected doses, not taken / (taken + skipped) - Add formatSchedule() helper for readable display - Update client types and API layer - Rename brilli-ins-client → synculous-client - Make client PWA: add manifest, service worker, icons - Bind dev server to 0.0.0.0 for network access - Fix SVG icon bugs in Icons.tsx - Add .dockerignore for client npm caching Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
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
# 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:
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:
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:
import api.routes.tasks as tasks_routes
register_routes(tasks_routes)
3. Create Bot Commands
Create bot/commands/tasks.py:
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:
{
"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
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
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
Description
Languages
Python
53.2%
TypeScript
45.5%
Mermaid
0.6%
JavaScript
0.4%
CSS
0.2%
Other
0.1%