@botpress/zai 1.1.0 → 1.2.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": "1.2.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.6",
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,12 +8,12 @@ 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' {
@@ -29,14 +30,15 @@ const END = '■END■'
29
30
  Zai.prototype.check = async function (this: Zai, input, condition, _options) {
30
31
  const options = Options.parse(_options ?? {})
31
32
  const tokenizer = await this.getTokenizer()
32
- const PROMPT_COMPONENT = Math.max(this.Model.input.maxTokens - PROMPT_INPUT_BUFFER, 100)
33
+ await this.fetchModelDetails()
34
+ const PROMPT_COMPONENT = Math.max(this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER, 100)
33
35
 
34
36
  const taskId = this.taskId
35
37
  const taskType = 'zai.check'
36
38
 
37
39
  const PROMPT_TOKENS = {
38
40
  INPUT: Math.floor(0.5 * PROMPT_COMPONENT),
39
- CONDITION: Math.floor(0.2 * PROMPT_COMPONENT)
41
+ CONDITION: Math.floor(0.2 * PROMPT_COMPONENT),
40
42
  }
41
43
 
42
44
  // Truncate the input to fit the model's input size
@@ -51,7 +53,7 @@ Zai.prototype.check = async function (this: Zai, input, condition, _options) {
51
53
  taskType,
52
54
  taskId,
53
55
  input: inputAsString,
54
- condition
56
+ condition,
55
57
  })
56
58
  )
57
59
 
@@ -59,7 +61,7 @@ Zai.prototype.check = async function (this: Zai, input, condition, _options) {
59
61
  ? await this.adapter.getExamples<string, boolean>({
60
62
  input: inputAsString,
61
63
  taskType,
62
- taskId
64
+ taskId,
63
65
  })
64
66
  : []
65
67
 
@@ -73,13 +75,14 @@ Zai.prototype.check = async function (this: Zai, input, condition, _options) {
73
75
  {
74
76
  input: ['apple', 'banana', 'carrot', 'house'],
75
77
  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
- }
78
+ reason:
79
+ 'The list contains a house, which is not a fruit. Also, the list contains a carrot, which is a vegetable.',
80
+ },
78
81
  ]
79
82
 
80
83
  const userExamples = [
81
84
  ...examples.map((e) => ({ input: e.input, check: e.output, reason: e.explanation })),
82
- ...options.examples
85
+ ...options.examples,
83
86
  ]
84
87
 
85
88
  let exampleId = 1
@@ -108,8 +111,8 @@ ${END}
108
111
  {
109
112
  type: 'text' as const,
110
113
  content: formatOutput(example.check, example.reason ?? ''),
111
- role: 'assistant' as const
112
- }
114
+ role: 'assistant' as const,
115
+ },
113
116
  ]
114
117
 
115
118
  const allExamples = takeUntilTokens(
@@ -129,7 +132,7 @@ ${END}
129
132
  `.trim()
130
133
  : ''
131
134
 
132
- const output = await this.callModel({
135
+ const { output, meta } = await this.callModel({
133
136
  systemPrompt: `
134
137
  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
138
  Justify your answer, then answer with either ${TRUE} or ${FALSE} at the very end, then add ${END} to finish the response.
@@ -147,9 +150,9 @@ ${specialInstructions}
147
150
  Considering the below input and above examples, is the following condition true or false?
148
151
  ${formatInput(inputAsString, condition)}
149
152
  In your "Analysis", please refer to the Expert Examples # to justify your decision.`.trim(),
150
- role: 'user'
151
- }
152
- ]
153
+ role: 'user',
154
+ },
155
+ ],
153
156
  })
154
157
 
155
158
  const answer = output.choices[0]?.content as string
@@ -177,9 +180,20 @@ In your "Analysis", please refer to the Expert Examples # to justify your decisi
177
180
  taskId,
178
181
  input: inputAsString,
179
182
  instructions: condition,
180
- metadata: output.metadata,
183
+ metadata: {
184
+ cost: {
185
+ input: meta.cost.input,
186
+ output: meta.cost.output,
187
+ },
188
+ latency: meta.latency,
189
+ model: this.Model,
190
+ tokens: {
191
+ input: meta.tokens.input,
192
+ output: meta.tokens.output,
193
+ },
194
+ },
181
195
  output: finalAnswer,
182
- explanation: answer.replace(TRUE, '').replace(FALSE, '').replace(END, '').replace('Final Answer:', '').trim()
196
+ explanation: answer.replace(TRUE, '').replace(FALSE, '').replace(END, '').replace('Final Answer:', '').trim(),
183
197
  })
184
198
  }
185
199
 
@@ -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
  ) {