tooluniverse 1.0.10__py3-none-any.whl → 1.0.11.1__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 (151) hide show
  1. tooluniverse/__init__.py +57 -1
  2. tooluniverse/blast_tool.py +132 -0
  3. tooluniverse/boltz_tool.py +2 -2
  4. tooluniverse/cbioportal_tool.py +42 -0
  5. tooluniverse/clinvar_tool.py +268 -74
  6. tooluniverse/compose_scripts/tool_discover.py +1941 -443
  7. tooluniverse/data/agentic_tools.json +0 -370
  8. tooluniverse/data/alphafold_tools.json +6 -6
  9. tooluniverse/data/blast_tools.json +112 -0
  10. tooluniverse/data/cbioportal_tools.json +87 -0
  11. tooluniverse/data/clinvar_tools.json +235 -0
  12. tooluniverse/data/compose_tools.json +0 -89
  13. tooluniverse/data/dbsnp_tools.json +275 -0
  14. tooluniverse/data/emdb_tools.json +61 -0
  15. tooluniverse/data/ensembl_tools.json +259 -0
  16. tooluniverse/data/file_download_tools.json +275 -0
  17. tooluniverse/data/geo_tools.json +200 -48
  18. tooluniverse/data/gnomad_tools.json +109 -0
  19. tooluniverse/data/gtopdb_tools.json +68 -0
  20. tooluniverse/data/gwas_tools.json +32 -0
  21. tooluniverse/data/interpro_tools.json +199 -0
  22. tooluniverse/data/jaspar_tools.json +70 -0
  23. tooluniverse/data/kegg_tools.json +356 -0
  24. tooluniverse/data/mpd_tools.json +87 -0
  25. tooluniverse/data/ols_tools.json +314 -0
  26. tooluniverse/data/package_discovery_tools.json +64 -0
  27. tooluniverse/data/packages/categorized_tools.txt +0 -1
  28. tooluniverse/data/packages/machine_learning_tools.json +0 -47
  29. tooluniverse/data/paleobiology_tools.json +91 -0
  30. tooluniverse/data/pride_tools.json +62 -0
  31. tooluniverse/data/pypi_package_inspector_tools.json +158 -0
  32. tooluniverse/data/python_executor_tools.json +341 -0
  33. tooluniverse/data/regulomedb_tools.json +50 -0
  34. tooluniverse/data/remap_tools.json +89 -0
  35. tooluniverse/data/screen_tools.json +89 -0
  36. tooluniverse/data/tool_discovery_agents.json +428 -0
  37. tooluniverse/data/tool_discovery_agents.json.backup +1343 -0
  38. tooluniverse/data/uniprot_tools.json +77 -0
  39. tooluniverse/data/web_search_tools.json +250 -0
  40. tooluniverse/data/worms_tools.json +55 -0
  41. tooluniverse/dbsnp_tool.py +196 -58
  42. tooluniverse/default_config.py +35 -2
  43. tooluniverse/emdb_tool.py +30 -0
  44. tooluniverse/ensembl_tool.py +140 -47
  45. tooluniverse/execute_function.py +78 -14
  46. tooluniverse/file_download_tool.py +269 -0
  47. tooluniverse/geo_tool.py +81 -28
  48. tooluniverse/gnomad_tool.py +100 -52
  49. tooluniverse/gtopdb_tool.py +41 -0
  50. tooluniverse/interpro_tool.py +72 -0
  51. tooluniverse/jaspar_tool.py +30 -0
  52. tooluniverse/kegg_tool.py +230 -0
  53. tooluniverse/mpd_tool.py +42 -0
  54. tooluniverse/ncbi_eutils_tool.py +96 -0
  55. tooluniverse/ols_tool.py +435 -0
  56. tooluniverse/package_discovery_tool.py +217 -0
  57. tooluniverse/paleobiology_tool.py +30 -0
  58. tooluniverse/pride_tool.py +30 -0
  59. tooluniverse/pypi_package_inspector_tool.py +593 -0
  60. tooluniverse/python_executor_tool.py +711 -0
  61. tooluniverse/regulomedb_tool.py +30 -0
  62. tooluniverse/remap_tool.py +44 -0
  63. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +1 -1
  64. tooluniverse/screen_tool.py +44 -0
  65. tooluniverse/smcp.py +10 -2
  66. tooluniverse/smcp_server.py +3 -3
  67. tooluniverse/tool_finder_embedding.py +3 -1
  68. tooluniverse/tool_finder_keyword.py +3 -1
  69. tooluniverse/tool_finder_llm.py +6 -2
  70. tooluniverse/tools/{UCSC_get_genes_by_region.py → BLAST_nucleotide_search.py} +22 -26
  71. tooluniverse/tools/BLAST_protein_search.py +63 -0
  72. tooluniverse/tools/ClinVar_search_variants.py +26 -15
  73. tooluniverse/tools/CodeQualityAnalyzer.py +3 -3
  74. tooluniverse/tools/EMDB_get_structure.py +46 -0
  75. tooluniverse/tools/GtoPdb_get_targets.py +52 -0
  76. tooluniverse/tools/InterPro_get_domain_details.py +46 -0
  77. tooluniverse/tools/InterPro_get_protein_domains.py +49 -0
  78. tooluniverse/tools/InterPro_search_domains.py +52 -0
  79. tooluniverse/tools/JASPAR_get_transcription_factors.py +52 -0
  80. tooluniverse/tools/MPD_get_phenotype_data.py +59 -0
  81. tooluniverse/tools/PRIDE_search_proteomics.py +52 -0
  82. tooluniverse/tools/PackageAnalyzer.py +55 -0
  83. tooluniverse/tools/Paleobiology_get_fossils.py +52 -0
  84. tooluniverse/tools/PyPIPackageInspector.py +59 -0
  85. tooluniverse/tools/ReMap_get_transcription_factor_binding.py +59 -0
  86. tooluniverse/tools/ReferenceInfoAnalyzer.py +55 -0
  87. tooluniverse/tools/RegulomeDB_query_variant.py +46 -0
  88. tooluniverse/tools/SCREEN_get_regulatory_elements.py +59 -0
  89. tooluniverse/tools/{ArgumentDescriptionOptimizer.py → TestResultsAnalyzer.py} +13 -13
  90. tooluniverse/tools/ToolDiscover.py +11 -11
  91. tooluniverse/tools/UniProt_id_mapping.py +63 -0
  92. tooluniverse/tools/UniProt_search.py +63 -0
  93. tooluniverse/tools/UnifiedToolGenerator.py +59 -0
  94. tooluniverse/tools/WoRMS_search_species.py +49 -0
  95. tooluniverse/tools/XMLToolOptimizer.py +55 -0
  96. tooluniverse/tools/__init__.py +119 -29
  97. tooluniverse/tools/alphafold_get_annotations.py +3 -3
  98. tooluniverse/tools/alphafold_get_prediction.py +3 -3
  99. tooluniverse/tools/alphafold_get_summary.py +3 -3
  100. tooluniverse/tools/cBioPortal_get_cancer_studies.py +46 -0
  101. tooluniverse/tools/cBioPortal_get_mutations.py +52 -0
  102. tooluniverse/tools/{gnomAD_query_variant.py → clinvar_get_clinical_significance.py} +8 -11
  103. tooluniverse/tools/clinvar_get_variant_details.py +49 -0
  104. tooluniverse/tools/dbSNP_get_variant_by_rsid.py +7 -7
  105. tooluniverse/tools/dbsnp_get_frequencies.py +46 -0
  106. tooluniverse/tools/dbsnp_search_by_gene.py +52 -0
  107. tooluniverse/tools/download_binary_file.py +66 -0
  108. tooluniverse/tools/download_file.py +71 -0
  109. tooluniverse/tools/download_text_content.py +55 -0
  110. tooluniverse/tools/dynamic_package_discovery.py +59 -0
  111. tooluniverse/tools/ensembl_get_sequence.py +52 -0
  112. tooluniverse/tools/{Ensembl_lookup_gene_by_symbol.py → ensembl_get_variants.py} +11 -11
  113. tooluniverse/tools/ensembl_lookup_gene.py +46 -0
  114. tooluniverse/tools/geo_get_dataset_info.py +46 -0
  115. tooluniverse/tools/geo_get_sample_info.py +46 -0
  116. tooluniverse/tools/geo_search_datasets.py +67 -0
  117. tooluniverse/tools/gnomad_get_gene_constraints.py +49 -0
  118. tooluniverse/tools/kegg_find_genes.py +52 -0
  119. tooluniverse/tools/kegg_get_gene_info.py +46 -0
  120. tooluniverse/tools/kegg_get_pathway_info.py +46 -0
  121. tooluniverse/tools/kegg_list_organisms.py +44 -0
  122. tooluniverse/tools/kegg_search_pathway.py +46 -0
  123. tooluniverse/tools/ols_find_similar_terms.py +63 -0
  124. tooluniverse/tools/{get_hyperopt_info.py → ols_get_ontology_info.py} +13 -10
  125. tooluniverse/tools/ols_get_term_ancestors.py +67 -0
  126. tooluniverse/tools/ols_get_term_children.py +67 -0
  127. tooluniverse/tools/{TestCaseGenerator.py → ols_get_term_info.py} +12 -9
  128. tooluniverse/tools/{CodeOptimizer.py → ols_search_ontologies.py} +22 -14
  129. tooluniverse/tools/ols_search_terms.py +71 -0
  130. tooluniverse/tools/python_code_executor.py +79 -0
  131. tooluniverse/tools/python_script_runner.py +79 -0
  132. tooluniverse/tools/web_api_documentation_search.py +63 -0
  133. tooluniverse/tools/web_search.py +71 -0
  134. tooluniverse/uniprot_tool.py +219 -16
  135. tooluniverse/url_tool.py +18 -0
  136. tooluniverse/utils.py +2 -2
  137. tooluniverse/web_search_tool.py +229 -0
  138. tooluniverse/worms_tool.py +64 -0
  139. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/METADATA +3 -2
  140. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/RECORD +144 -55
  141. tooluniverse/data/genomics_tools.json +0 -174
  142. tooluniverse/tools/ToolDescriptionOptimizer.py +0 -67
  143. tooluniverse/tools/ToolImplementationGenerator.py +0 -67
  144. tooluniverse/tools/ToolOptimizer.py +0 -59
  145. tooluniverse/tools/ToolSpecificationGenerator.py +0 -67
  146. tooluniverse/tools/ToolSpecificationOptimizer.py +0 -63
  147. tooluniverse/ucsc_tool.py +0 -60
  148. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/WHEEL +0 -0
  149. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/entry_points.txt +0 -0
  150. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/licenses/LICENSE +0 -0
  151. {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/top_level.txt +0 -0
@@ -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
+ ]
@@ -0,0 +1,217 @@
1
+ """Dynamic package discovery and evaluation"""
2
+
3
+ import requests
4
+ import time
5
+ from typing import Dict, Any, List
6
+ from .base_tool import BaseTool
7
+ from .tool_registry import register_tool
8
+
9
+
10
+ @register_tool("DynamicPackageDiscovery")
11
+ class DynamicPackageDiscovery(BaseTool):
12
+ """Searches PyPI and evaluates packages dynamically based on requirements"""
13
+
14
+ def __init__(self, tool_config: Dict[str, Any]):
15
+ super().__init__(tool_config)
16
+ self.pypi_search_url = "https://pypi.org/pypi/{package}/json"
17
+ self.pypi_search_api = "https://pypi.org/search/"
18
+ self.session = requests.Session()
19
+ self.session.headers.update({"User-Agent": "ToolUniverse-PackageDiscovery/1.0"})
20
+
21
+ # Initialize WebSearchTool instance
22
+ from .web_search_tool import WebSearchTool
23
+
24
+ self.web_search_tool = WebSearchTool({"name": "WebSearchTool"})
25
+
26
+ def _search_pypi_via_web(self, query: str) -> List[Dict[str, Any]]:
27
+ """Search PyPI using web search tool"""
28
+ try:
29
+ # Use pre-initialized WebSearchTool instance
30
+ result = self.web_search_tool.run(
31
+ {
32
+ "query": f"{query} site:pypi.org",
33
+ "max_results": 10,
34
+ "search_type": "python_packages",
35
+ }
36
+ )
37
+
38
+ packages = []
39
+ if result.get("status") == "success":
40
+ for item in result.get("results", []):
41
+ url = item.get("url", "")
42
+ if "pypi.org/project/" in url:
43
+ # Extract package name from URL
44
+ pkg_name = url.split("/project/")[-1].rstrip("/")
45
+ packages.append(
46
+ {
47
+ "name": pkg_name,
48
+ "source": "pypi_web",
49
+ "title": item.get("title", ""),
50
+ "snippet": item.get("snippet", ""),
51
+ "url": url,
52
+ }
53
+ )
54
+
55
+ return packages
56
+ except Exception as e:
57
+ print(f"⚠️ Web search for PyPI packages failed: {e}")
58
+ return []
59
+
60
+ def _evaluate_package(self, package_name: str) -> Dict[str, Any]:
61
+ """Evaluate a package's suitability by fetching PyPI metadata"""
62
+ try:
63
+ response = self.session.get(
64
+ self.pypi_search_url.format(package=package_name), timeout=10
65
+ )
66
+
67
+ if response.status_code == 200:
68
+ data = response.json()
69
+ info = data.get("info", {})
70
+ urls = info.get("project_urls", {})
71
+
72
+ # Extract key metrics
73
+ evaluation = {
74
+ "name": package_name,
75
+ "version": info.get("version"),
76
+ "description": info.get("summary", ""),
77
+ "author": info.get("author", ""),
78
+ "license": info.get("license", ""),
79
+ "home_page": info.get("home_page", ""),
80
+ "download_url": info.get("download_url", ""),
81
+ "requires_python": info.get("requires_python", ""),
82
+ "dependencies": info.get("requires_dist", []),
83
+ "classifiers": info.get("classifiers", []),
84
+ # Quality indicators
85
+ "has_docs": bool(urls.get("Documentation")),
86
+ "has_source": bool(urls.get("Source")),
87
+ "has_homepage": bool(info.get("home_page")),
88
+ "has_bug_tracker": bool(urls.get("Bug Reports")),
89
+ "project_urls": urls,
90
+ # Popularity indicators
91
+ "is_stable": "Development Status :: 5 - Production/Stable"
92
+ in info.get("classifiers", []),
93
+ "is_mature": "Development Status :: 6 - Mature"
94
+ in info.get("classifiers", []),
95
+ "has_tests": "Topic :: Software Development :: Testing"
96
+ in info.get("classifiers", []),
97
+ "is_typed": "Typing :: Typed" in info.get("classifiers", []),
98
+ }
99
+
100
+ # Calculate a basic quality score
101
+ quality_score = 0
102
+ if evaluation["has_docs"]:
103
+ quality_score += 20
104
+ if evaluation["has_source"]:
105
+ quality_score += 15
106
+ if evaluation["is_stable"] or evaluation["is_mature"]:
107
+ quality_score += 25
108
+ if evaluation["has_tests"]:
109
+ quality_score += 15
110
+ if evaluation["is_typed"]:
111
+ quality_score += 10
112
+ if evaluation["has_homepage"]:
113
+ quality_score += 10
114
+ if evaluation["has_bug_tracker"]:
115
+ quality_score += 5
116
+
117
+ evaluation["quality_score"] = min(quality_score, 100)
118
+
119
+ return evaluation
120
+ else:
121
+ return {
122
+ "name": package_name,
123
+ "error": f"HTTP {response.status_code}",
124
+ "quality_score": 0,
125
+ }
126
+
127
+ except Exception as e:
128
+ return {"name": package_name, "error": str(e), "quality_score": 0}
129
+
130
+ def _rank_packages(
131
+ self, packages: List[Dict[str, Any]], requirements: str, functionality: str
132
+ ) -> List[Dict[str, Any]]:
133
+ """Rank packages by relevance and quality"""
134
+ if not packages:
135
+ return []
136
+
137
+ # Filter out packages with errors
138
+ valid_packages = [pkg for pkg in packages if "error" not in pkg]
139
+
140
+ # Sort by quality score (descending)
141
+ ranked = sorted(
142
+ valid_packages, key=lambda x: x.get("quality_score", 0), reverse=True
143
+ )
144
+
145
+ # Add ranking metadata
146
+ for i, pkg in enumerate(ranked):
147
+ pkg["rank"] = i + 1
148
+ pkg["reasoning"] = f"Quality score: {pkg.get('quality_score', 0)}/100"
149
+
150
+ return ranked
151
+
152
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
153
+ """
154
+ Dynamically discover and evaluate packages
155
+
156
+ Args:
157
+ requirements: Description of what's needed
158
+ functionality: Specific functionality required
159
+ constraints: Any constraints (Python version, license, etc.)
160
+ """
161
+ try:
162
+ requirements = arguments.get("requirements", "")
163
+ functionality = arguments.get("functionality", "")
164
+
165
+ # Search for candidate packages
166
+ search_query = f"{requirements} {functionality}".strip()
167
+ print(f"🔍 Searching for packages: {search_query}")
168
+
169
+ candidates = self._search_pypi_via_web(search_query)
170
+
171
+ if not candidates:
172
+ return {
173
+ "status": "success",
174
+ "candidates": [],
175
+ "recommendation": None,
176
+ "message": "No packages found",
177
+ }
178
+
179
+ print(f"📦 Found {len(candidates)} package candidates")
180
+
181
+ # Evaluate each candidate
182
+ evaluated = []
183
+ for i, pkg in enumerate(candidates):
184
+ print(f" Evaluating {i+1}/{len(candidates)}: {pkg['name']}")
185
+ evaluation = self._evaluate_package(pkg["name"])
186
+ # Merge web search info with PyPI evaluation
187
+ evaluation.update({k: v for k, v in pkg.items() if k not in evaluation})
188
+ evaluated.append(evaluation)
189
+
190
+ # Rate limiting
191
+ time.sleep(0.2)
192
+
193
+ # Rank by suitability
194
+ ranked = self._rank_packages(evaluated, requirements, functionality)
195
+
196
+ top_recommendation = ranked[0] if ranked else None
197
+
198
+ if top_recommendation:
199
+ score = top_recommendation.get("quality_score", 0)
200
+ print(
201
+ f"🏆 Top recommendation: {top_recommendation['name']} (score: {score})"
202
+ )
203
+
204
+ return {
205
+ "status": "success",
206
+ "candidates": ranked,
207
+ "recommendation": top_recommendation,
208
+ "total_evaluated": len(evaluated),
209
+ }
210
+
211
+ except Exception as e:
212
+ return {
213
+ "status": "error",
214
+ "error": str(e),
215
+ "candidates": [],
216
+ "recommendation": None,
217
+ }
@@ -0,0 +1,30 @@
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("PaleobiologyRESTTool")
8
+ class PaleobiologyRESTTool(BaseTool):
9
+ def __init__(self, tool_config: Dict):
10
+ super().__init__(tool_config)
11
+ self.base_url = "https://paleobiodb.org/data1.2"
12
+ self.session = requests.Session()
13
+ self.session.headers.update({"Accept": "application/json"})
14
+ self.timeout = 30
15
+
16
+ def _build_url(self, args: Dict[str, Any]) -> str:
17
+ url = self.tool_config["fields"]["endpoint"]
18
+ for k, v in args.items():
19
+ url = url.replace(f"{{{k}}}", str(v))
20
+ return url
21
+
22
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
23
+ try:
24
+ url = self._build_url(arguments)
25
+ response = self.session.get(url, timeout=self.timeout)
26
+ response.raise_for_status()
27
+ data = response.json()
28
+ return {"status": "success", "data": data, "url": url}
29
+ except Exception as e:
30
+ return {"status": "error", "error": f"Paleobiology API error: {str(e)}"}