Spaces:
Sleeping
Sleeping
Commit ·
f103ad7
1
Parent(s): ead547a
Deploy BioMCP Explorer
Browse files- README.md +70 -7
- app.py +68 -0
- core/__init__.py +0 -0
- core/config.py +86 -0
- core/formatter.py +32 -0
- core/runner.py +246 -0
- requirements.txt +2 -0
- tabs/__init__.py +0 -0
- tabs/batch.py +88 -0
- tabs/discover.py +65 -0
- tabs/enrichment.py +64 -0
- tabs/get_detail.py +175 -0
- tabs/helpers.py +115 -0
- tabs/search.py +360 -0
- tabs/settings.py +99 -0
README.md
CHANGED
|
@@ -1,14 +1,77 @@
|
|
| 1 |
---
|
| 2 |
title: BioMCP Explorer
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.11.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
|
| 11 |
-
short_description: Exploring BioMCP using 30+ Biomedical API's
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
)
|