Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,19 @@ def _prepare_chat_history_for_request(
for message in chat_history.messages:
if message.role == AuthorRole.SYSTEM:
continue
messages.append(MESSAGE_CONVERTERS[message.role](message))
formatted_message = MESSAGE_CONVERTERS[message.role](message)
if messages and messages[-1][role_key] == formatted_message[role_key]:
# The Bedrock Converse API requires that consecutive messages with the same role be
# combined into a single message. In particular, SK emits one tool message per parallel
# tool result (all mapped to the "user" role), which Bedrock rejects unless every
# toolResult block for an assistant turn is grouped in a single user message.
# Build a new combined content list rather than mutating the previous message in place.
messages[-1][content_key] = [
*messages[-1][content_key],
*formatted_message[content_key],
]
else:
messages.append(formatted_message)
Comment on lines +178 to +190

return messages

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
Expand Down Expand Up @@ -150,6 +152,85 @@ def test_prepare_chat_history_for_request(mock_client, bedrock_unit_test_env, ch
assert all([item["role"] in ["user", "assistant"] for item in parsed_chat_history])


@patch.object(boto3, "client", return_value=Mock())
def test_prepare_chat_history_for_request_merges_parallel_tool_results(mock_client, bedrock_unit_test_env) -> None:
"""Test that parallel tool calls and their results are merged into single Bedrock messages.

When a model requests multiple tools in one turn, SK emits one assistant message per tool call
and one tool message per tool result. The Bedrock Converse API requires every toolUse block for a
turn to be in a single assistant message and every toolResult block to be in a single user message,
otherwise the request is rejected with an "Expected toolResult blocks ..." error.
"""
Comment on lines +155 to +163
chat_history = ChatHistory()
chat_history.add_user_message("What is the weather in Seattle and Tokyo?")
chat_history.add_message(
ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[FunctionCallContent(id="call_1", name="get_weather", arguments={"city": "Seattle"})],
)
)
chat_history.add_message(
ChatMessageContent(
role=AuthorRole.ASSISTANT,
items=[FunctionCallContent(id="call_2", name="get_weather", arguments={"city": "Tokyo"})],
)
)
chat_history.add_message(
ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(id="call_1", result="Sunny")],
)
)
chat_history.add_message(
ChatMessageContent(
role=AuthorRole.TOOL,
items=[FunctionResultContent(id="call_2", result="Rainy")],
)
)

bedrock_chat_completion = BedrockChatCompletion()
parsed_chat_history = bedrock_chat_completion._prepare_chat_history_for_request(chat_history)

# user message + merged assistant message (2 toolUse) + merged user message (2 toolResult)
assert len(parsed_chat_history) == 3

assistant_message = parsed_chat_history[1]
assert assistant_message["role"] == "assistant"
tool_use_ids = [block["toolUse"]["toolUseId"] for block in assistant_message["content"] if "toolUse" in block]
assert tool_use_ids == ["call_1", "call_2"]

tool_result_message = parsed_chat_history[2]
assert tool_result_message["role"] == "user"
tool_result_ids = [
block["toolResult"]["toolUseId"] for block in tool_result_message["content"] if "toolResult" in block
]
assert tool_result_ids == ["call_1", "call_2"]


@patch.object(boto3, "client", return_value=Mock())
def test_prepare_chat_history_for_request_merges_consecutive_same_role_messages(
mock_client, bedrock_unit_test_env
) -> None:
"""Test that consecutive same-role messages are merged even without tool content.

The merge applies to any consecutive messages mapping to the same Bedrock role, not just
tool-related ones, so two consecutive user text messages collapse into a single user
message whose content preserves both text blocks in order.
"""
chat_history = ChatHistory()
chat_history.add_user_message("First question.")
chat_history.add_user_message("Second question.")

bedrock_chat_completion = BedrockChatCompletion()
parsed_chat_history = bedrock_chat_completion._prepare_chat_history_for_request(chat_history)

assert len(parsed_chat_history) == 1
merged_message = parsed_chat_history[0]
assert merged_message["role"] == "user"
texts = [block["text"] for block in merged_message["content"] if "text" in block]
assert texts == ["First question.", "Second question."]


@patch.object(boto3, "client", return_value=Mock())
def test_prepare_system_message_for_request(mock_client, bedrock_unit_test_env, chat_history) -> None:
"""Test preparing system message for request"""
Expand Down
Loading