evals-lab 0.10.0 → 0.11.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 +41 -1
- package/README.md +8 -2
- package/bin/run.js +15 -15
- package/lab/VERSION +1 -1
- package/lab/demo/pipelines/demo-1.json +5 -3
- package/lab/demo/pipelines/demo-2.json +5 -3
- package/lab/evals-core.mjs +512 -155
- 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 +433 -69
- package/lab/web/dist/assets/gallery-DKy8QK3o.js +5 -0
- package/lab/web/dist/assets/main-BGmSu7aS.css +1 -0
- package/lab/web/dist/assets/main-DdGON-yl.js +16 -0
- package/lab/web/dist/assets/tokens-BmytrwxE.js +60 -0
- package/lab/web/dist/assets/tokens-DBN7I0ru.css +1 -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/evals-core.mjs
CHANGED
|
@@ -146,8 +146,7 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
146
146
|
held inline, run at the step's `stage` (#432: a set has no stage of its
|
|
147
147
|
own; the step runs its filters of that stage). A job may hold one `filters` step of
|
|
148
148
|
each stage -- a `text` one run on the raw reply before it is read into
|
|
149
|
-
items (
|
|
150
|
-
as). */
|
|
149
|
+
items (Pre-format), an `items` one run on the items after (Post-format). */
|
|
151
150
|
|
|
152
151
|
|
|
153
152
|
|
|
@@ -221,9 +220,11 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
221
220
|
|
|
222
221
|
|
|
223
222
|
/** What a target's step says beside its type: what to ask in that job,
|
|
224
|
-
and whom, where it names
|
|
223
|
+
and whom, where it names an AI Connection of its own -- and, optionally,
|
|
224
|
+
a Connection Profile over it (version 16). */
|
|
225
225
|
|
|
226
226
|
|
|
227
|
+
|
|
227
228
|
|
|
228
229
|
|
|
229
230
|
|
|
@@ -249,8 +250,11 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
249
250
|
|
|
250
251
|
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
254
258
|
|
|
255
259
|
|
|
256
260
|
|
|
@@ -345,7 +349,7 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
345
349
|
* at. `filters` are tried in order -- each drop-item narrows the items the
|
|
346
350
|
* next reads, and the first reject-reply that fires gates the answer.
|
|
347
351
|
*/
|
|
348
|
-
/** Which side of
|
|
352
|
+
/** Which side of Response Format a filter runs on (#405): `text` the raw reply before
|
|
349
353
|
it is read into items (rewrite-text / reject-text), `items` the items after
|
|
350
354
|
(drop-item, rewrite-item, keep-first, reject-reply). A filter's type decides
|
|
351
355
|
its stage; a set belongs to none (#432), and runs its share of filters in
|
|
@@ -356,6 +360,9 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
356
360
|
reply; item, list and answer filters read the items it is read into. */
|
|
357
361
|
export const stageOfScope = (scope ) => (scope === "text" ? "text" : "items");
|
|
358
362
|
|
|
363
|
+
/** A filter stage as the page names it: before the reply is formatted, or after. */
|
|
364
|
+
export const FILTER_STAGE_LABEL = { text: "Pre-format", items: "Post-format" };
|
|
365
|
+
|
|
359
366
|
|
|
360
367
|
|
|
361
368
|
|
|
@@ -401,11 +408,16 @@ export const stageOfScope = (scope ) => (scope === "text" ?
|
|
|
401
408
|
|
|
402
409
|
|
|
403
410
|
|
|
404
|
-
/**
|
|
411
|
+
/** What a target asked, as a run carries it: the AI Connection and its
|
|
412
|
+
Connection Profile's settings resolved into one -- request settings,
|
|
413
|
+
never the key. */
|
|
405
414
|
|
|
406
415
|
|
|
407
|
-
|
|
416
|
+
|
|
408
417
|
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
|
|
409
421
|
|
|
410
422
|
|
|
411
423
|
|
|
@@ -420,6 +432,8 @@ export const stageOfScope = (scope ) => (scope === "text" ?
|
|
|
420
432
|
/** A pipeline resolved at submit (§5): what the queue runs and History
|
|
421
433
|
reads. The additions, and nothing else. */
|
|
422
434
|
|
|
435
|
+
|
|
436
|
+
|
|
423
437
|
|
|
424
438
|
|
|
425
439
|
|
|
@@ -455,6 +469,38 @@ export const stageOfScope = (scope ) => (scope === "text" ?
|
|
|
455
469
|
|
|
456
470
|
|
|
457
471
|
|
|
472
|
+
/**
|
|
473
|
+
* An AI Connection as Setup stores it (version 16): where the lab sends a
|
|
474
|
+
* prompt -- its type, address, key and model, and any of its type's settings
|
|
475
|
+
* that say how it is reached (`scope: "connection"`). Its key is one typed
|
|
476
|
+
* in this browser, or KEY_HELD and its id, or empty.
|
|
477
|
+
*/
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* A Connection Profile as Setup stores it: settings a target may carry over
|
|
491
|
+
* its connection -- model and image settings, each optional, an absent one
|
|
492
|
+
* the lab's default -- and, optionally, the connection it answers on in
|
|
493
|
+
* place of the target's. One split from a Target profile is stamped with
|
|
494
|
+
* that profile's id (`fromProfile`), which is what an upgrade finds it by.
|
|
495
|
+
*/
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
|
|
458
504
|
/** A connection's settings in one flat set, the shape a type reads. */
|
|
459
505
|
|
|
460
506
|
|
|
@@ -468,18 +514,22 @@ export const stageOfScope = (scope ) => (scope === "text" ?
|
|
|
468
514
|
/** A pipeline read from YAML, references remapped to this lab. */
|
|
469
515
|
|
|
470
516
|
|
|
471
|
-
|
|
517
|
+
|
|
472
518
|
|
|
473
519
|
|
|
474
520
|
|
|
475
521
|
|
|
476
|
-
/**
|
|
522
|
+
/** An AI Connection as an import or export matches it: its slug, where it
|
|
477
523
|
has one -- withSlugs gives the rest theirs. */
|
|
478
524
|
|
|
479
525
|
|
|
480
526
|
/** What an import matches references against: { id, name } each. */
|
|
481
527
|
|
|
528
|
+
|
|
529
|
+
|
|
482
530
|
|
|
531
|
+
|
|
532
|
+
|
|
483
533
|
|
|
484
534
|
|
|
485
535
|
|
|
@@ -487,6 +537,8 @@ export const stageOfScope = (scope ) => (scope === "text" ?
|
|
|
487
537
|
|
|
488
538
|
|
|
489
539
|
|
|
540
|
+
|
|
541
|
+
|
|
490
542
|
|
|
491
543
|
|
|
492
544
|
/** An import's outcome: the remapped pipeline, or one sentence refusing it. */
|
|
@@ -501,7 +553,7 @@ export const stageOfScope = (scope ) => (scope === "text" ?
|
|
|
501
553
|
|
|
502
554
|
|
|
503
555
|
|
|
504
|
-
|
|
556
|
+
|
|
505
557
|
|
|
506
558
|
|
|
507
559
|
|
|
@@ -576,10 +628,24 @@ export function kept(text , max = KEPT_CHARS) {
|
|
|
576
628
|
|
|
577
629
|
|
|
578
630
|
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
|
|
579
635
|
|
|
580
636
|
|
|
581
637
|
|
|
582
638
|
|
|
639
|
+
/** Which filter discarded a job's reply, or emptied it, and where (#435). */
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
|
|
583
649
|
/** What runPipeline is told besides the stages. */
|
|
584
650
|
|
|
585
651
|
|
|
@@ -943,6 +1009,9 @@ export function kept(text , max = KEPT_CHARS) {
|
|
|
943
1009
|
|
|
944
1010
|
|
|
945
1011
|
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
|
|
946
1015
|
|
|
947
1016
|
|
|
948
1017
|
/**
|
|
@@ -1000,7 +1069,12 @@ export function kept(text , max = KEPT_CHARS) {
|
|
|
1000
1069
|
the lab holds. Each is optional, and a
|
|
1001
1070
|
reference nothing is given to look up is not checked. */
|
|
1002
1071
|
|
|
1003
|
-
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
|
|
1004
1078
|
|
|
1005
1079
|
|
|
1006
1080
|
|
|
@@ -1299,7 +1373,7 @@ const SLOTS = ["content", "target", "responses"];
|
|
|
1299
1373
|
|
|
1300
1374
|
|
|
1301
1375
|
|
|
1302
|
-
|
|
1376
|
+
|
|
1303
1377
|
|
|
1304
1378
|
|
|
1305
1379
|
|
|
@@ -1578,6 +1652,10 @@ function datasetRules(body ) {
|
|
|
1578
1652
|
|
|
1579
1653
|
|
|
1580
1654
|
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
|
|
1581
1659
|
|
|
1582
1660
|
|
|
1583
1661
|
|
|
@@ -2417,12 +2495,12 @@ CONNECTION_TYPES.http = {
|
|
|
2417
2495
|
id: "http", label: "HTTP endpoint",
|
|
2418
2496
|
description: "Any API a Power Automate step calls: an address, a key and how the key is sent.",
|
|
2419
2497
|
settings: [
|
|
2420
|
-
{ key: "auth", label: "Key header", control: "select", default: "bearer", hint: "",
|
|
2498
|
+
{ key: "auth", label: "Key header", control: "select", default: "bearer", hint: "", scope: "connection",
|
|
2421
2499
|
choices: [{ value: "bearer", label: "Authorization: Bearer" }, { value: "x-api-key", label: "x-api-key" },
|
|
2422
2500
|
{ value: "api-key", label: "api-key" }, { value: "none", label: "none" }] },
|
|
2423
|
-
{ key: "headers", label: "Headers", control: "text", default: "", hint: "" },
|
|
2501
|
+
{ key: "headers", label: "Headers", control: "text", default: "", hint: "", scope: "connection" },
|
|
2424
2502
|
// What Test connection asks of it: a GET, keyed as a run is.
|
|
2425
|
-
{ key: "testPath", label: "Test path", control: "text", default: "", hint: "" },
|
|
2503
|
+
{ key: "testPath", label: "Test path", control: "text", default: "", hint: "", scope: "connection" },
|
|
2426
2504
|
],
|
|
2427
2505
|
answers: ["request"],
|
|
2428
2506
|
lists: false,
|
|
@@ -3100,22 +3178,30 @@ async function runPipeline(stages , dataUrl , cal
|
|
|
3100
3178
|
// and that answer is graded as though the job had worked.
|
|
3101
3179
|
const handsOn = i < stages.length - 1;
|
|
3102
3180
|
const against = res.readAgainst ?? echo;
|
|
3103
|
-
// The
|
|
3181
|
+
// The Pre-format (text) filters shape the raw reply before the kind
|
|
3104
3182
|
// reads it (#405): a Rewrite text reshapes it, a Reject text ends the job.
|
|
3105
3183
|
const pre = applyFilterSet(st.before || [], { body: res.raw ?? "", items: [], instruction: against }, "text");
|
|
3106
|
-
|
|
3184
|
+
const by = (filter , stage , rejected ) => ({ filter, stage, job: i + 1, rejected });
|
|
3185
|
+
if (pre.reject != null) return result({ error: named(`discarded: ${pre.reject}`), filteredBy: by(pre.reject, "text", true) });
|
|
3107
3186
|
const read = reader.read(pre.body, { echo: against, parsing: opts.parsing, settings: st.settings });
|
|
3108
3187
|
if (read.error) return result({ error: named(read.error) });
|
|
3109
3188
|
const mod = applyModifiers(mods[i], kind, read.value, { instruction: against, body: read.body ?? res.raw ?? "" });
|
|
3110
|
-
if (mod.reject)
|
|
3189
|
+
if (mod.reject) {
|
|
3190
|
+
return result({ error: named(`discarded: ${mod.reject}`), ...(mod.by ? { filteredBy: by(mod.by, "items", true) } : {}) });
|
|
3191
|
+
}
|
|
3192
|
+
// A filter that dropped the last item: what a zero result is explained by.
|
|
3193
|
+
const emptied = mod.by && reader.terms && !reader.terms(mod.value).length ? { filteredBy: by(mod.by, "items", false) } : {};
|
|
3111
3194
|
const nothing = handsOn ? reader.handsOn?.(mod.value) : null;
|
|
3112
|
-
if (nothing) return result({ error: named(nothing) });
|
|
3195
|
+
if (nothing) return result({ error: named(nothing), ...emptied });
|
|
3113
3196
|
const value = mod.value;
|
|
3114
3197
|
held = { kind, value };
|
|
3115
3198
|
const gone = [...(read.dropped || []), ...mod.dropped];
|
|
3116
3199
|
echoed = (read.echoed || 0) + mod.dropped.filter(d => d.by === ECHO).length;
|
|
3117
3200
|
done.push({ reply: res.raw ?? "", rendered: reader.render?.(value) || {} });
|
|
3118
|
-
if (!handsOn)
|
|
3201
|
+
if (!handsOn) {
|
|
3202
|
+
dropped = gone;
|
|
3203
|
+
return result({ echoed, dropped, ...emptied });
|
|
3204
|
+
}
|
|
3119
3205
|
}
|
|
3120
3206
|
return result({ echoed, dropped });
|
|
3121
3207
|
}
|
|
@@ -3132,7 +3218,7 @@ function readList(reply , out , instruction =
|
|
|
3132
3218
|
const reader = OUTPUT_KINDS[out.kind];
|
|
3133
3219
|
if (!reader) return { items: [], dropped: [], echoed: 0, error: `"${out.kind}" is not an output kind this lab has` };
|
|
3134
3220
|
const { kind, modifiers, before, ...settings } = out;
|
|
3135
|
-
// The
|
|
3221
|
+
// The Pre-format (text) filters shape the raw reply before the kind
|
|
3136
3222
|
// reads it (#405); a Reject text ends the job.
|
|
3137
3223
|
const pre = applyFilterSet(before || [], { body: reply, items: [], instruction }, "text");
|
|
3138
3224
|
if (pre.reject != null) return { items: [], dropped: [], echoed: 0, error: `discarded: ${pre.reject}` };
|
|
@@ -3153,7 +3239,7 @@ function readList(reply , out , instruction =
|
|
|
3153
3239
|
was thrown away. */
|
|
3154
3240
|
function applyModifiers (list , kind , value ,
|
|
3155
3241
|
ctx = { instruction: "", body: "" }) {
|
|
3156
|
-
let v = value;
|
|
3242
|
+
let v = value, by ;
|
|
3157
3243
|
const dropped = [];
|
|
3158
3244
|
for (const m of list || []) {
|
|
3159
3245
|
const entry = MODIFIERS[m.type];
|
|
@@ -3164,9 +3250,10 @@ function applyModifiers (list , kind ,
|
|
|
3164
3250
|
if (!outcome) { v = out ; continue; }
|
|
3165
3251
|
v = outcome.value;
|
|
3166
3252
|
dropped.push(...(outcome.dropped || []));
|
|
3167
|
-
if (outcome.reject) return { value: v, dropped, reject: outcome.reject };
|
|
3253
|
+
if (outcome.reject) return { value: v, dropped, reject: outcome.reject, ...(outcome.by ? { by: outcome.by } : {}) };
|
|
3254
|
+
by = outcome.by ?? by;
|
|
3168
3255
|
}
|
|
3169
|
-
return { value: v, dropped };
|
|
3256
|
+
return { value: v, dropped, ...(by ? { by } : {}) };
|
|
3170
3257
|
}
|
|
3171
3258
|
|
|
3172
3259
|
// ---- The pipeline model ----------------------------------------------------
|
|
@@ -3205,7 +3292,12 @@ function applyModifiers (list , kind ,
|
|
|
3205
3292
|
// 12: a Contains metric's Ignore case holds item by item too.
|
|
3206
3293
|
// 13: an eval is a link to an eval group, or a group of the pipeline's own,
|
|
3207
3294
|
// and the document has an overall pass rule (pipeline-model §17).
|
|
3208
|
-
|
|
3295
|
+
// 14: a job's Responses stage links filter sets on a `filters` step (§18).
|
|
3296
|
+
// 15: every cleaning step is a filter, Pre- or Post-format (#405).
|
|
3297
|
+
// 16: a target names an AI Connection and, optionally, a Connection Profile,
|
|
3298
|
+
// where it named one Target profile; a run's table is keyed by the pair
|
|
3299
|
+
// (answerKey) -- the Target profile split in the store as it opens (#445).
|
|
3300
|
+
const PIPELINE_VERSION = 16 ;
|
|
3209
3301
|
|
|
3210
3302
|
// Plain objects, so an entry is added by assignment and a reader never needs
|
|
3211
3303
|
// to know which registered it.
|
|
@@ -3338,7 +3430,7 @@ OUTPUT_KINDS.text = {
|
|
|
3338
3430
|
// name is only a label and two profiles can share one. #46: the profile
|
|
3339
3431
|
// carries `type`, and the run's resolved copy carries it too
|
|
3340
3432
|
// (docs/pipeline-model.md §7).
|
|
3341
|
-
const CONNECTION_FIELDS = ["name", "slug", "url", "model", "type", "temperature",
|
|
3433
|
+
const CONNECTION_FIELDS = ["name", "slug", "connection", "url", "model", "type", "temperature",
|
|
3342
3434
|
"px", "format", "quality", "options"];
|
|
3343
3435
|
const SETTING_KEYS = [...new Set(Object.values(CONNECTION_TYPES).flatMap(t => t.settings.map(s => s.key)))];
|
|
3344
3436
|
const OPTION_FIELDS = ["seed", "nPredict", ...DECODING_KEYS];
|
|
@@ -3346,6 +3438,9 @@ const IMAGE_FORMATS = ["image/jpeg", "image/webp", "image/png
|
|
|
3346
3438
|
// Ids are what a key variable is spelled from, so they stay spellable: a
|
|
3347
3439
|
// letter or digit run, with at most one hyphen.
|
|
3348
3440
|
const PROFILE_ID = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)?$/;
|
|
3441
|
+
// A run's table key (answerKey): a connection's id, then a dot and its
|
|
3442
|
+
// profile's id -- or nothing, for none -- or a grader's bare id.
|
|
3443
|
+
const ANSWER_KEY = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)?(?:\.(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)?)?)?$/;
|
|
3349
3444
|
const LOOKS_LIKE_A_KEY = /^(?:api[-_]?)?key$|^(?:authorization|bearer|secret|password|token)$/i;
|
|
3350
3445
|
|
|
3351
3446
|
/** The variable a profile's key is read from: its slug's, when it has one --
|
|
@@ -3441,6 +3536,138 @@ function connectionSettings(c ) {
|
|
|
3441
3536
|
return { ...rest, ...(options || {}) };
|
|
3442
3537
|
}
|
|
3443
3538
|
|
|
3539
|
+
// ---- AI Connections and Connection Profiles (version 16) ---------------------
|
|
3540
|
+
// A Target profile was one stored thing: where a prompt goes and the settings
|
|
3541
|
+
// it goes with. It is two now -- an AI Connection (type, address, key, model)
|
|
3542
|
+
// and an optional Connection Profile (settings, and optionally a connection of
|
|
3543
|
+
// its own in place of the target's) -- and a target names both. A store from
|
|
3544
|
+
// before is split once as it opens (server.py `split_profiles`): each profile
|
|
3545
|
+
// becomes a connection keeping its id and, where it set settings, a profile
|
|
3546
|
+
// stamped with that id (`fromProfile`). What a run carries of a pair is the
|
|
3547
|
+
// two resolved into one Connection, as a profile resolved before.
|
|
3548
|
+
|
|
3549
|
+
/** What an AI Connection keeps of a Target profile, beside its type's
|
|
3550
|
+
connection-scoped settings. A legacy `profile` (#46) stays with the
|
|
3551
|
+
connection: it says the type until the page converts it. */
|
|
3552
|
+
const AI_CONNECTION_FIELDS = ["id", "name", "slug", "type", "url", "key", "model", "profile"];
|
|
3553
|
+
/** What a Connection Profile holds that is not a setting. */
|
|
3554
|
+
const CONNECTION_PROFILE_FIELDS = ["id", "name", "connection", "fromProfile"];
|
|
3555
|
+
|
|
3556
|
+
/** The settings of [type] that say how a connection is reached. */
|
|
3557
|
+
const connectionScoped = (type ) =>
|
|
3558
|
+
(CONNECTION_TYPES[type]?.settings ?? []).filter(s => s.scope === "connection").map(s => s.key);
|
|
3559
|
+
|
|
3560
|
+
/** Whether a stored setting is set: blank is the lab's default. */
|
|
3561
|
+
const setValue = (v ) => v != null && v !== "";
|
|
3562
|
+
|
|
3563
|
+
/**
|
|
3564
|
+
* A Target profile as an AI Connection and, where it set any setting, the
|
|
3565
|
+
* Connection Profile holding them -- the same id, stamped with its origin.
|
|
3566
|
+
* Resolved together (`mergeAnswer`) they send what the profile sent.
|
|
3567
|
+
* server.py's `split_profile` is its twin, held to it by proxy-check.py.
|
|
3568
|
+
*/
|
|
3569
|
+
function splitProfile(p ) {
|
|
3570
|
+
const scoped = connectionScoped(typeOf(p ));
|
|
3571
|
+
const connection = {}, settings = {};
|
|
3572
|
+
for (const [k, v] of Object.entries(p)) {
|
|
3573
|
+
if (AI_CONNECTION_FIELDS.includes(k) || scoped.includes(k)) connection[k] = v;
|
|
3574
|
+
else if (setValue(v)) settings[k] = v;
|
|
3575
|
+
}
|
|
3576
|
+
const profile = Object.keys(settings).length
|
|
3577
|
+
? { id: String(p.id ?? ""), name: String(p.name ?? ""), fromProfile: String(p.id ?? ""), ...settings } : null;
|
|
3578
|
+
return { connection, profile };
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
/** [conn] with [profile]'s settings over it: the one set of settings a
|
|
3582
|
+
request is built from, the shape a Target profile had. A profile sets
|
|
3583
|
+
no connection field. */
|
|
3584
|
+
function mergeAnswer(conn , profile ) {
|
|
3585
|
+
const out = { ...conn };
|
|
3586
|
+
const scoped = connectionScoped(typeOf(conn ));
|
|
3587
|
+
for (const [k, v] of Object.entries(profile ?? {})) {
|
|
3588
|
+
if (!CONNECTION_PROFILE_FIELDS.includes(k) && !AI_CONNECTION_FIELDS.includes(k) && !scoped.includes(k) && setValue(v)) out[k] = v;
|
|
3589
|
+
}
|
|
3590
|
+
return out;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
/** Whom a target or one of its steps asks: an AI Connection, and the
|
|
3594
|
+
Connection Profile over it, or none. */
|
|
3595
|
+
|
|
3596
|
+
|
|
3597
|
+
|
|
3598
|
+
|
|
3599
|
+
|
|
3600
|
+
/** The key a run's table holds [connection] under, with [profile] -- or with
|
|
3601
|
+
none, the lab's defaults. A grader names its connection bare, its id alone,
|
|
3602
|
+
so it never meets a target's pair (see `answersOf`). */
|
|
3603
|
+
const answerKey = (connection , profile ) => `${connection}.${profile ?? ""}`;
|
|
3604
|
+
|
|
3605
|
+
/** [key] read back: a pair's connection and profile, or a bare id. */
|
|
3606
|
+
function answerOfKey(key ) {
|
|
3607
|
+
const at = key.indexOf(".");
|
|
3608
|
+
if (at < 0) return { connection: key, profile: null, bare: true };
|
|
3609
|
+
return { connection: key.slice(0, at), profile: key.slice(at + 1) || null, bare: false };
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
/** Whom target [i] asks in job [k]: its step's own connection or profile,
|
|
3613
|
+
where it names either, else the target's. */
|
|
3614
|
+
function targetAnswerOf(doc , i , k ) {
|
|
3615
|
+
const t = targetsOf(doc)[i];
|
|
3616
|
+
const st = targetStepOf(doc, i, k);
|
|
3617
|
+
const own = st?.connection != null || st?.profile != null;
|
|
3618
|
+
const connection = (own ? st?.connection ?? t?.connection : t?.connection) ?? null;
|
|
3619
|
+
if (!isRef(connection)) return undefined;
|
|
3620
|
+
const profile = (own ? st?.profile : t?.profile) ?? null;
|
|
3621
|
+
return { connection, profile: isRef(profile) ? profile : null };
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
/** The answer key of target [i] in job [k], or undefined where it asks none. */
|
|
3625
|
+
function targetAnswerKey(doc , i , k ) {
|
|
3626
|
+
const a = targetAnswerOf(doc, i, k);
|
|
3627
|
+
return a ? answerKey(a.connection.id, a.profile?.id) : undefined;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
/**
|
|
3631
|
+
* What the lab's two stores answer: each by id, the profile split from a
|
|
3632
|
+
* Target profile's id (what an upgrade and a grader read it by), and an
|
|
3633
|
+
* answer key resolved to the settings it sends -- the profile's connection
|
|
3634
|
+
* where it names one, else the key's, with the profile's settings over it.
|
|
3635
|
+
* A grader's bare id resolves as the Target profile it named did: its
|
|
3636
|
+
* connection under the profile split from it. Null where any part is gone.
|
|
3637
|
+
*/
|
|
3638
|
+
function answersOf(connections , profiles ) {
|
|
3639
|
+
const connection = (id ) => connections.find(c => c?.id === id) ?? null;
|
|
3640
|
+
const profile = (id ) => profiles.find(p => p?.id === id) ?? null;
|
|
3641
|
+
const split = (id ) => {
|
|
3642
|
+
const p = profiles.find(x => x?.fromProfile === id);
|
|
3643
|
+
return p ? { id: p.id, name: p.name } : null;
|
|
3644
|
+
};
|
|
3645
|
+
const resolve = (key ) => {
|
|
3646
|
+
const at = answerOfKey(key);
|
|
3647
|
+
const pid = at.bare ? split(at.connection)?.id ?? null : at.profile;
|
|
3648
|
+
const p = pid ? profile(pid) : null;
|
|
3649
|
+
if (pid && !p) return null;
|
|
3650
|
+
const c = connection(isRef(p?.connection) ? p .connection .id : at.connection);
|
|
3651
|
+
return c ? mergeAnswer(c, p) : null;
|
|
3652
|
+
};
|
|
3653
|
+
return { connection, profile, split, resolve };
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
/** The connection a run carries for [key]: its own entry, or -- a run from
|
|
3657
|
+
before version 16, whose table is keyed by Target profile id, and names
|
|
3658
|
+
the connection alone once upgraded -- the one under the connection's id. */
|
|
3659
|
+
function runAnswer(run , key ) {
|
|
3660
|
+
const table = run?.profiles ;
|
|
3661
|
+
if (!isObj(table)) return undefined;
|
|
3662
|
+
if (Object.hasOwn(table, key)) return table[key];
|
|
3663
|
+
const at = answerOfKey(key);
|
|
3664
|
+
return !at.bare && at.profile == null && Object.hasOwn(table, at.connection) ? table[at.connection] : undefined;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
/** The id of the AI Connection whose key a run's entry [key] is sent with. */
|
|
3668
|
+
const answerConnectionId = (key , conn ) =>
|
|
3669
|
+
(isStr(conn?.connection) && conn .connection) || answerOfKey(key).connection;
|
|
3670
|
+
|
|
3444
3671
|
// The rest of what a connection's fields mean lives in the connection-type
|
|
3445
3672
|
// registry above (#46): the runner asks a type how to build a request and how
|
|
3446
3673
|
// to read a reply, and validation asks connectionProblems (below, with the run
|
|
@@ -3721,7 +3948,7 @@ DYNAMIC_PROVIDERS["command"] = { // provider: the Command adapter's own id
|
|
|
3721
3948
|
holds, so a tick costs a predicate and not a request. A reader names a field
|
|
3722
3949
|
it needs; a wizard that reads more declares it here. */
|
|
3723
3950
|
|
|
3724
|
-
|
|
3951
|
+
|
|
3725
3952
|
|
|
3726
3953
|
|
|
3727
3954
|
|
|
@@ -4091,14 +4318,14 @@ LEGACY_TESTS.metrics = {
|
|
|
4091
4318
|
if (t.grader != null && !isRef(t.grader)) bad.push("the Metrics name their grader as { id, name }");
|
|
4092
4319
|
// The eval's own grader, or the lab's.
|
|
4093
4320
|
const grader = isRef(t.grader) ? t.grader : isRef(ctx.grader) ? ctx.grader : null;
|
|
4094
|
-
if (gradedIn(t.metrics) && !grader) bad.push("a model-graded metric needs a grader: pick one on the eval, or make a
|
|
4321
|
+
if (gradedIn(t.metrics) && !grader) bad.push("a model-graded metric needs a grader: pick one on the eval, or make a connection the lab's grader");
|
|
4095
4322
|
// A grader is asked words, and needs a model to ask.
|
|
4096
4323
|
const p = grader && ctx.profiles ? ctx.profiles(grader.id) : null;
|
|
4097
|
-
if (grader && ctx.profiles && !p) bad.push(`
|
|
4324
|
+
if (grader && ctx.profiles && !p) bad.push(`Connection ${grader.name || grader.id} not found`);
|
|
4098
4325
|
else if (p) {
|
|
4099
4326
|
const type = CONNECTION_TYPES[typeOf(p )];
|
|
4100
4327
|
if (type && !(type.answers ?? ["prompt"]).includes("prompt")) bad.push(`the grader has to be asked words, and ${type.label} is not`);
|
|
4101
|
-
else if (!type?.local && !String((p ).model || "").trim()) bad.push("no model on the grader's
|
|
4328
|
+
else if (!type?.local && !String((p ).model || "").trim()) bad.push("no model on the grader's connection");
|
|
4102
4329
|
}
|
|
4103
4330
|
},
|
|
4104
4331
|
rules: t => (Array.isArray(t.metrics) ? t.metrics : []).map((m , x ) => ({
|
|
@@ -4226,14 +4453,14 @@ EVAL_TYPES.group = {
|
|
|
4226
4453
|
if (own.run.length && !wholeRunGroup(t)) bad.push("the group's own body reads each item or the whole run, not both");
|
|
4227
4454
|
// The group's own grader, or the lab's.
|
|
4228
4455
|
const grader = isRef(own.grader) ? own.grader : isRef(ctx.grader) ? ctx.grader : null;
|
|
4229
|
-
if (gradedIn(own.every) && !grader) bad.push("a model-graded metric needs a grader: pick one on the eval, or make a
|
|
4456
|
+
if (gradedIn(own.every) && !grader) bad.push("a model-graded metric needs a grader: pick one on the eval, or make a connection the lab's grader");
|
|
4230
4457
|
// A grader is asked words, and needs a model to ask.
|
|
4231
4458
|
const p = grader && ctx.profiles ? ctx.profiles(grader.id) : null;
|
|
4232
|
-
if (grader && ctx.profiles && !p) bad.push(`
|
|
4459
|
+
if (grader && ctx.profiles && !p) bad.push(`Connection ${grader.name || grader.id} not found`);
|
|
4233
4460
|
else if (p) {
|
|
4234
4461
|
const type = CONNECTION_TYPES[typeOf(p )];
|
|
4235
4462
|
if (type && !(type.answers ?? ["prompt"]).includes("prompt")) bad.push(`the grader has to be asked words, and ${type.label} is not`);
|
|
4236
|
-
else if (!type?.local && !String((p ).model || "").trim()) bad.push("no model on the grader's
|
|
4463
|
+
else if (!type?.local && !String((p ).model || "").trim()) bad.push("no model on the grader's connection");
|
|
4237
4464
|
}
|
|
4238
4465
|
},
|
|
4239
4466
|
rules: t => ownList(t).map((m , x ) => ({
|
|
@@ -4565,7 +4792,7 @@ STEP_TYPES.prompt = {
|
|
|
4565
4792
|
description: "Asks each target's model its prompt about the item.",
|
|
4566
4793
|
in: "item", out: "text",
|
|
4567
4794
|
apply: "runPipeline",
|
|
4568
|
-
fields: ["type", "prompt", "profile", "from"],
|
|
4795
|
+
fields: ["type", "prompt", "connection", "profile", "from"],
|
|
4569
4796
|
validate(step, _ctx, bad, at){
|
|
4570
4797
|
if (!isStr(step.prompt)) bad.push(`${at}: prompt has to be text`);
|
|
4571
4798
|
},
|
|
@@ -4597,7 +4824,7 @@ STEP_TYPES.httpRequest = {
|
|
|
4597
4824
|
...(HTTP_APIS[step.api] ?? HTTP_APIS.raw ).cellOf(step.body) }),
|
|
4598
4825
|
in: "item", out: "text",
|
|
4599
4826
|
apply: "runPipeline",
|
|
4600
|
-
fields: ["type", "api", "method", "path", "query", "body", "prompt", "profile", "from", "system", "model", "settings", "prefill"],
|
|
4827
|
+
fields: ["type", "api", "method", "path", "query", "body", "prompt", "connection", "profile", "from", "system", "model", "settings", "prefill"],
|
|
4601
4828
|
validate(step, _ctx, bad, at){
|
|
4602
4829
|
const api = HTTP_APIS[step.api];
|
|
4603
4830
|
if (!api) bad.push(`${at}: "${step.api}" is not an HTTP API this lab has`);
|
|
@@ -4608,7 +4835,7 @@ STEP_TYPES.httpRequest = {
|
|
|
4608
4835
|
// A key is the Setup profile's, sent by its Key header; one in the
|
|
4609
4836
|
// template would be kept in the pipeline and every run of it.
|
|
4610
4837
|
const hit = keyField(step.body) ?? keyField(step.query, "query");
|
|
4611
|
-
if (hit) bad.push(`${at} carries a key at ${hit}, and a key never goes in a pipeline -- the
|
|
4838
|
+
if (hit) bad.push(`${at} carries a key at ${hit}, and a key never goes in a pipeline -- the connection sends it`);
|
|
4612
4839
|
if (!api) return;
|
|
4613
4840
|
// Its fields, where its HTTP API has a place for them.
|
|
4614
4841
|
for (const f of ["system", "model", "prefill"] ) {
|
|
@@ -4723,7 +4950,7 @@ STEP_TYPES.modifier = {
|
|
|
4723
4950
|
|
|
4724
4951
|
// A filters step: the filter sets a job cleans and gates its reply by, each a
|
|
4725
4952
|
// link or a private set (#339, #405), at the step's `stage`. A job may hold
|
|
4726
|
-
// one of each stage -- a `text` one
|
|
4953
|
+
// one of each stage -- a `text` one Pre-format, an `items` one Post-format -- so
|
|
4727
4954
|
// the type is `many`; two of a stage is the job's to refuse. A set belongs to
|
|
4728
4955
|
// no stage (#432): linked here, it runs the filters whose type fits the step's
|
|
4729
4956
|
// stage and skips the rest. A private set's body is the core's to judge
|
|
@@ -4824,11 +5051,6 @@ function jobTargetType(doc
|
|
|
4824
5051
|
return targetStepOf(doc, 0, k)?.type ?? "prompt";
|
|
4825
5052
|
}
|
|
4826
5053
|
|
|
4827
|
-
/** The Setup profile target [i] asks in job [k]: its step's own, or the target's. */
|
|
4828
|
-
function targetProfileOf(doc , i , k ) {
|
|
4829
|
-
return targetStepOf(doc, i, k)?.profile ?? targetsOf(doc)[i]?.profile ?? undefined;
|
|
4830
|
-
}
|
|
4831
|
-
|
|
4832
5054
|
/** Whether target [i] is a Recorded target: it replays each call's recorded
|
|
4833
5055
|
reply rather than sending anything (its job-1 step answers locally with a
|
|
4834
5056
|
connection that `replays`). A recorded-reply metric compares the recorded
|
|
@@ -4958,8 +5180,8 @@ function withOut(job , out ) {
|
|
|
4958
5180
|
}
|
|
4959
5181
|
|
|
4960
5182
|
/** [job] reading a reply as `legacyTagsOut` builds it, on the current model
|
|
4961
|
-
(#405): its output a List, a
|
|
4962
|
-
the old `preamble` did, and
|
|
5183
|
+
(#405): its output a List, a Pre-format text set that reads the reply as
|
|
5184
|
+
the old `preamble` did, and a Post-format items set that cleans and gates
|
|
4963
5185
|
it. The filters are `legacyTagsOut`'s, placed as private sets -- what the
|
|
4964
5186
|
version-15 upgrade builds for a legacy list job, for a test to build
|
|
4965
5187
|
directly. */
|
|
@@ -4978,7 +5200,7 @@ const filterStepOf = (job , stage )
|
|
|
4978
5200
|
|
|
4979
5201
|
/** The filter-set links a job's Responses stage carries for [stage], in
|
|
4980
5202
|
document order (#339, #405): read from that stage's filters step, or none.
|
|
4981
|
-
Defaults to the
|
|
5203
|
+
Defaults to the Post-format (items) stage, the one that was there before. */
|
|
4982
5204
|
function filterSetsOf(job , stage = "items") {
|
|
4983
5205
|
return (filterStepOf(job, stage)?.filterSets ?? []).slice();
|
|
4984
5206
|
}
|
|
@@ -4998,10 +5220,10 @@ function withFilterSets(job , links , stage
|
|
|
4998
5220
|
const linkFilters = (link , resolve ) =>
|
|
4999
5221
|
(link.own ?? (link.set && resolve ? resolve(link.set) : null))?.filters || [];
|
|
5000
5222
|
|
|
5001
|
-
/** A job's
|
|
5223
|
+
/** A job's Post-format (items) Responses as an ordered modifier list for a
|
|
5002
5224
|
run (#339): each modifier step as it is, and each items filter-set link as a
|
|
5003
5225
|
synthetic modifier that applies the set's filters where the link sits. The
|
|
5004
|
-
|
|
5226
|
+
Pre-format (text) filters are not modifiers -- `beforeFilters` gives
|
|
5005
5227
|
them, and a run applies them to the raw reply before it is read. [resolve]
|
|
5006
5228
|
gives a Library link's body. */
|
|
5007
5229
|
function stageModifiers(job , resolve ) {
|
|
@@ -5016,7 +5238,7 @@ function stageModifiers(job , resolve
|
|
|
5016
5238
|
return out;
|
|
5017
5239
|
}
|
|
5018
5240
|
|
|
5019
|
-
/** A job's
|
|
5241
|
+
/** A job's Pre-format (text) filters for a run (#405), in document order:
|
|
5020
5242
|
the filters of every link on its `text` filters step, which a run applies to
|
|
5021
5243
|
the raw reply before the kind reads it into items. [resolve] gives a Library
|
|
5022
5244
|
link's body, as `stageModifiers` does. */
|
|
@@ -5067,11 +5289,11 @@ STEP_TYPES.job = {
|
|
|
5067
5289
|
}
|
|
5068
5290
|
const twice = steps.find((st, x) => !STEP_TYPES[st.type] .many && steps.findIndex(o => o.type === st.type) !== x);
|
|
5069
5291
|
if (twice) return void bad.push(`${at} has two ${STEP_TYPES[twice.type] .label ?? twice.type} steps`);
|
|
5070
|
-
// A `filters` step is `many`, but only one per stage: a
|
|
5071
|
-
//
|
|
5292
|
+
// A `filters` step is `many`, but only one per stage: a Pre-format and
|
|
5293
|
+
// a Post-format set at most (#405).
|
|
5072
5294
|
const twoFilters = steps.find((st, x) => st.type === "filters"
|
|
5073
5295
|
&& steps.findIndex(o => o.type === "filters" && o.stage === st.stage) !== x);
|
|
5074
|
-
if (twoFilters) return void bad.push(`${at} has two ${twoFilters.stage === "text" ? "
|
|
5296
|
+
if (twoFilters) return void bad.push(`${at} has two ${twoFilters.stage === "text" ? "Pre-format" : "Post-format"} filter steps`);
|
|
5075
5297
|
if (steps.some((st, x) => x > 0 && placeOf(st) < placeOf(steps[x - 1]))) {
|
|
5076
5298
|
return void bad.push(`${at}: steps are its Content, then its Responses, each in order`);
|
|
5077
5299
|
}
|
|
@@ -5173,10 +5395,16 @@ function versionProblem(doc ) {
|
|
|
5173
5395
|
rules, as a run with no graded eval parsed. */
|
|
5174
5396
|
|
|
5175
5397
|
|
|
5176
|
-
|
|
5398
|
+
|
|
5177
5399
|
|
|
5178
|
-
|
|
5400
|
+
|
|
5401
|
+
|
|
5179
5402
|
|
|
5403
|
+
|
|
5404
|
+
|
|
5405
|
+
|
|
5406
|
+
|
|
5407
|
+
|
|
5180
5408
|
|
|
5181
5409
|
|
|
5182
5410
|
/**
|
|
@@ -5227,8 +5455,10 @@ function columnsOf(doc )
|
|
|
5227
5455
|
function profileRefs(doc ) {
|
|
5228
5456
|
const cut = (r ) => (isObj(r) && isStr(r.id) ? { id: r.id, name: isStr(r.name) ? r.name : "" } : r);
|
|
5229
5457
|
for (const { column, steps } of columnsOf(doc)) {
|
|
5230
|
-
|
|
5231
|
-
|
|
5458
|
+
for (const f of ["connection", "profile"]) {
|
|
5459
|
+
if (f in column) column[f] = cut(column[f]);
|
|
5460
|
+
for (const st of steps) if (st[f] != null) st[f] = cut(st[f]);
|
|
5461
|
+
}
|
|
5232
5462
|
}
|
|
5233
5463
|
}
|
|
5234
5464
|
|
|
@@ -5238,7 +5468,40 @@ const cutRefs = (doc ) => { profileRefs(doc); return doc; }
|
|
|
5238
5468
|
/** Whether any profile reference in a pipeline holds more than { id, name }. */
|
|
5239
5469
|
function fatProfileRef(doc ) {
|
|
5240
5470
|
const fat = (r ) => isObj(r) && Object.keys(r).some(k => k !== "id" && k !== "name");
|
|
5241
|
-
return isObj(doc) && columnsOf(doc).some(({ column, steps }) => fat(column.profile) ||
|
|
5471
|
+
return isObj(doc) && columnsOf(doc).some(({ column, steps }) => fat(column.profile) || fat(column.connection)
|
|
5472
|
+
|| steps.some(st => fat(st.profile) || fat(st.connection)));
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
/**
|
|
5476
|
+
* Version 15 to 16: a Target profile is an AI Connection now, keeping the
|
|
5477
|
+
* profile's id, and -- where the profile set settings -- the Connection
|
|
5478
|
+
* Profile split from it ([ctx]'s `connectionSplit`), so each target and step
|
|
5479
|
+
* asks what it asked. A run document carries each profile resolved, under
|
|
5480
|
+
* its id (`runAnswer` reads it so), so it names the connection alone, as
|
|
5481
|
+
* does a reader that cannot say what was split. Idempotent: a target that
|
|
5482
|
+
* names a connection already is left as it is.
|
|
5483
|
+
*/
|
|
5484
|
+
function answersFromProfiles(doc , ctx ) {
|
|
5485
|
+
doc.version = PIPELINE_VERSION;
|
|
5486
|
+
const split = (ref ) => (isObj(doc.profiles) || !isObj(ref) || !isStr(ref.id) ? null
|
|
5487
|
+
: ctx.connectionSplit?.(ref.id) ?? null);
|
|
5488
|
+
if (!Array.isArray(doc.targets)) return doc;
|
|
5489
|
+
doc.targets = doc.targets.map((t ) => {
|
|
5490
|
+
if (!isObj(t) || "connection" in t) return t;
|
|
5491
|
+
const { id, name, profile = null, steps, ...rest } = t;
|
|
5492
|
+
const p = split(profile);
|
|
5493
|
+
const out = { id, name, connection: profile, profile: p ? { id: p.id, name: p.name ?? "" } : null, steps, ...rest };
|
|
5494
|
+
if (Array.isArray(steps)) {
|
|
5495
|
+
out.steps = steps.map((st ) => {
|
|
5496
|
+
if (!isObj(st) || st.profile == null || "connection" in st) return st;
|
|
5497
|
+
const { profile: own, ...more } = st;
|
|
5498
|
+
const sp = split(own);
|
|
5499
|
+
return { ...more, connection: own, ...(sp ? { profile: { id: sp.id, name: sp.name ?? "" } } : {}) };
|
|
5500
|
+
});
|
|
5501
|
+
}
|
|
5502
|
+
return out;
|
|
5503
|
+
});
|
|
5504
|
+
return doc;
|
|
5242
5505
|
}
|
|
5243
5506
|
|
|
5244
5507
|
/** Version 5 to 6: the one test, or none, as a list. Its id is `t1`, so
|
|
@@ -5443,32 +5706,34 @@ function upgradePipeline (doc , ctx = {}) {
|
|
|
5443
5706
|
}
|
|
5444
5707
|
return localSteps(ownBodiesNow(out), ctx) ;
|
|
5445
5708
|
}
|
|
5446
|
-
if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14].includes(doc.version )) return doc;
|
|
5709
|
+
if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes(doc.version )) return doc;
|
|
5710
|
+
// Every version reads as 15 first, then as 16: its Target profiles split
|
|
5711
|
+
// into AI Connections and Connection Profiles (answersFromProfiles).
|
|
5712
|
+
const sixteen = (fifteen ) => localSteps(ownBodiesNow(answersFromProfiles(fifteen, ctx)), ctx) ;
|
|
5713
|
+
if (doc.version === 15) return sixteen(cutRefs(clone(doc) ));
|
|
5447
5714
|
// Versions 13 and 14 read as 15 by migrating a job's cleaning modifier steps
|
|
5448
|
-
// and its List `preamble` setting into
|
|
5715
|
+
// and its List `preamble` setting into Pre-/Post-format filter sets
|
|
5449
5716
|
// (#339, #405). A version-14 document already moved its Drop items / Reject
|
|
5450
5717
|
// into a (stageless) filters step; filtersFromCleaning stages it and folds in
|
|
5451
5718
|
// the remaining Tidy / Remove duplicates / Keep first / Case.
|
|
5452
|
-
if (doc.version === 13 || doc.version === 14)
|
|
5453
|
-
return localSteps(ownBodiesNow(filtersFromCleaning(clone(doc) )), ctx) ;
|
|
5454
|
-
}
|
|
5719
|
+
if (doc.version === 13 || doc.version === 14) return sixteen(filtersFromCleaning(clone(doc) ));
|
|
5455
5720
|
if (doc.version === 11 || doc.version === 12) {
|
|
5456
|
-
return
|
|
5721
|
+
return sixteen(filtersFromCleaning(groupsOf(withoutCaseMetric(caseAsWritten(cutRefs(clone(doc) ))))));
|
|
5457
5722
|
}
|
|
5458
5723
|
// Every version before 10 reads as version 9 first, then as 10, then up
|
|
5459
5724
|
// through 15. A version-10 document is cut as a current one was
|
|
5460
5725
|
// (profileRefs); the earlier ones are cut on their way through nineOf.
|
|
5461
5726
|
const ten = doc.version === 10 ? cutRefs(clone(doc) ) : targetsFromScenarios(nineOf(clone(doc) , ctx));
|
|
5462
|
-
return
|
|
5727
|
+
return sixteen(filtersFromCleaning(groupsOf(withoutCaseMetric(caseAsWritten(evalsKey(ten))))));
|
|
5463
5728
|
}
|
|
5464
5729
|
|
|
5465
5730
|
/** [doc] with every job's cleaning modifier steps and its List `preamble`
|
|
5466
5731
|
setting migrated into filter sets on `filters` steps, up to version 15
|
|
5467
|
-
(#339, #405). The
|
|
5732
|
+
(#339, #405). The Post-format (items) filters are built by walking the
|
|
5468
5733
|
Responses steps in document order -- each cleaning modifier its filters
|
|
5469
5734
|
(`cleaningFilters`), each existing filters step's links in place, a private
|
|
5470
5735
|
one's filters coalescing with adjacent modifier filters into one own set --
|
|
5471
|
-
and the `preamble: true` setting becomes a
|
|
5736
|
+
and the `preamble: true` setting becomes a Pre-format (text) set of
|
|
5472
5737
|
Remove thinking then Remove preamble. The steps take the place of the first
|
|
5473
5738
|
of them, and the preamble setting is stripped.
|
|
5474
5739
|
|
|
@@ -5556,12 +5821,20 @@ function withoutCaseMetric(doc ) {
|
|
|
5556
5821
|
step then asks none, or names its own, keeps no profile either. The same
|
|
5557
5822
|
document, untouched, when there is nothing to read so. */
|
|
5558
5823
|
function localSteps(doc , ctx ) {
|
|
5559
|
-
const
|
|
5560
|
-
|
|
5561
|
-
|
|
5824
|
+
const run = isObj(doc.profiles);
|
|
5825
|
+
if (!ctx.profileType && !run) return doc;
|
|
5826
|
+
// A connection's type: [ctx]'s, by its id, or what a run carries for it.
|
|
5827
|
+
const typeOfAnswer = (connection , profile ) => {
|
|
5828
|
+
if (!isObj(connection) || !isStr(connection.id)) return undefined;
|
|
5829
|
+
if (ctx.profileType) return ctx.profileType(connection.id);
|
|
5830
|
+
const conn = runAnswer(doc, answerKey(connection.id, isObj(profile) && isStr(profile.id) ? profile.id : null));
|
|
5831
|
+
return isObj(conn) ? conn.type : undefined;
|
|
5832
|
+
};
|
|
5562
5833
|
const stands = new Map(Object.entries(STEP_TYPES).filter(([, e]) => e.slot === "target" && e.replaces).map(([type, e]) => [e.replaces , type]));
|
|
5563
|
-
const as = (
|
|
5564
|
-
const
|
|
5834
|
+
const as = (connection , profile ) => stands.get(typeOfAnswer(connection, profile) ?? "");
|
|
5835
|
+
const ownsAnswer = (st ) => st.connection != null || st.profile != null;
|
|
5836
|
+
const turns = (t , st ) => isObj(st) && STEP_TYPES[st.type]?.asks === "prompt"
|
|
5837
|
+
&& (ownsAnswer(st) ? as(st.connection ?? t.connection, st.profile) : as(t.connection, t.profile));
|
|
5565
5838
|
const targets = Array.isArray(doc.targets) ? doc.targets : [];
|
|
5566
5839
|
if (!targets.some(t => isObj(t) && Array.isArray(t.steps) && t.steps.some((st ) => turns(t, st)))) return doc;
|
|
5567
5840
|
const next = clone(doc) ;
|
|
@@ -5571,9 +5844,12 @@ function localSteps(doc , ctx ) {
|
|
|
5571
5844
|
const type = turns(t, st);
|
|
5572
5845
|
return type ? { type, prompt: st.prompt, ...(st.from ? { from: st.from } : {}) } : st;
|
|
5573
5846
|
});
|
|
5574
|
-
// A run keeps the
|
|
5575
|
-
if (!
|
|
5576
|
-
&& t.steps.every((st ) => isObj(st) && (STEP_TYPES[st.type]?.local || st
|
|
5847
|
+
// A run keeps the connection it ran as, so its report names it as it did.
|
|
5848
|
+
if (!run && as(t.connection, t.profile)
|
|
5849
|
+
&& t.steps.every((st ) => isObj(st) && (STEP_TYPES[st.type]?.local || ownsAnswer(st)))) {
|
|
5850
|
+
t.connection = null;
|
|
5851
|
+
t.profile = null;
|
|
5852
|
+
}
|
|
5577
5853
|
}
|
|
5578
5854
|
return next;
|
|
5579
5855
|
}
|
|
@@ -5633,6 +5909,16 @@ function blankPipeline(opts
|
|
|
5633
5909
|
jobs: [job], targets: [], evals: [], pass: { mode: "all" } };
|
|
5634
5910
|
}
|
|
5635
5911
|
|
|
5912
|
+
/** What is wrong with how a target or a step [at] names whom it asks: each
|
|
5913
|
+
reference as { id, name }, and a profile only over a connection. */
|
|
5914
|
+
function answerRefsProblems(o , at , bad ) {
|
|
5915
|
+
for (const [f, what] of [["connection", "AI Connection"], ["profile", "Connection Profile"]] ) {
|
|
5916
|
+
if (o[f] == null) continue;
|
|
5917
|
+
if (!isRef(o[f])) bad.push(`${at} has to name its ${what} as { id, name }`);
|
|
5918
|
+
else onlyFields(o[f], `${at}'s ${f}`, ["id", "name"], bad);
|
|
5919
|
+
}
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5636
5922
|
/**
|
|
5637
5923
|
* Every reason [doc] cannot run, as sentences -- none when it can. The
|
|
5638
5924
|
* reference lookups are [ctx]'s: `profiles(id)` and `sources(id)` answer with
|
|
@@ -5645,9 +5931,7 @@ function validatePipeline(input , ctx = {}) {
|
|
|
5645
5931
|
const bad = [];
|
|
5646
5932
|
const run = doc.profiles !== undefined;
|
|
5647
5933
|
const c = { ...ctx, run };
|
|
5648
|
-
if (run && !ctx.profiles)
|
|
5649
|
-
c.profiles = (id ) => isObj(doc.profiles) && Object.hasOwn(doc.profiles, id) ? doc.profiles[id] : null;
|
|
5650
|
-
}
|
|
5934
|
+
if (run && !ctx.profiles) c.profiles = (key ) => (runAnswer(doc, key) ) ?? null;
|
|
5651
5935
|
onlyFields(doc, "the pipeline", run ? RUN_FIELDS : PIPELINE_FIELDS, bad);
|
|
5652
5936
|
if (!isStr(doc.id) || !doc.id.trim()) bad.push("the pipeline has no id");
|
|
5653
5937
|
if (doc.name != null && !isStr(doc.name)) bad.push("name has to be text");
|
|
@@ -5668,32 +5952,31 @@ function validatePipeline(input , ctx = {}) {
|
|
|
5668
5952
|
targets.forEach((t, i) => {
|
|
5669
5953
|
const at = targetLabel(doc, i);
|
|
5670
5954
|
if (!isObj(t)) return void bad.push(`${at} has to be an object`);
|
|
5671
|
-
onlyFields(t, at, ["id", "name", "profile", "steps"], bad);
|
|
5955
|
+
onlyFields(t, at, ["id", "name", "connection", "profile", "steps"], bad);
|
|
5672
5956
|
if (!isStr(t.id) || !t.id.trim()) bad.push(`${at} has no id`);
|
|
5673
5957
|
else if (targets.findIndex((o) => isObj(o) && o.id === t.id) !== i) bad.push(`${at} has the id of another target`);
|
|
5674
5958
|
if (t.name != null && !isStr(t.name)) bad.push(`${at}: name has to be text`);
|
|
5675
|
-
|
|
5676
|
-
else if (t.profile != null) onlyFields(t.profile, `${at}'s profile`, ["id", "name"], bad);
|
|
5959
|
+
answerRefsProblems(t, at, bad);
|
|
5677
5960
|
if (!Array.isArray(t.steps) || t.steps.length !== n) {
|
|
5678
5961
|
bad.push(`${at} has ${Array.isArray(t.steps) ? t.steps.length : "no"} prompts for ${n} job${n === 1 ? "" : "s"}`);
|
|
5679
5962
|
return;
|
|
5680
5963
|
}
|
|
5681
|
-
// A target with no
|
|
5682
|
-
// each name theirs.
|
|
5683
|
-
|
|
5684
|
-
|
|
5964
|
+
// A target with no connection of its own is one whose steps need none,
|
|
5965
|
+
// or each name theirs; a profile needs a connection to be over.
|
|
5966
|
+
const ownsOne = (st ) => isObj(st) && st.connection != null;
|
|
5967
|
+
if (t.connection == null && t.steps.some((st ) => !(isObj(st) && STEP_TYPES[st.type]?.local) && !ownsOne(st))) {
|
|
5968
|
+
bad.push(`${at} has to name its AI Connection as { id, name }`);
|
|
5685
5969
|
}
|
|
5686
5970
|
t.steps.forEach((st , k ) => {
|
|
5687
5971
|
const where = `${at}, ${jobLabel(doc, k)}`;
|
|
5688
5972
|
if (!isObj(st)) return void bad.push(`${where} has to be an object`);
|
|
5689
5973
|
const entry = STEP_TYPES[st.type];
|
|
5690
5974
|
if (entry?.slot !== "target") return void bad.push(`${where}: "${st.type}" is not something a target can send`);
|
|
5691
|
-
if (entry.local && st.profile != null) return void bad.push(`${where}: ${entry.label} asks no
|
|
5975
|
+
if (entry.local && (st.profile != null || st.connection != null)) return void bad.push(`${where}: ${entry.label} asks no connection`);
|
|
5692
5976
|
if (k > 0 && entry.firstJobOnly) return void bad.push(`${where}: ${entry.firstJobOnly}`);
|
|
5693
5977
|
onlyFields(st, where, entry.fields ?? [], bad);
|
|
5694
5978
|
entry.validate(st, c, bad, where);
|
|
5695
|
-
|
|
5696
|
-
else if (st.profile != null) onlyFields(st.profile, `${where}'s profile`, ["id", "name"], bad);
|
|
5979
|
+
answerRefsProblems(st, where, bad);
|
|
5697
5980
|
if (st.from != null && !isRef(st.from)) bad.push(`${where} has to name the prompt it was picked from as { id, name }`);
|
|
5698
5981
|
// A request is a flow step's, rebuilt from its records, and its
|
|
5699
5982
|
// template has no place for an image.
|
|
@@ -5703,13 +5986,16 @@ function validatePipeline(input , ctx = {}) {
|
|
|
5703
5986
|
});
|
|
5704
5987
|
if (bad.length) return bad;
|
|
5705
5988
|
|
|
5706
|
-
// A reference that resolves to nothing, once per reference
|
|
5989
|
+
// A reference that resolves to nothing, once per reference: the part
|
|
5990
|
+
// that is gone, where the lookups can say which.
|
|
5707
5991
|
const missing = new Set ();
|
|
5708
|
-
const lookup = (
|
|
5709
|
-
const
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5992
|
+
const lookup = (a ) => {
|
|
5993
|
+
const key = answerKey(a.connection.id, a.profile?.id);
|
|
5994
|
+
const p = c.profiles ? c.profiles(key) : {};
|
|
5995
|
+
if (!p && !missing.has(key)) {
|
|
5996
|
+
missing.add(key);
|
|
5997
|
+
const gone = a.profile && c.connections?.(a.connection.id) ? a.profile : a.connection;
|
|
5998
|
+
bad.push(`${gone === a.profile ? "Connection Profile" : "Connection"} ${gone.name || gone.id} not found`);
|
|
5713
5999
|
}
|
|
5714
6000
|
return p;
|
|
5715
6001
|
};
|
|
@@ -5725,7 +6011,7 @@ function validatePipeline(input , ctx = {}) {
|
|
|
5725
6011
|
// What target [i] asks in job [k]: Echo's own connection for a step
|
|
5726
6012
|
// answered here, else its profile as the lookups hold it.
|
|
5727
6013
|
const askedOf = (i , k ) => (targetEntryOf(doc, i, k)?.local ? localConnOf(targetEntryOf(doc, i, k))
|
|
5728
|
-
: c.profiles ? c.profiles(
|
|
6014
|
+
: c.profiles ? c.profiles(targetAnswerKey(doc, i, k) ) : null);
|
|
5729
6015
|
const itemsHaveText = !!content && !CONTENT_TYPES[content.type]?.bare;
|
|
5730
6016
|
for (let k = 0; k < n; k++) {
|
|
5731
6017
|
const i = targets.findIndex((_, i) => {
|
|
@@ -5746,7 +6032,7 @@ function validatePipeline(input , ctx = {}) {
|
|
|
5746
6032
|
const nonText = (contentFiles || []).map(f => String((f )?.name ?? f))
|
|
5747
6033
|
.find(n => !/\.(?:txt|md|csv)$/i.test(n));
|
|
5748
6034
|
targets.forEach((_, i) => {
|
|
5749
|
-
const conns = doc.jobs.map((_ , k ) => (targetEntryOf(doc, i, k)?.local ? localConnOf(targetEntryOf(doc, i, k)) : lookup(
|
|
6035
|
+
const conns = doc.jobs.map((_ , k ) => (targetEntryOf(doc, i, k)?.local ? localConnOf(targetEntryOf(doc, i, k)) : lookup(targetAnswerOf(doc, i, k) )));
|
|
5750
6036
|
conns.forEach((p , k ) => {
|
|
5751
6037
|
if (!local(p)) return;
|
|
5752
6038
|
const entry = CONNECTION_TYPES[typeOf(p )] ;
|
|
@@ -5771,8 +6057,8 @@ function validatePipeline(input , ctx = {}) {
|
|
|
5771
6057
|
&& targetEntryOf(doc, i, k)?.modelFrom !== "step" && !String(p.model || "").trim());
|
|
5772
6058
|
if (bare >= 0) {
|
|
5773
6059
|
bad.push(n > 1
|
|
5774
|
-
? `no model on the
|
|
5775
|
-
: `no model on ${targetLabel(doc, i)}'s
|
|
6060
|
+
? `no model on the connection ${jobLabel(doc, bare)} of ${targetLabel(doc, i)} uses — manage connections on the Setup tab`
|
|
6061
|
+
: `no model on ${targetLabel(doc, i)}'s connection — manage connections on the Setup tab`);
|
|
5776
6062
|
}
|
|
5777
6063
|
});
|
|
5778
6064
|
STEP_TYPES.evals .validate(doc.evals, c, bad, outOf(doc.jobs.at(-1)).kind, doc);
|
|
@@ -5841,15 +6127,22 @@ function pipelineWarnings(doc , ctx = {}) {
|
|
|
5841
6127
|
/** A run document's profiles table: id → connection, keyless, spellable. */
|
|
5842
6128
|
function profilesProblems(profiles , bad ) {
|
|
5843
6129
|
if (!isObj(profiles)) return void bad.push("profiles has to be an object of id → connection");
|
|
6130
|
+
// Two entries may share one connection, and so one key; two connections
|
|
6131
|
+
// spelling one variable would hand one the other's key.
|
|
5844
6132
|
const spelt = new Map ();
|
|
5845
6133
|
for (const [id, conn] of Object.entries(profiles)) {
|
|
5846
|
-
if (!
|
|
6134
|
+
if (!ANSWER_KEY.test(id)) {
|
|
5847
6135
|
bad.push(`profile id "${id}" has to be letters and digits, with at most one hyphen: its key comes from EVALSLAB_API_KEY_<ID>`);
|
|
5848
6136
|
continue;
|
|
5849
6137
|
}
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
6138
|
+
if (isObj(conn) && conn.connection != null && !(isStr(conn.connection) && PROFILE_ID.test(conn.connection))) {
|
|
6139
|
+
bad.push(`profile ${id}: connection has to be an AI Connection's id`);
|
|
6140
|
+
continue;
|
|
6141
|
+
}
|
|
6142
|
+
const cid = answerConnectionId(id, isObj(conn) ? conn : null);
|
|
6143
|
+
const from = keyVar(cid, isObj(conn) && isStr(conn.slug) ? conn.slug : null);
|
|
6144
|
+
if (spelt.has(from) && spelt.get(from) !== cid) bad.push(`profiles ${spelt.get(from)} and ${cid} both take their key from $${from}`);
|
|
6145
|
+
spelt.set(from, cid);
|
|
5853
6146
|
const at = `profile ${id}`;
|
|
5854
6147
|
if (!isObj(conn)) { bad.push(`${at} has to be an object`); continue; }
|
|
5855
6148
|
connectionProblems(conn, at, from, bad);
|
|
@@ -5915,24 +6208,34 @@ function connectionProblems(conn , at , from
|
|
|
5915
6208
|
for (const why of CONNECTION_TYPES[type]?.settingsProblems?.(connectionSettings(conn)) ?? []) bad.push(`${at} ${why}`);
|
|
5916
6209
|
}
|
|
5917
6210
|
|
|
5918
|
-
/** The
|
|
6211
|
+
/** The AI Connection ids a pipeline asks -- each target's and step's, then
|
|
6212
|
+
each grader's -- in order: the Target profile ids, before version 16. */
|
|
5919
6213
|
function profileIds(doc ) {
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
6214
|
+
return [...new Set(answerKeys(doc).map(k => answerOfKey(k).connection))];
|
|
6215
|
+
}
|
|
6216
|
+
|
|
6217
|
+
/** Every answer key a pipeline asks, targets first, in order: each target's
|
|
6218
|
+
and step's connection and profile, then each grader's bare id. What a run
|
|
6219
|
+
carries one connection for. */
|
|
6220
|
+
function answerKeys(doc ) {
|
|
6221
|
+
const keys = [];
|
|
6222
|
+
const add = (key ) => { if (key && !keys.includes(key)) keys.push(key); };
|
|
6223
|
+
targetsOf(doc).forEach((t, i) => {
|
|
6224
|
+
// The target's own, which History reads it by, then each step's.
|
|
6225
|
+
if (isRef(t.connection)) add(answerKey(t.connection.id, isRef(t.profile) ? t.profile.id : null));
|
|
6226
|
+
(Array.isArray(t.steps) ? t.steps : []).forEach((_, k) => add(targetAnswerKey(doc, i, k)));
|
|
6227
|
+
});
|
|
5926
6228
|
// Then the ones an eval asks (a grader), so a run carries them too.
|
|
5927
6229
|
for (const t of evalsOf(doc)) {
|
|
5928
|
-
for (const ref of EVAL_TYPES[t.type]?.profiles?.(t) ?? []) if (ref?.id && !
|
|
6230
|
+
for (const ref of EVAL_TYPES[t.type]?.profiles?.(t) ?? []) if (ref?.id && !keys.includes(ref.id)) keys.push(ref.id);
|
|
5929
6231
|
}
|
|
5930
|
-
return
|
|
6232
|
+
return keys;
|
|
5931
6233
|
}
|
|
5932
6234
|
|
|
5933
6235
|
/**
|
|
5934
6236
|
* The run document: [doc] plus what its references resolved to, once, at
|
|
5935
|
-
* submit. `ctx.profiles(
|
|
6237
|
+
* submit. `ctx.profiles(key)` answers an answer key with what it resolves to
|
|
6238
|
+
* (`answersOf`'s `resolve`), `ctx.files` is
|
|
5936
6239
|
* the Source's file list as it reads now, `ctx.comment` is the run's own.
|
|
5937
6240
|
*/
|
|
5938
6241
|
function resolvePipeline(doc ,
|
|
@@ -5942,10 +6245,16 @@ function resolvePipeline(doc ,
|
|
|
5942
6245
|
// What the lab supplies an eval -- its grader -- before the profiles it
|
|
5943
6246
|
// asks are carried.
|
|
5944
6247
|
run.evals = (Array.isArray(run.evals) ? run.evals : []).map((t ) => EVAL_TYPES[t?.type]?.resolve?.(t, ctx) ?? t);
|
|
6248
|
+
// Each pair asked, resolved and frozen on its own: two targets sharing a
|
|
6249
|
+
// connection under different profiles send different settings. The entry
|
|
6250
|
+
// names the connection whose key it is sent with.
|
|
5945
6251
|
run.profiles = {};
|
|
5946
|
-
for (const
|
|
5947
|
-
const p = ctx.profiles?.(
|
|
5948
|
-
if (p)
|
|
6252
|
+
for (const key of answerKeys(run)) {
|
|
6253
|
+
const p = ctx.profiles?.(key);
|
|
6254
|
+
if (!p) continue;
|
|
6255
|
+
const conn = connectionOf(p );
|
|
6256
|
+
const cid = isStr(p.id) && p.id ? p.id : answerOfKey(key).connection;
|
|
6257
|
+
run.profiles[key] = { ...conn, connection: cid };
|
|
5949
6258
|
}
|
|
5950
6259
|
const content = contentOf(run) ;
|
|
5951
6260
|
if (content?.type === "source") content.files = CONTENT_TYPES.source .expand (content, ctx.files);
|
|
@@ -6008,13 +6317,14 @@ function pipelineToYaml(doc , ctx
|
|
|
6008
6317
|
return yaml.dump(out, { lineWidth: -1, noRefs: true });
|
|
6009
6318
|
}
|
|
6010
6319
|
|
|
6011
|
-
/** Every
|
|
6012
|
-
steps', each eval's grader -- replaced, in place, by [f] of it.
|
|
6320
|
+
/** Every AI Connection reference in [doc] -- each target's, each of its
|
|
6321
|
+
steps', each eval's grader -- replaced, in place, by [f] of it. A
|
|
6322
|
+
Connection Profile is named by id and name, and left as it is. */
|
|
6013
6323
|
function eachProfileRef(doc , f ) {
|
|
6014
6324
|
for (const t of targetsOf(doc)) {
|
|
6015
|
-
if (t.
|
|
6325
|
+
if (t.connection) t.connection = f(t.connection);
|
|
6016
6326
|
for (const st of Array.isArray(t.steps) ? t.steps : []) {
|
|
6017
|
-
if (st?.
|
|
6327
|
+
if (st?.connection) st.connection = f(st.connection);
|
|
6018
6328
|
}
|
|
6019
6329
|
}
|
|
6020
6330
|
for (const t of Array.isArray(doc.evals) ? doc.evals : []) {
|
|
@@ -6035,6 +6345,9 @@ function eachProfileRef(doc , f )
|
|
|
6035
6345
|
* stands.
|
|
6036
6346
|
*/
|
|
6037
6347
|
function importPipeline(input , ctx = {}) {
|
|
6348
|
+
// A version-15 file names its profiles by slug, which the upgrade cannot
|
|
6349
|
+
// split (it needs this lab's id), so the split is asked again once remapped.
|
|
6350
|
+
const preSplit = isObj(input) && typeof input.version === "number" && input.version < 16 && !isObj(input.profiles);
|
|
6038
6351
|
// A file exported before version 2 still imports: it is upgraded, not refused.
|
|
6039
6352
|
const doc = upgradePipeline(input, ctx);
|
|
6040
6353
|
const v = versionProblem(doc);
|
|
@@ -6042,9 +6355,10 @@ function importPipeline(input , ctx = {}) {
|
|
|
6042
6355
|
const profiles = ctx.profiles ?? [], sources = ctx.sources ?? [], groups = ctx.groups ?? [];
|
|
6043
6356
|
const next = clone(doc) ;
|
|
6044
6357
|
const missing = [], seen = new Set ();
|
|
6045
|
-
const lists = { profile: profiles, source: sources, group: groups };
|
|
6358
|
+
const lists = { profile: profiles, connectionProfile: ctx.connectionProfiles ?? [], source: sources, group: groups };
|
|
6046
6359
|
const words = {
|
|
6047
|
-
profile: (ref ) => `
|
|
6360
|
+
profile: (ref ) => `Connection ${ref.slug || ref.name || ref.id} not found`,
|
|
6361
|
+
connectionProfile: (ref ) => `Connection Profile ${ref.name || ref.id} not found`,
|
|
6048
6362
|
source: (ref ) => `Source ${ref.name || ref.id} not found`,
|
|
6049
6363
|
group: (ref ) => `Eval group ${ref.name || ref.id} not found`,
|
|
6050
6364
|
};
|
|
@@ -6070,10 +6384,22 @@ function importPipeline(input , ctx = {}) {
|
|
|
6070
6384
|
};
|
|
6071
6385
|
const content = contentOf(next) ;
|
|
6072
6386
|
if (content?.type === "source") content.ref = remap(content.ref, "source");
|
|
6387
|
+
const split = (connection ) => {
|
|
6388
|
+
const p = preSplit && isRef(connection) && profiles.some(x => x.id === connection.id) ? ctx.connectionSplit?.(connection.id) : null;
|
|
6389
|
+
return p ? { id: p.id, name: p.name ?? "" } : null;
|
|
6390
|
+
};
|
|
6073
6391
|
for (const t of targetsOf(next)) {
|
|
6074
|
-
|
|
6392
|
+
// A profile is matched where the lab says which it holds.
|
|
6393
|
+
if (t.connection) t.connection = remapProfile(t.connection);
|
|
6394
|
+
if (isRef(t.profile) && ctx.connectionProfiles) t.profile = remap(t.profile, "connectionProfile");
|
|
6395
|
+
else if (t.profile == null) t.profile = split(t.connection);
|
|
6075
6396
|
for (const st of Array.isArray(t.steps) ? t.steps : []) {
|
|
6076
|
-
if (st?.
|
|
6397
|
+
if (st?.connection) st.connection = remapProfile(st.connection);
|
|
6398
|
+
if (isRef(st?.profile) && ctx.connectionProfiles) st.profile = remap(st.profile, "connectionProfile");
|
|
6399
|
+
else if (isObj(st) && st.connection && st.profile == null) {
|
|
6400
|
+
const sp = split(st.connection);
|
|
6401
|
+
if (sp) st.profile = sp;
|
|
6402
|
+
}
|
|
6077
6403
|
}
|
|
6078
6404
|
}
|
|
6079
6405
|
for (const t of Array.isArray(next.evals) ? next.evals : []) {
|
|
@@ -6173,7 +6499,7 @@ function junitXml(suites , runner ) {
|
|
|
6173
6499
|
//
|
|
6174
6500
|
// Export for CI writes a pipeline as files a repository keeps and
|
|
6175
6501
|
// `evals-lab run` runs with no lab around it (docs/pipeline-yaml.md, "Export
|
|
6176
|
-
// for CI"): the pipeline's YAML, its
|
|
6502
|
+
// for CI"): the pipeline's YAML, its connections resolved and keyed by slug, and each
|
|
6177
6503
|
// dataset it grades against as the Library's export of it. The server adds
|
|
6178
6504
|
// the plugins and, when asked, the Source's items, and zips the lot. Read
|
|
6179
6505
|
// back, the files are the run document the page would have submitted, with
|
|
@@ -6196,28 +6522,38 @@ const BUNDLE_DATASET = /^datasets\/([a-z0-9]+(?:-[a-z0-9]+)*)\.json$/;
|
|
|
6196
6522
|
|
|
6197
6523
|
/**
|
|
6198
6524
|
* [doc] as a bundle's text files: `pipeline.yaml`, `profiles.yaml` and
|
|
6199
|
-
* `datasets/<slug>.json`. [ctx.
|
|
6200
|
-
* none of which is written --
|
|
6201
|
-
*
|
|
6202
|
-
*
|
|
6203
|
-
*
|
|
6525
|
+
* `datasets/<slug>.json`. [ctx.connections] and [ctx.connectionProfiles]
|
|
6526
|
+
* are Setup's -- keys and all, none of which is written -- or
|
|
6527
|
+
* [ctx.profiles], Target profiles to split as the store was; [ctx.grader]
|
|
6528
|
+
* the lab's grader, written into each eval that would have asked it, and
|
|
6529
|
+
* [ctx.datasets] each graded dataset by id, as `{ name, body }`. Each
|
|
6530
|
+
* connection asked is written resolved, under its answer key with the
|
|
6531
|
+
* connection's slug for its id. `{ error }` when a dataset or a connection
|
|
6532
|
+
* the pipeline names is not given, since a bundle without it runs
|
|
6204
6533
|
* something else.
|
|
6205
6534
|
*/
|
|
6206
|
-
function exportBundle(doc , ctx
|
|
6535
|
+
function exportBundle(doc , ctx
|
|
6536
|
+
|
|
6207
6537
|
= {})
|
|
6208
6538
|
{
|
|
6209
|
-
const
|
|
6539
|
+
const split = (ctx.profiles ?? []).map(splitProfile);
|
|
6540
|
+
const profiles = withSlugs([...(ctx.connections ?? []), ...split.map(x => x.connection)] );
|
|
6541
|
+
const answers = answersOf(profiles, [...(ctx.connectionProfiles ?? []), ...split.flatMap(x => (x.profile ? [x.profile] : []))]);
|
|
6210
6542
|
// What the lab would supply at submit, written down: no lab supplies it later.
|
|
6211
6543
|
const out = clone(doc) ;
|
|
6212
6544
|
out.evals = (Array.isArray(out.evals) ? out.evals : [])
|
|
6213
6545
|
.map((t ) => EVAL_TYPES[t?.type]?.resolve?.(t, { grader: ctx.grader ?? null,
|
|
6214
6546
|
groups: Object.entries(ctx.datasets ?? {}).map(([id, d]) => ({ id, name: d.name, grader: d.body.grader ?? null })) }) ?? t);
|
|
6215
6547
|
const table = {};
|
|
6216
|
-
for (const
|
|
6217
|
-
const
|
|
6218
|
-
|
|
6548
|
+
for (const key of answerKeys(out)) {
|
|
6549
|
+
const at = answerOfKey(key);
|
|
6550
|
+
const named = answers.connection(at.connection);
|
|
6551
|
+
const p = answers.resolve(key);
|
|
6552
|
+
if (!named) return { error: `Connection ${at.connection} not found` };
|
|
6553
|
+
if (!p) return { error: `Connection Profile ${at.profile} not found` };
|
|
6219
6554
|
const { slug, ...flat } = connectionSettings(connectionOf(p) );
|
|
6220
|
-
|
|
6555
|
+
// A profile answering on a connection of its own takes that one's key.
|
|
6556
|
+
table[at.bare ? named.slug : answerKey(named.slug , at.profile)] = slug === named.slug ? flat : { ...flat, slug };
|
|
6221
6557
|
}
|
|
6222
6558
|
const files = {};
|
|
6223
6559
|
const taken = new Set (), written = new Set ();
|
|
@@ -6264,7 +6600,7 @@ function bundleProblems(files , keys = [])
|
|
|
6264
6600
|
for (const [path, text] of Object.entries(files)) {
|
|
6265
6601
|
for (const key of keys) {
|
|
6266
6602
|
if (isStr(key) && key.trim().length >= 4 && text.includes(key.trim())) {
|
|
6267
|
-
bad.push(`${path} holds a
|
|
6603
|
+
bad.push(`${path} holds a connection's key, and a key never leaves the lab`);
|
|
6268
6604
|
break;
|
|
6269
6605
|
}
|
|
6270
6606
|
}
|
|
@@ -6294,14 +6630,23 @@ function readBundle(files , ctx = {})
|
|
|
6294
6630
|
if (!isObj(table)) return { error: `${BUNDLE_PROFILES} maps each profile's slug to its settings` };
|
|
6295
6631
|
const keyed = bundleProblems(files);
|
|
6296
6632
|
if (keyed.length) return { error: keyed[0] };
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6633
|
+
// Each entry under its answer key, the connection's slug for its id: a
|
|
6634
|
+
// bundle from before version 16 keys each by its Target profile's slug.
|
|
6635
|
+
const profiles = [];
|
|
6636
|
+
const entries = {};
|
|
6637
|
+
for (const [key, p] of Object.entries(table)) {
|
|
6638
|
+
const at = answerOfKey(key);
|
|
6639
|
+
if (!isSlug(at.connection) || (at.profile != null && !PROFILE_ID.test(at.profile))) {
|
|
6640
|
+
return { error: `${BUNDLE_PROFILES}: ${JSON.stringify(key)} is not a slug` };
|
|
6641
|
+
}
|
|
6642
|
+
if (!isObj(p)) return { error: `${BUNDLE_PROFILES}: ${key} has to hold its settings` };
|
|
6301
6643
|
if (!CONNECTION_TYPES[typeOf(p )]) {
|
|
6302
|
-
return { error: `${BUNDLE_PROFILES}: ${
|
|
6644
|
+
return { error: `${BUNDLE_PROFILES}: ${key} is of type ${p.type}, which this lab does not have` };
|
|
6303
6645
|
}
|
|
6304
|
-
|
|
6646
|
+
const slug = isSlug(p.slug) ? p.slug : at.connection;
|
|
6647
|
+
const name = isStr(p.name) && p.name ? p.name : slug;
|
|
6648
|
+
entries[key] = { ...p, id: slug, slug, name };
|
|
6649
|
+
if (!profiles.some(x => x.id === at.connection)) profiles.push({ id: at.connection, slug: at.connection, name: slug === at.connection ? name : at.connection });
|
|
6305
6650
|
}
|
|
6306
6651
|
// Each dataset by its name, which is what joins it to the eval naming it.
|
|
6307
6652
|
const datasets = [];
|
|
@@ -6334,6 +6679,8 @@ function readBundle(files , ctx = {})
|
|
|
6334
6679
|
if (!ref) continue;
|
|
6335
6680
|
if (datasets.some(d => d.name === ref.name)) refs.push({ id: ref.id, name: ref.name });
|
|
6336
6681
|
}
|
|
6682
|
+
// A profile is named by the id the key holds it under; one the table
|
|
6683
|
+
// lacks is a reference the run does not carry, and validation says so.
|
|
6337
6684
|
const imported = importPipeline(raw, { profiles, groups: refs,
|
|
6338
6685
|
sources: content?.type === "source" && isRef(content.ref) ? [content.ref] : [] });
|
|
6339
6686
|
if ("error" in imported) return imported;
|
|
@@ -6342,7 +6689,7 @@ function readBundle(files , ctx = {})
|
|
|
6342
6689
|
for (const r of refs) byId[r.id] = datasets.find(d => d.name === r.name) .body;
|
|
6343
6690
|
const caseItems = [...new Set(Object.values(byId).flatMap(b => gradedSetFrom(b ).filter(c => !c.todo).map(caseItem)).filter(Boolean))];
|
|
6344
6691
|
const run = resolvePipeline(imported.doc, {
|
|
6345
|
-
profiles:
|
|
6692
|
+
profiles: key => runAnswer({ profiles: entries }, key) ?? null,
|
|
6346
6693
|
files: ctx.items ? [...ctx.items].sort() : caseItems,
|
|
6347
6694
|
});
|
|
6348
6695
|
const bad = validatePipeline(run, { groups: refs });
|
|
@@ -6361,24 +6708,28 @@ function stagesFor(run , i , resolveFilters
|
|
|
6361
6708
|
{
|
|
6362
6709
|
const stages = run.jobs.map((ch, k) => {
|
|
6363
6710
|
const { kind, modifiers: _mods, ...settings } = outOf(ch);
|
|
6364
|
-
// The modifiers a run applies, with each
|
|
6365
|
-
// synthetic modifier where its link sits (#339), and the
|
|
6711
|
+
// The modifiers a run applies, with each Post-format filter set a
|
|
6712
|
+
// synthetic modifier where its link sits (#339), and the Pre-format
|
|
6366
6713
|
// (text) filters applied to the raw reply before it is read (#405);
|
|
6367
6714
|
// without a resolver, a Library link reads nothing, a private one its own.
|
|
6368
6715
|
return { text: targetStepOf(run, i, k) .prompt, kind, withImage: sendsImage(ch),
|
|
6369
6716
|
verbatim: !!targetEntryOf(run, i, k)?.verbatim, before: beforeFilters(ch, resolveFilters),
|
|
6370
6717
|
modifiers: stageModifiers(ch, resolveFilters), settings };
|
|
6371
6718
|
});
|
|
6719
|
+
// Each named by the connection whose key it is sent with.
|
|
6720
|
+
const asked = (key , held ) => {
|
|
6721
|
+
const { connection: _c, ...conn } = held || {} ;
|
|
6722
|
+
return { id: answerConnectionId(key, held), ...conn };
|
|
6723
|
+
};
|
|
6372
6724
|
const connections = run.jobs.map((_, k) => {
|
|
6373
6725
|
// A step answered here asks nothing: the profile a run made before it
|
|
6374
6726
|
// kept is what it ran as, else Echo itself.
|
|
6727
|
+
const key = targetAnswerKey(run, i, k);
|
|
6375
6728
|
if (targetEntryOf(run, i, k)?.local) {
|
|
6376
|
-
const
|
|
6377
|
-
|
|
6378
|
-
return held && CONNECTION_TYPES[held.type]?.local ? { id: id , ...held } : { id: "", ...localConnOf(targetEntryOf(run, i, k)) };
|
|
6729
|
+
const held = key != null ? runAnswer(run, key) : undefined;
|
|
6730
|
+
return held && CONNECTION_TYPES[held.type]?.local ? asked(key , held) : { id: "", ...localConnOf(targetEntryOf(run, i, k)) };
|
|
6379
6731
|
}
|
|
6380
|
-
|
|
6381
|
-
return { id, ...(run.profiles?.[id] || {}) };
|
|
6732
|
+
return key != null ? asked(key, runAnswer(run, key)) : { id: "" };
|
|
6382
6733
|
});
|
|
6383
6734
|
// What a transport that builds its own request (an HTTP Request) builds
|
|
6384
6735
|
// it from in each job, and this target's step there.
|
|
@@ -6388,11 +6739,16 @@ function stagesFor(run , i , resolveFilters
|
|
|
6388
6739
|
|
|
6389
6740
|
/** The Setup profile scenario [i] of a run ran under, as History shows it: keyless. */
|
|
6390
6741
|
function scenarioProfile(run , i ) {
|
|
6391
|
-
|
|
6742
|
+
// Read by the target: its own connection and profile, frozen at submit
|
|
6743
|
+
// apart from any other target's that shares the connection.
|
|
6744
|
+
const t = targetsOf(run)[i];
|
|
6745
|
+
const key = isRef(t?.connection) ? answerKey(t.connection.id, isRef(t.profile) ? t.profile.id : null) : undefined;
|
|
6392
6746
|
// A target that asks nothing ran as Echo.
|
|
6393
|
-
if (
|
|
6394
|
-
const
|
|
6395
|
-
|
|
6747
|
+
if (key == null && targetEntryOf(run, i, 0)?.local) { const lc = localConnOf(targetEntryOf(run, i, 0)); return { id: "", name: lc.name, settings: { ...lc } }; }
|
|
6748
|
+
const held = key != null ? runAnswer(run, key) : undefined;
|
|
6749
|
+
if (!held) return null;
|
|
6750
|
+
const { connection: _c, ...conn } = held;
|
|
6751
|
+
return { id: answerConnectionId(key , held), name: conn.name, settings: connectionSettings(conn ) };
|
|
6396
6752
|
}
|
|
6397
6753
|
|
|
6398
6754
|
/** The kind a run's result is read as: its last job's. */
|
|
@@ -6758,13 +7114,14 @@ export {
|
|
|
6758
7114
|
PIPELINE_VERSION, DATASET_BODY_VERSION, STEP_TYPES, CONTENT_TYPES, OUTPUT_KINDS, MODIFIERS, EVAL_TYPES, LEGACY_TESTS, METRICS, readMetrics, metricSummary, metricCheck, itemScoresAsync, recordedReplyOf,
|
|
6759
7115
|
SOURCE_TYPES, DEFAULT_SOURCE_TYPE, sourceTypeOf, WORKFLOW_PLATFORMS, platformOf, DYNAMIC_PROVIDERS, providerOf, WIZARDS, wizardProgress,
|
|
6760
7116
|
registerKinds, defaultKind, jobDefaults, applyModifiers, keyVar, slugOf, uniqueSlug, isSlug, withSlugs, SLUG_MAX, connectionOf, addressOf, connectionSettings,
|
|
7117
|
+
splitProfile, mergeAnswer, answerKey, answerOfKey, answersOf, runAnswer, answerConnectionId, connectionScoped,
|
|
6761
7118
|
connectionRequest, connectionBase, connectionSend, connectionReply, connectionReplyProblem, connectionImage, connectionProblems,
|
|
6762
7119
|
CONNECTION_FIELDS, SETTING_KEYS, OPTION_FIELDS, jobLabel, scenarioLabel, jobWords, versionProblem,
|
|
6763
7120
|
contentOf, withContent, replyOf,
|
|
6764
|
-
targetsOf, targetLetter, targetLabel, targetStepOf, targetEntryOf, jobTargetType,
|
|
7121
|
+
targetsOf, targetLetter, targetLabel, targetStepOf, targetEntryOf, jobTargetType, targetAnswerOf, targetAnswerKey, recordedTargetOf, withTarget,
|
|
6765
7122
|
tokensOf, withTokens, sendsImage, withImage, flowStepOf, readAsOf, outOf, withOut, slotEntry, SLOTS,
|
|
6766
7123
|
filterSetsOf, withFilterSets, stageModifiers, beforeFilters,
|
|
6767
|
-
blankPipeline, upgradePipeline, fatProfileRef, newId, newEval, newLink, validatePipeline, resolvePipeline, pipelineOfRun, stagesFor, profileIds,
|
|
7124
|
+
blankPipeline, upgradePipeline, fatProfileRef, newId, newEval, newLink, validatePipeline, resolvePipeline, pipelineOfRun, stagesFor, profileIds, answerKeys,
|
|
6768
7125
|
scenarioProfile, lastKind, lastModifier, modifierSummary, itemScores, scenarioEvals, scenarioPasses, groupPass, overallVerdict, passVerdict, isSkipped, failedScore,
|
|
6769
7126
|
evalLabel, evalsOf, evalsDataset, casesRef, groupOf, groupOfMetrics, pipelineWarnings, itemsFound, previewOf, isWholeRun, EVAL_FIELDS,
|
|
6770
7127
|
pipelineToYaml, importPipeline, yamlToPipeline, exportBundle, readBundle, bundleProblems, junitXml,
|