mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-02 15:52:34 +00:00
feat: implement before and after tool call hooks in CrewAgentExecutor… (#4287)
* feat: implement before and after tool call hooks in CrewAgentExecutor and AgentExecutor - Added support for before and after tool call hooks in both CrewAgentExecutor and AgentExecutor classes. - Introduced ToolCallHookContext to manage context for hooks, allowing for enhanced control over tool execution. - Implemented logic to block tool execution based on before hooks and to modify results based on after hooks. - Added integration tests to validate the functionality of the new hooks, ensuring they work as expected in various scenarios. - Enhanced the overall flexibility and extensibility of tool interactions within the CrewAI framework. * Potential fix for pull request finding 'Unused local variable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Potential fix for pull request finding 'Unused local variable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * test: add integration test for before hook blocking tool execution in Crew - Implemented a new test to verify that the before hook can successfully block the execution of a tool within a crew. - The test checks that the tool is not executed when the before hook returns False, ensuring proper control over tool interactions. - Enhanced the validation of hook calls to confirm that both before and after hooks are triggered appropriately, even when execution is blocked. - This addition strengthens the testing coverage for tool call hooks in the CrewAI framework. * drop unused * refactor(tests): remove OPENAI_API_KEY check from tool hook tests - Eliminated the check for the OPENAI_API_KEY environment variable in the test cases for tool hooks. - This change simplifies the test setup and allows for running tests without requiring the API key to be set, improving test accessibility and flexibility. --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
@@ -590,3 +590,233 @@ class TestToolHooksIntegration:
|
||||
# Clean up hooks
|
||||
unregister_before_tool_call_hook(before_tool_call_hook)
|
||||
unregister_after_tool_call_hook(after_tool_call_hook)
|
||||
|
||||
|
||||
class TestNativeToolCallingHooksIntegration:
|
||||
"""Integration tests for hooks with native function calling (Agent and Crew)."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_agent_native_tool_hooks_before_and_after(self):
|
||||
"""Test that Agent with native tool calling executes before/after hooks."""
|
||||
import os
|
||||
from crewai import Agent
|
||||
from crewai.tools import tool
|
||||
|
||||
hook_calls = {"before": [], "after": []}
|
||||
|
||||
@tool("multiply_numbers")
|
||||
def multiply_numbers(a: int, b: int) -> int:
|
||||
"""Multiply two numbers together."""
|
||||
return a * b
|
||||
|
||||
def before_hook(context: ToolCallHookContext) -> bool | None:
|
||||
hook_calls["before"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_input": dict(context.tool_input),
|
||||
"has_agent": context.agent is not None,
|
||||
})
|
||||
return None
|
||||
|
||||
def after_hook(context: ToolCallHookContext) -> str | None:
|
||||
hook_calls["after"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_result": context.tool_result,
|
||||
"has_agent": context.agent is not None,
|
||||
})
|
||||
return None
|
||||
|
||||
register_before_tool_call_hook(before_hook)
|
||||
register_after_tool_call_hook(after_hook)
|
||||
|
||||
try:
|
||||
agent = Agent(
|
||||
role="Calculator",
|
||||
goal="Perform calculations",
|
||||
backstory="You are a calculator assistant",
|
||||
tools=[multiply_numbers],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
agent.kickoff(
|
||||
messages="What is 7 times 6? Use the multiply_numbers tool."
|
||||
)
|
||||
|
||||
# Verify before hook was called
|
||||
assert len(hook_calls["before"]) > 0, "Before hook was never called"
|
||||
before_call = hook_calls["before"][0]
|
||||
assert before_call["tool_name"] == "multiply_numbers"
|
||||
assert "a" in before_call["tool_input"]
|
||||
assert "b" in before_call["tool_input"]
|
||||
assert before_call["has_agent"] is True
|
||||
|
||||
# Verify after hook was called
|
||||
assert len(hook_calls["after"]) > 0, "After hook was never called"
|
||||
after_call = hook_calls["after"][0]
|
||||
assert after_call["tool_name"] == "multiply_numbers"
|
||||
assert "42" in str(after_call["tool_result"])
|
||||
assert after_call["has_agent"] is True
|
||||
|
||||
finally:
|
||||
unregister_before_tool_call_hook(before_hook)
|
||||
unregister_after_tool_call_hook(after_hook)
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_crew_native_tool_hooks_before_and_after(self):
|
||||
"""Test that Crew with Agent executes before/after hooks with full context."""
|
||||
import os
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai.tools import tool
|
||||
|
||||
|
||||
hook_calls = {"before": [], "after": []}
|
||||
|
||||
@tool("divide_numbers")
|
||||
def divide_numbers(a: int, b: int) -> float:
|
||||
"""Divide first number by second number."""
|
||||
return a / b
|
||||
|
||||
def before_hook(context: ToolCallHookContext) -> bool | None:
|
||||
hook_calls["before"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_input": dict(context.tool_input),
|
||||
"has_agent": context.agent is not None,
|
||||
"has_task": context.task is not None,
|
||||
"has_crew": context.crew is not None,
|
||||
"agent_role": context.agent.role if context.agent else None,
|
||||
})
|
||||
return None
|
||||
|
||||
def after_hook(context: ToolCallHookContext) -> str | None:
|
||||
hook_calls["after"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_result": context.tool_result,
|
||||
"has_agent": context.agent is not None,
|
||||
"has_task": context.task is not None,
|
||||
"has_crew": context.crew is not None,
|
||||
})
|
||||
return None
|
||||
|
||||
register_before_tool_call_hook(before_hook)
|
||||
register_after_tool_call_hook(after_hook)
|
||||
|
||||
try:
|
||||
agent = Agent(
|
||||
role="Math Assistant",
|
||||
goal="Perform division calculations accurately",
|
||||
backstory="You are a math assistant that helps with division",
|
||||
tools=[divide_numbers],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Calculate 100 divided by 4 using the divide_numbers tool.",
|
||||
expected_output="The result of the division",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
crew.kickoff()
|
||||
|
||||
# Verify before hook was called with full context
|
||||
assert len(hook_calls["before"]) > 0, "Before hook was never called"
|
||||
before_call = hook_calls["before"][0]
|
||||
assert before_call["tool_name"] == "divide_numbers"
|
||||
assert "a" in before_call["tool_input"]
|
||||
assert "b" in before_call["tool_input"]
|
||||
assert before_call["has_agent"] is True
|
||||
assert before_call["has_task"] is True
|
||||
assert before_call["has_crew"] is True
|
||||
assert before_call["agent_role"] == "Math Assistant"
|
||||
|
||||
# Verify after hook was called with full context
|
||||
assert len(hook_calls["after"]) > 0, "After hook was never called"
|
||||
after_call = hook_calls["after"][0]
|
||||
assert after_call["tool_name"] == "divide_numbers"
|
||||
assert "25" in str(after_call["tool_result"])
|
||||
assert after_call["has_agent"] is True
|
||||
assert after_call["has_task"] is True
|
||||
assert after_call["has_crew"] is True
|
||||
|
||||
finally:
|
||||
unregister_before_tool_call_hook(before_hook)
|
||||
unregister_after_tool_call_hook(after_hook)
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_before_hook_blocks_tool_execution_in_crew(self):
|
||||
"""Test that returning False from before hook blocks tool execution."""
|
||||
import os
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai.tools import tool
|
||||
|
||||
hook_calls = {"before": [], "after": [], "tool_executed": False}
|
||||
|
||||
@tool("dangerous_operation")
|
||||
def dangerous_operation(action: str) -> str:
|
||||
"""Perform a dangerous operation that should be blocked."""
|
||||
hook_calls["tool_executed"] = True
|
||||
return f"Executed: {action}"
|
||||
|
||||
def blocking_before_hook(context: ToolCallHookContext) -> bool | None:
|
||||
hook_calls["before"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_input": dict(context.tool_input),
|
||||
})
|
||||
# Block all calls to dangerous_operation
|
||||
if context.tool_name == "dangerous_operation":
|
||||
return False
|
||||
return None
|
||||
|
||||
def after_hook(context: ToolCallHookContext) -> str | None:
|
||||
hook_calls["after"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_result": context.tool_result,
|
||||
})
|
||||
return None
|
||||
|
||||
register_before_tool_call_hook(blocking_before_hook)
|
||||
register_after_tool_call_hook(after_hook)
|
||||
|
||||
try:
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Try to use the dangerous operation tool",
|
||||
backstory="You are a test agent",
|
||||
tools=[dangerous_operation],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Use the dangerous_operation tool with action 'delete_all'.",
|
||||
expected_output="The result of the operation",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
crew.kickoff()
|
||||
|
||||
# Verify before hook was called
|
||||
assert len(hook_calls["before"]) > 0, "Before hook was never called"
|
||||
before_call = hook_calls["before"][0]
|
||||
assert before_call["tool_name"] == "dangerous_operation"
|
||||
|
||||
# Verify the actual tool function was NOT executed
|
||||
assert hook_calls["tool_executed"] is False, "Tool should have been blocked"
|
||||
|
||||
# Verify after hook was still called (with blocked message)
|
||||
assert len(hook_calls["after"]) > 0, "After hook was never called"
|
||||
after_call = hook_calls["after"][0]
|
||||
assert "blocked" in after_call["tool_result"].lower()
|
||||
|
||||
finally:
|
||||
unregister_before_tool_call_hook(blocking_before_hook)
|
||||
unregister_after_tool_call_hook(after_hook)
|
||||
|
||||
Reference in New Issue
Block a user