@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.
@@ -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 { MastraStorage } from '@mastra/core/storage';
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
- getEvalsByAgentName(_agentName: string, _type?: 'test' | 'live'): Promise<EvalRow[]> {
17
- throw new Error('Method not implemented.');
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 === MastraStorage.TABLE_MESSAGES) {
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: MastraStorage.TABLE_THREADS,
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 = `${MastraStorage.TABLE_THREADS}:*`;
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: MastraStorage.TABLE_THREADS,
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(MastraStorage.TABLE_THREADS, { id: threadId });
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(MastraStorage.TABLE_MESSAGES, { threadId, id: messageId });
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
- const key = this.getKey(MastraStorage.TABLE_WORKFLOW_SNAPSHOT, {
279
- namespace,
280
- workflow_name: workflowName,
281
- run_id: runId,
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(MastraStorage.TABLE_WORKFLOW_SNAPSHOT, {
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<WorkflowRunState>(key);
298
- return data || null;
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> {