feat(extract): add split extraction prompt plumbing

This commit is contained in:
youzini
2026-06-09 04:37:13 +00:00
parent 7f02fd45e2
commit 63fba2c8b4
6 changed files with 259 additions and 3 deletions

View File

@@ -37,6 +37,12 @@ const INPUT_CONTEXT_MVU_FIELDS = [
"contradictionSummary", "contradictionSummary",
"charDescription", "charDescription",
"userPersona", "userPersona",
"objectiveExtractionDraft",
"objectiveRefMap",
"ownerContext",
"batchStoryTime",
"relevantPovMemories",
"cognitionStateDigest",
]; ];
const INPUT_REGEX_STAGE_BY_FIELD = { const INPUT_REGEX_STAGE_BY_FIELD = {
@@ -51,6 +57,12 @@ const INPUT_REGEX_STAGE_BY_FIELD = {
characterSummary: "input.candidateText", characterSummary: "input.candidateText",
threadSummary: "input.candidateText", threadSummary: "input.candidateText",
contradictionSummary: "input.candidateText", contradictionSummary: "input.candidateText",
objectiveExtractionDraft: "input.candidateText",
objectiveRefMap: "input.candidateText",
ownerContext: "input.candidateText",
batchStoryTime: "input.candidateText",
relevantPovMemories: "input.candidateText",
cognitionStateDigest: "input.candidateText",
}; };
const INPUT_REGEX_ROLE_BY_FIELD = { const INPUT_REGEX_ROLE_BY_FIELD = {
@@ -74,6 +86,12 @@ const INPUT_HOST_REGEX_SOURCE_BY_FIELD = {
contradictionSummary: "ai_output", contradictionSummary: "ai_output",
charDescription: "ai_output", charDescription: "ai_output",
userPersona: "user_input", userPersona: "user_input",
objectiveExtractionDraft: "ai_output",
objectiveRefMap: "ai_output",
ownerContext: "ai_output",
batchStoryTime: "ai_output",
relevantPovMemories: "ai_output",
cognitionStateDigest: "ai_output",
}; };
function cloneRuntimeDebugValue(value, fallback = null) { function cloneRuntimeDebugValue(value, fallback = null) {

View File

@@ -15,6 +15,8 @@ import { DEFAULT_TASK_PROFILE_TEMPLATES } from "./default-task-profile-templates
const TASK_TYPES = [ const TASK_TYPES = [
"extract", "extract",
"extract_objective",
"extract_subjective",
"recall", "recall",
"compress", "compress",
"synopsis", "synopsis",
@@ -29,6 +31,16 @@ const TASK_TYPE_META = {
label: "提取", label: "提取",
description: "从当前对话批次中抽取结构化记忆。", description: "从当前对话批次中抽取结构化记忆。",
}, },
extract_objective: {
label: "客观提取",
description: "从当前对话批次中抽取客观层结构化记忆。",
hidden: true,
},
extract_subjective: {
label: "主观提取",
description: "从客观提取草稿与视角上下文中抽取主观记忆。",
hidden: true,
},
recall: { recall: {
label: "召回", label: "召回",
description: "根据上下文筛选最相关的记忆节点。", description: "根据上下文筛选最相关的记忆节点。",
@@ -186,6 +198,48 @@ const BUILTIN_BLOCK_DEFINITIONS = [
role: "system", role: "system",
description: "注入当前活跃的故事时间线标签与来源。extract 任务使用,帮助 LLM 定位本批对话在剧情时间轴上的位置。", description: "注入当前活跃的故事时间线标签与来源。extract 任务使用,帮助 LLM 定位本批对话在剧情时间轴上的位置。",
}, },
{
sourceKey: "objectiveExtractionDraft",
name: "客观提取草稿",
role: "system",
description: "注入未来拆分提取链路中的客观层提取草稿。仅供客观/主观拆分提取预设显式添加时使用。",
taskTypes: ["extract_objective", "extract_subjective"],
},
{
sourceKey: "objectiveRefMap",
name: "客观引用映射",
role: "system",
description: "注入未来拆分提取链路中的客观层 ref 到节点/草稿的映射。仅供客观/主观拆分提取预设显式添加时使用。",
taskTypes: ["extract_objective", "extract_subjective"],
},
{
sourceKey: "ownerContext",
name: "视角主体上下文",
role: "system",
description: "注入未来主观提取链路中的 POV owner 身份、作用域和相关约束。仅供拆分提取预设显式添加时使用。",
taskTypes: ["extract_objective", "extract_subjective"],
},
{
sourceKey: "batchStoryTime",
name: "批次故事时间",
role: "system",
description: "注入未来拆分提取链路中的批次故事时间对象。仅供拆分提取预设显式添加时使用。",
taskTypes: ["extract_objective", "extract_subjective"],
},
{
sourceKey: "relevantPovMemories",
name: "相关主观记忆",
role: "system",
description: "注入未来主观提取链路中与当前 owner 相关的既有 POV 记忆。仅供拆分提取预设显式添加时使用。",
taskTypes: ["extract_objective", "extract_subjective"],
},
{
sourceKey: "cognitionStateDigest",
name: "认知状态摘要",
role: "system",
description: "注入未来主观提取链路中 owner 的认知状态摘要。仅供拆分提取预设显式添加时使用。",
taskTypes: ["extract_objective", "extract_subjective"],
},
{ {
sourceKey: "plannerCharacterCard", sourceKey: "plannerCharacterCard",
name: "规划:角色卡", name: "规划:角色卡",
@@ -239,6 +293,8 @@ const DEFAULT_TASK_INPUT = Object.freeze({
const LEGACY_PROMPT_FIELD_MAP = { const LEGACY_PROMPT_FIELD_MAP = {
extract: "extractPrompt", extract: "extractPrompt",
extract_objective: "extractObjectivePrompt",
extract_subjective: "extractSubjectivePrompt",
recall: "recallPrompt", recall: "recallPrompt",
compress: "compressPrompt", compress: "compressPrompt",
synopsis: "synopsisPrompt", synopsis: "synopsisPrompt",
@@ -1001,11 +1057,14 @@ function getDefaultTaskProfileTemplate(taskType) {
if (String(taskType || "") === "planner") { if (String(taskType || "") === "planner") {
return buildPlannerDefaultTaskProfileTemplate(); return buildPlannerDefaultTaskProfileTemplate();
} }
const template = DEFAULT_TASK_PROFILE_TEMPLATES?.[taskType]; const templateKey = ["extract_objective", "extract_subjective"].includes(String(taskType || ""))
? "extract"
: taskType;
const template = DEFAULT_TASK_PROFILE_TEMPLATES?.[templateKey];
if (!template || typeof template !== "object") { if (!template || typeof template !== "object") {
return null; return null;
} }
return applyRuntimeDefaultTemplateOverrides(taskType, cloneJson(template)); return applyRuntimeDefaultTemplateOverrides(templateKey, cloneJson(template));
} }
function hashTemplateFingerprint(value = "") { function hashTemplateFingerprint(value = "") {
@@ -2512,11 +2571,14 @@ export function getTaskTypeMeta(taskType) {
id: taskType, id: taskType,
label: TASK_TYPE_META[taskType]?.label || taskType, label: TASK_TYPE_META[taskType]?.label || taskType,
description: TASK_TYPE_META[taskType]?.description || "", description: TASK_TYPE_META[taskType]?.description || "",
hidden: TASK_TYPE_META[taskType]?.hidden === true,
}; };
} }
export function getTaskTypeOptions() { export function getTaskTypeOptions() {
return TASK_TYPES.map((taskType) => getTaskTypeMeta(taskType)); return TASK_TYPES
.map((taskType) => getTaskTypeMeta(taskType))
.filter((meta) => meta.hidden !== true);
} }
export function getTaskTypes() { export function getTaskTypes() {

View File

@@ -159,6 +159,8 @@ export const defaultSettings = {
// 自定义提示词 // 自定义提示词
extractPrompt: "", extractPrompt: "",
extractObjectivePrompt: "",
extractSubjectivePrompt: "",
recallPrompt: "", recallPrompt: "",
consolidationPrompt: "", consolidationPrompt: "",
compressPrompt: "", compressPrompt: "",

View File

@@ -110,8 +110,12 @@ assert.equal(defaultSettings.nativeRolloutVersion, 2);
assert.equal(defaultSettings.nativeEngineFailOpen, true); assert.equal(defaultSettings.nativeEngineFailOpen, true);
assert.equal(defaultSettings.graphNativeForceDisable, false); assert.equal(defaultSettings.graphNativeForceDisable, false);
assert.equal(defaultSettings.taskProfilesVersion, 3); assert.equal(defaultSettings.taskProfilesVersion, 3);
assert.equal(defaultSettings.extractObjectivePrompt, "");
assert.equal(defaultSettings.extractSubjectivePrompt, "");
assert.ok(defaultSettings.taskProfiles); assert.ok(defaultSettings.taskProfiles);
assert.ok(defaultSettings.taskProfiles.extract); assert.ok(defaultSettings.taskProfiles.extract);
assert.ok(defaultSettings.taskProfiles.extract_objective);
assert.ok(defaultSettings.taskProfiles.extract_subjective);
assert.ok(defaultSettings.taskProfiles.recall); assert.ok(defaultSettings.taskProfiles.recall);
assert.ok(defaultSettings.globalTaskRegex); assert.ok(defaultSettings.globalTaskRegex);
assert.deepEqual( assert.deepEqual(

View File

@@ -47,6 +47,7 @@ installResolveHooks([
const { buildTaskLlmPayload, buildTaskPrompt } = await import("../prompting/prompt-builder.js"); const { buildTaskLlmPayload, buildTaskPrompt } = await import("../prompting/prompt-builder.js");
const { const {
createBuiltinPromptBlock,
createDefaultGlobalTaskRegex, createDefaultGlobalTaskRegex,
createDefaultTaskProfiles, createDefaultTaskProfiles,
} = await import("../prompting/prompt-profiles.js"); } = await import("../prompting/prompt-profiles.js");
@@ -256,4 +257,98 @@ assert.equal(
initializeHostAdapter({}); initializeHostAdapter({});
const splitContextTaskProfiles = createDefaultTaskProfiles();
const subjectiveProfile = splitContextTaskProfiles.extract_subjective.profiles[0];
subjectiveProfile.blocks = [
createBuiltinPromptBlock("extract_subjective", "objectiveExtractionDraft", {
name: "客观提取草稿",
order: 0,
}),
createBuiltinPromptBlock("extract_subjective", "objectiveRefMap", {
name: "客观引用映射",
order: 1,
}),
createBuiltinPromptBlock("extract_subjective", "ownerContext", {
name: "视角主体上下文",
order: 2,
}),
createBuiltinPromptBlock("extract_subjective", "batchStoryTime", {
name: "批次故事时间",
order: 3,
}),
createBuiltinPromptBlock("extract_subjective", "relevantPovMemories", {
name: "相关主观记忆",
order: 4,
}),
createBuiltinPromptBlock("extract_subjective", "cognitionStateDigest", {
name: "认知状态摘要",
order: 5,
}),
];
const splitContextPromptBuild = await buildTaskPrompt(
{
taskProfilesVersion: 3,
taskProfiles: splitContextTaskProfiles,
},
"extract_subjective",
{
objectiveExtractionDraft: { operations: [{ ref: "evt1", type: "event" }] },
objectiveRefMap: { evt1: "node-evt1" },
ownerContext: { ownerType: "character", ownerName: "艾琳" },
batchStoryTime: { label: "第二天清晨", confidence: "high" },
relevantPovMemories: ["旧 POV 记忆"],
cognitionStateDigest: "艾琳知道 evt1",
},
);
const splitContextPayload = buildTaskLlmPayload(
splitContextPromptBuild,
"fallback-user",
);
assert.deepEqual(
splitContextPayload.promptMessages
.map((message) => message.sourceKey)
.filter(Boolean),
[
"objectiveExtractionDraft",
"objectiveRefMap",
"ownerContext",
"batchStoryTime",
"relevantPovMemories",
"cognitionStateDigest",
],
);
assert.match(
String(
splitContextPayload.promptMessages.find(
(message) => message.sourceKey === "objectiveExtractionDraft",
)?.content || "",
),
/"ref": "evt1"/,
);
assert.match(
String(
splitContextPayload.promptMessages.find(
(message) => message.sourceKey === "ownerContext",
)?.content || "",
),
/"ownerName": "艾琳"/,
);
assert.match(
String(
splitContextPayload.promptMessages.find(
(message) => message.sourceKey === "batchStoryTime",
)?.content || "",
),
/"第二天清晨"/,
);
assert.match(
String(
splitContextPayload.promptMessages.find(
(message) => message.sourceKey === "relevantPovMemories",
)?.content || "",
),
/旧 POV 记忆/,
);
console.log("prompt-builder-defaults tests passed"); console.log("prompt-builder-defaults tests passed");

View File

@@ -7,7 +7,11 @@ import {
createLocalRegexRule, createLocalRegexRule,
exportTaskProfile, exportTaskProfile,
getActiveTaskProfile, getActiveTaskProfile,
getBuiltinBlockDefinitions,
getLegacyPromptFieldForTask, getLegacyPromptFieldForTask,
getTaskTypeMeta,
getTaskTypeOptions,
getTaskTypes,
importTaskProfile, importTaskProfile,
restoreDefaultTaskProfile, restoreDefaultTaskProfile,
upsertTaskProfile, upsertTaskProfile,
@@ -97,4 +101,75 @@ const restoredActive = getActiveTaskProfile(
assert.equal(restoredActive.id, "default"); assert.equal(restoredActive.id, "default");
assert.equal(getLegacyPromptFieldForTask("extract"), "extractPrompt"); assert.equal(getLegacyPromptFieldForTask("extract"), "extractPrompt");
assert.ok(getTaskTypes().includes("extract_objective"));
assert.ok(getTaskTypes().includes("extract_subjective"));
assert.equal(
getTaskTypeOptions().some((option) => option.id === "extract_objective"),
false,
);
assert.equal(
getTaskTypeOptions().some((option) => option.id === "extract_subjective"),
false,
);
assert.deepEqual(
{
objective: getTaskTypeMeta("extract_objective"),
subjective: getTaskTypeMeta("extract_subjective"),
},
{
objective: {
id: "extract_objective",
label: "客观提取",
description: "从当前对话批次中抽取客观层结构化记忆。",
hidden: true,
},
subjective: {
id: "extract_subjective",
label: "主观提取",
description: "从客观提取草稿与视角上下文中抽取主观记忆。",
hidden: true,
},
},
);
assert.ok(taskProfiles.extract_objective?.profiles?.length > 0);
assert.ok(taskProfiles.extract_subjective?.profiles?.length > 0);
assert.equal(
taskProfiles.extract_objective.profiles[0].metadata.legacyPromptField,
"extractObjectivePrompt",
);
assert.equal(
taskProfiles.extract_subjective.profiles[0].metadata.legacyPromptField,
"extractSubjectivePrompt",
);
assert.equal(
taskProfiles.extract_objective.profiles[0].blocks.find((block) => block.id === "default-role")?.content,
baseProfile.blocks.find((block) => block.id === "default-role")?.content,
);
assert.equal(
taskProfiles.extract_subjective.profiles[0].blocks.find((block) => block.id === "default-rules")?.content,
baseProfile.blocks.find((block) => block.id === "default-rules")?.content,
);
assert.deepEqual(
getBuiltinBlockDefinitions("extract_subjective")
.map((definition) => definition.sourceKey)
.filter((sourceKey) =>
[
"objectiveExtractionDraft",
"objectiveRefMap",
"ownerContext",
"batchStoryTime",
"relevantPovMemories",
"cognitionStateDigest",
].includes(sourceKey),
),
[
"objectiveExtractionDraft",
"objectiveRefMap",
"ownerContext",
"batchStoryTime",
"relevantPovMemories",
"cognitionStateDigest",
],
);
console.log("task-profile-storage tests passed"); console.log("task-profile-storage tests passed");