Slaiwala commited on
Commit
c30526c
·
verified ·
1 Parent(s): 66b99bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +563 -210
app.py CHANGED
@@ -1,11 +1,11 @@
1
  # app.py — Spine Coder (Chatbot + Feedback + Session Logs) — Gradio 4.x
2
  # ------------------------------------------------------------------------------
3
- # FINAL-v3.0 (Stable)
4
- # - Bulletproof core import (SPINE_CORE_PATH override + SHA print)
5
- # - Structured local JSONL logging per session
6
- # - Live dataset logging to a Hugging Face dataset repo (no Space rebuilds)
7
- # - Diagnostics panel with Probe + Self-Test for live logging
8
- # - Clean UI with Case Modifiers, raw JSON, and feedback
9
  # ------------------------------------------------------------------------------
10
 
11
  import os
@@ -16,16 +16,18 @@ import pathlib
16
  import traceback
17
  from datetime import datetime, timezone
18
  from typing import Any, Dict, List, Tuple
 
19
  import pandas as pd
20
  import gradio as gr
21
 
22
- # ==== Core Import =============================================================
23
  import importlib, importlib.util, importlib.machinery
24
  import sys as __sys, os as __os, inspect as __inspect, hashlib
25
 
26
- __sys.path.insert(0, __os.path.abspath("."))
27
 
28
  def _purge_spine_modules():
 
29
  for k in list(__sys.modules):
30
  if k == "spine_coder" or k.startswith("spine_coder."):
31
  __sys.modules.pop(k, None)
@@ -33,7 +35,6 @@ def _purge_spine_modules():
33
 
34
  def _sha256(path: str) -> str:
35
  try:
36
- import hashlib
37
  h = hashlib.sha256()
38
  with open(path, "rb") as f:
39
  for chunk in iter(lambda: f.read(8192), b""):
@@ -43,6 +44,10 @@ def _sha256(path: str) -> str:
43
  return "unknown"
44
 
45
  def _load_core_from_file(path: str):
 
 
 
 
46
  path = __os.path.abspath(path)
47
  if not __os.path.exists(path):
48
  return None
@@ -50,22 +55,27 @@ def _load_core_from_file(path: str):
50
  loader = importlib.machinery.SourceFileLoader(name, path)
51
  spec = importlib.util.spec_from_loader(name, loader)
52
  mod = importlib.util.module_from_spec(spec)
53
- __sys.modules[name] = mod
54
- loader.exec_module(mod)
55
  return mod
56
 
57
  def _force_import_core():
 
58
  _purge_spine_modules()
 
 
59
  forced = __os.environ.get("SPINE_CORE_PATH")
60
  if forced and __os.path.exists(forced):
61
  mod = _load_core_from_file(forced)
62
  if mod:
63
  print("[CORE] forced path:", forced, "sha:", _sha256(forced))
64
  return mod
 
 
65
  for rel in [
66
  "spine_coder_core.py",
67
  "spine_coder/spine_coder_core.py",
68
- "spine_coder/spine_coder/spine_coder_core.py",
69
  ]:
70
  p = __os.path.abspath(rel)
71
  if __os.path.exists(p):
@@ -73,26 +83,38 @@ def _force_import_core():
73
  if mod:
74
  print("[CORE] loaded file:", p, "sha:", _sha256(p))
75
  return mod
 
 
76
  for modname in [
77
  "spine_coder.spine_coder.spine_coder_core",
78
  "spine_coder.spine_coder_core",
79
  ]:
80
  try:
81
  mod = importlib.import_module(modname)
82
- mod = importlib.reload(mod)
 
 
83
  return mod
84
  except Exception:
85
  pass
86
- raise ImportError("Unable to locate spine_coder_core.py")
 
87
 
88
  _core = _force_import_core()
89
  suggest_with_cpt_billing = _core.suggest_with_cpt_billing
90
 
91
- # ==== Startup Probe ===========================================================
 
 
 
 
 
 
92
  try:
93
  _probe_note = "Discectomies at C4–C5, C5–C6, and C6–C7. Interbody cages and anterior plate spanning C4–C7."
94
  _probe = suggest_with_cpt_billing(_probe_note, payer="Medicare", top_k=5)
95
  print("[PROBE] build:", _probe.get("build"))
 
96
  except Exception as e:
97
  print("[PROBE] failed:", e)
98
 
@@ -101,14 +123,17 @@ os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
101
  DEBUG = os.environ.get("DEBUG", "0") == "1"
102
  PAYER_CHOICES = ["Medicare", "BCBS", "Aetna", "Cigna", "UnitedHealthcare", "Other"]
103
 
104
- # ==== Live Logging Config =====================================================
 
 
 
 
 
105
  LOG_PUSH_ENABLE = os.environ.get("LOG_PUSH_ENABLE", "0") == "1"
106
- HF_TARGET_REPO = os.environ.get("HF_TARGET_REPO")
107
- HF_REPO_TYPE = os.environ.get("HF_REPO_TYPE", "dataset")
108
  HF_WRITE_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
109
 
110
- print(f"[LOG CFG] enable={LOG_PUSH_ENABLE} repo={HF_TARGET_REPO} type={HF_REPO_TYPE} token_len={(len(HF_WRITE_TOKEN) if HF_WRITE_TOKEN else 0)}")
111
-
112
  _hf_api = None
113
  def _get_hf_api():
114
  global _hf_api
@@ -117,241 +142,569 @@ def _get_hf_api():
117
  _hf_api = HfApi(token=HF_WRITE_TOKEN)
118
  return _hf_api
119
 
120
- def _ensure_logs_repo() -> bool:
121
- if not LOG_PUSH_ENABLE or not HF_TARGET_REPO or not HF_WRITE_TOKEN:
122
- return False
123
- try:
124
- api = _get_hf_api()
125
- api.create_repo(HF_TARGET_REPO, repo_type=HF_REPO_TYPE, private=True, exist_ok=True)
126
- return True
127
- except Exception as e:
128
- print("[LOG PUSH] repo check failed:", e)
129
- return False
130
-
131
- def _fetch_existing_day_blob(path_in_repo: str) -> bytes:
132
- try:
133
- from huggingface_hub import hf_hub_download
134
- fp = hf_hub_download(
135
- repo_id=HF_TARGET_REPO,
136
- filename=path_in_repo,
137
- repo_type=HF_REPO_TYPE,
138
- token=HF_WRITE_TOKEN,
139
- local_dir="/tmp",
140
- local_dir_use_symlinks=False,
141
- force_download=True,
142
- )
143
- with open(fp, "rb") as f:
144
- return f.read()
145
- except Exception:
146
- return b""
147
-
148
- def _commit_bytes(path_in_repo: str, data: bytes, msg: str) -> None:
149
- from huggingface_hub import CommitOperationAdd
150
- api = _get_hf_api()
151
- api.create_commit(
152
- repo_id=HF_TARGET_REPO,
153
- repo_type=HF_REPO_TYPE,
154
- operations=[CommitOperationAdd(path_in_repo=path_in_repo, path_or_fileobj=io.BytesIO(data))],
155
- commit_message=msg,
156
  )
 
 
 
 
 
 
 
 
 
157
 
 
 
 
 
158
  def _push_log_line_to_repo(entry: Dict[str, Any]) -> None:
159
- if not LOG_PUSH_ENABLE or not HF_TARGET_REPO or not HF_WRITE_TOKEN:
160
  return
161
- if not _ensure_logs_repo():
 
162
  return
163
- day = datetime.utcnow().strftime("%Y-%m-%d")
164
- path_in_repo = f"logs-live/{day}.jsonl"
165
- line = (json.dumps(entry, ensure_ascii=False) + "\n").encode("utf-8")
166
- for attempt in range(2):
 
 
 
 
 
167
  try:
168
- existing = _fetch_existing_day_blob(path_in_repo)
169
- if existing and not existing.endswith(b"\n"):
170
- existing += b"\n"
171
- data = existing + line
172
- _commit_bytes(path_in_repo, data, f"append {day} ({entry.get('event','')})")
173
- return
174
- except Exception as e:
175
- print(f"[LOG PUSH] attempt {attempt+1} failed:", e)
176
-
177
- # ==== Local Logging ===========================================================
178
- LOG_DIR = "logs"
179
- pathlib.Path(LOG_DIR).mkdir(exist_ok=True)
180
-
181
- def _log_path(sid): return os.path.join(LOG_DIR, f"{sid}.jsonl")
182
- def _utcnow_iso(): return datetime.now(timezone.utc).isoformat(timespec="seconds")
183
-
184
- def _append_log(sid: str, entry: Dict[str, Any]):
185
- entry = {"ts": _utcnow_iso(), "session_id": sid, **entry}
186
- with open(_log_path(sid), "a", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  f.write(json.dumps(entry, ensure_ascii=False) + "\n")
 
188
  _push_log_line_to_repo(entry)
189
 
 
 
 
 
 
 
 
 
 
 
 
190
  # ==== UI Helpers ==============================================================
191
- def _core_path():
192
- try: return __inspect.getsourcefile(suggest_with_cpt_billing) or "unknown"
193
- except Exception: return "unknown"
194
 
195
- SUGG_COLS = ["CPT","Description","Rationale","Confidence","Primary","Category","Laterality","Units"]
 
 
 
 
 
 
 
 
 
 
196
  EMPTY_SUGG_DF = pd.DataFrame(columns=SUGG_COLS)
197
- EMPTY_MODS_DF = pd.DataFrame([{"modifier":"—","reason":""}])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
- def _coalesce_rows(res: Dict[str,Any]):
200
- if not isinstance(res, dict): return EMPTY_SUGG_DF, {}
201
- rows = []
202
- for s in res.get("suggestions", []) or []:
203
  rows.append({
204
- "CPT": s.get("cpt",""),
205
- "Description": s.get("desc",""),
206
- "Rationale": s.get("rationale",""),
207
- "Confidence": round(float(s.get("confidence",0)),2) if s.get("confidence") else "",
208
  "Primary": "✓" if s.get("primary") else "",
209
- "Category": s.get("category",""),
210
- "Laterality": s.get("laterality",""),
211
- "Units": s.get("units",1)
212
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  meta = {
214
- "payer": res.get("payer",""),
215
- "region": res.get("region",""),
216
- "laterality": res.get("laterality",""),
217
- "build": res.get("build",""),
218
- "mode": res.get("mode",""),
219
- "core_path": _core_path()
 
 
 
 
 
 
220
  }
221
- df = pd.DataFrame(rows) if rows else EMPTY_SUGG_DF
222
- return df, meta
223
 
224
- def _case_mods_df(res):
225
- mods = res.get("case_modifiers",[]) or []
226
- if not mods: return EMPTY_MODS_DF.copy()
227
- return pd.DataFrame([{"modifier":f"-{m.get('modifier','')}", "reason":m.get("reason","")} for m in mods])
 
 
 
228
 
229
- def _summary_md(meta):
230
- chips=[]
231
- for k,v in meta.items():
232
- if v: chips.append(f"`{k}: {v}`")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  return " ".join(chips) if chips else "—"
234
 
235
- def new_session(): return str(uuid.uuid4())[:8]
 
 
 
236
 
237
- # ==== Core Actions ============================================================
238
- def run_inference(note,payer,top_k,sid):
239
- if not note.strip(): return EMPTY_SUGG_DF,EMPTY_MODS_DF.copy(),"—","", "",sid
240
- _append_log(sid,{"event":"request","payer":payer,"note":note})
 
 
 
 
 
241
  try:
242
- res = suggest_with_cpt_billing(note=note,payer=payer,top_k=top_k)
 
 
 
 
243
  except Exception as e:
244
- tb=traceback.format_exc()
245
- _append_log(sid,{"event":"error","error":repr(e),"trace":tb})
246
- return EMPTY_SUGG_DF,EMPTY_MODS_DF.copy(),"—","",f"⚠️ {e}",sid
247
- df,meta=_coalesce_rows(res)
248
- mods=_case_mods_df(res)
249
- summary=_summary_md(meta)
250
- jsonp=json.dumps(res,indent=2,ensure_ascii=False)
251
- _append_log(sid,{"event":"response","meta":meta,"rows":len(df)})
252
- return df,mods,summary,jsonp,"",sid
253
-
254
- def record_feedback(sid,vote,text):
255
- if not vote and not text: return "Please choose 👍/👎 or add a short note."
256
- _append_log(sid,{"event":"feedback","vote":vote,"text":text})
257
- return "Thanks! Feedback saved."
258
-
259
- def do_export(sid):
260
- p=_log_path(sid)
261
- out=os.path.join(LOG_DIR,f"export_{sid}.json")
262
- if os.path.exists(p):
263
- with open(p) as f: data=[json.loads(l) for l in f if l.strip()]
264
- json.dump(data,open(out,"w"),indent=2,ensure_ascii=False)
265
- _append_log(sid,{"event":"export","path":out})
266
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  def on_clear():
269
- return "","Medicare",10,EMPTY_SUGG_DF,EMPTY_MODS_DF.copy(),"—","","",new_session()
 
 
 
 
 
 
270
 
271
- # ==== Diagnostics =============================================================
272
- def run_probe(sid):
273
- info=[]
 
 
274
  try:
275
- t=[("Implicit TLIF","Left facetectomy L4–L5 with PEEK interbody cage and pedicle screws; rods secured.",["22633"]),
276
- ("ACDF chain w/ plate","Discectomies at C4–C5, C5–C6, and C6–C7. Interbody cages and anterior plate spanning C4–C7.",["22551","22552","22846"]),
277
- ("Exposure-only","Anterior exposure of L4–S1 performed by vascular surgeon for access. No fusion performed.",["00000"])]
278
- for lbl,note,exp in t:
279
- r=suggest_with_cpt_billing(note,payer="Medicare",top_k=10)
280
- codes=[s.get("cpt") for s in r.get("suggestions",[])]
281
- ok=all(ec in codes for ec in exp)
282
- info.append(f"- **{lbl}** `{', '.join(codes) or '∅'}` {'✅PASS' if ok else '❌CHECK'}")
283
- md="\n".join(info)
284
- _append_log(sid,{"event":"probe","details":md})
285
- return md,""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  except Exception as e:
287
- tb=traceback.format_exc()
288
- _append_log(sid,{"event":"probe_error","error":repr(e),"trace":tb})
289
- return "",f"⚠️ Probe failed: {e}"
290
 
291
- def run_log_selftest(sid):
 
292
  try:
293
- if not LOG_PUSH_ENABLE: return "","⚠️ LOG_PUSH_ENABLE=0"
294
- if not HF_TARGET_REPO: return "","⚠️ HF_TARGET_REPO missing"
295
- if not HF_WRITE_TOKEN: return "","⚠️ HF_TOKEN missing"
296
- entry={"ts":_utcnow_iso(),"session_id":sid,"event":"selftest","msg":"hello"}
 
 
297
  _push_log_line_to_repo(entry)
298
- day=datetime.utcnow().strftime("%Y-%m-%d")
299
- msg=f"✅ Live log push working!\n→ `{HF_TARGET_REPO}/logs-live/{day}.jsonl`"
300
- _append_log(sid,{"event":"selftest_ok"})
301
- return msg,""
 
 
 
 
 
 
302
  except Exception as e:
303
- tb=traceback.format_exc()
304
- _append_log(sid,{"event":"selftest_err","err":repr(e),"trace":tb})
305
- return "",f"⚠️ Self-test failed: {e}"
306
 
307
  # ==== Examples ================================================================
308
- EXAMPLES=[
309
- ["Left-sided TLIF L4–L5 with pedicle screws and interbody cage; posterolateral fusion performed.","Medicare",10],
310
- ["ACDF C5C6 with PEEK cage and anterior plate.","Medicare",10],
311
- ["Posterior cervical foraminotomy right C6–C7; no fusion.","Medicare",10]
 
 
 
 
 
 
 
 
312
  ]
313
 
314
  # ==== Theme / CSS =============================================================
315
- THEME=gr.themes.Soft(primary_hue="indigo").set(background_fill_primary="#fff",button_primary_background_fill="#4f46e5")
316
- CUSTOM_CSS="""
317
- :root{--radius-lg:16px;}
318
- .gradio-container{font-family:ui-sans-serif;}
319
- .header-card{border-radius:18px;padding:18px;border:1px solid #1f2937;background:linear-gradient(180deg,#0f172a,#0b1220);color:#e5e7eb;}
320
- .badge-row code{margin-right:8px;border-radius:12px;padding:2px 8px;background:#111827;color:#e5e7eb;}
321
- .table-wrap{max-height:520px;overflow:auto;}
322
- .footer-note{color:#94a3b8;font-size:12px;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  """
324
 
325
- # ==== UI Layout ===============================================================
326
- with gr.Blocks(theme=THEME,css=CUSTOM_CSS,title="Spine Coder — CPT Billing") as demo:
327
- sid=gr.State(new_session())
 
328
  with gr.Row():
329
  with gr.Column():
330
  gr.Markdown("### 🦴 Spine Coder — CPT Billing & Operative Note NLP")
331
- gr.Markdown('<div class="header-card">Structured CPT suggestions from spine operative notes — payer-aware modifiers & rationales.<br/><span class="footer-note">No PHI stored.</span></div>')
 
 
 
 
 
332
  with gr.Row():
 
333
  with gr.Column(scale=5):
334
- note=gr.Textbox(label="Operative Note",lines=14,placeholder="Paste an operative note…")
335
- payer=gr.Dropdown(PAYER_CHOICES,value="Medicare",label="Payer")
336
- topk=gr.Slider(1,15,value=10,step=1,label="Top-K")
337
- run=gr.Button("Analyze Note",variant="primary"); clear=gr.Button("Clear")
338
- gr.Examples(EXAMPLES,inputs=[note,payer,topk],label="Quick Examples")
339
- fb_choice=gr.Radio(["👍","👎"],label="Helpful?")
340
- fb_text=gr.Textbox(label="Optional comment",lines=2)
341
- fb_btn=gr.Button("Submit Feedback"); fb_status=gr.Markdown("")
342
- sid_show=gr.Textbox(label="Session ID",interactive=False)
343
- export=gr.Button("Export JSON"); export_file=gr.File(label="Download",interactive=False)
344
- probe_btn=gr.Button("Run Probe"); probe_md=gr.Markdown("")
345
- test_btn=gr.Button("Test Live Log Push"); test_md=gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  with gr.Column(scale=7):
347
  gr.Markdown("#### Results")
348
- summary=gr.Markdown("—",elem_classes=["badge-row"])
349
- table=gr.Dataframe(value=EMPTY_SUGG_DF,label="CPT Suggestions",interactive=False,elem_classes=["table-wrap"])
350
- gr.Markdown("### Case Modifiers")
351
- mods=gr.Dataframe(value=EMPTY_MODS_DF.copy(),interactive=False)
352
- json_out=gr.Code(language="json",value="",interactive=False)
353
- warn=gr.Markdown("")
354
- demo.load(lambda: (new_session(),new_session()),outputs=[sid,sid_show])
355
- run.click(run_inference,inputs=[note,payer,topk,sid],outputs=[table,mods,summary,json_out,warn,sid])
356
- note.submit(run_inference,inputs=[note,payer,topk,sid],outputs=[table,mods,summary,json_out,warn,sid])
357
- clear.click(on_clear,outputs=[note,payer,top
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # app.py — Spine Coder (Chatbot + Feedback + Session Logs) — Gradio 4.x
2
  # ------------------------------------------------------------------------------
3
+ # FINAL-v2.2 UI build (hardened) + LIVE DATASET LOGGING (no restarts):
4
+ # - Purge caches, dynamic file loader (SPINE_CORE_PATH override) + file SHA print
5
+ # - Startup PROBE to logs + in-UI Diagnostics/Probe + Live Log Push Self-Test
6
+ # - Clean CPT table (no per-row modifier columns) + Case Modifiers panel
7
+ # - Build/Core chips, structured logs/export, modern Gradio usage
8
+ # - Per-request log commits to a separate DATASET repo (no Space rebuilds)
9
  # ------------------------------------------------------------------------------
10
 
11
  import os
 
16
  import traceback
17
  from datetime import datetime, timezone
18
  from typing import Any, Dict, List, Tuple
19
+
20
  import pandas as pd
21
  import gradio as gr
22
 
23
+ # ==== Bulletproof Core Import ==================================================
24
  import importlib, importlib.util, importlib.machinery
25
  import sys as __sys, os as __os, inspect as __inspect, hashlib
26
 
27
+ __sys.path.insert(0, __os.path.abspath(".")) # ensure repo root is on sys.path
28
 
29
  def _purge_spine_modules():
30
+ """Remove cached spine_coder modules so we truly reload the file we want."""
31
  for k in list(__sys.modules):
32
  if k == "spine_coder" or k.startswith("spine_coder."):
33
  __sys.modules.pop(k, None)
 
35
 
36
  def _sha256(path: str) -> str:
37
  try:
 
38
  h = hashlib.sha256()
39
  with open(path, "rb") as f:
40
  for chunk in iter(lambda: f.read(8192), b""):
 
44
  return "unknown"
45
 
46
  def _load_core_from_file(path: str):
47
+ """
48
+ Load spine_coder_core.py directly from a given path (bypass module cache).
49
+ IMPORTANT: insert into sys.modules BEFORE exec_module; do NOT reload.
50
+ """
51
  path = __os.path.abspath(path)
52
  if not __os.path.exists(path):
53
  return None
 
55
  loader = importlib.machinery.SourceFileLoader(name, path)
56
  spec = importlib.util.spec_from_loader(name, loader)
57
  mod = importlib.util.module_from_spec(spec)
58
+ __sys.modules[name] = mod # register first
59
+ loader.exec_module(mod) # then exec
60
  return mod
61
 
62
  def _force_import_core():
63
+ """Order: purge caches → SPINE_CORE_PATH → local files → package modules."""
64
  _purge_spine_modules()
65
+
66
+ # 1) Explicit path override (Space secret/variable)
67
  forced = __os.environ.get("SPINE_CORE_PATH")
68
  if forced and __os.path.exists(forced):
69
  mod = _load_core_from_file(forced)
70
  if mod:
71
  print("[CORE] forced path:", forced, "sha:", _sha256(forced))
72
  return mod
73
+
74
+ # 2) Likely local paths (edited copies near app)
75
  for rel in [
76
  "spine_coder_core.py",
77
  "spine_coder/spine_coder_core.py",
78
+ "spine_coder/spine_coder/spine_coder_core.py", # package-style tree
79
  ]:
80
  p = __os.path.abspath(rel)
81
  if __os.path.exists(p):
 
83
  if mod:
84
  print("[CORE] loaded file:", p, "sha:", _sha256(p))
85
  return mod
86
+
87
+ # 3) Package modules (may be stale)
88
  for modname in [
89
  "spine_coder.spine_coder.spine_coder_core",
90
  "spine_coder.spine_coder_core",
91
  ]:
92
  try:
93
  mod = importlib.import_module(modname)
94
+ mod = importlib.reload(mod) # ok for package import
95
+ src = __inspect.getsourcefile(mod.suggest_with_cpt_billing) or "unknown"
96
+ print("[CORE] loaded module:", modname, "from:", src)
97
  return mod
98
  except Exception:
99
  pass
100
+
101
+ raise ImportError("Unable to locate spine_coder_core.py via modules or file paths.")
102
 
103
  _core = _force_import_core()
104
  suggest_with_cpt_billing = _core.suggest_with_cpt_billing
105
 
106
+ try:
107
+ _active_src = __inspect.getsourcefile(suggest_with_cpt_billing) or "unknown"
108
+ print("[CORE] active source:", _active_src)
109
+ except Exception:
110
+ _active_src = "unknown"
111
+
112
+ # ---- One-time startup probe (prints to Space logs) ----------------------------
113
  try:
114
  _probe_note = "Discectomies at C4–C5, C5–C6, and C6–C7. Interbody cages and anterior plate spanning C4–C7."
115
  _probe = suggest_with_cpt_billing(_probe_note, payer="Medicare", top_k=5)
116
  print("[PROBE] build:", _probe.get("build"))
117
+ print("[PROBE] first_suggestion:", (_probe.get("suggestions") or [{}])[0])
118
  except Exception as e:
119
  print("[PROBE] failed:", e)
120
 
 
123
  DEBUG = os.environ.get("DEBUG", "0") == "1"
124
  PAYER_CHOICES = ["Medicare", "BCBS", "Aetna", "Cigna", "UnitedHealthcare", "Other"]
125
 
126
+ # ==== Remote log push settings (dataset repo; avoids Space rebuilds) ==========
127
+ # Required env (set in Space Settings → Variables & secrets):
128
+ # LOG_PUSH_ENABLE=1
129
+ # HF_TARGET_REPO=Slaiwala/spinecoder-logs
130
+ # HF_REPO_TYPE=dataset
131
+ # HF_TOKEN (or HUGGINGFACEHUB_API_TOKEN) with WRITE access to that repo
132
  LOG_PUSH_ENABLE = os.environ.get("LOG_PUSH_ENABLE", "0") == "1"
133
+ HF_TARGET_REPO = os.environ.get("HF_TARGET_REPO") # e.g., "Slaiwala/spinecoder-logs"
134
+ HF_REPO_TYPE = os.environ.get("HF_REPO_TYPE", "dataset") # keep 'dataset' to prevent Space rebuilds
135
  HF_WRITE_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
136
 
 
 
137
  _hf_api = None
138
  def _get_hf_api():
139
  global _hf_api
 
142
  _hf_api = HfApi(token=HF_WRITE_TOKEN)
143
  return _hf_api
144
 
145
+ # Log config banner (for quick sanity in Space logs)
146
+ try:
147
+ print(
148
+ "[LOG CFG]",
149
+ "enable=" + str(LOG_PUSH_ENABLE),
150
+ "repo=" + str(HF_TARGET_REPO),
151
+ "type=" + str(HF_REPO_TYPE),
152
+ "token=" + ("set" if HF_WRITE_TOKEN else "missing"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  )
154
+ except Exception:
155
+ pass
156
+
157
+ # ==== Local logging ============================================================
158
+ LOG_DIR = os.environ.get("LOG_DIR", "logs")
159
+ pathlib.Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
160
+
161
+ def _log_path(session_id: str) -> str:
162
+ return os.path.join(LOG_DIR, f"{session_id}.jsonl")
163
 
164
+ def _utcnow_iso() -> str:
165
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
166
+
167
+ # ---- push a single line to dataset repo (no Space restart) -------------------
168
  def _push_log_line_to_repo(entry: Dict[str, Any]) -> None:
169
+ if not LOG_PUSH_ENABLE:
170
  return
171
+ if not HF_TARGET_REPO or not HF_WRITE_TOKEN:
172
+ # Misconfigured: silently skip rather than touching the Space repo
173
  return
174
+ try:
175
+ from huggingface_hub import CommitOperationAdd, hf_hub_download
176
+ api = _get_hf_api()
177
+
178
+ day = datetime.utcnow().strftime("%Y-%m-%d")
179
+ path_in_repo = f"logs-live/{day}.jsonl"
180
+
181
+ # Try to download existing file (may not exist yet)
182
+ existing = ""
183
  try:
184
+ local_fp = hf_hub_download(
185
+ repo_id=HF_TARGET_REPO,
186
+ filename=path_in_repo,
187
+ repo_type=HF_REPO_TYPE, # 'dataset'
188
+ token=HF_WRITE_TOKEN,
189
+ local_dir="/tmp",
190
+ local_dir_use_symlinks=False,
191
+ force_download=False,
192
+ )
193
+ with open(local_fp, "r", encoding="utf-8") as f:
194
+ existing = f.read()
195
+ except Exception:
196
+ existing = ""
197
+
198
+ # Append one line
199
+ new_line = json.dumps(entry, ensure_ascii=False)
200
+ if existing and not existing.endswith("\n"):
201
+ existing += "\n"
202
+ merged = (existing + new_line + "\n").encode("utf-8")
203
+
204
+ api.create_commit(
205
+ repo_id=HF_TARGET_REPO,
206
+ repo_type=HF_REPO_TYPE, # 'dataset'
207
+ operations=[CommitOperationAdd(path_in_repo=path_in_repo, path_or_fileobj=io.BytesIO(merged))],
208
+ commit_message=f"logs: append {day} ({entry.get('event','')})",
209
+ )
210
+ except Exception as e:
211
+ print("[LOG PUSH] failed:", e)
212
+
213
+ def _append_log(session_id: str, entry: Dict[str, Any]) -> None:
214
+ entry = {"ts": _utcnow_iso(), "session_id": session_id, **entry}
215
+ # Local JSONL (unchanged)
216
+ with open(_log_path(session_id), "a", encoding="utf-8") as f:
217
  f.write(json.dumps(entry, ensure_ascii=False) + "\n")
218
+ # Dataset push (no restart)
219
  _push_log_line_to_repo(entry)
220
 
221
+ def export_session(session_id: str) -> str:
222
+ src = _log_path(session_id)
223
+ data: List[Dict[str, Any]] = []
224
+ if os.path.exists(src):
225
+ with open(src, "r", encoding="utf-8") as f:
226
+ data = [json.loads(l) for l in f if l.strip()]
227
+ out_path = os.path.join(LOG_DIR, f"export_{session_id}.json")
228
+ with open(out_path, "w", encoding="utf-8") as f:
229
+ json.dump(data, f, indent=2, ensure_ascii=False)
230
+ return out_path
231
+
232
  # ==== UI Helpers ==============================================================
 
 
 
233
 
234
+ def _core_path() -> str:
235
+ try:
236
+ return __inspect.getsourcefile(suggest_with_cpt_billing) or "unknown"
237
+ except Exception:
238
+ return "unknown"
239
+
240
+ # Per-row modifiers intentionally removed from CPT table.
241
+ SUGG_COLS = [
242
+ "CPT", "Description", "Rationale",
243
+ "Confidence", "Primary", "Category", "Laterality", "Units"
244
+ ]
245
  EMPTY_SUGG_DF = pd.DataFrame(columns=SUGG_COLS)
246
+ EMPTY_MODS_DF = pd.DataFrame([{"modifier": "—", "reason": ""}])
247
+
248
+ def _coalesce_rows(result: Dict[str, Any]) -> Tuple[pd.DataFrame, Dict[str, Any]]:
249
+ """Build the suggestions table (no modifier columns) and meta badges."""
250
+ if not isinstance(result, dict):
251
+ return EMPTY_SUGG_DF, {}
252
+
253
+ sugg = result.get("suggestions") or []
254
+ rows: List[Dict[str, Any]] = []
255
+ case_lat = (result.get("laterality") or "").strip().lower()
256
+
257
+ for s in sugg:
258
+ if not isinstance(s, dict):
259
+ continue
260
+ mods = s.get("modifiers", []) or []
261
+
262
+ # Derive row laterality from LT/RT or case laterality
263
+ row_lat = (s.get("laterality") or "").strip().lower()
264
+ if not row_lat:
265
+ if isinstance(mods, list) and "LT" in mods:
266
+ row_lat = "left"
267
+ elif isinstance(mods, list) and "RT" in mods:
268
+ row_lat = "right"
269
+ elif case_lat in ("left", "right", "bilateral"):
270
+ row_lat = case_lat
271
+
272
+ conf_val = s.get("confidence")
273
+ conf_out = round(float(conf_val), 2) if isinstance(conf_val, (int, float)) else (conf_val or "")
274
 
 
 
 
 
275
  rows.append({
276
+ "CPT": s.get("cpt", ""),
277
+ "Description": s.get("desc", ""),
278
+ "Rationale": s.get("rationale", ""),
279
+ "Confidence": conf_out,
280
  "Primary": "✓" if s.get("primary") else "",
281
+ "Category": s.get("category", ""),
282
+ "Laterality": row_lat,
283
+ "Units": s.get("units", 1),
284
  })
285
+
286
+ # Normalize levels
287
+ segs: List[str] = []
288
+ inters_list: List[str] = []
289
+ lvl_lat = ""
290
+ levels_obj = result.get("levels")
291
+ if isinstance(levels_obj, dict):
292
+ segs = list(levels_obj.get("segments") or [])
293
+ inters_list = list(levels_obj.get("interspaces") or [])
294
+ lvl_lat = levels_obj.get("laterality", "") or ""
295
+ elif isinstance(levels_obj, list):
296
+ segs = [str(x) for x in levels_obj]
297
+
298
+ # Normalize flags
299
+ flags_obj = result.get("flags")
300
+ if isinstance(flags_obj, list):
301
+ flags_list = [str(x) for x in flags_obj]
302
+ elif isinstance(flags_obj, dict):
303
+ flags_list = [k for k, v in flags_obj.items() if v]
304
+ else:
305
+ flags_list = []
306
+
307
  meta = {
308
+ "payer": result.get("payer", ""),
309
+ "region": result.get("region", ""),
310
+ "laterality": result.get("laterality", "") or lvl_lat,
311
+ "levels_segments": ", ".join(segs),
312
+ "levels_interspaces": (
313
+ str(result.get("interspaces_est", "")) if "interspaces_est" in result
314
+ else ", ".join(inters_list)
315
+ ),
316
+ "flags": ", ".join(sorted(flags_list)),
317
+ "build": result.get("build", ""),
318
+ "mode": result.get("mode", ""),
319
+ "core_path": _core_path(),
320
  }
 
 
321
 
322
+ df = EMPTY_SUGG_DF if not rows else pd.DataFrame(rows)
323
+ if not df.empty:
324
+ for col in SUGG_COLS:
325
+ if col not in df.columns:
326
+ df[col] = ""
327
+ df = df[SUGG_COLS]
328
+ return df, meta
329
 
330
+ def _case_mods_df(result: Dict[str, Any]) -> pd.DataFrame:
331
+ mods = result.get("case_modifiers", []) or []
332
+ if not mods:
333
+ return EMPTY_MODS_DF.copy()
334
+ return pd.DataFrame([{"modifier": f"-{m.get('modifier','')}", "reason": m.get("reason","")} for m in mods])
335
+
336
+ def _summary_md(meta: Dict[str, Any]) -> str:
337
+ chips = []
338
+ if meta.get("region"): chips.append(f"`Region: {meta['region']}`")
339
+ if meta.get("laterality"): chips.append(f"`Laterality: {meta['laterality']}`")
340
+ if meta.get("levels_segments"): chips.append(f"`Segments: {meta['levels_segments']}`")
341
+ if meta.get("levels_interspaces"): chips.append(f"`Interspaces: {meta['levels_interspaces']}`")
342
+ if meta.get("flags"): chips.append(f"`Flags: {meta['flags']}`")
343
+ if meta.get("build"): chips.append(f"`Build: {meta['build']}`")
344
+ if meta.get("mode"): chips.append(f"`Mode: {meta['mode']}`")
345
+ try:
346
+ core_base = os.path.basename(meta.get("core_path","")) if meta.get("core_path") else ""
347
+ if core_base:
348
+ chips.append(f"`Core: {core_base}`")
349
+ except Exception:
350
+ pass
351
  return " ".join(chips) if chips else "—"
352
 
353
+ def new_session() -> str:
354
+ return str(uuid.uuid4())[:8]
355
+
356
+ # ==== Core actions ============================================================
357
 
358
+ def run_inference(note: str, payer: str, top_k: int, session_id: str):
359
+ if not note.strip():
360
+ return (
361
+ EMPTY_SUGG_DF,
362
+ EMPTY_MODS_DF.copy(),
363
+ "—", "", "", session_id
364
+ )
365
+
366
+ _append_log(session_id, {"event": "request", "payer": payer, "top_k": top_k, "note": note})
367
  try:
368
+ result = suggest_with_cpt_billing(note=note, payer=payer, top_k=top_k)
369
+ if DEBUG:
370
+ print("[DEBUG] build/region/laterality/flags:",
371
+ result.get("build"), result.get("region"),
372
+ result.get("laterality"), result.get("flags"))
373
  except Exception as e:
374
+ tb = traceback.format_exc()
375
+ _append_log(session_id, {"event": "error", "error": repr(e), "traceback": tb})
376
+ warn = f"⚠️ Error: {e}"
377
+ if DEBUG:
378
+ warn += f"\n\n```traceback\n{tb}\n```"
379
+ return (
380
+ EMPTY_SUGG_DF,
381
+ EMPTY_MODS_DF.copy(),
382
+ "—", "", warn, session_id
383
+ )
384
+
385
+ sugg_df, meta = _coalesce_rows(result)
386
+ case_mods_df = _case_mods_df(result)
387
+ summary = _summary_md(meta)
388
+ json_pretty = json.dumps(result, indent=2, ensure_ascii=False)
389
+
390
+ _append_log(session_id, {
391
+ "event": "response",
392
+ "meta": {
393
+ **meta,
394
+ "case_modifiers": ", ".join([f"-{m}" for m in [cm.get("modifier","") for cm in (result.get("case_modifiers") or [])] if m]) or ""
395
+ },
396
+ "rows_len": int(len(sugg_df) if hasattr(sugg_df, "__len__") else 0)
397
+ })
398
+ return sugg_df, case_mods_df, summary, json_pretty, "", session_id
399
+
400
+ def record_feedback(session_id: str, vote: str, text: str):
401
+ if not vote and not text:
402
+ return "Please choose 👍/👎 or add a short note."
403
+ _append_log(session_id, {"event": "feedback", "vote": vote, "text": text})
404
+ return "Thanks! Your feedback was recorded."
405
+
406
+ def do_export(session_id: str):
407
+ path = export_session(session_id)
408
+ _append_log(session_id, {"event": "export", "path": path})
409
+ return path
410
 
411
  def on_clear():
412
+ return (
413
+ "", "Medicare", 10,
414
+ EMPTY_SUGG_DF,
415
+ EMPTY_MODS_DF.copy(),
416
+ "—", "", "",
417
+ new_session()
418
+ )
419
 
420
+ # ==== Diagnostics / Probe =====================================================
421
+
422
+ def run_probe(session_id: str) -> Tuple[str, str]:
423
+ """Run a live diagnostic: show core path & build and 3 smoke tests."""
424
+ info_lines = []
425
  try:
426
+ core_path = _core_path()
427
+ tests = [
428
+ ("Implicit TLIF",
429
+ "Left facetectomy L4–L5 with PEEK interbody cage and pedicle screws; rods secured.",
430
+ ["22633"], # expect
431
+ ),
432
+ ("ACDF chain w/ plate",
433
+ "Discectomies at C4–C5, C5–C6, and C6–C7. Interbody cages and anterior plate spanning C4–C7.",
434
+ ["22551","22552","22846"],
435
+ ),
436
+ ("Exposure-only",
437
+ "Anterior exposure of L4–S1 performed by vascular surgeon for access. No fusion performed.",
438
+ ["00000"],
439
+ ),
440
+ ]
441
+ # Run once to get build
442
+ probe = suggest_with_cpt_billing(tests[1][1], payer="Medicare", top_k=10)
443
+ build = probe.get("build","")
444
+ info_lines.append(f"**Core path:** `{core_path}` \n**Build:** `{build}`")
445
+
446
+ # Execute each test
447
+ for label, note, expect_codes in tests:
448
+ res = suggest_with_cpt_billing(note, payer="Medicare", top_k=10)
449
+ codes = [s.get("cpt") for s in (res.get("suggestions") or [])]
450
+ ok = all(any(ec == c for c in codes) for ec in expect_codes)
451
+ info_lines.append(f"- **{label}** → codes: `{', '.join(codes) or '∅'}` — **{'PASS' if ok else 'CHECK'}**")
452
+ md = "\n".join(info_lines)
453
+ _append_log(session_id, {"event":"probe","details": md})
454
+ return md, ""
455
  except Exception as e:
456
+ tb = traceback.format_exc()
457
+ _append_log(session_id, {"event":"probe_error","error":repr(e),"traceback":tb})
458
+ return "", f"⚠️ Probe failed: {e}"
459
 
460
+ def run_live_log_selftest(session_id: str) -> Tuple[str, str]:
461
+ """Attempt a small append to dataset repo to verify live logging config."""
462
  try:
463
+ entry = {
464
+ "event": "selftest",
465
+ "note": "hello-from-selftest",
466
+ "ts": _utcnow_iso(),
467
+ "session_id": session_id,
468
+ }
469
  _push_log_line_to_repo(entry)
470
+ msg = (
471
+ f"✅ Live log push attempted.\n"
472
+ f"- enable={LOG_PUSH_ENABLE}\n"
473
+ f"- repo={HF_TARGET_REPO}\n"
474
+ f"- type={HF_REPO_TYPE}\n"
475
+ f"- token={'set' if HF_WRITE_TOKEN else 'missing'}\n"
476
+ f"- path=logs-live/{datetime.utcnow().strftime('%Y-%m-%d')}.jsonl"
477
+ )
478
+ _append_log(session_id, {"event":"selftest", "details":"manual push executed"})
479
+ return msg, ""
480
  except Exception as e:
481
+ tb = traceback.format_exc()
482
+ _append_log(session_id, {"event":"selftest_error","error":repr(e),"traceback":tb})
483
+ return "", f"⚠️ Self-test failed: {e}"
484
 
485
  # ==== Examples ================================================================
486
+
487
+ EXAMPLES = [
488
+ ["Left-sided TLIF L4L5 with pedicle screws and interbody cage; posterolateral fusion performed. Navigation used.", "Medicare", 10],
489
+ ["ACDF C5–C6 with PEEK cage and anterior plate; microscope and neuromonitoring used.", "Medicare", 10],
490
+ ["Posterior cervical foraminotomy right C6–C7; no fusion or instrumentation.", "Medicare", 10],
491
+ ["ALIF L5–S1 with structural allograft; non-segmental instrumentation placed.", "Medicare", 10],
492
+ ["Removal of posterior segmental instrumentation T10–L2; no new hardware placed.", "Medicare", 10],
493
+ # Case-modifier smoke tests:
494
+ ["TLIF L4–L5 was initiated but aborted midway due to neuromonitoring changes.", "Medicare", 10], # -53
495
+ ["Bilateral decompression and foraminotomy at L4–L5 and L5–S1.", "Medicare", 10], # -50
496
+ ["Assistant surgeon present; resident not available.", "Medicare", 10], # -82
497
+ ["Complex exposure with severe deformity and adhesiolysis.", "Medicare", 10], # -22
498
  ]
499
 
500
  # ==== Theme / CSS =============================================================
501
+
502
+ THEME = gr.themes.Soft(
503
+ primary_hue="indigo",
504
+ secondary_hue="blue",
505
+ neutral_hue="slate",
506
+ ).set(
507
+ body_text_color="#0f172a",
508
+ background_fill_primary="#ffffff",
509
+ button_primary_background_fill="#4f46e5",
510
+ input_background_fill="#ffffff",
511
+ )
512
+
513
+ CUSTOM_CSS = """
514
+ :root { --radius-lg: 16px; }
515
+ .gradio-container { font-family: ui-sans-serif, system-ui, -apple-system; }
516
+
517
+ /* Header card */
518
+ .header-card {
519
+ border-radius: 18px;
520
+ padding: 18px;
521
+ border: 1px solid #1f2937;
522
+ background: linear-gradient(180deg,#0f172a,#0b1220);
523
+ color: #e5e7eb;
524
+ }
525
+ .badge-row code {
526
+ margin-right: 8px;
527
+ border-radius: 12px;
528
+ padding: 2px 8px;
529
+ background: #111827;
530
+ color: #e5e7eb;
531
+ }
532
+
533
+ /* Table container */
534
+ .table-wrap { max-height: 520px; overflow: auto; }
535
+
536
+ /* Suggestions table target */
537
+ #suggestions_table .dataframe {
538
+ font-size: 15px;
539
+ width: 100% !important;
540
+ table-layout: auto !important;
541
+ border-collapse: collapse;
542
+ }
543
+ #suggestions_table .dataframe th,
544
+ #suggestions_table .dataframe td {
545
+ white-space: normal;
546
+ word-wrap: break-word;
547
+ text-align: left;
548
+ vertical-align: top;
549
+ padding: 10px 12px;
550
+ }
551
+
552
+ /* Column sizing (1-indexed):
553
+ 1=CPT, 2=Description, 3=Rationale, 4=Confidence,
554
+ 5=Primary, 6=Category, 7=Laterality, 8=Units */
555
+
556
+ /* CPT — wider & no wrap, monospace, centered */
557
+ #suggestions_table .dataframe th:nth-child(1),
558
+ #suggestions_table .dataframe td:nth-child(1) {
559
+ min-width: 120px;
560
+ max-width: 140px;
561
+ white-space: nowrap !important;
562
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
563
+ text-align: center;
564
+ }
565
+
566
+ /* Description — roomy */
567
+ #suggestions_table .dataframe th:nth-child(2),
568
+ #suggestions_table .dataframe td:nth-child(2) {
569
+ min-width: 360px;
570
+ max-width: 560px;
571
+ }
572
+
573
+ /* Rationale — roomy */
574
+ #suggestions_table .dataframe th:nth-child(3),
575
+ #suggestions_table .dataframe td:nth-child(3) {
576
+ min-width: 320px;
577
+ max-width: 520px;
578
+ }
579
+
580
+ /* Category — a bit wider */
581
+ #suggestions_table .dataframe th:nth-child(6),
582
+ #suggestions_table .dataframe td:nth-child(6) {
583
+ min-width: 180px;
584
+ }
585
+
586
+ /* Keep tiny columns compact */
587
+ #suggestions_table .dataframe th:nth-child(4),
588
+ #suggestions_table .dataframe td:nth-child(4),
589
+ #suggestions_table .dataframe th:nth-child(5),
590
+ #suggestions_table .dataframe td:nth-child(5),
591
+ #suggestions_table .dataframe th:nth-child(7),
592
+ #suggestions_table .dataframe td:nth-child(7),
593
+ #suggestions_table .dataframe th:nth-child(8),
594
+ #suggestions_table .dataframe td:nth-child(8) {
595
+ min-width: 90px;
596
+ }
597
+
598
+ .footer-note { color:#94a3b8; font-size:12px; }
599
  """
600
 
601
+ # ==== App Layout ==============================================================
602
+ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Spine Coder — CPT Billing") as demo:
603
+ session_id = gr.State(new_session())
604
+
605
  with gr.Row():
606
  with gr.Column():
607
  gr.Markdown("### 🦴 Spine Coder — CPT Billing & Operative Note NLP")
608
+ gr.Markdown(
609
+ '<div class="header-card">Structured CPT suggestions from spine operative notes — with payer-aware '
610
+ 'modifiers, laterality detection, and rationales.<br/>'
611
+ '<span class="footer-note">No PHI is stored; inputs are session-scoped and ephemeral.</span></div>'
612
+ )
613
+
614
  with gr.Row():
615
+ # Inputs
616
  with gr.Column(scale=5):
617
+ note_in = gr.Textbox(
618
+ label="Operative Note",
619
+ placeholder="Paste an operative note here…",
620
+ lines=14,
621
+ autofocus=True,
622
+ )
623
+ with gr.Row():
624
+ payer_dd = gr.Dropdown(PAYER_CHOICES, value="Medicare", label="Payer")
625
+ topk = gr.Slider(1, 15, value=10, step=1, label="Top-K suggestions")
626
+ with gr.Row():
627
+ run_btn = gr.Button("Analyze Note", variant="primary")
628
+ clear_btn = gr.Button("Clear")
629
+
630
+ with gr.Accordion("Quick Examples", open=False):
631
+ gr.Examples(
632
+ examples=EXAMPLES,
633
+ inputs=[note_in, payer_dd, topk],
634
+ label="Click a row to load an example"
635
+ )
636
+
637
+ with gr.Accordion("Feedback", open=False):
638
+ fb_choice = gr.Radio(choices=["👍", "👎"], label="Was this helpful?")
639
+ fb_text = gr.Textbox(label="Optional comment", lines=2, placeholder="Tell us what worked or what missed…")
640
+ fb_submit = gr.Button("Submit Feedback")
641
+ fb_status = gr.Markdown("")
642
+
643
+ with gr.Accordion("Session", open=False):
644
+ sid_show = gr.Textbox(label="Session ID", value="", interactive=False)
645
+ export_btn = gr.Button("Export Session as JSON")
646
+ export_file = gr.File(label="Download", interactive=False)
647
+
648
+ with gr.Accordion("Diagnostics", open=False):
649
+ probe_btn = gr.Button("Run Probe (core path + 3 tests)")
650
+ probe_md = gr.Markdown("")
651
+ selftest_btn = gr.Button("Test Live Log Push")
652
+ selftest_md = gr.Markdown("")
653
+
654
+ # Results
655
  with gr.Column(scale=7):
656
  gr.Markdown("#### Results")
657
+ summary_md = gr.Markdown("—", elem_classes=["badge-row"])
658
+
659
+ # Suggestions table (no modifier columns)
660
+ table = gr.Dataframe(
661
+ value=EMPTY_SUGG_DF,
662
+ label="CPT Suggestions",
663
+ interactive=False,
664
+ row_count=(0, "dynamic"),
665
+ wrap=True,
666
+ elem_classes=["table-wrap"],
667
+ elem_id="suggestions_table",
668
+ )
669
+
670
+ # Case-level modifiers table
671
+ gr.Markdown("### Case Modifiers (visit-level)")
672
+ case_mods_table = gr.Dataframe(
673
+ value=EMPTY_MODS_DF.copy(),
674
+ headers=["modifier","reason"],
675
+ interactive=False,
676
+ wrap=True,
677
+ label="Case Modifiers",
678
+ )
679
+
680
+ with gr.Accordion("Raw JSON", open=False):
681
+ json_out = gr.Code(language="json", value="", interactive=False)
682
+ warn_md = gr.Markdown("")
683
+
684
+ # ---- Events / Wiring ----
685
+ def _on_load():
686
+ sid = new_session()
687
+ return sid, sid
688
+
689
+ demo.load(_on_load, outputs=[session_id, sid_show])
690
+
691
+ run_inputs = [note_in, payer_dd, topk, session_id]
692
+ run_outputs = [table, case_mods_table, summary_md, json_out, warn_md, session_id]
693
+
694
+ run_btn.click(run_inference, inputs=run_inputs, outputs=run_outputs)
695
+ note_in.submit(run_inference, inputs=run_inputs, outputs=run_outputs)
696
+
697
+ clear_btn.click(
698
+ on_clear,
699
+ outputs=[note_in, payer_dd, topk, table, case_mods_table, summary_md, json_out, warn_md, session_id]
700
+ )
701
+
702
+ fb_submit.click(record_feedback, inputs=[session_id, fb_choice, fb_text], outputs=fb_status)
703
+ export_btn.click(do_export, inputs=[session_id], outputs=[export_file])
704
+
705
+ probe_btn.click(run_probe, inputs=[session_id], outputs=[probe_md, warn_md])
706
+ selftest_btn.click(run_live_log_selftest, inputs=[session_id], outputs=[selftest_md, warn_md])
707
+
708
+ if __name__ == "__main__":
709
+ # If running locally: set server_name to 0.0.0.0 for external access; Space ignores.
710
+ demo.launch()