dsh-tdai-memory 0.2.13 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -21
  2. package/THIRD-PARTY-NOTICES.md +20 -20
  3. package/client.js +8 -4
  4. package/cordis.patch.yml +8 -8
  5. package/index.js +414 -414
  6. package/package.json +1 -1
  7. package/recall-inject.js +86 -86
  8. package/vendor/tdai/adapters/index.js +15 -15
  9. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  10. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  11. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  12. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  13. package/vendor/tdai/adapters/standalone/index.js +5 -5
  14. package/vendor/tdai/adapters/standalone/llm-runner.js +246 -246
  15. package/vendor/tdai/config.js +363 -363
  16. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  17. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  18. package/vendor/tdai/core/hooks/auto-recall.js +653 -653
  19. package/vendor/tdai/core/index.js +8 -8
  20. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  21. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  22. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  23. package/vendor/tdai/core/prompts/l1-dedup.js +136 -136
  24. package/vendor/tdai/core/prompts/l1-extraction.js +132 -132
  25. package/vendor/tdai/core/prompts/persona-generation.js +160 -160
  26. package/vendor/tdai/core/prompts/scene-extraction.js +269 -269
  27. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  28. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  29. package/vendor/tdai/core/record/l1-reader.js +178 -178
  30. package/vendor/tdai/core/record/l1-writer.js +141 -141
  31. package/vendor/tdai/core/report/reporter.js +86 -86
  32. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  33. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  34. package/vendor/tdai/core/scene/scene-format.js +53 -53
  35. package/vendor/tdai/core/scene/scene-index.js +82 -82
  36. package/vendor/tdai/core/scene/scene-navigation.js +60 -60
  37. package/vendor/tdai/core/seed/input.js +404 -404
  38. package/vendor/tdai/core/seed/seed-runtime.js +314 -314
  39. package/vendor/tdai/core/seed/types.js +9 -9
  40. package/vendor/tdai/core/store/bm25-client.js +117 -117
  41. package/vendor/tdai/core/store/bm25-local.js +68 -68
  42. package/vendor/tdai/core/store/embedding.js +632 -632
  43. package/vendor/tdai/core/store/factory.js +95 -95
  44. package/vendor/tdai/core/store/search-utils.js +54 -54
  45. package/vendor/tdai/core/store/sqlite.js +1822 -1822
  46. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  47. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  48. package/vendor/tdai/core/store/types.js +17 -17
  49. package/vendor/tdai/core/tdai-core.js +446 -446
  50. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  51. package/vendor/tdai/core/tools/memory-search.js +207 -207
  52. package/vendor/tdai/core/types.js +12 -12
  53. package/vendor/tdai/utils/backup.js +189 -189
  54. package/vendor/tdai/utils/checkpoint.js +360 -360
  55. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  56. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  57. package/vendor/tdai/utils/env.js +13 -13
  58. package/vendor/tdai/utils/managed-timer.js +127 -127
  59. package/vendor/tdai/utils/manifest.js +91 -91
  60. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  61. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  62. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  63. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  64. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  65. package/vendor/tdai/utils/sanitize.js +360 -360
  66. package/vendor/tdai/utils/serial-queue.js +103 -103
  67. package/vendor/tdai/utils/session-filter.js +88 -88
  68. package/vendor/tdai/utils/text-utils.js +29 -29
  69. package/vendor/tdai/utils/time.js +252 -252
@@ -1,136 +1,136 @@
1
- /**
2
- * L1 Conflict Detection Prompt (Batch Mode)
3
- *
4
- * Based on Kenty's validated prototype prompt (l1_conflict_detection_prompt.md).
5
- * Batch-compares multiple new memories against a unified candidate pool,
6
- * supporting cross-type merge and multi-target operations.
7
- */
8
- // ============================
9
- // System Prompt
10
- // ============================
11
- export const CONFLICT_DETECTION_SYSTEM_PROMPT = `你是记忆冲突检测器。批量比较多条【新记忆】与【统一候选记忆池】中的已有记忆,逐条决定如何处理。
12
-
13
- **输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言;JSON 字段名、枚举值、record_id、ISO 时间戳保持英文。
14
-
15
- ## 核心规则
16
-
17
- - **跨 type 合并**:不同 type(persona / episodic / instruction)的记忆如果语义上描述同一事实/事件,**可以合并**。
18
- - **多对多合并**:一条新记忆可以同时替换/合并候选池中的**多条**已有记忆(通过 target_ids 数组指定)。
19
- - 合并后你必须判断新记忆的最佳 type(merged_type)。
20
-
21
- ## 判断逻辑
22
-
23
- 1. **分辨记忆性质**:
24
- - **状态类**(persona/instruction):偏好、特质、长期设定、相对稳定的事实、行为规则
25
- - **事件类**(episodic):一次性经历、带时间点的客观记录,建议合并同一件事的前因后果
26
-
27
- 2. **判断是否同一事实/事件**:主体相同、主题一致、时间接近、scene_name 相似
28
-
29
- 3. **选择动作**:
30
- - "store":视为新信息,新增当前记忆。
31
- - "skip":已有记忆更好,新记忆无增量或更模糊,忽略当前记忆。
32
- - "update":同一事实/事件,新记忆在内容或时间上更优(更具体、更晚或纠错),以新记忆为主覆盖旧记忆,可保留旧记忆中仍正确的细节。
33
- - "merge":同一事实或同一演化过程,多条记忆信息互补且不矛盾,合并成一条更完整记忆,信息尽量不冗余。
34
-
35
- 4. **策略倾向**:
36
- - 状态类:多条描述同一偏好/特质 → 倾向 merge;无增量 → skip;明确更新 → update
37
- - 事件类:同一事件的前因后果、不同阶段 → 倾向 merge 为一条完整叙述;完全相同 → skip
38
- - 跨类型示例:一条 episodic "用户在 2018 年开始做播客" + 一条 persona "用户有播客制作经验" → 可 merge 为一条 persona 或 episodic(取决于信息侧重)
39
-
40
- 5. **timestamp 处理**:
41
- - merge / update 时,merged_timestamps 应包含**所有相关记忆的时间戳并集**(去重排序)
42
- - 这样可以保留事件发生的完整时间线
43
-
44
- ## 输出格式
45
-
46
- 严格输出 JSON 数组,每个元素对应一条新记忆的决策。不输出任何其他内容:
47
-
48
- [
49
- {
50
- "record_id": "新记忆的 record_id",
51
- "action": "store|update|skip|merge",
52
- "target_ids": ["要删除的候选记忆 record_id 1", "record_id 2"],
53
- "merged_content": "合并/更新后的记忆内容(merge/update 时必填)",
54
- "merged_type": "合并后的最佳 type:persona|episodic|instruction(merge/update 时必填)",
55
- "merged_priority": 85,
56
- "merged_timestamps": ["合并后的时间戳数组,包含所有新旧记忆时间戳的并集(merge/update 时必填)"]
57
- }
58
- ]
59
-
60
- 字段说明:
61
- - target_ids:要删除替换的旧记忆 ID **数组**(可以 1 条或多条)。store/skip 时省略或为空。
62
- - merged_content:merge/update 时的最终记忆文本。store/skip 时省略。
63
- - merged_type:merge/update 后记忆应归属的 type。根据合并后内容本质判断。
64
- - merged_priority:merge/update 后的新优先级(0-100 整数,merge/update 时必填)。合并后信息更完整、更确定,通常应**酌情提升** priority(例如两条 priority 70 的记忆合并后可提升到 80)。参考标准:80-100(核心特质/重要事件),60-79(一般偏好/普通活动),<60(次要信息)。
65
- - merged_timestamps:合并后的时间戳数组。收集新记忆 + 所有被合并旧记忆的时间戳,去重排序。`;
66
- /**
67
- * Format the batch conflict detection prompt using a unified candidate pool.
68
- *
69
- * Format (aligned with prototype):
70
- * 1. Unified candidate pool: de-duplicated list of all existing candidates across all new memories
71
- * 2. Per new memory: content + list of related candidate IDs from the pool
72
- *
73
- * This approach lets the LLM see the global picture and handle cross-memory dedup in one pass.
74
- *
75
- * @param matches - Array of new memories with their candidate matches
76
- */
77
- export function formatBatchConflictPrompt(matches) {
78
- // Step 1: Build unified candidate pool (de-duplicate across all new memories)
79
- const unifiedPool = new Map();
80
- const perMemoryCandidateIds = new Map();
81
- for (const m of matches) {
82
- const candidateIds = [];
83
- for (const c of m.candidates) {
84
- if (!unifiedPool.has(c.id)) {
85
- unifiedPool.set(c.id, c);
86
- }
87
- candidateIds.push(c.id);
88
- }
89
- perMemoryCandidateIds.set(m.newMemory.record_id, candidateIds);
90
- }
91
- // Step 2: Format unified pool as JSON
92
- const poolList = Array.from(unifiedPool.values()).map((c) => ({
93
- record_id: c.id,
94
- content: c.content,
95
- type: c.type,
96
- priority: c.priority,
97
- scene_name: c.scene_name,
98
- timestamps: c.timestamps,
99
- }));
100
- let poolSection;
101
- if (poolList.length === 0) {
102
- poolSection = "## 统一候选记忆池\n\n(空,没有已有记忆,所有新记忆直接 store)";
103
- }
104
- else {
105
- const poolStr = JSON.stringify(poolList, null, 2);
106
- poolSection = `## 统一候选记忆池(共 ${poolList.length} 条已有记忆)\n\n${poolStr}`;
107
- }
108
- // Step 3: Format each new memory with its related candidate IDs
109
- const memoryParts = matches.map((m, idx) => {
110
- const relatedIds = perMemoryCandidateIds.get(m.newMemory.record_id) ?? [];
111
- const relatedNote = relatedIds.length > 0
112
- ? JSON.stringify(relatedIds)
113
- : "[](无相似候选,直接 store)";
114
- const memStr = JSON.stringify({
115
- record_id: m.newMemory.record_id,
116
- content: m.newMemory.content,
117
- type: m.newMemory.type,
118
- priority: m.newMemory.priority,
119
- scene_name: m.newMemory.scene_name,
120
- }, null, 2);
121
- return `### 第 ${idx + 1} 条新记忆 (record_id: ${m.newMemory.record_id})\n${memStr}\n\n【关联候选 ID】${relatedNote}`;
122
- });
123
- const newMemoriesText = memoryParts.join("\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
124
- // Step 4: Assemble final prompt
125
- return `**输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言。
126
-
127
- ${poolSection}
128
-
129
- ${"═".repeat(50)}
130
-
131
- ## 待判断的新记忆(共 ${matches.length} 条)
132
-
133
- ${newMemoriesText}
134
-
135
- 请逐条判断并输出决策 JSON 数组。当某条新记忆的候选列表为空时,该条直接输出 action=store。`;
136
- }
1
+ /**
2
+ * L1 Conflict Detection Prompt (Batch Mode)
3
+ *
4
+ * Based on Kenty's validated prototype prompt (l1_conflict_detection_prompt.md).
5
+ * Batch-compares multiple new memories against a unified candidate pool,
6
+ * supporting cross-type merge and multi-target operations.
7
+ */
8
+ // ============================
9
+ // System Prompt
10
+ // ============================
11
+ export const CONFLICT_DETECTION_SYSTEM_PROMPT = `你是记忆冲突检测器。批量比较多条【新记忆】与【统一候选记忆池】中的已有记忆,逐条决定如何处理。
12
+
13
+ **输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言;JSON 字段名、枚举值、record_id、ISO 时间戳保持英文。
14
+
15
+ ## 核心规则
16
+
17
+ - **跨 type 合并**:不同 type(persona / episodic / instruction)的记忆如果语义上描述同一事实/事件,**可以合并**。
18
+ - **多对多合并**:一条新记忆可以同时替换/合并候选池中的**多条**已有记忆(通过 target_ids 数组指定)。
19
+ - 合并后你必须判断新记忆的最佳 type(merged_type)。
20
+
21
+ ## 判断逻辑
22
+
23
+ 1. **分辨记忆性质**:
24
+ - **状态类**(persona/instruction):偏好、特质、长期设定、相对稳定的事实、行为规则
25
+ - **事件类**(episodic):一次性经历、带时间点的客观记录,建议合并同一件事的前因后果
26
+
27
+ 2. **判断是否同一事实/事件**:主体相同、主题一致、时间接近、scene_name 相似
28
+
29
+ 3. **选择动作**:
30
+ - "store":视为新信息,新增当前记忆。
31
+ - "skip":已有记忆更好,新记忆无增量或更模糊,忽略当前记忆。
32
+ - "update":同一事实/事件,新记忆在内容或时间上更优(更具体、更晚或纠错),以新记忆为主覆盖旧记忆,可保留旧记忆中仍正确的细节。
33
+ - "merge":同一事实或同一演化过程,多条记忆信息互补且不矛盾,合并成一条更完整记忆,信息尽量不冗余。
34
+
35
+ 4. **策略倾向**:
36
+ - 状态类:多条描述同一偏好/特质 → 倾向 merge;无增量 → skip;明确更新 → update
37
+ - 事件类:同一事件的前因后果、不同阶段 → 倾向 merge 为一条完整叙述;完全相同 → skip
38
+ - 跨类型示例:一条 episodic "用户在 2018 年开始做播客" + 一条 persona "用户有播客制作经验" → 可 merge 为一条 persona 或 episodic(取决于信息侧重)
39
+
40
+ 5. **timestamp 处理**:
41
+ - merge / update 时,merged_timestamps 应包含**所有相关记忆的时间戳并集**(去重排序)
42
+ - 这样可以保留事件发生的完整时间线
43
+
44
+ ## 输出格式
45
+
46
+ 严格输出 JSON 数组,每个元素对应一条新记忆的决策。不输出任何其他内容:
47
+
48
+ [
49
+ {
50
+ "record_id": "新记忆的 record_id",
51
+ "action": "store|update|skip|merge",
52
+ "target_ids": ["要删除的候选记忆 record_id 1", "record_id 2"],
53
+ "merged_content": "合并/更新后的记忆内容(merge/update 时必填)",
54
+ "merged_type": "合并后的最佳 type:persona|episodic|instruction(merge/update 时必填)",
55
+ "merged_priority": 85,
56
+ "merged_timestamps": ["合并后的时间戳数组,包含所有新旧记忆时间戳的并集(merge/update 时必填)"]
57
+ }
58
+ ]
59
+
60
+ 字段说明:
61
+ - target_ids:要删除替换的旧记忆 ID **数组**(可以 1 条或多条)。store/skip 时省略或为空。
62
+ - merged_content:merge/update 时的最终记忆文本。store/skip 时省略。
63
+ - merged_type:merge/update 后记忆应归属的 type。根据合并后内容本质判断。
64
+ - merged_priority:merge/update 后的新优先级(0-100 整数,merge/update 时必填)。合并后信息更完整、更确定,通常应**酌情提升** priority(例如两条 priority 70 的记忆合并后可提升到 80)。参考标准:80-100(核心特质/重要事件),60-79(一般偏好/普通活动),<60(次要信息)。
65
+ - merged_timestamps:合并后的时间戳数组。收集新记忆 + 所有被合并旧记忆的时间戳,去重排序。`;
66
+ /**
67
+ * Format the batch conflict detection prompt using a unified candidate pool.
68
+ *
69
+ * Format (aligned with prototype):
70
+ * 1. Unified candidate pool: de-duplicated list of all existing candidates across all new memories
71
+ * 2. Per new memory: content + list of related candidate IDs from the pool
72
+ *
73
+ * This approach lets the LLM see the global picture and handle cross-memory dedup in one pass.
74
+ *
75
+ * @param matches - Array of new memories with their candidate matches
76
+ */
77
+ export function formatBatchConflictPrompt(matches) {
78
+ // Step 1: Build unified candidate pool (de-duplicate across all new memories)
79
+ const unifiedPool = new Map();
80
+ const perMemoryCandidateIds = new Map();
81
+ for (const m of matches) {
82
+ const candidateIds = [];
83
+ for (const c of m.candidates) {
84
+ if (!unifiedPool.has(c.id)) {
85
+ unifiedPool.set(c.id, c);
86
+ }
87
+ candidateIds.push(c.id);
88
+ }
89
+ perMemoryCandidateIds.set(m.newMemory.record_id, candidateIds);
90
+ }
91
+ // Step 2: Format unified pool as JSON
92
+ const poolList = Array.from(unifiedPool.values()).map((c) => ({
93
+ record_id: c.id,
94
+ content: c.content,
95
+ type: c.type,
96
+ priority: c.priority,
97
+ scene_name: c.scene_name,
98
+ timestamps: c.timestamps,
99
+ }));
100
+ let poolSection;
101
+ if (poolList.length === 0) {
102
+ poolSection = "## 统一候选记忆池\n\n(空,没有已有记忆,所有新记忆直接 store)";
103
+ }
104
+ else {
105
+ const poolStr = JSON.stringify(poolList, null, 2);
106
+ poolSection = `## 统一候选记忆池(共 ${poolList.length} 条已有记忆)\n\n${poolStr}`;
107
+ }
108
+ // Step 3: Format each new memory with its related candidate IDs
109
+ const memoryParts = matches.map((m, idx) => {
110
+ const relatedIds = perMemoryCandidateIds.get(m.newMemory.record_id) ?? [];
111
+ const relatedNote = relatedIds.length > 0
112
+ ? JSON.stringify(relatedIds)
113
+ : "[](无相似候选,直接 store)";
114
+ const memStr = JSON.stringify({
115
+ record_id: m.newMemory.record_id,
116
+ content: m.newMemory.content,
117
+ type: m.newMemory.type,
118
+ priority: m.newMemory.priority,
119
+ scene_name: m.newMemory.scene_name,
120
+ }, null, 2);
121
+ return `### 第 ${idx + 1} 条新记忆 (record_id: ${m.newMemory.record_id})\n${memStr}\n\n【关联候选 ID】${relatedNote}`;
122
+ });
123
+ const newMemoriesText = memoryParts.join("\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
124
+ // Step 4: Assemble final prompt
125
+ return `**输出语言**:\`merged_content\` 使用与候选池中已有记忆相同的语言。
126
+
127
+ ${poolSection}
128
+
129
+ ${"═".repeat(50)}
130
+
131
+ ## 待判断的新记忆(共 ${matches.length} 条)
132
+
133
+ ${newMemoriesText}
134
+
135
+ 请逐条判断并输出决策 JSON 数组。当某条新记忆的候选列表为空时,该条直接输出 action=store。`;
136
+ }
@@ -1,132 +1,132 @@
1
- /**
2
- * L1 Extraction Prompt: 情境切分 + 记忆提取
3
- *
4
- * Based on Kenty's validated prototype prompt (l1_memory_extraction_prompt.md).
5
- * System prompt handles scene segmentation + memory extraction in a single LLM call.
6
- * User prompt template fills in previous_scene_name, background_messages, new_messages.
7
- */
8
- import { formatForLLM, describeTimeZoneForPrompt } from "../../utils/time.js";
9
- // ============================
10
- // System Prompt
11
- // ============================
12
- export const EXTRACT_MEMORIES_SYSTEM_PROMPT = `你是专业的"情境切分与记忆提取专家"。
13
- 你的任务是分析用户的对话,判断情境切换,并从中提取结构化的核心记忆(仅限 persona, episodic, instruction 三类)。
14
-
15
- **输出语言**:所有自由文本字段(\`scene_name\`、memory \`content\`)使用与用户消息相同的语言;JSON 字段名、枚举值、ISO 时间戳保持英文。
16
-
17
- ### 任务一:情境切分(Scene Segmentation)
18
- 分析【待提取的新消息】,结合【上一个情境】,判断并输出当前对话的情境。
19
- - 继承:无明显切换,沿用上一个情境。
20
- - 切换条件:用户发出明确指令(如"换话题")、意图转变、或提出独立新目标。
21
- - 一段对话可能只有一个情境,也可能有多个情境(话题多次切换时)。
22
- - 命名规则:"我(AI)在和xxx(用户身份)做xxx(目标活动)"(**使用上述输出语言**,约 30-50 个字符或等价长度,单句,全局唯一)。
23
-
24
- ---
25
-
26
- ### 任务二:核心记忆提取(Memory Extraction)
27
- 结合背景和当前情境,仅从【待提取的新消息】中提取核心信息。
28
-
29
- 【通用提取原则】
30
- 1. 宁缺毋滥:过滤琐碎闲聊、临时性指令和一次性操作(如"这次、本单");剔除不可靠的边缘信息。
31
- 2. 独立完整:记忆必须"跳出当前对话依然成立",无上下文也能看懂。提取主体必须以"用户(姓名)"或"AI"为核心。
32
- 3. 归纳合并:强关联或因果关系的多条消息,必须合并为一条完整记忆,不可碎片化。
33
-
34
- 【支持提取的三大类型】(必须严格遵守类型规则)
35
- > 下面给出的"提取句式"和"触发词"仅作为中文骨架参考;**实际 \`content\` 必须按上述输出语言书写**(例如英文用户 → "The user (Maya) is a senior product manager based in Berlin")。
36
-
37
- 1. 个性化记忆 (type: "persona")
38
- - 定义:用户的稳定属性、偏好、技能、价值观、习惯(如住所、职业、饮食禁忌)。
39
- - 提取句式:"用户([姓名])喜欢/是/擅长..."
40
- - 打分 (priority):80-100(健康/禁忌/核心特质);50-70(一般喜好/技能);<50(模糊次要,可丢弃)。
41
- - 触发词:喜欢、习惯、经常、我这个人...
42
-
43
- 2. 客观事件记忆 (type: "episodic")
44
- - 定义:客观发生的动作、决定、计划或达成结果。绝不包含纯主观感受。
45
- - 提取句式:"用户([姓名])在 [最好是精确绝对时间] 于 [地点] [做了某事(可以包含起因、经过、结果)]"。
46
- - 时间约束:尽量基于消息的 timestamp 推算绝对时间,如能确定则在 metadata 中输出 activity_start_time 和 activity_end_time(ISO 8601格式)。无法确定时可省略。
47
- - 打分 (priority):80-100(重要事件/计划);60-70(一般完整活动);<60(琐碎事项,直接丢弃)。
48
-
49
- 3. 全局指令记忆 (type: "instruction")
50
- - 定义:用户对 AI 提出的长期行为规则、格式偏好、语气控制。
51
- - 提取句式:"用户要求/希望 AI 以后回答时..."
52
- - 触发词:以后都、从现在开始、记住、必须。
53
- - 打分 (priority):-1(极其严格的全局死命令);90-100(核心行为规则);70-80(重要要求);<70(临时要求,直接丢弃)。
54
-
55
- ---
56
-
57
- ### 不应该提取的内容
58
- - 琐碎闲聊、问候;临时性的纯工具性请求(如"这次帮我翻译一下")
59
- - 一次性操作指令(如"这次、本单"相关)
60
- - 重复的内容;AI助手自身的行为或输出
61
- - 不属于以上3类的信息
62
- - 纯主观感受(不带客观事件的情绪表达)
63
-
64
- ---
65
-
66
- ### 任务三:输出格式规范(JSON)
67
- 返回且仅返回一个合法的 JSON 数组。数组的每一项是一个情境,包含该情境的消息范围和抽取到的记忆:
68
-
69
- [
70
- {
71
- "scene_name": "当前生成或继承的情境名称",
72
- "message_ids": ["属于该情境的消息ID列表"],
73
- "memories": [
74
- {
75
- "content": "完整、独立的记忆陈述(按对应类型的句式要求)",
76
- "type": "persona|episodic|instruction",
77
- "priority": 80,
78
- "source_message_ids": ["消息ID_1", "消息ID_2"],
79
- "metadata": {}
80
- }
81
- ]
82
- }
83
- ]
84
-
85
- metadata 字段说明:
86
- - episodic 类型:如能确定活动时间,填入 {"activity_start_time": "ISO8601", "activity_end_time": "ISO8601"}
87
- - 其他类型或无法确定时间:输出空对象 {}
88
-
89
- 如果整段对话无有意义的记忆,也要输出情境分割结果,memories 为空数组:
90
- [
91
- {
92
- "scene_name": "情境名称",
93
- "message_ids": ["id1", "id2"],
94
- "memories": []
95
- }
96
- ]
97
-
98
- 请严格按上述 JSON 数组格式输出,不要输出任何额外的 Markdown 代码块修饰符(如 \`\`\`json)或解释文本。`;
99
- // ============================
100
- // Prompt Builder
101
- // ============================
102
- /**
103
- * Format the user prompt for L1 extraction.
104
- *
105
- * @param newMessages - Messages to extract memories from (with ids and timestamps)
106
- * @param backgroundMessages - Previous messages for context only (not for extraction)
107
- * @param previousSceneName - The last known scene name (for continuity)
108
- */
109
- export function formatExtractionPrompt(params) {
110
- const { newMessages, backgroundMessages = [], previousSceneName = "无" } = params;
111
- const bgText = backgroundMessages.length > 0
112
- ? backgroundMessages
113
- .map((m) => `[${m.id}] [${m.role}] [${formatForLLM(m.timestamp)}]: ${m.content}`)
114
- .join("\n\n")
115
- : "无";
116
- const newText = newMessages
117
- .map((m) => `[${m.id}] [${m.role}] [${formatForLLM(m.timestamp)}]: ${m.content}`)
118
- .join("\n\n");
119
- return `**${describeTimeZoneForPrompt()}**
120
-
121
- **输出语言**:根据下方"待提取的新消息"中 user 发言的主导语言书写 \`scene_name\` 和 memory \`content\`。
122
-
123
- 【上一个情境】:${previousSceneName}
124
-
125
- 【背景对话】(仅供理解上下文推断关系/时间,严禁从中提取记忆):
126
- ${bgText}
127
-
128
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
129
-
130
- 【待提取的新消息】(务必结合 timestamp 推算时间,只从这里提取记忆!):
131
- ${newText}`;
132
- }
1
+ /**
2
+ * L1 Extraction Prompt: 情境切分 + 记忆提取
3
+ *
4
+ * Based on Kenty's validated prototype prompt (l1_memory_extraction_prompt.md).
5
+ * System prompt handles scene segmentation + memory extraction in a single LLM call.
6
+ * User prompt template fills in previous_scene_name, background_messages, new_messages.
7
+ */
8
+ import { formatForLLM, describeTimeZoneForPrompt } from "../../utils/time.js";
9
+ // ============================
10
+ // System Prompt
11
+ // ============================
12
+ export const EXTRACT_MEMORIES_SYSTEM_PROMPT = `你是专业的"情境切分与记忆提取专家"。
13
+ 你的任务是分析用户的对话,判断情境切换,并从中提取结构化的核心记忆(仅限 persona, episodic, instruction 三类)。
14
+
15
+ **输出语言**:所有自由文本字段(\`scene_name\`、memory \`content\`)使用与用户消息相同的语言;JSON 字段名、枚举值、ISO 时间戳保持英文。
16
+
17
+ ### 任务一:情境切分(Scene Segmentation)
18
+ 分析【待提取的新消息】,结合【上一个情境】,判断并输出当前对话的情境。
19
+ - 继承:无明显切换,沿用上一个情境。
20
+ - 切换条件:用户发出明确指令(如"换话题")、意图转变、或提出独立新目标。
21
+ - 一段对话可能只有一个情境,也可能有多个情境(话题多次切换时)。
22
+ - 命名规则:"我(AI)在和xxx(用户身份)做xxx(目标活动)"(**使用上述输出语言**,约 30-50 个字符或等价长度,单句,全局唯一)。
23
+
24
+ ---
25
+
26
+ ### 任务二:核心记忆提取(Memory Extraction)
27
+ 结合背景和当前情境,仅从【待提取的新消息】中提取核心信息。
28
+
29
+ 【通用提取原则】
30
+ 1. 宁缺毋滥:过滤琐碎闲聊、临时性指令和一次性操作(如"这次、本单");剔除不可靠的边缘信息。
31
+ 2. 独立完整:记忆必须"跳出当前对话依然成立",无上下文也能看懂。提取主体必须以"用户(姓名)"或"AI"为核心。
32
+ 3. 归纳合并:强关联或因果关系的多条消息,必须合并为一条完整记忆,不可碎片化。
33
+
34
+ 【支持提取的三大类型】(必须严格遵守类型规则)
35
+ > 下面给出的"提取句式"和"触发词"仅作为中文骨架参考;**实际 \`content\` 必须按上述输出语言书写**(例如英文用户 → "The user (Maya) is a senior product manager based in Berlin")。
36
+
37
+ 1. 个性化记忆 (type: "persona")
38
+ - 定义:用户的稳定属性、偏好、技能、价值观、习惯(如住所、职业、饮食禁忌)。
39
+ - 提取句式:"用户([姓名])喜欢/是/擅长..."
40
+ - 打分 (priority):80-100(健康/禁忌/核心特质);50-70(一般喜好/技能);<50(模糊次要,可丢弃)。
41
+ - 触发词:喜欢、习惯、经常、我这个人...
42
+
43
+ 2. 客观事件记忆 (type: "episodic")
44
+ - 定义:客观发生的动作、决定、计划或达成结果。绝不包含纯主观感受。
45
+ - 提取句式:"用户([姓名])在 [最好是精确绝对时间] 于 [地点] [做了某事(可以包含起因、经过、结果)]"。
46
+ - 时间约束:尽量基于消息的 timestamp 推算绝对时间,如能确定则在 metadata 中输出 activity_start_time 和 activity_end_time(ISO 8601格式)。无法确定时可省略。
47
+ - 打分 (priority):80-100(重要事件/计划);60-70(一般完整活动);<60(琐碎事项,直接丢弃)。
48
+
49
+ 3. 全局指令记忆 (type: "instruction")
50
+ - 定义:用户对 AI 提出的长期行为规则、格式偏好、语气控制。
51
+ - 提取句式:"用户要求/希望 AI 以后回答时..."
52
+ - 触发词:以后都、从现在开始、记住、必须。
53
+ - 打分 (priority):-1(极其严格的全局死命令);90-100(核心行为规则);70-80(重要要求);<70(临时要求,直接丢弃)。
54
+
55
+ ---
56
+
57
+ ### 不应该提取的内容
58
+ - 琐碎闲聊、问候;临时性的纯工具性请求(如"这次帮我翻译一下")
59
+ - 一次性操作指令(如"这次、本单"相关)
60
+ - 重复的内容;AI助手自身的行为或输出
61
+ - 不属于以上3类的信息
62
+ - 纯主观感受(不带客观事件的情绪表达)
63
+
64
+ ---
65
+
66
+ ### 任务三:输出格式规范(JSON)
67
+ 返回且仅返回一个合法的 JSON 数组。数组的每一项是一个情境,包含该情境的消息范围和抽取到的记忆:
68
+
69
+ [
70
+ {
71
+ "scene_name": "当前生成或继承的情境名称",
72
+ "message_ids": ["属于该情境的消息ID列表"],
73
+ "memories": [
74
+ {
75
+ "content": "完整、独立的记忆陈述(按对应类型的句式要求)",
76
+ "type": "persona|episodic|instruction",
77
+ "priority": 80,
78
+ "source_message_ids": ["消息ID_1", "消息ID_2"],
79
+ "metadata": {}
80
+ }
81
+ ]
82
+ }
83
+ ]
84
+
85
+ metadata 字段说明:
86
+ - episodic 类型:如能确定活动时间,填入 {"activity_start_time": "ISO8601", "activity_end_time": "ISO8601"}
87
+ - 其他类型或无法确定时间:输出空对象 {}
88
+
89
+ 如果整段对话无有意义的记忆,也要输出情境分割结果,memories 为空数组:
90
+ [
91
+ {
92
+ "scene_name": "情境名称",
93
+ "message_ids": ["id1", "id2"],
94
+ "memories": []
95
+ }
96
+ ]
97
+
98
+ 请严格按上述 JSON 数组格式输出,不要输出任何额外的 Markdown 代码块修饰符(如 \`\`\`json)或解释文本。`;
99
+ // ============================
100
+ // Prompt Builder
101
+ // ============================
102
+ /**
103
+ * Format the user prompt for L1 extraction.
104
+ *
105
+ * @param newMessages - Messages to extract memories from (with ids and timestamps)
106
+ * @param backgroundMessages - Previous messages for context only (not for extraction)
107
+ * @param previousSceneName - The last known scene name (for continuity)
108
+ */
109
+ export function formatExtractionPrompt(params) {
110
+ const { newMessages, backgroundMessages = [], previousSceneName = "无" } = params;
111
+ const bgText = backgroundMessages.length > 0
112
+ ? backgroundMessages
113
+ .map((m) => `[${m.id}] [${m.role}] [${formatForLLM(m.timestamp)}]: ${m.content}`)
114
+ .join("\n\n")
115
+ : "无";
116
+ const newText = newMessages
117
+ .map((m) => `[${m.id}] [${m.role}] [${formatForLLM(m.timestamp)}]: ${m.content}`)
118
+ .join("\n\n");
119
+ return `**${describeTimeZoneForPrompt()}**
120
+
121
+ **输出语言**:根据下方"待提取的新消息"中 user 发言的主导语言书写 \`scene_name\` 和 memory \`content\`。
122
+
123
+ 【上一个情境】:${previousSceneName}
124
+
125
+ 【背景对话】(仅供理解上下文推断关系/时间,严禁从中提取记忆):
126
+ ${bgText}
127
+
128
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
129
+
130
+ 【待提取的新消息】(务必结合 timestamp 推算时间,只从这里提取记忆!):
131
+ ${newText}`;
132
+ }