Source code for tooluniverse.reactome_interactors_tool

# reactome_interactors_tool.py
"""
Reactome Interactors API tool for ToolUniverse.

This tool provides access to curated protein-protein interactions from
Reactome's IntAct-derived interaction data. It enables discovery of
molecular interactors for any protein and pathway context for entities.

API: https://reactome.org/ContentService/
No authentication required.
"""

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

REACTOME_BASE_URL = "https://reactome.org/ContentService"


[docs] @register_tool("ReactomeInteractorsTool") class ReactomeInteractorsTool(BaseTool): """ Tool for querying protein interactors and entity pathways from Reactome. Reactome provides curated protein-protein interaction data derived from IntAct, with confidence scores and evidence counts. Also supports finding Reactome pathways associated with specific entities. Supports: get protein interactors, find pathways for entity, search Reactome entities. 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", "interactors")
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute the Reactome API call.""" try: return self._query(arguments) except requests.exceptions.Timeout: return { "status": "error", "error": f"Reactome API timed out after {self.timeout}s", } except requests.exceptions.ConnectionError: return {"status": "error", "error": "Failed to connect to Reactome API"} except requests.exceptions.HTTPError as e: return { "status": "error", "error": f"Reactome API HTTP error: {e.response.status_code}", } except Exception as e: return { "status": "error", "error": f"Unexpected error querying Reactome: {str(e)}", }
[docs] def _query(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Route to appropriate Reactome endpoint.""" if self.endpoint == "interactors": return self._get_interactors(arguments) elif self.endpoint == "entity_pathways": return self._get_entity_pathways(arguments) elif self.endpoint == "search_entity": return self._search_entity(arguments) else: return {"status": "error", "error": f"Unknown endpoint: {self.endpoint}"}
[docs] def _fetch_total_interactor_count(self, accession: str): """Return the true total interactor count, or None if unavailable. Fix-R18B-2/Feature-23C-2: the ``count`` field of the interactors ``/details`` response is the number of records on the page that was requested, NOT the size of the full result set -- live-verified for P04637, where page=1&pageSize=100 yields count=100 while page=1&pageSize=300 yields count=249. The dedicated ``/summary`` endpoint is therefore the only cheap source of a real total (live-verified: P04637 -> 249, P42345 -> 22). Returns None on any failure so that a page count is never passed off as a total. """ url = f"{REACTOME_BASE_URL}/interactors/static/molecule/{accession}/summary" try: response = requests.get(url, timeout=self.timeout) response.raise_for_status() entities = response.json().get("entities", []) except (requests.exceptions.RequestException, ValueError, AttributeError): return None if not entities: return None count = entities[0].get("count") return count if isinstance(count, int) else None
[docs] def _get_interactors(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Get protein-protein interactors for a UniProt accession.""" accession = arguments.get("accession", "") if not accession: return { "status": "error", "error": "accession parameter is required (UniProt accession, e.g., P04637)", } page_size = arguments.get("page_size") or 20 url = f"{REACTOME_BASE_URL}/interactors/static/molecule/{accession}/details" # The requested page_size is passed through unclamped: Reactome honours # any pageSize on this endpoint (live-verified: pageSize=300 for P04637 # returns all 249 interactors), so clamping here would silently truncate # results the caller explicitly asked for. params = {"page": 1, "pageSize": page_size} response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() data = response.json() total = self._fetch_total_interactor_count(accession) entities = data.get("entities", []) if not entities: return { "status": "success", "data": { "accession": accession, "total_interactors": total if total is not None else 0, "total_is_exact": total is not None, "returned_interactors": 0, "truncated": bool(total), "interactors": [], }, "metadata": {"source": "Reactome Interactors (IntAct)"}, } entity = entities[0] interactors = [] for i in entity.get("interactors", []): interactors.append( { "accession": i.get("acc"), "alias": i.get("alias"), "score": i.get("score"), "evidences": i.get("evidences"), } ) # Sort by score descending interactors.sort(key=lambda x: x.get("score") or 0, reverse=True) returned = len(interactors) truncated = total is not None and returned < total result = { "accession": entity.get("acc"), "total_interactors": total if total is not None else returned, "total_is_exact": total is not None, "returned_interactors": returned, "truncated": truncated, "interactors": interactors, } if truncated: result["note"] = ( f"Showing {returned} of {total} interactors " f"(page_size={page_size}). Re-run with page_size={total} " "to retrieve all of them." ) elif total is None: result["note"] = ( "The total interactor count is unavailable (Reactome summary " "endpoint did not respond), so total_interactors reports the " f"{returned} interactor(s) returned on this page and may be " "incomplete." ) return { "status": "success", "data": result, "metadata": { "source": "Reactome Interactors (IntAct)", "resource": data.get("resource"), }, }
[docs] def _get_entity_pathways(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Get Reactome pathways associated with a specific entity.""" entity_id = arguments.get("entity_id", "") if not entity_id: return { "status": "error", "error": "entity_id parameter is required (Reactome stable ID, e.g., R-HSA-199420)", } species = arguments.get("species") or 9606 url = f"{REACTOME_BASE_URL}/data/pathways/low/entity/{entity_id}" params = {"species": species} response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() data = response.json() pathways = [] if isinstance(data, list): for p in data: pathways.append( { "stable_id": p.get("stId"), "name": p.get("displayName"), "species": p.get("speciesName"), "is_disease": p.get("isInDisease", False), "has_diagram": p.get("hasDiagram", False), } ) return { "status": "success", "data": pathways, "metadata": { "source": "Reactome Content Service", "entity_id": entity_id, "species": species, "total_pathways": len(pathways), }, }
[docs] def _search_entity(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Search Reactome for entities (proteins, complexes, reactions).""" query = arguments.get("query", "") if not query: return {"status": "error", "error": "query parameter is required"} species = arguments.get("species") or "Homo sapiens" types = arguments.get("types") url = f"{REACTOME_BASE_URL}/search/query" params = { "query": query, "species": species, "cluster": "true", } if types: params["types"] = types response = requests.get(url, params=params, timeout=self.timeout) response.raise_for_status() data = response.json() results = [] for group in data.get("results", []): type_name = group.get("typeName", "") for entry in group.get("entries", []): # Strip HTML highlighting name = ( (entry.get("name") or "") .replace('<span class="highlighting" >', "") .replace("</span>", "") ) results.append( { "stable_id": entry.get("stId"), "name": name, "type": type_name, "species": entry.get("species"), "exact_type": entry.get("exactType"), "compartment_names": entry.get("compartmentNames"), } ) return { "status": "success", "data": results[:50], "metadata": { "source": "Reactome Content Service", "query": query, "total_matches": data.get("numberOfMatches", len(results)), "groups": data.get("numberOfGroups", 0), }, }