flare / app.py
ciyidogan's picture
Update app.py
6b1d010 verified
raw
history blame
12.8 kB
"""
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)