tooluniverse 0.1.4__py3-none-any.whl → 1.0.0__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 (187) hide show
  1. tooluniverse/__init__.py +340 -4
  2. tooluniverse/admetai_tool.py +84 -0
  3. tooluniverse/agentic_tool.py +563 -0
  4. tooluniverse/alphafold_tool.py +96 -0
  5. tooluniverse/base_tool.py +129 -6
  6. tooluniverse/boltz_tool.py +207 -0
  7. tooluniverse/chem_tool.py +192 -0
  8. tooluniverse/compose_scripts/__init__.py +1 -0
  9. tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
  10. tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
  11. tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
  12. tooluniverse/compose_scripts/literature_tool.py +34 -0
  13. tooluniverse/compose_scripts/output_summarizer.py +279 -0
  14. tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
  15. tooluniverse/compose_scripts/tool_discover.py +705 -0
  16. tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
  17. tooluniverse/compose_tool.py +371 -0
  18. tooluniverse/ctg_tool.py +1002 -0
  19. tooluniverse/custom_tool.py +81 -0
  20. tooluniverse/dailymed_tool.py +108 -0
  21. tooluniverse/data/admetai_tools.json +155 -0
  22. tooluniverse/data/agentic_tools.json +1156 -0
  23. tooluniverse/data/alphafold_tools.json +87 -0
  24. tooluniverse/data/boltz_tools.json +9 -0
  25. tooluniverse/data/chembl_tools.json +16 -0
  26. tooluniverse/data/clait_tools.json +108 -0
  27. tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
  28. tooluniverse/data/compose_tools.json +202 -0
  29. tooluniverse/data/dailymed_tools.json +70 -0
  30. tooluniverse/data/dataset_tools.json +646 -0
  31. tooluniverse/data/disease_target_score_tools.json +712 -0
  32. tooluniverse/data/efo_tools.json +17 -0
  33. tooluniverse/data/embedding_tools.json +319 -0
  34. tooluniverse/data/enrichr_tools.json +31 -0
  35. tooluniverse/data/europe_pmc_tools.json +22 -0
  36. tooluniverse/data/expert_feedback_tools.json +10 -0
  37. tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
  38. tooluniverse/data/fda_drug_labeling_tools.json +544 -168
  39. tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
  40. tooluniverse/data/finder_tools.json +209 -0
  41. tooluniverse/data/gene_ontology_tools.json +113 -0
  42. tooluniverse/data/gwas_tools.json +1082 -0
  43. tooluniverse/data/hpa_tools.json +333 -0
  44. tooluniverse/data/humanbase_tools.json +47 -0
  45. tooluniverse/data/idmap_tools.json +74 -0
  46. tooluniverse/data/mcp_client_tools_example.json +113 -0
  47. tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
  48. tooluniverse/data/medlineplus_tools.json +141 -0
  49. tooluniverse/data/monarch_tools.json +1 -1
  50. tooluniverse/data/openalex_tools.json +36 -0
  51. tooluniverse/data/opentarget_tools.json +82 -58
  52. tooluniverse/data/output_summarization_tools.json +101 -0
  53. tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
  54. tooluniverse/data/packages/categorized_tools.txt +206 -0
  55. tooluniverse/data/packages/cheminformatics_tools.json +347 -0
  56. tooluniverse/data/packages/earth_sciences_tools.json +74 -0
  57. tooluniverse/data/packages/genomics_tools.json +776 -0
  58. tooluniverse/data/packages/image_processing_tools.json +38 -0
  59. tooluniverse/data/packages/machine_learning_tools.json +789 -0
  60. tooluniverse/data/packages/neuroscience_tools.json +62 -0
  61. tooluniverse/data/packages/original_tools.txt +0 -0
  62. tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
  63. tooluniverse/data/packages/scientific_computing_tools.json +560 -0
  64. tooluniverse/data/packages/single_cell_tools.json +453 -0
  65. tooluniverse/data/packages/software_tools.json +4954 -0
  66. tooluniverse/data/packages/structural_biology_tools.json +396 -0
  67. tooluniverse/data/packages/visualization_tools.json +399 -0
  68. tooluniverse/data/pubchem_tools.json +215 -0
  69. tooluniverse/data/pubtator_tools.json +68 -0
  70. tooluniverse/data/rcsb_pdb_tools.json +1332 -0
  71. tooluniverse/data/reactome_tools.json +19 -0
  72. tooluniverse/data/semantic_scholar_tools.json +26 -0
  73. tooluniverse/data/special_tools.json +2 -25
  74. tooluniverse/data/tool_composition_tools.json +88 -0
  75. tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
  76. tooluniverse/data/txagent_client_tools.json +9 -0
  77. tooluniverse/data/uniprot_tools.json +211 -0
  78. tooluniverse/data/url_fetch_tools.json +94 -0
  79. tooluniverse/data/uspto_downloader_tools.json +9 -0
  80. tooluniverse/data/uspto_tools.json +811 -0
  81. tooluniverse/data/xml_tools.json +3275 -0
  82. tooluniverse/dataset_tool.py +296 -0
  83. tooluniverse/default_config.py +165 -0
  84. tooluniverse/efo_tool.py +42 -0
  85. tooluniverse/embedding_database.py +630 -0
  86. tooluniverse/embedding_sync.py +396 -0
  87. tooluniverse/enrichr_tool.py +266 -0
  88. tooluniverse/europe_pmc_tool.py +52 -0
  89. tooluniverse/execute_function.py +1775 -95
  90. tooluniverse/extended_hooks.py +444 -0
  91. tooluniverse/gene_ontology_tool.py +194 -0
  92. tooluniverse/graphql_tool.py +158 -36
  93. tooluniverse/gwas_tool.py +358 -0
  94. tooluniverse/hpa_tool.py +1645 -0
  95. tooluniverse/humanbase_tool.py +389 -0
  96. tooluniverse/logging_config.py +254 -0
  97. tooluniverse/mcp_client_tool.py +764 -0
  98. tooluniverse/mcp_integration.py +413 -0
  99. tooluniverse/mcp_tool_registry.py +925 -0
  100. tooluniverse/medlineplus_tool.py +337 -0
  101. tooluniverse/openalex_tool.py +228 -0
  102. tooluniverse/openfda_adv_tool.py +283 -0
  103. tooluniverse/openfda_tool.py +393 -160
  104. tooluniverse/output_hook.py +1122 -0
  105. tooluniverse/package_tool.py +195 -0
  106. tooluniverse/pubchem_tool.py +158 -0
  107. tooluniverse/pubtator_tool.py +168 -0
  108. tooluniverse/rcsb_pdb_tool.py +38 -0
  109. tooluniverse/reactome_tool.py +108 -0
  110. tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
  111. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
  112. tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
  113. tooluniverse/remote/expert_feedback/simple_test.py +23 -0
  114. tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
  115. tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
  116. tooluniverse/remote/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
  117. tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
  118. tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -0
  119. tooluniverse/remote/immune_compass/compass_tool.py +327 -0
  120. tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
  121. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
  122. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
  123. tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
  124. tooluniverse/remote_tool.py +99 -0
  125. tooluniverse/restful_tool.py +53 -30
  126. tooluniverse/scripts/generate_tool_graph.py +408 -0
  127. tooluniverse/scripts/visualize_tool_graph.py +829 -0
  128. tooluniverse/semantic_scholar_tool.py +62 -0
  129. tooluniverse/smcp.py +2452 -0
  130. tooluniverse/smcp_server.py +975 -0
  131. tooluniverse/test/mcp_server_test.py +0 -0
  132. tooluniverse/test/test_admetai_tool.py +370 -0
  133. tooluniverse/test/test_agentic_tool.py +129 -0
  134. tooluniverse/test/test_alphafold_tool.py +71 -0
  135. tooluniverse/test/test_chem_tool.py +37 -0
  136. tooluniverse/test/test_compose_lieraturereview.py +63 -0
  137. tooluniverse/test/test_compose_tool.py +448 -0
  138. tooluniverse/test/test_dailymed.py +69 -0
  139. tooluniverse/test/test_dataset_tool.py +200 -0
  140. tooluniverse/test/test_disease_target_score.py +56 -0
  141. tooluniverse/test/test_drugbank_filter_examples.py +179 -0
  142. tooluniverse/test/test_efo.py +31 -0
  143. tooluniverse/test/test_enrichr_tool.py +21 -0
  144. tooluniverse/test/test_europe_pmc_tool.py +20 -0
  145. tooluniverse/test/test_fda_adv.py +95 -0
  146. tooluniverse/test/test_fda_drug_labeling.py +91 -0
  147. tooluniverse/test/test_gene_ontology_tools.py +66 -0
  148. tooluniverse/test/test_gwas_tool.py +139 -0
  149. tooluniverse/test/test_hpa.py +625 -0
  150. tooluniverse/test/test_humanbase_tool.py +20 -0
  151. tooluniverse/test/test_idmap_tools.py +61 -0
  152. tooluniverse/test/test_mcp_server.py +211 -0
  153. tooluniverse/test/test_mcp_tool.py +247 -0
  154. tooluniverse/test/test_medlineplus.py +220 -0
  155. tooluniverse/test/test_openalex_tool.py +32 -0
  156. tooluniverse/test/test_opentargets.py +28 -0
  157. tooluniverse/test/test_pubchem_tool.py +116 -0
  158. tooluniverse/test/test_pubtator_tool.py +37 -0
  159. tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
  160. tooluniverse/test/test_reactome.py +54 -0
  161. tooluniverse/test/test_semantic_scholar_tool.py +24 -0
  162. tooluniverse/test/test_software_tools.py +147 -0
  163. tooluniverse/test/test_tool_description_optimizer.py +49 -0
  164. tooluniverse/test/test_tool_finder.py +26 -0
  165. tooluniverse/test/test_tool_finder_llm.py +252 -0
  166. tooluniverse/test/test_tools_find.py +195 -0
  167. tooluniverse/test/test_uniprot_tools.py +74 -0
  168. tooluniverse/test/test_uspto_tool.py +72 -0
  169. tooluniverse/test/test_xml_tool.py +113 -0
  170. tooluniverse/tool_finder_embedding.py +267 -0
  171. tooluniverse/tool_finder_keyword.py +693 -0
  172. tooluniverse/tool_finder_llm.py +699 -0
  173. tooluniverse/tool_graph_web_ui.py +955 -0
  174. tooluniverse/tool_registry.py +416 -0
  175. tooluniverse/uniprot_tool.py +155 -0
  176. tooluniverse/url_tool.py +253 -0
  177. tooluniverse/uspto_tool.py +240 -0
  178. tooluniverse/utils.py +369 -41
  179. tooluniverse/xml_tool.py +369 -0
  180. tooluniverse-1.0.0.dist-info/METADATA +377 -0
  181. tooluniverse-1.0.0.dist-info/RECORD +186 -0
  182. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +1 -1
  183. tooluniverse-1.0.0.dist-info/entry_points.txt +9 -0
  184. tooluniverse-0.1.4.dist-info/METADATA +0 -141
  185. tooluniverse-0.1.4.dist-info/RECORD +0 -18
  186. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
  187. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.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