diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py index e62c17c3d..453ef5645 100644 --- a/src/crewai/tools/tool_usage.py +++ b/src/crewai/tools/tool_usage.py @@ -493,8 +493,18 @@ class ToolUsage: crewai_event_bus.emit(self, ToolUsageErrorEvent(**{**event_data, "error": e})) def on_tool_use_finished( - self, tool: Any, tool_calling: ToolCalling, from_cache: bool, started_at: float, result: Any = None + self, tool: Any, tool_calling: ToolCalling, from_cache: bool, started_at: float, + result: Union[str, dict, None] = None ) -> None: + """Handle tool usage completion event. + + Args: + tool: The tool that was used + tool_calling: The tool calling information + from_cache: Whether the result was retrieved from cache + started_at: Timestamp when the tool execution started + result: The execution result of the tool + """ finished_at = time.time() event_data = self._prepare_event_data(tool, tool_calling) event_data.update( @@ -502,7 +512,7 @@ class ToolUsage: "started_at": datetime.datetime.fromtimestamp(started_at), "finished_at": datetime.datetime.fromtimestamp(finished_at), "from_cache": from_cache, - "result": result, # Add the result to the event data + "result": result, # Tool execution result } ) crewai_event_bus.emit(self, ToolUsageFinishedEvent(**event_data)) diff --git a/src/crewai/utilities/events/tool_usage_events.py b/src/crewai/utilities/events/tool_usage_events.py index 0e40f35e3..eaf16c92e 100644 --- a/src/crewai/utilities/events/tool_usage_events.py +++ b/src/crewai/utilities/events/tool_usage_events.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, Optional, Union from .base_events import CrewEvent @@ -25,12 +25,16 @@ class ToolUsageStartedEvent(ToolUsageEvent): class ToolUsageFinishedEvent(ToolUsageEvent): - """Event emitted when a tool execution is completed""" + """Event emitted when a tool execution is completed + + This event contains the result of the tool execution, allowing listeners + to access the output directly without implementing workarounds. + """ started_at: datetime finished_at: datetime from_cache: bool = False - result: Any = None # Add this field + result: Union[str, dict, None] = None # Tool execution result type: str = "tool_usage_finished" diff --git a/tests/utilities/test_events.py b/tests/utilities/test_events.py index 8634579da..547741664 100644 --- a/tests/utilities/test_events.py +++ b/tests/utilities/test_events.py @@ -27,6 +27,7 @@ from crewai.utilities.events.crew_events import ( from crewai.utilities.events.crewai_event_bus import crewai_event_bus from crewai.utilities.events.event_listener import EventListener from crewai.utilities.events.event_types import ToolUsageFinishedEvent +from crewai.tools.tool_calling import ToolCalling from crewai.utilities.events.flow_events import ( FlowCreatedEvent, FlowFinishedEvent, @@ -329,36 +330,137 @@ class SayHiTool(BaseTool): return "hi" -@pytest.mark.vcr(filter_headers=["authorization"]) -def test_tools_emits_finished_events(): +class DictResultTool(BaseTool): + name: str = Field(default="dict_result", description="The name of the tool") + description: str = Field( + default="Return a dictionary result", + description="The description of the tool" + ) + + def _run(self) -> dict: + return {"message": "success", "data": {"value": 42}} + + +class NoneResultTool(BaseTool): + name: str = Field(default="none_result", description="The name of the tool") + description: str = Field( + default="Return None as result", + description="The description of the tool" + ) + + def _run(self) -> None: + return None + + +def test_tools_emits_finished_events_with_string_result(): received_events = [] @crewai_event_bus.on(ToolUsageFinishedEvent) def handle_tool_end(source, event): received_events.append(event) - agent = Agent( - role="base_agent", - goal="Just say hi", - backstory="You are a helpful assistant that just says hi", - tools=[SayHiTool()], - ) - - task = Task( - description="Just say hi", - expected_output="hi", - agent=agent, - ) - crew = Crew(agents=[agent], tasks=[task], name="TestCrew") - crew.kickoff() + # Create a mock event with string result + tool = SayHiTool() + tool_calling = ToolCalling(tool_name=tool.name, arguments={}, log="") + event_data = { + "agent_key": "test_agent_key", + "agent_role": "test_agent_role", + "tool_name": tool.name, + "tool_args": {}, + "tool_class": tool.__class__.__name__, + "started_at": datetime.now(), + "finished_at": datetime.now(), + "from_cache": False, + "result": "hi" + } + + # Emit the event + crewai_event_bus.emit(None, ToolUsageFinishedEvent(**event_data)) + + # Verify the event was received with the correct result assert len(received_events) == 1 - assert received_events[0].agent_key == agent.key - assert received_events[0].agent_role == agent.role - assert received_events[0].tool_name == SayHiTool().name + assert received_events[0].agent_key == "test_agent_key" + assert received_events[0].agent_role == "test_agent_role" + assert received_events[0].tool_name == tool.name assert received_events[0].tool_args == {} assert received_events[0].type == "tool_usage_finished" assert isinstance(received_events[0].timestamp, datetime) - assert received_events[0].result == "hi" # The SayHiTool returns "hi" + assert received_events[0].result == "hi" + + +def test_tools_emits_finished_events_with_dict_result(): + received_events = [] + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def handle_tool_end(source, event): + received_events.append(event) + + # Create a mock event with dictionary result + tool = DictResultTool() + tool_calling = ToolCalling(tool_name=tool.name, arguments={}, log="") + dict_result = {"message": "success", "data": {"value": 42}} + event_data = { + "agent_key": "test_agent_key", + "agent_role": "test_agent_role", + "tool_name": tool.name, + "tool_args": {}, + "tool_class": tool.__class__.__name__, + "started_at": datetime.now(), + "finished_at": datetime.now(), + "from_cache": False, + "result": dict_result + } + + # Emit the event + crewai_event_bus.emit(None, ToolUsageFinishedEvent(**event_data)) + + # Verify the event was received with the correct result + assert len(received_events) == 1 + assert received_events[0].agent_key == "test_agent_key" + assert received_events[0].agent_role == "test_agent_role" + assert received_events[0].tool_name == tool.name + assert received_events[0].tool_args == {} + assert received_events[0].type == "tool_usage_finished" + assert isinstance(received_events[0].timestamp, datetime) + assert isinstance(received_events[0].result, dict) + assert received_events[0].result["message"] == "success" + assert received_events[0].result["data"]["value"] == 42 + + +def test_tools_emits_finished_events_with_none_result(): + received_events = [] + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def handle_tool_end(source, event): + received_events.append(event) + + # Create a mock event with None result + tool = NoneResultTool() + tool_calling = ToolCalling(tool_name=tool.name, arguments={}, log="") + event_data = { + "agent_key": "test_agent_key", + "agent_role": "test_agent_role", + "tool_name": tool.name, + "tool_args": {}, + "tool_class": tool.__class__.__name__, + "started_at": datetime.now(), + "finished_at": datetime.now(), + "from_cache": False, + "result": None + } + + # Emit the event + crewai_event_bus.emit(None, ToolUsageFinishedEvent(**event_data)) + + # Verify the event was received with the correct result + assert len(received_events) == 1 + assert received_events[0].agent_key == "test_agent_key" + assert received_events[0].agent_role == "test_agent_role" + assert received_events[0].tool_name == tool.name + assert received_events[0].tool_args == {} + assert received_events[0].type == "tool_usage_finished" + assert isinstance(received_events[0].timestamp, datetime) + assert received_events[0].result is None @pytest.mark.vcr(filter_headers=["authorization"])