469 lines
19 KiB
Python
469 lines
19 KiB
Python
"""
|
|
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"""
|
|
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_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:
|
|
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_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")
|
|
|
|
@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")
|