@workglow/eval 0.3.26
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/README.md +179 -0
- package/dist/commands/dataset.d.ts +9 -0
- package/dist/commands/dataset.d.ts.map +1 -0
- package/dist/commands/report.d.ts +11 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/run.d.ts +9 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/evals/classify.d.ts +25 -0
- package/dist/evals/classify.d.ts.map +1 -0
- package/dist/evals/extract.d.ts +26 -0
- package/dist/evals/extract.d.ts.map +1 -0
- package/dist/evals/prompt.d.ts +12 -0
- package/dist/evals/prompt.d.ts.map +1 -0
- package/dist/evals/runner.d.ts +25 -0
- package/dist/evals/runner.d.ts.map +1 -0
- package/dist/evals/similarity.d.ts +15 -0
- package/dist/evals/similarity.d.ts.map +1 -0
- package/dist/evals/types.d.ts +39 -0
- package/dist/evals/types.d.ts.map +1 -0
- package/dist/hf/auth.d.ts +8 -0
- package/dist/hf/auth.d.ts.map +1 -0
- package/dist/hf/datasetsServer.d.ts +14 -0
- package/dist/hf/datasetsServer.d.ts.map +1 -0
- package/dist/hf/hubFiles.d.ts +38 -0
- package/dist/hf/hubFiles.d.ts.map +1 -0
- package/dist/hf/ids.d.ts +20 -0
- package/dist/hf/ids.d.ts.map +1 -0
- package/dist/hf/pullDataset.d.ts +26 -0
- package/dist/hf/pullDataset.d.ts.map +1 -0
- package/dist/hf/types.d.ts +33 -0
- package/dist/hf/types.d.ts.map +1 -0
- package/dist/models.d.ts +49 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/providers.d.ts +17 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/report/aggregate.d.ts +35 -0
- package/dist/report/aggregate.d.ts.map +1 -0
- package/dist/score/classification.d.ts +29 -0
- package/dist/score/classification.d.ts.map +1 -0
- package/dist/score/correlation.d.ts +12 -0
- package/dist/score/correlation.d.ts.map +1 -0
- package/dist/score/extraction.d.ts +47 -0
- package/dist/score/extraction.d.ts.map +1 -0
- package/dist/storage.d.ts +152 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/test/aggregate.test.d.ts +7 -0
- package/dist/test/aggregate.test.d.ts.map +1 -0
- package/dist/test/extractSmoke.e2e.test.d.ts +7 -0
- package/dist/test/extractSmoke.e2e.test.d.ts.map +1 -0
- package/dist/test/extraction.test.d.ts +7 -0
- package/dist/test/extraction.test.d.ts.map +1 -0
- package/dist/test/ggufSmoke.e2e.test.d.ts +7 -0
- package/dist/test/ggufSmoke.e2e.test.d.ts.map +1 -0
- package/dist/test/hubFiles.test.d.ts +7 -0
- package/dist/test/hubFiles.test.d.ts.map +1 -0
- package/dist/test/ids.test.d.ts +7 -0
- package/dist/test/ids.test.d.ts.map +1 -0
- package/dist/test/liveSimilarity.e2e.test.d.ts +7 -0
- package/dist/test/liveSimilarity.e2e.test.d.ts.map +1 -0
- package/dist/test/models.test.d.ts +7 -0
- package/dist/test/models.test.d.ts.map +1 -0
- package/dist/test/score.test.d.ts +7 -0
- package/dist/test/score.test.d.ts.map +1 -0
- package/dist/test/storage.test.d.ts +7 -0
- package/dist/test/storage.test.d.ts.map +1 -0
- package/dist/util.d.ts +13 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/worker_hft.d.ts +7 -0
- package/dist/worker_hft.d.ts.map +1 -0
- package/dist/worker_hft.js +9 -0
- package/dist/worker_llamacpp.d.ts +7 -0
- package/dist/worker_llamacpp.d.ts.map +1 -0
- package/dist/worker_llamacpp.js +3 -0
- package/dist/workglow-eval.d.ts +8 -0
- package/dist/workglow-eval.d.ts.map +1 -0
- package/dist/workglow-eval.js +1266 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/workglow-eval.ts
|
|
5
|
+
import { program } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/hf/auth.ts
|
|
8
|
+
function hfAuthHeaders(token) {
|
|
9
|
+
const t = token ?? process.env.HF_TOKEN;
|
|
10
|
+
return t ? { Authorization: `Bearer ${t}` } : {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/hf/ids.ts
|
|
14
|
+
var ID_SEGMENT = /^[\w-][\w.-]*$/;
|
|
15
|
+
function sanitizeHubRepoId(id) {
|
|
16
|
+
const segments = id.split("/");
|
|
17
|
+
if (segments.length < 1 || segments.length > 2) {
|
|
18
|
+
throw new Error(`invalid HuggingFace repo id "${id}" \u2014 expected "name" or "org/name"`);
|
|
19
|
+
}
|
|
20
|
+
return segments.map((segment) => encodeIdSegment(segment, id)).join("/");
|
|
21
|
+
}
|
|
22
|
+
function sanitizeHubFilePath(path) {
|
|
23
|
+
const segments = path.split("/");
|
|
24
|
+
if (segments.length === 0)
|
|
25
|
+
throw new Error(`invalid hub file path "${path}"`);
|
|
26
|
+
return segments.map((segment) => {
|
|
27
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
28
|
+
throw new Error(`invalid hub file path segment in "${path}"`);
|
|
29
|
+
}
|
|
30
|
+
return encodeURIComponent(segment);
|
|
31
|
+
}).join("/");
|
|
32
|
+
}
|
|
33
|
+
function encodeIdSegment(segment, id) {
|
|
34
|
+
if (!ID_SEGMENT.test(segment)) {
|
|
35
|
+
throw new Error(`invalid HuggingFace repo id "${id}" \u2014 segments may contain letters, digits, ` + `".", "_", "-" and must not start with "."`);
|
|
36
|
+
}
|
|
37
|
+
return encodeURIComponent(segment);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/hf/datasetsServer.ts
|
|
41
|
+
var DATASETS_SERVER = "https://datasets-server.huggingface.co";
|
|
42
|
+
var PAGE_SIZE = 100;
|
|
43
|
+
async function getJson(url, token) {
|
|
44
|
+
const res = await fetch(url, { headers: hfAuthHeaders(token) });
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw new Error(`datasets-server request failed (${res.status}): ${url}`);
|
|
47
|
+
}
|
|
48
|
+
return await res.json();
|
|
49
|
+
}
|
|
50
|
+
async function resolveConfig(dataset, token) {
|
|
51
|
+
const data = await getJson(`${DATASETS_SERVER}/splits?dataset=${encodeURIComponent(dataset)}`, token);
|
|
52
|
+
const config = data.splits[0]?.config;
|
|
53
|
+
if (!config)
|
|
54
|
+
throw new Error(`datasets-server reported no configs for ${dataset}`);
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
function extractLabelNames(features) {
|
|
58
|
+
const labelNames = {};
|
|
59
|
+
for (const feature of features) {
|
|
60
|
+
const type = feature.type;
|
|
61
|
+
if (type?._type === "ClassLabel" && Array.isArray(type.names)) {
|
|
62
|
+
labelNames[feature.name] = type.names;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return labelNames;
|
|
66
|
+
}
|
|
67
|
+
async function fetchViaDatasetsServer(options) {
|
|
68
|
+
const { split, limit, token } = options;
|
|
69
|
+
const dataset = sanitizeHubRepoId(options.dataset);
|
|
70
|
+
const config = options.config ?? await resolveConfig(dataset, token);
|
|
71
|
+
const offset = options.offset ?? 0;
|
|
72
|
+
const rows = [];
|
|
73
|
+
let columns = [];
|
|
74
|
+
let labelNames = {};
|
|
75
|
+
let truncatedRows = 0;
|
|
76
|
+
while (rows.length < limit) {
|
|
77
|
+
const length = Math.min(PAGE_SIZE, limit - rows.length);
|
|
78
|
+
const url = `${DATASETS_SERVER}/rows?dataset=${encodeURIComponent(dataset)}` + `&config=${encodeURIComponent(config)}&split=${encodeURIComponent(split)}` + `&offset=${offset + rows.length}&length=${length}`;
|
|
79
|
+
const page = await getJson(url, token);
|
|
80
|
+
if (rows.length === 0) {
|
|
81
|
+
columns = page.features.map((f) => f.name);
|
|
82
|
+
labelNames = extractLabelNames(page.features);
|
|
83
|
+
}
|
|
84
|
+
for (const r of page.rows) {
|
|
85
|
+
if (r.truncated_cells && r.truncated_cells.length > 0)
|
|
86
|
+
truncatedRows++;
|
|
87
|
+
rows.push(r.row);
|
|
88
|
+
}
|
|
89
|
+
if (page.rows.length < length || offset + rows.length >= page.num_rows_total)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (truncatedRows > 0) {
|
|
93
|
+
console.error(`warning: the datasets viewer truncated large cells in ${truncatedRows} row(s); ` + `evals will run on the truncated text`);
|
|
94
|
+
}
|
|
95
|
+
return { rows, columns, labelNames, config, source: "datasets-server" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/hf/hubFiles.ts
|
|
99
|
+
import { parquetMetadataAsync, parquetReadObjects } from "hyparquet";
|
|
100
|
+
import { compressors } from "hyparquet-compressors";
|
|
101
|
+
import { gunzipSync } from "zlib";
|
|
102
|
+
var HUB = "https://huggingface.co";
|
|
103
|
+
var SUPPORTED_EXTENSIONS = [".parquet", ".jsonl.gz", ".jsonl", ".ndjson", ".json.gz", ".json"];
|
|
104
|
+
function fileExtension(path) {
|
|
105
|
+
const lower = path.toLowerCase();
|
|
106
|
+
return SUPPORTED_EXTENSIONS.find((ext) => lower.endsWith(ext));
|
|
107
|
+
}
|
|
108
|
+
function escapeRegExp(text) {
|
|
109
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
110
|
+
}
|
|
111
|
+
function scoreFileForSplit(path, split) {
|
|
112
|
+
if (!fileExtension(path))
|
|
113
|
+
return;
|
|
114
|
+
const lower = path.toLowerCase();
|
|
115
|
+
const name = lower.split("/").pop() ?? lower;
|
|
116
|
+
const s = split.toLowerCase();
|
|
117
|
+
if (name === `${s}${fileExtension(name)}`)
|
|
118
|
+
return 3;
|
|
119
|
+
if (name.startsWith(`${s}-`) || name.startsWith(`${s}.`) || name.startsWith(`${s}_`))
|
|
120
|
+
return 2;
|
|
121
|
+
if (new RegExp(`(^|[/_.-])${escapeRegExp(s)}([/_.-]|$)`).test(lower))
|
|
122
|
+
return 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
function selectSplitFiles(files, split) {
|
|
126
|
+
let best = 0;
|
|
127
|
+
let selected = [];
|
|
128
|
+
for (const path of files) {
|
|
129
|
+
const score = scoreFileForSplit(path, split) ?? 0;
|
|
130
|
+
if (score > best) {
|
|
131
|
+
best = score;
|
|
132
|
+
selected = [path];
|
|
133
|
+
} else if (score === best && score > 0) {
|
|
134
|
+
selected.push(path);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return selected.sort();
|
|
138
|
+
}
|
|
139
|
+
function sanitizeValue(value) {
|
|
140
|
+
if (typeof value === "bigint") {
|
|
141
|
+
return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
|
|
142
|
+
}
|
|
143
|
+
if (value instanceof Date)
|
|
144
|
+
return value.toISOString();
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return value.map(sanitizeValue);
|
|
147
|
+
if (value !== null && typeof value === "object" && value.constructor === Object) {
|
|
148
|
+
const out = {};
|
|
149
|
+
for (const [key, inner] of Object.entries(value))
|
|
150
|
+
out[key] = sanitizeValue(inner);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function sanitizeRow(row) {
|
|
156
|
+
return sanitizeValue(row);
|
|
157
|
+
}
|
|
158
|
+
function parseJsonLines(text) {
|
|
159
|
+
return text.split(`
|
|
160
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
161
|
+
}
|
|
162
|
+
async function parseDatasetFile(path, bytes, limit) {
|
|
163
|
+
const ext = fileExtension(path);
|
|
164
|
+
if (ext === ".parquet") {
|
|
165
|
+
const metadata = await parquetMetadataAsync(bytes);
|
|
166
|
+
const labelNames = {};
|
|
167
|
+
const hfMeta = metadata.key_value_metadata?.find((kv) => kv.key === "huggingface");
|
|
168
|
+
if (hfMeta?.value) {
|
|
169
|
+
try {
|
|
170
|
+
const info = JSON.parse(hfMeta.value);
|
|
171
|
+
for (const [name, feature] of Object.entries(info.info?.features ?? {})) {
|
|
172
|
+
if (feature?._type === "ClassLabel" && Array.isArray(feature.names)) {
|
|
173
|
+
labelNames[name] = feature.names;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
const rows = await parquetReadObjects({
|
|
179
|
+
file: bytes,
|
|
180
|
+
compressors,
|
|
181
|
+
metadata,
|
|
182
|
+
rowEnd: limit
|
|
183
|
+
});
|
|
184
|
+
return { rows: rows.map(sanitizeRow), labelNames };
|
|
185
|
+
}
|
|
186
|
+
const text = ext?.endsWith(".gz") ? gunzipSync(Buffer.from(bytes)).toString("utf-8") : new TextDecoder().decode(bytes);
|
|
187
|
+
if (ext === ".json" || ext === ".json.gz") {
|
|
188
|
+
const parsed = JSON.parse(text);
|
|
189
|
+
const rows = Array.isArray(parsed) ? parsed : parsed.rows ?? parsed.data ?? [];
|
|
190
|
+
return { rows: rows.slice(0, limit), labelNames: {} };
|
|
191
|
+
}
|
|
192
|
+
return { rows: parseJsonLines(text).slice(0, limit), labelNames: {} };
|
|
193
|
+
}
|
|
194
|
+
async function listRepoFiles(dataset, token) {
|
|
195
|
+
const url = `${HUB}/api/datasets/${sanitizeHubRepoId(dataset)}/tree/main?recursive=true`;
|
|
196
|
+
const res = await fetch(url, { headers: hfAuthHeaders(token) });
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
throw new Error(`hub tree listing failed (${res.status}) for dataset ${dataset}`);
|
|
199
|
+
}
|
|
200
|
+
const entries = await res.json();
|
|
201
|
+
return entries.filter((e) => e.type === "file").map((e) => e.path);
|
|
202
|
+
}
|
|
203
|
+
async function downloadRepoFile(dataset, path, token) {
|
|
204
|
+
const url = `${HUB}/datasets/${sanitizeHubRepoId(dataset)}/resolve/main/${sanitizeHubFilePath(path)}`;
|
|
205
|
+
const res = await fetch(url, { headers: hfAuthHeaders(token) });
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
throw new Error(`hub file download failed (${res.status}) for ${dataset}/${path}`);
|
|
208
|
+
}
|
|
209
|
+
return await res.arrayBuffer();
|
|
210
|
+
}
|
|
211
|
+
async function fetchViaHubFiles(options) {
|
|
212
|
+
const { dataset, split, limit, token } = options;
|
|
213
|
+
const allFiles = await listRepoFiles(dataset, token);
|
|
214
|
+
const configFiles = options.config ? allFiles.filter((f) => f.startsWith(`${options.config}/`) || !f.includes("/")) : allFiles;
|
|
215
|
+
const files = selectSplitFiles(configFiles, split);
|
|
216
|
+
if (files.length === 0) {
|
|
217
|
+
throw new Error(`no data files matching split "${split}" found in ${dataset} ` + `(supported: ${SUPPORTED_EXTENSIONS.join(", ")})`);
|
|
218
|
+
}
|
|
219
|
+
const offset = options.offset ?? 0;
|
|
220
|
+
const wanted = offset + limit;
|
|
221
|
+
const rows = [];
|
|
222
|
+
let labelNames = {};
|
|
223
|
+
for (const file of files) {
|
|
224
|
+
if (rows.length >= wanted)
|
|
225
|
+
break;
|
|
226
|
+
const bytes = await downloadRepoFile(dataset, file, token);
|
|
227
|
+
const parsed = await parseDatasetFile(file, bytes, wanted - rows.length);
|
|
228
|
+
rows.push(...parsed.rows);
|
|
229
|
+
if (Object.keys(parsed.labelNames).length > 0)
|
|
230
|
+
labelNames = parsed.labelNames;
|
|
231
|
+
}
|
|
232
|
+
const windowed = rows.slice(offset, wanted);
|
|
233
|
+
const columns = [...new Set(windowed.flatMap((row) => Object.keys(row)))];
|
|
234
|
+
return { rows: windowed, columns, labelNames, config: options.config ?? "", source: "hub-files" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/hf/pullDataset.ts
|
|
238
|
+
async function fetchDataset(options) {
|
|
239
|
+
try {
|
|
240
|
+
return await fetchViaDatasetsServer(options);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error(`datasets-server unavailable (${err instanceof Error ? err.message : String(err)}); ` + `falling back to direct hub file download`);
|
|
243
|
+
return await fetchViaHubFiles(options);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function pullDatasetIntoStorage(stores, options) {
|
|
247
|
+
const fetched = await fetchDataset(options);
|
|
248
|
+
const { dataset, split } = options;
|
|
249
|
+
const offset = options.offset ?? 0;
|
|
250
|
+
await stores.rows.withTransaction(async (tx) => {
|
|
251
|
+
if (offset > 0) {
|
|
252
|
+
await tx.deleteSearch({ dataset, split, row_index: { value: offset, operator: ">=" } });
|
|
253
|
+
} else {
|
|
254
|
+
await tx.deleteSearch({ dataset, split });
|
|
255
|
+
}
|
|
256
|
+
await tx.putBulk(fetched.rows.map((row, i) => ({
|
|
257
|
+
dataset,
|
|
258
|
+
split,
|
|
259
|
+
row_index: offset + i,
|
|
260
|
+
data: JSON.stringify(row)
|
|
261
|
+
})));
|
|
262
|
+
});
|
|
263
|
+
const numRows = await stores.rows.count({ dataset, split });
|
|
264
|
+
await stores.datasets.put({
|
|
265
|
+
dataset,
|
|
266
|
+
split,
|
|
267
|
+
config: fetched.config || (options.config ?? ""),
|
|
268
|
+
num_rows: numRows,
|
|
269
|
+
columns: JSON.stringify(fetched.columns),
|
|
270
|
+
label_names: Object.keys(fetched.labelNames).length > 0 ? JSON.stringify(fetched.labelNames) : null,
|
|
271
|
+
source: fetched.source,
|
|
272
|
+
fetched_at: new Date().toISOString()
|
|
273
|
+
});
|
|
274
|
+
return { numRows, source: fetched.source };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/util.ts
|
|
278
|
+
function formatTable(rows, columns) {
|
|
279
|
+
if (rows.length === 0)
|
|
280
|
+
return "(none)";
|
|
281
|
+
const widths = columns.map((col) => Math.max(col.length, ...rows.map((row) => (row[col] ?? "").length)));
|
|
282
|
+
const header = columns.map((col, i) => col.padEnd(widths[i])).join(" ");
|
|
283
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
284
|
+
const body = rows.map((row) => columns.map((col, i) => (row[col] ?? "").padEnd(widths[i])).join(" ")).join(`
|
|
285
|
+
`);
|
|
286
|
+
return `${header}
|
|
287
|
+
${separator}
|
|
288
|
+
${body}`;
|
|
289
|
+
}
|
|
290
|
+
function formatMetric(value, digits = 3) {
|
|
291
|
+
return Number.isNaN(value) ? "-" : value.toFixed(digits);
|
|
292
|
+
}
|
|
293
|
+
function formatError(err) {
|
|
294
|
+
return err instanceof Error ? err.message : String(err);
|
|
295
|
+
}
|
|
296
|
+
function parseIntFlag(value, flag, minimum) {
|
|
297
|
+
const parsed = Number(value);
|
|
298
|
+
if (!Number.isInteger(parsed) || parsed < minimum) {
|
|
299
|
+
throw new Error(`${flag} must be an integer >= ${minimum} (got "${value}")`);
|
|
300
|
+
}
|
|
301
|
+
return parsed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/commands/dataset.ts
|
|
305
|
+
function registerDatasetCommand(program, openStores) {
|
|
306
|
+
const dataset = program.command("dataset").description("Pull and inspect HuggingFace datasets stored locally");
|
|
307
|
+
dataset.command("pull").argument("<id>", "HuggingFace dataset id (e.g. dair-ai/emotion)").option("--split <split>", "dataset split", "test").option("--config <config>", "dataset config name (defaults to the first config)").option("--limit <n>", "maximum rows to pull", "100").option("--offset <n>", "row offset to start from", "0").description("Fetch dataset rows and store them in the local eval database").action(async (id, opts) => {
|
|
308
|
+
const stores = await openStores();
|
|
309
|
+
try {
|
|
310
|
+
const { numRows, source } = await pullDatasetIntoStorage(stores, {
|
|
311
|
+
dataset: id,
|
|
312
|
+
split: opts.split,
|
|
313
|
+
config: opts.config,
|
|
314
|
+
limit: parseIntFlag(opts.limit, "--limit", 1),
|
|
315
|
+
offset: parseIntFlag(opts.offset, "--offset", 0)
|
|
316
|
+
});
|
|
317
|
+
console.log(`stored ${numRows} rows of ${id} [${opts.split}] (via ${source})`);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error(`Error: ${formatError(err)}`);
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
dataset.command("list").description("List locally stored dataset splits").action(async () => {
|
|
324
|
+
const stores = await openStores();
|
|
325
|
+
const all = await stores.datasets.getAll() ?? [];
|
|
326
|
+
const rows = all.map((d) => ({
|
|
327
|
+
dataset: d.dataset,
|
|
328
|
+
split: d.split,
|
|
329
|
+
rows: String(d.num_rows),
|
|
330
|
+
columns: JSON.parse(d.columns).join(","),
|
|
331
|
+
source: d.source,
|
|
332
|
+
fetched: d.fetched_at
|
|
333
|
+
}));
|
|
334
|
+
console.log(formatTable(rows, ["dataset", "split", "rows", "columns", "source", "fetched"]));
|
|
335
|
+
});
|
|
336
|
+
dataset.command("show").argument("<id>", "HuggingFace dataset id").option("--split <split>", "dataset split", "test").option("--limit <n>", "rows to print", "5").description("Print stored rows of a dataset split as JSON lines").action(async (id, opts) => {
|
|
337
|
+
const stores = await openStores();
|
|
338
|
+
try {
|
|
339
|
+
const limit = parseIntFlag(opts.limit, "--limit", 1);
|
|
340
|
+
const rows = await stores.rows.query({ dataset: id, split: opts.split });
|
|
341
|
+
if (!rows || rows.length === 0) {
|
|
342
|
+
console.error(`No stored rows for ${id} [${opts.split}] \u2014 run \`dataset pull\` first.`);
|
|
343
|
+
process.exitCode = 1;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
rows.sort((a, b) => a.row_index - b.row_index);
|
|
347
|
+
for (const row of rows.slice(0, limit)) {
|
|
348
|
+
console.log(row.data);
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
console.error(`Error: ${formatError(err)}`);
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/score/classification.ts
|
|
358
|
+
function normalizeLabel(label) {
|
|
359
|
+
return label.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
360
|
+
}
|
|
361
|
+
function scoreClassification(pairs) {
|
|
362
|
+
const perLabel = new Map;
|
|
363
|
+
let correct = 0;
|
|
364
|
+
for (const { expected, predicted } of pairs) {
|
|
365
|
+
const key = normalizeLabel(expected);
|
|
366
|
+
const entry = perLabel.get(key) ?? { total: 0, correct: 0 };
|
|
367
|
+
entry.total++;
|
|
368
|
+
if (key === normalizeLabel(predicted)) {
|
|
369
|
+
entry.correct++;
|
|
370
|
+
correct++;
|
|
371
|
+
}
|
|
372
|
+
perLabel.set(key, entry);
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
scored: pairs.length,
|
|
376
|
+
correct,
|
|
377
|
+
accuracy: pairs.length > 0 ? correct / pairs.length : NaN,
|
|
378
|
+
perLabel
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/score/correlation.ts
|
|
383
|
+
function pearson(xs, ys) {
|
|
384
|
+
const n = Math.min(xs.length, ys.length);
|
|
385
|
+
if (n < 2)
|
|
386
|
+
return NaN;
|
|
387
|
+
let sumX = 0;
|
|
388
|
+
let sumY = 0;
|
|
389
|
+
for (let i = 0;i < n; i++) {
|
|
390
|
+
sumX += xs[i];
|
|
391
|
+
sumY += ys[i];
|
|
392
|
+
}
|
|
393
|
+
const meanX = sumX / n;
|
|
394
|
+
const meanY = sumY / n;
|
|
395
|
+
let cov = 0;
|
|
396
|
+
let varX = 0;
|
|
397
|
+
let varY = 0;
|
|
398
|
+
for (let i = 0;i < n; i++) {
|
|
399
|
+
const dx = xs[i] - meanX;
|
|
400
|
+
const dy = ys[i] - meanY;
|
|
401
|
+
cov += dx * dy;
|
|
402
|
+
varX += dx * dx;
|
|
403
|
+
varY += dy * dy;
|
|
404
|
+
}
|
|
405
|
+
if (varX === 0 || varY === 0)
|
|
406
|
+
return NaN;
|
|
407
|
+
return cov / Math.sqrt(varX * varY);
|
|
408
|
+
}
|
|
409
|
+
function ranks(values) {
|
|
410
|
+
const indexed = values.map((value, index) => ({ value, index }));
|
|
411
|
+
indexed.sort((a, b) => a.value - b.value);
|
|
412
|
+
const out = new Array(values.length);
|
|
413
|
+
let i = 0;
|
|
414
|
+
while (i < indexed.length) {
|
|
415
|
+
let j = i;
|
|
416
|
+
while (j + 1 < indexed.length && indexed[j + 1].value === indexed[i].value)
|
|
417
|
+
j++;
|
|
418
|
+
const rank = (i + j) / 2 + 1;
|
|
419
|
+
for (let k = i;k <= j; k++)
|
|
420
|
+
out[indexed[k].index] = rank;
|
|
421
|
+
i = j + 1;
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
function spearman(xs, ys) {
|
|
426
|
+
const n = Math.min(xs.length, ys.length);
|
|
427
|
+
return pearson(ranks(xs.slice(0, n)), ranks(ys.slice(0, n)));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/score/extraction.ts
|
|
431
|
+
function normalizeValue(value) {
|
|
432
|
+
if (typeof value === "object" && value !== null)
|
|
433
|
+
return normalizeLabel(JSON.stringify(value));
|
|
434
|
+
return normalizeLabel(String(value ?? ""));
|
|
435
|
+
}
|
|
436
|
+
function distinctByKey(rows, keyField) {
|
|
437
|
+
const map = new Map;
|
|
438
|
+
for (const row of rows) {
|
|
439
|
+
const key = normalizeValue(row[keyField]);
|
|
440
|
+
if (key.length > 0)
|
|
441
|
+
map.set(key, row);
|
|
442
|
+
}
|
|
443
|
+
return map;
|
|
444
|
+
}
|
|
445
|
+
function scoreExtraction(candidate, expected, keyField, fields) {
|
|
446
|
+
const candidateByKey = distinctByKey(candidate, keyField);
|
|
447
|
+
const expectedByKey = distinctByKey(expected, keyField);
|
|
448
|
+
let matched = 0;
|
|
449
|
+
let candidateMatched = 0;
|
|
450
|
+
let fieldsTotal = 0;
|
|
451
|
+
let fieldsAgreed = 0;
|
|
452
|
+
for (const [key, expectedRow] of expectedByKey) {
|
|
453
|
+
const candidateRow = candidateByKey.get(key);
|
|
454
|
+
if (!candidateRow)
|
|
455
|
+
continue;
|
|
456
|
+
matched++;
|
|
457
|
+
const scoredFields = (fields ?? Object.keys(expectedRow).filter((f) => normalizeValue(expectedRow[f]).length > 0)).filter((field) => field !== keyField);
|
|
458
|
+
for (const field of scoredFields) {
|
|
459
|
+
const expectedValue = normalizeValue(expectedRow[field]);
|
|
460
|
+
if (expectedValue.length === 0)
|
|
461
|
+
continue;
|
|
462
|
+
fieldsTotal++;
|
|
463
|
+
if (normalizeValue(candidateRow[field]) === expectedValue)
|
|
464
|
+
fieldsAgreed++;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const key of candidateByKey.keys()) {
|
|
468
|
+
if (expectedByKey.has(key))
|
|
469
|
+
candidateMatched++;
|
|
470
|
+
}
|
|
471
|
+
return withRates({
|
|
472
|
+
expected: expectedByKey.size,
|
|
473
|
+
matched,
|
|
474
|
+
candidateDistinct: candidateByKey.size,
|
|
475
|
+
candidateMatched,
|
|
476
|
+
fieldsTotal,
|
|
477
|
+
fieldsAgreed
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function combineExtractionCounts(counts) {
|
|
481
|
+
const total = counts.reduce((sum, c) => ({
|
|
482
|
+
expected: sum.expected + c.expected,
|
|
483
|
+
matched: sum.matched + c.matched,
|
|
484
|
+
candidateDistinct: sum.candidateDistinct + c.candidateDistinct,
|
|
485
|
+
candidateMatched: sum.candidateMatched + c.candidateMatched,
|
|
486
|
+
fieldsTotal: sum.fieldsTotal + c.fieldsTotal,
|
|
487
|
+
fieldsAgreed: sum.fieldsAgreed + c.fieldsAgreed
|
|
488
|
+
}), {
|
|
489
|
+
expected: 0,
|
|
490
|
+
matched: 0,
|
|
491
|
+
candidateDistinct: 0,
|
|
492
|
+
candidateMatched: 0,
|
|
493
|
+
fieldsTotal: 0,
|
|
494
|
+
fieldsAgreed: 0
|
|
495
|
+
});
|
|
496
|
+
return withRates(total);
|
|
497
|
+
}
|
|
498
|
+
function withRates(counts) {
|
|
499
|
+
return {
|
|
500
|
+
...counts,
|
|
501
|
+
score: counts.fieldsTotal > 0 ? counts.fieldsAgreed / counts.fieldsTotal : NaN,
|
|
502
|
+
found: counts.expected > 0 ? counts.matched / counts.expected : NaN,
|
|
503
|
+
prec: counts.candidateDistinct > 0 ? counts.candidateMatched / counts.candidateDistinct : NaN
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/report/aggregate.ts
|
|
508
|
+
var RANKING_METRIC = {
|
|
509
|
+
classify: (r) => r.accuracy,
|
|
510
|
+
similarity: (r) => r.spearman,
|
|
511
|
+
extract: (r) => Number.isFinite(r.score) ? r.score : r.found
|
|
512
|
+
};
|
|
513
|
+
function aggregateResults(kind, results, options = {}) {
|
|
514
|
+
const byModel = new Map;
|
|
515
|
+
for (const result of results) {
|
|
516
|
+
const list = byModel.get(result.model) ?? [];
|
|
517
|
+
list.push(result);
|
|
518
|
+
byModel.set(result.model, list);
|
|
519
|
+
}
|
|
520
|
+
const reports = [];
|
|
521
|
+
for (const [model, rows] of byModel) {
|
|
522
|
+
const ok = rows.filter((r) => r.ok === 1);
|
|
523
|
+
const pairs = ok.filter((r) => r.expected != null && r.predicted != null).map((r) => ({ expected: r.expected, predicted: r.predicted }));
|
|
524
|
+
const numeric = kind === "similarity" ? ok.filter((r) => Number.isFinite(r.expected_value ?? NaN) && Number.isFinite(r.predicted_value ?? NaN)) : [];
|
|
525
|
+
const expectedValues = numeric.map((r) => r.expected_value);
|
|
526
|
+
const predictedValues = numeric.map((r) => r.predicted_value);
|
|
527
|
+
const extraction = kind === "extract" ? combineExtractionCounts(extractionCountsFor(pairs, options)) : { score: NaN, found: NaN, prec: NaN };
|
|
528
|
+
const latency = rows.length > 0 ? rows.reduce((sum, r) => sum + r.latency_ms, 0) / rows.length : NaN;
|
|
529
|
+
reports.push({
|
|
530
|
+
model,
|
|
531
|
+
rows: rows.length,
|
|
532
|
+
okRows: ok.length,
|
|
533
|
+
accuracy: kind === "classify" ? scoreClassification(pairs).accuracy : NaN,
|
|
534
|
+
pearson: pearson(predictedValues, expectedValues),
|
|
535
|
+
spearman: spearman(predictedValues, expectedValues),
|
|
536
|
+
score: extraction.score,
|
|
537
|
+
found: extraction.found,
|
|
538
|
+
prec: extraction.prec,
|
|
539
|
+
avgLatencyMs: latency
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
const metric = (r) => {
|
|
543
|
+
const value = RANKING_METRIC[kind](r);
|
|
544
|
+
return Number.isNaN(value) ? -Infinity : value;
|
|
545
|
+
};
|
|
546
|
+
reports.sort((a, b) => metric(b) - metric(a) || a.avgLatencyMs - b.avgLatencyMs);
|
|
547
|
+
return reports;
|
|
548
|
+
}
|
|
549
|
+
function extractionCountsFor(pairs, options) {
|
|
550
|
+
const keyField = options.keyField ?? "name";
|
|
551
|
+
const counts = [];
|
|
552
|
+
for (const pair of pairs) {
|
|
553
|
+
try {
|
|
554
|
+
const expected = JSON.parse(pair.expected);
|
|
555
|
+
const predicted = JSON.parse(pair.predicted);
|
|
556
|
+
if (!Array.isArray(expected) || !Array.isArray(predicted))
|
|
557
|
+
continue;
|
|
558
|
+
counts.push(scoreExtraction(predicted, expected, keyField, options.fields));
|
|
559
|
+
} catch {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return counts;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/commands/report.ts
|
|
567
|
+
async function printReport(stores, runId, format) {
|
|
568
|
+
const run = await stores.runs.get({ run_id: runId });
|
|
569
|
+
if (!run)
|
|
570
|
+
throw new Error(`run ${runId} not found`);
|
|
571
|
+
if (run.kind !== "classify" && run.kind !== "similarity" && run.kind !== "extract") {
|
|
572
|
+
throw new Error(`run ${runId} has unknown eval kind "${run.kind}"`);
|
|
573
|
+
}
|
|
574
|
+
const kind = run.kind;
|
|
575
|
+
const results = await stores.results.query({ run_id: runId }) ?? [];
|
|
576
|
+
let runOptions = {};
|
|
577
|
+
if (kind === "extract") {
|
|
578
|
+
try {
|
|
579
|
+
const parsed = JSON.parse(run.options);
|
|
580
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
581
|
+
runOptions = parsed;
|
|
582
|
+
}
|
|
583
|
+
} catch {}
|
|
584
|
+
}
|
|
585
|
+
const reports = aggregateResults(kind, results, runOptions);
|
|
586
|
+
if (format === "json") {
|
|
587
|
+
console.log(JSON.stringify({ run, reports }, null, 2));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
console.log(`run ${run.run_id} \u2014 ${run.kind} on ${run.dataset} [${run.split}]`);
|
|
591
|
+
const REPORT_COLUMNS = {
|
|
592
|
+
classify: ["model", "rows", "ok", "accuracy", "avg_ms"],
|
|
593
|
+
similarity: ["model", "rows", "ok", "pearson", "spearman", "avg_ms"],
|
|
594
|
+
extract: ["model", "rows", "ok", "score", "found", "prec", "avg_ms"]
|
|
595
|
+
};
|
|
596
|
+
const tableRows = reports.map((r) => ({
|
|
597
|
+
model: r.model,
|
|
598
|
+
rows: String(r.rows),
|
|
599
|
+
ok: String(r.okRows),
|
|
600
|
+
accuracy: formatMetric(r.accuracy),
|
|
601
|
+
pearson: formatMetric(r.pearson),
|
|
602
|
+
spearman: formatMetric(r.spearman),
|
|
603
|
+
score: formatMetric(r.score),
|
|
604
|
+
found: formatMetric(r.found),
|
|
605
|
+
prec: formatMetric(r.prec),
|
|
606
|
+
avg_ms: formatMetric(r.avgLatencyMs, 0)
|
|
607
|
+
}));
|
|
608
|
+
console.log(formatTable(tableRows, REPORT_COLUMNS[kind]));
|
|
609
|
+
const failures = results.filter((r) => r.ok !== 1);
|
|
610
|
+
if (failures.length > 0) {
|
|
611
|
+
const sample = failures[0];
|
|
612
|
+
console.log(`
|
|
613
|
+
${failures.length} failed execution(s); first: ` + `${sample.model} row ${sample.row_index}: ${sample.error ?? "unknown error"}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function registerReportCommand(program, openStores) {
|
|
617
|
+
program.command("runs").description("List stored eval runs").action(async () => {
|
|
618
|
+
const stores = await openStores();
|
|
619
|
+
const all = await stores.runs.getAll() ?? [];
|
|
620
|
+
all.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
621
|
+
const rows = all.map((r) => ({
|
|
622
|
+
run: r.run_id,
|
|
623
|
+
kind: r.kind,
|
|
624
|
+
dataset: `${r.dataset} [${r.split}]`,
|
|
625
|
+
models: JSON.parse(r.models).join(","),
|
|
626
|
+
created: r.created_at
|
|
627
|
+
}));
|
|
628
|
+
console.log(formatTable(rows, ["run", "kind", "dataset", "models", "created"]));
|
|
629
|
+
});
|
|
630
|
+
program.command("report").argument("[runId]", "run id (defaults to the most recent run)").option("--format <fmt>", "table or json", "table").description("Score a stored run and rank its models").action(async (runId, opts) => {
|
|
631
|
+
const stores = await openStores();
|
|
632
|
+
try {
|
|
633
|
+
let target = runId;
|
|
634
|
+
if (!target) {
|
|
635
|
+
const all = await stores.runs.getAll() ?? [];
|
|
636
|
+
all.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
637
|
+
target = all[0]?.run_id;
|
|
638
|
+
}
|
|
639
|
+
if (!target) {
|
|
640
|
+
throw new Error("no stored runs \u2014 run `run-classify`, `run-similarity`, or `run-extract`");
|
|
641
|
+
}
|
|
642
|
+
await printReport(stores, target, opts.format);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.error(`Error: ${formatError(err)}`);
|
|
645
|
+
process.exitCode = 1;
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/models.ts
|
|
651
|
+
import { downloadModel } from "@workglow/ai";
|
|
652
|
+
|
|
653
|
+
// src/config.ts
|
|
654
|
+
import { mkdirSync } from "fs";
|
|
655
|
+
import { homedir } from "os";
|
|
656
|
+
import { join, resolve } from "path";
|
|
657
|
+
function evalHome() {
|
|
658
|
+
const override = process.env.WORKGLOW_EVAL_HOME;
|
|
659
|
+
return override ? resolve(override) : join(homedir(), ".workglow", "eval");
|
|
660
|
+
}
|
|
661
|
+
function ggufCacheDir() {
|
|
662
|
+
return join(evalHome(), "cache", "gguf");
|
|
663
|
+
}
|
|
664
|
+
function loadConfig() {
|
|
665
|
+
const home = evalHome();
|
|
666
|
+
mkdirSync(home, { recursive: true });
|
|
667
|
+
return {
|
|
668
|
+
home,
|
|
669
|
+
dbPath: join(home, "eval.sqlite"),
|
|
670
|
+
modelCache: join(home, "cache", "onnx")
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/models.ts
|
|
675
|
+
var GGUF_PREFIX = "gguf:";
|
|
676
|
+
function resolveModelConfig(id, kind) {
|
|
677
|
+
if (id.startsWith(GGUF_PREFIX)) {
|
|
678
|
+
const ref = id.slice(GGUF_PREFIX.length);
|
|
679
|
+
const provider_config = {
|
|
680
|
+
models_dir: ggufCacheDir()
|
|
681
|
+
};
|
|
682
|
+
if (kind === "similarity")
|
|
683
|
+
provider_config.embedding = true;
|
|
684
|
+
if (/^(?:hf:|https?:)/.test(ref)) {
|
|
685
|
+
provider_config.model_url = ref;
|
|
686
|
+
} else if (ref.endsWith(".gguf")) {
|
|
687
|
+
provider_config.model_path = ref;
|
|
688
|
+
} else {
|
|
689
|
+
provider_config.model_url = `hf:${ref}`;
|
|
690
|
+
}
|
|
691
|
+
return { provider: "LOCAL_LLAMACPP", provider_config };
|
|
692
|
+
}
|
|
693
|
+
if (id.includes("/")) {
|
|
694
|
+
const [path, dtype] = splitDtype(id);
|
|
695
|
+
return {
|
|
696
|
+
provider: "HF_TRANSFORMERS_ONNX",
|
|
697
|
+
provider_config: {
|
|
698
|
+
model_path: path,
|
|
699
|
+
pipeline: kind === "similarity" ? "feature-extraction" : "text-generation",
|
|
700
|
+
dtype: dtype ?? (kind === "similarity" ? "q8" : "q4")
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (/^claude-/.test(id)) {
|
|
705
|
+
return { provider: "ANTHROPIC", provider_config: { model_name: id, max_tokens: 1024 } };
|
|
706
|
+
}
|
|
707
|
+
if (/^(?:gpt-|o\d|chatgpt-|text-embedding-)/.test(id)) {
|
|
708
|
+
return { provider: "OPENAI", provider_config: { model_name: id } };
|
|
709
|
+
}
|
|
710
|
+
if (/^gemini-/.test(id)) {
|
|
711
|
+
return { provider: "GOOGLE_GEMINI", provider_config: { model_name: id } };
|
|
712
|
+
}
|
|
713
|
+
if (/^grok-/.test(id)) {
|
|
714
|
+
return { provider: "XAI", provider_config: { model_name: id } };
|
|
715
|
+
}
|
|
716
|
+
throw new Error(`cannot infer a provider for model "${id}" \u2014 use a claude-*/gpt-*/gemini-*/grok-* cloud id ` + `or an org/name HuggingFace ONNX model path`);
|
|
717
|
+
}
|
|
718
|
+
async function ensureEmbeddingDimensions(config) {
|
|
719
|
+
if (config.provider !== "HF_TRANSFORMERS_ONNX")
|
|
720
|
+
return config;
|
|
721
|
+
if (config.provider_config.native_dimensions !== undefined)
|
|
722
|
+
return config;
|
|
723
|
+
const path = sanitizeHubRepoId(config.provider_config.model_path);
|
|
724
|
+
const url = `https://huggingface.co/${path}/resolve/main/config.json`;
|
|
725
|
+
const res = await fetch(url, { headers: hfAuthHeaders() });
|
|
726
|
+
if (!res.ok) {
|
|
727
|
+
throw new Error(`could not read ${path} config.json (${res.status}) to determine dimensions`);
|
|
728
|
+
}
|
|
729
|
+
const modelInfo = await res.json();
|
|
730
|
+
const dimensions = modelInfo.hidden_size ?? modelInfo.d_model;
|
|
731
|
+
if (!dimensions) {
|
|
732
|
+
throw new Error(`no hidden_size/d_model in ${path} config.json \u2014 cannot infer dimensions`);
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
...config,
|
|
736
|
+
provider_config: { ...config.provider_config, native_dimensions: dimensions }
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async function ensureModelDownloaded(config) {
|
|
740
|
+
if (config.provider !== "LOCAL_LLAMACPP")
|
|
741
|
+
return;
|
|
742
|
+
if (config.provider_config.model_url === undefined)
|
|
743
|
+
return;
|
|
744
|
+
await downloadModel({ model: config });
|
|
745
|
+
}
|
|
746
|
+
function splitDtype(id) {
|
|
747
|
+
const colon = id.lastIndexOf(":");
|
|
748
|
+
if (colon <= id.indexOf("/"))
|
|
749
|
+
return [id, undefined];
|
|
750
|
+
return [id.slice(0, colon), id.slice(colon + 1)];
|
|
751
|
+
}
|
|
752
|
+
function parseModelList(models) {
|
|
753
|
+
return [
|
|
754
|
+
...new Set(models.split(",").map((m) => m.trim()).filter((m) => m.length > 0))
|
|
755
|
+
];
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/evals/classify.ts
|
|
759
|
+
import { Workflow } from "@workglow/task-graph";
|
|
760
|
+
|
|
761
|
+
// src/evals/prompt.ts
|
|
762
|
+
function fenceText(text) {
|
|
763
|
+
let fence = '"""';
|
|
764
|
+
while (text.includes(fence))
|
|
765
|
+
fence += '"';
|
|
766
|
+
return `${fence}
|
|
767
|
+
${text}
|
|
768
|
+
${fence}`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/evals/classify.ts
|
|
772
|
+
function expectedLabel(row, labelColumn, labelNames, candidateLabels = []) {
|
|
773
|
+
const raw = row[labelColumn];
|
|
774
|
+
if (typeof raw === "number" && Number.isInteger(raw)) {
|
|
775
|
+
const names = labelNames[labelColumn] ?? candidateLabels;
|
|
776
|
+
if (raw >= 0 && raw < names.length)
|
|
777
|
+
return names[raw];
|
|
778
|
+
}
|
|
779
|
+
return String(raw);
|
|
780
|
+
}
|
|
781
|
+
function resolveCandidateLabels(options, context) {
|
|
782
|
+
if (options.labels && options.labels.length > 0)
|
|
783
|
+
return options.labels;
|
|
784
|
+
const names = context.labelNames[options.labelColumn];
|
|
785
|
+
if (names && names.length > 0)
|
|
786
|
+
return names;
|
|
787
|
+
throw new Error(`no candidate labels: dataset column "${options.labelColumn}" has no ClassLabel names \u2014 ` + `pass --labels "a,b,c"`);
|
|
788
|
+
}
|
|
789
|
+
function buildClassifyPrompt(text, labels) {
|
|
790
|
+
return `Classify the following text into exactly one of these labels: ` + `${labels.join(", ")}.
|
|
791
|
+
|
|
792
|
+
` + `Text:
|
|
793
|
+
${fenceText(text)}
|
|
794
|
+
|
|
795
|
+
` + `Respond with a JSON object of the form {"label": "<one of the labels>"}.`;
|
|
796
|
+
}
|
|
797
|
+
function makeClassifyExecutor(model, options, context) {
|
|
798
|
+
for (const column of [options.textColumn, options.labelColumn]) {
|
|
799
|
+
if (context.columns.length > 0 && !context.columns.includes(column)) {
|
|
800
|
+
throw new Error(`dataset has no column "${column}" (columns: ${context.columns.join(", ")}) \u2014 ` + `set --text-column/--label-column`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const labels = resolveCandidateLabels(options, context);
|
|
804
|
+
const outputSchema = {
|
|
805
|
+
type: "object",
|
|
806
|
+
properties: { label: { type: "string", enum: [...labels] } },
|
|
807
|
+
required: ["label"],
|
|
808
|
+
additionalProperties: false
|
|
809
|
+
};
|
|
810
|
+
return async (row) => {
|
|
811
|
+
const text = String(row[options.textColumn] ?? "");
|
|
812
|
+
const workflow = new Workflow;
|
|
813
|
+
workflow.structuredGeneration({
|
|
814
|
+
model,
|
|
815
|
+
prompt: buildClassifyPrompt(text, labels),
|
|
816
|
+
outputSchema,
|
|
817
|
+
temperature: 0,
|
|
818
|
+
maxTokens: 256
|
|
819
|
+
});
|
|
820
|
+
const result = await workflow.run();
|
|
821
|
+
return {
|
|
822
|
+
expected: expectedLabel(row, options.labelColumn, context.labelNames, labels),
|
|
823
|
+
predicted: String(result.object?.label ?? "")
|
|
824
|
+
};
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/evals/extract.ts
|
|
829
|
+
import { Workflow as Workflow2 } from "@workglow/task-graph";
|
|
830
|
+
var DEFAULT_INSTRUCTION = "Extract every entity mentioned in the text.";
|
|
831
|
+
var UNSAFE_FIELDS = new Set(["__proto__", "constructor", "prototype"]);
|
|
832
|
+
function parseExpectedRows(value, column) {
|
|
833
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
834
|
+
if (!Array.isArray(parsed) || parsed.some((row) => row === null || typeof row !== "object" || Array.isArray(row))) {
|
|
835
|
+
throw new Error(`column "${column}" must hold an array of objects (or its JSON string)`);
|
|
836
|
+
}
|
|
837
|
+
return parsed;
|
|
838
|
+
}
|
|
839
|
+
function resolveExtractionFields(expectedRows, keyField, fields) {
|
|
840
|
+
if (UNSAFE_FIELDS.has(keyField)) {
|
|
841
|
+
throw new Error(`--key-field "${keyField}" is not a usable field name`);
|
|
842
|
+
}
|
|
843
|
+
if (fields && fields.length > 0) {
|
|
844
|
+
const safe = fields.filter((field) => !UNSAFE_FIELDS.has(field));
|
|
845
|
+
return safe.includes(keyField) ? [...safe] : [keyField, ...safe];
|
|
846
|
+
}
|
|
847
|
+
const seen = new Set([keyField]);
|
|
848
|
+
for (const row of expectedRows) {
|
|
849
|
+
for (const key of Object.keys(row))
|
|
850
|
+
if (!UNSAFE_FIELDS.has(key))
|
|
851
|
+
seen.add(key);
|
|
852
|
+
}
|
|
853
|
+
return [...seen];
|
|
854
|
+
}
|
|
855
|
+
function buildExtractPrompt(text, instruction, keyField, fields) {
|
|
856
|
+
return `${instruction}
|
|
857
|
+
|
|
858
|
+
` + `For each one, report the fields: ${fields.join(", ")}. ` + `"${keyField}" identifies the entity; use null when the text does not state a field.
|
|
859
|
+
|
|
860
|
+
` + `Text:
|
|
861
|
+
${fenceText(text)}
|
|
862
|
+
|
|
863
|
+
` + `Respond with a JSON object of the form {"items": [{"${keyField}": "...", ...}, ...]}. ` + `Report each entity once; use only information from the text.`;
|
|
864
|
+
}
|
|
865
|
+
function buildItemsSchema(fields, keyField) {
|
|
866
|
+
const itemProperties = {};
|
|
867
|
+
for (const field of fields) {
|
|
868
|
+
itemProperties[field] = field === keyField ? { type: "string" } : { type: ["string", "null"] };
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
type: "object",
|
|
872
|
+
properties: {
|
|
873
|
+
items: {
|
|
874
|
+
type: "array",
|
|
875
|
+
items: {
|
|
876
|
+
type: "object",
|
|
877
|
+
properties: itemProperties,
|
|
878
|
+
required: [keyField],
|
|
879
|
+
additionalProperties: false
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
required: ["items"],
|
|
884
|
+
additionalProperties: false
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function makeExtractExecutor(model, options, context) {
|
|
888
|
+
for (const column of [options.textColumn, options.expectedColumn]) {
|
|
889
|
+
if (context.columns.length > 0 && !context.columns.includes(column)) {
|
|
890
|
+
throw new Error(`dataset has no column "${column}" (columns: ${context.columns.join(", ")}) \u2014 ` + `set --text-column/--expected-column`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const instruction = options.instruction ?? DEFAULT_INSTRUCTION;
|
|
894
|
+
const explicitFields = options.fields && options.fields.length > 0 ? resolveExtractionFields([], options.keyField, options.fields) : undefined;
|
|
895
|
+
const explicitSchema = explicitFields ? buildItemsSchema(explicitFields, options.keyField) : undefined;
|
|
896
|
+
return async (row) => {
|
|
897
|
+
const text = String(row[options.textColumn] ?? "");
|
|
898
|
+
const expectedRows = parseExpectedRows(row[options.expectedColumn], options.expectedColumn);
|
|
899
|
+
const fields = explicitFields ?? resolveExtractionFields(expectedRows, options.keyField, undefined);
|
|
900
|
+
const outputSchema = explicitSchema ?? buildItemsSchema(fields, options.keyField);
|
|
901
|
+
const workflow = new Workflow2;
|
|
902
|
+
workflow.structuredGeneration({
|
|
903
|
+
model,
|
|
904
|
+
prompt: buildExtractPrompt(text, instruction, options.keyField, fields),
|
|
905
|
+
outputSchema,
|
|
906
|
+
temperature: 0,
|
|
907
|
+
maxTokens: 2048
|
|
908
|
+
});
|
|
909
|
+
const result = await workflow.run();
|
|
910
|
+
const items = Array.isArray(result.object?.items) ? result.object.items : [];
|
|
911
|
+
return {
|
|
912
|
+
expected: JSON.stringify(expectedRows),
|
|
913
|
+
predicted: JSON.stringify(items)
|
|
914
|
+
};
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/evals/similarity.ts
|
|
919
|
+
import { Workflow as Workflow3 } from "@workglow/task-graph";
|
|
920
|
+
import { cosineSimilarity } from "@workglow/util/schema";
|
|
921
|
+
function makeSimilarityExecutor(model, options, context) {
|
|
922
|
+
for (const column of [options.textColumn, options.pairColumn, options.scoreColumn]) {
|
|
923
|
+
if (context.columns.length > 0 && !context.columns.includes(column)) {
|
|
924
|
+
throw new Error(`dataset has no column "${column}" (columns: ${context.columns.join(", ")}) \u2014 ` + `set --text-column/--pair-column/--score-column`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return async (row) => {
|
|
928
|
+
const a = String(row[options.textColumn] ?? "");
|
|
929
|
+
const b = String(row[options.pairColumn] ?? "");
|
|
930
|
+
const workflow = new Workflow3;
|
|
931
|
+
workflow.textEmbedding({ model, text: [a, b] });
|
|
932
|
+
const result = await workflow.run();
|
|
933
|
+
const vectors = Array.isArray(result.vector) ? result.vector : [result.vector];
|
|
934
|
+
if (vectors.length < 2) {
|
|
935
|
+
throw new Error(`expected 2 embeddings, got ${vectors.length}`);
|
|
936
|
+
}
|
|
937
|
+
const gold = Number(row[options.scoreColumn]);
|
|
938
|
+
if (!Number.isFinite(gold)) {
|
|
939
|
+
throw new Error(`row has no numeric "${options.scoreColumn}" value (got ${JSON.stringify(row[options.scoreColumn])})`);
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
expectedValue: gold,
|
|
943
|
+
predictedValue: cosineSimilarity(vectors[0], vectors[1])
|
|
944
|
+
};
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/evals/runner.ts
|
|
949
|
+
var EXECUTORS = {
|
|
950
|
+
classify: makeClassifyExecutor,
|
|
951
|
+
similarity: makeSimilarityExecutor,
|
|
952
|
+
extract: makeExtractExecutor
|
|
953
|
+
};
|
|
954
|
+
function makeExecutor(kind, model, columns, context) {
|
|
955
|
+
return EXECUTORS[kind](model, columns, context);
|
|
956
|
+
}
|
|
957
|
+
async function runSweep(stores, rows, options) {
|
|
958
|
+
const run = await stores.runs.put({
|
|
959
|
+
kind: options.kind,
|
|
960
|
+
dataset: options.dataset,
|
|
961
|
+
split: options.split,
|
|
962
|
+
models: JSON.stringify(options.models),
|
|
963
|
+
options: JSON.stringify(options.columns),
|
|
964
|
+
created_at: new Date().toISOString()
|
|
965
|
+
});
|
|
966
|
+
const parsedRows = rows.map((record) => {
|
|
967
|
+
try {
|
|
968
|
+
return { record, row: JSON.parse(record.data), parseError: undefined };
|
|
969
|
+
} catch (err) {
|
|
970
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
971
|
+
return { record, row: undefined, parseError: `invalid stored row: ${message}` };
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
const total = rows.length * options.models.length;
|
|
975
|
+
let done = 0;
|
|
976
|
+
for (const modelId of options.models) {
|
|
977
|
+
let executor;
|
|
978
|
+
let setupError;
|
|
979
|
+
try {
|
|
980
|
+
let config = resolveModelConfig(modelId, options.kind);
|
|
981
|
+
if (options.kind === "similarity")
|
|
982
|
+
config = await ensureEmbeddingDimensions(config);
|
|
983
|
+
await ensureModelDownloaded(config);
|
|
984
|
+
executor = makeExecutor(options.kind, config, options.columns, options.context);
|
|
985
|
+
} catch (err) {
|
|
986
|
+
setupError = err instanceof Error ? err.message : String(err);
|
|
987
|
+
}
|
|
988
|
+
for (const { record, row, parseError } of parsedRows) {
|
|
989
|
+
const t0 = performance.now();
|
|
990
|
+
let outcome;
|
|
991
|
+
if (!executor) {
|
|
992
|
+
outcome = failedOutcome(setupError ?? "executor unavailable");
|
|
993
|
+
} else if (!row) {
|
|
994
|
+
outcome = failedOutcome(parseError ?? "invalid stored row");
|
|
995
|
+
} else {
|
|
996
|
+
try {
|
|
997
|
+
const prediction = await executor(row);
|
|
998
|
+
outcome = {
|
|
999
|
+
ok: 1,
|
|
1000
|
+
error: null,
|
|
1001
|
+
expected: prediction.expected ?? null,
|
|
1002
|
+
predicted: prediction.predicted ?? null,
|
|
1003
|
+
expected_value: prediction.expectedValue ?? null,
|
|
1004
|
+
predicted_value: prediction.predictedValue ?? null
|
|
1005
|
+
};
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
outcome = failedOutcome(err instanceof Error ? err.message : String(err));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const latency = performance.now() - t0;
|
|
1011
|
+
await stores.results.put({
|
|
1012
|
+
run_id: run.run_id,
|
|
1013
|
+
model: modelId,
|
|
1014
|
+
row_index: record.row_index,
|
|
1015
|
+
latency_ms: Math.round(latency * 100) / 100,
|
|
1016
|
+
...outcome
|
|
1017
|
+
});
|
|
1018
|
+
done++;
|
|
1019
|
+
options.onProgress?.(done, total, modelId, outcome.ok === 1);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return run.run_id;
|
|
1023
|
+
}
|
|
1024
|
+
function failedOutcome(error) {
|
|
1025
|
+
return {
|
|
1026
|
+
ok: 0,
|
|
1027
|
+
error,
|
|
1028
|
+
expected: null,
|
|
1029
|
+
predicted: null,
|
|
1030
|
+
expected_value: null,
|
|
1031
|
+
predicted_value: null
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/commands/run.ts
|
|
1036
|
+
var DEFAULT_MODELS = {
|
|
1037
|
+
classify: "onnx-community/LFM2.5-350M-ONNX",
|
|
1038
|
+
similarity: "Xenova/all-MiniLM-L6-v2",
|
|
1039
|
+
extract: "onnx-community/LFM2.5-350M-ONNX"
|
|
1040
|
+
};
|
|
1041
|
+
var DEFAULT_TEXT_COLUMN = {
|
|
1042
|
+
classify: "text",
|
|
1043
|
+
similarity: "sentence1",
|
|
1044
|
+
extract: "text"
|
|
1045
|
+
};
|
|
1046
|
+
var DESCRIPTIONS = {
|
|
1047
|
+
classify: "Classify each stored row with each model (StructuredGenerationTask) and score accuracy",
|
|
1048
|
+
similarity: "Embed stored sentence pairs with each model (TextEmbeddingTask) and correlate cosine similarity with the gold score",
|
|
1049
|
+
extract: "Extract structured records from each stored row (StructuredGenerationTask) and score field agreement, entity recall, and precision against the gold rows"
|
|
1050
|
+
};
|
|
1051
|
+
function registerRunCommand(program, openStores, ensureProviders) {
|
|
1052
|
+
for (const kind of ["classify", "similarity", "extract"]) {
|
|
1053
|
+
const command = program.command(`run-${kind}`).description(DESCRIPTIONS[kind]).requiredOption("--dataset <id>", "a dataset previously stored via `dataset pull`").option("--split <split>", "dataset split", "test").option("--models <list>", `comma-separated model ids (default: ${DEFAULT_MODELS[kind]})`).option("--limit <n>", "cap the number of stored rows to run").option("--text-column <name>", `input text column (default: ${DEFAULT_TEXT_COLUMN[kind]})`).option("--format <fmt>", "report output: table or json", "table");
|
|
1054
|
+
if (kind === "classify") {
|
|
1055
|
+
command.option("--label-column <name>", "gold label column (default: label)").option("--labels <list>", "comma-separated candidate labels override");
|
|
1056
|
+
} else if (kind === "similarity") {
|
|
1057
|
+
command.option("--pair-column <name>", "second sentence column (default: sentence2)").option("--score-column <name>", "gold score column (default: score)");
|
|
1058
|
+
} else {
|
|
1059
|
+
command.option("--expected-column <name>", "gold column: array of objects or JSON (default: expected)").option("--key-field <name>", "field that identifies an entity when aligning (default: name)").option("--fields <list>", "comma-separated fields to extract and score").option("--instruction <text>", "task sentence at the top of the prompt");
|
|
1060
|
+
}
|
|
1061
|
+
command.action(async (flags) => {
|
|
1062
|
+
try {
|
|
1063
|
+
await ensureProviders();
|
|
1064
|
+
await runEval(kind, flags, await openStores());
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
console.error(`Error: ${formatError(err)}`);
|
|
1067
|
+
process.exitCode = 1;
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function parseNameList(value, flag) {
|
|
1073
|
+
if (value === undefined)
|
|
1074
|
+
return;
|
|
1075
|
+
const names = [
|
|
1076
|
+
...new Set(value.split(",").map((name) => name.trim()).filter((name) => name.length > 0))
|
|
1077
|
+
];
|
|
1078
|
+
if (names.length === 0)
|
|
1079
|
+
throw new Error(`${flag} was given but contains no names`);
|
|
1080
|
+
return names;
|
|
1081
|
+
}
|
|
1082
|
+
async function runEval(kind, flags, stores) {
|
|
1083
|
+
const meta = await stores.datasets.get({ dataset: flags.dataset, split: flags.split });
|
|
1084
|
+
if (!meta) {
|
|
1085
|
+
throw new Error(`dataset ${flags.dataset} [${flags.split}] is not in storage \u2014 ` + `run \`workglow-eval dataset pull ${flags.dataset} --split ${flags.split}\` first`);
|
|
1086
|
+
}
|
|
1087
|
+
const rows = await stores.rows.query({ dataset: flags.dataset, split: flags.split }) ?? [];
|
|
1088
|
+
rows.sort((a, b) => a.row_index - b.row_index);
|
|
1089
|
+
const limit = flags.limit === undefined ? undefined : parseIntFlag(flags.limit, "--limit", 1);
|
|
1090
|
+
const limited = limit === undefined ? rows : rows.slice(0, limit);
|
|
1091
|
+
if (limited.length === 0)
|
|
1092
|
+
throw new Error("no stored rows to evaluate");
|
|
1093
|
+
const models = parseModelList(flags.models ?? DEFAULT_MODELS[kind]);
|
|
1094
|
+
const keyField = (flags.keyField ?? "name").trim();
|
|
1095
|
+
if (keyField.length === 0)
|
|
1096
|
+
throw new Error("--key-field must not be empty");
|
|
1097
|
+
const columns = {
|
|
1098
|
+
textColumn: flags.textColumn ?? DEFAULT_TEXT_COLUMN[kind],
|
|
1099
|
+
labelColumn: flags.labelColumn ?? "label",
|
|
1100
|
+
labels: parseNameList(flags.labels, "--labels"),
|
|
1101
|
+
pairColumn: flags.pairColumn ?? "sentence2",
|
|
1102
|
+
scoreColumn: flags.scoreColumn ?? "score",
|
|
1103
|
+
expectedColumn: flags.expectedColumn ?? "expected",
|
|
1104
|
+
keyField,
|
|
1105
|
+
fields: parseNameList(flags.fields, "--fields"),
|
|
1106
|
+
instruction: flags.instruction
|
|
1107
|
+
};
|
|
1108
|
+
const context = {
|
|
1109
|
+
columns: JSON.parse(meta.columns),
|
|
1110
|
+
labelNames: meta.label_names ? JSON.parse(meta.label_names) : {}
|
|
1111
|
+
};
|
|
1112
|
+
console.error(`running ${kind} over ${limited.length} rows of ${flags.dataset} [${flags.split}] ` + `with ${models.length} model(s)`);
|
|
1113
|
+
const runId = await runSweep(stores, limited, {
|
|
1114
|
+
kind,
|
|
1115
|
+
dataset: flags.dataset,
|
|
1116
|
+
split: flags.split,
|
|
1117
|
+
models,
|
|
1118
|
+
columns,
|
|
1119
|
+
context,
|
|
1120
|
+
onProgress: (done, total, model, ok) => {
|
|
1121
|
+
console.error(`[${done}/${total}] ${model} ${ok ? "ok" : "FAIL"}`);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
console.error(`run ${runId} complete
|
|
1125
|
+
`);
|
|
1126
|
+
await printReport(stores, runId, flags.format);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/providers.ts
|
|
1130
|
+
import { registerAiTasks } from "@workglow/ai";
|
|
1131
|
+
import { registerAnthropicInline } from "@workglow/anthropic/ai-runtime";
|
|
1132
|
+
import { registerGeminiInline } from "@workglow/google-gemini/ai-runtime";
|
|
1133
|
+
import { registerHuggingFaceTransformers } from "@workglow/huggingface-transformers/ai";
|
|
1134
|
+
import { registerLlamaCpp } from "@workglow/node-llama-cpp/ai";
|
|
1135
|
+
import { registerOpenAiInline } from "@workglow/openai/ai-runtime";
|
|
1136
|
+
import { registerBaseTasks, registerBuiltInTransforms } from "@workglow/task-graph";
|
|
1137
|
+
import { EnvCredentialStore, setGlobalCredentialStore } from "@workglow/util";
|
|
1138
|
+
import { registerXaiInline } from "@workglow/xai/ai-runtime";
|
|
1139
|
+
async function registerEvalProviders(config) {
|
|
1140
|
+
registerBaseTasks();
|
|
1141
|
+
registerAiTasks();
|
|
1142
|
+
registerBuiltInTransforms();
|
|
1143
|
+
setGlobalCredentialStore(new EnvCredentialStore);
|
|
1144
|
+
process.env.WORKGLOW_MODEL_CACHE = config.modelCache;
|
|
1145
|
+
const registrations = [
|
|
1146
|
+
[
|
|
1147
|
+
"HF_TRANSFORMERS_ONNX",
|
|
1148
|
+
() => registerHuggingFaceTransformers({
|
|
1149
|
+
worker: () => new Worker(new URL("./worker_hft.js", import.meta.url), { type: "module" })
|
|
1150
|
+
})
|
|
1151
|
+
],
|
|
1152
|
+
[
|
|
1153
|
+
"LOCAL_LLAMACPP",
|
|
1154
|
+
() => registerLlamaCpp({
|
|
1155
|
+
worker: () => new Worker(new URL("./worker_llamacpp.js", import.meta.url), { type: "module" })
|
|
1156
|
+
})
|
|
1157
|
+
],
|
|
1158
|
+
["ANTHROPIC", () => registerAnthropicInline()],
|
|
1159
|
+
["OPENAI", () => registerOpenAiInline()],
|
|
1160
|
+
["GOOGLE_GEMINI", () => registerGeminiInline()],
|
|
1161
|
+
["XAI", () => registerXaiInline()]
|
|
1162
|
+
];
|
|
1163
|
+
for (const [name, register] of registrations) {
|
|
1164
|
+
try {
|
|
1165
|
+
await register();
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
console.error(`warning: provider ${name} failed to register and its models will be unavailable: ` + `${err instanceof Error ? err.message : String(err)}`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/storage.ts
|
|
1173
|
+
import { Sqlite, SqliteTabularStorage } from "@workglow/sqlite/storage";
|
|
1174
|
+
import { InMemoryTabularStorage } from "@workglow/storage";
|
|
1175
|
+
var DatasetMetaSchema = {
|
|
1176
|
+
type: "object",
|
|
1177
|
+
properties: {
|
|
1178
|
+
dataset: { type: "string" },
|
|
1179
|
+
split: { type: "string" },
|
|
1180
|
+
config: { type: "string" },
|
|
1181
|
+
num_rows: { type: "number" },
|
|
1182
|
+
columns: { type: "string" },
|
|
1183
|
+
label_names: { type: ["string", "null"] },
|
|
1184
|
+
source: { type: "string" },
|
|
1185
|
+
fetched_at: { type: "string" }
|
|
1186
|
+
},
|
|
1187
|
+
required: ["dataset", "split", "config", "num_rows", "columns", "source", "fetched_at"],
|
|
1188
|
+
additionalProperties: false
|
|
1189
|
+
};
|
|
1190
|
+
var DatasetMetaPrimaryKeyNames = ["dataset", "split"];
|
|
1191
|
+
var DatasetRowSchema = {
|
|
1192
|
+
type: "object",
|
|
1193
|
+
properties: {
|
|
1194
|
+
dataset: { type: "string" },
|
|
1195
|
+
split: { type: "string" },
|
|
1196
|
+
row_index: { type: "number" },
|
|
1197
|
+
data: { type: "string" }
|
|
1198
|
+
},
|
|
1199
|
+
required: ["dataset", "split", "row_index", "data"],
|
|
1200
|
+
additionalProperties: false
|
|
1201
|
+
};
|
|
1202
|
+
var DatasetRowPrimaryKeyNames = ["dataset", "split", "row_index"];
|
|
1203
|
+
var EvalRunSchema = {
|
|
1204
|
+
type: "object",
|
|
1205
|
+
properties: {
|
|
1206
|
+
run_id: { type: "string", "x-auto-generated": true },
|
|
1207
|
+
kind: { type: "string" },
|
|
1208
|
+
dataset: { type: "string" },
|
|
1209
|
+
split: { type: "string" },
|
|
1210
|
+
models: { type: "string" },
|
|
1211
|
+
options: { type: "string" },
|
|
1212
|
+
created_at: { type: "string" }
|
|
1213
|
+
},
|
|
1214
|
+
required: ["run_id", "kind", "dataset", "split", "models", "options", "created_at"],
|
|
1215
|
+
additionalProperties: false
|
|
1216
|
+
};
|
|
1217
|
+
var EvalRunPrimaryKeyNames = ["run_id"];
|
|
1218
|
+
var EvalResultSchema = {
|
|
1219
|
+
type: "object",
|
|
1220
|
+
properties: {
|
|
1221
|
+
run_id: { type: "string" },
|
|
1222
|
+
model: { type: "string" },
|
|
1223
|
+
row_index: { type: "number" },
|
|
1224
|
+
ok: { type: "number" },
|
|
1225
|
+
error: { type: ["string", "null"] },
|
|
1226
|
+
expected: { type: ["string", "null"] },
|
|
1227
|
+
predicted: { type: ["string", "null"] },
|
|
1228
|
+
expected_value: { type: ["number", "null"] },
|
|
1229
|
+
predicted_value: { type: ["number", "null"] },
|
|
1230
|
+
latency_ms: { type: "number" }
|
|
1231
|
+
},
|
|
1232
|
+
required: ["run_id", "model", "row_index", "ok", "latency_ms"],
|
|
1233
|
+
additionalProperties: false
|
|
1234
|
+
};
|
|
1235
|
+
var EvalResultPrimaryKeyNames = ["run_id", "model", "row_index"];
|
|
1236
|
+
async function createSqliteStores(config) {
|
|
1237
|
+
await Sqlite.init();
|
|
1238
|
+
const db = new Sqlite.Database(config.dbPath);
|
|
1239
|
+
const stores = {
|
|
1240
|
+
datasets: new SqliteTabularStorage(db, "eval_dataset", DatasetMetaSchema, DatasetMetaPrimaryKeyNames),
|
|
1241
|
+
rows: new SqliteTabularStorage(db, "eval_dataset_row", DatasetRowSchema, DatasetRowPrimaryKeyNames),
|
|
1242
|
+
runs: new SqliteTabularStorage(db, "eval_run", EvalRunSchema, EvalRunPrimaryKeyNames),
|
|
1243
|
+
results: new SqliteTabularStorage(db, "eval_result", EvalResultSchema, EvalResultPrimaryKeyNames)
|
|
1244
|
+
};
|
|
1245
|
+
await setupStores(stores);
|
|
1246
|
+
return stores;
|
|
1247
|
+
}
|
|
1248
|
+
async function setupStores(stores) {
|
|
1249
|
+
await stores.datasets.setupDatabase();
|
|
1250
|
+
await stores.rows.setupDatabase();
|
|
1251
|
+
await stores.runs.setupDatabase();
|
|
1252
|
+
await stores.results.setupDatabase();
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/workglow-eval.ts
|
|
1256
|
+
var config = loadConfig();
|
|
1257
|
+
var stores;
|
|
1258
|
+
var openStores = () => stores ??= createSqliteStores(config);
|
|
1259
|
+
var providers;
|
|
1260
|
+
var ensureProviders = () => providers ??= registerEvalProviders(config);
|
|
1261
|
+
program.version("2.0.0").description("Workglow eval example \u2014 pull HuggingFace datasets into storage, run task workflows " + "across models, and score the stored results");
|
|
1262
|
+
registerDatasetCommand(program, openStores);
|
|
1263
|
+
registerRunCommand(program, openStores, ensureProviders);
|
|
1264
|
+
registerReportCommand(program, openStores);
|
|
1265
|
+
await program.parseAsync(process.argv);
|
|
1266
|
+
process.exit(process.exitCode ?? 0);
|