| from __future__ import annotations |
|
|
| import os |
| import socket |
| from pathlib import Path |
| from typing import Any |
|
|
| import gradio as gr |
|
|
| try: |
| from dotenv import load_dotenv |
| except ImportError: |
| load_dotenv = None |
|
|
| from evd_agent.conversation import ConversationManager |
| from evd_agent.explainability import ( |
| render_alert_banner, |
| render_classification_panel, |
| ) |
|
|
|
|
| def _load_environment_file() -> None: |
| if load_dotenv is not None: |
| load_dotenv() |
| return |
|
|
| env_path = Path(".env") |
| if not env_path.exists(): |
| return |
|
|
| for raw_line in env_path.read_text(encoding="utf-8").splitlines(): |
| line = raw_line.strip() |
| if not line or line.startswith("#") or "=" not in line: |
| continue |
|
|
| key, value = line.split("=", 1) |
| key = key.strip() |
| value = value.strip().strip('"').strip("'") |
|
|
| if key and key not in os.environ: |
| os.environ[key] = value |
|
|
|
|
| _load_environment_file() |
|
|
|
|
| def _require_llm_configuration() -> None: |
| if not ( |
| os.getenv("EVD_LLM_API_KEY") |
| or os.getenv("OPENAI_API_KEY") |
| ): |
| raise RuntimeError( |
| "LLM credentials are required. Set EVD_LLM_API_KEY or OPENAI_API_KEY before starting the app." |
| ) |
|
|
|
|
| manager = ConversationManager(context_path=os.getenv("EVD_CONTEXT_PATH")) |
|
|
|
|
| def _resolve_server_port() -> int: |
| preferred_port = int(os.getenv("GRADIO_SERVER_PORT", os.getenv("PORT", "7860"))) |
| for port in range(preferred_port, preferred_port + 100): |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: |
| sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| try: |
| sock.bind(("0.0.0.0", port)) |
| except OSError: |
| continue |
| return port |
| raise RuntimeError(f"Could not find a free port starting at {preferred_port}.") |
|
|
|
|
| def _history_to_chatbot(state) -> list[dict[str, Any]]: |
| messages: list[dict[str, Any]] = [] |
| for turn in state.history: |
| messages.append({"role": turn.role, "content": turn.content}) |
| return messages |
|
|
|
|
| def _context_panel() -> str: |
| return manager.context_engine.context_summary() |
|
|
|
|
| def initialize_session(): |
| state, _ = manager.start_session() |
| return ( |
| state, |
| _history_to_chatbot(state), |
| "", |
| render_classification_panel(state.decision), |
| _context_panel(), |
| ) |
|
|
|
|
| def submit_message(user_message: str, state): |
| if state is None: |
| state = manager.new_state() |
|
|
| cleaned = (user_message or "").strip() |
| if not cleaned: |
| return ( |
| state, |
| _history_to_chatbot(state), |
| "", |
| render_classification_panel(state.decision), |
| _context_panel(), |
| "", |
| ) |
|
|
| result = manager.process_turn(state, cleaned) |
|
|
| alert = render_alert_banner(result.decision) |
| classification = render_classification_panel(result.decision) |
|
|
| return ( |
| state, |
| _history_to_chatbot(state), |
| alert, |
| classification, |
| _context_panel(), |
| "", |
| ) |
|
|
|
|
| def build_app() -> gr.Blocks: |
| with gr.Blocks(title="EVD Clinical Screening Agent") as demo: |
| gr.Markdown("# EVD Clinical Screening AI Agent") |
| gr.Markdown( |
| "Adaptive clinical reasoning assistant for rapid Ebola suspected/probable case screening. " |
| "This tool is decision support and does not replace national case management protocols." |
| ) |
|
|
| interview_state = gr.State() |
|
|
| with gr.Row(): |
| with gr.Column(scale=2): |
| chatbot = gr.Chatbot(label="Clinical Interview", height=500) |
| message_box = gr.Textbox( |
| label="Clinician Input", |
| placeholder="Enter findings, symptoms, exposures, travel, and context." |
| ) |
| with gr.Row(): |
| submit_btn = gr.Button("Send", variant="primary") |
| reset_btn = gr.Button("Reset Session") |
|
|
| with gr.Column(scale=1): |
| alert_banner = gr.Markdown(label="Alert") |
| classification_panel = gr.Markdown(label="Classification") |
| context_panel = gr.Markdown(label="Epidemiological Context") |
|
|
| demo.load( |
| initialize_session, |
| inputs=[], |
| outputs=[ |
| interview_state, |
| chatbot, |
| alert_banner, |
| classification_panel, |
| context_panel, |
| ], |
| ) |
|
|
| submit_btn.click( |
| submit_message, |
| inputs=[message_box, interview_state], |
| outputs=[ |
| interview_state, |
| chatbot, |
| alert_banner, |
| classification_panel, |
| context_panel, |
| message_box, |
| ], |
| ) |
|
|
| message_box.submit( |
| submit_message, |
| inputs=[message_box, interview_state], |
| outputs=[ |
| interview_state, |
| chatbot, |
| alert_banner, |
| classification_panel, |
| context_panel, |
| message_box, |
| ], |
| ) |
|
|
| reset_btn.click( |
| initialize_session, |
| inputs=[], |
| outputs=[ |
| interview_state, |
| chatbot, |
| alert_banner, |
| classification_panel, |
| context_panel, |
| ], |
| ) |
|
|
| return demo |
|
|
|
|
| if __name__ == "__main__": |
| _require_llm_configuration() |
| app = build_app() |
| running_in_space = bool(os.getenv("SPACE_ID") or os.getenv("HF_SPACE_ID")) |
| app.launch( |
| server_name="0.0.0.0", |
| server_port=_resolve_server_port(), |
| share=running_in_space, |
| theme=gr.themes.Soft(), |
| ) |
|
|