@botpress/zai 2.4.1 → 2.4.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.
@@ -241,17 +241,19 @@ Question to answer: "${question}"`;
241
241
  }
242
242
  ],
243
243
  transform: (text) => {
244
+ text = text.slice(0, text.lastIndexOf(END.slice(0, -1)));
244
245
  return parseResponse(text || "", mappings);
245
246
  }
246
247
  });
247
248
  return extracted;
248
249
  };
249
- const parseResponse = (response, mappings) => {
250
+ export const parseResponse = (response, mappings) => {
250
251
  const text = response.trim();
251
- if (text.includes(ANSWER_START)) {
252
- return parseAnswerResponse(text, mappings);
253
- } else if (text.includes(AMBIGUOUS_START)) {
252
+ const answersCount = (text.match(new RegExp(ANSWER_START, "g")) || []).length;
253
+ if (text.includes(AMBIGUOUS_START) || answersCount >= 2) {
254
254
  return parseAmbiguousResponse(text, mappings);
255
+ } else if (text.includes(ANSWER_START)) {
256
+ return parseAnswerResponse(text, mappings);
255
257
  } else if (text.includes(OUT_OF_TOPIC_START)) {
256
258
  return parseOutOfTopicResponse(text);
257
259
  } else if (text.includes(INVALID_QUESTION_START)) {
package/dist/response.js CHANGED
@@ -29,19 +29,104 @@ export class Response {
29
29
  this._eventEmitter.emit("progress", usage);
30
30
  });
31
31
  }
32
- // Event emitter methods
32
+ /**
33
+ * Subscribes to events emitted during operation execution.
34
+ *
35
+ * @param type - Event type: 'progress', 'complete', or 'error'
36
+ * @param listener - Callback function to handle the event
37
+ * @returns This Response instance for chaining
38
+ *
39
+ * @example Track progress
40
+ * ```typescript
41
+ * response.on('progress', (usage) => {
42
+ * console.log(`${usage.requests.percentage * 100}% complete`)
43
+ * console.log(`Cost: $${usage.cost.total}`)
44
+ * })
45
+ * ```
46
+ *
47
+ * @example Handle completion
48
+ * ```typescript
49
+ * response.on('complete', (result) => {
50
+ * console.log('Operation completed:', result)
51
+ * })
52
+ * ```
53
+ *
54
+ * @example Handle errors
55
+ * ```typescript
56
+ * response.on('error', (error) => {
57
+ * console.error('Operation failed:', error)
58
+ * })
59
+ * ```
60
+ */
33
61
  on(type, listener) {
34
62
  this._eventEmitter.on(type, listener);
35
63
  return this;
36
64
  }
65
+ /**
66
+ * Unsubscribes from events.
67
+ *
68
+ * @param type - Event type to unsubscribe from
69
+ * @param listener - The exact listener function to remove
70
+ * @returns This Response instance for chaining
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const progressHandler = (usage) => console.log(usage.tokens.total)
75
+ * response.on('progress', progressHandler)
76
+ * // Later...
77
+ * response.off('progress', progressHandler)
78
+ * ```
79
+ */
37
80
  off(type, listener) {
38
81
  this._eventEmitter.off(type, listener);
39
82
  return this;
40
83
  }
84
+ /**
85
+ * Subscribes to an event for a single emission.
86
+ *
87
+ * The listener is automatically removed after being called once.
88
+ *
89
+ * @param type - Event type: 'progress', 'complete', or 'error'
90
+ * @param listener - Callback function to handle the event once
91
+ * @returns This Response instance for chaining
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * response.once('complete', (result) => {
96
+ * console.log('Finished:', result)
97
+ * })
98
+ * ```
99
+ */
41
100
  once(type, listener) {
42
101
  this._eventEmitter.once(type, listener);
43
102
  return this;
44
103
  }
104
+ /**
105
+ * Binds an external AbortSignal to this operation.
106
+ *
107
+ * When the signal is aborted, the operation will be cancelled automatically.
108
+ * Useful for integrating with UI cancel buttons or request timeouts.
109
+ *
110
+ * @param signal - AbortSignal to bind
111
+ * @returns This Response instance for chaining
112
+ *
113
+ * @example With AbortController
114
+ * ```typescript
115
+ * const controller = new AbortController()
116
+ * const response = zai.extract(data, schema).bindSignal(controller.signal)
117
+ *
118
+ * // Cancel from elsewhere
119
+ * cancelButton.onclick = () => controller.abort()
120
+ * ```
121
+ *
122
+ * @example With timeout
123
+ * ```typescript
124
+ * const controller = new AbortController()
125
+ * setTimeout(() => controller.abort('Timeout'), 10000)
126
+ *
127
+ * const response = zai.answer(docs, question).bindSignal(controller.signal)
128
+ * ```
129
+ */
45
130
  bindSignal(signal) {
46
131
  if (signal.aborted) {
47
132
  this.abort(signal.reason);
@@ -54,9 +139,48 @@ export class Response {
54
139
  void this.once("error", () => signal.removeEventListener("abort", signalAbort));
55
140
  return this;
56
141
  }
142
+ /**
143
+ * Aborts the operation in progress.
144
+ *
145
+ * The operation will be cancelled and throw an abort error.
146
+ * Any partial results will not be returned.
147
+ *
148
+ * @param reason - Optional reason for aborting (string or Error)
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const response = zai.extract(largeDocument, schema)
153
+ *
154
+ * // Abort after 5 seconds
155
+ * setTimeout(() => response.abort('Operation timeout'), 5000)
156
+ *
157
+ * try {
158
+ * await response
159
+ * } catch (error) {
160
+ * console.log('Aborted:', error)
161
+ * }
162
+ * ```
163
+ */
57
164
  abort(reason) {
58
165
  this._context.controller.abort(reason);
59
166
  }
167
+ /**
168
+ * Promise interface - allows awaiting the Response.
169
+ *
170
+ * When awaited, returns the simplified value (S).
171
+ * Use `.result()` for full output with usage statistics.
172
+ *
173
+ * @param onfulfilled - Success handler
174
+ * @param onrejected - Error handler
175
+ * @returns Promise resolving to simplified value
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * // Simplified value
180
+ * const isPositive = await zai.check(review, 'Is positive?')
181
+ * console.log(isPositive) // true
182
+ * ```
183
+ */
60
184
  // oxlint-disable-next-line no-thenable
61
185
  then(onfulfilled, onrejected) {
62
186
  return this._promise.then(
@@ -72,9 +196,50 @@ export class Response {
72
196
  }
73
197
  );
74
198
  }
199
+ /**
200
+ * Promise interface - handles errors.
201
+ *
202
+ * @param onrejected - Error handler
203
+ * @returns Promise resolving to simplified value or error result
204
+ */
75
205
  catch(onrejected) {
76
206
  return this._promise.catch(onrejected);
77
207
  }
208
+ /**
209
+ * Gets the full result with detailed usage statistics and timing.
210
+ *
211
+ * Unlike awaiting the Response directly (which returns simplified value),
212
+ * this method provides:
213
+ * - `output`: Full operation result (not simplified)
214
+ * - `usage`: Detailed token usage, cost, and request statistics
215
+ * - `elapsed`: Operation duration in milliseconds
216
+ *
217
+ * @returns Promise resolving to full result object
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * const { output, usage, elapsed } = await zai.check(text, condition).result()
222
+ *
223
+ * console.log(output.value) // true/false
224
+ * console.log(output.explanation) // "The text expresses..."
225
+ * console.log(usage.tokens.total) // 245
226
+ * console.log(usage.cost.total) // 0.0012
227
+ * console.log(elapsed) // 1523 (ms)
228
+ * ```
229
+ *
230
+ * @example Usage statistics breakdown
231
+ * ```typescript
232
+ * const { usage } = await response.result()
233
+ *
234
+ * console.log('Requests:', usage.requests.requests)
235
+ * console.log('Cached:', usage.requests.cached)
236
+ * console.log('Input tokens:', usage.tokens.input)
237
+ * console.log('Output tokens:', usage.tokens.output)
238
+ * console.log('Input cost:', usage.cost.input)
239
+ * console.log('Output cost:', usage.cost.output)
240
+ * console.log('Total cost:', usage.cost.total)
241
+ * ```
242
+ */
78
243
  async result() {
79
244
  const output = await this._promise;
80
245
  const usage = this._context.usage;
package/dist/zai.js CHANGED
@@ -47,6 +47,27 @@ export class Zai {
47
47
  namespace;
48
48
  adapter;
49
49
  activeLearning;
50
+ /**
51
+ * Creates a new Zai instance with the specified configuration.
52
+ *
53
+ * @param config - Configuration object containing client, model, and learning settings
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * import { Client } from '@botpress/client'
58
+ * import { Zai } from '@botpress/zai'
59
+ *
60
+ * const client = new Client({ token: 'your-token' })
61
+ * const zai = new Zai({
62
+ * client,
63
+ * modelId: 'best',
64
+ * namespace: 'my-app',
65
+ * userId: 'user-123'
66
+ * })
67
+ * ```
68
+ *
69
+ * @throws {Error} If the configuration is invalid (e.g., invalid modelId format)
70
+ */
50
71
  constructor(config) {
51
72
  this._originalConfig = config;
52
73
  const parsed = _ZaiConfig.parse(config);
@@ -89,12 +110,97 @@ export class Zai {
89
110
  }
90
111
  return `${this.namespace}/${this.activeLearning.taskId}`.replace(/\/+/g, "/");
91
112
  }
113
+ /**
114
+ * Creates a new Zai instance with merged configuration options.
115
+ *
116
+ * This method allows you to create variations of your Zai instance with different
117
+ * settings without modifying the original. Useful for switching models, namespaces,
118
+ * or other configuration on a per-operation basis.
119
+ *
120
+ * @param options - Partial configuration to override the current settings
121
+ * @returns A new Zai instance with the merged configuration
122
+ *
123
+ * @example Switch to a faster model
124
+ * ```typescript
125
+ * const zai = new Zai({ client })
126
+ *
127
+ * // Use fast model for simple operations
128
+ * const fastZai = zai.with({ modelId: 'fast' })
129
+ * await fastZai.check(text, 'Is this spam?')
130
+ *
131
+ * // Use best model for complex operations
132
+ * const bestZai = zai.with({ modelId: 'best' })
133
+ * await bestZai.extract(document, complexSchema)
134
+ * ```
135
+ *
136
+ * @example Change namespace
137
+ * ```typescript
138
+ * const customerZai = zai.with({ namespace: 'customer-support' })
139
+ * const salesZai = zai.with({ namespace: 'sales' })
140
+ * ```
141
+ *
142
+ * @example Use specific model
143
+ * ```typescript
144
+ * const gpt4 = zai.with({ modelId: 'openai:gpt-4' })
145
+ * const claude = zai.with({ modelId: 'anthropic:claude-3-5-sonnet-20241022' })
146
+ * ```
147
+ */
92
148
  with(options) {
93
149
  return new Zai({
94
150
  ...this._originalConfig,
95
151
  ...options
96
152
  });
97
153
  }
154
+ /**
155
+ * Creates a new Zai instance with active learning enabled for a specific task.
156
+ *
157
+ * Active learning stores successful operation results and uses them as examples for
158
+ * future operations, improving accuracy and consistency over time. Each task ID
159
+ * maintains its own set of learned examples.
160
+ *
161
+ * @param taskId - Unique identifier for the learning task (alphanumeric, hyphens, underscores, slashes)
162
+ * @returns A new Zai instance with active learning enabled for the specified task
163
+ *
164
+ * @example Sentiment analysis with learning
165
+ * ```typescript
166
+ * const zai = new Zai({
167
+ * client,
168
+ * activeLearning: {
169
+ * enable: false,
170
+ * tableName: 'AppLearningTable',
171
+ * taskId: 'default'
172
+ * }
173
+ * })
174
+ *
175
+ * // Enable learning for sentiment analysis
176
+ * const sentimentZai = zai.learn('sentiment-analysis')
177
+ * const result = await sentimentZai.check(review, 'Is this review positive?')
178
+ *
179
+ * // Each successful call is stored and used to improve future calls
180
+ * ```
181
+ *
182
+ * @example Different tasks for different purposes
183
+ * ```typescript
184
+ * // Extract user info with learning
185
+ * const userExtractor = zai.learn('user-extraction')
186
+ * await userExtractor.extract(text, userSchema)
187
+ *
188
+ * // Extract product info with separate learning
189
+ * const productExtractor = zai.learn('product-extraction')
190
+ * await productExtractor.extract(text, productSchema)
191
+ *
192
+ * // Each task learns independently
193
+ * ```
194
+ *
195
+ * @example Combining with other configuration
196
+ * ```typescript
197
+ * // Use fast model + learning
198
+ * const fastLearner = zai.with({ modelId: 'fast' }).learn('quick-checks')
199
+ * await fastLearner.check(email, 'Is this spam?')
200
+ * ```
201
+ *
202
+ * @see {@link ZaiConfig.activeLearning} for configuration options
203
+ */
98
204
  learn(taskId) {
99
205
  return new Zai({
100
206
  ...this._originalConfig,
@@ -1799,3 +1799,5 @@
1799
1799
  {"key":"a7dec8bb","input":"{\"body\":{\"messages\":[{\"content\":\"You are an expert research assistant specialized in answering questions using only the information provided in documents.\\n\\n# Task\\nAnswer the user's question based ONLY on the information in the provided documents. You MUST cite your sources using line numbers.\\n\\n# Document Format\\nDocuments are provided with line numbers:\\n■001 | First line of text\\n■002 | Second line of text\\n■003 | Third line of text\\n\\n# Citation Format\\nYou MUST include citations immediately after statements. Use these formats:\\n- Single line: ■035\\n- Range: ■005-010\\n- Multiple: ■035■046■094\\n\\n# Response Format\\n\\nChoose ONE of these response types:\\n\\n**TYPE 1 - ANSWER** (Use this when you can answer the question)\\n■answer\\n[Your answer with inline citations■001-003. Make sure each part is cited correctly■013. More text. ■015]\\n■end■\\n\\n**TYPE 2 - AMBIGUOUS** (Use when the question has multiple valid interpretations)\\n■ambiguous\\n[Explain the ambiguity]\\n■follow_up\\n[Ask a clarifying question]\\n■answer\\n[First interpretation with citations ■001 and part 2 as well.■002]\\n■answer\\n[Second interpretation with citations ■005 and part 2 of the answer.■006]\\n■end■\\n\\n**TYPE 3 - OUT OF TOPIC** (Use when question is completely unrelated to documents)\\n■out_of_topic\\n[Explain why it's unrelated]\\n■end■\\n\\n**TYPE 4 - INVALID QUESTION** (Use when input is not a proper question, e.g., gibberish, malformed or nonsensical)\\n■invalid_question\\n[Explain why it's invalid, e.g., \\\"The question is incomplete\\\" or \\\"The question contains nonsensical terms\\\", or \\\"Received gibberish\\\"]\\n■end■\\n\\n**TYPE 5 - MISSING KNOWLEDGE** (Use ONLY when documents lack specific details needed)\\n■missing_knowledge\\n[Explain what specific information is missing]\\n■end■\\n\\n# Important Rules\\n- PREFER answering when possible - only use missing_knowledge if truly no relevant info exists\\n- ALWAYS cite sources with line numbers\\n- Use ONLY information from the documents\\n- Be precise and factual\\n- Do NOT fabricate information\\n- Do NOT mention \\\"According to the documents\\\" or similar phrases – just provide a high-quality answer with citations\\n- Do not be too strict on the question format; assume high-level answers are acceptable unless the question clearly asks for very specific details or requests depth beyond the documents\\n\\n# Additional Instructions\\nHere are some additional instructions to follow about how to answer the question:\\nProvide a clear and concise answer based on the documents.\",\"role\":\"system\"},{\"content\":\"<documents>\\n■001 | Some content here\\n</documents>\\n\\nPlease answer the below question using the format specified above.\\nQuestion to answer: \\\"Question?1762472164277\\\"\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.answer\",\"promptSource\":\"zai:zai.answer:default\"},\"model\":\"fast\",\"reasoningEffort\":\"none\",\"signal\":{},\"stopSequences\":[\"■end■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■missing_knowledge\nThe provided document contains only the line “Some content here” and does not include any information related to the question “Question?1762472164277.” Therefore, the necessary details to answer the question are missing.\n■end","metadata":{"provider":"cerebras","usage":{"inputTokens":659,"outputTokens":82,"inputCost":0.00023065,"outputCost":0.0000615},"model":"cerebras:gpt-oss-120b","ttft":191,"latency":893,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.00029215,"warnings":[{"type":"parameter_ignored","message":"Reasoning effort \"none\" is not supported by the \"gpt-oss-120b\" model, using \"low\" effort instead"}]}}}
1800
1800
  {"key":"ed1db1d1","input":"{\"body\":{\"messages\":[{\"content\":\"You are an expert research assistant specialized in answering questions using only the information provided in documents.\\n\\n# Task\\nAnswer the user's question based ONLY on the information in the provided documents. You MUST cite your sources using line numbers.\\n\\n# Document Format\\nDocuments are provided with line numbers:\\n■001 | First line of text\\n■002 | Second line of text\\n■003 | Third line of text\\n\\n# Citation Format\\nYou MUST include citations immediately after statements. Use these formats:\\n- Single line: ■035\\n- Range: ■005-010\\n- Multiple: ■035■046■094\\n\\n# Response Format\\n\\nChoose ONE of these response types:\\n\\n**TYPE 1 - ANSWER** (Use this when you can answer the question)\\n■answer\\n[Your answer with inline citations■001-003. Make sure each part is cited correctly■013. More text. ■015]\\n■end■\\n\\n**TYPE 2 - AMBIGUOUS** (Use when the question has multiple valid interpretations)\\n■ambiguous\\n[Explain the ambiguity]\\n■follow_up\\n[Ask a clarifying question]\\n■answer\\n[First interpretation with citations ■001 and part 2 as well.■002]\\n■answer\\n[Second interpretation with citations ■005 and part 2 of the answer.■006]\\n■end■\\n\\n**TYPE 3 - OUT OF TOPIC** (Use when question is completely unrelated to documents)\\n■out_of_topic\\n[Explain why it's unrelated]\\n■end■\\n\\n**TYPE 4 - INVALID QUESTION** (Use when input is not a proper question, e.g., gibberish, malformed or nonsensical)\\n■invalid_question\\n[Explain why it's invalid, e.g., \\\"The question is incomplete\\\" or \\\"The question contains nonsensical terms\\\", or \\\"Received gibberish\\\"]\\n■end■\\n\\n**TYPE 5 - MISSING KNOWLEDGE** (Use ONLY when documents lack specific details needed)\\n■missing_knowledge\\n[Explain what specific information is missing]\\n■end■\\n\\n# Important Rules\\n- PREFER answering when possible - only use missing_knowledge if truly no relevant info exists\\n- ALWAYS cite sources with line numbers\\n- Use ONLY information from the documents\\n- Be precise and factual\\n- Do NOT fabricate information\\n- Do NOT mention \\\"According to the documents\\\" or similar phrases – just provide a high-quality answer with citations\\n- Do not be too strict on the question format; assume high-level answers are acceptable unless the question clearly asks for very specific details or requests depth beyond the documents\\n\\n# Additional Instructions\\nHere are some additional instructions to follow about how to answer the question:\\nProvide a clear and concise answer based on the documents.\",\"role\":\"system\"},{\"content\":\"<documents>\\n■001 | Some content here\\n</documents>\\n\\nPlease answer the below question using the format specified above.\\nQuestion to answer: \\\"Question?\\\"\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.answer\",\"promptSource\":\"zai:zai.answer:default\"},\"model\":\"fast\",\"reasoningEffort\":\"none\",\"signal\":{},\"stopSequences\":[\"■end■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■missing_knowledge\nThe provided document contains only a single line of vague text (\"Some content here\") and does not include any information relevant to answering the question posed. Therefore, the necessary details to respond are missing.\n■end","metadata":{"provider":"cerebras","usage":{"inputTokens":653,"outputTokens":77,"inputCost":0.00022855,"outputCost":0.00005775},"model":"cerebras:gpt-oss-120b","ttft":165,"latency":257,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.0002863,"warnings":[{"type":"parameter_ignored","message":"Reasoning effort \"none\" is not supported by the \"gpt-oss-120b\" model, using \"low\" effort instead"}]}}}
1801
1801
  {"key":"78e0f785","input":"{\"body\":{\"messages\":[{\"content\":\"You are an expert research assistant specialized in answering questions using only the information provided in documents.\\n\\n# Task\\nAnswer the user's question based ONLY on the information in the provided documents. You MUST cite your sources using line numbers.\\n\\n# Document Format\\nDocuments are provided with line numbers:\\n■001 | First line of text\\n■002 | Second line of text\\n■003 | Third line of text\\n\\n# Citation Format\\nYou MUST include citations immediately after statements. Use these formats:\\n- Single line: ■035\\n- Range: ■005-010\\n- Multiple: ■035■046■094\\n\\n# Response Format\\n\\nChoose ONE of these response types:\\n\\n**TYPE 1 - ANSWER** (Use this when you can answer the question)\\n■answer\\n[Your answer with inline citations■001-003. Make sure each part is cited correctly■013. More text. ■015]\\n■end■\\n\\n**TYPE 2 - AMBIGUOUS** (Use when the question has multiple valid interpretations)\\n■ambiguous\\n[Explain the ambiguity]\\n■follow_up\\n[Ask a clarifying question]\\n■answer\\n[First interpretation with citations ■001 and part 2 as well.■002]\\n■answer\\n[Second interpretation with citations ■005 and part 2 of the answer.■006]\\n■end■\\n\\n**TYPE 3 - OUT OF TOPIC** (Use when question is completely unrelated to documents)\\n■out_of_topic\\n[Explain why it's unrelated]\\n■end■\\n\\n**TYPE 4 - INVALID QUESTION** (Use when input is not a proper question, e.g., gibberish, malformed or nonsensical)\\n■invalid_question\\n[Explain why it's invalid, e.g., \\\"The question is incomplete\\\" or \\\"The question contains nonsensical terms\\\", or \\\"Received gibberish\\\"]\\n■end■\\n\\n**TYPE 5 - MISSING KNOWLEDGE** (Use ONLY when documents lack specific details needed)\\n■missing_knowledge\\n[Explain what specific information is missing]\\n■end■\\n\\n# Important Rules\\n- PREFER answering when possible - only use missing_knowledge if truly no relevant info exists\\n- ALWAYS cite sources with line numbers\\n- Use ONLY information from the documents\\n- Be precise and factual\\n- Do NOT fabricate information\\n- Do NOT mention \\\"According to the documents\\\" or similar phrases – just provide a high-quality answer with citations\\n- Do not be too strict on the question format; assume high-level answers are acceptable unless the question clearly asks for very specific details or requests depth beyond the documents\\n\\n# Additional Instructions\\nHere are some additional instructions to follow about how to answer the question:\\nProvide a clear and concise answer based on the documents.\",\"role\":\"system\"},{\"content\":\"<documents>\\n■001 | Document 1\\n■002 | Document 2\\n■003 | Document 3\\n</documents>\\n\\nPlease answer the below question using the format specified above.\\nQuestion to answer: \\\"What is this about?\\\"\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.answer\",\"promptSource\":\"zai:zai.answer:default\"},\"model\":\"fast\",\"reasoningEffort\":\"none\",\"signal\":{},\"stopSequences\":[\"■end■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■missing_knowledge\nThe provided documents contain only titles (\"Document 1\", \"Document 2\", \"Document 3\") and do not include any substantive information about a topic, making it impossible to determine what \"this\" refers to. Additional content is needed to answer the question.\n■end","metadata":{"provider":"cerebras","usage":{"inputTokens":670,"outputTokens":102,"inputCost":0.0002345,"outputCost":0.0000765},"model":"cerebras:gpt-oss-120b","ttft":189,"latency":279,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.000311,"warnings":[{"type":"parameter_ignored","message":"Reasoning effort \"none\" is not supported by the \"gpt-oss-120b\" model, using \"low\" effort instead"}]}}}
1802
+ {"key":"67eb9851","input":"{\"body\":{\"messages\":[{\"content\":\"You are an expert research assistant specialized in answering questions using only the information provided in documents.\\n\\n# Task\\nAnswer the user's question based ONLY on the information in the provided documents. You MUST cite your sources using line numbers.\\n\\n# Document Format\\nDocuments are provided with line numbers:\\n■001 | First line of text\\n■002 | Second line of text\\n■003 | Third line of text\\n\\n# Citation Format\\nYou MUST include citations immediately after statements. Use these formats:\\n- Single line: ■035\\n- Range: ■005-010\\n- Multiple: ■035■046■094\\n\\n# Response Format\\n\\nChoose ONE of these response types:\\n\\n**TYPE 1 - ANSWER** (Use this when you can answer the question)\\n■answer\\n[Your answer with inline citations■001-003. Make sure each part is cited correctly■013. More text. ■015]\\n■end■\\n\\n**TYPE 2 - AMBIGUOUS** (Use when the question has multiple valid interpretations)\\n■ambiguous\\n[Explain the ambiguity]\\n■follow_up\\n[Ask a clarifying question]\\n■answer\\n[First interpretation with citations ■001 and part 2 as well.■002]\\n■answer\\n[Second interpretation with citations ■005 and part 2 of the answer.■006]\\n■end■\\n\\n**TYPE 3 - OUT OF TOPIC** (Use when question is completely unrelated to documents)\\n■out_of_topic\\n[Explain why it's unrelated]\\n■end■\\n\\n**TYPE 4 - INVALID QUESTION** (Use when input is not a proper question, e.g., gibberish, malformed or nonsensical)\\n■invalid_question\\n[Explain why it's invalid, e.g., \\\"The question is incomplete\\\" or \\\"The question contains nonsensical terms\\\", or \\\"Received gibberish\\\"]\\n■end■\\n\\n**TYPE 5 - MISSING KNOWLEDGE** (Use ONLY when documents lack specific details needed)\\n■missing_knowledge\\n[Explain what specific information is missing]\\n■end■\\n\\n# Important Rules\\n- PREFER answering when possible - only use missing_knowledge if truly no relevant info exists\\n- ALWAYS cite sources with line numbers\\n- Use ONLY information from the documents\\n- Be precise and factual\\n- Do NOT fabricate information\\n- Do NOT mention \\\"According to the documents\\\" or similar phrases – just provide a high-quality answer with citations\\n- Do not be too strict on the question format; assume high-level answers are acceptable unless the question clearly asks for very specific details or requests depth beyond the documents\\n\\n# Additional Instructions\\nHere are some additional instructions to follow about how to answer the question:\\nProvide a clear and concise answer based on the documents.\",\"role\":\"system\"},{\"content\":\"<documents>\\n■001 | Botpress was founded in 2016.\\n■002 | It is an AI agent platform.\\n■003 | The company is headquartered in Quebec, Canada.\\n</documents>\\n\\nPlease answer the below question using the format specified above.\\nQuestion to answer: \\\"Tell me about Botpress.\\\"\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.answer\",\"promptSource\":\"zai:zai.answer:default\"},\"model\":\"fast\",\"reasoningEffort\":\"none\",\"signal\":{},\"stopSequences\":[\"■end■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■answer\nBotpress was founded in 2016. It is an AI agent platform and the company is headquartered in Quebec, Canada.■001■002■003\n■end","metadata":{"provider":"cerebras","usage":{"inputTokens":684,"outputTokens":61,"inputCost":0.0002394,"outputCost":0.00004575},"model":"cerebras:gpt-oss-120b","ttft":281,"latency":649,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.00028515,"warnings":[{"type":"parameter_ignored","message":"Reasoning effort \"none\" is not supported by the \"gpt-oss-120b\" model, using \"low\" effort instead"}]}}}
1803
+ {"key":"cbc6f03c","input":"{\"body\":{\"messages\":[{\"content\":\"You are an expert research assistant specialized in answering questions using only the information provided in documents.\\n\\n# Task\\nAnswer the user's question based ONLY on the information in the provided documents. You MUST cite your sources using line numbers.\\n\\n# Document Format\\nDocuments are provided with line numbers:\\n■001 | First line of text\\n■002 | Second line of text\\n■003 | Third line of text\\n\\n# Citation Format\\nYou MUST include citations immediately after statements. Use these formats:\\n- Single line: ■035\\n- Range: ■005-010\\n- Multiple: ■035■046■094\\n\\n# Response Format\\n\\nChoose ONE of these response types:\\n\\n**TYPE 1 - ANSWER** (Use this when you can answer the question)\\n■answer\\n[Your answer with inline citations■001-003. Make sure each part is cited correctly■013. More text. ■015]\\n■end■\\n\\n**TYPE 2 - AMBIGUOUS** (Use when the question has multiple valid interpretations)\\n■ambiguous\\n[Explain the ambiguity]\\n■follow_up\\n[Ask a clarifying question]\\n■answer\\n[First interpretation with citations ■001 and part 2 as well.■002]\\n■answer\\n[Second interpretation with citations ■005 and part 2 of the answer.■006]\\n■end■\\n\\n**TYPE 3 - OUT OF TOPIC** (Use when question is completely unrelated to documents)\\n■out_of_topic\\n[Explain why it's unrelated]\\n■end■\\n\\n**TYPE 4 - INVALID QUESTION** (Use when input is not a proper question, e.g., gibberish, malformed or nonsensical)\\n■invalid_question\\n[Explain why it's invalid, e.g., \\\"The question is incomplete\\\" or \\\"The question contains nonsensical terms\\\", or \\\"Received gibberish\\\"]\\n■end■\\n\\n**TYPE 5 - MISSING KNOWLEDGE** (Use ONLY when documents lack specific details needed)\\n■missing_knowledge\\n[Explain what specific information is missing]\\n■end■\\n\\n# Important Rules\\n- PREFER answering when possible - only use missing_knowledge if truly no relevant info exists\\n- ALWAYS cite sources with line numbers\\n- Use ONLY information from the documents\\n- Be precise and factual\\n- Do NOT fabricate information\\n- Do NOT mention \\\"According to the documents\\\" or similar phrases – just provide a high-quality answer with citations\\n- Do not be too strict on the question format; assume high-level answers are acceptable unless the question clearly asks for very specific details or requests depth beyond the documents\\n\\n# Additional Instructions\\nHere are some additional instructions to follow about how to answer the question:\\nProvide a clear and concise answer based on the documents.\",\"role\":\"system\"},{\"content\":\"<documents>\\n■001 | The iPhone was first released by Apple in 2007.\\n■002 | Steve Jobs announced the iPhone at the Macworld conference.\\n■003 | The original iPhone had a 3.5-inch display and 2-megapixel camera.\\n■004 | The iPhone revolutionized the smartphone industry.\\n</documents>\\n\\nPlease answer the below question using the format specified above.\\nQuestion to answer: \\\"When was the iPhone released and who announced it?\\\"\",\"role\":\"user\",\"type\":\"text\"}],\"meta\":{\"integrationName\":\"zai\",\"promptCategory\":\"zai:zai.answer\",\"promptSource\":\"zai:zai.answer:default\"},\"model\":\"fast\",\"reasoningEffort\":\"none\",\"signal\":{},\"stopSequences\":[\"■end■\"]},\"method\":\"POST\",\"url\":\"https://api.botpress.cloud/v2/cognitive/generate-text\"}","value":{"output":"■answer\nThe iPhone was first released by Apple in 2007■001, and it was announced by Steve Jobs at the Macworld conference■002.\n■end","metadata":{"provider":"cerebras","usage":{"inputTokens":721,"outputTokens":52,"inputCost":0.00025235,"outputCost":0.000039},"model":"cerebras:gpt-oss-120b","ttft":178,"latency":262,"cached":false,"fallbackPath":[],"stopReason":"stop","cost":0.00029135,"warnings":[{"type":"parameter_ignored","message":"Reasoning effort \"none\" is not supported by the \"gpt-oss-120b\" model, using \"low\" effort instead"}]}}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@botpress/zai",
3
3
  "description": "Zui AI (zai) – An LLM utility library written on top of Zui and the Botpress API",
4
- "version": "2.4.1",
4
+ "version": "2.4.2",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
package/src/context.ts CHANGED
@@ -18,22 +18,54 @@ export type ZaiContextProps = {
18
18
  source?: GenerateContentInput['meta']
19
19
  }
20
20
 
21
+ /**
22
+ * Usage statistics tracking tokens, cost, and request metrics for an operation.
23
+ *
24
+ * This type is returned via Response events and the `.result()` method, providing
25
+ * real-time visibility into:
26
+ * - Token consumption (input/output/total)
27
+ * - Cost in USD (input/output/total)
28
+ * - Request statistics (count, errors, cache hits, progress percentage)
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const { usage } = await zai.extract(text, schema).result()
33
+ *
34
+ * console.log(usage.tokens.total) // 1250
35
+ * console.log(usage.cost.total) // 0.0075 (USD)
36
+ * console.log(usage.requests.cached) // 0
37
+ * ```
38
+ */
21
39
  export type Usage = {
40
+ /** Request statistics */
22
41
  requests: {
42
+ /** Total number of requests initiated */
23
43
  requests: number
44
+ /** Number of requests that failed with errors */
24
45
  errors: number
46
+ /** Number of successful responses received */
25
47
  responses: number
48
+ /** Number of responses served from cache (no tokens used) */
26
49
  cached: number
50
+ /** Operation progress as a decimal (0.0 to 1.0) */
27
51
  percentage: number
28
52
  }
53
+ /** Cost statistics in USD */
29
54
  cost: {
55
+ /** Cost for input tokens */
30
56
  input: number
57
+ /** Cost for output tokens */
31
58
  output: number
59
+ /** Total cost (input + output) */
32
60
  total: number
33
61
  }
62
+ /** Token usage statistics */
34
63
  tokens: {
64
+ /** Input tokens consumed */
35
65
  input: number
66
+ /** Output tokens generated */
36
67
  output: number
68
+ /** Total tokens (input + output) */
37
69
  total: number
38
70
  }
39
71
  }
@@ -136,11 +136,103 @@ const _Options = z.object({
136
136
  declare module '@botpress/zai' {
137
137
  interface Zai {
138
138
  /**
139
- * Answer questions from a list of support documents with citations
140
- * @param documents - Array of support documents (can be strings, objects, etc.)
139
+ * Answers questions from documents with citations and intelligent handling of edge cases.
140
+ *
141
+ * This operation provides a production-ready question-answering system that:
142
+ * - Cites sources with precise line references
143
+ * - Handles ambiguous questions with multiple interpretations
144
+ * - Detects out-of-topic or invalid questions
145
+ * - Identifies missing knowledge
146
+ * - Automatically chunks and processes large document sets
147
+ *
148
+ * @param documents - Array of documents to search (strings, objects, or any type)
141
149
  * @param question - The question to answer
142
- * @param options - Optional configuration
143
- * @returns Response with answer and citations, or other response types (ambiguous, out_of_topic, etc.)
150
+ * @param options - Configuration for chunking, examples, and instructions
151
+ * @returns Response with answer + citations, or error states (ambiguous, out_of_topic, invalid, missing_knowledge)
152
+ *
153
+ * @example Basic usage with string documents
154
+ * ```typescript
155
+ * const documents = [
156
+ * 'Botpress was founded in 2016.',
157
+ * 'The company is based in Quebec, Canada.',
158
+ * 'Botpress provides an AI agent platform.'
159
+ * ]
160
+ *
161
+ * const result = await zai.answer(documents, 'When was Botpress founded?')
162
+ * if (result.type === 'answer') {
163
+ * console.log(result.answer) // "Botpress was founded in 2016."
164
+ * console.log(result.citations) // [{ offset: 30, item: documents[0], snippet: '...' }]
165
+ * }
166
+ * ```
167
+ *
168
+ * @example With object documents
169
+ * ```typescript
170
+ * const products = [
171
+ * { id: 1, name: 'Pro Plan', price: 99, features: ['AI', 'Analytics'] },
172
+ * { id: 2, name: 'Enterprise', price: 499, features: ['AI', 'Support', 'SLA'] }
173
+ * ]
174
+ *
175
+ * const result = await zai.answer(products, 'What features does the Pro Plan include?')
176
+ * // Returns answer with citations pointing to the product objects
177
+ * ```
178
+ *
179
+ * @example Handling different response types
180
+ * ```typescript
181
+ * const result = await zai.answer(documents, question)
182
+ *
183
+ * switch (result.type) {
184
+ * case 'answer':
185
+ * console.log('Answer:', result.answer)
186
+ * console.log('Sources:', result.citations)
187
+ * break
188
+ *
189
+ * case 'ambiguous':
190
+ * console.log('Question is ambiguous:', result.ambiguity)
191
+ * console.log('Clarifying question:', result.follow_up)
192
+ * console.log('Possible answers:', result.answers)
193
+ * break
194
+ *
195
+ * case 'out_of_topic':
196
+ * console.log('Question unrelated:', result.reason)
197
+ * break
198
+ *
199
+ * case 'invalid_question':
200
+ * console.log('Invalid question:', result.reason)
201
+ * break
202
+ *
203
+ * case 'missing_knowledge':
204
+ * console.log('Insufficient info:', result.reason)
205
+ * break
206
+ * }
207
+ * ```
208
+ *
209
+ * @example With custom instructions
210
+ * ```typescript
211
+ * const result = await zai.answer(documents, 'What is the pricing?', {
212
+ * instructions: 'Provide detailed pricing breakdown including all tiers',
213
+ * chunkLength: 8000 // Process in smaller chunks for accuracy
214
+ * })
215
+ * ```
216
+ *
217
+ * @example Large document sets (auto-chunking)
218
+ * ```typescript
219
+ * // Handles thousands of documents automatically
220
+ * const manyDocs = await loadDocuments() // 1000+ documents
221
+ * const result = await zai.answer(manyDocs, 'What is the refund policy?')
222
+ * // Automatically chunks, processes in parallel, and merges results
223
+ * ```
224
+ *
225
+ * @example Tracking citations
226
+ * ```typescript
227
+ * const result = await zai.answer(documents, question)
228
+ * if (result.type === 'answer') {
229
+ * result.citations.forEach(citation => {
230
+ * console.log(`At position ${citation.offset}:`)
231
+ * console.log(` Cited: "${citation.snippet}"`)
232
+ * console.log(` From document:`, citation.item)
233
+ * })
234
+ * }
235
+ * ```
144
236
  */
145
237
  answer<T>(documents: T[], question: string, options?: Options<T>): Response<AnswerResult<T>, AnswerResult<T>>
146
238
  }
@@ -490,6 +582,7 @@ Question to answer: "${question}"`
490
582
  },
491
583
  ],
492
584
  transform: (text) => {
585
+ text = text.slice(0, text.lastIndexOf(END.slice(0, -1))) // Remove anything after END
493
586
  // Parse and validate response - errors will be caught and retried
494
587
  return parseResponse(text || '', mappings)
495
588
  },
@@ -500,15 +593,18 @@ Question to answer: "${question}"`
500
593
 
501
594
  /**
502
595
  * Parse LLM response into structured result
596
+ * @internal - Exported for testing purposes only
503
597
  */
504
- const parseResponse = <T>(response: string, mappings: LineMapping<T>[]): AnswerResult<T> => {
598
+ export const parseResponse = <T>(response: string, mappings: LineMapping<T>[]): AnswerResult<T> => {
505
599
  const text = response.trim()
506
600
 
601
+ const answersCount = (text.match(new RegExp(ANSWER_START, 'g')) || []).length
602
+
507
603
  // Check response type
508
- if (text.includes(ANSWER_START)) {
509
- return parseAnswerResponse(text, mappings)
510
- } else if (text.includes(AMBIGUOUS_START)) {
604
+ if (text.includes(AMBIGUOUS_START) || answersCount >= 2) {
511
605
  return parseAmbiguousResponse(text, mappings)
606
+ } else if (text.includes(ANSWER_START)) {
607
+ return parseAnswerResponse(text, mappings)
512
608
  } else if (text.includes(OUT_OF_TOPIC_START)) {
513
609
  return parseOutOfTopicResponse(text)
514
610
  } else if (text.includes(INVALID_QUESTION_START)) {
@@ -569,7 +665,7 @@ const parseAmbiguousResponse = <T>(text: string, mappings: LineMapping<T>[]): Am
569
665
  // Extract all possible answers (match until next ■answer or end of string)
570
666
  const answerPattern = /■answer(.+?)(?=■answer|$)/gs
571
667
  const answers: AnswerWithCitations<T>[] = []
572
- let match
668
+ let match: RegExpExecArray | null
573
669
 
574
670
  while ((match = answerPattern.exec(text)) !== null) {
575
671
  const answerText = match[1].trim()