evals-lab 0.14.0 → 0.15.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 +44 -0
- package/lab/VERSION +1 -1
- package/lab/demo/manifest.json +2 -2
- package/lab/demo/pipelines/demo-1.json +14 -11
- package/lab/demo/pipelines/demo-2.json +14 -11
- package/lab/demo/pipelines/documents.json +3 -4
- package/lab/demo/pipelines/sentiment.json +1 -1
- package/lab/demo/pipelines/weather-alerts.json +1 -1
- package/lab/demo/pipelines/wordings.json +1 -1
- package/lab/evals-core.mjs +243 -164
- package/lab/kinds/list.mjs +327 -223
- package/lab/server.py +130 -28
- package/lab/web/dist/assets/gallery-Cy8oycht.js +5 -0
- package/lab/web/dist/assets/gallery-eVJoaK-6.css +1 -0
- package/lab/web/dist/assets/main-B-LGmhPu.css +1 -0
- package/lab/web/dist/assets/main-wqNvXpWp.js +15 -0
- package/lab/web/dist/assets/tokens-CWVzmtid.css +1 -0
- package/lab/web/dist/assets/tokens-mCS6IZGh.js +64 -0
- package/lab/web/dist/gallery.html +4 -4
- package/lab/web/dist/index.html +4 -4
- package/package.json +1 -1
- package/lab/web/dist/assets/gallery-B_-TH0F-.css +0 -1
- package/lab/web/dist/assets/gallery-qrf0ZGXK.js +0 -5
- package/lab/web/dist/assets/main-D6xARg2A.js +0 -16
- package/lab/web/dist/assets/main-xVU6J7Gi.css +0 -1
- package/lab/web/dist/assets/tokens-C8dqXDHN.js +0 -62
- package/lab/web/dist/assets/tokens-tQ7zTPkd.css +0 -1
package/lab/evals-core.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
// and that is a measurement.
|
|
34
34
|
|
|
35
35
|
import yaml from "./js-yaml.mjs";
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
import { evaluate, asText, templateProblem, WdlError, } from "./flows/wdl.mjs";
|
|
38
38
|
import { stepsOf as flowStepsOf, actionsByName, readerFor, readsOf, } from "./flows/record.mjs";
|
|
39
39
|
import { flowApi, } from "./flows/flowApi.mjs";
|
|
@@ -41,7 +41,7 @@ import registerMetrics from "./metrics/builtin.mjs";
|
|
|
41
41
|
import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFilters, preambleFilters, CLEANING_MODIFIERS,
|
|
42
42
|
splitItems, tidy, tryExamples, itemRulesProblems, dropBy, rejectBy, matcher,
|
|
43
43
|
FILTER_TYPES, FILTER_SEPARATORS, FILTER_MODIFIER, applyFilter, applyFilterSet, tryFilter, splitSample, filterProblems, filterFromItemRule,
|
|
44
|
-
|
|
44
|
+
filterV4, V3_TEXT_TYPES, } from "./kinds/list.mjs";
|
|
45
45
|
|
|
46
46
|
// ---- The types -------------------------------------------------------------
|
|
47
47
|
// docs/pipeline-model.md as types, and the shapes of the registries every
|
|
@@ -141,16 +141,18 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
/** The filter sets a job cleans and gates its reply by (#339, #
|
|
144
|
+
/** The filter sets a job cleans and gates its reply by (#339, #615): each a
|
|
145
145
|
link to a Library filter set (followed latest or pinned) or a private one
|
|
146
|
-
held inline,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
held inline, one list in run order. `format` is where the job's Response
|
|
147
|
+
Format reads the reply, when it reads items: before `filterSets[format]`
|
|
148
|
+
-- the filters above it run on the raw text, those below on each item.
|
|
149
|
+
Absent, a linked set's own `format` marks it (the first that has one),
|
|
150
|
+
or else the format reads after them all. A format that reads text reads
|
|
151
|
+
after them all whatever it says. A job holds one `filters` step. */
|
|
150
152
|
|
|
151
153
|
|
|
152
|
-
|
|
153
154
|
|
|
155
|
+
|
|
154
156
|
|
|
155
157
|
|
|
156
158
|
/** A job's steps: its Content stage, then its Responses (§16). */
|
|
@@ -358,32 +360,20 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
358
360
|
* at. `filters` are tried in order -- each drop-item narrows the items the
|
|
359
361
|
* next reads, and the first reject-reply that fires gates the answer.
|
|
360
362
|
*/
|
|
361
|
-
/**
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
belongs to no stage (#432), and runs its share in each it is linked in. */
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
/** The stage a filter type's scope belongs to: text filters read the raw
|
|
371
|
-
reply; item, list and answer filters read the items it is read into. */
|
|
372
|
-
export const stageOfScope = (scope ) => (scope === "text" ? "text" : "items");
|
|
373
|
-
|
|
374
|
-
/** What job's [stage] filters read when it answers as [kind] (#557):
|
|
375
|
-
Pre-format the text; Post-format the items after a kind that reads
|
|
376
|
-
items, and the text after any other. A filter runs there when its
|
|
377
|
-
`stageOfScope` is this. */
|
|
378
|
-
export const stageReads = (stage , kind ) =>
|
|
379
|
-
stage === "items" && OUTPUT_KINDS[kind ?? ""]?.offers?.includes("items") ? "items" : "text";
|
|
380
|
-
|
|
381
|
-
/** A filter stage as the page names it: before the reply is formatted, or after. */
|
|
382
|
-
export const FILTER_STAGE_LABEL = { text: "Pre-format", items: "Post-format" };
|
|
363
|
+
/** Whether a job answering as [kind] reads its reply into items, so that
|
|
364
|
+
where its Response Format sits among its filters matters (#615): the
|
|
365
|
+
filters above it run on the raw text, those below on each item. A format
|
|
366
|
+
that reads text reads after every filter. */
|
|
367
|
+
export const formatReadsItems = (kind ) =>
|
|
368
|
+
!!OUTPUT_KINDS[kind ?? ""]?.offers?.includes("items");
|
|
383
369
|
|
|
384
370
|
|
|
385
371
|
|
|
386
372
|
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
387
377
|
|
|
388
378
|
|
|
389
379
|
/** A Library filter set by reference; a run's copy says the version it used:
|
|
@@ -571,14 +561,10 @@ export const FILTER_STAGE_LABEL = { text: "Pre-form
|
|
|
571
561
|
|
|
572
562
|
|
|
573
563
|
|
|
564
|
+
|
|
574
565
|
|
|
575
|
-
|
|
576
|
-
|
|
566
|
+
|
|
577
567
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
568
|
|
|
583
569
|
|
|
584
570
|
|
|
@@ -668,17 +654,16 @@ export function kept(text , max = KEPT_CHARS) {
|
|
|
668
654
|
|
|
669
655
|
|
|
670
656
|
|
|
671
|
-
|
|
672
|
-
|
|
657
|
+
|
|
673
658
|
|
|
674
659
|
|
|
675
660
|
|
|
676
661
|
|
|
677
662
|
|
|
678
|
-
/** Which filter discarded a job's reply, or emptied it, and where (#435).
|
|
663
|
+
/** Which filter discarded a job's reply, or emptied it, and where (#435). A
|
|
664
|
+
run kept before #615 also says the stage it ran in, which nothing reads. */
|
|
679
665
|
|
|
680
666
|
|
|
681
|
-
|
|
682
667
|
|
|
683
668
|
|
|
684
669
|
|
|
@@ -713,7 +698,6 @@ export function kept(text , max = KEPT_CHARS) {
|
|
|
713
698
|
|
|
714
699
|
|
|
715
700
|
|
|
716
|
-
|
|
717
701
|
|
|
718
702
|
|
|
719
703
|
|
|
@@ -1247,10 +1231,10 @@ const platformOf = (src
|
|
|
1247
1231
|
|
|
1248
1232
|
|
|
1249
1233
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
|
|
1254
1238
|
|
|
1255
1239
|
|
|
1256
1240
|
|
|
@@ -1549,24 +1533,42 @@ function upgradeDatasetBody (body ) {
|
|
|
1549
1533
|
`{ version, filters }` body #334 introduces. A later format adds a step to
|
|
1550
1534
|
`upgradeFilterSetBody`, as `upgradeDatasetBody` grew, with server.py's
|
|
1551
1535
|
`upgrade_filter_set_body` its twin. */
|
|
1552
|
-
const FILTER_SET_VERSION =
|
|
1553
|
-
|
|
1554
|
-
/** [body] as this version of a filter set (
|
|
1555
|
-
(#405) named the stage a set shaped
|
|
1556
|
-
filter's type
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1536
|
+
const FILTER_SET_VERSION = 4 ;
|
|
1537
|
+
|
|
1538
|
+
/** [body] as this version of a filter set (4), from any earlier one. Version 2
|
|
1539
|
+
(#405) named the stage a set shaped, which version 3 (#432) dropped: each
|
|
1540
|
+
filter's type decided where it ran. Version 4 (#615) has seven verbs that
|
|
1541
|
+
run wherever they sit, around a Response Format a set may straddle: each
|
|
1542
|
+
filter becomes the verb that reads a reply as it did (`filterV4`), and a
|
|
1543
|
+
set that held both text and item filters puts its text ones first, in
|
|
1544
|
+
their order, then its `format` marker, then its item ones -- what it ran
|
|
1545
|
+
linked both before and after the format. Anything else comes back
|
|
1546
|
+
untouched for `filterSetBodyProblems` to refuse. Pure: the same body gives
|
|
1547
|
+
the same answer, and server.py's upgrade_filter_set_body is its twin. */
|
|
1560
1548
|
function upgradeFilterSetBody (body ) {
|
|
1561
1549
|
if (!isObj(body)) return body;
|
|
1562
1550
|
if (body.version === FILTER_SET_VERSION) return body;
|
|
1563
|
-
if (body.version === 1 || body.version === 2) {
|
|
1551
|
+
if (body.version === 1 || body.version === 2 || body.version === 3) {
|
|
1564
1552
|
const { stage: _stage, ...rest } = body;
|
|
1565
|
-
return { ...rest, version: FILTER_SET_VERSION } ;
|
|
1553
|
+
if (!Array.isArray(rest.filters)) return { ...rest, version: FILTER_SET_VERSION } ;
|
|
1554
|
+
const isText = (f ) => isObj(f) && V3_TEXT_TYPES.includes(f.type );
|
|
1555
|
+
const text = rest.filters.filter(isText), items = rest.filters.filter((f ) => !isText(f));
|
|
1556
|
+
return { ...rest, version: FILTER_SET_VERSION, filters: [...text, ...items].map(filterV4),
|
|
1557
|
+
...(text.length && items.length ? { format: text.length } : {}) } ;
|
|
1566
1558
|
}
|
|
1567
1559
|
return body;
|
|
1568
1560
|
}
|
|
1569
1561
|
|
|
1562
|
+
/** A version-1 to -3 private filter set (#615) as a version-4 one that runs
|
|
1563
|
+
on the side of the format its link sits: each filter its verb, in its
|
|
1564
|
+
order, and no marker -- a private set held one stage's filters. A current
|
|
1565
|
+
body comes back as it was. */
|
|
1566
|
+
function ownBodyV4(own ) {
|
|
1567
|
+
if (!isObj(own) || ![1, 2, 3].includes(own.version )) return own;
|
|
1568
|
+
const { stage: _stage, ...rest } = own;
|
|
1569
|
+
return { ...rest, version: FILTER_SET_VERSION, ...(Array.isArray(rest.filters) ? { filters: rest.filters.map(filterV4) } : {}) };
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1570
1572
|
/** A filter set with no filters, at this version: what New filter set starts
|
|
1571
1573
|
from, and what a lab with none holds. */
|
|
1572
1574
|
function blankFilterSetBody() {
|
|
@@ -1581,7 +1583,16 @@ function filterSetBodyProblems(body , at = "the filter set") {
|
|
|
1581
1583
|
if (body.version !== FILTER_SET_VERSION) {
|
|
1582
1584
|
return [`${at} is version ${JSON.stringify(body.version)}, and the lab reads version ${FILTER_SET_VERSION}`];
|
|
1583
1585
|
}
|
|
1584
|
-
return filterProblems(body.filters, at)
|
|
1586
|
+
return [...filterProblems(body.filters, at),
|
|
1587
|
+
...formatProblems(body.format, Array.isArray(body.filters) ? body.filters.length : 0, at)];
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/** Why [format] is not a place among [n] entries the Response Format can
|
|
1591
|
+
read at (#615): a whole number from 0 to [n], or none. */
|
|
1592
|
+
function formatProblems(format , n , at ) {
|
|
1593
|
+
if (format == null) return [];
|
|
1594
|
+
return Number.isInteger(format) && (format ) >= 0 && (format ) <= n
|
|
1595
|
+
? [] : [`${at}: Response Format has to sit among its filters`];
|
|
1585
1596
|
}
|
|
1586
1597
|
|
|
1587
1598
|
/** Why [link] is not a filter-set link a run's Responses step may carry, one
|
|
@@ -3324,30 +3335,20 @@ async function runPipeline(stages , dataUrl , cal
|
|
|
3324
3335
|
// and that answer is graded as though the job had worked.
|
|
3325
3336
|
const handsOn = i < stages.length - 1;
|
|
3326
3337
|
const against = res.readAgainst ?? echo;
|
|
3327
|
-
// The
|
|
3328
|
-
// reads it (#405):
|
|
3338
|
+
// The filters above the format shape the raw reply before the kind
|
|
3339
|
+
// reads it (#405, #615): they reshape it, or a Reject ends the job.
|
|
3329
3340
|
const pre = applyFilterSet(st.before || [], { body: res.raw ?? "", items: [], instruction: against }, "text");
|
|
3330
|
-
const by = (filter ,
|
|
3331
|
-
if (pre.reject != null) return result({ error: named(`discarded: ${pre.reject}`), filteredBy: by(pre.reject,
|
|
3341
|
+
const by = (filter , rejected ) => ({ filter, job: i + 1, rejected });
|
|
3342
|
+
if (pre.reject != null) return result({ error: named(`discarded: ${pre.reject}`), filteredBy: by(pre.reject, true) });
|
|
3332
3343
|
if (pre.rewrote) transcript[transcript.length - 1] .filtered = { got: pre.body, by: pre.rewrote };
|
|
3333
3344
|
const read = reader.read(pre.body, { echo: against, parsing: opts.parsing, settings: st.settings });
|
|
3334
3345
|
if (read.error) return result({ error: named(read.error) });
|
|
3335
3346
|
const mod = applyModifiers(mods[i], kind, read.value, { instruction: against, body: read.body ?? res.raw ?? "" });
|
|
3336
3347
|
if (mod.reject) {
|
|
3337
|
-
return result({ error: named(`discarded: ${mod.reject}`), ...(mod.by ? { filteredBy: by(mod.by,
|
|
3338
|
-
}
|
|
3339
|
-
// A kind that reads no items runs its Post-format filters on its text,
|
|
3340
|
-
// once read (#557); what they leave is what is graded.
|
|
3341
|
-
if (st.after?.length && typeof mod.value === "string") {
|
|
3342
|
-
const post = applyFilterSet(st.after, { body: mod.value, items: [], instruction: against }, "text");
|
|
3343
|
-
if (post.reject != null) return result({ error: named(`discarded: ${post.reject}`), filteredBy: by(post.reject, "items", true) });
|
|
3344
|
-
if (post.rewrote) {
|
|
3345
|
-
transcript[transcript.length - 1] .filtered = { got: post.body, by: [...(pre.rewrote || []), ...post.rewrote] };
|
|
3346
|
-
mod.value = post.body;
|
|
3347
|
-
}
|
|
3348
|
+
return result({ error: named(`discarded: ${mod.reject}`), ...(mod.by ? { filteredBy: by(mod.by, true) } : {}) });
|
|
3348
3349
|
}
|
|
3349
3350
|
// A filter that dropped the last item: what a zero result is explained by.
|
|
3350
|
-
const emptied = mod.by && reader.terms && !reader.terms(mod.value).length ? { filteredBy: by(mod.by,
|
|
3351
|
+
const emptied = mod.by && reader.terms && !reader.terms(mod.value).length ? { filteredBy: by(mod.by, false) } : {};
|
|
3351
3352
|
const nothing = handsOn ? reader.handsOn?.(mod.value) : null;
|
|
3352
3353
|
if (nothing) return result({ error: named(nothing), ...emptied });
|
|
3353
3354
|
const value = mod.value;
|
|
@@ -3377,8 +3378,8 @@ function readList(reply , out , instruction =
|
|
|
3377
3378
|
const reader = OUTPUT_KINDS[out.kind];
|
|
3378
3379
|
if (!reader) return { items: [], dropped: [], echoed: 0, error: `"${out.kind}" is not an output kind this lab has` };
|
|
3379
3380
|
const { kind, modifiers, before, ...settings } = out;
|
|
3380
|
-
// The
|
|
3381
|
-
//
|
|
3381
|
+
// The filters above the format shape the raw reply before the kind reads
|
|
3382
|
+
// it (#405, #615); a Reject ends the job.
|
|
3382
3383
|
const pre = applyFilterSet(before || [], { body: reply, items: [], instruction }, "text");
|
|
3383
3384
|
if (pre.reject != null) return { items: [], dropped: [], echoed: 0, error: `discarded: ${pre.reject}` };
|
|
3384
3385
|
const read = reader.read(pre.body, { echo: instruction, settings });
|
|
@@ -3456,7 +3457,11 @@ function applyModifiers (list , kind ,
|
|
|
3456
3457
|
// 16: a target names an AI Connection and, optionally, a Connection Profile,
|
|
3457
3458
|
// where it named one Target profile; a run's table is keyed by the pair
|
|
3458
3459
|
// (answerKey) -- the Target profile split in the store as it opens (#445).
|
|
3459
|
-
|
|
3460
|
+
// 17: a request reads by its job's HTTP API; 18: a saved-mode Source content
|
|
3461
|
+
// pins one response (#596).
|
|
3462
|
+
// 19: a job's filters are one list, run in order around its Response Format,
|
|
3463
|
+
// and filters are seven verbs (#615).
|
|
3464
|
+
const PIPELINE_VERSION = 19 ;
|
|
3460
3465
|
|
|
3461
3466
|
// Plain objects, so an entry is added by assignment and a reader never needs
|
|
3462
3467
|
// to know which registered it.
|
|
@@ -3965,7 +3970,7 @@ SOURCE_TYPES.dynamic = {
|
|
|
3965
3970
|
};
|
|
3966
3971
|
|
|
3967
3972
|
// HTTP GET (docs/dynamic-sources.md § 1): the safest pull provider. It fetches
|
|
3968
|
-
// a URL through the relay server-side --
|
|
3973
|
+
// a URL through the relay server-side -- Get response keeps a response, Live
|
|
3969
3974
|
// data fetches it again at submit -- so the page drives only the
|
|
3970
3975
|
// create dialog and the Call labels; the fetch, redaction and token-refusal are
|
|
3971
3976
|
// the server's (its SOURCE_PROVIDERS mirror in server.py).
|
|
@@ -3974,6 +3979,7 @@ DYNAMIC_PROVIDERS["http-get"] = { // provider: the HTTP GET adapter's own id
|
|
|
3974
3979
|
label: "HTTP GET",
|
|
3975
3980
|
icon: "square-code",
|
|
3976
3981
|
noun: "response",
|
|
3982
|
+
produceIcon: "cloud-sync",
|
|
3977
3983
|
tier: "pull",
|
|
3978
3984
|
config: ["url"],
|
|
3979
3985
|
configProblems(config){
|
|
@@ -4031,7 +4037,7 @@ DYNAMIC_PROVIDERS["api"] = { // provider: the API call adapter's own id
|
|
|
4031
4037
|
// or an Upload… by hand, formalised as a provider. The lab fetches and executes
|
|
4032
4038
|
// nothing: a Supplied item is named by the id the caller gives it (its file
|
|
4033
4039
|
// name), which is what joins it to a case. It has no config and no server-side
|
|
4034
|
-
// production, so no
|
|
4040
|
+
// production, so no Get response and no live data; its Calls are the items handed in,
|
|
4035
4041
|
// redacted on the way in like any other (server.py's take_dynamic). A hosted run
|
|
4036
4042
|
// grades those Calls; a solely-Supplied Source with none is run only via the CLI.
|
|
4037
4043
|
DYNAMIC_PROVIDERS["supplied"] = { // provider: the Supplied adapter's own id
|
|
@@ -4061,7 +4067,7 @@ DYNAMIC_PROVIDERS["file"] = { // provider: the File on disk adapter's own id
|
|
|
4061
4067
|
label: "File on disk",
|
|
4062
4068
|
icon: "file",
|
|
4063
4069
|
noun: "file",
|
|
4064
|
-
|
|
4070
|
+
produceIcon: "file-input",
|
|
4065
4071
|
tier: "local",
|
|
4066
4072
|
config: ["path"],
|
|
4067
4073
|
configProblems(config){
|
|
@@ -4085,7 +4091,7 @@ DYNAMIC_PROVIDERS["command"] = { // provider: the Command adapter's own id
|
|
|
4085
4091
|
label: "Command",
|
|
4086
4092
|
icon: "terminal",
|
|
4087
4093
|
noun: "call",
|
|
4088
|
-
|
|
4094
|
+
produceIcon: "square-terminal",
|
|
4089
4095
|
tier: "local",
|
|
4090
4096
|
config: ["command", "args"],
|
|
4091
4097
|
configProblems(config){
|
|
@@ -5163,23 +5169,20 @@ STEP_TYPES.modifier = {
|
|
|
5163
5169
|
};
|
|
5164
5170
|
|
|
5165
5171
|
// A filters step: the filter sets a job cleans and gates its reply by, each a
|
|
5166
|
-
// link or a private set (#339
|
|
5167
|
-
//
|
|
5168
|
-
// the type is `many`; two of a stage is the job's to refuse. A set belongs to
|
|
5169
|
-
// no stage (#432): linked here, it runs the filters whose type fits the step's
|
|
5170
|
-
// stage and skips the rest. A private set's body is the core's to judge
|
|
5172
|
+
// link or a private set (#339), in run order around the job's Response Format
|
|
5173
|
+
// (#615, `format`). A private set's body is the core's to judge
|
|
5171
5174
|
// (filterSetBodyProblems); a link to a Library set is the server's to resolve,
|
|
5172
5175
|
// so only its shape is checked here.
|
|
5173
5176
|
STEP_TYPES.filters = {
|
|
5174
|
-
label: "Filter sets", slot: "responses", rank: 2,
|
|
5177
|
+
label: "Filter sets", slot: "responses", rank: 2,
|
|
5175
5178
|
description: "Cleans and gates the reply with linked or private filter sets.",
|
|
5176
5179
|
in: "value", out: "value",
|
|
5177
5180
|
apply: "runPipeline",
|
|
5178
|
-
fields: ["type", "
|
|
5181
|
+
fields: ["type", "filterSets", "format"],
|
|
5179
5182
|
validate(step, _ctx, bad, at){
|
|
5180
|
-
if (step.stage !== "text" && step.stage !== "items") bad.push(`${at}: a filters step shapes the text or the items stage`);
|
|
5181
5183
|
if (!Array.isArray(step.filterSets)) return void bad.push(`${at}: filterSets has to be a list`);
|
|
5182
5184
|
for (const link of step.filterSets ) bad.push(...filterSetLinkProblems(link, at));
|
|
5185
|
+
bad.push(...formatProblems(step.format, step.filterSets.length, at));
|
|
5183
5186
|
},
|
|
5184
5187
|
};
|
|
5185
5188
|
|
|
@@ -5410,79 +5413,112 @@ function withOut(job , out ) {
|
|
|
5410
5413
|
}
|
|
5411
5414
|
|
|
5412
5415
|
/** [job] reading a reply as `legacyTagsOut` builds it, on the current model
|
|
5413
|
-
(#405): its output a List,
|
|
5414
|
-
the
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
directly. */
|
|
5416
|
+
(#405, #615): its output a List, and one filters list -- a private set
|
|
5417
|
+
above the format that reads the reply as the old `preamble` did, then the
|
|
5418
|
+
format, then a private set that cleans and gates its items. The filters
|
|
5419
|
+
are `legacyTagsOut`'s -- what the upgrade builds for a legacy list job, for
|
|
5420
|
+
a test to build directly. */
|
|
5418
5421
|
function legacyTagsJob(job , rules , mode = "LOWER") {
|
|
5419
5422
|
const legacy = legacyTagsOut(rules, mode);
|
|
5420
5423
|
const items = ((legacy.modifiers[0]?.filters ) || []);
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
}
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
const
|
|
5444
|
-
return
|
|
5445
|
-
}
|
|
5446
|
-
|
|
5447
|
-
/**
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5424
|
+
const j = withOut(job, { kind: "list", modifiers: [], parse: "csv" });
|
|
5425
|
+
const above = legacy.before.length
|
|
5426
|
+
? [{ id: `filters-${j.id}-before`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: legacy.before } }] : [];
|
|
5427
|
+
const below = items.length
|
|
5428
|
+
? [{ id: `filters-${j.id}`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: items } }] : [];
|
|
5429
|
+
return withFilterSets(j, [...above, ...below], above.length);
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5432
|
+
/** A job's filters step, when it holds one. */
|
|
5433
|
+
const filterStepOf = (job ) =>
|
|
5434
|
+
(job?.steps || []).find((st) => isObj(st) && st.type === "filters");
|
|
5435
|
+
|
|
5436
|
+
/** The filter-set links a job's Responses carries, in run order (#339,
|
|
5437
|
+
#615): read from its filters step, or none. */
|
|
5438
|
+
function filterSetsOf(job ) {
|
|
5439
|
+
return (filterStepOf(job)?.filterSets ?? []).slice();
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
/** Where job's own Response Format sits among its links (#615): before
|
|
5443
|
+
`filterSetsOf(job)[n]`, or undefined where it holds no place of its own --
|
|
5444
|
+
a straddling set's marker, or the end, is where it reads. */
|
|
5445
|
+
function filterFormatOf(job ) {
|
|
5446
|
+
const at = filterStepOf(job)?.format;
|
|
5447
|
+
return Number.isInteger(at) ? at : undefined;
|
|
5448
|
+
}
|
|
5449
|
+
|
|
5450
|
+
/** [job] with its filter-set links set to [links], and its own Response
|
|
5451
|
+
Format at [format] among them (none: undefined). The filters step is
|
|
5452
|
+
added, replaced, or taken away when no link is left. */
|
|
5453
|
+
function withFilterSets(job , links , format ) {
|
|
5454
|
+
const steps = job.steps.filter(st => st.type !== "filters");
|
|
5455
|
+
const step = { type: "filters", filterSets: [...links], ...(format != null ? { format } : {}) };
|
|
5456
|
+
return { ...job, steps: ordered(links.length ? [...steps, step] : steps) };
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
/** The body a link resolves to, or none: a private (`own`) link its own body,
|
|
5460
|
+
a Library link the body [resolve] gives (the worker's kept bodies). */
|
|
5461
|
+
const linkBody = (link , resolve ) =>
|
|
5462
|
+
link.own ?? (link.set && resolve ? resolve(link.set) ?? null : null);
|
|
5463
|
+
|
|
5464
|
+
/** Where job's Response Format reads among its links (#615), as the index
|
|
5465
|
+
of the link it reads in and the place among that link's filters: its own
|
|
5466
|
+
place (`format`) when it has one, else the first linked set whose body
|
|
5467
|
+
straddles the format (its `format`), else after every link. A format that
|
|
5468
|
+
reads text reads after every link. [resolve] gives a Library link's body. */
|
|
5469
|
+
function formatPlace(job , resolve ) {
|
|
5470
|
+
const links = filterSetsOf(job);
|
|
5471
|
+
if (!formatReadsItems(outOf(job)?.kind)) return { link: links.length, at: 0 };
|
|
5472
|
+
const own = filterFormatOf(job);
|
|
5473
|
+
if (own != null) return { link: Math.min(own, links.length), at: 0 };
|
|
5474
|
+
for (let i = 0; i < links.length; i++) {
|
|
5475
|
+
const at = linkBody(links[i] , resolve)?.format;
|
|
5476
|
+
if (Number.isInteger(at)) return { link: i, at: at };
|
|
5477
|
+
}
|
|
5478
|
+
return { link: links.length, at: 0 };
|
|
5479
|
+
}
|
|
5480
|
+
|
|
5481
|
+
/** A job's filters on either side of its Response Format (#615): `above`,
|
|
5482
|
+
run on the raw reply before it is read, and `below`, each link's share
|
|
5483
|
+
run on the items after it -- one list a link, so a filter that empties a
|
|
5484
|
+
list is named as a link's always was. */
|
|
5485
|
+
function filterSides(job , resolve ) {
|
|
5486
|
+
const links = filterSetsOf(job);
|
|
5487
|
+
const place = formatPlace(job, resolve);
|
|
5488
|
+
const above = [], below = [];
|
|
5489
|
+
links.forEach((link, i) => {
|
|
5490
|
+
const filters = linkBody(link, resolve)?.filters || [];
|
|
5491
|
+
if (i < place.link) above.push(...filters);
|
|
5492
|
+
else if (i > place.link) below.push(filters);
|
|
5493
|
+
else { above.push(...filters.slice(0, place.at)); below.push(filters.slice(place.at)); }
|
|
5494
|
+
});
|
|
5495
|
+
return { above, below };
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
/** A job's Responses below its Response Format as an ordered modifier list
|
|
5499
|
+
for a run (#339, #615): each modifier step as it is, and each link's share
|
|
5500
|
+
below the format as a synthetic modifier that applies its filters to the
|
|
5501
|
+
items where the filters step sits. The filters above the format are not
|
|
5502
|
+
modifiers -- `beforeFilters` gives them, and a run applies them to the raw
|
|
5503
|
+
reply before it is read. [resolve] gives a Library link's body. */
|
|
5459
5504
|
function stageModifiers(job , resolve ) {
|
|
5460
5505
|
const out = [];
|
|
5461
5506
|
for (const st of job?.steps || []) {
|
|
5462
5507
|
if (!isObj(st)) continue;
|
|
5463
5508
|
if (st.type === "modifier") out.push(st.modifier);
|
|
5464
|
-
else if (st.type === "filters"
|
|
5465
|
-
for (const
|
|
5509
|
+
else if (st.type === "filters") {
|
|
5510
|
+
for (const filters of filterSides(job, resolve).below) out.push({ type: FILTER_MODIFIER, filters });
|
|
5466
5511
|
}
|
|
5467
5512
|
}
|
|
5468
5513
|
return out;
|
|
5469
5514
|
}
|
|
5470
5515
|
|
|
5471
|
-
/** A job's
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
has none here -- `stageModifiers` runs its Post-format links. */
|
|
5475
|
-
function afterFilters(job , resolve ) {
|
|
5476
|
-
if (stageReads("items", outOf(job)?.kind) !== "text") return [];
|
|
5477
|
-
return (filterStepOf(job, "items")?.filterSets || []).flatMap(link => linkFilters(link, resolve));
|
|
5478
|
-
}
|
|
5479
|
-
|
|
5480
|
-
/** A job's Pre-format (text) filters for a run (#405), in document order:
|
|
5481
|
-
the filters of every link on its `text` filters step, which a run applies to
|
|
5482
|
-
the raw reply before the kind reads it into items. [resolve] gives a Library
|
|
5516
|
+
/** A job's filters above its Response Format, for a run (#405, #615), in
|
|
5517
|
+
run order: a run applies them to the raw reply before the kind reads it.
|
|
5518
|
+
Every filter of a job whose format reads text. [resolve] gives a Library
|
|
5483
5519
|
link's body, as `stageModifiers` does. */
|
|
5484
5520
|
function beforeFilters(job , resolve ) {
|
|
5485
|
-
return (
|
|
5521
|
+
return filterSides(job, resolve).above;
|
|
5486
5522
|
}
|
|
5487
5523
|
|
|
5488
5524
|
/** A new id: random, so one minted in one browser never collides with one
|
|
@@ -5528,11 +5564,6 @@ STEP_TYPES.job = {
|
|
|
5528
5564
|
}
|
|
5529
5565
|
const twice = steps.find((st, x) => !STEP_TYPES[st.type] .many && steps.findIndex(o => o.type === st.type) !== x);
|
|
5530
5566
|
if (twice) return void bad.push(`${at} has two ${STEP_TYPES[twice.type] .label ?? twice.type} steps`);
|
|
5531
|
-
// A `filters` step is `many`, but only one per stage: a Pre-format and
|
|
5532
|
-
// a Post-format set at most (#405).
|
|
5533
|
-
const twoFilters = steps.find((st, x) => st.type === "filters"
|
|
5534
|
-
&& steps.findIndex(o => o.type === "filters" && o.stage === st.stage) !== x);
|
|
5535
|
-
if (twoFilters) return void bad.push(`${at} has two ${twoFilters.stage === "text" ? "Pre-format" : "Post-format"} filter steps`);
|
|
5536
5567
|
if (steps.some((st, x) => x > 0 && placeOf(st) < placeOf(steps[x - 1]))) {
|
|
5537
5568
|
return void bad.push(`${at}: steps are its Content, then its Responses, each in order`);
|
|
5538
5569
|
}
|
|
@@ -5920,9 +5951,9 @@ function groupsOf(next ) {
|
|
|
5920
5951
|
/** [doc] with each private group's body read as this version of a group
|
|
5921
5952
|
body (`upgradeDatasetBody`): a version-8 one's `source` goes (#396); and
|
|
5922
5953
|
each private filter set's as this version of a filter-set body
|
|
5923
|
-
(`
|
|
5924
|
-
carries its own version, so this runs on a current pipeline too;
|
|
5925
|
-
bodies are all current comes back as it was. */
|
|
5954
|
+
(`ownBodyV4`): its filters the verbs that read a reply as they did (#615).
|
|
5955
|
+
A body carries its own version, so this runs on a current pipeline too;
|
|
5956
|
+
one whose bodies are all current comes back as it was. */
|
|
5926
5957
|
function ownBodiesNow(doc ) {
|
|
5927
5958
|
const stale = (t ) => isObj(t) && isObj(t.own) && t.own.version !== DATASET_BODY_VERSION;
|
|
5928
5959
|
const staleSet = (l ) => isObj(l) && isObj(l.own) && l.own.version !== FILTER_SET_VERSION;
|
|
@@ -5934,7 +5965,7 @@ function ownBodiesNow(doc ) {
|
|
|
5934
5965
|
}
|
|
5935
5966
|
if (Array.isArray(out.jobs) && out.jobs.some(staleJob)) {
|
|
5936
5967
|
out = { ...out, jobs: out.jobs.map((j ) => (!staleJob(j) ? j : { ...j, steps: j.steps.map((st ) => (!staleStep(st) ? st
|
|
5937
|
-
: { ...st, filterSets: st.filterSets.map((l ) => (staleSet(l) ? { ...l, own:
|
|
5968
|
+
: { ...st, filterSets: st.filterSets.map((l ) => (staleSet(l) ? { ...l, own: ownBodyV4(l.own) } : l)) })) })) };
|
|
5938
5969
|
}
|
|
5939
5970
|
return out;
|
|
5940
5971
|
}
|
|
@@ -5952,10 +5983,14 @@ function upgradePipeline (doc , ctx = {}) {
|
|
|
5952
5983
|
}
|
|
5953
5984
|
return localSteps(ownBodiesNow(withoutLeftoverPreamble(out)), ctx) ;
|
|
5954
5985
|
}
|
|
5955
|
-
if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17].includes(doc.version )) return doc;
|
|
5986
|
+
if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].includes(doc.version )) return doc;
|
|
5987
|
+
// Every version reads as 18 first, then as 19: a job's filters are one list
|
|
5988
|
+
// around its Response Format (oneFilterList).
|
|
5989
|
+
const nineteen = (d ) => localSteps(ownBodiesNow(oneFilterList(d)), ctx) ;
|
|
5990
|
+
if (doc.version === 18) return nineteen(withoutLeftoverPreamble(cutRefs(clone(doc) )));
|
|
5956
5991
|
// Every version reads as 17 first, then as 18: a saved-mode Source content
|
|
5957
5992
|
// pins one response (savedPinned).
|
|
5958
|
-
const eighteen = (d ) =>
|
|
5993
|
+
const eighteen = (d ) => nineteen(savedPinned(d, ctx));
|
|
5959
5994
|
if (doc.version === 17) return eighteen(cutRefs(clone(doc) ));
|
|
5960
5995
|
// Every version reads as 16 first, then as 17: its requests read by their
|
|
5961
5996
|
// job's HTTP API (requestsOnJobApi).
|
|
@@ -6026,14 +6061,16 @@ function filtersFromCleaning(next ) {
|
|
|
6026
6061
|
if (isCleaning(st)) buffer = [...buffer, ...cleaningFilters((st ).modifier )];
|
|
6027
6062
|
else if (isFilters(st)) {
|
|
6028
6063
|
for (const link of ((st ).filterSets ) || []) {
|
|
6029
|
-
if (link.own) buffer = [...buffer, ...(link.own.filters || [])];
|
|
6064
|
+
if (link.own) buffer = [...buffer, ...((ownBodyV4(link.own) ).filters || [])];
|
|
6030
6065
|
else { flush(); links.push(link); }
|
|
6031
6066
|
}
|
|
6032
6067
|
}
|
|
6033
6068
|
}
|
|
6034
6069
|
flush();
|
|
6035
|
-
|
|
6036
|
-
|
|
6070
|
+
// Version 18's two filters steps, a stage each, which oneFilterList reads
|
|
6071
|
+
// on the way to 19.
|
|
6072
|
+
const itemsStep = links.length ? { type: "filters", stage: "items", filterSets: links } : null;
|
|
6073
|
+
const textStep = preamble
|
|
6037
6074
|
? { type: "filters", stage: "text", filterSets: [{ id: `filters-${jid}-before`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: preambleFilters() } }] }
|
|
6038
6075
|
: null;
|
|
6039
6076
|
|
|
@@ -6052,6 +6089,48 @@ function filtersFromCleaning(next ) {
|
|
|
6052
6089
|
return next;
|
|
6053
6090
|
}
|
|
6054
6091
|
|
|
6092
|
+
/** Version 18 to 19 (#615): each job's Pre-format and Post-format filters
|
|
6093
|
+
steps become one, in run order -- the Pre-format links, then the
|
|
6094
|
+
Post-format ones -- and where the job's format reads items, its Response
|
|
6095
|
+
Format sits between them (`format`), so every filter runs where it did. A
|
|
6096
|
+
Library set linked on both steps is linked once, between them, and its
|
|
6097
|
+
body (now straddling the format, `upgradeFilterSetBody`) says where the
|
|
6098
|
+
format reads: the job holds no place of its own. A job whose format reads
|
|
6099
|
+
text runs every filter on its text, so it holds no place either. Each
|
|
6100
|
+
private set's body is read as version 4 (`ownBodyV4`). Every version
|
|
6101
|
+
before 19 ends here. */
|
|
6102
|
+
function oneFilterList(next ) {
|
|
6103
|
+
next.version = PIPELINE_VERSION;
|
|
6104
|
+
if (!Array.isArray(next.jobs)) return next;
|
|
6105
|
+
const isFilters = (st ) => isObj(st) && st.type === "filters";
|
|
6106
|
+
next.jobs = next.jobs.map((job ) => {
|
|
6107
|
+
if (!isObj(job) || !Array.isArray(job.steps) || !job.steps.some(isFilters)) return job;
|
|
6108
|
+
const steps = job.steps ;
|
|
6109
|
+
const linksOf = (stage ) => steps.filter(st => isFilters(st) && (st.stage ?? "items") === stage)
|
|
6110
|
+
.flatMap(st => (Array.isArray(st.filterSets) ? st.filterSets : []) )
|
|
6111
|
+
.map(l => (isObj(l) && l.own ? { ...l, own: ownBodyV4(l.own) } : l));
|
|
6112
|
+
const pre = linksOf("text"), post = linksOf("items");
|
|
6113
|
+
const setId = (l ) => (isObj(l) && isObj(l.set) ? l.set.id : null);
|
|
6114
|
+
const both = pre.find(l => setId(l) && post.some(o => setId(o) === setId(l) && o.pin === l.pin));
|
|
6115
|
+
let links , format ;
|
|
6116
|
+
if (!formatReadsItems(outOf(job )?.kind)) { links = [...pre, ...post]; format = undefined; }
|
|
6117
|
+
else if (both) {
|
|
6118
|
+
links = [...pre.filter(l => l !== both), both, ...post.filter(o => !(setId(o) === setId(both) && o.pin === both.pin))];
|
|
6119
|
+
format = undefined;
|
|
6120
|
+
} else { links = [...pre, ...post]; format = post.length ? pre.length : undefined; }
|
|
6121
|
+
const out = [];
|
|
6122
|
+
let placed = false;
|
|
6123
|
+
for (const st of steps) {
|
|
6124
|
+
if (!isFilters(st)) { out.push(st); continue; }
|
|
6125
|
+
if (placed || !links.length) continue;
|
|
6126
|
+
out.push({ type: "filters", filterSets: links, ...(format != null ? { format } : {}) });
|
|
6127
|
+
placed = true;
|
|
6128
|
+
}
|
|
6129
|
+
return { ...job, steps: out };
|
|
6130
|
+
});
|
|
6131
|
+
return next;
|
|
6132
|
+
}
|
|
6133
|
+
|
|
6055
6134
|
/** [st] without its List `preamble` setting, when it is a Read Reply holding
|
|
6056
6135
|
one; [st] itself otherwise. */
|
|
6057
6136
|
function withoutPreamble(st ) {
|
|
@@ -7057,13 +7136,13 @@ function stagesFor(run , i , resolveFilters
|
|
|
7057
7136
|
{
|
|
7058
7137
|
const stages = run.jobs.map((ch, k) => {
|
|
7059
7138
|
const { kind, modifiers: _mods, ...settings } = outOf(ch);
|
|
7060
|
-
// The modifiers a run applies, with
|
|
7061
|
-
// synthetic modifier where
|
|
7062
|
-
//
|
|
7139
|
+
// The modifiers a run applies, with the filters below the format a
|
|
7140
|
+
// synthetic modifier a link where the filters step sits (#339), and those
|
|
7141
|
+
// above it applied to the raw reply before it is read (#405, #615);
|
|
7063
7142
|
// without a resolver, a Library link reads nothing, a private one its own.
|
|
7064
7143
|
return { text: targetStepOf(run, i, k) .prompt, kind, withImage: sendsImage(ch),
|
|
7065
7144
|
verbatim: !!targetEntryOf(run, i, k)?.verbatim, before: beforeFilters(ch, resolveFilters),
|
|
7066
|
-
|
|
7145
|
+
modifiers: stageModifiers(ch, resolveFilters), settings };
|
|
7067
7146
|
});
|
|
7068
7147
|
// Each named by the connection whose key it is sent with.
|
|
7069
7148
|
const asked = (key , held ) => {
|
|
@@ -7478,7 +7557,7 @@ CONNECTION_FIELDS, SETTING_KEYS, OPTION_FIELDS, jobLabel, scenarioLabel, jobWord
|
|
|
7478
7557
|
contentOf, withContent, replyOf,
|
|
7479
7558
|
targetsOf, targetLetter, targetLabel, targetStepOf, targetEntryOf, jobTargetType, targetAnswerOf, targetAnswerKey, recordedTargetOf, withTarget,
|
|
7480
7559
|
tokensOf, withTokens, sendsImage, withImage, flowStepOf, requestStepOf, readAsOf, outOf, withOut, slotEntry, SLOTS,
|
|
7481
|
-
filterSetsOf, withFilterSets, stageModifiers, beforeFilters,
|
|
7560
|
+
filterSetsOf, filterFormatOf, withFilterSets, formatPlace, stageModifiers, beforeFilters, gradedText,
|
|
7482
7561
|
blankPipeline, upgradePipeline, fatProfileRef, newId, newEval, newLink, validatePipeline, resolvePipeline, pipelineOfRun, stagesFor, profileIds, answerKeys,
|
|
7483
7562
|
scenarioProfile, lastKind, lastModifier, modifierSummary, itemScores, scenarioEvals, scenarioPasses, groupPass, overallVerdict, passVerdict, isSkipped, failedScore,
|
|
7484
7563
|
evalLabel, evalsOf, evalsDataset, casesRef, groupOf, groupOfMetrics, pipelineWarnings, itemsFound, previewOf, isWholeRun, EVAL_FIELDS,
|