@botpress/zai 2.0.15 → 2.1.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/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.0.15",
4
+ "version": "2.1.0",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "check:type": "tsc --noEmit",
14
- "build": "bp add -y && pnpm run build:types && pnpm run build:neutral",
14
+ "build": "bp add -y && pnpm run build:types && pnpm run build:neutral && size-limit",
15
15
  "build:neutral": "ts-node -T ./build.ts",
16
16
  "build:types": "tsup",
17
17
  "watch": "tsup --watch",
@@ -19,11 +19,17 @@
19
19
  "test:e2e:update": "vitest -u run --config vitest.config.ts",
20
20
  "test:e2e:watch": "vitest --config vitest.config.ts"
21
21
  },
22
+ "size-limit": [
23
+ {
24
+ "limit": "50 kB",
25
+ "path": "dist/**/*.js"
26
+ }
27
+ ],
22
28
  "keywords": [],
23
29
  "author": "",
24
30
  "license": "ISC",
25
31
  "dependencies": {
26
- "@botpress/cognitive": "0.1.28",
32
+ "@botpress/cognitive": "0.1.29",
27
33
  "json5": "^2.2.3",
28
34
  "jsonrepair": "^3.10.0",
29
35
  "lodash-es": "^4.17.21"
@@ -32,12 +38,14 @@
32
38
  "@botpress/client": "workspace:^",
33
39
  "@botpress/common": "workspace:*",
34
40
  "@botpress/vai": "workspace:*",
41
+ "@size-limit/file": "^11.1.6",
35
42
  "@types/lodash-es": "^4.17.12",
36
43
  "diff": "^8.0.1",
37
44
  "dotenv": "^16.4.4",
38
45
  "esbuild": "^0.16.12",
39
46
  "glob": "^9.3.4",
40
47
  "lodash": "^4.17.21",
48
+ "size-limit": "^11.1.6",
41
49
  "tsup": "^8.0.2"
42
50
  },
43
51
  "peerDependencies": {
package/src/context.ts ADDED
@@ -0,0 +1,197 @@
1
+ import { Cognitive, Model, GenerateContentInput, GenerateContentOutput } from '@botpress/cognitive'
2
+ import { Adapter } from './adapters/adapter'
3
+ import { EventEmitter } from './emitter'
4
+
5
+ type Meta = Awaited<ReturnType<Cognitive['generateContent']>>['meta']
6
+
7
+ type GenerateContentProps<T> = Omit<GenerateContentInput, 'model' | 'signal'> & {
8
+ maxRetries?: number
9
+ transform?: (text: string | undefined, output: GenerateContentOutput) => T
10
+ }
11
+
12
+ export type ZaiContextProps = {
13
+ client: Cognitive
14
+ taskType: string
15
+ taskId: string
16
+ modelId: string
17
+ adapter?: Adapter
18
+ source?: GenerateContentInput['meta']
19
+ }
20
+
21
+ export type Usage = {
22
+ requests: {
23
+ requests: number
24
+ errors: number
25
+ responses: number
26
+ cached: number
27
+ percentage: number
28
+ }
29
+ cost: {
30
+ input: number
31
+ output: number
32
+ total: number
33
+ }
34
+ tokens: {
35
+ input: number
36
+ output: number
37
+ total: number
38
+ }
39
+ }
40
+
41
+ type ContextEvents = {
42
+ update: Usage
43
+ }
44
+
45
+ export class ZaiContext {
46
+ private _startedAt = Date.now()
47
+
48
+ private _inputCost = 0
49
+ private _outputCost = 0
50
+ private _inputTokens = 0
51
+ private _outputTokens = 0
52
+ private _totalCachedResponses = 0
53
+
54
+ private _totalRequests = 0
55
+ private _totalErrors = 0
56
+ private _totalResponses = 0
57
+
58
+ public taskId: string
59
+ public taskType: string
60
+ public modelId: GenerateContentInput['model']
61
+ public adapter?: Adapter
62
+ public source?: GenerateContentInput['meta']
63
+
64
+ private _eventEmitter: EventEmitter<ContextEvents>
65
+
66
+ public controller: AbortController = new AbortController()
67
+ private _client: Cognitive
68
+
69
+ public constructor(props: ZaiContextProps) {
70
+ this._client = props.client.clone()
71
+ this.taskId = props.taskId
72
+ this.modelId = props.modelId
73
+ this.adapter = props.adapter
74
+ this.source = props.source
75
+ this.taskType = props.taskType
76
+ this._eventEmitter = new EventEmitter<ContextEvents>()
77
+
78
+ this._client.on('request', () => {
79
+ this._totalRequests++
80
+ this._eventEmitter.emit('update', this.usage)
81
+ })
82
+
83
+ this._client.on('response', (_req, res) => {
84
+ this._totalResponses++
85
+
86
+ if (res.meta.cached) {
87
+ this._totalCachedResponses++
88
+ } else {
89
+ this._inputTokens += res.meta.tokens.input || 0
90
+ this._outputTokens += res.meta.tokens.output || 0
91
+ this._inputCost += res.meta.cost.input || 0
92
+ this._outputCost += res.meta.cost.output || 0
93
+ }
94
+
95
+ this._eventEmitter.emit('update', this.usage)
96
+ })
97
+
98
+ this._client.on('error', () => {
99
+ this._totalErrors++
100
+ this._eventEmitter.emit('update', this.usage)
101
+ })
102
+ }
103
+
104
+ public async getModel(): Promise<Model> {
105
+ return this._client.getModelDetails(this.modelId)
106
+ }
107
+
108
+ public on<K extends keyof ContextEvents>(type: K, listener: (event: ContextEvents[K]) => void) {
109
+ this._eventEmitter.on(type, listener)
110
+ return this
111
+ }
112
+
113
+ public clear() {
114
+ this._eventEmitter.clear()
115
+ }
116
+
117
+ public async generateContent<Out = string>(
118
+ props: GenerateContentProps<Out>
119
+ ): Promise<{ meta: Meta; output: GenerateContentOutput; text: string | undefined; extracted: Out }> {
120
+ const maxRetries = Math.max(props.maxRetries ?? 3, 0)
121
+ const transform = props.transform
122
+ let lastError: Error | null = null
123
+ const messages = [...(props.messages || [])]
124
+
125
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
126
+ try {
127
+ const response = await this._client.generateContent({
128
+ ...props,
129
+ messages,
130
+ signal: this.controller.signal,
131
+ model: this.modelId,
132
+ meta: {
133
+ integrationName: props.meta?.integrationName || 'zai',
134
+ promptCategory: props.meta?.promptCategory || `zai:${this.taskType}`,
135
+ promptSource: props.meta?.promptSource || `zai:${this.taskType}:${this.taskId ?? 'default'}`,
136
+ },
137
+ })
138
+
139
+ const content = response.output.choices[0]?.content
140
+ const str = typeof content === 'string' ? content : content?.[0]?.text || ''
141
+ let output: Out
142
+
143
+ messages.push({
144
+ role: 'assistant',
145
+ content: str || '<Invalid output, no content provided>',
146
+ })
147
+
148
+ if (!transform) {
149
+ output = str as any
150
+ } else {
151
+ output = transform(str, response.output)
152
+ }
153
+
154
+ return { meta: response.meta, output: response.output, text: str, extracted: output }
155
+ } catch (error) {
156
+ lastError = error as Error
157
+
158
+ if (attempt === maxRetries) {
159
+ throw lastError
160
+ }
161
+
162
+ messages.push({
163
+ role: 'user',
164
+ content: `ERROR PARSING OUTPUT\n\n${lastError.message}.\n\nPlease return a valid response addressing the error above.`,
165
+ })
166
+ }
167
+ }
168
+
169
+ throw lastError
170
+ }
171
+
172
+ public get elapsedTime(): number {
173
+ return Date.now() - this._startedAt
174
+ }
175
+
176
+ public get usage(): Usage {
177
+ return {
178
+ requests: {
179
+ errors: this._totalErrors,
180
+ requests: this._totalRequests,
181
+ responses: this._totalResponses,
182
+ cached: this._totalCachedResponses,
183
+ percentage: this._totalRequests > 0 ? (this._totalResponses + this._totalErrors) / this._totalRequests : 0,
184
+ },
185
+ tokens: {
186
+ input: this._inputTokens,
187
+ output: this._outputTokens,
188
+ total: this._inputTokens + this._outputTokens,
189
+ },
190
+ cost: {
191
+ input: this._inputCost,
192
+ output: this._outputCost,
193
+ total: this._inputCost + this._outputCost,
194
+ },
195
+ }
196
+ }
197
+ }
package/src/emitter.ts ADDED
@@ -0,0 +1,49 @@
1
+ export class EventEmitter<E extends object> {
2
+ private _listeners: {
3
+ [K in keyof E]?: ((event: E[K]) => void)[]
4
+ } = {}
5
+
6
+ public emit<K extends keyof E>(type: K, event: E[K]) {
7
+ const listeners = this._listeners[type]
8
+ if (!listeners) {
9
+ return
10
+ }
11
+ for (const listener of listeners) {
12
+ listener(event)
13
+ }
14
+ }
15
+
16
+ public once<K extends keyof E>(type: K, listener: (event: E[K]) => void) {
17
+ const wrapped = (event: E[K]) => {
18
+ this.off(type, wrapped)
19
+ listener(event)
20
+ }
21
+ this.on(type, wrapped)
22
+ }
23
+
24
+ public on<K extends keyof E>(type: K, listener: (event: E[K]) => void) {
25
+ if (!this._listeners[type]) {
26
+ this._listeners[type] = []
27
+ }
28
+ this._listeners[type]!.push(listener)
29
+ }
30
+
31
+ public off<K extends keyof E>(type: K, listener: (event: E[K]) => void) {
32
+ const listeners = this._listeners[type]
33
+ if (!listeners) {
34
+ return
35
+ }
36
+ const index = listeners.indexOf(listener)
37
+ if (index !== -1) {
38
+ listeners.splice(index, 1)
39
+ }
40
+ }
41
+
42
+ public clear<K extends keyof E>(type?: K) {
43
+ if (type) {
44
+ delete this._listeners[type]
45
+ } else {
46
+ this._listeners = {}
47
+ }
48
+ }
49
+ }
@@ -1,6 +1,9 @@
1
1
  // eslint-disable consistent-type-definitions
2
2
  import { z } from '@bpinternal/zui'
3
3
 
4
+ import { ZaiContext } from '../context'
5
+ import { Response } from '../response'
6
+ import { getTokenizer } from '../tokenizer'
4
7
  import { fastHash, stringify, takeUntilTokens } from '../utils'
5
8
  import { Zai } from '../zai'
6
9
  import { PROMPT_INPUT_BUFFER } from './constants'
@@ -35,12 +38,15 @@ declare module '@botpress/zai' {
35
38
  input: unknown,
36
39
  condition: string,
37
40
  options?: Options
38
- ): Promise<{
39
- /** Whether the condition is true or not */
40
- value: boolean
41
- /** The explanation of the decision */
42
- explanation: string
43
- }>
41
+ ): Response<
42
+ {
43
+ /** Whether the condition is true or not */
44
+ value: boolean
45
+ /** The explanation of the decision */
46
+ explanation: string
47
+ },
48
+ boolean
49
+ >
44
50
  }
45
51
  }
46
52
 
@@ -48,13 +54,21 @@ const TRUE = '■TRUE■'
48
54
  const FALSE = '■FALSE■'
49
55
  const END = '■END■'
50
56
 
51
- Zai.prototype.check = async function (this: Zai, input: unknown, condition: string, _options: Options | undefined) {
52
- const options = _Options.parse(_options ?? {}) as Options
53
- const tokenizer = await this.getTokenizer()
54
- await this.fetchModelDetails()
55
- const PROMPT_COMPONENT = Math.max(this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER, 100)
56
-
57
- const taskId = this.taskId
57
+ const check = async (
58
+ input: unknown,
59
+ condition: string,
60
+ options: Options,
61
+ ctx: ZaiContext
62
+ ): Promise<{
63
+ value: boolean
64
+ explanation: string
65
+ }> => {
66
+ ctx.controller.signal.throwIfAborted()
67
+ const tokenizer = await getTokenizer()
68
+ const model = await ctx.getModel()
69
+ const PROMPT_COMPONENT = Math.max(model.input.maxTokens - PROMPT_INPUT_BUFFER, 100)
70
+
71
+ const taskId = ctx.taskId
58
72
  const taskType = 'zai.check'
59
73
 
60
74
  const PROMPT_TOKENS = {
@@ -78,13 +92,14 @@ Zai.prototype.check = async function (this: Zai, input: unknown, condition: stri
78
92
  })
79
93
  )
80
94
 
81
- const examples = taskId
82
- ? await this.adapter.getExamples<string, boolean>({
83
- input: inputAsString,
84
- taskType,
85
- taskId,
86
- })
87
- : []
95
+ const examples =
96
+ taskId && ctx.adapter
97
+ ? await ctx.adapter.getExamples<string, boolean>({
98
+ input: inputAsString,
99
+ taskType,
100
+ taskId,
101
+ })
102
+ : []
88
103
 
89
104
  const exactMatch = examples.find((x) => x.key === Key)
90
105
  if (exactMatch) {
@@ -163,7 +178,10 @@ ${END}
163
178
  `.trim()
164
179
  : ''
165
180
 
166
- const { output, meta } = await this.callModel({
181
+ const {
182
+ extracted: { finalAnswer, explanation },
183
+ meta,
184
+ } = await ctx.generateContent({
167
185
  systemPrompt: `
168
186
  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.
169
187
  Justify your answer, then answer with either ${TRUE} or ${FALSE} at the very end, then add ${END} to finish the response.
@@ -184,35 +202,36 @@ In your "Analysis", please refer to the Expert Examples # to justify your decisi
184
202
  role: 'user',
185
203
  },
186
204
  ],
205
+ transform: (text) => {
206
+ const hasTrue = text.includes(TRUE)
207
+ const hasFalse = text.includes(FALSE)
208
+
209
+ if (!hasTrue && !hasFalse) {
210
+ throw new Error(`The model did not return a valid answer. The response was: ${text}`)
211
+ }
212
+
213
+ let finalAnswer: boolean
214
+ const explanation = text
215
+ .replace(TRUE, '')
216
+ .replace(FALSE, '')
217
+ .replace(END, '')
218
+ .replace('Final Answer:', '')
219
+ .replace('Analysis:', '')
220
+ .trim()
221
+
222
+ if (hasTrue && hasFalse) {
223
+ // If both TRUE and FALSE are present, we need to check which one was answered last
224
+ finalAnswer = text.lastIndexOf(TRUE) > text.lastIndexOf(FALSE)
225
+ } else {
226
+ finalAnswer = hasTrue
227
+ }
228
+
229
+ return { finalAnswer, explanation: explanation.trim() }
230
+ },
187
231
  })
188
232
 
189
- const answer = output.choices[0]?.content as string
190
-
191
- const hasTrue = answer.includes(TRUE)
192
- const hasFalse = answer.includes(FALSE)
193
-
194
- if (!hasTrue && !hasFalse) {
195
- throw new Error(`The model did not return a valid answer. The response was: ${answer}`)
196
- }
197
-
198
- let finalAnswer: boolean
199
- const explanation = answer
200
- .replace(TRUE, '')
201
- .replace(FALSE, '')
202
- .replace(END, '')
203
- .replace('Final Answer:', '')
204
- .replace('Analysis:', '')
205
- .trim()
206
-
207
- if (hasTrue && hasFalse) {
208
- // If both TRUE and FALSE are present, we need to check which one was answered last
209
- finalAnswer = answer.lastIndexOf(TRUE) > answer.lastIndexOf(FALSE)
210
- } else {
211
- finalAnswer = hasTrue
212
- }
213
-
214
- if (taskId) {
215
- await this.adapter.saveExample({
233
+ if (taskId && ctx.adapter && !ctx.controller.signal.aborted) {
234
+ await ctx.adapter.saveExample({
216
235
  key: Key,
217
236
  taskType,
218
237
  taskId,
@@ -224,7 +243,7 @@ In your "Analysis", please refer to the Expert Examples # to justify your decisi
224
243
  output: meta.cost.output,
225
244
  },
226
245
  latency: meta.latency,
227
- model: this.Model,
246
+ model: ctx.modelId,
228
247
  tokens: {
229
248
  input: meta.tokens.input,
230
249
  output: meta.tokens.output,
@@ -240,3 +259,34 @@ In your "Analysis", please refer to the Expert Examples # to justify your decisi
240
259
  explanation: explanation.trim(),
241
260
  }
242
261
  }
262
+
263
+ Zai.prototype.check = function (
264
+ this: Zai,
265
+ input: unknown,
266
+ condition: string,
267
+ _options: Options | undefined
268
+ ): Response<
269
+ {
270
+ value: boolean
271
+ explanation: string
272
+ },
273
+ boolean
274
+ > {
275
+ const options = _Options.parse(_options ?? {}) as Options
276
+
277
+ const context = new ZaiContext({
278
+ client: this.client,
279
+ modelId: this.Model,
280
+ taskId: this.taskId,
281
+ taskType: 'zai.check',
282
+ adapter: this.adapter,
283
+ })
284
+
285
+ return new Response<
286
+ {
287
+ value: boolean
288
+ explanation: string
289
+ },
290
+ boolean
291
+ >(context, check(input, condition, options, context), (result) => result.value)
292
+ }