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,444 @@
1
+ """
2
+ Extended Hook Types for ToolUniverse
3
+
4
+ This module demonstrates how to extend the hook system with additional
5
+ hook types beyond summarization. It shows the pattern for creating
6
+ new hook types while maintaining compatibility with the existing system.
7
+ """
8
+
9
+ import re
10
+ import json
11
+ from typing import Dict, Any, List
12
+ from .output_hook import OutputHook
13
+
14
+
15
+ class FilteringHook(OutputHook):
16
+ """
17
+ Hook for filtering sensitive or unwanted content from tool outputs.
18
+
19
+ This hook can be used to:
20
+ - Remove sensitive information (emails, phones, SSNs)
21
+ - Filter inappropriate content
22
+ - Sanitize data before display
23
+
24
+ Args:
25
+ config (Dict[str, Any]): Hook configuration containing filter settings
26
+ tooluniverse: Optional ToolUniverse instance (not used for filtering)
27
+ """
28
+
29
+ def __init__(self, config: Dict[str, Any], tooluniverse=None):
30
+ """
31
+ Initialize the filtering hook with configuration.
32
+
33
+ Args:
34
+ config (Dict[str, Any]): Hook configuration
35
+ tooluniverse: ToolUniverse instance (optional, not used)
36
+ """
37
+ super().__init__(config)
38
+ hook_config = config.get("hook_config", {})
39
+
40
+ # Filter configuration
41
+ self.filter_patterns = hook_config.get("filter_patterns", [])
42
+ self.replacement_text = hook_config.get("replacement_text", "[REDACTED]")
43
+ self.preserve_structure = hook_config.get("preserve_structure", True)
44
+ self.log_filtered_items = hook_config.get("log_filtered_items", False)
45
+
46
+ # Compile regex patterns for efficiency
47
+ self.compiled_patterns = []
48
+ for pattern in self.filter_patterns:
49
+ try:
50
+ compiled = re.compile(pattern, re.IGNORECASE)
51
+ self.compiled_patterns.append(compiled)
52
+ except re.error as e:
53
+ print(f"Warning: Invalid regex pattern '{pattern}': {e}")
54
+
55
+ def process(
56
+ self,
57
+ result: Any,
58
+ tool_name: str,
59
+ arguments: Dict[str, Any],
60
+ context: Dict[str, Any],
61
+ ) -> Any:
62
+ """
63
+ Apply filtering to the tool output.
64
+
65
+ Args:
66
+ result (Any): The tool output to filter
67
+ tool_name (str): Name of the tool that produced the output
68
+ arguments (Dict[str, Any]): Arguments passed to the tool
69
+ context (Dict[str, Any]): Additional context information
70
+
71
+ Returns:
72
+ Any: The filtered output, or original output if filtering fails
73
+ """
74
+ try:
75
+ if not self.compiled_patterns:
76
+ return result
77
+
78
+ output_str = result if isinstance(result, str) else str(result)
79
+ filtered_output = output_str
80
+ filtered_count = 0
81
+
82
+ # Apply each filter pattern
83
+ for pattern in self.compiled_patterns:
84
+ matches = pattern.findall(filtered_output)
85
+ if matches:
86
+ filtered_count += len(matches)
87
+ if self.log_filtered_items:
88
+ print(
89
+ f"🔒 Filtered {len(matches)} items matching pattern: {pattern.pattern}"
90
+ )
91
+
92
+ filtered_output = pattern.sub(
93
+ self.replacement_text, filtered_output
94
+ )
95
+
96
+ if filtered_count > 0:
97
+ print(
98
+ f"🔒 FilteringHook: Filtered {filtered_count} sensitive items from {tool_name} output"
99
+ )
100
+
101
+ return filtered_output
102
+
103
+ except Exception as e:
104
+ print(f"Error in filtering hook: {str(e)}")
105
+ return result
106
+
107
+
108
+ class FormattingHook(OutputHook):
109
+ """
110
+ Hook for formatting and beautifying tool outputs.
111
+
112
+ This hook can be used to:
113
+ - Pretty-print JSON/XML outputs
114
+ - Format text with proper indentation
115
+ - Standardize output formats
116
+
117
+ Args:
118
+ config (Dict[str, Any]): Hook configuration containing formatting settings
119
+ tooluniverse: Optional ToolUniverse instance (not used for formatting)
120
+ """
121
+
122
+ def __init__(self, config: Dict[str, Any], tooluniverse=None):
123
+ """
124
+ Initialize the formatting hook with configuration.
125
+
126
+ Args:
127
+ config (Dict[str, Any]): Hook configuration
128
+ tooluniverse: ToolUniverse instance (optional, not used)
129
+ """
130
+ super().__init__(config)
131
+ hook_config = config.get("hook_config", {})
132
+
133
+ # Formatting configuration
134
+ self.indent_size = hook_config.get("indent_size", 2)
135
+ self.sort_keys = hook_config.get("sort_keys", True)
136
+ self.ensure_ascii = hook_config.get("ensure_ascii", False)
137
+ self.max_line_length = hook_config.get("max_line_length", 100)
138
+ self.pretty_print = hook_config.get("pretty_print", True)
139
+
140
+ def process(
141
+ self,
142
+ result: Any,
143
+ tool_name: str,
144
+ arguments: Dict[str, Any],
145
+ context: Dict[str, Any],
146
+ ) -> Any:
147
+ """
148
+ Apply formatting to the tool output.
149
+
150
+ Args:
151
+ result (Any): The tool output to format
152
+ tool_name (str): Name of the tool that produced the output
153
+ arguments (Dict[str, Any]): Arguments passed to the tool
154
+ context (Dict[str, Any]): Additional context information
155
+
156
+ Returns:
157
+ Any: The formatted output, or original output if formatting fails
158
+ """
159
+ try:
160
+ if isinstance(result, dict):
161
+ return self._format_json(result)
162
+ elif isinstance(result, str):
163
+ return self._format_text(result)
164
+ elif isinstance(result, list):
165
+ return self._format_list(result)
166
+ else:
167
+ return result
168
+
169
+ except Exception as e:
170
+ print(f"Error in formatting hook: {str(e)}")
171
+ return result
172
+
173
+ def _format_json(self, data: Dict[str, Any]) -> str:
174
+ """Format JSON data with pretty printing."""
175
+ if self.pretty_print:
176
+ return json.dumps(
177
+ data,
178
+ indent=self.indent_size,
179
+ sort_keys=self.sort_keys,
180
+ ensure_ascii=self.ensure_ascii,
181
+ )
182
+ return json.dumps(data, ensure_ascii=self.ensure_ascii)
183
+
184
+ def _format_text(self, text: str) -> str:
185
+ """Format plain text with line wrapping."""
186
+ if len(text) <= self.max_line_length:
187
+ return text
188
+
189
+ # Simple line wrapping
190
+ lines = []
191
+ current_line = ""
192
+
193
+ for word in text.split():
194
+ if len(current_line + " " + word) <= self.max_line_length:
195
+ current_line += (" " + word) if current_line else word
196
+ else:
197
+ if current_line:
198
+ lines.append(current_line)
199
+ current_line = word
200
+
201
+ if current_line:
202
+ lines.append(current_line)
203
+
204
+ return "\n".join(lines)
205
+
206
+ def _format_list(self, data: List[Any]) -> str:
207
+ """Format list data."""
208
+ if self.pretty_print:
209
+ formatted_items = []
210
+ for i, item in enumerate(data):
211
+ if isinstance(item, dict):
212
+ formatted_items.append(f"{i+1}. {self._format_json(item)}")
213
+ else:
214
+ formatted_items.append(f"{i+1}. {str(item)}")
215
+ return "\n".join(formatted_items)
216
+ return str(data)
217
+
218
+
219
+ class ValidationHook(OutputHook):
220
+ """
221
+ Hook for validating tool outputs against schemas or rules.
222
+
223
+ This hook can be used to:
224
+ - Validate JSON against schemas
225
+ - Check required fields
226
+ - Ensure data quality
227
+
228
+ Args:
229
+ config (Dict[str, Any]): Hook configuration containing validation settings
230
+ tooluniverse: Optional ToolUniverse instance (not used for validation)
231
+ """
232
+
233
+ def __init__(self, config: Dict[str, Any], tooluniverse=None):
234
+ """
235
+ Initialize the validation hook with configuration.
236
+
237
+ Args:
238
+ config (Dict[str, Any]): Hook configuration
239
+ tooluniverse: ToolUniverse instance (optional, not used)
240
+ """
241
+ super().__init__(config)
242
+ hook_config = config.get("hook_config", {})
243
+
244
+ # Validation configuration
245
+ self.validation_schema = hook_config.get("validation_schema", None)
246
+ self.strict_mode = hook_config.get("strict_mode", True)
247
+ self.error_action = hook_config.get("error_action", "warn") # warn, fix, fail
248
+ self.required_fields = hook_config.get("required_fields", [])
249
+
250
+ def process(
251
+ self,
252
+ result: Any,
253
+ tool_name: str,
254
+ arguments: Dict[str, Any],
255
+ context: Dict[str, Any],
256
+ ) -> Any:
257
+ """
258
+ Apply validation to the tool output.
259
+
260
+ Args:
261
+ result (Any): The tool output to validate
262
+ tool_name (str): Name of the tool that produced the output
263
+ arguments (Dict[str, Any]): Arguments passed to the tool
264
+ context (Dict[str, Any]): Additional context information
265
+
266
+ Returns:
267
+ Any: The validated output, or original output if validation fails
268
+ """
269
+ try:
270
+ validation_result = self._validate_output(result)
271
+
272
+ if validation_result["valid"]:
273
+ if validation_result["warnings"]:
274
+ print(
275
+ f"✅ ValidationHook: {tool_name} output validated with warnings"
276
+ )
277
+ else:
278
+ print(
279
+ f"✅ ValidationHook: {tool_name} output validated successfully"
280
+ )
281
+ return result
282
+ else:
283
+ if self.error_action == "fail":
284
+ print(f"❌ ValidationHook: {tool_name} output validation failed")
285
+ return result
286
+ elif self.error_action == "fix":
287
+ fixed_result = self._fix_output(result, validation_result["errors"])
288
+ print(
289
+ f"🔧 ValidationHook: Fixed {tool_name} output based on validation errors"
290
+ )
291
+ return fixed_result
292
+ else: # warn
293
+ print(f"⚠️ ValidationHook: {tool_name} output has validation issues")
294
+ return result
295
+
296
+ except Exception as e:
297
+ print(f"Error in validation hook: {str(e)}")
298
+ return result
299
+
300
+ def _validate_output(self, result: Any) -> Dict[str, Any]:
301
+ """Validate the output against configured rules."""
302
+ validation_result = {"valid": True, "errors": [], "warnings": []}
303
+
304
+ # Check required fields for dict outputs
305
+ if isinstance(result, dict) and self.required_fields:
306
+ for field in self.required_fields:
307
+ if field not in result:
308
+ validation_result["errors"].append(
309
+ f"Missing required field: {field}"
310
+ )
311
+ validation_result["valid"] = False
312
+
313
+ # Add schema validation here if needed
314
+ # This would integrate with jsonschema or similar libraries
315
+
316
+ return validation_result
317
+
318
+ def _fix_output(self, result: Any, errors: List[str]) -> Any:
319
+ """Attempt to fix validation errors."""
320
+ # Simple fixes for common issues
321
+ if isinstance(result, dict):
322
+ fixed_result = result.copy()
323
+
324
+ for error in errors:
325
+ if "Missing required field" in error:
326
+ field_name = error.split(": ")[1]
327
+ fixed_result[field_name] = None # Add missing field with None value
328
+
329
+ return fixed_result
330
+
331
+ return result
332
+
333
+
334
+ class LoggingHook(OutputHook):
335
+ """
336
+ Hook for logging tool outputs and execution details.
337
+
338
+ This hook can be used to:
339
+ - Log all tool outputs
340
+ - Track execution metrics
341
+ - Audit tool usage
342
+
343
+ Args:
344
+ config (Dict[str, Any]): Hook configuration containing logging settings
345
+ tooluniverse: Optional ToolUniverse instance (not used for logging)
346
+ """
347
+
348
+ def __init__(self, config: Dict[str, Any], tooluniverse=None):
349
+ """
350
+ Initialize the logging hook with configuration.
351
+
352
+ Args:
353
+ config (Dict[str, Any]): Hook configuration
354
+ tooluniverse: ToolUniverse instance (optional, not used)
355
+ """
356
+ super().__init__(config)
357
+ hook_config = config.get("hook_config", {})
358
+
359
+ # Logging configuration
360
+ self.log_level = hook_config.get("log_level", "INFO")
361
+ self.log_format = hook_config.get(
362
+ "log_format", "detailed"
363
+ ) # simple, detailed, json
364
+ self.log_file = hook_config.get("log_file", None)
365
+ self.max_log_size = hook_config.get("max_log_size", 1000) # characters
366
+
367
+ def process(
368
+ self,
369
+ result: Any,
370
+ tool_name: str,
371
+ arguments: Dict[str, Any],
372
+ context: Dict[str, Any],
373
+ ) -> Any:
374
+ """
375
+ Log the tool output and execution details.
376
+
377
+ Args:
378
+ result (Any): The tool output to log
379
+ tool_name (str): Name of the tool that produced the output
380
+ arguments (Dict[str, Any]): Arguments passed to the tool
381
+ context (Dict[str, Any]): Additional context information
382
+
383
+ Returns:
384
+ Any: The original output (logging doesn't modify the output)
385
+ """
386
+ try:
387
+ log_entry = self._create_log_entry(result, tool_name, arguments, context)
388
+ self._write_log(log_entry)
389
+
390
+ except Exception as e:
391
+ print(f"Error in logging hook: {str(e)}")
392
+
393
+ # Logging hook always returns the original result unchanged
394
+ return result
395
+
396
+ def _create_log_entry(
397
+ self,
398
+ result: Any,
399
+ tool_name: str,
400
+ arguments: Dict[str, Any],
401
+ context: Dict[str, Any],
402
+ ) -> str:
403
+ """Create a log entry for the tool execution."""
404
+ if self.log_format == "simple":
405
+ return f"Tool: {tool_name} | Args: {arguments} | Output length: {len(str(result))}"
406
+ elif self.log_format == "json":
407
+ return json.dumps(
408
+ {
409
+ "tool_name": tool_name,
410
+ "arguments": arguments,
411
+ "output_length": len(str(result)),
412
+ "timestamp": context.get("execution_time", "unknown"),
413
+ "output_preview": str(result)[: self.max_log_size],
414
+ }
415
+ )
416
+ else: # detailed
417
+ return f"""
418
+ Tool Execution Log:
419
+ ==================
420
+ Tool: {tool_name}
421
+ Arguments: {arguments}
422
+ Execution Time: {context.get('execution_time', 'unknown')}
423
+ Output Length: {len(str(result))} characters
424
+ Output Preview: {str(result)[:self.max_log_size]}{'...' if len(str(result)) > self.max_log_size else ''}
425
+ ==================
426
+ """
427
+
428
+ def _write_log(self, log_entry: str):
429
+ """Write the log entry to the configured destination."""
430
+ if self.log_file:
431
+ with open(self.log_file, "a", encoding="utf-8") as f:
432
+ f.write(log_entry + "\n")
433
+ else:
434
+ print(f"📝 Log: {log_entry}")
435
+
436
+
437
+ # Hook type registry for easy extension
438
+ HOOK_TYPE_REGISTRY = {
439
+ "SummarizationHook": "SummarizationHook", # Import from parent module
440
+ "FilteringHook": FilteringHook,
441
+ "FormattingHook": FormattingHook,
442
+ "ValidationHook": ValidationHook,
443
+ "LoggingHook": LoggingHook,
444
+ }
@@ -0,0 +1,194 @@
1
+ import requests
2
+ from typing import Any, Dict, Optional
3
+ from urllib.parse import quote
4
+ from .base_tool import BaseTool
5
+ from .tool_registry import register_tool
6
+
7
+
8
+ @register_tool("GeneOntologyTool")
9
+ class GeneOntologyTool(BaseTool):
10
+ """
11
+ A general-purpose tool for calling the Gene Ontology (GO) API.
12
+ It is configured via a dictionary that defines the specific API endpoint.
13
+ """
14
+
15
+ def __init__(self, tool_config: Dict):
16
+ """
17
+ Initializes the tool with a configuration.
18
+
19
+ Args:
20
+ tool_config (Dict): A dictionary containing 'fields' with an 'endpoint'.
21
+ """
22
+ super().__init__(tool_config)
23
+ self.endpoint = tool_config["fields"]["endpoint"]
24
+ self.extract_path = tool_config["fields"].get("extract_path")
25
+ self.timeout = 20
26
+
27
+ def _build_url(self, args: Dict[str, Any]) -> str:
28
+ """Builds the request URL from arguments."""
29
+ url = self.endpoint
30
+ for key, value in args.items():
31
+ url = url.replace(f"{{{key}}}", quote(str(value)))
32
+ return url
33
+
34
+ def _extract_data(self, data: Dict, extract_path: str) -> Any:
35
+ """Extract specific data from the GO API response using custom paths."""
36
+
37
+ if extract_path == "response.docs[0]":
38
+ # Extract single document from GOlr response
39
+ response = data.get("response", {})
40
+ docs = response.get("docs", [])
41
+ if docs:
42
+ return docs[0]
43
+ else:
44
+ return {"error": "No GO term found"}
45
+
46
+ elif extract_path == "response.docs":
47
+ # Extract all documents from GOlr response
48
+ response = data.get("response", {})
49
+ docs = response.get("docs", [])
50
+ return docs
51
+
52
+ elif extract_path == "associations[*].subject":
53
+ # Extract gene/protein information from Biolink associations
54
+ result = []
55
+ # Handle both dict with associations and direct list from Biolink API
56
+ if isinstance(data, list):
57
+ # Direct list of associations from Biolink API
58
+ associations = data
59
+ else:
60
+ # Dictionary response with associations key
61
+ associations = data.get("associations", [])
62
+
63
+ for assoc in associations:
64
+ subject = assoc.get("subject", {})
65
+ result.append(subject)
66
+ return result
67
+
68
+ # For simple paths, try direct access
69
+ try:
70
+ if "." in extract_path:
71
+ keys = extract_path.split(".")
72
+ result = data
73
+ for key in keys:
74
+ if "[" in key and "]" in key:
75
+ # Handle array indexing like "docs[0]"
76
+ array_key = key.split("[")[0]
77
+ index_str = key.split("[")[1].split("]")[0]
78
+ result = result.get(array_key, [])
79
+ if index_str.isdigit():
80
+ index = int(index_str)
81
+ if index < len(result):
82
+ result = result[index]
83
+ else:
84
+ return {"error": f"Index {index} out of range"}
85
+ else:
86
+ return {"error": f"Invalid array index: {index_str}"}
87
+ else:
88
+ result = result.get(key, {})
89
+ return result
90
+ else:
91
+ return data.get(extract_path)
92
+ except Exception as e:
93
+ return {"error": f"Failed to extract data using path '{extract_path}': {e}"}
94
+
95
+ def run(self, arguments: Any = None) -> Any:
96
+ """
97
+ Executes the API call and returns the data.
98
+
99
+ Args:
100
+ arguments (Dict[str, Any]): Parameters for the API call.
101
+
102
+ Returns:
103
+ Any: The JSON data from the API or an error dictionary.
104
+ """
105
+ # Normalize arguments
106
+ if arguments is None:
107
+ arguments = {}
108
+ if not isinstance(arguments, dict):
109
+ return {"error": "Invalid arguments type; expected a mapping/dict."}
110
+
111
+ # Handle different endpoint formats
112
+ if "?" in self.endpoint:
113
+ # This is a complete URL with query parameters (GOlr format)
114
+ url = self.endpoint
115
+ for key, value in arguments.items():
116
+ url = url.replace(f"{{{key}}}", quote(str(value)))
117
+ params = {}
118
+ else:
119
+ # This is a template URL (Biolink format)
120
+ url_args = arguments.copy()
121
+ params = {}
122
+
123
+ # Move query parameters to params dict for Biolink API
124
+ if "taxon" in arguments:
125
+ params["taxon"] = url_args.pop("taxon")
126
+ if "rows" in arguments:
127
+ params["rows"] = url_args.pop("rows")
128
+ if "start" in arguments:
129
+ params["start"] = url_args.pop("start")
130
+
131
+ # Build URL with remaining arguments
132
+ url = self._build_url(url_args)
133
+
134
+ try:
135
+ resp = requests.get(
136
+ url,
137
+ params=params,
138
+ timeout=self.timeout,
139
+ headers={"Accept": "application/json"},
140
+ )
141
+ resp.raise_for_status()
142
+ data = resp.json()
143
+ except requests.exceptions.HTTPError as e:
144
+ if e.response.status_code == 404:
145
+ return {
146
+ "error": "The requested resource was not found (404 Not Found)."
147
+ }
148
+ return {
149
+ "error": f"GO API request failed with HTTP status: {e.response.status_code}",
150
+ "detail": e.response.text,
151
+ }
152
+ except requests.exceptions.RequestException as e:
153
+ return {
154
+ "error": f"A network error occurred while requesting the GO API: {e}"
155
+ }
156
+ except ValueError:
157
+ return {
158
+ "error": "Failed to parse GO API response, which may not be valid JSON.",
159
+ "content": resp.text,
160
+ }
161
+
162
+ # If extract_path is configured, extract the corresponding subset
163
+ if self.extract_path:
164
+ result = self._extract_data(data, self.extract_path)
165
+
166
+ # Handle empty results
167
+ if isinstance(result, list) and len(result) == 0:
168
+ return {"error": f"No data found for path: {self.extract_path}"}
169
+ elif isinstance(result, dict) and "error" in result:
170
+ return result
171
+
172
+ return result
173
+
174
+ return data
175
+
176
+ # Method bindings for backward compatibility and convenience
177
+ def search_terms(self, query: str) -> Any:
178
+ return self.run({"query": query})
179
+
180
+ def get_term_details(self, id: str) -> Any:
181
+ return self.run({"id": id})
182
+
183
+ def get_genes_for_term(
184
+ self, id: str, taxon: Optional[str] = None, rows: Optional[int] = None
185
+ ) -> Any:
186
+ args = {"id": id}
187
+ if taxon:
188
+ args["taxon"] = taxon
189
+ if rows:
190
+ args["rows"] = rows
191
+ return self.run(args)
192
+
193
+ def get_terms_for_gene(self, id: str) -> Any:
194
+ return self.run({"id": id})