@botpress/zai 2.1.19 → 2.1.20

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.1.19",
4
+ "version": "2.1.20",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
@@ -35,7 +35,8 @@
35
35
  "@botpress/cognitive": "0.1.50",
36
36
  "json5": "^2.2.3",
37
37
  "jsonrepair": "^3.10.0",
38
- "lodash-es": "^4.17.21"
38
+ "lodash-es": "^4.17.21",
39
+ "p-limit": "^7.2.0"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@botpress/client": "workspace:^",
@@ -53,7 +54,7 @@
53
54
  },
54
55
  "peerDependencies": {
55
56
  "@bpinternal/thicktoken": "^1.0.0",
56
- "@bpinternal/zui": "1.2.1"
57
+ "@bpinternal/zui": "^1.2.2"
57
58
  },
58
59
  "engines": {
59
60
  "node": ">=18.0.0"
@@ -1,9 +1,104 @@
1
+ import { ZodError } from '@bpinternal/zui'
2
+
1
3
  export class JsonParsingError extends Error {
2
4
  public constructor(
3
5
  public json: unknown,
4
6
  public error: Error
5
7
  ) {
6
- const message = `Error parsing JSON:\n\n---JSON---\n${json}\n\n---Error---\n\n ${error}`
8
+ const message = JsonParsingError._formatError(json, error)
7
9
  super(message)
8
10
  }
11
+
12
+ private static _formatError(json: unknown, error: Error): string {
13
+ let errorMessage = 'Error parsing JSON:\n\n'
14
+ errorMessage += `---JSON---\n${json}\n\n`
15
+
16
+ if (error instanceof ZodError) {
17
+ errorMessage += '---Validation Errors---\n\n'
18
+ errorMessage += JsonParsingError._formatZodError(error)
19
+ } else {
20
+ errorMessage += '---Error---\n\n'
21
+ errorMessage += 'The JSON provided is not valid JSON.\n'
22
+ errorMessage += `Details: ${error.message}\n`
23
+ }
24
+
25
+ return errorMessage
26
+ }
27
+
28
+ private static _formatZodError(zodError: ZodError): string {
29
+ const issues = zodError.issues
30
+ if (issues.length === 0) {
31
+ return 'Unknown validation error\n'
32
+ }
33
+
34
+ let message = ''
35
+ for (let i = 0; i < issues.length; i++) {
36
+ const issue = issues[i]
37
+ const path = issue.path.length > 0 ? issue.path.join('.') : 'root'
38
+
39
+ message += `${i + 1}. Field: "${path}"\n`
40
+
41
+ switch (issue.code) {
42
+ case 'invalid_type':
43
+ message += ` Problem: Expected ${issue.expected}, but received ${issue.received}\n`
44
+ message += ` Message: ${issue.message}\n`
45
+ break
46
+ case 'invalid_string':
47
+ if ('validation' in issue) {
48
+ message += ` Problem: Invalid ${issue.validation} format\n`
49
+ }
50
+ message += ` Message: ${issue.message}\n`
51
+ break
52
+ case 'too_small':
53
+ if (issue.type === 'string') {
54
+ if (issue.exact) {
55
+ message += ` Problem: String must be exactly ${issue.minimum} characters\n`
56
+ } else {
57
+ message += ` Problem: String must be at least ${issue.minimum} characters\n`
58
+ }
59
+ } else if (issue.type === 'number') {
60
+ message += ` Problem: Number must be ${issue.inclusive ? 'at least' : 'greater than'} ${issue.minimum}\n`
61
+ } else if (issue.type === 'array') {
62
+ message += ` Problem: Array must contain ${issue.inclusive ? 'at least' : 'more than'} ${issue.minimum} items\n`
63
+ }
64
+ message += ` Message: ${issue.message}\n`
65
+ break
66
+ case 'too_big':
67
+ if (issue.type === 'string') {
68
+ if (issue.exact) {
69
+ message += ` Problem: String must be exactly ${issue.maximum} characters\n`
70
+ } else {
71
+ message += ` Problem: String must be at most ${issue.maximum} characters\n`
72
+ }
73
+ } else if (issue.type === 'number') {
74
+ message += ` Problem: Number must be ${issue.inclusive ? 'at most' : 'less than'} ${issue.maximum}\n`
75
+ } else if (issue.type === 'array') {
76
+ message += ` Problem: Array must contain ${issue.inclusive ? 'at most' : 'fewer than'} ${issue.maximum} items\n`
77
+ }
78
+ message += ` Message: ${issue.message}\n`
79
+ break
80
+ case 'invalid_enum_value':
81
+ message += ` Problem: Invalid value "${issue.received}"\n`
82
+ message += ` Allowed values: ${issue.options.map((o: any) => `"${o}"`).join(', ')}\n`
83
+ message += ` Message: ${issue.message}\n`
84
+ break
85
+ case 'invalid_literal':
86
+ message += ` Problem: Expected the literal value "${issue.expected}", but received "${issue.received}"\n`
87
+ message += ` Message: ${issue.message}\n`
88
+ break
89
+ case 'invalid_union':
90
+ message += " Problem: Value doesn't match any of the expected formats\n"
91
+ message += ` Message: ${issue.message}\n`
92
+ break
93
+ default:
94
+ message += ` Problem: ${issue.message}\n`
95
+ }
96
+
97
+ if (i < issues.length - 1) {
98
+ message += '\n'
99
+ }
100
+ }
101
+
102
+ return message
103
+ }
9
104
  }
@@ -1,10 +1,11 @@
1
1
  // eslint-disable consistent-type-definitions
2
- import { z, ZodObject } from '@bpinternal/zui'
2
+ import { z, ZodObject, transforms } from '@bpinternal/zui'
3
3
 
4
4
  import JSON5 from 'json5'
5
5
  import { jsonrepair } from 'jsonrepair'
6
6
 
7
7
  import { chunk, isArray } from 'lodash-es'
8
+ import pLimit from 'p-limit'
8
9
  import { ZaiContext } from '../context'
9
10
  import { Response } from '../response'
10
11
  import { getTokenizer } from '../tokenizer'
@@ -48,6 +49,7 @@ declare module '@botpress/zai' {
48
49
  const START = '■json_start■'
49
50
  const END = '■json_end■'
50
51
  const NO_MORE = '■NO_MORE_ELEMENT■'
52
+ const ZERO_ELEMENTS = '■ZERO_ELEMENTS■'
51
53
 
52
54
  const extract = async <S extends OfType<AnyObjectOrArray>>(
53
55
  input: unknown,
@@ -56,7 +58,9 @@ const extract = async <S extends OfType<AnyObjectOrArray>>(
56
58
  ctx: ZaiContext
57
59
  ): Promise<S['_output']> => {
58
60
  ctx.controller.signal.throwIfAborted()
59
- let schema = _schema as any as z.ZodType
61
+
62
+ let schema = transforms.fromJSONSchema(transforms.toJSONSchema(_schema as any as z.ZodType))
63
+
60
64
  const options = Options.parse(_options ?? {})
61
65
  const tokenizer = await getTokenizer()
62
66
  const model = await ctx.getModel()
@@ -110,18 +114,21 @@ const extract = async <S extends OfType<AnyObjectOrArray>>(
110
114
  const inputAsString = stringify(input)
111
115
 
112
116
  if (tokenizer.count(inputAsString) > options.chunkLength) {
117
+ const limit = pLimit(10) // Limit to 10 concurrent extraction operations
113
118
  const tokens = tokenizer.split(inputAsString)
114
119
  const chunks = chunk(tokens, options.chunkLength).map((x) => x.join(''))
115
120
  const all = await Promise.allSettled(
116
121
  chunks.map((chunk) =>
117
- extract(
118
- chunk,
119
- originalSchema,
120
- {
121
- ...options,
122
- strict: false, // We don't want to fail on strict mode for sub-chunks
123
- },
124
- ctx
122
+ limit(() =>
123
+ extract(
124
+ chunk,
125
+ originalSchema,
126
+ {
127
+ ...options,
128
+ strict: false, // We don't want to fail on strict mode for sub-chunks
129
+ },
130
+ ctx
131
+ )
125
132
  )
126
133
  )
127
134
  ).then((results) =>
@@ -162,8 +169,11 @@ Merge it back into a final result.`.trim(),
162
169
  instructions.push('You may have multiple elements, or zero elements in the input.')
163
170
  instructions.push('You must extract each element separately.')
164
171
  instructions.push(`Each element must be a JSON object with exactly the format: ${START}${shape}${END}`)
172
+ instructions.push(`If there are no elements to extract, respond with ${ZERO_ELEMENTS}.`)
165
173
  instructions.push(`When you are done extracting all elements, type "${NO_MORE}" to finish.`)
166
- instructions.push(`For example, if you have zero elements, the output should look like this: ${NO_MORE}`)
174
+ instructions.push(
175
+ `For example, if you have zero elements, the output should look like this: ${ZERO_ELEMENTS}${NO_MORE}`
176
+ )
167
177
  instructions.push(
168
178
  `For example, if you have two elements, the output should look like this: ${START}${abbv}${END}${START}${abbv}${END}${NO_MORE}`
169
179
  )
@@ -2,6 +2,7 @@
2
2
  import { z } from '@bpinternal/zui'
3
3
 
4
4
  import { clamp } from 'lodash-es'
5
+ import pLimit from 'p-limit'
5
6
  import { ZaiContext } from '../context'
6
7
  import { Response } from '../response'
7
8
  import { getTokenizer } from '../tokenizer'
@@ -259,7 +260,8 @@ The condition is: "${condition}"
259
260
  return partial
260
261
  }
261
262
 
262
- const filteredChunks = await Promise.all(chunks.map(filterChunk))
263
+ const limit = pLimit(10) // Limit to 10 concurrent filtering operations
264
+ const filteredChunks = await Promise.all(chunks.map((chunk) => limit(() => filterChunk(chunk))))
263
265
 
264
266
  return filteredChunks.flat()
265
267
  }
@@ -2,6 +2,7 @@
2
2
  import { z } from '@bpinternal/zui'
3
3
 
4
4
  import { chunk, clamp } from 'lodash-es'
5
+ import pLimit from 'p-limit'
5
6
  import { ZaiContext } from '../context'
6
7
  import { Response } from '../response'
7
8
  import { getTokenizer } from '../tokenizer'
@@ -162,9 +163,10 @@ const label = async <T extends string>(
162
163
  const inputAsString = stringify(input)
163
164
 
164
165
  if (tokenizer.count(inputAsString) > CHUNK_INPUT_MAX_TOKENS) {
166
+ const limit = pLimit(10) // Limit to 10 concurrent labeling operations
165
167
  const tokens = tokenizer.split(inputAsString)
166
168
  const chunks = chunk(tokens, CHUNK_INPUT_MAX_TOKENS).map((x) => x.join(''))
167
- const allLabels = await Promise.all(chunks.map((chunk) => label(chunk, _labels, _options, ctx)))
169
+ const allLabels = await Promise.all(chunks.map((chunk) => limit(() => label(chunk, _labels, _options, ctx))))
168
170
 
169
171
  // Merge all the labels together (those who are true will remain true)
170
172
  return allLabels.reduce((acc, x) => {
@@ -2,6 +2,7 @@
2
2
  import { z } from '@bpinternal/zui'
3
3
 
4
4
  import { chunk } from 'lodash-es'
5
+ import pLimit from 'p-limit'
5
6
  import { ZaiContext } from '../context'
6
7
  import { Response } from '../response'
7
8
 
@@ -115,9 +116,9 @@ ${newText}
115
116
  const chunkSize = Math.ceil(tokens.length / (parts * N))
116
117
 
117
118
  if (useMergeSort) {
118
- // TODO: use pLimit here to not have too many chunks
119
+ const limit = pLimit(10) // Limit to 10 concurrent summarization operations
119
120
  const chunks = chunk(tokens, chunkSize).map((x) => x.join(''))
120
- const allSummaries = (await Promise.allSettled(chunks.map((chunk) => summarize(chunk, options, ctx))))
121
+ const allSummaries = (await Promise.allSettled(chunks.map((chunk) => limit(() => summarize(chunk, options, ctx)))))
121
122
  .filter((x) => x.status === 'fulfilled')
122
123
  .map((x) => x.value)
123
124
  return summarize(allSummaries.join('\n\n============\n\n'), options, ctx)
package/src/zai.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Client } from '@botpress/client'
2
- import { BotpressClientLike, Cognitive, Model } from '@botpress/cognitive'
2
+ import { BotpressClientLike, Cognitive, Model, Models } from '@botpress/cognitive'
3
3
 
4
4
  import { type TextTokenizer, getWasmTokenizer } from '@bpinternal/thicktoken'
5
5
  import { z } from '@bpinternal/zui'
@@ -8,8 +8,6 @@ import { Adapter } from './adapters/adapter'
8
8
  import { TableAdapter } from './adapters/botpress-table'
9
9
  import { MemoryAdapter } from './adapters/memory'
10
10
 
11
- type ModelId = Required<Parameters<Cognitive['generateContent']>[0]['model']>
12
-
13
11
  type ActiveLearning = {
14
12
  enable: boolean
15
13
  tableName: string
@@ -39,7 +37,7 @@ const _ActiveLearning = z.object({
39
37
  type ZaiConfig = {
40
38
  client: BotpressClientLike | Cognitive
41
39
  userId?: string
42
- modelId?: ModelId | string
40
+ modelId?: Models
43
41
  activeLearning?: ActiveLearning
44
42
  namespace?: string
45
43
  }
@@ -48,7 +46,7 @@ const _ZaiConfig = z.object({
48
46
  client: z.custom<BotpressClientLike | Cognitive>(),
49
47
  userId: z.string().describe('The ID of the user consuming the API').optional(),
50
48
  modelId: z
51
- .custom<ModelId | string>(
49
+ .custom<Models>(
52
50
  (value) => {
53
51
  if (typeof value !== 'string') {
54
52
  return false
@@ -65,7 +63,7 @@ const _ZaiConfig = z.object({
65
63
  }
66
64
  )
67
65
  .describe('The ID of the model you want to use')
68
- .default('best' satisfies ModelId),
66
+ .default('best' satisfies Models),
69
67
  activeLearning: _ActiveLearning.default({ enable: false }),
70
68
  namespace: z
71
69
  .string()
@@ -84,7 +82,7 @@ export class Zai {
84
82
 
85
83
  private _userId: string | undefined
86
84
 
87
- protected Model: ModelId
85
+ protected Model: Models
88
86
  protected ModelDetails: Model
89
87
  protected namespace: string
90
88
  protected adapter: Adapter
@@ -100,7 +98,7 @@ export class Zai {
100
98
 
101
99
  this.namespace = parsed.namespace
102
100
  this._userId = parsed.userId
103
- this.Model = parsed.modelId as ModelId
101
+ this.Model = parsed.modelId as Models
104
102
  this.activeLearning = parsed.activeLearning as ActiveLearning
105
103
 
106
104
  this.adapter = parsed.activeLearning?.enable
@@ -117,7 +115,7 @@ export class Zai {
117
115
  ): ReturnType<Cognitive['generateContent']> {
118
116
  return this.client.generateContent({
119
117
  ...props,
120
- model: this.Model,
118
+ model: this.Model as Required<Parameters<Cognitive['generateContent']>[0]>['model'],
121
119
  userId: this._userId,
122
120
  })
123
121
  }