@prometheus-ai/memory 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/dist/types/cli.d.ts +35 -0
- package/dist/types/config.d.ts +77 -0
- package/dist/types/core/aaak.d.ts +55 -0
- package/dist/types/core/annotations.d.ts +75 -0
- package/dist/types/core/banks.d.ts +33 -0
- package/dist/types/core/beam/consolidate.d.ts +32 -0
- package/dist/types/core/beam/helpers.d.ts +76 -0
- package/dist/types/core/beam/index.d.ts +59 -0
- package/dist/types/core/beam/recall.d.ts +32 -0
- package/dist/types/core/beam/schema.d.ts +2 -0
- package/dist/types/core/beam/store.d.ts +35 -0
- package/dist/types/core/beam/types.d.ts +233 -0
- package/dist/types/core/binary-vectors.d.ts +54 -0
- package/dist/types/core/chat-normalize.d.ts +13 -0
- package/dist/types/core/content-sanitizer.d.ts +18 -0
- package/dist/types/core/cost-log.d.ts +13 -0
- package/dist/types/core/embeddings.d.ts +44 -0
- package/dist/types/core/entities.d.ts +7 -0
- package/dist/types/core/episodic-graph.d.ts +89 -0
- package/dist/types/core/extraction/client.d.ts +31 -0
- package/dist/types/core/extraction/diagnostics.d.ts +51 -0
- package/dist/types/core/extraction/prompts.d.ts +2 -0
- package/dist/types/core/extraction.d.ts +6 -0
- package/dist/types/core/index.d.ts +4 -0
- package/dist/types/core/llm-backends.d.ts +21 -0
- package/dist/types/core/local-llm.d.ts +15 -0
- package/dist/types/core/memory.d.ts +160 -0
- package/dist/types/core/migrations/e6-triplestore-split.d.ts +17 -0
- package/dist/types/core/migrations/index.d.ts +1 -0
- package/dist/types/core/mmr.d.ts +8 -0
- package/dist/types/core/orchestrator.d.ts +20 -0
- package/dist/types/core/patterns.d.ts +61 -0
- package/dist/types/core/plugins.d.ts +109 -0
- package/dist/types/core/polyphonic-recall.d.ts +66 -0
- package/dist/types/core/query-cache.d.ts +46 -0
- package/dist/types/core/query-intent.d.ts +20 -0
- package/dist/types/core/recall-diagnostics.d.ts +48 -0
- package/dist/types/core/runtime-options.d.ts +68 -0
- package/dist/types/core/shmr.d.ts +56 -0
- package/dist/types/core/streaming.d.ts +136 -0
- package/dist/types/core/synonyms.d.ts +46 -0
- package/dist/types/core/temporal-parser.d.ts +16 -0
- package/dist/types/core/token-counter.d.ts +8 -0
- package/dist/types/core/triples.d.ts +63 -0
- package/dist/types/core/typed-memory.d.ts +39 -0
- package/dist/types/core/vector-math.d.ts +1 -0
- package/dist/types/core/veracity-consolidation.d.ts +60 -0
- package/dist/types/core/weibull.d.ts +96 -0
- package/dist/types/db.d.ts +16 -0
- package/dist/types/diagnose.d.ts +24 -0
- package/dist/types/dr/index.d.ts +1 -0
- package/dist/types/dr/recovery.d.ts +68 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/mcp-server.d.ts +40 -0
- package/dist/types/mcp-tools.d.ts +484 -0
- package/dist/types/migrations/e6-triplestore-split.d.ts +1 -0
- package/dist/types/migrations/index.d.ts +1 -0
- package/dist/types/types.d.ts +145 -0
- package/dist/types/util/datetime.d.ts +8 -0
- package/dist/types/util/env.d.ts +10 -0
- package/dist/types/util/ids.d.ts +3 -0
- package/dist/types/util/lru.d.ts +12 -0
- package/dist/types/util/regex.d.ts +10 -0
- package/package.json +85 -0
- package/src/cli.ts +398 -0
- package/src/config.ts +326 -0
- package/src/core/aaak.ts +142 -0
- package/src/core/annotations.ts +457 -0
- package/src/core/banks.ts +133 -0
- package/src/core/beam/consolidate.ts +965 -0
- package/src/core/beam/helpers.ts +977 -0
- package/src/core/beam/index.ts +353 -0
- package/src/core/beam/recall.ts +1100 -0
- package/src/core/beam/schema.ts +423 -0
- package/src/core/beam/store.ts +829 -0
- package/src/core/beam/types.ts +268 -0
- package/src/core/binary-vectors.ts +317 -0
- package/src/core/chat-normalize.ts +160 -0
- package/src/core/content-sanitizer.ts +136 -0
- package/src/core/cost-log.ts +103 -0
- package/src/core/embeddings.ts +423 -0
- package/src/core/entities.ts +259 -0
- package/src/core/episodic-graph.ts +708 -0
- package/src/core/extraction/client.ts +162 -0
- package/src/core/extraction/diagnostics.ts +193 -0
- package/src/core/extraction/prompts.ts +31 -0
- package/src/core/extraction.ts +335 -0
- package/src/core/index.ts +30 -0
- package/src/core/llm-backends.ts +51 -0
- package/src/core/local-llm.ts +436 -0
- package/src/core/memory.ts +630 -0
- package/src/core/migrations/e6-triplestore-split.ts +211 -0
- package/src/core/migrations/index.ts +1 -0
- package/src/core/mmr.ts +71 -0
- package/src/core/orchestrator.ts +62 -0
- package/src/core/patterns.ts +484 -0
- package/src/core/plugins.ts +375 -0
- package/src/core/polyphonic-recall.ts +563 -0
- package/src/core/query-cache.ts +354 -0
- package/src/core/query-intent.ts +139 -0
- package/src/core/recall-diagnostics.ts +157 -0
- package/src/core/runtime-options.ts +119 -0
- package/src/core/shmr.ts +460 -0
- package/src/core/streaming.ts +419 -0
- package/src/core/synonyms.ts +197 -0
- package/src/core/temporal-parser.ts +363 -0
- package/src/core/token-counter.ts +30 -0
- package/src/core/triples.ts +454 -0
- package/src/core/typed-memory.ts +407 -0
- package/src/core/vector-math.ts +23 -0
- package/src/core/veracity-consolidation.ts +477 -0
- package/src/core/weibull.ts +124 -0
- package/src/db.ts +128 -0
- package/src/diagnose.ts +174 -0
- package/src/dr/index.ts +1 -0
- package/src/dr/recovery.ts +405 -0
- package/src/index.ts +33 -0
- package/src/mcp-server.ts +155 -0
- package/src/mcp-tools.ts +970 -0
- package/src/migrations/e6-triplestore-split.ts +1 -0
- package/src/migrations/index.ts +1 -0
- package/src/types.ts +157 -0
- package/src/util/datetime.ts +69 -0
- package/src/util/env.ts +65 -0
- package/src/util/ids.ts +19 -0
- package/src/util/lru.ts +48 -0
- package/src/util/regex.ts +165 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
2
|
+
|
|
3
|
+
export interface CompressionStatsInit {
|
|
4
|
+
readonly originalSize?: number;
|
|
5
|
+
readonly compressedSize?: number;
|
|
6
|
+
readonly ratio?: number;
|
|
7
|
+
readonly method?: string;
|
|
8
|
+
readonly patternsFound?: number;
|
|
9
|
+
readonly memoriesCompressed?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CompressionStats {
|
|
13
|
+
originalSize: number;
|
|
14
|
+
compressedSize: number;
|
|
15
|
+
ratio: number;
|
|
16
|
+
method: string;
|
|
17
|
+
patternsFound: number;
|
|
18
|
+
memoriesCompressed: number;
|
|
19
|
+
|
|
20
|
+
constructor(init: CompressionStatsInit = {}) {
|
|
21
|
+
this.originalSize = init.originalSize ?? 0;
|
|
22
|
+
this.compressedSize = init.compressedSize ?? 0;
|
|
23
|
+
this.ratio = init.ratio ?? 0.0;
|
|
24
|
+
this.method = init.method ?? "";
|
|
25
|
+
this.patternsFound = init.patternsFound ?? 0;
|
|
26
|
+
this.memoriesCompressed = init.memoriesCompressed ?? 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get savingsPercent(): number {
|
|
30
|
+
if (this.originalSize === 0) return 0.0;
|
|
31
|
+
return (1.0 - this.compressedSize / this.originalSize) * 100;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type MemoryRecord = Record<string, unknown> & {
|
|
36
|
+
content?: string;
|
|
37
|
+
timestamp?: string;
|
|
38
|
+
created_at?: string;
|
|
39
|
+
source?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function utf8Size(value: string): number {
|
|
43
|
+
return UTF8_ENCODER.encode(value).byteLength;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class MemoryCompressor {
|
|
47
|
+
readonly dictionary: Readonly<Record<string, string>>;
|
|
48
|
+
|
|
49
|
+
constructor(dictionary?: Readonly<Record<string, string>>) {
|
|
50
|
+
this.dictionary = dictionary ?? MemoryCompressor.buildDefaultDict();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static buildDefaultDict(): Record<string, string> {
|
|
54
|
+
return {
|
|
55
|
+
"remember that ": "",
|
|
56
|
+
"the user said ": "",
|
|
57
|
+
"the user asked ": "",
|
|
58
|
+
"the user wants ": "",
|
|
59
|
+
"conversation about ": "",
|
|
60
|
+
"please note that ": "",
|
|
61
|
+
"important: ": "",
|
|
62
|
+
"user preference: ": "",
|
|
63
|
+
"project context: ": "\t",
|
|
64
|
+
"api key ": "\n",
|
|
65
|
+
"token ": "\v",
|
|
66
|
+
"session ": "\f",
|
|
67
|
+
"mnemopi ": "\r",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
compress(content: string, method = "dict"): readonly [string, CompressionStats] {
|
|
71
|
+
const originalSize = utf8Size(content);
|
|
72
|
+
if (method === "auto") {
|
|
73
|
+
let [compressed, stats] = this.dictCompress(content);
|
|
74
|
+
if (stats.savingsPercent < 5) [compressed, stats] = this.rleCompress(content);
|
|
75
|
+
return [compressed, stats];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (method === "dict") return this.dictCompress(content);
|
|
79
|
+
if (method === "rle") return this.rleCompress(content);
|
|
80
|
+
if (method === "semantic") return this.semanticCompressSingle(content);
|
|
81
|
+
return [
|
|
82
|
+
content,
|
|
83
|
+
new CompressionStats({
|
|
84
|
+
originalSize,
|
|
85
|
+
compressedSize: originalSize,
|
|
86
|
+
ratio: 1.0,
|
|
87
|
+
method: "none",
|
|
88
|
+
}),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private dictCompress(content: string): readonly [string, CompressionStats] {
|
|
93
|
+
const originalSize = utf8Size(content);
|
|
94
|
+
let compressed = content;
|
|
95
|
+
for (const phrase in this.dictionary) {
|
|
96
|
+
const token = this.dictionary[phrase];
|
|
97
|
+
if (token === undefined) continue;
|
|
98
|
+
compressed = compressed.replaceAll(phrase, token);
|
|
99
|
+
}
|
|
100
|
+
const compressedSize = utf8Size(compressed);
|
|
101
|
+
const ratio = originalSize > 0 ? compressedSize / originalSize : 1.0;
|
|
102
|
+
return [compressed, new CompressionStats({ originalSize, compressedSize, ratio, method: "dict" })];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private rleCompress(content: string): readonly [string, CompressionStats] {
|
|
106
|
+
const originalSize = utf8Size(content);
|
|
107
|
+
if (content.length === 0) {
|
|
108
|
+
return [content, new CompressionStats({ originalSize: 0, compressedSize: 0, ratio: 1.0, method: "rle" })];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const compressed: string[] = [];
|
|
112
|
+
let count = 1;
|
|
113
|
+
for (let i = 1; i < content.length; i++) {
|
|
114
|
+
if (content[i] === content[i - 1] && count < 255) {
|
|
115
|
+
count++;
|
|
116
|
+
} else {
|
|
117
|
+
const prev = content[i - 1] ?? "";
|
|
118
|
+
compressed.push(count > 3 ? `[${prev}*${count}]` : content.slice(i - count, i));
|
|
119
|
+
count = 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const last = content[content.length - 1] ?? "";
|
|
123
|
+
compressed.push(count > 3 ? `[${last}*${count}]` : content.slice(content.length - count));
|
|
124
|
+
const compressedString = compressed.join("");
|
|
125
|
+
const compressedSize = utf8Size(compressedString);
|
|
126
|
+
const ratio = originalSize > 0 ? compressedSize / originalSize : 1.0;
|
|
127
|
+
return [compressedString, new CompressionStats({ originalSize, compressedSize, ratio, method: "rle" })];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private semanticCompressSingle(content: string): readonly [string, CompressionStats] {
|
|
131
|
+
const originalSize = utf8Size(content);
|
|
132
|
+
const compressed = originalSize > 500 ? `${content.slice(0, 250)} [...] ${content.slice(-100)}` : content;
|
|
133
|
+
const compressedSize = utf8Size(compressed);
|
|
134
|
+
const ratio = originalSize > 0 ? compressedSize / originalSize : 1.0;
|
|
135
|
+
return [compressed, new CompressionStats({ originalSize, compressedSize, ratio, method: "semantic" })];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
compressBatch(memories: readonly MemoryRecord[], method = "auto"): readonly [MemoryRecord[], CompressionStats] {
|
|
139
|
+
let totalOriginal = 0;
|
|
140
|
+
let totalCompressed = 0;
|
|
141
|
+
const compressedMemories: MemoryRecord[] = [];
|
|
142
|
+
for (const mem of memories) {
|
|
143
|
+
const content = typeof mem.content === "string" ? mem.content : "";
|
|
144
|
+
const [compressed, stats] = this.compress(content, method);
|
|
145
|
+
totalOriginal += stats.originalSize;
|
|
146
|
+
totalCompressed += stats.compressedSize;
|
|
147
|
+
compressedMemories.push({
|
|
148
|
+
...mem,
|
|
149
|
+
content: compressed,
|
|
150
|
+
_compressed: true,
|
|
151
|
+
_compression_method: stats.method,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
const ratio = totalOriginal > 0 ? totalCompressed / totalOriginal : 1.0;
|
|
155
|
+
return [
|
|
156
|
+
compressedMemories,
|
|
157
|
+
new CompressionStats({
|
|
158
|
+
originalSize: totalOriginal,
|
|
159
|
+
compressedSize: totalCompressed,
|
|
160
|
+
ratio,
|
|
161
|
+
method,
|
|
162
|
+
memoriesCompressed: memories.length,
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
decompress(content: string, method = "dict"): string {
|
|
167
|
+
if (method === "dict") {
|
|
168
|
+
let decompressed = content;
|
|
169
|
+
for (const phrase in this.dictionary) {
|
|
170
|
+
const token = this.dictionary[phrase];
|
|
171
|
+
if (token === undefined || token.length === 0) continue;
|
|
172
|
+
decompressed = decompressed.replaceAll(token, phrase);
|
|
173
|
+
}
|
|
174
|
+
return decompressed;
|
|
175
|
+
}
|
|
176
|
+
if (method === "rle") {
|
|
177
|
+
return content.replace(/\[(.)\*(\d+)\]/g, (_match, char: string, count: string) =>
|
|
178
|
+
char.repeat(Number.parseInt(count, 10)),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return content;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface DetectedPatternInit {
|
|
186
|
+
readonly patternType?: string;
|
|
187
|
+
readonly pattern_type?: string;
|
|
188
|
+
readonly description: string;
|
|
189
|
+
readonly confidence: number;
|
|
190
|
+
readonly samples?: readonly string[];
|
|
191
|
+
readonly metadata?: Record<string, unknown>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export class DetectedPattern {
|
|
195
|
+
patternType: string;
|
|
196
|
+
description: string;
|
|
197
|
+
confidence: number;
|
|
198
|
+
samples: string[];
|
|
199
|
+
metadata: Record<string, unknown>;
|
|
200
|
+
|
|
201
|
+
constructor(init: DetectedPatternInit) {
|
|
202
|
+
this.patternType = init.patternType ?? init.pattern_type ?? "";
|
|
203
|
+
this.description = init.description;
|
|
204
|
+
this.confidence = init.confidence;
|
|
205
|
+
this.samples = [...(init.samples ?? [])];
|
|
206
|
+
this.metadata = { ...(init.metadata ?? {}) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
toDict(): Record<string, unknown> {
|
|
210
|
+
return {
|
|
211
|
+
pattern_type: this.patternType,
|
|
212
|
+
description: this.description,
|
|
213
|
+
confidence: this.confidence,
|
|
214
|
+
samples: [...this.samples],
|
|
215
|
+
metadata: { ...this.metadata },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function increment<K>(counter: Map<K, number>, key: K): void {
|
|
221
|
+
counter.set(key, (counter.get(key) ?? 0) + 1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function mostCommon<K>(counter: Map<K, number>, limit: number): Array<readonly [K, number]> {
|
|
225
|
+
return Array.from(counter.entries())
|
|
226
|
+
.sort((left, right) => right[1] - left[1])
|
|
227
|
+
.slice(0, limit);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const CONTENT_STOPWORDS = new Set([
|
|
231
|
+
"about",
|
|
232
|
+
"after",
|
|
233
|
+
"before",
|
|
234
|
+
"being",
|
|
235
|
+
"could",
|
|
236
|
+
"doing",
|
|
237
|
+
"every",
|
|
238
|
+
"having",
|
|
239
|
+
"might",
|
|
240
|
+
"other",
|
|
241
|
+
"should",
|
|
242
|
+
"their",
|
|
243
|
+
"there",
|
|
244
|
+
"these",
|
|
245
|
+
"those",
|
|
246
|
+
"through",
|
|
247
|
+
"under",
|
|
248
|
+
"where",
|
|
249
|
+
"which",
|
|
250
|
+
"while",
|
|
251
|
+
"would",
|
|
252
|
+
"mnemopi",
|
|
253
|
+
"memory",
|
|
254
|
+
"memories",
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
function contentOf(memory: MemoryRecord): string {
|
|
258
|
+
return typeof memory.content === "string" ? memory.content : "";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function sourceOf(memory: MemoryRecord): string {
|
|
262
|
+
return typeof memory.source === "string" ? memory.source : "unknown";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function timestampOf(memory: MemoryRecord): string | undefined {
|
|
266
|
+
if (typeof memory.timestamp === "string" && memory.timestamp.length > 0) return memory.timestamp;
|
|
267
|
+
if (typeof memory.created_at === "string" && memory.created_at.length > 0) return memory.created_at;
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isoSample(date: Date): string {
|
|
272
|
+
return date.toISOString();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export class PatternDetector {
|
|
276
|
+
readonly minConfidence: number;
|
|
277
|
+
|
|
278
|
+
constructor(minConfidence = 0.6) {
|
|
279
|
+
this.minConfidence = minConfidence;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
detectTemporal(memories: readonly MemoryRecord[]): DetectedPattern[] {
|
|
283
|
+
const patterns: DetectedPattern[] = [];
|
|
284
|
+
const timestamps: Date[] = [];
|
|
285
|
+
for (const mem of memories) {
|
|
286
|
+
const ts = timestampOf(mem);
|
|
287
|
+
if (ts === undefined) continue;
|
|
288
|
+
const date = new Date(ts.replace("Z", "+00:00"));
|
|
289
|
+
if (!Number.isNaN(date.getTime())) timestamps.push(date);
|
|
290
|
+
}
|
|
291
|
+
if (timestamps.length < 3) return patterns;
|
|
292
|
+
|
|
293
|
+
const hourCounts = new Map<number, number>();
|
|
294
|
+
for (const timestamp of timestamps) increment(hourCounts, timestamp.getHours());
|
|
295
|
+
const total = timestamps.length;
|
|
296
|
+
for (const [hour, count] of mostCommon(hourCounts, 3)) {
|
|
297
|
+
const confidence = count / total;
|
|
298
|
+
if (confidence >= this.minConfidence) {
|
|
299
|
+
patterns.push(
|
|
300
|
+
new DetectedPattern({
|
|
301
|
+
patternType: "temporal",
|
|
302
|
+
description: `Memories frequently created at ${hour.toString().padStart(2, "0")}:00 (${count}/${total} times)`,
|
|
303
|
+
confidence,
|
|
304
|
+
samples: timestamps
|
|
305
|
+
.filter(timestamp => timestamp.getHours() === hour)
|
|
306
|
+
.slice(0, 3)
|
|
307
|
+
.map(isoSample),
|
|
308
|
+
metadata: { hour, count, total },
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] as const;
|
|
315
|
+
const dayCounts = new Map<number, number>();
|
|
316
|
+
for (const timestamp of timestamps) increment(dayCounts, (timestamp.getDay() + 6) % 7);
|
|
317
|
+
for (const [day, count] of mostCommon(dayCounts, 2)) {
|
|
318
|
+
const confidence = count / total;
|
|
319
|
+
const dayName = dayNames[day];
|
|
320
|
+
if (dayName !== undefined && confidence >= this.minConfidence) {
|
|
321
|
+
patterns.push(
|
|
322
|
+
new DetectedPattern({
|
|
323
|
+
patternType: "temporal",
|
|
324
|
+
description: `Memories frequently created on ${dayName} (${count}/${total} times)`,
|
|
325
|
+
confidence,
|
|
326
|
+
samples: timestamps
|
|
327
|
+
.filter(timestamp => (timestamp.getDay() + 6) % 7 === day)
|
|
328
|
+
.slice(0, 3)
|
|
329
|
+
.map(isoSample),
|
|
330
|
+
metadata: { day: dayName, count, total },
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return patterns;
|
|
336
|
+
}
|
|
337
|
+
detectContent(memories: readonly MemoryRecord[]): DetectedPattern[] {
|
|
338
|
+
const patterns: DetectedPattern[] = [];
|
|
339
|
+
const allText = memories.map(contentOf).join(" ");
|
|
340
|
+
const words = Array.from(allText.toLowerCase().matchAll(/\b[a-zA-Z]{5,}\b/g), match => match[0]).filter(
|
|
341
|
+
word => !CONTENT_STOPWORDS.has(word),
|
|
342
|
+
);
|
|
343
|
+
const wordCounts = new Map<string, number>();
|
|
344
|
+
for (const word of words) increment(wordCounts, word);
|
|
345
|
+
const totalWords = words.length;
|
|
346
|
+
for (const [word, count] of mostCommon(wordCounts, 5)) {
|
|
347
|
+
const confidence = Math.min(1.0, count / Math.max(3, totalWords * 0.05));
|
|
348
|
+
if (count >= 2 && confidence >= this.minConfidence) {
|
|
349
|
+
patterns.push(
|
|
350
|
+
new DetectedPattern({
|
|
351
|
+
patternType: "content",
|
|
352
|
+
description: `Frequent topic: '${word}' appears ${count} times`,
|
|
353
|
+
confidence,
|
|
354
|
+
samples: memories
|
|
355
|
+
.filter(mem => contentOf(mem).toLowerCase().includes(word))
|
|
356
|
+
.slice(0, 3)
|
|
357
|
+
.map(contentOf),
|
|
358
|
+
metadata: { word, count },
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (memories.length >= 3) {
|
|
365
|
+
const cooccurrence = new Map<string, number>();
|
|
366
|
+
const pairWords = new Map<string, readonly [string, string]>();
|
|
367
|
+
for (const mem of memories) {
|
|
368
|
+
const memWords = new Set(
|
|
369
|
+
Array.from(
|
|
370
|
+
contentOf(mem)
|
|
371
|
+
.toLowerCase()
|
|
372
|
+
.matchAll(/\b[a-zA-Z]{5,}\b/g),
|
|
373
|
+
match => match[0],
|
|
374
|
+
).filter(word => !CONTENT_STOPWORDS.has(word)),
|
|
375
|
+
);
|
|
376
|
+
for (const w1 of memWords) {
|
|
377
|
+
for (const w2 of memWords) {
|
|
378
|
+
if (w1 >= w2) continue;
|
|
379
|
+
const key = `${w1}\u0000${w2}`;
|
|
380
|
+
pairWords.set(key, [w1, w2]);
|
|
381
|
+
increment(cooccurrence, key);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const [key, count] of mostCommon(cooccurrence, 3)) {
|
|
386
|
+
const pair = pairWords.get(key);
|
|
387
|
+
if (pair === undefined) continue;
|
|
388
|
+
const [w1, w2] = pair;
|
|
389
|
+
const confidence = Math.min(1.0, count / memories.length);
|
|
390
|
+
if (count >= 2 && confidence >= this.minConfidence) {
|
|
391
|
+
patterns.push(
|
|
392
|
+
new DetectedPattern({
|
|
393
|
+
patternType: "content",
|
|
394
|
+
description: `Co-occurring topics: '${w1}' + '${w2}' appear together ${count} times`,
|
|
395
|
+
confidence,
|
|
396
|
+
samples: memories
|
|
397
|
+
.filter(mem => {
|
|
398
|
+
const content = contentOf(mem).toLowerCase();
|
|
399
|
+
return content.includes(w1) && content.includes(w2);
|
|
400
|
+
})
|
|
401
|
+
.slice(0, 3)
|
|
402
|
+
.map(contentOf),
|
|
403
|
+
metadata: { word1: w1, word2: w2, count },
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return patterns;
|
|
410
|
+
}
|
|
411
|
+
detectSequence(memories: readonly MemoryRecord[]): DetectedPattern[] {
|
|
412
|
+
const patterns: DetectedPattern[] = [];
|
|
413
|
+
if (memories.length < 3) return patterns;
|
|
414
|
+
const sortedMems = memories
|
|
415
|
+
.filter(mem => typeof mem.timestamp === "string" && mem.timestamp.length > 0)
|
|
416
|
+
.sort((left, right) => String(left.timestamp).localeCompare(String(right.timestamp)));
|
|
417
|
+
const sources = sortedMems.map(sourceOf);
|
|
418
|
+
const pairCounts = new Map<string, number>();
|
|
419
|
+
const pairSources = new Map<string, readonly [string, string]>();
|
|
420
|
+
for (let i = 0; i < sources.length - 1; i++) {
|
|
421
|
+
const s1 = sources[i];
|
|
422
|
+
const s2 = sources[i + 1];
|
|
423
|
+
if (s1 === undefined || s2 === undefined) continue;
|
|
424
|
+
const key = `${s1}\u0000${s2}`;
|
|
425
|
+
pairSources.set(key, [s1, s2]);
|
|
426
|
+
increment(pairCounts, key);
|
|
427
|
+
}
|
|
428
|
+
for (const [key, count] of mostCommon(pairCounts, 3)) {
|
|
429
|
+
const pair = pairSources.get(key);
|
|
430
|
+
if (pair === undefined) continue;
|
|
431
|
+
const [s1, s2] = pair;
|
|
432
|
+
const confidence = Math.min(1.0, count / Math.max(2, sources.length - 1));
|
|
433
|
+
if (count >= 2 && confidence >= this.minConfidence) {
|
|
434
|
+
const samples: string[] = [];
|
|
435
|
+
for (let i = 0; i < sources.length - 1; i++) {
|
|
436
|
+
if (sources[i] === s1 && sources[i + 1] === s2) {
|
|
437
|
+
const first = sortedMems[i];
|
|
438
|
+
const second = sortedMems[i + 1];
|
|
439
|
+
if (first !== undefined && second !== undefined) {
|
|
440
|
+
samples.push(`${contentOf(first).slice(0, 50)}... -> ${contentOf(second).slice(0, 50)}...`);
|
|
441
|
+
}
|
|
442
|
+
if (samples.length >= 2) break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
patterns.push(
|
|
446
|
+
new DetectedPattern({
|
|
447
|
+
patternType: "sequence",
|
|
448
|
+
description: `Sequence pattern: '${s1}' often followed by '${s2}' (${count} times)`,
|
|
449
|
+
confidence,
|
|
450
|
+
samples,
|
|
451
|
+
metadata: { source1: s1, source2: s2, count },
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return patterns;
|
|
457
|
+
}
|
|
458
|
+
detectAll(memories: readonly MemoryRecord[]): DetectedPattern[] {
|
|
459
|
+
const patterns = [
|
|
460
|
+
...this.detectTemporal(memories),
|
|
461
|
+
...this.detectContent(memories),
|
|
462
|
+
...this.detectSequence(memories),
|
|
463
|
+
];
|
|
464
|
+
patterns.sort((left, right) => right.confidence - left.confidence);
|
|
465
|
+
return patterns;
|
|
466
|
+
}
|
|
467
|
+
summarizePatterns(memories: readonly MemoryRecord[]): Record<string, unknown> {
|
|
468
|
+
const patterns = this.detectAll(memories);
|
|
469
|
+
return {
|
|
470
|
+
total_memories: memories.length,
|
|
471
|
+
patterns_found: patterns.length,
|
|
472
|
+
temporal_patterns: patterns
|
|
473
|
+
.filter(pattern => pattern.patternType === "temporal")
|
|
474
|
+
.map(pattern => pattern.toDict()),
|
|
475
|
+
content_patterns: patterns
|
|
476
|
+
.filter(pattern => pattern.patternType === "content")
|
|
477
|
+
.map(pattern => pattern.toDict()),
|
|
478
|
+
sequence_patterns: patterns
|
|
479
|
+
.filter(pattern => pattern.patternType === "sequence")
|
|
480
|
+
.map(pattern => pattern.toDict()),
|
|
481
|
+
top_pattern: patterns[0]?.toDict() ?? null,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|