@botpress/vai 0.0.1-beta.1
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/.env +3 -0
- package/README.md +163 -0
- package/dist/index.cjs +506 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +473 -0
- package/dist/index.d.ts +473 -0
- package/dist/index.js +476 -0
- package/dist/index.js.map +1 -0
- package/ensure-env.cjs +9 -0
- package/package.json +45 -0
- package/src/assertions/check.ts +28 -0
- package/src/assertions/extension.ts +51 -0
- package/src/assertions/extract.ts +39 -0
- package/src/assertions/filter.ts +86 -0
- package/src/assertions/rate.ts +40 -0
- package/src/context.ts +65 -0
- package/src/hooks/setEvaluator.ts +13 -0
- package/src/hooks/setupClient.ts +6 -0
- package/src/index.ts +9 -0
- package/src/models.ts +394 -0
- package/src/scripts/update-models.ts +76 -0
- package/src/scripts/update-types.ts +59 -0
- package/src/sdk-interfaces/llm/generateContent.ts +127 -0
- package/src/sdk-interfaces/llm/listLanguageModels.ts +19 -0
- package/src/task/compare.ts +72 -0
- package/src/utils/asyncAssertion.ts +40 -0
- package/src/utils/deferred.ts +20 -0
- package/src/utils/predictJson.ts +114 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
- package/vitest.setup.ts +13 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from '@botpress/sdk'
|
|
2
|
+
import { TestFunction } from 'vitest'
|
|
3
|
+
import { createTaskCollector, getCurrentSuite } from 'vitest/suite'
|
|
4
|
+
import { TestMetadata } from '../context'
|
|
5
|
+
import { Deferred } from '../utils/deferred'
|
|
6
|
+
|
|
7
|
+
const scenarioId = z
|
|
8
|
+
.string()
|
|
9
|
+
.trim()
|
|
10
|
+
.min(1, 'Scenario ID/name must not be empty')
|
|
11
|
+
.max(50, 'Scenario ID/name is too long')
|
|
12
|
+
|
|
13
|
+
export type ScenarioLike = z.infer<typeof ScenarioLike>
|
|
14
|
+
const ScenarioLike = z.union([
|
|
15
|
+
scenarioId,
|
|
16
|
+
z.object({ name: scenarioId }).passthrough(),
|
|
17
|
+
z.object({ id: scenarioId }).passthrough()
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
const getScenarioName = (scenario: ScenarioLike) =>
|
|
21
|
+
(typeof scenario === 'string' ? scenario : 'name' in scenario ? scenario?.name : scenario?.id) as string
|
|
22
|
+
|
|
23
|
+
const scenarioArgs = z
|
|
24
|
+
.array(ScenarioLike)
|
|
25
|
+
.min(2, 'You need at least two scenarios to compare')
|
|
26
|
+
.max(10, 'You can only compare up to 10 scenarios')
|
|
27
|
+
.refine((scenarios) => {
|
|
28
|
+
const set = new Set<string>()
|
|
29
|
+
scenarios.forEach((scenario) => set.add(getScenarioName(scenario)))
|
|
30
|
+
return set.size === scenarios.length
|
|
31
|
+
}, 'Scenarios names must be unique')
|
|
32
|
+
|
|
33
|
+
export function compare<T extends ReadonlyArray<ScenarioLike>>(
|
|
34
|
+
name: string | Function,
|
|
35
|
+
scenarios: T,
|
|
36
|
+
fn?: TestFunction<{
|
|
37
|
+
scenario: T[number]
|
|
38
|
+
}>
|
|
39
|
+
) {
|
|
40
|
+
scenarios = scenarioArgs.parse(scenarios) as unknown as T
|
|
41
|
+
|
|
42
|
+
return createTaskCollector((_name, fn, timeout) => {
|
|
43
|
+
const currentSuite = getCurrentSuite()
|
|
44
|
+
|
|
45
|
+
let completedCount = 0
|
|
46
|
+
const finished = new Deferred<void>()
|
|
47
|
+
|
|
48
|
+
for (const scenario of scenarios) {
|
|
49
|
+
const key = getScenarioName(scenario)
|
|
50
|
+
|
|
51
|
+
currentSuite.task(key, {
|
|
52
|
+
meta: {
|
|
53
|
+
scenario: key,
|
|
54
|
+
isVaiTest: true
|
|
55
|
+
} satisfies TestMetadata,
|
|
56
|
+
handler: async (context) => {
|
|
57
|
+
const extendedContext = Object.freeze({
|
|
58
|
+
scenario
|
|
59
|
+
})
|
|
60
|
+
context.onTestFinished(() => {
|
|
61
|
+
if (++completedCount === scenarios.length) {
|
|
62
|
+
finished.resolve()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await fn({ ...context, ...extendedContext })
|
|
67
|
+
},
|
|
68
|
+
timeout: timeout ?? 10_000
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
})(name, fn)
|
|
72
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Assertion, expect } from 'vitest'
|
|
2
|
+
import { getCurrentTest } from 'vitest/suite'
|
|
3
|
+
import { Context } from '../context'
|
|
4
|
+
import { Output } from './predictJson'
|
|
5
|
+
|
|
6
|
+
export class AsyncExpectError<T> extends Error {
|
|
7
|
+
constructor(message: string, public readonly output: Output<T>) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = 'AsyncExpectError'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getErrorMessages = (e: unknown): string => {
|
|
14
|
+
if (e instanceof Error) {
|
|
15
|
+
return e.message
|
|
16
|
+
} else if (typeof e === 'string') {
|
|
17
|
+
return e
|
|
18
|
+
} else if (typeof e === 'object' && e !== null) {
|
|
19
|
+
return JSON.stringify(e)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `Unknown error: ${e}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const asyncExpect = <T>(output: Promise<Output<T>>, assertion: (assert: Assertion<T>) => void) => {
|
|
26
|
+
const promise = output.then((x) => {
|
|
27
|
+
try {
|
|
28
|
+
assertion(expect(x.result, x.reason))
|
|
29
|
+
} catch (e: unknown) {
|
|
30
|
+
if (Context.wrapError) {
|
|
31
|
+
return new AsyncExpectError<T>(getErrorMessages(e), x)
|
|
32
|
+
}
|
|
33
|
+
throw e
|
|
34
|
+
}
|
|
35
|
+
return x
|
|
36
|
+
})
|
|
37
|
+
getCurrentTest()!.promises ??= []
|
|
38
|
+
getCurrentTest()!.promises!.push(promise)
|
|
39
|
+
return promise
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class Deferred<T> {
|
|
2
|
+
promise: Promise<T>
|
|
3
|
+
private _resolve!: (value: T | PromiseLike<T>) => void
|
|
4
|
+
private _reject!: (reason?: unknown) => void
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.promise = new Promise<T>((resolve, reject) => {
|
|
8
|
+
this._resolve = resolve
|
|
9
|
+
this._reject = reject
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
resolve(value: T | PromiseLike<T>): void {
|
|
14
|
+
this._resolve(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
reject(reason?: unknown): void {
|
|
18
|
+
this._reject(reason)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z, ZodSchema } from '@botpress/sdk'
|
|
2
|
+
import JSON5 from 'json5'
|
|
3
|
+
import { Context } from '../context'
|
|
4
|
+
import { llm } from '../sdk-interfaces/llm/generateContent'
|
|
5
|
+
|
|
6
|
+
const nonEmptyString = z.string().trim().min(1)
|
|
7
|
+
const nonEmptyObject = z
|
|
8
|
+
.object({})
|
|
9
|
+
.passthrough()
|
|
10
|
+
.refine((value) => Object.keys(value).length > 0, {
|
|
11
|
+
message: 'Expected a non-empty object'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export type Input = z.infer<typeof Input>
|
|
15
|
+
const Input = nonEmptyString.or(nonEmptyObject).or(z.array(z.any()))
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
export type Output<T = any> = z.infer<typeof Output> & { result: T }
|
|
19
|
+
const Output = z.object({
|
|
20
|
+
reason: nonEmptyString.describe('A human-readable explanation of the result'),
|
|
21
|
+
result: z
|
|
22
|
+
.any()
|
|
23
|
+
.describe(
|
|
24
|
+
'Your best guess at the output according to the instructions provided, rooted in the context of the input and the reason above'
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
type Example = z.infer<typeof Example>
|
|
29
|
+
const Example = z.object({
|
|
30
|
+
input: Input,
|
|
31
|
+
output: Output
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
type InputOptions<T extends ZodSchema = ZodSchema> = z.input<typeof Options> & { outputSchema: T }
|
|
35
|
+
type Options = z.infer<typeof Options>
|
|
36
|
+
const Options = z.object({
|
|
37
|
+
systemMessage: z.string(),
|
|
38
|
+
examples: z.array(Example).default([]),
|
|
39
|
+
input: Input,
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
outputSchema: z.custom<ZodSchema<any>>((value) => value instanceof ZodSchema),
|
|
42
|
+
model: z.string()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
type Message = {
|
|
46
|
+
role: 'user' | 'assistant' | 'system'
|
|
47
|
+
content: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isValidExample =
|
|
51
|
+
(outputSchema: ZodSchema) =>
|
|
52
|
+
(example: Example): example is Example =>
|
|
53
|
+
Input.safeParse(example.input).success &&
|
|
54
|
+
Output.safeParse(example.output).success &&
|
|
55
|
+
outputSchema.safeParse(example.output.result).success
|
|
56
|
+
|
|
57
|
+
export async function predictJson<T extends ZodSchema>(_options: InputOptions<T>): Promise<Output<z.infer<T>>> {
|
|
58
|
+
const options = Options.parse(_options)
|
|
59
|
+
const [integration, model] = options.model.split('__')
|
|
60
|
+
|
|
61
|
+
if (!model?.length) {
|
|
62
|
+
throw new Error('Invalid model')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const exampleMessages = options.examples
|
|
66
|
+
.filter(isValidExample(options.outputSchema))
|
|
67
|
+
.flatMap(({ input, output }) => [
|
|
68
|
+
{ role: 'user', content: JSON.stringify(input, null, 2) } satisfies Message,
|
|
69
|
+
{ role: 'assistant', content: JSON.stringify(output, null, 2) } satisfies Message
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
const outputSchema = Output.extend({
|
|
73
|
+
result: options.outputSchema.describe(Output.shape.result.description!)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const result = await Context.client.callAction({
|
|
77
|
+
type: `${integration}:generateContent`,
|
|
78
|
+
input: {
|
|
79
|
+
systemPrompt: `
|
|
80
|
+
${options.systemMessage}
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
Please generate a JSON response with the following format:
|
|
84
|
+
\`\`\`typescript
|
|
85
|
+
${await outputSchema.toTypescriptAsync()}
|
|
86
|
+
\`\`\`
|
|
87
|
+
`.trim(),
|
|
88
|
+
messages: [
|
|
89
|
+
...exampleMessages,
|
|
90
|
+
{
|
|
91
|
+
role: 'user',
|
|
92
|
+
content: JSON.stringify(options.input, null, 2)
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
temperature: 0,
|
|
96
|
+
responseFormat: 'json_object',
|
|
97
|
+
model: { id: model! }
|
|
98
|
+
} satisfies llm.generateContent.Input
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const output = result.output as llm.generateContent.Output
|
|
102
|
+
|
|
103
|
+
if (!output.choices.length || typeof output.choices?.[0]?.content !== 'string') {
|
|
104
|
+
throw new Error('Invalid response from the model')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const json = output.choices[0].content.trim()
|
|
108
|
+
|
|
109
|
+
if (!json.length) {
|
|
110
|
+
throw new Error('No response from the model')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return outputSchema.parse(JSON5.parse(json)) as Output<z.infer<T>>
|
|
114
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"esModuleInterop": true,
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"target": "ES2017",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"jsx": "preserve",
|
|
9
|
+
"noUnusedLocals": true,
|
|
10
|
+
"noUnusedParameters": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"lib": ["dom", "ESNext", "dom.iterable"],
|
|
14
|
+
"allowJs": false,
|
|
15
|
+
"declaration": false,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"noEmit": false,
|
|
19
|
+
"resolveJsonModule": true,
|
|
20
|
+
"isolatedModules": true
|
|
21
|
+
},
|
|
22
|
+
"exclude": ["node_modules", "dist"],
|
|
23
|
+
"include": ["src/**/*", "vitest.d.ts"],
|
|
24
|
+
"ts-node": {
|
|
25
|
+
"esm": true,
|
|
26
|
+
"require": ["dotenv/config", "./ensure-env.cjs"]
|
|
27
|
+
}
|
|
28
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
splitting: false,
|
|
6
|
+
format: ['esm', 'cjs'],
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
keepNames: true,
|
|
9
|
+
dts: true,
|
|
10
|
+
platform: 'browser',
|
|
11
|
+
clean: true,
|
|
12
|
+
shims: true,
|
|
13
|
+
external: ['react', 'react-dom'],
|
|
14
|
+
bundle: true,
|
|
15
|
+
plugins: [],
|
|
16
|
+
})
|
package/vitest.config.ts
ADDED
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { beforeAll } from 'vitest'
|
|
2
|
+
import { setupClient } from './src/hooks/setupClient'
|
|
3
|
+
import { Client } from '@botpress/client'
|
|
4
|
+
|
|
5
|
+
beforeAll(async () => {
|
|
6
|
+
setupClient(
|
|
7
|
+
new Client({
|
|
8
|
+
apiUrl: process.env.CLOUD_API_ENDPOINT ?? 'https://api.botpress.dev',
|
|
9
|
+
botId: process.env.CLOUD_BOT_ID,
|
|
10
|
+
token: process.env.CLOUD_PAT
|
|
11
|
+
})
|
|
12
|
+
)
|
|
13
|
+
})
|