@mastra/lance 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,212 @@
1
+ import type { Connection } from '@lancedb/lancedb';
2
+ import { MastraError, ErrorDomain, ErrorCategory } from '@mastra/core/error';
3
+ import type { TraceType } from '@mastra/core/memory';
4
+ import { TABLE_TRACES, TracesStorage } from '@mastra/core/storage';
5
+ import type { PaginationInfo, StorageGetTracesPaginatedArg } from '@mastra/core/storage';
6
+ import type { Trace } from '@mastra/core/telemetry';
7
+ import type { StoreOperationsLance } from '../operations';
8
+
9
+ export class StoreTracesLance extends TracesStorage {
10
+ private client: Connection;
11
+ private operations: StoreOperationsLance;
12
+ constructor({ client, operations }: { client: Connection; operations: StoreOperationsLance }) {
13
+ super();
14
+ this.client = client;
15
+ this.operations = operations;
16
+ }
17
+
18
+ async saveTrace({ trace }: { trace: TraceType }): Promise<TraceType> {
19
+ try {
20
+ const table = await this.client.openTable(TABLE_TRACES);
21
+ const record = {
22
+ ...trace,
23
+ attributes: JSON.stringify(trace.attributes),
24
+ status: JSON.stringify(trace.status),
25
+ events: JSON.stringify(trace.events),
26
+ links: JSON.stringify(trace.links),
27
+ other: JSON.stringify(trace.other),
28
+ };
29
+ await table.add([record], { mode: 'append' });
30
+ return trace;
31
+ } catch (error: any) {
32
+ throw new MastraError(
33
+ {
34
+ id: 'LANCE_STORE_SAVE_TRACE_FAILED',
35
+ domain: ErrorDomain.STORAGE,
36
+ category: ErrorCategory.THIRD_PARTY,
37
+ },
38
+ error,
39
+ );
40
+ }
41
+ }
42
+
43
+ async getTraceById({ traceId }: { traceId: string }): Promise<TraceType> {
44
+ try {
45
+ const table = await this.client.openTable(TABLE_TRACES);
46
+ const query = table.query().where(`id = '${traceId}'`);
47
+ const records = await query.toArray();
48
+ return records[0] as TraceType;
49
+ } catch (error: any) {
50
+ throw new MastraError(
51
+ {
52
+ id: 'LANCE_STORE_GET_TRACE_BY_ID_FAILED',
53
+ domain: ErrorDomain.STORAGE,
54
+ category: ErrorCategory.THIRD_PARTY,
55
+ },
56
+ error,
57
+ );
58
+ }
59
+ }
60
+
61
+ async getTraces({
62
+ name,
63
+ scope,
64
+ page = 1,
65
+ perPage = 10,
66
+ attributes,
67
+ }: {
68
+ name?: string;
69
+ scope?: string;
70
+ page: number;
71
+ perPage: number;
72
+ attributes?: Record<string, string>;
73
+ }): Promise<Trace[]> {
74
+ try {
75
+ const table = await this.client.openTable(TABLE_TRACES);
76
+ const query = table.query();
77
+ if (name) {
78
+ query.where(`name = '${name}'`);
79
+ }
80
+ if (scope) {
81
+ query.where(`scope = '${scope}'`);
82
+ }
83
+ if (attributes) {
84
+ query.where(`attributes = '${JSON.stringify(attributes)}'`);
85
+ }
86
+ // Calculate offset based on page and perPage
87
+ const offset = (page - 1) * perPage;
88
+ query.limit(perPage);
89
+ if (offset > 0) {
90
+ query.offset(offset);
91
+ }
92
+ const records = await query.toArray();
93
+ return records.map(record => {
94
+ const processed = {
95
+ ...record,
96
+ attributes: record.attributes ? JSON.parse(record.attributes) : {},
97
+ status: record.status ? JSON.parse(record.status) : {},
98
+ events: record.events ? JSON.parse(record.events) : [],
99
+ links: record.links ? JSON.parse(record.links) : [],
100
+ other: record.other ? JSON.parse(record.other) : {},
101
+ startTime: new Date(record.startTime),
102
+ endTime: new Date(record.endTime),
103
+ createdAt: new Date(record.createdAt),
104
+ };
105
+ if (processed.parentSpanId === null || processed.parentSpanId === undefined) {
106
+ processed.parentSpanId = '';
107
+ } else {
108
+ processed.parentSpanId = String(processed.parentSpanId);
109
+ }
110
+ return processed as Trace;
111
+ });
112
+ } catch (error: any) {
113
+ throw new MastraError(
114
+ {
115
+ id: 'LANCE_STORE_GET_TRACES_FAILED',
116
+ domain: ErrorDomain.STORAGE,
117
+ category: ErrorCategory.THIRD_PARTY,
118
+ details: { name: name ?? '', scope: scope ?? '' },
119
+ },
120
+ error,
121
+ );
122
+ }
123
+ }
124
+
125
+ async getTracesPaginated(args: StorageGetTracesPaginatedArg): Promise<PaginationInfo & { traces: Trace[] }> {
126
+ try {
127
+ const table = await this.client.openTable(TABLE_TRACES);
128
+ const query = table.query();
129
+ const conditions: string[] = [];
130
+ if (args.name) {
131
+ conditions.push(`name = '${args.name}'`);
132
+ }
133
+ if (args.scope) {
134
+ conditions.push(`scope = '${args.scope}'`);
135
+ }
136
+ if (args.attributes) {
137
+ const attributesStr = JSON.stringify(args.attributes);
138
+ conditions.push(`attributes LIKE '%${attributesStr.replace(/"/g, '\\"')}%'`);
139
+ }
140
+ if (args.dateRange?.start) {
141
+ conditions.push(`\`createdAt\` >= ${args.dateRange.start.getTime()}`);
142
+ }
143
+ if (args.dateRange?.end) {
144
+ conditions.push(`\`createdAt\` <= ${args.dateRange.end.getTime()}`);
145
+ }
146
+ if (conditions.length > 0) {
147
+ const whereClause = conditions.join(' AND ');
148
+ query.where(whereClause);
149
+ }
150
+ let total = 0;
151
+ if (conditions.length > 0) {
152
+ const countQuery = table.query().where(conditions.join(' AND '));
153
+ const allRecords = await countQuery.toArray();
154
+ total = allRecords.length;
155
+ } else {
156
+ total = await table.countRows();
157
+ }
158
+ const page = args.page || 0;
159
+ const perPage = args.perPage || 10;
160
+ const offset = page * perPage;
161
+ query.limit(perPage);
162
+ if (offset > 0) {
163
+ query.offset(offset);
164
+ }
165
+ const records = await query.toArray();
166
+ const traces = records.map(record => {
167
+ const processed = {
168
+ ...record,
169
+ attributes: record.attributes ? JSON.parse(record.attributes) : {},
170
+ status: record.status ? JSON.parse(record.status) : {},
171
+ events: record.events ? JSON.parse(record.events) : [],
172
+ links: record.links ? JSON.parse(record.links) : [],
173
+ other: record.other ? JSON.parse(record.other) : {},
174
+ startTime: new Date(record.startTime),
175
+ endTime: new Date(record.endTime),
176
+ createdAt: new Date(record.createdAt),
177
+ };
178
+ if (processed.parentSpanId === null || processed.parentSpanId === undefined) {
179
+ processed.parentSpanId = '';
180
+ } else {
181
+ processed.parentSpanId = String(processed.parentSpanId);
182
+ }
183
+ return processed as Trace;
184
+ });
185
+ return {
186
+ traces,
187
+ total,
188
+ page,
189
+ perPage,
190
+ hasMore: total > (page + 1) * perPage,
191
+ };
192
+ } catch (error: any) {
193
+ throw new MastraError(
194
+ {
195
+ id: 'LANCE_STORE_GET_TRACES_PAGINATED_FAILED',
196
+ domain: ErrorDomain.STORAGE,
197
+ category: ErrorCategory.THIRD_PARTY,
198
+ details: { name: args.name ?? '', scope: args.scope ?? '' },
199
+ },
200
+ error,
201
+ );
202
+ }
203
+ }
204
+
205
+ async batchTraceInsert({ records }: { records: Record<string, any>[] }): Promise<void> {
206
+ this.logger.debug('Batch inserting traces', { count: records.length });
207
+ await this.operations.batchInsert({
208
+ tableName: TABLE_TRACES,
209
+ records,
210
+ });
211
+ }
212
+ }
@@ -0,0 +1,158 @@
1
+ import type { Connection, FieldLike, SchemaLike } from '@lancedb/lancedb';
2
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
+ import { TABLE_EVALS, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
4
+ import type { TABLE_NAMES } from '@mastra/core/storage';
5
+
6
+ export function getPrimaryKeys(tableName: TABLE_NAMES): string[] {
7
+ let primaryId: string[] = ['id'];
8
+ if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
9
+ primaryId = ['workflow_name', 'run_id'];
10
+ } else if (tableName === TABLE_EVALS) {
11
+ primaryId = ['agent_name', 'metric_name', 'run_id'];
12
+ }
13
+
14
+ return primaryId;
15
+ }
16
+
17
+ export function validateKeyTypes(keys: Record<string, any>, tableSchema: SchemaLike): void {
18
+ // Create a map of field names to their expected types
19
+ const fieldTypes = new Map(
20
+ tableSchema.fields.map((field: any) => [field.name, field.type?.toString().toLowerCase()]),
21
+ );
22
+
23
+ for (const [key, value] of Object.entries(keys)) {
24
+ const fieldType = fieldTypes.get(key);
25
+
26
+ if (!fieldType) {
27
+ throw new Error(`Field '${key}' does not exist in table schema`);
28
+ }
29
+
30
+ // Type validation
31
+ if (value !== null) {
32
+ if ((fieldType.includes('int') || fieldType.includes('bigint')) && typeof value !== 'number') {
33
+ throw new Error(`Expected numeric value for field '${key}', got ${typeof value}`);
34
+ }
35
+
36
+ if (fieldType.includes('utf8') && typeof value !== 'string') {
37
+ throw new Error(`Expected string value for field '${key}', got ${typeof value}`);
38
+ }
39
+
40
+ if (fieldType.includes('timestamp') && !(value instanceof Date) && typeof value !== 'string') {
41
+ throw new Error(`Expected Date or string value for field '${key}', got ${typeof value}`);
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ export function processResultWithTypeConversion(
48
+ rawResult: Record<string, any> | Record<string, any>[],
49
+ tableSchema: SchemaLike,
50
+ ): Record<string, any> | Record<string, any>[] {
51
+ // Build a map of field names to their schema types
52
+ const fieldTypeMap = new Map();
53
+ tableSchema.fields.forEach((field: any) => {
54
+ const fieldName = field.name;
55
+ const fieldTypeStr = field.type.toString().toLowerCase();
56
+ fieldTypeMap.set(fieldName, fieldTypeStr);
57
+ });
58
+
59
+ // Handle array case
60
+ if (Array.isArray(rawResult)) {
61
+ return rawResult.map(item => processResultWithTypeConversion(item, tableSchema));
62
+ }
63
+
64
+ // Handle single record case
65
+ const processedResult = { ...rawResult };
66
+
67
+ // Convert each field according to its schema type
68
+ for (const key in processedResult) {
69
+ const fieldTypeStr = fieldTypeMap.get(key);
70
+ if (!fieldTypeStr) continue;
71
+
72
+ // Skip conversion for ID fields - preserve their original format
73
+ // if (key === 'id') {
74
+ // continue;
75
+ // }
76
+
77
+ // Only try to convert string values
78
+ if (typeof processedResult[key] === 'string') {
79
+ // Numeric types
80
+ if (fieldTypeStr.includes('int32') || fieldTypeStr.includes('float32')) {
81
+ if (!isNaN(Number(processedResult[key]))) {
82
+ processedResult[key] = Number(processedResult[key]);
83
+ }
84
+ } else if (fieldTypeStr.includes('int64')) {
85
+ processedResult[key] = Number(processedResult[key]);
86
+ } else if (fieldTypeStr.includes('utf8') && key !== 'id') {
87
+ try {
88
+ const parsed = JSON.parse(processedResult[key]);
89
+ if (typeof parsed === 'object') {
90
+ processedResult[key] = JSON.parse(processedResult[key]);
91
+ }
92
+ } catch {}
93
+ }
94
+ } else if (typeof processedResult[key] === 'bigint') {
95
+ // Convert BigInt values to regular numbers for application layer
96
+ processedResult[key] = Number(processedResult[key]);
97
+ } else if (fieldTypeStr.includes('float64') && ['createdAt', 'updatedAt'].includes(key)) {
98
+ processedResult[key] = new Date(processedResult[key]);
99
+ }
100
+
101
+ console.log(key, 'processedResult', processedResult);
102
+ }
103
+
104
+ return processedResult;
105
+ }
106
+
107
+ export async function getTableSchema({
108
+ tableName,
109
+ client,
110
+ }: {
111
+ tableName: TABLE_NAMES;
112
+ client: Connection;
113
+ }): Promise<SchemaLike> {
114
+ try {
115
+ if (!client) {
116
+ throw new Error('LanceDB client not initialized. Call LanceStorage.create() first.');
117
+ }
118
+ if (!tableName) {
119
+ throw new Error('tableName is required for getTableSchema.');
120
+ }
121
+ } catch (validationError: any) {
122
+ throw new MastraError(
123
+ {
124
+ id: 'STORAGE_LANCE_STORAGE_GET_TABLE_SCHEMA_INVALID_ARGS',
125
+ domain: ErrorDomain.STORAGE,
126
+ category: ErrorCategory.USER,
127
+ text: validationError.message,
128
+ details: { tableName },
129
+ },
130
+ validationError,
131
+ );
132
+ }
133
+
134
+ try {
135
+ const table = await client.openTable(tableName);
136
+ const rawSchema = await table.schema();
137
+ const fields = rawSchema.fields as FieldLike[];
138
+
139
+ // Convert schema to SchemaLike format
140
+ return {
141
+ fields,
142
+ metadata: new Map<string, string>(),
143
+ get names() {
144
+ return fields.map((field: FieldLike) => field.name);
145
+ },
146
+ };
147
+ } catch (error: any) {
148
+ throw new MastraError(
149
+ {
150
+ id: 'STORAGE_LANCE_STORAGE_GET_TABLE_SCHEMA_FAILED',
151
+ domain: ErrorDomain.STORAGE,
152
+ category: ErrorCategory.THIRD_PARTY,
153
+ details: { tableName },
154
+ },
155
+ error,
156
+ );
157
+ }
158
+ }
@@ -0,0 +1,207 @@
1
+ import type { Connection } from '@lancedb/lancedb';
2
+ import type { WorkflowRun, WorkflowRunState, WorkflowRuns } from '@mastra/core';
3
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
4
+ import { ensureDate, TABLE_WORKFLOW_SNAPSHOT, WorkflowsStorage } from '@mastra/core/storage';
5
+
6
+ function parseWorkflowRun(row: any): WorkflowRun {
7
+ let parsedSnapshot: WorkflowRunState | string = row.snapshot;
8
+ if (typeof parsedSnapshot === 'string') {
9
+ try {
10
+ parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
11
+ } catch (e) {
12
+ // If parsing fails, return the raw snapshot string
13
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
14
+ }
15
+ }
16
+
17
+ return {
18
+ workflowName: row.workflow_name,
19
+ runId: row.run_id,
20
+ snapshot: parsedSnapshot,
21
+ createdAt: ensureDate(row.createdAt)!,
22
+ updatedAt: ensureDate(row.updatedAt)!,
23
+ resourceId: row.resourceId,
24
+ };
25
+ }
26
+
27
+ export class StoreWorkflowsLance extends WorkflowsStorage {
28
+ client: Connection;
29
+ constructor({ client }: { client: Connection }) {
30
+ super();
31
+ this.client = client;
32
+ }
33
+
34
+ async persistWorkflowSnapshot({
35
+ workflowName,
36
+ runId,
37
+ snapshot,
38
+ }: {
39
+ workflowName: string;
40
+ runId: string;
41
+ snapshot: WorkflowRunState;
42
+ }): Promise<void> {
43
+ try {
44
+ const table = await this.client.openTable(TABLE_WORKFLOW_SNAPSHOT);
45
+
46
+ // Try to find the existing record
47
+ const query = table.query().where(`workflow_name = '${workflowName}' AND run_id = '${runId}'`);
48
+ const records = await query.toArray();
49
+ let createdAt: number;
50
+ const now = Date.now();
51
+
52
+ if (records.length > 0) {
53
+ createdAt = records[0].createdAt ?? now;
54
+ } else {
55
+ createdAt = now;
56
+ }
57
+
58
+ const record = {
59
+ workflow_name: workflowName,
60
+ run_id: runId,
61
+ snapshot: JSON.stringify(snapshot),
62
+ createdAt,
63
+ updatedAt: now,
64
+ };
65
+
66
+ await table
67
+ .mergeInsert(['workflow_name', 'run_id'])
68
+ .whenMatchedUpdateAll()
69
+ .whenNotMatchedInsertAll()
70
+ .execute([record]);
71
+ } catch (error: any) {
72
+ throw new MastraError(
73
+ {
74
+ id: 'LANCE_STORE_PERSIST_WORKFLOW_SNAPSHOT_FAILED',
75
+ domain: ErrorDomain.STORAGE,
76
+ category: ErrorCategory.THIRD_PARTY,
77
+ details: { workflowName, runId },
78
+ },
79
+ error,
80
+ );
81
+ }
82
+ }
83
+ async loadWorkflowSnapshot({
84
+ workflowName,
85
+ runId,
86
+ }: {
87
+ workflowName: string;
88
+ runId: string;
89
+ }): Promise<WorkflowRunState | null> {
90
+ try {
91
+ const table = await this.client.openTable(TABLE_WORKFLOW_SNAPSHOT);
92
+ const query = table.query().where(`workflow_name = '${workflowName}' AND run_id = '${runId}'`);
93
+ const records = await query.toArray();
94
+ return records.length > 0 ? JSON.parse(records[0].snapshot) : null;
95
+ } catch (error: any) {
96
+ throw new MastraError(
97
+ {
98
+ id: 'LANCE_STORE_LOAD_WORKFLOW_SNAPSHOT_FAILED',
99
+ domain: ErrorDomain.STORAGE,
100
+ category: ErrorCategory.THIRD_PARTY,
101
+ details: { workflowName, runId },
102
+ },
103
+ error,
104
+ );
105
+ }
106
+ }
107
+
108
+ async getWorkflowRunById(args: { runId: string; workflowName?: string }): Promise<{
109
+ workflowName: string;
110
+ runId: string;
111
+ snapshot: any;
112
+ createdAt: Date;
113
+ updatedAt: Date;
114
+ } | null> {
115
+ try {
116
+ const table = await this.client.openTable(TABLE_WORKFLOW_SNAPSHOT);
117
+ let whereClause = `run_id = '${args.runId}'`;
118
+ if (args.workflowName) {
119
+ whereClause += ` AND workflow_name = '${args.workflowName}'`;
120
+ }
121
+ const query = table.query().where(whereClause);
122
+ const records = await query.toArray();
123
+ if (records.length === 0) return null;
124
+ const record = records[0];
125
+ return parseWorkflowRun(record);
126
+ } catch (error: any) {
127
+ throw new MastraError(
128
+ {
129
+ id: 'LANCE_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED',
130
+ domain: ErrorDomain.STORAGE,
131
+ category: ErrorCategory.THIRD_PARTY,
132
+ details: { runId: args.runId, workflowName: args.workflowName ?? '' },
133
+ },
134
+ error,
135
+ );
136
+ }
137
+ }
138
+
139
+ async getWorkflowRuns(args?: {
140
+ namespace?: string;
141
+ resourceId?: string;
142
+ workflowName?: string;
143
+ fromDate?: Date;
144
+ toDate?: Date;
145
+ limit?: number;
146
+ offset?: number;
147
+ }): Promise<WorkflowRuns> {
148
+ try {
149
+ const table = await this.client.openTable(TABLE_WORKFLOW_SNAPSHOT);
150
+
151
+ let query = table.query();
152
+
153
+ const conditions: string[] = [];
154
+
155
+ if (args?.workflowName) {
156
+ conditions.push(`workflow_name = '${args.workflowName.replace(/'/g, "''")}'`);
157
+ }
158
+
159
+ if (args?.resourceId) {
160
+ conditions.push(`\`resourceId\` = '${args.resourceId}'`);
161
+ }
162
+
163
+ if (args?.fromDate instanceof Date) {
164
+ conditions.push(`\`createdAt\` >= ${args.fromDate.getTime()}`);
165
+ }
166
+
167
+ if (args?.toDate instanceof Date) {
168
+ conditions.push(`\`createdAt\` <= ${args.toDate.getTime()}`);
169
+ }
170
+
171
+ let total = 0;
172
+
173
+ // Apply all conditions
174
+ if (conditions.length > 0) {
175
+ query = query.where(conditions.join(' AND '));
176
+ total = await table.countRows(conditions.join(' AND '));
177
+ } else {
178
+ total = await table.countRows();
179
+ }
180
+
181
+ if (args?.limit) {
182
+ query.limit(args.limit);
183
+ }
184
+
185
+ if (args?.offset) {
186
+ query.offset(args.offset);
187
+ }
188
+
189
+ const records = await query.toArray();
190
+
191
+ return {
192
+ runs: records.map(record => parseWorkflowRun(record)),
193
+ total: total || records.length,
194
+ };
195
+ } catch (error: any) {
196
+ throw new MastraError(
197
+ {
198
+ id: 'LANCE_STORE_GET_WORKFLOW_RUNS_FAILED',
199
+ domain: ErrorDomain.STORAGE,
200
+ category: ErrorCategory.THIRD_PARTY,
201
+ details: { namespace: args?.namespace ?? '', workflowName: args?.workflowName ?? '' },
202
+ },
203
+ error,
204
+ );
205
+ }
206
+ }
207
+ }