mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-03 00:02:36 +00:00
Lorenze/feat hooks (#3902)
* feat: implement LLM call hooks and enhance agent execution context - Introduced LLM call hooks to allow modification of messages and responses during LLM interactions. - Added support for before and after hooks in the CrewAgentExecutor, enabling dynamic adjustments to the execution flow. - Created LLMCallHookContext for comprehensive access to the executor state, facilitating in-place modifications. - Added validation for hook callables to ensure proper functionality. - Enhanced tests for LLM hooks and tool hooks to verify their behavior and error handling capabilities. - Updated LiteAgent and CrewAgentExecutor to accommodate the new crew context in their execution processes. * feat: implement LLM call hooks and enhance agent execution context - Introduced LLM call hooks to allow modification of messages and responses during LLM interactions. - Added support for before and after hooks in the CrewAgentExecutor, enabling dynamic adjustments to the execution flow. - Created LLMCallHookContext for comprehensive access to the executor state, facilitating in-place modifications. - Added validation for hook callables to ensure proper functionality. - Enhanced tests for LLM hooks and tool hooks to verify their behavior and error handling capabilities. - Updated LiteAgent and CrewAgentExecutor to accommodate the new crew context in their execution processes. * fix verbose * feat: introduce crew-scoped hook decorators and refactor hook registration - Added decorators for before and after LLM and tool calls to enhance flexibility in modifying execution behavior. - Implemented a centralized hook registration mechanism within CrewBase to automatically register crew-scoped hooks. - Removed the obsolete base.py file as its functionality has been integrated into the new decorators and registration system. - Enhanced tests for the new hook decorators to ensure proper registration and execution flow. - Updated existing hook handling to accommodate the new decorator-based approach, improving code organization and maintainability. * feat: enhance hook management with clear and unregister functions - Introduced functions to unregister specific before and after hooks for both LLM and tool calls, improving flexibility in hook management. - Added clear functions to remove all registered hooks of each type, facilitating easier state management and cleanup. - Implemented a convenience function to clear all global hooks in one call, streamlining the process for testing and execution context resets. - Enhanced tests to verify the functionality of unregistering and clearing hooks, ensuring robust behavior in various scenarios. * refactor: enhance hook type management for LLM and tool hooks - Updated hook type definitions to use generic protocols for better type safety and flexibility. - Replaced Callable type annotations with specific BeforeLLMCallHookType and AfterLLMCallHookType for clarity. - Improved the registration and retrieval functions for before and after hooks to align with the new type definitions. - Enhanced the setup functions to handle hook execution results, allowing for blocking of LLM calls based on hook logic. - Updated related tests to ensure proper functionality and type adherence across the hook management system. * feat: add execution and tool hooks documentation - Introduced new documentation for execution hooks, LLM call hooks, and tool call hooks to provide comprehensive guidance on their usage and implementation in CrewAI. - Updated existing documentation to include references to the new hooks, enhancing the learning resources available for users. - Ensured consistency across multiple languages (English, Portuguese, Korean) for the new documentation, improving accessibility for a wider audience. - Added examples and troubleshooting sections to assist users in effectively utilizing hooks for agent operations. --------- Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
This commit is contained in:
@@ -23,6 +23,10 @@ from crewai.events.types.logging_events import (
|
||||
AgentLogsExecutionEvent,
|
||||
AgentLogsStartedEvent,
|
||||
)
|
||||
from crewai.hooks.llm_hooks import (
|
||||
get_after_llm_call_hooks,
|
||||
get_before_llm_call_hooks,
|
||||
)
|
||||
from crewai.utilities.agent_utils import (
|
||||
enforce_rpm_limit,
|
||||
format_message_for_llm,
|
||||
@@ -38,10 +42,6 @@ from crewai.utilities.agent_utils import (
|
||||
)
|
||||
from crewai.utilities.constants import TRAINING_DATA_FILE
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
from crewai.utilities.llm_call_hooks import (
|
||||
get_after_llm_call_hooks,
|
||||
get_before_llm_call_hooks,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||
@@ -263,6 +263,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
task=self.task,
|
||||
agent=self.agent,
|
||||
function_calling_llm=self.function_calling_llm,
|
||||
crew=self.crew,
|
||||
)
|
||||
formatted_answer = self._handle_agent_action(
|
||||
formatted_answer, tool_result
|
||||
|
||||
108
lib/crewai/src/crewai/hooks/__init__.py
Normal file
108
lib/crewai/src/crewai/hooks/__init__.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from crewai.hooks.decorators import (
|
||||
after_llm_call,
|
||||
after_tool_call,
|
||||
before_llm_call,
|
||||
before_tool_call,
|
||||
)
|
||||
from crewai.hooks.llm_hooks import (
|
||||
LLMCallHookContext,
|
||||
clear_after_llm_call_hooks,
|
||||
clear_all_llm_call_hooks,
|
||||
clear_before_llm_call_hooks,
|
||||
get_after_llm_call_hooks,
|
||||
get_before_llm_call_hooks,
|
||||
register_after_llm_call_hook,
|
||||
register_before_llm_call_hook,
|
||||
unregister_after_llm_call_hook,
|
||||
unregister_before_llm_call_hook,
|
||||
)
|
||||
from crewai.hooks.tool_hooks import (
|
||||
ToolCallHookContext,
|
||||
clear_after_tool_call_hooks,
|
||||
clear_all_tool_call_hooks,
|
||||
clear_before_tool_call_hooks,
|
||||
get_after_tool_call_hooks,
|
||||
get_before_tool_call_hooks,
|
||||
register_after_tool_call_hook,
|
||||
register_before_tool_call_hook,
|
||||
unregister_after_tool_call_hook,
|
||||
unregister_before_tool_call_hook,
|
||||
)
|
||||
|
||||
|
||||
def clear_all_global_hooks() -> dict[str, tuple[int, int]]:
|
||||
"""Clear all global hooks across all hook types (LLM and Tool).
|
||||
|
||||
This is a convenience function that clears all registered hooks in one call.
|
||||
Useful for testing, resetting state, or cleaning up between different
|
||||
execution contexts.
|
||||
|
||||
Returns:
|
||||
Dictionary with counts of cleared hooks:
|
||||
{
|
||||
"llm_hooks": (before_count, after_count),
|
||||
"tool_hooks": (before_count, after_count),
|
||||
"total": (total_before_count, total_after_count)
|
||||
}
|
||||
|
||||
Example:
|
||||
>>> # Register various hooks
|
||||
>>> register_before_llm_call_hook(llm_hook1)
|
||||
>>> register_after_llm_call_hook(llm_hook2)
|
||||
>>> register_before_tool_call_hook(tool_hook1)
|
||||
>>> register_after_tool_call_hook(tool_hook2)
|
||||
>>>
|
||||
>>> # Clear all hooks at once
|
||||
>>> result = clear_all_global_hooks()
|
||||
>>> print(result)
|
||||
{
|
||||
'llm_hooks': (1, 1),
|
||||
'tool_hooks': (1, 1),
|
||||
'total': (2, 2)
|
||||
}
|
||||
"""
|
||||
llm_counts = clear_all_llm_call_hooks()
|
||||
tool_counts = clear_all_tool_call_hooks()
|
||||
|
||||
return {
|
||||
"llm_hooks": llm_counts,
|
||||
"tool_hooks": tool_counts,
|
||||
"total": (llm_counts[0] + tool_counts[0], llm_counts[1] + tool_counts[1]),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Context classes
|
||||
"LLMCallHookContext",
|
||||
"ToolCallHookContext",
|
||||
# Decorators
|
||||
"after_llm_call",
|
||||
"after_tool_call",
|
||||
"before_llm_call",
|
||||
"before_tool_call",
|
||||
"clear_after_llm_call_hooks",
|
||||
"clear_after_tool_call_hooks",
|
||||
"clear_all_global_hooks",
|
||||
"clear_all_llm_call_hooks",
|
||||
"clear_all_tool_call_hooks",
|
||||
# Clear hooks
|
||||
"clear_before_llm_call_hooks",
|
||||
"clear_before_tool_call_hooks",
|
||||
"get_after_llm_call_hooks",
|
||||
"get_after_tool_call_hooks",
|
||||
# Get hooks
|
||||
"get_before_llm_call_hooks",
|
||||
"get_before_tool_call_hooks",
|
||||
"register_after_llm_call_hook",
|
||||
"register_after_tool_call_hook",
|
||||
# LLM Hook registration
|
||||
"register_before_llm_call_hook",
|
||||
# Tool Hook registration
|
||||
"register_before_tool_call_hook",
|
||||
"unregister_after_llm_call_hook",
|
||||
"unregister_after_tool_call_hook",
|
||||
"unregister_before_llm_call_hook",
|
||||
"unregister_before_tool_call_hook",
|
||||
]
|
||||
300
lib/crewai/src/crewai/hooks/decorators.py
Normal file
300
lib/crewai/src/crewai/hooks/decorators.py
Normal file
@@ -0,0 +1,300 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
from crewai.hooks.tool_hooks import ToolCallHookContext
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def _create_hook_decorator(
|
||||
hook_type: str,
|
||||
register_function: Callable[..., Any],
|
||||
marker_attribute: str,
|
||||
) -> Callable[..., Any]:
|
||||
"""Create a hook decorator with filtering support.
|
||||
|
||||
This factory function eliminates code duplication across the four hook decorators.
|
||||
|
||||
Args:
|
||||
hook_type: Type of hook ("llm" or "tool")
|
||||
register_function: Function to call for registration (e.g., register_before_llm_call_hook)
|
||||
marker_attribute: Attribute name to mark functions (e.g., "is_before_llm_call_hook")
|
||||
|
||||
Returns:
|
||||
A decorator function that supports filters and auto-registration
|
||||
"""
|
||||
|
||||
def decorator_factory(
|
||||
func: Callable[..., Any] | None = None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
agents: list[str] | None = None,
|
||||
) -> Callable[..., Any]:
|
||||
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
||||
setattr(f, marker_attribute, True)
|
||||
|
||||
sig = inspect.signature(f)
|
||||
params = list(sig.parameters.keys())
|
||||
is_method = len(params) >= 2 and params[0] == "self"
|
||||
|
||||
if tools:
|
||||
f._filter_tools = tools # type: ignore[attr-defined]
|
||||
if agents:
|
||||
f._filter_agents = agents # type: ignore[attr-defined]
|
||||
|
||||
if tools or agents:
|
||||
|
||||
@wraps(f)
|
||||
def filtered_hook(context: Any) -> Any:
|
||||
if tools and hasattr(context, "tool_name"):
|
||||
if context.tool_name not in tools:
|
||||
return None
|
||||
|
||||
if agents and hasattr(context, "agent"):
|
||||
if context.agent and context.agent.role not in agents:
|
||||
return None
|
||||
|
||||
return f(context)
|
||||
|
||||
if not is_method:
|
||||
register_function(filtered_hook)
|
||||
|
||||
return f
|
||||
|
||||
if not is_method:
|
||||
register_function(f)
|
||||
|
||||
return f
|
||||
|
||||
if func is None:
|
||||
return decorator
|
||||
return decorator(func)
|
||||
|
||||
return decorator_factory
|
||||
|
||||
|
||||
@overload
|
||||
def before_llm_call(
|
||||
func: Callable[[LLMCallHookContext], None],
|
||||
) -> Callable[[LLMCallHookContext], None]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def before_llm_call(
|
||||
*,
|
||||
agents: list[str] | None = None,
|
||||
) -> Callable[
|
||||
[Callable[[LLMCallHookContext], None]], Callable[[LLMCallHookContext], None]
|
||||
]: ...
|
||||
|
||||
|
||||
def before_llm_call(
|
||||
func: Callable[[LLMCallHookContext], None] | None = None,
|
||||
*,
|
||||
agents: list[str] | None = None,
|
||||
) -> (
|
||||
Callable[[LLMCallHookContext], None]
|
||||
| Callable[
|
||||
[Callable[[LLMCallHookContext], None]], Callable[[LLMCallHookContext], None]
|
||||
]
|
||||
):
|
||||
"""Decorator to register a function as a before_llm_call hook.
|
||||
|
||||
Example:
|
||||
Simple usage::
|
||||
|
||||
@before_llm_call
|
||||
def log_calls(context):
|
||||
print(f"LLM call by {context.agent.role}")
|
||||
|
||||
With agent filter::
|
||||
|
||||
@before_llm_call(agents=["Researcher", "Analyst"])
|
||||
def log_specific_agents(context):
|
||||
print(f"Filtered LLM call: {context.agent.role}")
|
||||
"""
|
||||
from crewai.hooks.llm_hooks import register_before_llm_call_hook
|
||||
|
||||
return _create_hook_decorator( # type: ignore[return-value]
|
||||
hook_type="llm",
|
||||
register_function=register_before_llm_call_hook,
|
||||
marker_attribute="is_before_llm_call_hook",
|
||||
)(func=func, agents=agents)
|
||||
|
||||
|
||||
@overload
|
||||
def after_llm_call(
|
||||
func: Callable[[LLMCallHookContext], str | None],
|
||||
) -> Callable[[LLMCallHookContext], str | None]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def after_llm_call(
|
||||
*,
|
||||
agents: list[str] | None = None,
|
||||
) -> Callable[
|
||||
[Callable[[LLMCallHookContext], str | None]],
|
||||
Callable[[LLMCallHookContext], str | None],
|
||||
]: ...
|
||||
|
||||
|
||||
def after_llm_call(
|
||||
func: Callable[[LLMCallHookContext], str | None] | None = None,
|
||||
*,
|
||||
agents: list[str] | None = None,
|
||||
) -> (
|
||||
Callable[[LLMCallHookContext], str | None]
|
||||
| Callable[
|
||||
[Callable[[LLMCallHookContext], str | None]],
|
||||
Callable[[LLMCallHookContext], str | None],
|
||||
]
|
||||
):
|
||||
"""Decorator to register a function as an after_llm_call hook.
|
||||
|
||||
Example:
|
||||
Simple usage::
|
||||
|
||||
@after_llm_call
|
||||
def sanitize(context):
|
||||
if "SECRET" in context.response:
|
||||
return context.response.replace("SECRET", "[REDACTED]")
|
||||
return None
|
||||
|
||||
With agent filter::
|
||||
|
||||
@after_llm_call(agents=["Researcher"])
|
||||
def log_researcher_responses(context):
|
||||
print(f"Response length: {len(context.response)}")
|
||||
return None
|
||||
"""
|
||||
from crewai.hooks.llm_hooks import register_after_llm_call_hook
|
||||
|
||||
return _create_hook_decorator( # type: ignore[return-value]
|
||||
hook_type="llm",
|
||||
register_function=register_after_llm_call_hook,
|
||||
marker_attribute="is_after_llm_call_hook",
|
||||
)(func=func, agents=agents)
|
||||
|
||||
|
||||
@overload
|
||||
def before_tool_call(
|
||||
func: Callable[[ToolCallHookContext], bool | None],
|
||||
) -> Callable[[ToolCallHookContext], bool | None]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def before_tool_call(
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
agents: list[str] | None = None,
|
||||
) -> Callable[
|
||||
[Callable[[ToolCallHookContext], bool | None]],
|
||||
Callable[[ToolCallHookContext], bool | None],
|
||||
]: ...
|
||||
|
||||
|
||||
def before_tool_call(
|
||||
func: Callable[[ToolCallHookContext], bool | None] | None = None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
agents: list[str] | None = None,
|
||||
) -> (
|
||||
Callable[[ToolCallHookContext], bool | None]
|
||||
| Callable[
|
||||
[Callable[[ToolCallHookContext], bool | None]],
|
||||
Callable[[ToolCallHookContext], bool | None],
|
||||
]
|
||||
):
|
||||
"""Decorator to register a function as a before_tool_call hook.
|
||||
|
||||
Example:
|
||||
Simple usage::
|
||||
|
||||
@before_tool_call
|
||||
def log_all_tools(context):
|
||||
print(f"Tool: {context.tool_name}")
|
||||
return None
|
||||
|
||||
With tool filter::
|
||||
|
||||
@before_tool_call(tools=["delete_file", "execute_code"])
|
||||
def approve_dangerous(context):
|
||||
response = context.request_human_input(prompt="Approve?")
|
||||
return None if response == "yes" else False
|
||||
|
||||
With combined filters::
|
||||
|
||||
@before_tool_call(tools=["write_file"], agents=["Developer"])
|
||||
def approve_dev_writes(context):
|
||||
return None # Only for Developer writing files
|
||||
"""
|
||||
from crewai.hooks.tool_hooks import register_before_tool_call_hook
|
||||
|
||||
return _create_hook_decorator( # type: ignore[return-value]
|
||||
hook_type="tool",
|
||||
register_function=register_before_tool_call_hook,
|
||||
marker_attribute="is_before_tool_call_hook",
|
||||
)(func=func, tools=tools, agents=agents)
|
||||
|
||||
|
||||
@overload
|
||||
def after_tool_call(
|
||||
func: Callable[[ToolCallHookContext], str | None],
|
||||
) -> Callable[[ToolCallHookContext], str | None]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def after_tool_call(
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
agents: list[str] | None = None,
|
||||
) -> Callable[
|
||||
[Callable[[ToolCallHookContext], str | None]],
|
||||
Callable[[ToolCallHookContext], str | None],
|
||||
]: ...
|
||||
|
||||
|
||||
def after_tool_call(
|
||||
func: Callable[[ToolCallHookContext], str | None] | None = None,
|
||||
*,
|
||||
tools: list[str] | None = None,
|
||||
agents: list[str] | None = None,
|
||||
) -> (
|
||||
Callable[[ToolCallHookContext], str | None]
|
||||
| Callable[
|
||||
[Callable[[ToolCallHookContext], str | None]],
|
||||
Callable[[ToolCallHookContext], str | None],
|
||||
]
|
||||
):
|
||||
"""Decorator to register a function as an after_tool_call hook.
|
||||
|
||||
Example:
|
||||
Simple usage::
|
||||
|
||||
@after_tool_call
|
||||
def log_results(context):
|
||||
print(f"Result: {len(context.tool_result)} chars")
|
||||
return None
|
||||
|
||||
With tool filter::
|
||||
|
||||
@after_tool_call(tools=["web_search", "ExaSearchTool"])
|
||||
def sanitize_search_results(context):
|
||||
if "SECRET" in context.tool_result:
|
||||
return context.tool_result.replace("SECRET", "[REDACTED]")
|
||||
return None
|
||||
"""
|
||||
from crewai.hooks.tool_hooks import register_after_tool_call_hook
|
||||
|
||||
return _create_hook_decorator( # type: ignore[return-value]
|
||||
hook_type="tool",
|
||||
register_function=register_after_tool_call_hook,
|
||||
marker_attribute="is_after_tool_call_hook",
|
||||
)(func=func, tools=tools, agents=agents)
|
||||
290
lib/crewai/src/crewai/hooks/llm_hooks.py
Normal file
290
lib/crewai/src/crewai/hooks/llm_hooks.py
Normal file
@@ -0,0 +1,290 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from crewai.events.event_listener import event_listener
|
||||
from crewai.hooks.types import AfterLLMCallHookType, BeforeLLMCallHookType
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
|
||||
|
||||
class LLMCallHookContext:
|
||||
"""Context object passed to LLM call hooks with full executor access.
|
||||
|
||||
Provides hooks with complete access to the executor state, allowing
|
||||
modification of messages, responses, and executor attributes.
|
||||
|
||||
Attributes:
|
||||
executor: Full reference to the CrewAgentExecutor instance
|
||||
messages: Direct reference to executor.messages (mutable list).
|
||||
Can be modified in both before_llm_call and after_llm_call hooks.
|
||||
Modifications in after_llm_call hooks persist to the next iteration,
|
||||
allowing hooks to modify conversation history for subsequent LLM calls.
|
||||
IMPORTANT: Modify messages in-place (e.g., append, extend, remove items).
|
||||
Do NOT replace the list (e.g., context.messages = []), as this will break
|
||||
the executor. Use context.messages.append() or context.messages.extend()
|
||||
instead of assignment.
|
||||
agent: Reference to the agent executing the task
|
||||
task: Reference to the task being executed
|
||||
crew: Reference to the crew instance
|
||||
llm: Reference to the LLM instance
|
||||
iterations: Current iteration count
|
||||
response: LLM response string (only set for after_llm_call hooks).
|
||||
Can be modified by returning a new string from after_llm_call hook.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
executor: CrewAgentExecutor,
|
||||
response: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize hook context with executor reference.
|
||||
|
||||
Args:
|
||||
executor: The CrewAgentExecutor instance
|
||||
response: Optional response string (for after_llm_call hooks)
|
||||
"""
|
||||
self.executor = executor
|
||||
self.messages = executor.messages
|
||||
self.agent = executor.agent
|
||||
self.task = executor.task
|
||||
self.crew = executor.crew
|
||||
self.llm = executor.llm
|
||||
self.iterations = executor.iterations
|
||||
self.response = response
|
||||
|
||||
def request_human_input(
|
||||
self,
|
||||
prompt: str,
|
||||
default_message: str = "Press Enter to continue, or provide feedback:",
|
||||
) -> str:
|
||||
"""Request human input during LLM hook execution.
|
||||
|
||||
This method pauses live console updates, displays a prompt to the user,
|
||||
waits for their input, and then resumes live updates. This is useful for
|
||||
approval gates, debugging, or getting human feedback during execution.
|
||||
|
||||
Args:
|
||||
prompt: Custom message to display to the user
|
||||
default_message: Message shown after the prompt
|
||||
|
||||
Returns:
|
||||
User's input as a string (empty string if just Enter pressed)
|
||||
|
||||
Example:
|
||||
>>> def approval_hook(context: LLMCallHookContext) -> None:
|
||||
... if context.iterations > 5:
|
||||
... response = context.request_human_input(
|
||||
... prompt="Allow this LLM call?",
|
||||
... default_message="Type 'no' to skip, or press Enter:",
|
||||
... )
|
||||
... if response.lower() == "no":
|
||||
... print("LLM call skipped by user")
|
||||
"""
|
||||
|
||||
printer = Printer()
|
||||
event_listener.formatter.pause_live_updates()
|
||||
|
||||
try:
|
||||
printer.print(content=f"\n{prompt}", color="bold_yellow")
|
||||
printer.print(content=default_message, color="cyan")
|
||||
response = input().strip()
|
||||
|
||||
if response:
|
||||
printer.print(content="\nProcessing your input...", color="cyan")
|
||||
|
||||
return response
|
||||
finally:
|
||||
event_listener.formatter.resume_live_updates()
|
||||
|
||||
|
||||
_before_llm_call_hooks: list[BeforeLLMCallHookType] = []
|
||||
_after_llm_call_hooks: list[AfterLLMCallHookType] = []
|
||||
|
||||
|
||||
def register_before_llm_call_hook(
|
||||
hook: BeforeLLMCallHookType,
|
||||
) -> None:
|
||||
"""Register a global before_llm_call hook.
|
||||
|
||||
Global hooks are added to all executors automatically.
|
||||
This is a convenience function for registering hooks that should
|
||||
apply to all LLM calls across all executors.
|
||||
|
||||
Args:
|
||||
hook: Function that receives LLMCallHookContext and can:
|
||||
- Modify context.messages directly (in-place)
|
||||
- Return False to block LLM execution
|
||||
- Return True or None to allow execution
|
||||
IMPORTANT: Modify messages in-place (append, extend, remove items).
|
||||
Do NOT replace the list (context.messages = []), as this will break execution.
|
||||
|
||||
Example:
|
||||
>>> def log_llm_calls(context: LLMCallHookContext) -> None:
|
||||
... print(f"LLM call by {context.agent.role}")
|
||||
... print(f"Messages: {len(context.messages)}")
|
||||
... return None # Allow execution
|
||||
>>>
|
||||
>>> register_before_llm_call_hook(log_llm_calls)
|
||||
>>>
|
||||
>>> def block_excessive_iterations(context: LLMCallHookContext) -> bool | None:
|
||||
... if context.iterations > 10:
|
||||
... print("Blocked: Too many iterations")
|
||||
... return False # Block execution
|
||||
... return None # Allow execution
|
||||
>>>
|
||||
>>> register_before_llm_call_hook(block_excessive_iterations)
|
||||
"""
|
||||
_before_llm_call_hooks.append(hook)
|
||||
|
||||
|
||||
def register_after_llm_call_hook(
|
||||
hook: AfterLLMCallHookType,
|
||||
) -> None:
|
||||
"""Register a global after_llm_call hook.
|
||||
|
||||
Global hooks are added to all executors automatically.
|
||||
This is a convenience function for registering hooks that should
|
||||
apply to all LLM calls across all executors.
|
||||
|
||||
Args:
|
||||
hook: Function that receives LLMCallHookContext and can modify:
|
||||
- The response: Return modified response string or None to keep original
|
||||
- The messages: Modify context.messages directly (mutable reference)
|
||||
Both modifications are supported and can be used together.
|
||||
IMPORTANT: Modify messages in-place (append, extend, remove items).
|
||||
Do NOT replace the list (context.messages = []), as this will break execution.
|
||||
|
||||
Example:
|
||||
>>> def sanitize_response(context: LLMCallHookContext) -> str | None:
|
||||
... if context.response and "SECRET" in context.response:
|
||||
... return context.response.replace("SECRET", "[REDACTED]")
|
||||
... return None
|
||||
>>>
|
||||
>>> register_after_llm_call_hook(sanitize_response)
|
||||
"""
|
||||
_after_llm_call_hooks.append(hook)
|
||||
|
||||
|
||||
def get_before_llm_call_hooks() -> list[BeforeLLMCallHookType]:
|
||||
"""Get all registered global before_llm_call hooks.
|
||||
|
||||
Returns:
|
||||
List of registered before hooks
|
||||
"""
|
||||
return _before_llm_call_hooks.copy()
|
||||
|
||||
|
||||
def get_after_llm_call_hooks() -> list[AfterLLMCallHookType]:
|
||||
"""Get all registered global after_llm_call hooks.
|
||||
|
||||
Returns:
|
||||
List of registered after hooks
|
||||
"""
|
||||
return _after_llm_call_hooks.copy()
|
||||
|
||||
|
||||
def unregister_before_llm_call_hook(
|
||||
hook: BeforeLLMCallHookType,
|
||||
) -> bool:
|
||||
"""Unregister a specific global before_llm_call hook.
|
||||
|
||||
Args:
|
||||
hook: The hook function to remove
|
||||
|
||||
Returns:
|
||||
True if the hook was found and removed, False otherwise
|
||||
|
||||
Example:
|
||||
>>> def my_hook(context: LLMCallHookContext) -> None:
|
||||
... print("Before LLM call")
|
||||
>>>
|
||||
>>> register_before_llm_call_hook(my_hook)
|
||||
>>> unregister_before_llm_call_hook(my_hook)
|
||||
True
|
||||
"""
|
||||
try:
|
||||
_before_llm_call_hooks.remove(hook)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def unregister_after_llm_call_hook(
|
||||
hook: AfterLLMCallHookType,
|
||||
) -> bool:
|
||||
"""Unregister a specific global after_llm_call hook.
|
||||
|
||||
Args:
|
||||
hook: The hook function to remove
|
||||
|
||||
Returns:
|
||||
True if the hook was found and removed, False otherwise
|
||||
|
||||
Example:
|
||||
>>> def my_hook(context: LLMCallHookContext) -> str | None:
|
||||
... return None
|
||||
>>>
|
||||
>>> register_after_llm_call_hook(my_hook)
|
||||
>>> unregister_after_llm_call_hook(my_hook)
|
||||
True
|
||||
"""
|
||||
try:
|
||||
_after_llm_call_hooks.remove(hook)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def clear_before_llm_call_hooks() -> int:
|
||||
"""Clear all registered global before_llm_call hooks.
|
||||
|
||||
Returns:
|
||||
Number of hooks that were cleared
|
||||
|
||||
Example:
|
||||
>>> register_before_llm_call_hook(hook1)
|
||||
>>> register_before_llm_call_hook(hook2)
|
||||
>>> clear_before_llm_call_hooks()
|
||||
2
|
||||
"""
|
||||
count = len(_before_llm_call_hooks)
|
||||
_before_llm_call_hooks.clear()
|
||||
return count
|
||||
|
||||
|
||||
def clear_after_llm_call_hooks() -> int:
|
||||
"""Clear all registered global after_llm_call hooks.
|
||||
|
||||
Returns:
|
||||
Number of hooks that were cleared
|
||||
|
||||
Example:
|
||||
>>> register_after_llm_call_hook(hook1)
|
||||
>>> register_after_llm_call_hook(hook2)
|
||||
>>> clear_after_llm_call_hooks()
|
||||
2
|
||||
"""
|
||||
count = len(_after_llm_call_hooks)
|
||||
_after_llm_call_hooks.clear()
|
||||
return count
|
||||
|
||||
|
||||
def clear_all_llm_call_hooks() -> tuple[int, int]:
|
||||
"""Clear all registered global LLM call hooks (both before and after).
|
||||
|
||||
Returns:
|
||||
Tuple of (before_hooks_cleared, after_hooks_cleared)
|
||||
|
||||
Example:
|
||||
>>> register_before_llm_call_hook(before_hook)
|
||||
>>> register_after_llm_call_hook(after_hook)
|
||||
>>> clear_all_llm_call_hooks()
|
||||
(1, 1)
|
||||
"""
|
||||
before_count = clear_before_llm_call_hooks()
|
||||
after_count = clear_after_llm_call_hooks()
|
||||
return (before_count, after_count)
|
||||
305
lib/crewai/src/crewai/hooks/tool_hooks.py
Normal file
305
lib/crewai/src/crewai/hooks/tool_hooks.py
Normal file
@@ -0,0 +1,305 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from crewai.events.event_listener import event_listener
|
||||
from crewai.hooks.types import AfterToolCallHookType, BeforeToolCallHookType
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.crew import Crew
|
||||
from crewai.task import Task
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
|
||||
|
||||
class ToolCallHookContext:
|
||||
"""Context object passed to tool call hooks.
|
||||
|
||||
Provides hooks with access to the tool being called, its input,
|
||||
the agent/task/crew context, and the result (for after hooks).
|
||||
|
||||
Attributes:
|
||||
tool_name: Name of the tool being called
|
||||
tool_input: Tool input parameters (mutable dict).
|
||||
Can be modified in-place by before_tool_call hooks.
|
||||
IMPORTANT: Modify in-place (e.g., context.tool_input['key'] = value).
|
||||
Do NOT replace the dict (e.g., context.tool_input = {}), as this
|
||||
will not affect the actual tool execution.
|
||||
tool: Reference to the CrewStructuredTool instance
|
||||
agent: Agent executing the tool (may be None)
|
||||
task: Current task being executed (may be None)
|
||||
crew: Crew instance (may be None)
|
||||
tool_result: Tool execution result (only set for after_tool_call hooks).
|
||||
Can be modified by returning a new string from after_tool_call hook.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tool_name: str,
|
||||
tool_input: dict[str, Any],
|
||||
tool: CrewStructuredTool,
|
||||
agent: Agent | BaseAgent | None = None,
|
||||
task: Task | None = None,
|
||||
crew: Crew | None = None,
|
||||
tool_result: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize tool call hook context.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool being called
|
||||
tool_input: Tool input parameters (mutable)
|
||||
tool: Tool instance reference
|
||||
agent: Optional agent executing the tool
|
||||
task: Optional current task
|
||||
crew: Optional crew instance
|
||||
tool_result: Optional tool result (for after hooks)
|
||||
"""
|
||||
self.tool_name = tool_name
|
||||
self.tool_input = tool_input
|
||||
self.tool = tool
|
||||
self.agent = agent
|
||||
self.task = task
|
||||
self.crew = crew
|
||||
self.tool_result = tool_result
|
||||
|
||||
def request_human_input(
|
||||
self,
|
||||
prompt: str,
|
||||
default_message: str = "Press Enter to continue, or provide feedback:",
|
||||
) -> str:
|
||||
"""Request human input during tool hook execution.
|
||||
|
||||
This method pauses live console updates, displays a prompt to the user,
|
||||
waits for their input, and then resumes live updates. This is useful for
|
||||
approval gates, reviewing tool results, or getting human feedback during execution.
|
||||
|
||||
Args:
|
||||
prompt: Custom message to display to the user
|
||||
default_message: Message shown after the prompt
|
||||
|
||||
Returns:
|
||||
User's input as a string (empty string if just Enter pressed)
|
||||
|
||||
Example:
|
||||
>>> def approval_hook(context: ToolCallHookContext) -> bool | None:
|
||||
... if context.tool_name == "delete_file":
|
||||
... response = context.request_human_input(
|
||||
... prompt="Allow file deletion?",
|
||||
... default_message="Type 'approve' to continue:",
|
||||
... )
|
||||
... if response.lower() != "approve":
|
||||
... return False # Block execution
|
||||
... return None # Allow execution
|
||||
"""
|
||||
|
||||
printer = Printer()
|
||||
event_listener.formatter.pause_live_updates()
|
||||
|
||||
try:
|
||||
printer.print(content=f"\n{prompt}", color="bold_yellow")
|
||||
printer.print(content=default_message, color="cyan")
|
||||
response = input().strip()
|
||||
|
||||
if response:
|
||||
printer.print(content="\nProcessing your input...", color="cyan")
|
||||
|
||||
return response
|
||||
finally:
|
||||
event_listener.formatter.resume_live_updates()
|
||||
|
||||
|
||||
# Global hook registries
|
||||
_before_tool_call_hooks: list[BeforeToolCallHookType] = []
|
||||
_after_tool_call_hooks: list[AfterToolCallHookType] = []
|
||||
|
||||
|
||||
def register_before_tool_call_hook(
|
||||
hook: BeforeToolCallHookType,
|
||||
) -> None:
|
||||
"""Register a global before_tool_call hook.
|
||||
|
||||
Global hooks are added to all tool executions automatically.
|
||||
This is a convenience function for registering hooks that should
|
||||
apply to all tool calls across all agents and crews.
|
||||
|
||||
Args:
|
||||
hook: Function that receives ToolCallHookContext and can:
|
||||
- Modify tool_input in-place
|
||||
- Return False to block tool execution
|
||||
- Return True or None to allow execution
|
||||
IMPORTANT: Modify tool_input in-place (e.g., context.tool_input['key'] = value).
|
||||
Do NOT replace the dict (context.tool_input = {}), as this will not affect
|
||||
the actual tool execution.
|
||||
|
||||
Example:
|
||||
>>> def log_tool_usage(context: ToolCallHookContext) -> None:
|
||||
... print(f"Executing tool: {context.tool_name}")
|
||||
... print(f"Input: {context.tool_input}")
|
||||
... return None # Allow execution
|
||||
>>>
|
||||
>>> register_before_tool_call_hook(log_tool_usage)
|
||||
|
||||
>>> def block_dangerous_tools(context: ToolCallHookContext) -> bool | None:
|
||||
... if context.tool_name == "delete_database":
|
||||
... print("Blocked dangerous tool execution!")
|
||||
... return False # Block execution
|
||||
... return None # Allow execution
|
||||
>>>
|
||||
>>> register_before_tool_call_hook(block_dangerous_tools)
|
||||
"""
|
||||
_before_tool_call_hooks.append(hook)
|
||||
|
||||
|
||||
def register_after_tool_call_hook(
|
||||
hook: AfterToolCallHookType,
|
||||
) -> None:
|
||||
"""Register a global after_tool_call hook.
|
||||
|
||||
Global hooks are added to all tool executions automatically.
|
||||
This is a convenience function for registering hooks that should
|
||||
apply to all tool calls across all agents and crews.
|
||||
|
||||
Args:
|
||||
hook: Function that receives ToolCallHookContext and can modify
|
||||
the tool result. Return modified result string or None to keep
|
||||
the original result. The tool_result is available in context.tool_result.
|
||||
|
||||
Example:
|
||||
>>> def sanitize_output(context: ToolCallHookContext) -> str | None:
|
||||
... if context.tool_result and "SECRET_KEY" in context.tool_result:
|
||||
... return context.tool_result.replace("SECRET_KEY=...", "[REDACTED]")
|
||||
... return None # Keep original result
|
||||
>>>
|
||||
>>> register_after_tool_call_hook(sanitize_output)
|
||||
|
||||
>>> def log_tool_results(context: ToolCallHookContext) -> None:
|
||||
... print(f"Tool {context.tool_name} returned: {context.tool_result[:100]}")
|
||||
... return None # Keep original result
|
||||
>>>
|
||||
>>> register_after_tool_call_hook(log_tool_results)
|
||||
"""
|
||||
_after_tool_call_hooks.append(hook)
|
||||
|
||||
|
||||
def get_before_tool_call_hooks() -> list[BeforeToolCallHookType]:
|
||||
"""Get all registered global before_tool_call hooks.
|
||||
|
||||
Returns:
|
||||
List of registered before hooks
|
||||
"""
|
||||
return _before_tool_call_hooks.copy()
|
||||
|
||||
|
||||
def get_after_tool_call_hooks() -> list[AfterToolCallHookType]:
|
||||
"""Get all registered global after_tool_call hooks.
|
||||
|
||||
Returns:
|
||||
List of registered after hooks
|
||||
"""
|
||||
return _after_tool_call_hooks.copy()
|
||||
|
||||
|
||||
def unregister_before_tool_call_hook(
|
||||
hook: BeforeToolCallHookType,
|
||||
) -> bool:
|
||||
"""Unregister a specific global before_tool_call hook.
|
||||
|
||||
Args:
|
||||
hook: The hook function to remove
|
||||
|
||||
Returns:
|
||||
True if the hook was found and removed, False otherwise
|
||||
|
||||
Example:
|
||||
>>> def my_hook(context: ToolCallHookContext) -> None:
|
||||
... print("Before tool call")
|
||||
>>>
|
||||
>>> register_before_tool_call_hook(my_hook)
|
||||
>>> unregister_before_tool_call_hook(my_hook)
|
||||
True
|
||||
"""
|
||||
try:
|
||||
_before_tool_call_hooks.remove(hook)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def unregister_after_tool_call_hook(
|
||||
hook: AfterToolCallHookType,
|
||||
) -> bool:
|
||||
"""Unregister a specific global after_tool_call hook.
|
||||
|
||||
Args:
|
||||
hook: The hook function to remove
|
||||
|
||||
Returns:
|
||||
True if the hook was found and removed, False otherwise
|
||||
|
||||
Example:
|
||||
>>> def my_hook(context: ToolCallHookContext) -> str | None:
|
||||
... return None
|
||||
>>>
|
||||
>>> register_after_tool_call_hook(my_hook)
|
||||
>>> unregister_after_tool_call_hook(my_hook)
|
||||
True
|
||||
"""
|
||||
try:
|
||||
_after_tool_call_hooks.remove(hook)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def clear_before_tool_call_hooks() -> int:
|
||||
"""Clear all registered global before_tool_call hooks.
|
||||
|
||||
Returns:
|
||||
Number of hooks that were cleared
|
||||
|
||||
Example:
|
||||
>>> register_before_tool_call_hook(hook1)
|
||||
>>> register_before_tool_call_hook(hook2)
|
||||
>>> clear_before_tool_call_hooks()
|
||||
2
|
||||
"""
|
||||
count = len(_before_tool_call_hooks)
|
||||
_before_tool_call_hooks.clear()
|
||||
return count
|
||||
|
||||
|
||||
def clear_after_tool_call_hooks() -> int:
|
||||
"""Clear all registered global after_tool_call hooks.
|
||||
|
||||
Returns:
|
||||
Number of hooks that were cleared
|
||||
|
||||
Example:
|
||||
>>> register_after_tool_call_hook(hook1)
|
||||
>>> register_after_tool_call_hook(hook2)
|
||||
>>> clear_after_tool_call_hooks()
|
||||
2
|
||||
"""
|
||||
count = len(_after_tool_call_hooks)
|
||||
_after_tool_call_hooks.clear()
|
||||
return count
|
||||
|
||||
|
||||
def clear_all_tool_call_hooks() -> tuple[int, int]:
|
||||
"""Clear all registered global tool call hooks (both before and after).
|
||||
|
||||
Returns:
|
||||
Tuple of (before_hooks_cleared, after_hooks_cleared)
|
||||
|
||||
Example:
|
||||
>>> register_before_tool_call_hook(before_hook)
|
||||
>>> register_after_tool_call_hook(after_hook)
|
||||
>>> clear_all_tool_call_hooks()
|
||||
(1, 1)
|
||||
"""
|
||||
before_count = clear_before_tool_call_hooks()
|
||||
after_count = clear_after_tool_call_hooks()
|
||||
return (before_count, after_count)
|
||||
137
lib/crewai/src/crewai/hooks/types.py
Normal file
137
lib/crewai/src/crewai/hooks/types.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, runtime_checkable
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
from crewai.hooks.tool_hooks import ToolCallHookContext
|
||||
|
||||
|
||||
ContextT = TypeVar("ContextT", contravariant=True)
|
||||
ReturnT = TypeVar("ReturnT", covariant=True)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Hook(Protocol, Generic[ContextT, ReturnT]):
|
||||
"""Generic protocol for hook functions.
|
||||
|
||||
This protocol defines the common interface for all hook types in CrewAI.
|
||||
Hooks receive a context object and optionally return a modified result.
|
||||
|
||||
Type Parameters:
|
||||
ContextT: The context type (LLMCallHookContext or ToolCallHookContext)
|
||||
ReturnT: The return type (None, str | None, or bool | None)
|
||||
|
||||
Example:
|
||||
>>> # Before LLM call hook: receives LLMCallHookContext, returns None
|
||||
>>> hook: Hook[LLMCallHookContext, None] = lambda ctx: print(ctx.iterations)
|
||||
>>>
|
||||
>>> # After LLM call hook: receives LLMCallHookContext, returns str | None
|
||||
>>> hook: Hook[LLMCallHookContext, str | None] = lambda ctx: ctx.response
|
||||
"""
|
||||
|
||||
def __call__(self, context: ContextT) -> ReturnT:
|
||||
"""Execute the hook with the given context.
|
||||
|
||||
Args:
|
||||
context: Context object with relevant execution state
|
||||
|
||||
Returns:
|
||||
Hook-specific return value (None, str | None, or bool | None)
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BeforeLLMCallHook(Hook["LLMCallHookContext", bool | None], Protocol):
|
||||
"""Protocol for before_llm_call hooks.
|
||||
|
||||
These hooks are called before an LLM is invoked and can modify the messages
|
||||
that will be sent to the LLM or block the execution entirely.
|
||||
"""
|
||||
|
||||
def __call__(self, context: LLMCallHookContext) -> bool | None:
|
||||
"""Execute the before LLM call hook.
|
||||
|
||||
Args:
|
||||
context: Context object with executor, messages, agent, task, etc.
|
||||
Messages can be modified in-place.
|
||||
|
||||
Returns:
|
||||
False to block LLM execution, True or None to allow execution
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class AfterLLMCallHook(Hook["LLMCallHookContext", str | None], Protocol):
|
||||
"""Protocol for after_llm_call hooks.
|
||||
|
||||
These hooks are called after an LLM returns a response and can modify
|
||||
the response or the message history.
|
||||
"""
|
||||
|
||||
def __call__(self, context: LLMCallHookContext) -> str | None:
|
||||
"""Execute the after LLM call hook.
|
||||
|
||||
Args:
|
||||
context: Context object with executor, messages, agent, task, response, etc.
|
||||
Messages can be modified in-place. Response is available in context.response.
|
||||
|
||||
Returns:
|
||||
Modified response string, or None to keep the original response
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BeforeToolCallHook(Hook["ToolCallHookContext", bool | None], Protocol):
|
||||
"""Protocol for before_tool_call hooks.
|
||||
|
||||
These hooks are called before a tool is executed and can modify the tool
|
||||
input or block the execution entirely.
|
||||
"""
|
||||
|
||||
def __call__(self, context: ToolCallHookContext) -> bool | None:
|
||||
"""Execute the before tool call hook.
|
||||
|
||||
Args:
|
||||
context: Context object with tool_name, tool_input, tool, agent, task, etc.
|
||||
Tool input can be modified in-place.
|
||||
|
||||
Returns:
|
||||
False to block tool execution, True or None to allow execution
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class AfterToolCallHook(Hook["ToolCallHookContext", str | None], Protocol):
|
||||
"""Protocol for after_tool_call hooks.
|
||||
|
||||
These hooks are called after a tool executes and can modify the result.
|
||||
"""
|
||||
|
||||
def __call__(self, context: ToolCallHookContext) -> str | None:
|
||||
"""Execute the after tool call hook.
|
||||
|
||||
Args:
|
||||
context: Context object with tool_name, tool_input, tool_result, etc.
|
||||
Tool result is available in context.tool_result.
|
||||
|
||||
Returns:
|
||||
Modified tool result string, or None to keep the original result
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
# - All before hooks: bool | None (False = block execution, True/None = allow)
|
||||
# - All after hooks: str | None (str = modified result, None = keep original)
|
||||
BeforeLLMCallHookType = Hook["LLMCallHookContext", bool | None]
|
||||
AfterLLMCallHookType = Hook["LLMCallHookContext", str | None]
|
||||
BeforeToolCallHookType = Hook["ToolCallHookContext", bool | None]
|
||||
AfterToolCallHookType = Hook["ToolCallHookContext", str | None]
|
||||
|
||||
# Alternative Callable-based type aliases for compatibility
|
||||
BeforeLLMCallHookCallable = Callable[["LLMCallHookContext"], bool | None]
|
||||
AfterLLMCallHookCallable = Callable[["LLMCallHookContext"], str | None]
|
||||
BeforeToolCallHookCallable = Callable[["ToolCallHookContext"], bool | None]
|
||||
AfterToolCallHookCallable = Callable[["ToolCallHookContext"], str | None]
|
||||
157
lib/crewai/src/crewai/hooks/wrappers.py
Normal file
157
lib/crewai/src/crewai/hooks/wrappers.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
from crewai.hooks.tool_hooks import ToolCallHookContext
|
||||
|
||||
P = TypeVar("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def _copy_method_metadata(wrapper: Any, original: Callable[..., Any]) -> None:
|
||||
"""Copy metadata from original function to wrapper.
|
||||
|
||||
Args:
|
||||
wrapper: The wrapper object to copy metadata to
|
||||
original: The original function to copy from
|
||||
"""
|
||||
wrapper.__name__ = original.__name__
|
||||
wrapper.__doc__ = original.__doc__
|
||||
wrapper.__module__ = original.__module__
|
||||
wrapper.__qualname__ = original.__qualname__
|
||||
wrapper.__annotations__ = original.__annotations__
|
||||
|
||||
|
||||
class BeforeLLMCallHookMethod:
|
||||
"""Wrapper for methods marked as before_llm_call hooks within @CrewBase classes.
|
||||
|
||||
This wrapper marks a method so it can be detected and registered as a
|
||||
crew-scoped hook during crew initialization.
|
||||
"""
|
||||
|
||||
is_before_llm_call_hook: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
meth: Callable[[Any, LLMCallHookContext], None],
|
||||
agents: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the hook method wrapper.
|
||||
|
||||
Args:
|
||||
meth: The method to wrap
|
||||
agents: Optional list of agent roles to filter
|
||||
"""
|
||||
self._meth = meth
|
||||
self.agents = agents
|
||||
_copy_method_metadata(self, meth)
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Call the wrapped method.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments
|
||||
**kwargs: Keyword arguments
|
||||
"""
|
||||
return self._meth(*args, **kwargs)
|
||||
|
||||
def __get__(self, obj: Any, objtype: type[Any] | None = None) -> Any:
|
||||
"""Support instance methods by implementing descriptor protocol.
|
||||
|
||||
Args:
|
||||
obj: The instance that the method is accessed through
|
||||
objtype: The type of the instance
|
||||
|
||||
Returns:
|
||||
Self when accessed through class, bound method when accessed through instance
|
||||
"""
|
||||
if obj is None:
|
||||
return self
|
||||
# Return bound method
|
||||
return lambda context: self._meth(obj, context)
|
||||
|
||||
|
||||
class AfterLLMCallHookMethod:
|
||||
"""Wrapper for methods marked as after_llm_call hooks within @CrewBase classes."""
|
||||
|
||||
is_after_llm_call_hook: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
meth: Callable[[Any, LLMCallHookContext], str | None],
|
||||
agents: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the hook method wrapper."""
|
||||
self._meth = meth
|
||||
self.agents = agents
|
||||
_copy_method_metadata(self, meth)
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> str | None:
|
||||
"""Call the wrapped method."""
|
||||
return self._meth(*args, **kwargs)
|
||||
|
||||
def __get__(self, obj: Any, objtype: type[Any] | None = None) -> Any:
|
||||
"""Support instance methods."""
|
||||
if obj is None:
|
||||
return self
|
||||
return lambda context: self._meth(obj, context)
|
||||
|
||||
|
||||
class BeforeToolCallHookMethod:
|
||||
"""Wrapper for methods marked as before_tool_call hooks within @CrewBase classes."""
|
||||
|
||||
is_before_tool_call_hook: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
meth: Callable[[Any, ToolCallHookContext], bool | None],
|
||||
tools: list[str] | None = None,
|
||||
agents: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the hook method wrapper."""
|
||||
self._meth = meth
|
||||
self.tools = tools
|
||||
self.agents = agents
|
||||
_copy_method_metadata(self, meth)
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> bool | None:
|
||||
"""Call the wrapped method."""
|
||||
return self._meth(*args, **kwargs)
|
||||
|
||||
def __get__(self, obj: Any, objtype: type[Any] | None = None) -> Any:
|
||||
"""Support instance methods."""
|
||||
if obj is None:
|
||||
return self
|
||||
return lambda context: self._meth(obj, context)
|
||||
|
||||
|
||||
class AfterToolCallHookMethod:
|
||||
"""Wrapper for methods marked as after_tool_call hooks within @CrewBase classes."""
|
||||
|
||||
is_after_tool_call_hook: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
meth: Callable[[Any, ToolCallHookContext], str | None],
|
||||
tools: list[str] | None = None,
|
||||
agents: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the hook method wrapper."""
|
||||
self._meth = meth
|
||||
self.tools = tools
|
||||
self.agents = agents
|
||||
_copy_method_metadata(self, meth)
|
||||
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> str | None:
|
||||
"""Call the wrapped method."""
|
||||
return self._meth(*args, **kwargs)
|
||||
|
||||
def __get__(self, obj: Any, objtype: type[Any] | None = None) -> Any:
|
||||
"""Support instance methods."""
|
||||
if obj is None:
|
||||
return self
|
||||
return lambda context: self._meth(obj, context)
|
||||
@@ -542,6 +542,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
agent_key=self.key,
|
||||
agent_role=self.role,
|
||||
agent=self.original_agent,
|
||||
crew=None,
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@@ -293,6 +293,8 @@ class CrewBaseMeta(type):
|
||||
kickoff=_filter_methods(original_methods, "is_kickoff"),
|
||||
)
|
||||
|
||||
_register_crew_hooks(instance, cls)
|
||||
|
||||
|
||||
def close_mcp_server(
|
||||
self: CrewInstance, _instance: CrewInstance, outputs: CrewOutput
|
||||
@@ -438,6 +440,144 @@ def _filter_methods(
|
||||
}
|
||||
|
||||
|
||||
def _register_crew_hooks(instance: CrewInstance, cls: type) -> None:
|
||||
"""Detect and register crew-scoped hook methods.
|
||||
|
||||
Args:
|
||||
instance: Crew instance to register hooks for.
|
||||
cls: Crew class type.
|
||||
"""
|
||||
hook_methods = {
|
||||
name: method
|
||||
for name, method in cls.__dict__.items()
|
||||
if any(
|
||||
hasattr(method, attr)
|
||||
for attr in [
|
||||
"is_before_llm_call_hook",
|
||||
"is_after_llm_call_hook",
|
||||
"is_before_tool_call_hook",
|
||||
"is_after_tool_call_hook",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
if not hook_methods:
|
||||
return
|
||||
|
||||
from crewai.hooks import (
|
||||
register_after_llm_call_hook,
|
||||
register_after_tool_call_hook,
|
||||
register_before_llm_call_hook,
|
||||
register_before_tool_call_hook,
|
||||
)
|
||||
|
||||
instance._registered_hook_functions = []
|
||||
|
||||
instance._hooks_being_registered = True
|
||||
|
||||
for hook_method in hook_methods.values():
|
||||
bound_hook = hook_method.__get__(instance, cls)
|
||||
|
||||
has_tool_filter = hasattr(hook_method, "_filter_tools")
|
||||
has_agent_filter = hasattr(hook_method, "_filter_agents")
|
||||
|
||||
if hasattr(hook_method, "is_before_llm_call_hook"):
|
||||
if has_agent_filter:
|
||||
agents_filter = hook_method._filter_agents
|
||||
|
||||
def make_filtered_before_llm(bound_fn, agents_list):
|
||||
def filtered(context):
|
||||
if context.agent and context.agent.role not in agents_list:
|
||||
return None
|
||||
return bound_fn(context)
|
||||
|
||||
return filtered
|
||||
|
||||
final_hook = make_filtered_before_llm(bound_hook, agents_filter)
|
||||
else:
|
||||
final_hook = bound_hook
|
||||
|
||||
register_before_llm_call_hook(final_hook)
|
||||
instance._registered_hook_functions.append(("before_llm_call", final_hook))
|
||||
|
||||
if hasattr(hook_method, "is_after_llm_call_hook"):
|
||||
if has_agent_filter:
|
||||
agents_filter = hook_method._filter_agents
|
||||
|
||||
def make_filtered_after_llm(bound_fn, agents_list):
|
||||
def filtered(context):
|
||||
if context.agent and context.agent.role not in agents_list:
|
||||
return None
|
||||
return bound_fn(context)
|
||||
|
||||
return filtered
|
||||
|
||||
final_hook = make_filtered_after_llm(bound_hook, agents_filter)
|
||||
else:
|
||||
final_hook = bound_hook
|
||||
|
||||
register_after_llm_call_hook(final_hook)
|
||||
instance._registered_hook_functions.append(("after_llm_call", final_hook))
|
||||
|
||||
if hasattr(hook_method, "is_before_tool_call_hook"):
|
||||
if has_tool_filter or has_agent_filter:
|
||||
tools_filter = getattr(hook_method, "_filter_tools", None)
|
||||
agents_filter = getattr(hook_method, "_filter_agents", None)
|
||||
|
||||
def make_filtered_before_tool(bound_fn, tools_list, agents_list):
|
||||
def filtered(context):
|
||||
if tools_list and context.tool_name not in tools_list:
|
||||
return None
|
||||
if (
|
||||
agents_list
|
||||
and context.agent
|
||||
and context.agent.role not in agents_list
|
||||
):
|
||||
return None
|
||||
return bound_fn(context)
|
||||
|
||||
return filtered
|
||||
|
||||
final_hook = make_filtered_before_tool(
|
||||
bound_hook, tools_filter, agents_filter
|
||||
)
|
||||
else:
|
||||
final_hook = bound_hook
|
||||
|
||||
register_before_tool_call_hook(final_hook)
|
||||
instance._registered_hook_functions.append(("before_tool_call", final_hook))
|
||||
|
||||
if hasattr(hook_method, "is_after_tool_call_hook"):
|
||||
if has_tool_filter or has_agent_filter:
|
||||
tools_filter = getattr(hook_method, "_filter_tools", None)
|
||||
agents_filter = getattr(hook_method, "_filter_agents", None)
|
||||
|
||||
def make_filtered_after_tool(bound_fn, tools_list, agents_list):
|
||||
def filtered(context):
|
||||
if tools_list and context.tool_name not in tools_list:
|
||||
return None
|
||||
if (
|
||||
agents_list
|
||||
and context.agent
|
||||
and context.agent.role not in agents_list
|
||||
):
|
||||
return None
|
||||
return bound_fn(context)
|
||||
|
||||
return filtered
|
||||
|
||||
final_hook = make_filtered_after_tool(
|
||||
bound_hook, tools_filter, agents_filter
|
||||
)
|
||||
else:
|
||||
final_hook = bound_hook
|
||||
|
||||
register_after_tool_call_hook(final_hook)
|
||||
instance._registered_hook_functions.append(("after_tool_call", final_hook))
|
||||
|
||||
instance._hooks_being_registered = False
|
||||
|
||||
|
||||
def map_all_agent_variables(self: CrewInstance) -> None:
|
||||
"""Map agent configuration variables to callable instances.
|
||||
|
||||
|
||||
@@ -260,7 +260,8 @@ def get_llm_response(
|
||||
"""
|
||||
|
||||
if executor_context is not None:
|
||||
_setup_before_llm_call_hooks(executor_context, printer)
|
||||
if not _setup_before_llm_call_hooks(executor_context, printer):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
messages = executor_context.messages
|
||||
|
||||
try:
|
||||
@@ -673,22 +674,31 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
|
||||
def _setup_before_llm_call_hooks(
|
||||
executor_context: CrewAgentExecutor | None, printer: Printer
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""Setup and invoke before_llm_call hooks for the executor context.
|
||||
|
||||
Args:
|
||||
executor_context: The executor context to setup the hooks for.
|
||||
printer: Printer instance for error logging.
|
||||
|
||||
Returns:
|
||||
True if LLM execution should proceed, False if blocked by a hook.
|
||||
"""
|
||||
if executor_context and executor_context.before_llm_call_hooks:
|
||||
from crewai.utilities.llm_call_hooks import LLMCallHookContext
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
|
||||
original_messages = executor_context.messages
|
||||
|
||||
hook_context = LLMCallHookContext(executor_context)
|
||||
try:
|
||||
for hook in executor_context.before_llm_call_hooks:
|
||||
hook(hook_context)
|
||||
result = hook(hook_context)
|
||||
if result is False:
|
||||
printer.print(
|
||||
content="LLM call blocked by before_llm_call hook",
|
||||
color="yellow",
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
printer.print(
|
||||
content=f"Error in before_llm_call hook: {e}",
|
||||
@@ -709,6 +719,8 @@ def _setup_before_llm_call_hooks(
|
||||
else:
|
||||
executor_context.messages = []
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _setup_after_llm_call_hooks(
|
||||
executor_context: CrewAgentExecutor | None,
|
||||
@@ -726,7 +738,7 @@ def _setup_after_llm_call_hooks(
|
||||
The potentially modified response string.
|
||||
"""
|
||||
if executor_context and executor_context.after_llm_call_hooks:
|
||||
from crewai.utilities.llm_call_hooks import LLMCallHookContext
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
|
||||
original_messages = executor_context.messages
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
|
||||
|
||||
class LLMCallHookContext:
|
||||
"""Context object passed to LLM call hooks with full executor access.
|
||||
|
||||
Provides hooks with complete access to the executor state, allowing
|
||||
modification of messages, responses, and executor attributes.
|
||||
|
||||
Attributes:
|
||||
executor: Full reference to the CrewAgentExecutor instance
|
||||
messages: Direct reference to executor.messages (mutable list).
|
||||
Can be modified in both before_llm_call and after_llm_call hooks.
|
||||
Modifications in after_llm_call hooks persist to the next iteration,
|
||||
allowing hooks to modify conversation history for subsequent LLM calls.
|
||||
IMPORTANT: Modify messages in-place (e.g., append, extend, remove items).
|
||||
Do NOT replace the list (e.g., context.messages = []), as this will break
|
||||
the executor. Use context.messages.append() or context.messages.extend()
|
||||
instead of assignment.
|
||||
agent: Reference to the agent executing the task
|
||||
task: Reference to the task being executed
|
||||
crew: Reference to the crew instance
|
||||
llm: Reference to the LLM instance
|
||||
iterations: Current iteration count
|
||||
response: LLM response string (only set for after_llm_call hooks).
|
||||
Can be modified by returning a new string from after_llm_call hook.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
executor: CrewAgentExecutor,
|
||||
response: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize hook context with executor reference.
|
||||
|
||||
Args:
|
||||
executor: The CrewAgentExecutor instance
|
||||
response: Optional response string (for after_llm_call hooks)
|
||||
"""
|
||||
self.executor = executor
|
||||
self.messages = executor.messages
|
||||
self.agent = executor.agent
|
||||
self.task = executor.task
|
||||
self.crew = executor.crew
|
||||
self.llm = executor.llm
|
||||
self.iterations = executor.iterations
|
||||
self.response = response
|
||||
|
||||
|
||||
# Global hook registries (optional convenience feature)
|
||||
_before_llm_call_hooks: list[Callable[[LLMCallHookContext], None]] = []
|
||||
_after_llm_call_hooks: list[Callable[[LLMCallHookContext], str | None]] = []
|
||||
|
||||
|
||||
def register_before_llm_call_hook(
|
||||
hook: Callable[[LLMCallHookContext], None],
|
||||
) -> None:
|
||||
"""Register a global before_llm_call hook.
|
||||
|
||||
Global hooks are added to all executors automatically.
|
||||
This is a convenience function for registering hooks that should
|
||||
apply to all LLM calls across all executors.
|
||||
|
||||
Args:
|
||||
hook: Function that receives LLMCallHookContext and can modify
|
||||
context.messages directly. Should return None.
|
||||
IMPORTANT: Modify messages in-place (append, extend, remove items).
|
||||
Do NOT replace the list (context.messages = []), as this will break execution.
|
||||
"""
|
||||
_before_llm_call_hooks.append(hook)
|
||||
|
||||
|
||||
def register_after_llm_call_hook(
|
||||
hook: Callable[[LLMCallHookContext], str | None],
|
||||
) -> None:
|
||||
"""Register a global after_llm_call hook.
|
||||
|
||||
Global hooks are added to all executors automatically.
|
||||
This is a convenience function for registering hooks that should
|
||||
apply to all LLM calls across all executors.
|
||||
|
||||
Args:
|
||||
hook: Function that receives LLMCallHookContext and can modify:
|
||||
- The response: Return modified response string or None to keep original
|
||||
- The messages: Modify context.messages directly (mutable reference)
|
||||
Both modifications are supported and can be used together.
|
||||
IMPORTANT: Modify messages in-place (append, extend, remove items).
|
||||
Do NOT replace the list (context.messages = []), as this will break execution.
|
||||
"""
|
||||
_after_llm_call_hooks.append(hook)
|
||||
|
||||
|
||||
def get_before_llm_call_hooks() -> list[Callable[[LLMCallHookContext], None]]:
|
||||
"""Get all registered global before_llm_call hooks.
|
||||
|
||||
Returns:
|
||||
List of registered before hooks
|
||||
"""
|
||||
return _before_llm_call_hooks.copy()
|
||||
|
||||
|
||||
def get_after_llm_call_hooks() -> list[Callable[[LLMCallHookContext], str | None]]:
|
||||
"""Get all registered global after_llm_call hooks.
|
||||
|
||||
Returns:
|
||||
List of registered after hooks
|
||||
"""
|
||||
return _after_llm_call_hooks.copy()
|
||||
@@ -4,16 +4,23 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from crewai.agents.parser import AgentAction
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.hooks.tool_hooks import (
|
||||
ToolCallHookContext,
|
||||
get_after_tool_call_hooks,
|
||||
get_before_tool_call_hooks,
|
||||
)
|
||||
from crewai.security.fingerprint import Fingerprint
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.tools.tool_types import ToolResult
|
||||
from crewai.tools.tool_usage import ToolUsage, ToolUsageError
|
||||
from crewai.utilities.i18n import I18N
|
||||
from crewai.utilities.logger import Logger
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.crew import Crew
|
||||
from crewai.llm import LLM
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.task import Task
|
||||
@@ -30,9 +37,13 @@ def execute_tool_and_check_finality(
|
||||
agent: Agent | BaseAgent | None = None,
|
||||
function_calling_llm: BaseLLM | LLM | None = None,
|
||||
fingerprint_context: dict[str, str] | None = None,
|
||||
crew: Crew | None = None,
|
||||
) -> ToolResult:
|
||||
"""Execute a tool and check if the result should be treated as a final answer.
|
||||
|
||||
This function integrates tool hooks for before and after tool execution,
|
||||
allowing programmatic interception and modification of tool calls.
|
||||
|
||||
Args:
|
||||
agent_action: The action containing the tool to execute
|
||||
tools: List of available tools
|
||||
@@ -44,10 +55,12 @@ def execute_tool_and_check_finality(
|
||||
agent: Optional agent instance for tool execution
|
||||
function_calling_llm: Optional LLM for function calling
|
||||
fingerprint_context: Optional context for fingerprinting
|
||||
crew: Optional crew instance for hook context
|
||||
|
||||
Returns:
|
||||
ToolResult containing the execution result and whether it should be treated as a final answer
|
||||
"""
|
||||
logger = Logger(verbose=crew.verbose if crew else False)
|
||||
tool_name_to_tool_map = {tool.name: tool for tool in tools}
|
||||
|
||||
if agent_key and agent_role and agent:
|
||||
@@ -83,10 +96,62 @@ def execute_tool_and_check_finality(
|
||||
] or tool_calling.tool_name.casefold().replace("_", " ") in [
|
||||
name.casefold().strip() for name in tool_name_to_tool_map
|
||||
]:
|
||||
tool_result = tool_usage.use(tool_calling, agent_action.text)
|
||||
tool = tool_name_to_tool_map.get(tool_calling.tool_name)
|
||||
if tool:
|
||||
return ToolResult(tool_result, tool.result_as_answer)
|
||||
if not tool:
|
||||
tool_result = i18n.errors("wrong_tool_name").format(
|
||||
tool=tool_calling.tool_name,
|
||||
tools=", ".join([t.name.casefold() for t in tools]),
|
||||
)
|
||||
return ToolResult(result=tool_result, result_as_answer=False)
|
||||
|
||||
tool_input = tool_calling.arguments if tool_calling.arguments else {}
|
||||
hook_context = ToolCallHookContext(
|
||||
tool_name=tool_calling.tool_name,
|
||||
tool_input=tool_input,
|
||||
tool=tool,
|
||||
agent=agent,
|
||||
task=task,
|
||||
crew=crew,
|
||||
)
|
||||
|
||||
before_hooks = get_before_tool_call_hooks()
|
||||
try:
|
||||
for hook in before_hooks:
|
||||
result = hook(hook_context)
|
||||
if result is False:
|
||||
blocked_message = (
|
||||
f"Tool execution blocked by hook. "
|
||||
f"Tool: {tool_calling.tool_name}"
|
||||
)
|
||||
return ToolResult(blocked_message, False)
|
||||
except Exception as e:
|
||||
logger.log("error", f"Error in before_tool_call hook: {e}")
|
||||
|
||||
tool_result = tool_usage.use(tool_calling, agent_action.text)
|
||||
|
||||
after_hook_context = ToolCallHookContext(
|
||||
tool_name=tool_calling.tool_name,
|
||||
tool_input=tool_input,
|
||||
tool=tool,
|
||||
agent=agent,
|
||||
task=task,
|
||||
crew=crew,
|
||||
tool_result=tool_result,
|
||||
)
|
||||
|
||||
# Execute after_tool_call hooks
|
||||
after_hooks = get_after_tool_call_hooks()
|
||||
modified_result = tool_result
|
||||
try:
|
||||
for hook in after_hooks:
|
||||
hook_result = hook(after_hook_context)
|
||||
if hook_result is not None:
|
||||
modified_result = hook_result
|
||||
after_hook_context.tool_result = modified_result
|
||||
except Exception as e:
|
||||
logger.log("error", f"Error in after_tool_call hook: {e}")
|
||||
|
||||
return ToolResult(modified_result, tool.result_as_answer)
|
||||
|
||||
# Handle invalid tool name
|
||||
tool_result = i18n.errors("wrong_tool_name").format(
|
||||
|
||||
Reference in New Issue
Block a user