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.
Files changed (48) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +2 -2
  3. package/bin/run.js +4 -2
  4. package/lab/VERSION +1 -1
  5. package/lab/demo/CREDITS.md +14 -0
  6. package/lab/demo/datasets/demo-1.json +2 -2
  7. package/lab/demo/datasets/demo-2.json +2 -2
  8. package/lab/demo/datasets/documents-type.json +79 -0
  9. package/lab/demo/datasets/documents-valid.json +117 -0
  10. package/lab/demo/datasets/sentiment.json +2 -2
  11. package/lab/demo/datasets/weather-alerts.json +2 -2
  12. package/lab/demo/datasets/wordings.json +1 -1
  13. package/lab/demo/manifest.json +3 -3
  14. package/lab/demo/pipelines/demo-1.json +4 -4
  15. package/lab/demo/pipelines/demo-2.json +4 -4
  16. package/lab/demo/pipelines/documents.json +102 -0
  17. package/lab/demo/pipelines/sentiment.json +4 -4
  18. package/lab/demo/pipelines/weather-alerts.json +9 -5
  19. package/lab/demo/pipelines/wordings.json +3 -3
  20. package/lab/demo/sources/Demo 1/source.json +1 -1
  21. package/lab/demo/sources/Demo 2/source.json +1 -1
  22. package/lab/demo/sources/documents/invoice.pdf +65 -0
  23. package/lab/demo/sources/documents/letter.pdf +63 -0
  24. package/lab/demo/sources/documents/not-a-pdf.pdf +2 -0
  25. package/lab/demo/sources/documents/receipt.pdf +62 -0
  26. package/lab/demo/sources/documents/report.pdf +59 -0
  27. package/lab/demo/sources/documents/source.json +3 -0
  28. package/lab/demo/sources/documents/truncated.pdf +19 -0
  29. package/lab/demo/sources/sentiment/source.json +1 -1
  30. package/lab/demo/sources/weather-alerts/source.json +1 -1
  31. package/lab/evals-core.mjs +217 -48
  32. package/lab/flows/record.mjs +5 -4
  33. package/lab/kinds/list.mjs +72 -21
  34. package/lab/run-evals.js +3 -1
  35. package/lab/server.py +536 -85
  36. package/lab/web/dist/assets/gallery-qrf0ZGXK.js +5 -0
  37. package/lab/web/dist/assets/main-D6xARg2A.js +16 -0
  38. package/lab/web/dist/assets/main-xVU6J7Gi.css +1 -0
  39. package/lab/web/dist/assets/tokens-C8dqXDHN.js +62 -0
  40. package/lab/web/dist/assets/tokens-tQ7zTPkd.css +1 -0
  41. package/lab/web/dist/gallery.html +3 -3
  42. package/lab/web/dist/index.html +4 -4
  43. package/package.json +1 -1
  44. package/lab/web/dist/assets/gallery-DpaqOg9Z.js +0 -5
  45. package/lab/web/dist/assets/main-BSw-jyYD.css +0 -1
  46. package/lab/web/dist/assets/main-CrB8wPZn.js +0 -17
  47. package/lab/web/dist/assets/tokens-CrjDMCbz.js +0 -60
  48. package/lab/web/dist/assets/tokens-DDOONQon.css +0 -1
@@ -304,10 +304,10 @@ export const FILTER_SEPARATORS = ["comma", "lines", "semicolon", "none"]
304
304
 
305
305
 
306
306
 
307
-
308
-
309
-
310
-
307
+
308
+
309
+
310
+
311
311
 
312
312
 
313
313
 
@@ -342,32 +342,81 @@ function rewriteItem(op )
342
342
  return t => t;
343
343
  }
344
344
 
345
- /** The function a Rewrite text filter's `op` names: Remove thinking, Remove
346
- preamble, or Replace pattern (a regex, global, and its replacement). An
347
- unknown op leaves the text as it is; a pattern that will not compile throws,
348
- so Ignore failures can carry a run past it. */
349
- function rewriteText(op ) {
350
- if (op?.kind === "thinking") return removeThinking;
351
- if (op?.kind === "preamble") return removePreamble;
352
- if (op?.kind === "replace") {
353
- const re = new RegExp(String(op.pattern ?? ""), "g");
354
- const to = typeof op.replacement === "string" ? op.replacement : "";
355
- return text => text.replace(re, to);
345
+ /** The function a Rewrite text filter's `op` names (#557): Remove thinking,
346
+ Remove preamble, Trim, Replace text (literal, every occurrence), Replace
347
+ pattern (a regex, global, and its replacement), Strip code fences, Change
348
+ case, Keep first (N lines or characters), or Extract (a pattern's first
349
+ match, its first group if it has one). It answers null where nothing is
350
+ left to keep -- Extract found no match -- which rejects the reply. An
351
+ unknown op leaves the text as it is; a pattern that will not compile
352
+ throws, so Ignore failures can carry a run past it. */
353
+ function rewriteText(op ) {
354
+ switch (op?.kind) {
355
+ case "thinking": return removeThinking;
356
+ case "preamble": return removePreamble;
357
+ case "trim": return text => text.trim();
358
+ case "literal": {
359
+ const find = String(op.find ?? "");
360
+ const to = typeof op.replacement === "string" ? op.replacement : "";
361
+ return text => (find ? text.split(find).join(to) : text);
362
+ }
363
+ case "replace": {
364
+ const re = new RegExp(String(op.pattern ?? ""), "g");
365
+ const to = typeof op.replacement === "string" ? op.replacement : "";
366
+ return text => text.replace(re, to);
367
+ }
368
+ // A fence's own lines go -- the opening one with its language, and the
369
+ // closing one -- wherever they sit, so a reply fenced whole or a fence
370
+ // after a line of preamble both read as what was inside.
371
+ case "fences": return text => text.replace(FENCE_LINE, "");
372
+ case "case": { const mode = op.mode ; return text => applyCase(text, mode); }
373
+ case "first": {
374
+ const n = op.n ;
375
+ return op.unit === "chars" ? text => text.slice(0, n) : text => text.split("\n").slice(0, n).join("\n");
376
+ }
377
+ case "extract": {
378
+ const re = new RegExp(String(op.pattern ?? ""));
379
+ return text => { const m = re.exec(text); return m ? (m.length > 1 ? m[1] ?? "" : m[0]) : null; };
380
+ }
381
+ default: return text => text;
356
382
  }
357
- return text => text;
358
383
  }
359
384
 
385
+ const FENCE_LINE = /^[ \t]*```[^`\n]*(?:\n|$)/gm;
386
+
387
+ /** Keep first's units, for a Rewrite text: lines, or characters. */
388
+ const TEXT_UNITS = ["lines", "chars"] ;
389
+
390
+ /** Why a Rewrite text op's pattern does not compile, as a sentence, or none. */
391
+ const patternProblems = (pattern , at ) => {
392
+ if (typeof pattern !== "string" || !pattern) return [`${at} needs a pattern`];
393
+ try { new RegExp(pattern); return []; } catch { return [`${at}'s pattern does not compile`]; }
394
+ };
395
+
360
396
  /** The lab's base filter types (#405). Generic code asks this registry by
361
397
  `filter.type`; it never names an id. `rewrite-text`/`reject-text` read the
362
- raw reply before it is read into items (the Pre-format stage); the rest
363
- read the items after (the Post-format stage). */
398
+ reply's text: Pre-format, or Post-format after a kind that reads no items
399
+ (#557); the rest read the items, Post-format after a kind that does. */
364
400
  export const FILTER_TYPES = {
365
401
  "rewrite-text": {
366
- label: "Rewrite text", about: "Remove thinking, a preamble or a pattern from the reply's text.", scope: "text", conditions: [],
402
+ label: "Rewrite text", about: "Trim, replace, unfence, recase, shorten or extract from the reply's text.", scope: "text", conditions: [],
367
403
  opProblems: (f, at) => opKindProblems(f.op, at, {
368
- thinking: [], preamble: [], replace: ["pattern", "replacement"],
404
+ thinking: [], preamble: [], trim: [], literal: ["find", "replacement"], replace: ["pattern", "replacement"],
405
+ fences: [], case: ["mode"], first: ["n", "unit"], extract: ["pattern"],
406
+ }, op => {
407
+ if (op.kind === "literal" && (typeof op.find !== "string" || !op.find)) return [`${at} needs text to find`];
408
+ if (op.kind === "extract") return patternProblems(op.pattern, at);
409
+ if (op.kind === "case" && !CASE_MODES.includes(op.mode )) return [`${at}'s case needs a mode`];
410
+ if (op.kind === "first") {
411
+ return [...(Number.isInteger(op.n) && (op.n ) >= 1 ? [] : [`${at} needs a whole number of 1 or more`]),
412
+ ...(TEXT_UNITS.includes(op.unit ) ? [] : [`${at} keeps lines or characters`])];
413
+ }
414
+ return [];
369
415
  }),
370
- apply: (filter, reply) => ({ body: rewriteText(filter.op)(reply.body), items: reply.items, dropped: [], reject: null }),
416
+ apply(filter, reply) {
417
+ const body = rewriteText(filter.op)(reply.body);
418
+ return { body: body ?? reply.body, items: reply.items, dropped: [], reject: body == null ? filter.id : null };
419
+ },
371
420
  },
372
421
  "reject-text": {
373
422
  label: "Reject text", about: "Discard the reply when it is blank or matches a pattern.", scope: "text", conditions: ["blank", "regex"], rejects: true,
@@ -522,6 +571,8 @@ export function tryFilter(filter )
522
571
  return { test, ok: rejected === !test.expectedOutput.trim(), got: rejected ? "rejected" : (scope === "text" ? out.body : out.items.join(", ")) };
523
572
  }
524
573
  if (scope === "text") {
574
+ // A Rewrite text that rejects (Extract, matching nothing) leaves nothing.
575
+ if (out.reject != null) return { test, ok: !test.expectedOutput.trim(), got: "rejected" };
525
576
  const got = out.body.trim();
526
577
  return { test, ok: got === test.expectedOutput.trim(), got };
527
578
  }
package/lab/run-evals.js CHANGED
@@ -724,7 +724,9 @@ async function ask(to, c, prompt, dataUrl, schema = null) {
724
724
  if (!r.ok) return said({ error: j.error?.message || j.error || `HTTP ${r.status}` });
725
725
  const { raw, finishReason } = core.connectionReply(c, j);
726
726
  const error = core.connectionReplyProblem(c, raw, finishReason);
727
- return said(error ? { raw, error } : { raw });
727
+ // Why it stopped, and the limit sent where the limit stopped it (#597).
728
+ const finish = core.replyFinish(c, req.body, finishReason);
729
+ return said(error ? { raw, error, ...finish } : { raw, ...finish });
728
730
  } catch (e) {
729
731
  if (e?.name === "TimeoutError" || e?.name === "AbortError") {
730
732
  return said({ error: `the model gave no reply within ${seconds}s — timed out` });