jev-recipes 0.8.4 → 0.9.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/README.md +35 -3
- package/dist/adapters/ai-sdk/index.d.ts +38 -0
- package/dist/adapters/ai-sdk/index.js +88 -0
- package/dist/adapters/langchain/index.d.ts +24 -0
- package/dist/adapters/langchain/index.js +42 -0
- package/dist/adapters/shared.d.ts +88 -0
- package/dist/adapters/shared.js +132 -0
- package/dist/catalog/evidence.d.ts +4 -1
- package/dist/catalog/generated/metadata.js +171 -259
- package/dist/evaluation/archive.d.ts +9 -1
- package/dist/evaluation/archive.js +41 -7
- package/dist/evaluation/comparison.d.ts +8 -0
- package/dist/evaluation/comparison.js +83 -0
- package/dist/evaluation/dataset.js +5 -34
- package/dist/evaluation/decisions.d.ts +0 -2
- package/dist/evaluation/decisions.js +0 -22
- package/dist/evaluation/engine.js +4 -3
- package/dist/evaluation/report.d.ts +3 -0
- package/dist/evaluation/report.js +6 -4
- package/dist/src/batch.js +14 -1
- package/package.json +27 -2
|
@@ -4,7 +4,13 @@ import type { EvaluationRow } from './engine.js';
|
|
|
4
4
|
import { buildReport } from './report.js';
|
|
5
5
|
import type { EvaluationOptions, EvaluationPolicy } from './schema.js';
|
|
6
6
|
declare const archiveSchema: z.ZodObject<{
|
|
7
|
-
format: z.ZodLiteral<
|
|
7
|
+
format: z.ZodLiteral<2>;
|
|
8
|
+
scoringRevision: z.ZodLiteral<2>;
|
|
9
|
+
sourceMode: z.ZodEnum<{
|
|
10
|
+
fixture: "fixture";
|
|
11
|
+
live: "live";
|
|
12
|
+
}>;
|
|
13
|
+
evaluatedAt: z.ZodISODateTime;
|
|
8
14
|
runId: z.ZodString;
|
|
9
15
|
createdAt: z.ZodISODateTime;
|
|
10
16
|
mode: z.ZodEnum<{
|
|
@@ -125,6 +131,7 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
|
|
|
125
131
|
} | undefined;
|
|
126
132
|
};
|
|
127
133
|
mode: "fixture" | "live" | "replay";
|
|
134
|
+
scoringRevision: 2;
|
|
128
135
|
report: {
|
|
129
136
|
contestedAccuracy: number | null;
|
|
130
137
|
adversarialAccuracy: number | null;
|
|
@@ -310,6 +317,7 @@ export declare function compare(baseline: EvaluationRun, candidate: EvaluationRu
|
|
|
310
317
|
} | undefined;
|
|
311
318
|
};
|
|
312
319
|
mode: "fixture" | "live" | "replay";
|
|
320
|
+
scoringRevision: 2;
|
|
313
321
|
report: {
|
|
314
322
|
contestedAccuracy: number | null;
|
|
315
323
|
adversarialAccuracy: number | null;
|
|
@@ -7,8 +7,13 @@ import { datasetFingerprints, fingerprint, loadEvaluationRecipe, recipeFingerpri
|
|
|
7
7
|
import { confidenceThresholds, evaluateCases } from './engine.js';
|
|
8
8
|
import { buildReport } from './report.js';
|
|
9
9
|
import { evaluationCaseSchema, evaluationOptionsSchema, evaluationPolicySchema, priceSchema, } from './schema.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { scoringRevision } from './comparison.js';
|
|
11
|
+
const archiveSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
format: z.literal(2),
|
|
14
|
+
scoringRevision: z.literal(scoringRevision),
|
|
15
|
+
sourceMode: z.enum(['live', 'fixture']),
|
|
16
|
+
evaluatedAt: z.iso.datetime(),
|
|
12
17
|
runId: z.string(),
|
|
13
18
|
createdAt: z.iso.datetime(),
|
|
14
19
|
mode: z.enum(['live', 'fixture', 'replay']),
|
|
@@ -53,6 +58,21 @@ const archiveSchema = z.object({
|
|
|
53
58
|
thresholds: z.array(z.number()),
|
|
54
59
|
sourceRun: z.string().optional(),
|
|
55
60
|
report: z.record(z.string(), z.unknown()),
|
|
61
|
+
})
|
|
62
|
+
.superRefine((run, context) => {
|
|
63
|
+
if (run.mode !== 'replay' && (run.sourceMode !== run.mode || run.evaluatedAt !== run.createdAt))
|
|
64
|
+
context.addIssue({
|
|
65
|
+
code: 'custom',
|
|
66
|
+
message: 'Direct runs must retain their own response mode and date.',
|
|
67
|
+
});
|
|
68
|
+
for (const [rowIndex, row] of run.rows.entries())
|
|
69
|
+
for (const [exchangeIndex, exchange] of row.exchanges.entries())
|
|
70
|
+
if (Object.hasOwn(exchange, 'response') === Object.hasOwn(exchange, 'error'))
|
|
71
|
+
context.addIssue({
|
|
72
|
+
code: 'custom',
|
|
73
|
+
path: ['rows', rowIndex, 'exchanges', exchangeIndex],
|
|
74
|
+
message: 'A completed exchange must contain exactly one response or error.',
|
|
75
|
+
});
|
|
56
76
|
});
|
|
57
77
|
export async function evaluate(name, values, options = {}) {
|
|
58
78
|
const settings = evaluationOptionsSchema.parse(options);
|
|
@@ -73,10 +93,14 @@ export async function evaluate(name, values, options = {}) {
|
|
|
73
93
|
throw new Error('The frozen policy belongs to a different recipe version or requested model.');
|
|
74
94
|
}
|
|
75
95
|
const manifest = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
96
|
+
const createdAt = new Date().toISOString();
|
|
76
97
|
const metadata = {
|
|
77
|
-
format:
|
|
98
|
+
format: 2,
|
|
99
|
+
scoringRevision,
|
|
100
|
+
sourceMode: options.mode ?? 'live',
|
|
101
|
+
evaluatedAt: createdAt,
|
|
78
102
|
runId: randomUUID(),
|
|
79
|
-
createdAt
|
|
103
|
+
createdAt,
|
|
80
104
|
mode: options.mode ?? 'live',
|
|
81
105
|
packageVersion: manifest.version,
|
|
82
106
|
recipe: recipe.id,
|
|
@@ -130,6 +154,8 @@ export async function replay(run, policy = run.policy, out) {
|
|
|
130
154
|
const cases = validateCases(recipe, run.cases);
|
|
131
155
|
const metadata = {
|
|
132
156
|
...run,
|
|
157
|
+
format: 2,
|
|
158
|
+
scoringRevision,
|
|
133
159
|
runId: randomUUID(),
|
|
134
160
|
createdAt: new Date().toISOString(),
|
|
135
161
|
mode: 'replay',
|
|
@@ -158,8 +184,10 @@ export async function readRun(directory) {
|
|
|
158
184
|
throw error;
|
|
159
185
|
return gunzipSync(await readFile(join(directory, 'run.json.gz'))).toString('utf8');
|
|
160
186
|
});
|
|
161
|
-
const
|
|
162
|
-
|
|
187
|
+
const raw = JSON.parse(content);
|
|
188
|
+
if (raw.format !== 2)
|
|
189
|
+
throw new Error('Unsupported archive format. This evaluator reads format 2; historical files remain available for direct inspection.');
|
|
190
|
+
const run = archiveSchema.parse(raw);
|
|
163
191
|
const hashes = datasetFingerprints(run.cases);
|
|
164
192
|
if (Object.entries(hashes).some(([key, hash]) => run[key] !== hash))
|
|
165
193
|
throw new Error('Archive dataset fingerprints do not match its cases.');
|
|
@@ -196,7 +224,10 @@ function reportEvidence(run) {
|
|
|
196
224
|
}
|
|
197
225
|
return {
|
|
198
226
|
runId: run.runId,
|
|
199
|
-
|
|
227
|
+
sourceMode: run.sourceMode,
|
|
228
|
+
evaluatedAt: run.evaluatedAt,
|
|
229
|
+
createdAt: run.createdAt,
|
|
230
|
+
scoringRevision: run.scoringRevision,
|
|
200
231
|
packageVersion: run.packageVersion,
|
|
201
232
|
recipeFingerprint: run.recipeFingerprint,
|
|
202
233
|
datasetFingerprint: run.datasetFingerprint,
|
|
@@ -207,6 +238,8 @@ function reportEvidence(run) {
|
|
|
207
238
|
};
|
|
208
239
|
}
|
|
209
240
|
export function compare(baseline, candidate) {
|
|
241
|
+
if (baseline.scoringRevision !== candidate.scoringRevision)
|
|
242
|
+
throw new Error('Comparison requires the same scoring revision. Replay compatible archives before comparing.');
|
|
210
243
|
if (baseline.recipe !== candidate.recipe ||
|
|
211
244
|
baseline.inputFingerprint !== candidate.inputFingerprint ||
|
|
212
245
|
baseline.answerKeyFingerprint !== candidate.answerKeyFingerprint)
|
|
@@ -220,6 +253,7 @@ export function compare(baseline, candidate) {
|
|
|
220
253
|
models: run.report.models,
|
|
221
254
|
policy: run.policy,
|
|
222
255
|
mode: run.mode,
|
|
256
|
+
scoringRevision: run.scoringRevision,
|
|
223
257
|
report: run.report,
|
|
224
258
|
});
|
|
225
259
|
return {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const scoringRevision: 2;
|
|
2
|
+
type Schema = Record<string, unknown>;
|
|
3
|
+
export declare function resultSchemaFor(recipe: string): Schema;
|
|
4
|
+
export declare function comparableDecision(value: unknown, recipe: string): unknown;
|
|
5
|
+
export declare function matchesExpected(actual: unknown, expected: unknown, recipe: string): boolean;
|
|
6
|
+
export declare function isMetadataPath(schema: Schema, segments: string[]): boolean;
|
|
7
|
+
export declare function schemaAtPath(schema: Schema, segments: string[]): Schema | undefined;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { describeRecipe } from '../catalog/index.js';
|
|
3
|
+
import { recipeNameSchema } from '../catalog/schema.js';
|
|
4
|
+
import { isRecord } from './decisions.js';
|
|
5
|
+
export const scoringRevision = 2;
|
|
6
|
+
const metadataFields = new Set([
|
|
7
|
+
'model',
|
|
8
|
+
'usage',
|
|
9
|
+
'confidence',
|
|
10
|
+
'probability',
|
|
11
|
+
'probabilities',
|
|
12
|
+
'score',
|
|
13
|
+
'status',
|
|
14
|
+
]);
|
|
15
|
+
const resultSchemas = new Map();
|
|
16
|
+
export function resultSchemaFor(recipe) {
|
|
17
|
+
const cached = resultSchemas.get(recipe);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
const schema = describeRecipe(recipeNameSchema.parse(recipe)).resultSchema;
|
|
21
|
+
resultSchemas.set(recipe, schema);
|
|
22
|
+
return schema;
|
|
23
|
+
}
|
|
24
|
+
// Expected keys are result paths. Below an unconstrained payload, every key is data.
|
|
25
|
+
export function comparableDecision(value, recipe) {
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return value;
|
|
28
|
+
const schema = resultSchemaFor(recipe);
|
|
29
|
+
return Object.fromEntries(Object.entries(value)
|
|
30
|
+
.filter(([path]) => !isMetadataPath(schema, path.split('.')))
|
|
31
|
+
.map(([path, field]) => [path, normalize(field, schemaAtPath(schema, path.split('.')))]));
|
|
32
|
+
}
|
|
33
|
+
export function matchesExpected(actual, expected, recipe) {
|
|
34
|
+
return isDeepStrictEqual(comparableDecision(actual, recipe), comparableDecision(expected, recipe));
|
|
35
|
+
}
|
|
36
|
+
function normalize(value, schema) {
|
|
37
|
+
if (!schema)
|
|
38
|
+
return value;
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return value.map((item, index) => normalize(item, schemaAtPath(schema, [String(index)])));
|
|
41
|
+
if (!isRecord(value))
|
|
42
|
+
return value;
|
|
43
|
+
return Object.fromEntries(Object.entries(value)
|
|
44
|
+
.filter(([name]) => !isMetadataPath(schema, [name]))
|
|
45
|
+
.map(([name, field]) => [name, normalize(field, schemaAtPath(schema, [name]))]));
|
|
46
|
+
}
|
|
47
|
+
export function isMetadataPath(schema, segments) {
|
|
48
|
+
if (!segments.length)
|
|
49
|
+
return false;
|
|
50
|
+
if (Array.isArray(schema.anyOf))
|
|
51
|
+
return schema.anyOf.some((branch) => isRecord(branch) && isMetadataPath(branch, segments));
|
|
52
|
+
const [first, ...rest] = segments;
|
|
53
|
+
if (isRecord(schema.properties) &&
|
|
54
|
+
Object.hasOwn(schema.properties, first) &&
|
|
55
|
+
metadataFields.has(first))
|
|
56
|
+
return true;
|
|
57
|
+
const child = schemaAtPath(schema, [first]);
|
|
58
|
+
return child !== undefined && isMetadataPath(child, rest);
|
|
59
|
+
}
|
|
60
|
+
export function schemaAtPath(schema, segments) {
|
|
61
|
+
if (!segments.length)
|
|
62
|
+
return schema;
|
|
63
|
+
if (Array.isArray(schema.anyOf)) {
|
|
64
|
+
const candidates = schema.anyOf.flatMap((branch) => {
|
|
65
|
+
const field = isRecord(branch) ? schemaAtPath(branch, segments) : undefined;
|
|
66
|
+
return field ? [field] : [];
|
|
67
|
+
});
|
|
68
|
+
return candidates.length ? { anyOf: candidates } : undefined;
|
|
69
|
+
}
|
|
70
|
+
const [first, ...rest] = segments;
|
|
71
|
+
if (isRecord(schema.properties) &&
|
|
72
|
+
Object.hasOwn(schema.properties, first) &&
|
|
73
|
+
isRecord(schema.properties[first]))
|
|
74
|
+
return schemaAtPath(schema.properties[first], rest);
|
|
75
|
+
if (schema.type === 'array' && /^(0|[1-9]\d*)$/.test(first) && isRecord(schema.items))
|
|
76
|
+
return schemaAtPath(schema.items, rest);
|
|
77
|
+
if (isRecord(schema.additionalProperties))
|
|
78
|
+
return schemaAtPath(schema.additionalProperties, rest);
|
|
79
|
+
if (schema.additionalProperties === true ||
|
|
80
|
+
(schema.type === undefined && schema.enum === undefined && schema.const === undefined))
|
|
81
|
+
return {};
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { describeRecipe } from '../catalog/index.js';
|
|
5
4
|
import { recipeNameSchema } from '../catalog/schema.js';
|
|
6
|
-
import { isRecord
|
|
5
|
+
import { isRecord } from './decisions.js';
|
|
6
|
+
import { comparableDecision, isMetadataPath, schemaAtPath, resultSchemaFor } from './comparison.js';
|
|
7
7
|
import { evaluationCaseSchema } from './schema.js';
|
|
8
8
|
export async function loadEvaluationRecipe(name) {
|
|
9
9
|
const id = recipeNameSchema.parse(name);
|
|
@@ -30,7 +30,7 @@ export async function readEvaluationCases(path) {
|
|
|
30
30
|
export function validateCases(recipe, values) {
|
|
31
31
|
if (!values.length)
|
|
32
32
|
throw new Error('The case file is empty.');
|
|
33
|
-
const resultSchema =
|
|
33
|
+
const resultSchema = resultSchemaFor(recipe.id);
|
|
34
34
|
const cases = values.map((value) => evaluationCaseSchema.parse(value));
|
|
35
35
|
const ids = new Set();
|
|
36
36
|
const familySplits = new Map();
|
|
@@ -45,17 +45,7 @@ export function validateCases(recipe, values) {
|
|
|
45
45
|
}
|
|
46
46
|
recipe.inputSchema.parse(entry.input);
|
|
47
47
|
for (const [path, expected] of Object.entries(entry.expected)) {
|
|
48
|
-
if (path
|
|
49
|
-
.split('.')
|
|
50
|
-
.some((segment) => [
|
|
51
|
-
'status',
|
|
52
|
-
'confidence',
|
|
53
|
-
'probabilities',
|
|
54
|
-
'probability',
|
|
55
|
-
'model',
|
|
56
|
-
'usage',
|
|
57
|
-
'score',
|
|
58
|
-
].includes(segment)))
|
|
48
|
+
if (isMetadataPath(resultSchema, path.split('.')))
|
|
59
49
|
throw new Error(`${entry.id}: expected must name an ungated decision, not ${path}.`);
|
|
60
50
|
const field = schemaAtPath(resultSchema, path.split('.'));
|
|
61
51
|
if (!field)
|
|
@@ -63,30 +53,11 @@ export function validateCases(recipe, values) {
|
|
|
63
53
|
if (!acceptsValue(field, expected))
|
|
64
54
|
throw new Error(`${entry.id}: expected ${path} does not match the result schema.`);
|
|
65
55
|
}
|
|
66
|
-
if (isEmptyDecision(comparableDecision(entry.expected)))
|
|
56
|
+
if (isEmptyDecision(comparableDecision(entry.expected, recipe.id)))
|
|
67
57
|
throw new Error(`${entry.id}: expected must contain a decision to compare.`);
|
|
68
58
|
}
|
|
69
59
|
return cases;
|
|
70
60
|
}
|
|
71
|
-
function schemaAtPath(schema, segments) {
|
|
72
|
-
if (!segments.length)
|
|
73
|
-
return schema;
|
|
74
|
-
if (Array.isArray(schema.anyOf)) {
|
|
75
|
-
const candidates = schema.anyOf.flatMap((branch) => {
|
|
76
|
-
const field = isRecord(branch) ? schemaAtPath(branch, segments) : undefined;
|
|
77
|
-
return field ? [field] : [];
|
|
78
|
-
});
|
|
79
|
-
return candidates.length ? { anyOf: candidates } : undefined;
|
|
80
|
-
}
|
|
81
|
-
const [first, ...rest] = segments;
|
|
82
|
-
if (isRecord(schema.properties) && isRecord(schema.properties[first]))
|
|
83
|
-
return schemaAtPath(schema.properties[first], rest);
|
|
84
|
-
if (schema.type === 'array' && /^(0|[1-9]\d*)$/.test(first) && isRecord(schema.items))
|
|
85
|
-
return schemaAtPath(schema.items, rest);
|
|
86
|
-
if (isRecord(schema.additionalProperties))
|
|
87
|
-
return schemaAtPath(schema.additionalProperties, rest);
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
61
|
function acceptsValue(schema, value) {
|
|
91
62
|
if (Array.isArray(schema.anyOf))
|
|
92
63
|
return schema.anyOf.some((candidate) => isRecord(candidate) && acceptsValue(candidate, value));
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export declare function comparableDecision(value: unknown): unknown;
|
|
2
|
-
export declare function matchesExpected(actual: unknown, expected: unknown): boolean;
|
|
3
1
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
4
2
|
export declare function valueAtPath(record: unknown, path: string): unknown;
|
|
5
3
|
export declare function pickPaths(record: unknown, paths: string[]): Record<string, unknown>;
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
const metadataFields = new Set([
|
|
3
|
-
'model',
|
|
4
|
-
'usage',
|
|
5
|
-
'confidence',
|
|
6
|
-
'probability',
|
|
7
|
-
'probabilities',
|
|
8
|
-
'score',
|
|
9
|
-
'status',
|
|
10
|
-
]);
|
|
11
|
-
export function comparableDecision(value) {
|
|
12
|
-
if (Array.isArray(value))
|
|
13
|
-
return value.map(comparableDecision);
|
|
14
|
-
if (isRecord(value))
|
|
15
|
-
return Object.fromEntries(Object.entries(value)
|
|
16
|
-
.filter(([name]) => !metadataFields.has(name))
|
|
17
|
-
.map(([name, field]) => [name, comparableDecision(field)]));
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
export function matchesExpected(actual, expected) {
|
|
21
|
-
return isDeepStrictEqual(comparableDecision(actual), comparableDecision(expected));
|
|
22
|
-
}
|
|
23
1
|
export function isRecord(value) {
|
|
24
2
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
25
3
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
2
|
import { createClient } from '../src/client.js';
|
|
3
|
-
import { caseConfidence, isReady, mapWithConcurrencyLimit,
|
|
3
|
+
import { caseConfidence, isReady, mapWithConcurrencyLimit, pickPaths } from './decisions.js';
|
|
4
|
+
import { matchesExpected } from './comparison.js';
|
|
4
5
|
export const confidenceThresholds = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95];
|
|
5
6
|
export async function evaluateCases(recipe, cases, options = {}) {
|
|
6
7
|
let requests = 0;
|
|
@@ -50,7 +51,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
|
|
|
50
51
|
try {
|
|
51
52
|
row.result = await recipe.run(goldenCase.input, recipeOptions);
|
|
52
53
|
row.actual = pickPaths(row.result, Object.keys(goldenCase.expected));
|
|
53
|
-
row.correct = matchesExpected(row.actual, row.expected);
|
|
54
|
+
row.correct = matchesExpected(row.actual, row.expected, recipe.id);
|
|
54
55
|
row.ready = isReady(row.result);
|
|
55
56
|
row.confidence = caseConfidence(row.result, Object.keys(goldenCase.expected));
|
|
56
57
|
const model = pickPaths(row.result, ['model']).model;
|
|
@@ -72,7 +73,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
|
|
|
72
73
|
row.decisions.push({
|
|
73
74
|
minConfidence,
|
|
74
75
|
ready: isReady(result),
|
|
75
|
-
correct: matchesExpected(pickPaths(result, Object.keys(row.expected)), row.expected),
|
|
76
|
+
correct: matchesExpected(pickPaths(result, Object.keys(row.expected)), row.expected, recipe.id),
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
return row;
|
|
@@ -4,6 +4,9 @@ import type { EvaluationCase, EvaluationPolicy } from './schema.js';
|
|
|
4
4
|
export interface ReportEvidence {
|
|
5
5
|
runId: string;
|
|
6
6
|
evaluatedAt: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
sourceMode: 'live' | 'fixture';
|
|
9
|
+
scoringRevision: number;
|
|
7
10
|
packageVersion: string;
|
|
8
11
|
recipeFingerprint: string;
|
|
9
12
|
datasetFingerprint: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isRecord,
|
|
1
|
+
import { isRecord, roundedTo } from './decisions.js';
|
|
2
|
+
import { matchesExpected } from './comparison.js';
|
|
2
3
|
export function buildReport(id, rows, confidenceThresholds, price, evidence) {
|
|
3
4
|
const completed = rows.filter((row) => row.error === undefined);
|
|
4
5
|
const ready = completed.filter((row) => row.ready);
|
|
@@ -41,7 +42,7 @@ export function buildReport(id, rows, confidenceThresholds, price, evidence) {
|
|
|
41
42
|
reviewRate: completed.length
|
|
42
43
|
? roundedTo(3, (completed.length - ready.length) / completed.length)
|
|
43
44
|
: null,
|
|
44
|
-
...itemAccuracyOf(rows),
|
|
45
|
+
...itemAccuracyOf(id, rows),
|
|
45
46
|
contestedAccuracy: accuracyOf(rows.filter((row) => row.contested)),
|
|
46
47
|
adversarialAccuracy: accuracyOf(rows.filter((row) => row.adversarial)),
|
|
47
48
|
calibration: [0, 0.5, 0.6, 0.7, 0.8, 0.9].map((lower, index, edges) => {
|
|
@@ -90,14 +91,15 @@ export function buildReport(id, rows, confidenceThresholds, price, evidence) {
|
|
|
90
91
|
function accuracyOf(rows) {
|
|
91
92
|
return rows.length ? roundedTo(3, rows.filter((row) => row.correct).length / rows.length) : null;
|
|
92
93
|
}
|
|
93
|
-
function itemAccuracyOf(rows) {
|
|
94
|
+
function itemAccuracyOf(recipe, rows) {
|
|
94
95
|
const paths = rows.flatMap((row) => Object.entries(row.expected).map(([path, expected]) => ({
|
|
96
|
+
path,
|
|
95
97
|
expected,
|
|
96
98
|
actual: row.error === undefined ? row.actual?.[path] : undefined,
|
|
97
99
|
})));
|
|
98
100
|
if (paths.length <= rows.length)
|
|
99
101
|
return {};
|
|
100
|
-
const correct = paths.filter((
|
|
102
|
+
const correct = paths.filter(({ path, actual, expected }) => matchesExpected({ [path]: actual }, { [path]: expected }, recipe)).length;
|
|
101
103
|
return {
|
|
102
104
|
items: paths.length,
|
|
103
105
|
correctItems: correct,
|
package/dist/src/batch.js
CHANGED
|
@@ -11,7 +11,20 @@ export async function evaluateInBatches(items, batchSize, evaluate) {
|
|
|
11
11
|
const chunks = [];
|
|
12
12
|
for (let offset = 0; offset < items.length; offset += batchSize)
|
|
13
13
|
chunks.push({ chunk: items.slice(offset, offset + batchSize), offset });
|
|
14
|
-
|
|
14
|
+
let firstFailure;
|
|
15
|
+
const settled = await Promise.allSettled(chunks.map(async ({ chunk, offset }) => {
|
|
16
|
+
try {
|
|
17
|
+
return await evaluate(chunk, offset);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
firstFailure ??= { error };
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
// Let request recorders finish before a caller archives this failed batch.
|
|
25
|
+
if (firstFailure)
|
|
26
|
+
throw firstFailure.error;
|
|
27
|
+
const evaluations = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
15
28
|
return {
|
|
16
29
|
items: evaluations.flatMap((evaluation) => evaluation.items),
|
|
17
30
|
model: evaluations[0]?.model ?? 'none',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-recipes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Small, composable Jev decisions for retrieval, conversations, and agent workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
"types": "./dist/evaluation/index.d.ts",
|
|
28
28
|
"import": "./dist/evaluation/index.js"
|
|
29
29
|
},
|
|
30
|
+
"./ai-sdk": {
|
|
31
|
+
"types": "./dist/adapters/ai-sdk/index.d.ts",
|
|
32
|
+
"import": "./dist/adapters/ai-sdk/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./langchain": {
|
|
35
|
+
"types": "./dist/adapters/langchain/index.d.ts",
|
|
36
|
+
"import": "./dist/adapters/langchain/index.js"
|
|
37
|
+
},
|
|
30
38
|
"./package.json": "./package.json",
|
|
31
39
|
"./action-compare": {
|
|
32
40
|
"types": "./dist/recipes/action-compare/index.d.ts",
|
|
@@ -1035,6 +1043,8 @@
|
|
|
1035
1043
|
"dist/cli/*.js",
|
|
1036
1044
|
"dist/evaluation/**/*.js",
|
|
1037
1045
|
"dist/evaluation/**/*.d.ts",
|
|
1046
|
+
"dist/adapters/**/*.js",
|
|
1047
|
+
"dist/adapters/**/*.d.ts",
|
|
1038
1048
|
"dist/cli/*.d.ts",
|
|
1039
1049
|
"dist/recipes/*/demo.json",
|
|
1040
1050
|
"README.md",
|
|
@@ -1054,6 +1064,7 @@
|
|
|
1054
1064
|
"eval": "node --env-file-if-exists=.env evals/run.mjs",
|
|
1055
1065
|
"eval:smoke": "node --env-file-if-exists=.env evals/smoke.mjs",
|
|
1056
1066
|
"eval:validate": "node evals/validate.mjs",
|
|
1067
|
+
"eval:audit": "node scripts/audit-evidence.mjs",
|
|
1057
1068
|
"jev-recipes": "node --env-file-if-exists=.env dist/cli/index.js",
|
|
1058
1069
|
"ci": "node scripts/ci.mjs",
|
|
1059
1070
|
"pack:check": "node scripts/pack-check.mjs",
|
|
@@ -1066,15 +1077,29 @@
|
|
|
1066
1077
|
"test:coverage": "vitest run --coverage",
|
|
1067
1078
|
"eval:featured": "node --env-file-if-exists=.env evals/featured.mjs",
|
|
1068
1079
|
"site:build": "npm run generate:check && npm run build && node site/build.mjs",
|
|
1069
|
-
"site:preview": "node site/preview.mjs"
|
|
1080
|
+
"site:preview": "node --env-file-if-exists=.env site/preview.mjs"
|
|
1070
1081
|
},
|
|
1071
1082
|
"dependencies": {
|
|
1072
1083
|
"@typesafe-ai/sdk": "^0.6.0",
|
|
1073
1084
|
"zod": "^4.6.5"
|
|
1074
1085
|
},
|
|
1086
|
+
"peerDependencies": {
|
|
1087
|
+
"ai": ">=7.0.0",
|
|
1088
|
+
"@langchain/core": ">=1.0.0"
|
|
1089
|
+
},
|
|
1090
|
+
"peerDependenciesMeta": {
|
|
1091
|
+
"ai": {
|
|
1092
|
+
"optional": true
|
|
1093
|
+
},
|
|
1094
|
+
"@langchain/core": {
|
|
1095
|
+
"optional": true
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1075
1098
|
"devDependencies": {
|
|
1099
|
+
"@langchain/core": "^1.2.13",
|
|
1076
1100
|
"@types/node": "^22.0.0",
|
|
1077
1101
|
"@vitest/coverage-v8": "4.1.11",
|
|
1102
|
+
"ai": "^7.0.122",
|
|
1078
1103
|
"prettier": "3.9.8",
|
|
1079
1104
|
"rimraf": "6.1.3",
|
|
1080
1105
|
"typescript": "~5.9.3",
|