Cardiosense-AG commited on
Commit
12750f2
Β·
verified Β·
1 Parent(s): 2489819

Update pages/02_Workflow_UI.py

Browse files
Files changed (1) hide show
  1. pages/02_Workflow_UI.py +36 -91
pages/02_Workflow_UI.py CHANGED
@@ -3,20 +3,28 @@
3
  # Phase 3 β€” Tabbed Workflow UI (PCP Referral ↔ Specialist Review)
4
  # Fully integrated with V2 backend: store, billing, modal_templates, config,
5
  # explainability, guideline_annotator, ai_core.
 
 
 
 
 
 
6
  # -----------------------------------------------------------------------------
7
 
8
  from __future__ import annotations
9
 
10
  import os
 
 
11
  from pathlib import Path
12
- from typing import Any, Dict, List, Tuple, Optional
13
 
14
  import streamlit as st
15
 
16
  from src import store, billing, modal_templates, config
17
  from src.styles import inject_base_css
18
  from src.paths import faiss_index_dir, initialize_environment
19
- from src.prompt_builder import build_referral_summary, normalize_intake
20
  from src.explainability import chips_from_text, ensure_chip_schema
21
  from src.guideline_annotator import annotate_guidelines
22
  from src.ai_core import generate_soap_draft
@@ -28,7 +36,7 @@ inject_base_css()
28
  initialize_environment()
29
 
30
  st.title("Step 2 β€” Workflow")
31
- st.caption("PCP Referral β†’ Specialist Review. Demo only β€” de‑identified data; not for clinical use.")
32
 
33
  # --------------------------- Helpers ------------------------------------
34
 
@@ -70,9 +78,7 @@ def _as_text(x: Any) -> str:
70
  if isinstance(x, str):
71
  return x
72
  if isinstance(x, list):
73
- # flatten list items to bullet lines
74
- items = [str(s).strip() for s in x if str(s).strip()]
75
- return "\n".join(f"- {s}" for s in items)
76
  return str(x)
77
 
78
  def _soap_is_empty(soap: Dict[str, str]) -> bool:
@@ -135,7 +141,9 @@ def _note_markdown(case: Dict[str, Any], summary: str, soap: Dict[str, str], end
135
  )
136
  cites = ""
137
  if endnotes:
138
- cites = "\n## Guideline Citations\n" + "\n".join([f"- [{e.get('n',i+1)}] {e.get('doc','Guideline')} p.{e.get('page','')}" for i, e in enumerate(endnotes)])
 
 
139
  return header + body + cites + "\n"
140
 
141
  def _seed_once() -> None:
@@ -151,8 +159,7 @@ def _case_options() -> List[Dict[str, Any]]:
151
  return items
152
 
153
  def _get_case(case_id: str) -> Dict[str, Any]:
154
- case = store.read_case(case_id) or {}
155
- return case
156
 
157
  # --------------------------- Top banners --------------------------------
158
 
@@ -180,9 +187,8 @@ if not items:
180
  st.error("No cases available.")
181
  st.stop()
182
 
183
- # --- Optional: create a new case for ad hoc testing ---
184
  if st.button("βž• New Case", use_container_width=True):
185
- # Generate a new unique case ID
186
  new_id = store.new_case_id()
187
  store.create_case({
188
  "case_id": new_id,
@@ -196,28 +202,18 @@ if st.button("βž• New Case", use_container_width=True):
196
  "labs": "",
197
  "consent_obtained": False,
198
  },
199
- "soap_draft": {
200
- "subjective": "",
201
- "objective": "",
202
- "assessment": "",
203
- "plan": "",
204
- },
205
  "billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
206
  "explainability": {"manually_modified": False},
207
  })
208
  st.success(f"Created new case {new_id}. Refreshing list…")
209
  st.rerun()
210
 
211
- # --- Demo reset control ---
212
- import shutil
213
- from src import config
214
-
215
  if st.button("πŸ—‘οΈ Reset Demo (clear all cases)", use_container_width=True):
216
- cases_dir = config.paths_dict()["cases_dir"]
217
  try:
218
- shutil.rmtree(cases_dir)
219
  st.success("βœ… All demo cases cleared. The 4 default draft cases will be recreated on reload.")
220
- # Reset seeding flag so defaults are reloaded automatically
221
  st.session_state["_seeded"] = False
222
  st.rerun()
223
  except Exception as e:
@@ -238,14 +234,11 @@ st.session_state["_current_case_id"] = selected
238
  case = _get_case(selected)
239
  _case_summary_header(case)
240
 
241
-
242
  # --------------------------- Tabs ----------------------------------------
243
 
244
  tab_pcp, tab_spec = st.tabs(["PCP Referral", "Specialist Review"])
245
 
246
  # ===== PCP Referral Tab =====
247
- # Inside the PCP Referral tab (replace your existing block):
248
-
249
  with tab_pcp:
250
  is_draft = case.get("status") == "draft"
251
  p = case.get("patient", {}) or {}
@@ -259,9 +252,8 @@ with tab_pcp:
259
  "Clinical Summary",
260
  value=c.get("summary", "") or c.get("history", ""),
261
  height=160,
262
- placeholder=("Example: 72yo man with NYHA class III HFrEF (EF 30%). "
263
- "BP 110/70 HR 62. Echo: mild MR, no effusion. "
264
- "BNP 230; here for med optimization."),
265
  disabled=not is_draft,
266
  key=f"summary_{selected}",
267
  )
@@ -301,7 +293,6 @@ with tab_pcp:
301
  else:
302
  st.info("Referral is not editable (status is In Review or Completed).")
303
 
304
-
305
  # ===== Specialist Review Tab =====
306
  with tab_spec:
307
  status = case.get("status")
@@ -326,7 +317,6 @@ with tab_spec:
326
 
327
  st.markdown("**CPT Autosuggest**")
328
  elig_codes = [s.code for s in suggestions if s.eligible]
329
- # Show suggestion cards
330
  for s in suggestions:
331
  with st.container():
332
  status_icon = "βœ…" if s.eligible else "⚠️"
@@ -341,23 +331,18 @@ with tab_spec:
341
  chosen = None
342
 
343
  attested_default = bool((case.get("billing", {}) or {}).get("attested", False))
344
- attested = st.checkbox("I attest that the consult was performed and documented per interprofessional e‑consult requirements.", value=attested_default if read_only else False, disabled=read_only, key=f"att_{selected}")
345
 
346
  can_finalize = (not read_only) and (chosen is not None) and attested and (minutes >= 5)
347
-
348
- # Mirror finalize CTA on right
349
  finalize_right = st.button("Finalize Consult Note", type="primary", disabled=not can_finalize, use_container_width=True, key=f"finalize_r_{selected}")
350
 
351
  # ---------- LEFT: Referral & SOAP ----------
352
  with left:
353
  st.subheader("Referral Summary")
354
  summary = build_referral_summary(case)
355
-
356
- # --- Debug expander for referral summary (shows text sent to MedGemma) ---
357
  with st.expander("πŸ” Referral Summary (debug view)", expanded=False):
358
  st.code(summary, language="markdown")
359
 
360
-
361
  st.subheader("SOAP Draft")
362
  soap = case.get("soap_draft", {}) or {"subjective": "", "objective": "", "assessment": "", "plan": ""}
363
  gen_needed = (status == "submitted") and _soap_is_empty(soap)
@@ -365,7 +350,6 @@ with tab_spec:
365
  if gen_needed and not read_only:
366
  if st.button("Generate SOAP Draft (LLM)", key=f"gen_{selected}"):
367
  try:
368
- # Use normalized intake (patient + consult)
369
  result = generate_soap_draft(
370
  intake=case,
371
  mode="mapping",
@@ -374,47 +358,28 @@ with tab_spec:
374
  top_p=0.95,
375
  )
376
  s = result.get("soap", {}) or {}
377
-
378
- # --- Normalize and flatten list fields ---
379
  subj = _as_text(s.get("subjective", ""))
380
  obj = _as_text(s.get("objective", ""))
381
 
382
  assess_raw = s.get("assessment", "")
383
  plan_raw = s.get("plan", "")
384
-
385
- # Convert lists into newline-separated strings
386
  assess = "\n".join(assess_raw) if isinstance(assess_raw, list) else str(assess_raw)
387
  plan = "\n".join(plan_raw) if isinstance(plan_raw, list) else str(plan_raw)
388
 
389
- except Exception as e: # graceful fallback
390
- subj = summary
391
- obj = ""
392
- assess = "β€”"
393
- plan = "β€”"
394
  st.warning(f"LLM generation unavailable; seeded a minimal draft. ({type(e).__name__})")
395
  result = {"error": str(e)}
396
 
397
- # --- Debug expander for raw MedGemma output (full JSON response) ---
398
  with st.expander("🧠 MedGemma raw response (debug view)", expanded=False):
399
- import json
400
  st.code(json.dumps(result, indent=2), language="json")
401
 
402
- # Persist generated draft and mark unmodified so chips/guidelines show
403
  store.update_case(selected, {
404
- "soap_draft": {
405
- "subjective": subj.strip(),
406
- "objective": obj.strip(),
407
- "assessment": assess.strip(),
408
- "plan": plan.strip(),
409
- },
410
  "explainability": {"manually_modified": False},
411
  })
412
-
413
- # Refresh case so UI shows the new SOAP fields immediately
414
- case = _get_case(selected)
415
- soap = case.get("soap_draft", {})
416
- st.success("SOAP draft generated and saved. You can review it below.")
417
-
418
 
419
  c1, c2 = st.columns(2)
420
  with c1:
@@ -437,21 +402,18 @@ with tab_spec:
437
  "soap_draft": {"subjective": subj_new, "objective": obj_new, "assessment": assess_new, "plan": plan_new},
438
  "explainability": {"manually_modified": True},
439
  })
440
- # Re-read for consistency
441
- case = _get_case(selected)
442
- soap = case.get("soap_draft", soap)
443
 
444
- # Post‑hoc Explainability & Guidelines
445
  exp = (case.get("explainability", {}) or {})
446
  manually_modified = bool(exp.get("manually_modified", False))
447
-
448
  if read_only:
449
  manually_modified = False # allow showing in completed
450
 
451
  if not manually_modified and (soap.get("plan") or "").strip():
452
- with st.expander("Explainability tokens (post‑hoc)", expanded=False):
453
- _render_token_chips(_explainability_chips(soap.get("plan","")))
454
- with st.expander("Guideline citations (post‑hoc)", expanded=True):
455
  g = _run_guidelines_cached(selected, soap.get("plan",""))
456
  warn = (g.get("warning") or "").strip()
457
  if warn:
@@ -468,13 +430,12 @@ with tab_spec:
468
  st.caption("_No references found._")
469
  else:
470
  if not read_only and (status == "submitted"):
471
- if st.button("Re‑run Guidelines", key=f"rerun_{selected}"):
472
- # Re-run and mark unmodified so blocks show
473
  _run_guidelines_cached(selected, plan_new)
474
  store.update_case(selected, {"explainability": {"manually_modified": False}})
475
  st.rerun()
476
  else:
477
- st.caption("Guideline citations hidden after edits. Click **Re‑run Guidelines** to refresh.")
478
 
479
  # Mirror finalize CTA on left
480
  finalize_left = st.button("Finalize Consult Note", type="primary", disabled=not can_finalize, key=f"finalize_l_{selected}")
@@ -506,8 +467,7 @@ with tab_spec:
506
  rate = float(getattr(pick, "rate", 0.0)) if pick else 0.0
507
  claim = billing.build_837_claim(case, code=str(chosen), rate=rate, minutes=int(minutes), spoke=bool(spoke), attested=True)
508
  claim_path = config.make_export_path(selected, "Sample 837 Json.json")
509
- import json as _json
510
- Path(claim_path).write_text(_json.dumps(claim, ensure_ascii=False, indent=2), encoding="utf-8")
511
 
512
  # Status β†’ completed
513
  store.set_status(selected, "completed")
@@ -521,21 +481,6 @@ with tab_spec:
521
  st.caption(f"Claim: {claim_path}")
522
  st.rerun()
523
 
524
- # If already completed, surface actions to preview artifacts again
525
- if status == "completed":
526
- st.markdown("---")
527
- st.subheader("Completed Actions")
528
- if st.button("Send Consult Note to EHR (Preview)", key=f"preview_note_{selected}"):
529
- # Recompose or read the most recent note (rebuild for simplicity)
530
- g = _run_guidelines_cached(selected, (soap.get("plan") or ""))
531
- endnotes = g.get("endnotes", [])
532
- note_md = _note_markdown(case, summary, soap, endnotes=endnotes)
533
- modal_templates.show_consult_note_preview(note_md)
534
- if st.button("Submit 837 Claim (Preview)", key=f"preview_claim_{selected}"):
535
- pick = next((s for s in billing.autosuggest_cpt(minutes=minutes, spoke=spoke) if s.code == (case.get('billing',{}).get('cpt_code'))), None)
536
- rate = float(getattr(pick, "rate", 0.0)) if pick else 0.0
537
- claim = billing.build_837_claim(case, code=str(case.get('billing',{}).get('cpt_code')), rate=rate, minutes=int(minutes), spoke=bool(spoke), attested=True)
538
- modal_templates.show_837_claim_preview(claim)
539
 
540
 
541
 
 
3
  # Phase 3 β€” Tabbed Workflow UI (PCP Referral ↔ Specialist Review)
4
  # Fully integrated with V2 backend: store, billing, modal_templates, config,
5
  # explainability, guideline_annotator, ai_core.
6
+ # Consolidated fixes:
7
+ # - Clinical Summary (consult.summary) + Meds + Labs
8
+ # - Safe seeding, New Case, Reset Demo
9
+ # - SOAP list flattening + reliable UI refresh
10
+ # - Explainability/guidelines visibility + re-run
11
+ # - Billing autosuggest + finalize exports + modals
12
  # -----------------------------------------------------------------------------
13
 
14
  from __future__ import annotations
15
 
16
  import os
17
+ import json
18
+ import shutil
19
  from pathlib import Path
20
+ from typing import Any, Dict, List, Optional
21
 
22
  import streamlit as st
23
 
24
  from src import store, billing, modal_templates, config
25
  from src.styles import inject_base_css
26
  from src.paths import faiss_index_dir, initialize_environment
27
+ from src.prompt_builder import build_referral_summary
28
  from src.explainability import chips_from_text, ensure_chip_schema
29
  from src.guideline_annotator import annotate_guidelines
30
  from src.ai_core import generate_soap_draft
 
36
  initialize_environment()
37
 
38
  st.title("Step 2 β€” Workflow")
39
+ st.caption("PCP Referral β†’ Specialist Review. Demo only β€” de-identified data; not for clinical use.")
40
 
41
  # --------------------------- Helpers ------------------------------------
42
 
 
78
  if isinstance(x, str):
79
  return x
80
  if isinstance(x, list):
81
+ return "\n".join(str(s) for s in x)
 
 
82
  return str(x)
83
 
84
  def _soap_is_empty(soap: Dict[str, str]) -> bool:
 
141
  )
142
  cites = ""
143
  if endnotes:
144
+ cites = "\n## Guideline Citations\n" + "\n".join(
145
+ [f"- [{e.get('n', i+1)}] {e.get('doc','Guideline')} p.{e.get('page','')}" for i, e in enumerate(endnotes)]
146
+ )
147
  return header + body + cites + "\n"
148
 
149
  def _seed_once() -> None:
 
159
  return items
160
 
161
  def _get_case(case_id: str) -> Dict[str, Any]:
162
+ return store.read_case(case_id) or {}
 
163
 
164
  # --------------------------- Top banners --------------------------------
165
 
 
187
  st.error("No cases available.")
188
  st.stop()
189
 
190
+ # --- New Case ---
191
  if st.button("βž• New Case", use_container_width=True):
 
192
  new_id = store.new_case_id()
193
  store.create_case({
194
  "case_id": new_id,
 
202
  "labs": "",
203
  "consent_obtained": False,
204
  },
205
+ "soap_draft": {"subjective": "", "objective": "", "assessment": "", "plan": ""},
 
 
 
 
 
206
  "billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
207
  "explainability": {"manually_modified": False},
208
  })
209
  st.success(f"Created new case {new_id}. Refreshing list…")
210
  st.rerun()
211
 
212
+ # --- Reset Demo ---
 
 
 
213
  if st.button("πŸ—‘οΈ Reset Demo (clear all cases)", use_container_width=True):
 
214
  try:
215
+ shutil.rmtree(config.paths_dict()["cases_dir"])
216
  st.success("βœ… All demo cases cleared. The 4 default draft cases will be recreated on reload.")
 
217
  st.session_state["_seeded"] = False
218
  st.rerun()
219
  except Exception as e:
 
234
  case = _get_case(selected)
235
  _case_summary_header(case)
236
 
 
237
  # --------------------------- Tabs ----------------------------------------
238
 
239
  tab_pcp, tab_spec = st.tabs(["PCP Referral", "Specialist Review"])
240
 
241
  # ===== PCP Referral Tab =====
 
 
242
  with tab_pcp:
243
  is_draft = case.get("status") == "draft"
244
  p = case.get("patient", {}) or {}
 
252
  "Clinical Summary",
253
  value=c.get("summary", "") or c.get("history", ""),
254
  height=160,
255
+ placeholder=("E.g., NYHA III HFrEF (EF 30%). BP 110/70 HR 62, euvolemic. "
256
+ "Echo: EF 30%, mild MR, no effusion. Stable; med optimization."),
 
257
  disabled=not is_draft,
258
  key=f"summary_{selected}",
259
  )
 
293
  else:
294
  st.info("Referral is not editable (status is In Review or Completed).")
295
 
 
296
  # ===== Specialist Review Tab =====
297
  with tab_spec:
298
  status = case.get("status")
 
317
 
318
  st.markdown("**CPT Autosuggest**")
319
  elig_codes = [s.code for s in suggestions if s.eligible]
 
320
  for s in suggestions:
321
  with st.container():
322
  status_icon = "βœ…" if s.eligible else "⚠️"
 
331
  chosen = None
332
 
333
  attested_default = bool((case.get("billing", {}) or {}).get("attested", False))
334
+ attested = st.checkbox("I attest that the consult was performed and documented per interprofessional e-consult requirements.", value=attested_default if read_only else False, disabled=read_only, key=f"att_{selected}")
335
 
336
  can_finalize = (not read_only) and (chosen is not None) and attested and (minutes >= 5)
 
 
337
  finalize_right = st.button("Finalize Consult Note", type="primary", disabled=not can_finalize, use_container_width=True, key=f"finalize_r_{selected}")
338
 
339
  # ---------- LEFT: Referral & SOAP ----------
340
  with left:
341
  st.subheader("Referral Summary")
342
  summary = build_referral_summary(case)
 
 
343
  with st.expander("πŸ” Referral Summary (debug view)", expanded=False):
344
  st.code(summary, language="markdown")
345
 
 
346
  st.subheader("SOAP Draft")
347
  soap = case.get("soap_draft", {}) or {"subjective": "", "objective": "", "assessment": "", "plan": ""}
348
  gen_needed = (status == "submitted") and _soap_is_empty(soap)
 
350
  if gen_needed and not read_only:
351
  if st.button("Generate SOAP Draft (LLM)", key=f"gen_{selected}"):
352
  try:
 
353
  result = generate_soap_draft(
354
  intake=case,
355
  mode="mapping",
 
358
  top_p=0.95,
359
  )
360
  s = result.get("soap", {}) or {}
 
 
361
  subj = _as_text(s.get("subjective", ""))
362
  obj = _as_text(s.get("objective", ""))
363
 
364
  assess_raw = s.get("assessment", "")
365
  plan_raw = s.get("plan", "")
 
 
366
  assess = "\n".join(assess_raw) if isinstance(assess_raw, list) else str(assess_raw)
367
  plan = "\n".join(plan_raw) if isinstance(plan_raw, list) else str(plan_raw)
368
 
369
+ except Exception as e:
370
+ subj, obj, assess, plan = summary, "", "β€”", "β€”"
 
 
 
371
  st.warning(f"LLM generation unavailable; seeded a minimal draft. ({type(e).__name__})")
372
  result = {"error": str(e)}
373
 
 
374
  with st.expander("🧠 MedGemma raw response (debug view)", expanded=False):
 
375
  st.code(json.dumps(result, indent=2), language="json")
376
 
 
377
  store.update_case(selected, {
378
+ "soap_draft": {"subjective": subj.strip(), "objective": obj.strip(), "assessment": assess.strip(), "plan": plan.strip()},
 
 
 
 
 
379
  "explainability": {"manually_modified": False},
380
  })
381
+ # Force clean reload so text areas show new content
382
+ st.rerun()
 
 
 
 
383
 
384
  c1, c2 = st.columns(2)
385
  with c1:
 
402
  "soap_draft": {"subjective": subj_new, "objective": obj_new, "assessment": assess_new, "plan": plan_new},
403
  "explainability": {"manually_modified": True},
404
  })
405
+ # Keep on same run; next interactions will show re-computed state
 
 
406
 
407
+ # Post-hoc Explainability & Guidelines
408
  exp = (case.get("explainability", {}) or {})
409
  manually_modified = bool(exp.get("manually_modified", False))
 
410
  if read_only:
411
  manually_modified = False # allow showing in completed
412
 
413
  if not manually_modified and (soap.get("plan") or "").strip():
414
+ with st.expander("Explainability tokens (post-hoc)", expanded=False):
415
+ _render_token_chips(ensure_chip_schema(chips_from_text(soap.get("plan",""))))
416
+ with st.expander("Guideline citations (post-hoc)", expanded=True):
417
  g = _run_guidelines_cached(selected, soap.get("plan",""))
418
  warn = (g.get("warning") or "").strip()
419
  if warn:
 
430
  st.caption("_No references found._")
431
  else:
432
  if not read_only and (status == "submitted"):
433
+ if st.button("Re-run Guidelines", key=f"rerun_{selected}"):
 
434
  _run_guidelines_cached(selected, plan_new)
435
  store.update_case(selected, {"explainability": {"manually_modified": False}})
436
  st.rerun()
437
  else:
438
+ st.caption("Guideline citations hidden after edits. Click **Re-run Guidelines** to refresh.")
439
 
440
  # Mirror finalize CTA on left
441
  finalize_left = st.button("Finalize Consult Note", type="primary", disabled=not can_finalize, key=f"finalize_l_{selected}")
 
467
  rate = float(getattr(pick, "rate", 0.0)) if pick else 0.0
468
  claim = billing.build_837_claim(case, code=str(chosen), rate=rate, minutes=int(minutes), spoke=bool(spoke), attested=True)
469
  claim_path = config.make_export_path(selected, "Sample 837 Json.json")
470
+ Path(claim_path).write_text(json.dumps(claim, ensure_ascii=False, indent=2), encoding="utf-8")
 
471
 
472
  # Status β†’ completed
473
  store.set_status(selected, "completed")
 
481
  st.caption(f"Claim: {claim_path}")
482
  st.rerun()
483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
 
486