From ea80a071af73bd96d7dc403c328c406b6427e8cb Mon Sep 17 00:00:00 2001 From: feelshana <151412598@qq.com> Date: Thu, 5 Feb 2026 18:23:40 +0800 Subject: [PATCH 1/6] fix(core):prevent openAIConversationMerger merge history message with messageRole (#731) --- .../io/agentscope/core/agent/AgentBase.java | 74 ++++++++++--------- .../openai/OpenAIConversationMerger.java | 65 ++++++---------- .../core/tool/subagent/SubAgentTool.java | 62 +++++++++------- 3 files changed, 101 insertions(+), 100 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java b/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java index a494c1c8d..0d266fe08 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java +++ b/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java @@ -664,40 +664,46 @@ public final Flux stream( * @return Flux of events emitted during execution */ private Flux createEventStream(StreamOptions options, Supplier> callSupplier) { - return Flux.create( - sink -> { - // Create streaming hook with options - StreamingHook streamingHook = new StreamingHook(sink, options); - - // Add temporary hook - hooks.add(streamingHook); - - // Execute call and manage hook lifecycle - callSupplier - .get() - .doFinally( - signalType -> { - // Remove temporary hook - hooks.remove(streamingHook); - }) - .subscribe( - finalMsg -> { - if (options.shouldStream(EventType.AGENT_RESULT)) { - Event finalEvent = - new Event( - EventType.AGENT_RESULT, - finalMsg, - true); - sink.next(finalEvent); - } - - // Complete the stream - sink.complete(); - }, - sink::error); - }, - FluxSink.OverflowStrategy.BUFFER) - .publishOn(Schedulers.boundedElastic()); + return Flux.deferContextual( + ctxView -> + Flux.create( + sink -> { + // Create streaming hook with options + StreamingHook streamingHook = + new StreamingHook(sink, options); + + // Add temporary hook + hooks.add(streamingHook); + + // Use Mono.defer to ensure trace context propagation + // while maintaining streaming hook functionality + Mono.defer(() -> callSupplier.get()) + .contextWrite( + context -> context.putAll(ctxView)) + .doFinally( + signalType -> { + // Remove temporary hook + hooks.remove(streamingHook); + }) + .subscribe( + finalMsg -> { + if (options.shouldStream( + EventType.AGENT_RESULT)) { + sink.next( + new Event( + EventType + .AGENT_RESULT, + finalMsg, + true)); + } + + // Complete the stream + sink.complete(); + }, + sink::error); + }, + FluxSink.OverflowStrategy.BUFFER) + .publishOn(Schedulers.boundedElastic())); } @Override 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..4c6dc1831 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,7 +277,7 @@ 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); + appendNamePrefix(textBuffer, agentName); } String thinking = thinkingBlock.getThinking(); if (thinking != null && !thinking.isEmpty()) { @@ -295,13 +291,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 +303,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/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java b/agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java index 8d5b581d1..d0cd8d0f6 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java @@ -122,8 +122,8 @@ public Mono callAsync(ToolCallParam param) { * @return A Mono emitting the tool result block */ private Mono executeConversation(ToolCallParam param) { - return Mono.defer( - () -> { + return Mono.deferContextual( + (ctxView) -> { try { Map input = param.getInput(); @@ -246,21 +246,28 @@ private Mono executeWithStreaming( ? config.getStreamOptions() : StreamOptions.defaults(); - return agent.stream(List.of(userMsg), streamOptions) - .doOnNext(event -> forwardEvent(event, emitter, agent, sessionId)) - .filter(Event::isLast) - .last() - .map( - lastEvent -> { - Msg response = lastEvent.getMessage(); - return buildResult(response, sessionId); - }) - .onErrorResume( - e -> { - logger.error("Error in streaming execution: {}", e.getMessage(), e); - return Mono.just( - ToolResultBlock.error("Execution error: " + e.getMessage())); - }); + return Mono.deferContextual( + ctxView -> + agent.stream(List.of(userMsg), streamOptions) + .doOnNext(event -> forwardEvent(event, emitter, agent, sessionId)) + .filter(Event::isLast) + .last() + .map( + lastEvent -> { + Msg response = lastEvent.getMessage(); + return buildResult(response, sessionId); + }) + .contextWrite(context -> context.putAll(ctxView)) + .onErrorResume( + e -> { + logger.error( + "Error in streaming execution:" + " {}", + e.getMessage(), + e); + return Mono.just( + ToolResultBlock.error( + "Execution error: " + e.getMessage())); + })); } /** @@ -276,14 +283,19 @@ private Mono executeWithStreaming( private Mono executeWithoutStreaming( Agent agent, Msg userMsg, String sessionId) { - return agent.call(List.of(userMsg)) - .map(response -> buildResult(response, sessionId)) - .onErrorResume( - e -> { - logger.error("Error in execution: {}", e.getMessage(), e); - return Mono.just( - ToolResultBlock.error("Execution error: " + e.getMessage())); - }); + return Mono.deferContextual( + ctxView -> + agent.call(List.of(userMsg)) + .map(response -> buildResult(response, sessionId)) + .onErrorResume( + e -> { + logger.error( + "Error in execution: {}", e.getMessage(), e); + return Mono.just( + ToolResultBlock.error( + "Execution error: " + e.getMessage())); + }) + .contextWrite(context -> context.putAll(ctxView))); } /** From 4c5af9e9c43c2e083c2e8f51af9bacfe43d6d019 Mon Sep 17 00:00:00 2001 From: feelshana <151412598@qq.com> Date: Thu, 5 Feb 2026 18:23:40 +0800 Subject: [PATCH 2/6] fix(core):prevent openAIConversationMerger merge history message with messageRole (#731) --- .../io/agentscope/core/agent/AgentBase.java | 74 ++++++++++--------- .../openai/OpenAIConversationMerger.java | 65 ++++++---------- .../core/tool/subagent/SubAgentTool.java | 62 +++++++++------- 3 files changed, 101 insertions(+), 100 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java b/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java index a494c1c8d..0d266fe08 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java +++ b/agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java @@ -664,40 +664,46 @@ public final Flux stream( * @return Flux of events emitted during execution */ private Flux createEventStream(StreamOptions options, Supplier> callSupplier) { - return Flux.create( - sink -> { - // Create streaming hook with options - StreamingHook streamingHook = new StreamingHook(sink, options); - - // Add temporary hook - hooks.add(streamingHook); - - // Execute call and manage hook lifecycle - callSupplier - .get() - .doFinally( - signalType -> { - // Remove temporary hook - hooks.remove(streamingHook); - }) - .subscribe( - finalMsg -> { - if (options.shouldStream(EventType.AGENT_RESULT)) { - Event finalEvent = - new Event( - EventType.AGENT_RESULT, - finalMsg, - true); - sink.next(finalEvent); - } - - // Complete the stream - sink.complete(); - }, - sink::error); - }, - FluxSink.OverflowStrategy.BUFFER) - .publishOn(Schedulers.boundedElastic()); + return Flux.deferContextual( + ctxView -> + Flux.create( + sink -> { + // Create streaming hook with options + StreamingHook streamingHook = + new StreamingHook(sink, options); + + // Add temporary hook + hooks.add(streamingHook); + + // Use Mono.defer to ensure trace context propagation + // while maintaining streaming hook functionality + Mono.defer(() -> callSupplier.get()) + .contextWrite( + context -> context.putAll(ctxView)) + .doFinally( + signalType -> { + // Remove temporary hook + hooks.remove(streamingHook); + }) + .subscribe( + finalMsg -> { + if (options.shouldStream( + EventType.AGENT_RESULT)) { + sink.next( + new Event( + EventType + .AGENT_RESULT, + finalMsg, + true)); + } + + // Complete the stream + sink.complete(); + }, + sink::error); + }, + FluxSink.OverflowStrategy.BUFFER) + .publishOn(Schedulers.boundedElastic())); } @Override 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..4c6dc1831 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,7 +277,7 @@ 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); + appendNamePrefix(textBuffer, agentName); } String thinking = thinkingBlock.getThinking(); if (thinking != null && !thinking.isEmpty()) { @@ -295,13 +291,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 +303,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/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java b/agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java index 8d5b581d1..d0cd8d0f6 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java +++ b/agentscope-core/src/main/java/io/agentscope/core/tool/subagent/SubAgentTool.java @@ -122,8 +122,8 @@ public Mono callAsync(ToolCallParam param) { * @return A Mono emitting the tool result block */ private Mono executeConversation(ToolCallParam param) { - return Mono.defer( - () -> { + return Mono.deferContextual( + (ctxView) -> { try { Map input = param.getInput(); @@ -246,21 +246,28 @@ private Mono executeWithStreaming( ? config.getStreamOptions() : StreamOptions.defaults(); - return agent.stream(List.of(userMsg), streamOptions) - .doOnNext(event -> forwardEvent(event, emitter, agent, sessionId)) - .filter(Event::isLast) - .last() - .map( - lastEvent -> { - Msg response = lastEvent.getMessage(); - return buildResult(response, sessionId); - }) - .onErrorResume( - e -> { - logger.error("Error in streaming execution: {}", e.getMessage(), e); - return Mono.just( - ToolResultBlock.error("Execution error: " + e.getMessage())); - }); + return Mono.deferContextual( + ctxView -> + agent.stream(List.of(userMsg), streamOptions) + .doOnNext(event -> forwardEvent(event, emitter, agent, sessionId)) + .filter(Event::isLast) + .last() + .map( + lastEvent -> { + Msg response = lastEvent.getMessage(); + return buildResult(response, sessionId); + }) + .contextWrite(context -> context.putAll(ctxView)) + .onErrorResume( + e -> { + logger.error( + "Error in streaming execution:" + " {}", + e.getMessage(), + e); + return Mono.just( + ToolResultBlock.error( + "Execution error: " + e.getMessage())); + })); } /** @@ -276,14 +283,19 @@ private Mono executeWithStreaming( private Mono executeWithoutStreaming( Agent agent, Msg userMsg, String sessionId) { - return agent.call(List.of(userMsg)) - .map(response -> buildResult(response, sessionId)) - .onErrorResume( - e -> { - logger.error("Error in execution: {}", e.getMessage(), e); - return Mono.just( - ToolResultBlock.error("Execution error: " + e.getMessage())); - }); + return Mono.deferContextual( + ctxView -> + agent.call(List.of(userMsg)) + .map(response -> buildResult(response, sessionId)) + .onErrorResume( + e -> { + logger.error( + "Error in execution: {}", e.getMessage(), e); + return Mono.just( + ToolResultBlock.error( + "Execution error: " + e.getMessage())); + }) + .contextWrite(context -> context.putAll(ctxView))); } /** From 5f4cced2acb329a71c8ebcd3795b84fbfc236d9f Mon Sep 17 00:00:00 2001 From: feelshana <151412598@qq.com> Date: Thu, 5 Feb 2026 19:07:28 +0800 Subject: [PATCH 3/6] fix(core):prevent openAIConversationMerger merge history message with messageRole (#731) --- .../openai/OpenAIConversationMergerTest.java | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) 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..b7ded22ae 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,181 @@ 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 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<>(); + + io.agentscope.core.message.ThinkingBlock thinkingBlock = + io.agentscope.core.message.ThinkingBlock.builder() + .thinking("Let me analyze this...") + .build(); + + Msg msg = + Msg.builder() + .role(MsgRole.ASSISTANT) + .name("Thinker") + .content( + List.of( + thinkingBlock, + TextBlock.builder().text("My conclusion").build())) + .build(); + + messages.add(msg); + + OpenAIMessage result = + merger.mergeToUserMessage( + messages, msg2 -> msg2.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"); + } } From 4791e120477b75ef030ccd84ab5a2b011c75cfa4 Mon Sep 17 00:00:00 2001 From: feelshana <151412598@qq.com> Date: Thu, 5 Feb 2026 19:19:19 +0800 Subject: [PATCH 4/6] fix(core):prevent openAIConversationMerger merge history message with messageRole (#731) --- .../openai/OpenAIConversationMerger.java | 13 ++++++---- .../openai/OpenAIConversationMergerTest.java | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 13 deletions(-) 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 4c6dc1831..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 @@ -278,10 +278,15 @@ private void processMessage( // Include ThinkingBlock in conversation history for models that support reasoning if (includePrefix) { appendNamePrefix(textBuffer, agentName); - } - String thinking = thinkingBlock.getThinking(); - if (thinking != null && !thinking.isEmpty()) { - textBuffer.append("[Thinking]: ").append(thinking).append("\n"); + 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 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 b7ded22ae..7e3d7a6fc 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 @@ -423,8 +423,7 @@ void testToolResultFormatWithNameOnly() { // Verify roleLabel is NOT present assertTrue( - !content.contains("TOOL ToolAgent"), - "Should not contain 'TOOL ToolAgent' format"); + !content.contains("TOOL ToolAgent"), "Should not contain 'TOOL ToolAgent' format"); } @Test @@ -467,8 +466,7 @@ void testMultimodalFormatWithNameOnly() { assertTrue( content.contains("Alice: Look at this"), "Should format as 'Alice: Look at this'"); - assertTrue( - content.contains("Bob: Interesting"), "Should format as 'Bob: Interesting'"); + assertTrue(content.contains("Bob: Interesting"), "Should format as 'Bob: Interesting'"); } } @@ -477,12 +475,20 @@ void testMultimodalFormatWithNameOnly() { 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 msg = + Msg msg2 = Msg.builder() .role(MsgRole.ASSISTANT) .name("Thinker") @@ -492,11 +498,12 @@ void testThinkingBlockFormatWithNameOnly() { TextBlock.builder().text("My conclusion").build())) .build(); - messages.add(msg); + messages.add(msg1); + messages.add(msg2); OpenAIMessage result = merger.mergeToUserMessage( - messages, msg2 -> msg2.getRole().toString(), blocks -> "Tool result"); + messages, msg -> msg.getRole().toString(), blocks -> "Tool result"); assertNotNull(result); String content = result.getContentAsString(); @@ -506,7 +513,6 @@ void testThinkingBlockFormatWithNameOnly() { 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"); + content.contains("Thinker: My conclusion"), "Should include text with name prefix"); } } From 5d385fa8dc2f65a3a38b5a27603f133b604fe9a3 Mon Sep 17 00:00:00 2001 From: feelshana <151412598@qq.com> Date: Fri, 6 Feb 2026 18:34:06 +0800 Subject: [PATCH 5/6] fix(core):prevent openAIConversationMerger merge history message with messageRole (#731) --- .../openai/OpenAIConversationMergerTest.java | 423 ++++++++++++++++++ 1 file changed, 423 insertions(+) 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 7e3d7a6fc..5c38e536b 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 @@ -426,6 +426,80 @@ void testToolResultFormatWithNameOnly() { !content.contains("TOOL ToolAgent"), "Should not contain 'TOOL ToolAgent' format"); } + @Test + @DisplayName("Should handle ImageBlock with null source") + void testImageBlockWithNullSource() { + List messages = new ArrayList<>(); + + ImageBlock imageBlock = ImageBlock.builder().source(null).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 - null source]"), + "Should handle null image source with name prefix"); + } + + @Test + @DisplayName("Should handle ImageBlock processing failure") + void testImageBlockProcessingFailure() { + List messages = new ArrayList<>(); + + // Create an invalid Base64Source that will cause processing to fail + Base64Source invalidSource = Base64Source.builder().data("invalid!!!").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() { @@ -515,4 +589,353 @@ void testThinkingBlockFormatWithNameOnly() { 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<>(); + + io.agentscope.core.message.VideoBlock videoBlock = + io.agentscope.core.message.VideoBlock.builder().source(null).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 - null source]"), + "Should handle null video source with name prefix"); + } + + @Test + @DisplayName("Should handle AudioBlock with null source") + void testAudioBlockWithNullSource() { + List messages = new ArrayList<>(); + + AudioBlock audioBlock = AudioBlock.builder().source(null).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 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 VideoBlock processing failure") + void testVideoBlockProcessingFailure() { + List messages = new ArrayList<>(); + + // Create an invalid source that will cause processing to fail + Base64Source invalidSource = Base64Source.builder().data("invalid!!!").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"); + } } From 718729f57e6155de232812647a8182f003a3ae12 Mon Sep 17 00:00:00 2001 From: feelshana <151412598@qq.com> Date: Fri, 6 Feb 2026 18:50:37 +0800 Subject: [PATCH 6/6] fix(core):prevent openAIConversationMerger merge history message with messageRole (#731) --- .../openai/OpenAIConversationMergerTest.java | 97 +++++++++++++++++-- 1 file changed, 89 insertions(+), 8 deletions(-) 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 5c38e536b..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 @@ -431,7 +431,17 @@ void testToolResultFormatWithNameOnly() { void testImageBlockWithNullSource() { List messages = new ArrayList<>(); - ImageBlock imageBlock = ImageBlock.builder().source(null).build(); + // 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() @@ -467,8 +477,8 @@ void testImageBlockWithNullSource() { void testImageBlockProcessingFailure() { List messages = new ArrayList<>(); - // Create an invalid Base64Source that will cause processing to fail - Base64Source invalidSource = Base64Source.builder().data("invalid!!!").build(); + // 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 = @@ -595,8 +605,20 @@ void testThinkingBlockFormatWithNameOnly() { void testVideoBlockWithNullSource() { List messages = new ArrayList<>(); - io.agentscope.core.message.VideoBlock videoBlock = - io.agentscope.core.message.VideoBlock.builder().source(null).build(); + // 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() @@ -632,7 +654,17 @@ void testVideoBlockWithNullSource() { void testAudioBlockWithNullSource() { List messages = new ArrayList<>(); - AudioBlock audioBlock = AudioBlock.builder().source(null).build(); + // 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() @@ -774,13 +806,62 @@ void testAudioBlockWithValidURL() { "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 an invalid source that will cause processing to fail - Base64Source invalidSource = Base64Source.builder().data("invalid!!!").build(); + // 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();