@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/dist/index.d.ts +1680 -44
- package/dist/index.js +1 -0
- package/dist/micropatch.js +273 -0
- package/dist/operations/answer.js +6 -4
- package/dist/operations/patch.js +398 -0
- package/dist/response.js +166 -1
- package/dist/zai.js +106 -0
- package/e2e/data/cache.jsonl +107 -0
- package/package.json +1 -1
- package/src/context.ts +32 -0
- package/src/index.ts +1 -0
- package/src/micropatch.ts +364 -0
- package/src/operations/answer.ts +105 -9
- package/src/operations/check.ts +75 -1
- package/src/operations/extract.ts +67 -1
- package/src/operations/filter.ts +86 -1
- package/src/operations/group.ts +150 -0
- package/src/operations/label.ts +119 -1
- package/src/operations/patch.ts +656 -0
- package/src/operations/rate.ts +112 -2
- package/src/operations/rewrite.ts +84 -1
- package/src/operations/sort.ts +111 -9
- package/src/operations/summarize.ts +74 -1
- package/src/operations/text.ts +50 -1
- package/src/response.ts +264 -2
- package/src/zai.ts +214 -0
package/package.json
CHANGED
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
|
@@ -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
|
+
}
|
package/src/operations/answer.ts
CHANGED
|
@@ -136,11 +136,103 @@ const _Options = z.object({
|
|
|
136
136
|
declare module '@botpress/zai' {
|
|
137
137
|
interface Zai {
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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 -
|
|
143
|
-
* @returns Response with answer
|
|
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(
|
|
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()
|
package/src/operations/check.ts
CHANGED
|
@@ -33,7 +33,81 @@ const _Options = z.object({
|
|
|
33
33
|
|
|
34
34
|
declare module '@botpress/zai' {
|
|
35
35
|
interface Zai {
|
|
36
|
-
/**
|
|
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,
|