@axlsdk/eval 0.17.9 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -19
- package/dist/{chunk-NETD4AUB.js → chunk-66EYSYQS.js} +251 -164
- package/dist/chunk-66EYSYQS.js.map +1 -0
- package/dist/cli.cjs +541 -208
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +305 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +322 -176
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +254 -2
- package/dist/index.d.ts +254 -2
- package/dist/index.js +80 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NETD4AUB.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,8 @@ __export(index_exports, {
|
|
|
34
34
|
dataset: () => dataset,
|
|
35
35
|
defineEval: () => defineEval,
|
|
36
36
|
evalCompare: () => evalCompare,
|
|
37
|
+
evaluateScorerErrorRateGate: () => evaluateScorerErrorRateGate,
|
|
38
|
+
evaluateScorerTolerance: () => evaluateScorerTolerance,
|
|
37
39
|
llmScorer: () => llmScorer,
|
|
38
40
|
normalizeScorerResult: () => normalizeScorerResult,
|
|
39
41
|
pairedBootstrapCI: () => pairedBootstrapCI,
|
|
@@ -46,11 +48,71 @@ module.exports = __toCommonJS(index_exports);
|
|
|
46
48
|
// src/dataset.ts
|
|
47
49
|
var import_promises = require("fs/promises");
|
|
48
50
|
var path = __toESM(require("path"), 1);
|
|
51
|
+
function isPlainObject(value) {
|
|
52
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
53
|
+
const proto = Object.getPrototypeOf(value);
|
|
54
|
+
return proto === Object.prototype || proto === null;
|
|
55
|
+
}
|
|
56
|
+
function isObjectSchema(schema) {
|
|
57
|
+
const shape = schema.shape;
|
|
58
|
+
return typeof shape === "object" && shape !== null;
|
|
59
|
+
}
|
|
60
|
+
function collectDroppedKeys(raw, parsed, prefix = "") {
|
|
61
|
+
if (Array.isArray(raw) && Array.isArray(parsed)) {
|
|
62
|
+
const dropped = [];
|
|
63
|
+
const n = Math.min(raw.length, parsed.length);
|
|
64
|
+
for (let i = 0; i < n; i++) {
|
|
65
|
+
dropped.push(...collectDroppedKeys(raw[i], parsed[i], `${prefix}[${i}]`));
|
|
66
|
+
}
|
|
67
|
+
return dropped;
|
|
68
|
+
}
|
|
69
|
+
if (isPlainObject(raw) && isPlainObject(parsed)) {
|
|
70
|
+
const dropped = [];
|
|
71
|
+
for (const key of Object.keys(raw)) {
|
|
72
|
+
const path2 = prefix ? `${prefix}.${key}` : key;
|
|
73
|
+
if (!(key in parsed)) {
|
|
74
|
+
dropped.push(path2);
|
|
75
|
+
} else {
|
|
76
|
+
dropped.push(...collectDroppedKeys(raw[key], parsed[key], path2));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return dropped;
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
49
83
|
function dataset(config) {
|
|
84
|
+
const policy = config.onExtraAnnotationKeys ?? "warn";
|
|
85
|
+
function parseItem(item, droppedAcc) {
|
|
86
|
+
const input = config.schema.parse(item.input);
|
|
87
|
+
if (item.annotations == null || !config.annotations) {
|
|
88
|
+
return { input, annotations: item.annotations };
|
|
89
|
+
}
|
|
90
|
+
const annotations = config.annotations.parse(item.annotations);
|
|
91
|
+
if (policy !== "ignore" && isObjectSchema(config.annotations)) {
|
|
92
|
+
for (const p of collectDroppedKeys(item.annotations, annotations)) droppedAcc.add(p);
|
|
93
|
+
}
|
|
94
|
+
return { input, annotations };
|
|
95
|
+
}
|
|
96
|
+
function reportDropped(dropped) {
|
|
97
|
+
if (dropped.size === 0) return;
|
|
98
|
+
const keys = [...dropped].join(", ");
|
|
99
|
+
if (policy === "error") {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Dataset "${config.name}": annotation key(s) dropped by schema: ${keys}. These keys are not declared in the annotations schema, so they were stripped and will never reach scorers. Add them to the schema, or set onExtraAnnotationKeys: 'ignore' to allow stripping.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
console.warn(
|
|
105
|
+
`[axl-eval] Dataset "${config.name}": annotation key(s) dropped by schema: ${keys}. These keys are not declared in the annotations schema, so they were stripped and will not reach scorers (a common cause of no-op scorers). Add them to the schema, set onExtraAnnotationKeys: 'error' to fail, or 'ignore' to silence this warning.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
let lastDroppedAnnotationKeys = [];
|
|
50
109
|
return {
|
|
51
110
|
name: config.name,
|
|
52
111
|
schema: config.schema,
|
|
53
112
|
annotationsSchema: config.annotations,
|
|
113
|
+
get droppedAnnotationKeys() {
|
|
114
|
+
return [...lastDroppedAnnotationKeys];
|
|
115
|
+
},
|
|
54
116
|
async getItems() {
|
|
55
117
|
if (config.items && config.file) {
|
|
56
118
|
throw new Error('Dataset config error: "items" and "file" are mutually exclusive');
|
|
@@ -58,19 +120,19 @@ function dataset(config) {
|
|
|
58
120
|
if (!config.items && !config.file) {
|
|
59
121
|
throw new Error('Dataset config error: either "items" or "file" must be provided');
|
|
60
122
|
}
|
|
123
|
+
let rawItems;
|
|
61
124
|
if (config.items) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
125
|
+
rawItems = config.items;
|
|
126
|
+
} else {
|
|
127
|
+
const resolvedPath = path.resolve(config.basePath ?? process.cwd(), config.file);
|
|
128
|
+
const content = await (0, import_promises.readFile)(resolvedPath, "utf-8");
|
|
129
|
+
rawItems = JSON.parse(content);
|
|
66
130
|
}
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
annotations: item.annotations && config.annotations ? config.annotations.parse(item.annotations) : item.annotations
|
|
73
|
-
}));
|
|
131
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
132
|
+
const parsed = rawItems.map((item) => parseItem(item, dropped));
|
|
133
|
+
lastDroppedAnnotationKeys = [...dropped];
|
|
134
|
+
reportDropped(dropped);
|
|
135
|
+
return parsed;
|
|
74
136
|
}
|
|
75
137
|
};
|
|
76
138
|
}
|
|
@@ -79,12 +141,22 @@ function dataset(config) {
|
|
|
79
141
|
function normalizeScorerResult(result) {
|
|
80
142
|
return typeof result === "number" ? { score: result } : result;
|
|
81
143
|
}
|
|
144
|
+
function attachScorerErrorCost(err, cost) {
|
|
145
|
+
if (err && typeof err === "object" && typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
146
|
+
err.cost = cost;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function extractScorerErrorCost(err) {
|
|
150
|
+
const v = err && typeof err === "object" ? err.cost : void 0;
|
|
151
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
|
|
152
|
+
}
|
|
82
153
|
function scorer(config) {
|
|
83
154
|
return {
|
|
84
155
|
name: config.name,
|
|
85
156
|
description: config.description,
|
|
86
157
|
isLlm: false,
|
|
87
|
-
score: config.score
|
|
158
|
+
score: config.score,
|
|
159
|
+
applies: config.applies
|
|
88
160
|
};
|
|
89
161
|
}
|
|
90
162
|
|
|
@@ -98,6 +170,7 @@ function llmScorer(config) {
|
|
|
98
170
|
name: config.name,
|
|
99
171
|
description: config.description,
|
|
100
172
|
isLlm: true,
|
|
173
|
+
applies: config.applies,
|
|
101
174
|
async score(output, input, annotations, context) {
|
|
102
175
|
if (!context?.resolveProvider) {
|
|
103
176
|
throw new Error(
|
|
@@ -144,16 +217,14 @@ ${JSON.stringify(annotations, null, 2)}` : "",
|
|
|
144
217
|
const parsed = JSON.parse((0, import_axl.extractJson)(response.content));
|
|
145
218
|
validated = schema.parse(parsed);
|
|
146
219
|
} catch (err) {
|
|
147
|
-
|
|
148
|
-
err.cost = responseCost;
|
|
149
|
-
}
|
|
220
|
+
attachScorerErrorCost(err, responseCost);
|
|
150
221
|
if (err && typeof err === "object" && "issues" in err && Array.isArray(err.issues)) {
|
|
151
222
|
const issues = err.issues;
|
|
152
223
|
const messages = issues.map((i) => `${i.path.length ? `${i.path.join(".")}: ` : ""}${i.message}`).join("; ");
|
|
153
224
|
const error = new Error(
|
|
154
225
|
`LLM scorer "${config.name}" returned an invalid response: ${messages}`
|
|
155
226
|
);
|
|
156
|
-
error
|
|
227
|
+
attachScorerErrorCost(error, responseCost);
|
|
157
228
|
throw error;
|
|
158
229
|
}
|
|
159
230
|
throw err;
|
|
@@ -187,6 +258,99 @@ function computeStats(scores) {
|
|
|
187
258
|
function round(n) {
|
|
188
259
|
return Math.round(n * 1e3) / 1e3;
|
|
189
260
|
}
|
|
261
|
+
function scorerCounts(items, name) {
|
|
262
|
+
let scored = 0;
|
|
263
|
+
let failed = 0;
|
|
264
|
+
let skipped = 0;
|
|
265
|
+
for (const i of items) {
|
|
266
|
+
if (i.error) continue;
|
|
267
|
+
if (i.scores[name] != null) scored++;
|
|
268
|
+
else if (i.scoreDetails?.[name]?.skipped === true) skipped++;
|
|
269
|
+
else if (i.scoreDetails?.[name]?.duration != null) failed++;
|
|
270
|
+
}
|
|
271
|
+
return { scored, failed, skipped };
|
|
272
|
+
}
|
|
273
|
+
function evaluateScorerTolerance(scored, failed, type, limit) {
|
|
274
|
+
const attempted = scored + failed;
|
|
275
|
+
const rate = attempted === 0 ? 0 : failed / attempted;
|
|
276
|
+
const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
|
|
277
|
+
return { attempted, rate, exceeds, zeroSample: attempted === 0 };
|
|
278
|
+
}
|
|
279
|
+
async function mapWithConcurrency(items, concurrency, task) {
|
|
280
|
+
const results = new Array(items.length);
|
|
281
|
+
const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
|
|
282
|
+
const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
|
|
283
|
+
let next = 0;
|
|
284
|
+
async function runNext() {
|
|
285
|
+
while (next < items.length) {
|
|
286
|
+
const i = next++;
|
|
287
|
+
results[i] = await task(items[i], i);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
await Promise.all(Array.from({ length: workerCount }, () => runNext()));
|
|
291
|
+
return results;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/score-item.ts
|
|
295
|
+
async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal) {
|
|
296
|
+
const scoreDetails = item.scoreDetails ??= {};
|
|
297
|
+
for (const s of scorers) {
|
|
298
|
+
item.scores[s.name] = null;
|
|
299
|
+
scoreDetails[s.name] = { score: null };
|
|
300
|
+
}
|
|
301
|
+
let itemScorerCost = 0;
|
|
302
|
+
const scorerErrorsByName = {};
|
|
303
|
+
await mapWithConcurrency(scorers, scorerConcurrency, async (scorer2) => {
|
|
304
|
+
if (signal?.aborted) return;
|
|
305
|
+
const scorerStart = Date.now();
|
|
306
|
+
try {
|
|
307
|
+
if (scorer2.applies && !scorer2.applies(item.output, item.input, item.annotations)) {
|
|
308
|
+
item.scores[scorer2.name] = null;
|
|
309
|
+
scoreDetails[scorer2.name] = { score: null, skipped: true };
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const raw = await scorer2.score(item.output, item.input, item.annotations, scorerContext);
|
|
313
|
+
const scorerResult = normalizeScorerResult(raw);
|
|
314
|
+
if (scorerResult.cost != null) {
|
|
315
|
+
itemScorerCost += scorerResult.cost;
|
|
316
|
+
}
|
|
317
|
+
const scorerDuration = Date.now() - scorerStart;
|
|
318
|
+
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
319
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`;
|
|
320
|
+
item.scores[scorer2.name] = null;
|
|
321
|
+
scoreDetails[scorer2.name] = {
|
|
322
|
+
score: null,
|
|
323
|
+
metadata: scorerResult.metadata,
|
|
324
|
+
duration: scorerDuration,
|
|
325
|
+
cost: scorerResult.cost
|
|
326
|
+
};
|
|
327
|
+
} else {
|
|
328
|
+
item.scores[scorer2.name] = round(scorerResult.score);
|
|
329
|
+
scoreDetails[scorer2.name] = {
|
|
330
|
+
score: round(scorerResult.score),
|
|
331
|
+
metadata: scorerResult.metadata,
|
|
332
|
+
duration: scorerDuration,
|
|
333
|
+
cost: scorerResult.cost
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (err?.name === "AbortError") return;
|
|
338
|
+
const errCost = extractScorerErrorCost(err);
|
|
339
|
+
if (errCost != null) itemScorerCost += errCost;
|
|
340
|
+
scorerErrorsByName[scorer2.name] = `Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
|
|
341
|
+
item.scores[scorer2.name] = null;
|
|
342
|
+
scoreDetails[scorer2.name] = {
|
|
343
|
+
score: null,
|
|
344
|
+
duration: Date.now() - scorerStart,
|
|
345
|
+
cost: errCost
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
|
|
350
|
+
if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
|
|
351
|
+
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
352
|
+
return itemScorerCost;
|
|
353
|
+
}
|
|
190
354
|
|
|
191
355
|
// src/runner.ts
|
|
192
356
|
var import_node_crypto = require("crypto");
|
|
@@ -228,7 +392,9 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
228
392
|
const startTime = Date.now();
|
|
229
393
|
const id = (0, import_node_crypto.randomUUID)();
|
|
230
394
|
const items = await config.dataset.getItems();
|
|
395
|
+
const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
|
|
231
396
|
const concurrency = config.concurrency ?? 5;
|
|
397
|
+
const scorerConcurrency = config.scorerConcurrency ?? 5;
|
|
232
398
|
const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
|
|
233
399
|
const scorerContext = {
|
|
234
400
|
resolveProvider: (uri) => {
|
|
@@ -317,89 +483,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
317
483
|
if (budgetLimit != null && totalCost > budgetLimit) {
|
|
318
484
|
budgetExceeded = true;
|
|
319
485
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
item.input,
|
|
329
|
-
item.annotations,
|
|
330
|
-
scorerContext
|
|
331
|
-
);
|
|
332
|
-
const scorerResult = normalizeScorerResult(raw);
|
|
333
|
-
if (scorerResult.cost != null) {
|
|
334
|
-
itemScorerCost += scorerResult.cost;
|
|
335
|
-
totalCost += scorerResult.cost;
|
|
336
|
-
}
|
|
337
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
338
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
339
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
340
|
-
evalItem.scorerErrors.push(
|
|
341
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
342
|
-
);
|
|
343
|
-
evalItem.scores[scorer2.name] = null;
|
|
344
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
345
|
-
score: null,
|
|
346
|
-
metadata: scorerResult.metadata,
|
|
347
|
-
duration: scorerDuration,
|
|
348
|
-
cost: scorerResult.cost
|
|
349
|
-
};
|
|
350
|
-
} else {
|
|
351
|
-
evalItem.scores[scorer2.name] = round(scorerResult.score);
|
|
352
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
353
|
-
score: round(scorerResult.score),
|
|
354
|
-
metadata: scorerResult.metadata,
|
|
355
|
-
duration: scorerDuration,
|
|
356
|
-
cost: scorerResult.cost
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
} catch (err) {
|
|
360
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
361
|
-
if (errCost != null) {
|
|
362
|
-
itemScorerCost += errCost;
|
|
363
|
-
totalCost += errCost;
|
|
364
|
-
}
|
|
365
|
-
if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
|
|
366
|
-
evalItem.scorerErrors.push(
|
|
367
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
368
|
-
);
|
|
369
|
-
evalItem.scores[scorer2.name] = null;
|
|
370
|
-
evalItem.scoreDetails[scorer2.name] = {
|
|
371
|
-
score: null,
|
|
372
|
-
duration: Date.now() - scorerStart,
|
|
373
|
-
cost: errCost
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
evalItem.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
486
|
+
const itemScorerCost = await scoreItem(
|
|
487
|
+
evalItem,
|
|
488
|
+
config.scorers,
|
|
489
|
+
scorerConcurrency,
|
|
490
|
+
scorerContext,
|
|
491
|
+
options?.signal
|
|
492
|
+
);
|
|
493
|
+
totalCost += itemScorerCost;
|
|
378
494
|
evalItems[itemIndex] = evalItem;
|
|
379
495
|
options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
|
|
380
496
|
}
|
|
381
|
-
|
|
382
|
-
async function runNext() {
|
|
383
|
-
while (index < items.length) {
|
|
384
|
-
const currentIndex = index++;
|
|
385
|
-
await processItem(items[currentIndex], currentIndex);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
|
|
389
|
-
await Promise.all(workers);
|
|
497
|
+
await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
|
|
390
498
|
const failures = evalItems.filter((i) => i.error).length;
|
|
391
499
|
const scorerNames = config.scorers.map((s) => s.name);
|
|
392
500
|
const scorerStats = {};
|
|
393
501
|
for (const name of scorerNames) {
|
|
394
502
|
const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
395
|
-
|
|
503
|
+
const { scored, failed, skipped } = scorerCounts(evalItems, name);
|
|
504
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
|
|
396
505
|
}
|
|
397
|
-
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
398
|
-
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
399
506
|
const scorerTypes = {};
|
|
400
507
|
for (const s of config.scorers) {
|
|
401
508
|
scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
|
|
402
509
|
}
|
|
510
|
+
let degraded;
|
|
511
|
+
const rawLimit = config.failOnScorerErrorRate;
|
|
512
|
+
if (rawLimit != null) {
|
|
513
|
+
if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
|
|
514
|
+
console.warn(
|
|
515
|
+
`[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
|
|
516
|
+
);
|
|
517
|
+
} else {
|
|
518
|
+
for (const name of scorerNames) {
|
|
519
|
+
const stats = scorerStats[name];
|
|
520
|
+
const scored = stats.scored ?? 0;
|
|
521
|
+
const failed = stats.failed ?? 0;
|
|
522
|
+
const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
|
|
523
|
+
const limit = type === "deterministic" ? 0 : rawLimit;
|
|
524
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
525
|
+
if (verdict.exceeds) {
|
|
526
|
+
(degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
|
|
532
|
+
const timing = durations.length > 0 ? computeStats(durations) : void 0;
|
|
403
533
|
const totalModelCalls = /* @__PURE__ */ new Map();
|
|
404
534
|
for (const item of evalItems) {
|
|
405
535
|
const itemCounts = item.metadata?.modelCallCounts;
|
|
@@ -458,13 +588,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
|
|
|
458
588
|
...config.metadata,
|
|
459
589
|
scorerTypes,
|
|
460
590
|
...modelsMeta,
|
|
461
|
-
...workflowsMeta
|
|
591
|
+
...workflowsMeta,
|
|
592
|
+
...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
|
|
462
593
|
},
|
|
463
594
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
464
595
|
totalCost,
|
|
465
596
|
duration: Date.now() - startTime,
|
|
466
597
|
items: evalItems,
|
|
467
|
-
summary: {
|
|
598
|
+
summary: {
|
|
599
|
+
count: items.length,
|
|
600
|
+
failures,
|
|
601
|
+
scorers: scorerStats,
|
|
602
|
+
timing,
|
|
603
|
+
...degraded ? { degraded } : {}
|
|
604
|
+
}
|
|
468
605
|
};
|
|
469
606
|
}
|
|
470
607
|
|
|
@@ -535,9 +672,6 @@ function xorshift32(seed) {
|
|
|
535
672
|
}
|
|
536
673
|
|
|
537
674
|
// src/compare.ts
|
|
538
|
-
function round3(n) {
|
|
539
|
-
return Math.round(n * 1e3) / 1e3;
|
|
540
|
-
}
|
|
541
675
|
var DEFAULT_LLM_THRESHOLD = 0.05;
|
|
542
676
|
var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
|
|
543
677
|
var LEGACY_THRESHOLD = 0.1;
|
|
@@ -606,24 +740,48 @@ function evalCompare(baseline, candidate, options) {
|
|
|
606
740
|
const bMeanRaw = baselineMeans[name];
|
|
607
741
|
const cMeanRaw = candidateMeans[name];
|
|
608
742
|
const deltaRaw = cMeanRaw - bMeanRaw;
|
|
609
|
-
const bMean =
|
|
610
|
-
const cMean =
|
|
611
|
-
const delta =
|
|
612
|
-
const deltaPercent = bMeanRaw > 0 ?
|
|
743
|
+
const bMean = round(bMeanRaw);
|
|
744
|
+
const cMean = round(cMeanRaw);
|
|
745
|
+
const delta = round(deltaRaw);
|
|
746
|
+
const deltaPercent = bMeanRaw > 0 ? round(deltaRaw / bMeanRaw * 100) : 0;
|
|
613
747
|
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
748
|
+
let baselineScored = 0;
|
|
749
|
+
let baselineFailed = 0;
|
|
750
|
+
let baselineSkipped = 0;
|
|
751
|
+
for (const run of baselineRuns) {
|
|
752
|
+
const c = scorerCounts(run.items, name);
|
|
753
|
+
baselineScored += c.scored;
|
|
754
|
+
baselineFailed += c.failed;
|
|
755
|
+
baselineSkipped += c.skipped;
|
|
756
|
+
}
|
|
757
|
+
let candidateScored = 0;
|
|
758
|
+
let candidateFailed = 0;
|
|
759
|
+
let candidateSkipped = 0;
|
|
760
|
+
for (const run of candidateRuns) {
|
|
761
|
+
const c = scorerCounts(run.items, name);
|
|
762
|
+
candidateScored += c.scored;
|
|
763
|
+
candidateFailed += c.failed;
|
|
764
|
+
candidateSkipped += c.skipped;
|
|
765
|
+
}
|
|
614
766
|
const entry = {
|
|
615
767
|
baselineMean: bMean,
|
|
616
768
|
candidateMean: cMean,
|
|
617
769
|
delta,
|
|
618
|
-
deltaPercent
|
|
770
|
+
deltaPercent,
|
|
771
|
+
baselineScored,
|
|
772
|
+
baselineFailed,
|
|
773
|
+
candidateScored,
|
|
774
|
+
candidateFailed,
|
|
775
|
+
baselineSkipped,
|
|
776
|
+
candidateSkipped
|
|
619
777
|
};
|
|
620
778
|
const diffs = pairedDiffs[name];
|
|
779
|
+
entry.n = diffs.length;
|
|
621
780
|
if (diffs.length >= 2) {
|
|
622
781
|
const ci = pairedBootstrapCI(diffs);
|
|
623
782
|
entry.ci = { lower: ci.lower, upper: ci.upper };
|
|
624
783
|
entry.pRegression = ci.pRegression;
|
|
625
784
|
entry.pImprovement = ci.pImprovement;
|
|
626
|
-
entry.n = diffs.length;
|
|
627
785
|
const ciExcludesZero = ci.lower > 0 || ci.upper < 0;
|
|
628
786
|
entry.significant = ciExcludesZero && Math.abs(ci.mean) >= threshold;
|
|
629
787
|
}
|
|
@@ -645,15 +803,15 @@ function evalCompare(baseline, candidate, options) {
|
|
|
645
803
|
if (bScores.length === 0 || cScores.length === 0) continue;
|
|
646
804
|
const bAvg = bScores.reduce((a, b) => a + b, 0) / bScores.length;
|
|
647
805
|
const cAvg = cScores.reduce((a, b) => a + b, 0) / cScores.length;
|
|
648
|
-
const delta =
|
|
806
|
+
const delta = round(cAvg - bAvg);
|
|
649
807
|
const threshold = resolveThreshold(name, options, baselineRef.metadata);
|
|
650
808
|
if (delta < -threshold)
|
|
651
809
|
regressions.push({
|
|
652
810
|
itemIndex: i,
|
|
653
811
|
input: baselineRef.items[i]?.input,
|
|
654
812
|
scorer: name,
|
|
655
|
-
baselineScore:
|
|
656
|
-
candidateScore:
|
|
813
|
+
baselineScore: round(bAvg),
|
|
814
|
+
candidateScore: round(cAvg),
|
|
657
815
|
delta
|
|
658
816
|
});
|
|
659
817
|
else if (delta > threshold)
|
|
@@ -661,8 +819,8 @@ function evalCompare(baseline, candidate, options) {
|
|
|
661
819
|
itemIndex: i,
|
|
662
820
|
input: baselineRef.items[i]?.input,
|
|
663
821
|
scorer: name,
|
|
664
|
-
baselineScore:
|
|
665
|
-
candidateScore:
|
|
822
|
+
baselineScore: round(bAvg),
|
|
823
|
+
candidateScore: round(cAvg),
|
|
666
824
|
delta
|
|
667
825
|
});
|
|
668
826
|
}
|
|
@@ -673,20 +831,20 @@ function evalCompare(baseline, candidate, options) {
|
|
|
673
831
|
if (baselineTimings.length > 0 && candidateTimings.length > 0) {
|
|
674
832
|
const bMean = baselineTimings.reduce((a, b) => a + b, 0) / baselineTimings.length;
|
|
675
833
|
const cMean = candidateTimings.reduce((a, b) => a + b, 0) / candidateTimings.length;
|
|
676
|
-
const delta =
|
|
677
|
-
const deltaPercent = bMean > 0 ?
|
|
678
|
-
timing = { baselineMean:
|
|
834
|
+
const delta = round(cMean - bMean);
|
|
835
|
+
const deltaPercent = bMean > 0 ? round((cMean - bMean) / bMean * 100) : 0;
|
|
836
|
+
timing = { baselineMean: round(bMean), candidateMean: round(cMean), delta, deltaPercent };
|
|
679
837
|
}
|
|
680
838
|
let cost;
|
|
681
839
|
const baselineAvgCost = baselineRuns.reduce((sum, r) => sum + r.totalCost, 0) / baselineRuns.length;
|
|
682
840
|
const candidateAvgCost = candidateRuns.reduce((sum, r) => sum + r.totalCost, 0) / candidateRuns.length;
|
|
683
841
|
if (baselineAvgCost > 0 || candidateAvgCost > 0) {
|
|
684
842
|
const deltaRaw = candidateAvgCost - baselineAvgCost;
|
|
685
|
-
const delta =
|
|
686
|
-
const deltaPercent = baselineAvgCost > 0 ?
|
|
843
|
+
const delta = round(deltaRaw);
|
|
844
|
+
const deltaPercent = baselineAvgCost > 0 ? round(deltaRaw / baselineAvgCost * 100) : 0;
|
|
687
845
|
cost = {
|
|
688
|
-
baselineTotal:
|
|
689
|
-
candidateTotal:
|
|
846
|
+
baselineTotal: round(baselineAvgCost),
|
|
847
|
+
candidateTotal: round(candidateAvgCost),
|
|
690
848
|
delta,
|
|
691
849
|
deltaPercent
|
|
692
850
|
};
|
|
@@ -751,12 +909,36 @@ function detectPartial(runs) {
|
|
|
751
909
|
if (runs.length >= attempted) return void 0;
|
|
752
910
|
return { completed: runs.length, attempted };
|
|
753
911
|
}
|
|
912
|
+
function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
|
|
913
|
+
const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
|
|
914
|
+
for (const name of Object.keys(comparison.scorers)) {
|
|
915
|
+
const s = comparison.scorers[name];
|
|
916
|
+
const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
|
|
917
|
+
const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
|
|
918
|
+
for (const side of ["baseline", "candidate"]) {
|
|
919
|
+
const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
|
|
920
|
+
const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
|
|
921
|
+
const skipped = (side === "baseline" ? s.baselineSkipped : s.candidateSkipped) ?? 0;
|
|
922
|
+
const verdict = evaluateScorerTolerance(scored, failed, type, limit);
|
|
923
|
+
if (verdict.zeroSample) {
|
|
924
|
+
const cause = skipped > 0 ? `every one of its items was N/A (the \`applies\` predicate skipped all ${skipped})` : `its items errored or produced no score`;
|
|
925
|
+
return `scorer "${name}" has no scored items on ${side} (${cause}) \u2014 cannot certify a mean over an empty sample.`;
|
|
926
|
+
}
|
|
927
|
+
if (verdict.exceeds) {
|
|
928
|
+
const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
|
|
929
|
+
return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
754
935
|
|
|
755
936
|
// src/rescore.ts
|
|
756
937
|
var import_node_crypto2 = require("crypto");
|
|
757
938
|
async function rescore(result, scorers, runtime, options) {
|
|
758
939
|
const startTime = Date.now();
|
|
759
940
|
const concurrency = options?.concurrency ?? 5;
|
|
941
|
+
const scorerConcurrency = options?.scorerConcurrency ?? 5;
|
|
760
942
|
const scorerContext = {
|
|
761
943
|
resolveProvider: (uri) => {
|
|
762
944
|
if (typeof runtime.resolveProvider !== "function") {
|
|
@@ -800,77 +982,24 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
800
982
|
scores: {},
|
|
801
983
|
scoreDetails: {}
|
|
802
984
|
};
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
totalCost += scorerResult.cost;
|
|
812
|
-
}
|
|
813
|
-
const scorerDuration = Date.now() - scorerStart;
|
|
814
|
-
if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
|
|
815
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
816
|
-
item.scorerErrors.push(
|
|
817
|
-
`Scorer "${scorer2.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
|
|
818
|
-
);
|
|
819
|
-
item.scores[scorer2.name] = null;
|
|
820
|
-
item.scoreDetails[scorer2.name] = {
|
|
821
|
-
score: null,
|
|
822
|
-
metadata: scorerResult.metadata,
|
|
823
|
-
duration: scorerDuration,
|
|
824
|
-
cost: scorerResult.cost
|
|
825
|
-
};
|
|
826
|
-
} else {
|
|
827
|
-
item.scores[scorer2.name] = round(scorerResult.score);
|
|
828
|
-
item.scoreDetails[scorer2.name] = {
|
|
829
|
-
score: round(scorerResult.score),
|
|
830
|
-
metadata: scorerResult.metadata,
|
|
831
|
-
duration: scorerDuration,
|
|
832
|
-
cost: scorerResult.cost
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
} catch (err) {
|
|
836
|
-
const errCost = typeof err?.cost === "number" ? err.cost : void 0;
|
|
837
|
-
if (errCost != null) {
|
|
838
|
-
itemScorerCost += errCost;
|
|
839
|
-
totalCost += errCost;
|
|
840
|
-
}
|
|
841
|
-
if (!item.scorerErrors) item.scorerErrors = [];
|
|
842
|
-
item.scorerErrors.push(
|
|
843
|
-
`Scorer "${scorer2.name}" threw: ${err instanceof Error ? err.message : String(err)}`
|
|
844
|
-
);
|
|
845
|
-
item.scores[scorer2.name] = null;
|
|
846
|
-
item.scoreDetails[scorer2.name] = {
|
|
847
|
-
score: null,
|
|
848
|
-
duration: Date.now() - scorerStart,
|
|
849
|
-
cost: errCost
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
|
|
985
|
+
const itemScorerCost = await scoreItem(
|
|
986
|
+
item,
|
|
987
|
+
scorers,
|
|
988
|
+
scorerConcurrency,
|
|
989
|
+
scorerContext,
|
|
990
|
+
options?.signal
|
|
991
|
+
);
|
|
992
|
+
totalCost += itemScorerCost;
|
|
854
993
|
rescored[itemIndex] = item;
|
|
855
994
|
}
|
|
856
|
-
|
|
857
|
-
async function runNext() {
|
|
858
|
-
while (index < result.items.length) {
|
|
859
|
-
const currentIndex = index++;
|
|
860
|
-
await rescoreItem(result.items[currentIndex], currentIndex);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
const workers = Array.from(
|
|
864
|
-
{ length: Math.min(concurrency, result.items.length) },
|
|
865
|
-
() => runNext()
|
|
866
|
-
);
|
|
867
|
-
await Promise.all(workers);
|
|
995
|
+
await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
|
|
868
996
|
const failures = rescored.filter((i) => i.error).length;
|
|
869
997
|
const scorerNames = scorers.map((s) => s.name);
|
|
870
998
|
const scorerStats = {};
|
|
871
999
|
for (const name of scorerNames) {
|
|
872
1000
|
const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
|
|
873
|
-
|
|
1001
|
+
const { scored, failed, skipped } = scorerCounts(rescored, name);
|
|
1002
|
+
scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
|
|
874
1003
|
}
|
|
875
1004
|
const scorerTypes = {};
|
|
876
1005
|
for (const s of scorers) {
|
|
@@ -880,7 +1009,11 @@ async function rescore(result, scorers, runtime, options) {
|
|
|
880
1009
|
id: (0, import_node_crypto2.randomUUID)(),
|
|
881
1010
|
dataset: result.dataset,
|
|
882
1011
|
metadata: (() => {
|
|
883
|
-
const
|
|
1012
|
+
const rest = { ...result.metadata };
|
|
1013
|
+
delete rest.runGroupId;
|
|
1014
|
+
delete rest.runIndex;
|
|
1015
|
+
delete rest.scorerFiltered;
|
|
1016
|
+
delete rest.scorersRun;
|
|
884
1017
|
const merged = {
|
|
885
1018
|
...rest,
|
|
886
1019
|
rescored: true,
|
|
@@ -933,15 +1066,26 @@ function aggregateRuns(runs) {
|
|
|
933
1066
|
const scorerNames = Object.keys(runs[0].summary.scorers);
|
|
934
1067
|
const scorers = {};
|
|
935
1068
|
for (const name of scorerNames) {
|
|
936
|
-
const
|
|
1069
|
+
const contributing = runs.filter((r) => {
|
|
1070
|
+
const sc = r.summary.scorers[name]?.scored;
|
|
1071
|
+
return sc == null || sc > 0;
|
|
1072
|
+
});
|
|
1073
|
+
const meanSource = contributing.length > 0 ? contributing : runs;
|
|
1074
|
+
const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
|
|
937
1075
|
const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
|
|
938
1076
|
const minMean = Math.min(...means);
|
|
939
1077
|
const maxMean = Math.max(...means);
|
|
1078
|
+
const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
|
|
1079
|
+
const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
|
|
1080
|
+
const skipped = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.skipped ?? 0), 0);
|
|
940
1081
|
scorers[name] = {
|
|
941
1082
|
mean: round(meanOfMeans),
|
|
942
1083
|
std: round(std(means, meanOfMeans)),
|
|
943
1084
|
min: round(minMean),
|
|
944
|
-
max: round(maxMean)
|
|
1085
|
+
max: round(maxMean),
|
|
1086
|
+
scored,
|
|
1087
|
+
failed,
|
|
1088
|
+
skipped
|
|
945
1089
|
};
|
|
946
1090
|
}
|
|
947
1091
|
let timing;
|
|
@@ -970,6 +1114,8 @@ function aggregateRuns(runs) {
|
|
|
970
1114
|
dataset,
|
|
971
1115
|
defineEval,
|
|
972
1116
|
evalCompare,
|
|
1117
|
+
evaluateScorerErrorRateGate,
|
|
1118
|
+
evaluateScorerTolerance,
|
|
973
1119
|
llmScorer,
|
|
974
1120
|
normalizeScorerResult,
|
|
975
1121
|
pairedBootstrapCI,
|