@axlsdk/eval 0.13.0 → 0.13.2
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 +218 -230
- package/dist/{chunk-7B5NFD5T.js → chunk-K73SNPVV.js} +5 -5
- package/dist/chunk-K73SNPVV.js.map +1 -0
- package/dist/cli.cjs +6 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +7 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-7B5NFD5T.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,63 +10,120 @@ Evaluation framework for [Axl](https://github.com/axl-sdk/axl) agentic workflows
|
|
|
10
10
|
npm install @axlsdk/eval
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
TypeScript eval files require [tsx](https://github.com/privatenumber/tsx) as a dev dependency (`npm install -D tsx`).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Quick Start
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
An eval file defines what to test (dataset), how to run it (execution function), and how to score it (scorers):
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// evals/qa.eval.ts
|
|
21
|
+
import { defineEval, dataset, scorer, llmScorer } from '@axlsdk/eval';
|
|
22
|
+
import type { AxlRuntime } from '@axlsdk/axl';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { qaAgent } from '../src/agents/qa.js';
|
|
25
|
+
|
|
26
|
+
export default defineEval({
|
|
27
|
+
workflow: 'qa-eval', // label for results (used in output table and comparisons)
|
|
28
|
+
dataset: dataset({
|
|
29
|
+
name: 'qa-basics',
|
|
30
|
+
schema: z.object({ question: z.string() }),
|
|
31
|
+
items: [
|
|
32
|
+
{ input: { question: 'What is TypeScript?' } },
|
|
33
|
+
{ input: { question: 'Explain closures.' } },
|
|
34
|
+
],
|
|
35
|
+
}),
|
|
36
|
+
scorers: [
|
|
37
|
+
// Deterministic — runs in-process, no LLM call
|
|
38
|
+
scorer({
|
|
39
|
+
name: 'not-empty',
|
|
40
|
+
description: 'Output is non-empty',
|
|
41
|
+
score: (output) => (String(output).length > 10 ? 1 : 0),
|
|
42
|
+
}),
|
|
43
|
+
// LLM-as-judge — calls an LLM to evaluate quality
|
|
44
|
+
llmScorer({
|
|
45
|
+
name: 'relevance',
|
|
46
|
+
description: 'Is the answer relevant to the question?',
|
|
47
|
+
model: 'openai:gpt-4o',
|
|
48
|
+
system: 'Rate whether the answer is relevant to the question asked.',
|
|
49
|
+
schema: z.object({ score: z.number(), reasoning: z.string() }),
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// How to produce output for each dataset item
|
|
55
|
+
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
56
|
+
const ctx = runtime.createContext();
|
|
57
|
+
return { output: await ctx.ask(qaAgent, input.question) };
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Run it:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
OPENAI_API_KEY=sk-... npx axl-eval ./evals/qa.eval.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Eval: qa-eval x qa-basics (2 items)
|
|
69
|
+
Scorer Mean Min Max p50 p95
|
|
70
|
+
─────────────────────────────────────────────────────
|
|
71
|
+
not-empty 1.00 1.00 1.00 1.00 1.00
|
|
72
|
+
relevance 0.90 0.80 1.00 0.90 1.00
|
|
73
|
+
|
|
74
|
+
Failures: 0/2 | Cost: $0.01 | Duration: 3.2s
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If an LLM scorer fails (wrong API key, provider down, invalid response), you'll see:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
relevance -- -- -- -- --
|
|
81
|
+
|
|
82
|
+
Scorer errors (2/2 items affected):
|
|
83
|
+
- Scorer "relevance" threw: OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Datasets
|
|
18
87
|
|
|
19
88
|
```typescript
|
|
20
89
|
import { dataset } from '@axlsdk/eval';
|
|
21
90
|
import { z } from 'zod';
|
|
22
91
|
|
|
23
|
-
// Inline items with schema and annotations
|
|
24
92
|
const ds = dataset({
|
|
25
93
|
name: 'math-basics',
|
|
26
94
|
schema: z.object({ question: z.string() }),
|
|
27
|
-
annotations: z.object({ answer: z.number() }),
|
|
95
|
+
annotations: z.object({ answer: z.number() }), // optional ground truth
|
|
28
96
|
items: [
|
|
29
97
|
{ input: { question: '2+2' }, annotations: { answer: 4 } },
|
|
30
98
|
{ input: { question: '3*5' }, annotations: { answer: 15 } },
|
|
31
|
-
{ input: { question: '10/2' }, annotations: { answer: 5 } },
|
|
32
99
|
],
|
|
33
100
|
});
|
|
34
|
-
|
|
35
|
-
// Load from file
|
|
36
|
-
const ds = dataset({
|
|
37
|
-
name: 'math-advanced',
|
|
38
|
-
schema: z.object({ question: z.string() }),
|
|
39
|
-
file: './datasets/math.json',
|
|
40
|
-
});
|
|
41
101
|
```
|
|
42
102
|
|
|
43
|
-
|
|
103
|
+
You can also load from a file: `dataset({ name: 'large', schema, file: './data.json' })`.
|
|
104
|
+
|
|
105
|
+
## Scorers
|
|
44
106
|
|
|
45
|
-
|
|
107
|
+
Scorers rate each output on a 0-1 scale. You can mix deterministic and LLM scorers in the same eval.
|
|
108
|
+
|
|
109
|
+
### Deterministic scorers
|
|
110
|
+
|
|
111
|
+
Pure functions — fast, free, deterministic. The `score` callback receives `(output, input, annotations?)`:
|
|
46
112
|
|
|
47
113
|
```typescript
|
|
48
114
|
import { scorer } from '@axlsdk/eval';
|
|
49
115
|
|
|
50
|
-
const
|
|
51
|
-
name: '
|
|
52
|
-
description: '
|
|
53
|
-
score: (output,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const partialMatch = scorer({
|
|
57
|
-
name: 'partial',
|
|
58
|
-
description: 'Checks if the output contains the expected answer',
|
|
59
|
-
score: (output, input, annotations) => {
|
|
60
|
-
const outputStr = String(output).toLowerCase();
|
|
61
|
-
const expected = String(annotations?.answer).toLowerCase();
|
|
62
|
-
return outputStr.includes(expected) ? 1 : 0;
|
|
63
|
-
},
|
|
116
|
+
const containsAnswer = scorer({
|
|
117
|
+
name: 'contains-answer',
|
|
118
|
+
description: 'Output contains the expected numeric answer',
|
|
119
|
+
score: (output, _input, annotations) =>
|
|
120
|
+
String(output).includes(String(annotations?.answer)) ? 1 : 0,
|
|
64
121
|
});
|
|
65
122
|
```
|
|
66
123
|
|
|
67
|
-
###
|
|
124
|
+
### LLM scorers
|
|
68
125
|
|
|
69
|
-
Use an LLM as a judge. The scorer
|
|
126
|
+
Use an LLM as a judge. The scorer constructs a prompt from the input, output, and annotations, calls the LLM, and validates the response against your Zod schema:
|
|
70
127
|
|
|
71
128
|
```typescript
|
|
72
129
|
import { llmScorer } from '@axlsdk/eval';
|
|
@@ -74,285 +131,216 @@ import { z } from 'zod';
|
|
|
74
131
|
|
|
75
132
|
const qualityJudge = llmScorer({
|
|
76
133
|
name: 'quality',
|
|
77
|
-
description: 'Rates output quality
|
|
78
|
-
model: 'openai
|
|
79
|
-
system: '
|
|
134
|
+
description: 'Rates overall output quality',
|
|
135
|
+
model: 'openai:gpt-4o', // provider:model URI
|
|
136
|
+
system: 'Rate the quality of the AI output.',
|
|
80
137
|
schema: z.object({
|
|
81
138
|
score: z.number().min(0).max(1),
|
|
82
139
|
reasoning: z.string(),
|
|
83
140
|
}),
|
|
84
|
-
temperature: 0.2,
|
|
141
|
+
temperature: 0.2, // default: 0.2 (low for consistency)
|
|
85
142
|
});
|
|
86
143
|
```
|
|
87
144
|
|
|
88
|
-
|
|
145
|
+
The `model` field uses a `provider:model` URI. The provider is resolved automatically at eval time — just set the right API key:
|
|
89
146
|
|
|
90
|
-
|
|
147
|
+
| Provider | URI prefix | Env var |
|
|
148
|
+
|----------|-----------|---------|
|
|
149
|
+
| OpenAI (Chat Completions) | `openai:` | `OPENAI_API_KEY` |
|
|
150
|
+
| OpenAI (Responses API) | `openai-responses:` | `OPENAI_API_KEY` |
|
|
151
|
+
| Anthropic | `anthropic:` | `ANTHROPIC_API_KEY` |
|
|
152
|
+
| Google Gemini | `google:` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` |
|
|
153
|
+
| Custom | `your-name:` | Via `runtime.registerProvider('your-name', provider)` |
|
|
91
154
|
|
|
92
|
-
|
|
93
|
-
import { runEval } from '@axlsdk/eval';
|
|
155
|
+
Different LLM scorers can use different providers — each resolves independently:
|
|
94
156
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
dataset: ds,
|
|
99
|
-
scorers: [exactMatch, qualityJudge],
|
|
100
|
-
concurrency: 5,
|
|
101
|
-
},
|
|
102
|
-
async (input) => {
|
|
103
|
-
const output = await runtime.execute('my-workflow', input);
|
|
104
|
-
return { output };
|
|
105
|
-
},
|
|
106
|
-
runtime,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
console.log(results.summary);
|
|
110
|
-
// { count: 3, failures: 0, scorers: { 'exact-match': { mean: 0.85, min: 0, max: 1, p50: 1, p95: 1 }, ... } }
|
|
157
|
+
```typescript
|
|
158
|
+
const qualityJudge = llmScorer({ name: 'quality', model: 'openai:gpt-4o', ... });
|
|
159
|
+
const safetyJudge = llmScorer({ name: 'safety', model: 'anthropic:claude-sonnet-4-5-20250514', ... });
|
|
111
160
|
```
|
|
112
161
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Compare two evaluation runs:
|
|
162
|
+
## Running Evals
|
|
116
163
|
|
|
117
|
-
|
|
118
|
-
import { evalCompare } from '@axlsdk/eval';
|
|
164
|
+
### CLI
|
|
119
165
|
|
|
120
|
-
|
|
166
|
+
The most common way to run evals:
|
|
121
167
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
```bash
|
|
169
|
+
npx axl-eval ./evals/qa.eval.ts # run a single file
|
|
170
|
+
npx axl-eval ./evals/ # run all *.eval.* files in a directory
|
|
171
|
+
npx axl-eval ./evals/ --output ./results/v1.json # save results to JSON
|
|
172
|
+
npx axl-eval ./evals/ --config ./axl.config.ts # use a specific runtime config
|
|
127
173
|
```
|
|
128
174
|
|
|
129
|
-
|
|
175
|
+
The CLI resolves a runtime automatically: `--config <path>` > auto-detect `axl.config.*` > bare `new AxlRuntime()` (providers from env vars).
|
|
130
176
|
|
|
131
|
-
|
|
177
|
+
### Programmatic
|
|
178
|
+
|
|
179
|
+
**`runtime.eval()`** — when you have a workflow registered on the runtime. The `workflow` field must match the registered name:
|
|
132
180
|
|
|
133
181
|
```typescript
|
|
134
|
-
|
|
135
|
-
import {
|
|
182
|
+
import { AxlRuntime, workflow, agent } from '@axlsdk/axl';
|
|
183
|
+
import { dataset, scorer } from '@axlsdk/eval';
|
|
136
184
|
import { z } from 'zod';
|
|
137
185
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{ input: { question: '2+2' }, annotations: { answer: 4 } },
|
|
144
|
-
{ input: { question: '3*5' }, annotations: { answer: 15 } },
|
|
145
|
-
],
|
|
186
|
+
const qaAgent = agent({ name: 'qa', model: 'openai:gpt-4o', system: 'Answer questions.' });
|
|
187
|
+
const qaWorkflow = workflow({
|
|
188
|
+
name: 'qa-workflow',
|
|
189
|
+
input: z.object({ question: z.string() }),
|
|
190
|
+
handler: async (ctx) => ctx.ask(qaAgent, ctx.input.question),
|
|
146
191
|
});
|
|
147
192
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
description: 'Checks if the output contains the expected numeric answer',
|
|
151
|
-
score: (output, _input, annotations) =>
|
|
152
|
-
String(output).includes(String(annotations?.answer)) ? 1 : 0,
|
|
153
|
-
});
|
|
193
|
+
const runtime = new AxlRuntime();
|
|
194
|
+
runtime.register(qaWorkflow);
|
|
154
195
|
|
|
155
|
-
|
|
156
|
-
workflow: '
|
|
157
|
-
dataset:
|
|
158
|
-
scorers: [
|
|
196
|
+
const results = await runtime.eval({
|
|
197
|
+
workflow: 'qa-workflow', // must match the registered workflow name
|
|
198
|
+
dataset: ds,
|
|
199
|
+
scorers: [containsAnswer, qualityJudge],
|
|
159
200
|
});
|
|
160
201
|
```
|
|
161
202
|
|
|
162
|
-
|
|
203
|
+
**`runEval()`** — when you want full control. The `workflow` field is just a label; the second argument is the function that produces output:
|
|
163
204
|
|
|
164
|
-
|
|
205
|
+
```typescript
|
|
206
|
+
import { runEval } from '@axlsdk/eval';
|
|
165
207
|
|
|
166
|
-
|
|
208
|
+
const results = await runEval(
|
|
209
|
+
{ workflow: 'my-eval', dataset: ds, scorers: [containsAnswer, qualityJudge] },
|
|
210
|
+
async (input, runtime) => {
|
|
211
|
+
const ctx = runtime.createContext();
|
|
212
|
+
return { output: await ctx.ask(qaAgent, input.question) };
|
|
213
|
+
},
|
|
214
|
+
runtime,
|
|
215
|
+
);
|
|
216
|
+
```
|
|
167
217
|
|
|
168
|
-
|
|
169
|
-
// evals/qa-quality.eval.ts — calls an agent directly via the runtime
|
|
170
|
-
import { defineEval, dataset, scorer } from '@axlsdk/eval';
|
|
171
|
-
import type { AxlRuntime } from '@axlsdk/axl';
|
|
172
|
-
import { z } from 'zod';
|
|
173
|
-
import { qaAgent } from '../src/agents/qa.js'; // import your agent definition
|
|
218
|
+
### Studio
|
|
174
219
|
|
|
175
|
-
|
|
176
|
-
name: 'qa-pairs',
|
|
177
|
-
schema: z.object({ question: z.string() }),
|
|
178
|
-
items: [
|
|
179
|
-
{ input: { question: 'What is TypeScript?' } },
|
|
180
|
-
{ input: { question: 'How do promises work?' } },
|
|
181
|
-
],
|
|
182
|
-
});
|
|
220
|
+
Eval files can be lazy-loaded by the Studio middleware for the Eval Runner panel:
|
|
183
221
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
description: 'Output is non-empty',
|
|
187
|
-
score: (output) => (String(output).length > 0 ? 1 : 0),
|
|
188
|
-
});
|
|
222
|
+
```typescript
|
|
223
|
+
import { createStudioMiddleware } from '@axlsdk/studio/middleware';
|
|
189
224
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
scorers: [notEmpty],
|
|
225
|
+
const studio = createStudioMiddleware({
|
|
226
|
+
runtime,
|
|
227
|
+
evals: 'evals/**/*.eval.ts',
|
|
194
228
|
});
|
|
195
|
-
|
|
196
|
-
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
197
|
-
const ctx = runtime.createContext();
|
|
198
|
-
const output = await ctx.ask(qaAgent, input.question);
|
|
199
|
-
return { output }; // cost tracked automatically via runtime.trackCost()
|
|
200
|
-
}
|
|
201
229
|
```
|
|
202
230
|
|
|
203
|
-
|
|
231
|
+
See the [@axlsdk/studio README](../axl-studio/README.md#lazy-eval-loading) for details.
|
|
204
232
|
|
|
205
|
-
|
|
206
|
-
// evals/parser.eval.ts — tests a pure function, no runtime needed
|
|
207
|
-
export async function executeWorkflow(input: { raw: string }) {
|
|
208
|
-
const result = parseDocument(input.raw);
|
|
209
|
-
return { output: result };
|
|
210
|
-
}
|
|
211
|
-
```
|
|
233
|
+
## Understanding Results
|
|
212
234
|
|
|
213
|
-
|
|
235
|
+
Each eval run returns an `EvalResult` with per-item scores and aggregate statistics.
|
|
214
236
|
|
|
215
|
-
|
|
237
|
+
**Scores** are `number | null` per scorer per item:
|
|
238
|
+
- `0` to `1` — valid score from the scorer
|
|
239
|
+
- `null` — the scorer failed (see `item.scorerErrors` for details)
|
|
216
240
|
|
|
217
|
-
|
|
241
|
+
Summary statistics (mean, p50, p95, etc.) exclude `null` scores. If all scores for a scorer are `null`, the CLI shows `--` instead of misleading `0.00`.
|
|
218
242
|
|
|
219
243
|
```typescript
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
const results = await runtime.eval({ ... });
|
|
245
|
+
|
|
246
|
+
// Aggregate stats
|
|
247
|
+
console.log(results.summary.scorers['quality'].mean); // 0.85
|
|
248
|
+
console.log(results.summary.count); // 50 items
|
|
249
|
+
console.log(results.summary.failures); // 2 workflow errors
|
|
250
|
+
|
|
251
|
+
// Per-item inspection
|
|
252
|
+
for (const item of results.items) {
|
|
253
|
+
if (item.error) continue; // workflow threw
|
|
254
|
+
if (item.scores['quality'] === null) {
|
|
255
|
+
console.log('Scorer failed:', item.scorerErrors); // e.g., ["Scorer "quality" threw: ..."]
|
|
256
|
+
}
|
|
224
257
|
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
To override the automatic cost (e.g., to exclude setup calls), return it explicitly:
|
|
228
258
|
|
|
229
|
-
|
|
230
|
-
|
|
259
|
+
// Cost includes both workflow and LLM scorer calls
|
|
260
|
+
console.log(results.totalCost); // 0.42
|
|
231
261
|
```
|
|
232
262
|
|
|
233
|
-
|
|
263
|
+
## Comparing Results
|
|
234
264
|
|
|
235
|
-
|
|
265
|
+
Compare two runs to detect regressions and improvements:
|
|
236
266
|
|
|
237
|
-
|
|
267
|
+
```bash
|
|
268
|
+
npx axl-eval compare ./results/v1.json ./results/v2.json
|
|
269
|
+
npx axl-eval compare v1.json v2.json --fail-on-regression # exit 1 if worse
|
|
270
|
+
```
|
|
238
271
|
|
|
239
272
|
```typescript
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
import { evalCompare } from '@axlsdk/eval';
|
|
274
|
+
|
|
275
|
+
const comparison = evalCompare(v1Results, v2Results);
|
|
276
|
+
console.log(comparison.regressions); // items that got worse
|
|
277
|
+
console.log(comparison.improvements); // items that got better
|
|
245
278
|
```
|
|
246
279
|
|
|
247
|
-
|
|
280
|
+
## Eval Files in Detail
|
|
248
281
|
|
|
249
|
-
|
|
250
|
-
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
251
|
-
const controller = new AbortController();
|
|
252
|
-
setTimeout(() => controller.abort(), 30_000);
|
|
253
|
-
const ctx = runtime.createContext({ signal: controller.signal });
|
|
254
|
-
const output = await ctx.ask(myAgent, input.question);
|
|
255
|
-
return { output };
|
|
256
|
-
}
|
|
257
|
-
```
|
|
282
|
+
### Execution function
|
|
258
283
|
|
|
259
|
-
|
|
284
|
+
By default, the runner calls `runtime.execute(workflow, input)` for each item. Export `executeWorkflow` to override:
|
|
260
285
|
|
|
261
286
|
```typescript
|
|
287
|
+
// evals/qa.eval.ts
|
|
288
|
+
import { defineEval, dataset, scorer } from '@axlsdk/eval';
|
|
289
|
+
import type { AxlRuntime } from '@axlsdk/axl';
|
|
290
|
+
import { qaAgent } from '../src/agents/qa.js';
|
|
291
|
+
|
|
292
|
+
export default defineEval({
|
|
293
|
+
workflow: 'qa-eval',
|
|
294
|
+
dataset: dataset({ ... }),
|
|
295
|
+
scorers: [scorer({ ... })],
|
|
296
|
+
});
|
|
297
|
+
|
|
262
298
|
export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
|
|
263
|
-
const ctx = runtime.createContext(
|
|
264
|
-
|
|
265
|
-
});
|
|
266
|
-
const output = await ctx.ask(myAgent, input.question);
|
|
267
|
-
return { output };
|
|
299
|
+
const ctx = runtime.createContext();
|
|
300
|
+
return { output: await ctx.ask(qaAgent, input.question) };
|
|
268
301
|
}
|
|
269
302
|
```
|
|
270
303
|
|
|
271
|
-
|
|
304
|
+
When no runtime is needed (e.g., testing a pure function):
|
|
272
305
|
|
|
273
306
|
```typescript
|
|
274
|
-
export async function executeWorkflow(
|
|
275
|
-
|
|
276
|
-
runtime: AxlRuntime,
|
|
277
|
-
) {
|
|
278
|
-
const ctx = runtime.createContext({
|
|
279
|
-
sessionHistory: [
|
|
280
|
-
{ role: 'user', content: input.setupPrompt },
|
|
281
|
-
{ role: 'assistant', content: input.setupResponse },
|
|
282
|
-
],
|
|
283
|
-
});
|
|
284
|
-
const output = await ctx.ask(myAgent, input.followUp);
|
|
285
|
-
return { output };
|
|
307
|
+
export async function executeWorkflow(input: { raw: string }) {
|
|
308
|
+
return { output: parseDocument(input.raw) };
|
|
286
309
|
}
|
|
287
310
|
```
|
|
288
311
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
Run evaluations from the command line:
|
|
292
|
-
|
|
293
|
-
```bash
|
|
294
|
-
# Run an eval file
|
|
295
|
-
npx axl-eval ./evals/math.eval.ts
|
|
312
|
+
### Cost tracking
|
|
296
313
|
|
|
297
|
-
|
|
298
|
-
npx axl-eval ./evals/
|
|
314
|
+
Cost is tracked automatically — the runner wraps each item with `runtime.trackCost()`. LLM scorer costs are also included in `totalCost` and count toward the `budget` limit.
|
|
299
315
|
|
|
300
|
-
|
|
301
|
-
npx axl-eval ./evals/math.eval.ts --output ./results/baseline.json
|
|
316
|
+
To override (e.g., exclude setup calls), return cost explicitly:
|
|
302
317
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
# Use custom import conditions (monorepo source exports)
|
|
307
|
-
npx axl-eval ./evals/ --config ./axl.config.ts --conditions development
|
|
308
|
-
|
|
309
|
-
# Compare two results
|
|
310
|
-
npx axl-eval compare ./results/baseline.json ./results/candidate.json
|
|
311
|
-
|
|
312
|
-
# Fail CI if regressions detected
|
|
313
|
-
npx axl-eval compare baseline.json candidate.json --fail-on-regression
|
|
318
|
+
```typescript
|
|
319
|
+
return { output, cost: ctx.totalCost };
|
|
314
320
|
```
|
|
315
321
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
### Runtime Resolution
|
|
319
|
-
|
|
320
|
-
The CLI resolves an `AxlRuntime` to pass to `executeWorkflow`:
|
|
321
|
-
|
|
322
|
-
1. **`--config <path>`** — use an explicit config file
|
|
323
|
-
2. **Auto-detect** — search for `axl.config.mts` → `.ts` → `.mjs` → `.js` in cwd
|
|
324
|
-
3. **Fallback** — create a bare `new AxlRuntime()` (providers resolve from environment variables)
|
|
325
|
-
|
|
326
|
-
When a runtime is available, each `executeWorkflow` call is wrapped with `runtime.trackCost()` for automatic cost attribution.
|
|
322
|
+
### Common patterns
|
|
327
323
|
|
|
328
|
-
|
|
324
|
+
**Budget** — cap cost per item with `runtime.createContext({ budget: '$0.50' })`.
|
|
329
325
|
|
|
330
|
-
|
|
326
|
+
**Timeout** — abort slow items:
|
|
331
327
|
|
|
332
328
|
```typescript
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
runtime,
|
|
337
|
-
evals: 'evals/**/*.eval.ts',
|
|
338
|
-
});
|
|
329
|
+
const controller = new AbortController();
|
|
330
|
+
setTimeout(() => controller.abort(), 30_000);
|
|
331
|
+
const ctx = runtime.createContext({ signal: controller.signal });
|
|
339
332
|
```
|
|
340
333
|
|
|
341
|
-
|
|
334
|
+
**Auto-approve tools** — skip human approval in evals: `runtime.createContext({ awaitHumanHandler: async () => ({ approved: true }) })`.
|
|
342
335
|
|
|
343
|
-
|
|
336
|
+
**Multi-turn** — provide conversation history:
|
|
344
337
|
|
|
345
338
|
```typescript
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const results = await runtime.eval({
|
|
352
|
-
workflow: 'my-workflow',
|
|
353
|
-
dataset: ds,
|
|
354
|
-
scorers: [exactMatch],
|
|
355
|
-
concurrency: 3,
|
|
339
|
+
const ctx = runtime.createContext({
|
|
340
|
+
sessionHistory: [
|
|
341
|
+
{ role: 'user', content: input.setupPrompt },
|
|
342
|
+
{ role: 'assistant', content: input.setupResponse },
|
|
343
|
+
],
|
|
356
344
|
});
|
|
357
345
|
```
|
|
358
346
|
|
|
@@ -158,8 +158,8 @@ async function runEval(config, executeWorkflow, runtime) {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
if (score < 0 || score > 1) {
|
|
161
|
-
if (!evalItem.
|
|
162
|
-
evalItem.
|
|
161
|
+
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
162
|
+
evalItem.scorerErrors.push(
|
|
163
163
|
`Scorer "${scorer.name}" returned out-of-range score ${score} for input ${JSON.stringify(item.input)}`
|
|
164
164
|
);
|
|
165
165
|
evalItem.scores[scorer.name] = null;
|
|
@@ -173,8 +173,8 @@ async function runEval(config, executeWorkflow, runtime) {
|
|
|
173
173
|
totalCost += lastCost;
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
if (!evalItem.
|
|
177
|
-
evalItem.
|
|
176
|
+
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
177
|
+
evalItem.scorerErrors.push(
|
|
178
178
|
`Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
179
179
|
);
|
|
180
180
|
evalItem.scores[scorer.name] = null;
|
|
@@ -215,4 +215,4 @@ export {
|
|
|
215
215
|
evalCompare,
|
|
216
216
|
runEval
|
|
217
217
|
};
|
|
218
|
-
//# sourceMappingURL=chunk-
|
|
218
|
+
//# sourceMappingURL=chunk-K73SNPVV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compare.ts","../src/runner.ts"],"sourcesContent":["import type { EvalResult, EvalComparison, EvalRegression, EvalImprovement } from './types.js';\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\nexport function evalCompare(baseline: EvalResult, candidate: EvalResult): EvalComparison {\n if (baseline.dataset !== candidate.dataset) {\n throw new Error(\n `Cannot compare evals from different datasets: \"${baseline.dataset}\" vs \"${candidate.dataset}\"`,\n );\n }\n\n const baselineScorerNames = Object.keys(baseline.summary.scorers).sort();\n const candidateScorerNames = Object.keys(candidate.summary.scorers).sort();\n if (JSON.stringify(baselineScorerNames) !== JSON.stringify(candidateScorerNames)) {\n throw new Error(`Cannot compare evals with different scorers`);\n }\n\n const scorers: EvalComparison['scorers'] = {};\n for (const name of baselineScorerNames) {\n const bMean = baseline.summary.scorers[name].mean;\n const cMean = candidate.summary.scorers[name].mean;\n const delta = round(cMean - bMean);\n const deltaPercent = bMean > 0 ? round((delta / bMean) * 100) : 0;\n scorers[name] = { baselineMean: bMean, candidateMean: cMean, delta, deltaPercent };\n }\n\n const regressions: EvalRegression[] = [];\n const improvements: EvalImprovement[] = [];\n\n const minLength = Math.min(baseline.items.length, candidate.items.length);\n for (let i = 0; i < minLength; i++) {\n const bItem = baseline.items[i];\n const cItem = candidate.items[i];\n if (bItem.error || cItem.error) continue;\n for (const name of baselineScorerNames) {\n const bScore = bItem.scores[name];\n const cScore = cItem.scores[name];\n if (bScore == null || cScore == null) continue;\n const delta = round(cScore - bScore);\n if (delta < -0.1)\n regressions.push({\n input: bItem.input,\n scorer: name,\n baselineScore: bScore,\n candidateScore: cScore,\n delta,\n });\n else if (delta > 0.1)\n improvements.push({\n input: bItem.input,\n scorer: name,\n baselineScore: bScore,\n candidateScore: cScore,\n delta,\n });\n }\n }\n\n const parts: string[] = [];\n for (const name of baselineScorerNames) {\n const s = scorers[name];\n if (Math.abs(s.delta) > 0.001) {\n const direction = s.delta > 0 ? 'improves' : 'regresses';\n const sign = s.delta > 0 ? '+' : '';\n parts.push(\n `'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})`,\n );\n }\n }\n const summaryStr = `candidate ${parts.length > 0 ? parts.join(', ') : 'no significant changes'} with ${regressions.length} regressions and ${improvements.length} improvements`;\n\n return {\n baseline: { id: baseline.id, metadata: baseline.metadata },\n candidate: { id: candidate.id, metadata: candidate.metadata },\n scorers,\n regressions,\n improvements,\n summary: summaryStr,\n };\n}\n","import type { AxlRuntime } from '@axlsdk/axl';\nimport type { EvalConfig, EvalResult, EvalItem, EvalSummary } from './types.js';\nimport type { ScorerContext } from './scorer.js';\nimport { randomUUID } from 'node:crypto';\n\nfunction computeStats(scores: number[]): {\n mean: number;\n min: number;\n max: number;\n p50: number;\n p95: number;\n} {\n if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };\n const sorted = [...scores].sort((a, b) => a - b);\n const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;\n const min = sorted[0];\n const max = sorted[sorted.length - 1];\n const p50 = sorted[Math.floor(sorted.length * 0.5)];\n const p95 = sorted[Math.floor(sorted.length * 0.95)];\n return { mean: round(mean), min: round(min), max: round(max), p50: round(p50), p95: round(p95) };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nfunction parseCost(cost: string): number {\n const match = cost.match(/^\\$?([\\d.]+)$/);\n if (!match) throw new Error(`Invalid cost format: \"${cost}\"`);\n return parseFloat(match[1]);\n}\n\nexport async function runEval(\n config: EvalConfig,\n executeWorkflow: (\n input: unknown,\n runtime: AxlRuntime,\n ) => Promise<{ output: unknown; cost?: number }>,\n runtime: AxlRuntime,\n): Promise<EvalResult> {\n const startTime = Date.now();\n const id = randomUUID();\n const items = await config.dataset.getItems();\n const concurrency = config.concurrency ?? 5;\n const budgetLimit = config.budget ? parseCost(config.budget) : undefined;\n\n // Create a scorer context that LLM scorers use to resolve providers.\n const scorerContext: ScorerContext = {\n resolveProvider: (uri: string) => {\n if (typeof runtime.resolveProvider !== 'function') {\n throw new Error(\n `LLM scorers require a runtime with resolveProvider(). ` +\n `Ensure you are using a real AxlRuntime instance, not a mock.`,\n );\n }\n return runtime.resolveProvider(uri);\n },\n };\n\n const evalItems: EvalItem[] = [];\n let totalCost = 0;\n let budgetExceeded = false;\n\n async function processItem(item: (typeof items)[0]): Promise<void> {\n if (budgetExceeded) {\n evalItems.push({\n input: item.input,\n annotations: item.annotations,\n output: null,\n error: 'Budget exceeded',\n scores: {},\n });\n return;\n }\n\n const evalItem: EvalItem = {\n input: item.input,\n annotations: item.annotations,\n output: null,\n scores: {},\n };\n try {\n const result = await executeWorkflow(item.input, runtime);\n evalItem.output = result.output;\n if (result.cost != null) {\n totalCost += result.cost;\n }\n } catch (err) {\n evalItem.error = err instanceof Error ? err.message : String(err);\n evalItems.push(evalItem);\n return;\n }\n\n if (budgetLimit != null && totalCost > budgetLimit) {\n budgetExceeded = true;\n }\n\n for (const scorer of config.scorers) {\n try {\n const score = await scorer.score(\n evalItem.output,\n item.input,\n item.annotations,\n scorerContext,\n );\n // Accumulate LLM scorer cost if available\n if (scorer.isLlm) {\n const lastCost = (scorer as unknown as { _lastCost?: number })._lastCost;\n if (lastCost != null) {\n totalCost += lastCost;\n }\n }\n if (score < 0 || score > 1) {\n if (!evalItem.scorerErrors) evalItem.scorerErrors = [];\n evalItem.scorerErrors.push(\n `Scorer \"${scorer.name}\" returned out-of-range score ${score} for input ${JSON.stringify(item.input)}`,\n );\n evalItem.scores[scorer.name] = null;\n } else {\n evalItem.scores[scorer.name] = round(score);\n }\n } catch (err) {\n // Accumulate cost even on failure — the LLM call may have succeeded\n // before a downstream error (e.g., invalid JSON, schema validation)\n if (scorer.isLlm) {\n const lastCost = (scorer as unknown as { _lastCost?: number })._lastCost;\n if (lastCost != null) {\n totalCost += lastCost;\n }\n }\n if (!evalItem.scorerErrors) evalItem.scorerErrors = [];\n evalItem.scorerErrors.push(\n `Scorer \"${scorer.name}\" threw: ${err instanceof Error ? err.message : String(err)}`,\n );\n evalItem.scores[scorer.name] = null;\n }\n }\n evalItems.push(evalItem);\n }\n\n let index = 0;\n async function runNext(): Promise<void> {\n while (index < items.length) {\n const currentIndex = index++;\n await processItem(items[currentIndex]);\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());\n await Promise.all(workers);\n\n const failures = evalItems.filter((i) => i.error).length;\n const scorerNames = config.scorers.map((s) => s.name);\n const scorerStats: EvalSummary['scorers'] = {};\n for (const name of scorerNames) {\n const scores = evalItems\n .filter((i) => !i.error && i.scores[name] != null)\n .map((i) => i.scores[name] as number);\n scorerStats[name] = computeStats(scores);\n }\n\n return {\n id,\n workflow: config.workflow,\n dataset: config.dataset.name,\n metadata: config.metadata ?? {},\n timestamp: new Date().toISOString(),\n totalCost,\n duration: Date.now() - startTime,\n items: evalItems,\n summary: { count: items.length, failures, scorers: scorerStats },\n };\n}\n"],"mappings":"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","names":["round"]}
|