evals-lab 0.14.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 +97 -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/manifest.json +2 -2
- package/lab/demo/pipelines/demo-1.json +105 -103
- package/lab/demo/pipelines/demo-2.json +105 -103
- package/lab/demo/pipelines/documents.json +54 -52
- 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 +1053 -353
- package/lab/kinds/list.mjs +328 -224
- package/lab/metrics/builtin.mjs +24 -24
- package/lab/run-evals.js +34 -56
- package/lab/server.py +478 -206
- package/lab/web/dist/assets/gallery-ICJNZQIZ.js +5 -0
- package/lab/web/dist/assets/gallery-bG2vNzKT.css +1 -0
- 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-B_-TH0F-.css +0 -1
- package/lab/web/dist/assets/gallery-qrf0ZGXK.js +0 -5
- package/lab/web/dist/assets/main-D6xARg2A.js +0 -16
- package/lab/web/dist/assets/main-xVU6J7Gi.css +0 -1
- package/lab/web/dist/assets/tokens-C8dqXDHN.js +0 -62
- package/lab/web/dist/assets/tokens-tQ7zTPkd.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,103 @@ Newest first. Read a release's **Upgrade notes** before installing it: an
|
|
|
5
5
|
upgrade can rewrite what the lab keeps in your data directory, and an older
|
|
6
6
|
version cannot always read it back.
|
|
7
7
|
|
|
8
|
+
## 0.16.0
|
|
9
|
+
|
|
10
|
+
### Upgrade notes
|
|
11
|
+
|
|
12
|
+
- **Copy your data directory before upgrading** (see "User data" in the
|
|
13
|
+
README). An experiment 0.16.0 saves is pipeline version 20, which 0.15.x
|
|
14
|
+
cannot read. To go back, reinstall 0.15.0 and restore the copy.
|
|
15
|
+
- An experiment's jobs and targets become one Steps list in run order (Get
|
|
16
|
+
items, Request, Read response, Filter), with Variants beside it. Every
|
|
17
|
+
experiment runs and grades as it did.
|
|
18
|
+
- The lab no longer ships presets of its own: presets come from a pack.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- The Experiment page is a Steps list: Add step, Move up and down, Clone and
|
|
23
|
+
Remove, and a step out of order says where it goes.
|
|
24
|
+
- A Variants table under the steps: each Variant reads (Same) or holds an
|
|
25
|
+
override, its prompt included. Add variants from presets fills it from a
|
|
26
|
+
pack's presets.
|
|
27
|
+
- An experiment can have several Get items steps; Results groups rows by
|
|
28
|
+
Source.
|
|
29
|
+
- A flow's Request chooses who answers it: the Request, its Recorded
|
|
30
|
+
answer, or Echo.
|
|
31
|
+
- Score with latest Eval: Results shows how kept responses grade against the
|
|
32
|
+
Eval as it reads now, without asking a model.
|
|
33
|
+
- Add eval case on a Results row that has none.
|
|
34
|
+
- History opens on Overview: an experiment's variants over its recent runs,
|
|
35
|
+
Previous performance with the best marked, New failures and New passing,
|
|
36
|
+
and an Items matrix.
|
|
37
|
+
- Guides on Home, replacing Setup › Wizards. Old links still land.
|
|
38
|
+
- `evals-lab presets compare` sets an app's decoding settings beside a lab's
|
|
39
|
+
or a pack's presets.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- On screen: Experiments, Eval, Eval Cases, Variant, Item and Response.
|
|
44
|
+
Nothing stored is renamed.
|
|
45
|
+
- Setup calls an AI connection only when a button is pressed. Model is
|
|
46
|
+
listed as its picker opens.
|
|
47
|
+
- Errors and statuses read the same everywhere, with field errors on the
|
|
48
|
+
field.
|
|
49
|
+
- Empty values read as a short parenthetical, such as (Not set).
|
|
50
|
+
- Runs are named by when they ran.
|
|
51
|
+
- History, Library and Setup reopen the section last used.
|
|
52
|
+
- The Evals Lab title links to Home.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- An open Request fits a phone's width.
|
|
57
|
+
- History's experiment filter lists each experiment once.
|
|
58
|
+
- Items strips keep their width in Firefox.
|
|
59
|
+
- An experiment opened unchanged is no longer saved again.
|
|
60
|
+
|
|
61
|
+
## 0.15.0
|
|
62
|
+
|
|
63
|
+
### Upgrade notes
|
|
64
|
+
|
|
65
|
+
- **Copy your data directory before upgrading** (see "User data" in the
|
|
66
|
+
README). A pipeline 0.15.0 saves is pipeline version 19 and a filter set is
|
|
67
|
+
version 4, neither of which 0.14.x can read. To go back, reinstall 0.14.0
|
|
68
|
+
and restore the copy.
|
|
69
|
+
- A job's Pre-format and Post-format filters become one Filters list, in run
|
|
70
|
+
order around its Response Format, and each filter becomes one of seven
|
|
71
|
+
verbs. Every reply reads as it did, except a filter that was linked where
|
|
72
|
+
it never ran: it now runs where it sits.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- Filters are seven verbs, in one Filters table with the Response Format as
|
|
77
|
+
a row.
|
|
78
|
+
- A typed prompt edited between runs becomes its next version, not a new
|
|
79
|
+
prompt.
|
|
80
|
+
- Results show a spinner on the row a run is processing, and (Not run) on
|
|
81
|
+
rows a stopped run never reached.
|
|
82
|
+
- Enter in a picker's search picks the top match.
|
|
83
|
+
|
|
84
|
+
### Changed
|
|
85
|
+
|
|
86
|
+
- A picker ends in Cancel and a viewer in Close.
|
|
87
|
+
- Escape closes the innermost dialog or menu, and focus returns to what
|
|
88
|
+
opened it.
|
|
89
|
+
- Enter presses the main button in more dialogs.
|
|
90
|
+
- Links are amber.
|
|
91
|
+
- Keyboard and screen readers: focus rings are no longer clipped, menus
|
|
92
|
+
move with the arrow keys, and Results' run tabs are a proper tab list.
|
|
93
|
+
- When another tab saved first, the conflict bar keeps that copy by default,
|
|
94
|
+
with one button to override it.
|
|
95
|
+
- A dynamic Source's Status is its newest fetch's, and its button says Get
|
|
96
|
+
response.
|
|
97
|
+
- The Sources toolbar stays on one row on a phone, and a tile's quota says
|
|
98
|
+
what it measures.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- Add source adds one Source, not two.
|
|
103
|
+
- A focus trap skips disabled buttons.
|
|
104
|
+
|
|
8
105
|
## 0.14.0
|
|
9
106
|
|
|
10
107
|
### Upgrade notes
|
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@ Test what a model, an HTTP endpoint or an API answers, side by side, and score
|
|
|
4
4
|
it with evals you define in advance. Runs locally in a friendly UI, and
|
|
5
5
|
doesn't send your data anywhere you haven't pointed it.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
**
|
|
7
|
+
An experiment takes items from a **Source** and sends each one to one or more
|
|
8
|
+
**Variants**, then scores the **Responses**.
|
|
9
9
|
|
|
10
|
-
- **
|
|
10
|
+
- **Variants** can be models, local (Ollama, llama.cpp) or hosted (Anthropic,
|
|
11
11
|
OpenAI, any compatible API), or **any HTTP endpoint**: your own service,
|
|
12
12
|
an internal API, a gateway in front of a model. The lab sends each request
|
|
13
13
|
as you write it, with the key held in a connection rather than in the
|
|
@@ -18,8 +18,8 @@ A pipeline takes items from a **Source** and sends each one to one or more
|
|
|
18
18
|
a fresh one at run time.
|
|
19
19
|
|
|
20
20
|
Evals include parsing checks, item counts, comparison with production
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
responses, grading by a model, and evals (evals) shared between
|
|
22
|
+
experiments. Run history is kept and can be exported.
|
|
23
23
|
|
|
24
24
|
The same evals run in CI: `evals-lab run` fails the build when an eval
|
|
25
25
|
fails, like a failing unit test.
|
|
@@ -33,7 +33,7 @@ Example uses:
|
|
|
33
33
|
keywords it returns
|
|
34
34
|
- Comparing two models, or two prompts, on the same items and seeing which
|
|
35
35
|
scores higher
|
|
36
|
-
- Grading what an API returns against
|
|
36
|
+
- Grading what an API returns against an eval of expected answers
|
|
37
37
|
- Checking which model can meet the evals in a Power Automate workflow
|
|
38
38
|
- Sending images to a model with vision and comparing its answers for
|
|
39
39
|
accuracy
|
|
@@ -45,8 +45,8 @@ npm install -g evals-lab
|
|
|
45
45
|
evals-lab
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Open <http://localhost:8080>. A new lab starts with six demos, each
|
|
49
|
-
and its
|
|
48
|
+
Open <http://localhost:8080>. A new lab starts with six demos, each an eval
|
|
49
|
+
and its experiment, which run once you add an AI Connection in Setup. Note the first start takes longer while it installs them. Without
|
|
50
50
|
installing: `npx evals-lab`.
|
|
51
51
|
|
|
52
52
|
| Needs | Windows | Linux |
|
|
@@ -89,7 +89,7 @@ evals-lab [--port <n>] [--host <address>] [--data-dir <dir>]
|
|
|
89
89
|
|
|
90
90
|
## In CI
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
An experiment can run with no lab, from a repository. In the lab, the experiment's
|
|
93
93
|
menu › **Export for CI…** downloads it as a bundle: unzip it into the
|
|
94
94
|
repository, then:
|
|
95
95
|
|
|
@@ -104,7 +104,7 @@ evals-lab run <bundle-dir | pipeline.yaml> [--items <dir>] [--json <file|->]
|
|
|
104
104
|
|
|
105
105
|
| Option | |
|
|
106
106
|
|---|---|
|
|
107
|
-
| `--items` | the items the
|
|
107
|
+
| `--items` | the items the experiment's Source reads; default the bundle's `items/` |
|
|
108
108
|
| `--json` | the JSON report; `-` for stdout |
|
|
109
109
|
| `--junit` | JUnit XML, for a CI test panel |
|
|
110
110
|
| `--summary` | the results as Markdown, added to the file: for GitHub, `"$GITHUB_STEP_SUMMARY"` |
|
|
@@ -124,7 +124,7 @@ goes to stderr.
|
|
|
124
124
|
| `2` | incomplete: an item is missing, or did not run |
|
|
125
125
|
| `3` | broken or refused |
|
|
126
126
|
|
|
127
|
-
An item
|
|
127
|
+
An item an eval case names that is not in the items is reported as Missing.
|
|
128
128
|
Images need ImageMagick, as in the lab.
|
|
129
129
|
|
|
130
130
|
### GitHub Actions
|
|
@@ -160,7 +160,7 @@ CI systems run the same command and read `--junit` in their test panel.
|
|
|
160
160
|
|
|
161
161
|
### Against a running lab
|
|
162
162
|
|
|
163
|
-
CI can run a lab's own
|
|
163
|
+
CI can run a lab's own experiment there instead, so the run shows in that lab's
|
|
164
164
|
History and its keys never leave it:
|
|
165
165
|
|
|
166
166
|
```bash
|
|
@@ -175,13 +175,34 @@ evals-lab run --lab <url> --pipeline <name> [--wait] [--json <file|->]
|
|
|
175
175
|
| Option | |
|
|
176
176
|
|---|---|
|
|
177
177
|
| `--lab` | the lab's address; `LAB_PASSWORD` is sent when it is set |
|
|
178
|
-
| `--pipeline` | the
|
|
178
|
+
| `--pipeline` | the experiment, by its name or its id |
|
|
179
179
|
| `--wait` | wait for the run, and exit with the lab's verdict on it |
|
|
180
180
|
|
|
181
181
|
Without `--wait` the run's id is printed once it is queued, and the exit code
|
|
182
182
|
is `0`; `--json`, `--junit`, `--summary`, `--min-pass` and `--progress` need
|
|
183
183
|
`--wait`. The exit codes are the ones above, and a cancelled run is `2`.
|
|
184
184
|
|
|
185
|
+
### Presets
|
|
186
|
+
|
|
187
|
+
An app that sends decoding settings of its own can compare them with the
|
|
188
|
+
presets its pack gives the lab, as the request fields each sends:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
evals-lab presets compare app-presets.json --pack my-app.labpack [--fail-on-difference]
|
|
192
|
+
evals-lab presets compare app-presets.json --lab https://lab.example
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
It reports each preset as the same, different or on one side only, and exits
|
|
196
|
+
`0`; `--fail-on-difference` exits `1` on a difference. The app's file names
|
|
197
|
+
each preset's request fields:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{ "format": "evals-lab/presets", "version": 1,
|
|
201
|
+
"presets": [{ "id": "repeat-1.2", "request": { "repeat_penalty": 1.2 } }] }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`evals-lab presets --help` has the rest.
|
|
205
|
+
|
|
185
206
|
## Getting started
|
|
186
207
|
|
|
187
208
|
First set up a Setup profile: **Setup › Add profile**, then pick its type —
|
|
@@ -190,7 +211,7 @@ endpoint — and give its address, model and key. NB: An Ollama profile without
|
|
|
190
211
|
|
|
191
212
|
Set up your data source(s) in Library.
|
|
192
213
|
|
|
193
|
-
Then create
|
|
214
|
+
Then create an Experiment to Run. Changes are auto-saved. Use the "Restore" menu to go back and undo any changes you don't want to keep.
|
|
194
215
|
|
|
195
216
|
## Power Automate
|
|
196
217
|
|
package/bin/evals-lab.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// The npm package's launcher: `evals-lab` starts the lab's server and says
|
|
3
3
|
// where it is, and `evals-lab run <bundle>` runs an Export for CI bundle with
|
|
4
4
|
// no server at all, or `evals-lab run --lab <url>` a running lab's pipeline
|
|
5
|
-
// (run.js).
|
|
5
|
+
// (run.js). `evals-lab presets compare` sets an app's decoding settings
|
|
6
|
+
// beside a lab's or a pack's presets (presets.js).
|
|
6
7
|
//
|
|
7
8
|
// The lab itself is server.py, a stdlib Python server, staged beside this file
|
|
8
9
|
// in ../lab by build.js. What this adds is what a lab run by someone who only
|
|
@@ -32,6 +33,8 @@ const USAGE = `Usage: evals-lab [options]
|
|
|
32
33
|
evals-lab run --lab <url> --pipeline <name> [--wait] [options]
|
|
33
34
|
evals-lab export --lab <url> [--workspace <slug>] [--out <file>]
|
|
34
35
|
evals-lab import <file> --lab <url> (evals-lab import --help)
|
|
36
|
+
evals-lab presets compare <file> (--lab <url> | --pack <file>)
|
|
37
|
+
(evals-lab presets --help)
|
|
35
38
|
|
|
36
39
|
--port <n> the port to listen on. Default: $PORT, else 8080.
|
|
37
40
|
--host <address> the address to listen on. Default: $LAB_HOST, else
|
|
@@ -167,6 +170,10 @@ async function main() {
|
|
|
167
170
|
process.exitCode = await require("./lab-file.js").labFile(process.argv[2], process.argv.slice(3));
|
|
168
171
|
return;
|
|
169
172
|
}
|
|
173
|
+
if (process.argv[2] === "presets") {
|
|
174
|
+
process.exitCode = await require("./presets.js").presets(process.argv.slice(3), { lab: LAB });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
170
177
|
if (process.argv[2] === "run") {
|
|
171
178
|
process.exitCode = await require("./run.js").run(process.argv.slice(3), { lab: LAB });
|
|
172
179
|
return;
|
package/bin/presets.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// `evals-lab presets compare <file>` (#652, docs/presets.md): an app's
|
|
2
|
+
// decoding settings beside the presets a lab or a pack offers, compared as
|
|
3
|
+
// what each one sends.
|
|
4
|
+
//
|
|
5
|
+
// WHAT A PRESET SENDS. A preset is written in the lab's spelling
|
|
6
|
+
// (repeatPenalty: "1.2") and an app's in whatever its own code says, so the
|
|
7
|
+
// two are compared where they meet: the request fields. A lab preset's are
|
|
8
|
+
// the fields its type's request carries with the preset applied and not
|
|
9
|
+
// without it -- the core builds both, so this is what a run would send. The
|
|
10
|
+
// app's file names its fields directly.
|
|
11
|
+
//
|
|
12
|
+
// A COMPARISON, NOT AN ASSERTION. It says what is the same, what differs and
|
|
13
|
+
// what is only on one side, and exits 0; --fail-on-difference exits 1 when
|
|
14
|
+
// anything is not the same, for a CI that wants that. 3 is a comparison
|
|
15
|
+
// refused: a file that is not one, a lab that did not answer.
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const zlib = require("zlib");
|
|
19
|
+
const { labApi, Refused } = require("./run.js");
|
|
20
|
+
|
|
21
|
+
const EXIT = { same: 0, different: 1, broken: 3 };
|
|
22
|
+
const FORMAT = "evals-lab/presets";
|
|
23
|
+
|
|
24
|
+
const USAGE = `Usage: evals-lab presets compare <file> (--lab <url> | --pack <file>) [options]
|
|
25
|
+
|
|
26
|
+
<file> the app's presets: { "format": "${FORMAT}", "version": 1,
|
|
27
|
+
"presets": [{ "id", "type"?, "request": { field: value } }] }
|
|
28
|
+
--lab <url> compare with a running lab's presets. LAB_PASSWORD is sent
|
|
29
|
+
when it is set.
|
|
30
|
+
--workspace <slug> the lab's workspace, by its /w/<slug> address. Default:
|
|
31
|
+
the lab's default workspace.
|
|
32
|
+
--pack <file> compare with a pack's presets: a .labpack, or its
|
|
33
|
+
presets.json.
|
|
34
|
+
--json <file|-> write the comparison as JSON. "-" means stdout.
|
|
35
|
+
--fail-on-difference
|
|
36
|
+
exit 1 when anything differs or is on one side only.
|
|
37
|
+
--help this.
|
|
38
|
+
|
|
39
|
+
Exit codes: 0 compared, 1 a difference with --fail-on-difference, 3 refused.`;
|
|
40
|
+
|
|
41
|
+
function options(argv) {
|
|
42
|
+
const o = { file: null, lab: null, workspace: null, pack: null, json: null, failOnDifference: false };
|
|
43
|
+
const value = { "--lab": "lab", "--workspace": "workspace", "--pack": "pack", "--json": "json" };
|
|
44
|
+
const [verb, ...rest] = argv;
|
|
45
|
+
if (verb === "--help" || verb === "-h" || verb === undefined) return { help: true };
|
|
46
|
+
if (verb !== "compare") throw new Refused(`no presets command ${verb}`, true);
|
|
47
|
+
for (let i = 0; i < rest.length; i++) {
|
|
48
|
+
const a = rest[i];
|
|
49
|
+
if (a === "--help" || a === "-h") return { help: true };
|
|
50
|
+
if (a === "--fail-on-difference") o.failOnDifference = true;
|
|
51
|
+
else if (value[a]) {
|
|
52
|
+
if (rest[i + 1] === undefined) throw new Refused(`${a} needs a value`, true);
|
|
53
|
+
o[value[a]] = rest[++i];
|
|
54
|
+
} else if (a.startsWith("-") && a !== "-") throw new Refused(`no option ${a}`, true);
|
|
55
|
+
else if (o.file === null) o.file = a;
|
|
56
|
+
else throw new Refused(`one file to compare, not ${a} as well`, true);
|
|
57
|
+
}
|
|
58
|
+
if (o.file === null) throw new Refused("name the app's presets file", true);
|
|
59
|
+
if ((o.lab === null) === (o.pack === null)) throw new Refused("compare with one of --lab or --pack", true);
|
|
60
|
+
if (o.lab !== null && !/^https?:\/\/[^/]/i.test(o.lab)) throw new Refused(`--lab ${o.lab} is not an http or https address`, true);
|
|
61
|
+
if (o.workspace !== null && o.lab === null) throw new Refused("--workspace is --lab's", true);
|
|
62
|
+
return o;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The entries of a zip, by name, as Buffers. A pack is a plain zip, stored
|
|
66
|
+
// or deflated, so this reads the central directory and nothing fancier.
|
|
67
|
+
function unzip(buf) {
|
|
68
|
+
let end = buf.length - 22;
|
|
69
|
+
while (end >= 0 && buf.readUInt32LE(end) !== 0x06054b50) end--;
|
|
70
|
+
if (end < 0) throw new Error("not a zip");
|
|
71
|
+
const count = buf.readUInt16LE(end + 10);
|
|
72
|
+
let at = buf.readUInt32LE(end + 16);
|
|
73
|
+
const files = {};
|
|
74
|
+
for (let n = 0; n < count; n++) {
|
|
75
|
+
if (buf.readUInt32LE(at) !== 0x02014b50) throw new Error("a broken zip directory");
|
|
76
|
+
const method = buf.readUInt16LE(at + 10), size = buf.readUInt32LE(at + 20);
|
|
77
|
+
const nameLen = buf.readUInt16LE(at + 28), extraLen = buf.readUInt16LE(at + 30), noteLen = buf.readUInt16LE(at + 32);
|
|
78
|
+
const local = buf.readUInt32LE(at + 42);
|
|
79
|
+
const name = buf.toString("utf8", at + 46, at + 46 + nameLen);
|
|
80
|
+
const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
|
|
81
|
+
const raw = buf.subarray(start, start + size);
|
|
82
|
+
if (method === 0) files[name] = raw;
|
|
83
|
+
else if (method === 8) files[name] = zlib.inflateRawSync(raw);
|
|
84
|
+
at += 46 + nameLen + extraLen + noteLen;
|
|
85
|
+
}
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A pack's presets: from its .labpack, or from its presets.json as it is.
|
|
90
|
+
function packPresets(file) {
|
|
91
|
+
let buf;
|
|
92
|
+
try { buf = fs.readFileSync(path.resolve(file)); }
|
|
93
|
+
catch (e) { throw new Refused(`${file}: ${e.message}`); }
|
|
94
|
+
let text = buf.toString("utf8");
|
|
95
|
+
if (buf.readUInt32LE(0) === 0x04034b50) {
|
|
96
|
+
let entries;
|
|
97
|
+
try { entries = unzip(buf); } catch (e) { throw new Refused(`${file}: ${e.message}`); }
|
|
98
|
+
if (!entries["presets.json"]) return [];
|
|
99
|
+
text = entries["presets.json"].toString("utf8");
|
|
100
|
+
}
|
|
101
|
+
let list;
|
|
102
|
+
try { list = JSON.parse(text); } catch (e) { throw new Refused(`${file}: ${e.message}`); }
|
|
103
|
+
if (!Array.isArray(list) || !list.every(p => p && typeof p.id === "string" && p.opts && typeof p.opts === "object")) {
|
|
104
|
+
throw new Refused(`${file}: a pack's presets are a list of { id, label, type?, opts }`);
|
|
105
|
+
}
|
|
106
|
+
return list;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The app's file, or a Refused saying what is wrong with it.
|
|
110
|
+
function appPresets(file, text) {
|
|
111
|
+
let doc;
|
|
112
|
+
try { doc = JSON.parse(text); } catch (e) { throw new Refused(`${file}: ${e.message}`); }
|
|
113
|
+
if (doc?.format !== FORMAT) throw new Refused(`${file} is not an ${FORMAT} file`);
|
|
114
|
+
if (doc.version !== 1) throw new Refused(`${file} is ${FORMAT} version ${doc.version}, and this reads 1`);
|
|
115
|
+
const list = doc.presets;
|
|
116
|
+
if (!Array.isArray(list) || !list.every(p => p && typeof p.id === "string"
|
|
117
|
+
&& p.request && typeof p.request === "object" && !Array.isArray(p.request))) {
|
|
118
|
+
throw new Refused(`${file}: presets are a list of { id, type?, request }`);
|
|
119
|
+
}
|
|
120
|
+
return list;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const typeOf = p => p.type ?? "llama.cpp";
|
|
124
|
+
|
|
125
|
+
// The request fields a lab [preset] sends: what its type's request carries
|
|
126
|
+
// with it applied and not without.
|
|
127
|
+
function presetRequest(core, preset) {
|
|
128
|
+
const conn = { type: typeOf(preset), model: "m", url: "" };
|
|
129
|
+
const body = c => {
|
|
130
|
+
const b = { ...core.connectionRequest(c, "p", null, "").body };
|
|
131
|
+
delete b.messages;
|
|
132
|
+
return b;
|
|
133
|
+
};
|
|
134
|
+
const base = body(conn), sent = body({ ...conn, ...preset.opts });
|
|
135
|
+
return Object.fromEntries(Object.entries(sent).filter(([k, v]) => JSON.stringify(base[k]) !== JSON.stringify(v)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
139
|
+
|
|
140
|
+
function unique(list, where) {
|
|
141
|
+
const ids = list.map(p => p.id);
|
|
142
|
+
const twice = ids.find((id, n) => ids.indexOf(id) !== n);
|
|
143
|
+
if (twice !== undefined) throw new Refused(`${where} has two presets named ${JSON.stringify(twice)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The comparison of the lab's [lab] presets (its spelling) with the app's
|
|
148
|
+
* [app] ones (request fields): a row per preset id, the lab's in their
|
|
149
|
+
* order and then the app's only, each with its fields.
|
|
150
|
+
*/
|
|
151
|
+
function compare(core, lab, app) {
|
|
152
|
+
unique(lab, "the lab"); unique(app, "the file");
|
|
153
|
+
const theirs = new Map(app.map(p => [p.id, p]));
|
|
154
|
+
const rows = [];
|
|
155
|
+
for (const p of lab) {
|
|
156
|
+
let sent;
|
|
157
|
+
try { sent = presetRequest(core, p); }
|
|
158
|
+
catch (e) { throw new Refused(`the lab's preset ${p.id}: ${e.message}`); }
|
|
159
|
+
const other = theirs.get(p.id);
|
|
160
|
+
if (!other) { rows.push({ id: p.id, status: "lab only", lab: sent }); continue; }
|
|
161
|
+
const fields = [];
|
|
162
|
+
if (typeOf(p) !== typeOf(other)) fields.push({ field: "(type)", status: "different", lab: typeOf(p), file: typeOf(other) });
|
|
163
|
+
for (const k of [...new Set([...Object.keys(sent), ...Object.keys(other.request)])].sort()) {
|
|
164
|
+
if (!(k in other.request)) fields.push({ field: k, status: "lab only", lab: sent[k] });
|
|
165
|
+
else if (!(k in sent)) fields.push({ field: k, status: "file only", file: other.request[k] });
|
|
166
|
+
else fields.push({ field: k, status: same(sent[k], other.request[k]) ? "same" : "different", lab: sent[k], file: other.request[k] });
|
|
167
|
+
}
|
|
168
|
+
rows.push({ id: p.id, status: fields.every(f => f.status === "same") ? "same" : "different", fields });
|
|
169
|
+
}
|
|
170
|
+
const ours = new Set(lab.map(p => p.id));
|
|
171
|
+
for (const p of app) if (!ours.has(p.id)) rows.push({ id: p.id, status: "file only", file: p.request });
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const shown = v => {
|
|
176
|
+
const s = JSON.stringify(v);
|
|
177
|
+
return s.length > 60 ? `${s.slice(0, 59)}…` : s;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// The comparison as text: a line per preset, then one per field not the same.
|
|
181
|
+
function table(rows) {
|
|
182
|
+
if (!rows.length) return "No presets on either side.";
|
|
183
|
+
const width = Math.max(...rows.map(r => r.id.length));
|
|
184
|
+
const lines = [];
|
|
185
|
+
for (const r of rows) {
|
|
186
|
+
lines.push(`${r.id.padEnd(width)} ${r.status}`);
|
|
187
|
+
const fields = r.fields ? r.fields.filter(f => f.status !== "same")
|
|
188
|
+
: Object.entries(r.lab ?? r.file).map(([field, v]) => ({ field, status: r.status, [r.lab ? "lab" : "file"]: v }));
|
|
189
|
+
for (const f of fields) {
|
|
190
|
+
const said = f.status === "different" ? `lab ${shown(f.lab)}, file ${shown(f.file)}`
|
|
191
|
+
: f.status === "lab only" ? `lab ${shown(f.lab)}` : `file ${shown(f.file)}`;
|
|
192
|
+
lines.push(`${"".padEnd(width)} ${f.field}: ${said}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return lines.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Runs [argv] -- what follows `evals-lab presets` -- against the staged core
|
|
200
|
+
* in [lab], and resolves to the exit code.
|
|
201
|
+
*/
|
|
202
|
+
async function presets(argv, { lab, env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
203
|
+
const say = text => stderr.write(text + "\n");
|
|
204
|
+
try {
|
|
205
|
+
const o = options(argv);
|
|
206
|
+
if (o.help) { stdout.write(USAGE + "\n"); return 0; }
|
|
207
|
+
const corePath = path.join(lab, "evals-core.mjs");
|
|
208
|
+
if (!fs.existsSync(corePath)) {
|
|
209
|
+
throw new Refused(`no ${corePath} -- in a checkout the package is staged by \`node npm/build.js\``);
|
|
210
|
+
}
|
|
211
|
+
const core = require(corePath);
|
|
212
|
+
let text;
|
|
213
|
+
try { text = fs.readFileSync(path.resolve(o.file), "utf8"); }
|
|
214
|
+
catch (e) { throw new Refused(`${o.file}: ${e.message}`); }
|
|
215
|
+
const app = appPresets(o.file, text);
|
|
216
|
+
const ours = o.pack !== null ? packPresets(o.pack)
|
|
217
|
+
: await labApi(o.lab.replace(/\/+$/, ""), env, o.workspace)("GET", "/api/presets");
|
|
218
|
+
const rows = compare(core, ours, app);
|
|
219
|
+
const out = table(rows);
|
|
220
|
+
if (o.json === "-") { stdout.write(JSON.stringify({ presets: rows }, null, 2) + "\n"); say(out); }
|
|
221
|
+
else {
|
|
222
|
+
if (o.json) fs.writeFileSync(path.resolve(o.json), JSON.stringify({ presets: rows }, null, 2) + "\n");
|
|
223
|
+
stdout.write(out + "\n");
|
|
224
|
+
}
|
|
225
|
+
return o.failOnDifference && rows.some(r => r.status !== "same") ? EXIT.different : EXIT.same;
|
|
226
|
+
} catch (e) {
|
|
227
|
+
if (!(e instanceof Refused)) throw e;
|
|
228
|
+
say(`evals-lab presets: ${e.message}`);
|
|
229
|
+
if (e.usage) say(`\n${USAGE}`);
|
|
230
|
+
return EXIT.broken;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = { presets, options, compare, presetRequest, packPresets, appPresets, unzip, table, USAGE, FORMAT };
|
package/lab/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.16.0 (2026.10.16-944)
|
package/lab/demo/manifest.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"version": 1,
|
|
4
4
|
"id": "demo",
|
|
5
5
|
"name": "Demo",
|
|
6
|
-
"packVersion": "2026.10.14.
|
|
6
|
+
"packVersion": "2026.10.14.3",
|
|
7
7
|
"requires": {
|
|
8
|
-
"lab": ">=
|
|
8
|
+
"lab": ">=19",
|
|
9
9
|
"plugins": []
|
|
10
10
|
},
|
|
11
11
|
"description": "Six demos, each a dataset with a starter pipeline: two sets of public-domain images, short texts to classify, one question in four wordings, a dynamic Source's snapshot, and PDFs to check and sort."
|