File size: 11,430 Bytes
f9a0bdb 39219c6 f9a0bdb 517de74 39219c6 994feb6 5e95a20 f9a0bdb 39219c6 517de74 39219c6 f9a0bdb 94febc8 f9a0bdb 39219c6 2b2ae99 39219c6 f9a0bdb 978c4cf 4372b0a a4f7e5c 39219c6 f9a0bdb 7e2c73b 39219c6 76418d6 7e2c73b 994feb6 7e2c73b 39219c6 7e2c73b 39219c6 5e95a20 7e2c73b 39219c6 f9a0bdb 39219c6 994feb6 39219c6 7e2c73b 39219c6 b6ee928 39219c6 4372b0a b6ee928 a4f7e5c b6ee928 39219c6 76418d6 a4f7e5c fe00e4d 39219c6 76418d6 f9a0bdb 39219c6 76418d6 fe00e4d 994feb6 fbb4b8d 39219c6 fe00e4d 39219c6 994feb6 76418d6 f9a0bdb 76418d6 f9a0bdb 39219c6 f9a0bdb 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 39219c6 76418d6 f9a0bdb 76418d6 f9a0bdb 76418d6 f9a0bdb 994feb6 39219c6 5e95a20 fe00e4d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
#!/usr/bin/env python3
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# MedGenesis AI β Streamlit UI (OpenAI + Gemini, CPU-only)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
import os, pathlib, asyncio, re
from pathlib import Path
from datetime import datetime
import streamlit as st
import pandas as pd
import plotly.express as px
from fpdf import FPDF
from streamlit_agraph import agraph
# ββ internal helpers --------------------------------------------------
from mcp.orchestrator import orchestrate_search, answer_ai_question
from mcp.workspace import get_workspace, save_query
from mcp.knowledge_graph import build_agraph
from mcp.graph_metrics import build_nx, get_top_hubs, get_density
from mcp.alerts import check_alerts
# ββ Streamlit telemetry dir fix (HF Spaces sandbox quirks) ------------
os.environ["STREAMLIT_DATA_DIR"] = "/tmp/.streamlit"
os.environ["XDG_STATE_HOME"] = "/tmp"
os.environ["STREAMLIT_BROWSER_GATHERUSAGESTATS"] = "false"
pathlib.Path("/tmp/.streamlit").mkdir(parents=True, exist_ok=True)
ROOT = Path(__file__).parent
LOGO = ROOT / "assets" / "logo.png"
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Small util helpers
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _latin1_safe(txt: str) -> str:
"""Replace non-Latin-1 chars β keeps FPDF happy."""
return txt.encode("latin-1", "replace").decode("latin-1")
def _pdf(papers: list[dict]) -> bytes:
pdf = FPDF()
pdf.set_auto_page_break(auto=True, margin=15)
pdf.add_page()
pdf.set_font("Helvetica", size=11)
pdf.cell(200, 8, _latin1_safe("MedGenesis AI β Literature results"),
ln=True, align="C")
pdf.ln(3)
for i, p in enumerate(papers, 1):
pdf.set_font("Helvetica", "B", 11)
pdf.multi_cell(0, 7, _latin1_safe(f"{i}. {p['title']}"))
pdf.set_font("Helvetica", "", 9)
body = (
f"{p['authors']}\n"
f"{p['summary']}\n"
f"{p['link']}\n"
)
pdf.multi_cell(0, 6, _latin1_safe(body))
pdf.ln(1)
# FPDF already returns latin-1 bytes β no extra encode needed
return pdf.output(dest="S").encode("latin-1", "replace")
def _workspace_sidebar() -> None:
with st.sidebar:
st.header("π Workspace")
ws = get_workspace()
if not ws:
st.info("Run a search then press **Save** to populate this list.")
return
for i, item in enumerate(ws, 1):
with st.expander(f"{i}. {item['query']}"):
st.write(item["result"]["ai_summary"])
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Main Streamlit UI
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def render_ui() -> None:
st.set_page_config("MedGenesis AI", layout="wide")
# ββ Session-state defaults ββββββββββββββββββββββββββββββββββββββββ
for k, v in {
"query_result": None,
"followup_input": "",
"followup_response": None,
"last_query": "",
"last_llm": "",
}.items():
st.session_state.setdefault(k, v)
_workspace_sidebar()
col_logo, col_title = st.columns([0.15, 0.85])
with col_logo:
if LOGO.exists():
st.image(LOGO, width=110)
with col_title:
st.markdown("## 𧬠**MedGenesis AI**")
st.caption("Multi-source biomedical assistant Β· OpenAI / Gemini")
llm = st.radio("LLM engine", ["openai", "gemini"], horizontal=True)
query = st.text_input("Enter biomedical question",
placeholder="e.g. CRISPR glioblastoma therapy")
# ββ alert notifications (async) βββββββββββββββββββββββββββββββββββ
saved_qs = [w["query"] for w in get_workspace()]
if saved_qs:
try:
news = asyncio.run(check_alerts(saved_qs))
if news:
with st.sidebar:
st.subheader("π New papers")
for q, lnks in news.items():
st.write(f"**{q}** β {len(lnks)} new")
except Exception:
pass # network hiccups β silent
# ββ Run Search ----------------------------------------------------
if st.button("Run Search π") and query.strip():
with st.spinner("Collecting literature & biomedical data β¦"):
res = asyncio.run(orchestrate_search(query, llm=llm))
# store in session
st.session_state.update(
query_result=res,
last_query=query,
last_llm=llm,
followup_input="",
followup_response=None,
)
st.success(f"Completed with **{res['llm_used'].title()}**")
res = st.session_state.query_result
if not res:
st.info("Enter a biomedical question and press **Run Search π**")
return
# ββ Tabs ----------------------------------------------------------
tabs = st.tabs(["Results", "Genes", "Trials",
"Graph", "Metrics", "Visuals"])
# 1) Results -------------------------------------------------------
with tabs[0]:
for i, p in enumerate(res["papers"], 1):
st.markdown(
f"**{i}. [{p['title']}]({p['link']})** "
f"*{p['authors']}*"
)
st.write(p["summary"])
c_csv, c_pdf = st.columns(2)
with c_csv:
st.download_button(
"CSV",
pd.DataFrame(res["papers"]).to_csv(index=False),
"papers.csv",
"text/csv",
)
with c_pdf:
st.download_button("PDF", _pdf(res["papers"]),
"papers.pdf", "application/pdf")
if st.button("πΎ Save"):
save_query(st.session_state.last_query, res)
st.success("Saved to workspace")
st.subheader("UMLS concepts")
for c in (res["umls"] or []):
if isinstance(c, dict) and c.get("cui"):
st.write(f"- **{c['name']}** ({c['cui']})")
st.subheader("OpenFDA safety signals")
for d in (res["drug_safety"] or []):
st.json(d)
st.subheader("AI summary")
st.info(res["ai_summary"])
# 2) Genes ---------------------------------------------------------
with tabs[1]:
st.header("Gene / Variant signals")
genes_list = [
g for g in res["genes"]
if isinstance(g, dict) and (g.get("symbol") or g.get("name"))
]
if not genes_list:
st.info("No gene hits (rate-limited or none found).")
for g in genes_list:
st.write(f"- **{g.get('symbol') or g.get('name')}** "
f"{g.get('description','')}")
if res["gene_disease"]:
st.markdown("### DisGeNET associations")
ok = [d for d in res["gene_disease"] if isinstance(d, dict)]
if ok:
st.json(ok[:15])
defs = [d for d in res["mesh_defs"] if isinstance(d, str) and d]
if defs:
st.markdown("### MeSH definitions")
for d in defs:
st.write("-", d)
# 3) Trials --------------------------------------------------------
with tabs[2]:
st.header("Clinical trials")
ct = res["clinical_trials"]
if not ct:
st.info("No trials (rate-limited or none found).")
for t in ct:
nct = t.get("NCTId", [""])[0]
bttl = t.get("BriefTitle", [""])[0]
phase= t.get("Phase", [""])[0]
stat = t.get("OverallStatus", [""])[0]
st.markdown(f"**{nct}** β {bttl}")
st.write(f"Phase {phase} | Status {stat}")
# 4) Graph ---------------------------------------------------------
with tabs[3]:
nodes, edges, cfg = build_agraph(
res["papers"], res["umls"], res["drug_safety"]
)
hl = st.text_input("Highlight node:", key="hl")
if hl:
pat = re.compile(re.escape(hl), re.I)
for n in nodes:
n.color = "#f1c40f" if pat.search(n.label) else "#d3d3d3"
agraph(nodes, edges, cfg)
# 5) Metrics -------------------------------------------------------
with tabs[4]:
G = build_nx(
[n.__dict__ for n in nodes],
[e.__dict__ for e in edges],
)
st.metric("Density", f"{get_density(G):.3f}")
st.markdown("**Top hubs**")
for nid, sc in get_top_hubs(G, k=5):
label = next((n.label for n in nodes if n.id == nid), nid)
st.write(f"- {label} {sc:.3f}")
# 6) Visuals -------------------------------------------------------
with tabs[5]:
years = [
p["published"][:4] for p in res["papers"]
if p.get("published") and len(p["published"]) >= 4
]
if years:
st.plotly_chart(
px.histogram(
years, nbins=min(15, len(set(years))),
title="Publication Year"
)
)
# ββ Follow-up Q-A -------------------------------------------------
st.markdown("---")
st.text_input("Ask follow-up question:",
key="followup_input",
placeholder="e.g. Any Phase III trials recruiting now?")
def _on_ask():
q = st.session_state.followup_input.strip()
if not q:
st.warning("Please type a question first.")
return
with st.spinner("Querying LLM β¦"):
ans = asyncio.run(
answer_ai_question(
q,
context=st.session_state.last_query,
llm=st.session_state.last_llm)
)
st.session_state.followup_response = (
ans.get("answer") or "LLM unavailable or quota exceeded."
)
st.button("Ask AI", on_click=_on_ask)
if st.session_state.followup_response:
st.write(st.session_state.followup_response)
# ββ entry-point βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
render_ui()
|