Compare commits

...

1 Commits

Author SHA1 Message Date
Devin AI
9900a79546 fix: Bedrock tool calls extract empty arguments - uses wrong field name
Fix argument extraction for AWS Bedrock tool calls in
_parse_native_tool_call. The old code used
func_info.get('arguments', '{}') which always returns the truthy
string '{}', preventing the 'or' fallback from ever reaching
tool_call.get('input', {}). This caused all Bedrock tool calls to
receive empty arguments regardless of what the LLM provides.

Changed to func_info.get('arguments') or tool_call.get('input') or {}
which correctly falls through to the Bedrock 'input' field when
'arguments' is not present.

Fixes #4748

Co-Authored-By: João <joao@crewai.com>
2026-03-06 15:44:10 +00:00
3 changed files with 223 additions and 2 deletions

View File

@@ -847,7 +847,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
func_name = sanitize_tool_name(
func_info.get("name", "") or tool_call.get("name", "")
)
func_args = func_info.get("arguments", "{}") or tool_call.get("input", {})
func_args = func_info.get("arguments") or tool_call.get("input") or {}
return call_id, func_name, func_args
return None

View File

@@ -331,6 +331,97 @@ class TestInvokeStepCallback:
executor._invoke_step_callback(answer)
class TestParseNativeToolCall:
"""Tests for _parse_native_tool_call covering multiple provider formats.
Regression tests for issue #4748: Bedrock tool calls with 'input' field
were returning empty arguments because the old code used
``func_info.get("arguments", "{}")`` which always returns a truthy
default, preventing the ``or`` fallback to ``tool_call.get("input")``.
"""
def test_openai_dict_format(self, executor: CrewAgentExecutor) -> None:
"""Test OpenAI dict format with function.arguments."""
tool_call = {
"id": "call_abc123",
"function": {
"name": "search_tool",
"arguments": '{"query": "test"}',
},
}
result = executor._parse_native_tool_call(tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "call_abc123"
assert func_name == "search_tool"
assert func_args == '{"query": "test"}'
def test_bedrock_dict_format_extracts_input(self, executor: CrewAgentExecutor) -> None:
"""Test AWS Bedrock dict format extracts 'input' field for arguments."""
tool_call = {
"toolUseId": "tooluse_xyz789",
"name": "search_tool",
"input": {"query": "AWS Bedrock features"},
}
result = executor._parse_native_tool_call(tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "tooluse_xyz789"
assert func_name == "search_tool"
assert func_args == {"query": "AWS Bedrock features"}
def test_bedrock_dict_format_not_empty_regression(self, executor: CrewAgentExecutor) -> None:
"""Regression test for #4748: Bedrock args must NOT be empty.
Before the fix, ``func_info.get("arguments", "{}")`` returned the
truthy string ``"{}"`` which short-circuited the ``or`` operator,
so ``tool_call.get("input", {})`` was never evaluated.
"""
tool_call = {
"toolUseId": "tooluse_regression",
"name": "search_tool",
"input": {"query": "important query", "limit": 5},
}
result = executor._parse_native_tool_call(tool_call)
assert result is not None
_, _, func_args = result
assert func_args == {"query": "important query", "limit": 5}
assert func_args != {}
assert func_args != "{}"
def test_dict_without_function_or_input_returns_empty(self, executor: CrewAgentExecutor) -> None:
"""Test dict format with neither function.arguments nor input."""
tool_call = {
"id": "call_noop",
"name": "no_args_tool",
}
result = executor._parse_native_tool_call(tool_call)
assert result is not None
_, func_name, func_args = result
assert func_name == "no_args_tool"
assert func_args == {}
def test_openai_arguments_preferred_over_input(self, executor: CrewAgentExecutor) -> None:
"""Test that function.arguments takes precedence over input."""
tool_call = {
"id": "call_both",
"function": {
"name": "dual_tool",
"arguments": '{"from": "openai"}',
},
"input": {"from": "bedrock"},
}
result = executor._parse_native_tool_call(tool_call)
assert result is not None
_, _, func_args = result
assert func_args == '{"from": "openai"}'
def test_returns_none_for_unrecognized(self, executor: CrewAgentExecutor) -> None:
"""Test that None is returned for unrecognized tool call formats."""
result = executor._parse_native_tool_call(12345)
assert result is None
class TestAsyncLLMResponseHelper:
"""Tests for aget_llm_response helper function."""
@@ -385,4 +476,4 @@ class TestAsyncLLMResponseHelper:
messages=[{"role": "user", "content": "test"}],
callbacks=[],
printer=Printer(),
)
)

View File

@@ -17,6 +17,7 @@ from crewai.utilities.agent_utils import (
_format_messages_for_summary,
_split_messages_into_chunks,
convert_tools_to_openai_schema,
extract_tool_call_info,
parse_tool_call_args,
summarize_messages,
)
@@ -1049,3 +1050,132 @@ class TestParseToolCallArgs:
_, error = parse_tool_call_args("{bad json}", "tool", "call_7")
assert error is not None
assert set(error.keys()) == {"call_id", "func_name", "result", "from_cache", "original_tool"}
class TestExtractToolCallInfo:
"""Tests for extract_tool_call_info covering multiple provider formats."""
def test_openai_dict_format(self) -> None:
"""Test OpenAI dict format with function.arguments."""
tool_call = {
"id": "call_abc123",
"function": {
"name": "search_tool",
"arguments": '{"query": "test"}',
},
}
result = extract_tool_call_info(tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "call_abc123"
assert func_name == "search_tool"
assert func_args == '{"query": "test"}'
def test_bedrock_dict_format_with_input(self) -> None:
"""Test AWS Bedrock dict format uses 'input' field for arguments."""
tool_call = {
"toolUseId": "tooluse_xyz789",
"name": "search_tool",
"input": {"query": "AWS Bedrock features"},
}
result = extract_tool_call_info(tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "tooluse_xyz789"
assert func_name == "search_tool"
assert func_args == {"query": "AWS Bedrock features"}
def test_bedrock_dict_format_does_not_return_empty_args(self) -> None:
"""Test that Bedrock format does not silently return empty args.
This is the core regression test for issue #4748: when the dict has no
'function' key but does have 'input', the arguments must come from
'input' rather than defaulting to an empty dict/string.
"""
tool_call = {
"toolUseId": "tooluse_abc",
"name": "search_tool",
"input": {"query": "important query", "limit": 5},
}
result = extract_tool_call_info(tool_call)
assert result is not None
_, _, func_args = result
# Must NOT be empty — the bug was that func_args came back as "{}"
assert func_args == {"query": "important query", "limit": 5}
def test_dict_format_without_function_or_input_returns_empty(self) -> None:
"""Test dict format with neither function.arguments nor input."""
tool_call = {
"id": "call_noop",
"name": "no_args_tool",
}
result = extract_tool_call_info(tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "call_noop"
assert func_name == "no_args_tool"
assert func_args == {}
def test_openai_dict_arguments_preferred_over_input(self) -> None:
"""Test that function.arguments takes precedence over input when both exist."""
tool_call = {
"id": "call_both",
"function": {
"name": "dual_tool",
"arguments": '{"from": "openai"}',
},
"input": {"from": "bedrock"},
}
result = extract_tool_call_info(tool_call)
assert result is not None
_, _, func_args = result
assert func_args == '{"from": "openai"}'
def test_dict_format_generates_id_when_missing(self) -> None:
"""Test that a call ID is generated when neither id nor toolUseId exist."""
tool_call = {
"name": "some_tool",
"input": {"key": "value"},
}
result = extract_tool_call_info(tool_call)
assert result is not None
call_id, _, _ = result
assert call_id.startswith("call_")
def test_returns_none_for_unrecognized_format(self) -> None:
"""Test that None is returned for unrecognized tool call formats."""
result = extract_tool_call_info(12345)
assert result is None
def test_openai_object_format(self) -> None:
"""Test OpenAI object format with .function attribute."""
mock_function = MagicMock()
mock_function.name = "calculator"
mock_function.arguments = '{"expression": "2+2"}'
mock_tool_call = MagicMock()
mock_tool_call.function = mock_function
mock_tool_call.id = "call_obj_123"
# Ensure it doesn't have function_call attribute
del mock_tool_call.function_call
result = extract_tool_call_info(mock_tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "call_obj_123"
assert func_name == "calculator"
assert func_args == '{"expression": "2+2"}'
def test_anthropic_object_format(self) -> None:
"""Test Anthropic ToolUseBlock format with .name and .input attributes."""
mock_tool_call = MagicMock(spec=["name", "input", "id"])
mock_tool_call.name = "search"
mock_tool_call.input = {"query": "hello"}
mock_tool_call.id = "toolu_abc"
result = extract_tool_call_info(mock_tool_call)
assert result is not None
call_id, func_name, func_args = result
assert call_id == "toolu_abc"
assert func_name == "search"
assert func_args == {"query": "hello"}