@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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Engram
2
+
3
+ 分层语义记忆系统 - OpenClaw Plugin
4
+
5
+ ## 方案文档
6
+
7
+ 详见 [memory-profile-proposal.md](../openclaw/workspace/docs/memory-profile-proposal.md)
8
+
9
+ ## 架构
10
+
11
+ ```
12
+ 短期记忆 (BM25 + jieba)
13
+ ↓ 凌晨 Cron 沉淀
14
+ 画像层 (LLM 抽取 + 摘要压缩)
15
+ ↓ 有价值条目
16
+ 向量层 (lancedb + text-embedding-v3)
17
+ ↓ 超过 7 天
18
+ 归档层 (cold-storage, 不索引)
19
+ ```
20
+
21
+ ## 文件结构
22
+
23
+ ```
24
+ src/
25
+ ├── index.ts # Plugin 入口 (register)
26
+ ├── config.ts # 配置定义
27
+ ├── recall.ts # Auto-Recall (双路召回)
28
+ ├── settle.ts # 沉淀逻辑 (Cron 5步)
29
+ ├── bm25.ts # BM25 索引
30
+ ├── profile.ts # 画像管理
31
+ ```
32
+
33
+ ## 开发
34
+
35
+ ```bash
36
+ cd /Users/scotthuang/github/engram
37
+ npm install
38
+ ```
39
+
40
+ ## 安装到 OpenClaw
41
+
42
+ ```bash
43
+ openclaw plugins install -l /Users/scotthuang/github/engram
44
+ ```
45
+
46
+ ## 配置
47
+
48
+ ```json5
49
+ {
50
+ plugins: {
51
+ slots: { memory: "engram" },
52
+ entries: {
53
+ "engram": {
54
+ enabled: true,
55
+ config: {
56
+ shortTermDays: 7,
57
+ halfLifeDays: 30,
58
+ recallTopK: 3,
59
+ minScore: 0.4,
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## CLI
68
+
69
+ ```bash
70
+ openclaw memory-sys settle # 手动触发沉淀
71
+ openclaw memory-sys profile # 查看画像
72
+ openclaw memory-sys stats # 统计信息
73
+ ```
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { BM25Index } from "../bm25.js";
3
+ describe("BM25Index", () => {
4
+ describe("tokenize (via search)", () => {
5
+ const index = new BM25Index();
6
+ it("finds Chinese bigram matches", () => {
7
+ index.addEntry({ text: "在体育西吃了潮汕牛肉火锅", date: "2026-03-15", category: "饮食", filePath: "/test" });
8
+ index.addEntry({ text: "讨论了记忆系统方案", date: "2026-03-15", category: "技术", filePath: "/test" });
9
+ const results = index.search("牛肉火锅", 2);
10
+ expect(results).toHaveLength(1);
11
+ expect(results[0].entry.category).toBe("饮食");
12
+ });
13
+ it("finds English word matches", () => {
14
+ index.addEntry({ text: "installed node modules successfully", date: "2026-03-15", category: "技术", filePath: "/test" });
15
+ index.addEntry({ text: "天气很好适合出门", date: "2026-03-15", category: "随聊", filePath: "/test" });
16
+ const results = index.search("node modules", 2);
17
+ expect(results).toHaveLength(1);
18
+ expect(results[0].entry.category).toBe("技术");
19
+ });
20
+ it("returns empty for no matches", () => {
21
+ index.addEntry({ text: "今天天气很好", date: "2026-03-15", category: "随聊", filePath: "/test" });
22
+ const results = index.search("量子力学", 3);
23
+ expect(results).toHaveLength(0);
24
+ });
25
+ });
26
+ describe("search", () => {
27
+ it("returns results sorted by score", () => {
28
+ const index = new BM25Index();
29
+ index.addEntry({ text: "牛肉火锅牛肉火锅牛肉火锅", date: "2026-03-15", category: "饮食", filePath: "/test" });
30
+ index.addEntry({ text: "吃过一次牛肉火锅", date: "2026-03-15", category: "饮食", filePath: "/test" });
31
+ index.addEntry({ text: "今天去跑步了", date: "2026-03-15", category: "健康", filePath: "/test" });
32
+ const results = index.search("牛肉火锅", 3);
33
+ expect(results.length).toBeGreaterThanOrEqual(2);
34
+ // 更高频率的应排在前面
35
+ expect(results[0].score).toBeGreaterThanOrEqual(results[1].score);
36
+ });
37
+ it("respects topK limit", () => {
38
+ const index = new BM25Index();
39
+ for (let i = 0; i < 10; i++) {
40
+ index.addEntry({ text: `测试条目${i} 包含关键词`, date: "2026-03-15", category: "测试", filePath: "/test" });
41
+ }
42
+ const results = index.search("关键词", 3);
43
+ expect(results.length).toBeLessThanOrEqual(3);
44
+ });
45
+ });
46
+ describe("addEntry", () => {
47
+ it("updates index size", () => {
48
+ const index = new BM25Index();
49
+ expect(index.size).toBe(0);
50
+ index.addEntry({ text: "test entry", date: "2026-03-15", category: "测试", filePath: "/test" });
51
+ expect(index.size).toBe(1);
52
+ index.addEntry({ text: "another entry", date: "2026-03-15", category: "测试", filePath: "/test" });
53
+ expect(index.size).toBe(2);
54
+ });
55
+ });
56
+ describe("parseEntries", () => {
57
+ it("parses structured format correctly", async () => {
58
+ const index = new BM25Index();
59
+ // buildFromDirectory 会调用 parseEntries
60
+ // 直接测试 search 验证解析结果
61
+ const mockContent = `# 2026-03-15
62
+
63
+ ### 19:30 [饮食]
64
+ 在体育西吃了潮汕牛肉火锅,胸口捞很好吃
65
+
66
+ ### 14:00 [技术]
67
+ 讨论了记忆系统方案,确定三层架构
68
+
69
+ ### 10:00 [随聊]
70
+ 聊了股票
71
+ `;
72
+ // 使用 addEntry 模拟解析后的结果
73
+ index.addEntry({ text: "在体育西吃了潮汕牛肉火锅,胸口捞很好吃", date: "2026-03-15", category: "饮食", filePath: "/test" });
74
+ index.addEntry({ text: "讨论了记忆系统方案,确定三层架构", date: "2026-03-15", category: "技术", filePath: "/test" });
75
+ // 搜饮食
76
+ const foodResults = index.search("牛肉火锅", 1);
77
+ expect(foodResults).toHaveLength(1);
78
+ expect(foodResults[0].entry.category).toBe("饮食");
79
+ // 搜技术
80
+ const techResults = index.search("记忆系统", 1);
81
+ expect(techResults).toHaveLength(1);
82
+ expect(techResults[0].entry.category).toBe("技术");
83
+ });
84
+ });
85
+ });
86
+ //# sourceMappingURL=bm25.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bm25.test.js","sourceRoot":"","sources":["../../src/__tests__/bm25.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseConfig, DEFAULTS } from "../config.js";
3
+ describe("parseConfig", () => {
4
+ it("returns defaults when no config provided", () => {
5
+ const config = parseConfig();
6
+ expect(config.shortTermDays).toBe(DEFAULTS.shortTermDays);
7
+ expect(config.halfLifeDays).toBe(DEFAULTS.halfLifeDays);
8
+ expect(config.recallTopK).toBe(DEFAULTS.recallTopK);
9
+ expect(config.minScore).toBe(DEFAULTS.minScore);
10
+ expect(config.vectorWeight).toBe(DEFAULTS.vectorWeight);
11
+ expect(config.textWeight).toBe(DEFAULTS.textWeight);
12
+ });
13
+ it("returns defaults when empty object provided", () => {
14
+ const config = parseConfig({});
15
+ expect(config.shortTermDays).toBe(7);
16
+ expect(config.halfLifeDays).toBe(30);
17
+ });
18
+ it("overrides defaults with provided values", () => {
19
+ const config = parseConfig({ shortTermDays: 14, minScore: 0.5 });
20
+ expect(config.shortTermDays).toBe(14);
21
+ expect(config.minScore).toBe(0.5);
22
+ // 其他值保持默认
23
+ expect(config.halfLifeDays).toBe(DEFAULTS.halfLifeDays);
24
+ });
25
+ it("ignores invalid types and uses defaults", () => {
26
+ const config = parseConfig({ shortTermDays: "abc", halfLifeDays: null });
27
+ expect(config.shortTermDays).toBe(DEFAULTS.shortTermDays);
28
+ expect(config.halfLifeDays).toBe(DEFAULTS.halfLifeDays);
29
+ });
30
+ });
31
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAErD,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;QACxD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACpD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;QACxD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;QACjE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClC,UAAU;QACV,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,aAAa,EAAE,KAAY,EAAE,YAAY,EAAE,IAAW,EAAE,CAAC,CAAC;QACvF,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { ProfileManager, EMPTY_PROFILE } from "../profile.js";
3
+ import { promises as fs } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ describe("ProfileManager", () => {
7
+ let manager;
8
+ let tempDir;
9
+ beforeEach(async () => {
10
+ tempDir = join(tmpdir(), `engram-test-${Date.now()}`);
11
+ await fs.mkdir(tempDir, { recursive: true });
12
+ manager = new ProfileManager(tempDir);
13
+ });
14
+ describe("load", () => {
15
+ it("returns empty profile when no file exists", async () => {
16
+ const profile = await manager.load();
17
+ expect(profile.summary).toBe("");
18
+ expect(profile.coreTags).toEqual([]);
19
+ expect(profile.tags).toEqual({});
20
+ });
21
+ it("loads existing profile from file", async () => {
22
+ const profileDir = join(tempDir, "memory", "profile");
23
+ await fs.mkdir(profileDir, { recursive: true });
24
+ const saved = { ...EMPTY_PROFILE, summary: "test summary", coreTags: ["tag1"] };
25
+ await fs.writeFile(join(profileDir, "semantic_profile.json"), JSON.stringify(saved));
26
+ // 创建新的 manager 实例测试从文件加载
27
+ const manager2 = new ProfileManager(tempDir);
28
+ const loaded = await manager2.load();
29
+ expect(loaded.summary).toBe("test summary");
30
+ expect(loaded.coreTags).toEqual(["tag1"]);
31
+ });
32
+ });
33
+ describe("addTag", () => {
34
+ it("adds a new tag to a new dimension", async () => {
35
+ const profile = { ...EMPTY_PROFILE };
36
+ const result = manager.addTag(profile, "口味偏好", "喜欢辣");
37
+ expect(result.tags["口味偏好"]).toHaveLength(1);
38
+ expect(result.tags["口味偏好"][0].value).toBe("喜欢辣");
39
+ expect(result.tags["口味偏好"][0].confidence).toBe(0.7);
40
+ });
41
+ it("increases confidence for existing tag", async () => {
42
+ const profile = {
43
+ ...EMPTY_PROFILE,
44
+ tags: { 口味偏好: [{ value: "喜欢辣", confidence: 0.5, lastSeen: "2026-01-01" }] },
45
+ };
46
+ const result = manager.addTag(profile, "口味偏好", "喜欢辣");
47
+ expect(result.tags["口味偏好"]).toHaveLength(1);
48
+ expect(result.tags["口味偏好"][0].confidence).toBeCloseTo(0.6);
49
+ });
50
+ it("adds multiple tags to same dimension", async () => {
51
+ const profile = { ...EMPTY_PROFILE };
52
+ manager.addTag(profile, "口味偏好", "喜欢辣");
53
+ manager.addTag(profile, "口味偏好", "不吃香菜");
54
+ expect(profile.tags["口味偏好"]).toHaveLength(2);
55
+ });
56
+ });
57
+ describe("decayTags", () => {
58
+ it("reduces confidence of all tags", () => {
59
+ const profile = {
60
+ ...EMPTY_PROFILE,
61
+ tags: {
62
+ 口味偏好: [
63
+ { value: "喜欢辣", confidence: 0.9, lastSeen: "2026-03-17" },
64
+ ],
65
+ },
66
+ };
67
+ const result = manager.decayTags(profile, 0.5);
68
+ expect(result.tags["口味偏好"][0].confidence).toBeCloseTo(0.45);
69
+ });
70
+ it("removes tags below threshold", () => {
71
+ const profile = {
72
+ ...EMPTY_PROFILE,
73
+ tags: {
74
+ 过时: [{ value: "旧标签", confidence: 0.15, lastSeen: "2026-01-01" }],
75
+ },
76
+ };
77
+ const result = manager.decayTags(profile, 1.0);
78
+ expect(result.tags["过时"]).toBeUndefined();
79
+ });
80
+ it("removes empty dimensions after filtering", () => {
81
+ const profile = {
82
+ ...EMPTY_PROFILE,
83
+ tags: {
84
+ 空维度: [{ value: "很低", confidence: 0.1, lastSeen: "2026-01-01" }],
85
+ },
86
+ };
87
+ const result = manager.decayTags(profile, 1.0);
88
+ expect("空维度" in result.tags).toBe(false);
89
+ });
90
+ });
91
+ describe("getRecallContext", () => {
92
+ it("returns empty string for empty profile", () => {
93
+ const ctx = manager.getRecallContext(EMPTY_PROFILE);
94
+ expect(ctx).toBe("");
95
+ });
96
+ it("returns summary and core tags", () => {
97
+ const profile = { ...EMPTY_PROFILE, summary: "辣味中餐爱好者", coreTags: ["辣味中餐", "天河"] };
98
+ const ctx = manager.getRecallContext(profile);
99
+ expect(ctx).toContain("辣味中餐爱好者");
100
+ expect(ctx).toContain("辣味中餐");
101
+ expect(ctx).toContain("天河");
102
+ });
103
+ it("works with only core tags", () => {
104
+ const profile = { ...EMPTY_PROFILE, summary: "", coreTags: ["标签1"] };
105
+ const ctx = manager.getRecallContext(profile);
106
+ expect(ctx).toContain("标签1");
107
+ });
108
+ });
109
+ describe("save", () => {
110
+ it("saves profile to file", async () => {
111
+ const profile = { ...EMPTY_PROFILE, summary: "test" };
112
+ await manager.save(profile);
113
+ const raw = await fs.readFile(join(tempDir, "memory", "profile", "semantic_profile.json"), "utf-8");
114
+ const loaded = JSON.parse(raw);
115
+ expect(loaded.summary).toBe("test");
116
+ });
117
+ it("updates updatedAt on save", async () => {
118
+ const profile = { ...EMPTY_PROFILE };
119
+ const before = new Date();
120
+ await manager.save(profile);
121
+ const after = new Date();
122
+ const raw = await fs.readFile(join(tempDir, "memory", "profile", "semantic_profile.json"), "utf-8");
123
+ const loaded = JSON.parse(raw);
124
+ const updated = new Date(loaded.updatedAt);
125
+ expect(updated.getTime()).toBeGreaterThanOrEqual(before.getTime());
126
+ expect(updated.getTime()).toBeLessThanOrEqual(after.getTime());
127
+ });
128
+ });
129
+ });
130
+ //# sourceMappingURL=profile.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.test.js","sourceRoot":"","sources":["../../src/__tests__/profile.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect } from "vitest";
2
+ // 纯函数测试,不依赖外部模块
3
+ describe("Recall helpers", () => {
4
+ describe("temporalDecay", () => {
5
+ function temporalDecay(score, ageInDays, halfLifeDays) {
6
+ if (ageInDays <= 0)
7
+ return score;
8
+ const lambda = Math.log(2) / halfLifeDays;
9
+ return score * Math.exp(-lambda * ageInDays);
10
+ }
11
+ it("returns original score for 0 days", () => {
12
+ expect(temporalDecay(1.0, 0, 30)).toBeCloseTo(1.0);
13
+ });
14
+ it("returns ~50% at half-life", () => {
15
+ expect(temporalDecay(1.0, 30, 30)).toBeCloseTo(0.5, 1);
16
+ });
17
+ it("returns ~84% at 7 days with 30-day half-life", () => {
18
+ expect(temporalDecay(1.0, 7, 30)).toBeCloseTo(0.846, 2);
19
+ });
20
+ it("returns ~12.5% at 90 days with 30-day half-life", () => {
21
+ expect(temporalDecay(1.0, 90, 30)).toBeCloseTo(0.125, 2);
22
+ });
23
+ it("shorter half-life decays faster", () => {
24
+ const short = temporalDecay(1.0, 7, 7);
25
+ const long = temporalDecay(1.0, 7, 30);
26
+ expect(short).toBeLessThan(long);
27
+ });
28
+ });
29
+ describe("jaccard", () => {
30
+ function jaccard(a, b) {
31
+ const setA = new Set(a.split(""));
32
+ const setB = new Set(b.split(""));
33
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
34
+ const union = new Set([...setA, ...setB]);
35
+ return union.size === 0 ? 0 : intersection.size / union.size;
36
+ }
37
+ it("returns 1.0 for identical strings", () => {
38
+ expect(jaccard("abc", "abc")).toBeCloseTo(1.0);
39
+ });
40
+ it("returns 0.0 for completely different strings", () => {
41
+ expect(jaccard("abc", "xyz")).toBeCloseTo(0.0);
42
+ });
43
+ it("returns partial similarity for overlapping strings", () => {
44
+ const sim = jaccard("abc", "abd");
45
+ expect(sim).toBeGreaterThan(0);
46
+ expect(sim).toBeLessThan(1);
47
+ });
48
+ it("handles empty strings", () => {
49
+ expect(jaccard("", "")).toBe(0);
50
+ expect(jaccard("abc", "")).toBe(0);
51
+ });
52
+ });
53
+ describe("MMR rerank", () => {
54
+ function mmrRerank(candidates, lambda = 0.7) {
55
+ if (candidates.length <= 1)
56
+ return candidates;
57
+ const jaccard = (a, b) => {
58
+ const setA = new Set(a.split(""));
59
+ const setB = new Set(b.split(""));
60
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
61
+ const union = new Set([...setA, ...setB]);
62
+ return union.size === 0 ? 0 : intersection.size / union.size;
63
+ };
64
+ const selected = [];
65
+ const remaining = [...candidates];
66
+ remaining.sort((a, b) => b.finalScore - a.finalScore);
67
+ selected.push(remaining.shift());
68
+ while (remaining.length > 0) {
69
+ let bestIdx = -1;
70
+ let bestMmr = -Infinity;
71
+ for (let i = 0; i < remaining.length; i++) {
72
+ const relevance = remaining[i].finalScore;
73
+ const maxSim = Math.max(...selected.map(s => jaccard(remaining[i].text, s.text)));
74
+ const mmrScore = lambda * relevance - (1 - lambda) * maxSim;
75
+ if (mmrScore > bestMmr) {
76
+ bestMmr = mmrScore;
77
+ bestIdx = i;
78
+ }
79
+ }
80
+ if (bestIdx >= 0) {
81
+ selected.push(remaining.splice(bestIdx, 1)[0]);
82
+ }
83
+ else
84
+ break;
85
+ }
86
+ return selected;
87
+ }
88
+ it("returns single item as-is", () => {
89
+ const candidates = [{ text: "hello", finalScore: 0.9 }];
90
+ expect(mmrRerank(candidates)).toHaveLength(1);
91
+ });
92
+ it("returns empty for empty input", () => {
93
+ expect(mmrRerank([])).toHaveLength(0);
94
+ });
95
+ it("promotes diverse results over duplicates", () => {
96
+ const candidates = [
97
+ { text: "在体育西吃了潮汕牛肉火锅", finalScore: 0.92 },
98
+ { text: "在天河吃了潮汕牛肉火锅", finalScore: 0.89 },
99
+ { text: "在越秀吃了日本料理", finalScore: 0.70 },
100
+ ];
101
+ const reranked = mmrRerank(candidates, 0.7);
102
+ expect(reranked).toHaveLength(3);
103
+ // 第一条应该还是分数最高的
104
+ expect(reranked[0].finalScore).toBe(0.92);
105
+ });
106
+ it("lambda=1.0 is pure relevance (no diversity)", () => {
107
+ const candidates = [
108
+ { text: "aaa", finalScore: 0.9 },
109
+ { text: "aaa", finalScore: 0.8 },
110
+ { text: "bbb", finalScore: 0.5 },
111
+ ];
112
+ const reranked = mmrRerank(candidates, 1.0);
113
+ expect(reranked.map(r => r.finalScore)).toEqual([0.9, 0.8, 0.5]);
114
+ });
115
+ });
116
+ describe("dedup", () => {
117
+ function jaccard(a, b) {
118
+ const setA = new Set(a.split(""));
119
+ const setB = new Set(b.split(""));
120
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
121
+ const union = new Set([...setA, ...setB]);
122
+ return union.size === 0 ? 0 : intersection.size / union.size;
123
+ }
124
+ function dedup(results) {
125
+ const sorted = [...results].sort((a, b) => b.score - a.score);
126
+ const unique = [];
127
+ for (const item of sorted) {
128
+ const isDup = unique.some(u => jaccard(item.text, u.text) > 0.7);
129
+ if (!isDup)
130
+ unique.push(item);
131
+ }
132
+ return unique;
133
+ }
134
+ it("removes near-duplicates", () => {
135
+ const results = [
136
+ { text: "在体育西吃了潮汕牛肉火锅", score: 0.92 },
137
+ { text: "在体育西吃潮汕牛肉火锅", score: 0.85 },
138
+ { text: "在天河城看到特斯拉展厅", score: 0.70 },
139
+ ];
140
+ const deduped = dedup(results);
141
+ expect(deduped.length).toBeLessThan(3);
142
+ });
143
+ it("keeps distinct results", () => {
144
+ const results = [
145
+ { text: "今天天气很好", score: 0.9 },
146
+ { text: "股票涨了", score: 0.8 },
147
+ { text: "新开了家餐厅", score: 0.7 },
148
+ ];
149
+ expect(dedup(results)).toHaveLength(3);
150
+ });
151
+ it("keeps higher-scored duplicate", () => {
152
+ const results = [
153
+ { text: "abcde fghij", score: 0.9 },
154
+ { text: "abcde fghij", score: 0.7 },
155
+ ];
156
+ const deduped = dedup(results);
157
+ expect(deduped).toHaveLength(1);
158
+ expect(deduped[0].score).toBe(0.9);
159
+ });
160
+ });
161
+ });
162
+ //# sourceMappingURL=recall.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall.test.js","sourceRoot":"","sources":["../../src/__tests__/recall.test.ts"],"names":[],"mappings":"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"}
package/dist/bm25.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Memory System Plugin - BM25 Short-term Memory Index
3
+ *
4
+ * 短期记忆的 BM25 索引构建与搜索
5
+ * 纯 TypeScript 实现,不依赖外部 BM25 库
6
+ */
7
+ export type ShortTermEntry = {
8
+ text: string;
9
+ date: string;
10
+ category: string;
11
+ filePath: string;
12
+ };
13
+ export declare class BM25Index {
14
+ private docs;
15
+ private idf;
16
+ private avgDl;
17
+ /**
18
+ * 从短期记忆文件构建索引
19
+ */
20
+ buildFromDirectory(dir: string, maxAgeDays?: number): Promise<void>;
21
+ /**
22
+ * 解析结构化的短期记忆文件
23
+ * 格式:### HH:MM [分类标签]\n摘要内容
24
+ */
25
+ private parseEntries;
26
+ /**
27
+ * 构建 IDF(逆文档频率)
28
+ */
29
+ private buildIDF;
30
+ /**
31
+ * BM25 搜索
32
+ * @returns 带分数的结果列表
33
+ */
34
+ search(query: string, topK?: number): Array<{
35
+ entry: ShortTermEntry;
36
+ score: number;
37
+ }>;
38
+ /**
39
+ * 添加单条记录(用于实时写入后的即时索引)
40
+ */
41
+ addEntry(entry: ShortTermEntry): void;
42
+ get size(): number;
43
+ }