import os import re import time import gradio as gr from PIL import Image from cpu_agent import ( full_agent, save_retriever_from_pdf, load_persisted_retriever, VECTOR_STORE_PATH, ) # Import the raw inference function so we can stream the orchestrator from gguf_engine import ( _load_text_model, _format_gemma_prompt, exclude_thinking_component, LLM_LORA_PATHS, ) import psutil import os def check_memory(): process = psutil.Process(os.getpid()) # RAM usage in GB return process.memory_info().rss / 1024**3 # ========================================== # STREAMING HELPER # Calls llama-cpp-python with stream=True so # the orchestrator output appears token-by-token. # ========================================== def _stream_generate(prompt: str, max_tokens: int = 800): """ Generator that yields incremental text from the base model (no LoRA). Used exclusively for the orchestrator's final answer. """ print(f"[BEFORE MODEL LOAD] RAM Usage: {check_memory():.2f} GB") model = _load_text_model("default") print(f"[AFTER MODEL LOAD] RAM Usage: {check_memory():.2f} GB") formatted = _format_gemma_prompt(prompt) accumulated = "" for chunk in model( formatted, max_tokens = max_tokens, stop = ["", ""], echo = False, temperature = 0.0, top_p = 1.0, stream = True, ): print(f"[STREAM] RAM Usage: {check_memory():.2f} GB") token = chunk["choices"][0]["text"] accumulated += token yield exclude_thinking_component(accumulated) # ========================================== # BACKEND: TAB 1 β€” ADMIN KNOWLEDGE BASE # ========================================== def initialize_knowledge_base(pdf_file, legal_consent): if not legal_consent: yield "β›” Cannot proceed: legal consent is required." return if pdf_file is None: yield "β›” No PDF uploaded." return pdf_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name filename = os.path.basename(pdf_path) yield f"πŸ“„ Received: {filename}\n⏳ Building vector index β€” please wait..." success, message = save_retriever_from_pdf(pdf_path) if success: yield f"{message}\n\nβœ… Knowledge base ready." else: yield f"{message}\n\n⚠️ Falling back to built-in NCCN/ESMO excerpts." def get_kb_status(): r, label = load_persisted_retriever() if r: return f"βœ… Active: {label}" return "⚠️ No knowledge base. Upload a PDF in the Configuration tab." # ========================================== # BACKEND: TAB 2 β€” PATIENT CONSULTATION # Streaming generator β€” yields partial UI # updates as each LangGraph node completes, # then streams the final answer token-by-token. # ========================================== def process_patient_data( user_query, lab_values, behaviour_changes, wsi_image, progress=gr.Progress(track_tqdm=True) ): print(f"[START] RAM Usage: {check_memory():.2f} GB") # ── Assemble clinical text ───────────────────────────────────────────── parts = [] if lab_values.strip(): parts.append(f"Lab Values:\n{lab_values.strip()}") if behaviour_changes.strip(): parts.append(f"Behaviour / Symptoms:\n{behaviour_changes.strip()}") raw_clinical_text = "\n\n".join(parts) if parts else "No clinical data provided." wsi_path = "" if wsi_image is not None: wsi_path = "/tmp/uploaded_wsi.bmp" wsi_image.save(wsi_path) # ── Helper: build the full yield-tuple from current partial state ────── def _build_yield( modules_str="", recommendation="⏳ Analysing...", risk="", show_m2=False, wsi_text="", show_m3=False, img=None, mal_stat=gr.update(visible=False), prog="", show_m4=False, rag="", show_m5=False, m5_src=gr.update(visible=False), raw_chunks=gr.update(visible=False), ): return ( modules_str, recommendation, gr.update(value=risk, visible=show_m2), gr.update(visible=show_m2), gr.update(value=wsi_text, visible=show_m3), gr.update(visible=show_m3), gr.update(value=img, visible=img is not None), mal_stat, gr.update(value=prog, visible=show_m4), gr.update(visible=show_m4), gr.update(value=rag, visible=show_m5), gr.update(visible=show_m5), m5_src, raw_chunks, ) # ── Initial "running" state ──────────────────────────────────────────── progress(0, desc="Initialising pipeline…") yield _build_yield(modules_str="Planning…", recommendation="⏳ Running diagnostic pipeline…") # ── Run the full agent (blocking β€” all modules run here) ─────────────── progress(0.1, desc="Routing clinical query to modules…") initial_state = { "patient_id": "VAJRAM_UI", "user_query": user_query.strip() or "Give me a full clinical workup.", "raw_clinical_text": raw_clinical_text, "modules_queue": [], "wsi_image_path": wsi_path, "wsi_output_path": "/tmp/annotated_wsi_output.png", "module2_risk_score": "", "module3_wsi_analysis": "", "module4_progression": "", "module5_guidelines": "", "module5_raw_chunks": "", "module5_source": "", "final_recommendation": "", } # Run everything up to (but not including) the orchestrator synthesis # by temporarily patching the orchestrator to return early. # Simpler: just run the full agent and grab intermediate results. progress(0.2, desc="Module 2 Β· Risk Stratification LoRA…") final_state = full_agent.invoke(initial_state) print(f"[AFTER AGENT] RAM Usage: {check_memory():.2f} GB") # ── Unpack results ───────────────────────────────────────────────────── ran_m2 = bool(final_state.get("module2_risk_score", "").strip()) ran_m3 = bool(final_state.get("module3_wsi_analysis", "").strip()) ran_m4 = bool(final_state.get("module4_progression", "").strip()) ran_m5 = bool(final_state.get("module5_guidelines", "").strip()) selected = [] if ran_m2: selected.append("Module 2 Β· Risk") if ran_m3: selected.append("Module 3 Β· WSI") if ran_m4: selected.append("Module 4 Β· Progression") if ran_m5: selected.append("Module 5 Β· RAG") modules_str = " β€Ί ".join(selected) if selected else "None" # ── Annotated WSI image ──────────────────────────────────────────────── annotated_img = None mal_stat = gr.update(visible=False) if ran_m3 and wsi_path: try: annotated_img = Image.open("/tmp/annotated_wsi_output.png") wsi_summary = final_state.get("module3_wsi_analysis", "") pct_str = "" m = re.search(r'(\d+\.?\d*)\s*%\s*\)', wsi_summary) if m: pct_str = m.group(1) counts_m = re.search(r'(\d+)/(\d+) patches', wsi_summary) if counts_m: n_mal, n_total = counts_m.group(1), counts_m.group(2) mal_stat = gr.update( value=( f"Malignant: {n_mal} / {n_total} patches ({pct_str}%)\n" f"Red = Malignant Β· Green = Normal Β· Gray = Background Β· Yellow = Unknown" ), visible=True ) else: mal_stat = gr.update(value=wsi_summary, visible=True) except Exception: annotated_img = None m5_src_update = gr.update( value=f"Source: {final_state.get('module5_source','')}" if ran_m5 else "", visible=ran_m5 ) raw_chunks_update = gr.update( value=final_state.get("module5_raw_chunks", ""), visible=ran_m5 ) # ── Yield intermediate state before streaming the final answer ───────── progress(0.7, desc="Modules complete β€” synthesising recommendation…") yield _build_yield( modules_str = modules_str, recommendation = "⏳ Synthesising final recommendation…", risk = final_state.get("module2_risk_score", ""), show_m2 = ran_m2, wsi_text = final_state.get("module3_wsi_analysis", ""), show_m3 = ran_m3, img = annotated_img, mal_stat = mal_stat, prog = final_state.get("module4_progression", ""), show_m4 = ran_m4, rag = final_state.get("module5_guidelines", ""), show_m5 = ran_m5, m5_src = m5_src_update, raw_chunks = raw_chunks_update, ) # ── Stream the final recommendation token-by-token ───────────────────── # Build the same prompt the orchestrator would use, then stream it. profile_parts = [] if final_state.get("module2_risk_score"): profile_parts.append(f"- Risk Score: {final_state['module2_risk_score']}") if final_state.get("module3_wsi_analysis"): profile_parts.append(f"- Bone Marrow WSI: {final_state['module3_wsi_analysis']}") if final_state.get("module4_progression"): profile_parts.append(f"- Progression: {final_state['module4_progression']}") profile_block = "\n".join(profile_parts) if profile_parts else "No module findings." if not ran_m5: stream_prompt = ( f"You are a hematology AI assistant.\n" f"Answer the clinician's question concisely using only the findings below.\n" f"Do not speculate. Do not mention treatment guidelines.\n\n" f"[QUESTION]\n{user_query}\n\n" f"[FINDINGS]\n{profile_block}\n\n" f"Answer in 2-4 sentences:" ) max_tok = 250 else: guidelines_block = final_state.get("module5_guidelines", "") stream_prompt = ( f"You are the Master Hematology Orchestrator.\n" f"Answer the clinician's question using ONLY the data below.\n" f"Do not speculate. Do not show your reasoning.\n\n" f"CITATION RULE: After every treatment or guideline recommendation,\n" f"append the exact [SOURCE: ... | PAGE: ...] tag from the guidelines.\n" f"If no supporting guideline exists for a statement, write (no guideline available).\n\n" f"[QUESTION]\n{user_query}\n\n" f"[PATIENT DATA]\n{raw_clinical_text}\n\n" f"[MODULE FINDINGS]\n{profile_block}\n\n" f"[RETRIEVED GUIDELINES β€” cite exactly]\n{guidelines_block}\n\n" f"Final Answer (with citations):" ) max_tok = 800 progress(0.85, desc="Streaming final recommendation…") for streamed_text in _stream_generate(stream_prompt, max_tokens=max_tok): yield _build_yield( modules_str = modules_str, recommendation = streamed_text, risk = final_state.get("module2_risk_score", ""), show_m2 = ran_m2, wsi_text = final_state.get("module3_wsi_analysis", ""), show_m3 = ran_m3, img = annotated_img, mal_stat = mal_stat, prog = final_state.get("module4_progression", ""), show_m4 = ran_m4, rag = final_state.get("module5_guidelines", ""), show_m5 = ran_m5, m5_src = m5_src_update, raw_chunks = raw_chunks_update, ) progress(1.0, desc="Complete") EXAMPLE_QUERY = "What is the recommended treatment for this transplant-eligible myeloma patient with renal impairment?" EXAMPLE_LABS = ( "Creatinine: 2.5 mg/dL\nCalcium: 10.5 mg/dL\nHemoglobin: 9.0 g/dL\n" "Beta-2 Microglobulin: 5.8 mg/L\nLDH: 280 U/L\n" "M-Spike: increased 1.2 g/dL over 3 months\n" "Patient: 65-year-old Male. Transplant eligible." ) EXAMPLE_BEHAVIOUR = "Dizziness, Chest Pain, Mental Confusion" # ========================================== # CUSTOM CSS β€” Medical Luxury Dark Theme # ========================================== CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=IBM+Plex+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap'); :root { --bg-void: #080c14; --bg-deep: #0d1320; --bg-panel: #111827; --bg-card: #161f30; --bg-input: #1a2438; --bg-hover: #1e2d45; --border-dim: #1e2d45; --border-mid: #2a3f5f; --border-bright: #3d5a80; --text-ivory: #f0ead8; --text-muted: #8a9bb8; --text-faint: #4a5d7a; --accent-amber: #c8963c; --accent-amber-dim: #8a6422; --accent-teal: #3d9e8c; --font-display: 'Playfair Display', Georgia, serif; --font-data: 'IBM Plex Mono', 'Courier New', monospace; --font-body: 'IBM Plex Sans', system-ui, sans-serif; } *, *::before, *::after { box-sizing: border-box; } body, .gradio-container { background: var(--bg-void) !important; color: var(--text-ivory) !important; font-family: var(--font-body) !important; font-weight: 300 !important; } .gradio-container { max-width: 1400px !important; margin: 0 auto !important; padding: 0 24px 48px !important; } /* ── Hardware banner ──────────────────────────────────────────────────────── */ .hw-banner { background: linear-gradient(135deg, #0d1a2e 0%, #0a1520 100%) !important; border: 1px solid var(--accent-amber-dim) !important; border-left: 3px solid var(--accent-amber) !important; border-radius: 0 4px 4px 0 !important; padding: 12px 18px !important; margin-bottom: 20px !important; } .hw-banner p { color: var(--text-muted) !important; font-size: 0.82rem !important; line-height: 1.5 !important; margin: 0 !important; } .hw-banner strong { color: var(--accent-amber) !important; font-weight: 500 !important; } /* ── Header ───────────────────────────────────────────────────────────────── */ .header-block { border-bottom: 1px solid var(--border-mid); padding: 40px 0 28px; margin-bottom: 24px; position: relative; } .header-block::before { content: ''; position: absolute; bottom: -1px; left: 0; width: 80px; height: 2px; background: var(--accent-amber); } /* ── Progress bar ─────────────────────────────────────────────────────────── */ .progress-bar-wrap { background: var(--bg-card) !important; border: 1px solid var(--border-dim) !important; border-radius: 3px !important; overflow: hidden !important; } .progress-bar { background: var(--accent-amber) !important; height: 3px !important; transition: width 0.4s ease !important; } /* ── Markdown headings ────────────────────────────────────────────────────── */ .gradio-container h1, .prose h1 { font-family: var(--font-display) !important; font-size: 2.4rem !important; font-weight: 700 !important; color: var(--text-ivory) !important; letter-spacing: -0.02em !important; line-height: 1.15 !important; margin: 0 0 8px !important; } .gradio-container h2, .prose h2 { font-family: var(--font-display) !important; font-size: 1.35rem !important; font-weight: 600 !important; color: var(--accent-amber) !important; letter-spacing: 0.04em !important; text-transform: uppercase !important; margin: 32px 0 16px !important; padding-bottom: 8px !important; border-bottom: 1px solid var(--border-dim) !important; } .gradio-container h3, .prose h3 { font-family: var(--font-body) !important; font-size: 0.78rem !important; font-weight: 500 !important; color: var(--text-muted) !important; letter-spacing: 0.12em !important; text-transform: uppercase !important; margin: 24px 0 12px !important; } .gradio-container h4, .prose h4 { font-family: var(--font-body) !important; font-size: 0.72rem !important; font-weight: 500 !important; color: var(--accent-teal) !important; letter-spacing: 0.14em !important; text-transform: uppercase !important; margin: 20px 0 10px !important; } .gradio-container p, .prose p { color: var(--text-muted) !important; font-size: 0.88rem !important; line-height: 1.7 !important; } .gradio-container strong, .prose strong { color: var(--text-ivory) !important; font-weight: 500 !important; } /* ── Tabs ─────────────────────────────────────────────────────────────────── */ .tab-nav { background: var(--bg-deep) !important; border-bottom: 1px solid var(--border-dim) !important; padding: 0 !important; gap: 0 !important; } .tab-nav button { font-family: var(--font-body) !important; font-size: 0.75rem !important; font-weight: 500 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: var(--text-faint) !important; background: transparent !important; border: none !important; border-bottom: 2px solid transparent !important; padding: 16px 28px !important; transition: all 0.2s ease !important; border-radius: 0 !important; } .tab-nav button:hover { color: var(--text-muted) !important; background: var(--bg-hover) !important; } .tab-nav button.selected { color: var(--accent-amber) !important; border-bottom-color: var(--accent-amber) !important; background: transparent !important; } /* ── Panels ───────────────────────────────────────────────────────────────── */ .panel, .gradio-group, .gr-group { background: var(--bg-panel) !important; border: 1px solid var(--border-dim) !important; border-radius: 4px !important; padding: 20px !important; } /* ── Labels ───────────────────────────────────────────────────────────────── */ label span, .gradio-container label { font-family: var(--font-body) !important; font-size: 0.72rem !important; font-weight: 500 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: var(--text-muted) !important; } /* ── Inputs ───────────────────────────────────────────────────────────────── */ textarea, input[type="text"], .gradio-textbox textarea { background: var(--bg-input) !important; border: 1px solid var(--border-dim) !important; border-radius: 3px !important; color: var(--text-ivory) !important; font-family: var(--font-data) !important; font-size: 0.82rem !important; font-weight: 300 !important; line-height: 1.6 !important; padding: 12px 14px !important; transition: border-color 0.2s ease !important; caret-color: var(--accent-amber) !important; } textarea:focus, input[type="text"]:focus { border-color: var(--border-bright) !important; outline: none !important; box-shadow: 0 0 0 1px var(--accent-amber-dim) !important; } textarea::placeholder, input::placeholder { color: var(--text-faint) !important; font-style: italic !important; } /* ── Buttons ──────────────────────────────────────────────────────────────── */ button.primary, .gr-button-primary { background: var(--accent-amber) !important; border: none !important; border-radius: 3px !important; color: var(--bg-void) !important; font-family: var(--font-body) !important; font-size: 0.78rem !important; font-weight: 600 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; padding: 14px 32px !important; transition: all 0.2s ease !important; cursor: pointer !important; } button.primary:hover, .gr-button-primary:hover { background: #d9a94d !important; transform: translateY(-1px) !important; box-shadow: 0 4px 16px rgba(200, 150, 60, 0.25) !important; } button.secondary, .gr-button-secondary { background: transparent !important; border: 1px solid var(--border-mid) !important; border-radius: 3px !important; color: var(--text-muted) !important; font-family: var(--font-body) !important; font-size: 0.75rem !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; padding: 12px 24px !important; transition: all 0.2s ease !important; } button.secondary:hover { border-color: var(--border-bright) !important; color: var(--text-ivory) !important; } /* ── Checkbox ─────────────────────────────────────────────────────────────── */ .gradio-checkbox label { color: var(--text-muted) !important; font-size: 0.82rem !important; font-weight: 300 !important; letter-spacing: 0.01em !important; text-transform: none !important; line-height: 1.6 !important; } input[type="checkbox"] { accent-color: var(--accent-amber) !important; } /* ── File / Image upload ──────────────────────────────────────────────────── */ .gradio-file { background: var(--bg-input) !important; border: 1px dashed var(--border-mid) !important; border-radius: 4px !important; transition: border-color 0.2s !important; } .gradio-file:hover { border-color: var(--accent-amber-dim) !important; } .gradio-image { background: var(--bg-input) !important; border: 1px solid var(--border-dim) !important; border-radius: 4px !important; } /* ── Accordion ────────────────────────────────────────────────────────────── */ .gradio-accordion > .label-wrap { background: var(--bg-card) !important; border: 1px solid var(--border-dim) !important; border-radius: 3px !important; padding: 10px 16px !important; } .gradio-accordion > .label-wrap span { font-family: var(--font-body) !important; font-size: 0.72rem !important; font-weight: 500 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: var(--text-muted) !important; } /* ── Status box ───────────────────────────────────────────────────────────── */ .status-box { background: var(--bg-card) !important; border-left: 3px solid var(--accent-teal) !important; border-top: 1px solid var(--border-dim) !important; border-right: 1px solid var(--border-dim) !important; border-bottom: 1px solid var(--border-dim) !important; border-radius: 0 3px 3px 0 !important; } hr { border: none !important; border-top: 1px solid var(--border-dim) !important; margin: 28px 0 !important; } ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { background: var(--bg-deep); } ::-webkit-scrollbar-thumb { background: var(--border-mid); border-radius: 2px; } ::-webkit-scrollbar-thumb:hover { background: var(--border-bright); } .examples table { background: var(--bg-card) !important; border: 1px solid var(--border-dim) !important; border-radius: 3px !important; } .examples table td, .examples table th { color: var(--text-muted) !important; font-family: var(--font-data) !important; font-size: 0.78rem !important; border-color: var(--border-dim) !important; padding: 8px 12px !important; } .examples table tr:hover td { background: var(--bg-hover) !important; color: var(--text-ivory) !important; } small { color: var(--text-faint) !important; font-size: 0.75rem !important; } ::selection { background: var(--accent-amber-dim) !important; color: var(--text-ivory) !important; } blockquote { border-left: 3px solid var(--accent-amber-dim) !important; background: var(--bg-card) !important; margin: 0 0 12px !important; padding: 10px 16px !important; border-radius: 0 3px 3px 0 !important; } blockquote p { color: var(--text-muted) !important; font-size: 0.82rem !important; margin: 0 !important; } code { font-family: var(--font-data) !important; background: var(--bg-input) !important; color: var(--accent-teal) !important; padding: 2px 6px !important; border-radius: 2px !important; font-size: 0.82em !important; } """ # ========================================== # GRADIO UI # ========================================== with gr.Blocks( theme=gr.themes.Base( primary_hue=gr.themes.colors.slate, secondary_hue=gr.themes.colors.slate, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("IBM Plex Sans"), "system-ui", "sans-serif"], font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"], ), css=CUSTOM_CSS, title="VAJRAM β€” Clinical Decision Support" ) as demo: # ── Header ───────────────────────────────────────────────────────────────── with gr.Column(elem_classes=["header-block"]): gr.Markdown(""" # VAJRAM **V**irtual **A**gent for **J**oint **R**isk **A**ssessment of **M**ultiple Myeloma MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β· Air-gapped deployment """) with gr.Tabs(): # ====================================================== # TAB 1: CLINIC CONFIGURATION # ====================================================== with gr.Tab("βš™ Configuration"): gr.Markdown("## Knowledge Base Initialization") gr.Markdown( "Upload your institution's **legally licensed** oncology guideline document. " "This becomes the evidence base for all treatment recommendations." ) gr.Markdown( "> **Supported formats:** Text-based PDF only. Scanned documents are not supported. \n" "> **Recommended sources:** ESMO, NCCN, ICMR, or your institution's local protocol." ) kb_status_box = gr.Textbox( label="Knowledge Base Status", value=get_kb_status(), interactive=False, lines=1, elem_classes=["status-box"], ) gr.Markdown("---") with gr.Row(): with gr.Column(scale=2): admin_pdf_upload = gr.File( label="Oncology Guideline Document", file_types=[".pdf"], file_count="single", ) legal_checkbox = gr.Checkbox( label=( "I confirm: (1) my institution holds a valid license for this document, " "(2) I am authorised to upload it for institutional AI use, and " "(3) VAJRAM outputs are for clinical decision support only and must be " "verified by a licensed physician before any clinical action." ), value=False, ) init_btn = gr.Button("Initialize Knowledge Base", variant="primary", size="lg") with gr.Column(scale=1): gr.Markdown(""" ### Process Overview 1. Text extracted page-by-page 2. Split into 1000-char chunks with overlap 3. Embedded via local sentence-transformer 4. FAISS index saved to `./local_vector_store/` 5. All future consultations load instantly ### Estimated Processing Time - 10-page document: 30 – 60 seconds - 50-page document: 3 – 5 minutes - 200-page document: 10 – 15 minutes *Runs once. No cloud calls.* """) admin_status = gr.Textbox(label="Initialization Log", interactive=False, lines=6) init_btn.click( fn=initialize_knowledge_base, inputs=[admin_pdf_upload, legal_checkbox], outputs=admin_status, ).then(fn=get_kb_status, inputs=None, outputs=kb_status_box) # ====================================================== # TAB 2: PATIENT CONSULTATION # ====================================================== with gr.Tab("🩺 Patient Consultation"): # ── Hardware disclaimer banner ──────────────────────────────── with gr.Column(elem_classes=["hw-banner"]): gr.Markdown( "**Live demo running on a throttled 2-core CPU** to demonstrate edge-deployment capability. " "Inference takes ~2 minutes on this hardware. " "Enterprise GPU deployments process in **< 5 seconds**. " "The progress bar and streaming output below keep you informed throughout." ) gr.Markdown("## Patient Data Entry") with gr.Row(): with gr.Column(scale=2): input_query = gr.Textbox( lines=2, label="Clinical Question", placeholder="e.g. What is the recommended treatment for this transplant-eligible patient with renal impairment?", ) with gr.Row(): input_labs = gr.Textbox( lines=9, label="Laboratory Values & Clinical Data", placeholder="Creatinine, Calcium, Haemoglobin, Beta-2 Microglobulin, LDH, M-Spike, biopsy findings, staging...", ) input_behaviour = gr.Textbox( lines=9, label="Behavioural Symptoms", placeholder="e.g. Dizziness, Chest Pain, Mental Confusion, Fatigue, Peripheral Neuropathy...", ) with gr.Column(scale=1): gr.Markdown("### Bone Marrow Biopsy") input_wsi = gr.Image( type="pil", label="Whole Slide Image (.bmp / .png / .tiff)", height=220, ) gr.Markdown( "If no image is uploaded, Module 3 generates a " "text-based WSI summary from clinical notes." ) with gr.Row(): gr.Examples( examples=[[EXAMPLE_QUERY, EXAMPLE_LABS, EXAMPLE_BEHAVIOUR, None]], inputs=[input_query, input_labs, input_behaviour, input_wsi], label="Load Example Patient Record", ) submit_btn = gr.Button("Run Analysis", variant="primary", scale=0, min_width=180) # ── OUTPUT ──────────────────────────────────────────────────────── gr.Markdown("---") gr.Markdown("## Analysis Output") with gr.Group(): gr.Markdown("### Orchestrator β€” Module Pipeline & Final Recommendation") out_modules_selected = gr.Textbox( label="Active Modules", interactive=False, lines=1, ) out_final = gr.Textbox( label="Final Recommendation (streaming Β· citations in [SOURCE | PAGE] format)", interactive=False, lines=10, ) gr.Markdown("---") gr.Markdown("### Specialist Module Outputs") with gr.Group(visible=True) as grp_m2: gr.Markdown("#### Module II β€” Risk Stratification") out_risk = gr.Textbox(label="Risk Profile", interactive=False, lines=3, visible=False) with gr.Group(visible=True) as grp_m3: gr.Markdown("#### Module III β€” Bone Marrow Pathology") out_wsi_text = gr.Textbox(label="WSI Analysis Summary", interactive=False, lines=3, visible=False) out_wsi_img = gr.Image( label="AI-Annotated WSI (Red = Malignant Β· Green = Normal Β· Gray = Background Β· Yellow = Unknown)", type="pil", height=420, interactive=False, visible=False, ) out_malignancy_stat = gr.Textbox(label="Patch Classification Statistics", interactive=False, lines=2, visible=False) with gr.Group(visible=True) as grp_m4: gr.Markdown("#### Module IV β€” Disease Progression") out_prog = gr.Textbox(label="Progression Summary", interactive=False, lines=3, visible=False) with gr.Group(visible=True) as grp_m5: gr.Markdown("#### Module V β€” Guideline Retrieval (RAG)") out_m5_source = gr.Textbox(label="Evidence Source", interactive=False, lines=1, visible=False) out_rag = gr.Textbox( label="Retrieved Guideline Passages (with source citations)", interactive=False, lines=6, visible=False, ) with gr.Accordion("View Raw Reference Chunks", open=False): out_raw_chunks = gr.Textbox( label="Raw knowledge base excerpts β€” verify AI citations against these", interactive=False, lines=12, visible=False, ) submit_btn.click( fn=process_patient_data, inputs=[input_query, input_labs, input_behaviour, input_wsi], outputs=[ out_modules_selected, out_final, out_risk, grp_m2, out_wsi_text, grp_m3, out_wsi_img, out_malignancy_stat, out_prog, grp_m4, out_rag, grp_m5, out_m5_source, out_raw_chunks, ], ) demo.queue().launch( server_name="0.0.0.0", server_port=7860, share=False, ssr_mode=False, )