evals-lab 0.15.0 → 0.16.1
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 +59 -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-Crw-d4cy.js +18 -0
- package/lab/web/dist/assets/main-Dxf5iQLA.css +1 -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/server.py
CHANGED
|
@@ -89,12 +89,6 @@ DISPLAYABLE = (".jpg", ".jpeg", ".png")
|
|
|
89
89
|
# is what issue #270 turned out to be.
|
|
90
90
|
UNDISPLAYABLE = (".heic", ".heif", ".dng", ".tif", ".tiff")
|
|
91
91
|
|
|
92
|
-
# The llama.cpp presets the Setup tab offers as Apply preset…: measured
|
|
93
|
-
# decoding settings, Setup's data and no dataset's. Read by the page (served
|
|
94
|
-
# at /api/presets) and by evals-check.js's Decoding.kt comparison; absent, the
|
|
95
|
-
# tab just has no presets to offer, so it is not fatal to start.
|
|
96
|
-
PRESETS = HERE / "presets.json" if (HERE / "presets.json").is_file() else None
|
|
97
|
-
|
|
98
92
|
# The page, built from web/ (React + Vite, #24 and #27's page PR): the image's
|
|
99
93
|
# web stage writes it here, as `npm run build` does in a checkout. Required,
|
|
100
94
|
# like the dataset: without it there is no page. WEB_DIST names another
|
|
@@ -3879,11 +3873,11 @@ def response_pins(db, sid, name) -> set:
|
|
|
3879
3873
|
except (ValueError, TypeError, AttributeError):
|
|
3880
3874
|
continue
|
|
3881
3875
|
for w in listed if isinstance(listed, list) else []:
|
|
3882
|
-
c
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3876
|
+
for _step, c, _at in item_contents(w.get("work")) if isinstance(w, dict) else []:
|
|
3877
|
+
pin = c.get("pin")
|
|
3878
|
+
if (isinstance(pin, dict) and isinstance(c.get("ref"), dict) and c["ref"].get("id") == sid
|
|
3879
|
+
and pin.get("name") == name and type(pin.get("n")) is int):
|
|
3880
|
+
out.add(pin["n"] or 1)
|
|
3887
3881
|
return out
|
|
3888
3882
|
|
|
3889
3883
|
|
|
@@ -3978,6 +3972,17 @@ def filter_set_links(doc) -> list:
|
|
|
3978
3972
|
Runs wires the step and migrates the inline rules in #339; this is the one
|
|
3979
3973
|
reader of where the links sit, for the store (#336) to resolve and keep."""
|
|
3980
3974
|
out = []
|
|
3975
|
+
# Version 20: filter sets live on the Filter steps of the flat step list
|
|
3976
|
+
# (evals-core.ts); version 19 and older held them on a job's Responses
|
|
3977
|
+
# steps.
|
|
3978
|
+
steps = doc.get("steps") if isinstance(doc, dict) else None
|
|
3979
|
+
if isinstance(steps, list) and any(isinstance(s, dict) and s.get("type") in ("getItems", "request") for s in steps):
|
|
3980
|
+
for st in steps:
|
|
3981
|
+
links = st.get("filterSets") if isinstance(st, dict) else None
|
|
3982
|
+
for link in links if isinstance(links, list) else []:
|
|
3983
|
+
if isinstance(link, dict):
|
|
3984
|
+
out.append(link)
|
|
3985
|
+
return out
|
|
3981
3986
|
jobs = doc.get("jobs") if isinstance(doc, dict) else None
|
|
3982
3987
|
for job in jobs if isinstance(jobs, list) else []:
|
|
3983
3988
|
steps = job.get("steps") if isinstance(job, dict) else None
|
|
@@ -4143,6 +4148,24 @@ class Prompts:
|
|
|
4143
4148
|
"ORDER BY at DESC, run_id, job", (r["id"],))]
|
|
4144
4149
|
return out
|
|
4145
4150
|
|
|
4151
|
+
def used_by(self, run_ids) -> dict:
|
|
4152
|
+
"""Each run's prompt uses, by run id: the target, the job, and the
|
|
4153
|
+
prompt and version its words were recorded as. A trashed prompt's
|
|
4154
|
+
uses are left out with it."""
|
|
4155
|
+
out = {}
|
|
4156
|
+
ids = list(run_ids)
|
|
4157
|
+
with self.store.lock, self._connect() as db:
|
|
4158
|
+
for k in range(0, len(ids), 500):
|
|
4159
|
+
chunk = ids[k:k + 500]
|
|
4160
|
+
for u in db.execute(
|
|
4161
|
+
"SELECT u.run_id, u.scenario_id, u.job, u.version, p.id, p.name FROM prompt_uses u "
|
|
4162
|
+
"JOIN prompts p ON p.id = u.prompt_id WHERE p.trash IS NULL AND p.workspace = ? "
|
|
4163
|
+
f"AND u.run_id IN ({', '.join('?' * len(chunk))}) ORDER BY u.job",
|
|
4164
|
+
(workspace_of(self.store), *chunk)):
|
|
4165
|
+
out.setdefault(u["run_id"], []).append(
|
|
4166
|
+
{"target": u["scenario_id"], "job": u["job"], "id": u["id"], "name": u["name"], "version": u["version"]})
|
|
4167
|
+
return out
|
|
4168
|
+
|
|
4146
4169
|
def list(self) -> list:
|
|
4147
4170
|
with self.store.lock, self._connect() as db:
|
|
4148
4171
|
return [self._summary(db, r) for r in db.execute(
|
|
@@ -5616,23 +5639,38 @@ def pack_connection(ws, documents=False):
|
|
|
5616
5639
|
|
|
5617
5640
|
|
|
5618
5641
|
def repoint_targets(doc: dict, ws, documents=False) -> None:
|
|
5619
|
-
"""A pack pipeline's
|
|
5620
|
-
pointed at the one pack_connection picks, its profile
|
|
5621
|
-
|
|
5622
|
-
|
|
5642
|
+
"""A pack pipeline's Variants or Request cells that name a connection this
|
|
5643
|
+
lab does not have, pointed at the one pack_connection picks, its profile
|
|
5644
|
+
dropped with it. Version 20 keeps the connection on each Variant and on the
|
|
5645
|
+
Request steps' cells (and a Variant's overrides), so each is repointed."""
|
|
5646
|
+
if not isinstance(doc, dict) or STORE is None:
|
|
5623
5647
|
return
|
|
5624
5648
|
have = {c.get("id") for c in ((STORE.all().get("promptlab.connections") or {}).get("body") or {}).get("list") or []
|
|
5625
5649
|
if isinstance(c, dict)}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
pick = pick or pack_connection(ws, documents)
|
|
5632
|
-
if pick is None:
|
|
5650
|
+
state = {"pick": None, "none": False}
|
|
5651
|
+
|
|
5652
|
+
def repoint(o):
|
|
5653
|
+
ref = o.get("connection") if isinstance(o, dict) else None
|
|
5654
|
+
if not isinstance(ref, dict) or ref.get("id") in have or state["none"]:
|
|
5633
5655
|
return
|
|
5634
|
-
|
|
5635
|
-
|
|
5656
|
+
if state["pick"] is None:
|
|
5657
|
+
state["pick"] = pack_connection(ws, documents)
|
|
5658
|
+
if state["pick"] is None:
|
|
5659
|
+
state["none"] = True
|
|
5660
|
+
return
|
|
5661
|
+
pick = state["pick"]
|
|
5662
|
+
o["connection"] = {"id": pick["id"], "name": pick.get("name") or pick["id"]}
|
|
5663
|
+
if "profile" in o:
|
|
5664
|
+
o["profile"] = None
|
|
5665
|
+
|
|
5666
|
+
for v in doc.get("variants") or []:
|
|
5667
|
+
if isinstance(v, dict):
|
|
5668
|
+
repoint(v)
|
|
5669
|
+
for ov in (v.get("overrides") or {}).values():
|
|
5670
|
+
repoint(ov)
|
|
5671
|
+
for s in doc.get("steps") or []:
|
|
5672
|
+
if isinstance(s, dict) and s.get("type") == "request":
|
|
5673
|
+
repoint(s.get("cell"))
|
|
5636
5674
|
|
|
5637
5675
|
|
|
5638
5676
|
def pack_requires_problem(manifest: dict, plugins: set) -> str:
|
|
@@ -5728,7 +5766,8 @@ def read_pack(data: bytes):
|
|
|
5728
5766
|
if why:
|
|
5729
5767
|
return None, why
|
|
5730
5768
|
if (not isinstance(doc, dict) or doc.get("version") not in READABLE_VERSIONS
|
|
5731
|
-
or not (isinstance(doc.get("
|
|
5769
|
+
or not (isinstance(doc.get("steps"), list) or isinstance(doc.get("jobs"), list)
|
|
5770
|
+
or isinstance(doc.get("chains"), list))):
|
|
5732
5771
|
return None, f"{name} is not a pipeline document the lab reads"
|
|
5733
5772
|
pack["pipelines"].append((parts[1][:-5], doc))
|
|
5734
5773
|
elif len(parts) == 3 and parts[0] == "sources" and parts[2] == PACK_SOURCE_META:
|
|
@@ -5892,11 +5931,11 @@ class Packs:
|
|
|
5892
5931
|
return out
|
|
5893
5932
|
|
|
5894
5933
|
def presets(self) -> list:
|
|
5895
|
-
"""Every installed pack's Setup presets, each marked with its pack."""
|
|
5934
|
+
"""Every installed pack's Setup presets, each marked with its pack's id and name."""
|
|
5896
5935
|
with self.store.lock, self._connect() as db:
|
|
5897
|
-
rows = db.execute("SELECT id, presets FROM packs WHERE workspace = ? ORDER BY name COLLATE NOCASE",
|
|
5936
|
+
rows = db.execute("SELECT id, name, presets FROM packs WHERE workspace = ? ORDER BY name COLLATE NOCASE",
|
|
5898
5937
|
(workspace_of(self.store),)).fetchall()
|
|
5899
|
-
return [{**p, "pack": r["id"]} for r in rows for p in json.loads(r["presets"])]
|
|
5938
|
+
return [{**p, "pack": r["id"], "packName": r["name"]} for r in rows for p in json.loads(r["presets"])]
|
|
5900
5939
|
|
|
5901
5940
|
# The page's documents are the server's to write here too, through the
|
|
5902
5941
|
# docs table's own arbitration: a browser holding an older copy is told
|
|
@@ -6804,12 +6843,20 @@ def key_var(profile_id: str, slug=None) -> str:
|
|
|
6804
6843
|
|
|
6805
6844
|
|
|
6806
6845
|
def run_items(run):
|
|
6807
|
-
"""A run's items:
|
|
6808
|
-
or
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6846
|
+
"""A run's items (evals-core.ts runItemsOf): each Get items step's in turn,
|
|
6847
|
+
a Source's file list in order and with its repeats or one inline text; a
|
|
6848
|
+
run where none adds one goes over the bare item, its prompt alone."""
|
|
6849
|
+
out, bare = [], False
|
|
6850
|
+
for _step, content, _at in item_contents(run):
|
|
6851
|
+
kind = content.get("type")
|
|
6852
|
+
if kind == "source":
|
|
6853
|
+
out.extend(content.get("files") or [])
|
|
6854
|
+
elif kind == "text":
|
|
6855
|
+
if isinstance(content.get("text"), str) and content["text"].strip():
|
|
6856
|
+
out.append(None)
|
|
6857
|
+
elif kind == "prompt":
|
|
6858
|
+
bare = True
|
|
6859
|
+
return out or ([None] if bare else [])
|
|
6813
6860
|
|
|
6814
6861
|
|
|
6815
6862
|
def worker_refusal(code, stderr, env):
|
|
@@ -7024,18 +7071,23 @@ class Queue:
|
|
|
7024
7071
|
row = self._row(db.execute(sql, (rid, ws) if ws else (rid,)).fetchone())
|
|
7025
7072
|
return row if self._readable(row) else None
|
|
7026
7073
|
|
|
7027
|
-
def list(self, limit=RUNS_PAGE, before=None, before_id=None, full=False):
|
|
7074
|
+
def list(self, limit=RUNS_PAGE, before=None, before_id=None, full=False, pipeline=None):
|
|
7028
7075
|
"""Runs, newest first, and whether more follow. `before` and
|
|
7029
7076
|
`before_id` are the `submittedAt` and id of the last run on the page
|
|
7030
7077
|
before, so History can page through them the way it pages the runs
|
|
7031
7078
|
store. `submittedAt` is to the second, so two runs can share one; the
|
|
7032
7079
|
id breaks the tie, or a page ending between them would skip the
|
|
7033
|
-
second (#238). `before` alone stops at the second.
|
|
7080
|
+
second (#238). `before` alone stops at the second. `pipeline` keeps
|
|
7081
|
+
the runs of one pipeline, by its document's id: History's Overview
|
|
7082
|
+
(#642) reads a window of one pipeline's runs, which paging every run
|
|
7083
|
+
would find slowly where that pipeline runs rarely. Each row is
|
|
7034
7084
|
brief_row's unless `full` asks for the whole of it."""
|
|
7035
7085
|
with self.lock, closing(sqlite3.connect(self.store.path)) as db:
|
|
7036
7086
|
rows = self._all(db, workspace_of(self.store))
|
|
7037
7087
|
key = lambda r: (r["submittedAt"], r["id"])
|
|
7038
7088
|
rows = [r for r in rows if before is None or key(r) < (before, before_id or "")]
|
|
7089
|
+
if pipeline is not None:
|
|
7090
|
+
rows = [r for r in rows if (r["snapshot"] or {}).get("id") == pipeline]
|
|
7039
7091
|
rows.sort(key=key, reverse=True)
|
|
7040
7092
|
page = rows[:limit]
|
|
7041
7093
|
return (page if full else [brief_row(r) for r in page]), len(rows) > limit
|
|
@@ -7324,9 +7376,10 @@ class Queue:
|
|
|
7324
7376
|
if run["status"] in ("queued", "running"):
|
|
7325
7377
|
return None, (409, "a run still in progress cannot be re-run")
|
|
7326
7378
|
snap = run["snapshot"]
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7379
|
+
for _step, content, _at in item_contents(snap):
|
|
7380
|
+
_, err = self._pinned_files(content)
|
|
7381
|
+
if err:
|
|
7382
|
+
return None, (409, err)
|
|
7330
7383
|
# The group bodies the original kept: a re-run grades with exactly
|
|
7331
7384
|
# them, whatever the groups or the pins read now; its filter-set
|
|
7332
7385
|
# bodies travel the same way, so it cleans and gates as the original did.
|
|
@@ -7517,29 +7570,34 @@ class Queue:
|
|
|
7517
7570
|
files_dir = rundir / "files"
|
|
7518
7571
|
shutil.rmtree(rundir, ignore_errors=True)
|
|
7519
7572
|
files_dir.mkdir(parents=True)
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7573
|
+
# Each Get items step's files under its own folder (item_contents): a
|
|
7574
|
+
# content produced at submit from the bytes kept with the row, any
|
|
7575
|
+
# other from its Source.
|
|
7576
|
+
for _step, content, at in item_contents(snap):
|
|
7577
|
+
folder = files_dir / at if at else files_dir
|
|
7578
|
+
folder.mkdir(exist_ok=True)
|
|
7579
|
+
if live is not None and (content.get("live") is True or isinstance(content.get("pin"), dict)):
|
|
7580
|
+
for name in dict.fromkeys(content.get("files") or []):
|
|
7581
|
+
text = live.get(at + "/" + name if at else name)
|
|
7582
|
+
if text is None:
|
|
7583
|
+
return None, f"{name!r} was not produced for this run"
|
|
7584
|
+
(folder / name).write_text(text, encoding="utf-8")
|
|
7585
|
+
continue
|
|
7586
|
+
files, err = self._pinned_files(content)
|
|
7528
7587
|
if err:
|
|
7529
7588
|
return None, err
|
|
7530
7589
|
for name, path in files:
|
|
7531
|
-
shutil.copy2(path,
|
|
7590
|
+
shutil.copy2(path, folder / name)
|
|
7532
7591
|
(rundir / "run.json").write_text(json.dumps(snap))
|
|
7533
7592
|
return rundir, None
|
|
7534
7593
|
|
|
7535
7594
|
@staticmethod
|
|
7536
|
-
def _pinned_files(
|
|
7537
|
-
"""Each file
|
|
7538
|
-
bytes it pinned at submit -- or (None, why), naming the Source
|
|
7539
|
-
has gone, or the files gone from it or changed since. A
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
if content.get("type") != "source":
|
|
7595
|
+
def _pinned_files(content):
|
|
7596
|
+
"""Each file one Get items content reads, by name, and the path holding
|
|
7597
|
+
the bytes it pinned at submit -- or (None, why), naming the Source
|
|
7598
|
+
that has gone, or the files gone from it or changed since. A content
|
|
7599
|
+
that is not a Source's reads no files."""
|
|
7600
|
+
if not isinstance(content, dict) or content.get("type") != "source":
|
|
7543
7601
|
return [], None
|
|
7544
7602
|
ref = content.get("ref") or {}
|
|
7545
7603
|
label = ref.get("name") or ref.get("id")
|
|
@@ -7992,24 +8050,57 @@ def worker_destinations(run: dict):
|
|
|
7992
8050
|
# {name, n}`, which a fetch, an edit or a Restore never moves (#596).
|
|
7993
8051
|
# 19: a job's filters are one list around its Response Format, and filters are
|
|
7994
8052
|
# seven verbs (#615).
|
|
7995
|
-
PIPELINE_VERSION =
|
|
8053
|
+
PIPELINE_VERSION = 20
|
|
7996
8054
|
# What a stored run may be: the current version, and the ones evals-core.ts's
|
|
7997
8055
|
# upgradePipeline reads. A new submission is upgraded to the current one.
|
|
7998
|
-
READABLE_VERSIONS = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
|
|
8056
|
+
READABLE_VERSIONS = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
|
|
7999
8057
|
TARGET_CAP = 4
|
|
8000
8058
|
# Target steps whose words the Prompt library does not record as a use: they
|
|
8001
8059
|
# ask no model (evals-core.ts's STEP_TYPES.echo).
|
|
8002
8060
|
UNRECORDED_STEPS = {"echo"}
|
|
8003
|
-
|
|
8061
|
+
# The version-20 document is a flat step list and Variants (pipeline-model
|
|
8062
|
+
# §20): `steps` and `variants` in the place version 19 held `jobs` and
|
|
8063
|
+
# `targets`.
|
|
8064
|
+
RUN_FIELDS = ("version", "id", "name", "steps", "variants", "evals", "pass", "profiles", "comment", "plugins")
|
|
8065
|
+
|
|
8066
|
+
|
|
8067
|
+
def request_steps(doc):
|
|
8068
|
+
"""A version-20 document's Request steps, in order (evals-core.ts): one per
|
|
8069
|
+
job, holding the settings every Variant shares."""
|
|
8070
|
+
steps = doc.get("steps") if isinstance(doc, dict) else None
|
|
8071
|
+
return [s for s in (steps if isinstance(steps, list) else [])
|
|
8072
|
+
if isinstance(s, dict) and s.get("type") == "request"]
|
|
8073
|
+
|
|
8074
|
+
|
|
8075
|
+
def merge_cell(base, ov):
|
|
8076
|
+
"""A Variant's cell over a Request's base (evals-core.ts mergeCell): the
|
|
8077
|
+
base where there is no override, the override whole where it carries its
|
|
8078
|
+
own `type` (a different Answered by, or a different shape), else the base
|
|
8079
|
+
with the override's fields."""
|
|
8080
|
+
base = base if isinstance(base, dict) else {}
|
|
8081
|
+
if not isinstance(ov, dict) or not ov:
|
|
8082
|
+
return dict(base)
|
|
8083
|
+
if "type" in ov:
|
|
8084
|
+
return dict(ov)
|
|
8085
|
+
merged = dict(base)
|
|
8086
|
+
merged.update(ov)
|
|
8087
|
+
return merged
|
|
8004
8088
|
|
|
8005
8089
|
|
|
8006
8090
|
def content_of(doc):
|
|
8007
|
-
"""A pipeline's content, where its version keeps it:
|
|
8008
|
-
|
|
8009
|
-
`content` before. The dict itself, so a
|
|
8010
|
-
None for none."""
|
|
8091
|
+
"""A pipeline's content, where its version keeps it: version 20's first Get
|
|
8092
|
+
items step, version 8's job-1 Attach Content step (evals-core.ts
|
|
8093
|
+
contentOf), the document's own `content` before. The dict itself, so a
|
|
8094
|
+
caller may stamp it in place. None for none."""
|
|
8011
8095
|
if not isinstance(doc, dict):
|
|
8012
8096
|
return None
|
|
8097
|
+
steps = doc.get("steps")
|
|
8098
|
+
if isinstance(steps, list) and any(isinstance(s, dict) and s.get("type") in ("getItems", "request") for s in steps):
|
|
8099
|
+
for step in steps:
|
|
8100
|
+
if isinstance(step, dict) and step.get("type") == "getItems":
|
|
8101
|
+
c = step.get("content")
|
|
8102
|
+
return c if isinstance(c, dict) else None
|
|
8103
|
+
return None
|
|
8013
8104
|
jobs = doc.get("jobs") if isinstance(doc.get("jobs"), list) else doc.get("chains")
|
|
8014
8105
|
first = jobs[0] if isinstance(jobs, list) and jobs and isinstance(jobs[0], dict) else None
|
|
8015
8106
|
if first is not None and isinstance(first.get("steps"), list):
|
|
@@ -8020,6 +8111,25 @@ def content_of(doc):
|
|
|
8020
8111
|
return None
|
|
8021
8112
|
c = doc.get("content")
|
|
8022
8113
|
return c if isinstance(c, dict) else None
|
|
8114
|
+
def item_contents(doc):
|
|
8115
|
+
"""Every Get items step's content, in list order, as (step id, content,
|
|
8116
|
+
folder) -- evals-core.ts itemContents: the first's files at the top of a
|
|
8117
|
+
run's files, each later one's under `@N`, N its place among them, a name
|
|
8118
|
+
no item can have (NAME_RESTRICT). A document before version 20 has its
|
|
8119
|
+
one content."""
|
|
8120
|
+
if not isinstance(doc, dict):
|
|
8121
|
+
return []
|
|
8122
|
+
steps = doc.get("steps")
|
|
8123
|
+
if isinstance(steps, list) and any(isinstance(s, dict) and s.get("type") in ("getItems", "request") for s in steps):
|
|
8124
|
+
out = []
|
|
8125
|
+
for k, step in enumerate(s for s in steps if isinstance(s, dict) and s.get("type") == "getItems"):
|
|
8126
|
+
if isinstance(step.get("content"), dict):
|
|
8127
|
+
out.append((step.get("id") or "", step["content"], f"@{k + 1}" if k else ""))
|
|
8128
|
+
return out
|
|
8129
|
+
c = content_of(doc)
|
|
8130
|
+
return [("", c, "")] if c is not None else []
|
|
8131
|
+
|
|
8132
|
+
|
|
8023
8133
|
# The connection-type registry, mirrored from evals-core.ts (#46) because the
|
|
8024
8134
|
# server is stdlib-only and cannot load it: which types ship, and the settings
|
|
8025
8135
|
# each one's options may carry. The server only needs the field allow-list and
|
|
@@ -8298,12 +8408,23 @@ def upgrade_run(run):
|
|
|
8298
8408
|
|
|
8299
8409
|
|
|
8300
8410
|
def cells_of(run):
|
|
8301
|
-
"""Every
|
|
8302
|
-
version
|
|
8303
|
-
and their cells -- what the
|
|
8304
|
-
version a stored run is.
|
|
8411
|
+
"""Every Variant's cell in every Request of a run, as (i, variant, k, cell):
|
|
8412
|
+
version 20's Variants over the Request steps (evals-core.ts targetsOf), an
|
|
8413
|
+
earlier version's targets/scenarios and their steps/cells -- what the
|
|
8414
|
+
Prompt library records a use from, whichever version a stored run is. The
|
|
8415
|
+
cell is the Request's base with the Variant's override over it."""
|
|
8305
8416
|
if not isinstance(run, dict):
|
|
8306
8417
|
return
|
|
8418
|
+
reqs = request_steps(run)
|
|
8419
|
+
if reqs:
|
|
8420
|
+
variants = run.get("variants")
|
|
8421
|
+
for i, v in enumerate(variants if isinstance(variants, list) else []):
|
|
8422
|
+
if not isinstance(v, dict):
|
|
8423
|
+
continue
|
|
8424
|
+
overrides = v.get("overrides") if isinstance(v.get("overrides"), dict) else {}
|
|
8425
|
+
for k, req in enumerate(reqs):
|
|
8426
|
+
yield i, v, k, merge_cell(req.get("cell"), overrides.get(req.get("id")))
|
|
8427
|
+
return
|
|
8307
8428
|
targets = run.get("targets") if isinstance(run.get("targets"), list) else run.get("scenarios")
|
|
8308
8429
|
for i, t in enumerate(targets if isinstance(targets, list) else []):
|
|
8309
8430
|
if not isinstance(t, dict):
|
|
@@ -8327,22 +8448,46 @@ def run_problems(run):
|
|
|
8327
8448
|
_fields(run, "the run", RUN_FIELDS, bad)
|
|
8328
8449
|
if not isinstance(run.get("id"), str) or not run["id"].strip():
|
|
8329
8450
|
bad.append("the run has no id")
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8451
|
+
# The version-20 step list: Get items before Request 1, each Read Response
|
|
8452
|
+
# and Filter after the Request it reads (evals-core.ts stepsProblems). The
|
|
8453
|
+
# server checks the shape and order; tokens and kinds are the runner's.
|
|
8454
|
+
steps = run.get("steps")
|
|
8455
|
+
if not isinstance(steps, list):
|
|
8456
|
+
return bad + ["steps has to be a list"]
|
|
8457
|
+
req_ids = [s.get("id") for s in steps if isinstance(s, dict) and s.get("type") == "request"]
|
|
8458
|
+
seen_ids = set()
|
|
8459
|
+
saw_request = False
|
|
8460
|
+
for pos, s in enumerate(steps):
|
|
8461
|
+
if not isinstance(s, dict) or s.get("type") not in ("getItems", "request", "readResponse", "filter"):
|
|
8462
|
+
bad.append(f"\"{s.get('type') if isinstance(s, dict) else s}\" is not a step this lab has")
|
|
8338
8463
|
continue
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
bad.append(f"
|
|
8344
|
-
|
|
8345
|
-
|
|
8464
|
+
sid = s.get("id")
|
|
8465
|
+
if not isinstance(sid, str) or not sid.strip():
|
|
8466
|
+
bad.append(f"a {s['type']} step has no id")
|
|
8467
|
+
elif sid in seen_ids:
|
|
8468
|
+
bad.append(f"two steps have the id {sid}")
|
|
8469
|
+
else:
|
|
8470
|
+
seen_ids.add(sid)
|
|
8471
|
+
if s["type"] == "getItems":
|
|
8472
|
+
if saw_request:
|
|
8473
|
+
bad.append("Get items comes before Request 1")
|
|
8474
|
+
elif s["type"] == "request":
|
|
8475
|
+
saw_request = True
|
|
8476
|
+
else:
|
|
8477
|
+
# A Read Response or Filter reads the response of a Request, and
|
|
8478
|
+
# comes after it: the step it must come before names the Request.
|
|
8479
|
+
label = "Read Response" if s["type"] == "readResponse" else "Filter"
|
|
8480
|
+
rid = s.get("request")
|
|
8481
|
+
if not isinstance(rid, str) or rid not in req_ids:
|
|
8482
|
+
bad.append(f"a {label} reads a Request the experiment does not have")
|
|
8483
|
+
else:
|
|
8484
|
+
n = req_ids.index(rid) + 1
|
|
8485
|
+
req_pos = next((j for j, o in enumerate(steps)
|
|
8486
|
+
if isinstance(o, dict) and o.get("type") == "request" and o.get("id") == rid), -1)
|
|
8487
|
+
if pos < req_pos:
|
|
8488
|
+
bad.append(f"{label} {n} comes after Request {n}")
|
|
8489
|
+
if not req_ids:
|
|
8490
|
+
return bad + ["add a Request first"]
|
|
8346
8491
|
table = run.get("profiles")
|
|
8347
8492
|
if not isinstance(table, dict):
|
|
8348
8493
|
return bad + ["profiles has to be an object of id → connection"]
|
|
@@ -8367,76 +8512,81 @@ def run_problems(run):
|
|
|
8367
8512
|
bad.append(f"profiles {spelt[var]} and {cid} both take their key from ${var}")
|
|
8368
8513
|
spelt[var] = cid
|
|
8369
8514
|
connection_problems(pid, conn, at, bad)
|
|
8370
|
-
|
|
8371
|
-
if not isinstance(
|
|
8372
|
-
return bad + [f"a run needs between one and {TARGET_CAP}
|
|
8373
|
-
|
|
8374
|
-
for i,
|
|
8375
|
-
at = f"
|
|
8376
|
-
if not isinstance(
|
|
8515
|
+
variants = run.get("variants")
|
|
8516
|
+
if not isinstance(variants, list) or not 1 <= len(variants) <= TARGET_CAP:
|
|
8517
|
+
return bad + [f"a run needs between one and {TARGET_CAP} variants"]
|
|
8518
|
+
vids = [v.get("id") for v in variants if isinstance(v, dict)]
|
|
8519
|
+
for i, v in enumerate(variants):
|
|
8520
|
+
at = f"variant {i + 1}"
|
|
8521
|
+
if not isinstance(v, dict):
|
|
8377
8522
|
bad.append(f"{at} has to be an object")
|
|
8378
8523
|
continue
|
|
8379
8524
|
# The id the Prompt library records a use under (version 4).
|
|
8380
|
-
if not isinstance(
|
|
8525
|
+
if not isinstance(v.get("id"), str) or not v["id"].strip():
|
|
8381
8526
|
bad.append(f"{at} has no id")
|
|
8382
|
-
elif
|
|
8383
|
-
bad.append(f"{at} has the id of another
|
|
8384
|
-
|
|
8385
|
-
if not isinstance(
|
|
8386
|
-
bad.append(f"{at}
|
|
8527
|
+
elif vids.count(v["id"]) > 1:
|
|
8528
|
+
bad.append(f"{at} has the id of another variant")
|
|
8529
|
+
overrides = v.get("overrides")
|
|
8530
|
+
if overrides is not None and (not isinstance(overrides, dict) or any(k not in req_ids for k in overrides)):
|
|
8531
|
+
bad.append(f"{at}: overrides are keyed by Request ids")
|
|
8532
|
+
# Each Variant's cell in each Request -- the Request's base with the
|
|
8533
|
+
# Variant's override over it (cells_of) -- what it sends and the
|
|
8534
|
+
# connection the run carries for it. A Variant with no connection is one
|
|
8535
|
+
# whose cells ask none (Echo) or each name their own; which needs one is
|
|
8536
|
+
# the core's to judge. Whether the words may be blank (Echo answering from
|
|
8537
|
+
# the item) is the core's too, and the worker refuses the run in its words.
|
|
8538
|
+
seen_ref = set()
|
|
8539
|
+
for i, v, k, cell in cells_of(run):
|
|
8540
|
+
at = f"variant {i + 1}, job {k + 1}"
|
|
8541
|
+
if not isinstance(cell, dict) or not isinstance(cell.get("type"), str):
|
|
8542
|
+
bad.append(f"{at} has to name what it sends")
|
|
8387
8543
|
continue
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
if not isinstance(content.get("text"), str) or not content["text"].strip():
|
|
8435
|
-
bad.append("a run over text needs some text")
|
|
8436
|
-
elif kind == "prompt":
|
|
8437
|
-
pass
|
|
8438
|
-
else:
|
|
8439
|
-
bad.append("a run needs a Source's files, some inline text, or Prompt only")
|
|
8544
|
+
if not isinstance(cell.get("prompt"), str):
|
|
8545
|
+
bad.append(f"{at} has no prompt")
|
|
8546
|
+
elif cell.get("from") is not None and not (
|
|
8547
|
+
isinstance(cell["from"], dict) and isinstance(cell["from"].get("id"), str)):
|
|
8548
|
+
bad.append(f"{at} names the prompt it was picked from without an id")
|
|
8549
|
+
c = cell.get("connection") if cell.get("connection") is not None else v.get("connection")
|
|
8550
|
+
p = cell.get("profile") if cell.get("profile") is not None else v.get("profile")
|
|
8551
|
+
if c is None and p is None:
|
|
8552
|
+
continue
|
|
8553
|
+
if (not isinstance(c, dict) or not isinstance(c.get("id"), str)
|
|
8554
|
+
or (p is not None and not (isinstance(p, dict) and isinstance(p.get("id"), str)))
|
|
8555
|
+
or run_answer(table, answer_key(c["id"], p["id"] if p else None)) is None):
|
|
8556
|
+
if i not in seen_ref:
|
|
8557
|
+
seen_ref.add(i)
|
|
8558
|
+
bad.append(f"variant {i + 1} names a connection the run does not carry")
|
|
8559
|
+
# Each Get items step's content; a later one is named by its place, and
|
|
8560
|
+
# may add no files (a run of one file of the first, say).
|
|
8561
|
+
contents = item_contents(run) or [("", None, "")]
|
|
8562
|
+
for k, (_step, content, _at) in enumerate(contents):
|
|
8563
|
+
said = []
|
|
8564
|
+
kind = content.get("type") if isinstance(content, dict) else None
|
|
8565
|
+
if kind == "source":
|
|
8566
|
+
ref = content.get("ref")
|
|
8567
|
+
files = content.get("files")
|
|
8568
|
+
if not isinstance(ref, dict) or not isinstance(ref.get("id"), str) or not ref["id"]:
|
|
8569
|
+
said.append("a Source content has to name its Source")
|
|
8570
|
+
# A live dynamic run (docs/dynamic-sources.md) produces its items at
|
|
8571
|
+
# submit, so its file list is the server's to fill -- empty here is
|
|
8572
|
+
# fine. Every other source run names the files it froze.
|
|
8573
|
+
elif content.get("live") is True:
|
|
8574
|
+
if not isinstance(files, list) or not all(isinstance(f, str) for f in files):
|
|
8575
|
+
said.append("a run over a Source needs its file list")
|
|
8576
|
+
elif not isinstance(files, list) or not (files or k) or not all(isinstance(f, str) for f in files):
|
|
8577
|
+
said.append("a run over a Source needs its file list")
|
|
8578
|
+
pin = content.get("pin")
|
|
8579
|
+
if pin is not None and not (isinstance(pin, dict) and isinstance(pin.get("name"), str) and pin["name"]
|
|
8580
|
+
and type(pin.get("n")) is int and pin["n"] >= 0):
|
|
8581
|
+
said.append("a Source content's pin names a response as { name, n }")
|
|
8582
|
+
elif kind == "text":
|
|
8583
|
+
if not isinstance(content.get("text"), str) or not content["text"].strip():
|
|
8584
|
+
said.append("a run over text needs some text")
|
|
8585
|
+
elif kind == "prompt":
|
|
8586
|
+
pass
|
|
8587
|
+
else:
|
|
8588
|
+
said.append("a run needs a Source's files, some inline text, or (No content)")
|
|
8589
|
+
bad.extend(f"Get items {k + 1}: {x}" if k else x for x in said)
|
|
8440
8590
|
evals = run.get("evals")
|
|
8441
8591
|
if not isinstance(evals, list) or not all(isinstance(t, dict) and isinstance(t.get("type"), str) for t in evals):
|
|
8442
8592
|
bad.append("evals has to be a list of evals, each naming its type")
|
|
@@ -8612,7 +8762,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
8612
8762
|
|
|
8613
8763
|
# The eval groups' routes (docs/pipeline-model.md §17) are the datasets'
|
|
8614
8764
|
# under their new name: /api/datasets goes on answering the same rows, so
|
|
8615
|
-
#
|
|
8765
|
+
# the app's comparison (docs/app-data.md) and a script written against it
|
|
8766
|
+
# keep working: another repository's contract, so keep its shape. A list
|
|
8616
8767
|
# asked for by the new name is `groups`.
|
|
8617
8768
|
GROUPS_ROUTE = "/api/eval-groups"
|
|
8618
8769
|
grouped = False
|
|
@@ -8706,8 +8857,14 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
8706
8857
|
return self._json(400, {"error": "limit has to be a number"})
|
|
8707
8858
|
before = (query.get("before") or [None])[0]
|
|
8708
8859
|
before_id = (query.get("beforeId") or [None])[0]
|
|
8709
|
-
|
|
8710
|
-
|
|
8860
|
+
pipeline = (query.get("pipeline") or [None])[0]
|
|
8861
|
+
runs, more = QUEUE.list(limit, before, before_id, full=(query.get("full") or [""])[0] == "1",
|
|
8862
|
+
pipeline=pipeline)
|
|
8863
|
+
# Which version of a Library prompt each target step ran: the
|
|
8864
|
+
# prompt library records it, not the run's document (§14), and
|
|
8865
|
+
# Overview breaks a target's line where it changes.
|
|
8866
|
+
used = PROMPTS.used_by([r["id"] for r in runs]) if PROMPTS else {}
|
|
8867
|
+
return self._json(200, {"runs": [{**r, "prompts": used.get(r["id"], [])} for r in runs], "more": more})
|
|
8711
8868
|
if path.startswith("/api/queue/") and path.endswith("/groups") and path.count("/") == 4:
|
|
8712
8869
|
# The bodies of the eval groups a run grades with, by `<id>@<n>`
|
|
8713
8870
|
# (§17); null for a run that kept none, as /dataset answers.
|
|
@@ -8748,10 +8905,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
8748
8905
|
run["behind"] = QUEUE._behind(run["id"]) or 0
|
|
8749
8906
|
return self._json(200, run)
|
|
8750
8907
|
if path == "/api/presets":
|
|
8751
|
-
#
|
|
8752
|
-
|
|
8753
|
-
own = json.loads(PRESETS.read_text("utf-8")) if PRESETS is not None else []
|
|
8754
|
-
return self._json(200, own + (PACKS.presets() if PACKS is not None else []))
|
|
8908
|
+
# Every installed pack's: the lab ships none of its own (#652).
|
|
8909
|
+
return self._json(200, PACKS.presets() if PACKS is not None else [])
|
|
8755
8910
|
if path == "/api/plugins":
|
|
8756
8911
|
if PLUGINS is None:
|
|
8757
8912
|
return self._send(404, b"not found", "text/plain")
|
|
@@ -9446,28 +9601,18 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9446
9601
|
|
|
9447
9602
|
# ---- The run queue (#530) ----------------------------------------------
|
|
9448
9603
|
|
|
9449
|
-
def
|
|
9450
|
-
"""
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
run = self._payload()
|
|
9456
|
-
if run is None:
|
|
9457
|
-
return self._json(400, {"error": "bad body"})
|
|
9458
|
-
run, why = upgrade_run(run)
|
|
9459
|
-
if why:
|
|
9460
|
-
return self._json(400, {"error": "the run cannot be queued: " + why})
|
|
9461
|
-
bad = run_problems(run)
|
|
9462
|
-
if bad:
|
|
9463
|
-
return self._json(400, {"error": "the run cannot be queued: " + "; ".join(bad)})
|
|
9464
|
-
content = content_of(run)
|
|
9604
|
+
def _stamp_content(self, content):
|
|
9605
|
+
"""One Get items step's content made ready to queue, in place: a
|
|
9606
|
+
Source's files pinned at their revision, or a dynamic Source's items
|
|
9607
|
+
produced or its pinned response read. Returns (produced, None) -- the
|
|
9608
|
+
bytes kept with the run by item name, or None for none -- or (None,
|
|
9609
|
+
(status, error))."""
|
|
9465
9610
|
produced = None
|
|
9466
9611
|
if content["type"] == "source":
|
|
9467
9612
|
sid = content["ref"]["id"]
|
|
9468
9613
|
src = SOURCES.get(sid)
|
|
9469
9614
|
if src is None:
|
|
9470
|
-
return
|
|
9615
|
+
return None, (404, "no such Source")
|
|
9471
9616
|
# A dynamic Source (docs/dynamic-sources.md) grades either its stored
|
|
9472
9617
|
# Calls (snapshot) or content produced fresh at submit (live). The
|
|
9473
9618
|
# choice is the run's (content.live) over the Source's default
|
|
@@ -9477,20 +9622,19 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9477
9622
|
if src.get("type") == "dynamic":
|
|
9478
9623
|
entry = source_entry("dynamic", src.get("config") or {})
|
|
9479
9624
|
if entry is None:
|
|
9480
|
-
return
|
|
9625
|
+
return None, (403, "the lab does not offer that Source's provider")
|
|
9481
9626
|
cfg = src.get("config") or {}
|
|
9482
9627
|
if entry.get("produce") is None:
|
|
9483
9628
|
# A Supplied provider hands items in (the caller's --items,
|
|
9484
9629
|
# or an Upload…); the server produces nothing, so there is no
|
|
9485
9630
|
# live for it -- a hosted run grades the items it holds. A
|
|
9486
|
-
# Source with none is refused
|
|
9631
|
+
# Source with none is refused by run_problems, naming
|
|
9487
9632
|
# the Source with no files; a run that asks it to go live is
|
|
9488
9633
|
# refused here, pointing at the CLI (docs/ci.md).
|
|
9489
9634
|
if content.get("live"):
|
|
9490
9635
|
noun = entry.get("noun", "item")
|
|
9491
|
-
return
|
|
9492
|
-
|
|
9493
|
-
f"-- add one, or run it from the CLI with evals-lab run --items"})
|
|
9636
|
+
return None, (400, f"this Source produces nothing: it grades the {noun}s handed in "
|
|
9637
|
+
f"-- add one, or run it from the CLI with evals-lab run --items")
|
|
9494
9638
|
content["live"] = False
|
|
9495
9639
|
else:
|
|
9496
9640
|
live = content.get("live")
|
|
@@ -9503,14 +9647,14 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9503
9647
|
# written by hand is graded as saved.
|
|
9504
9648
|
produced, err = self._produce_endpoints(sid, entry, cfg, content["files"])
|
|
9505
9649
|
if err:
|
|
9506
|
-
return
|
|
9650
|
+
return None, err
|
|
9507
9651
|
content["live"] = True
|
|
9508
9652
|
content["revs"] = {name: "live:" + hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
9509
9653
|
for name, text in produced.items()}
|
|
9510
9654
|
elif live:
|
|
9511
9655
|
produced, name, err = self._produce_live(sid, entry, cfg)
|
|
9512
9656
|
if err:
|
|
9513
|
-
return
|
|
9657
|
+
return None, err
|
|
9514
9658
|
content["live"] = True
|
|
9515
9659
|
# The one capture, looped as a snapshot's files are
|
|
9516
9660
|
# (evals-core.ts expand): Run first cannot cut below
|
|
@@ -9528,8 +9672,8 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9528
9672
|
# whatever a fetch, an edit or a Restore made current since.
|
|
9529
9673
|
body = SOURCES.pinned_body(sid, pin["name"], pin["n"])
|
|
9530
9674
|
if body is None:
|
|
9531
|
-
return
|
|
9532
|
-
|
|
9675
|
+
return None, (400, f"the response this run grades, {pin['name']}, "
|
|
9676
|
+
"was removed: pick another")
|
|
9533
9677
|
text = body.decode("utf-8", errors="replace")
|
|
9534
9678
|
produced = {pin["name"]: text}
|
|
9535
9679
|
loops = content.get("loops")
|
|
@@ -9543,8 +9687,34 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
9543
9687
|
for name in dict.fromkeys(content["files"]):
|
|
9544
9688
|
revs[name] = SOURCES.rev(sid, name)
|
|
9545
9689
|
if revs[name] is None:
|
|
9546
|
-
return
|
|
9690
|
+
return None, (400, f"{name!r} is not in that Source")
|
|
9547
9691
|
content["revs"] = revs
|
|
9692
|
+
return produced, None
|
|
9693
|
+
|
|
9694
|
+
def _queue_submit(self):
|
|
9695
|
+
"""A run document, checked and queued. The page resolves it at submit
|
|
9696
|
+
(evals-core.ts resolvePipeline); anything else posting here is held to
|
|
9697
|
+
the same fields."""
|
|
9698
|
+
if QUEUE is None:
|
|
9699
|
+
return self._send(404, b"not found", "text/plain")
|
|
9700
|
+
run = self._payload()
|
|
9701
|
+
if run is None:
|
|
9702
|
+
return self._json(400, {"error": "bad body"})
|
|
9703
|
+
run, why = upgrade_run(run)
|
|
9704
|
+
if why:
|
|
9705
|
+
return self._json(400, {"error": "the run cannot be queued: " + why})
|
|
9706
|
+
bad = run_problems(run)
|
|
9707
|
+
if bad:
|
|
9708
|
+
return self._json(400, {"error": "the run cannot be queued: " + "; ".join(bad)})
|
|
9709
|
+
produced = None
|
|
9710
|
+
for _step, content, at in item_contents(run):
|
|
9711
|
+
got, err = self._stamp_content(content)
|
|
9712
|
+
if err:
|
|
9713
|
+
return self._json(err[0], {"error": err[1]})
|
|
9714
|
+
if got is not None:
|
|
9715
|
+
# A later Get items' items are kept under its own folder
|
|
9716
|
+
# (evals-core.ts itemContents), so two Sources' names never meet.
|
|
9717
|
+
produced = {**(produced or {}), **{(at + "/" + n if at else n): t for n, t in got.items()}}
|
|
9548
9718
|
# A connection not shared with this workspace blocks the run with the
|
|
9549
9719
|
# sentence the Run bar shows and the connections it names, so its Share
|
|
9550
9720
|
# link can grant them at once (docs/workspaces.md).
|