Source code for tooluniverse.uniprot_ref_tool

# uniprot_ref_tool.py
"""
UniProt Reference Datasets API tool for ToolUniverse.

Provides access to UniProt's controlled vocabularies and reference datasets:
- Diseases: Curated disease definitions with OMIM/MeSH cross-references
- Keywords: Standardized annotation terms for protein function/process/location
- Proteomes: Reference proteome data with chromosome-level protein counts

API: https://rest.uniprot.org/
No authentication required.
"""

import requests
from typing import Dict, Any
from .base_tool import BaseTool
from .tool_registry import register_tool
from .uniprot_pagination import pagination_metadata

UNIPROT_BASE_URL = "https://rest.uniprot.org"


def _validated_search_size(arguments: Dict[str, Any]) -> tuple[int | None, str | None]:
    """Validate the shared UniProt REST search page size (official max 500)."""
    raw_size = arguments.get("size")
    raw_size = 10 if raw_size is None else raw_size
    try:
        if isinstance(raw_size, bool):
            raise ValueError
        size = int(raw_size)
        if isinstance(raw_size, float) and not raw_size.is_integer():
            raise ValueError
        if not 1 <= size <= 500:
            raise ValueError
    except (TypeError, ValueError):
        return None, "size must be an integer from 1 to 500"
    return size, None


[docs] @register_tool("UniProtRefTool") class UniProtRefTool(BaseTool): """ Tool for querying UniProt reference datasets (diseases, keywords, proteomes). UniProt maintains curated controlled vocabularies that standardize protein annotations across the database. This tool provides access to: - Disease vocabulary: 6K+ curated disease entries with cross-refs to OMIM, MeSH, MedGen - Keyword vocabulary: 1.2K+ standardized terms for biological process, function, etc. - Proteomes: Reference proteome summaries with assembly and component data No authentication required. """
[docs] def __init__(self, tool_config: Dict[str, Any]): super().__init__(tool_config) self.timeout = tool_config.get("timeout", 30) fields = tool_config.get("fields", {}) self.endpoint = fields.get("endpoint", "search_diseases")
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute the UniProt reference dataset API call.""" try: return self._query(arguments) except requests.exceptions.Timeout: return { "status": "error", "error": f"UniProt API timed out after {self.timeout}s", } except requests.exceptions.ConnectionError: return {"status": "error", "error": "Failed to connect to UniProt API"} except requests.exceptions.HTTPError as e: if e.response.status_code == 404: return {"status": "error", "error": "Resource not found in UniProt"} return { "status": "error", "error": f"UniProt API HTTP error: {e.response.status_code}", } except Exception as e: return { "status": "error", "error": f"Unexpected error querying UniProt: {str(e)}", }
[docs] def _query(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Route to appropriate UniProt reference endpoint.""" if self.endpoint == "search_diseases": return self._search_diseases(arguments) elif self.endpoint == "get_disease": return self._get_disease(arguments) elif self.endpoint == "search_keywords": return self._search_keywords(arguments) elif self.endpoint == "get_keyword": return self._get_keyword(arguments) elif self.endpoint == "get_proteome": return self._get_proteome(arguments) elif self.endpoint == "search_proteomes": return self._search_proteomes(arguments) else: return {"status": "error", "error": f"Unknown endpoint: {self.endpoint}"}
[docs] def _search_diseases(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Search UniProt controlled disease vocabulary.""" query = arguments.get("query", "") if not query: return {"status": "error", "error": "query parameter is required"} size, size_error = _validated_search_size(arguments) if size_error: return {"status": "error", "error": size_error} assert size is not None url = f"{UNIPROT_BASE_URL}/diseases/search" params = {"query": query, "size": size, "format": "json"} if arguments.get("cursor"): params["cursor"] = arguments["cursor"] response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() data = response.json() diseases = [] for r in data.get("results", []): xrefs = [] for xr in r.get("crossReferences", []): xrefs.append( { "database": xr.get("databaseType"), "id": xr.get("id"), } ) diseases.append( { "id": r.get("id"), "name": r.get("name"), "acronym": r.get("acronym"), "definition": r.get("definition"), "alternative_names": r.get("alternativeNames", []), "cross_references": xrefs, "reviewed_protein_count": r.get("statistics", {}).get( "reviewedProteinCount" ), } ) return { "status": "success", "data": diseases, "metadata": { "source": "UniProt Controlled Disease Vocabulary", "query": query, **pagination_metadata(response, len(diseases)), }, }
[docs] def _get_disease(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Get a specific disease entry by UniProt disease ID.""" disease_id = arguments.get("disease_id", "") if not disease_id: return {"status": "error", "error": "disease_id parameter is required"} url = f"{UNIPROT_BASE_URL}/diseases/{disease_id}" params = {"format": "json"} response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() r = response.json() xrefs = [] for xr in r.get("crossReferences", []): xrefs.append( { "database": xr.get("databaseType"), "id": xr.get("id"), "properties": xr.get("properties", []), } ) return { "status": "success", "data": { "id": r.get("id"), "name": r.get("name"), "acronym": r.get("acronym"), "definition": r.get("definition"), "alternative_names": r.get("alternativeNames", []), "cross_references": xrefs, "reviewed_protein_count": r.get("statistics", {}).get( "reviewedProteinCount" ), "unreviewed_protein_count": r.get("statistics", {}).get( "unreviewedProteinCount" ), }, "metadata": { "source": "UniProt Controlled Disease Vocabulary", }, }
[docs] def _search_keywords(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Search UniProt keyword controlled vocabulary.""" query = arguments.get("query", "") if not query: return {"status": "error", "error": "query parameter is required"} size, size_error = _validated_search_size(arguments) if size_error: return {"status": "error", "error": size_error} assert size is not None url = f"{UNIPROT_BASE_URL}/keywords/search" params = {"query": query, "size": size, "format": "json"} if arguments.get("cursor"): params["cursor"] = arguments["cursor"] response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() data = response.json() keywords = [] for r in data.get("results", []): kw = r.get("keyword", {}) keywords.append( { "id": kw.get("id"), "name": kw.get("name"), "category": r.get("category", {}).get("name") if isinstance(r.get("category"), dict) else r.get("category"), "definition": r.get("definition"), "reviewed_protein_count": r.get("statistics", {}).get( "reviewedProteinCount" ), } ) return { "status": "success", "data": keywords, "metadata": { "source": "UniProt Keyword Controlled Vocabulary", "query": query, **pagination_metadata(response, len(keywords)), }, }
[docs] def _get_keyword(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Get a specific keyword entry by UniProt keyword ID.""" keyword_id = arguments.get("keyword_id", "") if not keyword_id: return {"status": "error", "error": "keyword_id parameter is required"} url = f"{UNIPROT_BASE_URL}/keywords/{keyword_id}" params = {"format": "json"} response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() r = response.json() kw = r.get("keyword", {}) parents = [] for p in r.get("parents", []): pkw = p.get("keyword", {}) parents.append( { "id": pkw.get("id"), "name": pkw.get("name"), } ) go_mappings = [] for g in r.get("geneOntologies", []): go_mappings.append( { "go_id": g.get("goId"), "name": g.get("name"), } ) return { "status": "success", "data": { "id": kw.get("id"), "name": kw.get("name"), "category": r.get("category", {}).get("name") if isinstance(r.get("category"), dict) else r.get("category"), "definition": r.get("definition"), "parents": parents, "go_mappings": go_mappings, "reviewed_protein_count": r.get("statistics", {}).get( "reviewedProteinCount" ), "unreviewed_protein_count": r.get("statistics", {}).get( "unreviewedProteinCount" ), }, "metadata": { "source": "UniProt Keyword Controlled Vocabulary", }, }
[docs] def _get_proteome(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Get reference proteome information by UniProt proteome ID.""" proteome_id = arguments.get("proteome_id", "") if not proteome_id: return {"status": "error", "error": "proteome_id parameter is required"} url = f"{UNIPROT_BASE_URL}/proteomes/{proteome_id}" params = {"format": "json"} response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() r = response.json() tax = r.get("taxonomy", {}) all_components = r.get("components", []) total_proteins = sum((c.get("proteinCount") or 0) for c in all_components) component_offset = max(arguments.get("component_offset") or 0, 0) component_limit = min(max(arguments.get("component_limit") or 30, 1), 500) component_page = all_components[ component_offset : component_offset + component_limit ] components = [] for c in component_page: count = c.get("proteinCount", 0) or 0 genome_acc = None for xr in c.get("proteomeCrossReferences", []): if xr.get("database") == "GenomeAccession": genome_acc = xr.get("id") break components.append( { "name": c.get("name"), "protein_count": count, "genome_accession": genome_acc, } ) has_more = component_offset + len(components) < len(all_components) description = r.get("description") or "" return { "status": "success", "data": { "id": r.get("id"), "description": description, "description_length": len(description), "organism": { "scientific_name": tax.get("scientificName"), "common_name": tax.get("commonName"), "taxon_id": tax.get("taxonId"), }, "proteome_type": r.get("proteomeType"), "components": components, "total_protein_count": total_proteins, }, "metadata": { "source": "UniProt Proteomes", "total_components": len(all_components), "components_returned": len(components), "component_offset": component_offset, "components_truncated": has_more, "next_component_offset": ( component_offset + len(components) if has_more else None ), }, }
[docs] def _search_proteomes(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Search UniProt reference proteomes.""" query = arguments.get("query", "") if not query: return {"status": "error", "error": "query parameter is required"} size, size_error = _validated_search_size(arguments) if size_error: return {"status": "error", "error": size_error} assert size is not None url = f"{UNIPROT_BASE_URL}/proteomes/search" params = {"query": query, "size": size, "format": "json"} if arguments.get("cursor"): params["cursor"] = arguments["cursor"] response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() data = response.json() proteomes = [] for r in data.get("results", []): tax = r.get("taxonomy", {}) total_proteins = sum( (c.get("proteinCount") or 0) for c in r.get("components", []) ) proteomes.append( { "id": r.get("id"), "organism": tax.get("scientificName"), "common_name": tax.get("commonName"), "taxon_id": tax.get("taxonId"), "proteome_type": r.get("proteomeType"), "protein_count": total_proteins, } ) return { "status": "success", "data": proteomes, "metadata": { "source": "UniProt Proteomes", "query": query, **pagination_metadata(response, len(proteomes)), }, }