@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
package/dist/cli.cjs
CHANGED
|
@@ -28,76 +28,223 @@ var import_node_fs2 = require("fs");
|
|
|
28
28
|
var import_promises = require("fs/promises");
|
|
29
29
|
var path = __toESM(require("path"), 1);
|
|
30
30
|
|
|
31
|
-
// src/
|
|
31
|
+
// src/bootstrap.ts
|
|
32
|
+
function pairedBootstrapCI(differences, options) {
|
|
33
|
+
const n = differences.length;
|
|
34
|
+
if (n === 0) return { lower: 0, upper: 0, mean: 0, pRegression: 0, pImprovement: 0 };
|
|
35
|
+
const nResamples = options?.nResamples ?? 1e3;
|
|
36
|
+
const alpha = options?.alpha ?? 0.05;
|
|
37
|
+
const mean = differences.reduce((a, b) => a + b, 0) / n;
|
|
38
|
+
if (n === 1)
|
|
39
|
+
return {
|
|
40
|
+
lower: differences[0],
|
|
41
|
+
upper: differences[0],
|
|
42
|
+
mean,
|
|
43
|
+
pRegression: mean < 0 ? 1 : 0,
|
|
44
|
+
pImprovement: mean > 0 ? 1 : 0
|
|
45
|
+
};
|
|
46
|
+
if (nResamples <= 0)
|
|
47
|
+
return {
|
|
48
|
+
lower: round(mean),
|
|
49
|
+
upper: round(mean),
|
|
50
|
+
mean: round(mean),
|
|
51
|
+
pRegression: mean < 0 ? 1 : 0,
|
|
52
|
+
pImprovement: mean > 0 ? 1 : 0
|
|
53
|
+
};
|
|
54
|
+
const rng = options?.seed != null ? xorshift32(options.seed) : () => Math.random();
|
|
55
|
+
const resampleMeans = new Array(nResamples);
|
|
56
|
+
for (let r = 0; r < nResamples; r++) {
|
|
57
|
+
let sum = 0;
|
|
58
|
+
for (let i = 0; i < n; i++) {
|
|
59
|
+
sum += differences[Math.floor(rng() * n)];
|
|
60
|
+
}
|
|
61
|
+
resampleMeans[r] = sum / n;
|
|
62
|
+
}
|
|
63
|
+
resampleMeans.sort((a, b) => a - b);
|
|
64
|
+
const lowerIdx = Math.floor(alpha / 2 * nResamples);
|
|
65
|
+
const upperIdx = Math.floor((1 - alpha / 2) * nResamples) - 1;
|
|
66
|
+
let belowZero = 0;
|
|
67
|
+
for (let i = 0; i < nResamples; i++) {
|
|
68
|
+
if (resampleMeans[i] < 0) belowZero++;
|
|
69
|
+
else break;
|
|
70
|
+
}
|
|
71
|
+
let aboveZero = 0;
|
|
72
|
+
for (let i = nResamples - 1; i >= 0; i--) {
|
|
73
|
+
if (resampleMeans[i] > 0) aboveZero++;
|
|
74
|
+
else break;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
lower: round(resampleMeans[Math.max(0, lowerIdx)]),
|
|
78
|
+
upper: round(resampleMeans[Math.min(nResamples - 1, upperIdx)]),
|
|
79
|
+
mean: round(mean),
|
|
80
|
+
pRegression: round(belowZero / nResamples),
|
|
81
|
+
pImprovement: round(aboveZero / nResamples)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
32
84
|
function round(n) {
|
|
33
|
-
return Math.round(n *
|
|
85
|
+
return Math.round(n * 1e4) / 1e4;
|
|
86
|
+
}
|
|
87
|
+
function xorshift32(seed) {
|
|
88
|
+
let state = seed | 0 || 1;
|
|
89
|
+
return () => {
|
|
90
|
+
state ^= state << 13;
|
|
91
|
+
state ^= state >> 17;
|
|
92
|
+
state ^= state << 5;
|
|
93
|
+
return (state >>> 0) / 4294967296;
|
|
94
|
+
};
|
|
34
95
|
}
|
|
35
|
-
|
|
36
|
-
|
|
96
|
+
|
|
97
|
+
// src/compare.ts
|
|
98
|
+
function round2(n) {
|
|
99
|
+
return Math.round(n * 1e3) / 1e3;
|
|
100
|
+
}
|
|
101
|
+
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
102
|
+
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
103
|
+
var LEGACY_THRESHOLD = 0.1;
|
|
104
|
+
function resolveThreshold(scorerName, options, metadata) {
|
|
105
|
+
if (typeof options?.thresholds === "number") return options.thresholds;
|
|
106
|
+
if (options?.thresholds && typeof options.thresholds === "object") {
|
|
107
|
+
const perScorer = options.thresholds[scorerName];
|
|
108
|
+
if (perScorer != null) return perScorer;
|
|
109
|
+
}
|
|
110
|
+
const scorerTypes = metadata.scorerTypes;
|
|
111
|
+
if (scorerTypes && scorerName in scorerTypes) {
|
|
112
|
+
return scorerTypes[scorerName] === "llm" ? DEFAULT_LLM_THRESHOLD : DEFAULT_DETERMINISTIC_THRESHOLD;
|
|
113
|
+
}
|
|
114
|
+
return LEGACY_THRESHOLD;
|
|
115
|
+
}
|
|
116
|
+
function evalCompare(baseline, candidate, options) {
|
|
117
|
+
const baselineRuns = Array.isArray(baseline) ? baseline : [baseline];
|
|
118
|
+
const candidateRuns = Array.isArray(candidate) ? candidate : [candidate];
|
|
119
|
+
if (baselineRuns.length === 0 || candidateRuns.length === 0) {
|
|
120
|
+
throw new Error("Cannot compare empty eval result arrays");
|
|
121
|
+
}
|
|
122
|
+
const baselineRef = baselineRuns[0];
|
|
123
|
+
const candidateRef = candidateRuns[0];
|
|
124
|
+
if (baselineRef.dataset !== candidateRef.dataset) {
|
|
37
125
|
throw new Error(
|
|
38
|
-
`Cannot compare evals from different datasets: "${
|
|
126
|
+
`Cannot compare evals from different datasets: "${baselineRef.dataset}" vs "${candidateRef.dataset}"`
|
|
39
127
|
);
|
|
40
128
|
}
|
|
41
|
-
const baselineScorerNames = Object.keys(
|
|
42
|
-
const candidateScorerNames = Object.keys(
|
|
129
|
+
const baselineScorerNames = Object.keys(baselineRef.summary.scorers).sort();
|
|
130
|
+
const candidateScorerNames = Object.keys(candidateRef.summary.scorers).sort();
|
|
43
131
|
if (JSON.stringify(baselineScorerNames) !== JSON.stringify(candidateScorerNames)) {
|
|
44
132
|
throw new Error(`Cannot compare evals with different scorers`);
|
|
45
133
|
}
|
|
134
|
+
const pairedDiffs = {};
|
|
135
|
+
for (const name of baselineScorerNames) {
|
|
136
|
+
pairedDiffs[name] = [];
|
|
137
|
+
}
|
|
138
|
+
const runCount = Math.min(baselineRuns.length, candidateRuns.length);
|
|
139
|
+
for (let r = 0; r < runCount; r++) {
|
|
140
|
+
const bRun = baselineRuns[r];
|
|
141
|
+
const cRun = candidateRuns[r];
|
|
142
|
+
const minLength = Math.min(bRun.items.length, cRun.items.length);
|
|
143
|
+
for (let i = 0; i < minLength; i++) {
|
|
144
|
+
const bItem = bRun.items[i];
|
|
145
|
+
const cItem = cRun.items[i];
|
|
146
|
+
if (bItem.error || cItem.error) continue;
|
|
147
|
+
for (const name of baselineScorerNames) {
|
|
148
|
+
const bScore = bItem.scores[name];
|
|
149
|
+
const cScore = cItem.scores[name];
|
|
150
|
+
if (bScore != null && cScore != null) {
|
|
151
|
+
pairedDiffs[name].push(cScore - bScore);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const baselineMeans = {};
|
|
157
|
+
const candidateMeans = {};
|
|
158
|
+
for (const name of baselineScorerNames) {
|
|
159
|
+
baselineMeans[name] = baselineRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / baselineRuns.length;
|
|
160
|
+
candidateMeans[name] = candidateRuns.reduce((sum, r) => sum + (r.summary.scorers[name]?.mean ?? 0), 0) / candidateRuns.length;
|
|
161
|
+
}
|
|
46
162
|
const scorers = {};
|
|
47
163
|
for (const name of baselineScorerNames) {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
164
|
+
const bMeanRaw = baselineMeans[name];
|
|
165
|
+
const cMeanRaw = candidateMeans[name];
|
|
166
|
+
const deltaRaw = cMeanRaw - bMeanRaw;
|
|
167
|
+
const bMean = round2(bMeanRaw);
|
|
168
|
+
const cMean = round2(cMeanRaw);
|
|
169
|
+
const delta = round2(deltaRaw);
|
|
170
|
+
const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
|
|
171
|
+
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
172
|
+
const entry = {
|
|
173
|
+
baselineMean: bMean,
|
|
174
|
+
candidateMean: cMean,
|
|
175
|
+
delta,
|
|
176
|
+
deltaPercent
|
|
177
|
+
};
|
|
178
|
+
const diffs = pairedDiffs[name];
|
|
179
|
+
if (diffs.length >= 2) {
|
|
180
|
+
const ci = pairedBootstrapCI(diffs);
|
|
181
|
+
entry.ci = { lower: ci.lower, upper: ci.upper };
|
|
182
|
+
entry.pRegression = ci.pRegression;
|
|
183
|
+
entry.pImprovement = ci.pImprovement;
|
|
184
|
+
entry.n = diffs.length;
|
|
185
|
+
const ciExcludesZero = ci.lower > 0 || ci.upper < 0;
|
|
186
|
+
entry.significant = ciExcludesZero && Math.abs(ci.mean) >= threshold;
|
|
187
|
+
}
|
|
188
|
+
scorers[name] = entry;
|
|
53
189
|
}
|
|
54
190
|
const regressions = [];
|
|
55
191
|
const improvements = [];
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
192
|
+
const itemCount = Math.min(
|
|
193
|
+
...baselineRuns.map((r) => r.items.length),
|
|
194
|
+
...candidateRuns.map((r) => r.items.length)
|
|
195
|
+
);
|
|
196
|
+
for (let i = 0; i < itemCount; i++) {
|
|
197
|
+
const hasBaselineError = baselineRuns.some((r) => r.items[i]?.error);
|
|
198
|
+
const hasCandidateError = candidateRuns.some((r) => r.items[i]?.error);
|
|
199
|
+
if (hasBaselineError || hasCandidateError) continue;
|
|
61
200
|
for (const name of baselineScorerNames) {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
const
|
|
66
|
-
|
|
201
|
+
const bScores = baselineRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
202
|
+
const cScores = candidateRuns.map((r) => r.items[i]?.scores[name]).filter((s) => s != null);
|
|
203
|
+
if (bScores.length === 0 || cScores.length === 0) continue;
|
|
204
|
+
const bAvg = bScores.reduce((a, b) => a + b, 0) / bScores.length;
|
|
205
|
+
const cAvg = cScores.reduce((a, b) => a + b, 0) / cScores.length;
|
|
206
|
+
const delta = round2(cAvg - bAvg);
|
|
207
|
+
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
208
|
+
if (delta < -threshold)
|
|
67
209
|
regressions.push({
|
|
68
210
|
itemIndex: i,
|
|
69
|
-
input:
|
|
211
|
+
input: baselineRef.items[i]?.input,
|
|
70
212
|
scorer: name,
|
|
71
|
-
baselineScore:
|
|
72
|
-
candidateScore:
|
|
213
|
+
baselineScore: round2(bAvg),
|
|
214
|
+
candidateScore: round2(cAvg),
|
|
73
215
|
delta
|
|
74
216
|
});
|
|
75
|
-
else if (delta >
|
|
217
|
+
else if (delta > threshold)
|
|
76
218
|
improvements.push({
|
|
77
219
|
itemIndex: i,
|
|
78
|
-
input:
|
|
220
|
+
input: baselineRef.items[i]?.input,
|
|
79
221
|
scorer: name,
|
|
80
|
-
baselineScore:
|
|
81
|
-
candidateScore:
|
|
222
|
+
baselineScore: round2(bAvg),
|
|
223
|
+
candidateScore: round2(cAvg),
|
|
82
224
|
delta
|
|
83
225
|
});
|
|
84
226
|
}
|
|
85
227
|
}
|
|
86
228
|
let timing;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
229
|
+
const baselineTimings = baselineRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
230
|
+
const candidateTimings = candidateRuns.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
231
|
+
if (baselineTimings.length > 0 && candidateTimings.length > 0) {
|
|
232
|
+
const bMean = baselineTimings.reduce((a, b) => a + b, 0) / baselineTimings.length;
|
|
233
|
+
const cMean = candidateTimings.reduce((a, b) => a + b, 0) / candidateTimings.length;
|
|
234
|
+
const delta = round2(cMean - bMean);
|
|
235
|
+
const deltaPercent = bMean > 0 ? round2((cMean - bMean) / bMean * 100) : 0;
|
|
236
|
+
timing = { baselineMean: round2(bMean), candidateMean: round2(cMean), delta, deltaPercent };
|
|
93
237
|
}
|
|
94
238
|
let cost;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
239
|
+
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + r.totalCost, 0) / baselineRuns.length;
|
|
240
|
+
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + r.totalCost, 0) / candidateRuns.length;
|
|
241
|
+
if (baselineAvgCost > 0 || candidateAvgCost > 0) {
|
|
242
|
+
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
243
|
+
const delta = round2(deltaRaw);
|
|
244
|
+
const deltaPercent = baselineAvgCost > 0 ? round2(deltaRaw / baselineAvgCost * 100) : 0;
|
|
98
245
|
cost = {
|
|
99
|
-
baselineTotal:
|
|
100
|
-
candidateTotal:
|
|
246
|
+
baselineTotal: round2(baselineAvgCost),
|
|
247
|
+
candidateTotal: round2(candidateAvgCost),
|
|
101
248
|
delta,
|
|
102
249
|
deltaPercent
|
|
103
250
|
};
|
|
@@ -108,8 +255,11 @@ function evalCompare(baseline, candidate) {
|
|
|
108
255
|
if (Math.abs(s.delta) > 1e-3) {
|
|
109
256
|
const direction = s.delta > 0 ? "improves" : "regresses";
|
|
110
257
|
const sign = s.delta > 0 ? "+" : "";
|
|
258
|
+
let sigLabel = "";
|
|
259
|
+
if (s.significant === true) sigLabel = " (significant)";
|
|
260
|
+
else if (s.significant === false) sigLabel = " (not significant)";
|
|
111
261
|
parts.push(
|
|
112
|
-
`'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})`
|
|
262
|
+
`'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})${sigLabel}`
|
|
113
263
|
);
|
|
114
264
|
}
|
|
115
265
|
}
|
|
@@ -121,10 +271,10 @@ function evalCompare(baseline, candidate) {
|
|
|
121
271
|
const dir = cost.delta > 0 ? "more expensive" : "cheaper";
|
|
122
272
|
parts.push(`${Math.abs(cost.deltaPercent).toFixed(0)}% ${dir}`);
|
|
123
273
|
}
|
|
124
|
-
const summaryStr = `candidate ${parts.length > 0 ? parts.join(", ") : "no
|
|
274
|
+
const summaryStr = `candidate ${parts.length > 0 ? parts.join(", ") : "no meaningful changes"} with ${regressions.length} regressions and ${improvements.length} improvements`;
|
|
125
275
|
return {
|
|
126
|
-
baseline: { id:
|
|
127
|
-
candidate: { id:
|
|
276
|
+
baseline: { id: baselineRef.id, metadata: baselineRef.metadata },
|
|
277
|
+
candidate: { id: candidateRef.id, metadata: candidateRef.metadata },
|
|
128
278
|
scorers,
|
|
129
279
|
timing,
|
|
130
280
|
cost,
|
|
@@ -139,8 +289,7 @@ function normalizeScorerResult(result) {
|
|
|
139
289
|
return typeof result === "number" ? { score: result } : result;
|
|
140
290
|
}
|
|
141
291
|
|
|
142
|
-
// src/
|
|
143
|
-
var import_node_crypto = require("crypto");
|
|
292
|
+
// src/utils.ts
|
|
144
293
|
function computeStats(scores) {
|
|
145
294
|
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
146
295
|
const sorted = [...scores].sort((a, b) => a - b);
|
|
@@ -149,11 +298,14 @@ function computeStats(scores) {
|
|
|
149
298
|
const max = sorted[sorted.length - 1];
|
|
150
299
|
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
151
300
|
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
152
|
-
return { mean:
|
|
301
|
+
return { mean: round3(mean), min: round3(min), max: round3(max), p50: round3(p50), p95: round3(p95) };
|
|
153
302
|
}
|
|
154
|
-
function
|
|
303
|
+
function round3(n) {
|
|
155
304
|
return Math.round(n * 1e3) / 1e3;
|
|
156
305
|
}
|
|
306
|
+
|
|
307
|
+
// src/runner.ts
|
|
308
|
+
var import_node_crypto = require("crypto");
|
|
157
309
|
function parseCost(cost) {
|
|
158
310
|
const match = cost.match(/^\$?([\d.]+)$/);
|
|
159
311
|
if (!match) throw new Error(`Invalid cost format: "${cost}"`);
|
|
@@ -201,6 +353,7 @@ async function runEval(config, executeWorkflow, runtime) {
|
|
|
201
353
|
evalItem.duration = Date.now() - itemStart;
|
|
202
354
|
evalItem.output = result.output;
|
|
203
355
|
evalItem.cost = result.cost;
|
|
356
|
+
if (result.metadata) evalItem.metadata = result.metadata;
|
|
204
357
|
if (result.cost != null) {
|
|
205
358
|
totalCost += result.cost;
|
|
206
359
|
}
|
|
@@ -243,9 +396,9 @@ async function runEval(config, executeWorkflow, runtime) {
|
|
|
243
396
|
cost: scorerResult.cost
|
|
244
397
|
};
|
|
245
398
|
} else {
|
|
246
|
-
evalItem.scores[scorer.name] =
|
|
399
|
+
evalItem.scores[scorer.name] = round3(scorerResult.score);
|
|
247
400
|
evalItem.scoreDetails[scorer.name] = {
|
|
248
|
-
score:
|
|
401
|
+
score: round3(scorerResult.score),
|
|
249
402
|
metadata: scorerResult.metadata,
|
|
250
403
|
duration: scorerDuration,
|
|
251
404
|
cost: scorerResult.cost
|
|
@@ -290,11 +443,42 @@ async function runEval(config, executeWorkflow, runtime) {
|
|
|
290
443
|
}
|
|
291
444
|
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
292
445
|
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
446
|
+
const scorerTypes = {};
|
|
447
|
+
for (const s of config.scorers) {
|
|
448
|
+
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
449
|
+
}
|
|
450
|
+
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
451
|
+
for (const item of evalItems) {
|
|
452
|
+
const itemCounts = item.metadata?.modelCallCounts;
|
|
453
|
+
if (itemCounts && typeof itemCounts === "object") {
|
|
454
|
+
for (const [m, count] of Object.entries(itemCounts)) {
|
|
455
|
+
if (typeof count === "number")
|
|
456
|
+
totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + count);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
const itemModels = item.metadata?.models;
|
|
460
|
+
if (Array.isArray(itemModels)) {
|
|
461
|
+
for (const m of itemModels) {
|
|
462
|
+
if (typeof m === "string") totalModelCalls.set(m, (totalModelCalls.get(m) ?? 0) + 1);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const modelsMeta = {};
|
|
468
|
+
if (totalModelCalls.size > 0) {
|
|
469
|
+
const sorted = [...totalModelCalls.entries()].sort((a, b) => b[1] - a[1]);
|
|
470
|
+
modelsMeta.models = sorted.map(([m]) => m);
|
|
471
|
+
modelsMeta.modelCounts = Object.fromEntries(sorted);
|
|
472
|
+
}
|
|
293
473
|
return {
|
|
294
474
|
id,
|
|
295
475
|
workflow: config.workflow,
|
|
296
476
|
dataset: config.dataset.name,
|
|
297
|
-
metadata:
|
|
477
|
+
metadata: {
|
|
478
|
+
...config.metadata,
|
|
479
|
+
scorerTypes,
|
|
480
|
+
...modelsMeta
|
|
481
|
+
},
|
|
298
482
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
299
483
|
totalCost,
|
|
300
484
|
duration: Date.now() - startTime,
|
|
@@ -303,6 +487,182 @@ async function runEval(config, executeWorkflow, runtime) {
|
|
|
303
487
|
};
|
|
304
488
|
}
|
|
305
489
|
|
|
490
|
+
// src/rescore.ts
|
|
491
|
+
var import_node_crypto2 = require("crypto");
|
|
492
|
+
async function rescore(result, scorers, runtime, options) {
|
|
493
|
+
const startTime = Date.now();
|
|
494
|
+
const concurrency = options?.concurrency ?? 5;
|
|
495
|
+
const scorerContext = {
|
|
496
|
+
resolveProvider: (uri) => {
|
|
497
|
+
if (typeof runtime.resolveProvider !== "function") {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`LLM scorers require a runtime with resolveProvider(). Ensure you are using a real AxlRuntime instance, not a mock.`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return runtime.resolveProvider(uri);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
const rescored = new Array(result.items.length);
|
|
506
|
+
let totalCost = 0;
|
|
507
|
+
async function rescoreItem(original, itemIndex) {
|
|
508
|
+
if (original.error) {
|
|
509
|
+
rescored[itemIndex] = {
|
|
510
|
+
input: original.input,
|
|
511
|
+
annotations: original.annotations,
|
|
512
|
+
output: original.output,
|
|
513
|
+
error: original.error,
|
|
514
|
+
scores: {}
|
|
515
|
+
};
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const item = {
|
|
519
|
+
input: original.input,
|
|
520
|
+
annotations: original.annotations,
|
|
521
|
+
output: original.output,
|
|
522
|
+
metadata: original.metadata,
|
|
523
|
+
scores: {},
|
|
524
|
+
scoreDetails: {}
|
|
525
|
+
};
|
|
526
|
+
let itemScorerCost = 0;
|
|
527
|
+
for (const scorer of scorers) {
|
|
528
|
+
const scorerStart = Date.now();
|
|
529
|
+
try {
|
|
530
|
+
const raw = await scorer.score(item.output, item.input, item.annotations, scorerContext);
|
|
531
|
+
const scorerResult = normalizeScorerResult(raw);
|
|
532
|
+
if (scorerResult.cost != null) {
|
|
533
|
+
itemScorerCost += scorerResult.cost;
|
|
534
|
+
totalCost += scorerResult.cost;
|
|
535
|
+
}
|
|
536
|
+
const scorerDuration = Date.now() - scorerStart;
|
|
537
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
538
|
+
if (!item.scorerErrors) item.scorerErrors = [];
|
|
539
|
+
item.scorerErrors.push(
|
|
540
|
+
`Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
541
|
+
);
|
|
542
|
+
item.scores[scorer.name] = null;
|
|
543
|
+
item.scoreDetails[scorer.name] = {
|
|
544
|
+
score: null,
|
|
545
|
+
metadata: scorerResult.metadata,
|
|
546
|
+
duration: scorerDuration,
|
|
547
|
+
cost: scorerResult.cost
|
|
548
|
+
};
|
|
549
|
+
} else {
|
|
550
|
+
item.scores[scorer.name] = round3(scorerResult.score);
|
|
551
|
+
item.scoreDetails[scorer.name] = {
|
|
552
|
+
score: round3(scorerResult.score),
|
|
553
|
+
metadata: scorerResult.metadata,
|
|
554
|
+
duration: scorerDuration,
|
|
555
|
+
cost: scorerResult.cost
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
} catch (err) {
|
|
559
|
+
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
560
|
+
if (errCost != null) {
|
|
561
|
+
itemScorerCost += errCost;
|
|
562
|
+
totalCost += errCost;
|
|
563
|
+
}
|
|
564
|
+
if (!item.scorerErrors) item.scorerErrors = [];
|
|
565
|
+
item.scorerErrors.push(
|
|
566
|
+
`Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
567
|
+
);
|
|
568
|
+
item.scores[scorer.name] = null;
|
|
569
|
+
item.scoreDetails[scorer.name] = {
|
|
570
|
+
score: null,
|
|
571
|
+
duration: Date.now() - scorerStart,
|
|
572
|
+
cost: errCost
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
577
|
+
rescored[itemIndex] = item;
|
|
578
|
+
}
|
|
579
|
+
let index = 0;
|
|
580
|
+
async function runNext() {
|
|
581
|
+
while (index < result.items.length) {
|
|
582
|
+
const currentIndex = index++;
|
|
583
|
+
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const workers = Array.from(
|
|
587
|
+
{ length: Math.min(concurrency, result.items.length) },
|
|
588
|
+
() => runNext()
|
|
589
|
+
);
|
|
590
|
+
await Promise.all(workers);
|
|
591
|
+
const failures = rescored.filter((i) => i.error).length;
|
|
592
|
+
const scorerNames = scorers.map((s) => s.name);
|
|
593
|
+
const scorerStats = {};
|
|
594
|
+
for (const name of scorerNames) {
|
|
595
|
+
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
596
|
+
scorerStats[name] = computeStats(scores);
|
|
597
|
+
}
|
|
598
|
+
const scorerTypes = {};
|
|
599
|
+
for (const s of scorers) {
|
|
600
|
+
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
604
|
+
workflow: result.workflow,
|
|
605
|
+
dataset: result.dataset,
|
|
606
|
+
metadata: (() => {
|
|
607
|
+
const { runGroupId: _, runIndex: __, ...rest } = result.metadata;
|
|
608
|
+
return { ...rest, rescored: true, originalId: result.id, scorerTypes };
|
|
609
|
+
})(),
|
|
610
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
611
|
+
totalCost,
|
|
612
|
+
duration: Date.now() - startTime,
|
|
613
|
+
items: rescored,
|
|
614
|
+
summary: { count: result.items.length, failures, scorers: scorerStats }
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/multi-run.ts
|
|
619
|
+
var import_node_crypto3 = require("crypto");
|
|
620
|
+
function std(values, mean) {
|
|
621
|
+
if (values.length <= 1) return 0;
|
|
622
|
+
const sumSqDiffs = values.reduce((sum, v) => sum + (v - mean) ** 2, 0);
|
|
623
|
+
return Math.sqrt(sumSqDiffs / (values.length - 1));
|
|
624
|
+
}
|
|
625
|
+
function aggregateRuns(runs) {
|
|
626
|
+
if (runs.length === 0) throw new Error("Cannot aggregate zero runs");
|
|
627
|
+
const runGroupId = runs[0].metadata.runGroupId ?? (0, import_node_crypto3.randomUUID)();
|
|
628
|
+
const { workflow, dataset } = runs[0];
|
|
629
|
+
const totalCost = runs.reduce((sum, r) => sum + r.totalCost, 0);
|
|
630
|
+
const totalDuration = runs.reduce((sum, r) => sum + r.duration, 0);
|
|
631
|
+
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
632
|
+
const scorers = {};
|
|
633
|
+
for (const name of scorerNames) {
|
|
634
|
+
const means = runs.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
635
|
+
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
636
|
+
const minMean = Math.min(...means);
|
|
637
|
+
const maxMean = Math.max(...means);
|
|
638
|
+
scorers[name] = {
|
|
639
|
+
mean: round3(meanOfMeans),
|
|
640
|
+
std: round3(std(means, meanOfMeans)),
|
|
641
|
+
min: round3(minMean),
|
|
642
|
+
max: round3(maxMean)
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
let timing;
|
|
646
|
+
const timingMeans = runs.filter((r) => r.summary.timing).map((r) => r.summary.timing.mean);
|
|
647
|
+
if (timingMeans.length === runs.length && timingMeans.length > 0) {
|
|
648
|
+
const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
|
|
649
|
+
timing = {
|
|
650
|
+
mean: round3(meanTiming),
|
|
651
|
+
std: round3(std(timingMeans, meanTiming))
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
runGroupId,
|
|
656
|
+
runCount: runs.length,
|
|
657
|
+
workflow,
|
|
658
|
+
dataset,
|
|
659
|
+
totalCost: round3(totalCost),
|
|
660
|
+
totalDuration: round3(totalDuration),
|
|
661
|
+
scorers,
|
|
662
|
+
timing
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
306
666
|
// src/cli-utils.ts
|
|
307
667
|
var import_node_path = require("path");
|
|
308
668
|
var import_node_fs = require("fs");
|
|
@@ -367,17 +727,27 @@ async function registerConditions(conditions) {
|
|
|
367
727
|
|
|
368
728
|
// src/cli.ts
|
|
369
729
|
var import_meta = {};
|
|
370
|
-
var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
730
|
+
var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
731
|
+
"--output",
|
|
732
|
+
"--config",
|
|
733
|
+
"--conditions",
|
|
734
|
+
"--fail-on-regression",
|
|
735
|
+
"--threshold",
|
|
736
|
+
"--runs"
|
|
737
|
+
]);
|
|
371
738
|
async function main() {
|
|
372
739
|
const args = process.argv.slice(2);
|
|
373
740
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
374
741
|
console.log(`
|
|
375
742
|
Usage:
|
|
376
743
|
axl-eval <path> Run eval file(s)
|
|
744
|
+
axl-eval <path> --runs <n> Run eval N times (multi-run)
|
|
377
745
|
axl-eval <path> --output <file> Save results to JSON
|
|
378
746
|
axl-eval <path> --config <file> Use config file for runtime
|
|
379
747
|
axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
|
|
748
|
+
axl-eval rescore <results> <eval-file> Re-run scorers on saved outputs
|
|
380
749
|
axl-eval compare <a> <b> Compare two eval result files
|
|
750
|
+
axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
|
|
381
751
|
axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
|
|
382
752
|
|
|
383
753
|
Config auto-detection (when --config is not specified):
|
|
@@ -393,18 +763,40 @@ When no config is found, a bare AxlRuntime is created (providers from env vars).
|
|
|
393
763
|
await runCompare(args.slice(1));
|
|
394
764
|
return;
|
|
395
765
|
}
|
|
766
|
+
if (args[0] === "rescore") {
|
|
767
|
+
await runRescore(args.slice(1));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
396
770
|
await runEvalCommand(args);
|
|
397
771
|
}
|
|
772
|
+
function parseThresholdArg(args) {
|
|
773
|
+
const idx = args.indexOf("--threshold");
|
|
774
|
+
if (idx === -1 || idx + 1 >= args.length) return void 0;
|
|
775
|
+
const value = args[idx + 1];
|
|
776
|
+
if (/^[\d.]+$/.test(value)) return parseFloat(value);
|
|
777
|
+
const map = {};
|
|
778
|
+
for (const pair of value.split(",")) {
|
|
779
|
+
const [key, val] = pair.split("=");
|
|
780
|
+
if (key && val != null) map[key.trim()] = parseFloat(val.trim());
|
|
781
|
+
}
|
|
782
|
+
return Object.keys(map).length > 0 ? map : void 0;
|
|
783
|
+
}
|
|
398
784
|
async function runCompare(args) {
|
|
399
785
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
400
|
-
const
|
|
786
|
+
const thresholds = parseThresholdArg(args);
|
|
787
|
+
const files = args.filter(
|
|
788
|
+
(a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1] === "--threshold")
|
|
789
|
+
);
|
|
401
790
|
if (files.length !== 2) {
|
|
402
|
-
console.error("Usage: axl-eval compare <baseline.json> <candidate.json>");
|
|
791
|
+
console.error("Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>]");
|
|
403
792
|
process.exit(1);
|
|
404
793
|
}
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
const
|
|
794
|
+
const baselineRaw = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
|
|
795
|
+
const candidateRaw = JSON.parse(await (0, import_promises.readFile)(files[1], "utf-8"));
|
|
796
|
+
const baseline = baselineRaw;
|
|
797
|
+
const candidate = candidateRaw;
|
|
798
|
+
const compareOptions = thresholds != null ? { thresholds } : void 0;
|
|
799
|
+
const comparison = evalCompare(baseline, candidate, compareOptions);
|
|
408
800
|
console.log(
|
|
409
801
|
`
|
|
410
802
|
Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${comparison.candidate.id.slice(0, 8)})
|
|
@@ -412,14 +804,26 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
412
804
|
);
|
|
413
805
|
const scorerNames = Object.keys(comparison.scorers);
|
|
414
806
|
const maxNameLen = Math.max(...scorerNames.map((n) => n.length), 6);
|
|
415
|
-
|
|
416
|
-
|
|
807
|
+
const hasCI = scorerNames.some((n) => comparison.scorers[n].ci != null);
|
|
808
|
+
const ciHeader = hasCI ? " CI 95% Sig" : "";
|
|
809
|
+
const ciRule = hasCI ? " ---------------- ---" : "";
|
|
810
|
+
console.log(` ${"Scorer".padEnd(maxNameLen)} Baseline Candidate Delta Change${ciHeader}`);
|
|
811
|
+
console.log(` ${"".padEnd(maxNameLen, "-")} -------- --------- -------- ------${ciRule}`);
|
|
417
812
|
for (const name of scorerNames) {
|
|
418
813
|
const s = comparison.scorers[name];
|
|
419
814
|
const sign = s.delta > 0 ? "+" : "";
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
815
|
+
let line = ` ${name.padEnd(maxNameLen)} ${s.baselineMean.toFixed(3).padStart(8)} ${s.candidateMean.toFixed(3).padStart(9)} ${(sign + s.delta.toFixed(3)).padStart(8)} ${(sign + s.deltaPercent.toFixed(1) + "%").padStart(6)}`;
|
|
816
|
+
if (hasCI) {
|
|
817
|
+
if (s.ci) {
|
|
818
|
+
const lo = (s.ci.lower >= 0 ? "+" : "") + s.ci.lower.toFixed(4);
|
|
819
|
+
const hi = (s.ci.upper >= 0 ? "+" : "") + s.ci.upper.toFixed(4);
|
|
820
|
+
const sig = s.significant ? " *" : " ";
|
|
821
|
+
line += ` [${lo}, ${hi}]${sig}`;
|
|
822
|
+
} else {
|
|
823
|
+
line += " \u2014".padEnd(22);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
console.log(line);
|
|
423
827
|
}
|
|
424
828
|
if (comparison.timing) {
|
|
425
829
|
const t = comparison.timing;
|
|
@@ -436,9 +840,10 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
436
840
|
` Cost: baseline $${c.baselineTotal.toFixed(2)} -> candidate $${c.candidateTotal.toFixed(2)} (${sign}${c.deltaPercent.toFixed(1)}%)`
|
|
437
841
|
);
|
|
438
842
|
}
|
|
843
|
+
const baselineRef = Array.isArray(baseline) ? baseline[0] : baseline;
|
|
439
844
|
const stable = Math.max(
|
|
440
845
|
0,
|
|
441
|
-
|
|
846
|
+
baselineRef.items.length - comparison.regressions.length - comparison.improvements.length
|
|
442
847
|
);
|
|
443
848
|
console.log(
|
|
444
849
|
`
|
|
@@ -446,8 +851,52 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
446
851
|
`
|
|
447
852
|
);
|
|
448
853
|
if (failOnRegression && comparison.regressions.length > 0) {
|
|
854
|
+
const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
|
|
855
|
+
if (hasSignificance) {
|
|
856
|
+
const hasSignificantRegression = scorerNames.some(
|
|
857
|
+
(n) => comparison.scorers[n].significant === true && comparison.scorers[n].delta < 0
|
|
858
|
+
);
|
|
859
|
+
if (hasSignificantRegression) process.exit(1);
|
|
860
|
+
} else {
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function runRescore(args) {
|
|
866
|
+
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
867
|
+
if (paths.length < 2) {
|
|
868
|
+
console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
|
|
449
869
|
process.exit(1);
|
|
450
870
|
}
|
|
871
|
+
const [resultsPath, evalFilePath] = paths;
|
|
872
|
+
const raw = JSON.parse(await (0, import_promises.readFile)(resultsPath, "utf-8"));
|
|
873
|
+
const results = Array.isArray(raw) ? raw : [raw];
|
|
874
|
+
const runtime = await getRuntime(configArg, conditions);
|
|
875
|
+
try {
|
|
876
|
+
const mod = await importModule(path.resolve(evalFilePath), import_meta.url);
|
|
877
|
+
const evalConfig = mod.default?.default ?? mod.default ?? mod.config ?? mod;
|
|
878
|
+
if (!evalConfig.scorers || evalConfig.scorers.length === 0) {
|
|
879
|
+
console.error(`Error: ${evalFilePath} does not export scorers`);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
const rescored = [];
|
|
883
|
+
for (const resultData of results) {
|
|
884
|
+
rescored.push(await rescore(resultData, evalConfig.scorers, runtime));
|
|
885
|
+
}
|
|
886
|
+
for (const r of rescored) {
|
|
887
|
+
console.log("\n" + formatTable(r) + "\n");
|
|
888
|
+
}
|
|
889
|
+
if (outputPath) {
|
|
890
|
+
const output = Array.isArray(raw) ? rescored : rescored[0];
|
|
891
|
+
const outputDir = path.dirname(path.resolve(outputPath));
|
|
892
|
+
await (0, import_promises.mkdir)(outputDir, { recursive: true });
|
|
893
|
+
await (0, import_promises.writeFile)(outputPath, JSON.stringify(output, null, 2), "utf-8");
|
|
894
|
+
console.log(`Rescored results saved to ${outputPath}`);
|
|
895
|
+
}
|
|
896
|
+
} finally {
|
|
897
|
+
await runtime.shutdown().catch(() => {
|
|
898
|
+
});
|
|
899
|
+
}
|
|
451
900
|
}
|
|
452
901
|
function collectEvalFiles(p) {
|
|
453
902
|
const resolved = path.resolve(p);
|
|
@@ -514,6 +963,35 @@ function formatTable(result) {
|
|
|
514
963
|
}
|
|
515
964
|
return lines.join("\n");
|
|
516
965
|
}
|
|
966
|
+
function formatMultiRunTable(summary) {
|
|
967
|
+
const lines = [];
|
|
968
|
+
const scorerNames = Object.keys(summary.scorers);
|
|
969
|
+
const maxNameLen = Math.max(...scorerNames.map((n) => n.length), "Scorer".length);
|
|
970
|
+
const colWidth = 16;
|
|
971
|
+
lines.push(`Eval: ${summary.workflow} x ${summary.dataset} \u2014 ${summary.runCount} runs`);
|
|
972
|
+
lines.push(
|
|
973
|
+
` ${"Scorer".padEnd(maxNameLen)} ${"Mean \xB1 Std".padStart(colWidth)} ${"Min".padStart(8)} ${"Max".padStart(8)}`
|
|
974
|
+
);
|
|
975
|
+
const ruleLen = maxNameLen + 2 + colWidth + 2 + 8 + 2 + 8;
|
|
976
|
+
lines.push(" " + "\u2500".repeat(ruleLen));
|
|
977
|
+
for (const name of scorerNames) {
|
|
978
|
+
const s = summary.scorers[name];
|
|
979
|
+
const meanStd = `${s.mean.toFixed(3)} \xB1 ${s.std.toFixed(3)}`;
|
|
980
|
+
lines.push(
|
|
981
|
+
` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}`
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
if (summary.timing) {
|
|
985
|
+
lines.push(
|
|
986
|
+
` ${"Timing".padEnd(maxNameLen)} ${((summary.timing.mean / 1e3).toFixed(2) + " \xB1 " + (summary.timing.std / 1e3).toFixed(2) + "s").padStart(colWidth)}`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const costStr = summary.totalCost > 0 ? `$${summary.totalCost.toFixed(2)}` : "$0.00";
|
|
990
|
+
const durationStr = (summary.totalDuration / 1e3).toFixed(1);
|
|
991
|
+
lines.push("");
|
|
992
|
+
lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
|
|
993
|
+
return lines.join("\n");
|
|
994
|
+
}
|
|
517
995
|
async function resolveRuntimeFromConfig(configPath) {
|
|
518
996
|
try {
|
|
519
997
|
const mod = await importModule(configPath, import_meta.url);
|
|
@@ -578,10 +1056,11 @@ function parseEvalArgs(args) {
|
|
|
578
1056
|
let outputPath;
|
|
579
1057
|
let configArg;
|
|
580
1058
|
let conditions = [];
|
|
1059
|
+
let runs = 1;
|
|
581
1060
|
const paths = [];
|
|
582
1061
|
for (let i = 0; i < args.length; i++) {
|
|
583
1062
|
const arg = args[i];
|
|
584
|
-
if (arg === "--output" || arg === "--config" || arg === "--conditions") {
|
|
1063
|
+
if (arg === "--output" || arg === "--config" || arg === "--conditions" || arg === "--runs") {
|
|
585
1064
|
if (i + 1 >= args.length) {
|
|
586
1065
|
console.error(`Error: ${arg} requires a value`);
|
|
587
1066
|
process.exit(1);
|
|
@@ -589,6 +1068,7 @@ function parseEvalArgs(args) {
|
|
|
589
1068
|
const value = args[++i];
|
|
590
1069
|
if (arg === "--output") outputPath = value;
|
|
591
1070
|
else if (arg === "--config") configArg = value;
|
|
1071
|
+
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
592
1072
|
else
|
|
593
1073
|
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
594
1074
|
} else if (arg.startsWith("--")) {
|
|
@@ -600,10 +1080,10 @@ function parseEvalArgs(args) {
|
|
|
600
1080
|
paths.push(arg);
|
|
601
1081
|
}
|
|
602
1082
|
}
|
|
603
|
-
return { outputPath, configArg, conditions, paths };
|
|
1083
|
+
return { outputPath, configArg, conditions, runs, paths };
|
|
604
1084
|
}
|
|
605
1085
|
async function runEvalCommand(args) {
|
|
606
|
-
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
1086
|
+
const { outputPath, configArg, conditions, runs, paths } = parseEvalArgs(args);
|
|
607
1087
|
if (paths.length === 0) {
|
|
608
1088
|
console.error("Error: No eval file path provided");
|
|
609
1089
|
process.exit(1);
|
|
@@ -632,17 +1112,25 @@ async function runEvalCommand(args) {
|
|
|
632
1112
|
let executeWorkflow;
|
|
633
1113
|
if (mod.executeWorkflow) {
|
|
634
1114
|
executeWorkflow = async (input, rt) => {
|
|
635
|
-
const {
|
|
1115
|
+
const {
|
|
1116
|
+
result,
|
|
1117
|
+
cost: trackedCost,
|
|
1118
|
+
metadata
|
|
1119
|
+
} = await runtime.trackExecution(async () => {
|
|
636
1120
|
return mod.executeWorkflow(input, rt);
|
|
637
1121
|
});
|
|
638
|
-
return {
|
|
1122
|
+
return {
|
|
1123
|
+
output: result.output,
|
|
1124
|
+
cost: result.cost ?? trackedCost,
|
|
1125
|
+
metadata: result.metadata ?? metadata
|
|
1126
|
+
};
|
|
639
1127
|
};
|
|
640
1128
|
} else if (runtime.getWorkflow(evalConfig.workflow)) {
|
|
641
1129
|
executeWorkflow = async (input) => {
|
|
642
|
-
const { result
|
|
1130
|
+
const { result, cost, metadata } = await runtime.trackExecution(async () => {
|
|
643
1131
|
return runtime.execute(evalConfig.workflow, input);
|
|
644
1132
|
});
|
|
645
|
-
return { output:
|
|
1133
|
+
return { output: result, cost, metadata };
|
|
646
1134
|
};
|
|
647
1135
|
} else {
|
|
648
1136
|
console.warn(
|
|
@@ -650,9 +1138,25 @@ async function runEvalCommand(args) {
|
|
|
650
1138
|
);
|
|
651
1139
|
executeWorkflow = async (input) => ({ output: input });
|
|
652
1140
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1141
|
+
if (runs > 1) {
|
|
1142
|
+
const { randomUUID: randomUUID4 } = await import("crypto");
|
|
1143
|
+
const runGroupId = randomUUID4();
|
|
1144
|
+
const runResults = [];
|
|
1145
|
+
for (let r = 0; r < runs; r++) {
|
|
1146
|
+
console.error(`[axl-eval] Run ${r + 1}/${runs}...`);
|
|
1147
|
+
const result = await runEval(evalConfig, executeWorkflow, runtime);
|
|
1148
|
+
result.metadata.runGroupId = runGroupId;
|
|
1149
|
+
result.metadata.runIndex = r;
|
|
1150
|
+
runResults.push(result);
|
|
1151
|
+
results.push(result);
|
|
1152
|
+
}
|
|
1153
|
+
const summary = aggregateRuns(runResults);
|
|
1154
|
+
console.log("\n" + formatMultiRunTable(summary) + "\n");
|
|
1155
|
+
} else {
|
|
1156
|
+
const result = await runEval(evalConfig, executeWorkflow, runtime);
|
|
1157
|
+
results.push(result);
|
|
1158
|
+
console.log("\n" + formatTable(result) + "\n");
|
|
1159
|
+
}
|
|
656
1160
|
} catch (err) {
|
|
657
1161
|
console.error(
|
|
658
1162
|
`Error running eval ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|