mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-03 14:09:24 +00:00
fix: extract Bedrock Converse API tool arguments from 'input' key (#4972)
The dict branch in _parse_native_tool_call used a truthy default '{}' for
func_info.get('arguments', '{}'), which prevented the or-chain from ever
reaching tool_call.get('input'). Bedrock returns tool calls as
{name, input, toolUseId} dicts (no 'function' key), so every tool call
received an empty dict instead of its actual arguments.
Fix: remove the default from get('arguments') so it returns None (falsy)
when the key is absent, allowing the fallback to 'input'.
Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -1276,3 +1276,160 @@ class TestNativeToolCallingJsonParseError:
|
||||
|
||||
assert "Error" in result["result"]
|
||||
assert "validation failed" in result["result"].lower() or "missing" in result["result"].lower()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# _parse_native_tool_call — Bedrock Converse API dict format (issue #4972)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestParseNativeToolCallBedrockDict:
|
||||
"""Verify that _parse_native_tool_call correctly extracts arguments from
|
||||
Bedrock-style dict tool calls that use ``{"name": ..., "input": {...}, "toolUseId": ...}``
|
||||
instead of OpenAI-style ``{"function": {"name": ..., "arguments": ...}}``.
|
||||
|
||||
Regression tests for https://github.com/crewAIInc/crewAI/issues/4972
|
||||
"""
|
||||
|
||||
def _make_executor(self) -> "CrewAgentExecutor":
|
||||
"""Create a minimal CrewAgentExecutor for unit-testing parsing."""
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
|
||||
executor = object.__new__(CrewAgentExecutor)
|
||||
return executor
|
||||
|
||||
# --- Bedrock-style dicts (the bug scenario) ---
|
||||
|
||||
def test_bedrock_dict_tool_call_extracts_input_args(self) -> None:
|
||||
"""Bedrock Converse API returns {name, input, toolUseId}; args must come from 'input'."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"name": "search_knowledge",
|
||||
"input": {"search_query": "latest updates"},
|
||||
"toolUseId": "tooluse_abc123",
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
call_id, func_name, func_args = result
|
||||
assert call_id == "tooluse_abc123"
|
||||
assert func_name == "search_knowledge"
|
||||
assert func_args == {"search_query": "latest updates"}
|
||||
|
||||
def test_bedrock_dict_with_multiple_input_args(self) -> None:
|
||||
"""Multiple args in the Bedrock 'input' dict should all be preserved."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"name": "create_document",
|
||||
"input": {"title": "Report", "content": "body text", "format": "pdf"},
|
||||
"toolUseId": "tooluse_xyz789",
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
_, _, func_args = result
|
||||
assert func_args == {"title": "Report", "content": "body text", "format": "pdf"}
|
||||
|
||||
def test_bedrock_dict_with_empty_input(self) -> None:
|
||||
"""A Bedrock tool call with an empty 'input' dict should fall through to default '{}'."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"name": "no_args_tool",
|
||||
"input": {},
|
||||
"toolUseId": "tooluse_empty",
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
_, _, func_args = result
|
||||
# Empty dict is falsy, so the or-chain falls through to the final "{}"
|
||||
assert func_args == "{}"
|
||||
|
||||
# --- OpenAI-style dicts (must still work after the fix) ---
|
||||
|
||||
def test_openai_dict_tool_call_still_works(self) -> None:
|
||||
"""OpenAI-style dict tool calls must continue to extract from 'function.arguments'."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"id": "call_openai_123",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"arguments": '{"expression": "15 * 8"}',
|
||||
},
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
call_id, func_name, func_args = result
|
||||
assert call_id == "call_openai_123"
|
||||
assert func_name == "calculator"
|
||||
assert func_args == '{"expression": "15 * 8"}'
|
||||
|
||||
def test_openai_dict_with_empty_string_arguments(self) -> None:
|
||||
"""OpenAI dict with empty string arguments should fall through to '{}'."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"id": "call_empty",
|
||||
"function": {
|
||||
"name": "ping",
|
||||
"arguments": "",
|
||||
},
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
_, _, func_args = result
|
||||
# Empty string is falsy, so we fall through to "{}"
|
||||
assert func_args == "{}"
|
||||
|
||||
# --- Dict with neither function nor input ---
|
||||
|
||||
def test_dict_with_only_name_no_function_no_input(self) -> None:
|
||||
"""Dict with 'name' but no 'function' and no 'input' keys should default to '{}'."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"name": "simple_tool",
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
_, func_name, func_args = result
|
||||
assert func_name == "simple_tool"
|
||||
assert func_args == "{}"
|
||||
|
||||
# --- Bedrock toolUseId used as call_id ---
|
||||
|
||||
def test_bedrock_dict_uses_toolUseId_as_call_id(self) -> None:
|
||||
"""Bedrock's 'toolUseId' should be used as the call_id."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"name": "my_tool",
|
||||
"input": {"query": "test"},
|
||||
"toolUseId": "tooluse_unique_id",
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
call_id, _, _ = result
|
||||
assert call_id == "tooluse_unique_id"
|
||||
|
||||
def test_bedrock_dict_fallback_call_id(self) -> None:
|
||||
"""Without 'id' or 'toolUseId', should generate a fallback call_id."""
|
||||
executor = self._make_executor()
|
||||
tool_call = {
|
||||
"name": "my_tool",
|
||||
"input": {"query": "test"},
|
||||
}
|
||||
|
||||
result = executor._parse_native_tool_call(tool_call)
|
||||
|
||||
assert result is not None
|
||||
call_id, _, _ = result
|
||||
assert call_id.startswith("call_")
|
||||
|
||||
Reference in New Issue
Block a user