ai-experiments 2.1.3 → 2.4.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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +13 -1
  4. package/dist/clickhouse-storage.d.ts +227 -0
  5. package/dist/clickhouse-storage.d.ts.map +1 -0
  6. package/dist/clickhouse-storage.js +522 -0
  7. package/dist/clickhouse-storage.js.map +1 -0
  8. package/dist/decide.d.ts.map +1 -1
  9. package/dist/decide.js +6 -2
  10. package/dist/decide.js.map +1 -1
  11. package/dist/experiment.js +9 -9
  12. package/dist/experiment.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/tracking.js +3 -3
  18. package/dist/tracking.js.map +1 -1
  19. package/package.json +16 -16
  20. package/src/clickhouse-storage.ts +698 -0
  21. package/src/decide.ts +8 -10
  22. package/src/experiment.ts +12 -12
  23. package/src/index.ts +13 -3
  24. package/src/tracking.ts +3 -3
  25. package/test/cartesian.test.ts +287 -0
  26. package/test/clickhouse-storage.test.ts +470 -0
  27. package/test/decide.test.ts +414 -0
  28. package/test/experiment.test.ts +473 -0
  29. package/test/tracking.test.ts +347 -0
  30. package/vitest.config.ts +34 -0
  31. package/LICENSE +0 -21
  32. package/dist/chdb-storage.d.ts +0 -135
  33. package/dist/chdb-storage.d.ts.map +0 -1
  34. package/dist/chdb-storage.js +0 -468
  35. package/dist/chdb-storage.js.map +0 -1
  36. package/src/cartesian.js +0 -215
  37. package/src/chdb-storage.js +0 -464
  38. package/src/chdb-storage.ts +0 -567
  39. package/src/decide.js +0 -328
  40. package/src/experiment.js +0 -291
  41. package/src/index.js +0 -20
  42. package/src/tracking.js +0 -309
  43. package/src/types.js +0 -4
package/src/cartesian.js DELETED
@@ -1,215 +0,0 @@
1
- /**
2
- * Cartesian product utilities for parameter exploration
3
- */
4
- /**
5
- * Generate cartesian product of parameter sets
6
- *
7
- * Takes an object where each key maps to an array of possible values,
8
- * and returns all possible combinations as an array of objects.
9
- *
10
- * @example
11
- * ```ts
12
- * import { cartesian } from 'ai-experiments'
13
- *
14
- * const combinations = cartesian({
15
- * model: ['sonnet', 'opus', 'gpt-4o'],
16
- * temperature: [0.3, 0.7, 1.0],
17
- * maxTokens: [100, 500, 1000],
18
- * })
19
- *
20
- * // Returns 27 combinations (3 * 3 * 3):
21
- * // [
22
- * // { model: 'sonnet', temperature: 0.3, maxTokens: 100 },
23
- * // { model: 'sonnet', temperature: 0.3, maxTokens: 500 },
24
- * // { model: 'sonnet', temperature: 0.3, maxTokens: 1000 },
25
- * // { model: 'sonnet', temperature: 0.7, maxTokens: 100 },
26
- * // ...
27
- * // ]
28
- *
29
- * // Use with experiments:
30
- * const variants = combinations.map((config, i) => ({
31
- * id: `variant-${i}`,
32
- * name: `${config.model} T=${config.temperature} max=${config.maxTokens}`,
33
- * config,
34
- * }))
35
- * ```
36
- */
37
- export function cartesian(params) {
38
- const keys = Object.keys(params);
39
- const values = keys.map((k) => params[k]);
40
- // Handle empty input
41
- if (keys.length === 0) {
42
- return [];
43
- }
44
- // Handle single parameter
45
- if (keys.length === 1) {
46
- const key = keys[0];
47
- return values[0].map((val) => ({ [key]: val }));
48
- }
49
- // Generate all combinations using recursive helper
50
- const combinations = cartesianProduct(values);
51
- // Map back to objects
52
- return combinations.map((combo) => {
53
- const obj = {};
54
- keys.forEach((key, i) => {
55
- obj[key] = combo[i];
56
- });
57
- return obj;
58
- });
59
- }
60
- /**
61
- * Recursive cartesian product implementation
62
- */
63
- function cartesianProduct(arrays) {
64
- if (arrays.length === 0)
65
- return [[]];
66
- const [first, ...rest] = arrays;
67
- // Base case: single array
68
- if (rest.length === 0) {
69
- return first.map((x) => [x]);
70
- }
71
- // Recursive case
72
- const restProduct = cartesianProduct(rest);
73
- const result = [];
74
- for (const x of first) {
75
- for (const restCombo of restProduct) {
76
- result.push([x, ...restCombo]);
77
- }
78
- }
79
- return result;
80
- }
81
- /**
82
- * Generate a grid of parameter combinations with filtering
83
- *
84
- * Similar to cartesian(), but allows filtering out invalid combinations.
85
- *
86
- * @example
87
- * ```ts
88
- * import { cartesianFilter } from 'ai-experiments'
89
- *
90
- * const combinations = cartesianFilter(
91
- * {
92
- * model: ['sonnet', 'opus'],
93
- * temperature: [0.3, 0.7, 1.0],
94
- * maxTokens: [100, 500],
95
- * },
96
- * // Filter out combinations where opus uses high temperature
97
- * (combo) => !(combo.model === 'opus' && combo.temperature > 0.7)
98
- * )
99
- * ```
100
- */
101
- export function cartesianFilter(params, filter) {
102
- const allCombinations = cartesian(params);
103
- return allCombinations.filter(filter);
104
- }
105
- /**
106
- * Generate a random sample from the cartesian product
107
- *
108
- * Useful when the full cartesian product is too large to test all combinations.
109
- *
110
- * @example
111
- * ```ts
112
- * import { cartesianSample } from 'ai-experiments'
113
- *
114
- * // Full product would be 1000 combinations (10 * 10 * 10)
115
- * // Sample just 20 random combinations
116
- * const sample = cartesianSample(
117
- * {
118
- * param1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
119
- * param2: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
120
- * param3: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
121
- * },
122
- * 20
123
- * )
124
- * ```
125
- */
126
- export function cartesianSample(params, sampleSize, options = {}) {
127
- const { unique = true } = options;
128
- // Generate all combinations first
129
- const allCombinations = cartesian(params);
130
- // If sample size is larger than available combinations, return all
131
- if (sampleSize >= allCombinations.length) {
132
- return allCombinations;
133
- }
134
- // Shuffle and take first n items
135
- const shuffled = [...allCombinations];
136
- // Simple Fisher-Yates shuffle
137
- for (let i = shuffled.length - 1; i > 0; i--) {
138
- const j = Math.floor(Math.random() * (i + 1));
139
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
140
- }
141
- return shuffled.slice(0, sampleSize);
142
- }
143
- /**
144
- * Count the total number of combinations without generating them
145
- *
146
- * Useful for checking if cartesian product is feasible before generating.
147
- *
148
- * @example
149
- * ```ts
150
- * import { cartesianCount } from 'ai-experiments'
151
- *
152
- * const count = cartesianCount({
153
- * model: ['sonnet', 'opus', 'gpt-4o'],
154
- * temperature: [0.3, 0.5, 0.7, 0.9],
155
- * maxTokens: [100, 500, 1000, 2000],
156
- * })
157
- * // Returns 48 (3 * 4 * 4)
158
- *
159
- * if (count > 100) {
160
- * console.log('Too many combinations, use cartesianSample instead')
161
- * }
162
- * ```
163
- */
164
- export function cartesianCount(params) {
165
- const keys = Object.keys(params);
166
- if (keys.length === 0)
167
- return 0;
168
- return keys.reduce((total, key) => {
169
- const arr = params[key];
170
- return total * (arr?.length ?? 0);
171
- }, 1);
172
- }
173
- /**
174
- * Generate cartesian product with labels for each dimension
175
- *
176
- * Returns combinations with additional metadata about which dimension each value came from.
177
- *
178
- * @example
179
- * ```ts
180
- * import { cartesianWithLabels } from 'ai-experiments'
181
- *
182
- * const labeled = cartesianWithLabels({
183
- * model: ['sonnet', 'opus'],
184
- * temperature: [0.3, 0.7],
185
- * })
186
- * // [
187
- * // { values: { model: 'sonnet', temperature: 0.3 }, labels: { model: 0, temperature: 0 } },
188
- * // { values: { model: 'sonnet', temperature: 0.7 }, labels: { model: 0, temperature: 1 } },
189
- * // { values: { model: 'opus', temperature: 0.3 }, labels: { model: 1, temperature: 0 } },
190
- * // { values: { model: 'opus', temperature: 0.7 }, labels: { model: 1, temperature: 1 } },
191
- * // ]
192
- * ```
193
- */
194
- export function cartesianWithLabels(params) {
195
- const keys = Object.keys(params);
196
- const values = keys.map((k) => params[k]);
197
- if (keys.length === 0) {
198
- return [];
199
- }
200
- const combinations = cartesianProduct(values);
201
- return combinations.map((combo) => {
202
- const valuesObj = {};
203
- const labelsObj = {};
204
- keys.forEach((key, i) => {
205
- const value = combo[i];
206
- valuesObj[key] = value;
207
- const arr = params[key];
208
- labelsObj[key] = arr ? arr.indexOf(value) : -1;
209
- });
210
- return {
211
- values: valuesObj,
212
- labels: labelsObj,
213
- };
214
- });
215
- }
@@ -1,464 +0,0 @@
1
- /**
2
- * ClickHouse storage backend for AI experiments using chdb (embedded ClickHouse)
3
- *
4
- * Design: Single flat table where experiment events are the atomic unit.
5
- * Cartesian product results, variant comparisons, and winner analysis
6
- * are all derived via aggregation queries.
7
- *
8
- * This maximizes compression and enables fast analytical queries across
9
- * thousands of experiment runs.
10
- */
11
- import { Session } from 'chdb';
12
- const CREATE_TABLE_SQL = `
13
- CREATE TABLE IF NOT EXISTS experiments (
14
- -- Event identity
15
- eventId String,
16
- eventType LowCardinality(String),
17
- timestamp DateTime64(3),
18
-
19
- -- Experiment context
20
- experimentId String,
21
- experimentName String DEFAULT '',
22
-
23
- -- Variant context
24
- variantId String DEFAULT '',
25
- variantName String DEFAULT '',
26
-
27
- -- Cartesian product dimensions (stored as JSON for flexibility)
28
- dimensions String DEFAULT '{}',
29
-
30
- -- Execution data
31
- runId String DEFAULT '',
32
- success UInt8 DEFAULT 1,
33
- durationMs UInt32 DEFAULT 0,
34
-
35
- -- Result data
36
- result String DEFAULT '{}',
37
- metricName LowCardinality(String) DEFAULT '',
38
- metricValue Float64 DEFAULT 0,
39
-
40
- -- Error tracking
41
- errorMessage String DEFAULT '',
42
- errorStack String DEFAULT '',
43
-
44
- -- Metadata
45
- metadata String DEFAULT '{}'
46
- )
47
- ENGINE = MergeTree()
48
- ORDER BY (experimentId, variantId, timestamp, eventId)
49
- SETTINGS index_granularity = 8192
50
- `;
51
- export class ChdbStorage {
52
- options;
53
- session;
54
- initialized = false;
55
- eventCounter = 0;
56
- constructor(options = {}) {
57
- this.options = options;
58
- this.session = new Session(options.dataPath ?? './.experiments-chdb');
59
- }
60
- async init() {
61
- if (this.initialized)
62
- return;
63
- if (this.options.autoInit !== false) {
64
- this.session.query(CREATE_TABLE_SQL);
65
- }
66
- this.initialized = true;
67
- }
68
- escapeString(s) {
69
- return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
70
- }
71
- toJson(value) {
72
- return JSON.stringify(value ?? {});
73
- }
74
- parseJson(value) {
75
- try {
76
- return JSON.parse(value || '{}');
77
- }
78
- catch {
79
- return {};
80
- }
81
- }
82
- nextEventId() {
83
- return `evt-${Date.now()}-${++this.eventCounter}`;
84
- }
85
- // ─────────────────────────────────────────────────────────────────────────────
86
- // TrackingBackend Implementation
87
- // ─────────────────────────────────────────────────────────────────────────────
88
- async track(event) {
89
- await this.init();
90
- const eventId = this.nextEventId();
91
- const timestamp = event.timestamp.toISOString();
92
- const data = event.data;
93
- // Extract standard fields from event data
94
- const experimentId = String(data.experimentId ?? '');
95
- const experimentName = String(data.experimentName ?? '');
96
- const variantId = String(data.variantId ?? '');
97
- const variantName = String(data.variantName ?? '');
98
- const runId = String(data.runId ?? '');
99
- const success = data.success !== false ? 1 : 0;
100
- const durationMs = Number(data.duration ?? data.durationMs ?? 0);
101
- const metricName = String(data.metricName ?? '');
102
- const metricValue = Number(data.metricValue ?? 0);
103
- const errorMessage = data.error instanceof Error ? data.error.message : String(data.errorMessage ?? '');
104
- const errorStack = data.error instanceof Error ? (data.error.stack ?? '') : '';
105
- // Extract dimensions (for cartesian product tracking)
106
- const dimensions = data.dimensions ?? data.config ?? {};
107
- const result = data.result ?? {};
108
- const metadata = data.metadata ?? {};
109
- this.session.query(`
110
- INSERT INTO experiments (
111
- eventId, eventType, timestamp,
112
- experimentId, experimentName,
113
- variantId, variantName,
114
- dimensions, runId, success, durationMs,
115
- result, metricName, metricValue,
116
- errorMessage, errorStack, metadata
117
- ) VALUES (
118
- '${eventId}', '${event.type}', '${timestamp}',
119
- '${this.escapeString(experimentId)}', '${this.escapeString(experimentName)}',
120
- '${this.escapeString(variantId)}', '${this.escapeString(variantName)}',
121
- '${this.escapeString(this.toJson(dimensions))}',
122
- '${this.escapeString(runId)}', ${success}, ${durationMs},
123
- '${this.escapeString(this.toJson(result))}',
124
- '${this.escapeString(metricName)}', ${metricValue},
125
- '${this.escapeString(errorMessage)}', '${this.escapeString(errorStack)}',
126
- '${this.escapeString(this.toJson(metadata))}'
127
- )
128
- `);
129
- }
130
- async flush() {
131
- // chdb writes are synchronous, nothing to flush
132
- }
133
- // ─────────────────────────────────────────────────────────────────────────────
134
- // Experiment Result Storage (direct API)
135
- // ─────────────────────────────────────────────────────────────────────────────
136
- async storeResult(result) {
137
- await this.track({
138
- type: 'variant.complete',
139
- timestamp: result.completedAt,
140
- data: {
141
- experimentId: result.experimentId,
142
- variantId: result.variantId,
143
- variantName: result.variantName,
144
- runId: result.runId,
145
- success: result.success,
146
- duration: result.duration,
147
- result: result.result,
148
- metricValue: result.metricValue,
149
- error: result.error,
150
- metadata: result.metadata,
151
- },
152
- });
153
- }
154
- async storeResults(results) {
155
- for (const result of results) {
156
- await this.storeResult(result);
157
- }
158
- }
159
- // ─────────────────────────────────────────────────────────────────────────────
160
- // Analytics Queries
161
- // ─────────────────────────────────────────────────────────────────────────────
162
- /**
163
- * Get all experiments
164
- */
165
- async getExperiments() {
166
- await this.init();
167
- const result = this.session.query(`
168
- SELECT
169
- experimentId,
170
- any(experimentName) AS experimentName,
171
- uniq(variantId) AS variantCount,
172
- count() AS runCount,
173
- min(timestamp) AS firstRun,
174
- max(timestamp) AS lastRun
175
- FROM experiments
176
- WHERE experimentId != '' AND eventType = 'variant.complete'
177
- GROUP BY experimentId
178
- ORDER BY lastRun DESC
179
- `, 'JSONEachRow');
180
- if (!result.trim())
181
- return [];
182
- return result.trim().split('\n')
183
- .filter(Boolean)
184
- .map(line => JSON.parse(line));
185
- }
186
- /**
187
- * Get variant performance for an experiment
188
- */
189
- async getVariantStats(experimentId) {
190
- await this.init();
191
- const result = this.session.query(`
192
- SELECT
193
- variantId,
194
- any(variantName) AS variantName,
195
- count() AS runCount,
196
- countIf(success = 1) AS successCount,
197
- countIf(success = 1) / count() AS successRate,
198
- avg(durationMs) AS avgDuration,
199
- avg(metricValue) AS avgMetric,
200
- min(metricValue) AS minMetric,
201
- max(metricValue) AS maxMetric
202
- FROM experiments
203
- WHERE experimentId = '${this.escapeString(experimentId)}'
204
- AND eventType = 'variant.complete'
205
- GROUP BY variantId
206
- ORDER BY avgMetric DESC
207
- `, 'JSONEachRow');
208
- if (!result.trim())
209
- return [];
210
- return result.trim().split('\n')
211
- .filter(Boolean)
212
- .map(line => {
213
- const row = JSON.parse(line);
214
- return {
215
- ...row,
216
- runCount: Number(row.runCount),
217
- successCount: Number(row.successCount),
218
- successRate: Number(row.successRate),
219
- avgDuration: Number(row.avgDuration),
220
- avgMetric: Number(row.avgMetric),
221
- minMetric: Number(row.minMetric),
222
- maxMetric: Number(row.maxMetric),
223
- };
224
- });
225
- }
226
- /**
227
- * Get the best performing variant for an experiment
228
- */
229
- async getBestVariant(experimentId, options = {}) {
230
- const { metric = 'avgMetric', minimumRuns = 1 } = options;
231
- await this.init();
232
- const orderBy = metric === 'avgDuration' ? 'ASC' : 'DESC';
233
- const metricExpr = metric === 'successRate'
234
- ? 'countIf(success = 1) / count()'
235
- : metric === 'avgDuration'
236
- ? 'avg(durationMs)'
237
- : 'avg(metricValue)';
238
- const result = this.session.query(`
239
- SELECT
240
- variantId,
241
- any(variantName) AS variantName,
242
- ${metricExpr} AS metricValue,
243
- count() AS runCount
244
- FROM experiments
245
- WHERE experimentId = '${this.escapeString(experimentId)}'
246
- AND eventType = 'variant.complete'
247
- GROUP BY variantId
248
- HAVING runCount >= ${minimumRuns}
249
- ORDER BY metricValue ${orderBy}
250
- LIMIT 1
251
- `, 'JSONEachRow');
252
- if (!result.trim())
253
- return null;
254
- const row = JSON.parse(result.trim().split('\n')[0]);
255
- return {
256
- variantId: row.variantId,
257
- variantName: row.variantName,
258
- metricValue: Number(row.metricValue),
259
- runCount: Number(row.runCount),
260
- };
261
- }
262
- /**
263
- * Get cartesian product analysis - performance by dimension values
264
- */
265
- async getCartesianAnalysis(experimentId, dimension) {
266
- await this.init();
267
- const result = this.session.query(`
268
- SELECT
269
- JSONExtractString(dimensions, '${this.escapeString(dimension)}') AS dimensionValue,
270
- count() AS runCount,
271
- avg(metricValue) AS avgMetric,
272
- countIf(success = 1) / count() AS successRate
273
- FROM experiments
274
- WHERE experimentId = '${this.escapeString(experimentId)}'
275
- AND eventType = 'variant.complete'
276
- AND dimensionValue != ''
277
- GROUP BY dimensionValue
278
- ORDER BY avgMetric DESC
279
- `, 'JSONEachRow');
280
- if (!result.trim())
281
- return [];
282
- return result.trim().split('\n')
283
- .filter(Boolean)
284
- .map(line => {
285
- const row = JSON.parse(line);
286
- return {
287
- dimensionValue: row.dimensionValue,
288
- runCount: Number(row.runCount),
289
- avgMetric: Number(row.avgMetric),
290
- successRate: Number(row.successRate),
291
- };
292
- });
293
- }
294
- /**
295
- * Get multi-dimensional cartesian analysis
296
- */
297
- async getCartesianGrid(experimentId, dimensions) {
298
- await this.init();
299
- const dimExtracts = dimensions.map(d => `JSONExtractString(dimensions, '${this.escapeString(d)}') AS dim_${d}`).join(', ');
300
- const dimGroupBy = dimensions.map(d => `dim_${d}`).join(', ');
301
- const result = this.session.query(`
302
- SELECT
303
- ${dimExtracts},
304
- count() AS runCount,
305
- avg(metricValue) AS avgMetric,
306
- countIf(success = 1) / count() AS successRate
307
- FROM experiments
308
- WHERE experimentId = '${this.escapeString(experimentId)}'
309
- AND eventType = 'variant.complete'
310
- GROUP BY ${dimGroupBy}
311
- ORDER BY avgMetric DESC
312
- `, 'JSONEachRow');
313
- if (!result.trim())
314
- return [];
315
- return result.trim().split('\n')
316
- .filter(Boolean)
317
- .map(line => {
318
- const row = JSON.parse(line);
319
- const dims = {};
320
- for (const d of dimensions) {
321
- dims[d] = row[`dim_${d}`];
322
- }
323
- return {
324
- dimensions: dims,
325
- runCount: Number(row.runCount),
326
- avgMetric: Number(row.avgMetric),
327
- successRate: Number(row.successRate),
328
- };
329
- });
330
- }
331
- /**
332
- * Get time series of experiment metrics
333
- */
334
- async getTimeSeries(experimentId, options = {}) {
335
- const { interval = 'day', variantId } = options;
336
- await this.init();
337
- const dateFunc = interval === 'hour'
338
- ? 'toStartOfHour(timestamp)'
339
- : interval === 'week'
340
- ? 'toStartOfWeek(timestamp)'
341
- : 'toStartOfDay(timestamp)';
342
- const variantFilter = variantId
343
- ? `AND variantId = '${this.escapeString(variantId)}'`
344
- : '';
345
- const result = this.session.query(`
346
- SELECT
347
- ${dateFunc} AS period,
348
- count() AS runCount,
349
- avg(metricValue) AS avgMetric,
350
- countIf(success = 1) / count() AS successRate
351
- FROM experiments
352
- WHERE experimentId = '${this.escapeString(experimentId)}'
353
- AND eventType = 'variant.complete'
354
- ${variantFilter}
355
- GROUP BY period
356
- ORDER BY period ASC
357
- `, 'JSONEachRow');
358
- if (!result.trim())
359
- return [];
360
- return result.trim().split('\n')
361
- .filter(Boolean)
362
- .map(line => {
363
- const row = JSON.parse(line);
364
- return {
365
- period: row.period,
366
- runCount: Number(row.runCount),
367
- avgMetric: Number(row.avgMetric),
368
- successRate: Number(row.successRate),
369
- };
370
- });
371
- }
372
- /**
373
- * Get raw events for an experiment
374
- */
375
- async getEvents(experimentId, options = {}) {
376
- const { eventType, variantId, limit = 100 } = options;
377
- await this.init();
378
- const filters = [`experimentId = '${this.escapeString(experimentId)}'`];
379
- if (eventType)
380
- filters.push(`eventType = '${this.escapeString(eventType)}'`);
381
- if (variantId)
382
- filters.push(`variantId = '${this.escapeString(variantId)}'`);
383
- const result = this.session.query(`
384
- SELECT
385
- eventType,
386
- timestamp,
387
- experimentId,
388
- experimentName,
389
- variantId,
390
- variantName,
391
- dimensions,
392
- runId,
393
- success,
394
- durationMs,
395
- result,
396
- metricName,
397
- metricValue,
398
- errorMessage,
399
- metadata
400
- FROM experiments
401
- WHERE ${filters.join(' AND ')}
402
- ORDER BY timestamp DESC
403
- LIMIT ${limit}
404
- `, 'JSONEachRow');
405
- if (!result.trim())
406
- return [];
407
- return result.trim().split('\n')
408
- .filter(Boolean)
409
- .map(line => {
410
- const row = JSON.parse(line);
411
- return {
412
- type: row.eventType,
413
- timestamp: new Date(row.timestamp),
414
- data: {
415
- experimentId: row.experimentId,
416
- experimentName: row.experimentName,
417
- variantId: row.variantId,
418
- variantName: row.variantName,
419
- dimensions: this.parseJson(row.dimensions),
420
- runId: row.runId,
421
- success: row.success === 1,
422
- duration: row.durationMs,
423
- result: this.parseJson(row.result),
424
- metricName: row.metricName,
425
- metricValue: row.metricValue,
426
- errorMessage: row.errorMessage,
427
- metadata: this.parseJson(row.metadata),
428
- },
429
- };
430
- });
431
- }
432
- /**
433
- * Raw SQL query access for custom analytics
434
- */
435
- query(sql, format = 'JSONEachRow') {
436
- return this.session.query(sql, format);
437
- }
438
- /**
439
- * Close the storage (cleanup)
440
- */
441
- close() {
442
- // Session cleanup handled by chdb
443
- }
444
- }
445
- /**
446
- * Create a chdb storage backend for experiments
447
- *
448
- * @example
449
- * ```ts
450
- * import { configureTracking } from 'ai-experiments'
451
- * import { createChdbBackend } from 'ai-experiments/storage'
452
- *
453
- * const storage = createChdbBackend({ dataPath: './.my-experiments' })
454
- *
455
- * configureTracking({ backend: storage })
456
- *
457
- * // Later: analyze results
458
- * const best = await storage.getBestVariant('my-experiment')
459
- * console.log(`Best variant: ${best.variantName} (${best.metricValue})`)
460
- * ```
461
- */
462
- export function createChdbBackend(options) {
463
- return new ChdbStorage(options);
464
- }