Spaces:
Sleeping
Sleeping
Claude commited on
Commit Β·
c7ad943
1
Parent(s): aad09a1
feat: Add Code Blue Mode
Browse files- app.py +104 -1
- 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("
|
| 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()
|