@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,51 @@
|
|
|
1
|
+
import json5 from 'json5'
|
|
2
|
+
import { expect } from 'vitest'
|
|
3
|
+
import { getCurrentTest } from 'vitest/suite'
|
|
4
|
+
|
|
5
|
+
import { asyncExpect } from '../utils/asyncAssertion'
|
|
6
|
+
import { Output } from '../utils/predictJson'
|
|
7
|
+
|
|
8
|
+
export type ExtendedPromise<T> = PromiseLike<Output<T>> & {
|
|
9
|
+
value: PromiseLike<T>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const toAssertion = <T>(promise: Promise<Output<T>>): ExtendedPromise<T> => {
|
|
13
|
+
return {
|
|
14
|
+
then: promise.then.bind(promise),
|
|
15
|
+
value: promise.then((value) => value.result)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const makeToMatchInlineSnapshot =
|
|
20
|
+
<T>(promise: Promise<Output<T>>) =>
|
|
21
|
+
async (expected?: string) => {
|
|
22
|
+
const stack = new Error().stack!.split('\n')[2]
|
|
23
|
+
const newStack = `
|
|
24
|
+
at __INLINE_SNAPSHOT__ (node:internal/process/task_queues:1:1)
|
|
25
|
+
at randomLine (node:internal/process/task_queues:1:1)
|
|
26
|
+
${stack}
|
|
27
|
+
`.trim()
|
|
28
|
+
|
|
29
|
+
const obj = json5.parse(expected ?? '""')
|
|
30
|
+
const expectation = asyncExpect(promise, (expect) => expect.toMatchObject(obj)).catch(() => {
|
|
31
|
+
// we swallow the error here, as we're going to throw a new one with the correct stack
|
|
32
|
+
// this is just to make vitest happy and show a nice error message
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
expect((await promise).result).toMatchObject(obj)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const newError = new Error()
|
|
39
|
+
newError.stack = newStack
|
|
40
|
+
|
|
41
|
+
expect.getState().snapshotState.match({
|
|
42
|
+
isInline: true,
|
|
43
|
+
received: (await promise).result,
|
|
44
|
+
testName: getCurrentTest()!.name,
|
|
45
|
+
error: newError,
|
|
46
|
+
inlineSnapshot: expected
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return expectation
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from '@botpress/sdk'
|
|
2
|
+
|
|
3
|
+
import { Context } from '../context'
|
|
4
|
+
import { asyncExpect } from '../utils/asyncAssertion'
|
|
5
|
+
import { Input, predictJson } from '../utils/predictJson'
|
|
6
|
+
import { makeToMatchInlineSnapshot, toAssertion } from './extension'
|
|
7
|
+
|
|
8
|
+
export type ExtractOptions<T, S> = {
|
|
9
|
+
description?: string
|
|
10
|
+
examples?: { value: T; extracted: S; reason: string }[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function extract<T extends Input, S extends z.AnyZodObject>(
|
|
14
|
+
value: T,
|
|
15
|
+
shape: S,
|
|
16
|
+
options?: ExtractOptions<T, z.infer<S>>
|
|
17
|
+
) {
|
|
18
|
+
const additionalMessage = options?.description
|
|
19
|
+
? `\nIn order to extract the right information, follow these instructions:\n${options.description}\n`
|
|
20
|
+
: ''
|
|
21
|
+
const promise = predictJson({
|
|
22
|
+
systemMessage:
|
|
23
|
+
'From the given input, extract the required information into the requested format.' + additionalMessage.trim(),
|
|
24
|
+
examples: options?.examples?.map(({ value, reason, extracted }) => ({
|
|
25
|
+
input: value,
|
|
26
|
+
output: { reason, result: extracted }
|
|
27
|
+
})),
|
|
28
|
+
outputSchema: shape,
|
|
29
|
+
model: Context.evaluatorModel,
|
|
30
|
+
input: value
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
...toAssertion(promise),
|
|
35
|
+
toBe: (expected: z.infer<S>) => asyncExpect(promise, (expect) => expect.toEqual(expected)),
|
|
36
|
+
toMatchObject: (expected: Partial<z.infer<S>>) => asyncExpect(promise, (expect) => expect.toMatchObject(expected)),
|
|
37
|
+
toMatchInlineSnapshot: makeToMatchInlineSnapshot(promise)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { literal, z } from '@botpress/sdk'
|
|
2
|
+
|
|
3
|
+
import { Context } from '../context'
|
|
4
|
+
import { asyncExpect } from '../utils/asyncAssertion'
|
|
5
|
+
import { predictJson } from '../utils/predictJson'
|
|
6
|
+
import { makeToMatchInlineSnapshot, toAssertion } from './extension'
|
|
7
|
+
|
|
8
|
+
export type FilterOptions<T> = {
|
|
9
|
+
examples?: { value: T; reason: string; keep: boolean }[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function filter<U>(values: U[], condition: string, options?: FilterOptions<U>) {
|
|
13
|
+
const mappedValues = values.map((_, idx) =>
|
|
14
|
+
z.object({
|
|
15
|
+
index: literal(idx),
|
|
16
|
+
reason: z.string(),
|
|
17
|
+
keep: z.boolean()
|
|
18
|
+
})
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const input = values.map((value, idx) => ({
|
|
22
|
+
index: idx,
|
|
23
|
+
value
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
const schema = z
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
.tuple(mappedValues as any)
|
|
29
|
+
.describe(
|
|
30
|
+
'An array of the objects with the index and a boolean value indicating if the object should be kept or not'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const promise = predictJson({
|
|
34
|
+
systemMessage: `
|
|
35
|
+
Based on the following qualification criteria, you need to filter the given list of objects.
|
|
36
|
+
Citeria: ${condition}
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
You need to return an array of objects with the index and a boolean value indicating if the object should be kept or not.
|
|
40
|
+
`.trim(),
|
|
41
|
+
examples: options?.examples
|
|
42
|
+
? [
|
|
43
|
+
{
|
|
44
|
+
input: options?.examples?.map((v, index) => ({
|
|
45
|
+
index,
|
|
46
|
+
value: v.value
|
|
47
|
+
})),
|
|
48
|
+
output: {
|
|
49
|
+
reason: 'Here are some examples',
|
|
50
|
+
result: options?.examples?.map((v, idx) => ({
|
|
51
|
+
index: idx,
|
|
52
|
+
reason: v.reason,
|
|
53
|
+
keep: v.keep
|
|
54
|
+
}))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
: undefined,
|
|
59
|
+
outputSchema: schema,
|
|
60
|
+
model: Context.evaluatorModel,
|
|
61
|
+
input
|
|
62
|
+
}).then((x) => {
|
|
63
|
+
const results = schema.parse(x.result) as { index: number; keep: boolean }[]
|
|
64
|
+
return {
|
|
65
|
+
result: values.filter((_, idx) => results.find((r) => r.index === idx)?.keep),
|
|
66
|
+
reason: x.reason
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...toAssertion(promise),
|
|
72
|
+
toBe: (expected: U[]) => asyncExpect(promise, (expect) => expect.toEqual(expected)),
|
|
73
|
+
toMatchInlineSnapshot: makeToMatchInlineSnapshot(promise),
|
|
74
|
+
toHaveNoneFiltered: () => asyncExpect(promise, (expect) => expect.toEqual(values)),
|
|
75
|
+
toHaveSomeFiltered: () => asyncExpect(promise, (expect) => expect.not.toEqual(values)),
|
|
76
|
+
toBeEmpty: () => asyncExpect(promise, (expect) => expect.toHaveLength(0)),
|
|
77
|
+
length: {
|
|
78
|
+
toBe: (expected: number) => asyncExpect(promise, (expect) => expect.toHaveLength(expected)),
|
|
79
|
+
toBeGreaterThanOrEqual: (expected: number) =>
|
|
80
|
+
asyncExpect(promise, (expect) => expect.length.greaterThanOrEqual(expected)),
|
|
81
|
+
toBeLessThanOrEqual: (expected: number) =>
|
|
82
|
+
asyncExpect(promise, (expect) => expect.length.lessThanOrEqual(expected)),
|
|
83
|
+
toBeBetween: (min: number, max: number) => asyncExpect(promise, (expect) => expect.length.within(min, max))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from '@botpress/sdk'
|
|
2
|
+
|
|
3
|
+
import { Context } from '../context'
|
|
4
|
+
import { asyncExpect } from '../utils/asyncAssertion'
|
|
5
|
+
import { Input, predictJson } from '../utils/predictJson'
|
|
6
|
+
import { makeToMatchInlineSnapshot, toAssertion } from './extension'
|
|
7
|
+
|
|
8
|
+
export type RatingScore = 1 | 2 | 3 | 4 | 5
|
|
9
|
+
export type RateOptions<T> = {
|
|
10
|
+
examples?: { value: T; rating: number; reason: string }[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function rate<T extends Input>(value: T, condition: string, options?: RateOptions<T>) {
|
|
14
|
+
const schema = z.number().min(1).max(5).describe('Rating score, higher is better (1 is the worst, 5 is the best)')
|
|
15
|
+
const promise = predictJson({
|
|
16
|
+
systemMessage: `Based on the following qualification criteria, you need to rate the given situation from a score of 1 to 5.\nScoring: 1 is the worst score, 5 is the best score possible.\nCriteria: ${condition}`,
|
|
17
|
+
examples: options?.examples?.map(({ value, reason, rating }) => ({
|
|
18
|
+
input: value,
|
|
19
|
+
output: { reason, result: rating }
|
|
20
|
+
})),
|
|
21
|
+
outputSchema: schema,
|
|
22
|
+
model: Context.evaluatorModel,
|
|
23
|
+
input: value
|
|
24
|
+
}).then((x) => {
|
|
25
|
+
return {
|
|
26
|
+
result: typeof x.result === 'number' ? x.result : parseInt(x.result, 10),
|
|
27
|
+
reason: x.reason
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...toAssertion(promise),
|
|
33
|
+
toBe: (expected: number) => asyncExpect(promise, (expect) => expect.toEqual(expected)),
|
|
34
|
+
toMatchInlineSnapshot: makeToMatchInlineSnapshot(promise),
|
|
35
|
+
toBeGreaterThanOrEqual: (expected: RatingScore) =>
|
|
36
|
+
asyncExpect(promise, (expect) => expect.toBeGreaterThanOrEqual(expected)),
|
|
37
|
+
toBeLessThanOrEqual: (expected: RatingScore) =>
|
|
38
|
+
asyncExpect(promise, (expect) => expect.toBeLessThanOrEqual(expected))
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Client } from '@botpress/client'
|
|
2
|
+
import { onTestFinished } from 'vitest'
|
|
3
|
+
import { getCurrentTest } from 'vitest/suite'
|
|
4
|
+
import { Models } from './models'
|
|
5
|
+
|
|
6
|
+
export type EvaluatorModel = (typeof Models)[number]['id']
|
|
7
|
+
|
|
8
|
+
export type TestMetadata = {
|
|
9
|
+
isVaiTest: boolean
|
|
10
|
+
scenario?: string
|
|
11
|
+
evaluatorModel?: EvaluatorModel
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const getTestMetadata = (): TestMetadata => {
|
|
15
|
+
const test = getCurrentTest()
|
|
16
|
+
return (test?.meta ?? {
|
|
17
|
+
isVaiTest: false
|
|
18
|
+
}) as TestMetadata
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class VaiContext {
|
|
22
|
+
#client: Client | null = null
|
|
23
|
+
#wrapError = false
|
|
24
|
+
|
|
25
|
+
get wrapError() {
|
|
26
|
+
return this.#wrapError
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get client() {
|
|
30
|
+
if (!this.#client) {
|
|
31
|
+
throw new Error('Botpress client is not set')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.#client
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get evaluatorModel(): EvaluatorModel {
|
|
38
|
+
return getTestMetadata().evaluatorModel ?? 'openai__gpt-4o-mini-2024-07-18'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get scenario() {
|
|
42
|
+
return getTestMetadata().scenario
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get isVaiTest() {
|
|
46
|
+
return getTestMetadata().isVaiTest
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setClient(cognitive: Client) {
|
|
50
|
+
this.#client = cognitive
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
swallowErrors() {
|
|
54
|
+
if (!getCurrentTest()) {
|
|
55
|
+
throw new Error('cancelBail is a Vitest hook and must be called within a test')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#wrapError = true
|
|
59
|
+
onTestFinished(() => {
|
|
60
|
+
this.#wrapError = false
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const Context = new VaiContext()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getCurrentTest } from 'vitest/suite'
|
|
2
|
+
import { EvaluatorModel, TestMetadata } from '../context'
|
|
3
|
+
|
|
4
|
+
export const setEvaluator = (model: EvaluatorModel) => {
|
|
5
|
+
const test = getCurrentTest()
|
|
6
|
+
|
|
7
|
+
if (!test) {
|
|
8
|
+
throw new Error('setEvaluator is a Vitest hook and must be called within a test')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const meta = test.meta as TestMetadata
|
|
12
|
+
meta.evaluatorModel = model
|
|
13
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { compare } from './task/compare'
|
|
2
|
+
|
|
3
|
+
export { check } from './assertions/check'
|
|
4
|
+
export { extract } from './assertions/extract'
|
|
5
|
+
export { filter } from './assertions/filter'
|
|
6
|
+
export { rate } from './assertions/rate'
|
|
7
|
+
|
|
8
|
+
export { setEvaluator } from './hooks/setEvaluator'
|
|
9
|
+
export { setupClient } from './hooks/setupClient'
|