ragbits-agents 1.4.0.dev202512050236__py3-none-any.whl → 1.4.0.dev202601310254__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.
@@ -9,8 +9,12 @@ from ragbits.agents._main import (
9
9
  ToolCall,
10
10
  ToolCallResult,
11
11
  )
12
+ from ragbits.agents.hooks import (
13
+ EventType,
14
+ Hook,
15
+ HookManager,
16
+ )
12
17
  from ragbits.agents.post_processors.base import PostProcessor, StreamingPostProcessor
13
- from ragbits.agents.tool import requires_confirmation
14
18
  from ragbits.agents.tools import LongTermMemory, MemoryEntry, create_memory_tools
15
19
  from ragbits.agents.types import QuestionAnswerAgent, QuestionAnswerPromptInput, QuestionAnswerPromptOutput
16
20
 
@@ -22,6 +26,9 @@ __all__ = [
22
26
  "AgentResultStreaming",
23
27
  "AgentRunContext",
24
28
  "DownstreamAgentResult",
29
+ "EventType",
30
+ "Hook",
31
+ "HookManager",
25
32
  "LongTermMemory",
26
33
  "MemoryEntry",
27
34
  "PostProcessor",
@@ -32,5 +39,4 @@ __all__ = [
32
39
  "ToolCall",
33
40
  "ToolCallResult",
34
41
  "create_memory_tools",
35
- "requires_confirmation",
36
42
  ]
ragbits/agents/_main.py CHANGED
@@ -1,6 +1,4 @@
1
1
  import asyncio
2
- import hashlib
3
- import json
4
2
  import types
5
3
  import uuid
6
4
  from collections.abc import AsyncGenerator, AsyncIterator, Callable
@@ -32,6 +30,10 @@ from ragbits.agents.exceptions import (
32
30
  AgentToolNotAvailableError,
33
31
  AgentToolNotSupportedError,
34
32
  )
33
+ from ragbits.agents.hooks import (
34
+ Hook,
35
+ HookManager,
36
+ )
35
37
  from ragbits.agents.mcp.server import MCPServer, MCPServerStdio, MCPServerStreamableHttp
36
38
  from ragbits.agents.mcp.utils import get_tools
37
39
  from ragbits.agents.post_processors.base import (
@@ -40,7 +42,7 @@ from ragbits.agents.post_processors.base import (
40
42
  StreamingPostProcessor,
41
43
  stream_with_post_processing,
42
44
  )
43
- from ragbits.agents.tool import Tool, ToolCallResult, ToolChoice
45
+ from ragbits.agents.tool import Tool, ToolCallResult, ToolChoice, ToolReturn
44
46
  from ragbits.core.audit.traces import trace
45
47
  from ragbits.core.llms.base import (
46
48
  LLM,
@@ -65,10 +67,6 @@ with suppress(ImportError):
65
67
 
66
68
  from ragbits.core.llms import LiteLLM
67
69
 
68
- # Confirmation ID length: 16 hex chars provides sufficient uniqueness
69
- # while being compact for display and storage
70
- CONFIRMATION_ID_LENGTH = 16
71
-
72
70
  _Input = TypeVar("_Input", bound=BaseModel)
73
71
  _Output = TypeVar("_Output")
74
72
 
@@ -212,9 +210,10 @@ class AgentRunContext(BaseModel, Generic[DepsT]):
212
210
  """Whether to stream events from downstream agents when tools execute other agents."""
213
211
  downstream_agents: dict[str, "Agent"] = Field(default_factory=dict)
214
212
  """Registry of all agents that participated in this run"""
215
- confirmed_tools: list[dict[str, Any]] | None = Field(
216
- default=None,
217
- description="List of confirmed/declined tools from the frontend",
213
+ tool_confirmations: list[dict[str, Any]] = Field(
214
+ default_factory=list,
215
+ description="List of confirmed/declined tool executions. Each entry has 'confirmation_id' and 'confirmed' "
216
+ "(bool)",
218
217
  )
219
218
 
220
219
  def register_agent(self, agent: "Agent") -> None:
@@ -375,6 +374,7 @@ class Agent(
375
374
  keep_history: bool = False,
376
375
  tools: list[Callable] | None = None,
377
376
  mcp_servers: list[MCPServer] | None = None,
377
+ hooks: list[Hook] | None = None,
378
378
  default_options: AgentOptions[LLMClientOptionsT] | None = None,
379
379
  ) -> None:
380
380
  """
@@ -394,6 +394,7 @@ class Agent(
394
394
  keep_history: Whether to keep the history of the agent.
395
395
  tools: The tools available to the agent.
396
396
  mcp_servers: The MCP servers available to the agent.
397
+ hooks: List of tool hooks to register for tool lifecycle events.
397
398
  default_options: The default options for the agent run.
398
399
  """
399
400
  super().__init__(default_options)
@@ -409,11 +410,14 @@ class Agent(
409
410
  self.tools.append(Tool.from_agent(agent, **kwargs))
410
411
  elif isinstance(tool, Agent):
411
412
  self.tools.append(Tool.from_agent(tool))
413
+ elif isinstance(tool, Tool):
414
+ self.tools.append(tool)
412
415
  else:
413
416
  self.tools.append(Tool.from_callable(tool))
414
417
  self.mcp_servers = mcp_servers or []
415
418
  self.history = history or []
416
419
  self.keep_history = keep_history
420
+ self.hook_manager = HookManager(hooks)
417
421
 
418
422
  if getattr(self, "system_prompt", None) and not getattr(self, "input_type", None):
419
423
  raise ValueError(
@@ -534,7 +538,9 @@ class Agent(
534
538
  ):
535
539
  if isinstance(result, ToolCallResult):
536
540
  tool_calls.append(result)
537
- prompt_with_history = prompt_with_history.add_tool_use_message(**result.__dict__)
541
+ prompt_with_history = prompt_with_history.add_tool_use_message(
542
+ id=result.id, name=result.name, arguments=result.arguments, result=result.result
543
+ )
538
544
 
539
545
  turn_count += 1
540
546
  else:
@@ -760,7 +766,9 @@ class Agent(
760
766
  elif isinstance(result, ToolCallResult):
761
767
  # Add ALL tool results to history (including pending confirmations)
762
768
  # This allows the agent to see them in the next turn
763
- prompt_with_history = prompt_with_history.add_tool_use_message(**result.__dict__)
769
+ prompt_with_history = prompt_with_history.add_tool_use_message(
770
+ id=result.id, name=result.name, arguments=result.arguments, result=result.result
771
+ )
764
772
  returned_tool_call = True
765
773
 
766
774
  # If we have pending confirmations, prepare for text-only summary generation
@@ -956,54 +964,37 @@ class Agent(
956
964
 
957
965
  tool = tools_mapping[tool_call.name]
958
966
 
959
- # Check if tool requires confirmation
960
- if tool.requires_confirmation:
961
- # Check if this tool has been confirmed in the context
962
- confirmed_tools = context.confirmed_tools or []
963
-
964
- # Generate a stable confirmation ID based on tool name and arguments
965
- confirmation_id = hashlib.sha256(
966
- f"{tool_call.name}:{json.dumps(tool_call.arguments, sort_keys=True)}".encode()
967
- ).hexdigest()[:CONFIRMATION_ID_LENGTH]
967
+ # Execute PRE_TOOL hooks with chaining
968
+ pre_tool_result = await self.hook_manager.execute_pre_tool(
969
+ tool_call=tool_call,
970
+ context=context,
971
+ )
968
972
 
969
- # Check if this specific tool call has been confirmed or declined
970
- is_confirmed = any(
971
- ct.get("confirmation_id") == confirmation_id and ct.get("confirmed") for ct in confirmed_tools
973
+ # Check decision
974
+ if pre_tool_result.decision == "deny":
975
+ yield ToolCallResult(
976
+ id=tool_call.id,
977
+ name=tool_call.name,
978
+ arguments=tool_call.arguments,
979
+ result=pre_tool_result.reason or "Tool execution denied",
972
980
  )
973
- is_declined = any(
974
- ct.get("confirmation_id") == confirmation_id and not ct.get("confirmed", True) for ct in confirmed_tools
981
+ return
982
+ # Handle "ask" decision from hooks
983
+ elif pre_tool_result.decision == "ask" and pre_tool_result.confirmation_request is not None:
984
+ yield pre_tool_result.confirmation_request
985
+
986
+ yield ToolCallResult(
987
+ id=tool_call.id,
988
+ name=tool_call.name,
989
+ arguments=tool_call.arguments,
990
+ result=pre_tool_result.reason or "Hook requires user confirmation",
975
991
  )
992
+ return
976
993
 
977
- if is_declined:
978
- # Tool was explicitly declined - skip execution entirely
979
- yield ToolCallResult(
980
- id=tool_call.id,
981
- name=tool_call.name,
982
- arguments=tool_call.arguments,
983
- result="❌ Action declined by user",
984
- )
985
- return
986
-
987
- if not is_confirmed:
988
- # Tool not confirmed yet - create and yield confirmation request
989
- request = ConfirmationRequest(
990
- confirmation_id=confirmation_id,
991
- tool_name=tool_call.name,
992
- tool_description=tool.description or "",
993
- arguments=tool_call.arguments,
994
- )
995
-
996
- # Yield confirmation request (will be streamed to frontend)
997
- yield request
994
+ # Always update arguments (chained from hooks)
995
+ tool_call.arguments = pre_tool_result.arguments
998
996
 
999
- # Yield a pending result and exit without executing
1000
- yield ToolCallResult(
1001
- id=tool_call.id,
1002
- name=tool_call.name,
1003
- arguments=tool_call.arguments,
1004
- result="⏳ Awaiting user confirmation",
1005
- )
1006
- return
997
+ tool_error: Exception | None = None
1007
998
 
1008
999
  with trace(agent_id=self.id, tool_name=tool_call.name, tool_arguments=tool_call.arguments) as outputs:
1009
1000
  try:
@@ -1017,35 +1008,51 @@ class Agent(
1017
1008
  else asyncio.to_thread(tool.on_tool_call, **call_args)
1018
1009
  )
1019
1010
 
1020
- if isinstance(tool_output, AgentResultStreaming):
1011
+ if isinstance(tool_output, ToolReturn):
1012
+ tool_return = tool_output
1013
+ elif isinstance(tool_output, AgentResultStreaming):
1021
1014
  async for downstream_item in tool_output:
1022
1015
  if context.stream_downstream_events:
1023
1016
  yield DownstreamAgentResult(agent_id=tool.id, item=downstream_item)
1024
-
1025
- tool_output = {
1026
- "content": tool_output.content,
1017
+ metadata = {
1027
1018
  "metadata": tool_output.metadata,
1028
1019
  "tool_calls": tool_output.tool_calls,
1029
1020
  "usage": tool_output.usage,
1030
1021
  }
1022
+ tool_return = ToolReturn(value=tool_output.content, metadata=metadata)
1023
+ else:
1024
+ tool_return = ToolReturn(value=tool_output, metadata=None)
1031
1025
 
1032
1026
  outputs.result = {
1033
- "tool_output": tool_output,
1027
+ "tool_output": tool_return.value,
1034
1028
  "tool_call_id": tool_call.id,
1035
1029
  }
1036
1030
 
1037
1031
  except Exception as e:
1032
+ tool_error = e
1038
1033
  outputs.result = {
1039
1034
  "error": str(e),
1040
1035
  "tool_call_id": tool_call.id,
1041
1036
  }
1042
- raise AgentToolExecutionError(tool_call.name, e) from e
1037
+
1038
+ # Execute POST_TOOL hooks with chaining
1039
+ post_tool_result = await self.hook_manager.execute_post_tool(
1040
+ tool_call=tool_call,
1041
+ output=tool_output,
1042
+ error=tool_error,
1043
+ )
1044
+ tool_output = post_tool_result.output
1045
+
1046
+ # Raise error after hooks have been executed
1047
+ if tool_error:
1048
+ raise AgentToolExecutionError(tool_call.name, tool_error) from tool_error
1043
1049
 
1044
1050
  yield ToolCallResult(
1045
1051
  id=tool_call.id,
1046
1052
  name=tool_call.name,
1047
1053
  arguments=tool_call.arguments,
1048
- result=tool_output,
1054
+ result=tool_return.value,
1055
+ metadata=tool_return.metadata,
1049
1056
  )
1050
1057
 
1051
1058
  @requires_dependencies(["a2a.types"], "a2a")
@@ -0,0 +1,77 @@
1
+ """
2
+ Hooks system for lifecycle events.
3
+
4
+ This module provides a comprehensive hook system that allows users to register
5
+ custom logic at various points in the execution lifecycle.
6
+
7
+ Available event types:
8
+ - PRE_TOOL: Before a tool is invoked
9
+ - POST_TOOL: After a tool completes
10
+
11
+ Example usage:
12
+
13
+ from ragbits.agents.hooks import (
14
+ EventType,
15
+ Hook,
16
+ PreToolInput,
17
+ PreToolOutput,
18
+ )
19
+
20
+ # Create a pre-tool hook callback
21
+ async def validate_input(input_data: PreToolInput) -> PreToolOutput:
22
+ if input_data.tool_call.name == "dangerous_tool":
23
+ return PreToolOutput(
24
+ arguments=input_data.tool_call.arguments,
25
+ decision="deny",
26
+ reason="This tool is not allowed"
27
+ )
28
+ return PreToolOutput(arguments=input_data.tool_call.arguments, decision="pass")
29
+
30
+ # Create hook instance with proper type annotation
31
+ hook: Hook[PreToolInput, PreToolOutput] = Hook(
32
+ event_type=EventType.PRE_TOOL,
33
+ callback=validate_input,
34
+ tool_names=["dangerous_tool"],
35
+ priority=10
36
+ )
37
+
38
+ # Register hooks with agent
39
+ agent = Agent(
40
+ ...,
41
+ hooks=[hook]
42
+ )
43
+ """
44
+
45
+ from ragbits.agents.hooks.base import Hook, HookInputT, HookOutputT
46
+ from ragbits.agents.hooks.confirmation import create_confirmation_hook
47
+ from ragbits.agents.hooks.manager import HookManager
48
+ from ragbits.agents.hooks.types import (
49
+ EventType,
50
+ PostToolHookCallback,
51
+ PostToolInput,
52
+ PostToolOutput,
53
+ PreToolHookCallback,
54
+ PreToolInput,
55
+ PreToolOutput,
56
+ )
57
+
58
+ __all__ = [
59
+ # Event types
60
+ "EventType",
61
+ # Core classes
62
+ "Hook",
63
+ # Type variables
64
+ "HookInputT",
65
+ "HookManager",
66
+ "HookOutputT",
67
+ "PostToolHookCallback",
68
+ # Input/output types
69
+ "PostToolInput",
70
+ "PostToolOutput",
71
+ # Callback type aliases
72
+ "PreToolHookCallback",
73
+ "PreToolInput",
74
+ "PreToolOutput",
75
+ # Hook factories
76
+ "create_confirmation_hook",
77
+ ]
@@ -0,0 +1,94 @@
1
+ """
2
+ Base classes for the hooks system.
3
+ """
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Generic, TypeVar
7
+
8
+ from ragbits.agents.hooks.types import EventType, HookEventIO
9
+
10
+ HookInputT = TypeVar("HookInputT", bound=HookEventIO)
11
+ HookOutputT = TypeVar("HookOutputT", bound=HookEventIO)
12
+
13
+
14
+ class Hook(Generic[HookInputT, HookOutputT]):
15
+ """
16
+ A hook that intercepts execution at various lifecycle points.
17
+
18
+ Hooks allow you to:
19
+ - Validate inputs before execution (pre hooks)
20
+ - Control access (pre hooks)
21
+ - Modify inputs (pre hooks)
22
+ - Deny execution (pre hooks)
23
+ - Modify outputs (post hooks)
24
+ - Handle errors (post hooks)
25
+
26
+ Attributes:
27
+ event_type: The type of event (e.g., PRE_TOOL, POST_TOOL)
28
+ callback: The async function to call when the event is triggered
29
+ tool_names: List of tool names this hook applies to. If None, applies to all tools.
30
+ priority: Execution priority (lower numbers execute first, default: 100)
31
+
32
+ Example:
33
+ ```python
34
+ from ragbits.agents.hooks import Hook, EventType, PreToolInput, PreToolOutput
35
+
36
+
37
+ async def validate_input(input_data: PreToolInput) -> PreToolOutput:
38
+ if input_data.tool_call.name == "dangerous_tool":
39
+ return PreToolOutput(arguments=input_data.tool_call.arguments, decision="deny", reason="Not allowed")
40
+ return PreToolOutput(arguments=input_data.tool_call.arguments, decision="pass")
41
+
42
+
43
+ hook: Hook[PreToolInput, PreToolOutput] = Hook(
44
+ event_type=EventType.PRE_TOOL, callback=validate_input, tool_names=["dangerous_tool"], priority=10
45
+ )
46
+ ```
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ event_type: EventType,
52
+ callback: Callable[[HookInputT], Awaitable[HookOutputT]],
53
+ tool_names: list[str] | None = None,
54
+ priority: int = 100,
55
+ ) -> None:
56
+ """
57
+ Initialize a hook.
58
+
59
+ Args:
60
+ event_type: The type of event (e.g., PRE_TOOL, POST_TOOL)
61
+ callback: The async function to call when the event is triggered
62
+ tool_names: List of tool names this hook applies to. If None, applies to all tools.
63
+ priority: Execution priority (lower numbers execute first, default: 100)
64
+ """
65
+ self.event_type = event_type
66
+ self.callback = callback
67
+ self.tool_names = tool_names
68
+ self.priority = priority
69
+
70
+ def matches_tool(self, tool_name: str) -> bool:
71
+ """
72
+ Check if this hook applies to the given tool name.
73
+
74
+ Args:
75
+ tool_name: The name of the tool to check
76
+
77
+ Returns:
78
+ True if this hook should be executed for the given tool
79
+ """
80
+ if self.tool_names is None:
81
+ return True
82
+ return tool_name in self.tool_names
83
+
84
+ async def execute(self, hook_input: HookInputT) -> HookOutputT:
85
+ """
86
+ Execute the hook callback with the given input.
87
+
88
+ Args:
89
+ hook_input: The input to pass to the callback
90
+
91
+ Returns:
92
+ The output from the callback
93
+ """
94
+ return await self.callback(hook_input)
@@ -0,0 +1,51 @@
1
+ """
2
+ Helper functions for creating common hooks.
3
+
4
+ This module provides factory functions for creating commonly used hooks.
5
+ """
6
+
7
+ from ragbits.agents.hooks.base import Hook
8
+ from ragbits.agents.hooks.types import EventType, PreToolInput, PreToolOutput
9
+
10
+
11
+ def create_confirmation_hook(
12
+ tool_names: list[str] | None = None, priority: int = 1
13
+ ) -> Hook[PreToolInput, PreToolOutput]:
14
+ """
15
+ Create a hook that requires user confirmation before tool execution.
16
+
17
+ The hook returns "ask" decision, which causes the agent to yield a ConfirmationRequest
18
+ and wait for user approval/decline.
19
+
20
+ Args:
21
+ tool_names: List of tool names to require confirmation for. If None, applies to all tools.
22
+ priority: Hook priority (default: 1, runs first)
23
+
24
+ Returns:
25
+ Hook configured to require confirmation
26
+
27
+ Example:
28
+ ```python
29
+ from ragbits.agents import Agent
30
+ from ragbits.agents.hooks.confirmation import create_confirmation_hook
31
+
32
+ agent = Agent(
33
+ tools=[delete_file, send_email], hooks=[create_confirmation_hook(tool_names=["delete_file", "send_email"])]
34
+ )
35
+ ```
36
+ """
37
+
38
+ async def confirm_hook(input_data: PreToolInput) -> PreToolOutput:
39
+ """Hook that always returns 'ask' to require confirmation."""
40
+ return PreToolOutput(
41
+ arguments=input_data.tool_call.arguments,
42
+ decision="ask",
43
+ reason=f"Tool '{input_data.tool_call.name}' requires user confirmation",
44
+ )
45
+
46
+ return Hook(
47
+ event_type=EventType.PRE_TOOL,
48
+ callback=confirm_hook,
49
+ tool_names=tool_names,
50
+ priority=priority,
51
+ )
@@ -0,0 +1,194 @@
1
+ """
2
+ Hook manager for organizing and executing hooks.
3
+
4
+ This module provides the HookManager class which handles registration,
5
+ organization, and execution of hooks during lifecycle events.
6
+ """
7
+
8
+ import hashlib
9
+ import json
10
+ from collections import defaultdict
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from ragbits.agents.confirmation import ConfirmationRequest
14
+ from ragbits.agents.hooks.base import Hook
15
+ from ragbits.agents.hooks.types import EventType, PostToolInput, PostToolOutput, PreToolInput, PreToolOutput
16
+ from ragbits.core.llms.base import ToolCall
17
+
18
+ if TYPE_CHECKING:
19
+ from ragbits.agents._main import AgentRunContext
20
+
21
+ # Confirmation ID length: 16 hex chars provides sufficient uniqueness
22
+ # while being compact for display and storage
23
+ CONFIRMATION_ID_LENGTH = 16
24
+
25
+
26
+ class HookManager:
27
+ """
28
+ Manages registration and execution of hooks for an agent.
29
+
30
+ The HookManager organizes hooks by type and executes them in priority order,
31
+ with proper chaining of modifications between hooks.
32
+ """
33
+
34
+ def __init__(self, hooks: list[Hook] | None = None) -> None:
35
+ """
36
+ Initialize the hook manager.
37
+
38
+ Args:
39
+ hooks: Initial list of hooks to register
40
+ """
41
+ self._hooks: dict[EventType, list[Hook]] = defaultdict(list)
42
+
43
+ if hooks:
44
+ for hook in hooks:
45
+ self.register(hook)
46
+
47
+ def register(self, hook: Hook) -> None:
48
+ """
49
+ Register a hook.
50
+
51
+ Hooks are organized by type and sorted by priority
52
+ (lower numbers execute first).
53
+
54
+ Args:
55
+ hook: The hook to register
56
+ """
57
+ self._hooks[hook.event_type].append(hook)
58
+ self._hooks[hook.event_type].sort(key=lambda h: h.priority)
59
+
60
+ def get_hooks(self, event_type: EventType, tool_name: str | None) -> list[Hook]:
61
+ """
62
+ Get all hooks for a specific event type that match the tool name.
63
+
64
+ Args:
65
+ event_type: The type of event
66
+ tool_name: Optional tool name to filter hooks. If None, returns all hooks for the event type.
67
+
68
+ Returns:
69
+ List of matching hooks, sorted by priority
70
+ """
71
+ hooks = self._hooks.get(event_type, [])
72
+
73
+ if tool_name is None:
74
+ return hooks
75
+
76
+ return [hook for hook in hooks if hook.matches_tool(tool_name)]
77
+
78
+ async def execute_pre_tool(
79
+ self,
80
+ tool_call: ToolCall,
81
+ context: "AgentRunContext",
82
+ ) -> PreToolOutput:
83
+ """
84
+ Execute pre-tool hooks with proper chaining.
85
+
86
+ Each hook sees the modified arguments from the previous hook.
87
+ Execution stops immediately if any hook returns "deny" or "ask" (unless confirmed).
88
+
89
+ Args:
90
+ tool_call: The tool call to process
91
+ context: Agent run context containing tool_confirmations
92
+
93
+ Returns:
94
+ PreToolOutput with final arguments and decision
95
+ """
96
+ hooks = self.get_hooks(EventType.PRE_TOOL, tool_call.name)
97
+
98
+ # Start with original arguments
99
+ current_arguments = tool_call.arguments
100
+
101
+ for hook in hooks:
102
+ # Generate confirmation_id: hash(hook_function_name + tool_name + arguments)
103
+ hook_name = hook.callback.__name__
104
+ confirmation_id_str = f"{hook_name}:{tool_call.name}:{json.dumps(current_arguments, sort_keys=True)}"
105
+ confirmation_id = hashlib.sha256(confirmation_id_str.encode()).hexdigest()[:CONFIRMATION_ID_LENGTH]
106
+
107
+ # Create input with current state (chained from previous hook)
108
+ hook_input = PreToolInput(
109
+ tool_call=tool_call.model_copy(update={"arguments": current_arguments}),
110
+ )
111
+
112
+ result: PreToolOutput = await hook.execute(hook_input)
113
+
114
+ if result.decision == "deny":
115
+ return PreToolOutput(
116
+ arguments=current_arguments,
117
+ decision="deny",
118
+ reason=result.reason,
119
+ )
120
+
121
+ elif result.decision == "ask":
122
+ # Check if already confirmed/declined in context
123
+ for conf in context.tool_confirmations:
124
+ if conf.get("confirmation_id") == confirmation_id:
125
+ if conf.get("confirmed"):
126
+ # Approved → convert to "pass" and continue to next hook
127
+ result = PreToolOutput(arguments=current_arguments, decision="pass")
128
+ break
129
+ else:
130
+ # Declined → convert to "deny" and stop immediately
131
+ return PreToolOutput(
132
+ arguments=current_arguments,
133
+ decision="deny",
134
+ reason=result.reason or "Tool execution declined by user",
135
+ )
136
+ else:
137
+ # Not in context → return "ask" with full ConfirmationRequest
138
+ return PreToolOutput(
139
+ arguments=current_arguments,
140
+ decision="ask",
141
+ reason=result.reason,
142
+ confirmation_request=ConfirmationRequest(
143
+ confirmation_id=confirmation_id,
144
+ tool_name=tool_call.name,
145
+ tool_description=result.reason or "Hook requires user confirmation",
146
+ arguments=current_arguments,
147
+ ),
148
+ )
149
+
150
+ # Chain arguments for next hook
151
+ current_arguments = result.arguments
152
+
153
+ # All hooks passed
154
+ return PreToolOutput(arguments=current_arguments, decision="pass")
155
+
156
+ async def execute_post_tool(
157
+ self,
158
+ tool_call: ToolCall,
159
+ output: Any, # noqa: ANN401
160
+ error: Exception | None,
161
+ ) -> PostToolOutput:
162
+ """
163
+ Execute post-tool hooks with proper output chaining.
164
+
165
+ Each hook sees the modified output from the previous hook.
166
+
167
+ Args:
168
+ tool_call: The tool call that was executed
169
+ output: The tool output
170
+ error: Any error that occurred
171
+
172
+ Returns:
173
+ PostToolOutput with final output
174
+ """
175
+ hooks = self.get_hooks(EventType.POST_TOOL, tool_call.name)
176
+
177
+ # Start with original output
178
+ current_output = output
179
+
180
+ for hook in hooks:
181
+ # Create input with current state (chained from previous hook)
182
+ hook_input = PostToolInput(
183
+ tool_call=tool_call,
184
+ output=current_output,
185
+ error=error,
186
+ )
187
+
188
+ result: PostToolOutput = await hook.execute(hook_input)
189
+
190
+ # Chain output for next hook
191
+ current_output = result.output
192
+
193
+ # Return final chained result
194
+ return PostToolOutput(output=current_output)
@@ -0,0 +1,141 @@
1
+ """
2
+ Type definitions for the hooks system.
3
+
4
+ This module contains all type definitions including EventType, callback types,
5
+ input types, and output types for the hooks system.
6
+ """
7
+
8
+ from collections.abc import Awaitable, Callable
9
+ from enum import Enum
10
+ from typing import Any, Literal, TypeAlias
11
+
12
+ from pydantic import BaseModel, Field, model_validator
13
+
14
+ from ragbits.agents.confirmation import ConfirmationRequest
15
+ from ragbits.core.llms.base import ToolCall
16
+
17
+
18
+ class EventType(str, Enum):
19
+ """
20
+ Types of events that can be hooked.
21
+
22
+ Attributes:
23
+ PRE_TOOL: Triggered before a tool is invoked
24
+ POST_TOOL: Triggered after a tool completes
25
+ """
26
+
27
+ PRE_TOOL = "pre_tool"
28
+ POST_TOOL = "post_tool"
29
+
30
+
31
+ class HookEventIO(BaseModel):
32
+ """
33
+ Base class for hook inputs and outputs.
34
+
35
+ Contains the common event_type attribute shared by all hook events.
36
+
37
+ Attributes:
38
+ event_type: The type of event
39
+ """
40
+
41
+ model_config = {"arbitrary_types_allowed": True}
42
+
43
+ event_type: EventType
44
+
45
+
46
+ class PreToolInput(HookEventIO):
47
+ """
48
+ Input passed to pre-tool hook callbacks.
49
+
50
+ This is provided before a tool is invoked, allowing hooks to:
51
+ - Inspect the tool call
52
+ - Modify tool arguments
53
+ - Deny execution
54
+
55
+ Attributes:
56
+ event_type: Always EventType.PRE_TOOL (unchangeable)
57
+ tool_call: The complete tool call (contains name, arguments, id, type)
58
+ """
59
+
60
+ event_type: Literal[EventType.PRE_TOOL] = Field(default=EventType.PRE_TOOL, frozen=True)
61
+ tool_call: ToolCall
62
+
63
+
64
+ class PostToolInput(HookEventIO):
65
+ """
66
+ Input passed to post-tool hook callbacks.
67
+
68
+ This is provided after a tool completes, allowing hooks to:
69
+ - Inspect the tool result
70
+ - Modify tool output
71
+ - Handle errors
72
+
73
+ Attributes:
74
+ event_type: Always EventType.POST_TOOL (unchangeable)
75
+ tool_call: The original tool call
76
+ output: The result returned by the tool (None if error occurred)
77
+ error: Any error that occurred during execution (None if successful)
78
+ """
79
+
80
+ event_type: Literal[EventType.POST_TOOL] = Field(default=EventType.POST_TOOL, frozen=True)
81
+ tool_call: ToolCall
82
+ output: Any = None
83
+ error: Exception | None = None
84
+
85
+
86
+ class PreToolOutput(HookEventIO):
87
+ """
88
+ Output returned by pre-tool hook callbacks.
89
+
90
+ This allows hooks to control tool execution. The output always contains
91
+ arguments (either original or modified).
92
+
93
+ Attributes:
94
+ event_type: Always EventType.PRE_TOOL (unchangeable)
95
+ arguments: Tool arguments to use (original or modified) - always present
96
+ decision: The decision on tool execution ("pass", "ask", "deny")
97
+ reason: Explanation for ask/deny decisions (required for "ask" and "deny", can be None for "pass")
98
+ confirmation_request: Full confirmation request when decision is "ask" (set by HookManager)
99
+ """
100
+
101
+ event_type: Literal[EventType.PRE_TOOL] = Field(default=EventType.PRE_TOOL, frozen=True) # type: ignore[assignment]
102
+ arguments: dict[str, Any]
103
+ decision: Literal["pass", "ask", "deny"] = "pass"
104
+ reason: str | None = None
105
+ confirmation_request: ConfirmationRequest | None = None
106
+
107
+ @model_validator(mode="after")
108
+ def validate_reason(self) -> "PreToolOutput":
109
+ """Validate that reason is provided for ask and deny decisions."""
110
+ if self.decision in ("ask", "deny") and not self.reason:
111
+ raise ValueError(f"reason is required when decision='{self.decision}'")
112
+ return self
113
+
114
+
115
+ class PostToolOutput(HookEventIO):
116
+ """
117
+ Output returned by post-tool hook callbacks.
118
+
119
+ The output always contains the tool output (either original or modified).
120
+
121
+ Attributes:
122
+ event_type: Always EventType.POST_TOOL (unchangeable)
123
+ output: Tool output to use (original or modified) - always present
124
+
125
+ Example:
126
+ ```python
127
+ # Pass through unchanged
128
+ return PostToolOutput(output=input.output)
129
+
130
+ # Modify output
131
+ return PostToolOutput(output={"filtered": data})
132
+ ```
133
+ """
134
+
135
+ event_type: Literal[EventType.POST_TOOL] = Field(default=EventType.POST_TOOL, frozen=True) # type: ignore[assignment]
136
+ output: Any
137
+
138
+
139
+ # Type aliases for hook callbacks
140
+ PreToolHookCallback: TypeAlias = Callable[["PreToolInput"], Awaitable["PreToolOutput"]]
141
+ PostToolHookCallback: TypeAlias = Callable[["PostToolInput"], Awaitable["PostToolOutput"]]
ragbits/agents/tool.py CHANGED
@@ -1,11 +1,10 @@
1
1
  from collections.abc import Callable
2
2
  from contextlib import suppress
3
3
  from dataclasses import dataclass
4
- from functools import wraps
5
- from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
4
+ from typing import TYPE_CHECKING, Any, Literal, cast
6
5
 
7
6
  from pydantic import BaseModel
8
- from typing_extensions import ParamSpec, Self
7
+ from typing_extensions import Self
9
8
 
10
9
  from ragbits.core.llms.base import LLMClientOptionsT
11
10
  from ragbits.core.prompt.prompt import PromptInputT, PromptOutputT
@@ -18,45 +17,18 @@ if TYPE_CHECKING:
18
17
  with suppress(ImportError):
19
18
  from pydantic_ai import Tool as PydanticAITool
20
19
 
21
- P = ParamSpec("P")
22
- T = TypeVar("T")
23
20
 
24
-
25
- def requires_confirmation(func: Callable[P, T]) -> Callable[P, T]:
21
+ @dataclass
22
+ class ToolReturn:
26
23
  """
27
- Decorator to mark a tool function as requiring user confirmation before execution.
28
-
29
- When a function decorated with @requires_confirmation is used as a tool in an Agent,
30
- the agent will request user confirmation before executing the tool.
31
-
32
- Example:
33
- ```python
34
- @requires_confirmation
35
- def delete_file(filepath: str) -> str:
36
- '''Delete a file from the system.'''
37
- # Implementation
38
- return "File deleted"
39
-
40
-
41
- agent = Agent(llm=llm, tools=[delete_file])
42
- # The agent will automatically mark delete_file as requiring confirmation
43
- ```
44
-
45
- Args:
46
- func: The function to mark as requiring confirmation
47
-
48
- Returns:
49
- The same function with a _requires_confirmation attribute set to True
24
+ Represents an object returned from the tool. If a tool wants to return a value with some content hidden
25
+ from LLM, it needs to return an object of this class directly.
50
26
  """
51
27
 
52
- @wraps(func)
53
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
54
- return func(*args, **kwargs)
55
-
56
- # Mark the function as requiring confirmation
57
- wrapper._requires_confirmation = True # type: ignore[attr-defined]
58
-
59
- return wrapper
28
+ value: Any
29
+ "Value passed directly to LLM as a result of the tool"
30
+ metadata: Any = None
31
+ "Metadata not passed to the LLM, but which can be used in the application later on"
60
32
 
61
33
 
62
34
  @dataclass
@@ -72,7 +44,9 @@ class ToolCallResult:
72
44
  arguments: dict[str, Any]
73
45
  """Dictionary containing the arguments passed to the tool"""
74
46
  result: Any
75
- """The output from the tool call."""
47
+ """The output from the tool call passed to the LLM"""
48
+ metadata: Any = None
49
+ """Metadata returned from a tool that is not meant to be seen by the LLM"""
76
50
 
77
51
 
78
52
  @dataclass
@@ -92,35 +66,26 @@ class Tool:
92
66
  context_var_name: str | None = None
93
67
  """The name of the context variable that this tool accepts."""
94
68
  id: str | None = None
95
- requires_confirmation: bool = False
96
- """Whether this tool requires user confirmation before execution."""
97
69
 
98
70
  @classmethod
99
- def from_callable(cls, callable: Callable, requires_confirmation: bool = False) -> Self:
71
+ def from_callable(cls, callable: Callable) -> Self:
100
72
  """
101
73
  Create a Tool instance from a callable function.
102
74
 
103
75
  Args:
104
76
  callable: The function to convert into a Tool
105
- requires_confirmation: Whether this tool requires user confirmation before execution.
106
- If not provided, checks if the callable has been decorated with @requires_confirmation.
107
77
 
108
78
  Returns:
109
79
  A new Tool instance representing the callable function.
110
80
  """
111
81
  schema = convert_function_to_function_schema(callable)
112
82
 
113
- # Check if the callable has been decorated with @requires_confirmation
114
- # Priority: explicit parameter > decorator attribute
115
- needs_confirmation = requires_confirmation or getattr(callable, "_requires_confirmation", False)
116
-
117
83
  return cls(
118
84
  name=schema["function"]["name"],
119
85
  description=schema["function"]["description"],
120
86
  parameters=schema["function"]["parameters"],
121
87
  on_tool_call=callable,
122
88
  context_var_name=get_context_variable_name(callable),
123
- requires_confirmation=needs_confirmation,
124
89
  )
125
90
 
126
91
  def to_function_schema(self) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragbits-agents
3
- Version: 1.4.0.dev202512050236
3
+ Version: 1.4.0.dev202601310254
4
4
  Summary: Building blocks for rapid development of GenAI applications
5
5
  Project-URL: Homepage, https://github.com/deepsense-ai/ragbits
6
6
  Project-URL: Bug Reports, https://github.com/deepsense-ai/ragbits/issues
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Python: >=3.10
25
- Requires-Dist: ragbits-core==1.4.0.dev202512050236
25
+ Requires-Dist: ragbits-core==1.4.0.dev202601310254
26
26
  Provides-Extra: a2a
27
27
  Requires-Dist: a2a-sdk<1.0.0,>=0.2.9; extra == 'a2a'
28
28
  Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == 'a2a'
@@ -1,13 +1,18 @@
1
- ragbits/agents/__init__.py,sha256=SuM-RMiS_EkMsOgVXwhNtfZKSoKiA5B4QmaaOYWw7a4,996
2
- ragbits/agents/_main.py,sha256=nW2CL0VSaYrLZdMJPJ-s7Hz6IbpK_CXCvyjNHluL1d0,51823
1
+ ragbits/agents/__init__.py,sha256=rKqDbbppy70HmDN6AtC3eXzyVWTTagkRpLvPjWulGuY,1040
2
+ ragbits/agents/_main.py,sha256=bxMBogDl1dWxfGZk3cSztogj8gK8VoYo38jfoMzsBXU,52013
3
3
  ragbits/agents/cli.py,sha256=xUS7k8IAn0479n165i4YVFKo4Jx0M2iCpaMILZi9xt8,17649
4
4
  ragbits/agents/confirmation.py,sha256=cwdd1feSSobxa7gxBvEZcL9e3tcLdc8CyfvvQwaHF1Y,619
5
5
  ragbits/agents/exceptions.py,sha256=TiompKlP1QRD4TZ7hpp52dyZqg0rjoq1t5QUTxFZud8,3552
6
6
  ragbits/agents/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- ragbits/agents/tool.py,sha256=86a1ilbErXLFLgIQeLu0HBXUy5yUvkL91XXaYqgkZPE,7829
7
+ ragbits/agents/tool.py,sha256=c7TcexLMZSzWGS3nWKp5BX0zAipdkVAuRyvjCL220EE,6501
8
8
  ragbits/agents/types.py,sha256=_dzhn4HzrWuSK1dNVZEpznWTrxKMnKuTJfdUCwJWKuc,869
9
9
  ragbits/agents/a2a/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  ragbits/agents/a2a/server.py,sha256=4a-cq87OeMoVOjEM_PbwTSHhPTpJv8r_xOVaZAzFIiI,3031
11
+ ragbits/agents/hooks/__init__.py,sha256=xvWTFLkbM6aRLEz-Ypw9MMDOKVIJqhZzN5XE0jEvy3o,1984
12
+ ragbits/agents/hooks/base.py,sha256=Rl_SUgnI-e8Rgrg7TAlxDsdC2nqtRAOIGpIsPV3Y_Sk,3137
13
+ ragbits/agents/hooks/confirmation.py,sha256=ykSRR2sF8lbxG4pJN5amtT6qstnDoTbrYWZaafoSa9Y,1648
14
+ ragbits/agents/hooks/manager.py,sha256=ssQjICFmmstQhfVY3Vzhr5mj8K3npGVZ7Q5HL-dj6RE,6910
15
+ ragbits/agents/hooks/types.py,sha256=XIJZIVtjgrBGFh59T8ljkfj3qF9fzY0tqC_Iv59zLak,4407
11
16
  ragbits/agents/mcp/__init__.py,sha256=7icbSe4FCBMgPADNNoeQBKiLY8J6gIlqFS77gFGr6Ks,405
12
17
  ragbits/agents/mcp/server.py,sha256=slob6ns-sbLmG-bQAbwz-23MWsaAENeKhE2eNdoI3Vs,16397
13
18
  ragbits/agents/mcp/utils.py,sha256=o_UXS-olS1uRdfZHTRJdG697Xmq3q77twfcHzmyRTY0,1394
@@ -20,6 +25,6 @@ ragbits/agents/tools/memory.py,sha256=IrRGpZvdylNbn_Z7FFFwun9Rx3OSQimjVynSt3WgUo
20
25
  ragbits/agents/tools/openai.py,sha256=SpoB6my_T6LwfHjrP7ivQL6H4KvwInVampXq-6nGAHE,4955
21
26
  ragbits/agents/tools/todo.py,sha256=R86_Hu0HIl5Ujp8B2ctOo1iSLAHmfrzyu6jnyCLs4uc,18576
22
27
  ragbits/agents/tools/types.py,sha256=6yNG7IjG476muiCIcXKRgDJSemCeO2ZgycpXLyfu8jc,441
23
- ragbits_agents-1.4.0.dev202512050236.dist-info/METADATA,sha256=SiT1brR1hYVL-AIf9RUVK-vhpuER3ENA0imopbiBxhk,2273
24
- ragbits_agents-1.4.0.dev202512050236.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
- ragbits_agents-1.4.0.dev202512050236.dist-info/RECORD,,
28
+ ragbits_agents-1.4.0.dev202601310254.dist-info/METADATA,sha256=O2-1QB13Z7km26KnXDhIGOpy4vjU5AaRAYyPYwf4Uwg,2273
29
+ ragbits_agents-1.4.0.dev202601310254.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
30
+ ragbits_agents-1.4.0.dev202601310254.dist-info/RECORD,,