From 537186d4d5ab1b1dcb61e166145c3640e1cc512b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 10 Mar 2026 18:11:50 +0000 Subject: [PATCH] fix: group bedrock tool results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Moura --- .../llms/providers/bedrock/completion.py | 80 ++++++++++--------- lib/crewai/tests/llms/bedrock/test_bedrock.py | 67 ++++++++++++++++ 2 files changed, 109 insertions(+), 38 deletions(-) diff --git a/lib/crewai/src/crewai/llms/providers/bedrock/completion.py b/lib/crewai/src/crewai/llms/providers/bedrock/completion.py index c707be3af..9bb87c6e9 100644 --- a/lib/crewai/src/crewai/llms/providers/bedrock/completion.py +++ b/lib/crewai/src/crewai/llms/providers/bedrock/completion.py @@ -1781,6 +1781,7 @@ class BedrockCompletion(BaseLLM): converse_messages: list[LLMMessage] = [] system_message: str | None = None + pending_tool_results: list[dict[str, Any]] = [] for message in formatted_messages: role = message.get("role") @@ -1794,53 +1795,56 @@ class BedrockCompletion(BaseLLM): system_message += f"\n\n{content}" else: system_message = cast(str, content) - elif role == "assistant" and tool_calls: - # Convert OpenAI-style tool_calls to Bedrock toolUse format - bedrock_content = [] - for tc in tool_calls: - func = tc.get("function", {}) - tool_use_block = { - "toolUse": { - "toolUseId": tc.get("id", f"call_{id(tc)}"), - "name": func.get("name", ""), - "input": func.get("arguments", {}) - if isinstance(func.get("arguments"), dict) - else json.loads(func.get("arguments", "{}") or "{}"), - } - } - bedrock_content.append(tool_use_block) - converse_messages.append( - {"role": "assistant", "content": bedrock_content} - ) elif role == "tool": if not tool_call_id: raise ValueError("Tool message missing required tool_call_id") - converse_messages.append( + pending_tool_results.append( { - "role": "user", - "content": [ - { - "toolResult": { - "toolUseId": tool_call_id, - "content": [ - {"text": str(content) if content else ""} - ], - } - } - ], + "toolResult": { + "toolUseId": tool_call_id, + "content": [{"text": str(content) if content else ""}], + } } ) else: - # Convert to Converse API format with proper content structure - if isinstance(content, list): - # Already formatted as multimodal content blocks - converse_messages.append({"role": role, "content": content}) - else: - # String content - wrap in text block - text_content = content if content else "" + if pending_tool_results: converse_messages.append( - {"role": role, "content": [{"text": text_content}]} + {"role": "user", "content": pending_tool_results} ) + pending_tool_results = [] + + if role == "assistant" and tool_calls: + # Convert OpenAI-style tool_calls to Bedrock toolUse format + bedrock_content = [] + for tc in tool_calls: + func = tc.get("function", {}) + tool_use_block = { + "toolUse": { + "toolUseId": tc.get("id", f"call_{id(tc)}"), + "name": func.get("name", ""), + "input": func.get("arguments", {}) + if isinstance(func.get("arguments"), dict) + else json.loads(func.get("arguments", "{}") or "{}"), + } + } + bedrock_content.append(tool_use_block) + converse_messages.append( + {"role": "assistant", "content": bedrock_content} + ) + else: + # Convert to Converse API format with proper content structure + if isinstance(content, list): + # Already formatted as multimodal content blocks + converse_messages.append({"role": role, "content": content}) + else: + # String content - wrap in text block + text_content = content if content else "" + converse_messages.append( + {"role": role, "content": [{"text": text_content}]} + ) + + if pending_tool_results: + converse_messages.append({"role": "user", "content": pending_tool_results}) # CRITICAL: Handle model-specific conversation requirements # Cohere and some other models require conversation to end with user message diff --git a/lib/crewai/tests/llms/bedrock/test_bedrock.py b/lib/crewai/tests/llms/bedrock/test_bedrock.py index 531e4d967..5f9dab30e 100644 --- a/lib/crewai/tests/llms/bedrock/test_bedrock.py +++ b/lib/crewai/tests/llms/bedrock/test_bedrock.py @@ -967,3 +967,70 @@ def test_bedrock_agent_kickoff_structured_output_with_tools(): assert result.pydantic.result == 42, f"Expected result 42 but got {result.pydantic.result}" assert result.pydantic.operation, "Operation should not be empty" assert result.pydantic.explanation, "Explanation should not be empty" + + +def test_bedrock_groups_three_tool_results(): + """Consecutive tool results should be grouped into one Bedrock user message.""" + llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0") + + test_messages = [ + {"role": "user", "content": "Use all three tools, then continue."}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "tool-1", + "type": "function", + "function": { + "name": "lookup_weather", + "arguments": '{"location": "New York"}', + }, + }, + { + "id": "tool-2", + "type": "function", + "function": { + "name": "lookup_news", + "arguments": '{"topic": "AI"}', + }, + }, + { + "id": "tool-3", + "type": "function", + "function": { + "name": "lookup_stock", + "arguments": '{"ticker": "AMZN"}', + }, + }, + ], + }, + {"role": "tool", "tool_call_id": "tool-1", "content": "72F and sunny"}, + {"role": "tool", "tool_call_id": "tool-2", "content": "AI news summary"}, + {"role": "tool", "tool_call_id": "tool-3", "content": "AMZN up 1.2%"}, + ] + + formatted_messages, system_message = llm._format_messages_for_converse( + test_messages + ) + + assert system_message is None + assert [message["role"] for message in formatted_messages] == [ + "user", + "assistant", + "user", + ] + assert len(formatted_messages[1]["content"]) == 3 + + tool_results = formatted_messages[2]["content"] + assert len(tool_results) == 3 + assert [block["toolResult"]["toolUseId"] for block in tool_results] == [ + "tool-1", + "tool-2", + "tool-3", + ] + assert [block["toolResult"]["content"][0]["text"] for block in tool_results] == [ + "72F and sunny", + "AI news summary", + "AMZN up 1.2%", + ]