KevinIsInCoding Claude Sonnet 4.6 commited on
Commit
d515e2b
·
1 Parent(s): 3c9dd57

feat: Stage 5 — Gradio UI + HuggingFace Spaces deployment

Browse files

- app.py: Gradio 6.20 Blocks UI with streaming chatbot, 6 example
question buttons (two columns), status bar showing chunk/trial/KG
counts, medical disclaimer footer
- Streaming pattern: status updates shown as italic while waiting,
tokens accumulate in real-time, input locked during generation
- Module-level resource loading: BioLORD collection + KG graph +
trials loaded once at startup (not per request)
- requirements.txt: HF Spaces mirror of pyproject.toml deps
- README.md: HF Spaces metadata header + full setup guide including
offline pipeline run order, architecture diagram, data sources table

Verified: app starts in <5s, serves on localhost:7860, all 510 chunks
/ 112 trials / 220 KG nodes loaded at startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +105 -0
  2. app.py +174 -2
  3. requirements.txt +10 -0
README.md ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Candle Fire
3
+ emoji: 🕯️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: "6.14.0"
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # 🕯️ Candle-Fire
13
+
14
+ **ALS Research Intelligence for Physicians**
15
+
16
+ Candle-fire is a physician-facing tool that synthesizes evidence from ~500 curated ALS research papers and a biomedical knowledge graph. Ask a free-text question about ALS biology, drug targets, or clinical trials — get a structured, cited answer in under 30 seconds.
17
+
18
+ ## What It Does
19
+
20
+ - **Two-layer retrieval**: Knowledge graph expansion (BioLORD-2023-C embeddings + NetworkX) → RAG over ~500 ALS papers
21
+ - **Citation-weighted ranking**: Highly-cited papers surface first
22
+ - **Structured synthesis**: Claude Sonnet produces mechanism summaries, entity tables, evidence strength assessments, and trial links
23
+ - **Biomedical synonyms**: BioLORD understands that "TDP-43" = "TARDBP" = "TAR DNA-binding protein 43"
24
+
25
+ ## Setup
26
+
27
+ ### Prerequisites
28
+
29
+ ```bash
30
+ # Python 3.11+, uv package manager
31
+ pip install uv
32
+ uv sync
33
+ ```
34
+
35
+ ### Environment variables
36
+
37
+ ```bash
38
+ cp .env.example .env
39
+ # Fill in:
40
+ # ANTHROPIC_API_KEY — required
41
+ # ENTREZ_EMAIL — required for PubMed ingestion
42
+ # NCBI_API_KEY — optional, raises rate limit 3→10 req/s
43
+ ```
44
+
45
+ ### Run the offline pipeline (once)
46
+
47
+ Build the knowledge assets before launching the app. Each step is resumable.
48
+
49
+ ```bash
50
+ # 1. Ingest ~500 ALS papers from PubMed + PMC full text + citation counts (~15 min)
51
+ uv run python scripts/ingest_papers.py
52
+
53
+ # 2. Ingest ALS clinical trials from ClinicalTrials.gov (< 1 min, run in parallel)
54
+ uv run python scripts/ingest_trials.py
55
+
56
+ # 3. Extract biomedical entities using Claude Sonnet (~$1.50, ~50 min, resumable)
57
+ uv run python scripts/extract_entities.py
58
+
59
+ # 4. Build the knowledge graph (~5 sec)
60
+ uv run python scripts/build_graph.py
61
+
62
+ # 5. Build the ChromaDB vector index with BioLORD embeddings (~10 min, one-time model download)
63
+ uv run python scripts/build_index.py
64
+ ```
65
+
66
+ ### Launch
67
+
68
+ ```bash
69
+ # Web UI
70
+ uv run python app.py
71
+
72
+ # CLI
73
+ uv run python main.py "What is the evidence for tofersen targeting SOD1?"
74
+ ```
75
+
76
+ ## Architecture
77
+
78
+ ```
79
+ Physician query
80
+ → agents/research_agent.py
81
+ 1. Claude: extract query entities → ["SOD1", "tofersen"]
82
+ 2. graph/query.py: KG expansion → ["SOD1", "TARDBP", "antisense oligonucleotide", ...]
83
+ 3. rag/retriever.py: BioLORD semantic search + entity search → top 15 papers
84
+ (re-ranked by: similarity × log(citation_count + 2))
85
+ 4. graph/query.py: find linked clinical trials
86
+ 5. Claude Sonnet (streaming): synthesize research landscape
87
+ → Gradio UI (streaming response with citations)
88
+ ```
89
+
90
+ **Embedding model**: `FremyCompany/BioLORD-2023-C` — anchored to UMLS/SNOMED CT/MeSH ontologies, natively resolves biomedical synonyms.
91
+
92
+ **Knowledge graph**: NetworkX DiGraph with Gene/Protein/Compound/Pathway/Phenotype/Mechanism/ClinicalTrial nodes. 1-hop BFS expansion before RAG retrieval.
93
+
94
+ ## Data Sources
95
+
96
+ | Source | Content | Volume |
97
+ |---|---|---|
98
+ | PubMed Entrez | ALS paper abstracts + metadata | ~500 papers (2018–2024) |
99
+ | PubMed Central | Full text for Open Access papers | ~50% coverage |
100
+ | Semantic Scholar | Citation counts per paper | All papers |
101
+ | ClinicalTrials.gov v2 | Active ALS recruiting trials | ~112 trials |
102
+
103
+ ## Disclaimer
104
+
105
+ Research synthesis tool. Always verify claims with primary sources before applying to patient care. Not a substitute for clinical judgment.
app.py CHANGED
@@ -1,2 +1,174 @@
1
- """Gradio web UI for candle-fire."""
2
- # Stage 5 implementation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio web UI for candle-fire — physician-facing ALS research intelligence."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import anthropic
8
+ import gradio as gr
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ from agents.research_agent import stream_research_agent
14
+ from config import CHROMA_COLLECTION, CHROMA_DIR, GRAPH_PICKLE_PATH, TRIALS_PATH
15
+ from logging_config import get_logger
16
+ from rag.indexer import load_collection
17
+
18
+ _logger = get_logger("app")
19
+
20
+ # ── Load resources once at startup ───────────────────────────────────────────
21
+
22
+ def _load_graph():
23
+ try:
24
+ from graph.serializer import load_graph
25
+ G = load_graph(GRAPH_PICKLE_PATH)
26
+ _logger.info(f"KG loaded: {G.number_of_nodes()} nodes")
27
+ return G
28
+ except FileNotFoundError:
29
+ _logger.warning("KG not found — running RAG-only mode")
30
+ return None
31
+
32
+
33
+ def _load_trials() -> list[dict]:
34
+ if not TRIALS_PATH.exists():
35
+ return []
36
+ with open(TRIALS_PATH, encoding="utf-8") as f:
37
+ return [json.loads(line) for line in f if line.strip()]
38
+
39
+
40
+ _collection = load_collection(CHROMA_DIR, CHROMA_COLLECTION)
41
+ _graph = _load_graph()
42
+ _trials = _load_trials()
43
+ _client = anthropic.Anthropic()
44
+
45
+ _n_chunks = _collection.count()
46
+ _n_trials = len(_trials)
47
+ _kg_nodes = _graph.number_of_nodes() if _graph else 0
48
+
49
+ # ── Example questions ─────────────────────────────────────────────────────────
50
+
51
+ _EXAMPLES = [
52
+ "What is the evidence for tofersen targeting SOD1 in ALS?",
53
+ "What mechanisms link TDP-43 aggregation to motor neuron death?",
54
+ "What compounds target glutamate excitotoxicity in ALS?",
55
+ "What is the role of C9orf72 repeat expansion in neurodegeneration?",
56
+ "How does riluzole work and what is the clinical evidence?",
57
+ "What biomarkers track ALS disease progression?",
58
+ ]
59
+
60
+ # ── Streaming respond function ────────────────────────────────────────────────
61
+
62
+ def respond(message: str, history: list[dict]):
63
+ if not message.strip():
64
+ yield history, gr.update(value="", interactive=True)
65
+ return
66
+
67
+ history = history + [{"role": "user", "content": message}]
68
+ history = history + [{"role": "assistant", "content": ""}]
69
+ yield history, gr.update(value="", interactive=False)
70
+
71
+ response_text = ""
72
+
73
+ for event_type, content in stream_research_agent(
74
+ _client, message, _collection, _trials, graph=_graph
75
+ ):
76
+ if event_type == "status":
77
+ if not response_text:
78
+ history[-1]["content"] = f"*{content}*"
79
+ yield history, gr.update()
80
+ elif event_type == "token":
81
+ response_text += content
82
+ history[-1]["content"] = response_text
83
+ yield history, gr.update()
84
+ elif event_type == "done":
85
+ history[-1]["content"] = response_text or content
86
+ yield history, gr.update(interactive=True)
87
+ return
88
+
89
+ yield history, gr.update(interactive=True)
90
+
91
+
92
+ # ── UI ────────────────────────────────────────────────────────────────────────
93
+
94
+ _CSS = """
95
+ .container { max-width: 900px; margin: 0 auto; }
96
+ .disclaimer { font-size: 0.78rem; color: #888; text-align: center; margin-top: 6px; }
97
+ .status-bar { font-size: 0.82rem; color: #666; text-align: center; margin-bottom: 8px; }
98
+ footer { display: none !important; }
99
+ """
100
+
101
+ _TITLE_MD = """# 🕯️ Candle-Fire
102
+ ### ALS Research Intelligence for Physicians
103
+ Ask a free-text question about ALS biology, drug targets, or clinical trials.
104
+ Answers are synthesized from ~500 curated ALS papers and enriched by a biomedical knowledge graph.
105
+ """
106
+
107
+ _DISCLAIMER_MD = """<div class="disclaimer">
108
+ ⚕️ Research synthesis tool — not a substitute for clinical judgment.
109
+ Always verify claims with primary sources before applying to patient care.
110
+ </div>"""
111
+
112
+
113
+ with gr.Blocks(title="Candle-Fire — ALS Research Intelligence") as demo:
114
+
115
+ with gr.Column(elem_classes="container"):
116
+
117
+ gr.Markdown(_TITLE_MD)
118
+
119
+ gr.HTML(
120
+ f'<div class="status-bar">'
121
+ f'{_n_chunks} paper chunks &nbsp;·&nbsp; '
122
+ f'{_n_trials} clinical trials &nbsp;·&nbsp; '
123
+ f'{_kg_nodes} knowledge graph nodes'
124
+ f'</div>'
125
+ )
126
+
127
+ chatbot = gr.Chatbot(
128
+ value=[],
129
+ height=520,
130
+ show_label=False,
131
+ sanitize_html=False,
132
+ avatar_images=(None, "https://api.dicebear.com/7.x/icons/svg?seed=candle&icon=flame"),
133
+ placeholder="Ask a question about ALS research to get started.",
134
+ )
135
+
136
+ with gr.Row():
137
+ msg_box = gr.Textbox(
138
+ placeholder="e.g. What is the evidence for tofersen targeting SOD1?",
139
+ show_label=False,
140
+ scale=9,
141
+ autofocus=True,
142
+ lines=1,
143
+ )
144
+ send_btn = gr.Button("Ask", scale=1, variant="primary", min_width=80)
145
+
146
+ gr.Markdown("**Example questions** — click to populate:")
147
+
148
+ with gr.Row():
149
+ with gr.Column(scale=1):
150
+ for ex in _EXAMPLES[:3]:
151
+ btn = gr.Button(ex, size="sm", variant="secondary")
152
+ btn.click(fn=lambda t=ex: t, outputs=[msg_box])
153
+ with gr.Column(scale=1):
154
+ for ex in _EXAMPLES[3:]:
155
+ btn = gr.Button(ex, size="sm", variant="secondary")
156
+ btn.click(fn=lambda t=ex: t, outputs=[msg_box])
157
+
158
+ gr.HTML(_DISCLAIMER_MD)
159
+
160
+ submit_kwargs = dict(
161
+ fn=respond,
162
+ inputs=[msg_box, chatbot],
163
+ outputs=[chatbot, msg_box],
164
+ )
165
+ msg_box.submit(**submit_kwargs)
166
+ send_btn.click(**submit_kwargs)
167
+
168
+
169
+ if __name__ == "__main__":
170
+ demo.launch(
171
+ share=False,
172
+ css=_CSS,
173
+ theme=gr.themes.Soft(),
174
+ )
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ anthropic>=0.50.0
2
+ gradio>=6.14.0,<7.0.0
3
+ chromadb>=0.5.0
4
+ networkx>=3.3
5
+ biopython>=1.84
6
+ httpx>=0.27.0
7
+ python-dotenv>=1.2.2
8
+ rich>=13.0.0
9
+ sentence-transformers>=3.0.0
10
+ openai>=2.44.0