evals-lab 0.15.0 → 0.16.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 +53 -0
- package/README.md +35 -14
- package/bin/evals-lab.js +8 -1
- package/bin/presets.js +234 -0
- package/lab/VERSION +1 -1
- package/lab/demo/pipelines/demo-1.json +105 -106
- package/lab/demo/pipelines/demo-2.json +105 -106
- package/lab/demo/pipelines/documents.json +54 -51
- package/lab/demo/pipelines/sentiment.json +30 -30
- package/lab/demo/pipelines/weather-alerts.json +35 -35
- package/lab/demo/pipelines/wordings.json +33 -36
- package/lab/evals-core.mjs +815 -194
- package/lab/kinds/list.mjs +2 -2
- package/lab/metrics/builtin.mjs +24 -24
- package/lab/run-evals.js +34 -56
- package/lab/server.py +350 -180
- package/lab/web/dist/assets/gallery-ICJNZQIZ.js +5 -0
- package/lab/web/dist/assets/{gallery-eVJoaK-6.css → gallery-bG2vNzKT.css} +1 -1
- package/lab/web/dist/assets/main-BOkQWswU.css +1 -0
- package/lab/web/dist/assets/main-rJDsPFWT.js +18 -0
- package/lab/web/dist/assets/tokens-C5ist-xG.js +64 -0
- package/lab/web/dist/assets/tokens-CuK6hUMK.css +1 -0
- package/lab/web/dist/gallery.html +4 -4
- package/lab/web/dist/index.html +4 -4
- package/package.json +1 -1
- package/lab/presets.json +0 -62
- package/lab/web/dist/assets/gallery-Cy8oycht.js +0 -5
- package/lab/web/dist/assets/main-B-LGmhPu.css +0 -1
- package/lab/web/dist/assets/main-wqNvXpWp.js +0 -15
- package/lab/web/dist/assets/tokens-CWVzmtid.css +0 -1
- package/lab/web/dist/assets/tokens-mCS6IZGh.js +0 -64
package/lab/kinds/list.mjs
CHANGED
|
@@ -454,7 +454,7 @@ export const FILTER_TYPES = {
|
|
|
454
454
|
},
|
|
455
455
|
},
|
|
456
456
|
"reject": {
|
|
457
|
-
label: "Reject", about: "Discard the
|
|
457
|
+
label: "Reject", about: "Discard the response when it matches its conditions.", icon: "trash",
|
|
458
458
|
matches: "reply", conditions: ["blank", "regex", "itemCount", "share", "items"], rejects: true,
|
|
459
459
|
apply: (filter, reply, on) => ({ body: reply.body, items: reply.items, dropped: [],
|
|
460
460
|
reject: rejectFires(filter.match, reply.body, on === "text" ? oneItem(reply.body) : reply.items) ? filter.id : null }),
|
|
@@ -833,7 +833,7 @@ const LIST_MODIFIERS = {
|
|
|
833
833
|
|
|
834
834
|
const LIST_KIND = {
|
|
835
835
|
label: "List", noun: "items", offers: ["items"],
|
|
836
|
-
description: "Splits the
|
|
836
|
+
description: "Splits the response into items (by commas, lines or a JSON array) that evals and later jobs read one by one.",
|
|
837
837
|
settings: [
|
|
838
838
|
{ key: "parse", label: "Parse as", type: "select", choices: [
|
|
839
839
|
{ value: "csv", label: "CSV" }, { value: "lines", label: "One a line" }, { value: "json", label: "JSON array" }] },
|
package/lab/metrics/builtin.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export function atPath(v , path ) {
|
|
|
37
37
|
|
|
38
38
|
/** Where [v] breaks [schema] -- the parts of JSON Schema a Parse JSON uses --
|
|
39
39
|
or null where it holds. */
|
|
40
|
-
export function schemaProblem(v , schema , at = "the
|
|
40
|
+
export function schemaProblem(v , schema , at = "the response") {
|
|
41
41
|
if (!schema || typeof schema !== "object") return null;
|
|
42
42
|
const s = schema ;
|
|
43
43
|
const type = s.type;
|
|
@@ -122,7 +122,7 @@ const spoken = (g ) => (g.length === 1 ? g[0] : g);
|
|
|
122
122
|
|
|
123
123
|
/** The recorded reply, or the reason a metric that compares with it cannot. */
|
|
124
124
|
const recorded = (input ) => {
|
|
125
|
-
if (input.recorded == null) throw new Error("this item has no recorded
|
|
125
|
+
if (input.recorded == null) throw new Error("this item has no recorded response to compare with");
|
|
126
126
|
return input.recorded;
|
|
127
127
|
};
|
|
128
128
|
|
|
@@ -132,7 +132,7 @@ const recorded = (input ) => {
|
|
|
132
132
|
function verdictOf(said ) {
|
|
133
133
|
const m = /\{[\s\S]*\}/.exec(said);
|
|
134
134
|
const read = m ? json(m[0]) : { error: "no JSON" };
|
|
135
|
-
if (read.error || !read.value || typeof read.value !== "object") throw new Error(`the grader's
|
|
135
|
+
if (read.error || !read.value || typeof read.value !== "object") throw new Error(`the grader's response could not be read: ${short(said)}`);
|
|
136
136
|
return read.value ;
|
|
137
137
|
}
|
|
138
138
|
/** The grader's verdict on [prompt], its reply held to [schema] where its API
|
|
@@ -203,13 +203,13 @@ const caseSaid = (m ) => (m.ignoreCase === true ? "" : "matching
|
|
|
203
203
|
const phrase = (...parts ) => parts.filter(Boolean).join(", ");
|
|
204
204
|
|
|
205
205
|
// The families the Add metric picker lists the metrics under: what each reads.
|
|
206
|
-
const TEXT = "The
|
|
206
|
+
const TEXT = "The response's text", RESULT = "The result", RECORDED = "Recorded response", GRADED = "Model-graded";
|
|
207
207
|
|
|
208
208
|
const metrics = {
|
|
209
209
|
equals: {
|
|
210
210
|
family: TEXT,
|
|
211
211
|
label: "Equals",
|
|
212
|
-
description: "Passes when the
|
|
212
|
+
description: "Passes when the response is exactly the value; as JSON, the same value with keys in any order.",
|
|
213
213
|
// As JSON, a reply equals the value as a value -- keys in any order;
|
|
214
214
|
// otherwise, as the text, once trimmed.
|
|
215
215
|
options: [{ key: "value", label: "Value", type: "textarea" }, { key: "json", label: "As JSON", type: "checkbox" }],
|
|
@@ -225,7 +225,7 @@ const metrics = {
|
|
|
225
225
|
contains: {
|
|
226
226
|
family: TEXT,
|
|
227
227
|
label: "Contains",
|
|
228
|
-
description: "Passes when the
|
|
228
|
+
description: "Passes when the response contains the value; turned round, an exception excuses the value inside it.",
|
|
229
229
|
options: [{ key: "value", label: "Value", type: "text" }, { key: "ignoreCase", label: "Ignore case", type: "checkbox" },
|
|
230
230
|
{ key: "except", label: "Except", type: "textarea" }],
|
|
231
231
|
defaults: () => ({ value: "", ignoreCase: true }),
|
|
@@ -243,7 +243,7 @@ const metrics = {
|
|
|
243
243
|
"contains-any": {
|
|
244
244
|
family: TEXT,
|
|
245
245
|
label: "Contains any",
|
|
246
|
-
description: "Passes when the
|
|
246
|
+
description: "Passes when the response contains at least one of the values, one a line.",
|
|
247
247
|
options: [{ key: "values", label: "Values", type: "textarea" }, { key: "ignoreCase", label: "Ignore case", type: "checkbox" }],
|
|
248
248
|
defaults: () => ({ values: "", ignoreCase: true }),
|
|
249
249
|
says: (m) => phrase(quotedAll(lines(m.values)), caseSaid(m)),
|
|
@@ -258,7 +258,7 @@ const metrics = {
|
|
|
258
258
|
"contains-all": {
|
|
259
259
|
family: TEXT,
|
|
260
260
|
label: "Contains all",
|
|
261
|
-
description: "Passes when the
|
|
261
|
+
description: "Passes when the response contains every value, one a line.",
|
|
262
262
|
options: [{ key: "values", label: "Values", type: "textarea" }, { key: "ignoreCase", label: "Ignore case", type: "checkbox" }],
|
|
263
263
|
defaults: () => ({ values: "", ignoreCase: true }),
|
|
264
264
|
says: (m) => phrase(quotedAll(lines(m.values), "and"), caseSaid(m)),
|
|
@@ -274,7 +274,7 @@ const metrics = {
|
|
|
274
274
|
regex: {
|
|
275
275
|
family: TEXT,
|
|
276
276
|
label: "Matches",
|
|
277
|
-
description: "Passes when the
|
|
277
|
+
description: "Passes when the response matches the regular expression.",
|
|
278
278
|
options: [{ key: "pattern", label: "Pattern", type: "text" }, { key: "flags", label: "Flags", type: "text" }],
|
|
279
279
|
defaults: () => ({ pattern: "", flags: "" }),
|
|
280
280
|
says: (m) => `/${str(m.pattern)}/${str(m.flags)}`,
|
|
@@ -287,7 +287,7 @@ const metrics = {
|
|
|
287
287
|
"starts-with": {
|
|
288
288
|
family: TEXT,
|
|
289
289
|
label: "Starts with",
|
|
290
|
-
description: "Passes when the
|
|
290
|
+
description: "Passes when the response starts with the value.",
|
|
291
291
|
options: [{ key: "value", label: "Value", type: "text" }],
|
|
292
292
|
defaults: () => ({ value: "" }),
|
|
293
293
|
says: (m) => quoted(str(m.value)),
|
|
@@ -300,7 +300,7 @@ const metrics = {
|
|
|
300
300
|
"is-json": {
|
|
301
301
|
family: TEXT,
|
|
302
302
|
label: "Is JSON",
|
|
303
|
-
description: "Passes when the
|
|
303
|
+
description: "Passes when the response is JSON and, with a schema, holds to it.",
|
|
304
304
|
options: [{ key: "schema", label: "Schema", type: "textarea" }],
|
|
305
305
|
defaults: () => ({ schema: "" }),
|
|
306
306
|
validate: (m, at, bad) => { if (str(m.schema).trim() && json(str(m.schema)).error) bad.push(`${at}: the schema is not JSON`); },
|
|
@@ -315,7 +315,7 @@ const metrics = {
|
|
|
315
315
|
"json-field": {
|
|
316
316
|
family: TEXT,
|
|
317
317
|
label: "JSON field",
|
|
318
|
-
description: "Reads one field of a JSON
|
|
318
|
+
description: "Reads one field of a JSON response and compares it.",
|
|
319
319
|
options: [
|
|
320
320
|
{ key: "path", label: "Field", type: "text" },
|
|
321
321
|
{ key: "op", label: "Is", type: "select", choices: [
|
|
@@ -348,7 +348,7 @@ const metrics = {
|
|
|
348
348
|
family: RESULT,
|
|
349
349
|
readsResult: true,
|
|
350
350
|
label: "Discarded",
|
|
351
|
-
description: "Passes only when the job's rules discarded the
|
|
351
|
+
description: "Passes only when the job's rules discarded the response.",
|
|
352
352
|
options: [],
|
|
353
353
|
defaults: () => ({}),
|
|
354
354
|
score: (input) => {
|
|
@@ -364,7 +364,7 @@ const metrics = {
|
|
|
364
364
|
readsResult: true,
|
|
365
365
|
readsRefusal: true,
|
|
366
366
|
label: "Refused",
|
|
367
|
-
description: "Passes only when the
|
|
367
|
+
description: "Passes only when the variant's call was refused: an error from the connection, not a response.",
|
|
368
368
|
options: [],
|
|
369
369
|
defaults: () => ({}),
|
|
370
370
|
score: (input) => {
|
|
@@ -413,7 +413,7 @@ const metrics = {
|
|
|
413
413
|
family: RESULT,
|
|
414
414
|
readsResult: true,
|
|
415
415
|
label: "Latency",
|
|
416
|
-
description: "Passes when the
|
|
416
|
+
description: "Passes when the response came back within the time set.",
|
|
417
417
|
options: [{ key: "max", label: "At most (ms)", type: "number" }],
|
|
418
418
|
defaults: () => ({ max: 5000 }),
|
|
419
419
|
// Unset, it is scored against 0 ms, which validate refuses: no number to say.
|
|
@@ -424,7 +424,7 @@ const metrics = {
|
|
|
424
424
|
levenshtein: {
|
|
425
425
|
family: TEXT,
|
|
426
426
|
label: "Near",
|
|
427
|
-
description: "Passes when the
|
|
427
|
+
description: "Passes when the response is within a few edits of the value.",
|
|
428
428
|
options: [{ key: "value", label: "Value", type: "textarea" }, { key: "max", label: "Edits at most", type: "number" }],
|
|
429
429
|
defaults: () => ({ value: "", max: 5 }),
|
|
430
430
|
says: (m) => `within ${num(m.max) ?? 0} ${num(m.max) === 1 ? "edit" : "edits"} of ${quoted(str(m.value).trim())}`,
|
|
@@ -442,7 +442,7 @@ const metrics = {
|
|
|
442
442
|
family: RECORDED,
|
|
443
443
|
parsedOnly: true,
|
|
444
444
|
label: "Same as recorded",
|
|
445
|
-
description: "Passes when the
|
|
445
|
+
description: "Passes when the response is what production recorded for the same item.",
|
|
446
446
|
options: [],
|
|
447
447
|
defaults: () => ({}),
|
|
448
448
|
score: (input, _m, ctx) => {
|
|
@@ -457,7 +457,7 @@ const metrics = {
|
|
|
457
457
|
family: RECORDED,
|
|
458
458
|
parsedOnly: true,
|
|
459
459
|
label: "Fields as recorded",
|
|
460
|
-
description: "Passes when the fields listed are what the recorded
|
|
460
|
+
description: "Passes when the fields listed are what the recorded response had.",
|
|
461
461
|
options: [{ key: "fields", label: "Fields", type: "textarea" }],
|
|
462
462
|
defaults: () => ({ fields: "" }),
|
|
463
463
|
says: (m) => lines(m.fields).join(", "),
|
|
@@ -466,7 +466,7 @@ const metrics = {
|
|
|
466
466
|
if (ctx.recordedTarget) return null;
|
|
467
467
|
const a = json(input.text), b = json(recorded(input));
|
|
468
468
|
if (a.error) return ok(false, `not JSON: ${a.error}`);
|
|
469
|
-
if (b.error) return ok(false, "the recorded
|
|
469
|
+
if (b.error) return ok(false, "the recorded response was not JSON");
|
|
470
470
|
const fields = lines(m.fields);
|
|
471
471
|
const differ = fields.filter((f) => !same(atPath(a.value, f), atPath(b.value, f)));
|
|
472
472
|
return ok(!differ.length, differ.length ? `${differ.join(", ")} differ from recorded` : "the fields match recorded",
|
|
@@ -477,7 +477,7 @@ const metrics = {
|
|
|
477
477
|
family: RECORDED,
|
|
478
478
|
parsedOnly: true,
|
|
479
479
|
label: "Parses as recorded",
|
|
480
|
-
description: "Passes when the
|
|
480
|
+
description: "Passes when the response parses as JSON exactly when the recorded response did.",
|
|
481
481
|
options: [],
|
|
482
482
|
defaults: () => ({}),
|
|
483
483
|
score: (input, _m, ctx) => {
|
|
@@ -490,7 +490,7 @@ const metrics = {
|
|
|
490
490
|
"llm-rubric": {
|
|
491
491
|
family: GRADED,
|
|
492
492
|
label: "Rubric",
|
|
493
|
-
description: "Asks the grader whether the
|
|
493
|
+
description: "Asks the grader whether the response meets the rubric.",
|
|
494
494
|
graded: true,
|
|
495
495
|
parsedOnly: true,
|
|
496
496
|
options: [{ key: "rubric", label: "Rubric", type: "textarea" }, { key: "threshold", label: "Expected: score at least", type: "number" }],
|
|
@@ -504,12 +504,12 @@ const metrics = {
|
|
|
504
504
|
factuality: {
|
|
505
505
|
family: GRADED,
|
|
506
506
|
label: "Factual",
|
|
507
|
-
description: "Asks the grader whether the
|
|
507
|
+
description: "Asks the grader whether the response agrees with the reference, or the recorded response.",
|
|
508
508
|
graded: true,
|
|
509
509
|
parsedOnly: true,
|
|
510
510
|
options: [{ key: "reference", label: "Reference", type: "textarea" }, { key: "threshold", label: "Expected: score at least", type: "number" }],
|
|
511
511
|
defaults: () => ({ reference: "", threshold: 0.5 }),
|
|
512
|
-
says: (m) => phrase(str(m.reference).trim() ? `with ${quoted(str(m.reference).replace(/\s+/g, " ").trim())}` : "with the recorded
|
|
512
|
+
says: (m) => phrase(str(m.reference).trim() ? `with ${quoted(str(m.reference).replace(/\s+/g, " ").trim())}` : "with the recorded response",
|
|
513
513
|
num(m.threshold) != null && num(m.threshold) !== 0.5 ? `score at least ${num(m.threshold)}` : ""),
|
|
514
514
|
// A blank reference is the recorded reply.
|
|
515
515
|
score: async (input, m, ctx) => gradedPass(await graded(ctx,
|
|
@@ -520,7 +520,7 @@ const metrics = {
|
|
|
520
520
|
"judge-vs-production": {
|
|
521
521
|
family: GRADED,
|
|
522
522
|
label: "Judged against recorded",
|
|
523
|
-
description: "Asks the grader whether the
|
|
523
|
+
description: "Asks the grader whether the response is better than, the same as or worse than the recorded one.",
|
|
524
524
|
graded: true,
|
|
525
525
|
parsedOnly: true,
|
|
526
526
|
expects: true,
|
package/lab/run-evals.js
CHANGED
|
@@ -628,7 +628,7 @@ function callsFor(connections, links, text, plan, record) {
|
|
|
628
628
|
// one, and the loop job 1's flow step sits in.
|
|
629
629
|
const tokenRecord = (run, record, text) => {
|
|
630
630
|
const r = record ?? (text != null ? textRecord(text) : null);
|
|
631
|
-
return r && { scope: r.scope || {}, at: r.at ?? null, loop: core.flowStepOf(run
|
|
631
|
+
return r && { scope: r.scope || {}, at: r.at ?? null, loop: core.flowStepOf(core.jobsOf(run)[0])?.loop ?? null };
|
|
632
632
|
};
|
|
633
633
|
|
|
634
634
|
// A prompt as a report restates it: an expression token reads an item's
|
|
@@ -768,27 +768,6 @@ function readUtf8(file) {
|
|
|
768
768
|
const outcomesOf = (run, items, settled) =>
|
|
769
769
|
core.targetsOf(run).map((_, i) => core.scenarioEvals(run, i, items, settled));
|
|
770
770
|
|
|
771
|
-
// A link's Whole run verdict, per Target, keyed by the eval's id: a Library
|
|
772
|
-
// group's `run` metrics read over the replies so far (readGroupRun). A private
|
|
773
|
-
// group's Whole run is scenarioEvals' own (it holds the body); a link's body
|
|
774
|
-
// is held apart, in --groups, so reading it beside the link's items is the
|
|
775
|
-
// runner's (§17). [resolve] gives a link's body by its reference.
|
|
776
|
-
function wholeRunsOf(run, items, resolve) {
|
|
777
|
-
const kind = core.lastKind(run);
|
|
778
|
-
return core.targetsOf(run).map((_, i) => {
|
|
779
|
-
const out = {};
|
|
780
|
-
const ress = (items || []).map(it => (it && !it.unrun ? it.scenarios?.[i]?.res : undefined));
|
|
781
|
-
for (const t of core.evalsOf(run)) {
|
|
782
|
-
// A private whole-run group is already scenarioEvals' verdict; here is
|
|
783
|
-
// the run beside a link's (or private group's) own items.
|
|
784
|
-
if (t.type !== "group" || core.isWholeRun(core.EVAL_TYPES.group, t)) continue;
|
|
785
|
-
const body = core.groupOf(t, { group: resolve });
|
|
786
|
-
if (body && Array.isArray(body.run) && body.run.length) out[t.id] = core.readGroupRun(body, ress, kind);
|
|
787
|
-
}
|
|
788
|
-
return out;
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
771
|
/**
|
|
793
772
|
* One eval's verdict on one target, for the build: its own rule -- every
|
|
794
773
|
* item it read passed, or a whole-run eval's verdict -- with [minPass] the
|
|
@@ -818,7 +797,7 @@ function evalVerdict(o, settled, minPass, wholeRun) {
|
|
|
818
797
|
// Each target's evals, keyed by the eval's id: a whole-run eval's verdict, a
|
|
819
798
|
// per-item eval's counts -- and, where a link reads a Whole run beside its
|
|
820
799
|
// items, that verdict too -- with the build's verdict on each. [wholes] is
|
|
821
|
-
// wholeRunsOf, one map per target.
|
|
800
|
+
// core.wholeRunsOf, one map per target.
|
|
822
801
|
const verdictsOf = (outcomes, settled, minPass, wholes = []) => outcomes.map((evals, i) =>
|
|
823
802
|
Object.fromEntries(evals.map(o => {
|
|
824
803
|
const wr = wholes[i] && wholes[i][o.id];
|
|
@@ -911,32 +890,30 @@ const scenariosOf = run => core.targetsOf(run).map((sc, i) => {
|
|
|
911
890
|
async function runSnapshot(o) {
|
|
912
891
|
const gctx = gradingContext(o);
|
|
913
892
|
const run = readRun(o.run, gctx);
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
if (!bare && !files.length && !text.trim()) {
|
|
922
|
-
broken(`${o.run} names no files and no text, so there is nothing to run`);
|
|
923
|
-
}
|
|
893
|
+
// Each Get items step's items in turn: a Source's files, a text the
|
|
894
|
+
// pipeline holds itself, or Prompt only's bare one.
|
|
895
|
+
const listed = core.runItemsOf(run);
|
|
896
|
+
if (!listed.length) broken(`${o.run} names no files and no text, so there is nothing to run`);
|
|
897
|
+
// A row keeps the Get items step it came from when there are several, so
|
|
898
|
+
// Results groups its rows by Source; a run over one reads as it always did.
|
|
899
|
+
const several = core.itemContents(run).length > 1;
|
|
924
900
|
|
|
925
901
|
// The one group a single-group run names, for an eval type of its own that
|
|
926
902
|
// reads a case; a `group` eval reads each group's case by the item's name.
|
|
927
903
|
const graded = gradedBy(gctx.primary());
|
|
928
904
|
|
|
929
|
-
const items =
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
905
|
+
const items = listed.map(({ name, text, bare, dir, step }) => {
|
|
906
|
+
const at = { dir, ...(several ? { step } : {}) };
|
|
907
|
+
if (bare) return { name: null, kind: "text", text: null, bare: true, ...at };
|
|
908
|
+
if (name == null) return { name: null, kind: "text", text, ...at };
|
|
909
|
+
const ext = path.extname(name).toLowerCase();
|
|
910
|
+
if (RUN_IMAGE_TYPES.has(ext)) return { name, kind: "image", ...at };
|
|
911
|
+
if (core.documentTypeOf(name)) return { name, kind: "document", ...at };
|
|
912
|
+
// A record (flows/record.ts): one call of a Power Automate step, what
|
|
913
|
+
// an HTTP Request is rebuilt from.
|
|
914
|
+
if (ext === ".json") return { name, kind: "record", ...at };
|
|
915
|
+
return { name, kind: "text", unmapped: !RUN_TEXT_TYPES.has(ext), ...at };
|
|
916
|
+
});
|
|
940
917
|
const total = items.length;
|
|
941
918
|
// Only the run's own image items need ImageMagick: a text-only Source runs
|
|
942
919
|
// with nothing on PATH but the node running this.
|
|
@@ -987,8 +964,9 @@ async function runSnapshot(o) {
|
|
|
987
964
|
let failed = null;
|
|
988
965
|
for (const plan of plans) {
|
|
989
966
|
let dataUrl = null;
|
|
967
|
+
// A later Get items' files are kept under its own folder (itemContents).
|
|
968
|
+
const file = item.name != null ? path.join(o.source, item.dir, item.name) : null;
|
|
990
969
|
if (item.kind === "image") {
|
|
991
|
-
const file = path.join(o.source, item.name);
|
|
992
970
|
if (!fs.existsSync(file)) { failed = `the file ${item.name} is not in the source`; break; }
|
|
993
971
|
try {
|
|
994
972
|
({ dataUrl } = prepare(bin, file, plan.connections[0]));
|
|
@@ -998,13 +976,11 @@ async function runSnapshot(o) {
|
|
|
998
976
|
}
|
|
999
977
|
}
|
|
1000
978
|
if (item.kind === "document") {
|
|
1001
|
-
const file = path.join(o.source, item.name);
|
|
1002
979
|
if (!fs.existsSync(file)) { failed = `the file ${item.name} is not in the source`; break; }
|
|
1003
980
|
dataUrl = documentOf(file);
|
|
1004
981
|
}
|
|
1005
982
|
let record = null;
|
|
1006
983
|
if (item.kind === "record") {
|
|
1007
|
-
const file = path.join(o.source, item.name);
|
|
1008
984
|
try { record = JSON.parse(readUtf8(file)); } catch (e) {
|
|
1009
985
|
failed = fs.existsSync(file) ? `${item.name} is not a record -- ${e.message}` : `the file ${item.name} is not in the source`;
|
|
1010
986
|
break;
|
|
@@ -1012,12 +988,12 @@ async function runSnapshot(o) {
|
|
|
1012
988
|
}
|
|
1013
989
|
// A text file the Source does not hold is unrun, as an image is: the
|
|
1014
990
|
// run is incomplete rather than broken.
|
|
1015
|
-
if (item.kind === "text" && !item.bare && item.text == null && !fs.existsSync(
|
|
991
|
+
if (item.kind === "text" && !item.bare && item.text == null && !fs.existsSync(file)) {
|
|
1016
992
|
failed = `the file ${item.name} is not in the source`;
|
|
1017
993
|
break;
|
|
1018
994
|
}
|
|
1019
995
|
const textOf = item.kind === "text" && !item.bare
|
|
1020
|
-
? (item.text != null ? item.text : readUtf8(
|
|
996
|
+
? (item.text != null ? item.text : readUtf8(file)) : null;
|
|
1021
997
|
const res = await core.runPipeline(plan.stages, dataUrl, callsFor(plan.connections, plan.links, textOf, plan, record),
|
|
1022
998
|
{ tokens: plan.tokens, text: textOf, record: tokenRecord(run, record, textOf) });
|
|
1023
999
|
// A case is found by its file's name, whatever kind of file it is: a
|
|
@@ -1029,7 +1005,8 @@ async function runSnapshot(o) {
|
|
|
1029
1005
|
}
|
|
1030
1006
|
// Production's reply to the item, where its record holds one: what a
|
|
1031
1007
|
// metric compares with, kept so a re-score reads it again.
|
|
1032
|
-
const out = { item: i, name: item.name, kind: item.kind, ...(
|
|
1008
|
+
const out = { item: i, name: item.name, kind: item.kind, ...(item.step ? { getItems: item.step } : {}),
|
|
1009
|
+
...(production != null ? { production } : {}),
|
|
1033
1010
|
unmapped: item.unmapped || null, ms: Date.now() - t0,
|
|
1034
1011
|
scenarios: sides, ...(failed ? { unrun: failed } : {}) };
|
|
1035
1012
|
results[i] = out;
|
|
@@ -1044,7 +1021,7 @@ async function runSnapshot(o) {
|
|
|
1044
1021
|
const complete = !cancelled && !unrun.length
|
|
1045
1022
|
&& ranItems.length === total && ranItems.every(it => it && !it.unrun);
|
|
1046
1023
|
const outcomes = outcomesOf(run, ranItems, complete);
|
|
1047
|
-
const wholes = wholeRunsOf(run, ranItems, gctx.resolve);
|
|
1024
|
+
const wholes = core.wholeRunsOf(run, ranItems, gctx.resolve);
|
|
1048
1025
|
const verdicts = verdictsOf(outcomes, complete, o.minPass, wholes);
|
|
1049
1026
|
const report = {
|
|
1050
1027
|
runner: RUNNER,
|
|
@@ -1139,7 +1116,7 @@ async function runRescore(o){
|
|
|
1139
1116
|
// A stored run that stopped short re-scores as what it is: incomplete.
|
|
1140
1117
|
const complete = items.every(it => isObject(it) && !it.unrun);
|
|
1141
1118
|
const outcomes = outcomesOf(run, items, complete);
|
|
1142
|
-
const wholes = wholeRunsOf(run, items, gctx.resolve);
|
|
1119
|
+
const wholes = core.wholeRunsOf(run, items, gctx.resolve);
|
|
1143
1120
|
const verdicts = verdictsOf(outcomes, complete, o.minPass, wholes);
|
|
1144
1121
|
const report = {
|
|
1145
1122
|
runner: RUNNER,
|
|
@@ -1172,17 +1149,18 @@ function cliRun(o, dataset) {
|
|
|
1172
1149
|
// Graded, so the job answers with a list: the configuration that reads a
|
|
1173
1150
|
// reply as this command always has (legacyTagsOut), under the dataset's
|
|
1174
1151
|
// rules. A --pipeline states its own.
|
|
1175
|
-
|
|
1152
|
+
let job = core.withOut(core.jobsOf(doc)[0], core.legacyTagsOut(rulesOf(dataset), "LOWER"));
|
|
1176
1153
|
if (o.tokens) {
|
|
1177
1154
|
try {
|
|
1178
|
-
|
|
1155
|
+
job = core.withTokens(job, JSON.parse(fs.readFileSync(o.tokens, "utf8")));
|
|
1179
1156
|
} catch (e) {
|
|
1180
1157
|
broken(`${o.tokens}: ${e.message}`);
|
|
1181
1158
|
}
|
|
1182
1159
|
}
|
|
1160
|
+
doc = core.withJobs(doc, [job]);
|
|
1183
1161
|
doc = core.withContent(doc, { type: "source", ref: { id: "cli", name: "" }, first: null, loops: 1, files: [] });
|
|
1184
|
-
doc
|
|
1185
|
-
steps: [{ type: "prompt", prompt: o.prompt ?? "" }] }];
|
|
1162
|
+
doc = core.withTargets(doc, [{ id: "cli", name: "", connection: { id: "cli", name: "" }, profile: null,
|
|
1163
|
+
steps: [{ type: "prompt", prompt: o.prompt ?? "" }] }]);
|
|
1186
1164
|
doc.evals = [{ id: "cli", type: "group", name: "", continueOnFailure: true, group: { id: "cli", name: "" }, pin: null }];
|
|
1187
1165
|
doc.profiles = { [core.answerKey("cli")]: { name: null, url: o.url, model: o.model, type: "openai-compatible" } };
|
|
1188
1166
|
return doc;
|