diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIConversationMerger.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIConversationMerger.java
index 6bd06ed3d..4768a87b1 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIConversationMerger.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/openai/OpenAIConversationMerger.java
@@ -35,11 +35,11 @@
import org.slf4j.LoggerFactory;
/**
- * Merges multi-agent conversation messages for OpenAI HTTP API.
- * Consolidates multiple agent messages into single user messages with history tags.
+ * Merges multi-agent conversation messages for OpenAI HTTP API. Consolidates multiple agent
+ * messages into single user messages with history tags.
*
- *
This class combines all agent messages into a single user message with conversation
- * history wrapped in special tags. Images and audio are preserved as separate ContentParts.
+ *
This class combines all agent messages into a single user message with conversation history
+ * wrapped in special tags. Images and audio are preserved as separate ContentParts.
*/
public class OpenAIConversationMerger {
@@ -132,10 +132,6 @@ private void processMessage(
List allParts,
boolean includePrefix) {
String agentName = msg.getName();
- String roleLabel = roleFormatter.apply(msg);
- if (roleLabel == null) {
- roleLabel = "Unknown";
- }
// Process all blocks
List blocks = msg.getContent();
@@ -145,7 +141,7 @@ private void processMessage(
for (ContentBlock block : blocks) {
if (block instanceof TextBlock tb) {
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append(tb.getText()).append("\n");
@@ -162,7 +158,7 @@ private void processMessage(
if (source == null) {
log.warn("ImageBlock has null source, skipping");
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append("[Image - null source]\n");
} else {
@@ -174,7 +170,7 @@ private void processMessage(
e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
log.warn("Failed to process ImageBlock: {}", errorMsg);
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer
.append("[Image - processing failed: ")
@@ -195,7 +191,7 @@ private void processMessage(
if (source == null) {
log.warn("VideoBlock has null source, skipping");
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append("[Video - null source]\n");
} else {
@@ -207,7 +203,7 @@ private void processMessage(
e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
log.warn("Failed to process VideoBlock: {}", errorMsg);
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer
.append("[Video - processing failed: ")
@@ -228,7 +224,7 @@ private void processMessage(
if (source == null) {
log.warn("AudioBlock has null source, skipping");
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append("[Audio - null source]\n");
} else if (source instanceof Base64Source b64) {
@@ -236,7 +232,7 @@ private void processMessage(
if (audioData == null || audioData.isEmpty()) {
log.warn("Base64Source has null or empty data, skipping");
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append("[Audio - null or empty data]\n");
} else {
@@ -248,7 +244,7 @@ private void processMessage(
if (url == null || url.isEmpty()) {
log.warn("URLSource has null or empty URL, skipping");
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append("[Audio - null or empty URL]\n");
} else {
@@ -256,14 +252,14 @@ private void processMessage(
"URL-based audio not directly supported, using text"
+ " reference");
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append("[Audio URL: ").append(url).append("]\n");
}
} else {
log.warn("Unknown audio source type: {}", source.getClass());
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
}
textBuffer.append("[Audio - unsupported source type]\n");
}
@@ -271,7 +267,7 @@ private void processMessage(
String errorMsg =
e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
log.warn("Failed to process AudioBlock: {}", errorMsg);
- appendRoleAndName(textBuffer, roleLabel, agentName);
+ appendNamePrefix(textBuffer, agentName);
textBuffer
.append("[Audio - processing failed: ")
.append(errorMsg)
@@ -281,11 +277,16 @@ private void processMessage(
} else if (block instanceof ThinkingBlock thinkingBlock) {
// Include ThinkingBlock in conversation history for models that support reasoning
if (includePrefix) {
- appendRoleAndName(textBuffer, roleLabel, agentName);
- }
- String thinking = thinkingBlock.getThinking();
- if (thinking != null && !thinking.isEmpty()) {
- textBuffer.append("[Thinking]: ").append(thinking).append("\n");
+ appendNamePrefix(textBuffer, agentName);
+ String thinking = thinkingBlock.getThinking();
+ if (thinking != null && !thinking.isEmpty()) {
+ textBuffer.append("[Thinking]: ").append(thinking).append("\n");
+ }
+ } else {
+ String thinking = thinkingBlock.getThinking();
+ if (thinking != null && !thinking.isEmpty()) {
+ textBuffer.append("[Thinking]: ").append(thinking).append("\n");
+ }
}
} else if (block instanceof ToolResultBlock toolResult) {
// Use provided converter to handle multimodal content in tool results
@@ -295,13 +296,8 @@ private void processMessage(
? resultText
: "[Empty tool result]";
- // For tool results, we format slightly differently to include tool name
- textBuffer.append(roleLabel);
- if (agentName != null
- && !agentName.equals(roleLabel)
- && !agentName.equals("Unknown")) {
- textBuffer.append(" ").append(agentName);
- }
+ // For tool results, format as: name (tool_name): result
+ textBuffer.append(agentName);
textBuffer
.append(" (")
.append(toolResult.getName())
@@ -312,31 +308,23 @@ private void processMessage(
}
}
- private void appendRoleAndName(StringBuilder buffer, String roleLabel, String agentName) {
- buffer.append(roleLabel);
- if (agentName != null && !agentName.equals(roleLabel) && !agentName.equals("Unknown")) {
- buffer.append(" ").append(agentName);
+ private void appendNamePrefix(StringBuilder buffer, String agentName) {
+ if (agentName != null && !agentName.isEmpty()) {
+ buffer.append(agentName).append(": ");
}
- buffer.append(": ");
}
- /**
- * Convert image Source to URL string for OpenAI API.
- */
+ /** Convert image Source to URL string for OpenAI API. */
private String convertImageSourceToUrl(Source source) {
return OpenAIConverterUtils.convertImageSourceToUrl(source);
}
- /**
- * Convert video Source to URL string for OpenAI API.
- */
+ /** Convert video Source to URL string for OpenAI API. */
private String convertVideoSourceToUrl(Source source) {
return OpenAIConverterUtils.convertVideoSourceToUrl(source);
}
- /**
- * Detect audio format from media type.
- */
+ /** Detect audio format from media type. */
private String detectAudioFormat(String mediaType) {
return OpenAIConverterUtils.detectAudioFormat(mediaType);
}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIConversationMergerTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIConversationMergerTest.java
index 5dbdba33a..9581384a4 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIConversationMergerTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/openai/OpenAIConversationMergerTest.java
@@ -332,4 +332,691 @@ void testMixedContentWithNullHandling() {
assertTrue(content.contains("Message 1"), "Should contain first message");
assertTrue(content.contains("Message 2"), "Should contain second message");
}
+
+ @Test
+ @DisplayName("Should format history with only name prefix without roleLabel")
+ void testHistoryFormatWithNameOnly() {
+ List messages = new ArrayList<>();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("Hello").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(TextBlock.builder().text("Hi there").build()))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+
+ // Verify format is "name: text" without roleLabel
+ assertTrue(content.contains("Alice: Hello"), "Should format as 'Alice: Hello'");
+ assertTrue(content.contains("Bob: Hi there"), "Should format as 'Bob: Hi there'");
+
+ // Verify roleLabel (USER/ASSISTANT) is NOT present
+ int userIndex = content.indexOf("USER");
+ int assistantIndex = content.indexOf("ASSISTANT");
+ assertTrue(
+ userIndex == -1 || userIndex > content.indexOf("Alice: Hello"),
+ "Should not contain USER roleLabel before Alice's message");
+ assertTrue(
+ assistantIndex == -1 || assistantIndex > content.indexOf("Bob: Hi there"),
+ "Should not contain ASSISTANT roleLabel before Bob's message");
+ }
+
+ @Test
+ @DisplayName("Should format ToolResultBlock with name only")
+ void testToolResultFormatWithNameOnly() {
+ List messages = new ArrayList<>();
+
+ io.agentscope.core.message.ToolResultBlock toolResult =
+ io.agentscope.core.message.ToolResultBlock.builder()
+ .name("search_tool")
+ .output(List.of(TextBlock.builder().text("Search completed").build()))
+ .build();
+
+ Msg msg =
+ Msg.builder()
+ .role(MsgRole.TOOL)
+ .name("ToolAgent")
+ .content(List.of(toolResult))
+ .build();
+
+ messages.add(msg);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages,
+ msg2 -> msg2.getRole().toString(),
+ blocks -> {
+ StringBuilder sb = new StringBuilder();
+ for (var block : blocks) {
+ if (block instanceof TextBlock tb) {
+ sb.append(tb.getText());
+ }
+ }
+ return sb.toString();
+ });
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+
+ // Verify format is "name (tool_name): result"
+ assertTrue(
+ content.contains("ToolAgent (search_tool): Search completed"),
+ "Should format as 'ToolAgent (search_tool): Search completed'");
+
+ // Verify roleLabel is NOT present
+ assertTrue(
+ !content.contains("TOOL ToolAgent"), "Should not contain 'TOOL ToolAgent' format");
+ }
+
+ @Test
+ @DisplayName("Should handle ImageBlock with null source")
+ void testImageBlockWithNullSource() {
+ List messages = new ArrayList<>();
+
+ // Use reflection to create ImageBlock with null source
+ ImageBlock imageBlock;
+ try {
+ java.lang.reflect.Field sourceField = ImageBlock.class.getDeclaredField("source");
+ sourceField.setAccessible(true);
+ imageBlock =
+ ImageBlock.builder().source(URLSource.builder().url("temp").build()).build();
+ sourceField.set(imageBlock, null);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create test ImageBlock", e);
+ }
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(imageBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Image - null source]"),
+ "Should handle null image source with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle ImageBlock processing failure")
+ void testImageBlockProcessingFailure() {
+ List messages = new ArrayList<>();
+
+ // Create Base64Source with empty data to trigger processing failure
+ Base64Source invalidSource = Base64Source.builder().data("").mediaType("image/png").build();
+ ImageBlock imageBlock = ImageBlock.builder().source(invalidSource).build();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(imageBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Image - processing failed:"),
+ "Should handle image processing failure with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should format multimodal content with name prefix only")
+ void testMultimodalFormatWithNameOnly() {
+ List messages = new ArrayList<>();
+
+ URLSource imageSource = URLSource.builder().url("http://example.com/pic.jpg").build();
+ ImageBlock imageBlock = ImageBlock.builder().source(imageSource).build();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("Look at this").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(
+ List.of(
+ TextBlock.builder().text("Interesting").build(),
+ imageBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ assertTrue(result.isMultimodal() || result.getContentAsString() != null);
+
+ if (!result.isMultimodal()) {
+ String content = result.getContentAsString();
+ assertTrue(
+ content.contains("Alice: Look at this"),
+ "Should format as 'Alice: Look at this'");
+ assertTrue(content.contains("Bob: Interesting"), "Should format as 'Bob: Interesting'");
+ }
+ }
+
+ @Test
+ @DisplayName("Should handle ThinkingBlock with name prefix only")
+ void testThinkingBlockFormatWithNameOnly() {
+ List messages = new ArrayList<>();
+
+ // Add a first message to make thinking message part of history
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("User")
+ .content(List.of(TextBlock.builder().text("Question").build()))
+ .build();
+
+ io.agentscope.core.message.ThinkingBlock thinkingBlock =
+ io.agentscope.core.message.ThinkingBlock.builder()
+ .thinking("Let me analyze this...")
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Thinker")
+ .content(
+ List.of(
+ thinkingBlock,
+ TextBlock.builder().text("My conclusion").build()))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+
+ assertTrue(
+ content.contains("Thinker: [Thinking]: Let me analyze this..."),
+ "Should include thinking with name prefix");
+ assertTrue(
+ content.contains("Thinker: My conclusion"), "Should include text with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle VideoBlock with null source")
+ void testVideoBlockWithNullSource() {
+ List messages = new ArrayList<>();
+
+ // Use reflection to create VideoBlock with null source
+ io.agentscope.core.message.VideoBlock videoBlock;
+ try {
+ java.lang.reflect.Field sourceField =
+ io.agentscope.core.message.VideoBlock.class.getDeclaredField("source");
+ sourceField.setAccessible(true);
+ videoBlock =
+ io.agentscope.core.message.VideoBlock.builder()
+ .source(URLSource.builder().url("temp").build())
+ .build();
+ sourceField.set(videoBlock, null);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create test VideoBlock", e);
+ }
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(videoBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Video - null source]"),
+ "Should handle null video source with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle AudioBlock with null source")
+ void testAudioBlockWithNullSource() {
+ List messages = new ArrayList<>();
+
+ // Use reflection to create AudioBlock with null source
+ AudioBlock audioBlock;
+ try {
+ java.lang.reflect.Field sourceField = AudioBlock.class.getDeclaredField("source");
+ sourceField.setAccessible(true);
+ audioBlock =
+ AudioBlock.builder().source(URLSource.builder().url("temp").build()).build();
+ sourceField.set(audioBlock, null);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create test AudioBlock", e);
+ }
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(audioBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Audio - null source]"),
+ "Should handle null audio source with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle AudioBlock with empty Base64 data")
+ void testAudioBlockWithEmptyBase64Data() {
+ List messages = new ArrayList<>();
+
+ Base64Source audioSource = Base64Source.builder().data("").mediaType("audio/wav").build();
+ AudioBlock audioBlock = AudioBlock.builder().source(audioSource).build();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(audioBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Audio - null or empty data]"),
+ "Should handle empty audio data with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle AudioBlock with empty URL")
+ void testAudioBlockWithEmptyURL() {
+ List messages = new ArrayList<>();
+
+ URLSource audioSource = URLSource.builder().url("").build();
+ AudioBlock audioBlock = AudioBlock.builder().source(audioSource).build();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(audioBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Audio - null or empty URL]"),
+ "Should handle empty audio URL with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle AudioBlock with valid URL")
+ void testAudioBlockWithValidURL() {
+ List messages = new ArrayList<>();
+
+ URLSource audioSource = URLSource.builder().url("http://example.com/audio.mp3").build();
+ AudioBlock audioBlock = AudioBlock.builder().source(audioSource).build();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(audioBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Audio URL: http://example.com/audio.mp3]"),
+ "Should handle valid audio URL with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle AudioBlock with unsupported source type")
+ void testAudioBlockWithUnsupportedSourceType() {
+ List messages = new ArrayList<>();
+
+ // Use reflection to create AudioBlock with custom Source subclass
+ AudioBlock audioBlock;
+ try {
+ java.lang.reflect.Field sourceField = AudioBlock.class.getDeclaredField("source");
+ sourceField.setAccessible(true);
+ audioBlock =
+ AudioBlock.builder().source(URLSource.builder().url("temp").build()).build();
+ // Create an anonymous Source subclass (neither URLSource nor Base64Source)
+ io.agentscope.core.message.Source customSource =
+ new io.agentscope.core.message.Source() {};
+ sourceField.set(audioBlock, customSource);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create test AudioBlock", e);
+ }
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(audioBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Audio - unsupported source type]"),
+ "Should handle unsupported audio source type with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle VideoBlock processing failure")
+ void testVideoBlockProcessingFailure() {
+ List messages = new ArrayList<>();
+
+ // Create Base64Source with empty data to trigger processing failure
+ Base64Source invalidSource = Base64Source.builder().data("").mediaType("video/mp4").build();
+ io.agentscope.core.message.VideoBlock videoBlock =
+ io.agentscope.core.message.VideoBlock.builder().source(invalidSource).build();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content(List.of(videoBlock))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("Bob: [Video - processing failed:"),
+ "Should handle video processing failure with name prefix");
+ }
+
+ @Test
+ @DisplayName("Should handle message with null content blocks")
+ void testMessageWithNullContentBlocks() {
+ List messages = new ArrayList<>();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("Bob")
+ .content((List) null)
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ }
+
+ @Test
+ @DisplayName("Should handle ToolResultBlock with empty result")
+ void testToolResultBlockWithEmptyResult() {
+ List messages = new ArrayList<>();
+
+ io.agentscope.core.message.ToolResultBlock toolResult =
+ io.agentscope.core.message.ToolResultBlock.builder()
+ .name("empty_tool")
+ .output(List.of())
+ .build();
+
+ Msg msg =
+ Msg.builder()
+ .role(MsgRole.TOOL)
+ .name("ToolAgent")
+ .content(List.of(toolResult))
+ .build();
+
+ messages.add(msg);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(messages, m -> m.getRole().toString(), blocks -> "");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(
+ content.contains("ToolAgent (empty_tool): [Empty tool result]"),
+ "Should handle empty tool result");
+ }
+
+ @Test
+ @DisplayName("Should handle appendNamePrefix with null agentName")
+ void testAppendNamePrefixWithNullAgentName() {
+ List messages = new ArrayList<>();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name(null)
+ .content(List.of(TextBlock.builder().text("No name").build()))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(content.contains("No name"), "Should handle null agent name");
+ }
+
+ @Test
+ @DisplayName("Should handle appendNamePrefix with empty agentName")
+ void testAppendNamePrefixWithEmptyAgentName() {
+ List messages = new ArrayList<>();
+
+ Msg msg1 =
+ Msg.builder()
+ .role(MsgRole.USER)
+ .name("Alice")
+ .content(List.of(TextBlock.builder().text("First").build()))
+ .build();
+
+ Msg msg2 =
+ Msg.builder()
+ .role(MsgRole.ASSISTANT)
+ .name("")
+ .content(List.of(TextBlock.builder().text("Empty name").build()))
+ .build();
+
+ messages.add(msg1);
+ messages.add(msg2);
+
+ OpenAIMessage result =
+ merger.mergeToUserMessage(
+ messages, msg -> msg.getRole().toString(), blocks -> "Tool result");
+
+ assertNotNull(result);
+ String content = result.getContentAsString();
+ assertNotNull(content);
+ assertTrue(content.contains("Empty name"), "Should handle empty agent name");
+ }
}