@mastra/upstash 0.2.2-alpha.1 → 0.2.2-alpha.3
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 +27 -0
- package/dist/_tsup-dts-rollup.d.cts +20 -1
- package/dist/_tsup-dts-rollup.d.ts +20 -1
- package/dist/index.cjs +146 -15
- package/dist/index.js +147 -16
- package/package.json +2 -2
- package/src/storage/index.ts +234 -17
- package/src/storage/upstash.test.ts +369 -137
package/src/storage/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import type { MetricResult, TestInfo } from '@mastra/core/eval';
|
|
1
2
|
import type { StorageThreadType, MessageType } from '@mastra/core/memory';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
MastraStorage,
|
|
5
|
+
TABLE_MESSAGES,
|
|
6
|
+
TABLE_THREADS,
|
|
7
|
+
TABLE_WORKFLOW_SNAPSHOT,
|
|
8
|
+
TABLE_EVALS,
|
|
9
|
+
} from '@mastra/core/storage';
|
|
3
10
|
import type { TABLE_NAMES, StorageColumn, StorageGetMessagesArg, EvalRow } from '@mastra/core/storage';
|
|
4
11
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
5
12
|
import { Redis } from '@upstash/redis';
|
|
@@ -13,18 +20,124 @@ export class UpstashStore extends MastraStorage {
|
|
|
13
20
|
batchInsert(_input: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
|
|
14
21
|
throw new Error('Method not implemented.');
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
|
|
24
|
+
async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
|
|
25
|
+
try {
|
|
26
|
+
// Get all keys that match the evals table pattern
|
|
27
|
+
const pattern = `${TABLE_EVALS}:*`;
|
|
28
|
+
const keys = await this.redis.keys(pattern);
|
|
29
|
+
|
|
30
|
+
// Fetch all eval records
|
|
31
|
+
const evalRecords = await Promise.all(
|
|
32
|
+
keys.map(async key => {
|
|
33
|
+
const data = await this.redis.get<Record<string, any>>(key);
|
|
34
|
+
return data;
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Filter by agent name and remove nulls
|
|
39
|
+
const nonNullRecords = evalRecords.filter(
|
|
40
|
+
(record): record is Record<string, any> =>
|
|
41
|
+
record !== null && typeof record === 'object' && 'agent_name' in record && record.agent_name === agentName,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Apply additional filtering based on type
|
|
45
|
+
let filteredEvals = nonNullRecords;
|
|
46
|
+
|
|
47
|
+
if (type === 'test') {
|
|
48
|
+
filteredEvals = filteredEvals.filter(record => {
|
|
49
|
+
if (!record.test_info) return false;
|
|
50
|
+
|
|
51
|
+
// Handle test_info as a JSON string
|
|
52
|
+
try {
|
|
53
|
+
if (typeof record.test_info === 'string') {
|
|
54
|
+
const parsedTestInfo = JSON.parse(record.test_info);
|
|
55
|
+
return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle test_info as an object
|
|
59
|
+
return typeof record.test_info === 'object' && 'testPath' in record.test_info;
|
|
60
|
+
} catch (_e) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
} else if (type === 'live') {
|
|
65
|
+
filteredEvals = filteredEvals.filter(record => {
|
|
66
|
+
if (!record.test_info) return true;
|
|
67
|
+
|
|
68
|
+
// Handle test_info as a JSON string
|
|
69
|
+
try {
|
|
70
|
+
if (typeof record.test_info === 'string') {
|
|
71
|
+
const parsedTestInfo = JSON.parse(record.test_info);
|
|
72
|
+
return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle test_info as an object
|
|
76
|
+
return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
|
|
77
|
+
} catch (_e) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Transform to EvalRow format
|
|
84
|
+
return filteredEvals.map(record => this.transformEvalRecord(record));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Failed to get evals for the specified agent:', error);
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
18
89
|
}
|
|
90
|
+
|
|
91
|
+
private transformEvalRecord(record: Record<string, any>): EvalRow {
|
|
92
|
+
// Parse JSON strings if needed
|
|
93
|
+
let result = record.result;
|
|
94
|
+
if (typeof result === 'string') {
|
|
95
|
+
try {
|
|
96
|
+
result = JSON.parse(result);
|
|
97
|
+
} catch (_e) {
|
|
98
|
+
console.warn('Failed to parse result JSON:');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let testInfo = record.test_info;
|
|
103
|
+
if (typeof testInfo === 'string') {
|
|
104
|
+
try {
|
|
105
|
+
testInfo = JSON.parse(testInfo);
|
|
106
|
+
} catch (_e) {
|
|
107
|
+
console.warn('Failed to parse test_info JSON:');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
agentName: record.agent_name,
|
|
113
|
+
input: record.input,
|
|
114
|
+
output: record.output,
|
|
115
|
+
result: result as MetricResult,
|
|
116
|
+
metricName: record.metric_name,
|
|
117
|
+
instructions: record.instructions,
|
|
118
|
+
testInfo: testInfo as TestInfo | undefined,
|
|
119
|
+
globalRunId: record.global_run_id,
|
|
120
|
+
runId: record.run_id,
|
|
121
|
+
createdAt:
|
|
122
|
+
typeof record.created_at === 'string'
|
|
123
|
+
? record.created_at
|
|
124
|
+
: record.created_at instanceof Date
|
|
125
|
+
? record.created_at.toISOString()
|
|
126
|
+
: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
19
130
|
getTraces(_input: {
|
|
20
131
|
name?: string;
|
|
21
132
|
scope?: string;
|
|
22
133
|
page: number;
|
|
23
134
|
perPage: number;
|
|
24
135
|
attributes?: Record<string, string>;
|
|
136
|
+
filters?: Record<string, any>;
|
|
25
137
|
}): Promise<any[]> {
|
|
26
138
|
throw new Error('Method not implemented.');
|
|
27
139
|
}
|
|
140
|
+
|
|
28
141
|
private redis: Redis;
|
|
29
142
|
|
|
30
143
|
constructor(config: UpstashConfig) {
|
|
@@ -76,9 +189,17 @@ export class UpstashStore extends MastraStorage {
|
|
|
76
189
|
async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
|
|
77
190
|
let key: string;
|
|
78
191
|
|
|
79
|
-
if (tableName ===
|
|
192
|
+
if (tableName === TABLE_MESSAGES) {
|
|
80
193
|
// For messages, use threadId as the primary key component
|
|
81
194
|
key = this.getKey(tableName, { threadId: record.threadId, id: record.id });
|
|
195
|
+
} else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
196
|
+
key = this.getKey(tableName, {
|
|
197
|
+
namespace: record.namespace || 'workflows',
|
|
198
|
+
workflow_name: record.workflow_name,
|
|
199
|
+
run_id: record.run_id,
|
|
200
|
+
});
|
|
201
|
+
} else if (tableName === TABLE_EVALS) {
|
|
202
|
+
key = this.getKey(tableName, { id: record.run_id });
|
|
82
203
|
} else {
|
|
83
204
|
key = this.getKey(tableName, { id: record.id });
|
|
84
205
|
}
|
|
@@ -101,7 +222,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
101
222
|
|
|
102
223
|
async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
|
|
103
224
|
const thread = await this.load<StorageThreadType>({
|
|
104
|
-
tableName:
|
|
225
|
+
tableName: TABLE_THREADS,
|
|
105
226
|
keys: { id: threadId },
|
|
106
227
|
});
|
|
107
228
|
|
|
@@ -116,7 +237,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
116
237
|
}
|
|
117
238
|
|
|
118
239
|
async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
|
|
119
|
-
const pattern = `${
|
|
240
|
+
const pattern = `${TABLE_THREADS}:*`;
|
|
120
241
|
const keys = await this.redis.keys(pattern);
|
|
121
242
|
const threads = await Promise.all(
|
|
122
243
|
keys.map(async key => {
|
|
@@ -137,7 +258,7 @@ export class UpstashStore extends MastraStorage {
|
|
|
137
258
|
|
|
138
259
|
async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
|
|
139
260
|
await this.insert({
|
|
140
|
-
tableName:
|
|
261
|
+
tableName: TABLE_THREADS,
|
|
141
262
|
record: thread,
|
|
142
263
|
});
|
|
143
264
|
return thread;
|
|
@@ -171,12 +292,12 @@ export class UpstashStore extends MastraStorage {
|
|
|
171
292
|
}
|
|
172
293
|
|
|
173
294
|
async deleteThread({ threadId }: { threadId: string }): Promise<void> {
|
|
174
|
-
const key = this.getKey(
|
|
295
|
+
const key = this.getKey(TABLE_THREADS, { id: threadId });
|
|
175
296
|
await this.redis.del(key);
|
|
176
297
|
}
|
|
177
298
|
|
|
178
299
|
private getMessageKey(threadId: string, messageId: string): string {
|
|
179
|
-
return this.getKey(
|
|
300
|
+
return this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
|
|
180
301
|
}
|
|
181
302
|
|
|
182
303
|
private getThreadMessagesKey(threadId: string): string {
|
|
@@ -275,12 +396,17 @@ export class UpstashStore extends MastraStorage {
|
|
|
275
396
|
snapshot: WorkflowRunState;
|
|
276
397
|
}): Promise<void> {
|
|
277
398
|
const { namespace = 'workflows', workflowName, runId, snapshot } = params;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
399
|
+
await this.insert({
|
|
400
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
401
|
+
record: {
|
|
402
|
+
namespace,
|
|
403
|
+
workflow_name: workflowName,
|
|
404
|
+
run_id: runId,
|
|
405
|
+
snapshot,
|
|
406
|
+
createdAt: new Date(),
|
|
407
|
+
updatedAt: new Date(),
|
|
408
|
+
},
|
|
282
409
|
});
|
|
283
|
-
await this.redis.set(key, snapshot); // Store snapshot directly without wrapping
|
|
284
410
|
}
|
|
285
411
|
|
|
286
412
|
async loadWorkflowSnapshot(params: {
|
|
@@ -289,13 +415,104 @@ export class UpstashStore extends MastraStorage {
|
|
|
289
415
|
runId: string;
|
|
290
416
|
}): Promise<WorkflowRunState | null> {
|
|
291
417
|
const { namespace = 'workflows', workflowName, runId } = params;
|
|
292
|
-
const key = this.getKey(
|
|
418
|
+
const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, {
|
|
293
419
|
namespace,
|
|
294
420
|
workflow_name: workflowName,
|
|
295
421
|
run_id: runId,
|
|
296
422
|
});
|
|
297
|
-
const data = await this.redis.get<
|
|
298
|
-
|
|
423
|
+
const data = await this.redis.get<{
|
|
424
|
+
namespace: string;
|
|
425
|
+
workflow_name: string;
|
|
426
|
+
run_id: string;
|
|
427
|
+
snapshot: WorkflowRunState;
|
|
428
|
+
}>(key);
|
|
429
|
+
if (!data) return null;
|
|
430
|
+
return data.snapshot;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async getWorkflowRuns(
|
|
434
|
+
{
|
|
435
|
+
namespace,
|
|
436
|
+
workflowName,
|
|
437
|
+
fromDate,
|
|
438
|
+
toDate,
|
|
439
|
+
limit,
|
|
440
|
+
offset,
|
|
441
|
+
}: {
|
|
442
|
+
namespace: string;
|
|
443
|
+
workflowName?: string;
|
|
444
|
+
fromDate?: Date;
|
|
445
|
+
toDate?: Date;
|
|
446
|
+
limit?: number;
|
|
447
|
+
offset?: number;
|
|
448
|
+
} = { namespace: 'workflows' },
|
|
449
|
+
): Promise<{
|
|
450
|
+
runs: Array<{
|
|
451
|
+
workflowName: string;
|
|
452
|
+
runId: string;
|
|
453
|
+
snapshot: WorkflowRunState | string;
|
|
454
|
+
createdAt: Date;
|
|
455
|
+
updatedAt: Date;
|
|
456
|
+
}>;
|
|
457
|
+
total: number;
|
|
458
|
+
}> {
|
|
459
|
+
// Get all workflow keys
|
|
460
|
+
const pattern = workflowName
|
|
461
|
+
? this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName }) + ':*'
|
|
462
|
+
: this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace }) + ':*';
|
|
463
|
+
|
|
464
|
+
const keys = await this.redis.keys(pattern);
|
|
465
|
+
|
|
466
|
+
// Get all workflow data
|
|
467
|
+
const workflows = await Promise.all(
|
|
468
|
+
keys.map(async key => {
|
|
469
|
+
const data = await this.redis.get<{
|
|
470
|
+
workflow_name: string;
|
|
471
|
+
run_id: string;
|
|
472
|
+
snapshot: WorkflowRunState | string;
|
|
473
|
+
createdAt: string | Date;
|
|
474
|
+
updatedAt: string | Date;
|
|
475
|
+
}>(key);
|
|
476
|
+
return data;
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// Filter and transform results
|
|
481
|
+
let runs = workflows
|
|
482
|
+
.filter(w => w !== null)
|
|
483
|
+
.map(w => {
|
|
484
|
+
let parsedSnapshot: WorkflowRunState | string = w!.snapshot as string;
|
|
485
|
+
if (typeof parsedSnapshot === 'string') {
|
|
486
|
+
try {
|
|
487
|
+
parsedSnapshot = JSON.parse(w!.snapshot as string) as WorkflowRunState;
|
|
488
|
+
} catch (_e) {
|
|
489
|
+
// If parsing fails, return the raw snapshot string
|
|
490
|
+
console.warn(`Failed to parse snapshot for workflow ${w!.workflow_name}:`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
workflowName: w!.workflow_name,
|
|
495
|
+
runId: w!.run_id,
|
|
496
|
+
snapshot: parsedSnapshot,
|
|
497
|
+
createdAt: this.ensureDate(w!.createdAt)!,
|
|
498
|
+
updatedAt: this.ensureDate(w!.updatedAt)!,
|
|
499
|
+
};
|
|
500
|
+
})
|
|
501
|
+
.filter(w => {
|
|
502
|
+
if (fromDate && w.createdAt < fromDate) return false;
|
|
503
|
+
if (toDate && w.createdAt > toDate) return false;
|
|
504
|
+
return true;
|
|
505
|
+
})
|
|
506
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
507
|
+
|
|
508
|
+
const total = runs.length;
|
|
509
|
+
|
|
510
|
+
// Apply pagination if requested
|
|
511
|
+
if (limit !== undefined && offset !== undefined) {
|
|
512
|
+
runs = runs.slice(offset, offset + limit);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return { runs, total };
|
|
299
516
|
}
|
|
300
517
|
|
|
301
518
|
async close(): Promise<void> {
|