tools) {
+ AnthropicToolsHelper.applyTools(request, tools);
+ }
- // Clean up thread-local storage
- currentOptions.remove();
+ @Override
+ public void applyToolChoice(AnthropicRequest request, ToolChoice toolChoice) {
+ AnthropicToolsHelper.applyToolChoice(request, toolChoice);
}
/**
- * Extract and apply system message if present. Anthropic API requires system message to be set
+ * Extract and apply system message if present. Anthropic API requires system
+ * message to be set
* via the system parameter, not as a message.
*
- * This method is called by Model to extract the first system message from the messages list
+ *
+ * This method is called by Model to extract the first system message from the
+ * messages list
* and apply it to the system parameter.
*
- * @param paramsBuilder Anthropic request parameters builder
+ * @param request Anthropic request
* @param messages All messages including potential system message
*/
- public void applySystemMessage(MessageCreateParams.Builder paramsBuilder, List messages) {
+ public void applySystemMessage(AnthropicRequest request, List messages) {
String systemMessage = messageConverter.extractSystemMessage(messages);
if (systemMessage != null && !systemMessage.isEmpty()) {
- paramsBuilder.system(systemMessage);
+ request.setSystem(systemMessage);
}
}
}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/anthropic/AnthropicChatFormatter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/anthropic/AnthropicChatFormatter.java
index c3d7abc72..ff1a5999e 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/anthropic/AnthropicChatFormatter.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/anthropic/AnthropicChatFormatter.java
@@ -15,38 +15,238 @@
*/
package io.agentscope.core.formatter.anthropic;
-import com.anthropic.models.messages.Message;
-import com.anthropic.models.messages.MessageParam;
+import io.agentscope.core.formatter.anthropic.dto.AnthropicContent;
+import io.agentscope.core.formatter.anthropic.dto.AnthropicMessage;
+import io.agentscope.core.formatter.anthropic.dto.AnthropicResponse;
+import io.agentscope.core.message.ImageBlock;
import io.agentscope.core.message.Msg;
+import io.agentscope.core.message.MsgRole;
+import io.agentscope.core.message.ToolResultBlock;
+import io.agentscope.core.message.ToolUseBlock;
import io.agentscope.core.model.ChatResponse;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
- * Formatter for Anthropic Messages API. Converts between AgentScope Msg objects and Anthropic SDK
- * types.
+ * Formatter for Anthropic Messages API. Converts between AgentScope Msg objects
+ * and Anthropic DTO types.
*
* Important: Anthropic API has special requirements:
- *
*
* - Only the first message can be a system message (handled via system parameter)
*
- Tool results must be in separate user messages
*
- Supports thinking blocks natively (extended thinking feature)
+ *
- Automatic multi-agent conversation handling for MsgHub scenarios
*
+ *
+ * Multi-Agent Detection: This formatter automatically detects multi-agent scenarios
+ * (e.g., MsgHub conversations) by checking for multiple messages with different names but
+ * the same role. When detected, it uses {@link AnthropicConversationMerger} to consolidate the
+ * conversation into a format compatible with Anthropic's API.
*/
public class AnthropicChatFormatter extends AnthropicBaseFormatter {
+ private static final Logger log = LoggerFactory.getLogger(AnthropicChatFormatter.class);
+
+ private static final String DEFAULT_CONVERSATION_HISTORY_PROMPT =
+ "# Conversation History\n"
+ + "The content between tags contains your conversation"
+ + " history\n";
+
+ private final AnthropicMediaConverter mediaConverter;
+
+ public AnthropicChatFormatter() {
+ super();
+ this.mediaConverter = new AnthropicMediaConverter();
+ }
+
+ public AnthropicChatFormatter(AnthropicMessageConverter messageConverter) {
+ super(messageConverter);
+ this.mediaConverter = new AnthropicMediaConverter();
+ }
+
@Override
- public List doFormat(List msgs) {
+ public List doFormat(List msgs) {
+ // Detect multi-agent scenario (multiple messages with different names but same role)
+ boolean isMultiAgent = isMultiAgentConversation(msgs);
+ log.debug(
+ "doFormat: message count={}, isMultiAgent={}",
+ msgs != null ? msgs.size() : 0,
+ isMultiAgent);
+
+ if (isMultiAgent) {
+ log.info("Detected multi-agent conversation, using conversation merger");
+ return formatMultiAgentConversation(msgs);
+ }
+
+ // Single-agent or simple conversation - use standard formatting
+ log.debug("Using standard formatting for single-agent conversation");
return messageConverter.convert(msgs);
}
- @Override
- public ChatResponse parseResponse(Object response, Instant startTime) {
- if (response instanceof Message message) {
- return AnthropicResponseParser.parseMessage(message, startTime);
- } else {
- throw new IllegalArgumentException("Unsupported response type: " + response.getClass());
+ /**
+ * Detects if the message list represents a multi-agent conversation.
+ *
+ * A multi-agent conversation is detected when:
+ *
+ * - There are at least 2 ASSISTANT role messages with different names
+ * - OR there are multiple ASSISTANT messages that would create consecutive
+ * messages with the same role
+ *
+ *
+ * @param msgs List of messages to check
+ * @return true if this appears to be a multi-agent conversation
+ */
+ private boolean isMultiAgentConversation(List msgs) {
+ if (msgs == null || msgs.size() < 2) {
+ log.debug(
+ "isMultiAgentConversation: too few messages (count={})",
+ msgs != null ? msgs.size() : 0);
+ return false;
+ }
+
+ Set assistantNames = new HashSet<>();
+ MsgRole lastRole = null;
+ boolean hasConsecutiveAssistant = false;
+ boolean hasSystemNamedUserMessage = false;
+
+ for (int i = 0; i < msgs.size(); i++) {
+ Msg msg = msgs.get(i);
+ MsgRole currentRole = msg.getRole();
+ String msgName = msg.getName();
+
+ log.trace("Message {}: role={}, name={}", i, currentRole, msgName);
+
+ // Check for consecutive ASSISTANT messages (without tool calls in between)
+ if (lastRole == MsgRole.ASSISTANT && currentRole == MsgRole.ASSISTANT) {
+ hasConsecutiveAssistant = true;
+ log.debug("Found consecutive ASSISTANT messages at index {}", i);
+ }
+
+ // Check if USER message has name="system" (indicates MsgHub announcement)
+ if (currentRole == MsgRole.USER && "system".equals(msgName)) {
+ hasSystemNamedUserMessage = true;
+ log.debug("Found USER message with name='system' (MsgHub announcement)");
+ }
+
+ // Collect ASSISTANT message names
+ if (currentRole == MsgRole.ASSISTANT && msgName != null) {
+ assistantNames.add(msgName);
+ }
+
+ lastRole = currentRole;
}
+
+ // Multi-agent if:
+ // 1. Multiple assistant names (different agents), OR
+ // 2. Consecutive assistant messages, OR
+ // 3. System-named USER message (MsgHub announcement)
+ boolean result =
+ assistantNames.size() > 1 || hasConsecutiveAssistant || hasSystemNamedUserMessage;
+ log.debug(
+ "isMultiAgentConversation: assistantNames={}, hasConsecutive={}, "
+ + "hasSystemNamedUserMessage={}, result={}",
+ assistantNames,
+ hasConsecutiveAssistant,
+ hasSystemNamedUserMessage,
+ result);
+ return result;
+ }
+
+ /**
+ * Formats a multi-agent conversation using the conversation merger.
+ * This consolidates multiple agent messages into a format compatible with Anthropic's API.
+ *
+ * @param msgs List of messages in the multi-agent conversation
+ * @return List of Anthropic-formatted messages
+ */
+ private List formatMultiAgentConversation(List msgs) {
+ log.debug("formatMultiAgentConversation: processing {} messages", msgs.size());
+
+ // Separate messages into groups: SYSTEM, TOOL_SEQUENCE, AGENT_CONVERSATION
+ List result = new ArrayList<>();
+ List systemMsgs = new ArrayList<>();
+ List toolSequence = new ArrayList<>();
+ List agentConversation = new ArrayList<>();
+
+ for (Msg msg : msgs) {
+ MsgRole role = msg.getRole();
+
+ if (role == MsgRole.SYSTEM) {
+ systemMsgs.add(msg);
+ } else if (msg.hasContentBlocks(ToolUseBlock.class)
+ || msg.hasContentBlocks(ToolResultBlock.class)) {
+ // Tool-related messages: use standard converter
+ toolSequence.add(msg);
+ } else {
+ // Regular conversation messages (USER, ASSISTANT without tools)
+ agentConversation.add(msg);
+ }
+ }
+
+ // Add system messages using standard converter
+ for (Msg sysMsg : systemMsgs) {
+ List converted = messageConverter.convert(List.of(sysMsg));
+ result.addAll(converted);
+ }
+
+ // Add tool sequence using standard converter
+ if (!toolSequence.isEmpty()) {
+ List converted = messageConverter.convert(toolSequence);
+ result.addAll(converted);
+ log.debug("Added {} tool messages using standard converter", toolSequence.size());
+ }
+
+ // Merge agent conversation into a single user message
+ if (!agentConversation.isEmpty()) {
+ log.debug("Merging {} agent conversation messages", agentConversation.size());
+
+ List