tooluniverse 0.2.0__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 (190) 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 +1 -1
  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 +1 -1
  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-1.0.0.dist-info/entry_points.txt +9 -0
  183. tooluniverse/generate_mcp_tools.py +0 -113
  184. tooluniverse/mcp_server.py +0 -3340
  185. tooluniverse-0.2.0.dist-info/METADATA +0 -139
  186. tooluniverse-0.2.0.dist-info/RECORD +0 -21
  187. tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
  188. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +0 -0
  189. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
  190. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1122 @@
1
+ """
2
+ Output Hook System for ToolUniverse
3
+
4
+ This module provides a comprehensive hook-based output processing system that allows
5
+ for intelligent post-processing of tool outputs. The system supports various types
6
+ of hooks including summarization, filtering, and transformation hooks.
7
+
8
+ Key Components:
9
+ - HookRule: Defines conditions for when hooks should trigger
10
+ - OutputHook: Base class for all output hooks
11
+ - SummarizationHook: Specialized hook for output summarization
12
+ - HookManager: Manages and coordinates all hooks
13
+
14
+ The hook system integrates seamlessly with ToolUniverse's existing architecture,
15
+ leveraging AgenticTool and ComposeTool for intelligent output processing.
16
+ """
17
+
18
+ import json
19
+ from typing import Dict, Any, List, Optional
20
+ from pathlib import Path
21
+
22
+
23
+ class HookRule:
24
+ """
25
+ Defines rules for when hooks should be triggered.
26
+
27
+ This class evaluates various conditions to determine if a hook should
28
+ be applied to a tool's output. Supports multiple condition types including
29
+ output length, content type, and tool-specific criteria.
30
+
31
+ Args:
32
+ conditions (Dict[str, Any]): Dictionary containing condition specifications
33
+
34
+ Attributes:
35
+ conditions (Dict[str, Any]): The condition specifications
36
+ """
37
+
38
+ def __init__(self, conditions: Dict[str, Any]):
39
+ """
40
+ Initialize the hook rule with conditions.
41
+
42
+ Args:
43
+ conditions (Dict[str, Any]): Condition specifications including
44
+ output_length, content_type, tool_type, etc.
45
+ """
46
+ self.conditions = conditions
47
+
48
+ def evaluate(
49
+ self,
50
+ result: Any,
51
+ tool_name: str,
52
+ arguments: Dict[str, Any],
53
+ context: Dict[str, Any],
54
+ ) -> bool:
55
+ """
56
+ Evaluate whether the rule conditions are met.
57
+
58
+ Args:
59
+ result (Any): The tool output to evaluate
60
+ tool_name (str): Name of the tool that produced the output
61
+ arguments (Dict[str, Any]): Arguments passed to the tool
62
+ context (Dict[str, Any]): Additional context information
63
+
64
+ Returns:
65
+ bool: True if conditions are met, False otherwise
66
+ """
67
+ # Evaluate output length conditions
68
+ if "output_length" in self.conditions:
69
+ result_str = str(result)
70
+ length_condition = self.conditions["output_length"]
71
+ threshold = length_condition.get("threshold", 5000)
72
+ operator = length_condition.get("operator", ">")
73
+
74
+ if operator == ">":
75
+ return len(result_str) > threshold
76
+ elif operator == ">=":
77
+ return len(result_str) >= threshold
78
+ elif operator == "<":
79
+ return len(result_str) < threshold
80
+ elif operator == "<=":
81
+ return len(result_str) <= threshold
82
+
83
+ # Evaluate content type conditions
84
+ if "content_type" in self.conditions:
85
+ content_type = self.conditions["content_type"]
86
+ if content_type == "json" and isinstance(result, dict):
87
+ return True
88
+ elif content_type == "text" and isinstance(result, str):
89
+ return True
90
+
91
+ # Evaluate tool type conditions
92
+ if "tool_type" in self.conditions:
93
+ tool_type = context.get("tool_type", "")
94
+ return tool_type == self.conditions["tool_type"]
95
+
96
+ # Evaluate tool name conditions
97
+ if "tool_name" in self.conditions:
98
+ return tool_name == self.conditions["tool_name"]
99
+
100
+ # If no specific conditions are met, return True for general rules
101
+ return True
102
+
103
+
104
+ class OutputHook:
105
+ """
106
+ Base class for all output hooks.
107
+
108
+ This abstract base class defines the interface that all output hooks must implement.
109
+ Hooks are used to process tool outputs after execution, enabling features like
110
+ summarization, filtering, transformation, and validation.
111
+
112
+ Args:
113
+ config (Dict[str, Any]): Hook configuration including name, enabled status,
114
+ priority, and conditions
115
+
116
+ Attributes:
117
+ config (Dict[str, Any]): Hook configuration
118
+ name (str): Name of the hook
119
+ enabled (bool): Whether the hook is enabled
120
+ priority (int): Hook priority (lower numbers execute first)
121
+ rule (HookRule): Rule for when this hook should trigger
122
+ """
123
+
124
+ def __init__(self, config: Dict[str, Any]):
125
+ """
126
+ Initialize the output hook with configuration.
127
+
128
+ Args:
129
+ config (Dict[str, Any]): Hook configuration containing:
130
+ - name: Hook identifier
131
+ - enabled: Whether hook is active
132
+ - priority: Execution priority
133
+ - conditions: Trigger conditions
134
+ """
135
+ self.config = config
136
+ self.name = config.get("name", "unnamed_hook")
137
+ self.enabled = config.get("enabled", True)
138
+ self.priority = config.get("priority", 1)
139
+ self.rule = HookRule(config.get("conditions", {}))
140
+
141
+ def should_trigger(
142
+ self,
143
+ result: Any,
144
+ tool_name: str,
145
+ arguments: Dict[str, Any],
146
+ context: Dict[str, Any],
147
+ ) -> bool:
148
+ """
149
+ Determine if this hook should be triggered for the given output.
150
+
151
+ Args:
152
+ result (Any): The tool output to evaluate
153
+ tool_name (str): Name of the tool that produced the output
154
+ arguments (Dict[str, Any]): Arguments passed to the tool
155
+ context (Dict[str, Any]): Additional context information
156
+
157
+ Returns:
158
+ bool: True if hook should trigger, False otherwise
159
+ """
160
+ if not self.enabled:
161
+ return False
162
+ return self.rule.evaluate(result, tool_name, arguments, context)
163
+
164
+ def process(
165
+ self,
166
+ result: Any,
167
+ tool_name: str,
168
+ arguments: Dict[str, Any],
169
+ context: Dict[str, Any],
170
+ ) -> Any:
171
+ """
172
+ Process the tool output.
173
+
174
+ This method must be implemented by subclasses to define the specific
175
+ processing logic for the hook.
176
+
177
+ Args:
178
+ result (Any): The tool output to process
179
+ tool_name (str): Name of the tool that produced the output
180
+ arguments (Dict[str, Any]): Arguments passed to the tool
181
+ context (Dict[str, Any]): Additional context information
182
+
183
+ Returns:
184
+ Any: The processed output
185
+
186
+ Raises:
187
+ NotImplementedError: If not implemented by subclass
188
+ """
189
+ raise NotImplementedError("Subclasses must implement process method")
190
+
191
+
192
+ class SummarizationHook(OutputHook):
193
+ """
194
+ Hook for intelligent output summarization using AI.
195
+
196
+ This hook uses the ToolUniverse's AgenticTool and ComposeTool infrastructure
197
+ to provide intelligent summarization of long tool outputs. It supports
198
+ chunking large outputs, processing each chunk with AI, and merging results.
199
+
200
+ Args:
201
+ config (Dict[str, Any]): Hook configuration including summarization parameters
202
+ tooluniverse: Reference to the ToolUniverse instance
203
+
204
+ Attributes:
205
+ tooluniverse: ToolUniverse instance for tool execution
206
+ composer_tool_name (str): Name of the ComposeTool for summarization
207
+ chunk_size (int): Size of chunks for processing large outputs
208
+ focus_areas (str): Areas to focus on during summarization
209
+ max_summary_length (int): Maximum length of final summary
210
+ """
211
+
212
+ def __init__(self, config: Dict[str, Any], tooluniverse):
213
+ """
214
+ Initialize the summarization hook.
215
+
216
+ Args:
217
+ config (Dict[str, Any]): Hook configuration
218
+ tooluniverse: ToolUniverse instance for executing summarization tools
219
+ """
220
+ super().__init__(config)
221
+ self.tooluniverse = tooluniverse
222
+ hook_config = config.get("hook_config", {})
223
+ self.composer_tool_name = hook_config.get(
224
+ "composer_tool", "OutputSummarizationComposer"
225
+ )
226
+ self.chunk_size = hook_config.get("chunk_size", 2000)
227
+ self.focus_areas = hook_config.get("focus_areas", "key_findings_and_results")
228
+ self.max_summary_length = hook_config.get("max_summary_length", 3000)
229
+
230
+ def process(
231
+ self,
232
+ result: Any,
233
+ tool_name: str,
234
+ arguments: Dict[str, Any],
235
+ context: Dict[str, Any],
236
+ ) -> Any:
237
+ """
238
+ Execute summarization processing using Compose Summarizer Tool.
239
+
240
+ This method orchestrates the summarization workflow by:
241
+ 1. Preparing parameters for the Compose Summarizer Tool
242
+ 2. Calling the tool through ToolUniverse
243
+ 3. Processing and returning the summarized result
244
+
245
+ Args:
246
+ result (Any): The tool output to summarize
247
+ tool_name (str): Name of the tool that produced the output
248
+ arguments (Dict[str, Any]): Arguments passed to the tool
249
+ context (Dict[str, Any]): Additional context information
250
+
251
+ Returns:
252
+ Any: The summarized output, or original output if summarization fails
253
+ """
254
+ try:
255
+ # Check if the required tools are available
256
+ if (
257
+ self.composer_tool_name not in self.tooluniverse.callable_functions
258
+ and self.composer_tool_name not in self.tooluniverse.all_tool_dict
259
+ ):
260
+ print(
261
+ f"❌ SummarizationHook: {self.composer_tool_name} tool is not available."
262
+ )
263
+ print(
264
+ " This usually means the output_summarization tools are not loaded."
265
+ )
266
+ print(" Returning original output without summarization.")
267
+ return result
268
+
269
+ # Prepare parameters for Compose Summarizer Tool
270
+ composer_args = {
271
+ "tool_output": str(result),
272
+ "query_context": self._extract_query_context(context),
273
+ "tool_name": tool_name,
274
+ "chunk_size": self.chunk_size,
275
+ "focus_areas": self.focus_areas,
276
+ "max_summary_length": self.max_summary_length,
277
+ }
278
+
279
+ # Call Compose Summarizer Tool through ToolUniverse
280
+ composer_result = self.tooluniverse.run_one_function(
281
+ {"name": self.composer_tool_name, "arguments": composer_args}
282
+ )
283
+
284
+ # Process Compose Tool result
285
+ if isinstance(composer_result, dict) and composer_result.get("success"):
286
+ return composer_result.get("summary", result)
287
+ elif isinstance(composer_result, str):
288
+ return composer_result
289
+ else:
290
+ print(
291
+ f"Warning: Compose Summarizer Tool returned unexpected result: {composer_result}"
292
+ )
293
+ return result
294
+
295
+ except Exception as e:
296
+ error_msg = str(e)
297
+ print(f"Error in summarization hook: {error_msg}")
298
+
299
+ # Check if the error is due to missing tools
300
+ if "not found" in error_msg.lower() or "ToolOutputSummarizer" in error_msg:
301
+ print(
302
+ "❌ SummarizationHook: Required summarization tools are not available."
303
+ )
304
+ print(" Please ensure the SMCP server is started with hooks enabled.")
305
+
306
+ return result
307
+
308
+ def _extract_query_context(self, context: Dict[str, Any]) -> str:
309
+ """
310
+ Extract query context from execution context.
311
+
312
+ This method attempts to identify the original user query or intent
313
+ from the context information to provide better summarization.
314
+
315
+ Args:
316
+ context (Dict[str, Any]): Execution context containing arguments and metadata
317
+
318
+ Returns:
319
+ str: Extracted query context or fallback description
320
+ """
321
+ arguments = context.get("arguments", {})
322
+
323
+ # Common query parameter names
324
+ query_keys = ["query", "question", "input", "text", "search_term", "prompt"]
325
+ for key in query_keys:
326
+ if key in arguments:
327
+ return str(arguments[key])
328
+
329
+ # If no explicit query found, return tool name as context
330
+ return f"Tool execution: {context.get('tool_name', 'unknown')}"
331
+
332
+
333
+ class HookManager:
334
+ """
335
+ Manages and coordinates all output hooks.
336
+
337
+ The HookManager is responsible for loading hook configurations, creating
338
+ hook instances, and applying hooks to tool outputs. It provides a unified
339
+ interface for hook management and supports dynamic configuration updates.
340
+
341
+ Args:
342
+ config (Dict[str, Any]): Hook manager configuration
343
+ tooluniverse: Reference to the ToolUniverse instance
344
+
345
+ Attributes:
346
+ config (Dict[str, Any]): Hook manager configuration
347
+ tooluniverse: ToolUniverse instance for tool execution
348
+ hooks (List[OutputHook]): List of loaded hook instances
349
+ enabled (bool): Whether hook processing is enabled
350
+ config_path (str): Path to hook configuration file
351
+ """
352
+
353
+ def __init__(self, config: Dict[str, Any], tooluniverse):
354
+ """
355
+ Initialize the hook manager.
356
+
357
+ Args:
358
+ config (Dict[str, Any]): Configuration for hook manager
359
+ tooluniverse: ToolUniverse instance for executing tools
360
+ """
361
+ self.config = config
362
+ self.tooluniverse = tooluniverse
363
+ self.hooks: List[OutputHook] = []
364
+ self.enabled = True
365
+ self.config_path = config.get("config_path", "template/hook_config.json")
366
+ self._pending_tools_to_load: List[str] = []
367
+ self._load_hook_config()
368
+ self._load_hooks()
369
+
370
+ def apply_hooks(
371
+ self,
372
+ result: Any,
373
+ tool_name: str,
374
+ arguments: Dict[str, Any],
375
+ context: Dict[str, Any],
376
+ ) -> Any:
377
+ """
378
+ Apply all applicable hooks to the tool output.
379
+
380
+ This method iterates through all loaded hooks, checks if they should
381
+ be applied to the current output, and processes the output through
382
+ each applicable hook in priority order.
383
+
384
+ Args:
385
+ result (Any): The tool output to process
386
+ tool_name (str): Name of the tool that produced the output
387
+ arguments (Dict[str, Any]): Arguments passed to the tool
388
+ context (Dict[str, Any]): Additional context information
389
+
390
+ Returns:
391
+ Any: The processed output after applying all applicable hooks
392
+ """
393
+ if not self.enabled:
394
+ return result
395
+
396
+ # Load pending tools if ToolUniverse is now ready
397
+ self._load_pending_tools()
398
+
399
+ # Prevent recursive hook processing
400
+ if self._is_hook_tool(tool_name):
401
+ return result
402
+
403
+ # Sort hooks by priority (lower numbers execute first)
404
+ sorted_hooks = sorted(self.hooks, key=lambda h: h.priority)
405
+
406
+ for hook in sorted_hooks:
407
+ if not hook.enabled:
408
+ continue
409
+
410
+ # Check if hook is applicable to current tool
411
+ if self._is_hook_applicable(hook, tool_name, context):
412
+ if hook.should_trigger(result, tool_name, arguments, context):
413
+ print(f"🔧 Applying hook: {hook.name} for tool: {tool_name}")
414
+ result = hook.process(result, tool_name, arguments, context)
415
+
416
+ return result
417
+
418
+ def enable_hook(self, hook_name: str):
419
+ """
420
+ Enable a specific hook by name.
421
+
422
+ Args:
423
+ hook_name (str): Name of the hook to enable
424
+ """
425
+ hook = self.get_hook(hook_name)
426
+ if hook:
427
+ hook.enabled = True
428
+ print(f"✅ Enabled hook: {hook_name}")
429
+ else:
430
+ print(f"❌ Hook not found: {hook_name}")
431
+
432
+ def disable_hook(self, hook_name: str):
433
+ """
434
+ Disable a specific hook by name.
435
+
436
+ Args:
437
+ hook_name (str): Name of the hook to disable
438
+ """
439
+ hook = self.get_hook(hook_name)
440
+ if hook:
441
+ hook.enabled = False
442
+ print(f"❌ Disabled hook: {hook_name}")
443
+ else:
444
+ print(f"❌ Hook not found: {hook_name}")
445
+
446
+ def toggle_hooks(self, enabled: bool):
447
+ """
448
+ Enable or disable all hooks globally.
449
+
450
+ Args:
451
+ enabled (bool): True to enable all hooks, False to disable
452
+ """
453
+ self.enabled = enabled
454
+ status = "enabled" if enabled else "disabled"
455
+ print(f"🔧 Hooks {status}")
456
+
457
+ def reload_config(self, config_path: Optional[str] = None):
458
+ """
459
+ Reload hook configuration from file.
460
+
461
+ Args:
462
+ config_path (Optional[str]): Path to configuration file.
463
+ If None, uses the current config_path
464
+ """
465
+ if config_path:
466
+ self.config_path = config_path
467
+ self._load_hook_config()
468
+ self._load_hooks()
469
+ print("🔄 Reloaded hook configuration")
470
+
471
+ def get_hook(self, hook_name: str) -> Optional[OutputHook]:
472
+ """
473
+ Get a hook instance by name.
474
+
475
+ Args:
476
+ hook_name (str): Name of the hook to retrieve
477
+
478
+ Returns:
479
+ Optional[OutputHook]: Hook instance if found, None otherwise
480
+ """
481
+ for hook in self.hooks:
482
+ if hook.name == hook_name:
483
+ return hook
484
+ return None
485
+
486
+ def _load_hook_config(self):
487
+ """
488
+ Load hook configuration from file.
489
+
490
+ This method attempts to load the hook configuration from the specified
491
+ file path, handling both package resources and file system paths.
492
+ If the config is already provided and not empty, it uses that instead.
493
+ """
494
+ # If config is already provided and not empty, use it
495
+ if self.config and (
496
+ ("hooks" in self.config)
497
+ or ("tool_specific_hooks" in self.config)
498
+ or ("category_hooks" in self.config)
499
+ ):
500
+ return
501
+
502
+ try:
503
+ config_file = self._get_config_file_path()
504
+
505
+ if hasattr(config_file, "read_text"):
506
+ content = config_file.read_text(encoding="utf-8")
507
+ else:
508
+ with open(config_file, "r", encoding="utf-8") as f:
509
+ content = f.read()
510
+
511
+ self.config = json.loads(content)
512
+ except Exception as e:
513
+ print(f"Warning: Could not load hook config: {e}")
514
+ if not self.config:
515
+ self.config = {}
516
+
517
+ def _get_config_file_path(self):
518
+ """
519
+ Get the path to the hook configuration file.
520
+
521
+ Returns:
522
+ Path: Path to the configuration file
523
+ """
524
+ try:
525
+ import importlib.resources as pkg_resources
526
+ except ImportError:
527
+ import importlib_resources as pkg_resources
528
+
529
+ try:
530
+ data_files = pkg_resources.files("tooluniverse.template")
531
+ config_file = data_files / "hook_config.json"
532
+ return config_file
533
+ except Exception:
534
+ # Fallback to file-based path resolution
535
+ current_dir = Path(__file__).parent
536
+ config_file = current_dir / "template" / "hook_config.json"
537
+ return config_file
538
+
539
+ def _load_hooks(self):
540
+ """
541
+ Load hook configurations and create hook instances.
542
+
543
+ This method processes the configuration and creates appropriate
544
+ hook instances for global, tool-specific, and category-specific hooks.
545
+ It also automatically loads any tools required by the hooks.
546
+ """
547
+ self.hooks = []
548
+
549
+ # Collect all hook configs first to determine required tools
550
+ all_hook_configs = []
551
+
552
+ # Load global hooks
553
+ global_hooks = self.config.get("hooks", [])
554
+ for hook_config in global_hooks:
555
+ all_hook_configs.append(hook_config)
556
+
557
+ # Load tool-specific hooks
558
+ tool_specific_hooks = self.config.get("tool_specific_hooks", {})
559
+ for tool_name, tool_hook_config in tool_specific_hooks.items():
560
+ if tool_hook_config.get("enabled", True):
561
+ tool_hooks = tool_hook_config.get("hooks", [])
562
+ for hook_config in tool_hooks:
563
+ hook_config["tool_name"] = tool_name
564
+ all_hook_configs.append(hook_config)
565
+
566
+ # Load category-specific hooks
567
+ category_hooks = self.config.get("category_hooks", {})
568
+ for category_name, category_hook_config in category_hooks.items():
569
+ if category_hook_config.get("enabled", True):
570
+ category_hooks_list = category_hook_config.get("hooks", [])
571
+ for hook_config in category_hooks_list:
572
+ hook_config["category"] = category_name
573
+ all_hook_configs.append(hook_config)
574
+
575
+ # Auto-load required tools for hooks
576
+ self._auto_load_hook_tools(all_hook_configs)
577
+
578
+ # Ensure hook tools are loaded
579
+ self._ensure_hook_tools_loaded()
580
+
581
+ # Create hook instances
582
+ for hook_config in all_hook_configs:
583
+ hook = self._create_hook_instance(hook_config)
584
+ if hook:
585
+ self.hooks.append(hook)
586
+
587
+ def _auto_load_hook_tools(self, hook_configs: List[Dict[str, Any]]):
588
+ """
589
+ Automatically load tools required by hooks.
590
+
591
+ This method analyzes hook configurations to determine which tools
592
+ are needed and automatically loads them into the ToolUniverse.
593
+
594
+ Args:
595
+ hook_configs (List[Dict[str, Any]]): List of hook configurations
596
+ """
597
+ required_tools = set()
598
+
599
+ for hook_config in hook_configs:
600
+ hook_type = hook_config.get("type", "SummarizationHook")
601
+ hook_config_section = hook_config.get("hook_config", {})
602
+
603
+ # Determine required tools based on hook type
604
+ if hook_type == "SummarizationHook":
605
+ composer_tool = hook_config_section.get(
606
+ "composer_tool", "OutputSummarizationComposer"
607
+ )
608
+ required_tools.add(composer_tool)
609
+ # Also need the agentic tool for summarization
610
+ required_tools.add("ToolOutputSummarizer")
611
+ elif hook_type == "FilteringHook":
612
+ # Add filtering-related tools if any
613
+ pass
614
+ elif hook_type == "FormattingHook":
615
+ # Add formatting-related tools if any
616
+ pass
617
+ elif hook_type == "ValidationHook":
618
+ # Add validation-related tools if any
619
+ pass
620
+ elif hook_type == "LoggingHook":
621
+ # Add logging-related tools if any
622
+ pass
623
+
624
+ # Load required tools
625
+ if required_tools:
626
+ tools_to_load = []
627
+ for tool in required_tools:
628
+ # Map tool names to their categories
629
+ if tool in ["OutputSummarizationComposer", "ToolOutputSummarizer"]:
630
+ tools_to_load.append("output_summarization")
631
+ # Add more mappings as needed
632
+
633
+ if tools_to_load:
634
+ try:
635
+ # Ensure ComposeTool is available
636
+ from .compose_tool import ComposeTool
637
+ from .tool_registry import register_external_tool
638
+
639
+ register_external_tool("ComposeTool", ComposeTool)
640
+
641
+ # Check if ToolUniverse is fully initialized
642
+ if hasattr(self.tooluniverse, "all_tools"):
643
+ # Load the tools and verify they were loaded
644
+ self.tooluniverse.load_tools(tools_to_load)
645
+
646
+ # Verify that the required tools are actually available
647
+ missing_tools = []
648
+ for tool in required_tools:
649
+ if (
650
+ tool not in self.tooluniverse.callable_functions
651
+ and tool not in self.tooluniverse.all_tool_dict
652
+ ):
653
+ missing_tools.append(tool)
654
+
655
+ if missing_tools:
656
+ print(
657
+ f"⚠️ Warning: Some hook tools could not be loaded: {missing_tools}"
658
+ )
659
+ print(" This may cause summarization hooks to fail.")
660
+ else:
661
+ print(
662
+ f"🔧 Auto-loaded hook tools: {', '.join(tools_to_load)}"
663
+ )
664
+ else:
665
+ # Store tools to load later when ToolUniverse is ready
666
+ self._pending_tools_to_load = tools_to_load
667
+ print(
668
+ f"🔧 Hook tools queued for loading: {', '.join(tools_to_load)}"
669
+ )
670
+ except Exception as e:
671
+ print(f"⚠️ Warning: Could not auto-load hook tools: {e}")
672
+ print(" This will cause summarization hooks to fail.")
673
+
674
+ def _ensure_hook_tools_loaded(self):
675
+ """
676
+ Ensure that tools required by hooks are loaded.
677
+
678
+ This method is called during HookManager initialization to make sure that
679
+ the necessary tools (like output_summarization tools) are available.
680
+ """
681
+ try:
682
+ # Ensure ComposeTool is available
683
+ from .compose_tool import ComposeTool
684
+ from .tool_registry import register_external_tool
685
+
686
+ register_external_tool("ComposeTool", ComposeTool)
687
+
688
+ # Load output_summarization tools if not already loaded
689
+ if (
690
+ not hasattr(self.tooluniverse, "tool_category_dicts")
691
+ or "output_summarization" not in self.tooluniverse.tool_category_dicts
692
+ ):
693
+ print("🔧 Loading output_summarization tools for hooks")
694
+ self.tooluniverse.load_tools(["output_summarization"])
695
+
696
+ # Verify the tools were loaded
697
+ missing_tools = []
698
+ required_tools = ["ToolOutputSummarizer", "OutputSummarizationComposer"]
699
+ for tool in required_tools:
700
+ if (
701
+ hasattr(self.tooluniverse, "callable_functions")
702
+ and tool not in self.tooluniverse.callable_functions
703
+ and hasattr(self.tooluniverse, "all_tool_dict")
704
+ and tool not in self.tooluniverse.all_tool_dict
705
+ ):
706
+ missing_tools.append(tool)
707
+
708
+ if missing_tools:
709
+ print(
710
+ f"⚠️ Warning: Some hook tools could not be loaded: {missing_tools}"
711
+ )
712
+ print(" This may cause summarization hooks to fail")
713
+ else:
714
+ print(f"✅ Hook tools loaded successfully: {required_tools}")
715
+ else:
716
+ print("🔧 Output_summarization tools already loaded")
717
+
718
+ except Exception as e:
719
+ print(f"❌ Error loading hook tools: {e}")
720
+ print(" This will cause summarization hooks to fail")
721
+
722
+ def _load_pending_tools(self):
723
+ """
724
+ Load any pending tools that were queued during initialization.
725
+
726
+ This method is called when hooks are applied to ensure that any tools
727
+ that couldn't be loaded during HookManager initialization are loaded
728
+ once the ToolUniverse is fully ready.
729
+ """
730
+ if self._pending_tools_to_load and hasattr(self.tooluniverse, "all_tools"):
731
+ try:
732
+ self.tooluniverse.load_tools(self._pending_tools_to_load)
733
+ print(
734
+ f"🔧 Loaded pending hook tools: {', '.join(self._pending_tools_to_load)}"
735
+ )
736
+ self._pending_tools_to_load = [] # Clear the pending list
737
+ except Exception as e:
738
+ print(f"⚠️ Warning: Could not load pending hook tools: {e}")
739
+
740
+ def _is_hook_tool(self, tool_name: str) -> bool:
741
+ """
742
+ Check if a tool is a hook-related tool that should not be processed by hooks.
743
+
744
+ This prevents recursive hook processing where hook tools (like ToolOutputSummarizer)
745
+ produce output that would trigger more hook processing.
746
+
747
+ Args:
748
+ tool_name (str): Name of the tool to check
749
+
750
+ Returns:
751
+ bool: True if the tool is a hook tool and should be excluded from hook processing
752
+ """
753
+ hook_tool_names = [
754
+ "ToolOutputSummarizer",
755
+ "OutputSummarizationComposer",
756
+ # Add more hook tool names as needed
757
+ ]
758
+ return tool_name in hook_tool_names
759
+
760
+ def _create_hook_instance(
761
+ self, hook_config: Dict[str, Any]
762
+ ) -> Optional[OutputHook]:
763
+ """
764
+ Create a hook instance based on configuration.
765
+
766
+ This method creates hook instances and applies hook type-specific defaults
767
+ from the configuration before initializing the hook.
768
+
769
+ Args:
770
+ hook_config (Dict[str, Any]): Hook configuration
771
+
772
+ Returns:
773
+ Optional[OutputHook]: Created hook instance or None if type not supported
774
+ """
775
+ hook_type = hook_config.get("type", "SummarizationHook")
776
+
777
+ # Apply hook type-specific defaults
778
+ enhanced_config = self._apply_hook_type_defaults(hook_config)
779
+
780
+ if hook_type == "SummarizationHook":
781
+ return SummarizationHook(enhanced_config, self.tooluniverse)
782
+ elif hook_type == "FileSaveHook":
783
+ # Merge hook_config with the main config for FileSaveHook
784
+ file_save_config = enhanced_config.copy()
785
+ file_save_config.update(enhanced_config.get("hook_config", {}))
786
+ return FileSaveHook(file_save_config)
787
+ else:
788
+ print(f"Unknown hook type: {hook_type}")
789
+ return None
790
+
791
+ def _apply_hook_type_defaults(self, hook_config: Dict[str, Any]) -> Dict[str, Any]:
792
+ """
793
+ Apply hook type-specific default values to hook configuration.
794
+
795
+ This method merges hook type defaults with individual hook configuration,
796
+ ensuring that each hook type gets its appropriate default values.
797
+
798
+ Args:
799
+ hook_config (Dict[str, Any]): Original hook configuration
800
+
801
+ Returns:
802
+ Dict[str, Any]: Enhanced configuration with defaults applied
803
+ """
804
+ hook_type = hook_config.get("type", "SummarizationHook")
805
+
806
+ # Get hook type defaults from configuration
807
+ hook_type_defaults = self.config.get("hook_type_defaults", {}).get(
808
+ hook_type, {}
809
+ )
810
+
811
+ # Create enhanced configuration
812
+ enhanced_config = hook_config.copy()
813
+
814
+ # Apply defaults to hook_config if not already specified
815
+ if "hook_config" not in enhanced_config:
816
+ enhanced_config["hook_config"] = {}
817
+
818
+ hook_config_section = enhanced_config["hook_config"]
819
+
820
+ # Apply defaults for each hook type
821
+ if hook_type == "SummarizationHook":
822
+ defaults = {
823
+ "composer_tool": "OutputSummarizationComposer",
824
+ "chunk_size": hook_type_defaults.get("default_chunk_size", 2000),
825
+ "focus_areas": hook_type_defaults.get(
826
+ "default_focus_areas", "key_findings_and_results"
827
+ ),
828
+ "max_summary_length": hook_type_defaults.get(
829
+ "default_max_summary_length", 3000
830
+ ),
831
+ }
832
+ elif hook_type == "FilteringHook":
833
+ defaults = {
834
+ "replacement_text": hook_type_defaults.get(
835
+ "default_replacement_text", "[REDACTED]"
836
+ ),
837
+ "preserve_structure": hook_type_defaults.get(
838
+ "default_preserve_structure", True
839
+ ),
840
+ "log_filtered_items": hook_type_defaults.get(
841
+ "default_log_filtered_items", False
842
+ ),
843
+ }
844
+ elif hook_type == "FormattingHook":
845
+ defaults = {
846
+ "indent_size": hook_type_defaults.get("default_indent_size", 2),
847
+ "sort_keys": hook_type_defaults.get("default_sort_keys", True),
848
+ "pretty_print": hook_type_defaults.get("default_pretty_print", True),
849
+ "max_line_length": hook_type_defaults.get(
850
+ "default_max_line_length", 100
851
+ ),
852
+ }
853
+ elif hook_type == "ValidationHook":
854
+ defaults = {
855
+ "strict_mode": hook_type_defaults.get("default_strict_mode", False),
856
+ "error_action": hook_type_defaults.get("default_error_action", "warn"),
857
+ }
858
+ elif hook_type == "LoggingHook":
859
+ defaults = {
860
+ "log_level": hook_type_defaults.get("default_log_level", "INFO"),
861
+ "log_format": hook_type_defaults.get("default_log_format", "simple"),
862
+ "max_log_size": hook_type_defaults.get("default_max_log_size", 1000),
863
+ }
864
+ elif hook_type == "FileSaveHook":
865
+ defaults = {
866
+ "temp_dir": hook_type_defaults.get("default_temp_dir", None),
867
+ "file_prefix": hook_type_defaults.get(
868
+ "default_file_prefix", "tool_output"
869
+ ),
870
+ "include_metadata": hook_type_defaults.get(
871
+ "default_include_metadata", True
872
+ ),
873
+ "auto_cleanup": hook_type_defaults.get("default_auto_cleanup", False),
874
+ "cleanup_age_hours": hook_type_defaults.get(
875
+ "default_cleanup_age_hours", 24
876
+ ),
877
+ }
878
+ else:
879
+ defaults = {}
880
+
881
+ # Apply defaults only if not already specified
882
+ for key, default_value in defaults.items():
883
+ if key not in hook_config_section:
884
+ hook_config_section[key] = default_value
885
+
886
+ return enhanced_config
887
+
888
+ def _is_hook_applicable(
889
+ self, hook: OutputHook, tool_name: str, context: Dict[str, Any]
890
+ ) -> bool:
891
+ """
892
+ Check if a hook is applicable to the current tool.
893
+
894
+ Args:
895
+ hook (OutputHook): Hook instance to check
896
+ tool_name (str): Name of the current tool
897
+ context (Dict[str, Any]): Execution context
898
+
899
+ Returns:
900
+ bool: True if hook is applicable, False otherwise
901
+ """
902
+ # Check tool-specific hooks
903
+ if "tool_name" in hook.config:
904
+ return hook.config["tool_name"] == tool_name
905
+
906
+ # Check category-specific hooks
907
+ if "category" in hook.config:
908
+ # This would need to be implemented based on actual tool categorization
909
+ # For now, return True to apply category hooks to all tools
910
+ return True
911
+
912
+ # Global hooks apply to all tools
913
+ return True
914
+
915
+
916
+ class FileSaveHook(OutputHook):
917
+ """
918
+ Hook that saves tool outputs to temporary files and returns file information.
919
+
920
+ This hook saves the tool output to a temporary file and returns information
921
+ about the file path, data format, and data structure instead of the original output.
922
+ This is useful for handling large outputs or when you need to process outputs
923
+ as files rather than in-memory data.
924
+
925
+ Configuration options:
926
+ - temp_dir: Directory to save temporary files (default: system temp)
927
+ - file_prefix: Prefix for generated filenames (default: 'tool_output')
928
+ - include_metadata: Whether to include metadata in the response (default: True)
929
+ - auto_cleanup: Whether to automatically clean up old files (default: False)
930
+ - cleanup_age_hours: Age in hours for auto cleanup (default: 24)
931
+ """
932
+
933
+ def __init__(self, config: Dict[str, Any]):
934
+ """
935
+ Initialize the FileSaveHook.
936
+
937
+ Args:
938
+ config (Dict[str, Any]): Hook configuration including:
939
+ - name: Hook name
940
+ - temp_dir: Directory for temporary files
941
+ - file_prefix: Prefix for filenames
942
+ - include_metadata: Include metadata flag
943
+ - auto_cleanup: Auto cleanup flag
944
+ - cleanup_age_hours: Cleanup age in hours
945
+ """
946
+ super().__init__(config)
947
+
948
+ # Set default configuration
949
+ self.temp_dir = config.get("temp_dir", None)
950
+ self.file_prefix = config.get("file_prefix", "tool_output")
951
+ self.include_metadata = config.get("include_metadata", True)
952
+ self.auto_cleanup = config.get("auto_cleanup", False)
953
+ self.cleanup_age_hours = config.get("cleanup_age_hours", 24)
954
+
955
+ # Import required modules
956
+ import tempfile
957
+ import os
958
+ from datetime import datetime, timedelta
959
+
960
+ self.tempfile = tempfile
961
+ self.os = os
962
+ self.datetime = datetime
963
+ self.timedelta = timedelta
964
+
965
+ # Create temp directory if specified
966
+ if self.temp_dir:
967
+ self.os.makedirs(self.temp_dir, exist_ok=True)
968
+
969
+ def process(
970
+ self,
971
+ result: Any,
972
+ tool_name: str,
973
+ arguments: Dict[str, Any],
974
+ context: Dict[str, Any],
975
+ ) -> Dict[str, Any]:
976
+ """
977
+ Process the tool output by saving it to a temporary file.
978
+
979
+ Args:
980
+ result (Any): The tool output to process
981
+ tool_name (str): Name of the tool that produced the output
982
+ arguments (Dict[str, Any]): Arguments passed to the tool
983
+ context (Dict[str, Any]): Execution context
984
+
985
+ Returns:
986
+ Dict[str, Any]: Dictionary containing file information:
987
+ - file_path: Path to the saved file
988
+ - data_format: Format of the data (json, text, binary, etc.)
989
+ - data_structure: Structure information about the data
990
+ - file_size: Size of the file in bytes
991
+ - created_at: Timestamp when file was created
992
+ - metadata: Additional metadata (if include_metadata is True)
993
+ """
994
+ try:
995
+ # Determine data format and structure
996
+ data_format, data_structure = self._analyze_data(result)
997
+
998
+ # Generate filename
999
+ timestamp = self.datetime.now().strftime("%Y%m%d_%H%M%S")
1000
+ filename = f"{self.file_prefix}_{tool_name}_{timestamp}.{data_format}"
1001
+
1002
+ # Save to temporary file
1003
+ if self.temp_dir:
1004
+ file_path = self.os.path.join(self.temp_dir, filename)
1005
+ else:
1006
+ # Use system temp directory
1007
+ temp_fd, file_path = self.tempfile.mkstemp(
1008
+ suffix=f"_{filename}", prefix=self.file_prefix, dir=self.temp_dir
1009
+ )
1010
+ self.os.close(temp_fd)
1011
+
1012
+ # Write data to file
1013
+ self._write_data_to_file(result, file_path, data_format)
1014
+
1015
+ # Get file size
1016
+ file_size = self.os.path.getsize(file_path)
1017
+
1018
+ # Prepare response
1019
+ response = {
1020
+ "file_path": file_path,
1021
+ "data_format": data_format,
1022
+ "data_structure": data_structure,
1023
+ "file_size": file_size,
1024
+ "created_at": self.datetime.now().isoformat(),
1025
+ "tool_name": tool_name,
1026
+ "original_arguments": arguments,
1027
+ }
1028
+
1029
+ # Add metadata if requested
1030
+ if self.include_metadata:
1031
+ response["metadata"] = {
1032
+ "hook_name": self.name,
1033
+ "hook_type": "FileSaveHook",
1034
+ "processing_time": self.datetime.now().isoformat(),
1035
+ "context": context,
1036
+ }
1037
+
1038
+ # Perform auto cleanup if enabled
1039
+ if self.auto_cleanup:
1040
+ self._cleanup_old_files()
1041
+
1042
+ return response
1043
+
1044
+ except Exception as e:
1045
+ # Return error information instead of failing
1046
+ return {
1047
+ "error": f"Failed to save output to file: {str(e)}",
1048
+ "original_output": str(result),
1049
+ "tool_name": tool_name,
1050
+ "hook_name": self.name,
1051
+ }
1052
+
1053
+ def _analyze_data(self, data: Any) -> tuple[str, str]:
1054
+ """
1055
+ Analyze the data to determine its format and structure.
1056
+
1057
+ Args:
1058
+ data (Any): The data to analyze
1059
+
1060
+ Returns:
1061
+ tuple[str, str]: (data_format, data_structure)
1062
+ """
1063
+ if isinstance(data, dict):
1064
+ return "json", f"dict with {len(data)} keys"
1065
+ elif isinstance(data, list):
1066
+ return "json", f"list with {len(data)} items"
1067
+ elif isinstance(data, str):
1068
+ if data.strip().startswith("{") or data.strip().startswith("["):
1069
+ return "json", "JSON string"
1070
+ else:
1071
+ return "txt", f"text with {len(data)} characters"
1072
+ elif isinstance(data, (int, float)):
1073
+ return "json", "numeric value"
1074
+ elif isinstance(data, bool):
1075
+ return "json", "boolean value"
1076
+ else:
1077
+ return "bin", f"binary data of type {type(data).__name__}"
1078
+
1079
+ def _write_data_to_file(self, data: Any, file_path: str, data_format: str) -> None:
1080
+ """
1081
+ Write data to file in the appropriate format.
1082
+
1083
+ Args:
1084
+ data (Any): The data to write
1085
+ file_path (str): Path to the file
1086
+ data_format (str): Format of the data
1087
+ """
1088
+ if data_format == "json":
1089
+ with open(file_path, "w", encoding="utf-8") as f:
1090
+ json.dump(data, f, indent=2, ensure_ascii=False)
1091
+ elif data_format == "txt":
1092
+ with open(file_path, "w", encoding="utf-8") as f:
1093
+ f.write(str(data))
1094
+ else:
1095
+ # For binary or other formats, write as string
1096
+ with open(file_path, "w", encoding="utf-8") as f:
1097
+ f.write(str(data))
1098
+
1099
+ def _cleanup_old_files(self) -> None:
1100
+ """
1101
+ Clean up old files based on the cleanup_age_hours setting.
1102
+ """
1103
+ if not self.temp_dir:
1104
+ return
1105
+
1106
+ try:
1107
+ current_time = self.datetime.now()
1108
+ cutoff_time = current_time - self.timedelta(hours=self.cleanup_age_hours)
1109
+
1110
+ for filename in self.os.listdir(self.temp_dir):
1111
+ if filename.startswith(self.file_prefix):
1112
+ file_path = self.os.path.join(self.temp_dir, filename)
1113
+ file_time = self.datetime.fromtimestamp(
1114
+ self.os.path.getmtime(file_path)
1115
+ )
1116
+
1117
+ if file_time < cutoff_time:
1118
+ self.os.remove(file_path)
1119
+
1120
+ except Exception as e:
1121
+ # Log error but don't fail the hook
1122
+ print(f"Warning: Failed to cleanup old files: {e}")