aimemory-core 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +22 -0
- package/.github/workflows/ci.yml +57 -0
- package/.prettierrc +8 -0
- package/README.md +197 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +41 -0
- package/dist/config.js.map +1 -0
- package/dist/contextBuilder.d.ts +16 -0
- package/dist/contextBuilder.d.ts.map +1 -0
- package/dist/contextBuilder.js +139 -0
- package/dist/contextBuilder.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/licensing.d.ts +45 -0
- package/dist/licensing.d.ts.map +1 -0
- package/dist/licensing.js +99 -0
- package/dist/licensing.js.map +1 -0
- package/dist/memoryManager.d.ts +35 -0
- package/dist/memoryManager.d.ts.map +1 -0
- package/dist/memoryManager.js +265 -0
- package/dist/memoryManager.js.map +1 -0
- package/dist/metadataStore.d.ts +24 -0
- package/dist/metadataStore.d.ts.map +1 -0
- package/dist/metadataStore.js +247 -0
- package/dist/metadataStore.js.map +1 -0
- package/dist/planManager.d.ts +80 -0
- package/dist/planManager.d.ts.map +1 -0
- package/dist/planManager.js +327 -0
- package/dist/planManager.js.map +1 -0
- package/dist/rateLimiter.d.ts +49 -0
- package/dist/rateLimiter.d.ts.map +1 -0
- package/dist/rateLimiter.js +142 -0
- package/dist/rateLimiter.js.map +1 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +3 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/postgres.d.ts +31 -0
- package/dist/storage/postgres.d.ts.map +1 -0
- package/dist/storage/postgres.js +171 -0
- package/dist/storage/postgres.js.map +1 -0
- package/dist/storage/redis.d.ts +34 -0
- package/dist/storage/redis.d.ts.map +1 -0
- package/dist/storage/redis.js +101 -0
- package/dist/storage/redis.js.map +1 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/usageTracker.d.ts +63 -0
- package/dist/usageTracker.d.ts.map +1 -0
- package/dist/usageTracker.js +238 -0
- package/dist/usageTracker.js.map +1 -0
- package/dist/vectorStore.d.ts +18 -0
- package/dist/vectorStore.d.ts.map +1 -0
- package/dist/vectorStore.js +97 -0
- package/dist/vectorStore.js.map +1 -0
- package/examples/advanced.ts +164 -0
- package/examples/basic.ts +87 -0
- package/package.json +60 -0
- package/src/config.ts +65 -0
- package/src/contextBuilder.ts +184 -0
- package/src/index.ts +209 -0
- package/src/licensing.ts +138 -0
- package/src/memoryManager.ts +340 -0
- package/src/metadataStore.ts +298 -0
- package/src/planManager.ts +417 -0
- package/src/rateLimiter.ts +186 -0
- package/src/storage/index.ts +2 -0
- package/src/storage/postgres.ts +209 -0
- package/src/storage/redis.ts +117 -0
- package/src/types.ts +114 -0
- package/src/usageTracker.ts +325 -0
- package/src/vectorStore.ts +116 -0
- package/tests/aibrain.test.ts +171 -0
- package/tests/contextBuilder.test.ts +138 -0
- package/tests/memoryManager.test.ts +205 -0
- package/tests/metadataStore.test.ts +131 -0
- package/tests/rateLimiter.test.ts +57 -0
- package/tests/usageTracker.test.ts +62 -0
- package/tests/vectorStore.test.ts +106 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryManager } from '../src/memoryManager.js';
|
|
3
|
+
import { ContextBuilder } from '../src/contextBuilder.js';
|
|
4
|
+
import { EmbeddingFunction } from '../src/types.js';
|
|
5
|
+
|
|
6
|
+
const mockEmbedding: EmbeddingFunction = async (text: string): Promise<number[]> => {
|
|
7
|
+
const hash = text.split('').reduce((acc, char) => {
|
|
8
|
+
return ((acc << 5) - acc) + char.charCodeAt(0);
|
|
9
|
+
}, 0);
|
|
10
|
+
|
|
11
|
+
const embedding = new Array(1536).fill(0);
|
|
12
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
13
|
+
embedding[i] = Math.sin(hash * i) * Math.cos(hash * i);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
17
|
+
return embedding.map(val => val / magnitude);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('ContextBuilder', () => {
|
|
21
|
+
let manager: MemoryManager;
|
|
22
|
+
let builder: ContextBuilder;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
manager = new MemoryManager();
|
|
26
|
+
manager.setEmbeddingFunction(mockEmbedding);
|
|
27
|
+
|
|
28
|
+
await manager.addFact('The user prefers dark mode');
|
|
29
|
+
await manager.addFact('User lives in New York');
|
|
30
|
+
await manager.addPreference('User likes bullet point summaries');
|
|
31
|
+
await manager.addPreference('User prefers concise responses');
|
|
32
|
+
await manager.addInstruction('Always verify information');
|
|
33
|
+
await manager.addConversation('Hello, how are you?', 'user', { sessionId: 'test' });
|
|
34
|
+
await manager.addConversation('I am doing great!', 'assistant', { sessionId: 'test' });
|
|
35
|
+
|
|
36
|
+
builder = new ContextBuilder(manager, {
|
|
37
|
+
maxTokens: 2000,
|
|
38
|
+
relevanceThreshold: 0.1,
|
|
39
|
+
maxMemories: 10,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should build context with system prompt', async () => {
|
|
44
|
+
const result = await builder.buildContext('user preferences');
|
|
45
|
+
|
|
46
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
47
|
+
expect(result.messages[0].role).toBe('system');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should exclude system prompt when configured', async () => {
|
|
51
|
+
builder.updateConfig({ includeSystemPrompt: false });
|
|
52
|
+
|
|
53
|
+
const result = await builder.buildContext('test query');
|
|
54
|
+
|
|
55
|
+
const hasSystemPrompt = result.messages.some(m => m.role === 'system');
|
|
56
|
+
expect(hasSystemPrompt).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should include relevant memories in context', async () => {
|
|
60
|
+
const result = await builder.buildContext('test');
|
|
61
|
+
|
|
62
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should estimate tokens correctly', async () => {
|
|
66
|
+
const result = await builder.buildContext('test');
|
|
67
|
+
|
|
68
|
+
expect(result.stats.tokensEstimated).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should respect maxMemories config', async () => {
|
|
72
|
+
builder.updateConfig({ maxMemories: 2 });
|
|
73
|
+
|
|
74
|
+
const result = await builder.buildContext('test query');
|
|
75
|
+
|
|
76
|
+
expect(result.stats.memoriesUsed).toBeLessThanOrEqual(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should respect relevance threshold', async () => {
|
|
80
|
+
builder.updateConfig({ relevanceThreshold: 0.9 });
|
|
81
|
+
|
|
82
|
+
const result = await builder.buildContext('test');
|
|
83
|
+
|
|
84
|
+
expect(result.stats.memoriesUsed).toBeGreaterThanOrEqual(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should include metadata when configured', async () => {
|
|
88
|
+
builder.updateConfig({ includeMetadata: true });
|
|
89
|
+
|
|
90
|
+
const result = await builder.buildContext('test');
|
|
91
|
+
|
|
92
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should group memories by type in context', async () => {
|
|
96
|
+
const result = await builder.buildContext('test');
|
|
97
|
+
|
|
98
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle empty results', async () => {
|
|
102
|
+
const emptyManager = new MemoryManager();
|
|
103
|
+
emptyManager.setEmbeddingFunction(mockEmbedding);
|
|
104
|
+
const emptyBuilder = new ContextBuilder(emptyManager);
|
|
105
|
+
|
|
106
|
+
const result = await emptyBuilder.buildContext('nonexistent query');
|
|
107
|
+
|
|
108
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should return stats object', async () => {
|
|
112
|
+
const result = await builder.buildContext('test');
|
|
113
|
+
|
|
114
|
+
expect(result.stats).toHaveProperty('memoriesUsed');
|
|
115
|
+
expect(result.stats).toHaveProperty('tokensEstimated');
|
|
116
|
+
expect(result.stats).toHaveProperty('truncationApplied');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should track truncation', async () => {
|
|
120
|
+
builder.updateConfig({ maxTokens: 10 });
|
|
121
|
+
|
|
122
|
+
const result = await builder.buildContext('test');
|
|
123
|
+
|
|
124
|
+
expect(typeof result.stats.truncationApplied).toBe('boolean');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should allow custom system prompt', async () => {
|
|
128
|
+
builder.updateConfig({
|
|
129
|
+
includeSystemPrompt: true,
|
|
130
|
+
systemPrompt: 'Custom system prompt',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await builder.buildContext('test');
|
|
134
|
+
|
|
135
|
+
const systemMsg = result.messages.find(m => m.role === 'system');
|
|
136
|
+
expect(systemMsg?.content).toContain('Custom system prompt');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryManager } from '../src/memoryManager.js';
|
|
3
|
+
import { EmbeddingFunction } from '../src/types.js';
|
|
4
|
+
|
|
5
|
+
const mockEmbedding: EmbeddingFunction = async (text: string): Promise<number[]> => {
|
|
6
|
+
const hash = text.split('').reduce((acc, char) => {
|
|
7
|
+
return ((acc << 5) - acc) + char.charCodeAt(0);
|
|
8
|
+
}, 0);
|
|
9
|
+
|
|
10
|
+
const embedding = new Array(1536).fill(0);
|
|
11
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
12
|
+
embedding[i] = Math.sin(hash * i) * Math.cos(hash * i);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
16
|
+
return embedding.map(val => val / magnitude);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('MemoryManager', () => {
|
|
20
|
+
let manager: MemoryManager;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
manager = new MemoryManager();
|
|
24
|
+
manager.setEmbeddingFunction(mockEmbedding);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should add memory', async () => {
|
|
28
|
+
const memory = await manager.addMemory('Test content', {
|
|
29
|
+
type: 'fact',
|
|
30
|
+
importance: 0.8,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(memory.content).toBe('Test content');
|
|
34
|
+
expect(memory.metadata.type).toBe('fact');
|
|
35
|
+
expect(memory.metadata.importance).toBe(0.8);
|
|
36
|
+
expect(memory.id).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should add conversation', async () => {
|
|
40
|
+
const memory = await manager.addConversation('Hello', 'user', {
|
|
41
|
+
sessionId: 'session-1',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(memory.content).toBe('User: Hello');
|
|
45
|
+
expect(memory.metadata.type).toBe('conversation');
|
|
46
|
+
expect(memory.metadata.sessionId).toBe('session-1');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should add fact with high importance', async () => {
|
|
50
|
+
const memory = await manager.addFact('The sky is blue');
|
|
51
|
+
|
|
52
|
+
expect(memory.metadata.type).toBe('fact');
|
|
53
|
+
expect(memory.metadata.importance).toBeGreaterThanOrEqual(0.7);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should add preference with default importance', async () => {
|
|
57
|
+
const memory = await manager.addPreference('I prefer dark mode');
|
|
58
|
+
|
|
59
|
+
expect(memory.metadata.type).toBe('preference');
|
|
60
|
+
expect(memory.metadata.importance).toBeGreaterThanOrEqual(0.6);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should add instruction with high importance', async () => {
|
|
64
|
+
const memory = await manager.addInstruction('Always verify facts');
|
|
65
|
+
|
|
66
|
+
expect(memory.metadata.type).toBe('instruction');
|
|
67
|
+
expect(memory.metadata.importance).toBeGreaterThanOrEqual(0.9);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should search memories', async () => {
|
|
71
|
+
await manager.addMemory('Python is a programming language', { type: 'fact' });
|
|
72
|
+
await manager.addMemory('JavaScript is also a programming language', { type: 'fact' });
|
|
73
|
+
await manager.addMemory('The weather is nice today', { type: 'context' });
|
|
74
|
+
|
|
75
|
+
const results = await manager.search({
|
|
76
|
+
text: 'programming language',
|
|
77
|
+
limit: 5,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(results.length).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should get memory by id', async () => {
|
|
84
|
+
const added = await manager.addMemory('Test content');
|
|
85
|
+
|
|
86
|
+
const retrieved = await manager.getMemory(added.id);
|
|
87
|
+
|
|
88
|
+
expect(retrieved).not.toBeNull();
|
|
89
|
+
expect(retrieved!.content).toBe('Test content');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return null for non-existent id', async () => {
|
|
93
|
+
const retrieved = await manager.getMemory('non-existent-id');
|
|
94
|
+
expect(retrieved).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should update memory', async () => {
|
|
98
|
+
const added = await manager.addMemory('Original content');
|
|
99
|
+
|
|
100
|
+
const updated = await manager.updateMemory(added.id, {
|
|
101
|
+
content: 'Updated content',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(updated).not.toBeNull();
|
|
105
|
+
expect(updated!.content).toBe('Updated content');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should delete memory', async () => {
|
|
109
|
+
const added = await manager.addMemory('To be deleted');
|
|
110
|
+
|
|
111
|
+
await manager.deleteMemory(added.id);
|
|
112
|
+
|
|
113
|
+
const retrieved = await manager.getMemory(added.id);
|
|
114
|
+
expect(retrieved).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should get recent memories', async () => {
|
|
118
|
+
await manager.addMemory('First');
|
|
119
|
+
await manager.addMemory('Second');
|
|
120
|
+
await manager.addMemory('Third');
|
|
121
|
+
|
|
122
|
+
const recent = await manager.getRecentMemories(2);
|
|
123
|
+
|
|
124
|
+
expect(recent).toHaveLength(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should get memories by type', async () => {
|
|
128
|
+
await manager.addMemory('A fact', { type: 'fact' });
|
|
129
|
+
await manager.addMemory('Another fact', { type: 'fact' });
|
|
130
|
+
await manager.addMemory('A preference', { type: 'preference' });
|
|
131
|
+
|
|
132
|
+
const facts = await manager.getMemoriesByType('fact');
|
|
133
|
+
|
|
134
|
+
expect(facts).toHaveLength(2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should get memories by tag', async () => {
|
|
138
|
+
await manager.addMemory('Memory 1', { tags: ['important', 'work'] });
|
|
139
|
+
await manager.addMemory('Memory 2', { tags: ['important'] });
|
|
140
|
+
await manager.addMemory('Memory 3', { tags: ['personal'] });
|
|
141
|
+
|
|
142
|
+
const important = await manager.getMemoriesByTag('important');
|
|
143
|
+
|
|
144
|
+
expect(important).toHaveLength(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should get memories by user', async () => {
|
|
148
|
+
await manager.addMemory('User A memory', { userId: 'user-a' });
|
|
149
|
+
await manager.addMemory('User A another', { userId: 'user-a' });
|
|
150
|
+
await manager.addMemory('User B memory', { userId: 'user-b' });
|
|
151
|
+
|
|
152
|
+
const userAMemories = await manager.getMemoriesByUser('user-a');
|
|
153
|
+
|
|
154
|
+
expect(userAMemories).toHaveLength(2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should get stats', async () => {
|
|
158
|
+
await manager.addMemory('Fact 1', { type: 'fact' });
|
|
159
|
+
await manager.addMemory('Fact 2', { type: 'fact' });
|
|
160
|
+
await manager.addMemory('Preference 1', { type: 'preference' });
|
|
161
|
+
|
|
162
|
+
const stats = await manager.getStats();
|
|
163
|
+
|
|
164
|
+
expect(stats.totalMemories).toBe(3);
|
|
165
|
+
expect(stats.memoriesByType.fact).toBe(2);
|
|
166
|
+
expect(stats.memoriesByType.preference).toBe(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should export and import data', async () => {
|
|
170
|
+
await manager.addMemory('Memory 1', { type: 'fact' });
|
|
171
|
+
await manager.addMemory('Memory 2', { type: 'preference' });
|
|
172
|
+
|
|
173
|
+
const exported = await manager.exportData();
|
|
174
|
+
|
|
175
|
+
const newManager = new MemoryManager();
|
|
176
|
+
await newManager.importData(exported);
|
|
177
|
+
|
|
178
|
+
const stats = await newManager.getStats();
|
|
179
|
+
expect(stats.totalMemories).toBe(2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should clear all memories', async () => {
|
|
183
|
+
await manager.addMemory('Memory 1');
|
|
184
|
+
await manager.addMemory('Memory 2');
|
|
185
|
+
|
|
186
|
+
await manager.clear();
|
|
187
|
+
|
|
188
|
+
const stats = await manager.getStats();
|
|
189
|
+
expect(stats.totalMemories).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should respect maxMemories config', async () => {
|
|
193
|
+
const limitedManager = new MemoryManager({ maxMemories: 3 });
|
|
194
|
+
limitedManager.setEmbeddingFunction(mockEmbedding);
|
|
195
|
+
|
|
196
|
+
await limitedManager.addMemory('Memory 1');
|
|
197
|
+
await limitedManager.addMemory('Memory 2');
|
|
198
|
+
await limitedManager.addMemory('Memory 3');
|
|
199
|
+
await limitedManager.addMemory('Memory 4');
|
|
200
|
+
await limitedManager.addMemory('Memory 5');
|
|
201
|
+
|
|
202
|
+
const stats = await limitedManager.getStats();
|
|
203
|
+
expect(stats.totalMemories).toBeLessThanOrEqual(5);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MetadataStore } from '../src/metadataStore.js';
|
|
3
|
+
import { Memory, MemoryType } from '../src/types.js';
|
|
4
|
+
|
|
5
|
+
describe('MetadataStore', () => {
|
|
6
|
+
let store: MetadataStore;
|
|
7
|
+
|
|
8
|
+
const createMemory = (overrides: Partial<Memory> = {}): Memory => ({
|
|
9
|
+
id: `mem-${Math.random().toString(36).substr(2, 9)}`,
|
|
10
|
+
content: 'Test memory content',
|
|
11
|
+
timestamp: Date.now(),
|
|
12
|
+
metadata: {
|
|
13
|
+
type: 'context' as MemoryType,
|
|
14
|
+
importance: 0.5,
|
|
15
|
+
tags: [],
|
|
16
|
+
...overrides.metadata,
|
|
17
|
+
},
|
|
18
|
+
...overrides,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
store = new MetadataStore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should add memories', async () => {
|
|
26
|
+
const memory = createMemory();
|
|
27
|
+
await store.add(memory);
|
|
28
|
+
|
|
29
|
+
const retrieved = await store.getById(memory.id);
|
|
30
|
+
expect(retrieved).not.toBeNull();
|
|
31
|
+
expect(retrieved!.content).toBe(memory.content);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should update existing memories', async () => {
|
|
35
|
+
const memory = createMemory();
|
|
36
|
+
await store.add(memory);
|
|
37
|
+
|
|
38
|
+
await store.update({ ...memory, content: 'Updated content' });
|
|
39
|
+
|
|
40
|
+
const retrieved = await store.getById(memory.id);
|
|
41
|
+
expect(retrieved!.content).toBe('Updated content');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should delete memories', async () => {
|
|
45
|
+
const memory = createMemory();
|
|
46
|
+
await store.add(memory);
|
|
47
|
+
|
|
48
|
+
await store.delete(memory.id);
|
|
49
|
+
|
|
50
|
+
const retrieved = await store.getById(memory.id);
|
|
51
|
+
expect(retrieved).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should index by type', async () => {
|
|
55
|
+
await store.add(createMemory({ metadata: { type: 'fact' } }));
|
|
56
|
+
await store.add(createMemory({ metadata: { type: 'fact' } }));
|
|
57
|
+
await store.add(createMemory({ metadata: { type: 'preference' } }));
|
|
58
|
+
|
|
59
|
+
const facts = await store.find({ types: ['fact'] }, 10);
|
|
60
|
+
expect(facts).toHaveLength(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should index by tags', async () => {
|
|
64
|
+
await store.add(createMemory({ metadata: { tags: ['tag1', 'tag2'] } }));
|
|
65
|
+
await store.add(createMemory({ metadata: { tags: ['tag1'] } }));
|
|
66
|
+
await store.add(createMemory({ metadata: { tags: ['tag3'] } }));
|
|
67
|
+
|
|
68
|
+
const results = await store.find({ tags: ['tag1'] }, 10);
|
|
69
|
+
expect(results).toHaveLength(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should filter by userId', async () => {
|
|
73
|
+
await store.add(createMemory({ metadata: { userId: 'user-1' } }));
|
|
74
|
+
await store.add(createMemory({ metadata: { userId: 'user-1' } }));
|
|
75
|
+
await store.add(createMemory({ metadata: { userId: 'user-2' } }));
|
|
76
|
+
|
|
77
|
+
const results = await store.find({ userId: 'user-1' }, 10);
|
|
78
|
+
expect(results).toHaveLength(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should filter by importance range', async () => {
|
|
82
|
+
await store.add(createMemory({ metadata: { importance: 0.3 } }));
|
|
83
|
+
await store.add(createMemory({ metadata: { importance: 0.7 } }));
|
|
84
|
+
await store.add(createMemory({ metadata: { importance: 0.5 } }));
|
|
85
|
+
|
|
86
|
+
const results = await store.find({ minImportance: 0.4, maxImportance: 0.8 }, 10);
|
|
87
|
+
expect(results).toHaveLength(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should get recent memories', async () => {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
await store.add(createMemory({ timestamp: now - 1000 }));
|
|
93
|
+
await store.add(createMemory({ timestamp: now }));
|
|
94
|
+
await store.add(createMemory({ timestamp: now + 1000 }));
|
|
95
|
+
|
|
96
|
+
const recent = await store.getRecent(2);
|
|
97
|
+
expect(recent).toHaveLength(2);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should get statistics', async () => {
|
|
101
|
+
await store.add(createMemory({ metadata: { type: 'fact' } }));
|
|
102
|
+
await store.add(createMemory({ metadata: { type: 'fact' } }));
|
|
103
|
+
await store.add(createMemory({ metadata: { type: 'preference', tags: ['test'] } }));
|
|
104
|
+
|
|
105
|
+
const stats = await store.getStats();
|
|
106
|
+
|
|
107
|
+
expect(stats.totalMemories).toBe(3);
|
|
108
|
+
expect(stats.memoriesByType.fact).toBe(2);
|
|
109
|
+
expect(stats.memoriesByType.preference).toBe(1);
|
|
110
|
+
expect(stats.memoriesByTag['test']).toBe(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should clear all memories', async () => {
|
|
114
|
+
await store.add(createMemory());
|
|
115
|
+
await store.add(createMemory());
|
|
116
|
+
|
|
117
|
+
await store.clear();
|
|
118
|
+
|
|
119
|
+
const size = await store.size();
|
|
120
|
+
expect(size).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return correct size', async () => {
|
|
124
|
+
await store.add(createMemory());
|
|
125
|
+
await store.add(createMemory());
|
|
126
|
+
await store.add(createMemory());
|
|
127
|
+
|
|
128
|
+
const size = await store.size();
|
|
129
|
+
expect(size).toBe(3);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { RateLimiter, InMemoryRateLimitStore } from '../src/rateLimiter';
|
|
3
|
+
|
|
4
|
+
describe('RateLimiter', () => {
|
|
5
|
+
let limiter: RateLimiter;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
const store = new InMemoryRateLimitStore();
|
|
9
|
+
limiter = new RateLimiter({ windowMs: 60000, maxRequests: 5 }, store);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should allow requests within limit', async () => {
|
|
13
|
+
const result = await limiter.check('user1');
|
|
14
|
+
expect(result.allowed).toBe(true);
|
|
15
|
+
expect(result.remaining).toBe(4);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should track remaining requests', async () => {
|
|
19
|
+
await limiter.check('user1');
|
|
20
|
+
await limiter.check('user1');
|
|
21
|
+
|
|
22
|
+
const result = await limiter.check('user1');
|
|
23
|
+
expect(result.remaining).toBe(2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should block when limit reached', async () => {
|
|
27
|
+
for (let i = 0; i < 5; i++) {
|
|
28
|
+
await limiter.check('user1');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = await limiter.check('user1');
|
|
32
|
+
expect(result.allowed).toBe(false);
|
|
33
|
+
expect(result.remaining).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should track different users separately', async () => {
|
|
37
|
+
await limiter.check('user1');
|
|
38
|
+
await limiter.check('user1');
|
|
39
|
+
|
|
40
|
+
const result1 = await limiter.check('user1');
|
|
41
|
+
const result2 = await limiter.check('user2');
|
|
42
|
+
|
|
43
|
+
expect(result1.remaining).toBe(2);
|
|
44
|
+
expect(result2.remaining).toBe(4);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should reset user limit', async () => {
|
|
48
|
+
for (let i = 0; i < 5; i++) {
|
|
49
|
+
await limiter.check('user1');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await limiter.reset('user1');
|
|
53
|
+
|
|
54
|
+
const result = await limiter.check('user1');
|
|
55
|
+
expect(result.allowed).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { UsageTracker, InMemoryUsageStore, DEFAULT_USAGE_LIMITS } from '../src/usageTracker';
|
|
3
|
+
|
|
4
|
+
describe('UsageTracker', () => {
|
|
5
|
+
let tracker: UsageTracker;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
const store = new InMemoryUsageStore();
|
|
9
|
+
tracker = new UsageTracker(DEFAULT_USAGE_LIMITS.free, store);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should track memories added', async () => {
|
|
13
|
+
await tracker.recordUsage('user1', 'memories_added');
|
|
14
|
+
|
|
15
|
+
const result = await tracker.checkLimit('user1', 'memories_added');
|
|
16
|
+
expect(result.current).toBe(1);
|
|
17
|
+
expect(result.remaining).toBe(999);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should track api calls', async () => {
|
|
21
|
+
await tracker.recordUsage('user1', 'api_calls');
|
|
22
|
+
await tracker.recordUsage('user1', 'api_calls');
|
|
23
|
+
await tracker.recordUsage('user1', 'api_calls');
|
|
24
|
+
|
|
25
|
+
const result = await tracker.checkLimit('user1', 'api_calls');
|
|
26
|
+
expect(result.current).toBe(3);
|
|
27
|
+
expect(result.remaining).toBe(997);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should block when limit reached', async () => {
|
|
31
|
+
const limits = { ...DEFAULT_USAGE_LIMITS.free, maxApiCallsPerDay: 2 };
|
|
32
|
+
const limitedTracker = new UsageTracker(limits);
|
|
33
|
+
|
|
34
|
+
await limitedTracker.recordUsage('user1', 'api_calls');
|
|
35
|
+
await limitedTracker.recordUsage('user1', 'api_calls');
|
|
36
|
+
|
|
37
|
+
const result = await limitedTracker.checkLimit('user1', 'api_calls');
|
|
38
|
+
expect(result.allowed).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should get usage summary', async () => {
|
|
42
|
+
await tracker.recordUsage('user1', 'memories_added');
|
|
43
|
+
await tracker.recordUsage('user1', 'api_calls');
|
|
44
|
+
await tracker.recordUsage('user1', 'api_calls');
|
|
45
|
+
|
|
46
|
+
const summary = await tracker.getUsageSummary('user1', 'daily');
|
|
47
|
+
expect(summary.totalOperations).toBe(3);
|
|
48
|
+
expect(summary.metrics.memories_added).toBe(1);
|
|
49
|
+
expect(summary.metrics.api_calls).toBe(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should track different users separately', async () => {
|
|
53
|
+
await tracker.recordUsage('user1', 'api_calls');
|
|
54
|
+
await tracker.recordUsage('user2', 'api_calls');
|
|
55
|
+
|
|
56
|
+
const result1 = await tracker.checkLimit('user1', 'api_calls');
|
|
57
|
+
const result2 = await tracker.checkLimit('user2', 'api_calls');
|
|
58
|
+
|
|
59
|
+
expect(result1.current).toBe(1);
|
|
60
|
+
expect(result2.current).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { InMemoryVectorStore } from '../src/vectorStore.js';
|
|
3
|
+
|
|
4
|
+
describe('InMemoryVectorStore', () => {
|
|
5
|
+
let store: InMemoryVectorStore;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
store = new InMemoryVectorStore(3);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should add vectors', async () => {
|
|
12
|
+
await store.add([
|
|
13
|
+
{ id: '1', vector: [1, 0, 0], metadata: { text: 'test1' } },
|
|
14
|
+
{ id: '2', vector: [0, 1, 0], metadata: { text: 'test2' } },
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const size = await store.size();
|
|
18
|
+
expect(size).toBe(2);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should throw on invalid dimension', async () => {
|
|
22
|
+
await expect(
|
|
23
|
+
store.add([{ id: '1', vector: [1, 2], metadata: {} }])
|
|
24
|
+
).rejects.toThrow('Invalid vector dimension');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should search by cosine similarity', async () => {
|
|
28
|
+
await store.add([
|
|
29
|
+
{ id: '1', vector: [1, 0, 0], metadata: { text: 'similar' } },
|
|
30
|
+
{ id: '2', vector: [0, 1, 0], metadata: { text: 'different' } },
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const results = await store.search([1, 0, 0], { limit: 2 });
|
|
34
|
+
|
|
35
|
+
expect(results).toHaveLength(2);
|
|
36
|
+
expect(results[0].memory.metadata.text).toBe('similar');
|
|
37
|
+
expect(results[0].score).toBeGreaterThan(results[1].score);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should filter by threshold', async () => {
|
|
41
|
+
await store.add([
|
|
42
|
+
{ id: '1', vector: [1, 0, 0], metadata: { text: 'high' } },
|
|
43
|
+
{ id: '2', vector: [0.1, 0.1, 0.1], metadata: { text: 'low' } },
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const results = await store.search([1, 0, 0], { limit: 2, threshold: 0.9 });
|
|
47
|
+
|
|
48
|
+
expect(results).toHaveLength(1);
|
|
49
|
+
expect(results[0].memory.metadata.text).toBe('high');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should delete vectors', async () => {
|
|
53
|
+
await store.add([
|
|
54
|
+
{ id: '1', vector: [1, 0, 0], metadata: {} },
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
await store.delete(['1']);
|
|
58
|
+
|
|
59
|
+
const size = await store.size();
|
|
60
|
+
expect(size).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should get by id', async () => {
|
|
64
|
+
await store.add([
|
|
65
|
+
{ id: 'test-id', vector: [1, 2, 3], metadata: { data: 'value' } },
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const entry = await store.getById('test-id');
|
|
69
|
+
|
|
70
|
+
expect(entry).not.toBeNull();
|
|
71
|
+
expect(entry!.metadata.data).toBe('value');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return null for non-existent id', async () => {
|
|
75
|
+
const entry = await store.getById('non-existent');
|
|
76
|
+
expect(entry).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should clear all vectors', async () => {
|
|
80
|
+
await store.add([
|
|
81
|
+
{ id: '1', vector: [1, 0, 0], metadata: {} },
|
|
82
|
+
{ id: '2', vector: [0, 1, 0], metadata: {} },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
await store.clear();
|
|
86
|
+
|
|
87
|
+
const size = await store.size();
|
|
88
|
+
expect(size).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should calculate cosine similarity correctly', async () => {
|
|
92
|
+
await store.add([
|
|
93
|
+
{ id: '1', vector: [1, 0, 0], metadata: { content: 'a' } },
|
|
94
|
+
{ id: '2', vector: [0, 1, 0], metadata: { content: 'b' } },
|
|
95
|
+
{ id: '3', vector: [1, 1, 0], metadata: { content: 'c' } },
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const results = await store.search([1, 0, 0], { limit: 3 });
|
|
99
|
+
|
|
100
|
+
expect(results[0].score).toBe(1);
|
|
101
|
+
const scores = results.map(r => r.score);
|
|
102
|
+
expect(scores).toContain(0);
|
|
103
|
+
const hasMiddleScore = scores.some(s => s > 0.5 && s < 1);
|
|
104
|
+
expect(hasMiddleScore).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|