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/decide.ts CHANGED
@@ -52,9 +52,7 @@ import { track } from './tracking.js'
52
52
  * // ]
53
53
  * ```
54
54
  */
55
- export async function decide<T>(
56
- options: DecideOptions<T>
57
- ): Promise<DecisionResult<T>> {
55
+ export async function decide<T>(options: DecideOptions<T>): Promise<DecisionResult<T>> {
58
56
  const { options: choices, score, context, returnAll = false } = options
59
57
 
60
58
  if (choices.length === 0) {
@@ -117,9 +115,7 @@ export async function decide<T>(
117
115
  * console.log(result) // Most likely 'A', but could be B or C
118
116
  * ```
119
117
  */
120
- export function decideWeighted<T>(
121
- options: Array<{ value: T; weight: number }>
122
- ): T {
118
+ export function decideWeighted<T>(options: Array<{ value: T; weight: number }>): T {
123
119
  if (options.length === 0) {
124
120
  throw new Error('Cannot decide with empty options')
125
121
  }
@@ -197,7 +193,11 @@ export async function decideEpsilonGreedy<T>(
197
193
  }
198
194
 
199
195
  // Exploitation: best option
200
- const result = await decide({ options: choices, score, context })
196
+ const result = await decide({
197
+ options: choices,
198
+ score,
199
+ ...(context !== undefined && { context }),
200
+ })
201
201
 
202
202
  track({
203
203
  type: 'decision.made',
@@ -252,9 +252,7 @@ export function decideThompsonSampling<T extends string>(
252
252
  })
253
253
 
254
254
  // Select option with highest sample
255
- const best = samples.reduce((prev, current) =>
256
- current.sample > prev.sample ? current : prev
257
- )
255
+ const best = samples.reduce((prev, current) => (current.sample > prev.sample ? current : prev))
258
256
 
259
257
  track({
260
258
  type: 'decision.made',
package/src/experiment.ts CHANGED
@@ -82,9 +82,9 @@ export async function Experiment<TConfig = unknown, TResult = unknown>(
82
82
  variant: ExperimentVariant<TConfig>
83
83
  ): Promise<ExperimentResult<TResult>> => {
84
84
  return runVariant(config, variant, contextData, {
85
- onVariantStart,
86
- onVariantComplete,
87
- onVariantError,
85
+ ...(onVariantStart !== undefined && { onVariantStart }),
86
+ ...(onVariantComplete !== undefined && { onVariantComplete }),
87
+ ...(onVariantError !== undefined && { onVariantError }),
88
88
  })
89
89
  }
90
90
 
@@ -109,9 +109,9 @@ export async function Experiment<TConfig = unknown, TResult = unknown>(
109
109
  // Sequential execution
110
110
  for (const variant of config.variants) {
111
111
  const result = await runVariant(config, variant, contextData, {
112
- onVariantStart,
113
- onVariantComplete,
114
- onVariantError,
112
+ ...(onVariantStart !== undefined && { onVariantStart }),
113
+ ...(onVariantComplete !== undefined && { onVariantComplete }),
114
+ ...(onVariantError !== undefined && { onVariantError }),
115
115
  })
116
116
  results.push(result)
117
117
 
@@ -143,7 +143,7 @@ export async function Experiment<TConfig = unknown, TResult = unknown>(
143
143
  experimentId: config.id,
144
144
  experimentName: config.name,
145
145
  results,
146
- bestVariant,
146
+ ...(bestVariant !== undefined && { bestVariant }),
147
147
  totalDuration,
148
148
  successCount: results.filter((r) => r.success).length,
149
149
  failureCount: results.filter((r) => !r.success).length,
@@ -190,7 +190,7 @@ async function runVariant<TConfig, TResult>(
190
190
  variantId: variant.id,
191
191
  runId,
192
192
  startedAt: startTime,
193
- data: contextData,
193
+ ...(contextData !== undefined && { data: contextData }),
194
194
  }
195
195
 
196
196
  // Track variant start
@@ -236,7 +236,7 @@ async function runVariant<TConfig, TResult>(
236
236
  variantName: variant.name,
237
237
  runId,
238
238
  result,
239
- metricValue,
239
+ ...(metricValue !== undefined && { metricValue }),
240
240
  duration,
241
241
  startedAt: startTime,
242
242
  completedAt: endTime,
@@ -332,9 +332,9 @@ export function createVariantsFromGrid<T extends Record<string, unknown[]>>(
332
332
  const combinations = cartesianProduct(values)
333
333
 
334
334
  return combinations.map((combo, index) => {
335
- const config = Object.fromEntries(
336
- keys.map((key, i) => [key, combo[i]])
337
- ) as { [K in keyof T]: T[K][number] }
335
+ const config = Object.fromEntries(keys.map((key, i) => [key, combo[i]])) as {
336
+ [K in keyof T]: T[K][number]
337
+ }
338
338
 
339
339
  return {
340
340
  id: `variant-${index}`,
package/src/index.ts CHANGED
@@ -43,6 +43,16 @@ export {
43
43
  createFileBackend,
44
44
  } from './tracking.js'
45
45
 
46
- // Export ClickHouse storage backend
47
- export { ChdbStorage, createChdbBackend } from './chdb-storage.js'
48
- export type { ChdbStorageOptions } from './chdb-storage.js'
46
+ // Export ClickHouse storage backend (canonical adapter via ai-database)
47
+ export {
48
+ ClickHouseExperimentStorage,
49
+ createClickHouseExperimentStorage,
50
+ bootstrapExperimentsSchema,
51
+ // Backwards-compat aliases
52
+ ChdbStorage,
53
+ createChdbBackend,
54
+ } from './clickhouse-storage.js'
55
+ export type {
56
+ ClickHouseExperimentStorageOptions,
57
+ ChdbStorageOptions,
58
+ } from './clickhouse-storage.js'
package/src/tracking.ts CHANGED
@@ -325,9 +325,9 @@ export function createFileBackend(options: {
325
325
  */
326
326
  function extractKey(event: TrackingEvent): string {
327
327
  const data = event.data
328
- if ('experimentId' in data) return `exp=${data.experimentId}`
329
- if ('variantId' in data) return `variant=${data.variantId}`
330
- if ('runId' in data) return `run=${data.runId}`
328
+ if ('experimentId' in data) return `exp=${data['experimentId']}`
329
+ if ('variantId' in data) return `variant=${data['variantId']}`
330
+ if ('runId' in data) return `run=${data['runId']}`
331
331
  return ''
332
332
  }
333
333
 
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Tests for cartesian product utilities
3
+ *
4
+ * These tests verify the parameter exploration functionality including:
5
+ * - Basic cartesian product generation
6
+ * - Filtering combinations
7
+ * - Sampling from large spaces
8
+ * - Counting combinations
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest'
12
+ import {
13
+ cartesian,
14
+ cartesianFilter,
15
+ cartesianSample,
16
+ cartesianCount,
17
+ cartesianWithLabels,
18
+ } from '../src/index.js'
19
+
20
+ describe('cartesian', () => {
21
+ describe('basic functionality', () => {
22
+ it('generates all combinations for single parameter', () => {
23
+ const result = cartesian({
24
+ color: ['red', 'blue', 'green'],
25
+ })
26
+
27
+ expect(result).toHaveLength(3)
28
+ expect(result).toContainEqual({ color: 'red' })
29
+ expect(result).toContainEqual({ color: 'blue' })
30
+ expect(result).toContainEqual({ color: 'green' })
31
+ })
32
+
33
+ it('generates cartesian product for two parameters', () => {
34
+ const result = cartesian({
35
+ size: ['small', 'large'],
36
+ color: ['red', 'blue'],
37
+ })
38
+
39
+ expect(result).toHaveLength(4)
40
+ expect(result).toContainEqual({ size: 'small', color: 'red' })
41
+ expect(result).toContainEqual({ size: 'small', color: 'blue' })
42
+ expect(result).toContainEqual({ size: 'large', color: 'red' })
43
+ expect(result).toContainEqual({ size: 'large', color: 'blue' })
44
+ })
45
+
46
+ it('generates cartesian product for three parameters', () => {
47
+ const result = cartesian({
48
+ model: ['a', 'b'],
49
+ temperature: [0.3, 0.7],
50
+ maxTokens: [100, 500],
51
+ })
52
+
53
+ // 2 * 2 * 2 = 8
54
+ expect(result).toHaveLength(8)
55
+ expect(result).toContainEqual({ model: 'a', temperature: 0.3, maxTokens: 100 })
56
+ expect(result).toContainEqual({ model: 'b', temperature: 0.7, maxTokens: 500 })
57
+ })
58
+
59
+ it('returns empty array for empty input', () => {
60
+ const result = cartesian({})
61
+ expect(result).toEqual([])
62
+ })
63
+
64
+ it('handles mixed types', () => {
65
+ const result = cartesian({
66
+ count: [1, 2],
67
+ enabled: [true, false],
68
+ name: ['test'],
69
+ })
70
+
71
+ expect(result).toHaveLength(4)
72
+ expect(result).toContainEqual({ count: 1, enabled: true, name: 'test' })
73
+ expect(result).toContainEqual({ count: 2, enabled: false, name: 'test' })
74
+ })
75
+
76
+ it('preserves object values', () => {
77
+ const objA = { complex: true }
78
+ const objB = { complex: false }
79
+
80
+ const result = cartesian({
81
+ config: [objA, objB],
82
+ })
83
+
84
+ expect(result).toHaveLength(2)
85
+ expect(result[0].config).toBe(objA)
86
+ expect(result[1].config).toBe(objB)
87
+ })
88
+ })
89
+ })
90
+
91
+ describe('cartesianFilter', () => {
92
+ it('filters out unwanted combinations', () => {
93
+ const result = cartesianFilter(
94
+ {
95
+ model: ['sonnet', 'opus'],
96
+ temperature: [0.3, 0.7, 1.0],
97
+ },
98
+ // Filter out opus with high temperature
99
+ (combo) => !(combo.model === 'opus' && combo.temperature > 0.7)
100
+ )
101
+
102
+ // 6 total - 1 (opus with 1.0) = 5
103
+ expect(result).toHaveLength(5)
104
+ expect(result).not.toContainEqual({ model: 'opus', temperature: 1.0 })
105
+ })
106
+
107
+ it('returns empty array when all filtered', () => {
108
+ const result = cartesianFilter({ x: [1, 2, 3] }, () => false)
109
+
110
+ expect(result).toEqual([])
111
+ })
112
+
113
+ it('returns all when nothing filtered', () => {
114
+ const result = cartesianFilter({ x: [1, 2] }, () => true)
115
+
116
+ expect(result).toHaveLength(2)
117
+ })
118
+
119
+ it('works with complex filter conditions', () => {
120
+ const result = cartesianFilter(
121
+ {
122
+ a: [1, 2, 3],
123
+ b: [10, 20, 30],
124
+ },
125
+ // Only keep combinations where a * b < 50
126
+ (combo) => combo.a * combo.b < 50
127
+ )
128
+
129
+ // Valid: (1,10), (1,20), (1,30), (2,10), (2,20), (3,10)
130
+ expect(result).toHaveLength(6)
131
+ expect(result).not.toContainEqual({ a: 2, b: 30 })
132
+ expect(result).not.toContainEqual({ a: 3, b: 20 })
133
+ expect(result).not.toContainEqual({ a: 3, b: 30 })
134
+ })
135
+ })
136
+
137
+ describe('cartesianSample', () => {
138
+ it('returns subset of specified size', () => {
139
+ const result = cartesianSample(
140
+ {
141
+ a: [1, 2, 3, 4, 5],
142
+ b: [1, 2, 3, 4, 5],
143
+ },
144
+ 5
145
+ )
146
+
147
+ expect(result).toHaveLength(5)
148
+ })
149
+
150
+ it('returns all combinations when sample size exceeds total', () => {
151
+ const result = cartesianSample({ x: [1, 2] }, 100)
152
+
153
+ expect(result).toHaveLength(2)
154
+ })
155
+
156
+ it('returns unique combinations by default', () => {
157
+ const result = cartesianSample({ a: [1, 2, 3], b: [1, 2, 3] }, 5)
158
+
159
+ const serialized = result.map((r) => JSON.stringify(r))
160
+ const unique = new Set(serialized)
161
+
162
+ expect(unique.size).toBe(serialized.length)
163
+ })
164
+
165
+ it('returns valid combinations', () => {
166
+ const result = cartesianSample({ color: ['red', 'blue'], size: ['small', 'large'] }, 3)
167
+
168
+ const validColors = ['red', 'blue']
169
+ const validSizes = ['small', 'large']
170
+
171
+ for (const combo of result) {
172
+ expect(validColors).toContain(combo.color)
173
+ expect(validSizes).toContain(combo.size)
174
+ }
175
+ })
176
+
177
+ it('handles sample size of 0', () => {
178
+ const result = cartesianSample({ x: [1, 2, 3] }, 0)
179
+
180
+ expect(result).toHaveLength(0)
181
+ })
182
+
183
+ it('handles empty parameters', () => {
184
+ const result = cartesianSample({}, 10)
185
+ expect(result).toEqual([])
186
+ })
187
+ })
188
+
189
+ describe('cartesianCount', () => {
190
+ it('counts single parameter correctly', () => {
191
+ const count = cartesianCount({
192
+ x: [1, 2, 3, 4, 5],
193
+ })
194
+
195
+ expect(count).toBe(5)
196
+ })
197
+
198
+ it('counts product of multiple parameters', () => {
199
+ const count = cartesianCount({
200
+ a: [1, 2, 3],
201
+ b: ['x', 'y'],
202
+ c: [true, false],
203
+ })
204
+
205
+ // 3 * 2 * 2 = 12
206
+ expect(count).toBe(12)
207
+ })
208
+
209
+ it('returns 0 for empty parameters', () => {
210
+ const count = cartesianCount({})
211
+ expect(count).toBe(0)
212
+ })
213
+
214
+ it('returns 0 if any parameter has empty array', () => {
215
+ const count = cartesianCount({
216
+ a: [1, 2, 3],
217
+ b: [],
218
+ })
219
+
220
+ expect(count).toBe(0)
221
+ })
222
+
223
+ it('handles large parameter spaces efficiently', () => {
224
+ // This would be 10^6 = 1,000,000 combinations
225
+ // Count should be fast even though generating would be slow
226
+ const count = cartesianCount({
227
+ a: Array.from({ length: 100 }, (_, i) => i),
228
+ b: Array.from({ length: 100 }, (_, i) => i),
229
+ c: Array.from({ length: 100 }, (_, i) => i),
230
+ })
231
+
232
+ expect(count).toBe(1000000)
233
+ })
234
+ })
235
+
236
+ describe('cartesianWithLabels', () => {
237
+ it('returns values with index labels', () => {
238
+ const result = cartesianWithLabels({
239
+ model: ['sonnet', 'opus'],
240
+ temperature: [0.3, 0.7],
241
+ })
242
+
243
+ expect(result).toHaveLength(4)
244
+
245
+ // Check first combination
246
+ const first = result.find((r) => r.values.model === 'sonnet' && r.values.temperature === 0.3)
247
+ expect(first).toBeDefined()
248
+ expect(first?.labels.model).toBe(0)
249
+ expect(first?.labels.temperature).toBe(0)
250
+
251
+ // Check last combination
252
+ const last = result.find((r) => r.values.model === 'opus' && r.values.temperature === 0.7)
253
+ expect(last).toBeDefined()
254
+ expect(last?.labels.model).toBe(1)
255
+ expect(last?.labels.temperature).toBe(1)
256
+ })
257
+
258
+ it('labels correspond to array positions', () => {
259
+ const options = ['a', 'b', 'c']
260
+ const result = cartesianWithLabels({
261
+ option: options,
262
+ })
263
+
264
+ for (let i = 0; i < options.length; i++) {
265
+ const item = result.find((r) => r.values.option === options[i])
266
+ expect(item?.labels.option).toBe(i)
267
+ }
268
+ })
269
+
270
+ it('returns empty array for empty input', () => {
271
+ const result = cartesianWithLabels({})
272
+ expect(result).toEqual([])
273
+ })
274
+
275
+ it('provides both values and labels in each result', () => {
276
+ const result = cartesianWithLabels({
277
+ x: [10, 20],
278
+ })
279
+
280
+ for (const item of result) {
281
+ expect(item).toHaveProperty('values')
282
+ expect(item).toHaveProperty('labels')
283
+ expect(typeof item.values.x).toBe('number')
284
+ expect(typeof item.labels.x).toBe('number')
285
+ }
286
+ })
287
+ })