conflictData / api /main.py
Hardik Singh
Final Overhaul: Hybrid Classifier + 500 Fix + Data Hunter
9456341
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")
@asynccontextmanager
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")
@app.get("/")
async def serve_index():
return FileResponse(os.path.join(front_dist, "index.html"))
@app.get("/{full_path:path}")
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:
@app.get("/", tags=["Root"])
async def root():
return {"service": "ConflictIQ API", "version": "2.0", "status": "running", "frontend": "not_found"}
@app.get("/health", tags=["Health"])
@app.get("/api/v1/health", tags=["Health"])
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,
}
@app.get("/api/v1/debug/training-data", tags=["Debug"])
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."}