import os import re import gradio as gr from PIL import Image from cpu_agent import ( full_agent, save_retriever_from_pdf, load_persisted_retriever, VECTOR_STORE_PATH, ) # ========================================== # BACKEND: TAB 1 โ€” ADMIN KNOWLEDGE BASE # ========================================== def initialize_knowledge_base(pdf_file, legal_consent): if not legal_consent: yield "โ›” Cannot proceed: you must accept the legal consent declaration before initialising the knowledge base." return if pdf_file is None: yield "โ›” No PDF uploaded. Please upload your institution's licensed oncology guideline document." 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โณ Extracting text and building vector index โ€” please wait..." success, message = save_retriever_from_pdf(pdf_path) if success: yield ( f"{message}\n\n" f"โœ… Knowledge base is ready.\n" f"Clinicians can now use the Patient Consultation tab." ) else: yield ( f"{message}\n\n" f"โš ๏ธ Falling back to built-in NCCN/ESMO guideline excerpts." ) def get_kb_status(): r, label = load_persisted_retriever() if r: return f"โœ… Active: {label}" return "โš ๏ธ No knowledge base configured. Upload a PDF in the Configuration tab first." # ========================================== # BACKEND: TAB 2 โ€” PATIENT CONSULTATION # ========================================== def process_patient_data(user_query, lab_values, behaviour_changes, wsi_image): 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) initial_state = { "patient_id": "VAJRAM_UI", "user_query": user_query.strip() or "Give me a full clinical workup and treatment plan.", "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": "", } final_state = full_agent.invoke(initial_state) ran_module2 = bool(final_state.get("module2_risk_score", "").strip()) ran_module3 = bool(final_state.get("module3_wsi_analysis", "").strip()) ran_module4 = bool(final_state.get("module4_progression", "").strip()) ran_module5 = bool(final_state.get("module5_guidelines", "").strip()) selected = [] if ran_module2: selected.append("Module 2 ยท Risk Assessment") if ran_module3: selected.append("Module 3 ยท Bone Marrow WSI") if ran_module4: selected.append("Module 4 ยท Progression Tracking") if ran_module5: selected.append("Module 5 ยท Guideline Retrieval") modules_selected_str = " โ€บ ".join(selected) if selected else "None selected" annotated_img = None malignancy_stat = gr.update(visible=False) if ran_module3 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) malignancy_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: malignancy_stat = gr.update(value=wsi_summary, visible=True) except Exception: annotated_img = None m5_source = final_state.get("module5_source", "") m5_source_update = gr.update( value=f"Source: {m5_source}" if m5_source else "", visible=ran_module5 ) raw_chunks_update = gr.update( value=final_state.get("module5_raw_chunks", ""), visible=ran_module5 ) return ( modules_selected_str, final_state.get("final_recommendation", "No recommendation generated."), gr.update(value=final_state.get("module2_risk_score", ""), visible=ran_module2), gr.update(visible=ran_module2), gr.update(value=final_state.get("module3_wsi_analysis", ""), visible=ran_module3), gr.update(visible=ran_module3), gr.update(value=annotated_img, visible=annotated_img is not None), malignancy_stat, gr.update(value=final_state.get("module4_progression", ""), visible=ran_module4), gr.update(visible=ran_module4), gr.update(value=final_state.get("module5_guidelines", ""), visible=ran_module5), gr.update(visible=ran_module5), m5_source_update, raw_chunks_update, ) 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 # Fonts: Playfair Display (headers) + IBM Plex Mono (data) # Palette: Deep navy slate ยท Warm ivory text ยท Amber accent # ========================================== 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 palette โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ :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; --accent-teal-dim: #1e4f47; --danger: #c84c3c; --success: #3d9e5a; --font-display: 'Playfair Display', Georgia, serif; --font-data: 'IBM Plex Mono', 'Courier New', monospace; --font-body: 'IBM Plex Sans', system-ui, sans-serif; } /* โ”€โ”€ Global reset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ *, *::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; } /* โ”€โ”€ Header block โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .header-block { border-bottom: 1px solid var(--border-mid); padding: 40px 0 28px; margin-bottom: 32px; position: relative; } .header-block::before { content: ''; position: absolute; bottom: -1px; left: 0; width: 80px; height: 2px; background: var(--accent-amber); } /* โ”€โ”€ 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; } /* โ”€โ”€ Tab navigation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .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 and cards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .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; } /* โ”€โ”€ Text inputs and textareas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ 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; } /* Output textareas โ€” distinct from inputs */ .gradio-textbox[data-testid] textarea[readonly], textarea[disabled] { background: var(--bg-card) !important; border-color: var(--border-dim) !important; color: var(--text-ivory) !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; font-weight: 400 !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 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; } /* โ”€โ”€ Image upload โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .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; } .gradio-accordion > .label-wrap:hover { border-color: var(--border-mid) !important; } /* โ”€โ”€ Status / info boxes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .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; padding: 12px 16px !important; } /* โ”€โ”€ Dividers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ hr { border: none !important; border-top: 1px solid var(--border-dim) !important; margin: 28px 0 !important; } /* โ”€โ”€ Scrollbars โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ ::-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 row โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .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 helper text โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ small, .small-text { color: var(--text-faint) !important; font-size: 0.75rem !important; line-height: 1.5 !important; } /* โ”€โ”€ Selection color โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ ::selection { background: var(--accent-amber-dim) !important; color: var(--text-ivory) !important; } /* โ”€โ”€ Loading spinner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .generating { border-color: var(--accent-amber) !important; } /* โ”€โ”€ Blockquotes (used in description) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ 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 / monospace in descriptions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ 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 ยท """) 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 document will serve as 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 AI use within this institution, and " "(3) I understand that 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. Content split into 1000-char chunks with overlap 3. Embedded via local sentence-transformer model 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"): 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 will generate 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 Treatment Recommendation (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 Whole Slide Image (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 passages", 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, ], ) if __name__ == "__main__": demo.queue().launch(share=False)