vitest-evals 0.2.0 → 0.4.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.
- package/README.md +211 -172
- package/dist/index.d.mts +2 -98
- package/dist/index.d.ts +2 -98
- package/dist/index.js +270 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +269 -11
- package/dist/index.mjs.map +1 -1
- package/dist/scorers/index.d.mts +2 -0
- package/dist/scorers/index.d.ts +2 -0
- package/dist/scorers/index.js +282 -0
- package/dist/scorers/index.js.map +1 -0
- package/dist/scorers/index.mjs +256 -0
- package/dist/scorers/index.mjs.map +1 -0
- package/dist/scorers/toolCallScorer.d.mts +240 -0
- package/dist/scorers/toolCallScorer.d.ts +240 -0
- package/dist/scorers/toolCallScorer.js +280 -0
- package/dist/scorers/toolCallScorer.js.map +1 -0
- package/dist/scorers/toolCallScorer.mjs +256 -0
- package/dist/scorers/toolCallScorer.mjs.map +1 -0
- package/package.json +16 -4
- package/dist/compatibility.test.d.mts +0 -2
- package/dist/compatibility.test.d.ts +0 -2
- package/dist/compatibility.test.js +0 -45009
- package/dist/compatibility.test.js.map +0 -1
- package/dist/compatibility.test.mjs +0 -45864
- package/dist/compatibility.test.mjs.map +0 -1
- package/dist/formatScores.test.d.mts +0 -2
- package/dist/formatScores.test.d.ts +0 -2
- package/dist/formatScores.test.js +0 -195
- package/dist/formatScores.test.js.map +0 -1
- package/dist/formatScores.test.mjs +0 -194
- package/dist/formatScores.test.mjs.map +0 -1
- package/dist/wrapText.test.d.mts +0 -2
- package/dist/wrapText.test.d.ts +0 -2
- package/dist/wrapText.test.js +0 -162
- package/dist/wrapText.test.js.map +0 -1
- package/dist/wrapText.test.mjs +0 -161
- package/dist/wrapText.test.mjs.map +0 -1
package/dist/wrapText.test.mjs
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __defProps = Object.defineProperties;
|
|
3
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
-
var __spreadValues = (a, b) => {
|
|
9
|
-
for (var prop in b || (b = {}))
|
|
10
|
-
if (__hasOwnProp.call(b, prop))
|
|
11
|
-
__defNormalProp(a, prop, b[prop]);
|
|
12
|
-
if (__getOwnPropSymbols)
|
|
13
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
-
if (__propIsEnum.call(b, prop))
|
|
15
|
-
__defNormalProp(a, prop, b[prop]);
|
|
16
|
-
}
|
|
17
|
-
return a;
|
|
18
|
-
};
|
|
19
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
-
var __async = (__this, __arguments, generator) => {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
var fulfilled = (value) => {
|
|
23
|
-
try {
|
|
24
|
-
step(generator.next(value));
|
|
25
|
-
} catch (e) {
|
|
26
|
-
reject(e);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
var rejected = (value) => {
|
|
30
|
-
try {
|
|
31
|
-
step(generator.throw(value));
|
|
32
|
-
} catch (e) {
|
|
33
|
-
reject(e);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
37
|
-
step((generator = generator.apply(__this, __arguments)).next());
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// src/wrapText.test.ts
|
|
42
|
-
import { describe as describe2, expect as expect2, test as test2 } from "vitest";
|
|
43
|
-
|
|
44
|
-
// src/index.ts
|
|
45
|
-
import { assert, describe, expect, test } from "vitest";
|
|
46
|
-
import "vitest";
|
|
47
|
-
expect.extend({
|
|
48
|
-
/**
|
|
49
|
-
* Evaluates a language model output against an expected answer using a scoring function.
|
|
50
|
-
*
|
|
51
|
-
* @param expected - The expected (ground truth) answer
|
|
52
|
-
* @param taskFn - Async function that processes the input and returns the model output
|
|
53
|
-
* @param scoreFn - Function that evaluates the model output against the expected answer
|
|
54
|
-
* @param threshold - Minimum acceptable score (0-1), defaults to 1.0
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```javascript
|
|
58
|
-
* test("checks capital of France", async () => {
|
|
59
|
-
* expect("What is the capital of France?").toEval(
|
|
60
|
-
* "Paris",
|
|
61
|
-
* async (input) => {
|
|
62
|
-
* // Query LLM here
|
|
63
|
-
* return "Paris";
|
|
64
|
-
* },
|
|
65
|
-
* checkFactuality,
|
|
66
|
-
* 0.8
|
|
67
|
-
* );
|
|
68
|
-
* });
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
toEval: function toEval(input, expected, taskFn, scoreFn, threshold = 1) {
|
|
72
|
-
return __async(this, null, function* () {
|
|
73
|
-
var _a;
|
|
74
|
-
const { isNot } = this;
|
|
75
|
-
const output = yield taskFn(input);
|
|
76
|
-
let result = scoreFn({ input, expected, output });
|
|
77
|
-
if (result instanceof Promise) {
|
|
78
|
-
result = yield result;
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
pass: ((_a = result.score) != null ? _a : 0) >= threshold,
|
|
82
|
-
message: () => formatScores([__spreadProps(__spreadValues({}, result), { name: scoreFn.name })])
|
|
83
|
-
};
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
function formatScores(scores) {
|
|
88
|
-
return scores.sort((a, b) => {
|
|
89
|
-
var _a, _b;
|
|
90
|
-
return ((_a = a.score) != null ? _a : 0) - ((_b = b.score) != null ? _b : 0);
|
|
91
|
-
}).map((s) => {
|
|
92
|
-
var _a, _b, _c, _d, _e, _f;
|
|
93
|
-
const scoreLine = `# ${s.name || "Unknown"} [${((_a = s.score) != null ? _a : 0).toFixed(1)}]`;
|
|
94
|
-
if (((_b = s.score) != null ? _b : 0) < 1 && ((_c = s.metadata) == null ? void 0 : _c.rationale) || ((_d = s.metadata) == null ? void 0 : _d.output)) {
|
|
95
|
-
return `${scoreLine}${((_e = s.metadata) == null ? void 0 : _e.rationale) ? `
|
|
96
|
-
|
|
97
|
-
## Rationale
|
|
98
|
-
|
|
99
|
-
${wrapText(s.metadata.rationale)}` : ""}${((_f = s.metadata) == null ? void 0 : _f.output) ? `
|
|
100
|
-
|
|
101
|
-
## Response
|
|
102
|
-
|
|
103
|
-
${wrapText(s.metadata.output)}` : ""}`;
|
|
104
|
-
}
|
|
105
|
-
return scoreLine;
|
|
106
|
-
}).join("\n\n");
|
|
107
|
-
}
|
|
108
|
-
function wrapText(text, width = 80) {
|
|
109
|
-
if (!text || text.length <= width) {
|
|
110
|
-
return text;
|
|
111
|
-
}
|
|
112
|
-
const words = text.split(/\s+/);
|
|
113
|
-
const lines = [];
|
|
114
|
-
let currentLine = "";
|
|
115
|
-
for (const word of words) {
|
|
116
|
-
if (currentLine.length + word.length + 1 > width) {
|
|
117
|
-
lines.push(currentLine.trim());
|
|
118
|
-
currentLine = word;
|
|
119
|
-
} else {
|
|
120
|
-
currentLine += (currentLine ? " " : "") + word;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (currentLine) {
|
|
124
|
-
lines.push(currentLine);
|
|
125
|
-
}
|
|
126
|
-
return lines.join("\n");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// src/wrapText.test.ts
|
|
130
|
-
describe2("wrapText", () => {
|
|
131
|
-
test2("should return the original text if it's shorter than the width", () => {
|
|
132
|
-
const text = "This is a short text";
|
|
133
|
-
expect2(wrapText(text, 30)).toBe(text);
|
|
134
|
-
});
|
|
135
|
-
test2("should wrap text at word boundaries", () => {
|
|
136
|
-
const text = "This is a very long text that needs to be wrapped";
|
|
137
|
-
const expected = "This is a very\nlong text that\nneeds to be\nwrapped";
|
|
138
|
-
expect2(wrapText(text, 15)).toBe(expected);
|
|
139
|
-
});
|
|
140
|
-
test2("should handle empty string", () => {
|
|
141
|
-
expect2(wrapText("")).toBe("");
|
|
142
|
-
});
|
|
143
|
-
test2("should handle null or undefined", () => {
|
|
144
|
-
expect2(wrapText(null)).toBe(null);
|
|
145
|
-
expect2(wrapText(void 0)).toBe(void 0);
|
|
146
|
-
});
|
|
147
|
-
test2("should use default width of 80", () => {
|
|
148
|
-
const text = "This is a very long text that needs to be wrapped to fit within an 80 character width.";
|
|
149
|
-
const result = wrapText(text);
|
|
150
|
-
const lines = result.split("\n");
|
|
151
|
-
for (const line of lines) {
|
|
152
|
-
expect2(line.length).toBeLessThanOrEqual(80);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
test2("should handle text with multiple spaces", () => {
|
|
156
|
-
const text = "This has multiple spaces";
|
|
157
|
-
const expected = "This has\nmultiple\nspaces";
|
|
158
|
-
expect2(wrapText(text, 10)).toBe(expected);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
//# sourceMappingURL=wrapText.test.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/wrapText.test.ts","../src/index.ts"],"sourcesContent":["import { describe, expect, test } from \"vitest\";\nimport { wrapText } from \"./index\";\n\ndescribe(\"wrapText\", () => {\n test(\"should return the original text if it's shorter than the width\", () => {\n const text = \"This is a short text\";\n expect(wrapText(text, 30)).toBe(text);\n });\n\n test(\"should wrap text at word boundaries\", () => {\n const text = \"This is a very long text that needs to be wrapped\";\n const expected = \"This is a very\\nlong text that\\nneeds to be\\nwrapped\";\n expect(wrapText(text, 15)).toBe(expected);\n });\n\n test(\"should handle empty string\", () => {\n expect(wrapText(\"\")).toBe(\"\");\n });\n\n test(\"should handle null or undefined\", () => {\n expect(wrapText(null as any)).toBe(null);\n expect(wrapText(undefined as any)).toBe(undefined);\n });\n\n test(\"should use default width of 80\", () => {\n const text =\n \"This is a very long text that needs to be wrapped to fit within an 80 character width.\";\n const result = wrapText(text);\n const lines = result.split(\"\\n\");\n\n // Check that no line exceeds 80 characters\n for (const line of lines) {\n expect(line.length).toBeLessThanOrEqual(80);\n }\n });\n\n test(\"should handle text with multiple spaces\", () => {\n const text = \"This has multiple spaces\";\n const expected = \"This has\\nmultiple\\nspaces\";\n expect(wrapText(text, 10)).toBe(expected);\n });\n});\n","import { assert, describe, expect, test } from \"vitest\";\nimport \"vitest\";\n\nexport type TaskFn = (input: string) => Promise<string>;\n\nexport type Score = {\n score: number | null;\n metadata?: {\n rationale?: string;\n output?: string;\n };\n};\n\nexport type ScoreFn = (opts: {\n input: string;\n output: string;\n expected?: string;\n}) => Promise<Score> | Score;\n\nexport type ToEval<R = unknown> = (\n expected: string,\n taskFn: TaskFn,\n scoreFn: ScoreFn,\n threshold?: number,\n) => Promise<R>;\n\nexport interface EvalMatchers<R = unknown> {\n toEval: ToEval<R>;\n}\n\ndeclare module \"vitest\" {\n interface Assertion<T = any> extends EvalMatchers<T> {}\n interface AsymmetricMatchersContaining extends EvalMatchers {}\n\n interface TaskMeta {\n eval?: {\n scores: (Score & { name: string })[];\n avgScore: number;\n };\n }\n}\n\nexpect.extend({\n /**\n * Evaluates a language model output against an expected answer using a scoring function.\n *\n * @param expected - The expected (ground truth) answer\n * @param taskFn - Async function that processes the input and returns the model output\n * @param scoreFn - Function that evaluates the model output against the expected answer\n * @param threshold - Minimum acceptable score (0-1), defaults to 1.0\n *\n * @example\n * ```javascript\n * test(\"checks capital of France\", async () => {\n * expect(\"What is the capital of France?\").toEval(\n * \"Paris\",\n * async (input) => {\n * // Query LLM here\n * return \"Paris\";\n * },\n * checkFactuality,\n * 0.8\n * );\n * });\n * ```\n */\n toEval: async function toEval(\n input: string,\n expected: string,\n taskFn: TaskFn,\n scoreFn: ScoreFn,\n threshold = 1.0,\n ) {\n const { isNot } = this;\n\n const output = await taskFn(input);\n\n let result = scoreFn({ input, expected, output });\n if (result instanceof Promise) {\n result = await result;\n }\n\n return {\n pass: (result.score ?? 0) >= threshold,\n message: () => formatScores([{ ...result, name: scoreFn.name }]),\n };\n },\n});\n\n/**\n * Creates a test suite for evaluating language model outputs.\n *\n * @param name - The name of the test suite\n * @param options - Configuration options\n * @param options.data - Async function that returns an array of test cases with input and expected values\n * @param options.task - Function that processes the input and returns the model output\n * @param options.skipIf - Optional function that determines if tests should be skipped\n * @param options.scorers - Array of scoring functions that evaluate model outputs\n * @param options.threshold - Minimum acceptable average score (0-1), defaults to 1.0\n * @param options.timeout - Test timeout in milliseconds, defaults to 60000 (60s)\n *\n * @example\n * ```javascript\n * describeEval(\"capital cities test\", {\n * data: async () => [{\n * input: \"What is the capital of France?\",\n * expected: \"Paris\"\n * }],\n * task: async (input) => {\n * // Query LLM here\n * return \"Paris\";\n * },\n * scorers: [checkFactuality],\n * threshold: 0.8\n * });\n * ```\n */\nexport function describeEval(\n name: string,\n {\n data,\n task,\n skipIf,\n scorers,\n threshold = 1.0,\n // increase default test timeout as 5s is usually not enough for\n // a single factuality check\n timeout = 60000,\n }: {\n data: () => Promise<{ input: string; expected: string }[]>;\n task: TaskFn;\n skipIf?: () => boolean;\n scorers: ScoreFn[];\n threshold?: number | null;\n timeout?: number;\n },\n) {\n return describe(name, async () => {\n const testFn = skipIf ? test.skipIf(skipIf()) : test;\n // TODO: should data just be a generator?\n for (const { input, expected } of await data()) {\n testFn(\n input,\n {\n timeout,\n },\n async ({ task: testTask }) => {\n const output = await task(input);\n\n const scores = await Promise.all(\n scorers.map((scorer) => {\n const result = scorer({ input, expected, output });\n if (result instanceof Promise) {\n return result;\n }\n return new Promise<Score>((resolve) => resolve(result));\n }),\n );\n const scoresWithName = scores.map((s, i) => ({\n ...s,\n name: scorers[i].name,\n }));\n\n const avgScore =\n scores.reduce((acc, s) => acc + (s.score ?? 0), 0) / scores.length;\n\n testTask.meta.eval = {\n scores: scoresWithName,\n avgScore,\n };\n\n if (threshold) {\n assert(\n avgScore >= threshold,\n `Score: ${avgScore} below threshold: ${threshold}\\n\\n## Output:\\n${wrapText(output)}\\n\\n${formatScores(\n scoresWithName,\n )}`,\n );\n }\n },\n );\n }\n });\n}\n\nexport function formatScores(scores: (Score & { name: string })[]) {\n return scores\n .sort((a, b) => (a.score ?? 0) - (b.score ?? 0))\n .map((s) => {\n const scoreLine = `# ${s.name || \"Unknown\"} [${(s.score ?? 0).toFixed(1)}]`;\n if (\n ((s.score ?? 0) < 1.0 && s.metadata?.rationale) ||\n s.metadata?.output\n ) {\n return `${scoreLine}${\n s.metadata?.rationale\n ? `\\n\\n## Rationale\\n\\n${wrapText(s.metadata.rationale)}`\n : \"\"\n }${s.metadata?.output ? `\\n\\n## Response\\n\\n${wrapText(s.metadata.output)}` : \"\"}`;\n }\n return scoreLine;\n })\n .join(\"\\n\\n\");\n}\n\n/**\n * Wraps text to fit within a specified width, breaking at word boundaries.\n *\n * @param text - The text to wrap\n * @param width - The maximum width in characters (default: 80)\n * @returns The wrapped text with line breaks\n *\n * @example\n * ```javascript\n * const wrapped = wrapText(\"This is a very long text that needs to be wrapped to fit within an 80 character width.\", 20);\n * console.log(wrapped);\n * // Output:\n * // This is a very\n * // long text that\n * // needs to be\n * // wrapped to fit\n * // within an 80\n * // character width.\n * ```\n */\nexport function wrapText(text: string, width = 80): string {\n if (!text || text.length <= width) {\n return text;\n }\n\n const words = text.split(/\\s+/);\n const lines: string[] = [];\n let currentLine = \"\";\n\n for (const word of words) {\n // If adding this word would exceed the width, start a new line\n if (currentLine.length + word.length + 1 > width) {\n lines.push(currentLine.trim());\n currentLine = word;\n } else {\n // Add the word to the current line\n currentLine += (currentLine ? \" \" : \"\") + word;\n }\n }\n\n // Add the last line if it's not empty\n if (currentLine) {\n lines.push(currentLine);\n }\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["describe","expect","test","describe","test","expect"]}
|