Source code for tooluniverse.osf_preprints_tool

import requests
from .base_tool import BaseTool
from .tool_registry import register_tool


[docs] @register_tool("OSFPreprintsTool") class OSFPreprintsTool(BaseTool): """ Search OSF Preprints via OSF API v2 filters. Parameters (arguments): query (str): Query string max_results (int): Max results (default 10, max 100) provider (str): Optional preprint provider (e.g., 'osf', 'psyarxiv') """
[docs] def __init__(self, tool_config): super().__init__(tool_config) self.base_url = "https://api.osf.io/v2/preprints/"
[docs] def run(self, arguments=None): arguments = arguments or {} query = arguments.get("query") max_results = arguments.get("max_results", 10) page = arguments.get("page", 1) provider = arguments.get("provider") search_field = arguments.get("search_field", "title") if not isinstance(query, str) or not query.strip(): return {"status": "error", "error": "`query` parameter is required."} if ( isinstance(max_results, bool) or not isinstance(max_results, int) or not 1 <= max_results <= 100 ): return { "status": "error", "error": "max_results must be an integer between 1 and 100", "parameter": "max_results", } if isinstance(page, bool) or not isinstance(page, int) or page < 1: return { "status": "error", "error": "page must be an integer greater than or equal to 1", "parameter": "page", } if search_field not in {"title", "description"}: return { "status": "error", "error": "search_field must be one of: title, description", "parameter": "search_field", } params = { "page[size]": max_results, # OSF uses the bare `page` parameter. `page[number]` is silently # ignored even though this is otherwise a JSON:API response. "page": page, f"filter[{search_field}]": query, } if provider: params["filter[provider]"] = provider try: resp = requests.get(self.base_url, params=params, timeout=20) resp.raise_for_status() data = resp.json() except requests.RequestException as e: return { "status": "error", "error": "Network/API error calling OSF", "reason": str(e), } except ValueError: ct = resp.headers.get("content-type", "") return { "status": "error", "error": "Failed to decode OSF response as JSON", "content_type": ct, "response_snippet": resp.text[:200], "retryable": "text/html" in ct or resp.text.lstrip().startswith("<"), "suggestion": "OSF API may be under maintenance. Retry in a few minutes.", } results = [] for item in data.get("data", []): attrs = item.get("attributes", {}) title = attrs.get("title") date_published = attrs.get("date_published") is_published = attrs.get("is_published") doi = attrs.get("doi") links_obj = item.get("links", {}) url = links_obj.get("html") or links_obj.get("self") doi = attrs.get("doi") if not doi: doi_url = links_obj.get("preprint_doi") if isinstance(doi_url, str): doi = doi_url.removeprefix("https://doi.org/") relationships = item.get("relationships", {}) provider_data = ( relationships.get("provider", {}).get("data", {}) if isinstance(relationships, dict) else {} ) results.append( { "id": item.get("id"), "title": title, "description": attrs.get("description"), "date_published": date_published, "published": is_published, "doi": doi, "url": url, "provider": provider_data.get("id"), "source": "OSF Preprints", } ) links = data.get("links", {}) if isinstance(data, dict) else {} link_meta = links.get("meta", {}) if isinstance(links, dict) else {} total = link_meta.get("total") if isinstance(link_meta, dict) else None total_is_exact = isinstance(total, int) and not isinstance(total, bool) returned = len(results) next_url = links.get("next") if isinstance(links, dict) else None has_more = isinstance(next_url, str) and bool(next_url) per_page = link_meta.get("per_page", max_results) if isinstance(per_page, bool) or not isinstance(per_page, int) or per_page < 1: per_page = max_results total_pages = (total + per_page - 1) // per_page if total_is_exact else None return { "status": "success", "data": results, "metadata": { "returned": returned, "total_results": total if total_is_exact else None, "total_results_is_exact": total_is_exact, "page": page, "page_size": per_page, "total_pages": total_pages, "has_more": has_more, "next_page": page + 1 if has_more else None, "next_url": next_url if has_more else None, "truncated": page > 1 or has_more, "result_set_complete": bool( total_is_exact and page == 1 and returned == total ), "search_field": search_field, "provider_filter": provider, "pagination_scope": "osf_bare_page_jsonapi_total", }, }