@axlsdk/eval 0.23.3 → 0.24.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/README.md +110 -9
- package/dist/chunk-HKEAPM4B.js +1799 -0
- package/dist/chunk-HKEAPM4B.js.map +1 -0
- package/dist/cli.cjs +1518 -537
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +277 -57
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1010 -168
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +546 -10
- package/dist/index.d.ts +546 -10
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-AFUNZE7W.js +0 -966
- package/dist/chunk-AFUNZE7W.js.map +0 -1
package/dist/chunk-AFUNZE7W.js
DELETED
|
@@ -1,966 +0,0 @@
|
|
|
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/utils.ts
|
|
68
|
-
function computeStats(scores) {
|
|
69
|
-
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
70
|
-
const sorted = [...scores].sort((a, b) => a - b);
|
|
71
|
-
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
72
|
-
const min = sorted[0];
|
|
73
|
-
const max = sorted[sorted.length - 1];
|
|
74
|
-
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
75
|
-
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
76
|
-
return { mean: round2(mean), min: round2(min), max: round2(max), p50: round2(p50), p95: round2(p95) };
|
|
77
|
-
}
|
|
78
|
-
function round2(n) {
|
|
79
|
-
return Math.round(n * 1e3) / 1e3;
|
|
80
|
-
}
|
|
81
|
-
function scorerCounts(items, name) {
|
|
82
|
-
let scored = 0;
|
|
83
|
-
let failed = 0;
|
|
84
|
-
let skipped = 0;
|
|
85
|
-
for (const i of items) {
|
|
86
|
-
if (i.error) continue;
|
|
87
|
-
if (i.scores[name] != null) scored++;
|
|
88
|
-
else if (i.scoreDetails?.[name]?.skipped === true) skipped++;
|
|
89
|
-
else if (i.scoreDetails?.[name]?.duration != null) failed++;
|
|
90
|
-
}
|
|
91
|
-
return { scored, failed, skipped };
|
|
92
|
-
}
|
|
93
|
-
function evaluateScorerTolerance(scored, failed, type, limit) {
|
|
94
|
-
const attempted = scored + failed;
|
|
95
|
-
const rate = attempted === 0 ? 0 : failed / attempted;
|
|
96
|
-
const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
|
|
97
|
-
return { attempted, rate, exceeds, zeroSample: attempted === 0 };
|
|
98
|
-
}
|
|
99
|
-
async function mapWithConcurrency(items, concurrency, task) {
|
|
100
|
-
const results = new Array(items.length);
|
|
101
|
-
const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
|
|
102
|
-
const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
|
|
103
|
-
let next = 0;
|
|
104
|
-
async function runNext() {
|
|
105
|
-
while (next < items.length) {
|
|
106
|
-
const i = next++;
|
|
107
|
-
results[i] = await task(items[i], i);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
await Promise.all(Array.from({ length: workerCount }, () => runNext()));
|
|
111
|
-
return results;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// src/compare.ts
|
|
115
|
-
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
116
|
-
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
117
|
-
var LEGACY_THRESHOLD = 0.1;
|
|
118
|
-
function resolveThreshold(scorerName, options, metadata) {
|
|
119
|
-
if (typeof options?.thresholds === "number") return options.thresholds;
|
|
120
|
-
if (options?.thresholds && typeof options.thresholds === "object") {
|
|
121
|
-
const perScorer = options.thresholds[scorerName];
|
|
122
|
-
if (perScorer != null) return perScorer;
|
|
123
|
-
}
|
|
124
|
-
const scorerTypes = metadata.scorerTypes;
|
|
125
|
-
if (scorerTypes && scorerName in scorerTypes) {
|
|
126
|
-
return scorerTypes[scorerName] === "llm" ? DEFAULT_LLM_THRESHOLD : DEFAULT_DETERMINISTIC_THRESHOLD;
|
|
127
|
-
}
|
|
128
|
-
return LEGACY_THRESHOLD;
|
|
129
|
-
}
|
|
130
|
-
function evalCompare(baseline, candidate, options) {
|
|
131
|
-
const baselineRunsRaw = Array.isArray(baseline) ? baseline : [baseline];
|
|
132
|
-
const candidateRunsRaw = Array.isArray(candidate) ? candidate : [candidate];
|
|
133
|
-
if (baselineRunsRaw.length === 0 || candidateRunsRaw.length === 0) {
|
|
134
|
-
throw new Error("Cannot compare empty eval result arrays");
|
|
135
|
-
}
|
|
136
|
-
const runCount = Math.min(baselineRunsRaw.length, candidateRunsRaw.length);
|
|
137
|
-
const baselineRuns = baselineRunsRaw.slice(0, runCount);
|
|
138
|
-
const candidateRuns = candidateRunsRaw.slice(0, runCount);
|
|
139
|
-
const baselineRef = baselineRunsRaw[0];
|
|
140
|
-
const candidateRef = candidateRunsRaw[0];
|
|
141
|
-
if (baselineRef.dataset !== candidateRef.dataset) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Cannot compare evals from different datasets: "${baselineRef.dataset}" vs "${candidateRef.dataset}"`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
const baselineScorerNames = Object.keys(baselineRef.summary.scorers).sort();
|
|
147
|
-
const candidateScorerNames = Object.keys(candidateRef.summary.scorers).sort();
|
|
148
|
-
if (JSON.stringify(baselineScorerNames) !== JSON.stringify(candidateScorerNames)) {
|
|
149
|
-
throw new Error(`Cannot compare evals with different scorers`);
|
|
150
|
-
}
|
|
151
|
-
const pairedDiffs = {};
|
|
152
|
-
for (const name of baselineScorerNames) {
|
|
153
|
-
pairedDiffs[name] = [];
|
|
154
|
-
}
|
|
155
|
-
for (let r = 0; r < runCount; r++) {
|
|
156
|
-
const bRun = baselineRuns[r];
|
|
157
|
-
const cRun = candidateRuns[r];
|
|
158
|
-
const minLength = Math.min(bRun.items.length, cRun.items.length);
|
|
159
|
-
for (let i = 0; i < minLength; i++) {
|
|
160
|
-
const bItem = bRun.items[i];
|
|
161
|
-
const cItem = cRun.items[i];
|
|
162
|
-
if (bItem.error || cItem.error) continue;
|
|
163
|
-
for (const name of baselineScorerNames) {
|
|
164
|
-
const bScore = bItem.scores[name];
|
|
165
|
-
const cScore = cItem.scores[name];
|
|
166
|
-
if (bScore != null && cScore != null) {
|
|
167
|
-
pairedDiffs[name].push(cScore - bScore);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const baselineMeans = {};
|
|
173
|
-
const candidateMeans = {};
|
|
174
|
-
for (const name of baselineScorerNames) {
|
|
175
|
-
baselineMeans[name] = baselineRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / baselineRuns.length;
|
|
176
|
-
candidateMeans[name] = candidateRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / candidateRuns.length;
|
|
177
|
-
}
|
|
178
|
-
const scorers = {};
|
|
179
|
-
for (const name of baselineScorerNames) {
|
|
180
|
-
const bMeanRaw = baselineMeans[name];
|
|
181
|
-
const cMeanRaw = candidateMeans[name];
|
|
182
|
-
const deltaRaw = cMeanRaw - bMeanRaw;
|
|
183
|
-
const bMean = round2(bMeanRaw);
|
|
184
|
-
const cMean = round2(cMeanRaw);
|
|
185
|
-
const delta = round2(deltaRaw);
|
|
186
|
-
const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
|
|
187
|
-
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
188
|
-
let baselineScored = 0;
|
|
189
|
-
let baselineFailed = 0;
|
|
190
|
-
let baselineSkipped = 0;
|
|
191
|
-
for (const run of baselineRuns) {
|
|
192
|
-
const c = scorerCounts(run.items, name);
|
|
193
|
-
baselineScored += c.scored;
|
|
194
|
-
baselineFailed += c.failed;
|
|
195
|
-
baselineSkipped += c.skipped;
|
|
196
|
-
}
|
|
197
|
-
let candidateScored = 0;
|
|
198
|
-
let candidateFailed = 0;
|
|
199
|
-
let candidateSkipped = 0;
|
|
200
|
-
for (const run of candidateRuns) {
|
|
201
|
-
const c = scorerCounts(run.items, name);
|
|
202
|
-
candidateScored += c.scored;
|
|
203
|
-
candidateFailed += c.failed;
|
|
204
|
-
candidateSkipped += c.skipped;
|
|
205
|
-
}
|
|
206
|
-
const entry = {
|
|
207
|
-
baselineMean: bMean,
|
|
208
|
-
candidateMean: cMean,
|
|
209
|
-
delta,
|
|
210
|
-
deltaPercent,
|
|
211
|
-
baselineScored,
|
|
212
|
-
baselineFailed,
|
|
213
|
-
candidateScored,
|
|
214
|
-
candidateFailed,
|
|
215
|
-
baselineSkipped,
|
|
216
|
-
candidateSkipped
|
|
217
|
-
};
|
|
218
|
-
const diffs = pairedDiffs[name];
|
|
219
|
-
entry.n = diffs.length;
|
|
220
|
-
if (diffs.length >= 2) {
|
|
221
|
-
const ci = pairedBootstrapCI(diffs);
|
|
222
|
-
entry.ci = { lower: ci.lower, upper: ci.upper };
|
|
223
|
-
entry.pRegression = ci.pRegression;
|
|
224
|
-
entry.pImprovement = ci.pImprovement;
|
|
225
|
-
const ciExcludesZero = ci.lower > 0 || ci.upper < 0;
|
|
226
|
-
entry.significant = ciExcludesZero && Math.abs(ci.mean) >= threshold;
|
|
227
|
-
}
|
|
228
|
-
scorers[name] = entry;
|
|
229
|
-
}
|
|
230
|
-
const regressions = [];
|
|
231
|
-
const improvements = [];
|
|
232
|
-
const itemCount = Math.min(
|
|
233
|
-
...baselineRuns.map((r) => r.items.length),
|
|
234
|
-
...candidateRuns.map((r) => r.items.length)
|
|
235
|
-
);
|
|
236
|
-
for (let i = 0; i < itemCount; i++) {
|
|
237
|
-
const hasBaselineError = baselineRuns.some((r) => r.items[i]?.error);
|
|
238
|
-
const hasCandidateError = candidateRuns.some((r) => r.items[i]?.error);
|
|
239
|
-
if (hasBaselineError || hasCandidateError) continue;
|
|
240
|
-
for (const name of baselineScorerNames) {
|
|
241
|
-
const bScores = baselineRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
242
|
-
const cScores = candidateRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
243
|
-
if (bScores.length === 0 || cScores.length === 0) continue;
|
|
244
|
-
const bAvg = bScores.reduce((a, b) => a + b, 0) / bScores.length;
|
|
245
|
-
const cAvg = cScores.reduce((a, b) => a + b, 0) / cScores.length;
|
|
246
|
-
const delta = round2(cAvg - bAvg);
|
|
247
|
-
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
248
|
-
if (delta < -threshold)
|
|
249
|
-
regressions.push({
|
|
250
|
-
itemIndex: i,
|
|
251
|
-
input: baselineRef.items[i]?.input,
|
|
252
|
-
scorer: name,
|
|
253
|
-
baselineScore: round2(bAvg),
|
|
254
|
-
candidateScore: round2(cAvg),
|
|
255
|
-
delta
|
|
256
|
-
});
|
|
257
|
-
else if (delta > threshold)
|
|
258
|
-
improvements.push({
|
|
259
|
-
itemIndex: i,
|
|
260
|
-
input: baselineRef.items[i]?.input,
|
|
261
|
-
scorer: name,
|
|
262
|
-
baselineScore: round2(bAvg),
|
|
263
|
-
candidateScore: round2(cAvg),
|
|
264
|
-
delta
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
let timing;
|
|
269
|
-
const baselineTimings = baselineRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
270
|
-
const candidateTimings = candidateRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
271
|
-
if (baselineTimings.length > 0 && candidateTimings.length > 0) {
|
|
272
|
-
const bMean = baselineTimings.reduce((a, b) => a + b, 0) / baselineTimings.length;
|
|
273
|
-
const cMean = candidateTimings.reduce((a, b) => a + b, 0) / candidateTimings.length;
|
|
274
|
-
const delta = round2(cMean - bMean);
|
|
275
|
-
const deltaPercent = bMean > 0 ? round2((cMean - bMean) / bMean * 100) : 0;
|
|
276
|
-
timing = { baselineMean: round2(bMean), candidateMean: round2(cMean), delta, deltaPercent };
|
|
277
|
-
}
|
|
278
|
-
let cost;
|
|
279
|
-
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + r.totalCost, 0) / baselineRuns.length;
|
|
280
|
-
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + r.totalCost, 0) / candidateRuns.length;
|
|
281
|
-
if (baselineAvgCost > 0 || candidateAvgCost > 0) {
|
|
282
|
-
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
283
|
-
const delta = round2(deltaRaw);
|
|
284
|
-
const deltaPercent = baselineAvgCost > 0 ? round2(deltaRaw / baselineAvgCost * 100) : 0;
|
|
285
|
-
cost = {
|
|
286
|
-
baselineTotal: round2(baselineAvgCost),
|
|
287
|
-
candidateTotal: round2(candidateAvgCost),
|
|
288
|
-
delta,
|
|
289
|
-
deltaPercent
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
const parts = [];
|
|
293
|
-
for (const name of baselineScorerNames) {
|
|
294
|
-
const s = scorers[name];
|
|
295
|
-
if (Math.abs(s.delta) > 1e-3) {
|
|
296
|
-
const direction = s.delta > 0 ? "improves" : "regresses";
|
|
297
|
-
const sign = s.delta > 0 ? "+" : "";
|
|
298
|
-
let sigLabel = "";
|
|
299
|
-
if (s.significant === true) sigLabel = " (significant)";
|
|
300
|
-
else if (s.significant === false) sigLabel = " (not significant)";
|
|
301
|
-
parts.push(
|
|
302
|
-
`'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})${sigLabel}`
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (timing && Math.abs(timing.deltaPercent) > 1) {
|
|
307
|
-
const dir = timing.delta > 0 ? "slower" : "faster";
|
|
308
|
-
parts.push(`${Math.abs(timing.deltaPercent).toFixed(0)}% ${dir}`);
|
|
309
|
-
}
|
|
310
|
-
if (cost && Math.abs(cost.deltaPercent) > 1) {
|
|
311
|
-
const dir = cost.delta > 0 ? "more expensive" : "cheaper";
|
|
312
|
-
parts.push(`${Math.abs(cost.deltaPercent).toFixed(0)}% ${dir}`);
|
|
313
|
-
}
|
|
314
|
-
const summaryStr = `candidate ${parts.length > 0 ? parts.join(", ") : "no meaningful changes"} with ${regressions.length} regressions and ${improvements.length} improvements`;
|
|
315
|
-
const baselinePartial = detectPartial(baselineRunsRaw);
|
|
316
|
-
const candidatePartial = detectPartial(candidateRunsRaw);
|
|
317
|
-
return {
|
|
318
|
-
baseline: {
|
|
319
|
-
id: baselineRef.id,
|
|
320
|
-
metadata: baselineRef.metadata,
|
|
321
|
-
runCount,
|
|
322
|
-
...baselinePartial ? { partial: baselinePartial } : {}
|
|
323
|
-
},
|
|
324
|
-
candidate: {
|
|
325
|
-
id: candidateRef.id,
|
|
326
|
-
metadata: candidateRef.metadata,
|
|
327
|
-
runCount,
|
|
328
|
-
...candidatePartial ? { partial: candidatePartial } : {}
|
|
329
|
-
},
|
|
330
|
-
scorers,
|
|
331
|
-
timing,
|
|
332
|
-
cost,
|
|
333
|
-
regressions,
|
|
334
|
-
improvements,
|
|
335
|
-
summary: summaryStr
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
function detectPartial(runs) {
|
|
339
|
-
if (runs.length === 0) return void 0;
|
|
340
|
-
let attempted;
|
|
341
|
-
for (const r of runs) {
|
|
342
|
-
const v = r.metadata?.batchAttempted;
|
|
343
|
-
if (typeof v === "number" && Number.isFinite(v)) {
|
|
344
|
-
attempted = v;
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (attempted === void 0) return void 0;
|
|
349
|
-
if (runs.length >= attempted) return void 0;
|
|
350
|
-
return { completed: runs.length, attempted };
|
|
351
|
-
}
|
|
352
|
-
function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
|
|
353
|
-
const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
|
|
354
|
-
for (const name of Object.keys(comparison.scorers)) {
|
|
355
|
-
const s = comparison.scorers[name];
|
|
356
|
-
const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
|
|
357
|
-
const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
|
|
358
|
-
for (const side of ["baseline", "candidate"]) {
|
|
359
|
-
const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
|
|
360
|
-
const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
|
|
361
|
-
const skipped = (side === "baseline" ? s.baselineSkipped : s.candidateSkipped) ?? 0;
|
|
362
|
-
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
363
|
-
if (verdict.zeroSample) {
|
|
364
|
-
const cause = skipped > 0 ? `every one of its items was N/A (the \`applies\` predicate skipped all ${skipped})` : `its items errored or produced no score`;
|
|
365
|
-
return `scorer "${name}" has no scored items on ${side} (${cause}) \u2014 cannot certify a mean over an empty sample.`;
|
|
366
|
-
}
|
|
367
|
-
if (verdict.exceeds) {
|
|
368
|
-
const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
|
|
369
|
-
return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// src/scorer.ts
|
|
377
|
-
function normalizeScorerResult(result) {
|
|
378
|
-
return typeof result === "number" ? { score: result } : result;
|
|
379
|
-
}
|
|
380
|
-
function attachScorerErrorCost(err, cost) {
|
|
381
|
-
if (err && typeof err === "object" && typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
382
|
-
err.cost = cost;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
function extractScorerErrorCost(err) {
|
|
386
|
-
const v = err && typeof err === "object" ? err.cost : void 0;
|
|
387
|
-
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
388
|
-
}
|
|
389
|
-
function scorer(config) {
|
|
390
|
-
return {
|
|
391
|
-
name: config.name,
|
|
392
|
-
description: config.description,
|
|
393
|
-
isLlm: false,
|
|
394
|
-
score: config.score,
|
|
395
|
-
applies: config.applies
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// src/score-item.ts
|
|
400
|
-
async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal) {
|
|
401
|
-
const scoreDetails = item.scoreDetails ??= {};
|
|
402
|
-
for (const s of scorers) {
|
|
403
|
-
item.scores[s.name] = null;
|
|
404
|
-
scoreDetails[s.name] = { score: null };
|
|
405
|
-
}
|
|
406
|
-
let itemScorerCost = 0;
|
|
407
|
-
const scorerErrorsByName = {};
|
|
408
|
-
await mapWithConcurrency(scorers, scorerConcurrency, async (scorer2) => {
|
|
409
|
-
if (signal?.aborted) return;
|
|
410
|
-
const scorerStart = Date.now();
|
|
411
|
-
try {
|
|
412
|
-
if (scorer2.applies && !scorer2.applies(item.output, item.input, item.annotations)) {
|
|
413
|
-
item.scores[scorer2.name] = null;
|
|
414
|
-
scoreDetails[scorer2.name] = { score: null, skipped: true };
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
const raw = await scorer2.score(item.output, item.input, item.annotations, scorerContext);
|
|
418
|
-
const scorerResult = normalizeScorerResult(raw);
|
|
419
|
-
if (scorerResult.cost != null) {
|
|
420
|
-
itemScorerCost += scorerResult.cost;
|
|
421
|
-
}
|
|
422
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
423
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
424
|
-
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`;
|
|
425
|
-
item.scores[scorer2.name] = null;
|
|
426
|
-
scoreDetails[scorer2.name] = {
|
|
427
|
-
score: null,
|
|
428
|
-
metadata: scorerResult.metadata,
|
|
429
|
-
duration: scorerDuration,
|
|
430
|
-
cost: scorerResult.cost
|
|
431
|
-
};
|
|
432
|
-
} else {
|
|
433
|
-
item.scores[scorer2.name] = round2(scorerResult.score);
|
|
434
|
-
scoreDetails[scorer2.name] = {
|
|
435
|
-
score: round2(scorerResult.score),
|
|
436
|
-
metadata: scorerResult.metadata,
|
|
437
|
-
duration: scorerDuration,
|
|
438
|
-
cost: scorerResult.cost
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
} catch (err) {
|
|
442
|
-
if (err?.name === "AbortError") return;
|
|
443
|
-
const errCost = extractScorerErrorCost(err);
|
|
444
|
-
if (errCost != null) itemScorerCost += errCost;
|
|
445
|
-
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
446
|
-
item.scores[scorer2.name] = null;
|
|
447
|
-
scoreDetails[scorer2.name] = {
|
|
448
|
-
score: null,
|
|
449
|
-
duration: Date.now() - scorerStart,
|
|
450
|
-
cost: errCost
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
|
|
455
|
-
if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
|
|
456
|
-
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
457
|
-
return itemScorerCost;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// src/runner.ts
|
|
461
|
-
import { randomUUID } from "crypto";
|
|
462
|
-
function parseCost(cost) {
|
|
463
|
-
const match = cost.match(/^\$?([\d.]+)$/);
|
|
464
|
-
if (!match) throw new Error(`Invalid cost format: "${cost}"`);
|
|
465
|
-
return parseFloat(match[1]);
|
|
466
|
-
}
|
|
467
|
-
function extractUserCost(result, label = "executeWorkflow") {
|
|
468
|
-
if (result === null || typeof result !== "object") return void 0;
|
|
469
|
-
const raw = result.cost;
|
|
470
|
-
if (raw === void 0) return void 0;
|
|
471
|
-
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) return raw;
|
|
472
|
-
console.warn(
|
|
473
|
-
`[axl-eval] Ignoring invalid \`cost\` from ${label} return: expected non-negative finite number, got ${typeof raw === "number" ? String(raw) : typeof raw}. Falling back to tracked cost (or undefined).`
|
|
474
|
-
);
|
|
475
|
-
return void 0;
|
|
476
|
-
}
|
|
477
|
-
function extractUserMetadata(result, label = "executeWorkflow") {
|
|
478
|
-
if (result === null || typeof result !== "object") return void 0;
|
|
479
|
-
const meta = result.metadata;
|
|
480
|
-
if (meta === void 0) return void 0;
|
|
481
|
-
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) {
|
|
482
|
-
console.warn(
|
|
483
|
-
`[axl-eval] Ignoring invalid \`metadata\` from ${label} return: expected plain object, got ${meta === null ? "null" : Array.isArray(meta) ? "array" : typeof meta}.`
|
|
484
|
-
);
|
|
485
|
-
return void 0;
|
|
486
|
-
}
|
|
487
|
-
const proto = Object.getPrototypeOf(meta);
|
|
488
|
-
if (proto !== Object.prototype && proto !== null) {
|
|
489
|
-
console.warn(
|
|
490
|
-
`[axl-eval] Ignoring invalid \`metadata\` from ${label} return: expected plain object, got ${proto?.constructor?.name ?? "exotic object"}.`
|
|
491
|
-
);
|
|
492
|
-
return void 0;
|
|
493
|
-
}
|
|
494
|
-
return meta;
|
|
495
|
-
}
|
|
496
|
-
function mergeMetadata(tracked, user) {
|
|
497
|
-
const merged = { ...tracked, ...user };
|
|
498
|
-
for (const [list, counts] of [
|
|
499
|
-
["models", "modelCallCounts"],
|
|
500
|
-
["workflows", "workflowCallCounts"]
|
|
501
|
-
]) {
|
|
502
|
-
if (user && Object.hasOwn(user, list) && !Object.hasOwn(user, counts)) {
|
|
503
|
-
delete merged[counts];
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
return merged;
|
|
507
|
-
}
|
|
508
|
-
async function runEval(config, executeWorkflow, runtime, options) {
|
|
509
|
-
const startTime = Date.now();
|
|
510
|
-
const id = randomUUID();
|
|
511
|
-
const items = await config.dataset.getItems();
|
|
512
|
-
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
513
|
-
const concurrency = config.concurrency ?? 5;
|
|
514
|
-
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
515
|
-
const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
|
|
516
|
-
const scorerContext = {
|
|
517
|
-
resolveProvider: (uri) => {
|
|
518
|
-
if (typeof runtime.resolveProvider !== "function") {
|
|
519
|
-
throw new Error(
|
|
520
|
-
`LLM scorers require a runtime with resolveProvider(). Ensure you are using a real AxlRuntime instance, not a mock.`
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
return runtime.resolveProvider(uri);
|
|
524
|
-
},
|
|
525
|
-
signal: options?.signal
|
|
526
|
-
};
|
|
527
|
-
const evalItems = new Array(items.length);
|
|
528
|
-
let totalCost = 0;
|
|
529
|
-
let budgetExceeded = false;
|
|
530
|
-
const runCallSamples = /* @__PURE__ */ new Map();
|
|
531
|
-
function absorbTiming(rollup, evalItem) {
|
|
532
|
-
if (!rollup) return;
|
|
533
|
-
const perItem = {};
|
|
534
|
-
for (const [model, bucket] of Object.entries(rollup)) {
|
|
535
|
-
const sums = {
|
|
536
|
-
calls: bucket.calls,
|
|
537
|
-
queuedMs: bucket.queuedMs,
|
|
538
|
-
retryMs: bucket.retryMs,
|
|
539
|
-
wireMs: bucket.wireMs,
|
|
540
|
-
...bucket.firstTokenMs !== void 0 && bucket.firstTokenCalls !== void 0 ? { firstTokenMs: bucket.firstTokenMs, firstTokenCalls: bucket.firstTokenCalls } : {}
|
|
541
|
-
};
|
|
542
|
-
perItem[model] = sums;
|
|
543
|
-
const samples = bucket.samples;
|
|
544
|
-
if (!samples) continue;
|
|
545
|
-
const pooled = runCallSamples.get(model) ?? [];
|
|
546
|
-
for (const sample of samples) pooled.push(sample);
|
|
547
|
-
runCallSamples.set(model, pooled);
|
|
548
|
-
}
|
|
549
|
-
evalItem.timing = perItem;
|
|
550
|
-
}
|
|
551
|
-
async function processItem(item, itemIndex) {
|
|
552
|
-
if (options?.signal?.aborted) {
|
|
553
|
-
evalItems[itemIndex] = {
|
|
554
|
-
input: item.input,
|
|
555
|
-
annotations: item.annotations,
|
|
556
|
-
output: null,
|
|
557
|
-
error: "Cancelled",
|
|
558
|
-
scores: {}
|
|
559
|
-
};
|
|
560
|
-
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
if (budgetExceeded) {
|
|
564
|
-
evalItems[itemIndex] = {
|
|
565
|
-
input: item.input,
|
|
566
|
-
annotations: item.annotations,
|
|
567
|
-
output: null,
|
|
568
|
-
error: "Budget exceeded",
|
|
569
|
-
scores: {}
|
|
570
|
-
};
|
|
571
|
-
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
const evalItem = {
|
|
575
|
-
input: item.input,
|
|
576
|
-
annotations: item.annotations,
|
|
577
|
-
output: null,
|
|
578
|
-
scores: {}
|
|
579
|
-
};
|
|
580
|
-
const itemStart = Date.now();
|
|
581
|
-
try {
|
|
582
|
-
if (options?.captureTraces) {
|
|
583
|
-
const tracked = await runtime.trackExecution(
|
|
584
|
-
async () => executeWorkflow(item.input, runtime),
|
|
585
|
-
{ captureTraces: true, captureTimingSamples: true }
|
|
586
|
-
);
|
|
587
|
-
evalItem.duration = Date.now() - itemStart;
|
|
588
|
-
evalItem.output = tracked.result.output;
|
|
589
|
-
evalItem.cost = extractUserCost(tracked.result) ?? tracked.cost;
|
|
590
|
-
if (tracked.unpriced) evalItem.unpriced = true;
|
|
591
|
-
evalItem.metadata = mergeMetadata(tracked.metadata, extractUserMetadata(tracked.result));
|
|
592
|
-
absorbTiming(tracked.modelTiming, evalItem);
|
|
593
|
-
if (tracked.traces && tracked.traces.length > 0) {
|
|
594
|
-
evalItem.traces = tracked.traces;
|
|
595
|
-
}
|
|
596
|
-
if (evalItem.cost != null) {
|
|
597
|
-
totalCost += evalItem.cost;
|
|
598
|
-
}
|
|
599
|
-
} else {
|
|
600
|
-
let trackedTiming;
|
|
601
|
-
let trackedMetadata;
|
|
602
|
-
let result;
|
|
603
|
-
if (typeof runtime.trackExecution === "function") {
|
|
604
|
-
const tracked = await runtime.trackExecution(
|
|
605
|
-
async () => executeWorkflow(item.input, runtime),
|
|
606
|
-
{ captureTraces: false, captureTimingSamples: true }
|
|
607
|
-
);
|
|
608
|
-
result = tracked.result;
|
|
609
|
-
trackedTiming = tracked.modelTiming;
|
|
610
|
-
trackedMetadata = tracked.metadata;
|
|
611
|
-
} else {
|
|
612
|
-
result = await executeWorkflow(item.input, runtime);
|
|
613
|
-
}
|
|
614
|
-
evalItem.duration = Date.now() - itemStart;
|
|
615
|
-
evalItem.output = result.output;
|
|
616
|
-
evalItem.cost = extractUserCost(result);
|
|
617
|
-
const meta = extractUserMetadata(result);
|
|
618
|
-
if (trackedMetadata || meta) evalItem.metadata = mergeMetadata(trackedMetadata, meta);
|
|
619
|
-
absorbTiming(trackedTiming, evalItem);
|
|
620
|
-
if (evalItem.cost != null) {
|
|
621
|
-
totalCost += evalItem.cost;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
} catch (err) {
|
|
625
|
-
evalItem.duration = Date.now() - itemStart;
|
|
626
|
-
evalItem.error = err instanceof Error ? err.message : String(err);
|
|
627
|
-
if (options?.captureTraces) {
|
|
628
|
-
const captured = err.axlCapturedTraces;
|
|
629
|
-
if (Array.isArray(captured) && captured.length > 0) {
|
|
630
|
-
evalItem.traces = captured;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
evalItems[itemIndex] = evalItem;
|
|
634
|
-
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
638
|
-
budgetExceeded = true;
|
|
639
|
-
}
|
|
640
|
-
const itemScorerCost = await scoreItem(
|
|
641
|
-
evalItem,
|
|
642
|
-
config.scorers,
|
|
643
|
-
scorerConcurrency,
|
|
644
|
-
scorerContext,
|
|
645
|
-
options?.signal
|
|
646
|
-
);
|
|
647
|
-
totalCost += itemScorerCost;
|
|
648
|
-
evalItems[itemIndex] = evalItem;
|
|
649
|
-
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
650
|
-
}
|
|
651
|
-
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
652
|
-
const failures = evalItems.filter((i) => i.error).length;
|
|
653
|
-
const scorerNames = config.scorers.map((s) => s.name);
|
|
654
|
-
const scorerStats = {};
|
|
655
|
-
for (const name of scorerNames) {
|
|
656
|
-
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
657
|
-
const { scored, failed, skipped } = scorerCounts(evalItems, name);
|
|
658
|
-
scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
|
|
659
|
-
}
|
|
660
|
-
const scorerTypes = {};
|
|
661
|
-
for (const s of config.scorers) {
|
|
662
|
-
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
663
|
-
}
|
|
664
|
-
let degraded;
|
|
665
|
-
const rawLimit = config.failOnScorerErrorRate;
|
|
666
|
-
if (rawLimit != null) {
|
|
667
|
-
if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
|
|
668
|
-
console.warn(
|
|
669
|
-
`[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
|
|
670
|
-
);
|
|
671
|
-
} else {
|
|
672
|
-
for (const name of scorerNames) {
|
|
673
|
-
const stats = scorerStats[name];
|
|
674
|
-
const scored = stats.scored ?? 0;
|
|
675
|
-
const failed = stats.failed ?? 0;
|
|
676
|
-
const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
|
|
677
|
-
const limit = type === "deterministic" ? 0 : rawLimit;
|
|
678
|
-
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
679
|
-
if (verdict.exceeds) {
|
|
680
|
-
(degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
686
|
-
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
687
|
-
const modelTiming = runCallSamples.size > 0 ? Object.fromEntries(
|
|
688
|
-
[...runCallSamples.entries()].map(([model, samples]) => {
|
|
689
|
-
const firstToken = samples.map((s) => s.firstTokenMs).filter((v) => v != null);
|
|
690
|
-
const stats = {
|
|
691
|
-
calls: samples.length,
|
|
692
|
-
wireMs: computeStats(samples.map((s) => s.wireMs)),
|
|
693
|
-
queuedMs: computeStats(samples.map((s) => s.queuedMs)),
|
|
694
|
-
retryMs: computeStats(samples.map((s) => s.retryMs)),
|
|
695
|
-
...firstToken.length > 0 ? { firstTokenMs: computeStats(firstToken), firstTokenCalls: firstToken.length } : {}
|
|
696
|
-
};
|
|
697
|
-
return [model, stats];
|
|
698
|
-
})
|
|
699
|
-
) : void 0;
|
|
700
|
-
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
701
|
-
for (const item of evalItems) {
|
|
702
|
-
const itemCounts = item.metadata?.modelCallCounts;
|
|
703
|
-
if (itemCounts && typeof itemCounts === "object") {
|
|
704
|
-
for (const [m, count] of Object.entries(itemCounts)) {
|
|
705
|
-
if (typeof count === "number")
|
|
706
|
-
totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + count);
|
|
707
|
-
}
|
|
708
|
-
} else {
|
|
709
|
-
const itemModels = item.metadata?.models;
|
|
710
|
-
if (Array.isArray(itemModels)) {
|
|
711
|
-
for (const m of itemModels) {
|
|
712
|
-
if (typeof m === "string") totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + 1);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
const modelsMeta = {};
|
|
718
|
-
if (totalModelCalls.size > 0) {
|
|
719
|
-
const sorted = [...totalModelCalls.entries()].sort((a, b) => b[1] - a[1]);
|
|
720
|
-
modelsMeta.models = sorted.map(([m]) => m);
|
|
721
|
-
modelsMeta.modelCounts = Object.fromEntries(sorted);
|
|
722
|
-
}
|
|
723
|
-
const totalWorkflowCalls = /* @__PURE__ */ new Map();
|
|
724
|
-
for (const item of evalItems) {
|
|
725
|
-
const itemCounts = item.metadata?.workflowCallCounts;
|
|
726
|
-
if (itemCounts && typeof itemCounts === "object") {
|
|
727
|
-
for (const [w, count] of Object.entries(itemCounts)) {
|
|
728
|
-
if (typeof count === "number")
|
|
729
|
-
totalWorkflowCalls.set(w, (totalWorkflowCalls.get(w) ?? 0) + count);
|
|
730
|
-
}
|
|
731
|
-
} else {
|
|
732
|
-
const itemWorkflows = item.metadata?.workflows;
|
|
733
|
-
if (Array.isArray(itemWorkflows)) {
|
|
734
|
-
for (const w of itemWorkflows) {
|
|
735
|
-
if (typeof w === "string")
|
|
736
|
-
totalWorkflowCalls.set(w, (totalWorkflowCalls.get(w) ?? 0) + 1);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
const workflowsMeta = {};
|
|
742
|
-
if (totalWorkflowCalls.size > 0) {
|
|
743
|
-
const sorted = [...totalWorkflowCalls.entries()].sort((a, b) => b[1] - a[1]);
|
|
744
|
-
workflowsMeta.workflows = sorted.map(([w]) => w);
|
|
745
|
-
workflowsMeta.workflowCounts = Object.fromEntries(sorted);
|
|
746
|
-
} else if (config.workflow) {
|
|
747
|
-
workflowsMeta.workflows = [config.workflow];
|
|
748
|
-
workflowsMeta.workflowCounts = { [config.workflow]: items.length };
|
|
749
|
-
}
|
|
750
|
-
options?.onProgress?.({ type: "run_done", totalItems: items.length, failures });
|
|
751
|
-
return {
|
|
752
|
-
id,
|
|
753
|
-
dataset: config.dataset.name,
|
|
754
|
-
metadata: {
|
|
755
|
-
...config.metadata,
|
|
756
|
-
scorerTypes,
|
|
757
|
-
...modelsMeta,
|
|
758
|
-
...workflowsMeta,
|
|
759
|
-
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
760
|
-
},
|
|
761
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
762
|
-
totalCost,
|
|
763
|
-
...evalItems.some((item) => item.unpriced) ? { unpriced: true } : {},
|
|
764
|
-
duration: Date.now() - startTime,
|
|
765
|
-
items: evalItems,
|
|
766
|
-
summary: {
|
|
767
|
-
count: items.length,
|
|
768
|
-
failures,
|
|
769
|
-
scorers: scorerStats,
|
|
770
|
-
timing,
|
|
771
|
-
...modelTiming ? { modelTiming } : {},
|
|
772
|
-
...degraded ? { degraded } : {}
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// src/rescore.ts
|
|
778
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
779
|
-
async function rescore(result, scorers, runtime, options) {
|
|
780
|
-
const startTime = Date.now();
|
|
781
|
-
const concurrency = options?.concurrency ?? 5;
|
|
782
|
-
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
783
|
-
const scorerContext = {
|
|
784
|
-
resolveProvider: (uri) => {
|
|
785
|
-
if (typeof runtime.resolveProvider !== "function") {
|
|
786
|
-
throw new Error(
|
|
787
|
-
`LLM scorers require a runtime with resolveProvider(). Ensure you are using a real AxlRuntime instance, not a mock.`
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
return runtime.resolveProvider(uri);
|
|
791
|
-
},
|
|
792
|
-
signal: options?.signal
|
|
793
|
-
};
|
|
794
|
-
const rescored = new Array(result.items.length);
|
|
795
|
-
let totalCost = 0;
|
|
796
|
-
async function rescoreItem(original, itemIndex) {
|
|
797
|
-
if (options?.signal?.aborted) {
|
|
798
|
-
rescored[itemIndex] = {
|
|
799
|
-
input: original.input,
|
|
800
|
-
annotations: original.annotations,
|
|
801
|
-
output: original.output,
|
|
802
|
-
error: "cancelled",
|
|
803
|
-
scores: {}
|
|
804
|
-
};
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
if (original.error) {
|
|
808
|
-
rescored[itemIndex] = {
|
|
809
|
-
input: original.input,
|
|
810
|
-
annotations: original.annotations,
|
|
811
|
-
output: original.output,
|
|
812
|
-
error: original.error,
|
|
813
|
-
scores: {}
|
|
814
|
-
};
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
const item = {
|
|
818
|
-
input: original.input,
|
|
819
|
-
annotations: original.annotations,
|
|
820
|
-
output: original.output,
|
|
821
|
-
metadata: original.metadata,
|
|
822
|
-
traces: original.traces,
|
|
823
|
-
scores: {},
|
|
824
|
-
scoreDetails: {}
|
|
825
|
-
};
|
|
826
|
-
const itemScorerCost = await scoreItem(
|
|
827
|
-
item,
|
|
828
|
-
scorers,
|
|
829
|
-
scorerConcurrency,
|
|
830
|
-
scorerContext,
|
|
831
|
-
options?.signal
|
|
832
|
-
);
|
|
833
|
-
totalCost += itemScorerCost;
|
|
834
|
-
rescored[itemIndex] = item;
|
|
835
|
-
}
|
|
836
|
-
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
837
|
-
const failures = rescored.filter((i) => i.error).length;
|
|
838
|
-
const scorerNames = scorers.map((s) => s.name);
|
|
839
|
-
const scorerStats = {};
|
|
840
|
-
for (const name of scorerNames) {
|
|
841
|
-
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
842
|
-
const { scored, failed, skipped } = scorerCounts(rescored, name);
|
|
843
|
-
scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
|
|
844
|
-
}
|
|
845
|
-
const scorerTypes = {};
|
|
846
|
-
for (const s of scorers) {
|
|
847
|
-
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
848
|
-
}
|
|
849
|
-
return {
|
|
850
|
-
id: randomUUID2(),
|
|
851
|
-
dataset: result.dataset,
|
|
852
|
-
metadata: (() => {
|
|
853
|
-
const rest = { ...result.metadata };
|
|
854
|
-
delete rest.runGroupId;
|
|
855
|
-
delete rest.runIndex;
|
|
856
|
-
delete rest.scorerFiltered;
|
|
857
|
-
delete rest.scorersRun;
|
|
858
|
-
const merged = {
|
|
859
|
-
...rest,
|
|
860
|
-
rescored: true,
|
|
861
|
-
originalId: result.id,
|
|
862
|
-
scorerTypes
|
|
863
|
-
};
|
|
864
|
-
if (!Array.isArray(merged.workflows)) {
|
|
865
|
-
const legacyWorkflow = result.workflow;
|
|
866
|
-
if (typeof legacyWorkflow === "string" && legacyWorkflow) {
|
|
867
|
-
merged.workflows = [legacyWorkflow];
|
|
868
|
-
merged.workflowCounts = { [legacyWorkflow]: result.items.length };
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
return merged;
|
|
872
|
-
})(),
|
|
873
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
874
|
-
totalCost,
|
|
875
|
-
duration: Date.now() - startTime,
|
|
876
|
-
items: rescored,
|
|
877
|
-
summary: { count: result.items.length, failures, scorers: scorerStats }
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// src/multi-run.ts
|
|
882
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
883
|
-
function std(values, mean) {
|
|
884
|
-
if (values.length <= 1) return 0;
|
|
885
|
-
const sumSqDiffs = values.reduce((sum, v) => sum + (v - mean) ** 2, 0);
|
|
886
|
-
return Math.sqrt(sumSqDiffs / (values.length - 1));
|
|
887
|
-
}
|
|
888
|
-
function aggregateRuns(runs) {
|
|
889
|
-
if (runs.length === 0) throw new Error("Cannot aggregate zero runs");
|
|
890
|
-
const runGroupId = runs[0].metadata.runGroupId ?? randomUUID3();
|
|
891
|
-
const { dataset } = runs[0];
|
|
892
|
-
const seenWorkflows = /* @__PURE__ */ new Set();
|
|
893
|
-
const workflows = [];
|
|
894
|
-
for (const run of runs) {
|
|
895
|
-
const list = run.metadata.workflows;
|
|
896
|
-
if (Array.isArray(list)) {
|
|
897
|
-
for (const w of list) {
|
|
898
|
-
if (typeof w === "string" && !seenWorkflows.has(w)) {
|
|
899
|
-
seenWorkflows.add(w);
|
|
900
|
-
workflows.push(w);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
const totalCost = runs.reduce((sum, r) => sum + r.totalCost, 0);
|
|
906
|
-
const totalDuration = runs.reduce((sum, r) => sum + r.duration, 0);
|
|
907
|
-
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
908
|
-
const scorers = {};
|
|
909
|
-
for (const name of scorerNames) {
|
|
910
|
-
const contributing = runs.filter((r) => {
|
|
911
|
-
const sc = r.summary.scorers[name]?.scored;
|
|
912
|
-
return sc == null || sc > 0;
|
|
913
|
-
});
|
|
914
|
-
const meanSource = contributing.length > 0 ? contributing : runs;
|
|
915
|
-
const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
916
|
-
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
917
|
-
const minMean = Math.min(...means);
|
|
918
|
-
const maxMean = Math.max(...means);
|
|
919
|
-
const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
|
|
920
|
-
const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
|
|
921
|
-
const skipped = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.skipped ?? 0), 0);
|
|
922
|
-
scorers[name] = {
|
|
923
|
-
mean: round2(meanOfMeans),
|
|
924
|
-
std: round2(std(means, meanOfMeans)),
|
|
925
|
-
min: round2(minMean),
|
|
926
|
-
max: round2(maxMean),
|
|
927
|
-
scored,
|
|
928
|
-
failed,
|
|
929
|
-
skipped
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
let timing;
|
|
933
|
-
const timingMeans = runs.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
934
|
-
if (timingMeans.length === runs.length && timingMeans.length > 0) {
|
|
935
|
-
const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
|
|
936
|
-
timing = {
|
|
937
|
-
mean: round2(meanTiming),
|
|
938
|
-
std: round2(std(timingMeans, meanTiming))
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
return {
|
|
942
|
-
runGroupId,
|
|
943
|
-
runCount: runs.length,
|
|
944
|
-
workflows,
|
|
945
|
-
dataset,
|
|
946
|
-
totalCost: round2(totalCost),
|
|
947
|
-
totalDuration: round2(totalDuration),
|
|
948
|
-
scorers,
|
|
949
|
-
timing
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
export {
|
|
954
|
-
pairedBootstrapCI,
|
|
955
|
-
scorerCounts,
|
|
956
|
-
evaluateScorerTolerance,
|
|
957
|
-
evalCompare,
|
|
958
|
-
evaluateScorerErrorRateGate,
|
|
959
|
-
normalizeScorerResult,
|
|
960
|
-
attachScorerErrorCost,
|
|
961
|
-
scorer,
|
|
962
|
-
runEval,
|
|
963
|
-
rescore,
|
|
964
|
-
aggregateRuns
|
|
965
|
-
};
|
|
966
|
-
//# sourceMappingURL=chunk-AFUNZE7W.js.map
|