mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat: hide assistant card names in extraction transcript, split sectioned recentMessages into 2 system messages
This commit is contained in:
@@ -289,6 +289,32 @@ function resolveSpeakerName(message = {}, role = "assistant", names = {}) {
|
||||
return role || "assistant";
|
||||
}
|
||||
|
||||
function shouldHideSpeakerLabel(message = {}, role = "assistant", names = {}) {
|
||||
if (message?.hideSpeakerLabel === true) {
|
||||
return true;
|
||||
}
|
||||
if (message?.hideSpeakerLabel === false) {
|
||||
return false;
|
||||
}
|
||||
if (role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
if (String(message?.source || "").trim() === "worldInfo-atDepth") {
|
||||
return false;
|
||||
}
|
||||
const explicitSpeaker = String(
|
||||
message?.speaker ?? message?.name ?? message?.displayName ?? "",
|
||||
).trim();
|
||||
if (!explicitSpeaker) {
|
||||
return true;
|
||||
}
|
||||
const activeCharName = String(names?.charName || "").trim();
|
||||
if (!activeCharName) {
|
||||
return false;
|
||||
}
|
||||
return explicitSpeaker === activeCharName;
|
||||
}
|
||||
|
||||
function normalizeExtractionMessage(message = {}, index = 0, names = {}) {
|
||||
const role = normalizeRole(
|
||||
message?.role ?? (message?.is_user === true ? "user" : "assistant"),
|
||||
@@ -296,6 +322,7 @@ function normalizeExtractionMessage(message = {}, index = 0, names = {}) {
|
||||
const content = String(resolveMessageContent(message) || "").trim();
|
||||
const rawContent = String(resolveMessageRawContent(message) || content).trim();
|
||||
const speaker = resolveSpeakerName(message, role, names);
|
||||
const hideSpeakerLabel = shouldHideSpeakerLabel(message, role, names);
|
||||
const seq = Number.isFinite(Number(message?.seq)) ? Number(message.seq) : null;
|
||||
|
||||
return {
|
||||
@@ -304,6 +331,7 @@ function normalizeExtractionMessage(message = {}, index = 0, names = {}) {
|
||||
role,
|
||||
speaker,
|
||||
name: speaker,
|
||||
hideSpeakerLabel,
|
||||
content,
|
||||
rawContent,
|
||||
sourceType: role === "user" ? "user_input" : "ai_output",
|
||||
@@ -347,7 +375,8 @@ export function formatExtractionTranscript(messages = []) {
|
||||
: `#${index + 1}`;
|
||||
const role = normalizeRole(message?.role || "assistant");
|
||||
const speaker = String(message?.speaker || message?.name || "").trim();
|
||||
const speakerLabel = speaker ? `|${speaker}` : "";
|
||||
const speakerLabel =
|
||||
message?.hideSpeakerLabel === true || !speaker ? "" : `|${speaker}`;
|
||||
const line = `${seqLabel} [${role}${speakerLabel}]: ${String(message?.content || "")}`;
|
||||
if (String(line || "").trim()) {
|
||||
lines.push(line);
|
||||
|
||||
@@ -873,6 +873,7 @@ export async function extractMemories({
|
||||
content: message?.content,
|
||||
speaker: message?.speaker,
|
||||
name: message?.name,
|
||||
hideSpeakerLabel: message?.hideSpeakerLabel === true,
|
||||
isContextOnly: message?.isContextOnly === true,
|
||||
}))
|
||||
: [];
|
||||
|
||||
@@ -296,6 +296,7 @@ function getPromptMessageLikeDescriptor(value) {
|
||||
role: role === "user" ? "user" : "assistant",
|
||||
seq: getOptionalFiniteNumber(value.seq),
|
||||
speaker,
|
||||
hideSpeakerLabel: value?.hideSpeakerLabel === true,
|
||||
isContextOnly:
|
||||
typeof value.isContextOnly === "boolean" ? value.isContextOnly : null,
|
||||
};
|
||||
@@ -310,6 +311,7 @@ function getPromptMessageLikeDescriptor(value) {
|
||||
role: value.is_user === true ? "user" : "assistant",
|
||||
seq: getOptionalFiniteNumber(value.seq),
|
||||
speaker,
|
||||
hideSpeakerLabel: value?.hideSpeakerLabel === true,
|
||||
isContextOnly:
|
||||
typeof value.isContextOnly === "boolean" ? value.isContextOnly : null,
|
||||
};
|
||||
@@ -326,6 +328,11 @@ function isPromptMessageArray(value) {
|
||||
);
|
||||
}
|
||||
|
||||
const EXTRACTION_CONTEXT_REVIEW_HEADER =
|
||||
"--- 以下是上下文回顾(已提取过),仅供理解剧情 ---";
|
||||
const EXTRACTION_TARGET_CONTENT_HEADER =
|
||||
"--- 以下是本次需要提取记忆的新对话内容 ---";
|
||||
|
||||
function getPromptMessageContextGroup(value) {
|
||||
const descriptor = getPromptMessageLikeDescriptor(value);
|
||||
if (!descriptor || typeof descriptor.isContextOnly !== "boolean") {
|
||||
@@ -334,6 +341,16 @@ function getPromptMessageContextGroup(value) {
|
||||
return descriptor.isContextOnly ? "context" : "target";
|
||||
}
|
||||
|
||||
function getPromptMessageContextHeader(group = "") {
|
||||
if (group === "context") {
|
||||
return EXTRACTION_CONTEXT_REVIEW_HEADER;
|
||||
}
|
||||
if (group === "target") {
|
||||
return EXTRACTION_TARGET_CONTENT_HEADER;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatPromptMessageTranscript(value) {
|
||||
const entries = Array.isArray(value) ? value : [value];
|
||||
const hasContextMessages = entries.some(
|
||||
@@ -353,16 +370,12 @@ function formatPromptMessageTranscript(value) {
|
||||
}
|
||||
const group = getPromptMessageContextGroup(entry);
|
||||
if (hasContextMessages && hasTargetMessages && group && group !== activeGroup) {
|
||||
lines.push(
|
||||
group === "context"
|
||||
? "--- 以下是上下文回顾(已提取过),仅供理解剧情 ---"
|
||||
: "--- 以下是本次需要提取记忆的新对话内容 ---",
|
||||
);
|
||||
lines.push(getPromptMessageContextHeader(group));
|
||||
activeGroup = group;
|
||||
}
|
||||
const seqLabel =
|
||||
descriptor.seq != null ? `#${descriptor.seq}` : `#${index + 1}`;
|
||||
const speakerLabel = descriptor.speaker
|
||||
const speakerLabel = !descriptor.hideSpeakerLabel && descriptor.speaker
|
||||
? `|${descriptor.speaker}`
|
||||
: "";
|
||||
lines.push(`${seqLabel} [${descriptor.role}${speakerLabel}]: ${descriptor.content}`);
|
||||
@@ -1910,6 +1923,87 @@ function clonePayloadMessage(message = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function splitSectionedTranscriptPayloadMessage(message = {}) {
|
||||
const normalizedRole = normalizeRole(message?.role);
|
||||
const sourceKey = String(message?.sourceKey || "").trim();
|
||||
const content = String(message?.content || "").trim();
|
||||
if (
|
||||
normalizedRole !== "system" ||
|
||||
!["recentMessages", "dialogueText"].includes(sourceKey) ||
|
||||
!content.includes(EXTRACTION_CONTEXT_REVIEW_HEADER) ||
|
||||
!content.includes(EXTRACTION_TARGET_CONTENT_HEADER)
|
||||
) {
|
||||
return [message];
|
||||
}
|
||||
|
||||
const headerMatches = [];
|
||||
let searchIndex = 0;
|
||||
while (searchIndex < content.length) {
|
||||
const contextIndex = content.indexOf(
|
||||
EXTRACTION_CONTEXT_REVIEW_HEADER,
|
||||
searchIndex,
|
||||
);
|
||||
const targetIndex = content.indexOf(
|
||||
EXTRACTION_TARGET_CONTENT_HEADER,
|
||||
searchIndex,
|
||||
);
|
||||
let nextIndex = -1;
|
||||
let nextHeader = "";
|
||||
if (contextIndex >= 0 && (targetIndex < 0 || contextIndex <= targetIndex)) {
|
||||
nextIndex = contextIndex;
|
||||
nextHeader = EXTRACTION_CONTEXT_REVIEW_HEADER;
|
||||
} else if (targetIndex >= 0) {
|
||||
nextIndex = targetIndex;
|
||||
nextHeader = EXTRACTION_TARGET_CONTENT_HEADER;
|
||||
}
|
||||
if (nextIndex < 0 || !nextHeader) {
|
||||
break;
|
||||
}
|
||||
headerMatches.push({
|
||||
index: nextIndex,
|
||||
header: nextHeader,
|
||||
});
|
||||
searchIndex = nextIndex + nextHeader.length;
|
||||
}
|
||||
|
||||
if (headerMatches.length < 2 || headerMatches[0].index !== 0) {
|
||||
return [message];
|
||||
}
|
||||
|
||||
const { role: _role, content: _content, ...sharedMeta } = message;
|
||||
const splitMessages = [];
|
||||
|
||||
for (let index = 0; index < headerMatches.length; index += 1) {
|
||||
const current = headerMatches[index];
|
||||
const next = headerMatches[index + 1];
|
||||
const sectionBody = content
|
||||
.slice(current.index + current.header.length, next ? next.index : content.length)
|
||||
.trim();
|
||||
const transcriptSection =
|
||||
current.header === EXTRACTION_CONTEXT_REVIEW_HEADER ? "context" : "target";
|
||||
splitMessages.push(
|
||||
createExecutionMessage(
|
||||
"system",
|
||||
sectionBody ? `${current.header}\n\n${sectionBody}` : current.header,
|
||||
{
|
||||
...sharedMeta,
|
||||
sourceKey,
|
||||
transcriptSection,
|
||||
transcriptSectionPart: "section",
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return splitMessages.filter(Boolean);
|
||||
}
|
||||
|
||||
function expandSectionedTranscriptPayloadMessages(messages = []) {
|
||||
return (Array.isArray(messages) ? messages : []).flatMap((message) =>
|
||||
splitSectionedTranscriptPayloadMessage(message),
|
||||
);
|
||||
}
|
||||
|
||||
function collectPayloadUserMessageTexts(messages = []) {
|
||||
return (Array.isArray(messages) ? messages : [])
|
||||
.filter((message) => String(message?.role || "").trim().toLowerCase() === "user")
|
||||
@@ -2008,8 +2102,11 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
|
||||
!(isCustomFilter && messageUsesWorldInfoContent(message)),
|
||||
},
|
||||
);
|
||||
const expandedExecutionMessages = expandSectionedTranscriptPayloadMessages(
|
||||
executionMessages,
|
||||
);
|
||||
|
||||
const hasUserMessage = executionMessages.some(
|
||||
const hasUserMessage = expandedExecutionMessages.some(
|
||||
(message) => message.role === "user",
|
||||
);
|
||||
if (!hasUserMessage && rawExecutionMessages.length > 0) {
|
||||
@@ -2028,7 +2125,7 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
|
||||
`after recreate=${userBlocksAfterRaw.length}, ` +
|
||||
`after sanitize=${userBlocksAfterSanitize.length}, ` +
|
||||
`blockedContents count=${blockedContents.length}, ` +
|
||||
`total executionMessages=${executionMessages.length}`,
|
||||
`total executionMessages=${expandedExecutionMessages.length}`,
|
||||
);
|
||||
if (userBlocksBefore.length > 0) {
|
||||
for (const block of userBlocksBefore) {
|
||||
@@ -2046,17 +2143,19 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
|
||||
}
|
||||
}
|
||||
const additionalMessages =
|
||||
executionMessages.length > 0
|
||||
expandedExecutionMessages.length > 0
|
||||
? []
|
||||
: sanitizePromptMessages(
|
||||
settings,
|
||||
taskType,
|
||||
rawPrivateTaskMessages,
|
||||
{
|
||||
blockedContents,
|
||||
applySanitizer: (message) =>
|
||||
!(isCustomFilter && messageUsesWorldInfoContent(message)),
|
||||
},
|
||||
: expandSectionedTranscriptPayloadMessages(
|
||||
sanitizePromptMessages(
|
||||
settings,
|
||||
taskType,
|
||||
rawPrivateTaskMessages,
|
||||
{
|
||||
blockedContents,
|
||||
applySanitizer: (message) =>
|
||||
!(isCustomFilter && messageUsesWorldInfoContent(message)),
|
||||
},
|
||||
),
|
||||
);
|
||||
const hasAdditionalUserMessage = additionalMessages.some(
|
||||
(message) => message.role === "user",
|
||||
@@ -2078,9 +2177,11 @@ export function buildTaskLlmPayload(promptBuild = null, fallbackUserPrompt = "")
|
||||
|
||||
return {
|
||||
systemPrompt:
|
||||
executionMessages.length > 0 ? "" : String(promptBuild?.systemPrompt || ""),
|
||||
expandedExecutionMessages.length > 0
|
||||
? ""
|
||||
: String(promptBuild?.systemPrompt || ""),
|
||||
userPrompt: fallbackUserPromptResult.text,
|
||||
promptMessages: executionMessages,
|
||||
promptMessages: expandedExecutionMessages,
|
||||
additionalMessages,
|
||||
fallbackUserPromptSource: fallbackUserPromptResult.source,
|
||||
fallbackUserPromptApplied: Boolean(fallbackUserPromptResult.text),
|
||||
|
||||
@@ -76,9 +76,23 @@ const chat = [
|
||||
{
|
||||
const mixed = [
|
||||
{ seq: 1, role: "user", content: "context user", speaker: "A", isContextOnly: true },
|
||||
{ seq: 2, role: "assistant", content: "context ai", speaker: "B", isContextOnly: true },
|
||||
{
|
||||
seq: 2,
|
||||
role: "assistant",
|
||||
content: "context ai",
|
||||
speaker: "B",
|
||||
hideSpeakerLabel: true,
|
||||
isContextOnly: true,
|
||||
},
|
||||
{ seq: 3, role: "user", content: "target user", speaker: "A", isContextOnly: false },
|
||||
{ seq: 4, role: "assistant", content: "target ai", speaker: "B", isContextOnly: false },
|
||||
{
|
||||
seq: 4,
|
||||
role: "assistant",
|
||||
content: "target ai",
|
||||
speaker: "B",
|
||||
hideSpeakerLabel: true,
|
||||
isContextOnly: false,
|
||||
},
|
||||
];
|
||||
const transcript = formatExtractionTranscript(mixed);
|
||||
assert.match(transcript, /已提取过/, "transcript should contain context review header");
|
||||
@@ -89,6 +103,8 @@ const chat = [
|
||||
);
|
||||
assert.match(transcript, /#1.*context user/, "context message should appear");
|
||||
assert.match(transcript, /#3.*target user/, "target message should appear");
|
||||
assert.match(transcript, /#2 \[assistant\]: context ai/, "assistant card name should be hidden");
|
||||
assert.doesNotMatch(transcript, /#2 \[assistant\|B\]:/, "assistant card name should not be rendered");
|
||||
console.log(" ✓ formatExtractionTranscript: section dividers for mixed context/target");
|
||||
}
|
||||
|
||||
@@ -132,8 +148,20 @@ const chat = [
|
||||
const targetFiltered = result.filteredMessages.filter((m) => !m.isContextOnly);
|
||||
assert.equal(contextFiltered.length, 2, "context messages propagated through filtering");
|
||||
assert.equal(targetFiltered.length, 2, "target messages propagated through filtering");
|
||||
assert.equal(
|
||||
result.filteredMessages.find((m) => m.seq === 2)?.hideSpeakerLabel,
|
||||
true,
|
||||
"active character assistant label should be hidden",
|
||||
);
|
||||
assert.equal(
|
||||
result.filteredMessages.find((m) => m.seq === 1)?.hideSpeakerLabel,
|
||||
false,
|
||||
"user label should remain visible",
|
||||
);
|
||||
assert.match(result.filteredTranscript, /已提取过/, "transcript includes context header");
|
||||
assert.match(result.filteredTranscript, /本次需要提取/, "transcript includes target header");
|
||||
assert.match(result.filteredTranscript, /#2 \[assistant\]: old answer/, "assistant transcript should hide character name");
|
||||
assert.doesNotMatch(result.filteredTranscript, /#2 \[assistant\|B\]:/, "assistant transcript should not show character name");
|
||||
console.log(" ✓ buildExtractionInputContext: isContextOnly propagated to filteredMessages and transcript");
|
||||
}
|
||||
|
||||
|
||||
@@ -141,8 +141,9 @@ try {
|
||||
(message) => message.sourceKey === "recentMessages",
|
||||
);
|
||||
assert.ok(recentBlock);
|
||||
assert.match(String(recentBlock?.content || ""), /#10 \[assistant\|艾琳\]: 继续说明/);
|
||||
assert.match(String(recentBlock?.content || ""), /#10 \[assistant\]: 继续说明/);
|
||||
assert.match(String(recentBlock?.content || ""), /#11 \[user\|玩家\]: 用户输入/);
|
||||
assert.doesNotMatch(String(recentBlock?.content || ""), /#10 \[assistant\|艾琳\]:/);
|
||||
assert.doesNotMatch(String(recentBlock?.content || ""), /隐式思维|<think>/);
|
||||
} finally {
|
||||
restore();
|
||||
|
||||
@@ -224,16 +224,25 @@ function collectAllPromptContent(captured) {
|
||||
assert.equal(result.success, true);
|
||||
assert.ok(captured);
|
||||
|
||||
const recentBlock = (Array.isArray(captured.promptMessages) ? captured.promptMessages : []).find(
|
||||
const recentMessages = (Array.isArray(captured.promptMessages)
|
||||
? captured.promptMessages
|
||||
: []
|
||||
).filter(
|
||||
(m) => m.sourceKey === "recentMessages",
|
||||
);
|
||||
assert.ok(recentBlock, "recentMessages block should exist");
|
||||
const recentContent = String(recentBlock?.content || "");
|
||||
assert.match(recentContent, /以下是上下文回顾(已提取过),仅供理解剧情/);
|
||||
assert.match(recentContent, /以下是本次需要提取记忆的新对话内容/);
|
||||
assert.equal(recentMessages.length, 2, "recentMessages should split into 2 section system messages");
|
||||
assert.equal(recentMessages[0]?.role, "system");
|
||||
assert.equal(recentMessages[0]?.transcriptSection, "context");
|
||||
assert.match(String(recentMessages[0]?.content || ""), /^--- 以下是上下文回顾(已提取过),仅供理解剧情 ---/);
|
||||
assert.match(String(recentMessages[0]?.content || ""), /#10 \[user\|玩家\]: 第一轮消息/);
|
||||
assert.equal(recentMessages[1]?.role, "system");
|
||||
assert.equal(recentMessages[1]?.transcriptSection, "target");
|
||||
assert.match(String(recentMessages[1]?.content || ""), /^--- 以下是本次需要提取记忆的新对话内容 ---/);
|
||||
assert.match(String(recentMessages[1]?.content || ""), /#12 \[user\|玩家\]: 第二轮消息/);
|
||||
assert.ok(
|
||||
recentContent.indexOf("已提取过") < recentContent.indexOf("本次需要提取"),
|
||||
"context review should appear before extraction target section",
|
||||
recentMessages[0].content.includes("已提取过") &&
|
||||
recentMessages[1].content.includes("本次需要提取"),
|
||||
"context and target sections should each be emitted as a single system message",
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
|
||||
@@ -286,7 +286,7 @@ try {
|
||||
).find((message) => message.sourceKey === "recentMessages");
|
||||
assert.ok(recentBlock, "recentMessages block should exist");
|
||||
const recentContent = String(recentBlock?.content || "");
|
||||
assert.match(recentContent, /#30 \[assistant\|艾琳\]: 艾琳说:去调查蓝钥匙。/);
|
||||
assert.match(recentContent, /#30 \[assistant\]: 艾琳说:去调查蓝钥匙。/);
|
||||
assert.match(
|
||||
recentContent,
|
||||
/#31 \[assistant\|旁白\]: 旁白补充:<status mood='tense'>雨夜<\/status>巷子很安静。/,
|
||||
@@ -351,7 +351,7 @@ try {
|
||||
: []
|
||||
).find((message) => message.sourceKey === "recentMessages");
|
||||
assert.ok(recentBlock, "recentMessages block should still exist when worldbook is disabled");
|
||||
assert.match(String(recentBlock?.content || ""), /#30 \[assistant\|艾琳\]: 艾琳说:去调查蓝钥匙。/);
|
||||
assert.match(String(recentBlock?.content || ""), /#30 \[assistant\]: 艾琳说:去调查蓝钥匙。/);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ const promptBuild = await buildTaskPrompt(settings, "extract", {
|
||||
content: "继续说明",
|
||||
name: "艾琳",
|
||||
speaker: "艾琳",
|
||||
hideSpeakerLabel: true,
|
||||
isContextOnly: true,
|
||||
},
|
||||
{
|
||||
@@ -133,25 +134,41 @@ const promptBuild = await buildTaskPrompt(settings, "extract", {
|
||||
currentRange: "41 ~ 42",
|
||||
});
|
||||
const payload = buildTaskLlmPayload(promptBuild, "fallback-user");
|
||||
const recentBlock = payload.promptMessages.find(
|
||||
const recentMessages = payload.promptMessages.filter(
|
||||
(message) => message.sourceKey === "recentMessages",
|
||||
);
|
||||
assert.match(
|
||||
String(recentBlock?.content || ""),
|
||||
/以下是上下文回顾(已提取过),仅供理解剧情/,
|
||||
assert.deepEqual(
|
||||
recentMessages.map((message) => ({
|
||||
role: message.role,
|
||||
sourceKey: message.sourceKey,
|
||||
transcriptSection: message.transcriptSection,
|
||||
transcriptSectionPart: message.transcriptSectionPart,
|
||||
})),
|
||||
[
|
||||
{
|
||||
role: "system",
|
||||
sourceKey: "recentMessages",
|
||||
transcriptSection: "context",
|
||||
transcriptSectionPart: "section",
|
||||
},
|
||||
{
|
||||
role: "system",
|
||||
sourceKey: "recentMessages",
|
||||
transcriptSection: "target",
|
||||
transcriptSectionPart: "section",
|
||||
},
|
||||
],
|
||||
);
|
||||
assert.match(
|
||||
String(recentBlock?.content || ""),
|
||||
/以下是本次需要提取记忆的新对话内容/,
|
||||
);
|
||||
assert.match(String(recentBlock?.content || ""), /#41 \[assistant\|艾琳\]: 助手已净化/);
|
||||
assert.match(String(recentBlock?.content || ""), /#42 \[user\|玩家\]: 用户已净化/);
|
||||
assert.match(String(recentMessages[0]?.content || ""), /^--- 以下是上下文回顾(已提取过),仅供理解剧情 ---/);
|
||||
assert.match(String(recentMessages[0]?.content || ""), /#41 \[assistant\]: 助手已净化/);
|
||||
assert.match(String(recentMessages[1]?.content || ""), /^--- 以下是本次需要提取记忆的新对话内容 ---/);
|
||||
assert.match(String(recentMessages[1]?.content || ""), /#42 \[user\|玩家\]: 用户已净化/);
|
||||
assert.doesNotMatch(
|
||||
String(recentBlock?.content || ""),
|
||||
/#41 \[assistant\|艾琳\]: 用户已净化/,
|
||||
String(recentMessages[0]?.content || ""),
|
||||
/#41 \[assistant\|艾琳\]:/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
String(recentBlock?.content || ""),
|
||||
String(recentMessages[1]?.content || ""),
|
||||
/#42 \[user\|玩家\]: 助手已净化/,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user