tooluniverse 0.2.0__py3-none-any.whl → 1.0.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 +340 -4
- tooluniverse/admetai_tool.py +84 -0
- tooluniverse/agentic_tool.py +563 -0
- tooluniverse/alphafold_tool.py +96 -0
- tooluniverse/base_tool.py +129 -6
- tooluniverse/boltz_tool.py +207 -0
- tooluniverse/chem_tool.py +192 -0
- tooluniverse/compose_scripts/__init__.py +1 -0
- tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
- tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
- tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
- tooluniverse/compose_scripts/literature_tool.py +34 -0
- tooluniverse/compose_scripts/output_summarizer.py +279 -0
- tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
- tooluniverse/compose_scripts/tool_discover.py +705 -0
- tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
- tooluniverse/compose_tool.py +371 -0
- tooluniverse/ctg_tool.py +1002 -0
- tooluniverse/custom_tool.py +81 -0
- tooluniverse/dailymed_tool.py +108 -0
- tooluniverse/data/admetai_tools.json +155 -0
- tooluniverse/data/adverse_event_tools.json +108 -0
- tooluniverse/data/agentic_tools.json +1156 -0
- tooluniverse/data/alphafold_tools.json +87 -0
- tooluniverse/data/boltz_tools.json +9 -0
- tooluniverse/data/chembl_tools.json +16 -0
- tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
- tooluniverse/data/compose_tools.json +202 -0
- tooluniverse/data/dailymed_tools.json +70 -0
- tooluniverse/data/dataset_tools.json +646 -0
- tooluniverse/data/disease_target_score_tools.json +712 -0
- tooluniverse/data/efo_tools.json +17 -0
- tooluniverse/data/embedding_tools.json +319 -0
- tooluniverse/data/enrichr_tools.json +31 -0
- tooluniverse/data/europe_pmc_tools.json +22 -0
- tooluniverse/data/expert_feedback_tools.json +10 -0
- tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
- tooluniverse/data/fda_drug_labeling_tools.json +1 -1
- tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
- tooluniverse/data/finder_tools.json +209 -0
- tooluniverse/data/gene_ontology_tools.json +113 -0
- tooluniverse/data/gwas_tools.json +1082 -0
- tooluniverse/data/hpa_tools.json +333 -0
- tooluniverse/data/humanbase_tools.json +47 -0
- tooluniverse/data/idmap_tools.json +74 -0
- tooluniverse/data/mcp_client_tools_example.json +113 -0
- tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
- tooluniverse/data/medlineplus_tools.json +141 -0
- tooluniverse/data/monarch_tools.json +1 -1
- tooluniverse/data/openalex_tools.json +36 -0
- tooluniverse/data/opentarget_tools.json +1 -1
- tooluniverse/data/output_summarization_tools.json +101 -0
- tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
- tooluniverse/data/packages/categorized_tools.txt +206 -0
- tooluniverse/data/packages/cheminformatics_tools.json +347 -0
- tooluniverse/data/packages/earth_sciences_tools.json +74 -0
- tooluniverse/data/packages/genomics_tools.json +776 -0
- tooluniverse/data/packages/image_processing_tools.json +38 -0
- tooluniverse/data/packages/machine_learning_tools.json +789 -0
- tooluniverse/data/packages/neuroscience_tools.json +62 -0
- tooluniverse/data/packages/original_tools.txt +0 -0
- tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
- tooluniverse/data/packages/scientific_computing_tools.json +560 -0
- tooluniverse/data/packages/single_cell_tools.json +453 -0
- tooluniverse/data/packages/structural_biology_tools.json +396 -0
- tooluniverse/data/packages/visualization_tools.json +399 -0
- tooluniverse/data/pubchem_tools.json +215 -0
- tooluniverse/data/pubtator_tools.json +68 -0
- tooluniverse/data/rcsb_pdb_tools.json +1332 -0
- tooluniverse/data/reactome_tools.json +19 -0
- tooluniverse/data/semantic_scholar_tools.json +26 -0
- tooluniverse/data/special_tools.json +2 -25
- tooluniverse/data/tool_composition_tools.json +88 -0
- tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
- tooluniverse/data/txagent_client_tools.json +9 -0
- tooluniverse/data/uniprot_tools.json +211 -0
- tooluniverse/data/url_fetch_tools.json +94 -0
- tooluniverse/data/uspto_downloader_tools.json +9 -0
- tooluniverse/data/uspto_tools.json +811 -0
- tooluniverse/data/xml_tools.json +3275 -0
- tooluniverse/dataset_tool.py +296 -0
- tooluniverse/default_config.py +165 -0
- tooluniverse/efo_tool.py +42 -0
- tooluniverse/embedding_database.py +630 -0
- tooluniverse/embedding_sync.py +396 -0
- tooluniverse/enrichr_tool.py +266 -0
- tooluniverse/europe_pmc_tool.py +52 -0
- tooluniverse/execute_function.py +1775 -95
- tooluniverse/extended_hooks.py +444 -0
- tooluniverse/gene_ontology_tool.py +194 -0
- tooluniverse/graphql_tool.py +158 -36
- tooluniverse/gwas_tool.py +358 -0
- tooluniverse/hpa_tool.py +1645 -0
- tooluniverse/humanbase_tool.py +389 -0
- tooluniverse/logging_config.py +254 -0
- tooluniverse/mcp_client_tool.py +764 -0
- tooluniverse/mcp_integration.py +413 -0
- tooluniverse/mcp_tool_registry.py +925 -0
- tooluniverse/medlineplus_tool.py +337 -0
- tooluniverse/openalex_tool.py +228 -0
- tooluniverse/openfda_adv_tool.py +283 -0
- tooluniverse/openfda_tool.py +393 -160
- tooluniverse/output_hook.py +1122 -0
- tooluniverse/package_tool.py +195 -0
- tooluniverse/pubchem_tool.py +158 -0
- tooluniverse/pubtator_tool.py +168 -0
- tooluniverse/rcsb_pdb_tool.py +38 -0
- tooluniverse/reactome_tool.py +108 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
- tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
- tooluniverse/remote/expert_feedback/simple_test.py +23 -0
- tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
- tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
- tooluniverse/remote/immune_compass/compass_tool.py +327 -0
- tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
- tooluniverse/remote_tool.py +99 -0
- tooluniverse/restful_tool.py +53 -30
- tooluniverse/scripts/generate_tool_graph.py +408 -0
- tooluniverse/scripts/visualize_tool_graph.py +829 -0
- tooluniverse/semantic_scholar_tool.py +62 -0
- tooluniverse/smcp.py +2452 -0
- tooluniverse/smcp_server.py +975 -0
- tooluniverse/test/mcp_server_test.py +0 -0
- tooluniverse/test/test_admetai_tool.py +370 -0
- tooluniverse/test/test_agentic_tool.py +129 -0
- tooluniverse/test/test_alphafold_tool.py +71 -0
- tooluniverse/test/test_chem_tool.py +37 -0
- tooluniverse/test/test_compose_lieraturereview.py +63 -0
- tooluniverse/test/test_compose_tool.py +448 -0
- tooluniverse/test/test_dailymed.py +69 -0
- tooluniverse/test/test_dataset_tool.py +200 -0
- tooluniverse/test/test_disease_target_score.py +56 -0
- tooluniverse/test/test_drugbank_filter_examples.py +179 -0
- tooluniverse/test/test_efo.py +31 -0
- tooluniverse/test/test_enrichr_tool.py +21 -0
- tooluniverse/test/test_europe_pmc_tool.py +20 -0
- tooluniverse/test/test_fda_adv.py +95 -0
- tooluniverse/test/test_fda_drug_labeling.py +91 -0
- tooluniverse/test/test_gene_ontology_tools.py +66 -0
- tooluniverse/test/test_gwas_tool.py +139 -0
- tooluniverse/test/test_hpa.py +625 -0
- tooluniverse/test/test_humanbase_tool.py +20 -0
- tooluniverse/test/test_idmap_tools.py +61 -0
- tooluniverse/test/test_mcp_server.py +211 -0
- tooluniverse/test/test_mcp_tool.py +247 -0
- tooluniverse/test/test_medlineplus.py +220 -0
- tooluniverse/test/test_openalex_tool.py +32 -0
- tooluniverse/test/test_opentargets.py +28 -0
- tooluniverse/test/test_pubchem_tool.py +116 -0
- tooluniverse/test/test_pubtator_tool.py +37 -0
- tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
- tooluniverse/test/test_reactome.py +54 -0
- tooluniverse/test/test_semantic_scholar_tool.py +24 -0
- tooluniverse/test/test_software_tools.py +147 -0
- tooluniverse/test/test_tool_description_optimizer.py +49 -0
- tooluniverse/test/test_tool_finder.py +26 -0
- tooluniverse/test/test_tool_finder_llm.py +252 -0
- tooluniverse/test/test_tools_find.py +195 -0
- tooluniverse/test/test_uniprot_tools.py +74 -0
- tooluniverse/test/test_uspto_tool.py +72 -0
- tooluniverse/test/test_xml_tool.py +113 -0
- tooluniverse/tool_finder_embedding.py +267 -0
- tooluniverse/tool_finder_keyword.py +693 -0
- tooluniverse/tool_finder_llm.py +699 -0
- tooluniverse/tool_graph_web_ui.py +955 -0
- tooluniverse/tool_registry.py +416 -0
- tooluniverse/uniprot_tool.py +155 -0
- tooluniverse/url_tool.py +253 -0
- tooluniverse/uspto_tool.py +240 -0
- tooluniverse/utils.py +369 -41
- tooluniverse/xml_tool.py +369 -0
- tooluniverse-1.0.1.dist-info/METADATA +387 -0
- tooluniverse-1.0.1.dist-info/RECORD +182 -0
- tooluniverse-1.0.1.dist-info/entry_points.txt +9 -0
- tooluniverse/generate_mcp_tools.py +0 -113
- tooluniverse/mcp_server.py +0 -3340
- tooluniverse-0.2.0.dist-info/METADATA +0 -139
- tooluniverse-0.2.0.dist-info/RECORD +0 -21
- tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/WHEEL +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# package_tool.py
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import json
|
|
5
|
+
from .base_tool import BaseTool
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
from .tool_registry import register_tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register_tool("PackageTool")
|
|
11
|
+
class PackageTool(BaseTool):
|
|
12
|
+
"""
|
|
13
|
+
Universal tool to provide information about Python packages.
|
|
14
|
+
Fetches real-time data from PyPI API with local fallback.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, tool_config):
|
|
18
|
+
super().__init__(tool_config)
|
|
19
|
+
self.package_name = tool_config.get("package_name", "")
|
|
20
|
+
self.local_info = tool_config.get("local_info", {})
|
|
21
|
+
self.pypi_timeout = tool_config.get("pypi_timeout", 5)
|
|
22
|
+
|
|
23
|
+
def run(self, arguments):
|
|
24
|
+
"""
|
|
25
|
+
Get comprehensive package information.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
arguments (dict): Optional parameters for customization
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
dict: Package information including name, description, installation, docs, usage
|
|
32
|
+
"""
|
|
33
|
+
include_examples = arguments.get("include_examples", True)
|
|
34
|
+
source = arguments.get("source", "auto") # 'auto', 'pypi', 'local'
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
if source == "local":
|
|
38
|
+
return self._get_local_info(include_examples)
|
|
39
|
+
elif source == "pypi":
|
|
40
|
+
return self._get_pypi_info(include_examples)
|
|
41
|
+
else: # auto - try PyPI first, fallback to local
|
|
42
|
+
try:
|
|
43
|
+
return self._get_pypi_info(include_examples)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"PyPI fetch failed: {e}, falling back to local info")
|
|
46
|
+
return self._get_local_info(include_examples)
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
return {
|
|
50
|
+
"error": f"Failed to get package information: {str(e)}",
|
|
51
|
+
"package_name": self.package_name,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def _get_pypi_info(self, include_examples: bool = True) -> Dict[str, Any]:
|
|
55
|
+
"""Fetch package information from PyPI API"""
|
|
56
|
+
url = f"https://pypi.org/pypi/{self.package_name}/json"
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
response = requests.get(url, timeout=self.pypi_timeout)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
pypi_data = response.json()
|
|
62
|
+
|
|
63
|
+
info = pypi_data.get("info", {})
|
|
64
|
+
|
|
65
|
+
# Build response with PyPI data
|
|
66
|
+
result = {
|
|
67
|
+
"package_name": info.get("name", self.package_name),
|
|
68
|
+
"description": info.get("summary", "No description available"),
|
|
69
|
+
"version": info.get("version", "Unknown"),
|
|
70
|
+
"author": info.get("author", "Unknown"),
|
|
71
|
+
"license": info.get("license", "Not specified"),
|
|
72
|
+
"home_page": info.get("home_page", ""),
|
|
73
|
+
"documentation": info.get("project_urls", {}).get("Documentation", ""),
|
|
74
|
+
"repository": info.get("project_urls", {}).get(
|
|
75
|
+
"Repository", info.get("project_urls", {}).get("Source", "")
|
|
76
|
+
),
|
|
77
|
+
"python_versions": info.get("classifiers", []),
|
|
78
|
+
"keywords": (
|
|
79
|
+
info.get("keywords", "").split(",") if info.get("keywords") else []
|
|
80
|
+
),
|
|
81
|
+
"installation": {
|
|
82
|
+
"pip": f"pip install {self.package_name}",
|
|
83
|
+
"conda": f"conda install {self.package_name}",
|
|
84
|
+
"pip_upgrade": f"pip install --upgrade {self.package_name}",
|
|
85
|
+
},
|
|
86
|
+
"source": "pypi",
|
|
87
|
+
"last_updated": pypi_data.get("last_serial", "Unknown"),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Merge with local enhanced information
|
|
91
|
+
local_info = self.local_info
|
|
92
|
+
if local_info:
|
|
93
|
+
result["category"] = local_info.get("category", "General")
|
|
94
|
+
result["import_name"] = local_info.get("import_name", self.package_name)
|
|
95
|
+
result["popularity"] = local_info.get("popularity", 0)
|
|
96
|
+
|
|
97
|
+
# Override with better local descriptions if available
|
|
98
|
+
if local_info.get("description") and len(
|
|
99
|
+
local_info["description"]
|
|
100
|
+
) > len(result["description"]):
|
|
101
|
+
result["description"] = local_info["description"]
|
|
102
|
+
|
|
103
|
+
# Add local documentation if PyPI doesn't have it
|
|
104
|
+
if not result["documentation"] and local_info.get("documentation"):
|
|
105
|
+
result["documentation"] = local_info["documentation"]
|
|
106
|
+
|
|
107
|
+
# Add custom installation instructions
|
|
108
|
+
if local_info.get("installation"):
|
|
109
|
+
result["installation"].update(local_info["installation"])
|
|
110
|
+
|
|
111
|
+
# Add examples if requested
|
|
112
|
+
if include_examples:
|
|
113
|
+
result["usage_example"] = self._get_usage_example()
|
|
114
|
+
result["quick_start"] = self._get_quick_start_guide()
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
except requests.exceptions.RequestException as e:
|
|
119
|
+
raise Exception(f"Failed to fetch from PyPI: {str(e)}")
|
|
120
|
+
except json.JSONDecodeError as e:
|
|
121
|
+
raise Exception(f"Failed to parse PyPI response: {str(e)}")
|
|
122
|
+
|
|
123
|
+
def _get_local_info(self, include_examples: bool = True) -> Dict[str, Any]:
|
|
124
|
+
"""Get package information from local configuration"""
|
|
125
|
+
if not self.local_info:
|
|
126
|
+
return {
|
|
127
|
+
"error": f"No local information available for package '{self.package_name}'",
|
|
128
|
+
"package_name": self.package_name,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
result = {
|
|
132
|
+
"package_name": self.local_info.get("name", self.package_name),
|
|
133
|
+
"description": self.local_info.get(
|
|
134
|
+
"description", "No description available"
|
|
135
|
+
),
|
|
136
|
+
"version": self.local_info.get("version", "Check PyPI for latest"),
|
|
137
|
+
"category": self.local_info.get("category", "General"),
|
|
138
|
+
"license": self.local_info.get("license", "Not specified"),
|
|
139
|
+
"documentation": self.local_info.get("documentation", ""),
|
|
140
|
+
"repository": self.local_info.get("repository", ""),
|
|
141
|
+
"import_name": self.local_info.get("import_name", self.package_name),
|
|
142
|
+
"python_versions": self.local_info.get("python_versions", ["3.6+"]),
|
|
143
|
+
"dependencies": self.local_info.get("dependencies", []),
|
|
144
|
+
"popularity": self.local_info.get("popularity", 0),
|
|
145
|
+
"keywords": self.local_info.get("keywords", []),
|
|
146
|
+
"installation": self._get_installation_instructions(),
|
|
147
|
+
"source": "local",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if include_examples:
|
|
151
|
+
result["usage_example"] = self._get_usage_example()
|
|
152
|
+
result["quick_start"] = self._get_quick_start_guide()
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def _get_installation_instructions(self) -> Dict[str, str]:
|
|
157
|
+
"""Generate installation instructions"""
|
|
158
|
+
custom_install = self.local_info.get("installation", {})
|
|
159
|
+
|
|
160
|
+
instructions = {
|
|
161
|
+
"pip": custom_install.get("pip", f"pip install {self.package_name}"),
|
|
162
|
+
"conda": custom_install.get("conda", f"conda install {self.package_name}"),
|
|
163
|
+
"pip_upgrade": f"pip install --upgrade {self.package_name}",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Add additional installation methods if specified
|
|
167
|
+
if "additional" in custom_install:
|
|
168
|
+
instructions.update(custom_install["additional"])
|
|
169
|
+
|
|
170
|
+
return instructions
|
|
171
|
+
|
|
172
|
+
def _get_usage_example(self) -> str:
|
|
173
|
+
"""Get usage example for the package"""
|
|
174
|
+
if self.local_info.get("usage_example"):
|
|
175
|
+
return self.local_info["usage_example"]
|
|
176
|
+
|
|
177
|
+
import_name = self.local_info.get("import_name", self.package_name)
|
|
178
|
+
return f"""# Basic usage example for {self.package_name}
|
|
179
|
+
import {import_name}
|
|
180
|
+
|
|
181
|
+
# Add your code here - check the documentation for specific usage
|
|
182
|
+
print({import_name}.__version__)"""
|
|
183
|
+
|
|
184
|
+
def _get_quick_start_guide(self) -> list:
|
|
185
|
+
"""Get quick start guide steps"""
|
|
186
|
+
if self.local_info.get("quick_start"):
|
|
187
|
+
return self.local_info["quick_start"]
|
|
188
|
+
|
|
189
|
+
import_name = self.local_info.get("import_name", self.package_name)
|
|
190
|
+
return [
|
|
191
|
+
f"1. Install the package: pip install {self.package_name}",
|
|
192
|
+
f"2. Import in your Python code: import {import_name}",
|
|
193
|
+
"3. Check the documentation for detailed usage examples",
|
|
194
|
+
"4. Start with basic examples and gradually explore advanced features",
|
|
195
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# pubchem_tool.py
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import re
|
|
5
|
+
from .base_tool import BaseTool
|
|
6
|
+
from .tool_registry import register_tool
|
|
7
|
+
|
|
8
|
+
# Base URL for PubChem PUG-REST
|
|
9
|
+
PUBCHEM_BASE_URL = "https://pubchem.ncbi.nlm.nih.gov/rest/pug"
|
|
10
|
+
|
|
11
|
+
# Base URL for PubChem PUG-View
|
|
12
|
+
PUBCHEM_PUGVIEW_URL = "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_tool("PubChemRESTTool")
|
|
16
|
+
class PubChemRESTTool(BaseTool):
|
|
17
|
+
"""
|
|
18
|
+
Generic PubChem PUG-REST tool class.
|
|
19
|
+
Directly concatenates URL from the fields.endpoint template and sends requests to PubChem PUG-REST.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, tool_config):
|
|
23
|
+
super().__init__(tool_config)
|
|
24
|
+
# Read endpoint template directly from fields config
|
|
25
|
+
self.endpoint_template = tool_config["fields"]["endpoint"]
|
|
26
|
+
# input_description and output_description might not be used, but kept for LLM reference
|
|
27
|
+
self.input_description = tool_config["fields"].get("input_description", "")
|
|
28
|
+
self.output_description = tool_config["fields"].get("output_description", "")
|
|
29
|
+
# If property_list exists, it will be used to replace {property_list} placeholder
|
|
30
|
+
self.property_list = tool_config["fields"].get("property_list", None)
|
|
31
|
+
# Parameter schema (properties may include required field)
|
|
32
|
+
self.param_schema = tool_config["parameter"]["properties"]
|
|
33
|
+
self.use_pugview = tool_config["fields"].get("use_pugview", False)
|
|
34
|
+
self.output_format = tool_config["fields"].get("return_format", None)
|
|
35
|
+
|
|
36
|
+
def _build_url(self, arguments: dict) -> str:
|
|
37
|
+
"""
|
|
38
|
+
Use regex to replace all {placeholder} in endpoint_template to generate complete URL path.
|
|
39
|
+
For example endpoint_template="/compound/cid/{cid}/property/{property_list}/JSON"
|
|
40
|
+
arguments={"cid":2244}, property_list=["MolecularWeight","IUPACName"]
|
|
41
|
+
→ "/compound/cid/2244/property/MolecularWeight,IUPACName/JSON"
|
|
42
|
+
Finally returns "https://pubchem.ncbi.nlm.nih.gov/rest/pug" + concatenated path.
|
|
43
|
+
"""
|
|
44
|
+
url_path = self.endpoint_template
|
|
45
|
+
|
|
46
|
+
# First replace property_list (if exists and endpoint_template contains this placeholder)
|
|
47
|
+
if self.property_list and "{property_list}" in url_path:
|
|
48
|
+
prop_str = ",".join(self.property_list)
|
|
49
|
+
url_path = url_path.replace("{property_list}", prop_str)
|
|
50
|
+
|
|
51
|
+
# Find all placeholders {xxx} in template
|
|
52
|
+
placeholders = re.findall(r"\{([^{}]+)\}", url_path)
|
|
53
|
+
for ph in placeholders:
|
|
54
|
+
if ph not in arguments:
|
|
55
|
+
# If a placeholder cannot find corresponding value in arguments, report error
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Missing required parameter '{ph}' to replace placeholder in URL."
|
|
58
|
+
)
|
|
59
|
+
val = arguments[ph]
|
|
60
|
+
# If input value is a list, join with commas
|
|
61
|
+
if isinstance(val, list):
|
|
62
|
+
val_str = ",".join(map(str, val))
|
|
63
|
+
else:
|
|
64
|
+
val_str = str(val)
|
|
65
|
+
url_path = url_path.replace(f"{{{ph}}}", val_str)
|
|
66
|
+
|
|
67
|
+
# Handle xref_types parameter
|
|
68
|
+
if "xref_types" in arguments:
|
|
69
|
+
xref_list = ",".join(arguments["xref_types"])
|
|
70
|
+
url_path = url_path.replace("{xref_list}", xref_list)
|
|
71
|
+
|
|
72
|
+
# Finally combine into complete URL
|
|
73
|
+
if self.use_pugview:
|
|
74
|
+
full_url = PUBCHEM_PUGVIEW_URL + url_path
|
|
75
|
+
else:
|
|
76
|
+
full_url = PUBCHEM_BASE_URL + url_path
|
|
77
|
+
|
|
78
|
+
# Handle special parameters
|
|
79
|
+
if "threshold" in arguments:
|
|
80
|
+
# Convert 0-1 threshold to 0-100 integer
|
|
81
|
+
threshold = float(arguments["threshold"])
|
|
82
|
+
if 0 <= threshold <= 1:
|
|
83
|
+
threshold = int(threshold * 100)
|
|
84
|
+
# Add threshold parameter to URL
|
|
85
|
+
if "?" in full_url:
|
|
86
|
+
full_url += f"&Threshold={threshold}"
|
|
87
|
+
else:
|
|
88
|
+
full_url += f"?Threshold={threshold}"
|
|
89
|
+
|
|
90
|
+
return full_url
|
|
91
|
+
|
|
92
|
+
def run(self, arguments: dict):
|
|
93
|
+
# 1. Validate required parameters
|
|
94
|
+
for key, prop in self.param_schema.items():
|
|
95
|
+
if prop.get("required", False) and key not in arguments:
|
|
96
|
+
return {"error": f"Parameter '{key}' is required."}
|
|
97
|
+
|
|
98
|
+
# 2. Build URL
|
|
99
|
+
try:
|
|
100
|
+
url = self._build_url(arguments)
|
|
101
|
+
except ValueError as e:
|
|
102
|
+
return {"error": str(e)}
|
|
103
|
+
|
|
104
|
+
# 3. Send HTTP GET request
|
|
105
|
+
try:
|
|
106
|
+
# Increase timeout to 30 seconds and add MaxRecords parameter to limit results
|
|
107
|
+
if "fastsubstructure" in url or "fastsimilarity" in url:
|
|
108
|
+
if "?" in url:
|
|
109
|
+
url += "&MaxRecords=10"
|
|
110
|
+
else:
|
|
111
|
+
url += "?MaxRecords=10"
|
|
112
|
+
|
|
113
|
+
resp = requests.get(url, timeout=30)
|
|
114
|
+
except requests.Timeout:
|
|
115
|
+
return {
|
|
116
|
+
"error": "Request to PubChem PUG-REST timed out, try reducing query scope or retry later."
|
|
117
|
+
}
|
|
118
|
+
except Exception as e:
|
|
119
|
+
return {"error": f"Failed to request PubChem PUG-REST: {str(e)}"}
|
|
120
|
+
|
|
121
|
+
# 4. Check HTTP status code
|
|
122
|
+
if resp.status_code != 200:
|
|
123
|
+
error_detail = resp.text
|
|
124
|
+
try:
|
|
125
|
+
error_json = resp.json()
|
|
126
|
+
if "Fault" in error_json:
|
|
127
|
+
error_detail = error_json["Fault"].get("Message", error_detail)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
return {
|
|
131
|
+
"error": f"PubChem API returned HTTP {resp.status_code}",
|
|
132
|
+
"detail": error_detail,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# 5. Determine return type based on URL suffix
|
|
136
|
+
# Look at the text after the last slash in endpoint_template, like "JSON","PNG","XML","TXT","CSV"
|
|
137
|
+
if self.output_format:
|
|
138
|
+
out_fmt = self.output_format
|
|
139
|
+
else:
|
|
140
|
+
out_fmt = self.endpoint_template.strip("/").split("/")[-1].upper()
|
|
141
|
+
|
|
142
|
+
if out_fmt == "JSON":
|
|
143
|
+
try:
|
|
144
|
+
return resp.json()
|
|
145
|
+
except ValueError:
|
|
146
|
+
return {
|
|
147
|
+
"error": "Response content cannot be parsed as JSON.",
|
|
148
|
+
"content": resp.text,
|
|
149
|
+
}
|
|
150
|
+
elif out_fmt in ["XML", "TXT", "CSV", "SDF"]:
|
|
151
|
+
# These are all text formats
|
|
152
|
+
return resp.text
|
|
153
|
+
elif out_fmt in ["PNG", "SVG"]:
|
|
154
|
+
# Return binary image
|
|
155
|
+
return resp.content
|
|
156
|
+
else:
|
|
157
|
+
# Return text for other cases
|
|
158
|
+
return resp.text
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .base_tool import BaseTool
|
|
11
|
+
from .tool_registry import register_tool
|
|
12
|
+
|
|
13
|
+
# Official REST root (cf. NIH “entity autocomplete” & “search” examples)
|
|
14
|
+
BASE_URL = "https://www.ncbi.nlm.nih.gov/research/pubtator3-api"
|
|
15
|
+
CONFIG_FILE = Path(__file__).with_name("pubtator_tool_config.json")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_tool("PubTatorTool")
|
|
19
|
+
class PubTatorTool(BaseTool):
|
|
20
|
+
"""Generic wrapper around a single PubTator 3 endpoint supporting JSON-defined configs."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, tool_config: Dict[str, Any]):
|
|
23
|
+
super().__init__(tool_config)
|
|
24
|
+
self._method: str = tool_config.get("method", "GET").upper()
|
|
25
|
+
self._path: str = tool_config["endpoint_path"]
|
|
26
|
+
self._param_map: Dict[str, str] = tool_config.get("param_map", {})
|
|
27
|
+
self._body_param: Optional[str] = tool_config.get("body_param")
|
|
28
|
+
self._id_in_path_key: Optional[str] = tool_config.get("id_in_path")
|
|
29
|
+
|
|
30
|
+
fields = tool_config.get("fields", {})
|
|
31
|
+
if "body_param" in fields:
|
|
32
|
+
self._body_param = fields["body_param"]
|
|
33
|
+
self._tool_subtype: str = fields.get("tool_subtype", "")
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------ public API --------------
|
|
36
|
+
def run(self, arguments: Dict[str, Any]):
|
|
37
|
+
args = arguments.copy()
|
|
38
|
+
# Special case for PubTatorRelation: combine parameters into a single "text" parameter and use "/search/" endpoint.
|
|
39
|
+
if self._tool_subtype == "PubTatorRelation":
|
|
40
|
+
subject = args.pop("subject_id", None)
|
|
41
|
+
obj = args.pop("object", None)
|
|
42
|
+
rel_type = args.pop("relation_type", None)
|
|
43
|
+
if not subject or not obj:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"Missing required parameters 'subject_id' or 'object' for relation search."
|
|
46
|
+
)
|
|
47
|
+
text_value = f"relations:{subject},{obj}"
|
|
48
|
+
if rel_type:
|
|
49
|
+
text_value += f",{rel_type}"
|
|
50
|
+
new_args = {"text": text_value}
|
|
51
|
+
new_args.update(args)
|
|
52
|
+
url = f"{BASE_URL.rstrip('/')}/search/"
|
|
53
|
+
data = None
|
|
54
|
+
headers: Dict[str, str] = {}
|
|
55
|
+
response = requests.request(
|
|
56
|
+
self._method,
|
|
57
|
+
url,
|
|
58
|
+
params=self._query_params(new_args),
|
|
59
|
+
data=data,
|
|
60
|
+
headers=headers,
|
|
61
|
+
timeout=30,
|
|
62
|
+
)
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
ctype = response.headers.get("Content-Type", "").lower()
|
|
65
|
+
if "json" in ctype:
|
|
66
|
+
return response.json()
|
|
67
|
+
if "text" in ctype or "xml" in ctype:
|
|
68
|
+
return response.text
|
|
69
|
+
return response.content
|
|
70
|
+
|
|
71
|
+
# Special handling for PubTatorAnnotate: override endpoint paths
|
|
72
|
+
if self._tool_subtype == "PubTatorAnnotate":
|
|
73
|
+
if self._method == "POST":
|
|
74
|
+
url = f"{BASE_URL.rstrip('/')}/annotations/annotate"
|
|
75
|
+
else:
|
|
76
|
+
url = f"{BASE_URL.rstrip('/')}/annotations/retrieve"
|
|
77
|
+
else:
|
|
78
|
+
url = self._compose_url(args)
|
|
79
|
+
|
|
80
|
+
# ---------- body handling for POST calls ----------
|
|
81
|
+
data: Optional[bytes] = None
|
|
82
|
+
headers: Dict[str, str] = {}
|
|
83
|
+
if self._method == "POST":
|
|
84
|
+
if self._body_param:
|
|
85
|
+
if self._body_param not in args:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Missing required body parameter '{self._body_param}'."
|
|
88
|
+
)
|
|
89
|
+
data = str(args.pop(self._body_param)).encode("utf-8")
|
|
90
|
+
headers["Content-Type"] = "text/plain; charset=utf-8"
|
|
91
|
+
else:
|
|
92
|
+
data = json.dumps(args).encode()
|
|
93
|
+
args.clear()
|
|
94
|
+
headers["Content-Type"] = "application/json"
|
|
95
|
+
|
|
96
|
+
# ---------- perform request ----------
|
|
97
|
+
response = requests.request(
|
|
98
|
+
self._method,
|
|
99
|
+
url,
|
|
100
|
+
params=self._query_params(args) if self._method != "POST" else {},
|
|
101
|
+
data=data,
|
|
102
|
+
headers=headers,
|
|
103
|
+
timeout=30,
|
|
104
|
+
)
|
|
105
|
+
if not response.ok:
|
|
106
|
+
return {
|
|
107
|
+
"error": f"Request failed with status code {response.status_code}: {response.text}"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# ---------- auto-detect & return ----------
|
|
111
|
+
ctype = response.headers.get("Content-Type", "").lower()
|
|
112
|
+
if "json" in ctype:
|
|
113
|
+
result = response.json()
|
|
114
|
+
# Extra filtering for PubTatorSearch: filter low-score items and facets.
|
|
115
|
+
if self._tool_subtype == "PubTatorSearch" and isinstance(result, dict):
|
|
116
|
+
result = self._filter_search_results(result)
|
|
117
|
+
return result
|
|
118
|
+
if "text" in ctype or "xml" in ctype:
|
|
119
|
+
return response.text
|
|
120
|
+
return response.content
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------ helpers -----------------
|
|
123
|
+
def _compose_url(self, args: Dict[str, Any]) -> str:
|
|
124
|
+
"""Substitute template vars & build full URL."""
|
|
125
|
+
path = self._path
|
|
126
|
+
for placeholder in re.findall(r"{(.*?)}", path):
|
|
127
|
+
if placeholder not in args:
|
|
128
|
+
raise ValueError(f"Missing URL placeholder argument '{placeholder}'.")
|
|
129
|
+
path = path.replace(f"{{{placeholder}}}", str(args.pop(placeholder)))
|
|
130
|
+
|
|
131
|
+
if self._id_in_path_key and self._id_in_path_key in args:
|
|
132
|
+
ids_val = args.pop(self._id_in_path_key)
|
|
133
|
+
if isinstance(ids_val, (list, tuple)):
|
|
134
|
+
ids_val = ",".join(map(str, ids_val))
|
|
135
|
+
path = f"{path}/{ids_val}"
|
|
136
|
+
|
|
137
|
+
return f"{BASE_URL.rstrip('/')}/{path.lstrip('/')}"
|
|
138
|
+
|
|
139
|
+
def _query_params(self, args: Dict[str, Any]) -> Dict[str, str]:
|
|
140
|
+
"""Translate caller arg names → API param names, drop Nones, serialise lists."""
|
|
141
|
+
q: Dict[str, str] = {}
|
|
142
|
+
for user_key, val in args.items():
|
|
143
|
+
if val is None:
|
|
144
|
+
continue
|
|
145
|
+
api_key = self._param_map.get(user_key, user_key)
|
|
146
|
+
if isinstance(val, (list, tuple)):
|
|
147
|
+
val = ",".join(map(str, val))
|
|
148
|
+
q[api_key] = str(val)
|
|
149
|
+
return q
|
|
150
|
+
|
|
151
|
+
def _filter_search_results(self, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
152
|
+
"""Filter PubTatorSearch results by score threshold and remove facet items that only have 'name', 'type', and 'value'."""
|
|
153
|
+
# Filter result items based on score threshold.
|
|
154
|
+
threshold = 230 # Adjust threshold as needed
|
|
155
|
+
if "results" in result and isinstance(result["results"], list):
|
|
156
|
+
filtered_results = []
|
|
157
|
+
for item in result["results"]:
|
|
158
|
+
score = item.get("score")
|
|
159
|
+
# If there's a numeric score and it's below threshold, skip the item.
|
|
160
|
+
if isinstance(score, (int, float)) and score < threshold:
|
|
161
|
+
continue
|
|
162
|
+
filtered_results.append(item)
|
|
163
|
+
result["results"] = filtered_results
|
|
164
|
+
|
|
165
|
+
# Also filter facets as before.
|
|
166
|
+
if "facets" in result and isinstance(result["facets"], dict):
|
|
167
|
+
del result["facets"]
|
|
168
|
+
return result
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from rcsbapi.data import DataQuery
|
|
2
|
+
from .base_tool import BaseTool
|
|
3
|
+
from .tool_registry import register_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register_tool("RCSBTool")
|
|
7
|
+
class RCSBTool(BaseTool):
|
|
8
|
+
def __init__(self, tool_config):
|
|
9
|
+
super().__init__(tool_config)
|
|
10
|
+
self.name = tool_config.get("name")
|
|
11
|
+
self.description = tool_config.get("description")
|
|
12
|
+
self.input_type = tool_config.get("input_type")
|
|
13
|
+
self.search_fields = tool_config.get("fields", {}).get("search_fields", {})
|
|
14
|
+
self.return_fields = tool_config.get("fields", {}).get("return_fields", [])
|
|
15
|
+
self.parameter_schema = tool_config.get("parameter", {}).get("properties", {})
|
|
16
|
+
|
|
17
|
+
def validate_params(self, params: dict):
|
|
18
|
+
for param_name, param_info in self.parameter_schema.items():
|
|
19
|
+
if param_info.get("required", False) and param_name not in params:
|
|
20
|
+
raise ValueError(f"Missing required parameter: {param_name}")
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
def prepare_input_ids(self, params: dict):
|
|
24
|
+
for param_name in self.search_fields:
|
|
25
|
+
if param_name in params:
|
|
26
|
+
val = params[param_name]
|
|
27
|
+
return val if isinstance(val, list) else [val]
|
|
28
|
+
raise ValueError("No valid search parameter provided")
|
|
29
|
+
|
|
30
|
+
def run(self, params: dict):
|
|
31
|
+
self.validate_params(params)
|
|
32
|
+
input_ids = self.prepare_input_ids(params)
|
|
33
|
+
query = DataQuery(
|
|
34
|
+
input_type=self.input_type,
|
|
35
|
+
input_ids=input_ids,
|
|
36
|
+
return_data_list=self.return_fields,
|
|
37
|
+
)
|
|
38
|
+
return query.exec()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# reactome_graph_tool.py
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import re
|
|
5
|
+
from .base_tool import BaseTool
|
|
6
|
+
from .tool_registry import register_tool
|
|
7
|
+
|
|
8
|
+
# Reactome Content Service Base URL
|
|
9
|
+
REACTOME_BASE_URL = "https://reactome.org/ContentService"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register_tool("ReactomeRESTTool")
|
|
13
|
+
class ReactomeRESTTool(BaseTool):
|
|
14
|
+
"""
|
|
15
|
+
Generic Reactome Content Service REST tool.
|
|
16
|
+
If there is no "fields.extract_path" in config or its value is empty, returns complete JSON;
|
|
17
|
+
Otherwise, drills down according to the "dot-separated path" in extract_path and returns corresponding sub-node.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, tool_config):
|
|
21
|
+
super().__init__(tool_config)
|
|
22
|
+
self.endpoint_template = tool_config["endpoint"] # e.g. "/data/pathway/{stId}"
|
|
23
|
+
self.method = tool_config.get("method", "GET").upper() # Default to GET
|
|
24
|
+
self.param_schema = tool_config["parameter"][
|
|
25
|
+
"properties"
|
|
26
|
+
] # Parameter schema (including required)
|
|
27
|
+
self.required_params = tool_config["parameter"].get(
|
|
28
|
+
"required", []
|
|
29
|
+
) # List of required parameters
|
|
30
|
+
# If config has fields and it contains extract_path, take it. Otherwise None.
|
|
31
|
+
self.extract_path = None
|
|
32
|
+
if "fields" in tool_config and isinstance(tool_config["fields"], dict):
|
|
33
|
+
ep = tool_config["fields"].get("extract_path", None)
|
|
34
|
+
if ep is not None and isinstance(ep, str) and ep.strip() != "":
|
|
35
|
+
# Only effective when extract_path is a non-empty string
|
|
36
|
+
self.extract_path = ep.strip()
|
|
37
|
+
|
|
38
|
+
def _build_url(self, arguments: dict) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Combines endpoint_template (containing {xxx}) with path parameters from arguments to generate complete URL.
|
|
41
|
+
For example endpoint_template="/data/pathway/{stId}", arguments={"stId":"R-HSA-73817"}
|
|
42
|
+
→ Returns "https://reactome.org/ContentService/data/pathway/R-HSA-73817"
|
|
43
|
+
"""
|
|
44
|
+
url_path = self.endpoint_template
|
|
45
|
+
# Find all {xxx} placeholders and replace with values from arguments
|
|
46
|
+
for key in re.findall(r"\{([^{}]+)\}", self.endpoint_template):
|
|
47
|
+
if key not in arguments:
|
|
48
|
+
raise ValueError(f"Missing path parameter '{key}'")
|
|
49
|
+
url_path = url_path.replace(f"{{{key}}}", str(arguments[key]))
|
|
50
|
+
return REACTOME_BASE_URL + url_path
|
|
51
|
+
|
|
52
|
+
def run(self, arguments: dict):
|
|
53
|
+
# 1. Validate required parameters (check from required_params list)
|
|
54
|
+
for required_param in self.required_params:
|
|
55
|
+
if required_param not in arguments:
|
|
56
|
+
return {"error": f"Parameter '{required_param}' is required."}
|
|
57
|
+
|
|
58
|
+
# 2. Build URL, replace {xxx} placeholders
|
|
59
|
+
try:
|
|
60
|
+
url = self._build_url(arguments)
|
|
61
|
+
except ValueError as e:
|
|
62
|
+
return {"error": str(e)}
|
|
63
|
+
|
|
64
|
+
# 3. Find remaining arguments besides path parameters as query parameters
|
|
65
|
+
path_keys = re.findall(r"\{([^{}]+)\}", self.endpoint_template)
|
|
66
|
+
query_params = {}
|
|
67
|
+
for k, v in arguments.items():
|
|
68
|
+
if k not in path_keys:
|
|
69
|
+
query_params[k] = v
|
|
70
|
+
|
|
71
|
+
# 4. Make HTTP request
|
|
72
|
+
try:
|
|
73
|
+
if self.method == "GET":
|
|
74
|
+
resp = requests.get(url, params=query_params, timeout=10)
|
|
75
|
+
else:
|
|
76
|
+
# If POST support needed in future, can extend here
|
|
77
|
+
resp = requests.post(url, json=query_params, timeout=10)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
return {"error": f"Failed to request Reactome Content Service: {str(e)}"}
|
|
80
|
+
|
|
81
|
+
# 5. Check HTTP status code
|
|
82
|
+
if resp.status_code != 200:
|
|
83
|
+
return {
|
|
84
|
+
"error": f"Reactome API returned HTTP {resp.status_code}",
|
|
85
|
+
"detail": resp.text,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# 6. Parse JSON
|
|
89
|
+
try:
|
|
90
|
+
data = resp.json()
|
|
91
|
+
except ValueError:
|
|
92
|
+
return {
|
|
93
|
+
"error": "Unable to parse Reactome returned JSON.",
|
|
94
|
+
"content": resp.text,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# 7. If no extract_path in config, return complete JSON
|
|
98
|
+
if not self.extract_path:
|
|
99
|
+
return data
|
|
100
|
+
|
|
101
|
+
# 8. Otherwise drill down according to "dot-separated path" in extract_path
|
|
102
|
+
fragment = data
|
|
103
|
+
for part in self.extract_path.split("."):
|
|
104
|
+
if isinstance(fragment, dict) and part in fragment:
|
|
105
|
+
fragment = fragment[part]
|
|
106
|
+
else:
|
|
107
|
+
return {"error": f"Path '{self.extract_path}' not found in JSON."}
|
|
108
|
+
return fragment
|