@scotthuang/engram 0.2.7 → 0.2.9

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.
@@ -41,7 +41,7 @@
41
41
  "settleModel": {
42
42
  "type": "string",
43
43
  "description": "沉淀使用的 LLM 模型",
44
- "default": "volcengine-plan/ark-code-latest"
44
+ "default": "doubao-seed-2.0-code"
45
45
  },
46
46
  "embedding": {
47
47
  "type": "object",
@@ -68,6 +68,31 @@
68
68
  },
69
69
  "required": ["apiKey", "baseUrl"]
70
70
  },
71
+ "condense": {
72
+ "type": "object",
73
+ "description": "精简 LLM API 配置(字节 Coding Plan)",
74
+ "properties": {
75
+ "apiKey": {
76
+ "type": "string",
77
+ "description": "字节 API Key(ARK_API_KEY)"
78
+ },
79
+ "baseUrl": {
80
+ "type": "string",
81
+ "description": "字节 Coding Plan API URL",
82
+ "default": "https://ark.cn-beijing.volces.com/api/coding/v3"
83
+ },
84
+ "model": {
85
+ "type": "string",
86
+ "description": "精简使用的模型",
87
+ "default": "doubao-seed-2.0-code"
88
+ }
89
+ }
90
+ },
91
+ "saveStagingFile": {
92
+ "type": "boolean",
93
+ "description": "是否保存中间文件(staging)供调试",
94
+ "default": true
95
+ },
71
96
  "lancedbDir": {
72
97
  "type": "string",
73
98
  "description": "LanceDB 数据库目录(默认 workspace/memory/lancedb)"
@@ -80,6 +105,7 @@
80
105
  "recallTopK": { "label": "召回条数", "placeholder": "3" },
81
106
  "minScore": { "label": "最低分数", "placeholder": "0.4" },
82
107
  "vectorWeight": { "label": "向量权重", "placeholder": "0.7" },
83
- "textWeight": { "label": "BM25权重", "placeholder": "0.3" }
108
+ "textWeight": { "label": "BM25权重", "placeholder": "0.3" },
109
+ "saveStagingFile": { "label": "保存调试文件", "placeholder": "true" }
84
110
  }
85
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scotthuang/engram",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "分层语义记忆系统 - OpenClaw Plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -36,6 +36,7 @@
36
36
  "license": "ISC",
37
37
  "files": [
38
38
  "dist",
39
+ "!dist/__tests__",
39
40
  "openclaw.plugin.json",
40
41
  "README.md"
41
42
  ]
@@ -1 +0,0 @@
1
- export {};
@@ -1,198 +0,0 @@
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", async () => {
7
- await index.addEntry({ text: "在体育西吃了潮汕牛肉火锅", date: "2026-03-15", category: "饮食", filePath: "/test" });
8
- await index.addEntry({ text: "讨论了记忆系统方案", date: "2026-03-15", category: "技术", filePath: "/test" });
9
- const results = await index.search("牛肉火锅", 2);
10
- expect(results).toHaveLength(1);
11
- expect(results[0].entry.category).toBe("饮食");
12
- });
13
- it("finds English word matches", async () => {
14
- await index.addEntry({ text: "installed node modules successfully", date: "2026-03-15", category: "技术", filePath: "/test" });
15
- await index.addEntry({ text: "天气很好适合出门", date: "2026-03-15", category: "随聊", filePath: "/test" });
16
- const results = await 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", async () => {
21
- await index.addEntry({ text: "今天天气很好", date: "2026-03-15", category: "随聊", filePath: "/test" });
22
- const results = await index.search("量子力学", 3);
23
- expect(results).toHaveLength(0);
24
- });
25
- it("tokenizes stock query correctly", async () => {
26
- const index = new BM25Index();
27
- // Add the daily memory entry that has the stock preference
28
- await index.addEntry({
29
- text: "查股票或画K线图时必须默认使用 akshare-stock skill(~/.openclaw/workspace/skills/akshare-stock/scripts/stock_kline.py),不知道股票代码时用百度搜索查询。",
30
- date: "2026-03-17",
31
- category: "偏好",
32
- filePath: "/test/2026-03-17.md"
33
- });
34
- await index.addEntry({
35
- text: "腾讯股票代码是 00700",
36
- date: "2026-03-17",
37
- category: "技术",
38
- filePath: "/test"
39
- });
40
- // Search for the user's query "你去画一条腾讯的K线图"
41
- const query = "你去画一条腾讯的K线图";
42
- const results = await index.search(query, 5);
43
- // Log for debugging
44
- console.log("\n----- BM25 检索日志 -----");
45
- console.log("Query:", query);
46
- // Log tokens and results - follow the same pattern as in bm25.ts
47
- const mod = await import("jieba-wasm");
48
- const jiebaInstance = await mod.default;
49
- // @ts-ignore
50
- console.log("Jieba 分词结果:", jiebaInstance.cut(query));
51
- console.log("召回结果(按得分降序):");
52
- results.forEach((r, i) => {
53
- console.log(`${i + 1}. [${r.entry.category}] score=${r.score.toFixed(4)} → ${r.entry.text}`);
54
- });
55
- console.log("------------------------\n");
56
- // Both entries should match
57
- expect(results.length).toBe(2);
58
- // The "腾讯股票代码" entry has exact "腾讯" match in a shorter doc → higher BM25 score
59
- // This is actually the correct behavior, since we're looking for a stock about Tencent
60
- expect(results[0].entry.category).toBe("技术");
61
- expect(results[1].entry.category).toBe("偏好");
62
- // Both entries contain matching tokens:
63
- // Query tokens: 你 去 画 一条 腾讯 的 K线 图 → jieba tokenizes correctly
64
- // Preference entry matches: 画, K线, 股票 → relevant to the query intent
65
- });
66
- });
67
- describe("search", () => {
68
- it("returns results sorted by score", async () => {
69
- const index = new BM25Index();
70
- await index.addEntry({ text: "牛肉火锅牛肉火锅牛肉火锅", date: "2026-03-15", category: "饮食", filePath: "/test" });
71
- await index.addEntry({ text: "吃过一次牛肉火锅", date: "2026-03-15", category: "饮食", filePath: "/test" });
72
- await index.addEntry({ text: "今天去跑步了", date: "2026-03-15", category: "健康", filePath: "/test" });
73
- const results = await index.search("牛肉火锅", 3);
74
- expect(results.length).toBeGreaterThanOrEqual(2);
75
- // 更高频率的应排在前面
76
- expect(results[0].score).toBeGreaterThanOrEqual(results[1].score);
77
- });
78
- it("respects topK limit", async () => {
79
- const index = new BM25Index();
80
- for (let i = 0; i < 10; i++) {
81
- await index.addEntry({ text: `测试条目${i} 包含关键词`, date: "2026-03-15", category: "测试", filePath: "/test" });
82
- }
83
- const results = await index.search("关键词", 3);
84
- expect(results.length).toBeLessThanOrEqual(3);
85
- });
86
- });
87
- describe("addEntry", () => {
88
- it("updates index size", async () => {
89
- const index = new BM25Index();
90
- expect(index.size).toBe(0);
91
- await index.addEntry({ text: "test entry", date: "2026-03-15", category: "测试", filePath: "/test" });
92
- expect(index.size).toBe(1);
93
- await index.addEntry({ text: "another entry", date: "2026-03-15", category: "测试", filePath: "/test" });
94
- expect(index.size).toBe(2);
95
- });
96
- });
97
- describe("parseEntries", () => {
98
- it("parses structured format correctly", async () => {
99
- const index = new BM25Index();
100
- // buildFromDirectory 会调用 parseEntries
101
- // 直接测试 search 验证解析结果
102
- const mockContent = `# 2026-03-15
103
-
104
- ### 19:30 [饮食]
105
- 在体育西吃了潮汕牛肉火锅,胸口捞很好吃
106
-
107
- ### 14:00 [技术]
108
- 讨论了记忆系统方案,确定三层架构
109
-
110
- ### 10:00 [随聊]
111
- 聊了股票
112
- `;
113
- // 使用 addEntry 模拟解析后的结果
114
- await index.addEntry({ text: "在体育西吃了潮汕牛肉火锅,胸口捞很好吃", date: "2026-03-15", category: "饮食", filePath: "/test" });
115
- await index.addEntry({ text: "讨论了记忆系统方案,确定三层架构", date: "2026-03-15", category: "技术", filePath: "/test" });
116
- // 搜饮食
117
- const foodResults = await index.search("牛肉火锅", 1);
118
- expect(foodResults).toHaveLength(1);
119
- expect(foodResults[0].entry.category).toBe("饮食");
120
- // 搜技术
121
- const techResults = await index.search("记忆系统", 1);
122
- expect(techResults).toHaveLength(1);
123
- expect(techResults[0].entry.category).toBe("技术");
124
- });
125
- });
126
- describe("buildFromTestFile", () => {
127
- it("builds index from test/2026-03-17.md and tests search", async () => {
128
- const index = new BM25Index();
129
- // Read the test file
130
- const fs = require('fs');
131
- const path = require('path');
132
- const filePath = path.join(__dirname, '../../test/2026-03-17.md');
133
- const content = fs.readFileSync(filePath, 'utf8');
134
- // Parse all entries from the file
135
- const lines = content.split('\n');
136
- let currentEntry = null;
137
- for (const line of lines) {
138
- const headerMatch = line.match(/^###\s+\d{2}:\d{2}\s+\[([^\]]+)\]/);
139
- if (headerMatch) {
140
- // Save previous entry if exists
141
- if (currentEntry && currentEntry.text.trim().length > 0) {
142
- await index.addEntry({
143
- text: currentEntry.text.trim(),
144
- category: currentEntry.category,
145
- date: "2026-03-17",
146
- filePath,
147
- });
148
- }
149
- // Start new entry
150
- currentEntry = {
151
- category: headerMatch[1],
152
- date: "2026-03-17",
153
- text: "",
154
- };
155
- }
156
- else if (currentEntry) {
157
- currentEntry.text += line + '\n';
158
- }
159
- }
160
- // Add the last entry
161
- if (currentEntry && currentEntry.text.trim().length > 0) {
162
- await index.addEntry({
163
- text: currentEntry.text.trim(),
164
- category: currentEntry.category,
165
- date: "2026-03-17",
166
- filePath,
167
- });
168
- }
169
- console.log("\n====== Test from file 2026-03-17.md ======");
170
- console.log(`Total entries parsed: ${index.size}`);
171
- console.log("==========================================\n");
172
- // Test search for "bm25 检索"
173
- const query1 = "bm25 检索 分词";
174
- const results1 = await index.search(query1, 5);
175
- console.log("\n----- Test search: 'bm25 检索 分词' -----");
176
- console.log(`Found ${results1.length} results:`);
177
- results1.forEach((r, i) => {
178
- const preview = r.entry.text.length > 60 ? r.entry.text.substring(0, 60) + "..." : r.entry.text;
179
- console.log(`${i + 1}. [${r.entry.category}] score=${r.score.toFixed(4)} → ${preview}`);
180
- });
181
- console.log("------------------------\n");
182
- // Should find entries about BM25
183
- expect(results1.length).toBeGreaterThan(0);
184
- // Test search for "腾讯股票 K线"
185
- const query2 = "腾讯股票 K线";
186
- const results2 = await index.search(query2, 5);
187
- console.log("\n----- Test search: '腾讯股票 K线' -----");
188
- console.log(`Found ${results2.length} results:`);
189
- results2.forEach((r, i) => {
190
- const preview = r.entry.text.length > 60 ? r.entry.text.substring(0, 60) + "..." : r.entry.text;
191
- console.log(`${i + 1}. [${r.entry.category}] score=${r.score.toFixed(4)} → ${preview}`);
192
- });
193
- console.log("------------------------\n");
194
- expect(results2.length).toBeGreaterThan(0);
195
- });
196
- });
197
- });
198
- //# sourceMappingURL=bm25.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bm25.test.js","sourceRoot":"","sources":["../../src/__tests__/bm25.test.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- export {};
@@ -1,31 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
1
- export {};
@@ -1,130 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"profile.test.js","sourceRoot":"","sources":["../../src/__tests__/profile.test.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- export {};
@@ -1,162 +0,0 @@
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