PatsnapLifeScience commited on
Commit
bb011f3
·
0 Parent(s):

v2: Full product redesign

Browse files

- 6-module tab layout (Agent Chat, Target, Drug, Disease, Company, Trial)
- Natural language agent with intent parsing & tool routing
- Product-grade CSS (cards, badges, animations, custom theme)
- MCP live data integration with knowledge base fallback
- Structured report generation for each module
- Quick examples & featured reports
- README with HF Space metadata

Files changed (6) hide show
  1. .gitignore +4 -0
  2. README.md +47 -0
  3. app.py +1394 -0
  4. fetch.py +98 -0
  5. fetch_real_data.py +51 -0
  6. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ .venv/
3
+ *.pyc
4
+ .env
README.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Pharma Intelligence
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 5.0.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # 🧬 PatSnap Pharma Intelligence
14
+
15
+ **AI-Powered Drug Discovery Intelligence Agent** — explore targets, drugs, diseases, companies, and clinical trials.
16
+
17
+ ## Modules
18
+
19
+ | Module | Description |
20
+ |--------|-------------|
21
+ | 🤖 **Agent Chat** | Natural language → intent routing → structured report |
22
+ | 🎯 **Target Intelligence** | Deep analysis of drug targets (biology, drugs, pipeline) |
23
+ | 💊 **Drug Exploration** | Pipeline drugs by target, disease, mechanism, or company |
24
+ | 🏥 **Disease Investigation** | Disease landscape, epidemiology, treatments |
25
+ | 🏢 **Company Profiling** | Pharma pipeline & strategic analysis |
26
+ | 🧪 **Clinical Trials** | Trial landscape by indication, phase, sponsor |
27
+
28
+ ## How It Works
29
+
30
+ 1. **Ask in natural language** — The agent parses your intent
31
+ 2. **MCP-powered search** — Connects to PatSnap's pharmaceutical intelligence database
32
+ 3. **Structured reports** — Generates professional-grade reports with pipeline tables, statistics, and insights
33
+
34
+ ## Configuration
35
+
36
+ ### API Key (Optional)
37
+
38
+ Set the `PATSNAP_API_KEY` environment variable in your Space settings to enable live pharmaceutical data. Without it, the demo uses a built-in knowledge base.
39
+
40
+ ```bash
41
+ PATSNAP_API_KEY=your_key_here
42
+ ```
43
+
44
+ ## Powered By
45
+
46
+ - [PatSnap Life Sciences MCP](https://github.com/patsnap/skills)
47
+ - [Gradio](https://gradio.app)
app.py ADDED
@@ -0,0 +1,1394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PatSnap Pharma Intelligence — Hugging Face Space Demo
4
+ Product-grade multi-module AI agent for life science intelligence.
5
+
6
+ Modules:
7
+ - Agent Chat (natural language → tool orchestration → report)
8
+ - Target Intelligence (靶点全景)
9
+ - Drug Exploration (药物管线)
10
+ - Disease Investigation (疾病格局)
11
+ - Company Profiling (公司分析)
12
+ - Clinical Trials (临床试验)
13
+ """
14
+
15
+ import os, json, asyncio, time, re
16
+ from collections import Counter
17
+ from datetime import datetime
18
+ from typing import Optional, Dict, List, Tuple
19
+
20
+ import gradio as gr
21
+
22
+ # =============================================================================
23
+ # CONFIGURATION
24
+ # =============================================================================
25
+
26
+ API_KEY = os.getenv("PATSNAP_API_KEY", "")
27
+ HF_TOKEN = os.getenv("HF_TOKEN", "") # optional: enable LLM agent mode
28
+ SERVER_URL = f"https://connect.patsnap.com/096456/logic-mcp?apiKey={API_KEY}"
29
+
30
+ # Module definitions
31
+ MODULES = {
32
+ "target": {"icon": "🎯", "label": "Target Intelligence", "label_cn": "靶点全景",
33
+ "tool": "ls_target_search", "entity": "target",
34
+ "desc": "Analyze any biomedical target — biology, drugs, pipeline, trials."},
35
+ "drug": {"icon": "💊", "label": "Drug Exploration", "label_cn": "药物管线",
36
+ "tool": "ls_drug_search", "entity": "drug",
37
+ "desc": "Search drugs by target, disease, mechanism, or company."},
38
+ "disease": {"icon": "🏥", "label": "Disease Investigation", "label_cn": "疾病格局",
39
+ "tool": "ls_disease_search", "entity": "disease",
40
+ "desc": "Understand disease landscape — epidemiology, treatments, pipeline."},
41
+ "company": {"icon": "🏢", "label": "Company Profiling", "label_cn": "公司分析",
42
+ "tool": "ls_company_search", "entity": "company",
43
+ "desc": "Profile pharma companies — pipeline, deals, therapeutic focus."},
44
+ "trial": {"icon": "🧪", "label": "Clinical Trials", "label_cn": "临床试验",
45
+ "tool": "ls_clinical_trial_search", "entity": "trial",
46
+ "desc": "Explore clinical trials by target, disease, phase, or sponsor."},
47
+ }
48
+
49
+ # Language: 'en' or 'zh'
50
+ DEFAULT_LANG = "en"
51
+
52
+ # =============================================================================
53
+ # MOCK DATA — Comprehensive, module-specific
54
+ # =============================================================================
55
+
56
+ MOCK_TARGETS = {
57
+ "EGFR": {
58
+ "name": "EGFR", "full_name": "Epidermal Growth Factor Receptor",
59
+ "family": "ErbB/HER receptor tyrosine kinase",
60
+ "class": "Kinase", "indication_count": 12, "drug_count": 28,
61
+ "pdb_ids": ["1M17", "1XKK", "2J5F"],
62
+ "pathways": ["RAS/RAF/MEK/ERK", "PI3K/AKT/mTOR", "JAK/STAT"],
63
+ "mutation_hotspots": ["L858R (exon 21)", "exon 19 deletion", "T790M (exon 20)", "C797S (exon 20)"],
64
+ "approved_drugs": [
65
+ {"name": "Osimertinib", "type": "Small molecule", "gen": "3rd-gen TKI",
66
+ "year": 2015, "indications": ["NSCLC (T790M+)", "NSCLC (1L EGFRm)", "NSCLC (adjuvant)"],
67
+ "company": "AstraZeneca"},
68
+ {"name": "Gefitinib", "type": "Small molecule", "gen": "1st-gen TKI",
69
+ "year": 2003, "indications": ["NSCLC (EGFRm)"], "company": "AstraZeneca"},
70
+ {"name": "Erlotinib", "type": "Small molecule", "gen": "1st-gen TKI",
71
+ "year": 2004, "indications": ["NSCLC", "Pancreatic"], "company": "Roche/Genentech"},
72
+ {"name": "Afatinib", "type": "Small molecule", "gen": "2nd-gen TKI",
73
+ "year": 2013, "indications": ["NSCLC (EGFRm)"], "company": "Boehringer Ingelheim"},
74
+ {"name": "Dacomitinib", "type": "Small molecule", "gen": "2nd-gen TKI",
75
+ "year": 2018, "indications": ["NSCLC (EGFRm)"], "company": "Pfizer"},
76
+ {"name": "Cetuximab", "type": "Monoclonal antibody", "gen": "mAb",
77
+ "year": 2004, "indications": ["CRC", "HNSCC"], "company": "Merck KGaA / BMS"},
78
+ {"name": "Panitumumab", "type": "Monoclonal antibody", "gen": "mAb",
79
+ "year": 2006, "indications": ["CRC"], "company": "Amgen"},
80
+ {"name": "Amivantamab", "type": "Bispecific antibody", "gen": "BsAb",
81
+ "year": 2021, "indications": ["NSCLC (ex20ins)"], "company": "Janssen"},
82
+ {"name": "Patritumab deruxtecan", "type": "ADC", "gen": "ADC (HER3)",
83
+ "year": 2024, "indications": ["NSCLC (post-TKI)"], "company": "Daiichi Sankyo / Merck"},
84
+ ],
85
+ "pipeline_summary": {
86
+ "phase_3": 45, "phase_2": 120, "phase_1": 85, "preclinical": 200,
87
+ "hot_topics": ["4th-gen TKIs (C797S)", "Bispecific ADCs", "PROTAC degraders",
88
+ "Combination with immunotherapy", "Brain-penetrant TKIs"],
89
+ },
90
+ "competitive_landscape": "Highly competitive — every major pharma has an EGFR asset. "
91
+ "Innovation is focused on resistance mechanisms and next-gen modalities.",
92
+ },
93
+ "HER2": {
94
+ "name": "HER2", "full_name": "Human Epidermal Growth Factor Receptor 2",
95
+ "family": "ErbB/HER receptor tyrosine kinase",
96
+ "class": "Kinase", "indication_count": 5, "drug_count": 15,
97
+ "pdb_ids": ["1N8Z", "3PP0"],
98
+ "pathways": ["RAS/RAF/MEK/ERK", "PI3K/AKT/mTOR"],
99
+ "mutation_hotspots": ["Amplification (breast/gastric)", "Exon 20 mutations (NSCLC)"],
100
+ "approved_drugs": [
101
+ {"name": "Trastuzumab", "type": "Monoclonal antibody", "gen": "mAb",
102
+ "year": 1998, "indications": ["HER2+ Breast Cancer", "HER2+ Gastric Cancer"], "company": "Roche"},
103
+ {"name": "Trastuzumab deruxtecan", "type": "ADC", "gen": "ADC",
104
+ "year": 2019, "indications": ["HER2+ Breast Cancer", "HER2-low Breast Cancer", "HER2+ Gastric Cancer",
105
+ "HER2-mutant NSCLC"], "company": "Daiichi Sankyo / AstraZeneca"},
106
+ {"name": "Pertuzumab", "type": "Monoclonal antibody", "gen": "mAb",
107
+ "year": 2012, "indications": ["HER2+ Breast Cancer"], "company": "Roche"},
108
+ {"name": "Lapatinib", "type": "Small molecule", "gen": "TKI",
109
+ "year": 2007, "indications": ["HER2+ Breast Cancer"], "company": "Novartis"},
110
+ {"name": "Tucatinib", "type": "Small molecule", "gen": "TKI",
111
+ "year": 2020, "indications": ["HER2+ Breast Cancer (CNS mets)"], "company": "Seagen / Merck"},
112
+ ],
113
+ "pipeline_summary": {
114
+ "phase_3": 25, "phase_2": 80, "phase_1": 55, "preclinical": 130,
115
+ "hot_topics": ["HER2-low targeting", "Bispecific ADCs", "Brain metastasis"],
116
+ },
117
+ "competitive_landscape": "HER2 ADC space is the current battleground. Enhertu dominates; "
118
+ "competitors focus on differentiation via payload, DAR, or epitope.",
119
+ },
120
+ "PD-L1": {
121
+ "name": "PD-L1", "full_name": "Programmed Death-Ligand 1",
122
+ "family": "B7 immune checkpoint",
123
+ "class": "Immune checkpoint ligand", "indication_count": 20, "drug_count": 20,
124
+ "approved_drugs": [
125
+ {"name": "Atezolizumab", "type": "Monoclonal antibody", "gen": "Anti-PD-L1 mAb",
126
+ "year": 2016, "indications": ["NSCLC", "SCLC", "Urothelial", "HCC"], "company": "Roche"},
127
+ {"name": "Durvalumab", "type": "Monoclonal antibody", "gen": "Anti-PD-L1 mAb",
128
+ "year": 2017, "indications": ["NSCLC (Stage III)", "SCLC", "Biliary Tract"], "company": "AstraZeneca"},
129
+ {"name": "Avelumab", "type": "Monoclonal antibody", "gen": "Anti-PD-L1 mAb",
130
+ "year": 2017, "indications": ["Merkel Cell", "Urothelial", "RCC"], "company": "Merck KGaA / Pfizer"},
131
+ ],
132
+ "pipeline_summary": {"phase_3": 35, "phase_2": 60, "phase_1": 40, "preclinical": 100},
133
+ "competitive_landscape": "PD-L1 is a companion to PD-1 — the focus is on combination strategies "
134
+ "and predictive biomarker development.",
135
+ },
136
+ }
137
+
138
+ MOCK_COMPANIES = {
139
+ "Roche": {
140
+ "name": "Roche", "ticker": "ROG.SW",
141
+ "headquarters": "Basel, Switzerland",
142
+ "employees": "~100,000",
143
+ "2024_revenue": "$65.4B",
144
+ "therapeutic_areas": ["Oncology", "Neuroscience", "Ophthalmology", "Immunology", "Infectious Disease"],
145
+ "flagship_drugs": [
146
+ {"name": "Trastuzumab (Herceptin)", "target": "HER2", "sales": "$3.2B", "phase": "Approved"},
147
+ {"name": "Atezolizumab (Tecentriq)", "target": "PD-L1", "sales": "$4.8B", "phase": "Approved"},
148
+ {"name": "Bevacizumab (Avastin)", "target": "VEGF", "sales": "$2.1B", "phase": "Approved"},
149
+ {"name": "Trastuzumab deruxtecan (co-developed)", "target": "HER2", "sales": "$3.5B", "phase": "Approved"},
150
+ ],
151
+ "pipeline_count": {"approved": 28, "phase_3": 15, "phase_2": 35, "phase_1": 22},
152
+ "recent_deals": [
153
+ "Acquired Carmot Therapeutics (obesity) — $2.7B upfront (2023)",
154
+ "Acquired Telavant (IBD) — $7.1B (2023)",
155
+ ],
156
+ "strategy": "Roche combines strong internal R&D with strategic bolt-on acquisitions. "
157
+ "Oncology remains the core, with growing investment in immunology and metabolic disease.",
158
+ },
159
+ "AstraZeneca": {
160
+ "name": "AstraZeneca", "ticker": "AZN.L",
161
+ "headquarters": "Cambridge, UK",
162
+ "employees": "~90,000",
163
+ "2024_revenue": "$54.1B",
164
+ "therapeutic_areas": ["Oncology", "CVRM", "Respiratory & Immunology", "Rare Disease"],
165
+ "flagship_drugs": [
166
+ {"name": "Osimertinib (Tagrisso)", "target": "EGFR", "sales": "$6.8B", "phase": "Approved"},
167
+ {"name": "Durvalumab (Imfinzi)", "target": "PD-L1", "sales": "$4.5B", "phase": "Approved"},
168
+ {"name": "Trastuzumab deruxtecan (co-developed)", "target": "HER2", "sales": "$3.5B", "phase": "Approved"},
169
+ {"name": "Dapagliflozin (Farxiga)", "target": "SGLT2", "sales": "$7.1B", "phase": "Approved"},
170
+ ],
171
+ "pipeline_count": {"approved": 22, "phase_3": 12, "phase_2": 28, "phase_1": 18},
172
+ "recent_deals": [
173
+ "Acquired Gracell Biotechnologies (CAR-T) — $1.2B (2023)",
174
+ "Acquired Icosavax (RSV/hMPV vaccine) — $1.1B (2023)",
175
+ "Acquired Fusion Pharmaceuticals (radiopharma) — $2.4B (2024)",
176
+ ],
177
+ "strategy": "AZ's oncology portfolio is anchored by Tagrisso, Imfinzi, and Enhertu. "
178
+ "Actively expanding into cell therapy, radiopharmaceuticals, and ADCs.",
179
+ },
180
+ }
181
+
182
+ MOCK_DISEASES = {
183
+ "NSCLC": {
184
+ "name": "Non-Small Cell Lung Cancer",
185
+ "global_incidence": "~2.2M new cases/year (2024)",
186
+ "mortality": "~1.8M deaths/year",
187
+ "5yr_survival": "Stage I: 65%, Stage IV: 8%",
188
+ "major_mutations": ["EGFR (15-20%)", "KRAS (25-30%)", "ALK (3-5%)",
189
+ "ROS1 (1-2%)", "BRAF (1-3%)", "MET exon 14 (3%)", "RET (1-2%)"],
190
+ "approved_drugs_count": 45,
191
+ "drug_classes": ["TKIs (1st/2nd/3rd gen)", "Immune checkpoint inhibitors",
192
+ "ADCs", "Bispecific antibodies", "Chemotherapy"],
193
+ "key_drugs": [
194
+ {"name": "Osimertinib", "target": "EGFR", "setting": "1L EGFRm ± adjuvant"},
195
+ {"name": "Pembrolizumab", "target": "PD-1", "setting": "1L PD-L1 ≥50% ± chemo"},
196
+ {"name": "Amivantamab", "target": "EGFR/MET", "setting": "ex20ins"},
197
+ {"name": "Sotorasib", "target": "KRAS G12C", "setting": "2L+"},
198
+ {"name": "Lorlatinib", "target": "ALK", "setting": "1L ALK+"},
199
+ ],
200
+ "market_size": "$32B (2024), projected $48B by 2030",
201
+ "pipeline": {"phase_3": 85, "phase_2": 150, "phase_1": 100},
202
+ "key_trends": ["Perioperative immunotherapy", "MRD-guided adjuvant therapy",
203
+ "Antibody-drug conjugates expanding", "Bispecifics entering 1L"],
204
+ },
205
+ "Breast Cancer": {
206
+ "name": "Breast Cancer",
207
+ "global_incidence": "~2.3M new cases/year (2024)",
208
+ "mortality": "~685K deaths/year",
209
+ "subtypes": ["HR+/HER2- (70%)", "HER2+ (15-20%)", "TNBC (10-15%)"],
210
+ "approved_drugs_count": 52,
211
+ "key_drugs": [
212
+ {"name": "Trastuzumab deruxtecan", "target": "HER2", "setting": "HER2+ and HER2-low"},
213
+ {"name": "Sacituzumab govitecan", "target": "Trop-2", "setting": "TNBC, HR+/HER2-"},
214
+ {"name": "Palbociclib", "target": "CDK4/6", "setting": "HR+/HER2- 1L"},
215
+ {"name": "Olaparib", "target": "PARP", "setting": "BRCA1/2-mutated"},
216
+ ],
217
+ "market_size": "$28B (2024)",
218
+ "key_trends": ["CDK4/6 moving to adjuvant", "ADCs dominating HER2 space",
219
+ "Immunotherapy for TNBC", "Oral SERDs entering market"],
220
+ },
221
+ }
222
+
223
+ MOCK_DRUG_SEARCH = {
224
+ # Returned by any drug search; keyed by target/disease
225
+ "default": [
226
+ {"name": "Osimertinib", "target": "EGFR", "type": "Small molecule",
227
+ "highest_phase": "Approved", "first_approved": "2015-11-13",
228
+ "indications": ["NSCLC"], "company": "AstraZeneca"},
229
+ {"name": "Gefitinib", "target": "EGFR", "type": "Small molecule",
230
+ "highest_phase": "Approved", "first_approved": "2003-05-05",
231
+ "indications": ["NSCLC"], "company": "AstraZeneca"},
232
+ {"name": "Cetuximab", "target": "EGFR", "type": "Monoclonal antibody",
233
+ "highest_phase": "Approved", "first_approved": "2004-02-12",
234
+ "indications": ["CRC", "HNSCC"], "company": "Merck KGaA / BMS"},
235
+ {"name": "Trastuzumab deruxtecan", "target": "HER2", "type": "ADC",
236
+ "highest_phase": "Approved", "first_approved": "2019-12-20",
237
+ "indications": ["Breast Cancer", "Gastric Cancer", "NSCLC"],
238
+ "company": "Daiichi Sankyo / AstraZeneca"},
239
+ {"name": "Pembrolizumab", "target": "PD-1", "type": "Monoclonal antibody",
240
+ "highest_phase": "Approved", "first_approved": "2014-09-04",
241
+ "indications": ["Melanoma", "NSCLC", "HNSCC", "cHL"], "company": "Merck (MSD)"},
242
+ {"name": "Amivantamab", "target": "EGFR/MET", "type": "Bispecific antibody",
243
+ "highest_phase": "Approved", "first_approved": "2021-05-21",
244
+ "indications": ["NSCLC (ex20ins)"], "company": "Janssen"},
245
+ {"name": "Sotorasib", "target": "KRAS G12C", "type": "Small molecule",
246
+ "highest_phase": "Approved", "first_approved": "2021-05-28",
247
+ "indications": ["NSCLC (KRAS G12C)"], "company": "Amgen"},
248
+ {"name": "Sacituzumab govitecan", "target": "Trop-2", "type": "ADC",
249
+ "highest_phase": "Approved", "first_approved": "2020-04-22",
250
+ "indications": ["TNBC", "HR+/HER2- Breast Cancer"], "company": "Gilead"},
251
+ ]
252
+ }
253
+
254
+ # =============================================================================
255
+ # CSS — Product-grade styling
256
+ # =============================================================================
257
+
258
+ CUSTOM_CSS = """
259
+ /* ===== Global ===== */
260
+ :root {
261
+ --primary: #1a56db;
262
+ --primary-light: #3b82f6;
263
+ --primary-dark: #1e3a8a;
264
+ --accent: #059669;
265
+ --accent-light: #10b981;
266
+ --bg: #f8fafc;
267
+ --bg-card: #ffffff;
268
+ --text: #1e293b;
269
+ --text-secondary: #64748b;
270
+ --border: #e2e8f0;
271
+ --radius: 12px;
272
+ --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
273
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.08), 0 4px 6px -2px rgba(0,0,0,0.04);
274
+ }
275
+
276
+ .gradio-container {
277
+ max-width: 1200px !important;
278
+ margin: 0 auto !important;
279
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
280
+ background: var(--bg) !important;
281
+ }
282
+
283
+ /* ===== Header ===== */
284
+ .header-container {
285
+ background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%);
286
+ border-radius: var(--radius);
287
+ padding: 32px 40px;
288
+ margin-bottom: 24px;
289
+ color: white;
290
+ position: relative;
291
+ overflow: hidden;
292
+ }
293
+ .header-container::after {
294
+ content: '';
295
+ position: absolute;
296
+ top: -50%;
297
+ right: -10%;
298
+ width: 300px;
299
+ height: 300px;
300
+ background: radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%);
301
+ border-radius: 50%;
302
+ }
303
+ .header-title {
304
+ font-size: 28px;
305
+ font-weight: 700;
306
+ margin: 0 0 6px 0;
307
+ letter-spacing: -0.5px;
308
+ }
309
+ .header-subtitle {
310
+ font-size: 15px;
311
+ opacity: 0.85;
312
+ margin: 0;
313
+ font-weight: 400;
314
+ }
315
+ .header-badges {
316
+ display: flex;
317
+ gap: 10px;
318
+ margin-top: 14px;
319
+ }
320
+ .header-badge {
321
+ display: inline-flex;
322
+ align-items: center;
323
+ gap: 6px;
324
+ background: rgba(255,255,255,0.15);
325
+ backdrop-filter: blur(4px);
326
+ padding: 5px 12px;
327
+ border-radius: 20px;
328
+ font-size: 13px;
329
+ font-weight: 500;
330
+ }
331
+
332
+ /* ===== Tabs ===== */
333
+ .tabs {
334
+ border: none !important;
335
+ }
336
+ .tab-nav {
337
+ background: var(--bg-card) !important;
338
+ border-radius: var(--radius) !important;
339
+ padding: 6px !important;
340
+ box-shadow: var(--shadow);
341
+ margin-bottom: 20px !important;
342
+ gap: 2px !important;
343
+ }
344
+ .tab-nav button {
345
+ border-radius: 10px !important;
346
+ padding: 10px 20px !important;
347
+ font-size: 14px !important;
348
+ font-weight: 500 !important;
349
+ border: none !important;
350
+ color: var(--text-secondary) !important;
351
+ transition: all 0.2s ease !important;
352
+ background: transparent !important;
353
+ }
354
+ .tab-nav button:hover {
355
+ background: #f1f5f9 !important;
356
+ color: var(--text) !important;
357
+ }
358
+ .tab-nav button.selected {
359
+ background: var(--primary) !important;
360
+ color: white !important;
361
+ box-shadow: 0 2px 8px rgba(26,86,219,0.25);
362
+ }
363
+
364
+ /* ===== Cards ===== */
365
+ .card {
366
+ background: var(--bg-card);
367
+ border-radius: var(--radius);
368
+ padding: 24px;
369
+ box-shadow: var(--shadow);
370
+ border: 1px solid var(--border);
371
+ margin-bottom: 16px;
372
+ transition: box-shadow 0.2s ease;
373
+ }
374
+ .card:hover {
375
+ box-shadow: var(--shadow-lg);
376
+ }
377
+
378
+ /* ===== Chat ===== */
379
+ .chat-container {
380
+ border-radius: var(--radius);
381
+ overflow: hidden;
382
+ box-shadow: var(--shadow);
383
+ border: 1px solid var(--border);
384
+ background: white;
385
+ }
386
+ .chat-message {
387
+ padding: 16px 20px;
388
+ border-bottom: 1px solid var(--border);
389
+ }
390
+ .chat-message.user {
391
+ background: #f0f9ff;
392
+ }
393
+ .chat-message.assistant {
394
+ background: white;
395
+ }
396
+
397
+ /* ===== Agent Thinking ===== */
398
+ .thinking-steps {
399
+ background: #fffbeb;
400
+ border: 1px solid #fde68a;
401
+ border-radius: 10px;
402
+ padding: 12px 16px;
403
+ margin: 8px 0;
404
+ font-size: 13px;
405
+ }
406
+ .thinking-step {
407
+ padding: 4px 0;
408
+ color: #92400e;
409
+ }
410
+
411
+ /* ===== Inputs ===== */
412
+ .agent-input textarea {
413
+ border-radius: 10px !important;
414
+ border: 2px solid var(--border) !important;
415
+ padding: 12px 16px !important;
416
+ font-size: 15px !important;
417
+ transition: border-color 0.2s ease !important;
418
+ resize: none !important;
419
+ }
420
+ .agent-input textarea:focus {
421
+ border-color: var(--primary-light) !important;
422
+ box-shadow: 0 0 0 3px rgba(59,130,246,0.15) !important;
423
+ }
424
+
425
+ /* ===== Buttons ===== */
426
+ .btn-primary {
427
+ background: linear-gradient(135deg, var(--primary), var(--primary-light)) !important;
428
+ color: white !important;
429
+ border: none !important;
430
+ border-radius: 10px !important;
431
+ padding: 10px 24px !important;
432
+ font-weight: 600 !important;
433
+ cursor: pointer !important;
434
+ transition: all 0.2s ease !important;
435
+ }
436
+ .btn-primary:hover {
437
+ transform: translateY(-1px);
438
+ box-shadow: 0 4px 12px rgba(26,86,219,0.3);
439
+ }
440
+
441
+ /* ===== Example Chips ===== */
442
+ .example-chips {
443
+ display: flex;
444
+ flex-wrap: wrap;
445
+ gap: 8px;
446
+ margin: 12px 0;
447
+ }
448
+
449
+ /* ===== Stats Grid ===== */
450
+ .stats-grid {
451
+ display: grid;
452
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
453
+ gap: 12px;
454
+ margin: 16px 0;
455
+ }
456
+
457
+ /* ===== Report Content ===== */
458
+ .report h2 { font-size: 22px; margin-top: 24px; color: var(--primary-dark); }
459
+ .report h3 { font-size: 17px; margin-top: 18px; color: var(--text); }
460
+ .report table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
461
+ .report th { background: #f1f5f9; padding: 10px 12px; text-align: left; font-weight: 600; border-bottom: 2px solid var(--border); }
462
+ .report td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
463
+ .report tr:hover td { background: #f8fafc; }
464
+
465
+ /* ===== Status Badge ===== */
466
+ .badge {
467
+ display: inline-block;
468
+ padding: 2px 10px;
469
+ border-radius: 12px;
470
+ font-size: 12px;
471
+ font-weight: 600;
472
+ }
473
+ .badge-approved { background: #dcfce7; color: #166534; }
474
+ .badge-phase3 { background: #dbeafe; color: #1e40af; }
475
+ .badge-phase2 { background: #fef3c7; color: #92400e; }
476
+ .badge-phase1 { background: #fce7f3; color: #9d174d; }
477
+
478
+ /* ===== Footer ===== */
479
+ .footer {
480
+ text-align: center;
481
+ padding: 32px 16px;
482
+ color: var(--text-secondary);
483
+ font-size: 13px;
484
+ border-top: 1px solid var(--border);
485
+ margin-top: 40px;
486
+ }
487
+
488
+ /* ===== Loading ===== */
489
+ .loading-pulse {
490
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
491
+ }
492
+ @keyframes pulse {
493
+ 0%, 100% { opacity: 1; }
494
+ 50% { opacity: 0.5; }
495
+ }
496
+
497
+ /* ===== Animations ===== */
498
+ @keyframes fadeIn {
499
+ from { opacity: 0; transform: translateY(8px); }
500
+ to { opacity: 1; transform: translateY(0); }
501
+ }
502
+ .fade-in { animation: fadeIn 0.4s ease-out; }
503
+
504
+ /* Misc overrides */
505
+ footer { display: none !important; }
506
+ """
507
+
508
+ # =============================================================================
509
+ # MCP CLIENT
510
+ # =============================================================================
511
+
512
+ _mcp_tools_cache: Optional[List[str]] = None
513
+
514
+ async def get_mcp_tools() -> List[str]:
515
+ """Fetch available MCP tool names with caching."""
516
+ global _mcp_tools_cache
517
+ if _mcp_tools_cache is not None:
518
+ return _mcp_tools_cache
519
+ if not API_KEY:
520
+ return []
521
+ try:
522
+ from mcp import ClientSession
523
+ from mcp.client.streamable_http import streamablehttp_client
524
+ async with streamablehttp_client(SERVER_URL, timeout=15, sse_read_timeout=15) as (read, write, _):
525
+ async with ClientSession(read, write) as session:
526
+ await session.initialize()
527
+ result = await session.list_tools()
528
+ _mcp_tools_cache = [t.name for t in result.tools]
529
+ return _mcp_tools_cache
530
+ except Exception as e:
531
+ print(f"[MCP] Tool discovery failed: {e}")
532
+ return []
533
+
534
+
535
+ async def call_mcp_tool(tool_name: str, args: dict) -> Optional[dict]:
536
+ """Call a specific MCP tool. Returns parsed JSON or None on failure."""
537
+ if not API_KEY:
538
+ return None
539
+ try:
540
+ from mcp import ClientSession
541
+ from mcp.client.streamable_http import streamablehttp_client
542
+ async with streamablehttp_client(SERVER_URL, timeout=30, sse_read_timeout=30) as (read, write, _):
543
+ async with ClientSession(read, write) as session:
544
+ await session.initialize()
545
+ result = await session.call_tool(tool_name, arguments=args)
546
+ if result.content:
547
+ text = result.content[0].text
548
+ return json.loads(text) if isinstance(text, str) else text
549
+ except Exception as e:
550
+ print(f"[MCP] {tool_name} failed: {e}")
551
+ return None
552
+
553
+
554
+ # =============================================================================
555
+ # AGENT ENGINE — Intent Parsing & Tool Routing
556
+ # =============================================================================
557
+
558
+ # Entity extraction patterns
559
+ TARGET_KEYWORDS = {
560
+ "egfr": "EGFR", "her2": "HER2", "her-2": "HER2", "erbb2": "HER2",
561
+ "pd-l1": "PD-L1", "pdl1": "PD-L1", "pd-1": "PD-1", "pd1": "PD-1",
562
+ "braf": "BRAF", "alk": "ALK", "ros1": "ROS1", "kras": "KRAS",
563
+ "vegf": "VEGF", "vegfr": "VEGFR", "ctla-4": "CTLA-4", "ctla4": "CTLA-4",
564
+ "stat3": "STAT3", "met": "c-MET", "c-met": "c-MET", "ret": "RET",
565
+ "ntrk": "NTRK", "fgfr": "FGFR", "parp": "PARP", "cdk4": "CDK4/6",
566
+ "cdk4/6": "CDK4/6", "btk": "BTK", "jak": "JAK", "flt3": "FLT3",
567
+ "idh1": "IDH1", "idh2": "IDH2", "tp53": "TP53", "p53": "TP53",
568
+ "brca1": "BRCA1", "brca2": "BRCA2", "trop-2": "Trop-2", "trop2": "Trop-2",
569
+ "claudin": "Claudin 18.2", "cldn18": "Claudin 18.2",
570
+ }
571
+
572
+ DISEASE_KEYWORDS = {
573
+ "non-small cell lung cancer": "NSCLC", "nsclc": "NSCLC", "lung cancer": "Lung Cancer",
574
+ "breast cancer": "Breast Cancer", "triple-negative breast": "Triple-Negative Breast Cancer",
575
+ "tnbc": "Triple-Negative Breast Cancer",
576
+ "colorectal": "Colorectal Cancer", "crc": "Colorectal Cancer",
577
+ "melanoma": "Melanoma",
578
+ "pancreatic": "Pancreatic Cancer",
579
+ "hepatocellular": "Hepatocellular Carcinoma", "hcc": "Hepatocellular Carcinoma",
580
+ "liver cancer": "Hepatocellular Carcinoma",
581
+ "gastric": "Gastric Cancer", "stomach cancer": "Gastric Cancer",
582
+ "leukemia": "Leukemia", "lymphoma": "Lymphoma",
583
+ "ovarian": "Ovarian Cancer", "prostate": "Prostate Cancer",
584
+ "multiple myeloma": "Multiple Myeloma", "mm": "Multiple Myeloma",
585
+ "renal cell": "Renal Cell Carcinoma", "rcc": "Renal Cell Carcinoma",
586
+ "bladder": "Urothelial Carcinoma", "urothelial": "Urothelial Carcinoma",
587
+ "head and neck": "Head and Neck Cancer", "hnscc": "Head and Neck Cancer",
588
+ "glioblastoma": "Glioblastoma", "gbm": "Glioblastoma",
589
+ "alzheimer": "Alzheimer's Disease", "parkinson": "Parkinson's Disease",
590
+ "diabetes": "Diabetes", "obesity": "Obesity",
591
+ }
592
+
593
+ COMPANY_KEYWORDS = {
594
+ "roche": "Roche", "genentech": "Roche",
595
+ "novartis": "Novartis",
596
+ "pfizer": "Pfizer",
597
+ "merck": "Merck (MSD)", "msd": "Merck (MSD)",
598
+ "bristol-myers": "Bristol-Myers Squibb", "bms": "Bristol-Myers Squibb",
599
+ "astrazeneca": "AstraZeneca", "az": "AstraZeneca",
600
+ "johnson": "Johnson & Johnson", "jnj": "Johnson & Johnson", "janssen": "Johnson & Johnson",
601
+ "sanofi": "Sanofi",
602
+ "gsk": "GlaxoSmithKline",
603
+ "abbvie": "AbbVie",
604
+ "amgen": "Amgen",
605
+ "gilead": "Gilead",
606
+ "lilly": "Eli Lilly", "eli lilly": "Eli Lilly",
607
+ "moderna": "Moderna",
608
+ "biontech": "BioNTech",
609
+ "daiichi": "Daiichi Sankyo",
610
+ "beigene": "BeiGene", "百济": "BeiGene",
611
+ "innovent": "Innovent", "信达": "Innovent",
612
+ "hengrui": "Hengrui", "恒瑞": "Hengrui",
613
+ "akebia": "Akebia",
614
+ }
615
+
616
+ PHASE_KEYWORDS = {
617
+ "phase 3": "phase_3", "phase iii": "phase_3", "phase iii": "phase_3", "pivotal": "phase_3",
618
+ "phase 2": "phase_2", "phase ii": "phase_2",
619
+ "phase 1": "phase_1", "phase i": "phase_1", "first-in-human": "phase_1", "fih": "phase_1",
620
+ "approved": "approved", "marketed": "approved", "launched": "approved",
621
+ "preclinical": "preclinical",
622
+ }
623
+
624
+ MODULE_KEYWORDS = {
625
+ "target": ["target", "靶点", "receptor", "kinase", "protein", "gene", "mutation", "pathway",
626
+ "inhibitor target", "antibody target", "drug target"],
627
+ "drug": ["drug", "药物", "medicine", "inhibitor", "antibody", "therapy", "treatment regimen",
628
+ "approved drug", "pipeline drug", "molecule", "compound", "modality"],
629
+ "disease": ["disease", "疾病", "cancer", "tumor", "indication", "适应症", "epidemiology",
630
+ "patients", "prevalence", "incidence", "mortality"],
631
+ "company": ["company", "公司", "pharma", "biotech", "pipeline of", "portfolio",
632
+ "acquisition", "merger", "partner", "revenue"],
633
+ "trial": ["trial", "试验", "clinical", "nct", "enrollment", "endpoint", "randomized",
634
+ "double-blind", "phase 3 trial", "phase 2 trial", "phase 1 trial"],
635
+ }
636
+
637
+
638
+ def parse_intent(query: str) -> Dict:
639
+ """
640
+ Parse a natural language query to determine:
641
+ - module (target/drug/disease/company/trial)
642
+ - entities (targets, diseases, companies, phases)
643
+ - mcp_args (for direct MCP call)
644
+ - confidence
645
+ """
646
+ lower = query.lower()
647
+ result = {
648
+ "module": "target", # default
649
+ "entities": {"targets": [], "diseases": [], "companies": [], "phases": []},
650
+ "mcp_args": {"limit": 10},
651
+ "confidence": 0.0,
652
+ "thinking": [],
653
+ }
654
+
655
+ # Extract entities
656
+ for kw, val in TARGET_KEYWORDS.items():
657
+ if kw in lower and val not in result["entities"]["targets"]:
658
+ result["entities"]["targets"].append(val)
659
+
660
+ for kw, val in DISEASE_KEYWORDS.items():
661
+ if kw in lower and val not in result["entities"]["diseases"]:
662
+ result["entities"]["diseases"].append(val)
663
+
664
+ for kw, val in COMPANY_KEYWORDS.items():
665
+ if kw in lower and val not in result["entities"]["companies"]:
666
+ result["entities"]["companies"].append(val)
667
+
668
+ for kw, val in PHASE_KEYWORDS.items():
669
+ if kw in lower:
670
+ result["entities"]["phases"].append(val)
671
+
672
+ # Determine module by scoring keyword matches
673
+ scores = {m: 0 for m in MODULE_KEYWORDS}
674
+ for module, keywords in MODULE_KEYWORDS.items():
675
+ for kw in keywords:
676
+ if kw in lower:
677
+ scores[module] += 1
678
+
679
+ best_module = max(scores, key=scores.get)
680
+ max_score = scores[best_module]
681
+
682
+ # Heuristic overrides based on entities found
683
+ if result["entities"]["targets"] and not result["entities"]["diseases"] and not result["entities"]["companies"]:
684
+ if max_score == 0 or best_module == "drug":
685
+ result["module"] = "target" if scores["target"] >= scores["drug"] else "drug"
686
+ else:
687
+ result["module"] = best_module
688
+ elif result["entities"]["diseases"] and not result["entities"]["targets"]:
689
+ result["module"] = "disease"
690
+ elif result["entities"]["companies"] and not result["entities"]["targets"]:
691
+ result["module"] = "company"
692
+ elif "trial" in lower or "clinical" in lower or "enrollment" in lower:
693
+ result["module"] = "trial"
694
+ else:
695
+ result["module"] = best_module if max_score > 0 else "target"
696
+
697
+ # Build MCP args based on module
698
+ mod = result["module"]
699
+ if mod == "target" and result["entities"]["targets"]:
700
+ result["mcp_args"] = {"target": result["entities"]["targets"][:3], "limit": 10}
701
+ result["thinking"].append(f"🔍 Detected target query → searching for: {', '.join(result['entities']['targets'][:3])}")
702
+ elif mod == "drug":
703
+ args = {"limit": 10}
704
+ if result["entities"]["targets"]:
705
+ args["target"] = result["entities"]["targets"][:3]
706
+ if result["entities"]["diseases"]:
707
+ args["disease"] = result["entities"]["diseases"][:3]
708
+ if result["entities"]["phases"]:
709
+ args["highest_phase"] = result["entities"]["phases"][:3]
710
+ result["mcp_args"] = args
711
+ result["thinking"].append(f"🔍 Detected drug query → searching with {json.dumps(args)}")
712
+ elif mod == "disease" and result["entities"]["diseases"]:
713
+ result["mcp_args"] = {"disease": result["entities"]["diseases"][:3], "limit": 10}
714
+ result["thinking"].append(f"🔍 Detected disease query → searching for: {', '.join(result['entities']['diseases'][:3])}")
715
+ elif mod == "company" and result["entities"]["companies"]:
716
+ result["mcp_args"] = {"company": result["entities"]["companies"][:3], "limit": 10}
717
+ result["thinking"].append(f"🔍 Detected company query → searching for: {', '.join(result['entities']['companies'][:3])}")
718
+ elif mod == "trial":
719
+ args = {"limit": 10}
720
+ if result["entities"]["targets"]:
721
+ args["target"] = result["entities"]["targets"][:3]
722
+ if result["entities"]["diseases"]:
723
+ args["disease"] = result["entities"]["diseases"][:3]
724
+ if result["entities"]["phases"]:
725
+ args["phase"] = result["entities"]["phases"][:3]
726
+ result["mcp_args"] = args
727
+ result["thinking"].append(f"🔍 Detected trial query → searching with {json.dumps(args)}")
728
+
729
+ # Confidence
730
+ entity_count = sum(len(v) for v in result["entities"].values())
731
+ result["confidence"] = min(entity_count * 0.25 + scores[result["module"]] * 0.15, 0.95)
732
+
733
+ return result
734
+
735
+
736
+ # =============================================================================
737
+ # REPORT BUILDERS
738
+ # =============================================================================
739
+
740
+ def _badge(phase: str) -> str:
741
+ """Generate an HTML badge for a drug phase."""
742
+ phase_lower = phase.lower()
743
+ if "approved" in phase_lower:
744
+ cls = "badge-approved"
745
+ elif "phase 3" in phase_lower or "phase iii" in phase_lower:
746
+ cls = "badge-phase3"
747
+ elif "phase 2" in phase_lower or "phase ii" in phase_lower:
748
+ cls = "badge-phase2"
749
+ elif "phase 1" in phase_lower or "phase i" in phase_lower:
750
+ cls = "badge-phase1"
751
+ else:
752
+ cls = "badge-phase2"
753
+ return f'<span class="badge {cls}">{phase}</span>'
754
+
755
+
756
+ def build_target_report(target_data: Dict) -> str:
757
+ """Build a structured target intelligence report."""
758
+ name = target_data.get("name", "Unknown")
759
+ full = target_data.get("full_name", "")
760
+ family = target_data.get("family", "N/A")
761
+ cls = target_data.get("class", "N/A")
762
+ drugs = target_data.get("approved_drugs", [])
763
+ pipeline = target_data.get("pipeline_summary", {})
764
+ pathways = target_data.get("pathways", [])
765
+ mutations = target_data.get("mutation_hotspots", [])
766
+ landscape = target_data.get("competitive_landscape", "")
767
+
768
+ report = []
769
+ report.append(f"## 🎯 {name} — Target Intelligence Report")
770
+ report.append("")
771
+
772
+ # Overview card
773
+ report.append("### 📋 Overview")
774
+ report.append(f"| Property | Value |")
775
+ report.append(f"|----------|-------|")
776
+ report.append(f"| **Full Name** | {full} |")
777
+ report.append(f"| **Family** | {family} |")
778
+ report.append(f"| **Class** | {cls} |")
779
+ report.append(f"| **Approved Drugs** | {len(drugs)} |")
780
+ report.append("")
781
+
782
+ # Pathways
783
+ if pathways:
784
+ report.append("### 🧬 Signaling Pathways")
785
+ for p in pathways:
786
+ report.append(f"- {p}")
787
+ report.append("")
788
+
789
+ # Mutations
790
+ if mutations:
791
+ report.append("### 🔬 Key Mutations / Variants")
792
+ for m in mutations:
793
+ report.append(f"- {m}")
794
+ report.append("")
795
+
796
+ # Approved Drugs Table
797
+ if drugs:
798
+ report.append(f"### 💊 Approved Drugs ({len(drugs)})")
799
+ report.append("| Drug | Type | Generation | Year | Indications | Company |")
800
+ report.append("|------|------|-----------|------|-------------|---------|")
801
+ for d in drugs:
802
+ inds = ", ".join(d.get("indications", [])[:2])
803
+ if len(d.get("indications", [])) > 2:
804
+ inds += f" +{len(d['indications']) - 2} more"
805
+ report.append(f"| {d['name']} | {d['type']} | {d.get('gen', '-')} | "
806
+ f"{d['year']} | {inds} | {d.get('company', '-')} |")
807
+ report.append("")
808
+
809
+ # Pipeline
810
+ if pipeline:
811
+ report.append("### 🔬 Pipeline Overview")
812
+ report.append(f"| Phase | Count |")
813
+ report.append(f"|-------|-------|")
814
+ for phase in ["phase_3", "phase_2", "phase_1", "preclinical"]:
815
+ label = phase.replace("_", " ").title()
816
+ report.append(f"| {label} | {pipeline.get(phase, 'N/A')} |")
817
+ report.append("")
818
+ hot = pipeline.get("hot_topics", [])
819
+ if hot:
820
+ report.append("**🔥 Hot Topics:**")
821
+ for t in hot:
822
+ report.append(f"- {t}")
823
+ report.append("")
824
+
825
+ # Competitive landscape
826
+ if landscape:
827
+ report.append("### 🏔️ Competitive Landscape")
828
+ report.append(landscape)
829
+ report.append("")
830
+
831
+ report.append("---")
832
+ report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*")
833
+ return "\n".join(report)
834
+
835
+
836
+ def build_drug_report(items: List[Dict], query_summary: str, total: int = 0) -> str:
837
+ """Build a drug pipeline report from search results."""
838
+ if not items:
839
+ return f"## 💊 Drug Search: {query_summary}\n\n📭 No results found. Try a different query."
840
+
841
+ drug_types = Counter()
842
+ companies = Counter()
843
+ years = []
844
+ phases = Counter()
845
+
846
+ for item in items:
847
+ drug_types[item.get("type", "Unknown")] += 1
848
+ companies[item.get("company", "Unknown")] += 1
849
+ phases[item.get("highest_phase", "Unknown")] += 1
850
+ date = item.get("first_approved", "")
851
+ if date and date != "N/A":
852
+ try:
853
+ years.append(int(date.split("-")[0]))
854
+ except (ValueError, IndexError):
855
+ pass
856
+
857
+ report = []
858
+ report.append(f"## 💊 {query_summary}")
859
+ report.append(f"*{len(items)} results{' of ' + str(total) if total else ''}*")
860
+ report.append("")
861
+
862
+ # Stats row
863
+ report.append("### 📊 Summary")
864
+ stats = []
865
+ if drug_types:
866
+ top = drug_types.most_common(3)
867
+ stats.append(f"**Top Types:** " + " · ".join(f"{t} ({c})" for t, c in top))
868
+ if phases:
869
+ top_p = phases.most_common(3)
870
+ stats.append(f"**Phases:** " + " · ".join(f"{p} ({c})" for p, c in top_p))
871
+ if years:
872
+ stats.append(f"**Timeline:** {min(years)} – {max(years)}")
873
+ if companies:
874
+ top_c = companies.most_common(3)
875
+ stats.append(f"**Top Companies:** " + " · ".join(f"{c} ({n})" for c, n in top_c))
876
+ for s in stats:
877
+ report.append(f"- {s}")
878
+ report.append("")
879
+
880
+ # Drug table
881
+ report.append("### 🧪 Pipeline Details")
882
+ report.append("| Drug | Type | Phase | Indications | Company |")
883
+ report.append("|------|------|-------|-------------|---------|")
884
+ for item in items[:15]:
885
+ name = item.get("name", "N/A")
886
+ dtype = item.get("type", "N/A")
887
+ phase = item.get("highest_phase", "N/A")
888
+ inds = ", ".join(item.get("indications", ["N/A"])[:2])
889
+ comp = item.get("company", "N/A")
890
+ report.append(f"| {name} | {dtype} | {_badge(phase)} | {inds} | {comp} |")
891
+ report.append("")
892
+
893
+ report.append("---")
894
+ report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*")
895
+ return "\n".join(report)
896
+
897
+
898
+ def build_disease_report(disease_data: Dict) -> str:
899
+ """Build a disease landscape report."""
900
+ name = disease_data.get("name", "Unknown")
901
+ report = []
902
+ report.append(f"## 🏥 {name} — Disease Landscape")
903
+ report.append("")
904
+
905
+ report.append("### 📊 Epidemiology")
906
+ for key in ["global_incidence", "mortality", "5yr_survival"]:
907
+ if key in disease_data:
908
+ label = key.replace("5yr", "5-Year ").replace("_", " ").title()
909
+ report.append(f"- **{label}:** {disease_data[key]}")
910
+ report.append("")
911
+
912
+ mutations = disease_data.get("major_mutations", [])
913
+ if mutations:
914
+ report.append("### 🧬 Key Driver Mutations")
915
+ for m in mutations:
916
+ report.append(f"- {m}")
917
+ report.append("")
918
+
919
+ key_drugs = disease_data.get("key_drugs", [])
920
+ if key_drugs:
921
+ report.append("### 💊 Key Therapies")
922
+ report.append("| Drug | Target | Setting |")
923
+ report.append("|------|--------|---------|")
924
+ for d in key_drugs:
925
+ report.append(f"| {d['name']} | {d['target']} | {d.get('setting', '-')} |")
926
+ report.append("")
927
+
928
+ report.append("### 📈 Market & Pipeline")
929
+ for key in ["market_size", "approved_drugs_count"]:
930
+ if key in disease_data:
931
+ label = key.replace("_", " ").title().replace("Drugs", "Drugs").replace("Count", "Count")
932
+ report.append(f"- **{label}:** {disease_data[key]}")
933
+ pipeline = disease_data.get("pipeline", {})
934
+ if pipeline:
935
+ report.append(f"- **Pipeline:** Phase 3: {pipeline.get('phase_3', 'N/A')} | "
936
+ f"Phase 2: {pipeline.get('phase_2', 'N/A')} | Phase 1: {pipeline.get('phase_1', 'N/A')}")
937
+ report.append("")
938
+
939
+ trends = disease_data.get("key_trends", [])
940
+ if trends:
941
+ report.append("### 🔥 Key Trends")
942
+ for t in trends:
943
+ report.append(f"- {t}")
944
+ report.append("")
945
+
946
+ report.append("---")
947
+ report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*")
948
+ return "\n".join(report)
949
+
950
+
951
+ def build_company_report(company_data: Dict) -> str:
952
+ """Build a company profile report."""
953
+ name = company_data.get("name", "Unknown")
954
+ report = []
955
+ report.append(f"## 🏢 {name} — Company Profile")
956
+ report.append("")
957
+
958
+ report.append("### 📋 Company Overview")
959
+ for key in ["ticker", "headquarters", "employees", "2024_revenue"]:
960
+ if key in company_data:
961
+ label = key.replace("_", " ").title().replace("2024", "2024")
962
+ report.append(f"- **{label}:** {company_data[key]}")
963
+ report.append("")
964
+
965
+ ta = company_data.get("therapeutic_areas", [])
966
+ if ta:
967
+ report.append(f"**Therapeutic Areas:** " + " · ".join(ta))
968
+ report.append("")
969
+
970
+ flagship = company_data.get("flagship_drugs", [])
971
+ if flagship:
972
+ report.append("### 💊 Flagship Drugs")
973
+ report.append("| Drug | Target | Sales | Phase |")
974
+ report.append("|------|--------|-------|-------|")
975
+ for d in flagship:
976
+ report.append(f"| {d['name']} | {d['target']} | {d['sales']} | {_badge(d['phase'])} |")
977
+ report.append("")
978
+
979
+ pipeline = company_data.get("pipeline_count", {})
980
+ if pipeline:
981
+ report.append("### 🔬 Pipeline Overview")
982
+ report.append(f"| Phase | Count |")
983
+ report.append(f"|-------|-------|")
984
+ for phase in ["approved", "phase_3", "phase_2", "phase_1"]:
985
+ label = phase.replace("_", " ").title()
986
+ report.append(f"| {label} | {pipeline.get(phase, 'N/A')} |")
987
+ report.append("")
988
+
989
+ deals = company_data.get("recent_deals", [])
990
+ if deals:
991
+ report.append("### 🤝 Recent Strategic Deals")
992
+ for d in deals:
993
+ report.append(f"- {d}")
994
+ report.append("")
995
+
996
+ strategy = company_data.get("strategy", "")
997
+ if strategy:
998
+ report.append(f"### 🎯 Strategic Outlook\n{strategy}")
999
+ report.append("")
1000
+
1001
+ report.append("---")
1002
+ report.append(f"*Report generated by PatSnap Pharma Intelligence Agent*")
1003
+ return "\n".join(report)
1004
+
1005
+
1006
+ def build_agent_thinking(intent: Dict, mcp_available: bool, mcp_result: Optional[Dict]) -> str:
1007
+ """Build the agent thinking trace HTML."""
1008
+ steps = []
1009
+ steps.append(f"🧠 **Intent:** {intent['module'].title()} query (confidence: {intent['confidence']:.0%})")
1010
+
1011
+ if intent["entities"]["targets"]:
1012
+ steps.append(f"🎯 **Targets:** {', '.join(intent['entities']['targets'])}")
1013
+ if intent["entities"]["diseases"]:
1014
+ steps.append(f"🏥 **Diseases:** {', '.join(intent['entities']['diseases'])}")
1015
+ if intent["entities"]["companies"]:
1016
+ steps.append(f"🏢 **Companies:** {', '.join(intent['entities']['companies'])}")
1017
+
1018
+ for think in intent.get("thinking", []):
1019
+ steps.append(think)
1020
+
1021
+ if mcp_available:
1022
+ if mcp_result:
1023
+ total = mcp_result.get("total", 0)
1024
+ items = len(mcp_result.get("items", []))
1025
+ steps.append(f"✅ **MCP Live Data:** Retrieved {items} of {total} records")
1026
+ else:
1027
+ steps.append(f"⚠️ **MCP:** No results from live API, using knowledge base")
1028
+ else:
1029
+ steps.append(f"📚 **Source:** Knowledge base (no API key configured)")
1030
+
1031
+ return "\n".join(f"<div class='thinking-step'>{s}</div>" for s in steps)
1032
+
1033
+
1034
+ # =============================================================================
1035
+ # CORE AGENT EXECUTION
1036
+ # =============================================================================
1037
+
1038
+ async def run_agent(query: str, lang: str = "en") -> Tuple[str, str]:
1039
+ """
1040
+ Execute the full agent pipeline:
1041
+ 1. Parse intent
1042
+ 2. Try MCP → fallback to knowledge base
1043
+ 3. Build report
1044
+
1045
+ Returns (thinking_html, report_markdown)
1046
+ """
1047
+ if not query or not query.strip():
1048
+ return "", "*👋 Welcome! Ask me anything about drug targets, diseases, companies, or clinical trials.*"
1049
+
1050
+ # Step 1: Parse intent
1051
+ intent = parse_intent(query)
1052
+ module = intent["module"]
1053
+ mod_info = MODULES[module]
1054
+
1055
+ # Step 2: Try MCP
1056
+ mcp_data = None
1057
+ mcp_available = bool(API_KEY)
1058
+ if mcp_available:
1059
+ try:
1060
+ from mcp import ClientSession
1061
+ from mcp.client.streamable_http import streamablehttp_client
1062
+ async with streamablehttp_client(SERVER_URL, timeout=25, sse_read_timeout=25) as (read, write, _):
1063
+ async with ClientSession(read, write) as session:
1064
+ await session.initialize()
1065
+ tools = await session.list_tools()
1066
+ tool_names = [t.name for t in tools.tools]
1067
+
1068
+ # Find matching tool
1069
+ tool_to_call = None
1070
+ for t in [mod_info["tool"], "ls_drug_search"]: # fallback to drug search
1071
+ if t in tool_names:
1072
+ tool_to_call = t
1073
+ break
1074
+
1075
+ if tool_to_call:
1076
+ result = await session.call_tool(tool_to_call, arguments=intent["mcp_args"])
1077
+ if result.content:
1078
+ text = result.content[0].text
1079
+ mcp_data = json.loads(text) if isinstance(text, str) else text
1080
+ except Exception as e:
1081
+ print(f"[Agent] MCP error: {e}")
1082
+
1083
+ # Step 3: Build thinking trace
1084
+ if module == "target" and intent["entities"]["targets"]:
1085
+ entity_name = intent["entities"]["targets"][0]
1086
+ elif module == "disease" and intent["entities"]["diseases"]:
1087
+ entity_name = intent["entities"]["diseases"][0]
1088
+ elif module == "company" and intent["entities"]["companies"]:
1089
+ entity_name = intent["entities"]["companies"][0]
1090
+ else:
1091
+ entity_name = query[:50]
1092
+
1093
+ thinking = build_agent_thinking(intent, mcp_available, mcp_data)
1094
+
1095
+ # Step 4: Build report
1096
+ report = ""
1097
+
1098
+ if mcp_data and mcp_data.get("items"):
1099
+ # Live MCP data
1100
+ items = mcp_data["items"]
1101
+ total = mcp_data.get("total", len(items))
1102
+ report = build_drug_report(items, f"Results for \"{entity_name}\"", total)
1103
+ elif module == "target":
1104
+ # Knowledge base fallback for targets
1105
+ target_key = entity_name.upper()
1106
+ target_data = MOCK_TARGETS.get(target_key)
1107
+ if target_data:
1108
+ report = build_target_report(target_data)
1109
+ else:
1110
+ report = build_drug_report(
1111
+ MOCK_DRUG_SEARCH["default"],
1112
+ f"Results for \"{entity_name}\" (target overview)",
1113
+ len(MOCK_DRUG_SEARCH["default"])
1114
+ )
1115
+ elif module == "disease":
1116
+ # Knowledge base fallback for diseases
1117
+ disease_key = None
1118
+ for kw, val in DISEASE_KEYWORDS.items():
1119
+ if val.upper() == entity_name.upper():
1120
+ disease_key = val
1121
+ break
1122
+ if not disease_key:
1123
+ disease_key = entity_name
1124
+ disease_data = MOCK_DISEASES.get(disease_key)
1125
+ if disease_data:
1126
+ report = build_disease_report(disease_data)
1127
+ else:
1128
+ report = build_drug_report(
1129
+ MOCK_DRUG_SEARCH["default"],
1130
+ f"Results for \"{entity_name}\" (disease overview)",
1131
+ )
1132
+ elif module == "company":
1133
+ # Knowledge base fallback for companies
1134
+ company_data = MOCK_COMPANIES.get(entity_name)
1135
+ if company_data:
1136
+ report = build_company_report(company_data)
1137
+ else:
1138
+ report = f"## 🏢 {entity_name}\n\n*Detailed company profile not available in demo mode. "
1139
+ report += "Connect a PatSnap API key for live data.*"
1140
+ else:
1141
+ # Default drug report from knowledge base
1142
+ report = build_drug_report(
1143
+ MOCK_DRUG_SEARCH["default"],
1144
+ f"Results for \"{entity_name}\"",
1145
+ )
1146
+
1147
+ return thinking, report
1148
+
1149
+
1150
+ # =============================================================================
1151
+ # GRADIO UI
1152
+ # =============================================================================
1153
+
1154
+ def create_header():
1155
+ """Create the app header block."""
1156
+ return gr.HTML("""
1157
+ <div class="header-container">
1158
+ <h1 class="header-title">🧬 PatSnap Pharma Intelligence</h1>
1159
+ <p class="header-subtitle">AI-powered drug discovery intelligence — explore targets, drugs, diseases, companies, and clinical trials.</p>
1160
+ <div class="header-badges">
1161
+ <span class="header-badge">🔬 Multi-Module Agent</span>
1162
+ <span class="header-badge">📊 Live MCP Data</span>
1163
+ <span class="header-badge">🤖 AI-Powered Reports</span>
1164
+ </div>
1165
+ </div>
1166
+ """)
1167
+
1168
+
1169
+ def create_footer():
1170
+ """Create the app footer."""
1171
+ return gr.HTML("""
1172
+ <div class="footer">
1173
+ <strong>PatSnap Pharma Intelligence</strong> — Powered by PatSnap Life Sciences MCP<br>
1174
+ <span style="opacity:0.7">Data sourced from PatSnap's pharmaceutical intelligence platform. Demo data shown where API key is not configured.</span>
1175
+ </div>
1176
+ """)
1177
+
1178
+
1179
+ def build_app():
1180
+ """Build the complete Gradio application."""
1181
+
1182
+ with gr.Blocks(
1183
+ title="PatSnap Pharma Intelligence",
1184
+ analytics_enabled=False,
1185
+ ) as app:
1186
+ create_header()
1187
+
1188
+ # ===== TABS =====
1189
+ with gr.Tabs(elem_classes="tabs"):
1190
+
1191
+ # ========== TAB 1: AGENT CHAT (Overview) ==========
1192
+ with gr.TabItem("🤖 Agent Chat", id="chat"):
1193
+ gr.Markdown(
1194
+ "### 💬 Ask anything about drug targets, diseases, or pharma companies.\n"
1195
+ "The AI agent will understand your intent, search relevant data, and generate a structured report.",
1196
+ elem_classes="fade-in"
1197
+ )
1198
+
1199
+ with gr.Row(elem_classes="card"):
1200
+ # Chat layout
1201
+ with gr.Column(scale=3):
1202
+ chat_input = gr.Textbox(
1203
+ label="Your question",
1204
+ placeholder="e.g. \"Analyze EGFR as a drug target\" or \"What drugs target PD-L1?\" or \"Compare Roche and AstraZeneca oncology pipelines\"",
1205
+ lines=2,
1206
+ elem_classes="agent-input",
1207
+ )
1208
+ with gr.Row():
1209
+ chat_btn = gr.Button("🔍 Analyze", variant="primary", elem_classes="btn-primary")
1210
+ clear_btn = gr.Button("🗑️ Clear", variant="secondary", size="sm")
1211
+
1212
+ with gr.Column(scale=1):
1213
+ gr.Markdown("#### ⚡ Quick Examples")
1214
+ example_btns = []
1215
+ examples = [
1216
+ ("🎯 EGFR", "Analyze EGFR as a drug target — approved drugs, pipeline, and competitive landscape"),
1217
+ ("💊 PD-1 drugs", "What drugs target PD-1? Show me approved and pipeline drugs"),
1218
+ ("🏥 NSCLC", "Give me a disease overview for non-small cell lung cancer"),
1219
+ ("🏢 Roche", "Profile Roche's oncology pipeline and recent deals"),
1220
+ ("🧪 ALK trials", "What clinical trials are targeting ALK in NSCLC?"),
1221
+ ("🔥 HER2 ADC", "Show me HER2-targeting antibody-drug conjugates"),
1222
+ ]
1223
+ with gr.Row():
1224
+ for i, (label, prompt) in enumerate(examples[:3]):
1225
+ btn = gr.Button(label, size="sm", scale=1)
1226
+ example_btns.append((btn, prompt))
1227
+ with gr.Row():
1228
+ for i, (label, prompt) in enumerate(examples[3:]):
1229
+ btn = gr.Button(label, size="sm", scale=1)
1230
+ example_btns.append((btn, prompt))
1231
+
1232
+ # Agent thinking (collapsible)
1233
+ thinking_display = gr.HTML(
1234
+ value="<div class='thinking-steps'><div class='thinking-step'>🤖 Agent ready. Ask a question to begin.</div></div>",
1235
+ visible=True,
1236
+ )
1237
+
1238
+ # Report output
1239
+ report_display = gr.Markdown(
1240
+ value="### 👋 Welcome to PatSnap Pharma Intelligence\n\n"
1241
+ "I'm your AI agent for drug discovery intelligence. I can help with:\n\n"
1242
+ "- 🎯 **Target Intelligence** — Deep analysis of drug targets\n"
1243
+ "- 💊 **Drug Exploration** — Pipeline drugs by target, disease, or company\n"
1244
+ "- 🏥 **Disease Investigation** — Disease landscape & treatment overview\n"
1245
+ "- 🏢 **Company Profiling** — Pharma pipeline & strategic analysis\n"
1246
+ "- 🧪 **Clinical Trials** — Trial landscape by indication\n\n"
1247
+ "*Click a quick example above or type your question below.*",
1248
+ elem_classes="report",
1249
+ )
1250
+
1251
+ # Wire up agent chat
1252
+ async def handle_chat(query):
1253
+ thinking, report = await run_agent(query)
1254
+ thinking_html = f"<div class='thinking-steps'>{thinking}</div>"
1255
+ return thinking_html, report
1256
+
1257
+ chat_btn.click(
1258
+ fn=handle_chat,
1259
+ inputs=[chat_input],
1260
+ outputs=[thinking_display, report_display],
1261
+ )
1262
+
1263
+ for btn, prompt in example_btns:
1264
+ btn.click(
1265
+ fn=lambda p=prompt: p,
1266
+ outputs=[chat_input],
1267
+ ).then(
1268
+ fn=handle_chat,
1269
+ inputs=[chat_input],
1270
+ outputs=[thinking_display, report_display],
1271
+ )
1272
+
1273
+ clear_btn.click(
1274
+ fn=lambda: ("", "", "### 👋 Welcome to PatSnap Pharma Intelligence\n\n*Ready for your next query.*"),
1275
+ outputs=[chat_input, thinking_display, report_display],
1276
+ )
1277
+
1278
+ # ========== TAB 2-6: Structured Module Views ==========
1279
+ # These reuse the same agent but with module-specific context
1280
+ for mod_key, mod_info in MODULES.items():
1281
+ icon = mod_info["icon"]
1282
+ label = mod_info["label"]
1283
+ desc = mod_info["desc"]
1284
+
1285
+ with gr.TabItem(f"{icon} {label}", id=mod_key):
1286
+ gr.Markdown(f"### {icon} {label}\n{desc}", elem_classes="fade-in")
1287
+
1288
+ with gr.Row(elem_classes="card"):
1289
+ with gr.Column(scale=3):
1290
+ mod_input = gr.Textbox(
1291
+ label=f"Search {mod_info['entity']}",
1292
+ placeholder=f"Enter a {mod_info['entity']} name, e.g. EGFR, HER2, PD-L1...",
1293
+ lines=1,
1294
+ elem_classes="agent-input",
1295
+ )
1296
+ mod_btn = gr.Button(f"{icon} Search", variant="primary", elem_classes="btn-primary")
1297
+
1298
+ mod_thinking = gr.HTML(visible=True)
1299
+ mod_report = gr.Markdown(
1300
+ value=f"### {icon} {label}\n\n*Enter a {mod_info['entity']} above to generate a report.*",
1301
+ elem_classes="report",
1302
+ )
1303
+
1304
+ async def handle_module_search(q, mk=mod_key):
1305
+ if not q.strip():
1306
+ return "", f"### {MODULES[mk]['icon']} {MODULES[mk]['label']}\n\n*Please enter a search term.*"
1307
+ # Route to the appropriate module by prefixing the query
1308
+ routed_query = f"[{mk}] {q}"
1309
+ thinking, report = await run_agent(routed_query)
1310
+ thinking_html = f"<div class='thinking-steps'>{thinking}</div>"
1311
+ return thinking_html, report
1312
+
1313
+ mod_btn.click(
1314
+ fn=handle_module_search,
1315
+ inputs=[mod_input],
1316
+ outputs=[mod_thinking, mod_report],
1317
+ )
1318
+
1319
+ # Pre-made reports
1320
+ if mod_key == "target":
1321
+ gr.Markdown("#### 📊 Featured Target Reports")
1322
+ with gr.Row():
1323
+ for target_name in ["EGFR", "HER2", "PD-L1"]:
1324
+ td = MOCK_TARGETS.get(target_name)
1325
+ if td:
1326
+ with gr.Column(scale=1):
1327
+ with gr.Group(elem_classes="card"):
1328
+ gr.Markdown(f"#### {target_name}\n{td.get('approved_drugs', [{}])[0].get('name', '')} ...")
1329
+ btn = gr.Button(f"View {target_name} Report →", size="sm")
1330
+ def show_report(tn=target_name):
1331
+ td = MOCK_TARGETS.get(tn)
1332
+ if td:
1333
+ return build_target_report(td)
1334
+ return "Report not available."
1335
+ btn.click(fn=show_report, outputs=[mod_report])
1336
+
1337
+ elif mod_key == "company":
1338
+ gr.Markdown("#### 📊 Featured Company Reports")
1339
+ with gr.Row():
1340
+ for cname in ["Roche", "AstraZeneca"]:
1341
+ cd = MOCK_COMPANIES.get(cname)
1342
+ if cd:
1343
+ with gr.Column(scale=1):
1344
+ with gr.Group(elem_classes="card"):
1345
+ gr.Markdown(f"#### {cname}\n{cd.get('headquarters', '')} — {cd.get('2024_revenue', '')}")
1346
+ btn = gr.Button(f"View {cname} Report →", size="sm")
1347
+ def show_crpt(cn=cname):
1348
+ cd = MOCK_COMPANIES.get(cn)
1349
+ if cd:
1350
+ return build_company_report(cd)
1351
+ return "Report not available."
1352
+ btn.click(fn=show_crpt, outputs=[mod_report])
1353
+
1354
+ elif mod_key == "disease":
1355
+ gr.Markdown("#### 📊 Featured Disease Reports")
1356
+ with gr.Row():
1357
+ for dname in ["NSCLC", "Breast Cancer"]:
1358
+ dd = MOCK_DISEASES.get(dname)
1359
+ if dd:
1360
+ with gr.Column(scale=1):
1361
+ with gr.Group(elem_classes="card"):
1362
+ gr.Markdown(f"#### {dname}\n{dd.get('global_incidence', '')}")
1363
+ btn = gr.Button(f"View {dname} Report →", size="sm")
1364
+ def show_drpt(dn=dname):
1365
+ dd = MOCK_DISEASES.get(dn)
1366
+ if dd:
1367
+ return build_disease_report(dd)
1368
+ return "Report not available."
1369
+ btn.click(fn=show_drpt, outputs=[mod_report])
1370
+
1371
+ # ===== FOOTER =====
1372
+ create_footer()
1373
+
1374
+ return app
1375
+
1376
+
1377
+ # =============================================================================
1378
+ # ENTRY POINT
1379
+ # =============================================================================
1380
+
1381
+ if __name__ == "__main__":
1382
+ app = build_app()
1383
+ app.queue(default_concurrency_limit=3, max_size=20)
1384
+ app.launch(
1385
+ server_name="0.0.0.0",
1386
+ server_port=7860,
1387
+ show_error=True,
1388
+ css=CUSTOM_CSS,
1389
+ theme=gr.themes.Soft(
1390
+ primary_hue="blue",
1391
+ secondary_hue="emerald",
1392
+ neutral_hue="slate",
1393
+ ),
1394
+ )
fetch.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PatSnap MCP 数据采集脚本 — 在你本地 VS Code 里跑
3
+ 用法:
4
+ 1. 把你的 API Key 设成环境变量: export PATSNAP_KEY="sk-xxx"
5
+ 2. python fetch.py
6
+ 3. 把生成的 real_data.json 内容发给我
7
+
8
+ 需要先装依赖: pip install mcp
9
+ """
10
+ import json
11
+ import asyncio
12
+ import os
13
+ import sys
14
+ from mcp import ClientSession
15
+ from mcp.client.streamable_http import streamablehttp_client
16
+
17
+ # 从命令行参数或环境变量获取 key
18
+ if len(sys.argv) > 1:
19
+ API_KEY = sys.argv[1]
20
+ elif os.getenv("PATSNAP_KEY", ""):
21
+ API_KEY = os.getenv("PATSNAP_KEY", "")
22
+ else:
23
+ print("❌ 用法: python fetch.py YOUR_API_KEY")
24
+ print(" 或: set PATSNAP_KEY=你的key && python fetch.py")
25
+ sys.exit(1)
26
+
27
+ SERVER_URL = f"https://connect.patsnap.com/096456/Logic-mcp?apikey={API_KEY}"
28
+
29
+ # ============ 要查的内容 ============
30
+ # 每个查询定义: (标签, 工具名, 参数)
31
+ QUERIES = [
32
+ # --- Pharma Intelligence: 药物搜索 ---
33
+ ("EGFR_drugs", "ls_drug_search", {"target": ["EGFR"], "limit": 15}),
34
+ ("PD1_drugs", "ls_drug_search", {"target": ["PD-1"], "limit": 15}),
35
+ ("NSCLC_drugs", "ls_drug_search", {"disease": ["non-small cell lung cancer"], "limit": 15}),
36
+ ("bispecific_drugs", "ls_drug_search", {"drug_type": ["Bispecific antibody"], "limit": 10}),
37
+
38
+ # --- 如果还有可用工具,试试这些 (按需取消注释) ---
39
+ # ("EGFR_target_info", "ls_target_search", {"target": ["EGFR"], "limit": 5}),
40
+ # ("PDL1_clinical_trials", "ls_clinical_trial_search", {"target": ["PD-L1"], "limit": 10}),
41
+ ]
42
+
43
+ # ============ 执行 ============
44
+ async def fetch_one(label, tool, args):
45
+ print(f"\n{'='*60}")
46
+ print(f"🔍 {label}")
47
+ print(f" Tool: {tool}")
48
+ print(f" Args: {json.dumps(args)}")
49
+
50
+ async with streamablehttp_client(SERVER_URL, timeout=60, sse_read_timeout=60) as (read, write, _):
51
+ async with ClientSession(read, write) as session:
52
+ await session.initialize()
53
+
54
+ # 先看看有哪些工具可用
55
+ tools_result = await session.list_tools()
56
+ tool_names = [t.name for t in tools_result.tools]
57
+ print(f" Available tools: {tool_names}")
58
+
59
+ if tool not in tool_names:
60
+ print(f" ⚠️ Tool '{tool}' not found, skipping")
61
+ return None
62
+
63
+ result = await session.call_tool(tool, arguments=args)
64
+ if result.content:
65
+ text = result.content[0].text
66
+ data = json.loads(text) if isinstance(text, str) else text
67
+ total = data.get("total", "?")
68
+ items = len(data.get("items", []))
69
+ print(f" ✅ {items} items / {total} total")
70
+ return data
71
+ return None
72
+
73
+
74
+ async def main():
75
+ results = {}
76
+ for label, tool, args in QUERIES:
77
+ try:
78
+ data = await fetch_one(label, tool, args)
79
+ if data:
80
+ results[label] = data
81
+ except Exception as e:
82
+ print(f" ❌ Error: {e}")
83
+
84
+ # 保存
85
+ if results:
86
+ out_path = "real_data.json"
87
+ with open(out_path, "w", encoding="utf-8") as f:
88
+ json.dump(results, f, indent=2, ensure_ascii=False)
89
+ print(f"\n{'='*60}")
90
+ print(f"✅ 成功获取 {len(results)}/{len(QUERIES)} 个查询结果")
91
+ print(f"📁 已保存到: {out_path}")
92
+ print(f"📏 文件大小: {os.path.getsize(out_path)} bytes")
93
+ print(f"\n把 real_data.json 文件内容发给青崖即可!")
94
+ else:
95
+ print("\n❌ 没有任何结果,检查 API Key 或网络")
96
+
97
+ if __name__ == "__main__":
98
+ asyncio.run(main())
fetch_real_data.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """抓取 PatSnap MCP 真实数据,替换 HF Space demo 的 mock"""
2
+ import json
3
+ import asyncio
4
+ import sys
5
+ from mcp import ClientSession
6
+ from mcp.client.streamable_http import streamablehttp_client
7
+
8
+ API_KEY = sys.argv[1] if len(sys.argv) > 1 else ""
9
+ SERVER_URL = f"https://connect.patsnap.com/096456/Logic-mcp?apikey={API_KEY}"
10
+
11
+ QUERIES = [
12
+ ("EGFR", {"target": ["EGFR"], "limit": 10}),
13
+ ("PD-1", {"target": ["PD-1"], "limit": 10}),
14
+ ("NSCLC", {"disease": ["non-small cell lung cancer"], "limit": 10}),
15
+ ]
16
+
17
+ async def fetch(label, args):
18
+ print(f"\n🔍 Fetching: {label} ...")
19
+ async with streamablehttp_client(SERVER_URL, timeout=30, sse_read_timeout=30) as (r, w, _):
20
+ async with ClientSession(r, w) as sess:
21
+ await sess.initialize()
22
+ res = await sess.call_tool("ls_drug_search", arguments=args)
23
+ if res.content:
24
+ txt = res.content[0].text
25
+ data = json.loads(txt) if isinstance(txt, str) else txt
26
+ print(f" ✅ Got {data.get('total', 0)} records, {len(data.get('items', []))} items")
27
+ return data
28
+ return None
29
+
30
+ async def main():
31
+ if not API_KEY:
32
+ print("❌ No API key. Usage: python fetch_real_data.py YOUR_KEY")
33
+ return
34
+
35
+ results = {}
36
+ for label, args in QUERIES:
37
+ try:
38
+ data = await fetch(label, args)
39
+ if data:
40
+ results[label] = data
41
+ except Exception as e:
42
+ print(f" ❌ Failed: {e}")
43
+
44
+ if results:
45
+ with open("real_data.json", "w") as f:
46
+ json.dump(results, f, indent=2, ensure_ascii=False)
47
+ print(f"\n✅ Saved {len(results)} result sets to real_data.json")
48
+ else:
49
+ print("\n❌ No data fetched.")
50
+
51
+ asyncio.run(main())
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=4.0.0
2
+ mcp