Repository: crewAIInc/crewAI
Pull request #2477 (2025-03-26 13:06:31)

feat: add output to ToolUsageFinishedEvent


Patch:
diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py
index 9c924027..edfedb4b 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 aa375dcd..e5027832 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"
 
 


Test patch:
diff --git a/tests/tools/test_tool_usage.py b/tests/tools/test_tool_usage.py
index e09d4d53..9cf9ae1d 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"