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.
- tooluniverse/__init__.py +57 -1
- tooluniverse/blast_tool.py +132 -0
- tooluniverse/boltz_tool.py +2 -2
- tooluniverse/cbioportal_tool.py +42 -0
- tooluniverse/clinvar_tool.py +268 -74
- tooluniverse/compose_scripts/tool_discover.py +1941 -443
- tooluniverse/data/agentic_tools.json +0 -370
- tooluniverse/data/alphafold_tools.json +6 -6
- tooluniverse/data/blast_tools.json +112 -0
- tooluniverse/data/cbioportal_tools.json +87 -0
- tooluniverse/data/clinvar_tools.json +235 -0
- tooluniverse/data/compose_tools.json +0 -89
- tooluniverse/data/dbsnp_tools.json +275 -0
- tooluniverse/data/emdb_tools.json +61 -0
- tooluniverse/data/ensembl_tools.json +259 -0
- tooluniverse/data/file_download_tools.json +275 -0
- tooluniverse/data/geo_tools.json +200 -48
- tooluniverse/data/gnomad_tools.json +109 -0
- tooluniverse/data/gtopdb_tools.json +68 -0
- tooluniverse/data/gwas_tools.json +32 -0
- tooluniverse/data/interpro_tools.json +199 -0
- tooluniverse/data/jaspar_tools.json +70 -0
- tooluniverse/data/kegg_tools.json +356 -0
- tooluniverse/data/mpd_tools.json +87 -0
- tooluniverse/data/ols_tools.json +314 -0
- tooluniverse/data/package_discovery_tools.json +64 -0
- tooluniverse/data/packages/categorized_tools.txt +0 -1
- tooluniverse/data/packages/machine_learning_tools.json +0 -47
- tooluniverse/data/paleobiology_tools.json +91 -0
- tooluniverse/data/pride_tools.json +62 -0
- tooluniverse/data/pypi_package_inspector_tools.json +158 -0
- tooluniverse/data/python_executor_tools.json +341 -0
- tooluniverse/data/regulomedb_tools.json +50 -0
- tooluniverse/data/remap_tools.json +89 -0
- tooluniverse/data/screen_tools.json +89 -0
- tooluniverse/data/tool_discovery_agents.json +428 -0
- tooluniverse/data/tool_discovery_agents.json.backup +1343 -0
- tooluniverse/data/uniprot_tools.json +77 -0
- tooluniverse/data/web_search_tools.json +250 -0
- tooluniverse/data/worms_tools.json +55 -0
- tooluniverse/dbsnp_tool.py +196 -58
- tooluniverse/default_config.py +35 -2
- tooluniverse/emdb_tool.py +30 -0
- tooluniverse/ensembl_tool.py +140 -47
- tooluniverse/execute_function.py +78 -14
- tooluniverse/file_download_tool.py +269 -0
- tooluniverse/geo_tool.py +81 -28
- tooluniverse/gnomad_tool.py +100 -52
- tooluniverse/gtopdb_tool.py +41 -0
- tooluniverse/interpro_tool.py +72 -0
- tooluniverse/jaspar_tool.py +30 -0
- tooluniverse/kegg_tool.py +230 -0
- tooluniverse/mpd_tool.py +42 -0
- tooluniverse/ncbi_eutils_tool.py +96 -0
- tooluniverse/ols_tool.py +435 -0
- tooluniverse/package_discovery_tool.py +217 -0
- tooluniverse/paleobiology_tool.py +30 -0
- tooluniverse/pride_tool.py +30 -0
- tooluniverse/pypi_package_inspector_tool.py +593 -0
- tooluniverse/python_executor_tool.py +711 -0
- tooluniverse/regulomedb_tool.py +30 -0
- tooluniverse/remap_tool.py +44 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +1 -1
- tooluniverse/screen_tool.py +44 -0
- tooluniverse/smcp.py +10 -2
- tooluniverse/smcp_server.py +3 -3
- tooluniverse/tool_finder_embedding.py +3 -1
- tooluniverse/tool_finder_keyword.py +3 -1
- tooluniverse/tool_finder_llm.py +6 -2
- tooluniverse/tools/{UCSC_get_genes_by_region.py → BLAST_nucleotide_search.py} +22 -26
- tooluniverse/tools/BLAST_protein_search.py +63 -0
- tooluniverse/tools/ClinVar_search_variants.py +26 -15
- tooluniverse/tools/CodeQualityAnalyzer.py +3 -3
- tooluniverse/tools/EMDB_get_structure.py +46 -0
- tooluniverse/tools/GtoPdb_get_targets.py +52 -0
- tooluniverse/tools/InterPro_get_domain_details.py +46 -0
- tooluniverse/tools/InterPro_get_protein_domains.py +49 -0
- tooluniverse/tools/InterPro_search_domains.py +52 -0
- tooluniverse/tools/JASPAR_get_transcription_factors.py +52 -0
- tooluniverse/tools/MPD_get_phenotype_data.py +59 -0
- tooluniverse/tools/PRIDE_search_proteomics.py +52 -0
- tooluniverse/tools/PackageAnalyzer.py +55 -0
- tooluniverse/tools/Paleobiology_get_fossils.py +52 -0
- tooluniverse/tools/PyPIPackageInspector.py +59 -0
- tooluniverse/tools/ReMap_get_transcription_factor_binding.py +59 -0
- tooluniverse/tools/ReferenceInfoAnalyzer.py +55 -0
- tooluniverse/tools/RegulomeDB_query_variant.py +46 -0
- tooluniverse/tools/SCREEN_get_regulatory_elements.py +59 -0
- tooluniverse/tools/{ArgumentDescriptionOptimizer.py → TestResultsAnalyzer.py} +13 -13
- tooluniverse/tools/ToolDiscover.py +11 -11
- tooluniverse/tools/UniProt_id_mapping.py +63 -0
- tooluniverse/tools/UniProt_search.py +63 -0
- tooluniverse/tools/UnifiedToolGenerator.py +59 -0
- tooluniverse/tools/WoRMS_search_species.py +49 -0
- tooluniverse/tools/XMLToolOptimizer.py +55 -0
- tooluniverse/tools/__init__.py +119 -29
- tooluniverse/tools/alphafold_get_annotations.py +3 -3
- tooluniverse/tools/alphafold_get_prediction.py +3 -3
- tooluniverse/tools/alphafold_get_summary.py +3 -3
- tooluniverse/tools/cBioPortal_get_cancer_studies.py +46 -0
- tooluniverse/tools/cBioPortal_get_mutations.py +52 -0
- tooluniverse/tools/{gnomAD_query_variant.py → clinvar_get_clinical_significance.py} +8 -11
- tooluniverse/tools/clinvar_get_variant_details.py +49 -0
- tooluniverse/tools/dbSNP_get_variant_by_rsid.py +7 -7
- tooluniverse/tools/dbsnp_get_frequencies.py +46 -0
- tooluniverse/tools/dbsnp_search_by_gene.py +52 -0
- tooluniverse/tools/download_binary_file.py +66 -0
- tooluniverse/tools/download_file.py +71 -0
- tooluniverse/tools/download_text_content.py +55 -0
- tooluniverse/tools/dynamic_package_discovery.py +59 -0
- tooluniverse/tools/ensembl_get_sequence.py +52 -0
- tooluniverse/tools/{Ensembl_lookup_gene_by_symbol.py → ensembl_get_variants.py} +11 -11
- tooluniverse/tools/ensembl_lookup_gene.py +46 -0
- tooluniverse/tools/geo_get_dataset_info.py +46 -0
- tooluniverse/tools/geo_get_sample_info.py +46 -0
- tooluniverse/tools/geo_search_datasets.py +67 -0
- tooluniverse/tools/gnomad_get_gene_constraints.py +49 -0
- tooluniverse/tools/kegg_find_genes.py +52 -0
- tooluniverse/tools/kegg_get_gene_info.py +46 -0
- tooluniverse/tools/kegg_get_pathway_info.py +46 -0
- tooluniverse/tools/kegg_list_organisms.py +44 -0
- tooluniverse/tools/kegg_search_pathway.py +46 -0
- tooluniverse/tools/ols_find_similar_terms.py +63 -0
- tooluniverse/tools/{get_hyperopt_info.py → ols_get_ontology_info.py} +13 -10
- tooluniverse/tools/ols_get_term_ancestors.py +67 -0
- tooluniverse/tools/ols_get_term_children.py +67 -0
- tooluniverse/tools/{TestCaseGenerator.py → ols_get_term_info.py} +12 -9
- tooluniverse/tools/{CodeOptimizer.py → ols_search_ontologies.py} +22 -14
- tooluniverse/tools/ols_search_terms.py +71 -0
- tooluniverse/tools/python_code_executor.py +79 -0
- tooluniverse/tools/python_script_runner.py +79 -0
- tooluniverse/tools/web_api_documentation_search.py +63 -0
- tooluniverse/tools/web_search.py +71 -0
- tooluniverse/uniprot_tool.py +219 -16
- tooluniverse/url_tool.py +18 -0
- tooluniverse/utils.py +2 -2
- tooluniverse/web_search_tool.py +229 -0
- tooluniverse/worms_tool.py +64 -0
- {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/METADATA +3 -2
- {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/RECORD +144 -55
- tooluniverse/data/genomics_tools.json +0 -174
- tooluniverse/tools/ToolDescriptionOptimizer.py +0 -67
- tooluniverse/tools/ToolImplementationGenerator.py +0 -67
- tooluniverse/tools/ToolOptimizer.py +0 -59
- tooluniverse/tools/ToolSpecificationGenerator.py +0 -67
- tooluniverse/tools/ToolSpecificationOptimizer.py +0 -63
- tooluniverse/ucsc_tool.py +0 -60
- {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/entry_points.txt +0 -0
- {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-1.0.10.dist-info → tooluniverse-1.0.11.1.dist-info}/top_level.txt +0 -0
tooluniverse/ols_tool.py
ADDED
|
@@ -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)}"}
|