AI Bot Claude Sonnet 4.6 commited on
Commit
23d79bc
·
unverified ·
1 Parent(s): 5d1ce68

fix: sanitize ICD-10 codes + UI/chat history fixes

Browse files

Strip non-ASCII characters (e.g. Chinese text Qwen prepends) from ICD-10
codes via _clean_icd10() and tighten prompt to require alphanumeric-only.

Also carries forward prior UI fixes: TTS button placement, chat history
messages format, and predict output count alignment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. app.py +44 -42
  2. src/agents.py +7 -1
  3. src/prompts.py +1 -1
app.py CHANGED
@@ -874,17 +874,6 @@ def _build_result_html(result: dict, lang: str) -> str:
874
  {t['actions_label']}
875
  </div>
876
  {msg_html}
877
- <div style='margin-top:12px;'>
878
- <button onclick="(function(){{var el=document.getElementById('tts-btn');if(el){{var b=el.querySelector('button');if(b)b.click();}}}})();"
879
- style='background:#1e3a5f; color:#93c5fd; border:1px solid #2563eb;
880
- border-radius:6px; padding:8px 18px; cursor:pointer;
881
- font-size:0.875rem; font-weight:600; min-height:44px;
882
- touch-action:manipulation; transition:background 0.2s;'
883
- onmouseover="this.style.background='#1d4ed8'"
884
- onmouseout="this.style.background='#1e3a5f'">
885
- {t['tts_btn']}
886
- </button>
887
- </div>
888
  </div>
889
 
890
  <div style='background:#1a1a2e; border-left:4px solid #ED1C24; border-radius:4px;
@@ -1028,22 +1017,24 @@ def on_svg_click(svg_id: str, current_regions: list, lang_choice: str) -> tuple:
1028
 
1029
  def on_lang_change(lang_choice: str, selected_regions):
1030
  lang = _LANG_MAP.get(lang_choice, "en")
1031
- mode_upd, day1_upd, dayx_upd, sym_upd, btn_upd, region_upd, hint_upd, chat_ph_upd, chat_send_upd, tts_upd, chat_lbl_upd = _ui_updates(
 
1032
  lang_choice, current_regions=selected_regions
1033
  )
1034
  return (mode_upd, day1_upd, dayx_upd, sym_upd, btn_upd, region_upd, hint_upd,
1035
  _empty_output_html(lang), _empty_soap_html(lang), get_backend_status_html(lang),
1036
- chat_ph_upd, chat_send_upd, tts_upd, chat_lbl_upd)
1037
 
1038
 
1039
  def on_load(request: gr.Request):
1040
  lang_display = _detect_lang_from_header(
1041
  request.headers.get("accept-language", "")
1042
  )
1043
- mode_upd, day1_upd, dayx_upd, sym_upd, btn_upd, region_upd, hint_upd, chat_ph_upd, chat_send_upd, tts_upd, chat_lbl_upd = _ui_updates(
1044
  lang_display, current_regions=[]
1045
  )
1046
  lang = _LANG_MAP.get(lang_display, "en")
 
1047
  return (
1048
  lang_display,
1049
  mode_upd, day1_upd, dayx_upd,
@@ -1052,7 +1043,7 @@ def on_load(request: gr.Request):
1052
  _empty_output_html(lang),
1053
  _empty_soap_html(lang),
1054
  get_backend_status_html(lang),
1055
- chat_ph_upd, chat_send_upd, tts_upd, chat_lbl_upd,
1056
  )
1057
 
1058
 
@@ -1068,7 +1059,8 @@ def predict(image_1, image_2, symptoms: str, lang_choice: str, selected_regions)
1068
  if not image_1 and not image_2 and not (symptoms or "").strip():
1069
  return (
1070
  _empty_output_html(lang), _empty_soap_html(lang),
1071
- get_backend_status_html(lang), _empty_ctx, [], gr.update(visible=False), "",
 
1072
  )
1073
 
1074
  region = _regions_to_prompt(selected_regions)
@@ -1093,12 +1085,14 @@ def predict(image_1, image_2, symptoms: str, lang_choice: str, selected_regions)
1093
  get_backend_status_html(lang),
1094
  ctx, [],
1095
  gr.update(visible=True),
 
1096
  patient_msg,
1097
  )
1098
  except Exception as exc:
1099
  return (
1100
  _error_html(t, exc), _empty_soap_html(lang),
1101
- get_backend_status_html(lang), _empty_ctx, [], gr.update(visible=False), "",
 
1102
  )
1103
 
1104
 
@@ -1118,11 +1112,20 @@ def on_chat_send(question: str, history: list, context: dict, lang_choice: str):
1118
  lang = _LANG_MAP.get(lang_choice, "en")
1119
  if not question or not question.strip():
1120
  return history, ""
 
 
 
 
 
 
1121
  try:
1122
- answer, _ = chat_agent(question.strip(), context, history, lang)
1123
  except Exception as exc:
1124
  answer = f"⚠️ {exc}"
1125
- history = list(history or []) + [[question.strip(), answer]]
 
 
 
1126
  return history, ""
1127
 
1128
 
@@ -1207,8 +1210,7 @@ label span, .gr-form > label {
1207
  }
1208
  footer { display: none !important; }
1209
 
1210
- /* ── TTS button (Gradio component hidden; HTML button embedded in result) ── */
1211
- #tts-btn { display: none !important; }
1212
  #tts-btn {
1213
  background: #1e3a5f !important;
1214
  color: #93c5fd !important;
@@ -1530,26 +1532,26 @@ with gr.Blocks(css=CSS, js=BLOCKS_JS, theme=gr.themes.Base(), title="MediVision
1530
  with gr.Tabs(elem_id="output-tabs"):
1531
  with gr.TabItem(_I18N["en"]["tab_patient"], elem_id="tab-patient"):
1532
  output_html = gr.HTML(value=_empty_output_html("en"))
1533
- with gr.Row():
1534
- tts_btn = gr.Button(
1535
- _I18N["en"]["tts_btn"],
1536
- variant="secondary",
1537
- size="sm",
1538
- elem_id="tts-btn",
1539
- scale=1,
1540
- min_width=100,
1541
- )
1542
- tts_audio = gr.Audio(
1543
- value=None,
1544
- label=None,
1545
- autoplay=True,
1546
- visible=False,
1547
- show_label=False,
1548
- show_download_button=False,
1549
- )
1550
  with gr.TabItem(_I18N["en"]["tab_doctor"], elem_id="tab-doctor"):
1551
  soap_html = gr.HTML(value=_empty_soap_html("en"))
1552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1553
  # ── Follow-up Q&A chat ────────────────────────────────────────
1554
  with gr.Group(visible=False, elem_id="chat-section") as chat_section:
1555
  chat_label_html = gr.HTML(
@@ -1562,7 +1564,7 @@ with gr.Blocks(css=CSS, js=BLOCKS_JS, theme=gr.themes.Base(), title="MediVision
1562
  elem_id="chat-box",
1563
  height=280,
1564
  show_label=False,
1565
- bubble_full_width=False,
1566
  )
1567
  with gr.Row(equal_height=True):
1568
  chat_input = gr.Textbox(
@@ -1629,7 +1631,7 @@ with gr.Blocks(css=CSS, js=BLOCKS_JS, theme=gr.themes.Base(), title="MediVision
1629
  inputs=[input_img, input_img_2, symptoms_txt, lang_radio, region_selector],
1630
  outputs=[output_html, soap_html, status_bar,
1631
  analysis_context_state, chat_history_state, chat_section,
1632
- patient_msg_state],
1633
  api_name="analyze",
1634
  ).then(
1635
  fn=lambda h: h,
@@ -1642,8 +1644,8 @@ with gr.Blocks(css=CSS, js=BLOCKS_JS, theme=gr.themes.Base(), title="MediVision
1642
  inputs=[patient_msg_state, lang_radio],
1643
  outputs=[tts_audio],
1644
  ).then(
1645
- fn=lambda p: gr.update(visible=bool(p)),
1646
- inputs=[patient_msg_state],
1647
  outputs=[tts_audio],
1648
  )
1649
 
 
874
  {t['actions_label']}
875
  </div>
876
  {msg_html}
 
 
 
 
 
 
 
 
 
 
 
877
  </div>
878
 
879
  <div style='background:#1a1a2e; border-left:4px solid #ED1C24; border-radius:4px;
 
1017
 
1018
  def on_lang_change(lang_choice: str, selected_regions):
1019
  lang = _LANG_MAP.get(lang_choice, "en")
1020
+ t = _I18N.get(lang, _I18N["en"])
1021
+ mode_upd, day1_upd, dayx_upd, sym_upd, btn_upd, region_upd, hint_upd, chat_ph_upd, chat_send_upd, _tts_upd, chat_lbl_upd = _ui_updates(
1022
  lang_choice, current_regions=selected_regions
1023
  )
1024
  return (mode_upd, day1_upd, dayx_upd, sym_upd, btn_upd, region_upd, hint_upd,
1025
  _empty_output_html(lang), _empty_soap_html(lang), get_backend_status_html(lang),
1026
+ chat_ph_upd, chat_send_upd, gr.update(visible=False, value=t["tts_btn"]), chat_lbl_upd)
1027
 
1028
 
1029
  def on_load(request: gr.Request):
1030
  lang_display = _detect_lang_from_header(
1031
  request.headers.get("accept-language", "")
1032
  )
1033
+ mode_upd, day1_upd, dayx_upd, sym_upd, btn_upd, region_upd, hint_upd, chat_ph_upd, chat_send_upd, _tts_upd, chat_lbl_upd = _ui_updates(
1034
  lang_display, current_regions=[]
1035
  )
1036
  lang = _LANG_MAP.get(lang_display, "en")
1037
+ t = _I18N.get(lang, _I18N["en"])
1038
  return (
1039
  lang_display,
1040
  mode_upd, day1_upd, dayx_upd,
 
1043
  _empty_output_html(lang),
1044
  _empty_soap_html(lang),
1045
  get_backend_status_html(lang),
1046
+ chat_ph_upd, chat_send_upd, gr.update(visible=False, value=t["tts_btn"]), chat_lbl_upd,
1047
  )
1048
 
1049
 
 
1059
  if not image_1 and not image_2 and not (symptoms or "").strip():
1060
  return (
1061
  _empty_output_html(lang), _empty_soap_html(lang),
1062
+ get_backend_status_html(lang), _empty_ctx, [], gr.update(visible=False),
1063
+ gr.update(visible=False), "",
1064
  )
1065
 
1066
  region = _regions_to_prompt(selected_regions)
 
1085
  get_backend_status_html(lang),
1086
  ctx, [],
1087
  gr.update(visible=True),
1088
+ gr.update(visible=bool(patient_msg)),
1089
  patient_msg,
1090
  )
1091
  except Exception as exc:
1092
  return (
1093
  _error_html(t, exc), _empty_soap_html(lang),
1094
+ get_backend_status_html(lang), _empty_ctx, [], gr.update(visible=False),
1095
+ gr.update(visible=False), "",
1096
  )
1097
 
1098
 
 
1112
  lang = _LANG_MAP.get(lang_choice, "en")
1113
  if not question or not question.strip():
1114
  return history, ""
1115
+ # Convert messages-format [{role,content},...] to [[user,bot],...] tuples for chat_agent
1116
+ msgs = list(history or [])
1117
+ tuples = []
1118
+ for i in range(0, len(msgs) - 1, 2):
1119
+ if msgs[i].get("role") == "user" and msgs[i+1].get("role") == "assistant":
1120
+ tuples.append([msgs[i]["content"], msgs[i+1]["content"]])
1121
  try:
1122
+ answer, _ = chat_agent(question.strip(), context, tuples, lang)
1123
  except Exception as exc:
1124
  answer = f"⚠️ {exc}"
1125
+ history = list(history or []) + [
1126
+ {"role": "user", "content": question.strip()},
1127
+ {"role": "assistant", "content": answer},
1128
+ ]
1129
  return history, ""
1130
 
1131
 
 
1210
  }
1211
  footer { display: none !important; }
1212
 
1213
+ /* ── TTS button ── */
 
1214
  #tts-btn {
1215
  background: #1e3a5f !important;
1216
  color: #93c5fd !important;
 
1532
  with gr.Tabs(elem_id="output-tabs"):
1533
  with gr.TabItem(_I18N["en"]["tab_patient"], elem_id="tab-patient"):
1534
  output_html = gr.HTML(value=_empty_output_html("en"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1535
  with gr.TabItem(_I18N["en"]["tab_doctor"], elem_id="tab-doctor"):
1536
  soap_html = gr.HTML(value=_empty_soap_html("en"))
1537
 
1538
+ tts_btn = gr.Button(
1539
+ _I18N["en"]["tts_btn"],
1540
+ variant="secondary",
1541
+ size="sm",
1542
+ elem_id="tts-btn",
1543
+ visible=False,
1544
+ min_width=100,
1545
+ )
1546
+ tts_audio = gr.Audio(
1547
+ value=None,
1548
+ label=None,
1549
+ autoplay=True,
1550
+ visible=False,
1551
+ show_label=False,
1552
+ show_download_button=False,
1553
+ )
1554
+
1555
  # ── Follow-up Q&A chat ────────────────────────────────────────
1556
  with gr.Group(visible=False, elem_id="chat-section") as chat_section:
1557
  chat_label_html = gr.HTML(
 
1564
  elem_id="chat-box",
1565
  height=280,
1566
  show_label=False,
1567
+ type="messages",
1568
  )
1569
  with gr.Row(equal_height=True):
1570
  chat_input = gr.Textbox(
 
1631
  inputs=[input_img, input_img_2, symptoms_txt, lang_radio, region_selector],
1632
  outputs=[output_html, soap_html, status_bar,
1633
  analysis_context_state, chat_history_state, chat_section,
1634
+ tts_btn, patient_msg_state],
1635
  api_name="analyze",
1636
  ).then(
1637
  fn=lambda h: h,
 
1644
  inputs=[patient_msg_state, lang_radio],
1645
  outputs=[tts_audio],
1646
  ).then(
1647
+ fn=lambda a: gr.update(visible=bool(a)),
1648
+ inputs=[tts_audio],
1649
  outputs=[tts_audio],
1650
  )
1651
 
src/agents.py CHANGED
@@ -1,6 +1,12 @@
1
  import json
2
  import re
3
 
 
 
 
 
 
 
4
  from src.model_loader import generate_response, generate_text
5
  from src.prompts import (
6
  VISION_AGENT_SYSTEM,
@@ -103,7 +109,7 @@ def clinical_agent(visual_description: str, symptoms: str, lang: str = "en") ->
103
  conditions.append({
104
  "name": str(item.get("name", item.get("condition", "Unknown"))),
105
  "probability": int(item.get("probability", item.get("match_probability", 50))),
106
- "icd10": str(item.get("icd10", item.get("icd10_code", ""))),
107
  })
108
  elif isinstance(item, str):
109
  conditions.append({"name": item, "probability": 50, "icd10": ""})
 
1
  import json
2
  import re
3
 
4
+
5
+ def _clean_icd10(code: str) -> str:
6
+ """Strip any non-ASCII or non-alphanumeric prefix/suffix from ICD-10 codes.
7
+ Models like Qwen sometimes prepend the Chinese translation before the code."""
8
+ return re.sub(r"[^A-Za-z0-9.\-]", "", code)
9
+
10
  from src.model_loader import generate_response, generate_text
11
  from src.prompts import (
12
  VISION_AGENT_SYSTEM,
 
109
  conditions.append({
110
  "name": str(item.get("name", item.get("condition", "Unknown"))),
111
  "probability": int(item.get("probability", item.get("match_probability", 50))),
112
+ "icd10": _clean_icd10(str(item.get("icd10", item.get("icd10_code", "")))),
113
  })
114
  elif isinstance(item, str):
115
  conditions.append({"name": item, "probability": 50, "icd10": ""})
src/prompts.py CHANGED
@@ -36,7 +36,7 @@ Required schema:
36
  "triage_level": "High" or "Medium" or "Low",
37
  "urgency_reason": "one sentence in English explaining WHY this triage level was assigned",
38
  "possible_conditions": [
39
- {"name": "condition name in TARGET LANGUAGE", "probability": integer 5 to 95, "icd10": "X00.0"}
40
  ],
41
  "red_flags": ["specific alarming sign from visual or symptom data — English only"],
42
  "watch_symptoms": ["symptom that should prompt immediate re-evaluation — English only"],
 
36
  "triage_level": "High" or "Medium" or "Low",
37
  "urgency_reason": "one sentence in English explaining WHY this triage level was assigned",
38
  "possible_conditions": [
39
+ {"name": "condition name in TARGET LANGUAGE", "probability": integer 5 to 95, "icd10": "alphanumeric code only e.g. S72.0 — NO text, NO translations, NO language characters before or after the code"}
40
  ],
41
  "red_flags": ["specific alarming sign from visual or symptom data — English only"],
42
  "watch_symptoms": ["symptom that should prompt immediate re-evaluation — English only"],