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
7 changes: 1 addition & 6 deletions agentscope-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,7 @@
<artifactId>google-genai</artifactId>
</dependency>

<!-- Anthropic Java SDK -->
<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
</dependency>


<!-- Model Context Protocol (MCP) SDK -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,92 +15,105 @@
*/
package io.agentscope.core.formatter.anthropic;

import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.MessageParam;
import io.agentscope.core.formatter.AbstractBaseFormatter;
import io.agentscope.core.formatter.anthropic.dto.AnthropicMessage;
import io.agentscope.core.formatter.anthropic.dto.AnthropicRequest;
import io.agentscope.core.formatter.anthropic.dto.AnthropicResponse;
import io.agentscope.core.message.Msg;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.ToolSchema;
import java.util.List;

/**
* Abstract base formatter for Anthropic API with shared logic for handling Anthropic-specific
* Abstract base formatter for Anthropic API with shared logic for handling
* Anthropic-specific
* requirements.
*
* <p>This class handles:
* <p>
* This class handles:
*
* <ul>
* <li>System message extraction and application (Anthropic requires system via system parameter)
* <li>Tool choice configuration with GenerateOptions
* <li>System message extraction and application (Anthropic requires system via
* system parameter)
* <li>Tool choice configuration with GenerateOptions
* </ul>
*/
public abstract class AnthropicBaseFormatter
extends AbstractBaseFormatter<MessageParam, Object, MessageCreateParams.Builder> {
extends AbstractBaseFormatter<AnthropicMessage, AnthropicResponse, AnthropicRequest> {

protected final AnthropicMessageConverter messageConverter;

/** Thread-local storage for generation options (passed from applyOptions to applyTools). */
/**
* Thread-local storage for generation options (passed from applyOptions to
* applyTools).
*/
private final ThreadLocal<GenerateOptions> currentOptions = new ThreadLocal<>();

protected AnthropicBaseFormatter() {
this.messageConverter = new AnthropicMessageConverter(this::convertToolResultToString);
}

protected AnthropicBaseFormatter(AnthropicMessageConverter messageConverter) {
this.messageConverter = messageConverter;
}

/**
* Apply generation options to Anthropic request parameters.
*
* @param paramsBuilder Anthropic request parameters builder
* @param options Generation options to apply
* @param request Anthropic request
* @param options Generation options to apply
* @param defaultOptions Default options to use if options parameter is null
*/
@Override
public void applyOptions(
MessageCreateParams.Builder paramsBuilder,
GenerateOptions options,
GenerateOptions defaultOptions) {
AnthropicRequest request, GenerateOptions options, GenerateOptions defaultOptions) {
// Save options for applyTools
currentOptions.set(options);

// Apply other options
AnthropicToolsHelper.applyOptions(paramsBuilder, options, defaultOptions);
AnthropicToolsHelper.applyOptions(request, options, defaultOptions);
}

/**
* Apply tool schemas to Anthropic request parameters. This method uses the options saved from
* Apply tool schemas to Anthropic request parameters. This method uses the
* options saved from
* applyOptions to apply tool choice configuration.
*
* @param paramsBuilder Anthropic request parameters builder
* @param tools List of tool schemas to apply (may be null or empty)
* @param request Anthropic request
* @param tools List of tool schemas to apply (may be null or empty)
*/
@Override
public void applyTools(MessageCreateParams.Builder paramsBuilder, List<ToolSchema> tools) {
public void applyTools(AnthropicRequest request, List<ToolSchema> tools) {
if (tools == null || tools.isEmpty()) {
currentOptions.remove();
return;
}

// Use saved options to apply tools with tool choice
GenerateOptions options = currentOptions.get();
AnthropicToolsHelper.applyTools(paramsBuilder, tools, options);
AnthropicToolsHelper.applyTools(request, tools, options);

// Clean up thread-local storage
currentOptions.remove();
}

/**
* 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.
*
* <p>This method is called by Model to extract the first system message from the messages list
* <p>
* 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<Msg> messages) {
public void applySystemMessage(AnthropicRequest request, List<Msg> messages) {
String systemMessage = messageConverter.extractSystemMessage(messages);
if (systemMessage != null && !systemMessage.isEmpty()) {
paramsBuilder.system(systemMessage);
request.setSystem(systemMessage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Important: Anthropic API has special requirements:
*
* <ul>
* <li>Only the first message can be a system message (handled via system parameter)
* <li>Tool results must be in separate user messages
* <li>Supports thinking blocks natively (extended thinking feature)
* <li>Automatic multi-agent conversation handling for MsgHub scenarios
* </ul>
*
* <p><b>Multi-Agent Detection:</b> 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 <history></history> 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<MessageParam> doFormat(List<Msg> msgs) {
public List<AnthropicMessage> doFormat(List<Msg> 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.
*
* <p>A multi-agent conversation is detected when:
* <ul>
* <li>There are at least 2 ASSISTANT role messages with different names</li>
* <li>OR there are multiple ASSISTANT messages that would create consecutive
* messages with the same role</li>
* </ul>
*
* @param msgs List of messages to check
* @return true if this appears to be a multi-agent conversation
*/
private boolean isMultiAgentConversation(List<Msg> msgs) {
if (msgs == null || msgs.size() < 2) {
log.debug(
"isMultiAgentConversation: too few messages (count={})",
msgs != null ? msgs.size() : 0);
return false;
}

Set<String> 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<AnthropicMessage> formatMultiAgentConversation(List<Msg> msgs) {
log.debug("formatMultiAgentConversation: processing {} messages", msgs.size());

// Separate messages into groups: SYSTEM, TOOL_SEQUENCE, AGENT_CONVERSATION
List<AnthropicMessage> result = new ArrayList<>();
List<Msg> systemMsgs = new ArrayList<>();
List<Msg> toolSequence = new ArrayList<>();
List<Msg> 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<AnthropicMessage> converted = messageConverter.convert(List.of(sysMsg));
result.addAll(converted);
}

// Add tool sequence using standard converter
if (!toolSequence.isEmpty()) {
List<AnthropicMessage> 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<Object> mergedContent =
AnthropicConversationMerger.mergeConversation(
agentConversation, DEFAULT_CONVERSATION_HISTORY_PROMPT);

List<AnthropicContent> contentBlocks = new ArrayList<>();

for (Object item : mergedContent) {
if (item instanceof String text) {
contentBlocks.add(AnthropicContent.text(text));
log.trace("Added text content block (length: {})", text.length());
} else if (item instanceof ImageBlock ib) {
try {
AnthropicContent.ImageSource imageSource =
mediaConverter.convertImageBlock(ib);
contentBlocks.add(
AnthropicContent.image(
imageSource.getMediaType(), imageSource.getData()));
log.trace("Added image content block");
} catch (Exception e) {
log.warn("Failed to convert image block: {}", e.getMessage());
contentBlocks.add(AnthropicContent.text("[Image - conversion failed]"));
}
}
}

if (!contentBlocks.isEmpty()) {
AnthropicMessage mergedMessage = new AnthropicMessage("user", contentBlocks);
result.add(mergedMessage);
log.debug(
"Created merged user message with {} content blocks", contentBlocks.size());
} else {
log.warn("No content blocks created from merged agent conversation");
}
}

log.debug("formatMultiAgentConversation: returning {} messages", result.size());
return result;
}

@Override
public ChatResponse parseResponse(AnthropicResponse response, Instant startTime) {
return AnthropicResponseParser.parseMessage(response, startTime);
}
}
Loading
Loading