ripperdoc 0.2.7__py3-none-any.whl → 0.2.8__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.
@@ -0,0 +1,353 @@
1
+ """Integration helpers for hooks with tool execution.
2
+
3
+ This module provides convenient integration points for running hooks
4
+ as part of tool execution flows.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, Optional, Tuple, TypeVar, Union
9
+
10
+ from ripperdoc.core.hooks.events import HookDecision
11
+ from ripperdoc.core.hooks.manager import HookManager, HookResult, hook_manager
12
+ from ripperdoc.utils.log import get_logger
13
+
14
+ logger = get_logger()
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ class HookInterceptor:
20
+ """Provides hook interception for tool execution.
21
+
22
+ This class wraps tool execution with pre/post hooks,
23
+ handling blocking decisions and context injection.
24
+ """
25
+
26
+ def __init__(self, manager: Optional[HookManager] = None):
27
+ """Initialize the interceptor.
28
+
29
+ Args:
30
+ manager: HookManager to use, defaults to global instance
31
+ """
32
+ self.manager = manager or hook_manager
33
+
34
+ def check_pre_tool_use(
35
+ self, tool_name: str, tool_input: Dict[str, Any]
36
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
37
+ """Check if a tool call should proceed based on PreToolUse hooks.
38
+
39
+ Args:
40
+ tool_name: Name of the tool
41
+ tool_input: Tool input parameters
42
+
43
+ Returns:
44
+ Tuple of (should_proceed, block_reason, additional_context)
45
+ - should_proceed: True if tool should execute
46
+ - block_reason: Reason if blocked, None otherwise
47
+ - additional_context: Additional context to add if any
48
+ """
49
+ result = self.manager.run_pre_tool_use(tool_name, tool_input)
50
+
51
+ if result.should_block:
52
+ logger.info(
53
+ f"Tool {tool_name} blocked by hook",
54
+ extra={"reason": result.block_reason},
55
+ )
56
+ return False, result.block_reason, result.additional_context
57
+
58
+ if result.should_ask:
59
+ # For 'ask' decision, we return a special flag
60
+ # The caller should prompt the user for confirmation
61
+ return False, "USER_CONFIRMATION_REQUIRED", result.additional_context
62
+
63
+ return True, None, result.additional_context
64
+
65
+ async def check_pre_tool_use_async(
66
+ self, tool_name: str, tool_input: Dict[str, Any]
67
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
68
+ """Async version of check_pre_tool_use."""
69
+ result = await self.manager.run_pre_tool_use_async(tool_name, tool_input)
70
+
71
+ if result.should_block:
72
+ logger.info(
73
+ f"Tool {tool_name} blocked by hook",
74
+ extra={"reason": result.block_reason},
75
+ )
76
+ return False, result.block_reason, result.additional_context
77
+
78
+ if result.should_ask:
79
+ return False, "USER_CONFIRMATION_REQUIRED", result.additional_context
80
+
81
+ return True, None, result.additional_context
82
+
83
+ def run_post_tool_use(
84
+ self,
85
+ tool_name: str,
86
+ tool_input: Dict[str, Any],
87
+ tool_output: Any = None,
88
+ tool_error: Optional[str] = None,
89
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
90
+ """Run PostToolUse hooks after tool execution.
91
+
92
+ Args:
93
+ tool_name: Name of the tool
94
+ tool_input: Tool input parameters
95
+ tool_output: Output from the tool
96
+ tool_error: Error message if tool failed
97
+
98
+ Returns:
99
+ Tuple of (should_continue, block_reason, additional_context)
100
+ """
101
+ result = self.manager.run_post_tool_use(
102
+ tool_name, tool_input, tool_output, tool_error
103
+ )
104
+
105
+ if result.should_block:
106
+ return False, result.block_reason, result.additional_context
107
+
108
+ return True, None, result.additional_context
109
+
110
+ async def run_post_tool_use_async(
111
+ self,
112
+ tool_name: str,
113
+ tool_input: Dict[str, Any],
114
+ tool_output: Any = None,
115
+ tool_error: Optional[str] = None,
116
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
117
+ """Async version of run_post_tool_use."""
118
+ result = await self.manager.run_post_tool_use_async(
119
+ tool_name, tool_input, tool_output, tool_error
120
+ )
121
+
122
+ if result.should_block:
123
+ return False, result.block_reason, result.additional_context
124
+
125
+ return True, None, result.additional_context
126
+
127
+ def wrap_tool_execution(
128
+ self,
129
+ tool_name: str,
130
+ tool_input: Dict[str, Any],
131
+ execute_fn: Callable[[], T],
132
+ ) -> Tuple[bool, Union[T, str], Optional[str]]:
133
+ """Wrap synchronous tool execution with pre/post hooks.
134
+
135
+ Args:
136
+ tool_name: Name of the tool
137
+ tool_input: Tool input parameters
138
+ execute_fn: Function to execute the tool
139
+
140
+ Returns:
141
+ Tuple of (success, result_or_error, additional_context)
142
+ """
143
+ # Run pre-tool hooks
144
+ should_proceed, block_reason, pre_context = self.check_pre_tool_use(
145
+ tool_name, tool_input
146
+ )
147
+
148
+ if not should_proceed:
149
+ return False, block_reason or "Blocked by hook", pre_context
150
+
151
+ # Execute the tool
152
+ try:
153
+ result = execute_fn()
154
+ tool_error = None
155
+ except Exception as e:
156
+ result = None
157
+ tool_error = str(e)
158
+
159
+ # Run post-tool hooks
160
+ _, _, post_context = self.run_post_tool_use(
161
+ tool_name, tool_input, result, tool_error
162
+ )
163
+
164
+ # Combine contexts
165
+ combined_context = None
166
+ if pre_context or post_context:
167
+ parts = [c for c in [pre_context, post_context] if c]
168
+ combined_context = "\n".join(parts) if parts else None
169
+
170
+ if tool_error:
171
+ return False, tool_error, combined_context
172
+
173
+ return True, result, combined_context
174
+
175
+ async def wrap_tool_execution_async(
176
+ self,
177
+ tool_name: str,
178
+ tool_input: Dict[str, Any],
179
+ execute_fn: Callable[[], T],
180
+ ) -> Tuple[bool, Union[T, str], Optional[str]]:
181
+ """Wrap async tool execution with pre/post hooks."""
182
+ # Run pre-tool hooks
183
+ should_proceed, block_reason, pre_context = await self.check_pre_tool_use_async(
184
+ tool_name, tool_input
185
+ )
186
+
187
+ if not should_proceed:
188
+ return False, block_reason or "Blocked by hook", pre_context
189
+
190
+ # Execute the tool
191
+ try:
192
+ import asyncio
193
+ if asyncio.iscoroutinefunction(execute_fn):
194
+ result = await execute_fn()
195
+ else:
196
+ result = execute_fn()
197
+ tool_error = None
198
+ except Exception as e:
199
+ result = None
200
+ tool_error = str(e)
201
+
202
+ # Run post-tool hooks
203
+ _, _, post_context = await self.run_post_tool_use_async(
204
+ tool_name, tool_input, result, tool_error
205
+ )
206
+
207
+ # Combine contexts
208
+ combined_context = None
209
+ if pre_context or post_context:
210
+ parts = [c for c in [pre_context, post_context] if c]
211
+ combined_context = "\n".join(parts) if parts else None
212
+
213
+ if tool_error:
214
+ return False, tool_error, combined_context
215
+
216
+ return True, result, combined_context
217
+
218
+
219
+ # Global interceptor instance
220
+ hook_interceptor = HookInterceptor()
221
+
222
+
223
+ def check_pre_tool_use(
224
+ tool_name: str, tool_input: Dict[str, Any]
225
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
226
+ """Convenience function to check pre-tool hooks using global interceptor."""
227
+ return hook_interceptor.check_pre_tool_use(tool_name, tool_input)
228
+
229
+
230
+ async def check_pre_tool_use_async(
231
+ tool_name: str, tool_input: Dict[str, Any]
232
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
233
+ """Async convenience function to check pre-tool hooks."""
234
+ return await hook_interceptor.check_pre_tool_use_async(tool_name, tool_input)
235
+
236
+
237
+ def run_post_tool_use(
238
+ tool_name: str,
239
+ tool_input: Dict[str, Any],
240
+ tool_output: Any = None,
241
+ tool_error: Optional[str] = None,
242
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
243
+ """Convenience function to run post-tool hooks using global interceptor."""
244
+ return hook_interceptor.run_post_tool_use(
245
+ tool_name, tool_input, tool_output, tool_error
246
+ )
247
+
248
+
249
+ async def run_post_tool_use_async(
250
+ tool_name: str,
251
+ tool_input: Dict[str, Any],
252
+ tool_output: Any = None,
253
+ tool_error: Optional[str] = None,
254
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
255
+ """Async convenience function to run post-tool hooks."""
256
+ return await hook_interceptor.run_post_tool_use_async(
257
+ tool_name, tool_input, tool_output, tool_error
258
+ )
259
+
260
+
261
+ def check_user_prompt(prompt: str) -> Tuple[bool, Optional[str], Optional[str]]:
262
+ """Check if a user prompt should be processed.
263
+
264
+ Args:
265
+ prompt: The user's prompt text
266
+
267
+ Returns:
268
+ Tuple of (should_process, block_reason, additional_context)
269
+ """
270
+ result = hook_manager.run_user_prompt_submit(prompt)
271
+
272
+ if result.should_block:
273
+ return False, result.block_reason, result.additional_context
274
+
275
+ return True, None, result.additional_context
276
+
277
+
278
+ async def check_user_prompt_async(
279
+ prompt: str,
280
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
281
+ """Async version of check_user_prompt."""
282
+ result = await hook_manager.run_user_prompt_submit_async(prompt)
283
+
284
+ if result.should_block:
285
+ return False, result.block_reason, result.additional_context
286
+
287
+ return True, None, result.additional_context
288
+
289
+
290
+ def notify_session_start(trigger: str) -> Optional[str]:
291
+ """Notify hooks that a session is starting.
292
+
293
+ Args:
294
+ trigger: "startup", "resume", "clear", or "compact"
295
+
296
+ Returns:
297
+ Additional context from hooks, if any
298
+ """
299
+ result = hook_manager.run_session_start(trigger)
300
+ return result.additional_context
301
+
302
+
303
+ def notify_session_end(
304
+ trigger: str,
305
+ duration_seconds: Optional[float] = None,
306
+ message_count: int = 0,
307
+ ) -> Optional[str]:
308
+ """Notify hooks that a session is ending.
309
+
310
+ Args:
311
+ trigger: "clear", "logout", "prompt_input_exit", or "other"
312
+ duration_seconds: How long the session lasted
313
+ message_count: Number of messages in the session
314
+
315
+ Returns:
316
+ Additional context from hooks, if any
317
+ """
318
+ result = hook_manager.run_session_end(trigger, duration_seconds, message_count)
319
+ return result.additional_context
320
+
321
+
322
+ def check_stop(
323
+ reason: Optional[str] = None, stop_sequence: Optional[str] = None
324
+ ) -> Tuple[bool, Optional[str]]:
325
+ """Check if the agent should stop.
326
+
327
+ Args:
328
+ reason: Why the agent wants to stop
329
+ stop_sequence: The stop sequence encountered, if any
330
+
331
+ Returns:
332
+ Tuple of (should_stop, continue_reason)
333
+ - should_stop: True if agent should stop
334
+ - continue_reason: Reason to continue if blocked
335
+ """
336
+ result = hook_manager.run_stop(reason, stop_sequence)
337
+
338
+ if result.should_block:
339
+ return False, result.block_reason
340
+
341
+ return True, None
342
+
343
+
344
+ async def check_stop_async(
345
+ reason: Optional[str] = None, stop_sequence: Optional[str] = None
346
+ ) -> Tuple[bool, Optional[str]]:
347
+ """Async version of check_stop."""
348
+ result = await hook_manager.run_stop_async(reason, stop_sequence)
349
+
350
+ if result.should_block:
351
+ return False, result.block_reason
352
+
353
+ return True, None