Spaces:
Sleeping
Sleeping
KevinIsInCoding
feat: structured eligibility assessment with per-criterion verdicts (#14)
e15864e unverified | from __future__ import annotations | |
| import dataclasses | |
| import re | |
| from typing import Generator | |
| import anthropic | |
| import gradio as gr | |
| from beacon_logging import get_logger | |
| from dotenv import load_dotenv | |
| from agents.intake import intake_greeting, stream_intake_turn | |
| from agents.research import stream_research_agent | |
| from models import PatientProfile | |
| from translations import LANGUAGES, UI | |
| load_dotenv() | |
| _logger = get_logger("app") | |
| _VERDICT_STYLE = { | |
| "✓": "background:#22c55e;color:#fff;padding:2px 10px;border-radius:12px;font-weight:700;", | |
| "✗": "background:#ef4444;color:#fff;padding:2px 10px;border-radius:12px;font-weight:700;", | |
| "!": "background:#eab308;color:#fff;padding:2px 10px;border-radius:12px;font-weight:700;", | |
| } | |
| _VERDICT_RE = re.compile(r"(?m)^(\s*)(✓|✗|!)\s") | |
| def _colorize(text: str) -> str: | |
| def _replace(m: re.Match) -> str: | |
| symbol = m.group(2) | |
| style = _VERDICT_STYLE[symbol] | |
| return f'{m.group(1)}<span style="{style}">{symbol}</span> ' | |
| return _VERDICT_RE.sub(_replace, text) | |
| def initialize(lang: str = "en"): | |
| client = anthropic.Anthropic() | |
| text, msgs = intake_greeting(client, lang) | |
| chat = [{"role": "assistant", "content": text}] | |
| return chat, msgs, None, "intake" | |
| def respond( | |
| user_msg: str, | |
| chat_history: list, | |
| intake_msgs: list, | |
| profile: PatientProfile | None, | |
| phase: str, | |
| lang: str, | |
| ) -> Generator: | |
| if not user_msg.strip() or phase == "done": | |
| yield chat_history, intake_msgs, profile, phase, gr.update(), gr.update() | |
| return | |
| t = UI[lang] | |
| client = anthropic.Anthropic() | |
| chat_history = chat_history + [{"role": "user", "content": user_msg}] | |
| new_intake_msgs = intake_msgs + [{"role": "user", "content": user_msg}] | |
| yield chat_history, intake_msgs, profile, phase, gr.update(value=""), gr.update() | |
| intake_text = "" | |
| for event in stream_intake_turn(client, new_intake_msgs, lang): | |
| if event[0] == "token": | |
| intake_text += event[1] | |
| yield ( | |
| chat_history + [{"role": "assistant", "content": intake_text}], | |
| intake_msgs, profile, phase, gr.update(), gr.update(), | |
| ) | |
| elif event[0] == "reset_stream": | |
| intake_text = "" | |
| elif event[0] == "text": | |
| _, full_text, updated_msgs = event | |
| chat_history = chat_history + [{"role": "assistant", "content": full_text}] | |
| yield ( | |
| chat_history, updated_msgs, profile, "intake", | |
| gr.update(interactive=True), gr.update(), | |
| ) | |
| return | |
| elif event[0] == "profile": | |
| _, new_profile, updated_msgs = event | |
| _logger.info( | |
| "Patient intake complete (web)", | |
| extra={"data": {"intake_summary": dataclasses.asdict(new_profile)}}, | |
| ) | |
| if intake_text: | |
| chat_history = chat_history + [{"role": "assistant", "content": intake_text}] | |
| chat_history = chat_history + [{"role": "assistant", "content": t["status_searching"]}] | |
| yield ( | |
| chat_history, updated_msgs, new_profile, "researching", | |
| gr.update(interactive=False, placeholder=t["searching"]), | |
| gr.update(visible=False), | |
| ) | |
| stream_text = "" | |
| for rev in stream_research_agent(client, new_profile): | |
| if rev[0] == "token": | |
| stream_text += rev[1] | |
| yield ( | |
| chat_history + [{"role": "assistant", "content": _colorize(stream_text)}], | |
| updated_msgs, new_profile, "researching", | |
| gr.update(interactive=False, placeholder=t["searching"]), | |
| gr.update(visible=False), | |
| ) | |
| elif rev[0] == "status": | |
| yield ( | |
| chat_history + [{"role": "assistant", "content": rev[1]}], | |
| updated_msgs, new_profile, "researching", | |
| gr.update(interactive=False, placeholder=t["searching"]), | |
| gr.update(visible=False), | |
| ) | |
| elif rev[0] == "done": | |
| analysis = _colorize(rev[1] or t["no_analysis"]) | |
| chat_history = chat_history + [{"role": "assistant", "content": analysis}] | |
| yield ( | |
| chat_history, updated_msgs, new_profile, "done", | |
| gr.update(interactive=False, placeholder=t["search_complete"]), | |
| gr.update(visible=True), | |
| ) | |
| return | |
| def change_language(lang: str): | |
| t = UI[lang] | |
| chat, msgs, _, phase = initialize(lang) | |
| return ( | |
| chat, msgs, None, phase, | |
| gr.update(value=t["heading"]), | |
| gr.update(placeholder=t["placeholder"], interactive=True), | |
| gr.update(value=t["send"]), | |
| gr.update(value=t["new_search"], visible=False), | |
| lang, | |
| ) | |
| with gr.Blocks(title=UI["en"]["page_title"]) as demo: | |
| heading_md = gr.Markdown(UI["en"]["heading"]) | |
| with gr.Row(): | |
| gr.Markdown("") # spacer | |
| lang_dropdown = gr.Dropdown( | |
| choices=[(label, code) for code, label in LANGUAGES.items()], | |
| value="en", | |
| show_label=False, | |
| scale=1, | |
| min_width=140, | |
| container=False, | |
| ) | |
| chatbot = gr.Chatbot(height=550, show_label=False, sanitize_html=False) | |
| with gr.Row(): | |
| msg_box = gr.Textbox( | |
| placeholder=UI["en"]["placeholder"], | |
| show_label=False, | |
| scale=9, | |
| autofocus=True, | |
| ) | |
| send_btn = gr.Button(UI["en"]["send"], scale=1, variant="primary") | |
| new_search_btn = gr.Button(UI["en"]["new_search"], visible=False, variant="secondary") | |
| # State | |
| intake_msgs_state = gr.State([]) | |
| profile_state = gr.State(None) | |
| phase_state = gr.State("intake") | |
| lang_state = gr.State("en") | |
| respond_inputs = [msg_box, chatbot, intake_msgs_state, profile_state, phase_state, lang_state] | |
| respond_outputs = [chatbot, intake_msgs_state, profile_state, phase_state, msg_box, new_search_btn] | |
| demo.load( | |
| lambda: initialize("en"), | |
| outputs=[chatbot, intake_msgs_state, profile_state, phase_state], | |
| ) | |
| msg_box.submit(respond, respond_inputs, respond_outputs) | |
| send_btn.click(respond, respond_inputs, respond_outputs) | |
| new_search_btn.click( | |
| initialize, | |
| inputs=[lang_state], | |
| outputs=[chatbot, intake_msgs_state, profile_state, phase_state], | |
| ).then( | |
| lambda lang: ( | |
| gr.update(interactive=True, placeholder=UI[lang]["placeholder"]), | |
| gr.update(visible=False), | |
| ), | |
| inputs=[lang_state], | |
| outputs=[msg_box, new_search_btn], | |
| ) | |
| lang_dropdown.change( | |
| change_language, | |
| inputs=[lang_dropdown], | |
| outputs=[ | |
| chatbot, intake_msgs_state, profile_state, phase_state, | |
| heading_md, msg_box, send_btn, new_search_btn, | |
| lang_state, | |
| ], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |