evals-lab 0.11.0 → 0.13.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/CHANGELOG.md +96 -0
- package/README.md +4 -4
- package/bin/evals-lab.js +9 -1
- package/bin/lab-file.js +97 -0
- package/bin/run.js +1 -1
- package/lab/VERSION +1 -1
- package/lab/demo/CREDITS.md +37 -3
- package/lab/demo/datasets/demo-1.json +2 -2
- package/lab/demo/datasets/demo-2.json +2 -2
- package/lab/demo/datasets/sentiment.json +383 -0
- package/lab/demo/datasets/weather-alerts.json +61 -0
- package/lab/demo/datasets/wordings.json +39 -0
- package/lab/demo/manifest.json +3 -3
- package/lab/demo/pipelines/demo-1.json +4 -4
- package/lab/demo/pipelines/demo-2.json +4 -4
- package/lab/demo/pipelines/sentiment.json +65 -0
- package/lab/demo/pipelines/weather-alerts.json +66 -0
- package/lab/demo/pipelines/wordings.json +104 -0
- package/lab/demo/sources/Demo 1/source.json +3 -0
- package/lab/demo/sources/Demo 2/source.json +3 -0
- package/lab/demo/sources/sentiment/bakery-croissant.txt +1 -0
- package/lab/demo/sources/sentiment/bus-timetable.txt +1 -0
- package/lab/demo/sources/sentiment/cafe-coffee.txt +1 -0
- package/lab/demo/sources/sentiment/concert-night.txt +1 -0
- package/lab/demo/sources/sentiment/film-sequel.txt +1 -0
- package/lab/demo/sources/sentiment/garden-tomatoes.txt +1 -0
- package/lab/demo/sources/sentiment/gym-contract.txt +1 -0
- package/lab/demo/sources/sentiment/headphones-review.txt +1 -0
- package/lab/demo/sources/sentiment/hotel-room.txt +1 -0
- package/lab/demo/sources/sentiment/library-hours.txt +1 -0
- package/lab/demo/sources/sentiment/meeting-minutes.txt +1 -0
- package/lab/demo/sources/sentiment/parcel-lost.txt +1 -0
- package/lab/demo/sources/sentiment/phone-update.txt +1 -0
- package/lab/demo/sources/sentiment/puppy-home.txt +1 -0
- package/lab/demo/sources/sentiment/recipe-note.txt +1 -0
- package/lab/demo/sources/sentiment/road-closure.txt +1 -0
- package/lab/demo/sources/sentiment/source.json +3 -0
- package/lab/demo/sources/sentiment/teacher-thanks.txt +1 -0
- package/lab/demo/sources/sentiment/train-delay.txt +1 -0
- package/lab/demo/sources/sentiment/volunteer-day.txt +1 -0
- package/lab/demo/sources/sentiment/weather-report.txt +1 -0
- package/lab/demo/sources/weather-alerts/https-api.weather.gov-alerts-types.txt +116 -0
- package/lab/demo/sources/weather-alerts/source.json +8 -0
- package/lab/evals-core.mjs +246 -57
- package/lab/flows/wdl.mjs +19 -0
- package/lab/kinds/list.mjs +5 -2
- package/lab/metrics/builtin.mjs +34 -7
- package/lab/run-evals.js +30 -11
- package/lab/server.py +614 -75
- package/lab/web/dist/assets/gallery-DpaqOg9Z.js +5 -0
- package/lab/web/dist/assets/main-BSw-jyYD.css +1 -0
- package/lab/web/dist/assets/main-CrB8wPZn.js +17 -0
- package/lab/web/dist/assets/tokens-CrjDMCbz.js +60 -0
- package/lab/web/dist/assets/tokens-DDOONQon.css +1 -0
- package/lab/web/dist/gallery.html +3 -3
- package/lab/web/dist/index.html +4 -4
- package/package.json +1 -1
- package/lab/web/dist/assets/gallery-DKy8QK3o.js +0 -5
- package/lab/web/dist/assets/main-BGmSu7aS.css +0 -1
- package/lab/web/dist/assets/main-DdGON-yl.js +0 -16
- package/lab/web/dist/assets/tokens-BmytrwxE.js +0 -60
- package/lab/web/dist/assets/tokens-DBN7I0ru.css +0 -1
package/lab/kinds/list.mjs
CHANGED
|
@@ -293,6 +293,8 @@ export const FILTER_SEPARATORS = ["comma", "lines", "semicolon", "none"]
|
|
|
293
293
|
|
|
294
294
|
|
|
295
295
|
|
|
296
|
+
|
|
297
|
+
|
|
296
298
|
|
|
297
299
|
|
|
298
300
|
/** A base filter type (#334's decision: a registry, not an enum). A reader
|
|
@@ -471,19 +473,20 @@ export function applyFilter(filter , reply ) {
|
|
|
471
473
|
export function applyFilterSet(filters , reply , stage ) {
|
|
472
474
|
const instruction = reply.instruction ?? "";
|
|
473
475
|
let body = reply.body, items = reply.items;
|
|
474
|
-
const dropped = [];
|
|
476
|
+
const dropped = [], rewrote = [];
|
|
475
477
|
let emptied ;
|
|
476
478
|
for (const filter of filters) {
|
|
477
479
|
const scope = FILTER_TYPES[filter.type]?.scope;
|
|
478
480
|
if (stage && scope && (scope === "text") !== (stage === "text")) continue;
|
|
479
481
|
const out = applyFilter(filter, { body, items, instruction });
|
|
482
|
+
if (out.body !== body) rewrote.push(filter.id);
|
|
480
483
|
body = out.body;
|
|
481
484
|
if (items.length && !out.items.length) emptied = filter.id;
|
|
482
485
|
items = out.items;
|
|
483
486
|
dropped.push(...out.dropped);
|
|
484
487
|
if (out.reject != null) return { body, items, dropped, reject: out.reject };
|
|
485
488
|
}
|
|
486
|
-
return { body, items, dropped, reject: null, ...(emptied ? { emptied } : {}) };
|
|
489
|
+
return { body, items, dropped, reject: null, ...(emptied ? { emptied } : {}), ...(rewrote.length ? { rewrote } : {}) };
|
|
487
490
|
}
|
|
488
491
|
|
|
489
492
|
// A newline always ends an item, as it does in the real csv/lines parse
|
package/lab/metrics/builtin.mjs
CHANGED
|
@@ -139,9 +139,9 @@ function verdictOf(said )
|
|
|
139
139
|
can hold one. A reply that still cannot be read is asked for once more;
|
|
140
140
|
a second, or a grader that failed, is the grader's error -- said as one,
|
|
141
141
|
so it is not read as the reply failing. [ok] says whether a verdict read
|
|
142
|
-
is one this metric can use. */
|
|
142
|
+
is one this metric can use: true, or why not. */
|
|
143
143
|
async function graded(ctx , prompt , schema ,
|
|
144
|
-
ok
|
|
144
|
+
ok = () => true) {
|
|
145
145
|
if (!ctx.ask) throw new Error("a model-graded metric needs a grader, and this run has none");
|
|
146
146
|
let last = "";
|
|
147
147
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
@@ -149,14 +149,21 @@ async function graded(ctx , prompt , schema
|
|
|
149
149
|
try { said = await ctx.ask(prompt, schema); } catch (e) { throw new Error(`grader error: ${e instanceof Error ? e.message : String(e)}`); }
|
|
150
150
|
try {
|
|
151
151
|
const v = verdictOf(said);
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
const usable = ok(v);
|
|
153
|
+
if (usable === true) return v;
|
|
154
|
+
last = typeof usable === "string" ? usable : `the grader said ${short(JSON.stringify(v))}`;
|
|
154
155
|
} catch (e) { last = e instanceof Error ? e.message : String(e); }
|
|
155
156
|
}
|
|
156
157
|
throw new Error(`grader error: ${last}`);
|
|
157
158
|
}
|
|
159
|
+
/** Whether a pass-or-fail verdict's score is on the scale it was asked for.
|
|
160
|
+
One off it -- a 4 echoing a 1-to-5 rating -- is not clamped to a score the
|
|
161
|
+
grader never gave: it is read as unreadable, asked for again, then the
|
|
162
|
+
grader's error (#514). */
|
|
163
|
+
const onScale = (v ) =>
|
|
164
|
+
typeof v.score !== "number" || (v.score >= 0 && v.score <= 1) || `the grader scored ${v.score}, off the 0 to 1 scale`;
|
|
158
165
|
const gradedPass = (v , threshold ) => {
|
|
159
|
-
const score = typeof v.score === "number" ?
|
|
166
|
+
const score = typeof v.score === "number" ? v.score : v.pass === true ? 1 : 0;
|
|
160
167
|
const pass = typeof v.pass === "boolean" ? v.pass && score >= threshold : score >= threshold;
|
|
161
168
|
return { pass, score, reason: str(v.reason) || (pass ? "the grader passed it" : "the grader failed it") };
|
|
162
169
|
};
|
|
@@ -339,6 +346,7 @@ const metrics = {
|
|
|
339
346
|
// rules discarded is the finding.
|
|
340
347
|
discarded: {
|
|
341
348
|
family: RESULT,
|
|
349
|
+
readsResult: true,
|
|
342
350
|
label: "Discarded",
|
|
343
351
|
description: "Passes only when the job's rules discarded the reply.",
|
|
344
352
|
options: [],
|
|
@@ -348,8 +356,26 @@ const metrics = {
|
|
|
348
356
|
return ok(thrown, thrown ? "discarded" : input.error ? input.error : `kept ${input.terms.length} items`);
|
|
349
357
|
},
|
|
350
358
|
},
|
|
359
|
+
// A case that expects no answer at all: the connection answering an error
|
|
360
|
+
// -- a 400 for a file it can't read -- is the finding. A discard is
|
|
361
|
+
// Discarded's, at whichever stage it was.
|
|
362
|
+
refused: {
|
|
363
|
+
family: RESULT,
|
|
364
|
+
readsResult: true,
|
|
365
|
+
readsRefusal: true,
|
|
366
|
+
label: "Refused",
|
|
367
|
+
description: "Passes only when the target's call was refused: an error from the connection, not a reply.",
|
|
368
|
+
options: [],
|
|
369
|
+
defaults: () => ({}),
|
|
370
|
+
score: (input) => {
|
|
371
|
+
if (!input.error) return ok(false, "replied");
|
|
372
|
+
const refused = !/^(stage \d+: )?discarded: /.test(input.error);
|
|
373
|
+
return ok(refused, refused ? `refused: ${input.error}` : input.error);
|
|
374
|
+
},
|
|
375
|
+
},
|
|
351
376
|
"item-count": {
|
|
352
377
|
family: RESULT,
|
|
378
|
+
readsResult: true,
|
|
353
379
|
label: "Item count",
|
|
354
380
|
description: "Passes when the number of items is within the bounds.",
|
|
355
381
|
options: [{ key: "min", label: "At least", type: "number" }, { key: "max", label: "At most", type: "number" }],
|
|
@@ -385,6 +411,7 @@ const metrics = {
|
|
|
385
411
|
},
|
|
386
412
|
latency: {
|
|
387
413
|
family: RESULT,
|
|
414
|
+
readsResult: true,
|
|
388
415
|
label: "Latency",
|
|
389
416
|
description: "Passes when the reply came back within the time set.",
|
|
390
417
|
options: [{ key: "max", label: "At most (ms)", type: "number" }],
|
|
@@ -472,7 +499,7 @@ const metrics = {
|
|
|
472
499
|
validate: (m, at, bad) => needs(m, ["rubric"], at, bad),
|
|
473
500
|
score: async (input, m, ctx) => gradedPass(await graded(ctx,
|
|
474
501
|
`You are grading an output against a rubric.\n\n${taskOf(input, "")}Rubric:\n${str(m.rubric)}\n\nOutput:\n${input.text}\n\n${REPLY}`,
|
|
475
|
-
VERDICT_SCHEMA), num(m.threshold) ?? 0.5),
|
|
502
|
+
VERDICT_SCHEMA, onScale), num(m.threshold) ?? 0.5),
|
|
476
503
|
},
|
|
477
504
|
factuality: {
|
|
478
505
|
family: GRADED,
|
|
@@ -488,7 +515,7 @@ const metrics = {
|
|
|
488
515
|
score: async (input, m, ctx) => gradedPass(await graded(ctx,
|
|
489
516
|
`You are checking an output for factual consistency with a reference. Differences in wording or detail are `
|
|
490
517
|
+ `fine; a contradiction, or a fact the reference does not support, is not.\n\n${taskOf(input, "")}Reference:\n`
|
|
491
|
-
+ `${str(m.reference).trim() || recorded(input)}\n\nOutput:\n${input.text}\n\n${REPLY}`, VERDICT_SCHEMA), num(m.threshold) ?? 0.5),
|
|
518
|
+
+ `${str(m.reference).trim() || recorded(input)}\n\nOutput:\n${input.text}\n\n${REPLY}`, VERDICT_SCHEMA, onScale), num(m.threshold) ?? 0.5),
|
|
492
519
|
},
|
|
493
520
|
"judge-vs-production": {
|
|
494
521
|
family: GRADED,
|
package/lab/run-evals.js
CHANGED
|
@@ -479,20 +479,24 @@ function prepare(bin, file, c) {
|
|
|
479
479
|
|
|
480
480
|
// ---- The type → handler registry ------------------------------------------
|
|
481
481
|
// Design §4. What a file *is* to a run is decided here and only here, so the
|
|
482
|
-
// queue and the page never learn about file types, and
|
|
483
|
-
//
|
|
482
|
+
// queue and the page never learn about file types, and xlsx is a later
|
|
483
|
+
// entry that touches nothing but this map.
|
|
484
484
|
//
|
|
485
485
|
// A file with no mapper is passed through anyway -- decoded as UTF-8 with
|
|
486
486
|
// replacement and joined at {text} like the text handler, and flagged in the
|
|
487
|
-
// transcript. Stating the failure mode beats hiding it: a
|
|
488
|
-
//
|
|
489
|
-
//
|
|
487
|
+
// transcript. Stating the failure mode beats hiding it: a binary format's
|
|
488
|
+
// bytes arrive as mojibake, and a run that read them "well" would be a
|
|
489
|
+
// finding about nothing.
|
|
490
490
|
const HANDLERS = new Map();
|
|
491
491
|
[
|
|
492
492
|
// Images: prepared to the profile's pixel budget by ImageMagick and sent
|
|
493
493
|
// as the image content part. The formats are server.py's FILE_TYPES
|
|
494
494
|
// image half, so nothing accepted for upload has no executor to run it.
|
|
495
495
|
["image", "jpg", "jpeg", "png", "heic", "heif", "dng", "tif", "tiff"],
|
|
496
|
+
// Documents: the file's bytes, unprocessed, handed to an API that reads
|
|
497
|
+
// them whole (core.DOCUMENT_TYPES); a connection that cannot is refused
|
|
498
|
+
// before the run, by validatePipeline.
|
|
499
|
+
["document", ...Object.keys(core.DOCUMENT_TYPES).map(e => e.slice(1))],
|
|
496
500
|
// Text-ish: decoded UTF-8, joining the stage at {text} -- the text-content
|
|
497
501
|
// semantics the canvas already has (core.textPrompt).
|
|
498
502
|
["text", "txt", "md", "csv"],
|
|
@@ -511,8 +515,8 @@ const UNMAPPED = "the file has no handler, so it was passed through as raw "
|
|
|
511
515
|
|
|
512
516
|
/**
|
|
513
517
|
* One file of the Source made into what the job consumes: an image as the
|
|
514
|
-
* dataUrl at [conn]'s budget, or text at {text}. A
|
|
515
|
-
* still runs, and says so.
|
|
518
|
+
* dataUrl at [conn]'s budget, a document as its bytes, or text at {text}. A
|
|
519
|
+
* file with no mapper still runs, and says so.
|
|
516
520
|
*/
|
|
517
521
|
function itemContent(bin, file, conn) {
|
|
518
522
|
const kind = HANDLERS.get(extOf(file));
|
|
@@ -520,6 +524,7 @@ function itemContent(bin, file, conn) {
|
|
|
520
524
|
const { dataUrl, dims } = prepare(bin, file, conn);
|
|
521
525
|
return { kind, dataUrl, text: null, flags: [] };
|
|
522
526
|
}
|
|
527
|
+
if (kind === "document") return { kind, dataUrl: documentOf(file), text: null, flags: [] };
|
|
523
528
|
// TextDecoder with fatal unset replaces malformed bytes with U+FFFD --
|
|
524
529
|
// what "decoded as UTF-8 with replacement" means, for a mapped-text file
|
|
525
530
|
// and an unmapped one alike.
|
|
@@ -527,6 +532,13 @@ function itemContent(bin, file, conn) {
|
|
|
527
532
|
return { kind: kind ?? "unmapped", dataUrl: null, text, flags: kind ? [] : [UNMAPPED] };
|
|
528
533
|
}
|
|
529
534
|
|
|
535
|
+
/** A document file as the attachment a stage sends: its bytes, its media
|
|
536
|
+
type and its name. */
|
|
537
|
+
function documentOf(file) {
|
|
538
|
+
const name = path.basename(file);
|
|
539
|
+
return core.documentUrl(name, core.documentTypeOf(name).media, fs.readFileSync(file).toString("base64"));
|
|
540
|
+
}
|
|
541
|
+
|
|
530
542
|
// ---- Reaching a model ----------------------------------------------------
|
|
531
543
|
|
|
532
544
|
/**
|
|
@@ -737,9 +749,10 @@ async function ask(to, c, prompt, dataUrl, schema = null) {
|
|
|
737
749
|
// this invocation did, so resume and re-run item agree with a fresh run.
|
|
738
750
|
|
|
739
751
|
// What a file *is* to a run, the same registry §4 gives the graded set: an
|
|
740
|
-
// image is prepared to the profile's pixel budget; a
|
|
741
|
-
// UTF-8 and joined at {text}; anything else is passed
|
|
742
|
-
// replacement and flagged, so the failure mode is
|
|
752
|
+
// image is prepared to the profile's pixel budget; a document is sent whole;
|
|
753
|
+
// a text file is decoded UTF-8 and joined at {text}; anything else is passed
|
|
754
|
+
// through as UTF-8 with replacement and flagged, so the failure mode is
|
|
755
|
+
// stated rather than hidden.
|
|
743
756
|
const RUN_IMAGE_TYPES = new Set([".jpg", ".jpeg", ".png", ".heic", ".heif", ".dng", ".tif", ".tiff"]);
|
|
744
757
|
const RUN_TEXT_TYPES = new Set([".txt", ".md", ".csv"]);
|
|
745
758
|
|
|
@@ -916,6 +929,7 @@ async function runSnapshot(o) {
|
|
|
916
929
|
: files.map(name => {
|
|
917
930
|
const ext = path.extname(name).toLowerCase();
|
|
918
931
|
if (RUN_IMAGE_TYPES.has(ext)) return { name, kind: "image" };
|
|
932
|
+
if (core.documentTypeOf(name)) return { name, kind: "document" };
|
|
919
933
|
// A record (flows/record.ts): one call of a Power Automate step, what
|
|
920
934
|
// an HTTP Request is rebuilt from.
|
|
921
935
|
if (ext === ".json") return { name, kind: "record" };
|
|
@@ -981,6 +995,11 @@ async function runSnapshot(o) {
|
|
|
981
995
|
break;
|
|
982
996
|
}
|
|
983
997
|
}
|
|
998
|
+
if (item.kind === "document") {
|
|
999
|
+
const file = path.join(o.source, item.name);
|
|
1000
|
+
if (!fs.existsSync(file)) { failed = `the file ${item.name} is not in the source`; break; }
|
|
1001
|
+
dataUrl = documentOf(file);
|
|
1002
|
+
}
|
|
984
1003
|
let record = null;
|
|
985
1004
|
if (item.kind === "record") {
|
|
986
1005
|
const file = path.join(o.source, item.name);
|
|
@@ -1450,7 +1469,7 @@ async function main() {
|
|
|
1450
1469
|
found: s.found, missed: s.missed, invented: s.invented, count: s.count,
|
|
1451
1470
|
metrics: s.metrics, watch: s.watch,
|
|
1452
1471
|
terms: res.terms || [], ms: res.ms,
|
|
1453
|
-
// What a file type was to this run -- "image", "text", "unmapped".
|
|
1472
|
+
// What a file type was to this run -- "image", "document", "text", "unmapped".
|
|
1454
1473
|
type: item?.kind ?? null,
|
|
1455
1474
|
// What the model actually said. #248 wants a failure written out as a
|
|
1456
1475
|
// task an agent can act on, and "missed water" without the reply that
|