@axlsdk/eval 0.17.8 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -7
- package/dist/{chunk-NPKIP4SB.js → chunk-BWBX5YVQ.js} +245 -164
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +537 -214
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +305 -52
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +327 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +199 -18
- package/dist/index.d.ts +199 -18
- package/dist/index.js +91 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NPKIP4SB.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -94,10 +94,52 @@ function xorshift32(seed) {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// src/
|
|
97
|
+
// src/utils.ts
|
|
98
|
+
function computeStats(scores) {
|
|
99
|
+
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
100
|
+
const sorted = [...scores].sort((a, b) => a - b);
|
|
101
|
+
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
102
|
+
const min = sorted[0];
|
|
103
|
+
const max = sorted[sorted.length - 1];
|
|
104
|
+
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
105
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
106
|
+
return { mean: round2(mean), min: round2(min), max: round2(max), p50: round2(p50), p95: round2(p95) };
|
|
107
|
+
}
|
|
98
108
|
function round2(n) {
|
|
99
109
|
return Math.round(n * 1e3) / 1e3;
|
|
100
110
|
}
|
|
111
|
+
function scorerCounts(items, name) {
|
|
112
|
+
let scored = 0;
|
|
113
|
+
let failed = 0;
|
|
114
|
+
for (const i of items) {
|
|
115
|
+
if (i.error) continue;
|
|
116
|
+
if (i.scores[name] != null) scored++;
|
|
117
|
+
else if (i.scoreDetails?.[name]?.duration != null) failed++;
|
|
118
|
+
}
|
|
119
|
+
return { scored, failed };
|
|
120
|
+
}
|
|
121
|
+
function evaluateScorerTolerance(scored, failed, type, limit) {
|
|
122
|
+
const attempted = scored + failed;
|
|
123
|
+
const rate = attempted === 0 ? 0 : failed / attempted;
|
|
124
|
+
const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
|
|
125
|
+
return { attempted, rate, exceeds, zeroSample: attempted === 0 };
|
|
126
|
+
}
|
|
127
|
+
async function mapWithConcurrency(items, concurrency, task) {
|
|
128
|
+
const results = new Array(items.length);
|
|
129
|
+
const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
|
|
130
|
+
const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
|
|
131
|
+
let next = 0;
|
|
132
|
+
async function runNext() {
|
|
133
|
+
while (next < items.length) {
|
|
134
|
+
const i = next++;
|
|
135
|
+
results[i] = await task(items[i], i);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await Promise.all(Array.from({ length: workerCount }, () => runNext()));
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/compare.ts
|
|
101
143
|
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
102
144
|
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
103
145
|
var LEGACY_THRESHOLD = 0.1;
|
|
@@ -171,11 +213,29 @@ function evalCompare(baseline, candidate, options) {
|
|
|
171
213
|
const delta = round2(deltaRaw);
|
|
172
214
|
const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
|
|
173
215
|
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
216
|
+
let baselineScored = 0;
|
|
217
|
+
let baselineFailed = 0;
|
|
218
|
+
for (const run of baselineRuns) {
|
|
219
|
+
const c = scorerCounts(run.items, name);
|
|
220
|
+
baselineScored += c.scored;
|
|
221
|
+
baselineFailed += c.failed;
|
|
222
|
+
}
|
|
223
|
+
let candidateScored = 0;
|
|
224
|
+
let candidateFailed = 0;
|
|
225
|
+
for (const run of candidateRuns) {
|
|
226
|
+
const c = scorerCounts(run.items, name);
|
|
227
|
+
candidateScored += c.scored;
|
|
228
|
+
candidateFailed += c.failed;
|
|
229
|
+
}
|
|
174
230
|
const entry = {
|
|
175
231
|
baselineMean: bMean,
|
|
176
232
|
candidateMean: cMean,
|
|
177
233
|
delta,
|
|
178
|
-
deltaPercent
|
|
234
|
+
deltaPercent,
|
|
235
|
+
baselineScored,
|
|
236
|
+
baselineFailed,
|
|
237
|
+
candidateScored,
|
|
238
|
+
candidateFailed
|
|
179
239
|
};
|
|
180
240
|
const diffs = pairedDiffs[name];
|
|
181
241
|
if (diffs.length >= 2) {
|
|
@@ -311,25 +371,91 @@ function detectPartial(runs) {
|
|
|
311
371
|
if (runs.length >= attempted) return void 0;
|
|
312
372
|
return { completed: runs.length, attempted };
|
|
313
373
|
}
|
|
374
|
+
function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
|
|
375
|
+
const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
|
|
376
|
+
for (const name of Object.keys(comparison.scorers)) {
|
|
377
|
+
const s = comparison.scorers[name];
|
|
378
|
+
const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
|
|
379
|
+
const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
|
|
380
|
+
for (const side of ["baseline", "candidate"]) {
|
|
381
|
+
const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
|
|
382
|
+
const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
|
|
383
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
384
|
+
if (verdict.zeroSample) {
|
|
385
|
+
return `scorer "${name}" has no scored items on ${side} (its items errored or were skipped) \u2014 cannot certify a mean over an empty sample.`;
|
|
386
|
+
}
|
|
387
|
+
if (verdict.exceeds) {
|
|
388
|
+
const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
|
|
389
|
+
return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
314
395
|
|
|
315
396
|
// src/scorer.ts
|
|
316
397
|
function normalizeScorerResult(result) {
|
|
317
398
|
return typeof result === "number" ? { score: result } : result;
|
|
318
399
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
|
|
323
|
-
const sorted = [...scores].sort((a, b) => a - b);
|
|
324
|
-
const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
325
|
-
const min = sorted[0];
|
|
326
|
-
const max = sorted[sorted.length - 1];
|
|
327
|
-
const p50 = sorted[Math.floor(sorted.length * 0.5)];
|
|
328
|
-
const p95 = sorted[Math.floor(sorted.length * 0.95)];
|
|
329
|
-
return { mean: round3(mean), min: round3(min), max: round3(max), p50: round3(p50), p95: round3(p95) };
|
|
400
|
+
function extractScorerErrorCost(err) {
|
|
401
|
+
const v = err && typeof err === "object" ? err.cost : void 0;
|
|
402
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
330
403
|
}
|
|
331
|
-
|
|
332
|
-
|
|
404
|
+
|
|
405
|
+
// src/score-item.ts
|
|
406
|
+
async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal) {
|
|
407
|
+
const scoreDetails = item.scoreDetails ??= {};
|
|
408
|
+
for (const s of scorers) {
|
|
409
|
+
item.scores[s.name] = null;
|
|
410
|
+
scoreDetails[s.name] = { score: null };
|
|
411
|
+
}
|
|
412
|
+
let itemScorerCost = 0;
|
|
413
|
+
const scorerErrorsByName = {};
|
|
414
|
+
await mapWithConcurrency(scorers, scorerConcurrency, async (scorer) => {
|
|
415
|
+
if (signal?.aborted) return;
|
|
416
|
+
const scorerStart = Date.now();
|
|
417
|
+
try {
|
|
418
|
+
const raw = await scorer.score(item.output, item.input, item.annotations, scorerContext);
|
|
419
|
+
const scorerResult = normalizeScorerResult(raw);
|
|
420
|
+
if (scorerResult.cost != null) {
|
|
421
|
+
itemScorerCost += scorerResult.cost;
|
|
422
|
+
}
|
|
423
|
+
const scorerDuration = Date.now() - scorerStart;
|
|
424
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
425
|
+
scorerErrorsByName[scorer.name] = `Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`;
|
|
426
|
+
item.scores[scorer.name] = null;
|
|
427
|
+
scoreDetails[scorer.name] = {
|
|
428
|
+
score: null,
|
|
429
|
+
metadata: scorerResult.metadata,
|
|
430
|
+
duration: scorerDuration,
|
|
431
|
+
cost: scorerResult.cost
|
|
432
|
+
};
|
|
433
|
+
} else {
|
|
434
|
+
item.scores[scorer.name] = round2(scorerResult.score);
|
|
435
|
+
scoreDetails[scorer.name] = {
|
|
436
|
+
score: round2(scorerResult.score),
|
|
437
|
+
metadata: scorerResult.metadata,
|
|
438
|
+
duration: scorerDuration,
|
|
439
|
+
cost: scorerResult.cost
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (err?.name === "AbortError") return;
|
|
444
|
+
const errCost = extractScorerErrorCost(err);
|
|
445
|
+
if (errCost != null) itemScorerCost += errCost;
|
|
446
|
+
scorerErrorsByName[scorer.name] = `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
447
|
+
item.scores[scorer.name] = null;
|
|
448
|
+
scoreDetails[scorer.name] = {
|
|
449
|
+
score: null,
|
|
450
|
+
duration: Date.now() - scorerStart,
|
|
451
|
+
cost: errCost
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
|
|
456
|
+
if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
|
|
457
|
+
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
458
|
+
return itemScorerCost;
|
|
333
459
|
}
|
|
334
460
|
|
|
335
461
|
// src/runner.ts
|
|
@@ -372,7 +498,9 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
372
498
|
const startTime = Date.now();
|
|
373
499
|
const id = (0, import_node_crypto.randomUUID)();
|
|
374
500
|
const items = await config.dataset.getItems();
|
|
501
|
+
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
375
502
|
const concurrency = config.concurrency ?? 5;
|
|
503
|
+
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
376
504
|
const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
|
|
377
505
|
const scorerContext = {
|
|
378
506
|
resolveProvider: (uri) => {
|
|
@@ -382,7 +510,8 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
382
510
|
);
|
|
383
511
|
}
|
|
384
512
|
return runtime.resolveProvider(uri);
|
|
385
|
-
}
|
|
513
|
+
},
|
|
514
|
+
signal: options?.signal
|
|
386
515
|
};
|
|
387
516
|
const evalItems = new Array(items.length);
|
|
388
517
|
let totalCost = 0;
|
|
@@ -460,89 +589,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
460
589
|
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
461
590
|
budgetExceeded = true;
|
|
462
591
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
item.input,
|
|
472
|
-
item.annotations,
|
|
473
|
-
scorerContext
|
|
474
|
-
);
|
|
475
|
-
const scorerResult = normalizeScorerResult(raw);
|
|
476
|
-
if (scorerResult.cost != null) {
|
|
477
|
-
itemScorerCost += scorerResult.cost;
|
|
478
|
-
totalCost += scorerResult.cost;
|
|
479
|
-
}
|
|
480
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
481
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
482
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
483
|
-
evalItem.scorerErrors.push(
|
|
484
|
-
`Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
485
|
-
);
|
|
486
|
-
evalItem.scores[scorer.name] = null;
|
|
487
|
-
evalItem.scoreDetails[scorer.name] = {
|
|
488
|
-
score: null,
|
|
489
|
-
metadata: scorerResult.metadata,
|
|
490
|
-
duration: scorerDuration,
|
|
491
|
-
cost: scorerResult.cost
|
|
492
|
-
};
|
|
493
|
-
} else {
|
|
494
|
-
evalItem.scores[scorer.name] = round3(scorerResult.score);
|
|
495
|
-
evalItem.scoreDetails[scorer.name] = {
|
|
496
|
-
score: round3(scorerResult.score),
|
|
497
|
-
metadata: scorerResult.metadata,
|
|
498
|
-
duration: scorerDuration,
|
|
499
|
-
cost: scorerResult.cost
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
} catch (err) {
|
|
503
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
504
|
-
if (errCost != null) {
|
|
505
|
-
itemScorerCost += errCost;
|
|
506
|
-
totalCost += errCost;
|
|
507
|
-
}
|
|
508
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
509
|
-
evalItem.scorerErrors.push(
|
|
510
|
-
`Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
511
|
-
);
|
|
512
|
-
evalItem.scores[scorer.name] = null;
|
|
513
|
-
evalItem.scoreDetails[scorer.name] = {
|
|
514
|
-
score: null,
|
|
515
|
-
duration: Date.now() - scorerStart,
|
|
516
|
-
cost: errCost
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
evalItem.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
592
|
+
const itemScorerCost = await scoreItem(
|
|
593
|
+
evalItem,
|
|
594
|
+
config.scorers,
|
|
595
|
+
scorerConcurrency,
|
|
596
|
+
scorerContext,
|
|
597
|
+
options?.signal
|
|
598
|
+
);
|
|
599
|
+
totalCost += itemScorerCost;
|
|
521
600
|
evalItems[itemIndex] = evalItem;
|
|
522
601
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
523
602
|
}
|
|
524
|
-
|
|
525
|
-
async function runNext() {
|
|
526
|
-
while (index < items.length) {
|
|
527
|
-
const currentIndex = index++;
|
|
528
|
-
await processItem(items[currentIndex], currentIndex);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
532
|
-
await Promise.all(workers);
|
|
603
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
533
604
|
const failures = evalItems.filter((i) => i.error).length;
|
|
534
605
|
const scorerNames = config.scorers.map((s) => s.name);
|
|
535
606
|
const scorerStats = {};
|
|
536
607
|
for (const name of scorerNames) {
|
|
537
608
|
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
538
|
-
|
|
609
|
+
const { scored, failed } = scorerCounts(evalItems, name);
|
|
610
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
539
611
|
}
|
|
540
|
-
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
541
|
-
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
542
612
|
const scorerTypes = {};
|
|
543
613
|
for (const s of config.scorers) {
|
|
544
614
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
545
615
|
}
|
|
616
|
+
let degraded;
|
|
617
|
+
const rawLimit = config.failOnScorerErrorRate;
|
|
618
|
+
if (rawLimit != null) {
|
|
619
|
+
if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
|
|
620
|
+
console.warn(
|
|
621
|
+
`[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
|
|
622
|
+
);
|
|
623
|
+
} else {
|
|
624
|
+
for (const name of scorerNames) {
|
|
625
|
+
const stats = scorerStats[name];
|
|
626
|
+
const scored = stats.scored ?? 0;
|
|
627
|
+
const failed = stats.failed ?? 0;
|
|
628
|
+
const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
|
|
629
|
+
const limit = type === "deterministic" ? 0 : rawLimit;
|
|
630
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
631
|
+
if (verdict.exceeds) {
|
|
632
|
+
(degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
638
|
+
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
546
639
|
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
547
640
|
for (const item of evalItems) {
|
|
548
641
|
const itemCounts = item.metadata?.modelCallCounts;
|
|
@@ -601,13 +694,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
601
694
|
...config.metadata,
|
|
602
695
|
scorerTypes,
|
|
603
696
|
...modelsMeta,
|
|
604
|
-
...workflowsMeta
|
|
697
|
+
...workflowsMeta,
|
|
698
|
+
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
605
699
|
},
|
|
606
700
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
607
701
|
totalCost,
|
|
608
702
|
duration: Date.now() - startTime,
|
|
609
703
|
items: evalItems,
|
|
610
|
-
summary: {
|
|
704
|
+
summary: {
|
|
705
|
+
count: items.length,
|
|
706
|
+
failures,
|
|
707
|
+
scorers: scorerStats,
|
|
708
|
+
timing,
|
|
709
|
+
...degraded ? { degraded } : {}
|
|
710
|
+
}
|
|
611
711
|
};
|
|
612
712
|
}
|
|
613
713
|
|
|
@@ -616,6 +716,7 @@ var import_node_crypto2 = require("crypto");
|
|
|
616
716
|
async function rescore(result, scorers, runtime, options) {
|
|
617
717
|
const startTime = Date.now();
|
|
618
718
|
const concurrency = options?.concurrency ?? 5;
|
|
719
|
+
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
619
720
|
const scorerContext = {
|
|
620
721
|
resolveProvider: (uri) => {
|
|
621
722
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -624,11 +725,22 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
624
725
|
);
|
|
625
726
|
}
|
|
626
727
|
return runtime.resolveProvider(uri);
|
|
627
|
-
}
|
|
728
|
+
},
|
|
729
|
+
signal: options?.signal
|
|
628
730
|
};
|
|
629
731
|
const rescored = new Array(result.items.length);
|
|
630
732
|
let totalCost = 0;
|
|
631
733
|
async function rescoreItem(original, itemIndex) {
|
|
734
|
+
if (options?.signal?.aborted) {
|
|
735
|
+
rescored[itemIndex] = {
|
|
736
|
+
input: original.input,
|
|
737
|
+
annotations: original.annotations,
|
|
738
|
+
output: original.output,
|
|
739
|
+
error: "cancelled",
|
|
740
|
+
scores: {}
|
|
741
|
+
};
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
632
744
|
if (original.error) {
|
|
633
745
|
rescored[itemIndex] = {
|
|
634
746
|
input: original.input,
|
|
@@ -648,77 +760,24 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
648
760
|
scores: {},
|
|
649
761
|
scoreDetails: {}
|
|
650
762
|
};
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
totalCost += scorerResult.cost;
|
|
660
|
-
}
|
|
661
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
662
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
663
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
664
|
-
item.scorerErrors.push(
|
|
665
|
-
`Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
666
|
-
);
|
|
667
|
-
item.scores[scorer.name] = null;
|
|
668
|
-
item.scoreDetails[scorer.name] = {
|
|
669
|
-
score: null,
|
|
670
|
-
metadata: scorerResult.metadata,
|
|
671
|
-
duration: scorerDuration,
|
|
672
|
-
cost: scorerResult.cost
|
|
673
|
-
};
|
|
674
|
-
} else {
|
|
675
|
-
item.scores[scorer.name] = round3(scorerResult.score);
|
|
676
|
-
item.scoreDetails[scorer.name] = {
|
|
677
|
-
score: round3(scorerResult.score),
|
|
678
|
-
metadata: scorerResult.metadata,
|
|
679
|
-
duration: scorerDuration,
|
|
680
|
-
cost: scorerResult.cost
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
} catch (err) {
|
|
684
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
685
|
-
if (errCost != null) {
|
|
686
|
-
itemScorerCost += errCost;
|
|
687
|
-
totalCost += errCost;
|
|
688
|
-
}
|
|
689
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
690
|
-
item.scorerErrors.push(
|
|
691
|
-
`Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
692
|
-
);
|
|
693
|
-
item.scores[scorer.name] = null;
|
|
694
|
-
item.scoreDetails[scorer.name] = {
|
|
695
|
-
score: null,
|
|
696
|
-
duration: Date.now() - scorerStart,
|
|
697
|
-
cost: errCost
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
763
|
+
const itemScorerCost = await scoreItem(
|
|
764
|
+
item,
|
|
765
|
+
scorers,
|
|
766
|
+
scorerConcurrency,
|
|
767
|
+
scorerContext,
|
|
768
|
+
options?.signal
|
|
769
|
+
);
|
|
770
|
+
totalCost += itemScorerCost;
|
|
702
771
|
rescored[itemIndex] = item;
|
|
703
772
|
}
|
|
704
|
-
|
|
705
|
-
async function runNext() {
|
|
706
|
-
while (index < result.items.length) {
|
|
707
|
-
const currentIndex = index++;
|
|
708
|
-
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
const workers = Array.from(
|
|
712
|
-
{ length: Math.min(concurrency, result.items.length) },
|
|
713
|
-
() => runNext()
|
|
714
|
-
);
|
|
715
|
-
await Promise.all(workers);
|
|
773
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
716
774
|
const failures = rescored.filter((i) => i.error).length;
|
|
717
775
|
const scorerNames = scorers.map((s) => s.name);
|
|
718
776
|
const scorerStats = {};
|
|
719
777
|
for (const name of scorerNames) {
|
|
720
778
|
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
721
|
-
|
|
779
|
+
const { scored, failed } = scorerCounts(rescored, name);
|
|
780
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
722
781
|
}
|
|
723
782
|
const scorerTypes = {};
|
|
724
783
|
for (const s of scorers) {
|
|
@@ -728,7 +787,11 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
728
787
|
id: (0, import_node_crypto2.randomUUID)(),
|
|
729
788
|
dataset: result.dataset,
|
|
730
789
|
metadata: (() => {
|
|
731
|
-
const
|
|
790
|
+
const rest = { ...result.metadata };
|
|
791
|
+
delete rest.runGroupId;
|
|
792
|
+
delete rest.runIndex;
|
|
793
|
+
delete rest.scorerFiltered;
|
|
794
|
+
delete rest.scorersRun;
|
|
732
795
|
const merged = {
|
|
733
796
|
...rest,
|
|
734
797
|
rescored: true,
|
|
@@ -781,15 +844,24 @@ function aggregateRuns(runs) {
|
|
|
781
844
|
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
782
845
|
const scorers = {};
|
|
783
846
|
for (const name of scorerNames) {
|
|
784
|
-
const
|
|
847
|
+
const contributing = runs.filter((r) => {
|
|
848
|
+
const sc = r.summary.scorers[name]?.scored;
|
|
849
|
+
return sc == null || sc > 0;
|
|
850
|
+
});
|
|
851
|
+
const meanSource = contributing.length > 0 ? contributing : runs;
|
|
852
|
+
const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
785
853
|
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
786
854
|
const minMean = Math.min(...means);
|
|
787
855
|
const maxMean = Math.max(...means);
|
|
856
|
+
const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
|
|
857
|
+
const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
|
|
788
858
|
scorers[name] = {
|
|
789
|
-
mean:
|
|
790
|
-
std:
|
|
791
|
-
min:
|
|
792
|
-
max:
|
|
859
|
+
mean: round2(meanOfMeans),
|
|
860
|
+
std: round2(std(means, meanOfMeans)),
|
|
861
|
+
min: round2(minMean),
|
|
862
|
+
max: round2(maxMean),
|
|
863
|
+
scored,
|
|
864
|
+
failed
|
|
793
865
|
};
|
|
794
866
|
}
|
|
795
867
|
let timing;
|
|
@@ -797,8 +869,8 @@ function aggregateRuns(runs) {
|
|
|
797
869
|
if (timingMeans.length === runs.length && timingMeans.length > 0) {
|
|
798
870
|
const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
|
|
799
871
|
timing = {
|
|
800
|
-
mean:
|
|
801
|
-
std:
|
|
872
|
+
mean: round2(meanTiming),
|
|
873
|
+
std: round2(std(timingMeans, meanTiming))
|
|
802
874
|
};
|
|
803
875
|
}
|
|
804
876
|
return {
|
|
@@ -806,8 +878,8 @@ function aggregateRuns(runs) {
|
|
|
806
878
|
runCount: runs.length,
|
|
807
879
|
workflows,
|
|
808
880
|
dataset,
|
|
809
|
-
totalCost:
|
|
810
|
-
totalDuration:
|
|
881
|
+
totalCost: round2(totalCost),
|
|
882
|
+
totalDuration: round2(totalDuration),
|
|
811
883
|
scorers,
|
|
812
884
|
timing
|
|
813
885
|
};
|
|
@@ -849,11 +921,22 @@ function validateEvalConfig(cfg) {
|
|
|
849
921
|
if (badScorerIdx !== -1) {
|
|
850
922
|
return `exports a scorers array with a non-scorer entry at index ${badScorerIdx} \u2014 each scorer must have a score() method (use the scorer() factory).`;
|
|
851
923
|
}
|
|
924
|
+
for (const key of ["concurrency", "scorerConcurrency"]) {
|
|
925
|
+
const v = c[key];
|
|
926
|
+
if (v !== void 0 && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
|
927
|
+
return `exports an invalid ${key} (${typeof v === "number" ? v : typeof v}) \u2014 must be a positive integer.`;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
{
|
|
931
|
+
const v = c.failOnScorerErrorRate;
|
|
932
|
+
if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1)) {
|
|
933
|
+
return `exports an invalid failOnScorerErrorRate (${typeof v === "number" ? v : typeof v}) \u2014 must be a number between 0 and 1.`;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
852
936
|
return void 0;
|
|
853
937
|
}
|
|
854
938
|
|
|
855
|
-
// src/cli.ts
|
|
856
|
-
var import_meta = {};
|
|
939
|
+
// src/cli-args.ts
|
|
857
940
|
var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
858
941
|
"--output",
|
|
859
942
|
"--config",
|
|
@@ -861,8 +944,114 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
861
944
|
"--fail-on-regression",
|
|
862
945
|
"--threshold",
|
|
863
946
|
"--runs",
|
|
864
|
-
"--capture-traces"
|
|
947
|
+
"--capture-traces",
|
|
948
|
+
"--concurrency",
|
|
949
|
+
"--scorers"
|
|
865
950
|
]);
|
|
951
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
952
|
+
"--output",
|
|
953
|
+
"--config",
|
|
954
|
+
"--conditions",
|
|
955
|
+
"--runs",
|
|
956
|
+
"--concurrency",
|
|
957
|
+
"--scorers"
|
|
958
|
+
]);
|
|
959
|
+
function envInt(name) {
|
|
960
|
+
const raw = process.env[name];
|
|
961
|
+
if (raw == null || raw === "") return void 0;
|
|
962
|
+
const n = parseInt(raw, 10);
|
|
963
|
+
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
964
|
+
return n;
|
|
965
|
+
}
|
|
966
|
+
function parseEvalArgs(args) {
|
|
967
|
+
let outputPath;
|
|
968
|
+
let configArg;
|
|
969
|
+
let conditions = [];
|
|
970
|
+
let runs = 1;
|
|
971
|
+
let captureTraces = false;
|
|
972
|
+
let concurrency;
|
|
973
|
+
let scorerNames;
|
|
974
|
+
const paths = [];
|
|
975
|
+
for (let i = 0; i < args.length; i++) {
|
|
976
|
+
const arg = args[i];
|
|
977
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
978
|
+
if (i + 1 >= args.length) {
|
|
979
|
+
console.error(`Error: ${arg} requires a value`);
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
const value = args[++i];
|
|
983
|
+
if (arg === "--output") outputPath = value;
|
|
984
|
+
else if (arg === "--config") configArg = value;
|
|
985
|
+
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
986
|
+
else if (arg === "--concurrency") {
|
|
987
|
+
const n = parseInt(value, 10);
|
|
988
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
989
|
+
console.error(`[axl-eval] Ignoring invalid --concurrency "${value}"; using 1.`);
|
|
990
|
+
concurrency = 1;
|
|
991
|
+
} else {
|
|
992
|
+
concurrency = n;
|
|
993
|
+
}
|
|
994
|
+
} else if (arg === "--scorers") {
|
|
995
|
+
scorerNames = [
|
|
996
|
+
...new Set(
|
|
997
|
+
value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
998
|
+
)
|
|
999
|
+
];
|
|
1000
|
+
} else {
|
|
1001
|
+
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
1002
|
+
}
|
|
1003
|
+
} else if (arg === "--capture-traces") {
|
|
1004
|
+
captureTraces = true;
|
|
1005
|
+
} else if (arg.startsWith("--")) {
|
|
1006
|
+
if (!KNOWN_FLAGS.has(arg)) {
|
|
1007
|
+
console.error(`Unknown flag: ${arg}`);
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
paths.push(arg);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
outputPath,
|
|
1016
|
+
configArg,
|
|
1017
|
+
conditions,
|
|
1018
|
+
runs,
|
|
1019
|
+
captureTraces,
|
|
1020
|
+
concurrency,
|
|
1021
|
+
scorerNames,
|
|
1022
|
+
paths
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/cli.ts
|
|
1027
|
+
var import_meta = {};
|
|
1028
|
+
function refuseToGate(reason) {
|
|
1029
|
+
console.error(`[axl-eval] Refusing to gate: ${reason}`);
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
function scorerSampleCounts(entry, name, items) {
|
|
1033
|
+
if (entry.scored != null && entry.failed != null) {
|
|
1034
|
+
return { scored: entry.scored, failed: entry.failed };
|
|
1035
|
+
}
|
|
1036
|
+
if (items) return scorerCounts(items, name);
|
|
1037
|
+
return { scored: entry.scored ?? 0, failed: entry.failed ?? 0 };
|
|
1038
|
+
}
|
|
1039
|
+
function installAbortHandler(controller) {
|
|
1040
|
+
let signalled = false;
|
|
1041
|
+
const onSignal = (sig) => {
|
|
1042
|
+
if (signalled) {
|
|
1043
|
+
console.error(`
|
|
1044
|
+
[axl-eval] Second ${sig} \u2014 forcing exit.`);
|
|
1045
|
+
process.exit(130);
|
|
1046
|
+
}
|
|
1047
|
+
signalled = true;
|
|
1048
|
+
console.error(`
|
|
1049
|
+
[axl-eval] ${sig} received \u2014 cancelling eval (press again to force exit)...`);
|
|
1050
|
+
controller.abort();
|
|
1051
|
+
};
|
|
1052
|
+
process.on("SIGINT", onSignal);
|
|
1053
|
+
process.on("SIGTERM", onSignal);
|
|
1054
|
+
}
|
|
866
1055
|
async function main() {
|
|
867
1056
|
const args = process.argv.slice(2);
|
|
868
1057
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
@@ -873,6 +1062,15 @@ Usage:
|
|
|
873
1062
|
axl-eval <path> --output <file> Save results to JSON
|
|
874
1063
|
axl-eval <path> --config <file> Use config file for runtime
|
|
875
1064
|
axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
|
|
1065
|
+
axl-eval <path> --concurrency <n> Override item concurrency (flag > env
|
|
1066
|
+
AXL_EVAL_CONCURRENCY > config > 5).
|
|
1067
|
+
Max concurrent scorer calls is
|
|
1068
|
+
concurrency x scorerConcurrency (the
|
|
1069
|
+
per-eval scorerConcurrency defaults to 5).
|
|
1070
|
+
axl-eval <path> --scorers <a,b> Run only the named scorers (single eval
|
|
1071
|
+
file only). Stamps the result as a
|
|
1072
|
+
subset so compare won't treat it as a
|
|
1073
|
+
full baseline.
|
|
876
1074
|
axl-eval <path> --capture-traces Populate EvalItem.traces on every item
|
|
877
1075
|
(success + failure). Adds memory
|
|
878
1076
|
overhead proportional to dataset size
|
|
@@ -881,6 +1079,12 @@ Usage:
|
|
|
881
1079
|
axl-eval compare <a> <b> Compare two eval result files
|
|
882
1080
|
axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
|
|
883
1081
|
axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
|
|
1082
|
+
axl-eval compare <a> <b> --max-scorer-error-rate <0..1> Exit 1 if a scorer's
|
|
1083
|
+
failure rate exceeds the limit on
|
|
1084
|
+
either side (deterministic scorers
|
|
1085
|
+
tolerate zero failures). Always warns
|
|
1086
|
+
when either side rests on a thinned
|
|
1087
|
+
sample, even without this flag.
|
|
884
1088
|
|
|
885
1089
|
Config auto-detection (when --config is not specified):
|
|
886
1090
|
${import_axl.CONFIG_CANDIDATES.join(" -> ")}
|
|
@@ -895,11 +1099,13 @@ When no config is found, a bare AxlRuntime is created (providers from env vars).
|
|
|
895
1099
|
await runCompare(args.slice(1));
|
|
896
1100
|
return;
|
|
897
1101
|
}
|
|
1102
|
+
const controller = new AbortController();
|
|
1103
|
+
installAbortHandler(controller);
|
|
898
1104
|
if (args[0] === "rescore") {
|
|
899
|
-
await runRescore(args.slice(1));
|
|
1105
|
+
await runRescore(args.slice(1), controller.signal);
|
|
900
1106
|
return;
|
|
901
1107
|
}
|
|
902
|
-
await runEvalCommand(args);
|
|
1108
|
+
await runEvalCommand(args, controller.signal);
|
|
903
1109
|
}
|
|
904
1110
|
function parseThresholdArg(args) {
|
|
905
1111
|
const idx = args.indexOf("--threshold");
|
|
@@ -913,20 +1119,61 @@ function parseThresholdArg(args) {
|
|
|
913
1119
|
}
|
|
914
1120
|
return Object.keys(map).length > 0 ? map : void 0;
|
|
915
1121
|
}
|
|
1122
|
+
function parseMaxScorerErrorRate(args) {
|
|
1123
|
+
const idx = args.indexOf("--max-scorer-error-rate");
|
|
1124
|
+
if (idx === -1) return void 0;
|
|
1125
|
+
if (idx + 1 >= args.length) {
|
|
1126
|
+
console.error("Error: --max-scorer-error-rate requires a value in [0, 1]");
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
}
|
|
1129
|
+
const raw = args[idx + 1];
|
|
1130
|
+
const n = /^\d*\.?\d+$/.test(raw.trim()) ? Number(raw) : NaN;
|
|
1131
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
1132
|
+
console.error(`Error: --max-scorer-error-rate must be a number in [0, 1], got "${raw}"`);
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
return n;
|
|
1136
|
+
}
|
|
1137
|
+
function scorerFilteredScorers(r) {
|
|
1138
|
+
const first = Array.isArray(r) ? r[0] : r;
|
|
1139
|
+
const meta = first?.metadata;
|
|
1140
|
+
if (!meta || meta.scorerFiltered !== true) return void 0;
|
|
1141
|
+
return Array.isArray(meta.scorersRun) ? meta.scorersRun : [];
|
|
1142
|
+
}
|
|
916
1143
|
async function runCompare(args) {
|
|
917
1144
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
918
1145
|
const thresholds = parseThresholdArg(args);
|
|
1146
|
+
const maxScorerErrorRate = parseMaxScorerErrorRate(args);
|
|
1147
|
+
const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
|
|
919
1148
|
const files = args.filter(
|
|
920
|
-
(a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1]
|
|
1149
|
+
(a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
|
|
921
1150
|
);
|
|
922
1151
|
if (files.length !== 2) {
|
|
923
|
-
console.error(
|
|
1152
|
+
console.error(
|
|
1153
|
+
"Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>]"
|
|
1154
|
+
);
|
|
924
1155
|
process.exit(1);
|
|
925
1156
|
}
|
|
926
1157
|
const baselineRaw = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
|
|
927
1158
|
const candidateRaw = JSON.parse(await (0, import_promises.readFile)(files[1], "utf-8"));
|
|
928
1159
|
const baseline = baselineRaw;
|
|
929
1160
|
const candidate = candidateRaw;
|
|
1161
|
+
const filteredSides = [
|
|
1162
|
+
["baseline", baseline],
|
|
1163
|
+
["candidate", candidate]
|
|
1164
|
+
].filter(([, r]) => scorerFilteredScorers(r) != null);
|
|
1165
|
+
for (const [side, r] of filteredSides) {
|
|
1166
|
+
const ran = scorerFilteredScorers(r);
|
|
1167
|
+
const ranStr = ran && ran.length ? ` (ran: ${ran.join(", ")})` : "";
|
|
1168
|
+
console.error(
|
|
1169
|
+
`[axl-eval] WARNING: ${side} was run with --scorers \u2014 it tested only a subset${ranStr}, so this comparison is incomplete.`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
if (failOnRegression && filteredSides.length > 0) {
|
|
1173
|
+
refuseToGate(
|
|
1174
|
+
`${filteredSides.map(([s]) => s).join(", ")} was run with --scorers (a scorer subset); re-run without --scorers for a complete baseline.`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
930
1177
|
const compareOptions = thresholds != null ? { thresholds } : void 0;
|
|
931
1178
|
const comparison = evalCompare(baseline, candidate, compareOptions);
|
|
932
1179
|
console.log(
|
|
@@ -982,6 +1229,29 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
982
1229
|
Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
|
|
983
1230
|
`
|
|
984
1231
|
);
|
|
1232
|
+
let sawThinnedSample = false;
|
|
1233
|
+
for (const name of scorerNames) {
|
|
1234
|
+
const s = comparison.scorers[name];
|
|
1235
|
+
const bFailed = s.baselineFailed ?? 0;
|
|
1236
|
+
const cFailed = s.candidateFailed ?? 0;
|
|
1237
|
+
if (bFailed > 0 || cFailed > 0) {
|
|
1238
|
+
sawThinnedSample = true;
|
|
1239
|
+
const bAtt = (s.baselineScored ?? 0) + bFailed;
|
|
1240
|
+
const cAtt = (s.candidateScored ?? 0) + cFailed;
|
|
1241
|
+
console.error(
|
|
1242
|
+
`[axl-eval] WARNING: scorer "${name}" failed on some items (baseline ${bFailed}/${bAtt}, candidate ${cFailed}/${cAtt}); the means above are computed over the surviving sample.`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (sawThinnedSample && maxScorerErrorRate == null) {
|
|
1247
|
+
console.error(
|
|
1248
|
+
`[axl-eval] (advisory \u2014 pass --max-scorer-error-rate <0..1> to fail CI on a thinned sample)`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
if (maxScorerErrorRate != null) {
|
|
1252
|
+
const reason = evaluateScorerErrorRateGate(comparison, maxScorerErrorRate);
|
|
1253
|
+
if (reason) refuseToGate(reason);
|
|
1254
|
+
}
|
|
985
1255
|
if (failOnRegression && comparison.regressions.length > 0) {
|
|
986
1256
|
const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
|
|
987
1257
|
if (hasSignificance) {
|
|
@@ -994,12 +1264,19 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
994
1264
|
}
|
|
995
1265
|
}
|
|
996
1266
|
}
|
|
997
|
-
async function runRescore(args) {
|
|
998
|
-
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
1267
|
+
async function runRescore(args, signal) {
|
|
1268
|
+
const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
|
|
999
1269
|
if (paths.length < 2) {
|
|
1000
1270
|
console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
|
|
1001
1271
|
process.exit(1);
|
|
1002
1272
|
}
|
|
1273
|
+
if (scorerNames?.length) {
|
|
1274
|
+
console.error(
|
|
1275
|
+
"Error: --scorers is not supported with rescore; pass a scorer subset in the eval file instead."
|
|
1276
|
+
);
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
const itemConcurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY");
|
|
1003
1280
|
const [resultsPath, evalFilePath] = paths;
|
|
1004
1281
|
const raw = JSON.parse(await (0, import_promises.readFile)(resultsPath, "utf-8"));
|
|
1005
1282
|
const results = Array.isArray(raw) ? raw : [raw];
|
|
@@ -1019,7 +1296,13 @@ async function runRescore(args) {
|
|
|
1019
1296
|
}
|
|
1020
1297
|
const rescored = [];
|
|
1021
1298
|
for (const resultData of results) {
|
|
1022
|
-
|
|
1299
|
+
if (signal.aborted) break;
|
|
1300
|
+
rescored.push(
|
|
1301
|
+
await rescore(resultData, evalConfig.scorers, runtime, {
|
|
1302
|
+
signal,
|
|
1303
|
+
...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
|
|
1304
|
+
})
|
|
1305
|
+
);
|
|
1023
1306
|
}
|
|
1024
1307
|
for (const r of rescored) {
|
|
1025
1308
|
console.log("\n" + formatTable(r) + "\n");
|
|
@@ -1069,14 +1352,15 @@ function formatTable(result) {
|
|
|
1069
1352
|
lines.push(" " + "\u2500".repeat(ruleLen));
|
|
1070
1353
|
for (const name of scorerNames) {
|
|
1071
1354
|
const s = result.summary.scorers[name];
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1355
|
+
const { scored, failed } = scorerSampleCounts(s, name, result.items);
|
|
1356
|
+
const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
|
|
1357
|
+
if (scored === 0) {
|
|
1074
1358
|
lines.push(
|
|
1075
|
-
` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}`
|
|
1359
|
+
` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}${annotation}`
|
|
1076
1360
|
);
|
|
1077
1361
|
} else {
|
|
1078
1362
|
lines.push(
|
|
1079
|
-
` ${name.padEnd(maxNameLen)} ${s.mean.toFixed(2).padStart(colWidth)} ${s.min.toFixed(2).padStart(colWidth)} ${s.max.toFixed(2).padStart(colWidth)} ${s.p50.toFixed(2).padStart(colWidth)} ${s.p95.toFixed(2).padStart(colWidth)}`
|
|
1363
|
+
` ${name.padEnd(maxNameLen)} ${s.mean.toFixed(2).padStart(colWidth)} ${s.min.toFixed(2).padStart(colWidth)} ${s.max.toFixed(2).padStart(colWidth)} ${s.p50.toFixed(2).padStart(colWidth)} ${s.p95.toFixed(2).padStart(colWidth)}${annotation}`
|
|
1080
1364
|
);
|
|
1081
1365
|
}
|
|
1082
1366
|
}
|
|
@@ -1124,8 +1408,10 @@ function formatMultiRunTable(summary) {
|
|
|
1124
1408
|
for (const name of scorerNames) {
|
|
1125
1409
|
const s = summary.scorers[name];
|
|
1126
1410
|
const meanStd = `${s.mean.toFixed(3)} \xB1 ${s.std.toFixed(3)}`;
|
|
1411
|
+
const { scored, failed } = scorerSampleCounts(s, name);
|
|
1412
|
+
const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
|
|
1127
1413
|
lines.push(
|
|
1128
|
-
` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}`
|
|
1414
|
+
` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}${annotation}`
|
|
1129
1415
|
);
|
|
1130
1416
|
}
|
|
1131
1417
|
if (summary.timing) {
|
|
@@ -1139,6 +1425,26 @@ function formatMultiRunTable(summary) {
|
|
|
1139
1425
|
lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
|
|
1140
1426
|
return lines.join("\n");
|
|
1141
1427
|
}
|
|
1428
|
+
function reportDegraded(result, label) {
|
|
1429
|
+
const d = result.summary.degraded;
|
|
1430
|
+
if (!d || d.length === 0) return false;
|
|
1431
|
+
for (const x of d) {
|
|
1432
|
+
const attempted = x.scored + x.failed;
|
|
1433
|
+
const limitStr = x.type === "deterministic" ? "deterministic zero-tolerance" : `${(x.limit * 100).toFixed(1)}% limit`;
|
|
1434
|
+
console.error(
|
|
1435
|
+
`[axl-eval] DEGRADED: ${label} scorer "${x.scorer}" (${x.type}) failed ${(x.rate * 100).toFixed(1)}% (${x.failed}/${attempted}), over the ${limitStr}.`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
function reportTotalWipeout(result, label) {
|
|
1441
|
+
const { count, failures } = result.summary;
|
|
1442
|
+
if (count === 0 || failures < count) return false;
|
|
1443
|
+
console.error(
|
|
1444
|
+
`[axl-eval] FAILED: ${label} \u2014 all ${count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
|
|
1445
|
+
);
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1142
1448
|
async function resolveRuntimeFromConfig(configPath) {
|
|
1143
1449
|
try {
|
|
1144
1450
|
const mod = await (0, import_axl.importModule)(configPath, import_meta.url);
|
|
@@ -1199,41 +1505,17 @@ async function getRuntime(configArg, conditions) {
|
|
|
1199
1505
|
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
1200
1506
|
return new AxlRuntime();
|
|
1201
1507
|
}
|
|
1202
|
-
function
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
console.error(`Error: ${arg} requires a value`);
|
|
1214
|
-
process.exit(1);
|
|
1215
|
-
}
|
|
1216
|
-
const value = args[++i];
|
|
1217
|
-
if (arg === "--output") outputPath = value;
|
|
1218
|
-
else if (arg === "--config") configArg = value;
|
|
1219
|
-
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
1220
|
-
else
|
|
1221
|
-
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
1222
|
-
} else if (arg === "--capture-traces") {
|
|
1223
|
-
captureTraces = true;
|
|
1224
|
-
} else if (arg.startsWith("--")) {
|
|
1225
|
-
if (!KNOWN_FLAGS.has(arg)) {
|
|
1226
|
-
console.error(`Unknown flag: ${arg}`);
|
|
1227
|
-
process.exit(1);
|
|
1228
|
-
}
|
|
1229
|
-
} else {
|
|
1230
|
-
paths.push(arg);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
return { outputPath, configArg, conditions, runs, captureTraces, paths };
|
|
1234
|
-
}
|
|
1235
|
-
async function runEvalCommand(args) {
|
|
1236
|
-
const { outputPath, configArg, conditions, runs, captureTraces, paths } = parseEvalArgs(args);
|
|
1508
|
+
async function runEvalCommand(args, signal) {
|
|
1509
|
+
const {
|
|
1510
|
+
outputPath,
|
|
1511
|
+
configArg,
|
|
1512
|
+
conditions,
|
|
1513
|
+
runs,
|
|
1514
|
+
captureTraces,
|
|
1515
|
+
concurrency,
|
|
1516
|
+
scorerNames,
|
|
1517
|
+
paths
|
|
1518
|
+
} = parseEvalArgs(args);
|
|
1237
1519
|
if (paths.length === 0) {
|
|
1238
1520
|
console.error("Error: No eval file path provided");
|
|
1239
1521
|
process.exit(1);
|
|
@@ -1246,11 +1528,16 @@ async function runEvalCommand(args) {
|
|
|
1246
1528
|
console.error("Error: No eval files found");
|
|
1247
1529
|
process.exit(1);
|
|
1248
1530
|
}
|
|
1531
|
+
if (scorerNames?.length && evalFiles.length > 1) {
|
|
1532
|
+
console.error(`Error: --scorers requires a single eval file (matched ${evalFiles.length}).`);
|
|
1533
|
+
process.exit(1);
|
|
1534
|
+
}
|
|
1249
1535
|
const runtime = await getRuntime(configArg, conditions);
|
|
1250
1536
|
const results = [];
|
|
1251
1537
|
let failedFiles = 0;
|
|
1252
1538
|
try {
|
|
1253
1539
|
for (const filePath of evalFiles) {
|
|
1540
|
+
if (signal.aborted) break;
|
|
1254
1541
|
try {
|
|
1255
1542
|
const mod = await (0, import_axl.importModule)(path.resolve(filePath), import_meta.url);
|
|
1256
1543
|
const evalConfig = (0, import_axl.pickDefault)(mod);
|
|
@@ -1260,6 +1547,22 @@ async function runEvalCommand(args) {
|
|
|
1260
1547
|
failedFiles++;
|
|
1261
1548
|
continue;
|
|
1262
1549
|
}
|
|
1550
|
+
evalConfig.concurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY") ?? evalConfig.concurrency ?? 5;
|
|
1551
|
+
if (scorerNames?.length) {
|
|
1552
|
+
const available = evalConfig.scorers.map((s) => s.name);
|
|
1553
|
+
const unknown = scorerNames.filter((n) => !available.includes(n));
|
|
1554
|
+
if (unknown.length) {
|
|
1555
|
+
console.error(
|
|
1556
|
+
`Error: --scorers: unknown scorer(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`
|
|
1557
|
+
);
|
|
1558
|
+
process.exit(1);
|
|
1559
|
+
}
|
|
1560
|
+
evalConfig.scorers = evalConfig.scorers.filter((s) => scorerNames.includes(s.name));
|
|
1561
|
+
if (evalConfig.scorers.length === 0) {
|
|
1562
|
+
console.error("Error: --scorers matched no scorers.");
|
|
1563
|
+
process.exit(1);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1263
1566
|
const exported = (0, import_axl.pickExport)(mod, "executeWorkflow");
|
|
1264
1567
|
let customExecute;
|
|
1265
1568
|
if (typeof exported === "function") {
|
|
@@ -1315,18 +1618,27 @@ async function runEvalCommand(args) {
|
|
|
1315
1618
|
failedFiles++;
|
|
1316
1619
|
continue;
|
|
1317
1620
|
}
|
|
1318
|
-
const runOptions = captureTraces ? { captureTraces: true } :
|
|
1621
|
+
const runOptions = { signal, ...captureTraces ? { captureTraces: true } : {} };
|
|
1622
|
+
const scorersRun = scorerNames?.length ? evalConfig.scorers.map((s) => s.name) : void 0;
|
|
1623
|
+
const stampFiltered = (result) => {
|
|
1624
|
+
if (scorersRun) {
|
|
1625
|
+
result.metadata.scorerFiltered = true;
|
|
1626
|
+
result.metadata.scorersRun = scorersRun;
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1319
1629
|
if (runs > 1) {
|
|
1320
1630
|
const { randomUUID: randomUUID4 } = await import("crypto");
|
|
1321
1631
|
const runGroupId = randomUUID4();
|
|
1322
1632
|
const runResults = [];
|
|
1323
1633
|
let runFailure;
|
|
1324
1634
|
for (let r = 0; r < runs; r++) {
|
|
1635
|
+
if (signal.aborted) break;
|
|
1325
1636
|
console.error(`[axl-eval] Run ${r + 1}/${runs}...`);
|
|
1326
1637
|
try {
|
|
1327
1638
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
1328
1639
|
result.metadata.runGroupId = runGroupId;
|
|
1329
1640
|
result.metadata.runIndex = r;
|
|
1641
|
+
stampFiltered(result);
|
|
1330
1642
|
runResults.push(result);
|
|
1331
1643
|
} catch (err) {
|
|
1332
1644
|
runFailure = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1358,10 +1670,21 @@ async function runEvalCommand(args) {
|
|
|
1358
1670
|
const summary = aggregateRuns(runResults);
|
|
1359
1671
|
console.log("\n" + formatMultiRunTable(summary) + "\n");
|
|
1360
1672
|
for (const r of runResults) results.push(r);
|
|
1673
|
+
let anyFailing = false;
|
|
1674
|
+
for (const r of runResults) {
|
|
1675
|
+
const wipeout = reportTotalWipeout(r, filePath);
|
|
1676
|
+
const degraded = reportDegraded(r, filePath);
|
|
1677
|
+
if (wipeout || degraded) anyFailing = true;
|
|
1678
|
+
}
|
|
1679
|
+
if (anyFailing && !partial) failedFiles++;
|
|
1361
1680
|
} else {
|
|
1362
1681
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
1682
|
+
stampFiltered(result);
|
|
1363
1683
|
results.push(result);
|
|
1364
1684
|
console.log("\n" + formatTable(result) + "\n");
|
|
1685
|
+
const wipeout = reportTotalWipeout(result, filePath);
|
|
1686
|
+
const degraded = reportDegraded(result, filePath);
|
|
1687
|
+
if (wipeout || degraded) failedFiles++;
|
|
1365
1688
|
}
|
|
1366
1689
|
} catch (err) {
|
|
1367
1690
|
console.error(
|