@1mbrain/benchmarks 0.1.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/README.md +85 -0
- package/fixtures/1mbrain-focused-mini/1mbrain-focused-mini.json +928 -0
- package/fixtures/1mbrain-focused-mini/README.md +45 -0
- package/fixtures/adversarial-memory/dataset_claude_adversarial.json +3333 -0
- package/fixtures/adversarial-memory/dataset_gemini_adversarial_memory.json +2984 -0
- package/fixtures/balanced-mini/dataset_claude_balanced_mini.json +2077 -0
- package/fixtures/balanced-mini/dataset_gemini_balanced_mini.json +1995 -0
- package/fixtures/generate_datasets.js +1741 -0
- package/fixtures/graph-stress-hard/README.md +43 -0
- package/fixtures/graph-stress-hard/dataset_graph_stress_hard.json +4374 -0
- package/fixtures/graph-stress-hard/generate_graph_stress_hard.js +526 -0
- package/fixtures/realistic-medium/dataset_claude_realistic_medium.json +7462 -0
- package/fixtures/realistic-medium/dataset_gemini_realistic_medium.json +7277 -0
- package/fixtures/realistic-medium/gen_claude_medium.js +600 -0
- package/package.json +22 -0
- package/reports/benchmark_report.md +48 -0
- package/reports/benchmark_report_claude_adversarial.md +42 -0
- package/reports/benchmark_report_claude_adversarial_adaptive.md +42 -0
- package/reports/benchmark_report_claude_adversarial_adaptive2_fast.md +42 -0
- package/reports/benchmark_report_claude_adversarial_adaptive_fast.md +42 -0
- package/reports/benchmark_report_claude_adversarial_rerank.md +42 -0
- package/reports/benchmark_report_claude_balanced_mini.md +42 -0
- package/reports/benchmark_report_claude_balanced_mini_adaptive.md +42 -0
- package/reports/benchmark_report_claude_balanced_mini_adaptive2_fast.md +42 -0
- package/reports/benchmark_report_claude_balanced_mini_adaptive_fast.md +42 -0
- package/reports/benchmark_report_claude_balanced_mini_rerank.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium_adaptive.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium_adaptive2_fast.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium_adaptive_fast.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium_evidence_rerank_local.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_evidence_rerank.md +41 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_multi_signal.md +41 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_multi_signal_scoped.md +41 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_phase8_no_judge.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_rankingpolicy.md +41 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_stale_filter.md +41 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_stale_filter_absence_fix.md +41 -0
- package/reports/benchmark_report_claude_realistic_medium_openai_write_time_invalidation.md +41 -0
- package/reports/benchmark_report_claude_realistic_medium_rerank.md +42 -0
- package/reports/benchmark_report_claude_realistic_medium_stale_filter_local.md +42 -0
- package/reports/benchmark_report_graph_stress_hard.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_absence_fix.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_adaptive.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_evidence_rerank.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_multi_signal_current_guardrail.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_multi_signal_guardrail_fixed.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_multi_signal_local.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_multi_signal_scoped_guardrail.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_multi_signal_vector_pure_guardrail.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_phase8_sdk_guardrail.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_rerank.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_stale_filter.md +42 -0
- package/reports/benchmark_report_graph_stress_hard_write_time_invalidation.md +42 -0
- package/results/.gitignore +2 -0
- package/src/adapters/1mbrain.ts +317 -0
- package/src/adapters/keyword-embedding.ts +48 -0
- package/src/adapters/mem0.ts +124 -0
- package/src/adapters/qdrant.ts +214 -0
- package/src/adapters/unavailable.ts +49 -0
- package/src/adapters/vector-baseline.ts +149 -0
- package/src/datasets/focused-mini.ts +158 -0
- package/src/datasets/synthetic-agent-memory.ts +532 -0
- package/src/llm-evaluator.ts +262 -0
- package/src/metrics.ts +482 -0
- package/src/provider.ts +151 -0
- package/src/runner.ts +635 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
InMemoryEventBus,
|
|
7
|
+
MemoryEngine,
|
|
8
|
+
SqliteDatabaseProvider,
|
|
9
|
+
logger,
|
|
10
|
+
} from '@1mbrain/core';
|
|
11
|
+
import type { MemoryPassport } from '@1mbrain/core';
|
|
12
|
+
import type {
|
|
13
|
+
BenchmarkMemoryRecord,
|
|
14
|
+
BenchmarkRecallRequest,
|
|
15
|
+
BenchmarkRecallResult,
|
|
16
|
+
MemoryProviderAdapter,
|
|
17
|
+
ProviderAvailability,
|
|
18
|
+
ProviderStats,
|
|
19
|
+
} from '../provider.js';
|
|
20
|
+
import { KeywordEmbeddingProvider } from './keyword-embedding.js';
|
|
21
|
+
import { OpenAIEmbeddingProvider } from '@1mbrain/core';
|
|
22
|
+
|
|
23
|
+
type OneMBrainMode =
|
|
24
|
+
| {
|
|
25
|
+
name: '1mbrain_vector_only';
|
|
26
|
+
label: '1MBrain Vector Only';
|
|
27
|
+
useSpreadingActivation: false;
|
|
28
|
+
maxHops: 0;
|
|
29
|
+
activationThreshold: 1;
|
|
30
|
+
blendWeight: 0;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
name: '1mbrain_graph_light';
|
|
34
|
+
label: '1MBrain Graph Light';
|
|
35
|
+
useSpreadingActivation: true;
|
|
36
|
+
maxHops: 1;
|
|
37
|
+
activationThreshold: 0.08;
|
|
38
|
+
blendWeight: 0.25;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
name: '1mbrain_graph_full';
|
|
42
|
+
label: '1MBrain Graph Full';
|
|
43
|
+
useSpreadingActivation: true;
|
|
44
|
+
maxHops: 3;
|
|
45
|
+
activationThreshold: 0.05;
|
|
46
|
+
blendWeight: 0.45;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const MODES: Record<OneMBrainMode['name'], OneMBrainMode> = {
|
|
50
|
+
'1mbrain_vector_only': {
|
|
51
|
+
name: '1mbrain_vector_only',
|
|
52
|
+
label: '1MBrain Vector Only',
|
|
53
|
+
useSpreadingActivation: false,
|
|
54
|
+
maxHops: 0,
|
|
55
|
+
activationThreshold: 1,
|
|
56
|
+
blendWeight: 0,
|
|
57
|
+
},
|
|
58
|
+
'1mbrain_graph_light': {
|
|
59
|
+
name: '1mbrain_graph_light',
|
|
60
|
+
label: '1MBrain Graph Light',
|
|
61
|
+
useSpreadingActivation: true,
|
|
62
|
+
maxHops: 1,
|
|
63
|
+
activationThreshold: 0.08,
|
|
64
|
+
blendWeight: 0.25,
|
|
65
|
+
},
|
|
66
|
+
'1mbrain_graph_full': {
|
|
67
|
+
name: '1mbrain_graph_full',
|
|
68
|
+
label: '1MBrain Graph Full',
|
|
69
|
+
useSpreadingActivation: true,
|
|
70
|
+
maxHops: 3,
|
|
71
|
+
activationThreshold: 0.05,
|
|
72
|
+
blendWeight: 0.45,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export class OneMBrainBenchmarkAdapter implements MemoryProviderAdapter {
|
|
77
|
+
readonly name: OneMBrainMode['name'];
|
|
78
|
+
readonly label: string;
|
|
79
|
+
readonly capabilities = {
|
|
80
|
+
associations: true,
|
|
81
|
+
forget: true,
|
|
82
|
+
decay: true,
|
|
83
|
+
portability: true,
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
private readonly mode: OneMBrainMode;
|
|
87
|
+
private readonly embedder = process.env['OPENAI_API_KEY']
|
|
88
|
+
? new OpenAIEmbeddingProvider(process.env['OPENAI_API_KEY'], 'text-embedding-3-small')
|
|
89
|
+
: new KeywordEmbeddingProvider();
|
|
90
|
+
private db: SqliteDatabaseProvider | null = null;
|
|
91
|
+
private engine: MemoryEngine | null = null;
|
|
92
|
+
private dbPath: string | null = null;
|
|
93
|
+
private readonly idMap = new Map<string, string>();
|
|
94
|
+
|
|
95
|
+
constructor(modeName: OneMBrainMode['name']) {
|
|
96
|
+
this.mode = MODES[modeName];
|
|
97
|
+
this.name = this.mode.name;
|
|
98
|
+
this.label = this.mode.label;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async availability(): Promise<ProviderAvailability> {
|
|
102
|
+
return { status: 'available' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async reset(_agentId: string): Promise<void> {
|
|
106
|
+
await this.close();
|
|
107
|
+
logger.level = 'silent';
|
|
108
|
+
|
|
109
|
+
this.dbPath = join(tmpdir(), `${this.name}-${process.pid}-${Date.now()}.sqlite`);
|
|
110
|
+
this.db = new SqliteDatabaseProvider(this.dbPath);
|
|
111
|
+
await this.db.initialize();
|
|
112
|
+
this.engine = new MemoryEngine(this.db, this.embedder, new InMemoryEventBus());
|
|
113
|
+
this.idMap.clear();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async remember(memory: BenchmarkMemoryRecord, agentId: string): Promise<void> {
|
|
117
|
+
if (!this.engine) {
|
|
118
|
+
throw new Error(`${this.name} is not initialized`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stored = await this.engine.remember({
|
|
122
|
+
agentId,
|
|
123
|
+
type: memory.type,
|
|
124
|
+
content: memory.content,
|
|
125
|
+
importance: memory.importance,
|
|
126
|
+
tags: memory.tags,
|
|
127
|
+
metadata: {
|
|
128
|
+
...(memory.metadata ?? {}),
|
|
129
|
+
benchId: memory.id,
|
|
130
|
+
benchTimestamp: memory.timestamp,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.idMap.set(memory.id, stored.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async associate(sourceId: string, targetId: string, strength: number, agentId: string): Promise<void> {
|
|
138
|
+
if (!this.engine) {
|
|
139
|
+
throw new Error(`${this.name} is not initialized`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const mappedSource = this.requireMappedId(sourceId);
|
|
143
|
+
const mappedTarget = this.requireMappedId(targetId);
|
|
144
|
+
|
|
145
|
+
await this.engine.associate({
|
|
146
|
+
sourceId: mappedSource,
|
|
147
|
+
targetId: mappedTarget,
|
|
148
|
+
agentId,
|
|
149
|
+
strength,
|
|
150
|
+
origin: 'explicit',
|
|
151
|
+
relationType: 'relates_to',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async recall(
|
|
156
|
+
request: BenchmarkRecallRequest & {
|
|
157
|
+
agentId: string;
|
|
158
|
+
},
|
|
159
|
+
): Promise<BenchmarkRecallResult[]> {
|
|
160
|
+
if (!this.engine) {
|
|
161
|
+
throw new Error(`${this.name} is not initialized`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const results = await this.engine.recall({
|
|
165
|
+
agentId: request.agentId,
|
|
166
|
+
query: request.query ?? '',
|
|
167
|
+
limit: request.limit ?? 5,
|
|
168
|
+
threshold: request.minScore ?? 0.08,
|
|
169
|
+
useSpreadingActivation: this.mode.useSpreadingActivation,
|
|
170
|
+
maxHops: request.maxHops ?? this.mode.maxHops,
|
|
171
|
+
activationThreshold: request.activationThreshold ?? this.mode.activationThreshold,
|
|
172
|
+
blendWeight: request.blendWeight ?? this.mode.blendWeight,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return results.map((result) => ({
|
|
176
|
+
memoryId: String(result.memory.metadata?.['benchId'] ?? result.memory.id),
|
|
177
|
+
content: result.memory.content,
|
|
178
|
+
score: result.score,
|
|
179
|
+
type: result.memory.type as BenchmarkMemoryRecord['type'],
|
|
180
|
+
source: result.source,
|
|
181
|
+
rankingTrace: result.rankingTrace,
|
|
182
|
+
metadata: result.memory.metadata,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async forget(memoryId: string, agentId: string): Promise<void> {
|
|
187
|
+
if (!this.engine) {
|
|
188
|
+
throw new Error(`${this.name} is not initialized`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await this.engine.forget(this.requireMappedId(memoryId), agentId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async applyDecay(decayRate: number, minScore: number): Promise<number> {
|
|
195
|
+
if (!this.db) {
|
|
196
|
+
throw new Error(`${this.name} is not initialized`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const affectedMemories = await this.db.applyDecay(decayRate, minScore);
|
|
200
|
+
await this.db.applyAssociationDecay(decayRate, minScore);
|
|
201
|
+
return affectedMemories;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async exportMemory(agentId: string): Promise<unknown> {
|
|
205
|
+
if (!this.engine) {
|
|
206
|
+
throw new Error(`${this.name} is not initialized`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return this.engine.exportPassport(agentId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async importMemory(payload: unknown, agentId: string): Promise<void> {
|
|
213
|
+
if (!this.db) {
|
|
214
|
+
throw new Error(`${this.name} is not initialized`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const passport = payload as MemoryPassport;
|
|
218
|
+
const embeddings = await this.embedder.embedBatch(passport.memories.map((memory) => memory.content));
|
|
219
|
+
const oldToNewIds = new Map<string, string>();
|
|
220
|
+
|
|
221
|
+
for (let index = 0; index < passport.memories.length; index++) {
|
|
222
|
+
const memory = passport.memories[index];
|
|
223
|
+
const newId = randomUUID();
|
|
224
|
+
await this.db.createMemory({
|
|
225
|
+
id: newId,
|
|
226
|
+
agentId,
|
|
227
|
+
type: memory.type,
|
|
228
|
+
content: memory.content,
|
|
229
|
+
embeddingModel: this.embedder.model,
|
|
230
|
+
embedding: embeddings[index],
|
|
231
|
+
importance: memory.importance,
|
|
232
|
+
decayScore: memory.decayScore,
|
|
233
|
+
tags: memory.tags,
|
|
234
|
+
metadata: memory.metadata,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
oldToNewIds.set(memory.id, newId);
|
|
238
|
+
const benchId = String(memory.metadata?.['benchId'] ?? newId);
|
|
239
|
+
this.idMap.set(benchId, newId);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const association of passport.associations) {
|
|
243
|
+
const mappedSource = oldToNewIds.get(association.sourceId);
|
|
244
|
+
const mappedTarget = oldToNewIds.get(association.targetId);
|
|
245
|
+
if (!mappedSource || !mappedTarget) continue;
|
|
246
|
+
|
|
247
|
+
await this.db.createAssociation({
|
|
248
|
+
sourceId: mappedSource,
|
|
249
|
+
targetId: mappedTarget,
|
|
250
|
+
strength: association.strength,
|
|
251
|
+
origin: association.origin,
|
|
252
|
+
relationType: association.relationType ?? 'relates_to',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async getStats(): Promise<ProviderStats> {
|
|
258
|
+
return {
|
|
259
|
+
storageSizeBytes: await sqliteFootprint(this.dbPath),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async close(): Promise<void> {
|
|
264
|
+
if (this.engine) {
|
|
265
|
+
await this.engine.shutdown();
|
|
266
|
+
this.engine = null;
|
|
267
|
+
this.db = null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (this.dbPath) {
|
|
271
|
+
await removeSqliteArtifacts(this.dbPath);
|
|
272
|
+
this.dbPath = null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.idMap.clear();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private requireMappedId(memoryId: string): string {
|
|
279
|
+
const mapped = this.idMap.get(memoryId);
|
|
280
|
+
if (!mapped) {
|
|
281
|
+
throw new Error(`Unknown benchmark memory id ${memoryId}`);
|
|
282
|
+
}
|
|
283
|
+
return mapped;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function createOneMBrainAdapters(): MemoryProviderAdapter[] {
|
|
288
|
+
return [
|
|
289
|
+
new OneMBrainBenchmarkAdapter('1mbrain_vector_only'),
|
|
290
|
+
new OneMBrainBenchmarkAdapter('1mbrain_graph_light'),
|
|
291
|
+
new OneMBrainBenchmarkAdapter('1mbrain_graph_full'),
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function sqliteFootprint(dbPath: string | null): Promise<number | null> {
|
|
296
|
+
if (!dbPath) return null;
|
|
297
|
+
|
|
298
|
+
const candidates = [dbPath, `${dbPath}-wal`, `${dbPath}-shm`];
|
|
299
|
+
let total = 0;
|
|
300
|
+
|
|
301
|
+
for (const candidate of candidates) {
|
|
302
|
+
try {
|
|
303
|
+
total += (await stat(candidate)).size;
|
|
304
|
+
} catch {
|
|
305
|
+
// Ignore files that do not exist.
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return total;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function removeSqliteArtifacts(dbPath: string): Promise<void> {
|
|
313
|
+
const candidates = [dbPath, `${dbPath}-wal`, `${dbPath}-shm`];
|
|
314
|
+
for (const candidate of candidates) {
|
|
315
|
+
await rm(candidate, { force: true }).catch(() => undefined);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EmbeddingProvider } from '@1mbrain/core';
|
|
2
|
+
|
|
3
|
+
const DIMENSIONS = 256;
|
|
4
|
+
|
|
5
|
+
export class KeywordEmbeddingProvider implements EmbeddingProvider {
|
|
6
|
+
readonly name = 'keyword-benchmark';
|
|
7
|
+
readonly model = 'hashed-token-benchmark-v2';
|
|
8
|
+
readonly dimensions = DIMENSIONS;
|
|
9
|
+
|
|
10
|
+
async embed(text: string): Promise<number[]> {
|
|
11
|
+
const vector = new Array<number>(this.dimensions).fill(0);
|
|
12
|
+
const tokens = tokenize(text);
|
|
13
|
+
|
|
14
|
+
for (const token of tokens) {
|
|
15
|
+
const bucket = hashToken(token) % this.dimensions;
|
|
16
|
+
vector[bucket] += 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return normalize(vector);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
23
|
+
return Promise.all(texts.map((text) => this.embed(text)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tokenize(text: string): string[] {
|
|
28
|
+
return text
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hashToken(token: string): number {
|
|
36
|
+
let hash = 2166136261;
|
|
37
|
+
for (let index = 0; index < token.length; index++) {
|
|
38
|
+
hash ^= token.charCodeAt(index);
|
|
39
|
+
hash = Math.imul(hash, 16777619);
|
|
40
|
+
}
|
|
41
|
+
return hash >>> 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalize(vector: number[]): number[] {
|
|
45
|
+
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
|
|
46
|
+
if (norm === 0) return vector;
|
|
47
|
+
return vector.map((value) => value / norm);
|
|
48
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { MemoryClient } from 'mem0ai';
|
|
2
|
+
import type {
|
|
3
|
+
BenchmarkMemoryRecord,
|
|
4
|
+
BenchmarkRecallRequest,
|
|
5
|
+
BenchmarkRecallResult,
|
|
6
|
+
MemoryProviderAdapter,
|
|
7
|
+
ProviderAvailability,
|
|
8
|
+
} from '../provider.js';
|
|
9
|
+
|
|
10
|
+
export class Mem0BenchmarkAdapter implements MemoryProviderAdapter {
|
|
11
|
+
readonly name = 'mem0';
|
|
12
|
+
readonly label = 'Mem0 (Cloud)';
|
|
13
|
+
readonly capabilities = {
|
|
14
|
+
associations: false,
|
|
15
|
+
forget: true,
|
|
16
|
+
decay: false,
|
|
17
|
+
portability: false,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
private client: MemoryClient | null = null;
|
|
21
|
+
private readonly apiKey: string;
|
|
22
|
+
private readonly idMap = new Map<string, string>(); // benchId -> mem0Id
|
|
23
|
+
private readonly reverseIdMap = new Map<string, string>(); // mem0Id -> benchId
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this.apiKey = process.env.MEM0_API_KEY ?? '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async availability(): Promise<ProviderAvailability> {
|
|
30
|
+
if (!this.apiKey) {
|
|
31
|
+
return {
|
|
32
|
+
status: 'unsupported',
|
|
33
|
+
reason: 'MEM0_API_KEY is not set.',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Basic initialization check
|
|
39
|
+
const testClient = new MemoryClient({ apiKey: this.apiKey });
|
|
40
|
+
if (!testClient) {
|
|
41
|
+
throw new Error('Failed to instantiate MemoryClient');
|
|
42
|
+
}
|
|
43
|
+
return { status: 'available' };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
status: 'unsupported',
|
|
47
|
+
reason: `Mem0 is not initialized: ${error instanceof Error ? error.message : String(error)}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async reset(agentId: string): Promise<void> {
|
|
53
|
+
this.client = new MemoryClient({ apiKey: this.apiKey });
|
|
54
|
+
this.idMap.clear();
|
|
55
|
+
this.reverseIdMap.clear();
|
|
56
|
+
try {
|
|
57
|
+
await this.client.deleteUsers({ userId: agentId });
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore if user does not exist or API fails on empty reset
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async remember(memory: BenchmarkMemoryRecord, agentId: string): Promise<void> {
|
|
64
|
+
if (!this.client) {
|
|
65
|
+
throw new Error(`${this.label} is not initialized`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const response = await (this.client as any).add(
|
|
69
|
+
[{ role: 'user', content: memory.content }],
|
|
70
|
+
{ user_id: agentId, userId: agentId, infer: false }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const mem0Id = response?.results?.[0]?.id;
|
|
74
|
+
if (mem0Id) {
|
|
75
|
+
this.idMap.set(memory.id, mem0Id);
|
|
76
|
+
this.reverseIdMap.set(mem0Id, memory.id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async recall(
|
|
81
|
+
request: BenchmarkRecallRequest & {
|
|
82
|
+
agentId: string;
|
|
83
|
+
},
|
|
84
|
+
): Promise<BenchmarkRecallResult[]> {
|
|
85
|
+
if (!this.client) {
|
|
86
|
+
throw new Error(`${this.label} is not initialized`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const results = await (this.client as any).search(request.query ?? '', {
|
|
90
|
+
filters: {
|
|
91
|
+
user_id: request.agentId,
|
|
92
|
+
},
|
|
93
|
+
limit: request.limit ?? 5,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const rawResults = Array.isArray(results) ? results : (results as any)?.results ?? [];
|
|
97
|
+
|
|
98
|
+
return rawResults.map((result: any, index: number) => {
|
|
99
|
+
const mem0Id = result.id;
|
|
100
|
+
const benchId = mem0Id ? this.reverseIdMap.get(mem0Id) : undefined;
|
|
101
|
+
return {
|
|
102
|
+
memoryId: benchId ?? result.id ?? `mem0-${index}`,
|
|
103
|
+
content: result.memory ?? result.content ?? '',
|
|
104
|
+
score: result.score ?? 1.0,
|
|
105
|
+
source: 'vector',
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async forget(memoryId: string, _agentId: string): Promise<void> {
|
|
111
|
+
if (!this.client) {
|
|
112
|
+
throw new Error(`${this.label} is not initialized`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mappedId = this.idMap.get(memoryId) || memoryId;
|
|
116
|
+
await this.client.delete(mappedId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async close(): Promise<void> {
|
|
120
|
+
this.client = null;
|
|
121
|
+
this.idMap.clear();
|
|
122
|
+
this.reverseIdMap.clear();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BenchmarkMemoryRecord,
|
|
3
|
+
BenchmarkRecallRequest,
|
|
4
|
+
BenchmarkRecallResult,
|
|
5
|
+
MemoryProviderAdapter,
|
|
6
|
+
ProviderAvailability,
|
|
7
|
+
} from '../provider.js';
|
|
8
|
+
import { KeywordEmbeddingProvider } from './keyword-embedding.js';
|
|
9
|
+
|
|
10
|
+
type QdrantPoint = {
|
|
11
|
+
id: number;
|
|
12
|
+
vector: number[];
|
|
13
|
+
payload: {
|
|
14
|
+
benchId: string;
|
|
15
|
+
agentId: string;
|
|
16
|
+
type: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
content: string;
|
|
19
|
+
importance: number;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type QdrantSearchResult = {
|
|
25
|
+
id: number;
|
|
26
|
+
score: number;
|
|
27
|
+
payload?: {
|
|
28
|
+
benchId?: string;
|
|
29
|
+
content?: string;
|
|
30
|
+
type?: string;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export class QdrantBenchmarkAdapter implements MemoryProviderAdapter {
|
|
36
|
+
readonly name = 'qdrant_vector';
|
|
37
|
+
readonly label = 'Qdrant Vector';
|
|
38
|
+
readonly capabilities = {
|
|
39
|
+
associations: false,
|
|
40
|
+
forget: true,
|
|
41
|
+
decay: false,
|
|
42
|
+
portability: false,
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
private readonly embedder = new KeywordEmbeddingProvider();
|
|
46
|
+
private readonly baseUrl: string;
|
|
47
|
+
private readonly apiKey?: string;
|
|
48
|
+
private readonly collectionName: string;
|
|
49
|
+
private pointCounter = 0;
|
|
50
|
+
private readonly idMap = new Map<string, number>();
|
|
51
|
+
|
|
52
|
+
constructor(options: { url?: string; apiKey?: string; collectionName?: string } = {}) {
|
|
53
|
+
this.baseUrl = (options.url ?? process.env.QDRANT_URL ?? '').replace(/\/+$/, '');
|
|
54
|
+
this.apiKey = options.apiKey ?? process.env.QDRANT_API_KEY;
|
|
55
|
+
this.collectionName =
|
|
56
|
+
options.collectionName ?? process.env.QDRANT_COLLECTION ?? 'one_million_brain_bench_v2';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async availability(): Promise<ProviderAvailability> {
|
|
60
|
+
if (!this.baseUrl) {
|
|
61
|
+
return {
|
|
62
|
+
status: 'unsupported',
|
|
63
|
+
reason: 'QDRANT_URL is not set.',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await this.request('/collections', { method: 'GET' });
|
|
69
|
+
return { status: 'available' };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
status: 'unsupported',
|
|
73
|
+
reason: `Qdrant is not reachable at ${this.baseUrl}: ${error instanceof Error ? error.message : String(error)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async reset(_agentId: string): Promise<void> {
|
|
79
|
+
await this.request(`/collections/${encodeURIComponent(this.collectionName)}`, {
|
|
80
|
+
method: 'DELETE',
|
|
81
|
+
allowNotFound: true,
|
|
82
|
+
});
|
|
83
|
+
await this.request(`/collections/${encodeURIComponent(this.collectionName)}`, {
|
|
84
|
+
method: 'PUT',
|
|
85
|
+
body: {
|
|
86
|
+
vectors: {
|
|
87
|
+
size: this.embedder.dimensions,
|
|
88
|
+
distance: 'Cosine',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.pointCounter = 0;
|
|
94
|
+
this.idMap.clear();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async remember(memory: BenchmarkMemoryRecord, agentId: string): Promise<void> {
|
|
98
|
+
this.pointCounter += 1;
|
|
99
|
+
this.idMap.set(memory.id, this.pointCounter);
|
|
100
|
+
|
|
101
|
+
const point: QdrantPoint = {
|
|
102
|
+
id: this.pointCounter,
|
|
103
|
+
vector: await this.embedder.embed(memory.content),
|
|
104
|
+
payload: {
|
|
105
|
+
benchId: memory.id,
|
|
106
|
+
agentId,
|
|
107
|
+
type: memory.type,
|
|
108
|
+
tags: memory.tags,
|
|
109
|
+
content: memory.content,
|
|
110
|
+
importance: memory.importance ?? 0.75,
|
|
111
|
+
metadata: {
|
|
112
|
+
...(memory.metadata ?? {}),
|
|
113
|
+
benchId: memory.id,
|
|
114
|
+
benchTimestamp: memory.timestamp,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
await this.request(`/collections/${encodeURIComponent(this.collectionName)}/points?wait=true`, {
|
|
120
|
+
method: 'PUT',
|
|
121
|
+
body: {
|
|
122
|
+
points: [point],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async recall(
|
|
128
|
+
request: BenchmarkRecallRequest & {
|
|
129
|
+
agentId: string;
|
|
130
|
+
},
|
|
131
|
+
): Promise<BenchmarkRecallResult[]> {
|
|
132
|
+
const response = (await this.request(
|
|
133
|
+
`/collections/${encodeURIComponent(this.collectionName)}/points/search`,
|
|
134
|
+
{
|
|
135
|
+
method: 'POST',
|
|
136
|
+
body: {
|
|
137
|
+
vector: await this.embedder.embed(request.query ?? ''),
|
|
138
|
+
limit: request.limit ?? 5,
|
|
139
|
+
score_threshold: request.minScore ?? 0.08,
|
|
140
|
+
with_payload: true,
|
|
141
|
+
filter: {
|
|
142
|
+
must: [
|
|
143
|
+
{
|
|
144
|
+
key: 'agentId',
|
|
145
|
+
match: { value: request.agentId },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
)) as { result: QdrantSearchResult[] };
|
|
152
|
+
|
|
153
|
+
return response.result.map((result) => ({
|
|
154
|
+
memoryId: result.payload?.benchId ?? String(result.id),
|
|
155
|
+
content: result.payload?.content ?? '',
|
|
156
|
+
score: result.score,
|
|
157
|
+
type: result.payload?.type as BenchmarkMemoryRecord['type'] | undefined,
|
|
158
|
+
source: 'vector',
|
|
159
|
+
metadata: result.payload?.metadata,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async forget(memoryId: string, _agentId: string): Promise<void> {
|
|
164
|
+
const pointId = this.idMap.get(memoryId);
|
|
165
|
+
if (!pointId) return;
|
|
166
|
+
|
|
167
|
+
await this.request(`/collections/${encodeURIComponent(this.collectionName)}/points/delete?wait=true`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: {
|
|
170
|
+
points: [pointId],
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async close(): Promise<void> {
|
|
176
|
+
if (!this.baseUrl) return;
|
|
177
|
+
await this.request(`/collections/${encodeURIComponent(this.collectionName)}`, {
|
|
178
|
+
method: 'DELETE',
|
|
179
|
+
allowNotFound: true,
|
|
180
|
+
}).catch(() => undefined);
|
|
181
|
+
this.idMap.clear();
|
|
182
|
+
this.pointCounter = 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async request(
|
|
186
|
+
path: string,
|
|
187
|
+
options: {
|
|
188
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
189
|
+
body?: unknown;
|
|
190
|
+
allowNotFound?: boolean;
|
|
191
|
+
},
|
|
192
|
+
): Promise<unknown> {
|
|
193
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
194
|
+
method: options.method,
|
|
195
|
+
headers: {
|
|
196
|
+
'content-type': 'application/json',
|
|
197
|
+
...(this.apiKey ? { 'api-key': this.apiKey } : {}),
|
|
198
|
+
},
|
|
199
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (options.allowNotFound && response.status === 404) {
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
const text = await response.text();
|
|
208
|
+
throw new Error(`Qdrant ${response.status}: ${text}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const text = await response.text();
|
|
212
|
+
return text ? JSON.parse(text) : {};
|
|
213
|
+
}
|
|
214
|
+
}
|