diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py index 9c924027d..edfedb4b8 100644 --- a/src/crewai/tools/tool_usage.py +++ b/src/crewai/tools/tool_usage.py @@ -117,7 +117,10 @@ class ToolUsage: self._printer.print(content=f"\n\n{error}\n", color="red") return error - if isinstance(tool, CrewStructuredTool) and tool.name == self._i18n.tools("add_image")["name"]: # type: ignore + if ( + isinstance(tool, CrewStructuredTool) + and tool.name == self._i18n.tools("add_image")["name"] # type: ignore + ): try: result = self._use(tool_string=tool_string, tool=tool, calling=calling) return result @@ -181,7 +184,9 @@ class ToolUsage: if calling.arguments: try: - acceptable_args = tool.args_schema.model_json_schema()["properties"].keys() # type: ignore + acceptable_args = tool.args_schema.model_json_schema()[ + "properties" + ].keys() # type: ignore arguments = { k: v for k, v in calling.arguments.items() @@ -202,7 +207,7 @@ class ToolUsage: error=e, tool=tool.name, tool_inputs=tool.description ) error = ToolUsageErrorException( - f'\n{error_message}.\nMoving on then. {self._i18n.slice("format").format(tool_names=self.tools_names)}' + f"\n{error_message}.\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}" ).message self.task.increment_tools_errors() if self.agent.verbose: @@ -244,6 +249,7 @@ class ToolUsage: tool_calling=calling, from_cache=from_cache, started_at=started_at, + result=result, ) if ( @@ -380,7 +386,7 @@ class ToolUsage: raise else: return ToolUsageErrorException( - f'{self._i18n.errors("tool_arguments_error")}' + f"{self._i18n.errors('tool_arguments_error')}" ) if not isinstance(arguments, dict): @@ -388,7 +394,7 @@ class ToolUsage: raise else: return ToolUsageErrorException( - f'{self._i18n.errors("tool_arguments_error")}' + f"{self._i18n.errors('tool_arguments_error')}" ) return ToolCalling( @@ -416,7 +422,7 @@ class ToolUsage: if self.agent.verbose: self._printer.print(content=f"\n\n{e}\n", color="red") return ToolUsageErrorException( # type: ignore # Incompatible return value type (got "ToolUsageErrorException", expected "ToolCalling | InstructorToolCalling") - f'{self._i18n.errors("tool_usage_error").format(error=e)}\nMoving on then. {self._i18n.slice("format").format(tool_names=self.tools_names)}' + f"{self._i18n.errors('tool_usage_error').format(error=e)}\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}" ) return self._tool_calling(tool_string) @@ -492,7 +498,12 @@ 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 + self, + tool: Any, + tool_calling: ToolCalling, + from_cache: bool, + started_at: float, + result: Any, ) -> None: finished_at = time.time() event_data = self._prepare_event_data(tool, tool_calling) @@ -501,6 +512,7 @@ class ToolUsage: "started_at": datetime.datetime.fromtimestamp(started_at), "finished_at": datetime.datetime.fromtimestamp(finished_at), "from_cache": from_cache, + "output": 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 aa375dcd7..e5027832c 100644 --- a/src/crewai/utilities/events/tool_usage_events.py +++ b/src/crewai/utilities/events/tool_usage_events.py @@ -30,6 +30,7 @@ class ToolUsageFinishedEvent(ToolUsageEvent): started_at: datetime finished_at: datetime from_cache: bool = False + output: Any type: str = "tool_usage_finished" diff --git a/tests/tools/test_tool_usage.py b/tests/tools/test_tool_usage.py index e09d4d537..9cf9ae1d4 100644 --- a/tests/tools/test_tool_usage.py +++ b/tests/tools/test_tool_usage.py @@ -1,5 +1,7 @@ +import datetime import json import random +import time from unittest.mock import MagicMock, patch import pytest @@ -11,6 +13,7 @@ from crewai.tools.tool_usage import ToolUsage from crewai.utilities.events import crewai_event_bus from crewai.utilities.events.tool_usage_events import ( ToolSelectionErrorEvent, + ToolUsageFinishedEvent, ToolValidateInputErrorEvent, ) @@ -624,3 +627,161 @@ def test_tool_validate_input_error_event(): assert event.agent_role == "test_role" assert event.tool_name == "test_tool" assert "must be a valid dictionary" in event.error + + +def test_tool_usage_finished_event_with_result(): + """Test that ToolUsageFinishedEvent is emitted with correct result attributes.""" + # Create mock agent with proper string values + mock_agent = MagicMock() + mock_agent.key = "test_agent_key" + mock_agent.role = "test_agent_role" + mock_agent._original_role = "test_agent_role" + mock_agent.i18n = MagicMock() + mock_agent.verbose = False + + # Create mock task + mock_task = MagicMock() + mock_task.delegations = 0 + + # Create mock tool + class TestTool(BaseTool): + name: str = "Test Tool" + description: str = "A test tool" + + def _run(self, input: dict) -> str: + return "test result" + + test_tool = TestTool() + + # Create mock tool calling + mock_tool_calling = MagicMock() + mock_tool_calling.arguments = {"arg1": "value1"} + + # Create ToolUsage instance + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[test_tool], + original_tools=[test_tool], + tools_description="Test Tool Description", + tools_names="Test Tool", + task=mock_task, + function_calling_llm=None, + agent=mock_agent, + action=MagicMock(), + ) + + # Track received events + received_events = [] + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def event_handler(source, event): + received_events.append(event) + + # Call on_tool_use_finished with test data + started_at = time.time() + result = "test output result" + tool_usage.on_tool_use_finished( + tool=test_tool, + tool_calling=mock_tool_calling, + from_cache=False, + started_at=started_at, + result=result, + ) + + # Verify event was emitted + assert len(received_events) == 1, "Expected one event to be emitted" + event = received_events[0] + assert isinstance(event, ToolUsageFinishedEvent) + + # Verify event attributes + assert event.agent_key == "test_agent_key" + assert event.agent_role == "test_agent_role" + assert event.tool_name == "Test Tool" + assert event.tool_args == {"arg1": "value1"} + assert event.tool_class == "TestTool" + assert event.run_attempts == 1 # Default value from ToolUsage + assert event.delegations == 0 + assert event.from_cache is False + assert event.output == "test output result" + assert isinstance(event.started_at, datetime.datetime) + assert isinstance(event.finished_at, datetime.datetime) + assert event.type == "tool_usage_finished" + + +def test_tool_usage_finished_event_with_cached_result(): + """Test that ToolUsageFinishedEvent is emitted with correct result attributes when using cached result.""" + # Create mock agent with proper string values + mock_agent = MagicMock() + mock_agent.key = "test_agent_key" + mock_agent.role = "test_agent_role" + mock_agent._original_role = "test_agent_role" + mock_agent.i18n = MagicMock() + mock_agent.verbose = False + + # Create mock task + mock_task = MagicMock() + mock_task.delegations = 0 + + # Create mock tool + class TestTool(BaseTool): + name: str = "Test Tool" + description: str = "A test tool" + + def _run(self, input: dict) -> str: + return "test result" + + test_tool = TestTool() + + # Create mock tool calling + mock_tool_calling = MagicMock() + mock_tool_calling.arguments = {"arg1": "value1"} + + # Create ToolUsage instance + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[test_tool], + original_tools=[test_tool], + tools_description="Test Tool Description", + tools_names="Test Tool", + task=mock_task, + function_calling_llm=None, + agent=mock_agent, + action=MagicMock(), + ) + + # Track received events + received_events = [] + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def event_handler(source, event): + received_events.append(event) + + # Call on_tool_use_finished with test data and from_cache=True + started_at = time.time() + result = "cached test output result" + tool_usage.on_tool_use_finished( + tool=test_tool, + tool_calling=mock_tool_calling, + from_cache=True, + started_at=started_at, + result=result, + ) + + # Verify event was emitted + assert len(received_events) == 1, "Expected one event to be emitted" + event = received_events[0] + assert isinstance(event, ToolUsageFinishedEvent) + + # Verify event attributes + assert event.agent_key == "test_agent_key" + assert event.agent_role == "test_agent_role" + assert event.tool_name == "Test Tool" + assert event.tool_args == {"arg1": "value1"} + assert event.tool_class == "TestTool" + assert event.run_attempts == 1 # Default value from ToolUsage + assert event.delegations == 0 + assert event.from_cache is True + assert event.output == "cached test output result" + assert isinstance(event.started_at, datetime.datetime) + assert isinstance(event.finished_at, datetime.datetime) + assert event.type == "tool_usage_finished"