tooluniverse 0.2.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/__init__.py +340 -4
- tooluniverse/admetai_tool.py +84 -0
- tooluniverse/agentic_tool.py +563 -0
- tooluniverse/alphafold_tool.py +96 -0
- tooluniverse/base_tool.py +129 -6
- tooluniverse/boltz_tool.py +207 -0
- tooluniverse/chem_tool.py +192 -0
- tooluniverse/compose_scripts/__init__.py +1 -0
- tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
- tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
- tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
- tooluniverse/compose_scripts/literature_tool.py +34 -0
- tooluniverse/compose_scripts/output_summarizer.py +279 -0
- tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
- tooluniverse/compose_scripts/tool_discover.py +705 -0
- tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
- tooluniverse/compose_tool.py +371 -0
- tooluniverse/ctg_tool.py +1002 -0
- tooluniverse/custom_tool.py +81 -0
- tooluniverse/dailymed_tool.py +108 -0
- tooluniverse/data/admetai_tools.json +155 -0
- tooluniverse/data/adverse_event_tools.json +108 -0
- tooluniverse/data/agentic_tools.json +1156 -0
- tooluniverse/data/alphafold_tools.json +87 -0
- tooluniverse/data/boltz_tools.json +9 -0
- tooluniverse/data/chembl_tools.json +16 -0
- tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
- tooluniverse/data/compose_tools.json +202 -0
- tooluniverse/data/dailymed_tools.json +70 -0
- tooluniverse/data/dataset_tools.json +646 -0
- tooluniverse/data/disease_target_score_tools.json +712 -0
- tooluniverse/data/efo_tools.json +17 -0
- tooluniverse/data/embedding_tools.json +319 -0
- tooluniverse/data/enrichr_tools.json +31 -0
- tooluniverse/data/europe_pmc_tools.json +22 -0
- tooluniverse/data/expert_feedback_tools.json +10 -0
- tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
- tooluniverse/data/fda_drug_labeling_tools.json +1 -1
- tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
- tooluniverse/data/finder_tools.json +209 -0
- tooluniverse/data/gene_ontology_tools.json +113 -0
- tooluniverse/data/gwas_tools.json +1082 -0
- tooluniverse/data/hpa_tools.json +333 -0
- tooluniverse/data/humanbase_tools.json +47 -0
- tooluniverse/data/idmap_tools.json +74 -0
- tooluniverse/data/mcp_client_tools_example.json +113 -0
- tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
- tooluniverse/data/medlineplus_tools.json +141 -0
- tooluniverse/data/monarch_tools.json +1 -1
- tooluniverse/data/openalex_tools.json +36 -0
- tooluniverse/data/opentarget_tools.json +1 -1
- tooluniverse/data/output_summarization_tools.json +101 -0
- tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
- tooluniverse/data/packages/categorized_tools.txt +206 -0
- tooluniverse/data/packages/cheminformatics_tools.json +347 -0
- tooluniverse/data/packages/earth_sciences_tools.json +74 -0
- tooluniverse/data/packages/genomics_tools.json +776 -0
- tooluniverse/data/packages/image_processing_tools.json +38 -0
- tooluniverse/data/packages/machine_learning_tools.json +789 -0
- tooluniverse/data/packages/neuroscience_tools.json +62 -0
- tooluniverse/data/packages/original_tools.txt +0 -0
- tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
- tooluniverse/data/packages/scientific_computing_tools.json +560 -0
- tooluniverse/data/packages/single_cell_tools.json +453 -0
- tooluniverse/data/packages/structural_biology_tools.json +396 -0
- tooluniverse/data/packages/visualization_tools.json +399 -0
- tooluniverse/data/pubchem_tools.json +215 -0
- tooluniverse/data/pubtator_tools.json +68 -0
- tooluniverse/data/rcsb_pdb_tools.json +1332 -0
- tooluniverse/data/reactome_tools.json +19 -0
- tooluniverse/data/semantic_scholar_tools.json +26 -0
- tooluniverse/data/special_tools.json +2 -25
- tooluniverse/data/tool_composition_tools.json +88 -0
- tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
- tooluniverse/data/txagent_client_tools.json +9 -0
- tooluniverse/data/uniprot_tools.json +211 -0
- tooluniverse/data/url_fetch_tools.json +94 -0
- tooluniverse/data/uspto_downloader_tools.json +9 -0
- tooluniverse/data/uspto_tools.json +811 -0
- tooluniverse/data/xml_tools.json +3275 -0
- tooluniverse/dataset_tool.py +296 -0
- tooluniverse/default_config.py +165 -0
- tooluniverse/efo_tool.py +42 -0
- tooluniverse/embedding_database.py +630 -0
- tooluniverse/embedding_sync.py +396 -0
- tooluniverse/enrichr_tool.py +266 -0
- tooluniverse/europe_pmc_tool.py +52 -0
- tooluniverse/execute_function.py +1775 -95
- tooluniverse/extended_hooks.py +444 -0
- tooluniverse/gene_ontology_tool.py +194 -0
- tooluniverse/graphql_tool.py +158 -36
- tooluniverse/gwas_tool.py +358 -0
- tooluniverse/hpa_tool.py +1645 -0
- tooluniverse/humanbase_tool.py +389 -0
- tooluniverse/logging_config.py +254 -0
- tooluniverse/mcp_client_tool.py +764 -0
- tooluniverse/mcp_integration.py +413 -0
- tooluniverse/mcp_tool_registry.py +925 -0
- tooluniverse/medlineplus_tool.py +337 -0
- tooluniverse/openalex_tool.py +228 -0
- tooluniverse/openfda_adv_tool.py +283 -0
- tooluniverse/openfda_tool.py +393 -160
- tooluniverse/output_hook.py +1122 -0
- tooluniverse/package_tool.py +195 -0
- tooluniverse/pubchem_tool.py +158 -0
- tooluniverse/pubtator_tool.py +168 -0
- tooluniverse/rcsb_pdb_tool.py +38 -0
- tooluniverse/reactome_tool.py +108 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
- tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
- tooluniverse/remote/expert_feedback/simple_test.py +23 -0
- tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
- tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
- tooluniverse/remote/immune_compass/compass_tool.py +327 -0
- tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
- tooluniverse/remote_tool.py +99 -0
- tooluniverse/restful_tool.py +53 -30
- tooluniverse/scripts/generate_tool_graph.py +408 -0
- tooluniverse/scripts/visualize_tool_graph.py +829 -0
- tooluniverse/semantic_scholar_tool.py +62 -0
- tooluniverse/smcp.py +2452 -0
- tooluniverse/smcp_server.py +975 -0
- tooluniverse/test/mcp_server_test.py +0 -0
- tooluniverse/test/test_admetai_tool.py +370 -0
- tooluniverse/test/test_agentic_tool.py +129 -0
- tooluniverse/test/test_alphafold_tool.py +71 -0
- tooluniverse/test/test_chem_tool.py +37 -0
- tooluniverse/test/test_compose_lieraturereview.py +63 -0
- tooluniverse/test/test_compose_tool.py +448 -0
- tooluniverse/test/test_dailymed.py +69 -0
- tooluniverse/test/test_dataset_tool.py +200 -0
- tooluniverse/test/test_disease_target_score.py +56 -0
- tooluniverse/test/test_drugbank_filter_examples.py +179 -0
- tooluniverse/test/test_efo.py +31 -0
- tooluniverse/test/test_enrichr_tool.py +21 -0
- tooluniverse/test/test_europe_pmc_tool.py +20 -0
- tooluniverse/test/test_fda_adv.py +95 -0
- tooluniverse/test/test_fda_drug_labeling.py +91 -0
- tooluniverse/test/test_gene_ontology_tools.py +66 -0
- tooluniverse/test/test_gwas_tool.py +139 -0
- tooluniverse/test/test_hpa.py +625 -0
- tooluniverse/test/test_humanbase_tool.py +20 -0
- tooluniverse/test/test_idmap_tools.py +61 -0
- tooluniverse/test/test_mcp_server.py +211 -0
- tooluniverse/test/test_mcp_tool.py +247 -0
- tooluniverse/test/test_medlineplus.py +220 -0
- tooluniverse/test/test_openalex_tool.py +32 -0
- tooluniverse/test/test_opentargets.py +28 -0
- tooluniverse/test/test_pubchem_tool.py +116 -0
- tooluniverse/test/test_pubtator_tool.py +37 -0
- tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
- tooluniverse/test/test_reactome.py +54 -0
- tooluniverse/test/test_semantic_scholar_tool.py +24 -0
- tooluniverse/test/test_software_tools.py +147 -0
- tooluniverse/test/test_tool_description_optimizer.py +49 -0
- tooluniverse/test/test_tool_finder.py +26 -0
- tooluniverse/test/test_tool_finder_llm.py +252 -0
- tooluniverse/test/test_tools_find.py +195 -0
- tooluniverse/test/test_uniprot_tools.py +74 -0
- tooluniverse/test/test_uspto_tool.py +72 -0
- tooluniverse/test/test_xml_tool.py +113 -0
- tooluniverse/tool_finder_embedding.py +267 -0
- tooluniverse/tool_finder_keyword.py +693 -0
- tooluniverse/tool_finder_llm.py +699 -0
- tooluniverse/tool_graph_web_ui.py +955 -0
- tooluniverse/tool_registry.py +416 -0
- tooluniverse/uniprot_tool.py +155 -0
- tooluniverse/url_tool.py +253 -0
- tooluniverse/uspto_tool.py +240 -0
- tooluniverse/utils.py +369 -41
- tooluniverse/xml_tool.py +369 -0
- tooluniverse-1.0.1.dist-info/METADATA +387 -0
- tooluniverse-1.0.1.dist-info/RECORD +182 -0
- tooluniverse-1.0.1.dist-info/entry_points.txt +9 -0
- tooluniverse/generate_mcp_tools.py +0 -113
- tooluniverse/mcp_server.py +0 -3340
- tooluniverse-0.2.0.dist-info/METADATA +0 -139
- tooluniverse-0.2.0.dist-info/RECORD +0 -21
- tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/WHEEL +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,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}")
|