128 lines
4.0 KiB
Python
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()
|