Slaiwala commited on
Commit
95e8e59
·
verified ·
1 Parent(s): 70d7b49

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +107 -66
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # app.py — Spine Coder (Chatbot + Feedback + Session Logs) — Gradio 4.x
2
  # ------------------------------------------------------------------------------
3
- # Stable build: FINAL-v2.0 core wired, transitional regions, flags, laterality,
4
- # structured logs, graceful errors, and a clean UI.
5
  # ------------------------------------------------------------------------------
6
 
7
  import os
@@ -20,13 +20,13 @@ import os as _os, sys as _sys, inspect as _inspect
20
  _sys.path.insert(0, _os.path.abspath(".")) # ensure repo root is first on sys.path
21
 
22
  try:
23
- # If your package layout is spine_coder/spine_coder/spine_coder_core.py
24
  from spine_coder.spine_coder.spine_coder_core import suggest_with_cpt_billing
25
  except ImportError:
26
- # Fallback to flat layout if you later flatten the folder tree
27
  from spine_coder.spine_coder_core import suggest_with_cpt_billing
28
 
29
- # Prove which file is actually loaded (shows in Space logs)
30
  try:
31
  print("[CORE] loaded from:", _inspect.getsourcefile(suggest_with_cpt_billing))
32
  except Exception:
@@ -64,63 +64,56 @@ def export_session(session_id: str) -> str:
64
  json.dump(data, f, indent=2, ensure_ascii=False)
65
  return out_path
66
 
 
67
  # ==== UI helpers ==============================================================
68
 
69
- TABLE_COLS = [
70
  "CPT","Modifier","Modifiers","Description","Rationale",
71
  "Confidence","Primary","Category","Laterality","Units"
72
  ]
73
- EMPTY_DF = pd.DataFrame(columns=TABLE_COLS)
 
74
 
75
- def _coalesce_suggestions(result: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
76
- """
77
- Return (rows_for_table, meta_badges) from core result.
78
- Robust to minor schema differences (list vs dict for levels/flags).
79
- """
80
  if not isinstance(result, dict):
81
- return [], {}
82
 
83
- # ---------- rows ----------
84
  sugg = result.get("suggestions") or []
85
  rows: List[Dict[str, Any]] = []
86
  case_lat = (result.get("laterality") or "").strip().lower()
87
 
88
  for s in sugg:
89
- if not isinstance(s, dict):
90
  continue
91
-
92
- # derive row laterality
93
- row_lat = (s.get("laterality") or "").strip().lower()
94
  mods = s.get("modifiers", []) or []
 
 
95
  if not row_lat:
96
- if isinstance(mods, list) and "LT" in mods:
97
- row_lat = "left"
98
- elif isinstance(mods, list) and "RT" in mods:
99
- row_lat = "right"
100
- elif case_lat in ("left", "right", "bilateral"):
101
- row_lat = case_lat
102
 
103
  conf_val = s.get("confidence")
104
- conf_out = round(float(conf_val), 2) if isinstance(conf_val, (int, float)) else (conf_val or "")
105
 
106
  rows.append({
107
- "CPT": s.get("cpt", ""),
108
  "Modifier": s.get("modifier") or "",
109
  "Modifiers": ", ".join(mods) if isinstance(mods, list) else (s.get("modifiers") or ""),
110
- "Description": s.get("desc", ""),
111
- "Rationale": s.get("rationale", ""),
112
  "Confidence": conf_out,
113
  "Primary": "✓" if s.get("primary") else "",
114
- "Category": s.get("category", ""),
115
  "Laterality": row_lat,
116
- "Units": s.get("units", 1),
117
  })
118
 
119
- # ---------- normalize levels ----------
120
  segs: List[str] = []
121
  inters_list: List[str] = []
122
- lvl_lat: str = ""
123
-
124
  levels_obj = result.get("levels")
125
  if isinstance(levels_obj, dict):
126
  segs = list(levels_obj.get("segments") or [])
@@ -129,7 +122,7 @@ def _coalesce_suggestions(result: Dict[str, Any]) -> Tuple[List[Dict[str, Any]],
129
  elif isinstance(levels_obj, list):
130
  segs = [str(x) for x in levels_obj]
131
 
132
- # ---------- normalize flags ----------
133
  flags_obj = result.get("flags")
134
  if isinstance(flags_obj, list):
135
  flags_list = [str(x) for x in flags_obj]
@@ -138,22 +131,33 @@ def _coalesce_suggestions(result: Dict[str, Any]) -> Tuple[List[Dict[str, Any]],
138
  else:
139
  flags_list = []
140
 
141
- # ---------- meta ----------
142
  meta = {
143
- "payer": result.get("payer", ""),
144
- "region": result.get("region", ""),
145
- "laterality": result.get("laterality", "") or lvl_lat,
146
  "levels_segments": ", ".join(segs),
147
- # Prefer numeric estimate if present; else join the interspaces list
148
  "levels_interspaces": (
149
- str(result.get("interspaces_est", "")) if "interspaces_est" in result
150
  else ", ".join(inters_list)
151
  ),
152
  "flags": ", ".join(sorted(flags_list)),
153
- "build": result.get("build", ""),
154
- "mode": result.get("mode", ""),
155
  }
156
- return rows, meta
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  def _summary_md(meta: Dict[str, Any]) -> str:
159
  chips = []
@@ -169,11 +173,16 @@ def _summary_md(meta: Dict[str, Any]) -> str:
169
  def new_session() -> str:
170
  return str(uuid.uuid4())[:8]
171
 
 
172
  # ==== Core actions ============================================================
173
 
174
  def run_inference(note: str, payer: str, top_k: int, session_id: str):
175
  if not note.strip():
176
- return EMPTY_DF, "—", "", "", session_id
 
 
 
 
177
 
178
  _append_log(session_id, {"event": "request", "payer": payer, "top_k": top_k, "note": note})
179
  warn_text = ""
@@ -189,22 +198,27 @@ def run_inference(note: str, payer: str, top_k: int, session_id: str):
189
  warn = f"⚠️ Error: {e}"
190
  if DEBUG:
191
  warn += f"\n\n```traceback\n{tb}\n```"
192
- return EMPTY_DF, "—", "", warn, session_id
193
-
194
- rows, meta = _coalesce_suggestions(result)
195
- df = EMPTY_DF if not rows else pd.DataFrame(rows)
196
- if not df.empty:
197
- # ensure consistent column order
198
- for col in TABLE_COLS:
199
- if col not in df.columns:
200
- df[col] = ""
201
- df = df[TABLE_COLS]
202
-
203
  summary = _summary_md(meta)
204
  json_pretty = json.dumps(result, indent=2, ensure_ascii=False)
205
 
206
- _append_log(session_id, {"event": "response", "meta": meta, "rows_len": len(rows)})
207
- return df, summary, json_pretty, warn_text, session_id
 
 
 
 
 
 
 
 
208
 
209
  def record_feedback(session_id: str, vote: str, text: str):
210
  if not vote and not text:
@@ -218,7 +232,14 @@ def do_export(session_id: str):
218
  return path
219
 
220
  def on_clear():
221
- return "", "Medicare", 10, EMPTY_DF, "—", "", "", new_session()
 
 
 
 
 
 
 
222
 
223
  # ==== Examples ================================================================
224
 
@@ -228,6 +249,11 @@ EXAMPLES = [
228
  ["Posterior cervical foraminotomy right C6–C7; no fusion or instrumentation.", "Medicare", 10],
229
  ["ALIF L5–S1 with structural allograft; non-segmental instrumentation placed.", "Medicare", 10],
230
  ["Removal of posterior segmental instrumentation T10–L2; no new hardware placed.", "Medicare", 10],
 
 
 
 
 
231
  ]
232
 
233
  # ==== Theme / CSS =============================================================
@@ -266,11 +292,11 @@ CUSTOM_CSS = """
266
  /* Table container */
267
  .table-wrap { max-height: 520px; overflow: auto; }
268
 
269
- /* Base table styles */
270
  #suggestions_table .dataframe {
271
  font-size: 15px;
272
  width: 100% !important;
273
- table-layout: auto !important; /* allow columns to size naturally */
274
  border-collapse: collapse;
275
  }
276
  #suggestions_table .dataframe th,
@@ -386,15 +412,28 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Spine Coder — CPT Billing")
386
  with gr.Column(scale=7):
387
  gr.Markdown("#### Results")
388
  summary_md = gr.Markdown("—", elem_classes=["badge-row"])
 
 
389
  table = gr.Dataframe(
390
- value=EMPTY_DF,
391
- label="Suggestions",
392
  interactive=False,
393
  row_count=(0, "dynamic"),
394
  wrap=True,
395
  elem_classes=["table-wrap"],
396
- elem_id="suggestions_table", # important for CSS targeting
397
  )
 
 
 
 
 
 
 
 
 
 
 
398
  with gr.Accordion("Raw JSON", open=False):
399
  json_out = gr.Code(language="json", value="", interactive=False)
400
  warn_md = gr.Markdown("")
@@ -407,15 +446,17 @@ with gr.Blocks(theme=THEME, css=CUSTOM_CSS, title="Spine Coder — CPT Billing")
407
  demo.load(_on_load, outputs=[session_id, sid_show])
408
 
409
  run_inputs = [note_in, payer_dd, topk, session_id]
410
- run_outputs = [table, summary_md, json_out, warn_md, session_id]
411
 
412
  run_btn.click(run_inference, inputs=run_inputs, outputs=run_outputs)
413
  note_in.submit(run_inference, inputs=run_inputs, outputs=run_outputs)
414
 
415
- clear_btn.click(on_clear, outputs=[note_in, payer_dd, topk, table, summary_md, json_out, warn_md, session_id])
 
 
 
416
 
417
  fb_submit.click(record_feedback, inputs=[session_id, fb_choice, fb_text], outputs=fb_status)
418
-
419
  export_btn.click(do_export, inputs=[session_id], outputs=[export_file])
420
 
421
  if __name__ == "__main__":
 
1
  # app.py — Spine Coder (Chatbot + Feedback + Session Logs) — Gradio 4.x
2
  # ------------------------------------------------------------------------------
3
+ # Stable build for FINAL-v2.1 core: transitional regions, flags, laterality,
4
+ # case-level modifiers panel, structured logs, graceful errors, and clean UI.
5
  # ------------------------------------------------------------------------------
6
 
7
  import os
 
20
  _sys.path.insert(0, _os.path.abspath(".")) # ensure repo root is first on sys.path
21
 
22
  try:
23
+ # Preferred package layout: spine_coder/spine_coder/spine_coder_core.py
24
  from spine_coder.spine_coder.spine_coder_core import suggest_with_cpt_billing
25
  except ImportError:
26
+ # Fallback to flat layout if you flattened the folder tree
27
  from spine_coder.spine_coder_core import suggest_with_cpt_billing
28
 
29
+ # Prove which file is actually loaded (visible in Space logs)
30
  try:
31
  print("[CORE] loaded from:", _inspect.getsourcefile(suggest_with_cpt_billing))
32
  except Exception:
 
64
  json.dump(data, f, indent=2, ensure_ascii=False)
65
  return out_path
66
 
67
+
68
  # ==== UI helpers ==============================================================
69
 
70
+ SUGG_COLS = [
71
  "CPT","Modifier","Modifiers","Description","Rationale",
72
  "Confidence","Primary","Category","Laterality","Units"
73
  ]
74
+ EMPTY_SUGG_DF = pd.DataFrame(columns=SUGG_COLS)
75
+ EMPTY_MODS_DF = pd.DataFrame(columns=["modifier","reason"])
76
 
77
+ def _coalesce_rows(result: Dict[str, Any]) -> Tuple[pd.DataFrame, Dict[str, Any]]:
78
+ """Build the suggestions table and meta badges from core result."""
 
 
 
79
  if not isinstance(result, dict):
80
+ return EMPTY_SUGG_DF, {}
81
 
 
82
  sugg = result.get("suggestions") or []
83
  rows: List[Dict[str, Any]] = []
84
  case_lat = (result.get("laterality") or "").strip().lower()
85
 
86
  for s in sugg:
87
+ if not isinstance(s, dict):
88
  continue
 
 
 
89
  mods = s.get("modifiers", []) or []
90
+ # derive row laterality from LT/RT or case laterality
91
+ row_lat = (s.get("laterality") or "").strip().lower()
92
  if not row_lat:
93
+ if isinstance(mods, list) and "LT" in mods: row_lat = "left"
94
+ elif isinstance(mods, list) and "RT" in mods: row_lat = "right"
95
+ elif case_lat in ("left","right","bilateral"): row_lat = case_lat
 
 
 
96
 
97
  conf_val = s.get("confidence")
98
+ conf_out = round(float(conf_val), 2) if isinstance(conf_val, (int,float)) else (conf_val or "")
99
 
100
  rows.append({
101
+ "CPT": s.get("cpt",""),
102
  "Modifier": s.get("modifier") or "",
103
  "Modifiers": ", ".join(mods) if isinstance(mods, list) else (s.get("modifiers") or ""),
104
+ "Description": s.get("desc",""),
105
+ "Rationale": s.get("rationale",""),
106
  "Confidence": conf_out,
107
  "Primary": "✓" if s.get("primary") else "",
108
+ "Category": s.get("category",""),
109
  "Laterality": row_lat,
110
+ "Units": s.get("units",1),
111
  })
112
 
113
+ # normalize levels
114
  segs: List[str] = []
115
  inters_list: List[str] = []
116
+ lvl_lat = ""
 
117
  levels_obj = result.get("levels")
118
  if isinstance(levels_obj, dict):
119
  segs = list(levels_obj.get("segments") or [])
 
122
  elif isinstance(levels_obj, list):
123
  segs = [str(x) for x in levels_obj]
124
 
125
+ # normalize flags
126
  flags_obj = result.get("flags")
127
  if isinstance(flags_obj, list):
128
  flags_list = [str(x) for x in flags_obj]
 
131
  else:
132
  flags_list = []
133
 
 
134
  meta = {
135
+ "payer": result.get("payer",""),
136
+ "region": result.get("region",""),
137
+ "laterality": result.get("laterality","") or lvl_lat,
138
  "levels_segments": ", ".join(segs),
 
139
  "levels_interspaces": (
140
+ str(result.get("interspaces_est","")) if "interspaces_est" in result
141
  else ", ".join(inters_list)
142
  ),
143
  "flags": ", ".join(sorted(flags_list)),
144
+ "build": result.get("build",""),
145
+ "mode": result.get("mode",""),
146
  }
147
+
148
+ df = EMPTY_SUGG_DF if not rows else pd.DataFrame(rows)
149
+ if not df.empty:
150
+ for col in SUGG_COLS:
151
+ if col not in df.columns:
152
+ df[col] = ""
153
+ df = df[SUGG_COLS]
154
+ return df, meta
155
+
156
+ def _case_mods_df(result: Dict[str, Any]) -> pd.DataFrame:
157
+ mods = result.get("case_modifiers", []) or []
158
+ if not mods:
159
+ return pd.DataFrame([{"modifier":"—","reason":"No case-level modifiers"}])
160
+ return pd.DataFrame([{"modifier": f"-{m.get('modifier','')}", "reason": m.get("reason","")} for m in mods])
161
 
162
  def _summary_md(meta: Dict[str, Any]) -> str:
163
  chips = []
 
173
  def new_session() -> str:
174
  return str(uuid.uuid4())[:8]
175
 
176
+
177
  # ==== Core actions ============================================================
178
 
179
  def run_inference(note: str, payer: str, top_k: int, session_id: str):
180
  if not note.strip():
181
+ return (
182
+ EMPTY_SUGG_DF, # suggestions table
183
+ pd.DataFrame([{"modifier":"—","reason":""}]), # case mods table
184
+ "—", "", "", session_id # summary, json, warn, session
185
+ )
186
 
187
  _append_log(session_id, {"event": "request", "payer": payer, "top_k": top_k, "note": note})
188
  warn_text = ""
 
198
  warn = f"⚠️ Error: {e}"
199
  if DEBUG:
200
  warn += f"\n\n```traceback\n{tb}\n```"
201
+ return (
202
+ EMPTY_SUGG_DF,
203
+ pd.DataFrame([{"modifier":"—","reason":""}]),
204
+ "—", "", warn, session_id
205
+ )
206
+
207
+ sugg_df, meta = _coalesce_rows(result)
208
+ case_mods_df = _case_mods_df(result)
 
 
 
209
  summary = _summary_md(meta)
210
  json_pretty = json.dumps(result, indent=2, ensure_ascii=False)
211
 
212
+ _append_log(session_id, {
213
+ "event": "response",
214
+ "meta": {
215
+ **meta,
216
+ # log case modifiers as a short string for quick scanning
217
+ "case_modifiers": ", ".join([f"-{m}" for m in [cm.get("modifier","") for cm in (result.get("case_modifiers") or [])] if m]) or ""
218
+ },
219
+ "rows_len": int(len(sugg_df) if hasattr(sugg_df, "__len__") else 0)
220
+ })
221
+ return sugg_df, case_mods_df, summary, json_pretty, "", session_id
222
 
223
  def record_feedback(session_id: str, vote: str, text: str):
224
  if not vote and not text:
 
232
  return path
233
 
234
  def on_clear():
235
+ return (
236
+ "", "Medicare", 10,
237
+ EMPTY_SUGG_DF,
238
+ pd.DataFrame([{"modifier":"—","reason":""}]),
239
+ "—", "", "",
240
+ new_session()
241
+ )
242
+
243
 
244
  # ==== Examples ================================================================
245
 
 
249
  ["Posterior cervical foraminotomy right C6–C7; no fusion or instrumentation.", "Medicare", 10],
250
  ["ALIF L5–S1 with structural allograft; non-segmental instrumentation placed.", "Medicare", 10],
251
  ["Removal of posterior segmental instrumentation T10–L2; no new hardware placed.", "Medicare", 10],
252
+ # Case-modifier smoke tests:
253
+ ["TLIF L4–L5 was initiated but aborted midway due to neuromonitoring changes.", "Medicare", 10], # -53
254
+ ["Bilateral decompression and foraminotomy at L4–L5 and L5–S1.", "Medicare", 10], # -50
255
+ ["Assistant surgeon present; resident not available.", "Medicare", 10], # -82 not -80
256
+ ["Complex exposure with severe deformity and adhesiolysis.", "Medicare", 10], # -22
257
  ]
258
 
259
  # ==== Theme / CSS =============================================================
 
292
  /* Table container */
293
  .table-wrap { max-height: 520px; overflow: auto; }
294
 
295
+ /* Suggestions table target */
296
  #suggestions_table .dataframe {
297
  font-size: 15px;
298
  width: 100% !important;
299
+ table-layout: auto !important;
300
  border-collapse: collapse;
301
  }
302
  #suggestions_table .dataframe th,
 
412
  with gr.Column(scale=7):
413
  gr.Markdown("#### Results")
414
  summary_md = gr.Markdown("—", elem_classes=["badge-row"])
415
+
416
+ # Suggestions table
417
  table = gr.Dataframe(
418
+ value=EMPTY_SUGG_DF,
419
+ label="CPT Suggestions",
420
  interactive=False,
421
  row_count=(0, "dynamic"),
422
  wrap=True,
423
  elem_classes=["table-wrap"],
424
+ elem_id="suggestions_table",
425
  )
426
+
427
+ # Case-level modifiers table
428
+ gr.Markdown("### Case Modifiers (visit-level)")
429
+ case_mods_table = gr.Dataframe(
430
+ value=EMPTY_MODS_DF,
431
+ headers=["modifier","reason"],
432
+ interactive=False,
433
+ wrap=True,
434
+ label="Case Modifiers",
435
+ )
436
+
437
  with gr.Accordion("Raw JSON", open=False):
438
  json_out = gr.Code(language="json", value="", interactive=False)
439
  warn_md = gr.Markdown("")
 
446
  demo.load(_on_load, outputs=[session_id, sid_show])
447
 
448
  run_inputs = [note_in, payer_dd, topk, session_id]
449
+ run_outputs = [table, case_mods_table, summary_md, json_out, warn_md, session_id]
450
 
451
  run_btn.click(run_inference, inputs=run_inputs, outputs=run_outputs)
452
  note_in.submit(run_inference, inputs=run_inputs, outputs=run_outputs)
453
 
454
+ clear_btn.click(
455
+ on_clear,
456
+ outputs=[note_in, payer_dd, topk, table, case_mods_table, summary_md, json_out, warn_md, session_id]
457
+ )
458
 
459
  fb_submit.click(record_feedback, inputs=[session_id, fb_choice, fb_text], outputs=fb_status)
 
460
  export_btn.click(do_export, inputs=[session_id], outputs=[export_file])
461
 
462
  if __name__ == "__main__":