Claude commited on
Commit
c7ad943
Β·
1 Parent(s): aad09a1

feat: Add Code Blue Mode

Browse files
Files changed (2) hide show
  1. app.py +104 -1
  2. code_blue_agent.py +717 -0
app.py CHANGED
@@ -937,6 +937,10 @@ volumetric_agent = VolumetricAgent(gemini_model, medgemma_model, medgemma_proces
937
  lab_agent = LabReportAgent(gemini_model, medgemma_model, medgemma_processor)
938
  anatomy_agent = AnatomyAgent(gemini_model, medgemma_model, medgemma_processor)
939
 
 
 
 
 
940
 
941
  # ============================================================================
942
  # MAIN PROCESSING
@@ -1095,7 +1099,7 @@ def create_ui():
1095
  with gr.Row():
1096
  gr.Markdown("🎀 **MedASR Voice Input** - Hands-free dictation", elem_classes="new-feature")
1097
  gr.Markdown("πŸ“Š **Longitudinal Comparison** - Track progression", elem_classes="new-feature")
1098
- gr.Markdown("🧊 **3D CT/MRI Volumes** - Multi-slice analysis", elem_classes="new-feature")
1099
  gr.Markdown("πŸ”¬ **Lab Extraction** - Structured data", elem_classes="new-feature")
1100
 
1101
  gr.Markdown("---")
@@ -1232,6 +1236,105 @@ def create_ui():
1232
  inputs=[query_input],
1233
  )
1234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1235
  # Footer
1236
  gr.Markdown("""
1237
  ---
 
937
  lab_agent = LabReportAgent(gemini_model, medgemma_model, medgemma_processor)
938
  anatomy_agent = AnatomyAgent(gemini_model, medgemma_model, medgemma_processor)
939
 
940
+ # Code Blue Agent (voice-activated ACLS documentation)
941
+ from code_blue_agent import CodeBlueAgent
942
+ code_blue_agent = CodeBlueAgent()
943
+
944
 
945
  # ============================================================================
946
  # MAIN PROCESSING
 
1099
  with gr.Row():
1100
  gr.Markdown("🎀 **MedASR Voice Input** - Hands-free dictation", elem_classes="new-feature")
1101
  gr.Markdown("πŸ“Š **Longitudinal Comparison** - Track progression", elem_classes="new-feature")
1102
+ gr.Markdown("🚨 **Code Blue Mode** - Voice-activated ACLS", elem_classes="new-feature")
1103
  gr.Markdown("πŸ”¬ **Lab Extraction** - Structured data", elem_classes="new-feature")
1104
 
1105
  gr.Markdown("---")
 
1236
  inputs=[query_input],
1237
  )
1238
 
1239
+ # ========================================================================
1240
+ # 🚨 CODE BLUE MODE - Voice-Activated ACLS Documentation
1241
+ # ========================================================================
1242
+ gr.Markdown("---")
1243
+ gr.Markdown("## 🚨 Code Blue Mode")
1244
+ gr.Markdown("*Voice-activated cardiac arrest documentation with ACLS algorithm guidance*")
1245
+
1246
+ with gr.Row():
1247
+ with gr.Column(scale=1):
1248
+ gr.Markdown("### 🎀 Voice Commands")
1249
+
1250
+ code_blue_audio = gr.Audio(
1251
+ sources=["microphone"],
1252
+ type="filepath",
1253
+ label="πŸ”΄ Click to speak (hands-free during code)"
1254
+ )
1255
+
1256
+ code_blue_text = gr.Textbox(
1257
+ label="Or type command",
1258
+ placeholder="CPR started, Epi given, V-fib, Shock delivered, ROSC...",
1259
+ lines=1
1260
+ )
1261
+
1262
+ with gr.Row():
1263
+ start_code_btn = gr.Button("🚨 Start Code Blue", variant="primary")
1264
+ end_code_btn = gr.Button("⏹️ End Code", variant="secondary")
1265
+
1266
+ gr.Markdown("""
1267
+ **Voice Commands:**
1268
+ | Say This | Action |
1269
+ |----------|--------|
1270
+ | "CPR started" | Start CPR timer |
1271
+ | "V-fib" / "Asystole" / "PEA" | Log rhythm |
1272
+ | "Epi given" | Log epinephrine |
1273
+ | "Shock delivered" | Log defibrillation |
1274
+ | "ROSC" | Return of circulation |
1275
+ | "Switch" | Compressor change |
1276
+ """)
1277
+
1278
+ with gr.Column(scale=1):
1279
+ gr.Markdown("### πŸ“‹ Code Blue Record")
1280
+ code_blue_output = gr.Markdown(
1281
+ value="🎀 Say **'Code called'** or click **Start Code Blue** to begin documentation",
1282
+ label="Live Documentation"
1283
+ )
1284
+ code_blue_status = gr.Markdown(value="", label="Status")
1285
+
1286
+ # Code Blue processing function
1287
+ def process_code_blue(audio_path, text_input):
1288
+ """Process Code Blue voice/text input."""
1289
+ # Get text from voice or text input
1290
+ if audio_path:
1291
+ text = transcribe_medical_audio(audio_path)
1292
+ else:
1293
+ text = text_input
1294
+
1295
+ if not text or not text.strip():
1296
+ return code_blue_agent.get_status() if code_blue_agent.session else "🎀 Waiting for input...", ""
1297
+
1298
+ # Process through Code Blue agent
1299
+ response = code_blue_agent.process_voice(text)
1300
+ status = code_blue_agent.get_status() if code_blue_agent.session else ""
1301
+
1302
+ return response, status
1303
+
1304
+ def start_code():
1305
+ """Start a new Code Blue session."""
1306
+ return code_blue_agent.start_code(), code_blue_agent.get_status()
1307
+
1308
+ def end_code():
1309
+ """End Code Blue and generate record."""
1310
+ if code_blue_agent.session:
1311
+ response = code_blue_agent.process_voice("code ended")
1312
+ return response, ""
1313
+ return "No active code", ""
1314
+
1315
+ # Wire up Code Blue
1316
+ code_blue_audio.change(
1317
+ process_code_blue,
1318
+ inputs=[code_blue_audio, code_blue_text],
1319
+ outputs=[code_blue_output, code_blue_status]
1320
+ )
1321
+
1322
+ code_blue_text.submit(
1323
+ process_code_blue,
1324
+ inputs=[code_blue_audio, code_blue_text],
1325
+ outputs=[code_blue_output, code_blue_status]
1326
+ )
1327
+
1328
+ start_code_btn.click(
1329
+ start_code,
1330
+ outputs=[code_blue_output, code_blue_status]
1331
+ )
1332
+
1333
+ end_code_btn.click(
1334
+ end_code,
1335
+ outputs=[code_blue_output, code_blue_status]
1336
+ )
1337
+
1338
  # Footer
1339
  gr.Markdown("""
1340
  ---
code_blue_agent.py ADDED
@@ -0,0 +1,717 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NurseGemma Code Blue Agent
3
+ Real-time voice-activated cardiac arrest documentation with ACLS algorithm
4
+
5
+ Voice commands β†’ Auto-timestamped events β†’ ACLS-guided prompts β†’ Code Blue Record
6
+
7
+ "CPR started" β†’ timestamp
8
+ "Pulse check, no pulse" β†’ timestamp, rhythm prompt
9
+ "Epi given" β†’ timestamp, dose logged, next dose timer
10
+ "Shock given" β†’ timestamp, joules logged
11
+ "ROSC" β†’ timestamp, post-arrest care prompts
12
+ """
13
+
14
+ import time
15
+ from datetime import datetime, timedelta
16
+ from typing import List, Dict, Optional
17
+ from dataclasses import dataclass, field
18
+ from enum import Enum
19
+
20
+
21
+ class Rhythm(Enum):
22
+ UNKNOWN = "Unknown"
23
+ VF = "Ventricular Fibrillation"
24
+ VT = "Pulseless VT"
25
+ PEA = "Pulseless Electrical Activity"
26
+ ASYSTOLE = "Asystole"
27
+ ROSC = "ROSC"
28
+
29
+
30
+ class ACLSPath(Enum):
31
+ SHOCKABLE = "VF/pVT (Shockable)"
32
+ NON_SHOCKABLE = "PEA/Asystole (Non-Shockable)"
33
+
34
+
35
+ @dataclass
36
+ class CodeEvent:
37
+ """Single event during code blue."""
38
+ timestamp: datetime
39
+ event_type: str
40
+ details: str
41
+ run_time_seconds: int # Time since code started
42
+
43
+ def format_time(self) -> str:
44
+ """Format as HH:MM:SS."""
45
+ return self.timestamp.strftime("%H:%M:%S")
46
+
47
+ def format_run_time(self) -> str:
48
+ """Format run time as MM:SS."""
49
+ mins = self.run_time_seconds // 60
50
+ secs = self.run_time_seconds % 60
51
+ return f"{mins:02d}:{secs:02d}"
52
+
53
+
54
+ @dataclass
55
+ class CodeBlueSession:
56
+ """Active code blue documentation session."""
57
+
58
+ # Timing
59
+ start_time: datetime = field(default_factory=datetime.now)
60
+ end_time: Optional[datetime] = None
61
+
62
+ # Events log
63
+ events: List[CodeEvent] = field(default_factory=list)
64
+
65
+ # ACLS tracking
66
+ current_rhythm: Rhythm = Rhythm.UNKNOWN
67
+ acls_path: Optional[ACLSPath] = None
68
+
69
+ # CPR tracking
70
+ cpr_cycles: int = 0
71
+ last_cpr_start: Optional[datetime] = None
72
+ compressor_changes: List[datetime] = field(default_factory=list)
73
+
74
+ # Medication tracking
75
+ epi_doses: List[datetime] = field(default_factory=list)
76
+ amiodarone_doses: List[tuple] = field(default_factory=list) # (time, mg)
77
+ lidocaine_doses: List[tuple] = field(default_factory=list)
78
+ other_meds: List[tuple] = field(default_factory=list) # (time, med, dose)
79
+
80
+ # Defibrillation
81
+ shocks: List[tuple] = field(default_factory=list) # (time, joules)
82
+
83
+ # Airway
84
+ airway_type: Optional[str] = None
85
+ airway_time: Optional[datetime] = None
86
+
87
+ # Access
88
+ iv_access: bool = False
89
+ io_access: bool = False
90
+ access_time: Optional[datetime] = None
91
+
92
+ # Outcome
93
+ outcome: Optional[str] = None # ROSC, Expired, Transferred
94
+
95
+ def get_run_time(self) -> int:
96
+ """Get seconds since code started."""
97
+ return int((datetime.now() - self.start_time).total_seconds())
98
+
99
+ def add_event(self, event_type: str, details: str = ""):
100
+ """Add timestamped event."""
101
+ event = CodeEvent(
102
+ timestamp=datetime.now(),
103
+ event_type=event_type,
104
+ details=details,
105
+ run_time_seconds=self.get_run_time()
106
+ )
107
+ self.events.append(event)
108
+ return event
109
+
110
+ def time_since_last_epi(self) -> Optional[int]:
111
+ """Seconds since last epinephrine dose."""
112
+ if not self.epi_doses:
113
+ return None
114
+ return int((datetime.now() - self.epi_doses[-1]).total_seconds())
115
+
116
+ def is_epi_due(self) -> bool:
117
+ """Check if epinephrine is due (every 3-5 min)."""
118
+ elapsed = self.time_since_last_epi()
119
+ if elapsed is None:
120
+ return True # No epi given yet
121
+ return elapsed >= 180 # 3 minutes
122
+
123
+ def time_since_cpr_start(self) -> Optional[int]:
124
+ """Seconds since current CPR cycle started."""
125
+ if not self.last_cpr_start:
126
+ return None
127
+ return int((datetime.now() - self.last_cpr_start).total_seconds())
128
+
129
+ def is_rhythm_check_due(self) -> bool:
130
+ """Check if 2-minute rhythm check is due."""
131
+ elapsed = self.time_since_cpr_start()
132
+ if elapsed is None:
133
+ return False
134
+ return elapsed >= 120 # 2 minutes
135
+
136
+
137
+ class CodeBlueAgent:
138
+ """
139
+ Voice-activated Code Blue documentation agent.
140
+
141
+ Integrates with ACLS 2025 algorithm:
142
+ - VF/pVT pathway: Shock β†’ CPR β†’ Epi β†’ Shock β†’ Amio/Lido
143
+ - PEA/Asystole pathway: CPR β†’ Epi β†’ CPR β†’ Treat reversible causes
144
+ """
145
+
146
+ # Voice command patterns
147
+ COMMANDS = {
148
+ # CPR
149
+ "cpr started": "start_cpr",
150
+ "cpr start": "start_cpr",
151
+ "start cpr": "start_cpr",
152
+ "compressions started": "start_cpr",
153
+ "cpr stopped": "stop_cpr",
154
+ "cpr paused": "pause_cpr",
155
+ "switch": "switch_compressor",
156
+ "switch compressor": "switch_compressor",
157
+ "compressor change": "switch_compressor",
158
+
159
+ # Rhythm/Pulse
160
+ "pulse check": "pulse_check",
161
+ "check pulse": "pulse_check",
162
+ "rhythm check": "rhythm_check",
163
+ "check rhythm": "rhythm_check",
164
+ "no pulse": "no_pulse",
165
+ "pulse present": "pulse_present",
166
+ "rosc": "rosc",
167
+ "got a pulse": "rosc",
168
+ "we have a pulse": "rosc",
169
+
170
+ # Rhythms
171
+ "v fib": "rhythm_vf",
172
+ "vf": "rhythm_vf",
173
+ "ventricular fibrillation": "rhythm_vf",
174
+ "v tach": "rhythm_vt",
175
+ "vt": "rhythm_vt",
176
+ "pulseless vt": "rhythm_vt",
177
+ "pea": "rhythm_pea",
178
+ "asystole": "rhythm_asystole",
179
+ "flatline": "rhythm_asystole",
180
+
181
+ # Defibrillation
182
+ "shock advised": "shock_advised",
183
+ "charging": "charging",
184
+ "clear": "clear",
185
+ "shock delivered": "shock_delivered",
186
+ "shock given": "shock_delivered",
187
+ "no shock advised": "no_shock_advised",
188
+
189
+ # Medications
190
+ "epi given": "epi_given",
191
+ "epinephrine given": "epi_given",
192
+ "epi in": "epi_given",
193
+ "1 of epi": "epi_given",
194
+ "amiodarone": "amio_given",
195
+ "amio given": "amio_given",
196
+ "300 of amio": "amio_300",
197
+ "150 of amio": "amio_150",
198
+ "lidocaine": "lido_given",
199
+ "lido given": "lido_given",
200
+ "bicarb": "bicarb_given",
201
+ "calcium": "calcium_given",
202
+ "mag": "mag_given",
203
+ "magnesium": "mag_given",
204
+
205
+ # Airway
206
+ "intubated": "intubated",
207
+ "tube in": "intubated",
208
+ "et tube placed": "intubated",
209
+ "lma placed": "lma_placed",
210
+ "supraglottic": "lma_placed",
211
+ "bagging": "bvm",
212
+ "bvm": "bvm",
213
+
214
+ # Access
215
+ "iv access": "iv_access",
216
+ "iv in": "iv_access",
217
+ "io access": "io_access",
218
+ "io in": "io_access",
219
+
220
+ # Code status
221
+ "code called": "code_start",
222
+ "time of death": "time_of_death",
223
+ "code ended": "code_end",
224
+ "stop code": "code_end",
225
+ }
226
+
227
+ def __init__(self):
228
+ self.session: Optional[CodeBlueSession] = None
229
+
230
+ def start_code(self) -> str:
231
+ """Initialize new code blue session."""
232
+ self.session = CodeBlueSession()
233
+ event = self.session.add_event("CODE_CALLED", "Code Blue initiated")
234
+ return f"""
235
+ 🚨 **CODE BLUE INITIATED** - {event.format_time()}
236
+
237
+ **ACLS Protocol Active**
238
+ ━━━━━━━━━━━━━━━━━━━━━━━
239
+
240
+ πŸ“‹ **Immediate Actions:**
241
+ 1. Start high-quality CPR (100-120/min, 2+ inches)
242
+ 2. Attach monitor/defibrillator
243
+ 3. Establish IV/IO access
244
+ 4. Identify rhythm
245
+
246
+ 🎀 **Voice Commands Ready:**
247
+ - "CPR started"
248
+ - "Rhythm check" / "V-fib" / "Asystole" / "PEA"
249
+ - "Epi given" / "Shock delivered"
250
+ - "ROSC" when pulse returns
251
+
252
+ ⏱️ Timer running...
253
+ """
254
+
255
+ def process_voice(self, text: str) -> str:
256
+ """Process voice input and return response with prompts."""
257
+ if not self.session:
258
+ # Auto-start if saying code-related things
259
+ if any(cmd in text.lower() for cmd in ["code", "cpr", "arrest"]):
260
+ return self.start_code()
261
+ return "🎀 Say 'Code called' to start Code Blue documentation"
262
+
263
+ text_lower = text.lower().strip()
264
+
265
+ # Find matching command
266
+ for pattern, action in self.COMMANDS.items():
267
+ if pattern in text_lower:
268
+ return self._execute_action(action, text)
269
+
270
+ # No command matched - log as note
271
+ event = self.session.add_event("NOTE", text)
272
+ return f"πŸ“ [{event.format_run_time()}] Note: {text}"
273
+
274
+ def _execute_action(self, action: str, original_text: str) -> str:
275
+ """Execute recognized action and return formatted response."""
276
+
277
+ # === CPR ===
278
+ if action == "start_cpr":
279
+ self.session.last_cpr_start = datetime.now()
280
+ self.session.cpr_cycles += 1
281
+ event = self.session.add_event("CPR_START", f"Cycle {self.session.cpr_cycles}")
282
+ return self._format_cpr_start(event)
283
+
284
+ if action == "switch_compressor":
285
+ self.session.compressor_changes.append(datetime.now())
286
+ event = self.session.add_event("COMPRESSOR_SWITCH", "")
287
+ return f"πŸ”„ [{event.format_run_time()}] **Compressor switched** - Good teamwork!"
288
+
289
+ # === Rhythm ===
290
+ if action == "pulse_check" or action == "rhythm_check":
291
+ event = self.session.add_event("RHYTHM_CHECK", "")
292
+ return self._format_rhythm_check(event)
293
+
294
+ if action == "rhythm_vf":
295
+ self.session.current_rhythm = Rhythm.VF
296
+ self.session.acls_path = ACLSPath.SHOCKABLE
297
+ event = self.session.add_event("RHYTHM", "VF identified")
298
+ return self._format_shockable_rhythm(event, "V-FIB")
299
+
300
+ if action == "rhythm_vt":
301
+ self.session.current_rhythm = Rhythm.VT
302
+ self.session.acls_path = ACLSPath.SHOCKABLE
303
+ event = self.session.add_event("RHYTHM", "Pulseless VT identified")
304
+ return self._format_shockable_rhythm(event, "Pulseless V-TACH")
305
+
306
+ if action == "rhythm_pea":
307
+ self.session.current_rhythm = Rhythm.PEA
308
+ self.session.acls_path = ACLSPath.NON_SHOCKABLE
309
+ event = self.session.add_event("RHYTHM", "PEA identified")
310
+ return self._format_non_shockable_rhythm(event, "PEA")
311
+
312
+ if action == "rhythm_asystole":
313
+ self.session.current_rhythm = Rhythm.ASYSTOLE
314
+ self.session.acls_path = ACLSPath.NON_SHOCKABLE
315
+ event = self.session.add_event("RHYTHM", "Asystole")
316
+ return self._format_non_shockable_rhythm(event, "ASYSTOLE")
317
+
318
+ if action == "no_pulse":
319
+ event = self.session.add_event("PULSE_CHECK", "No pulse")
320
+ return f"❌ [{event.format_run_time()}] **No pulse** - Continue CPR\n\n{self._get_next_prompt()}"
321
+
322
+ # === ROSC ===
323
+ if action == "rosc" or action == "pulse_present":
324
+ self.session.current_rhythm = Rhythm.ROSC
325
+ self.session.outcome = "ROSC"
326
+ event = self.session.add_event("ROSC", "Return of spontaneous circulation")
327
+ return self._format_rosc(event)
328
+
329
+ # === Defibrillation ===
330
+ if action == "shock_advised":
331
+ event = self.session.add_event("DEFIB", "Shock advised")
332
+ return f"⚑ [{event.format_run_time()}] **Shock advised** - Charging...\nπŸ”Š Say 'Clear' then 'Shock delivered'"
333
+
334
+ if action == "clear":
335
+ event = self.session.add_event("DEFIB", "Clear called")
336
+ return f"⚠️ [{event.format_run_time()}] **CLEAR!** - Deliver shock"
337
+
338
+ if action == "shock_delivered":
339
+ joules = self._extract_joules(original_text) or 200
340
+ self.session.shocks.append((datetime.now(), joules))
341
+ event = self.session.add_event("SHOCK", f"{joules}J delivered")
342
+ return self._format_shock_delivered(event, joules)
343
+
344
+ if action == "no_shock_advised":
345
+ event = self.session.add_event("DEFIB", "No shock advised")
346
+ return f"🚫 [{event.format_run_time()}] **No shock advised** - Non-shockable rhythm\n\n➑️ Continue CPR, give Epi ASAP"
347
+
348
+ # === Medications ===
349
+ if action == "epi_given":
350
+ self.session.epi_doses.append(datetime.now())
351
+ dose_num = len(self.session.epi_doses)
352
+ event = self.session.add_event("MED", f"Epinephrine 1mg IV (dose #{dose_num})")
353
+ return self._format_epi_given(event, dose_num)
354
+
355
+ if action == "amio_given" or action == "amio_300":
356
+ mg = 300 if not self.session.amiodarone_doses else 150
357
+ self.session.amiodarone_doses.append((datetime.now(), mg))
358
+ event = self.session.add_event("MED", f"Amiodarone {mg}mg IV")
359
+ return f"πŸ’Š [{event.format_run_time()}] **Amiodarone {mg}mg** given\n\n{'➑️ Second dose: 150mg if needed' if mg == 300 else ''}"
360
+
361
+ if action == "amio_150":
362
+ self.session.amiodarone_doses.append((datetime.now(), 150))
363
+ event = self.session.add_event("MED", "Amiodarone 150mg IV")
364
+ return f"πŸ’Š [{event.format_run_time()}] **Amiodarone 150mg** given"
365
+
366
+ if action == "bicarb_given":
367
+ self.session.other_meds.append((datetime.now(), "Sodium Bicarbonate", "50mEq"))
368
+ event = self.session.add_event("MED", "Sodium Bicarbonate 50mEq IV")
369
+ return f"πŸ’Š [{event.format_run_time()}] **Bicarb 50mEq** given"
370
+
371
+ if action == "calcium_given":
372
+ self.session.other_meds.append((datetime.now(), "Calcium Chloride", "1g"))
373
+ event = self.session.add_event("MED", "Calcium Chloride 1g IV")
374
+ return f"πŸ’Š [{event.format_run_time()}] **Calcium 1g** given"
375
+
376
+ if action == "mag_given":
377
+ self.session.other_meds.append((datetime.now(), "Magnesium Sulfate", "2g"))
378
+ event = self.session.add_event("MED", "Magnesium Sulfate 2g IV")
379
+ return f"πŸ’Š [{event.format_run_time()}] **Mag 2g** given (Torsades protocol)"
380
+
381
+ # === Airway ===
382
+ if action == "intubated":
383
+ self.session.airway_type = "ETT"
384
+ self.session.airway_time = datetime.now()
385
+ event = self.session.add_event("AIRWAY", "ET tube placed")
386
+ return f"🫁 [{event.format_run_time()}] **Intubated** - Confirm with waveform capnography\n\n➑️ Continuous compressions, 1 breath q6 sec"
387
+
388
+ if action == "lma_placed":
389
+ self.session.airway_type = "LMA"
390
+ self.session.airway_time = datetime.now()
391
+ event = self.session.add_event("AIRWAY", "Supraglottic airway placed")
392
+ return f"🫁 [{event.format_run_time()}] **LMA placed** - Confirm placement\n\n➑️ Continuous compressions, 1 breath q6 sec"
393
+
394
+ # === Access ===
395
+ if action == "iv_access":
396
+ self.session.iv_access = True
397
+ self.session.access_time = datetime.now()
398
+ event = self.session.add_event("ACCESS", "IV access established")
399
+ return f"πŸ’‰ [{event.format_run_time()}] **IV access** established\n\n{'➑️ Give Epinephrine 1mg now!' if self.session.is_epi_due() else ''}"
400
+
401
+ if action == "io_access":
402
+ self.session.io_access = True
403
+ self.session.access_time = datetime.now()
404
+ event = self.session.add_event("ACCESS", "IO access established")
405
+ return f"🦴 [{event.format_run_time()}] **IO access** established\n\n{'➑️ Give Epinephrine 1mg now!' if self.session.is_epi_due() else ''}"
406
+
407
+ # === Code End ===
408
+ if action == "time_of_death":
409
+ self.session.end_time = datetime.now()
410
+ self.session.outcome = "Expired"
411
+ event = self.session.add_event("CODE_END", "Time of death called")
412
+ return self._format_code_end(event, "Expired")
413
+
414
+ if action == "code_end":
415
+ self.session.end_time = datetime.now()
416
+ if not self.session.outcome:
417
+ self.session.outcome = "Ended"
418
+ event = self.session.add_event("CODE_END", "Code concluded")
419
+ return self._format_code_end(event, self.session.outcome)
420
+
421
+ return f"πŸ“ Noted: {original_text}"
422
+
423
+ # === Formatting Helpers ===
424
+
425
+ def _format_cpr_start(self, event: CodeEvent) -> str:
426
+ cycle = self.session.cpr_cycles
427
+ return f"""
428
+ πŸ’ͺ [{event.format_run_time()}] **CPR Cycle {cycle} Started**
429
+
430
+ πŸ“‹ **High-Quality CPR:**
431
+ β€’ Push hard: β‰₯2 inches (5 cm)
432
+ β€’ Push fast: 100-120/min
433
+ β€’ Full chest recoil
434
+ β€’ Minimize interruptions
435
+
436
+ ⏱️ 2-minute timer started...
437
+ {self._get_next_prompt()}
438
+ """
439
+
440
+ def _format_rhythm_check(self, event: CodeEvent) -> str:
441
+ prompts = []
442
+ if self.session.is_epi_due():
443
+ epi_time = self.session.time_since_last_epi()
444
+ if epi_time:
445
+ prompts.append(f"⚠️ Epi due! Last dose {epi_time//60}:{epi_time%60:02d} ago")
446
+ else:
447
+ prompts.append("⚠️ No Epi given yet!")
448
+
449
+ return f"""
450
+ πŸ” [{event.format_run_time()}] **RHYTHM CHECK**
451
+
452
+ 🎀 **What's the rhythm?**
453
+ β€’ "V-fib" or "V-tach" β†’ Shockable
454
+ β€’ "PEA" or "Asystole" β†’ Non-shockable
455
+ β€’ "ROSC" β†’ We got 'em back!
456
+
457
+ {chr(10).join(prompts)}
458
+ """
459
+
460
+ def _format_shockable_rhythm(self, event: CodeEvent, rhythm: str) -> str:
461
+ shock_num = len(self.session.shocks) + 1
462
+ return f"""
463
+ ⚑ [{event.format_run_time()}] **{rhythm}** - SHOCKABLE RHYTHM
464
+
465
+ **ACLS VF/pVT Protocol:**
466
+ ━━━━━━━━━━━━━━━━━━━━━━━
467
+
468
+ 1. ⚑ **SHOCK** (200J biphasic) - Shock #{shock_num}
469
+ 2. πŸ’ͺ Resume CPR immediately x 2 min
470
+ 3. πŸ’‰ Epi 1mg q3-5min
471
+ 4. πŸ’Š Amiodarone 300mg after 2nd shock
472
+
473
+ πŸ”Š Say "Shock delivered" after defibrillation
474
+ """
475
+
476
+ def _format_non_shockable_rhythm(self, event: CodeEvent, rhythm: str) -> str:
477
+ return f"""
478
+ 🚫 [{event.format_run_time()}] **{rhythm}** - NON-SHOCKABLE
479
+
480
+ **ACLS PEA/Asystole Protocol:**
481
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━
482
+
483
+ 1. πŸ’ͺ Continue high-quality CPR
484
+ 2. πŸ’‰ **Epi 1mg IV NOW** (then q3-5min)
485
+ 3. πŸ” Treat reversible causes (H's & T's)
486
+
487
+ **5 H's:** Hypovolemia, Hypoxia, H+ (acidosis), Hypo/Hyperkalemia, Hypothermia
488
+ **5 T's:** Tension pneumo, Tamponade, Toxins, Thrombosis (PE), Thrombosis (MI)
489
+
490
+ πŸ”Š Say "Epi given" when administered
491
+ """
492
+
493
+ def _format_shock_delivered(self, event: CodeEvent, joules: int) -> str:
494
+ shock_num = len(self.session.shocks)
495
+ return f"""
496
+ ⚑ [{event.format_run_time()}] **SHOCK #{shock_num} DELIVERED** - {joules}J
497
+
498
+ ➑️ **RESUME CPR IMMEDIATELY!**
499
+
500
+ {self._get_post_shock_prompt(shock_num)}
501
+ """
502
+
503
+ def _get_post_shock_prompt(self, shock_num: int) -> str:
504
+ prompts = ["πŸ’ͺ CPR x 2 minutes"]
505
+
506
+ if self.session.is_epi_due():
507
+ prompts.append("πŸ’‰ Give Epi 1mg now!")
508
+
509
+ if shock_num >= 2 and not self.session.amiodarone_doses:
510
+ prompts.append("πŸ’Š Consider Amiodarone 300mg")
511
+
512
+ return "\n".join(prompts)
513
+
514
+ def _format_epi_given(self, event: CodeEvent, dose_num: int) -> str:
515
+ return f"""
516
+ πŸ’‰ [{event.format_run_time()}] **Epinephrine 1mg IV** (Dose #{dose_num})
517
+
518
+ ⏱️ Next Epi due in 3-5 minutes
519
+ {self._get_next_prompt()}
520
+ """
521
+
522
+ def _format_rosc(self, event: CodeEvent) -> str:
523
+ duration = event.run_time_seconds
524
+ mins = duration // 60
525
+ secs = duration % 60
526
+
527
+ return f"""
528
+ πŸŽ‰ [{event.format_run_time()}] **ROSC ACHIEVED!**
529
+
530
+ **Code Duration:** {mins} min {secs} sec
531
+ **Total Shocks:** {len(self.session.shocks)}
532
+ **Total Epi Doses:** {len(self.session.epi_doses)}
533
+
534
+ ━━━━━━━━━━━━━━━━���━━━━━━━━━━
535
+
536
+ **POST-CARDIAC ARREST CARE:**
537
+
538
+ 1. 🫁 **Airway** - Secure, confirm ETCO2
539
+ 2. 🩸 **Circulation** - Target MAP β‰₯65, treat hypotension
540
+ 3. 🧠 **Neuro** - Targeted temperature management?
541
+ 4. ❀️ **Cardiac** - 12-lead ECG, cath lab if STEMI
542
+ 5. πŸ”¬ **Labs** - ABG, lactate, electrolytes
543
+
544
+ πŸ”Š Say "Code ended" when documentation complete
545
+ """
546
+
547
+ def _format_code_end(self, event: CodeEvent, outcome: str) -> str:
548
+ duration = event.run_time_seconds
549
+ mins = duration // 60
550
+ secs = duration % 60
551
+
552
+ return f"""
553
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━
554
+ **CODE BLUE ENDED** - {event.format_time()}
555
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━
556
+
557
+ **Outcome:** {outcome}
558
+ **Duration:** {mins} min {secs} sec
559
+
560
+ **Summary:**
561
+ β€’ CPR Cycles: {self.session.cpr_cycles}
562
+ β€’ Shocks: {len(self.session.shocks)}
563
+ β€’ Epi Doses: {len(self.session.epi_doses)}
564
+ β€’ Amiodarone: {len(self.session.amiodarone_doses)} doses
565
+
566
+ πŸ“„ **Generating Code Blue Record...**
567
+
568
+ {self.generate_code_record()}
569
+ """
570
+
571
+ def _get_next_prompt(self) -> str:
572
+ """Get contextual next-action prompt."""
573
+ prompts = []
574
+
575
+ # Check if epi is due
576
+ if self.session.is_epi_due():
577
+ epi_time = self.session.time_since_last_epi()
578
+ if epi_time:
579
+ prompts.append(f"πŸ’‰ **Epi due!** (Last: {epi_time//60}m {epi_time%60}s ago)")
580
+ elif self.session.iv_access or self.session.io_access:
581
+ prompts.append("πŸ’‰ **Give Epi 1mg!** (Access established)")
582
+
583
+ # Check if rhythm check due
584
+ if self.session.is_rhythm_check_due():
585
+ prompts.append("πŸ” **2-min rhythm check due!**")
586
+
587
+ # Shockable rhythm reminders
588
+ if self.session.acls_path == ACLSPath.SHOCKABLE:
589
+ if len(self.session.shocks) >= 2 and not self.session.amiodarone_doses:
590
+ prompts.append("πŸ’Š Consider **Amiodarone 300mg**")
591
+
592
+ # Access reminder
593
+ if not self.session.iv_access and not self.session.io_access:
594
+ prompts.append("πŸ’‰ Need IV/IO access for meds!")
595
+
596
+ return "\n".join(prompts) if prompts else ""
597
+
598
+ def _extract_joules(self, text: str) -> Optional[int]:
599
+ """Extract joules from text like '200 joules' or '360J'."""
600
+ import re
601
+ match = re.search(r'(\d+)\s*[jJ]', text)
602
+ if match:
603
+ return int(match.group(1))
604
+ return None
605
+
606
+ def generate_code_record(self) -> str:
607
+ """Generate formatted Code Blue documentation."""
608
+ if not self.session:
609
+ return "No active session"
610
+
611
+ s = self.session
612
+ duration = s.get_run_time()
613
+
614
+ record = f"""
615
+ ╔══════════════════════════════════════════════════════════════╗
616
+ β•‘ CODE BLUE RECORD β•‘
617
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
618
+
619
+ **Date:** {s.start_time.strftime("%Y-%m-%d")}
620
+ **Time Called:** {s.start_time.strftime("%H:%M:%S")}
621
+ **Time Ended:** {s.end_time.strftime("%H:%M:%S") if s.end_time else "Ongoing"}
622
+ **Duration:** {duration//60} min {duration%60} sec
623
+ **Outcome:** {s.outcome or "Ongoing"}
624
+
625
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
626
+ **RHYTHM PROGRESSION:**
627
+ Initial: {s.events[0].details if s.events else "Unknown"}
628
+ Final: {s.current_rhythm.value}
629
+ ACLS Pathway: {s.acls_path.value if s.acls_path else "N/A"}
630
+
631
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
632
+ **CPR:**
633
+ Cycles: {s.cpr_cycles}
634
+ Compressor Changes: {len(s.compressor_changes)}
635
+
636
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
637
+ **DEFIBRILLATION:**
638
+ Total Shocks: {len(s.shocks)}
639
+ """
640
+ for i, (t, j) in enumerate(s.shocks, 1):
641
+ record += f" Shock {i}: {t.strftime('%H:%M:%S')} - {j}J\n"
642
+
643
+ record += f"""
644
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
645
+ **MEDICATIONS:**
646
+ Epinephrine 1mg x {len(s.epi_doses)} doses
647
+ """
648
+ for i, t in enumerate(s.epi_doses, 1):
649
+ record += f" Dose {i}: {t.strftime('%H:%M:%S')}\n"
650
+
651
+ if s.amiodarone_doses:
652
+ record += f"Amiodarone: {len(s.amiodarone_doses)} doses\n"
653
+ for t, mg in s.amiodarone_doses:
654
+ record += f" {t.strftime('%H:%M:%S')} - {mg}mg\n"
655
+
656
+ if s.other_meds:
657
+ record += "Other Medications:\n"
658
+ for t, med, dose in s.other_meds:
659
+ record += f" {t.strftime('%H:%M:%S')} - {med} {dose}\n"
660
+
661
+ record += f"""
662
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
663
+ **AIRWAY:**
664
+ Type: {s.airway_type or "BVM"}
665
+ Time Secured: {s.airway_time.strftime('%H:%M:%S') if s.airway_time else "N/A"}
666
+
667
+ **ACCESS:**
668
+ IV: {"Yes" if s.iv_access else "No"}
669
+ IO: {"Yes" if s.io_access else "No"}
670
+ Time: {s.access_time.strftime('%H:%M:%S') if s.access_time else "N/A"}
671
+
672
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
673
+ **EVENT LOG:**
674
+ """
675
+ record += "| Time | Run | Event | Details |\n"
676
+ record += "|------|-----|-------|--------|\n"
677
+ for e in s.events:
678
+ record += f"| {e.format_time()} | {e.format_run_time()} | {e.event_type} | {e.details} |\n"
679
+
680
+ record += """
681
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
682
+ Recorder: NurseGemma AI
683
+ Verified by: _______________________
684
+ """
685
+ return record
686
+
687
+ def get_status(self) -> str:
688
+ """Get current code status summary."""
689
+ if not self.session:
690
+ return "No active code"
691
+
692
+ s = self.session
693
+ run_time = s.get_run_time()
694
+
695
+ status = f"""
696
+ **⏱️ Run Time:** {run_time//60}:{run_time%60:02d}
697
+ **πŸ’“ Rhythm:** {s.current_rhythm.value}
698
+ **πŸ’ͺ CPR Cycles:** {s.cpr_cycles}
699
+ **⚑ Shocks:** {len(s.shocks)}
700
+ **πŸ’‰ Epi Doses:** {len(s.epi_doses)}
701
+ """
702
+
703
+ if s.is_epi_due():
704
+ elapsed = s.time_since_last_epi()
705
+ if elapsed:
706
+ status += f"\n⚠️ **Epi due!** ({elapsed//60}m {elapsed%60}s since last)"
707
+ else:
708
+ status += "\n⚠️ **No Epi given yet!**"
709
+
710
+ if s.is_rhythm_check_due():
711
+ status += "\nπŸ” **Rhythm check due!**"
712
+
713
+ return status
714
+
715
+
716
+ # Export for use in main app
717
+ code_blue_agent = CodeBlueAgent()