@mastra/upstash 0.10.2 → 0.10.3-alpha.0
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 +8 -8
- package/CHANGELOG.md +15 -0
- package/PAGINATION.md +397 -0
- package/dist/_tsup-dts-rollup.d.cts +69 -4
- package/dist/_tsup-dts-rollup.d.ts +69 -4
- package/dist/index.cjs +1561 -1009
- package/dist/index.js +1561 -1009
- package/package.json +9 -9
- package/src/storage/index.ts +543 -108
- package/src/storage/upstash.test.ts +313 -0
package/src/storage/index.ts
CHANGED
|
@@ -204,27 +204,30 @@ export class UpstashStore extends MastraStorage {
|
|
|
204
204
|
return { key, processedRecord };
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* @deprecated Use getEvals instead
|
|
209
|
+
*/
|
|
207
210
|
async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
|
|
208
211
|
try {
|
|
209
|
-
// Get all keys that match the evals table pattern
|
|
210
212
|
const pattern = `${TABLE_EVALS}:*`;
|
|
211
213
|
const keys = await this.scanKeys(pattern);
|
|
212
214
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
215
|
+
// Check if we have any keys before using pipeline
|
|
216
|
+
if (keys.length === 0) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Use pipeline for batch fetching to improve performance
|
|
221
|
+
const pipeline = this.redis.pipeline();
|
|
222
|
+
keys.forEach(key => pipeline.get(key));
|
|
223
|
+
const results = await pipeline.exec();
|
|
220
224
|
|
|
221
225
|
// Filter by agent name and remove nulls
|
|
222
|
-
const nonNullRecords =
|
|
226
|
+
const nonNullRecords = results.filter(
|
|
223
227
|
(record): record is Record<string, any> =>
|
|
224
228
|
record !== null && typeof record === 'object' && 'agent_name' in record && record.agent_name === agentName,
|
|
225
229
|
);
|
|
226
230
|
|
|
227
|
-
// Apply additional filtering based on type
|
|
228
231
|
let filteredEvals = nonNullRecords;
|
|
229
232
|
|
|
230
233
|
if (type === 'test') {
|
|
@@ -271,103 +274,126 @@ export class UpstashStore extends MastraStorage {
|
|
|
271
274
|
}
|
|
272
275
|
}
|
|
273
276
|
|
|
274
|
-
async getTraces(
|
|
275
|
-
|
|
277
|
+
public async getTraces(args: {
|
|
278
|
+
name?: string;
|
|
279
|
+
scope?: string;
|
|
280
|
+
attributes?: Record<string, string>;
|
|
281
|
+
filters?: Record<string, any>;
|
|
282
|
+
page: number;
|
|
283
|
+
perPage?: number;
|
|
284
|
+
fromDate?: Date;
|
|
285
|
+
toDate?: Date;
|
|
286
|
+
}): Promise<any[]>;
|
|
287
|
+
public async getTraces(args: {
|
|
288
|
+
name?: string;
|
|
289
|
+
scope?: string;
|
|
290
|
+
page: number;
|
|
291
|
+
perPage?: number;
|
|
292
|
+
attributes?: Record<string, string>;
|
|
293
|
+
filters?: Record<string, any>;
|
|
294
|
+
fromDate?: Date;
|
|
295
|
+
toDate?: Date;
|
|
296
|
+
returnPaginationResults: true;
|
|
297
|
+
}): Promise<{
|
|
298
|
+
traces: any[];
|
|
299
|
+
total: number;
|
|
300
|
+
page: number;
|
|
301
|
+
perPage: number;
|
|
302
|
+
hasMore: boolean;
|
|
303
|
+
}>;
|
|
304
|
+
public async getTraces(args: {
|
|
305
|
+
name?: string;
|
|
306
|
+
scope?: string;
|
|
307
|
+
page: number;
|
|
308
|
+
perPage?: number;
|
|
309
|
+
attributes?: Record<string, string>;
|
|
310
|
+
filters?: Record<string, any>;
|
|
311
|
+
fromDate?: Date;
|
|
312
|
+
toDate?: Date;
|
|
313
|
+
returnPaginationResults?: boolean;
|
|
314
|
+
}): Promise<
|
|
315
|
+
| any[]
|
|
316
|
+
| {
|
|
317
|
+
traces: any[];
|
|
318
|
+
total: number;
|
|
319
|
+
page: number;
|
|
320
|
+
perPage: number;
|
|
321
|
+
hasMore: boolean;
|
|
322
|
+
}
|
|
323
|
+
> {
|
|
324
|
+
const {
|
|
276
325
|
name,
|
|
277
326
|
scope,
|
|
278
|
-
page
|
|
279
|
-
perPage
|
|
327
|
+
page,
|
|
328
|
+
perPage: perPageInput,
|
|
280
329
|
attributes,
|
|
281
330
|
filters,
|
|
282
331
|
fromDate,
|
|
283
332
|
toDate,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
attributes?: Record<string, string>;
|
|
290
|
-
filters?: Record<string, any>;
|
|
291
|
-
fromDate?: Date;
|
|
292
|
-
toDate?: Date;
|
|
293
|
-
} = {
|
|
294
|
-
page: 0,
|
|
295
|
-
perPage: 100,
|
|
296
|
-
},
|
|
297
|
-
): Promise<any[]> {
|
|
333
|
+
returnPaginationResults,
|
|
334
|
+
} = args;
|
|
335
|
+
|
|
336
|
+
const perPage = perPageInput !== undefined ? perPageInput : 100;
|
|
337
|
+
|
|
298
338
|
try {
|
|
299
|
-
// Get all keys that match the traces table pattern
|
|
300
339
|
const pattern = `${TABLE_TRACES}:*`;
|
|
301
340
|
const keys = await this.scanKeys(pattern);
|
|
302
341
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
342
|
+
if (keys.length === 0) {
|
|
343
|
+
if (returnPaginationResults) {
|
|
344
|
+
return {
|
|
345
|
+
traces: [],
|
|
346
|
+
total: 0,
|
|
347
|
+
page,
|
|
348
|
+
perPage: perPage || 100,
|
|
349
|
+
hasMore: false,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const pipeline = this.redis.pipeline();
|
|
356
|
+
keys.forEach(key => pipeline.get(key));
|
|
357
|
+
const results = await pipeline.exec();
|
|
310
358
|
|
|
311
|
-
|
|
312
|
-
let filteredTraces = traceRecords.filter(
|
|
359
|
+
let filteredTraces = results.filter(
|
|
313
360
|
(record): record is Record<string, any> => record !== null && typeof record === 'object',
|
|
314
361
|
);
|
|
315
362
|
|
|
316
|
-
// Apply name filter if provided
|
|
317
363
|
if (name) {
|
|
318
364
|
filteredTraces = filteredTraces.filter(record => record.name?.toLowerCase().startsWith(name.toLowerCase()));
|
|
319
365
|
}
|
|
320
|
-
|
|
321
|
-
// Apply scope filter if provided
|
|
322
366
|
if (scope) {
|
|
323
367
|
filteredTraces = filteredTraces.filter(record => record.scope === scope);
|
|
324
368
|
}
|
|
325
|
-
|
|
326
|
-
// Apply attributes filter if provided
|
|
327
369
|
if (attributes) {
|
|
328
370
|
filteredTraces = filteredTraces.filter(record => {
|
|
329
371
|
const recordAttributes = record.attributes;
|
|
330
372
|
if (!recordAttributes) return false;
|
|
331
|
-
|
|
332
|
-
// Parse attributes if stored as string
|
|
333
373
|
const parsedAttributes =
|
|
334
374
|
typeof recordAttributes === 'string' ? JSON.parse(recordAttributes) : recordAttributes;
|
|
335
|
-
|
|
336
375
|
return Object.entries(attributes).every(([key, value]) => parsedAttributes[key] === value);
|
|
337
376
|
});
|
|
338
377
|
}
|
|
339
|
-
|
|
340
|
-
// Apply custom filters if provided
|
|
341
378
|
if (filters) {
|
|
342
379
|
filteredTraces = filteredTraces.filter(record =>
|
|
343
380
|
Object.entries(filters).every(([key, value]) => record[key] === value),
|
|
344
381
|
);
|
|
345
382
|
}
|
|
346
|
-
|
|
347
|
-
// Apply fromDate filter if provided
|
|
348
383
|
if (fromDate) {
|
|
349
384
|
filteredTraces = filteredTraces.filter(
|
|
350
385
|
record => new Date(record.createdAt).getTime() >= new Date(fromDate).getTime(),
|
|
351
386
|
);
|
|
352
387
|
}
|
|
353
|
-
|
|
354
|
-
// Apply toDate filter if provided
|
|
355
388
|
if (toDate) {
|
|
356
389
|
filteredTraces = filteredTraces.filter(
|
|
357
390
|
record => new Date(record.createdAt).getTime() <= new Date(toDate).getTime(),
|
|
358
391
|
);
|
|
359
392
|
}
|
|
360
393
|
|
|
361
|
-
// Sort traces by creation date (newest first)
|
|
362
394
|
filteredTraces.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
363
395
|
|
|
364
|
-
|
|
365
|
-
const start = page * perPage;
|
|
366
|
-
const end = start + perPage;
|
|
367
|
-
const paginatedTraces = filteredTraces.slice(start, end);
|
|
368
|
-
|
|
369
|
-
// Transform and return the traces
|
|
370
|
-
return paginatedTraces.map(record => ({
|
|
396
|
+
const transformedTraces = filteredTraces.map(record => ({
|
|
371
397
|
id: record.id,
|
|
372
398
|
parentSpanId: record.parentSpanId,
|
|
373
399
|
traceId: record.traceId,
|
|
@@ -383,8 +409,35 @@ export class UpstashStore extends MastraStorage {
|
|
|
383
409
|
other: this.parseJSON(record.other),
|
|
384
410
|
createdAt: this.ensureDate(record.createdAt),
|
|
385
411
|
}));
|
|
412
|
+
|
|
413
|
+
const total = transformedTraces.length;
|
|
414
|
+
const resolvedPerPage = perPage || 100;
|
|
415
|
+
const start = page * resolvedPerPage;
|
|
416
|
+
const end = start + resolvedPerPage;
|
|
417
|
+
const paginatedTraces = transformedTraces.slice(start, end);
|
|
418
|
+
const hasMore = end < total;
|
|
419
|
+
if (returnPaginationResults) {
|
|
420
|
+
return {
|
|
421
|
+
traces: paginatedTraces,
|
|
422
|
+
total,
|
|
423
|
+
page,
|
|
424
|
+
perPage: resolvedPerPage,
|
|
425
|
+
hasMore,
|
|
426
|
+
};
|
|
427
|
+
} else {
|
|
428
|
+
return paginatedTraces;
|
|
429
|
+
}
|
|
386
430
|
} catch (error) {
|
|
387
431
|
console.error('Failed to get traces:', error);
|
|
432
|
+
if (returnPaginationResults) {
|
|
433
|
+
return {
|
|
434
|
+
traces: [],
|
|
435
|
+
total: 0,
|
|
436
|
+
page,
|
|
437
|
+
perPage: perPage || 100,
|
|
438
|
+
hasMore: false,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
388
441
|
return [];
|
|
389
442
|
}
|
|
390
443
|
}
|
|
@@ -450,24 +503,97 @@ export class UpstashStore extends MastraStorage {
|
|
|
450
503
|
};
|
|
451
504
|
}
|
|
452
505
|
|
|
453
|
-
async getThreadsByResourceId(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
506
|
+
async getThreadsByResourceId(args: { resourceId: string }): Promise<StorageThreadType[]>;
|
|
507
|
+
async getThreadsByResourceId(args: { resourceId: string; page: number; perPage?: number }): Promise<{
|
|
508
|
+
threads: StorageThreadType[];
|
|
509
|
+
total: number;
|
|
510
|
+
page: number;
|
|
511
|
+
perPage: number;
|
|
512
|
+
hasMore: boolean;
|
|
513
|
+
}>;
|
|
514
|
+
async getThreadsByResourceId(args: { resourceId: string; page?: number; perPage?: number }): Promise<
|
|
515
|
+
| StorageThreadType[]
|
|
516
|
+
| {
|
|
517
|
+
threads: StorageThreadType[];
|
|
518
|
+
total: number;
|
|
519
|
+
page: number;
|
|
520
|
+
perPage: number;
|
|
521
|
+
hasMore: boolean;
|
|
522
|
+
}
|
|
523
|
+
> {
|
|
524
|
+
const resourceId: string = args.resourceId;
|
|
525
|
+
const page: number | undefined = args.page;
|
|
526
|
+
// Determine perPage only if page is actually provided. Otherwise, its value is not critical for the non-paginated path.
|
|
527
|
+
// If page is provided, perPage defaults to 100 if not specified.
|
|
528
|
+
const perPage: number = page !== undefined ? (args.perPage !== undefined ? args.perPage : 100) : 100;
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const pattern = `${TABLE_THREADS}:*`;
|
|
532
|
+
const keys = await this.scanKeys(pattern);
|
|
533
|
+
|
|
534
|
+
if (keys.length === 0) {
|
|
535
|
+
if (page !== undefined) {
|
|
536
|
+
return {
|
|
537
|
+
threads: [],
|
|
538
|
+
total: 0,
|
|
539
|
+
page,
|
|
540
|
+
perPage, // perPage is number here
|
|
541
|
+
hasMore: false,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const allThreads: StorageThreadType[] = [];
|
|
548
|
+
const pipeline = this.redis.pipeline();
|
|
549
|
+
keys.forEach(key => pipeline.get(key));
|
|
550
|
+
const results = await pipeline.exec();
|
|
551
|
+
|
|
552
|
+
for (let i = 0; i < results.length; i++) {
|
|
553
|
+
const thread = results[i] as StorageThreadType | null;
|
|
554
|
+
if (thread && thread.resourceId === resourceId) {
|
|
555
|
+
allThreads.push({
|
|
556
|
+
...thread,
|
|
557
|
+
createdAt: this.ensureDate(thread.createdAt)!,
|
|
558
|
+
updatedAt: this.ensureDate(thread.updatedAt)!,
|
|
559
|
+
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
allThreads.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
565
|
+
|
|
566
|
+
if (page !== undefined) {
|
|
567
|
+
// If page is defined, perPage is also a number (due to the defaulting logic above)
|
|
568
|
+
const total = allThreads.length;
|
|
569
|
+
const start = page * perPage;
|
|
570
|
+
const end = start + perPage;
|
|
571
|
+
const paginatedThreads = allThreads.slice(start, end);
|
|
572
|
+
const hasMore = end < total;
|
|
573
|
+
return {
|
|
574
|
+
threads: paginatedThreads,
|
|
575
|
+
total,
|
|
576
|
+
page,
|
|
577
|
+
perPage,
|
|
578
|
+
hasMore,
|
|
579
|
+
};
|
|
580
|
+
} else {
|
|
581
|
+
// page is undefined, return all threads
|
|
582
|
+
return allThreads;
|
|
583
|
+
}
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error('Error in getThreadsByResourceId:', error);
|
|
586
|
+
if (page !== undefined) {
|
|
587
|
+
return {
|
|
588
|
+
threads: [],
|
|
589
|
+
total: 0,
|
|
590
|
+
page,
|
|
591
|
+
perPage, // perPage is number here
|
|
592
|
+
hasMore: false,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
471
597
|
}
|
|
472
598
|
|
|
473
599
|
async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
|
|
@@ -550,17 +676,155 @@ export class UpstashStore extends MastraStorage {
|
|
|
550
676
|
return list.get.all.v1();
|
|
551
677
|
}
|
|
552
678
|
|
|
679
|
+
// Function overloads for different return types
|
|
553
680
|
public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
|
|
554
681
|
public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
|
|
682
|
+
public async getMessages(
|
|
683
|
+
args: StorageGetMessagesArg & {
|
|
684
|
+
format?: 'v1' | 'v2';
|
|
685
|
+
page: number;
|
|
686
|
+
perPage?: number;
|
|
687
|
+
fromDate?: Date;
|
|
688
|
+
toDate?: Date;
|
|
689
|
+
},
|
|
690
|
+
): Promise<{
|
|
691
|
+
messages: MastraMessageV1[] | MastraMessageV2[];
|
|
692
|
+
total: number;
|
|
693
|
+
page: number;
|
|
694
|
+
perPage: number;
|
|
695
|
+
hasMore: boolean;
|
|
696
|
+
}>;
|
|
555
697
|
public async getMessages({
|
|
556
698
|
threadId,
|
|
557
699
|
selectBy,
|
|
558
700
|
format,
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
701
|
+
page,
|
|
702
|
+
perPage = 40,
|
|
703
|
+
fromDate,
|
|
704
|
+
toDate,
|
|
705
|
+
}: StorageGetMessagesArg & {
|
|
706
|
+
format?: 'v1' | 'v2';
|
|
707
|
+
page?: number;
|
|
708
|
+
perPage?: number;
|
|
709
|
+
fromDate?: Date;
|
|
710
|
+
toDate?: Date;
|
|
711
|
+
}): Promise<
|
|
712
|
+
| MastraMessageV1[]
|
|
713
|
+
| MastraMessageV2[]
|
|
714
|
+
| {
|
|
715
|
+
messages: MastraMessageV1[] | MastraMessageV2[];
|
|
716
|
+
total: number;
|
|
717
|
+
page: number;
|
|
718
|
+
perPage: number;
|
|
719
|
+
hasMore: boolean;
|
|
720
|
+
}
|
|
721
|
+
> {
|
|
562
722
|
const threadMessagesKey = this.getThreadMessagesKey(threadId);
|
|
563
723
|
|
|
724
|
+
const allMessageIds = await this.redis.zrange(threadMessagesKey, 0, -1);
|
|
725
|
+
// If pagination is requested, use the new pagination logic
|
|
726
|
+
if (page !== undefined) {
|
|
727
|
+
try {
|
|
728
|
+
// Get all message IDs from the sorted set
|
|
729
|
+
|
|
730
|
+
if (allMessageIds.length === 0) {
|
|
731
|
+
return {
|
|
732
|
+
messages: [],
|
|
733
|
+
total: 0,
|
|
734
|
+
page,
|
|
735
|
+
perPage,
|
|
736
|
+
hasMore: false,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Use pipeline to fetch all messages efficiently
|
|
741
|
+
const pipeline = this.redis.pipeline();
|
|
742
|
+
allMessageIds.forEach(id => pipeline.get(this.getMessageKey(threadId, id as string)));
|
|
743
|
+
const results = await pipeline.exec();
|
|
744
|
+
|
|
745
|
+
// Process messages and apply filters - handle undefined results from pipeline
|
|
746
|
+
let messages = results
|
|
747
|
+
.map((result: any) => result as MastraMessageV2 | null)
|
|
748
|
+
.filter((msg): msg is MastraMessageV2 => msg !== null) as (MastraMessageV2 & { _index?: number })[];
|
|
749
|
+
|
|
750
|
+
// Apply date filters if provided
|
|
751
|
+
if (fromDate) {
|
|
752
|
+
messages = messages.filter(msg => msg && new Date(msg.createdAt).getTime() >= fromDate.getTime());
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (toDate) {
|
|
756
|
+
messages = messages.filter(msg => msg && new Date(msg.createdAt).getTime() <= toDate.getTime());
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Sort messages by their position in the sorted set
|
|
760
|
+
messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
|
|
761
|
+
|
|
762
|
+
const total = messages.length;
|
|
763
|
+
|
|
764
|
+
// Apply pagination
|
|
765
|
+
const start = page * perPage;
|
|
766
|
+
const end = start + perPage;
|
|
767
|
+
const hasMore = end < total;
|
|
768
|
+
const paginatedMessages = messages.slice(start, end);
|
|
769
|
+
|
|
770
|
+
// Remove _index before returning and handle format conversion properly
|
|
771
|
+
const prepared = paginatedMessages
|
|
772
|
+
.filter(message => message !== null && message !== undefined)
|
|
773
|
+
.map(message => {
|
|
774
|
+
const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
|
|
775
|
+
return messageWithoutIndex as unknown as MastraMessageV1;
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Return pagination object with correct format
|
|
779
|
+
if (format === 'v2') {
|
|
780
|
+
// Convert V1 format back to V2 format
|
|
781
|
+
const v2Messages = prepared.map(msg => ({
|
|
782
|
+
...msg,
|
|
783
|
+
content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
|
|
784
|
+
})) as MastraMessageV2[];
|
|
785
|
+
|
|
786
|
+
return {
|
|
787
|
+
messages: v2Messages,
|
|
788
|
+
total,
|
|
789
|
+
page,
|
|
790
|
+
perPage,
|
|
791
|
+
hasMore,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
messages: prepared,
|
|
797
|
+
total,
|
|
798
|
+
page,
|
|
799
|
+
perPage,
|
|
800
|
+
hasMore,
|
|
801
|
+
};
|
|
802
|
+
} catch (error) {
|
|
803
|
+
console.error('Failed to get paginated messages:', error);
|
|
804
|
+
return {
|
|
805
|
+
messages: [],
|
|
806
|
+
total: 0,
|
|
807
|
+
page,
|
|
808
|
+
perPage,
|
|
809
|
+
hasMore: false,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Original logic for backward compatibility
|
|
815
|
+
// When selectBy is undefined or selectBy.last is undefined, get ALL messages (not just 40)
|
|
816
|
+
let limit: number;
|
|
817
|
+
if (typeof selectBy?.last === 'number') {
|
|
818
|
+
limit = Math.max(0, selectBy.last);
|
|
819
|
+
} else if (selectBy?.last === false) {
|
|
820
|
+
limit = 0;
|
|
821
|
+
} else {
|
|
822
|
+
// No limit specified - get all messages
|
|
823
|
+
limit = Number.MAX_SAFE_INTEGER;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const messageIds = new Set<string>();
|
|
827
|
+
|
|
564
828
|
if (limit === 0 && !selectBy?.include) {
|
|
565
829
|
return [];
|
|
566
830
|
}
|
|
@@ -591,9 +855,16 @@ export class UpstashStore extends MastraStorage {
|
|
|
591
855
|
}
|
|
592
856
|
}
|
|
593
857
|
|
|
594
|
-
// Then get the most recent messages
|
|
595
|
-
|
|
596
|
-
|
|
858
|
+
// Then get the most recent messages (or all if no limit)
|
|
859
|
+
if (limit === Number.MAX_SAFE_INTEGER) {
|
|
860
|
+
// Get all messages
|
|
861
|
+
const allIds = await this.redis.zrange(threadMessagesKey, 0, -1);
|
|
862
|
+
allIds.forEach(id => messageIds.add(id as string));
|
|
863
|
+
} else if (limit > 0) {
|
|
864
|
+
// Get limited number of recent messages
|
|
865
|
+
const latestIds = await this.redis.zrange(threadMessagesKey, -limit, -1);
|
|
866
|
+
latestIds.forEach(id => messageIds.add(id as string));
|
|
867
|
+
}
|
|
597
868
|
|
|
598
869
|
// Fetch all needed messages in parallel
|
|
599
870
|
const messages = (
|
|
@@ -605,15 +876,27 @@ export class UpstashStore extends MastraStorage {
|
|
|
605
876
|
).filter(msg => msg !== null) as (MastraMessageV2 & { _index?: number })[];
|
|
606
877
|
|
|
607
878
|
// Sort messages by their position in the sorted set
|
|
608
|
-
|
|
609
|
-
|
|
879
|
+
messages.sort((a, b) => allMessageIds.indexOf(a!.id) - allMessageIds.indexOf(b!.id));
|
|
880
|
+
|
|
881
|
+
// Remove _index before returning and handle format conversion properly
|
|
882
|
+
const prepared = messages
|
|
883
|
+
.filter(message => message !== null && message !== undefined)
|
|
884
|
+
.map(message => {
|
|
885
|
+
const { _index, ...messageWithoutIndex } = message as MastraMessageV2 & { _index?: number };
|
|
886
|
+
return messageWithoutIndex as unknown as MastraMessageV1;
|
|
887
|
+
});
|
|
610
888
|
|
|
611
|
-
//
|
|
612
|
-
|
|
889
|
+
// For backward compatibility, return messages directly without using MessageList
|
|
890
|
+
// since MessageList has deduplication logic that can cause issues
|
|
891
|
+
if (format === 'v2') {
|
|
892
|
+
// Convert V1 format back to V2 format
|
|
893
|
+
return prepared.map(msg => ({
|
|
894
|
+
...msg,
|
|
895
|
+
content: msg.content || { format: 2, parts: [{ type: 'text', text: '' }] },
|
|
896
|
+
})) as MastraMessageV2[];
|
|
897
|
+
}
|
|
613
898
|
|
|
614
|
-
|
|
615
|
-
if (format === `v2`) return list.get.all.v2();
|
|
616
|
-
return list.get.all.v1();
|
|
899
|
+
return prepared;
|
|
617
900
|
}
|
|
618
901
|
|
|
619
902
|
async persistWorkflowSnapshot(params: {
|
|
@@ -657,6 +940,157 @@ export class UpstashStore extends MastraStorage {
|
|
|
657
940
|
return data.snapshot;
|
|
658
941
|
}
|
|
659
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Get all evaluations with pagination and total count
|
|
945
|
+
* @param options Pagination and filtering options
|
|
946
|
+
* @returns Object with evals array and total count
|
|
947
|
+
*/
|
|
948
|
+
async getEvals(options?: {
|
|
949
|
+
agentName?: string;
|
|
950
|
+
type?: 'test' | 'live';
|
|
951
|
+
page?: number;
|
|
952
|
+
perPage?: number;
|
|
953
|
+
limit?: number;
|
|
954
|
+
offset?: number;
|
|
955
|
+
fromDate?: Date;
|
|
956
|
+
toDate?: Date;
|
|
957
|
+
}): Promise<{
|
|
958
|
+
evals: EvalRow[];
|
|
959
|
+
total: number;
|
|
960
|
+
page?: number;
|
|
961
|
+
perPage?: number;
|
|
962
|
+
hasMore?: boolean;
|
|
963
|
+
}> {
|
|
964
|
+
try {
|
|
965
|
+
// Default pagination parameters
|
|
966
|
+
const page = options?.page ?? 0;
|
|
967
|
+
const perPage = options?.perPage ?? 100;
|
|
968
|
+
const limit = options?.limit;
|
|
969
|
+
const offset = options?.offset;
|
|
970
|
+
|
|
971
|
+
// Get all keys that match the evals table pattern using cursor-based scanning
|
|
972
|
+
const pattern = `${TABLE_EVALS}:*`;
|
|
973
|
+
const keys = await this.scanKeys(pattern);
|
|
974
|
+
|
|
975
|
+
// Check if we have any keys before using pipeline
|
|
976
|
+
if (keys.length === 0) {
|
|
977
|
+
return {
|
|
978
|
+
evals: [],
|
|
979
|
+
total: 0,
|
|
980
|
+
page: options?.page ?? 0,
|
|
981
|
+
perPage: options?.perPage ?? 100,
|
|
982
|
+
hasMore: false,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Use pipeline for batch fetching to improve performance
|
|
987
|
+
const pipeline = this.redis.pipeline();
|
|
988
|
+
keys.forEach(key => pipeline.get(key));
|
|
989
|
+
const results = await pipeline.exec();
|
|
990
|
+
|
|
991
|
+
// Process results and apply filters
|
|
992
|
+
let filteredEvals = results
|
|
993
|
+
.map((result: any) => result as Record<string, any> | null)
|
|
994
|
+
.filter((record): record is Record<string, any> => record !== null && typeof record === 'object');
|
|
995
|
+
|
|
996
|
+
// Apply agent name filter if provided
|
|
997
|
+
if (options?.agentName) {
|
|
998
|
+
filteredEvals = filteredEvals.filter(record => record.agent_name === options.agentName);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Apply type filter if provided
|
|
1002
|
+
if (options?.type === 'test') {
|
|
1003
|
+
filteredEvals = filteredEvals.filter(record => {
|
|
1004
|
+
if (!record.test_info) return false;
|
|
1005
|
+
|
|
1006
|
+
try {
|
|
1007
|
+
if (typeof record.test_info === 'string') {
|
|
1008
|
+
const parsedTestInfo = JSON.parse(record.test_info);
|
|
1009
|
+
return parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo;
|
|
1010
|
+
}
|
|
1011
|
+
return typeof record.test_info === 'object' && 'testPath' in record.test_info;
|
|
1012
|
+
} catch {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
} else if (options?.type === 'live') {
|
|
1017
|
+
filteredEvals = filteredEvals.filter(record => {
|
|
1018
|
+
if (!record.test_info) return true;
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
if (typeof record.test_info === 'string') {
|
|
1022
|
+
const parsedTestInfo = JSON.parse(record.test_info);
|
|
1023
|
+
return !(parsedTestInfo && typeof parsedTestInfo === 'object' && 'testPath' in parsedTestInfo);
|
|
1024
|
+
}
|
|
1025
|
+
return !(typeof record.test_info === 'object' && 'testPath' in record.test_info);
|
|
1026
|
+
} catch {
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Apply date filters if provided
|
|
1033
|
+
if (options?.fromDate) {
|
|
1034
|
+
filteredEvals = filteredEvals.filter(record => {
|
|
1035
|
+
const createdAt = new Date(record.created_at || record.createdAt || 0);
|
|
1036
|
+
return createdAt.getTime() >= options.fromDate!.getTime();
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (options?.toDate) {
|
|
1041
|
+
filteredEvals = filteredEvals.filter(record => {
|
|
1042
|
+
const createdAt = new Date(record.created_at || record.createdAt || 0);
|
|
1043
|
+
return createdAt.getTime() <= options.toDate!.getTime();
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Sort by creation date (newest first)
|
|
1048
|
+
filteredEvals.sort((a, b) => {
|
|
1049
|
+
const dateA = new Date(a.created_at || a.createdAt || 0).getTime();
|
|
1050
|
+
const dateB = new Date(b.created_at || b.createdAt || 0).getTime();
|
|
1051
|
+
return dateB - dateA;
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
const total = filteredEvals.length;
|
|
1055
|
+
|
|
1056
|
+
// Apply pagination - support both page/perPage and limit/offset patterns
|
|
1057
|
+
let paginatedEvals: Record<string, any>[];
|
|
1058
|
+
let hasMore = false;
|
|
1059
|
+
|
|
1060
|
+
if (limit !== undefined && offset !== undefined) {
|
|
1061
|
+
// Offset-based pagination
|
|
1062
|
+
paginatedEvals = filteredEvals.slice(offset, offset + limit);
|
|
1063
|
+
hasMore = offset + limit < total;
|
|
1064
|
+
} else {
|
|
1065
|
+
// Page-based pagination
|
|
1066
|
+
const start = page * perPage;
|
|
1067
|
+
const end = start + perPage;
|
|
1068
|
+
paginatedEvals = filteredEvals.slice(start, end);
|
|
1069
|
+
hasMore = end < total;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Transform to EvalRow format
|
|
1073
|
+
const evals = paginatedEvals.map(record => this.transformEvalRecord(record));
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
evals,
|
|
1077
|
+
total,
|
|
1078
|
+
page: limit !== undefined ? undefined : page,
|
|
1079
|
+
perPage: limit !== undefined ? undefined : perPage,
|
|
1080
|
+
hasMore,
|
|
1081
|
+
};
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
console.error('Failed to get evals:', error);
|
|
1084
|
+
return {
|
|
1085
|
+
evals: [],
|
|
1086
|
+
total: 0,
|
|
1087
|
+
page: options?.page ?? 0,
|
|
1088
|
+
perPage: options?.perPage ?? 100,
|
|
1089
|
+
hasMore: false,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
660
1094
|
async getWorkflowRuns(
|
|
661
1095
|
{
|
|
662
1096
|
namespace,
|
|
@@ -693,24 +1127,25 @@ export class UpstashStore extends MastraStorage {
|
|
|
693
1127
|
}
|
|
694
1128
|
const keys = await this.scanKeys(pattern);
|
|
695
1129
|
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
workflow_name: string;
|
|
701
|
-
run_id: string;
|
|
702
|
-
snapshot: WorkflowRunState | string;
|
|
703
|
-
createdAt: string | Date;
|
|
704
|
-
updatedAt: string | Date;
|
|
705
|
-
resourceId: string;
|
|
706
|
-
}>(key);
|
|
707
|
-
return data;
|
|
708
|
-
}),
|
|
709
|
-
);
|
|
1130
|
+
// Check if we have any keys before using pipeline
|
|
1131
|
+
if (keys.length === 0) {
|
|
1132
|
+
return { runs: [], total: 0 };
|
|
1133
|
+
}
|
|
710
1134
|
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
1135
|
+
// Use pipeline for batch fetching to improve performance
|
|
1136
|
+
const pipeline = this.redis.pipeline();
|
|
1137
|
+
keys.forEach(key => pipeline.get(key));
|
|
1138
|
+
const results = await pipeline.exec();
|
|
1139
|
+
|
|
1140
|
+
// Filter and transform results - handle undefined results
|
|
1141
|
+
let runs = results
|
|
1142
|
+
.map((result: any) => result as Record<string, any> | null)
|
|
1143
|
+
.filter(
|
|
1144
|
+
(record): record is Record<string, any> =>
|
|
1145
|
+
record !== null && record !== undefined && typeof record === 'object' && 'workflow_name' in record,
|
|
1146
|
+
)
|
|
1147
|
+
// Only filter by workflowName if it was specifically requested
|
|
1148
|
+
.filter(record => !workflowName || record.workflow_name === workflowName)
|
|
714
1149
|
.map(w => this.parseWorkflowRun(w!))
|
|
715
1150
|
.filter(w => {
|
|
716
1151
|
if (fromDate && w.createdAt < fromDate) return false;
|