Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
import sys
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence
from typing import Any, ClassVar, Final, Generic, Literal, TypedDict
from typing import Any, ClassVar, Final, Generic, Literal, TypedDict, cast

from agent_framework import (
Annotation,
Expand Down Expand Up @@ -696,11 +696,42 @@ def _prepare_messages_for_anthropic(self, messages: Sequence[Message]) -> list[d

This skips the first message if it is a system message,
as Anthropic expects system instructions as a separate parameter.

Anthropic's API requires that the conversation ends with a user message.
If the last message is from the assistant, a synthetic user turn is
appended when it will not break Anthropic tool_use/tool_result pairing.
"""
# first system message is passed as instructions
if messages and isinstance(messages[0], Message) and messages[0].role == "system":
return [self._prepare_message_for_anthropic(msg) for msg in messages[1:]]
return [self._prepare_message_for_anthropic(msg) for msg in messages]
msgs = list(messages[1:])
else:
msgs = list(messages)

result = [self._prepare_message_for_anthropic(msg) for msg in msgs]

# Anthropic requires the conversation to end with a user message.
# Append a synthetic user turn so chained agent outputs work as
# valid context for the next agent without rewriting the assistant message.
if result and result[-1].get("role") == "assistant" and not self._message_has_tool_use(result[-1]):
result.append({"role": "user", "content": "Continue"})
Comment thread
hanhan761 marked this conversation as resolved.

return result

def _message_has_tool_use(self, message: dict[str, Any]) -> bool:
"""Return whether an Anthropic message contains tool_use blocks."""
content: object = message.get("content")
if not isinstance(content, list):
return False

content_blocks = cast(list[object], content)
for content_block in content_blocks:
match content_block:
case {"type": "tool_use" | "mcp_tool_use" | "server_tool_use"}:
return True
case _:
pass

return False

def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]:
"""Prepare a Message for the Anthropic client.
Expand Down
30 changes: 29 additions & 1 deletion python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,37 @@ def test_prepare_messages_for_anthropic_without_system(

result = client._prepare_messages_for_anthropic(messages)

assert len(result) == 2
assert len(result) == 3
assert result[0]["role"] == "user"
assert result[1]["role"] == "assistant"
assert result[2]["role"] == "user"
assert result[2]["content"] == "Continue"


def test_prepare_messages_for_anthropic_does_not_append_after_tool_use(
mock_anthropic_client: MagicMock,
) -> None:
"""Do not append plain user text after assistant tool_use blocks."""
client = create_test_anthropic_client(mock_anthropic_client)
messages = [
Message(role="user", contents=["What's the weather?"]),
Message(
role="assistant",
contents=[
Content.from_function_call(
call_id="call_123",
name="get_weather",
arguments={"location": "Seattle"},
)
],
),
]

result = client._prepare_messages_for_anthropic(messages)

assert len(result) == 2
assert result[1]["role"] == "assistant"
assert result[1]["content"][0]["type"] == "tool_use"


# Tool Conversion Tests
Expand Down
Loading