@botpress/zai 2.0.8 → 2.0.10

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/dist/index.d.ts CHANGED
@@ -85,14 +85,14 @@ declare module '@botpress/zai' {
85
85
  }
86
86
  }
87
87
 
88
- type Example$2 = {
88
+ type Example$3 = {
89
89
  input: string;
90
90
  output: string;
91
91
  instructions?: string;
92
92
  };
93
93
  type Options$5 = {
94
94
  /** Examples to guide the rewriting */
95
- examples?: Array<Example$2>;
95
+ examples?: Array<Example$3>;
96
96
  /** The maximum number of tokens to generate */
97
97
  length?: number;
98
98
  };
@@ -127,13 +127,15 @@ declare module '@botpress/zai' {
127
127
  }
128
128
  }
129
129
 
130
+ type Example$2 = {
131
+ input: unknown;
132
+ check: boolean;
133
+ reason?: string;
134
+ condition?: string;
135
+ };
130
136
  type Options$3 = {
131
137
  /** Examples to check the condition against */
132
- examples?: Array<{
133
- input: unknown;
134
- check: boolean;
135
- reason?: string;
136
- }>;
138
+ examples?: Array<Example$2>;
137
139
  };
138
140
  declare module '@botpress/zai' {
139
141
  interface Zai {
@@ -5,7 +5,8 @@ import { PROMPT_INPUT_BUFFER } from "./constants";
5
5
  const _Example = z.object({
6
6
  input: z.any(),
7
7
  check: z.boolean(),
8
- reason: z.string().optional()
8
+ reason: z.string().optional(),
9
+ condition: z.string().optional()
9
10
  });
10
11
  const _Options = z.object({
11
12
  examples: z.array(_Example).describe("Examples to check the condition against").default([])
@@ -45,11 +46,17 @@ Zai.prototype.check = async function(input, condition, _options) {
45
46
  return { explanation: exactMatch.explanation ?? "", value: exactMatch.output };
46
47
  }
47
48
  const defaultExamples = [
48
- { input: "50 Cent", check: true, reason: "50 Cent is widely recognized as a public personality." },
49
+ {
50
+ input: "50 Cent",
51
+ check: true,
52
+ reason: "50 Cent is widely recognized as a public personality.",
53
+ condition: "Is the input a public personality?"
54
+ },
49
55
  {
50
56
  input: ["apple", "banana", "carrot", "house"],
51
57
  check: false,
52
- reason: "The list contains a house, which is not a fruit. Also, the list contains a carrot, which is a vegetable."
58
+ reason: "The list contains a house, which is not a fruit. Also, the list contains a carrot, which is a vegetable.",
59
+ condition: "Is the input exclusively a list of fruits?"
53
60
  }
54
61
  ];
55
62
  const userExamples = [
@@ -74,7 +81,11 @@ ${END}
74
81
  `.trim();
75
82
  };
76
83
  const formatExample = (example) => [
77
- { type: "text", content: formatInput(stringify(example.input ?? null), condition), role: "user" },
84
+ {
85
+ type: "text",
86
+ content: formatInput(stringify(example.input ?? null), example.condition ?? condition),
87
+ role: "user"
88
+ },
78
89
  {
79
90
  type: "text",
80
91
  content: formatOutput(example.check, example.reason ?? ""),
package/e2e/client.ts ADDED
@@ -0,0 +1,151 @@
1
+ import { Client } from '@botpress/client'
2
+ import { Cognitive } from '@botpress/cognitive'
3
+ import { diffLines } from 'diff'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import { expect } from 'vitest'
7
+
8
+ function stringifyWithSortedKeys(obj: any, space?: number): string {
9
+ function sortKeys(input: any): any {
10
+ if (Array.isArray(input)) {
11
+ return input.map(sortKeys)
12
+ } else if (input && typeof input === 'object' && input.constructor === Object) {
13
+ return Object.keys(input)
14
+ .sort()
15
+ .reduce(
16
+ (acc, key) => {
17
+ acc[key] = sortKeys(input[key])
18
+ return acc
19
+ },
20
+ {} as Record<string, any>
21
+ )
22
+ } else {
23
+ return input
24
+ }
25
+ }
26
+
27
+ return JSON.stringify(sortKeys(obj), null, space)
28
+ }
29
+
30
+ function readJSONL<T>(filePath: string, keyProperty: keyof T): Map<string, T> {
31
+ const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/).filter(Boolean)
32
+
33
+ const map = new Map<string, T>()
34
+
35
+ for (const line of lines) {
36
+ try {
37
+ const obj = JSON.parse(line) as T
38
+ const key = String(obj[keyProperty])
39
+ map.set(key, obj)
40
+ } catch {}
41
+ }
42
+
43
+ return map
44
+ }
45
+
46
+ type CacheEntry = { key: string; value: any; test: string; input: string }
47
+
48
+ const cache: Map<string, CacheEntry> = readJSONL(path.resolve(__dirname, './data/cache.jsonl'), 'key')
49
+ const cacheByTest: Map<string, CacheEntry> = readJSONL(path.resolve(__dirname, './data/cache.jsonl'), 'test')
50
+
51
+ class CachedClient extends Client {
52
+ #client: Client
53
+ #callsByTest: Record<string, number> = {}
54
+
55
+ public constructor(options: ConstructorParameters<typeof Client>[0]) {
56
+ super(options)
57
+ this.#client = new Client(options)
58
+ }
59
+
60
+ public callAction = async (...args: Parameters<Client['callAction']>) => {
61
+ const currentTestName = expect.getState().currentTestName ?? 'default'
62
+ this.#callsByTest[currentTestName] ||= 0
63
+ this.#callsByTest[currentTestName]++
64
+
65
+ const testKey = `${currentTestName}-${this.#callsByTest[currentTestName]}`
66
+
67
+ const key = fastHash(stringifyWithSortedKeys(args))
68
+ const cached = cache.get(key)
69
+
70
+ if (cached) {
71
+ return cached.value
72
+ }
73
+
74
+ if (process.env.CI && cacheByTest.has(testKey)) {
75
+ console.info(`Cache miss for ${key} in test ${testKey}`)
76
+ console.info(
77
+ diffLines(
78
+ JSON.stringify(JSON.parse(cacheByTest.get(testKey)?.input!), null, 2),
79
+ JSON.stringify(JSON.parse(stringifyWithSortedKeys(args)), null, 2)
80
+ )
81
+ )
82
+ }
83
+
84
+ const response = await this.#client.callAction(...args)
85
+ cache.set(key, { key, value: response, test: testKey, input: stringifyWithSortedKeys(args) })
86
+
87
+ fs.appendFileSync(
88
+ path.resolve(__dirname, './data/cache.jsonl'),
89
+ JSON.stringify({
90
+ test: testKey,
91
+ key,
92
+ input: stringifyWithSortedKeys(args),
93
+ value: response,
94
+ }) + '\n'
95
+ )
96
+
97
+ return response
98
+ }
99
+
100
+ public clone() {
101
+ return this
102
+ }
103
+ }
104
+
105
+ export const getCachedCognitiveClient = () => {
106
+ const cognitive = new Cognitive({
107
+ client: new CachedClient({
108
+ apiUrl: process.env.CLOUD_API_ENDPOINT ?? 'https://api.botpress.dev',
109
+ botId: process.env.CLOUD_BOT_ID,
110
+ token: process.env.CLOUD_PAT,
111
+ }),
112
+ provider: {
113
+ deleteModelPreferences: async () => {},
114
+ saveModelPreferences: async () => {},
115
+ fetchInstalledModels: async () => [
116
+ {
117
+ ref: 'openai:gpt-4o-2024-11-20',
118
+ integration: 'openai',
119
+ id: 'gpt-4o-2024-11-20',
120
+ name: 'GPT-4o (November 2024)',
121
+ description:
122
+ "GPT-4o (“o” for “omni”) is OpenAI's most advanced model. It is multimodal (accepting text or image inputs and outputting text), and it has the same high intelligence as GPT-4 Turbo but is cheaper and more efficient.",
123
+ input: {
124
+ costPer1MTokens: 2.5,
125
+ maxTokens: 128000,
126
+ },
127
+ output: {
128
+ costPer1MTokens: 10,
129
+ maxTokens: 16384,
130
+ },
131
+ tags: ['recommended', 'vision', 'general-purpose', 'coding', 'agents', 'function-calling'],
132
+ },
133
+ ],
134
+ fetchModelPreferences: async () => ({
135
+ best: ['openai:gpt-4o-2024-11-20'] as const,
136
+ fast: ['openai:gpt-4o-2024-11-20'] as const,
137
+ downtimes: [],
138
+ }),
139
+ },
140
+ })
141
+ return cognitive
142
+ }
143
+
144
+ function fastHash(str: string): string {
145
+ let hash = 0
146
+ for (let i = 0; i < str.length; i++) {
147
+ hash = (hash << 5) - hash + str.charCodeAt(i)
148
+ hash |= 0 // Convert to 32bit integer
149
+ }
150
+ return (hash >>> 0).toString(16) // Convert to unsigned and then to hex
151
+ }