Source code for tooluniverse.bold_systems_tool
# bold_systems_tool.py
"""
BOLD Systems tools for ToolUniverse -- DNA barcode records and BINs.
BOLD (Barcode of Life Data System, portal.boldsystems.org) is the primary
global repository of DNA barcode sequences (mostly COI-5P) for species
identification, run by the Centre for Biodiversity Genomics. Its signature
concept is the BIN (Barcode Index Number): an algorithmically-clustered,
sequence-similarity-based operational taxonomic unit that often resolves
species-level identity where morphology or existing taxonomy cannot -- a
capability nothing else in ToolUniverse provides. ToolUniverse's existing
iDigBioSearchTool covers Darwin Core specimen/occurrence records but has no
genetic barcode data or BIN clustering.
The public query API (https://portal.boldsystems.org/api) is a two-step
flow: GET /api/query builds a query from semicolon-delimited "triplet"
tokens ([scope]:[subscope]:[value]) and returns a query_id; GET
/api/documents/{query_id} then pages through the actual records. Confirmed
working triplet scopes/subscopes (live-tested): tax:genus, tax:family,
tax:order, geo:country, ids:processid, bin:uri. tax:species was tested
extensively (several subscope-name guesses, e.g. "species", "epithet",
"binomial") and is silently ignored by the server rather than filtering --
so species-level search here is done by querying at the genus level and
filtering client-side on the returned "species" field, the same workaround
pattern used elsewhere in ToolUniverse for APIs with non-functional
server-side filters.
No authentication required.
"""
from typing import Any, Dict, List, Optional
import requests
from .base_tool import BaseTool
from .tool_registry import register_tool
BOLD_BASE_URL = "https://portal.boldsystems.org/api"
_RANK_SCOPES = {
"genus": "tax:genus",
"family": "tax:family",
"order": "tax:order",
"species": "tax:genus", # BOLD ignores tax:species; filter client-side.
}
_RECORD_FIELDS = (
"processid",
"sampleid",
"bin_uri",
"kingdom",
"phylum",
"class",
"order",
"family",
"subfamily",
"genus",
"species",
"identification",
"identification_rank",
"country/ocean",
"province/state",
"collectors",
"collection_date_start",
"collection_date_end",
"inst",
"marker_code",
"nuc_basecount",
"insdc_acs",
)
def _summarize(rec: Dict[str, Any]) -> Dict[str, Any]:
return {field: rec.get(field) for field in _RECORD_FIELDS}
def _bold_get(url: str, params: Dict[str, Any], timeout: int):
"""GET a BOLD endpoint, returning (payload, error_envelope).
Exactly one of the two is non-None; the error envelope is the standard
{"status": "error", ...} dict so callers never raise.
"""
try:
resp = requests.get(url, params=params, timeout=timeout)
if resp.status_code >= 500:
return None, {
"status": "error",
"error": f"BOLD Systems rejected the query (HTTP {resp.status_code}): "
f"{resp.text[:200]}",
}
resp.raise_for_status()
return resp.json(), None
except requests.exceptions.Timeout:
return None, {
"status": "error",
"error": f"BOLD Systems request timed out after {timeout}s",
}
except requests.exceptions.RequestException as e:
return None, {"status": "error", "error": f"BOLD Systems request failed: {e}"}
except ValueError:
return None, {
"status": "error",
"error": "BOLD Systems returned a non-JSON response",
}
def _run_query(
query: str, length: int, timeout: int
) -> "tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]":
"""Run the query -> query_id -> documents two-step flow."""
query_payload, err = _bold_get(
f"{BOLD_BASE_URL}/query",
{"query": query, "extent": "limited"},
timeout,
)
if err is not None:
return None, err
query_id = (query_payload or {}).get("query_id")
if not query_id:
return None, {
"status": "error",
"error": f"BOLD Systems returned no query_id for query '{query}'.",
}
doc_payload, err = _bold_get(
f"{BOLD_BASE_URL}/documents/{query_id}",
{"start": 0, "length": length},
timeout,
)
if err is not None:
return None, err
return doc_payload, None
[docs]
@register_tool("BOLDSystemsTool")
class BOLDSystemsTool(BaseTool):
"""
Tool for querying BOLD Systems (Barcode of Life Data System) DNA barcode
records, dispatched by fields.operation:
- "search_by_taxon" : records for a genus/family/order/species, optionally
narrowed by country
- "search_by_bin" : records sharing a BIN (Barcode Index Number) cluster
- "get_record" : a single record by its BOLD process ID
No authentication required.
"""
[docs]
def __init__(self, tool_config: Dict[str, Any]):
super().__init__(tool_config)
self.timeout = tool_config.get("timeout", 30)
self.operation = tool_config.get("fields", {}).get(
"operation", "search_by_taxon"
)
[docs]
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
try:
if self.operation == "search_by_taxon":
return self._search_by_taxon(arguments)
if self.operation == "search_by_bin":
return self._search_by_bin(arguments)
if self.operation == "get_record":
return self._get_record(arguments)
return {
"status": "error",
"error": f"Unknown operation: {self.operation}",
}
except Exception as e:
return {
"status": "error",
"error": f"Error querying BOLD Systems: {str(e)}",
}
[docs]
def _limit(self, arguments: Dict[str, Any], default: int = 30) -> int:
try:
return max(1, min(int(arguments.get("limit") or default), 200))
except (TypeError, ValueError):
return default
[docs]
def _search_by_taxon(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
taxon_name = (arguments.get("taxon_name") or "").strip()
if not taxon_name:
return {
"status": "error",
"error": "taxon_name is required, e.g. 'Panthera' (genus) or "
"'Panthera leo' (species).",
}
rank = (arguments.get("rank") or "genus").strip().lower()
if rank not in _RANK_SCOPES:
return {
"status": "error",
"error": f"rank must be one of {sorted(_RANK_SCOPES)}, got '{rank}'.",
}
# BOLD's tax:species triplet is silently ignored server-side, so
# species search runs at genus level and filters client-side below.
query_taxon = taxon_name.split()[0] if rank == "species" else taxon_name
triplets = [f"{_RANK_SCOPES[rank]}:{query_taxon}"]
country = (arguments.get("country") or "").strip()
if country:
triplets.append(f"geo:country:{country}")
limit = self._limit(arguments)
# Fetch extra raw rows when species-filtering client-side, since most
# of a genus's records won't match one target species.
fetch_length = min(limit * 10, 1000) if rank == "species" else limit
payload, err = _run_query(";".join(triplets), fetch_length, self.timeout)
if err is not None:
return err
rows = payload.get("data") or []
if rank == "species":
target = taxon_name.strip().lower()
rows = [r for r in rows if (r.get("species") or "").strip().lower() == target]
rows = rows[:limit]
return {
"status": "success",
"data": [_summarize(r) for r in rows],
"metadata": {
"query": ";".join(triplets),
"rank": rank,
"returned": len(rows),
"records_total": payload.get("recordsTotal"),
"note": (
"records_total is the genus-level count before "
"client-side species filtering; BOLD does not support "
"server-side species-level filtering."
if rank == "species"
else None
),
"source": "BOLD Systems (portal.boldsystems.org)",
},
}
[docs]
def _search_by_bin(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
bin_id = (arguments.get("bin_id") or "").strip()
if not bin_id:
return {
"status": "error",
"error": "bin_id is required, e.g. 'BOLD:AAD6819'.",
}
if not bin_id.upper().startswith("BOLD:"):
bin_id = f"BOLD:{bin_id}"
limit = self._limit(arguments)
payload, err = _run_query(f"bin:uri:{bin_id}", limit, self.timeout)
if err is not None:
return err
rows = (payload.get("data") or [])[:limit]
return {
"status": "success",
"data": [_summarize(r) for r in rows],
"metadata": {
"bin_id": bin_id,
"returned": len(rows),
"records_total": payload.get("recordsTotal"),
"source": "BOLD Systems (portal.boldsystems.org)",
},
}
[docs]
def _get_record(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
process_id = (arguments.get("process_id") or "").strip()
if not process_id:
return {
"status": "error",
"error": "process_id is required, e.g. 'ABRMM002-06'.",
}
payload, err = _run_query(f"ids:processid:{process_id}", 1, self.timeout)
if err is not None:
return err
rows = payload.get("data") or []
if not rows:
return {
"status": "error",
"error": f"No BOLD record found for process ID '{process_id}'.",
}
return {
"status": "success",
"data": _summarize(rows[0]),
"metadata": {
"process_id": process_id,
"source": "BOLD Systems (portal.boldsystems.org)",
},
}