File size: 4,479 Bytes
ba54ea9 | 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 | """Recap β FastAPI app entry point. Serves the React UI + JSON inference API.
GET / β static index.html (React via CDN, Babel-compiled JSX in browser)
GET /static/* β static assets (app.jsx, css)
GET /api/patients β list of patients with full event timelines
POST /api/answer β run the inference gateway and return a cited answer
GET /api/health β liveness + backend selection
"""
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from recap.cases import load_case
from recap.config import load as load_config
from recap.demo_patient import build_demo_patient
from recap.inference import answer as answer_question
from recap.models import Patient
CFG = load_config()
ROOT = Path(__file__).parent
STATIC_DIR = ROOT / "static"
def _discover_cases() -> dict[str, Patient]:
cases: dict[str, Patient] = {}
cases_dir = Path(CFG.cases_dir)
if cases_dir.exists():
for d in sorted(cases_dir.iterdir()):
if (d / "manifest.json").exists():
try:
cases[d.name] = load_case(CFG.cases_dir, d.name)
except Exception as e: # noqa: BLE001 β keep one bad case from breaking the whole API
print(f"[recap] failed to load case {d.name}: {e}")
if not cases:
cases["demo"] = build_demo_patient()
return cases
PATIENTS: dict[str, Patient] = _discover_cases()
app = FastAPI(title="Recap", version="0.1.0")
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
class AnswerRequest(BaseModel):
patient_id: str
question: str
@app.get("/")
def index() -> FileResponse:
return FileResponse(STATIC_DIR / "index.html")
@app.get("/api/patients")
def list_patients() -> JSONResponse:
"""Serialize all loaded patients in a shape the React app expects."""
out = []
for pid, p in PATIENTS.items():
out.append({
"id": p.id,
"display_name": p.display_name,
"age": p.age,
"gender": p.gender,
"mrn": getattr(p, "mrn", None) or f"MRN-{abs(hash(p.id)) % 9999999:07d}",
"summary": _patient_summary(p),
"hook": _patient_hook(p),
"tags": _patient_tags(p),
"events": [_event_to_dict(e) for e in p.events],
})
return JSONResponse(out)
@app.post("/api/answer")
def answer(req: AnswerRequest) -> JSONResponse:
if req.patient_id not in PATIENTS:
return JSONResponse({"error": f"unknown patient {req.patient_id}"}, status_code=404)
p = PATIENTS[req.patient_id]
a = answer_question(req.question, p.events)
return JSONResponse({
"text": a.text,
"citations": [
{"source_id": c.source_id, "page": c.page, "snippet": c.snippet}
for c in a.citations
],
})
@app.get("/api/health")
def health() -> JSONResponse:
return JSONResponse({
"ok": True,
"backend": CFG.backend,
"patient_count": len(PATIENTS),
"patient_ids": list(PATIENTS.keys()),
})
# βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _event_to_dict(e) -> dict:
return {
"id": e.id,
"date": e.date.date().isoformat(),
"category": e.category,
"title": e.title,
"source": e.source,
"body": e.body,
"page": e.metadata.get("page"),
"snippet": e.metadata.get("snippet"),
"flag": e.metadata.get("flag"),
}
def _patient_summary(p: Patient) -> str:
"""One-sentence dossier summary. Real cases override via manifest.summary later."""
n = len(p.events)
years = sorted({e.date.year for e in p.events})
span = f"{years[0]}β{years[-1]}" if years else "no record"
return f"{n} clinical events on file from {span}."
def _patient_hook(p: Patient) -> str:
return ""
def _patient_tags(p: Patient) -> list[str]:
"""Surface the most recent diagnosis titles as tag chips."""
dx = [e.title for e in sorted(p.events, key=lambda e: e.date, reverse=True) if e.category == "diagnosis"]
return dx[:3]
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)
|