""" Client FastAPI application for voicebot. This module provides the FastAPI application for client mode operations. """ import asyncio import os import fcntl import sys from contextlib import asynccontextmanager from typing import Optional # Add the parent directory to sys.path to allow absolute imports sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapi import FastAPI from logger import logger # Import shared models from shared.models import ClientStatusResponse from voicebot.models import VoicebotArgs # Global client arguments storage _client_args: Optional[VoicebotArgs] = None def create_client_app(args: VoicebotArgs) -> FastAPI: """Create a FastAPI app for client mode that uvicorn can import.""" global _client_args _client_args = args # Store the client task globally so we can manage it client_task = None lock_file = None @asynccontextmanager async def lifespan(app: FastAPI): nonlocal client_task, lock_file # Startup # Use a file lock to prevent multiple instances from starting lock_file_path = "/tmp/voicebot_client.lock" try: lock_file = open(lock_file_path, 'w') # Try to acquire an exclusive lock (non-blocking) fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) if _client_args is None: logger.error("Client args not initialized") if lock_file: lock_file.close() lock_file = None yield return logger.info("Starting voicebot client...") # Import here to avoid circular imports from .client_main import main_with_args client_task = asyncio.create_task(main_with_args(_client_args)) except (IOError, OSError): # Another process already has the lock logger.info("Another instance is already running - skipping client startup") if lock_file: lock_file.close() lock_file = None yield # Shutdown if client_task and not client_task.done(): logger.info("Shutting down voicebot client...") client_task.cancel() try: await client_task except asyncio.CancelledError: pass if lock_file: try: fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) lock_file.close() os.unlink(lock_file_path) except Exception: pass # Create the client FastAPI app app = FastAPI(title="voicebot-client", lifespan=lifespan) @app.get("/health") async def health_check(): # type: ignore """Simple health check endpoint""" return {"status": "running", "mode": "client"} @app.get("/status", response_model=ClientStatusResponse) async def client_status() -> ClientStatusResponse: # type: ignore """Get client status""" return ClientStatusResponse( client_running=client_task is not None and not client_task.done(), session_name=_client_args.session_name if _client_args else 'unknown', lobby=_client_args.lobby if _client_args else 'unknown', server_url=_client_args.server_url if _client_args else 'unknown' ) return app def get_app() -> FastAPI: """Get the appropriate FastAPI app based on VOICEBOT_MODE environment variable.""" mode = os.getenv('VOICEBOT_MODE', 'provider') if mode == 'client': # For client mode, we need to create the client app with args from environment args = VoicebotArgs.from_environment() return create_client_app(args) else: # Provider mode - return the main bot orchestration app from voicebot.bot_orchestrator import app return app # Create app instance for uvicorn import uvicorn_app = get_app()