Compare commits

...

1 Commits

Author SHA1 Message Date
Cursor Agent
537186d4d5 fix: group bedrock tool results
Co-authored-by: João Moura <joaomdmoura@gmail.com>
2026-03-10 18:11:50 +00:00
2 changed files with 109 additions and 38 deletions

View File

@@ -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

View File

@@ -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%",
]