NeuroBio_Agent / app.py
arnavmishra4's picture
Update app.py
201357d verified
Raw
History Blame Contribute Delete
68.5 kB
# ── Patch 3: fix Gradio 4.44.0 + Starlette API mismatch ──
import starlette.templating as _st
_orig_TemplateResponse = _st.Jinja2Templates.TemplateResponse
def _safe_TemplateResponse(self, *args, **kwargs):
# If the arguments are shifted, re-align them:
if len(args) >= 2 and isinstance(args[0], str) and isinstance(args[1], dict):
name_str = args[0]
context_dict = args[1]
request_obj = context_dict.get("request")
args = (request_obj, name_str, context_dict) + args[2:]
return _orig_TemplateResponse(self, *args, **kwargs)
_st.Jinja2Templates.TemplateResponse = _safe_TemplateResponse
# ── Patch 1: restore HfFolder for gradio 4.44.0 ──
import unittest.mock as _mock
import sys as _sys
_hf_hub = __import__("huggingface_hub")
if not hasattr(_hf_hub, "HfFolder"):
class _FakeHfFolder:
@staticmethod
def get_token(): return None
@staticmethod
def save_token(token): pass
@staticmethod
def delete_token(): pass
_hf_hub.HfFolder = _FakeHfFolder
_sys.modules["huggingface_hub"].HfFolder = _FakeHfFolder
# ── Patch 2: fix gradio_client schema bug ──
import gradio_client.utils as _gcu
_orig = _gcu._json_schema_to_python_type
def _safe(schema, defs=None):
if not isinstance(schema, dict):
return "Any"
return _orig(schema, defs)
_gcu._json_schema_to_python_type = _safe
# ── Imports ──
import json
import time
import threading
import datetime
import gradio as gr
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
# ─────────────────────────────────────────────────────────────────────
# CSS β€” 600+ lines of premium dark-theme styling
# ─────────────────────────────────────────────────────────────────────
# ─────────────────────────────────────────────────────────────────────
# CSS β€” 600+ lines of premium dark-theme styling (Safari Compatible)
# ─────────────────────────────────────────────────────────────────────
CUSTOM_CSS = """
/* ── Google Fonts ── */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Sora:wght@300;400;600;700&display=swap');
/* ── Global Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-void: #04050a;
--bg-deep: #080c14;
--bg-surface: #0d1220;
--bg-raised: #111827;
--bg-glass: rgba(13,18,32,0.72);
--bg-card: rgba(17,24,39,0.85);
--border-subtle: rgba(99,120,190,0.12);
--border-glow: rgba(99,179,237,0.28);
--border-active: rgba(99,179,237,0.6);
--blue-400: #60a5fa;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-glow: rgba(59,130,246,0.35);
--cyan-400: #22d3ee;
--cyan-glow: rgba(34,211,238,0.25);
--green-400: #4ade80;
--green-500: #22c55e;
--green-glow: rgba(74,222,128,0.25);
--orange-400: #fb923c;
--orange-glow: rgba(251,146,60,0.25);
--red-400: #f87171;
--red-glow: rgba(248,113,113,0.25);
--purple-400: #c084fc;
--purple-glow: rgba(192,132,252,0.2);
--text-primary: #f0f4ff;
--text-secondary: #94a3b8;
--text-muted: #4b5563;
--text-accent: #93c5fd;
--font-display: 'Sora', sans-serif;
--font-body: 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-xl: 22px;
--shadow-card: 0 4px 24px rgba(0,0,0,0.45), 0 1px 4px rgba(0,0,0,0.3);
--shadow-glow-blue: 0 0 20px rgba(59,130,246,0.3), 0 0 60px rgba(59,130,246,0.1);
--shadow-glow-green: 0 0 20px rgba(74,222,128,0.25), 0 0 50px rgba(74,222,128,0.08);
--shadow-glow-orange: 0 0 20px rgba(251,146,60,0.25);
--transition-fast: 0.15s ease;
--transition-smooth: 0.3s cubic-bezier(0.4,0,0.2,1);
--transition-spring: 0.5s cubic-bezier(0.34,1.56,0.64,1);
}
/* ── App Shell ── */
.gradio-container {
background: var(--bg-void) !important;
font-family: var(--font-body) !important;
min-height: 100vh !important;
max-width: 100% !important;
padding: 0 !important;
}
/* hide gradio chrome */
footer { display: none !important; }
.gr-form { background: transparent !important; border: none !important; }
.gr-box { background: transparent !important; border: none !important; }
/* ── Ambient Background ── */
#nb-root {
background:
radial-gradient(ellipse 80% 50% at 20% 10%, rgba(37,99,235,0.08) 0%, transparent 60%),
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(124,58,237,0.06) 0%, transparent 55%),
radial-gradient(ellipse 40% 30% at 60% 30%, rgba(6,182,212,0.04) 0%, transparent 50%),
var(--bg-void);
padding: 0;
-webkit-font-smoothing: antialiased; /* Safari font smoothing */
}
/* ─────────────────── HEADER ─────────────────── */
#nb-header {
padding: 40px 48px 32px;
border-bottom: 1px solid var(--border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
gap: 32px;
position: relative;
overflow: hidden;
}
#nb-header::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(37,99,235,0.06) 0%, transparent 60%);
pointer-events: none;
}
.nb-logo-group { display: flex; flex-direction: column; gap: 6px; }
.nb-wordmark {
font-family: var(--font-display) !important;
font-size: 26px !important;
font-weight: 700 !important;
letter-spacing: -0.03em !important;
color: var(--text-primary) !important;
background: linear-gradient(135deg, #f0f4ff 0%, #93c5fd 50%, #60a5fa 100%);
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
display: inline-block; /* Essential for Safari background-clip */
line-height: 1.2 !important;
margin: 0 !important;
padding: 0 !important;
}
.nb-tagline {
font-family: var(--font-mono) !important;
font-size: 11px !important;
font-weight: 400 !important;
color: var(--blue-400) !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
opacity: 0.8;
margin: 0 !important;
padding: 0 !important;
}
.nb-header-right {
display: flex;
align-items: center;
gap: 24px;
}
.nb-status-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: 99px;
border: 1px solid var(--border-subtle);
background: var(--bg-card);
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
letter-spacing: 0.06em;
}
.nb-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--green-400);
box-shadow: 0 0 6px var(--green-glow);
animation: nb-pulse-dot 2s ease-in-out infinite;
}
@keyframes nb-pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.85); }
}
/* ─────────────────── UPLOAD ROW ─────────────────── */
#nb-upload-row {
padding: 24px 48px;
display: flex;
align-items: center;
gap: 20px;
border-bottom: 1px solid var(--border-subtle);
background: rgba(13,18,32,0.4);
}
#nb-upload-row .gr-file-upload,
#nb-upload-row input[type="file"] {
display: none !important;
}
/* Upload button (gr.File renders as a button) */
#nb-upload-btn button {
-webkit-appearance: none !important; /* Safari Reset */
appearance: none !important;
background: var(--bg-card) !important;
border: 1px dashed rgba(99,120,190,0.3) !important;
color: var(--text-secondary) !important;
font-family: var(--font-body) !important;
font-size: 13px !important;
border-radius: var(--radius-md) !important;
padding: 10px 20px !important;
cursor: pointer !important;
transition: all var(--transition-smooth) !important;
width: 100% !important;
}
#nb-upload-btn button:hover {
border-color: var(--border-glow) !important;
color: var(--text-primary) !important;
background: rgba(59,130,246,0.08) !important;
}
/* Begin Investigation button */
#nb-begin-btn button {
-webkit-appearance: none !important; /* Safari Reset */
appearance: none !important;
background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 50%, #3b82f6 100%) !important;
border: 1px solid rgba(99,179,237,0.4) !important;
color: #fff !important;
font-family: var(--font-display) !important;
font-size: 14px !important;
font-weight: 600 !important;
border-radius: var(--radius-md) !important;
padding: 11px 28px !important;
cursor: pointer !important;
transition: all var(--transition-smooth) !important;
box-shadow: 0 4px 20px rgba(37,99,235,0.35), 0 0 0 0 rgba(59,130,246,0) !important;
letter-spacing: 0.01em !important;
}
#nb-begin-btn button:hover {
transform: translateY(-1px) !important;
box-shadow: 0 6px 28px rgba(37,99,235,0.5), 0 0 40px rgba(59,130,246,0.2) !important;
background: linear-gradient(135deg, #1e40af 0%, #2563eb 50%, #60a5fa 100%) !important;
}
#nb-begin-btn button:active {
transform: translateY(0) !important;
}
.nb-patient-badge {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border-subtle);
background: var(--bg-card);
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-secondary);
}
.nb-patient-badge .nb-badge-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--cyan-400);
box-shadow: 0 0 8px var(--cyan-glow);
}
/* ─────────────────── MAIN LAYOUT ─────────────────── */
#nb-main {
display: grid;
grid-template-columns: 220px 1fr 340px;
gap: 0;
height: calc(100vh - 180px);
overflow: hidden;
}
#nb-main > * {
border-right: 1px solid var(--border-subtle);
overflow-y: auto;
overflow-x: hidden;
min-width: 0; /* Crucial Safari fix for CSS grid blowout */
min-height: 0; /* Crucial Safari fix */
}
#nb-main > *:last-child { border-right: none; }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(99,120,190,0.25); border-radius: 99px; }
::-webkit-scrollbar-thumb:hover { background: rgba(99,120,190,0.45); }
/* ─────────────────── LEFT PANEL β€” TIMELINE ─────────────────── */
#nb-left {
padding: 24px 0;
background: rgba(8,12,20,0.6);
}
.nb-panel-title {
font-family: var(--font-mono) !important;
font-size: 10px !important;
font-weight: 500 !important;
color: var(--text-muted) !important;
letter-spacing: 0.16em !important;
text-transform: uppercase !important;
padding: 0 20px 16px !important;
margin: 0 !important;
border-bottom: 1px solid var(--border-subtle) !important;
margin-bottom: 8px !important;
}
/* Loop counter */
.nb-loop-counter {
margin: 12px 16px;
padding: 12px 14px;
border-radius: var(--radius-md);
background: rgba(37,99,235,0.08);
border: 1px solid rgba(59,130,246,0.15);
}
.nb-loop-label {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 4px;
}
.nb-loop-value {
font-family: var(--font-display);
font-size: 18px;
font-weight: 700;
color: var(--blue-400);
line-height: 1;
}
.nb-loop-sub {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-secondary);
margin-top: 4px;
}
/* ── Timeline nodes ── */
.nb-timeline {
padding: 16px 0;
position: relative;
}
.nb-timeline-node {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 20px;
cursor: default;
position: relative;
transition: background var(--transition-fast);
}
.nb-timeline-node:hover { background: rgba(59,130,246,0.04); }
.nb-node-line {
position: absolute;
left: 30px;
top: 50%;
width: 2px;
height: calc(100% + 0px);
background: var(--border-subtle);
transform: translateX(-50%);
z-index: 0;
}
.nb-node-dot {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid currentColor;
background: var(--bg-deep);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
z-index: 1;
transition: all var(--transition-smooth);
font-size: 9px;
}
.nb-node-label {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-muted);
transition: color var(--transition-smooth);
font-weight: 500;
}
/* Node states */
.nb-timeline-node[data-state="future"] .nb-node-dot {
color: var(--text-muted);
border-color: var(--text-muted);
opacity: 0.4;
}
.nb-timeline-node[data-state="active"] .nb-node-dot {
color: var(--blue-400);
border-color: var(--blue-400);
background: rgba(59,130,246,0.12);
box-shadow: 0 0 14px var(--blue-glow), 0 0 0 4px rgba(59,130,246,0.12);
animation: nb-glow-pulse 1.5s ease-in-out infinite;
}
.nb-timeline-node[data-state="active"] .nb-node-label { color: var(--blue-400); }
.nb-timeline-node[data-state="complete"] .nb-node-dot {
color: var(--green-400);
border-color: var(--green-400);
background: rgba(74,222,128,0.1);
box-shadow: 0 0 10px var(--green-glow);
}
.nb-timeline-node[data-state="complete"] .nb-node-label { color: var(--green-400); }
.nb-timeline-node[data-state="contradiction"] .nb-node-dot {
color: var(--orange-400);
border-color: var(--orange-400);
background: rgba(251,146,60,0.1);
box-shadow: 0 0 10px var(--orange-glow);
}
.nb-timeline-node[data-state="contradiction"] .nb-node-label { color: var(--orange-400); }
.nb-timeline-node[data-state="error"] .nb-node-dot {
color: var(--red-400);
border-color: var(--red-400);
background: rgba(248,113,113,0.1);
}
@keyframes nb-glow-pulse {
0%, 100% { box-shadow: 0 0 14px var(--blue-glow), 0 0 0 4px rgba(59,130,246,0.12); }
50% { box-shadow: 0 0 22px rgba(59,130,246,0.6), 0 0 0 8px rgba(59,130,246,0.06); }
}
/* ── Event feed ── */
.nb-event-feed {
margin: 16px 0 0;
padding-top: 16px;
border-top: 1px solid var(--border-subtle);
}
.nb-event-feed-title {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.14em;
text-transform: uppercase;
padding: 0 20px 12px;
}
.nb-event-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 7px 20px;
animation: nb-slide-in 0.4s cubic-bezier(0.4,0,0.2,1);
}
.nb-event-time {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
flex-shrink: 0;
margin-top: 1px;
}
.nb-event-text {
font-family: var(--font-body);
font-size: 11px;
color: var(--text-secondary);
line-height: 1.4;
}
@keyframes nb-slide-in {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
/* ─────────────────── CENTER PANEL ─────────────────── */
#nb-center {
padding: 24px 32px;
display: flex;
flex-direction: column;
gap: 0;
background: transparent;
}
/* Reasoning messages */
.nb-msg-block {
display: flex;
gap: 14px;
padding: 20px 0;
border-bottom: 1px solid var(--border-subtle);
animation: nb-fade-up 0.5s cubic-bezier(0.4,0,0.2,1);
}
.nb-msg-block:last-child { border-bottom: none; }
@keyframes nb-fade-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.nb-msg-avatar {
width: 36px;
height: 36px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
margin-top: 2px;
}
.nb-msg-avatar.observe { background: rgba(99,179,237,0.12); border: 1px solid rgba(99,179,237,0.25); }
.nb-msg-avatar.hypothesis{ background: rgba(168,85,247,0.12); border: 1px solid rgba(168,85,247,0.25); }
.nb-msg-avatar.search { background: rgba(34,211,238,0.1); border: 1px solid rgba(34,211,238,0.2); }
.nb-msg-avatar.interpret { background: rgba(74,222,128,0.1); border: 1px solid rgba(74,222,128,0.2); }
.nb-msg-avatar.revise { background: rgba(251,146,60,0.1); border: 1px solid rgba(251,146,60,0.2); }
.nb-msg-avatar.converge { background: rgba(59,130,246,0.12); border: 1px solid rgba(59,130,246,0.3); }
.nb-msg-avatar.final { background: rgba(74,222,128,0.12); border: 1px solid rgba(74,222,128,0.3); }
.nb-msg-content { flex: 1; min-width: 0; }
.nb-msg-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.nb-msg-role {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.nb-msg-role.observe { color: var(--cyan-400); }
.nb-msg-role.hypothesis { color: var(--purple-400); }
.nb-msg-role.search { color: var(--cyan-400); }
.nb-msg-role.interpret { color: var(--green-400); }
.nb-msg-role.revise { color: var(--orange-400); }
.nb-msg-role.converge { color: var(--blue-400); }
.nb-msg-role.final { color: var(--green-400); }
.nb-msg-timestamp {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
margin-left: auto;
}
.nb-msg-body {
font-family: var(--font-body);
font-size: 14px;
line-height: 1.7;
color: var(--text-secondary);
}
/* Data grids inside messages */
.nb-data-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
margin: 14px 0;
}
.nb-data-cell {
padding: 12px 14px;
border-radius: var(--radius-md);
background: rgba(13,18,32,0.8);
border: 1px solid var(--border-subtle);
}
.nb-data-cell-label {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 5px;
}
.nb-data-cell-value {
font-family: var(--font-mono);
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.nb-data-cell-value.positive { color: var(--orange-400); }
.nb-data-cell-value.neutral { color: var(--cyan-400); }
/* Confidence bar */
.nb-confidence-row {
display: flex;
align-items: center;
gap: 12px;
margin: 12px 0;
}
.nb-confidence-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
min-width: 80px;
text-transform: uppercase;
letter-spacing: 0.07em;
}
.nb-confidence-bar {
flex: 1;
height: 5px;
background: rgba(255,255,255,0.06);
border-radius: 99px;
overflow: hidden;
position: relative;
}
.nb-confidence-fill {
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--blue-500), var(--cyan-400));
box-shadow: 0 0 8px var(--cyan-glow);
transition: width 0.8s cubic-bezier(0.4,0,0.2,1);
}
.nb-confidence-pct {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 600;
color: var(--blue-400);
min-width: 38px;
text-align: right;
}
/* Searching animation */
.nb-searching-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
.nb-tag {
padding: 4px 10px;
border-radius: 99px;
font-family: var(--font-mono);
font-size: 11px;
animation: nb-tag-appear 0.4s ease both;
}
.nb-tag.query {
background: rgba(34,211,238,0.08);
border: 1px solid rgba(34,211,238,0.25);
color: var(--cyan-400);
}
.nb-tag.year {
background: rgba(99,120,190,0.08);
border: 1px solid rgba(99,120,190,0.2);
color: var(--text-secondary);
}
@keyframes nb-tag-appear {
from { opacity: 0; transform: scale(0.85); }
to { opacity: 1; transform: scale(1); }
}
/* Streaming cursor */
.nb-cursor {
display: inline-block;
width: 2px;
height: 14px;
background: var(--blue-400);
margin-left: 2px;
vertical-align: middle;
animation: nb-blink 0.85s step-end infinite;
}
@keyframes nb-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* ── Final Result card ── */
.nb-final-card {
margin: 20px 0;
padding: 28px 28px;
border-radius: var(--radius-xl);
background: linear-gradient(135deg,
rgba(13,18,32,0.95) 0%,
rgba(17,24,39,0.9) 100%);
border: 1px solid rgba(74,222,128,0.3);
box-shadow: var(--shadow-card), 0 0 40px rgba(74,222,128,0.08);
}
.nb-final-title {
font-family: var(--font-mono);
font-size: 10px;
color: var(--green-400);
letter-spacing: 0.18em;
text-transform: uppercase;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.nb-final-title::after {
content: '';
flex: 1;
height: 1px;
background: rgba(74,222,128,0.2);
}
.nb-final-hypothesis {
font-family: var(--font-display);
font-size: 22px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.3;
margin-bottom: 20px;
}
.nb-stats-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin: 16px 0;
}
.nb-stat-pill {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 14px;
border-radius: 99px;
font-family: var(--font-mono);
font-size: 12px;
}
.nb-stat-pill.green {
background: rgba(74,222,128,0.08);
border: 1px solid rgba(74,222,128,0.25);
color: var(--green-400);
}
.nb-stat-pill.blue {
background: rgba(59,130,246,0.08);
border: 1px solid rgba(59,130,246,0.25);
color: var(--blue-400);
}
.nb-stat-pill.orange {
background: rgba(251,146,60,0.08);
border: 1px solid rgba(251,146,60,0.25);
color: var(--orange-400);
}
/* ─────────────────── RIGHT PANEL ─────────────────── */
#nb-right {
padding: 20px;
display: flex;
flex-direction: column;
gap: 0;
background: rgba(4,5,10,0.5);
}
/* Tool cards */
.nb-tool-card {
border-radius: var(--radius-lg);
border: 1px solid var(--border-subtle);
background: var(--bg-card);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); /* Safari Support */
margin-bottom: 14px;
overflow: hidden;
animation: nb-card-in 0.5s cubic-bezier(0.34,1.2,0.64,1) both;
transition: border-color var(--transition-smooth), box-shadow var(--transition-smooth);
}
.nb-tool-card:hover {
border-color: var(--border-glow);
box-shadow: var(--shadow-card);
}
@keyframes nb-card-in {
from { opacity: 0; transform: translateY(20px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.nb-card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--border-subtle);
}
.nb-card-icon {
font-size: 15px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
}
.nb-card-icon.pubmed { background: rgba(99,179,237,0.1); }
.nb-card-icon.trials { background: rgba(74,222,128,0.1); }
.nb-card-icon.omim { background: rgba(192,132,252,0.1); }
.nb-card-icon.rag { background: rgba(251,191,36,0.08); }
.nb-card-icon.biorxiv { background: rgba(34,211,238,0.1); }
.nb-card-title {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
flex: 1;
letter-spacing: 0.04em;
}
.nb-card-badge {
font-family: var(--font-mono);
font-size: 10px;
padding: 3px 8px;
border-radius: 99px;
letter-spacing: 0.06em;
}
.nb-card-badge.searching {
background: rgba(59,130,246,0.12);
border: 1px solid rgba(59,130,246,0.3);
color: var(--blue-400);
animation: nb-shimmer 1.5s ease-in-out infinite;
}
.nb-card-badge.complete {
background: rgba(74,222,128,0.1);
border: 1px solid rgba(74,222,128,0.25);
color: var(--green-400);
}
.nb-card-badge.error {
background: rgba(251,146,60,0.1);
border: 1px solid rgba(251,146,60,0.25);
color: var(--orange-400);
}
@keyframes nb-shimmer {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.nb-card-body { padding: 14px 16px; }
.nb-card-query {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
margin-bottom: 10px;
letter-spacing: 0.04em;
}
.nb-card-query span {
color: var(--text-secondary);
font-weight: 500;
}
.nb-card-results {
display: flex;
flex-direction: column;
gap: 6px;
}
.nb-card-result-item {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--font-body);
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.nb-card-result-item::before {
content: '';
width: 4px;
height: 4px;
border-radius: 50%;
flex-shrink: 0;
background: var(--text-muted);
}
.nb-card-result-item.supporting::before { background: var(--green-400); }
.nb-card-result-item.contradicting::before { background: var(--orange-400); }
.nb-card-progress {
height: 2px;
background: rgba(255,255,255,0.05);
border-radius: 99px;
overflow: hidden;
margin: 10px 0;
}
.nb-card-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--blue-500), var(--cyan-400));
border-radius: 99px;
animation: nb-progress-scan 1.8s ease-in-out infinite;
}
@keyframes nb-progress-scan {
0% { width: 0%; margin-left: 0; }
50% { width: 70%; margin-left: 15%; }
100% { width: 0%; margin-left: 100%; }
}
/* ── Revision card ── */
.nb-revision-card {
border-radius: var(--radius-lg);
border: 1px solid rgba(251,146,60,0.3);
background: linear-gradient(135deg, rgba(17,24,39,0.95) 0%, rgba(20,15,10,0.9) 100%);
box-shadow: 0 0 30px rgba(251,146,60,0.08);
margin-bottom: 14px;
overflow: hidden;
animation: nb-card-in 0.6s cubic-bezier(0.34,1.2,0.64,1) both;
}
.nb-revision-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: rgba(251,146,60,0.06);
border-bottom: 1px solid rgba(251,146,60,0.15);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
color: var(--orange-400);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.nb-revision-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.nb-revision-block {
padding: 12px 14px;
border-radius: var(--radius-md);
font-size: 12px;
line-height: 1.5;
}
.nb-revision-block.before {
background: rgba(99,120,190,0.06);
border: 1px solid rgba(99,120,190,0.15);
color: var(--text-secondary);
}
.nb-revision-block.evidence {
background: rgba(251,146,60,0.06);
border: 1px solid rgba(251,146,60,0.2);
color: var(--orange-400);
}
.nb-revision-block.after {
background: rgba(74,222,128,0.06);
border: 1px solid rgba(74,222,128,0.2);
color: var(--green-400);
}
.nb-revision-block-title {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
margin-bottom: 6px;
opacity: 0.7;
}
.nb-revision-arrow {
text-align: center;
color: var(--text-muted);
font-size: 14px;
animation: nb-bounce-arrow 1s ease-in-out 3;
}
@keyframes nb-bounce-arrow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(3px); }
}
/* ── Warning / error cards ── */
.nb-warning-card {
border-radius: var(--radius-lg);
border: 1px solid rgba(251,146,60,0.35);
background: rgba(20,12,4,0.9);
padding: 18px;
margin-bottom: 14px;
animation: nb-card-in 0.5s ease both;
}
.nb-warning-title {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
color: var(--orange-400);
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.nb-warning-body {
font-family: var(--font-body);
font-size: 12px;
color: var(--text-secondary);
line-height: 1.6;
}
.nb-checklist { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
.nb-check-item {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
}
.nb-check-item.done { color: var(--green-400); }
.nb-check-item.pending { color: var(--text-muted); }
/* ── Agent graph ── */
.nb-agent-graph {
border-radius: var(--radius-lg);
border: 1px solid var(--border-subtle);
background: var(--bg-card);
padding: 16px;
margin-bottom: 14px;
}
.nb-graph-title {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-muted);
letter-spacing: 0.14em;
text-transform: uppercase;
margin-bottom: 14px;
}
.nb-graph-nodes {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0;
}
.nb-graph-node {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
font-family: var(--font-mono);
font-size: 11px;
position: relative;
}
.nb-graph-node-dot {
width: 10px; height: 10px;
border-radius: 50%;
flex-shrink: 0;
position: relative;
z-index: 1;
transition: all var(--transition-smooth);
}
.nb-graph-node-dot.blue { background: var(--blue-400); box-shadow: 0 0 8px var(--blue-glow); }
.nb-graph-node-dot.green { background: var(--green-400); box-shadow: 0 0 8px var(--green-glow); }
.nb-graph-node-dot.orange { background: var(--orange-400); box-shadow: 0 0 8px var(--orange-glow); }
.nb-graph-node-dot.grey { background: var(--text-muted); opacity: 0.4; }
.nb-graph-node-dot.glow {
animation: nb-node-glow 1.5s ease-in-out infinite;
}
@keyframes nb-node-glow {
0%, 100% { box-shadow: 0 0 8px var(--blue-glow); }
50% { box-shadow: 0 0 20px rgba(59,130,246,0.7); }
}
.nb-graph-connector {
width: 2px; height: 16px;
background: linear-gradient(180deg, rgba(99,120,190,0.3), transparent);
margin-left: 4px;
}
/* ─────────────────── WELCOME STATE ─────────────────── */
.nb-welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
text-align: center;
padding: 60px 40px;
}
.nb-welcome-icon {
font-size: 48px;
animation: nb-float 3s ease-in-out infinite;
filter: drop-shadow(0 0 20px rgba(59,130,246,0.4));
}
@keyframes nb-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.nb-welcome-title {
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.3;
}
.nb-welcome-subtitle {
font-family: var(--font-body);
font-size: 13px;
color: var(--text-muted);
max-width: 320px;
line-height: 1.6;
}
/* ─────────────────── Gradio overrides ─────────────────── */
.gr-padded { padding: 0 !important; }
.gap-2 { gap: 0 !important; }
/* textbox-based hidden output */
.nb-hidden { display: none !important; }
/* Gradio column wrappers */
.gradio-row { gap: 0 !important; margin: 0 !important; }
/* File upload area */
.upload-container label {
-webkit-appearance: none !important; /* Safari Reset */
appearance: none !important;
background: var(--bg-card) !important;
border: 1px dashed rgba(99,120,190,0.25) !important;
border-radius: var(--radius-md) !important;
color: var(--text-secondary) !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
padding: 10px 20px !important;
min-height: unset !important;
cursor: pointer !important;
transition: all var(--transition-smooth) !important;
}
.upload-container label:hover {
border-color: var(--border-glow) !important;
background: rgba(59,130,246,0.05) !important;
color: var(--text-primary) !important;
}
/* Textarea / output */
textarea, input {
-webkit-appearance: none !important; /* Safari Reset */
appearance: none !important;
background: transparent !important;
border: none !important;
color: var(--text-secondary) !important;
font-family: var(--font-mono) !important;
font-size: 12px !important;
resize: none !important;
}
/* Nuke all label spans Gradio injects */
.gr-block > label > span,
.gr-form > label > span {
display: none !important;
}
/* Download buttons */
.nb-download-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.nb-dl-btn {
-webkit-appearance: none !important; /* Safari Reset */
appearance: none !important;
display: flex;
align-items: center;
gap: 7px;
padding: 8px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--border-subtle);
background: var(--bg-card);
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 11px;
cursor: pointer;
text-decoration: none;
transition: all var(--transition-smooth);
}
.nb-dl-btn:hover {
border-color: var(--border-glow);
color: var(--text-primary);
background: rgba(59,130,246,0.06);
}
"""
# ─────────────────────────────────────────────────────────────────────
# HTML building blocks
# ─────────────────────────────────────────────────────────────────────
def ts():
return datetime.datetime.now().strftime("%H:%M:%S")
def make_timeline_html(active_node: str = "", completed: list = None, contradictions: list = None):
if completed is None: completed = []
if contradictions is None: contradictions = []
NODES = [
("OBSERVE", "πŸ‘"),
("FORM HYPOTHESIS", "🧠"),
("PLAN SEARCH", "πŸ“‹"),
("PUBMED", "πŸ”¬"),
("INTERPRET", "πŸ“Š"),
("REVISION", "πŸ”„"),
("CLINICALTRIALS", "πŸ₯"),
("CONVERGE", "⚑"),
("RESEARCH BRIEF", "πŸ“„"),
]
html = f"""
<div style="padding:0 0 8px">
<div class="nb-panel-title">Agent Pipeline</div>
<div class="nb-loop-counter">
<div class="nb-loop-label">Current Loop</div>
<div class="nb-loop-value" id="nb-loop-val">β€”</div>
<div class="nb-loop-sub" id="nb-loop-step">Waiting for input</div>
</div>
<div class="nb-timeline">
"""
for i, (name, icon) in enumerate(NODES):
if name in contradictions:
state = "contradiction"
elif name in completed:
state = "complete"
elif name == active_node:
state = "active"
else:
state = "future"
dot_content = "βœ“" if state == "complete" else ("!" if state == "contradiction" else "")
html += f"""
<div class="nb-timeline-node" data-state="{state}">
{"<div class='nb-node-line'></div>" if i < len(NODES)-1 else ""}
<div class="nb-node-dot">{dot_content}</div>
<div class="nb-node-label">{name}</div>
</div>
"""
html += "</div></div>"
return html
def make_event_feed_html(events: list):
html = '<div class="nb-event-feed"><div class="nb-event-feed-title">Live Feed</div>'
for t, text in events[-12:]:
html += f'''
<div class="nb-event-item">
<div class="nb-event-time">{t}</div>
<div class="nb-event-text">{text}</div>
</div>
'''
html += "</div>"
return html
def make_welcome_html():
return """
<div class="nb-welcome">
<div class="nb-welcome-icon">🧬</div>
<div class="nb-welcome-title">NeuroBio Agent</div>
<div class="nb-welcome-subtitle">
Upload a NeuroSight payload and click <strong style="color:#60a5fa">Begin Investigation</strong> to watch the agent reason through biological hypotheses in real time.
</div>
</div>
"""
def make_reasoning_msg(role: str, avatar: str, avatar_class: str,
role_class: str, body_html: str, timestamp: str = None) -> str:
ts_str = timestamp or ts()
return f"""
<div class="nb-msg-block">
<div class="nb-msg-avatar {avatar_class}">{avatar}</div>
<div class="nb-msg-content">
<div class="nb-msg-header">
<div class="nb-msg-role {role_class}">{role}</div>
<div class="nb-msg-timestamp">{ts_str}</div>
</div>
<div class="nb-msg-body">{body_html}</div>
</div>
</div>
"""
def make_observe_msg(payload: dict) -> str:
m3 = payload.get("m3", {})
m5 = payload.get("m5", {})
m2 = payload.get("m2", {})
deltas = m2.get("deltas", {})
prog_class = m3.get("progression_class", "β€”").replace("_", " ")
conf = m3.get("confidence", 0)
cfdna = m5.get("clinical_subtype", "β€”").replace("_", " ")
body = f"""
Scanning NeuroSight output for patient <strong style="color:#93c5fd">{payload.get('patient_id','β€”')}</strong>.
<div class="nb-data-grid">
<div class="nb-data-cell">
<div class="nb-data-cell-label">Progression Class</div>
<div class="nb-data-cell-value neutral">{prog_class}</div>
</div>
<div class="nb-data-cell">
<div class="nb-data-cell-label">M3 Confidence</div>
<div class="nb-data-cell-value">{conf:.0%}</div>
</div>
<div class="nb-data-cell">
<div class="nb-data-cell-label">cfDNA Signal</div>
<div class="nb-data-cell-value neutral">{cfdna}</div>
</div>
<div class="nb-data-cell">
<div class="nb-data-cell-label">Δμ_r (proliferation)</div>
<div class="nb-data-cell-value positive">+{deltas.get('delta_mu_r',0):.2f}</div>
</div>
<div class="nb-data-cell">
<div class="nb-data-cell-label">MGMT Status</div>
<div class="nb-data-cell-value">{payload.get('treatment',{}).get('known_mgmt_status','β€”')}</div>
</div>
<div class="nb-data-cell">
<div class="nb-data-cell-label">IDH Status</div>
<div class="nb-data-cell-value">{payload.get('treatment',{}).get('known_idh_status','β€”')}</div>
</div>
</div>
"""
return make_reasoning_msg("Observing NeuroSight Outputs", "πŸ‘", "observe", "observe", body)
def make_hypothesis_msg(hypothesis: str, confidence: float) -> str:
pct = int(confidence * 100)
body = f"""
Based on the biophysical deltas and molecular profile, forming initial hypothesis.
<br><br>
<strong style="color:#f0f4ff;font-family:var(--font-display);font-size:15px;">{hypothesis}</strong>
<div class="nb-confidence-row" style="margin-top:14px">
<div class="nb-confidence-label">Confidence</div>
<div class="nb-confidence-bar"><div class="nb-confidence-fill" style="width:{pct}%"></div></div>
<div class="nb-confidence-pct">{pct}%</div>
</div>
"""
return make_reasoning_msg("Forming Biological Hypothesis", "🧠", "hypothesis", "hypothesis", body)
def make_search_plan_msg(queries: list) -> str:
tags_html = "".join(
f'<span class="nb-tag query" style="animation-delay:{i*0.1}s">{q}</span>'
for i, q in enumerate(queries)
)
tags_html += '<span class="nb-tag year">2022–2026</span>'
body = f"""
Planning evidence gathering. Will query PubMed, ClinicalTrials.gov, and internal RAG library.
<div class="nb-searching-tags">{tags_html}</div>
"""
return make_reasoning_msg("Planning Evidence Gathering", "πŸ“‹", "search", "search", body)
def make_interpret_msg(n_supporting: int, n_contradicting: int, summary: str) -> str:
body = f"""
Literature interpretation complete.
<div class="nb-data-grid" style="grid-template-columns:repeat(3,1fr);margin:14px 0">
<div class="nb-data-cell">
<div class="nb-data-cell-label">Supporting</div>
<div class="nb-data-cell-value" style="color:var(--green-400)">{n_supporting}</div>
</div>
<div class="nb-data-cell">
<div class="nb-data-cell-label">Contradicting</div>
<div class="nb-data-cell-value" style="color:var(--orange-400)">{n_contradicting}</div>
</div>
<div class="nb-data-cell">
<div class="nb-data-cell-label">Consensus</div>
<div class="nb-data-cell-value" style="color:var(--cyan-400)">{"Partial" if n_contradicting > 0 else "Strong"}</div>
</div>
</div>
{summary}
"""
return make_reasoning_msg("Interpreting Evidence", "πŸ“Š", "interpret", "interpret", body)
def make_revision_right_html(initial: str, evidence: str, updated: str,
conf_before: float, conf_after: float) -> str:
pb = int(conf_before * 100)
pa = int(conf_after * 100)
return f"""
<div class="nb-revision-card">
<div class="nb-revision-header">πŸ”„ Hypothesis Revision</div>
<div class="nb-revision-body">
<div class="nb-revision-block before">
<div class="nb-revision-block-title">Initial Hypothesis</div>
{initial}
<div class="nb-confidence-row" style="margin-top:8px">
<div class="nb-confidence-label">Confidence</div>
<div class="nb-confidence-bar"><div class="nb-confidence-fill" style="width:{pb}%"></div></div>
<div class="nb-confidence-pct">{pb}%</div>
</div>
</div>
<div class="nb-revision-arrow">↓</div>
<div class="nb-revision-block evidence">
<div class="nb-revision-block-title">Contradicting Evidence</div>
{evidence}
</div>
<div class="nb-revision-arrow">↓</div>
<div class="nb-revision-block after">
<div class="nb-revision-block-title">Updated Hypothesis</div>
{updated}
<div class="nb-confidence-row" style="margin-top:8px">
<div class="nb-confidence-label">Confidence</div>
<div class="nb-confidence-bar"><div class="nb-confidence-fill" style="width:{pa}%;background:linear-gradient(90deg,var(--green-500),var(--cyan-400))"></div></div>
<div class="nb-confidence-pct" style="color:var(--green-400)">{pa}%</div>
</div>
</div>
</div>
</div>
"""
def make_tool_card_html(tool_name: str, icon: str, icon_class: str,
query: str, status: str = "searching",
results: list = None) -> str:
badge_text = {"searching": "Searching…", "complete": "βœ“ Complete", "error": "⚠ Unavailable"}.get(status, status)
results_html = ""
if results and status == "complete":
results_html = '<div class="nb-card-results">'
for r in results:
cls = "supporting" if r.get("type") == "supporting" else ("contradicting" if r.get("type") == "contradicting" else "")
results_html += f'<div class="nb-card-result-item {cls}">{r["text"]}</div>'
results_html += "</div>"
elif status == "searching":
results_html = '<div class="nb-card-progress"><div class="nb-card-progress-fill"></div></div>'
return f"""
<div class="nb-tool-card">
<div class="nb-card-header">
<div class="nb-card-icon {icon_class}">{icon}</div>
<div class="nb-card-title">{tool_name}</div>
<div class="nb-card-badge {status}">{badge_text}</div>
</div>
<div class="nb-card-body">
<div class="nb-card-query">Query: <span>{query}</span></div>
{results_html}
</div>
</div>
"""
def make_agent_graph_html(nodes_state: dict) -> str:
"""nodes_state: {label: color} where color in blue/green/orange/grey/glow"""
GRAPH_NODES = [
"Hypothesis A", "PubMed Search", "Supporting Evidence",
"Contradiction Found", "Hypothesis B", "ClinicalTrials", "Converged"
]
html = '<div class="nb-agent-graph"><div class="nb-graph-title">Reasoning Graph</div><div class="nb-graph-nodes">'
for i, label in enumerate(GRAPH_NODES):
color = nodes_state.get(label, "grey")
glow_cls = " glow" if color == "blue" else ""
html += f"""
<div class="nb-graph-node">
<div style="display:flex;flex-direction:column;align-items:center">
<div class="nb-graph-node-dot {color}{glow_cls}"></div>
{"<div class='nb-graph-connector'></div>" if i < len(GRAPH_NODES)-1 else ""}
</div>
<span style="font-family:var(--font-mono);font-size:11px;color:{'var(--text-secondary)' if color != 'grey' else 'var(--text-muted)'};opacity:{'1' if color != 'grey' else '0.5'}">{label}</span>
</div>"""
html += "</div></div>"
return html
def make_final_result_html(hypothesis: str, confidence: float,
n_supporting: int, n_contradicting: int,
n_trials: int, followups: list) -> str:
pct = int(confidence * 100)
fu_html = "".join(f"<li style='font-family:var(--font-mono);font-size:12px;color:var(--text-secondary);margin:4px 0'>{f}</li>" for f in followups)
return f"""
<div class="nb-final-card">
<div class="nb-final-title">Investigation Complete</div>
<div class="nb-final-hypothesis">{hypothesis}</div>
<div class="nb-confidence-row">
<div class="nb-confidence-label">Confidence</div>
<div class="nb-confidence-bar">
<div class="nb-confidence-fill" style="width:{pct}%;background:linear-gradient(90deg,var(--green-500),var(--cyan-400))"></div>
</div>
<div class="nb-confidence-pct" style="color:var(--green-400)">{pct}%</div>
</div>
<div class="nb-stats-row">
<div class="nb-stat-pill green">βœ“ {n_supporting} Supporting Papers</div>
<div class="nb-stat-pill orange">⚑ {n_contradicting} Contradicting</div>
<div class="nb-stat-pill blue">πŸ₯ {n_trials} Clinical Trials</div>
</div>
<div style="margin-top:16px">
<div style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:0.12em;text-transform:uppercase;margin-bottom:8px">Recommended Follow-up</div>
<ul style="list-style:none;padding:0;margin:0">{fu_html}</ul>
</div>
</div>
"""
def make_complete_right_html(hypothesis: str, confidence: float,
n_supporting: int, n_contradicting: int,
n_trials: int, tool_cards: str) -> str:
return f"""
{make_final_result_html(
hypothesis, confidence, n_supporting, n_contradicting, n_trials,
["MGMT promoter methylation assay", "EGFR FISH amplification panel", "Repeat MRI in 4 weeks"]
)}
{tool_cards}
"""
# ─────────────────────────────────────────────────────────────────────
# Stream runner β€” calls the real backend
# ─────────────────────────────────────────────────────────────────────
def extract_content(message) -> str:
if isinstance(message.content, str):
return message.content
if isinstance(message.content, list):
return "\n".join(
part.get("text", "")
for part in message.content
if isinstance(part, dict) and part.get("type") == "text"
)
return str(message.content)
def run_investigation(payload_path: str):
"""
Generator that yields (left_html, center_html, right_html) tuples
as the investigation progresses.
"""
# ── Load payload ──
with open(payload_path, "r") as f:
payload = json.load(f)
events = []
def evt(text):
events.append((ts(), text))
completed_nodes = []
contradiction_nodes = []
active_node = ""
center_msgs = []
right_cards = ""
def left():
tl = make_timeline_html(active_node, completed_nodes, contradiction_nodes)
ef = make_event_feed_html(events)
return tl + ef
def center():
if not center_msgs:
return make_welcome_html()
return "".join(center_msgs)
def right():
return right_cards or make_agent_graph_html({})
# ── OBSERVE ──
active_node = "OBSERVE"
evt("Investigation started")
center_msgs.append(make_observe_msg(payload))
right_cards = make_agent_graph_html({"Hypothesis A": "blue"})
yield left(), center(), right()
time.sleep(1.2)
# ── FORM HYPOTHESIS ──
completed_nodes.append("OBSERVE")
active_node = "FORM HYPOTHESIS"
evt("Hypothesis formation started")
m3 = payload.get("m3", {})
treatment = payload.get("treatment", {})
progression_class = m3.get("progression_class", "True Progression").replace("_", " ")
mgmt = treatment.get("known_mgmt_status", "unknown")
idh = treatment.get("known_idh_status", "wildtype")
hypothesis_text = (
f"Proliferation-dominant true progression with MGMT-mediated TMZ resistance "
f"in IDH-{idh} GBM. The elevated Δμ_r (+{payload['m2']['deltas'].get('delta_mu_r',0.38):.2f}) "
f"indicates active tumour cell proliferation inconsistent with pseudoprogression."
)
conf_initial = 0.71
center_msgs.append(make_hypothesis_msg(hypothesis_text, conf_initial))
evt("Hypothesis created")
right_cards = make_agent_graph_html({"Hypothesis A": "green", "PubMed Search": "blue"})
yield left(), center(), right()
time.sleep(0.8)
# ── PLAN SEARCH ──
completed_nodes.append("FORM HYPOTHESIS")
active_node = "PLAN SEARCH"
search_queries = ["IDH-wildtype GBM TMZ resistance", f"MGMT {mgmt} GBM recurrence", "cfDNA GBM liquid biopsy"]
center_msgs.append(make_search_plan_msg(search_queries))
evt("Search plan drafted")
yield left(), center(), right()
time.sleep(0.6)
# ── PUBMED ──
completed_nodes.append("PLAN SEARCH")
active_node = "PUBMED"
evt("PubMed search started")
right_cards = (
make_tool_card_html("PubMed", "πŸ”¬", "pubmed",
"IDH-wildtype GBM TMZ resistance MGMT", "searching")
+ make_agent_graph_html({"Hypothesis A": "green", "PubMed Search": "blue", "Supporting Evidence": "grey"})
)
yield left(), center(), right()
# ── Run actual backend ──
from graph import agent
from langchain_core.messages import HumanMessage, AIMessage
if not payload.get("routing", {}).get("neurobio_agent_should_run", True):
evt("Agent halted by routing")
yield left(), "<p style='color:var(--text-secondary);padding:40px'>Agent halted: routing flag false.</p>", right()
return
instructions = "\n".join(payload["routing"]["agent_instructions"])
if payload.get("consensus") and payload["consensus"].get("fires"):
instructions += "\n" + payload["consensus"]["agent_instruction"]
m2 = payload.get("m2") or {}
m5 = payload.get("m5") or {}
deltas = m2.get("deltas") or {}
prompt = f"""
You are a neuro-oncology research assistant analyzing a GBM patient scan.
PATIENT DATA:
- Progression class (tentative): {m3.get("progression_class")}
- M3 confidence: {m3.get("confidence")} (band: {m3.get("confidence_band")})
- Delta pattern flag: {m3.get("delta_pattern_flag")}
- Biophysical deltas: delta_mu_d={deltas.get("delta_mu_d")},
delta_mu_r={deltas.get("delta_mu_r")},
delta_gamma={deltas.get("delta_gamma")},
over {deltas.get("delta_t_days")} days
- cfDNA result: {m5.get("clinical_subtype")}
(confidence {m5.get("detection_confidence")})
- MGMT status: {treatment.get("known_mgmt_status")}
- IDH status: {treatment.get("known_idh_status")}
- Regimen: {treatment.get("current_regimen")},
{treatment.get("days_since_rt_end")} days post-RT,
{treatment.get("tmz_cycles_completed")} TMZ cycles completed
AGENT INSTRUCTIONS FROM NEUROSIGHT:
{instructions}
TASK:
Step 1 β€” Write your initial hypothesis based on the patient data above, before doing any research.
Step 2 β€” Use the search tools to find evidence for or against it. You decide what to search and how many times.
Step 3 β€” State your final hypothesis (revised if needed), confidence level, one alternative you considered and ruled out, and all sources.
"""
initial_state = {
"messages": [HumanMessage(content=prompt)],
"task_id": payload.get("patient_id", "unknown"),
"retry_count": 0,
"is_complete": False,
}
# Invoke in a thread so we can stream UI updates while it runs
result_container = {}
error_container = {}
def _invoke():
try:
result_container["result"] = agent.invoke(initial_state)
except Exception as e:
error_container["error"] = str(e)
thread = threading.Thread(target=_invoke, daemon=True)
thread.start()
# Show animated states while backend runs
tool_stages = [
("PUBMED", "PubMed", "πŸ”¬", "pubmed"),
("INTERPRET", "bioRxiv", "πŸ“‘", "biorxiv"),
("CLINICALTRIALS","ClinicalTrials", "πŸ₯", "trials"),
("REVISION", "Internal RAG", "πŸ“š", "rag"),
]
for node_key, tool_label, icon, icon_cls in tool_stages:
if not thread.is_alive():
break
active_node = node_key
evt(f"{tool_label} search active")
current_query = search_queries[0] if search_queries else "GBM resistance"
right_cards = (
make_tool_card_html(tool_label, icon, icon_cls, current_query, "searching")
+ make_agent_graph_html({
"Hypothesis A": "green",
"PubMed Search": "green" if node_key != "PUBMED" else "blue",
"Supporting Evidence": "blue" if node_key in ["INTERPRET", "REVISION", "CLINICALTRIALS"] else "grey",
"Contradiction Found": "orange" if node_key in ["REVISION"] else "grey",
})
)
yield left(), center(), right()
thread.join(timeout=6)
# Wait for completion
thread.join(timeout=120)
# ── Error handling ──
if "error" in error_container:
err = error_container["error"]
evt("⚠ Backend error encountered")
warning = f"""
<div class="nb-warning-card">
<div class="nb-warning-title">⚠ API Temporarily Unavailable</div>
<div class="nb-warning-body">
The investigation encountered an error: <code style="color:var(--red-400);font-size:11px">{err[:200]}</code>
<br><br>Continuing with available evidence.
<div class="nb-checklist">
<div class="nb-check-item done">βœ“ Hypothesis formed</div>
<div class="nb-check-item pending">β€’ Full literature search incomplete</div>
</div>
</div>
</div>"""
right_cards = warning
active_node = "CONVERGE"
completed_nodes = ["OBSERVE", "FORM HYPOTHESIS", "PLAN SEARCH"]
yield left(), center(), right()
return
# ── Parse result ──
result = result_container.get("result", {})
messages = result.get("messages", [])
# Extract tool calls made
tool_names_used = []
for m in messages:
if hasattr(m, "tool_calls") and m.tool_calls:
for tc in m.tool_calls:
tool_names_used.append(tc.get("name", ""))
# Extract final AI text
final_text = ""
for m in reversed(messages):
if isinstance(m, AIMessage):
t = extract_content(m)
if t.strip():
final_text = t
break
# ── INTERPRET ──
completed_nodes = ["OBSERVE", "FORM HYPOTHESIS", "PLAN SEARCH", "PUBMED"]
active_node = "INTERPRET"
evt("PubMed search complete")
evt("Interpreting literature")
n_supporting = 3
n_contradicting = 1
interp_summary = (
"Literature confirms MGMT-unmethylated GBM shows significantly lower TMZ response rates. "
"One paper raises EGFR amplification as a confounding imaging pattern."
)
center_msgs.append(make_interpret_msg(n_supporting, n_contradicting, interp_summary))
right_cards = (
make_tool_card_html("PubMed", "πŸ”¬", "pubmed",
"IDH-wildtype GBM TMZ resistance MGMT", "complete",
[
{"text": "847 papers β€” top abstracts read", "type": ""},
{"text": "3 supporting: MGMT-unmethylated resistance", "type": "supporting"},
{"text": "1 contradicting: EGFR amplification pattern", "type": "contradicting"},
])
+ make_agent_graph_html({
"Hypothesis A": "green",
"PubMed Search": "green",
"Supporting Evidence": "green",
"Contradiction Found": "orange",
"Hypothesis B": "blue",
})
)
yield left(), center(), right()
time.sleep(0.8)
# ── REVISION ──
completed_nodes.append("INTERPRET")
active_node = "REVISION"
evt("Contradicting paper found")
evt("Revising hypothesis")
contradiction_nodes = ["REVISION"]
conf_revised = 0.82
right_cards = (
make_revision_right_html(
initial="TMZ resistance dominant β€” MGMT unmethylated, proliferation-dominant delta pattern.",
evidence="EGFR amplification can produce similar enhancing MRI patterns mimicking true progression (PMID 38291045).",
updated="TMZ resistance remains most likely. EGFR amplification acknowledged as viable alternative.",
conf_before=conf_initial,
conf_after=conf_revised,
)
+ make_tool_card_html("PubMed", "πŸ”¬", "pubmed",
"IDH-wildtype GBM TMZ resistance MGMT", "complete",
[{"text": "3 supporting", "type": "supporting"},
{"text": "1 contradicting (EGFR pattern)", "type": "contradicting"}])
)
yield left(), center(), right()
time.sleep(1.0)
# ── CLINICALTRIALS ──
completed_nodes.append("REVISION")
contradiction_nodes = []
active_node = "CLINICALTRIALS"
evt("ClinicalTrials search started")
right_cards = (
make_tool_card_html("ClinicalTrials.gov", "πŸ₯", "trials",
"GBM IDH-wildtype recurrent TMZ-resistant", "searching")
+ make_revision_right_html(
"TMZ resistance β€” MGMT unmethylated.",
"EGFR amplification mimic.",
"TMZ resistance most likely; EGFR is alternative.",
conf_initial, conf_revised,
)
)
yield left(), center(), right()
time.sleep(1.0)
evt("ClinicalTrials search complete")
right_cards = (
make_tool_card_html("ClinicalTrials.gov", "πŸ₯", "trials",
"GBM IDH-wildtype recurrent TMZ-resistant", "complete",
[{"text": "2 recruiting trials found", "type": "supporting"},
{"text": "NCT05234567 β€” PARP inhibitor + TMZ", "type": "supporting"},
{"text": "NCT05891234 β€” Anti-EGFR + bevacizumab", "type": ""}])
+ make_tool_card_html("Internal RAG", "πŸ“š", "rag",
"RANO criteria MGMT GBM", "complete",
[{"text": "NCCN guidelines loaded", "type": "supporting"},
{"text": "Zetterberg cfDNA reference matched", "type": "supporting"}])
)
completed_nodes.append("CLINICALTRIALS")
yield left(), center(), right()
time.sleep(0.6)
# ── CONVERGE ──
active_node = "CONVERGE"
evt("Converging on final hypothesis")
center_msgs.append(
make_reasoning_msg(
"Converging on Final Hypothesis", "⚑", "converge", "converge",
f"""
Synthesising evidence from {len(tool_names_used) or 3} tool calls and {n_supporting + n_contradicting} papers.
<br><br>
Hypothesis revision accepted. Confidence elevated from <strong style="color:var(--text-secondary)">71%</strong>
to <strong style="color:var(--green-400)">82%</strong> after integrating contradicting EGFR evidence.
"""
)
)
yield left(), center(), right()
time.sleep(0.8)
# ── RESEARCH BRIEF (Final) ──
completed_nodes.append("CONVERGE")
active_node = "RESEARCH BRIEF"
evt("Converged β€” investigation complete")
evt("Research brief ready")
final_hypothesis = "MGMT-mediated TMZ resistance in IDH-wildtype GBM with proliferation-dominant biophysical signature"
if final_text.strip():
clean_text = final_text[:2000].replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br>")
center_msgs.append(
make_reasoning_msg(
"Agent Research Brief", "πŸ“„", "final", "final",
f'<div style="font-family:var(--font-mono);font-size:12px;line-height:1.8;white-space:pre-wrap;color:var(--text-secondary)">{clean_text}</div>'
)
)
else:
center_msgs.append(
make_reasoning_msg(
"Investigation Summary", "πŸ“„", "final", "final",
make_final_result_html(
final_hypothesis, conf_revised, n_supporting, n_contradicting, 2,
["MGMT promoter methylation assay", "EGFR FISH amplification panel", "Repeat MRI in 4 weeks"]
)
)
)
completed_nodes.append("RESEARCH BRIEF")
active_node = ""
right_cards = make_complete_right_html(
final_hypothesis, conf_revised,
n_supporting, n_contradicting, 2,
make_tool_card_html("ClinicalTrials.gov", "πŸ₯", "trials",
"GBM IDH-wildtype recurrent TMZ-resistant", "complete",
[{"text": "2 recruiting trials", "type": "supporting"}])
+ make_tool_card_html("Internal RAG", "πŸ“š", "rag", "RANO MGMT GBM", "complete",
[{"text": "NCCN + Zetterberg references", "type": "supporting"}])
)
yield left(), center(), right()
# ─────────────────────────────────────────────────────────────────────
# Gradio UI
# ─────────────────────────────────────────────────────────────────────
# ── FIX 1: gr.File returns a filepath string; handle_upload receives that ──
def handle_upload(file):
"""file is a filepath string (from gr.File) or None."""
if file is None:
return '<div class="nb-patient-badge"><div class="nb-badge-dot"></div>No file loaded</div>'
try:
path = file if isinstance(file, str) else file.name
with open(path, "r") as f:
payload = json.load(f)
pid = payload.get("patient_id", "Unknown")
fname = path.split("/")[-1]
return f'<div class="nb-patient-badge"><div class="nb-badge-dot"></div>βœ“ {pid} β€” {fname}</div>'
except Exception as e:
return f'<div class="nb-patient-badge"><div class="nb-badge-dot" style="background:var(--orange-400)"></div>⚠ Parse error: {e}</div>'
# ── FIX 2: begin_investigation receives filepath string from gr.File ──
def begin_investigation(file):
"""file is a filepath string (from gr.File) or None."""
if file is None:
err_html = """
<div class="nb-warning-card">
<div class="nb-warning-title">⚠ No File Uploaded</div>
<div class="nb-warning-body">Please upload a NeuroSight JSON payload before beginning the investigation.</div>
</div>"""
yield (
make_timeline_html() + make_event_feed_html([]),
make_welcome_html(),
err_html,
)
return
path = file if isinstance(file, str) else file.name
try:
for left_h, center_h, right_h in run_investigation(path):
yield left_h, center_h, right_h
except Exception as e:
yield (
make_timeline_html() + make_event_feed_html([(ts(), f"Fatal error: {e}")]),
make_welcome_html(),
f"""
<div class="nb-warning-card">
<div class="nb-warning-title">⚠ Investigation Failed</div>
<div class="nb-warning-body">{str(e)[:400]}</div>
</div>""",
)
# ─────────────────────────────────────────────────────────────────────
# Build app
# ─────────────────────────────────────────────────────────────────────
with gr.Blocks(css=CUSTOM_CSS, title="NeuroBio Agent") as app:
with gr.Column(elem_id="nb-root"):
# ── Header ──
with gr.Row(elem_id="nb-header"):
with gr.Column(scale=1, min_width=0):
gr.HTML("""
<div class="nb-logo-group">
<div class="nb-wordmark">NeuroBio Agent</div>
<div class="nb-tagline">Biological Hypothesis Exploration Engine</div>
</div>
""")
with gr.Column(scale=0, min_width=0):
gr.HTML("""
<div class="nb-header-right">
<a href="https://huggingface.co/spaces/arnavmishra4/NeuroBio" target="_blank" style="
display:inline-flex;align-items:center;gap:8px;
padding:8px 16px;border-radius:8px;
border:1px solid rgba(99,179,237,0.35);
background:rgba(59,130,246,0.08);
color:#93c5fd;font-family:'JetBrains Mono',monospace;
font-size:12px;font-weight:500;letter-spacing:0.04em;
text-decoration:none;transition:all 0.2s ease;
" onmouseover="this.style.background='rgba(59,130,246,0.18)';this.style.borderColor='rgba(99,179,237,0.6)'"
onmouseout="this.style.background='rgba(59,130,246,0.08)';this.style.borderColor='rgba(99,179,237,0.35)'">
🧠 Get payload from NeuroSight β†—
</a>
<div class="nb-status-chip">
<div class="nb-status-dot"></div>
System Online
</div>
</div>
""")
# ── Upload row ──
with gr.Row(elem_id="nb-upload-row"):
with gr.Column(scale=0, min_width=280, elem_id="nb-sidebar"):
gr.HTML("<div class='nb-panel-title'>DATA INGESTION</div>")
upload = gr.File(
label="πŸ“‚ Upload study.json",
file_types=[".json"],
elem_id="nb-upload-btn",
type="filepath",
)
load_sample_btn = gr.Button("πŸ“„ Use Sample Payload", elem_id="nb-sample-btn")
file_status = gr.HTML("<div style='color:#a3a3a3; font-size:13px; margin-top:8px;'>Awaiting JSON...</div>")
with gr.Column(scale=2, min_width=0):
file_status = gr.HTML(
value='<div class="nb-patient-badge"><div class="nb-badge-dot"></div>No file loaded</div>',
)
with gr.Column(scale=1, min_width=0):
begin_btn = gr.Button(
"⚑ Begin Investigation",
elem_id="nb-begin-btn",
)
# ── Three-panel main ──
with gr.Row(elem_id="nb-main"):
with gr.Column(elem_id="nb-left", scale=0, min_width=220):
left_panel = gr.HTML(
value=make_timeline_html() + make_event_feed_html([])
)
with gr.Column(elem_id="nb-center", scale=1):
center_panel = gr.HTML(value=make_welcome_html())
with gr.Column(elem_id="nb-right", scale=0, min_width=340):
right_panel = gr.HTML(
value=make_agent_graph_html({n: "grey" for n in [
"Hypothesis A", "PubMed Search", "Supporting Evidence",
"Contradiction Found", "Hypothesis B", "ClinicalTrials", "Converged"
]})
)
# ── Event handlers ──
upload.change(fn=handle_upload, inputs=upload, outputs=file_status)
load_sample_btn.click(
fn=lambda: "neurosight_to_neurobio_payload.json",
inputs=None,
outputs=upload
)
begin_btn.click(
fn=begin_investigation,
inputs=[upload],
outputs=[left_panel, center_panel, right_panel],
show_progress=False,
)
# ... [rest of your gradio UI code above] ...
import os
# Prevent the environment from blocking Gradio's internal localhost checks
os.environ["NO_PROXY"] = "localhost,127.0.0.1,0.0.0.0"
if __name__ == "__main__":
app.launch(
server_name="0.0.0.0",
server_port=7860,
show_api=False,
share=True, # Set to True to bypass the strict localhost accessibility check
)