ai-experiments 0.1.0 → 2.0.2

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.
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Event tracking for experiments
3
+ */
4
+
5
+ import type { TrackingEvent, TrackingBackend, TrackingOptions } from './types.js'
6
+
7
+ /**
8
+ * Default tracking configuration
9
+ */
10
+ let trackingConfig: Required<TrackingOptions> = {
11
+ backend: createConsoleBackend(),
12
+ enabled: true,
13
+ metadata: {},
14
+ }
15
+
16
+ /**
17
+ * Configure tracking
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { configureTracking } from 'ai-experiments'
22
+ *
23
+ * // Use console backend (default)
24
+ * configureTracking({
25
+ * enabled: true,
26
+ * metadata: { projectId: 'my-project' },
27
+ * })
28
+ *
29
+ * // Use custom backend
30
+ * configureTracking({
31
+ * backend: {
32
+ * track: async (event) => {
33
+ * await fetch('/api/analytics', {
34
+ * method: 'POST',
35
+ * body: JSON.stringify(event),
36
+ * })
37
+ * },
38
+ * },
39
+ * })
40
+ *
41
+ * // Disable tracking
42
+ * configureTracking({ enabled: false })
43
+ * ```
44
+ */
45
+ export function configureTracking(options: TrackingOptions): void {
46
+ trackingConfig = {
47
+ backend: options.backend ?? trackingConfig.backend,
48
+ enabled: options.enabled ?? trackingConfig.enabled,
49
+ metadata: options.metadata ?? trackingConfig.metadata,
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Track an event
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { track } from 'ai-experiments'
59
+ *
60
+ * track({
61
+ * type: 'experiment.start',
62
+ * timestamp: new Date(),
63
+ * data: {
64
+ * experimentId: 'my-experiment',
65
+ * variantCount: 3,
66
+ * },
67
+ * })
68
+ * ```
69
+ */
70
+ export function track(event: TrackingEvent): void {
71
+ if (!trackingConfig.enabled) {
72
+ return
73
+ }
74
+
75
+ // Merge global metadata
76
+ const enrichedEvent: TrackingEvent = {
77
+ ...event,
78
+ data: {
79
+ ...event.data,
80
+ ...trackingConfig.metadata,
81
+ },
82
+ }
83
+
84
+ // Track via backend (handle both sync and async)
85
+ const result = trackingConfig.backend.track(enrichedEvent)
86
+ if (result instanceof Promise) {
87
+ // Don't await - fire and forget
88
+ result.catch((error) => {
89
+ console.error('Error tracking event:', error)
90
+ })
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Flush pending events
96
+ *
97
+ * Call this before the process exits to ensure all events are sent.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { flush } from 'ai-experiments'
102
+ *
103
+ * process.on('SIGINT', async () => {
104
+ * await flush()
105
+ * process.exit(0)
106
+ * })
107
+ * ```
108
+ */
109
+ export async function flush(): Promise<void> {
110
+ if (trackingConfig.backend.flush) {
111
+ await trackingConfig.backend.flush()
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Create a console-based tracking backend
117
+ *
118
+ * Logs events to console.log in a human-readable format.
119
+ */
120
+ export function createConsoleBackend(options?: {
121
+ /** Whether to include full event data (default: false) */
122
+ verbose?: boolean
123
+ }): TrackingBackend {
124
+ const { verbose = false } = options ?? {}
125
+
126
+ return {
127
+ track: (event: TrackingEvent) => {
128
+ const timestamp = event.timestamp.toISOString()
129
+
130
+ if (verbose) {
131
+ console.log(`[${timestamp}] ${event.type}`, event.data)
132
+ } else {
133
+ // Condensed format
134
+ const key = extractKey(event)
135
+ console.log(`[${timestamp}] ${event.type} ${key}`)
136
+ }
137
+ },
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Create an in-memory tracking backend that stores events
143
+ *
144
+ * Useful for testing or collecting events for batch processing.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * import { createMemoryBackend } from 'ai-experiments'
149
+ *
150
+ * const backend = createMemoryBackend()
151
+ * configureTracking({ backend })
152
+ *
153
+ * // Run experiments...
154
+ *
155
+ * // Get all events
156
+ * const events = backend.getEvents()
157
+ * console.log(`Tracked ${events.length} events`)
158
+ *
159
+ * // Clear events
160
+ * backend.clear()
161
+ * ```
162
+ */
163
+ export function createMemoryBackend(): TrackingBackend & {
164
+ getEvents: () => TrackingEvent[]
165
+ clear: () => void
166
+ } {
167
+ const events: TrackingEvent[] = []
168
+
169
+ return {
170
+ track: (event: TrackingEvent) => {
171
+ events.push(event)
172
+ },
173
+ getEvents: () => [...events],
174
+ clear: () => {
175
+ events.length = 0
176
+ },
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Create a batching tracking backend
182
+ *
183
+ * Batches events and sends them in groups to reduce network overhead.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { createBatchBackend } from 'ai-experiments'
188
+ *
189
+ * const backend = createBatchBackend({
190
+ * batchSize: 10,
191
+ * flushInterval: 5000, // 5 seconds
192
+ * send: async (events) => {
193
+ * await fetch('/api/analytics/batch', {
194
+ * method: 'POST',
195
+ * body: JSON.stringify({ events }),
196
+ * })
197
+ * },
198
+ * })
199
+ *
200
+ * configureTracking({ backend })
201
+ * ```
202
+ */
203
+ export function createBatchBackend(options: {
204
+ /** Maximum batch size before auto-flush */
205
+ batchSize: number
206
+ /** Interval in ms to auto-flush (default: no auto-flush) */
207
+ flushInterval?: number
208
+ /** Function to send batched events */
209
+ send: (events: TrackingEvent[]) => Promise<void>
210
+ }): TrackingBackend {
211
+ const { batchSize, flushInterval, send } = options
212
+ const batch: TrackingEvent[] = []
213
+ let flushTimer: NodeJS.Timeout | null = null
214
+
215
+ const flush = async () => {
216
+ if (batch.length === 0) return
217
+
218
+ const eventsToSend = [...batch]
219
+ batch.length = 0
220
+
221
+ try {
222
+ await send(eventsToSend)
223
+ } catch (error) {
224
+ console.error('Error sending batch:', error)
225
+ // Re-add failed events to batch (simple retry strategy)
226
+ batch.unshift(...eventsToSend)
227
+ }
228
+ }
229
+
230
+ const scheduleFlush = () => {
231
+ if (flushTimer) {
232
+ clearTimeout(flushTimer)
233
+ }
234
+ if (flushInterval) {
235
+ flushTimer = setTimeout(() => {
236
+ flush().catch(console.error)
237
+ }, flushInterval)
238
+ }
239
+ }
240
+
241
+ return {
242
+ track: (event: TrackingEvent) => {
243
+ batch.push(event)
244
+
245
+ // Auto-flush if batch is full
246
+ if (batch.length >= batchSize) {
247
+ flush().catch(console.error)
248
+ } else {
249
+ scheduleFlush()
250
+ }
251
+ },
252
+ flush: async () => {
253
+ if (flushTimer) {
254
+ clearTimeout(flushTimer)
255
+ flushTimer = null
256
+ }
257
+ await flush()
258
+ },
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Create a file-based tracking backend
264
+ *
265
+ * Writes events to a file (JSONL format).
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * import { createFileBackend } from 'ai-experiments'
270
+ *
271
+ * const backend = createFileBackend({
272
+ * path: './experiments.jsonl',
273
+ * })
274
+ *
275
+ * configureTracking({ backend })
276
+ * ```
277
+ */
278
+ export function createFileBackend(options: {
279
+ /** Path to the file */
280
+ path: string
281
+ }): TrackingBackend {
282
+ // Note: This requires Node.js fs module
283
+ // Import dynamically to avoid breaking in non-Node environments
284
+ let fs: typeof import('fs') | null = null
285
+ let writeStream: ReturnType<typeof import('fs').createWriteStream> | null = null
286
+
287
+ const ensureStream = async () => {
288
+ if (!writeStream) {
289
+ try {
290
+ fs = await import('fs')
291
+ writeStream = fs.createWriteStream(options.path, { flags: 'a' })
292
+ } catch (error) {
293
+ console.error('Failed to create file stream:', error)
294
+ throw error
295
+ }
296
+ }
297
+ return writeStream
298
+ }
299
+
300
+ return {
301
+ track: async (event: TrackingEvent) => {
302
+ try {
303
+ const stream = await ensureStream()
304
+ const line = JSON.stringify(event) + '\n'
305
+ stream.write(line)
306
+ } catch (error) {
307
+ console.error('Failed to write event to file:', error)
308
+ }
309
+ },
310
+ flush: async () => {
311
+ if (writeStream) {
312
+ return new Promise<void>((resolve, reject) => {
313
+ writeStream!.end((error?: Error) => {
314
+ if (error) reject(error)
315
+ else resolve()
316
+ })
317
+ })
318
+ }
319
+ },
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Extract a key identifier from event data for logging
325
+ */
326
+ function extractKey(event: TrackingEvent): string {
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}`
331
+ return ''
332
+ }
333
+
334
+ /**
335
+ * Get current tracking configuration
336
+ */
337
+ export function getTrackingConfig(): Readonly<Required<TrackingOptions>> {
338
+ return { ...trackingConfig }
339
+ }
package/src/types.ts ADDED
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Core types for AI experimentation
3
+ */
4
+
5
+ /**
6
+ * A variant within an experiment
7
+ */
8
+ export interface ExperimentVariant<TConfig = unknown> {
9
+ /** Unique identifier for the variant */
10
+ id: string
11
+ /** Human-readable name */
12
+ name: string
13
+ /** Variant configuration */
14
+ config: TConfig
15
+ /** Weight for weighted random selection (default: 1) */
16
+ weight?: number
17
+ /** Optional description */
18
+ description?: string
19
+ }
20
+
21
+ /**
22
+ * Configuration for an experiment
23
+ */
24
+ export interface ExperimentConfig<TConfig = unknown, TResult = unknown> {
25
+ /** Unique experiment identifier */
26
+ id: string
27
+ /** Human-readable name */
28
+ name: string
29
+ /** Experiment description */
30
+ description?: string
31
+ /** List of variants to test */
32
+ variants: ExperimentVariant<TConfig>[]
33
+ /** Function to execute for each variant */
34
+ execute: (config: TConfig, context?: ExperimentContext) => Promise<TResult> | TResult
35
+ /** Optional success metric function */
36
+ metric?: (result: TResult) => number | Promise<number>
37
+ /** Metadata for the experiment */
38
+ metadata?: Record<string, unknown>
39
+ }
40
+
41
+ /**
42
+ * Context passed to experiment execution
43
+ */
44
+ export interface ExperimentContext {
45
+ /** Experiment ID */
46
+ experimentId: string
47
+ /** Variant ID */
48
+ variantId: string
49
+ /** Run ID (unique per execution) */
50
+ runId: string
51
+ /** Timestamp when execution started */
52
+ startedAt: Date
53
+ /** Additional context data */
54
+ data?: Record<string, unknown>
55
+ }
56
+
57
+ /**
58
+ * Result of executing an experiment variant
59
+ */
60
+ export interface ExperimentResult<TResult = unknown> {
61
+ /** Experiment ID */
62
+ experimentId: string
63
+ /** Variant ID */
64
+ variantId: string
65
+ /** Variant name */
66
+ variantName: string
67
+ /** Run ID */
68
+ runId: string
69
+ /** Execution result */
70
+ result: TResult
71
+ /** Computed metric value (if metric function provided) */
72
+ metricValue?: number
73
+ /** Execution duration in milliseconds */
74
+ duration: number
75
+ /** Timestamp when execution started */
76
+ startedAt: Date
77
+ /** Timestamp when execution completed */
78
+ completedAt: Date
79
+ /** Error if execution failed */
80
+ error?: Error
81
+ /** Success flag */
82
+ success: boolean
83
+ /** Additional metadata */
84
+ metadata?: Record<string, unknown>
85
+ }
86
+
87
+ /**
88
+ * Summary of experiment results across all variants
89
+ */
90
+ export interface ExperimentSummary<TResult = unknown> {
91
+ /** Experiment ID */
92
+ experimentId: string
93
+ /** Experiment name */
94
+ experimentName: string
95
+ /** Results for each variant */
96
+ results: ExperimentResult<TResult>[]
97
+ /** Best performing variant (by metric) */
98
+ bestVariant?: {
99
+ variantId: string
100
+ variantName: string
101
+ metricValue: number
102
+ }
103
+ /** Total execution duration */
104
+ totalDuration: number
105
+ /** Number of successful runs */
106
+ successCount: number
107
+ /** Number of failed runs */
108
+ failureCount: number
109
+ /** Timestamp when experiment started */
110
+ startedAt: Date
111
+ /** Timestamp when experiment completed */
112
+ completedAt: Date
113
+ }
114
+
115
+ /**
116
+ * Options for running an experiment
117
+ */
118
+ export interface RunExperimentOptions {
119
+ /** Run variants in parallel (default: true) */
120
+ parallel?: boolean
121
+ /** Maximum concurrent executions (default: unlimited) */
122
+ maxConcurrency?: number
123
+ /** Stop on first error (default: false) */
124
+ stopOnError?: boolean
125
+ /** Custom context data */
126
+ context?: Record<string, unknown>
127
+ /** Event callbacks */
128
+ onVariantStart?: (variantId: string, variantName: string) => void
129
+ onVariantComplete?: (result: ExperimentResult) => void
130
+ onVariantError?: (variantId: string, error: Error) => void
131
+ }
132
+
133
+ /**
134
+ * Parameters for cartesian product generation
135
+ */
136
+ export type CartesianParams = Record<string, unknown[]>
137
+
138
+ /**
139
+ * Result of cartesian product - array of parameter combinations
140
+ */
141
+ export type CartesianResult<T extends CartesianParams> = Array<{
142
+ [K in keyof T]: T[K][number]
143
+ }>
144
+
145
+ /**
146
+ * Decision options
147
+ */
148
+ export interface DecideOptions<T> {
149
+ /** Options to choose from */
150
+ options: T[]
151
+ /** Scoring function for each option */
152
+ score: (option: T) => number | Promise<number>
153
+ /** Context or prompt for decision making */
154
+ context?: string
155
+ /** Whether to return all options sorted by score (default: false) */
156
+ returnAll?: boolean
157
+ }
158
+
159
+ /**
160
+ * Result of a decision
161
+ */
162
+ export interface DecisionResult<T> {
163
+ /** The selected option */
164
+ selected: T
165
+ /** Score of the selected option */
166
+ score: number
167
+ /** All options with their scores (if returnAll was true) */
168
+ allOptions?: Array<{ option: T; score: number }>
169
+ }
170
+
171
+ /**
172
+ * Tracking event types
173
+ */
174
+ export type TrackingEventType =
175
+ | 'experiment.start'
176
+ | 'experiment.complete'
177
+ | 'variant.start'
178
+ | 'variant.complete'
179
+ | 'variant.error'
180
+ | 'metric.computed'
181
+ | 'decision.made'
182
+
183
+ /**
184
+ * Tracking event
185
+ */
186
+ export interface TrackingEvent {
187
+ /** Event type */
188
+ type: TrackingEventType
189
+ /** Timestamp */
190
+ timestamp: Date
191
+ /** Event data */
192
+ data: Record<string, unknown>
193
+ }
194
+
195
+ /**
196
+ * Tracking backend interface
197
+ */
198
+ export interface TrackingBackend {
199
+ /** Track an event */
200
+ track(event: TrackingEvent): void | Promise<void>
201
+ /** Flush pending events */
202
+ flush?(): void | Promise<void>
203
+ }
204
+
205
+ /**
206
+ * Options for tracking configuration
207
+ */
208
+ export interface TrackingOptions {
209
+ /** Custom tracking backend */
210
+ backend?: TrackingBackend
211
+ /** Whether tracking is enabled (default: true) */
212
+ enabled?: boolean
213
+ /** Additional metadata to include with all events */
214
+ metadata?: Record<string, unknown>
215
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
9
+ }