""" Contra — FastAPI backend serving the React frontend. Stage 1: minimal shell. Health endpoint + static-file mount for the built React app. Pipeline endpoints (`/api/query`) come in Stage 2. Run locally: # build the frontend once cd frontend && npm install && npm run build && cd .. # run the backend uv run uvicorn app:app --host 0.0.0.0 --port 8000 For development with hot-reload, run the Vite dev server (`npm run dev`) on port 5173 — it proxies /api to localhost:8000. Deploy: HuggingFace Docker Space. The Dockerfile builds the frontend and runs uvicorn. """ import os import time from pathlib import Path from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, Response from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from starlette.types import Scope load_dotenv() from contra.db import resolve_url from contra.pipeline import run_query from contra.retrieval import Retriever DEFAULT_TARGET = os.getenv("CONTRA_TARGET", "railway") EXAMPLES = [ "Does lithium slow cognitive decline in Alzheimer's?", "Do anti-amyloid antibodies improve clinical outcomes in Alzheimer's?", "Is the Mediterranean diet protective against Alzheimer's?", "Do statins reduce Alzheimer's risk?", "Is there a causal link between herpes simplex virus and Alzheimer's?", ] print(f"[startup] Loading Retriever (target={DEFAULT_TARGET})...") _t0 = time.time() RETRIEVER = Retriever(resolve_url(DEFAULT_TARGET)) print( f"[startup] Retriever ready: {len(RETRIEVER.papers):,} papers in " f"{time.time() - _t0:.1f}s" ) app = FastAPI(title="Contra") class QueryRequest(BaseModel): question: str = Field(..., min_length=1, max_length=500) @app.get("/api/health") def health() -> dict: return {"status": "ok", "papers": len(RETRIEVER.papers)} @app.get("/api/examples") def examples() -> dict: return {"examples": EXAMPLES} @app.post("/api/query") def query(req: QueryRequest) -> dict: question = req.question.strip() if not question: raise HTTPException(status_code=400, detail="Question is required.") try: result = run_query(question, target=DEFAULT_TARGET, retriever=RETRIEVER) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) return result.as_dict() # Mount the built React app last so /api/* routes take precedence. # # Cache strategy: # - Hashed assets under /assets/* are content-addressed (Vite puts a hash in # the filename), so they can be cached forever. # - index.html is the bootstrap that points at the hashed assets. It must # never be cached, or browsers will keep loading old JS/CSS bundles after # a deploy. FRONTEND_DIST = Path(__file__).parent / "frontend" / "dist" class CachedStatic(StaticFiles): async def get_response(self, path: str, scope: Scope) -> Response: response = await super().get_response(path, scope) if path.endswith(".html") or path in ("", "/"): response.headers["Cache-Control"] = "no-cache" else: response.headers["Cache-Control"] = "public, max-age=31536000, immutable" return response if FRONTEND_DIST.exists(): app.mount("/", CachedStatic(directory=FRONTEND_DIST, html=True), name="frontend") else: print( f"[startup] WARNING: {FRONTEND_DIST} not found. " "Run `cd frontend && npm run build` to build the React app." )