File size: 5,804 Bytes
abd4352 c5f9c5f abd4352 ea93193 abd4352 c5f9c5f abd4352 c5f9c5f ea93193 1f93fec ea93193 abd4352 ea93193 abd4352 7d3b4de 54e2e42 7d3b4de | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | """
api/main.py
FastAPI application β mounts all routers, configures CORS, rate limiting, lifespan.
"""
import os
from contextlib import asynccontextmanager
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from api.routers import query, upload, history, dashboard, report, schema
from api.routers import metrics as metrics_router
from api.routers import profile as profile_router
# ββ Rate limiter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
limiter = Limiter(key_func=get_remote_address)
import asyncio
import httpx
async def keep_awake():
"""Background task to ping the server and prevent HF Space from sleeping."""
while True:
try:
await asyncio.sleep(600) # Ping every 10 minutes
async with httpx.AsyncClient() as client:
await client.get("http://127.0.0.1:7860/health", timeout=10.0)
print("[Keep-Awake] Pinged health endpoint to prevent sleep.")
except asyncio.CancelledError:
break
except Exception as e:
print(f"[Keep-Awake] Ping failed: {e}")
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: pre-warm LLM clients + embedding model
from llm import get_groq_client, get_embedder
get_groq_client()
# Pre-download and load embedding model (first run downloads ~420MB)
print("[Lifespan] Loading embedding model (first run may download ~420MB)...")
get_embedder()
print("[Lifespan] Embedding model loaded successfully.")
# Run automatic database migrations on boot
print("[Lifespan] Ensuring database tables exist...")
try:
from db.pool import get_connection, release_connection
import os
conn = get_connection()
try:
with conn.cursor() as cur:
with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "scripts", "migrate.sql"), "r") as f:
cur.execute(f.read())
conn.commit()
print("[Lifespan] Database migrations executed successfully.")
finally:
release_connection(conn)
except Exception as e:
print(f"[Lifespan] Failed to run database migrations: {e}")
# Start the keep-awake background task
ping_task = asyncio.create_task(keep_awake())
yield
# Shutdown: cancel task and close DB pool
ping_task.cancel()
try:
from db.pool import get_pool
get_pool().closeall()
except Exception:
pass
app = FastAPI(
title="Cloud Data Analyst Agent",
version="2.0.0",
description="AI-powered data analyst with self-correcting LangGraph agent, "
"multi-turn conversations, anomaly detection, and real-time observability.",
docs_url="/docs",
lifespan=lifespan,
)
# Attach rate limiter
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# CORS β allow the Vite frontend and any Render preview URL
ALLOWED_ORIGINS = os.environ.get(
"ALLOWED_ORIGINS",
"http://localhost:5173,http://localhost:3000",
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"] if os.environ.get("DEMO_MODE") == "true" else ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Routers
app.include_router(query.router, prefix="/api/query", tags=["query"])
app.include_router(upload.router, prefix="/api/upload", tags=["upload"])
app.include_router(history.router, prefix="/api/history", tags=["history"])
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"])
app.include_router(report.router, prefix="/api/report", tags=["report"])
app.include_router(schema.router, prefix="/api/schema", tags=["schema"])
app.include_router(metrics_router.router, prefix="/api/metrics", tags=["metrics"])
app.include_router(profile_router.router, prefix="/api/profile", tags=["profile"])
@app.get("/health")
async def health():
return {"status": "ok", "version": "2.0.0"}
# ββ Serve Frontend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
frontend_dist = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist")
# Mount the assets directory specifically if it exists
assets_dir = os.path.join(frontend_dist, "assets")
if os.path.exists(assets_dir):
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
@app.get("/{full_path:path}")
async def serve_frontend(full_path: str):
# Prevent API routes from being intercepted if they fall through
if full_path.startswith("api/"):
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="API route not found")
file_path = os.path.join(frontend_dist, full_path)
if full_path and os.path.isfile(file_path):
return FileResponse(file_path)
# Fallback to index.html for React Router
index_file = os.path.join(frontend_dist, "index.html")
if os.path.isfile(index_file):
return FileResponse(index_file)
return {"error": "Frontend build not found"}
|