tooluniverse 1.0.9.1__py3-none-any.whl → 1.0.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tooluniverse might be problematic. Click here for more details.

Files changed (191) hide show
  1. tooluniverse/__init__.py +57 -1
  2. tooluniverse/admetai_tool.py +1 -1
  3. tooluniverse/agentic_tool.py +65 -17
  4. tooluniverse/base_tool.py +19 -8
  5. tooluniverse/blast_tool.py +132 -0
  6. tooluniverse/boltz_tool.py +3 -3
  7. tooluniverse/cache/result_cache_manager.py +167 -12
  8. tooluniverse/cbioportal_tool.py +42 -0
  9. tooluniverse/clinvar_tool.py +268 -74
  10. tooluniverse/compose_scripts/drug_safety_analyzer.py +1 -1
  11. tooluniverse/compose_scripts/multi_agent_literature_search.py +1 -1
  12. tooluniverse/compose_scripts/output_summarizer.py +4 -4
  13. tooluniverse/compose_scripts/tool_discover.py +1941 -443
  14. tooluniverse/compose_scripts/tool_graph_composer.py +1 -1
  15. tooluniverse/compose_scripts/tool_metadata_generator.py +1 -1
  16. tooluniverse/compose_tool.py +9 -9
  17. tooluniverse/core_tool.py +2 -2
  18. tooluniverse/ctg_tool.py +4 -4
  19. tooluniverse/custom_tool.py +1 -1
  20. tooluniverse/data/agentic_tools.json +0 -370
  21. tooluniverse/data/alphafold_tools.json +6 -6
  22. tooluniverse/data/blast_tools.json +112 -0
  23. tooluniverse/data/cbioportal_tools.json +87 -0
  24. tooluniverse/data/clinvar_tools.json +235 -0
  25. tooluniverse/data/compose_tools.json +0 -89
  26. tooluniverse/data/dbsnp_tools.json +275 -0
  27. tooluniverse/data/emdb_tools.json +61 -0
  28. tooluniverse/data/ensembl_tools.json +259 -0
  29. tooluniverse/data/file_download_tools.json +275 -0
  30. tooluniverse/data/geo_tools.json +200 -48
  31. tooluniverse/data/gnomad_tools.json +109 -0
  32. tooluniverse/data/gtopdb_tools.json +68 -0
  33. tooluniverse/data/gwas_tools.json +32 -0
  34. tooluniverse/data/interpro_tools.json +199 -0
  35. tooluniverse/data/jaspar_tools.json +70 -0
  36. tooluniverse/data/kegg_tools.json +356 -0
  37. tooluniverse/data/mpd_tools.json +87 -0
  38. tooluniverse/data/ols_tools.json +314 -0
  39. tooluniverse/data/package_discovery_tools.json +64 -0
  40. tooluniverse/data/packages/categorized_tools.txt +0 -1
  41. tooluniverse/data/packages/machine_learning_tools.json +0 -47
  42. tooluniverse/data/paleobiology_tools.json +91 -0
  43. tooluniverse/data/pride_tools.json +62 -0
  44. tooluniverse/data/pypi_package_inspector_tools.json +158 -0
  45. tooluniverse/data/python_executor_tools.json +341 -0
  46. tooluniverse/data/regulomedb_tools.json +50 -0
  47. tooluniverse/data/remap_tools.json +89 -0
  48. tooluniverse/data/screen_tools.json +89 -0
  49. tooluniverse/data/tool_discovery_agents.json +428 -0
  50. tooluniverse/data/tool_discovery_agents.json.backup +1343 -0
  51. tooluniverse/data/uniprot_tools.json +77 -0
  52. tooluniverse/data/web_search_tools.json +250 -0
  53. tooluniverse/data/worms_tools.json +55 -0
  54. tooluniverse/dataset_tool.py +2 -2
  55. tooluniverse/dbsnp_tool.py +196 -58
  56. tooluniverse/default_config.py +36 -3
  57. tooluniverse/emdb_tool.py +30 -0
  58. tooluniverse/enrichr_tool.py +14 -14
  59. tooluniverse/ensembl_tool.py +140 -47
  60. tooluniverse/execute_function.py +594 -29
  61. tooluniverse/extended_hooks.py +4 -4
  62. tooluniverse/file_download_tool.py +269 -0
  63. tooluniverse/gene_ontology_tool.py +1 -1
  64. tooluniverse/generate_tools.py +3 -3
  65. tooluniverse/geo_tool.py +81 -28
  66. tooluniverse/gnomad_tool.py +100 -52
  67. tooluniverse/gtopdb_tool.py +41 -0
  68. tooluniverse/humanbase_tool.py +10 -10
  69. tooluniverse/interpro_tool.py +72 -0
  70. tooluniverse/jaspar_tool.py +30 -0
  71. tooluniverse/kegg_tool.py +230 -0
  72. tooluniverse/logging_config.py +2 -2
  73. tooluniverse/mcp_client_tool.py +57 -129
  74. tooluniverse/mcp_integration.py +52 -49
  75. tooluniverse/mcp_tool_registry.py +147 -528
  76. tooluniverse/mpd_tool.py +42 -0
  77. tooluniverse/ncbi_eutils_tool.py +96 -0
  78. tooluniverse/ols_tool.py +435 -0
  79. tooluniverse/openalex_tool.py +8 -8
  80. tooluniverse/openfda_tool.py +2 -2
  81. tooluniverse/output_hook.py +15 -15
  82. tooluniverse/package_discovery_tool.py +217 -0
  83. tooluniverse/package_tool.py +1 -1
  84. tooluniverse/paleobiology_tool.py +30 -0
  85. tooluniverse/pmc_tool.py +2 -2
  86. tooluniverse/pride_tool.py +30 -0
  87. tooluniverse/pypi_package_inspector_tool.py +593 -0
  88. tooluniverse/python_executor_tool.py +711 -0
  89. tooluniverse/regulomedb_tool.py +30 -0
  90. tooluniverse/remap_tool.py +44 -0
  91. tooluniverse/remote/boltz/boltz_mcp_server.py +1 -1
  92. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +3 -3
  93. tooluniverse/remote/immune_compass/compass_tool.py +3 -3
  94. tooluniverse/remote/pinnacle/pinnacle_tool.py +2 -2
  95. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +3 -3
  96. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +3 -3
  97. tooluniverse/remote_tool.py +4 -4
  98. tooluniverse/screen_tool.py +44 -0
  99. tooluniverse/scripts/filter_tool_files.py +2 -2
  100. tooluniverse/smcp.py +93 -12
  101. tooluniverse/smcp_server.py +100 -21
  102. tooluniverse/space/__init__.py +46 -0
  103. tooluniverse/space/loader.py +133 -0
  104. tooluniverse/space/validator.py +353 -0
  105. tooluniverse/tool_finder_embedding.py +5 -3
  106. tooluniverse/tool_finder_keyword.py +12 -10
  107. tooluniverse/tool_finder_llm.py +12 -8
  108. tooluniverse/tools/{UCSC_get_genes_by_region.py → BLAST_nucleotide_search.py} +22 -26
  109. tooluniverse/tools/BLAST_protein_search.py +63 -0
  110. tooluniverse/tools/ClinVar_search_variants.py +26 -15
  111. tooluniverse/tools/CodeQualityAnalyzer.py +3 -3
  112. tooluniverse/tools/EMDB_get_structure.py +46 -0
  113. tooluniverse/tools/GtoPdb_get_targets.py +52 -0
  114. tooluniverse/tools/InterPro_get_domain_details.py +46 -0
  115. tooluniverse/tools/InterPro_get_protein_domains.py +49 -0
  116. tooluniverse/tools/InterPro_search_domains.py +52 -0
  117. tooluniverse/tools/JASPAR_get_transcription_factors.py +52 -0
  118. tooluniverse/tools/MPD_get_phenotype_data.py +59 -0
  119. tooluniverse/tools/PRIDE_search_proteomics.py +52 -0
  120. tooluniverse/tools/PackageAnalyzer.py +55 -0
  121. tooluniverse/tools/Paleobiology_get_fossils.py +52 -0
  122. tooluniverse/tools/PyPIPackageInspector.py +59 -0
  123. tooluniverse/tools/ReMap_get_transcription_factor_binding.py +59 -0
  124. tooluniverse/tools/ReferenceInfoAnalyzer.py +55 -0
  125. tooluniverse/tools/RegulomeDB_query_variant.py +46 -0
  126. tooluniverse/tools/SCREEN_get_regulatory_elements.py +59 -0
  127. tooluniverse/tools/{ArgumentDescriptionOptimizer.py → TestResultsAnalyzer.py} +13 -13
  128. tooluniverse/tools/ToolDiscover.py +11 -11
  129. tooluniverse/tools/UniProt_id_mapping.py +63 -0
  130. tooluniverse/tools/UniProt_search.py +63 -0
  131. tooluniverse/tools/UnifiedToolGenerator.py +59 -0
  132. tooluniverse/tools/WoRMS_search_species.py +49 -0
  133. tooluniverse/tools/XMLToolOptimizer.py +55 -0
  134. tooluniverse/tools/__init__.py +119 -29
  135. tooluniverse/tools/_shared_client.py +3 -3
  136. tooluniverse/tools/alphafold_get_annotations.py +3 -3
  137. tooluniverse/tools/alphafold_get_prediction.py +3 -3
  138. tooluniverse/tools/alphafold_get_summary.py +3 -3
  139. tooluniverse/tools/cBioPortal_get_cancer_studies.py +46 -0
  140. tooluniverse/tools/cBioPortal_get_mutations.py +52 -0
  141. tooluniverse/tools/{gnomAD_query_variant.py → clinvar_get_clinical_significance.py} +8 -11
  142. tooluniverse/tools/clinvar_get_variant_details.py +49 -0
  143. tooluniverse/tools/dbSNP_get_variant_by_rsid.py +7 -7
  144. tooluniverse/tools/dbsnp_get_frequencies.py +46 -0
  145. tooluniverse/tools/dbsnp_search_by_gene.py +52 -0
  146. tooluniverse/tools/download_binary_file.py +66 -0
  147. tooluniverse/tools/download_file.py +71 -0
  148. tooluniverse/tools/download_text_content.py +55 -0
  149. tooluniverse/tools/dynamic_package_discovery.py +59 -0
  150. tooluniverse/tools/ensembl_get_sequence.py +52 -0
  151. tooluniverse/tools/{Ensembl_lookup_gene_by_symbol.py → ensembl_get_variants.py} +11 -11
  152. tooluniverse/tools/ensembl_lookup_gene.py +46 -0
  153. tooluniverse/tools/geo_get_dataset_info.py +46 -0
  154. tooluniverse/tools/geo_get_sample_info.py +46 -0
  155. tooluniverse/tools/geo_search_datasets.py +67 -0
  156. tooluniverse/tools/gnomad_get_gene_constraints.py +49 -0
  157. tooluniverse/tools/kegg_find_genes.py +52 -0
  158. tooluniverse/tools/kegg_get_gene_info.py +46 -0
  159. tooluniverse/tools/kegg_get_pathway_info.py +46 -0
  160. tooluniverse/tools/kegg_list_organisms.py +44 -0
  161. tooluniverse/tools/kegg_search_pathway.py +46 -0
  162. tooluniverse/tools/ols_find_similar_terms.py +63 -0
  163. tooluniverse/tools/{get_hyperopt_info.py → ols_get_ontology_info.py} +13 -10
  164. tooluniverse/tools/ols_get_term_ancestors.py +67 -0
  165. tooluniverse/tools/ols_get_term_children.py +67 -0
  166. tooluniverse/tools/{TestCaseGenerator.py → ols_get_term_info.py} +12 -9
  167. tooluniverse/tools/{CodeOptimizer.py → ols_search_ontologies.py} +22 -14
  168. tooluniverse/tools/ols_search_terms.py +71 -0
  169. tooluniverse/tools/python_code_executor.py +79 -0
  170. tooluniverse/tools/python_script_runner.py +79 -0
  171. tooluniverse/tools/web_api_documentation_search.py +63 -0
  172. tooluniverse/tools/web_search.py +71 -0
  173. tooluniverse/uniprot_tool.py +219 -16
  174. tooluniverse/url_tool.py +19 -1
  175. tooluniverse/uspto_tool.py +1 -1
  176. tooluniverse/utils.py +12 -12
  177. tooluniverse/web_search_tool.py +229 -0
  178. tooluniverse/worms_tool.py +64 -0
  179. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/METADATA +8 -3
  180. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/RECORD +184 -92
  181. tooluniverse/data/genomics_tools.json +0 -174
  182. tooluniverse/tools/ToolDescriptionOptimizer.py +0 -67
  183. tooluniverse/tools/ToolImplementationGenerator.py +0 -67
  184. tooluniverse/tools/ToolOptimizer.py +0 -59
  185. tooluniverse/tools/ToolSpecificationGenerator.py +0 -67
  186. tooluniverse/tools/ToolSpecificationOptimizer.py +0 -63
  187. tooluniverse/ucsc_tool.py +0 -60
  188. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/WHEEL +0 -0
  189. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/entry_points.txt +0 -0
  190. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/licenses/LICENSE +0 -0
  191. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,42 @@
1
+ import requests
2
+ from typing import Any, Dict
3
+ from .base_tool import BaseTool
4
+ from .tool_registry import register_tool
5
+
6
+
7
+ @register_tool("MPDRESTTool")
8
+ class MPDRESTTool(BaseTool):
9
+ def __init__(self, tool_config: Dict):
10
+ super().__init__(tool_config)
11
+ self.session = requests.Session()
12
+ self.session.headers.update({"Accept": "application/json"})
13
+ self.timeout = 30
14
+
15
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
16
+ try:
17
+ # Use ENCODE as alternative data source for biological samples
18
+ strain = arguments.get("strain", "C57BL/6J")
19
+ limit = arguments.get("limit", 5)
20
+
21
+ # Build ENCODE API URL for experiments
22
+ # Query for general experiments as MPD alternative
23
+ url = f"https://www.encodeproject.org/search/?type=Experiment&format=json&limit={limit}"
24
+
25
+ response = self.session.get(url, timeout=self.timeout)
26
+ response.raise_for_status()
27
+
28
+ # Parse JSON response
29
+ data = response.json()
30
+
31
+ return {
32
+ "status": "success",
33
+ "data": data,
34
+ "url": url,
35
+ "query_info": {
36
+ "strain": strain,
37
+ "limit": limit,
38
+ "data_source": "ENCODE (MPD alternative)",
39
+ },
40
+ }
41
+ except Exception as e:
42
+ return {"status": "error", "error": f"MPD API error: {str(e)}"}
@@ -0,0 +1,96 @@
1
+ """
2
+ NCBI E-utilities Tool with Rate Limiting
3
+
4
+ This module provides a base class for NCBI E-utilities API tools with
5
+ built-in rate limiting and retry logic to handle 429 errors.
6
+ """
7
+
8
+ import time
9
+ import requests
10
+ from typing import Dict, Any, Optional
11
+ from .base_tool import BaseTool
12
+
13
+
14
+ class NCBIEUtilsTool(BaseTool):
15
+ """Base class for NCBI E-utilities tools with rate limiting."""
16
+
17
+ def __init__(self, tool_config):
18
+ super().__init__(tool_config)
19
+ self.base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
20
+ self.last_request_time = 0
21
+ self.min_interval = 0.34 # ~3 requests/second (NCBI limit without API key)
22
+ self.max_retries = 3
23
+ self.initial_retry_delay = 1
24
+ self.session = requests.Session()
25
+ self.session.headers.update(
26
+ {"Accept": "application/json", "User-Agent": "ToolUniverse/1.0"}
27
+ )
28
+ self.timeout = 30
29
+
30
+ def _make_request(
31
+ self, endpoint: str, params: Optional[Dict] = None
32
+ ) -> Dict[str, Any]:
33
+ """Make request with rate limiting and retry logic."""
34
+ url = f"{self.base_url}{endpoint}"
35
+
36
+ for attempt in range(self.max_retries):
37
+ # Rate limiting
38
+ elapsed = time.time() - self.last_request_time
39
+ if elapsed < self.min_interval:
40
+ time.sleep(self.min_interval - elapsed)
41
+
42
+ try:
43
+ response = self.session.get(url, params=params, timeout=self.timeout)
44
+ self.last_request_time = time.time()
45
+ response.raise_for_status()
46
+
47
+ # Try to parse JSON response
48
+ try:
49
+ data = response.json()
50
+ except ValueError:
51
+ # If not JSON, return text
52
+ data = response.text
53
+
54
+ return {
55
+ "status": "success",
56
+ "data": data,
57
+ "url": url,
58
+ "content_type": response.headers.get(
59
+ "content-type", "application/json"
60
+ ),
61
+ }
62
+
63
+ except requests.exceptions.HTTPError as e:
64
+ if e.response.status_code == 429 and attempt < self.max_retries - 1:
65
+ # Exponential backoff for rate limiting
66
+ delay = self.initial_retry_delay * (2**attempt)
67
+ print(
68
+ f"Rate limited, retrying in {delay} seconds... (attempt {attempt + 1}/{self.max_retries})"
69
+ )
70
+ time.sleep(delay)
71
+ continue
72
+ else:
73
+ return {
74
+ "status": "error",
75
+ "error": f"NCBI E-utilities API request failed: {str(e)}",
76
+ "url": url,
77
+ "status_code": (
78
+ e.response.status_code if hasattr(e, "response") else None
79
+ ),
80
+ }
81
+ except requests.exceptions.RequestException as e:
82
+ return {
83
+ "status": "error",
84
+ "error": f"NCBI E-utilities API request failed: {str(e)}",
85
+ "url": url,
86
+ }
87
+
88
+ return {
89
+ "status": "error",
90
+ "error": f"NCBI E-utilities API request failed after {self.max_retries} attempts",
91
+ "url": url,
92
+ }
93
+
94
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
95
+ """Execute the tool with given arguments."""
96
+ return self._make_request(self.endpoint, arguments)
@@ -0,0 +1,435 @@
1
+ """OLS API tool for ToolUniverse.
2
+
3
+ This module exposes the Ontology Lookup Service (OLS) endpoints that were
4
+ previously available through the dedicated MCP server. The MCP tooling has been
5
+ adapted into a synchronous local tool that fits the ToolUniverse runtime.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import urllib.parse
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ import requests
14
+ from pydantic import BaseModel, Field, HttpUrl, ValidationError
15
+
16
+ from .base_tool import BaseTool
17
+ from .tool_registry import register_tool
18
+
19
+ OLS_BASE_URL = "https://www.ebi.ac.uk/ols4"
20
+ REQUEST_TIMEOUT = 30.0
21
+
22
+
23
+ def url_encode_iri(iri: str) -> str:
24
+ """Double URL encode an IRI as required by the OLS API."""
25
+
26
+ return urllib.parse.quote(urllib.parse.quote(iri, safe=""), safe="")
27
+
28
+
29
+ class OntologyInfo(BaseModel):
30
+ """Description of a single ontology entry in OLS."""
31
+
32
+ id: str = Field(
33
+ ..., description="Unique identifier for the ontology", alias="ontologyId"
34
+ )
35
+ title: str = Field(..., description="Name of the ontology")
36
+ version: Optional[str] = Field(None, description="Version of the ontology")
37
+ description: Optional[str] = Field(None, description="Description of the ontology")
38
+ domain: Optional[str] = Field(None, description="Domain of the ontology")
39
+ homepage: Optional[HttpUrl] = Field(None, description="URL for the ontology")
40
+ preferred_prefix: Optional[str] = Field(
41
+ None, description="Preferred prefix for the ontology", alias="preferredPrefix"
42
+ )
43
+ number_of_terms: Optional[int] = Field(
44
+ None, description="Number of terms in the ontology"
45
+ )
46
+ number_of_classes: Optional[int] = Field(
47
+ None, description="Number of classes in the ontology", alias="numberOfClasses"
48
+ )
49
+ repository: Optional[HttpUrl] = Field(
50
+ None, description="Repository URL for the ontology"
51
+ )
52
+
53
+
54
+ class PagedResponse(BaseModel):
55
+ """Base structure for paginated responses returned by OLS."""
56
+
57
+ total_elements: int = Field(
58
+ 0, description="Total number of items", alias="totalElements"
59
+ )
60
+ page: int = Field(0, description="Current page number")
61
+ size: int = Field(
62
+ 20, description="Number of items in current page", alias="numElements"
63
+ )
64
+ total_pages: int = Field(0, description="Total number of pages", alias="totalPages")
65
+
66
+
67
+ class OntologySearchResponse(PagedResponse):
68
+ """Paginated collection of ontologies returned by the search endpoint."""
69
+
70
+ ontologies: List[OntologyInfo] = Field(
71
+ ..., description="List of ontologies matching the search criteria"
72
+ )
73
+
74
+
75
+ class TermInfo(BaseModel):
76
+ """Basic term representation returned by OLS."""
77
+
78
+ model_config = {"populate_by_name": True}
79
+
80
+ iri: HttpUrl = Field(..., description="IRI of the term")
81
+ ontology_name: str = Field(
82
+ ...,
83
+ description="Name of the ontology containing the term",
84
+ alias="ontologyName",
85
+ )
86
+ short_form: str = Field(
87
+ ..., description="Short form identifier for the term", alias="shortForm"
88
+ )
89
+ label: str = Field(..., description="Human-readable label for the term")
90
+ obo_id: Optional[str] = Field(
91
+ None, description="OBOLibrary ID for the term", alias="oboId"
92
+ )
93
+ is_obsolete: Optional[bool] = Field(
94
+ False, description="Indicates if the term is obsolete", alias="isObsolete"
95
+ )
96
+
97
+
98
+ class TermSearchResponse(PagedResponse):
99
+ """Paginated set of OLS terms."""
100
+
101
+ num_found: int = Field(
102
+ 0, description="Total number of terms found", alias="numFound"
103
+ )
104
+ terms: List[TermInfo] = Field(
105
+ ..., description="List of terms matching the search criteria"
106
+ )
107
+
108
+
109
+ class DetailedTermInfo(TermInfo):
110
+ """Extended term details in OLS."""
111
+
112
+ description: Optional[List[str]] = Field(None, description="Definition of the term")
113
+ synonyms: Optional[List[str]] = Field(
114
+ None, description="List of synonyms for the term"
115
+ )
116
+
117
+
118
+ @register_tool("OLSTool")
119
+ class OLSTool(BaseTool):
120
+ """Interact with the EMBL-EBI Ontology Lookup Service (OLS) REST API."""
121
+
122
+ _OPERATIONS = {
123
+ "search_terms": "_handle_search_terms",
124
+ "get_ontology_info": "_handle_get_ontology_info",
125
+ "search_ontologies": "_handle_search_ontologies",
126
+ "get_term_info": "_handle_get_term_info",
127
+ "get_term_children": "_handle_get_term_children",
128
+ "get_term_ancestors": "_handle_get_term_ancestors",
129
+ "find_similar_terms": "_handle_find_similar_terms",
130
+ }
131
+
132
+ def __init__(self, tool_config):
133
+ super().__init__(tool_config)
134
+ self.base_url = tool_config.get("base_url", OLS_BASE_URL).rstrip("/")
135
+ self.timeout = tool_config.get("timeout", REQUEST_TIMEOUT)
136
+ self.session = requests.Session()
137
+
138
+ def __del__(self):
139
+ try:
140
+ self.session.close()
141
+ except Exception:
142
+ pass
143
+
144
+ def run(self, arguments=None, **_: Any):
145
+ """Dispatch the requested OLS operation."""
146
+
147
+ arguments = arguments or {}
148
+ operation = arguments.get("operation")
149
+ if not operation:
150
+ return {
151
+ "error": "`operation` argument is required.",
152
+ "available_operations": sorted(self._OPERATIONS.keys()),
153
+ }
154
+
155
+ handler_name = self._OPERATIONS.get(operation)
156
+ if not handler_name:
157
+ return {
158
+ "error": f"Unsupported operation '{operation}'.",
159
+ "available_operations": sorted(self._OPERATIONS.keys()),
160
+ }
161
+
162
+ handler = getattr(self, handler_name)
163
+ try:
164
+ return handler(arguments)
165
+ except requests.RequestException as exc:
166
+ return {"error": "OLS API request failed.", "details": str(exc)}
167
+ except ValidationError as exc:
168
+ return {
169
+ "error": "Failed to validate OLS response.",
170
+ "details": exc.errors(),
171
+ }
172
+
173
+ def _handle_search_terms(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
174
+ query = arguments.get("query")
175
+ if not query:
176
+ return {"error": "`query` parameter is required for `search_terms`."}
177
+
178
+ rows = int(arguments.get("rows", 10))
179
+ ontology = arguments.get("ontology")
180
+ exact_match = bool(arguments.get("exact_match", False))
181
+ include_obsolete = bool(arguments.get("include_obsolete", False))
182
+
183
+ params = {
184
+ "q": query,
185
+ "rows": rows,
186
+ "start": 0,
187
+ "exact": exact_match,
188
+ "obsoletes": include_obsolete,
189
+ }
190
+ if ontology:
191
+ params["ontology"] = ontology
192
+
193
+ data = self._get_json("/api/search", params=params)
194
+ formatted = self._format_term_collection(data, rows)
195
+ formatted["query"] = query
196
+ formatted["filters"] = {
197
+ "ontology": ontology,
198
+ "exact_match": exact_match,
199
+ "include_obsolete": include_obsolete,
200
+ }
201
+ return formatted
202
+
203
+ def _handle_get_ontology_info(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
204
+ ontology_id = arguments.get("ontology_id")
205
+ if not ontology_id:
206
+ return {
207
+ "error": "`ontology_id` parameter is required for `get_ontology_info`."
208
+ }
209
+
210
+ data = self._get_json(f"/api/v2/ontologies/{ontology_id}")
211
+ ontology = OntologyInfo.model_validate(data)
212
+ return ontology.model_dump(by_alias=True)
213
+
214
+ def _handle_search_ontologies(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
215
+ search = arguments.get("search")
216
+ page = int(arguments.get("page", 0))
217
+ size = int(arguments.get("size", 20))
218
+
219
+ params: Dict[str, Any] = {"page": page, "size": size}
220
+ if search:
221
+ params["search"] = search
222
+
223
+ data = self._get_json("/api/v2/ontologies", params=params)
224
+ embedded = data.get("_embedded", {})
225
+ ontologies = embedded.get("ontologies", [])
226
+
227
+ validated: List[Dict[str, Any]] = []
228
+ for item in ontologies:
229
+ try:
230
+ validated.append(
231
+ OntologyInfo.model_validate(item).model_dump(by_alias=True)
232
+ )
233
+ except ValidationError:
234
+ continue
235
+
236
+ page_info = data.get("page", {})
237
+ return {
238
+ "results": validated or ontologies,
239
+ "pagination": {
240
+ "page": page_info.get("number", page),
241
+ "size": page_info.get("size", size),
242
+ "total_pages": page_info.get("totalPages", 0),
243
+ "total_items": page_info.get("totalElements", len(ontologies)),
244
+ },
245
+ "search": search,
246
+ }
247
+
248
+ def _handle_get_term_info(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
249
+ identifier = arguments.get("id")
250
+ if not identifier:
251
+ return {"error": "`id` parameter is required for `get_term_info`."}
252
+
253
+ data = self._get_json("/api/terms", params={"id": identifier})
254
+ embedded = data.get("_embedded", {})
255
+ terms = embedded.get("terms") if isinstance(embedded, dict) else None
256
+ if not terms:
257
+ return {"error": f"Term with ID '{identifier}' was not found in OLS."}
258
+
259
+ # Normalize the term data before validation
260
+ term_data = terms[0]
261
+ if "ontologyId" in term_data and "ontologyName" not in term_data:
262
+ term_data["ontologyName"] = term_data["ontologyId"]
263
+
264
+ term = DetailedTermInfo.model_validate(term_data)
265
+ return term.model_dump(by_alias=True)
266
+
267
+ def _handle_get_term_children(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
268
+ term_iri = arguments.get("term_iri")
269
+ ontology = arguments.get("ontology")
270
+ if not term_iri or not ontology:
271
+ return {
272
+ "error": "`term_iri` and `ontology` parameters are required for `get_term_children`."
273
+ }
274
+
275
+ include_obsolete = bool(arguments.get("include_obsolete", False))
276
+ size = int(arguments.get("size", 20))
277
+ encoded = url_encode_iri(term_iri)
278
+
279
+ params = {
280
+ "page": 0,
281
+ "size": size,
282
+ "includeObsoleteEntities": include_obsolete,
283
+ }
284
+
285
+ data = self._get_json(
286
+ f"/api/v2/ontologies/{ontology}/classes/{encoded}/children", params=params
287
+ )
288
+ formatted = self._format_term_collection(data, size)
289
+ formatted["term_iri"] = term_iri
290
+ formatted["ontology"] = ontology
291
+ formatted["filters"] = {"include_obsolete": include_obsolete}
292
+ return formatted
293
+
294
+ def _handle_get_term_ancestors(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
295
+ term_iri = arguments.get("term_iri")
296
+ ontology = arguments.get("ontology")
297
+ if not term_iri or not ontology:
298
+ return {
299
+ "error": "`term_iri` and `ontology` parameters are required for `get_term_ancestors`."
300
+ }
301
+
302
+ include_obsolete = bool(arguments.get("include_obsolete", False))
303
+ size = int(arguments.get("size", 20))
304
+ encoded = url_encode_iri(term_iri)
305
+
306
+ params = {
307
+ "page": 0,
308
+ "size": size,
309
+ "includeObsoleteEntities": include_obsolete,
310
+ }
311
+
312
+ data = self._get_json(
313
+ f"/api/v2/ontologies/{ontology}/classes/{encoded}/ancestors", params=params
314
+ )
315
+ formatted = self._format_term_collection(data, size)
316
+ formatted["term_iri"] = term_iri
317
+ formatted["ontology"] = ontology
318
+ formatted["filters"] = {"include_obsolete": include_obsolete}
319
+ return formatted
320
+
321
+ def _handle_find_similar_terms(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
322
+ term_iri = arguments.get("term_iri")
323
+ ontology = arguments.get("ontology")
324
+ if not term_iri or not ontology:
325
+ return {
326
+ "error": "`term_iri` and `ontology` parameters are required for `find_similar_terms`."
327
+ }
328
+
329
+ size = int(arguments.get("size", 10))
330
+ encoded = url_encode_iri(term_iri)
331
+
332
+ params = {"page": 0, "size": size}
333
+ data = self._get_json(
334
+ f"/api/v2/ontologies/{ontology}/classes/{encoded}/llm_similar",
335
+ params=params,
336
+ )
337
+ formatted = self._format_term_collection(data, size)
338
+ formatted["term_iri"] = term_iri
339
+ formatted["ontology"] = ontology
340
+ return formatted
341
+
342
+ def _get_json(
343
+ self, path: str, params: Optional[Dict[str, Any]] = None
344
+ ) -> Dict[str, Any]:
345
+ url = f"{self.base_url}{path}"
346
+ response = self.session.get(url, params=params, timeout=self.timeout)
347
+ response.raise_for_status()
348
+ return response.json()
349
+
350
+ def _format_term_collection(
351
+ self, data: Dict[str, Any], size: int
352
+ ) -> Dict[str, Any]:
353
+ elements: Optional[List[Dict[str, Any]]] = None
354
+
355
+ if isinstance(data, dict):
356
+ if "elements" in data and isinstance(data["elements"], list):
357
+ elements = data["elements"]
358
+ else:
359
+ embedded = data.get("_embedded")
360
+ if isinstance(embedded, dict):
361
+ for key in ("terms", "children", "ancestors"):
362
+ if key in embedded and isinstance(embedded[key], list):
363
+ elements = embedded[key]
364
+ break
365
+ if elements is None:
366
+ candidates = [
367
+ value
368
+ for value in embedded.values()
369
+ if isinstance(value, list)
370
+ ]
371
+ if candidates:
372
+ elements = candidates[0]
373
+
374
+ if not elements:
375
+ return data if isinstance(data, dict) else {"items": data}
376
+
377
+ limited = elements[:size]
378
+ term_models = [self._build_term_model(item) for item in limited]
379
+ term_models = [model for model in term_models if model is not None]
380
+
381
+ total = (
382
+ data.get("totalElements")
383
+ or data.get("page", {}).get("totalElements")
384
+ or len(elements)
385
+ )
386
+
387
+ result: Dict[str, Any] = {
388
+ "terms": [model.model_dump(by_alias=True) for model in term_models],
389
+ "total_items": total,
390
+ "showing": len(term_models),
391
+ }
392
+
393
+ page_info = data.get("page") if isinstance(data, dict) else None
394
+ if isinstance(page_info, dict):
395
+ result["pagination"] = {
396
+ "page": page_info.get("number", 0),
397
+ "size": page_info.get("size", len(limited)),
398
+ "total_pages": page_info.get("totalPages", 0),
399
+ "total_items": page_info.get("totalElements", total),
400
+ }
401
+
402
+ return result
403
+
404
+ @staticmethod
405
+ def _build_term_model(item: Dict[str, Any]) -> Optional[TermInfo]:
406
+ payload = {
407
+ "iri": item.get("iri"),
408
+ "ontology_name": item.get("ontologyName")
409
+ or item.get("ontology_name")
410
+ or item.get("ontologyId")
411
+ or "",
412
+ "short_form": item.get("shortForm") or item.get("short_form") or "",
413
+ "label": item.get("label") or "",
414
+ "oboId": item.get("oboId") or item.get("obo_id"),
415
+ "isObsolete": item.get("isObsolete") or item.get("is_obsolete", False),
416
+ }
417
+
418
+ if not payload["iri"]:
419
+ return None
420
+
421
+ try:
422
+ return TermInfo.model_validate(payload)
423
+ except ValidationError:
424
+ return None
425
+
426
+
427
+ __all__ = [
428
+ "OLSTool",
429
+ "OntologyInfo",
430
+ "OntologySearchResponse",
431
+ "TermInfo",
432
+ "TermSearchResponse",
433
+ "DetailedTermInfo",
434
+ "url_encode_iri",
435
+ ]
@@ -37,14 +37,14 @@ class OpenAlexTool(BaseTool):
37
37
  """
38
38
  Search for literature using OpenAlex API.
39
39
 
40
- Parameters:
40
+ Parameters
41
41
  search_keywords (str): Keywords to search for in title, abstract, and content.
42
42
  max_results (int): Maximum number of results to return (default: 10).
43
43
  year_from (int): Start year for publication date filter (optional).
44
44
  year_to (int): End year for publication date filter (optional).
45
45
  open_access (bool): Filter for open access papers only (optional).
46
46
 
47
- Returns:
47
+ Returns
48
48
  list: List of dictionaries containing paper information.
49
49
  """
50
50
  # Encode search keywords for URL
@@ -98,10 +98,10 @@ class OpenAlexTool(BaseTool):
98
98
  """
99
99
  Extract relevant information from a work object returned by OpenAlex API.
100
100
 
101
- Parameters:
101
+ Parameters
102
102
  work (dict): Work object from OpenAlex API response.
103
103
 
104
- Returns:
104
+ Returns
105
105
  dict: Formatted paper information.
106
106
  """
107
107
  # Extract title
@@ -204,10 +204,10 @@ class OpenAlexTool(BaseTool):
204
204
  """
205
205
  Retrieve a specific paper by its DOI.
206
206
 
207
- Parameters:
207
+ Parameters
208
208
  doi (str): DOI of the paper to retrieve.
209
209
 
210
- Returns:
210
+ Returns
211
211
  dict: Paper information or None if not found.
212
212
  """
213
213
  try:
@@ -229,11 +229,11 @@ class OpenAlexTool(BaseTool):
229
229
  """
230
230
  Retrieve papers by a specific author.
231
231
 
232
- Parameters:
232
+ Parameters
233
233
  author_name (str): Name of the author to search for.
234
234
  max_results (int): Maximum number of results to return.
235
235
 
236
- Returns:
236
+ Returns
237
237
  list: List of papers by the author.
238
238
  """
239
239
  try:
@@ -85,11 +85,11 @@ def extract_sentences_with_keywords(text_list, keywords):
85
85
  """
86
86
  Extracts sentences containing any of the specified keywords from the text.
87
87
 
88
- Parameters:
88
+ Parameters
89
89
  - text (str): The input text from which to extract sentences.
90
90
  - keywords (list): A list of keywords to search for in the text.
91
91
 
92
- Returns:
92
+ Returns
93
93
  - list: A list of sentences containing any of the keywords.
94
94
  """
95
95
  sentences_with_keywords = []