""" main.py — FastAPI app for the CodeDebug Multi-LLM Debugger. Endpoints: GET / → serves frontend/index.html GET /health → health check GET /api/v1/models → list configured models POST /api/v1/debug → main pipeline (3 panel LLMs → Qwen3 judge) """ from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from pydantic import BaseModel, Field, validator from app.config import settings from app.llm_chain import debug_query # ── Lifespan ────────────────────────────────────────────────────────────────── @asynccontextmanager async def lifespan(app: FastAPI): print("⚡ Starting CodeDebug Multi-LLM Debugger...") print(f" Panel models : {settings.PANEL_MODELS}") print(f" Judge model : {settings.JUDGE_MODEL}") if not settings.OPENROUTER_API_KEY: print("⚠️ WARNING: OPENROUTER_API_KEY is not set — requests will fail!") else: print("✅ API key loaded. Ready!") yield print("🛑 Shutting down") # ── App ─────────────────────────────────────────────────────────────────────── app = FastAPI( title="CodeDebug Multi-LLM Debugger", version="2.0.0", description="3 panel LLMs in parallel → Qwen3 judge synthesizes the final answer.", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # ── Validation error handler ────────────────────────────────────────────────── @app.exception_handler(RequestValidationError) async def validation_error_handler(request: Request, exc: RequestValidationError): print(f"❌ Validation error: {exc.errors()}") return JSONResponse( status_code=422, content={"error": str(exc.errors()), "detail": exc.errors()}, ) # ── Request schema ──────────────────────────────────────────────────────────── class DebugRequest(BaseModel): question: str = Field(..., min_length=1, description="The debugging question or code snippet") temperature: float = Field(default=0.3, ge=0.0, le=1.0, description="Sampling temperature for panel models") @validator("temperature", pre=True) def coerce_float(cls, v): try: return float(v) except Exception: return 0.3 @validator("question") def strip_question(cls, v): v = v.strip() if not v: raise ValueError("question must not be empty") return v # ── Frontend ────────────────────────────────────────────────────────────────── FRONTEND = Path(__file__).parent.parent / "frontend" @app.get("/", include_in_schema=False) def serve_ui(): """Serve the single-page frontend.""" index = FRONTEND / "index.html" if index.exists(): return FileResponse(str(index)) return JSONResponse( status_code=200, content={"message": "CodeDebug API running. See /docs for endpoints."}, ) # ── System endpoints ────────────────────────────────────────────────────────── @app.get("/health", tags=["system"]) def health(): """Health check.""" return { "status": "ok", "version": "2.0.0", "api_key_set": bool(settings.OPENROUTER_API_KEY), } @app.get("/api/v1/models", tags=["system"]) def list_models(): """List all configured models.""" return { "panel_models": [ {"model": m, "label": settings.PANEL_MODEL_LABELS.get(m, m)} for m in settings.PANEL_MODELS ], "judge_model": { "model": settings.JUDGE_MODEL, "label": settings.JUDGE_LABEL, }, } # ── Debug endpoint ──────────────────────────────────────────────────────────── @app.post("/api/v1/debug", tags=["debug"]) async def debug(body: DebugRequest): """ Main endpoint. Sends `question` to 3 panel LLMs in parallel (LangChain LCEL chains), then forwards all responses to the Qwen3 judge which returns: - reasoning : judge's analysis of the three answers - final_answer: synthesised definitive answer """ if not settings.OPENROUTER_API_KEY: raise HTTPException( status_code=503, detail="OPENROUTER_API_KEY is not configured on the server.", ) print(f"📨 '{body.question[:70]}...' | temp={body.temperature}") result = await debug_query(body.question, body.temperature) successful = sum(1 for p in result["panel"] if not p.get("error")) print(f"✅ {successful}/3 panel responses · judge {'ok' if not result['judge'].get('error') else 'failed'} · {result['total_ms']}ms") return result