Update app.py
Browse files
app.py
CHANGED
|
@@ -1,148 +1,164 @@
|
|
| 1 |
-
# app.py
|
| 2 |
|
| 3 |
-
import asyncio, os, re
|
| 4 |
from pathlib import Path
|
| 5 |
-
|
| 6 |
import streamlit as st
|
| 7 |
import pandas as pd
|
| 8 |
import plotly.express as px
|
| 9 |
from fpdf import FPDF
|
| 10 |
from streamlit_agraph import agraph
|
| 11 |
|
| 12 |
-
from mcp.orchestrator
|
| 13 |
-
from mcp.workspace
|
| 14 |
from mcp.knowledge_graph import build_agraph
|
|
|
|
|
|
|
| 15 |
|
| 16 |
ROOT = Path(__file__).parent
|
| 17 |
LOGO = ROOT / "assets" / "logo.png"
|
| 18 |
|
| 19 |
-
# ----------
|
| 20 |
-
def
|
| 21 |
pdf = FPDF(); pdf.add_page(); pdf.set_font("Arial", size=12)
|
| 22 |
-
pdf.cell(200, 10, "MedGenesis AI
|
| 23 |
-
pdf.ln(10)
|
| 24 |
for i, p in enumerate(papers, 1):
|
| 25 |
-
pdf.set_font("Arial", "B", 12)
|
| 26 |
-
pdf.
|
| 27 |
-
pdf.
|
| 28 |
-
pdf.multi_cell(0, 8,
|
| 29 |
-
f"Authors: {p['authors']}\nLink: {p['link']}\nSummary: {p['summary']}\n")
|
| 30 |
pdf.ln(2)
|
| 31 |
return pdf.output(dest="S").encode("latin-1")
|
| 32 |
|
| 33 |
-
# ----------
|
| 34 |
def render_ui():
|
| 35 |
st.set_page_config(page_title="MedGenesis AI", layout="wide")
|
| 36 |
|
| 37 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
with st.sidebar:
|
| 39 |
st.header("ποΈ Workspace")
|
| 40 |
-
for i,
|
| 41 |
-
with st.expander(f"{i}. {
|
| 42 |
-
st.write("
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
| 45 |
if not get_workspace():
|
| 46 |
-
st.info("
|
| 47 |
|
| 48 |
-
#
|
| 49 |
col1, col2 = st.columns([0.15, 0.85])
|
| 50 |
with col1:
|
| 51 |
if LOGO.exists(): st.image(str(LOGO), width=100)
|
| 52 |
with col2:
|
| 53 |
-
st.markdown("## 𧬠MedGenesis AI")
|
| 54 |
-
st.
|
| 55 |
|
| 56 |
st.markdown("---")
|
| 57 |
-
query = st.text_input("π
|
|
|
|
| 58 |
|
| 59 |
-
# ------------- Search -------------
|
| 60 |
if st.button("Run Search π") and query:
|
| 61 |
-
with st.spinner("
|
| 62 |
-
|
| 63 |
-
st.success("
|
| 64 |
|
| 65 |
-
# -------- Tabs ---------
|
| 66 |
tabs = st.tabs([
|
| 67 |
-
"
|
| 68 |
-
"πΊοΈ Graph", "π Visuals"
|
| 69 |
])
|
| 70 |
|
| 71 |
-
# -- Results ---
|
| 72 |
with tabs[0]:
|
| 73 |
st.header("π Top Papers")
|
| 74 |
-
for i, p in enumerate(
|
| 75 |
-
st.markdown(f"**{i}. [{p['title']}]({p['link']})**
|
| 76 |
st.markdown(f"<span style='color:gray'>{p['summary']}</span>", unsafe_allow_html=True)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
st.
|
| 84 |
-
|
| 85 |
-
st.subheader("π§ UMLS Concepts")
|
| 86 |
-
for c in results["umls"]:
|
| 87 |
if c.get("cui"):
|
| 88 |
-
st.
|
| 89 |
|
| 90 |
st.subheader("π Drug Safety (OpenFDA)")
|
| 91 |
-
for d in
|
| 92 |
-
st.json(d)
|
| 93 |
|
| 94 |
-
st.subheader("π€ AI
|
| 95 |
-
st.info(
|
| 96 |
|
| 97 |
-
# -- Genes
|
| 98 |
with tabs[1]:
|
| 99 |
-
st.header("𧬠Gene
|
| 100 |
-
for g in
|
| 101 |
st.write(f"- **{g.get('name', g.get('geneid'))}** β {g.get('description','')}")
|
| 102 |
-
if
|
| 103 |
-
st.
|
| 104 |
-
st.json(
|
| 105 |
-
if
|
| 106 |
-
st.
|
| 107 |
-
for d in
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
# -- Clinical Trials ----------
|
| 111 |
with tabs[2]:
|
| 112 |
st.header("π Registered Clinical Trials")
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
st.markdown(f"**{t['NCTId'][0]}** β {t['BriefTitle'][0]}")
|
| 115 |
-
st.write(f"
|
| 116 |
|
| 117 |
-
# -- Graph ---
|
| 118 |
with tabs[3]:
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
# -- Visualizations ----------
|
| 132 |
with tabs[4]:
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
if yrs: st.plotly_chart(px.histogram(yrs, nbins=10, title="Publication Year"))
|
| 135 |
|
| 136 |
-
# -- Follow-up Q&A ---
|
| 137 |
st.markdown("---")
|
| 138 |
-
q = st.text_input("Ask follow-up:"
|
| 139 |
if st.button("Ask AI"):
|
| 140 |
-
|
| 141 |
-
st.write(ans["answer"])
|
| 142 |
-
|
| 143 |
else:
|
| 144 |
st.info("Enter a question and press **Run Search π**")
|
| 145 |
|
| 146 |
-
#
|
| 147 |
if __name__ == "__main__":
|
| 148 |
render_ui()
|
|
|
|
| 1 |
+
# app.py β’ MedGenesis AI β CPU-only powerhouse
|
| 2 |
|
| 3 |
+
import asyncio, os, re, httpx
|
| 4 |
from pathlib import Path
|
|
|
|
| 5 |
import streamlit as st
|
| 6 |
import pandas as pd
|
| 7 |
import plotly.express as px
|
| 8 |
from fpdf import FPDF
|
| 9 |
from streamlit_agraph import agraph
|
| 10 |
|
| 11 |
+
from mcp.orchestrator import orchestrate_search, answer_ai_question
|
| 12 |
+
from mcp.workspace import get_workspace, save_query
|
| 13 |
from mcp.knowledge_graph import build_agraph
|
| 14 |
+
from mcp.graph_metrics import build_nx, get_top_hubs, get_density
|
| 15 |
+
from mcp.alerts import check_alerts
|
| 16 |
|
| 17 |
ROOT = Path(__file__).parent
|
| 18 |
LOGO = ROOT / "assets" / "logo.png"
|
| 19 |
|
| 20 |
+
# ---------- utilities ----------
|
| 21 |
+
def gen_pdf(papers):
|
| 22 |
pdf = FPDF(); pdf.add_page(); pdf.set_font("Arial", size=12)
|
| 23 |
+
pdf.cell(200, 10, "MedGenesis AI β Results", ln=True, align="C"); pdf.ln(10)
|
|
|
|
| 24 |
for i, p in enumerate(papers, 1):
|
| 25 |
+
pdf.set_font("Arial", "B", 12); pdf.multi_cell(0, 10, f"{i}. {p['title']}")
|
| 26 |
+
pdf.set_font("Arial", "", 9)
|
| 27 |
+
pdf.multi_cell(0, 7, f"Authors: {p['authors']}\n{p['summary']}\n{p['link']}\n")
|
|
|
|
|
|
|
| 28 |
pdf.ln(2)
|
| 29 |
return pdf.output(dest="S").encode("latin-1")
|
| 30 |
|
| 31 |
+
# ---------- UI ----------
|
| 32 |
def render_ui():
|
| 33 |
st.set_page_config(page_title="MedGenesis AI", layout="wide")
|
| 34 |
|
| 35 |
+
# π Alert check (non-blocking)
|
| 36 |
+
saved_qs = [w["query"] for w in get_workspace()]
|
| 37 |
+
if saved_qs:
|
| 38 |
+
try:
|
| 39 |
+
news = asyncio.run(check_alerts(saved_qs))
|
| 40 |
+
if news:
|
| 41 |
+
with st.sidebar:
|
| 42 |
+
st.subheader("π New Papers")
|
| 43 |
+
for q, links in news.items():
|
| 44 |
+
st.write(f"**{q}** β {len(links)} new")
|
| 45 |
+
except Exception as e:
|
| 46 |
+
st.sidebar.error(f"Alert check error: {e}")
|
| 47 |
+
|
| 48 |
+
# Workspace sidebar
|
| 49 |
with st.sidebar:
|
| 50 |
st.header("ποΈ Workspace")
|
| 51 |
+
for i, itm in enumerate(get_workspace(), 1):
|
| 52 |
+
with st.expander(f"{i}. {itm['query']}"):
|
| 53 |
+
st.write("AI summary:", itm["result"]["ai_summary"])
|
| 54 |
+
st.download_button(
|
| 55 |
+
"CSV", pd.DataFrame(itm["result"]["papers"]).to_csv(index=False),
|
| 56 |
+
f"ws_{i}.csv", "text/csv"
|
| 57 |
+
)
|
| 58 |
if not get_workspace():
|
| 59 |
+
st.info("No saved queries.")
|
| 60 |
|
| 61 |
+
# Header
|
| 62 |
col1, col2 = st.columns([0.15, 0.85])
|
| 63 |
with col1:
|
| 64 |
if LOGO.exists(): st.image(str(LOGO), width=100)
|
| 65 |
with col2:
|
| 66 |
+
st.markdown("## 𧬠**MedGenesis AI**")
|
| 67 |
+
st.caption("PubMed β’ ArXiv β’ OpenFDA β’ UMLS β’ NCBI β’ DisGeNET β’ ClinicalTrials β’ GPT-4o")
|
| 68 |
|
| 69 |
st.markdown("---")
|
| 70 |
+
query = st.text_input("π Ask a biomedical research question:",
|
| 71 |
+
placeholder="e.g. CRISPR glioblastoma treatment")
|
| 72 |
|
|
|
|
| 73 |
if st.button("Run Search π") and query:
|
| 74 |
+
with st.spinner("Crunching literature & biomedical databasesβ¦"):
|
| 75 |
+
res = asyncio.run(orchestrate_search(query))
|
| 76 |
+
st.success("Done!")
|
| 77 |
|
|
|
|
| 78 |
tabs = st.tabs([
|
| 79 |
+
"Results", "Genes", "Trials", "Graph", "Metrics", "Visuals"
|
|
|
|
| 80 |
])
|
| 81 |
|
| 82 |
+
# --- Results ---
|
| 83 |
with tabs[0]:
|
| 84 |
st.header("π Top Papers")
|
| 85 |
+
for i, p in enumerate(res["papers"], 1):
|
| 86 |
+
st.markdown(f"**{i}. [{p['title']}]({p['link']})** β *{p['authors']}*")
|
| 87 |
st.markdown(f"<span style='color:gray'>{p['summary']}</span>", unsafe_allow_html=True)
|
| 88 |
+
if st.button("Save Query"):
|
| 89 |
+
save_query(query, res); st.success("Saved to workspace")
|
| 90 |
+
csv = pd.DataFrame(res["papers"]).to_csv(index=False)
|
| 91 |
+
st.download_button("CSV", csv, "papers.csv", "text/csv")
|
| 92 |
+
st.download_button("PDF", gen_pdf(res["papers"]), "papers.pdf", "application/pdf")
|
| 93 |
+
|
| 94 |
+
st.subheader("π§ Key UMLS Concepts")
|
| 95 |
+
for c in res["umls"]:
|
|
|
|
|
|
|
| 96 |
if c.get("cui"):
|
| 97 |
+
st.write(f"- **{c['name']}** ({c['cui']})")
|
| 98 |
|
| 99 |
st.subheader("π Drug Safety (OpenFDA)")
|
| 100 |
+
for d in res["drug_safety"]: st.json(d)
|
|
|
|
| 101 |
|
| 102 |
+
st.subheader("π€ AI Synthesis")
|
| 103 |
+
st.info(res["ai_summary"])
|
| 104 |
|
| 105 |
+
# --- Genes / Variants ---
|
| 106 |
with tabs[1]:
|
| 107 |
+
st.header("𧬠Gene & Variant Signals")
|
| 108 |
+
for g in res["genes"]:
|
| 109 |
st.write(f"- **{g.get('name', g.get('geneid'))}** β {g.get('description','')}")
|
| 110 |
+
if res["gene_disease"]:
|
| 111 |
+
st.write("### DisGeNET Links")
|
| 112 |
+
st.json(res["gene_disease"][:15])
|
| 113 |
+
if res["mesh_defs"]:
|
| 114 |
+
st.write("### MeSH Definitions")
|
| 115 |
+
for d in res["mesh_defs"]: st.write("-", d)
|
| 116 |
+
|
| 117 |
+
# --- Clinical Trials ---
|
|
|
|
| 118 |
with tabs[2]:
|
| 119 |
st.header("π Registered Clinical Trials")
|
| 120 |
+
if not res["clinical_trials"]:
|
| 121 |
+
st.info("No trials (API rate-limited or none found).")
|
| 122 |
+
for t in res["clinical_trials"]:
|
| 123 |
st.markdown(f"**{t['NCTId'][0]}** β {t['BriefTitle'][0]}")
|
| 124 |
+
st.write(f"Phase: {t.get('Phase', [''])[0]} | Status: {t['OverallStatus'][0]}")
|
| 125 |
|
| 126 |
+
# --- Knowledge Graph ---
|
| 127 |
with tabs[3]:
|
| 128 |
+
st.header("πΊοΈ Knowledge Graph")
|
| 129 |
+
nodes, edges, cfg = build_agraph(res["papers"], res["umls"], res["drug_safety"])
|
| 130 |
+
highlight = st.text_input("Highlight nodes:", key="hl")
|
| 131 |
+
if highlight:
|
| 132 |
+
pat = re.compile(re.escape(highlight), re.I)
|
| 133 |
+
for n in nodes:
|
| 134 |
+
if pat.search(n.label): n.color, n.size = "#f1c40f", 30
|
| 135 |
+
else: n.color = "#d3d3d3"
|
| 136 |
+
agraph(nodes=nodes, edges=edges, config=cfg)
|
| 137 |
+
|
| 138 |
+
# --- Metrics ---
|
|
|
|
|
|
|
| 139 |
with tabs[4]:
|
| 140 |
+
st.header("π Graph Metrics")
|
| 141 |
+
import networkx as nx
|
| 142 |
+
G = build_nx([n.__dict__ for n in nodes], [e.__dict__ for e in edges])
|
| 143 |
+
st.metric("Density", f"{get_density(G):.3f}")
|
| 144 |
+
st.markdown("#### Hub Nodes")
|
| 145 |
+
for nid, sc in get_top_hubs(G):
|
| 146 |
+
lab = next((n.label for n in nodes if n.id == nid), nid)
|
| 147 |
+
st.write(f"- **{lab}** β {sc:.3f}")
|
| 148 |
+
|
| 149 |
+
# --- Visuals ---
|
| 150 |
+
with tabs[5]:
|
| 151 |
+
yrs = [p["published"] for p in res["papers"] if p.get("published")]
|
| 152 |
if yrs: st.plotly_chart(px.histogram(yrs, nbins=10, title="Publication Year"))
|
| 153 |
|
| 154 |
+
# --- Follow-up Q&A ---
|
| 155 |
st.markdown("---")
|
| 156 |
+
q = st.text_input("Ask follow-up question:")
|
| 157 |
if st.button("Ask AI"):
|
| 158 |
+
st.write(asyncio.run(answer_ai_question(q, context=query))["answer"])
|
|
|
|
|
|
|
| 159 |
else:
|
| 160 |
st.info("Enter a question and press **Run Search π**")
|
| 161 |
|
| 162 |
+
# Run
|
| 163 |
if __name__ == "__main__":
|
| 164 |
render_ui()
|