# ClinicalMatch AI — Agent Instructions > Project memory (build state, completed features, constraints) is also tracked in `.claude/project_memory.md` in this repo. This is a hackathon submission for **"Agents Assemble: Healthcare AI Endgame Challenge"** on the Prompt Opinion platform. Judging criteria: MCP compliance, A2A workflow, FHIR R4 standards, AI quality, impact, feasibility. ## Stack at a glance | Layer | Technology | |---|---| | Backend | FastAPI (Python 3.12), uvicorn | | Graph DB | Neo4j Community 5.x via bolt | | LLM | claude-opus-4-7 via aimlapi.com (OpenAI-compatible) | | GraphRAG | LangChain `GraphCypherQAChain` + custom Cypher prompt | | Frontend | Next.js 16 (webpack mode), React 19, Tailwind CSS 3, Recharts, Leaflet | | Standards | FHIR R4 · MCP (stdio) · A2A state machine | ## Critical: LLM API **Never use the Anthropic SDK directly.** All LLM calls go through aimlapi.com or a compatible alternative using the OpenAI-compatible interface: ```python from openai import OpenAI client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL", "https://ai.aimlapi.com/v1"), ) model = os.getenv("OPENAI_MODEL", "claude-opus-4-7") ``` See `backend/llm_client.py` for the canonical pattern. Do not add `import anthropic` anywhere. ## Starting the services ```bash # Backend — always use --reload for hot reload cd backend && source venv/bin/activate uvicorn main:app --reload --port 8000 # Frontend — always use --webpack (Turbopack is broken on this system) cd frontend && npm run dev # runs: next dev --webpack # MCP server (separate process, stdio transport) cd backend && python mcp_server.py # Seed graph data (~15 min first run) curl -X POST http://localhost:8000/seed ``` After changing backend Python files, uvicorn `--reload` should pick them up. If a 404 appears for a newly-added endpoint or old errors persist, the server needs a manual restart — kill the process and re-run the uvicorn command. ## Project layout ``` promptop/ ├── CLAUDE.md ← you are here ├── README.md ← user-facing docs ├── backend/ │ ├── main.py ← FastAPI app, all routes │ ├── clinicaltrials_api.py ← ClinicalTrials.gov v2 API (async + sync) │ ├── intake_matching.py ← SI-unit clinical intake → trial scoring │ ├── trial_enrichment.py ← passive graph enrichment on search │ ├── matching_engine.py ← FHIR patient → trial scoring (LLM-assisted) │ ├── a2a_workflow.py ← A2A state machine (INGEST→PARSE→MATCH→SCORE→RECRUIT) │ ├── graphrag.py ← LangChain GraphCypherQAChain with custom prompt │ ├── graph_seeder.py ← seeds 500 patients + real NCT trials from APIs │ ├── fhir_adapter.py ← FHIR R4 patient models (P001–P005 mock patients) │ ├── neo4j_setup.py ← Neo4j connection + schema setup │ ├── analytics.py ← dashboard KPIs, funnel, demographics, map data │ ├── recruitment_pipeline.py ← kanban board, outreach generation │ ├── llm_client.py ← all LLM calls (aimlapi.com / claude-opus-4-7) │ ├── mcp_server.py ← MCP stdio server (6 tools) │ └── requirements.txt ├── frontend/ │ ├── src/app/ │ │ ├── page.tsx ← Trial Finder (real-time CT.gov, recency sort) │ │ ├── intake/page.tsx ← Eligibility Check (SI-unit clinical intake form) │ │ ├── screening/page.tsx ← Patient Screening (A2A pipeline, FHIR patients) │ │ ├── recruitment/page.tsx← Recruitment Hub (kanban + outreach generation) │ │ ├── dashboard/page.tsx ← Analytics dashboard (Recharts) │ │ ├── map/page.tsx ← Leaflet site map │ │ ├── graph/page.tsx ← GraphRAG natural language query │ │ └── layout.tsx ← App shell with Sidebar │ ├── src/components/ │ │ ├── Sidebar.tsx ← Navigation sidebar │ │ └── MapComponent.tsx ← Raw Leaflet map (no react-leaflet SSR issues) │ ├── src/lib/api.ts ← Typed API client for all backend endpoints │ └── next.config.ts ← webpack mode, filesystem cache, optimizePackageImports └── docker/ ← Docker + Nginx for HuggingFace Spaces deployment ``` ## Neo4j graph schema ``` (Patient) id, name, age, sex, ecog, condition, city, state, ethnicity, biomarkers[], medications[], source, stage (Trial) id (NCT), title, condition, phase, status, sponsor, eligibility_criteria, min_age, max_age, sex, enrollment, start_date, completion_date, last_updated, ctgov_url (Diagnosis) id, name, icd10 (Biomarker) id (e.g. HER2_POS), name (e.g. "HER2 Positive") (Medication) id (e.g. TAMOXIFEN), name (StudySite) id, name, city, state, lat, lon, trials, enrolled, capacity Relationships: (Patient)-[:ELIGIBLE_FOR {score}]->(Trial) (Patient)-[:HAS_DIAGNOSIS]->(Diagnosis) (Patient)-[:HAS_BIOMARKER]->(Biomarker) (Patient)-[:TAKES_MEDICATION]->(Medication) (Trial)-[:LOCATED_AT]->(StudySite) ``` **Graph scale after seeding:** ~500 patients, ~250 trials, ~9,100 ELIGIBLE_FOR edges. Patient IDs from seeder: `P_C50_0001` (breast), `P_C61_0001` (prostate), etc. Mock FHIR patients: `P001`–`P005` (used by screening/workflow pages). ## Key backend modules ### `clinicaltrials_api.py` - `search_trials()` — async, `sort=LastUpdatePostDate:desc` - `get_trial_details()` — async - `search_trials_sync()` / `get_trial_details_sync()` — sync using `httpx.Client` (NOT `asyncio.run()`). Safe to call from both sync functions and FastAPI async handlers. - `_normalize_study()` — extracts `last_updated`, `ctgov_url` in addition to core fields. **Do not** use `asyncio.run()` inside these sync wrappers — it breaks when called from a running FastAPI event loop. The sync wrappers use `httpx.Client` directly. ### `intake_matching.py` Implements SI-unit clinical intake → trial eligibility matching without requiring a patient ID: - `BIOMARKER_REGISTRY` — maps graph node IDs to labels and eligibility text search terms - `score_intake_against_trial()` — weighted scoring: age (25), sex (15), ECOG (15), biomarkers (30), labs (15) - `_check_labs()` — parses thresholds from eligibility criteria text, converts SI units (creatinine μmol/L ↔ mg/dL, bilirubin μmol/L ↔ mg/dL) - `save_intake_as_patient()` — persists intake as `Patient` node for long-term graph enrichment ### `trial_enrichment.py` - `enrich_trials_from_search()` — called as a `BackgroundTask` on every `/api/v1/trials/search` response; upserts Trial + StudySite nodes - `get_eligible_patient_counts()` — batch graph query, returns `{nct_id: count}` - `get_graph_intelligence()` — per-trial: eligible count + top biomarkers + similar trials ### `graphrag.py` Uses a custom `_CYPHER_PROMPT` with explicit schema examples. Critical rules in the prompt: - Biomarker lookups use `id` property (`{id: 'HER2_POS'}`), never `{name: 'HER2', status: 'positive'}` - Condition lookups use lowercase on Trial nodes - Patient eligibility always via `(Patient)-[:ELIGIBLE_FOR]->(Trial)` ### `a2a_workflow.py` Five-state machine: `INGESTING → PARSING_PROTOCOL → MATCHING → SCORING → RECRUITING` - Calls `search_trials_sync()` / `get_trial_details_sync()` — these are safe (use httpx.Client) - `run_pipeline()` is synchronous; called from async FastAPI endpoint without `await` ## Key frontend pages ### `/intake` — Eligibility Check The primary self-service interface. Accepts raw clinical data in SI units; no patient ID needed. - Six sections: Diagnosis & Demographics, Biomarkers, Lab Values, Treatment History - Biomarker registry loaded from `GET /api/v1/intake/biomarkers` - Submits to `POST /api/v1/intake/match` - Optional "Save to graph" checkbox persists profile as Patient node ### `/` — Trial Finder - Sorted by `LastUpdatePostDate:desc` (most recently updated first) - Each search result triggers background graph enrichment - Expanded cards show Graph Intelligence panel: eligible patient count, top biomarkers, similar trials - Direct ClinicalTrials.gov link per trial ### `/screening` — Patient Screening - Patient ID field is a `` combobox loading from `GET /api/v1/graph/patients` - NCT ID field is a combobox with quick-pick suggestions - Validates non-empty inputs before submitting - Two modes: Single Trial Screen and A2A Full Pipeline ## API endpoints (key ones) ``` GET /api/v1/trials/search — real-time CT.gov search, sorted by recency, graph-enriched POST /api/v1/intake/match — SI-unit clinical intake → ranked trial matches GET /api/v1/intake/biomarkers — biomarker registry for the intake form GET /api/v1/trials/{nct_id}/intelligence — graph-derived insights per trial GET /api/v1/graph/patients — query Neo4j for seeded patient IDs POST /api/v1/patients/{id}/screen/{nct_id} — screen FHIR patient against trial POST /api/v1/workflow/run — run full A2A pipeline GET /api/v1/analytics/kpi — dashboard KPIs GET /api/v1/map/data — site coordinates + patient clusters POST /api/v1/graph/query — GraphRAG natural language POST /seed — trigger full graph seeding GET /api/v1/graph/stats — node/edge counts ``` Full interactive docs at `http://localhost:8000/docs`. ## Environment variables ```env NEO4J_URI=bolt://localhost:7687 NEO4J_USERNAME=neo4j NEO4J_PASSWORD=clinicalmatch2024 NEO4J_DATABASE=neo4j OPENAI_API_KEY= OPENAI_BASE_URL=https://ai.aimlapi.com/v1 OPENAI_MODEL=claude-opus-4-7 NEXT_PUBLIC_API_URL=http://localhost:8000 # dev only; empty string in Docker ``` ## Known issues and constraints - **Turbopack is broken** on this machine — always use `next dev --webpack`. Never suggest `next dev` without `--webpack`. - **`next/font/google`** causes compilation to hang (network request during bundling). Geist font is installed as a package but the `next/font/google` import is removed. Use plain Tailwind `font-sans`. - **`asyncio.run()` from async context** — the sync CT.gov wrappers use `httpx.Client` to avoid this. Never re-introduce `asyncio.run()` into the sync wrappers; it will fail when called from FastAPI's running event loop. - **Leaflet SSR** — `MapComponent.tsx` uses raw Leaflet (not react-leaflet) via `useEffect`. The `MapComponent` dynamic import has `ssr: false`. Do not switch to react-leaflet's `MapContainer`. - **`suppressHydrationWarning`** on `` in `layout.tsx` — required for Grammarly browser extension compatibility. - **Mock FHIR patients** (P001–P005) live in `fhir_adapter.py`. The 500 seeded graph patients (`P_C50_0001` etc.) are in Neo4j only. The screening page loads graph patients from `GET /api/v1/graph/patients` for the combobox. ## Adding new features 1. **New backend route**: add to `main.py`, import the module at the top, add a Pydantic request model if needed 2. **New API function**: add a typed function to `frontend/src/lib/api.ts` 3. **New page**: create `frontend/src/app//page.tsx`, add to `nav` array in `Sidebar.tsx` 4. **Graph schema change**: update `neo4j_setup.py` constraints/indexes, update `_CYPHER_PROMPT` in `graphrag.py` with the new node/property examples 5. **New biomarker**: add to `BIOMARKER_REGISTRY` in `intake_matching.py` and to `BM_GROUPS` in `frontend/src/app/intake/page.tsx` ## Demo script (for judges) 1. `GET /api/v1/graph/stats` — confirm 500+ patients and 9,100+ edges 2. `/` — search "breast cancer" → observe recency sort, graph-matched patient count badges 3. Expand a trial → Graph Intelligence panel shows eligible patients, top biomarkers, similar trials 4. `/intake` — enter: Age 52, Female, ECOG 1, HER2+, Hgb 12.5 g/dL, Creatinine 88 μmol/L → ranked trials with pass/fail breakdown 5. `/screening` — select P_C50_0001 from combobox → run A2A Pipeline → observe 5-state machine 6. `/recruitment` — kanban board, generate PCP letter outreach 7. `/dashboard` — KPI cards, enrollment funnel, demographics 8. `/graph` — ask "which patients are eligible for breast cancer trials?" 9. In Prompt Opinion: call MCP tool `find_trials(condition="breast cancer")`