From 63fba2c8b49ed8635dfc5bf0d300f628c3f09b8e Mon Sep 17 00:00:00 2001 From: youzini Date: Tue, 9 Jun 2026 04:37:13 +0000 Subject: [PATCH] feat(extract): add split extraction prompt plumbing --- prompting/prompt-builder.js | 18 ++++++ prompting/prompt-profiles.js | 68 +++++++++++++++++++++- runtime/settings-defaults.js | 2 + tests/default-settings.mjs | 4 ++ tests/prompt-builder-defaults.mjs | 95 +++++++++++++++++++++++++++++++ tests/task-profile-storage.mjs | 75 ++++++++++++++++++++++++ 6 files changed, 259 insertions(+), 3 deletions(-) diff --git a/prompting/prompt-builder.js b/prompting/prompt-builder.js index de4c0bd..eedd562 100644 --- a/prompting/prompt-builder.js +++ b/prompting/prompt-builder.js @@ -37,6 +37,12 @@ const INPUT_CONTEXT_MVU_FIELDS = [ "contradictionSummary", "charDescription", "userPersona", + "objectiveExtractionDraft", + "objectiveRefMap", + "ownerContext", + "batchStoryTime", + "relevantPovMemories", + "cognitionStateDigest", ]; const INPUT_REGEX_STAGE_BY_FIELD = { @@ -51,6 +57,12 @@ const INPUT_REGEX_STAGE_BY_FIELD = { characterSummary: "input.candidateText", threadSummary: "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 = { @@ -74,6 +86,12 @@ const INPUT_HOST_REGEX_SOURCE_BY_FIELD = { contradictionSummary: "ai_output", charDescription: "ai_output", 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) { diff --git a/prompting/prompt-profiles.js b/prompting/prompt-profiles.js index 59b4a5b..b17688d 100644 --- a/prompting/prompt-profiles.js +++ b/prompting/prompt-profiles.js @@ -15,6 +15,8 @@ import { DEFAULT_TASK_PROFILE_TEMPLATES } from "./default-task-profile-templates const TASK_TYPES = [ "extract", + "extract_objective", + "extract_subjective", "recall", "compress", "synopsis", @@ -29,6 +31,16 @@ const TASK_TYPE_META = { label: "提取", description: "从当前对话批次中抽取结构化记忆。", }, + extract_objective: { + label: "客观提取", + description: "从当前对话批次中抽取客观层结构化记忆。", + hidden: true, + }, + extract_subjective: { + label: "主观提取", + description: "从客观提取草稿与视角上下文中抽取主观记忆。", + hidden: true, + }, recall: { label: "召回", description: "根据上下文筛选最相关的记忆节点。", @@ -186,6 +198,48 @@ const BUILTIN_BLOCK_DEFINITIONS = [ role: "system", 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", name: "规划:角色卡", @@ -239,6 +293,8 @@ const DEFAULT_TASK_INPUT = Object.freeze({ const LEGACY_PROMPT_FIELD_MAP = { extract: "extractPrompt", + extract_objective: "extractObjectivePrompt", + extract_subjective: "extractSubjectivePrompt", recall: "recallPrompt", compress: "compressPrompt", synopsis: "synopsisPrompt", @@ -1001,11 +1057,14 @@ function getDefaultTaskProfileTemplate(taskType) { if (String(taskType || "") === "planner") { 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") { return null; } - return applyRuntimeDefaultTemplateOverrides(taskType, cloneJson(template)); + return applyRuntimeDefaultTemplateOverrides(templateKey, cloneJson(template)); } function hashTemplateFingerprint(value = "") { @@ -2512,11 +2571,14 @@ export function getTaskTypeMeta(taskType) { id: taskType, label: TASK_TYPE_META[taskType]?.label || taskType, description: TASK_TYPE_META[taskType]?.description || "", + hidden: TASK_TYPE_META[taskType]?.hidden === true, }; } 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() { diff --git a/runtime/settings-defaults.js b/runtime/settings-defaults.js index 8363255..41686f8 100644 --- a/runtime/settings-defaults.js +++ b/runtime/settings-defaults.js @@ -159,6 +159,8 @@ export const defaultSettings = { // 自定义提示词 extractPrompt: "", + extractObjectivePrompt: "", + extractSubjectivePrompt: "", recallPrompt: "", consolidationPrompt: "", compressPrompt: "", diff --git a/tests/default-settings.mjs b/tests/default-settings.mjs index 9538e8a..a889231 100644 --- a/tests/default-settings.mjs +++ b/tests/default-settings.mjs @@ -110,8 +110,12 @@ assert.equal(defaultSettings.nativeRolloutVersion, 2); assert.equal(defaultSettings.nativeEngineFailOpen, true); assert.equal(defaultSettings.graphNativeForceDisable, false); assert.equal(defaultSettings.taskProfilesVersion, 3); +assert.equal(defaultSettings.extractObjectivePrompt, ""); +assert.equal(defaultSettings.extractSubjectivePrompt, ""); assert.ok(defaultSettings.taskProfiles); 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.globalTaskRegex); assert.deepEqual( diff --git a/tests/prompt-builder-defaults.mjs b/tests/prompt-builder-defaults.mjs index 392cd2c..0c568f3 100644 --- a/tests/prompt-builder-defaults.mjs +++ b/tests/prompt-builder-defaults.mjs @@ -47,6 +47,7 @@ installResolveHooks([ const { buildTaskLlmPayload, buildTaskPrompt } = await import("../prompting/prompt-builder.js"); const { + createBuiltinPromptBlock, createDefaultGlobalTaskRegex, createDefaultTaskProfiles, } = await import("../prompting/prompt-profiles.js"); @@ -256,4 +257,98 @@ assert.equal( 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"); diff --git a/tests/task-profile-storage.mjs b/tests/task-profile-storage.mjs index bc9704b..c85ac64 100644 --- a/tests/task-profile-storage.mjs +++ b/tests/task-profile-storage.mjs @@ -7,7 +7,11 @@ import { createLocalRegexRule, exportTaskProfile, getActiveTaskProfile, + getBuiltinBlockDefinitions, getLegacyPromptFieldForTask, + getTaskTypeMeta, + getTaskTypeOptions, + getTaskTypes, importTaskProfile, restoreDefaultTaskProfile, upsertTaskProfile, @@ -97,4 +101,75 @@ const restoredActive = getActiveTaskProfile( assert.equal(restoredActive.id, "default"); 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");