evals-lab 0.10.0 → 0.12.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 +88 -1
- package/README.md +12 -6
- package/bin/evals-lab.js +9 -1
- package/bin/lab-file.js +97 -0
- package/bin/run.js +16 -16
- 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 +8 -6
- package/lab/demo/pipelines/demo-2.json +8 -6
- 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 +584 -167
- package/lab/flows/record.mjs +16 -4
- package/lab/kinds/list.mjs +32 -23
- package/lab/run-evals.js +3 -3
- package/lab/server.py +913 -119
- package/lab/web/dist/assets/gallery-CFntjQFG.js +5 -0
- package/lab/web/dist/assets/main-BlTG2i20.css +1 -0
- package/lab/web/dist/assets/main-D6MNWqak.js +17 -0
- package/lab/web/dist/assets/tokens-DwzzdSIF.css +1 -0
- package/lab/web/dist/assets/tokens-bvuNFuYg.js +60 -0
- package/lab/web/dist/gallery.html +3 -3
- package/lab/web/dist/index.html +4 -4
- package/package.json +10 -2
- package/lab/web/dist/assets/gallery-BcvmOPqb.js +0 -3
- package/lab/web/dist/assets/main-BBUxHZ-D.js +0 -16
- package/lab/web/dist/assets/main-CMoyjImZ.css +0 -1
- package/lab/web/dist/assets/tokens-B7N4NvmM.css +0 -1
- package/lab/web/dist/assets/tokens-CiJkwD3V.js +0 -60
package/lab/flows/record.mjs
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
import { evaluate, } from "./wdl.mjs";
|
|
9
9
|
|
|
10
10
|
export const RECORD_FORMAT = "evals-lab/record";
|
|
11
|
-
export const RECORD_VERSION =
|
|
11
|
+
export const RECORD_VERSION = 2;
|
|
12
|
+
/** The origins a record names. Version 2 added `edited`: a call changed in the
|
|
13
|
+
Call dialog, keeping the `run` and `iteration` it came from. */
|
|
14
|
+
export const RECORD_ORIGINS = ["run", "hand", "upload", "edited"] ;
|
|
12
15
|
|
|
13
16
|
/** What a step's template read, by where the expression reads it from. */
|
|
14
17
|
|
|
@@ -39,11 +42,20 @@ export const RECORD_VERSION = 1;
|
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
const isObj = (v ) => v != null && typeof v === "object" && !Array.isArray(v);
|
|
46
49
|
|
|
50
|
+
/** A version-1 record as today's, as every reader takes one (server.py's
|
|
51
|
+
`upgrade_record`, its twin). Version 2 only widened the origins, so the
|
|
52
|
+
upgrade is the number; anything else is returned as it is, for
|
|
53
|
+
recordProblems to judge. */
|
|
54
|
+
export function upgradeRecord(value ) {
|
|
55
|
+
return isObj(value) && value.format === RECORD_FORMAT && value.version === 1
|
|
56
|
+
? { ...value, version: RECORD_VERSION } : value;
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
/** Every reason [value] is not a record this lab keeps, as sentences. */
|
|
48
60
|
export function recordProblems(value ) {
|
|
49
61
|
if (!isObj(value)) return ["a record is a JSON object"];
|
|
@@ -68,8 +80,8 @@ export function recordProblems(value ) {
|
|
|
68
80
|
bad.push("a record's result is { status, body }, { error } or null");
|
|
69
81
|
}
|
|
70
82
|
const origin = value.origin;
|
|
71
|
-
if (!isObj(origin) || !
|
|
72
|
-
bad.push("a record's origin is run, hand or
|
|
83
|
+
if (!isObj(origin) || !(RECORD_ORIGINS ).includes(origin.kind)) {
|
|
84
|
+
bad.push("a record's origin is run, hand, upload or edited");
|
|
73
85
|
}
|
|
74
86
|
return bad;
|
|
75
87
|
}
|
package/lab/kinds/list.mjs
CHANGED
|
@@ -58,7 +58,7 @@ export const PARSE_FORMATS = ["csv", "lines", "json"] ;
|
|
|
58
58
|
|
|
59
59
|
/** A reply as the text its items come from, and the items, each trimmed and
|
|
60
60
|
none blank. Nothing else is done to them: cleaning is the filters' -- a
|
|
61
|
-
|
|
61
|
+
Pre-format text filter shapes the reply, this splits it (#405). */
|
|
62
62
|
export function splitItems(reply , parse = "csv") {
|
|
63
63
|
const body = String(reply ?? "");
|
|
64
64
|
if (parse === "json") {
|
|
@@ -288,7 +288,12 @@ export const FILTER_SEPARATORS = ["comma", "lines", "semicolon", "none"]
|
|
|
288
288
|
/** What applying a filter or a set left: the reply text (a Rewrite text filter
|
|
289
289
|
changes it), the items kept, what was dropped and by which filter, and --
|
|
290
290
|
the first filter that rejected -- the reply's rejection (null when none). */
|
|
291
|
-
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
292
297
|
|
|
293
298
|
/** A base filter type (#334's decision: a registry, not an enum). A reader
|
|
294
299
|
asks the entry by `filter.type`; nothing branches on the id. */
|
|
@@ -296,8 +301,10 @@ export const FILTER_SEPARATORS = ["comma", "lines", "semicolon", "none"]
|
|
|
296
301
|
|
|
297
302
|
|
|
298
303
|
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
301
308
|
|
|
302
309
|
|
|
303
310
|
|
|
@@ -350,22 +357,22 @@ function rewriteText(op )
|
|
|
350
357
|
|
|
351
358
|
/** The lab's base filter types (#405). Generic code asks this registry by
|
|
352
359
|
`filter.type`; it never names an id. `rewrite-text`/`reject-text` read the
|
|
353
|
-
raw reply before it is read into items (the
|
|
354
|
-
read the items after (the
|
|
360
|
+
raw reply before it is read into items (the Pre-format stage); the rest
|
|
361
|
+
read the items after (the Post-format stage). */
|
|
355
362
|
export const FILTER_TYPES = {
|
|
356
363
|
"rewrite-text": {
|
|
357
|
-
label: "Rewrite text", scope: "text", conditions: [],
|
|
364
|
+
label: "Rewrite text", about: "Remove thinking, a preamble or a pattern from the reply's text.", scope: "text", conditions: [],
|
|
358
365
|
opProblems: (f, at) => opKindProblems(f.op, at, {
|
|
359
366
|
thinking: [], preamble: [], replace: ["pattern", "replacement"],
|
|
360
367
|
}),
|
|
361
368
|
apply: (filter, reply) => ({ body: rewriteText(filter.op)(reply.body), items: reply.items, dropped: [], reject: null }),
|
|
362
369
|
},
|
|
363
370
|
"reject-text": {
|
|
364
|
-
label: "Reject text", scope: "text", conditions: ["blank", "regex"], rejects: true,
|
|
371
|
+
label: "Reject text", about: "Discard the reply when it is blank or matches a pattern.", scope: "text", conditions: ["blank", "regex"], rejects: true,
|
|
365
372
|
apply: (filter, reply) => ({ body: reply.body, items: reply.items, dropped: [], reject: rejectTextHolds(filter.match, reply.body) ? filter.id : null }),
|
|
366
373
|
},
|
|
367
374
|
"drop-item": {
|
|
368
|
-
label: "Drop item", scope: "item", conditions: ITEM_CONDITIONS,
|
|
375
|
+
label: "Drop item", about: "Remove the items that match its conditions.", scope: "item", conditions: ITEM_CONDITIONS,
|
|
369
376
|
apply(filter, reply) {
|
|
370
377
|
const test = matcher(filter.match);
|
|
371
378
|
if (!test) return outcome(reply);
|
|
@@ -380,7 +387,7 @@ export const FILTER_TYPES = {
|
|
|
380
387
|
},
|
|
381
388
|
},
|
|
382
389
|
"rewrite-item": {
|
|
383
|
-
label: "Rewrite item", scope: "item", conditions: MATCHER_KEYS,
|
|
390
|
+
label: "Rewrite item", about: "Tidy each item, or change its case.", scope: "item", conditions: MATCHER_KEYS,
|
|
384
391
|
opProblems: (f, at) => opKindProblems(f.op, at, { tidy: [], case: ["mode"] }, op =>
|
|
385
392
|
op.kind === "case" && !CASE_MODES.includes(op.mode ) ? [`${at}'s case needs a mode`] : []),
|
|
386
393
|
apply(filter, reply) {
|
|
@@ -393,7 +400,7 @@ export const FILTER_TYPES = {
|
|
|
393
400
|
},
|
|
394
401
|
},
|
|
395
402
|
"keep-first": {
|
|
396
|
-
label: "Keep first", scope: "list", conditions: [],
|
|
403
|
+
label: "Keep first", about: "Keep the first N items.", scope: "list", conditions: [],
|
|
397
404
|
opProblems: (f, at) => (Number.isInteger(f.op?.n) && (f.op .n ) >= 1
|
|
398
405
|
? extraOpKeys(f.op, ["n"], at) : [`${at} needs a whole number of 1 or more`]),
|
|
399
406
|
apply(filter, reply) {
|
|
@@ -402,7 +409,7 @@ export const FILTER_TYPES = {
|
|
|
402
409
|
},
|
|
403
410
|
},
|
|
404
411
|
"reject-reply": {
|
|
405
|
-
label: "Reject reply", scope: "answer", conditions: ["blank", "itemCount", "share", "items"], rejects: true,
|
|
412
|
+
label: "Reject reply", about: "Discard the whole reply when its items match its conditions.", scope: "answer", conditions: ["blank", "itemCount", "share", "items"], rejects: true,
|
|
406
413
|
apply: (filter, reply) => ({ body: reply.body, items: reply.items, dropped: [], reject: rejectHolds(filter.match, reply.body, reply.items) ? filter.id : null }),
|
|
407
414
|
},
|
|
408
415
|
};
|
|
@@ -459,22 +466,24 @@ export function applyFilter(filter , reply ) {
|
|
|
459
466
|
reshapes the text the next reads, each item filter narrows or rewrites the
|
|
460
467
|
items, and the first filter that rejects gates the answer and stops the
|
|
461
468
|
rest. Given a [stage], only the filters whose type runs there apply: a set
|
|
462
|
-
belongs to no stage (#432), so one linked both
|
|
469
|
+
belongs to no stage (#432), so one linked both Pre-format and Post-format runs
|
|
463
470
|
its text filters on the raw reply and the rest on the items. Pure. */
|
|
464
471
|
export function applyFilterSet(filters , reply , stage ) {
|
|
465
472
|
const instruction = reply.instruction ?? "";
|
|
466
473
|
let body = reply.body, items = reply.items;
|
|
467
474
|
const dropped = [];
|
|
475
|
+
let emptied ;
|
|
468
476
|
for (const filter of filters) {
|
|
469
477
|
const scope = FILTER_TYPES[filter.type]?.scope;
|
|
470
478
|
if (stage && scope && (scope === "text") !== (stage === "text")) continue;
|
|
471
479
|
const out = applyFilter(filter, { body, items, instruction });
|
|
472
480
|
body = out.body;
|
|
481
|
+
if (items.length && !out.items.length) emptied = filter.id;
|
|
473
482
|
items = out.items;
|
|
474
483
|
dropped.push(...out.dropped);
|
|
475
484
|
if (out.reject != null) return { body, items, dropped, reject: out.reject };
|
|
476
485
|
}
|
|
477
|
-
return { body, items, dropped, reject: null };
|
|
486
|
+
return { body, items, dropped, reject: null, ...(emptied ? { emptied } : {}) };
|
|
478
487
|
}
|
|
479
488
|
|
|
480
489
|
// A newline always ends an item, as it does in the real csv/lines parse
|
|
@@ -639,16 +648,16 @@ export function applyCase(item , mode ) {
|
|
|
639
648
|
}
|
|
640
649
|
}
|
|
641
650
|
|
|
642
|
-
/** The id of the synthetic modifier a linked or private
|
|
651
|
+
/** The id of the synthetic modifier a linked or private Post-format filter
|
|
643
652
|
set applies under at run time (#339, #405): it is never in a document or
|
|
644
653
|
the Add menu -- a job carries a `filters` step, which `stageModifiers`
|
|
645
654
|
turns into one of these where the step sits, so a set cleans and gates a
|
|
646
|
-
list reply exactly as it reads. The
|
|
655
|
+
list reply exactly as it reads. The Pre-format (text) stage runs its
|
|
647
656
|
filters on the raw reply before it is read, not as a modifier. */
|
|
648
657
|
export const FILTER_MODIFIER = "filterSet";
|
|
649
658
|
|
|
650
659
|
const LIST_MODIFIERS = {
|
|
651
|
-
//
|
|
660
|
+
// A Post-format filter set's filters, applied where its link sits: the
|
|
652
661
|
// run's only reader of a resolved items-stage set, hidden from the Add menu
|
|
653
662
|
// (a set is linked in Responses, not added as a modifier). The lab's own
|
|
654
663
|
// cleaning modifiers are retired (#405) -- every cleaning step is a filter.
|
|
@@ -657,8 +666,8 @@ const LIST_MODIFIERS = {
|
|
|
657
666
|
apply(items, m, ctx) {
|
|
658
667
|
const out = applyFilterSet((m.filters ) || [], { body: ctx.body, items, instruction: ctx.instruction }, "items");
|
|
659
668
|
return out.reject != null
|
|
660
|
-
? { value: out.items, dropped: out.dropped, reject: out.reject }
|
|
661
|
-
: { value: out.items, dropped: out.dropped };
|
|
669
|
+
? { value: out.items, dropped: out.dropped, reject: out.reject, by: out.reject }
|
|
670
|
+
: { value: out.items, dropped: out.dropped, ...(out.emptied ? { by: out.emptied } : {}) };
|
|
662
671
|
},
|
|
663
672
|
},
|
|
664
673
|
};
|
|
@@ -703,7 +712,7 @@ export const CLEANING_MODIFIERS = ["reject", "drop", "tidy", "drop-echoes", "ded
|
|
|
703
712
|
const baseFilter = (id , type , extra = {}) =>
|
|
704
713
|
({ id, type, match: {}, options: { ignoreFailures: false, repeat: 1 }, tests: [], ...extra });
|
|
705
714
|
|
|
706
|
-
/** The
|
|
715
|
+
/** The Post-format filter(s) a retired cleaning modifier [m] becomes, in the
|
|
707
716
|
same order and reading a reply the same way: Tidy and Case are a Rewrite
|
|
708
717
|
item, Drop prompt echoes and Remove duplicates a Drop item on the `echoes`
|
|
709
718
|
and `duplicate` conditions, Keep first its own type, and Drop items / Reject
|
|
@@ -722,7 +731,7 @@ export function cleaningFilters(m ) {
|
|
|
722
731
|
}
|
|
723
732
|
}
|
|
724
733
|
|
|
725
|
-
/** The
|
|
734
|
+
/** The Pre-format (text) filters the List `preamble` setting becomes:
|
|
726
735
|
Remove thinking then Remove preamble, which together read a reply exactly as
|
|
727
736
|
`preamble: true` did (#405). */
|
|
728
737
|
export function preambleFilters() {
|
|
@@ -777,8 +786,8 @@ function legacyTagsCleaning(rules , mode )
|
|
|
777
786
|
|
|
778
787
|
/** What a v2 pipeline's `tags` job is upgraded to, and the runtime List
|
|
779
788
|
configuration the checks hold to that kind's answers (parity-check.js): the
|
|
780
|
-
reply read by the
|
|
781
|
-
items cleaned and gated by the
|
|
789
|
+
reply read by the Pre-format text filters (`before`) then split, its
|
|
790
|
+
items cleaned and gated by the Post-format filters, carried as the
|
|
782
791
|
synthetic `FILTER_MODIFIER` modifier so a caller runs them as a run would.
|
|
783
792
|
Derived from `legacyTagsCleaning` through `cleaningFilters`, the same path
|
|
784
793
|
the version-15 upgrade takes, so they cannot disagree. */
|
package/lab/run-evals.js
CHANGED
|
@@ -633,7 +633,7 @@ function graderFor(run) {
|
|
|
633
633
|
const c = run.profiles?.[ref.id];
|
|
634
634
|
if (!c) return undefined;
|
|
635
635
|
if (!made.has(ref.id)) {
|
|
636
|
-
const link = reach(c, core.keyVar(ref.id, c.slug));
|
|
636
|
+
const link = reach(c, core.keyVar(core.answerConnectionId(ref.id, c), c.slug));
|
|
637
637
|
made.set(ref.id, async (prompt, schema) => {
|
|
638
638
|
// Held to the metric's schema where the grader's API can hold a
|
|
639
639
|
// reply to one; a model or server that refuses the schema is asked
|
|
@@ -1160,10 +1160,10 @@ function cliRun(o, dataset) {
|
|
|
1160
1160
|
}
|
|
1161
1161
|
}
|
|
1162
1162
|
doc = core.withContent(doc, { type: "source", ref: { id: "cli", name: "" }, first: null, loops: 1, files: [] });
|
|
1163
|
-
doc.targets = [{ id: "cli", name: "",
|
|
1163
|
+
doc.targets = [{ id: "cli", name: "", connection: { id: "cli", name: "" }, profile: null,
|
|
1164
1164
|
steps: [{ type: "prompt", prompt: o.prompt ?? "" }] }];
|
|
1165
1165
|
doc.evals = [{ id: "cli", type: "group", name: "", continueOnFailure: true, group: { id: "cli", name: "" }, pin: null }];
|
|
1166
|
-
doc.profiles = { cli: { name: null, url: o.url, model: o.model, type: "openai-compatible" } };
|
|
1166
|
+
doc.profiles = { [core.answerKey("cli")]: { name: null, url: o.url, model: o.model, type: "openai-compatible" } };
|
|
1167
1167
|
return doc;
|
|
1168
1168
|
}
|
|
1169
1169
|
|