ragbits-agents 1.4.0.dev202601300258__tar.gz → 1.4.0.dev202602030301__tar.gz

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.
Files changed (52) hide show
  1. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/CHANGELOG.md +3 -2
  2. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/PKG-INFO +2 -2
  3. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/pyproject.toml +2 -2
  4. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/__init__.py +8 -2
  5. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/_main.py +66 -62
  6. ragbits_agents-1.4.0.dev202602030301/src/ragbits/agents/hooks/__init__.py +77 -0
  7. ragbits_agents-1.4.0.dev202602030301/src/ragbits/agents/hooks/base.py +94 -0
  8. ragbits_agents-1.4.0.dev202602030301/src/ragbits/agents/hooks/confirmation.py +51 -0
  9. ragbits_agents-1.4.0.dev202602030301/src/ragbits/agents/hooks/manager.py +195 -0
  10. ragbits_agents-1.4.0.dev202602030301/src/ragbits/agents/hooks/types.py +142 -0
  11. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/tool.py +14 -49
  12. ragbits_agents-1.4.0.dev202602030301/tests/__init__.py +0 -0
  13. ragbits_agents-1.4.0.dev202602030301/tests/unit/__init__.py +0 -0
  14. ragbits_agents-1.4.0.dev202602030301/tests/unit/conftest.py +73 -0
  15. ragbits_agents-1.4.0.dev202602030301/tests/unit/hooks/__init__.py +0 -0
  16. ragbits_agents-1.4.0.dev202602030301/tests/unit/hooks/conftest.py +10 -0
  17. ragbits_agents-1.4.0.dev202602030301/tests/unit/hooks/test_base.py +35 -0
  18. ragbits_agents-1.4.0.dev202602030301/tests/unit/hooks/test_confirmation.py +34 -0
  19. ragbits_agents-1.4.0.dev202602030301/tests/unit/hooks/test_manager.py +184 -0
  20. ragbits_agents-1.4.0.dev202602030301/tests/unit/hooks/test_types.py +96 -0
  21. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/test_agent.py +122 -0
  22. ragbits_agents-1.4.0.dev202601300258/tests/unit/test_confirmation.py +0 -535
  23. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/.gitignore +0 -0
  24. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/README.md +0 -0
  25. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/a2a/__init__.py +0 -0
  26. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/a2a/server.py +0 -0
  27. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/cli.py +0 -0
  28. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/confirmation.py +0 -0
  29. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/exceptions.py +0 -0
  30. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/mcp/__init__.py +0 -0
  31. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/mcp/server.py +0 -0
  32. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/mcp/utils.py +0 -0
  33. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/post_processors/__init__.py +0 -0
  34. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/post_processors/base.py +0 -0
  35. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/post_processors/exceptions.py +0 -0
  36. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/post_processors/supervisor.py +0 -0
  37. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/py.typed +0 -0
  38. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/tools/__init__.py +0 -0
  39. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/tools/memory.py +0 -0
  40. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/tools/openai.py +0 -0
  41. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/tools/todo.py +0 -0
  42. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/tools/types.py +0 -0
  43. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/src/ragbits/agents/types.py +0 -0
  44. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/mcp/helpers.py +0 -0
  45. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/mcp/test_caching.py +0 -0
  46. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/mcp/test_connect_disconnect.py +0 -0
  47. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/mcp/test_exceptions.py +0 -0
  48. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/mcp/test_mcp_utils.py +0 -0
  49. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/post_processors/test_base_post_processors.py +0 -0
  50. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/post_processors/test_supervisor.py +0 -0
  51. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/tools/test_memory.py +0 -0
  52. {ragbits_agents-1.4.0.dev202601300258 → ragbits_agents-1.4.0.dev202602030301}/tests/unit/tools/test_openai.py +0 -0
@@ -6,7 +6,6 @@
6
6
  - Add Support for Thinking in agents (#837)
7
7
  - Add parallel tool calling support to agents for concurrent tool execution (#836)
8
8
  - TodoOrchestrator & Todo list for agent (#823)
9
- - Support wrapping downstream agents as tools (#818)
10
9
  - Add syntax sugar allowing easier Agents definition (#820)
11
10
  - Add post-processors (#821)
12
11
  - Support streaming from downstream agents (#812)
@@ -17,7 +16,9 @@
17
16
  - Add supervisor post-processor (#830)
18
17
  - Add support for todo lists generated by agents with examples (#827)
19
18
  - Add long-term semantic memory tools for agents (#839)
20
- - Add support for confirmation requests in agents (#853)
19
+ - Add support for confirmation requests in agents (#853) (#914)
20
+ - Add hooks system (pre- and post-tool) for lifecycle event interception (#914)
21
+ - Add ToolReturn allowing the control which part of the tool output we pass to LLM (#920)
21
22
 
22
23
  ## 1.3.0 (2025-09-11)
23
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragbits-agents
3
- Version: 1.4.0.dev202601300258
3
+ Version: 1.4.0.dev202602030301
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.dev202601300258
25
+ Requires-Dist: ragbits-core==1.4.0.dev202602030301
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "ragbits-agents"
3
- version = "1.4.0.dev202601300258"
3
+ version = "1.4.0.dev202602030301"
4
4
  description = "Building blocks for rapid development of GenAI applications"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -31,7 +31,7 @@ classifiers = [
31
31
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
32
32
  "Topic :: Software Development :: Libraries :: Python Modules",
33
33
  ]
34
- dependencies = ["ragbits-core==1.4.0.dev202601300258"]
34
+ dependencies = ["ragbits-core==1.4.0.dev202602030301"]
35
35
 
36
36
  [project.optional-dependencies]
37
37
  a2a = [
@@ -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
  ]
@@ -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)
@@ -416,6 +417,7 @@ class Agent(
416
417
  self.mcp_servers = mcp_servers or []
417
418
  self.history = history or []
418
419
  self.keep_history = keep_history
420
+ self.hook_manager = HookManager(hooks)
419
421
 
420
422
  if getattr(self, "system_prompt", None) and not getattr(self, "input_type", None):
421
423
  raise ValueError(
@@ -536,7 +538,9 @@ class Agent(
536
538
  ):
537
539
  if isinstance(result, ToolCallResult):
538
540
  tool_calls.append(result)
539
- 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
+ )
540
544
 
541
545
  turn_count += 1
542
546
  else:
@@ -762,7 +766,9 @@ class Agent(
762
766
  elif isinstance(result, ToolCallResult):
763
767
  # Add ALL tool results to history (including pending confirmations)
764
768
  # This allows the agent to see them in the next turn
765
- 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
+ )
766
772
  returned_tool_call = True
767
773
 
768
774
  # If we have pending confirmations, prepare for text-only summary generation
@@ -958,54 +964,37 @@ class Agent(
958
964
 
959
965
  tool = tools_mapping[tool_call.name]
960
966
 
961
- # Check if tool requires confirmation
962
- if tool.requires_confirmation:
963
- # Check if this tool has been confirmed in the context
964
- confirmed_tools = context.confirmed_tools or []
965
-
966
- # Generate a stable confirmation ID based on tool name and arguments
967
- confirmation_id = hashlib.sha256(
968
- f"{tool_call.name}:{json.dumps(tool_call.arguments, sort_keys=True)}".encode()
969
- ).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
+ )
970
972
 
971
- # Check if this specific tool call has been confirmed or declined
972
- is_confirmed = any(
973
- 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",
974
980
  )
975
- is_declined = any(
976
- 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",
977
991
  )
992
+ return
978
993
 
979
- if is_declined:
980
- # Tool was explicitly declined - skip execution entirely
981
- yield ToolCallResult(
982
- id=tool_call.id,
983
- name=tool_call.name,
984
- arguments=tool_call.arguments,
985
- result="❌ Action declined by user",
986
- )
987
- return
988
-
989
- if not is_confirmed:
990
- # Tool not confirmed yet - create and yield confirmation request
991
- request = ConfirmationRequest(
992
- confirmation_id=confirmation_id,
993
- tool_name=tool_call.name,
994
- tool_description=tool.description or "",
995
- arguments=tool_call.arguments,
996
- )
997
-
998
- # Yield confirmation request (will be streamed to frontend)
999
- yield request
994
+ # Always update arguments (chained from hooks)
995
+ tool_call.arguments = pre_tool_result.arguments
1000
996
 
1001
- # Yield a pending result and exit without executing
1002
- yield ToolCallResult(
1003
- id=tool_call.id,
1004
- name=tool_call.name,
1005
- arguments=tool_call.arguments,
1006
- result="⏳ Awaiting user confirmation",
1007
- )
1008
- return
997
+ tool_error: Exception | None = None
1009
998
 
1010
999
  with trace(agent_id=self.id, tool_name=tool_call.name, tool_arguments=tool_call.arguments) as outputs:
1011
1000
  try:
@@ -1019,35 +1008,50 @@ class Agent(
1019
1008
  else asyncio.to_thread(tool.on_tool_call, **call_args)
1020
1009
  )
1021
1010
 
1022
- if isinstance(tool_output, AgentResultStreaming):
1011
+ if isinstance(tool_output, ToolReturn):
1012
+ tool_return = tool_output
1013
+ elif isinstance(tool_output, AgentResultStreaming):
1023
1014
  async for downstream_item in tool_output:
1024
1015
  if context.stream_downstream_events:
1025
1016
  yield DownstreamAgentResult(agent_id=tool.id, item=downstream_item)
1026
-
1027
- tool_output = {
1028
- "content": tool_output.content,
1017
+ metadata = {
1029
1018
  "metadata": tool_output.metadata,
1030
1019
  "tool_calls": tool_output.tool_calls,
1031
1020
  "usage": tool_output.usage,
1032
1021
  }
1022
+ tool_return = ToolReturn(value=tool_output.content, metadata=metadata)
1023
+ else:
1024
+ tool_return = ToolReturn(value=tool_output, metadata=None)
1033
1025
 
1034
1026
  outputs.result = {
1035
- "tool_output": tool_output,
1027
+ "tool_output": tool_return.value,
1036
1028
  "tool_call_id": tool_call.id,
1037
1029
  }
1038
1030
 
1039
1031
  except Exception as e:
1032
+ tool_error = e
1040
1033
  outputs.result = {
1041
1034
  "error": str(e),
1042
1035
  "tool_call_id": tool_call.id,
1043
1036
  }
1044
- raise AgentToolExecutionError(tool_call.name, e) from e
1037
+
1038
+ # Execute POST_TOOL hooks with chaining
1039
+ post_tool_output = await self.hook_manager.execute_post_tool(
1040
+ tool_call=tool_call,
1041
+ tool_return=tool_return,
1042
+ error=tool_error,
1043
+ )
1044
+
1045
+ # Raise error after hooks have been executed
1046
+ if tool_error:
1047
+ raise AgentToolExecutionError(tool_call.name, tool_error) from tool_error
1045
1048
 
1046
1049
  yield ToolCallResult(
1047
1050
  id=tool_call.id,
1048
1051
  name=tool_call.name,
1049
1052
  arguments=tool_call.arguments,
1050
- result=tool_output,
1053
+ result=post_tool_output.tool_return.value if post_tool_output.tool_return else None,
1054
+ metadata=post_tool_output.tool_return.metadata if post_tool_output.tool_return else None,
1051
1055
  )
1052
1056
 
1053
1057
  @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
+ )