singhankit16 commited on
Commit
f103ad7
·
1 Parent(s): ead547a

Deploy BioMCP Explorer

Browse files
README.md CHANGED
@@ -1,14 +1,77 @@
1
  ---
2
  title: BioMCP Explorer
3
- emoji: 🏃
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.11.0
8
  app_file: app.py
9
  pinned: false
10
- license: mit
11
- short_description: Exploring BioMCP using 30+ Biomedical API's
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: BioMCP Explorer
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: "6.11.0"
8
  app_file: app.py
9
  pinned: false
10
+ short_description: Explore genes, variants, drugs, trials & more
 
11
  ---
12
 
13
+ # BioMCP Explorer Gradio App
14
+
15
+ A feature-rich Gradio web interface for exploring all BioMCP tools and features.
16
+ Search, discover, and analyze biomedical data across 13+ entity types and 30+ upstream APIs.
17
+
18
+ ## Quick Start
19
+
20
+ ### 1. Install BioMCP CLI
21
+
22
+ ```bash
23
+ uv tool install biomcp-cli
24
+ # or: pip install biomcp-cli
25
+ ```
26
+
27
+ ### 2. Install Python dependencies
28
+
29
+ ```bash
30
+ pip install -r requirements.txt
31
+ ```
32
+
33
+ ### 3. Configure API keys (optional)
34
+
35
+ Copy `.env.example` to `.env` and fill in your keys. Or enter them in the Settings tab at runtime.
36
+
37
+ ### 4. Launch
38
+
39
+ ```bash
40
+ python app.py
41
+ ```
42
+
43
+ Open http://localhost:7860 in your browser.
44
+
45
+ ## Tabs Overview
46
+
47
+ | Tab | Purpose |
48
+ |-----|---------|
49
+ | ⚙️ Settings & Health | API keys, health checks, version info |
50
+ | 🔍 Discover | Free-text concept resolution |
51
+ | 🔎 Search | Entity search across 13 types (gene, variant, article, trial, drug, disease, pathway, protein, adverse-event, pgx, gwas, phenotype, cross-entity) |
52
+ | 📋 Get Detail | Focused entity detail with selectable sections |
53
+ | 🔗 Cross-Entity Helpers | 20 pivot commands between related entities |
54
+ | 🧬 Enrichment | g:Profiler gene-set enrichment |
55
+ | 📦 Batch | Parallel get calls for up to 10 IDs |
56
+ | 📊 Study Analytics | Local cBioPortal study analysis (query, cohort, survival, compare, co-occurrence) |
57
+
58
+ ## API Keys
59
+
60
+ All optional — BioMCP works without them at reduced rate/features:
61
+
62
+ | Key | Purpose |
63
+ |-----|---------|
64
+ | `NCBI_API_KEY` | Better PubMed/PubTator rate limits |
65
+ | `S2_API_KEY` | Faster Semantic Scholar access |
66
+ | `ONCOKB_TOKEN` | OncoKB variant therapy evidence |
67
+ | `OPENFDA_API_KEY` | Better OpenFDA rate limits |
68
+ | `NCI_API_KEY` | NCI CTS trial search |
69
+ | `DISGENET_API_KEY` | DisGeNET gene-disease scores |
70
+ | `UMLS_API_KEY` | Clinical crosswalk in discover |
71
+ | `ALPHAGENOME_API_KEY` | Variant effect predictions |
72
+
73
+ ## Requirements
74
+
75
+ - Python 3.11+
76
+ - `biomcp` CLI on PATH
77
+ - Gradio 5.x
app.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BioMCP Gradio App — Main entry point.
3
+ Assembles all tabs into one cohesive application.
4
+ """
5
+
6
+ import gradio as gr
7
+ from core.config import load_env_keys
8
+ from tabs.settings import create_settings_tab
9
+ from tabs.discover import create_discover_tab
10
+ from tabs.search import create_search_tab
11
+ from tabs.get_detail import create_get_tab
12
+ from tabs.helpers import create_helpers_tab
13
+ from tabs.enrichment import create_enrichment_tab
14
+ from tabs.batch import create_batch_tab
15
+
16
+ DESCRIPTION = """
17
+ # 🧬 BioMCP Explorer
18
+
19
+ **One interface. Every biomedical entity. Evidence from the sources you trust.**
20
+
21
+ Explore genes, variants, articles, trials, drugs, diseases, pathways, proteins, adverse events, PGx, GWAS, and phenotypes
22
+ — all powered by [BioMCP](https://biomcp.org/) across 30+ upstream APIs.
23
+
24
+ > **Tip:** Start with the **⚙️ Settings** tab to configure API keys, then explore any entity tab.
25
+ """
26
+
27
+
28
+ def create_app():
29
+ with gr.Blocks(title="BioMCP Explorer") as app:
30
+ gr.Markdown(DESCRIPTION)
31
+
32
+ # Session state lives outside tabs so every tab can share it
33
+ session_keys = gr.State(load_env_keys())
34
+
35
+ with gr.Tabs():
36
+ # Settings tab first — pass session_keys in
37
+ create_settings_tab(session_keys)
38
+
39
+ # All feature tabs, sharing the same session_keys
40
+ create_discover_tab(session_keys)
41
+ create_search_tab(session_keys)
42
+ create_get_tab(session_keys)
43
+ create_helpers_tab(session_keys)
44
+ create_enrichment_tab(session_keys)
45
+ create_batch_tab(session_keys)
46
+
47
+ return app
48
+
49
+
50
+ if __name__ == "__main__":
51
+ import os
52
+ is_hf = os.environ.get("SPACE_ID") is not None
53
+ app = create_app()
54
+ app.launch(
55
+ server_name="0.0.0.0" if is_hf else "127.0.0.1",
56
+ server_port=7860 if is_hf else 7865,
57
+ share=False,
58
+ show_error=True,
59
+ theme=gr.themes.Soft(
60
+ primary_hue="blue",
61
+ secondary_hue="cyan",
62
+ neutral_hue="slate",
63
+ ),
64
+ css="""
65
+ .gradio-container { max-width: 1400px !important; }
66
+ footer { display: none !important; }
67
+ """,
68
+ )
core/__init__.py ADDED
File without changes
core/config.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API key and environment configuration management.
3
+ Keys are stored in session state only — never written to disk.
4
+ """
5
+
6
+ import os
7
+ from dotenv import load_dotenv
8
+
9
+ # All supported API keys with metadata
10
+ API_KEYS = {
11
+ "NCBI_API_KEY": {
12
+ "label": "NCBI API Key",
13
+ "description": "Improves PubTator/PMC OA rate limits (3→10 req/sec)",
14
+ "url": "https://www.ncbi.nlm.nih.gov/account/settings/",
15
+ "required": False,
16
+ },
17
+ "S2_API_KEY": {
18
+ "label": "Semantic Scholar API Key",
19
+ "description": "Dedicated S2 quota (1 req/sec) for article search/TLDR/citations",
20
+ "url": "https://www.semanticscholar.org/product/api",
21
+ "required": False,
22
+ },
23
+ "ONCOKB_TOKEN": {
24
+ "label": "OncoKB Token",
25
+ "description": "Production OncoKB variant therapy & level evidence",
26
+ "url": "https://www.oncokb.org/account/register",
27
+ "required": False,
28
+ },
29
+ "OPENFDA_API_KEY": {
30
+ "label": "OpenFDA API Key",
31
+ "description": "Better OpenFDA rate limits for drug safety/adverse event lookups",
32
+ "url": "https://open.fda.gov/apis/authentication/",
33
+ "required": False,
34
+ },
35
+ "NCI_API_KEY": {
36
+ "label": "NCI CTS API Key",
37
+ "description": "NCI Clinical Trials Search (--source nci)",
38
+ "url": "https://clinicaltrialsapi.cancer.gov/",
39
+ "required": False,
40
+ },
41
+ "DISGENET_API_KEY": {
42
+ "label": "DisGeNET API Key",
43
+ "description": "Scored gene-disease associations in gene/disease lookups",
44
+ "url": "https://www.disgenet.com/",
45
+ "required": False,
46
+ },
47
+ "UMLS_API_KEY": {
48
+ "label": "UMLS API Key",
49
+ "description": "Clinical crosswalk enrichment in discover command",
50
+ "url": "https://uts.nlm.nih.gov/uts/signup-login",
51
+ "required": False,
52
+ },
53
+ "ALPHAGENOME_API_KEY": {
54
+ "label": "AlphaGenome API Key",
55
+ "description": "Variant effect prediction (get variant ... predict)",
56
+ "url": "https://deepmind.google/science/alphagenome/",
57
+ "required": False,
58
+ },
59
+ }
60
+
61
+ # Additional config env vars
62
+ CONFIG_VARS = {
63
+ "BIOMCP_STUDY_DIR": {
64
+ "label": "Study Directory",
65
+ "description": "Local study root for cBioPortal datasets (leave blank for default)",
66
+ },
67
+ }
68
+
69
+
70
+ def load_env_keys() -> dict[str, str]:
71
+ """Load API keys from .env file and environment, return as dict."""
72
+ load_dotenv()
73
+ keys = {}
74
+ for key_name in API_KEYS:
75
+ keys[key_name] = os.environ.get(key_name, "")
76
+ for key_name in CONFIG_VARS:
77
+ keys[key_name] = os.environ.get(key_name, "")
78
+ return keys
79
+
80
+
81
+ def build_env_overrides(session_keys: dict[str, str]) -> dict[str, str]:
82
+ """
83
+ Build env overrides dict from session keys.
84
+ Only includes non-empty values.
85
+ """
86
+ return {k: v for k, v in session_keys.items() if v}
core/formatter.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Formatting helpers: convert biomcp output into Gradio-friendly display.
3
+ """
4
+
5
+ import json
6
+ import re
7
+
8
+
9
+ def _strip_suggested_commands(md: str) -> str:
10
+ """Remove the '## Suggested Commands' section from markdown output."""
11
+ return re.split(r"\n##\s+Suggested Commands", md, maxsplit=1)[0].rstrip()
12
+
13
+
14
+ def format_result(result: dict) -> tuple[str, str]:
15
+ """
16
+ Format a runner result into (markdown_display, json_display).
17
+ Returns two strings suitable for gr.Markdown and gr.Code components.
18
+ """
19
+ if not result["success"]:
20
+ error_md = f"⚠️ **Error:** {result['error']}"
21
+ return error_md, json.dumps({"error": result["error"]}, indent=2)
22
+
23
+ # Markdown display
24
+ md = _strip_suggested_commands(result["markdown"])
25
+
26
+ # JSON display
27
+ if result["data"] is not None:
28
+ json_str = json.dumps(result["data"], indent=2, default=str)
29
+ else:
30
+ json_str = ""
31
+
32
+ return md, json_str
core/runner.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Subprocess wrapper for the biomcp CLI.
3
+ Runs biomcp commands and returns structured (JSON) or markdown output.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import shutil
10
+ import sys
11
+ from typing import Optional
12
+
13
+
14
+ def find_biomcp() -> str:
15
+ """Locate the biomcp binary on PATH or common install locations."""
16
+ path = shutil.which("biomcp")
17
+ if path:
18
+ return path
19
+
20
+ # Check Python Scripts directory (pip install location)
21
+ candidates = [
22
+ os.path.join(os.path.dirname(sys.executable), "Scripts", "biomcp.exe"),
23
+ os.path.join(os.path.dirname(sys.executable), "Scripts", "biomcp"),
24
+ os.path.join(os.path.dirname(sys.executable), "biomcp.exe"),
25
+ os.path.join(os.path.dirname(sys.executable), "biomcp"),
26
+ ]
27
+ # Also check the Python that `py` launcher uses
28
+ for base in [
29
+ os.path.expanduser("~/.local/bin"),
30
+ os.path.expandvars(r"%LOCALAPPDATA%\Python\pythoncore-3.14-64\Scripts"),
31
+ os.path.expandvars(r"%LOCALAPPDATA%\Python\pythoncore-3.13-64\Scripts"),
32
+ os.path.expandvars(r"%LOCALAPPDATA%\Python\pythoncore-3.12-64\Scripts"),
33
+ ]:
34
+ candidates.append(os.path.join(base, "biomcp.exe"))
35
+ candidates.append(os.path.join(base, "biomcp"))
36
+
37
+ for c in candidates:
38
+ if os.path.isfile(c):
39
+ return c
40
+
41
+ raise FileNotFoundError(
42
+ "biomcp not found on PATH. Install with: pip install biomcp-cli"
43
+ )
44
+
45
+
46
+ def run(
47
+ args: list[str],
48
+ *,
49
+ json_mode: bool = True,
50
+ no_cache: bool = False,
51
+ env_overrides: Optional[dict[str, str]] = None,
52
+ timeout: int = 120,
53
+ ) -> dict:
54
+ """
55
+ Execute a biomcp CLI command.
56
+
57
+ Returns dict with keys:
58
+ - success: bool
59
+ - command: str (the full command string)
60
+ - markdown: str (raw stdout, always present)
61
+ - data: dict | list | None (parsed JSON when json_mode=True)
62
+ - error: str | None
63
+ """
64
+ import os
65
+
66
+ biomcp = find_biomcp()
67
+
68
+ cmd = [biomcp]
69
+ if json_mode:
70
+ cmd.append("--json")
71
+ if no_cache:
72
+ cmd.append("--no-cache")
73
+ cmd.extend(args)
74
+
75
+ env = os.environ.copy()
76
+ if env_overrides:
77
+ for k, v in env_overrides.items():
78
+ if v: # only set non-empty values
79
+ env[k] = v
80
+
81
+ command_str = " ".join(cmd)
82
+
83
+ try:
84
+ result = subprocess.run(
85
+ cmd,
86
+ capture_output=True,
87
+ text=True,
88
+ timeout=timeout,
89
+ env=env,
90
+ )
91
+
92
+ stdout = result.stdout.strip()
93
+ stderr = result.stderr.strip()
94
+
95
+ if result.returncode != 0:
96
+ error_msg = stderr or stdout or f"Command exited with code {result.returncode}"
97
+ return {
98
+ "success": False,
99
+ "command": command_str,
100
+ "markdown": error_msg,
101
+ "data": None,
102
+ "error": error_msg,
103
+ }
104
+
105
+ # Try parsing JSON
106
+ data = None
107
+ if json_mode and stdout:
108
+ try:
109
+ data = json.loads(stdout)
110
+ except json.JSONDecodeError:
111
+ pass
112
+
113
+ # For markdown display: if we got JSON, pretty-print it; otherwise use raw stdout
114
+ if data is not None:
115
+ markdown = _json_to_markdown(data)
116
+ else:
117
+ markdown = stdout
118
+
119
+ return {
120
+ "success": True,
121
+ "command": command_str,
122
+ "markdown": markdown,
123
+ "data": data,
124
+ "error": None,
125
+ }
126
+
127
+ except subprocess.TimeoutExpired:
128
+ return {
129
+ "success": False,
130
+ "command": command_str,
131
+ "markdown": "",
132
+ "data": None,
133
+ "error": f"Command timed out after {timeout}s",
134
+ }
135
+ except FileNotFoundError:
136
+ return {
137
+ "success": False,
138
+ "command": command_str,
139
+ "markdown": "",
140
+ "data": None,
141
+ "error": "biomcp binary not found. Install with: uv tool install biomcp-cli",
142
+ }
143
+ except Exception as e:
144
+ return {
145
+ "success": False,
146
+ "command": command_str,
147
+ "markdown": "",
148
+ "data": None,
149
+ "error": str(e),
150
+ }
151
+
152
+
153
+ def run_markdown(
154
+ args: list[str],
155
+ *,
156
+ no_cache: bool = False,
157
+ env_overrides: Optional[dict[str, str]] = None,
158
+ timeout: int = 120,
159
+ ) -> dict:
160
+ """Run command in markdown mode (no --json flag)."""
161
+ return run(
162
+ args,
163
+ json_mode=False,
164
+ no_cache=no_cache,
165
+ env_overrides=env_overrides,
166
+ timeout=timeout,
167
+ )
168
+
169
+
170
+ def _is_small_dict(d: dict) -> bool:
171
+ """Check if a dict is simple enough to render inline."""
172
+ return (len(d) <= 4
173
+ and all(isinstance(v, (str, int, float, bool, type(None))) for v in d.values()))
174
+
175
+
176
+ def _json_to_markdown(data, level=1) -> str:
177
+ """Convert structured JSON response to readable markdown."""
178
+ if isinstance(data, str):
179
+ return data
180
+ if isinstance(data, (int, float, bool)):
181
+ return str(data)
182
+ if data is None:
183
+ return ""
184
+ if isinstance(data, list):
185
+ if not data:
186
+ return ""
187
+ # List of simple values
188
+ if all(isinstance(v, (str, int, float, bool)) for v in data):
189
+ return ", ".join(str(v) for v in data)
190
+ # List of small dicts — render as compact table-like rows
191
+ if all(isinstance(v, dict) and _is_small_dict(v) for v in data):
192
+ rows = []
193
+ for item in data[:15]:
194
+ parts = [f"{v}" for v in item.values() if v is not None and v != ""]
195
+ rows.append(" · ".join(parts))
196
+ result = "\n".join(f"- {r}" for r in rows)
197
+ if len(data) > 15:
198
+ result += f"\n- *(+{len(data) - 15} more)*"
199
+ return result
200
+ # List of larger dicts — render each as a block
201
+ parts = []
202
+ for i, item in enumerate(data, 1):
203
+ if isinstance(item, dict):
204
+ label = (item.get("label") or item.get("name") or item.get("title")
205
+ or item.get("primary_id") or item.get("id") or f"Item {i}")
206
+ hdr = "#" * min(level + 1, 5)
207
+ parts.append(f"{hdr} {i}. {label}")
208
+ parts.append(_format_dict(item, level + 1))
209
+ else:
210
+ parts.append(f"- {item}")
211
+ return "\n\n".join(parts)
212
+ if isinstance(data, dict):
213
+ return _format_dict(data, level)
214
+ return str(data)
215
+
216
+
217
+ def _format_dict(d: dict, level: int = 1) -> str:
218
+ """Format a dict as readable markdown key-value pairs."""
219
+ lines = []
220
+ skip = {"_meta", "_links"}
221
+ for key, value in d.items():
222
+ if key in skip or key.startswith("_"):
223
+ continue
224
+ nice_key = key.replace("_", " ").replace("-", " ").title()
225
+ if value is None or value == "" or value == []:
226
+ continue
227
+ if isinstance(value, (str, int, float, bool)):
228
+ lines.append(f"**{nice_key}:** {value}")
229
+ elif isinstance(value, list):
230
+ if all(isinstance(v, (str, int, float)) for v in value):
231
+ shown = ", ".join(str(v) for v in value[:8])
232
+ more = f" *(+{len(value) - 8} more)*" if len(value) > 8 else ""
233
+ lines.append(f"**{nice_key}:** {shown}{more}")
234
+ elif all(isinstance(v, dict) and _is_small_dict(v) for v in value):
235
+ hdr = "#" * min(level + 1, 5)
236
+ lines.append(f"\n{hdr} {nice_key}\n")
237
+ lines.append(_json_to_markdown(value, level + 1))
238
+ else:
239
+ hdr = "#" * min(level + 1, 5)
240
+ lines.append(f"\n{hdr} {nice_key}\n")
241
+ lines.append(_json_to_markdown(value, level + 1))
242
+ elif isinstance(value, dict):
243
+ hdr = "#" * min(level + 1, 5)
244
+ lines.append(f"\n{hdr} {nice_key}\n")
245
+ lines.append(_format_dict(value, level + 1))
246
+ return "\n\n".join(lines)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=5.0
2
+ python-dotenv
tabs/__init__.py ADDED
File without changes
tabs/batch.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Batch tab — parallel get calls for up to 10 IDs.
3
+ """
4
+
5
+ import gradio as gr
6
+ from core import config, runner
7
+ from core.formatter import format_result
8
+
9
+ BATCH_ENTITIES = [
10
+ "gene", "variant", "article", "trial", "drug",
11
+ "disease", "pathway", "protein", "adverse-event", "pgx",
12
+ ]
13
+
14
+
15
+ def create_batch_tab(session_keys):
16
+ """Build the Batch tab."""
17
+
18
+ with gr.Tab("📦 Batch"):
19
+ gr.Markdown(
20
+ "## Batch Mode\n"
21
+ "Run parallel `get` calls for up to 10 entity IDs in one command.\n"
22
+ "Enter comma-separated IDs."
23
+ )
24
+
25
+ with gr.Row():
26
+ entity = gr.Dropdown(
27
+ choices=BATCH_ENTITIES,
28
+ value="gene",
29
+ label="Entity Type",
30
+ scale=1,
31
+ )
32
+ ids = gr.Textbox(
33
+ label="IDs (comma-separated, max 10)",
34
+ placeholder="e.g., BRAF,TP53 or NCT02576665,NCT03715933",
35
+ scale=3,
36
+ )
37
+
38
+ with gr.Row():
39
+ sections = gr.Textbox(
40
+ label="Sections (comma-separated, optional)",
41
+ placeholder="e.g., pathways,interactions",
42
+ )
43
+ source = gr.Textbox(
44
+ label="Source (optional)",
45
+ placeholder="e.g., nci",
46
+ )
47
+
48
+ with gr.Row():
49
+ no_cache = gr.Checkbox(label="Bypass cache", value=False)
50
+
51
+ # Quick examples
52
+ gr.Markdown("**Quick examples:**")
53
+ with gr.Row():
54
+ ex1 = gr.Button("Genes: BRAF,TP53", size="sm", variant="secondary")
55
+ ex2 = gr.Button("Trials: NCT02576665,NCT03715933", size="sm", variant="secondary")
56
+ ex3 = gr.Button("Variants: BRAF V600E, KRAS G12D", size="sm", variant="secondary")
57
+
58
+ ex1.click(fn=lambda: ("gene", "BRAF,TP53"), outputs=[entity, ids])
59
+ ex2.click(fn=lambda: ("trial", "NCT02576665,NCT03715933"), outputs=[entity, ids])
60
+ ex3.click(fn=lambda: ("variant", "BRAF V600E,KRAS G12D"), outputs=[entity, ids])
61
+
62
+ run_btn = gr.Button("📦 Run Batch", variant="primary")
63
+ output_md = gr.Markdown(label="Results")
64
+ with gr.Accordion("Raw JSON", open=False):
65
+ output_json = gr.Code(language="json")
66
+
67
+ def run_batch(ent, id_str, secs, src, skip_cache, keys):
68
+ if not id_str.strip():
69
+ raise gr.Error("Please enter at least one ID (comma-separated, max 10).")
70
+
71
+ args = ["batch", ent, id_str.strip()]
72
+ if secs.strip():
73
+ args.extend(["--sections", secs.strip()])
74
+ if src.strip():
75
+ args.extend(["--source", src.strip()])
76
+
77
+ env = config.build_env_overrides(keys)
78
+ result = runner.run(args, json_mode=True, no_cache=skip_cache, env_overrides=env)
79
+ if not result["success"]:
80
+ raise gr.Error(f"BioMCP error: {result['error']}")
81
+ md, js = format_result(result)
82
+ return md, js
83
+
84
+ run_btn.click(
85
+ fn=run_batch,
86
+ inputs=[entity, ids, sections, source, no_cache, session_keys],
87
+ outputs=[output_md, output_json],
88
+ )
tabs/discover.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Discover tab — free-text concept resolution.
3
+ """
4
+
5
+ import gradio as gr
6
+ from core import config, runner
7
+ from core.formatter import format_result
8
+
9
+
10
+ def create_discover_tab(session_keys):
11
+ """Build the Discover tab."""
12
+
13
+ with gr.Tab("🔍 Discover"):
14
+ gr.Markdown(
15
+ "## Concept Discovery\n"
16
+ "Start with free text — BioMCP resolves concepts by type and suggests follow-up commands.\n"
17
+ "Use this when you don't know which entity type to search."
18
+ )
19
+
20
+ with gr.Row():
21
+ with gr.Column(scale=3):
22
+ query = gr.Textbox(
23
+ label="Query",
24
+ placeholder='e.g., "ERBB1", "chest pain", "diabetes", "Keytruda"',
25
+ lines=1,
26
+ )
27
+ with gr.Column(scale=1):
28
+ no_cache = gr.Checkbox(label="Bypass cache", value=False)
29
+
30
+ run_btn = gr.Button("🔍 Discover", variant="primary")
31
+
32
+ # Quick examples
33
+ gr.Markdown("**Quick examples:**")
34
+ with gr.Row():
35
+ ex1 = gr.Button("ERBB1", size="sm", variant="secondary")
36
+ ex2 = gr.Button("chest pain", size="sm", variant="secondary")
37
+ ex3 = gr.Button("diabetes", size="sm", variant="secondary")
38
+ ex4 = gr.Button("Keytruda", size="sm", variant="secondary")
39
+ ex5 = gr.Button("BRAF V600E", size="sm", variant="secondary")
40
+
41
+ output_md = gr.Markdown(label="Results")
42
+ with gr.Accordion("Raw JSON", open=False):
43
+ output_json = gr.Code(language="json")
44
+
45
+ def run_discover(q, skip_cache, keys):
46
+ if not q.strip():
47
+ raise gr.Error("Please enter a query to discover.")
48
+ args = ["discover", q.strip()]
49
+ env = config.build_env_overrides(keys)
50
+ result = runner.run(args, json_mode=True, no_cache=skip_cache, env_overrides=env)
51
+ if not result["success"]:
52
+ raise gr.Error(f"BioMCP error: {result['error']}")
53
+ md, js = format_result(result)
54
+ return md, js
55
+
56
+ run_btn.click(
57
+ fn=run_discover,
58
+ inputs=[query, no_cache, session_keys],
59
+ outputs=[output_md, output_json],
60
+ )
61
+
62
+ # Wire example buttons
63
+ for btn, text in [(ex1, "ERBB1"), (ex2, "chest pain"), (ex3, "diabetes"),
64
+ (ex4, "Keytruda"), (ex5, "BRAF V600E")]:
65
+ btn.click(fn=lambda t=text: t, outputs=[query])
tabs/enrichment.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Enrichment tab — gene-set enrichment via g:Profiler.
3
+ """
4
+
5
+ import gradio as gr
6
+ from core import config, runner
7
+ from core.formatter import format_result
8
+
9
+
10
+ def create_enrichment_tab(session_keys):
11
+ """Build the Enrichment tab."""
12
+
13
+ with gr.Tab("🧬 Enrichment"):
14
+ gr.Markdown(
15
+ "## Gene-Set Enrichment\n"
16
+ "Run g:Profiler gene-set enrichment analysis.\n"
17
+ "Enter a comma-separated list of gene symbols."
18
+ )
19
+
20
+ genes = gr.Textbox(
21
+ label="Gene List (comma-separated)",
22
+ placeholder="e.g., BRAF,KRAS,NRAS,EGFR,PIK3CA",
23
+ lines=2,
24
+ )
25
+
26
+ with gr.Row():
27
+ limit = gr.Number(label="Limit", value=10, minimum=1, maximum=100)
28
+ no_cache = gr.Checkbox(label="Bypass cache", value=False)
29
+
30
+ # Quick examples
31
+ gr.Markdown("**Quick examples:**")
32
+ with gr.Row():
33
+ ex1 = gr.Button("BRAF,KRAS,NRAS", size="sm", variant="secondary")
34
+ ex2 = gr.Button("TP53,BRCA1,BRCA2,ATM,CHEK2", size="sm", variant="secondary")
35
+ ex3 = gr.Button("EGFR,ERBB2,ERBB3,ERBB4", size="sm", variant="secondary")
36
+
37
+ ex1.click(fn=lambda: "BRAF,KRAS,NRAS", outputs=[genes])
38
+ ex2.click(fn=lambda: "TP53,BRCA1,BRCA2,ATM,CHEK2", outputs=[genes])
39
+ ex3.click(fn=lambda: "EGFR,ERBB2,ERBB3,ERBB4", outputs=[genes])
40
+
41
+ run_btn = gr.Button("🧬 Run Enrichment", variant="primary")
42
+ output_md = gr.Markdown(label="Results")
43
+ with gr.Accordion("Raw JSON", open=False):
44
+ output_json = gr.Code(language="json")
45
+
46
+ def run_enrich(gene_list, lim, skip_cache, keys):
47
+ if not gene_list.strip():
48
+ raise gr.Error("Please enter at least one gene symbol (e.g., BRAF,KRAS,NRAS).")
49
+
50
+ clean = gene_list.strip().replace(" ", "")
51
+ args = ["enrich", clean, "--limit", str(int(lim))]
52
+
53
+ env = config.build_env_overrides(keys)
54
+ result = runner.run(args, json_mode=True, no_cache=skip_cache, env_overrides=env)
55
+ if not result["success"]:
56
+ raise gr.Error(f"BioMCP error: {result['error']}")
57
+ md, js = format_result(result)
58
+ return md, js
59
+
60
+ run_btn.click(
61
+ fn=run_enrich,
62
+ inputs=[genes, limit, no_cache, session_keys],
63
+ outputs=[output_md, output_json],
64
+ )
tabs/get_detail.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Get Detail tab — focused entity detail with selectable sections (progressive disclosure).
3
+ """
4
+
5
+ import gradio as gr
6
+ from core import config, runner
7
+ from core.formatter import format_result
8
+
9
+ # Sections available per entity type
10
+ ENTITY_SECTIONS = {
11
+ "gene": [
12
+ "pathways", "ontology", "diseases", "protein", "go", "interactions",
13
+ "civic", "expression", "hpa", "druggability", "clingen", "constraint",
14
+ "disgenet", "all",
15
+ ],
16
+ "variant": ["clinvar", "population", "conservation", "predict", "gwas", "all"],
17
+ "article": ["fulltext", "tldr", "all"],
18
+ "trial": ["eligibility", "locations", "outcomes", "all"],
19
+ "drug": ["label", "targets", "civic", "approvals", "regulatory", "safety", "shortage", "all"],
20
+ "disease": [
21
+ "genes", "phenotypes", "variants", "models", "pathways",
22
+ "prevalence", "civic", "disgenet", "all",
23
+ ],
24
+ "pathway": ["genes", "all"],
25
+ "protein": ["domains", "interactions", "complexes", "all"],
26
+ "adverse-event": ["reactions", "outcomes", "concomitant", "guidance", "all"],
27
+ "pgx": ["recommendations", "frequencies", "annotations"],
28
+ }
29
+
30
+ # Default sections per entity — use "all" for drug/trial to get full detail
31
+ ENTITY_DEFAULTS = {
32
+ "drug": ["all"],
33
+ "trial": ["all"],
34
+ }
35
+
36
+ ENTITY_CHOICES = list(ENTITY_SECTIONS.keys())
37
+
38
+ GET_EXAMPLES = {
39
+ "gene": ("BRAF", ["pathways", "hpa"], "Gene detail with pathways + tissue expression"),
40
+ "variant": ("BRAF V600E", ["clinvar", "population"], "ClinVar significance + population freq"),
41
+ "article": ("22663011", ["tldr"], "Article summary with TLDR"),
42
+ "trial": ("NCT02576665", ["all"], "Full trial details"),
43
+ "drug": ("pembrolizumab", ["all"], "Full drug details"),
44
+ "disease": ("MONDO:0005105", ["genes", "phenotypes"], "Disease genes + phenotypes"),
45
+ "pathway": ("hsa05200", ["genes"], "Pathway gene list"),
46
+ "protein": ("P15056", ["domains", "interactions"], "Protein domains + interactions"),
47
+ "adverse-event": ("10222779", ["reactions", "outcomes"], "Adverse event details"),
48
+ "pgx": ("CYP2D6", ["recommendations"], "PGx recommendations"),
49
+ }
50
+
51
+
52
+ def create_get_tab(session_keys):
53
+ """Build the Get Detail tab."""
54
+
55
+ with gr.Tab("📋 Get Detail"):
56
+ gr.Markdown(
57
+ "## Entity Detail\n"
58
+ "Retrieve focused detail for a specific entity ID with selectable sections.\n"
59
+ "**Drug** and **Trial** default to full details (`all` sections). Customize sections as needed."
60
+ )
61
+
62
+ with gr.Row():
63
+ entity = gr.Dropdown(
64
+ choices=ENTITY_CHOICES,
65
+ value="gene",
66
+ label="Entity Type",
67
+ scale=1,
68
+ )
69
+ entity_id = gr.Textbox(
70
+ label="Entity ID",
71
+ placeholder="e.g., BRAF, BRAF V600E, 22663011, NCT02576665",
72
+ scale=2,
73
+ )
74
+
75
+ sections = gr.CheckboxGroup(
76
+ choices=ENTITY_SECTIONS["gene"],
77
+ value=[],
78
+ label="Sections (leave empty for summary only)",
79
+ )
80
+
81
+ with gr.Row():
82
+ # Drug-specific region option
83
+ drug_region = gr.Dropdown(
84
+ choices=[
85
+ ("— Default (US)", ""),
86
+ ("🇺🇸 United States (FDA)", "us"),
87
+ ("🇪🇺 European Union (EMA)", "eu"),
88
+ ("🌐 All Regions (US + EU)", "all"),
89
+ ],
90
+ value="",
91
+ label="Region (drug regulatory/safety/shortage)",
92
+ visible=False,
93
+ )
94
+ no_cache = gr.Checkbox(label="Bypass cache", value=False)
95
+
96
+ # Update sections when entity changes
97
+ def update_sections(ent):
98
+ choices = ENTITY_SECTIONS.get(ent, [])
99
+ defaults = ENTITY_DEFAULTS.get(ent, [])
100
+ drug_vis = ent == "drug"
101
+ example = GET_EXAMPLES.get(ent)
102
+ if example:
103
+ eid, secs, desc = example
104
+ hint = f"**Example:** `biomcp get {ent} \"{eid}\" {' '.join(secs)}` — {desc}"
105
+ else:
106
+ eid, hint = "", ""
107
+ return (
108
+ gr.CheckboxGroup(choices=choices, value=defaults),
109
+ gr.Dropdown(visible=drug_vis),
110
+ eid,
111
+ hint,
112
+ )
113
+
114
+ example_display = gr.Markdown("")
115
+
116
+ entity.change(
117
+ fn=update_sections,
118
+ inputs=[entity],
119
+ outputs=[sections, drug_region, entity_id, example_display],
120
+ )
121
+
122
+ # Quick examples
123
+ gr.Markdown("**Quick examples:**")
124
+ with gr.Row():
125
+ ex1 = gr.Button("Gene: BRAF", size="sm", variant="secondary")
126
+ ex2 = gr.Button("Variant: BRAF V600E", size="sm", variant="secondary")
127
+ ex3 = gr.Button("Article: 22663011", size="sm", variant="secondary")
128
+ ex4 = gr.Button("Trial: NCT02576665", size="sm", variant="secondary")
129
+ ex5 = gr.Button("Drug: pembrolizumab", size="sm", variant="secondary")
130
+
131
+ def set_example(ent, eid):
132
+ secs = ENTITY_SECTIONS.get(ent, [])
133
+ defaults = ENTITY_DEFAULTS.get(ent, [])
134
+ return ent, eid, gr.CheckboxGroup(choices=secs, value=defaults)
135
+
136
+ ex1.click(fn=lambda: set_example("gene", "BRAF"), outputs=[entity, entity_id, sections])
137
+ ex2.click(fn=lambda: set_example("variant", "BRAF V600E"), outputs=[entity, entity_id, sections])
138
+ ex3.click(fn=lambda: set_example("article", "22663011"), outputs=[entity, entity_id, sections])
139
+ ex4.click(fn=lambda: set_example("trial", "NCT02576665"), outputs=[entity, entity_id, sections])
140
+ ex5.click(fn=lambda: set_example("drug", "pembrolizumab"), outputs=[entity, entity_id, sections])
141
+
142
+ run_btn = gr.Button("📋 Get Details", variant="primary")
143
+ output_md = gr.Markdown(label="Results")
144
+ with gr.Accordion("Raw JSON", open=False):
145
+ output_json = gr.Code(language="json")
146
+
147
+ def run_get(ent, eid, secs, region, skip_cache, keys):
148
+ if not eid.strip():
149
+ raise gr.Error("Please enter an entity ID (e.g., BRAF, NCT02576665, pembrolizumab).")
150
+
151
+ args = ["get", ent, eid.strip()]
152
+
153
+ # Add sections
154
+ if secs:
155
+ if "all" in secs:
156
+ args.append("all")
157
+ else:
158
+ args.extend(secs)
159
+
160
+ # Drug region (only for relevant sections)
161
+ if ent == "drug" and region and region != "us":
162
+ args.extend(["--region", region])
163
+
164
+ env = config.build_env_overrides(keys)
165
+ result = runner.run(args, json_mode=True, no_cache=skip_cache, env_overrides=env)
166
+ if not result["success"]:
167
+ raise gr.Error(f"BioMCP error: {result['error']}")
168
+ md, js = format_result(result)
169
+ return md, js
170
+
171
+ run_btn.click(
172
+ fn=run_get,
173
+ inputs=[entity, entity_id, sections, drug_region, no_cache, session_keys],
174
+ outputs=[output_md, output_json],
175
+ )
tabs/helpers.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cross-Entity Helpers tab — pivot between related entities.
3
+ """
4
+
5
+ import gradio as gr
6
+ from core import config, runner
7
+ from core.formatter import format_result
8
+
9
+ # All helper commands: (source_entity, helper_verb, id_label, id_placeholder, description)
10
+ HELPERS = [
11
+ ("variant", "trials", "Variant", "BRAF V600E", "Find trials for a variant"),
12
+ ("variant", "articles", "Variant", "BRAF V600E", "Find articles about a variant"),
13
+ ("variant", "oncokb", "Variant", "BRAF V600E", "OncoKB therapy evidence (needs ONCOKB_TOKEN)"),
14
+ ("drug", "adverse-events", "Drug", "pembrolizumab", "Adverse events for a drug"),
15
+ ("drug", "trials", "Drug", "pembrolizumab", "Trials for a drug"),
16
+ ("disease", "trials", "Disease", "melanoma", "Trials for a disease"),
17
+ ("disease", "drugs", "Disease", "melanoma", "Drugs for a disease"),
18
+ ("disease", "articles", "Disease", "Lynch syndrome", "Articles about a disease"),
19
+ ("gene", "trials", "Gene", "BRAF", "Trials involving a gene"),
20
+ ("gene", "drugs", "Gene", "BRAF", "Drugs targeting a gene"),
21
+ ("gene", "articles", "Gene", "BRCA1", "Articles about a gene"),
22
+ ("gene", "pathways", "Gene", "BRAF", "Pathways involving a gene"),
23
+ ("pathway", "drugs", "Pathway ID", "R-HSA-5673001", "Drugs related to a pathway"),
24
+ ("pathway", "articles", "Pathway ID", "R-HSA-5673001", "Articles about a pathway"),
25
+ ("pathway", "trials", "Pathway ID", "R-HSA-5673001", "Trials related to a pathway"),
26
+ ("protein", "structures", "UniProt ID", "P15056", "Protein structures"),
27
+ ("article", "entities", "PMID", "22663011", "Named entities from an article"),
28
+ ("article", "citations", "PMID", "22663011", "Papers that cite this article"),
29
+ ("article", "references", "PMID", "22663011", "References cited by this article"),
30
+ ("article", "recommendations", "PMID", "22663011", "Recommended similar articles"),
31
+ ]
32
+
33
+ HELPER_LABELS = [f"{h[0]} {h[1]} — {h[4]}" for h in HELPERS]
34
+
35
+
36
+ def create_helpers_tab(session_keys):
37
+ """Build the Cross-Entity Helpers tab."""
38
+
39
+ with gr.Tab("🔗 Cross-Entity Helpers"):
40
+ gr.Markdown(
41
+ "## Cross-Entity Pivots\n"
42
+ "Pivot from one entity to another without rebuilding filters.\n"
43
+ "Select a helper command, enter the ID, and go."
44
+ )
45
+
46
+ helper_choice = gr.Dropdown(
47
+ choices=HELPER_LABELS,
48
+ value=HELPER_LABELS[0],
49
+ label="Helper Command",
50
+ )
51
+
52
+ with gr.Row():
53
+ helper_id = gr.Textbox(
54
+ label="ID / Name",
55
+ placeholder="BRAF V600E",
56
+ scale=3,
57
+ )
58
+ helper_limit = gr.Number(label="Limit", value=5, minimum=1, maximum=50, scale=1)
59
+
60
+ with gr.Row():
61
+ no_cache = gr.Checkbox(label="Bypass cache", value=False)
62
+
63
+ # Update placeholder when helper changes
64
+ def update_placeholder(choice):
65
+ idx = HELPER_LABELS.index(choice) if choice in HELPER_LABELS else 0
66
+ h = HELPERS[idx]
67
+ return gr.Textbox(placeholder=f"e.g., {h[3]}", label=h[2])
68
+
69
+ helper_choice.change(fn=update_placeholder, inputs=[helper_choice], outputs=[helper_id])
70
+
71
+ # Quick examples
72
+ gr.Markdown("**Quick examples:**")
73
+ with gr.Row():
74
+ ex1 = gr.Button('variant trials "BRAF V600E"', size="sm", variant="secondary")
75
+ ex2 = gr.Button("gene drugs BRAF", size="sm", variant="secondary")
76
+ ex3 = gr.Button("drug adverse-events pembrolizumab", size="sm", variant="secondary")
77
+ ex4 = gr.Button("article citations 22663011", size="sm", variant="secondary")
78
+
79
+ def set_ex(label, eid):
80
+ return label, eid
81
+
82
+ ex1.click(fn=lambda: set_ex(HELPER_LABELS[0], "BRAF V600E"), outputs=[helper_choice, helper_id])
83
+ ex2.click(fn=lambda: set_ex(HELPER_LABELS[9], "BRAF"), outputs=[helper_choice, helper_id])
84
+ ex3.click(fn=lambda: set_ex(HELPER_LABELS[3], "pembrolizumab"), outputs=[helper_choice, helper_id])
85
+ ex4.click(fn=lambda: set_ex(HELPER_LABELS[17], "22663011"), outputs=[helper_choice, helper_id])
86
+
87
+ run_btn = gr.Button("🔗 Run Helper", variant="primary")
88
+ output_md = gr.Markdown(label="Results")
89
+ with gr.Accordion("Raw JSON", open=False):
90
+ output_json = gr.Code(language="json")
91
+
92
+ def run_helper(choice, eid, lim, skip_cache, keys):
93
+ if not eid.strip():
94
+ raise gr.Error("Please enter an ID or name (e.g., BRAF V600E, pembrolizumab, 22663011).")
95
+
96
+ idx = HELPER_LABELS.index(choice) if choice in HELPER_LABELS else 0
97
+ h = HELPERS[idx]
98
+ entity, verb = h[0], h[1]
99
+
100
+ args = [entity, verb, eid.strip()]
101
+ lim = int(lim) if lim else 5
102
+ args.extend(["--limit", str(lim)])
103
+
104
+ env = config.build_env_overrides(keys)
105
+ result = runner.run(args, json_mode=True, no_cache=skip_cache, env_overrides=env)
106
+ if not result["success"]:
107
+ raise gr.Error(f"BioMCP error: {result['error']}")
108
+ md, js = format_result(result)
109
+ return md, js
110
+
111
+ run_btn.click(
112
+ fn=run_helper,
113
+ inputs=[helper_choice, helper_id, helper_limit, no_cache, session_keys],
114
+ outputs=[output_md, output_json],
115
+ )
tabs/search.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Search tab — entity-based discovery across all 13+ entity types.
3
+ Dynamic forms show/hide filters based on selected entity.
4
+ """
5
+
6
+ import gradio as gr
7
+ from core import config, runner
8
+ from core.formatter import format_result
9
+
10
+ ENTITY_CHOICES = [
11
+ "all", "gene", "disease", "variant", "article", "trial",
12
+ "drug", "pathway", "protein", "adverse-event", "pgx", "gwas", "phenotype",
13
+ ]
14
+
15
+ TRIAL_STATUS = [
16
+ "", "recruiting", "not yet recruiting", "active, not recruiting",
17
+ "completed", "terminated", "suspended", "withdrawn",
18
+ ]
19
+
20
+ TRIAL_PHASES = ["", "1", "2", "3", "4"]
21
+
22
+ VARIANT_SIGNIFICANCE = [
23
+ "", "pathogenic", "likely_pathogenic", "uncertain_significance",
24
+ "likely_benign", "benign", "conflicting_interpretations", "risk_factor",
25
+ ]
26
+
27
+ VARIANT_CONSEQUENCE = [
28
+ "", "missense_variant", "nonsense_variant", "synonymous_variant",
29
+ "frameshift_variant", "splice_acceptor_variant", "splice_donor_variant",
30
+ "inframe_deletion", "inframe_insertion", "stop_lost", "start_lost",
31
+ ]
32
+
33
+ ARTICLE_SOURCE = ["", "all", "pubtator", "europepmc", "pubmed"]
34
+
35
+ TRIAL_SOURCE = ["", "ctgov", "nci"]
36
+
37
+ DISEASE_SOURCE = ["", "mondo"]
38
+
39
+ SEARCH_EXAMPLES = {
40
+ "all": ("--gene BRAF --disease melanoma", "Cross-entity overview for BRAF + melanoma"),
41
+ "gene": ("-q BRAF", "Search genes matching BRAF"),
42
+ "disease": ("-q melanoma", "Search diseases matching melanoma"),
43
+ "variant": ("-g BRAF --hgvsp V600E", "Search BRAF V600E variants"),
44
+ "article": ("-g BRAF -d melanoma --since 2024-01-01", "BRAF melanoma articles since 2024"),
45
+ "trial": ("-c melanoma --status recruiting", "Recruiting melanoma trials"),
46
+ "drug": ("-q pembrolizumab", "Search drug pembrolizumab"),
47
+ "pathway": ('-q "MAPK signaling"', "Search MAPK signaling pathways"),
48
+ "protein": ("-q kinase", "Search kinase proteins"),
49
+ "adverse-event": ("--drug pembrolizumab --serious", "Serious adverse events for pembrolizumab"),
50
+ "pgx": ("-g CYP2D6", "PGx data for CYP2D6"),
51
+ "gwas": ('--trait "type 2 diabetes"', "GWAS for type 2 diabetes"),
52
+ "phenotype": ('"HP:0001250 HP:0001263"', "Phenotype similarity search"),
53
+ }
54
+
55
+
56
+ def create_search_tab(session_keys):
57
+ """Build the Search tab with dynamic entity forms."""
58
+
59
+ with gr.Tab("🔎 Search"):
60
+ gr.Markdown(
61
+ "## Entity Search\n"
62
+ "Discovery across 13 biomedical entity types. Select an entity and fill in the relevant filters."
63
+ )
64
+
65
+ with gr.Row():
66
+ entity = gr.Dropdown(
67
+ choices=ENTITY_CHOICES,
68
+ value="gene",
69
+ label="Entity Type",
70
+ scale=1,
71
+ )
72
+ limit = gr.Number(label="Limit", value=10, minimum=1, maximum=100, scale=1)
73
+ offset = gr.Number(label="Offset", value=0, minimum=0, scale=1)
74
+
75
+ with gr.Row():
76
+ no_cache = gr.Checkbox(label="Bypass cache", value=False)
77
+
78
+ # === All (cross-entity) filters ===
79
+ with gr.Group(visible=False) as all_group:
80
+ gr.Markdown("### Cross-Entity Search Filters")
81
+ with gr.Row():
82
+ all_gene = gr.Textbox(label="Gene", placeholder="e.g., BRAF")
83
+ all_disease = gr.Textbox(label="Disease", placeholder="e.g., melanoma")
84
+ all_keyword = gr.Textbox(label="Keyword", placeholder="e.g., immunotherapy resistance")
85
+ with gr.Row():
86
+ all_since = gr.Textbox(label="Since (date)", placeholder="e.g., 2024-01-01")
87
+ all_counts_only = gr.Checkbox(label="Counts only")
88
+ all_debug_plan = gr.Checkbox(label="Debug plan")
89
+
90
+ # === Gene filters ===
91
+ with gr.Group(visible=True) as gene_group:
92
+ gr.Markdown("### Gene Search")
93
+ gene_query = gr.Textbox(label="Query", placeholder="e.g., BRAF, TP53, EGFR")
94
+
95
+ # === Disease filters ===
96
+ with gr.Group(visible=False) as disease_group:
97
+ gr.Markdown("### Disease Search")
98
+ with gr.Row():
99
+ disease_query = gr.Textbox(label="Query", placeholder="e.g., melanoma, Lynch syndrome")
100
+ disease_source = gr.Dropdown(choices=DISEASE_SOURCE, value="", label="Source")
101
+
102
+ # === Variant filters ===
103
+ with gr.Group(visible=False) as variant_group:
104
+ gr.Markdown("### Variant Search")
105
+ with gr.Row():
106
+ variant_gene = gr.Textbox(label="Gene (-g)", placeholder="e.g., BRAF")
107
+ variant_hgvsp = gr.Textbox(label="HGVSp", placeholder="e.g., V600E")
108
+ with gr.Row():
109
+ variant_sig = gr.Dropdown(choices=VARIANT_SIGNIFICANCE, value="", label="Significance")
110
+ variant_consequence = gr.Dropdown(choices=VARIANT_CONSEQUENCE, value="", label="Consequence")
111
+
112
+ # === Article filters ===
113
+ with gr.Group(visible=False) as article_group:
114
+ gr.Markdown("### Article Search")
115
+ with gr.Row():
116
+ article_gene = gr.Textbox(label="Gene (-g)", placeholder="e.g., BRAF")
117
+ article_disease = gr.Textbox(label="Disease (-d)", placeholder="e.g., melanoma")
118
+ with gr.Row():
119
+ article_since = gr.Textbox(label="Since", placeholder="e.g., 2024-01-01")
120
+ article_source = gr.Dropdown(choices=ARTICLE_SOURCE, value="", label="Source")
121
+
122
+ # === Trial filters ===
123
+ with gr.Group(visible=False) as trial_group:
124
+ gr.Markdown("### Trial Search")
125
+ with gr.Row():
126
+ trial_condition = gr.Textbox(label="Condition (-c)", placeholder="e.g., melanoma")
127
+ trial_status = gr.Dropdown(choices=TRIAL_STATUS, value="", label="Status")
128
+ trial_phase = gr.Dropdown(choices=TRIAL_PHASES, value="", label="Phase")
129
+ with gr.Row():
130
+ trial_source = gr.Dropdown(choices=TRIAL_SOURCE, value="", label="Source")
131
+ trial_lat = gr.Textbox(label="Latitude", placeholder="e.g., 42.3601")
132
+ trial_lon = gr.Textbox(label="Longitude", placeholder="e.g., -71.0589")
133
+ trial_distance = gr.Textbox(label="Distance (mi)", placeholder="e.g., 50")
134
+
135
+ # === Drug filters ===
136
+ with gr.Group(visible=False) as drug_group:
137
+ gr.Markdown("### Drug Search")
138
+ with gr.Row():
139
+ drug_query = gr.Textbox(label="Query", placeholder="e.g., pembrolizumab, kinase inhibitor")
140
+ drug_region = gr.Dropdown(
141
+ choices=[
142
+ ("— Default (US + EU auto)", ""),
143
+ ("🇪🇺 EU (European Medicines Agency)", "eu"),
144
+ ],
145
+ value="",
146
+ label="Region",
147
+ )
148
+
149
+ # === Pathway filters ===
150
+ with gr.Group(visible=False) as pathway_group:
151
+ gr.Markdown("### Pathway Search")
152
+ pathway_query = gr.Textbox(label="Query", placeholder='e.g., "MAPK signaling", "Pathways in cancer"')
153
+
154
+ # === Protein filters ===
155
+ with gr.Group(visible=False) as protein_group:
156
+ gr.Markdown("### Protein Search")
157
+ with gr.Row():
158
+ protein_query = gr.Textbox(label="Query", placeholder="e.g., kinase")
159
+ protein_all_species = gr.Checkbox(label="All species")
160
+
161
+ # === Adverse Event filters ===
162
+ with gr.Group(visible=False) as ae_group:
163
+ gr.Markdown("### Adverse Event Search")
164
+ with gr.Row():
165
+ ae_drug = gr.Textbox(label="Drug", placeholder="e.g., pembrolizumab")
166
+ ae_serious = gr.Checkbox(label="Serious only")
167
+ with gr.Row():
168
+ ae_type = gr.Dropdown(choices=["", "device"], value="", label="Type")
169
+ ae_manufacturer = gr.Textbox(label="Manufacturer", placeholder="e.g., Medtronic")
170
+ ae_product_code = gr.Textbox(label="Product code", placeholder="e.g., PQP")
171
+
172
+ # === PGx filters ===
173
+ with gr.Group(visible=False) as pgx_group:
174
+ gr.Markdown("### PGx Search")
175
+ with gr.Row():
176
+ pgx_gene = gr.Textbox(label="Gene (-g)", placeholder="e.g., CYP2D6")
177
+ pgx_drug = gr.Textbox(label="Drug (-d)", placeholder="e.g., warfarin")
178
+
179
+ # === GWAS filters ===
180
+ with gr.Group(visible=False) as gwas_group:
181
+ gr.Markdown("### GWAS Search")
182
+ with gr.Row():
183
+ gwas_gene = gr.Textbox(label="Gene (-g)", placeholder="e.g., TCF7L2")
184
+ gwas_trait = gr.Textbox(label="Trait", placeholder='e.g., "type 2 diabetes"')
185
+
186
+ # === Phenotype filters ===
187
+ with gr.Group(visible=False) as phenotype_group:
188
+ gr.Markdown("### Phenotype Search (Monarch Semsim)")
189
+ phenotype_terms = gr.Textbox(label="HPO Terms", placeholder="e.g., HP:0001250 HP:0001263")
190
+
191
+ # Dynamic visibility
192
+ entity_groups = {
193
+ "all": all_group, "gene": gene_group, "disease": disease_group,
194
+ "variant": variant_group, "article": article_group, "trial": trial_group,
195
+ "drug": drug_group, "pathway": pathway_group, "protein": protein_group,
196
+ "adverse-event": ae_group, "pgx": pgx_group, "gwas": gwas_group,
197
+ "phenotype": phenotype_group,
198
+ }
199
+
200
+ def toggle_visibility(selected):
201
+ return [gr.Group(visible=(k == selected)) for k in entity_groups]
202
+
203
+ entity.change(
204
+ fn=toggle_visibility,
205
+ inputs=[entity],
206
+ outputs=list(entity_groups.values()),
207
+ )
208
+
209
+ # Example button
210
+ example_display = gr.Markdown("")
211
+
212
+ def show_example(ent):
213
+ if ent in SEARCH_EXAMPLES:
214
+ hint, desc = SEARCH_EXAMPLES[ent]
215
+ return f"**Example:** `biomcp search {ent} {hint}` — {desc}"
216
+ return ""
217
+
218
+ entity.change(fn=show_example, inputs=[entity], outputs=[example_display])
219
+
220
+ # Run button
221
+ run_btn = gr.Button("🔎 Search", variant="primary")
222
+ output_md = gr.Markdown(label="Results")
223
+ with gr.Accordion("Raw JSON", open=False):
224
+ output_json = gr.Code(language="json")
225
+
226
+ def run_search(
227
+ ent, lim, off, skip_cache, keys,
228
+ # all
229
+ a_gene, a_disease, a_keyword, a_since, a_counts, a_debug,
230
+ # gene
231
+ g_query,
232
+ # disease
233
+ d_query, d_source,
234
+ # variant
235
+ v_gene, v_hgvsp, v_sig, v_cons,
236
+ # article
237
+ ar_gene, ar_disease, ar_since, ar_source,
238
+ # trial
239
+ t_cond, t_status, t_phase, t_source, t_lat, t_lon, t_dist,
240
+ # drug
241
+ dr_query, dr_region,
242
+ # pathway
243
+ pw_query,
244
+ # protein
245
+ pr_query, pr_all_species,
246
+ # adverse event
247
+ ae_d, ae_s, ae_t, ae_m, ae_pc,
248
+ # pgx
249
+ pgx_g, pgx_d,
250
+ # gwas
251
+ gw_g, gw_t,
252
+ # phenotype
253
+ ph_terms,
254
+ ):
255
+ args = ["search", ent]
256
+
257
+ lim = int(lim) if lim else 10
258
+ off = int(off) if off else 0
259
+
260
+ if ent == "all":
261
+ if a_gene: args.extend(["--gene", a_gene.strip()])
262
+ if a_disease: args.extend(["--disease", a_disease.strip()])
263
+ if a_keyword: args.extend(["--keyword", a_keyword.strip()])
264
+ if a_since: args.extend(["--since", a_since.strip()])
265
+ if a_counts: args.append("--counts-only")
266
+ if a_debug: args.append("--debug-plan")
267
+ elif ent == "gene":
268
+ if g_query: args.extend(["-q", g_query.strip()])
269
+ elif ent == "disease":
270
+ if d_query: args.extend(["-q", d_query.strip()])
271
+ if d_source: args.extend(["--source", d_source])
272
+ elif ent == "variant":
273
+ if v_gene: args.extend(["-g", v_gene.strip()])
274
+ if v_hgvsp: args.extend(["--hgvsp", v_hgvsp.strip()])
275
+ if v_sig: args.extend(["--significance", v_sig])
276
+ if v_cons: args.extend(["--consequence", v_cons])
277
+ elif ent == "article":
278
+ if ar_gene: args.extend(["-g", ar_gene.strip()])
279
+ if ar_disease: args.extend(["-d", ar_disease.strip()])
280
+ if ar_since: args.extend(["--since", ar_since.strip()])
281
+ if ar_source: args.extend(["--source", ar_source])
282
+ elif ent == "trial":
283
+ if t_cond: args.extend(["-c", t_cond.strip()])
284
+ if t_status: args.extend(["--status", t_status])
285
+ if t_phase: args.extend(["--phase", t_phase])
286
+ if t_source: args.extend(["--source", t_source])
287
+ if t_lat: args.extend(["--lat", t_lat.strip()])
288
+ if t_lon: args.extend(["--lon", t_lon.strip()])
289
+ if t_dist: args.extend(["--distance", t_dist.strip()])
290
+ elif ent == "drug":
291
+ if dr_query: args.extend(["-q", dr_query.strip()])
292
+ if dr_region: args.extend(["--region", dr_region])
293
+ elif ent == "pathway":
294
+ if pw_query: args.extend(["-q", pw_query.strip()])
295
+ elif ent == "protein":
296
+ if pr_query: args.extend(["-q", pr_query.strip()])
297
+ if pr_all_species: args.append("--all-species")
298
+ elif ent == "adverse-event":
299
+ if ae_d: args.extend(["--drug", ae_d.strip()])
300
+ if ae_s: args.append("--serious")
301
+ if ae_t: args.extend(["--type", ae_t])
302
+ if ae_m: args.extend(["--manufacturer", ae_m.strip()])
303
+ if ae_pc: args.extend(["--product-code", ae_pc.strip()])
304
+ elif ent == "pgx":
305
+ if pgx_g: args.extend(["-g", pgx_g.strip()])
306
+ if pgx_d: args.extend(["-d", pgx_d.strip()])
307
+ elif ent == "gwas":
308
+ if gw_g: args.extend(["-g", gw_g.strip()])
309
+ if gw_t: args.extend(["--trait", gw_t.strip()])
310
+ elif ent == "phenotype":
311
+ if ph_terms: args.append(ph_terms.strip())
312
+
313
+ # Add limit/offset
314
+ args.extend(["--limit", str(lim)])
315
+ if off > 0:
316
+ args.extend(["--offset", str(off)])
317
+
318
+ env = config.build_env_overrides(keys)
319
+ result = runner.run(args, json_mode=True, no_cache=skip_cache, env_overrides=env)
320
+ if not result["success"]:
321
+ raise gr.Error(f"BioMCP error: {result['error']}")
322
+ md, js = format_result(result)
323
+ return md, js
324
+
325
+ all_inputs = [
326
+ entity, limit, offset, no_cache, session_keys,
327
+ # all
328
+ all_gene, all_disease, all_keyword, all_since, all_counts_only, all_debug_plan,
329
+ # gene
330
+ gene_query,
331
+ # disease
332
+ disease_query, disease_source,
333
+ # variant
334
+ variant_gene, variant_hgvsp, variant_sig, variant_consequence,
335
+ # article
336
+ article_gene, article_disease, article_since, article_source,
337
+ # trial
338
+ trial_condition, trial_status, trial_phase, trial_source,
339
+ trial_lat, trial_lon, trial_distance,
340
+ # drug
341
+ drug_query, drug_region,
342
+ # pathway
343
+ pathway_query,
344
+ # protein
345
+ protein_query, protein_all_species,
346
+ # adverse-event
347
+ ae_drug, ae_serious, ae_type, ae_manufacturer, ae_product_code,
348
+ # pgx
349
+ pgx_gene, pgx_drug,
350
+ # gwas
351
+ gwas_gene, gwas_trait,
352
+ # phenotype
353
+ phenotype_terms,
354
+ ]
355
+
356
+ run_btn.click(
357
+ fn=run_search,
358
+ inputs=all_inputs,
359
+ outputs=[output_md, output_json],
360
+ )
tabs/settings.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Settings & Health tab — API key management, health checks, and version info.
3
+ """
4
+
5
+ import gradio as gr
6
+ from core import config, runner
7
+ from core.formatter import format_result
8
+
9
+
10
+ def create_settings_tab(session_keys):
11
+ """Build the Settings & Health tab."""
12
+
13
+ with gr.Tab("⚙️ Settings & Health"):
14
+ gr.Markdown("## API Key Configuration\nKeys are stored in session only — never written to disk.")
15
+
16
+ key_inputs = {}
17
+
18
+ with gr.Row():
19
+ with gr.Column():
20
+ for key_name, meta in list(config.API_KEYS.items())[:4]:
21
+ key_inputs[key_name] = gr.Textbox(
22
+ label=f"{meta['label']}",
23
+ info=f"{meta['description']} — [Get key]({meta['url']})",
24
+ type="password",
25
+ value="",
26
+ placeholder="Paste key here...",
27
+ )
28
+ with gr.Column():
29
+ for key_name, meta in list(config.API_KEYS.items())[4:]:
30
+ key_inputs[key_name] = gr.Textbox(
31
+ label=f"{meta['label']}",
32
+ info=f"{meta['description']} — [Get key]({meta['url']})",
33
+ type="password",
34
+ value="",
35
+ placeholder="Paste key here...",
36
+ )
37
+
38
+ gr.Markdown("### Additional Configuration")
39
+ config_inputs = {}
40
+ for var_name, meta in config.CONFIG_VARS.items():
41
+ config_inputs[var_name] = gr.Textbox(
42
+ label=meta["label"],
43
+ info=meta["description"],
44
+ value="",
45
+ placeholder="Leave blank for default",
46
+ )
47
+
48
+ save_btn = gr.Button("💾 Save Keys to Session", variant="primary")
49
+ save_status = gr.Markdown("")
50
+
51
+ gr.Markdown("---")
52
+ gr.Markdown("## System Checks")
53
+
54
+ with gr.Row():
55
+ health_btn = gr.Button("🏥 Health Check", variant="secondary")
56
+ version_btn = gr.Button("ℹ️ Version Info", variant="secondary")
57
+
58
+ check_output_md = gr.Markdown("")
59
+ with gr.Accordion("Raw JSON", open=False):
60
+ check_output_json = gr.Code(language="json")
61
+
62
+ def save_keys(*values):
63
+ all_keys = list(config.API_KEYS.keys()) + list(config.CONFIG_VARS.keys())
64
+ keys = {}
65
+ for name, val in zip(all_keys, values):
66
+ keys[name] = val.strip() if val else ""
67
+ filled = sum(1 for v in keys.values() if v)
68
+ return keys, f"**Saved {filled} key(s) to session.** These will be used for all commands."
69
+
70
+ all_key_components = list(key_inputs.values()) + list(config_inputs.values())
71
+ save_btn.click(
72
+ fn=save_keys,
73
+ inputs=all_key_components,
74
+ outputs=[session_keys, save_status],
75
+ )
76
+
77
+ def run_health(keys):
78
+ env = config.build_env_overrides(keys)
79
+ result = runner.run_markdown(["health", "--apis-only"], env_overrides=env, timeout=30)
80
+ md, js = format_result(result)
81
+ return md, js
82
+
83
+ health_btn.click(
84
+ fn=run_health,
85
+ inputs=[session_keys],
86
+ outputs=[check_output_md, check_output_json],
87
+ )
88
+
89
+ def run_version(keys):
90
+ env = config.build_env_overrides(keys)
91
+ result = runner.run_markdown(["version"], env_overrides=env, timeout=10)
92
+ md, js = format_result(result)
93
+ return md, js
94
+
95
+ version_btn.click(
96
+ fn=run_version,
97
+ inputs=[session_keys],
98
+ outputs=[check_output_md, check_output_json],
99
+ )