evals-lab 0.11.0 → 0.13.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 +96 -0
- package/README.md +4 -4
- package/bin/evals-lab.js +9 -1
- package/bin/lab-file.js +97 -0
- package/bin/run.js +1 -1
- package/lab/VERSION +1 -1
- package/lab/demo/CREDITS.md +37 -3
- package/lab/demo/datasets/demo-1.json +2 -2
- package/lab/demo/datasets/demo-2.json +2 -2
- package/lab/demo/datasets/sentiment.json +383 -0
- package/lab/demo/datasets/weather-alerts.json +61 -0
- package/lab/demo/datasets/wordings.json +39 -0
- package/lab/demo/manifest.json +3 -3
- package/lab/demo/pipelines/demo-1.json +4 -4
- package/lab/demo/pipelines/demo-2.json +4 -4
- package/lab/demo/pipelines/sentiment.json +65 -0
- package/lab/demo/pipelines/weather-alerts.json +66 -0
- package/lab/demo/pipelines/wordings.json +104 -0
- package/lab/demo/sources/Demo 1/source.json +3 -0
- package/lab/demo/sources/Demo 2/source.json +3 -0
- package/lab/demo/sources/sentiment/bakery-croissant.txt +1 -0
- package/lab/demo/sources/sentiment/bus-timetable.txt +1 -0
- package/lab/demo/sources/sentiment/cafe-coffee.txt +1 -0
- package/lab/demo/sources/sentiment/concert-night.txt +1 -0
- package/lab/demo/sources/sentiment/film-sequel.txt +1 -0
- package/lab/demo/sources/sentiment/garden-tomatoes.txt +1 -0
- package/lab/demo/sources/sentiment/gym-contract.txt +1 -0
- package/lab/demo/sources/sentiment/headphones-review.txt +1 -0
- package/lab/demo/sources/sentiment/hotel-room.txt +1 -0
- package/lab/demo/sources/sentiment/library-hours.txt +1 -0
- package/lab/demo/sources/sentiment/meeting-minutes.txt +1 -0
- package/lab/demo/sources/sentiment/parcel-lost.txt +1 -0
- package/lab/demo/sources/sentiment/phone-update.txt +1 -0
- package/lab/demo/sources/sentiment/puppy-home.txt +1 -0
- package/lab/demo/sources/sentiment/recipe-note.txt +1 -0
- package/lab/demo/sources/sentiment/road-closure.txt +1 -0
- package/lab/demo/sources/sentiment/source.json +3 -0
- package/lab/demo/sources/sentiment/teacher-thanks.txt +1 -0
- package/lab/demo/sources/sentiment/train-delay.txt +1 -0
- package/lab/demo/sources/sentiment/volunteer-day.txt +1 -0
- package/lab/demo/sources/sentiment/weather-report.txt +1 -0
- package/lab/demo/sources/weather-alerts/https-api.weather.gov-alerts-types.txt +116 -0
- package/lab/demo/sources/weather-alerts/source.json +8 -0
- package/lab/evals-core.mjs +246 -57
- package/lab/flows/wdl.mjs +19 -0
- package/lab/kinds/list.mjs +5 -2
- package/lab/metrics/builtin.mjs +34 -7
- package/lab/run-evals.js +30 -11
- package/lab/server.py +614 -75
- package/lab/web/dist/assets/gallery-DpaqOg9Z.js +5 -0
- package/lab/web/dist/assets/main-BSw-jyYD.css +1 -0
- package/lab/web/dist/assets/main-CrB8wPZn.js +17 -0
- package/lab/web/dist/assets/tokens-CrjDMCbz.js +60 -0
- package/lab/web/dist/assets/tokens-DDOONQon.css +1 -0
- package/lab/web/dist/gallery.html +3 -3
- package/lab/web/dist/index.html +4 -4
- package/package.json +1 -1
- package/lab/web/dist/assets/gallery-DKy8QK3o.js +0 -5
- package/lab/web/dist/assets/main-BGmSu7aS.css +0 -1
- package/lab/web/dist/assets/main-DdGON-yl.js +0 -16
- package/lab/web/dist/assets/tokens-BmytrwxE.js +0 -60
- package/lab/web/dist/assets/tokens-DBN7I0ru.css +0 -1
package/lab/evals-core.mjs
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import yaml from "./js-yaml.mjs";
|
|
36
36
|
|
|
37
|
-
import { evaluate, asText, WdlError, } from "./flows/wdl.mjs";
|
|
37
|
+
import { evaluate, asText, templateProblem, WdlError, } from "./flows/wdl.mjs";
|
|
38
38
|
import { stepsOf as flowStepsOf, actionsByName, readerFor, readsOf, } from "./flows/record.mjs";
|
|
39
39
|
import { flowApi, } from "./flows/flowApi.mjs";
|
|
40
40
|
import registerMetrics from "./metrics/builtin.mjs";
|
|
@@ -166,13 +166,11 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
166
166
|
/** A target step that sends a Power Automate step's request
|
|
167
167
|
(docs/power-automate.md § The HTTP Request step): the flow's own template
|
|
168
168
|
-- method, path, query and body, Workflow Definition Language expressions
|
|
169
|
-
and all -- with the target's words and fields placed in the body by
|
|
170
|
-
HTTP API, evaluated against each item's record.
|
|
171
|
-
address and the key. */
|
|
169
|
+
and all -- with the target's words and fields placed in the body by the
|
|
170
|
+
job's HTTP API (its flow step's), evaluated against each item's record.
|
|
171
|
+
The profile gives the address and the key. */
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
176
174
|
|
|
177
175
|
|
|
178
176
|
|
|
@@ -240,7 +238,7 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
|
|
|
240
238
|
|
|
241
239
|
|
|
242
240
|
/** What a target sends in one job: one of the target step types. */
|
|
243
|
-
|
|
241
|
+
|
|
244
242
|
|
|
245
243
|
/** A target (pipeline-model §16): one column through every job, compared
|
|
246
244
|
side by side in Results -- what version 9 called a scenario. Its step in
|
|
@@ -609,6 +607,9 @@ export const FILTER_STAGE_LABEL = { text: "Pre-form
|
|
|
609
607
|
|
|
610
608
|
|
|
611
609
|
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
|
|
612
613
|
|
|
613
614
|
|
|
614
615
|
/** How much of one text a run keeps -- a reply's body, a grader's prompt or
|
|
@@ -899,6 +900,14 @@ export function kept(text , max = KEPT_CHARS) {
|
|
|
899
900
|
|
|
900
901
|
|
|
901
902
|
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
|
|
902
911
|
|
|
903
912
|
|
|
904
913
|
|
|
@@ -1692,9 +1701,17 @@ function datasetRules(body ) {
|
|
|
1692
1701
|
|
|
1693
1702
|
|
|
1694
1703
|
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
|
|
1695
1708
|
|
|
1696
1709
|
|
|
1697
1710
|
|
|
1711
|
+
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
|
|
1698
1715
|
|
|
1699
1716
|
|
|
1700
1717
|
|
|
@@ -1719,6 +1736,10 @@ function datasetRules(body ) {
|
|
|
1719
1736
|
|
|
1720
1737
|
|
|
1721
1738
|
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
|
|
1722
1743
|
|
|
1723
1744
|
|
|
1724
1745
|
|
|
@@ -1991,6 +2012,35 @@ function asNumber(v ) {
|
|
|
1991
2012
|
// a tuned local engine is a llama.cpp profile with its settings.
|
|
1992
2013
|
const CONNECTION_TYPES = Object.create(null);
|
|
1993
2014
|
|
|
2015
|
+
// ---- documents (#558) ------------------------------------------------------
|
|
2016
|
+
// A file a model's API reads whole, sent unprocessed: the lab does not parse,
|
|
2017
|
+
// render or extract it. It travels where an image does -- the item's
|
|
2018
|
+
// attachment, a data URL -- carrying its name, which some APIs want beside
|
|
2019
|
+
// the bytes. Only a connection type marked `documents` is handed one.
|
|
2020
|
+
const DOCUMENT_TYPES = {
|
|
2021
|
+
".pdf": { media: "application/pdf", label: "PDF" },
|
|
2022
|
+
};
|
|
2023
|
+
|
|
2024
|
+
/** The DOCUMENT_TYPES entry a file's name is, or null. */
|
|
2025
|
+
function documentTypeOf(name ) {
|
|
2026
|
+
const dot = String(name).lastIndexOf(".");
|
|
2027
|
+
return dot < 0 ? null : DOCUMENT_TYPES[String(name).slice(dot).toLowerCase()] ?? null;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/** A document's bytes, base64, as the attachment a stage sends. */
|
|
2031
|
+
function documentUrl(name , media , base64 ) {
|
|
2032
|
+
return `data:${media};name=${encodeURIComponent(name)};base64,${base64}`;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/** An attachment's media type, bytes and name (null for an image, which
|
|
2036
|
+
carries none), or null for anything that is not a base64 data URL. */
|
|
2037
|
+
function attachmentOf(dataUrl ) {
|
|
2038
|
+
const m = /^data:([^;,]+)((?:;[^;,=]+=[^;,]*)*);base64,(.*)$/s.exec(dataUrl);
|
|
2039
|
+
if (!m) return null;
|
|
2040
|
+
const name = /;name=([^;,]*)/.exec(m[2] )?.[1];
|
|
2041
|
+
return { media: m[1] , data: m[3] , name: name == null ? null : decodeURIComponent(name) };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1994
2044
|
/** The key [key] as the header most endpoints expect, when there is one. */
|
|
1995
2045
|
const bearer = (key ) => (key ? { Authorization: "Bearer " + key } : {});
|
|
1996
2046
|
|
|
@@ -2103,6 +2153,13 @@ CONNECTION_TYPES.recorded = {
|
|
|
2103
2153
|
request: sendsNothing, parseReply: sendsNothing, listModels: sendsNothing, parseModels: () => [],
|
|
2104
2154
|
};
|
|
2105
2155
|
|
|
2156
|
+
/** Whether Setup and the connection pickers list a stored connection. A local
|
|
2157
|
+
type's (Echo, Recorded) is a target step's stand-in now (`localSteps`), so
|
|
2158
|
+
a row of one is kept only for the pipelines and runs that name it. */
|
|
2159
|
+
function listedConnection(conn ) {
|
|
2160
|
+
return !CONNECTION_TYPES[typeOf(conn )]?.local;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2106
2163
|
/** A local type's answer to one stage, as a model's would come back. An item
|
|
2107
2164
|
with no text of its own (Prompt only) is answered from the prompt, and
|
|
2108
2165
|
that reply is read against nothing -- the prompt is the reply, not an
|
|
@@ -2118,9 +2175,19 @@ function localAnswer(conn , text , sent , record
|
|
|
2118
2175
|
const chatSchema = (body , schema ) =>
|
|
2119
2176
|
({ ...body, response_format: { type: "json_schema", json_schema: { name: "reply", strict: true, schema } } });
|
|
2120
2177
|
|
|
2178
|
+
/** An item's attachment as a chat completions content part: an image as
|
|
2179
|
+
image_url, a document as a file part, named, its bytes a data URL. */
|
|
2180
|
+
function chatAttachment(dataUrl ) {
|
|
2181
|
+
const a = attachmentOf(dataUrl);
|
|
2182
|
+
if (!a || a.media.startsWith("image/")) return { type: "image_url", image_url: { url: dataUrl } };
|
|
2183
|
+
return { type: "file", file: { filename: a.name ?? "document", file_data: `data:${a.media};base64,${a.data}` } };
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2121
2186
|
CONNECTION_TYPES["openai-compatible"] = {
|
|
2122
2187
|
id: "openai-compatible", label: "OpenAI-compatible",
|
|
2123
2188
|
description: "OpenAI, OpenRouter, vLLM, LM Studio: any chat completions endpoint.",
|
|
2189
|
+
// A hosted provider wants a key; a server of your own may not.
|
|
2190
|
+
keyOptional: (conn) => !isHostedUrl(String(conn.url ?? "")),
|
|
2124
2191
|
settings: [
|
|
2125
2192
|
// A reasoning model refuses a temperature.
|
|
2126
2193
|
{ key: "temperature", label: "Temperature", control: "text", default: "", hint: "",
|
|
@@ -2143,11 +2210,14 @@ CONNECTION_TYPES["openai-compatible"] = {
|
|
|
2143
2210
|
...(predict == null ? {} : hosted ? { max_completion_tokens: predict } : { max_tokens: predict }),
|
|
2144
2211
|
messages: [{ role: "user", content: [
|
|
2145
2212
|
{ type: "text", text: prompt },
|
|
2146
|
-
...(dataUrl ? [
|
|
2213
|
+
...(dataUrl ? [chatAttachment(dataUrl)] : []),
|
|
2147
2214
|
] }],
|
|
2148
2215
|
},
|
|
2149
2216
|
};
|
|
2150
2217
|
},
|
|
2218
|
+
// OpenAI's file part; a compatible server that has none refuses the call,
|
|
2219
|
+
// and the refusal is that item's error.
|
|
2220
|
+
documents: true,
|
|
2151
2221
|
structured: chatSchema,
|
|
2152
2222
|
parseReply: chatReply,
|
|
2153
2223
|
listModels, parseModels: idsFrom,
|
|
@@ -2170,16 +2240,17 @@ CONNECTION_TYPES.anthropic = {
|
|
|
2170
2240
|
// Asked words (a Prompt step, a grader) or sent a flow's whole request (an
|
|
2171
2241
|
// HTTP Request step): one profile and one key do both.
|
|
2172
2242
|
answers: ["prompt", "request"],
|
|
2243
|
+
speaks: () => "anthropic",
|
|
2173
2244
|
// The Messages API: the x-api-key and anthropic-version headers, base64
|
|
2174
|
-
// image content blocks, and /v1/models for the list.
|
|
2245
|
+
// image and document content blocks, and /v1/models for the list.
|
|
2246
|
+
documents: true,
|
|
2175
2247
|
request(conn, prompt, dataUrl, base, key) {
|
|
2176
2248
|
const temp = asNumber(conn.temperature);
|
|
2177
2249
|
const predict = asNumber(conn.maxTokens) ?? ANTHROPIC_MAX_TOKENS;
|
|
2178
2250
|
const content = [];
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
}
|
|
2251
|
+
const a = dataUrl ? attachmentOf(dataUrl) : null;
|
|
2252
|
+
if (a?.media.startsWith("image/")) content.push({ type: "image", source: { type: "base64", media_type: a.media, data: a.data } });
|
|
2253
|
+
else if (a) content.push({ type: "document", source: { type: "base64", media_type: a.media, data: a.data } });
|
|
2183
2254
|
content.push({ type: "text", text: prompt });
|
|
2184
2255
|
return {
|
|
2185
2256
|
url: base + "/messages",
|
|
@@ -2224,6 +2295,7 @@ const SPEED_IMAGE = "before", SPEED_CACHE = "off", SPEED_THINKING = "off";
|
|
|
2224
2295
|
CONNECTION_TYPES["llama.cpp"] = {
|
|
2225
2296
|
id: "llama.cpp", label: "llama.cpp",
|
|
2226
2297
|
description: "A llama-server you run, with its sampler settings.",
|
|
2298
|
+
keyOptional: () => true,
|
|
2227
2299
|
settings: [
|
|
2228
2300
|
{ key: "temperature", label: "Temperature", control: "text", default: "", hint: "" },
|
|
2229
2301
|
{ key: "seed", label: "Seed", control: "text", default: "", hint: "" },
|
|
@@ -2501,8 +2573,18 @@ CONNECTION_TYPES.http = {
|
|
|
2501
2573
|
{ key: "headers", label: "Headers", control: "text", default: "", hint: "", scope: "connection" },
|
|
2502
2574
|
// What Test connection asks of it: a GET, keyed as a run is.
|
|
2503
2575
|
{ key: "testPath", label: "Test path", control: "text", default: "", hint: "", scope: "connection" },
|
|
2576
|
+
// Read off the address (httpApiFor); set by hand for one the lab cannot
|
|
2577
|
+
// tell, such as a gateway in front of a provider.
|
|
2578
|
+
{ key: "requestFormat", label: "Request format", control: "select", default: "", hint: "", scope: "connection",
|
|
2579
|
+
choices: [{ value: "", label: "From address" },
|
|
2580
|
+
...Object.entries(HTTP_APIS).map(([value, a]) => ({ value, label: a.label }))] },
|
|
2504
2581
|
],
|
|
2505
2582
|
answers: ["request"],
|
|
2583
|
+
keyOptional: (conn) => conn.auth === "none",
|
|
2584
|
+
speaks(conn) {
|
|
2585
|
+
const api = String(conn.requestFormat || "") || httpApiFor(originBase(conn.url));
|
|
2586
|
+
return HTTP_APIS[api] && api !== "raw" ? api : null;
|
|
2587
|
+
},
|
|
2506
2588
|
lists: false,
|
|
2507
2589
|
baseOf: originBase,
|
|
2508
2590
|
send(conn, req, base, key) {
|
|
@@ -2785,7 +2867,7 @@ function readCase(kase , res , plain = fa
|
|
|
2785
2867
|
}
|
|
2786
2868
|
return r ? [r ] : [];
|
|
2787
2869
|
});
|
|
2788
|
-
const s = scoreOf(metrics) ?? { pass:
|
|
2870
|
+
const s = erroredScore(scoreOf(metrics), res.error) ?? { pass: !res.error, score: null, metrics };
|
|
2789
2871
|
const counted = metrics.filter(r => r.weight !== 0);
|
|
2790
2872
|
return { ...s, metrics, found: s.found ?? [], missed: s.missed ?? [], invented: s.invented ?? [],
|
|
2791
2873
|
discarded: !!res.error, count: res.error ? 0 : (res.terms || []).length,
|
|
@@ -3183,6 +3265,7 @@ async function runPipeline(stages , dataUrl , cal
|
|
|
3183
3265
|
const pre = applyFilterSet(st.before || [], { body: res.raw ?? "", items: [], instruction: against }, "text");
|
|
3184
3266
|
const by = (filter , stage , rejected ) => ({ filter, stage, job: i + 1, rejected });
|
|
3185
3267
|
if (pre.reject != null) return result({ error: named(`discarded: ${pre.reject}`), filteredBy: by(pre.reject, "text", true) });
|
|
3268
|
+
if (pre.rewrote) transcript[transcript.length - 1] .filtered = { got: pre.body, by: pre.rewrote };
|
|
3186
3269
|
const read = reader.read(pre.body, { echo: against, parsing: opts.parsing, settings: st.settings });
|
|
3187
3270
|
if (read.error) return result({ error: named(read.error) });
|
|
3188
3271
|
const mod = applyModifiers(mods[i], kind, read.value, { instruction: against, body: read.body ?? res.raw ?? "" });
|
|
@@ -3297,7 +3380,7 @@ function applyModifiers (list , kind ,
|
|
|
3297
3380
|
// 16: a target names an AI Connection and, optionally, a Connection Profile,
|
|
3298
3381
|
// where it named one Target profile; a run's table is keyed by the pair
|
|
3299
3382
|
// (answerKey) -- the Target profile split in the store as it opens (#445).
|
|
3300
|
-
const PIPELINE_VERSION =
|
|
3383
|
+
const PIPELINE_VERSION = 17 ;
|
|
3301
3384
|
|
|
3302
3385
|
// Plain objects, so an entry is added by assignment and a reader never needs
|
|
3303
3386
|
// to know which registered it.
|
|
@@ -3719,16 +3802,17 @@ function onlyFields(obj , at , allowed , bad
|
|
|
3719
3802
|
|
|
3720
3803
|
// A Source is referenced, with the two numbers that decide how many items a
|
|
3721
3804
|
// run over it is: the first N files, and how many times over.
|
|
3722
|
-
// A folder of files: images sent as images,
|
|
3723
|
-
// extensions are server.py's FILE_TYPES, which the
|
|
3805
|
+
// A folder of files: images sent as images, documents whole (DOCUMENT_TYPES),
|
|
3806
|
+
// text files as {text}. The extensions are server.py's FILE_TYPES, which the
|
|
3807
|
+
// server holds uploads to.
|
|
3724
3808
|
const FILE_LIBRARY_TEXT = [".txt", ".md", ".csv"] ;
|
|
3725
3809
|
SOURCE_TYPES.files = {
|
|
3726
3810
|
label: "File Library",
|
|
3727
3811
|
icon: "folder",
|
|
3728
|
-
description: "A folder of images and text files you upload.",
|
|
3812
|
+
description: "A folder of images, PDFs and text files you upload.",
|
|
3729
3813
|
group: "Files",
|
|
3730
3814
|
noun: "file",
|
|
3731
|
-
uploads: [".jpg", ".jpeg", ".png", ".heic", ".heif", ".dng", ".tif", ".tiff", ...FILE_LIBRARY_TEXT],
|
|
3815
|
+
uploads: [".jpg", ".jpeg", ".png", ".heic", ".heif", ".dng", ".tif", ".tiff", ...Object.keys(DOCUMENT_TYPES), ...FILE_LIBRARY_TEXT],
|
|
3732
3816
|
sendsImage: true,
|
|
3733
3817
|
hasActions: false,
|
|
3734
3818
|
itemText: name => FILE_LIBRARY_TEXT.some(ext => name.toLowerCase().endsWith(ext)),
|
|
@@ -3745,7 +3829,9 @@ SOURCE_TYPES.workflow = {
|
|
|
3745
3829
|
icon: "workflow",
|
|
3746
3830
|
description: "A cloud flow, with records of its steps' calls from its run history.",
|
|
3747
3831
|
group: "Workflows",
|
|
3748
|
-
|
|
3832
|
+
// Each item reads as a call, as the Source's Calls group names them, though
|
|
3833
|
+
// the file holding one is a record.
|
|
3834
|
+
noun: "call",
|
|
3749
3835
|
uploads: [".json"],
|
|
3750
3836
|
sendsImage: false,
|
|
3751
3837
|
hasActions: true,
|
|
@@ -3834,7 +3920,7 @@ const API_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
|
3834
3920
|
DYNAMIC_PROVIDERS["api"] = { // provider: the API call adapter's own id
|
|
3835
3921
|
id: "api", // provider: its id, the row's config.provider
|
|
3836
3922
|
label: "API call",
|
|
3837
|
-
icon: "
|
|
3923
|
+
icon: "arrow-left-right",
|
|
3838
3924
|
noun: "call",
|
|
3839
3925
|
tier: "pull",
|
|
3840
3926
|
config: ["method", "url", "reqHeaders", "body", "connection"],
|
|
@@ -4104,7 +4190,7 @@ LEGACY_TESTS.single = {
|
|
|
4104
4190
|
const raws = [], terms = [];
|
|
4105
4191
|
for (const r of ress) {
|
|
4106
4192
|
if (!r) continue;
|
|
4107
|
-
raws.push(r
|
|
4193
|
+
raws.push(gradedText(r));
|
|
4108
4194
|
terms.push(...(r.terms || []));
|
|
4109
4195
|
}
|
|
4110
4196
|
const s = scoreSingle(t, terms, raws, !(kind != null && OUTPUT_KINDS[kind]?.terms));
|
|
@@ -4169,11 +4255,32 @@ function metricsProblems(list , at , bad ) {
|
|
|
4169
4255
|
/** Whether any of [list] asks a grader. */
|
|
4170
4256
|
const gradedIn = (list ) => Array.isArray(list) && list.some((m ) => !!METRICS[m?.type]?.graded);
|
|
4171
4257
|
|
|
4258
|
+
/** Whether [error] is a reply the job's rules threw away, as the Discarded
|
|
4259
|
+
metric reads it: a result that came back, where any other error is a
|
|
4260
|
+
request that never did. */
|
|
4261
|
+
const thrownAway = (error ) => !!error && error.startsWith("discarded: ");
|
|
4262
|
+
|
|
4263
|
+
/** [s] as an errored result's score: whatever its metrics read, a result
|
|
4264
|
+
with no reply fails, and only a discarded reply is left to them (#544) --
|
|
4265
|
+
or a refused call, to a case that scores whether it was (#559). */
|
|
4266
|
+
function erroredScore (s , error ) {
|
|
4267
|
+
const asksRefusal = !!s?.metrics?.some(r => r.weight !== 0 && METRICS[r.type]?.readsRefusal);
|
|
4268
|
+
return s && error != null && !thrownAway(error) && !asksRefusal ? { ...s, pass: false } : s;
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
/** The reply a metric grades and Results shows (#546): the last stage's
|
|
4272
|
+
reply after its Pre-format filters, or the raw reply. */
|
|
4273
|
+
function gradedText(res ) {
|
|
4274
|
+
const last = res?.transcript?.at(-1);
|
|
4275
|
+
return last?.filtered?.got ?? last?.got ?? res?.raw ?? "";
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4172
4278
|
/** What a metric reads of one result. */
|
|
4173
4279
|
function metricInput(res , kase , more ) {
|
|
4174
4280
|
const last = res.transcript?.at(-1);
|
|
4175
|
-
const text =
|
|
4176
|
-
|
|
4281
|
+
const text = gradedText(res);
|
|
4282
|
+
// `of: "said"` reads the reply as it came back, before any filter.
|
|
4283
|
+
return { text, said: last?.said ?? (last?.filtered ? last.got ?? null : null), terms: res.terms ?? [], replies: [text], plain: !!more.plain,
|
|
4177
4284
|
asked: last?.sent ?? null,
|
|
4178
4285
|
// A case that has been given its own expected reply ("Take B as
|
|
4179
4286
|
// expected") is compared with that; otherwise the Source's recorded
|
|
@@ -4189,7 +4296,7 @@ function runInput(ress , plain )
|
|
|
4189
4296
|
let ms = 0;
|
|
4190
4297
|
for (const r of ress) {
|
|
4191
4298
|
if (!r) continue;
|
|
4192
|
-
replies.push(r
|
|
4299
|
+
replies.push(gradedText(r));
|
|
4193
4300
|
terms.push(...(r.terms || []));
|
|
4194
4301
|
ms += r.ms ?? 0;
|
|
4195
4302
|
}
|
|
@@ -4205,6 +4312,19 @@ function readMetric(m , input , ctx )
|
|
|
4205
4312
|
const label = (isStr(m.metric) && m.metric.trim()) || entry?.label || m.type;
|
|
4206
4313
|
if (!entry) return { type: m.type, label, weight, pass: false, score: 0, reason: `"${m.type}" is not a metric this lab has` };
|
|
4207
4314
|
const read = m.of === "said" ? { ...input, text: input.said ?? input.text } : input;
|
|
4315
|
+
// An errored result has no reply to read (#544): a metric that reads one
|
|
4316
|
+
// fails on the error, and a grader is never asked about the empty reply.
|
|
4317
|
+
if (input.error != null && !(entry.readsResult && (thrownAway(input.error) || entry.readsRefusal))) {
|
|
4318
|
+
const fail = { type: m.type, label, weight, pass: false, score: 0, reason: input.error };
|
|
4319
|
+
if (entry.graded) return fail;
|
|
4320
|
+
// What it would have missed still counts toward a run's totals.
|
|
4321
|
+
let o = null;
|
|
4322
|
+
try { o = entry.score(read, m, {}); } catch { o = null; }
|
|
4323
|
+
if (!o) return null;
|
|
4324
|
+
if (typeof (o ).then === "function") return fail;
|
|
4325
|
+
const { found, missed, invented } = o ;
|
|
4326
|
+
return { ...fail, ...(found ? { found } : {}), ...(missed ? { missed } : {}), ...(invented ? { invented } : {}) };
|
|
4327
|
+
}
|
|
4208
4328
|
const done = (o ) => {
|
|
4209
4329
|
if (!o) return null;
|
|
4210
4330
|
const turned = m.not ? { ...o, pass: !o.pass, score: entry.counts ? -o.score : 1 - o.score, reason: `not: ${o.reason}` } : o;
|
|
@@ -4237,6 +4357,18 @@ function readMetric(m , input , ctx )
|
|
|
4237
4357
|
} catch (e) { return timed(failed(e)); }
|
|
4238
4358
|
}
|
|
4239
4359
|
|
|
4360
|
+
/** What stops [grader] being asked, where the lookups can say: a grader is
|
|
4361
|
+
asked words, and needs a model to ask. */
|
|
4362
|
+
function graderProblems(grader , ctx , bad ) {
|
|
4363
|
+
const p = grader && ctx.profiles ? ctx.profiles(grader.id) : null;
|
|
4364
|
+
if (grader && ctx.profiles && !p) bad.push(`Grader ${grader.name || grader.id} not found`);
|
|
4365
|
+
else if (p) {
|
|
4366
|
+
const type = CONNECTION_TYPES[typeOf(p )];
|
|
4367
|
+
if (type && !(type.answers ?? ["prompt"]).includes("prompt")) bad.push(`the grader has to be asked words, and ${type.label} is not`);
|
|
4368
|
+
else if (!type?.local && !String((p ).model || "").trim()) bad.push("no model on the grader's connection");
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4240
4372
|
/** An eval's score from its metrics' readings, the way [mode] says: all must
|
|
4241
4373
|
pass, or weighted points against [threshold]. What a case metric found,
|
|
4242
4374
|
missed and invented is carried up, for a run's totals. Null for none. */
|
|
@@ -4265,7 +4397,7 @@ async function readMetrics(list , input , ctx
|
|
|
4265
4397
|
const r = await readMetric(m, input, ctx);
|
|
4266
4398
|
if (r) metrics.push(r);
|
|
4267
4399
|
}
|
|
4268
|
-
return scoreOf(metrics, mode, threshold);
|
|
4400
|
+
return erroredScore(scoreOf(metrics, mode, threshold), input.error);
|
|
4269
4401
|
}
|
|
4270
4402
|
|
|
4271
4403
|
/** [list] read over a whole run, synchronously: each metric over the run's
|
|
@@ -4319,14 +4451,7 @@ LEGACY_TESTS.metrics = {
|
|
|
4319
4451
|
// The eval's own grader, or the lab's.
|
|
4320
4452
|
const grader = isRef(t.grader) ? t.grader : isRef(ctx.grader) ? ctx.grader : null;
|
|
4321
4453
|
if (gradedIn(t.metrics) && !grader) bad.push("a model-graded metric needs a grader: pick one on the eval, or make a connection the lab's grader");
|
|
4322
|
-
|
|
4323
|
-
const p = grader && ctx.profiles ? ctx.profiles(grader.id) : null;
|
|
4324
|
-
if (grader && ctx.profiles && !p) bad.push(`Connection ${grader.name || grader.id} not found`);
|
|
4325
|
-
else if (p) {
|
|
4326
|
-
const type = CONNECTION_TYPES[typeOf(p )];
|
|
4327
|
-
if (type && !(type.answers ?? ["prompt"]).includes("prompt")) bad.push(`the grader has to be asked words, and ${type.label} is not`);
|
|
4328
|
-
else if (!type?.local && !String((p ).model || "").trim()) bad.push("no model on the grader's connection");
|
|
4329
|
-
}
|
|
4454
|
+
graderProblems(grader, ctx, bad);
|
|
4330
4455
|
},
|
|
4331
4456
|
rules: t => (Array.isArray(t.metrics) ? t.metrics : []).map((m , x ) => ({
|
|
4332
4457
|
key: `m${x}`, label: (isStr(m.metric) && m.metric) || METRICS[m.type]?.label || m.type,
|
|
@@ -4438,6 +4563,11 @@ EVAL_TYPES.group = {
|
|
|
4438
4563
|
if (held && t.pin != null && typeof held.versions === "number" && t.pin > held.versions) {
|
|
4439
4564
|
bad.push(`${g.name || g.id} has no version ${t.pin}`);
|
|
4440
4565
|
}
|
|
4566
|
+
// The grader the group asks: the run's copy of the link carries it, a
|
|
4567
|
+
// pipeline's reads it from the group as the lab lists it. One that is
|
|
4568
|
+
// gone would fail every item on a metric that cannot run (#545).
|
|
4569
|
+
const grader = isRef(t.grader) ? t.grader : isRef(held?.grader) ? held .grader : null;
|
|
4570
|
+
graderProblems(grader, ctx, bad);
|
|
4441
4571
|
return;
|
|
4442
4572
|
}
|
|
4443
4573
|
if (!isObj(own)) return void bad.push("an eval links a Library group, or holds its own");
|
|
@@ -4454,14 +4584,7 @@ EVAL_TYPES.group = {
|
|
|
4454
4584
|
// The group's own grader, or the lab's.
|
|
4455
4585
|
const grader = isRef(own.grader) ? own.grader : isRef(ctx.grader) ? ctx.grader : null;
|
|
4456
4586
|
if (gradedIn(own.every) && !grader) bad.push("a model-graded metric needs a grader: pick one on the eval, or make a connection the lab's grader");
|
|
4457
|
-
|
|
4458
|
-
const p = grader && ctx.profiles ? ctx.profiles(grader.id) : null;
|
|
4459
|
-
if (grader && ctx.profiles && !p) bad.push(`Connection ${grader.name || grader.id} not found`);
|
|
4460
|
-
else if (p) {
|
|
4461
|
-
const type = CONNECTION_TYPES[typeOf(p )];
|
|
4462
|
-
if (type && !(type.answers ?? ["prompt"]).includes("prompt")) bad.push(`the grader has to be asked words, and ${type.label} is not`);
|
|
4463
|
-
else if (!type?.local && !String((p ).model || "").trim()) bad.push("no model on the grader's connection");
|
|
4464
|
-
}
|
|
4587
|
+
graderProblems(grader, ctx, bad);
|
|
4465
4588
|
},
|
|
4466
4589
|
rules: t => ownList(t).map((m , x ) => ({
|
|
4467
4590
|
key: `m${x}`, label: (isStr(m.metric) && m.metric) || METRICS[m.type]?.label || m.type,
|
|
@@ -4818,16 +4941,18 @@ STEP_TYPES.httpRequest = {
|
|
|
4818
4941
|
label: "HTTP Request", slot: "target", asks: "request", verbatim: true, modelFrom: "step",
|
|
4819
4942
|
description: "Sends a Power Automate step's own request, rebuilt from each record, with each target's words in it.",
|
|
4820
4943
|
firstJobOnly: "an HTTP Request is sent from its item's record, so it is job 1's",
|
|
4821
|
-
// A new target starts from the flow's own words and fields
|
|
4822
|
-
|
|
4944
|
+
// A new target starts from the flow's own words and fields, read by the
|
|
4945
|
+
// job's HTTP API ([step] carries it: requestStepOf).
|
|
4946
|
+
newStep: (step) => ({ type: "httpRequest", method: step.method, path: step.path,
|
|
4823
4947
|
query: clone(step.query), body: clone(step.body),
|
|
4824
4948
|
...(HTTP_APIS[step.api] ?? HTTP_APIS.raw ).cellOf(step.body) }),
|
|
4825
4949
|
in: "item", out: "text",
|
|
4826
4950
|
apply: "runPipeline",
|
|
4827
|
-
fields: ["type", "
|
|
4951
|
+
fields: ["type", "method", "path", "query", "body", "prompt", "connection", "profile", "from", "system", "model", "settings", "prefill"],
|
|
4952
|
+
// [step] carries its job's HTTP API, where the job has a flow step to say
|
|
4953
|
+
// it (validatePipeline); the flow step's own check names an unknown one.
|
|
4828
4954
|
validate(step, _ctx, bad, at){
|
|
4829
4955
|
const api = HTTP_APIS[step.api];
|
|
4830
|
-
if (!api) bad.push(`${at}: "${step.api}" is not an HTTP API this lab has`);
|
|
4831
4956
|
if (!HTTP_METHODS.includes(step.method)) bad.push(`${at}: the method is one of ${HTTP_METHODS.join(", ")}`);
|
|
4832
4957
|
if (!isStr(step.path) || !/^[/@]/.test(step.path)) bad.push(`${at}: the path starts with /`);
|
|
4833
4958
|
if (!isObj(step.query) || !Object.values(step.query).every(isStr)) bad.push(`${at}: the query is names to text`);
|
|
@@ -4850,8 +4975,12 @@ STEP_TYPES.httpRequest = {
|
|
|
4850
4975
|
}
|
|
4851
4976
|
}
|
|
4852
4977
|
if (isStr(step.prompt)) {
|
|
4853
|
-
|
|
4854
|
-
|
|
4978
|
+
// Built here as each call builds it, so an expression none of them
|
|
4979
|
+
// could read is refused before the run rather than failing every call.
|
|
4980
|
+
try {
|
|
4981
|
+
const why = templateProblem([step.path, step.query, api.place(step.body, step )]);
|
|
4982
|
+
if (why) bad.push(`${at}: the request could not be built -- ${why}`);
|
|
4983
|
+
} catch (e) { bad.push(`${at}: ${(e ).message}`); }
|
|
4855
4984
|
}
|
|
4856
4985
|
},
|
|
4857
4986
|
};
|
|
@@ -4876,7 +5005,6 @@ STEP_TYPES.echo = {
|
|
|
4876
5005
|
// the flow reads one. No profile, no prompt -- the record is the reply.
|
|
4877
5006
|
STEP_TYPES.recorded = {
|
|
4878
5007
|
label: "Recorded", slot: "target", asks: "nothing", local: true, replaces: "recorded",
|
|
4879
|
-
description: "Pre-recorded from previous workflow runs, replays only.",
|
|
4880
5008
|
firstJobOnly: "a call's recorded reply is job 1's",
|
|
4881
5009
|
in: "item", out: "text",
|
|
4882
5010
|
apply: "runPipeline",
|
|
@@ -5104,7 +5232,7 @@ function requestStepOf(doc
|
|
|
5104
5232
|
|
|
5105
5233
|
/** What httpRequestOf builds a request from: a target's template, and the
|
|
5106
5234
|
loop its flow step sits in. */
|
|
5107
|
-
|
|
5235
|
+
|
|
5108
5236
|
/** What httpReplyOf reads a reply with: the flow step, its API, its reading. */
|
|
5109
5237
|
|
|
5110
5238
|
|
|
@@ -5706,10 +5834,14 @@ function upgradePipeline (doc , ctx = {}) {
|
|
|
5706
5834
|
}
|
|
5707
5835
|
return localSteps(ownBodiesNow(out), ctx) ;
|
|
5708
5836
|
}
|
|
5709
|
-
if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes(doc.version )) return doc;
|
|
5837
|
+
if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].includes(doc.version )) return doc;
|
|
5838
|
+
// Every version reads as 16 first, then as 17: its requests read by their
|
|
5839
|
+
// job's HTTP API (requestsOnJobApi).
|
|
5840
|
+
const seventeen = (d ) => localSteps(ownBodiesNow(requestsOnJobApi(d)), ctx) ;
|
|
5841
|
+
if (doc.version === 16) return seventeen(cutRefs(clone(doc) ));
|
|
5710
5842
|
// Every version reads as 15 first, then as 16: its Target profiles split
|
|
5711
5843
|
// into AI Connections and Connection Profiles (answersFromProfiles).
|
|
5712
|
-
const sixteen = (fifteen ) =>
|
|
5844
|
+
const sixteen = (fifteen ) => seventeen(answersFromProfiles(fifteen, ctx));
|
|
5713
5845
|
if (doc.version === 15) return sixteen(cutRefs(clone(doc) ));
|
|
5714
5846
|
// Versions 13 and 14 read as 15 by migrating a job's cleaning modifier steps
|
|
5715
5847
|
// and its List `preamble` setting into Pre-/Post-format filter sets
|
|
@@ -5854,6 +5986,37 @@ function localSteps(doc , ctx ) {
|
|
|
5854
5986
|
return next;
|
|
5855
5987
|
}
|
|
5856
5988
|
|
|
5989
|
+
/**
|
|
5990
|
+
* Version 16 to 17 (#477): a request step has no HTTP API of its own; its
|
|
5991
|
+
* job's flow step says it. A step that named another -- Any endpoint over an
|
|
5992
|
+
* Anthropic flow, say -- keeps its body as it would have sent it, read again
|
|
5993
|
+
* by the job's (its words and fields as that API holds them). A request in a
|
|
5994
|
+
* job with no flow step, which could never run, is a Prompt keeping its words.
|
|
5995
|
+
*/
|
|
5996
|
+
function requestsOnJobApi(next ) {
|
|
5997
|
+
next.version = PIPELINE_VERSION;
|
|
5998
|
+
const jobs = Array.isArray(next.jobs) ? next.jobs : [];
|
|
5999
|
+
const step = (st , k ) => {
|
|
6000
|
+
if (!isObj(st) || st.type !== "httpRequest") return st;
|
|
6001
|
+
const { api: own, ...rest } = st;
|
|
6002
|
+
const flow = isObj(jobs[k]) ? flowStepOf(jobs[k] ) : null;
|
|
6003
|
+
if (!flow) {
|
|
6004
|
+
return { type: "prompt", prompt: rest.prompt,
|
|
6005
|
+
...Object.fromEntries(["connection", "profile", "from"].filter(f => rest[f] != null).map(f => [f, rest[f]])) };
|
|
6006
|
+
}
|
|
6007
|
+
const was = HTTP_APIS[own ], now = HTTP_APIS[flow.api];
|
|
6008
|
+
if (!was || !now || own === flow.api) return rest;
|
|
6009
|
+
let body = rest.body;
|
|
6010
|
+
try { body = was.place(rest.body, rest ); } catch { /* words that never placed: the template stands */ }
|
|
6011
|
+
const { system: _s, model: _m, settings: _t, prefill: _p, ...kept } = rest;
|
|
6012
|
+
return { ...kept, body, ...now.cellOf(body) };
|
|
6013
|
+
};
|
|
6014
|
+
if (Array.isArray(next.targets)) {
|
|
6015
|
+
next.targets = next.targets.map((t ) => (isObj(t) && Array.isArray(t.steps) ? { ...t, steps: t.steps.map(step) } : t));
|
|
6016
|
+
}
|
|
6017
|
+
return next;
|
|
6018
|
+
}
|
|
6019
|
+
|
|
5857
6020
|
/** [next] as version 9 held it, from any version before 10. */
|
|
5858
6021
|
function nineOf(next , ctx ) {
|
|
5859
6022
|
if (next.version === 9) { profileRefs(next); return next; }
|
|
@@ -5975,7 +6138,9 @@ function validatePipeline(input , ctx = {}) {
|
|
|
5975
6138
|
if (entry.local && (st.profile != null || st.connection != null)) return void bad.push(`${where}: ${entry.label} asks no connection`);
|
|
5976
6139
|
if (k > 0 && entry.firstJobOnly) return void bad.push(`${where}: ${entry.firstJobOnly}`);
|
|
5977
6140
|
onlyFields(st, where, entry.fields ?? [], bad);
|
|
5978
|
-
|
|
6141
|
+
// A request is read and placed by its job's HTTP API.
|
|
6142
|
+
const flow = entry.asks === "request" ? flowStepOf(doc.jobs[k]) : null;
|
|
6143
|
+
entry.validate(flow ? { ...st, api: flow.api } : st, c, bad, where);
|
|
5979
6144
|
answerRefsProblems(st, where, bad);
|
|
5980
6145
|
if (st.from != null && !isRef(st.from)) bad.push(`${where} has to name the prompt it was picked from as { id, name }`);
|
|
5981
6146
|
// A request is a flow step's, rebuilt from its records, and its
|
|
@@ -6031,6 +6196,10 @@ function validatePipeline(input , ctx = {}) {
|
|
|
6031
6196
|
? (c.sources?.(content.ref?.id)?.files ?? content.files ?? null) : null;
|
|
6032
6197
|
const nonText = (contentFiles || []).map(f => String((f )?.name ?? f))
|
|
6033
6198
|
.find(n => !/\.(?:txt|md|csv)$/i.test(n));
|
|
6199
|
+
// A document goes whole to the API, so only a type that reads one is
|
|
6200
|
+
// handed it -- never sent as an image or as text it is not.
|
|
6201
|
+
const docType = (contentFiles || []).map(f => documentTypeOf(String((f )?.name ?? f)))
|
|
6202
|
+
.find(Boolean) ?? null;
|
|
6034
6203
|
targets.forEach((_, i) => {
|
|
6035
6204
|
const conns = doc.jobs.map((_ , k ) => (targetEntryOf(doc, i, k)?.local ? localConnOf(targetEntryOf(doc, i, k)) : lookup(targetAnswerOf(doc, i, k) )));
|
|
6036
6205
|
conns.forEach((p , k ) => {
|
|
@@ -6050,6 +6219,19 @@ function validatePipeline(input , ctx = {}) {
|
|
|
6050
6219
|
const type = CONNECTION_TYPES[typeOf(p )];
|
|
6051
6220
|
if (type && !(type.answers ?? ["prompt"]).includes(step.asks ?? "prompt")) {
|
|
6052
6221
|
bad.push(`${n > 1 ? `${jobLabel(doc, k)}, ` : ""}${targetLabel(doc, i)}: ${step.label ?? "this step"} needs a profile that answers it, and ${type.label} does not`);
|
|
6222
|
+
return;
|
|
6223
|
+
}
|
|
6224
|
+
// A request goes as the flow wrote it, so its connection has to speak
|
|
6225
|
+
// the job's HTTP API; one the lab cannot tell (Any endpoint) passes.
|
|
6226
|
+
const api = step.asks === "request" ? flowStepOf(doc.jobs[k])?.api : undefined;
|
|
6227
|
+
const speaks = api && api !== "raw" && type?.speaks ? type.speaks(p ) : null;
|
|
6228
|
+
if (api && speaks && speaks !== api) {
|
|
6229
|
+
const name = targetAnswerOf(doc, i, k)?.connection.name || type .label;
|
|
6230
|
+
bad.push(`${n > 1 ? `${jobLabel(doc, k)}, ` : ""}${targetLabel(doc, i)}: ${name} can't send an ${HTTP_APIS[api]?.label ?? api} request`);
|
|
6231
|
+
}
|
|
6232
|
+
if (docType && type && !type.documents && sendsImage(doc.jobs[k])) {
|
|
6233
|
+
const name = targetAnswerOf(doc, i, k)?.connection.name || type.label;
|
|
6234
|
+
bad.push(`${n > 1 ? `${jobLabel(doc, k)}, ` : ""}${targetLabel(doc, i)}: ${name} can't read ${docType.label}s`);
|
|
6053
6235
|
}
|
|
6054
6236
|
});
|
|
6055
6237
|
// The model is the profile's unless the target's step carries it.
|
|
@@ -6853,7 +7035,9 @@ function scenarioEvals(run , i , items
|
|
|
6853
7035
|
return evals.map((t, j) => {
|
|
6854
7036
|
const type = EVAL_TYPES[t.type];
|
|
6855
7037
|
const whole = isWholeRun(type, t);
|
|
6856
|
-
|
|
7038
|
+
// An unnamed eval that links a group goes by the group's name, as the
|
|
7039
|
+
// Evals Summary names it, not "Eval 1" (#519).
|
|
7040
|
+
const base = { id: t.id, label: (t.name || "").trim() || t.group?.name || evalLabel({ evals }, j), whole, skipped: skipRest,
|
|
6857
7041
|
verdict: null , rules: type?.rules?.(t) ?? [], items: cells.map(() => null) ,
|
|
6858
7042
|
ran: 0, passed: 0, skippedItems: 0 };
|
|
6859
7043
|
if (skipRest) {
|
|
@@ -6993,6 +7177,11 @@ function validateEvals(ev , files
|
|
|
6993
7177
|
if (scored.length > 1) say("expects its answer discarded, and states something the answer should hold too");
|
|
6994
7178
|
continue;
|
|
6995
7179
|
}
|
|
7180
|
+
if (scored.some(m => m.type === "refused" && !m.not)) {
|
|
7181
|
+
// Nor does a call that was refused: there is no answer.
|
|
7182
|
+
if (scored.length > 1) say("expects its call refused, and states something the answer should hold too");
|
|
7183
|
+
continue;
|
|
7184
|
+
}
|
|
6996
7185
|
const wants = scored.filter(m => !m.not && (m.type === "contains-all" || m.type === "contains-any"));
|
|
6997
7186
|
const forbids = scored.filter(m => m.not && m.type === "contains");
|
|
6998
7187
|
for (const m of wants) for (const t of values(m)) if (!matchable(t)) say(`expects ${t}, which its own scorer cannot match`);
|
|
@@ -7106,7 +7295,7 @@ export {
|
|
|
7106
7295
|
loopReplyError, preparedSize,
|
|
7107
7296
|
termIn, forbiddenIn, readCase, gradedSetFrom, caseItem, caseMetrics, metricLines, upgradeDatasetBody, takeAsExpected, readGroup, readGroupRun, datasetRules, upgradeResults, emptyTally, addToTally, isHostedUrl,
|
|
7108
7297
|
SEED, REPLY_TOKENS_BEFORE, ANTHROPIC_MAX_TOKENS, asNumber, pinReplyTokens, mappingsFor,
|
|
7109
|
-
CONNECTION_TYPES, HTTP_APIS, httpApiFor, httpRequestOf, httpReplyOf, readFlowReply, WdlError, localAnswer, typeOf, profileType, convertProfile, splitOllama, DECODING_KEYS, apiBase,
|
|
7298
|
+
CONNECTION_TYPES, DOCUMENT_TYPES, documentTypeOf, documentUrl, attachmentOf, HTTP_APIS, httpApiFor, httpRequestOf, httpReplyOf, readFlowReply, WdlError, localAnswer, typeOf, listedConnection, profileType, convertProfile, splitOllama, DECODING_KEYS, apiBase,
|
|
7110
7299
|
EDGE_448, budgetLabel, IMAGE_FORMATS, encoderQuality,
|
|
7111
7300
|
tallyPercent, runPipeline, validateEvals, evalsWarnings, evalsJson, readList,
|
|
7112
7301
|
scoreSingle, singleRules, parseCount, lengthHolds, countHolds, COUNT_OPS, LENGTH_OPS,
|
|
@@ -7119,8 +7308,8 @@ export {
|
|
|
7119
7308
|
CONNECTION_FIELDS, SETTING_KEYS, OPTION_FIELDS, jobLabel, scenarioLabel, jobWords, versionProblem,
|
|
7120
7309
|
contentOf, withContent, replyOf,
|
|
7121
7310
|
targetsOf, targetLetter, targetLabel, targetStepOf, targetEntryOf, jobTargetType, targetAnswerOf, targetAnswerKey, recordedTargetOf, withTarget,
|
|
7122
|
-
tokensOf, withTokens, sendsImage, withImage, flowStepOf, readAsOf, outOf, withOut, slotEntry, SLOTS,
|
|
7123
|
-
filterSetsOf, withFilterSets, stageModifiers, beforeFilters,
|
|
7311
|
+
tokensOf, withTokens, sendsImage, withImage, flowStepOf, requestStepOf, readAsOf, outOf, withOut, slotEntry, SLOTS,
|
|
7312
|
+
filterSetsOf, withFilterSets, stageModifiers, beforeFilters, gradedText,
|
|
7124
7313
|
blankPipeline, upgradePipeline, fatProfileRef, newId, newEval, newLink, validatePipeline, resolvePipeline, pipelineOfRun, stagesFor, profileIds, answerKeys,
|
|
7125
7314
|
scenarioProfile, lastKind, lastModifier, modifierSummary, itemScores, scenarioEvals, scenarioPasses, groupPass, overallVerdict, passVerdict, isSkipped, failedScore,
|
|
7126
7315
|
evalLabel, evalsOf, evalsDataset, casesRef, groupOf, groupOfMetrics, pipelineWarnings, itemsFound, previewOf, isWholeRun, EVAL_FIELDS,
|
package/lab/flows/wdl.mjs
CHANGED
|
@@ -363,6 +363,25 @@ function endOf(s , from ) {
|
|
|
363
363
|
throw new WdlError("an @{ is never closed");
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
/** Why [template] cannot be evaluated whatever it is evaluated against -- an
|
|
367
|
+
expression in it that does not parse -- or null. Read before a run, so a
|
|
368
|
+
template every call would fail on is refused rather than sent. */
|
|
369
|
+
export function templateProblem(template ) {
|
|
370
|
+
try { walk(template); return null; }
|
|
371
|
+
catch (e) { if (e instanceof WdlError) return e.message; throw e; }
|
|
372
|
+
function walk(v ) {
|
|
373
|
+
if (Array.isArray(v)) return v.forEach(walk);
|
|
374
|
+
if (isObj(v)) return Object.values(v).forEach(walk);
|
|
375
|
+
if (typeof v !== "string" || v.startsWith("@@")) return;
|
|
376
|
+
if (v.startsWith("@") && !v.startsWith("@{")) return void parse(v.slice(1));
|
|
377
|
+
for (let open = v.indexOf("@{"); open >= 0; open = v.indexOf("@{", open)) {
|
|
378
|
+
const close = endOf(v, open + 2);
|
|
379
|
+
parse(v.slice(open + 2, close));
|
|
380
|
+
open = close + 1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
366
385
|
/** [template] with every expression in it evaluated: each string that is
|
|
367
386
|
one, or holds one, wherever it sits in objects and arrays. */
|
|
368
387
|
export function evaluate(template , ctx ) {
|