@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,1402 @@
|
|
|
1
|
+
// src/stores/implementations/InMemoryStore.ts
|
|
2
|
+
import { LRUCache } from "lru-cache";
|
|
3
|
+
var InMemoryStore = class {
|
|
4
|
+
cache;
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.cache = new LRUCache({
|
|
7
|
+
max: config.maxSize ?? 1e4,
|
|
8
|
+
ttl: config.ttl,
|
|
9
|
+
updateAgeOnGet: true
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Add a memory entry
|
|
14
|
+
*/
|
|
15
|
+
async add(entry) {
|
|
16
|
+
this.cache.set(entry.id, entry);
|
|
17
|
+
return Promise.resolve(entry.id);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get a memory entry by ID
|
|
21
|
+
*/
|
|
22
|
+
async get(id) {
|
|
23
|
+
const entry = this.cache.get(id);
|
|
24
|
+
if (entry) {
|
|
25
|
+
entry.accessCount++;
|
|
26
|
+
entry.lastAccessedAt = Date.now();
|
|
27
|
+
this.cache.set(id, entry);
|
|
28
|
+
}
|
|
29
|
+
return Promise.resolve(entry ?? null);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Update a memory entry
|
|
33
|
+
*/
|
|
34
|
+
async update(id, updates) {
|
|
35
|
+
const entry = this.cache.get(id);
|
|
36
|
+
if (!entry) {
|
|
37
|
+
return Promise.resolve(false);
|
|
38
|
+
}
|
|
39
|
+
const updated = {
|
|
40
|
+
...entry,
|
|
41
|
+
...updates,
|
|
42
|
+
metadata: {
|
|
43
|
+
...entry.metadata,
|
|
44
|
+
...updates.metadata
|
|
45
|
+
},
|
|
46
|
+
updatedAt: Date.now()
|
|
47
|
+
};
|
|
48
|
+
this.cache.set(id, updated);
|
|
49
|
+
return Promise.resolve(true);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Delete a memory entry
|
|
53
|
+
*/
|
|
54
|
+
async delete(id) {
|
|
55
|
+
return Promise.resolve(this.cache.delete(id));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Query memory entries
|
|
59
|
+
*/
|
|
60
|
+
async query(options) {
|
|
61
|
+
const entries = [];
|
|
62
|
+
const limit = options.limit ?? 100;
|
|
63
|
+
const offset = options.offset ?? 0;
|
|
64
|
+
for (const entry of this.cache.values()) {
|
|
65
|
+
if (this.matchesQuery(entry, options)) {
|
|
66
|
+
entries.push(entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
entries.sort((a, b) => b.timestamp - a.timestamp);
|
|
70
|
+
const paginated = entries.slice(offset, offset + limit);
|
|
71
|
+
return Promise.resolve({
|
|
72
|
+
entries: paginated,
|
|
73
|
+
total: entries.length,
|
|
74
|
+
hasMore: offset + limit < entries.length
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Search by vector similarity
|
|
79
|
+
*/
|
|
80
|
+
async search(embedding, options) {
|
|
81
|
+
const results = [];
|
|
82
|
+
for (const entry of this.cache.values()) {
|
|
83
|
+
if (!entry.embedding) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (options.filter) {
|
|
87
|
+
if (!this.matchesFilter(entry, options.filter)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (options.namespace && entry.metadata.namespace !== options.namespace) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const score = this.cosineSimilarity(embedding, entry.embedding);
|
|
95
|
+
if (options.minScore !== void 0 && score < options.minScore) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
results.push({ entry, score });
|
|
99
|
+
}
|
|
100
|
+
results.sort((a, b) => b.score - a.score);
|
|
101
|
+
return Promise.resolve(results.slice(0, options.topK));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Clear entries
|
|
105
|
+
*/
|
|
106
|
+
async clear(options) {
|
|
107
|
+
if (!options) {
|
|
108
|
+
const size = this.cache.size;
|
|
109
|
+
this.cache.clear();
|
|
110
|
+
return Promise.resolve(size);
|
|
111
|
+
}
|
|
112
|
+
let deleted = 0;
|
|
113
|
+
const toDelete = [];
|
|
114
|
+
for (const [id, entry] of this.cache.entries()) {
|
|
115
|
+
let shouldDelete = true;
|
|
116
|
+
if (options.namespace && entry.metadata.namespace !== options.namespace) {
|
|
117
|
+
shouldDelete = false;
|
|
118
|
+
}
|
|
119
|
+
if (options.userId && entry.metadata.userId !== options.userId) {
|
|
120
|
+
shouldDelete = false;
|
|
121
|
+
}
|
|
122
|
+
if (shouldDelete) {
|
|
123
|
+
toDelete.push(id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const id of toDelete) {
|
|
127
|
+
this.cache.delete(id);
|
|
128
|
+
deleted++;
|
|
129
|
+
}
|
|
130
|
+
return Promise.resolve(deleted);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Count entries
|
|
134
|
+
*/
|
|
135
|
+
async count(options) {
|
|
136
|
+
if (!options) {
|
|
137
|
+
return Promise.resolve(this.cache.size);
|
|
138
|
+
}
|
|
139
|
+
let count = 0;
|
|
140
|
+
for (const entry of this.cache.values()) {
|
|
141
|
+
if (this.matchesQuery(entry, options)) {
|
|
142
|
+
count++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return Promise.resolve(count);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Close the store (no-op for in-memory)
|
|
149
|
+
*/
|
|
150
|
+
async close() {
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check if entry matches query
|
|
154
|
+
*/
|
|
155
|
+
matchesQuery(entry, options) {
|
|
156
|
+
if (options.query) {
|
|
157
|
+
const queryLower = options.query.toLowerCase();
|
|
158
|
+
if (!entry.content.toLowerCase().includes(queryLower)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (options.userId && entry.metadata.userId !== options.userId) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (options.agentId && entry.metadata.agentId !== options.agentId) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (options.conversationId && entry.metadata.conversationId !== options.conversationId) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (options.sessionId && entry.metadata.sessionId !== options.sessionId) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (options.namespace && entry.metadata.namespace !== options.namespace) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (options.types && options.types.length > 0) {
|
|
178
|
+
if (!options.types.includes(entry.type)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (options.tags && options.tags.length > 0) {
|
|
183
|
+
const entryTags = entry.metadata.tags ?? [];
|
|
184
|
+
const hasAllTags = options.tags.every((tag) => entryTags.includes(tag));
|
|
185
|
+
if (!hasAllTags) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (options.minImportance !== void 0 && entry.importance < options.minImportance) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
if (options.startTime !== void 0 && entry.timestamp < options.startTime) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (options.endTime !== void 0 && entry.timestamp > options.endTime) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (!options.includeExpired && entry.expiresAt) {
|
|
199
|
+
if (entry.expiresAt < Date.now()) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if entry matches filter
|
|
207
|
+
*/
|
|
208
|
+
matchesFilter(entry, filter) {
|
|
209
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
210
|
+
if (value === void 0) continue;
|
|
211
|
+
if (key in entry.metadata) {
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
if (!value.includes(entry.metadata[key])) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
} else if (entry.metadata[key] !== value) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
} else if (key in entry) {
|
|
220
|
+
const entryValue = entry[key];
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
if (!value.includes(entryValue)) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
} else if (entryValue !== value) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Calculate cosine similarity
|
|
234
|
+
*/
|
|
235
|
+
cosineSimilarity(a, b) {
|
|
236
|
+
if (a.length !== b.length) {
|
|
237
|
+
throw new Error("Vectors must have the same length");
|
|
238
|
+
}
|
|
239
|
+
let dotProduct = 0;
|
|
240
|
+
let normA = 0;
|
|
241
|
+
let normB = 0;
|
|
242
|
+
for (let i = 0; i < a.length; i++) {
|
|
243
|
+
dotProduct += a[i] * b[i];
|
|
244
|
+
normA += a[i] * a[i];
|
|
245
|
+
normB += b[i] * b[i];
|
|
246
|
+
}
|
|
247
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
248
|
+
if (magnitude === 0) {
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
return dotProduct / magnitude;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get all entries (for debugging)
|
|
255
|
+
*/
|
|
256
|
+
getAllEntries() {
|
|
257
|
+
return Array.from(this.cache.values());
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get cache size
|
|
261
|
+
*/
|
|
262
|
+
get size() {
|
|
263
|
+
return this.cache.size;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
function createInMemoryStore(config) {
|
|
267
|
+
return new InMemoryStore(config);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/stores/implementations/SQLiteStore.ts
|
|
271
|
+
var SQLiteStore = class {
|
|
272
|
+
db = null;
|
|
273
|
+
config;
|
|
274
|
+
tableName;
|
|
275
|
+
constructor(config) {
|
|
276
|
+
this.config = config;
|
|
277
|
+
this.tableName = config.tableName ?? "memories";
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Initialize the database
|
|
281
|
+
*/
|
|
282
|
+
async initialize() {
|
|
283
|
+
const BetterSqlite3 = (await import("better-sqlite3")).default;
|
|
284
|
+
this.db = new BetterSqlite3(this.config.path);
|
|
285
|
+
if (this.config.enableWAL !== false) {
|
|
286
|
+
this.db.pragma("journal_mode = WAL");
|
|
287
|
+
}
|
|
288
|
+
this.db.exec(`
|
|
289
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
290
|
+
id TEXT PRIMARY KEY,
|
|
291
|
+
content TEXT NOT NULL,
|
|
292
|
+
embedding BLOB,
|
|
293
|
+
type TEXT NOT NULL,
|
|
294
|
+
importance REAL NOT NULL,
|
|
295
|
+
metadata TEXT NOT NULL,
|
|
296
|
+
timestamp INTEGER NOT NULL,
|
|
297
|
+
expires_at INTEGER,
|
|
298
|
+
parent_id TEXT,
|
|
299
|
+
access_count INTEGER DEFAULT 0,
|
|
300
|
+
last_accessed_at INTEGER,
|
|
301
|
+
created_at INTEGER NOT NULL,
|
|
302
|
+
updated_at INTEGER NOT NULL
|
|
303
|
+
)
|
|
304
|
+
`);
|
|
305
|
+
this.db.exec(`
|
|
306
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_timestamp ON ${this.tableName}(timestamp);
|
|
307
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_type ON ${this.tableName}(type);
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_importance ON ${this.tableName}(importance);
|
|
309
|
+
`);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Ensure database is initialized
|
|
313
|
+
*/
|
|
314
|
+
async ensureInitialized() {
|
|
315
|
+
if (!this.db) {
|
|
316
|
+
await this.initialize();
|
|
317
|
+
}
|
|
318
|
+
return this.db;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Add a memory entry
|
|
322
|
+
*/
|
|
323
|
+
async add(entry) {
|
|
324
|
+
const db = await this.ensureInitialized();
|
|
325
|
+
const stmt = db.prepare(`
|
|
326
|
+
INSERT INTO ${this.tableName} (
|
|
327
|
+
id, content, embedding, type, importance, metadata,
|
|
328
|
+
timestamp, expires_at, parent_id, access_count,
|
|
329
|
+
last_accessed_at, created_at, updated_at
|
|
330
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
331
|
+
`);
|
|
332
|
+
stmt.run(
|
|
333
|
+
entry.id,
|
|
334
|
+
entry.content,
|
|
335
|
+
entry.embedding ? this.serializeVector(entry.embedding) : null,
|
|
336
|
+
entry.type,
|
|
337
|
+
entry.importance,
|
|
338
|
+
JSON.stringify(entry.metadata),
|
|
339
|
+
entry.timestamp,
|
|
340
|
+
entry.expiresAt ?? null,
|
|
341
|
+
entry.parentId ?? null,
|
|
342
|
+
entry.accessCount,
|
|
343
|
+
entry.lastAccessedAt ?? null,
|
|
344
|
+
entry.createdAt,
|
|
345
|
+
entry.updatedAt
|
|
346
|
+
);
|
|
347
|
+
return entry.id;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get a memory entry by ID
|
|
351
|
+
*/
|
|
352
|
+
async get(id) {
|
|
353
|
+
const db = await this.ensureInitialized();
|
|
354
|
+
const stmt = db.prepare(`SELECT * FROM ${this.tableName} WHERE id = ?`);
|
|
355
|
+
const row = stmt.get(id);
|
|
356
|
+
if (!row) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const updateStmt = db.prepare(`
|
|
360
|
+
UPDATE ${this.tableName}
|
|
361
|
+
SET access_count = access_count + 1, last_accessed_at = ?
|
|
362
|
+
WHERE id = ?
|
|
363
|
+
`);
|
|
364
|
+
updateStmt.run(Date.now(), id);
|
|
365
|
+
return Promise.resolve(this.rowToEntry(row));
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Update a memory entry
|
|
369
|
+
*/
|
|
370
|
+
async update(id, updates) {
|
|
371
|
+
const db = await this.ensureInitialized();
|
|
372
|
+
const existing = await this.get(id);
|
|
373
|
+
if (!existing) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
const updated = {
|
|
377
|
+
...existing,
|
|
378
|
+
...updates,
|
|
379
|
+
metadata: {
|
|
380
|
+
...existing.metadata,
|
|
381
|
+
...updates.metadata
|
|
382
|
+
},
|
|
383
|
+
updatedAt: Date.now()
|
|
384
|
+
};
|
|
385
|
+
const stmt = db.prepare(`
|
|
386
|
+
UPDATE ${this.tableName}
|
|
387
|
+
SET content = ?, embedding = ?, type = ?, importance = ?,
|
|
388
|
+
metadata = ?, expires_at = ?, updated_at = ?
|
|
389
|
+
WHERE id = ?
|
|
390
|
+
`);
|
|
391
|
+
const result = stmt.run(
|
|
392
|
+
updated.content,
|
|
393
|
+
updated.embedding ? this.serializeVector(updated.embedding) : null,
|
|
394
|
+
updated.type,
|
|
395
|
+
updated.importance,
|
|
396
|
+
JSON.stringify(updated.metadata),
|
|
397
|
+
updated.expiresAt ?? null,
|
|
398
|
+
updated.updatedAt,
|
|
399
|
+
id
|
|
400
|
+
);
|
|
401
|
+
return result.changes > 0;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Delete a memory entry
|
|
405
|
+
*/
|
|
406
|
+
async delete(id) {
|
|
407
|
+
const db = await this.ensureInitialized();
|
|
408
|
+
const stmt = db.prepare(`DELETE FROM ${this.tableName} WHERE id = ?`);
|
|
409
|
+
const result = stmt.run(id);
|
|
410
|
+
return result.changes > 0;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Query memory entries
|
|
414
|
+
*/
|
|
415
|
+
async query(options) {
|
|
416
|
+
const db = await this.ensureInitialized();
|
|
417
|
+
const conditions = [];
|
|
418
|
+
const params = [];
|
|
419
|
+
if (options.query) {
|
|
420
|
+
conditions.push("content LIKE ?");
|
|
421
|
+
params.push(`%${options.query}%`);
|
|
422
|
+
}
|
|
423
|
+
if (options.userId) {
|
|
424
|
+
conditions.push("json_extract(metadata, '$.userId') = ?");
|
|
425
|
+
params.push(options.userId);
|
|
426
|
+
}
|
|
427
|
+
if (options.agentId) {
|
|
428
|
+
conditions.push("json_extract(metadata, '$.agentId') = ?");
|
|
429
|
+
params.push(options.agentId);
|
|
430
|
+
}
|
|
431
|
+
if (options.conversationId) {
|
|
432
|
+
conditions.push("json_extract(metadata, '$.conversationId') = ?");
|
|
433
|
+
params.push(options.conversationId);
|
|
434
|
+
}
|
|
435
|
+
if (options.namespace) {
|
|
436
|
+
conditions.push("json_extract(metadata, '$.namespace') = ?");
|
|
437
|
+
params.push(options.namespace);
|
|
438
|
+
}
|
|
439
|
+
if (options.types && options.types.length > 0) {
|
|
440
|
+
const placeholders = options.types.map(() => "?").join(", ");
|
|
441
|
+
conditions.push(`type IN (${placeholders})`);
|
|
442
|
+
params.push(...options.types);
|
|
443
|
+
}
|
|
444
|
+
if (options.minImportance !== void 0) {
|
|
445
|
+
conditions.push("importance >= ?");
|
|
446
|
+
params.push(options.minImportance);
|
|
447
|
+
}
|
|
448
|
+
if (options.startTime !== void 0) {
|
|
449
|
+
conditions.push("timestamp >= ?");
|
|
450
|
+
params.push(options.startTime);
|
|
451
|
+
}
|
|
452
|
+
if (options.endTime !== void 0) {
|
|
453
|
+
conditions.push("timestamp <= ?");
|
|
454
|
+
params.push(options.endTime);
|
|
455
|
+
}
|
|
456
|
+
if (!options.includeExpired) {
|
|
457
|
+
conditions.push("(expires_at IS NULL OR expires_at > ?)");
|
|
458
|
+
params.push(Date.now());
|
|
459
|
+
}
|
|
460
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
461
|
+
const countStmt = db.prepare(
|
|
462
|
+
`SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
|
|
463
|
+
);
|
|
464
|
+
const countResult = countStmt.get(...params);
|
|
465
|
+
const total = countResult.count;
|
|
466
|
+
const limit = options.limit ?? 100;
|
|
467
|
+
const offset = options.offset ?? 0;
|
|
468
|
+
const queryStmt = db.prepare(`
|
|
469
|
+
SELECT * FROM ${this.tableName}
|
|
470
|
+
${whereClause}
|
|
471
|
+
ORDER BY timestamp DESC
|
|
472
|
+
LIMIT ? OFFSET ?
|
|
473
|
+
`);
|
|
474
|
+
const rows = queryStmt.all(...params, limit, offset);
|
|
475
|
+
const entries = rows.map((row) => this.rowToEntry(row));
|
|
476
|
+
return {
|
|
477
|
+
entries,
|
|
478
|
+
total,
|
|
479
|
+
hasMore: offset + limit < total
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Search by vector similarity
|
|
484
|
+
*/
|
|
485
|
+
async search(embedding, options) {
|
|
486
|
+
const db = await this.ensureInitialized();
|
|
487
|
+
const conditions = ["embedding IS NOT NULL"];
|
|
488
|
+
const params = [];
|
|
489
|
+
if (options.namespace) {
|
|
490
|
+
conditions.push("json_extract(metadata, '$.namespace') = ?");
|
|
491
|
+
params.push(options.namespace);
|
|
492
|
+
}
|
|
493
|
+
if (options.filter) {
|
|
494
|
+
for (const [key, value] of Object.entries(options.filter)) {
|
|
495
|
+
if (value !== void 0) {
|
|
496
|
+
if (Array.isArray(value)) {
|
|
497
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
498
|
+
conditions.push(
|
|
499
|
+
`json_extract(metadata, '$.${key}') IN (${placeholders})`
|
|
500
|
+
);
|
|
501
|
+
params.push(...value);
|
|
502
|
+
} else {
|
|
503
|
+
conditions.push(`json_extract(metadata, '$.${key}') = ?`);
|
|
504
|
+
params.push(value);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
510
|
+
const stmt = db.prepare(`SELECT * FROM ${this.tableName} ${whereClause}`);
|
|
511
|
+
const rows = stmt.all(...params);
|
|
512
|
+
const results = [];
|
|
513
|
+
for (const row of rows) {
|
|
514
|
+
const entry = this.rowToEntry(row);
|
|
515
|
+
if (!entry.embedding) continue;
|
|
516
|
+
const score = this.cosineSimilarity(embedding, entry.embedding);
|
|
517
|
+
if (options.minScore === void 0 || score >= options.minScore) {
|
|
518
|
+
results.push({ entry, score });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
results.sort((a, b) => b.score - a.score);
|
|
522
|
+
return Promise.resolve(results.slice(0, options.topK));
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Clear entries
|
|
526
|
+
*/
|
|
527
|
+
async clear(options) {
|
|
528
|
+
const db = await this.ensureInitialized();
|
|
529
|
+
if (!options) {
|
|
530
|
+
const stmt2 = db.prepare(`DELETE FROM ${this.tableName}`);
|
|
531
|
+
const result2 = stmt2.run();
|
|
532
|
+
return result2.changes;
|
|
533
|
+
}
|
|
534
|
+
const conditions = [];
|
|
535
|
+
const params = [];
|
|
536
|
+
if (options.namespace) {
|
|
537
|
+
conditions.push("json_extract(metadata, '$.namespace') = ?");
|
|
538
|
+
params.push(options.namespace);
|
|
539
|
+
}
|
|
540
|
+
if (options.userId) {
|
|
541
|
+
conditions.push("json_extract(metadata, '$.userId') = ?");
|
|
542
|
+
params.push(options.userId);
|
|
543
|
+
}
|
|
544
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
545
|
+
const stmt = db.prepare(`DELETE FROM ${this.tableName} ${whereClause}`);
|
|
546
|
+
const result = stmt.run(...params);
|
|
547
|
+
return result.changes;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Count entries
|
|
551
|
+
*/
|
|
552
|
+
async count(options) {
|
|
553
|
+
const db = await this.ensureInitialized();
|
|
554
|
+
if (!options) {
|
|
555
|
+
const stmt = db.prepare(
|
|
556
|
+
`SELECT COUNT(*) as count FROM ${this.tableName}`
|
|
557
|
+
);
|
|
558
|
+
const result = stmt.get();
|
|
559
|
+
return result.count;
|
|
560
|
+
}
|
|
561
|
+
const { total } = await this.query({ ...options, limit: 0 });
|
|
562
|
+
return total;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Close the database
|
|
566
|
+
*/
|
|
567
|
+
async close() {
|
|
568
|
+
if (this.db) {
|
|
569
|
+
this.db.close();
|
|
570
|
+
this.db = null;
|
|
571
|
+
}
|
|
572
|
+
return Promise.resolve();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Convert database row to MemoryEntry
|
|
576
|
+
*/
|
|
577
|
+
rowToEntry(row) {
|
|
578
|
+
return {
|
|
579
|
+
id: row.id,
|
|
580
|
+
content: row.content,
|
|
581
|
+
embedding: row.embedding ? this.deserializeVector(row.embedding) : void 0,
|
|
582
|
+
type: row.type,
|
|
583
|
+
importance: row.importance,
|
|
584
|
+
metadata: JSON.parse(row.metadata),
|
|
585
|
+
timestamp: row.timestamp,
|
|
586
|
+
expiresAt: row.expires_at,
|
|
587
|
+
parentId: row.parent_id,
|
|
588
|
+
accessCount: row.access_count,
|
|
589
|
+
lastAccessedAt: row.last_accessed_at,
|
|
590
|
+
createdAt: row.created_at,
|
|
591
|
+
updatedAt: row.updated_at
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Serialize vector to buffer
|
|
596
|
+
*/
|
|
597
|
+
serializeVector(vector) {
|
|
598
|
+
const buffer = Buffer.alloc(vector.length * 4);
|
|
599
|
+
for (let i = 0; i < vector.length; i++) {
|
|
600
|
+
buffer.writeFloatLE(vector[i], i * 4);
|
|
601
|
+
}
|
|
602
|
+
return buffer;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Deserialize vector from buffer
|
|
606
|
+
*/
|
|
607
|
+
deserializeVector(buffer) {
|
|
608
|
+
const vector = [];
|
|
609
|
+
for (let i = 0; i < buffer.length / 4; i++) {
|
|
610
|
+
vector.push(buffer.readFloatLE(i * 4));
|
|
611
|
+
}
|
|
612
|
+
return vector;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Calculate cosine similarity
|
|
616
|
+
*/
|
|
617
|
+
cosineSimilarity(a, b) {
|
|
618
|
+
if (a.length !== b.length) {
|
|
619
|
+
throw new Error("Vectors must have the same length");
|
|
620
|
+
}
|
|
621
|
+
let dotProduct = 0;
|
|
622
|
+
let normA = 0;
|
|
623
|
+
let normB = 0;
|
|
624
|
+
for (let i = 0; i < a.length; i++) {
|
|
625
|
+
dotProduct += a[i] * b[i];
|
|
626
|
+
normA += a[i] * a[i];
|
|
627
|
+
normB += b[i] * b[i];
|
|
628
|
+
}
|
|
629
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
630
|
+
if (magnitude === 0) {
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
return dotProduct / magnitude;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
async function createSQLiteStore(config) {
|
|
637
|
+
const store = new SQLiteStore(config);
|
|
638
|
+
await store.initialize();
|
|
639
|
+
return store;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/stores/implementations/PostgresStore.ts
|
|
643
|
+
var PostgresStore = class {
|
|
644
|
+
pool = null;
|
|
645
|
+
config;
|
|
646
|
+
tableName;
|
|
647
|
+
vectorDimensions;
|
|
648
|
+
initialized = false;
|
|
649
|
+
constructor(config) {
|
|
650
|
+
this.config = config;
|
|
651
|
+
this.tableName = config.tableName ?? "memories";
|
|
652
|
+
this.vectorDimensions = config.vectorDimensions ?? 1536;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Initialize the database
|
|
656
|
+
*/
|
|
657
|
+
async initialize() {
|
|
658
|
+
if (this.initialized) return;
|
|
659
|
+
const { Pool } = await import("pg");
|
|
660
|
+
if (this.config.connectionString) {
|
|
661
|
+
this.pool = new Pool({
|
|
662
|
+
connectionString: this.config.connectionString,
|
|
663
|
+
max: this.config.poolSize ?? 10,
|
|
664
|
+
ssl: this.config.ssl
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
this.pool = new Pool({
|
|
668
|
+
host: this.config.host ?? "localhost",
|
|
669
|
+
port: this.config.port ?? 5432,
|
|
670
|
+
database: this.config.database ?? "agentsea",
|
|
671
|
+
user: this.config.user ?? "postgres",
|
|
672
|
+
password: this.config.password,
|
|
673
|
+
max: this.config.poolSize ?? 10,
|
|
674
|
+
ssl: this.config.ssl
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
await this.pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
678
|
+
await this.pool.query(`
|
|
679
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
680
|
+
id TEXT PRIMARY KEY,
|
|
681
|
+
content TEXT NOT NULL,
|
|
682
|
+
embedding vector(${this.vectorDimensions}),
|
|
683
|
+
type TEXT NOT NULL,
|
|
684
|
+
importance REAL NOT NULL,
|
|
685
|
+
metadata JSONB NOT NULL,
|
|
686
|
+
timestamp BIGINT NOT NULL,
|
|
687
|
+
expires_at BIGINT,
|
|
688
|
+
parent_id TEXT,
|
|
689
|
+
access_count INTEGER DEFAULT 0,
|
|
690
|
+
last_accessed_at BIGINT,
|
|
691
|
+
created_at BIGINT NOT NULL,
|
|
692
|
+
updated_at BIGINT NOT NULL
|
|
693
|
+
)
|
|
694
|
+
`);
|
|
695
|
+
await this.pool.query(`
|
|
696
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_timestamp ON ${this.tableName}(timestamp);
|
|
697
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_type ON ${this.tableName}(type);
|
|
698
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_importance ON ${this.tableName}(importance);
|
|
699
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_metadata ON ${this.tableName} USING GIN(metadata);
|
|
700
|
+
`);
|
|
701
|
+
await this.pool.query(
|
|
702
|
+
`
|
|
703
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_embedding
|
|
704
|
+
ON ${this.tableName} USING ivfflat (embedding vector_cosine_ops)
|
|
705
|
+
WITH (lists = 100)
|
|
706
|
+
`
|
|
707
|
+
).catch(() => {
|
|
708
|
+
});
|
|
709
|
+
this.initialized = true;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Ensure database is initialized
|
|
713
|
+
*/
|
|
714
|
+
async ensureInitialized() {
|
|
715
|
+
if (!this.initialized) {
|
|
716
|
+
await this.initialize();
|
|
717
|
+
}
|
|
718
|
+
return this.pool;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Add a memory entry
|
|
722
|
+
*/
|
|
723
|
+
async add(entry) {
|
|
724
|
+
const pool = await this.ensureInitialized();
|
|
725
|
+
const embeddingValue = entry.embedding ? `[${entry.embedding.join(",")}]` : null;
|
|
726
|
+
await pool.query(
|
|
727
|
+
`INSERT INTO ${this.tableName} (
|
|
728
|
+
id, content, embedding, type, importance, metadata,
|
|
729
|
+
timestamp, expires_at, parent_id, access_count,
|
|
730
|
+
last_accessed_at, created_at, updated_at
|
|
731
|
+
) VALUES ($1, $2, $3::vector, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
|
732
|
+
[
|
|
733
|
+
entry.id,
|
|
734
|
+
entry.content,
|
|
735
|
+
embeddingValue,
|
|
736
|
+
entry.type,
|
|
737
|
+
entry.importance,
|
|
738
|
+
JSON.stringify(entry.metadata),
|
|
739
|
+
entry.timestamp,
|
|
740
|
+
entry.expiresAt ?? null,
|
|
741
|
+
entry.parentId ?? null,
|
|
742
|
+
entry.accessCount,
|
|
743
|
+
entry.lastAccessedAt ?? null,
|
|
744
|
+
entry.createdAt,
|
|
745
|
+
entry.updatedAt
|
|
746
|
+
]
|
|
747
|
+
);
|
|
748
|
+
return entry.id;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Get a memory entry by ID
|
|
752
|
+
*/
|
|
753
|
+
async get(id) {
|
|
754
|
+
const pool = await this.ensureInitialized();
|
|
755
|
+
const result = await pool.query(
|
|
756
|
+
`SELECT * FROM ${this.tableName} WHERE id = $1`,
|
|
757
|
+
[id]
|
|
758
|
+
);
|
|
759
|
+
if (result.rows.length === 0) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
await pool.query(
|
|
763
|
+
`UPDATE ${this.tableName}
|
|
764
|
+
SET access_count = access_count + 1, last_accessed_at = $1
|
|
765
|
+
WHERE id = $2`,
|
|
766
|
+
[Date.now(), id]
|
|
767
|
+
);
|
|
768
|
+
return Promise.resolve(this.rowToEntry(result.rows[0]));
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Update a memory entry
|
|
772
|
+
*/
|
|
773
|
+
async update(id, updates) {
|
|
774
|
+
const pool = await this.ensureInitialized();
|
|
775
|
+
const existing = await this.get(id);
|
|
776
|
+
if (!existing) {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
const updated = {
|
|
780
|
+
...existing,
|
|
781
|
+
...updates,
|
|
782
|
+
metadata: {
|
|
783
|
+
...existing.metadata,
|
|
784
|
+
...updates.metadata
|
|
785
|
+
},
|
|
786
|
+
updatedAt: Date.now()
|
|
787
|
+
};
|
|
788
|
+
const embeddingValue = updated.embedding ? `[${updated.embedding.join(",")}]` : null;
|
|
789
|
+
const result = await pool.query(
|
|
790
|
+
`UPDATE ${this.tableName}
|
|
791
|
+
SET content = $1, embedding = $2::vector, type = $3, importance = $4,
|
|
792
|
+
metadata = $5, expires_at = $6, updated_at = $7
|
|
793
|
+
WHERE id = $8`,
|
|
794
|
+
[
|
|
795
|
+
updated.content,
|
|
796
|
+
embeddingValue,
|
|
797
|
+
updated.type,
|
|
798
|
+
updated.importance,
|
|
799
|
+
JSON.stringify(updated.metadata),
|
|
800
|
+
updated.expiresAt ?? null,
|
|
801
|
+
updated.updatedAt,
|
|
802
|
+
id
|
|
803
|
+
]
|
|
804
|
+
);
|
|
805
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Delete a memory entry
|
|
809
|
+
*/
|
|
810
|
+
async delete(id) {
|
|
811
|
+
const pool = await this.ensureInitialized();
|
|
812
|
+
const result = await pool.query(
|
|
813
|
+
`DELETE FROM ${this.tableName} WHERE id = $1`,
|
|
814
|
+
[id]
|
|
815
|
+
);
|
|
816
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Query memory entries
|
|
820
|
+
*/
|
|
821
|
+
async query(options) {
|
|
822
|
+
const pool = await this.ensureInitialized();
|
|
823
|
+
const conditions = [];
|
|
824
|
+
const params = [];
|
|
825
|
+
let paramIndex = 1;
|
|
826
|
+
if (options.query) {
|
|
827
|
+
conditions.push(`content ILIKE $${paramIndex}`);
|
|
828
|
+
params.push(`%${options.query}%`);
|
|
829
|
+
paramIndex++;
|
|
830
|
+
}
|
|
831
|
+
if (options.userId) {
|
|
832
|
+
conditions.push(`metadata->>'userId' = $${paramIndex}`);
|
|
833
|
+
params.push(options.userId);
|
|
834
|
+
paramIndex++;
|
|
835
|
+
}
|
|
836
|
+
if (options.agentId) {
|
|
837
|
+
conditions.push(`metadata->>'agentId' = $${paramIndex}`);
|
|
838
|
+
params.push(options.agentId);
|
|
839
|
+
paramIndex++;
|
|
840
|
+
}
|
|
841
|
+
if (options.conversationId) {
|
|
842
|
+
conditions.push(`metadata->>'conversationId' = $${paramIndex}`);
|
|
843
|
+
params.push(options.conversationId);
|
|
844
|
+
paramIndex++;
|
|
845
|
+
}
|
|
846
|
+
if (options.namespace) {
|
|
847
|
+
conditions.push(`metadata->>'namespace' = $${paramIndex}`);
|
|
848
|
+
params.push(options.namespace);
|
|
849
|
+
paramIndex++;
|
|
850
|
+
}
|
|
851
|
+
if (options.types && options.types.length > 0) {
|
|
852
|
+
conditions.push(`type = ANY($${paramIndex})`);
|
|
853
|
+
params.push(options.types);
|
|
854
|
+
paramIndex++;
|
|
855
|
+
}
|
|
856
|
+
if (options.minImportance !== void 0) {
|
|
857
|
+
conditions.push(`importance >= $${paramIndex}`);
|
|
858
|
+
params.push(options.minImportance);
|
|
859
|
+
paramIndex++;
|
|
860
|
+
}
|
|
861
|
+
if (options.startTime !== void 0) {
|
|
862
|
+
conditions.push(`timestamp >= $${paramIndex}`);
|
|
863
|
+
params.push(options.startTime);
|
|
864
|
+
paramIndex++;
|
|
865
|
+
}
|
|
866
|
+
if (options.endTime !== void 0) {
|
|
867
|
+
conditions.push(`timestamp <= $${paramIndex}`);
|
|
868
|
+
params.push(options.endTime);
|
|
869
|
+
paramIndex++;
|
|
870
|
+
}
|
|
871
|
+
if (!options.includeExpired) {
|
|
872
|
+
conditions.push(`(expires_at IS NULL OR expires_at > $${paramIndex})`);
|
|
873
|
+
params.push(Date.now());
|
|
874
|
+
paramIndex++;
|
|
875
|
+
}
|
|
876
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
877
|
+
const countResult = await pool.query(
|
|
878
|
+
`SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`,
|
|
879
|
+
params
|
|
880
|
+
);
|
|
881
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
882
|
+
const limit = options.limit ?? 100;
|
|
883
|
+
const offset = options.offset ?? 0;
|
|
884
|
+
const queryResult = await pool.query(
|
|
885
|
+
`SELECT * FROM ${this.tableName}
|
|
886
|
+
${whereClause}
|
|
887
|
+
ORDER BY timestamp DESC
|
|
888
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
889
|
+
[...params, limit, offset]
|
|
890
|
+
);
|
|
891
|
+
const entries = queryResult.rows.map((row) => this.rowToEntry(row));
|
|
892
|
+
return {
|
|
893
|
+
entries,
|
|
894
|
+
total,
|
|
895
|
+
hasMore: offset + limit < total
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Search by vector similarity using pgvector
|
|
900
|
+
*/
|
|
901
|
+
async search(embedding, options) {
|
|
902
|
+
const pool = await this.ensureInitialized();
|
|
903
|
+
const conditions = ["embedding IS NOT NULL"];
|
|
904
|
+
const params = [`[${embedding.join(",")}]`];
|
|
905
|
+
let paramIndex = 2;
|
|
906
|
+
if (options.namespace) {
|
|
907
|
+
conditions.push(`metadata->>'namespace' = $${paramIndex}`);
|
|
908
|
+
params.push(options.namespace);
|
|
909
|
+
paramIndex++;
|
|
910
|
+
}
|
|
911
|
+
if (options.filter) {
|
|
912
|
+
for (const [key, value] of Object.entries(options.filter)) {
|
|
913
|
+
if (value !== void 0) {
|
|
914
|
+
if (Array.isArray(value)) {
|
|
915
|
+
conditions.push(`metadata->>'${key}' = ANY($${paramIndex})`);
|
|
916
|
+
params.push(value);
|
|
917
|
+
} else {
|
|
918
|
+
conditions.push(`metadata->>'${key}' = $${paramIndex}`);
|
|
919
|
+
params.push(value);
|
|
920
|
+
}
|
|
921
|
+
paramIndex++;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
926
|
+
const minScore = options.minScore ?? 0;
|
|
927
|
+
const result = await pool.query(
|
|
928
|
+
`SELECT *, 1 - (embedding <=> $1::vector) as score
|
|
929
|
+
FROM ${this.tableName}
|
|
930
|
+
${whereClause}
|
|
931
|
+
AND 1 - (embedding <=> $1::vector) >= $${paramIndex}
|
|
932
|
+
ORDER BY embedding <=> $1::vector
|
|
933
|
+
LIMIT $${paramIndex + 1}`,
|
|
934
|
+
[...params, minScore, options.topK]
|
|
935
|
+
);
|
|
936
|
+
return result.rows.map((row) => ({
|
|
937
|
+
entry: this.rowToEntry(row),
|
|
938
|
+
score: parseFloat(row.score)
|
|
939
|
+
}));
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Clear entries
|
|
943
|
+
*/
|
|
944
|
+
async clear(options) {
|
|
945
|
+
const pool = await this.ensureInitialized();
|
|
946
|
+
if (!options) {
|
|
947
|
+
const result2 = await pool.query(`DELETE FROM ${this.tableName}`);
|
|
948
|
+
return result2.rowCount ?? 0;
|
|
949
|
+
}
|
|
950
|
+
const conditions = [];
|
|
951
|
+
const params = [];
|
|
952
|
+
let paramIndex = 1;
|
|
953
|
+
if (options.namespace) {
|
|
954
|
+
conditions.push(`metadata->>'namespace' = $${paramIndex}`);
|
|
955
|
+
params.push(options.namespace);
|
|
956
|
+
paramIndex++;
|
|
957
|
+
}
|
|
958
|
+
if (options.userId) {
|
|
959
|
+
conditions.push(`metadata->>'userId' = $${paramIndex}`);
|
|
960
|
+
params.push(options.userId);
|
|
961
|
+
}
|
|
962
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
963
|
+
const result = await pool.query(
|
|
964
|
+
`DELETE FROM ${this.tableName} ${whereClause}`,
|
|
965
|
+
params
|
|
966
|
+
);
|
|
967
|
+
return result.rowCount ?? 0;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Count entries
|
|
971
|
+
*/
|
|
972
|
+
async count(options) {
|
|
973
|
+
const pool = await this.ensureInitialized();
|
|
974
|
+
if (!options) {
|
|
975
|
+
const result = await pool.query(
|
|
976
|
+
`SELECT COUNT(*) as count FROM ${this.tableName}`
|
|
977
|
+
);
|
|
978
|
+
return Promise.resolve(parseInt(result.rows[0].count, 10));
|
|
979
|
+
}
|
|
980
|
+
const { total } = await this.query({ ...options, limit: 0 });
|
|
981
|
+
return total;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Close the connection pool
|
|
985
|
+
*/
|
|
986
|
+
async close() {
|
|
987
|
+
if (this.pool) {
|
|
988
|
+
await this.pool.end();
|
|
989
|
+
this.pool = null;
|
|
990
|
+
this.initialized = false;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Convert database row to MemoryEntry
|
|
995
|
+
*/
|
|
996
|
+
rowToEntry(row) {
|
|
997
|
+
const embedding = row.embedding;
|
|
998
|
+
let embeddingArray;
|
|
999
|
+
if (embedding) {
|
|
1000
|
+
const cleaned = embedding.replace(/[[\]]/g, "");
|
|
1001
|
+
embeddingArray = cleaned.split(",").map((v) => parseFloat(v));
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
id: row.id,
|
|
1005
|
+
content: row.content,
|
|
1006
|
+
embedding: embeddingArray,
|
|
1007
|
+
type: row.type,
|
|
1008
|
+
importance: parseFloat(row.importance),
|
|
1009
|
+
metadata: row.metadata,
|
|
1010
|
+
timestamp: parseInt(row.timestamp, 10),
|
|
1011
|
+
expiresAt: row.expires_at ? parseInt(row.expires_at, 10) : void 0,
|
|
1012
|
+
parentId: row.parent_id,
|
|
1013
|
+
accessCount: parseInt(row.access_count, 10),
|
|
1014
|
+
lastAccessedAt: row.last_accessed_at ? parseInt(row.last_accessed_at, 10) : void 0,
|
|
1015
|
+
createdAt: parseInt(row.created_at, 10),
|
|
1016
|
+
updatedAt: parseInt(row.updated_at, 10)
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
async function createPostgresStore(config) {
|
|
1021
|
+
const store = new PostgresStore(config);
|
|
1022
|
+
await store.initialize();
|
|
1023
|
+
return store;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/stores/implementations/RedisStore.ts
|
|
1027
|
+
var RedisStore = class {
|
|
1028
|
+
redis = null;
|
|
1029
|
+
config;
|
|
1030
|
+
keyPrefix;
|
|
1031
|
+
ttl;
|
|
1032
|
+
initialized = false;
|
|
1033
|
+
constructor(config) {
|
|
1034
|
+
this.config = config;
|
|
1035
|
+
this.keyPrefix = config.keyPrefix ?? "memory:";
|
|
1036
|
+
this.ttl = config.ttl;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Initialize Redis connection
|
|
1040
|
+
*/
|
|
1041
|
+
async initialize() {
|
|
1042
|
+
if (this.initialized) return;
|
|
1043
|
+
const Redis = (await import("ioredis")).default;
|
|
1044
|
+
if (this.config.url) {
|
|
1045
|
+
this.redis = new Redis(this.config.url);
|
|
1046
|
+
} else {
|
|
1047
|
+
this.redis = new Redis({
|
|
1048
|
+
host: this.config.host ?? "localhost",
|
|
1049
|
+
port: this.config.port ?? 6379,
|
|
1050
|
+
password: this.config.password,
|
|
1051
|
+
db: this.config.db ?? 0
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
this.initialized = true;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Ensure Redis is initialized
|
|
1058
|
+
*/
|
|
1059
|
+
async ensureInitialized() {
|
|
1060
|
+
if (!this.initialized) {
|
|
1061
|
+
await this.initialize();
|
|
1062
|
+
}
|
|
1063
|
+
return this.redis;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Get key for memory entry
|
|
1067
|
+
*/
|
|
1068
|
+
getKey(id) {
|
|
1069
|
+
return `${this.keyPrefix}${id}`;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Get index key
|
|
1073
|
+
*/
|
|
1074
|
+
getIndexKey(index) {
|
|
1075
|
+
return `${this.keyPrefix}index:${index}`;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Add a memory entry
|
|
1079
|
+
*/
|
|
1080
|
+
async add(entry) {
|
|
1081
|
+
const redis = await this.ensureInitialized();
|
|
1082
|
+
const key = this.getKey(entry.id);
|
|
1083
|
+
const data = JSON.stringify(entry);
|
|
1084
|
+
if (this.ttl || entry.expiresAt) {
|
|
1085
|
+
const ttlMs = entry.expiresAt ? entry.expiresAt - Date.now() : this.ttl * 1e3;
|
|
1086
|
+
if (ttlMs > 0) {
|
|
1087
|
+
await redis.setex(key, Math.ceil(ttlMs / 1e3), data);
|
|
1088
|
+
} else {
|
|
1089
|
+
await redis.set(key, data);
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
await redis.set(key, data);
|
|
1093
|
+
}
|
|
1094
|
+
await this.addToIndexes(entry);
|
|
1095
|
+
return entry.id;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Add entry to indexes
|
|
1099
|
+
*/
|
|
1100
|
+
async addToIndexes(entry) {
|
|
1101
|
+
const redis = await this.ensureInitialized();
|
|
1102
|
+
const score = entry.timestamp;
|
|
1103
|
+
await redis.zadd(this.getIndexKey("all"), score, entry.id);
|
|
1104
|
+
await redis.zadd(this.getIndexKey(`type:${entry.type}`), score, entry.id);
|
|
1105
|
+
const namespace = entry.metadata.namespace ?? "default";
|
|
1106
|
+
await redis.zadd(
|
|
1107
|
+
this.getIndexKey(`namespace:${namespace}`),
|
|
1108
|
+
score,
|
|
1109
|
+
entry.id
|
|
1110
|
+
);
|
|
1111
|
+
if (entry.metadata.userId) {
|
|
1112
|
+
await redis.zadd(
|
|
1113
|
+
this.getIndexKey(`user:${entry.metadata.userId}`),
|
|
1114
|
+
score,
|
|
1115
|
+
entry.id
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
if (entry.metadata.conversationId) {
|
|
1119
|
+
await redis.zadd(
|
|
1120
|
+
this.getIndexKey(`conversation:${entry.metadata.conversationId}`),
|
|
1121
|
+
score,
|
|
1122
|
+
entry.id
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Remove entry from indexes
|
|
1128
|
+
*/
|
|
1129
|
+
async removeFromIndexes(entry) {
|
|
1130
|
+
const redis = await this.ensureInitialized();
|
|
1131
|
+
await redis.zrem(this.getIndexKey("all"), entry.id);
|
|
1132
|
+
await redis.zrem(this.getIndexKey(`type:${entry.type}`), entry.id);
|
|
1133
|
+
const namespace = entry.metadata.namespace ?? "default";
|
|
1134
|
+
await redis.zrem(this.getIndexKey(`namespace:${namespace}`), entry.id);
|
|
1135
|
+
if (entry.metadata.userId) {
|
|
1136
|
+
await redis.zrem(
|
|
1137
|
+
this.getIndexKey(`user:${entry.metadata.userId}`),
|
|
1138
|
+
entry.id
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (entry.metadata.conversationId) {
|
|
1142
|
+
await redis.zrem(
|
|
1143
|
+
this.getIndexKey(`conversation:${entry.metadata.conversationId}`),
|
|
1144
|
+
entry.id
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Get a memory entry by ID
|
|
1150
|
+
*/
|
|
1151
|
+
async get(id) {
|
|
1152
|
+
const redis = await this.ensureInitialized();
|
|
1153
|
+
const key = this.getKey(id);
|
|
1154
|
+
const data = await redis.get(key);
|
|
1155
|
+
if (!data) {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
const entry = JSON.parse(data);
|
|
1159
|
+
entry.accessCount++;
|
|
1160
|
+
entry.lastAccessedAt = Date.now();
|
|
1161
|
+
await redis.set(key, JSON.stringify(entry));
|
|
1162
|
+
return entry;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Update a memory entry
|
|
1166
|
+
*/
|
|
1167
|
+
async update(id, updates) {
|
|
1168
|
+
const redis = await this.ensureInitialized();
|
|
1169
|
+
const existing = await this.get(id);
|
|
1170
|
+
if (!existing) {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
await this.removeFromIndexes(existing);
|
|
1174
|
+
const updated = {
|
|
1175
|
+
...existing,
|
|
1176
|
+
...updates,
|
|
1177
|
+
metadata: {
|
|
1178
|
+
...existing.metadata,
|
|
1179
|
+
...updates.metadata
|
|
1180
|
+
},
|
|
1181
|
+
updatedAt: Date.now()
|
|
1182
|
+
};
|
|
1183
|
+
const key = this.getKey(id);
|
|
1184
|
+
await redis.set(key, JSON.stringify(updated));
|
|
1185
|
+
await this.addToIndexes(updated);
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Delete a memory entry
|
|
1190
|
+
*/
|
|
1191
|
+
async delete(id) {
|
|
1192
|
+
const redis = await this.ensureInitialized();
|
|
1193
|
+
const existing = await this.get(id);
|
|
1194
|
+
if (!existing) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
await this.removeFromIndexes(existing);
|
|
1198
|
+
const result = await redis.del(this.getKey(id));
|
|
1199
|
+
return result > 0;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Query memory entries
|
|
1203
|
+
*/
|
|
1204
|
+
async query(options) {
|
|
1205
|
+
const redis = await this.ensureInitialized();
|
|
1206
|
+
let indexKey = this.getIndexKey("all");
|
|
1207
|
+
if (options.conversationId) {
|
|
1208
|
+
indexKey = this.getIndexKey(`conversation:${options.conversationId}`);
|
|
1209
|
+
} else if (options.userId) {
|
|
1210
|
+
indexKey = this.getIndexKey(`user:${options.userId}`);
|
|
1211
|
+
} else if (options.namespace) {
|
|
1212
|
+
indexKey = this.getIndexKey(`namespace:${options.namespace}`);
|
|
1213
|
+
} else if (options.types && options.types.length === 1) {
|
|
1214
|
+
indexKey = this.getIndexKey(`type:${options.types[0]}`);
|
|
1215
|
+
}
|
|
1216
|
+
const minScore = options.startTime ?? "-inf";
|
|
1217
|
+
const maxScore = options.endTime ?? "+inf";
|
|
1218
|
+
const ids = await redis.zrevrangebyscore(
|
|
1219
|
+
indexKey,
|
|
1220
|
+
maxScore,
|
|
1221
|
+
minScore,
|
|
1222
|
+
"LIMIT",
|
|
1223
|
+
0,
|
|
1224
|
+
1e3
|
|
1225
|
+
// Get more than needed for filtering
|
|
1226
|
+
);
|
|
1227
|
+
const entries = [];
|
|
1228
|
+
const pipeline = redis.pipeline();
|
|
1229
|
+
for (const id of ids) {
|
|
1230
|
+
pipeline.get(this.getKey(id));
|
|
1231
|
+
}
|
|
1232
|
+
const results = await pipeline.exec();
|
|
1233
|
+
if (results) {
|
|
1234
|
+
for (const [err, data] of results) {
|
|
1235
|
+
if (!err && data) {
|
|
1236
|
+
const entry = JSON.parse(data);
|
|
1237
|
+
if (this.matchesQuery(entry, options)) {
|
|
1238
|
+
entries.push(entry);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
const limit = options.limit ?? 100;
|
|
1244
|
+
const offset = options.offset ?? 0;
|
|
1245
|
+
const paginated = entries.slice(offset, offset + limit);
|
|
1246
|
+
return {
|
|
1247
|
+
entries: paginated,
|
|
1248
|
+
total: entries.length,
|
|
1249
|
+
hasMore: offset + limit < entries.length
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Check if entry matches query
|
|
1254
|
+
*/
|
|
1255
|
+
matchesQuery(entry, options) {
|
|
1256
|
+
if (options.query) {
|
|
1257
|
+
if (!entry.content.toLowerCase().includes(options.query.toLowerCase())) {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (options.userId && entry.metadata.userId !== options.userId) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
if (options.agentId && entry.metadata.agentId !== options.agentId) {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
if (options.conversationId && entry.metadata.conversationId !== options.conversationId) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
if (options.namespace && entry.metadata.namespace !== options.namespace) {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
if (options.types && options.types.length > 0 && !options.types.includes(entry.type)) {
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
if (options.minImportance !== void 0 && entry.importance < options.minImportance) {
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
if (options.tags && options.tags.length > 0) {
|
|
1280
|
+
const entryTags = entry.metadata.tags ?? [];
|
|
1281
|
+
if (!options.tags.every((tag) => entryTags.includes(tag))) {
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (!options.includeExpired && entry.expiresAt && entry.expiresAt < Date.now()) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Search by vector similarity
|
|
1292
|
+
*/
|
|
1293
|
+
async search(embedding, options) {
|
|
1294
|
+
const { entries } = await this.query({
|
|
1295
|
+
limit: 1e4,
|
|
1296
|
+
namespace: options.namespace
|
|
1297
|
+
});
|
|
1298
|
+
const results = [];
|
|
1299
|
+
for (const entry of entries) {
|
|
1300
|
+
if (!entry.embedding) continue;
|
|
1301
|
+
if (options.filter) {
|
|
1302
|
+
let matches = true;
|
|
1303
|
+
for (const [key, value] of Object.entries(options.filter)) {
|
|
1304
|
+
if (value !== void 0) {
|
|
1305
|
+
const metaValue = entry.metadata[key];
|
|
1306
|
+
if (Array.isArray(value)) {
|
|
1307
|
+
if (!value.includes(metaValue)) {
|
|
1308
|
+
matches = false;
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
} else if (metaValue !== value) {
|
|
1312
|
+
matches = false;
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (!matches) continue;
|
|
1318
|
+
}
|
|
1319
|
+
const score = this.cosineSimilarity(embedding, entry.embedding);
|
|
1320
|
+
if (options.minScore === void 0 || score >= options.minScore) {
|
|
1321
|
+
results.push({ entry, score });
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
results.sort((a, b) => b.score - a.score);
|
|
1325
|
+
return Promise.resolve(results.slice(0, options.topK));
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Calculate cosine similarity
|
|
1329
|
+
*/
|
|
1330
|
+
cosineSimilarity(a, b) {
|
|
1331
|
+
if (a.length !== b.length) return 0;
|
|
1332
|
+
let dotProduct = 0;
|
|
1333
|
+
let normA = 0;
|
|
1334
|
+
let normB = 0;
|
|
1335
|
+
for (let i = 0; i < a.length; i++) {
|
|
1336
|
+
dotProduct += a[i] * b[i];
|
|
1337
|
+
normA += a[i] * a[i];
|
|
1338
|
+
normB += b[i] * b[i];
|
|
1339
|
+
}
|
|
1340
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1341
|
+
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Clear entries
|
|
1345
|
+
*/
|
|
1346
|
+
async clear(options) {
|
|
1347
|
+
const redis = await this.ensureInitialized();
|
|
1348
|
+
if (!options) {
|
|
1349
|
+
const keys = await redis.keys(`${this.keyPrefix}*`);
|
|
1350
|
+
if (keys.length > 0) {
|
|
1351
|
+
await redis.del(...keys);
|
|
1352
|
+
}
|
|
1353
|
+
return keys.length;
|
|
1354
|
+
}
|
|
1355
|
+
const { entries } = await this.query({
|
|
1356
|
+
namespace: options.namespace,
|
|
1357
|
+
userId: options.userId,
|
|
1358
|
+
limit: 1e5
|
|
1359
|
+
});
|
|
1360
|
+
for (const entry of entries) {
|
|
1361
|
+
await this.delete(entry.id);
|
|
1362
|
+
}
|
|
1363
|
+
return entries.length;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Count entries
|
|
1367
|
+
*/
|
|
1368
|
+
async count(options) {
|
|
1369
|
+
const redis = await this.ensureInitialized();
|
|
1370
|
+
if (!options) {
|
|
1371
|
+
return Promise.resolve(redis.zcard(this.getIndexKey("all")));
|
|
1372
|
+
}
|
|
1373
|
+
const { total } = await this.query({ ...options, limit: 0 });
|
|
1374
|
+
return total;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Close Redis connection
|
|
1378
|
+
*/
|
|
1379
|
+
async close() {
|
|
1380
|
+
if (this.redis) {
|
|
1381
|
+
await this.redis.quit();
|
|
1382
|
+
this.redis = null;
|
|
1383
|
+
this.initialized = false;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
async function createRedisStore(config) {
|
|
1388
|
+
const store = new RedisStore(config);
|
|
1389
|
+
await store.initialize();
|
|
1390
|
+
return store;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
export {
|
|
1394
|
+
InMemoryStore,
|
|
1395
|
+
createInMemoryStore,
|
|
1396
|
+
SQLiteStore,
|
|
1397
|
+
createSQLiteStore,
|
|
1398
|
+
PostgresStore,
|
|
1399
|
+
createPostgresStore,
|
|
1400
|
+
RedisStore,
|
|
1401
|
+
createRedisStore
|
|
1402
|
+
};
|