@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.
@@ -1,7 +1,7 @@
1
1
  version: '3'
2
2
  services:
3
3
  redis:
4
- image: redis:7-alpine
4
+ image: redis:8-alpine
5
5
  ports:
6
6
  - '6379:6379'
7
7
  command: redis-server --requirepass redis_password
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/upstash",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Upstash provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,13 +27,13 @@
27
27
  "@microsoft/api-extractor": "^7.52.8",
28
28
  "@types/node": "^20.19.0",
29
29
  "dotenv": "^17.0.0",
30
- "eslint": "^9.29.0",
30
+ "eslint": "^9.30.1",
31
31
  "tsup": "^8.5.0",
32
32
  "typescript": "^5.8.3",
33
33
  "vitest": "^3.2.4",
34
- "@internal/lint": "0.0.18",
35
- "@mastra/core": "0.10.11",
36
- "@internal/storage-test-utils": "0.0.14"
34
+ "@internal/storage-test-utils": "0.0.17",
35
+ "@internal/lint": "0.0.21",
36
+ "@mastra/core": "0.11.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@mastra/core": ">=0.10.7-0 <0.11.0-0"
@@ -0,0 +1,279 @@
1
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
2
+ import type { MetricResult, TestInfo } from '@mastra/core/eval';
3
+ import type { EvalRow, PaginationArgs, PaginationInfo } from '@mastra/core/storage';
4
+ import { LegacyEvalsStorage, TABLE_EVALS } from '@mastra/core/storage';
5
+ import type { Redis } from '@upstash/redis';
6
+ import type { StoreOperationsUpstash } from '../operations';
7
+
8
+ function transformEvalRecord(record: Record<string, any>): EvalRow {
9
+ // Parse JSON strings if needed
10
+ let result = record.result;
11
+ if (typeof result === 'string') {
12
+ try {
13
+ result = JSON.parse(result);
14
+ } catch {
15
+ console.warn('Failed to parse result JSON:');
16
+ }
17
+ }
18
+
19
+ let testInfo = record.test_info;
20
+ if (typeof testInfo === 'string') {
21
+ try {
22
+ testInfo = JSON.parse(testInfo);
23
+ } catch {
24
+ console.warn('Failed to parse test_info JSON:');
25
+ }
26
+ }
27
+
28
+ return {
29
+ agentName: record.agent_name,
30
+ input: record.input,
31
+ output: record.output,
32
+ result: result as MetricResult,
33
+ metricName: record.metric_name,
34
+ instructions: record.instructions,
35
+ testInfo: testInfo as TestInfo | undefined,
36
+ globalRunId: record.global_run_id,
37
+ runId: record.run_id,
38
+ createdAt:
39
+ typeof record.created_at === 'string'
40
+ ? record.created_at
41
+ : record.created_at instanceof Date
42
+ ? record.created_at.toISOString()
43
+ : new Date().toISOString(),
44
+ };
45
+ }
46
+
47
+ export class StoreLegacyEvalsUpstash extends LegacyEvalsStorage {
48
+ private client: Redis;
49
+ private operations: StoreOperationsUpstash;
50
+ constructor({ client, operations }: { client: Redis; operations: StoreOperationsUpstash }) {
51
+ super();
52
+ this.client = client;
53
+ this.operations = operations;
54
+ }
55
+
56
+ /**
57
+ * @deprecated Use getEvals instead
58
+ */
59
+ async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
60
+ try {
61
+ const pattern = `${TABLE_EVALS}:*`;
62
+ const keys = await this.operations.scanKeys(pattern);
63
+
64
+ // Check if we have any keys before using pipeline
65
+ if (keys.length === 0) {
66
+ return [];
67
+ }
68
+
69
+ // Use pipeline for batch fetching to improve performance
70
+ const pipeline = this.client.pipeline();
71
+ keys.forEach(key => pipeline.get(key));
72
+ const results = await pipeline.exec();
73
+
74
+ // Filter by agent name and remove nulls
75
+ const nonNullRecords = results.filter(
76
+ (record): record is Record<string, any> =>
77
+ record !== null && typeof record === 'object' && 'agent_name' in record && record.agent_name === agentName,
78
+ );
79
+
80
+ let filteredEvals = nonNullRecords;
81
+
82
+ if (type === 'test') {
83
+ filteredEvals = filteredEvals.filter(record => {
84
+ if (!record.test_info) return false;
85
+
86
+ // Handle test_info as a JSON string
87
+ try {
88
+ if (typeof record.test_info === 'string') {
89
+ const parsedTestInfo = JSON.parse(record.test_info);
90
+ return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
91
+ }
92
+
93
+ // Handle test_info as an object
94
+ return typeof record.test_info === 'object' && 'testPath' in record.test_info;
95
+ } catch {
96
+ return false;
97
+ }
98
+ });
99
+ } else if (type === 'live') {
100
+ filteredEvals = filteredEvals.filter(record => {
101
+ if (!record.test_info) return true;
102
+
103
+ // Handle test_info as a JSON string
104
+ try {
105
+ if (typeof record.test_info === 'string') {
106
+ const parsedTestInfo = JSON.parse(record.test_info);
107
+ return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
108
+ }
109
+
110
+ // Handle test_info as an object
111
+ return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
112
+ } catch {
113
+ return true;
114
+ }
115
+ });
116
+ }
117
+
118
+ // Transform to EvalRow format
119
+ return filteredEvals.map(record => transformEvalRecord(record));
120
+ } catch (error) {
121
+ const mastraError = new MastraError(
122
+ {
123
+ id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_BY_AGENT_NAME_FAILED',
124
+ domain: ErrorDomain.STORAGE,
125
+ category: ErrorCategory.THIRD_PARTY,
126
+ details: { agentName },
127
+ },
128
+ error,
129
+ );
130
+ this.logger?.trackException(mastraError);
131
+ this.logger.error(mastraError.toString());
132
+ return [];
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get all evaluations with pagination and total count
138
+ * @param options Pagination and filtering options
139
+ * @returns Object with evals array and total count
140
+ */
141
+ async getEvals(
142
+ options?: {
143
+ agentName?: string;
144
+ type?: 'test' | 'live';
145
+ } & PaginationArgs,
146
+ ): Promise<PaginationInfo & { evals: EvalRow[] }> {
147
+ try {
148
+ // Default pagination parameters
149
+ const { agentName, type, page = 0, perPage = 100, dateRange } = options || {};
150
+ const fromDate = dateRange?.start;
151
+ const toDate = dateRange?.end;
152
+
153
+ // Get all keys that match the evals table pattern using cursor-based scanning
154
+ const pattern = `${TABLE_EVALS}:*`;
155
+ const keys = await this.operations.scanKeys(pattern);
156
+
157
+ // Check if we have any keys before using pipeline
158
+ if (keys.length === 0) {
159
+ return {
160
+ evals: [],
161
+ total: 0,
162
+ page,
163
+ perPage,
164
+ hasMore: false,
165
+ };
166
+ }
167
+
168
+ // Use pipeline for batch fetching to improve performance
169
+ const pipeline = this.client.pipeline();
170
+ keys.forEach(key => pipeline.get(key));
171
+ const results = await pipeline.exec();
172
+
173
+ // Process results and apply filters
174
+ let filteredEvals = results
175
+ .map((result: any) => result as Record<string, any> | null)
176
+ .filter((record): record is Record<string, any> => record !== null && typeof record === 'object');
177
+
178
+ // Apply agent name filter if provided
179
+ if (agentName) {
180
+ filteredEvals = filteredEvals.filter(record => record.agent_name === agentName);
181
+ }
182
+
183
+ // Apply type filter if provided
184
+ if (type === 'test') {
185
+ filteredEvals = filteredEvals.filter(record => {
186
+ if (!record.test_info) return false;
187
+
188
+ try {
189
+ if (typeof record.test_info === 'string') {
190
+ const parsedTestInfo = JSON.parse(record.test_info);
191
+ return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
192
+ }
193
+ return typeof record.test_info === 'object' && 'testPath' in record.test_info;
194
+ } catch {
195
+ return false;
196
+ }
197
+ });
198
+ } else if (type === 'live') {
199
+ filteredEvals = filteredEvals.filter(record => {
200
+ if (!record.test_info) return true;
201
+
202
+ try {
203
+ if (typeof record.test_info === 'string') {
204
+ const parsedTestInfo = JSON.parse(record.test_info);
205
+ return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
206
+ }
207
+ return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
208
+ } catch {
209
+ return true;
210
+ }
211
+ });
212
+ }
213
+
214
+ // Apply date filters if provided
215
+ if (fromDate) {
216
+ filteredEvals = filteredEvals.filter(record => {
217
+ const createdAt = new Date(record.created_at || record.createdAt || 0);
218
+ return createdAt.getTime() >= fromDate.getTime();
219
+ });
220
+ }
221
+
222
+ if (toDate) {
223
+ filteredEvals = filteredEvals.filter(record => {
224
+ const createdAt = new Date(record.created_at || record.createdAt || 0);
225
+ return createdAt.getTime() <= toDate.getTime();
226
+ });
227
+ }
228
+
229
+ // Sort by creation date (newest first)
230
+ filteredEvals.sort((a, b) => {
231
+ const dateA = new Date(a.created_at || a.createdAt || 0).getTime();
232
+ const dateB = new Date(b.created_at || b.createdAt || 0).getTime();
233
+ return dateB - dateA;
234
+ });
235
+
236
+ const total = filteredEvals.length;
237
+
238
+ // Apply pagination
239
+ const start = page * perPage;
240
+ const end = start + perPage;
241
+ const paginatedEvals = filteredEvals.slice(start, end);
242
+ const hasMore = end < total;
243
+
244
+ // Transform to EvalRow format
245
+ const evals = paginatedEvals.map(record => transformEvalRecord(record));
246
+
247
+ return {
248
+ evals,
249
+ total,
250
+ page,
251
+ perPage,
252
+ hasMore,
253
+ };
254
+ } catch (error) {
255
+ const { page = 0, perPage = 100 } = options || {};
256
+ const mastraError = new MastraError(
257
+ {
258
+ id: 'STORAGE_UPSTASH_STORAGE_GET_EVALS_FAILED',
259
+ domain: ErrorDomain.STORAGE,
260
+ category: ErrorCategory.THIRD_PARTY,
261
+ details: {
262
+ page,
263
+ perPage,
264
+ },
265
+ },
266
+ error,
267
+ );
268
+ this.logger.error(mastraError.toString());
269
+ this.logger?.trackException(mastraError);
270
+ return {
271
+ evals: [],
272
+ total: 0,
273
+ page,
274
+ perPage,
275
+ hasMore: false,
276
+ };
277
+ }
278
+ }
279
+ }