Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -730,7 +730,7 @@ private Msg generateCurrentRoundSummaryFromMessages(List<Msg> messages, String o
TextBlock.builder()
.text(
PromptProvider.getCurrentRoundCompressPrompt(
customPrompt))
customPrompt, filteredMessages))
.build())
.build());
newMessages.addAll(filteredMessages);
Expand Down Expand Up @@ -1095,7 +1095,7 @@ private Msg summaryPreviousRoundConversation(List<Msg> messages, String offloadU
TextBlock.builder()
.text(
PromptProvider.getPreviousRoundSummaryPrompt(
customPrompt))
customPrompt, filteredMessages))
.build())
.build());
newMessages.addAll(filteredMessages);
Expand Down Expand Up @@ -1599,7 +1599,7 @@ private Msg compressToolsInvocation(List<Msg> messages, String offloadUUid) {
TextBlock.builder()
.text(
PromptProvider.getPreviousRoundToolCompressPrompt(
customPrompt))
customPrompt, filteredMessages))
.build())
.build());
newMessages.addAll(filteredMessages);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Msg> messages) {
return basePrompt + "\n\n" + inferLanguageRequirement(messages);
}

static String inferLanguageRequirement(List<Msg> 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<Msg> 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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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<Msg> 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.
Expand All @@ -57,6 +66,12 @@ public static String getPreviousRoundSummaryPrompt(PromptConfig customPrompt) {
return Prompts.PREVIOUS_ROUND_CONVERSATION_SUMMARY_PROMPT;
}

public static String getPreviousRoundSummaryPrompt(
PromptConfig customPrompt, List<Msg> 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.
Expand All @@ -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<Msg> 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.
Expand All @@ -93,4 +114,10 @@ public static String getCurrentRoundCompressPrompt(PromptConfig customPrompt) {
}
return Prompts.CURRENT_ROUND_MESSAGE_COMPRESS_PROMPT;
}

public static String getCurrentRoundCompressPrompt(
PromptConfig customPrompt, List<Msg> messages) {
return CompressionLanguageHintResolver.appendLanguageRequirement(
getCurrentRoundCompressPrompt(customPrompt), messages);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
}
Loading