@mastra/lance 0.1.1-alpha.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/dist/index.js ADDED
@@ -0,0 +1,1561 @@
1
+ import { connect, Index } from '@lancedb/lancedb';
2
+ import { MessageList } from '@mastra/core/agent';
3
+ import { MastraStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_TRACES, TABLE_EVALS, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
4
+ import { Utf8, Float64, Binary, Float32, Int32, Field, Schema } from 'apache-arrow';
5
+ import { MastraVector } from '@mastra/core/vector';
6
+ import { BaseFilterTranslator } from '@mastra/core/vector/filter';
7
+
8
+ // src/storage/index.ts
9
+ var LanceStorage = class _LanceStorage extends MastraStorage {
10
+ lanceClient;
11
+ /**
12
+ * Creates a new instance of LanceStorage
13
+ * @param uri The URI to connect to LanceDB
14
+ * @param options connection options
15
+ *
16
+ * Usage:
17
+ *
18
+ * Connect to a local database
19
+ * ```ts
20
+ * const store = await LanceStorage.create('/path/to/db');
21
+ * ```
22
+ *
23
+ * Connect to a LanceDB cloud database
24
+ * ```ts
25
+ * const store = await LanceStorage.create('db://host:port');
26
+ * ```
27
+ *
28
+ * Connect to a cloud database
29
+ * ```ts
30
+ * const store = await LanceStorage.create('s3://bucket/db', { storageOptions: { timeout: '60s' } });
31
+ * ```
32
+ */
33
+ static async create(name, uri, options) {
34
+ const instance = new _LanceStorage(name);
35
+ try {
36
+ instance.lanceClient = await connect(uri, options);
37
+ return instance;
38
+ } catch (e) {
39
+ throw new Error(`Failed to connect to LanceDB: ${e}`);
40
+ }
41
+ }
42
+ /**
43
+ * @internal
44
+ * Private constructor to enforce using the create factory method
45
+ */
46
+ constructor(name) {
47
+ super({ name });
48
+ }
49
+ async createTable({
50
+ tableName,
51
+ schema
52
+ }) {
53
+ try {
54
+ const arrowSchema = this.translateSchema(schema);
55
+ await this.lanceClient.createEmptyTable(tableName, arrowSchema);
56
+ } catch (error) {
57
+ throw new Error(`Failed to create table: ${error}`);
58
+ }
59
+ }
60
+ translateSchema(schema) {
61
+ const fields = Object.entries(schema).map(([name, column]) => {
62
+ let arrowType;
63
+ switch (column.type.toLowerCase()) {
64
+ case "text":
65
+ case "uuid":
66
+ arrowType = new Utf8();
67
+ break;
68
+ case "int":
69
+ case "integer":
70
+ arrowType = new Int32();
71
+ break;
72
+ case "bigint":
73
+ arrowType = new Float64();
74
+ break;
75
+ case "float":
76
+ arrowType = new Float32();
77
+ break;
78
+ case "jsonb":
79
+ case "json":
80
+ arrowType = new Utf8();
81
+ break;
82
+ case "binary":
83
+ arrowType = new Binary();
84
+ break;
85
+ case "timestamp":
86
+ arrowType = new Float64();
87
+ break;
88
+ default:
89
+ arrowType = new Utf8();
90
+ }
91
+ return new Field(name, arrowType, column.nullable ?? true);
92
+ });
93
+ return new Schema(fields);
94
+ }
95
+ /**
96
+ * Drop a table if it exists
97
+ * @param tableName Name of the table to drop
98
+ */
99
+ async dropTable(tableName) {
100
+ try {
101
+ await this.lanceClient.dropTable(tableName);
102
+ } catch (error) {
103
+ if (error.toString().includes("was not found")) {
104
+ this.logger.debug(`Table '${tableName}' does not exist, skipping drop`);
105
+ return;
106
+ }
107
+ throw new Error(`Failed to drop table: ${error}`);
108
+ }
109
+ }
110
+ /**
111
+ * Get table schema
112
+ * @param tableName Name of the table
113
+ * @returns Table schema
114
+ */
115
+ async getTableSchema(tableName) {
116
+ try {
117
+ const table = await this.lanceClient.openTable(tableName);
118
+ const rawSchema = await table.schema();
119
+ const fields = rawSchema.fields;
120
+ return {
121
+ fields,
122
+ metadata: /* @__PURE__ */ new Map(),
123
+ get names() {
124
+ return fields.map((field) => field.name);
125
+ }
126
+ };
127
+ } catch (error) {
128
+ throw new Error(`Failed to get table schema: ${error}`);
129
+ }
130
+ }
131
+ getDefaultValue(type) {
132
+ switch (type) {
133
+ case "text":
134
+ return "''";
135
+ case "timestamp":
136
+ return "CURRENT_TIMESTAMP";
137
+ case "integer":
138
+ case "bigint":
139
+ return "0";
140
+ case "jsonb":
141
+ return "'{}'";
142
+ case "uuid":
143
+ return "''";
144
+ default:
145
+ return super.getDefaultValue(type);
146
+ }
147
+ }
148
+ /**
149
+ * Alters table schema to add columns if they don't exist
150
+ * @param tableName Name of the table
151
+ * @param schema Schema of the table
152
+ * @param ifNotExists Array of column names to add if they don't exist
153
+ */
154
+ async alterTable({
155
+ tableName,
156
+ schema,
157
+ ifNotExists
158
+ }) {
159
+ const table = await this.lanceClient.openTable(tableName);
160
+ const currentSchema = await table.schema();
161
+ const existingFields = new Set(currentSchema.fields.map((f) => f.name));
162
+ const typeMap = {
163
+ text: "string",
164
+ integer: "int",
165
+ bigint: "bigint",
166
+ timestamp: "timestamp",
167
+ jsonb: "string",
168
+ uuid: "string"
169
+ };
170
+ const columnsToAdd = ifNotExists.filter((col) => schema[col] && !existingFields.has(col)).map((col) => {
171
+ const colDef = schema[col];
172
+ return {
173
+ name: col,
174
+ valueSql: colDef?.nullable ? `cast(NULL as ${typeMap[colDef.type ?? "text"]})` : `cast(${this.getDefaultValue(colDef?.type ?? "text")} as ${typeMap[colDef?.type ?? "text"]})`
175
+ };
176
+ });
177
+ if (columnsToAdd.length > 0) {
178
+ await table.addColumns(columnsToAdd);
179
+ this.logger?.info?.(`Added columns [${columnsToAdd.map((c) => c.name).join(", ")}] to table ${tableName}`);
180
+ }
181
+ }
182
+ async clearTable({ tableName }) {
183
+ const table = await this.lanceClient.openTable(tableName);
184
+ await table.delete("1=1");
185
+ }
186
+ /**
187
+ * Insert a single record into a table. This function overwrites the existing record if it exists. Use this function for inserting records into tables with custom schemas.
188
+ * @param tableName The name of the table to insert into.
189
+ * @param record The record to insert.
190
+ */
191
+ async insert({ tableName, record }) {
192
+ try {
193
+ const table = await this.lanceClient.openTable(tableName);
194
+ const processedRecord = { ...record };
195
+ for (const key in processedRecord) {
196
+ if (processedRecord[key] !== null && typeof processedRecord[key] === "object" && !(processedRecord[key] instanceof Date)) {
197
+ this.logger.debug("Converting object to JSON string: ", processedRecord[key]);
198
+ processedRecord[key] = JSON.stringify(processedRecord[key]);
199
+ }
200
+ }
201
+ await table.add([processedRecord], { mode: "overwrite" });
202
+ } catch (error) {
203
+ throw new Error(`Failed to insert record: ${error}`);
204
+ }
205
+ }
206
+ /**
207
+ * Insert multiple records into a table. This function overwrites the existing records if they exist. Use this function for inserting records into tables with custom schemas.
208
+ * @param tableName The name of the table to insert into.
209
+ * @param records The records to insert.
210
+ */
211
+ async batchInsert({ tableName, records }) {
212
+ try {
213
+ const table = await this.lanceClient.openTable(tableName);
214
+ const processedRecords = records.map((record) => {
215
+ const processedRecord = { ...record };
216
+ for (const key in processedRecord) {
217
+ if (processedRecord[key] == null) continue;
218
+ if (processedRecord[key] !== null && typeof processedRecord[key] === "object" && !(processedRecord[key] instanceof Date)) {
219
+ processedRecord[key] = JSON.stringify(processedRecord[key]);
220
+ }
221
+ }
222
+ return processedRecord;
223
+ });
224
+ await table.add(processedRecords, { mode: "overwrite" });
225
+ } catch (error) {
226
+ throw new Error(`Failed to batch insert records: ${error}`);
227
+ }
228
+ }
229
+ /**
230
+ * Load a record from the database by its key(s)
231
+ * @param tableName The name of the table to query
232
+ * @param keys Record of key-value pairs to use for lookup
233
+ * @throws Error if invalid types are provided for keys
234
+ * @returns The loaded record with proper type conversions, or null if not found
235
+ */
236
+ async load({ tableName, keys }) {
237
+ try {
238
+ const table = await this.lanceClient.openTable(tableName);
239
+ const tableSchema = await this.getTableSchema(tableName);
240
+ const query = table.query();
241
+ if (Object.keys(keys).length > 0) {
242
+ this.validateKeyTypes(keys, tableSchema);
243
+ const filterConditions = Object.entries(keys).map(([key, value]) => {
244
+ const isCamelCase = /^[a-z][a-zA-Z]*$/.test(key) && /[A-Z]/.test(key);
245
+ const quotedKey = isCamelCase ? `\`${key}\`` : key;
246
+ if (typeof value === "string") {
247
+ return `${quotedKey} = '${value}'`;
248
+ } else if (value === null) {
249
+ return `${quotedKey} IS NULL`;
250
+ } else {
251
+ return `${quotedKey} = ${value}`;
252
+ }
253
+ }).join(" AND ");
254
+ this.logger.debug("where clause generated: " + filterConditions);
255
+ query.where(filterConditions);
256
+ }
257
+ const result = await query.limit(1).toArray();
258
+ if (result.length === 0) {
259
+ this.logger.debug("No record found");
260
+ return null;
261
+ }
262
+ return this.processResultWithTypeConversion(result[0], tableSchema);
263
+ } catch (error) {
264
+ throw new Error(`Failed to load record: ${error}`);
265
+ }
266
+ }
267
+ /**
268
+ * Validates that key types match the schema definition
269
+ * @param keys The keys to validate
270
+ * @param tableSchema The table schema to validate against
271
+ * @throws Error if a key has an incompatible type
272
+ */
273
+ validateKeyTypes(keys, tableSchema) {
274
+ const fieldTypes = new Map(
275
+ tableSchema.fields.map((field) => [field.name, field.type?.toString().toLowerCase()])
276
+ );
277
+ for (const [key, value] of Object.entries(keys)) {
278
+ const fieldType = fieldTypes.get(key);
279
+ if (!fieldType) {
280
+ throw new Error(`Field '${key}' does not exist in table schema`);
281
+ }
282
+ if (value !== null) {
283
+ if ((fieldType.includes("int") || fieldType.includes("bigint")) && typeof value !== "number") {
284
+ throw new Error(`Expected numeric value for field '${key}', got ${typeof value}`);
285
+ }
286
+ if (fieldType.includes("utf8") && typeof value !== "string") {
287
+ throw new Error(`Expected string value for field '${key}', got ${typeof value}`);
288
+ }
289
+ if (fieldType.includes("timestamp") && !(value instanceof Date) && typeof value !== "string") {
290
+ throw new Error(`Expected Date or string value for field '${key}', got ${typeof value}`);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ /**
296
+ * Process a database result with appropriate type conversions based on the table schema
297
+ * @param rawResult The raw result object from the database
298
+ * @param tableSchema The schema of the table containing type information
299
+ * @returns Processed result with correct data types
300
+ */
301
+ processResultWithTypeConversion(rawResult, tableSchema) {
302
+ const fieldTypeMap = /* @__PURE__ */ new Map();
303
+ tableSchema.fields.forEach((field) => {
304
+ const fieldName = field.name;
305
+ const fieldTypeStr = field.type.toString().toLowerCase();
306
+ fieldTypeMap.set(fieldName, fieldTypeStr);
307
+ });
308
+ if (Array.isArray(rawResult)) {
309
+ return rawResult.map((item) => this.processResultWithTypeConversion(item, tableSchema));
310
+ }
311
+ const processedResult = { ...rawResult };
312
+ for (const key in processedResult) {
313
+ const fieldTypeStr = fieldTypeMap.get(key);
314
+ if (!fieldTypeStr) continue;
315
+ if (typeof processedResult[key] === "string") {
316
+ if (fieldTypeStr.includes("int32") || fieldTypeStr.includes("float32")) {
317
+ if (!isNaN(Number(processedResult[key]))) {
318
+ processedResult[key] = Number(processedResult[key]);
319
+ }
320
+ } else if (fieldTypeStr.includes("int64")) {
321
+ processedResult[key] = Number(processedResult[key]);
322
+ } else if (fieldTypeStr.includes("utf8")) {
323
+ try {
324
+ processedResult[key] = JSON.parse(processedResult[key]);
325
+ } catch (e) {
326
+ this.logger.debug(`Failed to parse JSON for key ${key}: ${e}`);
327
+ }
328
+ }
329
+ } else if (typeof processedResult[key] === "bigint") {
330
+ processedResult[key] = Number(processedResult[key]);
331
+ }
332
+ }
333
+ return processedResult;
334
+ }
335
+ getThreadById({ threadId }) {
336
+ try {
337
+ return this.load({ tableName: TABLE_THREADS, keys: { id: threadId } });
338
+ } catch (error) {
339
+ throw new Error(`Failed to get thread by ID: ${error}`);
340
+ }
341
+ }
342
+ async getThreadsByResourceId({ resourceId }) {
343
+ try {
344
+ const table = await this.lanceClient.openTable(TABLE_THREADS);
345
+ const query = table.query().where(`\`resourceId\` = '${resourceId}'`);
346
+ const records = await query.toArray();
347
+ return this.processResultWithTypeConversion(
348
+ records,
349
+ await this.getTableSchema(TABLE_THREADS)
350
+ );
351
+ } catch (error) {
352
+ throw new Error(`Failed to get threads by resource ID: ${error}`);
353
+ }
354
+ }
355
+ /**
356
+ * Saves a thread to the database. This function doesn't overwrite existing threads.
357
+ * @param thread - The thread to save
358
+ * @returns The saved thread
359
+ */
360
+ async saveThread({ thread }) {
361
+ try {
362
+ const record = { ...thread, metadata: JSON.stringify(thread.metadata) };
363
+ const table = await this.lanceClient.openTable(TABLE_THREADS);
364
+ await table.add([record], { mode: "append" });
365
+ return thread;
366
+ } catch (error) {
367
+ throw new Error(`Failed to save thread: ${error}`);
368
+ }
369
+ }
370
+ async updateThread({
371
+ id,
372
+ title,
373
+ metadata
374
+ }) {
375
+ try {
376
+ const record = { id, title, metadata: JSON.stringify(metadata) };
377
+ const table = await this.lanceClient.openTable(TABLE_THREADS);
378
+ await table.add([record], { mode: "overwrite" });
379
+ const query = table.query().where(`id = '${id}'`);
380
+ const records = await query.toArray();
381
+ return this.processResultWithTypeConversion(
382
+ records[0],
383
+ await this.getTableSchema(TABLE_THREADS)
384
+ );
385
+ } catch (error) {
386
+ throw new Error(`Failed to update thread: ${error}`);
387
+ }
388
+ }
389
+ async deleteThread({ threadId }) {
390
+ try {
391
+ const table = await this.lanceClient.openTable(TABLE_THREADS);
392
+ await table.delete(`id = '${threadId}'`);
393
+ } catch (error) {
394
+ throw new Error(`Failed to delete thread: ${error}`);
395
+ }
396
+ }
397
+ /**
398
+ * Processes messages to include context messages based on withPreviousMessages and withNextMessages
399
+ * @param records - The sorted array of records to process
400
+ * @param include - The array of include specifications with context parameters
401
+ * @returns The processed array with context messages included
402
+ */
403
+ processMessagesWithContext(records, include) {
404
+ const messagesWithContext = include.filter((item) => item.withPreviousMessages || item.withNextMessages);
405
+ if (messagesWithContext.length === 0) {
406
+ return records;
407
+ }
408
+ const messageIndexMap = /* @__PURE__ */ new Map();
409
+ records.forEach((message, index) => {
410
+ messageIndexMap.set(message.id, index);
411
+ });
412
+ const additionalIndices = /* @__PURE__ */ new Set();
413
+ for (const item of messagesWithContext) {
414
+ const messageIndex = messageIndexMap.get(item.id);
415
+ if (messageIndex !== void 0) {
416
+ if (item.withPreviousMessages) {
417
+ const startIdx = Math.max(0, messageIndex - item.withPreviousMessages);
418
+ for (let i = startIdx; i < messageIndex; i++) {
419
+ additionalIndices.add(i);
420
+ }
421
+ }
422
+ if (item.withNextMessages) {
423
+ const endIdx = Math.min(records.length - 1, messageIndex + item.withNextMessages);
424
+ for (let i = messageIndex + 1; i <= endIdx; i++) {
425
+ additionalIndices.add(i);
426
+ }
427
+ }
428
+ }
429
+ }
430
+ if (additionalIndices.size === 0) {
431
+ return records;
432
+ }
433
+ const originalMatchIds = new Set(include.map((item) => item.id));
434
+ const allIndices = /* @__PURE__ */ new Set();
435
+ records.forEach((record, index) => {
436
+ if (originalMatchIds.has(record.id)) {
437
+ allIndices.add(index);
438
+ }
439
+ });
440
+ additionalIndices.forEach((index) => {
441
+ allIndices.add(index);
442
+ });
443
+ return Array.from(allIndices).sort((a, b) => a - b).map((index) => records[index]);
444
+ }
445
+ async getMessages({
446
+ threadId,
447
+ resourceId,
448
+ selectBy,
449
+ format,
450
+ threadConfig
451
+ }) {
452
+ try {
453
+ if (threadConfig) {
454
+ throw new Error("ThreadConfig is not supported by LanceDB storage");
455
+ }
456
+ const table = await this.lanceClient.openTable(TABLE_MESSAGES);
457
+ let query = table.query().where(`\`threadId\` = '${threadId}'`);
458
+ if (selectBy) {
459
+ if (selectBy.include && selectBy.include.length > 0) {
460
+ const includeIds = selectBy.include.map((item) => item.id);
461
+ const includeClause = includeIds.map((id) => `\`id\` = '${id}'`).join(" OR ");
462
+ query = query.where(`(\`threadId\` = '${threadId}' OR (${includeClause}))`);
463
+ }
464
+ }
465
+ let records = await query.toArray();
466
+ records.sort((a, b) => {
467
+ const dateA = new Date(a.createdAt).getTime();
468
+ const dateB = new Date(b.createdAt).getTime();
469
+ return dateA - dateB;
470
+ });
471
+ if (selectBy?.include && selectBy.include.length > 0) {
472
+ records = this.processMessagesWithContext(records, selectBy.include);
473
+ }
474
+ if (selectBy?.last !== void 0 && selectBy.last !== false) {
475
+ records = records.slice(-selectBy.last);
476
+ }
477
+ const messages = this.processResultWithTypeConversion(records, await this.getTableSchema(TABLE_MESSAGES));
478
+ const normalized = messages.map((msg) => ({
479
+ ...msg,
480
+ content: typeof msg.content === "string" ? (() => {
481
+ try {
482
+ return JSON.parse(msg.content);
483
+ } catch {
484
+ return msg.content;
485
+ }
486
+ })() : msg.content
487
+ }));
488
+ const list = new MessageList({ threadId, resourceId }).add(normalized, "memory");
489
+ if (format === "v2") return list.get.all.v2();
490
+ return list.get.all.v1();
491
+ } catch (error) {
492
+ throw new Error(`Failed to get messages: ${error}`);
493
+ }
494
+ }
495
+ async saveMessages(args) {
496
+ try {
497
+ const { messages, format = "v1" } = args;
498
+ if (messages.length === 0) {
499
+ return [];
500
+ }
501
+ const threadId = messages[0]?.threadId;
502
+ if (!threadId) {
503
+ throw new Error("Thread ID is required");
504
+ }
505
+ const transformedMessages = messages.map((message) => ({
506
+ ...message,
507
+ content: JSON.stringify(message.content)
508
+ }));
509
+ const table = await this.lanceClient.openTable(TABLE_MESSAGES);
510
+ await table.add(transformedMessages, { mode: "overwrite" });
511
+ const list = new MessageList().add(messages, "memory");
512
+ if (format === `v2`) return list.get.all.v2();
513
+ return list.get.all.v1();
514
+ } catch (error) {
515
+ throw new Error(`Failed to save messages: ${error}`);
516
+ }
517
+ }
518
+ async saveTrace({ trace }) {
519
+ try {
520
+ const table = await this.lanceClient.openTable(TABLE_TRACES);
521
+ const record = {
522
+ ...trace,
523
+ attributes: JSON.stringify(trace.attributes),
524
+ status: JSON.stringify(trace.status),
525
+ events: JSON.stringify(trace.events),
526
+ links: JSON.stringify(trace.links),
527
+ other: JSON.stringify(trace.other)
528
+ };
529
+ await table.add([record], { mode: "append" });
530
+ return trace;
531
+ } catch (error) {
532
+ throw new Error(`Failed to save trace: ${error}`);
533
+ }
534
+ }
535
+ async getTraceById({ traceId }) {
536
+ try {
537
+ const table = await this.lanceClient.openTable(TABLE_TRACES);
538
+ const query = table.query().where(`id = '${traceId}'`);
539
+ const records = await query.toArray();
540
+ return this.processResultWithTypeConversion(records[0], await this.getTableSchema(TABLE_TRACES));
541
+ } catch (error) {
542
+ throw new Error(`Failed to get trace by ID: ${error}`);
543
+ }
544
+ }
545
+ async getTraces({
546
+ name,
547
+ scope,
548
+ page = 1,
549
+ perPage = 10,
550
+ attributes
551
+ }) {
552
+ try {
553
+ const table = await this.lanceClient.openTable(TABLE_TRACES);
554
+ const query = table.query();
555
+ if (name) {
556
+ query.where(`name = '${name}'`);
557
+ }
558
+ if (scope) {
559
+ query.where(`scope = '${scope}'`);
560
+ }
561
+ if (attributes) {
562
+ query.where(`attributes = '${JSON.stringify(attributes)}'`);
563
+ }
564
+ const offset = (page - 1) * perPage;
565
+ query.limit(perPage);
566
+ if (offset > 0) {
567
+ query.offset(offset);
568
+ }
569
+ const records = await query.toArray();
570
+ return records.map((record) => {
571
+ return {
572
+ ...record,
573
+ attributes: JSON.parse(record.attributes),
574
+ status: JSON.parse(record.status),
575
+ events: JSON.parse(record.events),
576
+ links: JSON.parse(record.links),
577
+ other: JSON.parse(record.other),
578
+ startTime: new Date(record.startTime),
579
+ endTime: new Date(record.endTime),
580
+ createdAt: new Date(record.createdAt)
581
+ };
582
+ });
583
+ } catch (error) {
584
+ throw new Error(`Failed to get traces: ${error}`);
585
+ }
586
+ }
587
+ async saveEvals({ evals }) {
588
+ try {
589
+ const table = await this.lanceClient.openTable(TABLE_EVALS);
590
+ const transformedEvals = evals.map((evalRecord) => ({
591
+ input: evalRecord.input,
592
+ output: evalRecord.output,
593
+ agent_name: evalRecord.agentName,
594
+ metric_name: evalRecord.metricName,
595
+ result: JSON.stringify(evalRecord.result),
596
+ instructions: evalRecord.instructions,
597
+ test_info: JSON.stringify(evalRecord.testInfo),
598
+ global_run_id: evalRecord.globalRunId,
599
+ run_id: evalRecord.runId,
600
+ created_at: new Date(evalRecord.createdAt).getTime()
601
+ }));
602
+ await table.add(transformedEvals, { mode: "append" });
603
+ return evals;
604
+ } catch (error) {
605
+ throw new Error(`Failed to save evals: ${error}`);
606
+ }
607
+ }
608
+ async getEvalsByAgentName(agentName, type) {
609
+ try {
610
+ if (type) {
611
+ this.logger.warn("Type is not implemented yet in LanceDB storage");
612
+ }
613
+ const table = await this.lanceClient.openTable(TABLE_EVALS);
614
+ const query = table.query().where(`agent_name = '${agentName}'`);
615
+ const records = await query.toArray();
616
+ return records.map((record) => {
617
+ return {
618
+ id: record.id,
619
+ input: record.input,
620
+ output: record.output,
621
+ agentName: record.agent_name,
622
+ metricName: record.metric_name,
623
+ result: JSON.parse(record.result),
624
+ instructions: record.instructions,
625
+ testInfo: JSON.parse(record.test_info),
626
+ globalRunId: record.global_run_id,
627
+ runId: record.run_id,
628
+ createdAt: new Date(record.created_at).toString()
629
+ };
630
+ });
631
+ } catch (error) {
632
+ throw new Error(`Failed to get evals by agent name: ${error}`);
633
+ }
634
+ }
635
+ parseWorkflowRun(row) {
636
+ let parsedSnapshot = row.snapshot;
637
+ if (typeof parsedSnapshot === "string") {
638
+ try {
639
+ parsedSnapshot = JSON.parse(row.snapshot);
640
+ } catch (e) {
641
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
642
+ }
643
+ }
644
+ return {
645
+ workflowName: row.workflow_name,
646
+ runId: row.run_id,
647
+ snapshot: parsedSnapshot,
648
+ createdAt: this.ensureDate(row.createdAt),
649
+ updatedAt: this.ensureDate(row.updatedAt),
650
+ resourceId: row.resourceId
651
+ };
652
+ }
653
+ async getWorkflowRuns(args) {
654
+ try {
655
+ const table = await this.lanceClient.openTable(TABLE_WORKFLOW_SNAPSHOT);
656
+ const query = table.query();
657
+ if (args?.workflowName) {
658
+ query.where(`workflow_name = '${args.workflowName}'`);
659
+ }
660
+ if (args?.fromDate) {
661
+ query.where(`\`createdAt\` >= ${args.fromDate.getTime()}`);
662
+ }
663
+ if (args?.toDate) {
664
+ query.where(`\`createdAt\` <= ${args.toDate.getTime()}`);
665
+ }
666
+ if (args?.limit) {
667
+ query.limit(args.limit);
668
+ }
669
+ if (args?.offset) {
670
+ query.offset(args.offset);
671
+ }
672
+ const records = await query.toArray();
673
+ return {
674
+ runs: records.map((record) => this.parseWorkflowRun(record)),
675
+ total: records.length
676
+ };
677
+ } catch (error) {
678
+ throw new Error(`Failed to get workflow runs: ${error}`);
679
+ }
680
+ }
681
+ /**
682
+ * Retrieve a single workflow run by its runId.
683
+ * @param args The ID of the workflow run to retrieve
684
+ * @returns The workflow run object or null if not found
685
+ */
686
+ async getWorkflowRunById(args) {
687
+ try {
688
+ const table = await this.lanceClient.openTable(TABLE_WORKFLOW_SNAPSHOT);
689
+ let whereClause = `run_id = '${args.runId}'`;
690
+ if (args.workflowName) {
691
+ whereClause += ` AND workflow_name = '${args.workflowName}'`;
692
+ }
693
+ const query = table.query().where(whereClause);
694
+ const records = await query.toArray();
695
+ if (records.length === 0) return null;
696
+ const record = records[0];
697
+ return this.parseWorkflowRun(record);
698
+ } catch (error) {
699
+ throw new Error(`Failed to get workflow run by id: ${error}`);
700
+ }
701
+ }
702
+ async persistWorkflowSnapshot({
703
+ workflowName,
704
+ runId,
705
+ snapshot
706
+ }) {
707
+ try {
708
+ const table = await this.lanceClient.openTable(TABLE_WORKFLOW_SNAPSHOT);
709
+ const query = table.query().where(`workflow_name = '${workflowName}' AND run_id = '${runId}'`);
710
+ const records = await query.toArray();
711
+ let createdAt;
712
+ const now = Date.now();
713
+ let mode = "append";
714
+ if (records.length > 0) {
715
+ createdAt = records[0].createdAt ?? now;
716
+ mode = "overwrite";
717
+ } else {
718
+ createdAt = now;
719
+ }
720
+ const record = {
721
+ workflow_name: workflowName,
722
+ run_id: runId,
723
+ snapshot: JSON.stringify(snapshot),
724
+ createdAt,
725
+ updatedAt: now
726
+ };
727
+ await table.add([record], { mode });
728
+ } catch (error) {
729
+ throw new Error(`Failed to persist workflow snapshot: ${error}`);
730
+ }
731
+ }
732
+ async loadWorkflowSnapshot({
733
+ workflowName,
734
+ runId
735
+ }) {
736
+ try {
737
+ const table = await this.lanceClient.openTable(TABLE_WORKFLOW_SNAPSHOT);
738
+ const query = table.query().where(`workflow_name = '${workflowName}' AND run_id = '${runId}'`);
739
+ const records = await query.toArray();
740
+ return records.length > 0 ? JSON.parse(records[0].snapshot) : null;
741
+ } catch (error) {
742
+ throw new Error(`Failed to load workflow snapshot: ${error}`);
743
+ }
744
+ }
745
+ };
746
+ var LanceFilterTranslator = class extends BaseFilterTranslator {
747
+ translate(filter) {
748
+ if (!filter || Object.keys(filter).length === 0) {
749
+ return "";
750
+ }
751
+ if (typeof filter === "object" && filter !== null) {
752
+ const keys = Object.keys(filter);
753
+ for (const key of keys) {
754
+ if (key.includes(".") && !this.isNormalNestedField(key)) {
755
+ throw new Error(`Field names containing periods (.) are not supported: ${key}`);
756
+ }
757
+ }
758
+ }
759
+ return this.processFilter(filter);
760
+ }
761
+ processFilter(filter, parentPath = "") {
762
+ if (filter === null) {
763
+ return `${parentPath} IS NULL`;
764
+ }
765
+ if (filter instanceof Date) {
766
+ return `${parentPath} = ${this.formatValue(filter)}`;
767
+ }
768
+ if (typeof filter === "object" && filter !== null) {
769
+ const obj = filter;
770
+ const keys = Object.keys(obj);
771
+ if (keys.length === 1 && this.isOperator(keys[0])) {
772
+ const operator = keys[0];
773
+ const operatorValue = obj[operator];
774
+ if (this.isLogicalOperator(operator)) {
775
+ if (operator === "$and" || operator === "$or") {
776
+ return this.processLogicalOperator(operator, operatorValue);
777
+ }
778
+ throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(operator));
779
+ }
780
+ throw new Error(BaseFilterTranslator.ErrorMessages.INVALID_TOP_LEVEL_OPERATOR(operator));
781
+ }
782
+ for (const key of keys) {
783
+ if (key.includes(".") && !this.isNormalNestedField(key)) {
784
+ throw new Error(`Field names containing periods (.) are not supported: ${key}`);
785
+ }
786
+ }
787
+ if (keys.length > 1) {
788
+ const conditions = keys.map((key) => {
789
+ const value = obj[key];
790
+ if (this.isNestedObject(value) && !this.isDateObject(value)) {
791
+ return this.processNestedObject(key, value);
792
+ } else {
793
+ return this.processField(key, value);
794
+ }
795
+ });
796
+ return conditions.join(" AND ");
797
+ }
798
+ if (keys.length === 1) {
799
+ const key = keys[0];
800
+ const value = obj[key];
801
+ if (this.isNestedObject(value) && !this.isDateObject(value)) {
802
+ return this.processNestedObject(key, value);
803
+ } else {
804
+ return this.processField(key, value);
805
+ }
806
+ }
807
+ }
808
+ return "";
809
+ }
810
+ processLogicalOperator(operator, conditions) {
811
+ if (!Array.isArray(conditions)) {
812
+ throw new Error(`Logical operator ${operator} must have an array value`);
813
+ }
814
+ if (conditions.length === 0) {
815
+ return operator === "$and" ? "true" : "false";
816
+ }
817
+ const sqlOperator = operator === "$and" ? "AND" : "OR";
818
+ const processedConditions = conditions.map((condition) => {
819
+ if (typeof condition !== "object" || condition === null) {
820
+ throw new Error(BaseFilterTranslator.ErrorMessages.INVALID_LOGICAL_OPERATOR_CONTENT(operator));
821
+ }
822
+ const condObj = condition;
823
+ const keys = Object.keys(condObj);
824
+ if (keys.length === 1 && this.isOperator(keys[0])) {
825
+ if (this.isLogicalOperator(keys[0])) {
826
+ return `(${this.processLogicalOperator(keys[0], condObj[keys[0]])})`;
827
+ } else {
828
+ throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(keys[0]));
829
+ }
830
+ }
831
+ if (keys.length > 1) {
832
+ return `(${this.processFilter(condition)})`;
833
+ }
834
+ return this.processFilter(condition);
835
+ });
836
+ return processedConditions.join(` ${sqlOperator} `);
837
+ }
838
+ processNestedObject(path, value) {
839
+ if (typeof value !== "object" || value === null) {
840
+ throw new Error(`Expected object for nested path ${path}`);
841
+ }
842
+ const obj = value;
843
+ const keys = Object.keys(obj);
844
+ if (keys.length === 0) {
845
+ return `${path} = {}`;
846
+ }
847
+ if (keys.every((k) => this.isOperator(k))) {
848
+ return this.processOperators(path, obj);
849
+ }
850
+ const conditions = keys.map((key) => {
851
+ const nestedPath = key.includes(".") ? `${path}.${key}` : `${path}.${key}`;
852
+ if (this.isNestedObject(obj[key]) && !this.isDateObject(obj[key])) {
853
+ return this.processNestedObject(nestedPath, obj[key]);
854
+ } else {
855
+ return this.processField(nestedPath, obj[key]);
856
+ }
857
+ });
858
+ return conditions.join(" AND ");
859
+ }
860
+ processField(field, value) {
861
+ if (field.includes(".") && !this.isNormalNestedField(field)) {
862
+ throw new Error(`Field names containing periods (.) are not supported: ${field}`);
863
+ }
864
+ const escapedField = this.escapeFieldName(field);
865
+ if (value === null) {
866
+ return `${escapedField} IS NULL`;
867
+ }
868
+ if (value instanceof Date) {
869
+ return `${escapedField} = ${this.formatValue(value)}`;
870
+ }
871
+ if (Array.isArray(value)) {
872
+ if (value.length === 0) {
873
+ return "false";
874
+ }
875
+ const normalizedValues = this.normalizeArrayValues(value);
876
+ return `${escapedField} IN (${this.formatArrayValues(normalizedValues)})`;
877
+ }
878
+ if (this.isOperatorObject(value)) {
879
+ return this.processOperators(field, value);
880
+ }
881
+ return `${escapedField} = ${this.formatValue(this.normalizeComparisonValue(value))}`;
882
+ }
883
+ processOperators(field, operators) {
884
+ const escapedField = this.escapeFieldName(field);
885
+ const operatorKeys = Object.keys(operators);
886
+ if (operatorKeys.some((op) => this.isLogicalOperator(op))) {
887
+ const logicalOp = operatorKeys.find((op) => this.isLogicalOperator(op)) || "";
888
+ throw new Error(`Unsupported operator: ${logicalOp} cannot be used at field level`);
889
+ }
890
+ return operatorKeys.map((op) => {
891
+ const value = operators[op];
892
+ if (!this.isFieldOperator(op) && !this.isCustomOperator(op)) {
893
+ throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(op));
894
+ }
895
+ switch (op) {
896
+ case "$eq":
897
+ if (value === null) {
898
+ return `${escapedField} IS NULL`;
899
+ }
900
+ return `${escapedField} = ${this.formatValue(this.normalizeComparisonValue(value))}`;
901
+ case "$ne":
902
+ if (value === null) {
903
+ return `${escapedField} IS NOT NULL`;
904
+ }
905
+ return `${escapedField} != ${this.formatValue(this.normalizeComparisonValue(value))}`;
906
+ case "$gt":
907
+ return `${escapedField} > ${this.formatValue(this.normalizeComparisonValue(value))}`;
908
+ case "$gte":
909
+ return `${escapedField} >= ${this.formatValue(this.normalizeComparisonValue(value))}`;
910
+ case "$lt":
911
+ return `${escapedField} < ${this.formatValue(this.normalizeComparisonValue(value))}`;
912
+ case "$lte":
913
+ return `${escapedField} <= ${this.formatValue(this.normalizeComparisonValue(value))}`;
914
+ case "$in":
915
+ if (!Array.isArray(value)) {
916
+ throw new Error(`$in operator requires array value for field: ${field}`);
917
+ }
918
+ if (value.length === 0) {
919
+ return "false";
920
+ }
921
+ const normalizedValues = this.normalizeArrayValues(value);
922
+ return `${escapedField} IN (${this.formatArrayValues(normalizedValues)})`;
923
+ case "$like":
924
+ return `${escapedField} LIKE ${this.formatValue(value)}`;
925
+ case "$notLike":
926
+ return `${escapedField} NOT LIKE ${this.formatValue(value)}`;
927
+ case "$regex":
928
+ return `regexp_match(${escapedField}, ${this.formatValue(value)})`;
929
+ default:
930
+ throw new Error(BaseFilterTranslator.ErrorMessages.UNSUPPORTED_OPERATOR(op));
931
+ }
932
+ }).join(" AND ");
933
+ }
934
+ formatValue(value) {
935
+ if (value === null) {
936
+ return "NULL";
937
+ }
938
+ if (typeof value === "string") {
939
+ return `'${value.replace(/'/g, "''")}'`;
940
+ }
941
+ if (typeof value === "number") {
942
+ return value.toString();
943
+ }
944
+ if (typeof value === "boolean") {
945
+ return value ? "true" : "false";
946
+ }
947
+ if (value instanceof Date) {
948
+ return `timestamp '${value.toISOString()}'`;
949
+ }
950
+ if (typeof value === "object") {
951
+ if (value instanceof Date) {
952
+ return `timestamp '${value.toISOString()}'`;
953
+ }
954
+ return JSON.stringify(value);
955
+ }
956
+ return String(value);
957
+ }
958
+ formatArrayValues(array) {
959
+ return array.map((item) => this.formatValue(item)).join(", ");
960
+ }
961
+ normalizeArrayValues(array) {
962
+ return array.map((item) => {
963
+ if (item instanceof Date) {
964
+ return item;
965
+ }
966
+ return this.normalizeComparisonValue(item);
967
+ });
968
+ }
969
+ normalizeComparisonValue(value) {
970
+ if (value instanceof Date) {
971
+ return value;
972
+ }
973
+ return super.normalizeComparisonValue(value);
974
+ }
975
+ isOperatorObject(value) {
976
+ if (typeof value !== "object" || value === null) {
977
+ return false;
978
+ }
979
+ const obj = value;
980
+ const keys = Object.keys(obj);
981
+ return keys.length > 0 && keys.some((key) => this.isOperator(key));
982
+ }
983
+ isNestedObject(value) {
984
+ return typeof value === "object" && value !== null && !Array.isArray(value);
985
+ }
986
+ isNormalNestedField(field) {
987
+ const parts = field.split(".");
988
+ return !field.startsWith(".") && !field.endsWith(".") && parts.every((part) => part.trim().length > 0);
989
+ }
990
+ escapeFieldName(field) {
991
+ if (field.includes(" ") || field.includes("-") || /^[A-Z]+$/.test(field) || this.isSqlKeyword(field)) {
992
+ if (field.includes(".")) {
993
+ return field.split(".").map((part) => `\`${part}\``).join(".");
994
+ }
995
+ return `\`${field}\``;
996
+ }
997
+ return field;
998
+ }
999
+ isSqlKeyword(str) {
1000
+ const sqlKeywords = [
1001
+ "SELECT",
1002
+ "FROM",
1003
+ "WHERE",
1004
+ "AND",
1005
+ "OR",
1006
+ "NOT",
1007
+ "INSERT",
1008
+ "UPDATE",
1009
+ "DELETE",
1010
+ "CREATE",
1011
+ "ALTER",
1012
+ "DROP",
1013
+ "TABLE",
1014
+ "VIEW",
1015
+ "INDEX",
1016
+ "JOIN",
1017
+ "INNER",
1018
+ "OUTER",
1019
+ "LEFT",
1020
+ "RIGHT",
1021
+ "FULL",
1022
+ "UNION",
1023
+ "ALL",
1024
+ "DISTINCT",
1025
+ "AS",
1026
+ "ON",
1027
+ "BETWEEN",
1028
+ "LIKE",
1029
+ "IN",
1030
+ "IS",
1031
+ "NULL",
1032
+ "TRUE",
1033
+ "FALSE",
1034
+ "ASC",
1035
+ "DESC",
1036
+ "GROUP",
1037
+ "ORDER",
1038
+ "BY",
1039
+ "HAVING",
1040
+ "LIMIT",
1041
+ "OFFSET",
1042
+ "CASE",
1043
+ "WHEN",
1044
+ "THEN",
1045
+ "ELSE",
1046
+ "END",
1047
+ "CAST",
1048
+ "CUBE"
1049
+ ];
1050
+ return sqlKeywords.includes(str.toUpperCase());
1051
+ }
1052
+ isDateObject(value) {
1053
+ return value instanceof Date;
1054
+ }
1055
+ /**
1056
+ * Override getSupportedOperators to add custom operators for LanceDB
1057
+ */
1058
+ getSupportedOperators() {
1059
+ return {
1060
+ ...BaseFilterTranslator.DEFAULT_OPERATORS,
1061
+ custom: ["$like", "$notLike", "$regex"]
1062
+ };
1063
+ }
1064
+ };
1065
+
1066
+ // src/vector/index.ts
1067
+ var LanceVectorStore = class _LanceVectorStore extends MastraVector {
1068
+ lanceClient;
1069
+ /**
1070
+ * Creates a new instance of LanceVectorStore
1071
+ * @param uri The URI to connect to LanceDB
1072
+ * @param options connection options
1073
+ *
1074
+ * Usage:
1075
+ *
1076
+ * Connect to a local database
1077
+ * ```ts
1078
+ * const store = await LanceVectorStore.create('/path/to/db');
1079
+ * ```
1080
+ *
1081
+ * Connect to a LanceDB cloud database
1082
+ * ```ts
1083
+ * const store = await LanceVectorStore.create('db://host:port');
1084
+ * ```
1085
+ *
1086
+ * Connect to a cloud database
1087
+ * ```ts
1088
+ * const store = await LanceVectorStore.create('s3://bucket/db', { storageOptions: { timeout: '60s' } });
1089
+ * ```
1090
+ */
1091
+ static async create(uri, options) {
1092
+ const instance = new _LanceVectorStore();
1093
+ try {
1094
+ instance.lanceClient = await connect(uri, options);
1095
+ return instance;
1096
+ } catch (e) {
1097
+ throw new Error(`Failed to connect to LanceDB: ${e}`);
1098
+ }
1099
+ }
1100
+ /**
1101
+ * @internal
1102
+ * Private constructor to enforce using the create factory method
1103
+ */
1104
+ constructor() {
1105
+ super();
1106
+ }
1107
+ close() {
1108
+ if (this.lanceClient) {
1109
+ this.lanceClient.close();
1110
+ }
1111
+ }
1112
+ async query({
1113
+ tableName,
1114
+ queryVector,
1115
+ filter,
1116
+ includeVector = false,
1117
+ topK = 10,
1118
+ columns = [],
1119
+ includeAllColumns = false
1120
+ }) {
1121
+ if (!this.lanceClient) {
1122
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1123
+ }
1124
+ if (!tableName) {
1125
+ throw new Error("tableName is required");
1126
+ }
1127
+ if (!queryVector) {
1128
+ throw new Error("queryVector is required");
1129
+ }
1130
+ try {
1131
+ const table = await this.lanceClient.openTable(tableName);
1132
+ const selectColumns = [...columns];
1133
+ if (!selectColumns.includes("id")) {
1134
+ selectColumns.push("id");
1135
+ }
1136
+ let query = table.search(queryVector);
1137
+ if (filter && Object.keys(filter).length > 0) {
1138
+ const whereClause = this.filterTranslator(filter);
1139
+ this.logger.debug(`Where clause generated: ${whereClause}`);
1140
+ query = query.where(whereClause);
1141
+ }
1142
+ if (!includeAllColumns && selectColumns.length > 0) {
1143
+ query = query.select(selectColumns);
1144
+ }
1145
+ query = query.limit(topK);
1146
+ const results = await query.toArray();
1147
+ return results.map((result) => {
1148
+ const flatMetadata = {};
1149
+ Object.keys(result).forEach((key) => {
1150
+ if (key !== "id" && key !== "score" && key !== "vector" && key !== "_distance") {
1151
+ if (key.startsWith("metadata_")) {
1152
+ const metadataKey = key.substring("metadata_".length);
1153
+ flatMetadata[metadataKey] = result[key];
1154
+ }
1155
+ }
1156
+ });
1157
+ const metadata = this.unflattenObject(flatMetadata);
1158
+ return {
1159
+ id: String(result.id || ""),
1160
+ metadata,
1161
+ vector: includeVector && result.vector ? Array.isArray(result.vector) ? result.vector : Array.from(result.vector) : void 0,
1162
+ document: result.document,
1163
+ score: result._distance
1164
+ };
1165
+ });
1166
+ } catch (error) {
1167
+ throw new Error(`Failed to query vectors: ${error.message}`);
1168
+ }
1169
+ }
1170
+ filterTranslator(filter) {
1171
+ const processFilterKeys = (filterObj) => {
1172
+ const result = {};
1173
+ Object.entries(filterObj).forEach(([key, value]) => {
1174
+ if (key === "$or" || key === "$and" || key === "$not" || key === "$in") {
1175
+ if (Array.isArray(value)) {
1176
+ result[key] = value.map(
1177
+ (item) => typeof item === "object" && item !== null ? processFilterKeys(item) : item
1178
+ );
1179
+ } else {
1180
+ result[key] = value;
1181
+ }
1182
+ } else if (key.startsWith("metadata_")) {
1183
+ result[key] = value;
1184
+ } else {
1185
+ if (key.includes(".")) {
1186
+ const convertedKey = `metadata_${key.replace(/\./g, "_")}`;
1187
+ result[convertedKey] = value;
1188
+ } else {
1189
+ result[`metadata_${key}`] = value;
1190
+ }
1191
+ }
1192
+ });
1193
+ return result;
1194
+ };
1195
+ const prefixedFilter = filter && typeof filter === "object" ? processFilterKeys(filter) : {};
1196
+ const translator = new LanceFilterTranslator();
1197
+ return translator.translate(prefixedFilter);
1198
+ }
1199
+ async upsert({ tableName, vectors, metadata = [], ids = [] }) {
1200
+ if (!this.lanceClient) {
1201
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1202
+ }
1203
+ if (!tableName) {
1204
+ throw new Error("tableName is required");
1205
+ }
1206
+ if (!vectors || !Array.isArray(vectors) || vectors.length === 0) {
1207
+ throw new Error("vectors array is required and must not be empty");
1208
+ }
1209
+ try {
1210
+ const tables = await this.lanceClient.tableNames();
1211
+ if (!tables.includes(tableName)) {
1212
+ throw new Error(`Table ${tableName} does not exist`);
1213
+ }
1214
+ const table = await this.lanceClient.openTable(tableName);
1215
+ const vectorIds = ids.length === vectors.length ? ids : vectors.map((_, i) => ids[i] || crypto.randomUUID());
1216
+ const data = vectors.map((vector, i) => {
1217
+ const id = String(vectorIds[i]);
1218
+ const metadataItem = metadata[i] || {};
1219
+ const rowData = {
1220
+ id,
1221
+ vector
1222
+ };
1223
+ if (Object.keys(metadataItem).length > 0) {
1224
+ const flattenedMetadata = this.flattenObject(metadataItem, "metadata");
1225
+ Object.entries(flattenedMetadata).forEach(([key, value]) => {
1226
+ rowData[key] = value;
1227
+ });
1228
+ }
1229
+ return rowData;
1230
+ });
1231
+ await table.add(data, { mode: "overwrite" });
1232
+ return vectorIds;
1233
+ } catch (error) {
1234
+ throw new Error(`Failed to upsert vectors: ${error.message}`);
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Flattens a nested object, creating new keys with underscores for nested properties.
1239
+ * Example: { metadata: { text: 'test' } } → { metadata_text: 'test' }
1240
+ */
1241
+ flattenObject(obj, prefix = "") {
1242
+ return Object.keys(obj).reduce((acc, k) => {
1243
+ const pre = prefix.length ? `${prefix}_` : "";
1244
+ if (typeof obj[k] === "object" && obj[k] !== null && !Array.isArray(obj[k])) {
1245
+ Object.assign(acc, this.flattenObject(obj[k], pre + k));
1246
+ } else {
1247
+ acc[pre + k] = obj[k];
1248
+ }
1249
+ return acc;
1250
+ }, {});
1251
+ }
1252
+ async createTable(tableName, data, options) {
1253
+ if (!this.lanceClient) {
1254
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1255
+ }
1256
+ try {
1257
+ if (Array.isArray(data)) {
1258
+ data = data.map((record) => this.flattenObject(record));
1259
+ }
1260
+ return await this.lanceClient.createTable(tableName, data, options);
1261
+ } catch (error) {
1262
+ throw new Error(`Failed to create table: ${error.message}`);
1263
+ }
1264
+ }
1265
+ async listTables() {
1266
+ if (!this.lanceClient) {
1267
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1268
+ }
1269
+ return await this.lanceClient.tableNames();
1270
+ }
1271
+ async getTableSchema(tableName) {
1272
+ if (!this.lanceClient) {
1273
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1274
+ }
1275
+ const table = await this.lanceClient.openTable(tableName);
1276
+ return await table.schema();
1277
+ }
1278
+ /**
1279
+ * indexName is actually a column name in a table in lanceDB
1280
+ */
1281
+ async createIndex({
1282
+ tableName,
1283
+ indexName,
1284
+ dimension,
1285
+ metric = "cosine",
1286
+ indexConfig = {}
1287
+ }) {
1288
+ if (!this.lanceClient) {
1289
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1290
+ }
1291
+ try {
1292
+ if (!tableName) {
1293
+ throw new Error("tableName is required");
1294
+ }
1295
+ if (!indexName) {
1296
+ throw new Error("indexName is required");
1297
+ }
1298
+ if (typeof dimension !== "number" || dimension <= 0) {
1299
+ throw new Error("dimension must be a positive number");
1300
+ }
1301
+ const tables = await this.lanceClient.tableNames();
1302
+ if (!tables.includes(tableName)) {
1303
+ throw new Error(
1304
+ `Table ${tableName} does not exist. Please create the table first by calling createTable() method.`
1305
+ );
1306
+ }
1307
+ const table = await this.lanceClient.openTable(tableName);
1308
+ let metricType;
1309
+ if (metric === "euclidean") {
1310
+ metricType = "l2";
1311
+ } else if (metric === "dotproduct") {
1312
+ metricType = "dot";
1313
+ } else if (metric === "cosine") {
1314
+ metricType = "cosine";
1315
+ }
1316
+ if (indexConfig.type === "ivfflat") {
1317
+ await table.createIndex(indexName, {
1318
+ config: Index.ivfPq({
1319
+ numPartitions: indexConfig.numPartitions || 128,
1320
+ numSubVectors: indexConfig.numSubVectors || 16,
1321
+ distanceType: metricType
1322
+ })
1323
+ });
1324
+ } else {
1325
+ this.logger.debug("Creating HNSW PQ index with config:", indexConfig);
1326
+ await table.createIndex(indexName, {
1327
+ config: Index.hnswPq({
1328
+ m: indexConfig?.hnsw?.m || 16,
1329
+ efConstruction: indexConfig?.hnsw?.efConstruction || 100,
1330
+ distanceType: metricType
1331
+ })
1332
+ });
1333
+ }
1334
+ } catch (error) {
1335
+ throw new Error(`Failed to create index: ${error.message}`);
1336
+ }
1337
+ }
1338
+ async listIndexes() {
1339
+ if (!this.lanceClient) {
1340
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1341
+ }
1342
+ try {
1343
+ const tables = await this.lanceClient.tableNames();
1344
+ const allIndices = [];
1345
+ for (const tableName of tables) {
1346
+ const table = await this.lanceClient.openTable(tableName);
1347
+ const tableIndices = await table.listIndices();
1348
+ allIndices.push(...tableIndices.map((index) => index.name));
1349
+ }
1350
+ return allIndices;
1351
+ } catch (error) {
1352
+ throw new Error(`Failed to list indexes: ${error.message}`);
1353
+ }
1354
+ }
1355
+ async describeIndex({ indexName }) {
1356
+ if (!this.lanceClient) {
1357
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1358
+ }
1359
+ if (!indexName) {
1360
+ throw new Error("indexName is required");
1361
+ }
1362
+ try {
1363
+ const tables = await this.lanceClient.tableNames();
1364
+ for (const tableName of tables) {
1365
+ this.logger.debug("Checking table:" + tableName);
1366
+ const table = await this.lanceClient.openTable(tableName);
1367
+ const tableIndices = await table.listIndices();
1368
+ const foundIndex = tableIndices.find((index) => index.name === indexName);
1369
+ if (foundIndex) {
1370
+ const stats = await table.indexStats(foundIndex.name);
1371
+ if (!stats) {
1372
+ throw new Error(`Index stats not found for index: ${indexName}`);
1373
+ }
1374
+ const schema = await table.schema();
1375
+ const vectorCol = foundIndex.columns[0] || "vector";
1376
+ const vectorField = schema.fields.find((field) => field.name === vectorCol);
1377
+ const dimension = vectorField?.type?.["listSize"] || 0;
1378
+ return {
1379
+ dimension,
1380
+ metric: stats.distanceType,
1381
+ count: stats.numIndexedRows
1382
+ };
1383
+ }
1384
+ }
1385
+ throw new Error(`IndexName: ${indexName} not found`);
1386
+ } catch (error) {
1387
+ throw new Error(`Failed to describe index: ${error.message}`);
1388
+ }
1389
+ }
1390
+ async deleteIndex({ indexName }) {
1391
+ if (!this.lanceClient) {
1392
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1393
+ }
1394
+ if (!indexName) {
1395
+ throw new Error("indexName is required");
1396
+ }
1397
+ try {
1398
+ const tables = await this.lanceClient.tableNames();
1399
+ for (const tableName of tables) {
1400
+ const table = await this.lanceClient.openTable(tableName);
1401
+ const tableIndices = await table.listIndices();
1402
+ const foundIndex = tableIndices.find((index) => index.name === indexName);
1403
+ if (foundIndex) {
1404
+ await table.dropIndex(indexName);
1405
+ return;
1406
+ }
1407
+ }
1408
+ throw new Error(`Index ${indexName} not found`);
1409
+ } catch (error) {
1410
+ throw new Error(`Failed to delete index: ${error.message}`);
1411
+ }
1412
+ }
1413
+ /**
1414
+ * Deletes all tables in the database
1415
+ */
1416
+ async deleteAllTables() {
1417
+ if (!this.lanceClient) {
1418
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1419
+ }
1420
+ try {
1421
+ await this.lanceClient.dropAllTables();
1422
+ } catch (error) {
1423
+ throw new Error(`Failed to delete tables: ${error.message}`);
1424
+ }
1425
+ }
1426
+ async deleteTable(tableName) {
1427
+ if (!this.lanceClient) {
1428
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1429
+ }
1430
+ try {
1431
+ await this.lanceClient.dropTable(tableName);
1432
+ } catch (error) {
1433
+ throw new Error(`Failed to delete tables: ${error.message}`);
1434
+ }
1435
+ }
1436
+ async updateVector({ indexName, id, update }) {
1437
+ if (!this.lanceClient) {
1438
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1439
+ }
1440
+ if (!indexName) {
1441
+ throw new Error("indexName is required");
1442
+ }
1443
+ if (!id) {
1444
+ throw new Error("id is required");
1445
+ }
1446
+ try {
1447
+ const tables = await this.lanceClient.tableNames();
1448
+ for (const tableName of tables) {
1449
+ this.logger.debug("Checking table:" + tableName);
1450
+ const table = await this.lanceClient.openTable(tableName);
1451
+ try {
1452
+ const schema = await table.schema();
1453
+ const hasColumn = schema.fields.some((field) => field.name === indexName);
1454
+ if (hasColumn) {
1455
+ this.logger.debug(`Found column ${indexName} in table ${tableName}`);
1456
+ const existingRecord = await table.query().where(`id = '${id}'`).select(schema.fields.map((field) => field.name)).limit(1).toArray();
1457
+ if (existingRecord.length === 0) {
1458
+ throw new Error(`Record with id '${id}' not found in table ${tableName}`);
1459
+ }
1460
+ const rowData = {
1461
+ id
1462
+ };
1463
+ Object.entries(existingRecord[0]).forEach(([key, value]) => {
1464
+ if (key !== "id" && key !== "_distance") {
1465
+ if (key === indexName) {
1466
+ if (!update.vector) {
1467
+ if (Array.isArray(value)) {
1468
+ rowData[key] = [...value];
1469
+ } else if (typeof value === "object" && value !== null) {
1470
+ rowData[key] = Array.from(value);
1471
+ } else {
1472
+ rowData[key] = value;
1473
+ }
1474
+ }
1475
+ } else {
1476
+ rowData[key] = value;
1477
+ }
1478
+ }
1479
+ });
1480
+ if (update.vector) {
1481
+ rowData[indexName] = update.vector;
1482
+ }
1483
+ if (update.metadata) {
1484
+ Object.entries(update.metadata).forEach(([key, value]) => {
1485
+ rowData[`metadata_${key}`] = value;
1486
+ });
1487
+ }
1488
+ await table.add([rowData], { mode: "overwrite" });
1489
+ return;
1490
+ }
1491
+ } catch (err) {
1492
+ this.logger.error(`Error checking schema for table ${tableName}:` + err);
1493
+ continue;
1494
+ }
1495
+ }
1496
+ throw new Error(`No table found with column/index '${indexName}'`);
1497
+ } catch (error) {
1498
+ throw new Error(`Failed to update index: ${error.message}`);
1499
+ }
1500
+ }
1501
+ async deleteVector({ indexName, id }) {
1502
+ if (!this.lanceClient) {
1503
+ throw new Error("LanceDB client not initialized. Use LanceVectorStore.create() to create an instance");
1504
+ }
1505
+ if (!indexName) {
1506
+ throw new Error("indexName is required");
1507
+ }
1508
+ if (!id) {
1509
+ throw new Error("id is required");
1510
+ }
1511
+ try {
1512
+ const tables = await this.lanceClient.tableNames();
1513
+ for (const tableName of tables) {
1514
+ this.logger.debug("Checking table:" + tableName);
1515
+ const table = await this.lanceClient.openTable(tableName);
1516
+ try {
1517
+ const schema = await table.schema();
1518
+ const hasColumn = schema.fields.some((field) => field.name === indexName);
1519
+ if (hasColumn) {
1520
+ this.logger.debug(`Found column ${indexName} in table ${tableName}`);
1521
+ await table.delete(`id = '${id}'`);
1522
+ return;
1523
+ }
1524
+ } catch (err) {
1525
+ this.logger.error(`Error checking schema for table ${tableName}:` + err);
1526
+ continue;
1527
+ }
1528
+ }
1529
+ throw new Error(`No table found with column/index '${indexName}'`);
1530
+ } catch (error) {
1531
+ throw new Error(`Failed to delete index: ${error.message}`);
1532
+ }
1533
+ }
1534
+ /**
1535
+ * Converts a flattened object with keys using underscore notation back to a nested object.
1536
+ * Example: { name: 'test', details_text: 'test' } → { name: 'test', details: { text: 'test' } }
1537
+ */
1538
+ unflattenObject(obj) {
1539
+ const result = {};
1540
+ Object.keys(obj).forEach((key) => {
1541
+ const value = obj[key];
1542
+ const parts = key.split("_");
1543
+ let current = result;
1544
+ for (let i = 0; i < parts.length - 1; i++) {
1545
+ const part = parts[i];
1546
+ if (!part) continue;
1547
+ if (!current[part] || typeof current[part] !== "object") {
1548
+ current[part] = {};
1549
+ }
1550
+ current = current[part];
1551
+ }
1552
+ const lastPart = parts[parts.length - 1];
1553
+ if (lastPart) {
1554
+ current[lastPart] = value;
1555
+ }
1556
+ });
1557
+ return result;
1558
+ }
1559
+ };
1560
+
1561
+ export { LanceStorage, LanceVectorStore };