@scotthuang/engram 0.1.0

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 (45) hide show
  1. package/README.md +73 -0
  2. package/dist/__tests__/bm25.test.d.ts +1 -0
  3. package/dist/__tests__/bm25.test.js +86 -0
  4. package/dist/__tests__/bm25.test.js.map +1 -0
  5. package/dist/__tests__/config.test.d.ts +1 -0
  6. package/dist/__tests__/config.test.js +31 -0
  7. package/dist/__tests__/config.test.js.map +1 -0
  8. package/dist/__tests__/profile.test.d.ts +1 -0
  9. package/dist/__tests__/profile.test.js +130 -0
  10. package/dist/__tests__/profile.test.js.map +1 -0
  11. package/dist/__tests__/recall.test.d.ts +1 -0
  12. package/dist/__tests__/recall.test.js +162 -0
  13. package/dist/__tests__/recall.test.js.map +1 -0
  14. package/dist/bm25.d.ts +43 -0
  15. package/dist/bm25.js +172 -0
  16. package/dist/bm25.js.map +1 -0
  17. package/dist/config.d.ts +15 -0
  18. package/dist/config.js +28 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.js +200 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/profile.d.ts +37 -0
  24. package/dist/profile.js +95 -0
  25. package/dist/profile.js.map +1 -0
  26. package/dist/recall.d.ts +37 -0
  27. package/dist/recall.js +173 -0
  28. package/dist/recall.js.map +1 -0
  29. package/dist/settle.d.ts +43 -0
  30. package/dist/settle.js +227 -0
  31. package/dist/settle.js.map +1 -0
  32. package/eslint.config.js +17 -0
  33. package/openclaw.plugin.json +63 -0
  34. package/package.json +34 -0
  35. package/src/__tests__/bm25.test.ts +102 -0
  36. package/src/__tests__/config.test.ts +34 -0
  37. package/src/__tests__/profile.test.ts +147 -0
  38. package/src/__tests__/recall.test.ts +186 -0
  39. package/src/bm25.ts +202 -0
  40. package/src/config.ts +39 -0
  41. package/src/index.ts +246 -0
  42. package/src/profile.ts +114 -0
  43. package/src/recall.ts +213 -0
  44. package/src/settle.ts +277 -0
  45. package/tsconfig.json +16 -0
package/src/settle.ts ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Memory System Plugin - Settle (沉淀机制)
3
+ *
4
+ * 凌晨 Cron 5 步流程:
5
+ * 1. 结构化短期记忆
6
+ * 2. 筛选 + 向量化
7
+ * 3. 更新画像
8
+ * 4. 生成画像摘要
9
+ * 5. 归档清理
10
+ */
11
+
12
+ import { promises as fs } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { ProfileManager } from "./profile.js";
15
+ import { type MemorySystemConfig } from "./config.js";
16
+
17
+ type SettleOptions = {
18
+ workspaceDir: string;
19
+ config: MemorySystemConfig;
20
+ llmCall?: (prompt: string, systemPrompt?: string) => Promise<string>;
21
+ vectorStore?: (text: string, category: string) => Promise<void>;
22
+ };
23
+
24
+ /**
25
+ * 步骤 1: 结构化短期记忆
26
+ * 读取当天原始记录,用 LLM 整理成结构化格式
27
+ */
28
+ export async function structurizeShortTerm(
29
+ opts: SettleOptions,
30
+ date: string = new Date().toISOString().split("T")[0]
31
+ ): Promise<string> {
32
+ const filePath = join(opts.workspaceDir, "memory", `${date}.md`);
33
+ try {
34
+ const raw = await fs.readFile(filePath, "utf-8");
35
+ if (!raw.trim()) return "No content to structurize.";
36
+
37
+ if (!opts.llmCall) return "No LLM configured for structurization.";
38
+
39
+ const systemPrompt = `你是一个记忆整理助手。请将以下原始对话记录整理成结构化格式。
40
+
41
+ 规则:
42
+ 1. 按时间倒序排列
43
+ 2. 每条记录格式:### HH:MM [分类标签]\n摘要内容(一句话)
44
+ 3. 过滤掉无意义的闲聊(打招呼、HEARTBEAT_OK、确认回复、"好的"、"嗯"等)
45
+ 4. 保留所有有价值的信息(决策、偏好、事件、数字、人名、地点等)
46
+ 5. 分类标签可选值:饮食、工作、家庭、技术、决策、健康、购物、出行、随聊
47
+
48
+ 只输出整理后的内容,不要解释。`;
49
+
50
+ const result = await opts.llmCall(raw, systemPrompt);
51
+
52
+ // 写回结构化内容到 short-term 目录
53
+ const shortTermDir = join(opts.workspaceDir, "memory", "short-term");
54
+ await fs.mkdir(shortTermDir, { recursive: true });
55
+ await fs.writeFile(join(shortTermDir, `${date}.md`), result, "utf-8");
56
+
57
+ return `Structurized ${date}.md → short-term/${date}.md`;
58
+ } catch (err) {
59
+ return `Skipped: ${date}.md not found or error: ${err}`;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 步骤 2: 筛选有价值条目 + 向量化
65
+ */
66
+ export async function extractAndVectorize(
67
+ opts: SettleOptions,
68
+ date: string = new Date().toISOString().split("T")[0]
69
+ ): Promise<string> {
70
+ const filePath = join(opts.workspaceDir, "memory", "short-term", `${date}.md`);
71
+ try {
72
+ const content = await fs.readFile(filePath, "utf-8");
73
+ if (!content.trim()) return "No content.";
74
+
75
+ if (!opts.llmCall || !opts.vectorStore) return "No LLM/vector configured.";
76
+
77
+ const systemPrompt = `你是一个信息筛选助手。从以下结构化记忆中筛选出值得长期保留的条目。
78
+
79
+ 保留标准:
80
+ - 包含用户偏好、决策、重要事件、具体信息(地点、金额、人名)
81
+ - 去掉纯闲聊、日常寒暄、临时信息
82
+
83
+ 对每条保留的条目,输出 JSON 数组:
84
+ [{"text": "条目摘要", "category": "分类"}]
85
+
86
+ 只输出 JSON,不要其他内容。`;
87
+
88
+ const result = await opts.llmCall(content, systemPrompt);
89
+ const items = JSON.parse(result);
90
+
91
+ let stored = 0;
92
+ for (const item of items) {
93
+ if (item.text && item.category) {
94
+ await opts.vectorStore(item.text, item.category);
95
+ stored++;
96
+ }
97
+ }
98
+
99
+ return `Extracted ${stored} items for vectorization.`;
100
+ } catch (err) {
101
+ return `Skipped: ${err}`;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * 步骤 3: 更新画像
107
+ */
108
+ export async function updateProfile(
109
+ opts: SettleOptions,
110
+ date: string = new Date().toISOString().split("T")[0]
111
+ ): Promise<string> {
112
+ const filePath = join(opts.workspaceDir, "memory", "short-term", `${date}.md`);
113
+ try {
114
+ const content = await fs.readFile(filePath, "utf-8");
115
+ if (!content.trim()) return "No content.";
116
+
117
+ if (!opts.llmCall) return "No LLM configured.";
118
+
119
+ const profileManager = new ProfileManager(opts.workspaceDir);
120
+ const profile = await profileManager.load();
121
+
122
+ const systemPrompt = `你是一个用户画像分析助手。从以下记忆中抽取用户标签,更新画像。
123
+
124
+ 当前画像:
125
+ ${JSON.stringify(profile.tags, null, 2)}
126
+
127
+ 请输出 JSON:
128
+ {
129
+ "added": [{"dimension": "分类", "value": "标签"}],
130
+ "removed": [{"dimension": "分类", "value": "标签"}],
131
+ "reason": "简要说明"
132
+ }
133
+
134
+ 规则:
135
+ - 只添加有充分依据的标签(至少 2 条记忆佐证)
136
+ - 如果旧标签与新信息矛盾,放入 removed
137
+ - 不要重复添加已有标签
138
+
139
+ 只输出 JSON。`;
140
+
141
+ const result = await opts.llmCall(content, systemPrompt);
142
+ const changes = JSON.parse(result);
143
+
144
+ let added = 0;
145
+ let removed = 0;
146
+
147
+ if (changes.added) {
148
+ for (const tag of changes.added) {
149
+ profileManager.addTag(profile, tag.dimension, tag.value);
150
+ added++;
151
+ }
152
+ }
153
+
154
+ if (changes.removed) {
155
+ for (const tag of changes.removed) {
156
+ if (profile.tags[tag.dimension]) {
157
+ profile.tags[tag.dimension] = profile.tags[tag.dimension].filter(
158
+ t => t.value !== tag.value
159
+ );
160
+ removed++;
161
+ }
162
+ }
163
+ }
164
+
165
+ // 衰减未更新的标签
166
+ profileManager.decayTags(profile, 0.98);
167
+
168
+ await profileManager.save(profile);
169
+
170
+ return `Profile updated: +${added} -${removed}`;
171
+ } catch (err) {
172
+ return `Skipped: ${err}`;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * 步骤 4: 生成画像摘要
178
+ */
179
+ export async function generateProfileSummary(
180
+ opts: SettleOptions
181
+ ): Promise<string> {
182
+ if (!opts.llmCall) return "No LLM configured.";
183
+
184
+ const profileManager = new ProfileManager(opts.workspaceDir);
185
+ const profile = await profileManager.load();
186
+
187
+ const allTags = Object.entries(profile.tags)
188
+ .map(([dim, tags]) => `${dim}: ${tags.map(t => t.value).join(", ")}`)
189
+ .join("\n");
190
+
191
+ if (!allTags) return "No tags to summarize.";
192
+
193
+ const systemPrompt = `将以下用户画像标签压缩为一段 100 字以内的中文摘要,用于 AI 检索时快速理解用户特征。
194
+
195
+ 标签:
196
+ ${allTags}
197
+
198
+ 只输出摘要文本,不要其他内容。`;
199
+
200
+ try {
201
+ const summary = await opts.llmCall(allTags, systemPrompt);
202
+ profile.summary = summary.trim();
203
+
204
+ // 更新 coreTags:取每个维度置信度最高的标签
205
+ const coreTags: string[] = [];
206
+ for (const tags of Object.values(profile.tags)) {
207
+ const sorted = [...tags].sort((a, b) => b.confidence - a.confidence);
208
+ if (sorted.length > 0) {
209
+ coreTags.push(sorted[0].value);
210
+ }
211
+ }
212
+ profile.coreTags = coreTags.slice(0, 10);
213
+
214
+ await profileManager.save(profile);
215
+ return `Summary generated: "${summary.trim()}"`;
216
+ } catch (err) {
217
+ return `Failed: ${err}`;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 步骤 5: 归档清理
223
+ */
224
+ export async function archiveShortTerm(
225
+ opts: SettleOptions
226
+ ): Promise<string> {
227
+ const shortTermDir = join(opts.workspaceDir, "memory", "short-term");
228
+ const coldStorageDir = join(opts.workspaceDir, "memory", "cold-storage");
229
+
230
+ try {
231
+ await fs.mkdir(coldStorageDir, { recursive: true });
232
+ const files = await fs.readdir(shortTermDir);
233
+ let archived = 0;
234
+
235
+ const cutoff = new Date();
236
+ cutoff.setDate(cutoff.getDate() - opts.config.shortTermDays);
237
+
238
+ for (const file of files) {
239
+ if (!file.endsWith(".md")) continue;
240
+ const filePath = join(shortTermDir, file);
241
+ const stat = await fs.stat(filePath);
242
+
243
+ if (stat.mtime < cutoff) {
244
+ // 按月归档
245
+ const monthMatch = file.match(/(\d{4}-\d{2})/);
246
+ const month = monthMatch ? monthMatch[1] : "unknown";
247
+ const destDir = join(coldStorageDir, month);
248
+ await fs.mkdir(destDir, { recursive: true });
249
+ await fs.rename(filePath, join(destDir, file));
250
+ archived++;
251
+ }
252
+ }
253
+
254
+ return `Archived ${archived} files to cold-storage.`;
255
+ } catch (err) {
256
+ return `Archive skipped: ${err}`;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * 执行完整沉淀流程
262
+ */
263
+ export async function runSettlement(opts: SettleOptions): Promise<string[]> {
264
+ const date = new Date().toISOString().split("T")[0];
265
+ const results: string[] = [];
266
+
267
+ console.log(`[engram] Starting settlement for ${date}...`);
268
+
269
+ results.push(await structurizeShortTerm(opts, date));
270
+ results.push(await extractAndVectorize(opts, date));
271
+ results.push(await updateProfile(opts, date));
272
+ results.push(await generateProfileSummary(opts));
273
+ results.push(await archiveShortTerm(opts));
274
+
275
+ console.log(`[engram] Settlement complete: ${results.join("; ")}`);
276
+ return results;
277
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true,
12
+ "sourceMap": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }