Source code for agentscope.formatter._anthropic_formatter
# -*- coding: utf-8 -*-
# pylint: disable=too-many-branches
"""The Anthropic formatter module."""
from typing import Any
from ._truncated_formatter_base import TruncatedFormatterBase
from .._logging import logger
from ..message import Msg, TextBlock, ImageBlock, ToolUseBlock, ToolResultBlock
from ..token import TokenCounterBase
[docs]
class AnthropicChatFormatter(TruncatedFormatterBase):
"""Formatter for Anthropic messages."""
support_tools_api: bool = True
"""Whether support tools API"""
support_multiagent: bool = False
"""Whether support multi-agent conversations"""
support_vision: bool = True
"""Whether support vision data"""
supported_blocks: list[type] = [
TextBlock,
# Multimodal
ImageBlock,
# Tool use
ToolUseBlock,
ToolResultBlock,
]
"""The list of supported message blocks"""
[docs]
async def _format(
self,
msgs: list[Msg],
) -> list[dict[str, Any]]:
"""Format message objects into Anthropic API format.
Args:
msgs (`list[Msg]`):
The list of message objects to format.
Returns:
`list[dict[str, Any]]`:
The formatted messages as a list of dictionaries.
.. note:: Anthropic suggests always passing all previous thinking
blocks back to the API in subsequent calls to maintain reasoning
continuity. For more details, please refer to
`Anthropic's documentation
<https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#preserving-thinking-blocks>`_.
"""
self.assert_list_of_msgs(msgs)
messages: list[dict] = []
for index, msg in enumerate(msgs):
content_blocks = []
for block in msg.get_content_blocks():
typ = block.get("type")
if typ in ["thinking", "text", "image"]:
content_blocks.append({**block})
elif typ == "tool_use":
content_blocks.append(
{
"id": block.get("id"),
"type": "tool_use",
"name": block.get("name"),
"input": block.get("input", {}),
},
)
elif typ == "tool_result":
output = block.get("output")
if output is None:
content_value = [{"type": "text", "text": None}]
elif isinstance(output, list):
content_value = output
else:
content_value = [{"type": "text", "text": str(output)}]
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": block.get("id"),
"content": content_value,
},
],
},
)
else:
logger.warning(
"Unsupported block type %s in the message, skipped.",
typ,
)
# Claude only allow the first message to be system message
if msg.role == "system" and index != 0:
role = "user"
else:
role = msg.role
msg_anthropic = {
"role": role,
"content": content_blocks or None,
}
# When both content and tool_calls are None, skipped
if msg_anthropic["content"] or msg_anthropic.get("tool_calls"):
messages.append(msg_anthropic)
return messages
[docs]
class AnthropicMultiAgentFormatter(TruncatedFormatterBase):
"""
Anthropic formatter for multi-agent conversations, where more than
a user and an agent are involved.
"""
support_tools_api: bool = True
"""Whether support tools API"""
support_multiagent: bool = True
"""Whether support multi-agent conversations"""
support_vision: bool = True
"""Whether support vision data"""
supported_blocks: list[type] = [
TextBlock,
# Multimodal
ImageBlock,
# Tool use
ToolUseBlock,
ToolResultBlock,
]
"""The list of supported message blocks"""
[docs]
def __init__(
self,
conversation_history_prompt: str = (
"# Conversation History\n"
"The content between <history></history> tags contains "
"your conversation history\n"
),
token_counter: TokenCounterBase | None = None,
max_tokens: int | None = None,
) -> None:
"""Initialize the DashScope multi-agent formatter.
Args:
conversation_history_prompt (`str`):
The prompt to use for the conversation history section.
"""
super().__init__(token_counter=token_counter, max_tokens=max_tokens)
self.conversation_history_prompt = conversation_history_prompt
[docs]
async def _format_tool_sequence(
self,
msgs: list[Msg],
) -> list[dict[str, Any]]:
"""Given a sequence of tool call/result messages, format them into
the required format for the Anthropic API."""
return await AnthropicChatFormatter().format(msgs)
[docs]
async def _format_agent_message(
self,
msgs: list[Msg],
is_first: bool = True,
) -> list[dict[str, Any]]:
"""Given a sequence of messages without tool calls/results, format
them into the required format for the Anthropic API."""
if is_first:
conversation_history_prompt = self.conversation_history_prompt
else:
conversation_history_prompt = ""
# Format into required Anthropic format
formatted_msgs: list[dict] = []
# Collect the multimodal files
conversation_blocks: list = []
accumulated_text = []
for msg in msgs:
for block in msg.get_content_blocks():
if block["type"] == "text":
accumulated_text.append(f"{msg.name}: {block['text']}")
elif block["type"] == "image":
# Handle the accumulated text as a single block
if accumulated_text:
conversation_blocks.append(
{
"text": "\n".join(accumulated_text),
"type": "text",
},
)
accumulated_text.clear()
conversation_blocks.append({**block})
if accumulated_text:
conversation_blocks.append(
{
"text": "\n".join(accumulated_text),
"type": "text",
},
)
if conversation_blocks:
if conversation_blocks[0].get("text"):
conversation_blocks[0]["text"] = (
conversation_history_prompt
+ "<history>\n"
+ conversation_blocks[0]["text"]
)
else:
conversation_blocks.insert(
0,
{
"type": "text",
"text": conversation_history_prompt + "<history>\n",
},
)
if conversation_blocks[-1].get("text"):
conversation_blocks[-1]["text"] += "\n</history>"
else:
conversation_blocks.append(
{"type": "text", "text": "</history>"},
)
if conversation_blocks:
formatted_msgs.append(
{
"role": "user",
"content": conversation_blocks,
},
)
return formatted_msgs