""" Flare – Main Application (Refactored) ===================================== """ # 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 from pydantic import ValidationError from dotenv import load_dotenv # Project imports from websocket_handler import websocket_endpoint from admin_routes import router as admin_router, start_cleanup_task from llm_startup import run_in_thread from session import session_store, start_session_cleanup from config_provider import ConfigProvider # Logger imports (utils.log yerine) from logger import log_error, log_info, log_warning # Exception imports from exceptions import ( DuplicateResourceError, RaceConditionError, ValidationError, ResourceNotFoundError, AuthenticationError, AuthorizationError, ConfigurationError, get_http_status_code ) # 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 @app.middleware("http") 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 run_in_thread() # Start LLM startup notifier if needed start_cleanup_task() # Activity log cleanup start_session_cleanup() # Session cleanup # ---------------- Core chat/session routes -------------------------- from chat_handler import router as chat_router app.include_router(chat_router, prefix="/api") # ---------------- Audio (TTS/STT) routes ------------------------------ from 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") # ---------------- Exception Handlers ---------------------------------- # Add global exception handler @app.exception_handler(Exception) 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 = format_error_response(exc, request_id) # 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 @app.exception_handler(DuplicateResourceError) 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") } ) @app.exception_handler(RaceConditionError) async def race_condition_handler(request: Request, exc: RaceConditionError): """Handle race condition errors""" return JSONResponse( status_code=409, content=exc.to_http_detail() ) @app.exception_handler(ValidationError) async def validation_error_handler(request: Request, exc: ValidationError): """Handle validation errors""" return JSONResponse( status_code=422, content={ "detail": str(exc), "error_type": "validation_error", "details": exc.details } ) @app.exception_handler(ResourceNotFoundError) 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") } ) @app.exception_handler(AuthenticationError) 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" } ) @app.exception_handler(AuthorizationError) 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" } ) @app.exception_handler(ConfigurationError) 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 ----------------- @app.get("/metrics") async def get_metrics(): """Get system metrics""" import psutil import gc # Memory info process = psutil.Process() memory_info = process.memory_info() # Session stats session_stats = session_store.get_session_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, "gc": { "collections": gc.get_count(), "objects": len(gc.get_objects()) }, "uptime_seconds": time.time() - process.create_time() } return metrics # ---------------- Health probe (HF Spaces watchdog) ----------------- @app.get("/api/health") def health_check(): """Health check endpoint - moved to /api/health""" return { "status": "ok", "version": "2.0.0", "timestamp": datetime.utcnow().isoformat(), "environment": os.getenv("ENVIRONMENT", "development") } # ---------------- WebSocket route for real-time STT ------------------ @app.websocket("/ws/conversation/{session_id}") async def conversation_websocket(websocket: WebSocket, session_id: str): await websocket_endpoint(websocket, session_id) # ---------------- 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) @app.get("/", response_class=FileResponse) 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 @app.get("/{full_path:path}") 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") @app.get("/") 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)