@aitytech/agentkits-memory 1.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +54 -5
  2. package/dist/better-sqlite3-backend.d.ts +192 -0
  3. package/dist/better-sqlite3-backend.d.ts.map +1 -0
  4. package/dist/better-sqlite3-backend.js +801 -0
  5. package/dist/better-sqlite3-backend.js.map +1 -0
  6. package/dist/cli/save.js +0 -0
  7. package/dist/cli/setup.d.ts +6 -2
  8. package/dist/cli/setup.d.ts.map +1 -1
  9. package/dist/cli/setup.js +289 -42
  10. package/dist/cli/setup.js.map +1 -1
  11. package/dist/cli/viewer.js +25 -56
  12. package/dist/cli/viewer.js.map +1 -1
  13. package/dist/cli/web-viewer.d.ts +2 -1
  14. package/dist/cli/web-viewer.d.ts.map +1 -1
  15. package/dist/cli/web-viewer.js +791 -141
  16. package/dist/cli/web-viewer.js.map +1 -1
  17. package/dist/embeddings/embedding-cache.d.ts +131 -0
  18. package/dist/embeddings/embedding-cache.d.ts.map +1 -0
  19. package/dist/embeddings/embedding-cache.js +217 -0
  20. package/dist/embeddings/embedding-cache.js.map +1 -0
  21. package/dist/embeddings/index.d.ts +11 -0
  22. package/dist/embeddings/index.d.ts.map +1 -0
  23. package/dist/embeddings/index.js +11 -0
  24. package/dist/embeddings/index.js.map +1 -0
  25. package/dist/embeddings/local-embeddings.d.ts +140 -0
  26. package/dist/embeddings/local-embeddings.d.ts.map +1 -0
  27. package/dist/embeddings/local-embeddings.js +293 -0
  28. package/dist/embeddings/local-embeddings.js.map +1 -0
  29. package/dist/hooks/context.d.ts +6 -1
  30. package/dist/hooks/context.d.ts.map +1 -1
  31. package/dist/hooks/context.js +12 -2
  32. package/dist/hooks/context.js.map +1 -1
  33. package/dist/hooks/observation.d.ts +6 -1
  34. package/dist/hooks/observation.d.ts.map +1 -1
  35. package/dist/hooks/observation.js +12 -2
  36. package/dist/hooks/observation.js.map +1 -1
  37. package/dist/hooks/service.d.ts +1 -6
  38. package/dist/hooks/service.d.ts.map +1 -1
  39. package/dist/hooks/service.js +33 -85
  40. package/dist/hooks/service.js.map +1 -1
  41. package/dist/hooks/session-init.d.ts +6 -1
  42. package/dist/hooks/session-init.d.ts.map +1 -1
  43. package/dist/hooks/session-init.js +12 -2
  44. package/dist/hooks/session-init.js.map +1 -1
  45. package/dist/hooks/summarize.d.ts +6 -1
  46. package/dist/hooks/summarize.d.ts.map +1 -1
  47. package/dist/hooks/summarize.js +12 -2
  48. package/dist/hooks/summarize.js.map +1 -1
  49. package/dist/index.d.ts +10 -17
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +172 -94
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/server.js +17 -3
  54. package/dist/mcp/server.js.map +1 -1
  55. package/dist/migration.js +3 -3
  56. package/dist/migration.js.map +1 -1
  57. package/dist/search/hybrid-search.d.ts +262 -0
  58. package/dist/search/hybrid-search.d.ts.map +1 -0
  59. package/dist/search/hybrid-search.js +688 -0
  60. package/dist/search/hybrid-search.js.map +1 -0
  61. package/dist/search/index.d.ts +13 -0
  62. package/dist/search/index.d.ts.map +1 -0
  63. package/dist/search/index.js +13 -0
  64. package/dist/search/index.js.map +1 -0
  65. package/dist/search/token-economics.d.ts +161 -0
  66. package/dist/search/token-economics.d.ts.map +1 -0
  67. package/dist/search/token-economics.js +239 -0
  68. package/dist/search/token-economics.js.map +1 -0
  69. package/dist/types.d.ts +0 -68
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js.map +1 -1
  72. package/package.json +6 -4
  73. package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
  74. package/src/__tests__/cache-manager.test.ts +499 -0
  75. package/src/__tests__/embedding-integration.test.ts +481 -0
  76. package/src/__tests__/hnsw-index.test.ts +727 -0
  77. package/src/__tests__/index.test.ts +432 -0
  78. package/src/better-sqlite3-backend.ts +1000 -0
  79. package/src/cli/setup.ts +358 -47
  80. package/src/cli/viewer.ts +28 -63
  81. package/src/cli/web-viewer.ts +936 -182
  82. package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
  83. package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
  84. package/src/embeddings/embedding-cache.ts +318 -0
  85. package/src/embeddings/index.ts +20 -0
  86. package/src/embeddings/local-embeddings.ts +419 -0
  87. package/src/hooks/__tests__/handlers.test.ts +58 -17
  88. package/src/hooks/__tests__/integration.test.ts +77 -26
  89. package/src/hooks/context.ts +13 -2
  90. package/src/hooks/observation.ts +13 -2
  91. package/src/hooks/service.ts +39 -100
  92. package/src/hooks/session-init.ts +13 -2
  93. package/src/hooks/summarize.ts +13 -2
  94. package/src/index.ts +210 -116
  95. package/src/mcp/server.ts +20 -3
  96. package/src/search/__tests__/hybrid-search.test.ts +669 -0
  97. package/src/search/__tests__/token-economics.test.ts +276 -0
  98. package/src/search/hybrid-search.ts +968 -0
  99. package/src/search/index.ts +29 -0
  100. package/src/search/token-economics.ts +367 -0
  101. package/src/types.ts +0 -96
  102. package/src/__tests__/sqljs-backend.test.ts +0 -410
  103. package/src/migration.ts +0 -574
  104. package/src/sql.js.d.ts +0 -70
  105. package/src/sqljs-backend.ts +0 -789
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Embedding Integration Tests
3
+ *
4
+ * Tests for embedding support across ProjectMemoryService,
5
+ * HybridSearchEngine, and MCP Server integration.
6
+ *
7
+ * @module @aitytech/agentkits-memory/__tests__/embedding-integration.test
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import * as os from 'node:os';
14
+ import Database from 'better-sqlite3';
15
+ import {
16
+ ProjectMemoryService,
17
+ LocalEmbeddingsService,
18
+ HybridSearchEngine,
19
+ type MemoryEntry,
20
+ } from '../index.js';
21
+
22
+ describe('Embedding Integration', () => {
23
+ let tempDir: string;
24
+
25
+ beforeEach(() => {
26
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'embed-integration-'));
27
+ });
28
+
29
+ afterEach(() => {
30
+ fs.rmSync(tempDir, { recursive: true, force: true });
31
+ });
32
+
33
+ describe('ProjectMemoryService with LocalEmbeddingsService', () => {
34
+ it('should generate embeddings when storing entries', async () => {
35
+ const embeddingsService = new LocalEmbeddingsService({
36
+ cacheDir: path.join(tempDir, 'cache'),
37
+ });
38
+ await embeddingsService.initialize();
39
+
40
+ const embeddingGenerator = async (text: string): Promise<Float32Array> => {
41
+ const result = await embeddingsService.embed(text);
42
+ return result.embedding;
43
+ };
44
+
45
+ const service = new ProjectMemoryService({
46
+ baseDir: tempDir,
47
+ dbFilename: 'test.db',
48
+ embeddingGenerator,
49
+ });
50
+ await service.initialize();
51
+
52
+ const entry = await service.storeEntry({
53
+ key: 'test-entry',
54
+ content: 'This is a test content for embedding generation',
55
+ namespace: 'test',
56
+ });
57
+
58
+ expect(entry.embedding).toBeDefined();
59
+ expect(entry.embedding).toBeInstanceOf(Float32Array);
60
+ expect(entry.embedding!.length).toBe(384); // Default dimension
61
+
62
+ await service.shutdown();
63
+ });
64
+
65
+ it('should perform semantic search with embeddings', async () => {
66
+ const embeddingsService = new LocalEmbeddingsService({
67
+ cacheDir: path.join(tempDir, 'cache'),
68
+ });
69
+ await embeddingsService.initialize();
70
+
71
+ const embeddingGenerator = async (text: string): Promise<Float32Array> => {
72
+ const result = await embeddingsService.embed(text);
73
+ return result.embedding;
74
+ };
75
+
76
+ const service = new ProjectMemoryService({
77
+ baseDir: tempDir,
78
+ dbFilename: 'test.db',
79
+ embeddingGenerator,
80
+ });
81
+ await service.initialize();
82
+
83
+ // Store entries with different content
84
+ await service.storeEntry({
85
+ key: 'auth-pattern',
86
+ content: 'Authentication using JWT tokens with refresh mechanism',
87
+ namespace: 'patterns',
88
+ });
89
+
90
+ await service.storeEntry({
91
+ key: 'db-pattern',
92
+ content: 'Database connection pooling for PostgreSQL',
93
+ namespace: 'patterns',
94
+ });
95
+
96
+ await service.storeEntry({
97
+ key: 'error-handling',
98
+ content: 'Global error handler for API exceptions',
99
+ namespace: 'errors',
100
+ });
101
+
102
+ // Semantic search should find related content
103
+ const results = await service.semanticSearch('JWT authentication', 5);
104
+
105
+ expect(results.length).toBeGreaterThan(0);
106
+ expect(results[0].entry.key).toBe('auth-pattern');
107
+ expect(results[0].score).toBeGreaterThan(0);
108
+
109
+ await service.shutdown();
110
+ });
111
+
112
+ it('should update embeddings when content changes', async () => {
113
+ const embeddingsService = new LocalEmbeddingsService({
114
+ cacheDir: path.join(tempDir, 'cache'),
115
+ });
116
+ await embeddingsService.initialize();
117
+
118
+ const embeddingGenerator = async (text: string): Promise<Float32Array> => {
119
+ const result = await embeddingsService.embed(text);
120
+ return result.embedding;
121
+ };
122
+
123
+ const service = new ProjectMemoryService({
124
+ baseDir: tempDir,
125
+ dbFilename: 'test.db',
126
+ embeddingGenerator,
127
+ });
128
+ await service.initialize();
129
+
130
+ const entry = await service.storeEntry({
131
+ key: 'updateable',
132
+ content: 'Original content about cats',
133
+ namespace: 'test',
134
+ });
135
+
136
+ const originalEmbedding = entry.embedding!.slice();
137
+
138
+ // Update content
139
+ const updated = await service.update(entry.id, {
140
+ content: 'Updated content about dogs',
141
+ });
142
+
143
+ expect(updated).not.toBeNull();
144
+ expect(updated!.embedding).toBeDefined();
145
+
146
+ // Embedding should be different after content change
147
+ const isDifferent = !originalEmbedding.every(
148
+ (val, i) => val === updated!.embedding![i]
149
+ );
150
+ expect(isDifferent).toBe(true);
151
+
152
+ await service.shutdown();
153
+ });
154
+ });
155
+
156
+ describe('HybridSearchEngine with embeddings', () => {
157
+ it('should perform semantic search and return scored results', async () => {
158
+ const dbPath = path.join(tempDir, 'hybrid.db');
159
+ const db = new Database(dbPath);
160
+ db.pragma('journal_mode = WAL');
161
+
162
+ // Create table with rowid for FTS sync
163
+ db.exec(`
164
+ CREATE TABLE memory_entries (
165
+ id TEXT PRIMARY KEY,
166
+ key TEXT NOT NULL,
167
+ content TEXT NOT NULL,
168
+ type TEXT DEFAULT 'semantic',
169
+ namespace TEXT DEFAULT 'general',
170
+ tags TEXT DEFAULT '[]',
171
+ embedding BLOB,
172
+ created_at INTEGER NOT NULL,
173
+ updated_at INTEGER NOT NULL
174
+ )
175
+ `);
176
+
177
+ const embeddingsService = new LocalEmbeddingsService({
178
+ cacheDir: path.join(tempDir, 'cache'),
179
+ });
180
+ await embeddingsService.initialize();
181
+
182
+ const embeddingGenerator = async (text: string): Promise<Float32Array> => {
183
+ const result = await embeddingsService.embed(text);
184
+ return result.embedding;
185
+ };
186
+
187
+ const engine = new HybridSearchEngine(db, {}, embeddingGenerator);
188
+ await engine.initialize();
189
+
190
+ // Insert test data with embeddings
191
+ const now = Date.now();
192
+ const entries = [
193
+ { id: '1', key: 'react-hooks', content: 'React hooks for state management with useState and useEffect' },
194
+ { id: '2', key: 'vue-composition', content: 'Vue 3 composition API for reactive state' },
195
+ { id: '3', key: 'angular-services', content: 'Angular dependency injection and services' },
196
+ ];
197
+
198
+ for (const entry of entries) {
199
+ const embedding = await embeddingGenerator(entry.content);
200
+ const embeddingBuffer = Buffer.from(embedding.buffer);
201
+
202
+ db.prepare(`
203
+ INSERT INTO memory_entries (id, key, content, embedding, created_at, updated_at)
204
+ VALUES (?, ?, ?, ?, ?, ?)
205
+ `).run(entry.id, entry.key, entry.content, embeddingBuffer, now, now);
206
+ }
207
+
208
+ await engine.rebuildFtsIndex();
209
+
210
+ // Search using semantic only (more reliable in tests)
211
+ const results = await engine.searchCompact('React state hooks', {
212
+ limit: 10,
213
+ includeKeyword: false,
214
+ includeSemantic: true,
215
+ });
216
+
217
+ expect(results.length).toBeGreaterThan(0);
218
+ expect(results[0].id).toBe('1'); // React entry should be first
219
+
220
+ // Semantic score should be present
221
+ expect(results[0].semanticScore).toBeGreaterThan(0);
222
+ expect(results[0].score).toBeGreaterThan(0);
223
+
224
+ db.close();
225
+ });
226
+
227
+ it('should work with text-only search when no embeddings', async () => {
228
+ const dbPath = path.join(tempDir, 'text-only.db');
229
+ const db = new Database(dbPath);
230
+ db.pragma('journal_mode = WAL');
231
+
232
+ db.exec(`
233
+ CREATE TABLE memory_entries (
234
+ id TEXT PRIMARY KEY,
235
+ key TEXT NOT NULL,
236
+ content TEXT NOT NULL,
237
+ type TEXT DEFAULT 'semantic',
238
+ namespace TEXT DEFAULT 'general',
239
+ tags TEXT DEFAULT '[]',
240
+ embedding BLOB,
241
+ created_at INTEGER NOT NULL,
242
+ updated_at INTEGER NOT NULL
243
+ )
244
+ `);
245
+
246
+ // Engine without embedding generator
247
+ const engine = new HybridSearchEngine(db);
248
+ await engine.initialize();
249
+
250
+ const now = Date.now();
251
+ db.prepare(`
252
+ INSERT INTO memory_entries (id, key, content, created_at, updated_at)
253
+ VALUES (?, ?, ?, ?, ?)
254
+ `).run('1', 'test', 'Test content with keywords', now, now);
255
+
256
+ await engine.rebuildFtsIndex();
257
+
258
+ const results = await engine.searchCompact('keywords', {
259
+ limit: 10,
260
+ includeKeyword: true,
261
+ includeSemantic: true, // Should not fail even without embedding generator
262
+ });
263
+
264
+ expect(results.length).toBeGreaterThan(0);
265
+ expect(results[0].keywordScore).toBeGreaterThan(0);
266
+ expect(results[0].semanticScore).toBe(0); // No semantic without embeddings
267
+
268
+ db.close();
269
+ });
270
+
271
+ it('should support vector-only search mode', async () => {
272
+ const dbPath = path.join(tempDir, 'vector-only.db');
273
+ const db = new Database(dbPath);
274
+ db.pragma('journal_mode = WAL');
275
+
276
+ db.exec(`
277
+ CREATE TABLE memory_entries (
278
+ id TEXT PRIMARY KEY,
279
+ key TEXT NOT NULL,
280
+ content TEXT NOT NULL,
281
+ type TEXT DEFAULT 'semantic',
282
+ namespace TEXT DEFAULT 'general',
283
+ tags TEXT DEFAULT '[]',
284
+ embedding BLOB,
285
+ created_at INTEGER NOT NULL,
286
+ updated_at INTEGER NOT NULL
287
+ )
288
+ `);
289
+
290
+ const embeddingsService = new LocalEmbeddingsService({
291
+ cacheDir: path.join(tempDir, 'cache'),
292
+ });
293
+ await embeddingsService.initialize();
294
+
295
+ const embeddingGenerator = async (text: string): Promise<Float32Array> => {
296
+ const result = await embeddingsService.embed(text);
297
+ return result.embedding;
298
+ };
299
+
300
+ const engine = new HybridSearchEngine(db, {}, embeddingGenerator);
301
+ await engine.initialize();
302
+
303
+ const now = Date.now();
304
+ const embedding = await embeddingGenerator('Machine learning algorithms');
305
+ const embeddingBuffer = Buffer.from(embedding.buffer);
306
+
307
+ db.prepare(`
308
+ INSERT INTO memory_entries (id, key, content, embedding, created_at, updated_at)
309
+ VALUES (?, ?, ?, ?, ?, ?)
310
+ `).run('1', 'ml', 'Machine learning algorithms', embeddingBuffer, now, now);
311
+
312
+ // Vector-only search (no keyword)
313
+ const results = await engine.searchCompact('AI neural networks', {
314
+ limit: 10,
315
+ includeKeyword: false,
316
+ includeSemantic: true,
317
+ });
318
+
319
+ expect(results.length).toBeGreaterThan(0);
320
+ expect(results[0].keywordScore).toBe(0); // Keyword disabled
321
+ expect(results[0].semanticScore).toBeGreaterThan(0);
322
+
323
+ db.close();
324
+ });
325
+ });
326
+
327
+ describe('CJK language support with embeddings', () => {
328
+ it('should generate embeddings for Japanese text', async () => {
329
+ const embeddingsService = new LocalEmbeddingsService({
330
+ cacheDir: path.join(tempDir, 'cache'),
331
+ });
332
+ await embeddingsService.initialize();
333
+
334
+ const result = await embeddingsService.embed('これは日本語のテストです');
335
+
336
+ expect(result.embedding).toBeDefined();
337
+ expect(result.embedding.length).toBe(384);
338
+ });
339
+
340
+ it('should generate embeddings for Chinese text', async () => {
341
+ const embeddingsService = new LocalEmbeddingsService({
342
+ cacheDir: path.join(tempDir, 'cache'),
343
+ });
344
+ await embeddingsService.initialize();
345
+
346
+ const result = await embeddingsService.embed('这是中文测试');
347
+
348
+ expect(result.embedding).toBeDefined();
349
+ expect(result.embedding.length).toBe(384);
350
+ });
351
+
352
+ it('should find semantically similar CJK content', async () => {
353
+ const embeddingsService = new LocalEmbeddingsService({
354
+ cacheDir: path.join(tempDir, 'cache'),
355
+ });
356
+ await embeddingsService.initialize();
357
+
358
+ const embeddingGenerator = async (text: string): Promise<Float32Array> => {
359
+ const result = await embeddingsService.embed(text);
360
+ return result.embedding;
361
+ };
362
+
363
+ const service = new ProjectMemoryService({
364
+ baseDir: tempDir,
365
+ dbFilename: 'cjk.db',
366
+ embeddingGenerator,
367
+ });
368
+ await service.initialize();
369
+
370
+ await service.storeEntry({
371
+ key: 'japanese-greeting',
372
+ content: 'おはようございます。今日はいい天気ですね。',
373
+ namespace: 'test',
374
+ });
375
+
376
+ await service.storeEntry({
377
+ key: 'japanese-farewell',
378
+ content: 'さようなら。また会いましょう。',
379
+ namespace: 'test',
380
+ });
381
+
382
+ // Search for morning greeting
383
+ const results = await service.semanticSearch('朝の挨拶', 5);
384
+
385
+ expect(results.length).toBeGreaterThan(0);
386
+
387
+ await service.shutdown();
388
+ });
389
+ });
390
+
391
+ describe('Embedding caching', () => {
392
+ it('should cache embeddings and return faster on second call', async () => {
393
+ const embeddingsService = new LocalEmbeddingsService({
394
+ cacheDir: path.join(tempDir, 'cache'),
395
+ });
396
+ await embeddingsService.initialize();
397
+
398
+ const text = 'This is a test sentence for caching';
399
+
400
+ // First call - should compute
401
+ const result1 = await embeddingsService.embed(text);
402
+ expect(result1.cached).toBe(false);
403
+
404
+ // Second call - should be cached
405
+ const result2 = await embeddingsService.embed(text);
406
+ expect(result2.cached).toBe(true);
407
+
408
+ // Embeddings should be identical
409
+ expect(result1.embedding.length).toBe(result2.embedding.length);
410
+ const areSame = result1.embedding.every(
411
+ (val, i) => val === result2.embedding[i]
412
+ );
413
+ expect(areSame).toBe(true);
414
+ });
415
+
416
+ it('should use in-memory cache within same service instance', async () => {
417
+ const embeddingsService = new LocalEmbeddingsService({
418
+ cacheDir: path.join(tempDir, 'cache'),
419
+ maxCacheSize: 100,
420
+ });
421
+ await embeddingsService.initialize();
422
+
423
+ const text = 'Cache consistency test';
424
+
425
+ // First call
426
+ const result1 = await embeddingsService.embed(text);
427
+ expect(result1.cached).toBe(false);
428
+
429
+ // Second call with same text - should hit cache
430
+ const result2 = await embeddingsService.embed(text);
431
+ expect(result2.cached).toBe(true);
432
+
433
+ // Third call with different text - cache miss
434
+ const result3 = await embeddingsService.embed('Different text');
435
+ expect(result3.cached).toBe(false);
436
+
437
+ // Same different text again - cache hit
438
+ const result4 = await embeddingsService.embed('Different text');
439
+ expect(result4.cached).toBe(true);
440
+ });
441
+ });
442
+
443
+ describe('Error handling', () => {
444
+ it('should handle embedding generation failure gracefully', async () => {
445
+ const failingGenerator = async (): Promise<Float32Array> => {
446
+ throw new Error('Embedding service unavailable');
447
+ };
448
+
449
+ const service = new ProjectMemoryService({
450
+ baseDir: tempDir,
451
+ dbFilename: 'fail.db',
452
+ embeddingGenerator: failingGenerator,
453
+ });
454
+ await service.initialize();
455
+
456
+ // Should still store entry even if embedding fails
457
+ const entry = await service.storeEntry({
458
+ key: 'no-embedding',
459
+ content: 'Content without embedding due to failure',
460
+ namespace: 'test',
461
+ });
462
+
463
+ expect(entry.id).toBeDefined();
464
+ expect(entry.embedding).toBeUndefined();
465
+
466
+ await service.shutdown();
467
+ });
468
+
469
+ it('should handle empty content gracefully', async () => {
470
+ const embeddingsService = new LocalEmbeddingsService({
471
+ cacheDir: path.join(tempDir, 'cache'),
472
+ });
473
+ await embeddingsService.initialize();
474
+
475
+ // Empty string should still work
476
+ const result = await embeddingsService.embed('');
477
+ expect(result.embedding).toBeDefined();
478
+ expect(result.embedding.length).toBe(384);
479
+ });
480
+ });
481
+ });