Source code for tooluniverse.smartapi_tool

# smartapi_tool.py
"""
SmartAPI registry tool for ToolUniverse.

SmartAPI is the machine-readable registry of ~270 biomedical web APIs
(OpenAPI-described, many part of the NCATS Biomedical Data Translator),
searchable by keyword or tag. ToolUniverse already wraps a large fraction
of the APIs registered here individually, but had no way to discover what
else is registered, or resolve an API name mentioned in a paper or Translator
component list to its actual base URL and endpoint list.

The full per-API document is the complete OpenAPI spec (often hundreds of
KB with every path/schema/component); this tool asks the API's own `fields`
parameter for a slimmed summary rather than returning that raw.

API: https://smart-api.info/api
No authentication required.
"""

from typing import Any, Dict, List

import requests

from .base_tool import BaseTool
from .tool_registry import register_tool

SMARTAPI_BASE_URL = "https://smart-api.info/api"

_SEARCH_FIELDS = (
    "_id,_meta.slug,info.title,info.description,info.contact,tags,servers"
)


def _summarize_hit(hit: Dict[str, Any]) -> Dict[str, Any]:
    """Condense one SmartAPI search hit to its useful summary fields."""
    info = hit.get("info") or {}
    servers = hit.get("servers") or []
    return {
        "api_id": hit.get("_id"),
        "slug": (hit.get("_meta") or {}).get("slug"),
        "title": info.get("title"),
        "description": info.get("description"),
        "base_url": servers[0].get("url") if servers else None,
        "tags": [t.get("name") for t in hit.get("tags") or [] if t.get("name")],
    }


[docs] @register_tool("SmartAPITool") class SmartAPITool(BaseTool): """ Tool for searching the SmartAPI registry of biomedical web APIs. Supports keyword/tag search across ~270 registered OpenAPI specs, and fetching one API's full metadata (servers, tags, endpoint count, contact) by its registry id or slug. 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_apis" )
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute the SmartAPI lookup.""" try: if self.operation == "search_apis": return self._search_apis(arguments) if self.operation == "get_api": return self._get_api(arguments) return { "status": "error", "error": f"Unknown operation: {self.operation}", } except requests.exceptions.Timeout: return { "status": "error", "error": f"SmartAPI request timed out after {self.timeout}s", } except requests.exceptions.ConnectionError: return { "status": "error", "error": "Failed to connect to SmartAPI. Check network.", } except requests.exceptions.HTTPError as e: code = e.response.status_code if e.response is not None else "unknown" return {"status": "error", "error": f"SmartAPI returned HTTP {code}"} except ValueError: return { "status": "error", "error": "SmartAPI returned a non-JSON response", } except Exception as e: return {"status": "error", "error": f"Error querying SmartAPI: {str(e)}"}
[docs] def _search_apis(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Search the SmartAPI registry by keyword or tag.""" query = (arguments.get("query") or "").strip() if not query: return { "status": "error", "error": "query is required, e.g. 'variant' or " "'tags.name:translator'.", } limit = arguments.get("limit") if not isinstance(limit, int) or limit <= 0: limit = 25 limit = min(limit, 100) response = requests.get( f"{SMARTAPI_BASE_URL}/query", params={ "q": query, "size": limit, "raw": 1, "fields": _SEARCH_FIELDS, }, timeout=self.timeout, ) response.raise_for_status() payload = response.json() hits = payload.get("hits") or [] if not hits: return { "status": "error", "error": f"No SmartAPI entries matching '{query}'.", } rows = [_summarize_hit(h) for h in hits] return { "status": "success", "data": rows, "metadata": { "query": query, "total_matching": payload.get("total"), "returned": len(rows), "note": "api_id (or slug, when present) is what get_api " "expects for the full record.", "source": "SmartAPI registry", }, }
[docs] def _get_api(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Fetch one API's full metadata by its registry id or slug.""" api_id = (arguments.get("api_id") or "").strip() if not api_id: return { "status": "error", "error": "api_id is required: a SmartAPI registry id or " "slug, e.g. 'myvariant'. Use search_apis to find one.", } response = requests.get( f"{SMARTAPI_BASE_URL}/metadata/{api_id}", timeout=self.timeout ) if response.status_code == 404: return { "status": "error", "error": f"No SmartAPI entry with id or slug '{api_id}'.", } response.raise_for_status() spec = response.json() info = spec.get("info") or {} servers = spec.get("servers") or [] paths = spec.get("paths") or {} endpoints: List[str] = list(paths.keys()) return { "status": "success", "data": { "title": info.get("title"), "description": info.get("description"), "version": info.get("version"), "contact": (info.get("contact") or {}).get("email"), "terms_of_service": info.get("termsOfService"), "base_urls": [s.get("url") for s in servers if s.get("url")], "tags": [ t.get("name") for t in spec.get("tags") or [] if t.get("name") ], "endpoint_count": len(endpoints), "endpoints": endpoints[:50], }, "metadata": { "api_id": api_id, "endpoints_truncated": len(endpoints) > 50, "source": "SmartAPI registry", }, }