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