mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
refactor(communication): create prompts for summarizer, and optimize message structure
This commit is contained in:
@@ -81,6 +81,7 @@ public class ActionCore implements StateSerializable {
|
||||
log.warn("{} tasks still running", count);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package work.slhaf.partner.core.cognition;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
@@ -21,6 +22,8 @@ public interface CognitionCapability {
|
||||
|
||||
void refreshRecentChatMessagesContext();
|
||||
|
||||
Element messageNotesElement();
|
||||
|
||||
Lock getMessageLock();
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.common.base.Block;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentRuntime;
|
||||
@@ -30,13 +31,22 @@ public class CognitionCore implements StateSerializable {
|
||||
|
||||
private static final String RECENT_CHAT_MESSAGE_NOTES = """
|
||||
消息格式:
|
||||
- user 消息当前写入格式: [[USER]: <userId>]: <正文> 或 [[AGENT]: self]: <正文>
|
||||
- assistant 消息直接记录回复正文;若以 [NOT_REPLIED]: <正文> 形式出现,表示该结果未直接发送给用户
|
||||
- 所有消息统一写为“标记行 + 空行 + 正文”,比如:
|
||||
|
||||
[[AGENT]: self]: [NOT_REPLIED][COMPRESSED]:
|
||||
|
||||
正文内容
|
||||
|
||||
- 标记行一定包含身份标签,通常格式为 [[USER]: <userName>] 或 [[AGENT]: self]
|
||||
- 若身份标签提取失败,可能回退为 [[Unknown]: Unknown]
|
||||
- 若存在其他标签,则写为“身份标签: 状态标签串:”
|
||||
- 正文永远从空行后开始
|
||||
|
||||
标记含义:
|
||||
- [USER]: 外部用户来源
|
||||
- [AGENT]: 系统内部来源
|
||||
- [NOT_REPLIED]: 仅保留在历史中的未直接回复结果
|
||||
- [COMPRESSED]: 该消息正文经过压缩
|
||||
""";
|
||||
|
||||
private final ReentrantLock messageLock = new ReentrantLock();
|
||||
@@ -117,7 +127,7 @@ public class CognitionCore implements StateSerializable {
|
||||
new BlockContent("recent_chat_messages", "communication_producer") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "message_tag_notes", RECENT_CHAT_MESSAGE_NOTES);
|
||||
document.appendChild(document.importNode(messageNotesElement(), true));
|
||||
Element chatMessagesElement = document.createElement("chat_messages");
|
||||
root.appendChild(chatMessagesElement);
|
||||
appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> {
|
||||
@@ -135,6 +145,16 @@ public class CognitionCore implements StateSerializable {
|
||||
contextWorkspace.register(block);
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public Element messageNotesElement() {
|
||||
return new Block("message_tag_notes") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
root.setTextContent(RECENT_CHAT_MESSAGE_NOTES);
|
||||
}
|
||||
}.encodeToXml();
|
||||
}
|
||||
|
||||
private List<Message> resolveRecentChatMessages() {
|
||||
int exclusiveEnd = Math.max(chatMessages.size() - 1, 0);
|
||||
if (exclusiveEnd == 0) {
|
||||
|
||||
@@ -29,8 +29,9 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
|
||||
private static final String INTERRUPTED_MARKER = " [response interrupted due to internal exception]";
|
||||
private static final String NO_REPLY_MARKER = "NO_REPLY";
|
||||
private static final String NOT_REPLIED_MARKER = "NOT_REPLIED";
|
||||
private static final String NOT_REPLIED_PREFIX = "[" + NOT_REPLIED_MARKER + "]: ";
|
||||
private static final String AGENT_MARKER = "[[AGENT]: self]";
|
||||
private static final String NOT_REPLIED_PREFIX = "[NOT_REPLIED]";
|
||||
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。
|
||||
@@ -86,9 +87,7 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
|
||||
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
|
||||
this.streamChat(buildChatMessages(runningFlowContext), consumer)
|
||||
.onFailure(exception -> {
|
||||
consumer.onDelta(INTERRUPTED_MARKER);
|
||||
});
|
||||
.onFailure(exception -> consumer.onDelta(INTERRUPTED_MARKER));
|
||||
updateChatMessages(runningFlowContext, consumer.collectResponse());
|
||||
cognitionCapability.refreshRecentChatMessagesContext();
|
||||
}
|
||||
@@ -115,7 +114,10 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
formatConversationUserMessage(runningFlowContext)
|
||||
);
|
||||
chatMessages.add(primaryUserMessage);
|
||||
Message assistantMessage = new Message(Message.Character.ASSISTANT, normalizeAssistantHistoryMessage(response));
|
||||
Message assistantMessage = new Message(
|
||||
Message.Character.ASSISTANT,
|
||||
normalizeAssistantHistoryMessage(response)
|
||||
);
|
||||
chatMessages.add(assistantMessage);
|
||||
} finally {
|
||||
cognitionCapability.getMessageLock().unlock();
|
||||
@@ -125,15 +127,23 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
private String normalizeAssistantHistoryMessage(String response) {
|
||||
String trimmed = response == null ? "" : response.trim();
|
||||
if (trimmed.equals(NO_REPLY_MARKER)) {
|
||||
return NOT_REPLIED_PREFIX.trim();
|
||||
return formatMarkedHistoryMessage(AGENT_MARKER, NOT_REPLIED_PREFIX, "");
|
||||
}
|
||||
if (trimmed.startsWith(NO_REPLY_MARKER + "\n")) {
|
||||
return NOT_REPLIED_PREFIX + trimmed.substring((NO_REPLY_MARKER + "\n").length()).trim();
|
||||
return formatMarkedHistoryMessage(
|
||||
AGENT_MARKER,
|
||||
NOT_REPLIED_PREFIX,
|
||||
trimmed.substring((NO_REPLY_MARKER + "\n").length()).trim()
|
||||
);
|
||||
}
|
||||
if (trimmed.startsWith(NO_REPLY_MARKER + "\r\n")) {
|
||||
return NOT_REPLIED_PREFIX + trimmed.substring((NO_REPLY_MARKER + "\r\n").length()).trim();
|
||||
return formatMarkedHistoryMessage(
|
||||
AGENT_MARKER,
|
||||
NOT_REPLIED_PREFIX,
|
||||
trimmed.substring((NO_REPLY_MARKER + "\r\n").length()).trim()
|
||||
);
|
||||
}
|
||||
return response;
|
||||
return formatMarkedHistoryMessage(AGENT_MARKER, "", trimmed);
|
||||
}
|
||||
|
||||
private List<Message> snapshotConversationMessages() {
|
||||
@@ -189,7 +199,17 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
}
|
||||
|
||||
private String formatConversationUserMessage(PartnerRunningFlowContext runningFlowContext) {
|
||||
return "[" + runningFlowContext.getSource() + "]" + ": " + runningFlowContext.formatInputsForHistory();
|
||||
return formatMarkedHistoryMessage("[" + runningFlowContext.getSource() + "]", "", runningFlowContext.formatInputsForHistory());
|
||||
}
|
||||
|
||||
private String formatMarkedHistoryMessage(String identityMarker, String statusMarkers, String body) {
|
||||
String markerLine = statusMarkers == null || statusMarkers.isBlank()
|
||||
? identityMarker
|
||||
: identityMarker + ": " + statusMarkers;
|
||||
if (body == null || body.isBlank()) {
|
||||
return markerLine + ":";
|
||||
}
|
||||
return markerLine + MARKER_BODY_SEPARATOR + body.trim();
|
||||
}
|
||||
|
||||
private Document newDocument() throws Exception {
|
||||
|
||||
@@ -24,8 +24,8 @@ import work.slhaf.partner.framework.agent.factory.component.annotation.InjectMod
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
|
||||
import work.slhaf.partner.module.communication.summarizer.MultiSummarizer;
|
||||
import work.slhaf.partner.module.communication.summarizer.SingleSummarizer;
|
||||
import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
|
||||
import work.slhaf.partner.module.communication.summarizer.MessageSummarizer;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -53,9 +53,9 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
@InjectModule
|
||||
private MultiSummarizer multiSummarizer;
|
||||
private MessageSummarizer messageSummarizer;
|
||||
@InjectModule
|
||||
private SingleSummarizer singleSummarizer;
|
||||
private MessageCompressor messageCompressor;
|
||||
@InjectModule
|
||||
private ActionScheduler actionScheduler;
|
||||
@InjectModule
|
||||
@@ -154,8 +154,8 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
|
||||
@NotNull
|
||||
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
|
||||
singleSummarizer.execute(chatSnapshot);
|
||||
Result<String> summaryResult = multiSummarizer.execute(chatSnapshot);
|
||||
messageCompressor.execute(chatSnapshot);
|
||||
Result<String> summaryResult = messageSummarizer.execute(chatSnapshot);
|
||||
String summary = summaryResult.fold(
|
||||
value -> value,
|
||||
exp -> "no summary, due to exception"
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package work.slhaf.partner.module.communication.summarizer;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class MessageCompressor extends AbstractAgentModule.Sub<List<Message>, Void> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责对单条消息进行压缩改写。
|
||||
|
||||
目标:
|
||||
- 在不改变原意的前提下,压缩冗余表达,减少长度;
|
||||
- 保留消息中真正有价值的信息;
|
||||
- 让压缩结果仍然像原消息,而不是另一种文体的摘要。
|
||||
|
||||
核心要求:
|
||||
- 尽量保留原消息的视角、语气、态度、情绪与表达倾向,不要无故改写成中性、客观、旁白式总结。
|
||||
- 不要把第一人称改成第三人称;不要把直接表达改成“用户表示……”“其意思是……”这类转述,除非原消息本身就是这种口吻。
|
||||
- 若原消息包含明显的情绪、评价、犹豫、强调、否定、推进意图、反问、吐槽等内容,压缩后应尽量保留这些信息。
|
||||
- 压缩的重点是删除冗余、合并重复、收紧表达,不是改写说话风格。
|
||||
|
||||
格式要求:
|
||||
- 允许保留原消息中已有的 markdown、标题、项目符号、编号列表、引用、代码块、代码片段等结构。
|
||||
- 不要为了压缩而强行去除这些结构;若这些结构本身承载了信息层级或语义边界,应尽量保留。
|
||||
- 也不要为了“更整齐”主动新增原消息没有的标题、列表或代码块。
|
||||
- 原消息有结构时,优先继承其组织方式;原消息没有结构时,保持自然文本即可。
|
||||
|
||||
压缩策略:
|
||||
- 删除明显重复、空转、口头垫话、对主旨无帮助的展开。
|
||||
- 合并语义接近、重复推进的句子。
|
||||
- 保留真正影响理解的事实、判断、条件、限制、结论、态度和情绪。
|
||||
- 若原消息包含技术内容、代码、配置、接口、规则、步骤等,优先保留这些实质信息,不要只保留泛泛结论。
|
||||
- 若原消息本身已经很短或进一步压缩会损失重要语义,则可基本保持原样。
|
||||
|
||||
关于日志、代码及长文本片段:
|
||||
- 若原消息中包含日志、代码、配置、报错堆栈、命令输出等长片段,且内容较长、重复性强或并非全部都对理解当前消息同等重要,则可以进行截断。
|
||||
- 截断时应优先保留:
|
||||
- 与当前问题、判断、结论直接相关的部分;
|
||||
- 首尾中能体现上下文和结果的关键部分;
|
||||
- 报错、异常、返回值、状态变化、关键参数、关键命令、关键代码段。
|
||||
- 不要无说明地直接删去中间内容;若发生截断,必须显式标注。
|
||||
- 截断标注统一使用以下格式之一,并与原文风格保持尽量一致:
|
||||
- `...[中间内容已截断]...`
|
||||
- 代码或日志块内可使用:`// ...[中间内容已截断]...` 或 `# ...[中间内容已截断]...`
|
||||
- 截断后的内容仍应保持可读,且不能歪曲原始含义。
|
||||
- 若长片段本身就是当前消息的核心,且截断会损失关键语义,则不要截断。
|
||||
|
||||
禁止事项:
|
||||
- 不要补充原消息没有的新信息。
|
||||
- 不要替原消息做解释、分析、总结或评价。
|
||||
- 不要把技术表达改写得过于口语化,也不要把口语表达改写得过于书面化。
|
||||
- 不要输出“压缩后:”之类前缀,只直接输出压缩结果。
|
||||
|
||||
输出要求:
|
||||
- 只输出压缩后的消息正文。
|
||||
""";
|
||||
|
||||
private static final int COMPRESS_TRIGGER_LENGTH = 1200;
|
||||
private static final int FALLBACK_MAX_LENGTH = 900;
|
||||
private static final String FALLBACK_OMITTED_MARKER = "\n...[中间内容已裁剪]...\n";
|
||||
private static final String COMPRESSED_MARKER = "[COMPRESSED]";
|
||||
private static final String UNKNOWN_ROLE_MARKER = "[[Unknown]: Unknown]";
|
||||
private static final String MARKER_BODY_SEPARATOR = ":\n\n";
|
||||
private static final Pattern ROLE_PREFIX_PATTERN = Pattern.compile("(\\[\\[(?:USER|AGENT)]:\\s*[^]]+])");
|
||||
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doExecute(List<Message> chatMessages) {
|
||||
List<Integer> targetIndexes = IntStream.range(0, chatMessages.size())
|
||||
.filter(index -> shouldCompress(chatMessages.get(index)))
|
||||
.boxed()
|
||||
.toList();
|
||||
CountDownLatch latch = new CountDownLatch(targetIndexes.size());
|
||||
for (Integer index : targetIndexes) {
|
||||
Message chatMessage = chatMessages.get(index);
|
||||
ParsedMessage parsedMessage = parseMessage(chatMessage.getContent());
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String summarized = summarizeOrFallback(parsedMessage.body());
|
||||
chatMessages.set(index, new Message(chatMessage.getRole(), rebuildMessage(parsedMessage, summarized)));
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean shouldCompress(Message chatMessage) {
|
||||
return parseMessage(chatMessage.getContent()).body().length() > COMPRESS_TRIGGER_LENGTH;
|
||||
}
|
||||
|
||||
private String summarizeOrFallback(String content) {
|
||||
String summarized = chat(List.of(new Message(Message.Character.USER, content))).fold(
|
||||
res -> res,
|
||||
exp -> null
|
||||
);
|
||||
if (isAcceptableSummary(summarized, content)) {
|
||||
return summarized.trim();
|
||||
}
|
||||
return truncateForFallback(content);
|
||||
}
|
||||
|
||||
private boolean isAcceptableSummary(String summarized, String originalContent) {
|
||||
if (summarized == null) {
|
||||
return false;
|
||||
}
|
||||
String normalized = summarized.trim();
|
||||
return !normalized.isEmpty() && normalized.length() < originalContent.length();
|
||||
}
|
||||
|
||||
private String truncateForFallback(String content) {
|
||||
if (content == null || content.length() <= FALLBACK_MAX_LENGTH) {
|
||||
return content;
|
||||
}
|
||||
int available = FALLBACK_MAX_LENGTH - FALLBACK_OMITTED_MARKER.length();
|
||||
int headBudget = available / 2;
|
||||
int tailBudget = available - headBudget;
|
||||
int headEnd = adjustHeadEnd(content, headBudget);
|
||||
int tailStart = adjustTailStart(content, content.length() - tailBudget);
|
||||
if (tailStart <= headEnd) {
|
||||
return content.substring(0, FALLBACK_MAX_LENGTH).stripTrailing();
|
||||
}
|
||||
return content.substring(0, headEnd).stripTrailing()
|
||||
+ FALLBACK_OMITTED_MARKER
|
||||
+ content.substring(tailStart).stripLeading();
|
||||
}
|
||||
|
||||
private int adjustHeadEnd(String content, int preferredEnd) {
|
||||
int safePreferredEnd = Math.clamp(preferredEnd, 0, content.length());
|
||||
int windowEnd = Math.min(content.length(), safePreferredEnd + 80);
|
||||
for (int i = safePreferredEnd; i < windowEnd; i++) {
|
||||
if (isBoundary(content.charAt(i))) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return safePreferredEnd;
|
||||
}
|
||||
|
||||
private int adjustTailStart(String content, int preferredStart) {
|
||||
int safePreferredStart = Math.clamp(preferredStart, 0, content.length());
|
||||
int windowStart = Math.max(0, safePreferredStart - 80);
|
||||
for (int i = safePreferredStart; i > windowStart; i--) {
|
||||
if (isBoundary(content.charAt(i - 1))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return safePreferredStart;
|
||||
}
|
||||
|
||||
private boolean isBoundary(char ch) {
|
||||
return ch == '\n'
|
||||
|| ch == '。'
|
||||
|| ch == '!'
|
||||
|| ch == '?'
|
||||
|| ch == ';'
|
||||
|| ch == ';'
|
||||
|| ch == '.';
|
||||
}
|
||||
|
||||
private ParsedMessage parseMessage(String content) {
|
||||
String source = content == null ? "" : content;
|
||||
int separatorIndex = source.indexOf(MARKER_BODY_SEPARATOR);
|
||||
String markerLine = separatorIndex >= 0 ? source.substring(0, separatorIndex).trim() : "";
|
||||
String remaining = separatorIndex >= 0 ? source.substring(separatorIndex + MARKER_BODY_SEPARATOR.length()).trim() : source.trim();
|
||||
String rolePrefix = null;
|
||||
String statusMarkers = "";
|
||||
|
||||
Matcher roleMatcher = ROLE_PREFIX_PATTERN.matcher(markerLine);
|
||||
if (roleMatcher.find()) {
|
||||
rolePrefix = roleMatcher.group(1);
|
||||
statusMarkers = markerLine.substring(roleMatcher.end()).trim();
|
||||
if (statusMarkers.startsWith(":")) {
|
||||
statusMarkers = statusMarkers.substring(1).trim();
|
||||
}
|
||||
if (statusMarkers.endsWith(":")) {
|
||||
statusMarkers = statusMarkers.substring(0, statusMarkers.length() - 1).trim();
|
||||
}
|
||||
}
|
||||
return new ParsedMessage(rolePrefix, statusMarkers, remaining);
|
||||
}
|
||||
|
||||
private String rebuildMessage(ParsedMessage parsedMessage, String compressedBody) {
|
||||
return buildMarkerHeader(parsedMessage.rolePrefix(), parsedMessage.statusMarkers())
|
||||
+ MARKER_BODY_SEPARATOR
|
||||
+ compressedBody;
|
||||
}
|
||||
|
||||
private String buildMarkerHeader(String rolePrefix, String statusMarkers) {
|
||||
String identityMarker = rolePrefix == null || rolePrefix.isBlank() ? UNKNOWN_ROLE_MARKER : rolePrefix;
|
||||
String normalizedStatusMarkers = statusMarkers == null ? "" : statusMarkers.trim();
|
||||
normalizedStatusMarkers = normalizedStatusMarkers.replace(COMPRESSED_MARKER, "").trim();
|
||||
normalizedStatusMarkers += COMPRESSED_MARKER;
|
||||
return identityMarker + ": " + normalizedStatusMarkers;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "single_summarizer";
|
||||
}
|
||||
|
||||
private record ParsedMessage(String rolePrefix, String statusMarkers, String body) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package work.slhaf.partner.module.communication.summarizer;
|
||||
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.TaskBlock;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MessageSummarizer extends AbstractAgentModule.Sub<List<Message>, Result<String>> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责对一组已经发生过的聊天消息进行总结整理,生成一段可供后续系统使用的摘要结果。
|
||||
|
||||
你会收到一条结构化任务消息,其中:
|
||||
- <message_tag_notes> 说明聊天消息中可能出现的标签及其含义;
|
||||
- <chat_messages> 承载本次需要总结的消息列表,每条消息都带有 role 与正文内容。
|
||||
|
||||
你的任务:
|
||||
- 基于整组消息,提炼出一段紧凑、连贯、信息完整的摘要;
|
||||
- 摘要应尽量覆盖这组消息中的主要事实、结论、约束、推进情况、未决点、明显态度与情绪变化;
|
||||
- 若消息中包含技术讨论、配置、代码、报错、规则、方案比较、设计判断等内容,应优先保留这些对后续理解真正有帮助的信息。
|
||||
|
||||
摘要视角要求:
|
||||
- 摘要默认采用 AGENT 视角书写,即以“我”的立场整理这组对话,而不是使用外部旁观者口吻。
|
||||
- 对于来自 [AGENT] 或 assistant 的消息,可将其理解为我的表达、我的判断、我的推进、我的反思或我的内部反馈,并以“我”来概括。
|
||||
- 对于来自 [USER] 的消息,应明确保留其“用户”身份,不要模糊为无来源的陈述,也不要误写成“我”的观点。
|
||||
- 不要默认把整组消息改写成“用户近期……”“系统如何……”这类第三人称阶段报告,除非原消息本身就是这种汇报视角。
|
||||
- 若消息中出现 [NOT_REPLIED],表示这是一条我未直接发给用户、但保留在交流轨迹中的内部交流结果;必要时可在摘要中说明这是我内部保留的判断或反馈。
|
||||
|
||||
总结原则:
|
||||
- 重点提炼这组消息中真正影响后续理解和推进的信息,不要平均分配篇幅。
|
||||
- 合并重复表达、重复确认和多轮来回拉扯后的同类结论。
|
||||
- 若消息中形成了明确结论、决定、偏好、限制条件、行动推进或阶段性判断,应优先写出。
|
||||
- 若消息中仍存在未解决问题、待确认事项、分歧点或风险点,也应明确保留。
|
||||
- 若消息整体只是闲聊、感叹或状态表达,也应如实概括其主要情绪和交流走向,不要硬总结出不存在的任务结论。
|
||||
|
||||
关于技术内容:
|
||||
- 若消息中包含代码、日志、命令输出、配置片段等长内容,不要原样大段复写;
|
||||
- 应概括其中真正关键的信息,例如:关键报错、关键配置、关键判断、关键修改点、关键结果。
|
||||
- 只有在少量原文片段对后续理解不可替代时,才可保留必要短句。
|
||||
|
||||
输出要求:
|
||||
- 只输出一段摘要正文,不要添加标题、前缀、说明或额外标签。
|
||||
- 不要输出项目符号列表,除非原始内容极度结构化且不用列表会明显损失可读性。
|
||||
- 不要输出结构之外的解释、注释或额外文本。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<String> doExecute(List<Message> messages) {
|
||||
return chat(List.of(buildChatMessagesBlock(messages).encodeToMessage()));
|
||||
}
|
||||
|
||||
private @NotNull TaskBlock buildChatMessagesBlock(List<Message> messages) {
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
document.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
|
||||
appendListElement(document, root, "chat_messages", "message", messages, (element, message) -> {
|
||||
element.setAttribute("role", message.roleValue());
|
||||
element.setTextContent(message.getContent());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<Message> modulePrompt() {
|
||||
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "multi_summarizer";
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package work.slhaf.partner.module.communication.summarizer;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.memory.runtime.MemoryRuntime;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MultiSummarizer extends AbstractAgentModule.Sub<List<Message>, Result<String>> implements ActivateModel {
|
||||
|
||||
@InjectModule
|
||||
private MemoryRuntime memoryRuntime;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<String> doExecute(List<Message> messages) {
|
||||
return chat(List.of(new Message(
|
||||
Message.Character.USER,
|
||||
JSONUtil.toJsonPrettyStr(messages)))
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "multi_summarizer";
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package work.slhaf.partner.module.communication.summarizer;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
public class SingleSummarizer extends AbstractAgentModule.Sub<List<Message>, Void> implements ActivateModel {
|
||||
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
private ExecutorService executor;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
executor = actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doExecute(List<Message> chatMessages) {
|
||||
CountDownLatch latch = new CountDownLatch(chatMessages.size());
|
||||
for (int i = 0; i < chatMessages.size(); i++) {
|
||||
Message chatMessage = chatMessages.get(i);
|
||||
if (chatMessage.getRole() == Message.Character.ASSISTANT) {
|
||||
String content = chatMessage.getContent();
|
||||
if (chatMessage.getContent().length() > 500) {
|
||||
int index = i;
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String summarized = chat(List.of(new Message(Message.Character.USER, content))).fold(
|
||||
res -> res,
|
||||
exp -> content
|
||||
);
|
||||
chatMessages.set(index, new Message(Message.Character.ASSISTANT, summarized));
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "single_summarizer";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package work.slhaf.partner.module.communication;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextWorkspace;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
@@ -51,8 +52,8 @@ class CommunicationProducerTest {
|
||||
|
||||
List<Message> chatMessages = cognitionCapability.getChatMessages();
|
||||
assertEquals(2, chatMessages.size());
|
||||
assertEquals("[[USER]: user-1]: hello", chatMessages.get(0).getContent());
|
||||
assertEquals("[NOT_REPLIED]: not now", chatMessages.get(1).getContent());
|
||||
assertEquals("[[USER]: user-1]:\n\nhello", chatMessages.get(0).getContent());
|
||||
assertEquals("[[AGENT]: self]: [NOT_REPLIED]:\n\nnot now", chatMessages.get(1).getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,7 +69,7 @@ class CommunicationProducerTest {
|
||||
);
|
||||
|
||||
List<Message> chatMessages = cognitionCapability.getChatMessages();
|
||||
assertEquals("normal reply", chatMessages.get(1).getContent());
|
||||
assertEquals("[[AGENT]: self]:\n\nnormal reply", chatMessages.get(1).getContent());
|
||||
}
|
||||
|
||||
private static final class StubCognitionCapability implements CognitionCapability {
|
||||
@@ -103,6 +104,11 @@ class CommunicationProducerTest {
|
||||
public void refreshRecentChatMessagesContext() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Element messageNotesElement() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Lock getMessageLock() {
|
||||
return lock;
|
||||
|
||||
@@ -9,8 +9,8 @@ import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.communication.summarizer.MultiSummarizer;
|
||||
import work.slhaf.partner.module.communication.summarizer.SingleSummarizer;
|
||||
import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
|
||||
import work.slhaf.partner.module.communication.summarizer.MessageSummarizer;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Path;
|
||||
@@ -42,13 +42,13 @@ class DialogRollingTest {
|
||||
String sessionId = "dialog-rolling-" + UUID.randomUUID();
|
||||
StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId);
|
||||
DialogRolling dialogRolling = new DialogRolling();
|
||||
MultiSummarizer multiSummarizer = Mockito.mock(MultiSummarizer.class);
|
||||
SingleSummarizer singleSummarizer = Mockito.mock(SingleSummarizer.class);
|
||||
MessageSummarizer messageSummarizer = Mockito.mock(MessageSummarizer.class);
|
||||
MessageCompressor messageCompressor = Mockito.mock(MessageCompressor.class);
|
||||
setField(dialogRolling, "memoryCapability", memoryCapability);
|
||||
setField(dialogRolling, "multiSummarizer", multiSummarizer);
|
||||
setField(dialogRolling, "singleSummarizer", singleSummarizer);
|
||||
setField(dialogRolling, "messageSummarizer", messageSummarizer);
|
||||
setField(dialogRolling, "messageCompressor", messageCompressor);
|
||||
|
||||
when(multiSummarizer.execute(Mockito.any())).thenReturn(Result.success("new-summary"));
|
||||
when(messageSummarizer.execute(Mockito.any())).thenReturn(Result.success("new-summary"));
|
||||
|
||||
MemoryUnit existingUnit = new MemoryUnit(sessionId);
|
||||
existingUnit.getConversationMessages().addAll(List.of(
|
||||
@@ -83,13 +83,13 @@ class DialogRollingTest {
|
||||
String sessionId = "dialog-rolling-" + UUID.randomUUID();
|
||||
StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId);
|
||||
DialogRolling dialogRolling = new DialogRolling();
|
||||
MultiSummarizer multiSummarizer = Mockito.mock(MultiSummarizer.class);
|
||||
SingleSummarizer singleSummarizer = Mockito.mock(SingleSummarizer.class);
|
||||
MessageSummarizer messageSummarizer = Mockito.mock(MessageSummarizer.class);
|
||||
MessageCompressor messageCompressor = Mockito.mock(MessageCompressor.class);
|
||||
setField(dialogRolling, "memoryCapability", memoryCapability);
|
||||
setField(dialogRolling, "multiSummarizer", multiSummarizer);
|
||||
setField(dialogRolling, "singleSummarizer", singleSummarizer);
|
||||
setField(dialogRolling, "messageSummarizer", messageSummarizer);
|
||||
setField(dialogRolling, "messageCompressor", messageCompressor);
|
||||
|
||||
when(multiSummarizer.execute(Mockito.any())).thenReturn(Result.success("fresh-summary"));
|
||||
when(messageSummarizer.execute(Mockito.any())).thenReturn(Result.success("fresh-summary"));
|
||||
|
||||
RollingResult rollingResult = dialogRolling.buildRollingResult(List.of(
|
||||
message(Message.Character.USER, "first"),
|
||||
@@ -138,13 +138,13 @@ class DialogRollingTest {
|
||||
String sessionId = "dialog-rolling-" + UUID.randomUUID();
|
||||
StubMemoryCapability memoryCapability = new StubMemoryCapability(sessionId);
|
||||
DialogRolling dialogRolling = new DialogRolling();
|
||||
MultiSummarizer multiSummarizer = Mockito.mock(MultiSummarizer.class);
|
||||
SingleSummarizer singleSummarizer = Mockito.mock(SingleSummarizer.class);
|
||||
MessageSummarizer messageSummarizer = Mockito.mock(MessageSummarizer.class);
|
||||
MessageCompressor messageCompressor = Mockito.mock(MessageCompressor.class);
|
||||
setField(dialogRolling, "memoryCapability", memoryCapability);
|
||||
setField(dialogRolling, "multiSummarizer", multiSummarizer);
|
||||
setField(dialogRolling, "singleSummarizer", singleSummarizer);
|
||||
setField(dialogRolling, "messageSummarizer", messageSummarizer);
|
||||
setField(dialogRolling, "messageCompressor", messageCompressor);
|
||||
|
||||
when(multiSummarizer.execute(Mockito.any())).thenReturn(Result.success(" "));
|
||||
when(messageSummarizer.execute(Mockito.any())).thenReturn(Result.success(" "));
|
||||
|
||||
RollingResult rollingResult = dialogRolling.buildRollingResult(List.of(
|
||||
message(Message.Character.USER, "u1"),
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
@@ -93,6 +94,11 @@ class MemoryRuntimeTest {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Element messageNotesElement() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Lock getMessageLock() {
|
||||
return lock;
|
||||
|
||||
Reference in New Issue
Block a user