@aipack-ai/memory 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/index.d.ts +836 -0
- package/dist/index.js +1839 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1839 @@
|
|
|
1
|
+
// src/store/file-memory-store.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
// src/retrieval/tokenizer.ts
|
|
7
|
+
function isCJK(ch) {
|
|
8
|
+
if (!ch) return false;
|
|
9
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
10
|
+
return code >= 19968 && code <= 40959 || // CJK 统一表意文字
|
|
11
|
+
code >= 13312 && code <= 19903 || // CJK 扩展 A
|
|
12
|
+
code >= 63744 && code <= 64255 || // CJK 兼容表意
|
|
13
|
+
code >= 12352 && code <= 12543 || // 日文假名(平假名 + 片假名)
|
|
14
|
+
code >= 44032 && code <= 55215;
|
|
15
|
+
}
|
|
16
|
+
function isCJKSymbol(ch) {
|
|
17
|
+
const code = ch.charCodeAt(0);
|
|
18
|
+
return code >= 12288 && code <= 12351 || code >= 65280 && code <= 65519;
|
|
19
|
+
}
|
|
20
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
21
|
+
// 英文
|
|
22
|
+
"the",
|
|
23
|
+
"a",
|
|
24
|
+
"an",
|
|
25
|
+
"and",
|
|
26
|
+
"or",
|
|
27
|
+
"but",
|
|
28
|
+
"if",
|
|
29
|
+
"then",
|
|
30
|
+
"else",
|
|
31
|
+
"when",
|
|
32
|
+
"of",
|
|
33
|
+
"to",
|
|
34
|
+
"in",
|
|
35
|
+
"on",
|
|
36
|
+
"at",
|
|
37
|
+
"by",
|
|
38
|
+
"for",
|
|
39
|
+
"with",
|
|
40
|
+
"without",
|
|
41
|
+
"is",
|
|
42
|
+
"are",
|
|
43
|
+
"was",
|
|
44
|
+
"were",
|
|
45
|
+
"be",
|
|
46
|
+
"been",
|
|
47
|
+
"being",
|
|
48
|
+
"have",
|
|
49
|
+
"has",
|
|
50
|
+
"had",
|
|
51
|
+
"do",
|
|
52
|
+
"does",
|
|
53
|
+
"did",
|
|
54
|
+
"will",
|
|
55
|
+
"would",
|
|
56
|
+
"can",
|
|
57
|
+
"could",
|
|
58
|
+
"should",
|
|
59
|
+
"shall",
|
|
60
|
+
"may",
|
|
61
|
+
"might",
|
|
62
|
+
"must",
|
|
63
|
+
"i",
|
|
64
|
+
"you",
|
|
65
|
+
"he",
|
|
66
|
+
"she",
|
|
67
|
+
"it",
|
|
68
|
+
"we",
|
|
69
|
+
"they",
|
|
70
|
+
"this",
|
|
71
|
+
"that",
|
|
72
|
+
"these",
|
|
73
|
+
"those",
|
|
74
|
+
"my",
|
|
75
|
+
"your",
|
|
76
|
+
"his",
|
|
77
|
+
"her",
|
|
78
|
+
"its",
|
|
79
|
+
"our",
|
|
80
|
+
"their",
|
|
81
|
+
"me",
|
|
82
|
+
"him",
|
|
83
|
+
"us",
|
|
84
|
+
"them",
|
|
85
|
+
"as",
|
|
86
|
+
"from",
|
|
87
|
+
"so",
|
|
88
|
+
"not",
|
|
89
|
+
"no",
|
|
90
|
+
"yes",
|
|
91
|
+
// 中文高频虚词
|
|
92
|
+
"\u7684",
|
|
93
|
+
"\u4E86",
|
|
94
|
+
"\u662F",
|
|
95
|
+
"\u5728",
|
|
96
|
+
"\u6211",
|
|
97
|
+
"\u4F60",
|
|
98
|
+
"\u4ED6",
|
|
99
|
+
"\u5979",
|
|
100
|
+
"\u5B83",
|
|
101
|
+
"\u4EEC",
|
|
102
|
+
"\u8FD9",
|
|
103
|
+
"\u90A3",
|
|
104
|
+
"\u548C",
|
|
105
|
+
"\u4E0E",
|
|
106
|
+
"\u6216",
|
|
107
|
+
"\u4E5F",
|
|
108
|
+
"\u90FD",
|
|
109
|
+
"\u5C31",
|
|
110
|
+
"\u628A",
|
|
111
|
+
"\u88AB",
|
|
112
|
+
"\u8BA9",
|
|
113
|
+
"\u7ED9",
|
|
114
|
+
"\u5BF9",
|
|
115
|
+
"\u4E3A",
|
|
116
|
+
"\u4ECE",
|
|
117
|
+
"\u5230",
|
|
118
|
+
"\u5411",
|
|
119
|
+
"\u7740",
|
|
120
|
+
"\u8FC7",
|
|
121
|
+
"\u5427",
|
|
122
|
+
"\u5417",
|
|
123
|
+
"\u5462",
|
|
124
|
+
"\u554A",
|
|
125
|
+
"\u54E6",
|
|
126
|
+
"\u55EF",
|
|
127
|
+
"\u4E2A",
|
|
128
|
+
"\u4E9B",
|
|
129
|
+
"\u4E0A",
|
|
130
|
+
"\u4E0B",
|
|
131
|
+
"\u91CC",
|
|
132
|
+
"\u4E2D"
|
|
133
|
+
]);
|
|
134
|
+
function tokenize(text) {
|
|
135
|
+
if (!text) return [];
|
|
136
|
+
const tokens = [];
|
|
137
|
+
let latinBuf = "";
|
|
138
|
+
let cjkBuf = "";
|
|
139
|
+
const flushLatin = () => {
|
|
140
|
+
if (latinBuf) {
|
|
141
|
+
const parts = latinBuf.toLowerCase().match(/[a-z0-9]+/g);
|
|
142
|
+
if (parts) tokens.push(...parts);
|
|
143
|
+
latinBuf = "";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const flushCJK = () => {
|
|
147
|
+
if (!cjkBuf) return;
|
|
148
|
+
const chars = [...cjkBuf];
|
|
149
|
+
if (chars.length === 1) {
|
|
150
|
+
tokens.push(chars[0]);
|
|
151
|
+
} else {
|
|
152
|
+
for (let i = 0; i + 1 < chars.length; i++) {
|
|
153
|
+
tokens.push(chars[i] + chars[i + 1]);
|
|
154
|
+
}
|
|
155
|
+
if (chars.length % 2 === 1) {
|
|
156
|
+
tokens.push(chars[chars.length - 1]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
cjkBuf = "";
|
|
160
|
+
};
|
|
161
|
+
for (const ch of text) {
|
|
162
|
+
if (isCJK(ch)) {
|
|
163
|
+
flushLatin();
|
|
164
|
+
cjkBuf += ch;
|
|
165
|
+
} else if (isCJKSymbol(ch)) {
|
|
166
|
+
flushLatin();
|
|
167
|
+
flushCJK();
|
|
168
|
+
} else {
|
|
169
|
+
flushCJK();
|
|
170
|
+
latinBuf += ch;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
flushLatin();
|
|
174
|
+
flushCJK();
|
|
175
|
+
return tokens;
|
|
176
|
+
}
|
|
177
|
+
function extractConcepts(text, maxConcepts = 8) {
|
|
178
|
+
const tokens = tokenize(text);
|
|
179
|
+
const freq = /* @__PURE__ */ new Map();
|
|
180
|
+
for (const t of tokens) {
|
|
181
|
+
if (STOPWORDS.has(t)) continue;
|
|
182
|
+
if (t.length < 2 && !isCJK(t)) continue;
|
|
183
|
+
freq.set(t, (freq.get(t) ?? 0) + 1);
|
|
184
|
+
}
|
|
185
|
+
return [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, maxConcepts).map(([t]) => t);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/retrieval/bm25.ts
|
|
189
|
+
var BM25Index = class {
|
|
190
|
+
k1;
|
|
191
|
+
b;
|
|
192
|
+
/** 文档集合 */
|
|
193
|
+
docs = /* @__PURE__ */ new Map();
|
|
194
|
+
/** 倒排表:token -> 文档 id 集合 */
|
|
195
|
+
inverted = /* @__PURE__ */ new Map();
|
|
196
|
+
totalLength = 0;
|
|
197
|
+
constructor(options = {}) {
|
|
198
|
+
this.k1 = options.k1 ?? 1.5;
|
|
199
|
+
this.b = options.b ?? 0.75;
|
|
200
|
+
}
|
|
201
|
+
/** 平均文档长度 */
|
|
202
|
+
get avgdl() {
|
|
203
|
+
return this.docs.size === 0 ? 0 : this.totalLength / this.docs.size;
|
|
204
|
+
}
|
|
205
|
+
/** 文档总数 */
|
|
206
|
+
get size() {
|
|
207
|
+
return this.docs.size;
|
|
208
|
+
}
|
|
209
|
+
/** 添加或替换文档(同 id 覆盖) */
|
|
210
|
+
add(id, tokens) {
|
|
211
|
+
if (this.docs.has(id)) this.remove(id);
|
|
212
|
+
const tf = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const t of tokens) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
214
|
+
this.docs.set(id, { id, tokens, length: tokens.length, tf });
|
|
215
|
+
this.totalLength += tokens.length;
|
|
216
|
+
for (const t of tf.keys()) {
|
|
217
|
+
let set = this.inverted.get(t);
|
|
218
|
+
if (!set) {
|
|
219
|
+
set = /* @__PURE__ */ new Set();
|
|
220
|
+
this.inverted.set(t, set);
|
|
221
|
+
}
|
|
222
|
+
set.add(id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** 移除文档 */
|
|
226
|
+
remove(id) {
|
|
227
|
+
const doc = this.docs.get(id);
|
|
228
|
+
if (!doc) return;
|
|
229
|
+
this.docs.delete(id);
|
|
230
|
+
this.totalLength -= doc.length;
|
|
231
|
+
for (const t of doc.tf.keys()) {
|
|
232
|
+
const set = this.inverted.get(t);
|
|
233
|
+
if (!set) continue;
|
|
234
|
+
set.delete(id);
|
|
235
|
+
if (set.size === 0) this.inverted.delete(t);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** 清空索引 */
|
|
239
|
+
clear() {
|
|
240
|
+
this.docs.clear();
|
|
241
|
+
this.inverted.clear();
|
|
242
|
+
this.totalLength = 0;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 检索:返回 top-N 的 {id, score}(按分数降序)。
|
|
246
|
+
* 只扫描 query token 命中的文档,避免全量计算。
|
|
247
|
+
*/
|
|
248
|
+
search(queryTokens, limit = 10) {
|
|
249
|
+
if (this.docs.size === 0 || queryTokens.length === 0) return [];
|
|
250
|
+
const N = this.docs.size;
|
|
251
|
+
const avgdl = this.avgdl;
|
|
252
|
+
const scores = /* @__PURE__ */ new Map();
|
|
253
|
+
for (const t of queryTokens) {
|
|
254
|
+
const set = this.inverted.get(t);
|
|
255
|
+
if (!set || set.size === 0) continue;
|
|
256
|
+
const df = set.size;
|
|
257
|
+
const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
|
|
258
|
+
for (const docId of set) {
|
|
259
|
+
const doc = this.docs.get(docId);
|
|
260
|
+
if (!doc) continue;
|
|
261
|
+
const tf = doc.tf.get(t) ?? 0;
|
|
262
|
+
if (tf === 0) continue;
|
|
263
|
+
const denom = tf + this.k1 * (1 - this.b + this.b * doc.length / (avgdl || 1));
|
|
264
|
+
const contrib = idf * (tf * (this.k1 + 1)) / (denom || 1);
|
|
265
|
+
scores.set(docId, (scores.get(docId) ?? 0) + contrib);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return [...scores.entries()].map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 单个 token 的 idf(未出现返回 0)。
|
|
272
|
+
* 供上层计算查询的理论满分(完全同文、tf=1、len≈avgdl 时的分数),
|
|
273
|
+
* 把无界 BM25 原始分规范化为 [0,1] 相似度。
|
|
274
|
+
*/
|
|
275
|
+
idf(token) {
|
|
276
|
+
const df = this.inverted.get(token)?.size ?? 0;
|
|
277
|
+
if (df === 0) return 0;
|
|
278
|
+
const N = this.docs.size;
|
|
279
|
+
return Math.log((N - df + 0.5) / (df + 0.5) + 1);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
var BM25Retriever = class {
|
|
283
|
+
constructor(index, entries) {
|
|
284
|
+
this.index = index;
|
|
285
|
+
this.entries = entries;
|
|
286
|
+
}
|
|
287
|
+
index;
|
|
288
|
+
entries;
|
|
289
|
+
async search(query, limit = 5) {
|
|
290
|
+
const queryTokens = tokenize(query);
|
|
291
|
+
const hits = this.index.search(queryTokens, limit);
|
|
292
|
+
const maxPossible = queryTokens.reduce((acc, t) => acc + this.index.idf(t), 0);
|
|
293
|
+
return hits.map(({ id, score }) => ({
|
|
294
|
+
entry: this.entries.get(id),
|
|
295
|
+
score: maxPossible > 0 ? Math.min(1, score / maxPossible) : 0,
|
|
296
|
+
matchedBy: "bm25"
|
|
297
|
+
})).filter((r) => r.entry != null);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/retrieval/vector-index.ts
|
|
302
|
+
function normOf(v) {
|
|
303
|
+
let s = 0;
|
|
304
|
+
for (let i = 0; i < v.length; i++) s += v[i] * v[i];
|
|
305
|
+
return Math.sqrt(s);
|
|
306
|
+
}
|
|
307
|
+
function dot(a, b) {
|
|
308
|
+
let s = 0;
|
|
309
|
+
for (let i = 0; i < a.length; i++) s += a[i] * b[i];
|
|
310
|
+
return s;
|
|
311
|
+
}
|
|
312
|
+
var VectorIndex = class {
|
|
313
|
+
entries = /* @__PURE__ */ new Map();
|
|
314
|
+
/** bucket -> id 列表(仅 ivfBuckets > 0 时使用) */
|
|
315
|
+
buckets = /* @__PURE__ */ new Map();
|
|
316
|
+
dim = 0;
|
|
317
|
+
bucketCount;
|
|
318
|
+
constructor(options = {}) {
|
|
319
|
+
this.bucketCount = options.ivfBuckets ?? 0;
|
|
320
|
+
}
|
|
321
|
+
get size() {
|
|
322
|
+
return this.entries.size;
|
|
323
|
+
}
|
|
324
|
+
/** 添加或替换向量(同 id 覆盖)。维度不一致时忽略,返回是否生效 */
|
|
325
|
+
add(id, vector) {
|
|
326
|
+
if (!vector || vector.length === 0) return false;
|
|
327
|
+
if (this.dim === 0) this.dim = vector.length;
|
|
328
|
+
if (vector.length !== this.dim) return false;
|
|
329
|
+
const prev = this.entries.get(id);
|
|
330
|
+
if (prev && this.bucketCount > 0) this.removeFromBucket(id, prev.bucket);
|
|
331
|
+
const entry = {
|
|
332
|
+
id,
|
|
333
|
+
vec: vector,
|
|
334
|
+
norm: normOf(vector),
|
|
335
|
+
bucket: this.bucketOf(vector)
|
|
336
|
+
};
|
|
337
|
+
this.entries.set(id, entry);
|
|
338
|
+
if (this.bucketCount > 0) {
|
|
339
|
+
let list = this.buckets.get(entry.bucket);
|
|
340
|
+
if (!list) {
|
|
341
|
+
list = [];
|
|
342
|
+
this.buckets.set(entry.bucket, list);
|
|
343
|
+
}
|
|
344
|
+
if (!list.includes(id)) list.push(id);
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
remove(id) {
|
|
349
|
+
const e = this.entries.get(id);
|
|
350
|
+
if (!e) return false;
|
|
351
|
+
this.entries.delete(id);
|
|
352
|
+
if (this.bucketCount > 0) this.removeFromBucket(id, e.bucket);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
clear() {
|
|
356
|
+
this.entries.clear();
|
|
357
|
+
this.buckets.clear();
|
|
358
|
+
this.dim = 0;
|
|
359
|
+
}
|
|
360
|
+
/** 检索 top-k(cosine 降序,score > 0) */
|
|
361
|
+
search(query, k) {
|
|
362
|
+
if (this.entries.size === 0 || k <= 0 || !query || query.length === 0) return [];
|
|
363
|
+
if (query.length !== this.dim) return [];
|
|
364
|
+
const qNorm = normOf(query);
|
|
365
|
+
if (qNorm === 0) return [];
|
|
366
|
+
const pool = this.bucketCount > 0 ? this.probeCandidates(query, k * 4) : [...this.entries.values()];
|
|
367
|
+
const results = [];
|
|
368
|
+
for (const e of pool) {
|
|
369
|
+
const score = dot(query, e.vec) / (qNorm * e.norm || 1);
|
|
370
|
+
if (!(score > 0)) continue;
|
|
371
|
+
results.push({ id: e.id, score });
|
|
372
|
+
}
|
|
373
|
+
results.sort((a, b) => b.score - a.score);
|
|
374
|
+
return results.slice(0, k);
|
|
375
|
+
}
|
|
376
|
+
/** 主导维度(argmax |v|)分桶 */
|
|
377
|
+
bucketOf(vec) {
|
|
378
|
+
if (this.bucketCount <= 0) return 0;
|
|
379
|
+
let best = 0;
|
|
380
|
+
let bestAbs = -1;
|
|
381
|
+
for (let i = 0; i < vec.length; i++) {
|
|
382
|
+
const a = Math.abs(vec[i]);
|
|
383
|
+
if (a > bestAbs) {
|
|
384
|
+
bestAbs = a;
|
|
385
|
+
best = i;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return best % this.bucketCount;
|
|
389
|
+
}
|
|
390
|
+
/** 从查询桶开始探测邻近分桶,直到凑够 minCandidates */
|
|
391
|
+
probeCandidates(query, minCandidates) {
|
|
392
|
+
const qBucket = this.bucketOf(query);
|
|
393
|
+
const out = [];
|
|
394
|
+
for (let r = 0; r < this.bucketCount && out.length < minCandidates; r++) {
|
|
395
|
+
const list = this.buckets.get((qBucket + r) % this.bucketCount);
|
|
396
|
+
if (!list) continue;
|
|
397
|
+
for (const id of list) {
|
|
398
|
+
const e = this.entries.get(id);
|
|
399
|
+
if (e) out.push(e);
|
|
400
|
+
if (out.length >= minCandidates) break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
removeFromBucket(id, bucket) {
|
|
406
|
+
const list = this.buckets.get(bucket);
|
|
407
|
+
if (!list) return;
|
|
408
|
+
const i = list.indexOf(id);
|
|
409
|
+
if (i !== -1) list.splice(i, 1);
|
|
410
|
+
if (list.length === 0) this.buckets.delete(bucket);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/store/memory-index.ts
|
|
415
|
+
function byUpdatedAtDesc(a, b) {
|
|
416
|
+
return b.updatedAt - a.updatedAt;
|
|
417
|
+
}
|
|
418
|
+
var MemoryIndex = class {
|
|
419
|
+
entries = /* @__PURE__ */ new Map();
|
|
420
|
+
index = new BM25Index();
|
|
421
|
+
/** 独立向量索引:保证向量召回不被 BM25 候选池封顶 */
|
|
422
|
+
vectors = new VectorIndex();
|
|
423
|
+
retriever = new BM25Retriever(this.index, this.entries);
|
|
424
|
+
/** 共享底层索引与条目表,返回一个 BM25Retriever(与 store 实时同步) */
|
|
425
|
+
getRetriever() {
|
|
426
|
+
return this.retriever;
|
|
427
|
+
}
|
|
428
|
+
get(id) {
|
|
429
|
+
return this.entries.get(id) ?? null;
|
|
430
|
+
}
|
|
431
|
+
/** 全部条目(updatedAt 降序) */
|
|
432
|
+
all() {
|
|
433
|
+
return [...this.entries.values()].sort(byUpdatedAtDesc);
|
|
434
|
+
}
|
|
435
|
+
list(limit) {
|
|
436
|
+
const all = this.all();
|
|
437
|
+
return limit != null ? all.slice(0, limit) : all;
|
|
438
|
+
}
|
|
439
|
+
count() {
|
|
440
|
+
return this.entries.size;
|
|
441
|
+
}
|
|
442
|
+
/** 索引内容 = content + concepts(提升概念命中) */
|
|
443
|
+
add(entry) {
|
|
444
|
+
this.entries.set(entry.id, entry);
|
|
445
|
+
const tokens = tokenize(`${entry.content} ${entry.concepts.join(" ")}`);
|
|
446
|
+
this.index.add(entry.id, tokens);
|
|
447
|
+
if (entry.embedding && entry.embedding.length > 0) {
|
|
448
|
+
this.vectors.add(entry.id, entry.embedding);
|
|
449
|
+
} else {
|
|
450
|
+
this.vectors.remove(entry.id);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
remove(id) {
|
|
454
|
+
if (!this.entries.has(id)) return false;
|
|
455
|
+
this.index.remove(id);
|
|
456
|
+
this.vectors.remove(id);
|
|
457
|
+
this.entries.delete(id);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
clear() {
|
|
461
|
+
this.entries.clear();
|
|
462
|
+
this.index.clear();
|
|
463
|
+
this.vectors.clear();
|
|
464
|
+
}
|
|
465
|
+
async search(query, limit) {
|
|
466
|
+
return this.retriever.search(query, limit ?? 5);
|
|
467
|
+
}
|
|
468
|
+
async searchVectors(queryVec, limit) {
|
|
469
|
+
const hits = this.vectors.search(queryVec, limit ?? 5);
|
|
470
|
+
const results = [];
|
|
471
|
+
for (const { id, score } of hits) {
|
|
472
|
+
const entry = this.entries.get(id);
|
|
473
|
+
if (entry) results.push({ entry, score, matchedBy: "embedding" });
|
|
474
|
+
}
|
|
475
|
+
return results;
|
|
476
|
+
}
|
|
477
|
+
/** 统计快照 */
|
|
478
|
+
stats() {
|
|
479
|
+
const bySource = { capture: 0, tool: 0, consolidation: 0 };
|
|
480
|
+
let avgConfidence = 0;
|
|
481
|
+
let recallTotal = 0;
|
|
482
|
+
let lastRecalledAt;
|
|
483
|
+
for (const e of this.entries.values()) {
|
|
484
|
+
if (bySource[e.source] == null) bySource[e.source] = 0;
|
|
485
|
+
bySource[e.source] += 1;
|
|
486
|
+
avgConfidence += e.confidence;
|
|
487
|
+
recallTotal += e.recallCount;
|
|
488
|
+
if (e.lastRecalledAt != null && (lastRecalledAt == null || e.lastRecalledAt > lastRecalledAt)) {
|
|
489
|
+
lastRecalledAt = e.lastRecalledAt;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const count = this.entries.size;
|
|
493
|
+
return {
|
|
494
|
+
count,
|
|
495
|
+
embeddingCount: this.vectors.size,
|
|
496
|
+
bySource,
|
|
497
|
+
avgConfidence: count > 0 ? avgConfidence / count : 0,
|
|
498
|
+
recallTotal,
|
|
499
|
+
lastRecalledAt
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// src/utils/keyed-mutex.ts
|
|
505
|
+
var KeyedMutex = class {
|
|
506
|
+
/** key -> 当前链尾 promise */
|
|
507
|
+
tails = /* @__PURE__ */ new Map();
|
|
508
|
+
/**
|
|
509
|
+
* 在 key 上串行执行 fn:同一 key 的并发调用按调用顺序排队,逐个执行。
|
|
510
|
+
* 不同 key 之间互不阻塞(`withLock('*', ...)` 可作全局锁)。
|
|
511
|
+
*/
|
|
512
|
+
withLock(key, fn) {
|
|
513
|
+
const prev = this.tails.get(key) ?? Promise.resolve();
|
|
514
|
+
let release;
|
|
515
|
+
const gate = new Promise((resolve) => {
|
|
516
|
+
release = resolve;
|
|
517
|
+
});
|
|
518
|
+
const tail = prev.then(() => gate);
|
|
519
|
+
this.tails.set(key, tail);
|
|
520
|
+
return prev.then(async () => {
|
|
521
|
+
try {
|
|
522
|
+
return await fn();
|
|
523
|
+
} finally {
|
|
524
|
+
release();
|
|
525
|
+
if (this.tails.get(key) === tail) {
|
|
526
|
+
this.tails.delete(key);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
/** 当前持锁 key 数量(调试用) */
|
|
532
|
+
get pending() {
|
|
533
|
+
return this.tails.size;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
var globalMutex = new KeyedMutex();
|
|
537
|
+
|
|
538
|
+
// src/store/in-memory-store.ts
|
|
539
|
+
function genId() {
|
|
540
|
+
return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
541
|
+
}
|
|
542
|
+
function finalizeEntry(entry, now = Date.now()) {
|
|
543
|
+
const createdAt = entry.createdAt ?? now;
|
|
544
|
+
const { ttlMs, ...rest } = entry;
|
|
545
|
+
return {
|
|
546
|
+
...rest,
|
|
547
|
+
id: entry.id ?? genId(),
|
|
548
|
+
createdAt,
|
|
549
|
+
updatedAt: now,
|
|
550
|
+
recallCount: entry.recallCount ?? 0,
|
|
551
|
+
expiresAt: ttlMs != null ? createdAt + ttlMs : entry.expiresAt
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
var InMemoryStore = class {
|
|
555
|
+
idx = new MemoryIndex();
|
|
556
|
+
consolidator;
|
|
557
|
+
embedder;
|
|
558
|
+
onEvent;
|
|
559
|
+
writeLock = new KeyedMutex();
|
|
560
|
+
lastConsolidatedAt;
|
|
561
|
+
constructor(options = {}) {
|
|
562
|
+
this.embedder = options.embedder;
|
|
563
|
+
this.onEvent = options.onEvent;
|
|
564
|
+
}
|
|
565
|
+
setConsolidator(consolidator) {
|
|
566
|
+
this.consolidator = consolidator;
|
|
567
|
+
}
|
|
568
|
+
async save(entry) {
|
|
569
|
+
return this.writeLock.withLock(entry.id ?? "*", async () => {
|
|
570
|
+
const prev = entry.id ? this.idx.get(entry.id) : null;
|
|
571
|
+
const finalized = finalizeEntry(
|
|
572
|
+
{
|
|
573
|
+
...entry,
|
|
574
|
+
embedding: entry.embedding ?? prev?.embedding,
|
|
575
|
+
// 显式 createdAt 优先(如合并器保留最早创建时间);未提供时沿用已有条目的
|
|
576
|
+
createdAt: entry.createdAt ?? prev?.createdAt
|
|
577
|
+
},
|
|
578
|
+
Date.now()
|
|
579
|
+
);
|
|
580
|
+
if (this.embedder && !finalized.embedding) {
|
|
581
|
+
try {
|
|
582
|
+
finalized.embedding = await this.embedder.embed(finalized.content);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
this.onEvent?.({ type: "embedding:error", id: finalized.id, error: err.message });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
this.idx.add(finalized);
|
|
588
|
+
return finalized;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
async get(id) {
|
|
592
|
+
return this.idx.get(id);
|
|
593
|
+
}
|
|
594
|
+
async delete(id) {
|
|
595
|
+
return this.writeLock.withLock(id, async () => this.idx.remove(id));
|
|
596
|
+
}
|
|
597
|
+
async list(limit) {
|
|
598
|
+
return this.idx.list(limit);
|
|
599
|
+
}
|
|
600
|
+
async search(query, limit) {
|
|
601
|
+
return this.idx.search(query, limit);
|
|
602
|
+
}
|
|
603
|
+
async searchVectors(queryVec, limit) {
|
|
604
|
+
return this.idx.searchVectors(queryVec, limit);
|
|
605
|
+
}
|
|
606
|
+
async touchRecall(id, at = Date.now()) {
|
|
607
|
+
return this.writeLock.withLock(id, async () => {
|
|
608
|
+
const e = this.idx.get(id);
|
|
609
|
+
if (!e) return;
|
|
610
|
+
e.lastRecalledAt = at;
|
|
611
|
+
e.recallCount += 1;
|
|
612
|
+
this.idx.add(e);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async consolidate(options) {
|
|
616
|
+
if (!this.consolidator) return { merged: 0, pruned: 0 };
|
|
617
|
+
const result = await this.consolidator.run(options);
|
|
618
|
+
this.lastConsolidatedAt = Date.now();
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
async prune(options) {
|
|
622
|
+
const now = Date.now();
|
|
623
|
+
const maxAge = options?.maxAgeMs;
|
|
624
|
+
const minConf = options?.minConfidence ?? 0;
|
|
625
|
+
let removed = 0;
|
|
626
|
+
for (const e of this.idx.all()) {
|
|
627
|
+
const expiredByAge = maxAge != null && now - e.updatedAt > maxAge;
|
|
628
|
+
const expiredByTtl = e.expiresAt != null && now > e.expiresAt;
|
|
629
|
+
const lowConf = e.confidence < minConf;
|
|
630
|
+
if (expiredByAge || expiredByTtl || lowConf) {
|
|
631
|
+
if (await this.delete(e.id)) removed++;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (removed > 0) this.onEvent?.({ type: "prune", removed });
|
|
635
|
+
return removed;
|
|
636
|
+
}
|
|
637
|
+
async count() {
|
|
638
|
+
return this.idx.count();
|
|
639
|
+
}
|
|
640
|
+
markConsolidated(at = Date.now()) {
|
|
641
|
+
this.lastConsolidatedAt = at;
|
|
642
|
+
}
|
|
643
|
+
async stats() {
|
|
644
|
+
const base = this.idx.stats();
|
|
645
|
+
return { ...base, lastConsolidatedAt: this.lastConsolidatedAt };
|
|
646
|
+
}
|
|
647
|
+
dispose() {
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
function createInMemoryStore() {
|
|
651
|
+
return new InMemoryStore();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/store/file-memory-store.ts
|
|
655
|
+
function resolveBaseDir(baseDir) {
|
|
656
|
+
if (baseDir) {
|
|
657
|
+
if (baseDir === "~") return os.homedir();
|
|
658
|
+
if (baseDir.startsWith("~/")) return path.join(os.homedir(), baseDir.slice(2));
|
|
659
|
+
if (path.isAbsolute(baseDir)) return baseDir;
|
|
660
|
+
return path.join(process.cwd(), baseDir);
|
|
661
|
+
}
|
|
662
|
+
return path.join(process.cwd(), ".aipack", "memory");
|
|
663
|
+
}
|
|
664
|
+
function encodeId(id) {
|
|
665
|
+
return encodeURIComponent(id);
|
|
666
|
+
}
|
|
667
|
+
var LOAD_CONCURRENCY = 64;
|
|
668
|
+
async function runPool(tasks) {
|
|
669
|
+
let done = 0;
|
|
670
|
+
for (let i = 0; i < tasks.length; i += LOAD_CONCURRENCY) {
|
|
671
|
+
const batch = tasks.slice(i, i + LOAD_CONCURRENCY);
|
|
672
|
+
await Promise.all(
|
|
673
|
+
batch.map(async (t) => {
|
|
674
|
+
try {
|
|
675
|
+
await t();
|
|
676
|
+
} finally {
|
|
677
|
+
done++;
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return done;
|
|
683
|
+
}
|
|
684
|
+
var FileMemoryStore = class {
|
|
685
|
+
baseDir;
|
|
686
|
+
maxAge;
|
|
687
|
+
idx;
|
|
688
|
+
loaded = false;
|
|
689
|
+
loading = null;
|
|
690
|
+
consolidator;
|
|
691
|
+
embedder;
|
|
692
|
+
onEvent;
|
|
693
|
+
/** 同 id 写互斥(save/delete/touchRecall) */
|
|
694
|
+
writeLocks = new KeyedMutex();
|
|
695
|
+
lastConsolidatedAt;
|
|
696
|
+
constructor(options = {}) {
|
|
697
|
+
this.baseDir = resolveBaseDir(options.baseDir);
|
|
698
|
+
this.maxAge = options.maxAge;
|
|
699
|
+
this.idx = options.index ?? new MemoryIndex();
|
|
700
|
+
this.embedder = options.embedder;
|
|
701
|
+
this.onEvent = options.onEvent;
|
|
702
|
+
}
|
|
703
|
+
get dir() {
|
|
704
|
+
return this.baseDir;
|
|
705
|
+
}
|
|
706
|
+
setConsolidator(consolidator) {
|
|
707
|
+
this.consolidator = consolidator;
|
|
708
|
+
}
|
|
709
|
+
// ─── 加载 ───────────────────────────────────────────────────────
|
|
710
|
+
/** 懒加载:读取目录所有 JSON → 内存索引。并发安全。 */
|
|
711
|
+
async ensureLoaded() {
|
|
712
|
+
if (this.loaded) return;
|
|
713
|
+
if (this.loading) return this.loading;
|
|
714
|
+
this.loading = (async () => {
|
|
715
|
+
const start = Date.now();
|
|
716
|
+
let loaded = 0;
|
|
717
|
+
let skipped = 0;
|
|
718
|
+
try {
|
|
719
|
+
await fs.mkdir(this.baseDir, { recursive: true });
|
|
720
|
+
const files = await fs.readdir(this.baseDir);
|
|
721
|
+
const now = Date.now();
|
|
722
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
|
|
723
|
+
await runPool(
|
|
724
|
+
jsonFiles.map((f) => async () => {
|
|
725
|
+
const filePath = path.join(this.baseDir, f);
|
|
726
|
+
try {
|
|
727
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
728
|
+
const entry = JSON.parse(raw);
|
|
729
|
+
if (!entry?.id) {
|
|
730
|
+
skipped++;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const expiredByAge = this.maxAge != null && now - entry.updatedAt > this.maxAge;
|
|
734
|
+
const expiredByTtl = entry.expiresAt != null && now > entry.expiresAt;
|
|
735
|
+
if (expiredByAge || expiredByTtl) {
|
|
736
|
+
await fs.unlink(filePath).catch(() => {
|
|
737
|
+
});
|
|
738
|
+
skipped++;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.idx.add(entry);
|
|
742
|
+
loaded++;
|
|
743
|
+
} catch {
|
|
744
|
+
skipped++;
|
|
745
|
+
this.onEvent?.({ type: "store:corrupt", file: f });
|
|
746
|
+
}
|
|
747
|
+
})
|
|
748
|
+
);
|
|
749
|
+
} catch (err) {
|
|
750
|
+
this.onEvent?.({ type: "store:corrupt", file: `<baseDir> ${err.message}` });
|
|
751
|
+
}
|
|
752
|
+
this.loaded = true;
|
|
753
|
+
this.loading = null;
|
|
754
|
+
if (loaded > 0 || skipped > 0) {
|
|
755
|
+
this.onEvent?.({
|
|
756
|
+
type: "store:load",
|
|
757
|
+
loaded,
|
|
758
|
+
skipped,
|
|
759
|
+
ms: Date.now() - start
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
})();
|
|
763
|
+
return this.loading;
|
|
764
|
+
}
|
|
765
|
+
entryPath(id) {
|
|
766
|
+
return path.join(this.baseDir, `${encodeId(id)}.json`);
|
|
767
|
+
}
|
|
768
|
+
async writeEntry(entry) {
|
|
769
|
+
await fs.mkdir(this.baseDir, { recursive: true });
|
|
770
|
+
const target = this.entryPath(entry.id);
|
|
771
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
772
|
+
await fs.writeFile(tmp, JSON.stringify(entry, null, 2), "utf-8");
|
|
773
|
+
await fs.rename(tmp, target);
|
|
774
|
+
}
|
|
775
|
+
// ─── MemoryStore ────────────────────────────────────────────────
|
|
776
|
+
async save(entry) {
|
|
777
|
+
await this.ensureLoaded();
|
|
778
|
+
return this.writeLocks.withLock(entry.id ?? "*", async () => {
|
|
779
|
+
const prev = entry.id ? this.idx.get(entry.id) : null;
|
|
780
|
+
const finalized = finalizeEntry(
|
|
781
|
+
{
|
|
782
|
+
...entry,
|
|
783
|
+
// 保留 embedding(若已存在且新 entry 未提供)
|
|
784
|
+
embedding: entry.embedding ?? prev?.embedding,
|
|
785
|
+
// 显式 createdAt 优先(如合并器保留最早创建时间);未提供时沿用已有条目的
|
|
786
|
+
createdAt: entry.createdAt ?? prev?.createdAt
|
|
787
|
+
},
|
|
788
|
+
Date.now()
|
|
789
|
+
);
|
|
790
|
+
if (this.embedder && !finalized.embedding) {
|
|
791
|
+
try {
|
|
792
|
+
finalized.embedding = await this.embedder.embed(finalized.content);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
this.onEvent?.({
|
|
795
|
+
type: "embedding:error",
|
|
796
|
+
id: finalized.id,
|
|
797
|
+
error: err.message
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
await this.writeEntry(finalized);
|
|
802
|
+
this.idx.add(finalized);
|
|
803
|
+
return finalized;
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
async get(id) {
|
|
807
|
+
await this.ensureLoaded();
|
|
808
|
+
return this.idx.get(id);
|
|
809
|
+
}
|
|
810
|
+
async delete(id) {
|
|
811
|
+
await this.ensureLoaded();
|
|
812
|
+
return this.writeLocks.withLock(id, async () => {
|
|
813
|
+
const existed = this.idx.remove(id);
|
|
814
|
+
if (existed) {
|
|
815
|
+
await fs.unlink(this.entryPath(id)).catch(() => {
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
return existed;
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
async list(limit) {
|
|
822
|
+
await this.ensureLoaded();
|
|
823
|
+
return this.idx.list(limit);
|
|
824
|
+
}
|
|
825
|
+
async search(query, limit) {
|
|
826
|
+
await this.ensureLoaded();
|
|
827
|
+
return this.idx.search(query, limit);
|
|
828
|
+
}
|
|
829
|
+
async searchVectors(queryVec, limit) {
|
|
830
|
+
await this.ensureLoaded();
|
|
831
|
+
return this.idx.searchVectors(queryVec, limit);
|
|
832
|
+
}
|
|
833
|
+
async touchRecall(id, at = Date.now()) {
|
|
834
|
+
await this.ensureLoaded();
|
|
835
|
+
return this.writeLocks.withLock(id, async () => {
|
|
836
|
+
const e = this.idx.get(id);
|
|
837
|
+
if (!e) return;
|
|
838
|
+
e.lastRecalledAt = at;
|
|
839
|
+
e.recallCount += 1;
|
|
840
|
+
await this.writeEntry(e);
|
|
841
|
+
this.idx.add(e);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
async consolidate(options) {
|
|
845
|
+
if (!this.consolidator) return { merged: 0, pruned: 0 };
|
|
846
|
+
await this.ensureLoaded();
|
|
847
|
+
const result = await this.consolidator.run(options);
|
|
848
|
+
this.lastConsolidatedAt = Date.now();
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
async prune(options) {
|
|
852
|
+
await this.ensureLoaded();
|
|
853
|
+
const now = Date.now();
|
|
854
|
+
const maxAge = options?.maxAgeMs;
|
|
855
|
+
const minConf = options?.minConfidence ?? 0;
|
|
856
|
+
let removed = 0;
|
|
857
|
+
for (const e of this.idx.all()) {
|
|
858
|
+
const expiredByAge = maxAge != null && now - e.updatedAt > maxAge;
|
|
859
|
+
const expiredByTtl = e.expiresAt != null && now > e.expiresAt;
|
|
860
|
+
const lowConf = e.confidence < minConf;
|
|
861
|
+
if (expiredByAge || expiredByTtl || lowConf) {
|
|
862
|
+
if (await this.delete(e.id)) removed++;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (removed > 0) this.onEvent?.({ type: "prune", removed });
|
|
866
|
+
return removed;
|
|
867
|
+
}
|
|
868
|
+
async count() {
|
|
869
|
+
await this.ensureLoaded();
|
|
870
|
+
return this.idx.count();
|
|
871
|
+
}
|
|
872
|
+
markConsolidated(at = Date.now()) {
|
|
873
|
+
this.lastConsolidatedAt = at;
|
|
874
|
+
}
|
|
875
|
+
async stats() {
|
|
876
|
+
await this.ensureLoaded();
|
|
877
|
+
const base = this.idx.stats();
|
|
878
|
+
return { ...base, lastConsolidatedAt: this.lastConsolidatedAt };
|
|
879
|
+
}
|
|
880
|
+
dispose() {
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
function createFileMemoryStore(options) {
|
|
884
|
+
return new FileMemoryStore(options);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/retrieval/embedder.ts
|
|
888
|
+
function cosine(a, b) {
|
|
889
|
+
if (!a || !b || a.length === 0 || a.length !== b.length) return 0;
|
|
890
|
+
let dot2 = 0;
|
|
891
|
+
let na = 0;
|
|
892
|
+
let nb = 0;
|
|
893
|
+
for (let i = 0; i < a.length; i++) {
|
|
894
|
+
dot2 += a[i] * b[i];
|
|
895
|
+
na += a[i] * a[i];
|
|
896
|
+
nb += b[i] * b[i];
|
|
897
|
+
}
|
|
898
|
+
if (na === 0 || nb === 0) return 0;
|
|
899
|
+
return dot2 / (Math.sqrt(na) * Math.sqrt(nb));
|
|
900
|
+
}
|
|
901
|
+
function minMaxNormalize(scores) {
|
|
902
|
+
if (scores.length === 0) return [];
|
|
903
|
+
const min = Math.min(...scores);
|
|
904
|
+
const max = Math.max(...scores);
|
|
905
|
+
if (max - min === 0) return scores.map(() => 1);
|
|
906
|
+
return scores.map((s) => (s - min) / (max - min));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/retrieval/hybrid-retriever.ts
|
|
910
|
+
var HybridRetriever = class {
|
|
911
|
+
bm25;
|
|
912
|
+
vector;
|
|
913
|
+
embedder;
|
|
914
|
+
bm25Weight;
|
|
915
|
+
embedWeight;
|
|
916
|
+
minScore;
|
|
917
|
+
constructor(options) {
|
|
918
|
+
this.bm25 = options.bm25;
|
|
919
|
+
this.vector = options.vector;
|
|
920
|
+
this.embedder = options.embedder;
|
|
921
|
+
this.bm25Weight = options.bm25Weight ?? 0.5;
|
|
922
|
+
this.embedWeight = options.embedWeight ?? 0.5;
|
|
923
|
+
this.minScore = options.minScore ?? 0.1;
|
|
924
|
+
}
|
|
925
|
+
/** 是否启用了向量检索 */
|
|
926
|
+
get hasEmbedder() {
|
|
927
|
+
return !!this.embedder;
|
|
928
|
+
}
|
|
929
|
+
async search(query, limit = 5, opts) {
|
|
930
|
+
const minScore = opts?.minScore ?? this.minScore;
|
|
931
|
+
const raw = opts?.raw ?? false;
|
|
932
|
+
if (limit <= 0) return [];
|
|
933
|
+
const bm25Limit = this.embedder ? Math.max(limit * 3, limit) : limit;
|
|
934
|
+
const bm25Results = await this.bm25.search(query, bm25Limit);
|
|
935
|
+
if (!this.embedder) {
|
|
936
|
+
if (bm25Results.length === 0) return [];
|
|
937
|
+
return raw ? this.fuseRaw(bm25Results, [], minScore, limit) : this.fuseSingle(bm25Results, minScore, limit);
|
|
938
|
+
}
|
|
939
|
+
let queryVec = null;
|
|
940
|
+
try {
|
|
941
|
+
queryVec = await this.embedder.embed(query);
|
|
942
|
+
} catch {
|
|
943
|
+
queryVec = null;
|
|
944
|
+
}
|
|
945
|
+
if (!this.vector || !queryVec || queryVec.length === 0) {
|
|
946
|
+
if (bm25Results.length === 0) return [];
|
|
947
|
+
if (raw) return this.fuseRaw(bm25Results, [], minScore, limit);
|
|
948
|
+
return queryVec && this.vector ? this.rerank(bm25Results, queryVec, minScore, limit) : this.fuseSingle(bm25Results, minScore, limit);
|
|
949
|
+
}
|
|
950
|
+
let vecResults = [];
|
|
951
|
+
try {
|
|
952
|
+
vecResults = await this.vector.searchVectors(queryVec, bm25Limit);
|
|
953
|
+
} catch {
|
|
954
|
+
vecResults = [];
|
|
955
|
+
}
|
|
956
|
+
if (bm25Results.length === 0 && vecResults.length === 0) return [];
|
|
957
|
+
return raw ? this.fuseRaw(bm25Results, vecResults, minScore, limit) : this.fuseDual(bm25Results, vecResults, minScore, limit);
|
|
958
|
+
}
|
|
959
|
+
// ─── 融合策略 ───────────────────────────────────────────────────
|
|
960
|
+
/** 单路(纯 BM25):归一化 + 过滤 + 截断 */
|
|
961
|
+
fuseSingle(results, minScore, limit) {
|
|
962
|
+
const normed = minMaxNormalize(results.map((r) => r.score));
|
|
963
|
+
return results.map((r, i) => ({ entry: r.entry, score: normed[i], matchedBy: "bm25" })).filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
964
|
+
}
|
|
965
|
+
/** 候选重排(兼容路径):BM25 候选 + 向量 cosine 加权融合 */
|
|
966
|
+
rerank(bm25Results, queryVec, minScore, limit) {
|
|
967
|
+
const cosScores = bm25Results.map((r) => {
|
|
968
|
+
const ev = r.entry.embedding;
|
|
969
|
+
return ev && ev.length === queryVec.length ? cosine(queryVec, ev) : 0;
|
|
970
|
+
});
|
|
971
|
+
const bm25Normed = minMaxNormalize(bm25Results.map((r) => r.score));
|
|
972
|
+
const cosNormed = minMaxNormalize(cosScores);
|
|
973
|
+
const wSum = this.bm25Weight + this.embedWeight || 1;
|
|
974
|
+
return bm25Results.map((r, i) => ({
|
|
975
|
+
entry: r.entry,
|
|
976
|
+
score: (this.bm25Weight * bm25Normed[i] + this.embedWeight * cosNormed[i]) / wSum,
|
|
977
|
+
matchedBy: cosNormed[i] >= bm25Normed[i] ? "embedding" : "hybrid"
|
|
978
|
+
})).filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* 原始分数融合(raw 模式):保留检索源原始分数,不做 min-max 归一化,
|
|
982
|
+
* 供合并器按「绝对相似度阈值」判定可合并项。双路命中时取 max
|
|
983
|
+
* (任一来源判定相似即相似),避免归一化把次优分数抹平。
|
|
984
|
+
*
|
|
985
|
+
* 量纲说明:BM25 分数在 BM25Retriever 层已按 Σidf 归一化到 [0,1]
|
|
986
|
+
* (完全同文≈1),cosine 本身在 [0,1],因此这里的绝对阈值(如 0.85)
|
|
987
|
+
* 对两路统一成立。
|
|
988
|
+
*/
|
|
989
|
+
fuseRaw(bm25Results, vecResults, minScore, limit) {
|
|
990
|
+
const union = /* @__PURE__ */ new Map();
|
|
991
|
+
for (const r of bm25Results) {
|
|
992
|
+
union.set(r.entry.id, { entry: r.entry, score: r.score, matchedBy: "bm25" });
|
|
993
|
+
}
|
|
994
|
+
for (const r of vecResults) {
|
|
995
|
+
const prev = union.get(r.entry.id);
|
|
996
|
+
if (!prev) {
|
|
997
|
+
union.set(r.entry.id, { entry: r.entry, score: r.score, matchedBy: "embedding" });
|
|
998
|
+
} else {
|
|
999
|
+
prev.score = Math.max(prev.score, r.score);
|
|
1000
|
+
if (prev.matchedBy === "bm25") prev.matchedBy = "hybrid";
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return [...union.values()].filter((x) => x.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1004
|
+
}
|
|
1005
|
+
/** 双路独立召回融合:按 id 取并集,加权求和,再按权重和归一化 */
|
|
1006
|
+
fuseDual(bm25Results, vecResults, minScore, limit) {
|
|
1007
|
+
const bm25Normed = minMaxNormalize(bm25Results.map((r) => r.score));
|
|
1008
|
+
const vecNormed = minMaxNormalize(vecResults.map((r) => r.score));
|
|
1009
|
+
const wB = this.bm25Weight;
|
|
1010
|
+
const wE = this.embedWeight;
|
|
1011
|
+
const wSum = wB + wE || 1;
|
|
1012
|
+
const union = /* @__PURE__ */ new Map();
|
|
1013
|
+
bm25Results.forEach((r, i) => {
|
|
1014
|
+
union.set(r.entry.id, { entry: r.entry, score: bm25Normed[i] * wB, matchedBy: "bm25" });
|
|
1015
|
+
});
|
|
1016
|
+
vecResults.forEach((r, i) => {
|
|
1017
|
+
const prev = union.get(r.entry.id);
|
|
1018
|
+
const s = vecNormed[i] * wE;
|
|
1019
|
+
if (!prev) {
|
|
1020
|
+
union.set(r.entry.id, { entry: r.entry, score: s, matchedBy: "embedding" });
|
|
1021
|
+
} else {
|
|
1022
|
+
prev.score += s;
|
|
1023
|
+
if (prev.matchedBy === "bm25") prev.matchedBy = "hybrid";
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
return [...union.values()].map((x) => ({ entry: x.entry, score: x.score / wSum, matchedBy: x.matchedBy })).filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// ../agent/core/types.ts
|
|
1031
|
+
function createTextContent(text) {
|
|
1032
|
+
return { type: "text", text };
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ../agent/core/context-resource.ts
|
|
1036
|
+
var ContextResourceBuilder = class {
|
|
1037
|
+
_id;
|
|
1038
|
+
_type = "custom";
|
|
1039
|
+
_role = "user";
|
|
1040
|
+
_content = "";
|
|
1041
|
+
_timestamp = Date.now();
|
|
1042
|
+
_dependencies = [];
|
|
1043
|
+
_meta = {};
|
|
1044
|
+
_pinned = false;
|
|
1045
|
+
constructor(id) {
|
|
1046
|
+
this._id = id || `res_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1047
|
+
}
|
|
1048
|
+
id(id) {
|
|
1049
|
+
this._id = id;
|
|
1050
|
+
return this;
|
|
1051
|
+
}
|
|
1052
|
+
type(t) {
|
|
1053
|
+
this._type = t;
|
|
1054
|
+
return this;
|
|
1055
|
+
}
|
|
1056
|
+
role(r) {
|
|
1057
|
+
this._role = r;
|
|
1058
|
+
return this;
|
|
1059
|
+
}
|
|
1060
|
+
content(c) {
|
|
1061
|
+
this._content = c;
|
|
1062
|
+
return this;
|
|
1063
|
+
}
|
|
1064
|
+
timestamp(ts) {
|
|
1065
|
+
this._timestamp = ts;
|
|
1066
|
+
return this;
|
|
1067
|
+
}
|
|
1068
|
+
dependsOn(...ids) {
|
|
1069
|
+
this._dependencies.push(...ids);
|
|
1070
|
+
return this;
|
|
1071
|
+
}
|
|
1072
|
+
meta(key, value) {
|
|
1073
|
+
this._meta[key] = value;
|
|
1074
|
+
return this;
|
|
1075
|
+
}
|
|
1076
|
+
pinned(p = true) {
|
|
1077
|
+
this._pinned = p;
|
|
1078
|
+
return this;
|
|
1079
|
+
}
|
|
1080
|
+
build() {
|
|
1081
|
+
return {
|
|
1082
|
+
id: this._id,
|
|
1083
|
+
type: this._type,
|
|
1084
|
+
role: this._role,
|
|
1085
|
+
content: this._content,
|
|
1086
|
+
timestamp: this._timestamp,
|
|
1087
|
+
dependencies: this._dependencies,
|
|
1088
|
+
meta: this._meta,
|
|
1089
|
+
pinned: this._pinned
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// ../agent/core/transformer.ts
|
|
1095
|
+
var BaseTransformer = class {
|
|
1096
|
+
config;
|
|
1097
|
+
constructor(options = {}) {
|
|
1098
|
+
this.config = options.config ?? {};
|
|
1099
|
+
}
|
|
1100
|
+
async transform(resources, context) {
|
|
1101
|
+
return this.run(resources, context);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// ../agent/core/extension.ts
|
|
1106
|
+
var BaseExtension = class {
|
|
1107
|
+
apply(hooks, context) {
|
|
1108
|
+
this.setup(hooks, context);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// src/capture/extractor.ts
|
|
1113
|
+
function truncate(text, max) {
|
|
1114
|
+
if (text.length <= max) return text;
|
|
1115
|
+
return `${text.slice(0, max - 1)}\u2026`;
|
|
1116
|
+
}
|
|
1117
|
+
function firstSentence(text, max) {
|
|
1118
|
+
const trimmed = text.trim();
|
|
1119
|
+
if (!trimmed) return "";
|
|
1120
|
+
const match = trimmed.match(/^[^。!?!?.…\n]+[。!?!?.…]?/);
|
|
1121
|
+
const first = match ? match[0].trim() : trimmed;
|
|
1122
|
+
return truncate(first, max);
|
|
1123
|
+
}
|
|
1124
|
+
function extractFromTurn(userMessage, assistantContent, toolsUsed, options = {}) {
|
|
1125
|
+
const maxChars = options.maxChars ?? 2e3;
|
|
1126
|
+
const maxConcepts = options.maxConcepts ?? 8;
|
|
1127
|
+
const parts = [];
|
|
1128
|
+
if (userMessage) parts.push(`Q: ${firstSentence(userMessage, 160)}`);
|
|
1129
|
+
if (assistantContent) parts.push(`A: ${firstSentence(assistantContent, 320)}`);
|
|
1130
|
+
if (toolsUsed.length > 0) parts.push(`tools: ${toolsUsed.slice(0, 5).join(", ")}`);
|
|
1131
|
+
const content = truncate(parts.join("\n"), maxChars);
|
|
1132
|
+
const conceptSource = `${userMessage} ${assistantContent} ${toolsUsed.join(" ")}`;
|
|
1133
|
+
const concepts = extractConcepts(conceptSource, maxConcepts);
|
|
1134
|
+
return { content, concepts, summarized: false };
|
|
1135
|
+
}
|
|
1136
|
+
async function runCaptureExtractor(input, options = {}) {
|
|
1137
|
+
const { userMessage, assistantContent, toolsUsed, summarizeFn } = input;
|
|
1138
|
+
if (summarizeFn) {
|
|
1139
|
+
try {
|
|
1140
|
+
const result = await summarizeFn({
|
|
1141
|
+
userMessage,
|
|
1142
|
+
assistantContent,
|
|
1143
|
+
toolsUsed
|
|
1144
|
+
});
|
|
1145
|
+
if (result && result.summary && result.summary.trim()) {
|
|
1146
|
+
const maxChars = options.maxChars ?? 2e3;
|
|
1147
|
+
const concepts = result.concepts && result.concepts.length > 0 ? result.concepts : extractConcepts(
|
|
1148
|
+
`${userMessage} ${result.summary}`,
|
|
1149
|
+
options.maxConcepts ?? 8
|
|
1150
|
+
);
|
|
1151
|
+
return {
|
|
1152
|
+
content: truncate(result.summary.trim(), maxChars),
|
|
1153
|
+
concepts,
|
|
1154
|
+
summarized: true
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return extractFromTurn(userMessage, assistantContent, toolsUsed, options);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/capture/capture-extension.ts
|
|
1164
|
+
var MemoryCaptureExtension = class extends BaseExtension {
|
|
1165
|
+
name = "memory-capture";
|
|
1166
|
+
store;
|
|
1167
|
+
summarizeFn;
|
|
1168
|
+
minLength;
|
|
1169
|
+
maxConcepts;
|
|
1170
|
+
maxContentChars;
|
|
1171
|
+
consolidateEvery;
|
|
1172
|
+
ttlMs;
|
|
1173
|
+
onEvent;
|
|
1174
|
+
/** 本轮待捕获的用户消息(sessionKey -> 请求信息) */
|
|
1175
|
+
pending = /* @__PURE__ */ new Map();
|
|
1176
|
+
/** 捕获计数(用于触发 consolidate) */
|
|
1177
|
+
captureCount = 0;
|
|
1178
|
+
constructor(store, options = {}) {
|
|
1179
|
+
super();
|
|
1180
|
+
this.store = store;
|
|
1181
|
+
this.summarizeFn = options.summarizeFn;
|
|
1182
|
+
this.minLength = options.minLength ?? 12;
|
|
1183
|
+
this.maxConcepts = options.maxConcepts ?? 8;
|
|
1184
|
+
this.maxContentChars = options.maxContentChars ?? 2e3;
|
|
1185
|
+
this.consolidateEvery = options.consolidateEvery ?? 0;
|
|
1186
|
+
this.ttlMs = options.ttlMs;
|
|
1187
|
+
this.onEvent = options.onEvent;
|
|
1188
|
+
}
|
|
1189
|
+
setup(hooks, context) {
|
|
1190
|
+
const sessionKey = context.sessionKey;
|
|
1191
|
+
hooks.beforeRun.tapPromise("memory-capture", async (request) => {
|
|
1192
|
+
try {
|
|
1193
|
+
this.pending.set(sessionKey, { message: request.message });
|
|
1194
|
+
} catch {
|
|
1195
|
+
}
|
|
1196
|
+
return request;
|
|
1197
|
+
});
|
|
1198
|
+
hooks.done.tapPromise("memory-capture", async (result, _request) => {
|
|
1199
|
+
await this.captureFromResult(result, sessionKey);
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
async captureFromResult(result, sessionKey) {
|
|
1203
|
+
if (!sessionKey) return;
|
|
1204
|
+
const stashed = this.pending.get(sessionKey);
|
|
1205
|
+
this.pending.delete(sessionKey);
|
|
1206
|
+
if (!stashed) return;
|
|
1207
|
+
if (!result.success) return;
|
|
1208
|
+
const userMessage = stashed.message;
|
|
1209
|
+
const assistantContent = result.content ?? "";
|
|
1210
|
+
if (!userMessage || userMessage.trim().length < this.minLength) return;
|
|
1211
|
+
if (!assistantContent || !assistantContent.trim()) return;
|
|
1212
|
+
try {
|
|
1213
|
+
const extracted = await runCaptureExtractor(
|
|
1214
|
+
{
|
|
1215
|
+
userMessage,
|
|
1216
|
+
assistantContent,
|
|
1217
|
+
toolsUsed: result.toolsUsed ?? [],
|
|
1218
|
+
summarizeFn: this.summarizeFn
|
|
1219
|
+
},
|
|
1220
|
+
{ maxConcepts: this.maxConcepts, maxChars: this.maxContentChars }
|
|
1221
|
+
);
|
|
1222
|
+
await this.store.save({
|
|
1223
|
+
content: extracted.content,
|
|
1224
|
+
concepts: extracted.concepts,
|
|
1225
|
+
confidence: extracted.summarized ? 0.8 : 0.6,
|
|
1226
|
+
source: "capture",
|
|
1227
|
+
sessionKey,
|
|
1228
|
+
ttlMs: this.ttlMs
|
|
1229
|
+
});
|
|
1230
|
+
this.captureCount += 1;
|
|
1231
|
+
if (this.consolidateEvery > 0 && this.captureCount % this.consolidateEvery === 0) {
|
|
1232
|
+
try {
|
|
1233
|
+
await this.store.consolidate();
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
this.onEvent?.({
|
|
1236
|
+
type: "consolidate:failed",
|
|
1237
|
+
error: err.message
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
this.onEvent?.({ type: "capture:failed", sessionKey, error: err.message });
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
// src/injection/sentinels.ts
|
|
1248
|
+
var MEMORY_BLOCK_START = "<<<AIPACK_MEMORY>>>";
|
|
1249
|
+
var MEMORY_BLOCK_END = "<<</AIPACK_MEMORY>>>";
|
|
1250
|
+
function stripMemoryBlock(text) {
|
|
1251
|
+
if (!text) return text;
|
|
1252
|
+
const re = new RegExp(
|
|
1253
|
+
`${escapeRegex(MEMORY_BLOCK_START)}[\\s\\S]*?${escapeRegex(MEMORY_BLOCK_END)}\\s*`,
|
|
1254
|
+
"g"
|
|
1255
|
+
);
|
|
1256
|
+
return text.replace(re, "").trim();
|
|
1257
|
+
}
|
|
1258
|
+
function hasMemoryBlock(text) {
|
|
1259
|
+
if (!text) return false;
|
|
1260
|
+
return text.includes(MEMORY_BLOCK_START);
|
|
1261
|
+
}
|
|
1262
|
+
function wrapMemoryBlock(lines) {
|
|
1263
|
+
const body = lines.join("\n").trimEnd();
|
|
1264
|
+
return `${MEMORY_BLOCK_START}
|
|
1265
|
+
${body}
|
|
1266
|
+
${MEMORY_BLOCK_END}`;
|
|
1267
|
+
}
|
|
1268
|
+
function buildMemoryBlock(results) {
|
|
1269
|
+
if (results.length === 0) return "";
|
|
1270
|
+
const lines = ["[Relevant memories]"];
|
|
1271
|
+
for (const r of results) {
|
|
1272
|
+
const score = r.score.toFixed(2);
|
|
1273
|
+
const content = r.entry.content.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
1274
|
+
lines.push(`- ${content} (score=${score}, id=${r.entry.id})`);
|
|
1275
|
+
}
|
|
1276
|
+
return wrapMemoryBlock(lines);
|
|
1277
|
+
}
|
|
1278
|
+
function escapeRegex(s) {
|
|
1279
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/injection/injection-transformer.ts
|
|
1283
|
+
var MemoryInjectionTransformer = class extends BaseTransformer {
|
|
1284
|
+
name = "memory-injection";
|
|
1285
|
+
retriever;
|
|
1286
|
+
maxMemories;
|
|
1287
|
+
minScore;
|
|
1288
|
+
queryTransform;
|
|
1289
|
+
onRecall;
|
|
1290
|
+
constructor(retriever, options = {}) {
|
|
1291
|
+
super();
|
|
1292
|
+
this.retriever = retriever;
|
|
1293
|
+
this.maxMemories = options.maxMemories ?? 5;
|
|
1294
|
+
this.minScore = options.minScore ?? 0.1;
|
|
1295
|
+
this.queryTransform = options.queryTransform;
|
|
1296
|
+
this.onRecall = options.onRecall;
|
|
1297
|
+
}
|
|
1298
|
+
async run(resources, _context) {
|
|
1299
|
+
if (resources.length === 0) return resources;
|
|
1300
|
+
let cleaned = resources.map((r) => this.stripResource(r));
|
|
1301
|
+
let latestIdx = -1;
|
|
1302
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
1303
|
+
if (cleaned[i].type === "user_message") {
|
|
1304
|
+
latestIdx = i;
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (latestIdx === -1) return cleaned;
|
|
1309
|
+
const latest = cleaned[latestIdx];
|
|
1310
|
+
const queryText = this.extractUserText(latest);
|
|
1311
|
+
if (!queryText || !queryText.trim()) return cleaned;
|
|
1312
|
+
const query = this.queryTransform ? this.queryTransform(queryText) : queryText;
|
|
1313
|
+
const results = await this.retriever.search(query, this.maxMemories, {
|
|
1314
|
+
minScore: this.minScore
|
|
1315
|
+
});
|
|
1316
|
+
if (results.length === 0) return cleaned;
|
|
1317
|
+
if (this.onRecall) {
|
|
1318
|
+
const ids = results.map((r) => r.entry.id);
|
|
1319
|
+
Promise.resolve().then(() => this.onRecall(ids)).catch(() => {
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
const block = buildMemoryBlock(results);
|
|
1323
|
+
const injected = this.injectIntoResource(latest, block);
|
|
1324
|
+
cleaned = [...cleaned];
|
|
1325
|
+
cleaned[latestIdx] = injected;
|
|
1326
|
+
return cleaned;
|
|
1327
|
+
}
|
|
1328
|
+
// ─── 工具方法 ───────────────────────────────────────────────────
|
|
1329
|
+
/** 从资源中抽取纯文本(兼容 string 与 ContentBlock[]) */
|
|
1330
|
+
extractUserText(resource) {
|
|
1331
|
+
const content = resource.content;
|
|
1332
|
+
if (typeof content === "string") return stripMemoryBlock(content);
|
|
1333
|
+
if (Array.isArray(content)) {
|
|
1334
|
+
return content.map((b) => this.isTextBlock(b) ? stripMemoryBlock(b.text) : "").join("").trim();
|
|
1335
|
+
}
|
|
1336
|
+
return "";
|
|
1337
|
+
}
|
|
1338
|
+
isTextBlock(b) {
|
|
1339
|
+
return b.type === "text";
|
|
1340
|
+
}
|
|
1341
|
+
/** 剥除单个资源中的 sentinel 块,返回新资源(若无需剥除返回原资源) */
|
|
1342
|
+
stripResource(resource) {
|
|
1343
|
+
if (resource.type !== "user_message") return resource;
|
|
1344
|
+
const content = resource.content;
|
|
1345
|
+
if (typeof content === "string") {
|
|
1346
|
+
if (!hasMemoryBlock(content)) return resource;
|
|
1347
|
+
return this.rebuildResource(resource, stripMemoryBlock(content));
|
|
1348
|
+
}
|
|
1349
|
+
if (Array.isArray(content)) {
|
|
1350
|
+
let changed = false;
|
|
1351
|
+
const newBlocks = [];
|
|
1352
|
+
for (const b of content) {
|
|
1353
|
+
if (this.isTextBlock(b)) {
|
|
1354
|
+
if (!hasMemoryBlock(b.text)) {
|
|
1355
|
+
newBlocks.push(b);
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
const stripped = stripMemoryBlock(b.text);
|
|
1359
|
+
if (stripped) {
|
|
1360
|
+
newBlocks.push({ type: "text", text: stripped });
|
|
1361
|
+
}
|
|
1362
|
+
changed = true;
|
|
1363
|
+
} else {
|
|
1364
|
+
newBlocks.push(b);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (!changed) return resource;
|
|
1368
|
+
return this.rebuildResource(resource, newBlocks);
|
|
1369
|
+
}
|
|
1370
|
+
return resource;
|
|
1371
|
+
}
|
|
1372
|
+
/** 把记忆块前插进资源内容 */
|
|
1373
|
+
injectIntoResource(resource, block) {
|
|
1374
|
+
const content = resource.content;
|
|
1375
|
+
if (typeof content === "string") {
|
|
1376
|
+
const original = stripMemoryBlock(content);
|
|
1377
|
+
const injected = `${block}
|
|
1378
|
+
|
|
1379
|
+
${original}`;
|
|
1380
|
+
return this.rebuildResource(resource, injected);
|
|
1381
|
+
}
|
|
1382
|
+
if (Array.isArray(content)) {
|
|
1383
|
+
const kept = content.filter(
|
|
1384
|
+
(b) => !(this.isTextBlock(b) && hasMemoryBlock(b.text))
|
|
1385
|
+
);
|
|
1386
|
+
const injected = [
|
|
1387
|
+
{ type: "text", text: `${block}
|
|
1388
|
+
|
|
1389
|
+
` },
|
|
1390
|
+
...kept
|
|
1391
|
+
];
|
|
1392
|
+
return this.rebuildResource(resource, injected);
|
|
1393
|
+
}
|
|
1394
|
+
return resource;
|
|
1395
|
+
}
|
|
1396
|
+
/** 重建资源(保持 id/type/role/timestamp/dependencies/meta/pinned,替换 content) */
|
|
1397
|
+
rebuildResource(resource, content) {
|
|
1398
|
+
return new ContextResourceBuilder().id(resource.id).type(resource.type).role(resource.role).content(content).timestamp(resource.timestamp).pinned(resource.pinned).build();
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
// src/consolidation/consolidator.ts
|
|
1403
|
+
function mergeTwo(a, b) {
|
|
1404
|
+
const newer = a.updatedAt >= b.updatedAt ? a : b;
|
|
1405
|
+
const older = newer === a ? b : a;
|
|
1406
|
+
const content = newer.content.length >= older.content.length ? newer.content : older.content;
|
|
1407
|
+
const concepts = [.../* @__PURE__ */ new Set([...a.concepts, ...b.concepts])];
|
|
1408
|
+
const confidence = Math.min(1, Math.max(a.confidence, b.confidence) + 0.05);
|
|
1409
|
+
const lastRecalledAt = [a.lastRecalledAt, b.lastRecalledAt].filter((t) => t != null).sort().pop();
|
|
1410
|
+
return {
|
|
1411
|
+
id: newer.id,
|
|
1412
|
+
content,
|
|
1413
|
+
concepts,
|
|
1414
|
+
confidence,
|
|
1415
|
+
source: "consolidation",
|
|
1416
|
+
sessionKey: newer.sessionKey,
|
|
1417
|
+
createdAt: Math.min(a.createdAt, b.createdAt),
|
|
1418
|
+
updatedAt: Date.now(),
|
|
1419
|
+
lastRecalledAt,
|
|
1420
|
+
recallCount: a.recallCount + b.recallCount,
|
|
1421
|
+
embedding: newer.embedding ?? older.embedding,
|
|
1422
|
+
expiresAt: newer.expiresAt ?? older.expiresAt,
|
|
1423
|
+
meta: { ...older.meta ?? {}, ...newer.meta ?? {} }
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
var Consolidator = class {
|
|
1427
|
+
store;
|
|
1428
|
+
retriever;
|
|
1429
|
+
similarityThreshold;
|
|
1430
|
+
onEvent;
|
|
1431
|
+
constructor(store, retriever, options = {}) {
|
|
1432
|
+
this.store = store;
|
|
1433
|
+
this.retriever = retriever;
|
|
1434
|
+
this.similarityThreshold = options.similarityThreshold ?? 0.85;
|
|
1435
|
+
this.onEvent = options.onEvent;
|
|
1436
|
+
}
|
|
1437
|
+
async run(options = {}) {
|
|
1438
|
+
const start = Date.now();
|
|
1439
|
+
const threshold = options.similarityThreshold ?? this.similarityThreshold;
|
|
1440
|
+
const all = await this.store.list();
|
|
1441
|
+
const stats = await this.store.stats();
|
|
1442
|
+
const since = stats.lastConsolidatedAt;
|
|
1443
|
+
const candidates = (since == null ? all : all.filter((e) => e.updatedAt >= since)).sort(
|
|
1444
|
+
// 显式按 updatedAt 降序(新条目先处理,可吸收旧的相似条目);
|
|
1445
|
+
// 同毫秒写入时以 createdAt 决胜,保证处理顺序确定
|
|
1446
|
+
(a, b) => b.updatedAt - a.updatedAt || b.createdAt - a.createdAt
|
|
1447
|
+
);
|
|
1448
|
+
const processed = /* @__PURE__ */ new Set();
|
|
1449
|
+
let merged = 0;
|
|
1450
|
+
for (const entry of candidates) {
|
|
1451
|
+
if (processed.has(entry.id)) continue;
|
|
1452
|
+
let results = [];
|
|
1453
|
+
try {
|
|
1454
|
+
results = await this.retriever.search(entry.content, 10, { raw: true });
|
|
1455
|
+
} catch {
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
const similar = results.filter(
|
|
1459
|
+
(r) => r.entry.id !== entry.id && !processed.has(r.entry.id) && r.score >= threshold
|
|
1460
|
+
);
|
|
1461
|
+
if (similar.length === 0) {
|
|
1462
|
+
processed.add(entry.id);
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
let survivor = entry;
|
|
1466
|
+
for (const r of similar) {
|
|
1467
|
+
survivor = mergeTwo(survivor, r.entry);
|
|
1468
|
+
processed.add(r.entry.id);
|
|
1469
|
+
}
|
|
1470
|
+
await this.store.save({
|
|
1471
|
+
id: survivor.id,
|
|
1472
|
+
content: survivor.content,
|
|
1473
|
+
concepts: survivor.concepts,
|
|
1474
|
+
confidence: survivor.confidence,
|
|
1475
|
+
source: "consolidation",
|
|
1476
|
+
sessionKey: survivor.sessionKey,
|
|
1477
|
+
createdAt: survivor.createdAt,
|
|
1478
|
+
embedding: survivor.embedding,
|
|
1479
|
+
expiresAt: survivor.expiresAt,
|
|
1480
|
+
recallCount: survivor.recallCount
|
|
1481
|
+
});
|
|
1482
|
+
merged += similar.length;
|
|
1483
|
+
for (const r of similar) {
|
|
1484
|
+
try {
|
|
1485
|
+
await this.store.delete(r.entry.id);
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
this.onEvent?.({ type: "consolidate:failed", error: `delete:${err.message}` });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
processed.add(survivor.id);
|
|
1491
|
+
}
|
|
1492
|
+
let pruned = 0;
|
|
1493
|
+
try {
|
|
1494
|
+
pruned += await this.store.prune({
|
|
1495
|
+
maxAgeMs: options.maxAgeMs,
|
|
1496
|
+
minConfidence: options.minConfidence ?? 0.1
|
|
1497
|
+
});
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
this.onEvent?.({ type: "consolidate:failed", error: err.message });
|
|
1500
|
+
}
|
|
1501
|
+
if (options.maxMemories != null) {
|
|
1502
|
+
const after = await this.store.list();
|
|
1503
|
+
if (after.length > options.maxMemories) {
|
|
1504
|
+
const sorted = [...after].sort(
|
|
1505
|
+
(a, b) => b.confidence - a.confidence || b.updatedAt - a.updatedAt
|
|
1506
|
+
);
|
|
1507
|
+
const toRemove = sorted.slice(options.maxMemories);
|
|
1508
|
+
for (const e of toRemove) {
|
|
1509
|
+
const ok = await this.store.delete(e.id);
|
|
1510
|
+
if (ok) pruned++;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
this.store.markConsolidated();
|
|
1515
|
+
const ms = Date.now() - start;
|
|
1516
|
+
this.onEvent?.({ type: "consolidate", merged, pruned, ms });
|
|
1517
|
+
return { merged, pruned };
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
// src/tools/memory-tools.ts
|
|
1522
|
+
var MAX_CONTENT_CHARS = 2e3;
|
|
1523
|
+
var MAX_CONCEPTS = 20;
|
|
1524
|
+
var MAX_LIST_LIMIT = 200;
|
|
1525
|
+
var MAX_SEARCH_LIMIT = 50;
|
|
1526
|
+
function clampInt(v, fallback, min, max) {
|
|
1527
|
+
const n = typeof v === "number" && Number.isFinite(v) ? Math.floor(v) : fallback;
|
|
1528
|
+
return Math.min(max, Math.max(min, n));
|
|
1529
|
+
}
|
|
1530
|
+
function createMemoryTools(store, options = {}) {
|
|
1531
|
+
const listLimit = clampInt(options.listLimit, 20, 1, MAX_LIST_LIMIT);
|
|
1532
|
+
const searchLimit = clampInt(options.searchLimit, 5, 1, MAX_SEARCH_LIMIT);
|
|
1533
|
+
const saveTtlMs = options.saveTtlMs;
|
|
1534
|
+
const saveMemory = {
|
|
1535
|
+
name: "save_memory",
|
|
1536
|
+
description: "\u4FDD\u5B58\u4E00\u6761\u957F\u671F\u8BB0\u5FC6\uFF08\u8DE8\u4F1A\u8BDD\u4FDD\u7559\uFF09\u3002\u7528\u4E8E\u8BB0\u5F55\u7528\u6237\u504F\u597D\u3001\u5173\u952E\u51B3\u7B56\u3001\u4E8B\u5B9E\u7EA6\u675F\u3001\u67B6\u6784\u7EA6\u5B9A\u7B49\uFF0C\u4EE5\u4FBF\u540E\u7EED\u4F1A\u8BDD\u81EA\u52A8\u68C0\u7D22\u6CE8\u5165\u3002content \u4E3A\u8BB0\u5FC6\u6B63\u6587\uFF0Cconcepts \u4E3A\u53EF\u9009\u5173\u952E\u8BCD\u6807\u7B7E\u3002",
|
|
1537
|
+
parameters: {
|
|
1538
|
+
type: "object",
|
|
1539
|
+
properties: {
|
|
1540
|
+
content: { type: "string", description: "\u8BB0\u5FC6\u6B63\u6587" },
|
|
1541
|
+
concepts: {
|
|
1542
|
+
type: "array",
|
|
1543
|
+
items: { type: "string" },
|
|
1544
|
+
description: "\u53EF\u9009\u5173\u952E\u8BCD / \u6982\u5FF5\u6807\u7B7E"
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
required: ["content"]
|
|
1548
|
+
},
|
|
1549
|
+
async execute(_toolCallId, args) {
|
|
1550
|
+
const a = args ?? {};
|
|
1551
|
+
if (!a.content || !a.content.trim()) {
|
|
1552
|
+
return {
|
|
1553
|
+
content: [createTextContent("save_memory \u5931\u8D25\uFF1Acontent \u4E0D\u80FD\u4E3A\u7A7A")],
|
|
1554
|
+
details: { error: "empty content" }
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const content = a.content.trim().slice(0, MAX_CONTENT_CHARS);
|
|
1558
|
+
const concepts = (a.concepts ?? []).filter((c) => typeof c === "string" && c.length > 0).map((c) => c.trim().slice(0, 50)).slice(0, MAX_CONCEPTS);
|
|
1559
|
+
return store.save({
|
|
1560
|
+
content,
|
|
1561
|
+
concepts,
|
|
1562
|
+
confidence: 0.7,
|
|
1563
|
+
source: "tool",
|
|
1564
|
+
ttlMs: saveTtlMs
|
|
1565
|
+
}).then((entry) => ({
|
|
1566
|
+
content: [createTextContent(`\u5DF2\u4FDD\u5B58\u8BB0\u5FC6\uFF08id=${entry.id}\uFF09`)],
|
|
1567
|
+
details: { id: entry.id, saved: true }
|
|
1568
|
+
}));
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
const searchMemory = {
|
|
1572
|
+
name: "search_memory",
|
|
1573
|
+
description: "\u68C0\u7D22\u4E0E\u67E5\u8BE2\u76F8\u5173\u7684\u957F\u671F\u8BB0\u5FC6\uFF08BM25 \u5173\u952E\u8BCD + \u53EF\u9009\u5411\u91CF\u6DF7\u5408\u68C0\u7D22\uFF09\u3002\u8FD4\u56DE\u6309\u76F8\u5173\u5EA6\u6392\u5E8F\u7684\u8BB0\u5FC6\u5217\u8868\u3002",
|
|
1574
|
+
parameters: {
|
|
1575
|
+
type: "object",
|
|
1576
|
+
properties: {
|
|
1577
|
+
query: { type: "string", description: "\u68C0\u7D22\u67E5\u8BE2" },
|
|
1578
|
+
limit: { type: "integer", description: "\u8FD4\u56DE\u4E0A\u9650\uFF08\u9ED8\u8BA4 5\uFF09" }
|
|
1579
|
+
},
|
|
1580
|
+
required: ["query"]
|
|
1581
|
+
},
|
|
1582
|
+
async execute(_toolCallId, args) {
|
|
1583
|
+
const a = args ?? {};
|
|
1584
|
+
if (!a.query || !a.query.trim()) {
|
|
1585
|
+
return {
|
|
1586
|
+
content: [createTextContent("search_memory \u5931\u8D25\uFF1Aquery \u4E0D\u80FD\u4E3A\u7A7A")],
|
|
1587
|
+
details: { error: "empty query" }
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
const limit = clampInt(a.limit, searchLimit, 1, MAX_SEARCH_LIMIT);
|
|
1591
|
+
const results = await store.search(a.query.trim(), limit);
|
|
1592
|
+
if (results.length === 0) {
|
|
1593
|
+
return {
|
|
1594
|
+
content: [createTextContent("\u672A\u627E\u5230\u76F8\u5173\u8BB0\u5FC6\u3002")],
|
|
1595
|
+
details: { count: 0 }
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
const lines = results.map(
|
|
1599
|
+
(r, i) => `${i + 1}. [score=${r.score.toFixed(2)} id=${r.entry.id}] ${r.entry.content.replace(/\s+/g, " ").slice(0, 200)}`
|
|
1600
|
+
);
|
|
1601
|
+
return {
|
|
1602
|
+
content: [createTextContent(`\u627E\u5230 ${results.length} \u6761\u76F8\u5173\u8BB0\u5FC6\uFF1A
|
|
1603
|
+
${lines.join("\n")}`)],
|
|
1604
|
+
details: { count: results.length, ids: results.map((r) => r.entry.id) }
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
const listMemories = {
|
|
1609
|
+
name: "list_memories",
|
|
1610
|
+
description: "\u5217\u51FA\u6700\u8FD1\u7684\u957F\u671F\u8BB0\u5FC6\uFF08\u6309\u66F4\u65B0\u65F6\u95F4\u5012\u5E8F\uFF09\u3002",
|
|
1611
|
+
parameters: {
|
|
1612
|
+
type: "object",
|
|
1613
|
+
properties: {
|
|
1614
|
+
limit: { type: "integer", description: "\u8FD4\u56DE\u4E0A\u9650\uFF08\u9ED8\u8BA4 20\uFF09" }
|
|
1615
|
+
}
|
|
1616
|
+
},
|
|
1617
|
+
async execute(_toolCallId, args) {
|
|
1618
|
+
const a = args ?? {};
|
|
1619
|
+
const limit = clampInt(a.limit, listLimit, 1, MAX_LIST_LIMIT);
|
|
1620
|
+
const entries = await store.list(limit);
|
|
1621
|
+
if (entries.length === 0) {
|
|
1622
|
+
return {
|
|
1623
|
+
content: [createTextContent("\u5F53\u524D\u65E0\u4EFB\u4F55\u8BB0\u5FC6\u3002")],
|
|
1624
|
+
details: { count: 0 }
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
const lines = entries.map(
|
|
1628
|
+
(e, i) => `${i + 1}. [id=${e.id} conf=${e.confidence.toFixed(2)}] ${e.content.replace(/\s+/g, " ").slice(0, 200)}`
|
|
1629
|
+
);
|
|
1630
|
+
return {
|
|
1631
|
+
content: [
|
|
1632
|
+
createTextContent(`\u5171 ${entries.length} \u6761\u8BB0\u5FC6\uFF1A
|
|
1633
|
+
${lines.join("\n")}`)
|
|
1634
|
+
],
|
|
1635
|
+
details: { count: entries.length, ids: entries.map((e) => e.id) }
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
const deleteMemory = {
|
|
1640
|
+
name: "delete_memory",
|
|
1641
|
+
description: "\u6309 id \u5220\u9664\u4E00\u6761\u957F\u671F\u8BB0\u5FC6\u3002",
|
|
1642
|
+
parameters: {
|
|
1643
|
+
type: "object",
|
|
1644
|
+
properties: {
|
|
1645
|
+
id: { type: "string", description: "\u8BB0\u5FC6 id" }
|
|
1646
|
+
},
|
|
1647
|
+
required: ["id"]
|
|
1648
|
+
},
|
|
1649
|
+
async execute(_toolCallId, args) {
|
|
1650
|
+
const a = args ?? {};
|
|
1651
|
+
if (!a.id) {
|
|
1652
|
+
return {
|
|
1653
|
+
content: [createTextContent("delete_memory \u5931\u8D25\uFF1Aid \u4E0D\u80FD\u4E3A\u7A7A")],
|
|
1654
|
+
details: { error: "empty id" }
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
const ok = await store.delete(a.id);
|
|
1658
|
+
return {
|
|
1659
|
+
content: [
|
|
1660
|
+
createTextContent(ok ? `\u5DF2\u5220\u9664\u8BB0\u5FC6\uFF08id=${a.id}\uFF09` : `\u672A\u627E\u5230\u8BB0\u5FC6\uFF08id=${a.id}\uFF09`)
|
|
1661
|
+
],
|
|
1662
|
+
details: { id: a.id, deleted: ok }
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
return [saveMemory, searchMemory, listMemories, deleteMemory];
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/plugin.ts
|
|
1670
|
+
var StoreBackedRetriever = class {
|
|
1671
|
+
constructor(store) {
|
|
1672
|
+
this.store = store;
|
|
1673
|
+
}
|
|
1674
|
+
store;
|
|
1675
|
+
async search(query, limit = 5) {
|
|
1676
|
+
return this.store.search(query, limit);
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
var StoreVectorSource = class {
|
|
1680
|
+
constructor(store) {
|
|
1681
|
+
this.store = store;
|
|
1682
|
+
}
|
|
1683
|
+
store;
|
|
1684
|
+
async searchVectors(queryVec, limit = 5) {
|
|
1685
|
+
return this.store.searchVectors(queryVec, limit);
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
var FAILURE_EVENTS = /* @__PURE__ */ new Set([
|
|
1689
|
+
"store:corrupt",
|
|
1690
|
+
"embedding:error",
|
|
1691
|
+
"capture:failed",
|
|
1692
|
+
"consolidate:failed"
|
|
1693
|
+
]);
|
|
1694
|
+
function defaultEventSink(event) {
|
|
1695
|
+
if (FAILURE_EVENTS.has(event.type)) {
|
|
1696
|
+
console.warn(`[aipack-memory] ${event.type}: ${event.error ?? ""}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function isCaptureEnabled(v) {
|
|
1700
|
+
if (v === false) return false;
|
|
1701
|
+
return true;
|
|
1702
|
+
}
|
|
1703
|
+
function isInjectEnabled(v) {
|
|
1704
|
+
if (v === false) return false;
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1707
|
+
function resolveCaptureOptions(v, fallback) {
|
|
1708
|
+
if (typeof v === "object" && v !== null) {
|
|
1709
|
+
return {
|
|
1710
|
+
...v,
|
|
1711
|
+
summarizeFn: v.summarizeFn ?? fallback.summarizeFn,
|
|
1712
|
+
consolidateEvery: v.consolidateEvery ?? fallback.consolidateEvery,
|
|
1713
|
+
ttlMs: v.ttlMs ?? fallback.ttlMs,
|
|
1714
|
+
onEvent: v.onEvent ?? fallback.onEvent
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
return {
|
|
1718
|
+
summarizeFn: fallback.summarizeFn,
|
|
1719
|
+
consolidateEvery: fallback.consolidateEvery,
|
|
1720
|
+
ttlMs: fallback.ttlMs,
|
|
1721
|
+
onEvent: fallback.onEvent
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function resolveInjectOptions(v, fallback) {
|
|
1725
|
+
if (typeof v === "object" && v !== null) {
|
|
1726
|
+
return {
|
|
1727
|
+
...v,
|
|
1728
|
+
maxMemories: v.maxMemories ?? fallback.maxMemories,
|
|
1729
|
+
minScore: v.minScore ?? fallback.minScore
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
return {
|
|
1733
|
+
maxMemories: fallback.maxMemories,
|
|
1734
|
+
minScore: fallback.minScore
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function createMemoryPlugin(options = {}) {
|
|
1738
|
+
const embedder = options.embedder;
|
|
1739
|
+
const onEvent = options.onEvent ?? defaultEventSink;
|
|
1740
|
+
const store = options.store ?? new FileMemoryStore({
|
|
1741
|
+
baseDir: options.baseDir,
|
|
1742
|
+
embedder,
|
|
1743
|
+
onEvent
|
|
1744
|
+
});
|
|
1745
|
+
const bm25Source = new StoreBackedRetriever(store);
|
|
1746
|
+
const retriever = new HybridRetriever({
|
|
1747
|
+
bm25: bm25Source,
|
|
1748
|
+
vector: embedder ? new StoreVectorSource(store) : void 0,
|
|
1749
|
+
embedder,
|
|
1750
|
+
minScore: options.minScore ?? 0.1
|
|
1751
|
+
});
|
|
1752
|
+
const consolidator = new Consolidator(store, retriever, {
|
|
1753
|
+
similarityThreshold: 0.85,
|
|
1754
|
+
onEvent
|
|
1755
|
+
});
|
|
1756
|
+
store.setConsolidator(consolidator);
|
|
1757
|
+
const extensions = [];
|
|
1758
|
+
if (isCaptureEnabled(options.capture)) {
|
|
1759
|
+
const captureOpts = resolveCaptureOptions(options.capture, {
|
|
1760
|
+
summarizeFn: options.summarizeFn,
|
|
1761
|
+
consolidateEvery: options.consolidateEvery,
|
|
1762
|
+
ttlMs: options.captureTtlMs,
|
|
1763
|
+
onEvent
|
|
1764
|
+
});
|
|
1765
|
+
extensions.push(new MemoryCaptureExtension(store, captureOpts));
|
|
1766
|
+
}
|
|
1767
|
+
const transformers = [];
|
|
1768
|
+
if (isInjectEnabled(options.inject)) {
|
|
1769
|
+
const injectOpts = resolveInjectOptions(options.inject, {
|
|
1770
|
+
maxMemories: options.maxMemories,
|
|
1771
|
+
minScore: options.minScore
|
|
1772
|
+
});
|
|
1773
|
+
transformers.push(
|
|
1774
|
+
new MemoryInjectionTransformer(retriever, {
|
|
1775
|
+
...injectOpts,
|
|
1776
|
+
// 命中后更新检索统计(fire-and-forget,不阻塞注入);
|
|
1777
|
+
// 失败不静默:告警日志便于排障(检索统计丢失不致命)
|
|
1778
|
+
onRecall: async (ids) => {
|
|
1779
|
+
await Promise.allSettled(ids.map(async (id) => {
|
|
1780
|
+
try {
|
|
1781
|
+
await store.touchRecall(id);
|
|
1782
|
+
} catch (err) {
|
|
1783
|
+
console.warn(`[memory] touchRecall \u5931\u8D25\uFF08id=${id}\uFF09: ${err.message}`);
|
|
1784
|
+
}
|
|
1785
|
+
}));
|
|
1786
|
+
}
|
|
1787
|
+
})
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
const tools = [];
|
|
1791
|
+
if (options.tools !== false) {
|
|
1792
|
+
tools.push(...createMemoryTools(store, { saveTtlMs: options.toolTtlMs }));
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
store,
|
|
1796
|
+
retriever,
|
|
1797
|
+
extensions,
|
|
1798
|
+
transformers,
|
|
1799
|
+
tools,
|
|
1800
|
+
install() {
|
|
1801
|
+
return { extensions, transformers, tools };
|
|
1802
|
+
},
|
|
1803
|
+
dispose() {
|
|
1804
|
+
store.dispose();
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
export {
|
|
1809
|
+
BM25Index,
|
|
1810
|
+
BM25Retriever,
|
|
1811
|
+
Consolidator,
|
|
1812
|
+
FileMemoryStore,
|
|
1813
|
+
HybridRetriever,
|
|
1814
|
+
InMemoryStore,
|
|
1815
|
+
MEMORY_BLOCK_END,
|
|
1816
|
+
MEMORY_BLOCK_START,
|
|
1817
|
+
MemoryCaptureExtension,
|
|
1818
|
+
MemoryIndex,
|
|
1819
|
+
MemoryInjectionTransformer,
|
|
1820
|
+
STOPWORDS,
|
|
1821
|
+
VectorIndex,
|
|
1822
|
+
buildMemoryBlock,
|
|
1823
|
+
cosine,
|
|
1824
|
+
createFileMemoryStore,
|
|
1825
|
+
createInMemoryStore,
|
|
1826
|
+
createMemoryPlugin,
|
|
1827
|
+
createMemoryTools,
|
|
1828
|
+
extractConcepts,
|
|
1829
|
+
extractFromTurn,
|
|
1830
|
+
finalizeEntry,
|
|
1831
|
+
hasMemoryBlock,
|
|
1832
|
+
isCJK,
|
|
1833
|
+
minMaxNormalize,
|
|
1834
|
+
runCaptureExtractor,
|
|
1835
|
+
stripMemoryBlock,
|
|
1836
|
+
tokenize,
|
|
1837
|
+
wrapMemoryBlock
|
|
1838
|
+
};
|
|
1839
|
+
//# sourceMappingURL=index.js.map
|