""" Bot Configuration API This module provides REST API endpoints for managing bot configurations including schema discovery, configuration CRUD operations, and real-time updates. """ from typing import Dict, Any from fastapi import APIRouter, HTTPException, BackgroundTasks, WebSocket from core.bot_manager import BotManager from shared.logger import logger from core.bot_config_manager import BotConfigManager # Import WebSocket handler base class try: from websocket.message_handlers import MessageHandler except ImportError: from ..websocket.message_handlers import MessageHandler # Import shared models with fallback handling try: from ...shared.models import ( BotConfigSchema, BotLobbyConfig, BotConfigUpdateRequest, BotConfigUpdateResponse, BotConfigListResponse ) except ImportError: try: # Try direct import (when PYTHONPATH is set) from shared.models import ( BotConfigSchema, BotLobbyConfig, BotConfigUpdateRequest, BotConfigUpdateResponse, BotConfigListResponse, ) except ImportError: # Log a warning for debugging (optional) import warnings warnings.warn( "Relative import failed, ensure PYTHONPATH includes project root or run as package" ) # Rely on environment setup or raise a clear error raise ImportError( "Cannot import shared.models. Ensure the project is run as a package or PYTHONPATH is set." ) def create_bot_config_router( config_manager: BotConfigManager, bot_manager: BotManager ) -> APIRouter: """Create FastAPI router for bot configuration endpoints""" router = APIRouter(prefix="/api/bots/config", tags=["Bot Configuration"]) @router.get("/schema/{bot_name}") async def get_bot_config_schema(bot_name: str) -> BotConfigSchema: # type: ignore """Get configuration schema for a specific bot""" try: # Check if we have cached schema schema = config_manager.get_bot_config_schema(bot_name) if not schema: # Try to discover schema from bot provider providers_response = bot_manager.list_providers() for provider in providers_response.providers: try: # Check if this provider has the bot provider_bots = await bot_manager.get_provider_bots( provider.provider_id ) bot_names = [bot.name for bot in provider_bots.bots] if bot_name in bot_names: schema = await config_manager.discover_bot_config_schema( bot_name, provider.base_url ) if schema: break except Exception as e: logger.warning( f"Failed to check provider {provider.provider_id} for bot {bot_name}: {e}" ) continue else: # We have a cached schema, but check if it might be stale # Try to refresh it automatically if it's older than 1 hour providers_response = bot_manager.list_providers() for provider in providers_response.providers: try: provider_bots = await bot_manager.get_provider_bots( provider.provider_id ) bot_names = [bot.name for bot in provider_bots.bots] if bot_name in bot_names: # This will only refresh if the cached schema is older than 1 hour fresh_schema = ( await config_manager.discover_bot_config_schema( bot_name, provider.base_url, force_refresh=False ) ) if fresh_schema: schema = fresh_schema break except Exception as e: logger.debug(f"Failed to refresh schema for {bot_name}: {e}") # Continue with cached schema if refresh fails continue if not schema: raise HTTPException( status_code=404, detail=f"No configuration schema found for bot '{bot_name}'", ) return schema except HTTPException: raise except Exception as e: logger.error(f"Failed to get bot config schema for {bot_name}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/lobby/{lobby_id}") async def get_lobby_bot_configs(lobby_id: str) -> BotConfigListResponse: """Get all bot configurations for a lobby""" try: configs = config_manager.get_lobby_configs(lobby_id) return BotConfigListResponse(lobby_id=lobby_id, configs=configs) except Exception as e: logger.error(f"Failed to get lobby configs for {lobby_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/lobby/{lobby_id}/bot/{bot_name}") async def get_lobby_bot_config(lobby_id: str, bot_name: str) -> BotLobbyConfig: """Get specific bot configuration for a lobby""" try: config = config_manager.get_lobby_bot_config(lobby_id, bot_name) if not config: raise HTTPException( status_code=404, detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'", ) return config except HTTPException: raise except Exception as e: logger.error( f"Failed to get config for bot {bot_name} in lobby {lobby_id}: {e}" ) raise HTTPException(status_code=500, detail="Internal server error") @router.post("/update") async def update_bot_config( request: BotConfigUpdateRequest, background_tasks: BackgroundTasks, session_id: str = "unknown", # TODO: Get from auth/session context ) -> BotConfigUpdateResponse: """Update bot configuration for a lobby""" try: # Find the provider for this bot provider_id = None provider_url = None providers_response = bot_manager.list_providers() for provider in providers_response.providers: try: provider_bots = await bot_manager.get_provider_bots( provider.provider_id ) bot_names = [bot.name for bot in provider_bots.bots] if request.bot_name in bot_names: provider_id = provider.provider_id provider_url = provider.base_url break except Exception: continue if not provider_id: raise HTTPException( status_code=404, detail=f"Bot '{request.bot_name}' not found in any provider" ) # Update configuration if not provider_id or not provider_url: raise HTTPException( status_code=404, detail=f"Bot {request.bot_name} not found in any registered provider", ) config = config_manager.set_bot_config( lobby_id=request.lobby_id, bot_name=request.bot_name, provider_id=provider_id, config_values=request.config_values, session_id=session_id ) # Notify bot provider in background background_tasks.add_task( config_manager.notify_bot_config_change, provider_url, request.bot_name, request.lobby_id, config ) return BotConfigUpdateResponse( success=True, message="Configuration updated successfully", updated_config=config ) except ValueError as e: # Validation error return BotConfigUpdateResponse( success=False, message=f"Validation error: {str(e)}" ) except HTTPException: raise except Exception as e: logger.error(f"Failed to update bot config: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/lobby/{lobby_id}/bot/{bot_name}") async def delete_bot_config(lobby_id: str, bot_name: str) -> Dict[str, Any]: """Delete bot configuration for a lobby""" try: success = config_manager.delete_bot_config(lobby_id, bot_name) if not success: raise HTTPException( status_code=404, detail=f"No configuration found for bot '{bot_name}' in lobby '{lobby_id}'" ) return {"success": True, "message": "Configuration deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Failed to delete bot config: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/lobby/{lobby_id}") async def delete_lobby_configs(lobby_id: str) -> Dict[str, Any]: """Delete all bot configurations for a lobby""" try: success = config_manager.delete_lobby_configs(lobby_id) return { "success": success, "message": "All lobby configurations deleted" if success else "No configurations found" } except Exception as e: logger.error(f"Failed to delete lobby configs: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/statistics") async def get_config_statistics() -> Dict[str, Any]: """Get configuration manager statistics""" try: return config_manager.get_statistics() except Exception as e: logger.error(f"Failed to get config statistics: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/refresh-schemas") async def refresh_bot_schemas(background_tasks: BackgroundTasks) -> Dict[str, Any]: """Refresh all bot configuration schemas from providers""" try: async def refresh_task(): refreshed = 0 providers_response = bot_manager.list_providers() for provider in providers_response.providers: try: provider_bots = await bot_manager.get_provider_bots( provider.provider_id ) for bot in provider_bots.bots: try: schema = ( await config_manager.discover_bot_config_schema( bot.name, provider.base_url, force_refresh=True ) ) if schema: refreshed += 1 except Exception as e: logger.warning(f"Failed to refresh schema for {bot.name}: {e}") except Exception as e: logger.warning( f"Failed to refresh schemas from provider {provider.provider_id}: {e}" ) logger.info(f"Refreshed {refreshed} bot configuration schemas") background_tasks.add_task(refresh_task) return { "success": True, "message": "Schema refresh started in background" } except Exception as e: logger.error(f"Failed to start schema refresh: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/schema/{bot_name}/refresh") async def refresh_bot_schema(bot_name: str) -> Dict[str, Any]: """Refresh configuration schema for a specific bot""" try: # Find the provider for this bot providers_response = bot_manager.list_providers() for provider in providers_response.providers: try: provider_bots = await bot_manager.get_provider_bots( provider.provider_id ) for bot in provider_bots.bots: if bot.name == bot_name: schema = await config_manager.refresh_bot_schema( bot_name, provider.base_url ) if schema: return { "success": True, "message": f"Schema refreshed for bot {bot_name}", "schema": schema.model_dump(), } else: raise HTTPException( status_code=404, detail=f"Bot {bot_name} does not support configuration", ) except Exception as e: logger.warning( f"Failed to check provider {provider.provider_id}: {e}" ) continue raise HTTPException(status_code=404, detail=f"Bot {bot_name} not found") except HTTPException: raise except Exception as e: logger.error(f"Failed to refresh schema for bot {bot_name}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/schema/{bot_name}/cache") async def clear_bot_schema_cache(bot_name: str) -> Dict[str, Any]: """Clear cached schema for a specific bot""" try: success = config_manager.clear_bot_schema_cache(bot_name) if success: return { "success": True, "message": f"Schema cache cleared for bot {bot_name}", } else: raise HTTPException( status_code=404, detail=f"No cached schema found for bot {bot_name}" ) except HTTPException: raise except Exception as e: logger.error(f"Failed to clear schema cache for bot {bot_name}: {e}") raise HTTPException(status_code=500, detail="Internal server error") return router class BotConfigUpdateHandler(MessageHandler): """WebSocket handler for real-time bot configuration updates""" def __init__(self, config_manager: BotConfigManager): self.config_manager = config_manager async def handle(self, session, lobby, data: Dict[str, Any], websocket: WebSocket, managers: Dict[str, Any]): """Handle real-time bot configuration updates via WebSocket""" try: # Extract update data lobby_id = lobby.lobby_id if lobby else data.get("lobby_id") bot_name = data.get("bot_name") config_values = data.get("config_values") session_id = session.session_id if session else "unknown" if not all([lobby_id, bot_name, config_values]): await websocket.send_json({ "type": "bot_config_error", "error": "Missing required fields: lobby_id, bot_name, config_values" }) return # Update configuration (this will validate the values) config = self.config_manager.set_bot_config( lobby_id=lobby_id, bot_name=bot_name, provider_id="", # Will be resolved config_values=config_values, session_id=session_id ) # Send success response await websocket.send_json({ "type": "bot_config_updated", "config": { "bot_name": config.bot_name, "lobby_id": config.lobby_id, "config_values": config.config_values, "updated_at": config.updated_at } }) logger.info(f"Bot configuration updated via WebSocket: {bot_name} in lobby {lobby_id}") except Exception as e: error_msg = f"Error updating bot configuration: {str(e)}" logger.error(error_msg) await websocket.send_json({ "type": "bot_config_error", "error": error_msg }) def setup_websocket_config_handlers(websocket_manager, config_manager: BotConfigManager): """Setup WebSocket handlers for real-time configuration updates""" # Register the bot configuration update handler config_handler = BotConfigUpdateHandler(config_manager) websocket_manager.message_router.register("bot_config_update", config_handler) logger.info("Bot configuration WebSocket handlers registered")