@outputai/evals 0.1.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.
@@ -0,0 +1,181 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Verdict } from './verdict.js';
3
+ import { EvaluationBooleanResult, EvaluationVerdictResult, EvaluationNumberResult, EvaluationStringResult } from '@outputai/core';
4
+ describe('Verdict', () => {
5
+ // --- Original helpers regression ---
6
+ describe('pass', () => {
7
+ it('returns EvaluationVerdictResult with value "pass"', () => {
8
+ const r = Verdict.pass('looks good');
9
+ expect(r).toBeInstanceOf(EvaluationVerdictResult);
10
+ expect(r.value).toBe('pass');
11
+ expect(r.confidence).toBe(1.0);
12
+ expect(r.reasoning).toBe('looks good');
13
+ });
14
+ });
15
+ describe('partial', () => {
16
+ it('returns EvaluationVerdictResult with value "partial"', () => {
17
+ const r = Verdict.partial(0.7, 'mostly ok');
18
+ expect(r).toBeInstanceOf(EvaluationVerdictResult);
19
+ expect(r.value).toBe('partial');
20
+ expect(r.confidence).toBe(0.7);
21
+ });
22
+ });
23
+ describe('fail', () => {
24
+ it('returns EvaluationVerdictResult with value "fail"', () => {
25
+ const r = Verdict.fail('wrong');
26
+ expect(r).toBeInstanceOf(EvaluationVerdictResult);
27
+ expect(r.value).toBe('fail');
28
+ expect(r.confidence).toBe(0.0);
29
+ });
30
+ });
31
+ // --- Deterministic assertions ---
32
+ describe('equals', () => {
33
+ it('passes when values are strictly equal', () => {
34
+ const r = Verdict.equals(42, 42);
35
+ expect(r).toBeInstanceOf(EvaluationBooleanResult);
36
+ expect(r.value).toBe(true);
37
+ expect(r.confidence).toBe(1.0);
38
+ });
39
+ it('fails when values differ', () => {
40
+ const r = Verdict.equals(42, 43);
41
+ expect(r.value).toBe(false);
42
+ expect(r.confidence).toBe(1.0);
43
+ });
44
+ });
45
+ describe('closeTo', () => {
46
+ it('passes within tolerance', () => {
47
+ const r = Verdict.closeTo(3.14, 3.15, 0.02);
48
+ expect(r.value).toBe(true);
49
+ });
50
+ it('fails outside tolerance', () => {
51
+ const r = Verdict.closeTo(3.14, 3.20, 0.02);
52
+ expect(r.value).toBe(false);
53
+ });
54
+ });
55
+ describe('gt', () => {
56
+ it('passes when actual > threshold', () => {
57
+ expect(Verdict.gt(5, 3).value).toBe(true);
58
+ });
59
+ it('fails when actual <= threshold', () => {
60
+ expect(Verdict.gt(3, 3).value).toBe(false);
61
+ });
62
+ });
63
+ describe('gte', () => {
64
+ it('passes when actual >= threshold', () => {
65
+ expect(Verdict.gte(3, 3).value).toBe(true);
66
+ });
67
+ it('fails when actual < threshold', () => {
68
+ expect(Verdict.gte(2, 3).value).toBe(false);
69
+ });
70
+ });
71
+ describe('lt', () => {
72
+ it('passes when actual < threshold', () => {
73
+ expect(Verdict.lt(2, 3).value).toBe(true);
74
+ });
75
+ it('fails when actual >= threshold', () => {
76
+ expect(Verdict.lt(3, 3).value).toBe(false);
77
+ });
78
+ });
79
+ describe('lte', () => {
80
+ it('passes when actual <= threshold', () => {
81
+ expect(Verdict.lte(3, 3).value).toBe(true);
82
+ });
83
+ it('fails when actual > threshold', () => {
84
+ expect(Verdict.lte(4, 3).value).toBe(false);
85
+ });
86
+ });
87
+ describe('inRange', () => {
88
+ it('passes when value is in range', () => {
89
+ expect(Verdict.inRange(5, 1, 10).value).toBe(true);
90
+ });
91
+ it('passes at boundaries', () => {
92
+ expect(Verdict.inRange(1, 1, 10).value).toBe(true);
93
+ expect(Verdict.inRange(10, 1, 10).value).toBe(true);
94
+ });
95
+ it('fails when value is out of range', () => {
96
+ expect(Verdict.inRange(11, 1, 10).value).toBe(false);
97
+ });
98
+ });
99
+ describe('contains', () => {
100
+ it('passes when haystack includes needle', () => {
101
+ expect(Verdict.contains('hello world', 'world').value).toBe(true);
102
+ });
103
+ it('fails when haystack does not include needle', () => {
104
+ expect(Verdict.contains('hello world', 'xyz').value).toBe(false);
105
+ });
106
+ });
107
+ describe('matches', () => {
108
+ it('passes when value matches pattern', () => {
109
+ expect(Verdict.matches('abc123', /\d+/).value).toBe(true);
110
+ });
111
+ it('fails when value does not match', () => {
112
+ expect(Verdict.matches('abc', /\d+/).value).toBe(false);
113
+ });
114
+ });
115
+ describe('includesAll', () => {
116
+ it('passes when all expected values are present', () => {
117
+ expect(Verdict.includesAll([1, 2, 3], [1, 3]).value).toBe(true);
118
+ });
119
+ it('fails when some expected values are missing', () => {
120
+ const r = Verdict.includesAll([1, 2], [2, 3]);
121
+ expect(r.value).toBe(false);
122
+ expect(r.reasoning).toContain('3');
123
+ });
124
+ });
125
+ describe('includesAny', () => {
126
+ it('passes when at least one expected value is present', () => {
127
+ expect(Verdict.includesAny([1, 2, 3], [5, 2]).value).toBe(true);
128
+ });
129
+ it('fails when no expected values are present', () => {
130
+ expect(Verdict.includesAny([1, 2], [5, 6]).value).toBe(false);
131
+ });
132
+ });
133
+ describe('isTrue', () => {
134
+ it('passes for true', () => {
135
+ expect(Verdict.isTrue(true).value).toBe(true);
136
+ });
137
+ it('fails for false', () => {
138
+ expect(Verdict.isTrue(false).value).toBe(false);
139
+ });
140
+ });
141
+ describe('isFalse', () => {
142
+ it('passes for false', () => {
143
+ expect(Verdict.isFalse(false).value).toBe(true);
144
+ });
145
+ it('fails for true', () => {
146
+ expect(Verdict.isFalse(true).value).toBe(false);
147
+ });
148
+ });
149
+ // --- LLM judge helpers ---
150
+ describe('fromJudge', () => {
151
+ it('returns EvaluationVerdictResult with 0.9 confidence', () => {
152
+ const r = Verdict.fromJudge({ verdict: 'pass', reasoning: 'on topic' });
153
+ expect(r).toBeInstanceOf(EvaluationVerdictResult);
154
+ expect(r.value).toBe('pass');
155
+ expect(r.confidence).toBe(0.9);
156
+ expect(r.reasoning).toBe('on topic');
157
+ });
158
+ it('handles all verdict values', () => {
159
+ expect(Verdict.fromJudge({ verdict: 'partial', reasoning: 'ok' }).value).toBe('partial');
160
+ expect(Verdict.fromJudge({ verdict: 'fail', reasoning: 'bad' }).value).toBe('fail');
161
+ });
162
+ });
163
+ describe('score', () => {
164
+ it('returns EvaluationNumberResult with 0.9 confidence', () => {
165
+ const r = Verdict.score(0.85, 'good quality');
166
+ expect(r).toBeInstanceOf(EvaluationNumberResult);
167
+ expect(r.value).toBe(0.85);
168
+ expect(r.confidence).toBe(0.9);
169
+ expect(r.reasoning).toBe('good quality');
170
+ });
171
+ });
172
+ describe('label', () => {
173
+ it('returns EvaluationStringResult with 0.9 confidence', () => {
174
+ const r = Verdict.label('formal', 'professional writing');
175
+ expect(r).toBeInstanceOf(EvaluationStringResult);
176
+ expect(r.value).toBe('formal');
177
+ expect(r.confidence).toBe(0.9);
178
+ expect(r.reasoning).toBe('professional writing');
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,41 @@
1
+ import { z } from '@outputai/core';
2
+ import type { EvaluationResult } from '@outputai/core';
3
+ export type CheckContext<TInput = unknown, TOutput = unknown> = {
4
+ input: TInput;
5
+ output: TOutput;
6
+ context: {
7
+ ground_truth: Record<string, unknown>;
8
+ };
9
+ };
10
+ type CheckFn<TInput, TOutput> = (ctx: CheckContext<TInput, TOutput>) => EvaluationResult | Promise<EvaluationResult>;
11
+ type VerifyOptions<I extends z.ZodType = z.ZodType<unknown>, O extends z.ZodType = z.ZodType<unknown>> = {
12
+ name: string;
13
+ input?: I;
14
+ output?: O;
15
+ };
16
+ export declare const verify: <I extends z.ZodType, O extends z.ZodType>(options: VerifyOptions<I, O>, fn: CheckFn<z.infer<I>, z.infer<O>>) => (input: ({
17
+ input: z.ZodAny | I;
18
+ output: z.ZodAny | O;
19
+ ground_truth: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
20
+ } extends infer T_1 extends z.core.$ZodLooseShape ? { -readonly [k in keyof T_1 as {
21
+ input: z.ZodAny | I;
22
+ output: z.ZodAny | O;
23
+ ground_truth: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
24
+ }[k] extends {
25
+ _zod: {
26
+ optout: "optional";
27
+ };
28
+ } ? never : k]: T_1[k]["_zod"]["output"]; } : never) & ({
29
+ input: z.ZodAny | I;
30
+ output: z.ZodAny | O;
31
+ ground_truth: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
32
+ } extends infer T_2 extends z.core.$ZodLooseShape ? { -readonly [k_1 in keyof T_2 as {
33
+ input: z.ZodAny | I;
34
+ output: z.ZodAny | O;
35
+ ground_truth: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
36
+ }[k_1] extends {
37
+ _zod: {
38
+ optout: "optional";
39
+ };
40
+ } ? k_1 : never]?: T_2[k_1]["_zod"]["output"] | undefined; } : never) extends infer T ? { [K in keyof T]: T[K]; } : never) => Promise<EvaluationResult>;
41
+ export {};
package/dist/verify.js ADDED
@@ -0,0 +1,18 @@
1
+ import { evaluator, z } from '@outputai/core';
2
+ export const verify = (options, fn) => {
3
+ const inputSchema = z.object({
4
+ input: options.input ?? z.any(),
5
+ output: options.output ?? z.any(),
6
+ ground_truth: z.record(z.string(), z.unknown()).optional()
7
+ });
8
+ const wrappedFn = async (data) => {
9
+ const groundTruth = data.ground_truth ?? {};
10
+ return fn({ input: data.input, output: data.output, context: { ground_truth: groundTruth } });
11
+ };
12
+ return evaluator({
13
+ name: options.name,
14
+ description: options.name,
15
+ inputSchema,
16
+ fn: wrappedFn
17
+ });
18
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { verify } from './verify.js';
3
+ import { getMetadata } from '@outputai/core/sdk_utils';
4
+ import { EvaluationBooleanResult, z } from '@outputai/core';
5
+ import { Verdict } from './verdict.js';
6
+ describe('verify', () => {
7
+ it('returns a function with metadata matching the given name', () => {
8
+ const ev = verify({ name: 'my_eval' }, () => Verdict.isTrue(true));
9
+ const meta = getMetadata(ev);
10
+ expect(meta).not.toBeNull();
11
+ expect(meta.name).toBe('my_eval');
12
+ expect(meta.description).toBe('my_eval');
13
+ });
14
+ it('passes input and output to the user function', async () => {
15
+ const captured = [];
16
+ const ev = verify({ name: 'capture_test' }, ctx => {
17
+ captured.push(ctx);
18
+ return Verdict.isTrue(true);
19
+ });
20
+ await ev({
21
+ input: { values: [1, 2] },
22
+ output: { result: 3 },
23
+ ground_truth: { key: 'val' }
24
+ });
25
+ expect(captured).toHaveLength(1);
26
+ expect(captured[0].input).toEqual({ values: [1, 2] });
27
+ expect(captured[0].output).toEqual({ result: 3 });
28
+ expect(captured[0].context.ground_truth).toEqual({ key: 'val' });
29
+ });
30
+ it('defaults ground_truth to empty object when undefined', async () => {
31
+ const captured = [];
32
+ const ev = verify({ name: 'default_ground_truth_test' }, ({ context }) => {
33
+ captured.push(context.ground_truth);
34
+ return Verdict.isTrue(true);
35
+ });
36
+ await ev({ input: {}, output: {} });
37
+ expect(captured[0]).toEqual({});
38
+ });
39
+ it('returns EvaluationResult from user fn as-is', async () => {
40
+ const ev = verify({
41
+ name: 'return_test',
42
+ input: z.object({ x: z.number() }),
43
+ output: z.object({ result: z.number() })
44
+ }, ({ input, output }) => Verdict.equals(output.result, input.x));
45
+ const result = await ev({ input: { x: 5 }, output: { result: 5 } });
46
+ expect(result).toBeInstanceOf(EvaluationBooleanResult);
47
+ expect(result.value).toBe(true);
48
+ expect(result.confidence).toBe(1.0);
49
+ });
50
+ it('returns failing result when assertion fails', async () => {
51
+ const ev = verify({
52
+ name: 'fail_test',
53
+ input: z.object({ x: z.number() }),
54
+ output: z.object({ result: z.number() })
55
+ }, ({ input, output }) => Verdict.equals(output.result, input.x));
56
+ const result = await ev({ input: { x: 5 }, output: { result: 10 } });
57
+ expect(result).toBeInstanceOf(EvaluationBooleanResult);
58
+ expect(result.value).toBe(false);
59
+ expect(result.confidence).toBe(1.0);
60
+ });
61
+ it('provides type inference via Zod schemas without as-casts', async () => {
62
+ const ev = verify({
63
+ name: 'typed_test',
64
+ input: z.object({ values: z.array(z.number()) }),
65
+ output: z.object({ sum: z.number() })
66
+ }, ({ input, output }) => Verdict.equals(output.sum, input.values.reduce((a, b) => a + b, 0)));
67
+ const result = await ev({ input: { values: [1, 2, 3] }, output: { sum: 6 } });
68
+ expect(result.value).toBe(true);
69
+ });
70
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@outputai/evals",
3
+ "version": "0.1.0",
4
+ "description": "Offline evaluation framework for Output.ai workflows",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "output-workflow-bundle": "./dist/index_sandbox.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "./dist"
17
+ ],
18
+ "dependencies": {
19
+ "@outputai/core": "0.1.0",
20
+ "@outputai/llm": "0.1.0"
21
+ },
22
+ "license": "Apache-2.0",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "build": "rm -rf ./dist && tsc"
28
+ }
29
+ }