ai-voicebot/voicebot/client_app.py

128 lines
4.0 KiB
Python

"""
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()