Source code for tooluniverse.genebe_tool

"""
GeneBe variant ACMG-classification tool for ToolUniverse.

GeneBe (genebe.net) automatically classifies a germline variant under the
ACMG/AMP guidelines and returns the verdict, the numeric score, and the exact
triggered criteria (e.g. ``PS3,PM1,PM2,PM5,PP2,PP3_Moderate,PP5``), alongside
gene/transcript context, dbSNP id, gnomAD allele frequency, ClinVar
classification, and an AlphaMissense score.

It is complementary to ``InterVar_classify_variant`` (a different group's
implementation of ACMG/AMP): GeneBe is independently maintained and layers in
AlphaMissense / APOGEE2 and gene-specific ACMG, so cross-checking the two
sources is useful when a classification is borderline.

API: https://api.genebe.net/cloud/api-public/v1/variant (public, no key; a
free account raises the anonymous rate limit). Input is chromosome / position
/ ref / alt on genome build hg38 (default) or hg19.
"""

from typing import Any, Dict

import requests

from .base_tool import BaseTool
from .tool_registry import register_tool

GENEBE_API = "https://api.genebe.net/cloud/api-public/v1/variant"

# Accept common build aliases; GeneBe expects hg38 / hg19.
_BUILD_MAP = {
    "hg38": "hg38",
    "grch38": "hg38",
    "38": "hg38",
    "hg19": "hg19",
    "grch37": "hg19",
    "37": "hg19",
}

# The raw record has ~54 fields; surface the clinically useful subset.
_USEFUL_FIELDS = (
    "gene_symbol",
    "transcript",
    "effects",
    "hgvs_c",
    "hgvs_p",
    "acmg_classification",
    "acmg_score",
    "acmg_criteria",
    "clinvar_classification",
    "alphamissense_score",
    "alphamissense_prediction",
    "dbsnp",
    "gnomad_exomes_af",
    "frequency_reference_population",
)


[docs] @register_tool("GeneBeTool") class GeneBeTool(BaseTool): """Classify a germline variant with GeneBe's ACMG/AMP auto-classifier."""
[docs] def __init__(self, tool_config: Dict[str, Any]): super().__init__(tool_config) self.timeout: int = tool_config.get("timeout", 30)
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: chrom = arguments.get("chr") or arguments.get("chrom") pos = arguments.get("pos") ref = arguments.get("ref") alt = arguments.get("alt") missing = [ name for name, val in (("chr", chrom), ("pos", pos), ("ref", ref), ("alt", alt)) if val in (None, "") ] if missing: return { "status": "error", "error": f"Missing required parameter(s): {', '.join(missing)}. " "Provide chr, pos, ref, alt (and optionally genome=hg38/hg19).", } build_in = str(arguments.get("genome") or arguments.get("build") or "hg38") genome = _BUILD_MAP.get(build_in.lower()) if genome is None: return { "status": "error", "error": f"Unsupported genome build '{build_in}'. Use hg38/GRCh38 or hg19/GRCh37.", } params = { "chr": str(chrom).replace("chr", ""), "pos": pos, "ref": ref, "alt": alt, "genome": genome, } try: resp = requests.get( GENEBE_API, params=params, headers={"Accept": "application/json"}, timeout=self.timeout, ) except requests.Timeout: return { "status": "error", "error": f"GeneBe request timed out after {self.timeout}s.", } except requests.exceptions.RequestException as e: return {"status": "error", "error": f"Failed to reach GeneBe: {str(e)}"} if resp.status_code == 429: return { "status": "error", "error": "GeneBe rate limit reached. Create a free account at https://genebe.net to raise it.", } if resp.status_code != 200: return { "status": "error", "error": f"GeneBe returned HTTP {resp.status_code}", "detail": resp.text[:300], } try: variants = resp.json().get("variants", []) except ValueError: return {"status": "error", "error": "GeneBe returned a non-JSON response."} if not variants: return { "status": "error", "error": f"GeneBe returned no result for {params['chr']}-{pos}-{ref}-{alt} ({genome}).", } v = variants[0] data = {k: v[k] for k in _USEFUL_FIELDS if v.get(k) not in (None, "")} data["variant"] = f"{params['chr']}-{pos}-{ref}-{alt}" return { "status": "success", "data": data, "metadata": {"source": "GeneBe (genebe.net)", "genome": genome}, }