@lov3kaizen/agentsea-memory 0.5.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 +450 -0
- package/dist/chunk-GACX3FPR.js +1402 -0
- package/dist/chunk-M44NB53O.js +1226 -0
- package/dist/chunk-MQDWBPZU.js +972 -0
- package/dist/chunk-TPC7MYWK.js +1495 -0
- package/dist/chunk-XD2CQGSD.js +1540 -0
- package/dist/chunk-YI7RPDEV.js +1215 -0
- package/dist/core.types-lkxKv-bW.d.cts +242 -0
- package/dist/core.types-lkxKv-bW.d.ts +242 -0
- package/dist/debug/index.cjs +1248 -0
- package/dist/debug/index.d.cts +3 -0
- package/dist/debug/index.d.ts +3 -0
- package/dist/debug/index.js +20 -0
- package/dist/index-7SsAJ4et.d.ts +525 -0
- package/dist/index-BGxYqpFb.d.cts +601 -0
- package/dist/index-BX62efZu.d.ts +565 -0
- package/dist/index-Bbc3COw0.d.cts +748 -0
- package/dist/index-Bczz1Eyk.d.ts +637 -0
- package/dist/index-C7pEiT8L.d.cts +637 -0
- package/dist/index-CHetLTb0.d.ts +389 -0
- package/dist/index-CloeiFyx.d.ts +748 -0
- package/dist/index-DNOhq-3y.d.cts +525 -0
- package/dist/index-Da-M8FOV.d.cts +389 -0
- package/dist/index-Dy8UjRFz.d.cts +565 -0
- package/dist/index-aVcITW0B.d.ts +601 -0
- package/dist/index.cjs +8554 -0
- package/dist/index.d.cts +293 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +742 -0
- package/dist/processing/index.cjs +1575 -0
- package/dist/processing/index.d.cts +2 -0
- package/dist/processing/index.d.ts +2 -0
- package/dist/processing/index.js +24 -0
- package/dist/retrieval/index.cjs +1262 -0
- package/dist/retrieval/index.d.cts +2 -0
- package/dist/retrieval/index.d.ts +2 -0
- package/dist/retrieval/index.js +26 -0
- package/dist/sharing/index.cjs +1003 -0
- package/dist/sharing/index.d.cts +3 -0
- package/dist/sharing/index.d.ts +3 -0
- package/dist/sharing/index.js +16 -0
- package/dist/stores/index.cjs +1445 -0
- package/dist/stores/index.d.cts +2 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/index.js +20 -0
- package/dist/structures/index.cjs +1530 -0
- package/dist/structures/index.d.cts +3 -0
- package/dist/structures/index.d.ts +3 -0
- package/dist/structures/index.js +24 -0
- package/package.json +141 -0
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
// src/processing/Summarizer.ts
|
|
2
|
+
var Summarizer = class {
|
|
3
|
+
config;
|
|
4
|
+
summaryFn;
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.config = {
|
|
7
|
+
provider: config.provider ?? void 0,
|
|
8
|
+
model: config.model ?? "default",
|
|
9
|
+
strategy: config.strategy ?? "abstractive",
|
|
10
|
+
maxLength: config.maxLength ?? 500,
|
|
11
|
+
preserveEntities: config.preserveEntities ?? true,
|
|
12
|
+
focusPrompt: config.focusPrompt ?? "",
|
|
13
|
+
maxSummaryLength: config.maxSummaryLength ?? 500,
|
|
14
|
+
minEntriesForSummary: config.minEntriesForSummary ?? 3,
|
|
15
|
+
preserveKeyEntities: config.preserveKeyEntities ?? true,
|
|
16
|
+
summaryStyle: config.summaryStyle ?? "concise"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Set custom summary function (for LLM integration)
|
|
21
|
+
*/
|
|
22
|
+
setSummaryFunction(fn) {
|
|
23
|
+
this.summaryFn = fn;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Summarize a collection of memories
|
|
27
|
+
*/
|
|
28
|
+
async summarize(entries) {
|
|
29
|
+
if (entries.length < this.config.minEntriesForSummary) {
|
|
30
|
+
return Promise.resolve(this.createSimpleSummary(entries));
|
|
31
|
+
}
|
|
32
|
+
if (this.summaryFn) {
|
|
33
|
+
const summary = await this.summaryFn(entries, {
|
|
34
|
+
maxLength: this.config.maxSummaryLength,
|
|
35
|
+
style: this.config.summaryStyle
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
summary,
|
|
39
|
+
keyPoints: this.extractKeyPoints(entries),
|
|
40
|
+
sourceCount: entries.length,
|
|
41
|
+
compressionRatio: this.calculateCompressionRatio(entries, summary),
|
|
42
|
+
metadata: {
|
|
43
|
+
method: "llm",
|
|
44
|
+
style: this.config.summaryStyle
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve(this.heuristicSummarize(entries));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Summarize memories by time period
|
|
52
|
+
*/
|
|
53
|
+
async summarizeByPeriod(entries, period) {
|
|
54
|
+
const grouped = this.groupByPeriod(entries, period);
|
|
55
|
+
const results = /* @__PURE__ */ new Map();
|
|
56
|
+
for (const [key, groupEntries] of grouped) {
|
|
57
|
+
const summary = await this.summarize(groupEntries);
|
|
58
|
+
results.set(key, summary);
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Summarize memories by topic/type
|
|
64
|
+
*/
|
|
65
|
+
async summarizeByTopic(entries) {
|
|
66
|
+
const grouped = this.groupByTopic(entries);
|
|
67
|
+
const results = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const [topic, topicEntries] of grouped) {
|
|
69
|
+
const summary = await this.summarize(topicEntries);
|
|
70
|
+
results.set(topic, summary);
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create incremental summary (add to existing summary)
|
|
76
|
+
*/
|
|
77
|
+
async incrementalSummarize(existingSummary, newEntries) {
|
|
78
|
+
if (this.summaryFn) {
|
|
79
|
+
const contextEntry = {
|
|
80
|
+
id: "context",
|
|
81
|
+
content: `Previous summary: ${existingSummary}`,
|
|
82
|
+
type: "summary",
|
|
83
|
+
importance: 0.8,
|
|
84
|
+
metadata: {
|
|
85
|
+
source: "system",
|
|
86
|
+
confidence: 1
|
|
87
|
+
},
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
accessCount: 0,
|
|
90
|
+
createdAt: Date.now(),
|
|
91
|
+
updatedAt: Date.now()
|
|
92
|
+
};
|
|
93
|
+
const summary = await this.summaryFn([contextEntry, ...newEntries], {
|
|
94
|
+
maxLength: this.config.maxSummaryLength,
|
|
95
|
+
style: this.config.summaryStyle
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
summary,
|
|
99
|
+
keyPoints: this.extractKeyPoints(newEntries),
|
|
100
|
+
sourceCount: newEntries.length,
|
|
101
|
+
compressionRatio: this.calculateCompressionRatio(newEntries, summary),
|
|
102
|
+
metadata: {
|
|
103
|
+
method: "incremental",
|
|
104
|
+
hadExistingSummary: true
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const newPoints = this.extractKeyPoints(newEntries);
|
|
109
|
+
const combinedSummary = `${existingSummary} Additionally: ${newPoints.join("; ")}.`;
|
|
110
|
+
return {
|
|
111
|
+
summary: combinedSummary.slice(0, this.config.maxSummaryLength),
|
|
112
|
+
keyPoints: newPoints,
|
|
113
|
+
sourceCount: newEntries.length,
|
|
114
|
+
compressionRatio: this.calculateCompressionRatio(
|
|
115
|
+
newEntries,
|
|
116
|
+
combinedSummary
|
|
117
|
+
),
|
|
118
|
+
metadata: {
|
|
119
|
+
method: "heuristic-incremental"
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Heuristic-based summarization
|
|
125
|
+
*/
|
|
126
|
+
heuristicSummarize(entries) {
|
|
127
|
+
const sorted = [...entries].sort((a, b) => b.importance - a.importance);
|
|
128
|
+
const keyPoints = this.extractKeyPoints(sorted);
|
|
129
|
+
const typeGroups = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
typeGroups.set(entry.type, (typeGroups.get(entry.type) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
const typesSummary = Array.from(typeGroups.entries()).map(([type, count]) => `${count} ${type}(s)`).join(", ");
|
|
134
|
+
const timeRange = this.getTimeRange(entries);
|
|
135
|
+
const summary = `Summary of ${entries.length} memories (${typesSummary}) from ${timeRange}: ${keyPoints.slice(0, 3).join(". ")}.`;
|
|
136
|
+
return {
|
|
137
|
+
summary: summary.slice(0, this.config.maxSummaryLength),
|
|
138
|
+
keyPoints,
|
|
139
|
+
sourceCount: entries.length,
|
|
140
|
+
compressionRatio: this.calculateCompressionRatio(entries, summary),
|
|
141
|
+
metadata: {
|
|
142
|
+
method: "heuristic",
|
|
143
|
+
typeDistribution: Object.fromEntries(typeGroups)
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create simple summary for few entries
|
|
149
|
+
*/
|
|
150
|
+
createSimpleSummary(entries) {
|
|
151
|
+
const summary = entries.map((e) => e.content.slice(0, 100)).join("; ");
|
|
152
|
+
return {
|
|
153
|
+
summary: summary.slice(0, this.config.maxSummaryLength),
|
|
154
|
+
keyPoints: entries.map((e) => e.content.slice(0, 50)),
|
|
155
|
+
sourceCount: entries.length,
|
|
156
|
+
compressionRatio: 1,
|
|
157
|
+
metadata: {
|
|
158
|
+
method: "simple"
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Extract key points from entries
|
|
164
|
+
*/
|
|
165
|
+
extractKeyPoints(entries) {
|
|
166
|
+
const points = [];
|
|
167
|
+
for (const entry of entries.slice(0, 10)) {
|
|
168
|
+
const firstSentence = entry.content.split(/[.!?]/)[0];
|
|
169
|
+
if (firstSentence && firstSentence.length > 10) {
|
|
170
|
+
points.push(firstSentence.trim());
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return this.deduplicateStrings(points);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Group entries by time period
|
|
177
|
+
*/
|
|
178
|
+
groupByPeriod(entries, period) {
|
|
179
|
+
const groups = /* @__PURE__ */ new Map();
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const date = new Date(entry.timestamp);
|
|
182
|
+
let key;
|
|
183
|
+
switch (period) {
|
|
184
|
+
case "hour":
|
|
185
|
+
key = `${date.toISOString().slice(0, 13)}:00`;
|
|
186
|
+
break;
|
|
187
|
+
case "day":
|
|
188
|
+
key = date.toISOString().slice(0, 10);
|
|
189
|
+
break;
|
|
190
|
+
case "week": {
|
|
191
|
+
const weekStart = new Date(date);
|
|
192
|
+
weekStart.setDate(date.getDate() - date.getDay());
|
|
193
|
+
key = `week-${weekStart.toISOString().slice(0, 10)}`;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!groups.has(key)) {
|
|
198
|
+
groups.set(key, []);
|
|
199
|
+
}
|
|
200
|
+
groups.get(key).push(entry);
|
|
201
|
+
}
|
|
202
|
+
return groups;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Group entries by topic/type
|
|
206
|
+
*/
|
|
207
|
+
groupByTopic(entries) {
|
|
208
|
+
const groups = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const topic = String(entry.metadata.topic ?? entry.type);
|
|
211
|
+
if (!groups.has(topic)) {
|
|
212
|
+
groups.set(topic, []);
|
|
213
|
+
}
|
|
214
|
+
groups.get(topic).push(entry);
|
|
215
|
+
}
|
|
216
|
+
return groups;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get time range description
|
|
220
|
+
*/
|
|
221
|
+
getTimeRange(entries) {
|
|
222
|
+
if (entries.length === 0) return "unknown period";
|
|
223
|
+
const timestamps = entries.map((e) => e.timestamp);
|
|
224
|
+
const start = new Date(Math.min(...timestamps));
|
|
225
|
+
const end = new Date(Math.max(...timestamps));
|
|
226
|
+
const diffMs = end.getTime() - start.getTime();
|
|
227
|
+
const diffHours = diffMs / (1e3 * 60 * 60);
|
|
228
|
+
if (diffHours < 1) {
|
|
229
|
+
return "the last hour";
|
|
230
|
+
} else if (diffHours < 24) {
|
|
231
|
+
return "today";
|
|
232
|
+
} else if (diffHours < 168) {
|
|
233
|
+
return "this week";
|
|
234
|
+
} else {
|
|
235
|
+
return `${start.toLocaleDateString()} to ${end.toLocaleDateString()}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Calculate compression ratio
|
|
240
|
+
*/
|
|
241
|
+
calculateCompressionRatio(entries, summary) {
|
|
242
|
+
const originalLength = entries.reduce(
|
|
243
|
+
(sum, e) => sum + e.content.length,
|
|
244
|
+
0
|
|
245
|
+
);
|
|
246
|
+
return originalLength > 0 ? originalLength / summary.length : 1;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Deduplicate similar strings
|
|
250
|
+
*/
|
|
251
|
+
deduplicateStrings(strings) {
|
|
252
|
+
const result = [];
|
|
253
|
+
for (const str of strings) {
|
|
254
|
+
const isDuplicate = result.some(
|
|
255
|
+
(existing) => this.stringSimilarity(str, existing) > 0.8
|
|
256
|
+
);
|
|
257
|
+
if (!isDuplicate) {
|
|
258
|
+
result.push(str);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Simple string similarity (Jaccard)
|
|
265
|
+
*/
|
|
266
|
+
stringSimilarity(a, b) {
|
|
267
|
+
const setA = new Set(a.toLowerCase().split(/\s+/));
|
|
268
|
+
const setB = new Set(b.toLowerCase().split(/\s+/));
|
|
269
|
+
const intersection = new Set([...setA].filter((x) => setB.has(x)));
|
|
270
|
+
const union = /* @__PURE__ */ new Set([...setA, ...setB]);
|
|
271
|
+
return intersection.size / union.size;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Update configuration
|
|
275
|
+
*/
|
|
276
|
+
configure(config) {
|
|
277
|
+
this.config = { ...this.config, ...config };
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
function createSummarizer(config) {
|
|
281
|
+
return new Summarizer(config);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/processing/Compressor.ts
|
|
285
|
+
var Compressor = class {
|
|
286
|
+
config;
|
|
287
|
+
constructor(config = {}) {
|
|
288
|
+
this.config = {
|
|
289
|
+
targetRatio: config.targetRatio ?? 0.5,
|
|
290
|
+
preserveImportant: config.preserveImportant ?? true,
|
|
291
|
+
strategy: config.strategy ?? "importance-weighted",
|
|
292
|
+
minImportance: config.minImportance ?? 0.3,
|
|
293
|
+
minContentLength: config.minContentLength ?? 50,
|
|
294
|
+
removeEmbeddings: config.removeEmbeddings ?? false,
|
|
295
|
+
truncateMetadata: config.truncateMetadata ?? true
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Compress a single memory entry
|
|
300
|
+
*/
|
|
301
|
+
compress(entry) {
|
|
302
|
+
const originalSize = this.calculateSize(entry);
|
|
303
|
+
const preservedFields = [];
|
|
304
|
+
const compressed = { ...entry };
|
|
305
|
+
if (entry.content.length > this.config.minContentLength) {
|
|
306
|
+
compressed.content = this.truncateContent(
|
|
307
|
+
entry.content,
|
|
308
|
+
entry.importance
|
|
309
|
+
);
|
|
310
|
+
preservedFields.push("content (truncated)");
|
|
311
|
+
} else {
|
|
312
|
+
preservedFields.push("content");
|
|
313
|
+
}
|
|
314
|
+
if (this.config.removeEmbeddings && entry.embedding) {
|
|
315
|
+
delete compressed.embedding;
|
|
316
|
+
} else if (entry.embedding) {
|
|
317
|
+
preservedFields.push("embedding");
|
|
318
|
+
}
|
|
319
|
+
if (this.config.truncateMetadata) {
|
|
320
|
+
compressed.metadata = this.truncateMetadata(entry.metadata);
|
|
321
|
+
preservedFields.push("metadata (truncated)");
|
|
322
|
+
} else {
|
|
323
|
+
preservedFields.push("metadata");
|
|
324
|
+
}
|
|
325
|
+
const compressedSize = this.calculateSize(compressed);
|
|
326
|
+
return {
|
|
327
|
+
compressed,
|
|
328
|
+
originalSize,
|
|
329
|
+
compressedSize,
|
|
330
|
+
ratio: compressedSize / originalSize,
|
|
331
|
+
preservedFields
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Compress multiple entries
|
|
336
|
+
*/
|
|
337
|
+
compressBatch(entries) {
|
|
338
|
+
let totalOriginalSize = 0;
|
|
339
|
+
let totalCompressedSize = 0;
|
|
340
|
+
let removedCount = 0;
|
|
341
|
+
const compressedEntries = [];
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
const originalSize = this.calculateSize(entry);
|
|
344
|
+
totalOriginalSize += originalSize;
|
|
345
|
+
if (this.config.preserveImportant && entry.importance < 0.2) {
|
|
346
|
+
removedCount++;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const result = this.compress(entry);
|
|
350
|
+
compressedEntries.push(result.compressed);
|
|
351
|
+
totalCompressedSize += result.compressedSize;
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
entries: compressedEntries,
|
|
355
|
+
totalOriginalSize,
|
|
356
|
+
totalCompressedSize,
|
|
357
|
+
avgRatio: totalOriginalSize > 0 ? totalCompressedSize / totalOriginalSize : 1,
|
|
358
|
+
removedCount
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Compress entries to target size
|
|
363
|
+
*/
|
|
364
|
+
compressToSize(entries, targetSize) {
|
|
365
|
+
const sorted = [...entries].sort((a, b) => b.importance - a.importance);
|
|
366
|
+
let currentSize = 0;
|
|
367
|
+
const result = [];
|
|
368
|
+
let removedCount = 0;
|
|
369
|
+
for (const entry of sorted) {
|
|
370
|
+
const compressed = this.compress(entry);
|
|
371
|
+
if (currentSize + compressed.compressedSize <= targetSize) {
|
|
372
|
+
result.push(compressed.compressed);
|
|
373
|
+
currentSize += compressed.compressedSize;
|
|
374
|
+
} else {
|
|
375
|
+
const aggressive = this.aggressiveCompress(entry);
|
|
376
|
+
if (currentSize + this.calculateSize(aggressive) <= targetSize) {
|
|
377
|
+
result.push(aggressive);
|
|
378
|
+
currentSize += this.calculateSize(aggressive);
|
|
379
|
+
} else {
|
|
380
|
+
removedCount++;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
entries: result,
|
|
386
|
+
totalOriginalSize: entries.reduce(
|
|
387
|
+
(sum, e) => sum + this.calculateSize(e),
|
|
388
|
+
0
|
|
389
|
+
),
|
|
390
|
+
totalCompressedSize: currentSize,
|
|
391
|
+
avgRatio: currentSize / Math.max(
|
|
392
|
+
entries.reduce((sum, e) => sum + this.calculateSize(e), 0),
|
|
393
|
+
1
|
|
394
|
+
),
|
|
395
|
+
removedCount
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Deduplicate and compress similar entries
|
|
400
|
+
*/
|
|
401
|
+
deduplicateAndCompress(entries) {
|
|
402
|
+
const seen = /* @__PURE__ */ new Map();
|
|
403
|
+
let removedCount = 0;
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
const key = this.getDedupeKey(entry);
|
|
406
|
+
const existing = seen.get(key);
|
|
407
|
+
if (!existing) {
|
|
408
|
+
seen.set(key, entry);
|
|
409
|
+
} else {
|
|
410
|
+
if (entry.importance > existing.importance || entry.importance === existing.importance && entry.timestamp > existing.timestamp) {
|
|
411
|
+
seen.set(key, entry);
|
|
412
|
+
}
|
|
413
|
+
removedCount++;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const unique = Array.from(seen.values());
|
|
417
|
+
const batchResult = this.compressBatch(unique);
|
|
418
|
+
return {
|
|
419
|
+
...batchResult,
|
|
420
|
+
removedCount: removedCount + batchResult.removedCount
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Truncate content based on importance
|
|
425
|
+
*/
|
|
426
|
+
truncateContent(content, importance) {
|
|
427
|
+
const keepRatio = 0.3 + importance * 0.5;
|
|
428
|
+
const targetLength = Math.max(
|
|
429
|
+
this.config.minContentLength,
|
|
430
|
+
Math.floor(content.length * keepRatio)
|
|
431
|
+
);
|
|
432
|
+
if (content.length <= targetLength) {
|
|
433
|
+
return content;
|
|
434
|
+
}
|
|
435
|
+
const truncated = content.slice(0, targetLength);
|
|
436
|
+
const lastSentenceEnd = Math.max(
|
|
437
|
+
truncated.lastIndexOf("."),
|
|
438
|
+
truncated.lastIndexOf("!"),
|
|
439
|
+
truncated.lastIndexOf("?")
|
|
440
|
+
);
|
|
441
|
+
if (lastSentenceEnd > targetLength * 0.5) {
|
|
442
|
+
return truncated.slice(0, lastSentenceEnd + 1);
|
|
443
|
+
}
|
|
444
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
445
|
+
if (lastSpace > targetLength * 0.8) {
|
|
446
|
+
return truncated.slice(0, lastSpace) + "...";
|
|
447
|
+
}
|
|
448
|
+
return truncated + "...";
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Truncate metadata
|
|
452
|
+
*/
|
|
453
|
+
truncateMetadata(metadata) {
|
|
454
|
+
const result = {
|
|
455
|
+
source: metadata.source,
|
|
456
|
+
confidence: metadata.confidence
|
|
457
|
+
};
|
|
458
|
+
const essentialFields = [
|
|
459
|
+
"userId",
|
|
460
|
+
"agentId",
|
|
461
|
+
"conversationId",
|
|
462
|
+
"namespace",
|
|
463
|
+
"tags"
|
|
464
|
+
];
|
|
465
|
+
for (const field of essentialFields) {
|
|
466
|
+
if (field in metadata) {
|
|
467
|
+
result[field] = metadata[field];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Aggressive compression for tight space constraints
|
|
474
|
+
*/
|
|
475
|
+
aggressiveCompress(entry) {
|
|
476
|
+
return {
|
|
477
|
+
...entry,
|
|
478
|
+
content: entry.content.slice(0, 100) + (entry.content.length > 100 ? "..." : ""),
|
|
479
|
+
embedding: void 0,
|
|
480
|
+
metadata: {
|
|
481
|
+
source: entry.metadata.source,
|
|
482
|
+
confidence: entry.metadata.confidence,
|
|
483
|
+
namespace: entry.metadata.namespace
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Get deduplication key for entry
|
|
489
|
+
*/
|
|
490
|
+
getDedupeKey(entry) {
|
|
491
|
+
const contentKey = entry.content.toLowerCase().replace(/[^\w\s]/g, "").slice(0, 100);
|
|
492
|
+
return `${entry.type}:${contentKey}`;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Calculate approximate size of entry in bytes
|
|
496
|
+
*/
|
|
497
|
+
calculateSize(entry) {
|
|
498
|
+
let size = 0;
|
|
499
|
+
size += entry.content.length * 2;
|
|
500
|
+
if (entry.embedding) {
|
|
501
|
+
size += entry.embedding.length * 8;
|
|
502
|
+
}
|
|
503
|
+
size += JSON.stringify(entry.metadata).length * 2;
|
|
504
|
+
size += 200;
|
|
505
|
+
return size;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Update configuration
|
|
509
|
+
*/
|
|
510
|
+
configure(config) {
|
|
511
|
+
this.config = { ...this.config, ...config };
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
function createCompressor(config) {
|
|
515
|
+
return new Compressor(config);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/processing/Consolidator.ts
|
|
519
|
+
var Consolidator = class {
|
|
520
|
+
config;
|
|
521
|
+
summarizer;
|
|
522
|
+
embedFn;
|
|
523
|
+
constructor(config = {}) {
|
|
524
|
+
this.config = {
|
|
525
|
+
similarityThreshold: config.similarityThreshold ?? 0.8,
|
|
526
|
+
mergeStrategy: config.mergeStrategy ?? "confidence-weighted",
|
|
527
|
+
extractRelations: config.extractRelations ?? false,
|
|
528
|
+
maxBatchSize: config.maxBatchSize ?? 100,
|
|
529
|
+
minGroupSize: config.minGroupSize ?? 2,
|
|
530
|
+
maxGroupSize: config.maxGroupSize ?? 20,
|
|
531
|
+
groupingStrategy: config.groupingStrategy ?? "semantic",
|
|
532
|
+
preserveOriginals: config.preserveOriginals ?? false
|
|
533
|
+
};
|
|
534
|
+
this.summarizer = new Summarizer();
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Set embedding function for semantic grouping
|
|
538
|
+
*/
|
|
539
|
+
setEmbeddingFunction(fn) {
|
|
540
|
+
this.embedFn = fn;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Set summarizer function
|
|
544
|
+
*/
|
|
545
|
+
setSummarizerFunction(fn) {
|
|
546
|
+
this.summarizer.setSummaryFunction(fn);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Find and consolidate similar memories
|
|
550
|
+
*/
|
|
551
|
+
async consolidate(entries, store) {
|
|
552
|
+
const groups = await this.groupSimilar(entries);
|
|
553
|
+
const results = [];
|
|
554
|
+
for (const group of groups) {
|
|
555
|
+
if (group.entries.length < this.config.minGroupSize) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const consolidated = await this.consolidateGroup(group);
|
|
559
|
+
results.push(consolidated);
|
|
560
|
+
if (store) {
|
|
561
|
+
await store.add(consolidated.consolidated);
|
|
562
|
+
if (!this.config.preserveOriginals) {
|
|
563
|
+
for (const id of consolidated.sourceIds) {
|
|
564
|
+
await store.delete(id);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return results;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Group similar entries
|
|
573
|
+
*/
|
|
574
|
+
async groupSimilar(entries) {
|
|
575
|
+
switch (this.config.groupingStrategy) {
|
|
576
|
+
case "semantic":
|
|
577
|
+
return Promise.resolve(this.groupBySemantic(entries));
|
|
578
|
+
case "temporal":
|
|
579
|
+
return Promise.resolve(this.groupByTemporal(entries));
|
|
580
|
+
case "type":
|
|
581
|
+
return Promise.resolve(this.groupByType(entries));
|
|
582
|
+
default:
|
|
583
|
+
return Promise.resolve(this.groupBySemantic(entries));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Group by semantic similarity
|
|
588
|
+
*/
|
|
589
|
+
async groupBySemantic(entries) {
|
|
590
|
+
if (!this.embedFn) {
|
|
591
|
+
return Promise.resolve(this.groupByTextSimilarity(entries));
|
|
592
|
+
}
|
|
593
|
+
const withEmbeddings = await Promise.all(
|
|
594
|
+
entries.map(async (entry) => {
|
|
595
|
+
if (entry.embedding) return entry;
|
|
596
|
+
return {
|
|
597
|
+
...entry,
|
|
598
|
+
embedding: await this.embedFn(entry.content)
|
|
599
|
+
};
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
const groups = [];
|
|
603
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
604
|
+
for (let i = 0; i < withEmbeddings.length; i++) {
|
|
605
|
+
if (assigned.has(withEmbeddings[i].id)) continue;
|
|
606
|
+
const group = [withEmbeddings[i]];
|
|
607
|
+
assigned.add(withEmbeddings[i].id);
|
|
608
|
+
for (let j = i + 1; j < withEmbeddings.length; j++) {
|
|
609
|
+
if (assigned.has(withEmbeddings[j].id)) continue;
|
|
610
|
+
if (group.length >= this.config.maxGroupSize) break;
|
|
611
|
+
const similarity = this.cosineSimilarity(
|
|
612
|
+
withEmbeddings[i].embedding,
|
|
613
|
+
withEmbeddings[j].embedding
|
|
614
|
+
);
|
|
615
|
+
if (similarity >= this.config.similarityThreshold) {
|
|
616
|
+
group.push(withEmbeddings[j]);
|
|
617
|
+
assigned.add(withEmbeddings[j].id);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (group.length >= this.config.minGroupSize) {
|
|
621
|
+
groups.push({
|
|
622
|
+
id: this.generateId(),
|
|
623
|
+
entries: group,
|
|
624
|
+
similarity: this.calculateGroupSimilarity(group),
|
|
625
|
+
groupKey: `semantic-${i}`
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return groups;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Group by text similarity (fallback)
|
|
633
|
+
*/
|
|
634
|
+
groupByTextSimilarity(entries) {
|
|
635
|
+
const groups = [];
|
|
636
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
637
|
+
for (let i = 0; i < entries.length; i++) {
|
|
638
|
+
if (assigned.has(entries[i].id)) continue;
|
|
639
|
+
const group = [entries[i]];
|
|
640
|
+
assigned.add(entries[i].id);
|
|
641
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
642
|
+
if (assigned.has(entries[j].id)) continue;
|
|
643
|
+
if (group.length >= this.config.maxGroupSize) break;
|
|
644
|
+
const similarity = this.textSimilarity(
|
|
645
|
+
entries[i].content,
|
|
646
|
+
entries[j].content
|
|
647
|
+
);
|
|
648
|
+
if (similarity >= this.config.similarityThreshold) {
|
|
649
|
+
group.push(entries[j]);
|
|
650
|
+
assigned.add(entries[j].id);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (group.length >= this.config.minGroupSize) {
|
|
654
|
+
groups.push({
|
|
655
|
+
id: this.generateId(),
|
|
656
|
+
entries: group,
|
|
657
|
+
similarity: 0.8,
|
|
658
|
+
groupKey: `text-${i}`
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return groups;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Group by temporal proximity
|
|
666
|
+
*/
|
|
667
|
+
groupByTemporal(entries) {
|
|
668
|
+
const sorted = [...entries].sort((a, b) => a.timestamp - b.timestamp);
|
|
669
|
+
const groups = [];
|
|
670
|
+
const windowMs = 60 * 60 * 1e3;
|
|
671
|
+
let currentGroup = [];
|
|
672
|
+
let windowStart = sorted[0]?.timestamp ?? 0;
|
|
673
|
+
for (const entry of sorted) {
|
|
674
|
+
if (entry.timestamp - windowStart <= windowMs && currentGroup.length < this.config.maxGroupSize) {
|
|
675
|
+
currentGroup.push(entry);
|
|
676
|
+
} else {
|
|
677
|
+
if (currentGroup.length >= this.config.minGroupSize) {
|
|
678
|
+
groups.push({
|
|
679
|
+
id: this.generateId(),
|
|
680
|
+
entries: currentGroup,
|
|
681
|
+
similarity: 1,
|
|
682
|
+
groupKey: `temporal-${new Date(windowStart).toISOString()}`
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
currentGroup = [entry];
|
|
686
|
+
windowStart = entry.timestamp;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (currentGroup.length >= this.config.minGroupSize) {
|
|
690
|
+
groups.push({
|
|
691
|
+
id: this.generateId(),
|
|
692
|
+
entries: currentGroup,
|
|
693
|
+
similarity: 1,
|
|
694
|
+
groupKey: `temporal-${new Date(windowStart).toISOString()}`
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return groups;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Group by memory type
|
|
701
|
+
*/
|
|
702
|
+
groupByType(entries) {
|
|
703
|
+
const typeGroups = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const entry of entries) {
|
|
705
|
+
if (!typeGroups.has(entry.type)) {
|
|
706
|
+
typeGroups.set(entry.type, []);
|
|
707
|
+
}
|
|
708
|
+
typeGroups.get(entry.type).push(entry);
|
|
709
|
+
}
|
|
710
|
+
const groups = [];
|
|
711
|
+
for (const [type, typeEntries] of typeGroups) {
|
|
712
|
+
for (let i = 0; i < typeEntries.length; i += this.config.maxGroupSize) {
|
|
713
|
+
const chunk = typeEntries.slice(i, i + this.config.maxGroupSize);
|
|
714
|
+
if (chunk.length >= this.config.minGroupSize) {
|
|
715
|
+
groups.push({
|
|
716
|
+
id: this.generateId(),
|
|
717
|
+
entries: chunk,
|
|
718
|
+
similarity: 1,
|
|
719
|
+
groupKey: `type-${type}-${i}`
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return groups;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Consolidate a group into a single entry
|
|
728
|
+
*/
|
|
729
|
+
async consolidateGroup(group) {
|
|
730
|
+
const entries = group.entries;
|
|
731
|
+
const summaryResult = await this.summarizer.summarize(entries);
|
|
732
|
+
const avgImportance = entries.reduce((sum, e) => sum + e.importance, 0) / entries.length;
|
|
733
|
+
const maxImportance = Math.max(...entries.map((e) => e.importance));
|
|
734
|
+
const combinedImportance = avgImportance * 0.7 + maxImportance * 0.3;
|
|
735
|
+
const timestamps = entries.map((e) => e.timestamp);
|
|
736
|
+
const timeSpan = {
|
|
737
|
+
start: Math.min(...timestamps),
|
|
738
|
+
end: Math.max(...timestamps)
|
|
739
|
+
};
|
|
740
|
+
const allTags = /* @__PURE__ */ new Set();
|
|
741
|
+
for (const entry of entries) {
|
|
742
|
+
const tags = entry.metadata.tags;
|
|
743
|
+
if (tags) {
|
|
744
|
+
tags.forEach((t) => allTags.add(t));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const consolidated = {
|
|
748
|
+
id: `consolidated-${group.id}`,
|
|
749
|
+
content: summaryResult.summary,
|
|
750
|
+
type: "summary",
|
|
751
|
+
importance: Math.min(combinedImportance + 0.1, 1),
|
|
752
|
+
// Boost for consolidation
|
|
753
|
+
metadata: {
|
|
754
|
+
source: "system",
|
|
755
|
+
confidence: 0.9,
|
|
756
|
+
consolidated: true,
|
|
757
|
+
sourceCount: entries.length,
|
|
758
|
+
groupKey: group.groupKey,
|
|
759
|
+
tags: Array.from(allTags),
|
|
760
|
+
keyPoints: summaryResult.keyPoints
|
|
761
|
+
},
|
|
762
|
+
timestamp: timeSpan.start,
|
|
763
|
+
accessCount: entries.reduce((sum, e) => sum + e.accessCount, 0),
|
|
764
|
+
createdAt: Date.now(),
|
|
765
|
+
updatedAt: Date.now()
|
|
766
|
+
};
|
|
767
|
+
if (this.embedFn) {
|
|
768
|
+
consolidated.embedding = await this.embedFn(consolidated.content);
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
consolidated,
|
|
772
|
+
sourceIds: entries.map((e) => e.id),
|
|
773
|
+
groupKey: group.groupKey,
|
|
774
|
+
avgImportance,
|
|
775
|
+
timeSpan
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Calculate cosine similarity
|
|
780
|
+
*/
|
|
781
|
+
cosineSimilarity(a, b) {
|
|
782
|
+
if (a.length !== b.length) return 0;
|
|
783
|
+
let dotProduct = 0;
|
|
784
|
+
let normA = 0;
|
|
785
|
+
let normB = 0;
|
|
786
|
+
for (let i = 0; i < a.length; i++) {
|
|
787
|
+
dotProduct += a[i] * b[i];
|
|
788
|
+
normA += a[i] * a[i];
|
|
789
|
+
normB += b[i] * b[i];
|
|
790
|
+
}
|
|
791
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
792
|
+
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Calculate text similarity (Jaccard)
|
|
796
|
+
*/
|
|
797
|
+
textSimilarity(a, b) {
|
|
798
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
799
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/));
|
|
800
|
+
const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
|
|
801
|
+
const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
|
|
802
|
+
return intersection.size / union.size;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Calculate average similarity within a group
|
|
806
|
+
*/
|
|
807
|
+
calculateGroupSimilarity(entries) {
|
|
808
|
+
if (entries.length < 2) return 1;
|
|
809
|
+
if (!entries[0].embedding) return 0.8;
|
|
810
|
+
let totalSimilarity = 0;
|
|
811
|
+
let comparisons = 0;
|
|
812
|
+
for (let i = 0; i < entries.length; i++) {
|
|
813
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
814
|
+
if (entries[i].embedding && entries[j].embedding) {
|
|
815
|
+
totalSimilarity += this.cosineSimilarity(
|
|
816
|
+
entries[i].embedding,
|
|
817
|
+
entries[j].embedding
|
|
818
|
+
);
|
|
819
|
+
comparisons++;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return comparisons > 0 ? totalSimilarity / comparisons : 0.8;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Generate unique ID
|
|
827
|
+
*/
|
|
828
|
+
generateId() {
|
|
829
|
+
return `group-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Update configuration
|
|
833
|
+
*/
|
|
834
|
+
configure(config) {
|
|
835
|
+
this.config = { ...this.config, ...config };
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
function createConsolidator(config) {
|
|
839
|
+
return new Consolidator(config);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/processing/Forgetter.ts
|
|
843
|
+
var Forgetter = class {
|
|
844
|
+
config;
|
|
845
|
+
constructor(config = {}) {
|
|
846
|
+
this.config = {
|
|
847
|
+
retentionPolicy: config.retentionPolicy ?? {},
|
|
848
|
+
importanceThreshold: config.importanceThreshold ?? 0.3,
|
|
849
|
+
maxAge: config.maxAge ?? 30 * 24 * 60 * 60 * 1e3,
|
|
850
|
+
// 30 days
|
|
851
|
+
preserveTypes: config.preserveTypes ?? [],
|
|
852
|
+
curve: config.curve ?? "exponential",
|
|
853
|
+
halfLife: config.halfLife ?? 7 * 24 * 60 * 60 * 1e3,
|
|
854
|
+
// 7 days
|
|
855
|
+
minRetention: config.minRetention ?? 0.1,
|
|
856
|
+
accessBoost: config.accessBoost ?? 0.1,
|
|
857
|
+
importanceWeight: config.importanceWeight ?? 0.5,
|
|
858
|
+
forgetThreshold: config.forgetThreshold ?? 0.05
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Calculate retention score for a memory
|
|
863
|
+
*/
|
|
864
|
+
calculateRetention(entry, now = Date.now()) {
|
|
865
|
+
const ageMs = now - entry.timestamp;
|
|
866
|
+
const baseRetention = this.calculateBaseRetention(ageMs);
|
|
867
|
+
const accessBoost = Math.min(
|
|
868
|
+
entry.accessCount * this.config.accessBoost,
|
|
869
|
+
0.5
|
|
870
|
+
);
|
|
871
|
+
const importanceBoost = entry.importance * this.config.importanceWeight;
|
|
872
|
+
let retention = baseRetention + accessBoost + importanceBoost;
|
|
873
|
+
if (entry.importance >= 0.8) {
|
|
874
|
+
retention = Math.max(retention, 0.5);
|
|
875
|
+
}
|
|
876
|
+
retention = Math.min(Math.max(retention, this.config.minRetention), 1);
|
|
877
|
+
return {
|
|
878
|
+
entryId: entry.id,
|
|
879
|
+
retention,
|
|
880
|
+
ageMs,
|
|
881
|
+
accessCount: entry.accessCount,
|
|
882
|
+
importance: entry.importance,
|
|
883
|
+
shouldForget: retention < this.config.forgetThreshold
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Calculate base retention using the configured curve
|
|
888
|
+
*/
|
|
889
|
+
calculateBaseRetention(ageMs) {
|
|
890
|
+
const halfLife = this.config.halfLife;
|
|
891
|
+
switch (this.config.curve) {
|
|
892
|
+
case "exponential":
|
|
893
|
+
return Math.exp(-Math.LN2 * ageMs / halfLife);
|
|
894
|
+
case "power": {
|
|
895
|
+
const c = halfLife;
|
|
896
|
+
const b = 0.5;
|
|
897
|
+
return Math.pow(1 + ageMs / c, -b);
|
|
898
|
+
}
|
|
899
|
+
case "ebbinghaus": {
|
|
900
|
+
const stability = halfLife;
|
|
901
|
+
return Math.exp(-ageMs / stability);
|
|
902
|
+
}
|
|
903
|
+
default:
|
|
904
|
+
return Math.exp(-Math.LN2 * ageMs / halfLife);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Apply forgetting to memories in a store
|
|
909
|
+
*/
|
|
910
|
+
async applyForgetting(store) {
|
|
911
|
+
const { entries } = await store.query({ limit: 1e4 });
|
|
912
|
+
const now = Date.now();
|
|
913
|
+
const forgotten = [];
|
|
914
|
+
const decayed = [];
|
|
915
|
+
const retained = [];
|
|
916
|
+
let totalRetention = 0;
|
|
917
|
+
for (const entry of entries) {
|
|
918
|
+
const score = this.calculateRetention(entry, now);
|
|
919
|
+
totalRetention += score.retention;
|
|
920
|
+
if (score.shouldForget) {
|
|
921
|
+
await store.delete(entry.id);
|
|
922
|
+
forgotten.push(entry.id);
|
|
923
|
+
} else if (score.retention < 0.5 && entry.importance > 0.3) {
|
|
924
|
+
const newImportance = Math.max(
|
|
925
|
+
entry.importance * score.retention,
|
|
926
|
+
this.config.minRetention
|
|
927
|
+
);
|
|
928
|
+
if (newImportance < entry.importance) {
|
|
929
|
+
await store.update(entry.id, { importance: newImportance });
|
|
930
|
+
decayed.push({
|
|
931
|
+
id: entry.id,
|
|
932
|
+
oldImportance: entry.importance,
|
|
933
|
+
newImportance
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
retained.push(entry.id);
|
|
937
|
+
} else {
|
|
938
|
+
retained.push(entry.id);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
forgotten,
|
|
943
|
+
decayed,
|
|
944
|
+
retained,
|
|
945
|
+
avgRetention: entries.length > 0 ? totalRetention / entries.length : 1
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Simulate forgetting over time (for testing/visualization)
|
|
950
|
+
*/
|
|
951
|
+
simulateForgetting(entries, timePeriodMs, steps = 10) {
|
|
952
|
+
const results = [];
|
|
953
|
+
const stepMs = timePeriodMs / steps;
|
|
954
|
+
const baseTime = Date.now();
|
|
955
|
+
for (let i = 0; i <= steps; i++) {
|
|
956
|
+
const simulatedNow = baseTime + i * stepMs;
|
|
957
|
+
let totalRetention = 0;
|
|
958
|
+
let forgottenCount = 0;
|
|
959
|
+
for (const entry of entries) {
|
|
960
|
+
const score = this.calculateRetention(entry, simulatedNow);
|
|
961
|
+
totalRetention += score.retention;
|
|
962
|
+
if (score.shouldForget) forgottenCount++;
|
|
963
|
+
}
|
|
964
|
+
results.push({
|
|
965
|
+
time: i * stepMs,
|
|
966
|
+
avgRetention: entries.length > 0 ? totalRetention / entries.length : 1,
|
|
967
|
+
forgottenCount
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
return results;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Get memories at risk of being forgotten
|
|
974
|
+
*/
|
|
975
|
+
async getAtRiskMemories(store, threshold = 0.2) {
|
|
976
|
+
const { entries } = await store.query({ limit: 1e3 });
|
|
977
|
+
const now = Date.now();
|
|
978
|
+
const atRisk = [];
|
|
979
|
+
for (const entry of entries) {
|
|
980
|
+
const score = this.calculateRetention(entry, now);
|
|
981
|
+
if (score.retention < threshold && score.retention >= this.config.forgetThreshold) {
|
|
982
|
+
atRisk.push({ entry, retention: score.retention });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return Promise.resolve(atRisk.sort((a, b) => a.retention - b.retention));
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Reinforce a memory (simulate rehearsal)
|
|
989
|
+
*/
|
|
990
|
+
async reinforce(store, id, boost = 0.2) {
|
|
991
|
+
const entry = await store.get(id);
|
|
992
|
+
if (!entry) return false;
|
|
993
|
+
const newImportance = Math.min(entry.importance + boost, 1);
|
|
994
|
+
return store.update(id, {
|
|
995
|
+
importance: newImportance,
|
|
996
|
+
lastAccessedAt: Date.now()
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Batch reinforce multiple memories
|
|
1001
|
+
*/
|
|
1002
|
+
async reinforceBatch(store, ids, boost = 0.1) {
|
|
1003
|
+
let count = 0;
|
|
1004
|
+
for (const id of ids) {
|
|
1005
|
+
if (await this.reinforce(store, id, boost)) {
|
|
1006
|
+
count++;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return count;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Calculate optimal review schedule for a memory
|
|
1013
|
+
*/
|
|
1014
|
+
calculateReviewSchedule(entry, targetRetention = 0.8) {
|
|
1015
|
+
const baseInterval = this.config.halfLife / 10;
|
|
1016
|
+
const intervals = [];
|
|
1017
|
+
let _currentInterval = baseInterval;
|
|
1018
|
+
let simulatedTime = 0;
|
|
1019
|
+
const maxReviews = 10;
|
|
1020
|
+
for (let i = 0; i < maxReviews; i++) {
|
|
1021
|
+
let retention = 1;
|
|
1022
|
+
let checkTime = simulatedTime;
|
|
1023
|
+
while (retention > targetRetention) {
|
|
1024
|
+
checkTime += baseInterval / 10;
|
|
1025
|
+
const ageMs = checkTime - entry.timestamp;
|
|
1026
|
+
retention = this.calculateBaseRetention(ageMs);
|
|
1027
|
+
}
|
|
1028
|
+
intervals.push(checkTime);
|
|
1029
|
+
simulatedTime = checkTime;
|
|
1030
|
+
_currentInterval *= 2;
|
|
1031
|
+
}
|
|
1032
|
+
return intervals;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Get statistics about memory retention
|
|
1036
|
+
*/
|
|
1037
|
+
async getRetentionStats(store) {
|
|
1038
|
+
const { entries } = await store.query({ limit: 1e4 });
|
|
1039
|
+
const now = Date.now();
|
|
1040
|
+
let totalRetention = 0;
|
|
1041
|
+
let atRiskCount = 0;
|
|
1042
|
+
let forgettableCount = 0;
|
|
1043
|
+
let healthyCount = 0;
|
|
1044
|
+
const distribution = {
|
|
1045
|
+
"0-20%": 0,
|
|
1046
|
+
"20-40%": 0,
|
|
1047
|
+
"40-60%": 0,
|
|
1048
|
+
"60-80%": 0,
|
|
1049
|
+
"80-100%": 0
|
|
1050
|
+
};
|
|
1051
|
+
for (const entry of entries) {
|
|
1052
|
+
const score = this.calculateRetention(entry, now);
|
|
1053
|
+
totalRetention += score.retention;
|
|
1054
|
+
if (score.shouldForget) {
|
|
1055
|
+
forgettableCount++;
|
|
1056
|
+
} else if (score.retention < 0.3) {
|
|
1057
|
+
atRiskCount++;
|
|
1058
|
+
} else {
|
|
1059
|
+
healthyCount++;
|
|
1060
|
+
}
|
|
1061
|
+
if (score.retention < 0.2) distribution["0-20%"]++;
|
|
1062
|
+
else if (score.retention < 0.4) distribution["20-40%"]++;
|
|
1063
|
+
else if (score.retention < 0.6) distribution["40-60%"]++;
|
|
1064
|
+
else if (score.retention < 0.8) distribution["60-80%"]++;
|
|
1065
|
+
else distribution["80-100%"]++;
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
avgRetention: entries.length > 0 ? totalRetention / entries.length : 1,
|
|
1069
|
+
atRiskCount,
|
|
1070
|
+
forgettableCount,
|
|
1071
|
+
healthyCount,
|
|
1072
|
+
distribution
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Update configuration
|
|
1077
|
+
*/
|
|
1078
|
+
configure(config) {
|
|
1079
|
+
this.config = { ...this.config, ...config };
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
function createForgetter(config) {
|
|
1083
|
+
return new Forgetter(config);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/processing/Extractor.ts
|
|
1087
|
+
var Extractor = class {
|
|
1088
|
+
config;
|
|
1089
|
+
extractFn;
|
|
1090
|
+
constructor(config = {}) {
|
|
1091
|
+
this.config = {
|
|
1092
|
+
provider: config.provider ?? void 0,
|
|
1093
|
+
model: config.model ?? "default",
|
|
1094
|
+
extractTypes: config.extractTypes ?? [],
|
|
1095
|
+
customPrompt: config.customPrompt ?? "",
|
|
1096
|
+
confidence: config.confidence ?? 0.5,
|
|
1097
|
+
extractEntities: config.extractEntities ?? true,
|
|
1098
|
+
extractRelations: config.extractRelations ?? true,
|
|
1099
|
+
extractKeywords: config.extractKeywords ?? true,
|
|
1100
|
+
extractSentiment: config.extractSentiment ?? false,
|
|
1101
|
+
minConfidence: config.minConfidence ?? 0.5,
|
|
1102
|
+
maxEntitiesPerEntry: config.maxEntitiesPerEntry ?? 20
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Set custom extraction function (for LLM integration)
|
|
1107
|
+
*/
|
|
1108
|
+
setExtractionFunction(fn) {
|
|
1109
|
+
this.extractFn = fn;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Extract information from a memory entry
|
|
1113
|
+
*/
|
|
1114
|
+
async extract(entry) {
|
|
1115
|
+
if (this.extractFn) {
|
|
1116
|
+
return this.extractFn(entry.content, {
|
|
1117
|
+
extractRelations: this.config.extractRelations,
|
|
1118
|
+
extractSentiment: this.config.extractSentiment
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
return Promise.resolve(this.heuristicExtract(entry.content));
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Extract from multiple entries
|
|
1125
|
+
*/
|
|
1126
|
+
async extractBatch(entries) {
|
|
1127
|
+
const results = /* @__PURE__ */ new Map();
|
|
1128
|
+
for (const entry of entries) {
|
|
1129
|
+
const result = await this.extract(entry);
|
|
1130
|
+
results.set(entry.id, result);
|
|
1131
|
+
}
|
|
1132
|
+
return results;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Extract and aggregate across multiple entries
|
|
1136
|
+
*/
|
|
1137
|
+
async extractAggregate(entries) {
|
|
1138
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
1139
|
+
const allRelations = [];
|
|
1140
|
+
const keywordCounts = /* @__PURE__ */ new Map();
|
|
1141
|
+
let sentimentSum = 0;
|
|
1142
|
+
let sentimentCount = 0;
|
|
1143
|
+
for (const entry of entries) {
|
|
1144
|
+
const result = await this.extract(entry);
|
|
1145
|
+
for (const entity of result.entities) {
|
|
1146
|
+
const key = `${entity.type}:${entity.text.toLowerCase()}`;
|
|
1147
|
+
const existing = entityMap.get(key);
|
|
1148
|
+
if (existing) {
|
|
1149
|
+
existing.count++;
|
|
1150
|
+
existing.entity.confidence = Math.max(
|
|
1151
|
+
existing.entity.confidence,
|
|
1152
|
+
entity.confidence
|
|
1153
|
+
);
|
|
1154
|
+
} else {
|
|
1155
|
+
entityMap.set(key, { entity, count: 1 });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
allRelations.push(...result.relations);
|
|
1159
|
+
for (const keyword of result.keywords) {
|
|
1160
|
+
const lower = keyword.toLowerCase();
|
|
1161
|
+
keywordCounts.set(lower, (keywordCounts.get(lower) ?? 0) + 1);
|
|
1162
|
+
}
|
|
1163
|
+
if (result.sentiment) {
|
|
1164
|
+
sentimentSum += result.sentiment.score;
|
|
1165
|
+
sentimentCount++;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const topKeywords = Array.from(keywordCounts.entries()).map(([keyword, count]) => ({ keyword, count })).sort((a, b) => b.count - a.count).slice(0, 20);
|
|
1169
|
+
return {
|
|
1170
|
+
allEntities: entityMap,
|
|
1171
|
+
allRelations,
|
|
1172
|
+
topKeywords,
|
|
1173
|
+
avgSentiment: sentimentCount > 0 ? sentimentSum / sentimentCount : null
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Heuristic-based extraction (fallback)
|
|
1178
|
+
*/
|
|
1179
|
+
heuristicExtract(content) {
|
|
1180
|
+
const entities = [];
|
|
1181
|
+
const relations = [];
|
|
1182
|
+
const keywords = [];
|
|
1183
|
+
if (this.config.extractEntities) {
|
|
1184
|
+
entities.push(...this.extractEntities(content));
|
|
1185
|
+
}
|
|
1186
|
+
if (this.config.extractRelations) {
|
|
1187
|
+
relations.push(...this.extractRelations(content));
|
|
1188
|
+
}
|
|
1189
|
+
if (this.config.extractKeywords) {
|
|
1190
|
+
keywords.push(...this.extractKeywords(content));
|
|
1191
|
+
}
|
|
1192
|
+
const sentiment = this.config.extractSentiment ? this.extractSentiment(content) : void 0;
|
|
1193
|
+
return {
|
|
1194
|
+
entities: entities.slice(0, this.config.maxEntitiesPerEntry),
|
|
1195
|
+
relations,
|
|
1196
|
+
keywords,
|
|
1197
|
+
sentiment
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Extract entities using patterns
|
|
1202
|
+
*/
|
|
1203
|
+
extractEntities(content) {
|
|
1204
|
+
const entities = [];
|
|
1205
|
+
const datePatterns = [
|
|
1206
|
+
/\b\d{1,2}\/\d{1,2}\/\d{2,4}\b/g,
|
|
1207
|
+
/\b\d{4}-\d{2}-\d{2}\b/g,
|
|
1208
|
+
/\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2}(?:,\s*\d{4})?\b/gi,
|
|
1209
|
+
/\b(?:today|yesterday|tomorrow|last\s+week|next\s+month)\b/gi
|
|
1210
|
+
];
|
|
1211
|
+
for (const pattern of datePatterns) {
|
|
1212
|
+
let match2;
|
|
1213
|
+
while ((match2 = pattern.exec(content)) !== null) {
|
|
1214
|
+
entities.push({
|
|
1215
|
+
text: match2[0],
|
|
1216
|
+
type: "date",
|
|
1217
|
+
confidence: 0.9,
|
|
1218
|
+
position: { start: match2.index, end: match2.index + match2[0].length }
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const numberPattern = /\b\d+(?:\.\d+)?(?:\s*(?:percent|%|dollars|\$|euros|€|pounds|£))?\b/gi;
|
|
1223
|
+
let match;
|
|
1224
|
+
while ((match = numberPattern.exec(content)) !== null) {
|
|
1225
|
+
entities.push({
|
|
1226
|
+
text: match[0],
|
|
1227
|
+
type: "number",
|
|
1228
|
+
confidence: 0.8,
|
|
1229
|
+
position: { start: match.index, end: match.index + match[0].length }
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
const capitalizedPattern = /\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/g;
|
|
1233
|
+
while ((match = capitalizedPattern.exec(content)) !== null) {
|
|
1234
|
+
const text = match[0];
|
|
1235
|
+
if (!this.isSentenceStarter(text, content, match.index)) {
|
|
1236
|
+
const type = this.guessEntityType(text);
|
|
1237
|
+
entities.push({
|
|
1238
|
+
text,
|
|
1239
|
+
type,
|
|
1240
|
+
confidence: 0.6,
|
|
1241
|
+
position: { start: match.index, end: match.index + text.length }
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const emailPattern = /\b[\w.-]+@[\w.-]+\.\w+\b/g;
|
|
1246
|
+
while ((match = emailPattern.exec(content)) !== null) {
|
|
1247
|
+
entities.push({
|
|
1248
|
+
text: match[0],
|
|
1249
|
+
type: "other",
|
|
1250
|
+
confidence: 0.95,
|
|
1251
|
+
position: { start: match.index, end: match.index + match[0].length },
|
|
1252
|
+
metadata: { subtype: "email" }
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
const urlPattern = /https?:\/\/[^\s]+/g;
|
|
1256
|
+
while ((match = urlPattern.exec(content)) !== null) {
|
|
1257
|
+
entities.push({
|
|
1258
|
+
text: match[0],
|
|
1259
|
+
type: "other",
|
|
1260
|
+
confidence: 0.95,
|
|
1261
|
+
position: { start: match.index, end: match.index + match[0].length },
|
|
1262
|
+
metadata: { subtype: "url" }
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
return entities.filter((e) => e.confidence >= this.config.minConfidence);
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Extract relations using patterns
|
|
1269
|
+
*/
|
|
1270
|
+
extractRelations(content) {
|
|
1271
|
+
const relations = [];
|
|
1272
|
+
const patterns = [
|
|
1273
|
+
// "X is a/an Y"
|
|
1274
|
+
{
|
|
1275
|
+
regex: /(\w+(?:\s+\w+)?)\s+is\s+a(?:n)?\s+(\w+(?:\s+\w+)?)/gi,
|
|
1276
|
+
predicate: "is_a"
|
|
1277
|
+
},
|
|
1278
|
+
// "X works at Y"
|
|
1279
|
+
{
|
|
1280
|
+
regex: /(\w+(?:\s+\w+)?)\s+works\s+at\s+(\w+(?:\s+\w+)?)/gi,
|
|
1281
|
+
predicate: "works_at"
|
|
1282
|
+
},
|
|
1283
|
+
// "X is located in Y"
|
|
1284
|
+
{
|
|
1285
|
+
regex: /(\w+(?:\s+\w+)?)\s+is\s+located\s+in\s+(\w+(?:\s+\w+)?)/gi,
|
|
1286
|
+
predicate: "located_in"
|
|
1287
|
+
},
|
|
1288
|
+
// "X belongs to Y"
|
|
1289
|
+
{
|
|
1290
|
+
regex: /(\w+(?:\s+\w+)?)\s+belongs\s+to\s+(\w+(?:\s+\w+)?)/gi,
|
|
1291
|
+
predicate: "belongs_to"
|
|
1292
|
+
},
|
|
1293
|
+
// "X created Y"
|
|
1294
|
+
{
|
|
1295
|
+
regex: /(\w+(?:\s+\w+)?)\s+created\s+(\w+(?:\s+\w+)?)/gi,
|
|
1296
|
+
predicate: "created"
|
|
1297
|
+
},
|
|
1298
|
+
// "X contains Y"
|
|
1299
|
+
{
|
|
1300
|
+
regex: /(\w+(?:\s+\w+)?)\s+contains\s+(\w+(?:\s+\w+)?)/gi,
|
|
1301
|
+
predicate: "contains"
|
|
1302
|
+
}
|
|
1303
|
+
];
|
|
1304
|
+
for (const { regex, predicate } of patterns) {
|
|
1305
|
+
let match;
|
|
1306
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1307
|
+
relations.push({
|
|
1308
|
+
subject: match[1].trim(),
|
|
1309
|
+
predicate,
|
|
1310
|
+
object: match[2].trim(),
|
|
1311
|
+
confidence: 0.7,
|
|
1312
|
+
sourceText: match[0]
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return relations.filter((r) => r.confidence >= this.config.minConfidence);
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Extract keywords
|
|
1320
|
+
*/
|
|
1321
|
+
extractKeywords(content) {
|
|
1322
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
1323
|
+
"the",
|
|
1324
|
+
"a",
|
|
1325
|
+
"an",
|
|
1326
|
+
"and",
|
|
1327
|
+
"or",
|
|
1328
|
+
"but",
|
|
1329
|
+
"in",
|
|
1330
|
+
"on",
|
|
1331
|
+
"at",
|
|
1332
|
+
"to",
|
|
1333
|
+
"for",
|
|
1334
|
+
"of",
|
|
1335
|
+
"with",
|
|
1336
|
+
"by",
|
|
1337
|
+
"from",
|
|
1338
|
+
"as",
|
|
1339
|
+
"is",
|
|
1340
|
+
"was",
|
|
1341
|
+
"are",
|
|
1342
|
+
"were",
|
|
1343
|
+
"been",
|
|
1344
|
+
"be",
|
|
1345
|
+
"have",
|
|
1346
|
+
"has",
|
|
1347
|
+
"had",
|
|
1348
|
+
"do",
|
|
1349
|
+
"does",
|
|
1350
|
+
"did",
|
|
1351
|
+
"will",
|
|
1352
|
+
"would",
|
|
1353
|
+
"could",
|
|
1354
|
+
"should",
|
|
1355
|
+
"may",
|
|
1356
|
+
"might",
|
|
1357
|
+
"must",
|
|
1358
|
+
"shall",
|
|
1359
|
+
"can",
|
|
1360
|
+
"need",
|
|
1361
|
+
"it",
|
|
1362
|
+
"its",
|
|
1363
|
+
"this",
|
|
1364
|
+
"that",
|
|
1365
|
+
"these",
|
|
1366
|
+
"those",
|
|
1367
|
+
"i",
|
|
1368
|
+
"you",
|
|
1369
|
+
"he",
|
|
1370
|
+
"she",
|
|
1371
|
+
"we",
|
|
1372
|
+
"they",
|
|
1373
|
+
"what",
|
|
1374
|
+
"which",
|
|
1375
|
+
"who",
|
|
1376
|
+
"when",
|
|
1377
|
+
"where",
|
|
1378
|
+
"why",
|
|
1379
|
+
"how",
|
|
1380
|
+
"all",
|
|
1381
|
+
"each",
|
|
1382
|
+
"every",
|
|
1383
|
+
"both",
|
|
1384
|
+
"few",
|
|
1385
|
+
"more",
|
|
1386
|
+
"most",
|
|
1387
|
+
"other",
|
|
1388
|
+
"some",
|
|
1389
|
+
"such",
|
|
1390
|
+
"no",
|
|
1391
|
+
"not",
|
|
1392
|
+
"only",
|
|
1393
|
+
"own",
|
|
1394
|
+
"same",
|
|
1395
|
+
"so",
|
|
1396
|
+
"than",
|
|
1397
|
+
"too",
|
|
1398
|
+
"very"
|
|
1399
|
+
]);
|
|
1400
|
+
const words = content.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((word) => word.length > 3 && !stopWords.has(word));
|
|
1401
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1402
|
+
for (const word of words) {
|
|
1403
|
+
counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
1404
|
+
}
|
|
1405
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([word]) => word);
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Extract sentiment
|
|
1409
|
+
*/
|
|
1410
|
+
extractSentiment(content) {
|
|
1411
|
+
const positiveWords = /* @__PURE__ */ new Set([
|
|
1412
|
+
"good",
|
|
1413
|
+
"great",
|
|
1414
|
+
"excellent",
|
|
1415
|
+
"amazing",
|
|
1416
|
+
"wonderful",
|
|
1417
|
+
"fantastic",
|
|
1418
|
+
"happy",
|
|
1419
|
+
"love",
|
|
1420
|
+
"best",
|
|
1421
|
+
"awesome",
|
|
1422
|
+
"nice",
|
|
1423
|
+
"beautiful",
|
|
1424
|
+
"perfect",
|
|
1425
|
+
"success",
|
|
1426
|
+
"successful",
|
|
1427
|
+
"pleased",
|
|
1428
|
+
"delighted",
|
|
1429
|
+
"enjoy",
|
|
1430
|
+
"enjoyed",
|
|
1431
|
+
"helpful",
|
|
1432
|
+
"thanks",
|
|
1433
|
+
"thank"
|
|
1434
|
+
]);
|
|
1435
|
+
const negativeWords = /* @__PURE__ */ new Set([
|
|
1436
|
+
"bad",
|
|
1437
|
+
"terrible",
|
|
1438
|
+
"awful",
|
|
1439
|
+
"horrible",
|
|
1440
|
+
"poor",
|
|
1441
|
+
"worst",
|
|
1442
|
+
"hate",
|
|
1443
|
+
"sad",
|
|
1444
|
+
"angry",
|
|
1445
|
+
"disappointed",
|
|
1446
|
+
"frustrating",
|
|
1447
|
+
"annoying",
|
|
1448
|
+
"problem",
|
|
1449
|
+
"issue",
|
|
1450
|
+
"error",
|
|
1451
|
+
"fail",
|
|
1452
|
+
"failed",
|
|
1453
|
+
"failure",
|
|
1454
|
+
"wrong",
|
|
1455
|
+
"broken",
|
|
1456
|
+
"difficult",
|
|
1457
|
+
"hard"
|
|
1458
|
+
]);
|
|
1459
|
+
const words = content.toLowerCase().split(/\s+/);
|
|
1460
|
+
let positiveCount = 0;
|
|
1461
|
+
let negativeCount = 0;
|
|
1462
|
+
for (const word of words) {
|
|
1463
|
+
if (positiveWords.has(word)) positiveCount++;
|
|
1464
|
+
if (negativeWords.has(word)) negativeCount++;
|
|
1465
|
+
}
|
|
1466
|
+
const total = positiveCount + negativeCount;
|
|
1467
|
+
if (total === 0) {
|
|
1468
|
+
return { score: 0, label: "neutral" };
|
|
1469
|
+
}
|
|
1470
|
+
const score = (positiveCount - negativeCount) / total;
|
|
1471
|
+
let label;
|
|
1472
|
+
if (score > 0.2) label = "positive";
|
|
1473
|
+
else if (score < -0.2) label = "negative";
|
|
1474
|
+
else label = "neutral";
|
|
1475
|
+
return { score, label };
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Check if text is a sentence starter
|
|
1479
|
+
*/
|
|
1480
|
+
isSentenceStarter(_text, content, index) {
|
|
1481
|
+
if (index === 0) return true;
|
|
1482
|
+
const prevChar = content[index - 1];
|
|
1483
|
+
return prevChar === "." || prevChar === "!" || prevChar === "?" || prevChar === "\n";
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Guess entity type from text
|
|
1487
|
+
*/
|
|
1488
|
+
guessEntityType(text) {
|
|
1489
|
+
const lowerText = text.toLowerCase();
|
|
1490
|
+
const locationIndicators = [
|
|
1491
|
+
"city",
|
|
1492
|
+
"state",
|
|
1493
|
+
"country",
|
|
1494
|
+
"street",
|
|
1495
|
+
"avenue",
|
|
1496
|
+
"road"
|
|
1497
|
+
];
|
|
1498
|
+
if (locationIndicators.some((ind) => lowerText.includes(ind))) {
|
|
1499
|
+
return "location";
|
|
1500
|
+
}
|
|
1501
|
+
const orgIndicators = [
|
|
1502
|
+
"inc",
|
|
1503
|
+
"corp",
|
|
1504
|
+
"company",
|
|
1505
|
+
"organization",
|
|
1506
|
+
"foundation",
|
|
1507
|
+
"institute"
|
|
1508
|
+
];
|
|
1509
|
+
if (orgIndicators.some((ind) => lowerText.includes(ind))) {
|
|
1510
|
+
return "organization";
|
|
1511
|
+
}
|
|
1512
|
+
const wordCount = text.split(/\s+/).length;
|
|
1513
|
+
if (wordCount >= 2 && wordCount <= 3) {
|
|
1514
|
+
return "person";
|
|
1515
|
+
}
|
|
1516
|
+
return "concept";
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Update configuration
|
|
1520
|
+
*/
|
|
1521
|
+
configure(config) {
|
|
1522
|
+
this.config = { ...this.config, ...config };
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
function createExtractor(config) {
|
|
1526
|
+
return new Extractor(config);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
export {
|
|
1530
|
+
Summarizer,
|
|
1531
|
+
createSummarizer,
|
|
1532
|
+
Compressor,
|
|
1533
|
+
createCompressor,
|
|
1534
|
+
Consolidator,
|
|
1535
|
+
createConsolidator,
|
|
1536
|
+
Forgetter,
|
|
1537
|
+
createForgetter,
|
|
1538
|
+
Extractor,
|
|
1539
|
+
createExtractor
|
|
1540
|
+
};
|