Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a16bd59
feat(anthropic): implement DTOs for Anthropic API requests and responses
Aias00 Jan 8, 2026
a2e2e24
Merge branch 'refs/heads/main' into feat/replace_claude
Aias00 Jan 10, 2026
252e514
feat(anthropic): enhance multi-agent conversation handling in Anthrop…
Aias00 Jan 10, 2026
1d445d5
feat(anthropic): improve multi-agent conversation detection and messa…
Aias00 Jan 10, 2026
3196b55
Update agentscope-core/src/main/java/io/agentscope/core/formatter/ant…
Aias00 Jan 10, 2026
4aa7870
feat(anthropic): return unmodifiable view of metadata map in Anthropi…
Aias00 Jan 10, 2026
68d7c1e
Revert "feat(anthropic): return unmodifiable view of metadata map in …
Aias00 Jan 10, 2026
4e57829
Merge branch 'feat/replace_claude' of https://github.com/Aias00/agent…
Aias00 Jan 10, 2026
3f4f826
feat(anthropic): return unmodifiable view of metadata and enhance mul…
Aias00 Jan 11, 2026
06226bb
test(anthropic): update message_delta event handling in stream parser…
Aias00 Jan 11, 2026
a289a69
Merge branch 'main' into feat/replace_claude
Aias00 Jan 12, 2026
86307f0
Merge branch 'main' into feat/replace_claude
Aias00 Jan 19, 2026
0768b6d
Merge branch 'refs/heads/main' into feat/replace_claude
Aias00 Jan 21, 2026
5ecf547
Merge branch 'main' into feat/replace_claude
Aias00 Jan 22, 2026
cc3faaf
Merge branch 'refs/heads/main' into feat/replace_claude
Aias00 Mar 10, 2026
8004bdf
feat(anthropic): enhance tool application and conversation formatting
Aias00 Mar 10, 2026
b9851f1
feat(anthropic): return defensive copy of metadata in getter and setter
Aias00 Mar 10, 2026
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 @@ -113,12 +113,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,89 @@
*/
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.ToolChoice;
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). */
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) {
// Save options for applyTools
currentOptions.set(options);

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

/**
* Apply tool schemas to Anthropic request parameters. This method uses the options saved from
* applyOptions to apply tool choice configuration.
* Apply tool schemas to Anthropic request parameters.
*
* @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) {
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);
public void applyTools(AnthropicRequest request, List<ToolSchema> 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.
*
* <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