@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/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.5.0",
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
  }
package/src/index.ts CHANGED
@@ -11,5 +11,6 @@ import './operations/group'
11
11
  import './operations/rate'
12
12
  import './operations/sort'
13
13
  import './operations/answer'
14
+ import './operations/patch'
14
15
 
15
16
  export { Zai }
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Micropatch v0.3
3
+ *
4
+ * A tiny engine to parse and apply ultra-compact line-based patches designed for LLMs.
5
+ *
6
+ * Protocol recap:
7
+ * - Source lines are referenced by ORIGINAL 1-based numbers (pre-edit).
8
+ * - Ops start at the beginning of a line with the marker `◼︎`.
9
+ * - Allowed ops:
10
+ * ◼︎<NNN|text → insert single line BEFORE original NNN
11
+ * ◼︎>NNN|text → insert single line AFTER original NNN
12
+ * ◼︎=NNN|line → replace line NNN with one or more lines (multiline payload allowed)
13
+ * ◼︎=NNN-MMM|lines → replace inclusive range NNN..MMM with one or more lines
14
+ * ◼︎-NNN → delete line NNN
15
+ * ◼︎-NNN-MMM → delete inclusive range NNN..MMM
16
+ * - Multiline `=` payload: starts after `|` and continues **until the next line that begins with `◼︎`** or EOF.
17
+ * - Only escaping rule: `\◼︎` inside payload/text becomes a literal `◼︎`. No other escaping is recognized.
18
+ * - Ranges are allowed **only** for `-` (delete) and `=` (replace).
19
+ * - Deterministic apply order on ORIGINAL addresses:
20
+ * 1) Deletes `-` (desc by start)
21
+ * 2) Single-line replaces `=NNN` (asc)
22
+ * 3) Range replaces `=NNN-MMM` (asc by start)
23
+ * 4) Inserts `<` (asc)
24
+ * 5) Inserts `>` (asc)
25
+ *
26
+ * Notes:
27
+ * - The engine maintains a live index map so ORIGINAL references remain valid despite shifting.
28
+ * - Idempotency-friendly: if a target original line no longer maps into current text (e.g., already deleted), the op is skipped.
29
+ * - Whitespace is preserved; EOL style can be chosen or auto-detected.
30
+ */
31
+
32
+ export type EOL = 'lf' | 'crlf'
33
+
34
+ /** Parsed operations (internal normalized form). */
35
+ type Op =
36
+ | { k: '<'; n: number; s: string } // insert before (single-line)
37
+ | { k: '>'; n: number; s: string } // insert after (single-line)
38
+ | { k: '='; n: number; s: string[] } // replace single line with N lines
39
+ | { k: '=-'; a: number; b: number; s: string[] } // replace range with N lines
40
+ | { k: '-'; n: number } // delete single
41
+ | { k: '--'; a: number; b: number } // delete range
42
+
43
+ /**
44
+ * Micropatch: parse & apply patches to text.
45
+ */
46
+ export class Micropatch {
47
+ private _text: string
48
+ private _eol: EOL
49
+
50
+ /**
51
+ * Create a Micropatch instance.
52
+ * @param source The file contents.
53
+ * @param eol Line ending style. If omitted, it is auto-detected from `source` (CRLF if any CRLF is present; otherwise LF).
54
+ */
55
+ public constructor(source: string, eol?: EOL) {
56
+ this._text = source
57
+ this._eol = eol ?? Micropatch.detectEOL(source)
58
+ }
59
+
60
+ /** Get current text. */
61
+ public getText(): string {
62
+ return this._text
63
+ }
64
+
65
+ /**
66
+ * Replace current text.
67
+ * Useful if you want to "load" a new snapshot without reconstructing the class.
68
+ */
69
+ public setText(source: string, eol?: EOL): void {
70
+ this._text = source
71
+ this._eol = eol ?? Micropatch.detectEOL(source)
72
+ }
73
+
74
+ /**
75
+ * Apply ops text to current buffer.
76
+ * @param opsText One or more operations in the v0.3 syntax.
77
+ * @returns The updated text (also stored internally).
78
+ * @throws If the patch contains invalid syntax (e.g., range on insert).
79
+ */
80
+ public apply(opsText: string): string {
81
+ const ops = Micropatch.parseOps(opsText)
82
+ this._text = Micropatch._applyOps(this._text, ops, this._eol)
83
+ return this._text
84
+ }
85
+
86
+ /**
87
+ * Render a numbered view of the current buffer (token-cheap preview for models).
88
+ * Format: `NNN|<line>`, starting at 001.
89
+ */
90
+ public renderNumberedView(): string {
91
+ const NL = this._eol === 'lf' ? '\n' : '\r\n'
92
+ const lines = Micropatch._splitEOL(this._text)
93
+ return lines.map((l, i) => `${String(i + 1).padStart(3, '0')}|${l}`).join(NL)
94
+ }
95
+
96
+ // ---------------------- Static helpers ----------------------
97
+
98
+ /** Detect EOL style from content. */
99
+ public static detectEOL(source: string): EOL {
100
+ return /\r\n/.test(source) ? 'crlf' : 'lf'
101
+ }
102
+
103
+ /** Split text into lines, preserving empty last line if present. */
104
+ private static _splitEOL(text: string): string[] {
105
+ // Use universal split, but keep trailing empty line if final NL exists.
106
+ const parts = text.split(/\r?\n/)
107
+ // If text ends with NL, split leaves an extra empty string we want to keep.
108
+ return parts
109
+ }
110
+
111
+ /** Join lines with the chosen EOL. */
112
+ private static _joinEOL(lines: string[], eol: EOL): string {
113
+ const NL = eol === 'lf' ? '\n' : '\r\n'
114
+ return lines.join(NL)
115
+ }
116
+
117
+ /** Unescape payload text: `\◼︎` → `◼︎`. */
118
+ private static _unescapeMarker(s: string): string {
119
+ return s.replace(/\\◼︎/g, '◼︎')
120
+ }
121
+
122
+ /**
123
+ * Parse ops text (v0.3).
124
+ * - Ignores blank lines and lines not starting with `◼︎` (you can keep comments elsewhere).
125
+ * - Validates ranges for allowed ops.
126
+ */
127
+ public static parseOps(opsText: string): Op[] {
128
+ const lines = opsText.split(/\r?\n/)
129
+ const ops: Op[] = []
130
+
131
+ // Regex for op header line:
132
+ // ◼︎([<>=-])(\d+)(?:-(\d+))?(?:\|(.*))?$
133
+ const headerRe = /^◼︎([<>=-])(\d+)(?:-(\d+))?(?:\|(.*))?$/
134
+
135
+ let i = 0
136
+ while (i < lines.length) {
137
+ const line = lines[i]
138
+ if (!line) {
139
+ i++
140
+ continue
141
+ }
142
+
143
+ if (!line.startsWith('◼︎')) {
144
+ i++
145
+ continue // ignore non-op lines
146
+ }
147
+
148
+ const m = headerRe.exec(line)
149
+ if (!m || !m[1] || !m[2]) {
150
+ throw new Error(`Invalid op syntax at line ${i + 1}: ${line}`)
151
+ }
152
+
153
+ const op = m[1] as '<' | '>' | '=' | '-'
154
+ const aNum = parseInt(m[2], 10)
155
+ const bNum = m[3] ? parseInt(m[3], 10) : undefined
156
+ const firstPayload = m[4] ?? ''
157
+
158
+ if (aNum < 1 || (bNum !== undefined && bNum < aNum)) {
159
+ throw new Error(`Invalid line/range at line ${i + 1}: ${line}`)
160
+ }
161
+
162
+ // Inserts: single-line payload only; ranges are illegal.
163
+ if (op === '<' || op === '>') {
164
+ if (bNum !== undefined) {
165
+ throw new Error(`Insert cannot target a range (line ${i + 1})`)
166
+ }
167
+ const text = Micropatch._unescapeMarker(firstPayload)
168
+ ops.push({ k: op, n: aNum, s: text })
169
+ i++
170
+ continue
171
+ }
172
+
173
+ // Deletes: may be single or range; no payload allowed.
174
+ if (op === '-') {
175
+ if (firstPayload !== '') {
176
+ throw new Error(`Delete must not have a payload (line ${i + 1})`)
177
+ }
178
+ if (bNum === undefined) {
179
+ ops.push({ k: '-', n: aNum })
180
+ } else {
181
+ ops.push({ k: '--', a: aNum, b: bNum })
182
+ }
183
+ i++
184
+ continue
185
+ }
186
+
187
+ // Replaces: '=' supports single or range, and MULTILINE payload.
188
+ if (op === '=') {
189
+ // Collect multiline payload until next line starting with '◼︎' or EOF.
190
+ const payload: string[] = [Micropatch._unescapeMarker(firstPayload)]
191
+ let j = i + 1
192
+ while (j < lines.length) {
193
+ const nextLine = lines[j]
194
+ if (!nextLine || nextLine.startsWith('◼︎')) break
195
+ payload.push(Micropatch._unescapeMarker(nextLine))
196
+ j++
197
+ }
198
+ // Normalize potential trailing empty due to splitting; keep as-is (exact payload).
199
+ if (bNum === undefined) {
200
+ ops.push({ k: '=', n: aNum, s: payload })
201
+ } else {
202
+ ops.push({ k: '=-', a: aNum, b: bNum, s: payload })
203
+ }
204
+ i = j
205
+ continue
206
+ }
207
+
208
+ // Should be unreachable.
209
+ i++
210
+ }
211
+
212
+ return Micropatch._canonicalizeOrder(ops)
213
+ }
214
+
215
+ /** Order ops deterministically according to the spec. */
216
+ private static _canonicalizeOrder(ops: Op[]): Op[] {
217
+ const delS: Array<Extract<Op, { k: '-' }>> = []
218
+ const delR: Array<Extract<Op, { k: '--' }>> = []
219
+ const eqS: Array<Extract<Op, { k: '=' }>> = []
220
+ const eqR: Array<Extract<Op, { k: '=-' }>> = []
221
+ const insB: Array<Extract<Op, { k: '<' }>> = []
222
+ const insA: Array<Extract<Op, { k: '>' }>> = []
223
+
224
+ for (const o of ops) {
225
+ switch (o.k) {
226
+ case '-':
227
+ delS.push(o)
228
+ break
229
+ case '--':
230
+ delR.push(o)
231
+ break
232
+ case '=':
233
+ eqS.push(o)
234
+ break
235
+ case '=-':
236
+ eqR.push(o)
237
+ break
238
+ case '<':
239
+ insB.push(o)
240
+ break
241
+ case '>':
242
+ insA.push(o)
243
+ break
244
+ default:
245
+ break
246
+ }
247
+ }
248
+
249
+ delS.sort((a, b) => b.n - a.n)
250
+ delR.sort((a, b) => b.a - a.a)
251
+ eqS.sort((a, b) => a.n - b.n)
252
+ eqR.sort((a, b) => a.a - b.a)
253
+ insB.sort((a, b) => a.n - b.n)
254
+ insA.sort((a, b) => a.n - b.n)
255
+
256
+ return ([] as Op[]).concat(delS, delR, eqS, eqR, insB, insA)
257
+ }
258
+
259
+ /**
260
+ * Apply normalized ops to given source.
261
+ * - Uses a live index map from ORIGINAL 1-based addresses → current positions.
262
+ * - Skips ops whose targets can no longer be mapped (idempotency-friendly).
263
+ */
264
+ private static _applyOps(source: string, ops: Op[], eol: EOL): string {
265
+ const lines = Micropatch._splitEOL(source)
266
+
267
+ // idx[i] = current position (0-based) of original line (i+1).
268
+ const idx: number[] = Array.from({ length: lines.length }, (_, i) => i)
269
+
270
+ const map = (n: number): number => idx[n - 1] ?? -1
271
+ const bump = (from: number, delta: number) => {
272
+ // Shift all tracked indices at or after `from` by delta.
273
+ for (let i = 0; i < idx.length; i++) {
274
+ const current = idx[i]
275
+ if (current !== undefined && current >= from) {
276
+ idx[i] = current + delta
277
+ }
278
+ }
279
+ }
280
+
281
+ for (const o of ops) {
282
+ switch (o.k) {
283
+ case '-': {
284
+ const i = map(o.n)
285
+ if (i >= 0 && i < lines.length) {
286
+ lines.splice(i, 1)
287
+ bump(i, -1)
288
+ }
289
+ break
290
+ }
291
+ case '--': {
292
+ const a = map(o.a)
293
+ const b = map(o.b)
294
+ if (a >= 0 && b >= a && b < lines.length) {
295
+ lines.splice(a, b - a + 1)
296
+ bump(a, -(b - a + 1))
297
+ }
298
+ break
299
+ }
300
+ case '=': {
301
+ const i = map(o.n)
302
+ if (i >= 0 && i < lines.length) {
303
+ const rep = o.s
304
+ lines.splice(i, 1, ...rep)
305
+ bump(i + 1, rep.length - 1)
306
+ }
307
+ break
308
+ }
309
+ case '=-': {
310
+ const a = map(o.a)
311
+ const b = map(o.b)
312
+ if (a >= 0 && b >= a && b < lines.length) {
313
+ const rep = o.s
314
+ lines.splice(a, b - a + 1, ...rep)
315
+ bump(a + 1, rep.length - (b - a + 1))
316
+ }
317
+ break
318
+ }
319
+ case '<': {
320
+ const i = Math.max(0, Math.min(map(o.n), lines.length))
321
+ if (i >= 0) {
322
+ lines.splice(i, 0, o.s)
323
+ bump(i, +1)
324
+ }
325
+ break
326
+ }
327
+ case '>': {
328
+ const i = Math.max(0, Math.min(map(o.n) + 1, lines.length))
329
+ if (i >= 0) {
330
+ lines.splice(i, 0, o.s)
331
+ bump(i, +1)
332
+ }
333
+ break
334
+ }
335
+ default:
336
+ break
337
+ }
338
+ }
339
+
340
+ return Micropatch._joinEOL(lines, eol)
341
+ }
342
+
343
+ // ---------------------- Convenience APIs ----------------------
344
+
345
+ /**
346
+ * Convenience: one-shot apply.
347
+ * @param source Text to patch.
348
+ * @param opsText Operations text.
349
+ * @param eol EOL style (auto-detected if omitted).
350
+ */
351
+ public static applyText(source: string, opsText: string, eol?: EOL): string {
352
+ const inst = new Micropatch(source, eol)
353
+ return inst.apply(opsText)
354
+ }
355
+
356
+ /**
357
+ * Convenience: parse only.
358
+ * Useful for validation without applying.
359
+ */
360
+ public static validate(opsText: string): { ok: true; count: number } {
361
+ const ops = Micropatch.parseOps(opsText)
362
+ return { ok: true, count: ops.length }
363
+ }
364
+ }
@@ -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()
@@ -33,7 +33,81 @@ const _Options = z.object({
33
33
 
34
34
  declare module '@botpress/zai' {
35
35
  interface Zai {
36
- /** Checks wether a condition is true or not */
36
+ /**
37
+ * Checks whether a condition is true for the given input, with an explanation.
38
+ *
39
+ * This operation evaluates natural language conditions against your input data,
40
+ * returning both a boolean result and a detailed explanation. Perfect for
41
+ * content moderation, sentiment analysis, quality checks, and business rule validation.
42
+ *
43
+ * @param input - The data to evaluate (text, object, or any value)
44
+ * @param condition - Natural language description of the condition to check
45
+ * @param options - Optional examples to guide the evaluation
46
+ * @returns Response with { value: boolean, explanation: string }, simplified to boolean when awaited
47
+ *
48
+ * @example Basic sentiment check
49
+ * ```typescript
50
+ * const review = "This product exceeded my expectations!"
51
+ * const isPositive = await zai.check(review, 'Is the sentiment positive?')
52
+ * // Result: true
53
+ *
54
+ * // Get full details
55
+ * const { value, explanation } = await zai.check(review, 'Is the sentiment positive?').result()
56
+ * // value: true
57
+ * // explanation: "The review expresses satisfaction and exceeded expectations..."
58
+ * ```
59
+ *
60
+ * @example Content moderation
61
+ * ```typescript
62
+ * const comment = "Great article! Very informative."
63
+ * const isSpam = await zai.check(comment, 'Is this spam or promotional content?')
64
+ * // Result: false
65
+ *
66
+ * const hasProfanity = await zai.check(comment, 'Does this contain profanity or offensive language?')
67
+ * // Result: false
68
+ * ```
69
+ *
70
+ * @example Business rules validation
71
+ * ```typescript
72
+ * const invoice = {
73
+ * total: 1500,
74
+ * items: ['laptop', 'mouse'],
75
+ * customer: 'Enterprise Corp'
76
+ * }
77
+ *
78
+ * const needsApproval = await zai.check(
79
+ * invoice,
80
+ * 'Does this invoice require manager approval? (over $1000 or enterprise customer)'
81
+ * )
82
+ * // Result: true
83
+ * ```
84
+ *
85
+ * @example With examples for consistency
86
+ * ```typescript
87
+ * const result = await zai.check(text, 'Is this a technical question?', {
88
+ * examples: [
89
+ * {
90
+ * input: 'How do I deploy to production?',
91
+ * check: true,
92
+ * reason: 'Question about deployment process'
93
+ * },
94
+ * {
95
+ * input: 'What time is the meeting?',
96
+ * check: false,
97
+ * reason: 'Not a technical question'
98
+ * }
99
+ * ]
100
+ * })
101
+ * ```
102
+ *
103
+ * @example Quality assurance
104
+ * ```typescript
105
+ * const code = "function add(a, b) { return a + b }"
106
+ * const hasDocumentation = await zai.check(code, 'Is this code properly documented?')
107
+ * const hasTests = await zai.check(code, 'Does this include unit tests?')
108
+ * const followsConventions = await zai.check(code, 'Does this follow naming conventions?')
109
+ * ```
110
+ */
37
111
  check(
38
112
  input: unknown,
39
113
  condition: string,