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