This fixes issue #2826 where tool inputs were being modified when using different LLM providers. The fix handles nested dictionaries with 'value' fields for all parameter types, ensuring string inputs are correctly extracted from nested dictionaries.

Co-Authored-By: Joe Moura <joao@crewai.com>
This commit is contained in:
Devin AI
2025-05-14 00:18:56 +00:00
parent fed397f745
commit aac875508d
2 changed files with 150 additions and 0 deletions

View File

@@ -187,6 +187,16 @@ class CrewStructuredTool:
except json.JSONDecodeError as e:
raise ValueError(f"Failed to parse arguments as JSON: {e}")
# Handle nested dictionaries with 'value' field for all parameter types
if isinstance(raw_args, dict):
schema_fields = self.args_schema.model_fields
for field_name, field_value in list(raw_args.items()):
# Check if this field exists in the schema
if field_name in schema_fields:
if (isinstance(field_value, dict) and 'value' in field_value):
raw_args[field_name] = field_value['value']
try:
validated_args = self.args_schema.model_validate(raw_args)
return validated_args.model_dump()

View File

@@ -0,0 +1,140 @@
import pytest
from pydantic import BaseModel, Field
from typing import Dict, Any, Union
from crewai.tools.structured_tool import CrewStructuredTool
class StringInputSchema(BaseModel):
"""Schema with a string input field."""
query: str = Field(description="A string input parameter")
class IntInputSchema(BaseModel):
"""Schema with an integer input field."""
number: int = Field(description="An integer input parameter")
class ComplexInputSchema(BaseModel):
"""Schema with multiple fields of different types."""
text: str = Field(description="A string parameter")
number: int = Field(description="An integer parameter")
flag: bool = Field(description="A boolean parameter")
def test_parse_args_with_string_input():
"""Test that string inputs are parsed correctly."""
def test_func(query: str) -> str:
return f"Processed: {query}"
tool = CrewStructuredTool.from_function(
func=test_func,
name="StringTool",
description="A tool that processes string input"
)
# Test with direct string input
result = tool._parse_args({"query": "test string"})
assert result["query"] == "test string"
assert isinstance(result["query"], str)
# Test with JSON string input
result = tool._parse_args('{"query": "json string"}')
assert result["query"] == "json string"
assert isinstance(result["query"], str)
def test_parse_args_with_nested_dict_for_string():
"""Test that nested dictionaries with 'value' field are handled correctly for string fields."""
def test_func(query: str) -> str:
return f"Processed: {query}"
tool = CrewStructuredTool.from_function(
func=test_func,
name="StringTool",
description="A tool that processes string input"
)
# Test with nested dict input (simulating the issue from different LLM providers)
nested_input = {"query": {"description": "A string input parameter", "value": "test value"}}
result = tool._parse_args(nested_input)
assert result["query"] == "test value"
assert isinstance(result["query"], str)
def test_parse_args_with_nested_dict_for_int():
"""Test that nested dictionaries with 'value' field are handled correctly for int fields."""
def test_func(number: int) -> str:
return f"Processed: {number}"
tool = CrewStructuredTool.from_function(
func=test_func,
name="IntTool",
description="A tool that processes integer input"
)
# Test with nested dict input for int field
nested_input = {"number": {"description": "An integer input parameter", "value": 42}}
result = tool._parse_args(nested_input)
assert result["number"] == 42
assert isinstance(result["number"], int)
def test_parse_args_with_complex_input():
"""Test that complex inputs with multiple fields are handled correctly."""
def test_func(text: str, number: int, flag: bool) -> str:
return f"Processed: {text}, {number}, {flag}"
tool = CrewStructuredTool.from_function(
func=test_func,
name="ComplexTool",
description="A tool that processes complex input"
)
# Test with mixed nested dict input
complex_input = {
"text": {"description": "A string parameter", "value": "test text"},
"number": 42,
"flag": True
}
result = tool._parse_args(complex_input)
assert result["text"] == "test text"
assert isinstance(result["text"], str)
assert result["number"] == 42
assert isinstance(result["number"], int)
assert result["flag"] is True
assert isinstance(result["flag"], bool)
def test_invoke_with_nested_dict():
"""Test that invoking a tool with nested dict input works correctly."""
def test_func(query: str) -> str:
return f"Processed: {query}"
tool = CrewStructuredTool.from_function(
func=test_func,
name="StringTool",
description="A tool that processes string input"
)
# Test invoking with nested dict input
nested_input = {"query": {"description": "A string input parameter", "value": "test value"}}
result = tool.invoke(nested_input)
assert result == "Processed: test value"
def test_nested_dict_without_value_key():
"""Test that nested dictionaries without 'value' field raise appropriate errors."""
def test_func(query: str) -> str:
return f"Processed: {query}"
tool = CrewStructuredTool.from_function(
func=test_func,
name="StringTool",
description="A tool that processes string input"
)
# Test with nested dict without 'value' key
invalid_input = {"query": {"description": "A string input parameter", "other_key": "test"}}
with pytest.raises(ValueError):
tool._parse_args(invalid_input)