jev-recipes 0.7.0 → 0.8.1
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 +5 -1
- package/dist/catalog/generated/details/callback-responsibility.json +45 -0
- package/dist/catalog/generated/details/contact-opt-out.json +45 -0
- package/dist/catalog/generated/details/followup-timing.json +45 -0
- package/dist/catalog/generated/details/game-action.json +8 -109
- package/dist/catalog/generated/loaders.js +3 -0
- package/dist/catalog/generated/metadata.js +78 -0
- package/dist/catalog/generated/names.d.ts +1 -1
- package/dist/catalog/generated/names.js +3 -0
- package/dist/catalog/schema.d.ts +3 -0
- package/dist/cli/evaluation.d.ts +1 -0
- package/dist/cli/evaluation.js +82 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/schema.d.ts +9 -0
- package/dist/evaluation/archive.d.ts +501 -0
- package/dist/evaluation/archive.js +276 -0
- package/dist/evaluation/dataset.d.ts +12 -0
- package/dist/evaluation/dataset.js +162 -0
- package/dist/evaluation/decisions.d.ts +10 -0
- package/dist/evaluation/decisions.js +87 -0
- package/dist/evaluation/engine.d.ts +50 -0
- package/dist/evaluation/engine.js +130 -0
- package/dist/evaluation/index.d.ts +5 -0
- package/dist/evaluation/index.js +3 -0
- package/dist/evaluation/report.d.ts +186 -0
- package/dist/evaluation/report.js +177 -0
- package/dist/evaluation/schema.d.ts +82 -0
- package/dist/evaluation/schema.js +50 -0
- package/dist/recipes/callback-responsibility/demo.json +25 -0
- package/dist/recipes/callback-responsibility/index.d.ts +5 -0
- package/dist/recipes/callback-responsibility/index.js +17 -0
- package/dist/recipes/callback-responsibility/schema.d.ts +42 -0
- package/dist/recipes/callback-responsibility/schema.js +20 -0
- package/dist/recipes/checkers-move/schema.d.ts +1 -1
- package/dist/recipes/choose-action/schema.d.ts +1 -1
- package/dist/recipes/contact-opt-out/demo.json +24 -0
- package/dist/recipes/contact-opt-out/index.d.ts +5 -0
- package/dist/recipes/contact-opt-out/index.js +17 -0
- package/dist/recipes/contact-opt-out/schema.d.ts +42 -0
- package/dist/recipes/contact-opt-out/schema.js +20 -0
- package/dist/recipes/correction-target/schema.d.ts +1 -1
- package/dist/recipes/exercise-select/schema.d.ts +1 -1
- package/dist/recipes/failure-kind/schema.d.ts +1 -1
- package/dist/recipes/field-select/schema.d.ts +1 -1
- package/dist/recipes/finger-actions/schema.d.ts +1 -1
- package/dist/recipes/followup-link/schema.d.ts +1 -1
- package/dist/recipes/followup-timing/demo.json +25 -0
- package/dist/recipes/followup-timing/index.d.ts +5 -0
- package/dist/recipes/followup-timing/index.js +18 -0
- package/dist/recipes/followup-timing/schema.d.ts +45 -0
- package/dist/recipes/followup-timing/schema.js +21 -0
- package/dist/recipes/game-action/schema.d.ts +6 -6
- package/dist/recipes/game-action/schema.js +11 -6
- package/dist/recipes/handoff/schema.d.ts +3 -3
- package/dist/recipes/incident-match/schema.d.ts +1 -1
- package/dist/recipes/model-route/schema.d.ts +1 -1
- package/dist/recipes/next-chord/schema.d.ts +1 -1
- package/dist/recipes/next-duration/schema.d.ts +1 -1
- package/dist/recipes/next-note/schema.d.ts +1 -1
- package/dist/recipes/objection-kind/schema.d.ts +3 -3
- package/dist/recipes/reference-resolve/schema.d.ts +1 -1
- package/dist/recipes/reply-template-match/schema.d.ts +1 -1
- package/dist/recipes/route/schema.d.ts +2 -2
- package/dist/recipes/route/schema.js +3 -3
- package/dist/recipes/route-many/schema.d.ts +3 -3
- package/dist/recipes/route-many/schema.js +3 -3
- package/dist/recipes/style-kind/schema.d.ts +3 -3
- package/dist/src/answers.d.ts +1 -1
- package/dist/src/answers.js +6 -2
- package/dist/src/data.d.ts +6 -0
- package/dist/src/data.js +41 -0
- package/dist/src/decisions.d.ts +1 -1
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +3 -0
- package/dist/src/schema.d.ts +4 -4
- package/dist/src/schema.js +4 -3
- package/package.json +23 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { createClient } from '../src/client.js';
|
|
3
|
+
import { caseConfidence, isReady, mapWithConcurrencyLimit, matchesExpected, pickPaths, } from './decisions.js';
|
|
4
|
+
export const confidenceThresholds = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95];
|
|
5
|
+
export async function evaluateCases(recipe, cases, options = {}) {
|
|
6
|
+
let requests = 0;
|
|
7
|
+
let client = options.client;
|
|
8
|
+
const limitedClient = {
|
|
9
|
+
async systemOne(request, requestOptions) {
|
|
10
|
+
if (requests >= (options.maxRequests ?? 1000))
|
|
11
|
+
throw new Error('Evaluation request limit reached.');
|
|
12
|
+
requests++;
|
|
13
|
+
client ??= createClient();
|
|
14
|
+
return client.systemOne(request, requestOptions);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
return mapWithConcurrencyLimit(cases, options.concurrency ?? 4, async (goldenCase, index) => {
|
|
18
|
+
const recorded = options.recorded?.find((row) => row.id === goldenCase.id);
|
|
19
|
+
if (options.recorded && !recorded)
|
|
20
|
+
throw new Error(`Missing archived case: ${goldenCase.id}`);
|
|
21
|
+
const playback = recorded ? replayResponses(recorded.exchanges) : undefined;
|
|
22
|
+
const row = await evaluateCase(recipe, goldenCase, playback?.client ?? limitedClient, options);
|
|
23
|
+
playback?.assertComplete();
|
|
24
|
+
if (recorded) {
|
|
25
|
+
row.durationMs = recorded.durationMs;
|
|
26
|
+
row.exchanges = recorded.exchanges;
|
|
27
|
+
}
|
|
28
|
+
await options.onCase?.(row, index);
|
|
29
|
+
return row;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function evaluateCase(recipe, goldenCase, client, options) {
|
|
33
|
+
const row = {
|
|
34
|
+
id: goldenCase.id,
|
|
35
|
+
expected: goldenCase.expected,
|
|
36
|
+
contested: goldenCase.contested,
|
|
37
|
+
adversarial: goldenCase.adversarial,
|
|
38
|
+
correct: false,
|
|
39
|
+
ready: false,
|
|
40
|
+
durationMs: 0,
|
|
41
|
+
exchanges: [],
|
|
42
|
+
decisions: [],
|
|
43
|
+
};
|
|
44
|
+
const started = performance.now();
|
|
45
|
+
const recipeOptions = { client: recordResponses(client, row.exchanges) };
|
|
46
|
+
if (options.model !== undefined)
|
|
47
|
+
recipeOptions.model = options.model;
|
|
48
|
+
if (options.signal !== undefined)
|
|
49
|
+
recipeOptions.signal = options.signal;
|
|
50
|
+
try {
|
|
51
|
+
row.result = await recipe.run(goldenCase.input, recipeOptions);
|
|
52
|
+
row.actual = pickPaths(row.result, Object.keys(goldenCase.expected));
|
|
53
|
+
row.correct = matchesExpected(row.actual, row.expected);
|
|
54
|
+
row.ready = isReady(row.result);
|
|
55
|
+
row.confidence = caseConfidence(row.result, Object.keys(goldenCase.expected));
|
|
56
|
+
const model = pickPaths(row.result, ['model']).model;
|
|
57
|
+
if (typeof model === 'string')
|
|
58
|
+
row.model = model;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof ReplayMismatch)
|
|
62
|
+
throw error;
|
|
63
|
+
row.error = safeError(error);
|
|
64
|
+
}
|
|
65
|
+
row.durationMs = Math.round((performance.now() - started) * 100) / 100;
|
|
66
|
+
if (row.error !== undefined)
|
|
67
|
+
return row;
|
|
68
|
+
for (const minConfidence of options.thresholds ?? []) {
|
|
69
|
+
const playback = replayResponses(row.exchanges);
|
|
70
|
+
const result = await recipe.run({ ...goldenCase.input, minConfidence }, { ...recipeOptions, client: playback.client });
|
|
71
|
+
playback.assertComplete();
|
|
72
|
+
row.decisions.push({
|
|
73
|
+
minConfidence,
|
|
74
|
+
ready: isReady(result),
|
|
75
|
+
correct: matchesExpected(pickPaths(result, Object.keys(row.expected)), row.expected),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return row;
|
|
79
|
+
}
|
|
80
|
+
function recordResponses(client, exchanges) {
|
|
81
|
+
return {
|
|
82
|
+
async systemOne(request, options) {
|
|
83
|
+
const exchange = { request: structuredClone(request), durationMs: 0 };
|
|
84
|
+
exchanges.push(exchange);
|
|
85
|
+
const started = performance.now();
|
|
86
|
+
try {
|
|
87
|
+
const response = await client.systemOne(request, options);
|
|
88
|
+
exchange.response = structuredClone(response);
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
exchange.error = safeError(error);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
exchange.durationMs = Math.round((performance.now() - started) * 100) / 100;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
class ReplayMismatch extends Error {
|
|
102
|
+
}
|
|
103
|
+
export function replayResponses(exchanges) {
|
|
104
|
+
const remaining = [...exchanges];
|
|
105
|
+
return {
|
|
106
|
+
client: {
|
|
107
|
+
async systemOne(request) {
|
|
108
|
+
const index = remaining.findIndex((exchange) => isDeepStrictEqual(exchange.request, request));
|
|
109
|
+
if (index === -1)
|
|
110
|
+
throw new ReplayMismatch('Replay requested an unrecorded model response. The prompt or model has changed.');
|
|
111
|
+
const [exchange] = remaining.splice(index, 1);
|
|
112
|
+
if (exchange.error !== undefined)
|
|
113
|
+
throw new Error(exchange.error);
|
|
114
|
+
return structuredClone(exchange.response);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
assertComplete() {
|
|
118
|
+
if (remaining.length)
|
|
119
|
+
throw new ReplayMismatch('Replay did not consume every recorded request. The recipe has changed.');
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function safeError(error) {
|
|
124
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
125
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
126
|
+
if (/KEY|TOKEN|SECRET|PASSWORD/i.test(name) && value && value.length >= 6)
|
|
127
|
+
message = message.replaceAll(value, '[redacted]');
|
|
128
|
+
}
|
|
129
|
+
return message.replace(/Bearer\s+\S+/gi, 'Bearer [redacted]');
|
|
130
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { evaluate, replay, compare, readRun, freezePolicy } from './archive.js';
|
|
2
|
+
export type { EvaluationRun } from './archive.js';
|
|
3
|
+
export { readEvaluationCases } from './dataset.js';
|
|
4
|
+
export { evaluationCaseSchema, evaluationOptionsSchema, evaluationPolicySchema } from './schema.js';
|
|
5
|
+
export type { EvaluationCase, EvaluationPolicy, EvaluationOptions, Price } from './schema.js';
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { EvaluationRow } from './engine.js';
|
|
2
|
+
import type { Price } from './schema.js';
|
|
3
|
+
import type { EvaluationCase, EvaluationPolicy } from './schema.js';
|
|
4
|
+
export interface ReportEvidence {
|
|
5
|
+
runId: string;
|
|
6
|
+
evaluatedAt: string;
|
|
7
|
+
packageVersion: string;
|
|
8
|
+
recipeFingerprint: string;
|
|
9
|
+
datasetFingerprint: string;
|
|
10
|
+
split: 'development' | 'held-out';
|
|
11
|
+
mode: 'live' | 'fixture' | 'replay';
|
|
12
|
+
policy: EvaluationPolicy;
|
|
13
|
+
provenance: {
|
|
14
|
+
method: EvaluationCase['provenance']['method'];
|
|
15
|
+
source: string;
|
|
16
|
+
cases: number;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
export declare function buildReport(id: string, rows: EvaluationRow[], confidenceThresholds: number[], price?: Price, evidence?: ReportEvidence): {
|
|
20
|
+
contestedAccuracy: number | null;
|
|
21
|
+
adversarialAccuracy: number | null;
|
|
22
|
+
calibration: {
|
|
23
|
+
confidence: string;
|
|
24
|
+
cases: number;
|
|
25
|
+
accuracy: number | null;
|
|
26
|
+
}[];
|
|
27
|
+
thresholdEvaluation: string;
|
|
28
|
+
thresholds: {
|
|
29
|
+
minConfidence: number;
|
|
30
|
+
deferRate: number | null;
|
|
31
|
+
readyAccuracy: number | null;
|
|
32
|
+
readyCases: number;
|
|
33
|
+
reviewCases: number;
|
|
34
|
+
failedCases: number;
|
|
35
|
+
readyAccuracyInterval95: [number, number] | null;
|
|
36
|
+
}[];
|
|
37
|
+
suggestedMinConfidence: number | null;
|
|
38
|
+
latencyMs: {
|
|
39
|
+
p50: number | null;
|
|
40
|
+
p95: number | null;
|
|
41
|
+
totalCaseTime: number;
|
|
42
|
+
};
|
|
43
|
+
usage: {
|
|
44
|
+
requests: number;
|
|
45
|
+
input_tokens: number;
|
|
46
|
+
output_tokens: number;
|
|
47
|
+
unknownResponses: number;
|
|
48
|
+
failedRequests: number;
|
|
49
|
+
models: string[];
|
|
50
|
+
};
|
|
51
|
+
cost: {
|
|
52
|
+
estimated: number;
|
|
53
|
+
model: string;
|
|
54
|
+
inputPerMillion: number;
|
|
55
|
+
outputPerMillion: number;
|
|
56
|
+
currency: "USD";
|
|
57
|
+
date: string;
|
|
58
|
+
source: string;
|
|
59
|
+
} | null;
|
|
60
|
+
confusion: {
|
|
61
|
+
[k: string]: {
|
|
62
|
+
total: number;
|
|
63
|
+
correct: number;
|
|
64
|
+
misses: Record<string, number>;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
failures: ({
|
|
68
|
+
adversarial?: true;
|
|
69
|
+
contested?: true;
|
|
70
|
+
confidence?: number;
|
|
71
|
+
actual: Record<string, unknown> | undefined;
|
|
72
|
+
id: string;
|
|
73
|
+
expected: Record<string, unknown>;
|
|
74
|
+
} | {
|
|
75
|
+
adversarial?: true;
|
|
76
|
+
contested?: true;
|
|
77
|
+
confidence?: number;
|
|
78
|
+
error: string;
|
|
79
|
+
id: string;
|
|
80
|
+
expected: Record<string, unknown>;
|
|
81
|
+
})[];
|
|
82
|
+
items?: never;
|
|
83
|
+
correctItems?: never;
|
|
84
|
+
itemAccuracy?: never;
|
|
85
|
+
recipe: string;
|
|
86
|
+
model: string;
|
|
87
|
+
models: string[];
|
|
88
|
+
cases: number;
|
|
89
|
+
completed: number;
|
|
90
|
+
failed: number;
|
|
91
|
+
correct: number;
|
|
92
|
+
wrong: number;
|
|
93
|
+
ready: number;
|
|
94
|
+
review: number;
|
|
95
|
+
accuracy: number | null;
|
|
96
|
+
completedAccuracy: number | null;
|
|
97
|
+
accuracyInterval95: [number, number] | null;
|
|
98
|
+
readyAccuracy: number | null;
|
|
99
|
+
readyAccuracyInterval95: [number, number] | null;
|
|
100
|
+
reviewRate: number | null;
|
|
101
|
+
evidence?: ReportEvidence;
|
|
102
|
+
} | {
|
|
103
|
+
contestedAccuracy: number | null;
|
|
104
|
+
adversarialAccuracy: number | null;
|
|
105
|
+
calibration: {
|
|
106
|
+
confidence: string;
|
|
107
|
+
cases: number;
|
|
108
|
+
accuracy: number | null;
|
|
109
|
+
}[];
|
|
110
|
+
thresholdEvaluation: string;
|
|
111
|
+
thresholds: {
|
|
112
|
+
minConfidence: number;
|
|
113
|
+
deferRate: number | null;
|
|
114
|
+
readyAccuracy: number | null;
|
|
115
|
+
readyCases: number;
|
|
116
|
+
reviewCases: number;
|
|
117
|
+
failedCases: number;
|
|
118
|
+
readyAccuracyInterval95: [number, number] | null;
|
|
119
|
+
}[];
|
|
120
|
+
suggestedMinConfidence: number | null;
|
|
121
|
+
latencyMs: {
|
|
122
|
+
p50: number | null;
|
|
123
|
+
p95: number | null;
|
|
124
|
+
totalCaseTime: number;
|
|
125
|
+
};
|
|
126
|
+
usage: {
|
|
127
|
+
requests: number;
|
|
128
|
+
input_tokens: number;
|
|
129
|
+
output_tokens: number;
|
|
130
|
+
unknownResponses: number;
|
|
131
|
+
failedRequests: number;
|
|
132
|
+
models: string[];
|
|
133
|
+
};
|
|
134
|
+
cost: {
|
|
135
|
+
estimated: number;
|
|
136
|
+
model: string;
|
|
137
|
+
inputPerMillion: number;
|
|
138
|
+
outputPerMillion: number;
|
|
139
|
+
currency: "USD";
|
|
140
|
+
date: string;
|
|
141
|
+
source: string;
|
|
142
|
+
} | null;
|
|
143
|
+
confusion: {
|
|
144
|
+
[k: string]: {
|
|
145
|
+
total: number;
|
|
146
|
+
correct: number;
|
|
147
|
+
misses: Record<string, number>;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
failures: ({
|
|
151
|
+
adversarial?: true;
|
|
152
|
+
contested?: true;
|
|
153
|
+
confidence?: number;
|
|
154
|
+
actual: Record<string, unknown> | undefined;
|
|
155
|
+
id: string;
|
|
156
|
+
expected: Record<string, unknown>;
|
|
157
|
+
} | {
|
|
158
|
+
adversarial?: true;
|
|
159
|
+
contested?: true;
|
|
160
|
+
confidence?: number;
|
|
161
|
+
error: string;
|
|
162
|
+
id: string;
|
|
163
|
+
expected: Record<string, unknown>;
|
|
164
|
+
})[];
|
|
165
|
+
items: number;
|
|
166
|
+
correctItems: number;
|
|
167
|
+
itemAccuracy: number;
|
|
168
|
+
recipe: string;
|
|
169
|
+
model: string;
|
|
170
|
+
models: string[];
|
|
171
|
+
cases: number;
|
|
172
|
+
completed: number;
|
|
173
|
+
failed: number;
|
|
174
|
+
correct: number;
|
|
175
|
+
wrong: number;
|
|
176
|
+
ready: number;
|
|
177
|
+
review: number;
|
|
178
|
+
accuracy: number | null;
|
|
179
|
+
completedAccuracy: number | null;
|
|
180
|
+
accuracyInterval95: [number, number] | null;
|
|
181
|
+
readyAccuracy: number | null;
|
|
182
|
+
readyAccuracyInterval95: [number, number] | null;
|
|
183
|
+
reviewRate: number | null;
|
|
184
|
+
evidence?: ReportEvidence;
|
|
185
|
+
};
|
|
186
|
+
export declare function wilsonInterval(correct: number, total: number): [number, number] | null;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { isRecord, matchesExpected, roundedTo } from './decisions.js';
|
|
2
|
+
export function buildReport(id, rows, confidenceThresholds, price, evidence) {
|
|
3
|
+
const completed = rows.filter((row) => row.error === undefined);
|
|
4
|
+
const ready = completed.filter((row) => row.ready);
|
|
5
|
+
const thresholds = confidenceThresholds.map((minConfidence) => {
|
|
6
|
+
const decisions = completed
|
|
7
|
+
.flatMap((row) => row.decisions)
|
|
8
|
+
.filter((decision) => decision.minConfidence === minConfidence);
|
|
9
|
+
const accepted = decisions.filter((decision) => decision.ready);
|
|
10
|
+
return {
|
|
11
|
+
minConfidence,
|
|
12
|
+
deferRate: completed.length
|
|
13
|
+
? roundedTo(3, (completed.length - accepted.length) / completed.length)
|
|
14
|
+
: null,
|
|
15
|
+
readyAccuracy: accuracyOf(accepted),
|
|
16
|
+
readyCases: accepted.length,
|
|
17
|
+
reviewCases: completed.length - accepted.length,
|
|
18
|
+
failedCases: rows.length - completed.length,
|
|
19
|
+
readyAccuracyInterval95: wilsonInterval(accepted.filter((entry) => entry.correct).length, accepted.length),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
const usage = usageOf(rows);
|
|
23
|
+
const models = [...new Set(rows.flatMap((row) => (row.model ? [row.model] : [])))].sort();
|
|
24
|
+
return {
|
|
25
|
+
...(evidence ? { evidence } : {}),
|
|
26
|
+
recipe: id,
|
|
27
|
+
model: models.length === 1 ? models[0] : models.length ? 'mixed' : 'unknown',
|
|
28
|
+
models,
|
|
29
|
+
cases: rows.length,
|
|
30
|
+
completed: completed.length,
|
|
31
|
+
failed: rows.length - completed.length,
|
|
32
|
+
correct: rows.filter((row) => row.correct).length,
|
|
33
|
+
wrong: completed.filter((row) => !row.correct).length,
|
|
34
|
+
ready: ready.length,
|
|
35
|
+
review: completed.length - ready.length,
|
|
36
|
+
accuracy: accuracyOf(rows),
|
|
37
|
+
completedAccuracy: accuracyOf(completed),
|
|
38
|
+
accuracyInterval95: wilsonInterval(rows.filter((row) => row.correct).length, rows.length),
|
|
39
|
+
readyAccuracy: accuracyOf(ready),
|
|
40
|
+
readyAccuracyInterval95: wilsonInterval(ready.filter((row) => row.correct).length, ready.length),
|
|
41
|
+
reviewRate: completed.length
|
|
42
|
+
? roundedTo(3, (completed.length - ready.length) / completed.length)
|
|
43
|
+
: null,
|
|
44
|
+
...itemAccuracyOf(rows),
|
|
45
|
+
contestedAccuracy: accuracyOf(rows.filter((row) => row.contested)),
|
|
46
|
+
adversarialAccuracy: accuracyOf(rows.filter((row) => row.adversarial)),
|
|
47
|
+
calibration: [0, 0.5, 0.6, 0.7, 0.8, 0.9].map((lower, index, edges) => {
|
|
48
|
+
const upper = edges[index + 1] ?? 1.000001;
|
|
49
|
+
const bucket = rows.filter((row) => row.confidence !== undefined && row.confidence >= lower && row.confidence < upper);
|
|
50
|
+
return {
|
|
51
|
+
confidence: `${lower}-${Math.min(upper, 1)}`,
|
|
52
|
+
cases: bucket.length,
|
|
53
|
+
accuracy: accuracyOf(bucket),
|
|
54
|
+
};
|
|
55
|
+
}),
|
|
56
|
+
thresholdEvaluation: evidence?.split === 'held-out'
|
|
57
|
+
? 'frozen-policy'
|
|
58
|
+
: confidenceThresholds.length
|
|
59
|
+
? 'recipe-replay'
|
|
60
|
+
: 'not-applicable',
|
|
61
|
+
thresholds,
|
|
62
|
+
suggestedMinConfidence: thresholds.find((entry) => entry.readyAccuracy !== null && entry.readyAccuracy >= 0.95)
|
|
63
|
+
?.minConfidence ?? null,
|
|
64
|
+
latencyMs: latencyOf(rows.map((row) => row.durationMs)),
|
|
65
|
+
usage,
|
|
66
|
+
cost: price &&
|
|
67
|
+
usage.models.every((model) => model === price.model) &&
|
|
68
|
+
usage.unknownResponses === 0 &&
|
|
69
|
+
usage.failedRequests === 0
|
|
70
|
+
? {
|
|
71
|
+
...price,
|
|
72
|
+
estimated: roundedTo(8, (usage.input_tokens * price.inputPerMillion +
|
|
73
|
+
usage.output_tokens * price.outputPerMillion) /
|
|
74
|
+
1_000_000),
|
|
75
|
+
}
|
|
76
|
+
: null,
|
|
77
|
+
confusion: confusionOf(rows),
|
|
78
|
+
failures: rows
|
|
79
|
+
.filter((row) => !row.correct)
|
|
80
|
+
.map(({ id, expected, actual, error, confidence, contested, adversarial }) => ({
|
|
81
|
+
id,
|
|
82
|
+
expected,
|
|
83
|
+
...(error === undefined ? { actual } : { error }),
|
|
84
|
+
...(confidence === undefined ? {} : { confidence: roundedTo(3, confidence) }),
|
|
85
|
+
...(contested ? { contested } : {}),
|
|
86
|
+
...(adversarial ? { adversarial } : {}),
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function accuracyOf(rows) {
|
|
91
|
+
return rows.length ? roundedTo(3, rows.filter((row) => row.correct).length / rows.length) : null;
|
|
92
|
+
}
|
|
93
|
+
function itemAccuracyOf(rows) {
|
|
94
|
+
const paths = rows.flatMap((row) => Object.entries(row.expected).map(([path, expected]) => ({
|
|
95
|
+
expected,
|
|
96
|
+
actual: row.error === undefined ? row.actual?.[path] : undefined,
|
|
97
|
+
})));
|
|
98
|
+
if (paths.length <= rows.length)
|
|
99
|
+
return {};
|
|
100
|
+
const correct = paths.filter((entry) => matchesExpected(entry.actual, entry.expected)).length;
|
|
101
|
+
return {
|
|
102
|
+
items: paths.length,
|
|
103
|
+
correctItems: correct,
|
|
104
|
+
itemAccuracy: roundedTo(3, correct / paths.length),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function wilsonInterval(correct, total) {
|
|
108
|
+
if (!total)
|
|
109
|
+
return null;
|
|
110
|
+
const z = 1.959963984540054;
|
|
111
|
+
const proportion = correct / total;
|
|
112
|
+
const denominator = 1 + (z * z) / total;
|
|
113
|
+
const center = (proportion + (z * z) / (2 * total)) / denominator;
|
|
114
|
+
const margin = (z * Math.sqrt((proportion * (1 - proportion)) / total + (z * z) / (4 * total * total))) /
|
|
115
|
+
denominator;
|
|
116
|
+
return [roundedTo(4, Math.max(0, center - margin)), roundedTo(4, Math.min(1, center + margin))];
|
|
117
|
+
}
|
|
118
|
+
function latencyOf(values) {
|
|
119
|
+
const ordered = [...values].sort((a, b) => a - b);
|
|
120
|
+
const percentile = (fraction) => ordered[Math.max(0, Math.ceil(ordered.length * fraction) - 1)] ?? null;
|
|
121
|
+
return {
|
|
122
|
+
p50: percentile(0.5),
|
|
123
|
+
p95: percentile(0.95),
|
|
124
|
+
totalCaseTime: roundedTo(2, values.reduce((sum, value) => sum + value, 0)),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function usageOf(rows) {
|
|
128
|
+
let input_tokens = 0, output_tokens = 0, unknownResponses = 0, failedRequests = 0;
|
|
129
|
+
const models = new Set();
|
|
130
|
+
const exchanges = rows.flatMap((row) => row.exchanges);
|
|
131
|
+
for (const exchange of exchanges) {
|
|
132
|
+
if (exchange.error !== undefined) {
|
|
133
|
+
failedRequests++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const response = exchange.response;
|
|
137
|
+
if (isRecord(response) && typeof response.model === 'string')
|
|
138
|
+
models.add(response.model);
|
|
139
|
+
if (!isRecord(response) ||
|
|
140
|
+
!isRecord(response.usage) ||
|
|
141
|
+
typeof response.usage.input_tokens !== 'number' ||
|
|
142
|
+
typeof response.usage.output_tokens !== 'number') {
|
|
143
|
+
unknownResponses++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
input_tokens += response.usage.input_tokens;
|
|
147
|
+
output_tokens += response.usage.output_tokens;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
requests: exchanges.length,
|
|
151
|
+
input_tokens,
|
|
152
|
+
output_tokens,
|
|
153
|
+
unknownResponses,
|
|
154
|
+
failedRequests,
|
|
155
|
+
models: [...models].sort(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function confusionOf(rows) {
|
|
159
|
+
const byExpected = new Map();
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
const label = JSON.stringify(row.expected);
|
|
162
|
+
const entry = byExpected.get(label) ?? {
|
|
163
|
+
total: 0,
|
|
164
|
+
correct: 0,
|
|
165
|
+
misses: Object.create(null),
|
|
166
|
+
};
|
|
167
|
+
entry.total++;
|
|
168
|
+
if (row.correct)
|
|
169
|
+
entry.correct++;
|
|
170
|
+
else {
|
|
171
|
+
const actual = row.error === undefined ? JSON.stringify(row.actual) : `error: ${row.error}`;
|
|
172
|
+
entry.misses[actual] = (entry.misses[actual] ?? 0) + 1;
|
|
173
|
+
}
|
|
174
|
+
byExpected.set(label, entry);
|
|
175
|
+
}
|
|
176
|
+
return Object.fromEntries([...byExpected.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
177
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const provenanceSchema: z.ZodObject<{
|
|
3
|
+
method: z.ZodEnum<{
|
|
4
|
+
"author-synthetic": "author-synthetic";
|
|
5
|
+
"human-reviewed": "human-reviewed";
|
|
6
|
+
"public-dataset": "public-dataset";
|
|
7
|
+
unspecified: "unspecified";
|
|
8
|
+
}>;
|
|
9
|
+
source: z.ZodString;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export declare const evaluationCaseSchema: z.ZodObject<{
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
input: z.ZodNonOptional<z.ZodUnknown & z.ZodType<Record<string, import("../src/data.js").JsonValue>, unknown, z.core.$ZodTypeInternals<Record<string, import("../src/data.js").JsonValue>, unknown>>>;
|
|
14
|
+
expected: z.ZodNonOptional<z.ZodUnknown & z.ZodType<Record<string, import("../src/data.js").JsonValue>, unknown, z.core.$ZodTypeInternals<Record<string, import("../src/data.js").JsonValue>, unknown>>>;
|
|
15
|
+
rationale: z.ZodString;
|
|
16
|
+
family: z.ZodOptional<z.ZodString>;
|
|
17
|
+
split: z.ZodDefault<z.ZodEnum<{
|
|
18
|
+
development: "development";
|
|
19
|
+
"held-out": "held-out";
|
|
20
|
+
}>>;
|
|
21
|
+
provenance: z.ZodDefault<z.ZodObject<{
|
|
22
|
+
method: z.ZodEnum<{
|
|
23
|
+
"author-synthetic": "author-synthetic";
|
|
24
|
+
"human-reviewed": "human-reviewed";
|
|
25
|
+
"public-dataset": "public-dataset";
|
|
26
|
+
unspecified: "unspecified";
|
|
27
|
+
}>;
|
|
28
|
+
source: z.ZodString;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
contested: z.ZodDefault<z.ZodBoolean>;
|
|
31
|
+
adversarial: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export declare const evaluationPolicySchema: z.ZodObject<{
|
|
34
|
+
minConfidence: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
selectedOn: z.ZodOptional<z.ZodObject<{
|
|
36
|
+
runId: z.ZodString;
|
|
37
|
+
datasetFingerprint: z.ZodString;
|
|
38
|
+
recipeFingerprint: z.ZodString;
|
|
39
|
+
model: z.ZodString;
|
|
40
|
+
split: z.ZodLiteral<"development">;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
export declare const priceSchema: z.ZodObject<{
|
|
44
|
+
model: z.ZodString;
|
|
45
|
+
inputPerMillion: z.ZodNumber;
|
|
46
|
+
outputPerMillion: z.ZodNumber;
|
|
47
|
+
currency: z.ZodLiteral<"USD">;
|
|
48
|
+
date: z.ZodISODate;
|
|
49
|
+
source: z.ZodString;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
export declare const evaluationOptionsSchema: z.ZodObject<{
|
|
52
|
+
concurrency: z.ZodDefault<z.ZodNumber>;
|
|
53
|
+
maxCases: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
maxRequests: z.ZodDefault<z.ZodNumber>;
|
|
55
|
+
model: z.ZodOptional<z.ZodString>;
|
|
56
|
+
split: z.ZodDefault<z.ZodEnum<{
|
|
57
|
+
development: "development";
|
|
58
|
+
"held-out": "held-out";
|
|
59
|
+
}>>;
|
|
60
|
+
policy: z.ZodDefault<z.ZodObject<{
|
|
61
|
+
minConfidence: z.ZodOptional<z.ZodNumber>;
|
|
62
|
+
selectedOn: z.ZodOptional<z.ZodObject<{
|
|
63
|
+
runId: z.ZodString;
|
|
64
|
+
datasetFingerprint: z.ZodString;
|
|
65
|
+
recipeFingerprint: z.ZodString;
|
|
66
|
+
model: z.ZodString;
|
|
67
|
+
split: z.ZodLiteral<"development">;
|
|
68
|
+
}, z.core.$strip>>;
|
|
69
|
+
}, z.core.$strip>>;
|
|
70
|
+
price: z.ZodOptional<z.ZodObject<{
|
|
71
|
+
model: z.ZodString;
|
|
72
|
+
inputPerMillion: z.ZodNumber;
|
|
73
|
+
outputPerMillion: z.ZodNumber;
|
|
74
|
+
currency: z.ZodLiteral<"USD">;
|
|
75
|
+
date: z.ZodISODate;
|
|
76
|
+
source: z.ZodString;
|
|
77
|
+
}, z.core.$strip>>;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
export type EvaluationCase = z.infer<typeof evaluationCaseSchema>;
|
|
80
|
+
export type EvaluationPolicy = z.infer<typeof evaluationPolicySchema>;
|
|
81
|
+
export type EvaluationOptions = z.input<typeof evaluationOptionsSchema>;
|
|
82
|
+
export type Price = z.infer<typeof priceSchema>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { nonEmptyText, probability } from '../src/schema.js';
|
|
3
|
+
import { recordWithOwnKeys, jsonValueSchema } from '../src/data.js';
|
|
4
|
+
export const provenanceSchema = z.object({
|
|
5
|
+
method: z.enum(['author-synthetic', 'human-reviewed', 'public-dataset', 'unspecified']),
|
|
6
|
+
source: nonEmptyText,
|
|
7
|
+
});
|
|
8
|
+
export const evaluationCaseSchema = z.object({
|
|
9
|
+
id: nonEmptyText,
|
|
10
|
+
input: recordWithOwnKeys(z.string(), jsonValueSchema),
|
|
11
|
+
expected: recordWithOwnKeys(nonEmptyText, jsonValueSchema).refine((expected) => Object.keys(expected).length > 0, 'Provide at least one expected result field.'),
|
|
12
|
+
rationale: nonEmptyText,
|
|
13
|
+
family: nonEmptyText.optional(),
|
|
14
|
+
split: z.enum(['development', 'held-out']).default('development'),
|
|
15
|
+
provenance: provenanceSchema.default({
|
|
16
|
+
method: 'unspecified',
|
|
17
|
+
source: 'Not recorded by the dataset author.',
|
|
18
|
+
}),
|
|
19
|
+
contested: z.boolean().default(false),
|
|
20
|
+
adversarial: z.boolean().default(false),
|
|
21
|
+
});
|
|
22
|
+
export const evaluationPolicySchema = z.object({
|
|
23
|
+
minConfidence: probability.optional(),
|
|
24
|
+
selectedOn: z
|
|
25
|
+
.object({
|
|
26
|
+
runId: nonEmptyText,
|
|
27
|
+
datasetFingerprint: nonEmptyText,
|
|
28
|
+
recipeFingerprint: nonEmptyText,
|
|
29
|
+
model: nonEmptyText,
|
|
30
|
+
split: z.literal('development'),
|
|
31
|
+
})
|
|
32
|
+
.optional(),
|
|
33
|
+
});
|
|
34
|
+
export const priceSchema = z.object({
|
|
35
|
+
model: nonEmptyText,
|
|
36
|
+
inputPerMillion: z.number().nonnegative(),
|
|
37
|
+
outputPerMillion: z.number().nonnegative(),
|
|
38
|
+
currency: z.literal('USD'),
|
|
39
|
+
date: z.iso.date(),
|
|
40
|
+
source: nonEmptyText,
|
|
41
|
+
});
|
|
42
|
+
export const evaluationOptionsSchema = z.object({
|
|
43
|
+
concurrency: z.number().int().min(1).max(32).default(4),
|
|
44
|
+
maxCases: z.number().int().positive().default(1000),
|
|
45
|
+
maxRequests: z.number().int().positive().default(1000),
|
|
46
|
+
model: nonEmptyText.optional(),
|
|
47
|
+
split: z.enum(['development', 'held-out']).default('development'),
|
|
48
|
+
policy: evaluationPolicySchema.default({}),
|
|
49
|
+
price: priceSchema.optional(),
|
|
50
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"input": {
|
|
3
|
+
"conversation": "Avery: I will call you when the replacement arrives. Sam: Thanks, I will wait for your call.",
|
|
4
|
+
"roles": "Avery is the business representative. Sam is the customer.",
|
|
5
|
+
"minConfidence": 0.8
|
|
6
|
+
},
|
|
7
|
+
"response": {
|
|
8
|
+
"model": "demo-fixture",
|
|
9
|
+
"answers": {
|
|
10
|
+
"decision": {
|
|
11
|
+
"type": "choice",
|
|
12
|
+
"choice": "business",
|
|
13
|
+
"confidence": 0.94,
|
|
14
|
+
"probabilities": {
|
|
15
|
+
"business": 0.94,
|
|
16
|
+
"customer": 0.01,
|
|
17
|
+
"either": 0.01,
|
|
18
|
+
"none": 0.01,
|
|
19
|
+
"unclear": 0.03
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"usage": { "input_tokens": 0, "output_tokens": 0 }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RecipeOptions } from '../../src/schema.js';
|
|
2
|
+
import type { CallbackResponsibilityInput, CallbackResponsibilityResult } from './schema.js';
|
|
3
|
+
export declare function callbackResponsibility(input: CallbackResponsibilityInput, options?: RecipeOptions): Promise<CallbackResponsibilityResult>;
|
|
4
|
+
export { callbackResponsibilityInputSchema, callbackResponsibilityResultSchema, callbackResponsibilityVerdictSchema, } from './schema.js';
|
|
5
|
+
export type { CallbackResponsibilityInput, CallbackResponsibilityResult, CallbackResponsibilityVerdict, } from './schema.js';
|