Spaces:
Paused
Paused
| """ | |
| Flare β Main Application (Refactored with Event-Driven Architecture) | |
| ==================================================================== | |
| """ | |
| # FastAPI imports | |
| from fastapi import FastAPI, WebSocket, Request, status | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.encoders import jsonable_encoder | |
| # Standard library | |
| import uvicorn | |
| import os | |
| from pathlib import Path | |
| import mimetypes | |
| import uuid | |
| import traceback | |
| from datetime import datetime | |
| import asyncio | |
| import time | |
| from pydantic import ValidationError | |
| from dotenv import load_dotenv | |
| # Event-driven architecture imports | |
| from event_bus import event_bus | |
| from state_orchestrator import StateOrchestrator | |
| from websocket_manager import WebSocketManager | |
| from resource_manager import ResourceManager | |
| from stt_lifecycle_manager import STTLifecycleManager | |
| from tts_lifecycle_manager import TTSLifecycleManager | |
| from llm_manager import LLMManager | |
| from audio_buffer_manager import AudioBufferManager | |
| # Project imports | |
| from routes.admin_routes import router as admin_router, start_cleanup_task | |
| from llm.llm_startup import run_in_thread | |
| from session import session_store, start_session_cleanup | |
| from config.config_provider import ConfigProvider | |
| # Logger imports | |
| from utils.logger import log_error, log_info, log_warning | |
| # Exception imports | |
| from utils.exceptions import ( | |
| DuplicateResourceError, | |
| RaceConditionError, | |
| ValidationError as FlareValidationError, | |
| ResourceNotFoundError, | |
| AuthenticationError, | |
| AuthorizationError, | |
| ConfigurationError, | |
| get_http_status_code, | |
| FlareException | |
| ) | |
| # Load .env file if exists | |
| load_dotenv() | |
| ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:4200").split(",") | |
| # ===================== Environment Setup ===================== | |
| def setup_environment(): | |
| """Setup environment based on deployment mode""" | |
| cfg = ConfigProvider.get() | |
| log_info("=" * 60) | |
| log_info("π Flare Starting", version="2.0.0") | |
| log_info(f"π LLM Provider: {cfg.global_config.llm_provider.name}") | |
| log_info(f"π€ TTS Provider: {cfg.global_config.tts_provider.name}") | |
| log_info(f"π§ STT Provider: {cfg.global_config.stt_provider.name}") | |
| log_info("=" * 60) | |
| if cfg.global_config.is_cloud_mode(): | |
| log_info("βοΈ Cloud Mode: Using HuggingFace Secrets") | |
| log_info("π Required secrets: JWT_SECRET, FLARE_TOKEN_KEY") | |
| # Check for provider-specific tokens | |
| llm_config = cfg.global_config.get_provider_config("llm", cfg.global_config.llm_provider.name) | |
| if llm_config and llm_config.requires_repo_info: | |
| log_info("π LLM requires SPARK_TOKEN for repository operations") | |
| else: | |
| log_info("π’ On-Premise Mode: Using .env file") | |
| if not Path(".env").exists(): | |
| log_warning("β οΈ WARNING: .env file not found!") | |
| log_info("π Copy .env.example to .env and configure it") | |
| # Run setup | |
| setup_environment() | |
| # Fix MIME types for JavaScript files | |
| mimetypes.add_type("application/javascript", ".js") | |
| mimetypes.add_type("text/css", ".css") | |
| app = FastAPI( | |
| title="Flare Orchestration Service", | |
| version="2.0.0", | |
| description="LLM-driven intent & API flow engine with multi-provider support", | |
| ) | |
| # CORS for development | |
| if os.getenv("ENVIRONMENT", "development") == "development": | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=ALLOWED_ORIGINS, | |
| allow_credentials=True, | |
| allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], | |
| allow_headers=["*"], | |
| max_age=3600, | |
| expose_headers=["X-Request-ID"] | |
| ) | |
| log_info(f"π§ CORS enabled for origins: {ALLOWED_ORIGINS}") | |
| # Request ID middleware | |
| async def add_request_id(request: Request, call_next): | |
| """Add request ID for tracking""" | |
| request_id = str(uuid.uuid4()) | |
| request.state.request_id = request_id | |
| # Log request start | |
| log_info( | |
| "Request started", | |
| request_id=request_id, | |
| method=request.method, | |
| path=request.url.path, | |
| client=request.client.host if request.client else "unknown" | |
| ) | |
| try: | |
| response = await call_next(request) | |
| # Add request ID to response headers | |
| response.headers["X-Request-ID"] = request_id | |
| # Log request completion | |
| log_info( | |
| "Request completed", | |
| request_id=request_id, | |
| status_code=response.status_code, | |
| method=request.method, | |
| path=request.url.path | |
| ) | |
| return response | |
| except Exception as e: | |
| log_error( | |
| "Request failed", | |
| request_id=request_id, | |
| error=str(e), | |
| traceback=traceback.format_exc() | |
| ) | |
| raise | |
| # ===================== Event-Driven Architecture Initialization ===================== | |
| async def startup_event(): | |
| """Initialize event-driven components on startup""" | |
| try: | |
| # Initialize event bus | |
| await event_bus.start() | |
| log_info("β Event bus started") | |
| # Initialize resource manager | |
| resource_manager = ResourceManager(event_bus) | |
| await resource_manager.start() | |
| log_info("β Resource manager started") | |
| # Initialize managers | |
| state_orchestrator = StateOrchestrator(event_bus) | |
| websocket_manager = WebSocketManager(event_bus) | |
| audio_buffer_manager = AudioBufferManager(event_bus) | |
| stt_manager = STTLifecycleManager(event_bus, resource_manager) | |
| tts_manager = TTSLifecycleManager(event_bus, resource_manager) | |
| llm_manager = LLMManager(event_bus, resource_manager) | |
| # Store in app state for access in routes | |
| app.state.event_bus = event_bus | |
| app.state.resource_manager = resource_manager | |
| app.state.state_orchestrator = state_orchestrator | |
| app.state.websocket_manager = websocket_manager | |
| app.state.audio_buffer_manager = audio_buffer_manager | |
| app.state.stt_manager = stt_manager | |
| app.state.tts_manager = tts_manager | |
| app.state.llm_manager = llm_manager | |
| log_info("β All managers initialized") | |
| # Start existing background tasks | |
| run_in_thread() # Start LLM startup notifier if needed | |
| start_cleanup_task() # Activity log cleanup | |
| start_session_cleanup() # Session cleanup | |
| log_info("β Background tasks started") | |
| except Exception as e: | |
| log_error("β Failed to start event-driven components", error=str(e), traceback=traceback.format_exc()) | |
| raise | |
| async def shutdown_event(): | |
| """Cleanup event-driven components on shutdown""" | |
| try: | |
| # Stop event bus | |
| await event_bus.stop() | |
| log_info("β Event bus stopped") | |
| # Stop resource manager | |
| if hasattr(app.state, 'resource_manager'): | |
| await app.state.resource_manager.stop() | |
| log_info("β Resource manager stopped") | |
| # Close all WebSocket connections | |
| if hasattr(app.state, 'websocket_manager'): | |
| await app.state.websocket_manager.close_all_connections() | |
| log_info("β All WebSocket connections closed") | |
| except Exception as e: | |
| log_error("β Error during shutdown", error=str(e)) | |
| # ---------------- Core chat/session routes -------------------------- | |
| from routes.chat_handler import router as chat_router | |
| app.include_router(chat_router, prefix="/api") | |
| # ---------------- Audio (TTS/STT) routes ------------------------------ | |
| from routes.audio_routes import router as audio_router | |
| app.include_router(audio_router, prefix="/api") | |
| # ---------------- Admin API routes ---------------------------------- | |
| app.include_router(admin_router, prefix="/api/admin") | |
| # ---------------- WebSocket route for real-time chat ------------------ | |
| async def websocket_route(websocket: WebSocket, session_id: str): | |
| """Handle WebSocket connections using the new WebSocketManager""" | |
| if hasattr(app.state, 'websocket_manager'): | |
| await app.state.websocket_manager.handle_connection(websocket, session_id) | |
| else: | |
| log_error("WebSocketManager not initialized") | |
| await websocket.close(code=1011, reason="Server not ready") | |
| # ---------------- Test endpoint for event-driven flow ------------------ | |
| async def test_realtime(): | |
| """Test endpoint for event-driven realtime flow""" | |
| from event_bus import Event, EventType | |
| try: | |
| # Create a test session | |
| session = session_store.create_session( | |
| project_name="kronos_jet", | |
| version_no=1, | |
| is_realtime=True | |
| ) | |
| # Get version config | |
| cfg = ConfigProvider.get() | |
| project = next((p for p in cfg.projects if p.name == "kronos_jet"), None) | |
| if project: | |
| version = next((v for v in project.versions if v.no == 1), None) | |
| if version: | |
| session.set_version_config(version) | |
| # Publish session started event | |
| await app.state.event_bus.publish(Event( | |
| type=EventType.SESSION_STARTED, | |
| session_id=session.session_id, | |
| data={ | |
| "session": session, | |
| "has_welcome": bool(version and version.welcome_prompt), | |
| "welcome_text": version.welcome_prompt if version and version.welcome_prompt else "HoΕ geldiniz!" | |
| } | |
| )) | |
| return { | |
| "session_id": session.session_id, | |
| "message": "Test session created. Connect via WebSocket to continue." | |
| } | |
| except Exception as e: | |
| log_error("Test endpoint error", error=str(e)) | |
| raise HTTPException(500, f"Test failed: {str(e)}") | |
| # ---------------- Exception Handlers ---------------------------------- | |
| async def global_exception_handler(request: Request, exc: Exception): | |
| """Handle all unhandled exceptions""" | |
| request_id = getattr(request.state, 'request_id', 'unknown') | |
| # Log the full exception | |
| log_error( | |
| "Unhandled exception", | |
| request_id=request_id, | |
| endpoint=str(request.url), | |
| method=request.method, | |
| error=str(exc), | |
| error_type=type(exc).__name__, | |
| traceback=traceback.format_exc() | |
| ) | |
| # Special handling for FlareExceptions | |
| if isinstance(exc, FlareException): | |
| status_code = get_http_status_code(exc) | |
| response_body = { | |
| "error": type(exc).__name__, | |
| "message": str(exc), | |
| "request_id": request_id, | |
| "timestamp": datetime.utcnow().isoformat(), | |
| "details": getattr(exc, 'details', {}) | |
| } | |
| # Special message for race conditions | |
| if isinstance(exc, RaceConditionError): | |
| response_body["user_action"] = "Please reload the data and try again" | |
| return JSONResponse( | |
| status_code=status_code, | |
| content=jsonable_encoder(response_body) | |
| ) | |
| # Generic error response | |
| return JSONResponse( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| content=jsonable_encoder({ | |
| "error": "InternalServerError", | |
| "message": "An unexpected error occurred. Please try again later.", | |
| "request_id": request_id, | |
| "timestamp": datetime.utcnow().isoformat() | |
| }) | |
| ) | |
| # Add custom exception handlers | |
| async def duplicate_resource_handler(request: Request, exc: DuplicateResourceError): | |
| """Handle duplicate resource errors""" | |
| return JSONResponse( | |
| status_code=409, | |
| content={ | |
| "detail": str(exc), | |
| "error_type": "duplicate_resource", | |
| "resource_type": exc.details.get("resource_type"), | |
| "identifier": exc.details.get("identifier") | |
| } | |
| ) | |
| async def race_condition_handler(request: Request, exc: RaceConditionError): | |
| """Handle race condition errors""" | |
| return JSONResponse( | |
| status_code=409, | |
| content=exc.to_http_detail() | |
| ) | |
| async def validation_error_handler(request: Request, exc: FlareValidationError): | |
| """Handle validation errors""" | |
| return JSONResponse( | |
| status_code=422, | |
| content={ | |
| "detail": str(exc), | |
| "error_type": "validation_error", | |
| "details": exc.details | |
| } | |
| ) | |
| async def resource_not_found_handler(request: Request, exc: ResourceNotFoundError): | |
| """Handle resource not found errors""" | |
| return JSONResponse( | |
| status_code=404, | |
| content={ | |
| "detail": str(exc), | |
| "error_type": "resource_not_found", | |
| "resource_type": exc.details.get("resource_type"), | |
| "identifier": exc.details.get("identifier") | |
| } | |
| ) | |
| async def authentication_error_handler(request: Request, exc: AuthenticationError): | |
| """Handle authentication errors""" | |
| return JSONResponse( | |
| status_code=401, | |
| content={ | |
| "detail": str(exc), | |
| "error_type": "authentication_error" | |
| } | |
| ) | |
| async def authorization_error_handler(request: Request, exc: AuthorizationError): | |
| """Handle authorization errors""" | |
| return JSONResponse( | |
| status_code=403, | |
| content={ | |
| "detail": str(exc), | |
| "error_type": "authorization_error" | |
| } | |
| ) | |
| async def configuration_error_handler(request: Request, exc: ConfigurationError): | |
| """Handle configuration errors""" | |
| return JSONResponse( | |
| status_code=500, | |
| content={ | |
| "detail": str(exc), | |
| "error_type": "configuration_error", | |
| "config_key": exc.details.get("config_key") | |
| } | |
| ) | |
| # ---------------- Metrics endpoint ----------------- | |
| async def get_metrics(): | |
| """Get system metrics including event-driven components""" | |
| import psutil | |
| import gc | |
| # Memory info | |
| process = psutil.Process() | |
| memory_info = process.memory_info() | |
| # Session stats | |
| session_stats = session_store.get_session_stats() | |
| # Event-driven component stats | |
| event_stats = {} | |
| if hasattr(app.state, 'stt_manager'): | |
| event_stats['stt'] = app.state.stt_manager.get_stats() | |
| if hasattr(app.state, 'tts_manager'): | |
| event_stats['tts'] = app.state.tts_manager.get_stats() | |
| if hasattr(app.state, 'llm_manager'): | |
| event_stats['llm'] = app.state.llm_manager.get_stats() | |
| if hasattr(app.state, 'websocket_manager'): | |
| event_stats['websocket'] = { | |
| 'active_connections': app.state.websocket_manager.get_connection_count() | |
| } | |
| if hasattr(app.state, 'resource_manager'): | |
| event_stats['resources'] = app.state.resource_manager.get_stats() | |
| if hasattr(app.state, 'audio_buffer_manager'): | |
| event_stats['audio_buffers'] = app.state.audio_buffer_manager.get_all_stats() | |
| metrics = { | |
| "memory": { | |
| "rss_mb": memory_info.rss / 1024 / 1024, | |
| "vms_mb": memory_info.vms / 1024 / 1024, | |
| "percent": process.memory_percent() | |
| }, | |
| "cpu": { | |
| "percent": process.cpu_percent(interval=0.1), | |
| "num_threads": process.num_threads() | |
| }, | |
| "sessions": session_stats, | |
| "event_driven_components": event_stats, | |
| "gc": { | |
| "collections": gc.get_count(), | |
| "objects": len(gc.get_objects()) | |
| }, | |
| "uptime_seconds": time.time() - process.create_time() | |
| } | |
| return metrics | |
| # ---------------- Health probe (HF Spaces watchdog) ----------------- | |
| def health_check(): | |
| """Health check endpoint - moved to /api/health""" | |
| # Check if event-driven components are healthy | |
| event_bus_healthy = hasattr(app.state, 'event_bus') and app.state.event_bus._running | |
| return { | |
| "status": "ok" if event_bus_healthy else "degraded", | |
| "version": "2.0.0", | |
| "timestamp": datetime.utcnow().isoformat(), | |
| "environment": os.getenv("ENVIRONMENT", "development"), | |
| "event_driven": { | |
| "event_bus": "running" if event_bus_healthy else "not_running", | |
| "managers": { | |
| "state_orchestrator": "initialized" if hasattr(app.state, 'state_orchestrator') else "not_initialized", | |
| "websocket_manager": "initialized" if hasattr(app.state, 'websocket_manager') else "not_initialized", | |
| "stt_manager": "initialized" if hasattr(app.state, 'stt_manager') else "not_initialized", | |
| "tts_manager": "initialized" if hasattr(app.state, 'tts_manager') else "not_initialized", | |
| "llm_manager": "initialized" if hasattr(app.state, 'llm_manager') else "not_initialized" | |
| } | |
| } | |
| } | |
| # ---------------- Serve static files ------------------------------------ | |
| # UI static files (production build) | |
| static_path = Path(__file__).parent / "static" | |
| if static_path.exists(): | |
| app.mount("/static", StaticFiles(directory=str(static_path)), name="static") | |
| # Serve index.html for all non-API routes (SPA support) | |
| async def serve_index(): | |
| """Serve Angular app""" | |
| index_path = static_path / "index.html" | |
| if index_path.exists(): | |
| return FileResponse(str(index_path)) | |
| else: | |
| return JSONResponse( | |
| status_code=404, | |
| content={"error": "UI not found. Please build the Angular app first."} | |
| ) | |
| # Catch-all route for SPA | |
| async def serve_spa(full_path: str): | |
| """Serve Angular app for all routes""" | |
| # Skip API routes | |
| if full_path.startswith("api/"): | |
| return JSONResponse(status_code=404, content={"error": "Not found"}) | |
| # Serve static files | |
| file_path = static_path / full_path | |
| if file_path.exists() and file_path.is_file(): | |
| return FileResponse(str(file_path)) | |
| # Fallback to index.html for SPA routing | |
| index_path = static_path / "index.html" | |
| if index_path.exists(): | |
| return FileResponse(str(index_path)) | |
| return JSONResponse(status_code=404, content={"error": "Not found"}) | |
| else: | |
| log_warning(f"β οΈ Static files directory not found at {static_path}") | |
| log_warning(" Run 'npm run build' in flare-ui directory to build the UI") | |
| async def no_ui(): | |
| """No UI available""" | |
| return JSONResponse( | |
| status_code=503, | |
| content={ | |
| "error": "UI not available", | |
| "message": "Please build the Angular UI first. Run: cd flare-ui && npm run build", | |
| "api_docs": "/docs" | |
| } | |
| ) | |
| if __name__ == "__main__": | |
| log_info("π Starting Flare backend on port 7860...") | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |