evals-lab 0.13.0 → 0.14.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 +39 -0
- package/README.md +2 -2
- package/bin/run.js +4 -2
- package/lab/VERSION +1 -1
- package/lab/demo/CREDITS.md +14 -0
- package/lab/demo/datasets/demo-1.json +2 -2
- package/lab/demo/datasets/demo-2.json +2 -2
- package/lab/demo/datasets/documents-type.json +79 -0
- package/lab/demo/datasets/documents-valid.json +117 -0
- package/lab/demo/datasets/sentiment.json +2 -2
- package/lab/demo/datasets/weather-alerts.json +2 -2
- package/lab/demo/datasets/wordings.json +1 -1
- package/lab/demo/manifest.json +3 -3
- package/lab/demo/pipelines/demo-1.json +4 -4
- package/lab/demo/pipelines/demo-2.json +4 -4
- package/lab/demo/pipelines/documents.json +102 -0
- package/lab/demo/pipelines/sentiment.json +4 -4
- package/lab/demo/pipelines/weather-alerts.json +9 -5
- package/lab/demo/pipelines/wordings.json +3 -3
- package/lab/demo/sources/Demo 1/source.json +1 -1
- package/lab/demo/sources/Demo 2/source.json +1 -1
- package/lab/demo/sources/documents/invoice.pdf +65 -0
- package/lab/demo/sources/documents/letter.pdf +63 -0
- package/lab/demo/sources/documents/not-a-pdf.pdf +2 -0
- package/lab/demo/sources/documents/receipt.pdf +62 -0
- package/lab/demo/sources/documents/report.pdf +59 -0
- package/lab/demo/sources/documents/source.json +3 -0
- package/lab/demo/sources/documents/truncated.pdf +19 -0
- package/lab/demo/sources/sentiment/source.json +1 -1
- package/lab/demo/sources/weather-alerts/source.json +1 -1
- package/lab/evals-core.mjs +217 -48
- package/lab/flows/record.mjs +5 -4
- package/lab/kinds/list.mjs +72 -21
- package/lab/run-evals.js +3 -1
- package/lab/server.py +536 -85
- package/lab/web/dist/assets/gallery-qrf0ZGXK.js +5 -0
- package/lab/web/dist/assets/main-D6xARg2A.js +16 -0
- package/lab/web/dist/assets/main-xVU6J7Gi.css +1 -0
- package/lab/web/dist/assets/tokens-C8dqXDHN.js +62 -0
- package/lab/web/dist/assets/tokens-tQ7zTPkd.css +1 -0
- package/lab/web/dist/gallery.html +3 -3
- package/lab/web/dist/index.html +4 -4
- package/package.json +1 -1
- package/lab/web/dist/assets/gallery-DpaqOg9Z.js +0 -5
- package/lab/web/dist/assets/main-BSw-jyYD.css +0 -1
- package/lab/web/dist/assets/main-CrB8wPZn.js +0 -17
- package/lab/web/dist/assets/tokens-CrjDMCbz.js +0 -60
- package/lab/web/dist/assets/tokens-DDOONQon.css +0 -1
package/lab/evals-core.mjs
CHANGED
|
@@ -199,12 +199,23 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
202
207
|
|
|
203
208
|
|
|
204
209
|
|
|
205
210
|
|
|
206
211
|
|
|
207
212
|
|
|
213
|
+
/** One kept response of a dynamic Source's item (#596). */
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
208
219
|
|
|
209
220
|
|
|
210
221
|
|
|
@@ -347,17 +358,26 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
347
358
|
* at. `filters` are tried in order -- each drop-item narrows the items the
|
|
348
359
|
* next reads, and the first reject-reply that fires gates the answer.
|
|
349
360
|
*/
|
|
350
|
-
/** Which side of Response Format a filter
|
|
351
|
-
it
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
/** Which side of Response Format a filter step sits on (#405): `text`
|
|
362
|
+
before it, on the raw reply (Pre-format), `items` after it (Post-format).
|
|
363
|
+
It also names what a filter reads: the reply's text (rewrite-text /
|
|
364
|
+
reject-text) or the items (drop-item, rewrite-item, keep-first,
|
|
365
|
+
reject-reply). A text filter runs at either stage, an item filter only
|
|
366
|
+
Post-format after a kind that reads items (#557, `stageReads`); a set
|
|
367
|
+
belongs to no stage (#432), and runs its share in each it is linked in. */
|
|
355
368
|
|
|
356
369
|
|
|
357
370
|
/** The stage a filter type's scope belongs to: text filters read the raw
|
|
358
371
|
reply; item, list and answer filters read the items it is read into. */
|
|
359
372
|
export const stageOfScope = (scope ) => (scope === "text" ? "text" : "items");
|
|
360
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
|
+
|
|
361
381
|
/** A filter stage as the page names it: before the reply is formatted, or after. */
|
|
362
382
|
export const FILTER_STAGE_LABEL = { text: "Pre-format", items: "Post-format" };
|
|
363
383
|
|
|
@@ -555,6 +575,10 @@ export const FILTER_STAGE_LABEL = { text: "Pre-form
|
|
|
555
575
|
|
|
556
576
|
|
|
557
577
|
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
|
|
558
582
|
|
|
559
583
|
|
|
560
584
|
|
|
@@ -586,7 +610,15 @@ export const FILTER_STAGE_LABEL = { text: "Pre-form
|
|
|
586
610
|
|
|
587
611
|
|
|
588
612
|
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
|
|
589
617
|
|
|
618
|
+
|
|
619
|
+
/** A reply the reply-token limit stopped: the limit the request sent, or
|
|
620
|
+
null where it sent none and the server's own applied. */
|
|
621
|
+
|
|
590
622
|
|
|
591
623
|
/** The transport: a stage's prompt and image in, its reply out. */
|
|
592
624
|
|
|
@@ -607,9 +639,15 @@ export const FILTER_STAGE_LABEL = { text: "Pre-form
|
|
|
607
639
|
|
|
608
640
|
|
|
609
641
|
|
|
610
|
-
|
|
611
642
|
|
|
643
|
+
|
|
644
|
+
|
|
612
645
|
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
|
|
613
651
|
|
|
614
652
|
|
|
615
653
|
/** How much of one text a run keeps -- a reply's body, a grader's prompt or
|
|
@@ -675,6 +713,7 @@ export function kept(text , max = KEPT_CHARS) {
|
|
|
675
713
|
|
|
676
714
|
|
|
677
715
|
|
|
716
|
+
|
|
678
717
|
|
|
679
718
|
|
|
680
719
|
|
|
@@ -1204,10 +1243,14 @@ const platformOf = (src
|
|
|
1204
1243
|
|
|
1205
1244
|
|
|
1206
1245
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
|
|
1210
1249
|
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
|
|
1211
1254
|
|
|
1212
1255
|
|
|
1213
1256
|
|
|
@@ -1697,6 +1740,10 @@ function datasetRules(body ) {
|
|
|
1697
1740
|
|
|
1698
1741
|
|
|
1699
1742
|
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
|
|
1700
1747
|
|
|
1701
1748
|
|
|
1702
1749
|
|
|
@@ -2106,6 +2153,7 @@ function ollamaType(id , label , cloud ) {
|
|
|
2106
2153
|
const r = j ;
|
|
2107
2154
|
return { raw: r?.message?.content ?? "", finishReason: r?.done_reason ?? null };
|
|
2108
2155
|
},
|
|
2156
|
+
cutOff: { reason: "length", limit: (body) => asNumber((body.options )?.num_predict) },
|
|
2109
2157
|
listModels: (base, key) => listModels(base, cloud ? key : null),
|
|
2110
2158
|
parseModels: idsFrom,
|
|
2111
2159
|
...(cloud ? { url: "https://ollama.com" } : { keyless: true }),
|
|
@@ -2220,6 +2268,7 @@ CONNECTION_TYPES["openai-compatible"] = {
|
|
|
2220
2268
|
documents: true,
|
|
2221
2269
|
structured: chatSchema,
|
|
2222
2270
|
parseReply: chatReply,
|
|
2271
|
+
cutOff: { reason: "length", limit: (body) => asNumber(body.max_completion_tokens ?? body.max_tokens) },
|
|
2223
2272
|
listModels, parseModels: idsFrom,
|
|
2224
2273
|
};
|
|
2225
2274
|
|
|
@@ -2271,6 +2320,7 @@ CONNECTION_TYPES.anthropic = {
|
|
|
2271
2320
|
const raw = (r?.content || []).filter(b => b.type === "text").map(b => b.text).join("");
|
|
2272
2321
|
return { raw, finishReason: r?.stop_reason ?? null };
|
|
2273
2322
|
},
|
|
2323
|
+
cutOff: { reason: "max_tokens", limit: (body) => asNumber(body.max_tokens) },
|
|
2274
2324
|
// A flow's request, keyed as the Messages API is -- what the HTTP endpoint
|
|
2275
2325
|
// type's Anthropic template sets. [base] is the /v1 base words hang off;
|
|
2276
2326
|
// the step's path is the whole path production called, so it goes to the
|
|
@@ -2354,6 +2404,7 @@ CONNECTION_TYPES["llama.cpp"] = {
|
|
|
2354
2404
|
// llama-server takes OpenAI's response_format, a schema and all.
|
|
2355
2405
|
structured: chatSchema,
|
|
2356
2406
|
parseReply: chatReply,
|
|
2407
|
+
cutOff: { reason: "length", limit: (body) => asNumber(body.n_predict) },
|
|
2357
2408
|
listModels, parseModels: idsFrom,
|
|
2358
2409
|
// A reply that arrived is still a failure when this profile asked for the
|
|
2359
2410
|
// Speed looping check (today's speedReplyError, now an opt-in setting).
|
|
@@ -2728,6 +2779,18 @@ function connectionReply(conn , j )
|
|
|
2728
2779
|
}
|
|
2729
2780
|
|
|
2730
2781
|
/** Why a reply [conn] received is a failure though it arrived, or null. */
|
|
2782
|
+
/** How a reply finished, as a stage keeps it (#597): the reason [conn]'s
|
|
2783
|
+
API gave, and -- where that reason is its type's reply-token limit -- the
|
|
2784
|
+
limit [body], the request sent, carried. A type that says no reason
|
|
2785
|
+
keeps nothing. */
|
|
2786
|
+
function replyFinish(conn , body , finishReason ) {
|
|
2787
|
+
if (!finishReason) return {};
|
|
2788
|
+
const cut = typeEntry(conn).cutOff;
|
|
2789
|
+
if (!cut || finishReason !== cut.reason) return { finish: finishReason };
|
|
2790
|
+
const limit = body && typeof body === "object" ? cut.limit(body ) : null;
|
|
2791
|
+
return { finish: finishReason, stopped: { limit } };
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2731
2794
|
function connectionReplyProblem(conn , raw , finishReason ) {
|
|
2732
2795
|
return typeEntry(conn).replyProblem?.(conn, raw, finishReason) ?? null;
|
|
2733
2796
|
}
|
|
@@ -3253,7 +3316,8 @@ async function runPipeline(stages , dataUrl , cal
|
|
|
3253
3316
|
total += res.ms;
|
|
3254
3317
|
transcript.push({ n: i + 1, kind, conn: res.conn ?? null, withImage, sent: res.sent ?? sent,
|
|
3255
3318
|
got: res.raw ?? "", ms: res.ms, error: res.error, ...(res.said != null ? { said: res.said } : {}),
|
|
3256
|
-
...(res.body != null ? { body: kept(res.body) } : {})
|
|
3319
|
+
...(res.body != null ? { body: kept(res.body) } : {}),
|
|
3320
|
+
...(res.finish ? { finish: res.finish } : {}), ...(res.stopped ? { stopped: res.stopped } : {}) });
|
|
3257
3321
|
if (res.error) return result({ error: named(res.error) });
|
|
3258
3322
|
// A stage that failed is the end of the job: nothing after it is asked.
|
|
3259
3323
|
// Handed nothing, the next stage still answers -- about an empty list --
|
|
@@ -3272,6 +3336,16 @@ async function runPipeline(stages , dataUrl , cal
|
|
|
3272
3336
|
if (mod.reject) {
|
|
3273
3337
|
return result({ error: named(`discarded: ${mod.reject}`), ...(mod.by ? { filteredBy: by(mod.by, "items", true) } : {}) });
|
|
3274
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
|
+
}
|
|
3275
3349
|
// A filter that dropped the last item: what a zero result is explained by.
|
|
3276
3350
|
const emptied = mod.by && reader.terms && !reader.terms(mod.value).length ? { filteredBy: by(mod.by, "items", false) } : {};
|
|
3277
3351
|
const nothing = handsOn ? reader.handsOn?.(mod.value) : null;
|
|
@@ -3280,7 +3354,9 @@ async function runPipeline(stages , dataUrl , cal
|
|
|
3280
3354
|
held = { kind, value };
|
|
3281
3355
|
const gone = [...(read.dropped || []), ...mod.dropped];
|
|
3282
3356
|
echoed = (read.echoed || 0) + mod.dropped.filter(d => d.by === ECHO).length;
|
|
3283
|
-
|
|
3357
|
+
// {reply} and {stageN} hand on the text as its filters left it -- what
|
|
3358
|
+
// was graded -- never the raw reply a filter cleaned (#557).
|
|
3359
|
+
done.push({ reply: transcript[transcript.length - 1] .filtered?.got ?? res.raw ?? "", rendered: reader.render?.(value) || {} });
|
|
3284
3360
|
if (!handsOn) {
|
|
3285
3361
|
dropped = gone;
|
|
3286
3362
|
return result({ echoed, dropped, ...emptied });
|
|
@@ -3380,7 +3456,7 @@ function applyModifiers (list , kind ,
|
|
|
3380
3456
|
// 16: a target names an AI Connection and, optionally, a Connection Profile,
|
|
3381
3457
|
// where it named one Target profile; a run's table is keyed by the pair
|
|
3382
3458
|
// (answerKey) -- the Target profile split in the store as it opens (#445).
|
|
3383
|
-
const PIPELINE_VERSION =
|
|
3459
|
+
const PIPELINE_VERSION = 18 ;
|
|
3384
3460
|
|
|
3385
3461
|
// Plain objects, so an entry is added by assignment and a reader never needs
|
|
3386
3462
|
// to know which registered it.
|
|
@@ -3662,9 +3738,10 @@ function splitProfile(p )
|
|
|
3662
3738
|
}
|
|
3663
3739
|
|
|
3664
3740
|
/** [conn] with [profile]'s settings over it: the one set of settings a
|
|
3665
|
-
request is built from, the shape a Target profile had. A profile
|
|
3666
|
-
|
|
3667
|
-
|
|
3741
|
+
request is built from, the shape a Target profile had. A profile -- or
|
|
3742
|
+
(Default)'s settings, which are one without its id and name -- sets no
|
|
3743
|
+
connection field. */
|
|
3744
|
+
function mergeAnswer(conn , profile ) {
|
|
3668
3745
|
const out = { ...conn };
|
|
3669
3746
|
const scoped = connectionScoped(typeOf(conn ));
|
|
3670
3747
|
for (const [k, v] of Object.entries(profile ?? {})) {
|
|
@@ -3681,7 +3758,7 @@ function mergeAnswer(conn , profile
|
|
|
3681
3758
|
|
|
3682
3759
|
|
|
3683
3760
|
/** The key a run's table holds [connection] under, with [profile] -- or with
|
|
3684
|
-
none,
|
|
3761
|
+
none, (Default)'s settings. A grader names its connection bare, its id alone,
|
|
3685
3762
|
so it never meets a target's pair (see `answersOf`). */
|
|
3686
3763
|
const answerKey = (connection , profile ) => `${connection}.${profile ?? ""}`;
|
|
3687
3764
|
|
|
@@ -3716,9 +3793,13 @@ function targetAnswerKey(doc
|
|
|
3716
3793
|
* answer key resolved to the settings it sends -- the profile's connection
|
|
3717
3794
|
* where it names one, else the key's, with the profile's settings over it.
|
|
3718
3795
|
* A grader's bare id resolves as the Target profile it named did: its
|
|
3719
|
-
* connection under the profile split from it.
|
|
3796
|
+
* connection under the profile split from it. One that names no profile, or
|
|
3797
|
+
* a grader with none split beside it, takes [fallback] instead -- (Default)'s
|
|
3798
|
+
* settings, which sit under no named profile, so a profile is still what it
|
|
3799
|
+
* sets (Setup's Used by counts the same, `defaultUses`). Null where any part
|
|
3800
|
+
* is gone.
|
|
3720
3801
|
*/
|
|
3721
|
-
function answersOf(connections , profiles ) {
|
|
3802
|
+
function answersOf(connections , profiles , fallback ) {
|
|
3722
3803
|
const connection = (id ) => connections.find(c => c?.id === id) ?? null;
|
|
3723
3804
|
const profile = (id ) => profiles.find(p => p?.id === id) ?? null;
|
|
3724
3805
|
const split = (id ) => {
|
|
@@ -3731,7 +3812,7 @@ function answersOf(connections , profiles
|
|
|
3731
3812
|
const p = pid ? profile(pid) : null;
|
|
3732
3813
|
if (pid && !p) return null;
|
|
3733
3814
|
const c = connection(isRef(p?.connection) ? p .connection .id : at.connection);
|
|
3734
|
-
return c ? mergeAnswer(c, p) : null;
|
|
3815
|
+
return c ? mergeAnswer(c, p ?? fallback) : null;
|
|
3735
3816
|
};
|
|
3736
3817
|
return { connection, profile, split, resolve };
|
|
3737
3818
|
}
|
|
@@ -3884,15 +3965,15 @@ SOURCE_TYPES.dynamic = {
|
|
|
3884
3965
|
};
|
|
3885
3966
|
|
|
3886
3967
|
// HTTP GET (docs/dynamic-sources.md § 1): the safest pull provider. It fetches
|
|
3887
|
-
// a URL through the relay server-side --
|
|
3888
|
-
//
|
|
3968
|
+
// a URL through the relay server-side -- Fetch now keeps a response, Live
|
|
3969
|
+
// data fetches it again at submit -- so the page drives only the
|
|
3889
3970
|
// create dialog and the Call labels; the fetch, redaction and token-refusal are
|
|
3890
3971
|
// the server's (its SOURCE_PROVIDERS mirror in server.py).
|
|
3891
3972
|
DYNAMIC_PROVIDERS["http-get"] = { // provider: the HTTP GET adapter's own id
|
|
3892
3973
|
id: "http-get", // provider: its id, the row's config.provider
|
|
3893
3974
|
label: "HTTP GET",
|
|
3894
3975
|
icon: "square-code",
|
|
3895
|
-
noun: "
|
|
3976
|
+
noun: "response",
|
|
3896
3977
|
tier: "pull",
|
|
3897
3978
|
config: ["url"],
|
|
3898
3979
|
configProblems(config){
|
|
@@ -3921,7 +4002,7 @@ DYNAMIC_PROVIDERS["api"] = { // provider: the API call adapter's own id
|
|
|
3921
4002
|
id: "api", // provider: its id, the row's config.provider
|
|
3922
4003
|
label: "API call",
|
|
3923
4004
|
icon: "arrow-left-right",
|
|
3924
|
-
noun: "
|
|
4005
|
+
noun: "response",
|
|
3925
4006
|
tier: "pull",
|
|
3926
4007
|
config: ["method", "url", "reqHeaders", "body", "connection"],
|
|
3927
4008
|
configProblems(config){
|
|
@@ -3950,7 +4031,7 @@ DYNAMIC_PROVIDERS["api"] = { // provider: the API call adapter's own id
|
|
|
3950
4031
|
// or an Upload… by hand, formalised as a provider. The lab fetches and executes
|
|
3951
4032
|
// nothing: a Supplied item is named by the id the caller gives it (its file
|
|
3952
4033
|
// name), which is what joins it to a case. It has no config and no server-side
|
|
3953
|
-
// production, so no
|
|
4034
|
+
// production, so no Fetch now and no live data; its Calls are the items handed in,
|
|
3954
4035
|
// redacted on the way in like any other (server.py's take_dynamic). A hosted run
|
|
3955
4036
|
// grades those Calls; a solely-Supplied Source with none is run only via the CLI.
|
|
3956
4037
|
DYNAMIC_PROVIDERS["supplied"] = { // provider: the Supplied adapter's own id
|
|
@@ -3980,6 +4061,7 @@ DYNAMIC_PROVIDERS["file"] = { // provider: the File on disk adapter's own id
|
|
|
3980
4061
|
label: "File on disk",
|
|
3981
4062
|
icon: "file",
|
|
3982
4063
|
noun: "file",
|
|
4064
|
+
produceLabel: "Read now",
|
|
3983
4065
|
tier: "local",
|
|
3984
4066
|
config: ["path"],
|
|
3985
4067
|
configProblems(config){
|
|
@@ -4003,6 +4085,7 @@ DYNAMIC_PROVIDERS["command"] = { // provider: the Command adapter's own id
|
|
|
4003
4085
|
label: "Command",
|
|
4004
4086
|
icon: "terminal",
|
|
4005
4087
|
noun: "call",
|
|
4088
|
+
produceLabel: "Run now",
|
|
4006
4089
|
tier: "local",
|
|
4007
4090
|
config: ["command", "args"],
|
|
4008
4091
|
configProblems(config){
|
|
@@ -4085,11 +4168,14 @@ CONTENT_TYPES.source = {
|
|
|
4085
4168
|
description: "Every file or record in a Source from the Library.",
|
|
4086
4169
|
// `live` is a dynamic Source's per-run snapshot-vs-live override
|
|
4087
4170
|
// (docs/dynamic-sources.md); meaningless for another kind, ignored there.
|
|
4088
|
-
fields: ["type", "ref", "first", "loops", "live"],
|
|
4171
|
+
fields: ["type", "ref", "first", "loops", "live", "pin"],
|
|
4089
4172
|
runFields: ["files", "revs"],
|
|
4090
4173
|
validate(c, ctx, bad){
|
|
4091
4174
|
if (!isRef(c.ref)) return void bad.push("a Source content has to name its Source as { id, name }");
|
|
4092
4175
|
if (c.live != null && typeof c.live !== "boolean") bad.push("a Source content's live is true or false");
|
|
4176
|
+
if (c.pin != null && !(isObj(c.pin) && isStr(c.pin.name) && c.pin.name && Number.isInteger(c.pin.n) && c.pin.n >= 0)) {
|
|
4177
|
+
bad.push("a Source content's pin names a response as { name, n }");
|
|
4178
|
+
}
|
|
4093
4179
|
if (c.first != null && !(Number.isInteger(c.first) && c.first > 0)) {
|
|
4094
4180
|
bad.push("Run first wants a positive number, or empty for all");
|
|
4095
4181
|
}
|
|
@@ -4269,7 +4355,7 @@ function erroredScore (s , error
|
|
|
4269
4355
|
}
|
|
4270
4356
|
|
|
4271
4357
|
/** The reply a metric grades and Results shows (#546): the last stage's
|
|
4272
|
-
reply after its
|
|
4358
|
+
reply after its filters (#557), or the raw reply. */
|
|
4273
4359
|
function gradedText(res ) {
|
|
4274
4360
|
const last = res?.transcript?.at(-1);
|
|
4275
4361
|
return last?.filtered?.got ?? last?.got ?? res?.raw ?? "";
|
|
@@ -5147,6 +5233,22 @@ function withContent (doc , content )
|
|
|
5147
5233
|
return { ...doc, jobs: [withStep(first, "attachContent", content ? { type: "attachContent", content } : null), ...rest] };
|
|
5148
5234
|
}
|
|
5149
5235
|
|
|
5236
|
+
/**
|
|
5237
|
+
* Whether target [i]'s words in job [k] have the item's text joined after
|
|
5238
|
+
* them (#597): job 1, a target that asks a prompt, text content -- a Source
|
|
5239
|
+
* of text items, or inline text, as its CONTENT_TYPES entry says -- and no
|
|
5240
|
+
* {text} in the words to place it (textPrompt). A later job's reply goes
|
|
5241
|
+
* only where a token places it, and an image or a document is sent as a part
|
|
5242
|
+
* of its own, so neither is joined. [ctx] reads a Source as validation does.
|
|
5243
|
+
*/
|
|
5244
|
+
function contentJoined(doc , i , k ,
|
|
5245
|
+
ctx = {}) {
|
|
5246
|
+
if (k !== 0 || targetEntryOf(doc, i, k)?.asks !== "prompt") return false;
|
|
5247
|
+
if (String(targetStepOf(doc, i, k)?.prompt ?? "").includes("{text}")) return false;
|
|
5248
|
+
const content = contentOf(doc);
|
|
5249
|
+
return !!content && (CONTENT_TYPES[content.type]?.text?.(content, ctx) ?? null) != null;
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5150
5252
|
/** A document's targets, in column order. */
|
|
5151
5253
|
function targetsOf(doc ) {
|
|
5152
5254
|
return Array.isArray(doc?.targets) ? doc .targets : [];
|
|
@@ -5366,6 +5468,15 @@ function stageModifiers(job , resolve
|
|
|
5366
5468
|
return out;
|
|
5367
5469
|
}
|
|
5368
5470
|
|
|
5471
|
+
/** A job's Post-format filters for a run when its kind reads no items
|
|
5472
|
+
(#557), in document order: every link's on its `items` filters step, which
|
|
5473
|
+
a run applies to the text after the kind reads it. A kind that reads items
|
|
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
|
+
|
|
5369
5480
|
/** A job's Pre-format (text) filters for a run (#405), in document order:
|
|
5370
5481
|
the filters of every link on its `text` filters step, which a run applies to
|
|
5371
5482
|
the raw reply before the kind reads it into items. [resolve] gives a Library
|
|
@@ -5533,6 +5644,12 @@ function versionProblem(doc ) {
|
|
|
5533
5644
|
|
|
5534
5645
|
|
|
5535
5646
|
|
|
5647
|
+
|
|
5648
|
+
|
|
5649
|
+
|
|
5650
|
+
|
|
5651
|
+
|
|
5652
|
+
|
|
5536
5653
|
|
|
5537
5654
|
|
|
5538
5655
|
/**
|
|
@@ -5824,7 +5941,8 @@ function ownBodiesNow(doc ) {
|
|
|
5824
5941
|
|
|
5825
5942
|
function upgradePipeline (doc , ctx = {}) {
|
|
5826
5943
|
// A current document is read as it is, but for a profile reference the Runs
|
|
5827
|
-
// tab saved whole (see profileRefs), which is cut back,
|
|
5944
|
+
// tab saved whole (see profileRefs), which is cut back, a Read Reply's
|
|
5945
|
+
// leftover `preamble: false` (withoutLeftoverPreamble), and a step on a
|
|
5828
5946
|
// profile a target step stands in for (localSteps).
|
|
5829
5947
|
if (isObj(doc) && doc.version === PIPELINE_VERSION) {
|
|
5830
5948
|
let out = doc ;
|
|
@@ -5832,12 +5950,16 @@ function upgradePipeline (doc , ctx = {}) {
|
|
|
5832
5950
|
out = clone(out) ;
|
|
5833
5951
|
profileRefs(out);
|
|
5834
5952
|
}
|
|
5835
|
-
return localSteps(ownBodiesNow(out), ctx) ;
|
|
5953
|
+
return localSteps(ownBodiesNow(withoutLeftoverPreamble(out)), ctx) ;
|
|
5836
5954
|
}
|
|
5837
|
-
if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].includes(doc.version )) return doc;
|
|
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;
|
|
5956
|
+
// Every version reads as 17 first, then as 18: a saved-mode Source content
|
|
5957
|
+
// pins one response (savedPinned).
|
|
5958
|
+
const eighteen = (d ) => localSteps(ownBodiesNow(savedPinned(d, ctx)), ctx) ;
|
|
5959
|
+
if (doc.version === 17) return eighteen(cutRefs(clone(doc) ));
|
|
5838
5960
|
// Every version reads as 16 first, then as 17: its requests read by their
|
|
5839
5961
|
// job's HTTP API (requestsOnJobApi).
|
|
5840
|
-
const seventeen = (d ) =>
|
|
5962
|
+
const seventeen = (d ) => eighteen(requestsOnJobApi(d));
|
|
5841
5963
|
if (doc.version === 16) return seventeen(cutRefs(clone(doc) ));
|
|
5842
5964
|
// Every version reads as 15 first, then as 16: its Target profiles split
|
|
5843
5965
|
// into AI Connections and Connection Profiles (answersFromProfiles).
|
|
@@ -5883,11 +6005,13 @@ function filtersFromCleaning(next ) {
|
|
|
5883
6005
|
const isFilters = (st ) => isObj(st) && st.type === "filters";
|
|
5884
6006
|
next.jobs = next.jobs.map((job ) => {
|
|
5885
6007
|
if (!isObj(job) || !Array.isArray(job.steps)) return job;
|
|
5886
|
-
const
|
|
5887
|
-
const readReply = steps.find(st => isObj(st) && st.type === "readReply") ;
|
|
6008
|
+
const readReply = job.steps.find((st ) => isObj(st) && st.type === "readReply") ;
|
|
5888
6009
|
const preamble = !!(readReply && isObj(readReply.out) && readReply.out.preamble === true);
|
|
6010
|
+
// Stripped from every Read Reply, `false` included, before the job is
|
|
6011
|
+
// asked whether it has anything to migrate (#604).
|
|
6012
|
+
const steps = (job.steps ).map(withoutPreamble);
|
|
5889
6013
|
const stale = steps.some(st => isFilters(st) && (st ).stage == null);
|
|
5890
|
-
if (!steps.some(isCleaning) && !preamble && !stale) return job;
|
|
6014
|
+
if (!steps.some(isCleaning) && !preamble && !stale) return { ...job, steps };
|
|
5891
6015
|
const jid = isStr(job.id) ? job.id : "job";
|
|
5892
6016
|
|
|
5893
6017
|
// Walk the Responses steps, turning each into a chunk of items filters
|
|
@@ -5916,11 +6040,6 @@ function filtersFromCleaning(next ) {
|
|
|
5916
6040
|
const out = [];
|
|
5917
6041
|
let placed = false;
|
|
5918
6042
|
for (const st of steps) {
|
|
5919
|
-
if (isObj(st) && st.type === "readReply" && isObj(st.out) && "preamble" in st.out) {
|
|
5920
|
-
const { preamble: _p, ...rest } = st.out ;
|
|
5921
|
-
out.push({ ...st, out: rest });
|
|
5922
|
-
continue;
|
|
5923
|
-
}
|
|
5924
6043
|
if (isCleaning(st) || isFilters(st)) {
|
|
5925
6044
|
if (!placed) { if (textStep) out.push(textStep); if (itemsStep) out.push(itemsStep); placed = true; }
|
|
5926
6045
|
continue;
|
|
@@ -5933,6 +6052,30 @@ function filtersFromCleaning(next ) {
|
|
|
5933
6052
|
return next;
|
|
5934
6053
|
}
|
|
5935
6054
|
|
|
6055
|
+
/** [st] without its List `preamble` setting, when it is a Read Reply holding
|
|
6056
|
+
one; [st] itself otherwise. */
|
|
6057
|
+
function withoutPreamble(st ) {
|
|
6058
|
+
if (!isObj(st) || st.type !== "readReply" || !isObj(st.out) || !("preamble" in st.out)) return st;
|
|
6059
|
+
const { preamble: _p, ...out } = st.out ;
|
|
6060
|
+
return { ...st, out };
|
|
6061
|
+
}
|
|
6062
|
+
|
|
6063
|
+
/** Whether a current document still carries a `preamble: false` that an
|
|
6064
|
+
upgrade before #604 left behind. It did nothing, so it is cut as the
|
|
6065
|
+
document is read; a `preamble: true` the lab never wrote at this version is
|
|
6066
|
+
left for validation to refuse. */
|
|
6067
|
+
function leftoverPreamble(doc ) {
|
|
6068
|
+
return Array.isArray(doc.jobs) && doc.jobs.some((job ) => isObj(job) && Array.isArray(job.steps)
|
|
6069
|
+
&& job.steps.some((st ) => isObj(st) && st.type === "readReply" && isObj(st.out) && st.out.preamble === false));
|
|
6070
|
+
}
|
|
6071
|
+
|
|
6072
|
+
/** [doc] with each Read Reply's leftover `preamble: false` cut (leftoverPreamble). */
|
|
6073
|
+
function withoutLeftoverPreamble(doc ) {
|
|
6074
|
+
if (!leftoverPreamble(doc)) return doc;
|
|
6075
|
+
return { ...doc, jobs: doc.jobs.map((job ) => (!isObj(job) || !Array.isArray(job.steps) ? job
|
|
6076
|
+
: { ...job, steps: job.steps.map((st ) => (isObj(st) && isObj(st.out) && st.out.preamble === false ? withoutPreamble(st) : st)) })) };
|
|
6077
|
+
}
|
|
6078
|
+
|
|
5936
6079
|
/** [doc] with no Metrics eval holding the `case` metric: a dataset's cases
|
|
5937
6080
|
are metrics now (dataset body version 5), which an eval naming the
|
|
5938
6081
|
dataset adds for each item, so the case's own metrics carry what that
|
|
@@ -5986,6 +6129,24 @@ function localSteps(doc , ctx ) {
|
|
|
5986
6129
|
return next;
|
|
5987
6130
|
}
|
|
5988
6131
|
|
|
6132
|
+
/**
|
|
6133
|
+
* Version 17 to 18 (#596): Saved response pins one response. A Source content
|
|
6134
|
+
* graded as saved -- `live` false, or unset over a Source whose default is
|
|
6135
|
+
* saved, which [ctx] says -- pins the Source's newest successful response
|
|
6136
|
+
* (owner), so a Source holding several grades one where it graded all. Live
|
|
6137
|
+
* data, a content that is not a Source's, or one [ctx] cannot say of, pins
|
|
6138
|
+
* none; a run document carries its files, and needs none.
|
|
6139
|
+
*/
|
|
6140
|
+
function savedPinned(next , ctx ) {
|
|
6141
|
+
next.version = PIPELINE_VERSION;
|
|
6142
|
+
const content = contentOf(next ) ;
|
|
6143
|
+
if (!isObj(content) || content.type !== "source" || content.live === true || content.pin != null
|
|
6144
|
+
|| isObj(next.profiles) || !isRef(content.ref) || !ctx.pinFor) return next;
|
|
6145
|
+
const pin = ctx.pinFor(content.ref.id, typeof content.live === "boolean" ? content.live : undefined);
|
|
6146
|
+
if (pin) content.pin = { name: pin.name, n: pin.n };
|
|
6147
|
+
return next;
|
|
6148
|
+
}
|
|
6149
|
+
|
|
5989
6150
|
/**
|
|
5990
6151
|
* Version 16 to 17 (#477): a request step has no HTTP API of its own; its
|
|
5991
6152
|
* job's flow step says it. A step that named another -- Any endpoint over an
|
|
@@ -6439,7 +6600,11 @@ function resolvePipeline(doc ,
|
|
|
6439
6600
|
run.profiles[key] = { ...conn, connection: cid };
|
|
6440
6601
|
}
|
|
6441
6602
|
const content = contentOf(run) ;
|
|
6442
|
-
|
|
6603
|
+
// Saved response grades its one pinned response (#596), whatever else the
|
|
6604
|
+
// Source holds; the server keeps that response's bytes with the run.
|
|
6605
|
+
if (content?.type === "source") {
|
|
6606
|
+
content.files = CONTENT_TYPES.source .expand (content, isObj(content.pin) && content.live !== true ? [content.pin.name] : ctx.files);
|
|
6607
|
+
}
|
|
6443
6608
|
// Each Library group the evals read, as the version it grades with: the
|
|
6444
6609
|
// body's fingerprint and, where the lab numbers them, `n` -- a pinned
|
|
6445
6610
|
// link's pin.
|
|
@@ -6706,7 +6871,8 @@ const BUNDLE_DATASET = /^datasets\/([a-z0-9]+(?:-[a-z0-9]+)*)\.json$/;
|
|
|
6706
6871
|
* [doc] as a bundle's text files: `pipeline.yaml`, `profiles.yaml` and
|
|
6707
6872
|
* `datasets/<slug>.json`. [ctx.connections] and [ctx.connectionProfiles]
|
|
6708
6873
|
* are Setup's -- keys and all, none of which is written -- or
|
|
6709
|
-
* [ctx.profiles], Target profiles to split as the store was
|
|
6874
|
+
* [ctx.profiles], Target profiles to split as the store was, and
|
|
6875
|
+
* [ctx.profileDefault] (Default)'s settings; [ctx.grader]
|
|
6710
6876
|
* the lab's grader, written into each eval that would have asked it, and
|
|
6711
6877
|
* [ctx.datasets] each graded dataset by id, as `{ name, body }`. Each
|
|
6712
6878
|
* connection asked is written resolved, under its answer key with the
|
|
@@ -6715,12 +6881,13 @@ const BUNDLE_DATASET = /^datasets\/([a-z0-9]+(?:-[a-z0-9]+)*)\.json$/;
|
|
|
6715
6881
|
* something else.
|
|
6716
6882
|
*/
|
|
6717
6883
|
function exportBundle(doc , ctx
|
|
6718
|
-
|
|
6719
|
-
|
|
6884
|
+
|
|
6885
|
+
= {})
|
|
6720
6886
|
{
|
|
6721
6887
|
const split = (ctx.profiles ?? []).map(splitProfile);
|
|
6722
6888
|
const profiles = withSlugs([...(ctx.connections ?? []), ...split.map(x => x.connection)] );
|
|
6723
|
-
const answers = answersOf(profiles, [...(ctx.connectionProfiles ?? []), ...split.flatMap(x => (x.profile ? [x.profile] : []))]
|
|
6889
|
+
const answers = answersOf(profiles, [...(ctx.connectionProfiles ?? []), ...split.flatMap(x => (x.profile ? [x.profile] : []))],
|
|
6890
|
+
ctx.profileDefault);
|
|
6724
6891
|
// What the lab would supply at submit, written down: no lab supplies it later.
|
|
6725
6892
|
const out = clone(doc) ;
|
|
6726
6893
|
out.evals = (Array.isArray(out.evals) ? out.evals : [])
|
|
@@ -6896,7 +7063,7 @@ function stagesFor(run , i , resolveFilters
|
|
|
6896
7063
|
// without a resolver, a Library link reads nothing, a private one its own.
|
|
6897
7064
|
return { text: targetStepOf(run, i, k) .prompt, kind, withImage: sendsImage(ch),
|
|
6898
7065
|
verbatim: !!targetEntryOf(run, i, k)?.verbatim, before: beforeFilters(ch, resolveFilters),
|
|
6899
|
-
modifiers: stageModifiers(ch, resolveFilters), settings };
|
|
7066
|
+
after: afterFilters(ch, resolveFilters), modifiers: stageModifiers(ch, resolveFilters), settings };
|
|
6900
7067
|
});
|
|
6901
7068
|
// Each named by the connection whose key it is sent with.
|
|
6902
7069
|
const asked = (key , held ) => {
|
|
@@ -7178,8 +7345,10 @@ function validateEvals(ev , files
|
|
|
7178
7345
|
continue;
|
|
7179
7346
|
}
|
|
7180
7347
|
if (scored.some(m => m.type === "refused" && !m.not)) {
|
|
7181
|
-
// Nor does a call that was refused: there is no answer.
|
|
7182
|
-
|
|
7348
|
+
// Nor does a call that was refused: there is no answer. Scored in
|
|
7349
|
+
// points, the other metrics are alternatives rather than more to hold
|
|
7350
|
+
// (#560: refused, or the reply says it can't read the file).
|
|
7351
|
+
if (scored.length > 1 && ev.scoring?.mode !== "weighted") say("expects its call refused, and states something the answer should hold too");
|
|
7183
7352
|
continue;
|
|
7184
7353
|
}
|
|
7185
7354
|
const wants = scored.filter(m => !m.not && (m.type === "contains-all" || m.type === "contains-any"));
|
|
@@ -7304,12 +7473,12 @@ export {
|
|
|
7304
7473
|
SOURCE_TYPES, DEFAULT_SOURCE_TYPE, sourceTypeOf, WORKFLOW_PLATFORMS, platformOf, DYNAMIC_PROVIDERS, providerOf, WIZARDS, wizardProgress,
|
|
7305
7474
|
registerKinds, defaultKind, jobDefaults, applyModifiers, keyVar, slugOf, uniqueSlug, isSlug, withSlugs, SLUG_MAX, connectionOf, addressOf, connectionSettings,
|
|
7306
7475
|
splitProfile, mergeAnswer, answerKey, answerOfKey, answersOf, runAnswer, answerConnectionId, connectionScoped,
|
|
7307
|
-
connectionRequest, connectionBase, connectionSend, connectionReply, connectionReplyProblem, connectionImage, connectionProblems,
|
|
7476
|
+
connectionRequest, connectionBase, connectionSend, connectionReply, connectionReplyProblem, replyFinish, contentJoined, connectionImage, connectionProblems,
|
|
7308
7477
|
CONNECTION_FIELDS, SETTING_KEYS, OPTION_FIELDS, jobLabel, scenarioLabel, jobWords, versionProblem,
|
|
7309
7478
|
contentOf, withContent, replyOf,
|
|
7310
7479
|
targetsOf, targetLetter, targetLabel, targetStepOf, targetEntryOf, jobTargetType, targetAnswerOf, targetAnswerKey, recordedTargetOf, withTarget,
|
|
7311
7480
|
tokensOf, withTokens, sendsImage, withImage, flowStepOf, requestStepOf, readAsOf, outOf, withOut, slotEntry, SLOTS,
|
|
7312
|
-
filterSetsOf, withFilterSets, stageModifiers, beforeFilters, gradedText,
|
|
7481
|
+
filterSetsOf, withFilterSets, stageModifiers, beforeFilters, afterFilters, gradedText,
|
|
7313
7482
|
blankPipeline, upgradePipeline, fatProfileRef, newId, newEval, newLink, validatePipeline, resolvePipeline, pipelineOfRun, stagesFor, profileIds, answerKeys,
|
|
7314
7483
|
scenarioProfile, lastKind, lastModifier, modifierSummary, itemScores, scenarioEvals, scenarioPasses, groupPass, overallVerdict, passVerdict, isSkipped, failedScore,
|
|
7315
7484
|
evalLabel, evalsOf, evalsDataset, casesRef, groupOf, groupOfMetrics, pipelineWarnings, itemsFound, previewOf, isWholeRun, EVAL_FIELDS,
|
package/lab/flows/record.mjs
CHANGED
|
@@ -153,11 +153,12 @@ export function skeletonFor(step , reads ) {
|
|
|
153
153
|
call of the step at that iteration: imported twice, it is skipped the
|
|
154
154
|
second time, while another run's call -- the same request, sent again,
|
|
155
155
|
answered afresh -- is its own record, since a flow that sends the same
|
|
156
|
-
words every time is exactly where production's replies differ.
|
|
157
|
-
|
|
158
|
-
same
|
|
156
|
+
words every time is exactly where production's replies differ. An edited
|
|
157
|
+
call keeps its run's key, so Import runs skips it too (#587). One written
|
|
158
|
+
by hand or uploaded is the same as another that read and sent the same
|
|
159
|
+
things. Mirrored by server.py's record_key. */
|
|
159
160
|
export function recordKey(r ) {
|
|
160
|
-
if (r.origin?.
|
|
161
|
+
if (r.origin?.run) {
|
|
161
162
|
return JSON.stringify({ step: r.step, run: r.origin.run, iteration: r.origin.iteration ?? "" });
|
|
162
163
|
}
|
|
163
164
|
const sorted = (v ) => Array.isArray(v) ? v.map(sorted)
|