Spaces:
Sleeping
Sleeping
| """ | |
| app.py | |
| ------ | |
| Main application entry point for the modular AI agent system. Handles UI (Gradio), agent orchestration, knowledge loading, and chat interface logic. | |
| - Initializes logging and structlog for consistent, timestamped logs. | |
| - Loads and manages agent configurations and RAG retriever. | |
| - Provides chat, knowledge ingestion, and agent builder functions. | |
| - Integrates with OpenAI via llm_connector and supports streaming responses. | |
| - Designed for extensibility and future integration with Model Context Protocol (MCP) and Agent-to-Agent (A2A) standards. | |
| """ | |
| # --- Imports --- | |
| import gradio as gr | |
| import json | |
| import re | |
| import openai | |
| from openai import RateLimitError, APIError, APIConnectionError, OpenAI | |
| from typing import Dict, cast | |
| from core.utils.rag import KnowledgeLoader, SimpleRAGRetriever | |
| from core.utils.skills_registry import tool_registry, get_tool_by_name | |
| import asyncio | |
| import structlog | |
| import logging | |
| from structlog.stdlib import LoggerFactory, BoundLogger | |
| from core.utils.llm_connector import AgentLLMConnector | |
| import pandas as pd | |
| from gradio import ChatMessage | |
| from core.agents.agent_utils import linkify_citations, build_agent, load_prefilled, prepare_download, preload_demo_chat, _safe_title, extract_clinical_variables_from_history | |
| from config import agents_config, skills_library, prefilled_agents | |
| from core.ui.ui import build_ui, show_landing, show_builder, show_chat, refresh_active_agents_widgets | |
| import tools | |
| import os | |
| # Print API keys for debugging | |
| # Print all SERPER_* environment variables for robust debugging | |
| #for k, v in os.environ.items(): | |
| # if k.startswith("SERPER"): | |
| # print(f"{k} in Python: {v}") | |
| #print("OPENAI_API_KEY in Python:", os.getenv("OPENAI_API_KEY")) | |
| #print("SERPER_API_KEY in Python:", os.getenv("SERPER_API_KEY")) | |
| # Logging setup | |
| logging.basicConfig(filename="app.log", level=logging.INFO, format="%(message)s") | |
| structlog.configure(logger_factory=LoggerFactory()) | |
| logger: BoundLogger = structlog.get_logger() | |
| # Structlog config | |
| structlog.configure( | |
| processors=[ | |
| structlog.processors.TimeStamper(fmt="iso"), | |
| structlog.dev.ConsoleRenderer() | |
| ], | |
| logger_factory=LoggerFactory(), | |
| wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), | |
| cache_logger_on_first_use=True, | |
| ) | |
| # OpenAI API Key Check | |
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") | |
| if not OPENAI_API_KEY: | |
| raise RuntimeError("OPENAI_API_KEY environment variable is not set. Please set it before running the application.") | |
| OPENAI_API_KEY = cast(str, OPENAI_API_KEY) | |
| # initialize client once, pulling key from your environment | |
| client = OpenAI(api_key=OPENAI_API_KEY) | |
| def simple_chat_response(user_message, history): | |
| """ | |
| A bare-bones GPT-3.5-turbo chat using the v1.0+ SDK. | |
| - history is a list of dicts: [{"role":"user"|"assistant","content":...}, …] | |
| - Returns (updated_history, "") | |
| """ | |
| if history is None: | |
| history = [] | |
| # 1) record the user’s message | |
| history.append({"role": "user", "content": user_message}) | |
| # 2) call the new chat endpoint | |
| completion = client.chat.completions.create( | |
| model="gpt-3.5-turbo", | |
| messages=history, | |
| temperature=0.7, | |
| ) # :contentReference[oaicite:0]{index=0} | |
| # 3) extract and record the assistant reply | |
| reply_content = completion.choices[0].message.content | |
| reply = reply_content.strip() if reply_content else "" | |
| history.append({"role": "assistant", "content": reply}) | |
| # 4) return the chat history (for gr.Chatbot) and clear the input box | |
| return history, "" | |
| # --- Chat orchestration logic is now in chat_orchestrator.py --- | |
| from core.agents.chat_orchestrator import simulate_agent_response_stream, build_log, _stream_to_agent, MAX_HISTORY, orchestrators | |
| def reset_chat(agent_json): | |
| """ | |
| Clears history, input, invocation log, and active_children state. | |
| Also clears orchestrator state to prevent response persistence. | |
| """ | |
| # Clear orchestrator state to prevent persistence across conversations | |
| from core.agents.chat_orchestrator import orchestrators | |
| orchestrators.clear() | |
| agent_data = json.loads(agent_json) | |
| name = agent_data.get("agent_name", "Agent") | |
| welcome = f"👋 Hello! I'm {name}. How can I assist you today?" | |
| chat_history = [{"role":"assistant","content":welcome}] | |
| invocation_log = "" | |
| active_children= [] | |
| return chat_history, "", invocation_log, active_children | |
| def load_agent_to_builder(agent_name): | |
| if agent_name in agents_config: | |
| agent_data = json.loads(agents_config[agent_name]) | |
| return ( | |
| agent_data.get("agent_type", ""), | |
| agent_data.get("agent_name", ""), | |
| agent_data.get("agent_mission", ""), | |
| agent_data.get("skills", []) | |
| ) | |
| else: | |
| return None, "", "", [] | |
| def remove_selected_agent(agent_name: str): | |
| # 1) Remove from your in-memory store | |
| if agent_name in agents_config: | |
| del agents_config[agent_name] | |
| # 2) Re-render the list of active agents | |
| if agents_config: | |
| active_md = "### 🧠 Active Agents\n" + "\n".join(f"- {name}" for name in agents_config) | |
| else: | |
| active_md = "### 🧠 Active Agents\n_(None yet)_" | |
| # 3) Build the new dropdown state | |
| new_choices = list(agents_config.keys()) | |
| # gr.update to reset selection (value=None) and update choices | |
| dropdown_update = gr.update(choices=new_choices, value=None) | |
| # Return in the same order you wired the outputs | |
| return active_md, dropdown_update | |
| def update_skills(selected_type: str): | |
| """Callback to repopulate the skills checkbox based on chosen agent type.""" | |
| return gr.update(choices=skills_library.get(selected_type, [])) | |
| def handle_uploaded_files(files): | |
| """ | |
| Callback for the File component: spin up (or reuse) your RAG retriever | |
| and index any newly uploaded docs. | |
| """ | |
| global rag_retriever | |
| rag_retriever = SimpleRAGRetriever(openai_api_key=os.getenv("OPENAI_API_KEY")) | |
| if files: | |
| for f in files: | |
| rag_retriever.add_knowledge(f) | |
| # Return a tuple of (visible-update, message) to your upload_alert Markdown | |
| return gr.update(visible=True), "✅ Files uploaded and indexed successfully!" | |
| def populate_from_preset(prefilled_name): | |
| if prefilled_name != "None": | |
| at, an, am, sk = load_prefilled(prefilled_name, prefilled_agents) | |
| return at, an, am, sk, True | |
| return None, "", "", [], False | |
| def on_agent_type_change(selected_type, was_prefilled): | |
| # always refresh the skill choices | |
| new_skills = skills_library.get(selected_type, []) | |
| skill_update = gr.update(choices=new_skills, value=[]) | |
| if was_prefilled: | |
| # consume the flag, but leave name & mission intact | |
| return skill_update, gr.update(), gr.update(), False | |
| # manual change: clear everything | |
| return skill_update, gr.update(value=""), gr.update(value=""), False | |
| def chat_selected_agent(agent_name): | |
| agent_json = agents_config.get(agent_name, "") | |
| if agent_json: | |
| # reset_chat returns 4 values: | |
| # (chat_history, cleared_input, invocation_log, active_children) | |
| chat_history, cleared_input, invocation_log, active_children = reset_chat(agent_json) | |
| return chat_history, cleared_input, invocation_log, active_children, agent_json | |
| # If no agent is selected, clear everything | |
| return [], "", "", [], "" | |
| def load_history(agent_name, histories): | |
| # if we’ve never chatted, start with a greeting | |
| if agent_name not in histories: | |
| histories[agent_name] = [ | |
| {"role":"assistant","content":f"👋 Hello! I'm {agent_name}. How can I help?"} | |
| ] | |
| return histories[agent_name] | |
| def chatpanel_handle(agent_name, user_text, histories): | |
| """ | |
| Uses your simulate_agent_response_stream (tool-aware) in a blocking way, | |
| so that tool invocations actually happen. | |
| Returns (final_history, updated_histories, cleared_input). | |
| """ | |
| # 1) Look up the JSON you saved in agents_config | |
| agent_json = agents_config.get(agent_name) | |
| if not agent_json: | |
| return [], histories, "", "" | |
| # 2) Grab the prior history (or seed a greeting) | |
| history = histories.get(agent_name, []) | |
| if not history: | |
| history = [{"role":"assistant", | |
| "content":f"👋 Hello! I'm {agent_name}. How can I help?"}] | |
| # 3) Call your streaming function synchronously | |
| # simulate_agent_response_stream is async, so we need to run it in an event loop | |
| import asyncio | |
| async def run_stream(): | |
| async for updated_history, _, invocation_log, _, challenger_info in simulate_agent_response_stream( | |
| agent_json=agent_json, | |
| history=history, | |
| user_input=user_text, | |
| debug_flag=False, | |
| active_children=[] | |
| ): | |
| yield updated_history, invocation_log | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| gen = run_stream() | |
| final_history = history | |
| final_invocation_log = "" | |
| try: | |
| while True: | |
| updated_history, invocation_log = loop.run_until_complete(gen.__anext__()) | |
| final_history = updated_history | |
| final_invocation_log = invocation_log | |
| except StopAsyncIteration: | |
| pass | |
| # 4) Save back and clear the input box | |
| histories[agent_name] = final_history | |
| return final_history, histories, "", final_invocation_log | |
| def refresh_chat_dropdown(): | |
| return gr.update(choices=list(agents_config.keys()), value=None) | |
| def build_ui(): | |
| # --- App Layout --- | |
| with gr.Blocks() as app: | |
| # Separate chat histories for each panel | |
| simple_chat_history = gr.State([]) | |
| builder_chat_histories = gr.State({}) | |
| deployed_chat_histories = gr.State({}) | |
| # --- Custom CSS --- | |
| app.css = """ | |
| /* Futuristic color palette (lighter background) */ | |
| :root { | |
| --futuristic-bg: #fcfdff; /* nearly white */ | |
| --futuristic-panel: #e8ecf7; | |
| --futuristic-accent: #00ffe7; | |
| --futuristic-accent2: #7f5cff; | |
| --futuristic-card: #fafdff; | |
| --futuristic-border: #c7d0e6; | |
| --futuristic-text: #23263a; | |
| --futuristic-muted: #7a7e8c; | |
| } | |
| body, .gradio-container, .gr-block, .gr-app { | |
| background: var(--futuristic-bg) !important; | |
| color: var(--futuristic-text) !important; | |
| } | |
| #landing_card, | |
| #agent_form { | |
| opacity: 1; | |
| transition: opacity 0.5s ease; | |
| } | |
| .hidden { | |
| opacity: 0 !important; | |
| pointer-events: none; | |
| } | |
| /* Ensure the row containing the panels stretches and has a white background */ | |
| #agent_form .gr-row { | |
| display: flex !important; | |
| align-items: stretch !important; | |
| background: #fff !important; | |
| border-radius: 20px; | |
| box-shadow: 0 4px 32px #7f5cff11, 0 1.5px 8px #00ffe711; | |
| min-height: 600px; | |
| padding: 0 0.5em; | |
| } | |
| /* Panels: remove min-height/height, keep original proportions */ | |
| #agent_form .left-panel, | |
| #agent_form .right-panel { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| /* No height or min-height here */ | |
| } | |
| /* Vibrant animated gradient for 'Get Started' button */ | |
| #start_button { | |
| background: linear-gradient(90deg, #ff6b6b, #fddb3a, #00ffe7, #7f5cff); | |
| background-size: 300% 300%; | |
| color: #181c27 !important; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 700; | |
| box-shadow: 0 0 16px #fddb3a88, 0 0 4px #ff6b6b88; | |
| animation: gradient-move 3s ease-in-out infinite, pulse 2s infinite; | |
| font-size: 18px; | |
| padding: 12px 24px; | |
| transition: background-position 0.5s; | |
| } | |
| /* Exciting but static look for 'Chat With the Agents You Deployed' button */ | |
| #to_chat_button { | |
| background: linear-gradient(90deg, #7f5cff, #a259ff, #6e4cff, #c2a3ff); | |
| background-size: 300% 300%; | |
| color: #fff !important; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 700; | |
| box-shadow: 0 0 16px #a259ff88, 0 0 4px #7f5cff88; | |
| /* No animation here, just static gradient */ | |
| font-size: 18px; | |
| padding: 12px 24px; | |
| } | |
| @keyframes gradient-move { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 16px var(--futuristic-accent2), 0 0 4px var(--futuristic-accent); } | |
| 50% { box-shadow: 0 0 32px var(--futuristic-accent), 0 0 8px var(--futuristic-accent2); } | |
| 100% { box-shadow: 0 0 16px var(--futuristic-accent2), 0 0 4px var(--futuristic-accent); } | |
| } | |
| .left-panel { | |
| background: linear-gradient(135deg, #e8ecf7 60%, #fafdff 100%); | |
| padding: 15px; | |
| border-radius: 16px; | |
| border: 1px solid var(--futuristic-border); | |
| box-shadow: 0 2px 16px #7f5cff22, 0 1.5px 8px #00ffe722; | |
| } | |
| .right-panel { | |
| background: linear-gradient(135deg, #e8ecf7 60%, #fafdff 100%); | |
| padding: 15px; | |
| border-radius: 16px; | |
| border: 1px solid var(--futuristic-border); | |
| box-shadow: 0 2px 16px #7f5cff22, 0 1.5px 8px #00ffe722; | |
| } | |
| #chat_history { | |
| height: 520px; | |
| overflow: auto; | |
| background: #181c27; | |
| border-radius: 12px; | |
| border: 1px solid var(--futuristic-border); | |
| } | |
| #chat_input { | |
| width: 100%; | |
| background: #23263a; | |
| color: var(--futuristic-text); | |
| border-radius: 8px; | |
| border: 1px solid var(--futuristic-accent2); | |
| } | |
| #chat_buttons .gr-button { | |
| width: 49%; | |
| } | |
| .right-panel { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .landing-card { | |
| background: linear-gradient(135deg, #23263a 60%, #1a1f2b 100%); | |
| border: 1.5px solid var(--futuristic-accent2); | |
| border-radius: 28px; | |
| padding: 32px 36px 32px 36px; | |
| margin-bottom: 40px; | |
| box-shadow: 0 4px 32px #00ffe733, 0 1.5px 8px #7f5cff33; | |
| box-sizing: border-box; | |
| } | |
| .landing-title { | |
| text-align: center; | |
| font-size: 2.1rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5em; | |
| } | |
| .landing-subtitle { | |
| text-align: center; | |
| font-size: 1.15rem; | |
| color: var(--futuristic-muted); | |
| margin-bottom: 1.2em; | |
| } | |
| .steps-box { | |
| background: var(--futuristic-card); | |
| border: 1px solid var(--futuristic-border); | |
| border-radius: 16px; | |
| max-width: 840px; | |
| margin: 0 auto 1.5em auto; | |
| padding: 18px 28px 18px 28px; | |
| box-shadow: 0 2px 12px #7f5cff22; | |
| text-align: left; | |
| } | |
| .steps-box ul { | |
| margin: 0; | |
| padding-left: 1.2em; | |
| } | |
| .steps-box li { | |
| margin-bottom: 0.5em; | |
| font-size: 1.05em; | |
| } | |
| .animated-button { | |
| animation: pulse 2s ease-out 1; | |
| font-size: 18px; | |
| padding: 12px 24px; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); box-shadow: 0 0 16px var(--futuristic-accent2); } | |
| 50% { transform: scale(1.05); box-shadow: 0 0 32px var(--futuristic-accent); } | |
| 100% { transform: scale(1); box-shadow: 0 0 16px var(--futuristic-accent2); } | |
| } | |
| #advanced_options .gr-accordion-content { | |
| font-size: 0.85em; | |
| line-height: 1.3; | |
| padding-left: 1rem; | |
| } | |
| #chat_history, | |
| #chat_input, | |
| #chat_input textarea { | |
| box-sizing: border-box; | |
| } | |
| button.generate-btn { | |
| background: linear-gradient(90deg, var(--futuristic-accent2), var(--futuristic-accent)); | |
| color: #181c27 !important; | |
| border: none !important; | |
| font-weight: 600; | |
| box-shadow: 0 4px 24px var(--futuristic-accent2), 0 2px 8px var(--futuristic-accent); | |
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| button.generate-btn:hover { | |
| transform: translateY(-2px) scale(1.03); | |
| box-shadow: 0 8px 32px var(--futuristic-accent), 0 4px 16px var(--futuristic-accent2); | |
| } | |
| button.generate-btn:active { | |
| transform: translateY(0) scale(0.98); | |
| box-shadow: 0 3px 8px var(--futuristic-accent2); | |
| } | |
| .agent-controls { | |
| display: inline-flex !important; | |
| flex-wrap: nowrap !important; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .agent-controls .action-btn, | |
| .agent-controls .agent-dropdown { | |
| width: auto !important; | |
| flex: none !important; | |
| } | |
| .agent-controls .agent-dropdown-column, | |
| .agent-controls .agent-button-group { | |
| background: var(--futuristic-panel) !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| } | |
| .agent-controls > div, | |
| .agent-controls > div > div, | |
| .agent-controls > div > div > div { | |
| background: transparent !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| } | |
| /* Chatbot message bubbles */ | |
| .gr-chat-message.user { | |
| background: linear-gradient(90deg, #23263a 60%, #181c27 100%); | |
| color: var(--futuristic-accent); | |
| border-radius: 12px 12px 4px 12px; | |
| border: 1px solid var(--futuristic-accent2); | |
| } | |
| .gr-chat-message.assistant { | |
| background: linear-gradient(90deg, #181c27 60%, #23263a 100%); | |
| color: var(--futuristic-text); | |
| border-radius: 12px 12px 12px 4px; | |
| border: 1px solid var(--futuristic-accent); | |
| } | |
| /* Muted text */ | |
| .gr-markdown, .gradio-markdown, .gradio-container .gr-markdown { | |
| color: var(--futuristic-muted) !important; | |
| } | |
| /* Patient Cards Section */ | |
| .patient-cards-section { | |
| margin-top: 2rem !important; | |
| padding-top: 1.5rem !important; | |
| border-top: 2px solid #ffffff !important; | |
| padding-bottom: 1rem !important; | |
| background: var(--futuristic-panel) !important; | |
| border-radius: 16px !important; | |
| border: 1px solid var(--futuristic-border) !important; | |
| box-shadow: 0 2px 12px rgba(127, 92, 255, 0.1) !important; | |
| } | |
| .patient-cards-header { | |
| text-align: center !important; | |
| margin-bottom: 1.5rem !important; | |
| color: var(--futuristic-text) !important; | |
| font-size: 1.1rem !important; | |
| font-weight: 600 !important; | |
| } | |
| .patient-cards-row { | |
| margin-bottom: 1rem !important; | |
| gap: 1rem !important; | |
| } | |
| .patient-cards-row:last-child { | |
| margin-bottom: 0 !important; | |
| } | |
| .patient-card { | |
| background: linear-gradient(135deg, var(--futuristic-card) 60%, #ffffff 100%) !important; | |
| border: 2px solid var(--futuristic-border) !important; | |
| border-radius: 12px !important; | |
| padding: 1rem !important; | |
| cursor: pointer !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 2px 8px rgba(127, 92, 255, 0.1) !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| justify-content: space-between !important; | |
| min-height: 120px !important; | |
| margin: 0.5rem !important; | |
| } | |
| .patient-card:hover { | |
| transform: translateY(-3px) !important; | |
| box-shadow: 0 6px 20px rgba(127, 92, 255, 0.2) !important; | |
| border-color: var(--futuristic-accent2) !important; | |
| background: linear-gradient(135deg, #f0f4ff 60%, #ffffff 100%) !important; | |
| } | |
| .patient-card.selected { | |
| border-color: var(--futuristic-accent) !important; | |
| box-shadow: 0 6px 24px rgba(0, 255, 231, 0.3) !important; | |
| background: linear-gradient(135deg, var(--futuristic-accent) 5%, var(--futuristic-card) 60%, #ffffff 100%) !important; | |
| } | |
| .patient-card-btn { | |
| background: transparent !important; | |
| border: none !important; | |
| padding: 0 !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| text-align: left !important; | |
| font-size: 0.85rem !important; | |
| line-height: 1.3 !important; | |
| color: var(--futuristic-text) !important; | |
| cursor: pointer !important; | |
| white-space: pre-line !important; | |
| } | |
| .patient-card-btn:hover { | |
| background: transparent !important; | |
| } | |
| /* Hide chat panel elements globally by default */ | |
| .chat-only-btn { | |
| display: none !important; | |
| } | |
| .chat-panel-buttons { | |
| display: none !important; | |
| } | |
| .patient-cards-group { | |
| display: none !important; | |
| } | |
| .patient-cards-section { | |
| display: none !important; | |
| } | |
| .patient-cards-row { | |
| display: none !important; | |
| } | |
| .patient-cards-grid { | |
| display: none !important; | |
| } | |
| .chat-only-content { | |
| display: none !important; | |
| } | |
| .patient-card-btn { | |
| display: none !important; | |
| } | |
| .patient-cards-container { | |
| display: none !important; | |
| } | |
| .chat-only-section { | |
| display: none !important; | |
| } | |
| /* Hide chat panel elements in builder panel (redundant but explicit) */ | |
| #agent_form .chat-only-btn { | |
| display: none !important; | |
| } | |
| #agent_form .chat-panel-buttons { | |
| display: none !important; | |
| } | |
| #agent_form .patient-cards-group { | |
| display: none !important; | |
| } | |
| #agent_form .patient-cards-section { | |
| display: none !important; | |
| } | |
| #agent_form .patient-cards-row { | |
| display: none !important; | |
| } | |
| #agent_form .patient-cards-grid { | |
| display: none !important; | |
| } | |
| #agent_form .chat-only-content { | |
| display: none !important; | |
| } | |
| #agent_form .patient-card-btn { | |
| display: none !important; | |
| } | |
| #agent_form .patient-cards-container { | |
| display: none !important; | |
| } | |
| #agent_form .chat-only-section { | |
| display: none !important; | |
| } | |
| /* Show chat panel elements only in chat panel */ | |
| #agent_chat .chat-only-btn { | |
| display: inline-block !important; | |
| } | |
| #agent_chat .chat-panel-buttons { | |
| display: flex !important; | |
| } | |
| #agent_chat .patient-cards-group { | |
| display: block !important; | |
| } | |
| #agent_chat .patient-cards-section { | |
| display: block !important; | |
| } | |
| #agent_chat .patient-cards-row { | |
| display: flex !important; | |
| } | |
| #agent_chat .patient-cards-grid { | |
| display: grid !important; | |
| } | |
| #agent_chat .chat-only-content { | |
| display: block !important; | |
| } | |
| #agent_chat .patient-card-btn { | |
| display: block !important; | |
| } | |
| #agent_chat .patient-cards-container { | |
| display: flex !important; | |
| } | |
| #agent_chat .chat-only-section { | |
| display: block !important; | |
| } | |
| """ | |
| # Helper functions for chat control state management | |
| def show_initial_instruction_state(): | |
| """Return initial state with instruction message and disabled controls""" | |
| instruction_chat = [{"role": "assistant", "content": "📋 **Agent generated successfully!**\n\nTo start testing your agent:\n1. Select your agent from the dropdown menu above\n2. Click '💬 Chat with Selected Agent'\n3. Then you can type your questions in the chat box\n\n*Please select an agent from the dropdown to begin testing.*"}] | |
| return ( | |
| instruction_chat, # builder_chatbot | |
| gr.update(value="", interactive=False, placeholder="Please select an agent to start chatting..."), # chat_input | |
| gr.update(interactive=False), # builder_send_button | |
| gr.update(interactive=False), # reset_button | |
| "", # invocation_log | |
| [] # active_children | |
| ) | |
| def enable_chat_controls_with_agent(agent_name): | |
| """Enable chat controls and show proper agent greeting when agent is selected""" | |
| agent_json = agents_config.get(agent_name, "") | |
| if agent_json: | |
| # Get agent data for greeting | |
| agent_data = json.loads(agent_json) | |
| agent_display_name = agent_data.get("agent_name", agent_name) | |
| # Create greeting message | |
| greeting_chat = [{"role": "assistant", "content": f"👋 Hello! I'm {agent_display_name}. How can I assist you today?"}] | |
| return ( | |
| greeting_chat, # builder_chatbot with agent greeting | |
| gr.update(value="", interactive=True, placeholder="Type your question here…"), # chat_input enabled | |
| gr.update(interactive=True), # builder_send_button enabled | |
| gr.update(interactive=True), # reset_button enabled | |
| "", # clear invocation_log | |
| [], # clear active_children | |
| agent_json # agent_output | |
| ) | |
| else: | |
| # No valid agent selected, return disabled state | |
| return show_initial_instruction_state() + ("",) # Add empty agent_output | |
| # Update instruction when agent is selected from dropdown | |
| def update_instruction_on_dropdown_change(agent_name): | |
| """Update instruction message when agent is selected from dropdown""" | |
| if agent_name: | |
| instruction_msg = f"📋 **Agent '{agent_name}' selected!**\n\nTo start testing this agent:\n• Click '💬 Chat with Selected Agent' button above\n• Then you can type your questions in the chat box\n\n*Click the chat button to begin testing.*" | |
| else: | |
| instruction_msg = "📋 **Welcome to the Agent Builder!**\n\nTo start testing your agents:\n1. Generate an agent using the form on the left\n2. Select your agent from the dropdown menu above\n3. Click '💬 Chat with Selected Agent'\n4. Then you can type your questions in the chat box\n\n*Please create and select an agent to begin testing.*" | |
| return [{"role": "assistant", "content": instruction_msg}] | |
| # 1) HEADER & LANDING CARD | |
| with gr.Group(elem_id="landing_card", elem_classes="landing-card", visible=True) as landing_panel: | |
| gr.Markdown("<div class='landing-title'>🦠 Infectious Diseases Agent Builder</div>", elem_id=None, elem_classes=None) | |
| gr.Markdown("<div class='landing-subtitle'>Build your own ID-focused chat agent in 5 easy steps — no coding required.</div>", elem_id=None, elem_classes=None) | |
| gr.HTML(""" | |
| <div class='steps-box'> | |
| <ul> | |
| <li><b>Step 1:</b> Pick an agent template or start from scratch</li> | |
| <li><b>Step 2:</b> Choose your agent’s focus (Stewardship, Research, Clinical…)</li> | |
| <li><b>Step 3:</b> Select from prebuilt skills (PubMed search, guideline summaries…)</li> | |
| <li><b>Step 4:</b> (Optional) Upload your own documents or trusted URLs</li> | |
| <li><b>Step 5:</b> Generate & start chatting live</li> | |
| </ul> | |
| </div> | |
| """) | |
| start_button = gr.Button( | |
| "🚀 Get Started", | |
| elem_id="start_button", | |
| elem_classes="animated-button" | |
| ) | |
| gr.HTML("<div style='height: 32px;'></div>") | |
| # Only the simple GPT-3.5 Chatbot (no active agents or builder UI) | |
| gr.Markdown("### 💬 Try A Simple Chatbot Before You Build Your ID Agents") | |
| simple_chatbot = gr.Chatbot(label="GPT-3.5 Chat", type="messages") | |
| simple_input = gr.Textbox( | |
| placeholder="Ask anything…", | |
| show_label=False, | |
| lines=2, | |
| max_lines=4, | |
| ) | |
| simple_send = gr.Button("Send") | |
| simple_reset = gr.Button("Reset") | |
| # 2) AGENT FORM (HIDDEN UNTIL CLICK) | |
| prefill_flag = gr.State(False) | |
| with gr.Group(elem_id="agent_form", visible=False) as agent_form: | |
| # Move Back to Home button to the very top | |
| back_button = gr.Button("🔙 Back to Home", elem_id="back_button") | |
| # Steps box at the top of the builder panel for user guidance | |
| gr.HTML(""" | |
| <div class='steps-box' style='margin-top: 0; margin-bottom: 1.5em;'> | |
| <ul> | |
| <li><b>Step 1:</b> Pick a prefilled agent template or start from scratch</li> | |
| <li><b>Step 2:</b> Choose your agent’s focus (Stewardship, Research, Clinical…), name, and mission.</li> | |
| <li><b>Step 3:</b> Select from prebuilt skills.</li> | |
| <li><b>Step 4:</b> (Optional) Upload your own documents or trusted URLs</li> | |
| <li><b>Step 5:</b> Generate & test & iterate & start chatting live with your deployed agents</li> | |
| </ul> | |
| </div> | |
| """) | |
| gr.Markdown("### 🎛️ Infectious Diseases Agent Builder") | |
| with gr.Row(): | |
| # Left panel | |
| with gr.Column(scale=3, elem_classes="left-panel"): | |
| prefilled = gr.Dropdown(choices=["None"] + list(prefilled_agents.keys()), label="Start with a prefilled agent?") | |
| with gr.Accordion("🛠️ Basic Settings", open=True): | |
| agent_type = gr.Radio( | |
| choices=[ | |
| "🛡️ Antimicrobial Stewardship", | |
| "🦠 Infection Prevention and Control", | |
| "🔬 Research Assistant", | |
| "🏥 Clinical Assistant", | |
| "📚 Education Assistant", | |
| "🎼 Orchestrator", | |
| ], | |
| label="Select Agent Type", | |
| elem_id="select_agent_type_radio" | |
| ) | |
| agent_name = gr.Textbox(label="Agent Name", placeholder="e.g., SmartSteward", max_lines=1) | |
| agent_mission = gr.Textbox(label="Agent Mission", placeholder="Describe what your agent should do…", lines=4) | |
| skills = gr.CheckboxGroup(choices=[], label="Select Skills") | |
| with gr.Accordion("⚙️ Advanced Options", open=False): | |
| link1, link2, link3, link4 = [ | |
| gr.Textbox(label=f"Trusted Source Link {i} (optional)") | |
| for i in range(1,5) | |
| ] | |
| web_access_toggle = gr.Checkbox(label="Allow Internet Search 🌐", value=True, interactive=True) | |
| allow_fallback_toggle = gr.Checkbox(label="Allow Fallback to LLM General Knowledge 🤖", value=True) | |
| challenger_toggle = gr.Checkbox(label="Enable Adversarial AI Validation (Challenger)", value=False, info="If enabled, agent replies will be critiqued by an adversarial LLM before being shown to the user.") | |
| # --- Auto-toggle logic for web_access_toggle --- | |
| def update_web_access_toggle(l1, l2, l3, l4): | |
| links = [l1, l2, l3, l4] | |
| any_links = any(l.strip() for l in links if l) | |
| if any_links: | |
| # If any trusted link is present, force checked and disable | |
| return gr.update(value=True, interactive=False) | |
| else: | |
| # If all empty, allow user to toggle | |
| return gr.update(interactive=True) | |
| # Wire up the logic: any change to link1-4 updates web_access_toggle | |
| for link in [link1, link2, link3, link4]: | |
| link.change( | |
| fn=update_web_access_toggle, | |
| inputs=[link1, link2, link3, link4], | |
| outputs=[web_access_toggle] | |
| ) | |
| generate_button = gr.Button("✨ Generate Agent Config", elem_classes="generate-btn") | |
| with gr.Accordion("📦 Generated Agent Config", open=False): | |
| agent_loader = gr.Markdown("") | |
| agent_output = gr.Code(label="Configuration (JSON)", language="json") | |
| download_button = gr.DownloadButton(label="Download Config") | |
| # Move Upload Knowledge Files section below agent config | |
| with gr.Accordion("📚 Upload Knowledge Files (Global)", open=False): | |
| uploaded_files = gr.File(label="Upload Knowledge Files", file_count="multiple") | |
| upload_alert = gr.Markdown("", visible=False) | |
| # Right panel | |
| with gr.Column(scale=9, elem_classes="right-panel"): | |
| builder_active_agents = gr.Markdown("### 🧠 Active Agents\n_(None yet)_") | |
| # dropdown + action buttons inline | |
| with gr.Row(elem_classes="agent-controls"): | |
| with gr.Column(scale=3, elem_classes="agent-dropdown-column"): | |
| agent_remove_dropdown = gr.Dropdown( | |
| label="Select an agent", | |
| choices=[], | |
| elem_classes="agent-dropdown" | |
| ) | |
| with gr.Column(scale=1, elem_classes="agent-button-group"): | |
| chat_agent_button = gr.Button( | |
| "💬 Chat with Selected Agent", | |
| elem_classes="action-btn" | |
| ) | |
| edit_agent_button = gr.Button( | |
| "🛠 Edit Selected Agent", | |
| elem_classes="action-btn" | |
| ) | |
| remove_agent_button = gr.Button( | |
| "❌ Remove Selected Agent", | |
| elem_classes="action-btn" | |
| ) | |
| show_debug = gr.Checkbox(label="🔎 Show tool reasoning", value=False) | |
| # Only one chatbot in builder panel | |
| builder_chatbot = gr.Chatbot( | |
| label="💬 Live Conversation with Your ID Agent", | |
| type="messages", | |
| value=[{"role": "assistant", "content": "📋 **Welcome to the Agent Builder!**\n\nTo start testing your agents:\n1. Generate an agent using the form on the left\n2. Select your agent from the dropdown menu above\n3. Click '💬 Chat with Selected Agent'\n4. Then you can type your questions in the chat box\n\n*Please create and select an agent to begin testing.*"}] | |
| ) | |
| chat_input = gr.Textbox( | |
| placeholder="Please select an agent to start chatting...", | |
| show_label=False, | |
| lines=3, | |
| max_lines=5, | |
| interactive=False | |
| ) | |
| active_children = gr.State([]) # will hold a list of JSON-configs | |
| # --- Builder panel clinical variable fields (hidden, but needed for wiring) --- | |
| builder_deescalation_culture = gr.Textbox(visible=False) | |
| builder_deescalation_meds = gr.Textbox(visible=False) | |
| builder_stewardship_site = gr.Textbox(visible=False) | |
| builder_stewardship_biofilm = gr.Textbox(visible=False) | |
| builder_stewardship_response = gr.Textbox(visible=False) | |
| builder_stewardship_crcl = gr.Textbox(visible=False) | |
| builder_stewardship_severity = gr.Textbox(visible=False) | |
| builder_stewardship_allergies = gr.Textbox(visible=False) | |
| builder_empiric_age = gr.Textbox(visible=False) | |
| builder_empiric_allergies = gr.Textbox(visible=False) | |
| builder_empiric_labs = gr.Textbox(visible=False) | |
| builder_empiric_culture = gr.Textbox(visible=False) | |
| builder_empiric_meds = gr.Textbox(visible=False) | |
| builder_empiric_site = gr.Textbox(visible=False) | |
| builder_empiric_biofilm = gr.Textbox(visible=False) | |
| builder_empiric_response = gr.Textbox(visible=False) | |
| builder_empiric_crcl = gr.Textbox(visible=False) | |
| builder_empiric_severity = gr.Textbox(visible=False) | |
| # Add the builder send button under the chatbox | |
| builder_send_button = gr.Button("Send", elem_id="builder_send_button", interactive=False) | |
| reset_button = gr.Button("🔄 Reset Chat", interactive=False) | |
| invocation_log = gr.Markdown( | |
| value="", | |
| label="🔍 Tool Invocation Log", | |
| visible=True | |
| ) | |
| gr.Markdown("---\nBuilt with ❤️ for ID Week 2025 — Empowering Infectious Diseases Innovation") | |
| # Move the Chat With the Agents You Deployed button to the bottom, below the disclaimer | |
| to_chat_button = gr.Button("🗨️ Chat With the Agents You Deployed", elem_id="to_chat_button") | |
| # --- Builder panel send button logic --- | |
| def builderpanel_handle_with_dynamic_vars( | |
| agent_name, user_text, histories, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity | |
| ): | |
| agent_json = agents_config.get(agent_name) | |
| if agent_json: | |
| agent_data = json.loads(agent_json) | |
| skills = agent_data.get("skills", []) | |
| history = histories.get(agent_name, []) | |
| # --- Trusted links wiring --- | |
| trusted_links = [] | |
| for k in ["trusted_links", "trusted_links_1", "trusted_links_2", "trusted_links_3", "trusted_links_4"]: | |
| # Support both list and individual keys | |
| if isinstance(agent_data.get(k), list): | |
| trusted_links.extend([l for l in agent_data[k] if l]) | |
| elif isinstance(agent_data.get(k), str) and agent_data[k]: | |
| trusted_links.append(agent_data[k]) | |
| # Also check for link1-link4 keys (legacy) | |
| for k in ["link1", "link2", "link3", "link4"]: | |
| if agent_data.get(k): | |
| trusted_links.append(agent_data[k]) | |
| trusted_links = [l for l in trusted_links if l] | |
| # Do not prepend trusted links to every user message; just keep them available for tools | |
| # if trusted_links: | |
| # links_str = ", ".join(trusted_links) | |
| # user_text = f"Trusted sources for this agent: {links_str}\n\n" + user_text | |
| # --- End trusted links wiring --- | |
| # Deescalation tool | |
| if "recommend_deescalation" in skills: | |
| var_names = ["culture", "meds", "site_of_infection", "risk_of_biofilm", "current_response", "creatinine_clearance", "severity_of_infection", "known_allergies"] | |
| user_vars = { | |
| "culture": deescalation_culture, | |
| "meds": deescalation_meds, | |
| "site_of_infection": stewardship_site, | |
| "risk_of_biofilm": stewardship_biofilm, | |
| "current_response": stewardship_response, | |
| "creatinine_clearance": stewardship_crcl, | |
| "severity_of_infection": stewardship_severity, | |
| "known_allergies": stewardship_allergies | |
| } | |
| extracted = extract_clinical_variables_from_history(history, var_names) | |
| for k in var_names: | |
| if not user_vars[k]: | |
| user_vars[k] = extracted.get(k) or "" | |
| # Only prepend if at least one field is non-empty | |
| if any(user_vars[k] for k in var_names): | |
| user_text = f"[DEESCALATION_TOOL_INPUT] {json.dumps(user_vars)}\n" + user_text | |
| elif "alert_prolonged_antibiotic_use" in skills: | |
| var_names = ["site_of_infection", "risk_of_biofilm", "current_response", "creatinine_clearance", "severity_of_infection", "known_allergies"] | |
| user_vars = { | |
| "site_of_infection": stewardship_site, | |
| "risk_of_biofilm": stewardship_biofilm, | |
| "current_response": stewardship_response, | |
| "creatinine_clearance": stewardship_crcl, | |
| "severity_of_infection": stewardship_severity, | |
| "known_allergies": stewardship_allergies | |
| } | |
| extracted = extract_clinical_variables_from_history(history, var_names) | |
| for k in var_names: | |
| if not user_vars[k]: | |
| user_vars[k] = extracted.get(k) or "" | |
| if any(user_vars[k] for k in var_names): | |
| user_text = f"[ALERT_PROLONGED_ABX_INPUT] {json.dumps(user_vars)}\n" + user_text | |
| elif "recommend_empiric_therapy" in skills: | |
| # Remove 'known_allergies' as a separate required field (it's covered by 'allergies') | |
| var_names = [ | |
| "age", "allergies", "labs", "culture", "meds", "site_of_infection", | |
| "risk_of_biofilm", "current_response", "creatinine_clearance", "severity_of_infection" | |
| ] | |
| user_vars = { | |
| "age": empiric_age, | |
| "allergies": empiric_allergies, | |
| "labs": empiric_labs, | |
| "culture": empiric_culture, | |
| "meds": empiric_meds, | |
| "site_of_infection": empiric_site, | |
| "risk_of_biofilm": empiric_biofilm, | |
| "current_response": empiric_response, | |
| "creatinine_clearance": empiric_crcl, | |
| "severity_of_infection": empiric_severity | |
| } | |
| extracted = extract_clinical_variables_from_history(history, var_names) | |
| for k in var_names: | |
| if not user_vars[k]: | |
| user_vars[k] = extracted.get(k) or "" | |
| # If any required field is missing, prompt = ... | |
| missing = [k.replace('_', ' ').capitalize() for k in var_names if not user_vars[k].strip()] | |
| if missing: | |
| prompt = f"Please provide the following required information for empiric therapy: {', '.join(missing)}." | |
| # Show this as an assistant message and do not call the tool | |
| history.append({"role": "assistant", "content": prompt}) | |
| return history, histories, "" | |
| # All required fields present, prepend tool input | |
| user_text = f"[EMPIRIC_THERAPY_INPUT] {json.dumps(user_vars)}\n" + user_text | |
| # Use the same chat handling logic, but ensure the builder_chatbot is updated and history is preserved | |
| # Call chatpanel_handle, but get extra challenger info from simulate_agent_response_stream | |
| import asyncio | |
| from core.agents.chat_orchestrator import simulate_agent_response_stream | |
| agent_json_val = agents_config.get(agent_name) | |
| history_val = histories.get(agent_name, []) | |
| result = None | |
| async def run_stream(): | |
| gen = simulate_agent_response_stream( | |
| agent_json=agent_json_val, | |
| history=history_val, | |
| user_input=user_text, | |
| debug_flag=False, | |
| active_children=[] | |
| ) | |
| last_result = None | |
| async for result in gen: | |
| last_result = result | |
| return last_result | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| try: | |
| result = loop.run_until_complete(run_stream()) | |
| if result is not None and len(result) == 5: | |
| final_history, _, invocation_log, _, challenger_info = result | |
| else: | |
| final_history, updated_histories, cleared_input, invocation_log = chatpanel_handle(agent_name, user_text, histories) | |
| challenger_info = None | |
| return final_history, updated_histories, cleared_input, invocation_log, "" | |
| except Exception: | |
| # fallback to old behavior if error | |
| final_history, updated_histories, cleared_input, invocation_log = chatpanel_handle(agent_name, user_text, histories) | |
| challenger_info = None | |
| return final_history, updated_histories, cleared_input, invocation_log, "" | |
| # Update histories | |
| updated_histories = histories.copy() | |
| updated_histories[agent_name] = final_history | |
| # Prepare challenger markdown (debug log for builder panel) | |
| challenger_md = "" | |
| if isinstance(challenger_info, dict): | |
| orig = challenger_info.get("original_reply", "") | |
| crit = challenger_info.get("challenger_critique", "") | |
| final = challenger_info.get("final_reply", "") | |
| # Ensure critique is never None or empty in the UI | |
| if not crit or str(crit).strip().lower() == "none": | |
| crit = "OK" | |
| # Only show the final (challenger-approved) answer in the chatbox | |
| if final and final_history and isinstance(final_history, list): | |
| final_history[-1]["content"] = final | |
| # In the builder panel log, show the critique, but in the chatbox, only show the final answer | |
| # If the challenger changed the answer, only show the suggested revision as the final answer, not the critique text | |
| if final != orig and crit != "OK": | |
| challenger_md = f"**Original Agent Answer:**\n\n{orig}\n\n**Challenger Critique:**\n\n{crit}\n\n**Final Answer Shown to User:**\n\n{final}" | |
| else: | |
| challenger_md = f"**Original Agent Answer:**\n\n{orig}\n\n**Final Answer Shown to User:**\n\n{final}" | |
| return final_history, updated_histories, "", invocation_log, challenger_md | |
| # Add a Markdown for challenger debug info under the invocation log, only in the builder panel | |
| challenger_debug_md = gr.Markdown("", visible=True) | |
| # Place the Markdown visually under the invocation log in the builder panel only | |
| builder_send_button.click( | |
| fn=builderpanel_handle_with_dynamic_vars, | |
| inputs=[ | |
| agent_remove_dropdown, chat_input, builder_chat_histories, | |
| builder_deescalation_culture, builder_deescalation_meds, | |
| builder_stewardship_site, builder_stewardship_biofilm, builder_stewardship_response, builder_stewardship_crcl, builder_stewardship_severity, builder_stewardship_allergies, | |
| builder_empiric_age, builder_empiric_allergies, builder_empiric_labs, builder_empiric_culture, builder_empiric_meds, builder_empiric_site, builder_empiric_biofilm, builder_empiric_response, builder_empiric_crcl, builder_empiric_severity | |
| ], | |
| outputs=[builder_chatbot, builder_chat_histories, chat_input, invocation_log, challenger_debug_md] | |
| ) | |
| # Hide the challenger debug markdown in the deployed chat and simple chat panels | |
| # (No code needed, as those panels do not use this Markdown block) | |
| # (3) Agent-Chat panel, hidden by default | |
| with gr.Group(elem_id="agent_chat", visible=False) as chat_panel: | |
| gr.Markdown("### 🗨️ Chat with Your ID Agents") | |
| chat_active_agents = gr.Markdown("### 🧠 Active Agents\n_(None yet)_") | |
| # Dropdown to pick which agent to chat with | |
| agent_picker = gr.Dropdown( | |
| label="Select Agent", | |
| choices=list(agents_config.keys()), | |
| interactive=True | |
| ) | |
| # The ChatGPT-style history box | |
| chat_view = gr.Chatbot(label="Conversation", type="messages") | |
| # --- Stewardship Tool Clinical Variables Section (Deescalation & Alert Prolonged Abx Use) --- | |
| with gr.Accordion("Stewardship Clinical Variables", open=False, visible=False) as stewardship_vars_section: | |
| deescalation_culture = gr.Textbox(label="Culture & Sensitivity Results", lines=2, visible=False) | |
| deescalation_meds = gr.Textbox(label="Current Antibiotic Regimen", lines=2, visible=False) | |
| stewardship_site = gr.Textbox(label="Site of Infection", lines=1) | |
| stewardship_biofilm = gr.Textbox(label="Risk or Presence of Biofilm", lines=1) | |
| stewardship_response = gr.Textbox(label="Current Response to Antibiotics", lines=1) | |
| stewardship_crcl = gr.Textbox(label="Creatinine Clearance", lines=1) | |
| stewardship_severity = gr.Textbox(label="Severity of Infection", lines=1) | |
| stewardship_allergies = gr.Textbox(label="Known Drug Allergies", lines=1) | |
| # --- Empiric Therapy Tool Clinical Variables Section --- | |
| with gr.Accordion("Empiric Therapy Clinical Variables", open=False, visible=False) as empiric_vars_section: | |
| empiric_age = gr.Textbox(label="Age", lines=1) | |
| empiric_allergies = gr.Textbox(label="Allergies", lines=1) | |
| empiric_labs = gr.Textbox(label="Recent Labs", lines=2) | |
| empiric_culture = gr.Textbox(label="Culture & Sensitivity Results", lines=2) | |
| empiric_meds = gr.Textbox(label="Current Antibiotic Regimen", lines=2) | |
| empiric_site = gr.Textbox(label="Site of Infection", lines=1) | |
| empiric_biofilm = gr.Textbox(label="Risk or Presence of Biofilm", lines=1) | |
| empiric_response = gr.Textbox(label="Current Response to Antibiotics", lines=1) | |
| empiric_crcl = gr.Textbox(label="Creatinine Clearance", lines=1) | |
| empiric_severity = gr.Textbox(label="Severity of Infection", lines=1) | |
| empiric_known_allergies = gr.Textbox(label="Known Drug Allergies", lines=1) | |
| # --- Infection Prevention and Control Clinical Variables Section --- | |
| with gr.Accordion("IPC Clinical Variables", open=False, visible=False) as ipc_vars_section: | |
| ipc_facility_name = gr.Textbox(label="Facility Name", lines=1) | |
| ipc_location = gr.Textbox(label="Location/Unit", lines=1) | |
| ipc_infection_type = gr.Textbox(label="Type of Infection (HAI, SSI, CLABSI, etc.)", lines=1) | |
| ipc_onset_date = gr.Textbox(label="Infection Onset Date", lines=1) | |
| ipc_device_days = gr.Textbox(label="Device Days (Central Line, Ventilator, etc.)", lines=1) | |
| ipc_pathogen = gr.Textbox(label="Pathogen Identified", lines=1) | |
| ipc_resistance_pattern = gr.Textbox(label="Resistance Pattern (MRSA, CRE, etc.)", lines=1) | |
| ipc_isolation_status = gr.Textbox(label="Current Isolation Precautions", lines=1) | |
| ipc_compliance_issues = gr.Textbox(label="Compliance/Breach Issues", lines=2) | |
| # --- Clinical Assistant Clinical Variables Section --- | |
| with gr.Accordion("Clinical Assessment Variables", open=False, visible=False) as clinical_vars_section: | |
| clinical_chief_complaint = gr.Textbox(label="Chief Complaint", lines=2) | |
| clinical_history_present = gr.Textbox(label="History of Present Illness", lines=3) | |
| clinical_past_medical = gr.Textbox(label="Past Medical History", lines=2) | |
| clinical_medications = gr.Textbox(label="Current Medications", lines=2) | |
| clinical_allergies = gr.Textbox(label="Allergies", lines=1) | |
| clinical_social_history = gr.Textbox(label="Social History (Travel, Exposures)", lines=2) | |
| clinical_vital_signs = gr.Textbox(label="Vital Signs", lines=1) | |
| clinical_physical_exam = gr.Textbox(label="Physical Examination Findings", lines=3) | |
| clinical_lab_results = gr.Textbox(label="Laboratory Results", lines=2) | |
| clinical_imaging = gr.Textbox(label="Imaging Results", lines=2) | |
| # --- Orchestrator Coordination Variables Section --- | |
| with gr.Accordion("Multi-Agent Coordination Variables", open=False, visible=False) as orchestrator_vars_section: | |
| # Stewardship Variables (8 fields) | |
| orchestrator_culture = gr.Textbox(label="Culture Results", lines=1) | |
| orchestrator_meds = gr.Textbox(label="Current Medications", lines=1) | |
| orchestrator_site = gr.Textbox(label="Site of Infection", lines=1) | |
| orchestrator_biofilm = gr.Textbox(label="Risk of Biofilm", lines=1) | |
| orchestrator_response = gr.Textbox(label="Current Response", lines=1) | |
| orchestrator_crcl = gr.Textbox(label="Creatinine Clearance", lines=1) | |
| orchestrator_severity = gr.Textbox(label="Severity of Infection", lines=1) | |
| orchestrator_allergies = gr.Textbox(label="Known Allergies", lines=1) | |
| # IPC Variables (9 fields) | |
| orchestrator_facility_name = gr.Textbox(label="Facility Name", lines=1) | |
| orchestrator_location = gr.Textbox(label="Location/Unit", lines=1) | |
| orchestrator_infection_type = gr.Textbox(label="Type of Infection (HAI, SSI, CLABSI, etc.)", lines=1) | |
| orchestrator_onset_date = gr.Textbox(label="Infection Onset Date", lines=1) | |
| orchestrator_device_days = gr.Textbox(label="Device Days (Central Line, Ventilator, etc.)", lines=1) | |
| orchestrator_pathogen = gr.Textbox(label="Pathogen Identified", lines=1) | |
| orchestrator_resistance_pattern = gr.Textbox(label="Resistance Pattern (MRSA, CRE, etc.)", lines=1) | |
| orchestrator_isolation_status = gr.Textbox(label="Current Isolation Precautions", lines=1) | |
| orchestrator_compliance_issues = gr.Textbox(label="Compliance/Breach Issues", lines=2) | |
| # Clinical Assistant Variables (10 fields) | |
| orchestrator_chief_complaint = gr.Textbox(label="Chief Complaint", lines=2) | |
| orchestrator_history_present = gr.Textbox(label="History of Present Illness", lines=3) | |
| orchestrator_past_medical = gr.Textbox(label="Past Medical History", lines=2) | |
| orchestrator_medications = gr.Textbox(label="Current Medications", lines=2) | |
| orchestrator_patient_allergies = gr.Textbox(label="Patient Allergies", lines=1) | |
| orchestrator_social_history = gr.Textbox(label="Social History (Travel, Exposures)", lines=2) | |
| orchestrator_vital_signs = gr.Textbox(label="Vital Signs", lines=1) | |
| orchestrator_physical_exam = gr.Textbox(label="Physical Examination Findings", lines=3) | |
| orchestrator_lab_results = gr.Textbox(label="Laboratory Results", lines=2) | |
| orchestrator_imaging = gr.Textbox(label="Imaging Results", lines=2) | |
| # Add chat input box to chat panel | |
| chat_panel_input = gr.Textbox( | |
| placeholder="Type your question here…", | |
| show_label=False, | |
| lines=3, | |
| max_lines=5 | |
| ) | |
| # Only show chat_send, chat_reset, and chat_back in the chat panel | |
| with gr.Row(elem_classes="chat-panel-buttons"): | |
| chat_send = gr.Button("Send", elem_classes="chat-only-btn") | |
| chat_reset = gr.Button("🔄 Reset Chat", elem_classes="chat-only-btn") | |
| chat_back = gr.Button("🔙 Back to Builder", elem_id="chat_back", elem_classes="chat-only-btn") | |
| # Patient cards section - positioned at bottom of chat panel | |
| with gr.Group(elem_classes="patient-cards-section"): | |
| gr.Markdown("### 🎯 Select Context-Aware Chat Scenario (Optional)", elem_classes="patient-cards-header") | |
| # First row - 3 cards | |
| with gr.Row(elem_classes="patient-cards-row"): | |
| # Patient Card 1: Stewardship Case | |
| with gr.Column(elem_classes="patient-card", scale=1): | |
| patient_card_1 = gr.Button( | |
| "📋 Patient A: ICU Sepsis\n\n🛡️ SmartSteward Case\n\n68F, ICU day 5, on vancomycin + piperacillin-tazobactam for sepsis. Blood cultures positive for MSSA. Patient improving, normal renal function.", | |
| elem_classes="patient-card-btn" | |
| ) | |
| # Patient Card 2: IPC Case | |
| with gr.Column(elem_classes="patient-card", scale=1): | |
| patient_card_2 = gr.Button( | |
| "🦠 Patient B: CLABSI Investigation\n\n🦠 InfectoGuard Case\n\n45M, ICU patient with central line x6 days. Developed fever, positive blood cultures for MRSA. Potential healthcare-associated infection.", | |
| elem_classes="patient-card-btn" | |
| ) | |
| # Patient Card 3: Research Case | |
| with gr.Column(elem_classes="patient-card", scale=1): | |
| patient_card_3 = gr.Button( | |
| "🔬 Research Query\n\n🔬 ResearchRanger Case\n\nLiterature search needed for novel carbapenem-resistant Enterobacterales treatment options and resistance mechanisms.", | |
| elem_classes="patient-card-btn" | |
| ) | |
| # Second row - 3 cards | |
| with gr.Row(elem_classes="patient-cards-row"): | |
| # Patient Card 4: Clinical Case | |
| with gr.Column(elem_classes="patient-card", scale=1): | |
| patient_card_4 = gr.Button( | |
| "🏥 Patient C: Complex Diagnosis\n\n🏥 ClinicoPilot Case\n\n32M with fever, rash, and joint pain after recent travel to Southeast Asia. Multiple differential diagnoses to consider.", | |
| elem_classes="patient-card-btn" | |
| ) | |
| # Patient Card 5: Education Case | |
| with gr.Column(elem_classes="patient-card", scale=1): | |
| patient_card_5 = gr.Button( | |
| "📚 Education Request\n\n📚 EduMedCoach Case\n\nMedical student requesting board exam questions and educational materials on antimicrobial resistance mechanisms.", | |
| elem_classes="patient-card-btn" | |
| ) | |
| # Patient Card 6: Orchestrator Case | |
| with gr.Column(elem_classes="patient-card", scale=1): | |
| patient_card_6 = gr.Button( | |
| "🎼 Complex Multi-Agent Case\n\n🎼 ID Maestro Case\n\n75M with multiple ID issues: MDRO pneumonia, C. diff colitis, and suspected endocarditis requiring comprehensive analysis.", | |
| elem_classes="patient-card-btn" | |
| ) | |
| # Store patient data in hidden state | |
| patient_data = gr.State({}) | |
| # Define patient case data | |
| patient_cases = { | |
| "patient_1": { | |
| "name": "Patient A", | |
| "age": "68", | |
| "summary": "68-year-old female in ICU, day 5 of admission for sepsis", | |
| "current_meds": "vancomycin 1g q12h, piperacillin-tazobactam 4.5g q6h", | |
| "culture_results": "Blood cultures (day 3): methicillin-sensitive Staphylococcus aureus (MSSA), sensitive to cefazolin, nafcillin, clindamycin", | |
| "site_of_infection": "bloodstream", | |
| "biofilm_risk": "central venous catheter present", | |
| "response": "clinically improving, fever resolved, WBC trending down", | |
| "creatinine_clearance": "75 mL/min (normal)", | |
| "severity": "severe sepsis, now stable", | |
| "allergies": "NKDA", | |
| "agent_focus": "🛡️ Antimicrobial Stewardship", | |
| "context": "This patient is a perfect candidate for antibiotic deescalation given the MSSA blood culture results and clinical improvement. Current broad-spectrum therapy can likely be narrowed." | |
| }, | |
| "patient_2": { | |
| "name": "Patient B", | |
| "age": "45", | |
| "summary": "45-year-old male, ICU patient with central line-associated bloodstream infection", | |
| "diagnosis": "Central line-associated bloodstream infection (CLABSI)", | |
| "central_line_days": "6 days", | |
| "culture_results": "Blood cultures positive for methicillin-resistant Staphylococcus aureus (MRSA)", | |
| "symptoms": "fever (38.8°C), chills, no other obvious source", | |
| "location": "Methodist Hospital, Dallas, Texas", | |
| "agent_focus": "🦠 Infection Prevention and Control", | |
| "context": "This case requires evaluation for NHSN CLABSI criteria, appropriate isolation precautions for MRSA, and reporting requirements for healthcare-associated infections in Texas." | |
| }, | |
| "patient_3": { | |
| "name": "Research Query", | |
| "topic": "Carbapenem-resistant Enterobacterales (CRE) treatment", | |
| "research_focus": "Novel treatment options for CRE infections", | |
| "specific_interests": "resistance mechanisms, combination therapies, newer antibiotics", | |
| "urgency": "clinical decision support needed", | |
| "agent_focus": "🔬 Research Assistant", | |
| "context": "Literature search and evidence synthesis needed for treatment of carbapenem-resistant Enterobacterales infections, including mechanism-based approaches and newest therapeutic options." | |
| }, | |
| "patient_4": { | |
| "name": "Patient C", | |
| "age": "32", | |
| "summary": "32-year-old male with fever, rash, and arthralgia after travel", | |
| "travel_history": "Recent travel to Southeast Asia (Thailand, Vietnam) 3 weeks ago", | |
| "symptoms": "fever (39.1°C), maculopapular rash on trunk and extremities, polyarthralgia", | |
| "duration": "symptoms for 5 days", | |
| "differential": "considering dengue fever, chikungunya, Zika virus, typhus, malaria", | |
| "agent_focus": "🏥 Clinical Assistant", | |
| "context": "Complex infectious disease case requiring systematic evaluation of travel-related illnesses and patient education about diagnostic workup and treatment options." | |
| }, | |
| "patient_5": { | |
| "name": "Education Request", | |
| "level": "Medical student, 3rd year", | |
| "topic": "Antimicrobial resistance mechanisms", | |
| "request": "Board exam questions and educational materials", | |
| "focus_areas": "beta-lactamase types, carbapenemases, ESBL, AmpC", | |
| "format_needed": "multiple choice questions, flashcards, presentation slides", | |
| "agent_focus": "📚 Education Assistant", | |
| "context": "Educational content creation for antimicrobial resistance mechanisms, suitable for medical student board exam preparation with varying difficulty levels." | |
| }, | |
| "patient_6": { | |
| "name": "Patient D", | |
| "age": "75", | |
| "summary": "75-year-old male with multiple infectious complications", | |
| "problem_1": "Ventilator-associated pneumonia with XDR Pseudomonas aeruginosa", | |
| "problem_2": "Clostridioides difficile colitis (severe, recurrent)", | |
| "problem_3": "Suspected infective endocarditis (blood cultures pending)", | |
| "comorbidities": "diabetes, chronic kidney disease (CrCl 30 mL/min), heart failure", | |
| "current_status": "ICU day 12, on multiple antibiotics, clinically complex", | |
| "agent_focus": "🎼 Orchestrator", | |
| "context": "Complex multi-system infectious disease case requiring coordination between stewardship, infection control, and clinical decision-making across multiple agents and specialties." | |
| } | |
| } | |
| # Patient card click handlers | |
| def load_patient_1(): | |
| case = patient_cases["patient_1"] | |
| context_msg = f"""**Patient Case Loaded: {case['name']}** | |
| **Clinical Summary:** {case['summary']} | |
| - **Age:** {case['age']} years old | |
| - **Current Antibiotics:** {case['current_meds']} | |
| - **Culture Results:** {case['culture_results']} | |
| - **Site of Infection:** {case['site_of_infection']} | |
| - **Current Response:** {case['response']} | |
| - **Renal Function:** {case['creatinine_clearance']} | |
| - **Allergies:** {case['allergies']} | |
| **Agent Focus:** {case['agent_focus']} | |
| *How can I help with this stewardship case?*""" | |
| # Auto-populate stewardship clinical variables | |
| return ( | |
| [{"role": "assistant", "content": context_msg}], | |
| case, | |
| case.get('culture_results', ''), # deescalation_culture | |
| case.get('current_meds', ''), # deescalation_meds | |
| case.get('site_of_infection', ''), # stewardship_site | |
| case.get('biofilm_risk', ''), # stewardship_biofilm | |
| case.get('response', ''), # stewardship_response | |
| case.get('creatinine_clearance', ''), # stewardship_crcl | |
| case.get('severity', ''), # stewardship_severity | |
| case.get('allergies', ''), # stewardship_allergies | |
| case.get('age', ''), # empiric_age | |
| case.get('allergies', ''), # empiric_allergies | |
| '', # empiric_labs (not in patient data) | |
| case.get('culture_results', ''), # empiric_culture | |
| case.get('current_meds', ''), # empiric_meds | |
| case.get('site_of_infection', ''), # empiric_site | |
| case.get('biofilm_risk', ''), # empiric_biofilm | |
| case.get('response', ''), # empiric_response | |
| case.get('creatinine_clearance', ''), # empiric_crcl | |
| case.get('severity', ''), # empiric_severity | |
| '', '', '', '', '', '', '', '', '', # ipc fields (9 fields) | |
| '', '', '', '', '', '', '', '', '', '', # clinical fields (10 fields) | |
| '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' # orchestrator fields (27 fields) | |
| ) | |
| def load_patient_2(): | |
| case = patient_cases["patient_2"] | |
| context_msg = f"""**Patient Case Loaded: {case['name']}** | |
| **Clinical Summary:** {case['summary']} | |
| - **Age:** {case['age']} years old | |
| - **Diagnosis:** {case['diagnosis']} | |
| - **Central Line Duration:** {case['central_line_days']} | |
| - **Culture Results:** {case['culture_results']} | |
| - **Symptoms:** {case['symptoms']} | |
| - **Location:** {case['location']} | |
| **Agent Focus:** {case['agent_focus']} | |
| *How can I help with this infection prevention case?*""" | |
| # Auto-populate IPC clinical variables for Patient B | |
| return ( | |
| [{"role": "assistant", "content": context_msg}], | |
| case, | |
| '', '', '', '', '', '', '', '', # stewardship fields (8 fields) | |
| '', '', '', '', '', '', '', '', '', '', # empiric fields (10 fields) | |
| '', # ipc_facility_name (blank for user input) | |
| '', # ipc_location (blank for user input) | |
| case.get('diagnosis', ''), # ipc_infection_type | |
| 'admission + ' + case.get('central_line_days', ''), # ipc_onset_date | |
| case.get('central_line_days', ''), # ipc_device_days | |
| 'MRSA' if 'MRSA' in case.get('culture_results', '') else case.get('culture_results', ''), # ipc_pathogen | |
| 'MRSA' if 'MRSA' in case.get('culture_results', '') else 'pending resistance testing', # ipc_resistance_pattern | |
| 'Contact precautions for MRSA', # ipc_isolation_status | |
| 'Review central line maintenance', # ipc_compliance_issues | |
| '', '', '', '', '', '', '', '', '', '', # clinical fields (10 fields) | |
| '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' # orchestrator fields (27 fields) | |
| ) | |
| def load_patient_3(): | |
| case = patient_cases["patient_3"] | |
| context_msg = f"""**Research Query Loaded: {case['name']}** | |
| **Research Focus:** {case['research_focus']} | |
| - **Topic:** {case['topic']} | |
| - **Specific Interests:** {case['specific_interests']} | |
| - **Urgency:** {case['urgency']} | |
| **Agent Focus:** {case['agent_focus']} | |
| *How can I help with your research needs?*""" | |
| # Return empty clinical variables for research case | |
| return ( | |
| [{"role": "assistant", "content": context_msg}], | |
| case, | |
| '', '', '', '', '', '', '', '', # stewardship fields (8 fields) | |
| '', '', '', '', '', '', '', '', '', '', # empiric fields (10 fields) | |
| '', '', '', '', '', '', '', '', '', # ipc fields (9 fields) | |
| '', '', '', '', '', '', '', '', '', '', # clinical fields (10 fields) | |
| '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' # orchestrator fields (27 fields) | |
| ) | |
| def load_patient_4(): | |
| case = patient_cases["patient_4"] | |
| context_msg = f"""**Patient Case Loaded: {case['name']}** | |
| **Clinical Summary:** {case['summary']} | |
| - **Age:** {case['age']} years old | |
| - **Travel History:** {case['travel_history']} | |
| - **Symptoms:** {case['symptoms']} | |
| - **Duration:** {case['duration']} | |
| - **Differential Diagnosis:** {case['differential']} | |
| **Agent Focus:** {case['agent_focus']} | |
| *How can I help with this clinical case?*""" | |
| # Return clinical assistant-focused variables for Patient E case | |
| return ( | |
| [{"role": "assistant", "content": context_msg}], | |
| case, | |
| '', '', '', '', '', '', '', '', # stewardship fields (8 fields) | |
| '', '', '', '', '', '', '', '', '', '', # empiric fields (10 fields) | |
| '', '', '', '', '', '', '', '', '', # ipc fields (9 fields) | |
| case['symptoms'], # chief_complaint | |
| f"Patient with travel history to {case['travel_history']} presenting with {case['symptoms']} for {case['duration']}", # history_present | |
| 'Travel medicine history as noted', # past_medical_history | |
| '', # current_medications | |
| '', # allergies | |
| f"Recent travel to {case['travel_history']}", # social_history | |
| '', # vital_signs | |
| '', # physical_exam | |
| '', # lab_results | |
| '', # imaging_results | |
| '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' # orchestrator fields (27 fields) | |
| ) | |
| def load_patient_5(): | |
| case = patient_cases["patient_5"] | |
| context_msg = f"""**Education Request Loaded: {case['name']}** | |
| **Educational Details:** | |
| - **Student Level:** {case['level']} | |
| - **Topic:** {case['topic']} | |
| - **Request:** {case['request']} | |
| - **Focus Areas:** {case['focus_areas']} | |
| - **Formats Needed:** {case['format_needed']} | |
| **Agent Focus:** {case['agent_focus']} | |
| *How can I help with your educational materials?*""" | |
| # Return empty clinical variables for education case | |
| return ( | |
| [{"role": "assistant", "content": context_msg}], | |
| case, | |
| '', '', '', '', '', '', '', '', # stewardship fields (8 fields) | |
| '', '', '', '', '', '', '', '', '', '', # empiric fields (10 fields) | |
| '', '', '', '', '', '', '', '', '', # ipc fields (9 fields) | |
| '', '', '', '', '', '', '', '', '', '', # clinical fields (10 fields) | |
| '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' # orchestrator fields (27 fields) | |
| ) | |
| def load_patient_6(): | |
| case = patient_cases["patient_6"] | |
| context_msg = f"""**Complex Case Loaded: {case['name']}** | |
| **Clinical Summary:** {case['summary']} | |
| - **Age:** {case['age']} years old | |
| - **Problem 1:** {case['problem_1']} | |
| - **Problem 2:** {case['problem_2']} | |
| - **Problem 3:** {case['problem_3']} | |
| - **Comorbidities:** {case['comorbidities']} | |
| - **Current Status:** {case['current_status']} | |
| **Agent Focus:** {case['agent_focus']} | |
| *This complex case requires multi-agent coordination. How can I help orchestrate the care?*""" | |
| # Auto-populate clinical variables for complex orchestrator case | |
| return ( | |
| [{"role": "assistant", "content": context_msg}], | |
| case, | |
| '', # deescalation_culture (no specific culture data) | |
| 'multiple antibiotics', # deescalation_meds (from current_status) | |
| 'multiple sites (respiratory, GI)', # stewardship_site (inferred from problems) | |
| 'biofilm risk from devices', # stewardship_biofilm (inferred) | |
| 'complex, multiple infections', # stewardship_response | |
| case.get('comorbidities', '').split(',')[1].strip() if 'CrCl' in case.get('comorbidities', '') else '30 mL/min', # stewardship_crcl | |
| 'severe, multiple complications', # stewardship_severity | |
| 'review allergies needed', # stewardship_allergies | |
| case.get('age', ''), # empiric_age | |
| 'review allergies needed', # empiric_allergies | |
| '', # empiric_labs | |
| '', # empiric_culture | |
| 'multiple antibiotics', # empiric_meds | |
| 'multiple sites (respiratory, GI)', # empiric_site | |
| 'biofilm risk from devices', # empiric_biofilm | |
| 'complex, multiple infections', # empiric_response | |
| '30 mL/min', # empiric_crcl | |
| 'severe, multiple complications', # empiric_severity | |
| '', '', '', '', '', '', '', '', '', # ipc fields (9 fields) | |
| 'Multiple acute medical problems', # chief_complaint | |
| f"Complex patient with {case['problem_1']}, {case['problem_2']}, and {case['problem_3']}", # history_present | |
| case['comorbidities'], # past_medical_history | |
| 'Multiple antibiotics, supportive care', # current_medications | |
| 'Review needed', # allergies | |
| 'ICU setting, complex care', # social_history | |
| 'Multiple abnormalities expected', # vital_signs | |
| 'Complex findings across systems', # physical_exam | |
| 'Multiple abnormal values', # lab_results | |
| 'Multiple studies indicated', # imaging_results | |
| # Auto-populate orchestrator variables from case data | |
| '', # orchestrator_culture | |
| 'multiple antibiotics', # orchestrator_meds | |
| 'multiple sites (respiratory, GI)', # orchestrator_site | |
| 'biofilm risk from devices', # orchestrator_biofilm | |
| 'complex, multiple infections', # orchestrator_response | |
| '30 mL/min', # orchestrator_crcl | |
| 'severe, multiple complications', # orchestrator_severity | |
| 'review allergies needed', # orchestrator_allergies | |
| '', # orchestrator_facility_name | |
| 'ICU', # orchestrator_location | |
| 'multiple HAIs', # orchestrator_infection_type | |
| 'admission', # orchestrator_onset_date | |
| 'multiple devices', # orchestrator_device_days | |
| 'multiple pathogens', # orchestrator_pathogen | |
| 'review resistance patterns', # orchestrator_resistance_pattern | |
| 'contact precautions', # orchestrator_isolation_status | |
| 'complex care coordination', # orchestrator_compliance_issues | |
| 'Multiple acute medical problems', # orchestrator_chief_complaint | |
| f"Complex patient with {case['problem_1']}, {case['problem_2']}, and {case['problem_3']}", # orchestrator_history_present | |
| case['comorbidities'], # orchestrator_past_medical | |
| 'Multiple antibiotics, supportive care', # orchestrator_medications | |
| 'Review needed', # orchestrator_patient_allergies | |
| 'ICU setting, complex care', # orchestrator_social_history | |
| 'Multiple abnormalities expected', # orchestrator_vital_signs | |
| 'Complex findings across systems', # orchestrator_physical_exam | |
| 'Multiple abnormal values', # orchestrator_lab_results | |
| 'Multiple studies indicated' # orchestrator_imaging | |
| ) | |
| # Store patient data in hidden state | |
| patient_data = gr.State({}) | |
| # Define patient case data | |
| patient_cases = { | |
| "patient_1": { | |
| "name": "Patient A", | |
| "age": "68", | |
| "summary": "68-year-old female in ICU, day 5 of admission for sepsis", | |
| "current_meds": "vancomycin 1g q12h, piperacillin-tazobactam 4.5g q6h", | |
| "culture_results": "Blood cultures (day 3): methicillin-sensitive Staphylococcus aureus (MSSA), sensitive to cefazolin, nafcillin, clindamycin", | |
| "site_of_infection": "bloodstream", | |
| "biofilm_risk": "central venous catheter present", | |
| "response": "clinically improving, fever resolved, WBC trending down", | |
| "creatinine_clearance": "75 mL/min (normal)", | |
| "severity": "severe sepsis, now stable", | |
| "allergies": "NKDA", | |
| "agent_focus": "🛡️ Antimicrobial Stewardship", | |
| "context": "This patient is a perfect candidate for antibiotic deescalation given the MSSA blood culture results and clinical improvement. Current broad-spectrum therapy can likely be narrowed." | |
| }, | |
| "patient_2": { | |
| "name": "Patient B", | |
| "age": "45", | |
| "summary": "45-year-old male, ICU patient with central line-associated bloodstream infection", | |
| "diagnosis": "Central line-associated bloodstream infection (CLABSI)", | |
| "central_line_days": "6 days", | |
| "culture_results": "Blood cultures positive for methicillin-resistant Staphylococcus aureus (MRSA)", | |
| "symptoms": "fever (38.8°C), chills, no other obvious source", | |
| "location": "Methodist Hospital, Dallas, Texas", | |
| "agent_focus": "🦠 Infection Prevention and Control", | |
| "context": "This case requires evaluation for NHSN CLABSI criteria, appropriate isolation precautions for MRSA, and reporting requirements for healthcare-associated infections in Texas." | |
| }, | |
| "patient_3": { | |
| "name": "Research Query", | |
| "topic": "Carbapenem-resistant Enterobacterales (CRE) treatment", | |
| "research_focus": "Novel treatment options for CRE infections", | |
| "specific_interests": "resistance mechanisms, combination therapies, newer antibiotics", | |
| "urgency": "clinical decision support needed", | |
| "agent_focus": "🔬 Research Assistant", | |
| "context": "Literature search and evidence synthesis needed for treatment of carbapenem-resistant Enterobacterales infections, including mechanism-based approaches and newest therapeutic options." | |
| }, | |
| "patient_4": { | |
| "name": "Patient C", | |
| "age": "32", | |
| "summary": "32-year-old male with fever, rash, and arthralgia after travel", | |
| "travel_history": "Recent travel to Southeast Asia (Thailand, Vietnam) 3 weeks ago", | |
| "symptoms": "fever (39.1°C), maculopapular rash on trunk and extremities, polyarthralgia", | |
| "duration": "symptoms for 5 days", | |
| "differential": "considering dengue fever, chikungunya, Zika virus, typhus, malaria", | |
| "agent_focus": "🏥 Clinical Assistant", | |
| "context": "Complex infectious disease case requiring systematic evaluation of travel-related illnesses and patient education about diagnostic workup and treatment options." | |
| }, | |
| "patient_5": { | |
| "name": "Education Request", | |
| "level": "Medical student, 3rd year", | |
| "topic": "Antimicrobial resistance mechanisms", | |
| "request": "Board exam questions and educational materials", | |
| "focus_areas": "beta-lactamase types, carbapenemases, ESBL, AmpC", | |
| "format_needed": "multiple choice questions, flashcards, presentation slides", | |
| "agent_focus": "📚 Education Assistant", | |
| "context": "Educational content creation for antimicrobial resistance mechanisms, suitable for medical student board exam preparation with varying difficulty levels." | |
| }, | |
| "patient_6": { | |
| "name": "Patient D", | |
| "age": "75", | |
| "summary": "75-year-old male with multiple infectious complications", | |
| "problem_1": "Ventilator-associated pneumonia with XDR Pseudomonas aeruginosa", | |
| "problem_2": "Clostridioides difficile colitis (severe, recurrent)", | |
| "problem_3": "Suspected infective endocarditis (blood cultures pending)", | |
| "comorbidities": "diabetes, chronic kidney disease (CrCl 30 mL/min), heart failure", | |
| "current_status": "ICU day 12, on multiple antibiotics, clinically complex", | |
| "agent_focus": "🎼 Orchestrator", | |
| "context": "Complex multi-system infectious disease case requiring coordination between stewardship, infection control, and clinical decision-making across multiple agents and specialties." | |
| } | |
| } | |
| # Connect patient card click handlers | |
| patient_card_1.click( | |
| fn=load_patient_1, | |
| inputs=[], | |
| outputs=[ | |
| chat_view, patient_data, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity, | |
| ipc_facility_name, ipc_location, ipc_infection_type, ipc_onset_date, ipc_device_days, ipc_pathogen, ipc_resistance_pattern, ipc_isolation_status, ipc_compliance_issues, | |
| clinical_chief_complaint, clinical_history_present, clinical_past_medical, clinical_medications, clinical_allergies, clinical_social_history, clinical_vital_signs, clinical_physical_exam, clinical_lab_results, clinical_imaging, | |
| orchestrator_culture, orchestrator_meds, orchestrator_site, orchestrator_biofilm, orchestrator_response, orchestrator_crcl, orchestrator_severity, orchestrator_allergies, orchestrator_facility_name, orchestrator_location, orchestrator_infection_type, orchestrator_onset_date, orchestrator_device_days, orchestrator_pathogen, orchestrator_resistance_pattern, orchestrator_isolation_status, orchestrator_compliance_issues, orchestrator_chief_complaint, orchestrator_history_present, orchestrator_past_medical, orchestrator_medications, orchestrator_patient_allergies, orchestrator_social_history, orchestrator_vital_signs, orchestrator_physical_exam, orchestrator_lab_results, orchestrator_imaging | |
| ] | |
| ) | |
| patient_card_2.click( | |
| fn=load_patient_2, | |
| inputs=[], | |
| outputs=[ | |
| chat_view, patient_data, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity, | |
| ipc_facility_name, ipc_location, ipc_infection_type, ipc_onset_date, ipc_device_days, ipc_pathogen, ipc_resistance_pattern, ipc_isolation_status, ipc_compliance_issues, | |
| clinical_chief_complaint, clinical_history_present, clinical_past_medical, clinical_medications, clinical_allergies, clinical_social_history, clinical_vital_signs, clinical_physical_exam, clinical_lab_results, clinical_imaging, | |
| orchestrator_culture, orchestrator_meds, orchestrator_site, orchestrator_biofilm, orchestrator_response, orchestrator_crcl, orchestrator_severity, orchestrator_allergies, orchestrator_facility_name, orchestrator_location, orchestrator_infection_type, orchestrator_onset_date, orchestrator_device_days, orchestrator_pathogen, orchestrator_resistance_pattern, orchestrator_isolation_status, orchestrator_compliance_issues, orchestrator_chief_complaint, orchestrator_history_present, orchestrator_past_medical, orchestrator_medications, orchestrator_patient_allergies, orchestrator_social_history, orchestrator_vital_signs, orchestrator_physical_exam, orchestrator_lab_results, orchestrator_imaging | |
| ] | |
| ) | |
| patient_card_3.click( | |
| fn=load_patient_3, | |
| inputs=[], | |
| outputs=[ | |
| chat_view, patient_data, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity, | |
| ipc_facility_name, ipc_location, ipc_infection_type, ipc_onset_date, ipc_device_days, ipc_pathogen, ipc_resistance_pattern, ipc_isolation_status, ipc_compliance_issues, | |
| clinical_chief_complaint, clinical_history_present, clinical_past_medical, clinical_medications, clinical_allergies, clinical_social_history, clinical_vital_signs, clinical_physical_exam, clinical_lab_results, clinical_imaging, | |
| orchestrator_culture, orchestrator_meds, orchestrator_site, orchestrator_biofilm, orchestrator_response, orchestrator_crcl, orchestrator_severity, orchestrator_allergies, orchestrator_facility_name, orchestrator_location, orchestrator_infection_type, orchestrator_onset_date, orchestrator_device_days, orchestrator_pathogen, orchestrator_resistance_pattern, orchestrator_isolation_status, orchestrator_compliance_issues, orchestrator_chief_complaint, orchestrator_history_present, orchestrator_past_medical, orchestrator_medications, orchestrator_patient_allergies, orchestrator_social_history, orchestrator_vital_signs, orchestrator_physical_exam, orchestrator_lab_results, orchestrator_imaging | |
| ] | |
| ) | |
| patient_card_4.click( | |
| fn=load_patient_4, | |
| inputs=[], | |
| outputs=[ | |
| chat_view, patient_data, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity, | |
| ipc_facility_name, ipc_location, ipc_infection_type, ipc_onset_date, ipc_device_days, ipc_pathogen, ipc_resistance_pattern, ipc_isolation_status, ipc_compliance_issues, | |
| clinical_chief_complaint, clinical_history_present, clinical_past_medical, clinical_medications, clinical_allergies, clinical_social_history, clinical_vital_signs, clinical_physical_exam, clinical_lab_results, clinical_imaging, | |
| orchestrator_culture, orchestrator_meds, orchestrator_site, orchestrator_biofilm, orchestrator_response, orchestrator_crcl, orchestrator_severity, orchestrator_allergies, orchestrator_facility_name, orchestrator_location, orchestrator_infection_type, orchestrator_onset_date, orchestrator_device_days, orchestrator_pathogen, orchestrator_resistance_pattern, orchestrator_isolation_status, orchestrator_compliance_issues, orchestrator_chief_complaint, orchestrator_history_present, orchestrator_past_medical, orchestrator_medications, orchestrator_patient_allergies, orchestrator_social_history, orchestrator_vital_signs, orchestrator_physical_exam, orchestrator_lab_results, orchestrator_imaging | |
| ] | |
| ) | |
| patient_card_5.click( | |
| fn=load_patient_5, | |
| inputs=[], | |
| outputs=[ | |
| chat_view, patient_data, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity, | |
| ipc_facility_name, ipc_location, ipc_infection_type, ipc_onset_date, ipc_device_days, ipc_pathogen, ipc_resistance_pattern, ipc_isolation_status, ipc_compliance_issues, | |
| clinical_chief_complaint, clinical_history_present, clinical_past_medical, clinical_medications, clinical_allergies, clinical_social_history, clinical_vital_signs, clinical_physical_exam, clinical_lab_results, clinical_imaging, | |
| orchestrator_culture, orchestrator_meds, orchestrator_site, orchestrator_biofilm, orchestrator_response, orchestrator_crcl, orchestrator_severity, orchestrator_allergies, orchestrator_facility_name, orchestrator_location, orchestrator_infection_type, orchestrator_onset_date, orchestrator_device_days, orchestrator_pathogen, orchestrator_resistance_pattern, orchestrator_isolation_status, orchestrator_compliance_issues, orchestrator_chief_complaint, orchestrator_history_present, orchestrator_past_medical, orchestrator_medications, orchestrator_patient_allergies, orchestrator_social_history, orchestrator_vital_signs, orchestrator_physical_exam, orchestrator_lab_results, orchestrator_imaging | |
| ] | |
| ) | |
| patient_card_6.click( | |
| fn=load_patient_6, | |
| inputs=[], | |
| outputs=[ | |
| chat_view, patient_data, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity, | |
| ipc_facility_name, ipc_location, ipc_infection_type, ipc_onset_date, ipc_device_days, ipc_pathogen, ipc_resistance_pattern, ipc_isolation_status, ipc_compliance_issues, | |
| clinical_chief_complaint, clinical_history_present, clinical_past_medical, clinical_medications, clinical_allergies, clinical_social_history, clinical_vital_signs, clinical_physical_exam, clinical_lab_results, clinical_imaging, | |
| orchestrator_culture, orchestrator_meds, orchestrator_site, orchestrator_biofilm, orchestrator_response, orchestrator_crcl, orchestrator_severity, orchestrator_allergies, orchestrator_facility_name, orchestrator_location, orchestrator_infection_type, orchestrator_onset_date, orchestrator_device_days, orchestrator_pathogen, orchestrator_resistance_pattern, orchestrator_isolation_status, orchestrator_compliance_issues, orchestrator_chief_complaint, orchestrator_history_present, orchestrator_past_medical, orchestrator_medications, orchestrator_patient_allergies, orchestrator_social_history, orchestrator_vital_signs, orchestrator_physical_exam, orchestrator_lab_results, orchestrator_imaging | |
| ] | |
| ) | |
| # --- Show/hide stewardship fields based on agent selection --- | |
| def update_dynamic_vars_visibility(agent_name): | |
| agent_json = agents_config.get(agent_name) | |
| if agent_json: | |
| agent_data = json.loads(agent_json) | |
| skills = agent_data.get("skills", []) | |
| agent_type = agent_data.get("agent_type", "") | |
| # Stewardship tools | |
| if "recommend_deescalation" in skills or "alert_prolonged_antibiotic_use" in skills: | |
| show_culture = "recommend_deescalation" in skills | |
| show_meds = "recommend_deescalation" in skills | |
| return ( | |
| gr.update(visible=True), # stewardship_vars_section | |
| gr.update(visible=show_culture), # deescalation_culture | |
| gr.update(visible=show_meds), # deescalation_meds | |
| gr.update(visible=False), # empiric_vars_section | |
| gr.update(visible=False), # ipc_vars_section | |
| gr.update(visible=False), # clinical_vars_section | |
| gr.update(visible=False) # orchestrator_vars_section | |
| ) | |
| # Empiric therapy tool | |
| elif "recommend_empiric_therapy" in skills: | |
| return ( | |
| gr.update(visible=False), # stewardship_vars_section | |
| gr.update(visible=False), # deescalation_culture | |
| gr.update(visible=False), # deescalation_meds | |
| gr.update(visible=True), # empiric_vars_section | |
| gr.update(visible=False), # ipc_vars_section | |
| gr.update(visible=False), # clinical_vars_section | |
| gr.update(visible=False) # orchestrator_vars_section | |
| ) | |
| # IPC tools | |
| elif "IPC_reporting" in skills or "NHSN_criteria_evaluator" in skills or "recommend_isolation_precautions" in skills: | |
| return ( | |
| gr.update(visible=False), # stewardship_vars_section | |
| gr.update(visible=False), # deescalation_culture | |
| gr.update(visible=False), # deescalation_meds | |
| gr.update(visible=False), # empiric_vars_section | |
| gr.update(visible=True), # ipc_vars_section | |
| gr.update(visible=False), # clinical_vars_section | |
| gr.update(visible=False) # orchestrator_vars_section | |
| ) | |
| # Clinical Assistant tools | |
| elif "retrieve_guidelines" in skills or "explain_in_layman_language" in skills or "history_taking" in skills: | |
| return ( | |
| gr.update(visible=False), # stewardship_vars_section | |
| gr.update(visible=False), # deescalation_culture | |
| gr.update(visible=False), # deescalation_meds | |
| gr.update(visible=False), # empiric_vars_section | |
| gr.update(visible=False), # ipc_vars_section | |
| gr.update(visible=True), # clinical_vars_section | |
| gr.update(visible=False) # orchestrator_vars_section | |
| ) | |
| # Orchestrator (check by agent type since it has no specific skills) | |
| elif "🎼 Orchestrator" in agent_type: | |
| return ( | |
| gr.update(visible=False), # stewardship_vars_section | |
| gr.update(visible=False), # deescalation_culture | |
| gr.update(visible=False), # deescalation_meds | |
| gr.update(visible=False), # empiric_vars_section | |
| gr.update(visible=False), # ipc_vars_section | |
| gr.update(visible=False), # clinical_vars_section | |
| gr.update(visible=True) # orchestrator_vars_section | |
| ) | |
| # Hide all | |
| return ( | |
| gr.update(visible=False), # stewardship_vars_section | |
| gr.update(visible=False), # deescalation_culture | |
| gr.update(visible=False), # deescalation_meds | |
| gr.update(visible=False), # empiric_vars_section | |
| gr.update(visible=False), # ipc_vars_section | |
| gr.update(visible=False), # clinical_vars_section | |
| gr.update(visible=False) # orchestrator_vars_section | |
| ) | |
| agent_picker.change( | |
| fn=update_dynamic_vars_visibility, | |
| inputs=[agent_picker], | |
| outputs=[ | |
| stewardship_vars_section, deescalation_culture, deescalation_meds, empiric_vars_section, | |
| ipc_vars_section, clinical_vars_section, orchestrator_vars_section | |
| ] | |
| ) | |
| # Client-side script | |
| gr.HTML(""" | |
| <script> | |
| document.addEventListener("DOMContentLoaded", function(){ | |
| const startBtn = document.getElementById('start_button'); | |
| const backBtn = document.getElementById('back_button'); | |
| const toChatBtn = document.getElementById('to_chat_button'); | |
| const chatBackBtn = document.getElementById('chat_back'); | |
| const landing = document.getElementById('landing_card'); | |
| const form = document.getElementById('agent_form'); | |
| const chat = document.getElementById('agent_chat'); | |
| // Landing → Builder | |
| startBtn.addEventListener('click', () => { | |
| landing.classList.add('hidden'); | |
| setTimeout(() => { | |
| landing.style.display = 'none'; | |
| form.style.display = 'block'; | |
| form.classList.add('hidden'); | |
| setTimeout(() => form.classList.remove('hidden'), 50); | |
| }, 500); | |
| }); | |
| // Builder → Landing | |
| backBtn.addEventListener('click', () => { | |
| form.classList.add('hidden'); | |
| setTimeout(() => { | |
| form.style.display = 'none'; | |
| landing.style.display = 'block'; | |
| landing.classList.add('hidden'); | |
| setTimeout(() => landing.classList.remove('hidden'), 50); | |
| }, 500); | |
| }); | |
| // Builder → Agent-Chat | |
| toChatBtn.addEventListener('click', () => { | |
| form.classList.add('hidden'); | |
| setTimeout(() => { | |
| form.style.display = 'none'; | |
| chat.style.display = 'block'; | |
| chat.classList.add('hidden'); | |
| setTimeout(() => chat.classList.remove('hidden'), 50); | |
| }, 500); | |
| }); | |
| // Agent-Chat → Builder | |
| chatBackBtn.addEventListener('click', () => { | |
| chat.classList.add('hidden'); | |
| setTimeout(() => { | |
| chat.style.display = 'none'; | |
| form.style.display = 'block'; | |
| form.classList.add('hidden'); | |
| setTimeout(() => form.classList.remove('hidden'), 50); | |
| }, 500); | |
| }); | |
| }); | |
| </script> | |
| """) | |
| # --- Interactions --- | |
| # Simple GPT-3.5 Chat callbacks (no skills, no internet) | |
| def simple_send_handler(user_message, history): | |
| updated_history, cleared = simple_chat_response(user_message, history) | |
| return updated_history, cleared, updated_history | |
| simple_send.click( | |
| simple_send_handler, | |
| inputs=[simple_input, simple_chat_history], | |
| outputs=[simple_chatbot, simple_input, simple_chat_history], | |
| ) | |
| simple_input.submit( | |
| simple_send_handler, | |
| inputs=[simple_input, simple_chat_history], | |
| outputs=[simple_chatbot, simple_input, simple_chat_history], | |
| ) | |
| def simple_reset_handler(): | |
| return [], "", [] | |
| simple_reset.click( | |
| simple_reset_handler, | |
| inputs=[], | |
| outputs=[simple_chatbot, simple_input, simple_chat_history], | |
| ) | |
| start_button.click( | |
| fn=lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), ""), | |
| inputs=[], | |
| outputs=[landing_panel, agent_form, chat_panel, challenger_debug_md], | |
| ) | |
| back_button.click( | |
| fn=show_landing, | |
| inputs=[], | |
| outputs=[landing_panel, agent_form, chat_panel], | |
| ) | |
| to_chat_button.click( | |
| fn=lambda: (gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), ""), | |
| outputs=[landing_panel, agent_form, chat_panel, challenger_debug_md] | |
| ).then( | |
| refresh_active_agents_widgets, | |
| inputs=[], | |
| outputs=[chat_active_agents, agent_picker] | |
| ) | |
| # Only wire up chat_back for the agent_chat panel (third page) | |
| chat_back.click( | |
| fn=lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), ""), | |
| outputs=[landing_panel, agent_form, chat_panel, challenger_debug_md] | |
| ) | |
| # when you pick a new agent, reload its history into chat_view | |
| agent_picker.change( | |
| fn=load_history, | |
| inputs=[agent_picker, deployed_chat_histories], | |
| outputs=[chat_view] | |
| ) | |
| # when you click Send, append & re-render | |
| def chatpanel_handle_with_dynamic_vars( | |
| agent_name, user_text, histories, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity | |
| ): | |
| agent_json = agents_config.get(agent_name) | |
| # Prevent tool invocation on empty/whitespace input (e.g., initial greeting or agent selection) | |
| if user_text is None or not str(user_text).strip(): | |
| # Just return the current history, do not invoke any tool | |
| return histories.get(agent_name, []), histories, user_text or "" | |
| # --- IPC dynamic requirements integration --- | |
| ipc_tool_triggered = False | |
| ipc_jurisdiction = None | |
| ipc_info = None | |
| if agent_json: | |
| agent_data = json.loads(agent_json) | |
| skills = agent_data.get("skills", []) | |
| history = histories.get(agent_name, []) | |
| # --- Trusted links wiring --- | |
| trusted_links = [] | |
| for k in ["trusted_links", "trusted_links_1", "trusted_links_2", "trusted_links_3", "trusted_links_4"]: | |
| if isinstance(agent_data.get(k), list): | |
| trusted_links.extend([l for l in agent_data[k] if l]) | |
| elif isinstance(agent_data.get(k), str) and agent_data[k]: | |
| trusted_links.append(agent_data[k]) | |
| for k in ["link1", "link2", "link3", "link4"]: | |
| if agent_data.get(k): | |
| trusted_links.append(agent_data[k]) | |
| trusted_links = [l for l in trusted_links if l] | |
| # --- End trusted links wiring --- | |
| # IPC tool dynamic requirements fetch | |
| if "ipc_reporting" in skills: | |
| # TODO: Fix IPC requirements fetch | |
| # fetch_ipc_requirements = tools.fetch_ipc_requirements | |
| # Always extract the latest jurisdiction from the most recent user message | |
| us_states = [ | |
| "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" | |
| ] | |
| # Try to find a state name (case-insensitive) in the latest user message | |
| state_match = None | |
| for state in us_states: | |
| if re.search(rf"\\b{state}\\b", user_text, re.IGNORECASE): | |
| state_match = state | |
| break | |
| match = re.search(r"\b(CDC|WHO|United States|US|World Health Organization|global)\b", user_text, re.IGNORECASE) | |
| if state_match: | |
| ipc_jurisdiction = state_match | |
| elif match: | |
| ipc_jurisdiction = match.group(1) | |
| else: | |
| # If not in this message, look back in history for last mentioned jurisdiction | |
| ipc_jurisdiction = None | |
| for msg in reversed(history): | |
| if msg["role"] == "user": | |
| for state in us_states: | |
| if re.search(rf"\\b{state}\\b", msg["content"], re.IGNORECASE): | |
| ipc_jurisdiction = state | |
| break | |
| if not ipc_jurisdiction: | |
| match2 = re.search(r"\b(CDC|WHO|United States|US|World Health Organization|global)\b", msg["content"], re.IGNORECASE) | |
| if match2: | |
| ipc_jurisdiction = match2.group(1) | |
| if ipc_jurisdiction: | |
| break | |
| if not ipc_jurisdiction: | |
| ipc_jurisdiction = "CDC" # Default | |
| # Fetch requirements for the extracted jurisdiction | |
| # ipc_info = fetch_ipc_requirements(ipc_jurisdiction) | |
| ipc_info = None # TODO: Fix IPC requirements fetch | |
| ipc_tool_triggered = True | |
| # Prepend IPC requirements info to user_text for the agent | |
| if ipc_info: | |
| req_fields_list = ipc_info.get("fields", []) | |
| req_fields = ", ".join(req_fields_list) | |
| summary = ipc_info.get("summary", "") | |
| source_url = ipc_info.get("source_url") | |
| warning = ipc_info.get("warning") | |
| else: | |
| req_fields_list = [] | |
| req_fields = "" | |
| summary = "" | |
| source_url = "" | |
| warning = "" | |
| if ipc_info: # Only process if we have valid info | |
| # --- Prevent repeated tool invocation logic --- | |
| # Gather current field values from user_text/history (simple: just use user_text for now) | |
| current_submission = { | |
| "jurisdiction": ipc_jurisdiction, | |
| "fields": req_fields_list, | |
| "summary": summary, | |
| "user_text": user_text.strip() | |
| } | |
| # Find last IPC_SUBMISSION in history | |
| last_ipc_submission = None | |
| for msg in reversed(history): | |
| if msg["role"] == "system" and msg["content"].startswith("[IPC_SUBMISSION]"): | |
| try: | |
| last_ipc_submission = json.loads(msg["content"][len("[IPC_SUBMISSION]"):].strip()) | |
| except Exception: | |
| last_ipc_submission = None | |
| break | |
| # Only proceed if something changed | |
| if last_ipc_submission == current_submission: | |
| # Requirements and user input unchanged, do not re-invoke | |
| return history, histories, "" | |
| # --- Dynamic required fields logic --- | |
| # Only prompt for missing fields if the live/static requirements specify them | |
| missing_fields = [] | |
| if req_fields_list: | |
| # Try to extract field values from user_text (very basic: look for each field name in user_text, case-insensitive) | |
| for field in req_fields_list: | |
| if not re.search(rf"\b{re.escape(field)}\b", user_text, re.IGNORECASE): | |
| missing_fields.append(field) | |
| if missing_fields: | |
| prompt = f"Please provide the following required information for IPC reporting: {', '.join(missing_fields)}." | |
| history.append({"role": "assistant", "content": prompt}) | |
| return history, histories, "" | |
| # If no required fields or all are present, proceed | |
| user_text = f"[IPC_REQUIREMENTS] Jurisdiction: {ipc_jurisdiction}\nRequired fields: {req_fields}\nSummary: {summary}\n" + user_text | |
| # Always add a visible assistant message to the chat so the user sees which requirements are being used | |
| visible_msg = f"📋 <b>Reporting requirements for <u>{ipc_jurisdiction}</u>:</b> <br>" | |
| if req_fields: | |
| visible_msg += f"<b>Required fields:</b> {req_fields}. " | |
| if summary: | |
| visible_msg += f"<b>Summary:</b> {summary} " | |
| if source_url: | |
| visible_msg += f'<br><b>Source:</b> <a href="{source_url}" target="_blank">{source_url}</a>' | |
| if warning: | |
| visible_msg += f'<br><b>Warning:</b> {warning}' | |
| try: | |
| with open("debug_log.txt", "a", encoding="utf-8") as f: | |
| f.write(f"[DEBUG] IPC visible_msg: {visible_msg}\n") | |
| except Exception as e: | |
| pass | |
| # Add the requirements message if it's new or the jurisdiction changed | |
| if not (history and history[-1]["role"] == "assistant" and ipc_jurisdiction in history[-1]["content"]): | |
| history.append({"role": "assistant", "content": visible_msg}) | |
| # Store this submission in history as a hidden system message | |
| history.append({"role": "system", "content": "[IPC_SUBMISSION] " + json.dumps(current_submission)}) | |
| # Deescalation tool | |
| if "recommend_deescalation" in skills: | |
| var_names = ["culture", "meds", "site_of_infection", "risk_of_biofilm", "current_response", "creatinine_clearance", "severity_of_infection", "known_allergies"] | |
| user_vars = { | |
| "culture": deescalation_culture, | |
| "meds": deescalation_meds, | |
| "site_of_infection": stewardship_site, | |
| "risk_of_biofilm": stewardship_biofilm, | |
| "current_response": stewardship_response, | |
| "creatinine_clearance": stewardship_crcl, | |
| "severity_of_infection": stewardship_severity, | |
| "known_allergies": stewardship_allergies | |
| } | |
| extracted = extract_clinical_variables_from_history(history, var_names) | |
| for k in var_names: | |
| if not user_vars[k]: | |
| user_vars[k] = extracted.get(k) or "" | |
| # Only prepend if at least one field is non-empty | |
| if any(user_vars[k] for k in var_names): | |
| user_text = f"[DEESCALATION_TOOL_INPUT] {json.dumps(user_vars)}\n" + user_text | |
| # Alert prolonged antibiotic use tool | |
| elif "alert_prolonged_antibiotic_use" in skills: | |
| var_names = ["site_of_infection", "risk_of_biofilm", "current_response", "creatinine_clearance", "severity_of_infection", "known_allergies"] | |
| user_vars = { | |
| "site_of_infection": stewardship_site, | |
| "risk_of_biofilm": stewardship_biofilm, | |
| "current_response": stewardship_response, | |
| "creatinine_clearance": stewardship_crcl, | |
| "severity_of_infection": stewardship_severity, | |
| "known_allergies": stewardship_allergies | |
| } | |
| extracted = extract_clinical_variables_from_history(history, var_names) | |
| for k in var_names: | |
| if not user_vars[k]: | |
| user_vars[k] = extracted.get(k) or "" | |
| if any(user_vars[k] for k in var_names): | |
| user_text = f"[ALERT_PROLONGED_ABX_INPUT] {json.dumps(user_vars)}\n" + user_text | |
| # Empiric therapy tool | |
| elif "recommend_empiric_therapy" in skills: | |
| var_names = [ | |
| "age", "allergies", "labs", "culture", "meds", "site_of_infection", | |
| "risk_of_biofilm", "current_response", "creatinine_clearance", "severity_of_infection" | |
| ] | |
| user_vars = { | |
| "age": empiric_age, | |
| "allergies": empiric_allergies, | |
| "labs": empiric_labs, | |
| "culture": empiric_culture, | |
| "meds": empiric_meds, | |
| "site_of_infection": empiric_site, | |
| "risk_of_biofilm": empiric_biofilm, | |
| "current_response": empiric_response, | |
| "creatinine_clearance": empiric_crcl, | |
| "severity_of_infection": empiric_severity | |
| } | |
| extracted = extract_clinical_variables_from_history(history, var_names) | |
| for k in var_names: | |
| if not user_vars[k]: | |
| user_vars[k] = extracted.get(k) or "" | |
| # If any required field is missing, prompt = ... | |
| missing = [k.replace('_', ' ').capitalize() for k in var_names if not user_vars[k].strip()] | |
| if missing: | |
| prompt = f"Please provide the following required information for empiric therapy: {', '.join(missing)}." | |
| # Show this as an assistant message and do not call the tool | |
| history.append({"role": "assistant", "content": prompt}) | |
| return history, histories, "" | |
| # All required fields present, prepend tool input | |
| if any(user_vars[k] for k in var_names): | |
| user_text = f"[EMPIRIC_THERAPY_INPUT] {json.dumps(user_vars)}\n" + user_text | |
| # Use simulate_agent_response_stream for all agents to ensure challenger logic is applied | |
| import asyncio | |
| from core.agents.chat_orchestrator import simulate_agent_response_stream | |
| agent_json_val = agents_config.get(agent_name) | |
| history_val = histories.get(agent_name, []) | |
| result = None | |
| # Prevent repeated tool invocation: if the last assistant message is a tool request for the same required fields, do not re-invoke | |
| if history_val and history_val[-1]["role"] == "assistant": | |
| last_content = history_val[-1]["content"] | |
| if "required fields" in last_content.lower() and "ipc_reporting" in last_content.lower(): | |
| # Don't re-invoke, just return | |
| return history_val, histories, "" | |
| async def run_stream(): | |
| final_history = history_val | |
| final_invocation_log = "" | |
| challenger_info = None | |
| gen = simulate_agent_response_stream( | |
| agent_json=agent_json_val, | |
| history=history_val, | |
| user_input=user_text, | |
| debug_flag=False, | |
| active_children=[] | |
| ) | |
| async for updated_history, _, invocation_log, _, challenger_info in gen: | |
| final_history = updated_history | |
| final_invocation_log = invocation_log | |
| return final_history, final_invocation_log, challenger_info | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| try: | |
| result = loop.run_until_complete(run_stream()) | |
| if result is not None and len(result) == 3: | |
| final_history, invocation_log, challenger_info = result | |
| else: | |
| final_history, updated_histories, cleared_input, invocation_log = chatpanel_handle(agent_name, user_text, histories) | |
| challenger_info = None | |
| return final_history, updated_histories, cleared_input, invocation_log | |
| except Exception: | |
| # fallback to old behavior if error | |
| final_history, updated_histories, cleared_input, invocation_log = chatpanel_handle(agent_name, user_text, histories) | |
| challenger_info = None | |
| return final_history, updated_histories, cleared_input, invocation_log | |
| # Update histories | |
| updated_histories = histories.copy() | |
| updated_histories[agent_name] = final_history | |
| # Prepare challenger markdown (debug log for builder panel) | |
| challenger_md = "" | |
| challenger_enabled = False | |
| try: | |
| if agent_json_val: | |
| agent_data = json.loads(agent_json_val) | |
| challenger_enabled = agent_data.get("challenger_enabled", False) | |
| except Exception: | |
| pass | |
| if isinstance(challenger_info, dict) and challenger_enabled: | |
| orig = challenger_info.get("original_reply", "") | |
| crit = challenger_info.get("challenger_critique", "") | |
| final = challenger_info.get("final_reply", "") | |
| challenger_md = f"**Original Agent Answer:**\n\n{orig}\n\n**Challenger Critique:**\n\n{crit}\n\n**Final Answer Shown to User:**\n\n{final}" | |
| # Only show the final (challenger-approved) answer in the chatbox | |
| if final and final_history and isinstance(final_history, list): | |
| final_history[-1]["content"] = final | |
| # If challenger is not enabled, do not show the markdown at all | |
| elif not challenger_enabled: | |
| challenger_md = "" | |
| return final_history, updated_histories, "", invocation_log, challenger_md | |
| chat_send.click( | |
| fn=chatpanel_handle_with_dynamic_vars, | |
| inputs=[ | |
| agent_picker, chat_panel_input, deployed_chat_histories, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity | |
| ], | |
| outputs=[chat_view, deployed_chat_histories, chat_panel_input] | |
| ) | |
| # --- Reset button for deployed agent chat panel --- | |
| def reset_and_clear_deployed_history(agent_name, histories): | |
| # Clear orchestrator state to prevent persistence across conversations | |
| from core.agents.chat_orchestrator import orchestrators | |
| orchestrators.clear() | |
| if not agent_name: | |
| return ( | |
| [], histories, "", | |
| "", "", "", "", "", "", "", "", # stewardship fields cleared (8 fields) | |
| "", "", "", "", "", "", "", "", "", "", # empiric fields cleared (10 fields) | |
| "", "", "", "", "", "", "", "", "", # ipc fields cleared (9 fields) | |
| "", "", "", "", "", "", "", "", "", "", # clinical fields cleared (10 fields) | |
| "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" # orchestrator fields cleared (27 fields) | |
| ) | |
| # Remove the agent's chat history and re-seed with greeting | |
| if agent_name in histories: | |
| del histories[agent_name] | |
| chat_history = [{"role": "assistant", "content": f"👋 Hello! I'm {agent_name}. How can I help?"}] | |
| histories[agent_name] = chat_history | |
| return ( | |
| chat_history, histories, "", | |
| "", "", "", "", "", "", "", "", # stewardship fields cleared (8 fields) | |
| "", "", "", "", "", "", "", "", "", "", # empiric fields cleared (10 fields) | |
| "", "", "", "", "", "", "", "", "", # ipc fields cleared (9 fields) | |
| "", "", "", "", "", "", "", "", "", "", # clinical fields cleared (10 fields) | |
| "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" # orchestrator fields cleared (27 fields) | |
| ) | |
| chat_reset.click( | |
| reset_and_clear_deployed_history, | |
| inputs=[agent_picker, deployed_chat_histories], | |
| outputs=[ | |
| chat_view, deployed_chat_histories, chat_panel_input, | |
| deescalation_culture, deescalation_meds, | |
| stewardship_site, stewardship_biofilm, stewardship_response, stewardship_crcl, stewardship_severity, stewardship_allergies, | |
| empiric_age, empiric_allergies, empiric_labs, empiric_culture, empiric_meds, empiric_site, empiric_biofilm, empiric_response, empiric_crcl, empiric_severity, | |
| ipc_facility_name, ipc_location, ipc_infection_type, ipc_onset_date, ipc_device_days, ipc_pathogen, ipc_resistance_pattern, ipc_isolation_status, ipc_compliance_issues, | |
| clinical_chief_complaint, clinical_history_present, clinical_past_medical, clinical_medications, clinical_allergies, clinical_social_history, clinical_vital_signs, clinical_physical_exam, clinical_lab_results, clinical_imaging, | |
| orchestrator_culture, orchestrator_meds, orchestrator_site, orchestrator_biofilm, orchestrator_response, orchestrator_crcl, orchestrator_severity, orchestrator_allergies, orchestrator_facility_name, orchestrator_location, orchestrator_infection_type, orchestrator_onset_date, orchestrator_device_days, orchestrator_pathogen, orchestrator_resistance_pattern, orchestrator_isolation_status, orchestrator_compliance_issues, orchestrator_chief_complaint, orchestrator_history_present, orchestrator_past_medical, orchestrator_medications, orchestrator_patient_allergies, orchestrator_social_history, orchestrator_vital_signs, orchestrator_physical_exam, orchestrator_lab_results, orchestrator_imaging | |
| ] | |
| ) | |
| agent_type.change( | |
| fn=on_agent_type_change, | |
| inputs=[agent_type, prefill_flag], | |
| outputs=[skills, agent_name, agent_mission, prefill_flag] | |
| ) | |
| prefilled.change( | |
| fn=populate_from_preset, | |
| inputs=[prefilled], | |
| outputs=[agent_type, agent_name, agent_mission, skills, prefill_flag] | |
| ) | |
| uploaded_files.upload(fn=handle_uploaded_files, inputs=[uploaded_files], outputs=[upload_alert, upload_alert]) | |
| def handle_generate(agent_type, agent_name, agent_mission, selected_skills, web_access, allow_fallback, uploaded_files, link1, link2, link3, link4, challenger_toggle): | |
| # Accept challenger_toggle as an argument | |
| agent_json = build_agent(agent_type, agent_name, agent_mission, selected_skills, web_access, allow_fallback, uploaded_files, link1, link2, link3, link4) | |
| # Add challenger_enabled to the agent config JSON | |
| agent_data = json.loads(agent_json) | |
| agent_data["challenger_enabled"] = challenger_toggle | |
| agent_json = json.dumps(agent_data) | |
| agents_config[agent_name] = agent_json | |
| return agent_json | |
| generate_button.click( | |
| lambda: gr.update(visible=True, value="⏳ Generating your agent..."), | |
| inputs=[], outputs=[agent_loader] | |
| ).then( # 1) add / save the agent | |
| handle_generate, | |
| inputs=[agent_type, agent_name, agent_mission, skills, | |
| web_access_toggle, allow_fallback_toggle, | |
| uploaded_files, link1, link2, link3, link4, challenger_toggle], | |
| outputs=[agent_output] # <- only the JSON | |
| ).then( # 2) show initial instruction instead of preload demo chat | |
| lambda: show_initial_instruction_state(), | |
| inputs=[], outputs=[builder_chatbot, chat_input, builder_send_button, reset_button, invocation_log, active_children] | |
| ).then( # 2.5) auto-load agent fields into builder panel | |
| lambda agent_json: ( | |
| json.loads(agent_json).get("agent_type", ""), | |
| json.loads(agent_json).get("agent_name", ""), | |
| json.loads(agent_json).get("agent_mission", ""), | |
| json.loads(agent_json).get("skills", []) | |
| ), | |
| inputs=[agent_output], | |
| outputs=[agent_type, agent_name, agent_mission, skills] | |
| ).then( # 3) refresh markdown & dropdown *atomically* | |
| refresh_active_agents_widgets, | |
| inputs=[], outputs=[builder_active_agents, agent_remove_dropdown] | |
| ).then( # 4) done spinner | |
| lambda: gr.update(visible=True, value="✅ Agent generated successfully!"), | |
| inputs=[], outputs=[agent_loader] | |
| ).then( | |
| refresh_chat_dropdown, | |
| inputs=[], | |
| outputs=[agent_picker] | |
| ) | |
| edit_agent_button.click( | |
| load_agent_to_builder, | |
| inputs=[agent_remove_dropdown], | |
| outputs=[agent_type, agent_name, agent_mission, skills] | |
| ) | |
| chat_agent_button.click( | |
| fn=enable_chat_controls_with_agent, | |
| inputs=[agent_remove_dropdown], | |
| outputs=[builder_chatbot, chat_input, builder_send_button, reset_button, invocation_log, active_children, agent_output] | |
| ) | |
| remove_agent_button.click( | |
| remove_selected_agent, | |
| inputs=[agent_remove_dropdown], | |
| outputs=[builder_active_agents, agent_remove_dropdown] | |
| ).then( | |
| refresh_chat_dropdown, | |
| inputs=[], | |
| outputs=[agent_picker] | |
| ) | |
| download_button.click( | |
| prepare_download, | |
| inputs=[agent_output], | |
| outputs=[download_button] | |
| ) | |
| # Only keep reset for builder panel, and chat_send for chat panel | |
| def reset_and_clear_builder_history(agent_json, histories): | |
| # Clear orchestrator state to prevent persistence across conversations | |
| from core.agents.chat_orchestrator import orchestrators | |
| orchestrators.clear() | |
| if not agent_json or agent_json.strip() == "": | |
| # No agent selected, return disabled state with instruction | |
| instruction_state = show_initial_instruction_state() | |
| return instruction_state[0], instruction_state[1], instruction_state[5], histories # chatbot, input, active_children, histories | |
| # Valid agent selected, show agent greeting with enabled controls | |
| agent_data = json.loads(agent_json) | |
| name = agent_data.get("agent_name", "Agent") | |
| welcome = f"👋 Hello! I'm {name}. How can I assist you today?" | |
| chat_history = [{"role":"assistant","content":welcome}] | |
| if name in histories: | |
| del histories[name] | |
| return chat_history, gr.update(value="", interactive=True, placeholder="Type your question here…"), [], histories | |
| reset_button.click( | |
| reset_and_clear_builder_history, | |
| inputs=[agent_output, builder_chat_histories], | |
| outputs=[builder_chatbot, chat_input, active_children, builder_chat_histories] | |
| ) | |
| return app | |
| if __name__ == "__main__": | |
| try: | |
| # Check if running on Hugging Face Spaces | |
| try: | |
| from hf_config import configure_hf_environment, get_hf_launch_config | |
| if configure_hf_environment(): | |
| # Use HF Spaces configuration | |
| launch_config = get_hf_launch_config() | |
| print("🚀 Launching on Hugging Face Spaces...") | |
| build_ui().launch(**launch_config) | |
| else: | |
| # Local development | |
| print("🔧 Launching locally...") | |
| build_ui().launch() | |
| except ImportError: | |
| # Fallback if hf_config not available | |
| print("🔧 Launching with default configuration...") | |
| build_ui().launch() | |
| except Exception as e: | |
| logger.error(f"Failed to launch Gradio app: {e}") | |
| print(f"Failed to launch Gradio app: {e}") | |
| print("💡 Check your API keys and environment configuration") |