@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,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,13 +8,13 @@ import { PROMPT_INPUT_BUFFER } from './constants'
7
8
  type Example = z.input<typeof Example> & { instructions?: string }
8
9
  const Example = z.object({
9
10
  input: z.string(),
10
- output: z.string()
11
+ output: z.string(),
11
12
  })
12
13
 
13
14
  export type Options = z.input<typeof Options>
14
15
  const Options = z.object({
15
16
  examples: z.array(Example).default([]),
16
- length: z.number().min(10).max(16_000).optional().describe('The maximum number of tokens to generate')
17
+ length: z.number().min(10).max(16_000).optional().describe('The maximum number of tokens to generate'),
17
18
  })
18
19
 
19
20
  declare module '@botpress/zai' {
@@ -29,18 +30,19 @@ const END = '■END■'
29
30
  Zai.prototype.rewrite = async function (this: Zai, original, prompt, _options) {
30
31
  const options = Options.parse(_options ?? {})
31
32
  const tokenizer = await this.getTokenizer()
33
+ await this.fetchModelDetails()
32
34
 
33
35
  const taskId = this.taskId
34
36
  const taskType = 'zai.rewrite'
35
37
 
36
- const INPUT_COMPONENT_SIZE = Math.max(100, (this.Model.input.maxTokens - PROMPT_INPUT_BUFFER) / 2)
38
+ const INPUT_COMPONENT_SIZE = Math.max(100, (this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER) / 2)
37
39
  prompt = tokenizer.truncate(prompt, INPUT_COMPONENT_SIZE)
38
40
 
39
41
  const inputSize = tokenizer.count(original) + tokenizer.count(prompt)
40
- const maxInputSize = this.Model.input.maxTokens - tokenizer.count(prompt) - PROMPT_INPUT_BUFFER
42
+ const maxInputSize = this.ModelDetails.input.maxTokens - tokenizer.count(prompt) - PROMPT_INPUT_BUFFER
41
43
  if (inputSize > maxInputSize) {
42
44
  throw new Error(
43
- `The input size is ${inputSize} tokens long, which is more than the maximum of ${maxInputSize} tokens for this model (${this.Model.name} = ${this.Model.input.maxTokens} tokens)`
45
+ `The input size is ${inputSize} tokens long, which is more than the maximum of ${maxInputSize} tokens for this model (${this.ModelDetails.name} = ${this.ModelDetails.input.maxTokens} tokens)`
44
46
  )
45
47
  }
46
48
 
@@ -69,27 +71,27 @@ ${END}
69
71
  taskId,
70
72
  taskType,
71
73
  input: original,
72
- prompt
74
+ prompt,
73
75
  })
74
76
  )
75
77
 
76
78
  const formatExample = ({ input, output, instructions }: Example) => {
77
79
  return [
78
80
  { type: 'text' as const, role: 'user' as const, content: format(input, instructions || prompt) },
79
- { type: 'text' as const, role: 'assistant' as const, content: `${START}${output}${END}` }
81
+ { type: 'text' as const, role: 'assistant' as const, content: `${START}${output}${END}` },
80
82
  ]
81
83
  }
82
84
 
83
85
  const defaultExamples: Example[] = [
84
86
  { input: 'Hello, how are you?', output: 'Bonjour, comment ça va?', instructions: 'translate to French' },
85
- { input: '1\n2\n3', output: '3\n2\n1', instructions: 'reverse the order' }
87
+ { input: '1\n2\n3', output: '3\n2\n1', instructions: 'reverse the order' },
86
88
  ]
87
89
 
88
90
  const tableExamples = taskId
89
91
  ? await this.adapter.getExamples<string, string>({
90
92
  input: original,
91
93
  taskId,
92
- taskType
94
+ taskType,
93
95
  })
94
96
  : []
95
97
 
@@ -100,10 +102,10 @@ ${END}
100
102
 
101
103
  const savedExamples: Example[] = [
102
104
  ...tableExamples.map((x) => ({ input: x.input as string, output: x.output as string })),
103
- ...options.examples
105
+ ...options.examples,
104
106
  ]
105
107
 
106
- const REMAINING_TOKENS = this.Model.input.maxTokens - tokenizer.count(prompt) - PROMPT_INPUT_BUFFER
108
+ const REMAINING_TOKENS = this.ModelDetails.input.maxTokens - tokenizer.count(prompt) - PROMPT_INPUT_BUFFER
107
109
  const examples = takeUntilTokens(
108
110
  savedExamples.length ? savedExamples : defaultExamples,
109
111
  REMAINING_TOKENS,
@@ -112,14 +114,14 @@ ${END}
112
114
  .map(formatExample)
113
115
  .flat()
114
116
 
115
- const output = await this.callModel({
117
+ const { output, meta } = await this.callModel({
116
118
  systemPrompt: `
117
119
  Rewrite the text between the ${START} and ${END} tags to match the user prompt.
118
120
  ${instructions.map((x) => `• ${x}`).join('\n')}
119
121
  `.trim(),
120
122
  messages: [...examples, { type: 'text', content: format(original, prompt), role: 'user' }],
121
123
  maxTokens: options.length,
122
- stopSequences: [END]
124
+ stopSequences: [END],
123
125
  })
124
126
 
125
127
  let result = output.choices[0]?.content as string
@@ -135,12 +137,23 @@ ${instructions.map((x) => `• ${x}`).join('\n')}
135
137
  if (taskId) {
136
138
  await this.adapter.saveExample({
137
139
  key: Key,
138
- metadata: output.metadata,
140
+ metadata: {
141
+ cost: {
142
+ input: meta.cost.input,
143
+ output: meta.cost.output,
144
+ },
145
+ latency: meta.latency,
146
+ model: this.Model,
147
+ tokens: {
148
+ input: meta.tokens.input,
149
+ output: meta.tokens.output,
150
+ },
151
+ },
139
152
  instructions: prompt,
140
153
  input: original,
141
154
  output: result,
142
155
  taskType,
143
- taskId
156
+ taskId,
144
157
  })
145
158
  }
146
159
 
@@ -1,3 +1,4 @@
1
+ // eslint-disable consistent-type-definitions
1
2
  import { z } from '@bpinternal/zui'
2
3
 
3
4
  import { chunk } from 'lodash-es'
@@ -27,10 +28,10 @@ const Options = z.object({
27
28
  sliding: z
28
29
  .object({
29
30
  window: z.number().min(10).max(100_000),
30
- overlap: z.number().min(0).max(100_000)
31
+ overlap: z.number().min(0).max(100_000),
31
32
  })
32
33
  .describe('Sliding window options')
33
- .default({ window: 50_000, overlap: 250 })
34
+ .default({ window: 50_000, overlap: 250 }),
34
35
  })
35
36
 
36
37
  declare module '@botpress/zai' {
@@ -46,20 +47,21 @@ const END = '■END■'
46
47
  Zai.prototype.summarize = async function (this: Zai, original, _options) {
47
48
  const options = Options.parse(_options ?? {})
48
49
  const tokenizer = await this.getTokenizer()
50
+ await this.fetchModelDetails()
49
51
 
50
- const INPUT_COMPONENT_SIZE = Math.max(100, (this.Model.input.maxTokens - PROMPT_INPUT_BUFFER) / 4)
52
+ const INPUT_COMPONENT_SIZE = Math.max(100, (this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER) / 4)
51
53
  options.prompt = tokenizer.truncate(options.prompt, INPUT_COMPONENT_SIZE)
52
54
  options.format = tokenizer.truncate(options.format, INPUT_COMPONENT_SIZE)
53
55
 
54
- const maxOutputSize = this.Model.output.maxTokens - PROMPT_OUTPUT_BUFFER
56
+ const maxOutputSize = this.ModelDetails.output.maxTokens - PROMPT_OUTPUT_BUFFER
55
57
  if (options.length > maxOutputSize) {
56
58
  throw new Error(
57
- `The desired output length is ${maxOutputSize} tokens long, which is more than the maximum of ${this.Model.output.maxTokens} tokens for this model (${this.Model.name})`
59
+ `The desired output length is ${maxOutputSize} tokens long, which is more than the maximum of ${this.ModelDetails.output.maxTokens} tokens for this model (${this.ModelDetails.name})`
58
60
  )
59
61
  }
60
62
 
61
63
  // Ensure the sliding window is not bigger than the model input size
62
- options.sliding.window = Math.min(options.sliding.window, this.Model.input.maxTokens - PROMPT_INPUT_BUFFER)
64
+ options.sliding.window = Math.min(options.sliding.window, this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER)
63
65
 
64
66
  // Ensure the overlap is not bigger than the window
65
67
  // Most extreme case possible (all 3 same size)
@@ -117,7 +119,7 @@ ${newText}
117
119
  'Summarize the text and make sure that the main points are included.',
118
120
  'Ignore any unnecessary details and focus on the main points.',
119
121
  'Use short and concise sentences to increase readability and information density.',
120
- 'When looking at the new information, focus on: ' + options.prompt
122
+ 'When looking at the new information, focus on: ' + options.prompt,
121
123
  ]
122
124
 
123
125
  if (isFirst) {
@@ -157,7 +159,7 @@ ${newText}
157
159
  }
158
160
  }
159
161
 
160
- const output = await this.callModel({
162
+ const { output } = await this.callModel({
161
163
  systemPrompt: `
162
164
  You are summarizing a text. The text is split into ${parts} parts, and you are currently working on part ${iteration}.
163
165
  At every step, you will receive the current summary and a new part of the text. You need to amend the summary to include the new information (if needed).
@@ -171,7 +173,7 @@ ${options.format}
171
173
  `.trim(),
172
174
  messages: [{ type: 'text', content: format(currentSummary, slice), role: 'user' }],
173
175
  maxTokens: generationLength,
174
- stopSequences: [END]
176
+ stopSequences: [END],
175
177
  })
176
178
 
177
179
  let result = output?.choices[0]?.content as string
@@ -1,3 +1,4 @@
1
+ // eslint-disable consistent-type-definitions
1
2
  import { z } from '@bpinternal/zui'
2
3
 
3
4
  import { clamp } from 'lodash-es'
@@ -6,7 +7,7 @@ import { PROMPT_INPUT_BUFFER, PROMPT_OUTPUT_BUFFER } from './constants'
6
7
 
7
8
  export type Options = z.input<typeof Options>
8
9
  const Options = z.object({
9
- length: z.number().min(1).max(100_000).optional().describe('The maximum number of tokens to generate')
10
+ length: z.number().min(1).max(100_000).optional().describe('The maximum number of tokens to generate'),
10
11
  })
11
12
 
12
13
  declare module '@botpress/zai' {
@@ -19,11 +20,12 @@ declare module '@botpress/zai' {
19
20
  Zai.prototype.text = async function (this: Zai, prompt, _options) {
20
21
  const options = Options.parse(_options ?? {})
21
22
  const tokenizer = await this.getTokenizer()
23
+ await this.fetchModelDetails()
22
24
 
23
- prompt = tokenizer.truncate(prompt, Math.max(this.Model.input.maxTokens - PROMPT_INPUT_BUFFER, 100))
25
+ prompt = tokenizer.truncate(prompt, Math.max(this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER, 100))
24
26
 
25
27
  if (options.length) {
26
- options.length = Math.min(this.Model.output.maxTokens - PROMPT_OUTPUT_BUFFER, options.length)
28
+ options.length = Math.min(this.ModelDetails.output.maxTokens - PROMPT_OUTPUT_BUFFER, options.length)
27
29
  }
28
30
 
29
31
  const instructions: string[] = []
@@ -49,7 +51,7 @@ Zai.prototype.text = async function (this: Zai, prompt, _options) {
49
51
  | 300-500 tokens| A long paragraph (200-300 words) |`.trim()
50
52
  }
51
53
 
52
- const output = await this.callModel({
54
+ const { output } = await this.callModel({
53
55
  systemPrompt: `
54
56
  Generate a text that fulfills the user prompt below. Answer directly to the prompt, without any acknowledgements or fluff. Also, make sure the text is standalone and complete.
55
57
  ${instructions.map((x) => `- ${x}`).join('\n')}
@@ -57,7 +59,7 @@ ${chart}
57
59
  `.trim(),
58
60
  temperature: 0.7,
59
61
  messages: [{ type: 'text', content: prompt, role: 'user' }],
60
- maxTokens: options.length
62
+ maxTokens: options.length,
61
63
  })
62
64
  return output?.choices?.[0]?.content! as string
63
65
  }
package/src/utils.ts CHANGED
@@ -1,22 +1,13 @@
1
- import type { Client } from '@botpress/client'
2
1
  import { z } from '@bpinternal/zui'
3
2
 
4
3
  export const stringify = (input: unknown, beautify = true) => {
5
4
  return typeof input === 'string' && !!input.length
6
5
  ? input
7
6
  : input
8
- ? JSON.stringify(input, beautify ? null : undefined, beautify ? 2 : undefined)
9
- : '<input is null, false, undefined or empty>'
7
+ ? JSON.stringify(input, beautify ? null : undefined, beautify ? 2 : undefined)
8
+ : '<input is null, false, undefined or empty>'
10
9
  }
11
10
 
12
- export const BotpressClient = z.custom<Client | any>(
13
- (value) =>
14
- typeof value === 'object' && value !== null && 'callAction' in value && typeof value.callAction === 'function',
15
- {
16
- message: 'Invalid Botpress Client. Make sure to pass an instance of @botpress/client'
17
- }
18
- )
19
-
20
11
  export function fastHash(str: string): string {
21
12
  let hash = 0
22
13
  for (let i = 0; i < str.length; i++) {
@@ -48,14 +39,14 @@ export const GenerationMetadata = z.object({
48
39
  cost: z
49
40
  .object({
50
41
  input: z.number(),
51
- output: z.number()
42
+ output: z.number(),
52
43
  })
53
44
  .describe('Cost in $USD'),
54
45
  latency: z.number().describe('Latency in milliseconds'),
55
46
  tokens: z
56
47
  .object({
57
48
  input: z.number(),
58
- output: z.number()
49
+ output: z.number(),
59
50
  })
60
- .describe('Number of tokens used')
51
+ .describe('Number of tokens used'),
61
52
  })
package/src/zai.ts CHANGED
@@ -1,16 +1,13 @@
1
- import { Client } from '@botpress/client'
1
+ import { BotpressClientLike, Cognitive, Model } from '@botpress/cognitive'
2
+
3
+ import { type TextTokenizer, getWasmTokenizer } from '@bpinternal/thicktoken'
2
4
  import { z } from '@bpinternal/zui'
3
- import { type TextTokenizer, getWasmTokenizer } from '@botpress/wasm'
4
5
 
5
6
  import { Adapter } from './adapters/adapter'
6
7
  import { TableAdapter } from './adapters/botpress-table'
7
8
  import { MemoryAdapter } from './adapters/memory'
8
- import { Models } from './models'
9
- import { llm } from './sdk-interfaces/llm/generateContent'
10
-
11
- import { BotpressClient, GenerationMetadata } from './utils'
12
9
 
13
- type ModelId = (typeof Models)[number]['id']
10
+ type ModelId = Required<Parameters<Cognitive['generateContent']>[0]['model']>
14
11
 
15
12
  type ActiveLearning = z.input<typeof ActiveLearning>
16
13
  const ActiveLearning = z.object({
@@ -30,29 +27,32 @@ const ActiveLearning = z.object({
30
27
  'Namespace must be alphanumeric and contain only letters, numbers, underscores, hyphens and slashes'
31
28
  )
32
29
  .describe('The ID of the task')
33
- .default('default')
30
+ .default('default'),
34
31
  })
35
32
 
36
33
  type ZaiConfig = z.input<typeof ZaiConfig>
37
34
  const ZaiConfig = z.object({
38
- client: BotpressClient,
35
+ client: z.custom<BotpressClientLike | Cognitive>(),
39
36
  userId: z.string().describe('The ID of the user consuming the API').optional(),
40
- retry: z.object({ maxRetries: z.number().min(0).max(100) }).default({ maxRetries: 3 }),
41
37
  modelId: z
42
38
  .custom<ModelId | string>(
43
39
  (value) => {
44
- if (typeof value !== 'string' || !value.includes('__')) {
40
+ if (typeof value !== 'string') {
41
+ return false
42
+ }
43
+
44
+ if (value !== 'best' && value !== 'fast' && !value.includes(':')) {
45
45
  return false
46
46
  }
47
47
 
48
48
  return true
49
49
  },
50
50
  {
51
- message: 'Invalid model ID'
51
+ message: 'Invalid model ID',
52
52
  }
53
53
  )
54
54
  .describe('The ID of the model you want to use')
55
- .default('openai__gpt-4o-mini-2024-07-18' satisfies ModelId),
55
+ .default('best' satisfies ModelId),
56
56
  activeLearning: ActiveLearning.default({ enable: false }),
57
57
  namespace: z
58
58
  .string()
@@ -60,102 +60,50 @@ const ZaiConfig = z.object({
60
60
  /^[A-Za-z0-9_/-]{1,100}$/,
61
61
  'Namespace must be alphanumeric and contain only letters, numbers, underscores, hyphens and slashes'
62
62
  )
63
- .default('zai')
63
+ .default('zai'),
64
64
  })
65
65
 
66
66
  export class Zai {
67
67
  protected static tokenizer: TextTokenizer = null!
68
- protected client: Client
68
+ protected client: Cognitive
69
69
 
70
- private originalConfig: ZaiConfig
70
+ private _originalConfig: ZaiConfig
71
71
 
72
- private userId: string | undefined
73
- private integration: string
74
- private model: string
75
- private retry: { maxRetries: number }
72
+ private _userId: string | undefined
76
73
 
77
- protected Model: (typeof Models)[number]
74
+ protected Model: ModelId
75
+ protected ModelDetails: Model
78
76
  protected namespace: string
79
77
  protected adapter: Adapter
80
78
  protected activeLearning: ActiveLearning
81
79
 
82
- constructor(config: ZaiConfig) {
83
- this.originalConfig = config
80
+ public constructor(config: ZaiConfig) {
81
+ this._originalConfig = config
84
82
  const parsed = ZaiConfig.parse(config)
85
83
 
86
- this.client = parsed.client
87
- const [integration, modelId] = parsed.modelId.split('__')
88
-
89
- if (!integration?.length || !modelId?.length) {
90
- throw new Error(`Invalid model ID: ${parsed.modelId}. Expected format: <integration>__<modelId>`)
91
- }
84
+ this.client = Cognitive.isCognitiveClient(parsed.client)
85
+ ? (parsed.client as unknown as Cognitive)
86
+ : new Cognitive({ client: parsed.client })
92
87
 
93
- this.integration = integration!
94
- this.model = modelId!
95
88
  this.namespace = parsed.namespace
96
- this.userId = parsed.userId
97
- this.retry = parsed.retry as { maxRetries: number }
98
- this.Model = Models.find((m) => m.id === parsed.modelId)!
89
+ this._userId = parsed.userId
90
+ this.Model = parsed.modelId as ModelId
99
91
  this.activeLearning = parsed.activeLearning
100
92
 
101
93
  this.adapter = parsed.activeLearning?.enable
102
- ? new TableAdapter({ client: this.client, tableName: parsed.activeLearning.tableName })
94
+ ? new TableAdapter({ client: this.client.client, tableName: parsed.activeLearning.tableName })
103
95
  : new MemoryAdapter([])
104
96
  }
105
97
 
106
98
  /** @internal */
107
99
  protected async callModel(
108
- props: Partial<llm.generateContent.Input>
109
- ): Promise<llm.generateContent.Output & { metadata: GenerationMetadata }> {
110
- let retries = this.retry.maxRetries
111
- while (retries-- >= 0) {
112
- try {
113
- return await this._callModel(props)
114
- } catch (e) {
115
- if (retries >= 0) {
116
- await new Promise((resolve) => setTimeout(resolve, 1000))
117
- } else {
118
- throw new Error('Failed to call model after multiple retries')
119
- }
120
- }
121
- }
122
-
123
- throw new Error('Failed to call model after multiple retries')
124
- }
125
-
126
- /** @internal */
127
- private async _callModel(
128
- props: Partial<llm.generateContent.Input>
129
- ): Promise<llm.generateContent.Output & { metadata: GenerationMetadata }> {
130
- let retries = this.retry.maxRetries
131
- do {
132
- const start = Date.now()
133
- const input: llm.generateContent.Input = {
134
- messages: [],
135
- temperature: 0.0,
136
- topP: 1,
137
- model: { id: this.model },
138
- userId: this.userId,
139
- ...props
140
- }
141
-
142
- const { output } = (await this.client.callAction({
143
- type: `${this.integration}:generateContent`,
144
- input
145
- })) as unknown as { output: llm.generateContent.Output }
146
-
147
- const latency = Date.now() - start
148
-
149
- return {
150
- ...output,
151
- metadata: {
152
- model: this.model,
153
- latency,
154
- cost: { input: output.usage.inputCost, output: output.usage.outputCost },
155
- tokens: { input: output.usage.inputTokens, output: output.usage.outputTokens }
156
- }
157
- }
158
- } while (--retries > 0)
100
+ props: Parameters<Cognitive['generateContent']>[0]
101
+ ): ReturnType<Cognitive['generateContent']> {
102
+ return this.client.generateContent({
103
+ ...props,
104
+ model: this.Model,
105
+ userId: this._userId,
106
+ })
159
107
  }
160
108
 
161
109
  protected async getTokenizer() {
@@ -164,11 +112,17 @@ export class Zai {
164
112
  // there's an issue with wasm, it doesn't load immediately
165
113
  await new Promise((resolve) => setTimeout(resolve, 25))
166
114
  }
167
- return getWasmTokenizer()
115
+ return getWasmTokenizer() as TextTokenizer
168
116
  })()
169
117
  return Zai.tokenizer
170
118
  }
171
119
 
120
+ protected async fetchModelDetails(): Promise<void> {
121
+ if (!this.ModelDetails) {
122
+ this.ModelDetails = await this.client.getModelDetails(this.Model)
123
+ }
124
+ }
125
+
172
126
  protected get taskId() {
173
127
  if (!this.activeLearning.enable) {
174
128
  return undefined
@@ -179,15 +133,15 @@ export class Zai {
179
133
 
180
134
  public with(options: Partial<ZaiConfig>): Zai {
181
135
  return new Zai({
182
- ...this.originalConfig,
183
- ...options
136
+ ...this._originalConfig,
137
+ ...options,
184
138
  })
185
139
  }
186
140
 
187
141
  public learn(taskId: string) {
188
142
  return new Zai({
189
- ...this.originalConfig,
190
- activeLearning: { ...this.activeLearning, taskId, enable: true }
143
+ ...this._originalConfig,
144
+ activeLearning: { ...this.activeLearning, taskId, enable: true },
191
145
  })
192
146
  }
193
147
  }
package/tsconfig.json CHANGED
@@ -1,32 +1,12 @@
1
1
  {
2
+ "extends": "../../tsconfig.json",
2
3
  "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
4
  "outDir": "dist",
7
- "allowJs": true,
8
- "skipLibCheck": true,
9
- "esModuleInterop": true,
10
- "allowSyntheticDefaultImports": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "disableReferencedProjectLoad": true,
13
- "resolveJsonModule": true,
14
- "isolatedModules": true,
15
5
  "strict": false,
16
- "noUnusedLocals": true,
17
- "noUnusedParameters": true,
18
- "noUncheckedIndexedAccess": true,
19
- "lib": ["dom", "ESNext", "dom.iterable"],
20
- "declaration": true,
21
- "noEmit": false,
22
6
  "paths": {
23
7
  "@botpress/zai": ["./src/zai.ts"]
24
8
  }
25
9
  },
26
10
  "exclude": ["node_modules", "dist"],
27
- "include": ["src/**/*", "vitest.d.ts"],
28
- "ts-node": {
29
- "esm": true,
30
- "require": ["dotenv/config", "./ensure-env.cjs"]
31
- }
11
+ "include": ["src/**/*", "vitest.d.ts", "e2e/**/*"]
32
12
  }