""" 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, public_url: str = "/" ) -> APIRouter: """Create FastAPI router for bot configuration endpoints""" router = APIRouter( prefix=f"{public_url}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. This endpoint will query registered bot providers each time and request the bot's /config-schema endpoint without relying on any server-side cached schema. This ensures the UI always receives the up-to-date schema from the provider. """ try: providers_response = bot_manager.list_providers() schema = None 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: # Get the full provider object to access base_url full_provider = bot_manager.get_provider(provider.provider_id) if full_provider: # Force discovery from the provider on every request schema = await config_manager.discover_bot_config_schema( bot_name, full_provider.base_url, force_refresh=True ) if schema: break except Exception as e: logger.warning( f"Failed to check provider {provider.provider_id} for bot {bot_name}: {e}" ) 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("/schema/instance/{bot_instance_id}") async def get_bot_config_schema_by_instance(bot_instance_id: str) -> BotConfigSchema: # type: ignore """Get configuration schema for a specific bot instance""" try: # Get bot instance to find the bot_name bot_instance = await bot_manager.get_bot_instance(bot_instance_id) bot_name = bot_instance.bot_name # Always query the provider directly for the latest schema provider = bot_manager.get_provider(bot_instance.provider_id) if provider: try: schema = await config_manager.discover_bot_config_schema( bot_name, provider.base_url, force_refresh=True ) except Exception as e: logger.warning( f"Failed to discover schema for bot {bot_name} from provider {bot_instance.provider_id}: {e}" ) else: logger.warning(f"Provider {bot_instance.provider_id} not found") if not schema: raise HTTPException( status_code=404, detail=f"No configuration schema found for bot instance '{bot_instance_id}' (bot: '{bot_name}')", ) return schema except ValueError: # Bot instance not found raise HTTPException( status_code=404, detail=f"Bot instance '{bot_instance_id}' not found" ) except HTTPException: raise except Exception as e: logger.error(f"Failed to get bot config schema for instance {bot_instance_id}: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.post("/schema/instance/{bot_instance_id}/refresh") async def refresh_bot_schema_by_instance(bot_instance_id: str) -> Dict[str, Any]: """Refresh configuration schema for a specific bot instance""" try: # Get bot instance to find the bot_name and provider bot_instance = await bot_manager.get_bot_instance(bot_instance_id) bot_name = bot_instance.bot_name provider_id = bot_instance.provider_id # Get the provider to access base_url provider = bot_manager.get_provider(provider_id) if not provider: raise HTTPException( status_code=404, detail=f"Provider '{provider_id}' not found" ) # Force a fresh fetch from the provider schema = await config_manager.discover_bot_config_schema( bot_name, provider.base_url, force_refresh=True ) if schema: return { "success": True, "message": f"Schema refreshed for bot instance {bot_instance_id} (bot: {bot_name})", "schema": schema.model_dump(), } else: raise HTTPException( status_code=404, detail=f"Bot instance {bot_instance_id} (bot: {bot_name}) does not support configuration", ) except ValueError: # Bot instance not found raise HTTPException( status_code=404, detail=f"Bot instance '{bot_instance_id}' not found" ) except HTTPException: raise except Exception as e: logger.error(f"Failed to refresh schema for bot instance {bot_instance_id}: {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_instance_id}") async def get_lobby_bot_config( lobby_id: str, bot_instance_id: str ) -> BotLobbyConfig: """Get specific bot configuration for a lobby""" logger.info( f"Route handler called: get_lobby_bot_config({lobby_id}, {bot_instance_id})" ) try: # Get bot instance to find the bot_name bot_instance = await bot_manager.get_bot_instance(bot_instance_id) bot_name = bot_instance.bot_name config = config_manager.get_lobby_bot_config(lobby_id, bot_name) if not config: logger.warning( f"No configuration found for bot instance '{bot_instance_id}' (bot: '{bot_name}') in lobby '{lobby_id}'" ) raise HTTPException( status_code=404, detail=f"No configuration found for bot instance '{bot_instance_id}' in lobby '{lobby_id}'", ) return config except ValueError: # Bot instance not found raise HTTPException( status_code=404, detail=f"Bot instance '{bot_instance_id}' not found" ) except HTTPException: raise except Exception as e: logger.error( f"Failed to get config for bot instance {bot_instance_id} 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""" logger.info( f"Route handler called: update_bot_config({request.bot_instance_id}, {request.lobby_id})" ) try: # Get bot instance information bot_instance = await bot_manager.get_bot_instance(request.bot_instance_id) bot_name = bot_instance.bot_name provider_id = bot_instance.provider_id # Find the provider URL provider = bot_manager.get_provider(provider_id) if not provider: raise HTTPException( status_code=404, detail=f"Provider '{provider_id}' not found" ) provider_url = provider.base_url # Update configuration config = config_manager.set_bot_config( lobby_id=request.lobby_id, bot_name=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, bot_name, request.lobby_id, config, ) return BotConfigUpdateResponse( success=True, message="Configuration updated successfully", updated_config=config ) except ValueError as e: # Bot instance not found return BotConfigUpdateResponse( success=False, message=f"Bot instance not found: {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_instance_id}") async def delete_bot_config(lobby_id: str, bot_instance_id: str) -> Dict[str, Any]: """Delete bot configuration for a lobby""" try: # Get bot instance to find the bot_name bot_instance = await bot_manager.get_bot_instance(bot_instance_id) bot_name = bot_instance.bot_name 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 instance '{bot_instance_id}' in lobby '{lobby_id}'", ) return {"success": True, "message": "Configuration deleted successfully"} except ValueError: # Bot instance not found raise HTTPException( status_code=404, detail=f"Bot instance '{bot_instance_id}' not found" ) 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: # Get the full provider object to access base_url full_provider = bot_manager.get_provider(provider.provider_id) if full_provider: schema = ( await config_manager.discover_bot_config_schema( bot.name, full_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_public in providers_response.providers: try: # Get the full provider object to access base_url provider = bot_manager.get_provider(provider_public.provider_id) if not provider: continue 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_public.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") # NOTE: schema cache management endpoints removed. Server no longer # stores or serves cached bot config schemas; callers should fetch # live schemas from providers via the /schema endpoints. 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")