From cf2a1346fd7601bb3a2c920198bfda311a5b3b2c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 22:17:34 +0000 Subject: [PATCH] Fix issue 2288: Handle list inputs in tool_usage._validate_tool_input Co-Authored-By: Joe Moura --- src/crewai/tools/tool_usage.py | 32 ++++++++-- tests/tools/test_tool_input_validation.py | 74 +++++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 tests/tools/test_tool_input_validation.py diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py index 25e4b126a..3ad41f15d 100644 --- a/src/crewai/tools/tool_usage.py +++ b/src/crewai/tools/tool_usage.py @@ -432,7 +432,13 @@ class ToolUsage: # Attempt 1: Parse as JSON try: arguments = json.loads(tool_input) - if isinstance(arguments, dict): + # Handle case where arguments is a list + if isinstance(arguments, list) and len(arguments) > 0 and isinstance(arguments[0], dict): + self._printer.print( + content=f"Tool input is a list, extracting first element: {arguments[0]}", color="blue" + ) + return arguments[0] + elif isinstance(arguments, dict): return arguments except (JSONDecodeError, TypeError): pass # Continue to the next parsing attempt @@ -440,7 +446,13 @@ class ToolUsage: # Attempt 2: Parse as Python literal try: arguments = ast.literal_eval(tool_input) - if isinstance(arguments, dict): + # Handle case where arguments is a list + if isinstance(arguments, list) and len(arguments) > 0 and isinstance(arguments[0], dict): + self._printer.print( + content=f"Tool input is a list, extracting first element: {arguments[0]}", color="blue" + ) + return arguments[0] + elif isinstance(arguments, dict): return arguments except (ValueError, SyntaxError): pass # Continue to the next parsing attempt @@ -448,7 +460,13 @@ class ToolUsage: # Attempt 3: Parse as JSON5 try: arguments = json5.loads(tool_input) - if isinstance(arguments, dict): + # Handle case where arguments is a list + if isinstance(arguments, list) and len(arguments) > 0 and isinstance(arguments[0], dict): + self._printer.print( + content=f"Tool input is a list, extracting first element: {arguments[0]}", color="blue" + ) + return arguments[0] + elif isinstance(arguments, dict): return arguments except (JSONDecodeError, ValueError, TypeError): pass # Continue to the next parsing attempt @@ -460,7 +478,13 @@ class ToolUsage: content=f"Repaired JSON: {repaired_input}", color="blue" ) arguments = json.loads(repaired_input) - if isinstance(arguments, dict): + # Handle case where arguments is a list + if isinstance(arguments, list) and len(arguments) > 0 and isinstance(arguments[0], dict): + self._printer.print( + content=f"Tool input is a list, extracting first element: {arguments[0]}", color="blue" + ) + return arguments[0] + elif isinstance(arguments, dict): return arguments except Exception as e: error = f"Failed to repair JSON: {e}" diff --git a/tests/tools/test_tool_input_validation.py b/tests/tools/test_tool_input_validation.py new file mode 100644 index 000000000..56a4bffd7 --- /dev/null +++ b/tests/tools/test_tool_input_validation.py @@ -0,0 +1,74 @@ +import pytest +from crewai.tools.tool_usage import ToolUsage +from unittest.mock import MagicMock, patch + + +class TestToolInputValidation: + def setup_method(self): + # Create mock objects for testing + self.mock_tools_handler = MagicMock() + self.mock_tools = [MagicMock()] + self.mock_original_tools = [MagicMock()] + self.mock_tools_description = "Mock tools description" + self.mock_tools_names = "Mock tools names" + self.mock_task = MagicMock() + self.mock_function_calling_llm = MagicMock() + + # Create mock agent with required string attributes + self.mock_agent = MagicMock() + self.mock_agent.key = "mock_agent_key" + self.mock_agent.role = "mock_agent_role" + self.mock_agent._original_role = "mock_original_role" + + # Create mock action with required string attributes + self.mock_action = MagicMock() + self.mock_action.tool = "mock_tool_name" + self.mock_action.tool_input = "mock_tool_input" + + # Create ToolUsage instance + self.tool_usage = ToolUsage( + tools_handler=self.mock_tools_handler, + tools=self.mock_tools, + original_tools=self.mock_original_tools, + tools_description=self.mock_tools_description, + tools_names=self.mock_tools_names, + task=self.mock_task, + function_calling_llm=self.mock_function_calling_llm, + agent=self.mock_agent, + action=self.mock_action, + ) + + # Patch the _emit_validate_input_error method to avoid event emission + self.original_emit_validate_input_error = self.tool_usage._emit_validate_input_error + self.tool_usage._emit_validate_input_error = MagicMock() + + def teardown_method(self): + # Restore the original method + if hasattr(self, 'original_emit_validate_input_error'): + self.tool_usage._emit_validate_input_error = self.original_emit_validate_input_error + + def test_validate_tool_input_with_dict(self): + # Test with a valid dictionary input + tool_input = '{"ticker": "VST"}' + result = self.tool_usage._validate_tool_input(tool_input) + assert result == {"ticker": "VST"} + + def test_validate_tool_input_with_list(self): + # Test with a list input containing a dictionary as the first element + tool_input = '[{"ticker": "VST"}, {"tool_code": "Stock Info", "tool_input": {"ticker": "VST"}}]' + result = self.tool_usage._validate_tool_input(tool_input) + assert result == {"ticker": "VST"} + + def test_validate_tool_input_with_empty_list(self): + # Test with an empty list input + tool_input = '[]' + with pytest.raises(Exception) as excinfo: + self.tool_usage._validate_tool_input(tool_input) + assert "Tool input must be a valid dictionary in JSON or Python literal format" in str(excinfo.value) + + def test_validate_tool_input_with_list_of_non_dicts(self): + # Test with a list input containing non-dictionary elements + tool_input = '["not a dict", 123]' + with pytest.raises(Exception) as excinfo: + self.tool_usage._validate_tool_input(tool_input) + assert "Tool input must be a valid dictionary in JSON or Python literal format" in str(excinfo.value)