atlas-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +32 -0
- package/README.md +282 -0
- package/package.json +72 -0
- package/public/app/assets/app-CxbS1w9p.js +3981 -0
- package/public/app/assets/index-BA6nxCuI.css +1 -0
- package/public/app/assets/index-BXmIRrQH.js +177 -0
- package/public/app/index.html +27 -0
- package/public/assets/brain-atlas.LICENSE.txt +16 -0
- package/public/assets/brain-atlas.glb +0 -0
- package/public/assets/brain.obj +27282 -0
- package/public/fonts/DepartureMono-Regular.woff +0 -0
- package/public/fonts/DepartureMono-Regular.woff2 +0 -0
- package/scripts/sync-memory-vectors.js +46 -0
- package/src/audit.js +9 -0
- package/src/cli/args.js +87 -0
- package/src/cli/commands/add.js +103 -0
- package/src/cli/commands/config.js +228 -0
- package/src/cli/commands/delete.js +75 -0
- package/src/cli/commands/entities.js +39 -0
- package/src/cli/commands/entity.js +47 -0
- package/src/cli/commands/get.js +46 -0
- package/src/cli/commands/list.js +53 -0
- package/src/cli/commands/related.js +56 -0
- package/src/cli/commands/search.js +68 -0
- package/src/cli/commands/update.js +58 -0
- package/src/cli/deps.js +114 -0
- package/src/cli/env-file.js +44 -0
- package/src/cli/format.js +246 -0
- package/src/cli.js +187 -0
- package/src/cognitive-worker.js +381 -0
- package/src/db.js +2674 -0
- package/src/extraction-context.js +31 -0
- package/src/ingestion-service.js +387 -0
- package/src/ingestion-worker.js +225 -0
- package/src/llm-config.js +31 -0
- package/src/llm.js +789 -0
- package/src/logger.js +51 -0
- package/src/mcp-server.js +577 -0
- package/src/memory-comparison.js +421 -0
- package/src/related-memories.js +232 -0
- package/src/run-cognitive-worker.js +12 -0
- package/src/run-ingestion-worker.js +13 -0
- package/src/run-vector-worker.js +12 -0
- package/src/schemas.js +413 -0
- package/src/semantic-validation.js +430 -0
- package/src/server.js +827 -0
- package/src/shared/brain-regions.js +61 -0
- package/src/shared/entity-lens.js +249 -0
- package/src/shared/memory-placement.js +171 -0
- package/src/shared/memory-search.js +55 -0
- package/src/shared/region-anchors.js +112 -0
- package/src/shared/region-mapper.js +247 -0
- package/src/vector-store.js +546 -0
- package/src/vector-worker.js +71 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { connect as connectLanceDb } from "@lancedb/lancedb";
|
|
3
|
+
import { QdrantClient } from "@qdrant/js-client-rest";
|
|
4
|
+
import {
|
|
5
|
+
Field,
|
|
6
|
+
FixedSizeList,
|
|
7
|
+
Float32,
|
|
8
|
+
Float64,
|
|
9
|
+
List,
|
|
10
|
+
Schema,
|
|
11
|
+
Utf8,
|
|
12
|
+
} from "apache-arrow";
|
|
13
|
+
|
|
14
|
+
export const EMBEDDING_MODEL =
|
|
15
|
+
process.env.EMBEDDING_MODEL || "Xenova/all-MiniLM-L6-v2";
|
|
16
|
+
export const EMBEDDING_DIMENSIONS = Number.parseInt(
|
|
17
|
+
process.env.EMBEDDING_DIMENSIONS || "384",
|
|
18
|
+
10,
|
|
19
|
+
);
|
|
20
|
+
export const QDRANT_COLLECTION =
|
|
21
|
+
process.env.QDRANT_COLLECTION || "atlas_memories";
|
|
22
|
+
export const LANCEDB_PATH = process.env.LANCEDB_PATH || "./lancedb";
|
|
23
|
+
export const LANCEDB_TABLE = process.env.LANCEDB_TABLE || "atlas_memories";
|
|
24
|
+
|
|
25
|
+
let embedderPromise;
|
|
26
|
+
let defaultStore;
|
|
27
|
+
|
|
28
|
+
async function loadEmbedder() {
|
|
29
|
+
if (!embedderPromise) {
|
|
30
|
+
embedderPromise = import("@huggingface/transformers")
|
|
31
|
+
.then(({ pipeline }) =>
|
|
32
|
+
pipeline("feature-extraction", EMBEDDING_MODEL, {
|
|
33
|
+
dtype: process.env.EMBEDDING_DTYPE || "q8",
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
.catch((error) => {
|
|
37
|
+
embedderPromise = undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return embedderPromise;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function embedText(text) {
|
|
45
|
+
const input = String(text || "").trim();
|
|
46
|
+
if (!input) throw new Error("Cannot embed empty text");
|
|
47
|
+
|
|
48
|
+
const embedder = await loadEmbedder();
|
|
49
|
+
const output = await embedder(input, {
|
|
50
|
+
pooling: "mean",
|
|
51
|
+
normalize: true,
|
|
52
|
+
});
|
|
53
|
+
const values = output.tolist();
|
|
54
|
+
const vector = Array.isArray(values[0]) ? values[0] : values;
|
|
55
|
+
|
|
56
|
+
if (vector.length !== EMBEDDING_DIMENSIONS) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Embedding model returned ${vector.length} dimensions; expected ${EMBEDDING_DIMENSIONS}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return vector;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function memoryEmbeddingText(memory) {
|
|
65
|
+
const rawText = String(memory?.raw_text || "").trim();
|
|
66
|
+
const summary = String(memory?.summary || "").trim();
|
|
67
|
+
return summary && summary !== rawText ? `${rawText}\n${summary}` : rawText;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function memoryPointId(memoryId) {
|
|
71
|
+
const bytes = createHash("sha256").update(String(memoryId)).digest().subarray(0, 16);
|
|
72
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
73
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
74
|
+
const hex = bytes.toString("hex");
|
|
75
|
+
return [
|
|
76
|
+
hex.slice(0, 8),
|
|
77
|
+
hex.slice(8, 12),
|
|
78
|
+
hex.slice(12, 16),
|
|
79
|
+
hex.slice(16, 20),
|
|
80
|
+
hex.slice(20),
|
|
81
|
+
].join("-");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createMemoryVectorStore({
|
|
85
|
+
client,
|
|
86
|
+
embed = embedText,
|
|
87
|
+
collectionName = QDRANT_COLLECTION,
|
|
88
|
+
vectorSize = EMBEDDING_DIMENSIONS,
|
|
89
|
+
embeddingModel = EMBEDDING_MODEL,
|
|
90
|
+
} = {}) {
|
|
91
|
+
let collectionPromise;
|
|
92
|
+
|
|
93
|
+
async function ensureCollection() {
|
|
94
|
+
if (!collectionPromise) {
|
|
95
|
+
collectionPromise = (async () => {
|
|
96
|
+
const { exists } = await client.collectionExists(collectionName);
|
|
97
|
+
if (!exists) {
|
|
98
|
+
try {
|
|
99
|
+
await client.createCollection(collectionName, {
|
|
100
|
+
vectors: { size: vectorSize, distance: "Cosine" },
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const result = await client.collectionExists(collectionName);
|
|
104
|
+
if (!result.exists) throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const collection = await client.getCollection(collectionName);
|
|
109
|
+
const vectors = collection.config?.params?.vectors;
|
|
110
|
+
if (vectors?.size && vectors.size !== vectorSize) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Qdrant collection "${collectionName}" uses ${vectors.size} dimensions; expected ${vectorSize}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
})().catch((error) => {
|
|
116
|
+
collectionPromise = undefined;
|
|
117
|
+
throw error;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return collectionPromise;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function indexMemory(memory) {
|
|
124
|
+
if (!memory?.id) throw new Error("Memory ID is required for vector indexing");
|
|
125
|
+
await ensureCollection();
|
|
126
|
+
|
|
127
|
+
const text = memoryEmbeddingText(memory);
|
|
128
|
+
const vector = await embed(text);
|
|
129
|
+
if (vector.length !== vectorSize) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Embedding has ${vector.length} dimensions; expected ${vectorSize}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await client.upsert(collectionName, {
|
|
136
|
+
wait: true,
|
|
137
|
+
points: [
|
|
138
|
+
{
|
|
139
|
+
id: memoryPointId(memory.id),
|
|
140
|
+
vector,
|
|
141
|
+
payload: {
|
|
142
|
+
memory_id: memory.id,
|
|
143
|
+
type: memory.type,
|
|
144
|
+
title: memory.title,
|
|
145
|
+
confidence: memory.confidence,
|
|
146
|
+
tags: memory.tags || [],
|
|
147
|
+
scope: "agent",
|
|
148
|
+
embedding_model: embeddingModel,
|
|
149
|
+
source: memory.source || "ui",
|
|
150
|
+
ingestion_date: memory.ingestion_date,
|
|
151
|
+
created_at: memory.created_at,
|
|
152
|
+
updated_at: memory.updated_at,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function searchMemories(query, { limit = 10, scoreThreshold } = {}) {
|
|
160
|
+
await ensureCollection();
|
|
161
|
+
const vector = await embed(query);
|
|
162
|
+
|
|
163
|
+
const response = await client.query(collectionName, {
|
|
164
|
+
query: vector,
|
|
165
|
+
limit,
|
|
166
|
+
score_threshold: scoreThreshold,
|
|
167
|
+
with_payload: true,
|
|
168
|
+
with_vector: false,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return response.points
|
|
172
|
+
.filter((point) => typeof point.payload?.memory_id === "string")
|
|
173
|
+
.map((point) => ({
|
|
174
|
+
id: point.payload.memory_id,
|
|
175
|
+
score: point.score,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function deleteMemory(memoryId) {
|
|
180
|
+
const { exists } = await client.collectionExists(collectionName);
|
|
181
|
+
if (!exists) return;
|
|
182
|
+
|
|
183
|
+
await client.delete(collectionName, {
|
|
184
|
+
wait: true,
|
|
185
|
+
points: [memoryPointId(memoryId)],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function deleteAllMemories() {
|
|
190
|
+
const { exists } = await client.collectionExists(collectionName);
|
|
191
|
+
if (!exists) return;
|
|
192
|
+
|
|
193
|
+
await client.deleteCollection(collectionName);
|
|
194
|
+
collectionPromise = undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
deleteAllMemories,
|
|
199
|
+
deleteMemory,
|
|
200
|
+
indexMemory,
|
|
201
|
+
searchMemories,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function lanceDbSchema(vectorSize, embeddingModel) {
|
|
206
|
+
return new Schema(
|
|
207
|
+
[
|
|
208
|
+
new Field("memory_id", new Utf8(), false),
|
|
209
|
+
new Field(
|
|
210
|
+
"vector",
|
|
211
|
+
new FixedSizeList(
|
|
212
|
+
vectorSize,
|
|
213
|
+
new Field("item", new Float32(), false),
|
|
214
|
+
),
|
|
215
|
+
false,
|
|
216
|
+
),
|
|
217
|
+
new Field("type", new Utf8(), true),
|
|
218
|
+
new Field("title", new Utf8(), true),
|
|
219
|
+
new Field("confidence", new Float64(), true),
|
|
220
|
+
new Field(
|
|
221
|
+
"tags",
|
|
222
|
+
new List(new Field("item", new Utf8(), false)),
|
|
223
|
+
false,
|
|
224
|
+
),
|
|
225
|
+
new Field("scope", new Utf8(), false),
|
|
226
|
+
new Field("embedding_model", new Utf8(), false),
|
|
227
|
+
new Field("source", new Utf8(), false),
|
|
228
|
+
new Field("ingestion_date", new Utf8(), true),
|
|
229
|
+
new Field("created_at", new Utf8(), true),
|
|
230
|
+
new Field("updated_at", new Utf8(), true),
|
|
231
|
+
],
|
|
232
|
+
new Map([
|
|
233
|
+
["embedding_model", embeddingModel],
|
|
234
|
+
["embedding_dimensions", String(vectorSize)],
|
|
235
|
+
]),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function escapeSqlString(value) {
|
|
240
|
+
return String(value).replaceAll("'", "''");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function validateVector(vector, vectorSize) {
|
|
244
|
+
if (!Array.isArray(vector) && !ArrayBuffer.isView(vector)) {
|
|
245
|
+
throw new Error("Embedding must be an array or typed array");
|
|
246
|
+
}
|
|
247
|
+
if (vector.length !== vectorSize) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Embedding has ${vector.length} dimensions; expected ${vectorSize}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function createLanceDbMemoryVectorStore({
|
|
255
|
+
connection,
|
|
256
|
+
path = LANCEDB_PATH,
|
|
257
|
+
tableName = LANCEDB_TABLE,
|
|
258
|
+
embed = embedText,
|
|
259
|
+
vectorSize = EMBEDDING_DIMENSIONS,
|
|
260
|
+
embeddingModel = EMBEDDING_MODEL,
|
|
261
|
+
} = {}) {
|
|
262
|
+
if (!Number.isInteger(vectorSize) || vectorSize <= 0) {
|
|
263
|
+
throw new Error("LanceDB vector size must be a positive integer");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let connectionPromise = connection
|
|
267
|
+
? Promise.resolve(connection)
|
|
268
|
+
: undefined;
|
|
269
|
+
let tablePromise;
|
|
270
|
+
|
|
271
|
+
async function getConnection() {
|
|
272
|
+
if (!connectionPromise) {
|
|
273
|
+
connectionPromise = connectLanceDb(path).catch((error) => {
|
|
274
|
+
connectionPromise = undefined;
|
|
275
|
+
throw error;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return connectionPromise;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function validateTable(table) {
|
|
282
|
+
const schema = await table.schema();
|
|
283
|
+
const vectorField = schema.fields.find((field) => field.name === "vector");
|
|
284
|
+
const actualSize = vectorField?.type?.listSize;
|
|
285
|
+
if (actualSize !== vectorSize) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`LanceDB table "${tableName}" uses ${actualSize ?? "unknown"} dimensions; expected ${vectorSize}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const actualModel = schema.metadata?.get("embedding_model");
|
|
292
|
+
if (actualModel !== embeddingModel) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`LanceDB table "${tableName}" uses embedding model "${actualModel || "unknown"}"; expected "${embeddingModel}"`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return table;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function ensureTable() {
|
|
301
|
+
if (!tablePromise) {
|
|
302
|
+
tablePromise = (async () => {
|
|
303
|
+
const db = await getConnection();
|
|
304
|
+
const names = await db.tableNames();
|
|
305
|
+
const table = names.includes(tableName)
|
|
306
|
+
? await db.openTable(tableName)
|
|
307
|
+
: await db.createEmptyTable(
|
|
308
|
+
tableName,
|
|
309
|
+
lanceDbSchema(vectorSize, embeddingModel),
|
|
310
|
+
{ mode: "create", existOk: true },
|
|
311
|
+
);
|
|
312
|
+
return validateTable(table);
|
|
313
|
+
})().catch((error) => {
|
|
314
|
+
tablePromise = undefined;
|
|
315
|
+
throw error;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return tablePromise;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function indexMemory(memory) {
|
|
322
|
+
if (!memory?.id) throw new Error("Memory ID is required for vector indexing");
|
|
323
|
+
|
|
324
|
+
const vector = await embed(memoryEmbeddingText(memory));
|
|
325
|
+
validateVector(vector, vectorSize);
|
|
326
|
+
const table = await ensureTable();
|
|
327
|
+
await table
|
|
328
|
+
.mergeInsert("memory_id")
|
|
329
|
+
.whenMatchedUpdateAll()
|
|
330
|
+
.whenNotMatchedInsertAll()
|
|
331
|
+
.execute([
|
|
332
|
+
{
|
|
333
|
+
memory_id: String(memory.id),
|
|
334
|
+
vector: Array.from(vector),
|
|
335
|
+
type: memory.type == null ? null : String(memory.type),
|
|
336
|
+
title: memory.title == null ? null : String(memory.title),
|
|
337
|
+
confidence:
|
|
338
|
+
memory.confidence == null ? null : Number(memory.confidence),
|
|
339
|
+
tags: (memory.tags || []).map(String),
|
|
340
|
+
scope: "agent",
|
|
341
|
+
embedding_model: embeddingModel,
|
|
342
|
+
source: String(memory.source || "ui"),
|
|
343
|
+
ingestion_date:
|
|
344
|
+
memory.ingestion_date == null
|
|
345
|
+
? null
|
|
346
|
+
: String(memory.ingestion_date),
|
|
347
|
+
created_at:
|
|
348
|
+
memory.created_at == null ? null : String(memory.created_at),
|
|
349
|
+
updated_at:
|
|
350
|
+
memory.updated_at == null ? null : String(memory.updated_at),
|
|
351
|
+
},
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function searchMemories(query, { limit = 10, scoreThreshold } = {}) {
|
|
356
|
+
const vector = await embed(query);
|
|
357
|
+
validateVector(vector, vectorSize);
|
|
358
|
+
const table = await ensureTable();
|
|
359
|
+
const rows = await table
|
|
360
|
+
.vectorSearch(Array.from(vector))
|
|
361
|
+
.distanceType("cosine")
|
|
362
|
+
.select(["memory_id", "_distance"])
|
|
363
|
+
.limit(limit)
|
|
364
|
+
.toArray();
|
|
365
|
+
|
|
366
|
+
return rows
|
|
367
|
+
.map((row) => ({
|
|
368
|
+
id: row.memory_id,
|
|
369
|
+
score: 1 - row._distance,
|
|
370
|
+
}))
|
|
371
|
+
.filter(
|
|
372
|
+
(row) =>
|
|
373
|
+
typeof row.id === "string" &&
|
|
374
|
+
(scoreThreshold == null || row.score >= scoreThreshold),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function deleteMemory(memoryId) {
|
|
379
|
+
const db = await getConnection();
|
|
380
|
+
if (!(await db.tableNames()).includes(tableName)) return;
|
|
381
|
+
const table = await ensureTable();
|
|
382
|
+
await table.delete(`memory_id = '${escapeSqlString(memoryId)}'`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function deleteAllMemories() {
|
|
386
|
+
const db = await getConnection();
|
|
387
|
+
if (!(await db.tableNames()).includes(tableName)) return;
|
|
388
|
+
|
|
389
|
+
const table = await tablePromise;
|
|
390
|
+
table?.close();
|
|
391
|
+
await db.dropTable(tableName);
|
|
392
|
+
tablePromise = undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
deleteAllMemories,
|
|
397
|
+
deleteMemory,
|
|
398
|
+
indexMemory,
|
|
399
|
+
searchMemories,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function getQdrantCloudConfig(environment = process.env) {
|
|
404
|
+
const url = String(environment.QDRANT_URL || "").trim();
|
|
405
|
+
const apiKey = String(environment.QDRANT_API_KEY || "").trim();
|
|
406
|
+
const timeout = Number.parseInt(
|
|
407
|
+
environment.QDRANT_TIMEOUT_MS || "10000",
|
|
408
|
+
10,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
if (!url) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
"QDRANT_URL is required. Use the HTTPS REST endpoint from your Qdrant Cloud cluster.",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (!apiKey) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
"QDRANT_API_KEY is required. Create a Database API key in Qdrant Cloud.",
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let parsedUrl;
|
|
423
|
+
try {
|
|
424
|
+
parsedUrl = new URL(url);
|
|
425
|
+
} catch {
|
|
426
|
+
throw new Error("QDRANT_URL must be a valid HTTPS URL.");
|
|
427
|
+
}
|
|
428
|
+
if (parsedUrl.protocol !== "https:") {
|
|
429
|
+
throw new Error("QDRANT_URL must use HTTPS for Qdrant Cloud.");
|
|
430
|
+
}
|
|
431
|
+
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
432
|
+
throw new Error("QDRANT_TIMEOUT_MS must be a positive integer.");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
url: parsedUrl.href.replace(/\/$/, ""),
|
|
437
|
+
apiKey,
|
|
438
|
+
timeout,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function createQdrantCloudClient(environment = process.env) {
|
|
443
|
+
const config = getQdrantCloudConfig(environment);
|
|
444
|
+
return new QdrantClient({
|
|
445
|
+
...config,
|
|
446
|
+
checkCompatibility: false,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function assertAtlasModeSupported(environment = process.env) {
|
|
451
|
+
const mode = String(environment.ATLAS_MODE || "local").trim().toLowerCase();
|
|
452
|
+
if (mode === "cloud") {
|
|
453
|
+
throw new Error(
|
|
454
|
+
"ATLAS_MODE=cloud is not available yet; managed Atlas cloud service support has not been implemented.",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (mode !== "local") {
|
|
458
|
+
throw new Error(`Invalid ATLAS_MODE "${mode}"; expected "local" or "cloud".`);
|
|
459
|
+
}
|
|
460
|
+
return mode;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function getDefaultStore() {
|
|
464
|
+
if (!defaultStore) {
|
|
465
|
+
assertAtlasModeSupported();
|
|
466
|
+
defaultStore = createLanceDbMemoryVectorStore();
|
|
467
|
+
}
|
|
468
|
+
return defaultStore;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export const indexMemoryVector = (memory) =>
|
|
472
|
+
getDefaultStore().indexMemory(memory);
|
|
473
|
+
export const searchMemoryVectors = (query, options) =>
|
|
474
|
+
getDefaultStore().searchMemories(query, options);
|
|
475
|
+
export const deleteMemoryVector = (memoryId) =>
|
|
476
|
+
getDefaultStore().deleteMemory(memoryId);
|
|
477
|
+
export const deleteAllMemoryVectors = () =>
|
|
478
|
+
getDefaultStore().deleteAllMemories();
|
|
479
|
+
|
|
480
|
+
const RRF_K = 60;
|
|
481
|
+
|
|
482
|
+
export async function hybridSearchMemories(
|
|
483
|
+
query,
|
|
484
|
+
{ limit = 10, strategy = "hybrid", scoreThreshold, searchMemoriesFts } = {},
|
|
485
|
+
) {
|
|
486
|
+
if (strategy === "bm25") {
|
|
487
|
+
return bm25OnlySearch(query, { limit, scoreThreshold, searchMemoriesFts });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const vectorResultsPromise = searchMemoryVectors(query, {
|
|
491
|
+
limit: Math.max(limit * 3, 30),
|
|
492
|
+
scoreThreshold,
|
|
493
|
+
}).catch(() => []);
|
|
494
|
+
|
|
495
|
+
const bm25ResultsPromise = Promise.resolve(
|
|
496
|
+
searchMemoriesFts
|
|
497
|
+
? searchMemoriesFts(query, { limit: Math.max(limit * 3, 30) })
|
|
498
|
+
: [],
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const [vectorResults, bm25Results] = await Promise.all([
|
|
502
|
+
vectorResultsPromise,
|
|
503
|
+
bm25ResultsPromise,
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
if (strategy === "vector") {
|
|
507
|
+
return vectorResults;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return fuseWithRrf(vectorResults, bm25Results, limit);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function bm25OnlySearch(query, { limit, scoreThreshold, searchMemoriesFts }) {
|
|
514
|
+
const bm25Results = searchMemoriesFts
|
|
515
|
+
? searchMemoriesFts(query, { limit })
|
|
516
|
+
: [];
|
|
517
|
+
if (!scoreThreshold) return bm25Results;
|
|
518
|
+
const maxScore = Math.max(...bm25Results.map((r) => r.score), 0);
|
|
519
|
+
if (maxScore === 0) return bm25Results;
|
|
520
|
+
return bm25Results.filter(
|
|
521
|
+
(r) => Math.abs(r.score / maxScore) >= Math.abs(scoreThreshold),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function fuseWithRrf(vectorResults, bm25Results, limit) {
|
|
526
|
+
const scores = new Map();
|
|
527
|
+
|
|
528
|
+
for (let rank = 0; rank < vectorResults.length; rank++) {
|
|
529
|
+
const { id } = vectorResults[rank];
|
|
530
|
+
const existing = scores.get(id) || 0;
|
|
531
|
+
scores.set(id, existing + 1 / (RRF_K + rank + 1));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (let rank = 0; rank < bm25Results.length; rank++) {
|
|
535
|
+
const { id } = bm25Results[rank];
|
|
536
|
+
const existing = scores.get(id) || 0;
|
|
537
|
+
scores.set(id, existing + 1 / (RRF_K + rank + 1));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const fused = [...scores.entries()]
|
|
541
|
+
.map(([id, score]) => ({ id, score }))
|
|
542
|
+
.sort((a, b) => b.score - a.score)
|
|
543
|
+
.slice(0, limit);
|
|
544
|
+
|
|
545
|
+
return fused;
|
|
546
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const DEFAULT_MAX_ATTEMPTS = 8;
|
|
2
|
+
|
|
3
|
+
export function createVectorWorker({
|
|
4
|
+
db,
|
|
5
|
+
indexMemoryVector,
|
|
6
|
+
now = () => new Date(),
|
|
7
|
+
pollIntervalMs = 1_000,
|
|
8
|
+
baseRetryMs = 1_000,
|
|
9
|
+
maxRetryMs = 60_000,
|
|
10
|
+
} = {}) {
|
|
11
|
+
if (!db || typeof db.claimVectorIndexJob !== "function") {
|
|
12
|
+
throw new TypeError("vector worker requires a database job adapter");
|
|
13
|
+
}
|
|
14
|
+
if (typeof indexMemoryVector !== "function") {
|
|
15
|
+
throw new TypeError("vector worker requires indexMemoryVector()");
|
|
16
|
+
}
|
|
17
|
+
let stopped = false;
|
|
18
|
+
|
|
19
|
+
const runOnce = async () => {
|
|
20
|
+
const claimedAt = now().toISOString();
|
|
21
|
+
const job = db.claimVectorIndexJob({ now: claimedAt });
|
|
22
|
+
if (!job) return { status: "idle" };
|
|
23
|
+
try {
|
|
24
|
+
const memory = db.getMemory(job.memory_id);
|
|
25
|
+
if (!memory) throw new Error(`Memory not found: ${job.memory_id}`);
|
|
26
|
+
if (memory.version === job.memory_version) await indexMemoryVector(memory);
|
|
27
|
+
db.completeVectorIndexJob({ jobId: job.id, completedAt: now().toISOString() });
|
|
28
|
+
return {
|
|
29
|
+
status: memory.version === job.memory_version ? "completed" : "superseded",
|
|
30
|
+
job,
|
|
31
|
+
};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const attempts = Number(job.attempts) || 1;
|
|
34
|
+
const terminal = attempts >= (job.max_attempts || DEFAULT_MAX_ATTEMPTS);
|
|
35
|
+
const failedAt = now();
|
|
36
|
+
if (terminal) {
|
|
37
|
+
db.failVectorIndexJob(job.id, {
|
|
38
|
+
error: error.message,
|
|
39
|
+
failedAt: failedAt.toISOString(),
|
|
40
|
+
});
|
|
41
|
+
return { status: "failed", job, error };
|
|
42
|
+
}
|
|
43
|
+
const delay = Math.min(maxRetryMs, baseRetryMs * 2 ** (attempts - 1));
|
|
44
|
+
db.retryVectorIndexJob({
|
|
45
|
+
jobId: job.id,
|
|
46
|
+
error: error.message,
|
|
47
|
+
retryAt: new Date(failedAt.getTime() + delay).toISOString(),
|
|
48
|
+
updatedAt: failedAt.toISOString(),
|
|
49
|
+
});
|
|
50
|
+
return { status: "retrying", job, error };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const run = async ({ signal } = {}) => {
|
|
55
|
+
stopped = false;
|
|
56
|
+
const recoveredAt = now();
|
|
57
|
+
db.recoverVectorIndexJobs({
|
|
58
|
+
now: recoveredAt.toISOString(),
|
|
59
|
+
retryAt: recoveredAt.toISOString(),
|
|
60
|
+
staleBefore: new Date(recoveredAt.getTime() - 5 * 60_000).toISOString(),
|
|
61
|
+
});
|
|
62
|
+
while (!stopped && !signal?.aborted) {
|
|
63
|
+
const result = await runOnce();
|
|
64
|
+
if (result.status === "idle") {
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return { run, runOnce, stop: () => { stopped = true; } };
|
|
71
|
+
}
|