Source code for tooluniverse.nih_reporter_tool

# nih_reporter_tool.py
"""
NIH RePORTER tool for ToolUniverse.

RePORTER is NIH's public database of funded research: ~3 million grants
since 1985 with title, abstract, funding amount, institute, principal
investigator, and institution. ToolUniverse has no funding-landscape layer
today, only the science itself (papers, structures, sequences).

The API's text search parameter is `advanced_text_search` (a differently
named, differently structured sibling of the more obviously named
`text_search`, which is silently ignored and matches every record).

API: https://api.reporter.nih.gov/v2/projects/search
No authentication required.
"""

from typing import Any, Dict, List

import requests

from .base_tool import BaseTool
from .tool_registry import register_tool

REPORTER_URL = "https://api.reporter.nih.gov/v2/projects/search"

_SEARCH_FIELDS = "projecttitle,terms,abstracttext"

_INCLUDE_FIELDS = [
    "ProjectTitle",
    "ProjectNum",
    "ApplId",
    "FiscalYear",
    "AwardAmount",
    "Organization",
    "ContactPiName",
    "AgencyIcAdmin",
    "ProjectStartDate",
    "ProjectEndDate",
    "AbstractText",
]


def _summarize(project: Dict[str, Any]) -> Dict[str, Any]:
    """Condense one RePORTER project record."""
    org = project.get("organization") or {}
    agency = project.get("agency_ic_admin") or {}
    return {
        "project_num": project.get("project_num"),
        "appl_id": project.get("appl_id"),
        "title": project.get("project_title"),
        "principal_investigator": project.get("contact_pi_name"),
        "organization": org.get("org_name"),
        "funding_institute": agency.get("name"),
        "fiscal_year": project.get("fiscal_year"),
        "award_amount": project.get("award_amount"),
        "project_start_date": project.get("project_start_date"),
        "project_end_date": project.get("project_end_date"),
        "abstract": project.get("abstract_text"),
    }


[docs] @register_tool("NIHReporterTool") class NIHReporterTool(BaseTool): """ Tool for querying NIH RePORTER, the public database of NIH-funded research projects. Supports full-text search over titles, abstracts, and terms with optional fiscal-year and organization filters, and direct lookup of one project by its project number or application id. No authentication required. """
[docs] def __init__(self, tool_config: Dict[str, Any]): super().__init__(tool_config) self.timeout = tool_config.get("timeout", 45) self.operation = tool_config.get("fields", {}).get( "operation", "search_projects" )
[docs] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Execute the RePORTER lookup.""" try: if self.operation == "search_projects": return self._search_projects(arguments) if self.operation == "get_project": return self._get_project(arguments) return { "status": "error", "error": f"Unknown operation: {self.operation}", } except requests.exceptions.Timeout: return { "status": "error", "error": f"RePORTER request timed out after {self.timeout}s", } except requests.exceptions.ConnectionError: return { "status": "error", "error": "Failed to connect to NIH RePORTER. 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"RePORTER returned HTTP {code}"} except ValueError: return { "status": "error", "error": "RePORTER returned a non-JSON response", } except Exception as e: return {"status": "error", "error": f"Error querying RePORTER: {str(e)}"}
[docs] def _search_projects(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Full-text search over NIH-funded project titles/abstracts/terms.""" query = (arguments.get("query") or "").strip() if not query: return { "status": "error", "error": "query is required, e.g. 'CRISPR gene editing'.", } limit = arguments.get("limit") if not isinstance(limit, int) or limit <= 0: limit = 25 limit = min(limit, 100) criteria: Dict[str, Any] = { "advanced_text_search": { "operator": "and", "search_field": _SEARCH_FIELDS, "search_text": query, } } fiscal_year = arguments.get("fiscal_year") if isinstance(fiscal_year, int): criteria["fiscal_years"] = [fiscal_year] organization = (arguments.get("organization") or "").strip() if organization: criteria["org_names"] = [organization] response = requests.post( REPORTER_URL, json={ "criteria": criteria, "include_fields": _INCLUDE_FIELDS, "limit": limit, }, timeout=self.timeout, ) response.raise_for_status() payload = response.json() results = payload.get("results") or [] if not results: return { "status": "error", "error": f"No NIH-funded projects matching '{query}'" + (f" in fiscal year {fiscal_year}" if fiscal_year else "") + (f" at '{organization}'" if organization else "") + ".", } rows = [_summarize(p) for p in results] return { "status": "success", "data": rows, "metadata": { "query": query, "fiscal_year": fiscal_year, "organization": organization or None, "total_matching": (payload.get("meta") or {}).get("total"), "returned": len(rows), "note": "project_num or appl_id can be used with get_project " "for the full record.", "source": "NIH RePORTER", }, }
[docs] def _get_project(self, arguments: Dict[str, Any]) -> Dict[str, Any]: """Fetch one project by its project number or application id.""" project_num = (arguments.get("project_num") or "").strip() appl_id = arguments.get("appl_id") if not project_num and not appl_id: return { "status": "error", "error": "Provide either project_num (e.g. '5R21EB036298-03') " "or appl_id (e.g. 11326711).", } criteria: Dict[str, Any] = {} if project_num: criteria["project_nums"] = [project_num] elif isinstance(appl_id, int): criteria["appl_ids"] = [appl_id] response = requests.post( REPORTER_URL, json={ "criteria": criteria, "include_fields": _INCLUDE_FIELDS, "limit": 1, }, timeout=self.timeout, ) response.raise_for_status() payload = response.json() results = payload.get("results") or [] if not results: identifier = project_num or appl_id return { "status": "error", "error": f"No NIH RePORTER project found for '{identifier}'.", } return { "status": "success", "data": _summarize(results[0]), "metadata": { "project_num": project_num or None, "appl_id": appl_id, "source": "NIH RePORTER", }, }