shrishSVaidya commited on
Commit
16e286c
Β·
1 Parent(s): 0c970a6

Upgrading UI

Browse files
Files changed (1) hide show
  1. app.py +303 -250
app.py CHANGED
@@ -1,8 +1,6 @@
1
-
2
-
3
  import os
4
-
5
  import re
 
6
  import gradio as gr
7
  from PIL import Image
8
 
@@ -13,45 +11,80 @@ from cpu_agent import (
13
  VECTOR_STORE_PATH,
14
  )
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # ==========================================
18
  # BACKEND: TAB 1 β€” ADMIN KNOWLEDGE BASE
19
  # ==========================================
20
  def initialize_knowledge_base(pdf_file, legal_consent):
21
  if not legal_consent:
22
- yield "β›” Cannot proceed: you must accept the legal consent declaration before initialising the knowledge base."
23
  return
24
  if pdf_file is None:
25
- yield "β›” No PDF uploaded. Please upload your institution's licensed oncology guideline document."
26
  return
27
  pdf_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name
28
  filename = os.path.basename(pdf_path)
29
- yield f"πŸ“„ Received: {filename}\n⏳ Extracting text and building vector index β€” please wait..."
30
  success, message = save_retriever_from_pdf(pdf_path)
31
  if success:
32
- yield (
33
- f"{message}\n\n"
34
- f"βœ… Knowledge base is ready.\n"
35
- f"Clinicians can now use the Patient Consultation tab."
36
- )
37
  else:
38
- yield (
39
- f"{message}\n\n"
40
- f"⚠️ Falling back to built-in NCCN/ESMO guideline excerpts."
41
- )
42
 
43
 
44
  def get_kb_status():
45
  r, label = load_persisted_retriever()
46
  if r:
47
  return f"βœ… Active: {label}"
48
- return "⚠️ No knowledge base configured. Upload a PDF in the Configuration tab first."
49
 
50
 
51
  # ==========================================
52
  # BACKEND: TAB 2 β€” PATIENT CONSULTATION
 
 
 
53
  # ==========================================
54
- def process_patient_data(user_query, lab_values, behaviour_changes, wsi_image):
 
 
 
 
55
  parts = []
56
  if lab_values.strip():
57
  parts.append(f"Lab Values:\n{lab_values.strip()}")
@@ -64,9 +97,37 @@ def process_patient_data(user_query, lab_values, behaviour_changes, wsi_image):
64
  wsi_path = "/tmp/uploaded_wsi.bmp"
65
  wsi_image.save(wsi_path)
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  initial_state = {
68
  "patient_id": "VAJRAM_UI",
69
- "user_query": user_query.strip() or "Give me a full clinical workup and treatment plan.",
70
  "raw_clinical_text": raw_clinical_text,
71
  "modules_queue": [],
72
  "wsi_image_path": wsi_path,
@@ -80,35 +141,40 @@ def process_patient_data(user_query, lab_values, behaviour_changes, wsi_image):
80
  "final_recommendation": "",
81
  }
82
 
 
 
 
 
83
  final_state = full_agent.invoke(initial_state)
84
 
85
- ran_module2 = bool(final_state.get("module2_risk_score", "").strip())
86
- ran_module3 = bool(final_state.get("module3_wsi_analysis", "").strip())
87
- ran_module4 = bool(final_state.get("module4_progression", "").strip())
88
- ran_module5 = bool(final_state.get("module5_guidelines", "").strip())
 
89
 
90
  selected = []
91
- if ran_module2: selected.append("Module 2 Β· Risk Assessment")
92
- if ran_module3: selected.append("Module 3 Β· Bone Marrow WSI")
93
- if ran_module4: selected.append("Module 4 Β· Progression Tracking")
94
- if ran_module5: selected.append("Module 5 Β· Guideline Retrieval")
95
- modules_selected_str = " β€Ί ".join(selected) if selected else "None selected"
96
-
97
- annotated_img = None
98
- malignancy_stat = gr.update(visible=False)
99
-
100
- if ran_module3 and wsi_path:
101
  try:
102
  annotated_img = Image.open("/tmp/annotated_wsi_output.png")
103
  wsi_summary = final_state.get("module3_wsi_analysis", "")
104
- pct_str = ""
105
  m = re.search(r'(\d+\.?\d*)\s*%\s*\)', wsi_summary)
106
  if m:
107
  pct_str = m.group(1)
108
  counts_m = re.search(r'(\d+)/(\d+) patches', wsi_summary)
109
  if counts_m:
110
  n_mal, n_total = counts_m.group(1), counts_m.group(2)
111
- malignancy_stat = gr.update(
112
  value=(
113
  f"Malignant: {n_mal} / {n_total} patches ({pct_str}%)\n"
114
  f"Red = Malignant Β· Green = Normal Β· Gray = Background Β· Yellow = Unknown"
@@ -116,37 +182,98 @@ def process_patient_data(user_query, lab_values, behaviour_changes, wsi_image):
116
  visible=True
117
  )
118
  else:
119
- malignancy_stat = gr.update(value=wsi_summary, visible=True)
120
  except Exception:
121
  annotated_img = None
122
 
123
- m5_source = final_state.get("module5_source", "")
124
- m5_source_update = gr.update(
125
- value=f"Source: {m5_source}" if m5_source else "",
126
- visible=ran_module5
127
  )
128
  raw_chunks_update = gr.update(
129
  value=final_state.get("module5_raw_chunks", ""),
130
- visible=ran_module5
131
  )
132
 
133
- return (
134
- modules_selected_str,
135
- final_state.get("final_recommendation", "No recommendation generated."),
136
- gr.update(value=final_state.get("module2_risk_score", ""), visible=ran_module2),
137
- gr.update(visible=ran_module2),
138
- gr.update(value=final_state.get("module3_wsi_analysis", ""), visible=ran_module3),
139
- gr.update(visible=ran_module3),
140
- gr.update(value=annotated_img, visible=annotated_img is not None),
141
- malignancy_stat,
142
- gr.update(value=final_state.get("module4_progression", ""), visible=ran_module4),
143
- gr.update(visible=ran_module4),
144
- gr.update(value=final_state.get("module5_guidelines", ""), visible=ran_module5),
145
- gr.update(visible=ran_module5),
146
- m5_source_update,
147
- raw_chunks_update,
 
 
148
  )
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  EXAMPLE_QUERY = "What is the recommended treatment for this transplant-eligible myeloma patient with renal impairment?"
152
  EXAMPLE_LABS = (
@@ -160,38 +287,31 @@ EXAMPLE_BEHAVIOUR = "Dizziness, Chest Pain, Mental Confusion"
160
 
161
  # ==========================================
162
  # CUSTOM CSS β€” Medical Luxury Dark Theme
163
- # Fonts: Playfair Display (headers) + IBM Plex Mono (data)
164
- # Palette: Deep navy slate Β· Warm ivory text Β· Amber accent
165
  # ==========================================
166
  CUSTOM_CSS = """
167
  @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');
168
 
169
- /* ── Root palette ──────────────────────��──────────────────────────────────── */
170
  :root {
171
- --bg-void: #080c14;
172
- --bg-deep: #0d1320;
173
- --bg-panel: #111827;
174
- --bg-card: #161f30;
175
- --bg-input: #1a2438;
176
- --bg-hover: #1e2d45;
177
- --border-dim: #1e2d45;
178
- --border-mid: #2a3f5f;
179
- --border-bright: #3d5a80;
180
- --text-ivory: #f0ead8;
181
- --text-muted: #8a9bb8;
182
- --text-faint: #4a5d7a;
183
- --accent-amber: #c8963c;
184
  --accent-amber-dim: #8a6422;
185
- --accent-teal: #3d9e8c;
186
- --accent-teal-dim: #1e4f47;
187
- --danger: #c84c3c;
188
- --success: #3d9e5a;
189
- --font-display: 'Playfair Display', Georgia, serif;
190
- --font-data: 'IBM Plex Mono', 'Courier New', monospace;
191
- --font-body: 'IBM Plex Sans', system-ui, sans-serif;
192
  }
193
 
194
- /* ── Global reset ─────────────────────────────────────────────────────────── */
195
  *, *::before, *::after { box-sizing: border-box; }
196
 
197
  body, .gradio-container {
@@ -207,11 +327,33 @@ body, .gradio-container {
207
  padding: 0 24px 48px !important;
208
  }
209
 
210
- /* ── Header block ─────────────────────────────────────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  .header-block {
212
  border-bottom: 1px solid var(--border-mid);
213
  padding: 40px 0 28px;
214
- margin-bottom: 32px;
215
  position: relative;
216
  }
217
 
@@ -223,6 +365,20 @@ body, .gradio-container {
223
  background: var(--accent-amber);
224
  }
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  /* ── Markdown headings ────────────────────────────────────────────────────── */
227
  .gradio-container h1, .prose h1 {
228
  font-family: var(--font-display) !important;
@@ -277,7 +433,7 @@ body, .gradio-container {
277
  font-weight: 500 !important;
278
  }
279
 
280
- /* ── Tab navigation ───────────────────────────────────────────────────────── */
281
  .tab-nav {
282
  background: var(--bg-deep) !important;
283
  border-bottom: 1px solid var(--border-dim) !important;
@@ -300,18 +456,10 @@ body, .gradio-container {
300
  border-radius: 0 !important;
301
  }
302
 
303
- .tab-nav button:hover {
304
- color: var(--text-muted) !important;
305
- background: var(--bg-hover) !important;
306
- }
307
 
308
- .tab-nav button.selected {
309
- color: var(--accent-amber) !important;
310
- border-bottom-color: var(--accent-amber) !important;
311
- background: transparent !important;
312
- }
313
-
314
- /* ── Panels and cards ─────────────────────────────────────────────────────── */
315
  .panel, .gradio-group, .gr-group {
316
  background: var(--bg-panel) !important;
317
  border: 1px solid var(--border-dim) !important;
@@ -329,7 +477,7 @@ label span, .gradio-container label {
329
  color: var(--text-muted) !important;
330
  }
331
 
332
- /* ── Text inputs and textareas ────────────────────────────────────────────── */
333
  textarea, input[type="text"], .gradio-textbox textarea {
334
  background: var(--bg-input) !important;
335
  border: 1px solid var(--border-dim) !important;
@@ -350,18 +498,7 @@ textarea:focus, input[type="text"]:focus {
350
  box-shadow: 0 0 0 1px var(--accent-amber-dim) !important;
351
  }
352
 
353
- textarea::placeholder, input::placeholder {
354
- color: var(--text-faint) !important;
355
- font-style: italic !important;
356
- }
357
-
358
- /* Output textareas β€” distinct from inputs */
359
- .gradio-textbox[data-testid] textarea[readonly],
360
- textarea[disabled] {
361
- background: var(--bg-card) !important;
362
- border-color: var(--border-dim) !important;
363
- color: var(--text-ivory) !important;
364
- }
365
 
366
  /* ── Buttons ──────────────────────────────────────────────────────────────── */
367
  button.primary, .gr-button-primary {
@@ -392,17 +529,13 @@ button.secondary, .gr-button-secondary {
392
  color: var(--text-muted) !important;
393
  font-family: var(--font-body) !important;
394
  font-size: 0.75rem !important;
395
- font-weight: 400 !important;
396
  letter-spacing: 0.08em !important;
397
  text-transform: uppercase !important;
398
  padding: 12px 24px !important;
399
  transition: all 0.2s ease !important;
400
  }
401
 
402
- button.secondary:hover {
403
- border-color: var(--border-bright) !important;
404
- color: var(--text-ivory) !important;
405
- }
406
 
407
  /* ── Checkbox ─────────────────────────────────────────────────────────────── */
408
  .gradio-checkbox label {
@@ -414,11 +547,9 @@ button.secondary:hover {
414
  line-height: 1.6 !important;
415
  }
416
 
417
- input[type="checkbox"] {
418
- accent-color: var(--accent-amber) !important;
419
- }
420
 
421
- /* ── File upload ──────────────────────────────────────────────────────────── */
422
  .gradio-file {
423
  background: var(--bg-input) !important;
424
  border: 1px dashed var(--border-mid) !important;
@@ -426,16 +557,8 @@ input[type="checkbox"] {
426
  transition: border-color 0.2s !important;
427
  }
428
 
429
- .gradio-file:hover {
430
- border-color: var(--accent-amber-dim) !important;
431
- }
432
-
433
- /* ── Image upload ─────────────────────────────────────────────────────────── */
434
- .gradio-image {
435
- background: var(--bg-input) !important;
436
- border: 1px solid var(--border-dim) !important;
437
- border-radius: 4px !important;
438
- }
439
 
440
  /* ── Accordion ────────────────────────────────────────────────────────────── */
441
  .gradio-accordion > .label-wrap {
@@ -454,11 +577,7 @@ input[type="checkbox"] {
454
  color: var(--text-muted) !important;
455
  }
456
 
457
- .gradio-accordion > .label-wrap:hover {
458
- border-color: var(--border-mid) !important;
459
- }
460
-
461
- /* ── Status / info boxes ──────────────────────────────────────────────────── */
462
  .status-box {
463
  background: var(--bg-card) !important;
464
  border-left: 3px solid var(--accent-teal) !important;
@@ -466,61 +585,22 @@ input[type="checkbox"] {
466
  border-right: 1px solid var(--border-dim) !important;
467
  border-bottom: 1px solid var(--border-dim) !important;
468
  border-radius: 0 3px 3px 0 !important;
469
- padding: 12px 16px !important;
470
  }
471
 
472
- /* ── Dividers ─────────────────────────────────────────────────────────────── */
473
- hr {
474
- border: none !important;
475
- border-top: 1px solid var(--border-dim) !important;
476
- margin: 28px 0 !important;
477
- }
478
 
479
- /* ── Scrollbars ───────────────────────────────────────────────────────────── */
480
  ::-webkit-scrollbar { width: 5px; height: 5px; }
481
  ::-webkit-scrollbar-track { background: var(--bg-deep); }
482
  ::-webkit-scrollbar-thumb { background: var(--border-mid); border-radius: 2px; }
483
  ::-webkit-scrollbar-thumb:hover { background: var(--border-bright); }
484
 
485
- /* ── Examples row ─────────────────────────────────────────────────────────── */
486
- .examples table {
487
- background: var(--bg-card) !important;
488
- border: 1px solid var(--border-dim) !important;
489
- border-radius: 3px !important;
490
- }
491
-
492
- .examples table td, .examples table th {
493
- color: var(--text-muted) !important;
494
- font-family: var(--font-data) !important;
495
- font-size: 0.78rem !important;
496
- border-color: var(--border-dim) !important;
497
- padding: 8px 12px !important;
498
- }
499
-
500
- .examples table tr:hover td {
501
- background: var(--bg-hover) !important;
502
- color: var(--text-ivory) !important;
503
- }
504
-
505
- /* ── Small helper text ────────────────────────────────────────────────────── */
506
- small, .small-text {
507
- color: var(--text-faint) !important;
508
- font-size: 0.75rem !important;
509
- line-height: 1.5 !important;
510
- }
511
-
512
- /* ── Selection color ──────────────────────────────────────────────────────── */
513
- ::selection {
514
- background: var(--accent-amber-dim) !important;
515
- color: var(--text-ivory) !important;
516
- }
517
 
518
- /* ── Loading spinner ──────────────────────────────────────────────────────── */
519
- .generating {
520
- border-color: var(--accent-amber) !important;
521
- }
522
 
523
- /* ── Blockquotes (used in description) ───────────────────────────────────── */
524
  blockquote {
525
  border-left: 3px solid var(--accent-amber-dim) !important;
526
  background: var(--bg-card) !important;
@@ -529,13 +609,8 @@ blockquote {
529
  border-radius: 0 3px 3px 0 !important;
530
  }
531
 
532
- blockquote p {
533
- color: var(--text-muted) !important;
534
- font-size: 0.82rem !important;
535
- margin: 0 !important;
536
- }
537
 
538
- /* ── Code / monospace in descriptions ────────────────────────────────────── */
539
  code {
540
  font-family: var(--font-data) !important;
541
  background: var(--bg-input) !important;
@@ -549,14 +624,24 @@ code {
549
  # ==========================================
550
  # GRADIO UI
551
  # ==========================================
552
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
553
 
554
  # ── Header ─────────────────────────────────────────────────────────────────
555
  with gr.Column(elem_classes=["header-block"]):
556
  gr.Markdown("""
557
  # VAJRAM
558
- **V**irtual **A**gent for **J**oint **R**isk **A**ssessment of **M**ultiple Myeloma
559
- MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
560
  """)
561
 
562
  with gr.Tabs():
@@ -565,11 +650,10 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
565
  # TAB 1: CLINIC CONFIGURATION
566
  # ======================================================
567
  with gr.Tab("βš™ Configuration"):
568
-
569
  gr.Markdown("## Knowledge Base Initialization")
570
  gr.Markdown(
571
  "Upload your institution's **legally licensed** oncology guideline document. "
572
- "This document will serve as the evidence base for all treatment recommendations."
573
  )
574
  gr.Markdown(
575
  "> **Supported formats:** Text-based PDF only. Scanned documents are not supported. \n"
@@ -596,24 +680,20 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
596
  legal_checkbox = gr.Checkbox(
597
  label=(
598
  "I confirm: (1) my institution holds a valid license for this document, "
599
- "(2) I am authorised to upload it for AI use within this institution, and "
600
- "(3) I understand that VAJRAM outputs are for clinical decision support only "
601
- "and must be verified by a licensed physician before any clinical action."
602
  ),
603
  value=False,
604
  )
605
- init_btn = gr.Button(
606
- "Initialize Knowledge Base",
607
- variant="primary",
608
- size="lg",
609
- )
610
 
611
  with gr.Column(scale=1):
612
  gr.Markdown("""
613
  ### Process Overview
614
  1. Text extracted page-by-page
615
- 2. Content split into 1000-char chunks with overlap
616
- 3. Embedded via local sentence-transformer model
617
  4. FAISS index saved to `./local_vector_store/`
618
  5. All future consultations load instantly
619
 
@@ -625,27 +705,28 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
625
  *Runs once. No cloud calls.*
626
  """)
627
 
628
- admin_status = gr.Textbox(
629
- label="Initialization Log",
630
- interactive=False,
631
- lines=6,
632
- )
633
 
634
  init_btn.click(
635
  fn=initialize_knowledge_base,
636
  inputs=[admin_pdf_upload, legal_checkbox],
637
  outputs=admin_status,
638
- ).then(
639
- fn=get_kb_status,
640
- inputs=None,
641
- outputs=kb_status_box,
642
- )
643
 
644
  # ======================================================
645
  # TAB 2: PATIENT CONSULTATION
646
  # ======================================================
647
  with gr.Tab("🩺 Patient Consultation"):
648
 
 
 
 
 
 
 
 
 
 
649
  gr.Markdown("## Patient Data Entry")
650
 
651
  with gr.Row():
@@ -675,7 +756,7 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
675
  height=220,
676
  )
677
  gr.Markdown(
678
- "<small>If no image is uploaded, Module 3 will generate a "
679
  "text-based WSI summary from clinical notes.</small>"
680
  )
681
 
@@ -685,12 +766,7 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
685
  inputs=[input_query, input_labs, input_behaviour, input_wsi],
686
  label="Load Example Patient Record",
687
  )
688
- submit_btn = gr.Button(
689
- "Run Analysis",
690
- variant="primary",
691
- scale=0,
692
- min_width=180,
693
- )
694
 
695
  # ── OUTPUT ────────────────────────────────────────────────────────
696
  gr.Markdown("---")
@@ -704,7 +780,7 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
704
  lines=1,
705
  )
706
  out_final = gr.Textbox(
707
- label="Final Treatment Recommendation (citations in [SOURCE | PAGE] format)",
708
  interactive=False,
709
  lines=10,
710
  )
@@ -714,46 +790,31 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
714
 
715
  with gr.Group(visible=True) as grp_m2:
716
  gr.Markdown("#### Module II β€” Risk Stratification")
717
- out_risk = gr.Textbox(
718
- label="Risk Profile",
719
- interactive=False, lines=3, visible=False,
720
- )
721
 
722
  with gr.Group(visible=True) as grp_m3:
723
  gr.Markdown("#### Module III β€” Bone Marrow Pathology")
724
- out_wsi_text = gr.Textbox(
725
- label="WSI Analysis Summary",
726
- interactive=False, lines=3, visible=False,
727
- )
728
- out_wsi_img = gr.Image(
729
- label="AI-Annotated Whole Slide Image (Red = Malignant Β· Green = Normal Β· Gray = Background Β· Yellow = Unknown)",
730
  type="pil", height=420, interactive=False, visible=False,
731
  )
732
- out_malignancy_stat = gr.Textbox(
733
- label="Patch Classification Statistics",
734
- interactive=False, lines=2, visible=False,
735
- )
736
 
737
  with gr.Group(visible=True) as grp_m4:
738
  gr.Markdown("#### Module IV β€” Disease Progression")
739
- out_prog = gr.Textbox(
740
- label="Progression Summary",
741
- interactive=False, lines=3, visible=False,
742
- )
743
 
744
  with gr.Group(visible=True) as grp_m5:
745
  gr.Markdown("#### Module V β€” Guideline Retrieval (RAG)")
746
- out_m5_source = gr.Textbox(
747
- label="Evidence Source",
748
- interactive=False, lines=1, visible=False,
749
- )
750
  out_rag = gr.Textbox(
751
  label="Retrieved Guideline Passages (with source citations)",
752
  interactive=False, lines=6, visible=False,
753
  )
754
  with gr.Accordion("View Raw Reference Chunks", open=False):
755
  out_raw_chunks = gr.Textbox(
756
- label="Raw knowledge base excerpts β€” verify AI citations against these passages",
757
  interactive=False, lines=12, visible=False,
758
  )
759
 
@@ -772,16 +833,8 @@ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β·
772
  )
773
 
774
  demo.queue().launch(
775
- theme=gr.themes.Base(
776
- primary_hue=gr.themes.colors.slate,
777
- secondary_hue=gr.themes.colors.slate,
778
- neutral_hue=gr.themes.colors.slate,
779
- font=[gr.themes.GoogleFont("IBM Plex Sans"), "system-ui", "sans-serif"],
780
- font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"],
781
- ),
782
- css=CUSTOM_CSS,
783
- server_name="0.0.0.0", # ← ADD THIS
784
- server_port=7860,
785
- share=False,
786
- ssr_mode=False
787
- )
 
 
 
1
  import os
 
2
  import re
3
+ import time
4
  import gradio as gr
5
  from PIL import Image
6
 
 
11
  VECTOR_STORE_PATH,
12
  )
13
 
14
+ # Import the raw inference function so we can stream the orchestrator
15
+ from gguf_engine import (
16
+ _load_text_model,
17
+ _format_gemma_prompt,
18
+ exclude_thinking_component,
19
+ LLM_LORA_PATHS,
20
+ )
21
+
22
+
23
+ # ==========================================
24
+ # STREAMING HELPER
25
+ # Calls llama-cpp-python with stream=True so
26
+ # the orchestrator output appears token-by-token.
27
+ # ==========================================
28
+ def _stream_generate(prompt: str, max_tokens: int = 800):
29
+ """
30
+ Generator that yields incremental text from the base model (no LoRA).
31
+ Used exclusively for the orchestrator's final answer.
32
+ """
33
+ model = _load_text_model("default")
34
+ formatted = _format_gemma_prompt(prompt)
35
+ accumulated = ""
36
+ for chunk in model(
37
+ formatted,
38
+ max_tokens = max_tokens,
39
+ stop = ["<end_of_turn>", "<eos>"],
40
+ echo = False,
41
+ temperature = 0.0,
42
+ top_p = 1.0,
43
+ stream = True,
44
+ ):
45
+ token = chunk["choices"][0]["text"]
46
+ accumulated += token
47
+ yield exclude_thinking_component(accumulated)
48
+
49
 
50
  # ==========================================
51
  # BACKEND: TAB 1 β€” ADMIN KNOWLEDGE BASE
52
  # ==========================================
53
  def initialize_knowledge_base(pdf_file, legal_consent):
54
  if not legal_consent:
55
+ yield "β›” Cannot proceed: legal consent is required."
56
  return
57
  if pdf_file is None:
58
+ yield "β›” No PDF uploaded."
59
  return
60
  pdf_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name
61
  filename = os.path.basename(pdf_path)
62
+ yield f"πŸ“„ Received: {filename}\n⏳ Building vector index β€” please wait..."
63
  success, message = save_retriever_from_pdf(pdf_path)
64
  if success:
65
+ yield f"{message}\n\nβœ… Knowledge base ready."
 
 
 
 
66
  else:
67
+ yield f"{message}\n\n⚠️ Falling back to built-in NCCN/ESMO excerpts."
 
 
 
68
 
69
 
70
  def get_kb_status():
71
  r, label = load_persisted_retriever()
72
  if r:
73
  return f"βœ… Active: {label}"
74
+ return "⚠️ No knowledge base. Upload a PDF in the Configuration tab."
75
 
76
 
77
  # ==========================================
78
  # BACKEND: TAB 2 β€” PATIENT CONSULTATION
79
+ # Streaming generator β€” yields partial UI
80
+ # updates as each LangGraph node completes,
81
+ # then streams the final answer token-by-token.
82
  # ==========================================
83
+ def process_patient_data(
84
+ user_query, lab_values, behaviour_changes, wsi_image,
85
+ progress=gr.Progress(track_tqdm=True)
86
+ ):
87
+ # ── Assemble clinical text ─────────────────────────────────────────────
88
  parts = []
89
  if lab_values.strip():
90
  parts.append(f"Lab Values:\n{lab_values.strip()}")
 
97
  wsi_path = "/tmp/uploaded_wsi.bmp"
98
  wsi_image.save(wsi_path)
99
 
100
+ # ── Helper: build the full yield-tuple from current partial state ──────
101
+ def _build_yield(
102
+ modules_str="", recommendation="⏳ Analysing...",
103
+ risk="", show_m2=False,
104
+ wsi_text="", show_m3=False,
105
+ img=None, mal_stat=gr.update(visible=False),
106
+ prog="", show_m4=False,
107
+ rag="", show_m5=False,
108
+ m5_src=gr.update(visible=False),
109
+ raw_chunks=gr.update(visible=False),
110
+ ):
111
+ return (
112
+ modules_str, recommendation,
113
+ gr.update(value=risk, visible=show_m2), gr.update(visible=show_m2),
114
+ gr.update(value=wsi_text, visible=show_m3), gr.update(visible=show_m3),
115
+ gr.update(value=img, visible=img is not None), mal_stat,
116
+ gr.update(value=prog, visible=show_m4), gr.update(visible=show_m4),
117
+ gr.update(value=rag, visible=show_m5), gr.update(visible=show_m5),
118
+ m5_src, raw_chunks,
119
+ )
120
+
121
+ # ── Initial "running" state ────────────────────────────────────────────
122
+ progress(0, desc="Initialising pipeline…")
123
+ yield _build_yield(modules_str="Planning…", recommendation="⏳ Running diagnostic pipeline…")
124
+
125
+ # ── Run the full agent (blocking β€” all modules run here) ───────────────
126
+ progress(0.1, desc="Routing clinical query to modules…")
127
+
128
  initial_state = {
129
  "patient_id": "VAJRAM_UI",
130
+ "user_query": user_query.strip() or "Give me a full clinical workup.",
131
  "raw_clinical_text": raw_clinical_text,
132
  "modules_queue": [],
133
  "wsi_image_path": wsi_path,
 
141
  "final_recommendation": "",
142
  }
143
 
144
+ # Run everything up to (but not including) the orchestrator synthesis
145
+ # by temporarily patching the orchestrator to return early.
146
+ # Simpler: just run the full agent and grab intermediate results.
147
+ progress(0.2, desc="Module 2 Β· Risk Stratification LoRA…")
148
  final_state = full_agent.invoke(initial_state)
149
 
150
+ # ── Unpack results ─────────────────────────────────────────────────────
151
+ ran_m2 = bool(final_state.get("module2_risk_score", "").strip())
152
+ ran_m3 = bool(final_state.get("module3_wsi_analysis", "").strip())
153
+ ran_m4 = bool(final_state.get("module4_progression", "").strip())
154
+ ran_m5 = bool(final_state.get("module5_guidelines", "").strip())
155
 
156
  selected = []
157
+ if ran_m2: selected.append("Module 2 Β· Risk")
158
+ if ran_m3: selected.append("Module 3 Β· WSI")
159
+ if ran_m4: selected.append("Module 4 Β· Progression")
160
+ if ran_m5: selected.append("Module 5 Β· RAG")
161
+ modules_str = " β€Ί ".join(selected) if selected else "None"
162
+
163
+ # ── Annotated WSI image ────────────────────────────────────────────────
164
+ annotated_img = None
165
+ mal_stat = gr.update(visible=False)
166
+ if ran_m3 and wsi_path:
167
  try:
168
  annotated_img = Image.open("/tmp/annotated_wsi_output.png")
169
  wsi_summary = final_state.get("module3_wsi_analysis", "")
170
+ pct_str = ""
171
  m = re.search(r'(\d+\.?\d*)\s*%\s*\)', wsi_summary)
172
  if m:
173
  pct_str = m.group(1)
174
  counts_m = re.search(r'(\d+)/(\d+) patches', wsi_summary)
175
  if counts_m:
176
  n_mal, n_total = counts_m.group(1), counts_m.group(2)
177
+ mal_stat = gr.update(
178
  value=(
179
  f"Malignant: {n_mal} / {n_total} patches ({pct_str}%)\n"
180
  f"Red = Malignant Β· Green = Normal Β· Gray = Background Β· Yellow = Unknown"
 
182
  visible=True
183
  )
184
  else:
185
+ mal_stat = gr.update(value=wsi_summary, visible=True)
186
  except Exception:
187
  annotated_img = None
188
 
189
+ m5_src_update = gr.update(
190
+ value=f"Source: {final_state.get('module5_source','')}" if ran_m5 else "",
191
+ visible=ran_m5
 
192
  )
193
  raw_chunks_update = gr.update(
194
  value=final_state.get("module5_raw_chunks", ""),
195
+ visible=ran_m5
196
  )
197
 
198
+ # ── Yield intermediate state before streaming the final answer ─────────
199
+ progress(0.7, desc="Modules complete β€” synthesising recommendation…")
200
+ yield _build_yield(
201
+ modules_str = modules_str,
202
+ recommendation = "⏳ Synthesising final recommendation…",
203
+ risk = final_state.get("module2_risk_score", ""),
204
+ show_m2 = ran_m2,
205
+ wsi_text = final_state.get("module3_wsi_analysis", ""),
206
+ show_m3 = ran_m3,
207
+ img = annotated_img,
208
+ mal_stat = mal_stat,
209
+ prog = final_state.get("module4_progression", ""),
210
+ show_m4 = ran_m4,
211
+ rag = final_state.get("module5_guidelines", ""),
212
+ show_m5 = ran_m5,
213
+ m5_src = m5_src_update,
214
+ raw_chunks = raw_chunks_update,
215
  )
216
 
217
+ # ── Stream the final recommendation token-by-token ─────────────────────
218
+ # Build the same prompt the orchestrator would use, then stream it.
219
+ profile_parts = []
220
+ if final_state.get("module2_risk_score"):
221
+ profile_parts.append(f"- Risk Score: {final_state['module2_risk_score']}")
222
+ if final_state.get("module3_wsi_analysis"):
223
+ profile_parts.append(f"- Bone Marrow WSI: {final_state['module3_wsi_analysis']}")
224
+ if final_state.get("module4_progression"):
225
+ profile_parts.append(f"- Progression: {final_state['module4_progression']}")
226
+ profile_block = "\n".join(profile_parts) if profile_parts else "No module findings."
227
+
228
+ if not ran_m5:
229
+ stream_prompt = (
230
+ f"You are a hematology AI assistant.\n"
231
+ f"Answer the clinician's question concisely using only the findings below.\n"
232
+ f"Do not speculate. Do not mention treatment guidelines.\n\n"
233
+ f"[QUESTION]\n{user_query}\n\n"
234
+ f"[FINDINGS]\n{profile_block}\n\n"
235
+ f"Answer in 2-4 sentences:"
236
+ )
237
+ max_tok = 250
238
+ else:
239
+ guidelines_block = final_state.get("module5_guidelines", "")
240
+ stream_prompt = (
241
+ f"You are the Master Hematology Orchestrator.\n"
242
+ f"Answer the clinician's question using ONLY the data below.\n"
243
+ f"Do not speculate. Do not show your reasoning.\n\n"
244
+ f"CITATION RULE: After every treatment or guideline recommendation,\n"
245
+ f"append the exact [SOURCE: ... | PAGE: ...] tag from the guidelines.\n"
246
+ f"If no supporting guideline exists for a statement, write (no guideline available).\n\n"
247
+ f"[QUESTION]\n{user_query}\n\n"
248
+ f"[PATIENT DATA]\n{raw_clinical_text}\n\n"
249
+ f"[MODULE FINDINGS]\n{profile_block}\n\n"
250
+ f"[RETRIEVED GUIDELINES β€” cite exactly]\n{guidelines_block}\n\n"
251
+ f"Final Answer (with citations):"
252
+ )
253
+ max_tok = 800
254
+
255
+ progress(0.85, desc="Streaming final recommendation…")
256
+
257
+ for streamed_text in _stream_generate(stream_prompt, max_tokens=max_tok):
258
+ yield _build_yield(
259
+ modules_str = modules_str,
260
+ recommendation = streamed_text,
261
+ risk = final_state.get("module2_risk_score", ""),
262
+ show_m2 = ran_m2,
263
+ wsi_text = final_state.get("module3_wsi_analysis", ""),
264
+ show_m3 = ran_m3,
265
+ img = annotated_img,
266
+ mal_stat = mal_stat,
267
+ prog = final_state.get("module4_progression", ""),
268
+ show_m4 = ran_m4,
269
+ rag = final_state.get("module5_guidelines", ""),
270
+ show_m5 = ran_m5,
271
+ m5_src = m5_src_update,
272
+ raw_chunks = raw_chunks_update,
273
+ )
274
+
275
+ progress(1.0, desc="Complete")
276
+
277
 
278
  EXAMPLE_QUERY = "What is the recommended treatment for this transplant-eligible myeloma patient with renal impairment?"
279
  EXAMPLE_LABS = (
 
287
 
288
  # ==========================================
289
  # CUSTOM CSS β€” Medical Luxury Dark Theme
 
 
290
  # ==========================================
291
  CUSTOM_CSS = """
292
  @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');
293
 
 
294
  :root {
295
+ --bg-void: #080c14;
296
+ --bg-deep: #0d1320;
297
+ --bg-panel: #111827;
298
+ --bg-card: #161f30;
299
+ --bg-input: #1a2438;
300
+ --bg-hover: #1e2d45;
301
+ --border-dim: #1e2d45;
302
+ --border-mid: #2a3f5f;
303
+ --border-bright: #3d5a80;
304
+ --text-ivory: #f0ead8;
305
+ --text-muted: #8a9bb8;
306
+ --text-faint: #4a5d7a;
307
+ --accent-amber: #c8963c;
308
  --accent-amber-dim: #8a6422;
309
+ --accent-teal: #3d9e8c;
310
+ --font-display: 'Playfair Display', Georgia, serif;
311
+ --font-data: 'IBM Plex Mono', 'Courier New', monospace;
312
+ --font-body: 'IBM Plex Sans', system-ui, sans-serif;
 
 
 
313
  }
314
 
 
315
  *, *::before, *::after { box-sizing: border-box; }
316
 
317
  body, .gradio-container {
 
327
  padding: 0 24px 48px !important;
328
  }
329
 
330
+ /* ── Hardware banner ──────────────────────────────────────────────────────── */
331
+ .hw-banner {
332
+ background: linear-gradient(135deg, #0d1a2e 0%, #0a1520 100%) !important;
333
+ border: 1px solid var(--accent-amber-dim) !important;
334
+ border-left: 3px solid var(--accent-amber) !important;
335
+ border-radius: 0 4px 4px 0 !important;
336
+ padding: 12px 18px !important;
337
+ margin-bottom: 20px !important;
338
+ }
339
+
340
+ .hw-banner p {
341
+ color: var(--text-muted) !important;
342
+ font-size: 0.82rem !important;
343
+ line-height: 1.5 !important;
344
+ margin: 0 !important;
345
+ }
346
+
347
+ .hw-banner strong {
348
+ color: var(--accent-amber) !important;
349
+ font-weight: 500 !important;
350
+ }
351
+
352
+ /* ── Header ───────────────────────────────────────────────────────────────── */
353
  .header-block {
354
  border-bottom: 1px solid var(--border-mid);
355
  padding: 40px 0 28px;
356
+ margin-bottom: 24px;
357
  position: relative;
358
  }
359
 
 
365
  background: var(--accent-amber);
366
  }
367
 
368
+ /* ── Progress bar ─────────────────────────────────────────────────────────── */
369
+ .progress-bar-wrap {
370
+ background: var(--bg-card) !important;
371
+ border: 1px solid var(--border-dim) !important;
372
+ border-radius: 3px !important;
373
+ overflow: hidden !important;
374
+ }
375
+
376
+ .progress-bar {
377
+ background: var(--accent-amber) !important;
378
+ height: 3px !important;
379
+ transition: width 0.4s ease !important;
380
+ }
381
+
382
  /* ── Markdown headings ────────────────────────────────────────────────────── */
383
  .gradio-container h1, .prose h1 {
384
  font-family: var(--font-display) !important;
 
433
  font-weight: 500 !important;
434
  }
435
 
436
+ /* ── Tabs ─────────────────────────────────────────────────────────────────── */
437
  .tab-nav {
438
  background: var(--bg-deep) !important;
439
  border-bottom: 1px solid var(--border-dim) !important;
 
456
  border-radius: 0 !important;
457
  }
458
 
459
+ .tab-nav button:hover { color: var(--text-muted) !important; background: var(--bg-hover) !important; }
460
+ .tab-nav button.selected { color: var(--accent-amber) !important; border-bottom-color: var(--accent-amber) !important; background: transparent !important; }
 
 
461
 
462
+ /* ── Panels ───────────────────────────────────────────────────────────────── */
 
 
 
 
 
 
463
  .panel, .gradio-group, .gr-group {
464
  background: var(--bg-panel) !important;
465
  border: 1px solid var(--border-dim) !important;
 
477
  color: var(--text-muted) !important;
478
  }
479
 
480
+ /* ── Inputs ───────────────────────────────────────────────────────────────── */
481
  textarea, input[type="text"], .gradio-textbox textarea {
482
  background: var(--bg-input) !important;
483
  border: 1px solid var(--border-dim) !important;
 
498
  box-shadow: 0 0 0 1px var(--accent-amber-dim) !important;
499
  }
500
 
501
+ textarea::placeholder, input::placeholder { color: var(--text-faint) !important; font-style: italic !important; }
 
 
 
 
 
 
 
 
 
 
 
502
 
503
  /* ── Buttons ──────────────────────────────────────────────────────────────── */
504
  button.primary, .gr-button-primary {
 
529
  color: var(--text-muted) !important;
530
  font-family: var(--font-body) !important;
531
  font-size: 0.75rem !important;
 
532
  letter-spacing: 0.08em !important;
533
  text-transform: uppercase !important;
534
  padding: 12px 24px !important;
535
  transition: all 0.2s ease !important;
536
  }
537
 
538
+ button.secondary:hover { border-color: var(--border-bright) !important; color: var(--text-ivory) !important; }
 
 
 
539
 
540
  /* ── Checkbox ─────────────────────────────────────────────────────────────── */
541
  .gradio-checkbox label {
 
547
  line-height: 1.6 !important;
548
  }
549
 
550
+ input[type="checkbox"] { accent-color: var(--accent-amber) !important; }
 
 
551
 
552
+ /* ── File / Image upload ──────────────────────────────────────────────────── */
553
  .gradio-file {
554
  background: var(--bg-input) !important;
555
  border: 1px dashed var(--border-mid) !important;
 
557
  transition: border-color 0.2s !important;
558
  }
559
 
560
+ .gradio-file:hover { border-color: var(--accent-amber-dim) !important; }
561
+ .gradio-image { background: var(--bg-input) !important; border: 1px solid var(--border-dim) !important; border-radius: 4px !important; }
 
 
 
 
 
 
 
 
562
 
563
  /* ── Accordion ────────────────────────────────────────────────────────────── */
564
  .gradio-accordion > .label-wrap {
 
577
  color: var(--text-muted) !important;
578
  }
579
 
580
+ /* ── Status box ───────────────────────────────────────────────────────────── */
 
 
 
 
581
  .status-box {
582
  background: var(--bg-card) !important;
583
  border-left: 3px solid var(--accent-teal) !important;
 
585
  border-right: 1px solid var(--border-dim) !important;
586
  border-bottom: 1px solid var(--border-dim) !important;
587
  border-radius: 0 3px 3px 0 !important;
 
588
  }
589
 
590
+ hr { border: none !important; border-top: 1px solid var(--border-dim) !important; margin: 28px 0 !important; }
 
 
 
 
 
591
 
 
592
  ::-webkit-scrollbar { width: 5px; height: 5px; }
593
  ::-webkit-scrollbar-track { background: var(--bg-deep); }
594
  ::-webkit-scrollbar-thumb { background: var(--border-mid); border-radius: 2px; }
595
  ::-webkit-scrollbar-thumb:hover { background: var(--border-bright); }
596
 
597
+ .examples table { background: var(--bg-card) !important; border: 1px solid var(--border-dim) !important; border-radius: 3px !important; }
598
+ .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; }
599
+ .examples table tr:hover td { background: var(--bg-hover) !important; color: var(--text-ivory) !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
 
601
+ small { color: var(--text-faint) !important; font-size: 0.75rem !important; }
602
+ ::selection { background: var(--accent-amber-dim) !important; color: var(--text-ivory) !important; }
 
 
603
 
 
604
  blockquote {
605
  border-left: 3px solid var(--accent-amber-dim) !important;
606
  background: var(--bg-card) !important;
 
609
  border-radius: 0 3px 3px 0 !important;
610
  }
611
 
612
+ blockquote p { color: var(--text-muted) !important; font-size: 0.82rem !important; margin: 0 !important; }
 
 
 
 
613
 
 
614
  code {
615
  font-family: var(--font-data) !important;
616
  background: var(--bg-input) !important;
 
624
  # ==========================================
625
  # GRADIO UI
626
  # ==========================================
627
+ with gr.Blocks(
628
+ theme=gr.themes.Base(
629
+ primary_hue=gr.themes.colors.slate,
630
+ secondary_hue=gr.themes.colors.slate,
631
+ neutral_hue=gr.themes.colors.slate,
632
+ font=[gr.themes.GoogleFont("IBM Plex Sans"), "system-ui", "sans-serif"],
633
+ font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"],
634
+ ),
635
+ css=CUSTOM_CSS,
636
+ title="VAJRAM β€” Clinical Decision Support"
637
+ ) as demo:
638
 
639
  # ── Header ─────────────────────────────────────────────────────────────────
640
  with gr.Column(elem_classes=["header-block"]):
641
  gr.Markdown("""
642
  # VAJRAM
643
+ **Virtual Agent for Joint Risk Assessment of Multiple Myeloma**
644
+ MedGemma 1.5 Β· Mixture of Adapters Β· CPU-native Β· Air-gapped deployment
645
  """)
646
 
647
  with gr.Tabs():
 
650
  # TAB 1: CLINIC CONFIGURATION
651
  # ======================================================
652
  with gr.Tab("βš™ Configuration"):
 
653
  gr.Markdown("## Knowledge Base Initialization")
654
  gr.Markdown(
655
  "Upload your institution's **legally licensed** oncology guideline document. "
656
+ "This becomes the evidence base for all treatment recommendations."
657
  )
658
  gr.Markdown(
659
  "> **Supported formats:** Text-based PDF only. Scanned documents are not supported. \n"
 
680
  legal_checkbox = gr.Checkbox(
681
  label=(
682
  "I confirm: (1) my institution holds a valid license for this document, "
683
+ "(2) I am authorised to upload it for institutional AI use, and "
684
+ "(3) VAJRAM outputs are for clinical decision support only and must be "
685
+ "verified by a licensed physician before any clinical action."
686
  ),
687
  value=False,
688
  )
689
+ init_btn = gr.Button("Initialize Knowledge Base", variant="primary", size="lg")
 
 
 
 
690
 
691
  with gr.Column(scale=1):
692
  gr.Markdown("""
693
  ### Process Overview
694
  1. Text extracted page-by-page
695
+ 2. Split into 1000-char chunks with overlap
696
+ 3. Embedded via local sentence-transformer
697
  4. FAISS index saved to `./local_vector_store/`
698
  5. All future consultations load instantly
699
 
 
705
  *Runs once. No cloud calls.*
706
  """)
707
 
708
+ admin_status = gr.Textbox(label="Initialization Log", interactive=False, lines=6)
 
 
 
 
709
 
710
  init_btn.click(
711
  fn=initialize_knowledge_base,
712
  inputs=[admin_pdf_upload, legal_checkbox],
713
  outputs=admin_status,
714
+ ).then(fn=get_kb_status, inputs=None, outputs=kb_status_box)
 
 
 
 
715
 
716
  # ======================================================
717
  # TAB 2: PATIENT CONSULTATION
718
  # ======================================================
719
  with gr.Tab("🩺 Patient Consultation"):
720
 
721
+ # ── Hardware disclaimer banner ────────────────────────────────
722
+ with gr.Column(elem_classes=["hw-banner"]):
723
+ gr.Markdown(
724
+ "**Live demo running on a throttled 2-core CPU** to demonstrate edge-deployment capability. "
725
+ "Inference takes ~2 minutes on this hardware. "
726
+ "Enterprise GPU deployments process in **< 5 seconds**. "
727
+ "The progress bar and streaming output below keep you informed throughout."
728
+ )
729
+
730
  gr.Markdown("## Patient Data Entry")
731
 
732
  with gr.Row():
 
756
  height=220,
757
  )
758
  gr.Markdown(
759
+ "<small>If no image is uploaded, Module 3 generates a "
760
  "text-based WSI summary from clinical notes.</small>"
761
  )
762
 
 
766
  inputs=[input_query, input_labs, input_behaviour, input_wsi],
767
  label="Load Example Patient Record",
768
  )
769
+ submit_btn = gr.Button("Run Analysis", variant="primary", scale=0, min_width=180)
 
 
 
 
 
770
 
771
  # ── OUTPUT ────────────────────────────────────────────────────────
772
  gr.Markdown("---")
 
780
  lines=1,
781
  )
782
  out_final = gr.Textbox(
783
+ label="Final Recommendation (streaming Β· citations in [SOURCE | PAGE] format)",
784
  interactive=False,
785
  lines=10,
786
  )
 
790
 
791
  with gr.Group(visible=True) as grp_m2:
792
  gr.Markdown("#### Module II β€” Risk Stratification")
793
+ out_risk = gr.Textbox(label="Risk Profile", interactive=False, lines=3, visible=False)
 
 
 
794
 
795
  with gr.Group(visible=True) as grp_m3:
796
  gr.Markdown("#### Module III β€” Bone Marrow Pathology")
797
+ out_wsi_text = gr.Textbox(label="WSI Analysis Summary", interactive=False, lines=3, visible=False)
798
+ out_wsi_img = gr.Image(
799
+ label="AI-Annotated WSI (Red = Malignant Β· Green = Normal Β· Gray = Background Β· Yellow = Unknown)",
 
 
 
800
  type="pil", height=420, interactive=False, visible=False,
801
  )
802
+ out_malignancy_stat = gr.Textbox(label="Patch Classification Statistics", interactive=False, lines=2, visible=False)
 
 
 
803
 
804
  with gr.Group(visible=True) as grp_m4:
805
  gr.Markdown("#### Module IV β€” Disease Progression")
806
+ out_prog = gr.Textbox(label="Progression Summary", interactive=False, lines=3, visible=False)
 
 
 
807
 
808
  with gr.Group(visible=True) as grp_m5:
809
  gr.Markdown("#### Module V β€” Guideline Retrieval (RAG)")
810
+ out_m5_source = gr.Textbox(label="Evidence Source", interactive=False, lines=1, visible=False)
 
 
 
811
  out_rag = gr.Textbox(
812
  label="Retrieved Guideline Passages (with source citations)",
813
  interactive=False, lines=6, visible=False,
814
  )
815
  with gr.Accordion("View Raw Reference Chunks", open=False):
816
  out_raw_chunks = gr.Textbox(
817
+ label="Raw knowledge base excerpts β€” verify AI citations against these",
818
  interactive=False, lines=12, visible=False,
819
  )
820
 
 
833
  )
834
 
835
  demo.queue().launch(
836
+ server_name="0.0.0.0",
837
+ server_port=7860,
838
+ share=False,
839
+ ssr_mode=False,
840
+ )