MaximoLopezChenlo commited on
Commit
64d5cdf
Β·
verified Β·
1 Parent(s): aa166d8

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +627 -0
app.py ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OncoAgent β€” Interactive Demo for Hugging Face Spaces.
3
+
4
+ Simulates the full multi-agent oncology triage pipeline with realistic
5
+ streaming, agent node transitions, and clinical recommendations.
6
+ Runs without GPU/vLLM β€” pure frontend showcase.
7
+
8
+ Hardware Target: AMD Instinct MI300X (production)
9
+ Demo Mode: CPU-only simulation for HF Spaces free tier
10
+ """
11
+
12
+ import gradio as gr
13
+ import time
14
+ from typing import Generator
15
+
16
+ # ── Design System (inline for HF Spaces portability) ──────────────────
17
+
18
+ FONTS_LINK: str = (
19
+ '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?'
20
+ 'family=Figtree:wght@400;500;600;700&'
21
+ 'family=Inter:wght@300;400;500;600&display=swap">'
22
+ )
23
+
24
+ CSS: str = """
25
+ /* OncoAgent β€” Clinical Dark Theme */
26
+ :root {
27
+ --shadow-drop: none !important;
28
+ --shadow-drop-lg: none !important;
29
+ --shadow-inset: none !important;
30
+ --block-shadow: none !important;
31
+ --body-background-fill: #0f172a !important;
32
+ --background-fill-primary: #0f172a !important;
33
+ }
34
+ html, body, gradio-app {
35
+ background-color: #0f172a !important;
36
+ margin: 0 !important; padding: 0 !important;
37
+ }
38
+ .gradio-container, .main, .wrap, .contain,
39
+ .gradio-container > div, footer, main {
40
+ background: #0f172a !important;
41
+ color: #e2e8f0 !important;
42
+ font-family: 'Inter', -apple-system, sans-serif !important;
43
+ box-shadow: none !important;
44
+ }
45
+ .gradio-container {
46
+ max-width: 960px !important;
47
+ margin: 0 auto !important;
48
+ border: none !important;
49
+ }
50
+ * { box-sizing: border-box; }
51
+
52
+ .gr-group, .gr-block, .gr-box, .gr-panel,
53
+ .block, .wrap, .panel { background: transparent !important; }
54
+
55
+ /* Header */
56
+ .header-bar {
57
+ display: flex; justify-content: space-between; align-items: center;
58
+ padding: 14px 24px;
59
+ background: #1e293b;
60
+ border: 1px solid #334155; border-radius: 14px;
61
+ margin-bottom: 16px;
62
+ }
63
+ .brand-name {
64
+ font-family: 'Figtree', sans-serif;
65
+ font-size: 1.6rem; font-weight: 700;
66
+ color: #f1f5f9; letter-spacing: -0.025em;
67
+ }
68
+ .hw-badge {
69
+ background: rgba(239, 68, 68, 0.15); color: #fca5a5;
70
+ padding: 5px 14px; border-radius: 6px;
71
+ font-size: 0.72rem; font-weight: 600;
72
+ letter-spacing: 0.05em;
73
+ border: 1px solid rgba(239, 68, 68, 0.25);
74
+ }
75
+ .demo-badge {
76
+ background: rgba(14, 165, 233, 0.15); color: #7dd3fc;
77
+ padding: 5px 14px; border-radius: 6px;
78
+ font-size: 0.72rem; font-weight: 600;
79
+ letter-spacing: 0.05em;
80
+ border: 1px solid rgba(14, 165, 233, 0.25);
81
+ }
82
+
83
+ /* Cards */
84
+ .card {
85
+ background: #1e293b !important;
86
+ border: 1px solid #334155 !important;
87
+ border-radius: 14px !important;
88
+ padding: 18px !important;
89
+ }
90
+ .card:hover { border-color: #475569 !important; }
91
+
92
+ /* Buttons */
93
+ .btn-primary {
94
+ background: linear-gradient(135deg, #0ea5e9, #0284c7) !important;
95
+ border: none !important; color: #fff !important;
96
+ font-weight: 600 !important; border-radius: 10px !important;
97
+ cursor: pointer !important;
98
+ transition: transform 0.15s ease-out, box-shadow 0.15s ease-out !important;
99
+ }
100
+ .btn-primary:hover {
101
+ transform: translateY(-1px) !important;
102
+ box-shadow: 0 4px 14px rgba(14, 165, 233, 0.4) !important;
103
+ }
104
+ .btn-demo {
105
+ background: linear-gradient(135deg, #10b981, #059669) !important;
106
+ border: none !important; color: #fff !important;
107
+ font-weight: 600 !important; border-radius: 10px !important;
108
+ cursor: pointer !important; font-size: 1rem !important;
109
+ padding: 12px 24px !important;
110
+ transition: transform 0.15s ease-out, box-shadow 0.15s ease-out !important;
111
+ }
112
+ .btn-demo:hover {
113
+ transform: translateY(-2px) !important;
114
+ box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4) !important;
115
+ }
116
+
117
+ /* Chat */
118
+ .gr-chatbot, [class*="chatbot"] {
119
+ background: transparent !important;
120
+ border: none !important; box-shadow: none !important;
121
+ }
122
+ .message {
123
+ padding: 16px 20px !important;
124
+ border-radius: 18px !important;
125
+ margin-bottom: 12px !important;
126
+ line-height: 1.7 !important;
127
+ font-size: 0.94rem !important;
128
+ }
129
+ .message.user {
130
+ background: rgba(14, 165, 233, 0.08) !important;
131
+ border: 1px solid rgba(14, 165, 233, 0.15) !important;
132
+ border-bottom-right-radius: 4px !important;
133
+ margin-left: 15% !important;
134
+ }
135
+ .message.bot {
136
+ background: rgba(30, 41, 59, 0.6) !important;
137
+ border: 1px solid rgba(51, 65, 85, 0.3) !important;
138
+ border-bottom-left-radius: 4px !important;
139
+ margin-right: 10% !important;
140
+ backdrop-filter: blur(12px) !important;
141
+ }
142
+
143
+ /* Safety Badges */
144
+ .badge-safe {
145
+ display: inline-flex; align-items: center; gap: 6px;
146
+ background: rgba(16, 185, 129, 0.12); color: #34d399;
147
+ border: 1px solid rgba(16, 185, 129, 0.3);
148
+ padding: 4px 12px; border-radius: 6px;
149
+ font-weight: 600; font-size: 0.8rem;
150
+ }
151
+
152
+ /* Node Progress */
153
+ .node-step {
154
+ display: inline-flex; align-items: center; gap: 6px;
155
+ font-size: 0.78rem; color: #94a3b8;
156
+ padding: 4px 10px; border-radius: 6px;
157
+ background: rgba(14, 165, 233, 0.08);
158
+ border: 1px solid rgba(14, 165, 233, 0.15);
159
+ margin-right: 6px; margin-bottom: 4px;
160
+ }
161
+ .node-step.active {
162
+ color: #38bdf8; border-color: rgba(14, 165, 233, 0.4);
163
+ animation: pulse-node 1.5s ease-in-out infinite;
164
+ }
165
+ .node-step.done { color: #34d399; border-color: rgba(16,185,129,0.3); }
166
+ @keyframes pulse-node {
167
+ 0%, 100% { opacity: 1; } 50% { opacity: 0.5; }
168
+ }
169
+
170
+ /* Info panel */
171
+ .info-panel {
172
+ background: rgba(14, 165, 233, 0.06);
173
+ border: 1px solid rgba(14, 165, 233, 0.15);
174
+ border-radius: 12px; padding: 16px;
175
+ margin-bottom: 12px;
176
+ }
177
+
178
+ /* Textarea & inputs */
179
+ textarea, input[type="text"] {
180
+ background: #0f172a !important;
181
+ border: 1px solid #334155 !important;
182
+ color: #e2e8f0 !important;
183
+ border-radius: 10px !important;
184
+ font-family: 'Inter', sans-serif !important;
185
+ }
186
+ textarea:focus, input[type="text"]:focus {
187
+ border-color: #0ea5e9 !important;
188
+ outline: none !important;
189
+ }
190
+
191
+ /* Labels */
192
+ label, .gr-input-label { color: #94a3b8 !important; }
193
+
194
+ /* KPI tiles */
195
+ .kpi-row { display: flex; gap: 12px; margin-top: 12px; }
196
+ .kpi-tile {
197
+ flex: 1; background: #1e293b; border: 1px solid #334155;
198
+ border-radius: 10px; padding: 14px; text-align: center;
199
+ }
200
+ .kpi-label {
201
+ font-size: 0.68rem; font-weight: 500; color: #64748b;
202
+ text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px;
203
+ }
204
+ .kpi-value {
205
+ font-family: 'Figtree', sans-serif;
206
+ font-size: 1.3rem; font-weight: 700; color: #f1f5f9;
207
+ }
208
+
209
+ /* Architecture diagram */
210
+ .arch-flow {
211
+ display: flex; align-items: center; gap: 8px;
212
+ flex-wrap: wrap; margin: 12px 0;
213
+ }
214
+ .arch-node {
215
+ background: #1e293b; border: 1px solid #334155;
216
+ border-radius: 8px; padding: 8px 14px;
217
+ font-size: 0.78rem; color: #cbd5e1;
218
+ font-weight: 500;
219
+ }
220
+ .arch-node.highlight {
221
+ border-color: #0ea5e9; color: #7dd3fc;
222
+ background: rgba(14, 165, 233, 0.08);
223
+ }
224
+ .arch-arrow { color: #475569; font-size: 1.2rem; }
225
+
226
+ /* Scrollbar */
227
+ ::-webkit-scrollbar { width: 6px; }
228
+ ::-webkit-scrollbar-track { background: #0f172a; }
229
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
230
+
231
+ /* Footer */
232
+ .footer-text {
233
+ text-align: center; color: #475569;
234
+ font-size: 0.72rem; margin-top: 20px;
235
+ padding: 12px; border-top: 1px solid #1e293b;
236
+ }
237
+
238
+ /* Reduced motion */
239
+ @media (prefers-reduced-motion: reduce) {
240
+ *, *::before, *::after {
241
+ animation-duration: 0.01ms !important;
242
+ transition-duration: 0.01ms !important;
243
+ }
244
+ }
245
+ """
246
+
247
+ # ── Demo Case Data ────────────────────────────────────────────────────
248
+
249
+ DEMO_CASE: str = (
250
+ "55-year-old female patient presents with postmenopausal bleeding. "
251
+ "Ultrasound shows an endometrial thickening of 12mm. "
252
+ "The endometrial biopsy report confirms Grade 1 endometrioid "
253
+ "adenocarcinoma. No evidence of myometrial invasion on MRI. "
254
+ "CA-125 within normal limits. Patient has BMI of 32 and "
255
+ "controlled type 2 diabetes."
256
+ )
257
+
258
+ # Simulated agent outputs β€” based on real NCCN Uterine guidelines
259
+ DEMO_STEPS: list = [
260
+ {
261
+ "node": "πŸ”€ Router Agent",
262
+ "delay": 0.8,
263
+ "output": (
264
+ "**Classification:** Oncological case detected.\n\n"
265
+ "- **Cancer Type:** Endometrial (Uterine)\n"
266
+ "- **Confidence:** 0.96\n"
267
+ "- **Routing Decision:** β†’ Specialist Agent (Tier 2 β€” Qwen3.6-27B)\n"
268
+ "- **Rationale:** Confirmed histopathology requires advanced reasoning."
269
+ ),
270
+ },
271
+ {
272
+ "node": "πŸ” Clinical Extraction",
273
+ "delay": 1.0,
274
+ "output": (
275
+ "**Extracted Clinical Entities:**\n\n"
276
+ "| Field | Value |\n"
277
+ "|---|---|\n"
278
+ "| Age | 55 years |\n"
279
+ "| Sex | Female |\n"
280
+ "| Chief Complaint | Postmenopausal bleeding |\n"
281
+ "| Imaging | Endometrial thickening 12mm (US) |\n"
282
+ "| MRI | No myometrial invasion |\n"
283
+ "| Pathology | **Grade 1 endometrioid adenocarcinoma** |\n"
284
+ "| Biomarker | CA-125 normal |\n"
285
+ "| Comorbidities | BMI 32, T2DM (controlled) |\n"
286
+ "| FIGO Stage | Likely IA (pending surgical staging) |"
287
+ ),
288
+ },
289
+ {
290
+ "node": "πŸ“š Corrective RAG",
291
+ "delay": 1.5,
292
+ "output": (
293
+ "**Retrieval Results** β€” NCCN Uterine Cancer Guidelines v2.2025\n\n"
294
+ "- πŸ“„ **Source:** `uterine.pdf` β€” Pages 12-18 (Endometrioid Adenocarcinoma)\n"
295
+ "- 🎯 **Bi-Encoder Score:** 0.89 | **Cross-Encoder Score:** 0.94\n"
296
+ "- βœ… **Distance Gate:** PASSED (threshold: 0.65)\n"
297
+ "- πŸ“Š **Chunks Retrieved:** 6 / 2,847 total\n\n"
298
+ "**Key Guideline Excerpts:**\n"
299
+ "> *\"For Grade 1 endometrioid adenocarcinoma confined to the endometrium "
300
+ "(Stage IA), total hysterectomy with bilateral salpingo-oophorectomy "
301
+ "(TH/BSO) is the primary treatment. Lymph node assessment should be "
302
+ "considered based on institutional protocols.\"*\n\n"
303
+ "> *\"Sentinel lymph node mapping is preferred over comprehensive "
304
+ "lymphadenectomy for clinically uterine-confined disease.\"*"
305
+ ),
306
+ },
307
+ {
308
+ "node": "🧠 Specialist Agent",
309
+ "delay": 2.0,
310
+ "output": (
311
+ "**OncoAgent β€” Clinical Recommendation**\n\n"
312
+ "---\n\n"
313
+ "## πŸ“‹ Clinical Summary\n\n"
314
+ "55-year-old postmenopausal female with biopsy-confirmed Grade 1 "
315
+ "endometrioid adenocarcinoma. MRI shows no myometrial invasion. "
316
+ "Tumor markers within normal limits. Comorbidities include obesity "
317
+ "(BMI 32) and controlled T2DM.\n\n"
318
+ "## πŸ”¬ Diagnostic Findings\n\n"
319
+ "- **Histology:** Endometrioid adenocarcinoma, Grade 1 (well-differentiated)\n"
320
+ "- **Probable FIGO Stage:** IA β€” disease confined to endometrium\n"
321
+ "- **Myometrial Invasion:** Not detected on MRI\n"
322
+ "- **Lymphovascular Space Invasion (LVSI):** Not reported\n\n"
323
+ "## πŸ’Š Treatment Recommendation\n\n"
324
+ "**Primary Treatment (NCCN Category 1):**\n"
325
+ "1. **Total Hysterectomy with Bilateral Salpingo-Oophorectomy (TH/BSO)**\n"
326
+ " - Minimally invasive approach (laparoscopic/robotic) preferred\n"
327
+ " - Consider peritoneal washings at time of surgery\n\n"
328
+ "2. **Sentinel Lymph Node (SLN) Mapping**\n"
329
+ " - Preferred over comprehensive lymphadenectomy\n"
330
+ " - Per NCCN institutional SLN algorithm\n\n"
331
+ "**Adjuvant Therapy Considerations:**\n"
332
+ "- If final pathology confirms Stage IA, Grade 1: **Observation only**\n"
333
+ "- No adjuvant radiation or chemotherapy indicated for this stage\n"
334
+ "- If upstaged post-surgery: Refer to NCCN adjuvant guidelines\n\n"
335
+ "## ⚠️ Additional Considerations\n\n"
336
+ "- **Obesity Management:** BMI 32 β€” perioperative risk optimization recommended\n"
337
+ "- **Diabetes Control:** HbA1c target < 7% pre-surgery\n"
338
+ "- **Genetic Counseling:** Consider Lynch syndrome screening "
339
+ "(immunohistochemistry for MMR proteins or MSI testing)\n"
340
+ "- **Fertility Preservation:** Not applicable (postmenopausal)\n\n"
341
+ "## πŸ“š Evidence Level\n\n"
342
+ "- **NCCN Evidence Category:** 1 (High-level evidence, uniform consensus)\n"
343
+ "- **Guideline Source:** NCCN Uterine Neoplasms v2.2025, Pages 12-18\n"
344
+ "- **RAG Confidence:** 0.94 (Cross-Encoder validated)"
345
+ ),
346
+ },
347
+ {
348
+ "node": "βœ… Critic (Reflexion Loop)",
349
+ "delay": 1.0,
350
+ "output": (
351
+ "**Critic Validation β€” PASSED βœ…**\n\n"
352
+ "| Check | Status |\n"
353
+ "|---|---|\n"
354
+ "| Clinical Summary present | βœ… |\n"
355
+ "| Diagnostic Findings present | βœ… |\n"
356
+ "| Treatment Recommendation present | βœ… |\n"
357
+ "| Evidence/Citations present | βœ… |\n"
358
+ "| Diagnostic Rigor (biopsy confirmed) | βœ… |\n"
359
+ "| Anti-Hallucination (RAG-grounded) | βœ… |\n"
360
+ "| PHI Sanitization | βœ… |\n\n"
361
+ "**Verdict:** Recommendation is clinically grounded and safe for review.\n\n"
362
+ "---\n"
363
+ "### Decision Status: "
364
+ "<span class='badge-safe'>"
365
+ "βœ… Clinically Validated"
366
+ "</span>"
367
+ ),
368
+ },
369
+ ]
370
+
371
+
372
+ def _node_progress_html(current_idx: int) -> str:
373
+ """Generate the agent pipeline progress bar HTML."""
374
+ nodes = ["Router", "Extraction", "RAG", "Specialist", "Critic"]
375
+ icons = ["πŸ”€", "πŸ”", "πŸ“š", "🧠", "βœ…"]
376
+ parts = []
377
+ for i, (name, icon) in enumerate(zip(nodes, icons)):
378
+ if i < current_idx:
379
+ cls = "done"
380
+ elif i == current_idx:
381
+ cls = "active"
382
+ else:
383
+ cls = ""
384
+ parts.append(f"<span class='node-step {cls}'>{icon} {name}</span>")
385
+ if i < len(nodes) - 1:
386
+ parts.append("<span style='color:#475569;'>β†’</span>")
387
+ return " ".join(parts)
388
+
389
+
390
+ def run_demo() -> Generator:
391
+ """Simulate the full OncoAgent pipeline with streaming."""
392
+ history = []
393
+
394
+ # Step 1: User message appears
395
+ history.append({"role": "user", "content": DEMO_CASE})
396
+ yield history
397
+
398
+ time.sleep(0.5)
399
+
400
+ # Step 2: Stream each agent node
401
+ for step_idx, step in enumerate(DEMO_STEPS):
402
+ node_name = step["node"]
403
+ delay = step["delay"]
404
+ output = step["output"]
405
+
406
+ # Build progress bar
407
+ progress = _node_progress_html(step_idx)
408
+
409
+ # Start with node header + progress
410
+ header = f"### {node_name}\n{progress}\n\n"
411
+
412
+ # Stream the output character by character (in chunks for speed)
413
+ full_text = header
414
+ chunk_size = 8
415
+ for i in range(0, len(output), chunk_size):
416
+ full_text += output[i:i + chunk_size]
417
+ # Update the last bot message
418
+ display_history = history.copy()
419
+ display_history.append({"role": "assistant", "content": full_text})
420
+ yield display_history
421
+ time.sleep(0.015)
422
+
423
+ # Finalize this step
424
+ history.append({"role": "assistant", "content": full_text})
425
+ yield history
426
+
427
+ # Pause between nodes
428
+ time.sleep(delay * 0.3)
429
+
430
+ # Final summary message
431
+ time.sleep(0.3)
432
+ final_msg = (
433
+ "---\n\n"
434
+ "### 🏁 Pipeline Complete\n\n"
435
+ "<div class='kpi-row'>"
436
+ "<div class='kpi-tile'><div class='kpi-label'>Agents Used</div>"
437
+ "<div class='kpi-value'>5</div></div>"
438
+ "<div class='kpi-tile'><div class='kpi-label'>RAG Sources</div>"
439
+ "<div class='kpi-value'>6</div></div>"
440
+ "<div class='kpi-tile'><div class='kpi-label'>Confidence</div>"
441
+ "<div class='kpi-value'>0.94</div></div>"
442
+ "<div class='kpi-tile'><div class='kpi-label'>Safety</div>"
443
+ "<div class='kpi-value'>βœ…</div></div>"
444
+ "</div>\n\n"
445
+ "<div style='margin-top:12px; font-size:0.8rem; color:#64748b;'>"
446
+ "⚑ In production, this pipeline runs on AMD Instinctβ„’ MI300X with "
447
+ "vLLM (PagedAttention) serving Qwen3.5-9B + Qwen3.6-27B models. "
448
+ "This demo simulates the agent flow for showcase purposes."
449
+ "</div>"
450
+ )
451
+ history.append({"role": "assistant", "content": final_msg})
452
+ yield history
453
+
454
+
455
+ def handle_user_message(
456
+ message: str,
457
+ history: list,
458
+ ) -> Generator:
459
+ """Handle custom user messages with a simulated response."""
460
+ if not message.strip():
461
+ yield history
462
+ return
463
+
464
+ history = history or []
465
+ history.append({"role": "user", "content": message})
466
+ yield history
467
+
468
+ time.sleep(0.5)
469
+
470
+ # Simulated response for any custom input
471
+ response = (
472
+ "### πŸ”€ Router Agent\n\n"
473
+ "**Note:** This is a demo environment running on HF Spaces "
474
+ "without GPU acceleration.\n\n"
475
+ "In the **production deployment** on AMD Instinctβ„’ MI300X, "
476
+ "your clinical case would be processed through our full "
477
+ "5-agent pipeline:\n\n"
478
+ "1. **Router** β€” Classifies oncological vs. non-oncological\n"
479
+ "2. **Clinical Extraction** β€” Extracts structured entities\n"
480
+ "3. **Corrective RAG** β€” Retrieves from NCCN/ESMO guidelines\n"
481
+ "4. **Specialist** β€” Generates evidence-based recommendation\n"
482
+ "5. **Critic (Reflexion)** β€” Validates safety and completeness\n\n"
483
+ "πŸ‘‰ Click **β–Ά View Demo** to see a complete simulated triage "
484
+ "with the endometrial cancer case.\n\n"
485
+ "πŸ”— **Production:** Deploy with `docker compose up` on MI300X hardware.\n"
486
+ "πŸ“– **Source:** [GitHub](https://github.com/maximolopezchenlo-lab/OncoAgent)"
487
+ )
488
+
489
+ # Stream it
490
+ partial = ""
491
+ chunk_size = 12
492
+ for i in range(0, len(response), chunk_size):
493
+ partial += response[i:i + chunk_size]
494
+ display = history.copy()
495
+ display.append({"role": "assistant", "content": partial})
496
+ yield display
497
+ time.sleep(0.01)
498
+
499
+ history.append({"role": "assistant", "content": response})
500
+ yield history
501
+
502
+
503
+ # ── Build the UI ──────────────────────────────────────────────────────
504
+
505
+ HEADER_HTML: str = """
506
+ <div class="header-bar">
507
+ <div style="display:flex; align-items:center; gap:12px;">
508
+ <span class="brand-name">🧬 OncoAgent</span>
509
+ <span class="demo-badge">INTERACTIVE DEMO</span>
510
+ </div>
511
+ <div style="display:flex; gap:8px; align-items:center;">
512
+ <span class="hw-badge">AMD INSTINCTβ„’ MI300X</span>
513
+ <span class="hw-badge">ROCm 7.2</span>
514
+ </div>
515
+ </div>
516
+ """
517
+
518
+ INFO_HTML: str = """
519
+ <div class="info-panel">
520
+ <div style="font-size:0.95rem; font-weight:600; color:#e2e8f0; margin-bottom:8px;">
521
+ πŸ₯ Multi-Agent Oncology Triage System
522
+ </div>
523
+ <div style="font-size:0.82rem; color:#94a3b8; line-height:1.6;">
524
+ OncoAgent uses a <strong style="color:#7dd3fc;">5-agent LangGraph pipeline</strong>
525
+ to analyze clinical cases against <strong style="color:#7dd3fc;">NCCN/ESMO guidelines</strong>
526
+ with built-in safety validation and anti-hallucination guardrails.
527
+ </div>
528
+ <div class="arch-flow">
529
+ <span class="arch-node highlight">πŸ”€ Router</span>
530
+ <span class="arch-arrow">β†’</span>
531
+ <span class="arch-node">πŸ” Extraction</span>
532
+ <span class="arch-arrow">β†’</span>
533
+ <span class="arch-node">πŸ“š Corrective RAG</span>
534
+ <span class="arch-arrow">β†’</span>
535
+ <span class="arch-node">🧠 Specialist</span>
536
+ <span class="arch-arrow">β†’</span>
537
+ <span class="arch-node">βœ… Critic</span>
538
+ </div>
539
+ <div style="font-size:0.72rem; color:#64748b; margin-top:8px;">
540
+ ⚑ Production: Qwen3.5-9B (Tier 1) + Qwen3.6-27B (Tier 2) via vLLM PagedAttention
541
+ &nbsp;|&nbsp; πŸ“„ 162 NCCN + 16 ESMO guidelines indexed
542
+ </div>
543
+ </div>
544
+ """
545
+
546
+ FOOTER_HTML: str = """
547
+ <div class="footer-text">
548
+ 🧬 OncoAgent β€” AMD Developer Hackathon 2026<br>
549
+ Built with LangGraph Β· vLLM Β· Gradio Β· ROCm 7.2<br>
550
+ <a href="https://github.com/maximolopezchenlo-lab/OncoAgent"
551
+ style="color:#0ea5e9; text-decoration:none;" target="_blank">
552
+ GitHub Repository</a>
553
+ &nbsp;Β·&nbsp;
554
+ <span style="color:#64748b;">100% Open Source Β· Apache 2.0</span>
555
+ </div>
556
+ """
557
+
558
+
559
+ with gr.Blocks(
560
+ css=CSS,
561
+ head=FONTS_LINK,
562
+ title="OncoAgent β€” Oncology Triage Demo",
563
+ theme=gr.themes.Base(),
564
+ ) as demo:
565
+ # Header
566
+ gr.HTML(HEADER_HTML)
567
+ gr.HTML(INFO_HTML)
568
+
569
+ # Chat
570
+ chatbot = gr.Chatbot(
571
+ type="messages",
572
+ label="Clinical Triage Chat",
573
+ height=520,
574
+ show_label=False,
575
+ show_copy_button=True,
576
+ render_markdown=True,
577
+ elem_classes=["card"],
578
+ )
579
+
580
+ # Controls
581
+ with gr.Row():
582
+ with gr.Column(scale=3):
583
+ txt = gr.Textbox(
584
+ placeholder="Enter a clinical case or click 'β–Ά View Demo'...",
585
+ show_label=False,
586
+ lines=2,
587
+ max_lines=5,
588
+ )
589
+ with gr.Column(scale=1, min_width=180):
590
+ demo_btn = gr.Button(
591
+ "β–Ά View Demo",
592
+ elem_classes=["btn-demo"],
593
+ size="lg",
594
+ )
595
+
596
+ with gr.Row():
597
+ send_btn = gr.Button("Send", elem_classes=["btn-primary"], size="sm")
598
+ clear_btn = gr.Button("πŸ—‘ Clear", variant="secondary", size="sm")
599
+
600
+ # Footer
601
+ gr.HTML(FOOTER_HTML)
602
+
603
+ # ── Event Handlers ────────────────────────────────────────────────
604
+
605
+ demo_btn.click(
606
+ fn=run_demo,
607
+ inputs=None,
608
+ outputs=chatbot,
609
+ )
610
+
611
+ send_btn.click(
612
+ fn=handle_user_message,
613
+ inputs=[txt, chatbot],
614
+ outputs=chatbot,
615
+ ).then(lambda: "", outputs=txt)
616
+
617
+ txt.submit(
618
+ fn=handle_user_message,
619
+ inputs=[txt, chatbot],
620
+ outputs=chatbot,
621
+ ).then(lambda: "", outputs=txt)
622
+
623
+ clear_btn.click(lambda: [], outputs=chatbot)
624
+
625
+
626
+ if __name__ == "__main__":
627
+ demo.launch(server_name="0.0.0.0", server_port=7860)