AI Bot Claude Sonnet 4.6 commited on
fix: sanitize ICD-10 codes + UI/chat history fixes
Browse filesStrip 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>
- app.py +44 -42
- src/agents.py +7 -1
- 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 |
-
|
|
|
|
| 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,
|
| 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,
|
| 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,
|
| 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,
|
| 1123 |
except Exception as exc:
|
| 1124 |
answer = f"⚠️ {exc}"
|
| 1125 |
-
history = list(history or []) + [
|
|
|
|
|
|
|
|
|
|
| 1126 |
return history, ""
|
| 1127 |
|
| 1128 |
|
|
@@ -1207,8 +1210,7 @@ label span, .gr-form > label {
|
|
| 1207 |
}
|
| 1208 |
footer { display: none !important; }
|
| 1209 |
|
| 1210 |
-
/* ── TTS button
|
| 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 |
-
|
| 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
|
| 1646 |
-
inputs=[
|
| 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": "
|
| 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"],
|