@botpress/zai 1.1.0 → 2.0.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.
@@ -1,19 +1,20 @@
1
1
  import { Client } from '@botpress/client'
2
- import { type TextTokenizer, getWasmTokenizer } from '@botpress/wasm'
3
-
2
+ import { type TextTokenizer, getWasmTokenizer } from '@bpinternal/thicktoken'
4
3
  import fs from 'node:fs'
5
4
  import path from 'node:path'
6
5
  import { beforeAll } from 'vitest'
6
+ import { Zai } from '../src'
7
+ import { fastHash } from '../src/utils'
7
8
 
8
- import { Zai } from '../..'
9
-
10
- import { fastHash } from '../../utils'
9
+ const DATA_PATH = path.join(__dirname, 'data')
10
+ const CACHE_PATH = path.join(DATA_PATH, 'cache.jsonl')
11
+ const DOC_PATH = path.join(DATA_PATH, 'botpress_docs.txt')
11
12
 
12
13
  export const getClient = () => {
13
14
  return new Client({
14
15
  apiUrl: process.env.CLOUD_API_ENDPOINT ?? 'https://api.botpress.dev',
15
16
  botId: process.env.CLOUD_BOT_ID,
16
- token: process.env.CLOUD_PAT
17
+ token: process.env.CLOUD_PAT,
17
18
  })
18
19
  }
19
20
 
@@ -31,10 +32,7 @@ function readJSONL<T>(filePath: string, keyProperty: keyof T): Map<string, T> {
31
32
  return map
32
33
  }
33
34
 
34
- const cache: Map<string, { key: string; value: any }> = readJSONL(
35
- path.resolve(import.meta.dirname, './cache.jsonl'),
36
- 'key'
37
- )
35
+ const cache: Map<string, { key: string; value: any }> = readJSONL(CACHE_PATH, 'key')
38
36
 
39
37
  export const getCachedClient = () => {
40
38
  const client = getClient()
@@ -54,10 +52,10 @@ export const getCachedClient = () => {
54
52
  cache.set(key, { key, value: response })
55
53
 
56
54
  fs.appendFileSync(
57
- path.resolve(import.meta.dirname, './cache.jsonl'),
55
+ CACHE_PATH,
58
56
  JSON.stringify({
59
57
  key,
60
- value: response
58
+ value: response,
61
59
  }) + '\n'
62
60
  )
63
61
 
@@ -65,23 +63,27 @@ export const getCachedClient = () => {
65
63
  }
66
64
  }
67
65
  return Reflect.get(target, prop)
68
- }
66
+ },
69
67
  })
70
68
 
69
+ ;(proxy as any).clone = () => {
70
+ return getCachedClient()
71
+ }
72
+
71
73
  return proxy
72
74
  }
73
75
 
74
76
  export const getZai = () => {
75
77
  const client = getCachedClient()
76
- return new Zai({ client, retry: { maxRetries: 0 } })
78
+ return new Zai({ client })
77
79
  }
78
80
 
79
81
  export let tokenizer: TextTokenizer = null!
80
82
 
81
83
  beforeAll(async () => {
82
- tokenizer = await getWasmTokenizer()
84
+ tokenizer = (await getWasmTokenizer()) as TextTokenizer
83
85
  })
84
86
 
85
- export const BotpressDocumentation = fs.readFileSync(path.join(__dirname, './botpress_docs.txt'), 'utf-8').trim()
87
+ export const BotpressDocumentation = fs.readFileSync(DOC_PATH, 'utf-8').trim()
86
88
 
87
89
  export const metadata = { cost: { input: 1, output: 1 }, latency: 0, model: '', tokens: { input: 1, output: 1 } }
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": "1.1.0",
4
+ "version": "2.0.0",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
@@ -10,39 +10,41 @@
10
10
  "import": "./dist/index.js"
11
11
  },
12
12
  "scripts": {
13
- "build": "npm run build:types && npm run build:neutral",
14
- "build:neutral": "esbuild src/**/*.ts src/*.ts --platform=neutral --outdir=dist",
13
+ "check:type": "tsc --noEmit",
14
+ "build": "bp add -y && pnpm run build:types && pnpm run build:neutral",
15
+ "build:neutral": "ts-node -T ./build.ts",
15
16
  "build:types": "tsup",
16
17
  "watch": "tsup --watch",
17
- "test": "vitest run --config vitest.config.ts",
18
- "test:update": "vitest -u run --config vitest.config.ts",
19
- "test:watch": "vitest --config vitest.config.ts",
20
- "build-with-latest-models": "pnpm run update-types && pnpm run update-models && pnpm run build",
21
- "update-models": "ts-node ./scripts/update-models.mts",
22
- "update-types": "ts-node ./scripts/update-types.mts"
18
+ "test:e2e": "vitest run --config vitest.config.ts",
19
+ "test:e2e:update": "vitest -u run --config vitest.config.ts",
20
+ "test:e2e:watch": "vitest --config vitest.config.ts"
23
21
  },
24
22
  "keywords": [],
25
23
  "author": "",
26
24
  "license": "ISC",
27
25
  "dependencies": {
26
+ "@botpress/cognitive": "0.1.16",
28
27
  "json5": "^2.2.3",
29
- "jsonrepair": "^3.11.2",
28
+ "jsonrepair": "^3.10.0",
30
29
  "lodash-es": "^4.17.21"
31
30
  },
32
31
  "devDependencies": {
33
- "@botpress/vai": "0.0.1-beta.7",
32
+ "@botpress/client": "workspace:^",
33
+ "@botpress/common": "workspace:*",
34
+ "@botpress/vai": "workspace:*",
34
35
  "@types/lodash-es": "^4.17.12",
35
- "dotenv": "^16.4.7",
36
- "esbuild": "^0.24.2",
36
+ "dotenv": "^16.4.4",
37
+ "esbuild": "^0.16.12",
38
+ "glob": "^9.3.4",
37
39
  "lodash": "^4.17.21",
38
- "ts-node": "^10.9.2",
39
- "tsup": "^8.3.5",
40
- "typescript": "^5.7.2",
41
- "vitest": "^2.1.8"
40
+ "tsup": "^8.0.2"
42
41
  },
43
42
  "peerDependencies": {
44
- "@botpress/client": "^0.40.0",
45
- "@botpress/wasm": "^1.0.0",
46
- "@bpinternal/zui": "^0.13.4"
47
- }
43
+ "@bpinternal/thicktoken": "^1.0.0",
44
+ "@bpinternal/zui": "^0.17.1"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
49
+ "packageManager": "pnpm@8.6.2"
48
50
  }
@@ -19,7 +19,7 @@ export type GetExamplesProps<TInput> = {
19
19
  }
20
20
 
21
21
  export abstract class Adapter {
22
- abstract getExamples<TInput, TOutput>(
22
+ public abstract getExamples<TInput, TOutput>(
23
23
  props: GetExamplesProps<TInput>
24
24
  ): Promise<
25
25
  Array<{
@@ -31,5 +31,5 @@ export abstract class Adapter {
31
31
  }>
32
32
  >
33
33
 
34
- abstract saveExample<TInput, TOutput>(props: SaveExampleProps<TInput, TOutput>): Promise<void>
34
+ public abstract saveExample<TInput, TOutput>(props: SaveExampleProps<TInput, TOutput>): Promise<void>
35
35
  }
@@ -1,13 +1,13 @@
1
1
  import { type Client } from '@botpress/client'
2
2
  import { z } from '@bpinternal/zui'
3
3
 
4
- import { BotpressClient, GenerationMetadata } from '../utils'
4
+ import { GenerationMetadata } from '../utils'
5
5
  import { Adapter, GetExamplesProps, SaveExampleProps } from './adapter'
6
6
 
7
7
  const CRITICAL_TAGS = {
8
8
  system: 'true',
9
9
  'schema-purpose': 'active-learning',
10
- 'schema-version': 'Oct-2024'
10
+ 'schema-version': 'Oct-2024',
11
11
  } as const
12
12
 
13
13
  const OPTIONAL_TAGS = {
@@ -15,19 +15,19 @@ const OPTIONAL_TAGS = {
15
15
  'x-studio-description': 'Table for storing active learning tasks and examples',
16
16
  'x-studio-readonly': 'true',
17
17
  'x-studio-icon': 'lucide://atom',
18
- 'x-studio-color': 'green'
18
+ 'x-studio-color': 'green',
19
19
  } as const
20
20
 
21
21
  const FACTOR = 30
22
22
 
23
23
  const Props = z.object({
24
- client: BotpressClient,
24
+ client: z.custom(() => true),
25
25
  tableName: z
26
26
  .string()
27
27
  .regex(
28
28
  /^[a-zA-Z0-9_]{1,45}Table$/,
29
29
  'Table name must be lowercase and contain only letters, numbers and underscores'
30
- )
30
+ ),
31
31
  })
32
32
 
33
33
  export type TableSchema = z.input<typeof TableSchema>
@@ -44,10 +44,10 @@ const TableSchema = z.object({
44
44
  feedback: z
45
45
  .object({
46
46
  rating: z.enum(['very-bad', 'bad', 'good', 'very-good']),
47
- comment: z.string().nullable()
47
+ comment: z.string().nullable(),
48
48
  })
49
49
  .nullable()
50
- .default(null)
50
+ .default(null),
51
51
  })
52
52
 
53
53
  const searchableColumns = ['input'] as const satisfies Array<keyof typeof TableSchema.shape> as string[]
@@ -60,33 +60,33 @@ const TableJsonSchema = Object.entries(TableSchema.shape).reduce((acc, [key, val
60
60
  }, {})
61
61
 
62
62
  export class TableAdapter extends Adapter {
63
- private client: Client
64
- private tableName: string
63
+ private _client: Client
64
+ private _tableName: string
65
65
 
66
- private status: 'initialized' | 'ready' | 'error'
66
+ private _status: 'initialized' | 'ready' | 'error'
67
67
 
68
- constructor(props: z.input<typeof Props>) {
68
+ public constructor(props: z.input<typeof Props>) {
69
69
  super()
70
70
  props = Props.parse(props)
71
- this.client = props.client
72
- this.tableName = props.tableName
73
- this.status = 'ready'
71
+ this._client = props.client
72
+ this._tableName = props.tableName
73
+ this._status = 'ready'
74
74
  }
75
75
 
76
76
  public async getExamples<TInput, TOutput>({ taskType, taskId, input }: GetExamplesProps<TInput>) {
77
- await this.assertTableExists()
77
+ await this._assertTableExists()
78
78
 
79
- const { rows } = await this.client
79
+ const { rows } = await this._client
80
80
  .findTableRows({
81
- table: this.tableName,
81
+ table: this._tableName,
82
82
  search: JSON.stringify({ value: input }).substring(0, 1023), // Search is limited to 1024 characters
83
83
  limit: 10, // TODO
84
84
  filter: {
85
85
  // Proximity match of approved examples
86
86
  taskType,
87
87
  taskId,
88
- status: 'approved'
89
- } satisfies Partial<TableSchema>
88
+ status: 'approved',
89
+ } satisfies Partial<TableSchema>,
90
90
  })
91
91
  .catch((err) => {
92
92
  // TODO: handle error
@@ -99,7 +99,7 @@ export class TableAdapter extends Adapter {
99
99
  input: row.input.value as TInput,
100
100
  output: row.output.value as TOutput,
101
101
  explanation: row.explanation,
102
- similarity: row.similarity ?? 0
102
+ similarity: row.similarity ?? 0,
103
103
  }))
104
104
  }
105
105
 
@@ -112,13 +112,13 @@ export class TableAdapter extends Adapter {
112
112
  output,
113
113
  explanation,
114
114
  metadata,
115
- status = 'pending'
115
+ status = 'pending',
116
116
  }: SaveExampleProps<TInput, TOutput>) {
117
- await this.assertTableExists()
117
+ await this._assertTableExists()
118
118
 
119
- await this.client
119
+ await this._client
120
120
  .upsertTableRows({
121
- table: this.tableName,
121
+ table: this._tableName,
122
122
  keyColumn: 'key',
123
123
  rows: [
124
124
  {
@@ -130,34 +130,34 @@ export class TableAdapter extends Adapter {
130
130
  output: { value: output },
131
131
  explanation: explanation ?? null,
132
132
  status,
133
- metadata
134
- } satisfies TableSchema
135
- ]
133
+ metadata,
134
+ } satisfies TableSchema,
135
+ ],
136
136
  })
137
137
  .catch(() => {
138
138
  // TODO: handle error
139
139
  })
140
140
  }
141
141
 
142
- private async assertTableExists() {
143
- if (this.status !== 'ready') {
142
+ private async _assertTableExists() {
143
+ if (this._status !== 'ready') {
144
144
  return
145
145
  }
146
146
 
147
- const { table, created } = await this.client
147
+ const { table, created } = await this._client
148
148
  .getOrCreateTable({
149
- table: this.tableName,
149
+ table: this._tableName,
150
150
  factor: FACTOR,
151
151
  frozen: true,
152
152
  isComputeEnabled: false,
153
153
  tags: {
154
154
  ...CRITICAL_TAGS,
155
- ...OPTIONAL_TAGS
155
+ ...OPTIONAL_TAGS,
156
156
  },
157
- schema: TableJsonSchema
157
+ schema: TableJsonSchema,
158
158
  })
159
159
  .catch(() => {
160
- this.status = 'error'
160
+ this._status = 'error'
161
161
  return { table: null, created: false }
162
162
  })
163
163
 
@@ -201,10 +201,10 @@ export class TableAdapter extends Adapter {
201
201
  }
202
202
 
203
203
  if (issues.length) {
204
- this.status = 'error'
204
+ this._status = 'error'
205
205
  }
206
206
  }
207
207
 
208
- this.status = 'initialized'
208
+ this._status = 'initialized'
209
209
  }
210
210
  }
@@ -1,13 +1,13 @@
1
1
  import { Adapter } from './adapter'
2
2
 
3
3
  export class MemoryAdapter extends Adapter {
4
- constructor(public examples: any[]) {
4
+ public constructor(public examples: any[]) {
5
5
  super()
6
6
  }
7
7
 
8
- async getExamples() {
8
+ public async getExamples() {
9
9
  return this.examples
10
10
  }
11
11
 
12
- async saveExample() {}
12
+ public async saveExample() {}
13
13
  }
@@ -1,3 +1,4 @@
1
+ // eslint-disable consistent-type-definitions
1
2
  import { z } from '@bpinternal/zui'
2
3
 
3
4
  import { fastHash, stringify, takeUntilTokens } from '../utils'
@@ -7,18 +8,27 @@ import { PROMPT_INPUT_BUFFER } from './constants'
7
8
  const Example = z.object({
8
9
  input: z.any(),
9
10
  check: z.boolean(),
10
- reason: z.string().optional()
11
+ reason: z.string().optional(),
11
12
  })
12
13
 
13
14
  export type Options = z.input<typeof Options>
14
15
  const Options = z.object({
15
- examples: z.array(Example).describe('Examples to check the condition against').default([])
16
+ examples: z.array(Example).describe('Examples to check the condition against').default([]),
16
17
  })
17
18
 
18
19
  declare module '@botpress/zai' {
19
20
  interface Zai {
20
21
  /** Checks wether a condition is true or not */
21
- check(input: unknown, condition: string, options?: Options): Promise<boolean>
22
+ check(
23
+ input: unknown,
24
+ condition: string,
25
+ options?: Options
26
+ ): Promise<{
27
+ /** Whether the condition is true or not */
28
+ value: boolean
29
+ /** The explanation of the decision */
30
+ explanation: string
31
+ }>
22
32
  }
23
33
  }
24
34
 
@@ -29,14 +39,15 @@ const END = '■END■'
29
39
  Zai.prototype.check = async function (this: Zai, input, condition, _options) {
30
40
  const options = Options.parse(_options ?? {})
31
41
  const tokenizer = await this.getTokenizer()
32
- const PROMPT_COMPONENT = Math.max(this.Model.input.maxTokens - PROMPT_INPUT_BUFFER, 100)
42
+ await this.fetchModelDetails()
43
+ const PROMPT_COMPONENT = Math.max(this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER, 100)
33
44
 
34
45
  const taskId = this.taskId
35
46
  const taskType = 'zai.check'
36
47
 
37
48
  const PROMPT_TOKENS = {
38
49
  INPUT: Math.floor(0.5 * PROMPT_COMPONENT),
39
- CONDITION: Math.floor(0.2 * PROMPT_COMPONENT)
50
+ CONDITION: Math.floor(0.2 * PROMPT_COMPONENT),
40
51
  }
41
52
 
42
53
  // Truncate the input to fit the model's input size
@@ -51,7 +62,7 @@ Zai.prototype.check = async function (this: Zai, input, condition, _options) {
51
62
  taskType,
52
63
  taskId,
53
64
  input: inputAsString,
54
- condition
65
+ condition,
55
66
  })
56
67
  )
57
68
 
@@ -59,13 +70,13 @@ Zai.prototype.check = async function (this: Zai, input, condition, _options) {
59
70
  ? await this.adapter.getExamples<string, boolean>({
60
71
  input: inputAsString,
61
72
  taskType,
62
- taskId
73
+ taskId,
63
74
  })
64
75
  : []
65
76
 
66
77
  const exactMatch = examples.find((x) => x.key === Key)
67
78
  if (exactMatch) {
68
- return exactMatch.output
79
+ return { explanation: exactMatch.explanation ?? '', value: exactMatch.output }
69
80
  }
70
81
 
71
82
  const defaultExamples = [
@@ -73,13 +84,14 @@ Zai.prototype.check = async function (this: Zai, input, condition, _options) {
73
84
  {
74
85
  input: ['apple', 'banana', 'carrot', 'house'],
75
86
  check: false,
76
- reason: 'The list contains a house, which is not a fruit. Also, the list contains a carrot, which is a vegetable.'
77
- }
87
+ reason:
88
+ 'The list contains a house, which is not a fruit. Also, the list contains a carrot, which is a vegetable.',
89
+ },
78
90
  ]
79
91
 
80
92
  const userExamples = [
81
93
  ...examples.map((e) => ({ input: e.input, check: e.output, reason: e.explanation })),
82
- ...options.examples
94
+ ...options.examples,
83
95
  ]
84
96
 
85
97
  let exampleId = 1
@@ -108,8 +120,8 @@ ${END}
108
120
  {
109
121
  type: 'text' as const,
110
122
  content: formatOutput(example.check, example.reason ?? ''),
111
- role: 'assistant' as const
112
- }
123
+ role: 'assistant' as const,
124
+ },
113
125
  ]
114
126
 
115
127
  const allExamples = takeUntilTokens(
@@ -129,7 +141,7 @@ ${END}
129
141
  `.trim()
130
142
  : ''
131
143
 
132
- const output = await this.callModel({
144
+ const { output, meta } = await this.callModel({
133
145
  systemPrompt: `
134
146
  Check if the following condition is true or false for the given input. Before answering, make sure to read the input and the condition carefully.
135
147
  Justify your answer, then answer with either ${TRUE} or ${FALSE} at the very end, then add ${END} to finish the response.
@@ -147,9 +159,9 @@ ${specialInstructions}
147
159
  Considering the below input and above examples, is the following condition true or false?
148
160
  ${formatInput(inputAsString, condition)}
149
161
  In your "Analysis", please refer to the Expert Examples # to justify your decision.`.trim(),
150
- role: 'user'
151
- }
152
- ]
162
+ role: 'user',
163
+ },
164
+ ],
153
165
  })
154
166
 
155
167
  const answer = output.choices[0]?.content as string
@@ -162,6 +174,13 @@ In your "Analysis", please refer to the Expert Examples # to justify your decisi
162
174
  }
163
175
 
164
176
  let finalAnswer: boolean
177
+ const explanation = answer
178
+ .replace(TRUE, '')
179
+ .replace(FALSE, '')
180
+ .replace(END, '')
181
+ .replace('Final Answer:', '')
182
+ .replace('Analysis:', '')
183
+ .trim()
165
184
 
166
185
  if (hasTrue && hasFalse) {
167
186
  // If both TRUE and FALSE are present, we need to check which one was answered last
@@ -177,11 +196,25 @@ In your "Analysis", please refer to the Expert Examples # to justify your decisi
177
196
  taskId,
178
197
  input: inputAsString,
179
198
  instructions: condition,
180
- metadata: output.metadata,
199
+ metadata: {
200
+ cost: {
201
+ input: meta.cost.input,
202
+ output: meta.cost.output,
203
+ },
204
+ latency: meta.latency,
205
+ model: this.Model,
206
+ tokens: {
207
+ input: meta.tokens.input,
208
+ output: meta.tokens.output,
209
+ },
210
+ },
181
211
  output: finalAnswer,
182
- explanation: answer.replace(TRUE, '').replace(FALSE, '').replace(END, '').replace('Final Answer:', '').trim()
212
+ explanation,
183
213
  })
184
214
  }
185
215
 
186
- return finalAnswer
216
+ return {
217
+ value: finalAnswer,
218
+ explanation: explanation.trim(),
219
+ }
187
220
  }
@@ -1,5 +1,5 @@
1
1
  export class JsonParsingError extends Error {
2
- constructor(
2
+ public constructor(
3
3
  public json: unknown,
4
4
  public error: Error
5
5
  ) {