@mastra/upstash 0.12.1 → 0.12.2
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +53 -0
- package/dist/_tsup-dts-rollup.d.cts +342 -40
- package/dist/_tsup-dts-rollup.d.ts +342 -40
- package/dist/index.cjs +1133 -612
- package/dist/index.js +1134 -613
- package/docker-compose.yaml +1 -1
- package/package.json +5 -5
- package/src/storage/domains/legacy-evals/index.ts +279 -0
- package/src/storage/domains/memory/index.ts +902 -0
- package/src/storage/domains/operations/index.ts +168 -0
- package/src/storage/domains/scores/index.ts +216 -0
- package/src/storage/domains/traces/index.ts +172 -0
- package/src/storage/domains/utils.ts +57 -0
- package/src/storage/domains/workflows/index.ts +243 -0
- package/src/storage/index.test.ts +13 -0
- package/src/storage/index.ts +143 -1416
- package/src/storage/upstash.test.ts +0 -1461
package/src/storage/index.ts
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
import { MessageList } from '@mastra/core/agent';
|
|
2
1
|
import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
|
|
3
|
-
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
4
|
-
import type { MetricResult, TestInfo } from '@mastra/core/eval';
|
|
5
2
|
import type { StorageThreadType, MastraMessageV1 } from '@mastra/core/memory';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
TABLE_MESSAGES,
|
|
9
|
-
TABLE_THREADS,
|
|
10
|
-
TABLE_RESOURCES,
|
|
11
|
-
TABLE_WORKFLOW_SNAPSHOT,
|
|
12
|
-
TABLE_EVALS,
|
|
13
|
-
TABLE_TRACES,
|
|
14
|
-
} from '@mastra/core/storage';
|
|
3
|
+
import type { ScoreRowData } from '@mastra/core/scores';
|
|
4
|
+
import { MastraStorage } from '@mastra/core/storage';
|
|
15
5
|
import type {
|
|
16
6
|
TABLE_NAMES,
|
|
17
7
|
StorageColumn,
|
|
@@ -23,9 +13,18 @@ import type {
|
|
|
23
13
|
PaginationInfo,
|
|
24
14
|
PaginationArgs,
|
|
25
15
|
StorageGetTracesArg,
|
|
16
|
+
StoragePagination,
|
|
17
|
+
StorageDomains,
|
|
26
18
|
} from '@mastra/core/storage';
|
|
19
|
+
|
|
27
20
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
28
21
|
import { Redis } from '@upstash/redis';
|
|
22
|
+
import { StoreLegacyEvalsUpstash } from './domains/legacy-evals';
|
|
23
|
+
import { StoreMemoryUpstash } from './domains/memory';
|
|
24
|
+
import { StoreOperationsUpstash } from './domains/operations';
|
|
25
|
+
import { ScoresUpstash } from './domains/scores';
|
|
26
|
+
import { TracesUpstash } from './domains/traces';
|
|
27
|
+
import { WorkflowsUpstash } from './domains/workflows';
|
|
29
28
|
|
|
30
29
|
export interface UpstashConfig {
|
|
31
30
|
url: string;
|
|
@@ -34,6 +33,7 @@ export interface UpstashConfig {
|
|
|
34
33
|
|
|
35
34
|
export class UpstashStore extends MastraStorage {
|
|
36
35
|
private redis: Redis;
|
|
36
|
+
stores: StorageDomains;
|
|
37
37
|
|
|
38
38
|
constructor(config: UpstashConfig) {
|
|
39
39
|
super({ name: 'Upstash' });
|
|
@@ -41,279 +41,59 @@ export class UpstashStore extends MastraStorage {
|
|
|
41
41
|
url: config.url,
|
|
42
42
|
token: config.token,
|
|
43
43
|
});
|
|
44
|
-
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
const operations = new StoreOperationsUpstash({ client: this.redis });
|
|
46
|
+
const traces = new TracesUpstash({ client: this.redis, operations });
|
|
47
|
+
const scores = new ScoresUpstash({ client: this.redis, operations });
|
|
48
|
+
const workflows = new WorkflowsUpstash({ client: this.redis, operations });
|
|
49
|
+
const memory = new StoreMemoryUpstash({ client: this.redis, operations });
|
|
50
|
+
const legacyEvals = new StoreLegacyEvalsUpstash({ client: this.redis, operations });
|
|
51
|
+
|
|
52
|
+
this.stores = {
|
|
53
|
+
operations,
|
|
54
|
+
traces,
|
|
55
|
+
scores,
|
|
56
|
+
workflows,
|
|
57
|
+
memory,
|
|
58
|
+
legacyEvals,
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
// Parse JSON strings if needed
|
|
58
|
-
let result = record.result;
|
|
59
|
-
if (typeof result === 'string') {
|
|
60
|
-
try {
|
|
61
|
-
result = JSON.parse(result);
|
|
62
|
-
} catch {
|
|
63
|
-
console.warn('Failed to parse result JSON:');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let testInfo = record.test_info;
|
|
68
|
-
if (typeof testInfo === 'string') {
|
|
69
|
-
try {
|
|
70
|
-
testInfo = JSON.parse(testInfo);
|
|
71
|
-
} catch {
|
|
72
|
-
console.warn('Failed to parse test_info JSON:');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
62
|
+
public get supports() {
|
|
76
63
|
return {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
metricName: record.metric_name,
|
|
82
|
-
instructions: record.instructions,
|
|
83
|
-
testInfo: testInfo as TestInfo | undefined,
|
|
84
|
-
globalRunId: record.global_run_id,
|
|
85
|
-
runId: record.run_id,
|
|
86
|
-
createdAt:
|
|
87
|
-
typeof record.created_at === 'string'
|
|
88
|
-
? record.created_at
|
|
89
|
-
: record.created_at instanceof Date
|
|
90
|
-
? record.created_at.toISOString()
|
|
91
|
-
: new Date().toISOString(),
|
|
64
|
+
selectByIncludeResourceScope: true,
|
|
65
|
+
resourceWorkingMemory: true,
|
|
66
|
+
hasColumn: false,
|
|
67
|
+
createTable: false,
|
|
92
68
|
};
|
|
93
69
|
}
|
|
94
70
|
|
|
95
|
-
private parseJSON(value: any): any {
|
|
96
|
-
if (typeof value === 'string') {
|
|
97
|
-
try {
|
|
98
|
-
return JSON.parse(value);
|
|
99
|
-
} catch {
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return value;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private getKey(tableName: TABLE_NAMES, keys: Record<string, any>): string {
|
|
107
|
-
const keyParts = Object.entries(keys)
|
|
108
|
-
.filter(([_, value]) => value !== undefined)
|
|
109
|
-
.map(([key, value]) => `${key}:${value}`);
|
|
110
|
-
return `${tableName}:${keyParts.join(':')}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Scans for keys matching the given pattern using SCAN and returns them as an array.
|
|
115
|
-
* @param pattern Redis key pattern, e.g. "table:*"
|
|
116
|
-
* @param batchSize Number of keys to scan per batch (default: 1000)
|
|
117
|
-
*/
|
|
118
|
-
private async scanKeys(pattern: string, batchSize = 10000): Promise<string[]> {
|
|
119
|
-
let cursor = '0';
|
|
120
|
-
let keys: string[] = [];
|
|
121
|
-
do {
|
|
122
|
-
// Upstash: scan(cursor, { match, count })
|
|
123
|
-
const [nextCursor, batch] = await this.redis.scan(cursor, {
|
|
124
|
-
match: pattern,
|
|
125
|
-
count: batchSize,
|
|
126
|
-
});
|
|
127
|
-
keys.push(...batch);
|
|
128
|
-
cursor = nextCursor;
|
|
129
|
-
} while (cursor !== '0');
|
|
130
|
-
return keys;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
71
|
/**
|
|
134
|
-
*
|
|
135
|
-
* @param pattern Redis key pattern, e.g. "table:*"
|
|
136
|
-
* @param batchSize Number of keys to delete per batch (default: 1000)
|
|
72
|
+
* @deprecated Use getEvals instead
|
|
137
73
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let totalDeleted = 0;
|
|
141
|
-
do {
|
|
142
|
-
const [nextCursor, keys] = await this.redis.scan(cursor, {
|
|
143
|
-
match: pattern,
|
|
144
|
-
count: batchSize,
|
|
145
|
-
});
|
|
146
|
-
if (keys.length > 0) {
|
|
147
|
-
await this.redis.del(...keys);
|
|
148
|
-
totalDeleted += keys.length;
|
|
149
|
-
}
|
|
150
|
-
cursor = nextCursor;
|
|
151
|
-
} while (cursor !== '0');
|
|
152
|
-
return totalDeleted;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private getMessageKey(threadId: string, messageId: string): string {
|
|
156
|
-
const key = this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
|
|
157
|
-
return key;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private getThreadMessagesKey(threadId: string): string {
|
|
161
|
-
return `thread:${threadId}:messages`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private parseWorkflowRun(row: any): WorkflowRun {
|
|
165
|
-
let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
|
|
166
|
-
if (typeof parsedSnapshot === 'string') {
|
|
167
|
-
try {
|
|
168
|
-
parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
|
|
169
|
-
} catch (e) {
|
|
170
|
-
// If parsing fails, return the raw snapshot string
|
|
171
|
-
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
workflowName: row.workflow_name,
|
|
177
|
-
runId: row.run_id,
|
|
178
|
-
snapshot: parsedSnapshot,
|
|
179
|
-
createdAt: this.ensureDate(row.createdAt)!,
|
|
180
|
-
updatedAt: this.ensureDate(row.updatedAt)!,
|
|
181
|
-
resourceId: row.resourceId,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private processRecord(tableName: TABLE_NAMES, record: Record<string, any>) {
|
|
186
|
-
let key: string;
|
|
187
|
-
|
|
188
|
-
if (tableName === TABLE_MESSAGES) {
|
|
189
|
-
// For messages, use threadId as the primary key component
|
|
190
|
-
key = this.getKey(tableName, { threadId: record.threadId, id: record.id });
|
|
191
|
-
} else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
192
|
-
key = this.getKey(tableName, {
|
|
193
|
-
namespace: record.namespace || 'workflows',
|
|
194
|
-
workflow_name: record.workflow_name,
|
|
195
|
-
run_id: record.run_id,
|
|
196
|
-
...(record.resourceId ? { resourceId: record.resourceId } : {}),
|
|
197
|
-
});
|
|
198
|
-
} else if (tableName === TABLE_EVALS) {
|
|
199
|
-
key = this.getKey(tableName, { id: record.run_id });
|
|
200
|
-
} else {
|
|
201
|
-
key = this.getKey(tableName, { id: record.id });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Convert dates to ISO strings before storing
|
|
205
|
-
const processedRecord = {
|
|
206
|
-
...record,
|
|
207
|
-
createdAt: this.serializeDate(record.createdAt),
|
|
208
|
-
updatedAt: this.serializeDate(record.updatedAt),
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
return { key, processedRecord };
|
|
74
|
+
async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
|
|
75
|
+
return this.stores.legacyEvals.getEvalsByAgentName(agentName, type);
|
|
212
76
|
}
|
|
213
77
|
|
|
214
78
|
/**
|
|
215
|
-
*
|
|
79
|
+
* Get all evaluations with pagination and total count
|
|
80
|
+
* @param options Pagination and filtering options
|
|
81
|
+
* @returns Object with evals array and total count
|
|
216
82
|
*/
|
|
217
|
-
async
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return [];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Use pipeline for batch fetching to improve performance
|
|
228
|
-
const pipeline = this.redis.pipeline();
|
|
229
|
-
keys.forEach(key => pipeline.get(key));
|
|
230
|
-
const results = await pipeline.exec();
|
|
231
|
-
|
|
232
|
-
// Filter by agent name and remove nulls
|
|
233
|
-
const nonNullRecords = results.filter(
|
|
234
|
-
(record): record is Record<string, any> =>
|
|
235
|
-
record !== null && typeof record === 'object' && 'agent_name' in record && record.agent_name === agentName,
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
let filteredEvals = nonNullRecords;
|
|
239
|
-
|
|
240
|
-
if (type === 'test') {
|
|
241
|
-
filteredEvals = filteredEvals.filter(record => {
|
|
242
|
-
if (!record.test_info) return false;
|
|
243
|
-
|
|
244
|
-
// Handle test_info as a JSON string
|
|
245
|
-
try {
|
|
246
|
-
if (typeof record.test_info === 'string') {
|
|
247
|
-
const parsedTestInfo = JSON.parse(record.test_info);
|
|
248
|
-
return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Handle test_info as an object
|
|
252
|
-
return typeof record.test_info === 'object' && 'testPath' in record.test_info;
|
|
253
|
-
} catch {
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
} else if (type === 'live') {
|
|
258
|
-
filteredEvals = filteredEvals.filter(record => {
|
|
259
|
-
if (!record.test_info) return true;
|
|
260
|
-
|
|
261
|
-
// Handle test_info as a JSON string
|
|
262
|
-
try {
|
|
263
|
-
if (typeof record.test_info === 'string') {
|
|
264
|
-
const parsedTestInfo = JSON.parse(record.test_info);
|
|
265
|
-
return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Handle test_info as an object
|
|
269
|
-
return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
|
|
270
|
-
} catch {
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Transform to EvalRow format
|
|
277
|
-
return filteredEvals.map(record => this.transformEvalRecord(record));
|
|
278
|
-
} catch (error) {
|
|
279
|
-
const mastraError = new MastraError(
|
|
280
|
-
{
|
|
281
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_BY_AGENT_NAME_FAILED',
|
|
282
|
-
domain: ErrorDomain.STORAGE,
|
|
283
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
284
|
-
details: { agentName },
|
|
285
|
-
},
|
|
286
|
-
error,
|
|
287
|
-
);
|
|
288
|
-
this.logger?.trackException(mastraError);
|
|
289
|
-
this.logger.error(mastraError.toString());
|
|
290
|
-
return [];
|
|
291
|
-
}
|
|
83
|
+
async getEvals(
|
|
84
|
+
options: {
|
|
85
|
+
agentName?: string;
|
|
86
|
+
type?: 'test' | 'live';
|
|
87
|
+
} & PaginationArgs,
|
|
88
|
+
): Promise<PaginationInfo & { evals: EvalRow[] }> {
|
|
89
|
+
return this.stores.legacyEvals.getEvals(options);
|
|
292
90
|
}
|
|
293
91
|
|
|
294
92
|
/**
|
|
295
93
|
* @deprecated use getTracesPaginated instead
|
|
296
94
|
*/
|
|
297
95
|
public async getTraces(args: StorageGetTracesArg): Promise<any[]> {
|
|
298
|
-
|
|
299
|
-
(args as any).dateRange = {
|
|
300
|
-
start: args.fromDate,
|
|
301
|
-
end: args.toDate,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
try {
|
|
305
|
-
const { traces } = await this.getTracesPaginated(args);
|
|
306
|
-
return traces;
|
|
307
|
-
} catch (error) {
|
|
308
|
-
throw new MastraError(
|
|
309
|
-
{
|
|
310
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_FAILED',
|
|
311
|
-
domain: ErrorDomain.STORAGE,
|
|
312
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
313
|
-
},
|
|
314
|
-
error,
|
|
315
|
-
);
|
|
316
|
-
}
|
|
96
|
+
return this.stores.traces.getTraces(args);
|
|
317
97
|
}
|
|
318
98
|
|
|
319
99
|
public async getTracesPaginated(
|
|
@@ -324,119 +104,11 @@ export class UpstashStore extends MastraStorage {
|
|
|
324
104
|
filters?: Record<string, any>;
|
|
325
105
|
} & PaginationArgs,
|
|
326
106
|
): Promise<PaginationInfo & { traces: any[] }> {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const toDate = dateRange?.end;
|
|
330
|
-
|
|
331
|
-
try {
|
|
332
|
-
const pattern = `${TABLE_TRACES}:*`;
|
|
333
|
-
const keys = await this.scanKeys(pattern);
|
|
334
|
-
|
|
335
|
-
if (keys.length === 0) {
|
|
336
|
-
return {
|
|
337
|
-
traces: [],
|
|
338
|
-
total: 0,
|
|
339
|
-
page,
|
|
340
|
-
perPage: perPage || 100,
|
|
341
|
-
hasMore: false,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const pipeline = this.redis.pipeline();
|
|
346
|
-
keys.forEach(key => pipeline.get(key));
|
|
347
|
-
const results = await pipeline.exec();
|
|
348
|
-
|
|
349
|
-
let filteredTraces = results.filter(
|
|
350
|
-
(record): record is Record<string, any> => record !== null && typeof record === 'object',
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
if (name) {
|
|
354
|
-
filteredTraces = filteredTraces.filter(record => record.name?.toLowerCase().startsWith(name.toLowerCase()));
|
|
355
|
-
}
|
|
356
|
-
if (scope) {
|
|
357
|
-
filteredTraces = filteredTraces.filter(record => record.scope === scope);
|
|
358
|
-
}
|
|
359
|
-
if (attributes) {
|
|
360
|
-
filteredTraces = filteredTraces.filter(record => {
|
|
361
|
-
const recordAttributes = record.attributes;
|
|
362
|
-
if (!recordAttributes) return false;
|
|
363
|
-
const parsedAttributes =
|
|
364
|
-
typeof recordAttributes === 'string' ? JSON.parse(recordAttributes) : recordAttributes;
|
|
365
|
-
return Object.entries(attributes).every(([key, value]) => parsedAttributes[key] === value);
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
if (filters) {
|
|
369
|
-
filteredTraces = filteredTraces.filter(record =>
|
|
370
|
-
Object.entries(filters).every(([key, value]) => record[key] === value),
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
if (fromDate) {
|
|
374
|
-
filteredTraces = filteredTraces.filter(
|
|
375
|
-
record => new Date(record.createdAt).getTime() >= new Date(fromDate).getTime(),
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
if (toDate) {
|
|
379
|
-
filteredTraces = filteredTraces.filter(
|
|
380
|
-
record => new Date(record.createdAt).getTime() <= new Date(toDate).getTime(),
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
filteredTraces.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
385
|
-
|
|
386
|
-
const transformedTraces = filteredTraces.map(record => ({
|
|
387
|
-
id: record.id,
|
|
388
|
-
parentSpanId: record.parentSpanId,
|
|
389
|
-
traceId: record.traceId,
|
|
390
|
-
name: record.name,
|
|
391
|
-
scope: record.scope,
|
|
392
|
-
kind: record.kind,
|
|
393
|
-
status: this.parseJSON(record.status),
|
|
394
|
-
events: this.parseJSON(record.events),
|
|
395
|
-
links: this.parseJSON(record.links),
|
|
396
|
-
attributes: this.parseJSON(record.attributes),
|
|
397
|
-
startTime: record.startTime,
|
|
398
|
-
endTime: record.endTime,
|
|
399
|
-
other: this.parseJSON(record.other),
|
|
400
|
-
createdAt: this.ensureDate(record.createdAt),
|
|
401
|
-
}));
|
|
402
|
-
|
|
403
|
-
const total = transformedTraces.length;
|
|
404
|
-
const resolvedPerPage = perPage || 100;
|
|
405
|
-
const start = page * resolvedPerPage;
|
|
406
|
-
const end = start + resolvedPerPage;
|
|
407
|
-
const paginatedTraces = transformedTraces.slice(start, end);
|
|
408
|
-
const hasMore = end < total;
|
|
107
|
+
return this.stores.traces.getTracesPaginated(args);
|
|
108
|
+
}
|
|
409
109
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
total,
|
|
413
|
-
page,
|
|
414
|
-
perPage: resolvedPerPage,
|
|
415
|
-
hasMore,
|
|
416
|
-
};
|
|
417
|
-
} catch (error) {
|
|
418
|
-
const mastraError = new MastraError(
|
|
419
|
-
{
|
|
420
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_TRACES_PAGINATED_FAILED',
|
|
421
|
-
domain: ErrorDomain.STORAGE,
|
|
422
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
423
|
-
details: {
|
|
424
|
-
name: args.name || '',
|
|
425
|
-
scope: args.scope || '',
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
error,
|
|
429
|
-
);
|
|
430
|
-
this.logger?.trackException(mastraError);
|
|
431
|
-
this.logger.error(mastraError.toString());
|
|
432
|
-
return {
|
|
433
|
-
traces: [],
|
|
434
|
-
total: 0,
|
|
435
|
-
page,
|
|
436
|
-
perPage: perPage || 100,
|
|
437
|
-
hasMore: false,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
110
|
+
async batchTraceInsert(args: { records: Record<string, any>[] }): Promise<void> {
|
|
111
|
+
return this.stores.traces.batchTraceInsert(args);
|
|
440
112
|
}
|
|
441
113
|
|
|
442
114
|
async createTable({
|
|
@@ -446,23 +118,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
446
118
|
tableName: TABLE_NAMES;
|
|
447
119
|
schema: Record<string, StorageColumn>;
|
|
448
120
|
}): Promise<void> {
|
|
449
|
-
|
|
450
|
-
// But we can store the schema for reference
|
|
451
|
-
try {
|
|
452
|
-
await this.redis.set(`schema:${tableName}`, schema);
|
|
453
|
-
} catch (error) {
|
|
454
|
-
throw new MastraError(
|
|
455
|
-
{
|
|
456
|
-
id: 'STORAGE_UPSTASH_STORAGE_CREATE_TABLE_FAILED',
|
|
457
|
-
domain: ErrorDomain.STORAGE,
|
|
458
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
459
|
-
details: {
|
|
460
|
-
tableName,
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
error,
|
|
464
|
-
);
|
|
465
|
-
}
|
|
121
|
+
return this.stores.operations.createTable({ tableName, schema });
|
|
466
122
|
}
|
|
467
123
|
|
|
468
124
|
/**
|
|
@@ -471,254 +127,55 @@ export class UpstashStore extends MastraStorage {
|
|
|
471
127
|
* @param schema Schema of the table
|
|
472
128
|
* @param ifNotExists Array of column names to add if they don't exist
|
|
473
129
|
*/
|
|
474
|
-
async alterTable(
|
|
130
|
+
async alterTable(args: {
|
|
475
131
|
tableName: TABLE_NAMES;
|
|
476
132
|
schema: Record<string, StorageColumn>;
|
|
477
133
|
ifNotExists: string[];
|
|
478
134
|
}): Promise<void> {
|
|
479
|
-
|
|
135
|
+
return this.stores.operations.alterTable(args);
|
|
480
136
|
}
|
|
481
137
|
|
|
482
138
|
async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
await this.scanAndDelete(pattern);
|
|
486
|
-
} catch (error) {
|
|
487
|
-
throw new MastraError(
|
|
488
|
-
{
|
|
489
|
-
id: 'STORAGE_UPSTASH_STORAGE_CLEAR_TABLE_FAILED',
|
|
490
|
-
domain: ErrorDomain.STORAGE,
|
|
491
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
492
|
-
details: {
|
|
493
|
-
tableName,
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
error,
|
|
497
|
-
);
|
|
498
|
-
}
|
|
139
|
+
return this.stores.operations.clearTable({ tableName });
|
|
499
140
|
}
|
|
500
141
|
|
|
501
|
-
async
|
|
502
|
-
|
|
142
|
+
async dropTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
143
|
+
return this.stores.operations.dropTable({ tableName });
|
|
144
|
+
}
|
|
503
145
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
} catch (error) {
|
|
507
|
-
throw new MastraError(
|
|
508
|
-
{
|
|
509
|
-
id: 'STORAGE_UPSTASH_STORAGE_INSERT_FAILED',
|
|
510
|
-
domain: ErrorDomain.STORAGE,
|
|
511
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
512
|
-
details: {
|
|
513
|
-
tableName,
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
error,
|
|
517
|
-
);
|
|
518
|
-
}
|
|
146
|
+
async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
|
|
147
|
+
return this.stores.operations.insert({ tableName, record });
|
|
519
148
|
}
|
|
520
149
|
|
|
521
150
|
async batchInsert(input: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
|
|
522
|
-
|
|
523
|
-
if (!records.length) return;
|
|
524
|
-
|
|
525
|
-
const batchSize = 1000;
|
|
526
|
-
try {
|
|
527
|
-
for (let i = 0; i < records.length; i += batchSize) {
|
|
528
|
-
const batch = records.slice(i, i + batchSize);
|
|
529
|
-
const pipeline = this.redis.pipeline();
|
|
530
|
-
for (const record of batch) {
|
|
531
|
-
const { key, processedRecord } = this.processRecord(tableName, record);
|
|
532
|
-
pipeline.set(key, processedRecord);
|
|
533
|
-
}
|
|
534
|
-
await pipeline.exec();
|
|
535
|
-
}
|
|
536
|
-
} catch (error) {
|
|
537
|
-
throw new MastraError(
|
|
538
|
-
{
|
|
539
|
-
id: 'STORAGE_UPSTASH_STORAGE_BATCH_INSERT_FAILED',
|
|
540
|
-
domain: ErrorDomain.STORAGE,
|
|
541
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
542
|
-
details: {
|
|
543
|
-
tableName,
|
|
544
|
-
},
|
|
545
|
-
},
|
|
546
|
-
error,
|
|
547
|
-
);
|
|
548
|
-
}
|
|
151
|
+
return this.stores.operations.batchInsert(input);
|
|
549
152
|
}
|
|
550
153
|
|
|
551
154
|
async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
const data = await this.redis.get<R>(key);
|
|
555
|
-
return data || null;
|
|
556
|
-
} catch (error) {
|
|
557
|
-
throw new MastraError(
|
|
558
|
-
{
|
|
559
|
-
id: 'STORAGE_UPSTASH_STORAGE_LOAD_FAILED',
|
|
560
|
-
domain: ErrorDomain.STORAGE,
|
|
561
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
562
|
-
details: {
|
|
563
|
-
tableName,
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
error,
|
|
567
|
-
);
|
|
568
|
-
}
|
|
155
|
+
return this.stores.operations.load<R>({ tableName, keys });
|
|
569
156
|
}
|
|
570
157
|
|
|
571
158
|
async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
|
|
572
|
-
|
|
573
|
-
const thread = await this.load<StorageThreadType>({
|
|
574
|
-
tableName: TABLE_THREADS,
|
|
575
|
-
keys: { id: threadId },
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
if (!thread) return null;
|
|
579
|
-
|
|
580
|
-
return {
|
|
581
|
-
...thread,
|
|
582
|
-
createdAt: this.ensureDate(thread.createdAt)!,
|
|
583
|
-
updatedAt: this.ensureDate(thread.updatedAt)!,
|
|
584
|
-
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
|
|
585
|
-
};
|
|
586
|
-
} catch (error) {
|
|
587
|
-
throw new MastraError(
|
|
588
|
-
{
|
|
589
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_THREAD_BY_ID_FAILED',
|
|
590
|
-
domain: ErrorDomain.STORAGE,
|
|
591
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
592
|
-
details: {
|
|
593
|
-
threadId,
|
|
594
|
-
},
|
|
595
|
-
},
|
|
596
|
-
error,
|
|
597
|
-
);
|
|
598
|
-
}
|
|
159
|
+
return this.stores.memory.getThreadById({ threadId });
|
|
599
160
|
}
|
|
600
161
|
|
|
601
162
|
/**
|
|
602
163
|
* @deprecated use getThreadsByResourceIdPaginated instead
|
|
603
164
|
*/
|
|
604
165
|
async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
|
|
605
|
-
|
|
606
|
-
const pattern = `${TABLE_THREADS}:*`;
|
|
607
|
-
const keys = await this.scanKeys(pattern);
|
|
608
|
-
|
|
609
|
-
if (keys.length === 0) {
|
|
610
|
-
return [];
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const allThreads: StorageThreadType[] = [];
|
|
614
|
-
const pipeline = this.redis.pipeline();
|
|
615
|
-
keys.forEach(key => pipeline.get(key));
|
|
616
|
-
const results = await pipeline.exec();
|
|
617
|
-
|
|
618
|
-
for (let i = 0; i < results.length; i++) {
|
|
619
|
-
const thread = results[i] as StorageThreadType | null;
|
|
620
|
-
if (thread && thread.resourceId === resourceId) {
|
|
621
|
-
allThreads.push({
|
|
622
|
-
...thread,
|
|
623
|
-
createdAt: this.ensureDate(thread.createdAt)!,
|
|
624
|
-
updatedAt: this.ensureDate(thread.updatedAt)!,
|
|
625
|
-
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
allThreads.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
631
|
-
return allThreads;
|
|
632
|
-
} catch (error) {
|
|
633
|
-
const mastraError = new MastraError(
|
|
634
|
-
{
|
|
635
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_THREADS_BY_RESOURCE_ID_FAILED',
|
|
636
|
-
domain: ErrorDomain.STORAGE,
|
|
637
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
638
|
-
details: {
|
|
639
|
-
resourceId,
|
|
640
|
-
},
|
|
641
|
-
},
|
|
642
|
-
error,
|
|
643
|
-
);
|
|
644
|
-
this.logger?.trackException(mastraError);
|
|
645
|
-
this.logger.error(mastraError.toString());
|
|
646
|
-
return [];
|
|
647
|
-
}
|
|
166
|
+
return this.stores.memory.getThreadsByResourceId({ resourceId });
|
|
648
167
|
}
|
|
649
168
|
|
|
650
|
-
public async getThreadsByResourceIdPaginated(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
try {
|
|
658
|
-
const allThreads = await this.getThreadsByResourceId({ resourceId });
|
|
659
|
-
|
|
660
|
-
const total = allThreads.length;
|
|
661
|
-
const start = page * perPage;
|
|
662
|
-
const end = start + perPage;
|
|
663
|
-
const paginatedThreads = allThreads.slice(start, end);
|
|
664
|
-
const hasMore = end < total;
|
|
665
|
-
|
|
666
|
-
return {
|
|
667
|
-
threads: paginatedThreads,
|
|
668
|
-
total,
|
|
669
|
-
page,
|
|
670
|
-
perPage,
|
|
671
|
-
hasMore,
|
|
672
|
-
};
|
|
673
|
-
} catch (error) {
|
|
674
|
-
const mastraError = new MastraError(
|
|
675
|
-
{
|
|
676
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
|
|
677
|
-
domain: ErrorDomain.STORAGE,
|
|
678
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
679
|
-
details: {
|
|
680
|
-
resourceId,
|
|
681
|
-
page,
|
|
682
|
-
perPage,
|
|
683
|
-
},
|
|
684
|
-
},
|
|
685
|
-
error,
|
|
686
|
-
);
|
|
687
|
-
this.logger?.trackException(mastraError);
|
|
688
|
-
this.logger.error(mastraError.toString());
|
|
689
|
-
return {
|
|
690
|
-
threads: [],
|
|
691
|
-
total: 0,
|
|
692
|
-
page,
|
|
693
|
-
perPage,
|
|
694
|
-
hasMore: false,
|
|
695
|
-
};
|
|
696
|
-
}
|
|
169
|
+
public async getThreadsByResourceIdPaginated(args: {
|
|
170
|
+
resourceId: string;
|
|
171
|
+
page: number;
|
|
172
|
+
perPage: number;
|
|
173
|
+
}): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
|
|
174
|
+
return this.stores.memory.getThreadsByResourceIdPaginated(args);
|
|
697
175
|
}
|
|
698
176
|
|
|
699
177
|
async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
|
|
700
|
-
|
|
701
|
-
await this.insert({
|
|
702
|
-
tableName: TABLE_THREADS,
|
|
703
|
-
record: thread,
|
|
704
|
-
});
|
|
705
|
-
return thread;
|
|
706
|
-
} catch (error) {
|
|
707
|
-
const mastraError = new MastraError(
|
|
708
|
-
{
|
|
709
|
-
id: 'STORAGE_UPSTASH_STORAGE_SAVE_THREAD_FAILED',
|
|
710
|
-
domain: ErrorDomain.STORAGE,
|
|
711
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
712
|
-
details: {
|
|
713
|
-
threadId: thread.id,
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
error,
|
|
717
|
-
);
|
|
718
|
-
this.logger?.trackException(mastraError);
|
|
719
|
-
this.logger.error(mastraError.toString());
|
|
720
|
-
throw mastraError;
|
|
721
|
-
}
|
|
178
|
+
return this.stores.memory.saveThread({ thread });
|
|
722
179
|
}
|
|
723
180
|
|
|
724
181
|
async updateThread({
|
|
@@ -730,80 +187,11 @@ export class UpstashStore extends MastraStorage {
|
|
|
730
187
|
title: string;
|
|
731
188
|
metadata: Record<string, unknown>;
|
|
732
189
|
}): Promise<StorageThreadType> {
|
|
733
|
-
|
|
734
|
-
if (!thread) {
|
|
735
|
-
throw new MastraError({
|
|
736
|
-
id: 'STORAGE_UPSTASH_STORAGE_UPDATE_THREAD_FAILED',
|
|
737
|
-
domain: ErrorDomain.STORAGE,
|
|
738
|
-
category: ErrorCategory.USER,
|
|
739
|
-
text: `Thread ${id} not found`,
|
|
740
|
-
details: {
|
|
741
|
-
threadId: id,
|
|
742
|
-
},
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const updatedThread = {
|
|
747
|
-
...thread,
|
|
748
|
-
title,
|
|
749
|
-
metadata: {
|
|
750
|
-
...thread.metadata,
|
|
751
|
-
...metadata,
|
|
752
|
-
},
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
try {
|
|
756
|
-
await this.saveThread({ thread: updatedThread });
|
|
757
|
-
return updatedThread;
|
|
758
|
-
} catch (error) {
|
|
759
|
-
throw new MastraError(
|
|
760
|
-
{
|
|
761
|
-
id: 'STORAGE_UPSTASH_STORAGE_UPDATE_THREAD_FAILED',
|
|
762
|
-
domain: ErrorDomain.STORAGE,
|
|
763
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
764
|
-
details: {
|
|
765
|
-
threadId: id,
|
|
766
|
-
},
|
|
767
|
-
},
|
|
768
|
-
error,
|
|
769
|
-
);
|
|
770
|
-
}
|
|
190
|
+
return this.stores.memory.updateThread({ id, title, metadata });
|
|
771
191
|
}
|
|
772
192
|
|
|
773
193
|
async deleteThread({ threadId }: { threadId: string }): Promise<void> {
|
|
774
|
-
|
|
775
|
-
const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
|
|
776
|
-
const threadMessagesKey = this.getThreadMessagesKey(threadId);
|
|
777
|
-
try {
|
|
778
|
-
const messageIds: string[] = await this.redis.zrange(threadMessagesKey, 0, -1);
|
|
779
|
-
|
|
780
|
-
const pipeline = this.redis.pipeline();
|
|
781
|
-
pipeline.del(threadKey);
|
|
782
|
-
pipeline.del(threadMessagesKey);
|
|
783
|
-
|
|
784
|
-
for (let i = 0; i < messageIds.length; i++) {
|
|
785
|
-
const messageId = messageIds[i];
|
|
786
|
-
const messageKey = this.getMessageKey(threadId, messageId as string);
|
|
787
|
-
pipeline.del(messageKey);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
await pipeline.exec();
|
|
791
|
-
|
|
792
|
-
// Bulk delete all message keys for this thread if any remain
|
|
793
|
-
await this.scanAndDelete(this.getMessageKey(threadId, '*'));
|
|
794
|
-
} catch (error) {
|
|
795
|
-
throw new MastraError(
|
|
796
|
-
{
|
|
797
|
-
id: 'STORAGE_UPSTASH_STORAGE_DELETE_THREAD_FAILED',
|
|
798
|
-
domain: ErrorDomain.STORAGE,
|
|
799
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
800
|
-
details: {
|
|
801
|
-
threadId,
|
|
802
|
-
},
|
|
803
|
-
},
|
|
804
|
-
error,
|
|
805
|
-
);
|
|
806
|
-
}
|
|
194
|
+
return this.stores.memory.deleteThread({ threadId });
|
|
807
195
|
}
|
|
808
196
|
|
|
809
197
|
async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
|
|
@@ -811,164 +199,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
811
199
|
async saveMessages(
|
|
812
200
|
args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
|
|
813
201
|
): Promise<MastraMessageV2[] | MastraMessageV1[]> {
|
|
814
|
-
|
|
815
|
-
if (messages.length === 0) return [];
|
|
816
|
-
|
|
817
|
-
const threadId = messages[0]?.threadId;
|
|
818
|
-
try {
|
|
819
|
-
if (!threadId) {
|
|
820
|
-
throw new Error('Thread ID is required');
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Check if thread exists
|
|
824
|
-
const thread = await this.getThreadById({ threadId });
|
|
825
|
-
if (!thread) {
|
|
826
|
-
throw new Error(`Thread ${threadId} not found`);
|
|
827
|
-
}
|
|
828
|
-
} catch (error) {
|
|
829
|
-
throw new MastraError(
|
|
830
|
-
{
|
|
831
|
-
id: 'STORAGE_UPSTASH_STORAGE_SAVE_MESSAGES_INVALID_ARGS',
|
|
832
|
-
domain: ErrorDomain.STORAGE,
|
|
833
|
-
category: ErrorCategory.USER,
|
|
834
|
-
},
|
|
835
|
-
error,
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Add an index to each message to maintain order
|
|
840
|
-
const messagesWithIndex = messages.map((message, index) => ({
|
|
841
|
-
...message,
|
|
842
|
-
_index: index,
|
|
843
|
-
}));
|
|
844
|
-
|
|
845
|
-
// Get current thread data once (all messages belong to same thread)
|
|
846
|
-
const threadKey = this.getKey(TABLE_THREADS, { id: threadId });
|
|
847
|
-
const existingThread = await this.redis.get<StorageThreadType>(threadKey);
|
|
848
|
-
|
|
849
|
-
try {
|
|
850
|
-
const batchSize = 1000;
|
|
851
|
-
for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
|
|
852
|
-
const batch = messagesWithIndex.slice(i, i + batchSize);
|
|
853
|
-
const pipeline = this.redis.pipeline();
|
|
854
|
-
|
|
855
|
-
for (const message of batch) {
|
|
856
|
-
const key = this.getMessageKey(message.threadId!, message.id);
|
|
857
|
-
const createdAtScore = new Date(message.createdAt).getTime();
|
|
858
|
-
const score = message._index !== undefined ? message._index : createdAtScore;
|
|
859
|
-
|
|
860
|
-
// Check if this message id exists in another thread
|
|
861
|
-
const existingKeyPattern = this.getMessageKey('*', message.id);
|
|
862
|
-
const keys = await this.scanKeys(existingKeyPattern);
|
|
863
|
-
|
|
864
|
-
if (keys.length > 0) {
|
|
865
|
-
const pipeline2 = this.redis.pipeline();
|
|
866
|
-
keys.forEach(key => pipeline2.get(key));
|
|
867
|
-
const results = await pipeline2.exec();
|
|
868
|
-
const existingMessages = results.filter(
|
|
869
|
-
(msg): msg is MastraMessageV2 | MastraMessageV1 => msg !== null,
|
|
870
|
-
) as (MastraMessageV2 | MastraMessageV1)[];
|
|
871
|
-
for (const existingMessage of existingMessages) {
|
|
872
|
-
const existingMessageKey = this.getMessageKey(existingMessage.threadId!, existingMessage.id);
|
|
873
|
-
if (existingMessage && existingMessage.threadId !== message.threadId) {
|
|
874
|
-
pipeline.del(existingMessageKey);
|
|
875
|
-
// Remove from old thread's sorted set
|
|
876
|
-
pipeline.zrem(this.getThreadMessagesKey(existingMessage.threadId!), existingMessage.id);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Store the message data
|
|
882
|
-
pipeline.set(key, message);
|
|
883
|
-
|
|
884
|
-
// Add to sorted set for this thread
|
|
885
|
-
pipeline.zadd(this.getThreadMessagesKey(message.threadId!), {
|
|
886
|
-
score,
|
|
887
|
-
member: message.id,
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// Update the thread's updatedAt field (only in the first batch)
|
|
892
|
-
if (i === 0 && existingThread) {
|
|
893
|
-
const updatedThread = {
|
|
894
|
-
...existingThread,
|
|
895
|
-
updatedAt: new Date(),
|
|
896
|
-
};
|
|
897
|
-
pipeline.set(threadKey, this.processRecord(TABLE_THREADS, updatedThread).processedRecord);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
await pipeline.exec();
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const list = new MessageList().add(messages, 'memory');
|
|
904
|
-
if (format === `v2`) return list.get.all.v2();
|
|
905
|
-
return list.get.all.v1();
|
|
906
|
-
} catch (error) {
|
|
907
|
-
throw new MastraError(
|
|
908
|
-
{
|
|
909
|
-
id: 'STORAGE_UPSTASH_STORAGE_SAVE_MESSAGES_FAILED',
|
|
910
|
-
domain: ErrorDomain.STORAGE,
|
|
911
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
912
|
-
details: {
|
|
913
|
-
threadId,
|
|
914
|
-
},
|
|
915
|
-
},
|
|
916
|
-
error,
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
private async _getIncludedMessages(
|
|
922
|
-
threadId: string,
|
|
923
|
-
selectBy: StorageGetMessagesArg['selectBy'],
|
|
924
|
-
): Promise<MastraMessageV2[] | MastraMessageV1[]> {
|
|
925
|
-
const messageIds = new Set<string>();
|
|
926
|
-
const messageIdToThreadIds: Record<string, string> = {};
|
|
927
|
-
|
|
928
|
-
// First, get specifically included messages and their context
|
|
929
|
-
if (selectBy?.include?.length) {
|
|
930
|
-
for (const item of selectBy.include) {
|
|
931
|
-
messageIds.add(item.id);
|
|
932
|
-
|
|
933
|
-
// Use per-include threadId if present, else fallback to main threadId
|
|
934
|
-
const itemThreadId = item.threadId || threadId;
|
|
935
|
-
messageIdToThreadIds[item.id] = itemThreadId;
|
|
936
|
-
const itemThreadMessagesKey = this.getThreadMessagesKey(itemThreadId);
|
|
937
|
-
|
|
938
|
-
// Get the rank of this message in the sorted set
|
|
939
|
-
const rank = await this.redis.zrank(itemThreadMessagesKey, item.id);
|
|
940
|
-
if (rank === null) continue;
|
|
941
|
-
|
|
942
|
-
// Get previous messages if requested
|
|
943
|
-
if (item.withPreviousMessages) {
|
|
944
|
-
const start = Math.max(0, rank - item.withPreviousMessages);
|
|
945
|
-
const prevIds = rank === 0 ? [] : await this.redis.zrange(itemThreadMessagesKey, start, rank - 1);
|
|
946
|
-
prevIds.forEach(id => {
|
|
947
|
-
messageIds.add(id as string);
|
|
948
|
-
messageIdToThreadIds[id as string] = itemThreadId;
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Get next messages if requested
|
|
953
|
-
if (item.withNextMessages) {
|
|
954
|
-
const nextIds = await this.redis.zrange(itemThreadMessagesKey, rank + 1, rank + item.withNextMessages);
|
|
955
|
-
nextIds.forEach(id => {
|
|
956
|
-
messageIds.add(id as string);
|
|
957
|
-
messageIdToThreadIds[id as string] = itemThreadId;
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
const pipeline = this.redis.pipeline();
|
|
963
|
-
Array.from(messageIds).forEach(id => {
|
|
964
|
-
const tId = messageIdToThreadIds[id] || threadId;
|
|
965
|
-
pipeline.get(this.getMessageKey(tId, id as string));
|
|
966
|
-
});
|
|
967
|
-
const results = await pipeline.exec();
|
|
968
|
-
return results.filter(result => result !== null) as MastraMessageV2[] | MastraMessageV1[];
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
return [];
|
|
202
|
+
return this.stores.memory.saveMessages(args);
|
|
972
203
|
}
|
|
973
204
|
|
|
974
205
|
/**
|
|
@@ -981,97 +212,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
981
212
|
selectBy,
|
|
982
213
|
format,
|
|
983
214
|
}: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
|
|
984
|
-
|
|
985
|
-
try {
|
|
986
|
-
const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
|
|
987
|
-
const limit = this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: Number.MAX_SAFE_INTEGER });
|
|
988
|
-
|
|
989
|
-
const messageIds = new Set<string>();
|
|
990
|
-
const messageIdToThreadIds: Record<string, string> = {};
|
|
991
|
-
|
|
992
|
-
if (limit === 0 && !selectBy?.include) {
|
|
993
|
-
return [];
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Then get the most recent messages (or all if no limit)
|
|
997
|
-
if (limit === Number.MAX_SAFE_INTEGER) {
|
|
998
|
-
// Get all messages
|
|
999
|
-
const allIds = await this.redis.zrange(threadMessagesKey, 0, -1);
|
|
1000
|
-
allIds.forEach(id => {
|
|
1001
|
-
messageIds.add(id as string);
|
|
1002
|
-
messageIdToThreadIds[id as string] = threadId;
|
|
1003
|
-
});
|
|
1004
|
-
} else if (limit > 0) {
|
|
1005
|
-
// Get limited number of recent messages
|
|
1006
|
-
const latestIds = await this.redis.zrange(threadMessagesKey, -limit, -1);
|
|
1007
|
-
latestIds.forEach(id => {
|
|
1008
|
-
messageIds.add(id as string);
|
|
1009
|
-
messageIdToThreadIds[id as string] = threadId;
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
const includedMessages = await this._getIncludedMessages(threadId, selectBy);
|
|
1014
|
-
|
|
1015
|
-
// Fetch all needed messages in parallel
|
|
1016
|
-
const messages = [
|
|
1017
|
-
...includedMessages,
|
|
1018
|
-
...((
|
|
1019
|
-
await Promise.all(
|
|
1020
|
-
Array.from(messageIds).map(async id => {
|
|
1021
|
-
const tId = messageIdToThreadIds[id] || threadId;
|
|
1022
|
-
const byThreadId = await this.redis.get<MastraMessageV2 & { _index?: number }>(
|
|
1023
|
-
this.getMessageKey(tId, id),
|
|
1024
|
-
);
|
|
1025
|
-
if (byThreadId) return byThreadId;
|
|
1026
|
-
|
|
1027
|
-
return null;
|
|
1028
|
-
}),
|
|
1029
|
-
)
|
|
1030
|
-
).filter(msg => msg !== null) as (MastraMessageV2 & { _index?: number })[]),
|
|
1031
|
-
];
|
|
1032
|
-
|
|
1033
|
-
// Sort messages by their position in the sorted set
|
|
1034
|
-
messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
|
|
1035
|
-
|
|
1036
|
-
const seen = new Set<string>();
|
|
1037
|
-
const dedupedMessages = messages.filter(row => {
|
|
1038
|
-
if (seen.has(row.id)) return false;
|
|
1039
|
-
seen.add(row.id);
|
|
1040
|
-
return true;
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
// Remove _index before returning and handle format conversion properly
|
|
1044
|
-
const prepared = dedupedMessages
|
|
1045
|
-
.filter(message => message !== null && message !== undefined)
|
|
1046
|
-
.map(message => {
|
|
1047
|
-
const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
|
|
1048
|
-
return messageWithoutIndex as unknown as MastraMessageV1;
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
// For backward compatibility, return messages directly without using MessageList
|
|
1052
|
-
// since MessageList has deduplication logic that can cause issues
|
|
1053
|
-
if (format === 'v2') {
|
|
1054
|
-
// Convert V1 format back to V2 format
|
|
1055
|
-
return prepared.map(msg => ({
|
|
1056
|
-
...msg,
|
|
1057
|
-
content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
|
|
1058
|
-
})) as MastraMessageV2[];
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
return prepared;
|
|
1062
|
-
} catch (error) {
|
|
1063
|
-
throw new MastraError(
|
|
1064
|
-
{
|
|
1065
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_MESSAGES_FAILED',
|
|
1066
|
-
domain: ErrorDomain.STORAGE,
|
|
1067
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
1068
|
-
details: {
|
|
1069
|
-
threadId,
|
|
1070
|
-
},
|
|
1071
|
-
},
|
|
1072
|
-
error,
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
215
|
+
return this.stores.memory.getMessages({ threadId, selectBy, format });
|
|
1075
216
|
}
|
|
1076
217
|
|
|
1077
218
|
public async getMessagesPaginated(
|
|
@@ -1079,94 +220,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
1079
220
|
format?: 'v1' | 'v2';
|
|
1080
221
|
},
|
|
1081
222
|
): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
|
|
1082
|
-
|
|
1083
|
-
const { page = 0, perPage = 40, dateRange } = selectBy?.pagination || {};
|
|
1084
|
-
const fromDate = dateRange?.start;
|
|
1085
|
-
const toDate = dateRange?.end;
|
|
1086
|
-
const threadMessagesKey = this.getThreadMessagesKey(threadId);
|
|
1087
|
-
const messages: (MastraMessageV2 | MastraMessageV1)[] = [];
|
|
1088
|
-
|
|
1089
|
-
try {
|
|
1090
|
-
const includedMessages = await this._getIncludedMessages(threadId, selectBy);
|
|
1091
|
-
messages.push(...includedMessages);
|
|
1092
|
-
|
|
1093
|
-
const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
|
|
1094
|
-
if (allMessageIds.length === 0) {
|
|
1095
|
-
return {
|
|
1096
|
-
messages: [],
|
|
1097
|
-
total: 0,
|
|
1098
|
-
page,
|
|
1099
|
-
perPage,
|
|
1100
|
-
hasMore: false,
|
|
1101
|
-
};
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// Use pipeline to fetch all messages efficiently
|
|
1105
|
-
const pipeline = this.redis.pipeline();
|
|
1106
|
-
allMessageIds.forEach(id => pipeline.get(this.getMessageKey(threadId, id as string)));
|
|
1107
|
-
const results = await pipeline.exec();
|
|
1108
|
-
|
|
1109
|
-
// Process messages and apply filters - handle undefined results from pipeline
|
|
1110
|
-
let messagesData = results.filter((msg): msg is MastraMessageV2 | MastraMessageV1 => msg !== null) as (
|
|
1111
|
-
| MastraMessageV2
|
|
1112
|
-
| MastraMessageV1
|
|
1113
|
-
)[];
|
|
1114
|
-
|
|
1115
|
-
// Apply date filters if provided
|
|
1116
|
-
if (fromDate) {
|
|
1117
|
-
messagesData = messagesData.filter(msg => msg && new Date(msg.createdAt).getTime() >= fromDate.getTime());
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
if (toDate) {
|
|
1121
|
-
messagesData = messagesData.filter(msg => msg && new Date(msg.createdAt).getTime() <= toDate.getTime());
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// Sort messages by their position in the sorted set
|
|
1125
|
-
messagesData.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
|
|
1126
|
-
|
|
1127
|
-
const total = messagesData.length;
|
|
1128
|
-
|
|
1129
|
-
const start = page * perPage;
|
|
1130
|
-
const end = start + perPage;
|
|
1131
|
-
const hasMore = end < total;
|
|
1132
|
-
const paginatedMessages = messagesData.slice(start, end);
|
|
1133
|
-
|
|
1134
|
-
messages.push(...paginatedMessages);
|
|
1135
|
-
|
|
1136
|
-
const list = new MessageList().add(messages, 'memory');
|
|
1137
|
-
const finalMessages = (format === `v2` ? list.get.all.v2() : list.get.all.v1()) as
|
|
1138
|
-
| MastraMessageV1[]
|
|
1139
|
-
| MastraMessageV2[];
|
|
1140
|
-
|
|
1141
|
-
return {
|
|
1142
|
-
messages: finalMessages,
|
|
1143
|
-
total,
|
|
1144
|
-
page,
|
|
1145
|
-
perPage,
|
|
1146
|
-
hasMore,
|
|
1147
|
-
};
|
|
1148
|
-
} catch (error) {
|
|
1149
|
-
const mastraError = new MastraError(
|
|
1150
|
-
{
|
|
1151
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_MESSAGES_PAGINATED_FAILED',
|
|
1152
|
-
domain: ErrorDomain.STORAGE,
|
|
1153
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
1154
|
-
details: {
|
|
1155
|
-
threadId,
|
|
1156
|
-
},
|
|
1157
|
-
},
|
|
1158
|
-
error,
|
|
1159
|
-
);
|
|
1160
|
-
this.logger.error(mastraError.toString());
|
|
1161
|
-
this.logger?.trackException(mastraError);
|
|
1162
|
-
return {
|
|
1163
|
-
messages: [],
|
|
1164
|
-
total: 0,
|
|
1165
|
-
page,
|
|
1166
|
-
perPage,
|
|
1167
|
-
hasMore: false,
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
223
|
+
return this.stores.memory.getMessagesPaginated(args);
|
|
1170
224
|
}
|
|
1171
225
|
|
|
1172
226
|
async persistWorkflowSnapshot(params: {
|
|
@@ -1175,34 +229,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
1175
229
|
runId: string;
|
|
1176
230
|
snapshot: WorkflowRunState;
|
|
1177
231
|
}): Promise<void> {
|
|
1178
|
-
|
|
1179
|
-
try {
|
|
1180
|
-
await this.insert({
|
|
1181
|
-
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
1182
|
-
record: {
|
|
1183
|
-
namespace,
|
|
1184
|
-
workflow_name: workflowName,
|
|
1185
|
-
run_id: runId,
|
|
1186
|
-
snapshot,
|
|
1187
|
-
createdAt: new Date(),
|
|
1188
|
-
updatedAt: new Date(),
|
|
1189
|
-
},
|
|
1190
|
-
});
|
|
1191
|
-
} catch (error) {
|
|
1192
|
-
throw new MastraError(
|
|
1193
|
-
{
|
|
1194
|
-
id: 'STORAGE_UPSTASH_STORAGE_PERSIST_WORKFLOW_SNAPSHOT_FAILED',
|
|
1195
|
-
domain: ErrorDomain.STORAGE,
|
|
1196
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
1197
|
-
details: {
|
|
1198
|
-
namespace,
|
|
1199
|
-
workflowName,
|
|
1200
|
-
runId,
|
|
1201
|
-
},
|
|
1202
|
-
},
|
|
1203
|
-
error,
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
232
|
+
return this.stores.workflows.persistWorkflowSnapshot(params);
|
|
1206
233
|
}
|
|
1207
234
|
|
|
1208
235
|
async loadWorkflowSnapshot(params: {
|
|
@@ -1210,370 +237,56 @@ export class UpstashStore extends MastraStorage {
|
|
|
1210
237
|
workflowName: string;
|
|
1211
238
|
runId: string;
|
|
1212
239
|
}): Promise<WorkflowRunState | null> {
|
|
1213
|
-
|
|
1214
|
-
const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
|
|
1215
|
-
namespace,
|
|
1216
|
-
workflow_name: workflowName,
|
|
1217
|
-
run_id: runId,
|
|
1218
|
-
});
|
|
1219
|
-
try {
|
|
1220
|
-
const data = await this.redis.get<{
|
|
1221
|
-
namespace: string;
|
|
1222
|
-
workflow_name: string;
|
|
1223
|
-
run_id: string;
|
|
1224
|
-
snapshot: WorkflowRunState;
|
|
1225
|
-
}>(key);
|
|
1226
|
-
if (!data) return null;
|
|
1227
|
-
return data.snapshot;
|
|
1228
|
-
} catch (error) {
|
|
1229
|
-
throw new MastraError(
|
|
1230
|
-
{
|
|
1231
|
-
id: 'STORAGE_UPSTASH_STORAGE_LOAD_WORKFLOW_SNAPSHOT_FAILED',
|
|
1232
|
-
domain: ErrorDomain.STORAGE,
|
|
1233
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
1234
|
-
details: {
|
|
1235
|
-
namespace,
|
|
1236
|
-
workflowName,
|
|
1237
|
-
runId,
|
|
1238
|
-
},
|
|
1239
|
-
},
|
|
1240
|
-
error,
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
/**
|
|
1246
|
-
* Get all evaluations with pagination and total count
|
|
1247
|
-
* @param options Pagination and filtering options
|
|
1248
|
-
* @returns Object with evals array and total count
|
|
1249
|
-
*/
|
|
1250
|
-
async getEvals(
|
|
1251
|
-
options?: {
|
|
1252
|
-
agentName?: string;
|
|
1253
|
-
type?: 'test' | 'live';
|
|
1254
|
-
} & PaginationArgs,
|
|
1255
|
-
): Promise<PaginationInfo & { evals: EvalRow[] }> {
|
|
1256
|
-
try {
|
|
1257
|
-
// Default pagination parameters
|
|
1258
|
-
const { agentName, type, page = 0, perPage = 100, dateRange } = options || {};
|
|
1259
|
-
const fromDate = dateRange?.start;
|
|
1260
|
-
const toDate = dateRange?.end;
|
|
1261
|
-
|
|
1262
|
-
// Get all keys that match the evals table pattern using cursor-based scanning
|
|
1263
|
-
const pattern = `${TABLE_EVALS}:*`;
|
|
1264
|
-
const keys = await this.scanKeys(pattern);
|
|
1265
|
-
|
|
1266
|
-
// Check if we have any keys before using pipeline
|
|
1267
|
-
if (keys.length === 0) {
|
|
1268
|
-
return {
|
|
1269
|
-
evals: [],
|
|
1270
|
-
total: 0,
|
|
1271
|
-
page,
|
|
1272
|
-
perPage,
|
|
1273
|
-
hasMore: false,
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
// Use pipeline for batch fetching to improve performance
|
|
1278
|
-
const pipeline = this.redis.pipeline();
|
|
1279
|
-
keys.forEach(key => pipeline.get(key));
|
|
1280
|
-
const results = await pipeline.exec();
|
|
1281
|
-
|
|
1282
|
-
// Process results and apply filters
|
|
1283
|
-
let filteredEvals = results
|
|
1284
|
-
.map((result: any) => result as Record<string, any> | null)
|
|
1285
|
-
.filter((record): record is Record<string, any> => record !== null && typeof record === 'object');
|
|
1286
|
-
|
|
1287
|
-
// Apply agent name filter if provided
|
|
1288
|
-
if (agentName) {
|
|
1289
|
-
filteredEvals = filteredEvals.filter(record => record.agent_name === agentName);
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
// Apply type filter if provided
|
|
1293
|
-
if (type === 'test') {
|
|
1294
|
-
filteredEvals = filteredEvals.filter(record => {
|
|
1295
|
-
if (!record.test_info) return false;
|
|
1296
|
-
|
|
1297
|
-
try {
|
|
1298
|
-
if (typeof record.test_info === 'string') {
|
|
1299
|
-
const parsedTestInfo = JSON.parse(record.test_info);
|
|
1300
|
-
return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
|
|
1301
|
-
}
|
|
1302
|
-
return typeof record.test_info === 'object' && 'testPath' in record.test_info;
|
|
1303
|
-
} catch {
|
|
1304
|
-
return false;
|
|
1305
|
-
}
|
|
1306
|
-
});
|
|
1307
|
-
} else if (type === 'live') {
|
|
1308
|
-
filteredEvals = filteredEvals.filter(record => {
|
|
1309
|
-
if (!record.test_info) return true;
|
|
1310
|
-
|
|
1311
|
-
try {
|
|
1312
|
-
if (typeof record.test_info === 'string') {
|
|
1313
|
-
const parsedTestInfo = JSON.parse(record.test_info);
|
|
1314
|
-
return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
|
|
1315
|
-
}
|
|
1316
|
-
return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
|
|
1317
|
-
} catch {
|
|
1318
|
-
return true;
|
|
1319
|
-
}
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
// Apply date filters if provided
|
|
1324
|
-
if (fromDate) {
|
|
1325
|
-
filteredEvals = filteredEvals.filter(record => {
|
|
1326
|
-
const createdAt = new Date(record.created_at || record.createdAt || 0);
|
|
1327
|
-
return createdAt.getTime() >= fromDate.getTime();
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
if (toDate) {
|
|
1332
|
-
filteredEvals = filteredEvals.filter(record => {
|
|
1333
|
-
const createdAt = new Date(record.created_at || record.createdAt || 0);
|
|
1334
|
-
return createdAt.getTime() <= toDate.getTime();
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// Sort by creation date (newest first)
|
|
1339
|
-
filteredEvals.sort((a, b) => {
|
|
1340
|
-
const dateA = new Date(a.created_at || a.createdAt || 0).getTime();
|
|
1341
|
-
const dateB = new Date(b.created_at || b.createdAt || 0).getTime();
|
|
1342
|
-
return dateB - dateA;
|
|
1343
|
-
});
|
|
1344
|
-
|
|
1345
|
-
const total = filteredEvals.length;
|
|
1346
|
-
|
|
1347
|
-
// Apply pagination
|
|
1348
|
-
const start = page * perPage;
|
|
1349
|
-
const end = start + perPage;
|
|
1350
|
-
const paginatedEvals = filteredEvals.slice(start, end);
|
|
1351
|
-
const hasMore = end < total;
|
|
1352
|
-
|
|
1353
|
-
// Transform to EvalRow format
|
|
1354
|
-
const evals = paginatedEvals.map(record => this.transformEvalRecord(record));
|
|
1355
|
-
|
|
1356
|
-
return {
|
|
1357
|
-
evals,
|
|
1358
|
-
total,
|
|
1359
|
-
page,
|
|
1360
|
-
perPage,
|
|
1361
|
-
hasMore,
|
|
1362
|
-
};
|
|
1363
|
-
} catch (error) {
|
|
1364
|
-
const { page = 0, perPage = 100 } = options || {};
|
|
1365
|
-
const mastraError = new MastraError(
|
|
1366
|
-
{
|
|
1367
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_FAILED',
|
|
1368
|
-
domain: ErrorDomain.STORAGE,
|
|
1369
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
1370
|
-
details: {
|
|
1371
|
-
page,
|
|
1372
|
-
perPage,
|
|
1373
|
-
},
|
|
1374
|
-
},
|
|
1375
|
-
error,
|
|
1376
|
-
);
|
|
1377
|
-
this.logger.error(mastraError.toString());
|
|
1378
|
-
this.logger?.trackException(mastraError);
|
|
1379
|
-
return {
|
|
1380
|
-
evals: [],
|
|
1381
|
-
total: 0,
|
|
1382
|
-
page,
|
|
1383
|
-
perPage,
|
|
1384
|
-
hasMore: false,
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
240
|
+
return this.stores.workflows.loadWorkflowSnapshot(params);
|
|
1387
241
|
}
|
|
1388
242
|
|
|
1389
|
-
async getWorkflowRuns(
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
resourceId?: string;
|
|
1406
|
-
} = { namespace: 'workflows' },
|
|
1407
|
-
): Promise<WorkflowRuns> {
|
|
1408
|
-
try {
|
|
1409
|
-
// Get all workflow keys
|
|
1410
|
-
let pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ':*';
|
|
1411
|
-
if (workflowName && resourceId) {
|
|
1412
|
-
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
|
|
1413
|
-
namespace,
|
|
1414
|
-
workflow_name: workflowName,
|
|
1415
|
-
run_id: '*',
|
|
1416
|
-
resourceId,
|
|
1417
|
-
});
|
|
1418
|
-
} else if (workflowName) {
|
|
1419
|
-
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ':*';
|
|
1420
|
-
} else if (resourceId) {
|
|
1421
|
-
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: '*', run_id: '*', resourceId });
|
|
1422
|
-
}
|
|
1423
|
-
const keys = await this.scanKeys(pattern);
|
|
1424
|
-
|
|
1425
|
-
// Check if we have any keys before using pipeline
|
|
1426
|
-
if (keys.length === 0) {
|
|
1427
|
-
return { runs: [], total: 0 };
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
// Use pipeline for batch fetching to improve performance
|
|
1431
|
-
const pipeline = this.redis.pipeline();
|
|
1432
|
-
keys.forEach(key => pipeline.get(key));
|
|
1433
|
-
const results = await pipeline.exec();
|
|
1434
|
-
|
|
1435
|
-
// Filter and transform results - handle undefined results
|
|
1436
|
-
let runs = results
|
|
1437
|
-
.map((result: any) => result as Record<string, any> | null)
|
|
1438
|
-
.filter(
|
|
1439
|
-
(record): record is Record<string, any> =>
|
|
1440
|
-
record !== null && record !== undefined && typeof record === 'object' && 'workflow_name' in record,
|
|
1441
|
-
)
|
|
1442
|
-
// Only filter by workflowName if it was specifically requested
|
|
1443
|
-
.filter(record => !workflowName || record.workflow_name === workflowName)
|
|
1444
|
-
.map(w => this.parseWorkflowRun(w!))
|
|
1445
|
-
.filter(w => {
|
|
1446
|
-
if (fromDate && w.createdAt < fromDate) return false;
|
|
1447
|
-
if (toDate && w.createdAt > toDate) return false;
|
|
1448
|
-
return true;
|
|
1449
|
-
})
|
|
1450
|
-
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
1451
|
-
|
|
1452
|
-
const total = runs.length;
|
|
1453
|
-
|
|
1454
|
-
// Apply pagination if requested
|
|
1455
|
-
if (limit !== undefined && offset !== undefined) {
|
|
1456
|
-
runs = runs.slice(offset, offset + limit);
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
return { runs, total };
|
|
1460
|
-
} catch (error) {
|
|
1461
|
-
throw new MastraError(
|
|
1462
|
-
{
|
|
1463
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_WORKFLOW_RUNS_FAILED',
|
|
1464
|
-
domain: ErrorDomain.STORAGE,
|
|
1465
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
1466
|
-
details: {
|
|
1467
|
-
namespace,
|
|
1468
|
-
workflowName: workflowName || '',
|
|
1469
|
-
resourceId: resourceId || '',
|
|
1470
|
-
},
|
|
1471
|
-
},
|
|
1472
|
-
error,
|
|
1473
|
-
);
|
|
1474
|
-
}
|
|
243
|
+
async getWorkflowRuns({
|
|
244
|
+
workflowName,
|
|
245
|
+
fromDate,
|
|
246
|
+
toDate,
|
|
247
|
+
limit,
|
|
248
|
+
offset,
|
|
249
|
+
resourceId,
|
|
250
|
+
}: {
|
|
251
|
+
workflowName?: string;
|
|
252
|
+
fromDate?: Date;
|
|
253
|
+
toDate?: Date;
|
|
254
|
+
limit?: number;
|
|
255
|
+
offset?: number;
|
|
256
|
+
resourceId?: string;
|
|
257
|
+
} = {}): Promise<WorkflowRuns> {
|
|
258
|
+
return this.stores.workflows.getWorkflowRuns({ workflowName, fromDate, toDate, limit, offset, resourceId });
|
|
1475
259
|
}
|
|
1476
260
|
|
|
1477
261
|
async getWorkflowRunById({
|
|
1478
|
-
namespace = 'workflows',
|
|
1479
262
|
runId,
|
|
1480
263
|
workflowName,
|
|
1481
264
|
}: {
|
|
1482
|
-
namespace: string;
|
|
1483
265
|
runId: string;
|
|
1484
266
|
workflowName?: string;
|
|
1485
267
|
}): Promise<WorkflowRun | null> {
|
|
1486
|
-
|
|
1487
|
-
const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + '*';
|
|
1488
|
-
const keys = await this.scanKeys(key);
|
|
1489
|
-
const workflows = await Promise.all(
|
|
1490
|
-
keys.map(async key => {
|
|
1491
|
-
const data = await this.redis.get<{
|
|
1492
|
-
workflow_name: string;
|
|
1493
|
-
run_id: string;
|
|
1494
|
-
snapshot: WorkflowRunState | string;
|
|
1495
|
-
createdAt: string | Date;
|
|
1496
|
-
updatedAt: string | Date;
|
|
1497
|
-
resourceId: string;
|
|
1498
|
-
}>(key);
|
|
1499
|
-
return data;
|
|
1500
|
-
}),
|
|
1501
|
-
);
|
|
1502
|
-
const data = workflows.find(w => w?.run_id === runId && w?.workflow_name === workflowName) as WorkflowRun | null;
|
|
1503
|
-
if (!data) return null;
|
|
1504
|
-
return this.parseWorkflowRun(data);
|
|
1505
|
-
} catch (error) {
|
|
1506
|
-
throw new MastraError(
|
|
1507
|
-
{
|
|
1508
|
-
id: 'STORAGE_UPSTASH_STORAGE_GET_WORKFLOW_RUN_BY_ID_FAILED',
|
|
1509
|
-
domain: ErrorDomain.STORAGE,
|
|
1510
|
-
category: ErrorCategory.THIRD_PARTY,
|
|
1511
|
-
details: {
|
|
1512
|
-
namespace,
|
|
1513
|
-
runId,
|
|
1514
|
-
workflowName: workflowName || '',
|
|
1515
|
-
},
|
|
1516
|
-
},
|
|
1517
|
-
error,
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
268
|
+
return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
|
|
1520
269
|
}
|
|
1521
270
|
|
|
1522
271
|
async close(): Promise<void> {
|
|
1523
272
|
// No explicit cleanup needed for Upstash Redis
|
|
1524
273
|
}
|
|
1525
274
|
|
|
1526
|
-
async updateMessages(
|
|
1527
|
-
messages: Partial<Omit<MastraMessageV2, 'createdAt'>> &
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}[];
|
|
275
|
+
async updateMessages(args: {
|
|
276
|
+
messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
|
|
277
|
+
id: string;
|
|
278
|
+
content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
|
|
279
|
+
})[];
|
|
1532
280
|
}): Promise<MastraMessageV2[]> {
|
|
1533
|
-
this.
|
|
1534
|
-
throw new Error('Method not implemented');
|
|
281
|
+
return this.stores.memory.updateMessages(args);
|
|
1535
282
|
}
|
|
1536
283
|
|
|
1537
284
|
async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
|
|
1538
|
-
|
|
1539
|
-
const key = `${TABLE_RESOURCES}:${resourceId}`;
|
|
1540
|
-
const data = await this.redis.get<StorageResourceType>(key);
|
|
1541
|
-
|
|
1542
|
-
if (!data) {
|
|
1543
|
-
return null;
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return {
|
|
1547
|
-
...data,
|
|
1548
|
-
createdAt: new Date(data.createdAt),
|
|
1549
|
-
updatedAt: new Date(data.updatedAt),
|
|
1550
|
-
// Ensure workingMemory is always returned as a string, regardless of automatic parsing
|
|
1551
|
-
workingMemory: typeof data.workingMemory === 'object' ? JSON.stringify(data.workingMemory) : data.workingMemory,
|
|
1552
|
-
metadata: typeof data.metadata === 'string' ? JSON.parse(data.metadata) : data.metadata,
|
|
1553
|
-
};
|
|
1554
|
-
} catch (error) {
|
|
1555
|
-
this.logger.error('Error getting resource by ID:', error);
|
|
1556
|
-
throw error;
|
|
1557
|
-
}
|
|
285
|
+
return this.stores.memory.getResourceById({ resourceId });
|
|
1558
286
|
}
|
|
1559
287
|
|
|
1560
288
|
async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
|
|
1561
|
-
|
|
1562
|
-
const key = `${TABLE_RESOURCES}:${resource.id}`;
|
|
1563
|
-
const serializedResource = {
|
|
1564
|
-
...resource,
|
|
1565
|
-
metadata: JSON.stringify(resource.metadata),
|
|
1566
|
-
createdAt: resource.createdAt.toISOString(),
|
|
1567
|
-
updatedAt: resource.updatedAt.toISOString(),
|
|
1568
|
-
};
|
|
1569
|
-
|
|
1570
|
-
await this.redis.set(key, serializedResource);
|
|
1571
|
-
|
|
1572
|
-
return resource;
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
this.logger.error('Error saving resource:', error);
|
|
1575
|
-
throw error;
|
|
1576
|
-
}
|
|
289
|
+
return this.stores.memory.saveResource({ resource });
|
|
1577
290
|
}
|
|
1578
291
|
|
|
1579
292
|
async updateResource({
|
|
@@ -1585,36 +298,50 @@ export class UpstashStore extends MastraStorage {
|
|
|
1585
298
|
workingMemory?: string;
|
|
1586
299
|
metadata?: Record<string, unknown>;
|
|
1587
300
|
}): Promise<StorageResourceType> {
|
|
1588
|
-
|
|
1589
|
-
|
|
301
|
+
return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
|
|
302
|
+
}
|
|
1590
303
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
id: resourceId,
|
|
1595
|
-
workingMemory,
|
|
1596
|
-
metadata: metadata || {},
|
|
1597
|
-
createdAt: new Date(),
|
|
1598
|
-
updatedAt: new Date(),
|
|
1599
|
-
};
|
|
1600
|
-
return this.saveResource({ resource: newResource });
|
|
1601
|
-
}
|
|
304
|
+
async getScoreById({ id: _id }: { id: string }): Promise<ScoreRowData | null> {
|
|
305
|
+
return this.stores.scores.getScoreById({ id: _id });
|
|
306
|
+
}
|
|
1602
307
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
308
|
+
async saveScore(score: ScoreRowData): Promise<{ score: ScoreRowData }> {
|
|
309
|
+
return this.stores.scores.saveScore(score);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async getScoresByRunId({
|
|
313
|
+
runId,
|
|
314
|
+
pagination,
|
|
315
|
+
}: {
|
|
316
|
+
runId: string;
|
|
317
|
+
pagination: StoragePagination;
|
|
318
|
+
}): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
|
|
319
|
+
return this.stores.scores.getScoresByRunId({ runId, pagination });
|
|
320
|
+
}
|
|
1612
321
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
322
|
+
async getScoresByEntityId({
|
|
323
|
+
entityId,
|
|
324
|
+
entityType,
|
|
325
|
+
pagination,
|
|
326
|
+
}: {
|
|
327
|
+
pagination: StoragePagination;
|
|
328
|
+
entityId: string;
|
|
329
|
+
entityType: string;
|
|
330
|
+
}): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
|
|
331
|
+
return this.stores.scores.getScoresByEntityId({
|
|
332
|
+
entityId,
|
|
333
|
+
entityType,
|
|
334
|
+
pagination,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async getScoresByScorerId({
|
|
339
|
+
scorerId,
|
|
340
|
+
pagination,
|
|
341
|
+
}: {
|
|
342
|
+
scorerId: string;
|
|
343
|
+
pagination: StoragePagination;
|
|
344
|
+
}): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
|
|
345
|
+
return this.stores.scores.getScoresByScorerId({ scorerId, pagination });
|
|
1619
346
|
}
|
|
1620
347
|
}
|