@mastra/clickhouse 0.0.0-vnext-inngest-20250508131921 → 0.0.0-vnext-20251104230439

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,1058 +0,0 @@
1
- import type { ClickHouseClient } from '@clickhouse/client';
2
- import { createClient } from '@clickhouse/client';
3
- import type { MetricResult, TestInfo } from '@mastra/core/eval';
4
- import type { MessageType, StorageThreadType } from '@mastra/core/memory';
5
- import {
6
- MastraStorage,
7
- TABLE_EVALS,
8
- TABLE_MESSAGES,
9
- TABLE_SCHEMAS,
10
- TABLE_THREADS,
11
- TABLE_TRACES,
12
- TABLE_WORKFLOW_SNAPSHOT,
13
- } from '@mastra/core/storage';
14
- import type {
15
- EvalRow,
16
- StorageColumn,
17
- StorageGetMessagesArg,
18
- TABLE_NAMES,
19
- WorkflowRun,
20
- WorkflowRuns,
21
- } from '@mastra/core/storage';
22
- import type { WorkflowRunState } from '@mastra/core/workflows';
23
-
24
- function safelyParseJSON(jsonString: string): any {
25
- try {
26
- return JSON.parse(jsonString);
27
- } catch {
28
- return {};
29
- }
30
- }
31
-
32
- type IntervalUnit =
33
- | 'NANOSECOND'
34
- | 'MICROSECOND'
35
- | 'MILLISECOND'
36
- | 'SECOND'
37
- | 'MINUTE'
38
- | 'HOUR'
39
- | 'DAY'
40
- | 'WEEK'
41
- | 'MONTH'
42
- | 'QUARTER'
43
- | 'YEAR';
44
-
45
- export type ClickhouseConfig = {
46
- url: string;
47
- username: string;
48
- password: string;
49
- ttl?: {
50
- [TableKey in TABLE_NAMES]?: {
51
- row?: { interval: number; unit: IntervalUnit; ttlKey?: string };
52
- columns?: Partial<{
53
- [ColumnKey in keyof (typeof TABLE_SCHEMAS)[TableKey]]: {
54
- interval: number;
55
- unit: IntervalUnit;
56
- ttlKey?: string;
57
- };
58
- }>;
59
- };
60
- };
61
- };
62
-
63
- export const TABLE_ENGINES: Record<TABLE_NAMES, string> = {
64
- [TABLE_MESSAGES]: `MergeTree()`,
65
- [TABLE_WORKFLOW_SNAPSHOT]: `ReplacingMergeTree()`,
66
- [TABLE_TRACES]: `MergeTree()`,
67
- [TABLE_THREADS]: `ReplacingMergeTree()`,
68
- [TABLE_EVALS]: `MergeTree()`,
69
- };
70
-
71
- export const COLUMN_TYPES: Record<StorageColumn['type'], string> = {
72
- text: 'String',
73
- timestamp: 'DateTime64(3)',
74
- uuid: 'String',
75
- jsonb: 'String',
76
- integer: 'Int64',
77
- bigint: 'Int64',
78
- };
79
-
80
- function transformRows<R>(rows: any[]): R[] {
81
- return rows.map((row: any) => transformRow<R>(row));
82
- }
83
-
84
- function transformRow<R>(row: any): R {
85
- if (!row) {
86
- return row;
87
- }
88
-
89
- if (row.createdAt) {
90
- row.createdAt = new Date(row.createdAt);
91
- }
92
- if (row.updatedAt) {
93
- row.updatedAt = new Date(row.updatedAt);
94
- }
95
- return row;
96
- }
97
-
98
- export class ClickhouseStore extends MastraStorage {
99
- protected db: ClickHouseClient;
100
- protected ttl: ClickhouseConfig['ttl'] = {};
101
-
102
- constructor(config: ClickhouseConfig) {
103
- super({ name: 'ClickhouseStore' });
104
- this.db = createClient({
105
- url: config.url,
106
- username: config.username,
107
- password: config.password,
108
- clickhouse_settings: {
109
- date_time_input_format: 'best_effort',
110
- date_time_output_format: 'iso', // This is crucial
111
- use_client_time_zone: 1,
112
- output_format_json_quote_64bit_integers: 0,
113
- },
114
- });
115
- this.ttl = config.ttl;
116
- }
117
-
118
- private transformEvalRow(row: Record<string, any>): EvalRow {
119
- row = transformRow(row);
120
- const resultValue = JSON.parse(row.result as string);
121
- const testInfoValue = row.test_info ? JSON.parse(row.test_info as string) : undefined;
122
-
123
- if (!resultValue || typeof resultValue !== 'object' || !('score' in resultValue)) {
124
- throw new Error(`Invalid MetricResult format: ${JSON.stringify(resultValue)}`);
125
- }
126
-
127
- return {
128
- input: row.input as string,
129
- output: row.output as string,
130
- result: resultValue as MetricResult,
131
- agentName: row.agent_name as string,
132
- metricName: row.metric_name as string,
133
- instructions: row.instructions as string,
134
- testInfo: testInfoValue as TestInfo,
135
- globalRunId: row.global_run_id as string,
136
- runId: row.run_id as string,
137
- createdAt: row.created_at as string,
138
- };
139
- }
140
-
141
- async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
142
- try {
143
- const baseQuery = `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_EVALS} WHERE agent_name = {var_agent_name:String}`;
144
- const typeCondition =
145
- type === 'test'
146
- ? " AND test_info IS NOT NULL AND JSONExtractString(test_info, 'testPath') IS NOT NULL"
147
- : type === 'live'
148
- ? " AND (test_info IS NULL OR JSONExtractString(test_info, 'testPath') IS NULL)"
149
- : '';
150
-
151
- const result = await this.db.query({
152
- query: `${baseQuery}${typeCondition} ORDER BY createdAt DESC`,
153
- query_params: { var_agent_name: agentName },
154
- clickhouse_settings: {
155
- date_time_input_format: 'best_effort',
156
- date_time_output_format: 'iso',
157
- use_client_time_zone: 1,
158
- output_format_json_quote_64bit_integers: 0,
159
- },
160
- });
161
-
162
- if (!result) {
163
- return [];
164
- }
165
-
166
- const rows = await result.json();
167
- return rows.data.map((row: any) => this.transformEvalRow(row));
168
- } catch (error) {
169
- // Handle case where table doesn't exist yet
170
- if (error instanceof Error && error.message.includes('no such table')) {
171
- return [];
172
- }
173
- this.logger.error('Failed to get evals for the specified agent: ' + (error as any)?.message);
174
- throw error;
175
- }
176
- }
177
-
178
- async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
179
- try {
180
- await this.db.insert({
181
- table: tableName,
182
- values: records.map(record => ({
183
- ...Object.fromEntries(
184
- Object.entries(record).map(([key, value]) => [
185
- key,
186
- TABLE_SCHEMAS[tableName as TABLE_NAMES]?.[key]?.type === 'timestamp'
187
- ? new Date(value).toISOString()
188
- : value,
189
- ]),
190
- ),
191
- })),
192
- format: 'JSONEachRow',
193
- clickhouse_settings: {
194
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
195
- date_time_input_format: 'best_effort',
196
- use_client_time_zone: 1,
197
- output_format_json_quote_64bit_integers: 0,
198
- },
199
- });
200
- } catch (error) {
201
- console.error(`Error inserting into ${tableName}:`, error);
202
- throw error;
203
- }
204
- }
205
-
206
- async getTraces({
207
- name,
208
- scope,
209
- page,
210
- perPage,
211
- attributes,
212
- filters,
213
- fromDate,
214
- toDate,
215
- }: {
216
- name?: string;
217
- scope?: string;
218
- page: number;
219
- perPage: number;
220
- attributes?: Record<string, string>;
221
- filters?: Record<string, any>;
222
- fromDate?: Date;
223
- toDate?: Date;
224
- }): Promise<any[]> {
225
- const limit = perPage;
226
- const offset = page * perPage;
227
-
228
- const args: Record<string, any> = {};
229
-
230
- const conditions: string[] = [];
231
- if (name) {
232
- conditions.push(`name LIKE CONCAT({var_name:String}, '%')`);
233
- args.var_name = name;
234
- }
235
- if (scope) {
236
- conditions.push(`scope = {var_scope:String}`);
237
- args.var_scope = scope;
238
- }
239
- if (attributes) {
240
- Object.entries(attributes).forEach(([key, value]) => {
241
- conditions.push(`JSONExtractString(attributes, '${key}') = {var_attr_${key}:String}`);
242
- args[`var_attr_${key}`] = value;
243
- });
244
- }
245
-
246
- if (filters) {
247
- Object.entries(filters).forEach(([key, value]) => {
248
- conditions.push(
249
- `${key} = {var_col_${key}:${COLUMN_TYPES[TABLE_SCHEMAS.mastra_traces?.[key]?.type ?? 'text']}}`,
250
- );
251
- args[`var_col_${key}`] = value;
252
- });
253
- }
254
-
255
- if (fromDate) {
256
- conditions.push(`createdAt >= {var_from_date:DateTime64(3)}`);
257
- args.var_from_date = fromDate.getTime() / 1000; // Convert to Unix timestamp
258
- }
259
-
260
- if (toDate) {
261
- conditions.push(`createdAt <= {var_to_date:DateTime64(3)}`);
262
- args.var_to_date = toDate.getTime() / 1000; // Convert to Unix timestamp
263
- }
264
-
265
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
266
-
267
- const result = await this.db.query({
268
- query: `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_TRACES} ${whereClause} ORDER BY "createdAt" DESC LIMIT ${limit} OFFSET ${offset}`,
269
- query_params: args,
270
- clickhouse_settings: {
271
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
272
- date_time_input_format: 'best_effort',
273
- date_time_output_format: 'iso',
274
- use_client_time_zone: 1,
275
- output_format_json_quote_64bit_integers: 0,
276
- },
277
- });
278
-
279
- if (!result) {
280
- return [];
281
- }
282
-
283
- const resp = await result.json();
284
- const rows: any[] = resp.data;
285
- return rows.map(row => ({
286
- id: row.id,
287
- parentSpanId: row.parentSpanId,
288
- traceId: row.traceId,
289
- name: row.name,
290
- scope: row.scope,
291
- kind: row.kind,
292
- status: safelyParseJSON(row.status as string),
293
- events: safelyParseJSON(row.events as string),
294
- links: safelyParseJSON(row.links as string),
295
- attributes: safelyParseJSON(row.attributes as string),
296
- startTime: row.startTime,
297
- endTime: row.endTime,
298
- other: safelyParseJSON(row.other as string),
299
- createdAt: row.createdAt,
300
- }));
301
- }
302
-
303
- async optimizeTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
304
- await this.db.command({
305
- query: `OPTIMIZE TABLE ${tableName} FINAL`,
306
- });
307
- }
308
-
309
- async materializeTtl({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
310
- await this.db.command({
311
- query: `ALTER TABLE ${tableName} MATERIALIZE TTL;`,
312
- });
313
- }
314
-
315
- async createTable({
316
- tableName,
317
- schema,
318
- }: {
319
- tableName: TABLE_NAMES;
320
- schema: Record<string, StorageColumn>;
321
- }): Promise<void> {
322
- try {
323
- const columns = Object.entries(schema)
324
- .map(([name, def]) => {
325
- const constraints = [];
326
- if (!def.nullable) constraints.push('NOT NULL');
327
- const columnTtl = this.ttl?.[tableName]?.columns?.[name];
328
- return `"${name}" ${COLUMN_TYPES[def.type]} ${constraints.join(' ')} ${columnTtl ? `TTL toDateTime(${columnTtl.ttlKey ?? 'createdAt'}) + INTERVAL ${columnTtl.interval} ${columnTtl.unit}` : ''}`;
329
- })
330
- .join(',\n');
331
-
332
- const rowTtl = this.ttl?.[tableName]?.row;
333
- const sql =
334
- tableName === TABLE_WORKFLOW_SNAPSHOT
335
- ? `
336
- CREATE TABLE IF NOT EXISTS ${tableName} (
337
- ${['id String'].concat(columns)}
338
- )
339
- ENGINE = ${TABLE_ENGINES[tableName]}
340
- PARTITION BY "createdAt"
341
- PRIMARY KEY (createdAt, run_id, workflow_name)
342
- ORDER BY (createdAt, run_id, workflow_name)
343
- ${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? 'createdAt'}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ''}
344
- SETTINGS index_granularity = 8192
345
- `
346
- : `
347
- CREATE TABLE IF NOT EXISTS ${tableName} (
348
- ${columns}
349
- )
350
- ENGINE = ${TABLE_ENGINES[tableName]}
351
- PARTITION BY "createdAt"
352
- PRIMARY KEY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
353
- ORDER BY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
354
- ${this.ttl?.[tableName]?.row ? `TTL toDateTime(createdAt) + INTERVAL ${this.ttl[tableName].row.interval} ${this.ttl[tableName].row.unit}` : ''}
355
- SETTINGS index_granularity = 8192
356
- `;
357
-
358
- await this.db.query({
359
- query: sql,
360
- clickhouse_settings: {
361
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
362
- date_time_input_format: 'best_effort',
363
- date_time_output_format: 'iso',
364
- use_client_time_zone: 1,
365
- output_format_json_quote_64bit_integers: 0,
366
- },
367
- });
368
- } catch (error) {
369
- console.error(`Error creating table ${tableName}:`, error);
370
- throw error;
371
- }
372
- }
373
-
374
- async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
375
- try {
376
- await this.db.query({
377
- query: `TRUNCATE TABLE ${tableName}`,
378
- clickhouse_settings: {
379
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
380
- date_time_input_format: 'best_effort',
381
- date_time_output_format: 'iso',
382
- use_client_time_zone: 1,
383
- output_format_json_quote_64bit_integers: 0,
384
- },
385
- });
386
- } catch (error) {
387
- console.error(`Error clearing table ${tableName}:`, error);
388
- throw error;
389
- }
390
- }
391
-
392
- async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
393
- try {
394
- await this.db.insert({
395
- table: tableName,
396
- values: [
397
- {
398
- ...record,
399
- createdAt: record.createdAt.toISOString(),
400
- updatedAt: record.updatedAt.toISOString(),
401
- },
402
- ],
403
- format: 'JSONEachRow',
404
- clickhouse_settings: {
405
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
406
- output_format_json_quote_64bit_integers: 0,
407
- date_time_input_format: 'best_effort',
408
- use_client_time_zone: 1,
409
- },
410
- });
411
- } catch (error) {
412
- console.error(`Error inserting into ${tableName}:`, error);
413
- throw error;
414
- }
415
- }
416
-
417
- async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
418
- try {
419
- const keyEntries = Object.entries(keys);
420
- const conditions = keyEntries
421
- .map(
422
- ([key]) =>
423
- `"${key}" = {var_${key}:${COLUMN_TYPES[TABLE_SCHEMAS[tableName as TABLE_NAMES]?.[key]?.type ?? 'text']}}`,
424
- )
425
- .join(' AND ');
426
- const values = keyEntries.reduce((acc, [key, value]) => {
427
- return { ...acc, [`var_${key}`]: value };
428
- }, {});
429
-
430
- const result = await this.db.query({
431
- query: `SELECT *, toDateTime64(createdAt, 3) as createdAt, toDateTime64(updatedAt, 3) as updatedAt FROM ${tableName} ${TABLE_ENGINES[tableName as TABLE_NAMES].startsWith('ReplacingMergeTree') ? 'FINAL' : ''} WHERE ${conditions}`,
432
- query_params: values,
433
- clickhouse_settings: {
434
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
435
- date_time_input_format: 'best_effort',
436
- date_time_output_format: 'iso',
437
- use_client_time_zone: 1,
438
- output_format_json_quote_64bit_integers: 0,
439
- },
440
- });
441
-
442
- if (!result) {
443
- return null;
444
- }
445
-
446
- const rows = await result.json();
447
- // If this is a workflow snapshot, parse the snapshot field
448
- if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
449
- const snapshot = rows.data[0] as any;
450
- if (!snapshot) {
451
- return null;
452
- }
453
- if (typeof snapshot.snapshot === 'string') {
454
- snapshot.snapshot = JSON.parse(snapshot.snapshot);
455
- }
456
- return transformRow(snapshot);
457
- }
458
-
459
- const data: R = transformRow(rows.data[0]);
460
- return data;
461
- } catch (error) {
462
- console.error(`Error loading from ${tableName}:`, error);
463
- throw error;
464
- }
465
- }
466
-
467
- async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
468
- try {
469
- const result = await this.db.query({
470
- query: `SELECT
471
- id,
472
- "resourceId",
473
- title,
474
- metadata,
475
- toDateTime64(createdAt, 3) as createdAt,
476
- toDateTime64(updatedAt, 3) as updatedAt
477
- FROM "${TABLE_THREADS}"
478
- FINAL
479
- WHERE id = {var_id:String}`,
480
- query_params: { var_id: threadId },
481
- clickhouse_settings: {
482
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
483
- date_time_input_format: 'best_effort',
484
- date_time_output_format: 'iso',
485
- use_client_time_zone: 1,
486
- output_format_json_quote_64bit_integers: 0,
487
- },
488
- });
489
-
490
- const rows = await result.json();
491
- const thread = transformRow(rows.data[0]) as StorageThreadType;
492
-
493
- if (!thread) {
494
- return null;
495
- }
496
-
497
- return {
498
- ...thread,
499
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
500
- createdAt: thread.createdAt,
501
- updatedAt: thread.updatedAt,
502
- };
503
- } catch (error) {
504
- console.error(`Error getting thread ${threadId}:`, error);
505
- throw error;
506
- }
507
- }
508
-
509
- async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
510
- try {
511
- const result = await this.db.query({
512
- query: `SELECT
513
- id,
514
- "resourceId",
515
- title,
516
- metadata,
517
- toDateTime64(createdAt, 3) as createdAt,
518
- toDateTime64(updatedAt, 3) as updatedAt
519
- FROM "${TABLE_THREADS}"
520
- WHERE "resourceId" = {var_resourceId:String}`,
521
- query_params: { var_resourceId: resourceId },
522
- clickhouse_settings: {
523
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
524
- date_time_input_format: 'best_effort',
525
- date_time_output_format: 'iso',
526
- use_client_time_zone: 1,
527
- output_format_json_quote_64bit_integers: 0,
528
- },
529
- });
530
-
531
- const rows = await result.json();
532
- const threads = transformRows(rows.data) as StorageThreadType[];
533
-
534
- return threads.map((thread: StorageThreadType) => ({
535
- ...thread,
536
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
537
- createdAt: thread.createdAt,
538
- updatedAt: thread.updatedAt,
539
- }));
540
- } catch (error) {
541
- console.error(`Error getting threads for resource ${resourceId}:`, error);
542
- throw error;
543
- }
544
- }
545
-
546
- async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
547
- try {
548
- await this.db.insert({
549
- table: TABLE_THREADS,
550
- values: [
551
- {
552
- ...thread,
553
- createdAt: thread.createdAt.toISOString(),
554
- updatedAt: thread.updatedAt.toISOString(),
555
- },
556
- ],
557
- format: 'JSONEachRow',
558
- clickhouse_settings: {
559
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
560
- date_time_input_format: 'best_effort',
561
- use_client_time_zone: 1,
562
- output_format_json_quote_64bit_integers: 0,
563
- },
564
- });
565
-
566
- return thread;
567
- } catch (error) {
568
- console.error('Error saving thread:', error);
569
- throw error;
570
- }
571
- }
572
-
573
- async updateThread({
574
- id,
575
- title,
576
- metadata,
577
- }: {
578
- id: string;
579
- title: string;
580
- metadata: Record<string, unknown>;
581
- }): Promise<StorageThreadType> {
582
- try {
583
- // First get the existing thread to merge metadata
584
- const existingThread = await this.getThreadById({ threadId: id });
585
- if (!existingThread) {
586
- throw new Error(`Thread ${id} not found`);
587
- }
588
-
589
- // Merge the existing metadata with the new metadata
590
- const mergedMetadata = {
591
- ...existingThread.metadata,
592
- ...metadata,
593
- };
594
-
595
- const updatedThread = {
596
- ...existingThread,
597
- title,
598
- metadata: mergedMetadata,
599
- updatedAt: new Date(),
600
- };
601
-
602
- await this.db.insert({
603
- table: TABLE_THREADS,
604
- values: [
605
- {
606
- ...updatedThread,
607
- updatedAt: updatedThread.updatedAt.toISOString(),
608
- },
609
- ],
610
- format: 'JSONEachRow',
611
- clickhouse_settings: {
612
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
613
- date_time_input_format: 'best_effort',
614
- use_client_time_zone: 1,
615
- output_format_json_quote_64bit_integers: 0,
616
- },
617
- });
618
-
619
- return updatedThread;
620
- } catch (error) {
621
- console.error('Error updating thread:', error);
622
- throw error;
623
- }
624
- }
625
-
626
- async deleteThread({ threadId }: { threadId: string }): Promise<void> {
627
- try {
628
- // First delete all messages associated with this thread
629
- await this.db.command({
630
- query: `DELETE FROM "${TABLE_MESSAGES}" WHERE thread_id = '${threadId}';`,
631
- query_params: { var_thread_id: threadId },
632
- clickhouse_settings: {
633
- output_format_json_quote_64bit_integers: 0,
634
- },
635
- });
636
-
637
- // Then delete the thread
638
- await this.db.command({
639
- query: `DELETE FROM "${TABLE_THREADS}" WHERE id = {var_id:String};`,
640
- query_params: { var_id: threadId },
641
- clickhouse_settings: {
642
- output_format_json_quote_64bit_integers: 0,
643
- },
644
- });
645
- } catch (error) {
646
- console.error('Error deleting thread:', error);
647
- throw error;
648
- }
649
- }
650
-
651
- async getMessages<T = unknown>({ threadId, selectBy }: StorageGetMessagesArg): Promise<T[]> {
652
- try {
653
- const messages: any[] = [];
654
- const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
655
- const include = selectBy?.include || [];
656
-
657
- if (include.length) {
658
- const includeResult = await this.db.query({
659
- query: `
660
- WITH ordered_messages AS (
661
- SELECT
662
- *,
663
- toDateTime64(createdAt, 3) as createdAt,
664
- toDateTime64(updatedAt, 3) as updatedAt,
665
- ROW_NUMBER() OVER (ORDER BY "createdAt" DESC) as row_num
666
- FROM "${TABLE_MESSAGES}"
667
- WHERE thread_id = {var_thread_id:String}
668
- )
669
- SELECT
670
- m.id AS id,
671
- m.content as content,
672
- m.role as role,
673
- m.type as type,
674
- m.createdAt as createdAt,
675
- m.updatedAt as updatedAt,
676
- m.thread_id AS "threadId"
677
- FROM ordered_messages m
678
- WHERE m.id = ANY({var_include:Array(String)})
679
- OR EXISTS (
680
- SELECT 1 FROM ordered_messages target
681
- WHERE target.id = ANY({var_include:Array(String)})
682
- AND (
683
- -- Get previous messages based on the max withPreviousMessages
684
- (m.row_num <= target.row_num + {var_withPreviousMessages:Int64} AND m.row_num > target.row_num)
685
- OR
686
- -- Get next messages based on the max withNextMessages
687
- (m.row_num >= target.row_num - {var_withNextMessages:Int64} AND m.row_num < target.row_num)
688
- )
689
- )
690
- ORDER BY m."createdAt" DESC
691
- `,
692
- query_params: {
693
- var_thread_id: threadId,
694
- var_include: include.map(i => i.id),
695
- var_withPreviousMessages: Math.max(...include.map(i => i.withPreviousMessages || 0)),
696
- var_withNextMessages: Math.max(...include.map(i => i.withNextMessages || 0)),
697
- },
698
- clickhouse_settings: {
699
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
700
- date_time_input_format: 'best_effort',
701
- date_time_output_format: 'iso',
702
- use_client_time_zone: 1,
703
- output_format_json_quote_64bit_integers: 0,
704
- },
705
- });
706
-
707
- const rows = await includeResult.json();
708
- messages.push(...transformRows(rows.data));
709
- }
710
-
711
- // Then get the remaining messages, excluding the ids we just fetched
712
- const result = await this.db.query({
713
- query: `
714
- SELECT
715
- id,
716
- content,
717
- role,
718
- type,
719
- toDateTime64(createdAt, 3) as createdAt,
720
- thread_id AS "threadId"
721
- FROM "${TABLE_MESSAGES}"
722
- WHERE thread_id = {threadId:String}
723
- AND id NOT IN ({exclude:Array(String)})
724
- ORDER BY "createdAt" DESC
725
- LIMIT {limit:Int64}
726
- `,
727
- query_params: {
728
- threadId,
729
- exclude: messages.map(m => m.id),
730
- limit,
731
- },
732
- clickhouse_settings: {
733
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
734
- date_time_input_format: 'best_effort',
735
- date_time_output_format: 'iso',
736
- use_client_time_zone: 1,
737
- output_format_json_quote_64bit_integers: 0,
738
- },
739
- });
740
-
741
- const rows = await result.json();
742
- messages.push(...transformRows(rows.data));
743
-
744
- // Sort all messages by creation date
745
- messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
746
-
747
- // Parse message content
748
- messages.forEach(message => {
749
- if (typeof message.content === 'string') {
750
- try {
751
- message.content = JSON.parse(message.content);
752
- } catch {
753
- // If parsing fails, leave as string
754
- }
755
- }
756
- });
757
-
758
- return messages as T[];
759
- } catch (error) {
760
- console.error('Error getting messages:', error);
761
- throw error;
762
- }
763
- }
764
-
765
- async saveMessages({ messages }: { messages: MessageType[] }): Promise<MessageType[]> {
766
- if (messages.length === 0) return messages;
767
-
768
- try {
769
- const threadId = messages[0]?.threadId;
770
- if (!threadId) {
771
- throw new Error('Thread ID is required');
772
- }
773
-
774
- // Check if thread exists
775
- const thread = await this.getThreadById({ threadId });
776
- if (!thread) {
777
- throw new Error(`Thread ${threadId} not found`);
778
- }
779
-
780
- await this.db.insert({
781
- table: TABLE_MESSAGES,
782
- format: 'JSONEachRow',
783
- values: messages.map(message => ({
784
- id: message.id,
785
- thread_id: threadId,
786
- content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
787
- createdAt: message.createdAt.toISOString(),
788
- role: message.role,
789
- type: message.type,
790
- })),
791
- clickhouse_settings: {
792
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
793
- date_time_input_format: 'best_effort',
794
- use_client_time_zone: 1,
795
- output_format_json_quote_64bit_integers: 0,
796
- },
797
- });
798
-
799
- return messages;
800
- } catch (error) {
801
- console.error('Error saving messages:', error);
802
- throw error;
803
- }
804
- }
805
-
806
- async persistWorkflowSnapshot({
807
- workflowName,
808
- runId,
809
- snapshot,
810
- }: {
811
- workflowName: string;
812
- runId: string;
813
- snapshot: WorkflowRunState;
814
- }): Promise<void> {
815
- try {
816
- const currentSnapshot = await this.load({
817
- tableName: TABLE_WORKFLOW_SNAPSHOT,
818
- keys: { workflow_name: workflowName, run_id: runId },
819
- });
820
-
821
- const now = new Date();
822
- const persisting = currentSnapshot
823
- ? {
824
- ...currentSnapshot,
825
- snapshot: JSON.stringify(snapshot),
826
- updatedAt: now.toISOString(),
827
- }
828
- : {
829
- workflow_name: workflowName,
830
- run_id: runId,
831
- snapshot: JSON.stringify(snapshot),
832
- createdAt: now.toISOString(),
833
- updatedAt: now.toISOString(),
834
- };
835
-
836
- await this.db.insert({
837
- table: TABLE_WORKFLOW_SNAPSHOT,
838
- format: 'JSONEachRow',
839
- values: [persisting],
840
- clickhouse_settings: {
841
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
842
- date_time_input_format: 'best_effort',
843
- use_client_time_zone: 1,
844
- output_format_json_quote_64bit_integers: 0,
845
- },
846
- });
847
- } catch (error) {
848
- console.error('Error persisting workflow snapshot:', error);
849
- throw error;
850
- }
851
- }
852
-
853
- async loadWorkflowSnapshot({
854
- workflowName,
855
- runId,
856
- }: {
857
- workflowName: string;
858
- runId: string;
859
- }): Promise<WorkflowRunState | null> {
860
- try {
861
- const result = await this.load({
862
- tableName: TABLE_WORKFLOW_SNAPSHOT,
863
- keys: {
864
- workflow_name: workflowName,
865
- run_id: runId,
866
- },
867
- });
868
-
869
- if (!result) {
870
- return null;
871
- }
872
-
873
- return (result as any).snapshot;
874
- } catch (error) {
875
- console.error('Error loading workflow snapshot:', error);
876
- throw error;
877
- }
878
- }
879
-
880
- private parseWorkflowRun(row: any): WorkflowRun {
881
- let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
882
- if (typeof parsedSnapshot === 'string') {
883
- try {
884
- parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
885
- } catch (e) {
886
- // If parsing fails, return the raw snapshot string
887
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
888
- }
889
- }
890
-
891
- return {
892
- workflowName: row.workflow_name,
893
- runId: row.run_id,
894
- snapshot: parsedSnapshot,
895
- createdAt: new Date(row.createdAt),
896
- updatedAt: new Date(row.updatedAt),
897
- resourceId: row.resourceId,
898
- };
899
- }
900
-
901
- async getWorkflowRuns({
902
- workflowName,
903
- fromDate,
904
- toDate,
905
- limit,
906
- offset,
907
- resourceId,
908
- }: {
909
- workflowName?: string;
910
- fromDate?: Date;
911
- toDate?: Date;
912
- limit?: number;
913
- offset?: number;
914
- resourceId?: string;
915
- } = {}): Promise<WorkflowRuns> {
916
- try {
917
- const conditions: string[] = [];
918
- const values: Record<string, any> = {};
919
-
920
- if (workflowName) {
921
- conditions.push(`workflow_name = {var_workflow_name:String}`);
922
- values.var_workflow_name = workflowName;
923
- }
924
-
925
- if (resourceId) {
926
- const hasResourceId = await this.hasColumn(TABLE_WORKFLOW_SNAPSHOT, 'resourceId');
927
- if (hasResourceId) {
928
- conditions.push(`resourceId = {var_resourceId:String}`);
929
- values.var_resourceId = resourceId;
930
- } else {
931
- console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
932
- }
933
- }
934
-
935
- if (fromDate) {
936
- conditions.push(`createdAt >= {var_from_date:DateTime64(3)}`);
937
- values.var_from_date = fromDate.getTime() / 1000; // Convert to Unix timestamp
938
- }
939
-
940
- if (toDate) {
941
- conditions.push(`createdAt <= {var_to_date:DateTime64(3)}`);
942
- values.var_to_date = toDate.getTime() / 1000; // Convert to Unix timestamp
943
- }
944
-
945
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
946
- const limitClause = limit !== undefined ? `LIMIT ${limit}` : '';
947
- const offsetClause = offset !== undefined ? `OFFSET ${offset}` : '';
948
-
949
- let total = 0;
950
- // Only get total count when using pagination
951
- if (limit !== undefined && offset !== undefined) {
952
- const countResult = await this.db.query({
953
- query: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith('ReplacingMergeTree') ? 'FINAL' : ''} ${whereClause}`,
954
- query_params: values,
955
- format: 'JSONEachRow',
956
- });
957
- const countRows = await countResult.json();
958
- total = Number((countRows as Array<{ count: string | number }>)[0]?.count ?? 0);
959
- }
960
-
961
- // Get results
962
- const result = await this.db.query({
963
- query: `
964
- SELECT
965
- workflow_name,
966
- run_id,
967
- snapshot,
968
- toDateTime64(createdAt, 3) as createdAt,
969
- toDateTime64(updatedAt, 3) as updatedAt,
970
- resourceId
971
- FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith('ReplacingMergeTree') ? 'FINAL' : ''}
972
- ${whereClause}
973
- ORDER BY createdAt DESC
974
- ${limitClause}
975
- ${offsetClause}
976
- `,
977
- query_params: values,
978
- format: 'JSONEachRow',
979
- });
980
-
981
- const resultJson = await result.json();
982
- const rows = resultJson as any[];
983
- const runs = rows.map(row => {
984
- return this.parseWorkflowRun(row);
985
- });
986
-
987
- // Use runs.length as total when not paginating
988
- return { runs, total: total || runs.length };
989
- } catch (error) {
990
- console.error('Error getting workflow runs:', error);
991
- throw error;
992
- }
993
- }
994
-
995
- async getWorkflowRunById({
996
- runId,
997
- workflowName,
998
- }: {
999
- runId: string;
1000
- workflowName?: string;
1001
- }): Promise<WorkflowRun | null> {
1002
- try {
1003
- const conditions: string[] = [];
1004
- const values: Record<string, any> = {};
1005
-
1006
- if (runId) {
1007
- conditions.push(`run_id = {var_runId:String}`);
1008
- values.var_runId = runId;
1009
- }
1010
-
1011
- if (workflowName) {
1012
- conditions.push(`workflow_name = {var_workflow_name:String}`);
1013
- values.var_workflow_name = workflowName;
1014
- }
1015
-
1016
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1017
-
1018
- // Get results
1019
- const result = await this.db.query({
1020
- query: `
1021
- SELECT
1022
- workflow_name,
1023
- run_id,
1024
- snapshot,
1025
- toDateTime64(createdAt, 3) as createdAt,
1026
- toDateTime64(updatedAt, 3) as updatedAt,
1027
- resourceId
1028
- FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith('ReplacingMergeTree') ? 'FINAL' : ''}
1029
- ${whereClause}
1030
- `,
1031
- query_params: values,
1032
- format: 'JSONEachRow',
1033
- });
1034
-
1035
- const resultJson = await result.json();
1036
- if (!Array.isArray(resultJson) || resultJson.length === 0) {
1037
- return null;
1038
- }
1039
- return this.parseWorkflowRun(resultJson[0]);
1040
- } catch (error) {
1041
- console.error('Error getting workflow run by ID:', error);
1042
- throw error;
1043
- }
1044
- }
1045
-
1046
- private async hasColumn(table: string, column: string): Promise<boolean> {
1047
- const result = await this.db.query({
1048
- query: `DESCRIBE TABLE ${table}`,
1049
- format: 'JSONEachRow',
1050
- });
1051
- const columns = (await result.json()) as { name: string }[];
1052
- return columns.some(c => c.name === column);
1053
- }
1054
-
1055
- async close(): Promise<void> {
1056
- await this.db.close();
1057
- }
1058
- }