bshepp
commited on
Commit
·
f2c113d
0
Parent(s):
Initial commit: CDS Agent - Clinical Decision Support System
Browse files- 5-step agentic pipeline: parse reason drug check RAG guidelines synthesize
- Backend: FastAPI + Python 3.10, Gemma 3 27B IT via Google AI Studio
- Frontend: Next.js 14 + React 18 + TypeScript + Tailwind CSS
- RAG: ChromaDB with 62 clinical guidelines across 14 specialties
- Drug APIs: OpenFDA + RxNorm/NLM
- Tests: E2E pipeline, 30 RAG quality queries (100% pass), 22 clinical scenarios
- Full documentation: architecture, test results, development log, writeup
This view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +52 -0
- DEVELOPMENT_LOG.md +219 -0
- README.md +303 -0
- RULES_SUMMARY.md +113 -0
- SUBMISSION_GUIDE.md +152 -0
- demo/.gitkeep +3 -0
- docs/architecture.md +268 -0
- docs/test_results.md +195 -0
- docs/writeup_draft.md +147 -0
- download_data.txt +1 -0
- models/.gitkeep +3 -0
- notebooks/.gitkeep +3 -0
- overview.txt +167 -0
- rules.txt +163 -0
- src/.gitkeep +3 -0
- src/backend/.env.template +29 -0
- src/backend/app/__init__.py +1 -0
- src/backend/app/agent/__init__.py +1 -0
- src/backend/app/agent/orchestrator.py +266 -0
- src/backend/app/api/__init__.py +1 -0
- src/backend/app/api/cases.py +80 -0
- src/backend/app/api/health.py +9 -0
- src/backend/app/api/ws.py +94 -0
- src/backend/app/config.py +48 -0
- src/backend/app/data/clinical_guidelines.json +498 -0
- src/backend/app/main.py +36 -0
- src/backend/app/models/__init__.py +1 -0
- src/backend/app/models/schemas.py +237 -0
- src/backend/app/services/__init__.py +1 -0
- src/backend/app/services/medgemma.py +183 -0
- src/backend/app/tools/__init__.py +1 -0
- src/backend/app/tools/clinical_reasoning.py +133 -0
- src/backend/app/tools/drug_interactions.py +212 -0
- src/backend/app/tools/guideline_retrieval.py +197 -0
- src/backend/app/tools/patient_parser.py +75 -0
- src/backend/app/tools/synthesis.py +180 -0
- src/backend/requirements.txt +27 -0
- src/backend/test_clinical_cases.py +762 -0
- src/backend/test_e2e.py +61 -0
- src/backend/test_poll.py +23 -0
- src/backend/test_rag_quality.py +441 -0
- src/frontend/next-env.d.ts +5 -0
- src/frontend/next.config.js +13 -0
- src/frontend/package-lock.json +1648 -0
- src/frontend/package.json +28 -0
- src/frontend/postcss.config.js +6 -0
- src/frontend/src/app/globals.css +24 -0
- src/frontend/src/app/layout.tsx +20 -0
- src/frontend/src/app/page.tsx +97 -0
- src/frontend/src/components/AgentPipeline.tsx +123 -0
.gitignore
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
*.egg-info/
|
| 7 |
+
dist/
|
| 8 |
+
build/
|
| 9 |
+
*.egg
|
| 10 |
+
|
| 11 |
+
# Virtual environment
|
| 12 |
+
venv/
|
| 13 |
+
.venv/
|
| 14 |
+
env/
|
| 15 |
+
|
| 16 |
+
# Environment variables (contains API keys!)
|
| 17 |
+
.env
|
| 18 |
+
|
| 19 |
+
# ChromaDB local data (rebuilds from clinical_guidelines.json)
|
| 20 |
+
src/backend/data/chroma/
|
| 21 |
+
|
| 22 |
+
# IDE
|
| 23 |
+
.vscode/
|
| 24 |
+
.idea/
|
| 25 |
+
*.swp
|
| 26 |
+
*.swo
|
| 27 |
+
*~
|
| 28 |
+
|
| 29 |
+
# OS
|
| 30 |
+
.DS_Store
|
| 31 |
+
Thumbs.db
|
| 32 |
+
desktop.ini
|
| 33 |
+
|
| 34 |
+
# Node.js
|
| 35 |
+
node_modules/
|
| 36 |
+
.next/
|
| 37 |
+
out/
|
| 38 |
+
|
| 39 |
+
# Test outputs
|
| 40 |
+
results.json
|
| 41 |
+
|
| 42 |
+
# Models (too large for git)
|
| 43 |
+
models/*.bin
|
| 44 |
+
models/*.pt
|
| 45 |
+
models/*.onnx
|
| 46 |
+
models/*.safetensors
|
| 47 |
+
|
| 48 |
+
# Notebooks checkpoints
|
| 49 |
+
.ipynb_checkpoints/
|
| 50 |
+
|
| 51 |
+
# Logs
|
| 52 |
+
*.log
|
DEVELOPMENT_LOG.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Development Log — CDS Agent
|
| 2 |
+
|
| 3 |
+
> Chronological record of the build process, problems encountered, and solutions applied.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Phase 1: Project Scaffolding
|
| 8 |
+
|
| 9 |
+
### Decision: Track Selection
|
| 10 |
+
|
| 11 |
+
Chose the **Agentic Workflow Prize** track ($10K) for the MedGemma Impact Challenge. The clinical decision support use case maps perfectly to an agentic architecture — multiple specialized tools orchestrated by a central agent.
|
| 12 |
+
|
| 13 |
+
### Architecture Design
|
| 14 |
+
|
| 15 |
+
Designed a 5-step sequential pipeline:
|
| 16 |
+
|
| 17 |
+
1. Parse patient data (LLM)
|
| 18 |
+
2. Clinical reasoning / differential diagnosis (LLM)
|
| 19 |
+
3. Drug interaction check (external APIs)
|
| 20 |
+
4. Guideline retrieval (RAG)
|
| 21 |
+
5. Synthesis into CDS report (LLM)
|
| 22 |
+
|
| 23 |
+
**Key design choices:**
|
| 24 |
+
- **Custom orchestrator** instead of LangChain — simpler, more transparent, no framework overhead
|
| 25 |
+
- **WebSocket streaming** — clinician sees each step execute in real time (critical for trust)
|
| 26 |
+
- **Pydantic v2 everywhere** — all inter-step data is strongly typed
|
| 27 |
+
|
| 28 |
+
### Backend Scaffold
|
| 29 |
+
|
| 30 |
+
Built the FastAPI backend from scratch:
|
| 31 |
+
|
| 32 |
+
- `app/main.py` — FastAPI app with CORS, router includes, lifespan
|
| 33 |
+
- `app/config.py` — Pydantic Settings from `.env`
|
| 34 |
+
- `app/models/schemas.py` — All domain models (~238 lines, 10+ Pydantic models)
|
| 35 |
+
- `app/agent/orchestrator.py` — 5-step pipeline (267 lines)
|
| 36 |
+
- `app/services/medgemma.py` — LLM service wrapping OpenAI SDK
|
| 37 |
+
- `app/tools/` — 5 tool modules (one per pipeline step)
|
| 38 |
+
- `app/api/` — 3 route modules (health, cases, WebSocket)
|
| 39 |
+
|
| 40 |
+
### Frontend Scaffold
|
| 41 |
+
|
| 42 |
+
Built the Next.js 14 frontend:
|
| 43 |
+
|
| 44 |
+
- `PatientInput.tsx` — Text area + 3 pre-loaded sample cases
|
| 45 |
+
- `AgentPipeline.tsx` — Real-time 5-step status visualization
|
| 46 |
+
- `CDSReport.tsx` — Final report renderer
|
| 47 |
+
- `useAgentWebSocket.ts` — WebSocket hook for real-time updates
|
| 48 |
+
- `next.config.js` — API proxy to backend
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## Phase 2: Integration & Bug Fixes
|
| 53 |
+
|
| 54 |
+
### Bug: Gemma System Prompt 400 Error
|
| 55 |
+
|
| 56 |
+
**Problem:** The first LLM call failed with HTTP 400. Gemma models via the Google AI Studio OpenAI-compatible endpoint do not support `role: "system"` messages — a fundamental difference from OpenAI's API.
|
| 57 |
+
|
| 58 |
+
**Solution:** Modified `medgemma.py` to detect system messages and fold them into the first user message with a `[System Instructions]` prefix. All pipeline steps now work correctly.
|
| 59 |
+
|
| 60 |
+
**File changed:** `src/backend/app/services/medgemma.py`
|
| 61 |
+
|
| 62 |
+
### Bug: RxNorm API — `rxnormId` Is a List
|
| 63 |
+
|
| 64 |
+
**Problem:** The drug interaction checker crashed when querying RxNorm. The NLM API returns `rxnormId` as a **list** (e.g., `["12345"]`), not a scalar string. The code assumed a string.
|
| 65 |
+
|
| 66 |
+
**Solution:** Added type checking — if `rxnormId` is a list, take the first element; if it's a string, use directly.
|
| 67 |
+
|
| 68 |
+
**File changed:** `src/backend/app/tools/drug_interactions.py`
|
| 69 |
+
|
| 70 |
+
### Bug: OpenAI SDK Version Mismatch
|
| 71 |
+
|
| 72 |
+
**Problem:** `openai==1.0.0` had breaking API changes compared to the code written for the older API pattern.
|
| 73 |
+
|
| 74 |
+
**Solution:** Pinned to `openai==1.51.0` in `requirements.txt`, which is compatible with both the modern SDK API and the Google AI Studio OpenAI-compatible endpoint.
|
| 75 |
+
|
| 76 |
+
**File changed:** `src/backend/requirements.txt`
|
| 77 |
+
|
| 78 |
+
### Bug: Port 8000 Zombie Processes
|
| 79 |
+
|
| 80 |
+
**Problem:** Previous server instances left zombie processes holding port 8000. New `uvicorn` instances couldn't bind.
|
| 81 |
+
|
| 82 |
+
**Solution:** Switched to port 8002 for development. Updated `next.config.js` and `useAgentWebSocket.ts` to proxy to 8002.
|
| 83 |
+
|
| 84 |
+
**Files changed:** `src/frontend/next.config.js`, `src/frontend/src/hooks/useAgentWebSocket.ts`
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## Phase 3: First Successful E2E Test
|
| 89 |
+
|
| 90 |
+
### Test Case: Chest Pain / ACS
|
| 91 |
+
|
| 92 |
+
Submitted a 62-year-old male with crushing substernal chest pain, diaphoresis, HTN, on lisinopril + metformin + atorvastatin.
|
| 93 |
+
|
| 94 |
+
**Results — all 5 steps passed:**
|
| 95 |
+
|
| 96 |
+
| Step | Duration | Outcome |
|
| 97 |
+
|------|----------|---------|
|
| 98 |
+
| Parse | 7.8 s | Correct structured extraction |
|
| 99 |
+
| Reason | 21.2 s | ACS as top differential (correct) |
|
| 100 |
+
| Drug Check | 11.3 s | Queried all 3 medications |
|
| 101 |
+
| Guidelines | 9.6 s | Retrieved ACS/chest pain guidelines |
|
| 102 |
+
| Synthesis | 25.3 s | Comprehensive report with recommendations |
|
| 103 |
+
|
| 104 |
+
This was the first end-to-end success. Total pipeline: ~75 seconds.
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
## Phase 4: Project Direction Shift
|
| 109 |
+
|
| 110 |
+
### Decision: From Competition to Real Application
|
| 111 |
+
|
| 112 |
+
After achieving the first successful E2E test, made the decision to shift focus from "winning a competition" to "building a genuinely important medical application." The clinical decision support problem is real and impactful regardless of competition outcomes.
|
| 113 |
+
|
| 114 |
+
This shift influenced subsequent work — emphasis on:
|
| 115 |
+
- Comprehensive clinical coverage (more specialties, more guidelines)
|
| 116 |
+
- Thorough testing (not just demos)
|
| 117 |
+
- Proper documentation
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## Phase 5: RAG Expansion
|
| 122 |
+
|
| 123 |
+
### Guideline Corpus: 2 → 62
|
| 124 |
+
|
| 125 |
+
The initial RAG system had only 2 minimal fallback guidelines. Expanded to a comprehensive corpus:
|
| 126 |
+
|
| 127 |
+
- **Created:** `app/data/clinical_guidelines.json` — 62 guidelines across 14 specialties
|
| 128 |
+
- **Updated:** `guideline_retrieval.py` — loads from JSON, stores specialty/ID metadata in ChromaDB
|
| 129 |
+
- **Sources:** ACC/AHA, ADA, GOLD, GINA, IDSA, ACOG, AAN, APA, AAP, ACR, ASH, KDIGO, WHO, USPSTF
|
| 130 |
+
|
| 131 |
+
### ChromaDB Rebuild
|
| 132 |
+
|
| 133 |
+
Had to kill locking processes holding the ChromaDB files before rebuilding. After clearing locks, ChromaDB successfully indexed all 62 guidelines with `all-MiniLM-L6-v2` embeddings (384 dimensions).
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## Phase 6: Comprehensive Test Suite
|
| 138 |
+
|
| 139 |
+
### RAG Quality Tests (30 queries)
|
| 140 |
+
|
| 141 |
+
Created `test_rag_quality.py` with 30 clinical queries, each mapped to an expected guideline ID:
|
| 142 |
+
|
| 143 |
+
- **Result: 30/30 passed (100%)**
|
| 144 |
+
- Average relevance score: 0.639
|
| 145 |
+
- Every query returned the correct guideline as the #1 result
|
| 146 |
+
- All 14 specialty categories achieved 100% pass rate
|
| 147 |
+
|
| 148 |
+
### Clinical Test Cases (22 scenarios)
|
| 149 |
+
|
| 150 |
+
Created `test_clinical_cases.py` with 22 diverse clinical scenarios:
|
| 151 |
+
|
| 152 |
+
- Covers 14+ specialties (Cardiology, EM, Endocrinology, Neurology, Pulmonology, GI, ID, Psych, Peds, Nephrology, Toxicology, Geriatrics)
|
| 153 |
+
- Each case has: clinical vignette, expected specialty, validation keywords
|
| 154 |
+
- Supports CLI flags: `--case`, `--specialty`, `--list`, `--report`, `--quiet`
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## Phase 7: Documentation (Current)
|
| 159 |
+
|
| 160 |
+
Performed comprehensive documentation audit. Found:
|
| 161 |
+
- README was outdated (wrong port, missing test info, incomplete structure tree)
|
| 162 |
+
- Architecture doc lacked implementation specifics (RAG details, Gemma workaround, timing)
|
| 163 |
+
- Writeup draft was 100% TODO placeholders
|
| 164 |
+
- No test results documentation existed
|
| 165 |
+
- No development log existed
|
| 166 |
+
|
| 167 |
+
Rewrote/created all documentation:
|
| 168 |
+
- **README.md** — Complete rewrite with results, RAG corpus info, updated structure, corrected setup
|
| 169 |
+
- **docs/architecture.md** — Updated with actual implementation details, timing, config, limitations
|
| 170 |
+
- **docs/test_results.md** — New file documenting all test results and reproduction steps
|
| 171 |
+
- **DEVELOPMENT_LOG.md** — This file
|
| 172 |
+
- **docs/writeup_draft.md** — Filled in with actual project information
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## Dependency Inventory
|
| 177 |
+
|
| 178 |
+
### Python Backend (`requirements.txt`)
|
| 179 |
+
|
| 180 |
+
| Package | Version | Purpose |
|
| 181 |
+
|---------|---------|---------|
|
| 182 |
+
| fastapi | 0.115.0 | Web framework |
|
| 183 |
+
| uvicorn | 0.30.6 | ASGI server |
|
| 184 |
+
| openai | 1.51.0 | LLM API client (OpenAI-compatible) |
|
| 185 |
+
| chromadb | 0.5.7 | Vector database for RAG |
|
| 186 |
+
| sentence-transformers | 3.1.1 | Embedding model |
|
| 187 |
+
| httpx | 0.27.2 | Async HTTP client (API calls) |
|
| 188 |
+
| torch | 2.4.1 | PyTorch (sentence-transformers dependency) |
|
| 189 |
+
| transformers | 4.45.0 | HuggingFace transformers |
|
| 190 |
+
| pydantic-settings | 2.5.2 | Settings management |
|
| 191 |
+
| pydantic | 2.9.2 | Data validation |
|
| 192 |
+
| websockets | 13.1 | WebSocket support |
|
| 193 |
+
| python-dotenv | 1.0.1 | .env file loading |
|
| 194 |
+
| numpy | 1.26.4 | Numerical computing |
|
| 195 |
+
|
| 196 |
+
### Frontend (`package.json`)
|
| 197 |
+
|
| 198 |
+
| Package | Purpose |
|
| 199 |
+
|---------|---------|
|
| 200 |
+
| next 14.x | React framework |
|
| 201 |
+
| react 18.x | UI library |
|
| 202 |
+
| typescript | Type safety |
|
| 203 |
+
| tailwindcss | Styling |
|
| 204 |
+
|
| 205 |
+
---
|
| 206 |
+
|
| 207 |
+
## Environment Configuration
|
| 208 |
+
|
| 209 |
+
All config via `.env` (template in `.env.template`):
|
| 210 |
+
|
| 211 |
+
| Variable | Required | Default | Description |
|
| 212 |
+
|----------|----------|---------|-------------|
|
| 213 |
+
| `MEDGEMMA_API_KEY` | Yes | — | Google AI Studio API key |
|
| 214 |
+
| `MEDGEMMA_BASE_URL` | No | `https://generativelanguage.googleapis.com/v1beta/openai/` | LLM endpoint |
|
| 215 |
+
| `MEDGEMMA_MODEL_ID` | No | `gemma-3-27b-it` | Model identifier |
|
| 216 |
+
| `CHROMA_PERSIST_DIR` | No | `./data/chroma` | ChromaDB storage |
|
| 217 |
+
| `EMBEDDING_MODEL` | No | `sentence-transformers/all-MiniLM-L6-v2` | RAG embeddings |
|
| 218 |
+
| `MAX_GUIDELINES` | No | `5` | Guidelines per RAG query |
|
| 219 |
+
| `AGENT_TIMEOUT` | No | `120` | Pipeline timeout (seconds) |
|
README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CDS Agent — Clinical Decision Support System
|
| 2 |
+
|
| 3 |
+
> An agentic clinical decision support application that orchestrates medical AI with specialized tools to assist clinicians in real time.
|
| 4 |
+
|
| 5 |
+
**Origin:** [MedGemma Impact Challenge](https://www.kaggle.com/competitions/med-gemma-impact-challenge) (Kaggle / Google Research)
|
| 6 |
+
**Focus:** Building a genuinely impactful medical application — not just a competition entry.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## What It Does
|
| 11 |
+
|
| 12 |
+
A clinician pastes a patient case. The system automatically:
|
| 13 |
+
|
| 14 |
+
1. **Parses** the free-text into structured patient data (demographics, vitals, labs, medications, history)
|
| 15 |
+
2. **Reasons** about the case to generate a ranked differential diagnosis with chain-of-thought transparency
|
| 16 |
+
3. **Checks drug interactions** against OpenFDA and RxNorm databases
|
| 17 |
+
4. **Retrieves clinical guidelines** from a 62-guideline RAG corpus spanning 14 medical specialties
|
| 18 |
+
5. **Synthesizes** everything into a structured CDS report with recommendations, warnings, and citations
|
| 19 |
+
|
| 20 |
+
All five steps stream to the frontend in real time via WebSocket — the clinician sees each step execute live.
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## System Architecture
|
| 25 |
+
|
| 26 |
+
```
|
| 27 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 28 |
+
│ FRONTEND (Next.js 14 + React) │
|
| 29 |
+
│ Patient Case Input │ Agent Activity Feed │ CDS Report View │
|
| 30 |
+
└──────────────────────────┬──────────────────────────────────────┘
|
| 31 |
+
│ REST API + WebSocket
|
| 32 |
+
┌──────────────────────────▼──────────────────────────────────────┐
|
| 33 |
+
│ BACKEND (FastAPI + Python 3.10) │
|
| 34 |
+
│ │
|
| 35 |
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
| 36 |
+
│ │ ORCHESTRATOR (5-Step Pipeline) │ │
|
| 37 |
+
│ └─────┬──────────┬──────────┬──────────┬──────────┬─────────┘ │
|
| 38 |
+
│ ┌────▼───┐ ┌───▼────┐ ┌──▼───┐ ┌───▼────┐ ┌───▼─────┐ │
|
| 39 |
+
│ │ Parse │ │Reason │ │ Drug │ │ RAG │ │Synth- │ │
|
| 40 |
+
│ │Patient │ │(LLM) │ │Check │ │Guide- │ │esize │ │
|
| 41 |
+
│ │Data │ │Differ- │ │OpenFDA│ │lines │ │(LLM) │ │
|
| 42 |
+
│ │ │ │ential │ │RxNorm │ │ChromaDB│ │Report │ │
|
| 43 |
+
│ └────────┘ └────────┘ └──────┘ └────────┘ └─────────┘ │
|
| 44 |
+
│ │
|
| 45 |
+
│ External: OpenFDA API │ RxNorm/NLM API │ ChromaDB (local) │
|
| 46 |
+
└──────────────────────────────────────────────────────────────────┘
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
See [docs/architecture.md](docs/architecture.md) for the full design document.
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
## Verified Test Results
|
| 54 |
+
|
| 55 |
+
### Full Pipeline E2E Test (Chest Pain / ACS Case)
|
| 56 |
+
|
| 57 |
+
All 5 pipeline steps completed successfully:
|
| 58 |
+
|
| 59 |
+
| Step | Duration | Result |
|
| 60 |
+
|------|----------|--------|
|
| 61 |
+
| Parse Patient Data | 7.8 s | Structured profile extracted |
|
| 62 |
+
| Clinical Reasoning | 21.2 s | ACS correctly identified as top differential |
|
| 63 |
+
| Drug Interaction Check | 11.3 s | Interactions queried against OpenFDA / RxNorm |
|
| 64 |
+
| Guideline Retrieval (RAG) | 9.6 s | Relevant cardiology guidelines retrieved |
|
| 65 |
+
| Synthesis | 25.3 s | Comprehensive CDS report generated |
|
| 66 |
+
|
| 67 |
+
### RAG Retrieval Quality Test
|
| 68 |
+
|
| 69 |
+
**30 / 30 queries passed (100%)** across all 14 specialties:
|
| 70 |
+
|
| 71 |
+
| Metric | Value |
|
| 72 |
+
|--------|-------|
|
| 73 |
+
| Queries tested | 30 |
|
| 74 |
+
| Pass rate | 100% (30/30) |
|
| 75 |
+
| Avg relevance score | 0.639 |
|
| 76 |
+
| Min relevance score | 0.519 |
|
| 77 |
+
| Max relevance score | 0.765 |
|
| 78 |
+
| Top-1 accuracy | 100% (correct guideline ranked #1 for every query) |
|
| 79 |
+
|
| 80 |
+
Full results: [docs/test_results.md](docs/test_results.md)
|
| 81 |
+
|
| 82 |
+
### Clinical Test Suite
|
| 83 |
+
|
| 84 |
+
22 comprehensive clinical scenarios covering: ACS, AFib, heart failure, stroke, sepsis, anaphylaxis, polytrauma, DKA, thyroid storm, adrenal crisis, massive PE, status asthmaticus, GI bleeding, pancreatitis, status epilepticus, meningitis, suicidal ideation, neonatal fever, pediatric dehydration, hyperkalemia, acetaminophen overdose, and elderly polypharmacy with falls.
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## RAG Clinical Guidelines Corpus
|
| 89 |
+
|
| 90 |
+
**62 clinical guidelines** across **14 medical specialties**, stored in ChromaDB with sentence-transformer embeddings (`all-MiniLM-L6-v2`):
|
| 91 |
+
|
| 92 |
+
| Specialty | Count | Key Topics |
|
| 93 |
+
|-----------|-------|------------|
|
| 94 |
+
| Cardiology | 8 | HTN, chest pain / ACS, HF, AFib, lipids, NSTEMI, PE, valvular disease |
|
| 95 |
+
| Emergency Medicine | 10 | Stroke, sepsis, trauma, anaphylaxis, burns, ACLS, seizures, toxicology, hyperkalemia, acute abdomen |
|
| 96 |
+
| Endocrinology | 7 | DM management, DKA, thyroid, adrenal insufficiency, osteoporosis, hypoglycemia, hypercalcemia |
|
| 97 |
+
| Pulmonology | 4 | COPD, asthma, CAP, pleural effusion |
|
| 98 |
+
| Neurology | 4 | Epilepsy, migraine, MS, meningitis |
|
| 99 |
+
| Gastroenterology | 5 | Upper GI bleed, pancreatitis, cirrhosis, IBD, CRC screening |
|
| 100 |
+
| Infectious Disease | 5 | STIs, UTI, HIV, SSTIs, COVID-19 |
|
| 101 |
+
| Psychiatry | 4 | MDD, suicide risk, GAD, substance use |
|
| 102 |
+
| Pediatrics | 4 | Fever without source, asthma, dehydration, neonatal jaundice |
|
| 103 |
+
| Nephrology | 2 | CKD, AKI |
|
| 104 |
+
| Hematology | 2 | VTE, sickle cell |
|
| 105 |
+
| Rheumatology | 2 | RA, gout |
|
| 106 |
+
| OB/GYN | 2 | Hypertensive disorders of pregnancy, postpartum hemorrhage |
|
| 107 |
+
| Other | 3+ | Preventive medicine (USPSTF), perioperative cardiac risk, dermatology (melanoma) |
|
| 108 |
+
|
| 109 |
+
Sources include ACC/AHA, ADA, GOLD, GINA, IDSA, ACOG, AAN, APA, AAP, ACR, ASH, KDIGO, WHO, and other major guideline organizations.
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## Project Structure
|
| 114 |
+
|
| 115 |
+
```
|
| 116 |
+
medgemma_impact_challenge/
|
| 117 |
+
├── README.md # This file
|
| 118 |
+
├── DEVELOPMENT_LOG.md # Chronological build history & decisions
|
| 119 |
+
├── SUBMISSION_GUIDE.md # Competition submission strategy
|
| 120 |
+
├── RULES_SUMMARY.md # Competition rules checklist
|
| 121 |
+
├── docs/
|
| 122 |
+
│ ├── architecture.md # System architecture & design decisions
|
| 123 |
+
│ ├── test_results.md # Detailed test results & benchmarks
|
| 124 |
+
│ └── writeup_draft.md # Project writeup / summary
|
| 125 |
+
├── src/
|
| 126 |
+
│ ├── backend/ # Python FastAPI backend
|
| 127 |
+
│ │ ├── .env.template # Environment config template
|
| 128 |
+
│ │ ├── .env # Local config (not committed)
|
| 129 |
+
│ │ ├── requirements.txt # Python dependencies (28 packages)
|
| 130 |
+
│ │ ├── test_e2e.py # End-to-end pipeline test
|
| 131 |
+
│ │ ├── test_clinical_cases.py # 22 clinical scenario test suite
|
| 132 |
+
│ │ ├── test_rag_quality.py # RAG retrieval quality tests (30 queries)
|
| 133 |
+
│ │ ├── test_poll.py # Simple case poller utility
|
| 134 |
+
│ │ └── app/
|
| 135 |
+
│ │ ├── main.py # FastAPI entry (CORS, routers, lifespan)
|
| 136 |
+
│ │ ├── config.py # Pydantic Settings (ports, models, dirs)
|
| 137 |
+
│ │ ├── __init__.py
|
| 138 |
+
│ │ ├── models/
|
| 139 |
+
│ │ │ └── schemas.py # All Pydantic models (~238 lines)
|
| 140 |
+
│ │ ├── agent/
|
| 141 |
+
│ │ │ └── orchestrator.py # 5-step pipeline orchestrator (267 lines)
|
| 142 |
+
│ │ ├── services/
|
| 143 |
+
│ │ │ └── medgemma.py # LLM service (OpenAI-compatible API)
|
| 144 |
+
│ │ ├── tools/
|
| 145 |
+
│ │ │ ├── patient_parser.py # Step 1: Free-text → structured data
|
| 146 |
+
│ │ │ ├── clinical_reasoning.py # Step 2: Differential diagnosis
|
| 147 |
+
│ │ │ ├── drug_interactions.py # Step 3: OpenFDA + RxNorm
|
| 148 |
+
│ │ │ ├── guideline_retrieval.py # Step 4: RAG over ChromaDB
|
| 149 |
+
│ │ │ └── synthesis.py # Step 5: CDS report generation
|
| 150 |
+
│ │ ├── data/
|
| 151 |
+
│ │ │ └── clinical_guidelines.json # 62 guidelines, 14 specialties
|
| 152 |
+
│ │ └── api/
|
| 153 |
+
│ │ ├── health.py # GET /api/health
|
| 154 |
+
│ │ ├── cases.py # POST /api/cases/submit, GET /api/cases/{id}
|
| 155 |
+
│ │ └── ws.py # WebSocket /ws/agent
|
| 156 |
+
│ └── frontend/ # Next.js 14 + React 18 + TypeScript
|
| 157 |
+
│ ├── package.json
|
| 158 |
+
│ ├── next.config.js # API proxy → backend
|
| 159 |
+
│ ├── tailwind.config.js
|
| 160 |
+
│ └── src/
|
| 161 |
+
│ ├── app/
|
| 162 |
+
│ │ ├── layout.tsx
|
| 163 |
+
│ │ ├── page.tsx # Main CDS interface
|
| 164 |
+
│ │ └── globals.css
|
| 165 |
+
│ ├── components/
|
| 166 |
+
│ │ ├── PatientInput.tsx # Patient case input + 3 sample cases
|
| 167 |
+
│ │ ├── AgentPipeline.tsx # Real-time step visualization
|
| 168 |
+
│ │ └── CDSReport.tsx # Final report renderer
|
| 169 |
+
│ └── hooks/
|
| 170 |
+
│ └── useAgentWebSocket.ts # WebSocket state management
|
| 171 |
+
├── notebooks/ # Experiment notebooks
|
| 172 |
+
├── models/ # Fine-tuned models (future)
|
| 173 |
+
└── demo/ # Video & demo assets
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
---
|
| 177 |
+
|
| 178 |
+
## Quick Start
|
| 179 |
+
|
| 180 |
+
### Prerequisites
|
| 181 |
+
|
| 182 |
+
- **Python 3.10+** (tested with Python 3.10)
|
| 183 |
+
- **Node.js 18+** (tested with Node.js 18)
|
| 184 |
+
- **API Key:** Google AI Studio API key for Gemma model access
|
| 185 |
+
|
| 186 |
+
### Backend Setup
|
| 187 |
+
|
| 188 |
+
```bash
|
| 189 |
+
cd src/backend
|
| 190 |
+
|
| 191 |
+
# Create and activate virtual environment
|
| 192 |
+
python -m venv venv
|
| 193 |
+
venv\Scripts\activate # Windows
|
| 194 |
+
# source venv/bin/activate # macOS/Linux
|
| 195 |
+
|
| 196 |
+
# Install dependencies
|
| 197 |
+
pip install -r requirements.txt
|
| 198 |
+
|
| 199 |
+
# Configure environment
|
| 200 |
+
copy .env.template .env # Windows (or: cp .env.template .env)
|
| 201 |
+
# Edit .env — set MEDGEMMA_API_KEY to your Google AI Studio key
|
| 202 |
+
|
| 203 |
+
# Start the backend
|
| 204 |
+
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
### Frontend Setup
|
| 208 |
+
|
| 209 |
+
```bash
|
| 210 |
+
cd src/frontend
|
| 211 |
+
|
| 212 |
+
npm install
|
| 213 |
+
npm run dev
|
| 214 |
+
# Open http://localhost:3000
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
> **Note:** The frontend proxies API requests to the backend. If using a non-default port, update `next.config.js` and `src/hooks/useAgentWebSocket.ts` accordingly.
|
| 218 |
+
|
| 219 |
+
### Running Tests
|
| 220 |
+
|
| 221 |
+
```bash
|
| 222 |
+
cd src/backend
|
| 223 |
+
|
| 224 |
+
# RAG retrieval quality test (no backend needed)
|
| 225 |
+
python test_rag_quality.py --rebuild --verbose
|
| 226 |
+
|
| 227 |
+
# Full pipeline E2E test (requires running backend)
|
| 228 |
+
python test_e2e.py
|
| 229 |
+
|
| 230 |
+
# Comprehensive clinical test suite (requires running backend)
|
| 231 |
+
python test_clinical_cases.py --list # See all 22 cases
|
| 232 |
+
python test_clinical_cases.py --case em_sepsis # Run one case
|
| 233 |
+
python test_clinical_cases.py --specialty Cardio # Run by specialty
|
| 234 |
+
python test_clinical_cases.py # Run all cases
|
| 235 |
+
python test_clinical_cases.py --report results.json # Save results
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### Usage
|
| 239 |
+
|
| 240 |
+
1. Open `http://localhost:3000`
|
| 241 |
+
2. Paste a patient case description (or click a sample case)
|
| 242 |
+
3. Click **"Analyze Patient Case"**
|
| 243 |
+
4. Watch the 5-step agent pipeline execute in real time
|
| 244 |
+
5. Review the CDS report: differential diagnosis, drug warnings, guideline recommendations, next steps
|
| 245 |
+
|
| 246 |
+
---
|
| 247 |
+
|
| 248 |
+
## Tech Stack
|
| 249 |
+
|
| 250 |
+
| Layer | Technology | Purpose |
|
| 251 |
+
|-------|-----------|---------|
|
| 252 |
+
| Frontend | Next.js 14, React 18, TypeScript, Tailwind CSS | Patient input, pipeline visualization, report display |
|
| 253 |
+
| API | FastAPI, WebSocket, Pydantic v2 | REST endpoints + real-time streaming |
|
| 254 |
+
| LLM | Gemma 3 27B IT (via Google AI Studio) | Clinical reasoning + synthesis |
|
| 255 |
+
| RAG | ChromaDB, sentence-transformers (all-MiniLM-L6-v2) | Clinical guideline retrieval |
|
| 256 |
+
| Drug Data | OpenFDA API, RxNorm / NLM API | Drug interactions, medication normalization |
|
| 257 |
+
| Validation | Pydantic | Structured output validation across all pipeline steps |
|
| 258 |
+
|
| 259 |
+
---
|
| 260 |
+
|
| 261 |
+
## API Reference
|
| 262 |
+
|
| 263 |
+
| Endpoint | Method | Description |
|
| 264 |
+
|----------|--------|-------------|
|
| 265 |
+
| `/api/health` | GET | Health check |
|
| 266 |
+
| `/api/cases/submit` | POST | Submit a patient case for analysis |
|
| 267 |
+
| `/api/cases/{case_id}` | GET | Get case results (poll for completion) |
|
| 268 |
+
| `/api/cases` | GET | List all cases |
|
| 269 |
+
| `/ws/agent` | WebSocket | Real-time pipeline step streaming |
|
| 270 |
+
|
| 271 |
+
### Submit a Case (REST)
|
| 272 |
+
|
| 273 |
+
```bash
|
| 274 |
+
curl -X POST http://localhost:8000/api/cases/submit \
|
| 275 |
+
-H "Content-Type: application/json" \
|
| 276 |
+
-d '{
|
| 277 |
+
"patient_text": "62yo male with crushing chest pain radiating to left arm...",
|
| 278 |
+
"include_drug_check": true,
|
| 279 |
+
"include_guidelines": true
|
| 280 |
+
}'
|
| 281 |
+
```
|
| 282 |
+
|
| 283 |
+
---
|
| 284 |
+
|
| 285 |
+
## Documentation Index
|
| 286 |
+
|
| 287 |
+
| Document | Description |
|
| 288 |
+
|----------|-------------|
|
| 289 |
+
| [README.md](README.md) | This file — overview, setup, results |
|
| 290 |
+
| [docs/architecture.md](docs/architecture.md) | System architecture, pipeline design, design decisions |
|
| 291 |
+
| [docs/test_results.md](docs/test_results.md) | Detailed test results, RAG benchmarks, pipeline timing |
|
| 292 |
+
| [DEVELOPMENT_LOG.md](DEVELOPMENT_LOG.md) | Chronological build history, problems solved, decisions made |
|
| 293 |
+
| [docs/writeup_draft.md](docs/writeup_draft.md) | Project writeup / summary |
|
| 294 |
+
| [SUBMISSION_GUIDE.md](SUBMISSION_GUIDE.md) | Competition submission strategy |
|
| 295 |
+
| [RULES_SUMMARY.md](RULES_SUMMARY.md) | Competition rules checklist |
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
## License
|
| 300 |
+
|
| 301 |
+
Subject to [HAI-DEF Terms of Use](https://developers.google.com/health-ai-developer-foundations/terms) for model usage.
|
| 302 |
+
|
| 303 |
+
> **Disclaimer:** This is a research / demonstration system. It is NOT a substitute for professional medical judgment. All clinical decisions must be made by qualified healthcare professionals.
|
RULES_SUMMARY.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rules Summary & Compliance Checklist
|
| 2 |
+
|
| 3 |
+
> Distilled from the full competition rules. When in doubt, refer to the [full rules](rules.txt).
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Eligibility
|
| 8 |
+
|
| 9 |
+
- [x] Must have a registered Kaggle account
|
| 10 |
+
- [x] Must be 18+ (or age of majority in your jurisdiction)
|
| 11 |
+
- [x] Cannot be a resident of: Crimea, DNR, LNR, Cuba, Iran, Syria, or North Korea
|
| 12 |
+
- [x] Cannot be under U.S. export controls or sanctions
|
| 13 |
+
- [x] Google/Kaggle employees may participate but **cannot win prizes**
|
| 14 |
+
- [x] Only **one Kaggle account** per person — no multi-accounting
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## Team Rules
|
| 19 |
+
|
| 20 |
+
| Rule | Detail |
|
| 21 |
+
|------|--------|
|
| 22 |
+
| Max team size | **5 members** |
|
| 23 |
+
| Team mergers | Allowed before merger deadline |
|
| 24 |
+
| Submissions per team | **1** (can be edited and re-submitted) |
|
| 25 |
+
| Account requirement | Each member needs their own Kaggle account |
|
| 26 |
+
| Must confirm membership | Respond to team notification message |
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## Submission Rules
|
| 31 |
+
|
| 32 |
+
- **One submission per team** — this single entry covers Main Track + one special award
|
| 33 |
+
- Submission format: **Kaggle Writeup** attached to the competition page
|
| 34 |
+
- Can un-submit, edit, and re-submit unlimited times before deadline
|
| 35 |
+
- Must be received before **February 24, 2026 at 11:59 PM UTC**
|
| 36 |
+
|
| 37 |
+
### Private Resources Warning
|
| 38 |
+
> If you attach a **private Kaggle Resource** to your public Writeup, it will **automatically become public** after the deadline.
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## Data & External Resources
|
| 43 |
+
|
| 44 |
+
| Rule | Detail |
|
| 45 |
+
|------|--------|
|
| 46 |
+
| Competition data | **None provided** |
|
| 47 |
+
| External data | Allowed — must be publicly available & free for all participants |
|
| 48 |
+
| HAI-DEF models | Subject to [HAI-DEF Terms of Use](https://developers.google.com/health-ai-developer-foundations/terms) |
|
| 49 |
+
| Proprietary datasets | Not allowed if cost exceeds "Reasonableness Standard" |
|
| 50 |
+
| AutoML tools | Allowed if properly licensed |
|
| 51 |
+
| Open source | Must use OSI-approved licenses |
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## Code Sharing Rules
|
| 56 |
+
|
| 57 |
+
| Type | Allowed? | Conditions |
|
| 58 |
+
|------|----------|------------|
|
| 59 |
+
| **Private sharing** (between teams) | **NO** | Grounds for disqualification |
|
| 60 |
+
| **Private sharing** (within team) | Yes | — |
|
| 61 |
+
| **Public sharing** | Yes | Must be shared on Kaggle (forums/notebooks) for all participants |
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## Winner Obligations
|
| 66 |
+
|
| 67 |
+
If you win, you must:
|
| 68 |
+
|
| 69 |
+
1. **Deliver final code** — training code, inference code, environment description
|
| 70 |
+
2. **Grant CC BY 4.0 license** on your winning submission
|
| 71 |
+
3. **Sign prize acceptance documents** within 2 weeks of notification
|
| 72 |
+
4. **Complete tax forms** (W-9 for US, W-8BEN for foreign residents)
|
| 73 |
+
5. **Respond to winner notification** within 1 week
|
| 74 |
+
|
| 75 |
+
> If using commercially available software you don't own, you must identify it and explain how to procure it.
|
| 76 |
+
> If input data/pretrained models have incompatible licenses, you don't need to grant open source license for those.
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## Prize Distribution
|
| 81 |
+
|
| 82 |
+
- Monetary prizes split **evenly** among eligible team members (unless team unanimously agrees to different split)
|
| 83 |
+
- **All taxes are the winner's responsibility**
|
| 84 |
+
- Prizes awarded ~30 days after acceptance documents received
|
| 85 |
+
- Prizes **cannot be transferred or assigned**
|
| 86 |
+
|
| 87 |
+
---
|
| 88 |
+
|
| 89 |
+
## Disqualification Risks
|
| 90 |
+
|
| 91 |
+
You can be disqualified for:
|
| 92 |
+
- Using multiple Kaggle accounts
|
| 93 |
+
- Private code sharing outside your team
|
| 94 |
+
- Cheating, deception, or unfair practices
|
| 95 |
+
- Threatening or harassing other participants
|
| 96 |
+
- Not meeting submission requirements
|
| 97 |
+
- Providing false personal information
|
| 98 |
+
- Using non-publicly-available external data
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## Governing Law
|
| 103 |
+
|
| 104 |
+
- California law applies
|
| 105 |
+
- Disputes litigated in Santa Clara County, California, USA
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## Key Contacts
|
| 110 |
+
|
| 111 |
+
- **Competition Sponsor:** Google Research — 1600 Amphitheatre Parkway, Mountain View, CA 94043
|
| 112 |
+
- **Platform:** Kaggle Inc.
|
| 113 |
+
- **Support:** www.kaggle.com/contact
|
SUBMISSION_GUIDE.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Submission & Strategy Guide
|
| 2 |
+
|
| 3 |
+
## Timeline at a Glance
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
Jan 13 ─────────────────────── Feb 24 ──────────── Mar 17-24
|
| 7 |
+
START DEADLINE 11:59 PM UTC RESULTS
|
| 8 |
+
◄────── Build & Iterate ──────►
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
**⏰ Days remaining as of Feb 13, 2026: ~11 days**
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Winning Strategy by Track
|
| 16 |
+
|
| 17 |
+
### Main Track ($75K)
|
| 18 |
+
Focus on **Execution & Communication (30%)** — this is the highest-weighted criterion. A polished video, clean write-up, and well-organized code can make the difference.
|
| 19 |
+
|
| 20 |
+
**Priority order:**
|
| 21 |
+
1. **Execution & Communication (30%)** — Polish everything
|
| 22 |
+
2. **Effective Use of HAI-DEF (20%)** — Show the models are essential, not bolted on
|
| 23 |
+
3. **Product Feasibility (20%)** — Prove it can work in production
|
| 24 |
+
4. **Problem Domain (15%)** — Tell a compelling story about who benefits
|
| 25 |
+
5. **Impact Potential (15%)** — Quantify the impact with clear estimates
|
| 26 |
+
|
| 27 |
+
### Agentic Workflow Prize ($10K)
|
| 28 |
+
- Deploy HAI-DEF models as **intelligent agents** or **callable tools**
|
| 29 |
+
- Demonstrate a **significant overhaul** of a challenging process
|
| 30 |
+
- Show improved efficiency and outcomes via agentic AI
|
| 31 |
+
|
| 32 |
+
### Novel Task Prize ($10K)
|
| 33 |
+
- **Fine-tune** a HAI-DEF model for a task it wasn't originally designed for
|
| 34 |
+
- The more creative and useful the adaptation, the better
|
| 35 |
+
- Document fine-tuning methodology thoroughly
|
| 36 |
+
|
| 37 |
+
### Edge AI Prize ($5K)
|
| 38 |
+
- Run a HAI-DEF model on **local/edge hardware** (phone, scanner, etc.)
|
| 39 |
+
- Focus on model optimization: quantization, distillation, pruning
|
| 40 |
+
- Demonstrate real-world field deployment scenarios
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## Submission Checklist
|
| 45 |
+
|
| 46 |
+
### Required Deliverables
|
| 47 |
+
- [ ] **Kaggle Writeup** — 3 pages or less, following the template
|
| 48 |
+
- [ ] **Video demo** — 3 minutes or less
|
| 49 |
+
- [ ] **Public code repository** — linked in writeup
|
| 50 |
+
- [ ] Uses **at least one HAI-DEF model** (e.g., MedGemma)
|
| 51 |
+
- [ ] Code is **reproducible**
|
| 52 |
+
|
| 53 |
+
### Bonus Deliverables
|
| 54 |
+
- [ ] Public interactive live demo app
|
| 55 |
+
- [ ] Open-weight Hugging Face model tracing to HAI-DEF
|
| 56 |
+
|
| 57 |
+
### Write-up Quality
|
| 58 |
+
- [ ] Clear project name
|
| 59 |
+
- [ ] Team members with specialties and roles listed
|
| 60 |
+
- [ ] Problem statement addresses "Problem Domain" and "Impact Potential" criteria
|
| 61 |
+
- [ ] Overall solution addresses "Effective Use of HAI-DEF Models" criterion
|
| 62 |
+
- [ ] Technical details address "Product Feasibility" criterion
|
| 63 |
+
- [ ] All links (video, code, demo) are working and accessible
|
| 64 |
+
|
| 65 |
+
### Video Quality
|
| 66 |
+
- [ ] 3 minutes or less
|
| 67 |
+
- [ ] Demonstrates the application in action
|
| 68 |
+
- [ ] Explains the problem and solution clearly
|
| 69 |
+
- [ ] Shows HAI-DEF model integration
|
| 70 |
+
- [ ] Professional quality (clear audio, good visuals)
|
| 71 |
+
|
| 72 |
+
### Code Quality
|
| 73 |
+
- [ ] Well-organized repository structure
|
| 74 |
+
- [ ] Clear README with setup instructions
|
| 75 |
+
- [ ] Code is commented and readable
|
| 76 |
+
- [ ] Dependencies are documented (requirements.txt / environment.yml)
|
| 77 |
+
- [ ] Results are reproducible from the repository
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## Video Tips (30% of score rides on execution)
|
| 82 |
+
|
| 83 |
+
1. **Open with the problem** (30 sec) — Who suffers? What's broken?
|
| 84 |
+
2. **Show the solution** (90 sec) — Live demo, not just slides
|
| 85 |
+
3. **Explain the tech** (30 sec) — Which HAI-DEF model, how it's used
|
| 86 |
+
4. **Quantify impact** (15 sec) — Numbers, estimates, or projections
|
| 87 |
+
5. **Close strong** (15 sec) — Vision for the future
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## Technical Approach Suggestions
|
| 92 |
+
|
| 93 |
+
### Application Ideas Aligned to Criteria
|
| 94 |
+
|
| 95 |
+
| Idea | Models | Special Award Fit |
|
| 96 |
+
|------|--------|-------------------|
|
| 97 |
+
| Clinical note summarizer with agent routing | MedGemma | Agentic Workflow |
|
| 98 |
+
| Radiology triage assistant | MedGemma (vision) | Main Track |
|
| 99 |
+
| Dermatology screening on mobile | MedGemma (quantized) | Edge AI |
|
| 100 |
+
| Pathology slide analysis for rare diseases | MedGemma (fine-tuned) | Novel Task |
|
| 101 |
+
| Patient education chatbot | MedGemma | Main Track |
|
| 102 |
+
| Lab result interpreter agent pipeline | MedGemma + tools | Agentic Workflow |
|
| 103 |
+
| Wound assessment via phone camera | MedGemma (vision, edge) | Edge AI |
|
| 104 |
+
|
| 105 |
+
### Key Technical Considerations
|
| 106 |
+
|
| 107 |
+
1. **Model Selection** — Choose the right HAI-DEF model variant for your task
|
| 108 |
+
2. **Fine-tuning** — Document methodology, hyperparameters, dataset curation
|
| 109 |
+
3. **Evaluation** — Include performance metrics and analysis
|
| 110 |
+
4. **Deployment** — Describe your app stack and how it would scale
|
| 111 |
+
5. **Privacy** — Healthcare data is sensitive; address HIPAA/privacy considerations
|
| 112 |
+
6. **External Data** — Must be publicly available and equally accessible to all participants
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## External Data & Tools Rules
|
| 117 |
+
|
| 118 |
+
- External data is allowed but must be **publicly available at no cost** to all participants
|
| 119 |
+
- Use of HAI-DEF/MedGemma is subject to [HAI-DEF Terms of Use](https://developers.google.com/health-ai-developer-foundations/terms)
|
| 120 |
+
- Open source code must use an **OSI-approved license**
|
| 121 |
+
- AutoML tools are permitted if properly licensed
|
| 122 |
+
- **No private code sharing** outside your team during the competition
|
| 123 |
+
- Public code sharing must be done on Kaggle forums/notebooks
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## Draft Writeup Workspace
|
| 128 |
+
|
| 129 |
+
Use `docs/writeup_draft.md` to iterate on your writeup before submitting on Kaggle:
|
| 130 |
+
|
| 131 |
+
```markdown
|
| 132 |
+
### Project name
|
| 133 |
+
[TODO]
|
| 134 |
+
|
| 135 |
+
### Your team
|
| 136 |
+
[TODO: Name, specialty, role for each member]
|
| 137 |
+
|
| 138 |
+
### Problem statement
|
| 139 |
+
[TODO: Define the problem, who's affected, magnitude, why AI is the right solution]
|
| 140 |
+
[TODO: Articulate impact — what changes if this works? How did you estimate impact?]
|
| 141 |
+
|
| 142 |
+
### Overall solution
|
| 143 |
+
[TODO: Which HAI-DEF model(s)? Why are they the right choice?]
|
| 144 |
+
[TODO: How does the application use them to their fullest potential?]
|
| 145 |
+
|
| 146 |
+
### Technical details
|
| 147 |
+
[TODO: Architecture diagram / description]
|
| 148 |
+
[TODO: Fine-tuning details (if applicable)]
|
| 149 |
+
[TODO: Performance metrics and analysis]
|
| 150 |
+
[TODO: Deployment stack and challenges]
|
| 151 |
+
[TODO: How this works in practice, not just benchmarks]
|
| 152 |
+
```
|
demo/.gitkeep
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Demo
|
| 2 |
+
|
| 3 |
+
Place your demo application and video assets here.
|
docs/architecture.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Clinical Decision Support Agent — Architecture
|
| 2 |
+
|
| 3 |
+
## The Problem
|
| 4 |
+
|
| 5 |
+
**Current workflow (painful, error-prone):**
|
| 6 |
+
A clinician sees a patient → manually reviews the chart, labs, medications → searches
|
| 7 |
+
UpToDate or reference materials → checks drug interactions → mentally synthesizes all
|
| 8 |
+
information → makes clinical decisions. This is slow, cognitively taxing, and mistakes
|
| 9 |
+
happen when clinicians are fatigued or overloaded.
|
| 10 |
+
|
| 11 |
+
**Agent-reimagined workflow:**
|
| 12 |
+
Patient data goes in → an orchestrated agent pipeline automatically gathers context,
|
| 13 |
+
reasons about the case, checks interactions, retrieves guidelines, and produces a
|
| 14 |
+
structured clinical decision support report — all in seconds.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## System Architecture
|
| 19 |
+
|
| 20 |
+
```
|
| 21 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 22 |
+
│ FRONTEND (Next.js 14 + React) │
|
| 23 |
+
│ PatientInput.tsx │ AgentPipeline.tsx │ CDSReport.tsx │
|
| 24 |
+
│ 3 sample cases │ Real-time step viz │ Full report render │
|
| 25 |
+
└──────────────────────────┬──────────────────────────────────────┘
|
| 26 |
+
│ REST API (port 3000 → proxy)
|
| 27 |
+
│ WebSocket (direct to backend)
|
| 28 |
+
┌──────────────────────────▼──────────────────────────────────────┐
|
| 29 |
+
│ BACKEND (FastAPI + Python 3.10) │
|
| 30 |
+
│ Port 8000 (default) / 8002 (dev) │
|
| 31 |
+
│ │
|
| 32 |
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
| 33 |
+
│ │ ORCHESTRATOR (orchestrator.py, 267 lines) │ │
|
| 34 |
+
│ │ Sequential 5-step pipeline with structured state passing │ │
|
| 35 |
+
│ └─────┬──────────┬──────────┬──────────┬──────────┬─────────┘ │
|
| 36 |
+
│ │ │ │ │ │ │
|
| 37 |
+
│ ┌────▼───┐ ┌───▼────┐ ┌──▼───┐ ┌───▼────┐ ┌───▼─────┐ │
|
| 38 |
+
│ │Step 1 │ │Step 2 │ │Step 3 │ │Step 4 │ │Step 5 │ │
|
| 39 |
+
│ │Patient │ │Clinical│ │Drug │ │Guide- │ │Synthe- │ │
|
| 40 |
+
│ │Parser │ │Reason- │ │Inter- │ │line │ │sis │ │
|
| 41 |
+
│ │ │ │ing │ │action │ │Retriev-│ │Agent │ │
|
| 42 |
+
│ │(LLM) │ │(LLM) │ │(APIs) │ │al(RAG) │ │(LLM) │ │
|
| 43 |
+
│ └────────┘ └────────┘ └──┬───┘ └──┬─────┘ └─────────┘ │
|
| 44 |
+
│ │ │ │
|
| 45 |
+
│ ┌────▼────┐ ┌─▼──────────────┐ │
|
| 46 |
+
│ │OpenFDA │ │ChromaDB │ │
|
| 47 |
+
│ │RxNorm │ │62 guidelines │ │
|
| 48 |
+
│ │NLM API │ │14 specialties │ │
|
| 49 |
+
│ └─────────┘ │MiniLM-L6-v2 │ │
|
| 50 |
+
│ └─────────────────┘ │
|
| 51 |
+
└──────────────────────────────────────────────────────────────────┘
|
| 52 |
+
|
| 53 |
+
LLM: gemma-3-27b-it via Google AI Studio
|
| 54 |
+
(OpenAI-compatible endpoint)
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
|
| 59 |
+
## Agent Pipeline — Step-by-Step
|
| 60 |
+
|
| 61 |
+
### Step 1: Patient Data Parser (`patient_parser.py`)
|
| 62 |
+
|
| 63 |
+
- **Input:** Raw patient case free-text
|
| 64 |
+
- **Output:** `PatientProfile` (Pydantic model)
|
| 65 |
+
- **Method:** LLM extraction with structured JSON output
|
| 66 |
+
- **Fields extracted:** Demographics, chief complaint, HPI, vital signs, labs, medications, allergies, past medical history, social history
|
| 67 |
+
- **Timing:** ~7.8 s (observed)
|
| 68 |
+
|
| 69 |
+
### Step 2: Clinical Reasoning Agent (`clinical_reasoning.py`)
|
| 70 |
+
|
| 71 |
+
- **Input:** `PatientProfile` from Step 1
|
| 72 |
+
- **Output:** `ClinicalReasoningResult` with ranked differential diagnosis
|
| 73 |
+
- **Method:** Chain-of-thought prompting for transparent reasoning
|
| 74 |
+
- **Key outputs:** Ranked `DiagnosisCandidate` list (name, likelihood, key findings for/against), risk assessment, recommended workup
|
| 75 |
+
- **Timing:** ~21.2 s (observed)
|
| 76 |
+
|
| 77 |
+
### Step 3: Drug Interaction Check (`drug_interactions.py`)
|
| 78 |
+
|
| 79 |
+
- **Input:** Medication list from Step 1 + any proposed medications from Step 2
|
| 80 |
+
- **Output:** `DrugInteractionResult` with interaction warnings
|
| 81 |
+
- **Method:** Two-API approach:
|
| 82 |
+
1. **RxNorm / NLM API** — Normalize medication names to RxCUI identifiers, check pairwise interactions
|
| 83 |
+
2. **OpenFDA API** — Query drug adverse event reports for additional safety data
|
| 84 |
+
- **Bug fix applied:** RxNorm API returns `rxnormId` as a list, not a scalar — code handles both formats
|
| 85 |
+
- **Timing:** ~11.3 s (observed)
|
| 86 |
+
|
| 87 |
+
### Step 4: Guideline Retrieval — RAG (`guideline_retrieval.py`)
|
| 88 |
+
|
| 89 |
+
- **Input:** Primary diagnosis/conditions from Step 2
|
| 90 |
+
- **Output:** `GuidelineRetrievalResult` with relevant guideline excerpts and citations
|
| 91 |
+
- **Method:** Retrieval-Augmented Generation over a curated guideline corpus
|
| 92 |
+
- **RAG details:**
|
| 93 |
+
- **Vector store:** ChromaDB `PersistentClient` (persist dir: `./data/chroma`)
|
| 94 |
+
- **Embedding model:** `sentence-transformers/all-MiniLM-L6-v2` (384-dim)
|
| 95 |
+
- **Corpus:** 62 clinical guidelines from `clinical_guidelines.json`
|
| 96 |
+
- **Specialties:** 14 (Cardiology, EM, Endocrinology, Pulmonology, Neurology, GI, ID, Psychiatry, Pediatrics, Nephrology, Hematology, Rheumatology, OB/GYN, Preventive/Other)
|
| 97 |
+
- **Metadata:** `specialty`, `guideline_id` stored per document in ChromaDB
|
| 98 |
+
- **Similarity:** Cosine similarity, top-k retrieval (k=5 default)
|
| 99 |
+
- **Sources:** ACC/AHA, ADA, GOLD, GINA, IDSA, ACOG, AAN, APA, AAP, ACR, ASH, KDIGO, WHO, USPSTF, and others
|
| 100 |
+
- **Fallback:** If `clinical_guidelines.json` is missing, falls back to 2 minimal embedded guidelines
|
| 101 |
+
- **Timing:** ~9.6 s (observed)
|
| 102 |
+
|
| 103 |
+
### Step 5: Synthesis Agent (`synthesis.py`)
|
| 104 |
+
|
| 105 |
+
- **Input:** All outputs from Steps 1–4
|
| 106 |
+
- **Output:** `CDSReport` (comprehensive structured report)
|
| 107 |
+
- **Report sections:**
|
| 108 |
+
- Patient summary
|
| 109 |
+
- Differential diagnosis with reasoning chains
|
| 110 |
+
- Drug interaction warnings with severity
|
| 111 |
+
- Guideline-concordant recommendations with citations
|
| 112 |
+
- Suggested next steps (immediate, short-term, long-term)
|
| 113 |
+
- Confidence levels and caveats
|
| 114 |
+
- **Timing:** ~25.3 s (observed)
|
| 115 |
+
|
| 116 |
+
**Total pipeline time:** ~75 s for a complex case (all 5 steps sequential).
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
## LLM Integration — Implementation Details
|
| 121 |
+
|
| 122 |
+
### Model Configuration
|
| 123 |
+
|
| 124 |
+
- **Model:** `gemma-3-27b-it`
|
| 125 |
+
- **API:** Google AI Studio (OpenAI-compatible endpoint)
|
| 126 |
+
- **Base URL:** `https://generativelanguage.googleapis.com/v1beta/openai/`
|
| 127 |
+
- **Client:** OpenAI Python SDK (`openai==1.51.0`)
|
| 128 |
+
- **Service:** `medgemma.py` wraps all LLM calls
|
| 129 |
+
|
| 130 |
+
### Gemma System Prompt Workaround
|
| 131 |
+
|
| 132 |
+
**Problem discovered during development:** Gemma models accessed via the Google AI Studio OpenAI-compatible endpoint return a 400 error if you include a `role: "system"` message. The API does not support the system role.
|
| 133 |
+
|
| 134 |
+
**Solution implemented:** `medgemma.py`'s `_generate_api` method detects system messages and folds them into the first user message with a `[System Instructions]` prefix:
|
| 135 |
+
|
| 136 |
+
```python
|
| 137 |
+
# If system message exists, fold it into the first user message
|
| 138 |
+
if messages[0]["role"] == "system":
|
| 139 |
+
system_content = messages[0]["content"]
|
| 140 |
+
messages = messages[1:]
|
| 141 |
+
if messages and messages[0]["role"] == "user":
|
| 142 |
+
messages[0]["content"] = f"[System Instructions]\n{system_content}\n\n{messages[0]['content']}"
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
This preserves the intended behavior while staying compatible with Gemma's API constraints.
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## Data Models (Pydantic v2)
|
| 150 |
+
|
| 151 |
+
All pipeline data is strongly typed via Pydantic models in `schemas.py` (~238 lines):
|
| 152 |
+
|
| 153 |
+
| Model | Purpose |
|
| 154 |
+
|-------|---------|
|
| 155 |
+
| `CaseSubmission` | Input: patient text + feature flags |
|
| 156 |
+
| `PatientProfile` | Step 1 output: demographics, vitals, labs, meds, history |
|
| 157 |
+
| `DiagnosisCandidate` | Individual diagnosis with likelihood + evidence |
|
| 158 |
+
| `ClinicalReasoningResult` | Step 2 output: ranked differentials + workup |
|
| 159 |
+
| `DrugInteraction` | Individual drug interaction warning |
|
| 160 |
+
| `DrugInteractionResult` | Step 3 output: all interaction data |
|
| 161 |
+
| `GuidelineExcerpt` | Individual guideline citation |
|
| 162 |
+
| `GuidelineRetrievalResult` | Step 4 output: relevant guidelines |
|
| 163 |
+
| `CDSReport` | Step 5 output: full synthesized report |
|
| 164 |
+
| `AgentStep` | WebSocket message: step name, status, data, timing |
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## Frontend Architecture
|
| 169 |
+
|
| 170 |
+
### Technology
|
| 171 |
+
|
| 172 |
+
- **Framework:** Next.js 14 (App Router)
|
| 173 |
+
- **UI:** React 18 + TypeScript + Tailwind CSS
|
| 174 |
+
- **State:** React hooks + WebSocket for real-time updates
|
| 175 |
+
|
| 176 |
+
### Components
|
| 177 |
+
|
| 178 |
+
| Component | Role |
|
| 179 |
+
|-----------|------|
|
| 180 |
+
| `PatientInput.tsx` | Text area for patient case + 3 pre-loaded sample cases (chest pain, DKA, pediatric fever) |
|
| 181 |
+
| `AgentPipeline.tsx` | Visualizes the 5-step pipeline in real time — shows status (pending / running / complete / error) for each step as WebSocket messages arrive |
|
| 182 |
+
| `CDSReport.tsx` | Renders the final CDS report: patient summary, differentials, drug warnings, guidelines, next steps |
|
| 183 |
+
|
| 184 |
+
### Communication
|
| 185 |
+
|
| 186 |
+
- **REST API:** `POST /api/cases/submit` (submit case), `GET /api/cases/{id}` (poll results)
|
| 187 |
+
- **WebSocket:** `ws://localhost:8000/ws/agent` — receives `AgentStep` messages as each pipeline step starts/completes
|
| 188 |
+
- **Proxy:** `next.config.js` proxies `/api/` requests to the backend
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
## API Endpoints
|
| 193 |
+
|
| 194 |
+
| Endpoint | Method | Description |
|
| 195 |
+
|----------|--------|-------------|
|
| 196 |
+
| `/api/health` | GET | Health check — returns backend status |
|
| 197 |
+
| `/api/cases/submit` | POST | Submit a `CaseSubmission` for analysis |
|
| 198 |
+
| `/api/cases/{case_id}` | GET | Poll for case results |
|
| 199 |
+
| `/api/cases` | GET | List all submitted cases |
|
| 200 |
+
| `/ws/agent` | WebSocket | Real-time pipeline step streaming |
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## External API Dependencies
|
| 205 |
+
|
| 206 |
+
| API | Purpose | Authentication | Rate Limits |
|
| 207 |
+
|-----|---------|---------------|-------------|
|
| 208 |
+
| Google AI Studio | Gemma 3 27B IT LLM inference | API key | Per-key quota |
|
| 209 |
+
| OpenFDA | Drug adverse event data | None (public) | 240 req/min (with key), 40/min (without) |
|
| 210 |
+
| RxNorm / NLM | Drug normalization (name → RxCUI), pairwise interactions | None (public) | 20 req/sec |
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## Why This Is Agentic (Not Just a Chatbot)
|
| 215 |
+
|
| 216 |
+
| Characteristic | Chatbot | This Agent System |
|
| 217 |
+
|----------------|---------|-------------------|
|
| 218 |
+
| Tool use | None | 4+ specialized tools (parser, drug API, RAG, synthesis) |
|
| 219 |
+
| Planning | None | Orchestrator executes a defined 5-step plan |
|
| 220 |
+
| State management | Stateless | Patient context flows through all steps |
|
| 221 |
+
| Error handling | Generic | Tool-specific fallbacks, graceful degradation |
|
| 222 |
+
| Output structure | Free text | Pydantic-validated, structured, cited |
|
| 223 |
+
| Transparency | Black box | Shows each reasoning step + tool outputs in real time |
|
| 224 |
+
| External data | None | Queries 3 external data sources (OpenFDA, RxNorm, ChromaDB) |
|
| 225 |
+
|
| 226 |
+
---
|
| 227 |
+
|
| 228 |
+
## Key Design Decisions
|
| 229 |
+
|
| 230 |
+
1. **Custom orchestrator over LangChain/LlamaIndex** — Simpler, more transparent, easier to debug. We control the pipeline loop explicitly. No framework overhead for a sequential 5-step pipeline.
|
| 231 |
+
|
| 232 |
+
2. **WebSocket for agent activity** — The frontend shows each step as it happens (parsing → reasoning → checking → retrieving → synthesizing). This real-time visibility is critical for clinician trust.
|
| 233 |
+
|
| 234 |
+
3. **Structured outputs everywhere** — Every tool returns a Pydantic model. The synthesis agent receives structured data, not messy text. This ensures consistency and enables frontend rendering.
|
| 235 |
+
|
| 236 |
+
4. **Gemma in two roles** — As the clinical reasoning engine (Step 2) AND as the synthesis engine (Step 5). The same model reasons about the case and then integrates all tool outputs into a coherent report.
|
| 237 |
+
|
| 238 |
+
5. **Graceful degradation** — If a tool fails (e.g., OpenFDA is down), the agent continues with available information and notes the gap in the final report.
|
| 239 |
+
|
| 240 |
+
6. **Curated guideline corpus over general web search** — 62 hand-selected guidelines from authoritative sources (ACC/AHA, ADA, etc.) ensure quality and citability. Better than scraping the web.
|
| 241 |
+
|
| 242 |
+
7. **ChromaDB for simplicity** — Embedded vector DB that persists locally. No external database service to manage. Rebuilds in seconds from the JSON source.
|
| 243 |
+
|
| 244 |
+
---
|
| 245 |
+
|
| 246 |
+
## Configuration
|
| 247 |
+
|
| 248 |
+
All configuration lives in `config.py` (Pydantic Settings) and `.env`:
|
| 249 |
+
|
| 250 |
+
| Setting | Default | Description |
|
| 251 |
+
|---------|---------|-------------|
|
| 252 |
+
| `MEDGEMMA_API_KEY` | (required) | Google AI Studio API key |
|
| 253 |
+
| `MEDGEMMA_BASE_URL` | `https://generativelanguage.googleapis.com/v1beta/openai/` | LLM API endpoint |
|
| 254 |
+
| `MEDGEMMA_MODEL_ID` | `gemma-3-27b-it` | Model identifier |
|
| 255 |
+
| `CHROMA_PERSIST_DIR` | `./data/chroma` | ChromaDB storage directory |
|
| 256 |
+
| `EMBEDDING_MODEL` | `sentence-transformers/all-MiniLM-L6-v2` | Embedding model for RAG |
|
| 257 |
+
| `MAX_GUIDELINES` | `5` | Number of guidelines to retrieve per query |
|
| 258 |
+
| `AGENT_TIMEOUT` | `120` | Max seconds for full pipeline execution |
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## Known Limitations
|
| 263 |
+
|
| 264 |
+
- **LLM latency:** Full pipeline takes ~75 s due to multiple sequential LLM calls. Could be improved with smaller models or parallel LLM calls.
|
| 265 |
+
- **No authentication:** No user auth — designed as a local demo / research tool.
|
| 266 |
+
- **Single-model:** Uses only Gemma 3 27B IT. Could benefit from specialized models for different steps.
|
| 267 |
+
- **Guideline currency:** Guidelines are a static snapshot. A production system would need automated updates.
|
| 268 |
+
- **No EHR integration:** Input is manual text paste. A production system would integrate with EHR FHIR APIs.
|
docs/test_results.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Test Results — CDS Agent
|
| 2 |
+
|
| 3 |
+
> Last updated after RAG expansion to 62 guidelines across 14 specialties.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. RAG Retrieval Quality Test
|
| 8 |
+
|
| 9 |
+
**Test file:** `src/backend/test_rag_quality.py`
|
| 10 |
+
**What it tests:** Whether the RAG system retrieves the correct clinical guideline for a given clinical query.
|
| 11 |
+
**Methodology:** 30 clinical queries, each with an expected guideline ID. For each query, the test retrieves the top-5 guidelines from ChromaDB and checks whether the expected guideline appears in the results, and whether it scores above the relevance threshold (0.4).
|
| 12 |
+
|
| 13 |
+
### Summary
|
| 14 |
+
|
| 15 |
+
| Metric | Value |
|
| 16 |
+
|--------|-------|
|
| 17 |
+
| Total queries | 30 |
|
| 18 |
+
| Passed | 30 |
|
| 19 |
+
| Failed | 0 |
|
| 20 |
+
| **Pass rate** | **100%** |
|
| 21 |
+
| Avg relevance score | 0.639 |
|
| 22 |
+
| Min relevance score | 0.519 |
|
| 23 |
+
| Max relevance score | 0.765 |
|
| 24 |
+
| Top-1 accuracy | 100% (correct guideline ranked #1 for all 30 queries) |
|
| 25 |
+
|
| 26 |
+
### Results by Specialty
|
| 27 |
+
|
| 28 |
+
| Specialty | Queries | Passed | Pass Rate | Avg Relevance |
|
| 29 |
+
|-----------|---------|--------|-----------|---------------|
|
| 30 |
+
| Cardiology | 4 | 4 | 100% | 0.65 |
|
| 31 |
+
| Emergency Medicine | 5 | 5 | 100% | 0.62 |
|
| 32 |
+
| Endocrinology | 3 | 3 | 100% | 0.64 |
|
| 33 |
+
| Pulmonology | 2 | 2 | 100% | 0.63 |
|
| 34 |
+
| Neurology | 2 | 2 | 100% | 0.66 |
|
| 35 |
+
| Gastroenterology | 2 | 2 | 100% | 0.61 |
|
| 36 |
+
| Infectious Disease | 2 | 2 | 100% | 0.67 |
|
| 37 |
+
| Psychiatry | 2 | 2 | 100% | 0.64 |
|
| 38 |
+
| Pediatrics | 2 | 2 | 100% | 0.63 |
|
| 39 |
+
| Nephrology | 2 | 2 | 100% | 0.65 |
|
| 40 |
+
| Hematology | 1 | 1 | 100% | 0.62 |
|
| 41 |
+
| Rheumatology | 1 | 1 | 100% | 0.64 |
|
| 42 |
+
| OB/GYN | 1 | 1 | 100% | 0.66 |
|
| 43 |
+
| Other | 1 | 1 | 100% | 0.61 |
|
| 44 |
+
|
| 45 |
+
### How to Reproduce
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
cd src/backend
|
| 49 |
+
python test_rag_quality.py --rebuild --verbose
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
**Flags:**
|
| 53 |
+
- `--rebuild` — Rebuild ChromaDB from `clinical_guidelines.json` before testing
|
| 54 |
+
- `--verbose` — Print each query, expected ID, actual top result, and relevance score
|
| 55 |
+
- `--stats` — Print summary statistics only
|
| 56 |
+
- `--query "chest pain"` — Test a single ad-hoc query
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## 2. End-to-End Pipeline Test
|
| 61 |
+
|
| 62 |
+
**Test file:** `src/backend/test_e2e.py`
|
| 63 |
+
**What it tests:** Full 5-step agent pipeline from free-text input to synthesized CDS report.
|
| 64 |
+
**Test case:** 62-year-old male with crushing substernal chest pain, diaphoresis, nausea, HTN history, on lisinopril + metformin + atorvastatin.
|
| 65 |
+
|
| 66 |
+
### Pipeline Step Results
|
| 67 |
+
|
| 68 |
+
| Step | Status | Duration | Key Findings |
|
| 69 |
+
|------|--------|----------|--------------|
|
| 70 |
+
| 1. Parse Patient Data | PASSED | 7.8 s | Correctly extracted: age 62, male, chest pain chief complaint, 3 medications, HTN/DM history |
|
| 71 |
+
| 2. Clinical Reasoning | PASSED | 21.2 s | Top differential: Acute Coronary Syndrome (ACS). Also considered: GERD, PE, aortic dissection |
|
| 72 |
+
| 3. Drug Interaction Check | PASSED | 11.3 s | Queried OpenFDA + RxNorm for lisinopril, metformin, atorvastatin interactions |
|
| 73 |
+
| 4. Guideline Retrieval | PASSED | 9.6 s | Retrieved ACC/AHA chest pain / ACS guidelines from RAG corpus |
|
| 74 |
+
| 5. Synthesis | PASSED | 25.3 s | Generated comprehensive CDS report with differential, warnings, guideline recommendations |
|
| 75 |
+
|
| 76 |
+
**Total pipeline time:** 75.2 s
|
| 77 |
+
|
| 78 |
+
### How to Reproduce
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
# Start the backend first
|
| 82 |
+
cd src/backend
|
| 83 |
+
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
| 84 |
+
|
| 85 |
+
# In another terminal
|
| 86 |
+
cd src/backend
|
| 87 |
+
python test_e2e.py
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## 3. Clinical Test Suite
|
| 93 |
+
|
| 94 |
+
**Test file:** `src/backend/test_clinical_cases.py`
|
| 95 |
+
**What it tests:** 22 diverse clinical scenarios across 14 medical specialties.
|
| 96 |
+
**Methodology:** Each case has a clinical vignette, expected keywords in the CDS report output, and specialty classification. The test submits each case through the full pipeline and validates that expected terms appear in the report.
|
| 97 |
+
|
| 98 |
+
### Test Cases
|
| 99 |
+
|
| 100 |
+
| ID | Specialty | Scenario | Key Validation Keywords |
|
| 101 |
+
|----|-----------|----------|------------------------|
|
| 102 |
+
| `cardio_acs` | Cardiology | 62M crushing chest pain | ACS, troponin, ECG |
|
| 103 |
+
| `cardio_afib` | Cardiology | 72F palpitations, irregular pulse | Atrial fibrillation, anticoagulation, CHA2DS2-VASc |
|
| 104 |
+
| `cardio_hf` | Cardiology | 68M progressive dyspnea, edema | Heart failure, BNP, diuretic |
|
| 105 |
+
| `neuro_stroke` | Neurology | 75M sudden left-sided weakness | Stroke, CT, tPA, NIH Stroke Scale |
|
| 106 |
+
| `em_sepsis` | Emergency Medicine | 45F fever, tachycardia, hypotension | Sepsis, lactate, blood cultures, fluids |
|
| 107 |
+
| `em_anaphylaxis` | Emergency Medicine | 28F bee sting, urticaria, wheezing | Anaphylaxis, epinephrine, airway |
|
| 108 |
+
| `em_polytrauma` | Emergency Medicine | 35M MVC, multiple injuries | Trauma, ATLS, FAST, C-spine |
|
| 109 |
+
| `endo_dka` | Endocrinology | 22F T1DM, vomiting, Kussmaul breathing | DKA, insulin, potassium, anion gap |
|
| 110 |
+
| `endo_thyroid_storm` | Endocrinology | 40F graves, fever, tachycardia, AMS | Thyroid storm, PTU, beta-blocker |
|
| 111 |
+
| `endo_adrenal` | Endocrinology | 55M weakness, hypotension, hyperpigmentation | Adrenal insufficiency, cortisol, hydrocortisone |
|
| 112 |
+
| `pulm_pe` | Pulmonology | 50F post-surgical, sudden dyspnea | Pulmonary embolism, CT angiography, anticoagulation |
|
| 113 |
+
| `pulm_asthma` | Pulmonology | 19M severe wheezing, accessory muscles | Status asthmaticus, albuterol, steroids |
|
| 114 |
+
| `gi_bleed` | Gastroenterology | 60M hematemesis, melena, cirrhosis history | Upper GI bleed, endoscopy, PPI, variceal |
|
| 115 |
+
| `gi_pancreatitis` | Gastroenterology | 48F epigastric pain, lipase elevated | Pancreatitis, NPO, IV fluids, imaging |
|
| 116 |
+
| `neuro_seizure` | Neurology | 30F witnessed generalized seizure | Status epilepticus, benzodiazepine, EEG |
|
| 117 |
+
| `id_meningitis` | Infectious Disease | 20M fever, neck stiffness, photophobia | Meningitis, lumbar puncture, empiric antibiotics |
|
| 118 |
+
| `psych_suicidal` | Psychiatry | 35M suicidal ideation, plan, access | Suicide risk, safety assessment, hospitalization |
|
| 119 |
+
| `peds_fever` | Pediatrics | 3-week-old neonate, fever 38.5°C | Neonatal fever, sepsis workup, admit |
|
| 120 |
+
| `peds_dehydration` | Pediatrics | 2-year-old, 5 days diarrhea/vomiting | Dehydration, ORS, electrolytes |
|
| 121 |
+
| `nephro_hyperkalemia` | Nephrology | 70M CKD, K+ 7.2, ECG changes | Hyperkalemia, calcium gluconate, insulin/glucose, dialysis |
|
| 122 |
+
| `tox_acetaminophen` | Emergency Medicine | 23F intentional APAP overdose | Acetaminophen, NAC, liver, Rumack-Matthew |
|
| 123 |
+
| `geri_polypharmacy` | Geriatrics | 82F on 12 medications, recurrent falls | Polypharmacy, fall risk, medication reconciliation, Beers criteria |
|
| 124 |
+
|
| 125 |
+
### How to Reproduce
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
cd src/backend
|
| 129 |
+
|
| 130 |
+
# List all available cases
|
| 131 |
+
python test_clinical_cases.py --list
|
| 132 |
+
|
| 133 |
+
# Run a single case
|
| 134 |
+
python test_clinical_cases.py --case em_sepsis
|
| 135 |
+
|
| 136 |
+
# Run all cases in a specialty
|
| 137 |
+
python test_clinical_cases.py --specialty Cardiology
|
| 138 |
+
|
| 139 |
+
# Run all 22 cases
|
| 140 |
+
python test_clinical_cases.py
|
| 141 |
+
|
| 142 |
+
# Run all and save report to JSON
|
| 143 |
+
python test_clinical_cases.py --report results.json
|
| 144 |
+
|
| 145 |
+
# Quiet mode (summary only)
|
| 146 |
+
python test_clinical_cases.py --quiet
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
## 4. RAG Corpus Statistics
|
| 152 |
+
|
| 153 |
+
| Metric | Value |
|
| 154 |
+
|--------|-------|
|
| 155 |
+
| Total guidelines | 62 |
|
| 156 |
+
| Specialties covered | 14 |
|
| 157 |
+
| Guidelines stored in ChromaDB | 62 |
|
| 158 |
+
| Embedding model | all-MiniLM-L6-v2 (384 dimensions) |
|
| 159 |
+
| Embedding time (full rebuild) | ~5 s |
|
| 160 |
+
| ChromaDB persist directory | `./data/chroma` |
|
| 161 |
+
| Source file | `app/data/clinical_guidelines.json` |
|
| 162 |
+
|
| 163 |
+
### Guidelines per Specialty
|
| 164 |
+
|
| 165 |
+
| Specialty | Count |
|
| 166 |
+
|-----------|-------|
|
| 167 |
+
| Emergency Medicine | 10 |
|
| 168 |
+
| Cardiology | 8 |
|
| 169 |
+
| Endocrinology | 7 |
|
| 170 |
+
| Gastroenterology | 5 |
|
| 171 |
+
| Infectious Disease | 5 |
|
| 172 |
+
| Pulmonology | 4 |
|
| 173 |
+
| Neurology | 4 |
|
| 174 |
+
| Psychiatry | 4 |
|
| 175 |
+
| Pediatrics | 4 |
|
| 176 |
+
| Nephrology | 2 |
|
| 177 |
+
| Hematology | 2 |
|
| 178 |
+
| Rheumatology | 2 |
|
| 179 |
+
| OB/GYN | 2 |
|
| 180 |
+
| Preventive / Perioperative / Dermatology | 3 |
|
| 181 |
+
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
## 5. Test Infrastructure
|
| 185 |
+
|
| 186 |
+
| File | Lines | Purpose |
|
| 187 |
+
|------|-------|---------|
|
| 188 |
+
| `test_e2e.py` | 57 | Submit chest pain case, poll for completion, validate all 5 steps |
|
| 189 |
+
| `test_clinical_cases.py` | ~400 | 22 clinical cases with keyword validation, CLI flags for filtering |
|
| 190 |
+
| `test_rag_quality.py` | ~350 | 30 RAG retrieval queries with expected guideline IDs, relevance scoring |
|
| 191 |
+
| `test_poll.py` | ~30 | Utility: poll a case ID until completion |
|
| 192 |
+
|
| 193 |
+
### Dependencies for Testing
|
| 194 |
+
|
| 195 |
+
Tests use only the standard library + `httpx` (for REST calls) and the backend's own modules (for RAG tests). No additional test frameworks required beyond what's in `requirements.txt`.
|
docs/writeup_draft.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CDS Agent — Project Writeup
|
| 2 |
+
|
| 3 |
+
> Competition writeup template filled in with actual project details.
|
| 4 |
+
> Also serves as the primary project summary document.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
### Project name
|
| 9 |
+
|
| 10 |
+
**CDS Agent** — Agentic Clinical Decision Support System
|
| 11 |
+
|
| 12 |
+
### Your team
|
| 13 |
+
|
| 14 |
+
| Name | Specialty | Role |
|
| 15 |
+
|------|-----------|------|
|
| 16 |
+
| (Developer) | Software Engineering / AI | Full-stack development, agent architecture, RAG system, testing |
|
| 17 |
+
|
| 18 |
+
### Problem statement
|
| 19 |
+
|
| 20 |
+
**The Problem:**
|
| 21 |
+
|
| 22 |
+
Clinical decision-making is one of the most cognitively demanding tasks in medicine. A clinician seeing a patient must simultaneously: review the patient's history and current presentation, mentally generate a differential diagnosis, recall drug interactions for current and proposed medications, remember relevant clinical guidelines, and synthesize all of this into a coherent care plan — often while fatigued, time-pressured, and managing multiple patients.
|
| 23 |
+
|
| 24 |
+
Medical errors remain a leading cause of patient harm. Studies estimate that diagnostic errors affect approximately 12 million Americans annually, and medication errors harm over 1.5 million people per year. Many of these errors stem not from lack of knowledge, but from the cognitive burden of integrating information from multiple sources under time pressure.
|
| 25 |
+
|
| 26 |
+
**Who is affected:**
|
| 27 |
+
|
| 28 |
+
- **Clinicians** (primary users) — physicians, nurse practitioners, physician assistants in emergency departments, urgent care, and inpatient settings where rapid, comprehensive decision-making is critical
|
| 29 |
+
- **Patients** — who benefit from more thorough, evidence-based care with fewer diagnostic and medication errors
|
| 30 |
+
- **Health systems** — which bear the cost of medical errors, readmissions, and liability
|
| 31 |
+
|
| 32 |
+
**Why AI is the right solution:**
|
| 33 |
+
|
| 34 |
+
This problem cannot be solved with traditional rule-based systems because:
|
| 35 |
+
1. Clinical reasoning requires understanding free-text narratives, not just coded data
|
| 36 |
+
2. Differential diagnosis generation requires probabilistic reasoning over thousands of conditions
|
| 37 |
+
3. Guideline retrieval requires semantic understanding of clinical context
|
| 38 |
+
4. Synthesis requires integrating heterogeneous data (structured labs, free-text guidelines, API-sourced drug data) into coherent recommendations
|
| 39 |
+
|
| 40 |
+
Large language models — specifically medical-domain models like Gemma — can perform all of these tasks. But a single LLM call is insufficient. The agent architecture orchestrates the LLM across multiple specialized steps, augmented with external tools (drug APIs, RAG) to produce a result that no single component could achieve alone.
|
| 41 |
+
|
| 42 |
+
**Impact potential:**
|
| 43 |
+
|
| 44 |
+
If deployed, this system could:
|
| 45 |
+
- Reduce diagnostic error rates by providing systematic differential diagnosis generation for every patient encounter
|
| 46 |
+
- Catch drug interactions that clinicians might miss, especially in polypharmacy patients
|
| 47 |
+
- Ensure guideline-concordant care by surfacing relevant, current clinical guidelines at the point of care
|
| 48 |
+
- Save clinician time by automating the information-gathering and synthesis steps of clinical reasoning
|
| 49 |
+
|
| 50 |
+
Estimated reach: There are approximately 140 million ED visits per year in the US alone. Even a modest improvement in diagnostic accuracy or medication safety across a fraction of these encounters would represent significant impact.
|
| 51 |
+
|
| 52 |
+
### Overall solution
|
| 53 |
+
|
| 54 |
+
**HAI-DEF models used:**
|
| 55 |
+
|
| 56 |
+
- **Gemma 3 27B IT** (`gemma-3-27b-it`) — accessed via Google AI Studio's OpenAI-compatible endpoint
|
| 57 |
+
|
| 58 |
+
**Why this model:**
|
| 59 |
+
|
| 60 |
+
Gemma 3 27B IT provides the right balance of capability and accessibility for a clinical decision support application:
|
| 61 |
+
- Large enough to perform complex clinical reasoning with chain-of-thought transparency
|
| 62 |
+
- Open-weight model that can be self-hosted for HIPAA compliance in production
|
| 63 |
+
- Available via API for rapid development and demonstration
|
| 64 |
+
- Part of the HAI-DEF family, designed with health AI applications in mind
|
| 65 |
+
|
| 66 |
+
**How the model is used:**
|
| 67 |
+
|
| 68 |
+
The model serves as the reasoning engine in a 5-step agentic pipeline:
|
| 69 |
+
|
| 70 |
+
1. **Patient Data Parsing** (LLM) — Extracts structured patient data from free-text clinical narratives
|
| 71 |
+
2. **Clinical Reasoning** (LLM) — Generates ranked differential diagnoses with chain-of-thought reasoning
|
| 72 |
+
3. **Drug Interaction Check** (External APIs) — Queries OpenFDA and RxNorm for medication safety
|
| 73 |
+
4. **Guideline Retrieval** (RAG) — Retrieves relevant clinical guidelines from a 62-guideline corpus using ChromaDB
|
| 74 |
+
5. **Synthesis** (LLM) — Integrates all outputs into a comprehensive CDS report
|
| 75 |
+
|
| 76 |
+
The model is used in Steps 1, 2, and 5 — parsing, reasoning, and synthesis. This demonstrates the model used "to its fullest potential" across multiple distinct clinical tasks within a single workflow.
|
| 77 |
+
|
| 78 |
+
### Technical details
|
| 79 |
+
|
| 80 |
+
**Architecture:**
|
| 81 |
+
|
| 82 |
+
```
|
| 83 |
+
Frontend (Next.js 14) ←→ Backend (FastAPI + Python 3.10)
|
| 84 |
+
│
|
| 85 |
+
Orchestrator (5-step pipeline)
|
| 86 |
+
├── Step 1: Patient Parser (LLM)
|
| 87 |
+
├── Step 2: Clinical Reasoning (LLM)
|
| 88 |
+
├── Step 3: Drug Check (OpenFDA + RxNorm APIs)
|
| 89 |
+
├── Step 4: Guideline Retrieval (ChromaDB RAG)
|
| 90 |
+
└── Step 5: Synthesis (LLM)
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
All inter-step data is strongly typed with Pydantic v2 models. The pipeline streams each step's progress to the frontend via WebSocket for real-time visibility.
|
| 94 |
+
|
| 95 |
+
**Fine-tuning:**
|
| 96 |
+
|
| 97 |
+
No fine-tuning was performed in the current version. The base `gemma-3-27b-it` model was used with carefully crafted prompt engineering for each pipeline step. Fine-tuning on clinical reasoning datasets is a planned improvement.
|
| 98 |
+
|
| 99 |
+
**Performance analysis:**
|
| 100 |
+
|
| 101 |
+
| Test | Result |
|
| 102 |
+
|------|--------|
|
| 103 |
+
| E2E pipeline (chest pain / ACS) | All 5 steps passed, 75 s total |
|
| 104 |
+
| RAG retrieval quality | 30/30 queries passed (100%), avg relevance 0.639 |
|
| 105 |
+
| Clinical test suite | 22 scenarios across 14 specialties |
|
| 106 |
+
| Top-1 RAG accuracy | 100% — correct guideline ranked #1 for all queries |
|
| 107 |
+
|
| 108 |
+
**Application stack:**
|
| 109 |
+
|
| 110 |
+
| Layer | Technology |
|
| 111 |
+
|-------|-----------|
|
| 112 |
+
| Frontend | Next.js 14, React 18, TypeScript, Tailwind CSS |
|
| 113 |
+
| Backend | FastAPI, Python 3.10, Pydantic v2, WebSocket |
|
| 114 |
+
| LLM | Gemma 3 27B IT via Google AI Studio |
|
| 115 |
+
| RAG | ChromaDB + sentence-transformers (all-MiniLM-L6-v2) |
|
| 116 |
+
| Drug Data | OpenFDA API, RxNorm / NLM API |
|
| 117 |
+
|
| 118 |
+
**Deployment considerations:**
|
| 119 |
+
|
| 120 |
+
- **HIPAA compliance:** Gemma is an open-weight model that can be self-hosted on-premises, eliminating the need to send patient data to external APIs. This is critical for healthcare deployment.
|
| 121 |
+
- **Latency:** Current pipeline takes ~75 s end-to-end. For production, this could be reduced with: smaller/distilled models, parallel LLM calls, or GPU-accelerated inference.
|
| 122 |
+
- **Scalability:** FastAPI + uvicorn supports async request handling. For high-throughput deployment, add worker processes and a task queue (e.g., Celery).
|
| 123 |
+
- **EHR integration:** Current input is manual text paste. A production system would integrate with EHR systems via FHIR APIs for automatic patient data extraction.
|
| 124 |
+
|
| 125 |
+
**Practical usage:**
|
| 126 |
+
|
| 127 |
+
In a real clinical setting, the system would be used at the point of care:
|
| 128 |
+
1. Clinician opens the CDS Agent interface (embedded in the EHR or as a standalone app)
|
| 129 |
+
2. Patient data is automatically pulled from the EHR (or pasted manually)
|
| 130 |
+
3. The agent pipeline runs in ~60-90 seconds, during which the clinician can continue other tasks
|
| 131 |
+
4. The CDS report appears with:
|
| 132 |
+
- Ranked differential diagnoses with reasoning chains (transparent AI)
|
| 133 |
+
- Drug interaction warnings with severity levels
|
| 134 |
+
- Relevant clinical guideline excerpts with citations to authoritative sources
|
| 135 |
+
- Suggested next steps (immediate, short-term, long-term)
|
| 136 |
+
5. The clinician reviews the recommendations and incorporates them into their clinical judgment
|
| 137 |
+
|
| 138 |
+
The system is explicitly designed as a **decision support** tool, not a decision-making tool. All recommendations include confidence levels and caveats. The clinician retains full authority over patient care.
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
**Links:**
|
| 143 |
+
|
| 144 |
+
- Video: [To be recorded]
|
| 145 |
+
- Code Repository: [GitHub link to be added]
|
| 146 |
+
- Live Demo: [To be deployed]
|
| 147 |
+
- Hugging Face Model: N/A (using base Gemma 3 27B IT)
|
download_data.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
kaggle competitions download -c med-gemma-impact-challenge
|
models/.gitkeep
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Models
|
| 2 |
+
|
| 3 |
+
Place fine-tuned models, configs, and weights here.
|
notebooks/.gitkeep
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Notebooks
|
| 2 |
+
|
| 3 |
+
Place your experiment and development notebooks here.
|
overview.txt
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MedGemma Impact Challenge
|
| 2 |
+
Build human-centered AI applications with MedGemma and other open models from Google’s Health AI Developer Foundations (HAI-DEF).
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
The MedGemma Impact Challenge
|
| 6 |
+
|
| 7 |
+
View Writeups
|
| 8 |
+
Overview
|
| 9 |
+
In this competition, you’ll use MedGemma and other open models from Google’s Health AI Developer Foundations (HAI-DEF) to build human-centered AI applications.
|
| 10 |
+
|
| 11 |
+
Start
|
| 12 |
+
|
| 13 |
+
a month ago
|
| 14 |
+
Close
|
| 15 |
+
11 days to go
|
| 16 |
+
Description
|
| 17 |
+
AI is already reshaping medicine, from diagnostics to drug discovery. But many clinical environments can’t rely on large, closed models that require constant internet access or centralized infrastructure. They need adaptable, privacy-focused tools that can run anywhere care is delivered.
|
| 18 |
+
|
| 19 |
+
To meet this need, Google has released open-weight models specifically designed to help developers more efficiently create novel healthcare and life sciences applications. MedGemma and the rest of HAI-DEF collection give developers a starting point for building powerful tools while allowing them full control over the models and associated infrastructure.
|
| 20 |
+
|
| 21 |
+
In this competition, you’ll use these models to build full fledged demonstration applications. Whether you’re building apps to streamline workflows, support patient communication, or facilitate diagnostics, your solution should demonstrate how these tools can enhance healthcare.
|
| 22 |
+
|
| 23 |
+
Evaluation
|
| 24 |
+
Minimum requirements
|
| 25 |
+
To be considered a valid contribution, your submission should include:
|
| 26 |
+
|
| 27 |
+
a high-quality writeup describing use of a specific HAI-DEF model,
|
| 28 |
+
associated reproducible code for your initial results, and
|
| 29 |
+
a video for judging.
|
| 30 |
+
Your complete submission consists of a single package containing your video (3 minutes or less) and write-up (3 pages or less). This single entry can be submitted to the main competition track, and one special technology award, so separate submissions are not required. Read the section Submission Instructions for more details. Please follow the provided write-up template and refer to the judging criteria for all content requirements.
|
| 31 |
+
|
| 32 |
+
Evaluation Criteria
|
| 33 |
+
Submissions are evaluated on the following criteria:
|
| 34 |
+
|
| 35 |
+
Criteria (percentage) Description
|
| 36 |
+
Effective use of HAI-DEF models
|
| 37 |
+
(20%) Are HAI-DEF models used appropriately?
|
| 38 |
+
|
| 39 |
+
You will be assessed on: whether the submission proposes an application that uses HAI-DEF models to their fullest potential, where other solutions would likely be less effective.
|
| 40 |
+
|
| 41 |
+
Note: Use of at least one of HAI-DEF models such as MedGemma is mandatory.
|
| 42 |
+
Problem domain
|
| 43 |
+
(15%) How important is this problem to solve and how plausible is it that AI is the right solution?
|
| 44 |
+
|
| 45 |
+
You will be assessed on: storytelling, clarity of problem definition, clarity on whether there is an unmet need, the magnitude of the problem, who the user is and their improved journey given your solution.
|
| 46 |
+
Impact potential
|
| 47 |
+
(15%) If the solution works, what impact would it have?
|
| 48 |
+
|
| 49 |
+
You will be assessed on: clear articulation of real or anticipated impact of your application within the given problem domain and description of how you calculated your estimates.
|
| 50 |
+
Product feasibility
|
| 51 |
+
(20%) Is the technical solution clearly feasible?
|
| 52 |
+
|
| 53 |
+
You will be assessed on: technical documentation detailing model fine-tuning, model's performance analysis, your user-facing application stack, deployment challenges and how you plan on overcoming them. Consideration of how a product might be used in practice, rather than only for benchmarking.
|
| 54 |
+
Execution and communication (30%) What is the quality of your project's execution and your clear and concise communication of your work? Your main submission package follows the provided template and includes a mandatory video demo and a write-up with links to your source material.
|
| 55 |
+
|
| 56 |
+
You will be assessed on: the clarity, polish, and effectiveness of your video demonstration; the completeness and readability of your technical write-up; and the quality of your source code (e.g., organization, comments, reusability). Judges will look for a cohesive and compelling narrative across all submitted materials that effectively articulates how you meet the rest of the judging criteria.
|
| 57 |
+
Timeline
|
| 58 |
+
January 13, 2026 - Start Date.
|
| 59 |
+
February 24, 2026 - Final Submission Deadline.
|
| 60 |
+
March 17 - 24, 2026 - Anticipated Results Announcement - Time required to evaluate results is dependent on the number of submissions.
|
| 61 |
+
All deadlines are at 11:59 PM UTC on the corresponding day unless otherwise noted. The competition organizers reserve the right to update the contest timeline if they deem it necessary.
|
| 62 |
+
|
| 63 |
+
Judges
|
| 64 |
+
Fereshteh Mahvar
|
| 65 |
+
Staff Medical Software Engineer & Solutions Architect, Google Health AI
|
| 66 |
+
Omar Sanseviero
|
| 67 |
+
Developer Experience Lead, Google DeepMind
|
| 68 |
+
Glenn Cameron
|
| 69 |
+
Sr. PMM, Google
|
| 70 |
+
Can "John" Kirmizi
|
| 71 |
+
Software Engineer, Google Research
|
| 72 |
+
Andrew Sellergren
|
| 73 |
+
Software Engineer, Google Research
|
| 74 |
+
Dave Steiner
|
| 75 |
+
Clinical Research Scientist, Google
|
| 76 |
+
Sunny Virmani
|
| 77 |
+
Group Product Manager, Google Research
|
| 78 |
+
Liron Yatziv
|
| 79 |
+
Research Engineer, Google Research
|
| 80 |
+
Daniel Golden
|
| 81 |
+
Engineering Manager, Google Research
|
| 82 |
+
Yun Liu
|
| 83 |
+
Research Scientist, Google Research
|
| 84 |
+
Rebecca Hemenway
|
| 85 |
+
Health AI Strategic Partnerships, Google Research
|
| 86 |
+
Fayaz Jamil
|
| 87 |
+
Technical Program Manager, Google Research
|
| 88 |
+
Tracks and Awards
|
| 89 |
+
Main Track · $75,000
|
| 90 |
+
Description
|
| 91 |
+
These prizes are awarded to the best overall projects that demonstrate exceptional vision, technical execution, and potential for real-world impact.
|
| 92 |
+
|
| 93 |
+
Track Awards
|
| 94 |
+
|
| 95 |
+
1st Place
|
| 96 |
+
$30,000
|
| 97 |
+
|
| 98 |
+
2nd Place
|
| 99 |
+
$20,000
|
| 100 |
+
|
| 101 |
+
3rd Place
|
| 102 |
+
$15,000
|
| 103 |
+
|
| 104 |
+
4th Place
|
| 105 |
+
$10,000
|
| 106 |
+
Agentic Workflow Prize · $10,000
|
| 107 |
+
Description
|
| 108 |
+
It is awarded for the project that most effectively reimagines a complex workflow by deploying HAI-DEF models as intelligent agents or callable tools. The winning solution will demonstrate a significant overhaul of a challenging process, showcasing the power of agentic AI to improve efficiency and outcomes.
|
| 109 |
+
|
| 110 |
+
Track Awards
|
| 111 |
+
|
| 112 |
+
Agentic Workflow Prize 1
|
| 113 |
+
$5,000
|
| 114 |
+
|
| 115 |
+
Agentic Workflow Prize 2
|
| 116 |
+
$5,000
|
| 117 |
+
The Novel Task Prize · $10,000
|
| 118 |
+
Description
|
| 119 |
+
Awarded for the most impressive fine-tuned model that successfully adapts a HAI-DEF model to perform a useful task for which it was not originally trained on pre-release.
|
| 120 |
+
|
| 121 |
+
Track Awards
|
| 122 |
+
|
| 123 |
+
The Novel Task Prize 1
|
| 124 |
+
$5,000
|
| 125 |
+
|
| 126 |
+
The Novel Task Prize 2
|
| 127 |
+
$5,000
|
| 128 |
+
The Edge AI Prize · $5,000
|
| 129 |
+
Description
|
| 130 |
+
This prize is awarded to the most impressive solution that brings AI out of the cloud and into the field. It will be awarded to the team that best adapts a HAI-DEF model to run effectively on a local device like a mobile phone, portable scanner, lab instrument, or other edge hardware.
|
| 131 |
+
|
| 132 |
+
Track Awards
|
| 133 |
+
|
| 134 |
+
The Edge AI Prize
|
| 135 |
+
$5,000
|
| 136 |
+
Submission Instructions
|
| 137 |
+
Your submission must be a Kaggle Writeup and it must be attached to this page. To create a new Writeup, click on the "New Writeup" button here. After you have saved your Writeup, you should see a "Submit" button in the top right corner. Each team is limited to submitting only a single Writeup, but that same Writeup can be un-submitted, edited, and re-submitted as many times as you'd like. Your Writeup should contain a summary of your overall project along with links to supporting resources.
|
| 138 |
+
|
| 139 |
+
Choosing a track
|
| 140 |
+
All submissions compete in the Main Track, and are eligible to win one special award prize (Agentic Workflow Prize, The Novel Task Prize, or The Edge of AI Prize). While you will have the option to select multiple tracks when you create your writeup, you can only chose the main track and one special award prize. If you choose multiple special awards, we will only consider your submission for one of your indicated special awards (randomly selected).
|
| 141 |
+
|
| 142 |
+
Links
|
| 143 |
+
Required: Video (3 min or less)
|
| 144 |
+
Required: Public code repository
|
| 145 |
+
Bonus: Public interactive live demo app
|
| 146 |
+
Bonus: Open-weight Hugging Face model tracing to a HAI-DEF model
|
| 147 |
+
Proposed Writeup template
|
| 148 |
+
Use the following structure and in 3 pages or less present your work. Less is more! You should take advantage of the video to convey most of the concepts and keep the write-up as high level as possible.
|
| 149 |
+
|
| 150 |
+
### Project name
|
| 151 |
+
[A concise name for your project.]
|
| 152 |
+
|
| 153 |
+
### Your team
|
| 154 |
+
[Name your team members, their speciality and the role they played.]
|
| 155 |
+
|
| 156 |
+
### Problem statement
|
| 157 |
+
[Your answer to the “Problem domain” & “Impact potential” criteria]
|
| 158 |
+
|
| 159 |
+
### Overall solution:
|
| 160 |
+
[Your answer to “Effective use of HAI-DEF models” criterion]
|
| 161 |
+
|
| 162 |
+
### Technical details
|
| 163 |
+
[Your answer to “Product feasibility” criterion]
|
| 164 |
+
Note: If you attach a private Kaggle Resource to your public Kaggle Writeup, your private Resource will automatically be made public after the deadline.
|
| 165 |
+
|
| 166 |
+
Citation
|
| 167 |
+
Fereshteh Mahvar, Yun Liu, Daniel Golden, Fayaz Jamil, Sunny Jansen, Can Kirmizi, Rory Pilgrim, David F. Steiner, Andrew Sellergren, Richa Tiwari, Sunny Virmani, Liron Yatziv, Rebecca Hemenway, Yossi Matias, Ronit Levavi Morad, Avinatan Hassidim, Shravya Shetty, and María Cruz. The MedGemma Impact Challenge. https://kaggle.com/competitions/med-gemma-impact-challenge, 2026. Kaggle.
|
rules.txt
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Competition Rules
|
| 2 |
+
ENTRY IN THIS COMPETITION CONSTITUTES YOUR ACCEPTANCE OF THESE OFFICIAL COMPETITION RULES.
|
| 3 |
+
See Section 3.18 for defined terms
|
| 4 |
+
|
| 5 |
+
The Competition named below is a skills-based competition to promote and further the field of data science. You must register via the Competition Website to enter. To enter the Competition, you must agree to these Official Competition Rules, which incorporate by reference the provisions and content of the Competition Website and any Specific Competition Rules herein (collectively, the "Rules"). Please read these Rules carefully before entry to ensure you understand and agree. You further agree that Submission in the Competition constitutes agreement to these Rules. You may not submit to the Competition and are not eligible to receive the prizes associated with this Competition unless you agree to these Rules. These Rules form a binding legal agreement between you and the Competition Sponsor with respect to the Competition. Your competition Submissions must conform to the requirements stated on the Competition Website. Your Submissions will be scored based on the evaluation metric described on the Competition Website. Subject to compliance with the Competition Rules, Prizes, if any, will be awarded to Participants with the best scores, based on the merits of the data science models submitted. See below for the complete Competition Rules. For Competitions designated as hackathons by the Competition Sponsor (“Hackathons”), your Submissions will be judged by the Competition Sponsor based on the evaluation rubric set forth on the Competition Website (“Evaluation Rubric”). The Prizes, if any, will be awarded to Participants with the highest ranking(s) as determined by the Competition Sponsor based on such rubric.
|
| 6 |
+
|
| 7 |
+
You cannot sign up to Kaggle from multiple accounts and therefore you cannot enter or submit from multiple accounts.
|
| 8 |
+
|
| 9 |
+
1. COMPETITION-SPECIFIC TERMS
|
| 10 |
+
1. COMPETITION TITLE
|
| 11 |
+
The MedGemma Impact Challenge
|
| 12 |
+
|
| 13 |
+
2. COMPETITION SPONSOR
|
| 14 |
+
Google Research
|
| 15 |
+
|
| 16 |
+
3. COMPETITION SPONSOR ADDRESS
|
| 17 |
+
1600 Amphitheatre Parkway, Mountain View, California 94043 USA
|
| 18 |
+
|
| 19 |
+
4. COMPETITION WEBSITE
|
| 20 |
+
https://www.kaggle.com/competitions/med-gemma-impact-challenge
|
| 21 |
+
|
| 22 |
+
5. TOTAL PRIZES AVAILABLE: $100,000
|
| 23 |
+
Main track: $75,000
|
| 24 |
+
|
| 25 |
+
1st Place: $30,000
|
| 26 |
+
2nd Place: $20,000
|
| 27 |
+
3rd Place: $15,000
|
| 28 |
+
4th Place: $10,000
|
| 29 |
+
Special Technology Awards: $25,000
|
| 30 |
+
|
| 31 |
+
Agentic Workflow prize: $10,000 (Two prizes of $5,000)
|
| 32 |
+
The Edge AI Prize: $5,000
|
| 33 |
+
The Novel Task Prize: $10,000 (Two prizes of $5,000)
|
| 34 |
+
6. WINNER LICENSE TYPE
|
| 35 |
+
CC BY 4.0
|
| 36 |
+
|
| 37 |
+
7. DATA ACCESS AND USE
|
| 38 |
+
No data is provided for this competition. Use of HAI-DEF and MedGemma are subject to the HAI-DEF Terms of Use.
|
| 39 |
+
|
| 40 |
+
2. COMPETITION-SPECIFIC RULES
|
| 41 |
+
In addition to the provisions of the General Competition Rules below, you understand and agree to these Competition-Specific Rules required by the Competition Sponsor:
|
| 42 |
+
|
| 43 |
+
1. TEAM LIMITS
|
| 44 |
+
The maximum Team size is five (5). b. Team mergers are allowed and can be performed by the Team leader. In order to merge, the combined Team must have a total Submission count less than or equal to the maximum allowed as of the Team Merger Deadline. The maximum allowed is the number of Submissions per day multiplied by the number of days the competition has been running. For Hackathons, each team is allowed one (1) Submission; any Submissions submitted by Participants before merging into a Team will be unsubmitted.
|
| 45 |
+
|
| 46 |
+
2. SUBMISSION LIMITS
|
| 47 |
+
For Hackathons, each Team may submit one (1) Submission. This single entry can be submitted to the main competition track, and one special technology award, so separate submissions are not required.
|
| 48 |
+
|
| 49 |
+
3. COMPETITION TIMELINE
|
| 50 |
+
Competition Timeline dates (including Entry Deadline, Final Submission Deadline, Start Date, and Team Merger Deadline, as applicable) are reflected on the competition’s Overview > Timeline page.
|
| 51 |
+
|
| 52 |
+
4. COMPETITION DATA
|
| 53 |
+
a. Data Access and Use
|
| 54 |
+
None. Competition Data will not be provided by Competition Sponsor for this Competition.
|
| 55 |
+
b. Data Security
|
| 56 |
+
You agree to use reasonable and suitable measures to prevent persons who have not formally agreed to these Rules from gaining access to the Competition Data. You agree not to transmit, duplicate, publish, redistribute or otherwise provide or make available the Competition Data to any party not participating in the Competition. You agree to notify Kaggle immediately upon learning of any possible unauthorized transmission of or unauthorized access to the Competition Data and agree to work with Kaggle to rectify any unauthorized transmission or access.
|
| 57 |
+
5. WINNER LICENSE
|
| 58 |
+
a. Under Section 2.8 (Winners Obligations) of the General Rules below, you hereby grant and will grant the Competition Sponsor the following license(s) with respect to your Submission if you are a Competition winner:
|
| 59 |
+
|
| 60 |
+
Open Source: You hereby license and will license your winning Submission and the source code used to generate the Submission to the Competition Sponsor under CC BY 4.0 that in no event limits commercial use of such code or model containing or depending on such code.
|
| 61 |
+
|
| 62 |
+
For generally commercially available software that you used to generate your Submission that is not owned by you, but that can be procured by the Competition Sponsor without undue expense, you do not need to grant the license in the preceding Section for that software.
|
| 63 |
+
|
| 64 |
+
In the event that input data or pretrained models with an incompatible license are used to generate your winning solution, you do not need to grant an open source license in the preceding Section for that data and/or model(s).
|
| 65 |
+
|
| 66 |
+
b. You may be required by the Sponsor to provide a detailed description of how the winning Submission was generated, to the Competition Sponsor’s specifications, as outlined in Section 2.8, Winner’s Obligations. This may include a detailed description of methodology, where one must be able to reproduce the approach by reading the description, and includes a detailed explanation of the architecture, preprocessing, loss function, training details, hyper-parameters, etc. The description should also include a link to a code repository with complete and detailed instructions so that the results obtained can be reproduced.
|
| 67 |
+
|
| 68 |
+
6. EXTERNAL DATA AND TOOLS
|
| 69 |
+
a. You may use data other than the Competition Data (“External Data”) to develop and test your Submissions. However, you will ensure the External Data is either publicly available and equally accessible to use by all Participants of the Competition for purposes of the competition at no cost to the other Participants, or satisfies the Reasonableness criteria as outlined in Section 2.6.b below. The ability to use External Data under this Section does not limit your other obligations under these Competition Rules, including but not limited to Section 2.8 (Winners Obligations).
|
| 70 |
+
|
| 71 |
+
b. Use of HAI-DEF and MedGemma are subject to the HAI-DEF Terms of Use
|
| 72 |
+
|
| 73 |
+
c. The use of external data and models is acceptable unless specifically prohibited by the Host. Because of the potential costs or restrictions (e.g., “geo restrictions”) associated with obtaining rights to use external data or certain software and associated tools, their use must be “reasonably accessible to all” and of “minimal cost”. Also, regardless of the cost challenges as they might affect all Participants during the course of the competition, the costs of potentially procuring a license for software used to generate a Submission, must also be considered. The Host will employ an assessment of whether or not the following criteria can exclude the use of the particular LLM, data set(s), or tool(s):
|
| 74 |
+
|
| 75 |
+
Are Participants being excluded from a competition because of the "excessive" costs for access to certain LLMs, external data, or tools that might be used by other Participants. The Host will assess the excessive cost concern by applying a “Reasonableness” standard (the “Reasonableness Standard”). The Reasonableness Standard will be determined and applied by the Host in light of things like cost thresholds and accessibility.
|
| 76 |
+
|
| 77 |
+
By way of example only, a small subscription charge to use additional elements of a large language model such as Gemini Advanced are acceptable if meeting the Reasonableness Standard of Sec. 8.2. Purchasing a license to use a proprietary dataset that exceeds the cost of a prize in the competition would not be considered reasonable.
|
| 78 |
+
|
| 79 |
+
d. Automated Machine Learning Tools (“AMLT”)
|
| 80 |
+
|
| 81 |
+
Individual Participants and Teams may use automated machine learning tool(s) (“AMLT”) (e.g., Google toML, H2O Driverless AI, etc.) to create a Submission, provided that the Participant or Team ensures that they have an appropriate license to the AMLT such that they are able to comply with the Competition Rules.
|
| 82 |
+
7. ELIGIBILITY
|
| 83 |
+
a. Unless otherwise stated in the Competition-Specific Rules above or prohibited by internal policies of the Competition Entities, employees, interns, contractors, officers and directors of Competition Entities may enter and participate in the Competition, but are not eligible to win any Prizes. "Competition Entities" means the Competition Sponsor, Kaggle Inc., and their respective parent companies, subsidiaries and affiliates. If you are such a Participant from a Competition Entity, you are subject to all applicable internal policies of your employer with respect to your participation.
|
| 84 |
+
|
| 85 |
+
8. WINNER’S OBLIGATIONS
|
| 86 |
+
a. As a condition to being awarded a Prize, a Prize winner must fulfill the following obligations:
|
| 87 |
+
|
| 88 |
+
Deliver to the Competition Sponsor the final model's software code as used to generate the winning Submission and associated documentation. The delivered software code should follow these documentation guidelines, must be capable of generating the winning Submission, and contain a description of resources required to build and/or run the executable code successfully. For avoidance of doubt, delivered software code should include training code, inference code, and a description of the required computational environment. For Hackathons, the Submission deliverables will be as described on the Competition Website, which may be information or materials that are not software code.
|
| 89 |
+
b. To the extent that the final model’s software code includes generally commercially available software that is not owned by you, but that can be procured by the Competition Sponsor without undue expense, then instead of delivering the code for that software to the Competition Sponsor, you must identify that software, method for procuring it, and any parameters or other information necessary to replicate the winning Submission; Individual Participants and Teams who create a Submission using an AMLT may win a Prize. However, for clarity, the potential winner’s Submission must still meet the requirements of these Rules, including but not limited to Section 2.5 (Winners License), Section 2.8 (Winners Obligations), and Section 3.14 (Warranty, Indemnity, and Release).”
|
| 90 |
+
|
| 91 |
+
c. Individual Participants and Teams who create a Submission using an AMLT may win a Prize. However, for clarity, the potential winner’s Submission must still meet the requirements of these Rules,
|
| 92 |
+
|
| 93 |
+
Grant to the Competition Sponsor the license to the winning Submission stated in the Competition Specific Rules above, and represent that you have the unrestricted right to grant that license;
|
| 94 |
+
|
| 95 |
+
Sign and return all Prize acceptance documents as may be required by Competition Sponsor or Kaggle, including without limitation: (a) eligibility certifications; (b) licenses, releases and other agreements required under the Rules; and (c) U.S. tax forms (such as IRS Form W-9 if U.S. resident, IRS Form W-8BEN if foreign resident, or future equivalents).
|
| 96 |
+
|
| 97 |
+
9. GOVERNING LAW
|
| 98 |
+
a. Unless otherwise provided in the Competition Specific Rules above, all claims arising out of or relating to these Rules will be governed by California law, excluding its conflict of laws rules, and will be litigated exclusively in the Federal or State courts of Santa Clara County, California, USA. The parties consent to personal jurisdiction in those courts. If any provision of these Rules is held to be invalid or unenforceable, all remaining provisions of the Rules will remain in full force and effect.
|
| 99 |
+
|
| 100 |
+
3. GENERAL COMPETITION RULES - BINDING AGREEMENT
|
| 101 |
+
1. ELIGIBILITY
|
| 102 |
+
a. To be eligible to enter the Competition, you must be:
|
| 103 |
+
|
| 104 |
+
a registered account holder at Kaggle.com;
|
| 105 |
+
the older of 18 years old or the age of majority in your jurisdiction of residence (unless otherwise agreed to by Competition Sponsor and appropriate parental/guardian consents have been obtained by Competition Sponsor);
|
| 106 |
+
not a resident of Crimea, so-called Donetsk People's Republic (DNR) or Luhansk People's Republic (LNR), Cuba, Iran, Syria, or North Korea; and
|
| 107 |
+
not a person or representative of an entity under U.S. export controls or sanctions (see: https://www.treasury.gov/resourcecenter/sanctions/Programs/Pages/Programs.aspx).
|
| 108 |
+
b. Competitions are open to residents of the United States and worldwide, except that if you are a resident of Crimea, so-called Donetsk People's Republic (DNR) or Luhansk People's Republic (LNR), Cuba, Iran, Syria, North Korea, or are subject to U.S. export controls or sanctions, you may not enter the Competition. Other local rules and regulations may apply to you, so please check your local laws to ensure that you are eligible to participate in skills-based competitions. The Competition Host reserves the right to forego or award alternative Prizes where needed to comply with local laws. If a winner is located in a country where prizes cannot be awarded, then they are not eligible to receive a prize.
|
| 109 |
+
|
| 110 |
+
c. If you are entering as a representative of a company, educational institution or other legal entity, or on behalf of your employer, these rules are binding on you, individually, and the entity you represent or where you are an employee. If you are acting within the scope of your employment, or as an agent of another party, you warrant that such party or your employer has full knowledge of your actions and has consented thereto, including your potential receipt of a Prize. You further warrant that your actions do not violate your employer's or entity's policies and procedures.
|
| 111 |
+
|
| 112 |
+
d. The Competition Sponsor reserves the right to verify eligibility and to adjudicate on any dispute at any time. If you provide any false information relating to the Competition concerning your identity, residency, mailing address, telephone number, email address, ownership of right, or information required for entering the Competition, you may be immediately disqualified from the Competition.
|
| 113 |
+
|
| 114 |
+
2. SPONSOR AND HOSTING PLATFORM
|
| 115 |
+
a. The Competition is sponsored by Competition Sponsor named above. The Competition is hosted on behalf of Competition Sponsor by Kaggle Inc. ("Kaggle"). Kaggle is an independent contractor of Competition Sponsor, and is not a party to this or any agreement between you and Competition Sponsor. You understand that Kaggle has no responsibility with respect to selecting the potential Competition winner(s) or awarding any Prizes. Kaggle will perform certain administrative functions relating to hosting the Competition, and you agree to abide by the provisions relating to Kaggle under these Rules. As a Kaggle.com account holder and user of the Kaggle competition platform, remember you have accepted and are subject to the Kaggle Terms of Service at www.kaggle.com/terms in addition to these Rules.
|
| 116 |
+
|
| 117 |
+
3. COMPETITION PERIOD
|
| 118 |
+
a. For the purposes of Prizes, the Competition will run from the Start Date and time to the Final Submission Deadline (such duration the “Competition Period”). The Competition Timeline is subject to change, and Competition Sponsor may introduce additional hurdle deadlines during the Competition Period. Any updated or additional deadlines will be publicized on the Competition Website. It is your responsibility to check the Competition Website regularly to stay informed of any deadline changes. YOU ARE RESPONSIBLE FOR DETERMINING THE CORRESPONDING TIME ZONE IN YOUR LOCATION.
|
| 119 |
+
|
| 120 |
+
4. COMPETITION ENTRY
|
| 121 |
+
a. NO PURCHASE NECESSARY TO ENTER OR WIN. To enter the Competition, you must register on the Competition Website prior to the Entry Deadline, and follow the instructions for developing and entering your Submission through the Competition Website. Your Submissions must be made in the manner and format, and in compliance with all other requirements, stated on the Competition Website (the "Requirements"). Submissions must be received before any Submission deadlines stated on the Competition Website. Submissions not received by the stated deadlines will not be eligible to receive a Prize. b. Except as expressly allowed in Hackathons as set forth on the Competition Website, submissions may not use or incorporate information from hand labeling or human prediction of the validation dataset or test data records. c. If the Competition is a multi-stage competition with temporally separate training and/or test data, one or more valid Submissions may be required during each Competition stage in the manner described on the Competition Website in order for the Submissions to be Prize eligible. d. Submissions are void if they are in whole or part illegible, incomplete, damaged, altered, counterfeit, obtained through fraud, or late. Competition Sponsor reserves the right to disqualify any entrant who does not follow these Rules, including making a Submission that does not meet the Requirements.
|
| 122 |
+
|
| 123 |
+
5. INDIVIDUALS AND TEAMS
|
| 124 |
+
a. Individual Account. You may make Submissions only under one, unique Kaggle.com account. You will be disqualified if you make Submissions through more than one Kaggle account, or attempt to falsify an account to act as your proxy. You may submit up to the maximum number of Submissions per day as specified on the Competition Website. b. Teams. If permitted under the Competition Website guidelines, multiple individuals may collaborate as a Team; however, you may join or form only one Team. Each Team member must be a single individual with a separate Kaggle account. You must register individually for the Competition before joining a Team. You must confirm your Team membership to make it official by responding to the Team notification message sent to your Kaggle account. Team membership may not exceed the Maximum Team Size stated on the Competition Website. c. Team Merger. Teams (or individual Participants) may request to merge via the Competition Website. Team mergers may be allowed provided that: (i) the combined Team does not exceed the Maximum Team Size; (ii) the number of Submissions made by the merging Teams does not exceed the number of Submissions permissible for one Team at the date of the merger request; (iii) the merger is completed before the earlier of: any merger deadline or the Competition deadline; and (iv) the proposed combined Team otherwise meets all the requirements of these Rules. d. Private Sharing. No private sharing outside of Teams. Privately sharing code or data outside of Teams is not permitted. It's okay to share code if made available to all Participants on the forums.
|
| 125 |
+
|
| 126 |
+
6. SUBMISSION CODE REQUIREMENTS
|
| 127 |
+
a. Private Code Sharing. Unless otherwise specifically permitted under the Competition Website or Competition Specific Rules above, during the Competition Period, you are not allowed to privately share source or executable code developed in connection with or based upon the Competition Data or other source or executable code relevant to the Competition (“Competition Code”). This prohibition includes sharing Competition Code between separate Teams, unless a Team merger occurs. Any such sharing of Competition Code is a breach of these Competition Rules and may result in disqualification. b. Public Code Sharing. You are permitted to publicly share Competition Code, provided that such public sharing does not violate the intellectual property rights of any third party. If you do choose to share Competition Code or other such code, you are required to share it on Kaggle.com on the discussion forum or notebooks associated specifically with the Competition for the benefit of all competitors. By so sharing, you are deemed to have licensed the shared code under an Open Source Initiative-approved license (see www.opensource.org) that in no event limits commercial use of such Competition Code or model containing or depending on such Competition Code. c. Use of Open Source. Unless otherwise stated in the Specific Competition Rules above, if open source code is used in the model to generate the Submission, then you must only use open source code licensed under an Open Source Initiative-approved license (see www.opensource.org) that in no event limits commercial use of such code or model containing or depending on such code.
|
| 128 |
+
|
| 129 |
+
7. DETERMINING WINNERS
|
| 130 |
+
a. Each Submission will be scored and/or ranked by the evaluation metric, or Evaluation Rubric (in the case of Hackathon Competitions),stated on the Competition Website. During the Competition Period, the current ranking will be visible on the Competition Website's Public Leaderboard. The potential winner(s) are determined solely by the leaderboard ranking on the Private Leaderboard, subject to compliance with these Rules. The Public Leaderboard will be based on the public test set and the Private Leaderboard will be based on the private test set. There will be no leaderboards for Hackathon Competitions. b. In the event of a tie, the Submission that was entered first to the Competition will be the winner. In the event a potential winner is disqualified for any reason, the Submission that received the next highest score rank will be chosen as the potential winner. For Hackathon Competitions, each of the top Submissions will get a unique ranking and there will be no tiebreakers.
|
| 131 |
+
|
| 132 |
+
8. NOTIFICATION OF WINNERS & DISQUALIFICATION
|
| 133 |
+
a. The potential winner(s) will be notified by email. b. If a potential winner (i) does not respond to the notification attempt within one (1) week from the first notification attempt or (ii) notifies Kaggle within one week after the Final Submission Deadline that the potential winner does not want to be nominated as a winner or does not want to receive a Prize, then, in each case (i) and (ii) such potential winner will not receive any Prize, and an alternate potential winner will be selected from among all eligible entries received based on the Competition’s judging criteria. c. In case (i) and (ii) above Kaggle may disqualify the Participant. However, in case (ii) above, if requested by Kaggle, such potential winner may provide code and documentation to verify the Participant’s compliance with these Rules. If the potential winner provides code and documentation to the satisfaction of Kaggle, the Participant will not be disqualified pursuant to this paragraph. d. Competition Sponsor reserves the right to disqualify any Participant from the Competition if the Competition Sponsor reasonably believes that the Participant has attempted to undermine the legitimate operation of the Competition by cheating, deception, or other unfair playing practices or abuses, threatens or harasses any other Participants, Competition Sponsor or Kaggle. e. A disqualified Participant may be removed from the Competition leaderboard, at Kaggle's sole discretion. If a Participant is removed from the Competition Leaderboard, additional winning features associated with the Kaggle competition platform, for example Kaggle points or medals, may also not be awarded. f. The final leaderboard list will be publicly displayed at Kaggle.com. Determinations of Competition Sponsor are final and binding.
|
| 134 |
+
|
| 135 |
+
9. PRIZES
|
| 136 |
+
a. Prize(s) are as described on the Competition Website and are only available for winning during the time period described on the Competition Website. The odds of winning any Prize depends on the number of eligible Submissions received during the Competition Period and the skill of the Participants. b. All Prizes are subject to Competition Sponsor's review and verification of the Participant’s eligibility and compliance with these Rules, and the compliance of the winning Submissions with the Submissions Requirements. In the event that the Submission demonstrates non-compliance with these Competition Rules, Competition Sponsor may at its discretion take either of the following actions: (i) disqualify the Submission(s); or (ii) require the potential winner to remediate within one week after notice all issues identified in the Submission(s) (including, without limitation, the resolution of license conflicts, the fulfillment of all obligations required by software licenses, and the removal of any software that violates the software restrictions). c. A potential winner may decline to be nominated as a Competition winner in accordance with Section 3.8. d. Potential winners must return all required Prize acceptance documents within two (2) weeks following notification of such required documents, or such potential winner will be deemed to have forfeited the prize and another potential winner will be selected. Prize(s) will be awarded within approximately thirty (30) days after receipt by Competition Sponsor or Kaggle of the required Prize acceptance documents. Transfer or assignment of a Prize is not allowed. e. You are not eligible to receive any Prize if you do not meet the Eligibility requirements in Section 2.7 and Section 3.1 above. f. If a Team wins a monetary Prize, the Prize money will be allocated in even shares between the eligible Team members, unless the Team unanimously opts for a different Prize split and notifies Kaggle before Prizes are issued.
|
| 137 |
+
|
| 138 |
+
10. TAXES
|
| 139 |
+
a. ALL TAXES IMPOSED ON PRIZES ARE THE SOLE RESPONSIBILITY OF THE WINNERS. Payments to potential winners are subject to the express requirement that they submit all documentation requested by Competition Sponsor or Kaggle for compliance with applicable state, federal, local and foreign (including provincial) tax reporting and withholding requirements. Prizes will be net of any taxes that Competition Sponsor is required by law to withhold. If a potential winner fails to provide any required documentation or comply with applicable laws, the Prize may be forfeited and Competition Sponsor may select an alternative potential winner. Any winners who are U.S. residents will receive an IRS Form-1099 in the amount of their Prize.
|
| 140 |
+
|
| 141 |
+
11. GENERAL CONDITIONS
|
| 142 |
+
a. All federal, state, provincial and local laws and regulations apply.
|
| 143 |
+
|
| 144 |
+
12. PUBLICITY
|
| 145 |
+
a. You agree that Competition Sponsor, Kaggle and its affiliates may use your name and likeness for advertising and promotional purposes without additional compensation, unless prohibited by law.
|
| 146 |
+
|
| 147 |
+
13. PRIVACY
|
| 148 |
+
a. You acknowledge and agree that Competition Sponsor and Kaggle may collect, store, share and otherwise use personally identifiable information provided by you during the Kaggle account registration process and the Competition, including but not limited to, name, mailing address, phone number, and email address (“Personal Information”). Kaggle acts as an independent controller with regard to its collection, storage, sharing, and other use of this Personal Information, and will use this Personal Information in accordance with its Privacy Policy <www.kaggle.com/privacy>, including for administering the Competition. As a Kaggle.com account holder, you have the right to request access to, review, rectification, portability or deletion of any personal data held by Kaggle about you by logging into your account and/or contacting Kaggle Support at <www.kaggle.com/contact>. b. As part of Competition Sponsor performing this contract between you and the Competition Sponsor, Kaggle will transfer your Personal Information to Competition Sponsor, which acts as an independent controller with regard to this Personal Information. As a controller of such Personal Information, Competition Sponsor agrees to comply with all U.S. and foreign data protection obligations with regard to your Personal Information. Kaggle will transfer your Personal Information to Competition Sponsor in the country specified in the Competition Sponsor Address listed above, which may be a country outside the country of your residence. Such country may not have privacy laws and regulations similar to those of the country of your residence.
|
| 149 |
+
|
| 150 |
+
14. WARRANTY, INDEMNITY AND RELEASE
|
| 151 |
+
a. You warrant that your Submission is your own original work and, as such, you are the sole and exclusive owner and rights holder of the Submission, and you have the right to make the Submission and grant all required licenses. You agree not to make any Submission that: (i) infringes any third party proprietary rights, intellectual property rights, industrial property rights, personal or moral rights or any other rights, including without limitation, copyright, trademark, patent, trade secret, privacy, publicity or confidentiality obligations, or defames any person; or (ii) otherwise violates any applicable U.S. or foreign state or federal law. b. To the maximum extent permitted by law, you indemnify and agree to keep indemnified Competition Entities at all times from and against any liability, claims, demands, losses, damages, costs and expenses resulting from any of your acts, defaults or omissions and/or a breach of any warranty set forth herein. To the maximum extent permitted by law, you agree to defend, indemnify and hold harmless the Competition Entities from and against any and all claims, actions, suits or proceedings, as well as any and all losses, liabilities, damages, costs and expenses (including reasonable attorneys fees) arising out of or accruing from: (a) your Submission or other material uploaded or otherwise provided by you that infringes any third party proprietary rights, intellectual property rights, industrial property rights, personal or moral rights or any other rights, including without limitation, copyright, trademark, patent, trade secret, privacy, publicity or confidentiality obligations, or defames any person; (b) any misrepresentation made by you in connection with the Competition; (c) any non-compliance by you with these Rules or any applicable U.S. or foreign state or federal law; (d) claims brought by persons or entities other than the parties to these Rules arising from or related to your involvement with the Competition; and (e) your acceptance, possession, misuse or use of any Prize, or your participation in the Competition and any Competition-related activity. c. You hereby release Competition Entities from any liability associated with: (a) any malfunction or other problem with the Competition Website; (b) any error in the collection, processing, or retention of any Submission; or (c) any typographical or other error in the printing, offering or announcement of any Prize or winners.
|
| 152 |
+
|
| 153 |
+
15. INTERNET
|
| 154 |
+
a. Competition Entities are not responsible for any malfunction of the Competition Website or any late, lost, damaged, misdirected, incomplete, illegible, undeliverable, or destroyed Submissions or entry materials due to system errors, failed, incomplete or garbled computer or other telecommunication transmission malfunctions, hardware or software failures of any kind, lost or unavailable network connections, typographical or system/human errors and failures, technical malfunction(s) of any telephone network or lines, cable connections, satellite transmissions, servers or providers, or computer equipment, traffic congestion on the Internet or at the Competition Website, or any combination thereof, which may limit a Participant’s ability to participate.
|
| 155 |
+
|
| 156 |
+
16. RIGHT TO CANCEL, MODIFY OR DISQUALIFY
|
| 157 |
+
a. If for any reason the Competition is not capable of running as planned, including infection by computer virus, bugs, tampering, unauthorized intervention, fraud, technical failures, or any other causes which corrupt or affect the administration, security, fairness, integrity, or proper conduct of the Competition, Competition Sponsor reserves the right to cancel, terminate, modify or suspend the Competition. Competition Sponsor further reserves the right to disqualify any Participant who tampers with the submission process or any other part of the Competition or Competition Website. Any attempt by a Participant to deliberately damage any website, including the Competition Website, or undermine the legitimate operation of the Competition is a violation of criminal and civil laws. Should such an attempt be made, Competition Sponsor and Kaggle each reserves the right to seek damages from any such Participant to the fullest extent of the applicable law.
|
| 158 |
+
|
| 159 |
+
17. NOT AN OFFER OR CONTRACT OF EMPLOYMENT
|
| 160 |
+
a. Under no circumstances will the entry of a Submission, the awarding of a Prize, or anything in these Rules be construed as an offer or contract of employment with Competition Sponsor or any of the Competition Entities. You acknowledge that you have submitted your Submission voluntarily and not in confidence or in trust. You acknowledge that no confidential, fiduciary, agency, employment or other similar relationship is created between you and Competition Sponsor or any of the Competition Entities by your acceptance of these Rules or your entry of your Submission.
|
| 161 |
+
|
| 162 |
+
18. DEFINITIONS
|
| 163 |
+
a. "Competition Data" are the data or datasets available from the Competition Website for the purpose of use in the Competition, including any prototype or executable code provided on the Competition Website. The Competition Data will contain private and public test sets. Which data belongs to which set will not be made available to Participants. b. An “Entry” is when a Participant has joined, signed up, or accepted the rules of a competition. Entry is required to make a Submission to a competition. c. A “Final Submission” is the Submission selected by the user, or automatically selected by Kaggle in the event not selected by the user, that is/are used for final placement on the competition leaderboard. d. A “Participant” or “Participant User” is an individual who participates in a competition by entering the competition and making a Submission. e. The “Private Leaderboard” is a ranked display of Participants’ Submission scores against the private test set. The Private Leaderboard determines the final standing in the competition. f. The “Public Leaderboard” is a ranked display of Participants’ Submission scores against a representative sample of the test data. This leaderboard is visible throughout the competition. g. A “Sponsor” is responsible for hosting the competition, which includes but is not limited to providing the data for the competition, determining winners, and enforcing competition rules. h. A “Submission” is anything provided by the Participant to the Sponsor to be evaluated for competition purposes and determine leaderboard position. A Submission may be made as a model, notebook, prediction file, or other format as determined by the Sponsor. i. A “Team” is one or more Participants participating together in a Kaggle competition, by officially merging together as a Team within the competition platform.
|
src/.gitkeep
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Source Code
|
| 2 |
+
|
| 3 |
+
Place your application source code here.
|
src/backend/.env.template
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================
|
| 2 |
+
# MedGemma CDS Agent - Environment Configuration
|
| 3 |
+
# ============================================================
|
| 4 |
+
# Copy this file to .env and fill in the values
|
| 5 |
+
|
| 6 |
+
# --- MedGemma Configuration ---
|
| 7 |
+
# For API mode (OpenAI-compatible endpoint):
|
| 8 |
+
MEDGEMMA_API_KEY=your_api_key_here
|
| 9 |
+
MEDGEMMA_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
|
| 10 |
+
MEDGEMMA_MODEL_ID=google/medgemma-27b-text-it
|
| 11 |
+
|
| 12 |
+
# For local mode (Hugging Face transformers):
|
| 13 |
+
# MEDGEMMA_DEVICE=cuda # or cpu, mps
|
| 14 |
+
# MEDGEMMA_MODEL_ID=google/medgemma-4b-it
|
| 15 |
+
|
| 16 |
+
# --- External API Configuration ---
|
| 17 |
+
# OpenFDA (no key required for basic use, add for higher rate limits)
|
| 18 |
+
# OPENFDA_API_KEY=
|
| 19 |
+
|
| 20 |
+
# --- RAG Configuration ---
|
| 21 |
+
CHROMA_PERSIST_DIR=./data/chroma
|
| 22 |
+
EMBEDDING_MODEL=all-MiniLM-L6-v2
|
| 23 |
+
|
| 24 |
+
# --- Agent Configuration ---
|
| 25 |
+
AGENT_MAX_RETRIES=2
|
| 26 |
+
AGENT_TIMEOUT_SECONDS=120
|
| 27 |
+
AGENT_MAX_STEPS=10
|
| 28 |
+
DEFAULT_INCLUDE_DRUG_CHECK=true
|
| 29 |
+
DEFAULT_INCLUDE_GUIDELINES=true
|
src/backend/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Backend app package
|
src/backend/app/agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Agent package
|
src/backend/app/agent/orchestrator.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent Orchestrator — the brain of the CDS Agent.
|
| 3 |
+
|
| 4 |
+
Controls the multi-step pipeline:
|
| 5 |
+
1. Parse patient data
|
| 6 |
+
2. Clinical reasoning (MedGemma)
|
| 7 |
+
3. Drug interaction check
|
| 8 |
+
4. Guideline retrieval (RAG)
|
| 9 |
+
5. Synthesis (MedGemma)
|
| 10 |
+
|
| 11 |
+
Each step is a tool call. The orchestrator manages state, handles errors,
|
| 12 |
+
and streams step updates to the frontend via a callback.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import asyncio
|
| 17 |
+
import time
|
| 18 |
+
import uuid
|
| 19 |
+
from datetime import datetime
|
| 20 |
+
from typing import AsyncGenerator, Callable, Optional
|
| 21 |
+
|
| 22 |
+
from app.models.schemas import (
|
| 23 |
+
AgentState,
|
| 24 |
+
AgentStep,
|
| 25 |
+
AgentStepStatus,
|
| 26 |
+
CaseSubmission,
|
| 27 |
+
CDSReport,
|
| 28 |
+
)
|
| 29 |
+
from app.tools.patient_parser import PatientParserTool
|
| 30 |
+
from app.tools.clinical_reasoning import ClinicalReasoningTool
|
| 31 |
+
from app.tools.drug_interactions import DrugInteractionTool
|
| 32 |
+
from app.tools.guideline_retrieval import GuidelineRetrievalTool
|
| 33 |
+
from app.tools.synthesis import SynthesisTool
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# Type for the callback that streams step updates
|
| 37 |
+
StepCallback = Callable[[AgentStep], None]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class Orchestrator:
|
| 41 |
+
"""
|
| 42 |
+
Orchestrates the clinical decision support agent pipeline.
|
| 43 |
+
|
| 44 |
+
Usage:
|
| 45 |
+
orchestrator = Orchestrator()
|
| 46 |
+
async for step_update in orchestrator.run(case):
|
| 47 |
+
# stream step_update to frontend
|
| 48 |
+
...
|
| 49 |
+
result = orchestrator.get_result()
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self):
|
| 53 |
+
# Initialize tools
|
| 54 |
+
self.patient_parser = PatientParserTool()
|
| 55 |
+
self.clinical_reasoning = ClinicalReasoningTool()
|
| 56 |
+
self.drug_interaction = DrugInteractionTool()
|
| 57 |
+
self.guideline_retrieval = GuidelineRetrievalTool()
|
| 58 |
+
self.synthesis = SynthesisTool()
|
| 59 |
+
|
| 60 |
+
# State
|
| 61 |
+
self._state: Optional[AgentState] = None
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def state(self) -> Optional[AgentState]:
|
| 65 |
+
return self._state
|
| 66 |
+
|
| 67 |
+
def _create_steps(self, case: CaseSubmission) -> list[AgentStep]:
|
| 68 |
+
"""Define the pipeline steps based on the case configuration."""
|
| 69 |
+
steps = [
|
| 70 |
+
AgentStep(
|
| 71 |
+
step_id="parse",
|
| 72 |
+
step_name="Parsing Patient Data",
|
| 73 |
+
tool_name="patient_parser",
|
| 74 |
+
),
|
| 75 |
+
AgentStep(
|
| 76 |
+
step_id="reason",
|
| 77 |
+
step_name="Clinical Reasoning",
|
| 78 |
+
tool_name="clinical_reasoning",
|
| 79 |
+
),
|
| 80 |
+
]
|
| 81 |
+
if case.include_drug_check:
|
| 82 |
+
steps.append(
|
| 83 |
+
AgentStep(
|
| 84 |
+
step_id="drugs",
|
| 85 |
+
step_name="Drug Interaction Check",
|
| 86 |
+
tool_name="drug_interactions",
|
| 87 |
+
)
|
| 88 |
+
)
|
| 89 |
+
if case.include_guidelines:
|
| 90 |
+
steps.append(
|
| 91 |
+
AgentStep(
|
| 92 |
+
step_id="guidelines",
|
| 93 |
+
step_name="Guideline Retrieval",
|
| 94 |
+
tool_name="guideline_retrieval",
|
| 95 |
+
)
|
| 96 |
+
)
|
| 97 |
+
steps.append(
|
| 98 |
+
AgentStep(
|
| 99 |
+
step_id="synthesize",
|
| 100 |
+
step_name="Synthesizing Report",
|
| 101 |
+
tool_name="synthesis",
|
| 102 |
+
)
|
| 103 |
+
)
|
| 104 |
+
return steps
|
| 105 |
+
|
| 106 |
+
async def run(self, case: CaseSubmission) -> AsyncGenerator[AgentStep, None]:
|
| 107 |
+
"""
|
| 108 |
+
Run the full agent pipeline. Yields step updates as they happen.
|
| 109 |
+
|
| 110 |
+
This is the main entry point. Each step is executed sequentially,
|
| 111 |
+
with state flowing from one step to the next. Steps that don't
|
| 112 |
+
depend on each other (drug check + guidelines) run in parallel.
|
| 113 |
+
"""
|
| 114 |
+
case_id = str(uuid.uuid4())[:8]
|
| 115 |
+
steps = self._create_steps(case)
|
| 116 |
+
|
| 117 |
+
self._state = AgentState(
|
| 118 |
+
case_id=case_id,
|
| 119 |
+
steps=steps,
|
| 120 |
+
started_at=datetime.utcnow(),
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
# ── Step 1: Parse patient data ──
|
| 125 |
+
yield await self._run_step("parse", self._step_parse, case.patient_text)
|
| 126 |
+
|
| 127 |
+
# ── Step 2: Clinical reasoning ──
|
| 128 |
+
yield await self._run_step("reason", self._step_reason)
|
| 129 |
+
|
| 130 |
+
# ── Step 3 & 4: Drug check + Guidelines (parallel) ──
|
| 131 |
+
parallel_tasks = []
|
| 132 |
+
if case.include_drug_check:
|
| 133 |
+
parallel_tasks.append(("drugs", self._step_drug_check))
|
| 134 |
+
if case.include_guidelines:
|
| 135 |
+
parallel_tasks.append(("guidelines", self._step_guidelines))
|
| 136 |
+
|
| 137 |
+
if parallel_tasks:
|
| 138 |
+
results = await asyncio.gather(
|
| 139 |
+
*[self._run_step(sid, fn) for sid, fn in parallel_tasks],
|
| 140 |
+
return_exceptions=True,
|
| 141 |
+
)
|
| 142 |
+
for result in results:
|
| 143 |
+
if isinstance(result, Exception):
|
| 144 |
+
# Log but don't fail — graceful degradation
|
| 145 |
+
pass
|
| 146 |
+
else:
|
| 147 |
+
yield result
|
| 148 |
+
|
| 149 |
+
# ── Step 5: Synthesis ──
|
| 150 |
+
yield await self._run_step("synthesize", self._step_synthesize)
|
| 151 |
+
|
| 152 |
+
self._state.completed_at = datetime.utcnow()
|
| 153 |
+
|
| 154 |
+
except Exception as e:
|
| 155 |
+
# Mark remaining steps as failed
|
| 156 |
+
for step in self._state.steps:
|
| 157 |
+
if step.status == AgentStepStatus.PENDING:
|
| 158 |
+
step.status = AgentStepStatus.FAILED
|
| 159 |
+
step.error = f"Pipeline aborted: {str(e)}"
|
| 160 |
+
raise
|
| 161 |
+
|
| 162 |
+
async def _run_step(self, step_id: str, fn, *args) -> AgentStep:
|
| 163 |
+
"""Execute a single step, tracking status and timing."""
|
| 164 |
+
step = self._get_step(step_id)
|
| 165 |
+
step.status = AgentStepStatus.RUNNING
|
| 166 |
+
start = time.monotonic()
|
| 167 |
+
|
| 168 |
+
try:
|
| 169 |
+
await fn(*args)
|
| 170 |
+
step.status = AgentStepStatus.COMPLETED
|
| 171 |
+
except Exception as e:
|
| 172 |
+
step.status = AgentStepStatus.FAILED
|
| 173 |
+
step.error = str(e)
|
| 174 |
+
finally:
|
| 175 |
+
step.duration_ms = int((time.monotonic() - start) * 1000)
|
| 176 |
+
|
| 177 |
+
return step
|
| 178 |
+
|
| 179 |
+
def _get_step(self, step_id: str) -> AgentStep:
|
| 180 |
+
for step in self._state.steps:
|
| 181 |
+
if step.step_id == step_id:
|
| 182 |
+
return step
|
| 183 |
+
raise ValueError(f"Unknown step: {step_id}")
|
| 184 |
+
|
| 185 |
+
# ──────────────────────────────────────────────
|
| 186 |
+
# Step implementations
|
| 187 |
+
# ──────────────────────────────────────────────
|
| 188 |
+
|
| 189 |
+
async def _step_parse(self, patient_text: str):
|
| 190 |
+
"""Step 1: Parse raw patient text into structured profile."""
|
| 191 |
+
profile = await self.patient_parser.run(patient_text)
|
| 192 |
+
self._state.patient_profile = profile
|
| 193 |
+
|
| 194 |
+
step = self._get_step("parse")
|
| 195 |
+
step.input_summary = patient_text[:100] + "..." if len(patient_text) > 100 else patient_text
|
| 196 |
+
step.output_summary = f"Parsed: {profile.chief_complaint}, {len(profile.current_medications)} meds, {len(profile.lab_results)} labs"
|
| 197 |
+
|
| 198 |
+
async def _step_reason(self):
|
| 199 |
+
"""Step 2: Clinical reasoning over the structured patient profile."""
|
| 200 |
+
if not self._state.patient_profile:
|
| 201 |
+
raise RuntimeError("Patient profile not available — parse step must run first")
|
| 202 |
+
|
| 203 |
+
result = await self.clinical_reasoning.run(self._state.patient_profile)
|
| 204 |
+
self._state.clinical_reasoning = result
|
| 205 |
+
|
| 206 |
+
step = self._get_step("reason")
|
| 207 |
+
step.output_summary = (
|
| 208 |
+
f"{len(result.differential_diagnosis)} diagnoses, "
|
| 209 |
+
f"{len(result.recommended_workup)} recommendations"
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
async def _step_drug_check(self):
|
| 213 |
+
"""Step 3: Check drug interactions for current + proposed medications."""
|
| 214 |
+
if not self._state.patient_profile:
|
| 215 |
+
raise RuntimeError("Patient profile not available")
|
| 216 |
+
|
| 217 |
+
meds = self._state.patient_profile.current_medications
|
| 218 |
+
# Also include any medications proposed by the reasoning step
|
| 219 |
+
proposed_meds = []
|
| 220 |
+
if self._state.clinical_reasoning:
|
| 221 |
+
for action in self._state.clinical_reasoning.recommended_workup:
|
| 222 |
+
if "medication" in action.action.lower() or "prescribe" in action.action.lower():
|
| 223 |
+
proposed_meds.append(action.action)
|
| 224 |
+
|
| 225 |
+
result = await self.drug_interaction.run(meds, proposed_meds)
|
| 226 |
+
self._state.drug_interactions = result
|
| 227 |
+
|
| 228 |
+
step = self._get_step("drugs")
|
| 229 |
+
step.output_summary = f"{len(result.interactions_found)} interactions found"
|
| 230 |
+
|
| 231 |
+
async def _step_guidelines(self):
|
| 232 |
+
"""Step 4: Retrieve relevant clinical guidelines via RAG."""
|
| 233 |
+
if not self._state.clinical_reasoning:
|
| 234 |
+
raise RuntimeError("Clinical reasoning not available")
|
| 235 |
+
|
| 236 |
+
# Build query from the top diagnosis
|
| 237 |
+
top_dx = self._state.clinical_reasoning.differential_diagnosis
|
| 238 |
+
if top_dx:
|
| 239 |
+
query = f"{top_dx[0].diagnosis} clinical guidelines management"
|
| 240 |
+
else:
|
| 241 |
+
query = self._state.patient_profile.chief_complaint + " clinical guidelines"
|
| 242 |
+
|
| 243 |
+
result = await self.guideline_retrieval.run(query)
|
| 244 |
+
self._state.guideline_retrieval = result
|
| 245 |
+
|
| 246 |
+
step = self._get_step("guidelines")
|
| 247 |
+
step.output_summary = f"{len(result.excerpts)} guideline excerpts retrieved"
|
| 248 |
+
|
| 249 |
+
async def _step_synthesize(self):
|
| 250 |
+
"""Step 5: Synthesize all tool outputs into a final CDS report."""
|
| 251 |
+
report = await self.synthesis.run(
|
| 252 |
+
patient_profile=self._state.patient_profile,
|
| 253 |
+
clinical_reasoning=self._state.clinical_reasoning,
|
| 254 |
+
drug_interactions=self._state.drug_interactions,
|
| 255 |
+
guideline_retrieval=self._state.guideline_retrieval,
|
| 256 |
+
)
|
| 257 |
+
self._state.final_report = report
|
| 258 |
+
|
| 259 |
+
step = self._get_step("synthesize")
|
| 260 |
+
step.output_summary = "Clinical Decision Support report generated"
|
| 261 |
+
|
| 262 |
+
def get_result(self) -> Optional[CDSReport]:
|
| 263 |
+
"""Return the final report, if synthesis completed."""
|
| 264 |
+
if self._state:
|
| 265 |
+
return self._state.final_report
|
| 266 |
+
return None
|
src/backend/app/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# API routes package
|
src/backend/app/api/cases.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
REST API for case submission and result retrieval.
|
| 3 |
+
"""
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import uuid
|
| 8 |
+
from typing import Dict
|
| 9 |
+
|
| 10 |
+
from fastapi import APIRouter, HTTPException
|
| 11 |
+
|
| 12 |
+
from app.agent.orchestrator import Orchestrator
|
| 13 |
+
from app.models.schemas import (
|
| 14 |
+
AgentState,
|
| 15 |
+
CaseResponse,
|
| 16 |
+
CaseResult,
|
| 17 |
+
CaseSubmission,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
router = APIRouter()
|
| 21 |
+
|
| 22 |
+
# In-memory store for active/completed cases
|
| 23 |
+
# In production, use Redis or a database
|
| 24 |
+
_cases: Dict[str, Orchestrator] = {}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@router.post("/submit", response_model=CaseResponse)
|
| 28 |
+
async def submit_case(case: CaseSubmission):
|
| 29 |
+
"""
|
| 30 |
+
Submit a patient case for analysis.
|
| 31 |
+
|
| 32 |
+
The agent pipeline runs asynchronously. Use the WebSocket endpoint
|
| 33 |
+
or poll /api/cases/{case_id} for real-time updates.
|
| 34 |
+
"""
|
| 35 |
+
orchestrator = Orchestrator()
|
| 36 |
+
|
| 37 |
+
# Generate a case_id upfront so we can return it immediately
|
| 38 |
+
case_id = str(uuid.uuid4())[:8]
|
| 39 |
+
|
| 40 |
+
async def _run_pipeline():
|
| 41 |
+
async for _step in orchestrator.run(case):
|
| 42 |
+
pass # Steps are tracked in orchestrator state
|
| 43 |
+
# Once run() creates state, store the orchestrator under the real case_id
|
| 44 |
+
if orchestrator.state:
|
| 45 |
+
_cases[orchestrator.state.case_id] = orchestrator
|
| 46 |
+
|
| 47 |
+
asyncio.create_task(_run_pipeline())
|
| 48 |
+
|
| 49 |
+
# Wait briefly for the orchestrator to initialise its state
|
| 50 |
+
await asyncio.sleep(0.15)
|
| 51 |
+
|
| 52 |
+
# Use the orchestrator's actual case_id if available, otherwise the pre-generated one
|
| 53 |
+
actual_id = orchestrator.state.case_id if orchestrator.state else case_id
|
| 54 |
+
_cases[actual_id] = orchestrator
|
| 55 |
+
|
| 56 |
+
return CaseResponse(
|
| 57 |
+
case_id=actual_id,
|
| 58 |
+
status="running",
|
| 59 |
+
message="Agent pipeline started. Connect to WebSocket for real-time updates.",
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@router.get("/{case_id}", response_model=CaseResult)
|
| 64 |
+
async def get_case(case_id: str):
|
| 65 |
+
"""Get the current state and results for a case."""
|
| 66 |
+
orchestrator = _cases.get(case_id)
|
| 67 |
+
if not orchestrator or not orchestrator.state:
|
| 68 |
+
raise HTTPException(status_code=404, detail=f"Case {case_id} not found")
|
| 69 |
+
|
| 70 |
+
return CaseResult(
|
| 71 |
+
case_id=case_id,
|
| 72 |
+
state=orchestrator.state,
|
| 73 |
+
report=orchestrator.get_result(),
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@router.get("/", response_model=list[str])
|
| 78 |
+
async def list_cases():
|
| 79 |
+
"""List all case IDs."""
|
| 80 |
+
return list(_cases.keys())
|
src/backend/app/api/health.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Health check endpoint."""
|
| 2 |
+
from fastapi import APIRouter
|
| 3 |
+
|
| 4 |
+
router = APIRouter()
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@router.get("/health")
|
| 8 |
+
async def health_check():
|
| 9 |
+
return {"status": "ok", "service": "CDS Agent"}
|
src/backend/app/api/ws.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WebSocket endpoint for real-time agent step streaming.
|
| 3 |
+
|
| 4 |
+
The frontend connects here to see each agent step as it happens:
|
| 5 |
+
- Step started (with tool name)
|
| 6 |
+
- Step completed (with output summary)
|
| 7 |
+
- Step failed (with error)
|
| 8 |
+
- Final report ready
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import asyncio
|
| 13 |
+
import json
|
| 14 |
+
|
| 15 |
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
| 16 |
+
|
| 17 |
+
from app.agent.orchestrator import Orchestrator
|
| 18 |
+
from app.models.schemas import CaseSubmission
|
| 19 |
+
|
| 20 |
+
router = APIRouter()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@router.websocket("/agent")
|
| 24 |
+
async def agent_websocket(websocket: WebSocket):
|
| 25 |
+
"""
|
| 26 |
+
WebSocket endpoint for real-time agent pipeline execution.
|
| 27 |
+
|
| 28 |
+
Protocol:
|
| 29 |
+
Client sends: JSON with patient case data (CaseSubmission format)
|
| 30 |
+
Server sends: JSON messages for each step update and final report
|
| 31 |
+
|
| 32 |
+
Message types:
|
| 33 |
+
- {"type": "step_update", "step": {...}}
|
| 34 |
+
- {"type": "report", "report": {...}}
|
| 35 |
+
- {"type": "error", "message": "..."}
|
| 36 |
+
- {"type": "complete", "case_id": "..."}
|
| 37 |
+
"""
|
| 38 |
+
await websocket.accept()
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
# Receive the case submission
|
| 42 |
+
raw = await websocket.receive_text()
|
| 43 |
+
data = json.loads(raw)
|
| 44 |
+
case = CaseSubmission(**data)
|
| 45 |
+
|
| 46 |
+
# Send acknowledgment
|
| 47 |
+
await websocket.send_json({
|
| 48 |
+
"type": "ack",
|
| 49 |
+
"message": "Case received. Starting agent pipeline...",
|
| 50 |
+
})
|
| 51 |
+
|
| 52 |
+
# Run the orchestrator and stream updates
|
| 53 |
+
orchestrator = Orchestrator()
|
| 54 |
+
|
| 55 |
+
async for step in orchestrator.run(case):
|
| 56 |
+
await websocket.send_json({
|
| 57 |
+
"type": "step_update",
|
| 58 |
+
"step": step.model_dump(mode="json"),
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
# Send final report
|
| 62 |
+
report = orchestrator.get_result()
|
| 63 |
+
if report:
|
| 64 |
+
await websocket.send_json({
|
| 65 |
+
"type": "report",
|
| 66 |
+
"report": report.model_dump(mode="json"),
|
| 67 |
+
})
|
| 68 |
+
|
| 69 |
+
# Send completion
|
| 70 |
+
await websocket.send_json({
|
| 71 |
+
"type": "complete",
|
| 72 |
+
"case_id": orchestrator.state.case_id if orchestrator.state else "unknown",
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
except WebSocketDisconnect:
|
| 76 |
+
pass
|
| 77 |
+
except json.JSONDecodeError:
|
| 78 |
+
await websocket.send_json({
|
| 79 |
+
"type": "error",
|
| 80 |
+
"message": "Invalid JSON received",
|
| 81 |
+
})
|
| 82 |
+
except Exception as e:
|
| 83 |
+
try:
|
| 84 |
+
await websocket.send_json({
|
| 85 |
+
"type": "error",
|
| 86 |
+
"message": str(e),
|
| 87 |
+
})
|
| 88 |
+
except Exception:
|
| 89 |
+
pass
|
| 90 |
+
finally:
|
| 91 |
+
try:
|
| 92 |
+
await websocket.close()
|
| 93 |
+
except Exception:
|
| 94 |
+
pass
|
src/backend/app/config.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Application configuration via environment variables.
|
| 3 |
+
"""
|
| 4 |
+
from pydantic_settings import BaseSettings
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Settings(BaseSettings):
|
| 9 |
+
"""Application settings loaded from environment / .env file."""
|
| 10 |
+
|
| 11 |
+
# App
|
| 12 |
+
app_name: str = "CDS Agent"
|
| 13 |
+
debug: bool = True
|
| 14 |
+
|
| 15 |
+
# CORS
|
| 16 |
+
cors_origins: List[str] = ["http://localhost:3000", "http://localhost:5173"]
|
| 17 |
+
|
| 18 |
+
# MedGemma
|
| 19 |
+
medgemma_model_id: str = "google/medgemma"
|
| 20 |
+
medgemma_api_key: str = ""
|
| 21 |
+
medgemma_base_url: str = "" # For API-based access
|
| 22 |
+
medgemma_device: str = "auto" # "cpu", "cuda", "auto"
|
| 23 |
+
medgemma_max_tokens: int = 4096
|
| 24 |
+
|
| 25 |
+
# External APIs
|
| 26 |
+
openfda_api_key: str = "" # Optional, increases rate limits
|
| 27 |
+
rxnorm_base_url: str = "https://rxnav.nlm.nih.gov/REST"
|
| 28 |
+
pubmed_base_url: str = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
|
| 29 |
+
pubmed_api_key: str = "" # Optional, increases rate limits
|
| 30 |
+
|
| 31 |
+
# RAG
|
| 32 |
+
chroma_persist_dir: str = "./data/chroma"
|
| 33 |
+
embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
|
| 34 |
+
|
| 35 |
+
# Agent
|
| 36 |
+
agent_max_retries: int = 2
|
| 37 |
+
agent_timeout_seconds: int = 120
|
| 38 |
+
agent_max_steps: int = 10
|
| 39 |
+
default_include_drug_check: bool = True
|
| 40 |
+
default_include_guidelines: bool = True
|
| 41 |
+
|
| 42 |
+
model_config = {
|
| 43 |
+
"env_file": ".env",
|
| 44 |
+
"env_file_encoding": "utf-8",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
settings = Settings()
|
src/backend/app/data/clinical_guidelines.json
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "cardio-001",
|
| 4 |
+
"specialty": "Cardiology",
|
| 5 |
+
"title": "AHA/ACC Hypertension Guidelines (2017)",
|
| 6 |
+
"source": "American Heart Association / American College of Cardiology",
|
| 7 |
+
"url": "https://www.ahajournals.org/doi/10.1161/HYP.0000000000000065",
|
| 8 |
+
"text": "Blood pressure categories: Normal (<120/<80 mmHg), Elevated (120-129/<80), Stage 1 HTN (130-139/80-89), Stage 2 HTN (≥140/≥90). For Stage 1 with ASCVD risk ≥10%, recommend antihypertensive medication plus lifestyle modifications. First-line agents include thiazide diuretics, ACE inhibitors, ARBs, and calcium channel blockers. For Stage 2, recommend two antihypertensive agents from different classes plus lifestyle modifications. Target BP <130/80 for most adults. Lifestyle modifications include DASH diet, sodium restriction <1500mg/day, regular aerobic exercise 90-150 min/week, weight loss if overweight, and moderation of alcohol."
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"id": "cardio-002",
|
| 12 |
+
"specialty": "Cardiology",
|
| 13 |
+
"title": "ACC/AHA Chest Pain Guidelines (2021)",
|
| 14 |
+
"source": "American College of Cardiology / American Heart Association",
|
| 15 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2021.07.053",
|
| 16 |
+
"text": "Acute chest pain evaluation: Assess pretest probability of ACS using history, risk factors, and ECG. High-sensitivity troponin is the preferred biomarker — obtain at presentation and serially (1-3 hours). Use HEART score for risk stratification: History, ECG, Age, Risk factors, Troponin. Low-risk (HEART 0-3): consider early discharge with outpatient follow-up. Moderate-risk (HEART 4-6): observation, serial troponins, consider stress testing or CCTA. High-risk (HEART 7-10): invasive strategy with cardiology consultation. For STEMI, activate cath lab with door-to-balloon time target <90 minutes. Consider fibrinolysis if PCI not available within 120 minutes. Typical anginal features: substernal pressure, radiation to arm/jaw, associated with exertion, relieved by rest/nitroglycerin."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"id": "cardio-003",
|
| 20 |
+
"specialty": "Cardiology",
|
| 21 |
+
"title": "AHA/ACC Heart Failure Guidelines (2022)",
|
| 22 |
+
"source": "American Heart Association / American College of Cardiology",
|
| 23 |
+
"url": "https://www.ahajournals.org/doi/10.1161/CIR.0000000000001063",
|
| 24 |
+
"text": "Heart failure classification: HFrEF (EF ≤40%), HFmrEF (EF 41-49%), HFpEF (EF ≥50%). HFrEF foundational therapy includes four pillars: ACEi/ARB/ARNI (sacubitril-valsartan preferred), evidence-based beta-blocker (carvedilol, metoprolol succinate, bisoprolol), mineralocorticoid receptor antagonist (spironolactone or eplerenone), and SGLT2 inhibitor (dapagliflozin or empagliflozin). Start all four drug classes as soon as feasible, uptitrate to target doses. Add hydralazine-isosorbide dinitrate in self-identified Black patients with NYHA III-IV. Consider ICD for primary prevention if EF ≤35% after ≥3 months of optimal therapy. Consider CRT if LBBB with QRS ≥150ms. For HFpEF, SGLT2 inhibitors recommended; manage comorbidities (HTN, AF, obesity, CAD). Diuretics for volume management in all HF types. Stage classification: A (at risk), B (pre-HF), C (symptomatic HF), D (advanced HF)."
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "cardio-004",
|
| 28 |
+
"specialty": "Cardiology",
|
| 29 |
+
"title": "ACC/AHA Atrial Fibrillation Guidelines (2023)",
|
| 30 |
+
"source": "American College of Cardiology / American Heart Association / Heart Rhythm Society",
|
| 31 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2023.08.017",
|
| 32 |
+
"text": "Atrial fibrillation management: Use CHA2DS2-VASc score for stroke risk assessment: C (CHF/LV dysfunction, 1pt), H (HTN, 1pt), A2 (Age ≥75, 2pts), D (DM, 1pt), S2 (prior Stroke/TIA, 2pts), V (Vascular disease, 1pt), A (Age 65-74, 1pt), Sc (female Sex category, 1pt). Anticoagulation recommended for CHA2DS2-VASc ≥2 in men or ≥3 in women. DOACs (apixaban, rivarelbaan, edoxaban, dabigatran) preferred over warfarin in non-valvular AF. Rate control: target resting HR <110 bpm for lenient control; beta-blockers, non-dihydropyridine CCBs, or digoxin. Rhythm control: consider for symptomatic patients, early AF, or when rate control insufficient. Options include catheter ablation (PVI), antiarrhythmic drugs (flecainide, propafenone, amiodarone, dofetilide, sotalol). Catheter ablation superior to AADs for maintaining sinus rhythm. Screen for modifiable risk factors: obesity, sleep apnea, alcohol use, HTN, DM."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "cardio-005",
|
| 36 |
+
"specialty": "Cardiology",
|
| 37 |
+
"title": "ACC/AHA Lipid Management Guidelines (2018)",
|
| 38 |
+
"source": "American College of Cardiology / American Heart Association",
|
| 39 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2018.11.003",
|
| 40 |
+
"text": "Statin therapy for primary prevention: In adults 40-75 with LDL-C ≥70 mg/dL and DM, use moderate-to-high intensity statin. Without DM but 10-year ASCVD risk ≥7.5%, initiate moderate-to-high intensity statin after clinician-patient discussion. For ASCVD risk 5-7.5%, consider risk-enhancing factors (family history, metabolic syndrome, CKD, inflammatory conditions, ethnicity-specific risk, elevated Lp(a), ABI <0.9). High-intensity statins: atorvastatin 40-80mg, rosuvastatin 20-40mg (lower LDL-C ≥50%). Moderate-intensity: atorvastatin 10-20mg, rosuvastatin 5-10mg, simvastatin 20-40mg (lower LDL-C 30-49%). For secondary prevention (clinical ASCVD): high-intensity statin. If LDL-C ≥70 mg/dL on maximally tolerated statin, add ezetimibe. If still ≥70, consider PCSK9 inhibitor (evolocumab, alirocumab). Coronary artery calcium (CAC) score of 0 in borderline/intermediate risk patients may allow deferral of statin therapy."
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "cardio-006",
|
| 44 |
+
"specialty": "Cardiology",
|
| 45 |
+
"title": "ACS/NSTEMI Management Guidelines (2014/2021 Update)",
|
| 46 |
+
"source": "American College of Cardiology / American Heart Association",
|
| 47 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2014.09.017",
|
| 48 |
+
"text": "Non-ST-elevation ACS (NSTE-ACS) initial management: Aspirin 162-325mg loading, then 81mg daily indefinitely. P2Y12 inhibitor: ticagrelor 180mg load then 90mg BID (preferred) or clopidogrel 300-600mg load then 75mg daily. Anticoagulation with heparin (UFH or enoxaparin). For very high-risk features (hemodynamic instability, recurrent angina despite treatment, new/worsening HF, sustained VT/VF, mechanical complication), immediate invasive strategy (<2 hours). For high-risk features (troponin rise/fall, dynamic ST/T changes, GRACE score >140), early invasive strategy (<24 hours). For low-risk, ischemia-guided strategy acceptable. Post-PCI: DAPT for minimum 12 months. Optimal medical therapy: high-intensity statin, beta-blocker, ACEi/ARB, BP control <130/80."
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"id": "cardio-007",
|
| 52 |
+
"specialty": "Cardiology",
|
| 53 |
+
"title": "ESC Acute/Chronic Pulmonary Embolism Guidelines (2019)",
|
| 54 |
+
"source": "European Society of Cardiology",
|
| 55 |
+
"url": "https://academic.oup.com/eurheartj/article/41/4/543/5556136",
|
| 56 |
+
"text": "Pulmonary embolism diagnosis and management: Use Wells score or Geneva score for pretest probability. Low probability: D-dimer testing — if negative (<500 ng/mL or age-adjusted), PE excluded. Positive D-dimer or moderate/high probability: CT pulmonary angiography (CTPA). Risk stratification: massive PE (hemodynamic instability) — systemic thrombolysis (alteplase) or catheter-directed therapy, surgical embolectomy if thrombolysis contraindicated. Submassive PE (RV dysfunction on imaging or elevated troponin/BNP but hemodynamically stable) — anticoagulation, consider escalation if deterioration. Low-risk PE (sPESI 0): consider outpatient treatment. Anticoagulation: DOAC preferred (rivaroxaban or apixaban as monotherapy, or LMWH lead-in then dabigatran/edoxaban). Duration: provoked (3 months), unprovoked (≥3 months, consider indefinite based on bleeding risk). Cancer-associated: LMWH or edoxaban/rivaroxaban."
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "cardio-008",
|
| 60 |
+
"specialty": "Cardiology",
|
| 61 |
+
"title": "ACC/AHA Valvular Heart Disease Guidelines (2020)",
|
| 62 |
+
"source": "American College of Cardiology / American Heart Association",
|
| 63 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2020.11.018",
|
| 64 |
+
"text": "Aortic stenosis: Severe AS defined by aortic valve area ≤1.0 cm², mean gradient ≥40 mmHg, or peak velocity ≥4.0 m/s. Intervention indicated for symptomatic severe AS (exertional dyspnea, angina, syncope). TAVI preferred for patients ≥65 years or high surgical risk; SAVR for younger patients or those requiring concomitant cardiac surgery. Mitral regurgitation: Primary (degenerative) MR — surgical repair preferred over replacement when feasible. Secondary (functional) MR — optimize GDMT for HF first; consider transcatheter edge-to-edge repair (MitraClip) if symptomatic despite optimal therapy. Aortic regurgitation: Surgery indicated when symptomatic or when asymptomatic with LV dilatation (LVESD >50mm or LVEF <55%). Serial echocardiographic monitoring for all moderate-severe valvular disease."
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"id": "endo-001",
|
| 68 |
+
"specialty": "Endocrinology",
|
| 69 |
+
"title": "ADA Standards of Care in Diabetes (2024)",
|
| 70 |
+
"source": "American Diabetes Association",
|
| 71 |
+
"url": "https://diabetesjournals.org/care/issue/47/Supplement_1",
|
| 72 |
+
"text": "Type 2 Diabetes management: First-line therapy is metformin plus comprehensive lifestyle modifications (weight management, 150+ min/week moderate physical activity, medical nutrition therapy). Target A1C <7% for most adults; individualize (tighter for younger patients without comorbidities, less stringent 7.5-8% for elderly or those with hypoglycemia risk). If A1C remains above target after 3-6 months, add second agent based on patient characteristics: GLP-1 receptor agonists (semaglutide, liraglutide, dulaglutide) or SGLT2 inhibitors (empagliflozin, dapagliflozin, canagliflozin) preferred for patients with established ASCVD, heart failure, or CKD. Insulin may be needed if A1C very high (>10%) or evidence of catabolism. Monitor A1C every 3-6 months. Screen annually for complications: retinopathy (dilated eye exam), nephropathy (urine albumin-to-creatinine ratio, eGFR), neuropathy (monofilament exam). Target BP <130/80 for most patients with diabetes. Statin therapy for all ages 40-75 with diabetes."
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"id": "endo-002",
|
| 76 |
+
"specialty": "Endocrinology",
|
| 77 |
+
"title": "ADA Diabetic Ketoacidosis (DKA) Management (2024)",
|
| 78 |
+
"source": "American Diabetes Association",
|
| 79 |
+
"url": "https://diabetesjournals.org/care/issue/47/Supplement_1",
|
| 80 |
+
"text": "DKA diagnostic criteria: blood glucose >250 mg/dL, arterial pH <7.3, serum bicarbonate <18 mEq/L, positive serum/urine ketones, elevated anion gap. Severity: Mild (pH 7.25-7.30, bicarb 15-18), Moderate (pH 7.00-7.24, bicarb 10-14), Severe (pH <7.00, bicarb <10). Management: Aggressive IV fluid resuscitation — 0.9% NaCl 15-20 mL/kg/hr first hour, then 250-500 mL/hr adjusted per hemodynamics. Potassium: If K+ <3.3, replace before starting insulin. If 3.3-5.3, add 20-30 mEq K+ per liter of IV fluid. If K+ >5.3, recheck in 2 hours. Insulin: Regular insulin 0.1-0.14 U/kg/hr IV continuous infusion. When glucose <200-250 mg/dL, reduce insulin rate and add dextrose to IV fluids. Transition to subcutaneous insulin when: pH >7.3, bicarb ≥15, anion gap closed, patient eating. Overlap IV and subQ insulin by 1-2 hours. Monitor glucose hourly, BMP every 2-4 hours. Identify and treat precipitating factors: infection, medication non-compliance, new-onset diabetes, MI."
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"id": "endo-003",
|
| 84 |
+
"specialty": "Endocrinology",
|
| 85 |
+
"title": "ATA Thyroid Disease Guidelines (2016/2023)",
|
| 86 |
+
"source": "American Thyroid Association",
|
| 87 |
+
"url": "https://www.thyroid.org/professionals/ata-professional-guidelines/",
|
| 88 |
+
"text": "Hypothyroidism: Diagnosis based on elevated TSH with low free T4 (overt) or elevated TSH with normal free T4 (subclinical). Treatment: levothyroxine (LT4) at 1.6 mcg/kg/day for full replacement (lower starting dose 25-50 mcg/day in elderly or cardiac patients). Target TSH: 0.5-2.5 mIU/L for most adults; 4-6 weeks between dose adjustments. Monitor TSH 4-6 weeks after dose changes, then annually when stable. Hyperthyroidism: Graves disease most common cause. Treatment options: antithyroid drugs (methimazole preferred over PTU except first trimester pregnancy), radioactive iodine ablation, or thyroidectomy. Methimazole 5-30 mg/day; monitor CBC and LFTs. Beta-blockers for symptomatic control. Thyroid storm: aggressive treatment with PTU, iodine (at least 1 hour after PTU), beta-blocker, corticosteroids, supportive care. Thyroid nodules: FNA biopsy recommended for nodules ≥1 cm with suspicious ultrasound features. Use Bethesda system for cytology classification."
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"id": "endo-004",
|
| 92 |
+
"specialty": "Endocrinology",
|
| 93 |
+
"title": "Endocrine Society Adrenal Insufficiency Guidelines (2016)",
|
| 94 |
+
"source": "Endocrine Society",
|
| 95 |
+
"url": "https://academic.oup.com/jcem/article/101/2/364/2810222",
|
| 96 |
+
"text": "Primary adrenal insufficiency (Addison disease): Diagnosis by morning cortisol <3 mcg/dL (highly suggestive) or ACTH stimulation test (cosyntropin 250 mcg IV, cortisol <18 mcg/dL at 30-60 min is diagnostic). Replacement: hydrocortisone 15-25 mg/day in 2-3 divided doses (largest dose AM), plus fludrocortisone 0.05-0.2 mg/day for mineralocorticoid replacement. Stress dosing: double or triple glucocorticoid dose for febrile illness, minor surgery, or trauma. For major surgery or adrenal crisis: hydrocortisone 100 mg IV bolus then 50 mg IV q6-8h. Adrenal crisis: life-threatening — presents with hypotension, hyponatremia, hyperkalemia, hypoglycemia. Treat with immediate IV hydrocortisone 100 mg and aggressive IV fluids (0.9% NaCl). Patient education: sick-day rules, emergency injection kit, medical alert identification. Secondary adrenal insufficiency (pituitary or chronic steroid use): glucocorticoid replacement only, no mineralocorticoid needed."
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "endo-005",
|
| 100 |
+
"specialty": "Endocrinology",
|
| 101 |
+
"title": "Endocrine Society Osteoporosis Guidelines (2020)",
|
| 102 |
+
"source": "Endocrine Society / AACE",
|
| 103 |
+
"url": "https://academic.oup.com/jcem/article/105/3/dgz291/5688848",
|
| 104 |
+
"text": "Osteoporosis screening: DXA scan recommended for all women ≥65, men ≥70, and younger postmenopausal women with risk factors. Diagnosis: T-score ≤-2.5 at spine, hip, or femoral neck. Osteopenia: T-score -1.0 to -2.5. Pharmacologic treatment indicated for: hip or vertebral fracture, T-score ≤-2.5, or osteopenia with FRAX 10-year hip fracture risk ≥3% or major osteoporotic fracture risk ≥20%. First-line: oral bisphosphonates (alendronate 70mg weekly, risedronate 35mg weekly). Alternatives: IV zoledronic acid 5mg annually, denosumab 60mg subQ q6 months. For very high fracture risk: anabolic agents first (teriparatide, abaloparatide, romosozumab) followed by antiresorptive. All patients: calcium 1000-1200 mg/day (diet + supplement), vitamin D 600-800 IU/day (target 25-OH-D >30 ng/mL), weight-bearing exercise, fall prevention. Bisphosphonate holiday after 3-5 years in non-high-risk patients."
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"id": "pulm-001",
|
| 108 |
+
"specialty": "Pulmonology",
|
| 109 |
+
"title": "GOLD COPD Guidelines (2024)",
|
| 110 |
+
"source": "Global Initiative for Chronic Obstructive Lung Disease",
|
| 111 |
+
"url": "https://goldcopd.org/",
|
| 112 |
+
"text": "COPD diagnosis requires post-bronchodilator spirometry with FEV1/FVC < 0.70. Severity by FEV1 % predicted: GOLD 1 (mild, ≥80%), GOLD 2 (moderate, 50-79%), GOLD 3 (severe, 30-49%), GOLD 4 (very severe, <30%). ABE assessment tool: Group A (low symptoms, 0-1 moderate exacerbation) — bronchodilator PRN. Group B (high symptoms, 0-1 moderate exacerbation) — LABA or LAMA. Group E (any exacerbation leading to hospitalization or ≥2 moderate exacerbations) — LABA + LAMA, consider adding ICS if blood eosinophils ≥300 cells/μL. All patients: smoking cessation (most important intervention), vaccinations (influenza, pneumococcal, COVID-19, Tdap, RSV if ≥60), pulmonary rehabilitation for symptomatic patients with mMRC ≥2. Oxygen therapy: long-term O2 if resting PaO2 ≤55 mmHg or SpO2 ≤88%. Acute exacerbation: short-acting bronchodilators, systemic corticosteroids (prednisone 40mg x 5 days), antibiotics if increased sputum purulence."
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"id": "pulm-002",
|
| 116 |
+
"specialty": "Pulmonology",
|
| 117 |
+
"title": "GINA Asthma Guidelines (2024)",
|
| 118 |
+
"source": "Global Initiative for Asthma",
|
| 119 |
+
"url": "https://ginasthma.org/",
|
| 120 |
+
"text": "Asthma diagnosis: variable expiratory airflow limitation with respiratory symptoms (wheeze, SOB, chest tightness, cough). Confirm with spirometry showing reversibility (FEV1 increase ≥12% and ≥200 mL post-bronchodilator) or positive bronchoprovocation test. Stepwise treatment (adults): Track 1 (preferred, anti-inflammatory reliever): Step 1-2: as-needed low-dose ICS-formoterol. Step 3: low-dose ICS-formoterol maintenance and reliever (MART). Step 4: medium-dose ICS-formoterol MART. Step 5: add LAMA (tiotropium), consider high-dose ICS-formoterol, or refer for biologics. Track 2 (alternative, SABA reliever): Step 1: as-needed SABA + take ICS whenever SABA used. Step 2: low-dose ICS maintenance + as-needed SABA. Step 3: low-dose ICS-LABA + as-needed SABA. Step 4: medium/high-dose ICS-LABA. Step 5: refer for phenotyping and biologics (anti-IgE: omalizumab; anti-IL5: mepolizumab, benralizumab; anti-IL4/13: dupilumab; anti-TSLP: tezepelumab). Assess and address: inhaler technique, adherence, comorbidities (rhinitis, GERD, obesity, OSA), trigger avoidance."
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"id": "pulm-003",
|
| 124 |
+
"specialty": "Pulmonology",
|
| 125 |
+
"title": "ATS/IDSA Community-Acquired Pneumonia Guidelines (2019)",
|
| 126 |
+
"source": "American Thoracic Society / Infectious Diseases Society of America",
|
| 127 |
+
"url": "https://www.atsjournals.org/doi/10.1164/rccm.201908-1581ST",
|
| 128 |
+
"text": "Community-acquired pneumonia (CAP) diagnosis: acute respiratory symptoms plus new infiltrate on chest imaging. Severity assessment: Use PSI (Pneumonia Severity Index) or CURB-65 (Confusion, Urea >7 mmol/L, RR ≥30, BP <90/60, age ≥65). CURB-65 score 0-1: outpatient treatment. Score 2: consider short hospitalization. Score 3-5: hospitalize, consider ICU. Outpatient treatment (healthy, no comorbidities): amoxicillin 1g TID or doxycycline 100mg BID x 5-7 days. Outpatient with comorbidities (chronic lung/heart/liver/renal disease, DM, alcoholism, malignancy, asplenia): amoxicillin-clavulanate or cephalosporin PLUS macrolide, or respiratory fluoroquinolone monotherapy (levofloxacin 750mg, moxifloxacin 400mg). Inpatient (non-ICU): beta-lactam PLUS macrolide, or respiratory fluoroquinolone alone. ICU: beta-lactam PLUS macrolide or beta-lactam + respiratory fluoroquinolone. If MRSA/Pseudomonas risk factors present, obtain cultures and add coverage. Duration: minimum 5 days, until afebrile ≥48h and clinically stable."
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"id": "pulm-004",
|
| 132 |
+
"specialty": "Pulmonology",
|
| 133 |
+
"title": "Pleural Effusion Management (BTS 2023)",
|
| 134 |
+
"source": "British Thoracic Society",
|
| 135 |
+
"url": "https://www.brit-thoracic.org.uk/quality-improvement/guidelines/pleural-disease/",
|
| 136 |
+
"text": "Pleural effusion evaluation: Diagnostic thoracentesis indicated for new unilateral effusion, bilateral effusion of unequal size, or clinical concern. Light's criteria to differentiate exudate from transudate: Exudate if any ONE met — pleural/serum protein >0.5, pleural/serum LDH >0.6, pleural LDH > 2/3 upper limit of normal serum. Common transudative causes: heart failure, cirrhosis, nephrotic syndrome. Common exudative causes: pneumonia/parapneumonic, malignancy, PE, TB. Parapneumonic effusion/empyema: If pH <7.2, glucose <60 mg/dL, positive Gram stain/culture, or frank pus — chest tube drainage indicated. Consider intrapleural fibrinolytic therapy (tPA + DNase) for loculated empyema. Malignant effusion: therapeutic thoracentesis for symptom relief; if recurrent, consider indwelling pleural catheter or talc pleurodesis. Send fluid for: cell count/differential, protein, LDH, pH, glucose, cytology, cultures, specific markers as clinically indicated (ADA for TB, amylase for pancreatitis/esophageal rupture)."
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": "em-001",
|
| 140 |
+
"specialty": "Emergency Medicine",
|
| 141 |
+
"title": "AHA/ASA Acute Ischemic Stroke Guidelines (2019)",
|
| 142 |
+
"source": "American Heart Association / American Stroke Association",
|
| 143 |
+
"url": "https://www.ahajournals.org/doi/10.1161/STR.0000000000000211",
|
| 144 |
+
"text": "Acute ischemic stroke: Time-critical emergency. FAST screening: Face drooping, Arm weakness, Speech difficulty, Time to call 911. Use NIHSS (National Institutes of Health Stroke Scale) for severity assessment. IV alteplase (tPA) 0.9 mg/kg (max 90mg): 10% bolus, remainder over 60 min. Eligibility: within 4.5 hours of symptom onset (or last known well). Exclusions: active bleeding, recent surgery, platelets <100K, INR >1.7, glucose <50. Extended window: if large vessel occlusion with favorable perfusion imaging, mechanical thrombectomy up to 24 hours. BP management: for tPA candidates, lower to <185/110 before treatment; maintain <180/105 for 24 hours post-tPA. For non-tPA patients, permissive hypertension up to 220/120 unless end-organ damage. Thrombectomy: for large vessel occlusion (ICA, M1, sometimes M2), NIHSS ≥6, ASPECTS ≥6, within 6 hours (up to 24 hours with perfusion imaging selection). Post-acute: antiplatelets, statins, identify and treat etiology (AF screening, carotid imaging, echocardiography)."
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"id": "em-002",
|
| 148 |
+
"specialty": "Emergency Medicine",
|
| 149 |
+
"title": "Surviving Sepsis Campaign Guidelines (2021)",
|
| 150 |
+
"source": "Society of Critical Care Medicine / European Society of Intensive Care Medicine",
|
| 151 |
+
"url": "https://www.sccm.org/SurvivingSepsisCampaign/Guidelines",
|
| 152 |
+
"text": "Sepsis: life-threatening organ dysfunction caused by dysregulated host response to infection. SOFA score ≥2 points above baseline = organ dysfunction. qSOFA screening (≥2 of: RR ≥22, altered mentation, SBP ≤100). Hour-1 bundle: measure lactate (remeasure if >2 mmol/L), obtain blood cultures before antibiotics, administer broad-spectrum antibiotics within 1 hour of recognition, begin rapid 30 mL/kg crystalloid (balanced crystalloids preferred over NS) for hypotension or lactate ≥4. Septic shock: sepsis with vasopressor requirement to maintain MAP ≥65 mmHg AND lactate >2 mmol/L despite adequate volume resuscitation. Vasopressors: norepinephrine first-line, add vasopressin 0.03 U/min as second agent, then epinephrine. Corticosteroids: IV hydrocortisone 200 mg/day for refractory shock. Source control: identify and address within 6-12 hours (drainage, debridement, device removal). De-escalate antibiotics based on culture results. Assess for fluid responsiveness before further boluses (passive leg raise, pulse pressure variation). Target: MAP ≥65, lactate normalization, UOP ≥0.5 mL/kg/hr."
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"id": "em-003",
|
| 156 |
+
"specialty": "Emergency Medicine",
|
| 157 |
+
"title": "ATLS Trauma Management Guidelines (2018)",
|
| 158 |
+
"source": "American College of Surgeons",
|
| 159 |
+
"url": "https://www.facs.org/quality-programs/trauma/atls/",
|
| 160 |
+
"text": "Primary survey (ABCDE): A — Airway with cervical spine protection (jaw thrust, definitive airway if GCS ≤8 or impending obstruction). B — Breathing (inspect, auscultate, treat tension pneumothorax with needle decompression 2nd ICS MCL then chest tube, treat open pneumothorax with 3-sided occlusive dressing). C — Circulation with hemorrhage control (direct pressure, tourniquet for extremity hemorrhage, massive transfusion protocol for hemorrhagic shock: 1:1:1 ratio PRBC:FFP:platelets, permissive hypotension SBP 80-90 until surgical control, TXA 1g IV within 3 hours). D — Disability (GCS, pupil exam). E — Exposure/Environment (fully expose, prevent hypothermia). Hemorrhagic shock classification: Class I (<15% blood loss, HR normal), Class II (15-30%, tachycardia), Class III (30-40%, hypotension, tachycardia, altered MS), Class IV (>40%, life-threatening). Secondary survey: head-to-toe exam after resuscitation is underway. FAST exam for intra-abdominal hemorrhage. Massive transfusion: activate for anticipated need of ≥10 units PRBC in 24h or ≥4 units in 1h."
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"id": "em-004",
|
| 164 |
+
"specialty": "Emergency Medicine",
|
| 165 |
+
"title": "Anaphylaxis Emergency Management (WAO/EAACI 2021)",
|
| 166 |
+
"source": "World Allergy Organization / European Academy of Allergy and Clinical Immunology",
|
| 167 |
+
"url": "https://doi.org/10.1016/j.waojou.2021.100525",
|
| 168 |
+
"text": "Anaphylaxis: acute, potentially fatal systemic allergic reaction. Clinical criteria: acute onset involving skin/mucosal tissue PLUS respiratory compromise and/or reduced BP/end-organ dysfunction; OR two or more of: skin involvement, respiratory, reduced BP, persistent GI symptoms after exposure to likely allergen. FIRST-LINE TREATMENT: Epinephrine IM 0.01 mg/kg (max 0.5 mg adult, 0.3 mg child) into mid-anterolateral thigh. Repeat every 5-15 minutes as needed. There is NO absolute contraindication to epinephrine in anaphylaxis. Adjunctive: Position supine with legs elevated (or sitting if respiratory distress), high-flow O2, large-bore IV access, IV normal saline bolus 20 mL/kg for hypotension. If refractory: epinephrine infusion 0.1-1.0 mcg/kg/min. Second-line: H1-antihistamine (cetirizine, diphenhydramine), H2-antihistamine (famotidine), systemic corticosteroids (methylprednisolone 1-2 mg/kg). Observe 4-6 hours minimum (biphasic reactions occur in 5-20%). Discharge with epinephrine auto-injector prescription and anaphylaxis action plan. Refer to allergist for trigger identification."
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"id": "em-005",
|
| 172 |
+
"specialty": "Emergency Medicine",
|
| 173 |
+
"title": "Burns Emergency Management (ABA 2023)",
|
| 174 |
+
"source": "American Burn Association",
|
| 175 |
+
"url": "https://ameriburn.org/resources/practice-guidelines/",
|
| 176 |
+
"text": "Burn assessment: Superficial (1st degree) — epidermal only, erythema, painful, no blisters. Partial thickness (2nd degree) — involves dermis, blisters, painful to touch. Full thickness (3rd degree) — through dermis, waxy/leathery, insensate. 4th degree — extends to muscle/bone. TBSA estimation: Rule of Nines (adults): head 9%, each arm 9%, each leg 18%, anterior trunk 18%, posterior trunk 18%, perineum 1%. Palm method: patient's palm ≈ 1% TBSA for scattered burns. Fluid resuscitation (Parkland formula): 4 mL × kg × %TBSA over 24 hours (half in first 8 hours from time of burn, half over remaining 16 hours). Use lactated Ringer's. Titrate to UOP 0.5-1.0 mL/kg/hr (adults). Indications for intubation: facial burns, singed nasal/facial hair, soot in airway, hoarseness, stridor, or enclosed-space fire with inhalation injury. Transfer to burn center: >10% TBSA partial thickness, full thickness, face/hands/feet/perineum/joint burns, electrical/chemical burns, inhalation injury. Wound care: gentle debridement, silver sulfadiazine or alternative topical antimicrobial, tetanus prophylaxis."
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"id": "em-006",
|
| 180 |
+
"specialty": "Emergency Medicine",
|
| 181 |
+
"title": "ACLS Cardiac Arrest Algorithm (2020)",
|
| 182 |
+
"source": "American Heart Association",
|
| 183 |
+
"url": "https://www.ahajournals.org/doi/10.1161/CIR.0000000000000916",
|
| 184 |
+
"text": "Cardiac arrest management (ACLS): Start high-quality CPR immediately — rate 100-120/min, depth 2-2.4 inches, full chest recoil, minimize interruptions. Attach defibrillator/monitor. Shockable rhythms (VF/pVT): Defibrillate (biphasic 120-200J or per manufacturer), resume CPR immediately for 2 minutes, then rhythm check. Epinephrine 1mg IV/IO every 3-5 minutes. After 2nd shock, consider amiodarone 300mg IV bolus (subsequent dose 150mg) or lidocaine. Non-shockable rhythms (asystole/PEA): CPR + epinephrine 1mg IV/IO every 3-5 minutes. Identify and treat reversible causes (Hs and Ts): Hypovolemia, Hypoxia, Hydrogen ion (acidosis), Hypo/Hyperkalemia, Hypothermia, Tension pneumothorax, Tamponade (cardiac), Toxins, Thrombosis (pulmonary/coronary). ROSC management: 12-lead ECG (STEMI → cath lab), targeted temperature management 32-36°C for 24 hours if comatose, avoid hyperthermia, optimize oxygenation (SpO2 92-98%), maintain SBP >90, serial neurological assessment, neuroprognostication after ≥72 hours."
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"id": "neuro-001",
|
| 188 |
+
"specialty": "Neurology",
|
| 189 |
+
"title": "AAN Epilepsy/Seizure Management Guidelines (2022)",
|
| 190 |
+
"source": "American Academy of Neurology / American Epilepsy Society",
|
| 191 |
+
"url": "https://www.aan.com/Guidelines/home/GuidelineDetail/1025",
|
| 192 |
+
"text": "Seizure classification: focal (aware or impaired awareness, motor or non-motor onset), generalized (tonic-clonic, absence, myoclonic, atonic), unknown onset. First unprovoked seizure: Risk of recurrence ~40% at 2 years. Treatment after first seizure is individualized; immediate treatment recommended if high recurrence risk (abnormal EEG, structural lesion, nocturnal seizure). Status epilepticus: seizure lasting >5 minutes or recurrent seizures without return to baseline. Treatment ladder: (1) Benzodiazepines: IV lorazepam 0.1 mg/kg (max 4mg, may repeat x1) or IM midazolam 10mg if no IV access. (2) If seizures persist after 2 doses of benzo: IV fosphenytoin 20 mg PE/kg, or valproate 40 mg/kg, or levetiracetam 60 mg/kg. (3) Refractory SE: continuous infusion of midazolam, propofol, or pentobarbital with EEG monitoring. First-line ASMs for focal epilepsy: levetiracetam, lamotrigine, oxcarbazepine. For generalized epilepsy: valproate (caution in women of childbearing potential — teratogenic), lamotrigine, levetiracetam. Avoid carbamazepine/oxcarbazepine in generalized epilepsy (may worsen absence/myoclonic seizures)."
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"id": "neuro-002",
|
| 196 |
+
"specialty": "Neurology",
|
| 197 |
+
"title": "AAN Headache/Migraine Guidelines (2021)",
|
| 198 |
+
"source": "American Academy of Neurology / American Headache Society",
|
| 199 |
+
"url": "https://www.aan.com/Guidelines/Home/GuidelineDetail/1043",
|
| 200 |
+
"text": "Migraine diagnosis (ICHD-3): ≥5 attacks lasting 4-72 hours with ≥2 of: unilateral, pulsating, moderate/severe intensity, aggravated by routine activity; PLUS ≥1 of: nausea/vomiting, photophobia and phonophobia. Red flag headaches (SNOOP): Systemic symptoms/disease, Neurologic signs, Onset sudden (thunderclap), Onset after 50 years, Pattern change — warrant urgent investigation (CT/CTA, MRI, LP). Acute migraine treatment: NSAIDs (ibuprofen, naproxen) or acetaminophen for mild-moderate. Triptans (sumatriptan, rizatriptan, eletriptan) for moderate-severe — first-line specific therapy. Gepants (ubrogepant, rimegepant) or ditans (lasmiditan) for triptan-intolerant or cardiovascular risk patients. Antiemetics (metoclopramide, prochlorperazine) as adjuncts. Preventive therapy indicated if ≥4 headache days/month, significant disability, or acute medication failure/overuse. Oral preventives: propranolol, topiramate, amitriptyline, valproate (Level A evidence). CGRP monoclonal antibodies (erenumab, fremanezumab, galcanezumab, eptinezumab) for patients failing ≥2 oral preventives or as first-line per clinician judgment. Behavioral approaches: biofeedback, cognitive behavioral therapy, relaxation training."
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"id": "neuro-003",
|
| 204 |
+
"specialty": "Neurology",
|
| 205 |
+
"title": "AAN Multiple Sclerosis Guidelines (2018)",
|
| 206 |
+
"source": "American Academy of Neurology",
|
| 207 |
+
"url": "https://www.aan.com/Guidelines/home/GuidelineDetail/898",
|
| 208 |
+
"text": "Multiple sclerosis: Diagnosis based on McDonald criteria (2017) — dissemination in space and time with CNS demyelinating disease, supported by MRI and/or CSF findings. Clinically isolated syndrome (CIS) with MRI lesions suggestive of MS should be offered disease-modifying therapy (DMT). Relapsing-remitting MS (RRMS) treatment: Platform therapies: interferon beta, glatiramer acetate (moderate efficacy). Oral DMTs: dimethyl fumarate, fingolimod, teriflunomide, cladribine. Higher-efficacy DMTs: natalizumab (JC virus risk — PML), ocrelizumab, ofatumumab, alemtuzumab. Treatment choice based on disease activity, risk tolerance, reproductive planning. Acute relapse: IV methylprednisolone 1g/day for 3-5 days, with or without oral taper. If steroid-refractory: plasma exchange. Progressive MS: ocrelizumab for primary progressive MS (PPMS) with active inflammation. Siponimod for active secondary progressive MS (SPMS). Symptom management: fatigue (amantadine, modafinil), spasticity (baclofen, tizanidine), neuropathic pain (gabapentin, pregabalin, duloxetine), bladder dysfunction, depression."
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"id": "neuro-004",
|
| 212 |
+
"specialty": "Neurology",
|
| 213 |
+
"title": "Meningitis Diagnosis and Management Guidelines (IDSA 2004/2017 Update)",
|
| 214 |
+
"source": "Infectious Diseases Society of America",
|
| 215 |
+
"url": "https://academic.oup.com/cid/article/39/9/1267/402182",
|
| 216 |
+
"text": "Bacterial meningitis: Medical emergency. Classic triad: fever, neck stiffness, altered mental status (present together in <50% of cases). Empiric antibiotics within 1 hour of suspicion — do NOT delay for LP or imaging. Empiric therapy by age: Neonates — ampicillin + cefotaxime (or gentamicin). Age 1 month-50 years — vancomycin + ceftriaxone (or cefotaxime). Age >50 years — vancomycin + ampicillin + ceftriaxone. Dexamethasone: 0.15 mg/kg IV q6h for 4 days, started before or with first antibiotic dose (proven benefit for S. pneumoniae meningitis in adults). CT before LP if: immunocompromised, history of CNS disease, new-onset seizure, papilledema, altered consciousness, focal neurologic deficit. CSF findings in bacterial meningitis: opening pressure elevated, WBC >1000 (neutrophil predominance), protein >250 mg/dL, glucose <40 mg/dL (CSF:serum ratio <0.4), positive Gram stain (60-90%), positive culture. Viral meningitis: lymphocytic pleocytosis, normal/mildly elevated protein, normal glucose — typically self-limited, supportive care."
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"id": "gi-001",
|
| 220 |
+
"specialty": "Gastroenterology",
|
| 221 |
+
"title": "ACG Upper GI Bleeding Guidelines (2021)",
|
| 222 |
+
"source": "American College of Gastroenterology",
|
| 223 |
+
"url": "https://journals.lww.com/ajg/fulltext/2021/05000/acg_clinical_guideline__upper_gastrointestinal_and.14.aspx",
|
| 224 |
+
"text": "Upper GI bleeding (UGIB): hematemesis, melena, or hematochezia with hemodynamic compromise suggesting upper source. Risk stratification: Glasgow-Blatchford Score (GBS) — GBS 0-1 may be managed outpatient. Resuscitation: IV access, crystalloid fluids, type and crossmatch, transfuse PRBC for Hb <7 g/dL (threshold 8-9 g/dL for ACS or hemodynamic instability). Restrictive transfusion strategy improves outcomes. Proton pump inhibitor: IV PPI (pantoprazole 80mg bolus then 8mg/hr infusion) not routinely recommended pre-endoscopy but may be given; high-dose PPI clearly indicated post-endoscopy for high-risk ulcer stigmata. Endoscopy: within 24 hours of presentation (within 12 hours for high-risk features). For active bleeding or visible vessel: endoscopic hemostasis (epinephrine injection + thermal/clips). Variceal bleeding: IV octreotide 50mcg bolus then 50mcg/hr, prophylactic antibiotics (ceftriaxone 1g IV), endoscopic variceal ligation. If refractory: TIPS. Hold anticoagulants; resume based on individual risk-benefit after hemostasis. H. pylori testing for all peptic ulcer disease."
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"id": "gi-002",
|
| 228 |
+
"specialty": "Gastroenterology",
|
| 229 |
+
"title": "ACG Acute Pancreatitis Guidelines (2024)",
|
| 230 |
+
"source": "American College of Gastroenterology",
|
| 231 |
+
"url": "https://journals.lww.com/ajg/fulltext/2024/01000/american_college_of_gastroenterology_guideline.18.aspx",
|
| 232 |
+
"text": "Acute pancreatitis diagnosis requires 2 of 3: (1) characteristic abdominal pain (epigastric, radiating to back), (2) serum lipase ≥3x upper limit of normal, (3) characteristic findings on imaging. Most common etiologies: gallstones (~40%) and alcohol (~30%). Severity classification (Revised Atlanta): Mild — no organ failure or complications. Moderately severe — transient organ failure (<48h) or local complications. Severe — persistent organ failure (>48h). Initial management: aggressive IV fluid resuscitation with lactated Ringer's (goal-directed, 1.5 mL/kg/hr initially, titrate based on clinical response — BUN, hematocrit, urine output). Adequate analgesia (multimodal approach; opioids if needed). NPO initially, but early oral feeding (within 24 hours if tolerated) is preferred over prolonged bowel rest — low-fat solid diet as tolerated. For gallstone pancreatitis: early ERCP (within 24h) only if concurrent cholangitis or persistent biliary obstruction. Cholecystectomy during index admission (or within 2 weeks) for mild gallstone pancreatitis to prevent recurrence. Prophylactic antibiotics NOT recommended. Infected necrosis: minimally invasive step-up approach (percutaneous drainage → endoscopic/laparoscopic necrosectomy if not resolving)."
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"id": "gi-003",
|
| 236 |
+
"specialty": "Gastroenterology",
|
| 237 |
+
"title": "AASLD Cirrhosis and Hepatic Decompensation Guidelines (2023)",
|
| 238 |
+
"source": "American Association for the Study of Liver Diseases",
|
| 239 |
+
"url": "https://www.aasld.org/practice-guidelines",
|
| 240 |
+
"text": "Cirrhosis management: Classify severity with Child-Pugh (A/B/C) and MELD score (for transplant prioritization). Screening: All cirrhotics need hepatocellular carcinoma (HCC) surveillance with ultrasound ± AFP every 6 months. Variceal screening: EGD at diagnosis of cirrhosis; if no varices, repeat in 2-3 years; if small varices, non-selective beta-blocker (NSBB) or repeat EGD in 1-2 years; if large varices, NSBB (nadolol, propranolol, carvedilol) or endoscopic variceal ligation for primary prophylaxis. Ascites: First-line sodium restriction <2g/day plus spironolactone (start 100mg, max 400mg) ± furosemide (start 40mg, max 160mg) in 100:40 ratio. Refractory ascites: serial large-volume paracentesis with albumin replacement (6-8 g per liter removed if >5L drained) or TIPS. Spontaneous bacterial peritonitis (SBP): diagnose if ascitic fluid PMN ≥250 cells/mm³; treat with ceftriaxone 2g/day IV plus IV albumin (1.5 g/kg day 1, 1 g/kg day 3). Hepatic encephalopathy: lactulose (titrate to 2-3 soft BM/day) ± rifaximin 550mg BID for prevention of recurrence. Hepatorenal syndrome: IV albumin + vasoconstrictors (terlipressin preferred where available, or norepinephrine + albumin, or midodrine + octreotide + albumin)."
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"id": "gi-004",
|
| 244 |
+
"specialty": "Gastroenterology",
|
| 245 |
+
"title": "ACG Inflammatory Bowel Disease Guidelines (2023)",
|
| 246 |
+
"source": "American College of Gastroenterology",
|
| 247 |
+
"url": "https://journals.lww.com/ajg/fulltext/2023/04000/acg_clinical_guideline_for_the_management_of.21.aspx",
|
| 248 |
+
"text": "Inflammatory bowel disease (IBD) — Crohn's disease (CD) and ulcerative colitis (UC). UC management: Mild-moderate — 5-ASA (mesalamine) oral and/or rectal. Moderate-severe — corticosteroids for induction (prednisone 40-60mg, budesonide MMX for UC), then transition to steroid-sparing maintenance: thiopurines (azathioprine, 6-MP), anti-TNF (infliximab, adalimumab), vedolizumab, ustekinumab, tofacitinib (JAK inhibitor), upadacitinib, or ozanimod. Acute severe UC (Truelove and Witts criteria): IV corticosteroids; if no response in 3-5 days, consider rescue therapy (infliximab or cyclosporine) or colectomy. CD management: Ileal/ileocolonic — budesonide for mild-moderate induction. Moderate-severe — anti-TNF, vedolizumab, ustekinumab, or risankizumab as first-line biologic therapy. Perianal fistulizing CD: combination of surgical drainage + anti-TNF (infliximab preferred). Stricturing CD: consider endoscopic balloon dilation or surgical stricturoplasty. All IBD patients: screen for osteoporosis, depression, colorectal cancer (surveillance colonoscopy starting 8 years after diagnosis), and ensure vaccinations before immunosuppression."
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"id": "gi-005",
|
| 252 |
+
"specialty": "Gastroenterology",
|
| 253 |
+
"title": "USPSTF/ACG Colorectal Cancer Screening Guidelines (2021)",
|
| 254 |
+
"source": "U.S. Preventive Services Task Force / American Cancer Society",
|
| 255 |
+
"url": "https://www.uspreventiveservicestaskforce.org/uspstf/recommendation/colorectal-cancer-screening",
|
| 256 |
+
"text": "Colorectal cancer screening: Average-risk adults start at age 45 (USPSTF and ACS recommendation). Screen until age 75 (selective screening 76-85 based on individual health, prior screening, life expectancy). Screening options: Stool-based tests: FIT (fecal immunochemical test) annually, FIT-DNA (Cologuard) every 1-3 years. Direct visualization: colonoscopy every 10 years (gold standard), CT colonography every 5 years, flexible sigmoidoscopy every 5 years. Positive stool-based test requires follow-up colonoscopy. High-risk patients (first-degree relative with CRC <60 or ≥2 FDR with CRC at any age): colonoscopy starting at age 40 or 10 years before youngest affected relative, whichever is earlier; repeat every 5 years. Hereditary syndromes (Lynch, FAP): genetic counseling, earlier and more frequent surveillance per syndrome-specific protocols. Post-polypectomy surveillance: 1-2 small tubular adenomas — repeat colonoscopy in 7-10 years; 3-4 small adenomas or any advanced adenoma — repeat in 3 years; >10 adenomas — repeat in 1 year."
|
| 257 |
+
},
|
| 258 |
+
{
|
| 259 |
+
"id": "id-001",
|
| 260 |
+
"specialty": "Infectious Disease",
|
| 261 |
+
"title": "CDC STI Treatment Guidelines (2021)",
|
| 262 |
+
"source": "Centers for Disease Control and Prevention",
|
| 263 |
+
"url": "https://www.cdc.gov/std/treatment-guidelines/",
|
| 264 |
+
"text": "Chlamydia: Doxycycline 100mg PO BID x 7 days (preferred over azithromycin due to superior efficacy, especially for rectal infection). Alternative: azithromycin 1g PO x 1 dose. Gonorrhea: Ceftriaxone 500mg IM x 1 dose (1g if ≥150kg); co-treat for chlamydia unless excluded by NAAT. If cephalosporin allergy: gentamicin 240mg IM + azithromycin 2g PO. Test of cure: NAAT 7-14 days after treatment for pharyngeal gonorrhea and if alternative regimen used. Syphilis (primary/secondary): Benzathine penicillin G 2.4 million units IM x 1. Latent syphilis (early <1 year): same. Late latent or unknown duration: benzathine penicillin G 2.4M units IM weekly x 3 weeks. Neurosyphilis: aqueous penicillin G 18-24 million units/day IV for 10-14 days. Penicillin allergy in syphilis: desensitize and treat with penicillin. Genital herpes (first episode): valacyclovir 1g PO BID x 7-10 days. Suppressive therapy: valacyclovir 500mg-1g PO daily. Trichomoniasis: metronidazole 500mg PO BID x 7 days (women), 2g PO x 1 dose (men). Screen for HIV, hepatitis B/C in all STI patients. Partner notification and treatment."
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"id": "id-002",
|
| 268 |
+
"specialty": "Infectious Disease",
|
| 269 |
+
"title": "IDSA UTI Guidelines (2010/2022 Update)",
|
| 270 |
+
"source": "Infectious Diseases Society of America",
|
| 271 |
+
"url": "https://academic.oup.com/cid/article/52/5/e103/388680",
|
| 272 |
+
"text": "Uncomplicated UTI (cystitis) in women: First-line: nitrofurantoin 100mg BID x 5 days (avoid if CrCl <30) or trimethoprim-sulfamethoxazole (TMP-SMX) DS BID x 3 days (if local resistance <20%) or fosfomycin 3g single dose. Avoid fluoroquinolones for uncomplicated cystitis (reserve for other indications). Acute uncomplicated pyelonephritis (outpatient, non-severe): fluoroquinolone (ciprofloxacin 500mg BID x 7 days or levofloxacin 750mg daily x 5 days) or TMP-SMX DS BID x 14 days (if susceptible, with initial parenteral dose of ceftriaxone or aminoglycoside). Inpatient pyelonephritis/urosepsis: IV ceftriaxone 1-2g daily, piperacillin-tazobactam, or carbapenem if MDR risk. Obtain urine culture and blood cultures before antibiotics. Complicated UTI (structural/functional abnormality, catheter-associated, male): broader spectrum, longer duration (7-14 days), address underlying factor. Recurrent UTI (≥3/year): consider prophylaxis — nitrofurantoin 100mg nightly, cranberry products (limited evidence), vaginal estrogen in postmenopausal women. Asymptomatic bacteriuria: treat ONLY in pregnancy or before urologic procedures."
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"id": "id-003",
|
| 276 |
+
"specialty": "Infectious Disease",
|
| 277 |
+
"title": "IDSA HIV Treatment Guidelines (2023)",
|
| 278 |
+
"source": "Department of Health and Human Services",
|
| 279 |
+
"url": "https://clinicalinfo.hiv.gov/en/guidelines/adult-and-adolescent-arv/whats-new",
|
| 280 |
+
"text": "HIV treatment: Recommended for ALL persons with HIV regardless of CD4 count — start ART as soon as possible after diagnosis (rapid ART initiation). Preferred initial regimens: Bictegravir/emtricitabine/tenofovir alafenamide (Biktarvy) — single tablet, once daily, high barrier to resistance. Alternative: dolutegravir + emtricitabine/tenofovir (TAF or TDF). Long-acting option: cabotegravir + rilpivirine IM every 2 months (for virologically suppressed patients). Monitor: HIV viral load at 2-8 weeks, then every 3-6 months until undetectable, then every 6 months; CD4 count every 3-6 months initially, can discontinue monitoring when consistently >500. Resistance testing: genotype at entry to care and if virologic failure. Opportunistic infection prophylaxis: PCP prophylaxis (TMP-SMX) if CD4 <200; toxoplasmosis prophylaxis (TMP-SMX DS) if CD4 <100 and Toxo IgG+; MAC prophylaxis (azithromycin 1200mg weekly) if CD4 <50, though less commonly needed with early ART. PrEP for HIV prevention: daily oral (TDF/FTC), or long-acting cabotegravir IM every 2 months. Screen all patients for STIs, hepatitis, and latent TB."
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"id": "id-004",
|
| 284 |
+
"specialty": "Infectious Disease",
|
| 285 |
+
"title": "IDSA Skin and Soft Tissue Infection Guidelines (2014)",
|
| 286 |
+
"source": "Infectious Diseases Society of America",
|
| 287 |
+
"url": "https://academic.oup.com/cid/article/59/2/e10/2895845",
|
| 288 |
+
"text": "Skin and soft tissue infections (SSTI): Non-purulent cellulitis/erysipelas: predominantly streptococcal. Mild — oral antibiotics: cephalexin 500mg QID or dicloxacillin 500mg QID x 5-7 days. Moderate — IV cefazolin or nafcillin. Severe (sepsis, failed oral) — IV vancomycin + piperacillin-tazobactam. Purulent SSTI (abscess, furuncle, carbuncle): Incision and drainage is PRIMARY treatment. Mild (no systemic signs) — I&D alone may suffice. Moderate (systemic signs) — I&D + TMP-SMX DS BID or doxycycline 100mg BID x 5-7 days (MRSA coverage). Severe — I&D + IV vancomycin or daptomycin. Necrotizing fasciitis: Surgical emergency — broad-spectrum antibiotics (vancomycin + piperacillin-tazobactam + clindamycin for toxin suppression) AND urgent surgical debridement. High mortality if surgical delay >24 hours. Clinical clues: pain out of proportion, skin necrosis, crepitus, hemodynamic instability, rapidly spreading. CT/MRI may show fascial thickening/gas but should not delay surgery if clinical suspicion is high. LRINEC score may aid diagnosis but sensitivity limited."
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"id": "id-005",
|
| 292 |
+
"specialty": "Infectious Disease",
|
| 293 |
+
"title": "NIH COVID-19 Treatment Guidelines (2024)",
|
| 294 |
+
"source": "National Institutes of Health",
|
| 295 |
+
"url": "https://www.covid19treatmentguidelines.nih.gov/",
|
| 296 |
+
"text": "COVID-19 management (2024 update): Outpatient treatment for high-risk patients with mild-moderate COVID-19 (within 5-7 days of symptom onset): nirmatrelvir/ritonavir (Paxlovid) — preferred first-line (CYP3A inhibitor — check drug interactions). Alternative: remdesivir IV x 3 days. Molnupiravir — alternative when above not available/appropriate. Hospitalized, non-severe (no supplemental O2): Remdesivir x 5 days may be considered in high-risk patients. Hospitalized, moderate (supplemental O2): Remdesivir + dexamethasone 6mg daily x up to 10 days. Hospitalized, severe (high-flow O2/NIV): Dexamethasone 6mg daily (up to 10 days) + remdesivir. Consider tocilizumab or baricitinib if rapidly increasing O2 needs, especially with elevated CRP. Critically ill (mechanical ventilation/ECMO): Dexamethasone 6mg daily. Do NOT use tocilizumab or baricitinib de novo in intubated patients unless recently intubated within 24 hours of escalation. Anticoagulation: prophylactic-dose heparin for non-critically ill hospitalized patients; therapeutic-dose heparin for non-critically ill with elevated D-dimer. Vaccination remains the most effective preventive measure."
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"id": "psych-001",
|
| 300 |
+
"specialty": "Psychiatry",
|
| 301 |
+
"title": "APA Major Depressive Disorder Guidelines (2023)",
|
| 302 |
+
"source": "American Psychiatric Association",
|
| 303 |
+
"url": "https://psychiatryonline.org/doi/book/10.1176/appi.books.9780890424462",
|
| 304 |
+
"text": "Major depressive disorder (MDD) treatment: Mild-moderate — psychotherapy (CBT or IPT) alone is appropriate initial treatment. Moderate-severe — antidepressant medication, or combination of medication + psychotherapy (most effective). First-line antidepressants: SSRIs (sertraline, escitalopram, fluoxetine), SNRIs (venlafaxine, duloxetine), bupropion, mirtazapine. Choice based on side effect profile, comorbidities, prior response, cost. Onset of effect typically 2-4 weeks; adequate trial is 4-8 weeks at therapeutic dose. If partial response, optimize dose. If no response after adequate trial, switch to different class or augment. Augmentation strategies: aripiprazole, lithium, thyroid hormone (T3), bupropion addition. Treatment-resistant depression (failed ≥2 adequate trials): consider esketamine nasal spray, TMS (transcranial magnetic stimulation), ECT (most effective for severe/refractory depression, psychotic features, catatonia, acute suicidality). Maintenance: Continue antidepressant for ≥6-9 months after remission for first episode; indefinite for recurrent episodes (≥3) or chronic depression. Screen for bipolar disorder before starting antidepressants. Monitor suicidality, especially in adolescents and young adults (FDA black box warning <25 years)."
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"id": "psych-002",
|
| 308 |
+
"specialty": "Psychiatry",
|
| 309 |
+
"title": "Suicide Risk Assessment and Prevention Guidelines (2023)",
|
| 310 |
+
"source": "American Psychiatric Association / VA/DoD",
|
| 311 |
+
"url": "https://www.psychiatry.org/psychiatrists/practice/clinical-practice-guidelines",
|
| 312 |
+
"text": "Suicide risk assessment: Use structured assessment — Columbia Suicide Severity Rating Scale (C-SSRS) or PHQ-9 Item 9. Risk factors: prior attempt (strongest predictor), psychiatric disorder (depression, bipolar, schizophrenia, PTSD, substance use), access to lethal means (firearms), recent loss/crisis, chronic pain, social isolation, family history of suicide. Protective factors: social connectedness, therapeutic relationship, restricted access to means, reasons for living, religious/spiritual beliefs. Acute management of high risk: ensure safety (constant observation, 1:1), lethal means restriction counseling (especially firearms — ask about access, encourage safe storage or temporary removal), voluntary or involuntary psychiatric hospitalization if imminent risk. Pharmacological interventions that reduce suicide risk: lithium (strongest evidence for bipolar and recurrent depression), clozapine (for schizophrenia), ketamine/esketamine (acute suicidal ideation). Safety planning: collaborative brief intervention — identify warning signs, coping strategies, social contacts for support, professional/agency contacts, means restriction, reasons for living. Follow-up: contact within 24-72 hours of ED/inpatient discharge (transition period is highest risk)."
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"id": "psych-003",
|
| 316 |
+
"specialty": "Psychiatry",
|
| 317 |
+
"title": "APA Generalized Anxiety Disorder Guidelines (2023)",
|
| 318 |
+
"source": "American Psychiatric Association",
|
| 319 |
+
"url": "https://psychiatryonline.org/doi/book/10.1176/appi.books.9780890424462",
|
| 320 |
+
"text": "Generalized anxiety disorder (GAD): Excessive worry about multiple domains for ≥6 months with ≥3 of: restlessness, fatigue, difficulty concentrating, irritability, muscle tension, sleep disturbance. First-line pharmacotherapy: SSRIs (escitalopram, sertraline, paroxetine) or SNRIs (venlafaxine, duloxetine). Start low, titrate slowly. Adequate trial: 4-8 weeks at therapeutic dose. Buspirone: effective anxiolytic without sedation or dependence risk; can be used as monotherapy or augmentation. Benzodiazepines: use short-term only for acute/severe anxiety; avoid long-term due to dependence, tolerance, cognitive effects, fall risk in elderly. If needed, short-acting (lorazepam) or long-acting (clonazepam). Psychotherapy: CBT is first-line (evidence Level A) — cognitive restructuring, exposure, relaxation training. Combination of CBT + medication more effective than either alone for moderate-severe GAD. Second-line/augmentation: pregabalin, hydroxyzine, mirtazapine. Treatment duration: continue medication ≥12 months after response, taper gradually. Assess and treat comorbid conditions: depression (very common), substance use, other anxiety disorders, insomnia."
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"id": "psych-004",
|
| 324 |
+
"specialty": "Psychiatry",
|
| 325 |
+
"title": "APA Substance Use Disorder Guidelines (2023)",
|
| 326 |
+
"source": "American Psychiatric Association / ASAM",
|
| 327 |
+
"url": "https://www.asam.org/quality-care/clinical-guidelines",
|
| 328 |
+
"text": "Opioid use disorder (OUD): Medications for OUD (MOUD) are first-line, life-saving treatment. Buprenorphine (sublingual, injection): partial agonist, can be prescribed in office settings (X-waiver no longer required post-2023). Initiation: typically when in mild-moderate withdrawal (COWS ≥8-12). Standard dose: 8-24 mg/day sublingual. Methadone: full agonist, requires OTP (opioid treatment program). Standard dose: 60-120 mg/day. Naltrexone (extended-release injection, Vivitrol): opioid antagonist, 380mg IM monthly — requires 7-14 days opioid-free before initiation. All three reduce opioid use, overdose, and mortality. Combine with psychosocial support. Alcohol use disorder (AUD): Naltrexone 50mg PO daily or 380mg IM monthly (reduces heavy drinking, craving). Acamprosate 666mg TID (supports abstinence). Disulfiram 250mg daily (aversion therapy). Alcohol withdrawal: CIWA protocol — benzodiazepines (chlordiazepoxide, lorazepam, diazepam) titrated to symptom severity. Severe withdrawal/delirium tremens: ICU-level care, IV benzodiazepines, consider phenobarbital adjunct. Wernicke encephalopathy: IV thiamine 500mg TID x 3-5 days BEFORE glucose."
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"id": "peds-001",
|
| 332 |
+
"specialty": "Pediatrics",
|
| 333 |
+
"title": "AAP Fever Without Source in Infants Guidelines (2021)",
|
| 334 |
+
"source": "American Academy of Pediatrics",
|
| 335 |
+
"url": "https://pediatrics.aappublications.org/content/148/2/e2021052228",
|
| 336 |
+
"text": "Fever without apparent source in well-appearing young infants (8-60 days): Risk stratification using inflammatory markers (WBC, ANC, CRP, procalcitonin), urinalysis, and clinical appearance. Neonates 0-28 days with fever ≥38.0°C (100.4°F): Full sepsis workup (blood culture, urine culture, LP with CSF culture), empiric antibiotics (ampicillin + cefotaxime or gentamicin), hospitalize regardless of appearance. Infants 29-60 days: If well-appearing with ALL low-risk criteria (normal urinalysis, ANC <4000, procalcitonin <0.5 ng/mL, CRP <20 mg/L): may observe at home without antibiotics with close follow-up in 24 hours (option to obtain blood/urine cultures). If ANY high-risk marker: obtain cultures (blood, urine ± CSF), empiric IV antibiotics (ceftriaxone), and hospitalize. UTI is the most common serious bacterial infection in this age group — always obtain urinalysis and culture via catheterization or suprapubic aspiration. For infants 2-24 months: evaluate based on clinical appearance and vaccination status."
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"id": "peds-002",
|
| 340 |
+
"specialty": "Pediatrics",
|
| 341 |
+
"title": "NAEPP/GINA Pediatric Asthma Guidelines (2020/2024)",
|
| 342 |
+
"source": "National Asthma Education and Prevention Program / GINA",
|
| 343 |
+
"url": "https://www.nhlbi.nih.gov/health-topics/guidelines-for-diagnosis-management-of-asthma",
|
| 344 |
+
"text": "Pediatric asthma management (ages 5-11): Stepwise approach. Step 1: As-needed SABA (albuterol) for intermittent asthma. Step 2: Low-dose ICS daily (fluticasone 44mcg 1 puff BID or budesonide nebulization 250mcg daily) — preferred for mild persistent. Alternative: LTRA (montelukast 5mg daily for 6-14y). Step 3: Medium-dose ICS or low-dose ICS + LABA (fluticasone/salmeterol). Step 4: Medium-dose ICS + LABA. Step 5: High-dose ICS + LABA, consider biologic therapy for severe allergic or eosinophilic asthma. Acute exacerbation (ED management): Albuterol nebulization 2.5-5mg every 20 min x 3, or continuous nebulization for severe. Add ipratropium bromide 0.5mg for moderate-severe. Systemic corticosteroids (prednisolone 1-2 mg/kg, max 60mg) early. Magnesium sulfate 25-50 mg/kg IV (max 2g) for severe, refractory exacerbation. Assess severity with PEF, SpO2, work of breathing. Asthma action plan: Green (well) / Yellow (caution: increase ICS, add SABA) / Red (emergency: SABA every 20 min, oral steroid, seek care). Review at every visit: symptom control, inhaler technique, adherence, triggers."
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"id": "peds-003",
|
| 348 |
+
"specialty": "Pediatrics",
|
| 349 |
+
"title": "AAP/WHO Pediatric Dehydration Management (2023)",
|
| 350 |
+
"source": "American Academy of Pediatrics / World Health Organization",
|
| 351 |
+
"url": "https://www.who.int/publications/i/item/9789241548373",
|
| 352 |
+
"text": "Pediatric dehydration assessment: Mild (3-5%): slightly dry mucous membranes, slightly decreased UOP, normal vitals. Moderate (6-9%): sunken eyes/fontanelle, decreased skin turgor, tachycardia, decreased UOP, irritable/lethargic. Severe (≥10%): signs of shock — tachycardia, poor perfusion, hypotension, altered consciousness, absent UOP. Oral rehydration therapy (ORT): First-line for mild-moderate dehydration — oral rehydration solution (ORS) 50-100 mL/kg over 3-4 hours. WHO ORS: 75 mEq/L sodium. Replace ongoing losses (10 mL/kg for each watery stool). Continue breastfeeding. Avoid high-sugar drinks (juice, soda) — osmotic diarrhea. IV rehydration for severe dehydration or ORT failure: Normal saline or lactated Ringer's 20 mL/kg bolus, repeat as needed (up to 60 mL/kg in first hour for shock). Then calculate deficit replacement + maintenance + ongoing losses over 24 hours. Maintenance fluids (Holliday-Segar): 4 mL/kg/hr for first 10 kg, +2 mL/kg/hr for next 10 kg, +1 mL/kg/hr for each kg over 20. Use isotonic fluids (D5-NS or D5-LR) for maintenance to prevent hyponatremia. Monitor and correct electrolyte abnormalities (especially Na+, K+). Ondansetron 0.15 mg/kg (max 4mg) PO/IV for vomiting to facilitate ORT."
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"id": "peds-004",
|
| 356 |
+
"specialty": "Pediatrics",
|
| 357 |
+
"title": "AAP Neonatal Hyperbilirubinemia Guidelines (2022)",
|
| 358 |
+
"source": "American Academy of Pediatrics",
|
| 359 |
+
"url": "https://publications.aap.org/pediatrics/article/150/3/e2022058859/188443",
|
| 360 |
+
"text": "Neonatal jaundice management: Universal predischarge bilirubin screening (TSB or TcB) for all newborns. Plot on hour-specific Bhutani nomogram to determine risk zone. Risk factors for severe hyperbilirubinemia: exclusive breastfeeding with poor intake/weight loss, gestational age <38 weeks, significant bruising (cephalohematoma), ABO/Rh incompatibility, prior sibling with jaundice treated, East Asian ethnicity, elevated predischarge bilirubin level. Phototherapy thresholds: Based on total serum bilirubin (TSB) plotted against gestational age and risk factors using AAP 2022 phototherapy nomogram. For term infants with no risk factors, phototherapy typically initiated at TSB ~18-20 mg/dL at 48-72 hours. Exchange transfusion: When TSB exceeds exchange transfusion threshold (typically 25+ mg/dL in term infant), or when intensive phototherapy fails to reduce bilirubin adequately, or if signs of acute bilirubin encephalopathy (hypertonia, retrocollis, opisthotonos, fever, high-pitched cry). Monitoring: Recheck TSB 4-8 hours after starting phototherapy. Adequate hydration and feeding essential. Discontinue phototherapy when TSB below threshold; recheck for rebound 12-24 hours after stopping."
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"id": "renal-001",
|
| 364 |
+
"specialty": "Nephrology",
|
| 365 |
+
"title": "KDIGO CKD Guidelines (2024)",
|
| 366 |
+
"source": "Kidney Disease: Improving Global Outcomes",
|
| 367 |
+
"url": "https://kdigo.org/guidelines/ckd-evaluation-and-management/",
|
| 368 |
+
"text": "Chronic kidney disease (CKD) staging by GFR: G1 (≥90, normal/high), G2 (60-89, mildly decreased), G3a (45-59, mild-moderately decreased), G3b (30-44, moderately-severely decreased), G4 (15-29, severely decreased), G5 (<15, kidney failure). Albuminuria staging: A1 (<30 mg/g), A2 (30-300 mg/g), A3 (>300 mg/g). Management: BP target <120 systolic (SPRINT-based) for CKD patients; ACEi or ARB first-line for proteinuric CKD (A2-A3). NEW: SGLT2 inhibitors (dapagliflozin, empagliflozin) recommended for CKD with eGFR ≥20 regardless of diabetes status — reduces progression to kidney failure and cardiovascular events. NEW: Finerenone (non-steroidal MRA) for diabetic kidney disease with persistent albuminuria despite RAS blockade. Avoid nephrotoxins (NSAIDs, aminoglycosides, contrast without precautions). Metabolic management: treat metabolic acidosis (oral bicarbonate if serum bicarb <22), hyperkalemia, hyperphosphatemia (phosphate binders), secondary hyperparathyroidism (vitamin D, calcimimetics). Anemia: ESAs (target Hb 10-11.5), IV iron (TSAT <30%, ferritin <500). HIF-PHIs (roxadustat) as alternative to ESAs. Dialysis preparation: educate when eGFR <30; AV fistula creation when eGFR <20; initiate dialysis based on symptoms rather than GFR alone. Kidney transplant evaluation for all eligible patients."
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"id": "renal-002",
|
| 372 |
+
"specialty": "Nephrology",
|
| 373 |
+
"title": "KDIGO Acute Kidney Injury Guidelines (2012/2024 Update)",
|
| 374 |
+
"source": "Kidney Disease: Improving Global Outcomes",
|
| 375 |
+
"url": "https://kdigo.org/guidelines/acute-kidney-injury/",
|
| 376 |
+
"text": "Acute kidney injury (AKI) staging (KDIGO): Stage 1: SCr increase ≥0.3 mg/dL within 48h or 1.5-1.9x baseline within 7 days, or UOP <0.5 mL/kg/h for 6-12h. Stage 2: SCr 2.0-2.9x baseline, or UOP <0.5 mL/kg/h for ≥12h. Stage 3: SCr ≥3.0x baseline or SCr ≥4.0 mg/dL, or initiated on RRT, or UOP <0.3 mL/kg/h for ≥24h or anuria ≥12h. Etiologic categories: Pre-renal (hypovolemia, hypotension, cardiorenal, hepatorenal) — FENa <1%, BUN/Cr ratio >20. Intrinsic renal (ATN, AIN, glomerulonephritis) — FENa >2%, muddy brown casts (ATN). Post-renal (obstruction) — hydronephrosis on US. Management: Optimize hemodynamics (euvolemia, adequate MAP), discontinue nephrotoxins, adjust drug doses for GFR, avoid hyperglycemia and hyperchloremia. Indications for urgent dialysis (AEIOU): Acidosis (refractory pH <7.1), Electrolyte emergencies (K >6.5 refractory), Ingestion/toxin removal (methanol, ethylene glycol, lithium, salicylate), Overload (pulmonary edema refractory to diuretics), Uremia symptoms (encephalopathy, pericarditis, bleeding). IV fluids: balanced crystalloids preferred (LR) over normal saline. Avoid contrast when possible; if needed, use iso-osmolar contrast with pre-hydration."
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"id": "heme-001",
|
| 380 |
+
"specialty": "Hematology",
|
| 381 |
+
"title": "ASH VTE Treatment Guidelines (2020)",
|
| 382 |
+
"source": "American Society of Hematology",
|
| 383 |
+
"url": "https://ashpublications.org/bloodadvances/article/4/19/4693/463232",
|
| 384 |
+
"text": "Venous thromboembolism (VTE) treatment — deep vein thrombosis (DVT) and pulmonary embolism (PE): Initial anticoagulation: DOACs preferred for most patients — rivaroxaban 15mg BID x 21 days then 20mg daily, or apixaban 10mg BID x 7 days then 5mg BID (single-drug approach, no parenteral lead-in needed). Alternative: LMWH (enoxaparin 1mg/kg BID) or fondaparinux for 5+ days overlapping with warfarin until INR 2-3, then warfarin alone. Or LMWH lead-in then switch to dabigatran 150mg BID or edoxaban 60mg daily. Duration: Provoked VTE by transient risk factor (surgery, immobilization, estrogen): 3 months. Unprovoked VTE: ≥3 months, then assess for extended anticoagulation (indefinite) based on bleeding risk vs recurrence risk. Cancer-associated VTE: LMWH or edoxaban/rivaroxaban preferred; increased GI bleeding risk with DOACs in luminal GI or urothelial cancers — use LMWH. IVC filter: only for acute VTE with absolute contraindication to anticoagulation; retrievable filter preferred, remove when anticoagulation can be resumed. Thrombolysis: for massive PE with hemodynamic instability (see PE guideline). Upper extremity DVT: anticoagulation for ≥3 months. Subsegmental PE: individualize based on risk factors."
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"id": "heme-002",
|
| 388 |
+
"specialty": "Hematology",
|
| 389 |
+
"title": "ASH Sickle Cell Disease Guidelines (2020)",
|
| 390 |
+
"source": "American Society of Hematology",
|
| 391 |
+
"url": "https://ashpublications.org/bloodadvances/article/4/8/1554/454444",
|
| 392 |
+
"text": "Sickle cell disease (SCD) management: Vaso-occlusive crisis (VOC): Aggressive pain management within 30 minutes — IV opioids (morphine, hydromorphone) with PCA, NSAIDs as adjunct, IV fluids at maintenance rate (avoid over-hydration). Acute chest syndrome (ACS): Fever/respiratory symptoms + new pulmonary infiltrate. Treatment: antibiotics (cephalosporin + macrolide), simple or exchange transfusion (target Hb 10 g/dL, HbS <30%), supplemental O2, incentive spirometry, bronchodilators. Consider exchange transfusion for severe ACS. Stroke: Exchange transfusion urgently to reduce HbS <30%. Chronic transfusion program for stroke prevention in children with TCD velocities ≥200 cm/s. Disease-modifying therapies: Hydroxyurea — first-line for all SCD patients ≥9 months (reduce VOC, ACS, transfusion need, mortality). Target dose: maximum tolerated dose (usually 15-35 mg/kg/day). L-glutamine (Endari): add-on therapy. Crizanlizumab (anti-P-selectin): reduces VOC frequency. Voxelotor: increases hemoglobin by inhibiting HbS polymerization. Gene therapy (Casgevy/Lyfgenia): potentially curative for eligible patients. Health maintenance: annual TCD (ages 2-16), retinopathy screening, renal function monitoring, iron overload assessment if transfused, pneumococcal prophylaxis, folic acid supplementation."
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"id": "rheum-001",
|
| 396 |
+
"specialty": "Rheumatology",
|
| 397 |
+
"title": "ACR/EULAR Rheumatoid Arthritis Guidelines (2021)",
|
| 398 |
+
"source": "American College of Rheumatology / European League Against Rheumatism",
|
| 399 |
+
"url": "https://www.rheumatology.org/Practice-Quality/Clinical-Support/Clinical-Practice-Guidelines",
|
| 400 |
+
"text": "Rheumatoid arthritis (RA) management: Treat-to-target strategy: aim for remission or low disease activity. Initiate DMARD therapy as early as possible after diagnosis. First-line: methotrexate (MTX) 15-25 mg/week (oral or subcutaneous) with folic acid 1mg daily. If MTX contraindicated: leflunomide or sulfasalazine. Glucocorticoids: short-term bridge therapy (prednisone ≤7.5mg/day, taper within 3-6 months). If inadequate response to MTX monotherapy at 3-6 months: add biologic DMARD (anti-TNF: adalimumab, etanercept, infliximab, certolizumab, golimumab; or non-TNF: abatacept, rituximab, tocilizumab, sarilumab) or targeted synthetic DMARD (JAK inhibitors: tofacitinib, baricitinib, upadacitinib). Prefer biologics + MTX combination over biologic monotherapy. If first biologic fails: switch mechanism of action. Screening before biologics: TB (quantiferon), hepatitis B/C, vaccination review (no live vaccines on biologics/DMARDs). Monitor: CBC, LFTs, renal function every 3 months on MTX. Disease activity assessment: DAS28, CDAI, or SDAI every 3-6 months. Joint protection, exercise, occupational therapy as adjuncts."
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"id": "rheum-002",
|
| 404 |
+
"specialty": "Rheumatology",
|
| 405 |
+
"title": "ACR Gout Management Guidelines (2020)",
|
| 406 |
+
"source": "American College of Rheumatology",
|
| 407 |
+
"url": "https://www.rheumatology.org/Practice-Quality/Clinical-Support/Clinical-Practice-Guidelines/Gout",
|
| 408 |
+
"text": "Gout management: Acute flare treatment: Colchicine 1.2mg then 0.6mg 1 hour later (within 36 hours of flare onset); NSAIDs (indomethacin 50mg TID or naproxen 500mg BID); or corticosteroids (prednisone 0.5 mg/kg/day for 5-10 days, or intra-articular injection). Combination of these agents for severe polyarticular flares. IL-1 inhibitor (anakinra, canakinumab) for refractory flares. Do NOT withhold or change urate-lowering therapy (ULT) during a flare. ULT indications: ≥2 flares/year, tophi, urate arthropathy, CKD stage ≥3, urolithiasis. First-line ULT: Allopurinol — start 100mg/day (50mg if CKD stage ≥3), titrate by 100mg increments every 2-5 weeks. Target serum urate <6 mg/dL (or <5 mg/dL if tophi). Alternative: febuxostat 40-80mg daily (avoid in cardiovascular disease per CARES trial). Pegloticase for refractory gout with immunomodulator co-therapy. Anti-inflammatory prophylaxis during ULT initiation: colchicine 0.6mg daily or BID for 3-6 months, or low-dose NSAID or low-dose prednisone (≤10mg/day). Lifestyle: limit alcohol (especially beer), reduce purine-rich foods, weight management, adequate hydration."
|
| 409 |
+
},
|
| 410 |
+
{
|
| 411 |
+
"id": "screen-001",
|
| 412 |
+
"specialty": "Preventive Medicine",
|
| 413 |
+
"title": "USPSTF Comprehensive Screening Recommendations (2024)",
|
| 414 |
+
"source": "U.S. Preventive Services Task Force",
|
| 415 |
+
"url": "https://www.uspreventiveservicestaskforce.org/",
|
| 416 |
+
"text": "Key USPSTF screening recommendations: Lung cancer: Annual low-dose CT for adults aged 50-80 with ≥20 pack-year smoking history who currently smoke or quit within past 15 years (Grade B). Colorectal cancer: Screen ages 45-75 (Grade A: 50-75, Grade B: 45-49). Breast cancer: Biennial mammography ages 40-74 (Grade B). Cervical cancer: Pap every 3 years ages 21-29; HPV testing or co-testing every 5 years ages 30-65 (Grade A). Prostate cancer: PSA-based screening ages 55-69, shared decision-making (Grade C). AAA screening: one-time ultrasound for men 65-75 who have ever smoked (Grade B). Hepatitis C: universal screening for all adults 18-79 (Grade B). Hepatitis B: universal screening for adults 15-65 (Grade B). HIV: screening all adults 15-65 (Grade A). Depression: screening for all adults, including pregnant/postpartum (Grade B). Prediabetes/T2DM: screen adults 35-70 who are overweight/obese (Grade B). Osteoporosis: DXA screening for women ≥65, younger postmenopausal women at risk (Grade B). Statin use: for adults 40-75 with CVD risk factors and 10-year ASCVD risk ≥10% (Grade B). Aspirin: do NOT initiate for primary CVD prevention in adults ≥60 (Grade D)."
|
| 417 |
+
},
|
| 418 |
+
{
|
| 419 |
+
"id": "anes-001",
|
| 420 |
+
"specialty": "Perioperative Medicine",
|
| 421 |
+
"title": "ACC/AHA Perioperative Cardiovascular Evaluation Guidelines (2014)",
|
| 422 |
+
"source": "American College of Cardiology / American Heart Association",
|
| 423 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2014.07.944",
|
| 424 |
+
"text": "Preoperative cardiovascular risk assessment for non-cardiac surgery: Step 1 — Is the surgery emergent? If yes, proceed to surgery with perioperative risk optimization. Step 2 — Does the patient have an acute coronary syndrome? If yes, manage ACS per guidelines before elective surgery. Step 3 — Estimate perioperative risk using RCRI (Revised Cardiac Risk Index): IHD, CHF, CVD, DM on insulin, CKD (Cr >2), high-risk surgery. Each factor = 1 point. RCRI 0 = ~3.9% risk, 1 = ~6%, 2 = ~10.1%, ≥3 = ~15%. Step 4 — Assess functional capacity: If ≥4 METs (able to climb flight of stairs, walk uphill, do heavy housework), proceed to surgery without further testing. If <4 METs or unknown: will further testing change management? If surgery-specific risk is elevated and stress testing will change management, consider pharmacologic stress testing. Continue beta-blockers if already on them. Do NOT start beta-blockers on the day of surgery. Continue statins perioperatively. Hold ACEi/ARBs on morning of surgery (risk of intraoperative hypotension). Antiplatelet management: continuation vs. holding based on stent type and time since implantation (dual antiplatelet for minimum 6 months post-DES). Perioperative cardiac monitoring per institutional protocols."
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"id": "derm-001",
|
| 428 |
+
"specialty": "Dermatology",
|
| 429 |
+
"title": "AAD Melanoma Detection and Management Guidelines (2019)",
|
| 430 |
+
"source": "American Academy of Dermatology",
|
| 431 |
+
"url": "https://www.aad.org/member/clinical-quality/guidelines/melanoma",
|
| 432 |
+
"text": "Melanoma: ABCDE criteria for suspicious lesions: A — Asymmetry, B — Border irregularity, C — Color variation, D — Diameter >6mm, E — Evolving (change in size, shape, color, or new symptom like itching/bleeding). Risk factors: UV exposure, fair skin, >50 moles, family history, prior melanoma, dysplastic nevi, immunosuppression. Biopsy: excisional biopsy preferred for suspicious lesions (punch or shave for sites where excision is impractical). Breslow thickness determines staging and management: ≤1.0mm (Stage I) — wide local excision (WLE) with 1cm margins. 1.01-2.0mm — WLE 1-2cm margins, sentinel lymph node biopsy (SLNB) recommended. >2.0mm — WLE 2cm margins, SLNB recommended. Positive SLNB: complete lymph node dissection vs. observation with serial ultrasound (both acceptable per MSLT-II). Adjuvant therapy for Stage III: immune checkpoint inhibitors (nivolumab, pembrolizumab) or targeted therapy (dabrafenib + trametinib for BRAF-mutant). Stage IV (metastatic): immunotherapy (nivolumab + ipilimumab or pembrolizumab) or targeted therapy (BRAF/MEK inhibitors). Regular skin surveillance: every 3-6 months for 5 years, then annually."
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"id": "obgyn-001",
|
| 436 |
+
"specialty": "Obstetrics/Gynecology",
|
| 437 |
+
"title": "ACOG Hypertensive Disorders of Pregnancy Guidelines (2020)",
|
| 438 |
+
"source": "American College of Obstetricians and Gynecologists",
|
| 439 |
+
"url": "https://www.acog.org/clinical/clinical-guidance/practice-bulletin/articles/2020/06/gestational-hypertension-and-preeclampsia",
|
| 440 |
+
"text": "Hypertensive disorders of pregnancy: Chronic HTN: preexisting or diagnosed <20 weeks. Gestational HTN: new HTN ≥140/90 after 20 weeks without proteinuria/end-organ dysfunction. Preeclampsia: HTN + proteinuria (≥300mg/24h or protein/creatinine ratio ≥0.3) or end-organ dysfunction (thrombocytopenia <100K, elevated LFTs 2x normal, renal insufficiency Cr >1.1, cerebral/visual symptoms, pulmonary edema). Preeclampsia with severe features: SBP ≥160 or DBP ≥110 (on 2 readings), platelets <100K, liver transaminases 2x ULN, severe persistent RUQ/epigastric pain, renal insufficiency, pulmonary edema, new-onset headache unresponsive to meds, visual disturbances. Management: Mild preeclampsia without severe features and <37 weeks: close monitoring, delivery at 37 weeks. Preeclampsia with severe features: hospitalize, give magnesium sulfate (4g IV load then 1-2g/hr for seizure prophylaxis), antihypertensives for severe-range BP (IV labetalol, IV hydralazine, or oral nifedipine), deliver at 34+ weeks (or earlier if maternal/fetal deterioration). HELLP syndrome (Hemolysis, Elevated Liver enzymes, Low Platelets): urgent delivery regardless of gestational age. Eclampsia: MgSO4 for seizure control + delivery. Prevention: low-dose aspirin 81mg starting at 12-16 weeks for high-risk patients."
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"id": "obgyn-002",
|
| 444 |
+
"specialty": "Obstetrics/Gynecology",
|
| 445 |
+
"title": "ACOG Postpartum Hemorrhage Guidelines (2017/2023)",
|
| 446 |
+
"source": "American College of Obstetricians and Gynecologists",
|
| 447 |
+
"url": "https://www.acog.org/clinical/clinical-guidance/practice-bulletin/articles/2017/10/postpartum-hemorrhage",
|
| 448 |
+
"text": "Postpartum hemorrhage (PPH): Cumulative blood loss ≥1000 mL or bleeding with hypovolemia signs regardless of delivery mode. Most common cause: uterine atony (70%). Other causes (4 T's): Tone (atony), Trauma (lacerations, hematoma, uterine rupture/inversion), Tissue (retained placenta/membranes), Thrombin (coagulopathy). Stage-based management: Stage 1 (1000-1500 mL, vital signs stable): uterine massage, uterotonics — oxytocin 10-40 units in 500-1000 mL NS IV, methylergonovine 0.2mg IM (avoid in HTN), carboprost 250mcg IM (avoid in asthma), misoprostol 800-1000mcg sublingual/rectal. Stage 2 (1500-2000 mL or continued bleeding): escalate uterotonics, intrauterine balloon tamponade (Bakri balloon), tranexamic acid 1g IV (within 3 hours of onset), consider uterine compression sutures (B-Lynch). Activate massive transfusion protocol. Stage 3 (>2000 mL or DIC): surgical intervention — uterine artery ligation, internal iliac artery ligation, hysterectomy. Consider uterine artery embolization if interventional radiology available. Transfusion: 1:1:1 (PRBC:FFP:platelets). Prevention: active management of third stage (oxytocin after delivery), TXA at cesarean for high-risk patients."
|
| 449 |
+
},
|
| 450 |
+
{
|
| 451 |
+
"id": "endo-006",
|
| 452 |
+
"specialty": "Endocrinology",
|
| 453 |
+
"title": "ADA Hypoglycemia Management Guidelines (2024)",
|
| 454 |
+
"source": "American Diabetes Association",
|
| 455 |
+
"url": "https://diabetesjournals.org/care/issue/47/Supplement_1",
|
| 456 |
+
"text": "Hypoglycemia classification: Level 1 (glucose <70 mg/dL, alert value), Level 2 (<54 mg/dL, clinically significant), Level 3 (severe, requiring external assistance regardless of glucose level). Symptoms: adrenergic (tremor, palpitations, diaphoresis, hunger, anxiety) at glucose ~70; neuroglycopenic (confusion, behavioral changes, seizures, LOC) at glucose <54. Treatment of conscious patient (Rule of 15): 15-20g fast-acting carbohydrate (glucose tablets, juice, regular soda), recheck glucose in 15 minutes, repeat if still <70 mg/dL. Once glucose normalizes, eat a snack/meal to prevent recurrence. Treatment of severe hypoglycemia (unconscious/unable to eat): Glucagon — injectable (1mg IM/subQ), nasal (3mg intranasal), or ready-to-use auto-injector (0.5mg or 1mg subQ/IM depending on age). In hospital: dextrose 50% (D50) 25-50 mL IV push. Prevention strategies: review and adjust insulin/sulfonylurea doses, CGM (continuous glucose monitoring) for insulin users, hypoglycemia awareness training, relaxed A1C targets for patients with hypoglycemia unawareness or recurrent severe hypoglycemia. Hypoglycemia unawareness: strict avoidance of hypoglycemia for 2-3 weeks can restore awareness. Medications associated with hypoglycemia: insulin, sulfonylureas (glipizide, glyburide, glimepiride), meglitinides."
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"id": "em-007",
|
| 460 |
+
"specialty": "Emergency Medicine",
|
| 461 |
+
"title": "Toxicology — Overdose and Poisoning Emergency Management",
|
| 462 |
+
"source": "American Association of Poison Control Centers / ACMT",
|
| 463 |
+
"url": "https://www.poison.org/",
|
| 464 |
+
"text": "General approach to poisoning/overdose: Stabilize (ABCDE), obtain history (substance, amount, time, co-ingestants), consult Poison Control (1-800-222-1222). Decontamination: Activated charcoal 1g/kg (max 50g) within 1-2 hours of ingestion (if airway protected and no caustic/hydrocarbon). Avoid ipecac. Whole bowel irrigation for sustained-release formulations, iron, lithium, body packers. Specific antidotes: Acetaminophen (APAP): N-acetylcysteine (NAC) — use Rumack-Matthew nomogram; NAC indicated if 4-hour level above treatment line. IV protocol: 150mg/kg over 1 hour, then 50mg/kg over 4 hours, then 100mg/kg over 16 hours. Opioids: Naloxone 0.4-2mg IV/IM/IN, repeat every 2-3 minutes (titrate to respiratory effort, not consciousness). Benzodiazepines: Flumazenil 0.2mg IV (use with caution — seizure risk in chronic benzo use or mixed overdose). Organophosphates: Atropine 2mg IV doubled every 5 min + pralidoxime 1-2g IV. TCA overdose: Sodium bicarbonate 1-2 mEq/kg IV for QRS >100ms. Beta-blocker: Glucagon 3-10mg IV. Calcium channel blocker: Calcium chloride/gluconate, high-dose insulin-euglycemia therapy. Methanol/ethylene glycol: Fomepizole 15mg/kg IV load + hemodialysis. Toxic alcohols: Ethanol drip or fomepizole. Carbon monoxide: 100% O2, consider HBO for severe (LOC, cardiac ischemia, pregnancy)."
|
| 465 |
+
},
|
| 466 |
+
{
|
| 467 |
+
"id": "em-008",
|
| 468 |
+
"specialty": "Emergency Medicine",
|
| 469 |
+
"title": "Hyperkalemia Emergency Management (AHA/Consensus 2021)",
|
| 470 |
+
"source": "American Heart Association / Consensus Guidelines",
|
| 471 |
+
"url": "https://www.ahajournals.org/doi/10.1161/CIRCULATIONAHA.121.055855",
|
| 472 |
+
"text": "Hyperkalemia: Mild (5.0-5.9 mEq/L), Moderate (6.0-6.4), Severe (≥6.5 or any level with ECG changes). ECG changes progression: peaked T waves → prolonged PR → loss of P waves → widened QRS → sine wave → VF/asystole. Immediate management for severe/symptomatic: (1) Cardiac membrane stabilization: Calcium gluconate 10% 10-20 mL IV over 2-3 minutes (or calcium chloride via central line) — onset 1-3 min, duration 30-60 min, repeat in 5 min if ECG unchanged. (2) Transcellular shift: Regular insulin 10 units IV + dextrose 25g IV (D50 50mL) — onset 15-30 min, duration 4-6 hours. Albuterol 10-20mg nebulized — onset 15-30 min. Sodium bicarbonate 50 mEq IV — mainly if acidotic, less effective alone. (3) Potassium removal: Sodium polystyrene sulfonate (Kayexalate) — limited/slow effect, use with caution. Patiromer or sodium zirconium cyclosilicate (Lokelma) — newer K+ binders, better tolerated. Loop diuretics (furosemide 40-80mg IV) if adequate renal function. Hemodialysis: definitive treatment for severe/refractory hyperkalemia, especially in ESRD or AKI. Continuous telemetry until K+ corrected. Identify and treat underlying cause (medications — ACEi, ARBs, K+-sparing diuretics, NSAIDs, TMP-SMX; renal failure; tissue destruction; acidosis)."
|
| 473 |
+
},
|
| 474 |
+
{
|
| 475 |
+
"id": "em-009",
|
| 476 |
+
"specialty": "Emergency Medicine",
|
| 477 |
+
"title": "Acute Abdomen — Appendicitis, Cholecystitis, Bowel Obstruction",
|
| 478 |
+
"source": "American College of Surgeons / WSES Guidelines",
|
| 479 |
+
"url": "https://www.sages.org/publications/guidelines/",
|
| 480 |
+
"text": "Acute appendicitis: Alvarado score (MANTRELS: Migration, Anorexia, Nausea, Tenderness RLQ, Rebound, Elevated temp, Leukocytosis, Shift to left). CT abdomen/pelvis with IV contrast (sensitivity >98%) for diagnosis. Uncomplicated appendicitis: laparoscopic appendectomy (standard of care) or antibiotic trial in select cases (amoxicillin-clavulanate or ciprofloxacin + metronidazole). Complicated (perforated/abscess): antibiotics ± percutaneous drainage if large abscess; interval appendectomy in 6-8 weeks vs. early appendectomy based on clinical scenario. Acute cholecystitis: RUQ pain, positive Murphy's sign, fever, elevated WBC. Ultrasound: gallbladder wall thickening >4mm, pericholecystic fluid, sonographic Murphy's sign. Tokyo Guidelines severity grading. Grade I (mild): early laparoscopic cholecystectomy (within 72 hours preferred). Grade II (moderate, WBC >18K, palpable mass, duration >72h): early surgery if tolerated, or percutaneous cholecystostomy if poor surgical candidate. Grade III (severe, organ dysfunction): stabilize + cholecystostomy. Antibiotics: ceftriaxone + metronidazole or piperacillin-tazobactam. Small bowel obstruction (SBO): NPO + NGT decompression + IV fluids. CT with IV and oral contrast to assess partial vs complete, strangulation signs (mesenteric haziness, absent bowel wall enhancement). Partial SBO: conservative trial 48-72 hours with Gastrografin challenge. Complete/strangulated SBO: urgent surgical exploration."
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"id": "endo-007",
|
| 484 |
+
"specialty": "Endocrinology",
|
| 485 |
+
"title": "Endocrine Society Hypercalcemia and Hyperparathyroidism Guidelines (2022)",
|
| 486 |
+
"source": "Endocrine Society / AACE",
|
| 487 |
+
"url": "https://academic.oup.com/jcem/article/107/8/2115/6591661",
|
| 488 |
+
"text": "Hypercalcemia: Mild (10.5-11.9 mg/dL), Moderate (12.0-13.9), Severe (≥14.0 or symptomatic). Most common causes: primary hyperparathyroidism (outpatient) and malignancy (inpatient). Symptoms: stones, bones, groans, psychiatric moans (nephrolithiasis, bone pain, abdominal pain/constipation/nausea, confusion/depression). Acute management of hypercalcemic crisis (Ca >14 or symptomatic): (1) Aggressive IV hydration — NS 200-500 mL/hr initially (adjust for cardiac/renal function). (2) Calcitonin 4 IU/kg IM/subQ every 12 hours (rapid onset but tachyphylaxis in 48 hours). (3) IV bisphosphonate — zoledronic acid 4mg IV over 15 minutes (onset 2-4 days, duration 2-4 weeks) or pamidronate 60-90mg IV over 2-4 hours. (4) Denosumab for bisphosphonate-refractory cases. (5) Glucocorticoids for granulomatous disease or lymphoma-related hypercalcemia. Avoid loop diuretics unless volume overloaded. Primary hyperparathyroidism: Parathyroidectomy is definitive treatment. Surgical indications (asymptomatic PHPT): age <50, Ca >1 mg/dL above ULN, T-score <-2.5, vertebral fracture, CrCl <60, 24h urine calcium >400 mg, nephrolithiasis/nephrocalcinosis. Calcimimetics (cinacalcet) for patients who decline/cannot undergo surgery. Monitor those managed non-surgically with annual serum calcium, BMD every 1-2 years, renal function."
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"id": "em-010",
|
| 492 |
+
"specialty": "Emergency Medicine",
|
| 493 |
+
"title": "Acute Aortic Syndrome — Aortic Dissection (ACC/AHA 2022)",
|
| 494 |
+
"source": "American College of Cardiology / American Heart Association",
|
| 495 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2022.08.004",
|
| 496 |
+
"text": "Aortic dissection: Stanford Type A (involves ascending aorta — surgical emergency) and Type B (descending aorta only — typically medical management). Clinical presentation: sudden-onset severe tearing/ripping chest or back pain, often radiating. May present with pulse/BP differential between arms, aortic regurgitation murmur, stroke symptoms, malperfusion syndrome (mesenteric, renal, limb ischemia). Diagnostic workup: D-dimer negative has high negative predictive value in low-risk patients. CT angiography (CTA) of entire aorta is gold standard (sensitivity >98%). TEE for hemodynamically unstable patients unable to transport to CT. Immediate management: Pain control (IV morphine/fentanyl), anti-impulse therapy — target HR <60 and SBP 100-120 mmHg. First-line: IV esmolol (500 mcg/kg bolus then 50-200 mcg/kg/min) or IV labetalol 20mg bolus then 1-2 mg/min infusion. If BP remains elevated: add IV nicardipine or nitroprusside (only after adequate beta-blockade). Type A dissection: emergent surgical repair (mortality increases 1-2% per hour of delay). Type B uncomplicated: medical management, serial imaging. Type B complicated (malperfusion, rapid expansion, rupture): urgent TEVAR (thoracic endovascular aortic repair). Long-term: lifelong antihypertensive therapy, serial aortic imaging (CTA or MRA at 1, 3, 6, 12 months then annually)."
|
| 497 |
+
}
|
| 498 |
+
]
|
src/backend/app/main.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Clinical Decision Support Agent — FastAPI Backend
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
|
| 7 |
+
from app.api import cases, health, ws
|
| 8 |
+
from app.config import settings
|
| 9 |
+
|
| 10 |
+
app = FastAPI(
|
| 11 |
+
title="Clinical Decision Support Agent",
|
| 12 |
+
description="Agentic clinical decision support powered by MedGemma (HAI-DEF)",
|
| 13 |
+
version="0.1.0",
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# CORS for frontend
|
| 17 |
+
app.add_middleware(
|
| 18 |
+
CORSMiddleware,
|
| 19 |
+
allow_origins=settings.cors_origins,
|
| 20 |
+
allow_credentials=True,
|
| 21 |
+
allow_methods=["*"],
|
| 22 |
+
allow_headers=["*"],
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Routes
|
| 26 |
+
app.include_router(health.router, tags=["health"])
|
| 27 |
+
app.include_router(cases.router, prefix="/api/cases", tags=["cases"])
|
| 28 |
+
app.include_router(ws.router, prefix="/ws", tags=["websocket"])
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@app.on_event("startup")
|
| 32 |
+
async def startup():
|
| 33 |
+
"""Initialize services on startup."""
|
| 34 |
+
# TODO: Initialize MedGemma model / connection
|
| 35 |
+
# TODO: Initialize RAG vector store
|
| 36 |
+
pass
|
src/backend/app/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Models (Pydantic schemas) package
|
src/backend/app/models/schemas.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Domain models for the Clinical Decision Support Agent.
|
| 3 |
+
|
| 4 |
+
These Pydantic models define the structured data flowing through the agent pipeline.
|
| 5 |
+
Every tool consumes and produces typed models — no loose dicts or unstructured text.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from datetime import date, datetime
|
| 10 |
+
from enum import Enum
|
| 11 |
+
from typing import List, Optional
|
| 12 |
+
from pydantic import BaseModel, Field
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ──────────────────────────────────────────────
|
| 16 |
+
# Enums
|
| 17 |
+
# ──────────────────────────────────────────────
|
| 18 |
+
|
| 19 |
+
class Gender(str, Enum):
|
| 20 |
+
MALE = "male"
|
| 21 |
+
FEMALE = "female"
|
| 22 |
+
OTHER = "other"
|
| 23 |
+
UNKNOWN = "unknown"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Severity(str, Enum):
|
| 27 |
+
LOW = "low"
|
| 28 |
+
MODERATE = "moderate"
|
| 29 |
+
HIGH = "high"
|
| 30 |
+
CRITICAL = "critical"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Confidence(str, Enum):
|
| 34 |
+
LOW = "low"
|
| 35 |
+
MODERATE = "moderate"
|
| 36 |
+
HIGH = "high"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class AgentStepStatus(str, Enum):
|
| 40 |
+
PENDING = "pending"
|
| 41 |
+
RUNNING = "running"
|
| 42 |
+
COMPLETED = "completed"
|
| 43 |
+
FAILED = "failed"
|
| 44 |
+
SKIPPED = "skipped"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ──────────────────────────────────────────────
|
| 48 |
+
# Patient Data Models
|
| 49 |
+
# ──────────────────────────────────────────────
|
| 50 |
+
|
| 51 |
+
class Medication(BaseModel):
|
| 52 |
+
name: str = Field(..., description="Medication name")
|
| 53 |
+
dose: Optional[str] = Field(None, description="Dosage, e.g. '10mg daily'")
|
| 54 |
+
rxcui: Optional[str] = Field(None, description="RxNorm concept ID")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class LabResult(BaseModel):
|
| 58 |
+
test_name: str = Field(..., description="Lab test name")
|
| 59 |
+
value: str = Field(..., description="Result value with units")
|
| 60 |
+
reference_range: Optional[str] = Field(None, description="Normal reference range")
|
| 61 |
+
is_abnormal: Optional[bool] = Field(None, description="Whether result is abnormal")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class VitalSigns(BaseModel):
|
| 65 |
+
blood_pressure: Optional[str] = None
|
| 66 |
+
heart_rate: Optional[str] = None
|
| 67 |
+
temperature: Optional[str] = None
|
| 68 |
+
respiratory_rate: Optional[str] = None
|
| 69 |
+
oxygen_saturation: Optional[str] = None
|
| 70 |
+
weight: Optional[str] = None
|
| 71 |
+
height: Optional[str] = None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class PatientProfile(BaseModel):
|
| 75 |
+
"""Structured patient profile — output of the Patient Data Parser tool."""
|
| 76 |
+
age: Optional[int] = None
|
| 77 |
+
gender: Gender = Gender.UNKNOWN
|
| 78 |
+
chief_complaint: str = Field(..., description="Primary reason for visit")
|
| 79 |
+
history_of_present_illness: str = Field("", description="HPI narrative")
|
| 80 |
+
past_medical_history: List[str] = Field(default_factory=list)
|
| 81 |
+
current_medications: List[Medication] = Field(default_factory=list)
|
| 82 |
+
allergies: List[str] = Field(default_factory=list)
|
| 83 |
+
lab_results: List[LabResult] = Field(default_factory=list)
|
| 84 |
+
vital_signs: Optional[VitalSigns] = None
|
| 85 |
+
social_history: Optional[str] = None
|
| 86 |
+
family_history: Optional[str] = None
|
| 87 |
+
additional_notes: Optional[str] = None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ──────────────────────────────────────────────
|
| 91 |
+
# Clinical Reasoning Models
|
| 92 |
+
# ──────────────────────────────────────────────
|
| 93 |
+
|
| 94 |
+
class DiagnosisCandidate(BaseModel):
|
| 95 |
+
diagnosis: str = Field(..., description="Diagnosis name")
|
| 96 |
+
icd10_code: Optional[str] = Field(None, description="ICD-10 code if known")
|
| 97 |
+
likelihood: Confidence = Field(..., description="Estimated likelihood")
|
| 98 |
+
supporting_evidence: List[str] = Field(default_factory=list, description="Evidence from patient data")
|
| 99 |
+
reasoning: str = Field("", description="Clinical reasoning chain")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class RecommendedAction(BaseModel):
|
| 103 |
+
action: str = Field(..., description="Recommended action (test, referral, treatment)")
|
| 104 |
+
priority: Severity = Field(..., description="Priority level")
|
| 105 |
+
rationale: str = Field("", description="Why this action is recommended")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class ClinicalReasoningResult(BaseModel):
|
| 109 |
+
"""Output of the Clinical Reasoning Agent (MedGemma)."""
|
| 110 |
+
differential_diagnosis: List[DiagnosisCandidate] = Field(
|
| 111 |
+
default_factory=list, description="Ranked differential diagnosis"
|
| 112 |
+
)
|
| 113 |
+
risk_assessment: Optional[str] = Field(None, description="Overall risk assessment")
|
| 114 |
+
recommended_workup: List[RecommendedAction] = Field(
|
| 115 |
+
default_factory=list, description="Recommended tests, referrals, treatments"
|
| 116 |
+
)
|
| 117 |
+
reasoning_chain: str = Field("", description="Full chain-of-thought reasoning")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ──────────────────────────────────────────────
|
| 121 |
+
# Drug Interaction Models
|
| 122 |
+
# ──────────────────────────────────────���───────
|
| 123 |
+
|
| 124 |
+
class DrugInteraction(BaseModel):
|
| 125 |
+
drug_a: str
|
| 126 |
+
drug_b: str
|
| 127 |
+
severity: Severity
|
| 128 |
+
description: str
|
| 129 |
+
clinical_significance: Optional[str] = None
|
| 130 |
+
source: str = Field("OpenFDA", description="Data source")
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class DrugInteractionResult(BaseModel):
|
| 134 |
+
"""Output of the Drug Interaction Checker tool."""
|
| 135 |
+
interactions_found: List[DrugInteraction] = Field(default_factory=list)
|
| 136 |
+
medications_checked: List[str] = Field(default_factory=list)
|
| 137 |
+
warnings: List[str] = Field(default_factory=list)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# ──────────────────────────────────────────────
|
| 141 |
+
# Guideline Retrieval Models
|
| 142 |
+
# ──────────────────────────────────────────────
|
| 143 |
+
|
| 144 |
+
class GuidelineExcerpt(BaseModel):
|
| 145 |
+
title: str = Field(..., description="Guideline or source title")
|
| 146 |
+
excerpt: str = Field(..., description="Relevant excerpt text")
|
| 147 |
+
source: str = Field(..., description="Publication or organization")
|
| 148 |
+
url: Optional[str] = None
|
| 149 |
+
relevance_score: Optional[float] = None
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class GuidelineRetrievalResult(BaseModel):
|
| 153 |
+
"""Output of the Guideline Retrieval (RAG) tool."""
|
| 154 |
+
query: str = Field(..., description="The query used for retrieval")
|
| 155 |
+
excerpts: List[GuidelineExcerpt] = Field(default_factory=list)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# ──────────────────────────────────────────────
|
| 159 |
+
# Final CDS Report
|
| 160 |
+
# ──────────────────────────────────────────────
|
| 161 |
+
|
| 162 |
+
class CDSReport(BaseModel):
|
| 163 |
+
"""
|
| 164 |
+
The final Clinical Decision Support report — synthesized by MedGemma
|
| 165 |
+
from all tool outputs. This is what the clinician sees.
|
| 166 |
+
"""
|
| 167 |
+
patient_summary: str = Field(..., description="Concise patient summary")
|
| 168 |
+
differential_diagnosis: List[DiagnosisCandidate] = Field(default_factory=list)
|
| 169 |
+
drug_interaction_warnings: List[DrugInteraction] = Field(default_factory=list)
|
| 170 |
+
guideline_recommendations: List[str] = Field(
|
| 171 |
+
default_factory=list, description="Guideline-concordant recommendations"
|
| 172 |
+
)
|
| 173 |
+
suggested_next_steps: List[RecommendedAction] = Field(default_factory=list)
|
| 174 |
+
caveats: List[str] = Field(
|
| 175 |
+
default_factory=list,
|
| 176 |
+
description="Limitations, uncertainties, and disclaimers"
|
| 177 |
+
)
|
| 178 |
+
sources_cited: List[str] = Field(default_factory=list)
|
| 179 |
+
generated_at: datetime = Field(default_factory=datetime.utcnow)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# ──────────────────────────────────────────────
|
| 183 |
+
# Agent Orchestration Models
|
| 184 |
+
# ──────────────────────────────────────────────
|
| 185 |
+
|
| 186 |
+
class AgentStep(BaseModel):
|
| 187 |
+
"""Represents a single step in the agent pipeline, streamed to the frontend."""
|
| 188 |
+
step_id: str
|
| 189 |
+
step_name: str
|
| 190 |
+
status: AgentStepStatus = AgentStepStatus.PENDING
|
| 191 |
+
tool_name: Optional[str] = None
|
| 192 |
+
input_summary: Optional[str] = None
|
| 193 |
+
output_summary: Optional[str] = None
|
| 194 |
+
duration_ms: Optional[int] = None
|
| 195 |
+
error: Optional[str] = None
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class AgentState(BaseModel):
|
| 199 |
+
"""Full state of the agent pipeline for a given case."""
|
| 200 |
+
case_id: str
|
| 201 |
+
steps: List[AgentStep] = Field(default_factory=list)
|
| 202 |
+
patient_profile: Optional[PatientProfile] = None
|
| 203 |
+
clinical_reasoning: Optional[ClinicalReasoningResult] = None
|
| 204 |
+
drug_interactions: Optional[DrugInteractionResult] = None
|
| 205 |
+
guideline_retrieval: Optional[GuidelineRetrievalResult] = None
|
| 206 |
+
final_report: Optional[CDSReport] = None
|
| 207 |
+
started_at: Optional[datetime] = None
|
| 208 |
+
completed_at: Optional[datetime] = None
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ──────────────────────────────────────────────
|
| 212 |
+
# API Request / Response Models
|
| 213 |
+
# ──────────────────────────────────────────────
|
| 214 |
+
|
| 215 |
+
class CaseSubmission(BaseModel):
|
| 216 |
+
"""API request to submit a new patient case for analysis."""
|
| 217 |
+
patient_text: str = Field(
|
| 218 |
+
...,
|
| 219 |
+
description="Free-text patient case description or structured data",
|
| 220 |
+
min_length=10,
|
| 221 |
+
)
|
| 222 |
+
include_drug_check: bool = Field(True, description="Run drug interaction check")
|
| 223 |
+
include_guidelines: bool = Field(True, description="Retrieve relevant guidelines")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
class CaseResponse(BaseModel):
|
| 227 |
+
"""API response for a submitted case."""
|
| 228 |
+
case_id: str
|
| 229 |
+
status: str
|
| 230 |
+
message: str
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class CaseResult(BaseModel):
|
| 234 |
+
"""API response with the full case result."""
|
| 235 |
+
case_id: str
|
| 236 |
+
state: AgentState
|
| 237 |
+
report: Optional[CDSReport] = None
|
src/backend/app/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Services package
|
src/backend/app/services/medgemma.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MedGemma Service — handles all communication with the MedGemma model.
|
| 3 |
+
|
| 4 |
+
Supports two modes:
|
| 5 |
+
1. API mode — calls MedGemma via an OpenAI-compatible API endpoint
|
| 6 |
+
2. Local mode — loads the model locally via transformers (for edge/offline)
|
| 7 |
+
|
| 8 |
+
All tools that need MedGemma go through this service.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Any, Optional, Type, TypeVar
|
| 15 |
+
|
| 16 |
+
from pydantic import BaseModel
|
| 17 |
+
|
| 18 |
+
from app.config import settings
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
T = TypeVar("T", bound=BaseModel)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class MedGemmaService:
|
| 26 |
+
"""
|
| 27 |
+
Unified interface for MedGemma inference.
|
| 28 |
+
|
| 29 |
+
Usage:
|
| 30 |
+
service = MedGemmaService()
|
| 31 |
+
result = await service.generate("Analyze this patient case...", max_tokens=2048)
|
| 32 |
+
structured = await service.generate_structured("...", ResponseModel)
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
self._client = None
|
| 37 |
+
self._local_model = None
|
| 38 |
+
self._mode = "api" if settings.medgemma_base_url else "local"
|
| 39 |
+
|
| 40 |
+
async def _get_client(self):
|
| 41 |
+
"""Lazy-initialize the API client."""
|
| 42 |
+
if self._client is None:
|
| 43 |
+
try:
|
| 44 |
+
from openai import AsyncOpenAI
|
| 45 |
+
self._client = AsyncOpenAI(
|
| 46 |
+
api_key=settings.medgemma_api_key or "not-needed",
|
| 47 |
+
base_url=settings.medgemma_base_url or "http://localhost:8000/v1",
|
| 48 |
+
)
|
| 49 |
+
except ImportError:
|
| 50 |
+
raise RuntimeError(
|
| 51 |
+
"openai package required for API mode. Install with: pip install openai"
|
| 52 |
+
)
|
| 53 |
+
return self._client
|
| 54 |
+
|
| 55 |
+
async def generate(
|
| 56 |
+
self,
|
| 57 |
+
prompt: str,
|
| 58 |
+
system_prompt: Optional[str] = None,
|
| 59 |
+
max_tokens: int = 0,
|
| 60 |
+
temperature: float = 0.3,
|
| 61 |
+
) -> str:
|
| 62 |
+
"""
|
| 63 |
+
Generate text from MedGemma.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
prompt: The user prompt
|
| 67 |
+
system_prompt: Optional system prompt for context setting
|
| 68 |
+
max_tokens: Max tokens to generate (0 = use default from config)
|
| 69 |
+
temperature: Sampling temperature
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Generated text response
|
| 73 |
+
"""
|
| 74 |
+
max_tokens = max_tokens or settings.medgemma_max_tokens
|
| 75 |
+
|
| 76 |
+
if self._mode == "api":
|
| 77 |
+
return await self._generate_api(prompt, system_prompt, max_tokens, temperature)
|
| 78 |
+
else:
|
| 79 |
+
return await self._generate_local(prompt, system_prompt, max_tokens, temperature)
|
| 80 |
+
|
| 81 |
+
async def generate_structured(
|
| 82 |
+
self,
|
| 83 |
+
prompt: str,
|
| 84 |
+
response_model: Type[T],
|
| 85 |
+
system_prompt: Optional[str] = None,
|
| 86 |
+
max_tokens: int = 0,
|
| 87 |
+
temperature: float = 0.2,
|
| 88 |
+
) -> T:
|
| 89 |
+
"""
|
| 90 |
+
Generate a structured (Pydantic model) response from MedGemma.
|
| 91 |
+
|
| 92 |
+
Appends JSON schema instructions to the prompt and parses the response.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
prompt: The user prompt
|
| 96 |
+
response_model: Pydantic model class to parse the response into
|
| 97 |
+
system_prompt: Optional system prompt
|
| 98 |
+
max_tokens: Max tokens
|
| 99 |
+
temperature: Sampling temperature
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Parsed Pydantic model instance
|
| 103 |
+
"""
|
| 104 |
+
schema = response_model.model_json_schema()
|
| 105 |
+
structured_prompt = (
|
| 106 |
+
f"{prompt}\n\n"
|
| 107 |
+
f"Respond ONLY with valid JSON matching this schema:\n"
|
| 108 |
+
f"```json\n{json.dumps(schema, indent=2)}\n```\n"
|
| 109 |
+
f"Do not include any text outside the JSON."
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
raw = await self.generate(structured_prompt, system_prompt, max_tokens, temperature)
|
| 113 |
+
|
| 114 |
+
# Extract JSON from response (handle markdown code blocks)
|
| 115 |
+
json_str = self._extract_json(raw)
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
data = json.loads(json_str)
|
| 119 |
+
return response_model.model_validate(data)
|
| 120 |
+
except (json.JSONDecodeError, Exception) as e:
|
| 121 |
+
logger.warning(f"Failed to parse structured response: {e}. Raw: {raw[:200]}")
|
| 122 |
+
# Retry with stricter prompt as fallback
|
| 123 |
+
raise ValueError(f"MedGemma returned invalid JSON for {response_model.__name__}: {e}")
|
| 124 |
+
|
| 125 |
+
async def _generate_api(
|
| 126 |
+
self, prompt: str, system_prompt: Optional[str], max_tokens: int, temperature: float
|
| 127 |
+
) -> str:
|
| 128 |
+
"""Generate via OpenAI-compatible API."""
|
| 129 |
+
client = await self._get_client()
|
| 130 |
+
|
| 131 |
+
messages = []
|
| 132 |
+
# Some models (e.g. Gemma via Google AI Studio) don't support system role.
|
| 133 |
+
# Try with system prompt first, fall back to folding it into the user message.
|
| 134 |
+
if system_prompt:
|
| 135 |
+
user_content = f"{system_prompt}\n\n{prompt}"
|
| 136 |
+
else:
|
| 137 |
+
user_content = prompt
|
| 138 |
+
messages.append({"role": "user", "content": user_content})
|
| 139 |
+
|
| 140 |
+
response = await client.chat.completions.create(
|
| 141 |
+
model=settings.medgemma_model_id,
|
| 142 |
+
messages=messages,
|
| 143 |
+
max_tokens=max_tokens,
|
| 144 |
+
temperature=temperature,
|
| 145 |
+
)
|
| 146 |
+
return response.choices[0].message.content
|
| 147 |
+
|
| 148 |
+
async def _generate_local(
|
| 149 |
+
self, prompt: str, system_prompt: Optional[str], max_tokens: int, temperature: float
|
| 150 |
+
) -> str:
|
| 151 |
+
"""Generate via locally loaded model (transformers)."""
|
| 152 |
+
# TODO: Implement local inference with transformers
|
| 153 |
+
# This is the path for edge deployment or offline development
|
| 154 |
+
raise NotImplementedError(
|
| 155 |
+
"Local inference not yet implemented. "
|
| 156 |
+
"Set MEDGEMMA_BASE_URL to use API mode, or implement local loading."
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
@staticmethod
|
| 160 |
+
def _extract_json(text: str) -> str:
|
| 161 |
+
"""Extract JSON from a response that might include markdown code blocks."""
|
| 162 |
+
# Try to find JSON in ```json ... ``` blocks
|
| 163 |
+
if "```json" in text:
|
| 164 |
+
start = text.index("```json") + 7
|
| 165 |
+
end = text.index("```", start)
|
| 166 |
+
return text[start:end].strip()
|
| 167 |
+
if "```" in text:
|
| 168 |
+
start = text.index("```") + 3
|
| 169 |
+
end = text.index("```", start)
|
| 170 |
+
return text[start:end].strip()
|
| 171 |
+
# Try to find raw JSON
|
| 172 |
+
for i, char in enumerate(text):
|
| 173 |
+
if char in "{[":
|
| 174 |
+
# Find matching closing bracket
|
| 175 |
+
depth = 0
|
| 176 |
+
for j in range(i, len(text)):
|
| 177 |
+
if text[j] in "{[":
|
| 178 |
+
depth += 1
|
| 179 |
+
elif text[j] in "}]":
|
| 180 |
+
depth -= 1
|
| 181 |
+
if depth == 0:
|
| 182 |
+
return text[i : j + 1]
|
| 183 |
+
return text.strip()
|
src/backend/app/tools/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Tools package
|
src/backend/app/tools/clinical_reasoning.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool: Clinical Reasoning Agent
|
| 3 |
+
|
| 4 |
+
Uses MedGemma to perform clinical reasoning over a structured patient profile.
|
| 5 |
+
Generates differential diagnosis, risk assessment, and recommended workup.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
from app.models.schemas import (
|
| 12 |
+
ClinicalReasoningResult,
|
| 13 |
+
PatientProfile,
|
| 14 |
+
)
|
| 15 |
+
from app.services.medgemma import MedGemmaService
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
SYSTEM_PROMPT = """You are an expert clinical reasoning assistant. Given a structured
|
| 20 |
+
patient profile, perform systematic clinical reasoning to generate a differential
|
| 21 |
+
diagnosis, risk assessment, and recommended workup.
|
| 22 |
+
|
| 23 |
+
IMPORTANT GUIDELINES:
|
| 24 |
+
- Think step-by-step through the clinical reasoning process
|
| 25 |
+
- Consider the most likely diagnoses first, then less common but important ones
|
| 26 |
+
- Always consider dangerous "can't miss" diagnoses
|
| 27 |
+
- Base your reasoning on the available evidence (symptoms, labs, history)
|
| 28 |
+
- Be explicit about your reasoning chain
|
| 29 |
+
- Rate likelihood as "low", "moderate", or "high"
|
| 30 |
+
- Rate priority of actions as "low", "moderate", "high", or "critical"
|
| 31 |
+
- This is a decision SUPPORT tool — always recommend clinician judgment"""
|
| 32 |
+
|
| 33 |
+
REASONING_PROMPT = """Perform clinical reasoning on the following patient case.
|
| 34 |
+
|
| 35 |
+
PATIENT PROFILE:
|
| 36 |
+
- Age: {age}, Gender: {gender}
|
| 37 |
+
- Chief Complaint: {chief_complaint}
|
| 38 |
+
- HPI: {hpi}
|
| 39 |
+
- Past Medical History: {pmh}
|
| 40 |
+
- Current Medications: {medications}
|
| 41 |
+
- Allergies: {allergies}
|
| 42 |
+
- Lab Results: {labs}
|
| 43 |
+
- Vital Signs: {vitals}
|
| 44 |
+
- Social History: {social_hx}
|
| 45 |
+
- Family History: {family_hx}
|
| 46 |
+
|
| 47 |
+
Provide:
|
| 48 |
+
1. A ranked differential diagnosis (most likely first) with supporting evidence and reasoning
|
| 49 |
+
2. An overall risk assessment
|
| 50 |
+
3. Recommended workup (tests, referrals, treatments) with priority levels and rationale
|
| 51 |
+
4. Your full chain-of-thought reasoning"""
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ClinicalReasoningTool:
|
| 55 |
+
"""Uses MedGemma for clinical reasoning over patient data."""
|
| 56 |
+
|
| 57 |
+
def __init__(self):
|
| 58 |
+
self.medgemma = MedGemmaService()
|
| 59 |
+
|
| 60 |
+
async def run(self, profile: PatientProfile) -> ClinicalReasoningResult:
|
| 61 |
+
"""
|
| 62 |
+
Perform clinical reasoning on a patient profile.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
profile: Structured patient profile from the parser
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
ClinicalReasoningResult with differential diagnosis, risk, and recommendations
|
| 69 |
+
"""
|
| 70 |
+
prompt = REASONING_PROMPT.format(
|
| 71 |
+
age=profile.age or "Unknown",
|
| 72 |
+
gender=profile.gender.value,
|
| 73 |
+
chief_complaint=profile.chief_complaint,
|
| 74 |
+
hpi=profile.history_of_present_illness or "Not provided",
|
| 75 |
+
pmh=", ".join(profile.past_medical_history) if profile.past_medical_history else "None reported",
|
| 76 |
+
medications=self._format_medications(profile),
|
| 77 |
+
allergies=", ".join(profile.allergies) if profile.allergies else "NKDA",
|
| 78 |
+
labs=self._format_labs(profile),
|
| 79 |
+
vitals=self._format_vitals(profile),
|
| 80 |
+
social_hx=profile.social_history or "Not provided",
|
| 81 |
+
family_hx=profile.family_history or "Not provided",
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
result = await self.medgemma.generate_structured(
|
| 85 |
+
prompt=prompt,
|
| 86 |
+
response_model=ClinicalReasoningResult,
|
| 87 |
+
system_prompt=SYSTEM_PROMPT,
|
| 88 |
+
temperature=0.3,
|
| 89 |
+
max_tokens=4096,
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
logger.info(
|
| 93 |
+
f"Clinical reasoning complete: {len(result.differential_diagnosis)} diagnoses, "
|
| 94 |
+
f"{len(result.recommended_workup)} recommendations"
|
| 95 |
+
)
|
| 96 |
+
return result
|
| 97 |
+
|
| 98 |
+
@staticmethod
|
| 99 |
+
def _format_medications(profile: PatientProfile) -> str:
|
| 100 |
+
if not profile.current_medications:
|
| 101 |
+
return "None reported"
|
| 102 |
+
return "; ".join(
|
| 103 |
+
f"{m.name} {m.dose or ''}" for m in profile.current_medications
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
@staticmethod
|
| 107 |
+
def _format_labs(profile: PatientProfile) -> str:
|
| 108 |
+
if not profile.lab_results:
|
| 109 |
+
return "None available"
|
| 110 |
+
lines = []
|
| 111 |
+
for lab in profile.lab_results:
|
| 112 |
+
abnormal = " [ABNORMAL]" if lab.is_abnormal else ""
|
| 113 |
+
ref = f" (ref: {lab.reference_range})" if lab.reference_range else ""
|
| 114 |
+
lines.append(f"{lab.test_name}: {lab.value}{ref}{abnormal}")
|
| 115 |
+
return "; ".join(lines)
|
| 116 |
+
|
| 117 |
+
@staticmethod
|
| 118 |
+
def _format_vitals(profile: PatientProfile) -> str:
|
| 119 |
+
if not profile.vital_signs:
|
| 120 |
+
return "Not available"
|
| 121 |
+
v = profile.vital_signs
|
| 122 |
+
parts = []
|
| 123 |
+
if v.blood_pressure:
|
| 124 |
+
parts.append(f"BP {v.blood_pressure}")
|
| 125 |
+
if v.heart_rate:
|
| 126 |
+
parts.append(f"HR {v.heart_rate}")
|
| 127 |
+
if v.temperature:
|
| 128 |
+
parts.append(f"Temp {v.temperature}")
|
| 129 |
+
if v.respiratory_rate:
|
| 130 |
+
parts.append(f"RR {v.respiratory_rate}")
|
| 131 |
+
if v.oxygen_saturation:
|
| 132 |
+
parts.append(f"SpO2 {v.oxygen_saturation}")
|
| 133 |
+
return ", ".join(parts) if parts else "Not available"
|
src/backend/app/tools/drug_interactions.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool: Drug Interaction Checker
|
| 3 |
+
|
| 4 |
+
Checks for drug-drug interactions using the OpenFDA API and RxNorm
|
| 5 |
+
for medication normalization.
|
| 6 |
+
|
| 7 |
+
This is a NON-LLM tool — it queries external databases, demonstrating
|
| 8 |
+
that MedGemma works alongside traditional tools in the agent pipeline.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import logging
|
| 13 |
+
from typing import List
|
| 14 |
+
|
| 15 |
+
import httpx
|
| 16 |
+
|
| 17 |
+
from app.config import settings
|
| 18 |
+
from app.models.schemas import (
|
| 19 |
+
DrugInteraction,
|
| 20 |
+
DrugInteractionResult,
|
| 21 |
+
Medication,
|
| 22 |
+
Severity,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger(__name__)
|
| 26 |
+
|
| 27 |
+
OPENFDA_INTERACTION_URL = "https://api.fda.gov/drug/event.json"
|
| 28 |
+
RXNORM_INTERACTION_URL = "https://rxnav.nlm.nih.gov/REST/interaction/list.json"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class DrugInteractionTool:
|
| 32 |
+
"""Checks drug interactions via OpenFDA and RxNorm APIs."""
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
self._http_client: httpx.AsyncClient | None = None
|
| 36 |
+
|
| 37 |
+
async def _get_client(self) -> httpx.AsyncClient:
|
| 38 |
+
if self._http_client is None:
|
| 39 |
+
self._http_client = httpx.AsyncClient(timeout=30.0)
|
| 40 |
+
return self._http_client
|
| 41 |
+
|
| 42 |
+
async def run(
|
| 43 |
+
self,
|
| 44 |
+
current_medications: List[Medication],
|
| 45 |
+
proposed_medications: List[str] | None = None,
|
| 46 |
+
) -> DrugInteractionResult:
|
| 47 |
+
"""
|
| 48 |
+
Check for drug interactions among current and proposed medications.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
current_medications: Patient's current medication list
|
| 52 |
+
proposed_medications: Any new medications being considered
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
DrugInteractionResult with found interactions and warnings
|
| 56 |
+
"""
|
| 57 |
+
all_med_names = [m.name for m in current_medications]
|
| 58 |
+
if proposed_medications:
|
| 59 |
+
all_med_names.extend(proposed_medications)
|
| 60 |
+
|
| 61 |
+
if len(all_med_names) < 2:
|
| 62 |
+
return DrugInteractionResult(
|
| 63 |
+
medications_checked=all_med_names,
|
| 64 |
+
warnings=["Fewer than 2 medications — no interaction check needed"],
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
interactions = []
|
| 68 |
+
warnings = []
|
| 69 |
+
|
| 70 |
+
# Try RxNorm interaction API (NIH — free, no key needed)
|
| 71 |
+
try:
|
| 72 |
+
rxnorm_interactions = await self._check_rxnorm(all_med_names)
|
| 73 |
+
interactions.extend(rxnorm_interactions)
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logger.warning(f"RxNorm API failed: {e}")
|
| 76 |
+
warnings.append(f"RxNorm API unavailable: {e}")
|
| 77 |
+
|
| 78 |
+
# Try OpenFDA as supplementary source
|
| 79 |
+
try:
|
| 80 |
+
fda_interactions = await self._check_openfda(all_med_names)
|
| 81 |
+
interactions.extend(fda_interactions)
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.warning(f"OpenFDA API failed: {e}")
|
| 84 |
+
warnings.append(f"OpenFDA API unavailable: {e}")
|
| 85 |
+
|
| 86 |
+
# Deduplicate
|
| 87 |
+
interactions = self._deduplicate(interactions)
|
| 88 |
+
|
| 89 |
+
return DrugInteractionResult(
|
| 90 |
+
interactions_found=interactions,
|
| 91 |
+
medications_checked=all_med_names,
|
| 92 |
+
warnings=warnings,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
async def _check_rxnorm(self, med_names: List[str]) -> List[DrugInteraction]:
|
| 96 |
+
"""Query RxNorm Interaction API."""
|
| 97 |
+
client = await self._get_client()
|
| 98 |
+
interactions = []
|
| 99 |
+
|
| 100 |
+
# First, resolve drug names to RxCUIs
|
| 101 |
+
rxcuis = []
|
| 102 |
+
for name in med_names:
|
| 103 |
+
try:
|
| 104 |
+
resp = await client.get(
|
| 105 |
+
f"{settings.rxnorm_base_url}/rxcui.json",
|
| 106 |
+
params={"name": name, "search": 1},
|
| 107 |
+
)
|
| 108 |
+
if resp.status_code == 200:
|
| 109 |
+
data = resp.json()
|
| 110 |
+
id_group = data.get("idGroup", {})
|
| 111 |
+
rxnorm_id = id_group.get("rxnormId")
|
| 112 |
+
if rxnorm_id:
|
| 113 |
+
# rxnormId can be a list of strings
|
| 114 |
+
if isinstance(rxnorm_id, list):
|
| 115 |
+
rxcuis.append(rxnorm_id[0])
|
| 116 |
+
else:
|
| 117 |
+
rxcuis.append(str(rxnorm_id))
|
| 118 |
+
except Exception:
|
| 119 |
+
continue
|
| 120 |
+
|
| 121 |
+
if len(rxcuis) < 2:
|
| 122 |
+
return interactions
|
| 123 |
+
|
| 124 |
+
# Query interaction API with RxCUIs
|
| 125 |
+
try:
|
| 126 |
+
resp = await client.get(
|
| 127 |
+
RXNORM_INTERACTION_URL,
|
| 128 |
+
params={"rxcuis": "+".join(rxcuis)},
|
| 129 |
+
)
|
| 130 |
+
if resp.status_code == 200:
|
| 131 |
+
data = resp.json()
|
| 132 |
+
interaction_groups = data.get("fullInteractionTypeGroup", [])
|
| 133 |
+
for group in interaction_groups:
|
| 134 |
+
for itype in group.get("fullInteractionType", []):
|
| 135 |
+
for pair in itype.get("interactionPair", []):
|
| 136 |
+
desc = pair.get("description", "")
|
| 137 |
+
severity_str = pair.get("severity", "N/A")
|
| 138 |
+
names = [
|
| 139 |
+
concept.get("minConceptItem", {}).get("name", "Unknown")
|
| 140 |
+
for concept in pair.get("interactionConcept", [])
|
| 141 |
+
]
|
| 142 |
+
interactions.append(
|
| 143 |
+
DrugInteraction(
|
| 144 |
+
drug_a=names[0] if len(names) > 0 else "Unknown",
|
| 145 |
+
drug_b=names[1] if len(names) > 1 else "Unknown",
|
| 146 |
+
severity=self._map_severity(severity_str),
|
| 147 |
+
description=desc,
|
| 148 |
+
source="RxNorm/NLM",
|
| 149 |
+
)
|
| 150 |
+
)
|
| 151 |
+
except Exception as e:
|
| 152 |
+
logger.warning(f"RxNorm interaction query failed: {e}")
|
| 153 |
+
|
| 154 |
+
return interactions
|
| 155 |
+
|
| 156 |
+
async def _check_openfda(self, med_names: List[str]) -> List[DrugInteraction]:
|
| 157 |
+
"""Query OpenFDA for adverse event reports involving these drugs together."""
|
| 158 |
+
client = await self._get_client()
|
| 159 |
+
interactions = []
|
| 160 |
+
|
| 161 |
+
# Check pairs of drugs for co-reported adverse events
|
| 162 |
+
for i, drug_a in enumerate(med_names):
|
| 163 |
+
for drug_b in med_names[i + 1 :]:
|
| 164 |
+
try:
|
| 165 |
+
search = f'patient.drug.medicinalproduct:"{drug_a}"+AND+patient.drug.medicinalproduct:"{drug_b}"'
|
| 166 |
+
params = {"search": search, "limit": 1}
|
| 167 |
+
if settings.openfda_api_key:
|
| 168 |
+
params["api_key"] = settings.openfda_api_key
|
| 169 |
+
|
| 170 |
+
resp = await client.get(OPENFDA_INTERACTION_URL, params=params)
|
| 171 |
+
if resp.status_code == 200:
|
| 172 |
+
data = resp.json()
|
| 173 |
+
total = data.get("meta", {}).get("results", {}).get("total", 0)
|
| 174 |
+
if total > 100:
|
| 175 |
+
interactions.append(
|
| 176 |
+
DrugInteraction(
|
| 177 |
+
drug_a=drug_a,
|
| 178 |
+
drug_b=drug_b,
|
| 179 |
+
severity=Severity.MODERATE,
|
| 180 |
+
description=f"{total} adverse event reports found involving both {drug_a} and {drug_b}.",
|
| 181 |
+
clinical_significance="Review recommended based on adverse event frequency",
|
| 182 |
+
source="OpenFDA",
|
| 183 |
+
)
|
| 184 |
+
)
|
| 185 |
+
except Exception:
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
return interactions
|
| 189 |
+
|
| 190 |
+
@staticmethod
|
| 191 |
+
def _map_severity(severity_str: str) -> Severity:
|
| 192 |
+
"""Map RxNorm severity strings to our Severity enum."""
|
| 193 |
+
s = severity_str.lower()
|
| 194 |
+
if "high" in s or "severe" in s or "serious" in s:
|
| 195 |
+
return Severity.HIGH
|
| 196 |
+
elif "moderate" in s:
|
| 197 |
+
return Severity.MODERATE
|
| 198 |
+
elif "low" in s or "minor" in s:
|
| 199 |
+
return Severity.LOW
|
| 200 |
+
return Severity.MODERATE # Default
|
| 201 |
+
|
| 202 |
+
@staticmethod
|
| 203 |
+
def _deduplicate(interactions: List[DrugInteraction]) -> List[DrugInteraction]:
|
| 204 |
+
"""Remove duplicate interactions (same drug pair from different sources)."""
|
| 205 |
+
seen = set()
|
| 206 |
+
unique = []
|
| 207 |
+
for interaction in interactions:
|
| 208 |
+
key = tuple(sorted([interaction.drug_a.lower(), interaction.drug_b.lower()]))
|
| 209 |
+
if key not in seen:
|
| 210 |
+
seen.add(key)
|
| 211 |
+
unique.append(interaction)
|
| 212 |
+
return unique
|
src/backend/app/tools/guideline_retrieval.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool: Guideline Retrieval (RAG)
|
| 3 |
+
|
| 4 |
+
Retrieves relevant clinical guideline excerpts using retrieval-augmented generation.
|
| 5 |
+
Uses ChromaDB for vector storage and sentence-transformers for embeddings.
|
| 6 |
+
|
| 7 |
+
This demonstrates RAG as a tool within the agent pipeline — the orchestrator
|
| 8 |
+
invokes it when clinical guidelines are needed for the current diagnosis.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import logging
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import List, Optional
|
| 16 |
+
|
| 17 |
+
from app.config import settings
|
| 18 |
+
from app.models.schemas import GuidelineExcerpt, GuidelineRetrievalResult
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
# Path to the comprehensive clinical guidelines corpus
|
| 23 |
+
GUIDELINES_DATA_PATH = Path(__file__).parent.parent / "data" / "clinical_guidelines.json"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class GuidelineRetrievalTool:
|
| 27 |
+
"""RAG-based clinical guideline retrieval."""
|
| 28 |
+
|
| 29 |
+
def __init__(self):
|
| 30 |
+
self._collection = None
|
| 31 |
+
self._embedding_fn = None
|
| 32 |
+
|
| 33 |
+
async def _ensure_initialized(self):
|
| 34 |
+
"""Lazy-initialize ChromaDB and embeddings."""
|
| 35 |
+
if self._collection is not None:
|
| 36 |
+
return
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
import chromadb
|
| 40 |
+
from chromadb.utils import embedding_functions
|
| 41 |
+
|
| 42 |
+
self._embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
|
| 43 |
+
model_name=settings.embedding_model,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
client = chromadb.PersistentClient(path=settings.chroma_persist_dir)
|
| 47 |
+
|
| 48 |
+
self._collection = client.get_or_create_collection(
|
| 49 |
+
name="clinical_guidelines",
|
| 50 |
+
embedding_function=self._embedding_fn,
|
| 51 |
+
metadata={"hnsw:space": "cosine"},
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
# If collection is empty, load seed guidelines
|
| 55 |
+
if self._collection.count() == 0:
|
| 56 |
+
await self._load_seed_guidelines()
|
| 57 |
+
|
| 58 |
+
except ImportError:
|
| 59 |
+
logger.error("chromadb or sentence-transformers not installed")
|
| 60 |
+
raise
|
| 61 |
+
|
| 62 |
+
async def run(self, query: str, n_results: int = 5) -> GuidelineRetrievalResult:
|
| 63 |
+
"""
|
| 64 |
+
Retrieve relevant clinical guideline excerpts for a query.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
query: Clinical query (e.g., "type 2 diabetes management guidelines")
|
| 68 |
+
n_results: Number of excerpts to retrieve
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
GuidelineRetrievalResult with relevant excerpts
|
| 72 |
+
"""
|
| 73 |
+
await self._ensure_initialized()
|
| 74 |
+
|
| 75 |
+
results = self._collection.query(
|
| 76 |
+
query_texts=[query],
|
| 77 |
+
n_results=min(n_results, self._collection.count() or 1),
|
| 78 |
+
include=["documents", "metadatas", "distances"],
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
excerpts = []
|
| 82 |
+
if results and results["documents"] and results["documents"][0]:
|
| 83 |
+
for doc, meta, distance in zip(
|
| 84 |
+
results["documents"][0],
|
| 85 |
+
results["metadatas"][0],
|
| 86 |
+
results["distances"][0],
|
| 87 |
+
):
|
| 88 |
+
excerpts.append(
|
| 89 |
+
GuidelineExcerpt(
|
| 90 |
+
title=meta.get("title", "Clinical Guideline"),
|
| 91 |
+
excerpt=doc,
|
| 92 |
+
source=meta.get("source", "Unknown"),
|
| 93 |
+
url=meta.get("url"),
|
| 94 |
+
relevance_score=round(1 - distance, 4), # Convert distance to similarity
|
| 95 |
+
)
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
return GuidelineRetrievalResult(query=query, excerpts=excerpts)
|
| 99 |
+
|
| 100 |
+
async def _load_seed_guidelines(self):
|
| 101 |
+
"""
|
| 102 |
+
Load seed clinical guidelines into the vector store.
|
| 103 |
+
|
| 104 |
+
Loads from the comprehensive JSON corpus covering 14+ medical specialties.
|
| 105 |
+
Each guideline is stored with metadata for filtering and attribution.
|
| 106 |
+
"""
|
| 107 |
+
seed_guidelines = self._get_seed_guidelines()
|
| 108 |
+
|
| 109 |
+
if not seed_guidelines:
|
| 110 |
+
logger.warning("No seed guidelines to load")
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
documents = [g["text"] for g in seed_guidelines]
|
| 114 |
+
metadatas = [
|
| 115 |
+
{
|
| 116 |
+
"title": g["title"],
|
| 117 |
+
"source": g["source"],
|
| 118 |
+
"url": g.get("url", ""),
|
| 119 |
+
"specialty": g.get("specialty", "General"),
|
| 120 |
+
"guideline_id": g.get("id", f"guideline_{i}"),
|
| 121 |
+
}
|
| 122 |
+
for i, g in enumerate(seed_guidelines)
|
| 123 |
+
]
|
| 124 |
+
ids = [g.get("id", f"guideline_{i}") for i, g in enumerate(seed_guidelines)]
|
| 125 |
+
|
| 126 |
+
self._collection.add(documents=documents, metadatas=metadatas, ids=ids)
|
| 127 |
+
logger.info(f"Loaded {len(seed_guidelines)} seed guidelines into vector store")
|
| 128 |
+
|
| 129 |
+
@staticmethod
|
| 130 |
+
def _get_seed_guidelines() -> List[dict]:
|
| 131 |
+
"""
|
| 132 |
+
Load clinical guidelines from the comprehensive JSON corpus.
|
| 133 |
+
|
| 134 |
+
The guidelines cover 14+ specialties including cardiology, emergency medicine,
|
| 135 |
+
endocrinology, pulmonology, neurology, gastroenterology, infectious disease,
|
| 136 |
+
psychiatry, pediatrics, nephrology, hematology, rheumatology, OB/GYN,
|
| 137 |
+
dermatology, preventive medicine, and perioperative medicine.
|
| 138 |
+
"""
|
| 139 |
+
if GUIDELINES_DATA_PATH.exists():
|
| 140 |
+
try:
|
| 141 |
+
with open(GUIDELINES_DATA_PATH, "r", encoding="utf-8") as f:
|
| 142 |
+
guidelines = json.load(f)
|
| 143 |
+
logger.info(
|
| 144 |
+
f"Loaded {len(guidelines)} guidelines from {GUIDELINES_DATA_PATH.name} "
|
| 145 |
+
f"covering specialties: {', '.join(sorted(set(g.get('specialty', 'Unknown') for g in guidelines)))}"
|
| 146 |
+
)
|
| 147 |
+
return guidelines
|
| 148 |
+
except (json.JSONDecodeError, OSError) as e:
|
| 149 |
+
logger.error(f"Failed to load guidelines from {GUIDELINES_DATA_PATH}: {e}")
|
| 150 |
+
|
| 151 |
+
# Fallback: minimal seed guidelines if JSON file not found
|
| 152 |
+
logger.warning("Guidelines JSON not found, using minimal fallback set")
|
| 153 |
+
return [
|
| 154 |
+
{
|
| 155 |
+
"title": "ACC/AHA Chest Pain Guidelines (2021)",
|
| 156 |
+
"source": "American College of Cardiology / American Heart Association",
|
| 157 |
+
"url": "https://www.jacc.org/doi/10.1016/j.jacc.2021.07.053",
|
| 158 |
+
"text": (
|
| 159 |
+
"Acute chest pain evaluation: Assess pretest probability of ACS. "
|
| 160 |
+
"High-sensitivity troponin is the preferred biomarker. Use HEART score "
|
| 161 |
+
"for risk stratification. Low-risk (HEART 0-3): consider early discharge. "
|
| 162 |
+
"High-risk (HEART 7-10): invasive strategy with cardiology consultation. "
|
| 163 |
+
"For STEMI, activate cath lab with door-to-balloon time <90 minutes."
|
| 164 |
+
),
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"title": "Surviving Sepsis Campaign (2021)",
|
| 168 |
+
"source": "SCCM / ESICM",
|
| 169 |
+
"url": "https://www.sccm.org/SurvivingSepsisCampaign/Guidelines",
|
| 170 |
+
"text": (
|
| 171 |
+
"Sepsis hour-1 bundle: measure lactate, obtain blood cultures before "
|
| 172 |
+
"antibiotics, administer broad-spectrum antibiotics within 1 hour, "
|
| 173 |
+
"begin 30 mL/kg crystalloid for hypotension or lactate ≥4. "
|
| 174 |
+
"Norepinephrine first-line vasopressor. Target MAP ≥65."
|
| 175 |
+
),
|
| 176 |
+
},
|
| 177 |
+
]
|
| 178 |
+
|
| 179 |
+
async def add_guidelines(self, guidelines: List[dict]):
|
| 180 |
+
"""
|
| 181 |
+
Add custom guidelines to the vector store.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
guidelines: List of dicts with 'title', 'source', 'text', optional 'url'
|
| 185 |
+
"""
|
| 186 |
+
await self._ensure_initialized()
|
| 187 |
+
|
| 188 |
+
existing_count = self._collection.count()
|
| 189 |
+
documents = [g["text"] for g in guidelines]
|
| 190 |
+
metadatas = [
|
| 191 |
+
{"title": g["title"], "source": g["source"], "url": g.get("url", "")}
|
| 192 |
+
for g in guidelines
|
| 193 |
+
]
|
| 194 |
+
ids = [f"guideline_{existing_count + i}" for i in range(len(guidelines))]
|
| 195 |
+
|
| 196 |
+
self._collection.add(documents=documents, metadatas=metadatas, ids=ids)
|
| 197 |
+
logger.info(f"Added {len(guidelines)} guidelines to vector store")
|
src/backend/app/tools/patient_parser.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool: Patient Data Parser
|
| 3 |
+
|
| 4 |
+
Parses raw free-text patient case descriptions into a structured PatientProfile.
|
| 5 |
+
Uses MedGemma for intelligent extraction.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
from app.models.schemas import PatientProfile
|
| 12 |
+
from app.services.medgemma import MedGemmaService
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
SYSTEM_PROMPT = """You are a clinical data extraction assistant. Your job is to parse
|
| 17 |
+
free-text patient case descriptions into structured data. Extract all available clinical
|
| 18 |
+
information accurately. If a field is not mentioned, leave it empty or use defaults.
|
| 19 |
+
Never fabricate information that isn't present in the input."""
|
| 20 |
+
|
| 21 |
+
EXTRACTION_PROMPT = """Parse the following patient case into structured clinical data.
|
| 22 |
+
|
| 23 |
+
Patient Case:
|
| 24 |
+
{patient_text}
|
| 25 |
+
|
| 26 |
+
Extract:
|
| 27 |
+
- Age, gender
|
| 28 |
+
- Chief complaint (primary reason for visit)
|
| 29 |
+
- History of present illness (HPI)
|
| 30 |
+
- Past medical history (list of conditions)
|
| 31 |
+
- Current medications (name and dose)
|
| 32 |
+
- Allergies
|
| 33 |
+
- Lab results (test name, value, reference range, abnormal flag)
|
| 34 |
+
- Vital signs
|
| 35 |
+
- Social history
|
| 36 |
+
- Family history
|
| 37 |
+
- Any additional relevant notes"""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class PatientParserTool:
|
| 41 |
+
"""Parses raw patient text into a structured PatientProfile."""
|
| 42 |
+
|
| 43 |
+
def __init__(self):
|
| 44 |
+
self.medgemma = MedGemmaService()
|
| 45 |
+
|
| 46 |
+
async def run(self, patient_text: str) -> PatientProfile:
|
| 47 |
+
"""
|
| 48 |
+
Parse free-text patient description into structured profile.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
patient_text: Raw patient case description
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
Structured PatientProfile
|
| 55 |
+
"""
|
| 56 |
+
prompt = EXTRACTION_PROMPT.format(patient_text=patient_text)
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
profile = await self.medgemma.generate_structured(
|
| 60 |
+
prompt=prompt,
|
| 61 |
+
response_model=PatientProfile,
|
| 62 |
+
system_prompt=SYSTEM_PROMPT,
|
| 63 |
+
temperature=0.1, # Low temp for factual extraction
|
| 64 |
+
)
|
| 65 |
+
logger.info(f"Parsed patient profile: {profile.chief_complaint}")
|
| 66 |
+
return profile
|
| 67 |
+
|
| 68 |
+
except ValueError:
|
| 69 |
+
# Fallback: If structured parsing fails, do basic extraction
|
| 70 |
+
logger.warning("Structured parsing failed, attempting basic extraction")
|
| 71 |
+
return PatientProfile(
|
| 72 |
+
chief_complaint=patient_text[:200],
|
| 73 |
+
history_of_present_illness=patient_text,
|
| 74 |
+
additional_notes="Auto-extracted from raw text (structured parsing failed)",
|
| 75 |
+
)
|
src/backend/app/tools/synthesis.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tool: Synthesis Agent
|
| 3 |
+
|
| 4 |
+
Uses MedGemma to synthesize all tool outputs into a final Clinical Decision Support report.
|
| 5 |
+
This is the capstone of the pipeline — it takes structured data from every tool
|
| 6 |
+
and produces a cohesive, clinician-ready report.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import logging
|
| 11 |
+
from typing import Optional
|
| 12 |
+
|
| 13 |
+
from app.models.schemas import (
|
| 14 |
+
CDSReport,
|
| 15 |
+
ClinicalReasoningResult,
|
| 16 |
+
DrugInteractionResult,
|
| 17 |
+
GuidelineRetrievalResult,
|
| 18 |
+
PatientProfile,
|
| 19 |
+
)
|
| 20 |
+
from app.services.medgemma import MedGemmaService
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
SYSTEM_PROMPT = """You are a clinical decision support synthesis engine. Your job is to
|
| 25 |
+
combine outputs from multiple clinical tools into a single, cohesive report for a clinician.
|
| 26 |
+
|
| 27 |
+
CRITICAL RULES:
|
| 28 |
+
1. Be concise and clinically precise
|
| 29 |
+
2. Prioritize safety — drug interactions and critical findings go first
|
| 30 |
+
3. Clearly distinguish between tool-verified facts and model-generated reasoning
|
| 31 |
+
4. Always include caveats and limitations
|
| 32 |
+
5. Cite sources when available
|
| 33 |
+
6. This report SUPPORTS clinical decision-making — it does NOT replace clinician judgment
|
| 34 |
+
7. Include a standard disclaimer about AI-generated content"""
|
| 35 |
+
|
| 36 |
+
SYNTHESIS_PROMPT = """Synthesize the following clinical tool outputs into a cohesive
|
| 37 |
+
Clinical Decision Support report.
|
| 38 |
+
|
| 39 |
+
═══ PATIENT PROFILE ═══
|
| 40 |
+
{patient_profile}
|
| 41 |
+
|
| 42 |
+
═══ CLINICAL REASONING (MedGemma) ═══
|
| 43 |
+
{clinical_reasoning}
|
| 44 |
+
|
| 45 |
+
═══ DRUG INTERACTION CHECK ═══
|
| 46 |
+
{drug_interactions}
|
| 47 |
+
|
| 48 |
+
═══ CLINICAL GUIDELINES ═══
|
| 49 |
+
{guidelines}
|
| 50 |
+
|
| 51 |
+
Create a comprehensive CDS report including:
|
| 52 |
+
1. Patient Summary — concise summary of the case
|
| 53 |
+
2. Differential Diagnosis — ranked with reasoning, integrating guideline concordance
|
| 54 |
+
3. Drug Interaction Warnings — any flagged interactions with clinical significance
|
| 55 |
+
4. Guideline-Concordant Recommendations — actionable steps aligned with guidelines
|
| 56 |
+
5. Suggested Next Steps — prioritized actions for the clinician
|
| 57 |
+
6. Caveats — limitations, uncertainties, and important disclaimers
|
| 58 |
+
7. Sources — cited guidelines and data sources used"""
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class SynthesisTool:
|
| 62 |
+
"""Synthesizes all tool outputs into a final CDS report using MedGemma."""
|
| 63 |
+
|
| 64 |
+
def __init__(self):
|
| 65 |
+
self.medgemma = MedGemmaService()
|
| 66 |
+
|
| 67 |
+
async def run(
|
| 68 |
+
self,
|
| 69 |
+
patient_profile: Optional[PatientProfile],
|
| 70 |
+
clinical_reasoning: Optional[ClinicalReasoningResult],
|
| 71 |
+
drug_interactions: Optional[DrugInteractionResult],
|
| 72 |
+
guideline_retrieval: Optional[GuidelineRetrievalResult],
|
| 73 |
+
) -> CDSReport:
|
| 74 |
+
"""
|
| 75 |
+
Synthesize all available tool outputs into a final CDS report.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
patient_profile: Structured patient data
|
| 79 |
+
clinical_reasoning: Differential diagnosis and recommendations
|
| 80 |
+
drug_interactions: Drug interaction check results
|
| 81 |
+
guideline_retrieval: Retrieved clinical guideline excerpts
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
CDSReport — the final clinician-facing report
|
| 85 |
+
"""
|
| 86 |
+
prompt = SYNTHESIS_PROMPT.format(
|
| 87 |
+
patient_profile=self._format_profile(patient_profile),
|
| 88 |
+
clinical_reasoning=self._format_reasoning(clinical_reasoning),
|
| 89 |
+
drug_interactions=self._format_interactions(drug_interactions),
|
| 90 |
+
guidelines=self._format_guidelines(guideline_retrieval),
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
report = await self.medgemma.generate_structured(
|
| 94 |
+
prompt=prompt,
|
| 95 |
+
response_model=CDSReport,
|
| 96 |
+
system_prompt=SYSTEM_PROMPT,
|
| 97 |
+
temperature=0.2,
|
| 98 |
+
max_tokens=4096,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Add standard disclaimer to caveats
|
| 102 |
+
report.caveats.append(
|
| 103 |
+
"This report is AI-generated and intended for clinical decision SUPPORT only. "
|
| 104 |
+
"It does not replace professional medical judgment. All recommendations should "
|
| 105 |
+
"be verified by a qualified clinician before acting on them."
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
logger.info("Synthesis complete — CDS report generated")
|
| 109 |
+
return report
|
| 110 |
+
|
| 111 |
+
@staticmethod
|
| 112 |
+
def _format_profile(profile: Optional[PatientProfile]) -> str:
|
| 113 |
+
if not profile:
|
| 114 |
+
return "Patient profile not available"
|
| 115 |
+
parts = [
|
| 116 |
+
f"Age: {profile.age or 'Unknown'}, Gender: {profile.gender.value}",
|
| 117 |
+
f"Chief Complaint: {profile.chief_complaint}",
|
| 118 |
+
f"HPI: {profile.history_of_present_illness}",
|
| 119 |
+
]
|
| 120 |
+
if profile.past_medical_history:
|
| 121 |
+
parts.append(f"PMH: {', '.join(profile.past_medical_history)}")
|
| 122 |
+
if profile.current_medications:
|
| 123 |
+
meds = "; ".join(f"{m.name} {m.dose or ''}" for m in profile.current_medications)
|
| 124 |
+
parts.append(f"Medications: {meds}")
|
| 125 |
+
if profile.allergies:
|
| 126 |
+
parts.append(f"Allergies: {', '.join(profile.allergies)}")
|
| 127 |
+
if profile.lab_results:
|
| 128 |
+
labs = "; ".join(
|
| 129 |
+
f"{l.test_name}: {l.value}{' [ABNORMAL]' if l.is_abnormal else ''}"
|
| 130 |
+
for l in profile.lab_results
|
| 131 |
+
)
|
| 132 |
+
parts.append(f"Labs: {labs}")
|
| 133 |
+
return "\n".join(parts)
|
| 134 |
+
|
| 135 |
+
@staticmethod
|
| 136 |
+
def _format_reasoning(reasoning: Optional[ClinicalReasoningResult]) -> str:
|
| 137 |
+
if not reasoning:
|
| 138 |
+
return "Clinical reasoning not available"
|
| 139 |
+
parts = []
|
| 140 |
+
if reasoning.differential_diagnosis:
|
| 141 |
+
parts.append("Differential Diagnosis:")
|
| 142 |
+
for i, dx in enumerate(reasoning.differential_diagnosis, 1):
|
| 143 |
+
parts.append(
|
| 144 |
+
f" {i}. {dx.diagnosis} (likelihood: {dx.likelihood.value}) — {dx.reasoning}"
|
| 145 |
+
)
|
| 146 |
+
if reasoning.risk_assessment:
|
| 147 |
+
parts.append(f"Risk Assessment: {reasoning.risk_assessment}")
|
| 148 |
+
if reasoning.recommended_workup:
|
| 149 |
+
parts.append("Recommended Workup:")
|
| 150 |
+
for action in reasoning.recommended_workup:
|
| 151 |
+
parts.append(
|
| 152 |
+
f" - [{action.priority.value.upper()}] {action.action} — {action.rationale}"
|
| 153 |
+
)
|
| 154 |
+
return "\n".join(parts)
|
| 155 |
+
|
| 156 |
+
@staticmethod
|
| 157 |
+
def _format_interactions(interactions: Optional[DrugInteractionResult]) -> str:
|
| 158 |
+
if not interactions:
|
| 159 |
+
return "Drug interaction check not performed"
|
| 160 |
+
if not interactions.interactions_found:
|
| 161 |
+
return f"No interactions found among {len(interactions.medications_checked)} medications checked"
|
| 162 |
+
parts = [f"Checked {len(interactions.medications_checked)} medications:"]
|
| 163 |
+
for ix in interactions.interactions_found:
|
| 164 |
+
parts.append(
|
| 165 |
+
f" ⚠ {ix.drug_a} + {ix.drug_b} [{ix.severity.value.upper()}]: {ix.description}"
|
| 166 |
+
)
|
| 167 |
+
if interactions.warnings:
|
| 168 |
+
parts.append("Warnings: " + "; ".join(interactions.warnings))
|
| 169 |
+
return "\n".join(parts)
|
| 170 |
+
|
| 171 |
+
@staticmethod
|
| 172 |
+
def _format_guidelines(guidelines: Optional[GuidelineRetrievalResult]) -> str:
|
| 173 |
+
if not guidelines or not guidelines.excerpts:
|
| 174 |
+
return "No guidelines retrieved"
|
| 175 |
+
parts = [f"Query: {guidelines.query}", "Retrieved excerpts:"]
|
| 176 |
+
for excerpt in guidelines.excerpts:
|
| 177 |
+
score = f" (relevance: {excerpt.relevance_score})" if excerpt.relevance_score else ""
|
| 178 |
+
parts.append(f" [{excerpt.source}] {excerpt.title}{score}")
|
| 179 |
+
parts.append(f" {excerpt.excerpt[:300]}...")
|
| 180 |
+
return "\n".join(parts)
|
src/backend/requirements.txt
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MedGemma CDS Agent - Backend Dependencies
|
| 2 |
+
# Python 3.10+
|
| 3 |
+
|
| 4 |
+
# Web Framework
|
| 5 |
+
fastapi==0.115.0
|
| 6 |
+
uvicorn[standard]==0.30.6
|
| 7 |
+
websockets==12.0
|
| 8 |
+
|
| 9 |
+
# Configuration
|
| 10 |
+
pydantic-settings==2.5.2
|
| 11 |
+
python-dotenv==1.0.1
|
| 12 |
+
|
| 13 |
+
# MedGemma / LLM
|
| 14 |
+
openai==1.51.0
|
| 15 |
+
transformers==4.45.0
|
| 16 |
+
torch==2.4.1
|
| 17 |
+
accelerate==0.34.2
|
| 18 |
+
|
| 19 |
+
# HTTP Client
|
| 20 |
+
httpx==0.27.2
|
| 21 |
+
|
| 22 |
+
# RAG / Vector Store
|
| 23 |
+
chromadb==0.5.7
|
| 24 |
+
sentence-transformers==3.1.1
|
| 25 |
+
|
| 26 |
+
# Utilities
|
| 27 |
+
python-multipart==0.0.10
|
src/backend/test_clinical_cases.py
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Comprehensive Clinical Test Suite for the CDS Agent.
|
| 3 |
+
|
| 4 |
+
Tests diverse clinical scenarios across specialties, acuity levels, demographics,
|
| 5 |
+
and edge cases. Each test case is a realistic patient presentation designed to
|
| 6 |
+
exercise different parts of the pipeline.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python test_clinical_cases.py # Run all cases sequentially
|
| 10 |
+
python test_clinical_cases.py --case cardio_acs # Run a single case by ID
|
| 11 |
+
python test_clinical_cases.py --specialty cardio # Run all cases in a specialty
|
| 12 |
+
python test_clinical_cases.py --list # List available cases
|
| 13 |
+
"""
|
| 14 |
+
import httpx
|
| 15 |
+
import asyncio
|
| 16 |
+
import json
|
| 17 |
+
import time
|
| 18 |
+
import sys
|
| 19 |
+
import argparse
|
| 20 |
+
|
| 21 |
+
API = "http://localhost:8002"
|
| 22 |
+
|
| 23 |
+
# ─────────────────────────────────────────────────
|
| 24 |
+
# Test Case Definitions
|
| 25 |
+
# ─────────────────────────────────────────────────
|
| 26 |
+
|
| 27 |
+
TEST_CASES = [
|
| 28 |
+
# ── Cardiology ──
|
| 29 |
+
{
|
| 30 |
+
"id": "cardio_acs",
|
| 31 |
+
"specialty": "Cardiology",
|
| 32 |
+
"title": "Acute Coronary Syndrome — Classic STEMI",
|
| 33 |
+
"expected_keywords": ["ACS", "STEMI", "troponin", "cath lab", "PCI", "aspirin", "heparin"],
|
| 34 |
+
"patient_text": (
|
| 35 |
+
"62-year-old male presenting to ED with crushing substernal chest pain radiating to "
|
| 36 |
+
"left arm and jaw for 45 minutes. Diaphoretic and nauseated. History: HTN, "
|
| 37 |
+
"hyperlipidemia, 30 pack-year smoking history (quit 5 years ago), father had MI at 55. "
|
| 38 |
+
"Medications: atorvastatin 40mg daily, lisinopril 10mg daily, ASA 81mg daily. "
|
| 39 |
+
"Vitals: BP 155/98, HR 105, RR 22, SpO2 94% on RA, Temp 37.2°C. "
|
| 40 |
+
"ECG: ST elevation in leads II, III, aVF with reciprocal changes in I, aVL. "
|
| 41 |
+
"Initial troponin I: 2.8 ng/mL (normal <0.04)."
|
| 42 |
+
),
|
| 43 |
+
"include_drug_check": True,
|
| 44 |
+
"include_guidelines": True,
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "cardio_afib",
|
| 48 |
+
"specialty": "Cardiology",
|
| 49 |
+
"title": "New-Onset Atrial Fibrillation with Rapid Ventricular Response",
|
| 50 |
+
"expected_keywords": ["atrial fibrillation", "rate control", "CHA2DS2-VASc", "anticoagulation", "DOAC"],
|
| 51 |
+
"patient_text": (
|
| 52 |
+
"73-year-old female with 2-day history of palpitations and lightheadedness. "
|
| 53 |
+
"She also reports mild exertional dyspnea. No syncope. PMH: HTN, type 2 DM, "
|
| 54 |
+
"osteoarthritis, hypothyroidism. Medications: metformin 500mg BID, levothyroxine "
|
| 55 |
+
"100mcg daily, amlodipine 5mg daily, ibuprofen 400mg PRN (takes daily for knee pain). "
|
| 56 |
+
"Vitals: BP 138/82, HR 142 (irregular), RR 18, SpO2 96%. "
|
| 57 |
+
"ECG: atrial fibrillation with rapid ventricular response, no ST changes. "
|
| 58 |
+
"Labs: TSH 0.8, K+ 4.2, Cr 1.1, BNP 350."
|
| 59 |
+
),
|
| 60 |
+
"include_drug_check": True,
|
| 61 |
+
"include_guidelines": True,
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"id": "cardio_hf",
|
| 65 |
+
"specialty": "Cardiology",
|
| 66 |
+
"title": "Acute Decompensated Heart Failure",
|
| 67 |
+
"expected_keywords": ["heart failure", "HFrEF", "diuretic", "volume overload", "BNP", "ejection fraction"],
|
| 68 |
+
"patient_text": (
|
| 69 |
+
"58-year-old male presents with progressive dyspnea over 1 week, now orthopneic "
|
| 70 |
+
"with 3-pillow requirement. Reports 10-lb weight gain. PMH: ischemic cardiomyopathy "
|
| 71 |
+
"(EF 25% 6 months ago), HTN, CKD stage 3b (baseline Cr 2.1). Medications: carvedilol "
|
| 72 |
+
"25mg BID, sacubitril-valsartan 97/103mg BID, spironolactone 25mg daily, furosemide "
|
| 73 |
+
"40mg BID, dapagliflozin 10mg daily. Vitals: BP 98/62, HR 98, RR 26, SpO2 89% on RA. "
|
| 74 |
+
"Exam: JVD to earlobes, bilateral crackles to mid-lung, S3, 3+ bilateral LE edema. "
|
| 75 |
+
"Labs: BNP 2,800, Cr 2.8 (from 2.1), K+ 5.4, Na+ 128, troponin I 0.08."
|
| 76 |
+
),
|
| 77 |
+
"include_drug_check": True,
|
| 78 |
+
"include_guidelines": True,
|
| 79 |
+
},
|
| 80 |
+
# ── Emergency Medicine ──
|
| 81 |
+
{
|
| 82 |
+
"id": "em_stroke",
|
| 83 |
+
"specialty": "Emergency Medicine",
|
| 84 |
+
"title": "Acute Ischemic Stroke — tPA Window",
|
| 85 |
+
"expected_keywords": ["stroke", "tPA", "alteplase", "NIHSS", "CT", "thrombolysis"],
|
| 86 |
+
"patient_text": (
|
| 87 |
+
"68-year-old female found by husband at 7:15 AM with right-sided weakness and difficulty "
|
| 88 |
+
"speaking. Last known well at 11 PM the night before (went to bed normal). PMH: "
|
| 89 |
+
"atrial fibrillation (not on anticoagulation — patient declined), HTN, "
|
| 90 |
+
"hyperlipidemia. Medications: metoprolol 50mg BID, atorvastatin 20mg daily. "
|
| 91 |
+
"Vitals: BP 182/105, HR 88 (irregular), RR 16, SpO2 97%. "
|
| 92 |
+
"Exam: NIHSS 14 — right hemiparesis (arm 4/5, leg 3/5), expressive aphasia, "
|
| 93 |
+
"right facial droop, neglect. CT head: no hemorrhage. CTA: left M1 occlusion."
|
| 94 |
+
),
|
| 95 |
+
"include_drug_check": True,
|
| 96 |
+
"include_guidelines": True,
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "em_sepsis",
|
| 100 |
+
"specialty": "Emergency Medicine",
|
| 101 |
+
"title": "Sepsis from Urinary Source",
|
| 102 |
+
"expected_keywords": ["sepsis", "antibiotics", "lactate", "fluid resuscitation", "vasopressor", "cultures"],
|
| 103 |
+
"patient_text": (
|
| 104 |
+
"82-year-old female nursing home resident brought to ED with confusion, fever, and "
|
| 105 |
+
"decreased oral intake for 2 days. PMH: Alzheimer dementia, type 2 DM, recurrent "
|
| 106 |
+
"UTIs, HTN, CKD stage 3a. Medications: donepezil 10mg daily, glipizide 5mg BID, "
|
| 107 |
+
"lisinopril 10mg daily, cranberry supplement. "
|
| 108 |
+
"Vitals: BP 85/50, HR 115, RR 26, SpO2 92% on RA, Temp 39.4°C (103°F). "
|
| 109 |
+
"Exam: confused (GCS 13), dry mucous membranes, suprapubic tenderness. "
|
| 110 |
+
"Labs: WBC 18.5K, lactate 4.2, Cr 2.4 (baseline 1.3), glucose 45, "
|
| 111 |
+
"UA positive for nitrites, leukocyte esterase 3+, bacteria many."
|
| 112 |
+
),
|
| 113 |
+
"include_drug_check": True,
|
| 114 |
+
"include_guidelines": True,
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"id": "em_anaphylaxis",
|
| 118 |
+
"specialty": "Emergency Medicine",
|
| 119 |
+
"title": "Anaphylaxis — Peanut Allergy",
|
| 120 |
+
"expected_keywords": ["anaphylaxis", "epinephrine", "airway", "allergic reaction", "antihistamine"],
|
| 121 |
+
"patient_text": (
|
| 122 |
+
"22-year-old male brought by EMS after eating a cookie at a party that contained "
|
| 123 |
+
"peanuts. Known severe peanut allergy. Symptoms started 15 minutes after ingestion: "
|
| 124 |
+
"diffuse urticaria, lip and tongue swelling, throat tightness, wheezing, "
|
| 125 |
+
"lightheadedness. EpiPen administered by friend at scene. PMH: peanut allergy, "
|
| 126 |
+
"asthma (mild intermittent). Medications: albuterol PRN, carries EpiPen. "
|
| 127 |
+
"Vitals: BP 88/52, HR 130, RR 28, SpO2 89%, Temp 37.0°C. "
|
| 128 |
+
"Exam: diffuse urticaria, angioedema of lips and tongue, stridor, bilateral wheezing, "
|
| 129 |
+
"use of accessory muscles."
|
| 130 |
+
),
|
| 131 |
+
"include_drug_check": True,
|
| 132 |
+
"include_guidelines": True,
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"id": "em_trauma",
|
| 136 |
+
"specialty": "Emergency Medicine",
|
| 137 |
+
"title": "Polytrauma — MVC with Hemorrhagic Shock",
|
| 138 |
+
"expected_keywords": ["trauma", "hemorrhage", "transfusion", "FAST", "ABC", "resuscitation"],
|
| 139 |
+
"patient_text": (
|
| 140 |
+
"35-year-old male restrained driver in high-speed MVC, significant front-end damage, "
|
| 141 |
+
"prolonged extrication (30 min). GCS 12 (E3V4M5) in the field. C-collar in place. "
|
| 142 |
+
"PMH: healthy, no medications, NKDA. Social: occasional alcohol (denies today). "
|
| 143 |
+
"Vitals: BP 78/45, HR 135, RR 30, SpO2 88%, Temp 35.8°C. "
|
| 144 |
+
"Primary survey: A — speaking in short sentences. B — decreased breath sounds left, "
|
| 145 |
+
"subcutaneous crepitus left chest wall. C — tachycardic, thready pulses, no external "
|
| 146 |
+
"hemorrhage, abdomen distended and tender. D — GCS 12. E — left leg deformity, "
|
| 147 |
+
"pelvis unstable on compression. "
|
| 148 |
+
"FAST: positive for free fluid in Morrison's pouch and pelvis. "
|
| 149 |
+
"CXR: left hemopneumothorax. Labs pending."
|
| 150 |
+
),
|
| 151 |
+
"include_drug_check": False,
|
| 152 |
+
"include_guidelines": True,
|
| 153 |
+
},
|
| 154 |
+
# ── Endocrinology ──
|
| 155 |
+
{
|
| 156 |
+
"id": "endo_dka",
|
| 157 |
+
"specialty": "Endocrinology",
|
| 158 |
+
"title": "Diabetic Ketoacidosis — New-Onset T1DM",
|
| 159 |
+
"expected_keywords": ["DKA", "insulin", "ketoacidosis", "potassium", "fluid resuscitation", "anion gap"],
|
| 160 |
+
"patient_text": (
|
| 161 |
+
"19-year-old male college student brought by roommate with nausea, vomiting, and "
|
| 162 |
+
"confusion. Reports 3-week history of polyuria, polydipsia, and 15-lb weight loss. "
|
| 163 |
+
"Denies alcohol or drug use. No significant PMH. Family history of type 1 diabetes "
|
| 164 |
+
"(mother). No medications. "
|
| 165 |
+
"Vitals: BP 100/60, HR 120, RR 32 (Kussmaul), SpO2 99%, Temp 37.1°C. "
|
| 166 |
+
"Exam: dry mucous membranes, fruity breath odor, diffusely tender abdomen, "
|
| 167 |
+
"lethargic but arousable (GCS 14). "
|
| 168 |
+
"Labs: glucose 485 mg/dL, pH 7.12, bicarb 8, K+ 5.8, Na+ 131 (corrected 138), "
|
| 169 |
+
"anion gap 28, BUN 32, Cr 1.6, serum ketones strongly positive, "
|
| 170 |
+
"urine ketones 3+, A1C 13.2%."
|
| 171 |
+
),
|
| 172 |
+
"include_drug_check": False,
|
| 173 |
+
"include_guidelines": True,
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"id": "endo_thyroid_storm",
|
| 177 |
+
"specialty": "Endocrinology",
|
| 178 |
+
"title": "Thyroid Storm",
|
| 179 |
+
"expected_keywords": ["thyroid storm", "PTU", "propylthiouracil", "beta-blocker", "hyperthyroidism", "Graves"],
|
| 180 |
+
"patient_text": (
|
| 181 |
+
"42-year-old female presents with high fever, agitation, tachycardia, and vomiting. "
|
| 182 |
+
"She was recently diagnosed with Graves disease 2 months ago but ran out of "
|
| 183 |
+
"methimazole 3 weeks ago and did not refill. She had a tooth extraction yesterday. "
|
| 184 |
+
"PMH: Graves disease, anxiety. Medications: none (ran out of methimazole). "
|
| 185 |
+
"Vitals: BP 160/70, HR 168, RR 28, SpO2 95%, Temp 40.2°C (104.4°F). "
|
| 186 |
+
"Exam: agitated, tremulous, diaphoretic, exophthalmos, diffuse goiter with bruit, "
|
| 187 |
+
"hyperactive bowel sounds, fine tremor, warm and flushed skin. "
|
| 188 |
+
"Labs: TSH <0.01, free T4 7.8 (normal 0.8-1.7), free T3 22 (normal 2-4.4), "
|
| 189 |
+
"WBC 11.2, AST 95, ALT 82, total bilirubin 2.1. "
|
| 190 |
+
"Burch-Wartofsky Point Scale: 65 (highly suggestive of thyroid storm)."
|
| 191 |
+
),
|
| 192 |
+
"include_drug_check": True,
|
| 193 |
+
"include_guidelines": True,
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"id": "endo_adrenal_crisis",
|
| 197 |
+
"specialty": "Endocrinology",
|
| 198 |
+
"title": "Adrenal Crisis",
|
| 199 |
+
"expected_keywords": ["adrenal crisis", "hydrocortisone", "Addison", "hypotension", "cortisol"],
|
| 200 |
+
"patient_text": (
|
| 201 |
+
"38-year-old female with known primary adrenal insufficiency (Addison disease) presents "
|
| 202 |
+
"with 2-day history of GI illness (vomiting and diarrhea) and progressive weakness. "
|
| 203 |
+
"She continued her usual hydrocortisone dose but could not keep medication down due to "
|
| 204 |
+
"vomiting. Found by partner lying on bathroom floor, minimally responsive. "
|
| 205 |
+
"PMH: Addison disease, hypothyroidism. Medications: hydrocortisone 15mg AM/5mg PM, "
|
| 206 |
+
"fludrocortisone 0.1mg daily, levothyroxine 75mcg. "
|
| 207 |
+
"Vitals: BP 72/40 (despite 1L NS in the field), HR 128, RR 22, SpO2 96%, Temp 38.5°C. "
|
| 208 |
+
"Labs: Na+ 122, K+ 6.3, glucose 52, Cr 1.8, random cortisol 1.2 mcg/dL. "
|
| 209 |
+
"Exam: obtunded (GCS 10), no focal neuro deficits, hyperpigmented skin creases."
|
| 210 |
+
),
|
| 211 |
+
"include_drug_check": True,
|
| 212 |
+
"include_guidelines": True,
|
| 213 |
+
},
|
| 214 |
+
# ── Pulmonology ──
|
| 215 |
+
{
|
| 216 |
+
"id": "pulm_pe",
|
| 217 |
+
"specialty": "Pulmonology",
|
| 218 |
+
"title": "Massive Pulmonary Embolism",
|
| 219 |
+
"expected_keywords": ["pulmonary embolism", "anticoagulation", "thrombolysis", "D-dimer", "CTPA", "tPA"],
|
| 220 |
+
"patient_text": (
|
| 221 |
+
"45-year-old female presents with sudden-onset severe dyspnea and near-syncope while "
|
| 222 |
+
"at work. She had right calf pain for 3 days. PMH: obesity (BMI 38), oral "
|
| 223 |
+
"contraceptive use, recent 8-hour flight 1 week ago. Medications: combined OCP. "
|
| 224 |
+
"Vitals: BP 82/50, HR 130, RR 32, SpO2 84% on high-flow O2, Temp 37.3°C. "
|
| 225 |
+
"Exam: distressed, diaphoretic, JVD, loud P2, right calf swelling and tenderness. "
|
| 226 |
+
"ECG: sinus tachycardia, S1Q3T3 pattern, right axis deviation. "
|
| 227 |
+
"Labs: troponin I 1.2 (elevated), BNP 890, D-dimer >5000. "
|
| 228 |
+
"Bedside echo: RV dilation with septal bowing, McConnell's sign. "
|
| 229 |
+
"CTPA: bilateral extensive PE with saddle embolus at main PA bifurcation."
|
| 230 |
+
),
|
| 231 |
+
"include_drug_check": True,
|
| 232 |
+
"include_guidelines": True,
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"id": "pulm_asthma_exacerbation",
|
| 236 |
+
"specialty": "Pulmonology",
|
| 237 |
+
"title": "Severe Asthma Exacerbation (Status Asthmaticus)",
|
| 238 |
+
"expected_keywords": ["asthma", "bronchodilator", "albuterol", "corticosteroid", "magnesium", "intubation"],
|
| 239 |
+
"patient_text": (
|
| 240 |
+
"28-year-old male with history of poorly controlled asthma presents to ED with severe "
|
| 241 |
+
"dyspnea that started 6 hours ago. He ran out of his controller inhaler (fluticasone) "
|
| 242 |
+
"2 weeks ago and has been using albuterol 6-8 times daily. Reports URI symptoms for "
|
| 243 |
+
"3 days. PMH: asthma (2 ICU admissions, 1 intubation last year), allergic rhinitis, "
|
| 244 |
+
"GERD. Medications: albuterol MDI PRN (using heavily), montelukast 10mg daily. "
|
| 245 |
+
"Vitals: BP 140/88, HR 125, RR 36, SpO2 87% on NRB, Temp 37.4°C. "
|
| 246 |
+
"Exam: tripod position, accessory muscle use, speaking in 1-2 word sentences, "
|
| 247 |
+
"bilateral inspiratory and expiratory wheezes with decreased air entry at bases, "
|
| 248 |
+
"pulsus paradoxus 22 mmHg. PEFR: unable to perform. "
|
| 249 |
+
"ABG: pH 7.28, pCO2 52, pO2 58. "
|
| 250 |
+
"Given 3 rounds of continuous albuterol/ipratropium, IV methylprednisolone — minimal improvement."
|
| 251 |
+
),
|
| 252 |
+
"include_drug_check": True,
|
| 253 |
+
"include_guidelines": True,
|
| 254 |
+
},
|
| 255 |
+
# ── Gastroenterology ──
|
| 256 |
+
{
|
| 257 |
+
"id": "gi_ugib",
|
| 258 |
+
"specialty": "Gastroenterology",
|
| 259 |
+
"title": "Upper GI Bleeding — Peptic Ulcer",
|
| 260 |
+
"expected_keywords": ["GI bleeding", "hematemesis", "PPI", "endoscopy", "transfusion", "H. pylori"],
|
| 261 |
+
"patient_text": (
|
| 262 |
+
"65-year-old male presents with 3 episodes of hematemesis (bright red blood) over "
|
| 263 |
+
"the past 4 hours. Also reports melena for 2 days. Mild epigastric pain. "
|
| 264 |
+
"PMH: osteoarthritis, atrial fibrillation on warfarin. Medications: warfarin 5mg daily, "
|
| 265 |
+
"naproxen 500mg BID (started 2 weeks ago for arthritis flare), omeprazole 20mg daily "
|
| 266 |
+
"(ran out 1 month ago). Allergies: sulfa. "
|
| 267 |
+
"Vitals: BP 92/58, HR 118, RR 20, SpO2 97%, Temp 36.8°C. "
|
| 268 |
+
"Exam: pale, diaphoretic, epigastric tenderness without rebound, "
|
| 269 |
+
"rectal exam positive for melena. "
|
| 270 |
+
"Labs: Hb 6.8 (from 13.2 three months ago), INR 3.8, BUN 45, Cr 1.0, "
|
| 271 |
+
"platelets 195K. Glasgow-Blatchford Score: 14."
|
| 272 |
+
),
|
| 273 |
+
"include_drug_check": True,
|
| 274 |
+
"include_guidelines": True,
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
"id": "gi_pancreatitis",
|
| 278 |
+
"specialty": "Gastroenterology",
|
| 279 |
+
"title": "Acute Gallstone Pancreatitis",
|
| 280 |
+
"expected_keywords": ["pancreatitis", "lipase", "gallstone", "fluid resuscitation", "cholecystectomy"],
|
| 281 |
+
"patient_text": (
|
| 282 |
+
"48-year-old female presents with severe epigastric pain radiating to the back for "
|
| 283 |
+
"12 hours, worsening after a fatty meal. Nausea and vomiting x6 episodes. "
|
| 284 |
+
"PMH: cholelithiasis (known but declined surgery), obesity (BMI 34), "
|
| 285 |
+
"hyperlipidemia. Medications: simvastatin 20mg daily. "
|
| 286 |
+
"Vitals: BP 105/70, HR 108, RR 22, SpO2 96%, Temp 38.1°C. "
|
| 287 |
+
"Exam: moderate distress, epigastric tenderness with guarding, decreased bowel sounds, "
|
| 288 |
+
"positive Murphy's sign. "
|
| 289 |
+
"Labs: lipase 2,850 U/L (normal <60), amylase 1,200, WBC 15.2K, "
|
| 290 |
+
"total bilirubin 3.2, direct bilirubin 2.5, ALT 285, AST 312, "
|
| 291 |
+
"Alk phos 380, triglycerides 220, Cr 0.9. "
|
| 292 |
+
"CT abdomen: diffuse pancreatic edema and peripancreatic fluid, "
|
| 293 |
+
"gallbladder with multiple stones, dilated CBD 10mm."
|
| 294 |
+
),
|
| 295 |
+
"include_drug_check": True,
|
| 296 |
+
"include_guidelines": True,
|
| 297 |
+
},
|
| 298 |
+
# ── Neurology ──
|
| 299 |
+
{
|
| 300 |
+
"id": "neuro_seizure",
|
| 301 |
+
"specialty": "Neurology",
|
| 302 |
+
"title": "Status Epilepticus",
|
| 303 |
+
"expected_keywords": ["seizure", "status epilepticus", "benzodiazepine", "lorazepam", "antiepileptic", "levetiracetam"],
|
| 304 |
+
"patient_text": (
|
| 305 |
+
"34-year-old male with known epilepsy brought by EMS with continuous seizure activity "
|
| 306 |
+
"for >10 minutes that has not stopped. Bystander reports he was walking, stiffened, "
|
| 307 |
+
"and fell with tonic-clonic activity. EMS gave midazolam 10mg IM 5 minutes ago with "
|
| 308 |
+
"brief pause, but seizure activity resumed. PMH: focal epilepsy (left temporal lesion), "
|
| 309 |
+
"prior breakthrough seizures (non-compliant with meds). Medications: levetiracetam "
|
| 310 |
+
"750mg BID (admits he hasn't taken it in 1 week). "
|
| 311 |
+
"Vitals: BP 175/95, HR 130, RR 8 (postictal), SpO2 85%, Temp 38.2°C. "
|
| 312 |
+
"Exam: continuous left-gaze deviation with rhythmic bilateral limb jerking, "
|
| 313 |
+
"no verbal response, bite mark on tongue, urinary incontinence."
|
| 314 |
+
),
|
| 315 |
+
"include_drug_check": True,
|
| 316 |
+
"include_guidelines": True,
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
"id": "neuro_meningitis",
|
| 320 |
+
"specialty": "Neurology",
|
| 321 |
+
"title": "Bacterial Meningitis",
|
| 322 |
+
"expected_keywords": ["meningitis", "lumbar puncture", "ceftriaxone", "vancomycin", "dexamethasone", "CSF"],
|
| 323 |
+
"patient_text": (
|
| 324 |
+
"20-year-old male college student presents with 18-hour history of severe headache, "
|
| 325 |
+
"fever, and neck stiffness. Roommate reports he's been confused for the past 3 hours. "
|
| 326 |
+
"Recently had an upper respiratory infection. Lives in a dormitory. "
|
| 327 |
+
"PMH: healthy, no medications, vaccinations up to date (received meningococcal "
|
| 328 |
+
"conjugate vaccine but not serogroup B vaccine). "
|
| 329 |
+
"Vitals: BP 98/62, HR 112, RR 24, SpO2 96%, Temp 39.8°C (103.6°F). "
|
| 330 |
+
"Exam: appears toxic, positive Kernig and Brudzinski signs, photophobia, "
|
| 331 |
+
"no focal neurologic deficits, GCS 12 (E3V4M5), petechial rash on trunk and extremities. "
|
| 332 |
+
"Labs: WBC 22K with 92% PMNs, lactate 3.1, Cr 1.0, platelets 120K."
|
| 333 |
+
),
|
| 334 |
+
"include_drug_check": False,
|
| 335 |
+
"include_guidelines": True,
|
| 336 |
+
},
|
| 337 |
+
# ── Psychiatry ──
|
| 338 |
+
{
|
| 339 |
+
"id": "psych_suicide",
|
| 340 |
+
"specialty": "Psychiatry",
|
| 341 |
+
"title": "Acute Suicidal Ideation with Plan",
|
| 342 |
+
"expected_keywords": ["suicide", "safety", "psychiatr", "risk assessment", "lethal means", "hospitalization"],
|
| 343 |
+
"patient_text": (
|
| 344 |
+
"45-year-old male veteran brought to ED by wife after she found a note. He reports "
|
| 345 |
+
"active suicidal ideation with plan to use his firearm (has access at home). States "
|
| 346 |
+
"he's been increasingly hopeless since job loss 3 months ago, not sleeping, lost "
|
| 347 |
+
"20 lbs, withdrawn from family and friends. Drinks 6-8 beers daily (increased from "
|
| 348 |
+
"occasional). PMH: PTSD (combat-related), major depressive disorder, chronic back pain. "
|
| 349 |
+
"Medications: sertraline 100mg daily, trazodone 50mg QHS, ibuprofen 600mg TID. "
|
| 350 |
+
"Denies prior suicide attempts. Father completed suicide at age 50. "
|
| 351 |
+
"Vitals: stable. Exam: sad affect, poor eye contact, psychomotor retardation, "
|
| 352 |
+
"coherent but hopeless in content. PHQ-9 score: 24 (severe). "
|
| 353 |
+
"Columbia Suicide Severity Rating Scale: active ideation with specific plan and intent."
|
| 354 |
+
),
|
| 355 |
+
"include_drug_check": True,
|
| 356 |
+
"include_guidelines": True,
|
| 357 |
+
},
|
| 358 |
+
# ── Pediatrics ──
|
| 359 |
+
{
|
| 360 |
+
"id": "peds_fever",
|
| 361 |
+
"specialty": "Pediatrics",
|
| 362 |
+
"title": "Febrile Neonate — 21-day-old",
|
| 363 |
+
"expected_keywords": ["neonate", "fever", "sepsis workup", "lumbar puncture", "ampicillin", "cefotaxime"],
|
| 364 |
+
"patient_text": (
|
| 365 |
+
"21-day-old male infant brought by concerned mother for fever. Temperature measured "
|
| 366 |
+
"at home: 38.4°C (101.1°F) rectal. Born full-term via uncomplicated vaginal delivery, "
|
| 367 |
+
"GBS negative. Breastfeeding well until today — decreased feeds for past 6 hours, "
|
| 368 |
+
"seems more sleepy than usual. No cough, rash, or vomiting. "
|
| 369 |
+
"One older sibling with runny nose. "
|
| 370 |
+
"Vitals: Temp 38.6°C (101.5°F) rectal, HR 180 (normal 100-160 for age), "
|
| 371 |
+
"RR 44, SpO2 98%. Birth weight 3.4 kg, current weight 3.7 kg. "
|
| 372 |
+
"Exam: slightly lethargic, soft fontanelle, no bulging, no rash, "
|
| 373 |
+
"cap refill 3 seconds, mottled skin, abdomen soft, no organomegaly. "
|
| 374 |
+
"Labs: WBC 22K, ANC 6500, CRP 35 mg/L, procalcitonin 0.8 ng/mL, "
|
| 375 |
+
"urinalysis negative."
|
| 376 |
+
),
|
| 377 |
+
"include_drug_check": False,
|
| 378 |
+
"include_guidelines": True,
|
| 379 |
+
},
|
| 380 |
+
{
|
| 381 |
+
"id": "peds_dehydration",
|
| 382 |
+
"specialty": "Pediatrics",
|
| 383 |
+
"title": "Severe Dehydration — Pediatric Gastroenteritis",
|
| 384 |
+
"expected_keywords": ["dehydration", "fluid", "oral rehydration", "IV fluid", "bolus", "electrolyte"],
|
| 385 |
+
"patient_text": (
|
| 386 |
+
"2-year-old female brought by parents for 3 days of watery diarrhea (8-10 episodes/day) "
|
| 387 |
+
"and vomiting (4-5 times/day). Decreased oral intake, last wet diaper >12 hours ago. "
|
| 388 |
+
"Daycare contacts have similar illness. PMH: healthy, vaccinations up to date including "
|
| 389 |
+
"rotavirus. No medications. Allergies: NKDA. "
|
| 390 |
+
"Vitals: HR 170 (tachycardic for age), BP 72/45, RR 30, SpO2 99%, "
|
| 391 |
+
"Temp 38.3°C, Weight 10.5 kg (12 kg at well child visit 1 month ago — 12.5% weight loss). "
|
| 392 |
+
"Exam: lethargic but arousable, sunken eyes, sunken anterior fontanelle, "
|
| 393 |
+
"dry oral mucosa with no tears, decreased skin turgor (tenting >2 seconds), "
|
| 394 |
+
"cap refill 4 seconds, cool extremities, abdomen diffusely tender, "
|
| 395 |
+
"hyperactive bowel sounds."
|
| 396 |
+
),
|
| 397 |
+
"include_drug_check": False,
|
| 398 |
+
"include_guidelines": True,
|
| 399 |
+
},
|
| 400 |
+
# ── Nephrology ──
|
| 401 |
+
{
|
| 402 |
+
"id": "renal_hyperkalemia",
|
| 403 |
+
"specialty": "Nephrology",
|
| 404 |
+
"title": "Severe Hyperkalemia with ECG Changes",
|
| 405 |
+
"expected_keywords": ["hyperkalemia", "calcium", "insulin", "dextrose", "dialysis", "potassium", "ECG"],
|
| 406 |
+
"patient_text": (
|
| 407 |
+
"72-year-old male with ESRD on hemodialysis (missed last 2 sessions due to "
|
| 408 |
+
"transportation issues) presents with generalized weakness and palpitations. "
|
| 409 |
+
"PMH: ESRD on MWF HD, type 2 DM, HTN, peripheral vascular disease. "
|
| 410 |
+
"Medications: sevelamer 800mg TID, calcitriol 0.25mcg daily, EPO injection "
|
| 411 |
+
"at dialysis, amlodipine 10mg, insulin glargine 20 units nightly, insulin lispro "
|
| 412 |
+
"sliding scale. "
|
| 413 |
+
"Vitals: BP 190/105, HR 52 (bradycardic), RR 22, SpO2 94%, Temp 36.5°C. "
|
| 414 |
+
"ECG: widened QRS (140ms), peaked T waves globally, loss of P waves, "
|
| 415 |
+
"junctional bradycardia. "
|
| 416 |
+
"Labs: K+ 7.8, BUN 98, Cr 11.2, bicarb 14, pH 7.22, glucose 220."
|
| 417 |
+
),
|
| 418 |
+
"include_drug_check": True,
|
| 419 |
+
"include_guidelines": True,
|
| 420 |
+
},
|
| 421 |
+
# ── Infectious Disease ──
|
| 422 |
+
{
|
| 423 |
+
"id": "id_pneumonia",
|
| 424 |
+
"specialty": "Infectious Disease",
|
| 425 |
+
"title": "Community-Acquired Pneumonia — Moderate Severity",
|
| 426 |
+
"expected_keywords": ["pneumonia", "antibiotic", "CURB-65", "ceftriaxone", "azithromycin", "chest x-ray"],
|
| 427 |
+
"patient_text": (
|
| 428 |
+
"58-year-old male presents with 4-day history of productive cough (yellow-green sputum), "
|
| 429 |
+
"fever, chills, and pleuritic right-sided chest pain. Reports dyspnea with minimal "
|
| 430 |
+
"exertion. PMH: COPD (FEV1 55% predicted), type 2 DM, moderate alcohol use "
|
| 431 |
+
"(2-3 drinks daily). Medications: tiotropium 18mcg inhaled daily, albuterol PRN, "
|
| 432 |
+
"metformin 1000mg BID. Former 20 pack-year smoker, quit 5 years ago. "
|
| 433 |
+
"Vitals: BP 128/78, HR 102, RR 26, SpO2 90% on RA, Temp 39.2°C (102.6°F). "
|
| 434 |
+
"Exam: appears ill, right basilar crackles with bronchial breath sounds, "
|
| 435 |
+
"egophony right base, no wheezing currently. "
|
| 436 |
+
"CXR: right lower lobe consolidation with air bronchograms. "
|
| 437 |
+
"Labs: WBC 16.8K, BUN 28, Cr 1.0, procalcitonin 3.2, lactate 1.8. "
|
| 438 |
+
"CURB-65 score: 2 (confusion absent, urea elevated, RR ≥30, BP normal, age <65)."
|
| 439 |
+
),
|
| 440 |
+
"include_drug_check": True,
|
| 441 |
+
"include_guidelines": True,
|
| 442 |
+
},
|
| 443 |
+
# ── Hematology ──
|
| 444 |
+
{
|
| 445 |
+
"id": "heme_dvt_pe",
|
| 446 |
+
"specialty": "Hematology",
|
| 447 |
+
"title": "DVT with Moderate PE — Cancer Patient",
|
| 448 |
+
"expected_keywords": ["DVT", "PE", "anticoagulation", "LMWH", "cancer", "thrombosis"],
|
| 449 |
+
"patient_text": (
|
| 450 |
+
"62-year-old female with recently diagnosed pancreatic adenocarcinoma (on chemotherapy, "
|
| 451 |
+
"cycle 2 of gemcitabine/nab-paclitaxel) presents with 5-day left leg swelling and "
|
| 452 |
+
"new-onset dyspnea on exertion since yesterday. No chest pain, hemoptysis, or syncope. "
|
| 453 |
+
"PMH: pancreatic cancer stage III (diagnosed 6 weeks ago), HTN, hypothyroidism. "
|
| 454 |
+
"Medications: gemcitabine/nab-paclitaxel q28d, ondansetron PRN, levothyroxine 88mcg, "
|
| 455 |
+
"lisinopril 10mg. "
|
| 456 |
+
"Vitals: BP 118/72, HR 98, RR 22, SpO2 93% on RA, Temp 37.0°C. "
|
| 457 |
+
"Exam: left leg circumference 4cm greater than right, tender calf, "
|
| 458 |
+
"Homan's sign positive. Lungs CTA bilaterally. "
|
| 459 |
+
"Labs: D-dimer 8,500, Hb 10.2, plt 85K, INR 1.0, Cr 0.8. "
|
| 460 |
+
"LE Doppler: occlusive thrombus left femoral and popliteal veins. "
|
| 461 |
+
"CTPA: segmental PE in right lower lobe pulmonary artery. "
|
| 462 |
+
"Troponin normal, BNP 120, RV normal on echo."
|
| 463 |
+
),
|
| 464 |
+
"include_drug_check": True,
|
| 465 |
+
"include_guidelines": True,
|
| 466 |
+
},
|
| 467 |
+
# ── OB/GYN ──
|
| 468 |
+
{
|
| 469 |
+
"id": "obgyn_preeclampsia",
|
| 470 |
+
"specialty": "OB/GYN",
|
| 471 |
+
"title": "Severe Preeclampsia at 33 Weeks",
|
| 472 |
+
"expected_keywords": ["preeclampsia", "magnesium sulfate", "blood pressure", "HELLP", "delivery"],
|
| 473 |
+
"patient_text": (
|
| 474 |
+
"28-year-old G2P1 at 33 weeks gestation presents with severe headache unresponsive "
|
| 475 |
+
"to acetaminophen, visual disturbances (scotomata), and right upper quadrant pain "
|
| 476 |
+
"for 6 hours. First pregnancy was uncomplicated. This pregnancy: mild chronic HTN, "
|
| 477 |
+
"started on labetalol 200mg BID at 16 weeks. Started low-dose aspirin at 12 weeks. "
|
| 478 |
+
"Medications: labetalol 200mg BID, aspirin 81mg daily, prenatal vitamins. "
|
| 479 |
+
"Vitals: BP 178/112 (confirmed on repeat after 15 min: 174/108), HR 92, "
|
| 480 |
+
"RR 18, SpO2 98%, Temp 37.0°C. "
|
| 481 |
+
"Exam: 3+ pitting edema bilateral LE, RUQ tenderness, brisk reflexes (3+) "
|
| 482 |
+
"with 2 beats of clonus bilaterally. Fetal heart tones 140s, reassuring. "
|
| 483 |
+
"Labs: PLT 82K, AST 220, ALT 195, LDH 650, Cr 1.3, uric acid 8.2, "
|
| 484 |
+
"protein/creatinine ratio 4.8, peripheral smear: schistocytes present."
|
| 485 |
+
),
|
| 486 |
+
"include_drug_check": True,
|
| 487 |
+
"include_guidelines": True,
|
| 488 |
+
},
|
| 489 |
+
# ── Toxicology ──
|
| 490 |
+
{
|
| 491 |
+
"id": "tox_acetaminophen",
|
| 492 |
+
"specialty": "Toxicology",
|
| 493 |
+
"title": "Acetaminophen Overdose",
|
| 494 |
+
"expected_keywords": ["acetaminophen", "NAC", "N-acetylcysteine", "Rumack", "liver", "antidote"],
|
| 495 |
+
"patient_text": (
|
| 496 |
+
"24-year-old female brought to ED by friend 6 hours after intentionally ingesting "
|
| 497 |
+
"~30 tablets of Extra Strength Tylenol (500mg each = ~15g total). She now has nausea "
|
| 498 |
+
"and vomiting. Reports this was a suicide attempt after breakup. Currently expressing "
|
| 499 |
+
"regret and requesting help. PMH: depression, anxiety. "
|
| 500 |
+
"Medications: escitalopram 10mg daily. No OTC meds regularly. "
|
| 501 |
+
"Vitals: BP 108/68, HR 92, RR 16, SpO2 99%, Temp 37.0°C. "
|
| 502 |
+
"Exam: mild epigastric tenderness, otherwise unremarkable. "
|
| 503 |
+
"Labs: APAP level at 6 hours: 180 mcg/mL (treatment line at 4h is 150), "
|
| 504 |
+
"AST 42, ALT 38, INR 1.0, Cr 0.7, glucose 95. "
|
| 505 |
+
"Above treatment line on Rumack-Matthew nomogram."
|
| 506 |
+
),
|
| 507 |
+
"include_drug_check": True,
|
| 508 |
+
"include_guidelines": True,
|
| 509 |
+
},
|
| 510 |
+
# ── Multi-system / Complex ──
|
| 511 |
+
{
|
| 512 |
+
"id": "complex_polypharmacy",
|
| 513 |
+
"specialty": "Geriatrics",
|
| 514 |
+
"title": "Elderly Polypharmacy with Falls and AKI",
|
| 515 |
+
"expected_keywords": ["fall", "polypharmacy", "kidney", "AKI", "dehydration", "medication review"],
|
| 516 |
+
"patient_text": (
|
| 517 |
+
"84-year-old female brought from assisted living after being found on the floor this "
|
| 518 |
+
"morning. Staff reports she's had decreased oral intake for 3 days due to 'stomach bug.' "
|
| 519 |
+
"History of 2 falls in the past month. PMH: HTN, CHF (EF 45%), atrial fibrillation, "
|
| 520 |
+
"type 2 DM, CKD stage 3 (baseline Cr 1.4), osteoporosis, depression, insomnia, "
|
| 521 |
+
"osteoarthritis. "
|
| 522 |
+
"Medications: metoprolol succinate 100mg daily, apixaban 5mg BID, lisinopril 20mg, "
|
| 523 |
+
"furosemide 40mg daily, metformin 500mg BID, spironolactone 25mg, amlodipine 5mg, "
|
| 524 |
+
"mirtazapine 15mg QHS, zolpidem 5mg QHS, alendronate 70mg weekly, "
|
| 525 |
+
"calcium/vitamin D, aspirin 81mg, acetaminophen 650mg TID. "
|
| 526 |
+
"Vitals: BP 88/52 (lying), 62/40 (sitting — orthostatic), HR 48 (irregular), "
|
| 527 |
+
"RR 18, SpO2 95%, Temp 36.2°C. "
|
| 528 |
+
"Exam: dry mucous membranes, irregular irregularly rhythm, clear lungs, "
|
| 529 |
+
"mild confusion (baseline oriented x3, now x1), right hip tenderness. "
|
| 530 |
+
"Labs: Na+ 126, K+ 6.1, BUN 62, Cr 3.2 (from 1.4), glucose 52, TSH 4.5, "
|
| 531 |
+
"Hb 10.2, WBC 9.8, INR 1.0 (on apixaban). "
|
| 532 |
+
"Hip XR: right intertrochanteric hip fracture."
|
| 533 |
+
),
|
| 534 |
+
"include_drug_check": True,
|
| 535 |
+
"include_guidelines": True,
|
| 536 |
+
},
|
| 537 |
+
]
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
# ─────────────────────────────────────────────────
|
| 541 |
+
# Test Runner
|
| 542 |
+
# ─────────────────────────────────────────────────
|
| 543 |
+
|
| 544 |
+
async def run_case(client: httpx.AsyncClient, case: dict, verbose: bool = True) -> dict:
|
| 545 |
+
"""Submit a case and poll until done. Returns result dict with timing."""
|
| 546 |
+
case_id_label = case["id"]
|
| 547 |
+
title = case["title"]
|
| 548 |
+
|
| 549 |
+
if verbose:
|
| 550 |
+
print(f"\n{'='*70}")
|
| 551 |
+
print(f" [{case_id_label}] {title}")
|
| 552 |
+
print(f" Specialty: {case['specialty']}")
|
| 553 |
+
print(f"{'='*70}")
|
| 554 |
+
|
| 555 |
+
start = time.time()
|
| 556 |
+
|
| 557 |
+
# Submit
|
| 558 |
+
body = {
|
| 559 |
+
"patient_text": case["patient_text"],
|
| 560 |
+
"include_drug_check": case.get("include_drug_check", True),
|
| 561 |
+
"include_guidelines": case.get("include_guidelines", True),
|
| 562 |
+
}
|
| 563 |
+
r = await client.post(f"{API}/api/cases/submit", json=body)
|
| 564 |
+
if r.status_code != 200:
|
| 565 |
+
return {"case_id": case_id_label, "error": f"Submit failed: {r.status_code} {r.text}", "elapsed": 0}
|
| 566 |
+
|
| 567 |
+
data = r.json()
|
| 568 |
+
server_case_id = data["case_id"]
|
| 569 |
+
if verbose:
|
| 570 |
+
print(f" Submitted: {server_case_id}")
|
| 571 |
+
|
| 572 |
+
# Poll
|
| 573 |
+
result = None
|
| 574 |
+
steps = []
|
| 575 |
+
for i in range(90): # up to 7.5 minutes
|
| 576 |
+
await asyncio.sleep(5)
|
| 577 |
+
r = await client.get(f"{API}/api/cases/{server_case_id}")
|
| 578 |
+
result = r.json()
|
| 579 |
+
state = result.get("state", {})
|
| 580 |
+
steps = state.get("steps", [])
|
| 581 |
+
|
| 582 |
+
if verbose and i % 3 == 0:
|
| 583 |
+
statuses = [f"{s['step_id']}={s['status']}" for s in steps]
|
| 584 |
+
print(f" [{i*5}s] {', '.join(statuses)}")
|
| 585 |
+
|
| 586 |
+
all_done = all(s["status"] in ("completed", "failed", "skipped") for s in steps)
|
| 587 |
+
if all_done:
|
| 588 |
+
break
|
| 589 |
+
|
| 590 |
+
elapsed = round(time.time() - start, 1)
|
| 591 |
+
|
| 592 |
+
# Analyze results
|
| 593 |
+
step_summary = {}
|
| 594 |
+
for s in steps:
|
| 595 |
+
step_summary[s["step_id"]] = {
|
| 596 |
+
"status": s["status"],
|
| 597 |
+
"duration_ms": s.get("duration_ms", 0),
|
| 598 |
+
"error": s.get("error", ""),
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
report = result.get("report") if result else None
|
| 602 |
+
all_passed = all(s["status"] == "completed" for s in steps)
|
| 603 |
+
any_failed = any(s["status"] == "failed" for s in steps)
|
| 604 |
+
|
| 605 |
+
# Check expected keywords in report
|
| 606 |
+
keyword_hits = []
|
| 607 |
+
keyword_misses = []
|
| 608 |
+
if report:
|
| 609 |
+
report_text = json.dumps(report).lower()
|
| 610 |
+
for kw in case.get("expected_keywords", []):
|
| 611 |
+
if kw.lower() in report_text:
|
| 612 |
+
keyword_hits.append(kw)
|
| 613 |
+
else:
|
| 614 |
+
keyword_misses.append(kw)
|
| 615 |
+
|
| 616 |
+
result_data = {
|
| 617 |
+
"case_id": case_id_label,
|
| 618 |
+
"title": title,
|
| 619 |
+
"specialty": case["specialty"],
|
| 620 |
+
"all_passed": all_passed,
|
| 621 |
+
"any_failed": any_failed,
|
| 622 |
+
"elapsed_seconds": elapsed,
|
| 623 |
+
"steps": step_summary,
|
| 624 |
+
"keyword_hits": keyword_hits,
|
| 625 |
+
"keyword_misses": keyword_misses,
|
| 626 |
+
"keyword_coverage": (
|
| 627 |
+
f"{len(keyword_hits)}/{len(keyword_hits) + len(keyword_misses)}"
|
| 628 |
+
if (keyword_hits or keyword_misses) else "N/A"
|
| 629 |
+
),
|
| 630 |
+
"report": report,
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
if verbose:
|
| 634 |
+
print(f"\n Results ({elapsed}s total):")
|
| 635 |
+
for sid, info in step_summary.items():
|
| 636 |
+
status_icon = "✓" if info["status"] == "completed" else ("✗" if info["status"] == "failed" else "○")
|
| 637 |
+
print(f" {status_icon} {sid:12s} {info['status']:10s} ({info['duration_ms']}ms)")
|
| 638 |
+
if info["error"]:
|
| 639 |
+
print(f" ERROR: {info['error'][:120]}")
|
| 640 |
+
|
| 641 |
+
if report:
|
| 642 |
+
print(f"\n Keywords found: {', '.join(keyword_hits) if keyword_hits else 'none'}")
|
| 643 |
+
if keyword_misses:
|
| 644 |
+
print(f" Keywords missing: {', '.join(keyword_misses)}")
|
| 645 |
+
print(f" Keyword coverage: {result_data['keyword_coverage']}")
|
| 646 |
+
|
| 647 |
+
# Print condensed report
|
| 648 |
+
if verbose:
|
| 649 |
+
print(f"\n --- Report Summary ---")
|
| 650 |
+
print(f" Patient: {report.get('patient_summary', 'N/A')[:200]}")
|
| 651 |
+
dx = report.get("differential_diagnosis", [])
|
| 652 |
+
if dx:
|
| 653 |
+
print(f" Top diagnosis: {dx[0].get('diagnosis', 'N/A')} ({dx[0].get('likelihood', 'N/A')})")
|
| 654 |
+
warnings = report.get("drug_interaction_warnings", [])
|
| 655 |
+
if warnings:
|
| 656 |
+
print(f" Drug warnings: {len(warnings)}")
|
| 657 |
+
recs = report.get("guideline_recommendations", [])
|
| 658 |
+
if recs:
|
| 659 |
+
print(f" Guideline recs: {len(recs)}")
|
| 660 |
+
steps_rec = report.get("suggested_next_steps", [])
|
| 661 |
+
if steps_rec:
|
| 662 |
+
print(f" Next steps: {len(steps_rec)}")
|
| 663 |
+
else:
|
| 664 |
+
print(f" ⚠ No report generated")
|
| 665 |
+
|
| 666 |
+
return result_data
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
async def main():
|
| 670 |
+
parser = argparse.ArgumentParser(description="CDS Agent Clinical Test Suite")
|
| 671 |
+
parser.add_argument("--case", help="Run a single test case by ID")
|
| 672 |
+
parser.add_argument("--specialty", help="Run all cases for a specialty (partial match)")
|
| 673 |
+
parser.add_argument("--list", action="store_true", help="List available test cases")
|
| 674 |
+
parser.add_argument("--quiet", action="store_true", help="Minimal output")
|
| 675 |
+
parser.add_argument("--report", help="Save results to JSON file")
|
| 676 |
+
args = parser.parse_args()
|
| 677 |
+
|
| 678 |
+
if args.list:
|
| 679 |
+
print(f"\nAvailable test cases ({len(TEST_CASES)} total):\n")
|
| 680 |
+
by_specialty = {}
|
| 681 |
+
for tc in TEST_CASES:
|
| 682 |
+
by_specialty.setdefault(tc["specialty"], []).append(tc)
|
| 683 |
+
for spec, cases in sorted(by_specialty.items()):
|
| 684 |
+
print(f" {spec}:")
|
| 685 |
+
for c in cases:
|
| 686 |
+
print(f" {c['id']:30s} {c['title']}")
|
| 687 |
+
return
|
| 688 |
+
|
| 689 |
+
# Filter cases
|
| 690 |
+
cases_to_run = TEST_CASES
|
| 691 |
+
if args.case:
|
| 692 |
+
cases_to_run = [c for c in TEST_CASES if c["id"] == args.case]
|
| 693 |
+
if not cases_to_run:
|
| 694 |
+
print(f"Case '{args.case}' not found. Use --list to see available cases.")
|
| 695 |
+
return
|
| 696 |
+
elif args.specialty:
|
| 697 |
+
cases_to_run = [c for c in TEST_CASES if args.specialty.lower() in c["specialty"].lower()]
|
| 698 |
+
if not cases_to_run:
|
| 699 |
+
print(f"No cases found for specialty '{args.specialty}'. Use --list.")
|
| 700 |
+
return
|
| 701 |
+
|
| 702 |
+
verbose = not args.quiet
|
| 703 |
+
print(f"\n{'#'*70}")
|
| 704 |
+
print(f" CDS Agent — Clinical Test Suite")
|
| 705 |
+
print(f" Running {len(cases_to_run)} test case(s)")
|
| 706 |
+
print(f" API: {API}")
|
| 707 |
+
print(f"{'#'*70}")
|
| 708 |
+
|
| 709 |
+
# Check health
|
| 710 |
+
async with httpx.AsyncClient(timeout=120) as client:
|
| 711 |
+
try:
|
| 712 |
+
health = await client.get(f"{API}/api/health")
|
| 713 |
+
if health.status_code != 200:
|
| 714 |
+
print(f"\n ✗ Backend health check failed: {health.status_code}")
|
| 715 |
+
return
|
| 716 |
+
print(f" ✓ Backend healthy\n")
|
| 717 |
+
except Exception as e:
|
| 718 |
+
print(f"\n ✗ Cannot reach backend at {API}: {e}")
|
| 719 |
+
return
|
| 720 |
+
|
| 721 |
+
results = []
|
| 722 |
+
for case in cases_to_run:
|
| 723 |
+
try:
|
| 724 |
+
result = await run_case(client, case, verbose=verbose)
|
| 725 |
+
results.append(result)
|
| 726 |
+
except Exception as e:
|
| 727 |
+
print(f"\n ✗ Exception running case {case['id']}: {e}")
|
| 728 |
+
results.append({
|
| 729 |
+
"case_id": case["id"],
|
| 730 |
+
"title": case["title"],
|
| 731 |
+
"error": str(e),
|
| 732 |
+
"all_passed": False,
|
| 733 |
+
})
|
| 734 |
+
|
| 735 |
+
# Summary
|
| 736 |
+
print(f"\n\n{'#'*70}")
|
| 737 |
+
print(f" SUMMARY — {len(results)} cases")
|
| 738 |
+
print(f"{'#'*70}\n")
|
| 739 |
+
|
| 740 |
+
passed = sum(1 for r in results if r.get("all_passed"))
|
| 741 |
+
failed = sum(1 for r in results if r.get("any_failed"))
|
| 742 |
+
total_time = sum(r.get("elapsed_seconds", 0) for r in results)
|
| 743 |
+
|
| 744 |
+
for r in results:
|
| 745 |
+
icon = "✓" if r.get("all_passed") else "✗"
|
| 746 |
+
kw = r.get("keyword_coverage", "N/A")
|
| 747 |
+
elapsed = r.get("elapsed_seconds", 0)
|
| 748 |
+
print(f" {icon} [{r['case_id']:30s}] {elapsed:6.1f}s keywords:{kw:>5s} {r.get('title', '')}")
|
| 749 |
+
|
| 750 |
+
print(f"\n Passed: {passed}/{len(results)}")
|
| 751 |
+
print(f" Failed: {failed}/{len(results)}")
|
| 752 |
+
print(f" Total time: {total_time:.0f}s ({total_time/60:.1f} min)")
|
| 753 |
+
|
| 754 |
+
# Save report
|
| 755 |
+
if args.report:
|
| 756 |
+
with open(args.report, "w") as f:
|
| 757 |
+
json.dump(results, f, indent=2, default=str)
|
| 758 |
+
print(f"\n Results saved to {args.report}")
|
| 759 |
+
|
| 760 |
+
|
| 761 |
+
if __name__ == "__main__":
|
| 762 |
+
asyncio.run(main())
|
src/backend/test_e2e.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Full E2E test: submit case, poll until done, print report."""
|
| 2 |
+
import httpx
|
| 3 |
+
import asyncio
|
| 4 |
+
import json
|
| 5 |
+
import time
|
| 6 |
+
|
| 7 |
+
API = "http://localhost:8002"
|
| 8 |
+
|
| 9 |
+
async def main():
|
| 10 |
+
async with httpx.AsyncClient(timeout=30) as client:
|
| 11 |
+
# Submit case
|
| 12 |
+
body = {
|
| 13 |
+
"patient_text": (
|
| 14 |
+
"55-year-old male presenting with chest pain radiating to left arm "
|
| 15 |
+
"for the past 2 hours. History of hypertension and type 2 diabetes. "
|
| 16 |
+
"Current medications: metformin 1000mg BID, lisinopril 20mg daily, "
|
| 17 |
+
"aspirin 81mg daily. Vitals: BP 160/95, HR 88, SpO2 97%."
|
| 18 |
+
),
|
| 19 |
+
"include_drug_check": True,
|
| 20 |
+
"include_guidelines": True,
|
| 21 |
+
}
|
| 22 |
+
r = await client.post(f"{API}/api/cases/submit", json=body)
|
| 23 |
+
data = r.json()
|
| 24 |
+
case_id = data["case_id"]
|
| 25 |
+
print(f"Submitted case: {case_id}")
|
| 26 |
+
|
| 27 |
+
# Poll until done
|
| 28 |
+
for i in range(60): # up to 5 minutes
|
| 29 |
+
await asyncio.sleep(5)
|
| 30 |
+
r = await client.get(f"{API}/api/cases/{case_id}")
|
| 31 |
+
result = r.json()
|
| 32 |
+
state = result.get("state", {})
|
| 33 |
+
steps = state.get("steps", [])
|
| 34 |
+
|
| 35 |
+
# Print status
|
| 36 |
+
statuses = [f"{s['step_id']}={s['status']}" for s in steps]
|
| 37 |
+
print(f" [{i*5}s] {', '.join(statuses)}")
|
| 38 |
+
|
| 39 |
+
# Check if all done
|
| 40 |
+
all_done = all(
|
| 41 |
+
s["status"] in ("completed", "failed") for s in steps
|
| 42 |
+
)
|
| 43 |
+
if all_done:
|
| 44 |
+
break
|
| 45 |
+
|
| 46 |
+
# Print step details
|
| 47 |
+
print("\n=== Step Results ===")
|
| 48 |
+
for s in steps:
|
| 49 |
+
err = s.get("error", "")
|
| 50 |
+
dur = s.get("duration_ms", "?")
|
| 51 |
+
print(f" {s['step_id']:12s} {s['status']:10s} ({dur}ms) {err[:100] if err else 'OK'}")
|
| 52 |
+
|
| 53 |
+
# Print report
|
| 54 |
+
report = result.get("report")
|
| 55 |
+
if report:
|
| 56 |
+
print("\n=== CDS Report ===")
|
| 57 |
+
print(json.dumps(report, indent=2, default=str)[:4000])
|
| 58 |
+
else:
|
| 59 |
+
print("\nNo report generated.")
|
| 60 |
+
|
| 61 |
+
asyncio.run(main())
|
src/backend/test_poll.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Quick script to poll a case result."""
|
| 2 |
+
import httpx
|
| 3 |
+
import asyncio
|
| 4 |
+
import json
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
async def main():
|
| 8 |
+
case_id = sys.argv[1] if len(sys.argv) > 1 else "55a04557"
|
| 9 |
+
r = await httpx.AsyncClient(timeout=30).get(f"http://localhost:8000/api/cases/{case_id}")
|
| 10 |
+
d = r.json()
|
| 11 |
+
steps = d.get("state", {}).get("steps", [])
|
| 12 |
+
for s in steps:
|
| 13 |
+
err = s.get("error", "")
|
| 14 |
+
print(f" {s['step_id']:12s} => {s['status']:10s} ({s.get('duration_ms','?')}ms) {err[:80] if err else ''}")
|
| 15 |
+
|
| 16 |
+
report = d.get("report")
|
| 17 |
+
if report:
|
| 18 |
+
print("\n=== REPORT (truncated) ===")
|
| 19 |
+
print(json.dumps(report, indent=2, default=str)[:3000])
|
| 20 |
+
else:
|
| 21 |
+
print("\nNo report yet.")
|
| 22 |
+
|
| 23 |
+
asyncio.run(main())
|
src/backend/test_rag_quality.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RAG Retrieval Quality Test
|
| 3 |
+
|
| 4 |
+
Directly tests the ChromaDB vector search to validate that the expanded
|
| 5 |
+
clinical guidelines corpus returns relevant results for diverse clinical queries.
|
| 6 |
+
|
| 7 |
+
This bypasses the full agent pipeline and tests the retrieval layer in isolation.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
cd src/backend
|
| 11 |
+
python test_rag_quality.py # Run all queries
|
| 12 |
+
python test_rag_quality.py --rebuild # Delete ChromaDB and rebuild from scratch
|
| 13 |
+
python test_rag_quality.py --stats # Show collection statistics only
|
| 14 |
+
python test_rag_quality.py --query "chest pain evaluation" # Test a single query
|
| 15 |
+
"""
|
| 16 |
+
import asyncio
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import shutil
|
| 20 |
+
import sys
|
| 21 |
+
import os
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
# Ensure the app package is importable
|
| 25 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 26 |
+
|
| 27 |
+
os.environ.setdefault("MEDGEMMA_API_KEY", "dummy") # Prevent settings validation error
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ─────────────────────────────────────────────────
|
| 31 |
+
# Test Queries: (query, expected_specialty, expected_title_substring)
|
| 32 |
+
# Each query simulates what the orchestrator would send to the RAG tool.
|
| 33 |
+
# ─────────────────────────────────────────────────
|
| 34 |
+
|
| 35 |
+
RAG_TEST_QUERIES = [
|
| 36 |
+
# Cardiology
|
| 37 |
+
{
|
| 38 |
+
"query": "Acute chest pain evaluation and troponin testing guidelines",
|
| 39 |
+
"expected_specialties": ["Cardiology"],
|
| 40 |
+
"expected_title_keywords": ["chest pain", "ACS", "STEMI"],
|
| 41 |
+
"min_relevance": 0.3,
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"query": "Heart failure management with reduced ejection fraction HFrEF",
|
| 45 |
+
"expected_specialties": ["Cardiology"],
|
| 46 |
+
"expected_title_keywords": ["heart failure"],
|
| 47 |
+
"min_relevance": 0.3,
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"query": "Atrial fibrillation anticoagulation and rate control",
|
| 51 |
+
"expected_specialties": ["Cardiology"],
|
| 52 |
+
"expected_title_keywords": ["atrial fibrillation", "AFib"],
|
| 53 |
+
"min_relevance": 0.3,
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"query": "Management of acute pulmonary embolism with hemodynamic instability",
|
| 57 |
+
"expected_specialties": ["Cardiology", "Pulmonology", "Hematology"],
|
| 58 |
+
"expected_title_keywords": ["pulmonary embolism", "PE", "VTE"],
|
| 59 |
+
"min_relevance": 0.25,
|
| 60 |
+
},
|
| 61 |
+
# Emergency Medicine
|
| 62 |
+
{
|
| 63 |
+
"query": "Acute ischemic stroke tPA thrombolysis eligibility criteria",
|
| 64 |
+
"expected_specialties": ["Emergency Medicine", "Neurology"],
|
| 65 |
+
"expected_title_keywords": ["stroke", "CVA"],
|
| 66 |
+
"min_relevance": 0.3,
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"query": "Sepsis hour-1 bundle treatment with IV fluids and antibiotics",
|
| 70 |
+
"expected_specialties": ["Emergency Medicine"],
|
| 71 |
+
"expected_title_keywords": ["sepsis"],
|
| 72 |
+
"min_relevance": 0.3,
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"query": "Anaphylaxis emergency treatment with epinephrine",
|
| 76 |
+
"expected_specialties": ["Emergency Medicine"],
|
| 77 |
+
"expected_title_keywords": ["anaphylaxis"],
|
| 78 |
+
"min_relevance": 0.3,
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"query": "Trauma ATLS assessment primary survey hemorrhagic shock",
|
| 82 |
+
"expected_specialties": ["Emergency Medicine"],
|
| 83 |
+
"expected_title_keywords": ["trauma"],
|
| 84 |
+
"min_relevance": 0.25,
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"query": "Seizure management status epilepticus benzodiazepine protocol",
|
| 88 |
+
"expected_specialties": ["Emergency Medicine", "Neurology"],
|
| 89 |
+
"expected_title_keywords": ["seizure"],
|
| 90 |
+
"min_relevance": 0.25,
|
| 91 |
+
},
|
| 92 |
+
# Endocrinology
|
| 93 |
+
{
|
| 94 |
+
"query": "Diabetic ketoacidosis DKA insulin drip and fluid management",
|
| 95 |
+
"expected_specialties": ["Endocrinology"],
|
| 96 |
+
"expected_title_keywords": ["DKA", "diabetic ketoacidosis"],
|
| 97 |
+
"min_relevance": 0.3,
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"query": "Type 2 diabetes management metformin A1C targets",
|
| 101 |
+
"expected_specialties": ["Endocrinology"],
|
| 102 |
+
"expected_title_keywords": ["diabetes", "DM"],
|
| 103 |
+
"min_relevance": 0.3,
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"query": "Thyroid disease hyperthyroidism Graves disease treatment",
|
| 107 |
+
"expected_specialties": ["Endocrinology"],
|
| 108 |
+
"expected_title_keywords": ["thyroid"],
|
| 109 |
+
"min_relevance": 0.3,
|
| 110 |
+
},
|
| 111 |
+
# Pulmonology
|
| 112 |
+
{
|
| 113 |
+
"query": "COPD exacerbation treatment bronchodilators steroids antibiotics",
|
| 114 |
+
"expected_specialties": ["Pulmonology"],
|
| 115 |
+
"expected_title_keywords": ["COPD"],
|
| 116 |
+
"min_relevance": 0.3,
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"query": "Acute asthma exacerbation treatment albuterol magnesium",
|
| 120 |
+
"expected_specialties": ["Pulmonology", "Pediatrics"],
|
| 121 |
+
"expected_title_keywords": ["asthma"],
|
| 122 |
+
"min_relevance": 0.3,
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"query": "Community acquired pneumonia antibiotic selection CURB-65",
|
| 126 |
+
"expected_specialties": ["Pulmonology", "Infectious Disease"],
|
| 127 |
+
"expected_title_keywords": ["pneumonia"],
|
| 128 |
+
"min_relevance": 0.3,
|
| 129 |
+
},
|
| 130 |
+
# Gastroenterology
|
| 131 |
+
{
|
| 132 |
+
"query": "Upper GI bleeding management endoscopy PPI transfusion",
|
| 133 |
+
"expected_specialties": ["Gastroenterology"],
|
| 134 |
+
"expected_title_keywords": ["GI bleed", "upper GI"],
|
| 135 |
+
"min_relevance": 0.3,
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"query": "Acute pancreatitis management fluid resuscitation pain control",
|
| 139 |
+
"expected_specialties": ["Gastroenterology"],
|
| 140 |
+
"expected_title_keywords": ["pancreatitis"],
|
| 141 |
+
"min_relevance": 0.3,
|
| 142 |
+
},
|
| 143 |
+
# Neurology
|
| 144 |
+
{
|
| 145 |
+
"query": "Epilepsy seizure medication selection antiepileptic drugs",
|
| 146 |
+
"expected_specialties": ["Neurology"],
|
| 147 |
+
"expected_title_keywords": ["epilepsy"],
|
| 148 |
+
"min_relevance": 0.3,
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"query": "Bacterial meningitis empiric antibiotics lumbar puncture",
|
| 152 |
+
"expected_specialties": ["Neurology", "Infectious Disease"],
|
| 153 |
+
"expected_title_keywords": ["meningitis"],
|
| 154 |
+
"min_relevance": 0.3,
|
| 155 |
+
},
|
| 156 |
+
# Psychiatry
|
| 157 |
+
{
|
| 158 |
+
"query": "Suicide risk assessment safety planning lethal means counseling",
|
| 159 |
+
"expected_specialties": ["Psychiatry"],
|
| 160 |
+
"expected_title_keywords": ["suicide", "suicid"],
|
| 161 |
+
"min_relevance": 0.3,
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"query": "Major depressive disorder SSRI treatment algorithm",
|
| 165 |
+
"expected_specialties": ["Psychiatry"],
|
| 166 |
+
"expected_title_keywords": ["depression", "depressive"],
|
| 167 |
+
"min_relevance": 0.3,
|
| 168 |
+
},
|
| 169 |
+
# Pediatrics
|
| 170 |
+
{
|
| 171 |
+
"query": "Neonatal fever sepsis workup guidelines for infants under 60 days",
|
| 172 |
+
"expected_specialties": ["Pediatrics"],
|
| 173 |
+
"expected_title_keywords": ["fever", "neonate", "neonatal"],
|
| 174 |
+
"min_relevance": 0.25,
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"query": "Pediatric dehydration oral rehydration IV fluid bolus",
|
| 178 |
+
"expected_specialties": ["Pediatrics"],
|
| 179 |
+
"expected_title_keywords": ["dehydration"],
|
| 180 |
+
"min_relevance": 0.25,
|
| 181 |
+
},
|
| 182 |
+
# Nephrology
|
| 183 |
+
{
|
| 184 |
+
"query": "Hyperkalemia emergency management calcium insulin kayexalate dialysis",
|
| 185 |
+
"expected_specialties": ["Nephrology", "Emergency Medicine"],
|
| 186 |
+
"expected_title_keywords": ["hyperkalemia"],
|
| 187 |
+
"min_relevance": 0.25,
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"query": "Acute kidney injury management and staging KDIGO",
|
| 191 |
+
"expected_specialties": ["Nephrology"],
|
| 192 |
+
"expected_title_keywords": ["AKI", "kidney injury"],
|
| 193 |
+
"min_relevance": 0.25,
|
| 194 |
+
},
|
| 195 |
+
# Hematology
|
| 196 |
+
{
|
| 197 |
+
"query": "Venous thromboembolism DVT PE anticoagulation treatment duration",
|
| 198 |
+
"expected_specialties": ["Hematology", "Cardiology"],
|
| 199 |
+
"expected_title_keywords": ["VTE", "thromboembolism"],
|
| 200 |
+
"min_relevance": 0.25,
|
| 201 |
+
},
|
| 202 |
+
# Infectious Disease
|
| 203 |
+
{
|
| 204 |
+
"query": "HIV antiretroviral therapy guidelines initial regimen",
|
| 205 |
+
"expected_specialties": ["Infectious Disease"],
|
| 206 |
+
"expected_title_keywords": ["HIV"],
|
| 207 |
+
"min_relevance": 0.3,
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"query": "Urinary tract infection treatment pyelonephritis uncomplicated cystitis",
|
| 211 |
+
"expected_specialties": ["Infectious Disease"],
|
| 212 |
+
"expected_title_keywords": ["UTI", "urinary tract"],
|
| 213 |
+
"min_relevance": 0.25,
|
| 214 |
+
},
|
| 215 |
+
# OB/GYN
|
| 216 |
+
{
|
| 217 |
+
"query": "Preeclampsia management magnesium sulfate antihypertensives",
|
| 218 |
+
"expected_specialties": ["OB/GYN"],
|
| 219 |
+
"expected_title_keywords": ["preeclampsia", "hypertensive"],
|
| 220 |
+
"min_relevance": 0.3,
|
| 221 |
+
},
|
| 222 |
+
# Rheumatology
|
| 223 |
+
{
|
| 224 |
+
"query": "Acute gout treatment colchicine NSAIDs corticosteroids",
|
| 225 |
+
"expected_specialties": ["Rheumatology"],
|
| 226 |
+
"expected_title_keywords": ["gout"],
|
| 227 |
+
"min_relevance": 0.3,
|
| 228 |
+
},
|
| 229 |
+
]
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
async def rebuild_chroma(persist_dir: str):
|
| 233 |
+
"""Delete and recreate the ChromaDB collection."""
|
| 234 |
+
p = Path(persist_dir)
|
| 235 |
+
if p.exists():
|
| 236 |
+
shutil.rmtree(p)
|
| 237 |
+
print(f" Deleted ChromaDB directory: {p}")
|
| 238 |
+
else:
|
| 239 |
+
print(f" ChromaDB directory does not exist: {p}")
|
| 240 |
+
|
| 241 |
+
# Re-init by creating a new tool instance and triggering load
|
| 242 |
+
from app.tools.guideline_retrieval import GuidelineRetrievalTool
|
| 243 |
+
tool = GuidelineRetrievalTool()
|
| 244 |
+
await tool._ensure_initialized()
|
| 245 |
+
count = tool._collection.count()
|
| 246 |
+
print(f" Rebuilt collection with {count} guidelines")
|
| 247 |
+
return tool
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
async def show_stats(persist_dir: str):
|
| 251 |
+
"""Show ChromaDB collection statistics."""
|
| 252 |
+
from app.tools.guideline_retrieval import GuidelineRetrievalTool
|
| 253 |
+
tool = GuidelineRetrievalTool()
|
| 254 |
+
await tool._ensure_initialized()
|
| 255 |
+
|
| 256 |
+
count = tool._collection.count()
|
| 257 |
+
print(f"\n Collection: clinical_guidelines")
|
| 258 |
+
print(f" Documents: {count}")
|
| 259 |
+
print(f" Persist dir: {persist_dir}")
|
| 260 |
+
|
| 261 |
+
if count > 0:
|
| 262 |
+
# Get all metadata to show specialties
|
| 263 |
+
all_data = tool._collection.get(include=["metadatas"])
|
| 264 |
+
specialties = {}
|
| 265 |
+
for meta in all_data["metadatas"]:
|
| 266 |
+
spec = meta.get("specialty", "Unknown")
|
| 267 |
+
specialties[spec] = specialties.get(spec, 0) + 1
|
| 268 |
+
|
| 269 |
+
print(f"\n Guidelines by specialty:")
|
| 270 |
+
for spec, cnt in sorted(specialties.items()):
|
| 271 |
+
print(f" {spec:30s} {cnt}")
|
| 272 |
+
|
| 273 |
+
return tool
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
async def test_single_query(tool, query_text: str, n_results: int = 5):
|
| 277 |
+
"""Test a single query and display results."""
|
| 278 |
+
result = await tool.run(query_text, n_results=n_results)
|
| 279 |
+
print(f"\n Query: \"{query_text}\"")
|
| 280 |
+
print(f" Results: {len(result.excerpts)}")
|
| 281 |
+
for i, exc in enumerate(result.excerpts):
|
| 282 |
+
print(f"\n [{i+1}] {exc.title}")
|
| 283 |
+
print(f" Source: {exc.source}")
|
| 284 |
+
print(f" Relevance: {exc.relevance_score:.4f}")
|
| 285 |
+
print(f" Excerpt: {exc.excerpt[:150]}...")
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
async def run_quality_tests(tool, test_queries):
|
| 289 |
+
"""Run all quality test queries and score results."""
|
| 290 |
+
results = []
|
| 291 |
+
|
| 292 |
+
for tq in test_queries:
|
| 293 |
+
query = tq["query"]
|
| 294 |
+
expected_specs = tq["expected_specialties"]
|
| 295 |
+
expected_keywords = tq["expected_title_keywords"]
|
| 296 |
+
min_rel = tq["min_relevance"]
|
| 297 |
+
|
| 298 |
+
result = await tool.run(query, n_results=5)
|
| 299 |
+
|
| 300 |
+
# Get top result info
|
| 301 |
+
top_excerpt = result.excerpts[0] if result.excerpts else None
|
| 302 |
+
top_title = top_excerpt.title if top_excerpt else "N/A"
|
| 303 |
+
top_relevance = top_excerpt.relevance_score if top_excerpt else 0
|
| 304 |
+
top_source = top_excerpt.source if top_excerpt else "N/A"
|
| 305 |
+
|
| 306 |
+
# Check if any of the top-3 results match expected specialty
|
| 307 |
+
specialty_match = False
|
| 308 |
+
keyword_match = False
|
| 309 |
+
matched_result_idx = -1
|
| 310 |
+
|
| 311 |
+
for idx, exc in enumerate(result.excerpts[:3]):
|
| 312 |
+
# Check source text or title for specialty/keyword matches
|
| 313 |
+
title_lower = exc.title.lower()
|
| 314 |
+
source_lower = exc.source.lower()
|
| 315 |
+
combined = title_lower + " " + source_lower + " " + exc.excerpt.lower()
|
| 316 |
+
|
| 317 |
+
for kw in expected_keywords:
|
| 318 |
+
if kw.lower() in combined:
|
| 319 |
+
keyword_match = True
|
| 320 |
+
if matched_result_idx == -1:
|
| 321 |
+
matched_result_idx = idx
|
| 322 |
+
break
|
| 323 |
+
|
| 324 |
+
# Relevance check
|
| 325 |
+
relevance_ok = top_relevance >= min_rel
|
| 326 |
+
|
| 327 |
+
# Overall pass: keyword match in top-3 AND minimum relevance
|
| 328 |
+
passed = keyword_match and relevance_ok
|
| 329 |
+
|
| 330 |
+
test_result = {
|
| 331 |
+
"query": query[:60] + ("..." if len(query) > 60 else ""),
|
| 332 |
+
"expected_specialties": expected_specs,
|
| 333 |
+
"expected_keywords": expected_keywords,
|
| 334 |
+
"top_title": top_title,
|
| 335 |
+
"top_relevance": top_relevance,
|
| 336 |
+
"keyword_match": keyword_match,
|
| 337 |
+
"keyword_match_position": matched_result_idx + 1 if matched_result_idx >= 0 else 0,
|
| 338 |
+
"relevance_ok": relevance_ok,
|
| 339 |
+
"passed": passed,
|
| 340 |
+
"all_titles": [e.title for e in result.excerpts[:5]],
|
| 341 |
+
"all_relevances": [e.relevance_score for e in result.excerpts[:5]],
|
| 342 |
+
}
|
| 343 |
+
results.append(test_result)
|
| 344 |
+
|
| 345 |
+
return results
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
async def main():
|
| 349 |
+
parser = argparse.ArgumentParser(description="RAG Retrieval Quality Test")
|
| 350 |
+
parser.add_argument("--rebuild", action="store_true", help="Rebuild ChromaDB from scratch")
|
| 351 |
+
parser.add_argument("--stats", action="store_true", help="Show collection statistics only")
|
| 352 |
+
parser.add_argument("--query", help="Test a single query")
|
| 353 |
+
parser.add_argument("--verbose", action="store_true", help="Show detailed results for each query")
|
| 354 |
+
args = parser.parse_args()
|
| 355 |
+
|
| 356 |
+
from app.config import settings
|
| 357 |
+
persist_dir = settings.chroma_persist_dir
|
| 358 |
+
|
| 359 |
+
print(f"\n{'='*70}")
|
| 360 |
+
print(f" RAG Retrieval Quality Test")
|
| 361 |
+
print(f" Persist dir: {persist_dir}")
|
| 362 |
+
print(f" Embedding model: {settings.embedding_model}")
|
| 363 |
+
print(f"{'='*70}")
|
| 364 |
+
|
| 365 |
+
if args.rebuild:
|
| 366 |
+
tool = await rebuild_chroma(persist_dir)
|
| 367 |
+
elif args.stats:
|
| 368 |
+
await show_stats(persist_dir)
|
| 369 |
+
return
|
| 370 |
+
else:
|
| 371 |
+
from app.tools.guideline_retrieval import GuidelineRetrievalTool
|
| 372 |
+
tool = GuidelineRetrievalTool()
|
| 373 |
+
await tool._ensure_initialized()
|
| 374 |
+
count = tool._collection.count()
|
| 375 |
+
print(f"\n Collection has {count} documents")
|
| 376 |
+
if count == 0:
|
| 377 |
+
print(" ⚠ Collection is empty! Run with --rebuild to load guidelines.")
|
| 378 |
+
return
|
| 379 |
+
|
| 380 |
+
if args.query:
|
| 381 |
+
await test_single_query(tool, args.query)
|
| 382 |
+
return
|
| 383 |
+
|
| 384 |
+
# Run all quality tests
|
| 385 |
+
print(f"\n Running {len(RAG_TEST_QUERIES)} retrieval quality tests...\n")
|
| 386 |
+
results = await run_quality_tests(tool, RAG_TEST_QUERIES)
|
| 387 |
+
|
| 388 |
+
# Display results
|
| 389 |
+
passed_count = 0
|
| 390 |
+
for r in results:
|
| 391 |
+
icon = "✓" if r["passed"] else "✗"
|
| 392 |
+
pos = f"@{r['keyword_match_position']}" if r["keyword_match"] else " "
|
| 393 |
+
rel = f"{r['top_relevance']:.3f}"
|
| 394 |
+
print(f" {icon} [{rel}] {pos:>3} {r['query']}")
|
| 395 |
+
if not r["passed"] or args.verbose:
|
| 396 |
+
print(f" → Top: {r['top_title']}")
|
| 397 |
+
if not r["keyword_match"]:
|
| 398 |
+
print(f" ✗ Expected keywords not found in top-3: {r['expected_keywords']}")
|
| 399 |
+
if not r["relevance_ok"]:
|
| 400 |
+
print(f" ✗ Relevance {r['top_relevance']:.3f} below threshold")
|
| 401 |
+
if args.verbose:
|
| 402 |
+
for i, (t, s) in enumerate(zip(r["all_titles"], r["all_relevances"])):
|
| 403 |
+
print(f" {i+1}. [{s:.3f}] {t}")
|
| 404 |
+
if r["passed"]:
|
| 405 |
+
passed_count += 1
|
| 406 |
+
|
| 407 |
+
# Summary
|
| 408 |
+
total = len(results)
|
| 409 |
+
pct = (passed_count / total * 100) if total else 0
|
| 410 |
+
print(f"\n{'='*70}")
|
| 411 |
+
print(f" RESULTS: {passed_count}/{total} passed ({pct:.0f}%)")
|
| 412 |
+
print(f"{'='*70}")
|
| 413 |
+
|
| 414 |
+
# By-specialty breakdown
|
| 415 |
+
spec_results = {}
|
| 416 |
+
for r in results:
|
| 417 |
+
for spec in r.get("expected_specialties", ["Unknown"]):
|
| 418 |
+
if spec not in spec_results:
|
| 419 |
+
spec_results[spec] = {"passed": 0, "total": 0}
|
| 420 |
+
spec_results[spec]["total"] += 1
|
| 421 |
+
if r["passed"]:
|
| 422 |
+
spec_results[spec]["passed"] += 1
|
| 423 |
+
|
| 424 |
+
print(f"\n By specialty:")
|
| 425 |
+
for spec, counts in sorted(spec_results.items()):
|
| 426 |
+
p = counts["passed"]
|
| 427 |
+
t = counts["total"]
|
| 428 |
+
bar = "█" * p + "░" * (t - p)
|
| 429 |
+
print(f" {spec:25s} {p}/{t} {bar}")
|
| 430 |
+
|
| 431 |
+
# Relevance distribution
|
| 432 |
+
all_rels = [r["top_relevance"] for r in results]
|
| 433 |
+
if all_rels:
|
| 434 |
+
avg_rel = sum(all_rels) / len(all_rels)
|
| 435 |
+
min_rel_val = min(all_rels)
|
| 436 |
+
max_rel_val = max(all_rels)
|
| 437 |
+
print(f"\n Relevance: avg={avg_rel:.3f} min={min_rel_val:.3f} max={max_rel_val:.3f}")
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
if __name__ == "__main__":
|
| 441 |
+
asyncio.run(main())
|
src/frontend/next-env.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="next" />
|
| 2 |
+
/// <reference types="next/image-types/global" />
|
| 3 |
+
|
| 4 |
+
// NOTE: This file should not be edited
|
| 5 |
+
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
src/frontend/next.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
async rewrites() {
|
| 4 |
+
return [
|
| 5 |
+
{
|
| 6 |
+
source: "/api/:path*",
|
| 7 |
+
destination: "http://localhost:8002/api/:path*",
|
| 8 |
+
},
|
| 9 |
+
];
|
| 10 |
+
},
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
module.exports = nextConfig;
|
src/frontend/package-lock.json
ADDED
|
@@ -0,0 +1,1648 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cds-agent-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "cds-agent-frontend",
|
| 9 |
+
"version": "0.1.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"clsx": "^2.0.0",
|
| 12 |
+
"lucide-react": "^0.300.0",
|
| 13 |
+
"next": "^14.0.0",
|
| 14 |
+
"react": "^18.2.0",
|
| 15 |
+
"react-dom": "^18.2.0",
|
| 16 |
+
"tailwind-merge": "^2.0.0"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@types/node": "^20.10.0",
|
| 20 |
+
"@types/react": "^18.2.0",
|
| 21 |
+
"@types/react-dom": "^18.2.0",
|
| 22 |
+
"autoprefixer": "^10.4.16",
|
| 23 |
+
"postcss": "^8.4.31",
|
| 24 |
+
"tailwindcss": "^3.3.0",
|
| 25 |
+
"typescript": "^5.3.0"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"node_modules/@alloc/quick-lru": {
|
| 29 |
+
"version": "5.2.0",
|
| 30 |
+
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
| 31 |
+
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
| 32 |
+
"dev": true,
|
| 33 |
+
"license": "MIT",
|
| 34 |
+
"engines": {
|
| 35 |
+
"node": ">=10"
|
| 36 |
+
},
|
| 37 |
+
"funding": {
|
| 38 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 42 |
+
"version": "0.3.13",
|
| 43 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 44 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 45 |
+
"dev": true,
|
| 46 |
+
"license": "MIT",
|
| 47 |
+
"dependencies": {
|
| 48 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 49 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 53 |
+
"version": "3.1.2",
|
| 54 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 55 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 56 |
+
"dev": true,
|
| 57 |
+
"license": "MIT",
|
| 58 |
+
"engines": {
|
| 59 |
+
"node": ">=6.0.0"
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 63 |
+
"version": "1.5.5",
|
| 64 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 65 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 66 |
+
"dev": true,
|
| 67 |
+
"license": "MIT"
|
| 68 |
+
},
|
| 69 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 70 |
+
"version": "0.3.31",
|
| 71 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 72 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 73 |
+
"dev": true,
|
| 74 |
+
"license": "MIT",
|
| 75 |
+
"dependencies": {
|
| 76 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 77 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"node_modules/@next/env": {
|
| 81 |
+
"version": "14.2.35",
|
| 82 |
+
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
|
| 83 |
+
"integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
|
| 84 |
+
"license": "MIT"
|
| 85 |
+
},
|
| 86 |
+
"node_modules/@next/swc-darwin-arm64": {
|
| 87 |
+
"version": "14.2.33",
|
| 88 |
+
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
|
| 89 |
+
"integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
|
| 90 |
+
"cpu": [
|
| 91 |
+
"arm64"
|
| 92 |
+
],
|
| 93 |
+
"license": "MIT",
|
| 94 |
+
"optional": true,
|
| 95 |
+
"os": [
|
| 96 |
+
"darwin"
|
| 97 |
+
],
|
| 98 |
+
"engines": {
|
| 99 |
+
"node": ">= 10"
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"node_modules/@next/swc-darwin-x64": {
|
| 103 |
+
"version": "14.2.33",
|
| 104 |
+
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
|
| 105 |
+
"integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
|
| 106 |
+
"cpu": [
|
| 107 |
+
"x64"
|
| 108 |
+
],
|
| 109 |
+
"license": "MIT",
|
| 110 |
+
"optional": true,
|
| 111 |
+
"os": [
|
| 112 |
+
"darwin"
|
| 113 |
+
],
|
| 114 |
+
"engines": {
|
| 115 |
+
"node": ">= 10"
|
| 116 |
+
}
|
| 117 |
+
},
|
| 118 |
+
"node_modules/@next/swc-linux-arm64-gnu": {
|
| 119 |
+
"version": "14.2.33",
|
| 120 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
|
| 121 |
+
"integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
|
| 122 |
+
"cpu": [
|
| 123 |
+
"arm64"
|
| 124 |
+
],
|
| 125 |
+
"license": "MIT",
|
| 126 |
+
"optional": true,
|
| 127 |
+
"os": [
|
| 128 |
+
"linux"
|
| 129 |
+
],
|
| 130 |
+
"engines": {
|
| 131 |
+
"node": ">= 10"
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
"node_modules/@next/swc-linux-arm64-musl": {
|
| 135 |
+
"version": "14.2.33",
|
| 136 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
|
| 137 |
+
"integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
|
| 138 |
+
"cpu": [
|
| 139 |
+
"arm64"
|
| 140 |
+
],
|
| 141 |
+
"license": "MIT",
|
| 142 |
+
"optional": true,
|
| 143 |
+
"os": [
|
| 144 |
+
"linux"
|
| 145 |
+
],
|
| 146 |
+
"engines": {
|
| 147 |
+
"node": ">= 10"
|
| 148 |
+
}
|
| 149 |
+
},
|
| 150 |
+
"node_modules/@next/swc-linux-x64-gnu": {
|
| 151 |
+
"version": "14.2.33",
|
| 152 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
|
| 153 |
+
"integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
|
| 154 |
+
"cpu": [
|
| 155 |
+
"x64"
|
| 156 |
+
],
|
| 157 |
+
"license": "MIT",
|
| 158 |
+
"optional": true,
|
| 159 |
+
"os": [
|
| 160 |
+
"linux"
|
| 161 |
+
],
|
| 162 |
+
"engines": {
|
| 163 |
+
"node": ">= 10"
|
| 164 |
+
}
|
| 165 |
+
},
|
| 166 |
+
"node_modules/@next/swc-linux-x64-musl": {
|
| 167 |
+
"version": "14.2.33",
|
| 168 |
+
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
|
| 169 |
+
"integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
|
| 170 |
+
"cpu": [
|
| 171 |
+
"x64"
|
| 172 |
+
],
|
| 173 |
+
"license": "MIT",
|
| 174 |
+
"optional": true,
|
| 175 |
+
"os": [
|
| 176 |
+
"linux"
|
| 177 |
+
],
|
| 178 |
+
"engines": {
|
| 179 |
+
"node": ">= 10"
|
| 180 |
+
}
|
| 181 |
+
},
|
| 182 |
+
"node_modules/@next/swc-win32-arm64-msvc": {
|
| 183 |
+
"version": "14.2.33",
|
| 184 |
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
|
| 185 |
+
"integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
|
| 186 |
+
"cpu": [
|
| 187 |
+
"arm64"
|
| 188 |
+
],
|
| 189 |
+
"license": "MIT",
|
| 190 |
+
"optional": true,
|
| 191 |
+
"os": [
|
| 192 |
+
"win32"
|
| 193 |
+
],
|
| 194 |
+
"engines": {
|
| 195 |
+
"node": ">= 10"
|
| 196 |
+
}
|
| 197 |
+
},
|
| 198 |
+
"node_modules/@next/swc-win32-ia32-msvc": {
|
| 199 |
+
"version": "14.2.33",
|
| 200 |
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
|
| 201 |
+
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
|
| 202 |
+
"cpu": [
|
| 203 |
+
"ia32"
|
| 204 |
+
],
|
| 205 |
+
"license": "MIT",
|
| 206 |
+
"optional": true,
|
| 207 |
+
"os": [
|
| 208 |
+
"win32"
|
| 209 |
+
],
|
| 210 |
+
"engines": {
|
| 211 |
+
"node": ">= 10"
|
| 212 |
+
}
|
| 213 |
+
},
|
| 214 |
+
"node_modules/@next/swc-win32-x64-msvc": {
|
| 215 |
+
"version": "14.2.33",
|
| 216 |
+
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
|
| 217 |
+
"integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
|
| 218 |
+
"cpu": [
|
| 219 |
+
"x64"
|
| 220 |
+
],
|
| 221 |
+
"license": "MIT",
|
| 222 |
+
"optional": true,
|
| 223 |
+
"os": [
|
| 224 |
+
"win32"
|
| 225 |
+
],
|
| 226 |
+
"engines": {
|
| 227 |
+
"node": ">= 10"
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
"node_modules/@nodelib/fs.scandir": {
|
| 231 |
+
"version": "2.1.5",
|
| 232 |
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
| 233 |
+
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
| 234 |
+
"dev": true,
|
| 235 |
+
"license": "MIT",
|
| 236 |
+
"dependencies": {
|
| 237 |
+
"@nodelib/fs.stat": "2.0.5",
|
| 238 |
+
"run-parallel": "^1.1.9"
|
| 239 |
+
},
|
| 240 |
+
"engines": {
|
| 241 |
+
"node": ">= 8"
|
| 242 |
+
}
|
| 243 |
+
},
|
| 244 |
+
"node_modules/@nodelib/fs.stat": {
|
| 245 |
+
"version": "2.0.5",
|
| 246 |
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
| 247 |
+
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
| 248 |
+
"dev": true,
|
| 249 |
+
"license": "MIT",
|
| 250 |
+
"engines": {
|
| 251 |
+
"node": ">= 8"
|
| 252 |
+
}
|
| 253 |
+
},
|
| 254 |
+
"node_modules/@nodelib/fs.walk": {
|
| 255 |
+
"version": "1.2.8",
|
| 256 |
+
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
| 257 |
+
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
| 258 |
+
"dev": true,
|
| 259 |
+
"license": "MIT",
|
| 260 |
+
"dependencies": {
|
| 261 |
+
"@nodelib/fs.scandir": "2.1.5",
|
| 262 |
+
"fastq": "^1.6.0"
|
| 263 |
+
},
|
| 264 |
+
"engines": {
|
| 265 |
+
"node": ">= 8"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"node_modules/@swc/counter": {
|
| 269 |
+
"version": "0.1.3",
|
| 270 |
+
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
| 271 |
+
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
| 272 |
+
"license": "Apache-2.0"
|
| 273 |
+
},
|
| 274 |
+
"node_modules/@swc/helpers": {
|
| 275 |
+
"version": "0.5.5",
|
| 276 |
+
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
|
| 277 |
+
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
|
| 278 |
+
"license": "Apache-2.0",
|
| 279 |
+
"dependencies": {
|
| 280 |
+
"@swc/counter": "^0.1.3",
|
| 281 |
+
"tslib": "^2.4.0"
|
| 282 |
+
}
|
| 283 |
+
},
|
| 284 |
+
"node_modules/@types/node": {
|
| 285 |
+
"version": "20.19.33",
|
| 286 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
|
| 287 |
+
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
|
| 288 |
+
"dev": true,
|
| 289 |
+
"license": "MIT",
|
| 290 |
+
"dependencies": {
|
| 291 |
+
"undici-types": "~6.21.0"
|
| 292 |
+
}
|
| 293 |
+
},
|
| 294 |
+
"node_modules/@types/prop-types": {
|
| 295 |
+
"version": "15.7.15",
|
| 296 |
+
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
| 297 |
+
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
| 298 |
+
"dev": true,
|
| 299 |
+
"license": "MIT"
|
| 300 |
+
},
|
| 301 |
+
"node_modules/@types/react": {
|
| 302 |
+
"version": "18.3.28",
|
| 303 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
| 304 |
+
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
| 305 |
+
"dev": true,
|
| 306 |
+
"license": "MIT",
|
| 307 |
+
"dependencies": {
|
| 308 |
+
"@types/prop-types": "*",
|
| 309 |
+
"csstype": "^3.2.2"
|
| 310 |
+
}
|
| 311 |
+
},
|
| 312 |
+
"node_modules/@types/react-dom": {
|
| 313 |
+
"version": "18.3.7",
|
| 314 |
+
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
| 315 |
+
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
| 316 |
+
"dev": true,
|
| 317 |
+
"license": "MIT",
|
| 318 |
+
"peerDependencies": {
|
| 319 |
+
"@types/react": "^18.0.0"
|
| 320 |
+
}
|
| 321 |
+
},
|
| 322 |
+
"node_modules/any-promise": {
|
| 323 |
+
"version": "1.3.0",
|
| 324 |
+
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
| 325 |
+
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
| 326 |
+
"dev": true,
|
| 327 |
+
"license": "MIT"
|
| 328 |
+
},
|
| 329 |
+
"node_modules/anymatch": {
|
| 330 |
+
"version": "3.1.3",
|
| 331 |
+
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
| 332 |
+
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
| 333 |
+
"dev": true,
|
| 334 |
+
"license": "ISC",
|
| 335 |
+
"dependencies": {
|
| 336 |
+
"normalize-path": "^3.0.0",
|
| 337 |
+
"picomatch": "^2.0.4"
|
| 338 |
+
},
|
| 339 |
+
"engines": {
|
| 340 |
+
"node": ">= 8"
|
| 341 |
+
}
|
| 342 |
+
},
|
| 343 |
+
"node_modules/arg": {
|
| 344 |
+
"version": "5.0.2",
|
| 345 |
+
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
| 346 |
+
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
| 347 |
+
"dev": true,
|
| 348 |
+
"license": "MIT"
|
| 349 |
+
},
|
| 350 |
+
"node_modules/autoprefixer": {
|
| 351 |
+
"version": "10.4.24",
|
| 352 |
+
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
|
| 353 |
+
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
|
| 354 |
+
"dev": true,
|
| 355 |
+
"funding": [
|
| 356 |
+
{
|
| 357 |
+
"type": "opencollective",
|
| 358 |
+
"url": "https://opencollective.com/postcss/"
|
| 359 |
+
},
|
| 360 |
+
{
|
| 361 |
+
"type": "tidelift",
|
| 362 |
+
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
| 363 |
+
},
|
| 364 |
+
{
|
| 365 |
+
"type": "github",
|
| 366 |
+
"url": "https://github.com/sponsors/ai"
|
| 367 |
+
}
|
| 368 |
+
],
|
| 369 |
+
"license": "MIT",
|
| 370 |
+
"dependencies": {
|
| 371 |
+
"browserslist": "^4.28.1",
|
| 372 |
+
"caniuse-lite": "^1.0.30001766",
|
| 373 |
+
"fraction.js": "^5.3.4",
|
| 374 |
+
"picocolors": "^1.1.1",
|
| 375 |
+
"postcss-value-parser": "^4.2.0"
|
| 376 |
+
},
|
| 377 |
+
"bin": {
|
| 378 |
+
"autoprefixer": "bin/autoprefixer"
|
| 379 |
+
},
|
| 380 |
+
"engines": {
|
| 381 |
+
"node": "^10 || ^12 || >=14"
|
| 382 |
+
},
|
| 383 |
+
"peerDependencies": {
|
| 384 |
+
"postcss": "^8.1.0"
|
| 385 |
+
}
|
| 386 |
+
},
|
| 387 |
+
"node_modules/baseline-browser-mapping": {
|
| 388 |
+
"version": "2.9.19",
|
| 389 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
| 390 |
+
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
| 391 |
+
"dev": true,
|
| 392 |
+
"license": "Apache-2.0",
|
| 393 |
+
"bin": {
|
| 394 |
+
"baseline-browser-mapping": "dist/cli.js"
|
| 395 |
+
}
|
| 396 |
+
},
|
| 397 |
+
"node_modules/binary-extensions": {
|
| 398 |
+
"version": "2.3.0",
|
| 399 |
+
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
| 400 |
+
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
| 401 |
+
"dev": true,
|
| 402 |
+
"license": "MIT",
|
| 403 |
+
"engines": {
|
| 404 |
+
"node": ">=8"
|
| 405 |
+
},
|
| 406 |
+
"funding": {
|
| 407 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 408 |
+
}
|
| 409 |
+
},
|
| 410 |
+
"node_modules/braces": {
|
| 411 |
+
"version": "3.0.3",
|
| 412 |
+
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
| 413 |
+
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
| 414 |
+
"dev": true,
|
| 415 |
+
"license": "MIT",
|
| 416 |
+
"dependencies": {
|
| 417 |
+
"fill-range": "^7.1.1"
|
| 418 |
+
},
|
| 419 |
+
"engines": {
|
| 420 |
+
"node": ">=8"
|
| 421 |
+
}
|
| 422 |
+
},
|
| 423 |
+
"node_modules/browserslist": {
|
| 424 |
+
"version": "4.28.1",
|
| 425 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
| 426 |
+
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
| 427 |
+
"dev": true,
|
| 428 |
+
"funding": [
|
| 429 |
+
{
|
| 430 |
+
"type": "opencollective",
|
| 431 |
+
"url": "https://opencollective.com/browserslist"
|
| 432 |
+
},
|
| 433 |
+
{
|
| 434 |
+
"type": "tidelift",
|
| 435 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 436 |
+
},
|
| 437 |
+
{
|
| 438 |
+
"type": "github",
|
| 439 |
+
"url": "https://github.com/sponsors/ai"
|
| 440 |
+
}
|
| 441 |
+
],
|
| 442 |
+
"license": "MIT",
|
| 443 |
+
"dependencies": {
|
| 444 |
+
"baseline-browser-mapping": "^2.9.0",
|
| 445 |
+
"caniuse-lite": "^1.0.30001759",
|
| 446 |
+
"electron-to-chromium": "^1.5.263",
|
| 447 |
+
"node-releases": "^2.0.27",
|
| 448 |
+
"update-browserslist-db": "^1.2.0"
|
| 449 |
+
},
|
| 450 |
+
"bin": {
|
| 451 |
+
"browserslist": "cli.js"
|
| 452 |
+
},
|
| 453 |
+
"engines": {
|
| 454 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 455 |
+
}
|
| 456 |
+
},
|
| 457 |
+
"node_modules/busboy": {
|
| 458 |
+
"version": "1.6.0",
|
| 459 |
+
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
| 460 |
+
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
| 461 |
+
"dependencies": {
|
| 462 |
+
"streamsearch": "^1.1.0"
|
| 463 |
+
},
|
| 464 |
+
"engines": {
|
| 465 |
+
"node": ">=10.16.0"
|
| 466 |
+
}
|
| 467 |
+
},
|
| 468 |
+
"node_modules/camelcase-css": {
|
| 469 |
+
"version": "2.0.1",
|
| 470 |
+
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
| 471 |
+
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
| 472 |
+
"dev": true,
|
| 473 |
+
"license": "MIT",
|
| 474 |
+
"engines": {
|
| 475 |
+
"node": ">= 6"
|
| 476 |
+
}
|
| 477 |
+
},
|
| 478 |
+
"node_modules/caniuse-lite": {
|
| 479 |
+
"version": "1.0.30001769",
|
| 480 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
|
| 481 |
+
"integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
|
| 482 |
+
"funding": [
|
| 483 |
+
{
|
| 484 |
+
"type": "opencollective",
|
| 485 |
+
"url": "https://opencollective.com/browserslist"
|
| 486 |
+
},
|
| 487 |
+
{
|
| 488 |
+
"type": "tidelift",
|
| 489 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 490 |
+
},
|
| 491 |
+
{
|
| 492 |
+
"type": "github",
|
| 493 |
+
"url": "https://github.com/sponsors/ai"
|
| 494 |
+
}
|
| 495 |
+
],
|
| 496 |
+
"license": "CC-BY-4.0"
|
| 497 |
+
},
|
| 498 |
+
"node_modules/chokidar": {
|
| 499 |
+
"version": "3.6.0",
|
| 500 |
+
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
| 501 |
+
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
| 502 |
+
"dev": true,
|
| 503 |
+
"license": "MIT",
|
| 504 |
+
"dependencies": {
|
| 505 |
+
"anymatch": "~3.1.2",
|
| 506 |
+
"braces": "~3.0.2",
|
| 507 |
+
"glob-parent": "~5.1.2",
|
| 508 |
+
"is-binary-path": "~2.1.0",
|
| 509 |
+
"is-glob": "~4.0.1",
|
| 510 |
+
"normalize-path": "~3.0.0",
|
| 511 |
+
"readdirp": "~3.6.0"
|
| 512 |
+
},
|
| 513 |
+
"engines": {
|
| 514 |
+
"node": ">= 8.10.0"
|
| 515 |
+
},
|
| 516 |
+
"funding": {
|
| 517 |
+
"url": "https://paulmillr.com/funding/"
|
| 518 |
+
},
|
| 519 |
+
"optionalDependencies": {
|
| 520 |
+
"fsevents": "~2.3.2"
|
| 521 |
+
}
|
| 522 |
+
},
|
| 523 |
+
"node_modules/chokidar/node_modules/glob-parent": {
|
| 524 |
+
"version": "5.1.2",
|
| 525 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
| 526 |
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
| 527 |
+
"dev": true,
|
| 528 |
+
"license": "ISC",
|
| 529 |
+
"dependencies": {
|
| 530 |
+
"is-glob": "^4.0.1"
|
| 531 |
+
},
|
| 532 |
+
"engines": {
|
| 533 |
+
"node": ">= 6"
|
| 534 |
+
}
|
| 535 |
+
},
|
| 536 |
+
"node_modules/client-only": {
|
| 537 |
+
"version": "0.0.1",
|
| 538 |
+
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
| 539 |
+
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
| 540 |
+
"license": "MIT"
|
| 541 |
+
},
|
| 542 |
+
"node_modules/clsx": {
|
| 543 |
+
"version": "2.1.1",
|
| 544 |
+
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
| 545 |
+
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
| 546 |
+
"license": "MIT",
|
| 547 |
+
"engines": {
|
| 548 |
+
"node": ">=6"
|
| 549 |
+
}
|
| 550 |
+
},
|
| 551 |
+
"node_modules/commander": {
|
| 552 |
+
"version": "4.1.1",
|
| 553 |
+
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
| 554 |
+
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
| 555 |
+
"dev": true,
|
| 556 |
+
"license": "MIT",
|
| 557 |
+
"engines": {
|
| 558 |
+
"node": ">= 6"
|
| 559 |
+
}
|
| 560 |
+
},
|
| 561 |
+
"node_modules/cssesc": {
|
| 562 |
+
"version": "3.0.0",
|
| 563 |
+
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
| 564 |
+
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
| 565 |
+
"dev": true,
|
| 566 |
+
"license": "MIT",
|
| 567 |
+
"bin": {
|
| 568 |
+
"cssesc": "bin/cssesc"
|
| 569 |
+
},
|
| 570 |
+
"engines": {
|
| 571 |
+
"node": ">=4"
|
| 572 |
+
}
|
| 573 |
+
},
|
| 574 |
+
"node_modules/csstype": {
|
| 575 |
+
"version": "3.2.3",
|
| 576 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 577 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 578 |
+
"dev": true,
|
| 579 |
+
"license": "MIT"
|
| 580 |
+
},
|
| 581 |
+
"node_modules/didyoumean": {
|
| 582 |
+
"version": "1.2.2",
|
| 583 |
+
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
| 584 |
+
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
| 585 |
+
"dev": true,
|
| 586 |
+
"license": "Apache-2.0"
|
| 587 |
+
},
|
| 588 |
+
"node_modules/dlv": {
|
| 589 |
+
"version": "1.1.3",
|
| 590 |
+
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
| 591 |
+
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
| 592 |
+
"dev": true,
|
| 593 |
+
"license": "MIT"
|
| 594 |
+
},
|
| 595 |
+
"node_modules/electron-to-chromium": {
|
| 596 |
+
"version": "1.5.286",
|
| 597 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
| 598 |
+
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
|
| 599 |
+
"dev": true,
|
| 600 |
+
"license": "ISC"
|
| 601 |
+
},
|
| 602 |
+
"node_modules/escalade": {
|
| 603 |
+
"version": "3.2.0",
|
| 604 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 605 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 606 |
+
"dev": true,
|
| 607 |
+
"license": "MIT",
|
| 608 |
+
"engines": {
|
| 609 |
+
"node": ">=6"
|
| 610 |
+
}
|
| 611 |
+
},
|
| 612 |
+
"node_modules/fast-glob": {
|
| 613 |
+
"version": "3.3.3",
|
| 614 |
+
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
| 615 |
+
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
| 616 |
+
"dev": true,
|
| 617 |
+
"license": "MIT",
|
| 618 |
+
"dependencies": {
|
| 619 |
+
"@nodelib/fs.stat": "^2.0.2",
|
| 620 |
+
"@nodelib/fs.walk": "^1.2.3",
|
| 621 |
+
"glob-parent": "^5.1.2",
|
| 622 |
+
"merge2": "^1.3.0",
|
| 623 |
+
"micromatch": "^4.0.8"
|
| 624 |
+
},
|
| 625 |
+
"engines": {
|
| 626 |
+
"node": ">=8.6.0"
|
| 627 |
+
}
|
| 628 |
+
},
|
| 629 |
+
"node_modules/fast-glob/node_modules/glob-parent": {
|
| 630 |
+
"version": "5.1.2",
|
| 631 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
| 632 |
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
| 633 |
+
"dev": true,
|
| 634 |
+
"license": "ISC",
|
| 635 |
+
"dependencies": {
|
| 636 |
+
"is-glob": "^4.0.1"
|
| 637 |
+
},
|
| 638 |
+
"engines": {
|
| 639 |
+
"node": ">= 6"
|
| 640 |
+
}
|
| 641 |
+
},
|
| 642 |
+
"node_modules/fastq": {
|
| 643 |
+
"version": "1.20.1",
|
| 644 |
+
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
| 645 |
+
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
| 646 |
+
"dev": true,
|
| 647 |
+
"license": "ISC",
|
| 648 |
+
"dependencies": {
|
| 649 |
+
"reusify": "^1.0.4"
|
| 650 |
+
}
|
| 651 |
+
},
|
| 652 |
+
"node_modules/fill-range": {
|
| 653 |
+
"version": "7.1.1",
|
| 654 |
+
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
| 655 |
+
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
| 656 |
+
"dev": true,
|
| 657 |
+
"license": "MIT",
|
| 658 |
+
"dependencies": {
|
| 659 |
+
"to-regex-range": "^5.0.1"
|
| 660 |
+
},
|
| 661 |
+
"engines": {
|
| 662 |
+
"node": ">=8"
|
| 663 |
+
}
|
| 664 |
+
},
|
| 665 |
+
"node_modules/fraction.js": {
|
| 666 |
+
"version": "5.3.4",
|
| 667 |
+
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
| 668 |
+
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
| 669 |
+
"dev": true,
|
| 670 |
+
"license": "MIT",
|
| 671 |
+
"engines": {
|
| 672 |
+
"node": "*"
|
| 673 |
+
},
|
| 674 |
+
"funding": {
|
| 675 |
+
"type": "github",
|
| 676 |
+
"url": "https://github.com/sponsors/rawify"
|
| 677 |
+
}
|
| 678 |
+
},
|
| 679 |
+
"node_modules/fsevents": {
|
| 680 |
+
"version": "2.3.3",
|
| 681 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 682 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 683 |
+
"dev": true,
|
| 684 |
+
"hasInstallScript": true,
|
| 685 |
+
"license": "MIT",
|
| 686 |
+
"optional": true,
|
| 687 |
+
"os": [
|
| 688 |
+
"darwin"
|
| 689 |
+
],
|
| 690 |
+
"engines": {
|
| 691 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 692 |
+
}
|
| 693 |
+
},
|
| 694 |
+
"node_modules/function-bind": {
|
| 695 |
+
"version": "1.1.2",
|
| 696 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 697 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 698 |
+
"dev": true,
|
| 699 |
+
"license": "MIT",
|
| 700 |
+
"funding": {
|
| 701 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 702 |
+
}
|
| 703 |
+
},
|
| 704 |
+
"node_modules/glob-parent": {
|
| 705 |
+
"version": "6.0.2",
|
| 706 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
| 707 |
+
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
| 708 |
+
"dev": true,
|
| 709 |
+
"license": "ISC",
|
| 710 |
+
"dependencies": {
|
| 711 |
+
"is-glob": "^4.0.3"
|
| 712 |
+
},
|
| 713 |
+
"engines": {
|
| 714 |
+
"node": ">=10.13.0"
|
| 715 |
+
}
|
| 716 |
+
},
|
| 717 |
+
"node_modules/graceful-fs": {
|
| 718 |
+
"version": "4.2.11",
|
| 719 |
+
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
| 720 |
+
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
| 721 |
+
"license": "ISC"
|
| 722 |
+
},
|
| 723 |
+
"node_modules/hasown": {
|
| 724 |
+
"version": "2.0.2",
|
| 725 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 726 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 727 |
+
"dev": true,
|
| 728 |
+
"license": "MIT",
|
| 729 |
+
"dependencies": {
|
| 730 |
+
"function-bind": "^1.1.2"
|
| 731 |
+
},
|
| 732 |
+
"engines": {
|
| 733 |
+
"node": ">= 0.4"
|
| 734 |
+
}
|
| 735 |
+
},
|
| 736 |
+
"node_modules/is-binary-path": {
|
| 737 |
+
"version": "2.1.0",
|
| 738 |
+
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
| 739 |
+
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
| 740 |
+
"dev": true,
|
| 741 |
+
"license": "MIT",
|
| 742 |
+
"dependencies": {
|
| 743 |
+
"binary-extensions": "^2.0.0"
|
| 744 |
+
},
|
| 745 |
+
"engines": {
|
| 746 |
+
"node": ">=8"
|
| 747 |
+
}
|
| 748 |
+
},
|
| 749 |
+
"node_modules/is-core-module": {
|
| 750 |
+
"version": "2.16.1",
|
| 751 |
+
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
| 752 |
+
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
| 753 |
+
"dev": true,
|
| 754 |
+
"license": "MIT",
|
| 755 |
+
"dependencies": {
|
| 756 |
+
"hasown": "^2.0.2"
|
| 757 |
+
},
|
| 758 |
+
"engines": {
|
| 759 |
+
"node": ">= 0.4"
|
| 760 |
+
},
|
| 761 |
+
"funding": {
|
| 762 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 763 |
+
}
|
| 764 |
+
},
|
| 765 |
+
"node_modules/is-extglob": {
|
| 766 |
+
"version": "2.1.1",
|
| 767 |
+
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
| 768 |
+
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
| 769 |
+
"dev": true,
|
| 770 |
+
"license": "MIT",
|
| 771 |
+
"engines": {
|
| 772 |
+
"node": ">=0.10.0"
|
| 773 |
+
}
|
| 774 |
+
},
|
| 775 |
+
"node_modules/is-glob": {
|
| 776 |
+
"version": "4.0.3",
|
| 777 |
+
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
| 778 |
+
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
| 779 |
+
"dev": true,
|
| 780 |
+
"license": "MIT",
|
| 781 |
+
"dependencies": {
|
| 782 |
+
"is-extglob": "^2.1.1"
|
| 783 |
+
},
|
| 784 |
+
"engines": {
|
| 785 |
+
"node": ">=0.10.0"
|
| 786 |
+
}
|
| 787 |
+
},
|
| 788 |
+
"node_modules/is-number": {
|
| 789 |
+
"version": "7.0.0",
|
| 790 |
+
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
| 791 |
+
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
| 792 |
+
"dev": true,
|
| 793 |
+
"license": "MIT",
|
| 794 |
+
"engines": {
|
| 795 |
+
"node": ">=0.12.0"
|
| 796 |
+
}
|
| 797 |
+
},
|
| 798 |
+
"node_modules/jiti": {
|
| 799 |
+
"version": "1.21.7",
|
| 800 |
+
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
| 801 |
+
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
| 802 |
+
"dev": true,
|
| 803 |
+
"license": "MIT",
|
| 804 |
+
"bin": {
|
| 805 |
+
"jiti": "bin/jiti.js"
|
| 806 |
+
}
|
| 807 |
+
},
|
| 808 |
+
"node_modules/js-tokens": {
|
| 809 |
+
"version": "4.0.0",
|
| 810 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 811 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 812 |
+
"license": "MIT"
|
| 813 |
+
},
|
| 814 |
+
"node_modules/lilconfig": {
|
| 815 |
+
"version": "3.1.3",
|
| 816 |
+
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
| 817 |
+
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
| 818 |
+
"dev": true,
|
| 819 |
+
"license": "MIT",
|
| 820 |
+
"engines": {
|
| 821 |
+
"node": ">=14"
|
| 822 |
+
},
|
| 823 |
+
"funding": {
|
| 824 |
+
"url": "https://github.com/sponsors/antonk52"
|
| 825 |
+
}
|
| 826 |
+
},
|
| 827 |
+
"node_modules/lines-and-columns": {
|
| 828 |
+
"version": "1.2.4",
|
| 829 |
+
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
| 830 |
+
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
| 831 |
+
"dev": true,
|
| 832 |
+
"license": "MIT"
|
| 833 |
+
},
|
| 834 |
+
"node_modules/loose-envify": {
|
| 835 |
+
"version": "1.4.0",
|
| 836 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 837 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 838 |
+
"license": "MIT",
|
| 839 |
+
"dependencies": {
|
| 840 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 841 |
+
},
|
| 842 |
+
"bin": {
|
| 843 |
+
"loose-envify": "cli.js"
|
| 844 |
+
}
|
| 845 |
+
},
|
| 846 |
+
"node_modules/lucide-react": {
|
| 847 |
+
"version": "0.300.0",
|
| 848 |
+
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.300.0.tgz",
|
| 849 |
+
"integrity": "sha512-rQxUUCmWAvNLoAsMZ5j04b2+OJv6UuNLYMY7VK0eVlm4aTwUEjEEHc09/DipkNIlhXUSDn2xoyIzVT0uh7dRsg==",
|
| 850 |
+
"license": "ISC",
|
| 851 |
+
"peerDependencies": {
|
| 852 |
+
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
| 853 |
+
}
|
| 854 |
+
},
|
| 855 |
+
"node_modules/merge2": {
|
| 856 |
+
"version": "1.4.1",
|
| 857 |
+
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
| 858 |
+
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
| 859 |
+
"dev": true,
|
| 860 |
+
"license": "MIT",
|
| 861 |
+
"engines": {
|
| 862 |
+
"node": ">= 8"
|
| 863 |
+
}
|
| 864 |
+
},
|
| 865 |
+
"node_modules/micromatch": {
|
| 866 |
+
"version": "4.0.8",
|
| 867 |
+
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
| 868 |
+
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
| 869 |
+
"dev": true,
|
| 870 |
+
"license": "MIT",
|
| 871 |
+
"dependencies": {
|
| 872 |
+
"braces": "^3.0.3",
|
| 873 |
+
"picomatch": "^2.3.1"
|
| 874 |
+
},
|
| 875 |
+
"engines": {
|
| 876 |
+
"node": ">=8.6"
|
| 877 |
+
}
|
| 878 |
+
},
|
| 879 |
+
"node_modules/mz": {
|
| 880 |
+
"version": "2.7.0",
|
| 881 |
+
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
| 882 |
+
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
| 883 |
+
"dev": true,
|
| 884 |
+
"license": "MIT",
|
| 885 |
+
"dependencies": {
|
| 886 |
+
"any-promise": "^1.0.0",
|
| 887 |
+
"object-assign": "^4.0.1",
|
| 888 |
+
"thenify-all": "^1.0.0"
|
| 889 |
+
}
|
| 890 |
+
},
|
| 891 |
+
"node_modules/nanoid": {
|
| 892 |
+
"version": "3.3.11",
|
| 893 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 894 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 895 |
+
"funding": [
|
| 896 |
+
{
|
| 897 |
+
"type": "github",
|
| 898 |
+
"url": "https://github.com/sponsors/ai"
|
| 899 |
+
}
|
| 900 |
+
],
|
| 901 |
+
"license": "MIT",
|
| 902 |
+
"bin": {
|
| 903 |
+
"nanoid": "bin/nanoid.cjs"
|
| 904 |
+
},
|
| 905 |
+
"engines": {
|
| 906 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 907 |
+
}
|
| 908 |
+
},
|
| 909 |
+
"node_modules/next": {
|
| 910 |
+
"version": "14.2.35",
|
| 911 |
+
"resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
|
| 912 |
+
"integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
|
| 913 |
+
"license": "MIT",
|
| 914 |
+
"dependencies": {
|
| 915 |
+
"@next/env": "14.2.35",
|
| 916 |
+
"@swc/helpers": "0.5.5",
|
| 917 |
+
"busboy": "1.6.0",
|
| 918 |
+
"caniuse-lite": "^1.0.30001579",
|
| 919 |
+
"graceful-fs": "^4.2.11",
|
| 920 |
+
"postcss": "8.4.31",
|
| 921 |
+
"styled-jsx": "5.1.1"
|
| 922 |
+
},
|
| 923 |
+
"bin": {
|
| 924 |
+
"next": "dist/bin/next"
|
| 925 |
+
},
|
| 926 |
+
"engines": {
|
| 927 |
+
"node": ">=18.17.0"
|
| 928 |
+
},
|
| 929 |
+
"optionalDependencies": {
|
| 930 |
+
"@next/swc-darwin-arm64": "14.2.33",
|
| 931 |
+
"@next/swc-darwin-x64": "14.2.33",
|
| 932 |
+
"@next/swc-linux-arm64-gnu": "14.2.33",
|
| 933 |
+
"@next/swc-linux-arm64-musl": "14.2.33",
|
| 934 |
+
"@next/swc-linux-x64-gnu": "14.2.33",
|
| 935 |
+
"@next/swc-linux-x64-musl": "14.2.33",
|
| 936 |
+
"@next/swc-win32-arm64-msvc": "14.2.33",
|
| 937 |
+
"@next/swc-win32-ia32-msvc": "14.2.33",
|
| 938 |
+
"@next/swc-win32-x64-msvc": "14.2.33"
|
| 939 |
+
},
|
| 940 |
+
"peerDependencies": {
|
| 941 |
+
"@opentelemetry/api": "^1.1.0",
|
| 942 |
+
"@playwright/test": "^1.41.2",
|
| 943 |
+
"react": "^18.2.0",
|
| 944 |
+
"react-dom": "^18.2.0",
|
| 945 |
+
"sass": "^1.3.0"
|
| 946 |
+
},
|
| 947 |
+
"peerDependenciesMeta": {
|
| 948 |
+
"@opentelemetry/api": {
|
| 949 |
+
"optional": true
|
| 950 |
+
},
|
| 951 |
+
"@playwright/test": {
|
| 952 |
+
"optional": true
|
| 953 |
+
},
|
| 954 |
+
"sass": {
|
| 955 |
+
"optional": true
|
| 956 |
+
}
|
| 957 |
+
}
|
| 958 |
+
},
|
| 959 |
+
"node_modules/next/node_modules/postcss": {
|
| 960 |
+
"version": "8.4.31",
|
| 961 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
| 962 |
+
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
| 963 |
+
"funding": [
|
| 964 |
+
{
|
| 965 |
+
"type": "opencollective",
|
| 966 |
+
"url": "https://opencollective.com/postcss/"
|
| 967 |
+
},
|
| 968 |
+
{
|
| 969 |
+
"type": "tidelift",
|
| 970 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 971 |
+
},
|
| 972 |
+
{
|
| 973 |
+
"type": "github",
|
| 974 |
+
"url": "https://github.com/sponsors/ai"
|
| 975 |
+
}
|
| 976 |
+
],
|
| 977 |
+
"license": "MIT",
|
| 978 |
+
"dependencies": {
|
| 979 |
+
"nanoid": "^3.3.6",
|
| 980 |
+
"picocolors": "^1.0.0",
|
| 981 |
+
"source-map-js": "^1.0.2"
|
| 982 |
+
},
|
| 983 |
+
"engines": {
|
| 984 |
+
"node": "^10 || ^12 || >=14"
|
| 985 |
+
}
|
| 986 |
+
},
|
| 987 |
+
"node_modules/node-releases": {
|
| 988 |
+
"version": "2.0.27",
|
| 989 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
| 990 |
+
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
| 991 |
+
"dev": true,
|
| 992 |
+
"license": "MIT"
|
| 993 |
+
},
|
| 994 |
+
"node_modules/normalize-path": {
|
| 995 |
+
"version": "3.0.0",
|
| 996 |
+
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
| 997 |
+
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
| 998 |
+
"dev": true,
|
| 999 |
+
"license": "MIT",
|
| 1000 |
+
"engines": {
|
| 1001 |
+
"node": ">=0.10.0"
|
| 1002 |
+
}
|
| 1003 |
+
},
|
| 1004 |
+
"node_modules/object-assign": {
|
| 1005 |
+
"version": "4.1.1",
|
| 1006 |
+
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
| 1007 |
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
| 1008 |
+
"dev": true,
|
| 1009 |
+
"license": "MIT",
|
| 1010 |
+
"engines": {
|
| 1011 |
+
"node": ">=0.10.0"
|
| 1012 |
+
}
|
| 1013 |
+
},
|
| 1014 |
+
"node_modules/object-hash": {
|
| 1015 |
+
"version": "3.0.0",
|
| 1016 |
+
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
| 1017 |
+
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
| 1018 |
+
"dev": true,
|
| 1019 |
+
"license": "MIT",
|
| 1020 |
+
"engines": {
|
| 1021 |
+
"node": ">= 6"
|
| 1022 |
+
}
|
| 1023 |
+
},
|
| 1024 |
+
"node_modules/path-parse": {
|
| 1025 |
+
"version": "1.0.7",
|
| 1026 |
+
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
| 1027 |
+
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
| 1028 |
+
"dev": true,
|
| 1029 |
+
"license": "MIT"
|
| 1030 |
+
},
|
| 1031 |
+
"node_modules/picocolors": {
|
| 1032 |
+
"version": "1.1.1",
|
| 1033 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1034 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1035 |
+
"license": "ISC"
|
| 1036 |
+
},
|
| 1037 |
+
"node_modules/picomatch": {
|
| 1038 |
+
"version": "2.3.1",
|
| 1039 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
| 1040 |
+
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
| 1041 |
+
"dev": true,
|
| 1042 |
+
"license": "MIT",
|
| 1043 |
+
"engines": {
|
| 1044 |
+
"node": ">=8.6"
|
| 1045 |
+
},
|
| 1046 |
+
"funding": {
|
| 1047 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 1048 |
+
}
|
| 1049 |
+
},
|
| 1050 |
+
"node_modules/pify": {
|
| 1051 |
+
"version": "2.3.0",
|
| 1052 |
+
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
| 1053 |
+
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
| 1054 |
+
"dev": true,
|
| 1055 |
+
"license": "MIT",
|
| 1056 |
+
"engines": {
|
| 1057 |
+
"node": ">=0.10.0"
|
| 1058 |
+
}
|
| 1059 |
+
},
|
| 1060 |
+
"node_modules/pirates": {
|
| 1061 |
+
"version": "4.0.7",
|
| 1062 |
+
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
| 1063 |
+
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
| 1064 |
+
"dev": true,
|
| 1065 |
+
"license": "MIT",
|
| 1066 |
+
"engines": {
|
| 1067 |
+
"node": ">= 6"
|
| 1068 |
+
}
|
| 1069 |
+
},
|
| 1070 |
+
"node_modules/postcss": {
|
| 1071 |
+
"version": "8.5.6",
|
| 1072 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
| 1073 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 1074 |
+
"dev": true,
|
| 1075 |
+
"funding": [
|
| 1076 |
+
{
|
| 1077 |
+
"type": "opencollective",
|
| 1078 |
+
"url": "https://opencollective.com/postcss/"
|
| 1079 |
+
},
|
| 1080 |
+
{
|
| 1081 |
+
"type": "tidelift",
|
| 1082 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1083 |
+
},
|
| 1084 |
+
{
|
| 1085 |
+
"type": "github",
|
| 1086 |
+
"url": "https://github.com/sponsors/ai"
|
| 1087 |
+
}
|
| 1088 |
+
],
|
| 1089 |
+
"license": "MIT",
|
| 1090 |
+
"dependencies": {
|
| 1091 |
+
"nanoid": "^3.3.11",
|
| 1092 |
+
"picocolors": "^1.1.1",
|
| 1093 |
+
"source-map-js": "^1.2.1"
|
| 1094 |
+
},
|
| 1095 |
+
"engines": {
|
| 1096 |
+
"node": "^10 || ^12 || >=14"
|
| 1097 |
+
}
|
| 1098 |
+
},
|
| 1099 |
+
"node_modules/postcss-import": {
|
| 1100 |
+
"version": "15.1.0",
|
| 1101 |
+
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
| 1102 |
+
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
| 1103 |
+
"dev": true,
|
| 1104 |
+
"license": "MIT",
|
| 1105 |
+
"dependencies": {
|
| 1106 |
+
"postcss-value-parser": "^4.0.0",
|
| 1107 |
+
"read-cache": "^1.0.0",
|
| 1108 |
+
"resolve": "^1.1.7"
|
| 1109 |
+
},
|
| 1110 |
+
"engines": {
|
| 1111 |
+
"node": ">=14.0.0"
|
| 1112 |
+
},
|
| 1113 |
+
"peerDependencies": {
|
| 1114 |
+
"postcss": "^8.0.0"
|
| 1115 |
+
}
|
| 1116 |
+
},
|
| 1117 |
+
"node_modules/postcss-js": {
|
| 1118 |
+
"version": "4.1.0",
|
| 1119 |
+
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
| 1120 |
+
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
| 1121 |
+
"dev": true,
|
| 1122 |
+
"funding": [
|
| 1123 |
+
{
|
| 1124 |
+
"type": "opencollective",
|
| 1125 |
+
"url": "https://opencollective.com/postcss/"
|
| 1126 |
+
},
|
| 1127 |
+
{
|
| 1128 |
+
"type": "github",
|
| 1129 |
+
"url": "https://github.com/sponsors/ai"
|
| 1130 |
+
}
|
| 1131 |
+
],
|
| 1132 |
+
"license": "MIT",
|
| 1133 |
+
"dependencies": {
|
| 1134 |
+
"camelcase-css": "^2.0.1"
|
| 1135 |
+
},
|
| 1136 |
+
"engines": {
|
| 1137 |
+
"node": "^12 || ^14 || >= 16"
|
| 1138 |
+
},
|
| 1139 |
+
"peerDependencies": {
|
| 1140 |
+
"postcss": "^8.4.21"
|
| 1141 |
+
}
|
| 1142 |
+
},
|
| 1143 |
+
"node_modules/postcss-load-config": {
|
| 1144 |
+
"version": "6.0.1",
|
| 1145 |
+
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
| 1146 |
+
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
| 1147 |
+
"dev": true,
|
| 1148 |
+
"funding": [
|
| 1149 |
+
{
|
| 1150 |
+
"type": "opencollective",
|
| 1151 |
+
"url": "https://opencollective.com/postcss/"
|
| 1152 |
+
},
|
| 1153 |
+
{
|
| 1154 |
+
"type": "github",
|
| 1155 |
+
"url": "https://github.com/sponsors/ai"
|
| 1156 |
+
}
|
| 1157 |
+
],
|
| 1158 |
+
"license": "MIT",
|
| 1159 |
+
"dependencies": {
|
| 1160 |
+
"lilconfig": "^3.1.1"
|
| 1161 |
+
},
|
| 1162 |
+
"engines": {
|
| 1163 |
+
"node": ">= 18"
|
| 1164 |
+
},
|
| 1165 |
+
"peerDependencies": {
|
| 1166 |
+
"jiti": ">=1.21.0",
|
| 1167 |
+
"postcss": ">=8.0.9",
|
| 1168 |
+
"tsx": "^4.8.1",
|
| 1169 |
+
"yaml": "^2.4.2"
|
| 1170 |
+
},
|
| 1171 |
+
"peerDependenciesMeta": {
|
| 1172 |
+
"jiti": {
|
| 1173 |
+
"optional": true
|
| 1174 |
+
},
|
| 1175 |
+
"postcss": {
|
| 1176 |
+
"optional": true
|
| 1177 |
+
},
|
| 1178 |
+
"tsx": {
|
| 1179 |
+
"optional": true
|
| 1180 |
+
},
|
| 1181 |
+
"yaml": {
|
| 1182 |
+
"optional": true
|
| 1183 |
+
}
|
| 1184 |
+
}
|
| 1185 |
+
},
|
| 1186 |
+
"node_modules/postcss-nested": {
|
| 1187 |
+
"version": "6.2.0",
|
| 1188 |
+
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
| 1189 |
+
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
| 1190 |
+
"dev": true,
|
| 1191 |
+
"funding": [
|
| 1192 |
+
{
|
| 1193 |
+
"type": "opencollective",
|
| 1194 |
+
"url": "https://opencollective.com/postcss/"
|
| 1195 |
+
},
|
| 1196 |
+
{
|
| 1197 |
+
"type": "github",
|
| 1198 |
+
"url": "https://github.com/sponsors/ai"
|
| 1199 |
+
}
|
| 1200 |
+
],
|
| 1201 |
+
"license": "MIT",
|
| 1202 |
+
"dependencies": {
|
| 1203 |
+
"postcss-selector-parser": "^6.1.1"
|
| 1204 |
+
},
|
| 1205 |
+
"engines": {
|
| 1206 |
+
"node": ">=12.0"
|
| 1207 |
+
},
|
| 1208 |
+
"peerDependencies": {
|
| 1209 |
+
"postcss": "^8.2.14"
|
| 1210 |
+
}
|
| 1211 |
+
},
|
| 1212 |
+
"node_modules/postcss-selector-parser": {
|
| 1213 |
+
"version": "6.1.2",
|
| 1214 |
+
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
| 1215 |
+
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
| 1216 |
+
"dev": true,
|
| 1217 |
+
"license": "MIT",
|
| 1218 |
+
"dependencies": {
|
| 1219 |
+
"cssesc": "^3.0.0",
|
| 1220 |
+
"util-deprecate": "^1.0.2"
|
| 1221 |
+
},
|
| 1222 |
+
"engines": {
|
| 1223 |
+
"node": ">=4"
|
| 1224 |
+
}
|
| 1225 |
+
},
|
| 1226 |
+
"node_modules/postcss-value-parser": {
|
| 1227 |
+
"version": "4.2.0",
|
| 1228 |
+
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
| 1229 |
+
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
| 1230 |
+
"dev": true,
|
| 1231 |
+
"license": "MIT"
|
| 1232 |
+
},
|
| 1233 |
+
"node_modules/queue-microtask": {
|
| 1234 |
+
"version": "1.2.3",
|
| 1235 |
+
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
| 1236 |
+
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
| 1237 |
+
"dev": true,
|
| 1238 |
+
"funding": [
|
| 1239 |
+
{
|
| 1240 |
+
"type": "github",
|
| 1241 |
+
"url": "https://github.com/sponsors/feross"
|
| 1242 |
+
},
|
| 1243 |
+
{
|
| 1244 |
+
"type": "patreon",
|
| 1245 |
+
"url": "https://www.patreon.com/feross"
|
| 1246 |
+
},
|
| 1247 |
+
{
|
| 1248 |
+
"type": "consulting",
|
| 1249 |
+
"url": "https://feross.org/support"
|
| 1250 |
+
}
|
| 1251 |
+
],
|
| 1252 |
+
"license": "MIT"
|
| 1253 |
+
},
|
| 1254 |
+
"node_modules/react": {
|
| 1255 |
+
"version": "18.3.1",
|
| 1256 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 1257 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 1258 |
+
"license": "MIT",
|
| 1259 |
+
"dependencies": {
|
| 1260 |
+
"loose-envify": "^1.1.0"
|
| 1261 |
+
},
|
| 1262 |
+
"engines": {
|
| 1263 |
+
"node": ">=0.10.0"
|
| 1264 |
+
}
|
| 1265 |
+
},
|
| 1266 |
+
"node_modules/react-dom": {
|
| 1267 |
+
"version": "18.3.1",
|
| 1268 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 1269 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 1270 |
+
"license": "MIT",
|
| 1271 |
+
"dependencies": {
|
| 1272 |
+
"loose-envify": "^1.1.0",
|
| 1273 |
+
"scheduler": "^0.23.2"
|
| 1274 |
+
},
|
| 1275 |
+
"peerDependencies": {
|
| 1276 |
+
"react": "^18.3.1"
|
| 1277 |
+
}
|
| 1278 |
+
},
|
| 1279 |
+
"node_modules/read-cache": {
|
| 1280 |
+
"version": "1.0.0",
|
| 1281 |
+
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
| 1282 |
+
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
| 1283 |
+
"dev": true,
|
| 1284 |
+
"license": "MIT",
|
| 1285 |
+
"dependencies": {
|
| 1286 |
+
"pify": "^2.3.0"
|
| 1287 |
+
}
|
| 1288 |
+
},
|
| 1289 |
+
"node_modules/readdirp": {
|
| 1290 |
+
"version": "3.6.0",
|
| 1291 |
+
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
| 1292 |
+
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
| 1293 |
+
"dev": true,
|
| 1294 |
+
"license": "MIT",
|
| 1295 |
+
"dependencies": {
|
| 1296 |
+
"picomatch": "^2.2.1"
|
| 1297 |
+
},
|
| 1298 |
+
"engines": {
|
| 1299 |
+
"node": ">=8.10.0"
|
| 1300 |
+
}
|
| 1301 |
+
},
|
| 1302 |
+
"node_modules/resolve": {
|
| 1303 |
+
"version": "1.22.11",
|
| 1304 |
+
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
| 1305 |
+
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
| 1306 |
+
"dev": true,
|
| 1307 |
+
"license": "MIT",
|
| 1308 |
+
"dependencies": {
|
| 1309 |
+
"is-core-module": "^2.16.1",
|
| 1310 |
+
"path-parse": "^1.0.7",
|
| 1311 |
+
"supports-preserve-symlinks-flag": "^1.0.0"
|
| 1312 |
+
},
|
| 1313 |
+
"bin": {
|
| 1314 |
+
"resolve": "bin/resolve"
|
| 1315 |
+
},
|
| 1316 |
+
"engines": {
|
| 1317 |
+
"node": ">= 0.4"
|
| 1318 |
+
},
|
| 1319 |
+
"funding": {
|
| 1320 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1321 |
+
}
|
| 1322 |
+
},
|
| 1323 |
+
"node_modules/reusify": {
|
| 1324 |
+
"version": "1.1.0",
|
| 1325 |
+
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
| 1326 |
+
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
| 1327 |
+
"dev": true,
|
| 1328 |
+
"license": "MIT",
|
| 1329 |
+
"engines": {
|
| 1330 |
+
"iojs": ">=1.0.0",
|
| 1331 |
+
"node": ">=0.10.0"
|
| 1332 |
+
}
|
| 1333 |
+
},
|
| 1334 |
+
"node_modules/run-parallel": {
|
| 1335 |
+
"version": "1.2.0",
|
| 1336 |
+
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
| 1337 |
+
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
| 1338 |
+
"dev": true,
|
| 1339 |
+
"funding": [
|
| 1340 |
+
{
|
| 1341 |
+
"type": "github",
|
| 1342 |
+
"url": "https://github.com/sponsors/feross"
|
| 1343 |
+
},
|
| 1344 |
+
{
|
| 1345 |
+
"type": "patreon",
|
| 1346 |
+
"url": "https://www.patreon.com/feross"
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"type": "consulting",
|
| 1350 |
+
"url": "https://feross.org/support"
|
| 1351 |
+
}
|
| 1352 |
+
],
|
| 1353 |
+
"license": "MIT",
|
| 1354 |
+
"dependencies": {
|
| 1355 |
+
"queue-microtask": "^1.2.2"
|
| 1356 |
+
}
|
| 1357 |
+
},
|
| 1358 |
+
"node_modules/scheduler": {
|
| 1359 |
+
"version": "0.23.2",
|
| 1360 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 1361 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 1362 |
+
"license": "MIT",
|
| 1363 |
+
"dependencies": {
|
| 1364 |
+
"loose-envify": "^1.1.0"
|
| 1365 |
+
}
|
| 1366 |
+
},
|
| 1367 |
+
"node_modules/source-map-js": {
|
| 1368 |
+
"version": "1.2.1",
|
| 1369 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1370 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1371 |
+
"license": "BSD-3-Clause",
|
| 1372 |
+
"engines": {
|
| 1373 |
+
"node": ">=0.10.0"
|
| 1374 |
+
}
|
| 1375 |
+
},
|
| 1376 |
+
"node_modules/streamsearch": {
|
| 1377 |
+
"version": "1.1.0",
|
| 1378 |
+
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
| 1379 |
+
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
| 1380 |
+
"engines": {
|
| 1381 |
+
"node": ">=10.0.0"
|
| 1382 |
+
}
|
| 1383 |
+
},
|
| 1384 |
+
"node_modules/styled-jsx": {
|
| 1385 |
+
"version": "5.1.1",
|
| 1386 |
+
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
|
| 1387 |
+
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
|
| 1388 |
+
"license": "MIT",
|
| 1389 |
+
"dependencies": {
|
| 1390 |
+
"client-only": "0.0.1"
|
| 1391 |
+
},
|
| 1392 |
+
"engines": {
|
| 1393 |
+
"node": ">= 12.0.0"
|
| 1394 |
+
},
|
| 1395 |
+
"peerDependencies": {
|
| 1396 |
+
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
|
| 1397 |
+
},
|
| 1398 |
+
"peerDependenciesMeta": {
|
| 1399 |
+
"@babel/core": {
|
| 1400 |
+
"optional": true
|
| 1401 |
+
},
|
| 1402 |
+
"babel-plugin-macros": {
|
| 1403 |
+
"optional": true
|
| 1404 |
+
}
|
| 1405 |
+
}
|
| 1406 |
+
},
|
| 1407 |
+
"node_modules/sucrase": {
|
| 1408 |
+
"version": "3.35.1",
|
| 1409 |
+
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
| 1410 |
+
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
| 1411 |
+
"dev": true,
|
| 1412 |
+
"license": "MIT",
|
| 1413 |
+
"dependencies": {
|
| 1414 |
+
"@jridgewell/gen-mapping": "^0.3.2",
|
| 1415 |
+
"commander": "^4.0.0",
|
| 1416 |
+
"lines-and-columns": "^1.1.6",
|
| 1417 |
+
"mz": "^2.7.0",
|
| 1418 |
+
"pirates": "^4.0.1",
|
| 1419 |
+
"tinyglobby": "^0.2.11",
|
| 1420 |
+
"ts-interface-checker": "^0.1.9"
|
| 1421 |
+
},
|
| 1422 |
+
"bin": {
|
| 1423 |
+
"sucrase": "bin/sucrase",
|
| 1424 |
+
"sucrase-node": "bin/sucrase-node"
|
| 1425 |
+
},
|
| 1426 |
+
"engines": {
|
| 1427 |
+
"node": ">=16 || 14 >=14.17"
|
| 1428 |
+
}
|
| 1429 |
+
},
|
| 1430 |
+
"node_modules/supports-preserve-symlinks-flag": {
|
| 1431 |
+
"version": "1.0.0",
|
| 1432 |
+
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
| 1433 |
+
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
| 1434 |
+
"dev": true,
|
| 1435 |
+
"license": "MIT",
|
| 1436 |
+
"engines": {
|
| 1437 |
+
"node": ">= 0.4"
|
| 1438 |
+
},
|
| 1439 |
+
"funding": {
|
| 1440 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1441 |
+
}
|
| 1442 |
+
},
|
| 1443 |
+
"node_modules/tailwind-merge": {
|
| 1444 |
+
"version": "2.6.1",
|
| 1445 |
+
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
|
| 1446 |
+
"integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
|
| 1447 |
+
"license": "MIT",
|
| 1448 |
+
"funding": {
|
| 1449 |
+
"type": "github",
|
| 1450 |
+
"url": "https://github.com/sponsors/dcastil"
|
| 1451 |
+
}
|
| 1452 |
+
},
|
| 1453 |
+
"node_modules/tailwindcss": {
|
| 1454 |
+
"version": "3.4.19",
|
| 1455 |
+
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
| 1456 |
+
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
| 1457 |
+
"dev": true,
|
| 1458 |
+
"license": "MIT",
|
| 1459 |
+
"dependencies": {
|
| 1460 |
+
"@alloc/quick-lru": "^5.2.0",
|
| 1461 |
+
"arg": "^5.0.2",
|
| 1462 |
+
"chokidar": "^3.6.0",
|
| 1463 |
+
"didyoumean": "^1.2.2",
|
| 1464 |
+
"dlv": "^1.1.3",
|
| 1465 |
+
"fast-glob": "^3.3.2",
|
| 1466 |
+
"glob-parent": "^6.0.2",
|
| 1467 |
+
"is-glob": "^4.0.3",
|
| 1468 |
+
"jiti": "^1.21.7",
|
| 1469 |
+
"lilconfig": "^3.1.3",
|
| 1470 |
+
"micromatch": "^4.0.8",
|
| 1471 |
+
"normalize-path": "^3.0.0",
|
| 1472 |
+
"object-hash": "^3.0.0",
|
| 1473 |
+
"picocolors": "^1.1.1",
|
| 1474 |
+
"postcss": "^8.4.47",
|
| 1475 |
+
"postcss-import": "^15.1.0",
|
| 1476 |
+
"postcss-js": "^4.0.1",
|
| 1477 |
+
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
|
| 1478 |
+
"postcss-nested": "^6.2.0",
|
| 1479 |
+
"postcss-selector-parser": "^6.1.2",
|
| 1480 |
+
"resolve": "^1.22.8",
|
| 1481 |
+
"sucrase": "^3.35.0"
|
| 1482 |
+
},
|
| 1483 |
+
"bin": {
|
| 1484 |
+
"tailwind": "lib/cli.js",
|
| 1485 |
+
"tailwindcss": "lib/cli.js"
|
| 1486 |
+
},
|
| 1487 |
+
"engines": {
|
| 1488 |
+
"node": ">=14.0.0"
|
| 1489 |
+
}
|
| 1490 |
+
},
|
| 1491 |
+
"node_modules/thenify": {
|
| 1492 |
+
"version": "3.3.1",
|
| 1493 |
+
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
| 1494 |
+
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
| 1495 |
+
"dev": true,
|
| 1496 |
+
"license": "MIT",
|
| 1497 |
+
"dependencies": {
|
| 1498 |
+
"any-promise": "^1.0.0"
|
| 1499 |
+
}
|
| 1500 |
+
},
|
| 1501 |
+
"node_modules/thenify-all": {
|
| 1502 |
+
"version": "1.6.0",
|
| 1503 |
+
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
| 1504 |
+
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
| 1505 |
+
"dev": true,
|
| 1506 |
+
"license": "MIT",
|
| 1507 |
+
"dependencies": {
|
| 1508 |
+
"thenify": ">= 3.1.0 < 4"
|
| 1509 |
+
},
|
| 1510 |
+
"engines": {
|
| 1511 |
+
"node": ">=0.8"
|
| 1512 |
+
}
|
| 1513 |
+
},
|
| 1514 |
+
"node_modules/tinyglobby": {
|
| 1515 |
+
"version": "0.2.15",
|
| 1516 |
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
| 1517 |
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
| 1518 |
+
"dev": true,
|
| 1519 |
+
"license": "MIT",
|
| 1520 |
+
"dependencies": {
|
| 1521 |
+
"fdir": "^6.5.0",
|
| 1522 |
+
"picomatch": "^4.0.3"
|
| 1523 |
+
},
|
| 1524 |
+
"engines": {
|
| 1525 |
+
"node": ">=12.0.0"
|
| 1526 |
+
},
|
| 1527 |
+
"funding": {
|
| 1528 |
+
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 1529 |
+
}
|
| 1530 |
+
},
|
| 1531 |
+
"node_modules/tinyglobby/node_modules/fdir": {
|
| 1532 |
+
"version": "6.5.0",
|
| 1533 |
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
| 1534 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 1535 |
+
"dev": true,
|
| 1536 |
+
"license": "MIT",
|
| 1537 |
+
"engines": {
|
| 1538 |
+
"node": ">=12.0.0"
|
| 1539 |
+
},
|
| 1540 |
+
"peerDependencies": {
|
| 1541 |
+
"picomatch": "^3 || ^4"
|
| 1542 |
+
},
|
| 1543 |
+
"peerDependenciesMeta": {
|
| 1544 |
+
"picomatch": {
|
| 1545 |
+
"optional": true
|
| 1546 |
+
}
|
| 1547 |
+
}
|
| 1548 |
+
},
|
| 1549 |
+
"node_modules/tinyglobby/node_modules/picomatch": {
|
| 1550 |
+
"version": "4.0.3",
|
| 1551 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 1552 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 1553 |
+
"dev": true,
|
| 1554 |
+
"license": "MIT",
|
| 1555 |
+
"engines": {
|
| 1556 |
+
"node": ">=12"
|
| 1557 |
+
},
|
| 1558 |
+
"funding": {
|
| 1559 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 1560 |
+
}
|
| 1561 |
+
},
|
| 1562 |
+
"node_modules/to-regex-range": {
|
| 1563 |
+
"version": "5.0.1",
|
| 1564 |
+
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
| 1565 |
+
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
| 1566 |
+
"dev": true,
|
| 1567 |
+
"license": "MIT",
|
| 1568 |
+
"dependencies": {
|
| 1569 |
+
"is-number": "^7.0.0"
|
| 1570 |
+
},
|
| 1571 |
+
"engines": {
|
| 1572 |
+
"node": ">=8.0"
|
| 1573 |
+
}
|
| 1574 |
+
},
|
| 1575 |
+
"node_modules/ts-interface-checker": {
|
| 1576 |
+
"version": "0.1.13",
|
| 1577 |
+
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
| 1578 |
+
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
| 1579 |
+
"dev": true,
|
| 1580 |
+
"license": "Apache-2.0"
|
| 1581 |
+
},
|
| 1582 |
+
"node_modules/tslib": {
|
| 1583 |
+
"version": "2.8.1",
|
| 1584 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 1585 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 1586 |
+
"license": "0BSD"
|
| 1587 |
+
},
|
| 1588 |
+
"node_modules/typescript": {
|
| 1589 |
+
"version": "5.9.3",
|
| 1590 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 1591 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 1592 |
+
"dev": true,
|
| 1593 |
+
"license": "Apache-2.0",
|
| 1594 |
+
"bin": {
|
| 1595 |
+
"tsc": "bin/tsc",
|
| 1596 |
+
"tsserver": "bin/tsserver"
|
| 1597 |
+
},
|
| 1598 |
+
"engines": {
|
| 1599 |
+
"node": ">=14.17"
|
| 1600 |
+
}
|
| 1601 |
+
},
|
| 1602 |
+
"node_modules/undici-types": {
|
| 1603 |
+
"version": "6.21.0",
|
| 1604 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 1605 |
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 1606 |
+
"dev": true,
|
| 1607 |
+
"license": "MIT"
|
| 1608 |
+
},
|
| 1609 |
+
"node_modules/update-browserslist-db": {
|
| 1610 |
+
"version": "1.2.3",
|
| 1611 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 1612 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 1613 |
+
"dev": true,
|
| 1614 |
+
"funding": [
|
| 1615 |
+
{
|
| 1616 |
+
"type": "opencollective",
|
| 1617 |
+
"url": "https://opencollective.com/browserslist"
|
| 1618 |
+
},
|
| 1619 |
+
{
|
| 1620 |
+
"type": "tidelift",
|
| 1621 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1622 |
+
},
|
| 1623 |
+
{
|
| 1624 |
+
"type": "github",
|
| 1625 |
+
"url": "https://github.com/sponsors/ai"
|
| 1626 |
+
}
|
| 1627 |
+
],
|
| 1628 |
+
"license": "MIT",
|
| 1629 |
+
"dependencies": {
|
| 1630 |
+
"escalade": "^3.2.0",
|
| 1631 |
+
"picocolors": "^1.1.1"
|
| 1632 |
+
},
|
| 1633 |
+
"bin": {
|
| 1634 |
+
"update-browserslist-db": "cli.js"
|
| 1635 |
+
},
|
| 1636 |
+
"peerDependencies": {
|
| 1637 |
+
"browserslist": ">= 4.21.0"
|
| 1638 |
+
}
|
| 1639 |
+
},
|
| 1640 |
+
"node_modules/util-deprecate": {
|
| 1641 |
+
"version": "1.0.2",
|
| 1642 |
+
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
| 1643 |
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
| 1644 |
+
"dev": true,
|
| 1645 |
+
"license": "MIT"
|
| 1646 |
+
}
|
| 1647 |
+
}
|
| 1648 |
+
}
|
src/frontend/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cds-agent-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "next lint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"next": "^14.0.0",
|
| 13 |
+
"react": "^18.2.0",
|
| 14 |
+
"react-dom": "^18.2.0",
|
| 15 |
+
"lucide-react": "^0.300.0",
|
| 16 |
+
"clsx": "^2.0.0",
|
| 17 |
+
"tailwind-merge": "^2.0.0"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/node": "^20.10.0",
|
| 21 |
+
"@types/react": "^18.2.0",
|
| 22 |
+
"@types/react-dom": "^18.2.0",
|
| 23 |
+
"autoprefixer": "^10.4.16",
|
| 24 |
+
"postcss": "^8.4.31",
|
| 25 |
+
"tailwindcss": "^3.3.0",
|
| 26 |
+
"typescript": "^5.3.0"
|
| 27 |
+
}
|
| 28 |
+
}
|
src/frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
};
|
src/frontend/src/app/globals.css
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
--foreground-rgb: 15, 23, 42;
|
| 7 |
+
--background-rgb: 248, 250, 252;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
body {
|
| 11 |
+
color: rgb(var(--foreground-rgb));
|
| 12 |
+
background: rgb(var(--background-rgb));
|
| 13 |
+
font-family: system-ui, -apple-system, sans-serif;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/* Pulse animation for running steps */
|
| 17 |
+
@keyframes pulse-dot {
|
| 18 |
+
0%, 100% { opacity: 1; }
|
| 19 |
+
50% { opacity: 0.3; }
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.animate-pulse-dot {
|
| 23 |
+
animation: pulse-dot 1.5s ease-in-out infinite;
|
| 24 |
+
}
|
src/frontend/src/app/layout.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import "./globals.css";
|
| 3 |
+
|
| 4 |
+
export const metadata: Metadata = {
|
| 5 |
+
title: "CDS Agent — Clinical Decision Support",
|
| 6 |
+
description:
|
| 7 |
+
"Agentic clinical decision support powered by MedGemma (HAI-DEF)",
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
export default function RootLayout({
|
| 11 |
+
children,
|
| 12 |
+
}: {
|
| 13 |
+
children: React.ReactNode;
|
| 14 |
+
}) {
|
| 15 |
+
return (
|
| 16 |
+
<html lang="en">
|
| 17 |
+
<body>{children}</body>
|
| 18 |
+
</html>
|
| 19 |
+
);
|
| 20 |
+
}
|
src/frontend/src/app/page.tsx
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import { PatientInput } from "@/components/PatientInput";
|
| 5 |
+
import { AgentPipeline } from "@/components/AgentPipeline";
|
| 6 |
+
import { CDSReport } from "@/components/CDSReport";
|
| 7 |
+
import { useAgentWebSocket } from "@/hooks/useAgentWebSocket";
|
| 8 |
+
|
| 9 |
+
export default function Home() {
|
| 10 |
+
const { steps, report, isRunning, error, submitCase } = useAgentWebSocket();
|
| 11 |
+
const [hasSubmitted, setHasSubmitted] = useState(false);
|
| 12 |
+
|
| 13 |
+
const handleSubmit = (patientText: string) => {
|
| 14 |
+
setHasSubmitted(true);
|
| 15 |
+
submitCase({
|
| 16 |
+
patient_text: patientText,
|
| 17 |
+
include_drug_check: true,
|
| 18 |
+
include_guidelines: true,
|
| 19 |
+
});
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<main className="min-h-screen">
|
| 24 |
+
{/* Header */}
|
| 25 |
+
<header className="bg-white border-b border-gray-200 px-6 py-4">
|
| 26 |
+
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
| 27 |
+
<div>
|
| 28 |
+
<h1 className="text-2xl font-bold text-gray-900">
|
| 29 |
+
🏥 CDS Agent
|
| 30 |
+
</h1>
|
| 31 |
+
<p className="text-sm text-gray-500">
|
| 32 |
+
Clinical Decision Support powered by MedGemma
|
| 33 |
+
</p>
|
| 34 |
+
</div>
|
| 35 |
+
<span className="text-xs bg-blue-100 text-blue-700 px-3 py-1 rounded-full font-medium">
|
| 36 |
+
HAI-DEF · Agentic Workflow
|
| 37 |
+
</span>
|
| 38 |
+
</div>
|
| 39 |
+
</header>
|
| 40 |
+
|
| 41 |
+
{/* Main content */}
|
| 42 |
+
<div className="max-w-7xl mx-auto px-6 py-8">
|
| 43 |
+
{!hasSubmitted ? (
|
| 44 |
+
/* Input view */
|
| 45 |
+
<div className="max-w-3xl mx-auto">
|
| 46 |
+
<div className="mb-8 text-center">
|
| 47 |
+
<h2 className="text-xl font-semibold text-gray-800 mb-2">
|
| 48 |
+
Submit a Patient Case
|
| 49 |
+
</h2>
|
| 50 |
+
<p className="text-gray-500">
|
| 51 |
+
Enter a patient case description. The agent pipeline will
|
| 52 |
+
parse, reason, check interactions, retrieve guidelines, and
|
| 53 |
+
synthesize a clinical decision support report.
|
| 54 |
+
</p>
|
| 55 |
+
</div>
|
| 56 |
+
<PatientInput onSubmit={handleSubmit} isLoading={isRunning} />
|
| 57 |
+
</div>
|
| 58 |
+
) : (
|
| 59 |
+
/* Pipeline + Results view */
|
| 60 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 61 |
+
{/* Agent Pipeline (left) */}
|
| 62 |
+
<div className="lg:col-span-1">
|
| 63 |
+
<AgentPipeline steps={steps} isRunning={isRunning} />
|
| 64 |
+
{error && (
|
| 65 |
+
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
| 66 |
+
{error}
|
| 67 |
+
</div>
|
| 68 |
+
)}
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
{/* CDS Report (right) */}
|
| 72 |
+
<div className="lg:col-span-2">
|
| 73 |
+
{report ? (
|
| 74 |
+
<CDSReport report={report} />
|
| 75 |
+
) : isRunning ? (
|
| 76 |
+
<div className="flex items-center justify-center h-64 text-gray-400">
|
| 77 |
+
<div className="text-center">
|
| 78 |
+
<div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-4" />
|
| 79 |
+
<p>Agent pipeline running...</p>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
) : null}
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
)}
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
{/* Disclaimer footer */}
|
| 89 |
+
<footer className="fixed bottom-0 left-0 right-0 bg-amber-50 border-t border-amber-200 px-6 py-2">
|
| 90 |
+
<p className="text-center text-xs text-amber-700">
|
| 91 |
+
⚠️ AI-generated clinical decision support — for demonstration purposes
|
| 92 |
+
only. Does not replace professional medical judgment.
|
| 93 |
+
</p>
|
| 94 |
+
</footer>
|
| 95 |
+
</main>
|
| 96 |
+
);
|
| 97 |
+
}
|
src/frontend/src/components/AgentPipeline.tsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
interface Step {
|
| 4 |
+
step_id: string;
|
| 5 |
+
step_name: string;
|
| 6 |
+
status: "pending" | "running" | "completed" | "failed" | "skipped";
|
| 7 |
+
tool_name?: string;
|
| 8 |
+
output_summary?: string;
|
| 9 |
+
duration_ms?: number;
|
| 10 |
+
error?: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
interface AgentPipelineProps {
|
| 14 |
+
steps: Step[];
|
| 15 |
+
isRunning: boolean;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const STATUS_CONFIG = {
|
| 19 |
+
pending: {
|
| 20 |
+
icon: "○",
|
| 21 |
+
color: "text-gray-400",
|
| 22 |
+
bg: "bg-gray-50",
|
| 23 |
+
border: "border-gray-200",
|
| 24 |
+
},
|
| 25 |
+
running: {
|
| 26 |
+
icon: "◉",
|
| 27 |
+
color: "text-blue-600",
|
| 28 |
+
bg: "bg-blue-50",
|
| 29 |
+
border: "border-blue-300",
|
| 30 |
+
},
|
| 31 |
+
completed: {
|
| 32 |
+
icon: "✓",
|
| 33 |
+
color: "text-green-600",
|
| 34 |
+
bg: "bg-green-50",
|
| 35 |
+
border: "border-green-300",
|
| 36 |
+
},
|
| 37 |
+
failed: {
|
| 38 |
+
icon: "✗",
|
| 39 |
+
color: "text-red-600",
|
| 40 |
+
bg: "bg-red-50",
|
| 41 |
+
border: "border-red-300",
|
| 42 |
+
},
|
| 43 |
+
skipped: {
|
| 44 |
+
icon: "–",
|
| 45 |
+
color: "text-gray-400",
|
| 46 |
+
bg: "bg-gray-50",
|
| 47 |
+
border: "border-gray-200",
|
| 48 |
+
},
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
export function AgentPipeline({ steps, isRunning }: AgentPipelineProps) {
|
| 52 |
+
return (
|
| 53 |
+
<div className="bg-white rounded-xl border border-gray-200 p-5">
|
| 54 |
+
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-4">
|
| 55 |
+
Agent Pipeline
|
| 56 |
+
</h3>
|
| 57 |
+
|
| 58 |
+
<div className="space-y-1">
|
| 59 |
+
{steps.map((step, index) => {
|
| 60 |
+
const config = STATUS_CONFIG[step.status];
|
| 61 |
+
return (
|
| 62 |
+
<div key={step.step_id}>
|
| 63 |
+
{/* Connector line */}
|
| 64 |
+
{index > 0 && (
|
| 65 |
+
<div className="ml-3 h-4 w-px bg-gray-200" />
|
| 66 |
+
)}
|
| 67 |
+
|
| 68 |
+
{/* Step card */}
|
| 69 |
+
<div
|
| 70 |
+
className={`flex items-start gap-3 p-3 rounded-lg border ${config.bg} ${config.border} transition-all duration-300`}
|
| 71 |
+
>
|
| 72 |
+
{/* Status icon */}
|
| 73 |
+
<span
|
| 74 |
+
className={`text-lg font-bold ${config.color} ${
|
| 75 |
+
step.status === "running" ? "animate-pulse-dot" : ""
|
| 76 |
+
}`}
|
| 77 |
+
>
|
| 78 |
+
{config.icon}
|
| 79 |
+
</span>
|
| 80 |
+
|
| 81 |
+
{/* Step info */}
|
| 82 |
+
<div className="flex-1 min-w-0">
|
| 83 |
+
<div className="flex items-center justify-between">
|
| 84 |
+
<span className="text-sm font-medium text-gray-800">
|
| 85 |
+
{step.step_name}
|
| 86 |
+
</span>
|
| 87 |
+
{step.duration_ms != null && (
|
| 88 |
+
<span className="text-xs text-gray-400">
|
| 89 |
+
{step.duration_ms}ms
|
| 90 |
+
</span>
|
| 91 |
+
)}
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
{step.tool_name && (
|
| 95 |
+
<span className="text-xs text-gray-400 font-mono">
|
| 96 |
+
{step.tool_name}
|
| 97 |
+
</span>
|
| 98 |
+
)}
|
| 99 |
+
|
| 100 |
+
{step.output_summary && (
|
| 101 |
+
<p className="text-xs text-gray-600 mt-1">
|
| 102 |
+
{step.output_summary}
|
| 103 |
+
</p>
|
| 104 |
+
)}
|
| 105 |
+
|
| 106 |
+
{step.error && (
|
| 107 |
+
<p className="text-xs text-red-600 mt-1">{step.error}</p>
|
| 108 |
+
)}
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
);
|
| 113 |
+
})}
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
{steps.length === 0 && (
|
| 117 |
+
<p className="text-sm text-gray-400 text-center py-8">
|
| 118 |
+
Pipeline will appear here once a case is submitted
|
| 119 |
+
</p>
|
| 120 |
+
)}
|
| 121 |
+
</div>
|
| 122 |
+
);
|
| 123 |
+
}
|