Spaces:
Running
Running
| import logging | |
| from fastapi import FastAPI | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse | |
| from api.database import connect_db, disconnect_db, db | |
| from api.routes import conflicts, stats, websocket, intel, intel_hub, ai_analyst, data_api | |
| import os | |
| from contextlib import asynccontextmanager | |
| log = logging.getLogger("api.main") | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") | |
| async def lifespan(app: FastAPI): | |
| # Startup — wrapped so a DB hiccup doesn't kill the whole process | |
| try: | |
| await connect_db() | |
| from api.bootstrap import bootstrap_db | |
| await bootstrap_db() | |
| log.info("Database and Redis connected successfully. Bootstrap complete.") | |
| except Exception as e: | |
| log.error(f"Startup DB/Redis connection failed (will retry on requests): {e}") | |
| yield | |
| # Shutdown | |
| try: | |
| await disconnect_db() | |
| except Exception as e: | |
| log.error(f"Error during shutdown disconnect: {e}") | |
| app = FastAPI( | |
| title="ConflictIQ API", | |
| version="2.0", | |
| description="Real-time conflict intelligence API", | |
| lifespan=lifespan, | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["GET", "POST", "OPTIONS"], | |
| allow_headers=["*"], | |
| ) | |
| # Mount API Routers | |
| app.include_router(conflicts.router, prefix="/api/v1") | |
| app.include_router(stats.router, prefix="/api/v1") | |
| app.include_router(websocket.router, prefix="/api/v1") | |
| app.include_router(intel.router, prefix="/api/v1") | |
| app.include_router(intel_hub.router, prefix="/api/v1") | |
| app.include_router(ai_analyst.router, prefix="/api/v1") | |
| app.include_router(data_api.router, prefix="/api/v1") | |
| # --- Frontend Serving Layer --- | |
| # Check if frontend/dist exists | |
| front_dist = os.path.join(os.getcwd(), "frontend", "dist") | |
| if os.path.exists(front_dist): | |
| app.mount("/assets", StaticFiles(directory=os.path.join(front_dist, "assets")), name="assets") | |
| async def serve_index(): | |
| return FileResponse(os.path.join(front_dist, "index.html")) | |
| async def catch_all(full_path: str): | |
| # Only serve index for non-API routes | |
| if full_path.startswith("api/v1") or full_path.startswith("health") or full_path.startswith("docs"): | |
| return None # FastAPI will handle via routing | |
| # Check if it's a static file that exists | |
| file_path = os.path.join(front_dist, full_path) | |
| if os.path.exists(file_path) and os.path.isfile(file_path): | |
| return FileResponse(file_path) | |
| return FileResponse(os.path.join(front_dist, "index.html")) | |
| else: | |
| async def root(): | |
| return {"service": "ConflictIQ API", "version": "2.0", "status": "running", "frontend": "not_found"} | |
| async def health_check(): | |
| row_count = 0 | |
| db_ok = False | |
| if db.pool: | |
| try: | |
| async with db.pool.acquire() as conn: | |
| row = await conn.fetchrow("SELECT count(*) FROM conflict_events") | |
| row_count = row["count"] if row else 0 | |
| db_ok = True | |
| except Exception as e: | |
| log.error(f"Health check DB query failed: {e}") | |
| return { | |
| "status": "OK", | |
| "events_total": row_count, | |
| "database_connected": db_ok, | |
| "redis_connected": db.redis is not None, | |
| } | |
| async def download_training_data(): | |
| """Download the collected training data (JSONL) for Phase 2 fine-tuning.""" | |
| file_path = os.path.join(os.path.dirname(__file__), "..", "poller", "training_data.jsonl") | |
| if os.path.exists(file_path): | |
| return FileResponse(path=file_path, filename="training_data.jsonl", media_type="application/x-jsonlines") | |
| return {"error": "Training data file not found yet. It will be created after the first poll classifies something."} | |