@axlsdk/eval 0.17.9 → 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 +48 -7
- package/dist/{chunk-NETD4AUB.js → chunk-BWBX5YVQ.js} +231 -162
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +497 -207
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +279 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +301 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -1
- package/dist/index.d.ts +176 -1
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NETD4AUB.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) => {
|
|
@@ -461,89 +589,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
461
589
|
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
462
590
|
budgetExceeded = true;
|
|
463
591
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
item.input,
|
|
473
|
-
item.annotations,
|
|
474
|
-
scorerContext
|
|
475
|
-
);
|
|
476
|
-
const scorerResult = normalizeScorerResult(raw);
|
|
477
|
-
if (scorerResult.cost != null) {
|
|
478
|
-
itemScorerCost += scorerResult.cost;
|
|
479
|
-
totalCost += scorerResult.cost;
|
|
480
|
-
}
|
|
481
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
482
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
483
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
484
|
-
evalItem.scorerErrors.push(
|
|
485
|
-
`Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
486
|
-
);
|
|
487
|
-
evalItem.scores[scorer.name] = null;
|
|
488
|
-
evalItem.scoreDetails[scorer.name] = {
|
|
489
|
-
score: null,
|
|
490
|
-
metadata: scorerResult.metadata,
|
|
491
|
-
duration: scorerDuration,
|
|
492
|
-
cost: scorerResult.cost
|
|
493
|
-
};
|
|
494
|
-
} else {
|
|
495
|
-
evalItem.scores[scorer.name] = round3(scorerResult.score);
|
|
496
|
-
evalItem.scoreDetails[scorer.name] = {
|
|
497
|
-
score: round3(scorerResult.score),
|
|
498
|
-
metadata: scorerResult.metadata,
|
|
499
|
-
duration: scorerDuration,
|
|
500
|
-
cost: scorerResult.cost
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
} catch (err) {
|
|
504
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
505
|
-
if (errCost != null) {
|
|
506
|
-
itemScorerCost += errCost;
|
|
507
|
-
totalCost += errCost;
|
|
508
|
-
}
|
|
509
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
510
|
-
evalItem.scorerErrors.push(
|
|
511
|
-
`Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
512
|
-
);
|
|
513
|
-
evalItem.scores[scorer.name] = null;
|
|
514
|
-
evalItem.scoreDetails[scorer.name] = {
|
|
515
|
-
score: null,
|
|
516
|
-
duration: Date.now() - scorerStart,
|
|
517
|
-
cost: errCost
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
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;
|
|
522
600
|
evalItems[itemIndex] = evalItem;
|
|
523
601
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
524
602
|
}
|
|
525
|
-
|
|
526
|
-
async function runNext() {
|
|
527
|
-
while (index < items.length) {
|
|
528
|
-
const currentIndex = index++;
|
|
529
|
-
await processItem(items[currentIndex], currentIndex);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
533
|
-
await Promise.all(workers);
|
|
603
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
534
604
|
const failures = evalItems.filter((i) => i.error).length;
|
|
535
605
|
const scorerNames = config.scorers.map((s) => s.name);
|
|
536
606
|
const scorerStats = {};
|
|
537
607
|
for (const name of scorerNames) {
|
|
538
608
|
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
539
|
-
|
|
609
|
+
const { scored, failed } = scorerCounts(evalItems, name);
|
|
610
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
540
611
|
}
|
|
541
|
-
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
542
|
-
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
543
612
|
const scorerTypes = {};
|
|
544
613
|
for (const s of config.scorers) {
|
|
545
614
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
546
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;
|
|
547
639
|
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
548
640
|
for (const item of evalItems) {
|
|
549
641
|
const itemCounts = item.metadata?.modelCallCounts;
|
|
@@ -602,13 +694,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
602
694
|
...config.metadata,
|
|
603
695
|
scorerTypes,
|
|
604
696
|
...modelsMeta,
|
|
605
|
-
...workflowsMeta
|
|
697
|
+
...workflowsMeta,
|
|
698
|
+
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
606
699
|
},
|
|
607
700
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
608
701
|
totalCost,
|
|
609
702
|
duration: Date.now() - startTime,
|
|
610
703
|
items: evalItems,
|
|
611
|
-
summary: {
|
|
704
|
+
summary: {
|
|
705
|
+
count: items.length,
|
|
706
|
+
failures,
|
|
707
|
+
scorers: scorerStats,
|
|
708
|
+
timing,
|
|
709
|
+
...degraded ? { degraded } : {}
|
|
710
|
+
}
|
|
612
711
|
};
|
|
613
712
|
}
|
|
614
713
|
|
|
@@ -617,6 +716,7 @@ var import_node_crypto2 = require("crypto");
|
|
|
617
716
|
async function rescore(result, scorers, runtime, options) {
|
|
618
717
|
const startTime = Date.now();
|
|
619
718
|
const concurrency = options?.concurrency ?? 5;
|
|
719
|
+
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
620
720
|
const scorerContext = {
|
|
621
721
|
resolveProvider: (uri) => {
|
|
622
722
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -660,77 +760,24 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
660
760
|
scores: {},
|
|
661
761
|
scoreDetails: {}
|
|
662
762
|
};
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
totalCost += scorerResult.cost;
|
|
672
|
-
}
|
|
673
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
674
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
675
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
676
|
-
item.scorerErrors.push(
|
|
677
|
-
`Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
678
|
-
);
|
|
679
|
-
item.scores[scorer.name] = null;
|
|
680
|
-
item.scoreDetails[scorer.name] = {
|
|
681
|
-
score: null,
|
|
682
|
-
metadata: scorerResult.metadata,
|
|
683
|
-
duration: scorerDuration,
|
|
684
|
-
cost: scorerResult.cost
|
|
685
|
-
};
|
|
686
|
-
} else {
|
|
687
|
-
item.scores[scorer.name] = round3(scorerResult.score);
|
|
688
|
-
item.scoreDetails[scorer.name] = {
|
|
689
|
-
score: round3(scorerResult.score),
|
|
690
|
-
metadata: scorerResult.metadata,
|
|
691
|
-
duration: scorerDuration,
|
|
692
|
-
cost: scorerResult.cost
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
} catch (err) {
|
|
696
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
697
|
-
if (errCost != null) {
|
|
698
|
-
itemScorerCost += errCost;
|
|
699
|
-
totalCost += errCost;
|
|
700
|
-
}
|
|
701
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
702
|
-
item.scorerErrors.push(
|
|
703
|
-
`Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
704
|
-
);
|
|
705
|
-
item.scores[scorer.name] = null;
|
|
706
|
-
item.scoreDetails[scorer.name] = {
|
|
707
|
-
score: null,
|
|
708
|
-
duration: Date.now() - scorerStart,
|
|
709
|
-
cost: errCost
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
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;
|
|
714
771
|
rescored[itemIndex] = item;
|
|
715
772
|
}
|
|
716
|
-
|
|
717
|
-
async function runNext() {
|
|
718
|
-
while (index < result.items.length) {
|
|
719
|
-
const currentIndex = index++;
|
|
720
|
-
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
const workers = Array.from(
|
|
724
|
-
{ length: Math.min(concurrency, result.items.length) },
|
|
725
|
-
() => runNext()
|
|
726
|
-
);
|
|
727
|
-
await Promise.all(workers);
|
|
773
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
728
774
|
const failures = rescored.filter((i) => i.error).length;
|
|
729
775
|
const scorerNames = scorers.map((s) => s.name);
|
|
730
776
|
const scorerStats = {};
|
|
731
777
|
for (const name of scorerNames) {
|
|
732
778
|
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
733
|
-
|
|
779
|
+
const { scored, failed } = scorerCounts(rescored, name);
|
|
780
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed };
|
|
734
781
|
}
|
|
735
782
|
const scorerTypes = {};
|
|
736
783
|
for (const s of scorers) {
|
|
@@ -740,7 +787,11 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
740
787
|
id: (0, import_node_crypto2.randomUUID)(),
|
|
741
788
|
dataset: result.dataset,
|
|
742
789
|
metadata: (() => {
|
|
743
|
-
const
|
|
790
|
+
const rest = { ...result.metadata };
|
|
791
|
+
delete rest.runGroupId;
|
|
792
|
+
delete rest.runIndex;
|
|
793
|
+
delete rest.scorerFiltered;
|
|
794
|
+
delete rest.scorersRun;
|
|
744
795
|
const merged = {
|
|
745
796
|
...rest,
|
|
746
797
|
rescored: true,
|
|
@@ -793,15 +844,24 @@ function aggregateRuns(runs) {
|
|
|
793
844
|
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
794
845
|
const scorers = {};
|
|
795
846
|
for (const name of scorerNames) {
|
|
796
|
-
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);
|
|
797
853
|
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
798
854
|
const minMean = Math.min(...means);
|
|
799
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);
|
|
800
858
|
scorers[name] = {
|
|
801
|
-
mean:
|
|
802
|
-
std:
|
|
803
|
-
min:
|
|
804
|
-
max:
|
|
859
|
+
mean: round2(meanOfMeans),
|
|
860
|
+
std: round2(std(means, meanOfMeans)),
|
|
861
|
+
min: round2(minMean),
|
|
862
|
+
max: round2(maxMean),
|
|
863
|
+
scored,
|
|
864
|
+
failed
|
|
805
865
|
};
|
|
806
866
|
}
|
|
807
867
|
let timing;
|
|
@@ -809,8 +869,8 @@ function aggregateRuns(runs) {
|
|
|
809
869
|
if (timingMeans.length === runs.length && timingMeans.length > 0) {
|
|
810
870
|
const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
|
|
811
871
|
timing = {
|
|
812
|
-
mean:
|
|
813
|
-
std:
|
|
872
|
+
mean: round2(meanTiming),
|
|
873
|
+
std: round2(std(timingMeans, meanTiming))
|
|
814
874
|
};
|
|
815
875
|
}
|
|
816
876
|
return {
|
|
@@ -818,8 +878,8 @@ function aggregateRuns(runs) {
|
|
|
818
878
|
runCount: runs.length,
|
|
819
879
|
workflows,
|
|
820
880
|
dataset,
|
|
821
|
-
totalCost:
|
|
822
|
-
totalDuration:
|
|
881
|
+
totalCost: round2(totalCost),
|
|
882
|
+
totalDuration: round2(totalDuration),
|
|
823
883
|
scorers,
|
|
824
884
|
timing
|
|
825
885
|
};
|
|
@@ -861,11 +921,22 @@ function validateEvalConfig(cfg) {
|
|
|
861
921
|
if (badScorerIdx !== -1) {
|
|
862
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).`;
|
|
863
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
|
+
}
|
|
864
936
|
return void 0;
|
|
865
937
|
}
|
|
866
938
|
|
|
867
|
-
// src/cli.ts
|
|
868
|
-
var import_meta = {};
|
|
939
|
+
// src/cli-args.ts
|
|
869
940
|
var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
870
941
|
"--output",
|
|
871
942
|
"--config",
|
|
@@ -873,8 +944,98 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
873
944
|
"--fail-on-regression",
|
|
874
945
|
"--threshold",
|
|
875
946
|
"--runs",
|
|
876
|
-
"--capture-traces"
|
|
947
|
+
"--capture-traces",
|
|
948
|
+
"--concurrency",
|
|
949
|
+
"--scorers"
|
|
950
|
+
]);
|
|
951
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
952
|
+
"--output",
|
|
953
|
+
"--config",
|
|
954
|
+
"--conditions",
|
|
955
|
+
"--runs",
|
|
956
|
+
"--concurrency",
|
|
957
|
+
"--scorers"
|
|
877
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
|
+
}
|
|
878
1039
|
function installAbortHandler(controller) {
|
|
879
1040
|
let signalled = false;
|
|
880
1041
|
const onSignal = (sig) => {
|
|
@@ -901,6 +1062,15 @@ Usage:
|
|
|
901
1062
|
axl-eval <path> --output <file> Save results to JSON
|
|
902
1063
|
axl-eval <path> --config <file> Use config file for runtime
|
|
903
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.
|
|
904
1074
|
axl-eval <path> --capture-traces Populate EvalItem.traces on every item
|
|
905
1075
|
(success + failure). Adds memory
|
|
906
1076
|
overhead proportional to dataset size
|
|
@@ -909,6 +1079,12 @@ Usage:
|
|
|
909
1079
|
axl-eval compare <a> <b> Compare two eval result files
|
|
910
1080
|
axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
|
|
911
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.
|
|
912
1088
|
|
|
913
1089
|
Config auto-detection (when --config is not specified):
|
|
914
1090
|
${import_axl.CONFIG_CANDIDATES.join(" -> ")}
|
|
@@ -943,20 +1119,61 @@ function parseThresholdArg(args) {
|
|
|
943
1119
|
}
|
|
944
1120
|
return Object.keys(map).length > 0 ? map : void 0;
|
|
945
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
|
+
}
|
|
946
1143
|
async function runCompare(args) {
|
|
947
1144
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
948
1145
|
const thresholds = parseThresholdArg(args);
|
|
1146
|
+
const maxScorerErrorRate = parseMaxScorerErrorRate(args);
|
|
1147
|
+
const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
|
|
949
1148
|
const files = args.filter(
|
|
950
|
-
(a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1]
|
|
1149
|
+
(a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
|
|
951
1150
|
);
|
|
952
1151
|
if (files.length !== 2) {
|
|
953
|
-
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
|
+
);
|
|
954
1155
|
process.exit(1);
|
|
955
1156
|
}
|
|
956
1157
|
const baselineRaw = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
|
|
957
1158
|
const candidateRaw = JSON.parse(await (0, import_promises.readFile)(files[1], "utf-8"));
|
|
958
1159
|
const baseline = baselineRaw;
|
|
959
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
|
+
}
|
|
960
1177
|
const compareOptions = thresholds != null ? { thresholds } : void 0;
|
|
961
1178
|
const comparison = evalCompare(baseline, candidate, compareOptions);
|
|
962
1179
|
console.log(
|
|
@@ -1012,6 +1229,29 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
1012
1229
|
Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
|
|
1013
1230
|
`
|
|
1014
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
|
+
}
|
|
1015
1255
|
if (failOnRegression && comparison.regressions.length > 0) {
|
|
1016
1256
|
const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
|
|
1017
1257
|
if (hasSignificance) {
|
|
@@ -1025,11 +1265,18 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
1025
1265
|
}
|
|
1026
1266
|
}
|
|
1027
1267
|
async function runRescore(args, signal) {
|
|
1028
|
-
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
1268
|
+
const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
|
|
1029
1269
|
if (paths.length < 2) {
|
|
1030
1270
|
console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
|
|
1031
1271
|
process.exit(1);
|
|
1032
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");
|
|
1033
1280
|
const [resultsPath, evalFilePath] = paths;
|
|
1034
1281
|
const raw = JSON.parse(await (0, import_promises.readFile)(resultsPath, "utf-8"));
|
|
1035
1282
|
const results = Array.isArray(raw) ? raw : [raw];
|
|
@@ -1050,7 +1297,12 @@ async function runRescore(args, signal) {
|
|
|
1050
1297
|
const rescored = [];
|
|
1051
1298
|
for (const resultData of results) {
|
|
1052
1299
|
if (signal.aborted) break;
|
|
1053
|
-
rescored.push(
|
|
1300
|
+
rescored.push(
|
|
1301
|
+
await rescore(resultData, evalConfig.scorers, runtime, {
|
|
1302
|
+
signal,
|
|
1303
|
+
...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
|
|
1304
|
+
})
|
|
1305
|
+
);
|
|
1054
1306
|
}
|
|
1055
1307
|
for (const r of rescored) {
|
|
1056
1308
|
console.log("\n" + formatTable(r) + "\n");
|
|
@@ -1100,14 +1352,15 @@ function formatTable(result) {
|
|
|
1100
1352
|
lines.push(" " + "\u2500".repeat(ruleLen));
|
|
1101
1353
|
for (const name of scorerNames) {
|
|
1102
1354
|
const s = result.summary.scorers[name];
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1355
|
+
const { scored, failed } = scorerSampleCounts(s, name, result.items);
|
|
1356
|
+
const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
|
|
1357
|
+
if (scored === 0) {
|
|
1105
1358
|
lines.push(
|
|
1106
|
-
` ${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}`
|
|
1107
1360
|
);
|
|
1108
1361
|
} else {
|
|
1109
1362
|
lines.push(
|
|
1110
|
-
` ${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}`
|
|
1111
1364
|
);
|
|
1112
1365
|
}
|
|
1113
1366
|
}
|
|
@@ -1155,8 +1408,10 @@ function formatMultiRunTable(summary) {
|
|
|
1155
1408
|
for (const name of scorerNames) {
|
|
1156
1409
|
const s = summary.scorers[name];
|
|
1157
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)` : "";
|
|
1158
1413
|
lines.push(
|
|
1159
|
-
` ${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}`
|
|
1160
1415
|
);
|
|
1161
1416
|
}
|
|
1162
1417
|
if (summary.timing) {
|
|
@@ -1170,6 +1425,26 @@ function formatMultiRunTable(summary) {
|
|
|
1170
1425
|
lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
|
|
1171
1426
|
return lines.join("\n");
|
|
1172
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
|
+
}
|
|
1173
1448
|
async function resolveRuntimeFromConfig(configPath) {
|
|
1174
1449
|
try {
|
|
1175
1450
|
const mod = await (0, import_axl.importModule)(configPath, import_meta.url);
|
|
@@ -1230,41 +1505,17 @@ async function getRuntime(configArg, conditions) {
|
|
|
1230
1505
|
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
1231
1506
|
return new AxlRuntime();
|
|
1232
1507
|
}
|
|
1233
|
-
function parseEvalArgs(args) {
|
|
1234
|
-
let outputPath;
|
|
1235
|
-
let configArg;
|
|
1236
|
-
let conditions = [];
|
|
1237
|
-
let runs = 1;
|
|
1238
|
-
let captureTraces = false;
|
|
1239
|
-
const paths = [];
|
|
1240
|
-
for (let i = 0; i < args.length; i++) {
|
|
1241
|
-
const arg = args[i];
|
|
1242
|
-
if (arg === "--output" || arg === "--config" || arg === "--conditions" || arg === "--runs") {
|
|
1243
|
-
if (i + 1 >= args.length) {
|
|
1244
|
-
console.error(`Error: ${arg} requires a value`);
|
|
1245
|
-
process.exit(1);
|
|
1246
|
-
}
|
|
1247
|
-
const value = args[++i];
|
|
1248
|
-
if (arg === "--output") outputPath = value;
|
|
1249
|
-
else if (arg === "--config") configArg = value;
|
|
1250
|
-
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
1251
|
-
else
|
|
1252
|
-
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
1253
|
-
} else if (arg === "--capture-traces") {
|
|
1254
|
-
captureTraces = true;
|
|
1255
|
-
} else if (arg.startsWith("--")) {
|
|
1256
|
-
if (!KNOWN_FLAGS.has(arg)) {
|
|
1257
|
-
console.error(`Unknown flag: ${arg}`);
|
|
1258
|
-
process.exit(1);
|
|
1259
|
-
}
|
|
1260
|
-
} else {
|
|
1261
|
-
paths.push(arg);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
return { outputPath, configArg, conditions, runs, captureTraces, paths };
|
|
1265
|
-
}
|
|
1266
1508
|
async function runEvalCommand(args, signal) {
|
|
1267
|
-
const {
|
|
1509
|
+
const {
|
|
1510
|
+
outputPath,
|
|
1511
|
+
configArg,
|
|
1512
|
+
conditions,
|
|
1513
|
+
runs,
|
|
1514
|
+
captureTraces,
|
|
1515
|
+
concurrency,
|
|
1516
|
+
scorerNames,
|
|
1517
|
+
paths
|
|
1518
|
+
} = parseEvalArgs(args);
|
|
1268
1519
|
if (paths.length === 0) {
|
|
1269
1520
|
console.error("Error: No eval file path provided");
|
|
1270
1521
|
process.exit(1);
|
|
@@ -1277,6 +1528,10 @@ async function runEvalCommand(args, signal) {
|
|
|
1277
1528
|
console.error("Error: No eval files found");
|
|
1278
1529
|
process.exit(1);
|
|
1279
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
|
+
}
|
|
1280
1535
|
const runtime = await getRuntime(configArg, conditions);
|
|
1281
1536
|
const results = [];
|
|
1282
1537
|
let failedFiles = 0;
|
|
@@ -1292,6 +1547,22 @@ async function runEvalCommand(args, signal) {
|
|
|
1292
1547
|
failedFiles++;
|
|
1293
1548
|
continue;
|
|
1294
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
|
+
}
|
|
1295
1566
|
const exported = (0, import_axl.pickExport)(mod, "executeWorkflow");
|
|
1296
1567
|
let customExecute;
|
|
1297
1568
|
if (typeof exported === "function") {
|
|
@@ -1348,6 +1619,13 @@ async function runEvalCommand(args, signal) {
|
|
|
1348
1619
|
continue;
|
|
1349
1620
|
}
|
|
1350
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
|
+
};
|
|
1351
1629
|
if (runs > 1) {
|
|
1352
1630
|
const { randomUUID: randomUUID4 } = await import("crypto");
|
|
1353
1631
|
const runGroupId = randomUUID4();
|
|
@@ -1360,6 +1638,7 @@ async function runEvalCommand(args, signal) {
|
|
|
1360
1638
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
1361
1639
|
result.metadata.runGroupId = runGroupId;
|
|
1362
1640
|
result.metadata.runIndex = r;
|
|
1641
|
+
stampFiltered(result);
|
|
1363
1642
|
runResults.push(result);
|
|
1364
1643
|
} catch (err) {
|
|
1365
1644
|
runFailure = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1391,10 +1670,21 @@ async function runEvalCommand(args, signal) {
|
|
|
1391
1670
|
const summary = aggregateRuns(runResults);
|
|
1392
1671
|
console.log("\n" + formatMultiRunTable(summary) + "\n");
|
|
1393
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++;
|
|
1394
1680
|
} else {
|
|
1395
1681
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
1682
|
+
stampFiltered(result);
|
|
1396
1683
|
results.push(result);
|
|
1397
1684
|
console.log("\n" + formatTable(result) + "\n");
|
|
1685
|
+
const wipeout = reportTotalWipeout(result, filePath);
|
|
1686
|
+
const degraded = reportDegraded(result, filePath);
|
|
1687
|
+
if (wipeout || degraded) failedFiles++;
|
|
1398
1688
|
}
|
|
1399
1689
|
} catch (err) {
|
|
1400
1690
|
console.error(
|