@botpress/zai 2.4.2 → 2.5.1
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 +146 -22
- package/dist/index.js +1 -0
- package/dist/micropatch.js +273 -0
- package/dist/operations/patch.js +398 -0
- package/e2e/data/cache.jsonl +105 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/micropatch.ts +364 -0
- package/src/operations/patch.ts +656 -0
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
|
+
"version": "2.5.1",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"author": "",
|
|
33
33
|
"license": "ISC",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@botpress/cognitive": "0.2.
|
|
35
|
+
"@botpress/cognitive": "0.2.1",
|
|
36
36
|
"json5": "^2.2.3",
|
|
37
37
|
"jsonrepair": "^3.10.0",
|
|
38
38
|
"lodash-es": "^4.17.21",
|
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
|
+
}
|