Spaces:
Sleeping
Sleeping
Hackathon Builder commited on
Commit ·
5ad56ab
1
Parent(s): dab42ad
Fix Dockerfile permissions & add application files
Browse files- Dockerfile +38 -0
- README.md +61 -9
- backend/__init__.py +1 -0
- backend/main.py +492 -0
- backend/sample_patients.json +303 -0
- backend/static/assets/index-BEynGZGH.css +1 -0
- backend/static/assets/index-BPTnkzQ_.js +0 -0
- backend/static/index.html +16 -0
- codeblue_context.py +467 -0
- frontend +1 -0
- requirements.txt +4 -0
Dockerfile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
# Install Node.js for frontend build
|
| 4 |
+
RUN apt-get update && apt-get install -y curl && \
|
| 5 |
+
curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
|
| 6 |
+
apt-get install -y nodejs && \
|
| 7 |
+
rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# Hugging Face Spaces require running as a non-root user with ID 1000
|
| 10 |
+
RUN useradd -m -u 1000 user
|
| 11 |
+
USER user
|
| 12 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 13 |
+
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
# Copy requirements and install Python deps
|
| 17 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 18 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 19 |
+
|
| 20 |
+
# Copy frontend source and build
|
| 21 |
+
COPY --chown=user frontend/ ./frontend/
|
| 22 |
+
RUN cd frontend && npm install && npm run build
|
| 23 |
+
|
| 24 |
+
# Copy backend source
|
| 25 |
+
COPY --chown=user backend/ ./backend/
|
| 26 |
+
COPY --chown=user codeblue_context.py .
|
| 27 |
+
|
| 28 |
+
# Move built frontend to backend static directory
|
| 29 |
+
RUN mv frontend/dist backend/static
|
| 30 |
+
|
| 31 |
+
# Set environment variables for Hugging Face
|
| 32 |
+
ENV PORT=7860
|
| 33 |
+
ENV HOST=0.0.0.0
|
| 34 |
+
|
| 35 |
+
EXPOSE 7860
|
| 36 |
+
|
| 37 |
+
# Run FastAPI server
|
| 38 |
+
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,62 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CodeBlue Context 🚨
|
| 2 |
+
|
| 3 |
+
**An explainable, 5-agent clinical emergency intelligence system built for the Agent Assemble Hackathon.**
|
| 4 |
+
|
| 5 |
+
CodeBlue Context is designed to tackle a critical hospital bottleneck: the time doctors waste reading lengthy patient histories during emergencies. Instead of spending 10-15 minutes extracting scattered chart data, CodeBlue compresses a patient's medical context into a structured, structured 10-second summary.
|
| 6 |
+
|
| 7 |
+
## The 5-Agent Architecture
|
| 8 |
+
|
| 9 |
+
This system uses a sequential orchestration of 5 Gemini-powered AI agents (using `gemini-2.0-flash`):
|
| 10 |
+
|
| 11 |
+
1. **Triage Agent**: Determines ESI severity level and identifies time-critical flags.
|
| 12 |
+
2. **Contraindication Guardian**: Cross-references medications and allergies against proposed treatments.
|
| 13 |
+
3. **Lab Interpreter**: Highlights critical abnormalities and their immediate clinical significance.
|
| 14 |
+
4. **Differential Diagnosis Agent**: Formulates the top 3 emergency differentials based strictly on evidence.
|
| 15 |
+
5. **Action Protocol Agent**: Suggests the immediate next steps and patient disposition.
|
| 16 |
+
|
| 17 |
+
*(A deterministic rules engine acts as a permanent fallback for safety, ensuring an emergency brief is always generated even if the AI is unavailable).*
|
| 18 |
+
|
| 19 |
+
## Features
|
| 20 |
|
| 21 |
+
- **MCP Integration**: Fully exposes an MCP Tool Manifest (`/mcp/tools`) making it compliant with PromptOpinion and other MCP-based systems.
|
| 22 |
+
- **Explainability**: Every AI reasoning step is exposed. It doesn't just give an answer; it shows *why*.
|
| 23 |
+
- **Live Treatment Checker**: Instantly type in a proposed drug (e.g., "morphine") and cross-check it against the patient's entire profile.
|
| 24 |
+
- **Medical-Grade UI**: A highly polished, dark-themed, glass-morphic React dashboard with pulse animations for critical alerts.
|
| 25 |
+
|
| 26 |
+
## Run Locally
|
| 27 |
+
|
| 28 |
+
### Requirements
|
| 29 |
+
- Python 3.9+
|
| 30 |
+
- Node.js 18+
|
| 31 |
+
|
| 32 |
+
### 1. Backend Setup
|
| 33 |
+
```bash
|
| 34 |
+
python -m venv .venv
|
| 35 |
+
source .venv/bin/activate
|
| 36 |
+
pip install -r requirements.txt
|
| 37 |
+
```
|
| 38 |
+
Set your API key in a `.env` file in the root directory:
|
| 39 |
+
```
|
| 40 |
+
gemini_api_key=YOUR_GEMINI_KEY
|
| 41 |
+
```
|
| 42 |
+
Start the backend:
|
| 43 |
+
```bash
|
| 44 |
+
uvicorn backend.main:app --reload
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### 2. Frontend Setup
|
| 48 |
+
```bash
|
| 49 |
+
cd frontend
|
| 50 |
+
npm install
|
| 51 |
+
npm run dev
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## Hackathon Judging Criteria Met
|
| 55 |
+
|
| 56 |
+
- **Real-world Usefulness:** Directly addresses emergency chart review bottlenecks.
|
| 57 |
+
- **Safety:** Hard deterministic fallbacks. The system is instructed to *never* hallucinate data.
|
| 58 |
+
- **Explainability:** Features a dedicated panel showing the "reasoning steps" the AI took.
|
| 59 |
+
- **Simplicity:** Drag-and-drop a patient JSON file, click "Generate Snapshot".
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
*Disclaimer: For demonstration purposes only. Not for actual clinical use.*
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
backend/main.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import json, os, re
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
# Load .env from project root
|
| 7 |
+
_env_file = Path(__file__).resolve().parent.parent / ".env"
|
| 8 |
+
if _env_file.exists():
|
| 9 |
+
for _line in _env_file.read_text().splitlines():
|
| 10 |
+
_line = _line.strip()
|
| 11 |
+
if _line and not _line.startswith("#") and "=" in _line:
|
| 12 |
+
_k, _, _v = _line.partition("=")
|
| 13 |
+
os.environ.setdefault(_k.strip(), _v.strip())
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
from fastapi import FastAPI, HTTPException, UploadFile, File, Body
|
| 17 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 18 |
+
from fastapi.responses import JSONResponse, FileResponse
|
| 19 |
+
from fastapi.staticfiles import StaticFiles
|
| 20 |
+
|
| 21 |
+
from codeblue_context import build_emergency_snapshot
|
| 22 |
+
|
| 23 |
+
import requests as _requests
|
| 24 |
+
|
| 25 |
+
_KEY = ""
|
| 26 |
+
_env_file2 = Path(__file__).resolve().parent.parent / ".env"
|
| 27 |
+
if _env_file2.exists():
|
| 28 |
+
for _l in _env_file2.read_text().splitlines():
|
| 29 |
+
_l = _l.strip()
|
| 30 |
+
if _l and "=" in _l and not _l.startswith("#"):
|
| 31 |
+
_k2, _, _v2 = _l.partition("=")
|
| 32 |
+
if _k2.strip() == "gemini_api_key":
|
| 33 |
+
_KEY = _v2.strip()
|
| 34 |
+
break
|
| 35 |
+
if not _KEY:
|
| 36 |
+
_KEY = os.getenv("gemini_api_key", "") or os.getenv("GEMINI_API_KEY", "")
|
| 37 |
+
|
| 38 |
+
_GEMINI_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={_KEY}"
|
| 39 |
+
|
| 40 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _load_patients() -> List[Dict[str, Any]]:
|
| 44 |
+
p = BASE_DIR / "sample_patients.json"
|
| 45 |
+
if not p.exists():
|
| 46 |
+
return []
|
| 47 |
+
with p.open() as f:
|
| 48 |
+
return json.load(f).get("patients", [])
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
PATIENTS: List[Dict[str, Any]] = _load_patients()
|
| 52 |
+
|
| 53 |
+
app = FastAPI(title="CodeBlue Context API", version="3.0.0",
|
| 54 |
+
description="5-Agent Emergency Clinical Intelligence System")
|
| 55 |
+
|
| 56 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"],
|
| 57 |
+
allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ── Gemini helper ─────────────────────────────────────────────────────────────
|
| 61 |
+
import time
|
| 62 |
+
def _gemini(prompt: str, retries: int = 4) -> Optional[Dict]:
|
| 63 |
+
if not _KEY:
|
| 64 |
+
return None
|
| 65 |
+
payload = {
|
| 66 |
+
"contents": [{"parts": [{"text": prompt}]}],
|
| 67 |
+
"generationConfig": {"temperature": 0.1, "maxOutputTokens": 1024}
|
| 68 |
+
}
|
| 69 |
+
for attempt in range(retries):
|
| 70 |
+
try:
|
| 71 |
+
resp = _requests.post(_GEMINI_URL, json=payload, timeout=20)
|
| 72 |
+
if resp.status_code == 429 and attempt < retries - 1:
|
| 73 |
+
time.sleep(2 ** attempt)
|
| 74 |
+
continue
|
| 75 |
+
resp.raise_for_status()
|
| 76 |
+
data = resp.json()
|
| 77 |
+
text = data["candidates"][0]["content"]["parts"][0]["text"].strip()
|
| 78 |
+
# Strip code fences if present
|
| 79 |
+
text = re.sub(r"^```(?:json)?\s*", "", text)
|
| 80 |
+
text = re.sub(r"\s*```$", "", text)
|
| 81 |
+
return json.loads(text)
|
| 82 |
+
except Exception as e:
|
| 83 |
+
if attempt < retries - 1:
|
| 84 |
+
time.sleep(2 ** attempt)
|
| 85 |
+
continue
|
| 86 |
+
print(f"Gemini error after {retries} attempts: {e}")
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ── Agent 1: Triage ───────────────────────────────────────────────────────────
|
| 91 |
+
def agent_triage(record: Dict) -> Dict:
|
| 92 |
+
prompt = f"""You are an emergency triage nurse AI. Analyze ONLY the provided patient data. Do NOT invent facts.
|
| 93 |
+
|
| 94 |
+
PATIENT DATA:
|
| 95 |
+
{json.dumps(record, indent=2)}
|
| 96 |
+
|
| 97 |
+
Return ONLY raw JSON (no markdown fences, no explanation):
|
| 98 |
+
{{
|
| 99 |
+
"severity": 1,
|
| 100 |
+
"severity_label": "IMMEDIATE",
|
| 101 |
+
"chief_complaint": "brief one-line presenting issue",
|
| 102 |
+
"time_critical_flags": ["finding1", "finding2"],
|
| 103 |
+
"do_not_delay": ["action in first 5 min"],
|
| 104 |
+
"triage_rationale": "2-3 sentence rationale"
|
| 105 |
+
}}
|
| 106 |
+
Severity scale: 1=IMMEDIATE 2=EMERGENT 3=URGENT 4=LESS URGENT 5=NON-URGENT"""
|
| 107 |
+
r = _gemini(prompt)
|
| 108 |
+
if r:
|
| 109 |
+
return r
|
| 110 |
+
# Deterministic fallback
|
| 111 |
+
conditions = record.get("conditions", [])
|
| 112 |
+
vitals = record.get("vitals", {})
|
| 113 |
+
spo2 = vitals.get("SpO2") or vitals.get("spo2", 100)
|
| 114 |
+
hr = vitals.get("heart_rate") or vitals.get("hr", 80)
|
| 115 |
+
try:
|
| 116 |
+
spo2 = float(spo2); hr = float(hr)
|
| 117 |
+
except Exception:
|
| 118 |
+
spo2 = 100; hr = 80
|
| 119 |
+
severity = 3
|
| 120 |
+
if spo2 < 90 or hr > 130 or hr < 45:
|
| 121 |
+
severity = 1
|
| 122 |
+
elif spo2 < 93 or hr > 110:
|
| 123 |
+
severity = 2
|
| 124 |
+
labels = {1: "IMMEDIATE", 2: "EMERGENT", 3: "URGENT", 4: "LESS URGENT", 5: "NON-URGENT"}
|
| 125 |
+
return {
|
| 126 |
+
"severity": severity,
|
| 127 |
+
"severity_label": labels[severity],
|
| 128 |
+
"chief_complaint": conditions[0] if conditions else "Emergency presentation",
|
| 129 |
+
"time_critical_flags": [],
|
| 130 |
+
"do_not_delay": ["Obtain IV access", "Continuous monitoring"],
|
| 131 |
+
"triage_rationale": "Triage AI unavailable. Clinical judgment required.",
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ── Agent 2: Contraindication Guardian ───────────────────────────────────────
|
| 136 |
+
def agent_contraindication(record: Dict, proposed_treatment: str = "") -> Dict:
|
| 137 |
+
treat_line = f"PROPOSED TREATMENT TO CHECK: {proposed_treatment}" if proposed_treatment else "Check all existing medications for interactions."
|
| 138 |
+
prompt = f"""You are a clinical pharmacist AI specializing in emergency contraindication detection.
|
| 139 |
+
Analyze ONLY the documented data. Never invent facts.
|
| 140 |
+
|
| 141 |
+
ALLERGIES: {json.dumps(record.get('allergies', []))}
|
| 142 |
+
CURRENT MEDICATIONS: {json.dumps(record.get('medications', []))}
|
| 143 |
+
CONDITIONS: {json.dumps(record.get('conditions', []))}
|
| 144 |
+
LABS: {json.dumps(record.get('labs', []))}
|
| 145 |
+
{treat_line}
|
| 146 |
+
|
| 147 |
+
Return ONLY raw JSON:
|
| 148 |
+
{{
|
| 149 |
+
"treatment_verdict": "SAFE",
|
| 150 |
+
"verdict_reason": "one-line verdict explanation",
|
| 151 |
+
"allergy_risks": [{{"substance": "", "risk": "", "severity": "HIGH"}}],
|
| 152 |
+
"drug_interactions": [{{"drug_a": "", "drug_b": "", "interaction": "", "severity": "HIGH"}}],
|
| 153 |
+
"condition_contraindications": ["condition-based risk"],
|
| 154 |
+
"safe_alternatives": ["alternative drug or approach"],
|
| 155 |
+
"monitoring_required": ["monitor SpO2 every 5 min"]
|
| 156 |
+
}}
|
| 157 |
+
treatment_verdict options: SAFE | CAUTION | CONTRAINDICATED | NOT_EVALUATED"""
|
| 158 |
+
r = _gemini(prompt)
|
| 159 |
+
if r:
|
| 160 |
+
return r
|
| 161 |
+
return {
|
| 162 |
+
"treatment_verdict": "NOT_EVALUATED",
|
| 163 |
+
"verdict_reason": "AI agent unavailable. Manual pharmacist review required.",
|
| 164 |
+
"allergy_risks": [],
|
| 165 |
+
"drug_interactions": [],
|
| 166 |
+
"condition_contraindications": [],
|
| 167 |
+
"safe_alternatives": [],
|
| 168 |
+
"monitoring_required": [],
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ── Agent 3: Lab Interpreter ──────────────────────────────────────────────────
|
| 173 |
+
def agent_labs(record: Dict) -> Dict:
|
| 174 |
+
labs = record.get("labs", [])
|
| 175 |
+
if not labs:
|
| 176 |
+
return {"critical_labs": [], "abnormal_labs": [], "lab_patterns": [], "immediate_concerns": []}
|
| 177 |
+
prompt = f"""You are a clinical pathologist AI. Interpret ONLY these labs in emergency context.
|
| 178 |
+
|
| 179 |
+
LABS: {json.dumps(labs)}
|
| 180 |
+
PATIENT AGE: {record.get('patient', {}).get('age')}
|
| 181 |
+
CONDITIONS: {json.dumps(record.get('conditions', []))}
|
| 182 |
+
|
| 183 |
+
Return ONLY raw JSON:
|
| 184 |
+
{{
|
| 185 |
+
"critical_labs": [{{"name": "", "value": "", "significance": "", "immediate_action": ""}}],
|
| 186 |
+
"abnormal_labs": [{{"name": "", "value": "", "significance": ""}}],
|
| 187 |
+
"lab_patterns": ["pattern across multiple results"],
|
| 188 |
+
"immediate_concerns": ["concern requiring action now"]
|
| 189 |
+
}}"""
|
| 190 |
+
r = _gemini(prompt)
|
| 191 |
+
if r:
|
| 192 |
+
return r
|
| 193 |
+
return {"critical_labs": [], "abnormal_labs": [], "lab_patterns": [], "immediate_concerns": []}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# ── Agent 4: Differential Diagnosis ──────────────────────────────────────────
|
| 197 |
+
def agent_differentials(record: Dict) -> Dict:
|
| 198 |
+
prompt = f"""You are an emergency physician AI. Suggest top differentials based ONLY on documented findings.
|
| 199 |
+
|
| 200 |
+
VITALS: {json.dumps(record.get('vitals', {}))}
|
| 201 |
+
CONDITIONS: {json.dumps(record.get('conditions', []))}
|
| 202 |
+
CLINICAL NOTES: {json.dumps(record.get('clinical_notes', []))}
|
| 203 |
+
LABS: {json.dumps(record.get('labs', []))}
|
| 204 |
+
HISTORY: {json.dumps(record.get('history', []))}
|
| 205 |
+
|
| 206 |
+
Return ONLY raw JSON (max 3 differentials):
|
| 207 |
+
{{
|
| 208 |
+
"differentials": [
|
| 209 |
+
{{
|
| 210 |
+
"diagnosis": "",
|
| 211 |
+
"likelihood": "HIGH",
|
| 212 |
+
"supporting_evidence": ["evidence from patient data"],
|
| 213 |
+
"must_rule_out": true
|
| 214 |
+
}}
|
| 215 |
+
],
|
| 216 |
+
"key_diagnostic_question": "single most important question to resolve uncertainty"
|
| 217 |
+
}}"""
|
| 218 |
+
r = _gemini(prompt)
|
| 219 |
+
if r:
|
| 220 |
+
return r
|
| 221 |
+
return {"differentials": [], "key_diagnostic_question": "Full clinical assessment required."}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# ── Agent 5: Action Protocol ──────────────────────────────────────────────────
|
| 225 |
+
def agent_action_protocol(record: Dict, triage: Dict, contraindication: Dict,
|
| 226 |
+
labs: Dict, differentials: Dict) -> Dict:
|
| 227 |
+
prompt = f"""You are an ED attending physician AI. Generate immediate action protocol for first 30 minutes.
|
| 228 |
+
|
| 229 |
+
TRIAGE RESULT: {json.dumps(triage)}
|
| 230 |
+
CONTRAINDICATIONS: {json.dumps(contraindication)}
|
| 231 |
+
LAB CONCERNS: {json.dumps(labs)}
|
| 232 |
+
DIFFERENTIALS: {json.dumps(differentials)}
|
| 233 |
+
PATIENT: Age {record.get('patient', {}).get('age')}, {record.get('patient', {}).get('gender')}
|
| 234 |
+
ALLERGIES: {json.dumps(record.get('allergies', []))}
|
| 235 |
+
|
| 236 |
+
Return ONLY raw JSON:
|
| 237 |
+
{{
|
| 238 |
+
"immediate_actions": ["Step 1: action", "Step 2: action"],
|
| 239 |
+
"drugs_to_avoid": [{{"drug": "", "reason": ""}}],
|
| 240 |
+
"monitoring_parameters": ["parameter every X min"],
|
| 241 |
+
"escalation_triggers": ["trigger requiring escalation"],
|
| 242 |
+
"disposition": "ICU"
|
| 243 |
+
}}
|
| 244 |
+
disposition options: ICU | ADMIT | OBSERVE | DISCHARGE | SURGERY_CONSULT | CATH_LAB"""
|
| 245 |
+
r = _gemini(prompt)
|
| 246 |
+
if r:
|
| 247 |
+
return r
|
| 248 |
+
return {
|
| 249 |
+
"immediate_actions": ["Obtain IV access", "Continuous monitoring", "Notify attending physician"],
|
| 250 |
+
"drugs_to_avoid": [],
|
| 251 |
+
"monitoring_parameters": ["Continuous SpO2", "BP every 5 min"],
|
| 252 |
+
"escalation_triggers": ["Deteriorating vitals"],
|
| 253 |
+
"disposition": "ADMIT",
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# ── Risk pre-classification (fast, no AI) ─────────────────────────────────────
|
| 258 |
+
def _quick_risk(record: Dict) -> str:
|
| 259 |
+
vitals = record.get("vitals", {})
|
| 260 |
+
spo2 = vitals.get("SpO2") or vitals.get("spo2", 100)
|
| 261 |
+
hr = vitals.get("heart_rate") or vitals.get("hr", 80)
|
| 262 |
+
try:
|
| 263 |
+
spo2 = float(spo2); hr = float(hr)
|
| 264 |
+
except Exception:
|
| 265 |
+
spo2 = 100; hr = 80
|
| 266 |
+
for lab in record.get("labs", []):
|
| 267 |
+
if isinstance(lab, dict) and "critical" in str(lab.get("flag", "")).lower():
|
| 268 |
+
return "critical"
|
| 269 |
+
if spo2 < 90 or hr > 130 or hr < 45:
|
| 270 |
+
return "critical"
|
| 271 |
+
if spo2 < 94 or hr > 110:
|
| 272 |
+
return "moderate"
|
| 273 |
+
if record.get("conditions") or record.get("medications"):
|
| 274 |
+
return "moderate"
|
| 275 |
+
return "stable"
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _patient_card(record: Dict) -> Dict:
|
| 279 |
+
p = record.get("patient", {})
|
| 280 |
+
return {
|
| 281 |
+
"id": record.get("id"),
|
| 282 |
+
"display_name": record.get("display_name", record.get("id")),
|
| 283 |
+
"age": p.get("age"),
|
| 284 |
+
"gender": p.get("gender"),
|
| 285 |
+
"risk_level": _quick_risk(record),
|
| 286 |
+
"conditions": record.get("conditions", [])[:2],
|
| 287 |
+
"allergy_count": len(record.get("allergies", [])),
|
| 288 |
+
"med_count": len(record.get("medications", [])),
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
# ── Routes ────────────────────────────────────────────────────────────────────
|
| 293 |
+
|
| 294 |
+
@app.get("/health")
|
| 295 |
+
def health():
|
| 296 |
+
return {
|
| 297 |
+
"status": "ok", "version": "3.0.0",
|
| 298 |
+
"patient_count": len(PATIENTS),
|
| 299 |
+
"gemini_enabled": bool(_KEY),
|
| 300 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
@app.get("/patients")
|
| 305 |
+
def list_patients():
|
| 306 |
+
return {"patients": [_patient_card(p) for p in PATIENTS], "total": len(PATIENTS)}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@app.get("/patients/{patient_id}")
|
| 310 |
+
def get_patient(patient_id: str):
|
| 311 |
+
for p in PATIENTS:
|
| 312 |
+
if p.get("id") == patient_id:
|
| 313 |
+
return p
|
| 314 |
+
raise HTTPException(404, f"Patient '{patient_id}' not found")
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
@app.post("/patients/upload")
|
| 318 |
+
async def upload_patient(file: UploadFile = File(...)):
|
| 319 |
+
raw = await file.read()
|
| 320 |
+
try:
|
| 321 |
+
data = json.loads(raw)
|
| 322 |
+
except Exception:
|
| 323 |
+
raise HTTPException(400, "Invalid JSON")
|
| 324 |
+
records = data if isinstance(data, list) else [data]
|
| 325 |
+
added = []
|
| 326 |
+
for rec in records:
|
| 327 |
+
if not isinstance(rec, dict):
|
| 328 |
+
continue
|
| 329 |
+
if not rec.get("id"):
|
| 330 |
+
rec["id"] = f"UP-{len(PATIENTS)+1:03d}"
|
| 331 |
+
if not rec.get("display_name"):
|
| 332 |
+
rec["display_name"] = f"Patient {rec['id']}"
|
| 333 |
+
PATIENTS.append(rec)
|
| 334 |
+
added.append(_patient_card(rec))
|
| 335 |
+
return {"added": added, "total_patients": len(PATIENTS)}
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
# ── CORE: 5-Agent Orchestrator ────────────────────────────────────────────────
|
| 339 |
+
|
| 340 |
+
@app.get("/orchestrate/{patient_id}")
|
| 341 |
+
@app.post("/orchestrate/{patient_id}")
|
| 342 |
+
def orchestrate(patient_id: str):
|
| 343 |
+
"""
|
| 344 |
+
Runs all 5 AI agents on the patient record and returns a complete
|
| 345 |
+
emergency intelligence brief.
|
| 346 |
+
"""
|
| 347 |
+
record = next((p for p in PATIENTS if p.get("id") == patient_id), None)
|
| 348 |
+
if not record:
|
| 349 |
+
raise HTTPException(404, f"Patient '{patient_id}' not found")
|
| 350 |
+
|
| 351 |
+
t0 = datetime.utcnow()
|
| 352 |
+
|
| 353 |
+
# Deterministic core (always runs, never fails)
|
| 354 |
+
det_snapshot, det_reasoning, det_risks, det_vitals = build_emergency_snapshot(record)
|
| 355 |
+
|
| 356 |
+
# 5 AI Agents (sequential; Gemini free-tier safe)
|
| 357 |
+
import time
|
| 358 |
+
|
| 359 |
+
triage = agent_triage(record)
|
| 360 |
+
time.sleep(2)
|
| 361 |
+
contra = agent_contraindication(record)
|
| 362 |
+
time.sleep(2)
|
| 363 |
+
labs = agent_labs(record)
|
| 364 |
+
time.sleep(2)
|
| 365 |
+
differentials = agent_differentials(record)
|
| 366 |
+
time.sleep(2)
|
| 367 |
+
action = agent_action_protocol(record, triage, contra, labs, differentials)
|
| 368 |
+
|
| 369 |
+
ms = int((datetime.utcnow() - t0).total_seconds() * 1000)
|
| 370 |
+
|
| 371 |
+
return {
|
| 372 |
+
"patient": _patient_card(record),
|
| 373 |
+
"agents": {
|
| 374 |
+
"triage": triage,
|
| 375 |
+
"contraindication": contra,
|
| 376 |
+
"labs": labs,
|
| 377 |
+
"differentials": differentials,
|
| 378 |
+
"action_protocol": action,
|
| 379 |
+
},
|
| 380 |
+
"deterministic": {
|
| 381 |
+
"snapshot": det_snapshot,
|
| 382 |
+
"risks": det_risks,
|
| 383 |
+
"vitals": det_vitals,
|
| 384 |
+
"reasoning_steps": det_reasoning,
|
| 385 |
+
},
|
| 386 |
+
"meta": {
|
| 387 |
+
"processing_ms": ms,
|
| 388 |
+
"gemini_enabled": bool(_KEY),
|
| 389 |
+
"generated_at": datetime.utcnow().isoformat() + "Z",
|
| 390 |
+
},
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
# ── Treatment Safety Check ────────────────────────────────────────────────────
|
| 395 |
+
|
| 396 |
+
@app.post("/check-treatment/{patient_id}")
|
| 397 |
+
def check_treatment(patient_id: str, body: Dict[str, Any] = Body(...)):
|
| 398 |
+
"""
|
| 399 |
+
Instant contraindication check for a proposed treatment.
|
| 400 |
+
Body: { "treatment": "morphine 4mg IV" }
|
| 401 |
+
"""
|
| 402 |
+
record = next((p for p in PATIENTS if p.get("id") == patient_id), None)
|
| 403 |
+
if not record:
|
| 404 |
+
raise HTTPException(404, f"Patient '{patient_id}' not found")
|
| 405 |
+
treatment = body.get("treatment", "")
|
| 406 |
+
if not treatment:
|
| 407 |
+
raise HTTPException(400, "treatment field required")
|
| 408 |
+
result = agent_contraindication(record, treatment)
|
| 409 |
+
return {"patient_id": patient_id, "proposed_treatment": treatment, "result": result}
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
# ── MCP Tool Manifest ─────────────────────────────────────────────────────────
|
| 413 |
+
|
| 414 |
+
@app.get("/mcp/tools")
|
| 415 |
+
def mcp_tools():
|
| 416 |
+
return {
|
| 417 |
+
"tools": [
|
| 418 |
+
{
|
| 419 |
+
"name": "list_patients",
|
| 420 |
+
"description": "List all patients in the CodeBlue system with risk levels.",
|
| 421 |
+
"inputSchema": {"type": "object", "properties": {}},
|
| 422 |
+
},
|
| 423 |
+
{
|
| 424 |
+
"name": "get_patient_details",
|
| 425 |
+
"description": "Get full clinical record for a specific patient.",
|
| 426 |
+
"inputSchema": {
|
| 427 |
+
"type": "object",
|
| 428 |
+
"properties": {"patient_id": {"type": "string"}},
|
| 429 |
+
"required": ["patient_id"],
|
| 430 |
+
},
|
| 431 |
+
},
|
| 432 |
+
{
|
| 433 |
+
"name": "generate_emergency_snapshot",
|
| 434 |
+
"description": (
|
| 435 |
+
"Run 5-agent emergency AI analysis: triage, contraindication, "
|
| 436 |
+
"lab interpretation, differential diagnosis, and action protocol."
|
| 437 |
+
),
|
| 438 |
+
"inputSchema": {
|
| 439 |
+
"type": "object",
|
| 440 |
+
"properties": {"patient_id": {"type": "string"}},
|
| 441 |
+
"required": ["patient_id"],
|
| 442 |
+
},
|
| 443 |
+
},
|
| 444 |
+
{
|
| 445 |
+
"name": "check_treatment_safety",
|
| 446 |
+
"description": "Check if a proposed treatment is safe for a specific patient given their allergies, medications, and conditions.",
|
| 447 |
+
"inputSchema": {
|
| 448 |
+
"type": "object",
|
| 449 |
+
"properties": {
|
| 450 |
+
"patient_id": {"type": "string"},
|
| 451 |
+
"treatment": {"type": "string", "description": "e.g. 'morphine 4mg IV'"},
|
| 452 |
+
},
|
| 453 |
+
"required": ["patient_id", "treatment"],
|
| 454 |
+
},
|
| 455 |
+
},
|
| 456 |
+
]
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
@app.post("/mcp/execute")
|
| 461 |
+
async def mcp_execute(body: Dict[str, Any] = Body(...)):
|
| 462 |
+
tool = body.get("tool_name") or body.get("name", "")
|
| 463 |
+
params = body.get("parameters") or body.get("input") or {}
|
| 464 |
+
|
| 465 |
+
if tool == "list_patients":
|
| 466 |
+
return {"result": list_patients()}
|
| 467 |
+
elif tool == "get_patient_details":
|
| 468 |
+
pid = params.get("patient_id", "")
|
| 469 |
+
return {"result": get_patient(pid)}
|
| 470 |
+
elif tool == "generate_emergency_snapshot":
|
| 471 |
+
pid = params.get("patient_id", "")
|
| 472 |
+
return {"result": orchestrate(pid)}
|
| 473 |
+
elif tool == "check_treatment_safety":
|
| 474 |
+
pid = params.get("patient_id", "")
|
| 475 |
+
return {"result": check_treatment(pid, {"treatment": params.get("treatment", "")})}
|
| 476 |
+
else:
|
| 477 |
+
raise HTTPException(400, f"Unknown tool: {tool}")
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
# ── Static frontend ───────────────────────────────────────────────────────────
|
| 481 |
+
_static = BASE_DIR / "static"
|
| 482 |
+
_assets = _static / "assets"
|
| 483 |
+
if _assets.exists():
|
| 484 |
+
app.mount("/assets", StaticFiles(directory=str(_assets)), name="assets")
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
@app.get("/{full_path:path}")
|
| 488 |
+
async def serve_spa(full_path: str):
|
| 489 |
+
idx = _static / "index.html"
|
| 490 |
+
if idx.exists():
|
| 491 |
+
return FileResponse(str(idx))
|
| 492 |
+
return JSONResponse({"status": "CodeBlue Context API running", "docs": "/docs"})
|
backend/sample_patients.json
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"patients": [
|
| 3 |
+
{
|
| 4 |
+
"id": "CB-001",
|
| 5 |
+
"display_name": "James Harrington",
|
| 6 |
+
"patient": {
|
| 7 |
+
"age": 65,
|
| 8 |
+
"gender": "male",
|
| 9 |
+
"dob": "1959-03-12"
|
| 10 |
+
},
|
| 11 |
+
"allergies": [
|
| 12 |
+
{
|
| 13 |
+
"substance": "Penicillin",
|
| 14 |
+
"severity": "severe",
|
| 15 |
+
"reaction": "anaphylaxis"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"substance": "Aspirin",
|
| 19 |
+
"severity": "moderate",
|
| 20 |
+
"reaction": "urticaria"
|
| 21 |
+
}
|
| 22 |
+
],
|
| 23 |
+
"medications": [
|
| 24 |
+
"Warfarin 5mg daily",
|
| 25 |
+
"Insulin glargine 20 units nightly",
|
| 26 |
+
"Metoprolol 50mg twice daily",
|
| 27 |
+
"Lisinopril 10mg daily"
|
| 28 |
+
],
|
| 29 |
+
"conditions": [
|
| 30 |
+
"Type 2 Diabetes",
|
| 31 |
+
"Coronary artery disease",
|
| 32 |
+
"Hypertension",
|
| 33 |
+
"Atrial fibrillation"
|
| 34 |
+
],
|
| 35 |
+
"labs": [
|
| 36 |
+
{
|
| 37 |
+
"name": "INR",
|
| 38 |
+
"value": 4.2,
|
| 39 |
+
"reference_range": { "low": 0.8, "high": 1.2 },
|
| 40 |
+
"flag": "critical high"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"name": "Glucose",
|
| 44 |
+
"value": 58,
|
| 45 |
+
"unit": "mg/dL",
|
| 46 |
+
"reference_range": { "low": 70, "high": 140 },
|
| 47 |
+
"flag": "low"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"name": "Troponin",
|
| 51 |
+
"value": 0.08,
|
| 52 |
+
"unit": "ng/mL",
|
| 53 |
+
"reference_range": { "low": 0, "high": 0.04 },
|
| 54 |
+
"flag": "critical high"
|
| 55 |
+
}
|
| 56 |
+
],
|
| 57 |
+
"vitals": {
|
| 58 |
+
"heart_rate": 138,
|
| 59 |
+
"SpO2": 88,
|
| 60 |
+
"blood_pressure": "188/124",
|
| 61 |
+
"temperature": 37.2,
|
| 62 |
+
"respiratory_rate": 22
|
| 63 |
+
},
|
| 64 |
+
"history": [
|
| 65 |
+
"Recent fall at home 3 days ago",
|
| 66 |
+
"ICU admission last year for cardiac event",
|
| 67 |
+
"CABG surgery 2019"
|
| 68 |
+
],
|
| 69 |
+
"clinical_notes": [
|
| 70 |
+
"High-risk emergency profile due to supratherapeutic anticoagulation and unstable vitals. HR: 138, SpO2: 88%, BP: 188/124. Patient presents with chest discomfort and confusion."
|
| 71 |
+
]
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"id": "CB-002",
|
| 75 |
+
"display_name": "Maria Santos",
|
| 76 |
+
"patient": {
|
| 77 |
+
"age": 47,
|
| 78 |
+
"gender": "female",
|
| 79 |
+
"dob": "1977-08-22"
|
| 80 |
+
},
|
| 81 |
+
"allergies": [],
|
| 82 |
+
"medications": [
|
| 83 |
+
"Albuterol inhaler PRN",
|
| 84 |
+
"Montelukast 10mg daily",
|
| 85 |
+
"Prednisone 20mg short course"
|
| 86 |
+
],
|
| 87 |
+
"conditions": [
|
| 88 |
+
"Asthma",
|
| 89 |
+
"Allergic rhinitis"
|
| 90 |
+
],
|
| 91 |
+
"labs": [
|
| 92 |
+
{
|
| 93 |
+
"name": "WBC",
|
| 94 |
+
"value": 12.4,
|
| 95 |
+
"unit": "K/uL",
|
| 96 |
+
"reference_range": { "low": 4.5, "high": 11.0 },
|
| 97 |
+
"flag": "high"
|
| 98 |
+
}
|
| 99 |
+
],
|
| 100 |
+
"vitals": {
|
| 101 |
+
"respiratory_rate": 26,
|
| 102 |
+
"SpO2": 93,
|
| 103 |
+
"temperature": 38.6,
|
| 104 |
+
"heart_rate": 108,
|
| 105 |
+
"blood_pressure": "128/82"
|
| 106 |
+
},
|
| 107 |
+
"history": [
|
| 108 |
+
"Two ER visits for asthma exacerbation this year",
|
| 109 |
+
"No ICU history"
|
| 110 |
+
],
|
| 111 |
+
"clinical_notes": [
|
| 112 |
+
"Patient presenting with acute bronchospasm. RR: 26, SpO2: 93%, Temp: 38.6C. Audible wheezing bilaterally. Not responding to initial albuterol."
|
| 113 |
+
]
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"id": "CB-003",
|
| 117 |
+
"display_name": "Robert Chen",
|
| 118 |
+
"patient": {
|
| 119 |
+
"age": 72,
|
| 120 |
+
"gender": "male",
|
| 121 |
+
"dob": "1952-11-05"
|
| 122 |
+
},
|
| 123 |
+
"allergies": [
|
| 124 |
+
{
|
| 125 |
+
"substance": "Iodinated contrast",
|
| 126 |
+
"severity": "moderate",
|
| 127 |
+
"reaction": "acute kidney injury"
|
| 128 |
+
}
|
| 129 |
+
],
|
| 130 |
+
"medications": [
|
| 131 |
+
"Furosemide 40mg daily",
|
| 132 |
+
"Digoxin 0.125mg daily",
|
| 133 |
+
"Carvedilol 6.25mg twice daily",
|
| 134 |
+
"Spironolactone 25mg"
|
| 135 |
+
],
|
| 136 |
+
"conditions": [
|
| 137 |
+
"Congestive heart failure (EF 25%)",
|
| 138 |
+
"Chronic kidney disease Stage 3",
|
| 139 |
+
"Hyperkalemia history"
|
| 140 |
+
],
|
| 141 |
+
"labs": [
|
| 142 |
+
{
|
| 143 |
+
"name": "Potassium",
|
| 144 |
+
"value": 5.9,
|
| 145 |
+
"unit": "mmol/L",
|
| 146 |
+
"reference_range": { "low": 3.5, "high": 5.1 },
|
| 147 |
+
"flag": "high"
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"name": "BNP",
|
| 151 |
+
"value": 1420,
|
| 152 |
+
"unit": "pg/mL",
|
| 153 |
+
"reference_range": { "low": 0, "high": 100 },
|
| 154 |
+
"flag": "critical high"
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"name": "Creatinine",
|
| 158 |
+
"value": 2.4,
|
| 159 |
+
"unit": "mg/dL",
|
| 160 |
+
"reference_range": { "low": 0.7, "high": 1.3 },
|
| 161 |
+
"flag": "high"
|
| 162 |
+
}
|
| 163 |
+
],
|
| 164 |
+
"vitals": {
|
| 165 |
+
"heart_rate": 58,
|
| 166 |
+
"blood_pressure": "95/62",
|
| 167 |
+
"SpO2": 91,
|
| 168 |
+
"respiratory_rate": 24,
|
| 169 |
+
"temperature": 36.8
|
| 170 |
+
},
|
| 171 |
+
"history": [
|
| 172 |
+
"Discharged 5 days ago after decompensated heart failure",
|
| 173 |
+
"3 prior hospitalizations for CHF this year",
|
| 174 |
+
"Dialysis evaluation pending"
|
| 175 |
+
],
|
| 176 |
+
"clinical_notes": [
|
| 177 |
+
"Presenting with worsening dyspnea and bilateral lower extremity edema. HR: 58 (bradycardic), BP: 95/62 (hypotensive). Digoxin toxicity must be ruled out given bradycardia + hyperkalemia."
|
| 178 |
+
]
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"id": "CB-004",
|
| 182 |
+
"display_name": "Tyler Brooks",
|
| 183 |
+
"patient": {
|
| 184 |
+
"age": 34,
|
| 185 |
+
"gender": "male",
|
| 186 |
+
"dob": "1990-06-18"
|
| 187 |
+
},
|
| 188 |
+
"allergies": [
|
| 189 |
+
{
|
| 190 |
+
"substance": "Codeine",
|
| 191 |
+
"severity": "moderate",
|
| 192 |
+
"reaction": "respiratory depression"
|
| 193 |
+
}
|
| 194 |
+
],
|
| 195 |
+
"medications": [
|
| 196 |
+
"Oxycodone 10mg PRN",
|
| 197 |
+
"Cyclobenzaprine 5mg",
|
| 198 |
+
"Fentanyl patch 25mcg/hr"
|
| 199 |
+
],
|
| 200 |
+
"conditions": [
|
| 201 |
+
"Chronic back pain",
|
| 202 |
+
"Post-traumatic stress disorder",
|
| 203 |
+
"Mild traumatic brain injury"
|
| 204 |
+
],
|
| 205 |
+
"labs": [
|
| 206 |
+
{
|
| 207 |
+
"name": "Blood Alcohol",
|
| 208 |
+
"value": 0.18,
|
| 209 |
+
"unit": "g/dL",
|
| 210 |
+
"reference_range": { "low": 0, "high": 0.08 },
|
| 211 |
+
"flag": "critical high"
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"name": "GCS",
|
| 215 |
+
"value": 11,
|
| 216 |
+
"reference_range": { "low": 15, "high": 15 },
|
| 217 |
+
"flag": "low"
|
| 218 |
+
}
|
| 219 |
+
],
|
| 220 |
+
"vitals": {
|
| 221 |
+
"heart_rate": 52,
|
| 222 |
+
"blood_pressure": "88/54",
|
| 223 |
+
"SpO2": 89,
|
| 224 |
+
"respiratory_rate": 8,
|
| 225 |
+
"temperature": 35.8
|
| 226 |
+
},
|
| 227 |
+
"history": [
|
| 228 |
+
"Motor vehicle accident 2 hours ago",
|
| 229 |
+
"Known polysubstance use",
|
| 230 |
+
"Fall from motorcycle at high speed"
|
| 231 |
+
],
|
| 232 |
+
"clinical_notes": [
|
| 233 |
+
"Trauma patient post-MVA. Multiple opioids on board including fentanyl patch. HR: 52, RR: 8, SpO2: 89%, BP: 88/54 — hemodynamically unstable. GCS 11. Risk of opioid-induced respiratory depression compounding traumatic injury. BAL: 0.18."
|
| 234 |
+
]
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"id": "CB-005",
|
| 238 |
+
"display_name": "Eleanor Voss",
|
| 239 |
+
"patient": {
|
| 240 |
+
"age": 81,
|
| 241 |
+
"gender": "female",
|
| 242 |
+
"dob": "1943-02-14"
|
| 243 |
+
},
|
| 244 |
+
"allergies": [
|
| 245 |
+
{
|
| 246 |
+
"substance": "Sulfa drugs",
|
| 247 |
+
"severity": "severe",
|
| 248 |
+
"reaction": "Stevens-Johnson syndrome"
|
| 249 |
+
}
|
| 250 |
+
],
|
| 251 |
+
"medications": [
|
| 252 |
+
"Metformin 500mg twice daily",
|
| 253 |
+
"Amlodipine 5mg",
|
| 254 |
+
"Warfarin 2.5mg",
|
| 255 |
+
"Donepezil 10mg"
|
| 256 |
+
],
|
| 257 |
+
"conditions": [
|
| 258 |
+
"Type 2 Diabetes",
|
| 259 |
+
"Hypertension",
|
| 260 |
+
"Mild cognitive impairment",
|
| 261 |
+
"Chronic UTI history"
|
| 262 |
+
],
|
| 263 |
+
"labs": [
|
| 264 |
+
{
|
| 265 |
+
"name": "Lactate",
|
| 266 |
+
"value": 4.8,
|
| 267 |
+
"unit": "mmol/L",
|
| 268 |
+
"reference_range": { "low": 0.5, "high": 2.0 },
|
| 269 |
+
"flag": "critical high"
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
"name": "WBC",
|
| 273 |
+
"value": 18.2,
|
| 274 |
+
"unit": "K/uL",
|
| 275 |
+
"reference_range": { "low": 4.5, "high": 11.0 },
|
| 276 |
+
"flag": "critical high"
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"name": "Procalcitonin",
|
| 280 |
+
"value": 12.4,
|
| 281 |
+
"unit": "ng/mL",
|
| 282 |
+
"reference_range": { "low": 0, "high": 0.25 },
|
| 283 |
+
"flag": "critical high"
|
| 284 |
+
}
|
| 285 |
+
],
|
| 286 |
+
"vitals": {
|
| 287 |
+
"heart_rate": 118,
|
| 288 |
+
"blood_pressure": "86/48",
|
| 289 |
+
"SpO2": 92,
|
| 290 |
+
"respiratory_rate": 28,
|
| 291 |
+
"temperature": 39.4
|
| 292 |
+
},
|
| 293 |
+
"history": [
|
| 294 |
+
"Lives alone, found by neighbor in altered mental status",
|
| 295 |
+
"No prior sepsis history",
|
| 296 |
+
"Refused hospitalization last month for UTI"
|
| 297 |
+
],
|
| 298 |
+
"clinical_notes": [
|
| 299 |
+
"Elderly patient with septic shock. Temp: 39.4C, HR: 118, BP: 86/48, RR: 28. Lactate 4.8 (critical), WBC 18.2, Procalcitonin 12.4. Suspected urosepsis. Sulfa allergy documented — avoid TMP-SMX. Antibiotic stewardship critical."
|
| 300 |
+
]
|
| 301 |
+
}
|
| 302 |
+
]
|
| 303 |
+
}
|
backend/static/assets/index-BEynGZGH.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap";:root{--bg-base: #060d1a;--bg-surface: #0d1b2e;--bg-elevated: #112236;--bg-card: #0f1e33;--border-subtle: rgba(255,255,255,.06);--border-default: rgba(255,255,255,.1);--border-accent: rgba(10,132,255,.4);--red-critical: #ff2d55;--red-glow: rgba(255,45,85,.25);--amber-warning: #ff9f0a;--amber-glow: rgba(255,159,10,.2);--green-safe: #30d158;--green-glow: rgba(48,209,88,.2);--blue-accent: #0a84ff;--blue-glow: rgba(10,132,255,.2);--purple-ai: #bf5af2;--text-primary: #f5f5f7;--text-secondary: #8e8e93;--text-tertiary: #48484a;--font-sans: "Inter", system-ui, sans-serif;--font-mono: "JetBrains Mono", monospace}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;background:var(--bg-base);color:var(--text-primary);font-family:var(--font-sans);font-size:14px;-webkit-font-smoothing:antialiased}::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-default);border-radius:99px}@keyframes ekg{0%{stroke-dashoffset:1000}to{stroke-dashoffset:0}}@keyframes pulse-red{0%,to{opacity:1;box-shadow:0 0 0 0 var(--red-glow)}50%{opacity:.8;box-shadow:0 0 0 8px transparent}}@keyframes pulse-amber{0%,to{box-shadow:0 0 0 0 var(--amber-glow)}50%{box-shadow:0 0 0 6px transparent}}@keyframes fade-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes glow-blue{0%,to{box-shadow:0 0 12px var(--blue-glow)}50%{box-shadow:0 0 24px var(--blue-glow),0 0 4px var(--blue-accent)}}@keyframes slide-in-right{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}@keyframes heartbeat{0%,to{transform:scale(1)}14%{transform:scale(1.15)}28%{transform:scale(1)}42%{transform:scale(1.1)}70%{transform:scale(1)}}.card{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:12px}.card-elevated{background:var(--bg-elevated);border:1px solid var(--border-default);border-radius:12px}.glass{background:#0d1b2eb3;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border-default);border-radius:12px}.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:99px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.badge-critical{background:#ff2d5526;color:var(--red-critical);border:1px solid rgba(255,45,85,.3);animation:pulse-red 2s infinite}.badge-moderate{background:#ff9f0a1f;color:var(--amber-warning);border:1px solid rgba(255,159,10,.3)}.badge-stable{background:#30d1581a;color:var(--green-safe);border:1px solid rgba(48,209,88,.25)}.badge-unknown{background:#8e8e931a;color:var(--text-secondary);border:1px solid rgba(142,142,147,.2)}.verdict-safe{background:#30d15826;color:var(--green-safe);border:1px solid rgba(48,209,88,.3)}.verdict-caution{background:#ff9f0a26;color:var(--amber-warning);border:1px solid rgba(255,159,10,.3);animation:pulse-amber 2s infinite}.verdict-contraindicated{background:#ff2d5526;color:var(--red-critical);border:1px solid rgba(255,45,85,.4);animation:pulse-red 1.5s infinite}.verdict-not_evaluated{background:#8e8e931a;color:var(--text-secondary);border:1px solid rgba(142,142,147,.2)}.agent-idle{border-color:var(--border-subtle)}.agent-working{border-color:var(--blue-accent);animation:glow-blue 1.2s infinite}.agent-complete{border-color:var(--green-safe);box-shadow:0 0 10px var(--green-glow)}.agent-error{border-color:var(--red-critical)}.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 20px;border-radius:8px;border:none;cursor:pointer;font-family:var(--font-sans);font-weight:600;font-size:13px;background:var(--blue-accent);color:#fff;transition:opacity .15s,transform .1s}.btn-primary:hover:not(:disabled){opacity:.85;transform:translateY(-1px)}.btn-primary:active{transform:translateY(0)}.btn-primary:disabled{opacity:.35;cursor:not-allowed}.btn-danger{background:linear-gradient(135deg,#c0392b,var(--red-critical));color:#fff;display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:12px 24px;border-radius:8px;border:none;cursor:pointer;font-family:var(--font-sans);font-weight:700;font-size:14px;letter-spacing:.02em;box-shadow:0 4px 20px #ff2d554d;transition:box-shadow .2s,transform .1s;width:100%}.btn-danger:hover:not(:disabled){box-shadow:0 6px 30px #ff2d5580;transform:translateY(-1px)}.btn-danger:disabled{opacity:.3;cursor:not-allowed;transform:none}.btn-ghost{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:7px;border:1px solid var(--border-default);background:transparent;color:var(--text-secondary);cursor:pointer;font-family:var(--font-sans);font-size:12px;font-weight:500;transition:border-color .15s,color .15s,background .15s}.btn-ghost:hover{border-color:var(--blue-accent);color:var(--blue-accent);background:var(--blue-glow)}.spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}.fade-up{animation:fade-up .35s ease both}.fade-in{animation:fade-in .3s ease both}.slide-in{animation:slide-in-right .35s ease both}.mono{font-family:var(--font-mono)}.input-dark{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;color:var(--text-primary);padding:9px 12px;font-family:var(--font-sans);font-size:13px;width:100%;outline:none;transition:border-color .15s}.input-dark:focus{border-color:var(--blue-accent);box-shadow:0 0 0 3px #0a84ff26}.input-dark::-moz-placeholder{color:var(--text-tertiary)}.input-dark::placeholder{color:var(--text-tertiary)}.divider{height:1px;background:var(--border-subtle)}.welcome-screen{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 0%,rgba(10,132,255,.08) 0%,transparent 70%),var(--bg-base);text-align:center;padding:40px 20px}
|
backend/static/assets/index-BPTnkzQ_.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/static/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 7 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<title>CodeBlue Context</title>
|
| 10 |
+
<script type="module" crossorigin src="/assets/index-BPTnkzQ_.js"></script>
|
| 11 |
+
<link rel="stylesheet" crossorigin href="/assets/index-BEynGZGH.css">
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
<div id="root"></div>
|
| 15 |
+
</body>
|
| 16 |
+
</html>
|
codeblue_context.py
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
import sys
|
| 4 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _to_list(value: Any) -> List[Any]:
|
| 8 |
+
if value is None:
|
| 9 |
+
return []
|
| 10 |
+
if isinstance(value, list):
|
| 11 |
+
return value
|
| 12 |
+
return [value]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _first_non_empty(*values: Any) -> str:
|
| 16 |
+
for value in values:
|
| 17 |
+
if isinstance(value, str) and value.strip():
|
| 18 |
+
return value.strip()
|
| 19 |
+
return ""
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _extract_text(value: Any) -> str:
|
| 23 |
+
if value is None:
|
| 24 |
+
return ""
|
| 25 |
+
if isinstance(value, str):
|
| 26 |
+
return value.strip()
|
| 27 |
+
if isinstance(value, (int, float)):
|
| 28 |
+
return str(value)
|
| 29 |
+
if isinstance(value, dict):
|
| 30 |
+
direct = _first_non_empty(
|
| 31 |
+
value.get("name"),
|
| 32 |
+
value.get("display"),
|
| 33 |
+
value.get("text"),
|
| 34 |
+
value.get("description"),
|
| 35 |
+
value.get("title"),
|
| 36 |
+
value.get("condition"),
|
| 37 |
+
value.get("diagnosis"),
|
| 38 |
+
value.get("substance"),
|
| 39 |
+
value.get("allergen"),
|
| 40 |
+
value.get("medication"),
|
| 41 |
+
value.get("drug"),
|
| 42 |
+
value.get("note"),
|
| 43 |
+
)
|
| 44 |
+
if direct:
|
| 45 |
+
return direct
|
| 46 |
+
|
| 47 |
+
code = value.get("code")
|
| 48 |
+
if isinstance(code, dict):
|
| 49 |
+
code_text = _first_non_empty(code.get("text"), code.get("display"))
|
| 50 |
+
if code_text:
|
| 51 |
+
return code_text
|
| 52 |
+
coding = _to_list(code.get("coding"))
|
| 53 |
+
for c in coding:
|
| 54 |
+
if isinstance(c, dict):
|
| 55 |
+
coding_text = _first_non_empty(c.get("display"), c.get("code"))
|
| 56 |
+
if coding_text:
|
| 57 |
+
return coding_text
|
| 58 |
+
elif isinstance(code, str) and code.strip():
|
| 59 |
+
return code.strip()
|
| 60 |
+
|
| 61 |
+
return ""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _parse_float(value: Any) -> Optional[float]:
|
| 65 |
+
if isinstance(value, (int, float)):
|
| 66 |
+
return float(value)
|
| 67 |
+
if isinstance(value, str):
|
| 68 |
+
cleaned = value.strip().replace(",", "")
|
| 69 |
+
if not cleaned:
|
| 70 |
+
return None
|
| 71 |
+
match = re.search(r"-?\d+(\.\d+)?", cleaned)
|
| 72 |
+
if match:
|
| 73 |
+
try:
|
| 74 |
+
return float(match.group(0))
|
| 75 |
+
except ValueError:
|
| 76 |
+
return None
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _extract_reference_range(lab: Dict[str, Any]) -> Tuple[Optional[float], Optional[float]]:
|
| 81 |
+
rr = lab.get("reference_range")
|
| 82 |
+
if isinstance(rr, dict):
|
| 83 |
+
low = _parse_float(rr.get("low"))
|
| 84 |
+
high = _parse_float(rr.get("high"))
|
| 85 |
+
return low, high
|
| 86 |
+
if isinstance(rr, list) and rr:
|
| 87 |
+
first = rr[0]
|
| 88 |
+
if isinstance(first, dict):
|
| 89 |
+
low = _parse_float(first.get("low"))
|
| 90 |
+
high = _parse_float(first.get("high"))
|
| 91 |
+
return low, high
|
| 92 |
+
return None, None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _normalize_vitals(vitals: Any) -> Dict[str, Any]:
|
| 96 |
+
normalized: Dict[str, Any] = {}
|
| 97 |
+
if isinstance(vitals, dict):
|
| 98 |
+
for k, v in vitals.items():
|
| 99 |
+
normalized[str(k).lower().strip()] = v
|
| 100 |
+
elif isinstance(vitals, list):
|
| 101 |
+
for item in vitals:
|
| 102 |
+
if not isinstance(item, dict):
|
| 103 |
+
continue
|
| 104 |
+
key = _extract_text(item.get("name") or item.get("type") or item.get("code"))
|
| 105 |
+
if not key:
|
| 106 |
+
continue
|
| 107 |
+
normalized[key.lower().strip()] = item.get("value")
|
| 108 |
+
return normalized
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _dedupe_keep_order(items: List[str]) -> List[str]:
|
| 112 |
+
seen = set()
|
| 113 |
+
result = []
|
| 114 |
+
for item in items:
|
| 115 |
+
text = item.strip()
|
| 116 |
+
if not text:
|
| 117 |
+
continue
|
| 118 |
+
if text not in seen:
|
| 119 |
+
seen.add(text)
|
| 120 |
+
result.append(text)
|
| 121 |
+
return result
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _has_documented_data(value: Any) -> bool:
|
| 125 |
+
if value is None:
|
| 126 |
+
return False
|
| 127 |
+
if isinstance(value, str):
|
| 128 |
+
return bool(value.strip())
|
| 129 |
+
if isinstance(value, (list, tuple, set, dict)):
|
| 130 |
+
return len(value) > 0
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _is_placeholder_entry(text: str) -> bool:
|
| 135 |
+
t = text.strip().lower()
|
| 136 |
+
return (
|
| 137 |
+
t == "unknown"
|
| 138 |
+
or t == "not documented"
|
| 139 |
+
or t == "none reported"
|
| 140 |
+
or t.startswith("none identified")
|
| 141 |
+
or t.startswith("no critical abnormalities identified")
|
| 142 |
+
or t.startswith("no immediate life-threatening alert identified")
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def build_emergency_snapshot(record: Dict[str, Any]) -> Tuple[str, List[Dict[str, str]], List[str], Dict[str, Any]]:
|
| 147 |
+
if not isinstance(record, dict):
|
| 148 |
+
record = {}
|
| 149 |
+
|
| 150 |
+
reasoning: List[Dict[str, str]] = []
|
| 151 |
+
def add_reasoning(explanation: str, source_text: str = ""):
|
| 152 |
+
reasoning.append({"explanation": explanation, "source_text": source_text})
|
| 153 |
+
|
| 154 |
+
add_reasoning("Initiating CodeBlue emergency analysis.")
|
| 155 |
+
|
| 156 |
+
patient = record.get("patient", {})
|
| 157 |
+
age = patient.get("age") if isinstance(patient, dict) else None
|
| 158 |
+
gender = patient.get("gender") if isinstance(patient, dict) else None
|
| 159 |
+
add_reasoning(f"Extracted patient demographics: Age {age or 'Unknown'}, Gender {gender or 'Unknown'}.")
|
| 160 |
+
|
| 161 |
+
raw_allergies = _to_list(record.get("allergies"))
|
| 162 |
+
raw_medications = _to_list(record.get("medications"))
|
| 163 |
+
raw_conditions = _to_list(record.get("conditions"))
|
| 164 |
+
raw_labs = _to_list(record.get("labs"))
|
| 165 |
+
raw_history = _to_list(record.get("history"))
|
| 166 |
+
raw_clinical_notes = _to_list(record.get("clinical_notes", record.get("clinical_notes", "")))
|
| 167 |
+
raw_notes = _to_list(record.get("notes"))
|
| 168 |
+
raw_vitals = record.get("vitals")
|
| 169 |
+
vitals = _normalize_vitals(raw_vitals)
|
| 170 |
+
add_reasoning("Normalized and loaded all available clinical data sources.")
|
| 171 |
+
|
| 172 |
+
# Combine all text sources for narrative analysis
|
| 173 |
+
all_text_sources = [
|
| 174 |
+
str(item) for item in
|
| 175 |
+
raw_allergies + raw_medications + raw_conditions + raw_labs +
|
| 176 |
+
raw_history + raw_clinical_notes + raw_notes
|
| 177 |
+
]
|
| 178 |
+
full_narrative = " ".join(all_text_sources).lower()
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
has_labs_documented = _has_documented_data(record.get("labs"))
|
| 182 |
+
has_vitals_documented = _has_documented_data(raw_vitals)
|
| 183 |
+
has_medications_documented = _has_documented_data(record.get("medications"))
|
| 184 |
+
|
| 185 |
+
critical_alerts: List[str] = []
|
| 186 |
+
allergies_out: List[str] = []
|
| 187 |
+
high_risk_medications: List[str] = []
|
| 188 |
+
chronic_conditions: List[str] = []
|
| 189 |
+
recent_abnormalities: List[str] = []
|
| 190 |
+
emergency_risks: List[str] = []
|
| 191 |
+
extracted_vitals: Dict[str, Any] = {}
|
| 192 |
+
|
| 193 |
+
# Allergies
|
| 194 |
+
severe_allergy = False
|
| 195 |
+
for allergy in raw_allergies:
|
| 196 |
+
allergy_text = _extract_text(allergy)
|
| 197 |
+
if not allergy_text and isinstance(allergy, dict):
|
| 198 |
+
allergy_text = _first_non_empty(
|
| 199 |
+
_extract_text(allergy.get("substance")),
|
| 200 |
+
_extract_text(allergy.get("allergen")),
|
| 201 |
+
_extract_text(allergy.get("code")),
|
| 202 |
+
)
|
| 203 |
+
if allergy_text:
|
| 204 |
+
allergies_out.append(allergy_text)
|
| 205 |
+
add_reasoning(f"Found allergy: {allergy_text}", source_text=allergy_text)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# Conditions
|
| 209 |
+
conditions = [_extract_text(c) for c in raw_conditions]
|
| 210 |
+
conditions = [c for c in conditions if c]
|
| 211 |
+
chronic_keywords = (
|
| 212 |
+
"diabetes",
|
| 213 |
+
"hypertension",
|
| 214 |
+
"heart",
|
| 215 |
+
"coronary",
|
| 216 |
+
"copd",
|
| 217 |
+
"asthma",
|
| 218 |
+
"ckd",
|
| 219 |
+
"kidney disease",
|
| 220 |
+
"renal",
|
| 221 |
+
"stroke",
|
| 222 |
+
"epilepsy",
|
| 223 |
+
"cancer",
|
| 224 |
+
"chf",
|
| 225 |
+
"heart failure",
|
| 226 |
+
"afib",
|
| 227 |
+
"atrial fibrillation",
|
| 228 |
+
)
|
| 229 |
+
for condition in conditions:
|
| 230 |
+
if any(keyword in condition.lower() for keyword in chronic_keywords):
|
| 231 |
+
chronic_conditions.append(condition)
|
| 232 |
+
add_reasoning(f"Identified emergency-relevant chronic condition: {condition}", source_text=condition)
|
| 233 |
+
|
| 234 |
+
# Medications
|
| 235 |
+
meds = [_extract_text(m) for m in raw_medications]
|
| 236 |
+
meds = [m for m in meds if m]
|
| 237 |
+
|
| 238 |
+
anticoagulants = (
|
| 239 |
+
"warfarin",
|
| 240 |
+
"heparin",
|
| 241 |
+
"enoxaparin",
|
| 242 |
+
"apixaban",
|
| 243 |
+
"rivaroxaban",
|
| 244 |
+
"dabigatran",
|
| 245 |
+
"edoxaban",
|
| 246 |
+
)
|
| 247 |
+
opioids = (
|
| 248 |
+
"morphine",
|
| 249 |
+
"oxycodone",
|
| 250 |
+
"hydromorphone",
|
| 251 |
+
"fentanyl",
|
| 252 |
+
"codeine",
|
| 253 |
+
"tramadol",
|
| 254 |
+
"methadone",
|
| 255 |
+
)
|
| 256 |
+
cardiac_meds = (
|
| 257 |
+
"digoxin",
|
| 258 |
+
"amiodarone",
|
| 259 |
+
"metoprolol",
|
| 260 |
+
"atenolol",
|
| 261 |
+
"carvedilol",
|
| 262 |
+
"diltiazem",
|
| 263 |
+
"verapamil",
|
| 264 |
+
"nitroglycerin",
|
| 265 |
+
)
|
| 266 |
+
has_anticoagulant = False
|
| 267 |
+
has_insulin = False
|
| 268 |
+
has_opioid = False
|
| 269 |
+
has_cardiac_med = False
|
| 270 |
+
|
| 271 |
+
for med in meds:
|
| 272 |
+
med_l = med.lower()
|
| 273 |
+
if any(name in med_l for name in anticoagulants):
|
| 274 |
+
has_anticoagulant = True
|
| 275 |
+
high_risk_medications.append(f"{med} (anticoagulant)")
|
| 276 |
+
add_reasoning(f"Detected high-risk medication (anticoagulant): {med}", source_text=med)
|
| 277 |
+
if "insulin" in med_l:
|
| 278 |
+
has_insulin = True
|
| 279 |
+
high_risk_medications.append(f"{med} (insulin)")
|
| 280 |
+
add_reasoning(f"Detected high-risk medication (insulin): {med}", source_text=med)
|
| 281 |
+
if any(name in med_l for name in opioids):
|
| 282 |
+
has_opioid = True
|
| 283 |
+
high_risk_medications.append(f"{med} (opioid)")
|
| 284 |
+
add_reasoning(f"Detected high-risk medication (opioid): {med}", source_text=med)
|
| 285 |
+
if any(name in med_l for name in cardiac_meds):
|
| 286 |
+
has_cardiac_med = True
|
| 287 |
+
high_risk_medications.append(f"{med} (cardiac medication)")
|
| 288 |
+
add_reasoning(f"Detected high-risk medication (cardiac): {med}", source_text=med)
|
| 289 |
+
|
| 290 |
+
# Labs
|
| 291 |
+
for lab in raw_labs:
|
| 292 |
+
if isinstance(lab, str):
|
| 293 |
+
text = lab.strip()
|
| 294 |
+
if text:
|
| 295 |
+
text_l = text.lower()
|
| 296 |
+
if any(word in text_l for word in ("critical", "abnormal", "high", "low")):
|
| 297 |
+
recent_abnormalities.append(text)
|
| 298 |
+
if "critical" in text_l:
|
| 299 |
+
critical_alerts.append(f"Critical lab finding: {text}.")
|
| 300 |
+
continue
|
| 301 |
+
|
| 302 |
+
if not isinstance(lab, dict):
|
| 303 |
+
continue
|
| 304 |
+
|
| 305 |
+
name = _extract_text(lab.get("name") or lab.get("test") or lab.get("code")) or "Lab result"
|
| 306 |
+
value_raw = lab.get("value")
|
| 307 |
+
if value_raw is None and isinstance(lab.get("result"), dict):
|
| 308 |
+
value_raw = lab["result"].get("value")
|
| 309 |
+
elif value_raw is None:
|
| 310 |
+
value_raw = lab.get("result")
|
| 311 |
+
|
| 312 |
+
value_num = _parse_float(value_raw)
|
| 313 |
+
unit = _extract_text(lab.get("unit"))
|
| 314 |
+
flag = _extract_text(lab.get("flag")).lower()
|
| 315 |
+
low, high = _extract_reference_range(lab)
|
| 316 |
+
|
| 317 |
+
abnormal = False
|
| 318 |
+
critical = False
|
| 319 |
+
if flag and any(token in flag for token in ("abnormal", "high", "low", "critical", "panic")):
|
| 320 |
+
abnormal = True
|
| 321 |
+
if flag and any(token in flag for token in ("critical", "panic", "severe")):
|
| 322 |
+
critical = True
|
| 323 |
+
if value_num is not None:
|
| 324 |
+
if low is not None and value_num < low:
|
| 325 |
+
abnormal = True
|
| 326 |
+
if high is not None and value_num > high:
|
| 327 |
+
abnormal = True
|
| 328 |
+
|
| 329 |
+
if abnormal:
|
| 330 |
+
value_text = str(value_raw) if value_raw is not None else "reported"
|
| 331 |
+
detail = f"{name}: {value_text}{(' ' + unit) if unit else ''}"
|
| 332 |
+
if flag:
|
| 333 |
+
detail += f" ({flag})"
|
| 334 |
+
recent_abnormalities.append(detail)
|
| 335 |
+
add_reasoning(f"Found abnormal lab: {detail}", source_text=str(lab))
|
| 336 |
+
if critical:
|
| 337 |
+
critical_alerts.append(f"Critical lab abnormality: {detail}.")
|
| 338 |
+
add_reasoning(f"Flagged CRITICAL lab: {detail}", source_text=str(lab))
|
| 339 |
+
|
| 340 |
+
# Vitals Extraction and Analysis
|
| 341 |
+
def add_vital_abnormality(label: str, value: Any, is_critical: bool, source: str) -> None:
|
| 342 |
+
detail = f"{label}: {value}"
|
| 343 |
+
recent_abnormalities.append(detail)
|
| 344 |
+
if is_critical:
|
| 345 |
+
critical_alerts.append(f"Critical vital sign: {detail}.")
|
| 346 |
+
add_reasoning(f"Flagged CRITICAL vital: {detail}", source_text=source)
|
| 347 |
+
else:
|
| 348 |
+
add_reasoning(f"Noted abnormal vital: {detail}", source_text=source)
|
| 349 |
+
|
| 350 |
+
# Regex patterns for vitals
|
| 351 |
+
vital_patterns = {
|
| 352 |
+
"Blood Pressure": r"bp\s*:?\s*(\d{2,3})\s*/\s*(\d{2,3})",
|
| 353 |
+
"Heart Rate": r"hr\s*:?\s*(\d{2,3})",
|
| 354 |
+
"SpO2": r"spo2\s*:?\s*(\d{2,3})%?",
|
| 355 |
+
"Respiratory Rate": r"rr\s*:?\s*(\d{1,2})",
|
| 356 |
+
"Temperature": r"temp\s*:?\s*(\d{2}(\.\d)?)",
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
for label, pattern in vital_patterns.items():
|
| 360 |
+
match = re.search(pattern, full_narrative, re.IGNORECASE)
|
| 361 |
+
if match:
|
| 362 |
+
if label == "Blood Pressure":
|
| 363 |
+
systolic, diastolic = map(int, match.groups())
|
| 364 |
+
value_str = f"{systolic}/{diastolic}"
|
| 365 |
+
extracted_vitals[label] = value_str
|
| 366 |
+
add_reasoning(f"Extracted {label}: {value_str}", source_text=match.group(0))
|
| 367 |
+
if systolic > 180 or diastolic > 120 or systolic < 90:
|
| 368 |
+
add_vital_abnormality(label, value_str, True, match.group(0))
|
| 369 |
+
else:
|
| 370 |
+
value = float(match.group(1))
|
| 371 |
+
extracted_vitals[label] = value
|
| 372 |
+
add_reasoning(f"Extracted {label}: {value}", source_text=match.group(0))
|
| 373 |
+
# Add abnormality checks here based on value
|
| 374 |
+
if label == "Heart Rate" and (value > 130 or value < 40):
|
| 375 |
+
add_vital_abnormality(label, f"{value} bpm", True, match.group(0))
|
| 376 |
+
elif label == "SpO2" and value < 90:
|
| 377 |
+
add_vital_abnormality(label, f"{value}%", True, match.group(0))
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
# History / notes signals
|
| 381 |
+
narrative_text = " ".join(
|
| 382 |
+
[_extract_text(item) for item in raw_history + raw_clinical_notes + raw_notes]
|
| 383 |
+
).lower()
|
| 384 |
+
|
| 385 |
+
if "icu" in narrative_text:
|
| 386 |
+
critical_alerts.append("ICU history documented.")
|
| 387 |
+
if "anaphyl" in narrative_text and not severe_allergy:
|
| 388 |
+
critical_alerts.append("History suggests severe allergic reaction.")
|
| 389 |
+
severe_allergy = True
|
| 390 |
+
|
| 391 |
+
# Emergency risks
|
| 392 |
+
has_diabetes = any("diabetes" in c.lower() for c in conditions + chronic_conditions)
|
| 393 |
+
has_cardiac_condition = any(
|
| 394 |
+
token in c.lower()
|
| 395 |
+
for c in conditions + chronic_conditions
|
| 396 |
+
for token in ("heart", "coronary", "chf", "afib", "atrial fibrillation", "myocardial infarction")
|
| 397 |
+
)
|
| 398 |
+
trauma_or_fall_risk = any(token in narrative_text for token in ("fall", "trauma", "injury", "post-op", "surgery"))
|
| 399 |
+
|
| 400 |
+
if has_anticoagulant:
|
| 401 |
+
emergency_risks.append("Bleeding Risk (High)")
|
| 402 |
+
add_reasoning("Synthesized risk: Anticoagulant exposure elevates bleeding risk.")
|
| 403 |
+
if has_insulin or has_diabetes:
|
| 404 |
+
emergency_risks.append("Hypoglycemia Risk (Moderate)")
|
| 405 |
+
add_reasoning("Synthesized risk: Diabetes/insulin factors increase hypoglycemia risk.")
|
| 406 |
+
if has_opioid:
|
| 407 |
+
emergency_risks.append("Respiratory Depression Risk (High)")
|
| 408 |
+
add_reasoning("Synthesized risk: Opioid exposure elevates respiratory depression risk.")
|
| 409 |
+
if has_cardiac_condition or has_cardiac_med:
|
| 410 |
+
emergency_risks.append("Cardiac Event Risk (Moderate)")
|
| 411 |
+
add_reasoning("Synthesized risk: Cardiac history/meds increase risk of cardiac events.")
|
| 412 |
+
if severe_allergy:
|
| 413 |
+
emergency_risks.append("Anaphylaxis Risk (High)")
|
| 414 |
+
add_reasoning("Synthesized risk: Severe allergy history poses high anaphylaxis risk.")
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# Final Snapshot Generation
|
| 418 |
+
final_lines = []
|
| 419 |
+
if critical_alerts:
|
| 420 |
+
final_lines.extend(critical_alerts)
|
| 421 |
+
|
| 422 |
+
summary_line = f"{age or 'Unknown'}-y/o {gender or 'Unknown'} with history of {', '.join(chronic_conditions[:2]) if chronic_conditions else 'undisclosed conditions'}."
|
| 423 |
+
final_lines.append(summary_line)
|
| 424 |
+
|
| 425 |
+
risk_summary_line = f"Primary risks: {', '.join(emergency_risks[:3]) if emergency_risks else 'none identified'}."
|
| 426 |
+
final_lines.append(risk_summary_line)
|
| 427 |
+
|
| 428 |
+
if not final_lines:
|
| 429 |
+
final_lines.append("No critical issues identified from available data.")
|
| 430 |
+
|
| 431 |
+
add_reasoning("Finalizing emergency snapshot based on synthesized risks and alerts.")
|
| 432 |
+
final_snapshot = "\n".join(_dedupe_keep_order(final_lines)[:6])
|
| 433 |
+
|
| 434 |
+
return final_snapshot, reasoning, emergency_risks, extracted_vitals
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def main() -> int:
|
| 438 |
+
raw = ""
|
| 439 |
+
if len(sys.argv) > 1:
|
| 440 |
+
path = sys.argv[1]
|
| 441 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 442 |
+
raw = f.read()
|
| 443 |
+
else:
|
| 444 |
+
raw = sys.stdin.read()
|
| 445 |
+
|
| 446 |
+
if not raw.strip():
|
| 447 |
+
record: Dict[str, Any] = {}
|
| 448 |
+
else:
|
| 449 |
+
try:
|
| 450 |
+
parsed = json.loads(raw)
|
| 451 |
+
record = parsed if isinstance(parsed, dict) else {}
|
| 452 |
+
except json.JSONDecodeError:
|
| 453 |
+
record = {}
|
| 454 |
+
|
| 455 |
+
snapshot, reasoning, risks, vitals = build_emergency_snapshot(record)
|
| 456 |
+
result = {
|
| 457 |
+
"snapshot": snapshot,
|
| 458 |
+
"reasoning": reasoning,
|
| 459 |
+
"risks": risks,
|
| 460 |
+
"vitals": vitals,
|
| 461 |
+
}
|
| 462 |
+
print(json.dumps(result, indent=2, ensure_ascii=True))
|
| 463 |
+
return 0
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
if __name__ == "__main__":
|
| 467 |
+
raise SystemExit(main())
|
frontend
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Subproject commit 70fcc96cc4551f6093f74a4a7f6c8ee9e75932bf
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.12
|
| 2 |
+
uvicorn==0.34.2
|
| 3 |
+
python-multipart==0.0.9
|
| 4 |
+
google-generativeai==0.8.3
|