From 6b8f7ef3dc8e995b42c0747a60e09eb6cdf33ac2 Mon Sep 17 00:00:00 2001 From: "alexgangxi@163.com" Date: Mon, 6 Apr 2026 18:21:14 +0800 Subject: [PATCH] fix(autocontext): preserve compression language --- .../memory/autocontext/AutoContextMemory.java | 8 +- .../CompressionLanguageHintResolver.java | 139 ++++++++++++++++++ .../memory/autocontext/PromptProvider.java | 27 ++++ .../CompressionLanguageHintResolverTest.java | 100 +++++++++++++ .../autocontext/PromptProviderTest.java | 37 +++++ 5 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolver.java create mode 100644 agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolverTest.java diff --git a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/AutoContextMemory.java b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/AutoContextMemory.java index 4c450821a..7d308a818 100644 --- a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/AutoContextMemory.java +++ b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/AutoContextMemory.java @@ -582,7 +582,7 @@ private Msg generateLargeMessageSummary(Msg message, String offloadUuid) { TextBlock.builder() .text( PromptProvider.getCurrentRoundLargeMessagePrompt( - customPrompt)) + customPrompt, List.of(message))) .build()) .build()); newMessages.add(message); @@ -730,7 +730,7 @@ private Msg generateCurrentRoundSummaryFromMessages(List messages, String o TextBlock.builder() .text( PromptProvider.getCurrentRoundCompressPrompt( - customPrompt)) + customPrompt, filteredMessages)) .build()) .build()); newMessages.addAll(filteredMessages); @@ -1095,7 +1095,7 @@ private Msg summaryPreviousRoundConversation(List messages, String offloadU TextBlock.builder() .text( PromptProvider.getPreviousRoundSummaryPrompt( - customPrompt)) + customPrompt, filteredMessages)) .build()) .build()); newMessages.addAll(filteredMessages); @@ -1599,7 +1599,7 @@ private Msg compressToolsInvocation(List messages, String offloadUUid) { TextBlock.builder() .text( PromptProvider.getPreviousRoundToolCompressPrompt( - customPrompt)) + customPrompt, filteredMessages)) .build()) .build()); newMessages.addAll(filteredMessages); diff --git a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolver.java b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolver.java new file mode 100644 index 000000000..f587dc1b9 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolver.java @@ -0,0 +1,139 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.memory.autocontext; + +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import java.util.List; + +/** Resolves language-preservation hints for AutoContextMemory compression prompts. */ +final class CompressionLanguageHintResolver { + + private static final String SAME_LANGUAGE_REQUIREMENT = + "LANGUAGE REQUIREMENT:\n" + + "Write the compressed result in the same primary language as the first user" + + " message in the provided conversation. Preserve multilingual fragments," + + " technical terms, IDs, paths, and proper nouns exactly as they appear in" + + " the source content."; + + private static final String CHINESE_LANGUAGE_REQUIREMENT = + "LANGUAGE REQUIREMENT:\n" + + "Write the compressed result primarily in Chinese to match the user's" + + " language. Do not translate Chinese names, addresses, or domain-specific" + + " phrases into English or pinyin unless the original content already used" + + " that form. Preserve embedded English technical terms, IDs, paths, and" + + " proper nouns exactly as they appear in the source content."; + + private static final String ENGLISH_LANGUAGE_REQUIREMENT = + "LANGUAGE REQUIREMENT:\n" + + "Write the compressed result primarily in English to match the user's" + + " language. Preserve embedded Chinese or other multilingual fragments," + + " technical terms, IDs, paths, and proper nouns exactly as they appear in" + + " the source content."; + + private CompressionLanguageHintResolver() {} + + static String appendLanguageRequirement(String basePrompt, List messages) { + return basePrompt + "\n\n" + inferLanguageRequirement(messages); + } + + static String inferLanguageRequirement(List messages) { + String referenceText = extractReferenceText(messages); + if (referenceText.isBlank()) { + return SAME_LANGUAGE_REQUIREMENT; + } + + LanguagePreference languagePreference = detectLanguagePreference(referenceText); + if (languagePreference == LanguagePreference.CHINESE) { + return CHINESE_LANGUAGE_REQUIREMENT; + } + if (languagePreference == LanguagePreference.ENGLISH) { + return ENGLISH_LANGUAGE_REQUIREMENT; + } + return SAME_LANGUAGE_REQUIREMENT; + } + + private static String extractReferenceText(List messages) { + if (messages == null || messages.isEmpty()) { + return ""; + } + + for (Msg message : messages) { + if (message == null || message.getRole() != MsgRole.USER) { + continue; + } + String text = message.getTextContent(); + if (text != null && !text.isBlank()) { + return text; + } + } + + StringBuilder fallback = new StringBuilder(); + for (Msg message : messages) { + if (message == null) { + continue; + } + String text = message.getTextContent(); + if (text == null || text.isBlank()) { + continue; + } + if (!fallback.isEmpty()) { + fallback.append('\n'); + } + fallback.append(text); + } + return fallback.toString(); + } + + private static LanguagePreference detectLanguagePreference(String text) { + int chineseChars = 0; + int latinChars = 0; + + for (int i = 0; i < text.length(); i++) { + char current = text.charAt(i); + Character.UnicodeScript script = Character.UnicodeScript.of(current); + if (script == Character.UnicodeScript.HAN) { + chineseChars++; + continue; + } + if (isLatinLetter(current)) { + latinChars++; + } + } + + if (chineseChars == 0 && latinChars == 0) { + return LanguagePreference.UNKNOWN; + } + if (chineseChars >= latinChars && chineseChars > 0) { + return LanguagePreference.CHINESE; + } + if (latinChars > 0) { + return LanguagePreference.ENGLISH; + } + return LanguagePreference.UNKNOWN; + } + + private static boolean isLatinLetter(char current) { + return Character.isLetter(current) + && Character.UnicodeScript.of(current) == Character.UnicodeScript.LATIN; + } + + private enum LanguagePreference { + CHINESE, + ENGLISH, + UNKNOWN, + } +} diff --git a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/PromptProvider.java b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/PromptProvider.java index 6c4860f18..f3f892cbb 100644 --- a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/PromptProvider.java +++ b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/main/java/io/agentscope/core/memory/autocontext/PromptProvider.java @@ -15,6 +15,9 @@ */ package io.agentscope.core.memory.autocontext; +import io.agentscope.core.message.Msg; +import java.util.List; + /** * Utility class for providing prompts with fallback to defaults. * @@ -40,6 +43,12 @@ public static String getPreviousRoundToolCompressPrompt(PromptConfig customPromp return Prompts.PREVIOUS_ROUND_TOOL_INVOCATION_COMPRESS_PROMPT; } + public static String getPreviousRoundToolCompressPrompt( + PromptConfig customPrompt, List messages) { + return CompressionLanguageHintResolver.appendLanguageRequirement( + getPreviousRoundToolCompressPrompt(customPrompt), messages); + } + /** * Strategy 4: Gets the prompt for summarizing previous round conversations. * Returns custom prompt if provided, otherwise returns default from Prompts. @@ -57,6 +66,12 @@ public static String getPreviousRoundSummaryPrompt(PromptConfig customPrompt) { return Prompts.PREVIOUS_ROUND_CONVERSATION_SUMMARY_PROMPT; } + public static String getPreviousRoundSummaryPrompt( + PromptConfig customPrompt, List messages) { + return CompressionLanguageHintResolver.appendLanguageRequirement( + getPreviousRoundSummaryPrompt(customPrompt), messages); + } + /** * Strategy 5: Gets the prompt for summarizing current round large messages. * Returns custom prompt if provided, otherwise returns default from Prompts. @@ -74,6 +89,12 @@ public static String getCurrentRoundLargeMessagePrompt(PromptConfig customPrompt return Prompts.CURRENT_ROUND_LARGE_MESSAGE_SUMMARY_PROMPT; } + public static String getCurrentRoundLargeMessagePrompt( + PromptConfig customPrompt, List messages) { + return CompressionLanguageHintResolver.appendLanguageRequirement( + getCurrentRoundLargeMessagePrompt(customPrompt), messages); + } + /** * Strategy 6: Gets the prompt for compressing current round messages. * Returns custom prompt if provided, otherwise returns default from Prompts. @@ -93,4 +114,10 @@ public static String getCurrentRoundCompressPrompt(PromptConfig customPrompt) { } return Prompts.CURRENT_ROUND_MESSAGE_COMPRESS_PROMPT; } + + public static String getCurrentRoundCompressPrompt( + PromptConfig customPrompt, List messages) { + return CompressionLanguageHintResolver.appendLanguageRequirement( + getCurrentRoundCompressPrompt(customPrompt), messages); + } } diff --git a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolverTest.java b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolverTest.java new file mode 100644 index 000000000..8d37bf574 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/CompressionLanguageHintResolverTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.core.memory.autocontext; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.message.TextBlock; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("CompressionLanguageHintResolver Tests") +class CompressionLanguageHintResolverTest { + + @Test + @DisplayName("Should infer Chinese requirement from first user message") + void testInferChineseRequirementFromFirstUserMessage() { + String requirement = + CompressionLanguageHintResolver.inferLanguageRequirement( + List.of( + msg(MsgRole.USER, "请用中文总结一下这个结果"), + msg(MsgRole.ASSISTANT, "I will summarize it."))); + + assertTrue(requirement.contains("primarily in Chinese")); + assertTrue(requirement.contains("pinyin")); + } + + @Test + @DisplayName("Should infer English requirement from first user message") + void testInferEnglishRequirementFromFirstUserMessage() { + String requirement = + CompressionLanguageHintResolver.inferLanguageRequirement( + List.of( + msg( + MsgRole.USER, + "Please summarize the previous steps in English."), + msg(MsgRole.ASSISTANT, "好的,我会总结。"))); + + assertTrue(requirement.contains("primarily in English")); + assertTrue(requirement.contains("embedded Chinese")); + } + + @Test + @DisplayName("Should prefer first user message over later assistant language") + void testInferRequirementPrefersFirstUserMessage() { + String requirement = + CompressionLanguageHintResolver.inferLanguageRequirement( + List.of( + msg(MsgRole.USER, "北京市海淀区中关村软件园"), + msg(MsgRole.ASSISTANT, "The office is located in Beijing."), + msg(MsgRole.USER, "Please keep the important details."))); + + assertTrue(requirement.contains("primarily in Chinese")); + } + + @Test + @DisplayName("Should fall back to same-language requirement when no text exists") + void testInferRequirementFallsBackWithoutText() { + String requirement = + CompressionLanguageHintResolver.inferLanguageRequirement( + List.of(msg(MsgRole.USER, " "), msg(MsgRole.ASSISTANT, ""))); + + assertTrue(requirement.contains("same primary language")); + } + + @Test + @DisplayName("Should append language requirement to base prompt") + void testAppendLanguageRequirement() { + String prompt = + CompressionLanguageHintResolver.appendLanguageRequirement( + "Base prompt", List.of(msg(MsgRole.USER, "请继续用中文压缩上下文"))); + + assertTrue(prompt.startsWith("Base prompt")); + assertTrue(prompt.contains("LANGUAGE REQUIREMENT")); + assertTrue(prompt.contains("primarily in Chinese")); + } + + private static Msg msg(MsgRole role, String text) { + return Msg.builder() + .role(role) + .name(role.name().toLowerCase()) + .content(TextBlock.builder().text(text).build()) + .build(); + } +} diff --git a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/PromptProviderTest.java b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/PromptProviderTest.java index 5aefd5856..d9673772c 100644 --- a/agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/PromptProviderTest.java +++ b/agentscope-extensions/agentscope-extensions-autocontext-memory/src/test/java/io/agentscope/core/memory/autocontext/PromptProviderTest.java @@ -17,7 +17,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.MsgRole; +import io.agentscope.core.message.TextBlock; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -215,4 +220,36 @@ void testBlankStringPrompts() { Prompts.CURRENT_ROUND_MESSAGE_COMPRESS_PROMPT, PromptProvider.getCurrentRoundCompressPrompt(customPrompt)); } + + @Test + @DisplayName("Should append Chinese language hint for previous round summary prompt") + void testPreviousRoundSummaryPromptWithChineseLanguageHint() { + String prompt = + PromptProvider.getPreviousRoundSummaryPrompt( + null, List.of(msg(MsgRole.USER, "请继续用中文总结刚才的内容"))); + + assertTrue(prompt.startsWith(Prompts.PREVIOUS_ROUND_CONVERSATION_SUMMARY_PROMPT)); + assertTrue(prompt.contains("LANGUAGE REQUIREMENT")); + assertTrue(prompt.contains("primarily in Chinese")); + } + + @Test + @DisplayName("Should append English language hint for current round prompt") + void testCurrentRoundCompressPromptWithEnglishLanguageHint() { + String prompt = + PromptProvider.getCurrentRoundCompressPrompt( + null, List.of(msg(MsgRole.USER, "Please keep the summary in English."))); + + assertTrue(prompt.startsWith(Prompts.CURRENT_ROUND_MESSAGE_COMPRESS_PROMPT)); + assertTrue(prompt.contains("LANGUAGE REQUIREMENT")); + assertTrue(prompt.contains("primarily in English")); + } + + private static Msg msg(MsgRole role, String text) { + return Msg.builder() + .role(role) + .name(role.name().toLowerCase()) + .content(TextBlock.builder().text(text).build()) + .build(); + } }