@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,277 @@
|
|
|
1
|
+
import * as yaml from 'yaml';
|
|
2
|
+
import { assertPortableJson } from './canonical.js';
|
|
3
|
+
import { normalizeEvaluationBaselineSnapshot } from './normalization.js';
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function requireRecord(value, path) {
|
|
8
|
+
if (!isRecord(value))
|
|
9
|
+
throw new Error(`${path} must be an object.`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function requireString(value, path) {
|
|
13
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
14
|
+
throw new Error(`${path} must be a non-empty string.`);
|
|
15
|
+
}
|
|
16
|
+
function requireText(value, path) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
throw new Error(`${path} must be a string.`);
|
|
19
|
+
}
|
|
20
|
+
function requireBoolean(value, path) {
|
|
21
|
+
if (typeof value !== 'boolean')
|
|
22
|
+
throw new Error(`${path} must be a boolean.`);
|
|
23
|
+
}
|
|
24
|
+
function requireFiniteNumber(value, path) {
|
|
25
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
26
|
+
throw new Error(`${path} must be a finite number.`);
|
|
27
|
+
}
|
|
28
|
+
function requireArray(value, path) {
|
|
29
|
+
if (!Array.isArray(value))
|
|
30
|
+
throw new Error(`${path} must be an array.`);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Selection is editor workspace state, not executable project data. Older
|
|
35
|
+
* project attachments can contain these fields, and previous editors could
|
|
36
|
+
* put an explicit `undefined` here, which is not portable JSON. They have no
|
|
37
|
+
* effect on project behavior, so discard both fields before validation.
|
|
38
|
+
*/
|
|
39
|
+
function stripLegacyEditorSelections(data) {
|
|
40
|
+
const { selectedSuiteId: _selectedSuiteId, selectedDatasetId: _selectedDatasetId, ...projectData } = data;
|
|
41
|
+
return projectData;
|
|
42
|
+
}
|
|
43
|
+
function validateOptionalPrimitive(record, key, type, path) {
|
|
44
|
+
const value = record[key];
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return;
|
|
47
|
+
if (type === 'number')
|
|
48
|
+
requireFiniteNumber(value, `${path}.${key}`);
|
|
49
|
+
else if (typeof value !== type)
|
|
50
|
+
throw new Error(`${path}.${key} must be a ${type}.`);
|
|
51
|
+
}
|
|
52
|
+
const ASSERTION_OPERATORS = new Set([
|
|
53
|
+
'equals',
|
|
54
|
+
'not-equals',
|
|
55
|
+
'contains',
|
|
56
|
+
'matches-regex',
|
|
57
|
+
'type-is',
|
|
58
|
+
'json-schema',
|
|
59
|
+
'number-at-least',
|
|
60
|
+
'number-at-most',
|
|
61
|
+
'number-between',
|
|
62
|
+
'array-includes',
|
|
63
|
+
'set-overlaps',
|
|
64
|
+
'contains-any',
|
|
65
|
+
'contains-all',
|
|
66
|
+
]);
|
|
67
|
+
const EVALUATOR_CONTEXT_INPUTS = new Set(['case', 'inputs', 'expected', 'outputs', 'run']);
|
|
68
|
+
function validateSuiteShape(value, index) {
|
|
69
|
+
const path = `evaluations.suites[${index}]`;
|
|
70
|
+
const suite = requireRecord(value, path);
|
|
71
|
+
for (const key of ['id', 'targetGraphId', 'datasetId'])
|
|
72
|
+
requireString(suite[key], `${path}.${key}`);
|
|
73
|
+
requireText(suite.name, `${path}.name`);
|
|
74
|
+
if (suite.evaluationMode !== undefined &&
|
|
75
|
+
suite.evaluationMode !== 'pass-fail' &&
|
|
76
|
+
suite.evaluationMode !== 'scoring') {
|
|
77
|
+
throw new Error(`${path}.evaluationMode must be "pass-fail" or "scoring".`);
|
|
78
|
+
}
|
|
79
|
+
for (const [bindingIndex, bindingValue] of requireArray(suite.inputBindings, `${path}.inputBindings`).entries()) {
|
|
80
|
+
const binding = requireRecord(bindingValue, `${path}.inputBindings[${bindingIndex}]`);
|
|
81
|
+
requireString(binding.graphInputId, `${path}.inputBindings[${bindingIndex}].graphInputId`);
|
|
82
|
+
requireString(binding.datasetFieldId, `${path}.inputBindings[${bindingIndex}].datasetFieldId`);
|
|
83
|
+
}
|
|
84
|
+
for (const [assertionIndex, assertionValue] of requireArray(suite.assertions, `${path}.assertions`).entries()) {
|
|
85
|
+
const assertionPath = `${path}.assertions[${assertionIndex}]`;
|
|
86
|
+
const assertion = requireRecord(assertionValue, assertionPath);
|
|
87
|
+
for (const key of ['id', 'outputPath'])
|
|
88
|
+
requireString(assertion[key], `${assertionPath}.${key}`);
|
|
89
|
+
requireText(assertion.name, `${assertionPath}.name`);
|
|
90
|
+
if (typeof assertion.operator !== 'string' || !ASSERTION_OPERATORS.has(assertion.operator)) {
|
|
91
|
+
throw new Error(`${assertionPath}.operator is not supported.`);
|
|
92
|
+
}
|
|
93
|
+
validateOptionalPrimitive(assertion, 'required', 'boolean', assertionPath);
|
|
94
|
+
const expected = requireRecord(assertion.expected, `${assertionPath}.expected`);
|
|
95
|
+
if (expected.kind === 'literal') {
|
|
96
|
+
if (!Object.hasOwn(expected, 'value'))
|
|
97
|
+
throw new Error(`${assertionPath}.expected.value is required.`);
|
|
98
|
+
}
|
|
99
|
+
else if (expected.kind === 'dataset-field') {
|
|
100
|
+
requireString(expected.fieldId, `${assertionPath}.expected.fieldId`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw new Error(`${assertionPath}.expected.kind must be "literal" or "dataset-field".`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [evaluatorIndex, evaluatorValue] of requireArray(suite.evaluators, `${path}.evaluators`).entries()) {
|
|
107
|
+
const evaluatorPath = `${path}.evaluators[${evaluatorIndex}]`;
|
|
108
|
+
const evaluator = requireRecord(evaluatorValue, evaluatorPath);
|
|
109
|
+
for (const key of ['id', 'graphId'])
|
|
110
|
+
requireString(evaluator[key], `${evaluatorPath}.${key}`);
|
|
111
|
+
requireText(evaluator.name, `${evaluatorPath}.name`);
|
|
112
|
+
validateOptionalPrimitive(evaluator, 'required', 'boolean', evaluatorPath);
|
|
113
|
+
validateOptionalPrimitive(evaluator, 'runOnTargetError', 'boolean', evaluatorPath);
|
|
114
|
+
validateOptionalPrimitive(evaluator, 'scoreWeight', 'number', evaluatorPath);
|
|
115
|
+
if (evaluator.inputBindings !== undefined) {
|
|
116
|
+
for (const [bindingIndex, bindingValue] of requireArray(evaluator.inputBindings, `${evaluatorPath}.inputBindings`).entries()) {
|
|
117
|
+
const bindingPath = `${evaluatorPath}.inputBindings[${bindingIndex}]`;
|
|
118
|
+
const binding = requireRecord(bindingValue, bindingPath);
|
|
119
|
+
requireString(binding.graphInputId, `${bindingPath}.graphInputId`);
|
|
120
|
+
const source = requireRecord(binding.source, `${bindingPath}.source`);
|
|
121
|
+
if (source.kind === 'dataset-field')
|
|
122
|
+
requireString(source.fieldId, `${bindingPath}.source.fieldId`);
|
|
123
|
+
else if (source.kind === 'target-output')
|
|
124
|
+
requireString(source.outputId, `${bindingPath}.source.outputId`);
|
|
125
|
+
else if (source.kind === 'context') {
|
|
126
|
+
if (typeof source.context !== 'string' || !EVALUATOR_CONTEXT_INPUTS.has(source.context)) {
|
|
127
|
+
throw new Error(`${bindingPath}.source.context is not supported.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
throw new Error(`${bindingPath}.source.kind is not supported.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (suite.thresholds !== undefined) {
|
|
137
|
+
for (const [thresholdIndex, thresholdValue] of requireArray(suite.thresholds, `${path}.thresholds`).entries()) {
|
|
138
|
+
const thresholdPath = `${path}.thresholds[${thresholdIndex}]`;
|
|
139
|
+
const threshold = requireRecord(thresholdValue, thresholdPath);
|
|
140
|
+
for (const key of ['id', 'metric', 'operator'])
|
|
141
|
+
requireString(threshold[key], `${thresholdPath}.${key}`);
|
|
142
|
+
requireFiniteNumber(threshold.value, `${thresholdPath}.value`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (suite.configuration !== undefined) {
|
|
146
|
+
const configuration = requireRecord(suite.configuration, `${path}.configuration`);
|
|
147
|
+
for (const key of ['trialCount', 'concurrency', 'timeoutMs', 'seed']) {
|
|
148
|
+
validateOptionalPrimitive(configuration, key, 'number', `${path}.configuration`);
|
|
149
|
+
}
|
|
150
|
+
validateOptionalPrimitive(configuration, 'seedGraphInputId', 'string', `${path}.configuration`);
|
|
151
|
+
validateOptionalPrimitive(configuration, 'recordingRetention', 'string', `${path}.configuration`);
|
|
152
|
+
}
|
|
153
|
+
validateOptionalPrimitive(suite, 'description', 'string', path);
|
|
154
|
+
if (suite.tags !== undefined) {
|
|
155
|
+
for (const [tagIndex, tag] of requireArray(suite.tags, `${path}.tags`).entries()) {
|
|
156
|
+
requireString(tag, `${path}.tags[${tagIndex}]`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function validateNumericRecord(value, path) {
|
|
161
|
+
const record = requireRecord(value, path);
|
|
162
|
+
for (const [key, entry] of Object.entries(record))
|
|
163
|
+
requireFiniteNumber(entry, `${path}.${key}`);
|
|
164
|
+
}
|
|
165
|
+
function validateBaselineShape(value, index) {
|
|
166
|
+
const path = `evaluations.baselines[${index}]`;
|
|
167
|
+
const baseline = requireRecord(value, path);
|
|
168
|
+
for (const key of ['id', 'suiteId', 'createdAt'])
|
|
169
|
+
requireString(baseline[key], `${path}.${key}`);
|
|
170
|
+
validateOptionalPrimitive(baseline, 'sourceRunId', 'string', path);
|
|
171
|
+
if (baseline.evaluationMode !== undefined &&
|
|
172
|
+
baseline.evaluationMode !== 'pass-fail' &&
|
|
173
|
+
baseline.evaluationMode !== 'scoring') {
|
|
174
|
+
throw new Error(`${path}.evaluationMode must be "pass-fail" or "scoring".`);
|
|
175
|
+
}
|
|
176
|
+
const provenance = requireRecord(baseline.provenance, `${path}.provenance`);
|
|
177
|
+
for (const key of [
|
|
178
|
+
'projectFingerprint',
|
|
179
|
+
'suiteFingerprint',
|
|
180
|
+
'datasetFingerprint',
|
|
181
|
+
'targetFingerprint',
|
|
182
|
+
'executionMode',
|
|
183
|
+
]) {
|
|
184
|
+
requireString(provenance[key], `${path}.provenance.${key}`);
|
|
185
|
+
}
|
|
186
|
+
const evaluatorFingerprints = requireRecord(provenance.evaluatorFingerprints, `${path}.provenance.evaluatorFingerprints`);
|
|
187
|
+
for (const [key, fingerprint] of Object.entries(evaluatorFingerprints)) {
|
|
188
|
+
requireString(fingerprint, `${path}.provenance.evaluatorFingerprints.${key}`);
|
|
189
|
+
}
|
|
190
|
+
requireBoolean(provenance.accountingComplete, `${path}.provenance.accountingComplete`);
|
|
191
|
+
const aggregate = requireRecord(baseline.aggregate, `${path}.aggregate`);
|
|
192
|
+
for (const key of [
|
|
193
|
+
'trialCount',
|
|
194
|
+
'passedTrialCount',
|
|
195
|
+
'failedTrialCount',
|
|
196
|
+
'erroredTrialCount',
|
|
197
|
+
'canceledTrialCount',
|
|
198
|
+
'passRate',
|
|
199
|
+
'averageLatencyMs',
|
|
200
|
+
'p95LatencyMs',
|
|
201
|
+
'targetErrorRate',
|
|
202
|
+
'evaluatorErrorRate',
|
|
203
|
+
'toolFailureRate',
|
|
204
|
+
]) {
|
|
205
|
+
requireFiniteNumber(aggregate[key], `${path}.aggregate.${key}`);
|
|
206
|
+
}
|
|
207
|
+
for (const key of [
|
|
208
|
+
'evaluatedTrialCount',
|
|
209
|
+
'notEvaluatedTrialCount',
|
|
210
|
+
'unableToEvaluateTrialCount',
|
|
211
|
+
'meanScore',
|
|
212
|
+
'totalCostUsd',
|
|
213
|
+
'averageCostUsd',
|
|
214
|
+
'scoredTrialCount',
|
|
215
|
+
'missingScoreTrialCount',
|
|
216
|
+
]) {
|
|
217
|
+
validateOptionalPrimitive(aggregate, key, 'number', `${path}.aggregate`);
|
|
218
|
+
}
|
|
219
|
+
validateNumericRecord(aggregate.metrics, `${path}.aggregate.metrics`);
|
|
220
|
+
for (const [caseIndex, caseValue] of requireArray(baseline.cases, `${path}.cases`).entries()) {
|
|
221
|
+
const casePath = `${path}.cases[${caseIndex}]`;
|
|
222
|
+
const caseAggregate = requireRecord(caseValue, casePath);
|
|
223
|
+
requireString(caseAggregate.caseId, `${casePath}.caseId`);
|
|
224
|
+
requireString(caseAggregate.caseName, `${casePath}.caseName`);
|
|
225
|
+
validateNumericRecord(caseAggregate.metrics, `${casePath}.metrics`);
|
|
226
|
+
for (const key of [
|
|
227
|
+
'passRate',
|
|
228
|
+
'meanScore',
|
|
229
|
+
'evaluatedTrialCount',
|
|
230
|
+
'passedTrialCount',
|
|
231
|
+
'failedTrialCount',
|
|
232
|
+
'notEvaluatedTrialCount',
|
|
233
|
+
'unableToEvaluateTrialCount',
|
|
234
|
+
'erroredTrialCount',
|
|
235
|
+
'canceledTrialCount',
|
|
236
|
+
'scoredTrialCount',
|
|
237
|
+
'missingScoreTrialCount',
|
|
238
|
+
]) {
|
|
239
|
+
validateOptionalPrimitive(caseAggregate, key, 'number', casePath);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export function createEmptyEvaluationProjectData() {
|
|
244
|
+
return { version: 1, suites: [], baselines: [] };
|
|
245
|
+
}
|
|
246
|
+
export function serializeEvaluationProjectData(data) {
|
|
247
|
+
if (data.version !== 1)
|
|
248
|
+
throw new Error(`Unsupported evaluation data version: ${data.version}`);
|
|
249
|
+
const persistentData = stripLegacyEditorSelections(data);
|
|
250
|
+
assertPortableJson(persistentData);
|
|
251
|
+
return {
|
|
252
|
+
...structuredClone(persistentData),
|
|
253
|
+
baselines: persistentData.baselines.map(normalizeEvaluationBaselineSnapshot),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export function deserializeEvaluationProjectData(data) {
|
|
257
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data))
|
|
258
|
+
throw new Error('Evaluation project data must be an object.');
|
|
259
|
+
const candidate = data;
|
|
260
|
+
if (candidate.version !== 1 || !Array.isArray(candidate.suites) || !Array.isArray(candidate.baselines)) {
|
|
261
|
+
throw new Error('Unsupported or invalid evaluation project data.');
|
|
262
|
+
}
|
|
263
|
+
const persistentData = stripLegacyEditorSelections(candidate);
|
|
264
|
+
assertPortableJson(persistentData);
|
|
265
|
+
persistentData.suites.forEach(validateSuiteShape);
|
|
266
|
+
persistentData.baselines.forEach(validateBaselineShape);
|
|
267
|
+
return {
|
|
268
|
+
...structuredClone(persistentData),
|
|
269
|
+
baselines: persistentData.baselines.map(normalizeEvaluationBaselineSnapshot),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function serializeEvaluationProjectDataToString(data) {
|
|
273
|
+
return yaml.stringify(serializeEvaluationProjectData(data), null, 2);
|
|
274
|
+
}
|
|
275
|
+
export function deserializeEvaluationProjectDataFromString(data) {
|
|
276
|
+
return deserializeEvaluationProjectData(yaml.parse(data));
|
|
277
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { assertEvaluationDatasetSnapshot } from './canonical.js';
|
|
2
|
+
import { normalizeEvaluationRun, shouldReplaceEvaluationRun } from './normalization.js';
|
|
3
|
+
export class InMemoryEvaluationRunStore {
|
|
4
|
+
#runs = new Map();
|
|
5
|
+
#datasetSnapshots = new Map();
|
|
6
|
+
#recordings = new Map();
|
|
7
|
+
async put(run) {
|
|
8
|
+
const normalized = normalizeEvaluationRun(run);
|
|
9
|
+
const key = `${normalized.projectId}/${normalized.id}`;
|
|
10
|
+
const existing = this.#runs.get(key);
|
|
11
|
+
if (!shouldReplaceEvaluationRun(existing, normalized))
|
|
12
|
+
return;
|
|
13
|
+
this.#runs.set(key, structuredClone(normalized));
|
|
14
|
+
}
|
|
15
|
+
async get(input) {
|
|
16
|
+
const run = this.#runs.get(`${input.projectId}/${input.runId}`);
|
|
17
|
+
return run ? normalizeEvaluationRun(run) : undefined;
|
|
18
|
+
}
|
|
19
|
+
async list(input) {
|
|
20
|
+
return Array.from(this.#runs.values())
|
|
21
|
+
.filter((run) => run.projectId === input.projectId && (input.suiteId === undefined || run.suiteId === input.suiteId))
|
|
22
|
+
.sort((left, right) => right.startedAt.localeCompare(left.startedAt))
|
|
23
|
+
.map(normalizeEvaluationRun);
|
|
24
|
+
}
|
|
25
|
+
async delete(input) {
|
|
26
|
+
this.#runs.delete(`${input.projectId}/${input.runId}`);
|
|
27
|
+
for (const [key, artifact] of this.#recordings) {
|
|
28
|
+
if (artifact.projectId === input.projectId && artifact.runId === input.runId)
|
|
29
|
+
this.#recordings.delete(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async putDatasetSnapshot(snapshot) {
|
|
33
|
+
assertEvaluationDatasetSnapshot(snapshot);
|
|
34
|
+
const key = `${snapshot.projectId}/${snapshot.fingerprint}`;
|
|
35
|
+
// A content-addressed historical snapshot must never be rewritten by a
|
|
36
|
+
// later edit of the live evaluation dataset.
|
|
37
|
+
if (this.#datasetSnapshots.has(key))
|
|
38
|
+
return;
|
|
39
|
+
this.#datasetSnapshots.set(key, structuredClone(snapshot));
|
|
40
|
+
}
|
|
41
|
+
async getDatasetSnapshot(input) {
|
|
42
|
+
const snapshot = this.#datasetSnapshots.get(`${input.projectId}/${input.fingerprint}`);
|
|
43
|
+
return snapshot ? structuredClone(snapshot) : undefined;
|
|
44
|
+
}
|
|
45
|
+
async putRecording(artifact) {
|
|
46
|
+
const key = `${artifact.projectId}/${artifact.reference.id}`;
|
|
47
|
+
const existing = this.#recordings.get(key);
|
|
48
|
+
if (existing && (existing.runId !== artifact.runId || existing.trialId !== artifact.trialId)) {
|
|
49
|
+
throw new Error('An evaluation recording ID cannot be reassigned to another run or trial.');
|
|
50
|
+
}
|
|
51
|
+
const next = structuredClone(artifact);
|
|
52
|
+
// Artifact persistence never owns retention transitions. A duplicate or
|
|
53
|
+
// delayed write keeps whatever explicit update/promotion already decided.
|
|
54
|
+
if (existing)
|
|
55
|
+
next.reference = structuredClone(existing.reference);
|
|
56
|
+
this.#recordings.set(key, next);
|
|
57
|
+
}
|
|
58
|
+
async getRecording(input) {
|
|
59
|
+
const artifact = this.#recordings.get(`${input.projectId}/${input.recordingId}`);
|
|
60
|
+
if (!artifact)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (artifact.reference.retention === 'temporary' &&
|
|
63
|
+
artifact.reference.expiresAt &&
|
|
64
|
+
Date.parse(artifact.reference.expiresAt) <= Date.now()) {
|
|
65
|
+
this.#recordings.delete(`${input.projectId}/${input.recordingId}`);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return structuredClone(artifact);
|
|
69
|
+
}
|
|
70
|
+
async updateRecordingRetention(input) {
|
|
71
|
+
const key = `${input.projectId}/${input.recordingId}`;
|
|
72
|
+
const artifact = this.#recordings.get(key);
|
|
73
|
+
if (!artifact)
|
|
74
|
+
return;
|
|
75
|
+
artifact.reference = {
|
|
76
|
+
id: artifact.reference.id,
|
|
77
|
+
retention: input.retention,
|
|
78
|
+
...(input.expiresAt === undefined ? {} : { expiresAt: input.expiresAt }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async promoteBaseline(input) {
|
|
82
|
+
for (const artifact of this.#recordings.values()) {
|
|
83
|
+
if (artifact.projectId !== input.projectId || artifact.runId !== input.runId)
|
|
84
|
+
continue;
|
|
85
|
+
artifact.reference = { id: artifact.reference.id, retention: 'baseline' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { localizeEvaluationDataset, validateEvaluationDatasetForTransfer } from './datasetTransfer.js';
|
|
2
|
+
import { deserializeEvaluationProjectData } from './serialization.js';
|
|
3
|
+
const SUITE_BUNDLE_EXPORT_VERSION = 1;
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function requireNonEmptyString(value, name) {
|
|
8
|
+
if (value.trim().length === 0)
|
|
9
|
+
throw new Error(`${name} must be a non-empty string.`);
|
|
10
|
+
}
|
|
11
|
+
function validateEvaluationSuite(value) {
|
|
12
|
+
return deserializeEvaluationProjectData({ version: 1, suites: [value], baselines: [] }).suites[0];
|
|
13
|
+
}
|
|
14
|
+
function validateBundle(value) {
|
|
15
|
+
if (!isRecord(value) ||
|
|
16
|
+
value.version !== SUITE_BUNDLE_EXPORT_VERSION ||
|
|
17
|
+
!isRecord(value.suite) ||
|
|
18
|
+
!isRecord(value.dataset)) {
|
|
19
|
+
throw new Error(`Expected an evaluation suite export with version ${SUITE_BUNDLE_EXPORT_VERSION}.`);
|
|
20
|
+
}
|
|
21
|
+
const suite = validateEvaluationSuite(value.suite);
|
|
22
|
+
const dataset = validateEvaluationDatasetForTransfer(value.dataset);
|
|
23
|
+
if (suite.datasetId !== dataset.id) {
|
|
24
|
+
throw new Error('The evaluation suite export must include the dataset referenced by its suite.');
|
|
25
|
+
}
|
|
26
|
+
return { suite, dataset };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A portable suite bundle includes the suite definition and its referenced
|
|
30
|
+
* dataset. It intentionally excludes graphs, baselines, recordings, and run
|
|
31
|
+
* history, which belong to the destination project or its run store.
|
|
32
|
+
*/
|
|
33
|
+
export function serializeEvaluationSuiteBundleJson(suite, dataset) {
|
|
34
|
+
const bundle = validateBundle({ version: SUITE_BUNDLE_EXPORT_VERSION, suite, dataset });
|
|
35
|
+
return JSON.stringify({ version: SUITE_BUNDLE_EXPORT_VERSION, ...bundle }, null, 2);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Imports a bundle as new local evaluation resources. Field and case IDs remain stable
|
|
39
|
+
* so the imported suite's bindings and expected-value references still point
|
|
40
|
+
* to its imported dataset; suite and dataset identities are always supplied by
|
|
41
|
+
* the destination to avoid overwriting existing resources.
|
|
42
|
+
*/
|
|
43
|
+
export function deserializeEvaluationSuiteBundleJson(source, scope) {
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(source);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error('Evaluation suite JSON is not valid JSON.');
|
|
50
|
+
}
|
|
51
|
+
requireNonEmptyString(scope.suiteId, 'suiteId');
|
|
52
|
+
requireNonEmptyString(scope.datasetId, 'datasetId');
|
|
53
|
+
const bundle = validateBundle(parsed);
|
|
54
|
+
const dataset = localizeEvaluationDataset(bundle.dataset, scope.datasetId);
|
|
55
|
+
return {
|
|
56
|
+
dataset,
|
|
57
|
+
suite: {
|
|
58
|
+
...bundle.suite,
|
|
59
|
+
id: scope.suiteId,
|
|
60
|
+
datasetId: dataset.id,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lets a target adapter preserve its metrics and replay reference when a
|
|
3
|
+
* graph itself fails. The runner still classifies that as a target failure.
|
|
4
|
+
*/
|
|
5
|
+
export class EvaluationGraphExecutionError extends Error {
|
|
6
|
+
metrics;
|
|
7
|
+
recording;
|
|
8
|
+
providerAttempts;
|
|
9
|
+
constructor(message, details = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'EvaluationGraphExecutionError';
|
|
12
|
+
this.metrics = details.metrics;
|
|
13
|
+
this.recording = details.recording;
|
|
14
|
+
this.providerAttempts = details.providerAttempts;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EvaluationAssertion, EvaluationDatasetCase, EvaluationObservation, PortableJson } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates the exact JSON-path subset supported by evaluation assertions.
|
|
4
|
+
* UI authoring and runtime execution intentionally share this parser so a
|
|
5
|
+
* path cannot pass preflight and then resolve with different grammar.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isEvaluationOutputPathSyntaxValid(path: string): boolean;
|
|
8
|
+
/** The Graph Output that owns an assertion path, including nested paths. */
|
|
9
|
+
export declare function getEvaluationTopLevelOutputId(path: string): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Validates author-controlled expected data without needing target outputs.
|
|
12
|
+
* The runner uses this before scheduling work; assertion execution uses the
|
|
13
|
+
* same function so direct callers cannot observe a looser contract.
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateEvaluationAssertionExpectedValue(assertion: EvaluationAssertion, expected: PortableJson): void;
|
|
16
|
+
export declare function validateEvaluationAssertionConfiguration(assertion: EvaluationAssertion, expected: PortableJson): void;
|
|
17
|
+
export declare function evaluateAssertion(assertion: EvaluationAssertion, outputs: Record<string, PortableJson>, testCase: EvaluationDatasetCase): EvaluationObservation;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EvaluationDatasetSnapshot, PortableJson } from './types.js';
|
|
2
|
+
export declare function assertPortableJson(value: unknown, path?: string, stack?: Set<object>): asserts value is PortableJson;
|
|
3
|
+
export declare function canonicalizePortableJson(value: unknown): PortableJson;
|
|
4
|
+
/**
|
|
5
|
+
* Evaluation inputs and observations cross concurrent graph-run boundaries.
|
|
6
|
+
* Clone them before handing them to an adapter so a Code or evaluator graph
|
|
7
|
+
* cannot mutate a dataset case, another trial, or the persisted run model.
|
|
8
|
+
*/
|
|
9
|
+
export declare function clonePortableJson<T extends PortableJson>(value: T): T;
|
|
10
|
+
export declare function canonicalStringify(value: unknown): string;
|
|
11
|
+
/** Stable, synchronously available content fingerprint for project-local identities. */
|
|
12
|
+
export declare function fingerprint(value: unknown): string;
|
|
13
|
+
export declare function fingerprintEvaluationDataset(value: {
|
|
14
|
+
id: unknown;
|
|
15
|
+
fields: unknown;
|
|
16
|
+
cases: unknown;
|
|
17
|
+
}): string;
|
|
18
|
+
/**
|
|
19
|
+
* A dataset fingerprint is the link between a compact run summary and its
|
|
20
|
+
* historical input cases. Stores must reject a mismatched key rather than
|
|
21
|
+
* accepting a snapshot that can no longer prove which cases were run.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertEvaluationDatasetSnapshot(snapshot: EvaluationDatasetSnapshot): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EvaluationDataset, PortableJson } from './types.js';
|
|
2
|
+
export declare const portableEvaluationDataTypes: readonly ["any", "any[]", "string", "string[]", "date", "date[]", "time", "time[]", "datetime", "datetime[]", "number", "number[]", "vector", "vector[]", "boolean", "boolean[]", "object", "object[]"];
|
|
3
|
+
export declare function isPortableEvaluationDataType(dataType: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Checks a portable dataset value against the Rivet data type selected for an
|
|
6
|
+
* evaluation field. This is shared by authoring UI and execution validation so
|
|
7
|
+
* a value cannot look valid in the editor and then fail under a different rule
|
|
8
|
+
* in the runner.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isEvaluationValueCompatibleWithDataType(value: PortableJson, dataType: string): boolean;
|
|
11
|
+
/** Shared declared-type check for graph/dataset binding authoring and execution validation. */
|
|
12
|
+
export declare function areEvaluationDataTypesCompatible(sourceDataType: string, targetDataType: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Enforces the declared portable JSON contract at explicit transfer
|
|
15
|
+
* boundaries. Durable local drafts remain recoverable even if an older build
|
|
16
|
+
* wrote an unsupported type; imports and exports fail with the precise field
|
|
17
|
+
* and case that needs repair instead of deferring the error until execution.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertEvaluationDatasetValuesMatchDeclaredTypes(dataset: EvaluationDataset): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EvaluationDataset } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates durable evaluation datasets and portable imports. `projectId` is
|
|
4
|
+
* accepted solely for backwards-compatible migration from project-owned
|
|
5
|
+
* datasets; newly created local datasets omit it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateEvaluationDataset(value: unknown, scope?: Pick<EvaluationDataset, 'id' | 'projectId'>): EvaluationDataset;
|
|
8
|
+
/**
|
|
9
|
+
* Converts a legacy project-owned dataset into the application-local form.
|
|
10
|
+
* Field and case identity stay stable, so existing suite bindings continue to
|
|
11
|
+
* work after migration.
|
|
12
|
+
*/
|
|
13
|
+
export declare function localizeEvaluationDataset(value: unknown, id?: string): EvaluationDataset;
|
|
14
|
+
/** Strict validation for explicit portable import and export boundaries. */
|
|
15
|
+
export declare function validateEvaluationDatasetForTransfer(value: unknown, scope?: Pick<EvaluationDataset, 'id' | 'projectId'>): EvaluationDataset;
|
|
16
|
+
/**
|
|
17
|
+
* Portable interchange envelope for a complete evaluation dataset. CSV is
|
|
18
|
+
* useful for case rows; JSON is the lossless form that also carries field
|
|
19
|
+
* roles and Rivet data types.
|
|
20
|
+
*/
|
|
21
|
+
export declare function serializeEvaluationDatasetJson(dataset: EvaluationDataset): string;
|
|
22
|
+
/**
|
|
23
|
+
* Reads a dataset export into a destination dataset identity. Import keeps
|
|
24
|
+
* incoming field/case IDs but never lets a file overwrite another resource.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deserializeEvaluationDatasetJson(source: string, scope: Pick<EvaluationDataset, 'id'>): EvaluationDataset;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './canonical.js';
|
|
3
|
+
export * from './assertions.js';
|
|
4
|
+
export * from './runner.js';
|
|
5
|
+
export * from './store.js';
|
|
6
|
+
export * from './serialization.js';
|
|
7
|
+
export * from './datasetTransfer.js';
|
|
8
|
+
export * from './suiteTransfer.js';
|
|
9
|
+
export * from './dataTypes.js';
|
|
10
|
+
export * from './normalization.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EvaluationAggregate, EvaluationBaselineSnapshot, EvaluationRun, EvaluationRunPurpose, EvaluationSuiteMode, EvaluationTrial } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Orders snapshots for one run without allowing an equal-revision progress
|
|
4
|
+
* update to demote a terminal result. Writers and delayed UI reads share this
|
|
5
|
+
* rule so persistence cannot disagree with what the workspace presents.
|
|
6
|
+
*/
|
|
7
|
+
export declare function shouldReplaceEvaluationRun(existing: Pick<EvaluationRun, 'executionStatus' | 'revision'> | undefined, incoming: Pick<EvaluationRun, 'executionStatus' | 'revision'>): boolean;
|
|
8
|
+
export declare function normalizeEvaluationTrial(value: EvaluationTrial | unknown, purpose?: EvaluationRunPurpose, evaluationMode?: EvaluationSuiteMode): EvaluationTrial;
|
|
9
|
+
export declare function normalizeEvaluationAggregate(value: EvaluationAggregate | unknown, trials?: readonly EvaluationTrial[]): EvaluationAggregate;
|
|
10
|
+
export declare function normalizeEvaluationBaselineSnapshot(value: EvaluationBaselineSnapshot | unknown): EvaluationBaselineSnapshot;
|
|
11
|
+
export declare function normalizeEvaluationRun(value: EvaluationRun | unknown): EvaluationRun;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EvaluationAggregate, EvaluationBaselineSnapshot, EvaluationCaseAggregate, EvaluationDataset, EvaluationExecutionMetrics, EvaluationGraphRunner, EvaluationGraphEvaluator, EvaluationProjectData, EvaluationRun, EvaluationRunPurpose, EvaluationSuite, EvaluationSuiteMode, EvaluationThreshold, EvaluationThresholdResult } from './types.js';
|
|
2
|
+
import type { Project } from '@valerypopoff/rivet2-core';
|
|
3
|
+
export declare const LEGACY_EVALUATOR_INPUT_IDS: readonly ["case", "inputs", "expected", "outputs", "run"];
|
|
4
|
+
export type RunEvaluationSuiteOptions = {
|
|
5
|
+
project: Project;
|
|
6
|
+
evaluationData: EvaluationProjectData;
|
|
7
|
+
dataset: EvaluationDataset;
|
|
8
|
+
suiteId: string;
|
|
9
|
+
runGraph: EvaluationGraphRunner;
|
|
10
|
+
projectFingerprint?: string;
|
|
11
|
+
executionMode?: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
runId?: string;
|
|
14
|
+
/** An evaluation judges quality; a benchmark deliberately measures execution only. */
|
|
15
|
+
purpose?: EvaluationRunPurpose;
|
|
16
|
+
/** Explicit caller selection; otherwise the suite's saved baseline is used. */
|
|
17
|
+
baseline?: EvaluationBaselineSnapshot;
|
|
18
|
+
onUpdate?: (run: EvaluationRun) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function hasAuthoritativeEvaluationCriteria(suite: EvaluationSuite): boolean;
|
|
21
|
+
/** Older saved suites predate scoring and therefore retain pass/fail semantics. */
|
|
22
|
+
export declare function getEvaluationSuiteMode(suite: Pick<EvaluationSuite, 'evaluationMode'>): EvaluationSuiteMode;
|
|
23
|
+
/** Existing evaluators with the complete reserved-input contract keep working without migration. */
|
|
24
|
+
export declare function usesLegacyEvaluatorInputEnvelope(evaluator: Pick<EvaluationGraphEvaluator, 'inputBindings'>, graphInputIds: readonly string[]): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Shared bounded work scheduler for evaluation adapters that need stable
|
|
27
|
+
* result ordering without serializing independent cases. It deliberately has
|
|
28
|
+
* no retry policy: callers own their authoritative execution semantics.
|
|
29
|
+
*
|
|
30
|
+
* The result array is aligned to `work`; entries that were still queued when
|
|
31
|
+
* the signal was canceled stay `undefined`. Completed work is never erased.
|
|
32
|
+
*/
|
|
33
|
+
export type RunEvaluationWorkPoolOptions<TWork, TResult> = {
|
|
34
|
+
work: readonly TWork[];
|
|
35
|
+
concurrency: number;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
execute: (work: TWork, index: number) => Promise<TResult>;
|
|
38
|
+
onSettled?: (result: TResult, index: number, results: readonly (TResult | undefined)[]) => void;
|
|
39
|
+
};
|
|
40
|
+
export declare function runEvaluationWorkPool<TWork, TResult>(options: RunEvaluationWorkPoolOptions<TWork, TResult>): Promise<readonly (TResult | undefined)[]>;
|
|
41
|
+
export declare function createEmptyMetrics(): EvaluationExecutionMetrics;
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the provisional recording references created while a suite is
|
|
44
|
+
* running into its final retention policy. The caller persists the returned
|
|
45
|
+
* references through its EvaluationRunStore.
|
|
46
|
+
*
|
|
47
|
+
* A successful candidate recording remains promotable for 24 hours by
|
|
48
|
+
* default. Failed/error trials are retained, and `all` pins every artifact.
|
|
49
|
+
* This deliberately operates per trial: one failed case must not pin every
|
|
50
|
+
* otherwise-successful recording in the suite.
|
|
51
|
+
*/
|
|
52
|
+
export declare function finalizeEvaluationRecordingRetention(run: EvaluationRun, policy?: 'failures-and-baselines' | 'all', now?: number): EvaluationRun;
|
|
53
|
+
export declare function evaluateThresholdResults(aggregateValue: EvaluationAggregate, thresholds?: EvaluationThreshold[], baseline?: EvaluationBaselineSnapshot): EvaluationThresholdResult[];
|
|
54
|
+
export declare function runEvaluationSuite(options: RunEvaluationSuiteOptions): Promise<EvaluationRun>;
|
|
55
|
+
export declare function runEvaluationCases(options: RunEvaluationSuiteOptions & {
|
|
56
|
+
caseIds: readonly string[];
|
|
57
|
+
}): Promise<EvaluationRun>;
|
|
58
|
+
export declare function summarizeEvaluationRun(run: EvaluationRun): {
|
|
59
|
+
aggregate: EvaluationAggregate;
|
|
60
|
+
cases: EvaluationCaseAggregate[];
|
|
61
|
+
} | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Baselines deliberately retain aggregate/provenance only. Raw prompts,
|
|
64
|
+
* outputs, credentials, and replay artifacts remain in the run store.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createEvaluationBaselineSnapshot(run: EvaluationRun): EvaluationBaselineSnapshot;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EvaluationProjectData } from './types.js';
|
|
2
|
+
export declare function createEmptyEvaluationProjectData(): EvaluationProjectData;
|
|
3
|
+
export declare function serializeEvaluationProjectData(data: EvaluationProjectData): EvaluationProjectData;
|
|
4
|
+
export declare function deserializeEvaluationProjectData(data: unknown): EvaluationProjectData;
|
|
5
|
+
export declare function serializeEvaluationProjectDataToString(data: EvaluationProjectData): string;
|
|
6
|
+
export declare function deserializeEvaluationProjectDataFromString(data: string): EvaluationProjectData;
|