@mastra/upstash 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +26 -0
- package/dist/_tsup-dts-rollup.d.cts +27 -14
- package/dist/_tsup-dts-rollup.d.ts +27 -14
- package/dist/index.cjs +181 -126
- package/dist/index.js +182 -127
- package/package.json +3 -3
- package/src/storage/index.ts +217 -156
- package/src/storage/upstash.test.ts +28 -21
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MastraStorage,
|
|
1
|
+
import { MastraStorage, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT, TABLE_EVALS, TABLE_TRACES, TABLE_THREADS } from '@mastra/core/storage';
|
|
2
2
|
import { Redis } from '@upstash/redis';
|
|
3
3
|
import { MastraVector } from '@mastra/core/vector';
|
|
4
4
|
import { Index } from '@upstash/vector';
|
|
@@ -6,13 +6,157 @@ import { BaseFilterTranslator } from '@mastra/core/vector/filter';
|
|
|
6
6
|
|
|
7
7
|
// src/storage/index.ts
|
|
8
8
|
var UpstashStore = class extends MastraStorage {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
redis;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
super({ name: "Upstash" });
|
|
12
|
+
this.redis = new Redis({
|
|
13
|
+
url: config.url,
|
|
14
|
+
token: config.token
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
transformEvalRecord(record) {
|
|
18
|
+
let result = record.result;
|
|
19
|
+
if (typeof result === "string") {
|
|
20
|
+
try {
|
|
21
|
+
result = JSON.parse(result);
|
|
22
|
+
} catch {
|
|
23
|
+
console.warn("Failed to parse result JSON:");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let testInfo = record.test_info;
|
|
27
|
+
if (typeof testInfo === "string") {
|
|
28
|
+
try {
|
|
29
|
+
testInfo = JSON.parse(testInfo);
|
|
30
|
+
} catch {
|
|
31
|
+
console.warn("Failed to parse test_info JSON:");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
agentName: record.agent_name,
|
|
36
|
+
input: record.input,
|
|
37
|
+
output: record.output,
|
|
38
|
+
result,
|
|
39
|
+
metricName: record.metric_name,
|
|
40
|
+
instructions: record.instructions,
|
|
41
|
+
testInfo,
|
|
42
|
+
globalRunId: record.global_run_id,
|
|
43
|
+
runId: record.run_id,
|
|
44
|
+
createdAt: typeof record.created_at === "string" ? record.created_at : record.created_at instanceof Date ? record.created_at.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
parseJSON(value) {
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(value);
|
|
51
|
+
} catch {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
getKey(tableName, keys) {
|
|
58
|
+
const keyParts = Object.entries(keys).filter(([_, value]) => value !== void 0).map(([key, value]) => `${key}:${value}`);
|
|
59
|
+
return `${tableName}:${keyParts.join(":")}`;
|
|
60
|
+
}
|
|
61
|
+
ensureDate(date) {
|
|
62
|
+
if (!date) return void 0;
|
|
63
|
+
return date instanceof Date ? date : new Date(date);
|
|
64
|
+
}
|
|
65
|
+
serializeDate(date) {
|
|
66
|
+
if (!date) return void 0;
|
|
67
|
+
const dateObj = this.ensureDate(date);
|
|
68
|
+
return dateObj?.toISOString();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Scans for keys matching the given pattern using SCAN and returns them as an array.
|
|
72
|
+
* @param pattern Redis key pattern, e.g. "table:*"
|
|
73
|
+
* @param batchSize Number of keys to scan per batch (default: 1000)
|
|
74
|
+
*/
|
|
75
|
+
async scanKeys(pattern, batchSize = 1e4) {
|
|
76
|
+
let cursor = "0";
|
|
77
|
+
let keys = [];
|
|
78
|
+
do {
|
|
79
|
+
const [nextCursor, batch] = await this.redis.scan(cursor, {
|
|
80
|
+
match: pattern,
|
|
81
|
+
count: batchSize
|
|
82
|
+
});
|
|
83
|
+
keys.push(...batch);
|
|
84
|
+
cursor = nextCursor;
|
|
85
|
+
} while (cursor !== "0");
|
|
86
|
+
return keys;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Deletes all keys matching the given pattern using SCAN and DEL in batches.
|
|
90
|
+
* @param pattern Redis key pattern, e.g. "table:*"
|
|
91
|
+
* @param batchSize Number of keys to delete per batch (default: 1000)
|
|
92
|
+
*/
|
|
93
|
+
async scanAndDelete(pattern, batchSize = 1e4) {
|
|
94
|
+
let cursor = "0";
|
|
95
|
+
let totalDeleted = 0;
|
|
96
|
+
do {
|
|
97
|
+
const [nextCursor, keys] = await this.redis.scan(cursor, {
|
|
98
|
+
match: pattern,
|
|
99
|
+
count: batchSize
|
|
100
|
+
});
|
|
101
|
+
if (keys.length > 0) {
|
|
102
|
+
await this.redis.del(...keys);
|
|
103
|
+
totalDeleted += keys.length;
|
|
104
|
+
}
|
|
105
|
+
cursor = nextCursor;
|
|
106
|
+
} while (cursor !== "0");
|
|
107
|
+
return totalDeleted;
|
|
108
|
+
}
|
|
109
|
+
getMessageKey(threadId, messageId) {
|
|
110
|
+
return this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
|
|
111
|
+
}
|
|
112
|
+
getThreadMessagesKey(threadId) {
|
|
113
|
+
return `thread:${threadId}:messages`;
|
|
114
|
+
}
|
|
115
|
+
parseWorkflowRun(row) {
|
|
116
|
+
let parsedSnapshot = row.snapshot;
|
|
117
|
+
if (typeof parsedSnapshot === "string") {
|
|
118
|
+
try {
|
|
119
|
+
parsedSnapshot = JSON.parse(row.snapshot);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
workflowName: row.workflow_name,
|
|
126
|
+
runId: row.run_id,
|
|
127
|
+
snapshot: parsedSnapshot,
|
|
128
|
+
createdAt: this.ensureDate(row.createdAt),
|
|
129
|
+
updatedAt: this.ensureDate(row.updatedAt),
|
|
130
|
+
resourceId: row.resourceId
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
processRecord(tableName, record) {
|
|
134
|
+
let key;
|
|
135
|
+
if (tableName === TABLE_MESSAGES) {
|
|
136
|
+
key = this.getKey(tableName, { threadId: record.threadId, id: record.id });
|
|
137
|
+
} else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
138
|
+
key = this.getKey(tableName, {
|
|
139
|
+
namespace: record.namespace || "workflows",
|
|
140
|
+
workflow_name: record.workflow_name,
|
|
141
|
+
run_id: record.run_id,
|
|
142
|
+
...record.resourceId ? { resourceId: record.resourceId } : {}
|
|
143
|
+
});
|
|
144
|
+
} else if (tableName === TABLE_EVALS) {
|
|
145
|
+
key = this.getKey(tableName, { id: record.run_id });
|
|
146
|
+
} else {
|
|
147
|
+
key = this.getKey(tableName, { id: record.id });
|
|
148
|
+
}
|
|
149
|
+
const processedRecord = {
|
|
150
|
+
...record,
|
|
151
|
+
createdAt: this.serializeDate(record.createdAt),
|
|
152
|
+
updatedAt: this.serializeDate(record.updatedAt)
|
|
153
|
+
};
|
|
154
|
+
return { key, processedRecord };
|
|
11
155
|
}
|
|
12
156
|
async getEvalsByAgentName(agentName, type) {
|
|
13
157
|
try {
|
|
14
158
|
const pattern = `${TABLE_EVALS}:*`;
|
|
15
|
-
const keys = await this.
|
|
159
|
+
const keys = await this.scanKeys(pattern);
|
|
16
160
|
const evalRecords = await Promise.all(
|
|
17
161
|
keys.map(async (key) => {
|
|
18
162
|
const data = await this.redis.get(key);
|
|
@@ -56,36 +200,6 @@ var UpstashStore = class extends MastraStorage {
|
|
|
56
200
|
return [];
|
|
57
201
|
}
|
|
58
202
|
}
|
|
59
|
-
transformEvalRecord(record) {
|
|
60
|
-
let result = record.result;
|
|
61
|
-
if (typeof result === "string") {
|
|
62
|
-
try {
|
|
63
|
-
result = JSON.parse(result);
|
|
64
|
-
} catch {
|
|
65
|
-
console.warn("Failed to parse result JSON:");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
let testInfo = record.test_info;
|
|
69
|
-
if (typeof testInfo === "string") {
|
|
70
|
-
try {
|
|
71
|
-
testInfo = JSON.parse(testInfo);
|
|
72
|
-
} catch {
|
|
73
|
-
console.warn("Failed to parse test_info JSON:");
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
agentName: record.agent_name,
|
|
78
|
-
input: record.input,
|
|
79
|
-
output: record.output,
|
|
80
|
-
result,
|
|
81
|
-
metricName: record.metric_name,
|
|
82
|
-
instructions: record.instructions,
|
|
83
|
-
testInfo,
|
|
84
|
-
globalRunId: record.global_run_id,
|
|
85
|
-
runId: record.run_id,
|
|
86
|
-
createdAt: typeof record.created_at === "string" ? record.created_at : record.created_at instanceof Date ? record.created_at.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
203
|
async getTraces({
|
|
90
204
|
name,
|
|
91
205
|
scope,
|
|
@@ -101,7 +215,7 @@ var UpstashStore = class extends MastraStorage {
|
|
|
101
215
|
}) {
|
|
102
216
|
try {
|
|
103
217
|
const pattern = `${TABLE_TRACES}:*`;
|
|
104
|
-
const keys = await this.
|
|
218
|
+
const keys = await this.scanKeys(pattern);
|
|
105
219
|
const traceRecords = await Promise.all(
|
|
106
220
|
keys.map(async (key) => {
|
|
107
221
|
const data = await this.redis.get(key);
|
|
@@ -165,37 +279,6 @@ var UpstashStore = class extends MastraStorage {
|
|
|
165
279
|
return [];
|
|
166
280
|
}
|
|
167
281
|
}
|
|
168
|
-
parseJSON(value) {
|
|
169
|
-
if (typeof value === "string") {
|
|
170
|
-
try {
|
|
171
|
-
return JSON.parse(value);
|
|
172
|
-
} catch {
|
|
173
|
-
return value;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return value;
|
|
177
|
-
}
|
|
178
|
-
redis;
|
|
179
|
-
constructor(config) {
|
|
180
|
-
super({ name: "Upstash" });
|
|
181
|
-
this.redis = new Redis({
|
|
182
|
-
url: config.url,
|
|
183
|
-
token: config.token
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
getKey(tableName, keys) {
|
|
187
|
-
const keyParts = Object.entries(keys).filter(([_, value]) => value !== void 0).map(([key, value]) => `${key}:${value}`);
|
|
188
|
-
return `${tableName}:${keyParts.join(":")}`;
|
|
189
|
-
}
|
|
190
|
-
ensureDate(date) {
|
|
191
|
-
if (!date) return void 0;
|
|
192
|
-
return date instanceof Date ? date : new Date(date);
|
|
193
|
-
}
|
|
194
|
-
serializeDate(date) {
|
|
195
|
-
if (!date) return void 0;
|
|
196
|
-
const dateObj = this.ensureDate(date);
|
|
197
|
-
return dateObj?.toISOString();
|
|
198
|
-
}
|
|
199
282
|
async createTable({
|
|
200
283
|
tableName,
|
|
201
284
|
schema
|
|
@@ -204,34 +287,26 @@ var UpstashStore = class extends MastraStorage {
|
|
|
204
287
|
}
|
|
205
288
|
async clearTable({ tableName }) {
|
|
206
289
|
const pattern = `${tableName}:*`;
|
|
207
|
-
|
|
208
|
-
if (keys.length > 0) {
|
|
209
|
-
await this.redis.del(...keys);
|
|
210
|
-
}
|
|
290
|
+
await this.scanAndDelete(pattern);
|
|
211
291
|
}
|
|
212
292
|
async insert({ tableName, record }) {
|
|
213
|
-
|
|
214
|
-
if (tableName === TABLE_MESSAGES) {
|
|
215
|
-
key = this.getKey(tableName, { threadId: record.threadId, id: record.id });
|
|
216
|
-
} else if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
217
|
-
key = this.getKey(tableName, {
|
|
218
|
-
namespace: record.namespace || "workflows",
|
|
219
|
-
workflow_name: record.workflow_name,
|
|
220
|
-
run_id: record.run_id,
|
|
221
|
-
...record.resourceId ? { resourceId: record.resourceId } : {}
|
|
222
|
-
});
|
|
223
|
-
} else if (tableName === TABLE_EVALS) {
|
|
224
|
-
key = this.getKey(tableName, { id: record.run_id });
|
|
225
|
-
} else {
|
|
226
|
-
key = this.getKey(tableName, { id: record.id });
|
|
227
|
-
}
|
|
228
|
-
const processedRecord = {
|
|
229
|
-
...record,
|
|
230
|
-
createdAt: this.serializeDate(record.createdAt),
|
|
231
|
-
updatedAt: this.serializeDate(record.updatedAt)
|
|
232
|
-
};
|
|
293
|
+
const { key, processedRecord } = this.processRecord(tableName, record);
|
|
233
294
|
await this.redis.set(key, processedRecord);
|
|
234
295
|
}
|
|
296
|
+
async batchInsert(input) {
|
|
297
|
+
const { tableName, records } = input;
|
|
298
|
+
if (!records.length) return;
|
|
299
|
+
const batchSize = 1e3;
|
|
300
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
301
|
+
const batch = records.slice(i, i + batchSize);
|
|
302
|
+
const pipeline = this.redis.pipeline();
|
|
303
|
+
for (const record of batch) {
|
|
304
|
+
const { key, processedRecord } = this.processRecord(tableName, record);
|
|
305
|
+
pipeline.set(key, processedRecord);
|
|
306
|
+
}
|
|
307
|
+
await pipeline.exec();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
235
310
|
async load({ tableName, keys }) {
|
|
236
311
|
const key = this.getKey(tableName, keys);
|
|
237
312
|
const data = await this.redis.get(key);
|
|
@@ -252,7 +327,7 @@ var UpstashStore = class extends MastraStorage {
|
|
|
252
327
|
}
|
|
253
328
|
async getThreadsByResourceId({ resourceId }) {
|
|
254
329
|
const pattern = `${TABLE_THREADS}:*`;
|
|
255
|
-
const keys = await this.
|
|
330
|
+
const keys = await this.scanKeys(pattern);
|
|
256
331
|
const threads = await Promise.all(
|
|
257
332
|
keys.map(async (key) => {
|
|
258
333
|
const data = await this.redis.get(key);
|
|
@@ -297,29 +372,27 @@ var UpstashStore = class extends MastraStorage {
|
|
|
297
372
|
const key = this.getKey(TABLE_THREADS, { id: threadId });
|
|
298
373
|
await this.redis.del(key);
|
|
299
374
|
}
|
|
300
|
-
getMessageKey(threadId, messageId) {
|
|
301
|
-
return this.getKey(TABLE_MESSAGES, { threadId, id: messageId });
|
|
302
|
-
}
|
|
303
|
-
getThreadMessagesKey(threadId) {
|
|
304
|
-
return `thread:${threadId}:messages`;
|
|
305
|
-
}
|
|
306
375
|
async saveMessages({ messages }) {
|
|
307
376
|
if (messages.length === 0) return [];
|
|
308
|
-
const pipeline = this.redis.pipeline();
|
|
309
377
|
const messagesWithIndex = messages.map((message, index) => ({
|
|
310
378
|
...message,
|
|
311
379
|
_index: index
|
|
312
380
|
}));
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
pipeline.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
381
|
+
const batchSize = 1e3;
|
|
382
|
+
for (let i = 0; i < messagesWithIndex.length; i += batchSize) {
|
|
383
|
+
const batch = messagesWithIndex.slice(i, i + batchSize);
|
|
384
|
+
const pipeline = this.redis.pipeline();
|
|
385
|
+
for (const message of batch) {
|
|
386
|
+
const key = this.getMessageKey(message.threadId, message.id);
|
|
387
|
+
const score = message._index !== void 0 ? message._index : new Date(message.createdAt).getTime();
|
|
388
|
+
pipeline.set(key, message);
|
|
389
|
+
pipeline.zadd(this.getThreadMessagesKey(message.threadId), {
|
|
390
|
+
score,
|
|
391
|
+
member: message.id
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
await pipeline.exec();
|
|
321
395
|
}
|
|
322
|
-
await pipeline.exec();
|
|
323
396
|
return messages;
|
|
324
397
|
}
|
|
325
398
|
async getMessages({ threadId, selectBy }) {
|
|
@@ -383,24 +456,6 @@ var UpstashStore = class extends MastraStorage {
|
|
|
383
456
|
if (!data) return null;
|
|
384
457
|
return data.snapshot;
|
|
385
458
|
}
|
|
386
|
-
parseWorkflowRun(row) {
|
|
387
|
-
let parsedSnapshot = row.snapshot;
|
|
388
|
-
if (typeof parsedSnapshot === "string") {
|
|
389
|
-
try {
|
|
390
|
-
parsedSnapshot = JSON.parse(row.snapshot);
|
|
391
|
-
} catch (e) {
|
|
392
|
-
console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return {
|
|
396
|
-
workflowName: row.workflow_name,
|
|
397
|
-
runId: row.run_id,
|
|
398
|
-
snapshot: parsedSnapshot,
|
|
399
|
-
createdAt: this.ensureDate(row.createdAt),
|
|
400
|
-
updatedAt: this.ensureDate(row.updatedAt),
|
|
401
|
-
resourceId: row.resourceId
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
459
|
async getWorkflowRuns({
|
|
405
460
|
namespace,
|
|
406
461
|
workflowName,
|
|
@@ -424,7 +479,7 @@ var UpstashStore = class extends MastraStorage {
|
|
|
424
479
|
} else if (resourceId) {
|
|
425
480
|
pattern = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: "*", run_id: "*", resourceId });
|
|
426
481
|
}
|
|
427
|
-
const keys = await this.
|
|
482
|
+
const keys = await this.scanKeys(pattern);
|
|
428
483
|
const workflows = await Promise.all(
|
|
429
484
|
keys.map(async (key) => {
|
|
430
485
|
const data = await this.redis.get(key);
|
|
@@ -453,7 +508,7 @@ var UpstashStore = class extends MastraStorage {
|
|
|
453
508
|
}) {
|
|
454
509
|
try {
|
|
455
510
|
const key = this.getKey(TABLE_WORKFLOW_SNAPSHOT, { namespace, workflow_name: workflowName, run_id: runId }) + "*";
|
|
456
|
-
const keys = await this.
|
|
511
|
+
const keys = await this.scanKeys(key);
|
|
457
512
|
const workflows = await Promise.all(
|
|
458
513
|
keys.map(async (key2) => {
|
|
459
514
|
const data2 = await this.redis.get(key2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/upstash",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Upstash provider for Mastra - includes both vector and db storage capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"tsup": "^8.4.0",
|
|
32
32
|
"typescript": "^5.8.2",
|
|
33
33
|
"vitest": "^3.1.2",
|
|
34
|
-
"@
|
|
35
|
-
"@
|
|
34
|
+
"@internal/lint": "0.0.7",
|
|
35
|
+
"@mastra/core": "0.10.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@mastra/core": "^0.10.0"
|