Add web interface and main entry points for Discord bot; update dependencies and queue state

This commit is contained in:
pacnpal
2025-02-11 20:08:20 -05:00
parent dbe932c3ea
commit de1d40298b
22 changed files with 697 additions and 6 deletions

7
discord_glhf/__main__.py Normal file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env python3
"""Main entry point for the Discord bot."""
from .bot import run_bot
if __name__ == '__main__':
run_bot()

Binary file not shown.

Binary file not shown.

View File

@@ -25,6 +25,8 @@ from .queue_manager import QueueManager
from .api import APIManager
from .handlers import MessageHandler, ImageHandler, ToolHandler, EventHandler
from .training import TrainingManager
from .http_server import HTTPServer
from aiohttp import web
class DiscordBot:
@@ -46,6 +48,9 @@ class DiscordBot:
self.tool_handler = None
self.event_handler = None
self.training_manager = TrainingManager() # Initialize training manager
self.http_server = None
self.internal_app = web.Application()
self.internal_runner = None
async def _initialize_services(self) -> None:
"""Initialize API and queue services."""
@@ -144,6 +149,20 @@ class DiscordBot:
await self.training_manager.start()
logger.info("Training manager started")
# Set up internal API routes
self.internal_app.router.add_post('/api/prompt', self._handle_prompt)
self.internal_runner = web.AppRunner(self.internal_app)
await self.internal_runner.setup()
internal_site = web.TCPSite(self.internal_runner, 'localhost', int(os.getenv('HTTP_PORT', '8000')))
await internal_site.start()
logger.info("Internal API server started")
# Start HTTP server for backend prompts
http_port = int(os.getenv('WEB_PORT', '8080'))
self.http_server = HTTPServer(self.event_handler)
await self.http_server.start(port=http_port)
logger.info(f"Web server started on port {http_port}")
# Set bot status
activity = Game(name="with roller coasters")
await self.bot.change_presence(activity=activity)
@@ -211,6 +230,11 @@ class DiscordBot:
await self.training_manager.stop()
logger.info("Training manager stopped")
# Stop HTTP server
if self.http_server:
await self.http_server.stop()
logger.info("HTTP server stopped")
# Stop API manager
if self.api_manager and self.api_manager.is_running:
await self.api_manager.shutdown()

View File

@@ -532,6 +532,77 @@ class EventHandler:
},
)
async def send_prompt_to_channel(self, prompt: str, channel_id: int) -> None:
"""Send a prompt to the LLM and post response in Discord channel."""
try:
# Get the channel
channel = self.bot.get_channel(channel_id)
if not channel:
logger.error(f"Could not find channel {channel_id}")
return
# Build messages array with system prompt
messages = [
{
"role": "system",
"content": SYSTEM_PROMPT,
"metadata": {
"bot_owner_id": str(BOT_OWNER_ID),
"current_user": {
"user_id": "0" # System user
}
}
},
{
"role": "user",
"content": prompt,
"context": {
"timeout_env": "GLHF_TIMEOUT"
}
}
]
# Get response from API
response = await self.api_manager.get_completion(messages)
if not response:
logger.error("No response received from API")
return
# Parse tool calls and get processed response
tool_calls, final_response, mentioned_users = self.tool_handler.parse_tool_calls(
response, channel_id=channel.id
)
# Execute tool calls
for tool_name, args in tool_calls:
try:
if tool_name == "create_embed":
await self.tool_handler.create_embed(
channel=channel, content=args["content"]
)
elif tool_name == "create_thread":
await self.tool_handler.create_thread(
channel.id, args["name"]
)
except Exception as e:
logger.error(f"Error executing tool {tool_name}: {e}")
# Send the response
if final_response:
logger.info(f"Bot response to backend prompt: {final_response}")
await self.message_handler.safe_send(channel, final_response)
except Exception as e:
logger.error(f"Error processing backend prompt: {e}")
await self.report_error(
e,
{
"action": "send_prompt_to_channel",
"channel_id": channel_id,
"prompt": prompt,
},
)
async def store_message(
self,
user_id: int,

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""HTTP server for backend prompt handling."""
from aiohttp import web
import os
import json
import logging
from typing import Optional
from .config import logger, AUTO_RESPONSE_CHANNEL_ID
class HTTPServer:
"""HTTP server that accepts prompts from backend."""
def __init__(self, event_handler):
"""Initialize with event handler reference."""
self.event_handler = event_handler
self.app = web.Application()
self.app.router.add_post('/api/prompt', self.handle_prompt)
self.runner: Optional[web.AppRunner] = None
async def handle_prompt(self, request: web.Request) -> web.Response:
"""Handle incoming prompt requests."""
try:
# Validate API key if provided in environment
expected_key = os.getenv('BACKEND_API_KEY')
if expected_key:
provided_key = request.headers.get('X-API-Key')
if not provided_key or provided_key != expected_key:
return web.Response(
status=401,
text=json.dumps({"error": "Invalid API key"}),
content_type='application/json'
)
# Parse request body
try:
body = await request.json()
except json.JSONDecodeError:
return web.Response(
status=400,
text=json.dumps({"error": "Invalid JSON"}),
content_type='application/json'
)
# Validate required fields
prompt = body.get('prompt')
if not prompt:
return web.Response(
status=400,
text=json.dumps({"error": "Missing required field: prompt"}),
content_type='application/json'
)
# Use provided channel_id or default
channel_id = body.get('channel_id', AUTO_RESPONSE_CHANNEL_ID)
# Send prompt to channel
await self.event_handler.send_prompt_to_channel(prompt, channel_id)
return web.Response(
text=json.dumps({"status": "processing"}),
content_type='application/json'
)
except Exception as e:
logger.error(f"Error handling prompt request: {e}")
return web.Response(
status=500,
text=json.dumps({"error": str(e)}),
content_type='application/json'
)
async def start(self, host: str = '127.0.0.1', port: int = 8000):
"""Start the HTTP server."""
self.runner = web.AppRunner(self.app)
await self.runner.setup()
site = web.TCPSite(self.runner, host, port)
await site.start()
logger.info(f"HTTP server started on http://{host}:{port}")
async def stop(self):
"""Stop the HTTP server."""
if self.runner:
await self.runner.cleanup()
logger.info("HTTP server stopped")

31
discord_glhf/start.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Start both the Discord bot and web interface
# Function to stop background processes on exit
cleanup() {
echo "Stopping processes..."
kill $(jobs -p) 2>/dev/null
exit
}
# Set up trap for cleanup
trap cleanup SIGINT SIGTERM
# Export default ports if not set
export HTTP_PORT=${HTTP_PORT:-8000}
export WEB_PORT=${WEB_PORT:-8080}
# Start the Discord bot in background
echo "Starting Discord bot..."
python3 -m discord_glhf &
# Wait a moment for bot to initialize
sleep 2
# Start the web interface
echo "Starting web interface on port ${WEB_PORT}..."
cd $(dirname "$0")
PYTHONPATH=/Volumes/macminissd/Projects/discord_glhf /Users/talor/Projects/discord_glhf/.venv/bin/python3 web/app.py
# This will be caught by the trap
wait

View File

@@ -0,0 +1,3 @@
"""Web interface package for the Discord bot."""
from .app import run_webserver

View File

@@ -0,0 +1,6 @@
"""Main entry point for the web interface."""
from .app import run_webserver
if __name__ == '__main__':
run_webserver()

Binary file not shown.

Binary file not shown.

55
discord_glhf/web/app.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Web interface for sending prompts to the Discord bot."""
from flask import Flask, render_template, request, jsonify
import requests
import os
from pathlib import Path
app = Flask(__name__)
app.template_folder = str(Path(__file__).parent / 'templates')
# Get configuration from environment variables
API_PORT = int(os.getenv('HTTP_PORT', '8000'))
BACKEND_API_KEY = os.getenv('BACKEND_API_KEY')
@app.route('/')
def index():
"""Render the main interface."""
return render_template('index.html')
@app.route('/api/prompt', methods=['POST'])
def send_prompt():
"""Handle prompt submission."""
try:
data = request.get_json()
if not data or 'prompt' not in data:
return jsonify({'error': 'Missing prompt'}), 400
# Forward the request to the bot's HTTP server
headers = {'Content-Type': 'application/json'}
if BACKEND_API_KEY:
headers['X-API-Key'] = BACKEND_API_KEY
response = requests.post(
f'http://localhost:{API_PORT}/api/prompt',
json=data,
headers=headers
)
return response.json(), response.status_code
except Exception as e:
return jsonify({'error': str(e)}), 500
def run_webserver(port=5000):
"""Run the web server."""
app.run(host='0.0.0.0', port=port, debug=True)
def run():
"""Run the web server."""
port = int(os.getenv('WEB_PORT', '5000'))
app.run(host='0.0.0.0', port=port)
if __name__ == '__main__':
run()

View File

@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Discord Bot Prompt Interface</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold text-center mb-8 text-gray-800">Discord Bot Prompt Interface</h1>
<div class="bg-white rounded-lg shadow-md p-6">
<form id="promptForm" class="space-y-4">
<div>
<label for="prompt" class="block text-sm font-medium text-gray-700 mb-1">
Prompt
</label>
<textarea
id="prompt"
name="prompt"
rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter your prompt here..."
></textarea>
</div>
<div>
<label for="channelId" class="block text-sm font-medium text-gray-700 mb-1">
Channel ID (optional)
</label>
<input
type="text"
id="channelId"
name="channelId"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter channel ID..."
>
</div>
<div>
<button
type="submit"
class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
>
Send Prompt
</button>
</div>
</form>
<div id="result" class="mt-6 hidden">
<div class="p-4 rounded-md">
<p id="resultMessage" class="text-sm"></p>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('promptForm').addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = document.getElementById('prompt').value;
const channelId = document.getElementById('channelId').value;
const resultDiv = document.getElementById('result');
const resultMessage = document.getElementById('resultMessage');
try {
const response = await fetch('/api/prompt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: prompt,
...(channelId && { channel_id: parseInt(channelId) })
})
});
const data = await response.json();
resultDiv.className = 'mt-6 block';
if (response.ok) {
resultDiv.firstElementChild.className = 'p-4 rounded-md bg-green-100 text-green-700';
resultMessage.textContent = 'Prompt sent successfully! Check Discord for the response.';
} else {
resultDiv.firstElementChild.className = 'p-4 rounded-md bg-red-100 text-red-700';
resultMessage.textContent = `Error: ${data.error}`;
}
} catch (error) {
resultDiv.className = 'mt-6 block';
resultDiv.firstElementChild.className = 'p-4 rounded-md bg-red-100 text-red-700';
resultMessage.textContent = `Error: ${error.message}`;
}
});
</script>
</body>
</html>