@botpress/zai 2.4.1 → 2.5.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.
package/src/response.ts CHANGED
@@ -1,13 +1,110 @@
1
1
  import { Usage, ZaiContext } from './context'
2
2
  import { EventEmitter } from './emitter'
3
3
 
4
- // Event types for the Response class
4
+ /**
5
+ * Event types emitted during operation execution.
6
+ *
7
+ * @property progress - Emitted periodically with usage statistics (tokens, cost, requests)
8
+ * @property complete - Emitted when operation completes successfully with the result
9
+ * @property error - Emitted when operation fails with the error
10
+ */
5
11
  export type ResponseEvents<TComplete = any> = {
12
+ /** Emitted during execution with updated usage statistics */
6
13
  progress: Usage
14
+ /** Emitted when the operation completes with the full result */
7
15
  complete: TComplete
16
+ /** Emitted when the operation fails with an error */
8
17
  error: unknown
9
18
  }
10
19
 
20
+ /**
21
+ * Promise-like wrapper for Zai operations with observability and control.
22
+ *
23
+ * Response provides a dual-value system:
24
+ * - **Simplified value**: When awaited directly, returns a simplified result (e.g., boolean for `check()`)
25
+ * - **Full result**: Via `.result()` method, returns `{ output, usage, elapsed }`
26
+ *
27
+ * All Zai operations return a Response instance, allowing you to:
28
+ * - Track progress and usage in real-time
29
+ * - Abort operations
30
+ * - Bind to external abort signals
31
+ * - Get detailed cost and performance metrics
32
+ *
33
+ * @template T - The full output type
34
+ * @template S - The simplified output type (defaults to T)
35
+ *
36
+ * @example Basic usage (simplified)
37
+ * ```typescript
38
+ * // Simplified result (boolean)
39
+ * const isPositive = await zai.check(review, 'Is this positive?')
40
+ * console.log(isPositive) // true or false
41
+ * ```
42
+ *
43
+ * @example Full result with usage
44
+ * ```typescript
45
+ * const response = zai.check(review, 'Is this positive?')
46
+ * const { output, usage, elapsed } = await response.result()
47
+ *
48
+ * console.log(output.value) // true/false
49
+ * console.log(output.explanation) // "The review expresses satisfaction..."
50
+ * console.log(usage.tokens.total) // 150
51
+ * console.log(usage.cost.total) // 0.002
52
+ * console.log(elapsed) // 1234 (ms)
53
+ * ```
54
+ *
55
+ * @example Progress tracking
56
+ * ```typescript
57
+ * const response = zai.summarize(longDocument, { length: 500 })
58
+ *
59
+ * response.on('progress', (usage) => {
60
+ * console.log(`Progress: ${usage.requests.percentage * 100}%`)
61
+ * console.log(`Tokens: ${usage.tokens.total}`)
62
+ * console.log(`Cost: $${usage.cost.total}`)
63
+ * })
64
+ *
65
+ * const summary = await response
66
+ * ```
67
+ *
68
+ * @example Aborting operations
69
+ * ```typescript
70
+ * const response = zai.extract(hugeDocument, schema)
71
+ *
72
+ * // Abort after 5 seconds
73
+ * setTimeout(() => response.abort('Timeout'), 5000)
74
+ *
75
+ * try {
76
+ * const result = await response
77
+ * } catch (error) {
78
+ * console.log('Operation aborted:', error)
79
+ * }
80
+ * ```
81
+ *
82
+ * @example External abort signal
83
+ * ```typescript
84
+ * const controller = new AbortController()
85
+ * const response = zai.answer(documents, question).bindSignal(controller.signal)
86
+ *
87
+ * // User clicks cancel button
88
+ * cancelButton.onclick = () => controller.abort()
89
+ *
90
+ * const answer = await response
91
+ * ```
92
+ *
93
+ * @example Error handling
94
+ * ```typescript
95
+ * const response = zai.extract(text, schema)
96
+ *
97
+ * response.on('error', (error) => {
98
+ * console.error('Operation failed:', error)
99
+ * })
100
+ *
101
+ * try {
102
+ * const result = await response
103
+ * } catch (error) {
104
+ * // Handle error
105
+ * }
106
+ * ```
107
+ */
11
108
  export class Response<T = any, S = T> implements PromiseLike<S> {
12
109
  private _promise: Promise<T>
13
110
  private _eventEmitter: EventEmitter<ResponseEvents<T>>
@@ -41,22 +138,107 @@ export class Response<T = any, S = T> implements PromiseLike<S> {
41
138
  })
42
139
  }
43
140
 
44
- // Event emitter methods
141
+ /**
142
+ * Subscribes to events emitted during operation execution.
143
+ *
144
+ * @param type - Event type: 'progress', 'complete', or 'error'
145
+ * @param listener - Callback function to handle the event
146
+ * @returns This Response instance for chaining
147
+ *
148
+ * @example Track progress
149
+ * ```typescript
150
+ * response.on('progress', (usage) => {
151
+ * console.log(`${usage.requests.percentage * 100}% complete`)
152
+ * console.log(`Cost: $${usage.cost.total}`)
153
+ * })
154
+ * ```
155
+ *
156
+ * @example Handle completion
157
+ * ```typescript
158
+ * response.on('complete', (result) => {
159
+ * console.log('Operation completed:', result)
160
+ * })
161
+ * ```
162
+ *
163
+ * @example Handle errors
164
+ * ```typescript
165
+ * response.on('error', (error) => {
166
+ * console.error('Operation failed:', error)
167
+ * })
168
+ * ```
169
+ */
45
170
  public on<K extends keyof ResponseEvents<T>>(type: K, listener: (event: ResponseEvents<T>[K]) => void) {
46
171
  this._eventEmitter.on(type, listener)
47
172
  return this
48
173
  }
49
174
 
175
+ /**
176
+ * Unsubscribes from events.
177
+ *
178
+ * @param type - Event type to unsubscribe from
179
+ * @param listener - The exact listener function to remove
180
+ * @returns This Response instance for chaining
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * const progressHandler = (usage) => console.log(usage.tokens.total)
185
+ * response.on('progress', progressHandler)
186
+ * // Later...
187
+ * response.off('progress', progressHandler)
188
+ * ```
189
+ */
50
190
  public off<K extends keyof ResponseEvents<T>>(type: K, listener: (event: ResponseEvents<T>[K]) => void) {
51
191
  this._eventEmitter.off(type, listener)
52
192
  return this
53
193
  }
54
194
 
195
+ /**
196
+ * Subscribes to an event for a single emission.
197
+ *
198
+ * The listener is automatically removed after being called once.
199
+ *
200
+ * @param type - Event type: 'progress', 'complete', or 'error'
201
+ * @param listener - Callback function to handle the event once
202
+ * @returns This Response instance for chaining
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * response.once('complete', (result) => {
207
+ * console.log('Finished:', result)
208
+ * })
209
+ * ```
210
+ */
55
211
  public once<K extends keyof ResponseEvents<T>>(type: K, listener: (event: ResponseEvents<T>[K]) => void) {
56
212
  this._eventEmitter.once(type, listener)
57
213
  return this
58
214
  }
59
215
 
216
+ /**
217
+ * Binds an external AbortSignal to this operation.
218
+ *
219
+ * When the signal is aborted, the operation will be cancelled automatically.
220
+ * Useful for integrating with UI cancel buttons or request timeouts.
221
+ *
222
+ * @param signal - AbortSignal to bind
223
+ * @returns This Response instance for chaining
224
+ *
225
+ * @example With AbortController
226
+ * ```typescript
227
+ * const controller = new AbortController()
228
+ * const response = zai.extract(data, schema).bindSignal(controller.signal)
229
+ *
230
+ * // Cancel from elsewhere
231
+ * cancelButton.onclick = () => controller.abort()
232
+ * ```
233
+ *
234
+ * @example With timeout
235
+ * ```typescript
236
+ * const controller = new AbortController()
237
+ * setTimeout(() => controller.abort('Timeout'), 10000)
238
+ *
239
+ * const response = zai.answer(docs, question).bindSignal(controller.signal)
240
+ * ```
241
+ */
60
242
  public bindSignal(signal: AbortSignal): this {
61
243
  if (signal.aborted) {
62
244
  this.abort(signal.reason)
@@ -74,10 +256,49 @@ export class Response<T = any, S = T> implements PromiseLike<S> {
74
256
  return this
75
257
  }
76
258
 
259
+ /**
260
+ * Aborts the operation in progress.
261
+ *
262
+ * The operation will be cancelled and throw an abort error.
263
+ * Any partial results will not be returned.
264
+ *
265
+ * @param reason - Optional reason for aborting (string or Error)
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * const response = zai.extract(largeDocument, schema)
270
+ *
271
+ * // Abort after 5 seconds
272
+ * setTimeout(() => response.abort('Operation timeout'), 5000)
273
+ *
274
+ * try {
275
+ * await response
276
+ * } catch (error) {
277
+ * console.log('Aborted:', error)
278
+ * }
279
+ * ```
280
+ */
77
281
  public abort(reason?: string | Error) {
78
282
  this._context.controller.abort(reason)
79
283
  }
80
284
 
285
+ /**
286
+ * Promise interface - allows awaiting the Response.
287
+ *
288
+ * When awaited, returns the simplified value (S).
289
+ * Use `.result()` for full output with usage statistics.
290
+ *
291
+ * @param onfulfilled - Success handler
292
+ * @param onrejected - Error handler
293
+ * @returns Promise resolving to simplified value
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * // Simplified value
298
+ * const isPositive = await zai.check(review, 'Is positive?')
299
+ * console.log(isPositive) // true
300
+ * ```
301
+ */
81
302
  // oxlint-disable-next-line no-thenable
82
303
  public then<TResult1 = S, TResult2 = never>(
83
304
  onfulfilled?: ((value: S) => TResult1 | PromiseLike<TResult1>) | null,
@@ -97,12 +318,53 @@ export class Response<T = any, S = T> implements PromiseLike<S> {
97
318
  ) as PromiseLike<TResult1 | TResult2>
98
319
  }
99
320
 
321
+ /**
322
+ * Promise interface - handles errors.
323
+ *
324
+ * @param onrejected - Error handler
325
+ * @returns Promise resolving to simplified value or error result
326
+ */
100
327
  public catch<TResult = never>(
101
328
  onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
102
329
  ): PromiseLike<S | TResult> {
103
330
  return this._promise.catch(onrejected) as PromiseLike<S | TResult>
104
331
  }
105
332
 
333
+ /**
334
+ * Gets the full result with detailed usage statistics and timing.
335
+ *
336
+ * Unlike awaiting the Response directly (which returns simplified value),
337
+ * this method provides:
338
+ * - `output`: Full operation result (not simplified)
339
+ * - `usage`: Detailed token usage, cost, and request statistics
340
+ * - `elapsed`: Operation duration in milliseconds
341
+ *
342
+ * @returns Promise resolving to full result object
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const { output, usage, elapsed } = await zai.check(text, condition).result()
347
+ *
348
+ * console.log(output.value) // true/false
349
+ * console.log(output.explanation) // "The text expresses..."
350
+ * console.log(usage.tokens.total) // 245
351
+ * console.log(usage.cost.total) // 0.0012
352
+ * console.log(elapsed) // 1523 (ms)
353
+ * ```
354
+ *
355
+ * @example Usage statistics breakdown
356
+ * ```typescript
357
+ * const { usage } = await response.result()
358
+ *
359
+ * console.log('Requests:', usage.requests.requests)
360
+ * console.log('Cached:', usage.requests.cached)
361
+ * console.log('Input tokens:', usage.tokens.input)
362
+ * console.log('Output tokens:', usage.tokens.output)
363
+ * console.log('Input cost:', usage.cost.input)
364
+ * console.log('Output cost:', usage.cost.output)
365
+ * console.log('Total cost:', usage.cost.total)
366
+ * ```
367
+ */
106
368
  public async result(): Promise<{
107
369
  output: T
108
370
  usage: Usage
package/src/zai.ts CHANGED
@@ -8,9 +8,27 @@ import { Adapter } from './adapters/adapter'
8
8
  import { TableAdapter } from './adapters/botpress-table'
9
9
  import { MemoryAdapter } from './adapters/memory'
10
10
 
11
+ /**
12
+ * Active learning configuration for improving AI operations over time.
13
+ *
14
+ * When enabled, Zai stores successful operation results in a table and uses them as examples
15
+ * for future operations, improving accuracy and consistency.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const activeLearning = {
20
+ * enable: true,
21
+ * tableName: 'MyAppLearningTable',
22
+ * taskId: 'sentiment-analysis'
23
+ * }
24
+ * ```
25
+ */
11
26
  type ActiveLearning = {
27
+ /** Whether to enable active learning for this Zai instance */
12
28
  enable: boolean
29
+ /** Name of the Botpress table to store learning examples (must end with 'Table') */
13
30
  tableName: string
31
+ /** Unique identifier for this learning task */
14
32
  taskId: string
15
33
  }
16
34
 
@@ -34,11 +52,39 @@ const _ActiveLearning = z.object({
34
52
  .default('default'),
35
53
  })
36
54
 
55
+ /**
56
+ * Configuration options for creating a Zai instance.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * import { Client } from '@botpress/client'
61
+ * import { Zai } from '@botpress/zai'
62
+ *
63
+ * const client = new Client({ token: 'your-token' })
64
+ * const config: ZaiConfig = {
65
+ * client,
66
+ * modelId: 'best', // Use the best available model
67
+ * userId: 'user-123',
68
+ * namespace: 'my-app',
69
+ * activeLearning: {
70
+ * enable: true,
71
+ * tableName: 'MyLearningTable',
72
+ * taskId: 'extraction'
73
+ * }
74
+ * }
75
+ * const zai = new Zai(config)
76
+ * ```
77
+ */
37
78
  type ZaiConfig = {
79
+ /** Botpress client or Cognitive client instance */
38
80
  client: BotpressClientLike | Cognitive
81
+ /** Optional user ID for tracking and attribution */
39
82
  userId?: string
83
+ /** Model to use: 'best' (default), 'fast', or specific model like 'openai:gpt-4' */
40
84
  modelId?: Models
85
+ /** Active learning configuration to improve operations over time */
41
86
  activeLearning?: ActiveLearning
87
+ /** Namespace for organizing tasks (default: 'zai') */
42
88
  namespace?: string
43
89
  }
44
90
 
@@ -74,6 +120,68 @@ const _ZaiConfig = z.object({
74
120
  .default('zai'),
75
121
  })
76
122
 
123
+ /**
124
+ * Zai - A type-safe LLM utility library for production-ready AI operations.
125
+ *
126
+ * Zai provides high-level abstractions for common AI tasks with built-in features like:
127
+ * - Active learning (learns from successful operations)
128
+ * - Automatic chunking for large inputs
129
+ * - Retry logic with error recovery
130
+ * - Usage tracking (tokens, cost, latency)
131
+ * - Type-safe schema validation with Zod
132
+ *
133
+ * @example Basic usage
134
+ * ```typescript
135
+ * import { Client } from '@botpress/client'
136
+ * import { Zai } from '@botpress/zai'
137
+ * import { z } from '@bpinternal/zui'
138
+ *
139
+ * const client = new Client({ token: process.env.BOTPRESS_TOKEN })
140
+ * const zai = new Zai({ client })
141
+ *
142
+ * // Extract structured data
143
+ * const schema = z.object({
144
+ * name: z.string(),
145
+ * age: z.number()
146
+ * })
147
+ * const person = await zai.extract('John is 30 years old', schema)
148
+ * // Output: { name: 'John', age: 30 }
149
+ *
150
+ * // Check conditions
151
+ * const isPositive = await zai.check('I love this product!', 'Is the sentiment positive?')
152
+ * // Output: true
153
+ *
154
+ * // Summarize text
155
+ * const summary = await zai.summarize(longDocument, { length: 100 })
156
+ * ```
157
+ *
158
+ * @example With active learning
159
+ * ```typescript
160
+ * const zai = new Zai({
161
+ * client,
162
+ * activeLearning: {
163
+ * enable: true,
164
+ * tableName: 'SentimentTable',
165
+ * taskId: 'product-reviews'
166
+ * }
167
+ * })
168
+ *
169
+ * // Enable learning for specific task
170
+ * const result = await zai.learn('sentiment').check(review, 'Is this positive?')
171
+ * // Future calls will use approved examples for better accuracy
172
+ * ```
173
+ *
174
+ * @example Chaining configuration
175
+ * ```typescript
176
+ * // Use fast model for quick operations
177
+ * const fastZai = zai.with({ modelId: 'fast' })
178
+ * await fastZai.check(text, 'Is this spam?')
179
+ *
180
+ * // Use specific model
181
+ * const gpt4Zai = zai.with({ modelId: 'openai:gpt-4' })
182
+ * await gpt4Zai.extract(document, complexSchema)
183
+ * ```
184
+ */
77
185
  export class Zai {
78
186
  protected static tokenizer: TextTokenizer = null!
79
187
  protected client: Cognitive
@@ -88,6 +196,27 @@ export class Zai {
88
196
  protected adapter: Adapter
89
197
  protected activeLearning: ActiveLearning
90
198
 
199
+ /**
200
+ * Creates a new Zai instance with the specified configuration.
201
+ *
202
+ * @param config - Configuration object containing client, model, and learning settings
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * import { Client } from '@botpress/client'
207
+ * import { Zai } from '@botpress/zai'
208
+ *
209
+ * const client = new Client({ token: 'your-token' })
210
+ * const zai = new Zai({
211
+ * client,
212
+ * modelId: 'best',
213
+ * namespace: 'my-app',
214
+ * userId: 'user-123'
215
+ * })
216
+ * ```
217
+ *
218
+ * @throws {Error} If the configuration is invalid (e.g., invalid modelId format)
219
+ */
91
220
  public constructor(config: ZaiConfig) {
92
221
  this._originalConfig = config
93
222
  const parsed = _ZaiConfig.parse(config) as ZaiConfig
@@ -146,6 +275,41 @@ export class Zai {
146
275
  return `${this.namespace}/${this.activeLearning.taskId}`.replace(/\/+/g, '/')
147
276
  }
148
277
 
278
+ /**
279
+ * Creates a new Zai instance with merged configuration options.
280
+ *
281
+ * This method allows you to create variations of your Zai instance with different
282
+ * settings without modifying the original. Useful for switching models, namespaces,
283
+ * or other configuration on a per-operation basis.
284
+ *
285
+ * @param options - Partial configuration to override the current settings
286
+ * @returns A new Zai instance with the merged configuration
287
+ *
288
+ * @example Switch to a faster model
289
+ * ```typescript
290
+ * const zai = new Zai({ client })
291
+ *
292
+ * // Use fast model for simple operations
293
+ * const fastZai = zai.with({ modelId: 'fast' })
294
+ * await fastZai.check(text, 'Is this spam?')
295
+ *
296
+ * // Use best model for complex operations
297
+ * const bestZai = zai.with({ modelId: 'best' })
298
+ * await bestZai.extract(document, complexSchema)
299
+ * ```
300
+ *
301
+ * @example Change namespace
302
+ * ```typescript
303
+ * const customerZai = zai.with({ namespace: 'customer-support' })
304
+ * const salesZai = zai.with({ namespace: 'sales' })
305
+ * ```
306
+ *
307
+ * @example Use specific model
308
+ * ```typescript
309
+ * const gpt4 = zai.with({ modelId: 'openai:gpt-4' })
310
+ * const claude = zai.with({ modelId: 'anthropic:claude-3-5-sonnet-20241022' })
311
+ * ```
312
+ */
149
313
  public with(options: Partial<ZaiConfig>): Zai {
150
314
  return new Zai({
151
315
  ...this._originalConfig,
@@ -153,6 +317,56 @@ export class Zai {
153
317
  })
154
318
  }
155
319
 
320
+ /**
321
+ * Creates a new Zai instance with active learning enabled for a specific task.
322
+ *
323
+ * Active learning stores successful operation results and uses them as examples for
324
+ * future operations, improving accuracy and consistency over time. Each task ID
325
+ * maintains its own set of learned examples.
326
+ *
327
+ * @param taskId - Unique identifier for the learning task (alphanumeric, hyphens, underscores, slashes)
328
+ * @returns A new Zai instance with active learning enabled for the specified task
329
+ *
330
+ * @example Sentiment analysis with learning
331
+ * ```typescript
332
+ * const zai = new Zai({
333
+ * client,
334
+ * activeLearning: {
335
+ * enable: false,
336
+ * tableName: 'AppLearningTable',
337
+ * taskId: 'default'
338
+ * }
339
+ * })
340
+ *
341
+ * // Enable learning for sentiment analysis
342
+ * const sentimentZai = zai.learn('sentiment-analysis')
343
+ * const result = await sentimentZai.check(review, 'Is this review positive?')
344
+ *
345
+ * // Each successful call is stored and used to improve future calls
346
+ * ```
347
+ *
348
+ * @example Different tasks for different purposes
349
+ * ```typescript
350
+ * // Extract user info with learning
351
+ * const userExtractor = zai.learn('user-extraction')
352
+ * await userExtractor.extract(text, userSchema)
353
+ *
354
+ * // Extract product info with separate learning
355
+ * const productExtractor = zai.learn('product-extraction')
356
+ * await productExtractor.extract(text, productSchema)
357
+ *
358
+ * // Each task learns independently
359
+ * ```
360
+ *
361
+ * @example Combining with other configuration
362
+ * ```typescript
363
+ * // Use fast model + learning
364
+ * const fastLearner = zai.with({ modelId: 'fast' }).learn('quick-checks')
365
+ * await fastLearner.check(email, 'Is this spam?')
366
+ * ```
367
+ *
368
+ * @see {@link ZaiConfig.activeLearning} for configuration options
369
+ */
156
370
  public learn(taskId: string) {
157
371
  return new Zai({
158
372
  ...this._originalConfig,