# ── 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"""
Agent Pipeline
Current Loop
Waiting for input
""" 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"""
{"
" if i < len(NODES)-1 else ""}
{dot_content}
{name}
""" html += "
" return html def make_event_feed_html(events: list): html = '
Live Feed
' for t, text in events[-12:]: html += f'''
{t}
{text}
''' html += "
" return html def make_welcome_html(): return """
🧬
NeuroBio Agent
Upload a NeuroSight payload and click Begin Investigation to watch the agent reason through biological hypotheses in real time.
""" 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"""
{avatar}
{role}
{ts_str}
{body_html}
""" 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 {payload.get('patient_id','—')}.
Progression Class
{prog_class}
M3 Confidence
{conf:.0%}
cfDNA Signal
{cfdna}
Δμ_r (proliferation)
+{deltas.get('delta_mu_r',0):.2f}
MGMT Status
{payload.get('treatment',{}).get('known_mgmt_status','—')}
IDH Status
{payload.get('treatment',{}).get('known_idh_status','—')}
""" 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.

{hypothesis}
Confidence
{pct}%
""" return make_reasoning_msg("Forming Biological Hypothesis", "🧠", "hypothesis", "hypothesis", body) def make_search_plan_msg(queries: list) -> str: tags_html = "".join( f'{q}' for i, q in enumerate(queries) ) tags_html += '2022–2026' body = f""" Planning evidence gathering. Will query PubMed, ClinicalTrials.gov, and internal RAG library.
{tags_html}
""" 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.
Supporting
{n_supporting}
Contradicting
{n_contradicting}
Consensus
{"Partial" if n_contradicting > 0 else "Strong"}
{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"""
🔄 Hypothesis Revision
Initial Hypothesis
{initial}
Confidence
{pb}%
Contradicting Evidence
{evidence}
Updated Hypothesis
{updated}
Confidence
{pa}%
""" 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 = '
' for r in results: cls = "supporting" if r.get("type") == "supporting" else ("contradicting" if r.get("type") == "contradicting" else "") results_html += f'
{r["text"]}
' results_html += "
" elif status == "searching": results_html = '
' return f"""
{icon}
{tool_name}
{badge_text}
Query: {query}
{results_html}
""" 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 = '
Reasoning Graph
' for i, label in enumerate(GRAPH_NODES): color = nodes_state.get(label, "grey") glow_cls = " glow" if color == "blue" else "" html += f"""
{"
" if i < len(GRAPH_NODES)-1 else ""}
{label}
""" html += "
" 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"
  • {f}
  • " for f in followups) return f"""
    Investigation Complete
    {hypothesis}
    Confidence
    {pct}%
    ✓ {n_supporting} Supporting Papers
    ⚡ {n_contradicting} Contradicting
    🏥 {n_trials} Clinical Trials
    Recommended Follow-up
    """ 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(), "

    Agent halted: routing flag false.

    ", 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"""
    ⚠ API Temporarily Unavailable
    The investigation encountered an error: {err[:200]}

    Continuing with available evidence.
    ✓ Hypothesis formed
    • Full literature search incomplete
    """ 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.

    Hypothesis revision accepted. Confidence elevated from 71% to 82% 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("<", "<").replace(">", ">").replace("\n", "
    ") center_msgs.append( make_reasoning_msg( "Agent Research Brief", "📄", "final", "final", f'
    {clean_text}
    ' ) ) 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 '
    No file loaded
    ' 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'
    ✓ {pid} — {fname}
    ' except Exception as e: return f'
    ⚠ Parse error: {e}
    ' # ── 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 = """
    ⚠ No File Uploaded
    Please upload a NeuroSight JSON payload before beginning the investigation.
    """ 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"""
    ⚠ Investigation Failed
    {str(e)[:400]}
    """, ) # ───────────────────────────────────────────────────────────────────── # 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("""
    NeuroBio Agent
    Biological Hypothesis Exploration Engine
    """) with gr.Column(scale=0, min_width=0): gr.HTML("""
    🧠 Get payload from NeuroSight ↗
    System Online
    """) # ── Upload row ── with gr.Row(elem_id="nb-upload-row"): with gr.Column(scale=0, min_width=280, elem_id="nb-sidebar"): gr.HTML("
    DATA INGESTION
    ") 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("
    Awaiting JSON...
    ") with gr.Column(scale=2, min_width=0): file_status = gr.HTML( value='
    No file loaded
    ', ) 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 )