Hackathon Builder commited on
Commit
5ad56ab
·
1 Parent(s): dab42ad

Fix Dockerfile permissions & add application files

Browse files
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
- title: CodeBlue Context
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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