An agentic interface to biological databases — UniProt, Ensembl, and NCBI Gene — with input guardrails and explicit failure-mode handling.
An LLM answers a natural-language question by calling small, validated tools. The tools never let the model fabricate a fact about a gene or protein: every query is validated before it hits the network, and every response comes back as a structured envelope (ok / not_found / malformed_input / upstream_error) that the model has to account for.
Biological database queries fail in quiet, characteristic ways, and a naive agent will rationalize right past them:
- a gene symbol (
TP53) gets passed where an accession (P04637) is expected; - a common name (
human) gets passed where a species slug (homo_sapiens) is expected; - a valid query legitimately returns nothing, which is different from the service being down.
bio-agent treats these as first-class outcomes. The guardrail layer rejects malformed queries with actionable messages; the tool layer distinguishes "found nothing" from "call failed." That taxonomy is exactly what you need to evaluate an agent's biological reasoning, which is why it ships with an eval harness.
question ──► agent loop (Claude tool-use) ──► tool ──► guardrails.precheck ──► HTTP ──► UniProt / Ensembl / NCBI
▲ │
└──────── structured result ◄──────────┘
{status: ok | not_found | malformed_input | upstream_error, data, message}
src/bio_agent/guardrails.py— input validation + aprecheckused by the evalssrc/bio_agent/tools.py— lookups (get_protein,lookup_gene,search_gene) plus the drug-target toolset (resolve_target,get_sequence,expression_hpa,get_structures,get_known_drugs,build_boltz_input, and theplannedstubs) + Anthropic tool schemassrc/bio_agent/dossier.py— the drug-target dossier orchestrator + CLIsrc/bio_agent/agent.py— the tool-calling looptests/— offline unit tests (network mocked)evals/— guardrail eval harness +cases.jsonl
pip install -e ".[dev]" # tools, tests, evals (no API key needed)
pip install -e ".[agent]" # adds the anthropic SDK for the live agentfrom bio_agent.tools import get_protein, lookup_gene, search_gene
get_protein("P04637")
# {'status': 'ok', 'data': {'gene': 'TP53', 'organism': 'Homo sapiens', 'length': 393, ...}}
get_protein("TP53") # a gene symbol, not an accession
# {'status': 'malformed_input', 'message': "'TP53' is not a valid UniProt accession ..."}
lookup_gene("BRCA1", "human") # wrong species format
# {'status': 'malformed_input', 'message': "'human' is not a valid species slug ..."}The anthropic SDK is an optional dependency, so install the agent extra first (the tools, tests, and dossier all work without it):
pip install -e ".[agent]" # installs the anthropic SDK
export ANTHROPIC_API_KEY=sk-...
export BIO_AGENT_MODEL=claude-sonnet-5 # or whichever model you have access to
python -m bio_agent.agent "What is protein P04637, and where is BRCA1 in the human genome?"pytest -q # offline unit tests
python evals/run_evals.py # guardrail eval — scores accept/reject on cases.jsonlExample eval output:
case expected got result
------------------------------------------------------------------------
uniprot_valid ok ok PASS
uniprot_symbol_not_accession malformed_input malformed_input PASS
gene_common_name_species malformed_input malformed_input PASS
...
------------------------------------------------------------------------
12/12 passed (100%)
CI (.github/workflows/ci.yml) runs on Python 3.9 / 3.11 / 3.12.
Given a gene symbol or UniProt accession, bio-agent can assemble a single targetability dossier by orchestrating tools across sources — identity, expression, structure, and known drugs — and emit a ready-to-run input for structure prediction.
python -m bio_agent.dossier BRCA1 # Markdown report
python -m bio_agent.dossier P04637 --json # structured JSON
python -m bio_agent.dossier BRCA1 --boltz-out target.yamlWhat it pulls together:
| Section | Source | Status |
|---|---|---|
| Identity, sequence, cross-refs | UniProt | live |
| Gene → accession resolution | UniProt search | live |
| Tissue / single-cell-type expression, subcellular location | Human Protein Atlas | live |
| Experimental structures + AlphaFold model | UniProt xrefs (PDB) + AlphaFold DB | live |
| Known drug/gene interactions | DGIdb | live |
| Boltz input (apo; add a ligand SMILES for complex prediction) | generated | live |
| Cell-type expression | CZ CELLxGENE census | planned |
| Chemical-similarity / antibody-motif search | ChEMBL + RDKit + SAbDab/IMGT | planned |
The planned tools ship with real interfaces and honest status: "planned" responses (with the intended data source) rather than fabricated results — the failure taxonomy applies to "not built yet," too. Every dossier includes a provenance block reporting the status of each underlying call, so a partial dossier is transparent about what succeeded.
The dossier runs deterministically without an LLM (each step is a plain tool call); the same tools are also exposed to the agent, so you can ask conversationally, e.g. "How druggable is BRCA1, and what structures exist?"
This is intentionally small and readable. Near-term: wire the two planned tools (CELLxGENE census expression; ChEMBL/RDKit similarity + antibody-motif search), pull a top-ranked ligand's SMILES from ChEMBL into the Boltz input automatically, add semantic evals that score the agent's final answer against a rubric (not just query validity), and cache/rate-limit the HTTP layer for batch use.
Developed iteratively with Claude as a pair-programmer — which is also how I use these models in my day-to-day computational-biology work. The design decisions (the failure taxonomy, the guardrail-before-network contract, the eval framing) are the point; the boilerplate is where an LLM helps most.
MIT © 2026 Katie Campbell