@mastra/clickhouse 0.2.7-alpha.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 +23 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +44 -0
- package/README.md +122 -0
- package/dist/_tsup-dts-rollup.d.cts +83 -0
- package/dist/_tsup-dts-rollup.d.ts +83 -0
- package/dist/index.cjs +635 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +633 -0
- package/docker-compose.yaml +15 -0
- package/eslint.config.js +6 -0
- package/package.json +45 -0
- package/src/index.ts +1 -0
- package/src/storage/index.test.ts +391 -0
- package/src/storage/index.ts +751 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
import type { MessageType, StorageThreadType } from '@mastra/core/memory';
|
|
2
|
+
import {
|
|
3
|
+
MastraStorage,
|
|
4
|
+
TABLE_EVALS,
|
|
5
|
+
TABLE_MESSAGES,
|
|
6
|
+
TABLE_SCHEMAS,
|
|
7
|
+
TABLE_THREADS,
|
|
8
|
+
TABLE_TRACES,
|
|
9
|
+
TABLE_WORKFLOW_SNAPSHOT,
|
|
10
|
+
} from '@mastra/core/storage';
|
|
11
|
+
import type { EvalRow, StorageColumn, StorageGetMessagesArg, TABLE_NAMES } from '@mastra/core/storage';
|
|
12
|
+
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
13
|
+
import { createClient, ClickHouseClient } from '@clickhouse/client';
|
|
14
|
+
|
|
15
|
+
function safelyParseJSON(jsonString: string): any {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(jsonString);
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ClickhouseConfig = {
|
|
24
|
+
url: string;
|
|
25
|
+
username: string;
|
|
26
|
+
password: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TABLE_ENGINES: Record<TABLE_NAMES, string> = {
|
|
30
|
+
[TABLE_MESSAGES]: `MergeTree()`,
|
|
31
|
+
[TABLE_WORKFLOW_SNAPSHOT]: `ReplacingMergeTree()`,
|
|
32
|
+
[TABLE_TRACES]: `MergeTree()`,
|
|
33
|
+
[TABLE_THREADS]: `ReplacingMergeTree()`,
|
|
34
|
+
[TABLE_EVALS]: `MergeTree()`,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const COLUMN_TYPES: Record<StorageColumn['type'], string> = {
|
|
38
|
+
text: 'String',
|
|
39
|
+
timestamp: 'DateTime64(3)',
|
|
40
|
+
uuid: 'String',
|
|
41
|
+
jsonb: 'String',
|
|
42
|
+
integer: 'Int64',
|
|
43
|
+
bigint: 'Int64',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function transformRows<R>(rows: any[]): R[] {
|
|
47
|
+
return rows.map((row: any) => transformRow<R>(row));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function transformRow<R>(row: any): R {
|
|
51
|
+
if (!row) {
|
|
52
|
+
return row;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (row.createdAt) {
|
|
56
|
+
row.createdAt = new Date(row.createdAt);
|
|
57
|
+
}
|
|
58
|
+
if (row.updatedAt) {
|
|
59
|
+
row.updatedAt = new Date(row.updatedAt);
|
|
60
|
+
}
|
|
61
|
+
return row;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ClickhouseStore extends MastraStorage {
|
|
65
|
+
private db: ClickHouseClient;
|
|
66
|
+
|
|
67
|
+
constructor(config: ClickhouseConfig) {
|
|
68
|
+
super({ name: 'ClickhouseStore' });
|
|
69
|
+
this.db = createClient({
|
|
70
|
+
url: config.url,
|
|
71
|
+
username: config.username,
|
|
72
|
+
password: config.password,
|
|
73
|
+
clickhouse_settings: {
|
|
74
|
+
date_time_input_format: 'best_effort',
|
|
75
|
+
date_time_output_format: 'iso', // This is crucial
|
|
76
|
+
use_client_time_zone: 1,
|
|
77
|
+
output_format_json_quote_64bit_integers: 0,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getEvalsByAgentName(_agentName: string, _type?: 'test' | 'live'): Promise<EvalRow[]> {
|
|
83
|
+
throw new Error('Method not implemented.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await this.db.insert({
|
|
89
|
+
table: tableName,
|
|
90
|
+
values: records.map(record => ({
|
|
91
|
+
...Object.fromEntries(
|
|
92
|
+
Object.entries(record).map(([key, value]) => [
|
|
93
|
+
key,
|
|
94
|
+
TABLE_SCHEMAS[tableName as TABLE_NAMES]?.[key]?.type === 'timestamp'
|
|
95
|
+
? new Date(value).toISOString()
|
|
96
|
+
: value,
|
|
97
|
+
]),
|
|
98
|
+
),
|
|
99
|
+
})),
|
|
100
|
+
format: 'JSONEachRow',
|
|
101
|
+
clickhouse_settings: {
|
|
102
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
103
|
+
date_time_input_format: 'best_effort',
|
|
104
|
+
use_client_time_zone: 1,
|
|
105
|
+
output_format_json_quote_64bit_integers: 0,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`Error inserting into ${tableName}:`, error);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getTraces({
|
|
115
|
+
name,
|
|
116
|
+
scope,
|
|
117
|
+
page,
|
|
118
|
+
perPage,
|
|
119
|
+
attributes,
|
|
120
|
+
}: {
|
|
121
|
+
name?: string;
|
|
122
|
+
scope?: string;
|
|
123
|
+
page: number;
|
|
124
|
+
perPage: number;
|
|
125
|
+
attributes?: Record<string, string>;
|
|
126
|
+
}): Promise<any[]> {
|
|
127
|
+
let idx = 1;
|
|
128
|
+
const limit = perPage;
|
|
129
|
+
const offset = page * perPage;
|
|
130
|
+
|
|
131
|
+
const args: Record<string, any> = {};
|
|
132
|
+
|
|
133
|
+
const conditions: string[] = [];
|
|
134
|
+
if (name) {
|
|
135
|
+
conditions.push(`name LIKE CONCAT({var_name:String}, '%')`);
|
|
136
|
+
args.var_name = name;
|
|
137
|
+
}
|
|
138
|
+
if (scope) {
|
|
139
|
+
conditions.push(`scope = {var_scope:String}`);
|
|
140
|
+
args.var_scope = scope;
|
|
141
|
+
}
|
|
142
|
+
if (attributes) {
|
|
143
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
144
|
+
conditions.push(`JSONExtractString(attributes, '${key}') = {var_${key}:String}`);
|
|
145
|
+
args[`var_${key}`] = value;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
150
|
+
|
|
151
|
+
const result = await this.db.query({
|
|
152
|
+
query: `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_TRACES} ${whereClause} ORDER BY "createdAt" DESC LIMIT ${limit} OFFSET ${offset}`,
|
|
153
|
+
query_params: args,
|
|
154
|
+
clickhouse_settings: {
|
|
155
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
156
|
+
date_time_input_format: 'best_effort',
|
|
157
|
+
date_time_output_format: 'iso',
|
|
158
|
+
use_client_time_zone: 1,
|
|
159
|
+
output_format_json_quote_64bit_integers: 0,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!result) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const resp = await result.json();
|
|
168
|
+
const rows: any[] = resp.data;
|
|
169
|
+
return rows.map(row => ({
|
|
170
|
+
id: row.id,
|
|
171
|
+
parentSpanId: row.parentSpanId,
|
|
172
|
+
traceId: row.traceId,
|
|
173
|
+
name: row.name,
|
|
174
|
+
scope: row.scope,
|
|
175
|
+
kind: row.kind,
|
|
176
|
+
status: safelyParseJSON(row.status as string),
|
|
177
|
+
events: safelyParseJSON(row.events as string),
|
|
178
|
+
links: safelyParseJSON(row.links as string),
|
|
179
|
+
attributes: safelyParseJSON(row.attributes as string),
|
|
180
|
+
startTime: row.startTime,
|
|
181
|
+
endTime: row.endTime,
|
|
182
|
+
other: safelyParseJSON(row.other as string),
|
|
183
|
+
createdAt: row.createdAt,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async createTable({
|
|
188
|
+
tableName,
|
|
189
|
+
schema,
|
|
190
|
+
}: {
|
|
191
|
+
tableName: TABLE_NAMES;
|
|
192
|
+
schema: Record<string, StorageColumn>;
|
|
193
|
+
}): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
const columns = Object.entries(schema)
|
|
196
|
+
.map(([name, def]) => {
|
|
197
|
+
const constraints = [];
|
|
198
|
+
if (!def.nullable) constraints.push('NOT NULL');
|
|
199
|
+
return `"${name}" ${COLUMN_TYPES[def.type]} ${constraints.join(' ')}`;
|
|
200
|
+
})
|
|
201
|
+
.join(',\n');
|
|
202
|
+
|
|
203
|
+
const sql =
|
|
204
|
+
tableName === TABLE_WORKFLOW_SNAPSHOT
|
|
205
|
+
? `
|
|
206
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
207
|
+
${['id String'].concat(columns)}
|
|
208
|
+
)
|
|
209
|
+
ENGINE = ${TABLE_ENGINES[tableName]}
|
|
210
|
+
PARTITION BY "createdAt"
|
|
211
|
+
PRIMARY KEY (createdAt, run_id, workflow_name)
|
|
212
|
+
ORDER BY (createdAt, run_id, workflow_name)
|
|
213
|
+
SETTINGS index_granularity = 8192;
|
|
214
|
+
`
|
|
215
|
+
: `
|
|
216
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
217
|
+
${columns}
|
|
218
|
+
)
|
|
219
|
+
ENGINE = ${TABLE_ENGINES[tableName]}
|
|
220
|
+
PARTITION BY "createdAt"
|
|
221
|
+
PRIMARY KEY (createdAt, id)
|
|
222
|
+
ORDER BY (createdAt, id)
|
|
223
|
+
SETTINGS index_granularity = 8192;
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
await this.db.query({
|
|
227
|
+
query: sql,
|
|
228
|
+
clickhouse_settings: {
|
|
229
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
230
|
+
date_time_input_format: 'best_effort',
|
|
231
|
+
date_time_output_format: 'iso',
|
|
232
|
+
use_client_time_zone: 1,
|
|
233
|
+
output_format_json_quote_64bit_integers: 0,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`Error creating table ${tableName}:`, error);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
await this.db.query({
|
|
245
|
+
query: `TRUNCATE TABLE ${tableName}`,
|
|
246
|
+
clickhouse_settings: {
|
|
247
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
248
|
+
date_time_input_format: 'best_effort',
|
|
249
|
+
date_time_output_format: 'iso',
|
|
250
|
+
use_client_time_zone: 1,
|
|
251
|
+
output_format_json_quote_64bit_integers: 0,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(`Error clearing table ${tableName}:`, error);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
|
|
261
|
+
try {
|
|
262
|
+
await this.db.insert({
|
|
263
|
+
table: tableName,
|
|
264
|
+
values: [
|
|
265
|
+
{
|
|
266
|
+
...record,
|
|
267
|
+
createdAt: record.createdAt.toISOString(),
|
|
268
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
format: 'JSONEachRow',
|
|
272
|
+
clickhouse_settings: {
|
|
273
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
274
|
+
output_format_json_quote_64bit_integers: 0,
|
|
275
|
+
date_time_input_format: 'best_effort',
|
|
276
|
+
use_client_time_zone: 1,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error(`Error inserting into ${tableName}:`, error);
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
|
|
286
|
+
try {
|
|
287
|
+
const keyEntries = Object.entries(keys);
|
|
288
|
+
const conditions = keyEntries
|
|
289
|
+
.map(
|
|
290
|
+
([key], index) =>
|
|
291
|
+
`"${key}" = {var_${key}:${COLUMN_TYPES[TABLE_SCHEMAS[tableName as TABLE_NAMES]?.[key]?.type ?? 'text']}}`,
|
|
292
|
+
)
|
|
293
|
+
.join(' AND ');
|
|
294
|
+
const values = keyEntries.reduce((acc, [key, value]) => {
|
|
295
|
+
return { ...acc, [`var_${key}`]: value };
|
|
296
|
+
}, {});
|
|
297
|
+
|
|
298
|
+
const result = await this.db.query({
|
|
299
|
+
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}`,
|
|
300
|
+
query_params: values,
|
|
301
|
+
clickhouse_settings: {
|
|
302
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
303
|
+
date_time_input_format: 'best_effort',
|
|
304
|
+
date_time_output_format: 'iso',
|
|
305
|
+
use_client_time_zone: 1,
|
|
306
|
+
output_format_json_quote_64bit_integers: 0,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (!result) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const rows = await result.json();
|
|
315
|
+
// If this is a workflow snapshot, parse the snapshot field
|
|
316
|
+
if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
|
|
317
|
+
const snapshot = rows.data[0] as any;
|
|
318
|
+
if (!snapshot) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
if (typeof snapshot.snapshot === 'string') {
|
|
322
|
+
snapshot.snapshot = JSON.parse(snapshot.snapshot);
|
|
323
|
+
}
|
|
324
|
+
return transformRow(snapshot);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const data: R = transformRow(rows.data[0]);
|
|
328
|
+
return data;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error(`Error loading from ${tableName}:`, error);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
|
|
336
|
+
try {
|
|
337
|
+
const result = await this.db.query({
|
|
338
|
+
query: `SELECT
|
|
339
|
+
id,
|
|
340
|
+
"resourceId",
|
|
341
|
+
title,
|
|
342
|
+
metadata,
|
|
343
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
344
|
+
toDateTime64(updatedAt, 3) as updatedAt
|
|
345
|
+
FROM "${TABLE_THREADS}"
|
|
346
|
+
FINAL
|
|
347
|
+
WHERE id = {var_id:String}`,
|
|
348
|
+
query_params: { var_id: threadId },
|
|
349
|
+
clickhouse_settings: {
|
|
350
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
351
|
+
date_time_input_format: 'best_effort',
|
|
352
|
+
date_time_output_format: 'iso',
|
|
353
|
+
use_client_time_zone: 1,
|
|
354
|
+
output_format_json_quote_64bit_integers: 0,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const rows = await result.json();
|
|
359
|
+
const thread = transformRow(rows.data[0]) as StorageThreadType;
|
|
360
|
+
|
|
361
|
+
if (!thread) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
...thread,
|
|
367
|
+
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
|
|
368
|
+
createdAt: thread.createdAt,
|
|
369
|
+
updatedAt: thread.updatedAt,
|
|
370
|
+
};
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error(`Error getting thread ${threadId}:`, error);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
|
|
378
|
+
try {
|
|
379
|
+
const result = await this.db.query({
|
|
380
|
+
query: `SELECT
|
|
381
|
+
id,
|
|
382
|
+
"resourceId",
|
|
383
|
+
title,
|
|
384
|
+
metadata,
|
|
385
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
386
|
+
toDateTime64(updatedAt, 3) as updatedAt
|
|
387
|
+
FROM "${TABLE_THREADS}"
|
|
388
|
+
WHERE "resourceId" = {var_resourceId:String}`,
|
|
389
|
+
query_params: { var_resourceId: resourceId },
|
|
390
|
+
clickhouse_settings: {
|
|
391
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
392
|
+
date_time_input_format: 'best_effort',
|
|
393
|
+
date_time_output_format: 'iso',
|
|
394
|
+
use_client_time_zone: 1,
|
|
395
|
+
output_format_json_quote_64bit_integers: 0,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const rows = await result.json();
|
|
400
|
+
const threads = transformRows(rows.data) as StorageThreadType[];
|
|
401
|
+
|
|
402
|
+
return threads.map((thread: StorageThreadType) => ({
|
|
403
|
+
...thread,
|
|
404
|
+
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
|
|
405
|
+
createdAt: thread.createdAt,
|
|
406
|
+
updatedAt: thread.updatedAt,
|
|
407
|
+
}));
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error(`Error getting threads for resource ${resourceId}:`, error);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
|
|
415
|
+
try {
|
|
416
|
+
await this.db.insert({
|
|
417
|
+
table: TABLE_THREADS,
|
|
418
|
+
values: [
|
|
419
|
+
{
|
|
420
|
+
...thread,
|
|
421
|
+
createdAt: thread.createdAt.toISOString(),
|
|
422
|
+
updatedAt: thread.updatedAt.toISOString(),
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
format: 'JSONEachRow',
|
|
426
|
+
clickhouse_settings: {
|
|
427
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
428
|
+
date_time_input_format: 'best_effort',
|
|
429
|
+
use_client_time_zone: 1,
|
|
430
|
+
output_format_json_quote_64bit_integers: 0,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
return thread;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error('Error saving thread:', error);
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async updateThread({
|
|
442
|
+
id,
|
|
443
|
+
title,
|
|
444
|
+
metadata,
|
|
445
|
+
}: {
|
|
446
|
+
id: string;
|
|
447
|
+
title: string;
|
|
448
|
+
metadata: Record<string, unknown>;
|
|
449
|
+
}): Promise<StorageThreadType> {
|
|
450
|
+
try {
|
|
451
|
+
// First get the existing thread to merge metadata
|
|
452
|
+
const existingThread = await this.getThreadById({ threadId: id });
|
|
453
|
+
if (!existingThread) {
|
|
454
|
+
throw new Error(`Thread ${id} not found`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Merge the existing metadata with the new metadata
|
|
458
|
+
const mergedMetadata = {
|
|
459
|
+
...existingThread.metadata,
|
|
460
|
+
...metadata,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const updatedThread = {
|
|
464
|
+
...existingThread,
|
|
465
|
+
title,
|
|
466
|
+
metadata: mergedMetadata,
|
|
467
|
+
updatedAt: new Date(),
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
await this.db.insert({
|
|
471
|
+
table: TABLE_THREADS,
|
|
472
|
+
values: [
|
|
473
|
+
{
|
|
474
|
+
...updatedThread,
|
|
475
|
+
updatedAt: updatedThread.updatedAt.toISOString(),
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
format: 'JSONEachRow',
|
|
479
|
+
clickhouse_settings: {
|
|
480
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
481
|
+
date_time_input_format: 'best_effort',
|
|
482
|
+
use_client_time_zone: 1,
|
|
483
|
+
output_format_json_quote_64bit_integers: 0,
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return updatedThread;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error('Error updating thread:', error);
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async deleteThread({ threadId }: { threadId: string }): Promise<void> {
|
|
495
|
+
try {
|
|
496
|
+
// First delete all messages associated with this thread
|
|
497
|
+
await this.db.command({
|
|
498
|
+
query: `DELETE FROM "${TABLE_MESSAGES}" WHERE thread_id = '${threadId}';`,
|
|
499
|
+
query_params: { var_thread_id: threadId },
|
|
500
|
+
clickhouse_settings: {
|
|
501
|
+
output_format_json_quote_64bit_integers: 0,
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Then delete the thread
|
|
506
|
+
await this.db.command({
|
|
507
|
+
query: `DELETE FROM "${TABLE_THREADS}" WHERE id = {var_id:String};`,
|
|
508
|
+
query_params: { var_id: threadId },
|
|
509
|
+
clickhouse_settings: {
|
|
510
|
+
output_format_json_quote_64bit_integers: 0,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
console.error('Error deleting thread:', error);
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async getMessages<T = unknown>({ threadId, selectBy }: StorageGetMessagesArg): Promise<T> {
|
|
520
|
+
try {
|
|
521
|
+
const messages: any[] = [];
|
|
522
|
+
const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
|
|
523
|
+
const include = selectBy?.include || [];
|
|
524
|
+
|
|
525
|
+
if (include.length) {
|
|
526
|
+
const includeResult = await this.db.query({
|
|
527
|
+
query: `
|
|
528
|
+
WITH ordered_messages AS (
|
|
529
|
+
SELECT
|
|
530
|
+
*,
|
|
531
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
532
|
+
toDateTime64(updatedAt, 3) as updatedAt,
|
|
533
|
+
ROW_NUMBER() OVER (ORDER BY "createdAt" DESC) as row_num
|
|
534
|
+
FROM "${TABLE_MESSAGES}"
|
|
535
|
+
WHERE thread_id = {var_thread_id:String}
|
|
536
|
+
)
|
|
537
|
+
SELECT
|
|
538
|
+
m.id AS id,
|
|
539
|
+
m.content as content,
|
|
540
|
+
m.role as role,
|
|
541
|
+
m.type as type,
|
|
542
|
+
m.createdAt as createdAt,
|
|
543
|
+
m.updatedAt as updatedAt,
|
|
544
|
+
m.thread_id AS "threadId"
|
|
545
|
+
FROM ordered_messages m
|
|
546
|
+
WHERE m.id = ANY({var_include:Array(String)})
|
|
547
|
+
OR EXISTS (
|
|
548
|
+
SELECT 1 FROM ordered_messages target
|
|
549
|
+
WHERE target.id = ANY({var_include:Array(String)})
|
|
550
|
+
AND (
|
|
551
|
+
-- Get previous messages based on the max withPreviousMessages
|
|
552
|
+
(m.row_num <= target.row_num + {var_withPreviousMessages:Int64} AND m.row_num > target.row_num)
|
|
553
|
+
OR
|
|
554
|
+
-- Get next messages based on the max withNextMessages
|
|
555
|
+
(m.row_num >= target.row_num - {var_withNextMessages:Int64} AND m.row_num < target.row_num)
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
ORDER BY m."createdAt" DESC
|
|
559
|
+
`,
|
|
560
|
+
query_params: {
|
|
561
|
+
var_thread_id: threadId,
|
|
562
|
+
var_include: include.map(i => i.id),
|
|
563
|
+
var_withPreviousMessages: Math.max(...include.map(i => i.withPreviousMessages || 0)),
|
|
564
|
+
var_withNextMessages: Math.max(...include.map(i => i.withNextMessages || 0)),
|
|
565
|
+
},
|
|
566
|
+
clickhouse_settings: {
|
|
567
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
568
|
+
date_time_input_format: 'best_effort',
|
|
569
|
+
date_time_output_format: 'iso',
|
|
570
|
+
use_client_time_zone: 1,
|
|
571
|
+
output_format_json_quote_64bit_integers: 0,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const rows = await includeResult.json();
|
|
576
|
+
messages.push(...transformRows(rows.data));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Then get the remaining messages, excluding the ids we just fetched
|
|
580
|
+
const result = await this.db.query({
|
|
581
|
+
query: `
|
|
582
|
+
SELECT
|
|
583
|
+
id,
|
|
584
|
+
content,
|
|
585
|
+
role,
|
|
586
|
+
type,
|
|
587
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
588
|
+
thread_id AS "threadId"
|
|
589
|
+
FROM "${TABLE_MESSAGES}"
|
|
590
|
+
WHERE thread_id = {threadId:String}
|
|
591
|
+
AND id NOT IN ({exclude:Array(String)})
|
|
592
|
+
ORDER BY "createdAt" DESC
|
|
593
|
+
LIMIT {limit:Int64}
|
|
594
|
+
`,
|
|
595
|
+
query_params: {
|
|
596
|
+
threadId,
|
|
597
|
+
exclude: messages.map(m => m.id),
|
|
598
|
+
limit,
|
|
599
|
+
},
|
|
600
|
+
clickhouse_settings: {
|
|
601
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
602
|
+
date_time_input_format: 'best_effort',
|
|
603
|
+
date_time_output_format: 'iso',
|
|
604
|
+
use_client_time_zone: 1,
|
|
605
|
+
output_format_json_quote_64bit_integers: 0,
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const rows = await result.json();
|
|
610
|
+
messages.push(...transformRows(rows.data));
|
|
611
|
+
|
|
612
|
+
// Sort all messages by creation date
|
|
613
|
+
messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
614
|
+
|
|
615
|
+
// Parse message content
|
|
616
|
+
messages.forEach(message => {
|
|
617
|
+
if (typeof message.content === 'string') {
|
|
618
|
+
try {
|
|
619
|
+
message.content = JSON.parse(message.content);
|
|
620
|
+
} catch {
|
|
621
|
+
// If parsing fails, leave as string
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return messages as T;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.error('Error getting messages:', error);
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async saveMessages({ messages }: { messages: MessageType[] }): Promise<MessageType[]> {
|
|
634
|
+
if (messages.length === 0) return messages;
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const threadId = messages[0]?.threadId;
|
|
638
|
+
if (!threadId) {
|
|
639
|
+
throw new Error('Thread ID is required');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Check if thread exists
|
|
643
|
+
const thread = await this.getThreadById({ threadId });
|
|
644
|
+
if (!thread) {
|
|
645
|
+
throw new Error(`Thread ${threadId} not found`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await this.db.insert({
|
|
649
|
+
table: TABLE_MESSAGES,
|
|
650
|
+
format: 'JSONEachRow',
|
|
651
|
+
values: messages.map(message => ({
|
|
652
|
+
id: message.id,
|
|
653
|
+
thread_id: threadId,
|
|
654
|
+
content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
|
|
655
|
+
createdAt: message.createdAt.toISOString(),
|
|
656
|
+
role: message.role,
|
|
657
|
+
type: message.type,
|
|
658
|
+
})),
|
|
659
|
+
clickhouse_settings: {
|
|
660
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
661
|
+
date_time_input_format: 'best_effort',
|
|
662
|
+
use_client_time_zone: 1,
|
|
663
|
+
output_format_json_quote_64bit_integers: 0,
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
return messages;
|
|
668
|
+
} catch (error) {
|
|
669
|
+
console.error('Error saving messages:', error);
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async persistWorkflowSnapshot({
|
|
675
|
+
workflowName,
|
|
676
|
+
runId,
|
|
677
|
+
snapshot,
|
|
678
|
+
}: {
|
|
679
|
+
workflowName: string;
|
|
680
|
+
runId: string;
|
|
681
|
+
snapshot: WorkflowRunState;
|
|
682
|
+
}): Promise<void> {
|
|
683
|
+
try {
|
|
684
|
+
const currentSnapshot = await this.load({
|
|
685
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
686
|
+
keys: { workflow_name: workflowName, run_id: runId },
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const now = new Date();
|
|
690
|
+
const persisting = currentSnapshot
|
|
691
|
+
? {
|
|
692
|
+
...currentSnapshot,
|
|
693
|
+
snapshot: JSON.stringify(snapshot),
|
|
694
|
+
updatedAt: now.toISOString(),
|
|
695
|
+
}
|
|
696
|
+
: {
|
|
697
|
+
workflow_name: workflowName,
|
|
698
|
+
run_id: runId,
|
|
699
|
+
snapshot: JSON.stringify(snapshot),
|
|
700
|
+
createdAt: now.toISOString(),
|
|
701
|
+
updatedAt: now.toISOString(),
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
await this.db.insert({
|
|
705
|
+
table: TABLE_WORKFLOW_SNAPSHOT,
|
|
706
|
+
format: 'JSONEachRow',
|
|
707
|
+
values: [persisting],
|
|
708
|
+
clickhouse_settings: {
|
|
709
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
710
|
+
date_time_input_format: 'best_effort',
|
|
711
|
+
use_client_time_zone: 1,
|
|
712
|
+
output_format_json_quote_64bit_integers: 0,
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.error('Error persisting workflow snapshot:', error);
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async loadWorkflowSnapshot({
|
|
722
|
+
workflowName,
|
|
723
|
+
runId,
|
|
724
|
+
}: {
|
|
725
|
+
workflowName: string;
|
|
726
|
+
runId: string;
|
|
727
|
+
}): Promise<WorkflowRunState | null> {
|
|
728
|
+
try {
|
|
729
|
+
const result = await this.load({
|
|
730
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
731
|
+
keys: {
|
|
732
|
+
workflow_name: workflowName,
|
|
733
|
+
run_id: runId,
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (!result) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return (result as any).snapshot;
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.error('Error loading workflow snapshot:', error);
|
|
744
|
+
throw error;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async close(): Promise<void> {
|
|
749
|
+
await this.db.close();
|
|
750
|
+
}
|
|
751
|
+
}
|