Skip to content

Commit 819054b

Browse files
kimnamuclaude
andcommitted
Python: Fix Bedrock parallel tool-call results rejected by Converse
When a model requests multiple tools in a single turn, the function-calling loop appends one ChatMessageContent per tool call and one per tool result. The Bedrock connector mapped each of these 1:1 to a Converse message, so N parallel tool results became N separate user messages each with a single toolResult block. Converse rejects this with: Expected toolResult blocks at messages.X.content for the following Ids: ... but found: ... Merge consecutive messages that map to the same Bedrock role in _prepare_chat_history_for_request, so all toolUse blocks for an assistant turn land in one assistant message and all toolResult blocks land in one user message. This mirrors the existing behavior of the sibling Anthropic connector, which already groups parallel tool messages. Python analogue of #13647 (reported for .NET). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 61331d8 commit 819054b

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

python/semantic_kernel/connectors/ai/bedrock/services/bedrock_chat_completion.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,15 @@ def _prepare_chat_history_for_request(
175175
for message in chat_history.messages:
176176
if message.role == AuthorRole.SYSTEM:
177177
continue
178-
messages.append(MESSAGE_CONVERTERS[message.role](message))
178+
formatted_message = MESSAGE_CONVERTERS[message.role](message)
179+
if messages and messages[-1][role_key] == formatted_message[role_key]:
180+
# The Bedrock Converse API requires that consecutive messages with the same role be
181+
# combined into a single message. In particular, SK emits one tool message per parallel
182+
# tool result (all mapped to the "user" role), which Bedrock rejects unless every
183+
# toolResult block for an assistant turn is grouped in a single user message.
184+
messages[-1][content_key] += formatted_message[content_key]
185+
else:
186+
messages.append(formatted_message)
179187

180188
return messages
181189

python/tests/unit/connectors/ai/bedrock/services/test_bedrock_chat_completion.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
1414
from semantic_kernel.contents.chat_history import ChatHistory
1515
from semantic_kernel.contents.chat_message_content import ChatMessageContent
16+
from semantic_kernel.contents.function_call_content import FunctionCallContent
17+
from semantic_kernel.contents.function_result_content import FunctionResultContent
1618
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
1719
from semantic_kernel.contents.text_content import TextContent
1820
from semantic_kernel.contents.utils.author_role import AuthorRole
@@ -150,6 +152,61 @@ def test_prepare_chat_history_for_request(mock_client, bedrock_unit_test_env, ch
150152
assert all([item["role"] in ["user", "assistant"] for item in parsed_chat_history])
151153

152154

155+
@patch.object(boto3, "client", return_value=Mock())
156+
def test_prepare_chat_history_for_request_merges_parallel_tool_results(mock_client, bedrock_unit_test_env) -> None:
157+
"""Test that parallel tool calls and their results are merged into single Bedrock messages.
158+
159+
When a model requests multiple tools in one turn, SK emits one assistant message per tool call
160+
and one tool message per tool result. The Bedrock Converse API requires every toolUse block for a
161+
turn to be in a single assistant message and every toolResult block to be in a single user message,
162+
otherwise the request is rejected with an "Expected toolResult blocks ..." error.
163+
"""
164+
chat_history = ChatHistory()
165+
chat_history.add_user_message("What is the weather in Seattle and Tokyo?")
166+
chat_history.add_message(
167+
ChatMessageContent(
168+
role=AuthorRole.ASSISTANT,
169+
items=[FunctionCallContent(id="call_1", name="get_weather", arguments={"city": "Seattle"})],
170+
)
171+
)
172+
chat_history.add_message(
173+
ChatMessageContent(
174+
role=AuthorRole.ASSISTANT,
175+
items=[FunctionCallContent(id="call_2", name="get_weather", arguments={"city": "Tokyo"})],
176+
)
177+
)
178+
chat_history.add_message(
179+
ChatMessageContent(
180+
role=AuthorRole.TOOL,
181+
items=[FunctionResultContent(id="call_1", result="Sunny")],
182+
)
183+
)
184+
chat_history.add_message(
185+
ChatMessageContent(
186+
role=AuthorRole.TOOL,
187+
items=[FunctionResultContent(id="call_2", result="Rainy")],
188+
)
189+
)
190+
191+
bedrock_chat_completion = BedrockChatCompletion()
192+
parsed_chat_history = bedrock_chat_completion._prepare_chat_history_for_request(chat_history)
193+
194+
# user message + merged assistant message (2 toolUse) + merged user message (2 toolResult)
195+
assert len(parsed_chat_history) == 3
196+
197+
assistant_message = parsed_chat_history[1]
198+
assert assistant_message["role"] == "assistant"
199+
tool_use_ids = [block["toolUse"]["toolUseId"] for block in assistant_message["content"] if "toolUse" in block]
200+
assert tool_use_ids == ["call_1", "call_2"]
201+
202+
tool_result_message = parsed_chat_history[2]
203+
assert tool_result_message["role"] == "user"
204+
tool_result_ids = [
205+
block["toolResult"]["toolUseId"] for block in tool_result_message["content"] if "toolResult" in block
206+
]
207+
assert tool_result_ids == ["call_1", "call_2"]
208+
209+
153210
@patch.object(boto3, "client", return_value=Mock())
154211
def test_prepare_system_message_for_request(mock_client, bedrock_unit_test_env, chat_history) -> None:
155212
"""Test preparing system message for request"""

0 commit comments

Comments
 (0)