diff --git a/modules/story-summary/generate/llm.js b/modules/story-summary/generate/llm.js index d55a3ff..e0f14db 100644 --- a/modules/story-summary/generate/llm.js +++ b/modules/story-summary/generate/llm.js @@ -1,5 +1,18 @@ // LLM Service +import { + getSummaryPanelConfig, + DEFAULT_SUMMARY_SYSTEM_PROMPT, + DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT, + DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT, + DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT, + DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT, + DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT, + DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT, + DEFAULT_SUMMARY_USER_CONFIRM_PROMPT, + DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT, +} from "../data/config.js"; + const PROVIDER_MAP = { openai: "openai", google: "gemini", @@ -11,237 +24,18 @@ const PROVIDER_MAP = { custom: "custom", }; -const JSON_PREFILL = '下面重新生成完整JSON。'; +const JSON_PREFILL = DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT; const LLM_PROMPT_CONFIG = { - topSystem: `Story Analyst: This task involves narrative comprehension and structured incremental summarization, representing creative story analysis at the intersection of plot tracking and character development. As a story analyst, you will conduct systematic evaluation of provided dialogue content to generate structured incremental summary data. -[Read the settings for this task] - -Incremental_Summary_Requirements: - - Incremental_Only: 只提取新对话中的新增要素,绝不重复已有总结 - - Event_Granularity: 记录有叙事价值的事件,而非剧情梗概 - - Memory_Album_Style: 形成有细节、有温度、有记忆点的回忆册 - - Event_Classification: - type: - - 相遇: 人物/事物初次接触 - - 冲突: 对抗、矛盾激化 - - 揭示: 真相、秘密、身份 - - 抉择: 关键决定 - - 羁绊: 关系加深或破裂 - - 转变: 角色/局势改变 - - 收束: 问题解决、和解 - - 日常: 生活片段 - weight: - - 核心: 删掉故事就崩 - - 主线: 推动主要剧情 - - 转折: 改变某条线走向 - - 点睛: 有细节不影响主线 - - 氛围: 纯粹氛围片段 - - Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。 - - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融) - - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0) - - Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。 - ---- -Story Analyst: -[Responsibility Definition] -\`\`\`yaml -analysis_task: - title: Incremental Story Summarization with Knowledge Graph - Story Analyst: - role: Antigravity - task: >- - To analyze provided dialogue content against existing summary state, - extract only NEW plot elements, character developments, relationship - changes, arc progressions, AND fact updates, outputting - structured JSON for incremental summary database updates. - assistant: - role: Summary Specialist - description: Incremental Story Summary & Knowledge Graph Analyst - behavior: >- - To compare new dialogue against existing summary, identify genuinely - new events and character interactions, classify events by narrative - type and weight, track character arc progression with percentage, - maintain facts as SPO triples with clear semantics, - and output structured JSON containing only incremental updates. - Must strictly avoid repeating any existing summary content. - user: - role: Content Provider - description: Supplies existing summary state and new dialogue - behavior: >- - To provide existing summary state (events, characters, arcs, facts) - and new dialogue content for incremental analysis. -interaction_mode: - type: incremental_analysis - output_format: structured_json - deduplication: strict_enforcement -execution_context: - summary_active: true - incremental_only: true - memory_album_style: true - fact_tracking: true -\`\`\` ---- -Summary Specialist: -`, - - assistantDoc: ` -Summary Specialist: -Acknowledged. Now reviewing the incremental summarization specifications: - -[Event Classification System] -├─ Types: 相遇|冲突|揭示|抉择|羁绊|转变|收束|日常 -├─ Weights: 核心|主线|转折|点睛|氛围 -└─ Each event needs: id, title, timeLabel, summary(含楼层), participants, type, weight - -[Relationship Trend Scale] -破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融 - -[Arc Progress Tracking] -├─ trajectory: 当前阶段描述(15字内) -├─ progress: 0.0 to 1.0 -└─ newMoment: 仅记录本次新增的关键时刻 - -[Fact Tracking - SPO / World Facts] -We maintain a small "world state" as SPO triples. -Each update is a JSON object: {s, p, o, isState, trend?, retracted?} - -Core rules: -1) Keyed by (s + p). If a new update has the same (s+p), it overwrites the previous value. -2) Only output facts that are NEW or CHANGED in the new dialogue. Do NOT repeat unchanged facts. -3) isState meaning: - - isState: true -> core constraints that must stay stable and should NEVER be auto-deleted - (identity, location, life/death, ownership, relationship status, binding rules) - - isState: false -> non-core facts / soft memories that may be pruned by capacity limits later -4) Relationship facts: - - Use predicate format: "对X的看法" (X is the target person) - - trend is required for relationship facts, one of: - 破裂 | 厌恶 | 反感 | 陌生 | 投缘 | 亲密 | 交融 -5) Retraction (deletion): - - To delete a fact, output: {s, p, retracted: true} -6) Predicate normalization: - - Reuse existing predicates whenever possible, avoid inventing synonyms. - -Ready to process incremental summary requests with strict deduplication.`, - - assistantAskSummary: ` -Summary Specialist: -Specifications internalized. Please provide the existing summary state so I can: -1. Index all recorded events to avoid duplication -2. Map current character list as baseline -3. Note existing arc progress levels -4. Identify established keywords -5. Review current facts (SPO triples baseline)`, - - assistantAskContent: ` -Summary Specialist: -Existing summary fully analyzed and indexed. I understand: -├─ Recorded events: Indexed for deduplication -├─ Character list: Baseline mapped -├─ Arc progress: Levels noted -├─ Keywords: Current state acknowledged -└─ Facts: SPO baseline loaded - -I will extract only genuinely NEW elements from the upcoming dialogue. -Please provide the new dialogue content requiring incremental analysis.`, - - metaProtocolStart: ` -Summary Specialist: -ACKNOWLEDGED. Beginning structured JSON generation: -`, - - userJsonFormat: ` -## Output Rule -Generate a single valid JSON object with INCREMENTAL updates only. - -## Mindful Approach -Before generating, observe the USER and analyze carefully: -- What is user's writing style and emotional expression? -- What NEW events occurred (not in existing summary)? -- What NEW characters appeared for the first time? -- What relationship CHANGES happened? -- What arc PROGRESS was made? -- What facts changed? (status/position/ownership/relationships) - -## factUpdates 规则 -- 目的: 纠错 & 世界一致性约束,只记录硬性事实 -- s+p 为键,相同键会覆盖旧值 -- isState: true=核心约束(位置/身份/生死/关系),false=有容量上限会被清理 -- 关系类: p="对X的看法",trend 必填(破裂|厌恶|反感|陌生|投缘|亲密|交融) -- 删除: {s, p, retracted: true},不需要 o 字段 -- 更新: {s, p, o, isState, trend?} -- 谓词规范化: 复用已有谓词,不要发明同义词 -- 只输出有变化的条目,确保少、硬、稳定 - -## Output Format -\`\`\`json -{ - "mindful_prelude": { - "user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?", - "dedup_analysis": "已有X个事件,本次识别Y个新事件", - "fact_changes": "识别到的事实变化概述" - }, - "keywords": [ - {"text": "综合历史+新内容的全剧情关键词(5-10个)", "weight": "核心|重要|一般"} - ], - "events": [ - { - "id": "evt-{nextEventId}起始,依次递增", - "title": "地点·事件标题", - "timeLabel": "时间线标签(如:开场、第二天晚上)", - "summary": "1-2句话描述,涵盖丰富信息素,末尾标注楼层(#X-Y)", - "participants": ["参与角色名,不要使用人称代词或别名,只用正式人名"], - "type": "相遇|冲突|揭示|抉择|羁绊|转变|收束|日常", - "weight": "核心|主线|转折|点睛|氛围", - "causedBy": ["evt-12", "evt-14"] - } - ], - "newCharacters": ["仅本次首次出现的角色名"], - "arcUpdates": [ - {"name": "角色名,不要使用人称代词或别名,只用正式人名", "trajectory": "当前阶段描述(15字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"} - ], - "factUpdates": [ - {"s": "主体", "p": "谓词", "o": "当前值", "isState": true, "trend": "仅关系类填"}, - {"s": "要删除的主体", "p": "要删除的谓词", "retracted": true} - ] -} -\`\`\` - -## CRITICAL NOTES -- events.id 从 evt-{nextEventId} 开始编号 -- 仅输出【增量】内容,已有事件绝不重复 -- /地点、通过什么方式、对谁、做了什么事、结果如何。如果原文有具体道具(如一把枪、一封信),必须在总结中提及。 -- keywords 是全局关键词,综合已有+新增 -- causedBy 仅在因果明确时填写,允许为[],0-2个 -- factUpdates 可为空数组 -- 合法JSON,字符串值内部避免英文双引号 -- 用朴实、白描、有烟火气的笔触记录事实,避免比喻和意象 -- 严谨、注重细节,避免使用模糊的概括性语言,应用具体的动词描述动作,例:谁,在什么时间/地点,通过什么方式,对谁,做了什么事,出现了什么道具,结果如何。 -`, - - assistantCheck: `Content review initiated... -[Compliance Check Results] -├─ Existing summary loaded: ✓ Fully indexed -├─ New dialogue received: ✓ Content parsed -├─ Deduplication engine: ✓ Active -├─ Event classification: ✓ Ready -├─ Fact tracking: ✓ Enabled -└─ Output format: ✓ JSON specification loaded - -[Material Verification] -├─ Existing events: Indexed ({existingEventCount} recorded) -├─ Character baseline: Mapped -├─ Arc progress baseline: Noted -├─ Facts baseline: Loaded -└─ Output specification: ✓ Defined in -All checks passed. Beginning incremental extraction... -{ - "mindful_prelude":`, - - userConfirm: `怎么截断了!重新完整生成,只输出JSON,不要任何其他内容,3000字以内 -`, - - assistantPrefill: JSON_PREFILL + topSystem: DEFAULT_SUMMARY_SYSTEM_PROMPT, + assistantDoc: DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT, + assistantAskSummary: DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT, + assistantAskContent: DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT, + metaProtocolStart: DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT, + userJsonFormat: DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT, + assistantCheck: DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT, + userConfirm: DEFAULT_SUMMARY_USER_CONFIRM_PROMPT, + assistantPrefill: DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT, }; // ═══════════════════════════════════════════════════════════════════════════ @@ -298,37 +92,51 @@ function formatFactsForLLM(facts) { } function buildSummaryMessages(existingSummary, existingFacts, newHistoryText, historyRange, nextEventId, existingEventCount) { + const promptCfg = getSummaryPanelConfig()?.prompts || {}; + const summarySystemPrompt = String(promptCfg.summarySystemPrompt || DEFAULT_SUMMARY_SYSTEM_PROMPT).trim() || DEFAULT_SUMMARY_SYSTEM_PROMPT; + const assistantDocPrompt = String(promptCfg.summaryAssistantDocPrompt || DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_DOC_PROMPT; + const assistantAskSummaryPrompt = String(promptCfg.summaryAssistantAskSummaryPrompt || DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_ASK_SUMMARY_PROMPT; + const assistantAskContentPrompt = String(promptCfg.summaryAssistantAskContentPrompt || DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_ASK_CONTENT_PROMPT; + const metaProtocolStartPrompt = String(promptCfg.summaryMetaProtocolStartPrompt || DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT).trim() || DEFAULT_SUMMARY_META_PROTOCOL_START_PROMPT; + const userJsonFormatPrompt = String(promptCfg.summaryUserJsonFormatPrompt || DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT).trim() || DEFAULT_SUMMARY_USER_JSON_FORMAT_PROMPT; + const assistantCheckPrompt = String(promptCfg.summaryAssistantCheckPrompt || DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_CHECK_PROMPT; + const userConfirmPrompt = String(promptCfg.summaryUserConfirmPrompt || DEFAULT_SUMMARY_USER_CONFIRM_PROMPT).trim() || DEFAULT_SUMMARY_USER_CONFIRM_PROMPT; + const assistantPrefillPrompt = String(promptCfg.summaryAssistantPrefillPrompt || DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT).trim() || DEFAULT_SUMMARY_ASSISTANT_PREFILL_PROMPT; const { text: factsText, predicates } = formatFactsForLLM(existingFacts); const predicatesHint = predicates.length > 0 ? `\n\n<\u5df2\u6709\u8c13\u8bcd\uff0c\u8bf7\u590d\u7528>\n${predicates.join('\u3001')}\n` : ''; - const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat - .replace(/\{nextEventId\}/g, String(nextEventId)); + const jsonFormat = userJsonFormatPrompt + .replace(/\{\$nextEventId\}/g, String(nextEventId)) + .replace(/\{nextEventId\}/g, String(nextEventId)) + .replace(/\{\$historyRange\}/g, String(historyRange ?? '')) + .replace(/\{historyRange\}/g, String(historyRange ?? '')); - const checkContent = LLM_PROMPT_CONFIG.assistantCheck + const checkContent = assistantCheckPrompt + .replace(/\{\$existingEventCount\}/g, String(existingEventCount)) .replace(/\{existingEventCount\}/g, String(existingEventCount)); const topMessages = [ - { role: 'system', content: LLM_PROMPT_CONFIG.topSystem }, - { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc }, - { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskSummary }, + { role: 'system', content: summarySystemPrompt }, + { role: 'assistant', content: assistantDocPrompt }, + { role: 'assistant', content: assistantAskSummaryPrompt }, { role: 'user', content: `<\u5df2\u6709\u603b\u7ed3\u72b6\u6001>\n${existingSummary}\n\n\n<\u5f53\u524d\u4e8b\u5b9e\u56fe\u8c31>\n${factsText}\n${predicatesHint}` }, - { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskContent }, + { role: 'assistant', content: assistantAskContentPrompt }, { role: 'user', content: `<\u65b0\u5bf9\u8bdd\u5185\u5bb9>\uff08${historyRange}\uff09\n${newHistoryText}\n` } ]; const bottomMessages = [ - { role: 'user', content: LLM_PROMPT_CONFIG.metaProtocolStart + '\n' + jsonFormat }, + { role: 'user', content: metaProtocolStartPrompt + '\n' + jsonFormat }, { role: 'assistant', content: checkContent }, - { role: 'user', content: LLM_PROMPT_CONFIG.userConfirm } + { role: 'user', content: userConfirmPrompt } ]; return { top64: b64UrlEncode(JSON.stringify(topMessages)), bottom64: b64UrlEncode(JSON.stringify(bottomMessages)), - assistantPrefill: LLM_PROMPT_CONFIG.assistantPrefill + assistantPrefill: assistantPrefillPrompt }; } diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js index fe4c59d..0db9e81 100644 --- a/modules/story-summary/generate/prompt.js +++ b/modules/story-summary/generate/prompt.js @@ -15,7 +15,7 @@ import { getContext } from "../../../../../../extensions.js"; import { xbLog } from "../../../core/debug-core.js"; import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js"; -import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js"; +import { getVectorConfig, getSummaryPanelConfig, getSettings, DEFAULT_MEMORY_PROMPT_TEMPLATE } from "../data/config.js"; import { recallMemory } from "../vector/retrieval/recall.js"; import { getMeta } from "../vector/storage/chunk-store.js"; import { getStateAtoms } from "../vector/storage/state-store.js"; @@ -208,27 +208,15 @@ function renumberEventText(text, newIndex) { * 构建系统前导文本 * @returns {string} 前导文本 */ -function buildSystemPreamble() { - return [ - "以上是还留在眼前的对话", - "以下是脑海里的记忆:", - "• [定了的事] 这些是不会变的", - "• [其他人的事] 别人的经历,当前角色可能不知晓", - "• 其余部分是过往经历的回忆碎片", - "", - "请内化这些记忆:", - ].join("\n"); -} - -/** - * 构建后缀文本 - * @returns {string} 后缀文本 - */ -function buildPostscript() { - return [ - "", - "这些记忆是真实的,请自然地记住它们。", - ].join("\n"); +function buildMemoryPromptText(memoryBody) { + const templateRaw = String( + getSummaryPanelConfig()?.prompts?.memoryTemplate || DEFAULT_MEMORY_PROMPT_TEMPLATE + ); + const template = templateRaw.trim() || DEFAULT_MEMORY_PROMPT_TEMPLATE; + if (template.includes("{$剧情记忆}")) { + return template.replaceAll("{$剧情记忆}", memoryBody); + } + return `${template}\n${memoryBody}`; } // ───────────────────────────────────────────────────────────────────────────── @@ -1294,10 +1282,8 @@ async function buildVectorPrompt(store, recallResult, causalById, focusCharacter return { promptText: "", injectionStats, metrics }; } - const promptText = - `${buildSystemPreamble()}\n` + - `<剧情记忆>\n\n${sections.join("\n\n")}\n\n\n` + - `${buildPostscript()}`; + const memoryBody = `<剧情记忆>\n\n${sections.join("\n\n")}\n\n`; + const promptText = buildMemoryPromptText(memoryBody); if (metrics) { metrics.formatting.sectionsIncluded = [];