Implement reaction history context handling; enhance HTTP server initialization and logging; add static file serving

This commit is contained in:
pacnpal
2025-02-24 16:37:04 -05:00
parent 18c99a0434
commit c5bd2bf65f
7 changed files with 173 additions and 108 deletions

View File

@@ -25,8 +25,7 @@ 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
from .web.app import init_app, run_webserver
class DiscordBot:
@@ -49,93 +48,71 @@ class DiscordBot:
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."""
try:
async with self._init_lock:
if not self._initialized:
# Initialize database first
await self.db_manager.init_db()
logger.info("Database initialized")
try:
# Initialize database first
await self.db_manager.init_db()
logger.info("Database initialized")
# Initialize all handlers
self.message_handler = MessageHandler(self.db_manager)
logger.info("Message handler initialized")
self.image_handler = ImageHandler(self.api_manager)
logger.info("Image handler initialized")
self.tool_handler = ToolHandler(self.bot)
logger.info("Tool handler initialized")
self.event_handler = EventHandler(
self.bot,
self.queue_manager,
self.db_manager,
self.api_manager,
message_handler=self.message_handler,
image_handler=self.image_handler,
tool_handler=self.tool_handler
)
logger.info("Event handler initialized with all handlers")
# Initialize all handlers first
self.message_handler = MessageHandler(self.db_manager)
logger.info("Message handler initialized")
self.image_handler = ImageHandler(self.api_manager)
logger.info("Image handler initialized")
self.tool_handler = ToolHandler(self.bot)
logger.info("Tool handler initialized")
self.event_handler = EventHandler(
self.bot,
self.queue_manager,
self.db_manager,
self.api_manager,
message_handler=self.message_handler,
image_handler=self.image_handler,
tool_handler=self.tool_handler
)
logger.info("Event handler initialized with all handlers")
# Start API manager
if not self.api_manager.is_running:
await self.api_manager.start()
# Initialize and start web app
logger.info("Initializing web interface...")
app = init_app(self.event_handler)
web_port = 5000
asyncio.create_task(run_webserver(start_port=web_port))
logger.info("Web interface initialized (first available port in range 5000-5009 will be used)")
# Start API manager
if not self.api_manager.is_running:
await self.api_manager.start()
logger.info("Started API health check loop")
# Wait for API manager to be ready
await asyncio.sleep(1)
# Wait for API manager to be ready
await asyncio.sleep(1)
# Start queue manager with event handler's process message
if not self.queue_manager.is_running:
await self.queue_manager.start()
logger.info("Queue processor started")
# Start queue manager with event handler's process message
if not self.queue_manager.is_running:
await self.queue_manager.start()
logger.info("Queue processor started")
self._initialized = True
# Mark initialization complete after all services are started
self._initialized = True
logger.info("All services initialized successfully")
except Exception as e:
logger.error(f"Error during services initialization: {e}")
# Clean up any partially initialized services
await self.stop()
raise
except Exception as e:
logger.error(f"Failed to initialize services: {e}")
self._initialized = False
raise
async def _handle_prompt(self, request: web.Request) -> web.Response:
"""Handle incoming prompt requests from the web interface."""
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.json_response({"error": "Invalid API key"}, status=401)
# Parse request body
try:
body = await request.json()
except ValueError:
return web.json_response({"error": "Invalid JSON"}, status=400)
# Validate required fields
prompt = body.get('prompt')
if not prompt:
return web.json_response({"error": "Missing required field: prompt"}, status=400)
# Use provided channel_id or default
channel_id = body.get('channel_id', AUTO_RESPONSE_CHANNEL_ID)
# Have the event handler process the prompt
if self.event_handler:
await self.event_handler.send_prompt_to_channel(prompt, channel_id)
return web.json_response({"status": "processing"})
else:
return web.json_response({"error": "Event handler not initialized"}, status=503)
except Exception as e:
logger.error(f"Error handling prompt request: {e}")
return web.json_response({"error": str(e)}, status=500)
async def _handle_connection(self, token: str) -> None:
"""Handle bot connection with retries."""
retry_count = 0
@@ -278,10 +255,8 @@ class DiscordBot:
stop_tasks = []
if self.training_manager and self.training_manager.is_running:
stop_tasks.append(self.training_manager.stop())
if self.internal_runner and hasattr(self.internal_runner, 'cleanup'):
stop_tasks.append(self.internal_runner.cleanup())
if self.http_server:
stop_tasks.append(self.http_server.stop())
# Web app will be stopped when the event loop closes
logger.info("Web app will be stopped with event loop")
if self.db_pool:
stop_tasks.append(self.db_pool.close())

View File

@@ -4,6 +4,7 @@
import re
import uuid
import asyncio
import json
from typing import Dict, Any
from datetime import datetime
from discord import Message, RawReactionActionEvent
@@ -335,12 +336,16 @@ class EventHandler:
try:
# Start typing indicator
async with item.channel.typing():
# Get fresh conversation history first
history = await self.db_manager.get_conversation_history(
user_id=0,
channel_id=item.channel.id,
)
logger.debug(f"Retrieved {len(history)} messages for context")
# Use history from context if available (like for reactions), otherwise get fresh history
history = item.context.get("history")
if not history:
history = await self.db_manager.get_conversation_history(
user_id=0,
channel_id=item.channel.id,
)
logger.debug(f"Retrieved {len(history)} messages for context")
else:
logger.debug(f"Using {len(history)} messages from existing context")
# Generate message UUID upfront
message_uuid = str(uuid.uuid4())

View File

@@ -12,12 +12,46 @@ 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
def __init__(self, event_handler=None):
"""Initialize with optional event handler reference."""
self._event_handler = None
try:
self.app = web.Application()
logger.info("Created HTTP Application")
# API endpoints
self.app.router.add_post('/api/prompt', self.handle_prompt)
logger.info("Added /api/prompt endpoint")
# Web interface configuration
self.app.router.add_get('/', self.serve_web_interface)
self.app.router.add_static('/static', 'discord_glhf/web/static')
logger.info("Configured web interface routes")
self.runner: Optional[web.AppRunner] = None
logger.info("HTTP server initialized and ready to start")
# Set event handler after initialization
self.event_handler = event_handler
except Exception as e:
logger.error(f"Error initializing HTTP server: {e}")
raise
@property
def event_handler(self):
"""Get the event handler."""
return self._event_handler
@event_handler.setter
def event_handler(self, handler):
"""Set the event handler."""
self._event_handler = handler
if handler:
logger.info("Event handler updated")
async def serve_web_interface(self, request: web.Request) -> web.Response:
"""Serve the web interface HTML."""
try:
return web.FileResponse('discord_glhf/web/templates/index.html')
except FileNotFoundError:
return web.Response(text="Web interface not found", status=404)
async def handle_prompt(self, request: web.Request) -> web.Response:
"""Handle incoming prompt requests."""
@@ -73,11 +107,30 @@ class HTTPServer:
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}")
try:
logger.info(f"Starting HTTP server on {host}:{port}...")
# Create and setup the runner
self.runner = web.AppRunner(self.app)
logger.info("Created AppRunner")
await self.runner.setup()
logger.info("AppRunner setup complete")
# Create and start the site
site = web.TCPSite(self.runner, host, port)
logger.info("Created TCPSite")
await site.start()
logger.info(f"HTTP server is now listening on http://{host}:{port}")
# Test if the server is actually running by trying to bind
logger.info("HTTP server startup complete and ready to handle requests")
return True
except Exception as e:
logger.error(f"Failed to start HTTP server: {e}")
if self.runner:
await self.runner.cleanup()
raise
async def stop(self):
"""Stop the HTTP server."""

View File

@@ -3,7 +3,10 @@
from quart import Quart, render_template, request, jsonify
import os
import asyncio
from pathlib import Path
from functools import wraps
from ..config import logger
def async_route(f):
@wraps(f)
@@ -64,26 +67,40 @@ async def send_prompt():
except Exception as e:
return jsonify({'error': str(e)}), 500
def run_webserver(port=5000):
async def run_webserver(start_port=5000):
"""Run the web server."""
import hypercorn.asyncio
from hypercorn.config import Config
import socket
config = Config()
config.bind = [f"0.0.0.0:{port}"]
config.use_reloader = True
# Try ports in range start_port to start_port + 10
for port in range(start_port, start_port + 10):
config = Config()
config.bind = [f"0.0.0.0:{port}"]
config.use_reloader = True
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(hypercorn.asyncio.serve(app, config))
except KeyboardInterrupt:
pass
finally:
loop.close()
try:
# Test if port is available
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', port))
sock.close()
logger.info(f"Starting web interface at http://localhost:{port}")
try:
await hypercorn.asyncio.serve(app, config)
break
except KeyboardInterrupt:
logger.info("Web server shutdown requested")
break
except OSError:
if port == start_port + 9: # Last attempt
logger.error(f"Could not find an available port in range {start_port}-{port}")
raise
logger.warning(f"Port {port} is in use, trying next port...")
continue
if __name__ == "__main__":
from discord_glhf.config import AUTO_RESPONSE_CHANNEL_ID
import asyncio
from ..config import AUTO_RESPONSE_CHANNEL_ID
port = int(os.getenv('WEB_PORT', '8080'))
run_webserver(port)
asyncio.run(run_webserver(port))

View File

View File

View File

@@ -0,0 +1,15 @@
# Reaction History Fix
## Issue
When handling reactions, the conversation history is correctly retrieved during the initial reaction handling, but this history is not being used during message processing. Instead, fresh history is retrieved, which may not include the full context needed for properly understanding the reaction.
## Fix
Modified _process_message() to use the history provided in the context if available, otherwise fall back to getting fresh history. This ensures that reactions maintain their full conversation context throughout processing.
## Changes
- Check for history in the passed context first
- Only fetch fresh history if none exists in context
- This preserves the reaction context gathered during handle_reaction()
## Files Modified
- discord_glhf/handlers/event_handler.py