File size: 27,363 Bytes
a8afca8 8c86ed5 89c193a 8f98a41 89c193a 8f98a41 8c86ed5 a8afca8 12750f2 c2ce181 914acd4 12750f2 8f98a41 a8afca8 ea0cbea a8afca8 914acd4 12750f2 ee4cfe2 fc9442e ee4cfe2 914acd4 89c193a 914acd4 89c193a c2ce181 914acd4 89c193a 914acd4 89c193a 914acd4 ee4cfe2 914acd4 ee4cfe2 914acd4 ee4cfe2 914acd4 8c86ed5 914acd4 90ef4af 914acd4 89c193a 914acd4 90ef4af 89c193a 90ef4af 89c193a 90ef4af 89c193a 90ef4af e914999 8f98a41 914acd4 e914999 ee4cfe2 e914999 ee4cfe2 e914999 914acd4 e914999 90ef4af e914999 914acd4 e914999 12750f2 e914999 914acd4 e914999 914acd4 e914999 914acd4 8f98a41 914acd4 89c193a 914acd4 ee4cfe2 914acd4 8c86ed5 914acd4 8c86ed5 89c193a 914acd4 8c86ed5 ba889f1 914acd4 8c86ed5 90ef4af ea20ae2 79d06d1 ea20ae2 12750f2 ea20ae2 ee4cfe2 89c193a ee4cfe2 ea20ae2 8f98a41 79d06d1 8f98a41 79d06d1 8f98a41 ea20ae2 90ef4af 914acd4 89c193a 914acd4 89c193a 8c86ed5 89c193a 914acd4 8c86ed5 914acd4 a8526cc 914acd4 6bc0e92 89c193a 6bc0e92 a8526cc 914acd4 6bc0e92 914acd4 a8526cc 6bc0e92 914acd4 6bc0e92 914acd4 6bc0e92 914acd4 89c193a 615d0ff 16bbb85 914acd4 89c193a 914acd4 89c193a 8f98a41 89c193a 8f98a41 89c193a 8f98a41 89c193a 8f98a41 89c193a 8af3158 8f98a41 8af3158 8f98a41 8af3158 89c193a 8f98a41 89c193a 8f98a41 89c193a 8f98a41 89c193a 8f98a41 89c193a 90ef4af 89c193a 914acd4 8f98a41 89c193a 8f98a41 89c193a e914999 89c193a 8f98a41 89c193a 8f98a41 e914999 8f98a41 e914999 8f98a41 e914999 89c193a 8f98a41 89c193a e914999 8f98a41 e914999 8f98a41 e914999 89c193a e914999 8f98a41 e914999 89c193a 8f98a41 89c193a 914acd4 c2ce181 914acd4 9eee2b6 9025511 a07352f 8fd3008 a61f194 a53275b 0884be0 c6dbabd ff730ad 99004db 8f98a41 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 |
# pages/02_Workflow_UI.py
# -----------------------------------------------------------------------------
# Phase-3+ Simplified β 3-tab workflow
# Tabs:
# 1) PCP Referral
# 2) Specialist Review (SOAP + Guideline Rationale + time/spoke/attestation)
# 3) Documentation & Billing (completed cases only: EHR note, CPT + ICD, 837 JSON)
#
# Assumes:
# - No Referral Rationale
# - Single Guideline Rationale layer (bullets) via guideline_annotator
# - ai_core.generate_soap_draft returns 4-string SOAP keys
# - billing.autosuggest_cpt + billing.autosuggest_icd + billing.build_837_claim
# -----------------------------------------------------------------------------
from __future__ import annotations
import json
import shutil
import html
from pathlib import Path
from typing import Any, Dict, List, Optional
from datetime import datetime
import streamlit as st
from src import store, billing, modal_templates, config
from src.styles import inject_base_css
from src.paths import faiss_index_dir, initialize_environment
from src.prompt_builder import build_referral_summary
from src.explainability import text_hash, normalize_text, is_stale
from src.guideline_annotator import generate_guideline_rationale
from src.ai_core import generate_soap_draft
from src.model_loader import active_model_status
# --------------------------- Page Setup ---------------------------------
st.set_page_config(page_title="Step 2 β Workflow", page_icon="π©Ί", layout="wide")
inject_base_css()
initialize_environment()
st.session_state.setdefault("_show_exports", True) # persist preview section visibility across reloads
st.title("Step 2 β Workflow")
st.caption("PCP Referral β Specialist Review β Documentation & Billing. Demo only β de-identified data; not for clinical use.")
# --------------------------- Helpers ------------------------------------
def _model_banner() -> None:
status = active_model_status()
sel = status.get("selected_id", "")
device = status.get("device", "CPU")
if device == "CPU":
text = f"βοΈ Using {sel} on CPU (fallback mode)"
else:
short = "27B" if "27" in str(sel) else ("4B" if "4" in str(sel) else "GPU")
text = f"βοΈ Using {sel} on GPU ({short})"
hint = f"Primary={status.get('primary_id','')} | Fallback={status.get('fallback_id','')} | forced_cpu={status.get('forced_cpu', False)}"
st.markdown(
f"<div style='text-align:right'><span class='badge accent' title='{hint}'>{text}</span></div>",
unsafe_allow_html=True,
)
def _faiss_index_present() -> bool:
idx = faiss_index_dir()
return (idx / "faiss.index").exists() and (idx / "chunks.jsonl").exists() and (idx / "index_info.json").exists()
def _as_text(x: Any) -> str:
return x if isinstance(x, str) else ("\n".join(x) if isinstance(x, list) else str(x or ""))
def _status_badge(status: str) -> str:
label = {"draft": "Draft", "submitted": "In Review", "completed": "Completed"}.get(status, status.title())
klass = "badge" + (" accent" if status in {"submitted"} else "")
return f"<span class='{klass}'>{label}</span>"
def _case_summary_header(case: Dict[str, Any]) -> None:
p = case.get("patient", {}) or {}
c = case.get("consult", {}) or {}
left, right = st.columns([3, 1])
with left:
demo = " β’ ".join([s for s in [p.get("name") or "Unknown", str(p.get("age") or ""), p.get("sex") or ""] if s])
specialty = c.get("specialty") or "β"
st.subheader(demo)
st.caption(f"Specialty: {specialty}")
with right:
st.markdown(_status_badge(case.get("status", "draft")), unsafe_allow_html=True)
def _green_info_box(title: str, html_body: str) -> None:
st.markdown(
f"""
<div style="
background:#e8f5e9;
border-left:4px solid #2e7d32;
padding:0.75rem 1rem;
border-radius:6px;
margin-top:0.25rem;">
<div style="font-weight:600;">{title}</div>
<div style="margin-top:0.25rem;">{html_body}</div>
</div>
""",
unsafe_allow_html=True,
)
def _render_guideline_bullets(items: List[str]) -> None:
if not items:
return
lis = "".join(f"<li>{html.escape(i)}</li>" for i in items)
_green_info_box("π Guideline Rationale", f"<ul style='margin:0 0 0 1.1rem'>{lis}</ul>")
def _exports_dir() -> Path:
# Robustly get exports dir from config; fallback to ./exports
try:
pd = getattr(config, "paths_dict", None)
if callable(pd):
return Path(pd()["exports_dir"])
except Exception:
pass
try:
ed = getattr(config, "EXPORTS_DIR", None) or getattr(config, "exports_dir", None)
if ed:
return Path(ed)
except Exception:
pass
return Path("exports")
def _latest_export(case_id: str, pattern_suffix: str) -> Optional[Path]:
# pattern_suffix examples:
# "Sample Consultation Report.md"
# "Sample 837 Json.json"
exp = _exports_dir()
if not exp.exists():
return None
matches = sorted(exp.glob(f"EC-{case_id}_*{pattern_suffix}"), key=lambda p: p.stat().st_mtime)
return matches[-1] if matches else None
def _note_markdown(case: Dict[str, Any], summary: str, soap: Dict[str, str],
guideline_points: List[str], endnotes: List[Dict[str, Any]]) -> str:
"""Generate formatted EHR-style consultation report (matches sample-like format)."""
p = case.get("patient", {}) or {}
c = case.get("consult", {}) or {}
consult_id = case.get("case_id", "")
today = datetime.utcnow().strftime("%B %d, %Y")
patient_line = f"**Patient:** {p.get('name','Unknown')} ({p.get('sex','')}, {p.get('age','')})"
specialty = c.get("specialty") or "Cardiology"
question = c.get("question") or ""
referring = c.get("referrer") or "Referring provider not specified"
header = (
f"# {specialty} E-Consult Report\n\n"
f"{patient_line} \n"
f"**Consult ID:** {consult_id} \n"
f"**Date:** {today} \n"
f"**Specialty:** {specialty} \n"
f"**Referring Provider:** {referring}\n\n"
"---\n\n"
)
referral_section = ""
if question:
referral_section += f"### Referral Question\n> {question.strip()}\n\n"
if summary.strip():
referral_section += f"### Clinical Summary\n{summary.strip()}\n\n---\n\n"
soap_section = (
f"### Subjective\n{(soap.get('subjective') or '').strip()}\n\n"
f"### Objective\n{(soap.get('objective') or '').strip()}\n\n"
f"### Assessment\n{(soap.get('assessment') or '').strip()}\n\n"
f"### Plan\n{(soap.get('plan') or '').strip()}\n\n---\n\n"
)
guideline_section = ""
if guideline_points:
bullets = "\n".join([f"- {pt}" for pt in guideline_points])
guideline_section = f"### Guideline Alignment\n{bullets}\n\n---\n\n"
endnote_section = ""
if endnotes:
cites = "\n".join(
[f"- [{e.get('n', i+1)}] {e.get('doc','Guideline')}"
+ (f" p.{e.get('page','')}" if e.get('page') else "") for i, e in enumerate(endnotes)]
)
endnote_section = f"### References\n{cites}\n\n---\n\n"
attestation = (
"### Consulting Clinician Attestation\n"
"I personally reviewed the provided information, generated this report, "
"and confirm that I spent the documented time reviewing and documenting this e-consult.\n"
)
return header + referral_section + soap_section + guideline_section + endnote_section + attestation
def _soap_is_empty(soap: Dict[str, str]) -> bool:
return not any((soap or {}).get(k, "").strip() for k in ("subjective", "objective", "assessment", "plan"))
def _seed_once() -> None:
if not st.session_state.get("_seeded", False):
store.seed_cases(reset=True)
st.session_state["_seeded"] = True
def _case_options() -> List[Dict[str, Any]]:
items = store.list_cases()
if not items:
_seed_once()
items = store.list_cases()
return items
def _get_case(case_id: str) -> Dict[str, Any]:
return store.read_case(case_id) or {}
# --------------------------- Global callouts --------------------------------
_model_banner()
if not _faiss_index_present():
with st.container():
st.info("Guideline rationales need a FAISS index. No index detected β build it in **Step 1 β RAG Corpus Prep**.", icon="βΉοΈ")
try:
st.page_link("pages/01_RAG_Corpus_Prep.py", label="Open RAG Prep")
except Exception:
st.caption("Open the βStep 1 β RAG Corpus Prepβ page from the sidebar.")
st.divider()
# --------------------------- Case selector --------------------------------
if not store.list_cases():
_seed_once()
items = _case_options()
if not items:
st.error("No cases available.")
st.stop()
# New case
if st.button("β New Case", use_container_width=True):
new_id = store.new_case_id()
store.create_case({
"case_id": new_id,
"status": "draft",
"patient": {"name": "New Patient", "age": None, "sex": ""},
"consult": {
"specialty": "Cardiology",
"question": "",
"summary": "",
"medications": "",
"labs": "",
"consent_obtained": False,
},
"soap_draft": {"subjective": "", "objective": "", "assessment": "", "plan": ""},
"billing": {"minutes": 5, "spoke": False, "cpt_code": None, "attested": False},
"explainability": {
"baseline": {"assessment_hash": "", "plan_hash": ""},
"guideline_rationale": [],
},
})
st.success(f"Created new case {new_id}. Refreshing listβ¦")
st.rerun()
# Reset demo (use seed reset; don't rely on internal paths)
if st.button("ποΈ Reset Demo (clear all cases)", use_container_width=True):
try:
store.seed_cases(reset=True)
st.success("β
All demo cases cleared and re-seeded.")
st.session_state["_seeded"] = True
st.rerun()
except Exception as e:
st.error(f"Failed to reset cases: {e}")
# Selection
default_case_id = st.session_state.get("_current_case_id") or items[0]["case_id"]
opt_by_id = {i["case_id"]: i for i in items}
case_ids = list(opt_by_id.keys())
selected = st.selectbox(
"Select case",
options=case_ids,
index=case_ids.index(default_case_id) if default_case_id in case_ids else 0,
format_func=lambda cid: f"{cid} β {opt_by_id[cid].get('patient_name') or ''} [{opt_by_id[cid]['status']}]",
)
st.session_state["_current_case_id"] = selected
case = _get_case(selected)
_case_summary_header(case)
# --------------------------- Tabs ----------------------------------------
tab_pcp, tab_spec, tab_bill = st.tabs(["PCP Referral", "Specialist Review", "Documentation & Billing"])
# ===== PCP REFERRAL TAB =====
with tab_pcp:
is_draft = case.get("status") == "draft"
p = case.get("patient", {}) or {}
c = case.get("consult", {}) or {}
c1, c2, c3 = st.columns([2, 1, 1])
with c1:
patient_name = st.text_input("Patient Name", value=p.get("name", ""), disabled=not is_draft, key=f"pt_name_{selected}")
consult_q = st.text_area("Consult Question", value=c.get("question", ""), height=80, disabled=not is_draft, key=f"q_{selected}")
clinical_summary = st.text_area(
"Clinical Summary",
value=c.get("summary", "") or c.get("history", ""),
height=160,
placeholder=("E.g., NYHA III HFrEF (EF 30%). BP 110/70 HR 62. Echo EF 30%, mild MR. Stable; med optimization."),
disabled=not is_draft,
key=f"summary_{selected}",
)
with c2:
age = st.number_input("Age", min_value=0, max_value=120, value=int(p.get("age") or 0), disabled=not is_draft, key=f"age_{selected}")
sex = st.selectbox("Sex", ["F", "M", "Other", ""], index=["F","M","Other",""].index(p.get("sex") or ""), disabled=not is_draft, key=f"sex_{selected}")
specialty = st.text_input("Specialty", value=c.get("specialty", "") or "", disabled=not is_draft, key=f"spec_{selected}")
with c3:
medications = st.text_area("Medications", value=c.get("medications", ""), height=100, disabled=not is_draft, key=f"meds_{selected}")
labs = st.text_area("Labs / Key Values", value=c.get("labs", ""), height=100, disabled=not is_draft, key=f"labs_{selected}")
consent = bool(c.get("consent_obtained", False))
consent = st.checkbox("Patient consent obtained", value=consent, disabled=not is_draft, key=f"consent_{selected}")
col = st.columns([1,1,6])[0]
with col:
if is_draft:
if st.button("Submit Referral", type="primary", use_container_width=True, key=f"submit_{selected}"):
if not consent:
st.error("Consent is required to submit.")
else:
patch = {
"patient": {"name": patient_name.strip(), "age": int(age) if age else None, "sex": sex},
"consult": {
"specialty": specialty.strip(),
"question": consult_q.strip(),
"summary": clinical_summary.strip(),
"medications": medications.strip(),
"labs": labs.strip(),
"consent_obtained": True,
},
}
store.update_case(selected, patch)
store.set_status(selected, "submitted")
st.success("Referral submitted. Activate the Specialist Review tab to continue.")
st.rerun()
else:
st.info("Referral is not editable (status is In Review or Completed).")
# ===== SPECIALIST REVIEW TAB =====
with tab_spec:
status = case.get("status")
if status == "draft":
st.info("Submit the referral on the PCP tab to activate Specialist Review.")
st.stop()
read_only = status == "completed"
st.subheader("SOAP Draft")
soap = case.get("soap_draft", {}) or {"subjective": "", "objective": "", "assessment": "", "plan": ""}
# Generate SOAP if needed
gen_needed = (status == "submitted") and _soap_is_empty(soap)
if gen_needed and not read_only:
if st.button("Generate SOAP Draft (LLM)", key=f"gen_{selected}"):
try:
result = generate_soap_draft(
intake=case,
max_new_tokens=550,
temperature=0.2,
top_p=0.95,
)
s = result.get("soap", {}) or {}
subj = _as_text(s.get("subjective", ""))
obj = _as_text(s.get("objective", ""))
assess = _as_text(s.get("assessment", ""))
plan = _as_text(s.get("plan", ""))
# initialize baseline hashes for guideline staleness
explain_patch = {
"baseline": {"assessment_hash": text_hash(assess), "plan_hash": text_hash(plan)},
"guideline_rationale": [],
}
except Exception as e:
subj, obj, assess, plan = build_referral_summary(case), "", "β", "β"
explain_patch = {"baseline": {"assessment_hash": text_hash(assess), "plan_hash": text_hash(plan)}, "guideline_rationale": []}
st.warning(f"LLM generation unavailable; seeded a minimal draft. ({type(e).__name__})")
result = {"error": str(e)}
store.update_case(selected, {
"soap_draft": {"subjective": subj.strip(), "objective": obj.strip(), "assessment": assess.strip(), "plan": plan.strip()},
"explainability": explain_patch,
})
# allow simple tab toggle to show new SOAP; no extra rerun complexity here
st.success("SOAP draft generated. If fields appear empty, toggle tabs once to refresh the view.")
# Editable fields
c1, c2 = st.columns(2)
with c1:
subj_new = st.text_area("Subjective", value=soap.get("subjective",""), height=140, disabled=read_only, key=f"subj_{selected}")
with c2:
obj_new = st.text_area("Objective", value=soap.get("objective",""), height=140, disabled=read_only, key=f"obj_{selected}")
assess_new = st.text_area("Assessment", value=soap.get("assessment",""), height=140, disabled=read_only, key=f"assess_{selected}")
plan_new = st.text_area("Plan", value=soap.get("plan",""), height=180, disabled=read_only, key=f"plan_{selected}")
# Persist edits (whitespace-insensitive) while in submitted state
if (not read_only) and (status == "submitted"):
changed = any([
normalize_text(subj_new) != normalize_text(soap.get("subjective") or ""),
normalize_text(obj_new) != normalize_text(soap.get("objective") or ""),
normalize_text(assess_new) != normalize_text(soap.get("assessment") or ""),
normalize_text(plan_new) != normalize_text(soap.get("plan") or ""),
])
if changed:
store.update_case(selected, {
"soap_draft": {"subjective": subj_new, "objective": obj_new, "assessment": assess_new, "plan": plan_new},
})
# Guideline rationale (bullets) β stale if Assessment OR Plan changed vs baselines
exp = (case.get("explainability", {}) or {})
baseline = (exp.get("baseline") or {})
assess_hash0 = str(baseline.get("assessment_hash") or "")
plan_hash0 = str(baseline.get("plan_hash") or "")
stale = is_stale(assess_new, assess_hash0) or is_stale(plan_new, plan_hash0)
# ---------------------------------------------------------
# HARD GUARD: NEVER run guideline rationale before SOAP exists
# ---------------------------------------------------------
if _soap_is_empty(soap):
# Do nothing; skip guideline logic entirely
# (Prevents early 200-token LLM call)
pass
else:
# Guard: only show guideline rationale if we have SOAP content
if not _soap_is_empty(soap):
if not stale and (normalize_text(assess_new + plan_new)):
# Use stored bullets if available; else compute once and store
stored_points = (exp.get("guideline_rationale") or [])
if stored_points:
_render_guideline_bullets(stored_points)
else:
g = generate_guideline_rationale(assessment_text=assess_new, plan_text=plan_new)
warn = (g.get("warning") or "").strip()
if warn:
st.info(warn)
try:
st.page_link("pages/01_RAG_Corpus_Prep.py", label="Open RAG Prep")
except Exception:
pass
points = g.get("rationale", []) or []
_render_guideline_bullets(points)
if points:
store.update_case(selected, {"explainability": {"guideline_rationale": points}})
else:
if (not read_only) and (status == "submitted"):
if st.button("β» Re-run Guidelines", key=f"rerun_{selected}"):
g = generate_guideline_rationale(assessment_text=assess_new, plan_text=plan_new)
points = g.get("rationale", []) or []
store.update_case(selected, {
"explainability": {
"baseline": {"assessment_hash": text_hash(assess_new), "plan_hash": text_hash(plan_new)},
"guideline_rationale": points,
}
})
st.rerun()
else:
st.caption("Guideline rationale hidden after edits. Click **Re-run Guidelines** to refresh.")
st.divider()
# Specialist Review metadata (3 controls)
st.subheader("Specialist Review")
b = case.get("billing", {}) or {}
minutes_val = int(b.get("minutes", 5))
minutes = st.slider("Time spent (minutes)", min_value=5, max_value=30, value=minutes_val, step=1, disabled=read_only, key=f"mins_{selected}")
spoke = bool(b.get("spoke", False))
spoke = st.checkbox("Spoke to Referring Physician", value=spoke, disabled=read_only, key=f"spoke_{selected}")
attested = bool(b.get("attested", False)) if read_only else False
attested = st.checkbox("I attest that the consult note is complete.", value=attested, disabled=read_only, key=f"att_{selected}")
# Persist time/spoke as they change (not attested until finalize)
if (not read_only) and (status == "submitted"):
if (minutes != minutes_val) or (spoke != bool(b.get("spoke", False))):
store.update_case(selected, {"billing": {"minutes": minutes, "spoke": bool(spoke)}})
# Finalize controls
can_finalize = (not read_only) and (minutes >= 5) and bool(st.session_state.get(f"att_{selected}", False))
finalize_btn = st.button("Finalize Consult", type="primary", disabled=not can_finalize, key=f"finalize_{selected}")
if finalize_btn:
# Use persisted guideline bullets; do NOT regenerate at finalize
points = (case.get("explainability", {}) or {}).get("guideline_rationale") or []
endnotes: List[Dict[str, Any]] = []
soap_now = case.get("soap_draft", {}) or {}
note_md = _note_markdown(
case,
build_referral_summary(case),
{
"subjective": subj_new or soap_now.get("subjective",""),
"objective": obj_new or soap_now.get("objective",""),
"assessment": assess_new or soap_now.get("assessment",""),
"plan": plan_new or soap_now.get("plan",""),
},
points, endnotes=endnotes
)
# Save exports deterministically
note_path = config.make_export_path(selected, "Sample Consultation Report.md")
Path(note_path).write_text(note_md, encoding="utf-8")
# Persist billing info and mark completed
store.update_case(selected, {
"billing": {"minutes": minutes, "spoke": bool(spoke), "attested": True},
"status": "completed",
})
st.session_state["_show_exports"] = True
st.success("β
Finalized. Consult note exported; proceed to the **Documentation & Billing** tab to submit a claim.")
st.caption(f"Note: {note_path}")
# ===== DOCUMENTATION & BILLING TAB =====
with tab_bill:
status = case.get("status")
if status != "completed":
st.info("π Complete the consult first in the **Specialist Review** tab to access Documentation & Billing.")
st.stop()
# EHR Note preview expander
st.subheader("EHR Consultation Note")
note_path = _latest_export(selected, "Sample Consultation Report.md")
if note_path and note_path.exists():
with st.expander("π View EHR Consultation Note", expanded=True):
md = note_path.read_text(encoding="utf-8")
st.markdown(md)
st.caption(f"File: {note_path.name}")
else:
st.warning("No consultation note found for this case. Finalize the consult to generate one.")
st.divider()
# CPT Autosuggest + selection
st.subheader("CPT Suggestions & Claim")
b = case.get("billing", {}) or {}
minutes = int(b.get("minutes", 5))
spoke = bool(b.get("spoke", False))
suggestions = billing.autosuggest_cpt(minutes=minutes, spoke=spoke)
elig_codes = [s.code for s in suggestions if s.eligible]
st.markdown("**CPT Autosuggest**")
for s in suggestions:
with st.container():
icon = "β
" if s.eligible else "β οΈ"
st.write(f"{icon} **{s.code}** β {s.descriptor} β ${s.rate:.2f}")
st.caption(s.why)
# ICD-10 suggestions block (from Assessment + Plan)
soap_now = case.get("soap_draft", {}) or {}
icd_suggestions = billing.autosuggest_icd(
assessment_text=soap_now.get("assessment", ""),
plan_text=soap_now.get("plan", "")
)
st.markdown("**ICD-10 Diagnosis Suggestions**")
if icd_suggestions:
for s in icd_suggestions:
st.write(f"β
**{s.code}** β {s.description} (confidence {int(s.confidence*100)}%)")
st.caption(s.why)
icd_options = [f"{s.code} β {s.description}" for s in icd_suggestions]
picked_icd = st.multiselect(
"Select diagnosis code(s) to include on the claim",
options=icd_options,
default=icd_options[:1],
key=f"icd_sel_{selected}"
)
picked_icd_codes = [o.split(" β ")[0] for o in picked_icd]
else:
st.info("No ICD-10 suggestions available for this case.")
picked_icd_codes: List[str] = []
chosen_default = b.get("cpt_code") or (elig_codes[0] if elig_codes else None)
if elig_codes:
chosen = st.selectbox(
"Choose CPT (eligible)",
options=elig_codes,
index=elig_codes.index(chosen_default) if (chosen_default in elig_codes) else 0,
key=f"cpt_{selected}"
)
else:
st.warning("No eligible CPT for the current minutes/spoke combination.", icon="β οΈ")
chosen = None
att_claim = st.checkbox("I attest the claim details are accurate and ready to submit.", key=f"claim_att_{selected}")
submit_claim = st.button("Submit Claim", type="primary", disabled=not (chosen and att_claim), key=f"submit_claim_{selected}")
if submit_claim and chosen:
# Persist chosen code + ICD selections
store.update_case(selected, {
"billing": {
"cpt_code": chosen,
"icd_codes": picked_icd_codes,
}
})
# Build and save 837 JSON (includes ICD codes)
pick = next((s for s in suggestions if s.code == chosen), None)
rate = float(getattr(pick, "rate", 0.0)) if pick else 0.0
claim = billing.build_837_claim(
case,
code=str(chosen),
rate=rate,
minutes=minutes,
spoke=bool(spoke),
attested=True,
icd_codes=picked_icd_codes,
)
claim_path = config.make_export_path(selected, "Sample 837 Json.json")
Path(claim_path).write_text(json.dumps(claim, ensure_ascii=False, indent=2), encoding="utf-8")
st.success("Claim submitted. 837 JSON generated (see preview below).")
st.caption(f"Claim: {claim_path.name}")
# 837 preview expander (always attempt to show latest if exists)
claim_path = _latest_export(selected, "Sample 837 Json.json")
if claim_path and claim_path.exists():
with st.expander("π§Ύ View 837 JSON Message", expanded=True):
try:
claim_data = json.loads(claim_path.read_text(encoding="utf-8"))
st.json(claim_data)
except Exception:
st.code(claim_path.read_text(encoding="utf-8"), language="json")
st.caption(f"File: {claim_path.name}")
|