""" 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 # Standard library import uvicorn import os from pathlib import Path import mimetypes import uuid import traceback from datetime import datetime # Pydantic from pydantic import ValidationError # Project imports from websocket_handler import websocket_endpoint from chat_handler import router as chat_router 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 ( FlareException, RaceConditionError, format_error_response, get_http_status_code ) 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 -------------------------- app.include_router(chat_router, prefix="/api") # ---------------- Admin API routes ---------------------------------- app.include_router(admin_router) # 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=response_body ) # Generic error response return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "error": "InternalServerError", "message": "An unexpected error occurred. Please try again later.", "request_id": request_id, "timestamp": datetime.utcnow().isoformat() } ) # Validation error handler @app.exception_handler(ValidationError) async def validation_exception_handler(request: Request, exc: ValidationError): """Handle Pydantic validation errors""" request_id = getattr(request.state, 'request_id', 'unknown') errors = [] for error in exc.errors(): field = " -> ".join(str(x) for x in error['loc']) errors.append({ 'field': field, 'message': error['msg'], 'type': error['type'], 'input': error.get('input') }) log_warning( "Validation error", request_id=request_id, errors=errors ) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "error": "ValidationError", "message": "Invalid request data. Please check the fields and try again.", "details": errors, "request_id": request_id, "timestamp": datetime.utcnow().isoformat() } ) # ---------------- 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("/") def health_check(): """Health check with detailed status""" 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 Angular UI if exists ------------------------ static_dir = Path(__file__).parent / "static" if static_dir.exists(): log_info("🎨 Serving Angular UI from /static directory") # Mount static files with custom handler for Angular routing @app.get("/{path:path}") async def serve_angular(path: str): # API routes should not be handled here if path.startswith("api/"): return {"error": "Not found"}, 404 # Try to serve the exact file first file_path = static_dir / path if file_path.is_file(): return FileResponse(file_path) # For Angular routes, always serve index.html index_path = static_dir / "index.html" if index_path.exists(): return FileResponse(index_path) return {"error": "Not found"}, 404 # Mount static files for assets app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static") else: log_info("⚠️ No UI found. Run 'cd flare-ui && npm run build' to build the UI.") if __name__ == "__main__": log_info("🌐 Starting Flare backend on port 7860...") uvicorn.run(app, host="0.0.0.0", port=7860)