@valerypopoff/rivet2-evaluations 2.3.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/LICENSE +7 -0
- package/README.md +27 -0
- package/dist/cjs/bundle.cjs +3160 -0
- package/dist/cjs/bundle.cjs.map +7 -0
- package/dist/esm/assertions.js +555 -0
- package/dist/esm/canonical.js +109 -0
- package/dist/esm/dataTypes.js +103 -0
- package/dist/esm/datasetTransfer.js +146 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/normalization.js +369 -0
- package/dist/esm/runner.js +1767 -0
- package/dist/esm/serialization.js +277 -0
- package/dist/esm/store.js +88 -0
- package/dist/esm/suiteTransfer.js +63 -0
- package/dist/esm/types.js +16 -0
- package/dist/types/assertions.d.ts +17 -0
- package/dist/types/canonical.d.ts +23 -0
- package/dist/types/dataTypes.d.ts +19 -0
- package/dist/types/datasetTransfer.d.ts +26 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/normalization.d.ts +11 -0
- package/dist/types/runner.d.ts +66 -0
- package/dist/types/serialization.d.ts +6 -0
- package/dist/types/store.d.ts +37 -0
- package/dist/types/suiteTransfer.d.ts +22 -0
- package/dist/types/types.d.ts +429 -0
- package/package.json +42 -0
|
@@ -0,0 +1,3160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
EvaluationGraphExecutionError: () => EvaluationGraphExecutionError,
|
|
34
|
+
InMemoryEvaluationRunStore: () => InMemoryEvaluationRunStore,
|
|
35
|
+
LEGACY_EVALUATOR_INPUT_IDS: () => LEGACY_EVALUATOR_INPUT_IDS,
|
|
36
|
+
areEvaluationDataTypesCompatible: () => areEvaluationDataTypesCompatible,
|
|
37
|
+
assertEvaluationDatasetSnapshot: () => assertEvaluationDatasetSnapshot,
|
|
38
|
+
assertEvaluationDatasetValuesMatchDeclaredTypes: () => assertEvaluationDatasetValuesMatchDeclaredTypes,
|
|
39
|
+
assertPortableJson: () => assertPortableJson,
|
|
40
|
+
canonicalStringify: () => canonicalStringify,
|
|
41
|
+
canonicalizePortableJson: () => canonicalizePortableJson,
|
|
42
|
+
clonePortableJson: () => clonePortableJson,
|
|
43
|
+
createEmptyEvaluationProjectData: () => createEmptyEvaluationProjectData,
|
|
44
|
+
createEmptyMetrics: () => createEmptyMetrics,
|
|
45
|
+
createEvaluationBaselineSnapshot: () => createEvaluationBaselineSnapshot,
|
|
46
|
+
deserializeEvaluationDatasetJson: () => deserializeEvaluationDatasetJson,
|
|
47
|
+
deserializeEvaluationProjectData: () => deserializeEvaluationProjectData,
|
|
48
|
+
deserializeEvaluationProjectDataFromString: () => deserializeEvaluationProjectDataFromString,
|
|
49
|
+
deserializeEvaluationSuiteBundleJson: () => deserializeEvaluationSuiteBundleJson,
|
|
50
|
+
evaluateAssertion: () => evaluateAssertion,
|
|
51
|
+
evaluateThresholdResults: () => evaluateThresholdResults,
|
|
52
|
+
finalizeEvaluationRecordingRetention: () => finalizeEvaluationRecordingRetention,
|
|
53
|
+
fingerprint: () => fingerprint,
|
|
54
|
+
fingerprintEvaluationDataset: () => fingerprintEvaluationDataset,
|
|
55
|
+
getEvaluationSuiteMode: () => getEvaluationSuiteMode,
|
|
56
|
+
getEvaluationTopLevelOutputId: () => getEvaluationTopLevelOutputId,
|
|
57
|
+
hasAuthoritativeEvaluationCriteria: () => hasAuthoritativeEvaluationCriteria,
|
|
58
|
+
isEvaluationOutputPathSyntaxValid: () => isEvaluationOutputPathSyntaxValid,
|
|
59
|
+
isEvaluationValueCompatibleWithDataType: () => isEvaluationValueCompatibleWithDataType,
|
|
60
|
+
isPortableEvaluationDataType: () => isPortableEvaluationDataType,
|
|
61
|
+
localizeEvaluationDataset: () => localizeEvaluationDataset,
|
|
62
|
+
normalizeEvaluationAggregate: () => normalizeEvaluationAggregate,
|
|
63
|
+
normalizeEvaluationBaselineSnapshot: () => normalizeEvaluationBaselineSnapshot,
|
|
64
|
+
normalizeEvaluationRun: () => normalizeEvaluationRun,
|
|
65
|
+
normalizeEvaluationTrial: () => normalizeEvaluationTrial,
|
|
66
|
+
portableEvaluationDataTypes: () => portableEvaluationDataTypes,
|
|
67
|
+
runEvaluationCases: () => runEvaluationCases,
|
|
68
|
+
runEvaluationSuite: () => runEvaluationSuite,
|
|
69
|
+
runEvaluationWorkPool: () => runEvaluationWorkPool,
|
|
70
|
+
serializeEvaluationDatasetJson: () => serializeEvaluationDatasetJson,
|
|
71
|
+
serializeEvaluationProjectData: () => serializeEvaluationProjectData,
|
|
72
|
+
serializeEvaluationProjectDataToString: () => serializeEvaluationProjectDataToString,
|
|
73
|
+
serializeEvaluationSuiteBundleJson: () => serializeEvaluationSuiteBundleJson,
|
|
74
|
+
shouldReplaceEvaluationRun: () => shouldReplaceEvaluationRun,
|
|
75
|
+
summarizeEvaluationRun: () => summarizeEvaluationRun,
|
|
76
|
+
usesLegacyEvaluatorInputEnvelope: () => usesLegacyEvaluatorInputEnvelope,
|
|
77
|
+
validateEvaluationAssertionConfiguration: () => validateEvaluationAssertionConfiguration,
|
|
78
|
+
validateEvaluationAssertionExpectedValue: () => validateEvaluationAssertionExpectedValue,
|
|
79
|
+
validateEvaluationDataset: () => validateEvaluationDataset,
|
|
80
|
+
validateEvaluationDatasetForTransfer: () => validateEvaluationDatasetForTransfer
|
|
81
|
+
});
|
|
82
|
+
module.exports = __toCommonJS(index_exports);
|
|
83
|
+
|
|
84
|
+
// src/types.ts
|
|
85
|
+
var EvaluationGraphExecutionError = class extends Error {
|
|
86
|
+
metrics;
|
|
87
|
+
recording;
|
|
88
|
+
providerAttempts;
|
|
89
|
+
constructor(message, details = {}) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "EvaluationGraphExecutionError";
|
|
92
|
+
this.metrics = details.metrics;
|
|
93
|
+
this.recording = details.recording;
|
|
94
|
+
this.providerAttempts = details.providerAttempts;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/canonical.ts
|
|
99
|
+
function assertPortableJson(value, path = "$", stack = /* @__PURE__ */ new Set()) {
|
|
100
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
101
|
+
if (typeof value === "number") {
|
|
102
|
+
if (!Number.isFinite(value)) throw new Error(`${path} must not contain a non-finite number.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value !== "object") throw new Error(`${path} must be portable JSON.`);
|
|
106
|
+
if (stack.has(value)) throw new Error(`${path} must not contain a cycle.`);
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
109
|
+
throw new Error(`${path} must not contain symbol-keyed array properties.`);
|
|
110
|
+
}
|
|
111
|
+
stack.add(value);
|
|
112
|
+
try {
|
|
113
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
114
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, index);
|
|
115
|
+
if (!descriptor) throw new Error(`${path}[${index}] must not be a sparse array entry.`);
|
|
116
|
+
if (!descriptor.enumerable || descriptor.get || descriptor.set) {
|
|
117
|
+
throw new Error(`${path}[${index}] must be an enumerable data property.`);
|
|
118
|
+
}
|
|
119
|
+
assertPortableJson(descriptor.value, `${path}[${index}]`, stack);
|
|
120
|
+
}
|
|
121
|
+
if (Object.getOwnPropertyNames(value).length !== value.length + 1) {
|
|
122
|
+
throw new Error(`${path} must not contain hidden or extra array properties.`);
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
stack.delete(value);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
130
|
+
throw new Error(`${path} must be a plain object.`);
|
|
131
|
+
}
|
|
132
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
133
|
+
throw new Error(`${path} must not contain symbol-keyed object properties.`);
|
|
134
|
+
}
|
|
135
|
+
const ownPropertyNames = Object.getOwnPropertyNames(value);
|
|
136
|
+
const keys = Object.keys(value);
|
|
137
|
+
if (ownPropertyNames.length !== keys.length) {
|
|
138
|
+
throw new Error(`${path} must not contain non-enumerable object properties.`);
|
|
139
|
+
}
|
|
140
|
+
stack.add(value);
|
|
141
|
+
try {
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
144
|
+
if (!descriptor || descriptor.get || descriptor.set) {
|
|
145
|
+
throw new Error(`${path}.${key} must be an enumerable data property.`);
|
|
146
|
+
}
|
|
147
|
+
assertPortableJson(descriptor.value, `${path}.${key}`, stack);
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
stack.delete(value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function canonicalizePortableJson(value) {
|
|
154
|
+
assertPortableJson(value);
|
|
155
|
+
if (value === null || typeof value !== "object") return value;
|
|
156
|
+
if (Array.isArray(value)) return value.map(canonicalizePortableJson);
|
|
157
|
+
return Object.fromEntries(
|
|
158
|
+
Object.keys(value).sort().map((key) => [key, canonicalizePortableJson(value[key])])
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
function clonePortableJson(value) {
|
|
162
|
+
assertPortableJson(value);
|
|
163
|
+
return structuredClone(value);
|
|
164
|
+
}
|
|
165
|
+
function canonicalStringify(value) {
|
|
166
|
+
return JSON.stringify(canonicalizePortableJson(value));
|
|
167
|
+
}
|
|
168
|
+
function fingerprint(value) {
|
|
169
|
+
const source = canonicalStringify(value);
|
|
170
|
+
let hash = 0xcbf29ce484222325n;
|
|
171
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
172
|
+
hash ^= BigInt(source.charCodeAt(index));
|
|
173
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
174
|
+
}
|
|
175
|
+
return `fnv1a64:${hash.toString(16).padStart(16, "0")}`;
|
|
176
|
+
}
|
|
177
|
+
function fingerprintEvaluationDataset(value) {
|
|
178
|
+
return fingerprint({ datasetId: value.id, fields: value.fields, cases: value.cases });
|
|
179
|
+
}
|
|
180
|
+
function assertEvaluationDatasetSnapshot(snapshot) {
|
|
181
|
+
if (snapshot.dataset.projectId !== void 0 && snapshot.dataset.projectId !== snapshot.projectId) {
|
|
182
|
+
throw new Error("An evaluation dataset snapshot must belong to its project.");
|
|
183
|
+
}
|
|
184
|
+
if (fingerprintEvaluationDataset(snapshot.dataset) !== snapshot.fingerprint) {
|
|
185
|
+
throw new Error("An evaluation dataset snapshot fingerprint must match its dataset ID, fields, and cases.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/assertions.ts
|
|
190
|
+
function readQuotedPathKey(source, start) {
|
|
191
|
+
const quote = source[start];
|
|
192
|
+
if (quote !== '"' && quote !== "'") return void 0;
|
|
193
|
+
let cursor = start + 1;
|
|
194
|
+
let key = "";
|
|
195
|
+
while (cursor < source.length) {
|
|
196
|
+
const character = source[cursor++];
|
|
197
|
+
if (character === quote) return { key, cursor };
|
|
198
|
+
if (character !== "\\") {
|
|
199
|
+
key += character;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const escaped = source[cursor++];
|
|
203
|
+
if (escaped === void 0) return void 0;
|
|
204
|
+
if (escaped === "u") {
|
|
205
|
+
const hex = source.slice(cursor, cursor + 4);
|
|
206
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return void 0;
|
|
207
|
+
key += String.fromCharCode(Number.parseInt(hex, 16));
|
|
208
|
+
cursor += 4;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const escapes = {
|
|
212
|
+
'"': '"',
|
|
213
|
+
"'": "'",
|
|
214
|
+
"\\": "\\",
|
|
215
|
+
"/": "/",
|
|
216
|
+
b: "\b",
|
|
217
|
+
f: "\f",
|
|
218
|
+
n: "\n",
|
|
219
|
+
r: "\r",
|
|
220
|
+
t: " "
|
|
221
|
+
};
|
|
222
|
+
if (!(escaped in escapes)) return void 0;
|
|
223
|
+
key += escapes[escaped];
|
|
224
|
+
}
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
function parseEvaluationOutputPath(path) {
|
|
228
|
+
const source = path.trim();
|
|
229
|
+
if (source === "$") return [];
|
|
230
|
+
if (!source.startsWith("$") || source.length === 1) return void 0;
|
|
231
|
+
let cursor = 1;
|
|
232
|
+
const segments = [];
|
|
233
|
+
while (cursor < source.length) {
|
|
234
|
+
if (source[cursor] === ".") {
|
|
235
|
+
cursor += 1;
|
|
236
|
+
while (/\s/u.test(source[cursor] ?? "")) cursor += 1;
|
|
237
|
+
const identifier = /^[A-Za-z_$][\w$]*/u.exec(source.slice(cursor));
|
|
238
|
+
if (!identifier) return void 0;
|
|
239
|
+
segments.push({ kind: "property", key: identifier[0] });
|
|
240
|
+
cursor += identifier[0].length;
|
|
241
|
+
} else if (source[cursor] === "[") {
|
|
242
|
+
cursor += 1;
|
|
243
|
+
while (/\s/u.test(source[cursor] ?? "")) cursor += 1;
|
|
244
|
+
const quoted = readQuotedPathKey(source, cursor);
|
|
245
|
+
if (quoted) {
|
|
246
|
+
segments.push({ kind: "property", key: quoted.key });
|
|
247
|
+
cursor = quoted.cursor;
|
|
248
|
+
} else {
|
|
249
|
+
const index = /^\d+/u.exec(source.slice(cursor));
|
|
250
|
+
if (!index) return void 0;
|
|
251
|
+
segments.push({ kind: "index", index: Number(index[0]) });
|
|
252
|
+
cursor += index[0].length;
|
|
253
|
+
}
|
|
254
|
+
while (/\s/u.test(source[cursor] ?? "")) cursor += 1;
|
|
255
|
+
if (source[cursor] !== "]") return void 0;
|
|
256
|
+
cursor += 1;
|
|
257
|
+
} else {
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return segments;
|
|
262
|
+
}
|
|
263
|
+
function isEvaluationOutputPathSyntaxValid(path) {
|
|
264
|
+
return parseEvaluationOutputPath(path) !== void 0;
|
|
265
|
+
}
|
|
266
|
+
function getEvaluationTopLevelOutputId(path) {
|
|
267
|
+
var _a;
|
|
268
|
+
const first = (_a = parseEvaluationOutputPath(path)) == null ? void 0 : _a[0];
|
|
269
|
+
return (first == null ? void 0 : first.kind) === "property" ? first.key : void 0;
|
|
270
|
+
}
|
|
271
|
+
function getPath(value, path) {
|
|
272
|
+
const segments = parseEvaluationOutputPath(path);
|
|
273
|
+
if (!segments) return void 0;
|
|
274
|
+
let current = value;
|
|
275
|
+
for (const segment of segments) {
|
|
276
|
+
if (Array.isArray(current)) {
|
|
277
|
+
if (segment.kind !== "index") return void 0;
|
|
278
|
+
const index = segment.index;
|
|
279
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) return void 0;
|
|
280
|
+
current = current[index];
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (segment.kind !== "property") return void 0;
|
|
284
|
+
if (current === null || typeof current !== "object" || !Object.hasOwn(current, segment.key)) return void 0;
|
|
285
|
+
current = current[segment.key];
|
|
286
|
+
}
|
|
287
|
+
return current;
|
|
288
|
+
}
|
|
289
|
+
function expectedValue(assertion, testCase) {
|
|
290
|
+
if (assertion.expected.kind === "literal") return assertion.expected.value;
|
|
291
|
+
const value = testCase.values[assertion.expected.fieldId];
|
|
292
|
+
if (value === void 0) throw new Error(`Expected dataset field "${assertion.expected.fieldId}" is missing.`);
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
function typeMatches(actual, expected) {
|
|
296
|
+
if (typeof expected !== "string") return false;
|
|
297
|
+
if (actual === void 0) return false;
|
|
298
|
+
if (expected === "array") return Array.isArray(actual);
|
|
299
|
+
if (expected === "null") return actual === null;
|
|
300
|
+
if (expected === "object") return actual !== null && typeof actual === "object" && !Array.isArray(actual);
|
|
301
|
+
return typeof actual === expected;
|
|
302
|
+
}
|
|
303
|
+
function expectedTextList(assertion, expected) {
|
|
304
|
+
if (!Array.isArray(expected) || expected.length === 0 || expected.some((value) => typeof value !== "string")) {
|
|
305
|
+
throw new Error(`Quality check "${assertion.name}" requires a non-empty array of expected text values.`);
|
|
306
|
+
}
|
|
307
|
+
return expected;
|
|
308
|
+
}
|
|
309
|
+
function assertionConfigurationError(assertion, message) {
|
|
310
|
+
throw new Error(`Quality check "${assertion.name}" ${message}`);
|
|
311
|
+
}
|
|
312
|
+
var ASSERTION_TYPE_NAMES = /* @__PURE__ */ new Set(["array", "boolean", "null", "number", "object", "string"]);
|
|
313
|
+
function expectedTypeName(assertion, expected) {
|
|
314
|
+
if (typeof expected !== "string" || !ASSERTION_TYPE_NAMES.has(expected)) {
|
|
315
|
+
assertionConfigurationError(
|
|
316
|
+
assertion,
|
|
317
|
+
"requires one of these expected type names: array, boolean, null, number, object, or string."
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return expected;
|
|
321
|
+
}
|
|
322
|
+
function expectedString(assertion, expected, description) {
|
|
323
|
+
if (typeof expected !== "string") {
|
|
324
|
+
assertionConfigurationError(assertion, `requires ${description} to be a string.`);
|
|
325
|
+
}
|
|
326
|
+
return expected;
|
|
327
|
+
}
|
|
328
|
+
function expectedFiniteNumber(assertion, expected, description) {
|
|
329
|
+
if (typeof expected !== "number" || !Number.isFinite(expected)) {
|
|
330
|
+
assertionConfigurationError(assertion, `requires ${description} to be a finite number.`);
|
|
331
|
+
}
|
|
332
|
+
return expected;
|
|
333
|
+
}
|
|
334
|
+
function expectedNumberRange(assertion, expected) {
|
|
335
|
+
if (!Array.isArray(expected) || expected.length !== 2 || typeof expected[0] !== "number" || !Number.isFinite(expected[0]) || typeof expected[1] !== "number" || !Number.isFinite(expected[1])) {
|
|
336
|
+
assertionConfigurationError(assertion, "requires an expected range containing exactly two finite numbers.");
|
|
337
|
+
}
|
|
338
|
+
if (expected[0] > expected[1]) {
|
|
339
|
+
assertionConfigurationError(assertion, "requires its expected range minimum to be no greater than its maximum.");
|
|
340
|
+
}
|
|
341
|
+
return expected;
|
|
342
|
+
}
|
|
343
|
+
function expectedSet(assertion, expected) {
|
|
344
|
+
if (!Array.isArray(expected)) assertionConfigurationError(assertion, "requires the expected set to be an array.");
|
|
345
|
+
return expected;
|
|
346
|
+
}
|
|
347
|
+
function operatorDescription(operator) {
|
|
348
|
+
switch (operator) {
|
|
349
|
+
case "equals":
|
|
350
|
+
return "equals";
|
|
351
|
+
case "not-equals":
|
|
352
|
+
return "does not equal";
|
|
353
|
+
case "contains":
|
|
354
|
+
return "contains the expected text";
|
|
355
|
+
case "matches-regex":
|
|
356
|
+
return "matches the regular expression";
|
|
357
|
+
case "type-is":
|
|
358
|
+
return "has the expected type";
|
|
359
|
+
case "json-schema":
|
|
360
|
+
return "matches the JSON Schema";
|
|
361
|
+
case "number-at-least":
|
|
362
|
+
return "is at least the expected value";
|
|
363
|
+
case "number-at-most":
|
|
364
|
+
return "is at most the expected value";
|
|
365
|
+
case "number-between":
|
|
366
|
+
return "is inside the expected range";
|
|
367
|
+
case "array-includes":
|
|
368
|
+
return "includes the expected value";
|
|
369
|
+
case "set-overlaps":
|
|
370
|
+
return "overlaps the expected set";
|
|
371
|
+
case "contains-any":
|
|
372
|
+
return "contains any expected text";
|
|
373
|
+
case "contains-all":
|
|
374
|
+
return "contains every expected text";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function validateEvaluationAssertionExpectedValue(assertion, expected) {
|
|
378
|
+
assertPortableJson(expected, `quality check ${assertion.name} expected value`);
|
|
379
|
+
switch (assertion.operator) {
|
|
380
|
+
case "equals":
|
|
381
|
+
case "not-equals":
|
|
382
|
+
case "array-includes":
|
|
383
|
+
return;
|
|
384
|
+
case "contains":
|
|
385
|
+
expectedString(assertion, expected, "its expected text");
|
|
386
|
+
return;
|
|
387
|
+
case "matches-regex": {
|
|
388
|
+
const pattern = expectedString(assertion, expected, "its expected regular expression");
|
|
389
|
+
try {
|
|
390
|
+
new RegExp(pattern);
|
|
391
|
+
} catch {
|
|
392
|
+
assertionConfigurationError(assertion, "contains an invalid expected regular expression.");
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
case "type-is":
|
|
397
|
+
expectedTypeName(assertion, expected);
|
|
398
|
+
return;
|
|
399
|
+
case "json-schema":
|
|
400
|
+
assertSupportedJsonSchema(assertion, expected);
|
|
401
|
+
return;
|
|
402
|
+
case "number-at-least":
|
|
403
|
+
expectedFiniteNumber(assertion, expected, "its expected minimum");
|
|
404
|
+
return;
|
|
405
|
+
case "number-at-most":
|
|
406
|
+
expectedFiniteNumber(assertion, expected, "its expected maximum");
|
|
407
|
+
return;
|
|
408
|
+
case "number-between":
|
|
409
|
+
expectedNumberRange(assertion, expected);
|
|
410
|
+
return;
|
|
411
|
+
case "set-overlaps":
|
|
412
|
+
expectedSet(assertion, expected);
|
|
413
|
+
return;
|
|
414
|
+
case "contains-any":
|
|
415
|
+
case "contains-all":
|
|
416
|
+
expectedTextList(assertion, expected);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function validateEvaluationAssertionConfiguration(assertion, expected) {
|
|
421
|
+
if (!isEvaluationOutputPathSyntaxValid(assertion.outputPath)) {
|
|
422
|
+
assertionConfigurationError(assertion, `uses invalid output path "${assertion.outputPath}".`);
|
|
423
|
+
}
|
|
424
|
+
validateEvaluationAssertionExpectedValue(assertion, expected);
|
|
425
|
+
}
|
|
426
|
+
var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
427
|
+
"type",
|
|
428
|
+
"enum",
|
|
429
|
+
"const",
|
|
430
|
+
"minLength",
|
|
431
|
+
"maxLength",
|
|
432
|
+
"pattern",
|
|
433
|
+
"minimum",
|
|
434
|
+
"maximum",
|
|
435
|
+
"minItems",
|
|
436
|
+
"maxItems",
|
|
437
|
+
"items",
|
|
438
|
+
"required",
|
|
439
|
+
"properties",
|
|
440
|
+
"additionalProperties"
|
|
441
|
+
]);
|
|
442
|
+
var SUPPORTED_SCHEMA_TYPES = /* @__PURE__ */ new Set(["array", "boolean", "integer", "null", "number", "object", "string"]);
|
|
443
|
+
function schemaRecord(assertion, schema, path) {
|
|
444
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
445
|
+
assertionConfigurationError(assertion, `requires the JSON Schema at ${path} to be an object.`);
|
|
446
|
+
}
|
|
447
|
+
return schema;
|
|
448
|
+
}
|
|
449
|
+
function assertNonNegativeInteger(assertion, value, path) {
|
|
450
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
451
|
+
assertionConfigurationError(assertion, `requires ${path} to be a non-negative safe integer.`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function assertSupportedJsonSchema(assertion, schema, path = "$") {
|
|
455
|
+
const objectSchema = schemaRecord(assertion, schema, path);
|
|
456
|
+
for (const keyword of Object.keys(objectSchema)) {
|
|
457
|
+
if (!SUPPORTED_SCHEMA_KEYWORDS.has(keyword)) {
|
|
458
|
+
assertionConfigurationError(assertion, `uses unsupported JSON Schema keyword "${keyword}" at ${path}.`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (objectSchema.type !== void 0) {
|
|
462
|
+
if (typeof objectSchema.type !== "string" || !SUPPORTED_SCHEMA_TYPES.has(objectSchema.type)) {
|
|
463
|
+
assertionConfigurationError(
|
|
464
|
+
assertion,
|
|
465
|
+
`requires ${path}.type to be one of: array, boolean, integer, null, number, object, or string.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (objectSchema.enum !== void 0 && (!Array.isArray(objectSchema.enum) || objectSchema.enum.length === 0)) {
|
|
470
|
+
assertionConfigurationError(assertion, `requires ${path}.enum to be a non-empty array.`);
|
|
471
|
+
}
|
|
472
|
+
for (const keyword of ["minLength", "maxLength", "minItems", "maxItems"]) {
|
|
473
|
+
if (objectSchema[keyword] !== void 0)
|
|
474
|
+
assertNonNegativeInteger(assertion, objectSchema[keyword], `${path}.${keyword}`);
|
|
475
|
+
}
|
|
476
|
+
if (typeof objectSchema.minLength === "number" && typeof objectSchema.maxLength === "number" && objectSchema.minLength > objectSchema.maxLength) {
|
|
477
|
+
assertionConfigurationError(assertion, `requires ${path}.minLength to be no greater than ${path}.maxLength.`);
|
|
478
|
+
}
|
|
479
|
+
if (typeof objectSchema.minItems === "number" && typeof objectSchema.maxItems === "number" && objectSchema.minItems > objectSchema.maxItems) {
|
|
480
|
+
assertionConfigurationError(assertion, `requires ${path}.minItems to be no greater than ${path}.maxItems.`);
|
|
481
|
+
}
|
|
482
|
+
for (const keyword of ["minimum", "maximum"]) {
|
|
483
|
+
const value = objectSchema[keyword];
|
|
484
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value))) {
|
|
485
|
+
assertionConfigurationError(assertion, `requires ${path}.${keyword} to be a finite number.`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (typeof objectSchema.minimum === "number" && typeof objectSchema.maximum === "number" && objectSchema.minimum > objectSchema.maximum) {
|
|
489
|
+
assertionConfigurationError(assertion, `requires ${path}.minimum to be no greater than ${path}.maximum.`);
|
|
490
|
+
}
|
|
491
|
+
if (objectSchema.pattern !== void 0) {
|
|
492
|
+
if (typeof objectSchema.pattern !== "string") {
|
|
493
|
+
assertionConfigurationError(assertion, `requires ${path}.pattern to be a string.`);
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
new RegExp(objectSchema.pattern);
|
|
497
|
+
} catch {
|
|
498
|
+
assertionConfigurationError(assertion, `contains an invalid regular expression at ${path}.pattern.`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (objectSchema.required !== void 0) {
|
|
502
|
+
if (!Array.isArray(objectSchema.required) || objectSchema.required.some((key) => typeof key !== "string" || key.length === 0) || new Set(objectSchema.required).size !== objectSchema.required.length) {
|
|
503
|
+
assertionConfigurationError(assertion, `requires ${path}.required to contain unique, non-empty strings.`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (objectSchema.additionalProperties !== void 0 && typeof objectSchema.additionalProperties !== "boolean") {
|
|
507
|
+
assertionConfigurationError(assertion, `supports only a boolean ${path}.additionalProperties value.`);
|
|
508
|
+
}
|
|
509
|
+
if (objectSchema.items !== void 0) {
|
|
510
|
+
assertSupportedJsonSchema(assertion, objectSchema.items, `${path}.items`);
|
|
511
|
+
}
|
|
512
|
+
if (objectSchema.properties !== void 0) {
|
|
513
|
+
const properties = schemaRecord(assertion, objectSchema.properties, `${path}.properties`);
|
|
514
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
515
|
+
assertSupportedJsonSchema(assertion, childSchema, `${path}.properties[${JSON.stringify(key)}]`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function schemaTypeMatches(actual, expected) {
|
|
520
|
+
if (expected === "integer") return typeof actual === "number" && Number.isInteger(actual);
|
|
521
|
+
return typeMatches(actual, expected);
|
|
522
|
+
}
|
|
523
|
+
function matchesSchema(actual, schema) {
|
|
524
|
+
if (actual === void 0) return false;
|
|
525
|
+
const objectSchema = schema;
|
|
526
|
+
const declaredType = objectSchema.type;
|
|
527
|
+
if (typeof declaredType === "string" && !schemaTypeMatches(actual, declaredType)) return false;
|
|
528
|
+
if (Array.isArray(objectSchema.enum) && !objectSchema.enum.some((candidate) => canonicalStringify(candidate) === canonicalStringify(actual ?? null)))
|
|
529
|
+
return false;
|
|
530
|
+
if (Object.hasOwn(objectSchema, "const") && canonicalStringify(objectSchema.const) !== canonicalStringify(actual ?? null))
|
|
531
|
+
return false;
|
|
532
|
+
if (typeof actual === "string") {
|
|
533
|
+
if (typeof objectSchema.minLength === "number" && actual.length < objectSchema.minLength) return false;
|
|
534
|
+
if (typeof objectSchema.maxLength === "number" && actual.length > objectSchema.maxLength) return false;
|
|
535
|
+
if (typeof objectSchema.pattern === "string") {
|
|
536
|
+
if (!new RegExp(objectSchema.pattern).test(actual)) return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (typeof actual === "number") {
|
|
540
|
+
if (typeof objectSchema.minimum === "number" && actual < objectSchema.minimum) return false;
|
|
541
|
+
if (typeof objectSchema.maximum === "number" && actual > objectSchema.maximum) return false;
|
|
542
|
+
}
|
|
543
|
+
if (Array.isArray(actual)) {
|
|
544
|
+
if (typeof objectSchema.minItems === "number" && actual.length < objectSchema.minItems) return false;
|
|
545
|
+
if (typeof objectSchema.maxItems === "number" && actual.length > objectSchema.maxItems) return false;
|
|
546
|
+
if (objectSchema.items !== void 0 && !actual.every((item) => matchesSchema(item, objectSchema.items)))
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
if (actual !== null && typeof actual === "object" && !Array.isArray(actual)) {
|
|
550
|
+
const record = actual;
|
|
551
|
+
if (Array.isArray(objectSchema.required) && !objectSchema.required.every((key) => typeof key === "string" && Object.hasOwn(record, key)))
|
|
552
|
+
return false;
|
|
553
|
+
const properties = objectSchema.properties;
|
|
554
|
+
if (properties !== void 0) {
|
|
555
|
+
const propertySchemas = properties;
|
|
556
|
+
for (const [key, childSchema] of Object.entries(propertySchemas)) {
|
|
557
|
+
if (Object.hasOwn(record, key) && !matchesSchema(record[key], childSchema)) return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (objectSchema.additionalProperties === false) {
|
|
561
|
+
const propertySchemas = properties ?? {};
|
|
562
|
+
if (Object.keys(record).some((key) => !Object.hasOwn(propertySchemas, key))) return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
function evaluateAssertion(assertion, outputs, testCase) {
|
|
568
|
+
const outputRoot = outputs;
|
|
569
|
+
const actual = getPath(outputRoot, assertion.outputPath);
|
|
570
|
+
const actualFound = actual !== void 0;
|
|
571
|
+
const expected = expectedValue(assertion, testCase);
|
|
572
|
+
validateEvaluationAssertionConfiguration(assertion, expected);
|
|
573
|
+
let passed = false;
|
|
574
|
+
switch (assertion.operator) {
|
|
575
|
+
case "equals":
|
|
576
|
+
passed = actual !== void 0 && canonicalStringify(actual) === canonicalStringify(expected);
|
|
577
|
+
break;
|
|
578
|
+
case "not-equals":
|
|
579
|
+
passed = actual !== void 0 && canonicalStringify(actual) !== canonicalStringify(expected);
|
|
580
|
+
break;
|
|
581
|
+
case "contains":
|
|
582
|
+
{
|
|
583
|
+
const expectedText = expected;
|
|
584
|
+
passed = typeof actual === "string" && actual.includes(expectedText);
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
case "matches-regex":
|
|
588
|
+
{
|
|
589
|
+
const pattern = new RegExp(expected);
|
|
590
|
+
passed = typeof actual === "string" && pattern.test(actual);
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
case "type-is":
|
|
594
|
+
passed = typeMatches(actual, expected);
|
|
595
|
+
break;
|
|
596
|
+
case "json-schema":
|
|
597
|
+
passed = matchesSchema(actual, expected);
|
|
598
|
+
break;
|
|
599
|
+
case "number-at-least":
|
|
600
|
+
{
|
|
601
|
+
const minimum = expected;
|
|
602
|
+
passed = typeof actual === "number" && actual >= minimum;
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
case "number-at-most":
|
|
606
|
+
{
|
|
607
|
+
const maximum = expected;
|
|
608
|
+
passed = typeof actual === "number" && actual <= maximum;
|
|
609
|
+
}
|
|
610
|
+
break;
|
|
611
|
+
case "number-between":
|
|
612
|
+
{
|
|
613
|
+
const [minimum, maximum] = expected;
|
|
614
|
+
passed = typeof actual === "number" && actual >= minimum && actual <= maximum;
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
case "array-includes":
|
|
618
|
+
passed = Array.isArray(actual) && actual.some((item) => canonicalStringify(item) === canonicalStringify(expected));
|
|
619
|
+
break;
|
|
620
|
+
case "set-overlaps":
|
|
621
|
+
{
|
|
622
|
+
const candidates = expected;
|
|
623
|
+
passed = Array.isArray(actual) && candidates.some(
|
|
624
|
+
(candidate) => actual.some((item) => canonicalStringify(item) === canonicalStringify(candidate))
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
case "contains-any":
|
|
629
|
+
{
|
|
630
|
+
const expectedTexts = expected;
|
|
631
|
+
passed = typeof actual === "string" && expectedTexts.some((candidate) => actual.includes(candidate));
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
case "contains-all":
|
|
635
|
+
{
|
|
636
|
+
const expectedTexts = expected;
|
|
637
|
+
passed = typeof actual === "string" && expectedTexts.every((candidate) => actual.includes(candidate));
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
const requirement = operatorDescription(assertion.operator);
|
|
642
|
+
return {
|
|
643
|
+
id: assertion.id,
|
|
644
|
+
kind: "assertion",
|
|
645
|
+
name: assertion.name,
|
|
646
|
+
required: assertion.required !== false,
|
|
647
|
+
status: passed ? "passed" : "failed",
|
|
648
|
+
message: passed ? `Target output ${requirement}.` : actualFound ? `Target output does not satisfy the requirement: ${requirement}.` : `Target output path "${assertion.outputPath}" does not exist.`,
|
|
649
|
+
evidence: {
|
|
650
|
+
actual: actual ?? null,
|
|
651
|
+
actualFound,
|
|
652
|
+
expected,
|
|
653
|
+
outputPath: assertion.outputPath,
|
|
654
|
+
operator: assertion.operator,
|
|
655
|
+
expectedSource: assertion.expected.kind,
|
|
656
|
+
...assertion.expected.kind === "dataset-field" ? { expectedFieldId: assertion.expected.fieldId } : {}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/normalization.ts
|
|
662
|
+
function isRecord(value) {
|
|
663
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
664
|
+
}
|
|
665
|
+
function isQualityStatus(value) {
|
|
666
|
+
return ["passed", "failed", "scored", "not-evaluated", "unable-to-evaluate"].includes(String(value));
|
|
667
|
+
}
|
|
668
|
+
function isEvaluationMode(value) {
|
|
669
|
+
return value === "pass-fail" || value === "scoring";
|
|
670
|
+
}
|
|
671
|
+
function isQualityStatusForMode(status, evaluationMode) {
|
|
672
|
+
return evaluationMode === "scoring" ? status !== "passed" && status !== "failed" : status !== "scored";
|
|
673
|
+
}
|
|
674
|
+
function isExecutionStatus(value) {
|
|
675
|
+
return ["completed", "error", "canceled"].includes(String(value));
|
|
676
|
+
}
|
|
677
|
+
function isRunExecutionStatus(value) {
|
|
678
|
+
return ["queued", "running", "completed", "canceled", "error"].includes(String(value));
|
|
679
|
+
}
|
|
680
|
+
function isTerminalRunExecutionStatus(status) {
|
|
681
|
+
return status === "completed" || status === "canceled" || status === "error";
|
|
682
|
+
}
|
|
683
|
+
function shouldReplaceEvaluationRun(existing, incoming) {
|
|
684
|
+
if (!existing) return true;
|
|
685
|
+
const existingRevision = existing.revision ?? 0;
|
|
686
|
+
const incomingRevision = incoming.revision ?? 0;
|
|
687
|
+
if (incomingRevision !== existingRevision) return incomingRevision > existingRevision;
|
|
688
|
+
return !isTerminalRunExecutionStatus(existing.executionStatus) || isTerminalRunExecutionStatus(incoming.executionStatus);
|
|
689
|
+
}
|
|
690
|
+
function isPurpose(value) {
|
|
691
|
+
return value === "evaluation" || value === "execution-benchmark";
|
|
692
|
+
}
|
|
693
|
+
function isAccountingStatus(value) {
|
|
694
|
+
return value === "complete" || value === "partial";
|
|
695
|
+
}
|
|
696
|
+
function isNormalizedScore(value) {
|
|
697
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
698
|
+
}
|
|
699
|
+
function reasonForQualityStatus(status, executionStatus) {
|
|
700
|
+
if (executionStatus === "canceled") return { code: "canceled", message: "The execution was canceled." };
|
|
701
|
+
if (executionStatus === "error" && status === "failed") {
|
|
702
|
+
return { code: "target-error", message: "The target graph failed before it could produce a valid result." };
|
|
703
|
+
}
|
|
704
|
+
switch (status) {
|
|
705
|
+
case "passed":
|
|
706
|
+
return { code: "checks-passed", message: "All required quality criteria passed." };
|
|
707
|
+
case "failed":
|
|
708
|
+
return { code: "checks-failed", message: "One or more required quality criteria failed." };
|
|
709
|
+
case "scored":
|
|
710
|
+
return { code: "scores-complete", message: "Every requested trial produced a score." };
|
|
711
|
+
case "not-evaluated":
|
|
712
|
+
return { code: "no-trial-quality-checks", message: "No per-trial quality check evaluated this result." };
|
|
713
|
+
case "unable-to-evaluate":
|
|
714
|
+
return { code: "required-check-error", message: "A required quality criterion could not be evaluated." };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function reasonForQualityStatusInMode(status, evaluationMode, executionStatus) {
|
|
718
|
+
if (evaluationMode === "scoring" && status === "unable-to-evaluate") {
|
|
719
|
+
return { code: "scores-incomplete", message: "One or more requested trials did not produce a usable score." };
|
|
720
|
+
}
|
|
721
|
+
return reasonForQualityStatus(status, executionStatus);
|
|
722
|
+
}
|
|
723
|
+
function isTargetExecutionError(value, observations) {
|
|
724
|
+
if (typeof value.error === "string" && value.error.length > 0) return true;
|
|
725
|
+
return observations.some(
|
|
726
|
+
(observation) => isRecord(observation) && observation.id === "target-error" && observation.kind === "assertion" && observation.status === "error"
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
function legacyQualityFromObservations(observations, targetExecutionError) {
|
|
730
|
+
if (targetExecutionError) return "failed";
|
|
731
|
+
const required = observations.filter(
|
|
732
|
+
(observation) => isRecord(observation) && observation.required !== false
|
|
733
|
+
);
|
|
734
|
+
if (required.some((observation) => observation.status === "failed")) return "failed";
|
|
735
|
+
if (required.some((observation) => observation.status === "error")) return "unable-to-evaluate";
|
|
736
|
+
if (required.some((observation) => observation.status === "passed")) return "passed";
|
|
737
|
+
return "not-evaluated";
|
|
738
|
+
}
|
|
739
|
+
function scoringQualityFromObservations(observations) {
|
|
740
|
+
const evaluatorObservations = observations.filter(
|
|
741
|
+
(observation) => isRecord(observation) && observation.kind === "graph"
|
|
742
|
+
);
|
|
743
|
+
const hasCompleteScore = evaluatorObservations.length > 0 && evaluatorObservations.every(
|
|
744
|
+
(observation) => observation.status === "scored" && typeof observation.score === "number" && Number.isFinite(observation.score) && observation.score >= 0 && observation.score <= 1
|
|
745
|
+
);
|
|
746
|
+
return hasCompleteScore ? "scored" : "unable-to-evaluate";
|
|
747
|
+
}
|
|
748
|
+
function isQualityReasonCompatible(reason, status, purpose, executionStatus) {
|
|
749
|
+
if (purpose === "execution-benchmark") {
|
|
750
|
+
return executionStatus === "canceled" ? reason.code === "canceled" : reason.code === "benchmark";
|
|
751
|
+
}
|
|
752
|
+
if (executionStatus === "queued" || executionStatus === "running") return reason.code === "in-progress";
|
|
753
|
+
if (executionStatus === "canceled") return reason.code === "canceled";
|
|
754
|
+
switch (status) {
|
|
755
|
+
case "passed":
|
|
756
|
+
return reason.code === "checks-passed" || reason.code === "thresholds-passed";
|
|
757
|
+
case "failed":
|
|
758
|
+
return reason.code === "checks-failed" || reason.code === "thresholds-failed" || reason.code === "target-error";
|
|
759
|
+
case "scored":
|
|
760
|
+
return reason.code === "scores-complete";
|
|
761
|
+
case "not-evaluated":
|
|
762
|
+
return reason.code === "no-trial-quality-checks" || reason.code === "no-completed-trials";
|
|
763
|
+
case "unable-to-evaluate":
|
|
764
|
+
return reason.code === "required-check-error" || reason.code === "required-metric-unavailable" || reason.code === "scores-incomplete";
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function normalizeQualityReason(value, fallback, status, purpose, executionStatus) {
|
|
768
|
+
if (!isRecord(value) || typeof value.code !== "string" || typeof value.message !== "string") return fallback;
|
|
769
|
+
const reason = value;
|
|
770
|
+
return isQualityReasonCompatible(reason, status, purpose, executionStatus) ? reason : fallback;
|
|
771
|
+
}
|
|
772
|
+
function normalizeEvaluationTrial(value, purpose = "evaluation", evaluationMode = "pass-fail") {
|
|
773
|
+
if (!isRecord(value)) throw new Error("Evaluation trial must be an object.");
|
|
774
|
+
const legacyStatus = value.status;
|
|
775
|
+
const observations = Array.isArray(value.observations) ? value.observations : [];
|
|
776
|
+
const targetExecutionError = isTargetExecutionError(value, observations);
|
|
777
|
+
const executionStatus = isExecutionStatus(value.executionStatus) ? value.executionStatus : legacyStatus === "canceled" ? "canceled" : targetExecutionError ? "error" : legacyStatus === "error" && !observations.some(
|
|
778
|
+
(observation) => isRecord(observation) && observation.required !== false && observation.status === "error"
|
|
779
|
+
) ? "error" : "completed";
|
|
780
|
+
const qualityStatus = purpose === "execution-benchmark" || executionStatus === "canceled" ? "not-evaluated" : executionStatus === "error" ? evaluationMode === "scoring" ? "unable-to-evaluate" : "failed" : evaluationMode === "scoring" ? scoringQualityFromObservations(observations) : isQualityStatus(value.qualityStatus) && isQualityStatusForMode(value.qualityStatus, evaluationMode) ? value.qualityStatus : legacyQualityFromObservations(observations, targetExecutionError);
|
|
781
|
+
const normalized = structuredClone(value);
|
|
782
|
+
delete normalized.status;
|
|
783
|
+
return {
|
|
784
|
+
...normalized,
|
|
785
|
+
executionStatus,
|
|
786
|
+
qualityStatus,
|
|
787
|
+
qualityReason: normalizeQualityReason(
|
|
788
|
+
value.qualityReason,
|
|
789
|
+
executionStatus === "canceled" ? { code: "canceled", message: "The execution was canceled." } : purpose === "execution-benchmark" ? { code: "benchmark", message: "This run measured execution without evaluating output quality." } : reasonForQualityStatusInMode(qualityStatus, evaluationMode, executionStatus),
|
|
790
|
+
qualityStatus,
|
|
791
|
+
purpose,
|
|
792
|
+
executionStatus
|
|
793
|
+
)
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function normalizeEvaluationAggregate(value, trials) {
|
|
797
|
+
if (!isRecord(value)) throw new Error("Evaluation aggregate must be an object.");
|
|
798
|
+
const normalized = structuredClone(value);
|
|
799
|
+
if (!isNormalizedScore(normalized.meanScore)) delete normalized.meanScore;
|
|
800
|
+
if (trials) {
|
|
801
|
+
const completed = trials.filter((trial) => trial.executionStatus === "completed");
|
|
802
|
+
const evaluated2 = completed.filter((trial) => trial.qualityStatus === "passed" || trial.qualityStatus === "failed");
|
|
803
|
+
const passed2 = evaluated2.filter((trial) => trial.qualityStatus === "passed");
|
|
804
|
+
const failed2 = evaluated2.filter((trial) => trial.qualityStatus === "failed");
|
|
805
|
+
const notEvaluated2 = completed.filter((trial) => trial.qualityStatus === "not-evaluated");
|
|
806
|
+
const unable2 = completed.filter((trial) => trial.qualityStatus === "unable-to-evaluate");
|
|
807
|
+
const scored = completed.filter((trial) => trial.qualityStatus === "scored");
|
|
808
|
+
return {
|
|
809
|
+
...normalized,
|
|
810
|
+
trialCount: trials.length,
|
|
811
|
+
evaluatedTrialCount: evaluated2.length,
|
|
812
|
+
notEvaluatedTrialCount: notEvaluated2.length,
|
|
813
|
+
unableToEvaluateTrialCount: unable2.length,
|
|
814
|
+
passedTrialCount: passed2.length,
|
|
815
|
+
failedTrialCount: failed2.length,
|
|
816
|
+
erroredTrialCount: trials.filter((trial) => trial.executionStatus === "error").length,
|
|
817
|
+
canceledTrialCount: trials.filter((trial) => trial.executionStatus === "canceled").length,
|
|
818
|
+
...scored.length > 0 || normalized.scoredTrialCount !== void 0 ? { scoredTrialCount: scored.length, missingScoreTrialCount: trials.length - scored.length } : {},
|
|
819
|
+
passRate: evaluated2.length === 0 ? 0 : passed2.length / evaluated2.length
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const trialCount = Number(value.trialCount ?? 0);
|
|
823
|
+
const passed = Number(value.passedTrialCount ?? 0);
|
|
824
|
+
const failed = Number(value.failedTrialCount ?? 0);
|
|
825
|
+
const errored = Number(value.erroredTrialCount ?? 0);
|
|
826
|
+
const canceled = Number(value.canceledTrialCount ?? 0);
|
|
827
|
+
const evaluated = Number(value.evaluatedTrialCount ?? passed + failed);
|
|
828
|
+
const unable = Number(value.unableToEvaluateTrialCount ?? 0);
|
|
829
|
+
const notEvaluated = Number(
|
|
830
|
+
value.notEvaluatedTrialCount ?? Math.max(0, trialCount - evaluated - unable - errored - canceled)
|
|
831
|
+
);
|
|
832
|
+
return {
|
|
833
|
+
...normalized,
|
|
834
|
+
evaluatedTrialCount: evaluated,
|
|
835
|
+
notEvaluatedTrialCount: notEvaluated,
|
|
836
|
+
unableToEvaluateTrialCount: unable,
|
|
837
|
+
passRate: evaluated === 0 ? 0 : passed / evaluated
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function withoutAuthoritativeCost(aggregate2) {
|
|
841
|
+
const normalized = { ...aggregate2 };
|
|
842
|
+
delete normalized.totalCostUsd;
|
|
843
|
+
delete normalized.averageCostUsd;
|
|
844
|
+
return normalized;
|
|
845
|
+
}
|
|
846
|
+
function normalizeEvaluationBaselineSnapshot(value) {
|
|
847
|
+
if (!isRecord(value)) throw new Error("Evaluation baseline snapshot must be an object.");
|
|
848
|
+
const accountingStatus = isAccountingStatus(value.accountingStatus) ? value.accountingStatus : isRecord(value.provenance) && value.provenance.accountingComplete === false ? "partial" : "complete";
|
|
849
|
+
const aggregate2 = normalizeEvaluationAggregate(value.aggregate);
|
|
850
|
+
const purpose = isPurpose(value.purpose) ? value.purpose : "evaluation";
|
|
851
|
+
const evaluationMode = isEvaluationMode(value.evaluationMode) ? value.evaluationMode : "pass-fail";
|
|
852
|
+
const hasCompleteScoringAggregate = aggregate2.trialCount > 0 && aggregate2.scoredTrialCount === aggregate2.trialCount && aggregate2.missingScoreTrialCount === 0 && aggregate2.meanScore !== void 0;
|
|
853
|
+
const qualityStatus = purpose === "execution-benchmark" ? "not-evaluated" : evaluationMode === "scoring" ? hasCompleteScoringAggregate ? "scored" : "unable-to-evaluate" : isQualityStatus(value.qualityStatus) && isQualityStatusForMode(value.qualityStatus, evaluationMode) ? value.qualityStatus : aggregate2.failedTrialCount > 0 || aggregate2.erroredTrialCount > 0 ? "failed" : aggregate2.evaluatedTrialCount > 0 ? "passed" : "not-evaluated";
|
|
854
|
+
const qualityReasonFallback = purpose === "execution-benchmark" ? { code: "benchmark", message: "This run measured execution without evaluating output quality." } : reasonForQualityStatusInMode(qualityStatus, evaluationMode);
|
|
855
|
+
return {
|
|
856
|
+
...structuredClone(value),
|
|
857
|
+
purpose,
|
|
858
|
+
evaluationMode,
|
|
859
|
+
qualityStatus,
|
|
860
|
+
qualityReason: normalizeQualityReason(value.qualityReason, qualityReasonFallback, qualityStatus, purpose),
|
|
861
|
+
accountingStatus,
|
|
862
|
+
aggregate: accountingStatus === "partial" ? withoutAuthoritativeCost(aggregate2) : aggregate2
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function normalizeEvaluationRun(value) {
|
|
866
|
+
if (!isRecord(value)) throw new Error("Evaluation run must be an object.");
|
|
867
|
+
const purpose = isPurpose(value.purpose) ? value.purpose : "evaluation";
|
|
868
|
+
const evaluationMode = isEvaluationMode(value.evaluationMode) ? value.evaluationMode : "pass-fail";
|
|
869
|
+
const executionStatus = isRunExecutionStatus(value.executionStatus) ? value.executionStatus : typeof value.completedAt === "string" ? "completed" : "running";
|
|
870
|
+
const trials = Array.isArray(value.trials) ? value.trials.map((trial) => normalizeEvaluationTrial(trial, purpose, evaluationMode)) : [];
|
|
871
|
+
const accountingStatus = isAccountingStatus(value.accountingStatus) ? value.accountingStatus : isRecord(value.provenance) && value.provenance.accountingComplete === false ? "partial" : "complete";
|
|
872
|
+
let qualityStatus;
|
|
873
|
+
if (purpose === "execution-benchmark" || executionStatus === "canceled") {
|
|
874
|
+
qualityStatus = "not-evaluated";
|
|
875
|
+
} else if (executionStatus === "error") {
|
|
876
|
+
qualityStatus = evaluationMode === "scoring" ? "unable-to-evaluate" : "failed";
|
|
877
|
+
} else if (executionStatus === "queued" || executionStatus === "running") {
|
|
878
|
+
qualityStatus = "not-evaluated";
|
|
879
|
+
} else if (evaluationMode === "scoring") {
|
|
880
|
+
const scored = trials.filter((trial) => trial.qualityStatus === "scored").length;
|
|
881
|
+
qualityStatus = scored > 0 && scored === trials.length ? "scored" : "unable-to-evaluate";
|
|
882
|
+
} else if (isQualityStatus(value.qualityStatus) && isQualityStatusForMode(value.qualityStatus, evaluationMode)) {
|
|
883
|
+
qualityStatus = value.qualityStatus;
|
|
884
|
+
} else if (trials.some((trial) => trial.executionStatus === "error" || trial.qualityStatus === "failed")) {
|
|
885
|
+
qualityStatus = "failed";
|
|
886
|
+
} else if (trials.some((trial) => trial.qualityStatus === "unable-to-evaluate")) {
|
|
887
|
+
qualityStatus = "unable-to-evaluate";
|
|
888
|
+
} else if (trials.some((trial) => trial.qualityStatus === "passed")) {
|
|
889
|
+
qualityStatus = "passed";
|
|
890
|
+
} else if (value.verdict === "fail") {
|
|
891
|
+
qualityStatus = "failed";
|
|
892
|
+
} else {
|
|
893
|
+
qualityStatus = "not-evaluated";
|
|
894
|
+
}
|
|
895
|
+
const qualityReasonFallback = executionStatus === "canceled" ? { code: "canceled", message: "The evaluation run was canceled." } : executionStatus === "queued" || executionStatus === "running" ? { code: "in-progress", message: "The run is still in progress." } : purpose === "execution-benchmark" ? { code: "benchmark", message: "This run measured execution without evaluating output quality." } : (executionStatus === "error" || trials.some((trial) => trial.executionStatus === "error")) && qualityStatus === "failed" ? { code: "target-error", message: "One or more target graph executions failed." } : reasonForQualityStatusInMode(qualityStatus, evaluationMode);
|
|
896
|
+
const provenance = isRecord(value.provenance) ? {
|
|
897
|
+
...structuredClone(value.provenance),
|
|
898
|
+
accountingComplete: accountingStatus === "complete"
|
|
899
|
+
} : value.provenance;
|
|
900
|
+
const normalized = structuredClone(value);
|
|
901
|
+
delete normalized.verdict;
|
|
902
|
+
const aggregate2 = value.aggregate === void 0 ? void 0 : normalizeEvaluationAggregate(value.aggregate, trials);
|
|
903
|
+
return {
|
|
904
|
+
...normalized,
|
|
905
|
+
version: 2,
|
|
906
|
+
purpose,
|
|
907
|
+
evaluationMode,
|
|
908
|
+
executionStatus,
|
|
909
|
+
qualityStatus,
|
|
910
|
+
qualityReason: normalizeQualityReason(
|
|
911
|
+
value.qualityReason,
|
|
912
|
+
qualityReasonFallback,
|
|
913
|
+
qualityStatus,
|
|
914
|
+
purpose,
|
|
915
|
+
executionStatus
|
|
916
|
+
),
|
|
917
|
+
accountingStatus,
|
|
918
|
+
provenance,
|
|
919
|
+
trials,
|
|
920
|
+
thresholdResults: Array.isArray(value.thresholdResults) ? structuredClone(value.thresholdResults) : [],
|
|
921
|
+
...aggregate2 === void 0 ? {} : { aggregate: accountingStatus === "partial" ? withoutAuthoritativeCost(aggregate2) : aggregate2 }
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/dataTypes.ts
|
|
926
|
+
var portableEvaluationDataTypes = [
|
|
927
|
+
"any",
|
|
928
|
+
"any[]",
|
|
929
|
+
"string",
|
|
930
|
+
"string[]",
|
|
931
|
+
"date",
|
|
932
|
+
"date[]",
|
|
933
|
+
"time",
|
|
934
|
+
"time[]",
|
|
935
|
+
"datetime",
|
|
936
|
+
"datetime[]",
|
|
937
|
+
"number",
|
|
938
|
+
"number[]",
|
|
939
|
+
"vector",
|
|
940
|
+
"vector[]",
|
|
941
|
+
"boolean",
|
|
942
|
+
"boolean[]",
|
|
943
|
+
"object",
|
|
944
|
+
"object[]"
|
|
945
|
+
];
|
|
946
|
+
var portableEvaluationDataTypeSet = new Set(portableEvaluationDataTypes);
|
|
947
|
+
function isPortableEvaluationDataType(dataType) {
|
|
948
|
+
return portableEvaluationDataTypeSet.has(dataType);
|
|
949
|
+
}
|
|
950
|
+
function isObject(value) {
|
|
951
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
952
|
+
}
|
|
953
|
+
function isFiniteNumber(value) {
|
|
954
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
955
|
+
}
|
|
956
|
+
function isArrayOf(value, predicate) {
|
|
957
|
+
return Array.isArray(value) && value.every(predicate);
|
|
958
|
+
}
|
|
959
|
+
function isEvaluationValueCompatibleWithDataType(value, dataType) {
|
|
960
|
+
try {
|
|
961
|
+
assertPortableJson(value);
|
|
962
|
+
} catch {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
if (dataType === "any") return true;
|
|
966
|
+
if (dataType === "any[]") return Array.isArray(value);
|
|
967
|
+
if (dataType === "string" || dataType === "date" || dataType === "time" || dataType === "datetime") {
|
|
968
|
+
return typeof value === "string";
|
|
969
|
+
}
|
|
970
|
+
if (dataType === "string[]" || dataType === "date[]" || dataType === "time[]" || dataType === "datetime[]") {
|
|
971
|
+
return isArrayOf(value, (item) => typeof item === "string");
|
|
972
|
+
}
|
|
973
|
+
if (dataType === "number") return isFiniteNumber(value);
|
|
974
|
+
if (dataType === "number[]" || dataType === "vector") return isArrayOf(value, isFiniteNumber);
|
|
975
|
+
if (dataType === "vector[]") {
|
|
976
|
+
return isArrayOf(value, (item) => isArrayOf(item, isFiniteNumber));
|
|
977
|
+
}
|
|
978
|
+
if (dataType === "boolean") return typeof value === "boolean";
|
|
979
|
+
if (dataType === "boolean[]") return isArrayOf(value, (item) => typeof item === "boolean");
|
|
980
|
+
if (dataType === "object") return isObject(value);
|
|
981
|
+
if (dataType === "object[]") return isArrayOf(value, isObject);
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
function areEvaluationDataTypesCompatible(sourceDataType, targetDataType) {
|
|
985
|
+
return sourceDataType === "any" || targetDataType === "any" || sourceDataType === targetDataType;
|
|
986
|
+
}
|
|
987
|
+
function assertEvaluationDatasetValuesMatchDeclaredTypes(dataset) {
|
|
988
|
+
const fieldsById = new Map(dataset.fields.map((field) => [field.id, field]));
|
|
989
|
+
for (const [fieldIndex, field] of dataset.fields.entries()) {
|
|
990
|
+
if (!isPortableEvaluationDataType(field.dataType)) {
|
|
991
|
+
throw new Error(
|
|
992
|
+
`fields[${fieldIndex}].dataType "${field.dataType}" has no portable evaluation JSON representation.`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
for (const [caseIndex, testCase] of dataset.cases.entries()) {
|
|
997
|
+
for (const [fieldId, value] of Object.entries(testCase.values)) {
|
|
998
|
+
const field = fieldsById.get(fieldId);
|
|
999
|
+
if (field && !isEvaluationValueCompatibleWithDataType(value, field.dataType)) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`cases[${caseIndex}].values.${fieldId} is not compatible with declared type "${field.dataType}".`
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/runner.ts
|
|
1009
|
+
var import_rivet2_core = require("@valerypopoff/rivet2-core");
|
|
1010
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
1011
|
+
var MAX_CONCURRENCY = 32;
|
|
1012
|
+
var LEGACY_EVALUATOR_INPUT_IDS = ["case", "inputs", "expected", "outputs", "run"];
|
|
1013
|
+
function hasAuthoritativeEvaluationCriteria(suite) {
|
|
1014
|
+
var _a;
|
|
1015
|
+
if (getEvaluationSuiteMode(suite) === "scoring") return suite.evaluators.length > 0;
|
|
1016
|
+
return suite.assertions.some((assertion) => assertion.required !== false) || suite.evaluators.some((evaluator) => evaluator.required !== false) || (((_a = suite.thresholds) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
1017
|
+
}
|
|
1018
|
+
function getEvaluationSuiteMode(suite) {
|
|
1019
|
+
return suite.evaluationMode === "scoring" ? "scoring" : "pass-fail";
|
|
1020
|
+
}
|
|
1021
|
+
function usesLegacyEvaluatorInputEnvelope(evaluator, graphInputIds) {
|
|
1022
|
+
return evaluator.inputBindings === void 0 && LEGACY_EVALUATOR_INPUT_IDS.every((inputId) => graphInputIds.includes(inputId));
|
|
1023
|
+
}
|
|
1024
|
+
async function runEvaluationWorkPool(options) {
|
|
1025
|
+
if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1) {
|
|
1026
|
+
throw new Error("Evaluation worker-pool concurrency must be a positive safe integer.");
|
|
1027
|
+
}
|
|
1028
|
+
const results = Array.from({ length: options.work.length });
|
|
1029
|
+
let nextIndex = 0;
|
|
1030
|
+
const worker = async () => {
|
|
1031
|
+
var _a, _b;
|
|
1032
|
+
while (true) {
|
|
1033
|
+
if ((_a = options.signal) == null ? void 0 : _a.aborted) return;
|
|
1034
|
+
const index = nextIndex++;
|
|
1035
|
+
if (index >= options.work.length) return;
|
|
1036
|
+
const work = options.work[index];
|
|
1037
|
+
const result = await options.execute(work, index);
|
|
1038
|
+
results[index] = result;
|
|
1039
|
+
(_b = options.onSettled) == null ? void 0 : _b.call(options, result, index, results);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
await Promise.all(Array.from({ length: Math.min(options.concurrency, options.work.length) }, worker));
|
|
1043
|
+
return results;
|
|
1044
|
+
}
|
|
1045
|
+
function createEmptyMetrics() {
|
|
1046
|
+
return { durationMs: 0 };
|
|
1047
|
+
}
|
|
1048
|
+
var executionMetricCountKeys = [
|
|
1049
|
+
"inputTokens",
|
|
1050
|
+
"outputTokens",
|
|
1051
|
+
"cachedInputTokens",
|
|
1052
|
+
"reasoningTokens",
|
|
1053
|
+
"modelCallCount",
|
|
1054
|
+
"toolCallCount",
|
|
1055
|
+
"toolFailureCount"
|
|
1056
|
+
];
|
|
1057
|
+
function cloneExecutionMetrics(value, path) {
|
|
1058
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1059
|
+
throw new Error(`${path} must be an object.`);
|
|
1060
|
+
}
|
|
1061
|
+
if (!Number.isFinite(value.durationMs) || value.durationMs < 0) {
|
|
1062
|
+
throw new Error(`${path}.durationMs must be a non-negative finite number.`);
|
|
1063
|
+
}
|
|
1064
|
+
const metrics = { durationMs: value.durationMs };
|
|
1065
|
+
for (const key of executionMetricCountKeys) {
|
|
1066
|
+
const count = value[key];
|
|
1067
|
+
if (count === void 0) continue;
|
|
1068
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
1069
|
+
throw new Error(`${path}.${key} must be a non-negative safe integer.`);
|
|
1070
|
+
}
|
|
1071
|
+
metrics[key] = count;
|
|
1072
|
+
}
|
|
1073
|
+
if (value.costUsd !== void 0) {
|
|
1074
|
+
if (!Number.isFinite(value.costUsd) || value.costUsd < 0) {
|
|
1075
|
+
throw new Error(`${path}.costUsd must be a non-negative finite number.`);
|
|
1076
|
+
}
|
|
1077
|
+
metrics.costUsd = value.costUsd;
|
|
1078
|
+
}
|
|
1079
|
+
if (value.hasUnknownCost !== void 0) {
|
|
1080
|
+
if (typeof value.hasUnknownCost !== "boolean") throw new Error(`${path}.hasUnknownCost must be a boolean.`);
|
|
1081
|
+
metrics.hasUnknownCost = value.hasUnknownCost;
|
|
1082
|
+
}
|
|
1083
|
+
return metrics;
|
|
1084
|
+
}
|
|
1085
|
+
function tryCloneExecutionMetrics(value, path) {
|
|
1086
|
+
if (value === void 0) return void 0;
|
|
1087
|
+
try {
|
|
1088
|
+
return cloneExecutionMetrics(value, path);
|
|
1089
|
+
} catch {
|
|
1090
|
+
return void 0;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function cloneOptionalPortableJson(value, path) {
|
|
1094
|
+
if (value === void 0) return void 0;
|
|
1095
|
+
assertPortableJson(value, path);
|
|
1096
|
+
return clonePortableJson(value);
|
|
1097
|
+
}
|
|
1098
|
+
function tryCloneOptionalPortableJson(value, path) {
|
|
1099
|
+
try {
|
|
1100
|
+
return cloneOptionalPortableJson(value, path);
|
|
1101
|
+
} catch {
|
|
1102
|
+
return void 0;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function finalizeEvaluationRecordingRetention(run, policy = "failures-and-baselines", now = Date.now()) {
|
|
1106
|
+
const retainAll = policy === "all";
|
|
1107
|
+
const finalize = (reference, failed) => {
|
|
1108
|
+
const retention = retainAll ? "retained" : failed ? "failure" : "temporary";
|
|
1109
|
+
return {
|
|
1110
|
+
id: reference.id,
|
|
1111
|
+
retention,
|
|
1112
|
+
...retention === "temporary" ? { expiresAt: new Date(now + 24 * 60 * 60 * 1e3).toISOString() } : {}
|
|
1113
|
+
};
|
|
1114
|
+
};
|
|
1115
|
+
return {
|
|
1116
|
+
...run,
|
|
1117
|
+
trials: run.trials.map((trial) => {
|
|
1118
|
+
const targetFailed = trial.executionStatus !== "completed" || trial.observations.some(
|
|
1119
|
+
(observation) => observation.kind === "assertion" && observation.required && (observation.status === "failed" || observation.status === "error")
|
|
1120
|
+
);
|
|
1121
|
+
return {
|
|
1122
|
+
...trial,
|
|
1123
|
+
...trial.recording === void 0 ? {} : { recording: finalize(trial.recording, targetFailed) },
|
|
1124
|
+
observations: trial.observations.map((observation) => ({
|
|
1125
|
+
...observation,
|
|
1126
|
+
...observation.recording === void 0 ? {} : {
|
|
1127
|
+
recording: finalize(
|
|
1128
|
+
observation.recording,
|
|
1129
|
+
observation.status === "failed" || observation.status === "error"
|
|
1130
|
+
)
|
|
1131
|
+
}
|
|
1132
|
+
}))
|
|
1133
|
+
};
|
|
1134
|
+
})
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
function mergeMetrics(...metrics) {
|
|
1138
|
+
const numberKeys = [
|
|
1139
|
+
"durationMs",
|
|
1140
|
+
"inputTokens",
|
|
1141
|
+
"outputTokens",
|
|
1142
|
+
"cachedInputTokens",
|
|
1143
|
+
"reasoningTokens",
|
|
1144
|
+
"modelCallCount",
|
|
1145
|
+
"toolCallCount",
|
|
1146
|
+
"toolFailureCount",
|
|
1147
|
+
"costUsd"
|
|
1148
|
+
];
|
|
1149
|
+
const merged = { durationMs: 0 };
|
|
1150
|
+
for (const key of numberKeys) {
|
|
1151
|
+
const value = metrics.reduce((sum, item) => sum + (item[key] ?? 0), 0);
|
|
1152
|
+
if (value !== 0 || key === "durationMs") merged[key] = value;
|
|
1153
|
+
}
|
|
1154
|
+
if (metrics.some((item) => item.hasUnknownCost)) merged.hasUnknownCost = true;
|
|
1155
|
+
return merged;
|
|
1156
|
+
}
|
|
1157
|
+
function makeId(prefix) {
|
|
1158
|
+
const cryptoValue = globalThis.crypto;
|
|
1159
|
+
if (cryptoValue == null ? void 0 : cryptoValue.randomUUID) return `${prefix}-${cryptoValue.randomUUID()}`;
|
|
1160
|
+
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1161
|
+
}
|
|
1162
|
+
function toErrorMessage(error) {
|
|
1163
|
+
return error instanceof Error ? error.message : String(error);
|
|
1164
|
+
}
|
|
1165
|
+
function throwIfAborted(signal) {
|
|
1166
|
+
if (signal == null ? void 0 : signal.aborted) throw signal.reason ?? new DOMException("Evaluation canceled.", "AbortError");
|
|
1167
|
+
}
|
|
1168
|
+
function isAbort(error) {
|
|
1169
|
+
return error instanceof DOMException && error.name === "AbortError" || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
1170
|
+
}
|
|
1171
|
+
function resolveSuite(data, id) {
|
|
1172
|
+
const suite = data.suites.find((candidate) => candidate.id === id);
|
|
1173
|
+
if (!suite) throw new Error(`Evaluation suite "${id}" does not exist.`);
|
|
1174
|
+
return suite;
|
|
1175
|
+
}
|
|
1176
|
+
function validateDefinitionIds(suite) {
|
|
1177
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1178
|
+
for (const [kind, definitions] of [
|
|
1179
|
+
["quality check", suite.assertions],
|
|
1180
|
+
["evaluator graph", suite.evaluators],
|
|
1181
|
+
["threshold", suite.thresholds ?? []]
|
|
1182
|
+
]) {
|
|
1183
|
+
for (const definition of definitions) {
|
|
1184
|
+
if (typeof definition.id !== "string" || definition.id.trim().length === 0) {
|
|
1185
|
+
throw new Error(`Every evaluation ${kind} must have a non-empty id.`);
|
|
1186
|
+
}
|
|
1187
|
+
const previous = seen.get(definition.id);
|
|
1188
|
+
if (previous) {
|
|
1189
|
+
throw new Error(`Evaluation ${kind} id "${definition.id}" duplicates a ${previous} id.`);
|
|
1190
|
+
}
|
|
1191
|
+
seen.set(definition.id, kind);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
var rateThresholdMetrics = /* @__PURE__ */ new Set([
|
|
1196
|
+
"pass-rate",
|
|
1197
|
+
"mean-score",
|
|
1198
|
+
"target-error-rate",
|
|
1199
|
+
"evaluator-error-rate",
|
|
1200
|
+
"tool-failure-rate"
|
|
1201
|
+
]);
|
|
1202
|
+
function formatThresholdResultValue(metric, operator, value) {
|
|
1203
|
+
return operator === "max-regression" || rateThresholdMetrics.has(metric) ? `${Number((value * 100).toFixed(4))}%` : String(value);
|
|
1204
|
+
}
|
|
1205
|
+
function validateThresholdConfiguration(threshold) {
|
|
1206
|
+
const { metric, operator, value } = threshold;
|
|
1207
|
+
if (typeof metric !== "string" || metric.length === 0) {
|
|
1208
|
+
throw new Error(`Evaluation threshold "${threshold.id}" must select a metric.`);
|
|
1209
|
+
}
|
|
1210
|
+
if (typeof operator !== "string") {
|
|
1211
|
+
throw new Error(`Evaluation threshold "${threshold.id}" must select an operator.`);
|
|
1212
|
+
}
|
|
1213
|
+
if (!Number.isFinite(value)) throw new Error(`Evaluation threshold "${threshold.id}" must use a finite value.`);
|
|
1214
|
+
if (rateThresholdMetrics.has(metric) && (value < 0 || value > 1)) {
|
|
1215
|
+
throw new Error(`Evaluation threshold "${threshold.id}" must use a value from 0 to 1.`);
|
|
1216
|
+
}
|
|
1217
|
+
if (["average-cost", "total-cost", "average-latency-ms", "p95-latency-ms"].includes(metric) && value < 0) {
|
|
1218
|
+
throw new Error(`Evaluation threshold "${threshold.id}" cannot use a negative value.`);
|
|
1219
|
+
}
|
|
1220
|
+
if (operator === "max-regression") {
|
|
1221
|
+
if (value < 0) throw new Error(`Evaluation regression threshold "${threshold.id}" cannot be negative.`);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const expectedOperator = metric === "pass-rate" || metric === "mean-score" ? "at-least" : metric.startsWith("custom:") ? void 0 : "at-most";
|
|
1225
|
+
if (expectedOperator !== void 0 && operator !== expectedOperator || expectedOperator === void 0 && operator !== "at-least" && operator !== "at-most") {
|
|
1226
|
+
throw new Error(`Evaluation threshold "${threshold.id}" uses an incompatible operator for "${metric}".`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function validateSuite(project, suite, dataset, purpose) {
|
|
1230
|
+
var _a;
|
|
1231
|
+
const evaluationMode = getEvaluationSuiteMode(suite);
|
|
1232
|
+
validateDefinitionIds(suite);
|
|
1233
|
+
if (dataset.projectId !== void 0 && dataset.projectId !== project.metadata.id) {
|
|
1234
|
+
throw new Error(`Evaluation dataset "${dataset.name}" belongs to a different project.`);
|
|
1235
|
+
}
|
|
1236
|
+
if (suite.datasetId !== dataset.id)
|
|
1237
|
+
throw new Error(`Suite "${suite.name}" is bound to a different evaluation dataset.`);
|
|
1238
|
+
const graph = project.graphs[suite.targetGraphId];
|
|
1239
|
+
if (!graph) throw new Error(`Target graph "${suite.targetGraphId}" does not exist.`);
|
|
1240
|
+
const graphInputs = graph.nodes.filter((node) => node.type === "graphInput");
|
|
1241
|
+
const graphInputIds = graphInputs.map((input) => input.data.id);
|
|
1242
|
+
if (new Set(graphInputIds).size !== graphInputIds.length) {
|
|
1243
|
+
throw new Error(`Target graph "${suite.targetGraphId}" has duplicate Graph Input ids.`);
|
|
1244
|
+
}
|
|
1245
|
+
const graphOutputs = graph.nodes.filter((node) => node.type === "graphOutput");
|
|
1246
|
+
const graphOutputIds = graphOutputs.map((output) => output.data.id);
|
|
1247
|
+
if (new Set(graphOutputIds).size !== graphOutputIds.length) {
|
|
1248
|
+
throw new Error(`Target graph "${suite.targetGraphId}" has duplicate Graph Output ids.`);
|
|
1249
|
+
}
|
|
1250
|
+
const inputIds = new Set(graphInputIds);
|
|
1251
|
+
const graphInputsById = new Map(graphInputs.map((input) => [input.data.id, input]));
|
|
1252
|
+
const fields = new Map(dataset.fields.map((field) => [field.id, field]));
|
|
1253
|
+
if (fields.size !== dataset.fields.length)
|
|
1254
|
+
throw new Error(`Evaluation dataset "${dataset.name}" has duplicate field ids.`);
|
|
1255
|
+
const boundGraphInputs = /* @__PURE__ */ new Set();
|
|
1256
|
+
for (const binding of suite.inputBindings) {
|
|
1257
|
+
if (boundGraphInputs.has(binding.graphInputId)) {
|
|
1258
|
+
throw new Error(`Target graph input "${binding.graphInputId}" is bound more than once.`);
|
|
1259
|
+
}
|
|
1260
|
+
boundGraphInputs.add(binding.graphInputId);
|
|
1261
|
+
if (!inputIds.has(binding.graphInputId))
|
|
1262
|
+
throw new Error(`Graph input "${binding.graphInputId}" does not exist on the target graph.`);
|
|
1263
|
+
const field = fields.get(binding.datasetFieldId);
|
|
1264
|
+
if (!field) throw new Error(`Evaluation dataset field "${binding.datasetFieldId}" does not exist.`);
|
|
1265
|
+
if (field.role !== "input")
|
|
1266
|
+
throw new Error(`Dataset field "${field.name}" must have the input role to bind it to a graph input.`);
|
|
1267
|
+
const graphInput = graphInputsById.get(binding.graphInputId);
|
|
1268
|
+
if (!areEvaluationDataTypesCompatible(field.dataType, graphInput.data.dataType)) {
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
`Dataset field "${field.name}" (${field.dataType}) is not compatible with graph input "${graphInput.data.id}" (${graphInput.data.dataType}).`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
for (const input of graphInputs) {
|
|
1275
|
+
if (suite.inputBindings.some((binding) => binding.graphInputId === input.data.id)) continue;
|
|
1276
|
+
if (input.data.defaultValue === void 0 || input.data.useDefaultValueInput) {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`Target graph input "${input.data.id}" must be bound to an evaluation dataset field or have a static graph default.`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
for (const testCase of dataset.cases.filter((candidate) => candidate.enabled !== false)) {
|
|
1283
|
+
for (const [fieldId, value] of Object.entries(testCase.values)) {
|
|
1284
|
+
const field = fields.get(fieldId);
|
|
1285
|
+
if (!field) {
|
|
1286
|
+
throw new Error(`Case "${testCase.name}" supplies unknown dataset field "${fieldId}".`);
|
|
1287
|
+
}
|
|
1288
|
+
assertPortableJson(value, `case.${testCase.id}.${fieldId}`);
|
|
1289
|
+
if (!isEvaluationValueCompatibleWithDataType(value, field.dataType)) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
`Case "${testCase.name}" value for "${field.name}" is not compatible with its declared ${field.dataType} type.`
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
for (const binding of suite.inputBindings) {
|
|
1296
|
+
const value = testCase.values[binding.datasetFieldId];
|
|
1297
|
+
if (value === void 0) {
|
|
1298
|
+
const field = fields.get(binding.datasetFieldId);
|
|
1299
|
+
throw new Error(`Case "${testCase.name}" has no saved value for bound input field "${field.name}".`);
|
|
1300
|
+
}
|
|
1301
|
+
const graphInput = graphInputsById.get(binding.graphInputId);
|
|
1302
|
+
if (!isEvaluationValueCompatibleWithDataType(value, graphInput.data.dataType)) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`Case "${testCase.name}" value for "${binding.datasetFieldId}" is not compatible with target graph input "${binding.graphInputId}" (${graphInput.data.dataType}).`
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
for (const field of dataset.fields) {
|
|
1309
|
+
const requiredForThisRun = field.required && (purpose === "evaluation" || field.role === "input");
|
|
1310
|
+
if (requiredForThisRun && testCase.values[field.id] === void 0) {
|
|
1311
|
+
throw new Error(`Case "${testCase.name}" is missing required field "${field.name}".`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (purpose === "evaluation" && evaluationMode === "pass-fail") {
|
|
1316
|
+
for (const assertion of suite.assertions) {
|
|
1317
|
+
const outputId = getEvaluationTopLevelOutputId(assertion.outputPath);
|
|
1318
|
+
if (assertion.outputPath.trim() !== "$" && outputId === void 0) {
|
|
1319
|
+
throw new Error(
|
|
1320
|
+
`Assertion "${assertion.name}" output path must start with a named target Graph Output.`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
if (outputId !== void 0 && !graphOutputIds.includes(outputId)) {
|
|
1324
|
+
throw new Error(`Assertion "${assertion.name}" references missing target output "${outputId}".`);
|
|
1325
|
+
}
|
|
1326
|
+
if (assertion.expected.kind === "dataset-field") {
|
|
1327
|
+
const field = fields.get(assertion.expected.fieldId);
|
|
1328
|
+
if (!field) throw new Error(`Assertion "${assertion.name}" references a missing expected dataset field.`);
|
|
1329
|
+
if (field.role !== "expected")
|
|
1330
|
+
throw new Error(`Assertion "${assertion.name}" must reference a dataset field with the expected role.`);
|
|
1331
|
+
for (const testCase of dataset.cases.filter((candidate) => candidate.enabled !== false)) {
|
|
1332
|
+
const expected = testCase.values[field.id];
|
|
1333
|
+
if (expected === void 0) {
|
|
1334
|
+
throw new Error(`Case "${testCase.name}" is missing expected field "${field.name}".`);
|
|
1335
|
+
}
|
|
1336
|
+
validateEvaluationAssertionConfiguration(assertion, expected);
|
|
1337
|
+
}
|
|
1338
|
+
} else {
|
|
1339
|
+
validateEvaluationAssertionConfiguration(assertion, assertion.expected.value);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (((_a = suite.configuration) == null ? void 0 : _a.seed) !== void 0) {
|
|
1344
|
+
const seedInputId = suite.configuration.seedGraphInputId;
|
|
1345
|
+
if (!seedInputId) throw new Error("A suite seed requires selecting the target graph input that receives it.");
|
|
1346
|
+
const seedInput = graphInputsById.get(seedInputId);
|
|
1347
|
+
if (!seedInput)
|
|
1348
|
+
throw new Error(`The selected seed graph input "${seedInputId}" does not exist on the target graph.`);
|
|
1349
|
+
if (seedInput.data.dataType !== "number" && seedInput.data.dataType !== "any") {
|
|
1350
|
+
throw new Error(`The selected seed graph input "${seedInputId}" must accept a number.`);
|
|
1351
|
+
}
|
|
1352
|
+
if (suite.inputBindings.some((binding) => binding.graphInputId === seedInputId)) {
|
|
1353
|
+
throw new Error(`The selected seed graph input "${seedInputId}" cannot also be bound to a dataset field.`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
for (const evaluator of purpose === "evaluation" ? suite.evaluators : []) {
|
|
1357
|
+
if (evaluator.scoreWeight !== void 0 && (!Number.isFinite(evaluator.scoreWeight) || evaluator.scoreWeight <= 0)) {
|
|
1358
|
+
throw new Error(`Evaluator graph "${evaluator.name}" score weight must be a positive finite number.`);
|
|
1359
|
+
}
|
|
1360
|
+
const evaluatorGraph = project.graphs[evaluator.graphId];
|
|
1361
|
+
if (!evaluatorGraph) throw new Error(`Evaluator graph "${evaluator.graphId}" does not exist.`);
|
|
1362
|
+
const evaluatorGraphInputs = evaluatorGraph.nodes.filter(
|
|
1363
|
+
(node) => node.type === "graphInput"
|
|
1364
|
+
);
|
|
1365
|
+
const evaluatorInputIds = evaluatorGraphInputs.map((input) => input.data.id);
|
|
1366
|
+
if (new Set(evaluatorInputIds).size !== evaluatorInputIds.length) {
|
|
1367
|
+
throw new Error(`Evaluator graph "${evaluator.name}" has duplicate Graph Input ids.`);
|
|
1368
|
+
}
|
|
1369
|
+
const evaluatorOutputIds = evaluatorGraph.nodes.filter((node) => node.type === "graphOutput").map((output) => output.data.id);
|
|
1370
|
+
if (new Set(evaluatorOutputIds).size !== evaluatorOutputIds.length) {
|
|
1371
|
+
throw new Error(`Evaluator graph "${evaluator.name}" has duplicate Graph Output ids.`);
|
|
1372
|
+
}
|
|
1373
|
+
const evaluatorInputsById = new Map(evaluatorGraphInputs.map((input) => [input.data.id, input]));
|
|
1374
|
+
const usesLegacyInputs = usesLegacyEvaluatorInputEnvelope(evaluator, evaluatorInputIds);
|
|
1375
|
+
if (usesLegacyInputs) {
|
|
1376
|
+
for (const inputId of LEGACY_EVALUATOR_INPUT_IDS) {
|
|
1377
|
+
const evaluatorInput = evaluatorInputsById.get(inputId);
|
|
1378
|
+
if (evaluatorInput.data.dataType !== "object" && evaluatorInput.data.dataType !== "any") {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`Evaluator graph "${evaluator.name}" legacy input "${inputId}" must accept the object or any data type.`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
for (const evaluatorInput of evaluatorGraphInputs) {
|
|
1385
|
+
if (LEGACY_EVALUATOR_INPUT_IDS.includes(evaluatorInput.data.id)) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (evaluatorInput.data.defaultValue === void 0 || evaluatorInput.data.useDefaultValueInput) {
|
|
1389
|
+
throw new Error(
|
|
1390
|
+
`Evaluator graph "${evaluator.name}" input "${evaluatorInput.data.id}" needs a direct binding or a static graph default.`
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
} else {
|
|
1395
|
+
const evaluatorBindings = evaluator.inputBindings ?? [];
|
|
1396
|
+
const boundEvaluatorInputs = /* @__PURE__ */ new Set();
|
|
1397
|
+
for (const binding of evaluatorBindings) {
|
|
1398
|
+
if (boundEvaluatorInputs.has(binding.graphInputId)) {
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
`Evaluator graph "${evaluator.name}" input "${binding.graphInputId}" is bound more than once.`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
boundEvaluatorInputs.add(binding.graphInputId);
|
|
1404
|
+
const evaluatorInput = evaluatorInputsById.get(binding.graphInputId);
|
|
1405
|
+
if (!evaluatorInput) {
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
`Evaluator graph "${evaluator.name}" input "${binding.graphInputId}" does not exist on the selected graph.`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
let sourceDataType;
|
|
1411
|
+
if (binding.source.kind === "dataset-field") {
|
|
1412
|
+
const field = fields.get(binding.source.fieldId);
|
|
1413
|
+
if (!field) {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
`Evaluator graph "${evaluator.name}" binding references missing dataset field "${binding.source.fieldId}".`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
sourceDataType = field.dataType;
|
|
1419
|
+
for (const testCase of dataset.cases.filter((candidate) => candidate.enabled !== false)) {
|
|
1420
|
+
const value = testCase.values[field.id];
|
|
1421
|
+
if (value === void 0) {
|
|
1422
|
+
throw new Error(
|
|
1423
|
+
`Case "${testCase.name}" has no saved value for evaluator input "${binding.graphInputId}" from dataset field "${field.name}".`
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
if (!isEvaluationValueCompatibleWithDataType(value, evaluatorInput.data.dataType)) {
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
`Case "${testCase.name}" value from dataset field "${field.name}" is not compatible with evaluator input "${binding.graphInputId}" (${evaluatorInput.data.dataType}).`
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
} else if (binding.source.kind === "target-output") {
|
|
1433
|
+
const outputId = binding.source.outputId;
|
|
1434
|
+
const targetOutput = graph.nodes.find(
|
|
1435
|
+
(node) => node.type === "graphOutput" && node.data.id === outputId
|
|
1436
|
+
);
|
|
1437
|
+
if (!targetOutput) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`Evaluator graph "${evaluator.name}" binding references missing target output "${outputId}".`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
sourceDataType = targetOutput.data.dataType;
|
|
1443
|
+
} else if (binding.source.kind === "context") {
|
|
1444
|
+
sourceDataType = "object";
|
|
1445
|
+
} else {
|
|
1446
|
+
throw new Error(
|
|
1447
|
+
`Evaluator graph "${evaluator.name}" input "${binding.graphInputId}" uses an unsupported source.`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
if (!areEvaluationDataTypesCompatible(sourceDataType, evaluatorInput.data.dataType)) {
|
|
1451
|
+
throw new Error(
|
|
1452
|
+
`Evaluator graph "${evaluator.name}" source for input "${binding.graphInputId}" (${sourceDataType}) is not compatible with ${evaluatorInput.data.dataType}.`
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
for (const evaluatorInput of evaluatorGraphInputs) {
|
|
1457
|
+
if (boundEvaluatorInputs.has(evaluatorInput.data.id)) continue;
|
|
1458
|
+
if (evaluatorInput.data.defaultValue === void 0 || evaluatorInput.data.useDefaultValueInput) {
|
|
1459
|
+
throw new Error(
|
|
1460
|
+
`Evaluator graph "${evaluator.name}" input "${evaluatorInput.data.id}" must be bound to a target output, dataset field, or evaluation context value, or have a static graph default.`
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const resultOutput = evaluatorGraph.nodes.find(
|
|
1466
|
+
(node) => node.type === "graphOutput" && node.data.id === "result"
|
|
1467
|
+
);
|
|
1468
|
+
if (!resultOutput) {
|
|
1469
|
+
throw new Error(`Evaluator graph "${evaluator.name}" must declare a Graph Output named "result".`);
|
|
1470
|
+
}
|
|
1471
|
+
if (resultOutput.data.dataType !== "object" && resultOutput.data.dataType !== "any") {
|
|
1472
|
+
throw new Error(`Evaluator graph "${evaluator.name}" output "result" must have the object data type.`);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (purpose === "evaluation" && evaluationMode === "pass-fail") {
|
|
1476
|
+
const hasRequiredPerTrialCheck = suite.assertions.some((assertion) => assertion.required !== false) || suite.evaluators.some((evaluator) => evaluator.required !== false);
|
|
1477
|
+
const hasEvaluator = suite.evaluators.length > 0;
|
|
1478
|
+
for (const threshold of suite.thresholds ?? []) {
|
|
1479
|
+
validateThresholdConfiguration(threshold);
|
|
1480
|
+
const metric = threshold.metric;
|
|
1481
|
+
const supportedExecutionMetric = [
|
|
1482
|
+
"target-error-rate",
|
|
1483
|
+
"tool-failure-rate",
|
|
1484
|
+
"average-cost",
|
|
1485
|
+
"total-cost",
|
|
1486
|
+
"average-latency-ms",
|
|
1487
|
+
"p95-latency-ms"
|
|
1488
|
+
].includes(metric);
|
|
1489
|
+
const supportedEvaluatorMetric = metric === "mean-score" || metric === "evaluator-error-rate" || metric.startsWith("custom:") && metric.length > "custom:".length;
|
|
1490
|
+
if (metric === "pass-rate" && !hasRequiredPerTrialCheck) {
|
|
1491
|
+
throw new Error("A pass-rate threshold requires at least one required quality check or evaluator graph.");
|
|
1492
|
+
}
|
|
1493
|
+
if (supportedEvaluatorMetric && !hasEvaluator) {
|
|
1494
|
+
throw new Error(`The "${metric}" threshold requires at least one evaluator graph.`);
|
|
1495
|
+
}
|
|
1496
|
+
if (metric !== "pass-rate" && !supportedExecutionMetric && !supportedEvaluatorMetric) {
|
|
1497
|
+
throw new Error(`Evaluation threshold metric "${metric}" is not supported.`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
} else if (purpose === "evaluation" && suite.evaluators.length === 0) {
|
|
1501
|
+
throw new Error("A scoring evaluation suite requires at least one evaluator graph.");
|
|
1502
|
+
}
|
|
1503
|
+
const caseIds = new Set(dataset.cases.map((testCase) => testCase.id));
|
|
1504
|
+
if (caseIds.size !== dataset.cases.length)
|
|
1505
|
+
throw new Error(`Evaluation dataset "${dataset.name}" has duplicate case ids.`);
|
|
1506
|
+
if (!dataset.cases.some((testCase) => testCase.enabled !== false)) {
|
|
1507
|
+
throw new Error(`Evaluation dataset "${dataset.name}" has no enabled cases.`);
|
|
1508
|
+
}
|
|
1509
|
+
const configuration = suite.configuration;
|
|
1510
|
+
if ((configuration == null ? void 0 : configuration.trialCount) !== void 0 && (!Number.isSafeInteger(configuration.trialCount) || configuration.trialCount < 1)) {
|
|
1511
|
+
throw new Error("Evaluation trial count must be a positive safe integer.");
|
|
1512
|
+
}
|
|
1513
|
+
if ((configuration == null ? void 0 : configuration.concurrency) !== void 0 && (!Number.isSafeInteger(configuration.concurrency) || configuration.concurrency < 1 || configuration.concurrency > MAX_CONCURRENCY)) {
|
|
1514
|
+
throw new Error(`Evaluation concurrency must be a whole number from 1 to ${MAX_CONCURRENCY}.`);
|
|
1515
|
+
}
|
|
1516
|
+
if ((configuration == null ? void 0 : configuration.timeoutMs) !== void 0 && (!Number.isFinite(configuration.timeoutMs) || configuration.timeoutMs <= 0)) {
|
|
1517
|
+
throw new Error("Evaluation timeout must be a positive number of milliseconds.");
|
|
1518
|
+
}
|
|
1519
|
+
if ((configuration == null ? void 0 : configuration.seed) !== void 0 && (!Number.isSafeInteger(configuration.seed) || configuration.seed < 0)) {
|
|
1520
|
+
throw new Error("Evaluation seed must be a non-negative safe integer.");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function resolveCaseInputs(suite, dataset, testCase, purpose) {
|
|
1524
|
+
const fields = new Map(dataset.fields.map((field) => [field.id, field]));
|
|
1525
|
+
const inputs = {};
|
|
1526
|
+
const expected = {};
|
|
1527
|
+
for (const field of dataset.fields) {
|
|
1528
|
+
const value = testCase.values[field.id];
|
|
1529
|
+
if (value === void 0) {
|
|
1530
|
+
const requiredForThisRun = field.required && (purpose === "evaluation" || field.role === "input");
|
|
1531
|
+
if (requiredForThisRun) {
|
|
1532
|
+
throw new Error(`Case "${testCase.name}" is missing required field "${field.name}".`);
|
|
1533
|
+
}
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
assertPortableJson(value, `case.${field.id}`);
|
|
1537
|
+
if (field.role === "expected") expected[field.id] = clonePortableJson(value);
|
|
1538
|
+
}
|
|
1539
|
+
for (const binding of suite.inputBindings) {
|
|
1540
|
+
const field = fields.get(binding.datasetFieldId);
|
|
1541
|
+
const value = testCase.values[field.id];
|
|
1542
|
+
if (value === void 0) {
|
|
1543
|
+
throw new Error(`Case "${testCase.name}" has no saved value for bound input field "${field.name}".`);
|
|
1544
|
+
}
|
|
1545
|
+
if (!isEvaluationValueCompatibleWithDataType(value, field.dataType)) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
`Case "${testCase.name}" input "${field.name}" is not compatible with its declared ${field.dataType} type.`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
inputs[binding.graphInputId] = clonePortableJson(value);
|
|
1551
|
+
}
|
|
1552
|
+
return { inputs, expected };
|
|
1553
|
+
}
|
|
1554
|
+
function createEvaluatorCaseInput(testCase) {
|
|
1555
|
+
return {
|
|
1556
|
+
id: testCase.id,
|
|
1557
|
+
name: testCase.name,
|
|
1558
|
+
enabled: testCase.enabled !== false,
|
|
1559
|
+
...testCase.tags === void 0 ? {} : { tags: [...testCase.tags] },
|
|
1560
|
+
...testCase.note === void 0 ? {} : { note: testCase.note },
|
|
1561
|
+
values: clonePortableJson(testCase.values)
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function createEvaluatorGraphInputs(input) {
|
|
1565
|
+
var _a;
|
|
1566
|
+
const { evaluator, project, testCase, targetInputs, expected, targetOutputs, run } = input;
|
|
1567
|
+
const contextValues = {
|
|
1568
|
+
case: createEvaluatorCaseInput(testCase),
|
|
1569
|
+
inputs: clonePortableJson(targetInputs),
|
|
1570
|
+
expected: clonePortableJson(expected),
|
|
1571
|
+
outputs: clonePortableJson(targetOutputs),
|
|
1572
|
+
run: clonePortableJson(run)
|
|
1573
|
+
};
|
|
1574
|
+
const evaluatorGraphInputs = ((_a = project.graphs[evaluator.graphId]) == null ? void 0 : _a.nodes.filter((node) => node.type === "graphInput")) ?? [];
|
|
1575
|
+
const evaluatorGraphInputIds = evaluatorGraphInputs.map((node) => node.data.id);
|
|
1576
|
+
if (usesLegacyEvaluatorInputEnvelope(evaluator, evaluatorGraphInputIds)) return contextValues;
|
|
1577
|
+
const evaluatorInputsById = new Map(evaluatorGraphInputs.map((node) => [node.data.id, node]));
|
|
1578
|
+
const evaluatorInputs = {};
|
|
1579
|
+
for (const binding of evaluator.inputBindings ?? []) {
|
|
1580
|
+
if (binding.source.kind === "dataset-field") {
|
|
1581
|
+
const value = testCase.values[binding.source.fieldId];
|
|
1582
|
+
if (value === void 0) {
|
|
1583
|
+
throw new Error(
|
|
1584
|
+
`Case "${testCase.name}" has no value for evaluator input "${binding.graphInputId}" from dataset field "${binding.source.fieldId}".`
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
evaluatorInputs[binding.graphInputId] = clonePortableJson(value);
|
|
1588
|
+
} else if (binding.source.kind === "target-output") {
|
|
1589
|
+
if (!Object.hasOwn(targetOutputs, binding.source.outputId)) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`Target graph did not produce output "${binding.source.outputId}" required by evaluator input "${binding.graphInputId}".`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
const targetOutputValue = targetOutputs[binding.source.outputId];
|
|
1595
|
+
const evaluatorInput = evaluatorInputsById.get(binding.graphInputId);
|
|
1596
|
+
if (evaluatorInput && !isEvaluationValueCompatibleWithDataType(targetOutputValue, evaluatorInput.data.dataType)) {
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
`Target output "${binding.source.outputId}" is not compatible with evaluator input "${binding.graphInputId}" (${evaluatorInput.data.dataType}).`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
evaluatorInputs[binding.graphInputId] = clonePortableJson(targetOutputValue);
|
|
1602
|
+
} else if (binding.source.kind === "context") {
|
|
1603
|
+
evaluatorInputs[binding.graphInputId] = clonePortableJson(contextValues[binding.source.context]);
|
|
1604
|
+
} else {
|
|
1605
|
+
throw new Error(`Evaluator input "${binding.graphInputId}" uses an unsupported source.`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return evaluatorInputs;
|
|
1609
|
+
}
|
|
1610
|
+
function hashStableText(value) {
|
|
1611
|
+
let hash = 2166136261;
|
|
1612
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1613
|
+
hash ^= value.charCodeAt(index);
|
|
1614
|
+
hash = Math.imul(hash, 16777619);
|
|
1615
|
+
}
|
|
1616
|
+
return hash >>> 0;
|
|
1617
|
+
}
|
|
1618
|
+
function deriveSeed(seed, caseId, trialIndex) {
|
|
1619
|
+
if (seed == null) return void 0;
|
|
1620
|
+
let value = seed >>> 0;
|
|
1621
|
+
value = Math.imul(value ^ hashStableText(caseId), 73244475);
|
|
1622
|
+
value = Math.imul(value ^ trialIndex + 1, 73244475);
|
|
1623
|
+
return (value ^ value >>> 16) >>> 0;
|
|
1624
|
+
}
|
|
1625
|
+
function normalizeEvaluatorResult(value, evaluationMode) {
|
|
1626
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1627
|
+
throw new Error('Evaluator graph output "result" must be an object.');
|
|
1628
|
+
}
|
|
1629
|
+
if (evaluationMode === "pass-fail" && typeof value.passed !== "boolean") {
|
|
1630
|
+
throw new Error('Evaluator graph output "result.passed" must be a boolean.');
|
|
1631
|
+
}
|
|
1632
|
+
const score = value.score;
|
|
1633
|
+
if ((evaluationMode === "scoring" || score !== void 0) && (typeof score !== "number" || !Number.isFinite(score) || score < 0 || score > 100)) {
|
|
1634
|
+
throw new Error(
|
|
1635
|
+
'Evaluator graph output "result.score" must be a number from 0 to 100 (for example, return 85 for 85/100).'
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
if (value.message !== void 0 && typeof value.message !== "string") {
|
|
1639
|
+
throw new Error('Evaluator graph output "result.message" must be a string.');
|
|
1640
|
+
}
|
|
1641
|
+
if (value.metrics !== void 0 && (typeof value.metrics !== "object" || value.metrics === null || Array.isArray(value.metrics))) {
|
|
1642
|
+
throw new Error('Evaluator graph output "result.metrics" must be an object.');
|
|
1643
|
+
}
|
|
1644
|
+
const metrics = value.metrics;
|
|
1645
|
+
if (metrics && Object.values(metrics).some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
|
|
1646
|
+
throw new Error('Evaluator graph output "result.metrics" values must be finite numbers.');
|
|
1647
|
+
}
|
|
1648
|
+
if (value.evidence !== void 0) assertPortableJson(value.evidence, "evaluator result.evidence");
|
|
1649
|
+
return {
|
|
1650
|
+
id: "",
|
|
1651
|
+
kind: "graph",
|
|
1652
|
+
name: "",
|
|
1653
|
+
required: true,
|
|
1654
|
+
status: evaluationMode === "scoring" ? "scored" : value.passed ? "passed" : "failed",
|
|
1655
|
+
// Evaluator authors use the same 100-point scale shown by the UI. Runs
|
|
1656
|
+
// retain normalized values so historical aggregation and thresholds stay
|
|
1657
|
+
// backward compatible with existing evaluation records.
|
|
1658
|
+
...typeof score === "number" ? { score: score / 100 } : {},
|
|
1659
|
+
...typeof value.message === "string" ? { message: value.message } : {},
|
|
1660
|
+
...value.evidence !== void 0 ? { evidence: clonePortableJson(value.evidence) } : {},
|
|
1661
|
+
...metrics ? { metrics: clonePortableJson(metrics) } : {}
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
async function runWithTimeout(run, timeoutMs, outer) {
|
|
1665
|
+
throwIfAborted(outer);
|
|
1666
|
+
const controller = new AbortController();
|
|
1667
|
+
let rejectCanceled;
|
|
1668
|
+
const canceled = new Promise((_, reject) => {
|
|
1669
|
+
rejectCanceled = reject;
|
|
1670
|
+
});
|
|
1671
|
+
const abort = () => {
|
|
1672
|
+
const error = (outer == null ? void 0 : outer.reason) ?? new DOMException("Evaluation canceled.", "AbortError");
|
|
1673
|
+
controller.abort(error);
|
|
1674
|
+
rejectCanceled == null ? void 0 : rejectCanceled(error);
|
|
1675
|
+
};
|
|
1676
|
+
outer == null ? void 0 : outer.addEventListener("abort", abort, { once: true });
|
|
1677
|
+
if (outer == null ? void 0 : outer.aborted) {
|
|
1678
|
+
outer.removeEventListener("abort", abort);
|
|
1679
|
+
throw outer.reason ?? new DOMException("Evaluation canceled.", "AbortError");
|
|
1680
|
+
}
|
|
1681
|
+
let timeout;
|
|
1682
|
+
let timeoutPromise;
|
|
1683
|
+
if (timeoutMs != null && timeoutMs > 0) {
|
|
1684
|
+
timeoutPromise = new Promise((_, reject) => {
|
|
1685
|
+
timeout = setTimeout(() => {
|
|
1686
|
+
const error = new DOMException("Evaluation trial timed out.", "TimeoutError");
|
|
1687
|
+
controller.abort(error);
|
|
1688
|
+
reject(error);
|
|
1689
|
+
}, timeoutMs);
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
const execution = run(controller.signal);
|
|
1693
|
+
try {
|
|
1694
|
+
return await Promise.race([execution, canceled, ...timeoutPromise ? [timeoutPromise] : []]);
|
|
1695
|
+
} finally {
|
|
1696
|
+
if (timeout) clearTimeout(timeout);
|
|
1697
|
+
outer == null ? void 0 : outer.removeEventListener("abort", abort);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
async function runTrial(input) {
|
|
1701
|
+
var _a, _b, _c, _d;
|
|
1702
|
+
const { project, suite, dataset, purpose, testCase, caseIndex, trialIndex, runId, runGraph, signal } = input;
|
|
1703
|
+
const evaluationMode = getEvaluationSuiteMode(suite);
|
|
1704
|
+
const { inputs, expected } = resolveCaseInputs(suite, dataset, testCase, purpose);
|
|
1705
|
+
const seed = deriveSeed((_a = suite.configuration) == null ? void 0 : _a.seed, testCase.id, trialIndex);
|
|
1706
|
+
if (seed !== void 0 && ((_b = suite.configuration) == null ? void 0 : _b.seedGraphInputId)) inputs[suite.configuration.seedGraphInputId] = seed;
|
|
1707
|
+
const metadata = {
|
|
1708
|
+
evaluationRunId: runId,
|
|
1709
|
+
suiteId: suite.id,
|
|
1710
|
+
caseId: testCase.id,
|
|
1711
|
+
trialIndex,
|
|
1712
|
+
phase: "target"
|
|
1713
|
+
};
|
|
1714
|
+
let targetMetrics = createEmptyMetrics();
|
|
1715
|
+
let evaluatorMetrics = createEmptyMetrics();
|
|
1716
|
+
let outputs = {};
|
|
1717
|
+
let targetError;
|
|
1718
|
+
let recording;
|
|
1719
|
+
let targetProviderAttempts;
|
|
1720
|
+
const targetStartedAt = Date.now();
|
|
1721
|
+
try {
|
|
1722
|
+
throwIfAborted(signal);
|
|
1723
|
+
const target = await runWithTimeout(
|
|
1724
|
+
(attemptSignal) => runGraph({ project, graphId: suite.targetGraphId, inputs, signal: attemptSignal, metadata }),
|
|
1725
|
+
(_c = suite.configuration) == null ? void 0 : _c.timeoutMs,
|
|
1726
|
+
signal
|
|
1727
|
+
);
|
|
1728
|
+
targetMetrics = cloneExecutionMetrics(target.metrics, "target metrics");
|
|
1729
|
+
assertPortableJson(target.outputs, "target outputs");
|
|
1730
|
+
outputs = clonePortableJson(target.outputs);
|
|
1731
|
+
recording = target.recording === void 0 ? void 0 : structuredClone(target.recording);
|
|
1732
|
+
targetProviderAttempts = cloneOptionalPortableJson(target.providerAttempts, "target provider attempts");
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
if (error instanceof EvaluationGraphExecutionError) {
|
|
1735
|
+
const errorMetrics = tryCloneExecutionMetrics(error.metrics, "target error metrics");
|
|
1736
|
+
targetMetrics = errorMetrics ?? { ...targetMetrics, hasUnknownCost: true };
|
|
1737
|
+
recording = error.recording === void 0 ? void 0 : structuredClone(error.recording);
|
|
1738
|
+
targetProviderAttempts = tryCloneOptionalPortableJson(error.providerAttempts, "target error provider attempts");
|
|
1739
|
+
} else {
|
|
1740
|
+
targetMetrics = { ...targetMetrics, hasUnknownCost: true };
|
|
1741
|
+
}
|
|
1742
|
+
targetError = toErrorMessage(error);
|
|
1743
|
+
targetMetrics = { ...targetMetrics, durationMs: Math.max(targetMetrics.durationMs, Date.now() - targetStartedAt) };
|
|
1744
|
+
if (isAbort(error) || (signal == null ? void 0 : signal.aborted)) {
|
|
1745
|
+
const executionStatus2 = "canceled";
|
|
1746
|
+
const qualityStatus2 = "not-evaluated";
|
|
1747
|
+
return {
|
|
1748
|
+
id: makeId("trial"),
|
|
1749
|
+
caseId: testCase.id,
|
|
1750
|
+
caseName: testCase.name,
|
|
1751
|
+
caseIndex,
|
|
1752
|
+
trialIndex,
|
|
1753
|
+
executionStatus: executionStatus2,
|
|
1754
|
+
qualityStatus: qualityStatus2,
|
|
1755
|
+
qualityReason: { code: "canceled", message: "The evaluation trial was canceled." },
|
|
1756
|
+
inputs,
|
|
1757
|
+
expected,
|
|
1758
|
+
outputs,
|
|
1759
|
+
observations: [],
|
|
1760
|
+
targetMetrics,
|
|
1761
|
+
evaluatorMetrics,
|
|
1762
|
+
totalMetrics: { ...mergeMetrics(targetMetrics, evaluatorMetrics), hasUnknownCost: true },
|
|
1763
|
+
error: targetError,
|
|
1764
|
+
...seed === void 0 ? {} : { seed },
|
|
1765
|
+
...recording === void 0 ? {} : { recording },
|
|
1766
|
+
...targetProviderAttempts === void 0 ? {} : { targetProviderAttempts }
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
const observations = [];
|
|
1771
|
+
if (purpose === "evaluation" && targetError) {
|
|
1772
|
+
observations.push({
|
|
1773
|
+
id: "target-error",
|
|
1774
|
+
kind: "assertion",
|
|
1775
|
+
name: "Target graph",
|
|
1776
|
+
required: true,
|
|
1777
|
+
status: "error",
|
|
1778
|
+
message: targetError
|
|
1779
|
+
});
|
|
1780
|
+
} else if (purpose === "evaluation" && evaluationMode === "pass-fail") {
|
|
1781
|
+
for (const assertion of suite.assertions) {
|
|
1782
|
+
try {
|
|
1783
|
+
observations.push(evaluateAssertion(assertion, outputs, testCase));
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
observations.push({
|
|
1786
|
+
id: assertion.id,
|
|
1787
|
+
kind: "assertion",
|
|
1788
|
+
name: assertion.name,
|
|
1789
|
+
required: assertion.required !== false,
|
|
1790
|
+
status: "error",
|
|
1791
|
+
message: toErrorMessage(error)
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
for (const evaluator of purpose === "evaluation" ? suite.evaluators : []) {
|
|
1797
|
+
if (targetError && (evaluationMode === "scoring" || !evaluator.runOnTargetError)) {
|
|
1798
|
+
observations.push({
|
|
1799
|
+
id: evaluator.id,
|
|
1800
|
+
kind: "graph",
|
|
1801
|
+
name: evaluator.name,
|
|
1802
|
+
required: evaluator.required !== false,
|
|
1803
|
+
status: "skipped",
|
|
1804
|
+
message: "Skipped because the target graph failed."
|
|
1805
|
+
});
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
const start = Date.now();
|
|
1809
|
+
let completedAttemptMetrics;
|
|
1810
|
+
let completedAttemptRecording;
|
|
1811
|
+
let completedAttemptProviderAttempts;
|
|
1812
|
+
try {
|
|
1813
|
+
const result = await runWithTimeout(
|
|
1814
|
+
(attemptSignal) => runGraph({
|
|
1815
|
+
project,
|
|
1816
|
+
graphId: evaluator.graphId,
|
|
1817
|
+
inputs: createEvaluatorGraphInputs({
|
|
1818
|
+
evaluator,
|
|
1819
|
+
project,
|
|
1820
|
+
testCase,
|
|
1821
|
+
targetInputs: inputs,
|
|
1822
|
+
expected,
|
|
1823
|
+
targetOutputs: outputs,
|
|
1824
|
+
run: { targetError: targetError ?? null, caseIndex, trialIndex, seed: seed ?? null }
|
|
1825
|
+
}),
|
|
1826
|
+
signal: attemptSignal,
|
|
1827
|
+
metadata: { ...metadata, phase: "evaluator" }
|
|
1828
|
+
}),
|
|
1829
|
+
(_d = suite.configuration) == null ? void 0 : _d.timeoutMs,
|
|
1830
|
+
signal
|
|
1831
|
+
);
|
|
1832
|
+
completedAttemptMetrics = cloneExecutionMetrics(result.metrics, `evaluator ${evaluator.name} metrics`);
|
|
1833
|
+
completedAttemptRecording = result.recording === void 0 ? void 0 : structuredClone(result.recording);
|
|
1834
|
+
completedAttemptProviderAttempts = cloneOptionalPortableJson(
|
|
1835
|
+
result.providerAttempts,
|
|
1836
|
+
`evaluator ${evaluator.name} provider attempts`
|
|
1837
|
+
);
|
|
1838
|
+
evaluatorMetrics = mergeMetrics(evaluatorMetrics, completedAttemptMetrics);
|
|
1839
|
+
assertPortableJson(result.outputs, `evaluator ${evaluator.name} outputs`);
|
|
1840
|
+
const observation = normalizeEvaluatorResult(result.outputs.result ?? null, evaluationMode);
|
|
1841
|
+
observations.push({
|
|
1842
|
+
...observation,
|
|
1843
|
+
id: evaluator.id,
|
|
1844
|
+
name: evaluator.name,
|
|
1845
|
+
required: evaluator.required !== false,
|
|
1846
|
+
scoreWeight: evaluator.scoreWeight ?? 1,
|
|
1847
|
+
durationMs: completedAttemptMetrics.durationMs || Date.now() - start,
|
|
1848
|
+
...completedAttemptMetrics.costUsd === void 0 ? {} : { costUsd: completedAttemptMetrics.costUsd },
|
|
1849
|
+
...completedAttemptProviderAttempts === void 0 ? {} : { providerAttempts: completedAttemptProviderAttempts },
|
|
1850
|
+
...completedAttemptRecording === void 0 ? {} : { recording: completedAttemptRecording }
|
|
1851
|
+
});
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
const executionError = error instanceof EvaluationGraphExecutionError ? error : void 0;
|
|
1854
|
+
const executionErrorMetrics = tryCloneExecutionMetrics(
|
|
1855
|
+
executionError == null ? void 0 : executionError.metrics,
|
|
1856
|
+
`evaluator ${evaluator.name} error metrics`
|
|
1857
|
+
);
|
|
1858
|
+
const failureMetrics = executionErrorMetrics ?? completedAttemptMetrics;
|
|
1859
|
+
const failureProviderAttempts = tryCloneOptionalPortableJson(
|
|
1860
|
+
executionError == null ? void 0 : executionError.providerAttempts,
|
|
1861
|
+
`evaluator ${evaluator.name} error provider attempts`
|
|
1862
|
+
) ?? completedAttemptProviderAttempts;
|
|
1863
|
+
const failureRecording = (executionError == null ? void 0 : executionError.recording) === void 0 ? completedAttemptRecording : structuredClone(executionError.recording);
|
|
1864
|
+
if (executionErrorMetrics) {
|
|
1865
|
+
evaluatorMetrics = mergeMetrics(evaluatorMetrics, executionErrorMetrics);
|
|
1866
|
+
} else if (!completedAttemptMetrics) {
|
|
1867
|
+
evaluatorMetrics = mergeMetrics(evaluatorMetrics, {
|
|
1868
|
+
durationMs: Date.now() - start,
|
|
1869
|
+
hasUnknownCost: true
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
if (isAbort(error) || (signal == null ? void 0 : signal.aborted)) {
|
|
1873
|
+
const executionStatus2 = "canceled";
|
|
1874
|
+
const qualityStatus2 = "not-evaluated";
|
|
1875
|
+
return {
|
|
1876
|
+
id: makeId("trial"),
|
|
1877
|
+
caseId: testCase.id,
|
|
1878
|
+
caseName: testCase.name,
|
|
1879
|
+
caseIndex,
|
|
1880
|
+
trialIndex,
|
|
1881
|
+
executionStatus: executionStatus2,
|
|
1882
|
+
qualityStatus: qualityStatus2,
|
|
1883
|
+
qualityReason: { code: "canceled", message: "The evaluation trial was canceled." },
|
|
1884
|
+
inputs,
|
|
1885
|
+
expected,
|
|
1886
|
+
outputs,
|
|
1887
|
+
observations,
|
|
1888
|
+
targetMetrics,
|
|
1889
|
+
evaluatorMetrics,
|
|
1890
|
+
totalMetrics: { ...mergeMetrics(targetMetrics, evaluatorMetrics), hasUnknownCost: true },
|
|
1891
|
+
error: toErrorMessage(error),
|
|
1892
|
+
...seed === void 0 ? {} : { seed },
|
|
1893
|
+
...recording === void 0 ? {} : { recording },
|
|
1894
|
+
...targetProviderAttempts === void 0 ? {} : { targetProviderAttempts }
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
observations.push({
|
|
1898
|
+
id: evaluator.id,
|
|
1899
|
+
kind: "graph",
|
|
1900
|
+
name: evaluator.name,
|
|
1901
|
+
required: evaluator.required !== false,
|
|
1902
|
+
status: "error",
|
|
1903
|
+
message: toErrorMessage(error),
|
|
1904
|
+
durationMs: (failureMetrics == null ? void 0 : failureMetrics.durationMs) ?? Date.now() - start,
|
|
1905
|
+
...(failureMetrics == null ? void 0 : failureMetrics.costUsd) === void 0 ? {} : { costUsd: failureMetrics.costUsd },
|
|
1906
|
+
...failureProviderAttempts === void 0 ? {} : { providerAttempts: failureProviderAttempts },
|
|
1907
|
+
...failureRecording === void 0 ? {} : { recording: failureRecording }
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const required = observations.filter((entry) => entry.required);
|
|
1912
|
+
const executionStatus = targetError === void 0 ? "completed" : "error";
|
|
1913
|
+
let qualityStatus;
|
|
1914
|
+
let qualityReason;
|
|
1915
|
+
if (purpose === "execution-benchmark") {
|
|
1916
|
+
qualityStatus = "not-evaluated";
|
|
1917
|
+
qualityReason = {
|
|
1918
|
+
code: "benchmark",
|
|
1919
|
+
message: "This trial measured execution without evaluating output quality."
|
|
1920
|
+
};
|
|
1921
|
+
} else if (evaluationMode === "scoring") {
|
|
1922
|
+
const scoringObservations = observations.filter((entry) => entry.kind === "graph");
|
|
1923
|
+
const hasCompleteScore = targetError === void 0 && scoringObservations.length === suite.evaluators.length && scoringObservations.every((entry) => entry.status === "scored" && entry.score !== void 0);
|
|
1924
|
+
if (hasCompleteScore) {
|
|
1925
|
+
qualityStatus = "scored";
|
|
1926
|
+
qualityReason = { code: "scores-complete", message: "All evaluator graphs returned a score for this trial." };
|
|
1927
|
+
} else {
|
|
1928
|
+
qualityStatus = "unable-to-evaluate";
|
|
1929
|
+
qualityReason = {
|
|
1930
|
+
code: "scores-incomplete",
|
|
1931
|
+
message: targetError === void 0 ? "One or more evaluator graphs did not return a usable score for this trial." : "The target graph failed, so this trial could not contribute a score."
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
} else if (targetError !== void 0) {
|
|
1935
|
+
qualityStatus = "failed";
|
|
1936
|
+
qualityReason = {
|
|
1937
|
+
code: "target-error",
|
|
1938
|
+
message: "The target graph failed before it could produce a valid result."
|
|
1939
|
+
};
|
|
1940
|
+
} else if (required.some((entry) => entry.status === "failed")) {
|
|
1941
|
+
qualityStatus = "failed";
|
|
1942
|
+
qualityReason = { code: "checks-failed", message: "One or more required quality checks failed." };
|
|
1943
|
+
} else if (required.some((entry) => entry.status === "error")) {
|
|
1944
|
+
qualityStatus = "unable-to-evaluate";
|
|
1945
|
+
qualityReason = {
|
|
1946
|
+
code: "required-check-error",
|
|
1947
|
+
message: "A required quality check could not be evaluated."
|
|
1948
|
+
};
|
|
1949
|
+
} else if (required.some((entry) => entry.status === "passed")) {
|
|
1950
|
+
qualityStatus = "passed";
|
|
1951
|
+
qualityReason = { code: "checks-passed", message: "All required per-trial quality checks passed." };
|
|
1952
|
+
} else {
|
|
1953
|
+
qualityStatus = "not-evaluated";
|
|
1954
|
+
qualityReason = {
|
|
1955
|
+
code: "no-trial-quality-checks",
|
|
1956
|
+
message: "No required per-trial quality check evaluated this result."
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
const totalMetrics = mergeMetrics(targetMetrics, evaluatorMetrics);
|
|
1960
|
+
return {
|
|
1961
|
+
id: makeId("trial"),
|
|
1962
|
+
caseId: testCase.id,
|
|
1963
|
+
caseName: testCase.name,
|
|
1964
|
+
caseIndex,
|
|
1965
|
+
trialIndex,
|
|
1966
|
+
executionStatus,
|
|
1967
|
+
qualityStatus,
|
|
1968
|
+
qualityReason,
|
|
1969
|
+
inputs,
|
|
1970
|
+
expected,
|
|
1971
|
+
outputs,
|
|
1972
|
+
observations,
|
|
1973
|
+
targetMetrics,
|
|
1974
|
+
evaluatorMetrics,
|
|
1975
|
+
totalMetrics,
|
|
1976
|
+
...targetError === void 0 ? {} : { error: targetError },
|
|
1977
|
+
...seed === void 0 ? {} : { seed },
|
|
1978
|
+
...recording === void 0 ? {} : { recording },
|
|
1979
|
+
...targetProviderAttempts === void 0 ? {} : { targetProviderAttempts }
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
function percentile(values, percentage) {
|
|
1983
|
+
if (values.length === 0) return 0;
|
|
1984
|
+
const ordered = values.slice().sort((left, right) => left - right);
|
|
1985
|
+
return ordered[Math.min(ordered.length - 1, Math.ceil(percentage / 100 * ordered.length) - 1)];
|
|
1986
|
+
}
|
|
1987
|
+
function meanObservationScore(observations) {
|
|
1988
|
+
let weightedScore = 0;
|
|
1989
|
+
let totalWeight = 0;
|
|
1990
|
+
for (const observation of observations) {
|
|
1991
|
+
if (observation.score === void 0) continue;
|
|
1992
|
+
const weight = observation.scoreWeight ?? 1;
|
|
1993
|
+
weightedScore += observation.score * weight;
|
|
1994
|
+
totalWeight += weight;
|
|
1995
|
+
}
|
|
1996
|
+
return totalWeight === 0 ? void 0 : weightedScore / totalWeight;
|
|
1997
|
+
}
|
|
1998
|
+
function meanObservationMetrics(observations) {
|
|
1999
|
+
const totals = {};
|
|
2000
|
+
const counts = {};
|
|
2001
|
+
for (const observation of observations) {
|
|
2002
|
+
for (const [name, value] of Object.entries(observation.metrics ?? {})) {
|
|
2003
|
+
totals[name] = (totals[name] ?? 0) + value;
|
|
2004
|
+
counts[name] = (counts[name] ?? 0) + 1;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return Object.fromEntries(Object.entries(totals).map(([name, total]) => [name, total / counts[name]]));
|
|
2008
|
+
}
|
|
2009
|
+
function aggregate(trials, evaluationMode = "pass-fail") {
|
|
2010
|
+
const incurred = trials;
|
|
2011
|
+
const settled = trials.filter((trial) => trial.executionStatus !== "canceled");
|
|
2012
|
+
const completed = trials.filter((trial) => trial.executionStatus === "completed");
|
|
2013
|
+
const evaluated = completed.filter((trial) => trial.qualityStatus === "passed" || trial.qualityStatus === "failed");
|
|
2014
|
+
const passed = evaluated.filter((trial) => trial.qualityStatus === "passed");
|
|
2015
|
+
const failed = evaluated.filter((trial) => trial.qualityStatus === "failed");
|
|
2016
|
+
const notEvaluated = completed.filter((trial) => trial.qualityStatus === "not-evaluated");
|
|
2017
|
+
const unable = completed.filter((trial) => trial.qualityStatus === "unable-to-evaluate");
|
|
2018
|
+
const errors = trials.filter((trial) => trial.executionStatus === "error");
|
|
2019
|
+
const canceled = trials.filter((trial) => trial.executionStatus === "canceled");
|
|
2020
|
+
const settledObservations = settled.flatMap((trial) => trial.observations);
|
|
2021
|
+
const meanScore = meanObservationScore(settledObservations);
|
|
2022
|
+
const accountingComplete = !incurred.some((trial) => trial.totalMetrics.hasUnknownCost);
|
|
2023
|
+
const totalCostUsd = accountingComplete ? incurred.reduce((sum, trial) => sum + (trial.totalMetrics.costUsd ?? 0), 0) : void 0;
|
|
2024
|
+
const meanMetrics = meanObservationMetrics(settledObservations);
|
|
2025
|
+
const byCase = /* @__PURE__ */ new Map();
|
|
2026
|
+
for (const trial of trials) byCase.set(trial.caseId, [...byCase.get(trial.caseId) ?? [], trial]);
|
|
2027
|
+
const cases = Array.from(byCase.values()).map((caseTrials) => {
|
|
2028
|
+
const caseSettled = caseTrials.filter((trial) => trial.executionStatus !== "canceled");
|
|
2029
|
+
const caseCompleted = caseSettled.filter((trial) => trial.executionStatus === "completed");
|
|
2030
|
+
const caseEvaluated = caseCompleted.filter(
|
|
2031
|
+
(trial) => trial.qualityStatus === "passed" || trial.qualityStatus === "failed"
|
|
2032
|
+
);
|
|
2033
|
+
const casePassed = caseEvaluated.filter((trial) => trial.qualityStatus === "passed");
|
|
2034
|
+
const caseFailed = caseEvaluated.filter((trial) => trial.qualityStatus === "failed");
|
|
2035
|
+
const caseScored = caseCompleted.filter((trial) => trial.qualityStatus === "scored");
|
|
2036
|
+
const caseObservations = caseSettled.flatMap((trial) => trial.observations);
|
|
2037
|
+
const caseTrialScores = caseScored.map((trial) => meanObservationScore(trial.observations)).filter((score) => score !== void 0);
|
|
2038
|
+
const caseMeanScore = evaluationMode === "scoring" ? caseTrialScores.length === 0 ? void 0 : caseTrialScores.reduce((sum, score) => sum + score, 0) / caseTrialScores.length : meanObservationScore(caseObservations);
|
|
2039
|
+
return {
|
|
2040
|
+
caseId: caseTrials[0].caseId,
|
|
2041
|
+
caseName: caseTrials[0].caseName,
|
|
2042
|
+
...caseEvaluated.length === 0 ? {} : { passRate: casePassed.length / caseEvaluated.length },
|
|
2043
|
+
evaluatedTrialCount: caseEvaluated.length,
|
|
2044
|
+
passedTrialCount: casePassed.length,
|
|
2045
|
+
failedTrialCount: caseFailed.length,
|
|
2046
|
+
notEvaluatedTrialCount: caseCompleted.filter((trial) => trial.qualityStatus === "not-evaluated").length,
|
|
2047
|
+
unableToEvaluateTrialCount: caseCompleted.filter((trial) => trial.qualityStatus === "unable-to-evaluate").length,
|
|
2048
|
+
erroredTrialCount: caseTrials.filter((trial) => trial.executionStatus === "error").length,
|
|
2049
|
+
canceledTrialCount: caseTrials.filter((trial) => trial.executionStatus === "canceled").length,
|
|
2050
|
+
...evaluationMode === "scoring" ? {
|
|
2051
|
+
scoredTrialCount: caseScored.length,
|
|
2052
|
+
missingScoreTrialCount: caseTrials.length - caseScored.length
|
|
2053
|
+
} : {},
|
|
2054
|
+
...caseMeanScore === void 0 ? {} : { meanScore: caseMeanScore },
|
|
2055
|
+
metrics: meanObservationMetrics(caseObservations)
|
|
2056
|
+
};
|
|
2057
|
+
});
|
|
2058
|
+
const scoredTrialCount = cases.reduce((sum, testCase) => sum + (testCase.scoredTrialCount ?? 0), 0);
|
|
2059
|
+
const scoringCaseMeans = cases.map((testCase) => testCase.meanScore).filter((score) => score !== void 0);
|
|
2060
|
+
const scoringMeanScore = scoringCaseMeans.length === 0 ? void 0 : scoringCaseMeans.reduce((sum, score) => sum + score, 0) / scoringCaseMeans.length;
|
|
2061
|
+
const aggregateValue = {
|
|
2062
|
+
trialCount: trials.length,
|
|
2063
|
+
evaluatedTrialCount: evaluated.length,
|
|
2064
|
+
notEvaluatedTrialCount: notEvaluated.length,
|
|
2065
|
+
unableToEvaluateTrialCount: unable.length,
|
|
2066
|
+
passedTrialCount: passed.length,
|
|
2067
|
+
failedTrialCount: failed.length,
|
|
2068
|
+
erroredTrialCount: errors.length,
|
|
2069
|
+
canceledTrialCount: canceled.length,
|
|
2070
|
+
passRate: evaluated.length === 0 ? 0 : passed.length / evaluated.length,
|
|
2071
|
+
...evaluationMode === "scoring" ? {
|
|
2072
|
+
scoredTrialCount,
|
|
2073
|
+
missingScoreTrialCount: trials.length - scoredTrialCount,
|
|
2074
|
+
...scoringMeanScore === void 0 ? {} : { meanScore: scoringMeanScore }
|
|
2075
|
+
} : meanScore === void 0 ? {} : { meanScore },
|
|
2076
|
+
averageLatencyMs: incurred.length === 0 ? 0 : incurred.reduce((sum, trial) => sum + trial.totalMetrics.durationMs, 0) / incurred.length,
|
|
2077
|
+
p95LatencyMs: percentile(
|
|
2078
|
+
incurred.map((trial) => trial.totalMetrics.durationMs),
|
|
2079
|
+
95
|
|
2080
|
+
),
|
|
2081
|
+
...incurred.length === 0 || totalCostUsd === void 0 ? {} : { totalCostUsd, averageCostUsd: totalCostUsd / incurred.length },
|
|
2082
|
+
targetErrorRate: settled.length === 0 ? 0 : errors.length / settled.length,
|
|
2083
|
+
evaluatorErrorRate: settled.length === 0 ? 0 : settled.filter(
|
|
2084
|
+
(trial) => trial.observations.some((observation) => observation.kind === "graph" && observation.status === "error")
|
|
2085
|
+
).length / settled.length,
|
|
2086
|
+
toolFailureRate: settled.reduce((sum, trial) => sum + (trial.totalMetrics.toolCallCount ?? 0), 0) === 0 ? 0 : settled.reduce((sum, trial) => sum + (trial.totalMetrics.toolFailureCount ?? 0), 0) / settled.reduce((sum, trial) => sum + (trial.totalMetrics.toolCallCount ?? 0), 0),
|
|
2087
|
+
metrics: meanMetrics
|
|
2088
|
+
};
|
|
2089
|
+
return {
|
|
2090
|
+
aggregate: aggregateValue,
|
|
2091
|
+
cases
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
function thresholdValue(aggregateValue, metric) {
|
|
2095
|
+
switch (metric) {
|
|
2096
|
+
case "pass-rate":
|
|
2097
|
+
return aggregateValue.evaluatedTrialCount === 0 ? void 0 : aggregateValue.passRate;
|
|
2098
|
+
case "mean-score":
|
|
2099
|
+
return aggregateValue.meanScore;
|
|
2100
|
+
case "target-error-rate":
|
|
2101
|
+
return aggregateValue.targetErrorRate;
|
|
2102
|
+
case "evaluator-error-rate":
|
|
2103
|
+
return aggregateValue.evaluatorErrorRate;
|
|
2104
|
+
case "tool-failure-rate":
|
|
2105
|
+
return aggregateValue.toolFailureRate;
|
|
2106
|
+
case "average-cost":
|
|
2107
|
+
return aggregateValue.averageCostUsd;
|
|
2108
|
+
case "total-cost":
|
|
2109
|
+
return aggregateValue.totalCostUsd;
|
|
2110
|
+
case "average-latency-ms":
|
|
2111
|
+
return aggregateValue.averageLatencyMs;
|
|
2112
|
+
case "p95-latency-ms":
|
|
2113
|
+
return aggregateValue.p95LatencyMs;
|
|
2114
|
+
default:
|
|
2115
|
+
return metric.startsWith("custom:") ? aggregateValue.metrics[metric.slice("custom:".length)] : void 0;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function isHigherBetterMetric(metric) {
|
|
2119
|
+
return metric === "pass-rate" || metric === "mean-score" || metric.startsWith("custom:");
|
|
2120
|
+
}
|
|
2121
|
+
function baselineCompatible(current, baseline) {
|
|
2122
|
+
return current.executionMode === baseline.provenance.executionMode && current.suiteFingerprint === baseline.provenance.suiteFingerprint && current.datasetFingerprint === baseline.provenance.datasetFingerprint && current.targetFingerprint === baseline.provenance.targetFingerprint && canonicalStringify(current.evaluatorFingerprints) === canonicalStringify(baseline.provenance.evaluatorFingerprints);
|
|
2123
|
+
}
|
|
2124
|
+
function executionNodeConfiguration(node) {
|
|
2125
|
+
return {
|
|
2126
|
+
id: node.id,
|
|
2127
|
+
type: node.type,
|
|
2128
|
+
title: node.title,
|
|
2129
|
+
data: node.data,
|
|
2130
|
+
variants: node.variants,
|
|
2131
|
+
isSplitRun: node.isSplitRun,
|
|
2132
|
+
isSplitSequential: node.isSplitSequential,
|
|
2133
|
+
splitRunMax: node.splitRunMax,
|
|
2134
|
+
splitRunConcurrency: node.splitRunConcurrency,
|
|
2135
|
+
disabled: node.disabled,
|
|
2136
|
+
isConditional: node.isConditional
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
function executionConnectionConfiguration(connection) {
|
|
2140
|
+
return {
|
|
2141
|
+
outputNodeId: connection.outputNodeId,
|
|
2142
|
+
inputNodeId: connection.inputNodeId,
|
|
2143
|
+
outputId: connection.outputId,
|
|
2144
|
+
inputId: connection.inputId
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
function executionGraphConfiguration(graph) {
|
|
2148
|
+
var _a, _b, _c;
|
|
2149
|
+
return {
|
|
2150
|
+
metadata: {
|
|
2151
|
+
id: (_a = graph.metadata) == null ? void 0 : _a.id,
|
|
2152
|
+
name: (_b = graph.metadata) == null ? void 0 : _b.name,
|
|
2153
|
+
attachedData: (_c = graph.metadata) == null ? void 0 : _c.attachedData
|
|
2154
|
+
},
|
|
2155
|
+
nodes: graph.nodes.map(executionNodeConfiguration),
|
|
2156
|
+
connections: graph.connections.map(executionConnectionConfiguration)
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
function executionProjectGraphs(project) {
|
|
2160
|
+
return Object.fromEntries(
|
|
2161
|
+
Object.entries(project.graphs).map(([graphId, graph]) => [graphId, executionGraphConfiguration(graph)])
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
function executionProjectConfiguration(project) {
|
|
2165
|
+
return {
|
|
2166
|
+
...project.plugins === void 0 ? {} : { plugins: project.plugins },
|
|
2167
|
+
...project.nodePrefabs === void 0 ? {} : {
|
|
2168
|
+
nodePrefabs: Object.fromEntries(
|
|
2169
|
+
Object.entries(project.nodePrefabs).map(([prefabId, prefab]) => [
|
|
2170
|
+
prefabId,
|
|
2171
|
+
{ id: prefab.id, sourceNode: executionNodeConfiguration(prefab.sourceNode) }
|
|
2172
|
+
])
|
|
2173
|
+
)
|
|
2174
|
+
},
|
|
2175
|
+
...project.data === void 0 ? {} : { data: project.data },
|
|
2176
|
+
...project.references === void 0 ? {} : { references: project.references },
|
|
2177
|
+
...project.metadata.mcpServer === void 0 ? {} : { mcpServer: project.metadata.mcpServer },
|
|
2178
|
+
...project.metadata.knowledgeStores === void 0 ? {} : { knowledgeStores: project.metadata.knowledgeStores }
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
function canonicalizeExecutionFingerprintValue(value, path = "$", stack = /* @__PURE__ */ new Set()) {
|
|
2182
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2183
|
+
if (typeof value === "number") {
|
|
2184
|
+
if (!Number.isFinite(value)) throw new Error(`${path} must not contain a non-finite number.`);
|
|
2185
|
+
return value;
|
|
2186
|
+
}
|
|
2187
|
+
if (value === void 0) return null;
|
|
2188
|
+
if (typeof value !== "object") throw new Error(`${path} must be JSON-compatible to fingerprint an evaluation.`);
|
|
2189
|
+
if (stack.has(value)) throw new Error(`${path} must not contain a cycle.`);
|
|
2190
|
+
if (Array.isArray(value)) {
|
|
2191
|
+
stack.add(value);
|
|
2192
|
+
const normalized2 = Array.from(
|
|
2193
|
+
value,
|
|
2194
|
+
(entry, index) => canonicalizeExecutionFingerprintValue(entry, `${path}[${index}]`, stack)
|
|
2195
|
+
);
|
|
2196
|
+
stack.delete(value);
|
|
2197
|
+
return normalized2;
|
|
2198
|
+
}
|
|
2199
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
2200
|
+
throw new Error(`${path} must be a plain object to fingerprint an evaluation.`);
|
|
2201
|
+
}
|
|
2202
|
+
stack.add(value);
|
|
2203
|
+
const normalized = Object.fromEntries(
|
|
2204
|
+
Object.entries(value).flatMap(
|
|
2205
|
+
([key, entry]) => entry === void 0 ? [] : [[key, canonicalizeExecutionFingerprintValue(entry, `${path}.${key}`, stack)]]
|
|
2206
|
+
)
|
|
2207
|
+
);
|
|
2208
|
+
stack.delete(value);
|
|
2209
|
+
return normalized;
|
|
2210
|
+
}
|
|
2211
|
+
function fingerprintExecutionProject(value) {
|
|
2212
|
+
return fingerprint(canonicalizeExecutionFingerprintValue(value));
|
|
2213
|
+
}
|
|
2214
|
+
function fingerprintGraphDependency(project, graphId) {
|
|
2215
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2216
|
+
const graphEntries = Object.entries(project.graphs);
|
|
2217
|
+
const asKnownGraphId = (value) => {
|
|
2218
|
+
return typeof value === "string" && project.graphs[value] ? value : void 0;
|
|
2219
|
+
};
|
|
2220
|
+
const getStaticChildren = (graph) => {
|
|
2221
|
+
const children = /* @__PURE__ */ new Set();
|
|
2222
|
+
const autoDelegatedToolNames = [];
|
|
2223
|
+
const add = (value) => {
|
|
2224
|
+
const id = asKnownGraphId(value);
|
|
2225
|
+
if (id) children.add(id);
|
|
2226
|
+
};
|
|
2227
|
+
for (const node of graph.nodes) {
|
|
2228
|
+
const data = node.data;
|
|
2229
|
+
switch (node.type) {
|
|
2230
|
+
case "subGraph":
|
|
2231
|
+
case "loopUntil":
|
|
2232
|
+
case "cron":
|
|
2233
|
+
add(node.type === "subGraph" ? data.graphId : data.targetGraph);
|
|
2234
|
+
break;
|
|
2235
|
+
case "delegateFunctionCall": {
|
|
2236
|
+
add(data.unknownHandler);
|
|
2237
|
+
if (data.autoDelegate === true) {
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
if (Array.isArray(data.handlers)) {
|
|
2241
|
+
for (const handler of data.handlers) {
|
|
2242
|
+
if (handler && typeof handler === "object" && "value" in handler) {
|
|
2243
|
+
add(handler.value);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
case "gptFunction":
|
|
2250
|
+
if (data.useNameInput !== true && typeof data.name === "string" && data.name) {
|
|
2251
|
+
autoDelegatedToolNames.push(data.name);
|
|
2252
|
+
}
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
if (graph.nodes.some(
|
|
2257
|
+
(node) => node.type === "delegateFunctionCall" && node.data.autoDelegate === true
|
|
2258
|
+
)) {
|
|
2259
|
+
for (const toolName of autoDelegatedToolNames) {
|
|
2260
|
+
const resolved = (0, import_rivet2_core.findAutoDelegateGraphCandidate)(
|
|
2261
|
+
graphEntries,
|
|
2262
|
+
toolName,
|
|
2263
|
+
([, candidate]) => {
|
|
2264
|
+
var _a;
|
|
2265
|
+
return (_a = candidate.metadata) == null ? void 0 : _a.name;
|
|
2266
|
+
}
|
|
2267
|
+
);
|
|
2268
|
+
if (resolved) children.add(resolved[0]);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return [...children];
|
|
2272
|
+
};
|
|
2273
|
+
const collect = (id) => {
|
|
2274
|
+
if (visited.has(id)) return { graphId: id, cycle: true };
|
|
2275
|
+
visited.add(id);
|
|
2276
|
+
const graph = project.graphs[id];
|
|
2277
|
+
if (!graph) return { graphId: id, missing: true };
|
|
2278
|
+
const dependencies = getStaticChildren(graph).map(collect);
|
|
2279
|
+
return { graph: executionGraphConfiguration(graph), dependencies };
|
|
2280
|
+
};
|
|
2281
|
+
return fingerprintExecutionProject({
|
|
2282
|
+
graphDependency: collect(graphId),
|
|
2283
|
+
projectExecutionConfiguration: executionProjectConfiguration(project)
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
function unavailableThresholdResult(threshold, message) {
|
|
2287
|
+
return {
|
|
2288
|
+
id: threshold.id,
|
|
2289
|
+
metric: threshold.metric,
|
|
2290
|
+
operator: threshold.operator,
|
|
2291
|
+
status: "unavailable",
|
|
2292
|
+
expectedValue: threshold.value,
|
|
2293
|
+
message
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
function evaluateThresholdResults(aggregateValue, thresholds = [], baseline) {
|
|
2297
|
+
return thresholds.map((threshold) => {
|
|
2298
|
+
const actual = thresholdValue(aggregateValue, threshold.metric);
|
|
2299
|
+
if (actual === void 0 || !Number.isFinite(actual)) {
|
|
2300
|
+
const message = threshold.metric === "average-cost" || threshold.metric === "total-cost" ? "Some provider pricing was unavailable, so this cost requirement could not be evaluated." : threshold.metric === "pass-rate" ? "No completed trial produced an evaluated pass or fail result." : `The metric "${threshold.metric}" was unavailable.`;
|
|
2301
|
+
return unavailableThresholdResult(threshold, message);
|
|
2302
|
+
}
|
|
2303
|
+
if (threshold.operator === "max-regression") {
|
|
2304
|
+
const isCostMetric = threshold.metric === "average-cost" || threshold.metric === "total-cost";
|
|
2305
|
+
if (isCostMetric && ((baseline == null ? void 0 : baseline.accountingStatus) === "partial" || (baseline == null ? void 0 : baseline.provenance.accountingComplete) === false)) {
|
|
2306
|
+
return unavailableThresholdResult(
|
|
2307
|
+
threshold,
|
|
2308
|
+
"The baseline has partial accounting because some provider pricing was unavailable, so this cost regression could not be evaluated."
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
const previous = baseline ? thresholdValue(baseline.aggregate, threshold.metric) : void 0;
|
|
2312
|
+
if (previous === void 0 || !Number.isFinite(previous)) {
|
|
2313
|
+
return unavailableThresholdResult(
|
|
2314
|
+
threshold,
|
|
2315
|
+
baseline ? `The baseline does not contain a comparable "${threshold.metric}" metric.` : "A compatible baseline is required for this regression requirement."
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
const delta = isHigherBetterMetric(threshold.metric) ? previous - actual : actual - previous;
|
|
2319
|
+
const zeroBaselineRegression = previous === 0 && delta > 0;
|
|
2320
|
+
const regression = previous === 0 ? 0 : delta / Math.abs(previous);
|
|
2321
|
+
const failed = zeroBaselineRegression || regression > threshold.value;
|
|
2322
|
+
return {
|
|
2323
|
+
id: threshold.id,
|
|
2324
|
+
metric: threshold.metric,
|
|
2325
|
+
operator: threshold.operator,
|
|
2326
|
+
status: failed ? "failed" : "passed",
|
|
2327
|
+
expectedValue: threshold.value,
|
|
2328
|
+
actualValue: actual,
|
|
2329
|
+
baselineValue: previous,
|
|
2330
|
+
...zeroBaselineRegression ? {} : { regression },
|
|
2331
|
+
message: failed ? `Regression exceeded the allowed ${formatThresholdResultValue(
|
|
2332
|
+
threshold.metric,
|
|
2333
|
+
threshold.operator,
|
|
2334
|
+
threshold.value
|
|
2335
|
+
)}.` : `Regression stayed within the allowed ${formatThresholdResultValue(
|
|
2336
|
+
threshold.metric,
|
|
2337
|
+
threshold.operator,
|
|
2338
|
+
threshold.value
|
|
2339
|
+
)}.`
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
const passed = threshold.operator === "at-least" ? actual >= threshold.value : actual <= threshold.value;
|
|
2343
|
+
return {
|
|
2344
|
+
id: threshold.id,
|
|
2345
|
+
metric: threshold.metric,
|
|
2346
|
+
operator: threshold.operator,
|
|
2347
|
+
status: passed ? "passed" : "failed",
|
|
2348
|
+
expectedValue: threshold.value,
|
|
2349
|
+
actualValue: actual,
|
|
2350
|
+
message: passed ? `Actual value ${formatThresholdResultValue(threshold.metric, threshold.operator, actual)} satisfied ${threshold.operator} ${formatThresholdResultValue(threshold.metric, threshold.operator, threshold.value)}.` : `Actual value ${formatThresholdResultValue(threshold.metric, threshold.operator, actual)} did not satisfy ${threshold.operator} ${formatThresholdResultValue(threshold.metric, threshold.operator, threshold.value)}.`
|
|
2351
|
+
};
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
function createProvenance(project, suite, dataset, options) {
|
|
2355
|
+
const purpose = options.purpose ?? "evaluation";
|
|
2356
|
+
const evaluationMode = getEvaluationSuiteMode(suite);
|
|
2357
|
+
const configuration = suite.configuration;
|
|
2358
|
+
const materialEvaluators = suite.evaluators.map((evaluator) => {
|
|
2359
|
+
var _a;
|
|
2360
|
+
return {
|
|
2361
|
+
graphId: evaluator.graphId,
|
|
2362
|
+
inputBindings: ((_a = evaluator.inputBindings) == null ? void 0 : _a.slice().sort((left, right) => left.graphInputId.localeCompare(right.graphInputId))) ?? null,
|
|
2363
|
+
// Normalize defaults so editing a control between its implicit and
|
|
2364
|
+
// explicit default does not invalidate a baseline.
|
|
2365
|
+
scoreWeight: evaluator.scoreWeight ?? 1,
|
|
2366
|
+
...evaluationMode === "scoring" ? {} : { required: evaluator.required !== false, runOnTargetError: evaluator.runOnTargetError === true }
|
|
2367
|
+
};
|
|
2368
|
+
});
|
|
2369
|
+
const materialSuiteDefinition = {
|
|
2370
|
+
purpose,
|
|
2371
|
+
evaluationMode,
|
|
2372
|
+
targetGraphId: suite.targetGraphId,
|
|
2373
|
+
datasetId: suite.datasetId,
|
|
2374
|
+
inputBindings: suite.inputBindings.slice().sort((left, right) => left.graphInputId.localeCompare(right.graphInputId)),
|
|
2375
|
+
configuration: {
|
|
2376
|
+
trialCount: (configuration == null ? void 0 : configuration.trialCount) ?? 1,
|
|
2377
|
+
concurrency: (configuration == null ? void 0 : configuration.concurrency) ?? DEFAULT_CONCURRENCY,
|
|
2378
|
+
timeoutMs: (configuration == null ? void 0 : configuration.timeoutMs) ?? null,
|
|
2379
|
+
seed: (configuration == null ? void 0 : configuration.seed) ?? null,
|
|
2380
|
+
seedGraphInputId: (configuration == null ? void 0 : configuration.seedGraphInputId) ?? null
|
|
2381
|
+
},
|
|
2382
|
+
...purpose !== "evaluation" ? {} : evaluationMode === "scoring" ? { evaluators: materialEvaluators } : {
|
|
2383
|
+
assertions: suite.assertions,
|
|
2384
|
+
evaluators: materialEvaluators,
|
|
2385
|
+
thresholds: suite.thresholds ?? []
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
return {
|
|
2389
|
+
projectFingerprint: options.projectFingerprint ?? fingerprintExecutionProject({
|
|
2390
|
+
projectId: project.metadata.id,
|
|
2391
|
+
graphs: executionProjectGraphs(project),
|
|
2392
|
+
executionConfiguration: executionProjectConfiguration(project)
|
|
2393
|
+
}),
|
|
2394
|
+
// Names, descriptions, tags, and recording-retention choices do not
|
|
2395
|
+
// change an execution result. Quality-only definitions likewise cannot
|
|
2396
|
+
// stale an execution-benchmark comparison because they never ran. Pass/
|
|
2397
|
+
// fail assertions and thresholds are equally dormant in scoring mode.
|
|
2398
|
+
suiteFingerprint: fingerprint(materialSuiteDefinition),
|
|
2399
|
+
datasetFingerprint: fingerprintEvaluationDataset(dataset),
|
|
2400
|
+
targetFingerprint: fingerprintGraphDependency(project, suite.targetGraphId),
|
|
2401
|
+
evaluatorFingerprints: purpose === "evaluation" ? Object.fromEntries(
|
|
2402
|
+
suite.evaluators.map((evaluator) => [evaluator.id, fingerprintGraphDependency(project, evaluator.graphId)])
|
|
2403
|
+
) : {},
|
|
2404
|
+
executionMode: options.executionMode ?? "unknown",
|
|
2405
|
+
accountingComplete: true
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
async function runEvaluationSuite(options) {
|
|
2409
|
+
var _a, _b, _c, _d, _e;
|
|
2410
|
+
const suite = resolveSuite(options.evaluationData, options.suiteId);
|
|
2411
|
+
const purpose = options.purpose ?? "evaluation";
|
|
2412
|
+
const evaluationMode = getEvaluationSuiteMode(suite);
|
|
2413
|
+
if (purpose === "evaluation" && !hasAuthoritativeEvaluationCriteria(suite)) {
|
|
2414
|
+
throw new Error(
|
|
2415
|
+
evaluationMode === "scoring" ? `Scoring evaluation suite "${suite.name}" has no evaluator graph. Add an evaluator that returns result.score or run an execution benchmark.` : `Evaluation suite "${suite.name}" has no required quality check or threshold. Add a quality criterion or run it as an execution benchmark.`
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
validateSuite(options.project, suite, options.dataset, purpose);
|
|
2419
|
+
const runId = options.runId ?? makeId("evaluation");
|
|
2420
|
+
const run = {
|
|
2421
|
+
version: 2,
|
|
2422
|
+
id: runId,
|
|
2423
|
+
projectId: options.project.metadata.id,
|
|
2424
|
+
suiteId: suite.id,
|
|
2425
|
+
suiteName: suite.name,
|
|
2426
|
+
revision: 0,
|
|
2427
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2428
|
+
purpose,
|
|
2429
|
+
evaluationMode,
|
|
2430
|
+
executionStatus: "running",
|
|
2431
|
+
qualityStatus: "not-evaluated",
|
|
2432
|
+
qualityReason: { code: "in-progress", message: "The run is still in progress." },
|
|
2433
|
+
accountingStatus: "complete",
|
|
2434
|
+
provenance: createProvenance(options.project, suite, options.dataset, options),
|
|
2435
|
+
trials: [],
|
|
2436
|
+
thresholdResults: [],
|
|
2437
|
+
warnings: []
|
|
2438
|
+
};
|
|
2439
|
+
const trialCount = Math.max(1, Math.floor(((_a = suite.configuration) == null ? void 0 : _a.trialCount) ?? 1));
|
|
2440
|
+
const concurrency = Math.min(
|
|
2441
|
+
MAX_CONCURRENCY,
|
|
2442
|
+
Math.max(1, Math.floor(((_b = suite.configuration) == null ? void 0 : _b.concurrency) ?? DEFAULT_CONCURRENCY))
|
|
2443
|
+
);
|
|
2444
|
+
const work = options.dataset.cases.filter((testCase) => testCase.enabled !== false).flatMap(
|
|
2445
|
+
(testCase, caseIndex) => Array.from({ length: trialCount }, (_, trialIndex) => ({ testCase, caseIndex, trialIndex }))
|
|
2446
|
+
);
|
|
2447
|
+
const publish = () => {
|
|
2448
|
+
var _a2;
|
|
2449
|
+
run.revision = (run.revision ?? 0) + 1;
|
|
2450
|
+
(_a2 = options.onUpdate) == null ? void 0 : _a2.call(options, structuredClone(run));
|
|
2451
|
+
};
|
|
2452
|
+
publish();
|
|
2453
|
+
const workPool = runEvaluationWorkPool({
|
|
2454
|
+
work,
|
|
2455
|
+
concurrency,
|
|
2456
|
+
signal: options.signal,
|
|
2457
|
+
execute: async (task) => {
|
|
2458
|
+
try {
|
|
2459
|
+
return await runTrial({
|
|
2460
|
+
...task,
|
|
2461
|
+
project: options.project,
|
|
2462
|
+
suite,
|
|
2463
|
+
dataset: options.dataset,
|
|
2464
|
+
purpose,
|
|
2465
|
+
runId,
|
|
2466
|
+
runGraph: options.runGraph,
|
|
2467
|
+
signal: options.signal
|
|
2468
|
+
});
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
const executionStatus = isAbort(error) ? "canceled" : "error";
|
|
2471
|
+
const qualityStatus = purpose === "evaluation" && executionStatus === "error" ? evaluationMode === "scoring" ? "unable-to-evaluate" : "failed" : "not-evaluated";
|
|
2472
|
+
return {
|
|
2473
|
+
id: makeId("trial"),
|
|
2474
|
+
caseId: task.testCase.id,
|
|
2475
|
+
caseName: task.testCase.name,
|
|
2476
|
+
caseIndex: task.caseIndex,
|
|
2477
|
+
trialIndex: task.trialIndex,
|
|
2478
|
+
executionStatus,
|
|
2479
|
+
qualityStatus,
|
|
2480
|
+
qualityReason: executionStatus === "canceled" ? { code: "canceled", message: "The evaluation trial was canceled." } : purpose === "evaluation" ? evaluationMode === "scoring" ? { code: "scores-incomplete", message: "The target graph could not produce a score." } : { code: "target-error", message: "The target graph could not be executed." } : { code: "benchmark", message: "This benchmark trial could not be executed." },
|
|
2481
|
+
inputs: {},
|
|
2482
|
+
expected: {},
|
|
2483
|
+
outputs: {},
|
|
2484
|
+
observations: [],
|
|
2485
|
+
targetMetrics: createEmptyMetrics(),
|
|
2486
|
+
evaluatorMetrics: createEmptyMetrics(),
|
|
2487
|
+
totalMetrics: executionStatus === "canceled" ? { ...createEmptyMetrics(), hasUnknownCost: true } : createEmptyMetrics(),
|
|
2488
|
+
error: toErrorMessage(error)
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
},
|
|
2492
|
+
onSettled: (_result, _index, currentResults) => {
|
|
2493
|
+
run.trials = currentResults.filter((result) => result !== void 0);
|
|
2494
|
+
publish();
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
const results = await workPool;
|
|
2498
|
+
run.trials = results.filter((result) => result !== void 0);
|
|
2499
|
+
run.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2500
|
+
const outcome = aggregate(run.trials, evaluationMode);
|
|
2501
|
+
run.aggregate = outcome.aggregate;
|
|
2502
|
+
run.provenance.accountingComplete = !run.trials.some((trial) => trial.totalMetrics.hasUnknownCost);
|
|
2503
|
+
run.accountingStatus = run.provenance.accountingComplete ? "complete" : "partial";
|
|
2504
|
+
if (run.accountingStatus === "partial") {
|
|
2505
|
+
run.warnings.push(
|
|
2506
|
+
"Some provider pricing was unavailable. Cost totals are unavailable, and cost requirements cannot be evaluated."
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
if ((_c = options.signal) == null ? void 0 : _c.aborted) {
|
|
2510
|
+
run.executionStatus = "canceled";
|
|
2511
|
+
run.qualityStatus = "not-evaluated";
|
|
2512
|
+
run.qualityReason = { code: "canceled", message: "The evaluation run was canceled." };
|
|
2513
|
+
} else if (purpose === "execution-benchmark") {
|
|
2514
|
+
run.executionStatus = "completed";
|
|
2515
|
+
run.qualityStatus = "not-evaluated";
|
|
2516
|
+
run.qualityReason = {
|
|
2517
|
+
code: "benchmark",
|
|
2518
|
+
message: "This run measured execution without evaluating output quality."
|
|
2519
|
+
};
|
|
2520
|
+
} else if (evaluationMode === "scoring") {
|
|
2521
|
+
run.executionStatus = "completed";
|
|
2522
|
+
if ((outcome.aggregate.missingScoreTrialCount ?? run.trials.length) > 0) {
|
|
2523
|
+
run.qualityStatus = "unable-to-evaluate";
|
|
2524
|
+
run.qualityReason = {
|
|
2525
|
+
code: "scores-incomplete",
|
|
2526
|
+
message: "One or more requested trials did not produce a usable score. Available averages are shown with coverage."
|
|
2527
|
+
};
|
|
2528
|
+
run.warnings.push(
|
|
2529
|
+
`Score coverage is ${outcome.aggregate.scoredTrialCount ?? 0} of ${outcome.aggregate.trialCount} requested trials.`
|
|
2530
|
+
);
|
|
2531
|
+
} else if ((outcome.aggregate.scoredTrialCount ?? 0) > 0) {
|
|
2532
|
+
run.qualityStatus = "scored";
|
|
2533
|
+
run.qualityReason = {
|
|
2534
|
+
code: "scores-complete",
|
|
2535
|
+
message: "Every requested trial produced a score. Overall score is the equal-weight average of case averages."
|
|
2536
|
+
};
|
|
2537
|
+
} else {
|
|
2538
|
+
run.qualityStatus = "unable-to-evaluate";
|
|
2539
|
+
run.qualityReason = { code: "scores-incomplete", message: "No requested trial produced a usable score." };
|
|
2540
|
+
}
|
|
2541
|
+
} else {
|
|
2542
|
+
const baseline = options.baseline ?? options.evaluationData.baselines.find((candidate) => candidate.suiteId === suite.id);
|
|
2543
|
+
const usableBaseline = baseline && baselineCompatible(run.provenance, baseline) ? baseline : void 0;
|
|
2544
|
+
if (baseline && !usableBaseline)
|
|
2545
|
+
run.warnings.push(
|
|
2546
|
+
"The suite baseline is stale because its target, dataset, bindings, or evaluator definition changed."
|
|
2547
|
+
);
|
|
2548
|
+
run.thresholdResults = evaluateThresholdResults(outcome.aggregate, suite.thresholds, usableBaseline);
|
|
2549
|
+
const hasPassRateThreshold = ((_d = suite.thresholds) == null ? void 0 : _d.some((threshold) => threshold.metric === "pass-rate")) ?? false;
|
|
2550
|
+
const hasTargetErrorRateThreshold = ((_e = suite.thresholds) == null ? void 0 : _e.some((threshold) => threshold.metric === "target-error-rate")) ?? false;
|
|
2551
|
+
const hasUngovernedCheckFailure = !hasPassRateThreshold && run.trials.some((trial) => trial.executionStatus === "completed" && trial.qualityStatus === "failed");
|
|
2552
|
+
const hasUngovernedTargetError = !hasTargetErrorRateThreshold && run.trials.some((trial) => trial.executionStatus === "error");
|
|
2553
|
+
const hasThresholdFailure = run.thresholdResults.some((result) => result.status === "failed");
|
|
2554
|
+
const hasUnavailableEvidence = run.trials.some((trial) => trial.qualityStatus === "unable-to-evaluate") || run.thresholdResults.some((result) => result.status === "unavailable");
|
|
2555
|
+
const hasAuthoritativePass = run.trials.some((trial) => trial.executionStatus === "completed" && trial.qualityStatus === "passed") || run.thresholdResults.some((result) => result.status === "passed");
|
|
2556
|
+
if (hasUngovernedCheckFailure || hasUngovernedTargetError || hasThresholdFailure) {
|
|
2557
|
+
run.qualityStatus = "failed";
|
|
2558
|
+
run.qualityReason = {
|
|
2559
|
+
code: hasThresholdFailure ? "thresholds-failed" : "checks-failed",
|
|
2560
|
+
message: hasThresholdFailure ? "One or more required aggregate thresholds failed." : "One or more target executions or required quality checks failed."
|
|
2561
|
+
};
|
|
2562
|
+
} else if (hasUnavailableEvidence) {
|
|
2563
|
+
run.qualityStatus = "unable-to-evaluate";
|
|
2564
|
+
run.qualityReason = {
|
|
2565
|
+
code: run.thresholdResults.some((result) => result.status === "unavailable") ? "required-metric-unavailable" : "required-check-error",
|
|
2566
|
+
message: run.thresholdResults.some((result) => result.status === "unavailable") ? "A required aggregate metric could not be evaluated." : "A required quality check could not be evaluated."
|
|
2567
|
+
};
|
|
2568
|
+
} else if (hasAuthoritativePass) {
|
|
2569
|
+
run.qualityStatus = "passed";
|
|
2570
|
+
run.qualityReason = {
|
|
2571
|
+
code: run.thresholdResults.length > 0 ? "thresholds-passed" : "checks-passed",
|
|
2572
|
+
message: run.thresholdResults.length > 0 ? "All required quality checks and aggregate thresholds passed." : "All required quality checks passed."
|
|
2573
|
+
};
|
|
2574
|
+
} else {
|
|
2575
|
+
run.qualityStatus = "not-evaluated";
|
|
2576
|
+
run.qualityReason = {
|
|
2577
|
+
code: "no-completed-trials",
|
|
2578
|
+
message: "No completed trial produced an authoritative quality result."
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
run.executionStatus = "completed";
|
|
2582
|
+
}
|
|
2583
|
+
publish();
|
|
2584
|
+
return run;
|
|
2585
|
+
}
|
|
2586
|
+
async function runEvaluationCases(options) {
|
|
2587
|
+
const permitted = new Set(options.caseIds);
|
|
2588
|
+
return runEvaluationSuite({
|
|
2589
|
+
...options,
|
|
2590
|
+
dataset: { ...options.dataset, cases: options.dataset.cases.filter((testCase) => permitted.has(testCase.id)) }
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
function summarizeEvaluationRun(run) {
|
|
2594
|
+
return run.aggregate ? aggregate(run.trials, getEvaluationSuiteMode(run)) : void 0;
|
|
2595
|
+
}
|
|
2596
|
+
function createEvaluationBaselineSnapshot(run) {
|
|
2597
|
+
const normalizedRun = normalizeEvaluationRun(run);
|
|
2598
|
+
if (normalizedRun.executionStatus !== "completed") {
|
|
2599
|
+
throw new Error("Only a completed evaluation run can be promoted to a baseline.");
|
|
2600
|
+
}
|
|
2601
|
+
if (normalizedRun.purpose !== "execution-benchmark" && getEvaluationSuiteMode(normalizedRun) === "scoring" && normalizedRun.qualityStatus !== "scored") {
|
|
2602
|
+
throw new Error("A scoring baseline needs a complete score for every requested trial.");
|
|
2603
|
+
}
|
|
2604
|
+
const summary = summarizeEvaluationRun(normalizedRun);
|
|
2605
|
+
if (!summary) throw new Error("Only a completed evaluation run can be promoted to a baseline.");
|
|
2606
|
+
return {
|
|
2607
|
+
id: makeId("baseline"),
|
|
2608
|
+
suiteId: normalizedRun.suiteId,
|
|
2609
|
+
sourceRunId: normalizedRun.id,
|
|
2610
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2611
|
+
provenance: normalizedRun.provenance,
|
|
2612
|
+
aggregate: summary.aggregate,
|
|
2613
|
+
purpose: normalizedRun.purpose,
|
|
2614
|
+
evaluationMode: getEvaluationSuiteMode(normalizedRun),
|
|
2615
|
+
qualityStatus: normalizedRun.qualityStatus,
|
|
2616
|
+
qualityReason: normalizedRun.qualityReason,
|
|
2617
|
+
accountingStatus: normalizedRun.accountingStatus,
|
|
2618
|
+
cases: summary.cases
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// src/store.ts
|
|
2623
|
+
var InMemoryEvaluationRunStore = class {
|
|
2624
|
+
#runs = /* @__PURE__ */ new Map();
|
|
2625
|
+
#datasetSnapshots = /* @__PURE__ */ new Map();
|
|
2626
|
+
#recordings = /* @__PURE__ */ new Map();
|
|
2627
|
+
async put(run) {
|
|
2628
|
+
const normalized = normalizeEvaluationRun(run);
|
|
2629
|
+
const key = `${normalized.projectId}/${normalized.id}`;
|
|
2630
|
+
const existing = this.#runs.get(key);
|
|
2631
|
+
if (!shouldReplaceEvaluationRun(existing, normalized)) return;
|
|
2632
|
+
this.#runs.set(key, structuredClone(normalized));
|
|
2633
|
+
}
|
|
2634
|
+
async get(input) {
|
|
2635
|
+
const run = this.#runs.get(`${input.projectId}/${input.runId}`);
|
|
2636
|
+
return run ? normalizeEvaluationRun(run) : void 0;
|
|
2637
|
+
}
|
|
2638
|
+
async list(input) {
|
|
2639
|
+
return Array.from(this.#runs.values()).filter(
|
|
2640
|
+
(run) => run.projectId === input.projectId && (input.suiteId === void 0 || run.suiteId === input.suiteId)
|
|
2641
|
+
).sort((left, right) => right.startedAt.localeCompare(left.startedAt)).map(normalizeEvaluationRun);
|
|
2642
|
+
}
|
|
2643
|
+
async delete(input) {
|
|
2644
|
+
this.#runs.delete(`${input.projectId}/${input.runId}`);
|
|
2645
|
+
for (const [key, artifact] of this.#recordings) {
|
|
2646
|
+
if (artifact.projectId === input.projectId && artifact.runId === input.runId) this.#recordings.delete(key);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
async putDatasetSnapshot(snapshot) {
|
|
2650
|
+
assertEvaluationDatasetSnapshot(snapshot);
|
|
2651
|
+
const key = `${snapshot.projectId}/${snapshot.fingerprint}`;
|
|
2652
|
+
if (this.#datasetSnapshots.has(key)) return;
|
|
2653
|
+
this.#datasetSnapshots.set(key, structuredClone(snapshot));
|
|
2654
|
+
}
|
|
2655
|
+
async getDatasetSnapshot(input) {
|
|
2656
|
+
const snapshot = this.#datasetSnapshots.get(`${input.projectId}/${input.fingerprint}`);
|
|
2657
|
+
return snapshot ? structuredClone(snapshot) : void 0;
|
|
2658
|
+
}
|
|
2659
|
+
async putRecording(artifact) {
|
|
2660
|
+
const key = `${artifact.projectId}/${artifact.reference.id}`;
|
|
2661
|
+
const existing = this.#recordings.get(key);
|
|
2662
|
+
if (existing && (existing.runId !== artifact.runId || existing.trialId !== artifact.trialId)) {
|
|
2663
|
+
throw new Error("An evaluation recording ID cannot be reassigned to another run or trial.");
|
|
2664
|
+
}
|
|
2665
|
+
const next = structuredClone(artifact);
|
|
2666
|
+
if (existing) next.reference = structuredClone(existing.reference);
|
|
2667
|
+
this.#recordings.set(key, next);
|
|
2668
|
+
}
|
|
2669
|
+
async getRecording(input) {
|
|
2670
|
+
const artifact = this.#recordings.get(`${input.projectId}/${input.recordingId}`);
|
|
2671
|
+
if (!artifact) return void 0;
|
|
2672
|
+
if (artifact.reference.retention === "temporary" && artifact.reference.expiresAt && Date.parse(artifact.reference.expiresAt) <= Date.now()) {
|
|
2673
|
+
this.#recordings.delete(`${input.projectId}/${input.recordingId}`);
|
|
2674
|
+
return void 0;
|
|
2675
|
+
}
|
|
2676
|
+
return structuredClone(artifact);
|
|
2677
|
+
}
|
|
2678
|
+
async updateRecordingRetention(input) {
|
|
2679
|
+
const key = `${input.projectId}/${input.recordingId}`;
|
|
2680
|
+
const artifact = this.#recordings.get(key);
|
|
2681
|
+
if (!artifact) return;
|
|
2682
|
+
artifact.reference = {
|
|
2683
|
+
id: artifact.reference.id,
|
|
2684
|
+
retention: input.retention,
|
|
2685
|
+
...input.expiresAt === void 0 ? {} : { expiresAt: input.expiresAt }
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
async promoteBaseline(input) {
|
|
2689
|
+
for (const artifact of this.#recordings.values()) {
|
|
2690
|
+
if (artifact.projectId !== input.projectId || artifact.runId !== input.runId) continue;
|
|
2691
|
+
artifact.reference = { id: artifact.reference.id, retention: "baseline" };
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
|
|
2696
|
+
// src/serialization.ts
|
|
2697
|
+
var yaml = __toESM(require("yaml"), 1);
|
|
2698
|
+
function isRecord2(value) {
|
|
2699
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2700
|
+
}
|
|
2701
|
+
function requireRecord(value, path) {
|
|
2702
|
+
if (!isRecord2(value)) throw new Error(`${path} must be an object.`);
|
|
2703
|
+
return value;
|
|
2704
|
+
}
|
|
2705
|
+
function requireString(value, path) {
|
|
2706
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path} must be a non-empty string.`);
|
|
2707
|
+
}
|
|
2708
|
+
function requireText(value, path) {
|
|
2709
|
+
if (typeof value !== "string") throw new Error(`${path} must be a string.`);
|
|
2710
|
+
}
|
|
2711
|
+
function requireBoolean(value, path) {
|
|
2712
|
+
if (typeof value !== "boolean") throw new Error(`${path} must be a boolean.`);
|
|
2713
|
+
}
|
|
2714
|
+
function requireFiniteNumber(value, path) {
|
|
2715
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${path} must be a finite number.`);
|
|
2716
|
+
}
|
|
2717
|
+
function requireArray(value, path) {
|
|
2718
|
+
if (!Array.isArray(value)) throw new Error(`${path} must be an array.`);
|
|
2719
|
+
return value;
|
|
2720
|
+
}
|
|
2721
|
+
function stripLegacyEditorSelections(data) {
|
|
2722
|
+
const { selectedSuiteId: _selectedSuiteId, selectedDatasetId: _selectedDatasetId, ...projectData } = data;
|
|
2723
|
+
return projectData;
|
|
2724
|
+
}
|
|
2725
|
+
function validateOptionalPrimitive(record, key, type, path) {
|
|
2726
|
+
const value = record[key];
|
|
2727
|
+
if (value === void 0) return;
|
|
2728
|
+
if (type === "number") requireFiniteNumber(value, `${path}.${key}`);
|
|
2729
|
+
else if (typeof value !== type) throw new Error(`${path}.${key} must be a ${type}.`);
|
|
2730
|
+
}
|
|
2731
|
+
var ASSERTION_OPERATORS = /* @__PURE__ */ new Set([
|
|
2732
|
+
"equals",
|
|
2733
|
+
"not-equals",
|
|
2734
|
+
"contains",
|
|
2735
|
+
"matches-regex",
|
|
2736
|
+
"type-is",
|
|
2737
|
+
"json-schema",
|
|
2738
|
+
"number-at-least",
|
|
2739
|
+
"number-at-most",
|
|
2740
|
+
"number-between",
|
|
2741
|
+
"array-includes",
|
|
2742
|
+
"set-overlaps",
|
|
2743
|
+
"contains-any",
|
|
2744
|
+
"contains-all"
|
|
2745
|
+
]);
|
|
2746
|
+
var EVALUATOR_CONTEXT_INPUTS = /* @__PURE__ */ new Set(["case", "inputs", "expected", "outputs", "run"]);
|
|
2747
|
+
function validateSuiteShape(value, index) {
|
|
2748
|
+
const path = `evaluations.suites[${index}]`;
|
|
2749
|
+
const suite = requireRecord(value, path);
|
|
2750
|
+
for (const key of ["id", "targetGraphId", "datasetId"]) requireString(suite[key], `${path}.${key}`);
|
|
2751
|
+
requireText(suite.name, `${path}.name`);
|
|
2752
|
+
if (suite.evaluationMode !== void 0 && suite.evaluationMode !== "pass-fail" && suite.evaluationMode !== "scoring") {
|
|
2753
|
+
throw new Error(`${path}.evaluationMode must be "pass-fail" or "scoring".`);
|
|
2754
|
+
}
|
|
2755
|
+
for (const [bindingIndex, bindingValue] of requireArray(suite.inputBindings, `${path}.inputBindings`).entries()) {
|
|
2756
|
+
const binding = requireRecord(bindingValue, `${path}.inputBindings[${bindingIndex}]`);
|
|
2757
|
+
requireString(binding.graphInputId, `${path}.inputBindings[${bindingIndex}].graphInputId`);
|
|
2758
|
+
requireString(binding.datasetFieldId, `${path}.inputBindings[${bindingIndex}].datasetFieldId`);
|
|
2759
|
+
}
|
|
2760
|
+
for (const [assertionIndex, assertionValue] of requireArray(suite.assertions, `${path}.assertions`).entries()) {
|
|
2761
|
+
const assertionPath = `${path}.assertions[${assertionIndex}]`;
|
|
2762
|
+
const assertion = requireRecord(assertionValue, assertionPath);
|
|
2763
|
+
for (const key of ["id", "outputPath"]) requireString(assertion[key], `${assertionPath}.${key}`);
|
|
2764
|
+
requireText(assertion.name, `${assertionPath}.name`);
|
|
2765
|
+
if (typeof assertion.operator !== "string" || !ASSERTION_OPERATORS.has(assertion.operator)) {
|
|
2766
|
+
throw new Error(`${assertionPath}.operator is not supported.`);
|
|
2767
|
+
}
|
|
2768
|
+
validateOptionalPrimitive(assertion, "required", "boolean", assertionPath);
|
|
2769
|
+
const expected = requireRecord(assertion.expected, `${assertionPath}.expected`);
|
|
2770
|
+
if (expected.kind === "literal") {
|
|
2771
|
+
if (!Object.hasOwn(expected, "value")) throw new Error(`${assertionPath}.expected.value is required.`);
|
|
2772
|
+
} else if (expected.kind === "dataset-field") {
|
|
2773
|
+
requireString(expected.fieldId, `${assertionPath}.expected.fieldId`);
|
|
2774
|
+
} else {
|
|
2775
|
+
throw new Error(`${assertionPath}.expected.kind must be "literal" or "dataset-field".`);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
for (const [evaluatorIndex, evaluatorValue] of requireArray(suite.evaluators, `${path}.evaluators`).entries()) {
|
|
2779
|
+
const evaluatorPath = `${path}.evaluators[${evaluatorIndex}]`;
|
|
2780
|
+
const evaluator = requireRecord(evaluatorValue, evaluatorPath);
|
|
2781
|
+
for (const key of ["id", "graphId"]) requireString(evaluator[key], `${evaluatorPath}.${key}`);
|
|
2782
|
+
requireText(evaluator.name, `${evaluatorPath}.name`);
|
|
2783
|
+
validateOptionalPrimitive(evaluator, "required", "boolean", evaluatorPath);
|
|
2784
|
+
validateOptionalPrimitive(evaluator, "runOnTargetError", "boolean", evaluatorPath);
|
|
2785
|
+
validateOptionalPrimitive(evaluator, "scoreWeight", "number", evaluatorPath);
|
|
2786
|
+
if (evaluator.inputBindings !== void 0) {
|
|
2787
|
+
for (const [bindingIndex, bindingValue] of requireArray(
|
|
2788
|
+
evaluator.inputBindings,
|
|
2789
|
+
`${evaluatorPath}.inputBindings`
|
|
2790
|
+
).entries()) {
|
|
2791
|
+
const bindingPath = `${evaluatorPath}.inputBindings[${bindingIndex}]`;
|
|
2792
|
+
const binding = requireRecord(bindingValue, bindingPath);
|
|
2793
|
+
requireString(binding.graphInputId, `${bindingPath}.graphInputId`);
|
|
2794
|
+
const source = requireRecord(binding.source, `${bindingPath}.source`);
|
|
2795
|
+
if (source.kind === "dataset-field") requireString(source.fieldId, `${bindingPath}.source.fieldId`);
|
|
2796
|
+
else if (source.kind === "target-output") requireString(source.outputId, `${bindingPath}.source.outputId`);
|
|
2797
|
+
else if (source.kind === "context") {
|
|
2798
|
+
if (typeof source.context !== "string" || !EVALUATOR_CONTEXT_INPUTS.has(source.context)) {
|
|
2799
|
+
throw new Error(`${bindingPath}.source.context is not supported.`);
|
|
2800
|
+
}
|
|
2801
|
+
} else {
|
|
2802
|
+
throw new Error(`${bindingPath}.source.kind is not supported.`);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
if (suite.thresholds !== void 0) {
|
|
2808
|
+
for (const [thresholdIndex, thresholdValue2] of requireArray(suite.thresholds, `${path}.thresholds`).entries()) {
|
|
2809
|
+
const thresholdPath = `${path}.thresholds[${thresholdIndex}]`;
|
|
2810
|
+
const threshold = requireRecord(thresholdValue2, thresholdPath);
|
|
2811
|
+
for (const key of ["id", "metric", "operator"]) requireString(threshold[key], `${thresholdPath}.${key}`);
|
|
2812
|
+
requireFiniteNumber(threshold.value, `${thresholdPath}.value`);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
if (suite.configuration !== void 0) {
|
|
2816
|
+
const configuration = requireRecord(suite.configuration, `${path}.configuration`);
|
|
2817
|
+
for (const key of ["trialCount", "concurrency", "timeoutMs", "seed"]) {
|
|
2818
|
+
validateOptionalPrimitive(configuration, key, "number", `${path}.configuration`);
|
|
2819
|
+
}
|
|
2820
|
+
validateOptionalPrimitive(configuration, "seedGraphInputId", "string", `${path}.configuration`);
|
|
2821
|
+
validateOptionalPrimitive(configuration, "recordingRetention", "string", `${path}.configuration`);
|
|
2822
|
+
}
|
|
2823
|
+
validateOptionalPrimitive(suite, "description", "string", path);
|
|
2824
|
+
if (suite.tags !== void 0) {
|
|
2825
|
+
for (const [tagIndex, tag] of requireArray(suite.tags, `${path}.tags`).entries()) {
|
|
2826
|
+
requireString(tag, `${path}.tags[${tagIndex}]`);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
function validateNumericRecord(value, path) {
|
|
2831
|
+
const record = requireRecord(value, path);
|
|
2832
|
+
for (const [key, entry] of Object.entries(record)) requireFiniteNumber(entry, `${path}.${key}`);
|
|
2833
|
+
}
|
|
2834
|
+
function validateBaselineShape(value, index) {
|
|
2835
|
+
const path = `evaluations.baselines[${index}]`;
|
|
2836
|
+
const baseline = requireRecord(value, path);
|
|
2837
|
+
for (const key of ["id", "suiteId", "createdAt"]) requireString(baseline[key], `${path}.${key}`);
|
|
2838
|
+
validateOptionalPrimitive(baseline, "sourceRunId", "string", path);
|
|
2839
|
+
if (baseline.evaluationMode !== void 0 && baseline.evaluationMode !== "pass-fail" && baseline.evaluationMode !== "scoring") {
|
|
2840
|
+
throw new Error(`${path}.evaluationMode must be "pass-fail" or "scoring".`);
|
|
2841
|
+
}
|
|
2842
|
+
const provenance = requireRecord(baseline.provenance, `${path}.provenance`);
|
|
2843
|
+
for (const key of [
|
|
2844
|
+
"projectFingerprint",
|
|
2845
|
+
"suiteFingerprint",
|
|
2846
|
+
"datasetFingerprint",
|
|
2847
|
+
"targetFingerprint",
|
|
2848
|
+
"executionMode"
|
|
2849
|
+
]) {
|
|
2850
|
+
requireString(provenance[key], `${path}.provenance.${key}`);
|
|
2851
|
+
}
|
|
2852
|
+
const evaluatorFingerprints = requireRecord(
|
|
2853
|
+
provenance.evaluatorFingerprints,
|
|
2854
|
+
`${path}.provenance.evaluatorFingerprints`
|
|
2855
|
+
);
|
|
2856
|
+
for (const [key, fingerprint2] of Object.entries(evaluatorFingerprints)) {
|
|
2857
|
+
requireString(fingerprint2, `${path}.provenance.evaluatorFingerprints.${key}`);
|
|
2858
|
+
}
|
|
2859
|
+
requireBoolean(provenance.accountingComplete, `${path}.provenance.accountingComplete`);
|
|
2860
|
+
const aggregate2 = requireRecord(baseline.aggregate, `${path}.aggregate`);
|
|
2861
|
+
for (const key of [
|
|
2862
|
+
"trialCount",
|
|
2863
|
+
"passedTrialCount",
|
|
2864
|
+
"failedTrialCount",
|
|
2865
|
+
"erroredTrialCount",
|
|
2866
|
+
"canceledTrialCount",
|
|
2867
|
+
"passRate",
|
|
2868
|
+
"averageLatencyMs",
|
|
2869
|
+
"p95LatencyMs",
|
|
2870
|
+
"targetErrorRate",
|
|
2871
|
+
"evaluatorErrorRate",
|
|
2872
|
+
"toolFailureRate"
|
|
2873
|
+
]) {
|
|
2874
|
+
requireFiniteNumber(aggregate2[key], `${path}.aggregate.${key}`);
|
|
2875
|
+
}
|
|
2876
|
+
for (const key of [
|
|
2877
|
+
"evaluatedTrialCount",
|
|
2878
|
+
"notEvaluatedTrialCount",
|
|
2879
|
+
"unableToEvaluateTrialCount",
|
|
2880
|
+
"meanScore",
|
|
2881
|
+
"totalCostUsd",
|
|
2882
|
+
"averageCostUsd",
|
|
2883
|
+
"scoredTrialCount",
|
|
2884
|
+
"missingScoreTrialCount"
|
|
2885
|
+
]) {
|
|
2886
|
+
validateOptionalPrimitive(aggregate2, key, "number", `${path}.aggregate`);
|
|
2887
|
+
}
|
|
2888
|
+
validateNumericRecord(aggregate2.metrics, `${path}.aggregate.metrics`);
|
|
2889
|
+
for (const [caseIndex, caseValue] of requireArray(baseline.cases, `${path}.cases`).entries()) {
|
|
2890
|
+
const casePath = `${path}.cases[${caseIndex}]`;
|
|
2891
|
+
const caseAggregate = requireRecord(caseValue, casePath);
|
|
2892
|
+
requireString(caseAggregate.caseId, `${casePath}.caseId`);
|
|
2893
|
+
requireString(caseAggregate.caseName, `${casePath}.caseName`);
|
|
2894
|
+
validateNumericRecord(caseAggregate.metrics, `${casePath}.metrics`);
|
|
2895
|
+
for (const key of [
|
|
2896
|
+
"passRate",
|
|
2897
|
+
"meanScore",
|
|
2898
|
+
"evaluatedTrialCount",
|
|
2899
|
+
"passedTrialCount",
|
|
2900
|
+
"failedTrialCount",
|
|
2901
|
+
"notEvaluatedTrialCount",
|
|
2902
|
+
"unableToEvaluateTrialCount",
|
|
2903
|
+
"erroredTrialCount",
|
|
2904
|
+
"canceledTrialCount",
|
|
2905
|
+
"scoredTrialCount",
|
|
2906
|
+
"missingScoreTrialCount"
|
|
2907
|
+
]) {
|
|
2908
|
+
validateOptionalPrimitive(caseAggregate, key, "number", casePath);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
function createEmptyEvaluationProjectData() {
|
|
2913
|
+
return { version: 1, suites: [], baselines: [] };
|
|
2914
|
+
}
|
|
2915
|
+
function serializeEvaluationProjectData(data) {
|
|
2916
|
+
if (data.version !== 1) throw new Error(`Unsupported evaluation data version: ${data.version}`);
|
|
2917
|
+
const persistentData = stripLegacyEditorSelections(data);
|
|
2918
|
+
assertPortableJson(persistentData);
|
|
2919
|
+
return {
|
|
2920
|
+
...structuredClone(persistentData),
|
|
2921
|
+
baselines: persistentData.baselines.map(normalizeEvaluationBaselineSnapshot)
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
function deserializeEvaluationProjectData(data) {
|
|
2925
|
+
if (data === null || typeof data !== "object" || Array.isArray(data))
|
|
2926
|
+
throw new Error("Evaluation project data must be an object.");
|
|
2927
|
+
const candidate = data;
|
|
2928
|
+
if (candidate.version !== 1 || !Array.isArray(candidate.suites) || !Array.isArray(candidate.baselines)) {
|
|
2929
|
+
throw new Error("Unsupported or invalid evaluation project data.");
|
|
2930
|
+
}
|
|
2931
|
+
const persistentData = stripLegacyEditorSelections(candidate);
|
|
2932
|
+
assertPortableJson(persistentData);
|
|
2933
|
+
persistentData.suites.forEach(validateSuiteShape);
|
|
2934
|
+
persistentData.baselines.forEach(validateBaselineShape);
|
|
2935
|
+
return {
|
|
2936
|
+
...structuredClone(persistentData),
|
|
2937
|
+
baselines: persistentData.baselines.map(normalizeEvaluationBaselineSnapshot)
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
function serializeEvaluationProjectDataToString(data) {
|
|
2941
|
+
return yaml.stringify(serializeEvaluationProjectData(data), null, 2);
|
|
2942
|
+
}
|
|
2943
|
+
function deserializeEvaluationProjectDataFromString(data) {
|
|
2944
|
+
return deserializeEvaluationProjectData(yaml.parse(data));
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// src/datasetTransfer.ts
|
|
2948
|
+
var DATASET_EXPORT_VERSION = 1;
|
|
2949
|
+
function isRecord3(value) {
|
|
2950
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2951
|
+
}
|
|
2952
|
+
function requireString2(value, path) {
|
|
2953
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${path} must be a non-empty string.`);
|
|
2954
|
+
return value;
|
|
2955
|
+
}
|
|
2956
|
+
function requireText2(value, path) {
|
|
2957
|
+
if (typeof value !== "string") throw new Error(`${path} must be a string.`);
|
|
2958
|
+
return value;
|
|
2959
|
+
}
|
|
2960
|
+
function parseFields(value) {
|
|
2961
|
+
if (!Array.isArray(value)) throw new Error("Evaluation dataset fields must be an array.");
|
|
2962
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2963
|
+
return value.map((field, index) => {
|
|
2964
|
+
if (!isRecord3(field)) throw new Error(`fields[${index}] must be an object.`);
|
|
2965
|
+
const id = requireString2(field.id, `fields[${index}].id`);
|
|
2966
|
+
if (ids.has(id)) throw new Error(`Evaluation dataset has duplicate field id "${id}".`);
|
|
2967
|
+
ids.add(id);
|
|
2968
|
+
const role = field.role;
|
|
2969
|
+
if (role !== "input" && role !== "expected" && role !== "metadata") {
|
|
2970
|
+
throw new Error(`fields[${index}].role must be input, expected, or metadata.`);
|
|
2971
|
+
}
|
|
2972
|
+
if (field.description !== void 0 && typeof field.description !== "string") {
|
|
2973
|
+
throw new Error(`fields[${index}].description must be a string.`);
|
|
2974
|
+
}
|
|
2975
|
+
if (field.required !== void 0 && typeof field.required !== "boolean") {
|
|
2976
|
+
throw new Error(`fields[${index}].required must be a boolean.`);
|
|
2977
|
+
}
|
|
2978
|
+
return {
|
|
2979
|
+
id,
|
|
2980
|
+
name: requireText2(field.name, `fields[${index}].name`),
|
|
2981
|
+
dataType: requireString2(field.dataType, `fields[${index}].dataType`),
|
|
2982
|
+
role,
|
|
2983
|
+
...typeof field.description === "string" ? { description: field.description } : {},
|
|
2984
|
+
...typeof field.required === "boolean" ? { required: field.required } : {}
|
|
2985
|
+
};
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
function parseCases(value, fieldIds) {
|
|
2989
|
+
if (!Array.isArray(value)) throw new Error("Evaluation dataset cases must be an array.");
|
|
2990
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2991
|
+
return value.map((testCase, index) => {
|
|
2992
|
+
if (!isRecord3(testCase)) throw new Error(`cases[${index}] must be an object.`);
|
|
2993
|
+
const id = requireString2(testCase.id, `cases[${index}].id`);
|
|
2994
|
+
if (ids.has(id)) throw new Error(`Evaluation dataset has duplicate case id "${id}".`);
|
|
2995
|
+
ids.add(id);
|
|
2996
|
+
if (!isRecord3(testCase.values)) throw new Error(`cases[${index}].values must be an object.`);
|
|
2997
|
+
for (const [fieldId, fieldValue] of Object.entries(testCase.values)) {
|
|
2998
|
+
if (!fieldIds.has(fieldId)) throw new Error(`cases[${index}].values references unknown field "${fieldId}".`);
|
|
2999
|
+
assertPortableJson(fieldValue, `cases[${index}].values.${fieldId}`);
|
|
3000
|
+
}
|
|
3001
|
+
if (testCase.tags !== void 0 && (!Array.isArray(testCase.tags) || testCase.tags.some((tag) => typeof tag !== "string"))) {
|
|
3002
|
+
throw new Error(`cases[${index}].tags must be an array of strings.`);
|
|
3003
|
+
}
|
|
3004
|
+
if (testCase.note !== void 0 && typeof testCase.note !== "string") throw new Error(`cases[${index}].note must be a string.`);
|
|
3005
|
+
if (testCase.enabled !== void 0 && typeof testCase.enabled !== "boolean") throw new Error(`cases[${index}].enabled must be a boolean.`);
|
|
3006
|
+
return {
|
|
3007
|
+
id,
|
|
3008
|
+
name: requireText2(testCase.name, `cases[${index}].name`),
|
|
3009
|
+
values: testCase.values,
|
|
3010
|
+
...typeof testCase.enabled === "boolean" ? { enabled: testCase.enabled } : {},
|
|
3011
|
+
...Array.isArray(testCase.tags) ? { tags: testCase.tags } : {},
|
|
3012
|
+
...typeof testCase.note === "string" ? { note: testCase.note } : {}
|
|
3013
|
+
};
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
function validateEvaluationDataset(value, scope) {
|
|
3017
|
+
if (!isRecord3(value)) throw new Error("Evaluation dataset must be an object.");
|
|
3018
|
+
const fields = parseFields(value.fields);
|
|
3019
|
+
const importedProjectId = value.projectId === void 0 ? void 0 : requireString2(value.projectId, "dataset.projectId");
|
|
3020
|
+
const projectId = (scope == null ? void 0 : scope.projectId) ?? importedProjectId;
|
|
3021
|
+
const dataset = {
|
|
3022
|
+
id: (scope == null ? void 0 : scope.id) ?? requireString2(value.id, "dataset.id"),
|
|
3023
|
+
name: requireText2(value.name, "dataset.name"),
|
|
3024
|
+
...projectId === void 0 ? {} : { projectId },
|
|
3025
|
+
fields,
|
|
3026
|
+
cases: parseCases(value.cases, new Set(fields.map((field) => field.id)))
|
|
3027
|
+
};
|
|
3028
|
+
return { ...dataset, contentFingerprint: fingerprintEvaluationDataset(dataset) };
|
|
3029
|
+
}
|
|
3030
|
+
function localizeEvaluationDataset(value, id) {
|
|
3031
|
+
const dataset = validateEvaluationDataset(value, id === void 0 ? void 0 : { id });
|
|
3032
|
+
const { projectId: _legacyProjectId, ...localDataset } = dataset;
|
|
3033
|
+
return { ...localDataset, contentFingerprint: fingerprintEvaluationDataset(localDataset) };
|
|
3034
|
+
}
|
|
3035
|
+
function validateEvaluationDatasetForTransfer(value, scope) {
|
|
3036
|
+
const dataset = validateEvaluationDataset(value, scope);
|
|
3037
|
+
assertEvaluationDatasetValuesMatchDeclaredTypes(dataset);
|
|
3038
|
+
return dataset;
|
|
3039
|
+
}
|
|
3040
|
+
function serializeEvaluationDatasetJson(dataset) {
|
|
3041
|
+
const validated = validateEvaluationDatasetForTransfer(dataset);
|
|
3042
|
+
return JSON.stringify({ version: DATASET_EXPORT_VERSION, dataset: validated }, null, 2);
|
|
3043
|
+
}
|
|
3044
|
+
function deserializeEvaluationDatasetJson(source, scope) {
|
|
3045
|
+
let parsed;
|
|
3046
|
+
try {
|
|
3047
|
+
parsed = JSON.parse(source);
|
|
3048
|
+
} catch {
|
|
3049
|
+
throw new Error("Evaluation dataset JSON is not valid JSON.");
|
|
3050
|
+
}
|
|
3051
|
+
if (!isRecord3(parsed) || parsed.version !== DATASET_EXPORT_VERSION || !isRecord3(parsed.dataset)) {
|
|
3052
|
+
throw new Error(`Expected an evaluation dataset export with version ${DATASET_EXPORT_VERSION}.`);
|
|
3053
|
+
}
|
|
3054
|
+
const dataset = validateEvaluationDatasetForTransfer(parsed.dataset, { id: scope.id });
|
|
3055
|
+
const { projectId: _legacyProjectId, ...localDataset } = dataset;
|
|
3056
|
+
return { ...localDataset, contentFingerprint: fingerprintEvaluationDataset(localDataset) };
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// src/suiteTransfer.ts
|
|
3060
|
+
var SUITE_BUNDLE_EXPORT_VERSION = 1;
|
|
3061
|
+
function isRecord4(value) {
|
|
3062
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3063
|
+
}
|
|
3064
|
+
function requireNonEmptyString(value, name) {
|
|
3065
|
+
if (value.trim().length === 0) throw new Error(`${name} must be a non-empty string.`);
|
|
3066
|
+
}
|
|
3067
|
+
function validateEvaluationSuite(value) {
|
|
3068
|
+
return deserializeEvaluationProjectData({ version: 1, suites: [value], baselines: [] }).suites[0];
|
|
3069
|
+
}
|
|
3070
|
+
function validateBundle(value) {
|
|
3071
|
+
if (!isRecord4(value) || value.version !== SUITE_BUNDLE_EXPORT_VERSION || !isRecord4(value.suite) || !isRecord4(value.dataset)) {
|
|
3072
|
+
throw new Error(`Expected an evaluation suite export with version ${SUITE_BUNDLE_EXPORT_VERSION}.`);
|
|
3073
|
+
}
|
|
3074
|
+
const suite = validateEvaluationSuite(value.suite);
|
|
3075
|
+
const dataset = validateEvaluationDatasetForTransfer(value.dataset);
|
|
3076
|
+
if (suite.datasetId !== dataset.id) {
|
|
3077
|
+
throw new Error("The evaluation suite export must include the dataset referenced by its suite.");
|
|
3078
|
+
}
|
|
3079
|
+
return { suite, dataset };
|
|
3080
|
+
}
|
|
3081
|
+
function serializeEvaluationSuiteBundleJson(suite, dataset) {
|
|
3082
|
+
const bundle = validateBundle({ version: SUITE_BUNDLE_EXPORT_VERSION, suite, dataset });
|
|
3083
|
+
return JSON.stringify(
|
|
3084
|
+
{ version: SUITE_BUNDLE_EXPORT_VERSION, ...bundle },
|
|
3085
|
+
null,
|
|
3086
|
+
2
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
function deserializeEvaluationSuiteBundleJson(source, scope) {
|
|
3090
|
+
let parsed;
|
|
3091
|
+
try {
|
|
3092
|
+
parsed = JSON.parse(source);
|
|
3093
|
+
} catch {
|
|
3094
|
+
throw new Error("Evaluation suite JSON is not valid JSON.");
|
|
3095
|
+
}
|
|
3096
|
+
requireNonEmptyString(scope.suiteId, "suiteId");
|
|
3097
|
+
requireNonEmptyString(scope.datasetId, "datasetId");
|
|
3098
|
+
const bundle = validateBundle(parsed);
|
|
3099
|
+
const dataset = localizeEvaluationDataset(bundle.dataset, scope.datasetId);
|
|
3100
|
+
return {
|
|
3101
|
+
dataset,
|
|
3102
|
+
suite: {
|
|
3103
|
+
...bundle.suite,
|
|
3104
|
+
id: scope.suiteId,
|
|
3105
|
+
datasetId: dataset.id
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3110
|
+
0 && (module.exports = {
|
|
3111
|
+
EvaluationGraphExecutionError,
|
|
3112
|
+
InMemoryEvaluationRunStore,
|
|
3113
|
+
LEGACY_EVALUATOR_INPUT_IDS,
|
|
3114
|
+
areEvaluationDataTypesCompatible,
|
|
3115
|
+
assertEvaluationDatasetSnapshot,
|
|
3116
|
+
assertEvaluationDatasetValuesMatchDeclaredTypes,
|
|
3117
|
+
assertPortableJson,
|
|
3118
|
+
canonicalStringify,
|
|
3119
|
+
canonicalizePortableJson,
|
|
3120
|
+
clonePortableJson,
|
|
3121
|
+
createEmptyEvaluationProjectData,
|
|
3122
|
+
createEmptyMetrics,
|
|
3123
|
+
createEvaluationBaselineSnapshot,
|
|
3124
|
+
deserializeEvaluationDatasetJson,
|
|
3125
|
+
deserializeEvaluationProjectData,
|
|
3126
|
+
deserializeEvaluationProjectDataFromString,
|
|
3127
|
+
deserializeEvaluationSuiteBundleJson,
|
|
3128
|
+
evaluateAssertion,
|
|
3129
|
+
evaluateThresholdResults,
|
|
3130
|
+
finalizeEvaluationRecordingRetention,
|
|
3131
|
+
fingerprint,
|
|
3132
|
+
fingerprintEvaluationDataset,
|
|
3133
|
+
getEvaluationSuiteMode,
|
|
3134
|
+
getEvaluationTopLevelOutputId,
|
|
3135
|
+
hasAuthoritativeEvaluationCriteria,
|
|
3136
|
+
isEvaluationOutputPathSyntaxValid,
|
|
3137
|
+
isEvaluationValueCompatibleWithDataType,
|
|
3138
|
+
isPortableEvaluationDataType,
|
|
3139
|
+
localizeEvaluationDataset,
|
|
3140
|
+
normalizeEvaluationAggregate,
|
|
3141
|
+
normalizeEvaluationBaselineSnapshot,
|
|
3142
|
+
normalizeEvaluationRun,
|
|
3143
|
+
normalizeEvaluationTrial,
|
|
3144
|
+
portableEvaluationDataTypes,
|
|
3145
|
+
runEvaluationCases,
|
|
3146
|
+
runEvaluationSuite,
|
|
3147
|
+
runEvaluationWorkPool,
|
|
3148
|
+
serializeEvaluationDatasetJson,
|
|
3149
|
+
serializeEvaluationProjectData,
|
|
3150
|
+
serializeEvaluationProjectDataToString,
|
|
3151
|
+
serializeEvaluationSuiteBundleJson,
|
|
3152
|
+
shouldReplaceEvaluationRun,
|
|
3153
|
+
summarizeEvaluationRun,
|
|
3154
|
+
usesLegacyEvaluatorInputEnvelope,
|
|
3155
|
+
validateEvaluationAssertionConfiguration,
|
|
3156
|
+
validateEvaluationAssertionExpectedValue,
|
|
3157
|
+
validateEvaluationDataset,
|
|
3158
|
+
validateEvaluationDatasetForTransfer
|
|
3159
|
+
});
|
|
3160
|
+
//# sourceMappingURL=bundle.cjs.map
|