@botpress/zai 1.0.1 → 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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/build.ts +9 -0
  3. package/dist/adapters/adapter.js +2 -0
  4. package/dist/adapters/botpress-table.js +168 -0
  5. package/dist/adapters/memory.js +12 -0
  6. package/dist/index.d.ts +111 -609
  7. package/dist/index.js +9 -1873
  8. package/dist/operations/check.js +153 -0
  9. package/dist/operations/constants.js +2 -0
  10. package/dist/operations/errors.js +15 -0
  11. package/dist/operations/extract.js +232 -0
  12. package/dist/operations/filter.js +191 -0
  13. package/dist/operations/label.js +249 -0
  14. package/dist/operations/rewrite.js +123 -0
  15. package/dist/operations/summarize.js +133 -0
  16. package/dist/operations/text.js +47 -0
  17. package/dist/utils.js +37 -0
  18. package/dist/zai.js +100 -0
  19. package/e2e/data/botpress_docs.txt +26040 -0
  20. package/e2e/data/cache.jsonl +107 -0
  21. package/e2e/utils.ts +89 -0
  22. package/package.json +33 -29
  23. package/src/adapters/adapter.ts +35 -0
  24. package/src/adapters/botpress-table.ts +210 -0
  25. package/src/adapters/memory.ts +13 -0
  26. package/src/index.ts +11 -0
  27. package/src/operations/check.ts +201 -0
  28. package/src/operations/constants.ts +2 -0
  29. package/src/operations/errors.ts +9 -0
  30. package/src/operations/extract.ts +309 -0
  31. package/src/operations/filter.ts +244 -0
  32. package/src/operations/label.ts +345 -0
  33. package/src/operations/rewrite.ts +161 -0
  34. package/src/operations/summarize.ts +195 -0
  35. package/src/operations/text.ts +65 -0
  36. package/src/utils.ts +52 -0
  37. package/src/zai.ts +147 -0
  38. package/tsconfig.json +3 -23
  39. package/dist/index.cjs +0 -1903
  40. package/dist/index.cjs.map +0 -1
  41. package/dist/index.d.cts +0 -916
  42. package/dist/index.js.map +0 -1
  43. package/tsup.config.ts +0 -16
  44. package/vitest.config.ts +0 -9
  45. package/vitest.setup.ts +0 -24
package/e2e/utils.ts ADDED
@@ -0,0 +1,89 @@
1
+ import { Client } from '@botpress/client'
2
+ import { type TextTokenizer, getWasmTokenizer } from '@bpinternal/thicktoken'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { beforeAll } from 'vitest'
6
+ import { Zai } from '../src'
7
+ import { fastHash } from '../src/utils'
8
+
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')
12
+
13
+ export const getClient = () => {
14
+ return new Client({
15
+ apiUrl: process.env.CLOUD_API_ENDPOINT ?? 'https://api.botpress.dev',
16
+ botId: process.env.CLOUD_BOT_ID,
17
+ token: process.env.CLOUD_PAT,
18
+ })
19
+ }
20
+
21
+ function readJSONL<T>(filePath: string, keyProperty: keyof T): Map<string, T> {
22
+ const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/).filter(Boolean)
23
+
24
+ const map = new Map<string, T>()
25
+
26
+ for (const line of lines) {
27
+ const obj = JSON.parse(line) as T
28
+ const key = String(obj[keyProperty])
29
+ map.set(key, obj)
30
+ }
31
+
32
+ return map
33
+ }
34
+
35
+ const cache: Map<string, { key: string; value: any }> = readJSONL(CACHE_PATH, 'key')
36
+
37
+ export const getCachedClient = () => {
38
+ const client = getClient()
39
+
40
+ const proxy = new Proxy(client, {
41
+ get(target, prop) {
42
+ if (prop === 'callAction') {
43
+ return async (...args: Parameters<Client['callAction']>) => {
44
+ const key = fastHash(JSON.stringify(args))
45
+ const cached = cache.get(key)
46
+
47
+ if (cached) {
48
+ return cached.value
49
+ }
50
+
51
+ const response = await target.callAction(...args)
52
+ cache.set(key, { key, value: response })
53
+
54
+ fs.appendFileSync(
55
+ CACHE_PATH,
56
+ JSON.stringify({
57
+ key,
58
+ value: response,
59
+ }) + '\n'
60
+ )
61
+
62
+ return response
63
+ }
64
+ }
65
+ return Reflect.get(target, prop)
66
+ },
67
+ })
68
+
69
+ ;(proxy as any).clone = () => {
70
+ return getCachedClient()
71
+ }
72
+
73
+ return proxy
74
+ }
75
+
76
+ export const getZai = () => {
77
+ const client = getCachedClient()
78
+ return new Zai({ client })
79
+ }
80
+
81
+ export let tokenizer: TextTokenizer = null!
82
+
83
+ beforeAll(async () => {
84
+ tokenizer = (await getWasmTokenizer()) as TextTokenizer
85
+ })
86
+
87
+ export const BotpressDocumentation = fs.readFileSync(DOC_PATH, 'utf-8').trim()
88
+
89
+ export const metadata = { cost: { input: 1, output: 1 }, latency: 0, model: '', tokens: { input: 1, output: 1 } }
package/package.json CHANGED
@@ -1,46 +1,50 @@
1
1
  {
2
2
  "name": "@botpress/zai",
3
- "version": "1.0.1",
4
- "type": "module",
5
- "private": false,
6
3
  "description": "Zui AI (zai) – An LLM utility library written on top of Zui and the Botpress API",
4
+ "version": "1.2.0",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
7
  "exports": {
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js",
11
- "require": "./dist/index.cjs"
12
- }
8
+ "types": "./dist/index.d.ts",
9
+ "require": "./dist/index.js",
10
+ "import": "./dist/index.js"
13
11
  },
14
12
  "scripts": {
15
- "build": "tsup",
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",
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 ./src/scripts/update-models.ts",
22
- "update-types": "ts-node ./src/scripts/update-types.ts"
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": {
28
- "json5": "^2.2.1",
29
- "jsonrepair": "^3.2.0"
26
+ "@botpress/cognitive": "^0.1.6",
27
+ "json5": "^2.2.3",
28
+ "jsonrepair": "^3.10.0",
29
+ "lodash-es": "^4.17.21"
30
30
  },
31
31
  "devDependencies": {
32
- "@botpress/vai": "0.0.1-beta.7",
33
- "@types/lodash": "^4.17.0",
34
- "dotenv": "^16.3.1",
35
- "ts-node": "^10.9.2",
36
- "tsup": "^8.3.5",
37
- "typescript": "^5.7.2",
38
- "vitest": "^2.0.5"
32
+ "@botpress/client": "workspace:^",
33
+ "@botpress/common": "workspace:*",
34
+ "@botpress/vai": "workspace:*",
35
+ "@types/lodash-es": "^4.17.12",
36
+ "dotenv": "^16.4.4",
37
+ "esbuild": "^0.16.12",
38
+ "glob": "^9.3.4",
39
+ "lodash": "^4.17.21",
40
+ "tsup": "^8.0.2"
39
41
  },
40
42
  "peerDependencies": {
41
- "@botpress/client": "^0.36.2",
42
- "@botpress/sdk": "^1.6.1",
43
- "@botpress/wasm": "^1.0.0",
44
- "lodash": "^4.17.21"
45
- }
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"
46
50
  }
@@ -0,0 +1,35 @@
1
+ import { GenerationMetadata } from '../utils'
2
+
3
+ export type SaveExampleProps<TInput, TOutput> = {
4
+ key: string
5
+ taskType: string
6
+ taskId: string
7
+ instructions: string
8
+ input: TInput
9
+ output: TOutput
10
+ explanation?: string
11
+ metadata: GenerationMetadata
12
+ status?: 'pending' | 'approved'
13
+ }
14
+
15
+ export type GetExamplesProps<TInput> = {
16
+ taskType: string
17
+ taskId: string
18
+ input: TInput
19
+ }
20
+
21
+ export abstract class Adapter {
22
+ public abstract getExamples<TInput, TOutput>(
23
+ props: GetExamplesProps<TInput>
24
+ ): Promise<
25
+ Array<{
26
+ key: string
27
+ input: TInput
28
+ output: TOutput
29
+ explanation?: string
30
+ similarity: number
31
+ }>
32
+ >
33
+
34
+ public abstract saveExample<TInput, TOutput>(props: SaveExampleProps<TInput, TOutput>): Promise<void>
35
+ }
@@ -0,0 +1,210 @@
1
+ import { type Client } from '@botpress/client'
2
+ import { z } from '@bpinternal/zui'
3
+
4
+ import { GenerationMetadata } from '../utils'
5
+ import { Adapter, GetExamplesProps, SaveExampleProps } from './adapter'
6
+
7
+ const CRITICAL_TAGS = {
8
+ system: 'true',
9
+ 'schema-purpose': 'active-learning',
10
+ 'schema-version': 'Oct-2024',
11
+ } as const
12
+
13
+ const OPTIONAL_TAGS = {
14
+ 'x-studio-title': 'Active Learning',
15
+ 'x-studio-description': 'Table for storing active learning tasks and examples',
16
+ 'x-studio-readonly': 'true',
17
+ 'x-studio-icon': 'lucide://atom',
18
+ 'x-studio-color': 'green',
19
+ } as const
20
+
21
+ const FACTOR = 30
22
+
23
+ const Props = z.object({
24
+ client: z.custom(() => true),
25
+ tableName: z
26
+ .string()
27
+ .regex(
28
+ /^[a-zA-Z0-9_]{1,45}Table$/,
29
+ 'Table name must be lowercase and contain only letters, numbers and underscores'
30
+ ),
31
+ })
32
+
33
+ export type TableSchema = z.input<typeof TableSchema>
34
+ const TableSchema = z.object({
35
+ taskType: z.string().describe('The type of the task (filter, extract, etc.)'),
36
+ taskId: z.string(),
37
+ key: z.string().describe('A unique key for the task (e.g. a hash of the input, taskId, taskType and instructions)'),
38
+ instructions: z.string(),
39
+ input: z.object({}).passthrough().describe('The input to the task'),
40
+ output: z.object({}).passthrough().describe('The expected output'),
41
+ explanation: z.string().nullable(),
42
+ metadata: GenerationMetadata,
43
+ status: z.enum(['pending', 'rejected', 'approved']),
44
+ feedback: z
45
+ .object({
46
+ rating: z.enum(['very-bad', 'bad', 'good', 'very-good']),
47
+ comment: z.string().nullable(),
48
+ })
49
+ .nullable()
50
+ .default(null),
51
+ })
52
+
53
+ const searchableColumns = ['input'] as const satisfies Array<keyof typeof TableSchema.shape> as string[]
54
+
55
+ const TableJsonSchema = Object.entries(TableSchema.shape).reduce((acc, [key, value]) => {
56
+ acc[key] = value.toJsonSchema()
57
+ acc[key]['x-zui'] ??= {}
58
+ acc[key]['x-zui'].searchable = searchableColumns.includes(key)
59
+ return acc
60
+ }, {})
61
+
62
+ export class TableAdapter extends Adapter {
63
+ private _client: Client
64
+ private _tableName: string
65
+
66
+ private _status: 'initialized' | 'ready' | 'error'
67
+
68
+ public constructor(props: z.input<typeof Props>) {
69
+ super()
70
+ props = Props.parse(props)
71
+ this._client = props.client
72
+ this._tableName = props.tableName
73
+ this._status = 'ready'
74
+ }
75
+
76
+ public async getExamples<TInput, TOutput>({ taskType, taskId, input }: GetExamplesProps<TInput>) {
77
+ await this._assertTableExists()
78
+
79
+ const { rows } = await this._client
80
+ .findTableRows({
81
+ table: this._tableName,
82
+ search: JSON.stringify({ value: input }).substring(0, 1023), // Search is limited to 1024 characters
83
+ limit: 10, // TODO
84
+ filter: {
85
+ // Proximity match of approved examples
86
+ taskType,
87
+ taskId,
88
+ status: 'approved',
89
+ } satisfies Partial<TableSchema>,
90
+ })
91
+ .catch((err) => {
92
+ // TODO: handle error
93
+ console.error(`Error fetching examples: ${err.message}`)
94
+ return { rows: [] }
95
+ })
96
+
97
+ return rows.map((row) => ({
98
+ key: row.key,
99
+ input: row.input.value as TInput,
100
+ output: row.output.value as TOutput,
101
+ explanation: row.explanation,
102
+ similarity: row.similarity ?? 0,
103
+ }))
104
+ }
105
+
106
+ public async saveExample<TInput, TOutput>({
107
+ key,
108
+ taskType,
109
+ taskId,
110
+ instructions,
111
+ input,
112
+ output,
113
+ explanation,
114
+ metadata,
115
+ status = 'pending',
116
+ }: SaveExampleProps<TInput, TOutput>) {
117
+ await this._assertTableExists()
118
+
119
+ await this._client
120
+ .upsertTableRows({
121
+ table: this._tableName,
122
+ keyColumn: 'key',
123
+ rows: [
124
+ {
125
+ key,
126
+ taskType,
127
+ taskId,
128
+ instructions,
129
+ input: { value: input },
130
+ output: { value: output },
131
+ explanation: explanation ?? null,
132
+ status,
133
+ metadata,
134
+ } satisfies TableSchema,
135
+ ],
136
+ })
137
+ .catch(() => {
138
+ // TODO: handle error
139
+ })
140
+ }
141
+
142
+ private async _assertTableExists() {
143
+ if (this._status !== 'ready') {
144
+ return
145
+ }
146
+
147
+ const { table, created } = await this._client
148
+ .getOrCreateTable({
149
+ table: this._tableName,
150
+ factor: FACTOR,
151
+ frozen: true,
152
+ isComputeEnabled: false,
153
+ tags: {
154
+ ...CRITICAL_TAGS,
155
+ ...OPTIONAL_TAGS,
156
+ },
157
+ schema: TableJsonSchema,
158
+ })
159
+ .catch(() => {
160
+ this._status = 'error'
161
+ return { table: null, created: false }
162
+ })
163
+
164
+ if (!table) {
165
+ return
166
+ }
167
+
168
+ if (!created) {
169
+ const issues: string[] = []
170
+
171
+ if (table.factor !== FACTOR) {
172
+ issues.push(`Factor is ${table.factor} instead of ${FACTOR}`)
173
+ }
174
+
175
+ if (table.frozen !== true) {
176
+ issues.push('Table is not frozen')
177
+ }
178
+
179
+ for (const [key, value] of Object.entries(CRITICAL_TAGS)) {
180
+ if (table.tags?.[key] !== value) {
181
+ issues.push(`Tag ${key} is ${table.tags?.[key]} instead of ${value}`)
182
+ }
183
+ }
184
+
185
+ for (const key of Object.keys(TableJsonSchema)) {
186
+ const column = table.schema?.properties[key]
187
+ const expected = TableJsonSchema[key] as { type: string }
188
+
189
+ if (!column) {
190
+ issues.push(`Column ${key} is missing`)
191
+ continue
192
+ }
193
+
194
+ if (column.type !== expected.type) {
195
+ issues.push(`Column ${key} has type ${column.type} instead of ${expected.type}`)
196
+ }
197
+
198
+ if (expected['x-zui'].searchable && !column['x-zui'].searchable) {
199
+ issues.push(`Column ${key} is not searchable but should be`)
200
+ }
201
+ }
202
+
203
+ if (issues.length) {
204
+ this._status = 'error'
205
+ }
206
+ }
207
+
208
+ this._status = 'initialized'
209
+ }
210
+ }
@@ -0,0 +1,13 @@
1
+ import { Adapter } from './adapter'
2
+
3
+ export class MemoryAdapter extends Adapter {
4
+ public constructor(public examples: any[]) {
5
+ super()
6
+ }
7
+
8
+ public async getExamples() {
9
+ return this.examples
10
+ }
11
+
12
+ public async saveExample() {}
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Zai } from './zai'
2
+
3
+ import './operations/text'
4
+ import './operations/rewrite'
5
+ import './operations/summarize'
6
+ import './operations/check'
7
+ import './operations/filter'
8
+ import './operations/extract'
9
+ import './operations/label'
10
+
11
+ export { Zai }
@@ -0,0 +1,201 @@
1
+ // eslint-disable consistent-type-definitions
2
+ import { z } from '@bpinternal/zui'
3
+
4
+ import { fastHash, stringify, takeUntilTokens } from '../utils'
5
+ import { Zai } from '../zai'
6
+ import { PROMPT_INPUT_BUFFER } from './constants'
7
+
8
+ const Example = z.object({
9
+ input: z.any(),
10
+ check: z.boolean(),
11
+ reason: z.string().optional(),
12
+ })
13
+
14
+ export type Options = z.input<typeof Options>
15
+ const Options = z.object({
16
+ examples: z.array(Example).describe('Examples to check the condition against').default([]),
17
+ })
18
+
19
+ declare module '@botpress/zai' {
20
+ interface Zai {
21
+ /** Checks wether a condition is true or not */
22
+ check(input: unknown, condition: string, options?: Options): Promise<boolean>
23
+ }
24
+ }
25
+
26
+ const TRUE = '■TRUE■'
27
+ const FALSE = '■FALSE■'
28
+ const END = '■END■'
29
+
30
+ Zai.prototype.check = async function (this: Zai, input, condition, _options) {
31
+ const options = Options.parse(_options ?? {})
32
+ const tokenizer = await this.getTokenizer()
33
+ await this.fetchModelDetails()
34
+ const PROMPT_COMPONENT = Math.max(this.ModelDetails.input.maxTokens - PROMPT_INPUT_BUFFER, 100)
35
+
36
+ const taskId = this.taskId
37
+ const taskType = 'zai.check'
38
+
39
+ const PROMPT_TOKENS = {
40
+ INPUT: Math.floor(0.5 * PROMPT_COMPONENT),
41
+ CONDITION: Math.floor(0.2 * PROMPT_COMPONENT),
42
+ }
43
+
44
+ // Truncate the input to fit the model's input size
45
+ const inputAsString = tokenizer.truncate(stringify(input), PROMPT_TOKENS.INPUT)
46
+ condition = tokenizer.truncate(condition, PROMPT_TOKENS.CONDITION)
47
+
48
+ // All tokens remaining after the input and condition are accounted can be used for examples
49
+ const EXAMPLES_TOKENS = PROMPT_COMPONENT - tokenizer.count(inputAsString) - tokenizer.count(condition)
50
+
51
+ const Key = fastHash(
52
+ JSON.stringify({
53
+ taskType,
54
+ taskId,
55
+ input: inputAsString,
56
+ condition,
57
+ })
58
+ )
59
+
60
+ const examples = taskId
61
+ ? await this.adapter.getExamples<string, boolean>({
62
+ input: inputAsString,
63
+ taskType,
64
+ taskId,
65
+ })
66
+ : []
67
+
68
+ const exactMatch = examples.find((x) => x.key === Key)
69
+ if (exactMatch) {
70
+ return exactMatch.output
71
+ }
72
+
73
+ const defaultExamples = [
74
+ { input: '50 Cent', check: true, reason: '50 Cent is widely recognized as a public personality.' },
75
+ {
76
+ input: ['apple', 'banana', 'carrot', 'house'],
77
+ check: false,
78
+ reason:
79
+ 'The list contains a house, which is not a fruit. Also, the list contains a carrot, which is a vegetable.',
80
+ },
81
+ ]
82
+
83
+ const userExamples = [
84
+ ...examples.map((e) => ({ input: e.input, check: e.output, reason: e.explanation })),
85
+ ...options.examples,
86
+ ]
87
+
88
+ let exampleId = 1
89
+
90
+ const formatInput = (input: string, condition: string) => {
91
+ const header = userExamples.length ? `Expert Example #${exampleId++}` : `Example of condition: "${condition}"`
92
+
93
+ return `
94
+ ${header}
95
+ <|start_input|>
96
+ ${input.trim()}
97
+ <|end_input|>
98
+ `.trim()
99
+ }
100
+
101
+ const formatOutput = (answer: boolean, justification: string) => {
102
+ return `
103
+ Analysis: ${justification}
104
+ Final Answer: ${answer ? TRUE : FALSE}
105
+ ${END}
106
+ `.trim()
107
+ }
108
+
109
+ const formatExample = (example: { input?: any; check: boolean; reason?: string }) => [
110
+ { type: 'text' as const, content: formatInput(stringify(example.input ?? null), condition), role: 'user' as const },
111
+ {
112
+ type: 'text' as const,
113
+ content: formatOutput(example.check, example.reason ?? ''),
114
+ role: 'assistant' as const,
115
+ },
116
+ ]
117
+
118
+ const allExamples = takeUntilTokens(
119
+ userExamples.length ? userExamples : defaultExamples,
120
+ EXAMPLES_TOKENS,
121
+ (el) => tokenizer.count(stringify(el.input)) + tokenizer.count(el.reason ?? '')
122
+ )
123
+ .map(formatExample)
124
+ .flat()
125
+
126
+ const specialInstructions = userExamples.length
127
+ ? `
128
+ - You have been provided with examples from previous experts. Make sure to read them carefully before making your decision.
129
+ - Make sure to refer to the examples provided by the experts to justify your decision (when applicable).
130
+ - When in doubt, ground your decision on the examples provided by the experts instead of your own intuition.
131
+ - When no example is similar to the input, make sure to provide a clear justification for your decision while inferring the decision-making process from the examples provided by the experts.
132
+ `.trim()
133
+ : ''
134
+
135
+ const { output, meta } = await this.callModel({
136
+ systemPrompt: `
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.
138
+ Justify your answer, then answer with either ${TRUE} or ${FALSE} at the very end, then add ${END} to finish the response.
139
+ IMPORTANT: Make sure to answer with either ${TRUE} or ${FALSE} at the end of your response, but NOT both.
140
+ ---
141
+ Expert Examples (#1 to #${exampleId - 1}):
142
+ ${specialInstructions}
143
+ `.trim(),
144
+ stopSequences: [END],
145
+ messages: [
146
+ ...allExamples,
147
+ {
148
+ type: 'text',
149
+ content: `
150
+ Considering the below input and above examples, is the following condition true or false?
151
+ ${formatInput(inputAsString, condition)}
152
+ In your "Analysis", please refer to the Expert Examples # to justify your decision.`.trim(),
153
+ role: 'user',
154
+ },
155
+ ],
156
+ })
157
+
158
+ const answer = output.choices[0]?.content as string
159
+
160
+ const hasTrue = answer.includes(TRUE)
161
+ const hasFalse = answer.includes(FALSE)
162
+
163
+ if (!hasTrue && !hasFalse) {
164
+ throw new Error(`The model did not return a valid answer. The response was: ${answer}`)
165
+ }
166
+
167
+ let finalAnswer: boolean
168
+
169
+ if (hasTrue && hasFalse) {
170
+ // If both TRUE and FALSE are present, we need to check which one was answered last
171
+ finalAnswer = answer.lastIndexOf(TRUE) > answer.lastIndexOf(FALSE)
172
+ } else {
173
+ finalAnswer = hasTrue
174
+ }
175
+
176
+ if (taskId) {
177
+ await this.adapter.saveExample({
178
+ key: Key,
179
+ taskType,
180
+ taskId,
181
+ input: inputAsString,
182
+ instructions: condition,
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
+ },
195
+ output: finalAnswer,
196
+ explanation: answer.replace(TRUE, '').replace(FALSE, '').replace(END, '').replace('Final Answer:', '').trim(),
197
+ })
198
+ }
199
+
200
+ return finalAnswer
201
+ }
@@ -0,0 +1,2 @@
1
+ export const PROMPT_INPUT_BUFFER = 1048
2
+ export const PROMPT_OUTPUT_BUFFER = 512
@@ -0,0 +1,9 @@
1
+ export class JsonParsingError extends Error {
2
+ public constructor(
3
+ public json: unknown,
4
+ public error: Error
5
+ ) {
6
+ const message = `Error parsing JSON:\n\n---JSON---\n${json}\n\n---Error---\n\n ${error}`
7
+ super(message)
8
+ }
9
+ }