2026-02-12 22:11:52 -06:00
2026-02-12 23:07:48 -06:00
2026-02-12 23:07:48 -06:00
2026-02-12 23:07:48 -06:00
2026-02-12 23:07:48 -06:00
2026-02-12 22:11:52 -06:00
2026-02-12 23:07:48 -06:00
2026-02-12 22:11:52 -06:00
2026-02-12 22:11:52 -06:00
2026-02-12 22:11:52 -06:00
2026-02-12 22:11:52 -06:00
2026-02-12 23:07:48 -06:00
2026-02-12 22:11:52 -06:00

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
A total rewrite of synculous.
Readme 207 KiB
Languages
Python 53.2%
TypeScript 45.5%
Mermaid 0.6%
JavaScript 0.4%
CSS 0.2%
Other 0.1%