@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
package/.env
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Vitest AI
|
|
2
|
+
|
|
3
|
+
**Vai** (stands for _Vitest + AI_) is a lightweight vitest extension that uses LLMs to do assertions.
|
|
4
|
+
The goal of this library is primarily to allow testing the output of LLMs like the new autonomous engine, as the output is dynamic and qualitative we can't rely on traditional hard-coded tests.
|
|
5
|
+
|
|
6
|
+
To remove the flakiness and human-input from these tests, we need LLMs.
|
|
7
|
+
|
|
8
|
+
It's built on top of Zui and the Botpress client to interface with the different LLMs.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { check, rate, filter, extract } from '@botpress/vai'
|
|
14
|
+
import { describe, test } from 'vitest'
|
|
15
|
+
|
|
16
|
+
describe('my test suite', () => {
|
|
17
|
+
test('example', () => {
|
|
18
|
+
check('botpress', 'is a chatbot company').toBe(true)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## `check (assertion)`
|
|
24
|
+
|
|
25
|
+
Checks that the provided value matches the provided condition
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
test('example', () => {
|
|
29
|
+
// works with strings
|
|
30
|
+
check('hello', 'is a greeting message').toBe(true)
|
|
31
|
+
|
|
32
|
+
// also works with objects, arrays etc..
|
|
33
|
+
check(
|
|
34
|
+
{
|
|
35
|
+
message: 'hello my friend',
|
|
36
|
+
from: 'user'
|
|
37
|
+
},
|
|
38
|
+
'is a greeting message'
|
|
39
|
+
).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## `extract (assertion)`
|
|
44
|
+
|
|
45
|
+
Extracts from anything in input the requested Zui Schema:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
const person = z.object({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
age: z.number().optional(),
|
|
51
|
+
country: z.string().optional()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
extract('My name is Sylvain, I am 33 yo and live in Canada', person).toMatchObject({
|
|
55
|
+
name: 'Sylvain',
|
|
56
|
+
age: 33,
|
|
57
|
+
country: 'Canada'
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Also added support for `toMatchInlineSnapshot`:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
test('toMatchInlineSnapshot', () => {
|
|
65
|
+
extract('My name is Eric!', z.object({ name: z.string() })).toMatchInlineSnapshot(`
|
|
66
|
+
{
|
|
67
|
+
"name": "Eric",
|
|
68
|
+
}
|
|
69
|
+
`)
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## `filter (assertion)`
|
|
74
|
+
|
|
75
|
+
Filters an array of anything `T[]` based on a provided condition:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const countries = ['canada', 'germany', 'usa', 'paris', 'mexico']
|
|
79
|
+
filter(countries, 'is in north america').toBe(['canada', 'usa', 'mexico'])
|
|
80
|
+
filter(countries, 'is the name of a country').length.toBe(4)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## `rate (assertion)`
|
|
84
|
+
|
|
85
|
+
Given any input `T`, gives a rating between `1` (worst) and `5` (best):
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
test('good', () => rate('ghandi', 'is a good person').toBeGreaterThanOrEqual(4))
|
|
89
|
+
test('evil', () => rate('hitler', 'is a good person').toBe(3))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Few-shot Examples
|
|
93
|
+
|
|
94
|
+
All assertion methods accept examples to provide the LLM with few-shot learning and help increase the accuracy.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
describe('learns from examples', () => {
|
|
98
|
+
test('examples are used to understand how to classify correctly', () => {
|
|
99
|
+
const examples = [
|
|
100
|
+
{
|
|
101
|
+
expected: true,
|
|
102
|
+
value: 'Rasa the chatbot framework',
|
|
103
|
+
reason: 'Rasa is a chatbot framework, so it competes with Botpress'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
expected: false,
|
|
107
|
+
value: 'Rasa the coffee company',
|
|
108
|
+
reason: 'Since Rasa is a coffee company, it does not compete with Botpress which is not in the coffee business'
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
check('Voiceflow', 'is competitor', { examples }).toBe(true)
|
|
113
|
+
check('Toyota', 'is competitor', { examples }).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Failure Messages
|
|
119
|
+
|
|
120
|
+
All model predictions have nice failure messages by default:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const countries = ['canada', 'germany', 'usa', 'paris', 'mexico']
|
|
124
|
+
filter(countries, 'is in north america').toBe(['canada', 'usa'])
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Promises
|
|
128
|
+
|
|
129
|
+
All assertion methods can also be used outside Vitest tests, as they return an `PromiseLike<T>` object that can be awaited.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
test('test truth', async () => {
|
|
133
|
+
const { result } = await check('hello', 'is a greeting message')
|
|
134
|
+
expect(result).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Bail on failure
|
|
139
|
+
|
|
140
|
+
You can await the assertion to bail immediately on failure and prevent other test cases to run:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
test('no bail', () => {
|
|
144
|
+
check('hello', 'is a greeting message').toBe(false)
|
|
145
|
+
console.log('this will run as the above is not awaited, it will bail at the end of the test')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('bail', async () => {
|
|
149
|
+
await check('hello', 'is a greeting message').toBe(false)
|
|
150
|
+
console.log('this will not run, the test has bailed')
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Changing the evaluator model
|
|
155
|
+
|
|
156
|
+
By default, GPT-4o mini is used to evaluate the tests, but the evaluator can be changed from inside a test:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
test('simple', () => {
|
|
160
|
+
setEvaluator('anthropic__claude-3-5-sonnet-20240620')
|
|
161
|
+
rate('hello', 'is a greeting message').toBe(5)
|
|
162
|
+
})
|
|
163
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
10
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
11
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
12
|
+
var __typeError = (msg) => {
|
|
13
|
+
throw TypeError(msg);
|
|
14
|
+
};
|
|
15
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
16
|
+
var __spreadValues = (a, b) => {
|
|
17
|
+
for (var prop in b || (b = {}))
|
|
18
|
+
if (__hasOwnProp.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
if (__getOwnPropSymbols)
|
|
21
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
22
|
+
if (__propIsEnum.call(b, prop))
|
|
23
|
+
__defNormalProp(a, prop, b[prop]);
|
|
24
|
+
}
|
|
25
|
+
return a;
|
|
26
|
+
};
|
|
27
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
28
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
32
|
+
};
|
|
33
|
+
var __copyProps = (to, from, except, desc) => {
|
|
34
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
35
|
+
for (let key of __getOwnPropNames(from))
|
|
36
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
37
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
38
|
+
}
|
|
39
|
+
return to;
|
|
40
|
+
};
|
|
41
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
42
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
43
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
44
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
45
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
46
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
47
|
+
mod
|
|
48
|
+
));
|
|
49
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
50
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
51
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
52
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
53
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
54
|
+
|
|
55
|
+
// src/index.ts
|
|
56
|
+
var index_exports = {};
|
|
57
|
+
__export(index_exports, {
|
|
58
|
+
check: () => check,
|
|
59
|
+
compare: () => compare,
|
|
60
|
+
extract: () => extract,
|
|
61
|
+
filter: () => filter,
|
|
62
|
+
rate: () => rate,
|
|
63
|
+
setEvaluator: () => setEvaluator,
|
|
64
|
+
setupClient: () => setupClient
|
|
65
|
+
});
|
|
66
|
+
module.exports = __toCommonJS(index_exports);
|
|
67
|
+
|
|
68
|
+
// src/task/compare.ts
|
|
69
|
+
var import_sdk = require("@botpress/sdk");
|
|
70
|
+
var import_suite = require("vitest/suite");
|
|
71
|
+
|
|
72
|
+
// src/utils/deferred.ts
|
|
73
|
+
var _Deferred = class _Deferred {
|
|
74
|
+
constructor() {
|
|
75
|
+
this.promise = new Promise((resolve, reject) => {
|
|
76
|
+
this._resolve = resolve;
|
|
77
|
+
this._reject = reject;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
resolve(value) {
|
|
81
|
+
this._resolve(value);
|
|
82
|
+
}
|
|
83
|
+
reject(reason) {
|
|
84
|
+
this._reject(reason);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
__name(_Deferred, "Deferred");
|
|
88
|
+
var Deferred = _Deferred;
|
|
89
|
+
|
|
90
|
+
// src/task/compare.ts
|
|
91
|
+
var scenarioId = import_sdk.z.string().trim().min(1, "Scenario ID/name must not be empty").max(50, "Scenario ID/name is too long");
|
|
92
|
+
var ScenarioLike = import_sdk.z.union([
|
|
93
|
+
scenarioId,
|
|
94
|
+
import_sdk.z.object({ name: scenarioId }).passthrough(),
|
|
95
|
+
import_sdk.z.object({ id: scenarioId }).passthrough()
|
|
96
|
+
]);
|
|
97
|
+
var getScenarioName = /* @__PURE__ */ __name((scenario) => typeof scenario === "string" ? scenario : "name" in scenario ? scenario == null ? void 0 : scenario.name : scenario == null ? void 0 : scenario.id, "getScenarioName");
|
|
98
|
+
var scenarioArgs = import_sdk.z.array(ScenarioLike).min(2, "You need at least two scenarios to compare").max(10, "You can only compare up to 10 scenarios").refine((scenarios) => {
|
|
99
|
+
const set = /* @__PURE__ */ new Set();
|
|
100
|
+
scenarios.forEach((scenario) => set.add(getScenarioName(scenario)));
|
|
101
|
+
return set.size === scenarios.length;
|
|
102
|
+
}, "Scenarios names must be unique");
|
|
103
|
+
function compare(name, scenarios, fn) {
|
|
104
|
+
scenarios = scenarioArgs.parse(scenarios);
|
|
105
|
+
return (0, import_suite.createTaskCollector)((_name, fn2, timeout) => {
|
|
106
|
+
const currentSuite = (0, import_suite.getCurrentSuite)();
|
|
107
|
+
let completedCount = 0;
|
|
108
|
+
const finished = new Deferred();
|
|
109
|
+
for (const scenario of scenarios) {
|
|
110
|
+
const key = getScenarioName(scenario);
|
|
111
|
+
currentSuite.task(key, {
|
|
112
|
+
meta: {
|
|
113
|
+
scenario: key,
|
|
114
|
+
isVaiTest: true
|
|
115
|
+
},
|
|
116
|
+
handler: /* @__PURE__ */ __name(async (context) => {
|
|
117
|
+
const extendedContext = Object.freeze({
|
|
118
|
+
scenario
|
|
119
|
+
});
|
|
120
|
+
context.onTestFinished(() => {
|
|
121
|
+
if (++completedCount === scenarios.length) {
|
|
122
|
+
finished.resolve();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
await fn2(__spreadValues(__spreadValues({}, context), extendedContext));
|
|
126
|
+
}, "handler"),
|
|
127
|
+
timeout: timeout != null ? timeout : 1e4
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
})(name, fn);
|
|
131
|
+
}
|
|
132
|
+
__name(compare, "compare");
|
|
133
|
+
|
|
134
|
+
// src/assertions/check.ts
|
|
135
|
+
var import_sdk3 = require("@botpress/sdk");
|
|
136
|
+
|
|
137
|
+
// src/context.ts
|
|
138
|
+
var import_vitest = require("vitest");
|
|
139
|
+
var import_suite2 = require("vitest/suite");
|
|
140
|
+
var getTestMetadata = /* @__PURE__ */ __name(() => {
|
|
141
|
+
var _a;
|
|
142
|
+
const test = (0, import_suite2.getCurrentTest)();
|
|
143
|
+
return (_a = test == null ? void 0 : test.meta) != null ? _a : {
|
|
144
|
+
isVaiTest: false
|
|
145
|
+
};
|
|
146
|
+
}, "getTestMetadata");
|
|
147
|
+
var _client, _wrapError;
|
|
148
|
+
var _VaiContext = class _VaiContext {
|
|
149
|
+
constructor() {
|
|
150
|
+
__privateAdd(this, _client, null);
|
|
151
|
+
__privateAdd(this, _wrapError, false);
|
|
152
|
+
}
|
|
153
|
+
get wrapError() {
|
|
154
|
+
return __privateGet(this, _wrapError);
|
|
155
|
+
}
|
|
156
|
+
get client() {
|
|
157
|
+
if (!__privateGet(this, _client)) {
|
|
158
|
+
throw new Error("Botpress client is not set");
|
|
159
|
+
}
|
|
160
|
+
return __privateGet(this, _client);
|
|
161
|
+
}
|
|
162
|
+
get evaluatorModel() {
|
|
163
|
+
var _a;
|
|
164
|
+
return (_a = getTestMetadata().evaluatorModel) != null ? _a : "openai__gpt-4o-mini-2024-07-18";
|
|
165
|
+
}
|
|
166
|
+
get scenario() {
|
|
167
|
+
return getTestMetadata().scenario;
|
|
168
|
+
}
|
|
169
|
+
get isVaiTest() {
|
|
170
|
+
return getTestMetadata().isVaiTest;
|
|
171
|
+
}
|
|
172
|
+
setClient(cognitive) {
|
|
173
|
+
__privateSet(this, _client, cognitive);
|
|
174
|
+
}
|
|
175
|
+
swallowErrors() {
|
|
176
|
+
if (!(0, import_suite2.getCurrentTest)()) {
|
|
177
|
+
throw new Error("cancelBail is a Vitest hook and must be called within a test");
|
|
178
|
+
}
|
|
179
|
+
__privateSet(this, _wrapError, true);
|
|
180
|
+
(0, import_vitest.onTestFinished)(() => {
|
|
181
|
+
__privateSet(this, _wrapError, false);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
_client = new WeakMap();
|
|
186
|
+
_wrapError = new WeakMap();
|
|
187
|
+
__name(_VaiContext, "VaiContext");
|
|
188
|
+
var VaiContext = _VaiContext;
|
|
189
|
+
var Context = new VaiContext();
|
|
190
|
+
|
|
191
|
+
// src/utils/asyncAssertion.ts
|
|
192
|
+
var import_vitest2 = require("vitest");
|
|
193
|
+
var import_suite3 = require("vitest/suite");
|
|
194
|
+
var _AsyncExpectError = class _AsyncExpectError extends Error {
|
|
195
|
+
constructor(message, output) {
|
|
196
|
+
super(message);
|
|
197
|
+
this.output = output;
|
|
198
|
+
this.name = "AsyncExpectError";
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
__name(_AsyncExpectError, "AsyncExpectError");
|
|
202
|
+
var AsyncExpectError = _AsyncExpectError;
|
|
203
|
+
var getErrorMessages = /* @__PURE__ */ __name((e) => {
|
|
204
|
+
if (e instanceof Error) {
|
|
205
|
+
return e.message;
|
|
206
|
+
} else if (typeof e === "string") {
|
|
207
|
+
return e;
|
|
208
|
+
} else if (typeof e === "object" && e !== null) {
|
|
209
|
+
return JSON.stringify(e);
|
|
210
|
+
}
|
|
211
|
+
return `Unknown error: ${e}`;
|
|
212
|
+
}, "getErrorMessages");
|
|
213
|
+
var asyncExpect = /* @__PURE__ */ __name((output, assertion) => {
|
|
214
|
+
var _a, _b;
|
|
215
|
+
const promise = output.then((x) => {
|
|
216
|
+
try {
|
|
217
|
+
assertion((0, import_vitest2.expect)(x.result, x.reason));
|
|
218
|
+
} catch (e) {
|
|
219
|
+
if (Context.wrapError) {
|
|
220
|
+
return new AsyncExpectError(getErrorMessages(e), x);
|
|
221
|
+
}
|
|
222
|
+
throw e;
|
|
223
|
+
}
|
|
224
|
+
return x;
|
|
225
|
+
});
|
|
226
|
+
(_b = (_a = (0, import_suite3.getCurrentTest)()).promises) != null ? _b : _a.promises = [];
|
|
227
|
+
(0, import_suite3.getCurrentTest)().promises.push(promise);
|
|
228
|
+
return promise;
|
|
229
|
+
}, "asyncExpect");
|
|
230
|
+
|
|
231
|
+
// src/utils/predictJson.ts
|
|
232
|
+
var import_sdk2 = require("@botpress/sdk");
|
|
233
|
+
var import_json5 = __toESM(require("json5"), 1);
|
|
234
|
+
var nonEmptyString = import_sdk2.z.string().trim().min(1);
|
|
235
|
+
var nonEmptyObject = import_sdk2.z.object({}).passthrough().refine((value) => Object.keys(value).length > 0, {
|
|
236
|
+
message: "Expected a non-empty object"
|
|
237
|
+
});
|
|
238
|
+
var Input = nonEmptyString.or(nonEmptyObject).or(import_sdk2.z.array(import_sdk2.z.any()));
|
|
239
|
+
var Output = import_sdk2.z.object({
|
|
240
|
+
reason: nonEmptyString.describe("A human-readable explanation of the result"),
|
|
241
|
+
result: import_sdk2.z.any().describe(
|
|
242
|
+
"Your best guess at the output according to the instructions provided, rooted in the context of the input and the reason above"
|
|
243
|
+
)
|
|
244
|
+
});
|
|
245
|
+
var Example = import_sdk2.z.object({
|
|
246
|
+
input: Input,
|
|
247
|
+
output: Output
|
|
248
|
+
});
|
|
249
|
+
var Options = import_sdk2.z.object({
|
|
250
|
+
systemMessage: import_sdk2.z.string(),
|
|
251
|
+
examples: import_sdk2.z.array(Example).default([]),
|
|
252
|
+
input: Input,
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
254
|
+
outputSchema: import_sdk2.z.custom((value) => value instanceof import_sdk2.ZodSchema),
|
|
255
|
+
model: import_sdk2.z.string()
|
|
256
|
+
});
|
|
257
|
+
var isValidExample = /* @__PURE__ */ __name((outputSchema) => (example) => Input.safeParse(example.input).success && Output.safeParse(example.output).success && outputSchema.safeParse(example.output.result).success, "isValidExample");
|
|
258
|
+
async function predictJson(_options) {
|
|
259
|
+
var _a, _b;
|
|
260
|
+
const options = Options.parse(_options);
|
|
261
|
+
const [integration, model] = options.model.split("__");
|
|
262
|
+
if (!(model == null ? void 0 : model.length)) {
|
|
263
|
+
throw new Error("Invalid model");
|
|
264
|
+
}
|
|
265
|
+
const exampleMessages = options.examples.filter(isValidExample(options.outputSchema)).flatMap(({ input, output: output2 }) => [
|
|
266
|
+
{ role: "user", content: JSON.stringify(input, null, 2) },
|
|
267
|
+
{ role: "assistant", content: JSON.stringify(output2, null, 2) }
|
|
268
|
+
]);
|
|
269
|
+
const outputSchema = Output.extend({
|
|
270
|
+
result: options.outputSchema.describe(Output.shape.result.description)
|
|
271
|
+
});
|
|
272
|
+
const result = await Context.client.callAction({
|
|
273
|
+
type: `${integration}:generateContent`,
|
|
274
|
+
input: {
|
|
275
|
+
systemPrompt: `
|
|
276
|
+
${options.systemMessage}
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
Please generate a JSON response with the following format:
|
|
280
|
+
\`\`\`typescript
|
|
281
|
+
${await outputSchema.toTypescriptAsync()}
|
|
282
|
+
\`\`\`
|
|
283
|
+
`.trim(),
|
|
284
|
+
messages: [
|
|
285
|
+
...exampleMessages,
|
|
286
|
+
{
|
|
287
|
+
role: "user",
|
|
288
|
+
content: JSON.stringify(options.input, null, 2)
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
temperature: 0,
|
|
292
|
+
responseFormat: "json_object",
|
|
293
|
+
model: { id: model }
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
const output = result.output;
|
|
297
|
+
if (!output.choices.length || typeof ((_b = (_a = output.choices) == null ? void 0 : _a[0]) == null ? void 0 : _b.content) !== "string") {
|
|
298
|
+
throw new Error("Invalid response from the model");
|
|
299
|
+
}
|
|
300
|
+
const json = output.choices[0].content.trim();
|
|
301
|
+
if (!json.length) {
|
|
302
|
+
throw new Error("No response from the model");
|
|
303
|
+
}
|
|
304
|
+
return outputSchema.parse(import_json5.default.parse(json));
|
|
305
|
+
}
|
|
306
|
+
__name(predictJson, "predictJson");
|
|
307
|
+
|
|
308
|
+
// src/assertions/extension.ts
|
|
309
|
+
var import_json52 = __toESM(require("json5"), 1);
|
|
310
|
+
var import_vitest3 = require("vitest");
|
|
311
|
+
var import_suite4 = require("vitest/suite");
|
|
312
|
+
var toAssertion = /* @__PURE__ */ __name((promise) => {
|
|
313
|
+
return {
|
|
314
|
+
then: promise.then.bind(promise),
|
|
315
|
+
value: promise.then((value) => value.result)
|
|
316
|
+
};
|
|
317
|
+
}, "toAssertion");
|
|
318
|
+
var makeToMatchInlineSnapshot = /* @__PURE__ */ __name((promise) => async (expected) => {
|
|
319
|
+
const stack = new Error().stack.split("\n")[2];
|
|
320
|
+
const newStack = `
|
|
321
|
+
at __INLINE_SNAPSHOT__ (node:internal/process/task_queues:1:1)
|
|
322
|
+
at randomLine (node:internal/process/task_queues:1:1)
|
|
323
|
+
${stack}
|
|
324
|
+
`.trim();
|
|
325
|
+
const obj = import_json52.default.parse(expected != null ? expected : '""');
|
|
326
|
+
const expectation = asyncExpect(promise, (expect3) => expect3.toMatchObject(obj)).catch(() => {
|
|
327
|
+
});
|
|
328
|
+
try {
|
|
329
|
+
(0, import_vitest3.expect)((await promise).result).toMatchObject(obj);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const newError = new Error();
|
|
332
|
+
newError.stack = newStack;
|
|
333
|
+
import_vitest3.expect.getState().snapshotState.match({
|
|
334
|
+
isInline: true,
|
|
335
|
+
received: (await promise).result,
|
|
336
|
+
testName: (0, import_suite4.getCurrentTest)().name,
|
|
337
|
+
error: newError,
|
|
338
|
+
inlineSnapshot: expected
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return expectation;
|
|
342
|
+
}, "makeToMatchInlineSnapshot");
|
|
343
|
+
|
|
344
|
+
// src/assertions/check.ts
|
|
345
|
+
function check(value, condition, options) {
|
|
346
|
+
var _a;
|
|
347
|
+
const promise = predictJson({
|
|
348
|
+
systemMessage: `Check that the value meets the condition: ${condition}`,
|
|
349
|
+
examples: (_a = options == null ? void 0 : options.examples) == null ? void 0 : _a.map(({ value: value2, reason, expected }) => ({
|
|
350
|
+
input: value2,
|
|
351
|
+
output: { reason, result: expected }
|
|
352
|
+
})),
|
|
353
|
+
outputSchema: import_sdk3.z.boolean(),
|
|
354
|
+
model: Context.evaluatorModel,
|
|
355
|
+
input: value
|
|
356
|
+
});
|
|
357
|
+
return __spreadProps(__spreadValues({}, toAssertion(promise)), {
|
|
358
|
+
toBe: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toEqual(expected)), "toBe"),
|
|
359
|
+
toMatchInlineSnapshot: makeToMatchInlineSnapshot(promise)
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
__name(check, "check");
|
|
363
|
+
|
|
364
|
+
// src/assertions/extract.ts
|
|
365
|
+
function extract(value, shape, options) {
|
|
366
|
+
var _a;
|
|
367
|
+
const additionalMessage = (options == null ? void 0 : options.description) ? `
|
|
368
|
+
In order to extract the right information, follow these instructions:
|
|
369
|
+
${options.description}
|
|
370
|
+
` : "";
|
|
371
|
+
const promise = predictJson({
|
|
372
|
+
systemMessage: "From the given input, extract the required information into the requested format." + additionalMessage.trim(),
|
|
373
|
+
examples: (_a = options == null ? void 0 : options.examples) == null ? void 0 : _a.map(({ value: value2, reason, extracted }) => ({
|
|
374
|
+
input: value2,
|
|
375
|
+
output: { reason, result: extracted }
|
|
376
|
+
})),
|
|
377
|
+
outputSchema: shape,
|
|
378
|
+
model: Context.evaluatorModel,
|
|
379
|
+
input: value
|
|
380
|
+
});
|
|
381
|
+
return __spreadProps(__spreadValues({}, toAssertion(promise)), {
|
|
382
|
+
toBe: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toEqual(expected)), "toBe"),
|
|
383
|
+
toMatchObject: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toMatchObject(expected)), "toMatchObject"),
|
|
384
|
+
toMatchInlineSnapshot: makeToMatchInlineSnapshot(promise)
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
__name(extract, "extract");
|
|
388
|
+
|
|
389
|
+
// src/assertions/filter.ts
|
|
390
|
+
var import_sdk4 = require("@botpress/sdk");
|
|
391
|
+
function filter(values, condition, options) {
|
|
392
|
+
var _a, _b;
|
|
393
|
+
const mappedValues = values.map(
|
|
394
|
+
(_, idx) => import_sdk4.z.object({
|
|
395
|
+
index: (0, import_sdk4.literal)(idx),
|
|
396
|
+
reason: import_sdk4.z.string(),
|
|
397
|
+
keep: import_sdk4.z.boolean()
|
|
398
|
+
})
|
|
399
|
+
);
|
|
400
|
+
const input = values.map((value, idx) => ({
|
|
401
|
+
index: idx,
|
|
402
|
+
value
|
|
403
|
+
}));
|
|
404
|
+
const schema = import_sdk4.z.tuple(mappedValues).describe(
|
|
405
|
+
"An array of the objects with the index and a boolean value indicating if the object should be kept or not"
|
|
406
|
+
);
|
|
407
|
+
const promise = predictJson({
|
|
408
|
+
systemMessage: `
|
|
409
|
+
Based on the following qualification criteria, you need to filter the given list of objects.
|
|
410
|
+
Citeria: ${condition}
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
You need to return an array of objects with the index and a boolean value indicating if the object should be kept or not.
|
|
414
|
+
`.trim(),
|
|
415
|
+
examples: (options == null ? void 0 : options.examples) ? [
|
|
416
|
+
{
|
|
417
|
+
input: (_a = options == null ? void 0 : options.examples) == null ? void 0 : _a.map((v, index) => ({
|
|
418
|
+
index,
|
|
419
|
+
value: v.value
|
|
420
|
+
})),
|
|
421
|
+
output: {
|
|
422
|
+
reason: "Here are some examples",
|
|
423
|
+
result: (_b = options == null ? void 0 : options.examples) == null ? void 0 : _b.map((v, idx) => ({
|
|
424
|
+
index: idx,
|
|
425
|
+
reason: v.reason,
|
|
426
|
+
keep: v.keep
|
|
427
|
+
}))
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
] : void 0,
|
|
431
|
+
outputSchema: schema,
|
|
432
|
+
model: Context.evaluatorModel,
|
|
433
|
+
input
|
|
434
|
+
}).then((x) => {
|
|
435
|
+
const results = schema.parse(x.result);
|
|
436
|
+
return {
|
|
437
|
+
result: values.filter((_, idx) => {
|
|
438
|
+
var _a2;
|
|
439
|
+
return (_a2 = results.find((r) => r.index === idx)) == null ? void 0 : _a2.keep;
|
|
440
|
+
}),
|
|
441
|
+
reason: x.reason
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
return __spreadProps(__spreadValues({}, toAssertion(promise)), {
|
|
445
|
+
toBe: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toEqual(expected)), "toBe"),
|
|
446
|
+
toMatchInlineSnapshot: makeToMatchInlineSnapshot(promise),
|
|
447
|
+
toHaveNoneFiltered: /* @__PURE__ */ __name(() => asyncExpect(promise, (expect3) => expect3.toEqual(values)), "toHaveNoneFiltered"),
|
|
448
|
+
toHaveSomeFiltered: /* @__PURE__ */ __name(() => asyncExpect(promise, (expect3) => expect3.not.toEqual(values)), "toHaveSomeFiltered"),
|
|
449
|
+
toBeEmpty: /* @__PURE__ */ __name(() => asyncExpect(promise, (expect3) => expect3.toHaveLength(0)), "toBeEmpty"),
|
|
450
|
+
length: {
|
|
451
|
+
toBe: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toHaveLength(expected)), "toBe"),
|
|
452
|
+
toBeGreaterThanOrEqual: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.length.greaterThanOrEqual(expected)), "toBeGreaterThanOrEqual"),
|
|
453
|
+
toBeLessThanOrEqual: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.length.lessThanOrEqual(expected)), "toBeLessThanOrEqual"),
|
|
454
|
+
toBeBetween: /* @__PURE__ */ __name((min, max) => asyncExpect(promise, (expect3) => expect3.length.within(min, max)), "toBeBetween")
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
__name(filter, "filter");
|
|
459
|
+
|
|
460
|
+
// src/assertions/rate.ts
|
|
461
|
+
var import_sdk5 = require("@botpress/sdk");
|
|
462
|
+
function rate(value, condition, options) {
|
|
463
|
+
var _a;
|
|
464
|
+
const schema = import_sdk5.z.number().min(1).max(5).describe("Rating score, higher is better (1 is the worst, 5 is the best)");
|
|
465
|
+
const promise = predictJson({
|
|
466
|
+
systemMessage: `Based on the following qualification criteria, you need to rate the given situation from a score of 1 to 5.
|
|
467
|
+
Scoring: 1 is the worst score, 5 is the best score possible.
|
|
468
|
+
Criteria: ${condition}`,
|
|
469
|
+
examples: (_a = options == null ? void 0 : options.examples) == null ? void 0 : _a.map(({ value: value2, reason, rating }) => ({
|
|
470
|
+
input: value2,
|
|
471
|
+
output: { reason, result: rating }
|
|
472
|
+
})),
|
|
473
|
+
outputSchema: schema,
|
|
474
|
+
model: Context.evaluatorModel,
|
|
475
|
+
input: value
|
|
476
|
+
}).then((x) => {
|
|
477
|
+
return {
|
|
478
|
+
result: typeof x.result === "number" ? x.result : parseInt(x.result, 10),
|
|
479
|
+
reason: x.reason
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
return __spreadProps(__spreadValues({}, toAssertion(promise)), {
|
|
483
|
+
toBe: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toEqual(expected)), "toBe"),
|
|
484
|
+
toMatchInlineSnapshot: makeToMatchInlineSnapshot(promise),
|
|
485
|
+
toBeGreaterThanOrEqual: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toBeGreaterThanOrEqual(expected)), "toBeGreaterThanOrEqual"),
|
|
486
|
+
toBeLessThanOrEqual: /* @__PURE__ */ __name((expected) => asyncExpect(promise, (expect3) => expect3.toBeLessThanOrEqual(expected)), "toBeLessThanOrEqual")
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
__name(rate, "rate");
|
|
490
|
+
|
|
491
|
+
// src/hooks/setEvaluator.ts
|
|
492
|
+
var import_suite5 = require("vitest/suite");
|
|
493
|
+
var setEvaluator = /* @__PURE__ */ __name((model) => {
|
|
494
|
+
const test = (0, import_suite5.getCurrentTest)();
|
|
495
|
+
if (!test) {
|
|
496
|
+
throw new Error("setEvaluator is a Vitest hook and must be called within a test");
|
|
497
|
+
}
|
|
498
|
+
const meta = test.meta;
|
|
499
|
+
meta.evaluatorModel = model;
|
|
500
|
+
}, "setEvaluator");
|
|
501
|
+
|
|
502
|
+
// src/hooks/setupClient.ts
|
|
503
|
+
var setupClient = /* @__PURE__ */ __name((client) => {
|
|
504
|
+
Context.setClient(client);
|
|
505
|
+
}, "setupClient");
|
|
506
|
+
//# sourceMappingURL=index.cjs.map
|