Source code for tooluniverse.nci_evs_tool

# nci_evs_tool.py
"""
NCI EVS (Enterprise Vocabulary Services) terminology tool for ToolUniverse.

The same public API that backs the existing NCIThesaurusTool hosts over a
dozen other clinical and biomedical terminologies: CTCAE (adverse event
grading, the clinical-trial safety standard), ICD-9-CM, RadLex
(radiology), NDF-RT/MedRT (drug reference terminology), CanMED, plus
GO/ChEBI/HGNC/SNOMED CT/LOINC/ICD-10-CM. CTCAE/ICD-9-CM/RadLex/NDF-RT/
MedRT specifically have no dedicated tool anywhere else in ToolUniverse.
Built as a sibling to NCIThesaurusTool rather than modifying it, since
that tool's contract is deliberately NCIt-only.

MedDRA is also listed in this API's terminology metadata but returns
HTTP 403 on every query (it is a separately licensed vocabulary); this
tool does not expose it. GO, ChEBI, HGNC, SNOMED CT, and ICD-10-CM
(ICD10Tool, via NLM Clinical Tables) already have dedicated ToolUniverse
tools that may return richer or more authoritative data than this
generic interface; prefer those five for those five vocabularies. This
tool still accepts them for completeness, since the underlying API is
shared and free either way.

API: https://api-evsrest.nci.nih.gov/api/v1
No authentication required for the terminologies this tool exposes.
"""

from typing import Any, Dict, List

import requests

from .base_tool import BaseTool
from .tool_registry import register_tool

EVS_BASE_URL = "https://api-evsrest.nci.nih.gov/api/v1"

# MedDRA ("mdr") is listed in this API's own terminology metadata but
# returns HTTP 403 on every query -- a separately licensed vocabulary.
_BLOCKED_TERMINOLOGIES = {"mdr", "meddra"}


[docs] @register_tool("NCIEVSTool") class NCIEVSTool(BaseTool): """ Tool for searching NCI EVS clinical and biomedical terminologies other than NCIt (use NCIThesaurusTool for NCIt specifically). Supports CTCAE (adverse event grading), ICD-10-CM, ICD-9-CM, ICD-10, RadLex, NDF-RT, MedRT, CanMED, and more, via keyword search and direct code lookup. 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_terminology" )
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute the NCI EVS lookup.""" try: if self.operation == "search_terminology": return self._search_terminology(arguments) if self.operation == "get_concept": return self._get_concept(arguments) return { "status": "error", "error": f"Unknown operation: {self.operation}", } except requests.exceptions.Timeout: return { "status": "error", "error": f"NCI EVS request timed out after {self.timeout}s", } except requests.exceptions.ConnectionError: return { "status": "error", "error": "Failed to connect to NCI EVS. Check network.", } except requests.exceptions.HTTPError as e: code = e.response.status_code if e.response is not None else "unknown" if code == 403: return { "status": "error", "error": "NCI EVS returned HTTP 403: this terminology is " "licensed and not openly queryable (e.g. MedDRA).", } return {"status": "error", "error": f"NCI EVS returned HTTP {code}"} except ValueError: return {"status": "error", "error": "NCI EVS returned a non-JSON response"} except Exception as e: return {"status": "error", "error": f"Error querying NCI EVS: {str(e)}"}
[docs] @staticmethod def _check_terminology(terminology: str) -> Dict[str, Any]: if not terminology: return { "status": "error", "error": "terminology is required, e.g. 'ctcae5' (adverse " "events), 'icd9cm' (US legacy diagnosis codes), 'radlex' " "(radiology).", } if terminology.lower() in _BLOCKED_TERMINOLOGIES: return { "status": "error", "error": "MedDRA is a separately licensed vocabulary; NCI " "EVS returns HTTP 403 for it. CTCAE concepts include a " "MedDRA_Code property as a free cross-reference instead.", } return {}
[docs] def _search_terminology(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Keyword-search one NCI EVS terminology.""" terminology = (arguments.get("terminology") or "").strip().lower() check = self._check_terminology(terminology) if check: return check term = (arguments.get("term") or "").strip() if not term: return { "status": "error", "error": "term is required, e.g. 'neutropenia' or 'diabetes'.", } limit = arguments.get("limit") if not isinstance(limit, int) or limit <= 0: limit = 20 limit = min(limit, 100) response = requests.get( f"{EVS_BASE_URL}/concept/{terminology}/search", params={"term": term, "type": "contains", "pageSize": limit}, timeout=self.timeout, ) if response.status_code == 404: return { "status": "error", "error": f"Unknown NCI EVS terminology '{terminology}'.", } response.raise_for_status() payload = response.json() concepts = payload.get("concepts") or [] if not concepts: return { "status": "error", "error": f"No {terminology} concepts matching '{term}'.", } rows = [ { "code": c.get("code"), "name": c.get("name"), "leaf": c.get("leaf"), } for c in concepts ] return { "status": "success", "data": rows, "metadata": { "terminology": terminology, "term": term, "total_matching": payload.get("total"), "returned": len(rows), "note": "code is what get_concept expects.", "source": "NCI EVS (Enterprise Vocabulary Services)", }, }
[docs] def _get_concept(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Fetch one concept's full detail from an NCI EVS terminology.""" terminology = (arguments.get("terminology") or "").strip().lower() check = self._check_terminology(terminology) if check: return check code = (arguments.get("code") or "").strip() if not code: return { "status": "error", "error": "code is required, e.g. 'C143481' (CTCAE5) or " "'E11.9' (ICD-10-CM).", } response = requests.get( f"{EVS_BASE_URL}/concept/{terminology}/{code}", params={"include": "summary"}, timeout=self.timeout, ) if response.status_code == 404: return { "status": "error", "error": f"No {terminology} concept with code '{code}'.", } response.raise_for_status() data = response.json() definitions = data.get("definitions") or [] synonyms: List[Dict[str, Any]] = [ { "name": s.get("name"), "type": s.get("termType"), "source": s.get("source"), } for s in data.get("synonyms") or [] ] properties = { p.get("type"): p.get("value") for p in data.get("properties") or [] if p.get("type") } return { "status": "success", "data": { "code": data.get("code"), "name": data.get("name"), "terminology": data.get("terminology") or terminology, "active": data.get("active"), "definition": definitions[0].get("definition") if definitions else None, "synonyms": synonyms[:20], "properties": properties, }, "metadata": { "terminology": terminology, "code": code, "source": "NCI EVS (Enterprise Vocabulary Services)", }, }