@axlsdk/eval 0.13.6 → 0.13.8
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 +170 -6
- package/dist/chunk-CMWRFU3P.js +653 -0
- package/dist/chunk-CMWRFU3P.js.map +1 -0
- package/dist/cli.cjs +577 -73
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +169 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +413 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -2
- package/dist/index.d.ts +74 -2
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-F2AALK5U.js +0 -290
- package/dist/chunk-F2AALK5U.js.map +0 -1
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
// src/bootstrap.ts
|
|
2
|
+
function pairedBootstrapCI(differences, options) {
|
|
3
|
+
const n = differences.length;
|
|
4
|
+
if (n === 0) return { lower: 0, upper: 0, mean: 0, pRegression: 0, pImprovement: 0 };
|
|
5
|
+
const nResamples = options?.nResamples ?? 1e3;
|
|
6
|
+
const alpha = options?.alpha ?? 0.05;
|
|
7
|
+
const mean = differences.reduce((a, b) => a + b, 0) / n;
|
|
8
|
+
if (n === 1)
|
|
9
|
+
return {
|
|
10
|
+
lower: differences[0],
|
|
11
|
+
upper: differences[0],
|
|
12
|
+
mean,
|
|
13
|
+
pRegression: mean < 0 ? 1 : 0,
|
|
14
|
+
pImprovement: mean > 0 ? 1 : 0
|
|
15
|
+
};
|
|
16
|
+
if (nResamples <= 0)
|
|
17
|
+
return {
|
|
18
|
+
lower: round(mean),
|
|
19
|
+
upper: round(mean),
|
|
20
|
+
mean: round(mean),
|
|
21
|
+
pRegression: mean < 0 ? 1 : 0,
|
|
22
|
+
pImprovement: mean > 0 ? 1 : 0
|
|
23
|
+
};
|
|
24
|
+
const rng = options?.seed != null ? xorshift32(options.seed) : () => Math.random();
|
|
25
|
+
const resampleMeans = new Array(nResamples);
|
|
26
|
+
for (let r = 0; r < nResamples; r++) {
|
|
27
|
+
let sum = 0;
|
|
28
|
+
for (let i = 0; i < n; i++) {
|
|
29
|
+
sum += differences[Math.floor(rng() * n)];
|
|
30
|
+
}
|
|
31
|
+
resampleMeans[r] = sum / n;
|
|
32
|
+
}
|
|
33
|
+
resampleMeans.sort((a, b) => a - b);
|
|
34
|
+
const lowerIdx = Math.floor(alpha / 2 * nResamples);
|
|
35
|
+
const upperIdx = Math.floor((1 - alpha / 2) * nResamples) - 1;
|
|
36
|
+
let belowZero = 0;
|
|
37
|
+
for (let i = 0; i < nResamples; i++) {
|
|
38
|
+
if (resampleMeans[i] < 0) belowZero++;
|
|
39
|
+
else break;
|
|
40
|
+
}
|
|
41
|
+
let aboveZero = 0;
|
|
42
|
+
for (let i = nResamples - 1; i >= 0; i--) {
|
|
43
|
+
if (resampleMeans[i] > 0) aboveZero++;
|
|
44
|
+
else break;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
lower: round(resampleMeans[Math.max(0, lowerIdx)]),
|
|
48
|
+
upper: round(resampleMeans[Math.min(nResamples - 1, upperIdx)]),
|
|
49
|
+
mean: round(mean),
|
|
50
|
+
pRegression: round(belowZero / nResamples),
|
|
51
|
+
pImprovement: round(aboveZero / nResamples)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function round(n) {
|
|
55
|
+
return Math.round(n * 1e4) / 1e4;
|
|
56
|
+
}
|
|
57
|
+
function xorshift32(seed) {
|
|
58
|
+
let state = seed | 0 || 1;
|
|
59
|
+
return () => {
|
|
60
|
+
state ^= state << 13;
|
|
61
|
+
state ^= state >> 17;
|
|
62
|
+
state ^= state << 5;
|
|
63
|
+
return (state >>> 0) / 4294967296;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/compare.ts
|
|
68
|
+
function round2(n) {
|
|
69
|
+
return Math.round(n * 1e3) / 1e3;
|
|
70
|
+
}
|
|
71
|
+
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
72
|
+
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
73
|
+
var LEGACY_THRESHOLD = 0.1;
|
|
74
|
+
function resolveThreshold(scorerName, options, metadata) {
|
|
75
|
+
if (typeof options?.thresholds === "number") return options.thresholds;
|
|
76
|
+
if (options?.thresholds && typeof options.thresholds === "object") {
|
|
77
|
+
const perScorer = options.thresholds[scorerName];
|
|
78
|
+
if (perScorer != null) return perScorer;
|
|
79
|
+
}
|
|
80
|
+
const scorerTypes = metadata.scorerTypes;
|
|
81
|
+
if (scorerTypes && scorerName in scorerTypes) {
|
|
82
|
+
return scorerTypes[scorerName] === "llm" ? DEFAULT_LLM_THRESHOLD : DEFAULT_DETERMINISTIC_THRESHOLD;
|
|
83
|
+
}
|
|
84
|
+
return LEGACY_THRESHOLD;
|
|
85
|
+
}
|
|
86
|
+
function evalCompare(baseline, candidate, options) {
|
|
87
|
+
const baselineRuns = Array.isArray(baseline) ? baseline : [baseline];
|
|
88
|
+
const candidateRuns = Array.isArray(candidate) ? candidate : [candidate];
|
|
89
|
+
if (baselineRuns.length === 0 || candidateRuns.length === 0) {
|
|
90
|
+
throw new Error("Cannot compare empty eval result arrays");
|
|
91
|
+
}
|
|
92
|
+
const baselineRef = baselineRuns[0];
|
|
93
|
+
const candidateRef = candidateRuns[0];
|
|
94
|
+
if (baselineRef.dataset !== candidateRef.dataset) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Cannot compare evals from different datasets: "${baselineRef.dataset}" vs "${candidateRef.dataset}"`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const baselineScorerNames = Object.keys(baselineRef.summary.scorers).sort();
|
|
100
|
+
const candidateScorerNames = Object.keys(candidateRef.summary.scorers).sort();
|
|
101
|
+
if (JSON.stringify(baselineScorerNames) !== JSON.stringify(candidateScorerNames)) {
|
|
102
|
+
throw new Error(`Cannot compare evals with different scorers`);
|
|
103
|
+
}
|
|
104
|
+
const pairedDiffs = {};
|
|
105
|
+
for (const name of baselineScorerNames) {
|
|
106
|
+
pairedDiffs[name] = [];
|
|
107
|
+
}
|
|
108
|
+
const runCount = Math.min(baselineRuns.length, candidateRuns.length);
|
|
109
|
+
for (let r = 0; r < runCount; r++) {
|
|
110
|
+
const bRun = baselineRuns[r];
|
|
111
|
+
const cRun = candidateRuns[r];
|
|
112
|
+
const minLength = Math.min(bRun.items.length, cRun.items.length);
|
|
113
|
+
for (let i = 0; i < minLength; i++) {
|
|
114
|
+
const bItem = bRun.items[i];
|
|
115
|
+
const cItem = cRun.items[i];
|
|
116
|
+
if (bItem.error || cItem.error) continue;
|
|
117
|
+
for (const name of baselineScorerNames) {
|
|
118
|
+
const bScore = bItem.scores[name];
|
|
119
|
+
const cScore = cItem.scores[name];
|
|
120
|
+
if (bScore != null && cScore != null) {
|
|
121
|
+
pairedDiffs[name].push(cScore - bScore);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const baselineMeans = {};
|
|
127
|
+
const candidateMeans = {};
|
|
128
|
+
for (const name of baselineScorerNames) {
|
|
129
|
+
baselineMeans[name] = baselineRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / baselineRuns.length;
|
|
130
|
+
candidateMeans[name] = candidateRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / candidateRuns.length;
|
|
131
|
+
}
|
|
132
|
+
const scorers = {};
|
|
133
|
+
for (const name of baselineScorerNames) {
|
|
134
|
+
const bMeanRaw = baselineMeans[name];
|
|
135
|
+
const cMeanRaw = candidateMeans[name];
|
|
136
|
+
const deltaRaw = cMeanRaw - bMeanRaw;
|
|
137
|
+
const bMean = round2(bMeanRaw);
|
|
138
|
+
const cMean = round2(cMeanRaw);
|
|
139
|
+
const delta = round2(deltaRaw);
|
|
140
|
+
const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
|
|
141
|
+
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
142
|
+
const entry = {
|
|
143
|
+
baselineMean: bMean,
|
|
144
|
+
candidateMean: cMean,
|
|
145
|
+
delta,
|
|
146
|
+
deltaPercent
|
|
147
|
+
};
|
|
148
|
+
const diffs = pairedDiffs[name];
|
|
149
|
+
if (diffs.length >= 2) {
|
|
150
|
+
const ci = pairedBootstrapCI(diffs);
|
|
151
|
+
entry.ci = { lower: ci.lower, upper: ci.upper };
|
|
152
|
+
entry.pRegression = ci.pRegression;
|
|
153
|
+
entry.pImprovement = ci.pImprovement;
|
|
154
|
+
entry.n = diffs.length;
|
|
155
|
+
const ciExcludesZero = ci.lower > 0 || ci.upper < 0;
|
|
156
|
+
entry.significant = ciExcludesZero && Math.abs(ci.mean) >= threshold;
|
|
157
|
+
}
|
|
158
|
+
scorers[name] = entry;
|
|
159
|
+
}
|
|
160
|
+
const regressions = [];
|
|
161
|
+
const improvements = [];
|
|
162
|
+
const itemCount = Math.min(
|
|
163
|
+
...baselineRuns.map((r) => r.items.length),
|
|
164
|
+
...candidateRuns.map((r) => r.items.length)
|
|
165
|
+
);
|
|
166
|
+
for (let i = 0; i < itemCount; i++) {
|
|
167
|
+
const hasBaselineError = baselineRuns.some((r) => r.items[i]?.error);
|
|
168
|
+
const hasCandidateError = candidateRuns.some((r) => r.items[i]?.error);
|
|
169
|
+
if (hasBaselineError || hasCandidateError) continue;
|
|
170
|
+
for (const name of baselineScorerNames) {
|
|
171
|
+
const bScores = baselineRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
172
|
+
const cScores = candidateRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
173
|
+
if (bScores.length === 0 || cScores.length === 0) continue;
|
|
174
|
+
const bAvg = bScores.reduce((a, b) => a + b, 0) / bScores.length;
|
|
175
|
+
const cAvg = cScores.reduce((a, b) => a + b, 0) / cScores.length;
|
|
176
|
+
const delta = round2(cAvg - bAvg);
|
|
177
|
+
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
178
|
+
if (delta < -threshold)
|
|
179
|
+
regressions.push({
|
|
180
|
+
itemIndex: i,
|
|
181
|
+
input: baselineRef.items[i]?.input,
|
|
182
|
+
scorer: name,
|
|
183
|
+
baselineScore: round2(bAvg),
|
|
184
|
+
candidateScore: round2(cAvg),
|
|
185
|
+
delta
|
|
186
|
+
});
|
|
187
|
+
else if (delta > threshold)
|
|
188
|
+
improvements.push({
|
|
189
|
+
itemIndex: i,
|
|
190
|
+
input: baselineRef.items[i]?.input,
|
|
191
|
+
scorer: name,
|
|
192
|
+
baselineScore: round2(bAvg),
|
|
193
|
+
candidateScore: round2(cAvg),
|
|
194
|
+
delta
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
let timing;
|
|
199
|
+
const baselineTimings = baselineRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
200
|
+
const candidateTimings = candidateRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
201
|
+
if (baselineTimings.length > 0 && candidateTimings.length > 0) {
|
|
202
|
+
const bMean = baselineTimings.reduce((a, b) => a + b, 0) / baselineTimings.length;
|
|
203
|
+
const cMean = candidateTimings.reduce((a, b) => a + b, 0) / candidateTimings.length;
|
|
204
|
+
const delta = round2(cMean - bMean);
|
|
205
|
+
const deltaPercent = bMean > 0 ? round2((cMean - bMean) / bMean * 100) : 0;
|
|
206
|
+
timing = { baselineMean: round2(bMean), candidateMean: round2(cMean), delta, deltaPercent };
|
|
207
|
+
}
|
|
208
|
+
let cost;
|
|
209
|
+
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + r.totalCost, 0) / baselineRuns.length;
|
|
210
|
+
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + r.totalCost, 0) / candidateRuns.length;
|
|
211
|
+
if (baselineAvgCost > 0 || candidateAvgCost > 0) {
|
|
212
|
+
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
213
|
+
const delta = round2(deltaRaw);
|
|
214
|
+
const deltaPercent = baselineAvgCost > 0 ? round2(deltaRaw / baselineAvgCost * 100) : 0;
|
|
215
|
+
cost = {
|
|
216
|
+
baselineTotal: round2(baselineAvgCost),
|
|
217
|
+
candidateTotal: round2(candidateAvgCost),
|
|
218
|
+
delta,
|
|
219
|
+
deltaPercent
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const parts = [];
|
|
223
|
+
for (const name of baselineScorerNames) {
|
|
224
|
+
const s = scorers[name];
|
|
225
|
+
if (Math.abs(s.delta) > 1e-3) {
|
|
226
|
+
const direction = s.delta > 0 ? "improves" : "regresses";
|
|
227
|
+
const sign = s.delta > 0 ? "+" : "";
|
|
228
|
+
let sigLabel = "";
|
|
229
|
+
if (s.significant === true) sigLabel = " (significant)";
|
|
230
|
+
else if (s.significant === false) sigLabel = " (not significant)";
|
|
231
|
+
parts.push(
|
|
232
|
+
`'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})${sigLabel}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (timing && Math.abs(timing.deltaPercent) > 1) {
|
|
237
|
+
const dir = timing.delta > 0 ? "slower" : "faster";
|
|
238
|
+
parts.push(`${Math.abs(timing.deltaPercent).toFixed(0)}% ${dir}`);
|
|
239
|
+
}
|
|
240
|
+
if (cost && Math.abs(cost.deltaPercent) > 1) {
|
|
241
|
+
const dir = cost.delta > 0 ? "more expensive" : "cheaper";
|
|
242
|
+
parts.push(`${Math.abs(cost.deltaPercent).toFixed(0)}% ${dir}`);
|
|
243
|
+
}
|
|
244
|
+
const summaryStr = `candidate ${parts.length > 0 ? parts.join(", ") : "no meaningful changes"} with ${regressions.length} regressions and ${improvements.length} improvements`;
|
|
245
|
+
return {
|
|
246
|
+
baseline: { id: baselineRef.id, metadata: baselineRef.metadata },
|
|
247
|
+
candidate: { id: candidateRef.id, metadata: candidateRef.metadata },
|
|
248
|
+
scorers,
|
|
249
|
+
timing,
|
|
250
|
+
cost,
|
|
251
|
+
regressions,
|
|
252
|
+
improvements,
|
|
253
|
+
summary: summaryStr
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/scorer.ts
|
|
258
|
+
function normalizeScorerResult(result) {
|
|
259
|
+
return typeof result === "number" ? { score: result } : result;
|
|
260
|
+
}
|
|
261
|
+
function scorer(config) {
|
|
262
|
+
return {
|
|
263
|
+
name: config.name,
|
|
264
|
+
description: config.description,
|
|
265
|
+
isLlm: false,
|
|
266
|
+
score: config.score
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/utils.ts
|
|
271
|
+
function computeStats(scores) {
|
|
272
|
+
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
273
|
+
const sorted = [...scores].sort((a, b) => a - b);
|
|
274
|
+
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
275
|
+
const min = sorted[0];
|
|
276
|
+
const max = sorted[sorted.length - 1];
|
|
277
|
+
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
278
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
279
|
+
return { mean: round3(mean), min: round3(min), max: round3(max), p50: round3(p50), p95: round3(p95) };
|
|
280
|
+
}
|
|
281
|
+
function round3(n) {
|
|
282
|
+
return Math.round(n * 1e3) / 1e3;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/runner.ts
|
|
286
|
+
import { randomUUID } from "crypto";
|
|
287
|
+
function parseCost(cost) {
|
|
288
|
+
const match = cost.match(/^\$?([\d.]+)$/);
|
|
289
|
+
if (!match) throw new Error(`Invalid cost format: "${cost}"`);
|
|
290
|
+
return parseFloat(match[1]);
|
|
291
|
+
}
|
|
292
|
+
async function runEval(config, executeWorkflow, runtime) {
|
|
293
|
+
const startTime = Date.now();
|
|
294
|
+
const id = randomUUID();
|
|
295
|
+
const items = await config.dataset.getItems();
|
|
296
|
+
const concurrency = config.concurrency ?? 5;
|
|
297
|
+
const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
|
|
298
|
+
const scorerContext = {
|
|
299
|
+
resolveProvider: (uri) => {
|
|
300
|
+
if (typeof runtime.resolveProvider !== "function") {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`LLM scorers require a runtime with resolveProvider(). Ensure you are using a real AxlRuntime instance, not a mock.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return runtime.resolveProvider(uri);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const evalItems = new Array(items.length);
|
|
309
|
+
let totalCost = 0;
|
|
310
|
+
let budgetExceeded = false;
|
|
311
|
+
async function processItem(item, itemIndex) {
|
|
312
|
+
if (budgetExceeded) {
|
|
313
|
+
evalItems[itemIndex] = {
|
|
314
|
+
input: item.input,
|
|
315
|
+
annotations: item.annotations,
|
|
316
|
+
output: null,
|
|
317
|
+
error: "Budget exceeded",
|
|
318
|
+
scores: {}
|
|
319
|
+
};
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const evalItem = {
|
|
323
|
+
input: item.input,
|
|
324
|
+
annotations: item.annotations,
|
|
325
|
+
output: null,
|
|
326
|
+
scores: {}
|
|
327
|
+
};
|
|
328
|
+
const itemStart = Date.now();
|
|
329
|
+
try {
|
|
330
|
+
const result = await executeWorkflow(item.input, runtime);
|
|
331
|
+
evalItem.duration = Date.now() - itemStart;
|
|
332
|
+
evalItem.output = result.output;
|
|
333
|
+
evalItem.cost = result.cost;
|
|
334
|
+
if (result.metadata) evalItem.metadata = result.metadata;
|
|
335
|
+
if (result.cost != null) {
|
|
336
|
+
totalCost += result.cost;
|
|
337
|
+
}
|
|
338
|
+
} catch (err) {
|
|
339
|
+
evalItem.duration = Date.now() - itemStart;
|
|
340
|
+
evalItem.error = err instanceof Error ? err.message : String(err);
|
|
341
|
+
evalItems[itemIndex] = evalItem;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
345
|
+
budgetExceeded = true;
|
|
346
|
+
}
|
|
347
|
+
evalItem.scoreDetails = {};
|
|
348
|
+
let itemScorerCost = 0;
|
|
349
|
+
for (const scorer2 of config.scorers) {
|
|
350
|
+
const scorerStart = Date.now();
|
|
351
|
+
try {
|
|
352
|
+
const raw = await scorer2.score(
|
|
353
|
+
evalItem.output,
|
|
354
|
+
item.input,
|
|
355
|
+
item.annotations,
|
|
356
|
+
scorerContext
|
|
357
|
+
);
|
|
358
|
+
const scorerResult = normalizeScorerResult(raw);
|
|
359
|
+
if (scorerResult.cost != null) {
|
|
360
|
+
itemScorerCost += scorerResult.cost;
|
|
361
|
+
totalCost += scorerResult.cost;
|
|
362
|
+
}
|
|
363
|
+
const scorerDuration = Date.now() - scorerStart;
|
|
364
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
365
|
+
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
366
|
+
evalItem.scorerErrors.push(
|
|
367
|
+
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
368
|
+
);
|
|
369
|
+
evalItem.scores[scorer2.name] = null;
|
|
370
|
+
evalItem.scoreDetails[scorer2.name] = {
|
|
371
|
+
score: null,
|
|
372
|
+
metadata: scorerResult.metadata,
|
|
373
|
+
duration: scorerDuration,
|
|
374
|
+
cost: scorerResult.cost
|
|
375
|
+
};
|
|
376
|
+
} else {
|
|
377
|
+
evalItem.scores[scorer2.name] = round3(scorerResult.score);
|
|
378
|
+
evalItem.scoreDetails[scorer2.name] = {
|
|
379
|
+
score: round3(scorerResult.score),
|
|
380
|
+
metadata: scorerResult.metadata,
|
|
381
|
+
duration: scorerDuration,
|
|
382
|
+
cost: scorerResult.cost
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
387
|
+
if (errCost != null) {
|
|
388
|
+
itemScorerCost += errCost;
|
|
389
|
+
totalCost += errCost;
|
|
390
|
+
}
|
|
391
|
+
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
392
|
+
evalItem.scorerErrors.push(
|
|
393
|
+
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
394
|
+
);
|
|
395
|
+
evalItem.scores[scorer2.name] = null;
|
|
396
|
+
evalItem.scoreDetails[scorer2.name] = {
|
|
397
|
+
score: null,
|
|
398
|
+
duration: Date.now() - scorerStart,
|
|
399
|
+
cost: errCost
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
evalItem.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
404
|
+
evalItems[itemIndex] = evalItem;
|
|
405
|
+
}
|
|
406
|
+
let index = 0;
|
|
407
|
+
async function runNext() {
|
|
408
|
+
while (index < items.length) {
|
|
409
|
+
const currentIndex = index++;
|
|
410
|
+
await processItem(items[currentIndex], currentIndex);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
414
|
+
await Promise.all(workers);
|
|
415
|
+
const failures = evalItems.filter((i) => i.error).length;
|
|
416
|
+
const scorerNames = config.scorers.map((s) => s.name);
|
|
417
|
+
const scorerStats = {};
|
|
418
|
+
for (const name of scorerNames) {
|
|
419
|
+
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
420
|
+
scorerStats[name] = computeStats(scores);
|
|
421
|
+
}
|
|
422
|
+
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
423
|
+
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
424
|
+
const scorerTypes = {};
|
|
425
|
+
for (const s of config.scorers) {
|
|
426
|
+
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
427
|
+
}
|
|
428
|
+
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const item of evalItems) {
|
|
430
|
+
const itemCounts = item.metadata?.modelCallCounts;
|
|
431
|
+
if (itemCounts && typeof itemCounts === "object") {
|
|
432
|
+
for (const [m, count] of Object.entries(itemCounts)) {
|
|
433
|
+
if (typeof count === "number")
|
|
434
|
+
totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + count);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
const itemModels = item.metadata?.models;
|
|
438
|
+
if (Array.isArray(itemModels)) {
|
|
439
|
+
for (const m of itemModels) {
|
|
440
|
+
if (typeof m === "string") totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + 1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const modelsMeta = {};
|
|
446
|
+
if (totalModelCalls.size > 0) {
|
|
447
|
+
const sorted = [...totalModelCalls.entries()].sort((a, b) => b[1] - a[1]);
|
|
448
|
+
modelsMeta.models = sorted.map(([m]) => m);
|
|
449
|
+
modelsMeta.modelCounts = Object.fromEntries(sorted);
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
id,
|
|
453
|
+
workflow: config.workflow,
|
|
454
|
+
dataset: config.dataset.name,
|
|
455
|
+
metadata: {
|
|
456
|
+
...config.metadata,
|
|
457
|
+
scorerTypes,
|
|
458
|
+
...modelsMeta
|
|
459
|
+
},
|
|
460
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
461
|
+
totalCost,
|
|
462
|
+
duration: Date.now() - startTime,
|
|
463
|
+
items: evalItems,
|
|
464
|
+
summary: { count: items.length, failures, scorers: scorerStats, timing }
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/rescore.ts
|
|
469
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
470
|
+
async function rescore(result, scorers, runtime, options) {
|
|
471
|
+
const startTime = Date.now();
|
|
472
|
+
const concurrency = options?.concurrency ?? 5;
|
|
473
|
+
const scorerContext = {
|
|
474
|
+
resolveProvider: (uri) => {
|
|
475
|
+
if (typeof runtime.resolveProvider !== "function") {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`LLM scorers require a runtime with resolveProvider(). Ensure you are using a real AxlRuntime instance, not a mock.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return runtime.resolveProvider(uri);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const rescored = new Array(result.items.length);
|
|
484
|
+
let totalCost = 0;
|
|
485
|
+
async function rescoreItem(original, itemIndex) {
|
|
486
|
+
if (original.error) {
|
|
487
|
+
rescored[itemIndex] = {
|
|
488
|
+
input: original.input,
|
|
489
|
+
annotations: original.annotations,
|
|
490
|
+
output: original.output,
|
|
491
|
+
error: original.error,
|
|
492
|
+
scores: {}
|
|
493
|
+
};
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const item = {
|
|
497
|
+
input: original.input,
|
|
498
|
+
annotations: original.annotations,
|
|
499
|
+
output: original.output,
|
|
500
|
+
metadata: original.metadata,
|
|
501
|
+
scores: {},
|
|
502
|
+
scoreDetails: {}
|
|
503
|
+
};
|
|
504
|
+
let itemScorerCost = 0;
|
|
505
|
+
for (const scorer2 of scorers) {
|
|
506
|
+
const scorerStart = Date.now();
|
|
507
|
+
try {
|
|
508
|
+
const raw = await scorer2.score(item.output, item.input, item.annotations, scorerContext);
|
|
509
|
+
const scorerResult = normalizeScorerResult(raw);
|
|
510
|
+
if (scorerResult.cost != null) {
|
|
511
|
+
itemScorerCost += scorerResult.cost;
|
|
512
|
+
totalCost += scorerResult.cost;
|
|
513
|
+
}
|
|
514
|
+
const scorerDuration = Date.now() - scorerStart;
|
|
515
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
516
|
+
if (!item.scorerErrors) item.scorerErrors = [];
|
|
517
|
+
item.scorerErrors.push(
|
|
518
|
+
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
519
|
+
);
|
|
520
|
+
item.scores[scorer2.name] = null;
|
|
521
|
+
item.scoreDetails[scorer2.name] = {
|
|
522
|
+
score: null,
|
|
523
|
+
metadata: scorerResult.metadata,
|
|
524
|
+
duration: scorerDuration,
|
|
525
|
+
cost: scorerResult.cost
|
|
526
|
+
};
|
|
527
|
+
} else {
|
|
528
|
+
item.scores[scorer2.name] = round3(scorerResult.score);
|
|
529
|
+
item.scoreDetails[scorer2.name] = {
|
|
530
|
+
score: round3(scorerResult.score),
|
|
531
|
+
metadata: scorerResult.metadata,
|
|
532
|
+
duration: scorerDuration,
|
|
533
|
+
cost: scorerResult.cost
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
} catch (err) {
|
|
537
|
+
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
538
|
+
if (errCost != null) {
|
|
539
|
+
itemScorerCost += errCost;
|
|
540
|
+
totalCost += errCost;
|
|
541
|
+
}
|
|
542
|
+
if (!item.scorerErrors) item.scorerErrors = [];
|
|
543
|
+
item.scorerErrors.push(
|
|
544
|
+
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
545
|
+
);
|
|
546
|
+
item.scores[scorer2.name] = null;
|
|
547
|
+
item.scoreDetails[scorer2.name] = {
|
|
548
|
+
score: null,
|
|
549
|
+
duration: Date.now() - scorerStart,
|
|
550
|
+
cost: errCost
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
555
|
+
rescored[itemIndex] = item;
|
|
556
|
+
}
|
|
557
|
+
let index = 0;
|
|
558
|
+
async function runNext() {
|
|
559
|
+
while (index < result.items.length) {
|
|
560
|
+
const currentIndex = index++;
|
|
561
|
+
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const workers = Array.from(
|
|
565
|
+
{ length: Math.min(concurrency, result.items.length) },
|
|
566
|
+
() => runNext()
|
|
567
|
+
);
|
|
568
|
+
await Promise.all(workers);
|
|
569
|
+
const failures = rescored.filter((i) => i.error).length;
|
|
570
|
+
const scorerNames = scorers.map((s) => s.name);
|
|
571
|
+
const scorerStats = {};
|
|
572
|
+
for (const name of scorerNames) {
|
|
573
|
+
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
574
|
+
scorerStats[name] = computeStats(scores);
|
|
575
|
+
}
|
|
576
|
+
const scorerTypes = {};
|
|
577
|
+
for (const s of scorers) {
|
|
578
|
+
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
id: randomUUID2(),
|
|
582
|
+
workflow: result.workflow,
|
|
583
|
+
dataset: result.dataset,
|
|
584
|
+
metadata: (() => {
|
|
585
|
+
const { runGroupId: _, runIndex: __, ...rest } = result.metadata;
|
|
586
|
+
return { ...rest, rescored: true, originalId: result.id, scorerTypes };
|
|
587
|
+
})(),
|
|
588
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
589
|
+
totalCost,
|
|
590
|
+
duration: Date.now() - startTime,
|
|
591
|
+
items: rescored,
|
|
592
|
+
summary: { count: result.items.length, failures, scorers: scorerStats }
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/multi-run.ts
|
|
597
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
598
|
+
function std(values, mean) {
|
|
599
|
+
if (values.length <= 1) return 0;
|
|
600
|
+
const sumSqDiffs = values.reduce((sum, v) => sum + (v - mean) ** 2, 0);
|
|
601
|
+
return Math.sqrt(sumSqDiffs / (values.length - 1));
|
|
602
|
+
}
|
|
603
|
+
function aggregateRuns(runs) {
|
|
604
|
+
if (runs.length === 0) throw new Error("Cannot aggregate zero runs");
|
|
605
|
+
const runGroupId = runs[0].metadata.runGroupId ?? randomUUID3();
|
|
606
|
+
const { workflow, dataset } = runs[0];
|
|
607
|
+
const totalCost = runs.reduce((sum, r) => sum + r.totalCost, 0);
|
|
608
|
+
const totalDuration = runs.reduce((sum, r) => sum + r.duration, 0);
|
|
609
|
+
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
610
|
+
const scorers = {};
|
|
611
|
+
for (const name of scorerNames) {
|
|
612
|
+
const means = runs.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
613
|
+
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
614
|
+
const minMean = Math.min(...means);
|
|
615
|
+
const maxMean = Math.max(...means);
|
|
616
|
+
scorers[name] = {
|
|
617
|
+
mean: round3(meanOfMeans),
|
|
618
|
+
std: round3(std(means, meanOfMeans)),
|
|
619
|
+
min: round3(minMean),
|
|
620
|
+
max: round3(maxMean)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
let timing;
|
|
624
|
+
const timingMeans = runs.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
625
|
+
if (timingMeans.length === runs.length && timingMeans.length > 0) {
|
|
626
|
+
const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
|
|
627
|
+
timing = {
|
|
628
|
+
mean: round3(meanTiming),
|
|
629
|
+
std: round3(std(timingMeans, meanTiming))
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
runGroupId,
|
|
634
|
+
runCount: runs.length,
|
|
635
|
+
workflow,
|
|
636
|
+
dataset,
|
|
637
|
+
totalCost: round3(totalCost),
|
|
638
|
+
totalDuration: round3(totalDuration),
|
|
639
|
+
scorers,
|
|
640
|
+
timing
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export {
|
|
645
|
+
pairedBootstrapCI,
|
|
646
|
+
evalCompare,
|
|
647
|
+
normalizeScorerResult,
|
|
648
|
+
scorer,
|
|
649
|
+
runEval,
|
|
650
|
+
rescore,
|
|
651
|
+
aggregateRuns
|
|
652
|
+
};
|
|
653
|
+
//# sourceMappingURL=chunk-CMWRFU3P.js.map
|