@cogitator-ai/evals 0.8.0 → 0.10.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 +255 -54
- package/dist/assertions/error-rate.d.ts +8 -0
- package/dist/assertions/error-rate.d.ts.map +1 -0
- package/dist/assertions/error-rate.js +27 -0
- package/dist/assertions/error-rate.js.map +1 -0
- package/dist/assertions/index.d.ts +3 -0
- package/dist/assertions/index.d.ts.map +1 -1
- package/dist/assertions/index.js +1 -0
- package/dist/assertions/index.js.map +1 -1
- package/dist/assertions/regression.d.ts.map +1 -1
- package/dist/assertions/regression.js +3 -1
- package/dist/assertions/regression.js.map +1 -1
- package/dist/assertions/threshold.d.ts.map +1 -1
- package/dist/assertions/threshold.js +4 -1
- package/dist/assertions/threshold.js.map +1 -1
- package/dist/datasets/csv-loader.d.ts +5 -0
- package/dist/datasets/csv-loader.d.ts.map +1 -1
- package/dist/datasets/csv-loader.js +89 -7
- package/dist/datasets/csv-loader.js.map +1 -1
- package/dist/datasets/dataset.d.ts +11 -2
- package/dist/datasets/dataset.d.ts.map +1 -1
- package/dist/datasets/dataset.js +42 -13
- package/dist/datasets/dataset.js.map +1 -1
- package/dist/datasets/issues.d.ts +4 -0
- package/dist/datasets/issues.d.ts.map +1 -0
- package/dist/datasets/issues.js +7 -0
- package/dist/datasets/issues.js.map +1 -0
- package/dist/datasets/jsonl-loader.d.ts.map +1 -1
- package/dist/datasets/jsonl-loader.js +6 -1
- package/dist/datasets/jsonl-loader.js.map +1 -1
- package/dist/eval-builder.d.ts +3 -0
- package/dist/eval-builder.d.ts.map +1 -1
- package/dist/eval-builder.js +7 -0
- package/dist/eval-builder.js.map +1 -1
- package/dist/eval-comparison.d.ts +7 -0
- package/dist/eval-comparison.d.ts.map +1 -1
- package/dist/eval-comparison.js +42 -12
- package/dist/eval-comparison.js.map +1 -1
- package/dist/eval-suite.d.ts +55 -8
- package/dist/eval-suite.d.ts.map +1 -1
- package/dist/eval-suite.js +254 -53
- package/dist/eval-suite.js.map +1 -1
- package/dist/index.d.ts +14 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/matchers.d.ts +68 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +112 -0
- package/dist/matchers.js.map +1 -0
- package/dist/metrics/custom.d.ts +19 -7
- package/dist/metrics/custom.d.ts.map +1 -1
- package/dist/metrics/custom.js +5 -0
- package/dist/metrics/custom.js.map +1 -1
- package/dist/metrics/index.d.ts +8 -4
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +4 -2
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/llm-judge.d.ts +20 -1
- package/dist/metrics/llm-judge.d.ts.map +1 -1
- package/dist/metrics/llm-judge.js +162 -20
- package/dist/metrics/llm-judge.js.map +1 -1
- package/dist/metrics/matching.d.ts +28 -0
- package/dist/metrics/matching.d.ts.map +1 -0
- package/dist/metrics/matching.js +124 -0
- package/dist/metrics/matching.js.map +1 -0
- package/dist/metrics/retrieval.d.ts +21 -0
- package/dist/metrics/retrieval.d.ts.map +1 -0
- package/dist/metrics/retrieval.js +108 -0
- package/dist/metrics/retrieval.js.map +1 -0
- package/dist/metrics/statistical.d.ts +28 -0
- package/dist/metrics/statistical.d.ts.map +1 -1
- package/dist/metrics/statistical.js +61 -0
- package/dist/metrics/statistical.js.map +1 -1
- package/dist/metrics/tool-calls.d.ts +43 -0
- package/dist/metrics/tool-calls.d.ts.map +1 -0
- package/dist/metrics/tool-calls.js +106 -0
- package/dist/metrics/tool-calls.js.map +1 -0
- package/dist/metrics/types.d.ts +53 -5
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/types.js.map +1 -1
- package/dist/reporters/ci.d.ts +4 -0
- package/dist/reporters/ci.d.ts.map +1 -1
- package/dist/reporters/ci.js +5 -1
- package/dist/reporters/ci.js.map +1 -1
- package/dist/reporters/console.d.ts.map +1 -1
- package/dist/reporters/console.js +11 -4
- package/dist/reporters/console.js.map +1 -1
- package/dist/reporters/csv.d.ts.map +1 -1
- package/dist/reporters/csv.js +5 -2
- package/dist/reporters/csv.js.map +1 -1
- package/dist/reporters/format.d.ts +19 -0
- package/dist/reporters/format.d.ts.map +1 -0
- package/dist/reporters/format.js +31 -0
- package/dist/reporters/format.js.map +1 -0
- package/dist/reporters/github.d.ts +11 -0
- package/dist/reporters/github.d.ts.map +1 -0
- package/dist/reporters/github.js +95 -0
- package/dist/reporters/github.js.map +1 -0
- package/dist/reporters/index.d.ts +44 -20
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +14 -3
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/junit.d.ts +8 -0
- package/dist/reporters/junit.d.ts.map +1 -0
- package/dist/reporters/junit.js +107 -0
- package/dist/reporters/junit.js.map +1 -0
- package/dist/schema.d.ts +25 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +29 -0
- package/dist/schema.js.map +1 -1
- package/dist/vitest.d.ts +35 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +85 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +17 -3
package/dist/eval-suite.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Dataset } from './datasets/index.js';
|
|
2
2
|
import type { EvalCase } from './schema.js';
|
|
3
3
|
import type { JudgeConfigInput } from './schema.js';
|
|
4
|
-
import type { MetricFn,
|
|
4
|
+
import type { MetricFn, Passage, ScoredCaseResult, StatisticalMetricFn } from './metrics/types.js';
|
|
5
5
|
import type { LLMMetricFn } from './metrics/llm-judge.js';
|
|
6
6
|
import type { AssertionFn, AssertionResult, AggregatedMetric, EvalStats } from './assertions/index.js';
|
|
7
7
|
import type { ReporterType, ReporterOptions } from './reporters/index.js';
|
|
8
|
+
/** A turn of a conversation: what the user said and what the target answered */
|
|
9
|
+
export interface EvalTurn {
|
|
10
|
+
input: string;
|
|
11
|
+
output: string;
|
|
12
|
+
}
|
|
8
13
|
/** What a function target gets besides the input */
|
|
9
14
|
export interface EvalTargetContext {
|
|
10
15
|
/**
|
|
@@ -17,11 +22,27 @@ export interface EvalTargetContext {
|
|
|
17
22
|
case: EvalCase;
|
|
18
23
|
/** 0 for the first attempt, then 1, 2 and so on for retries */
|
|
19
24
|
attempt: number;
|
|
25
|
+
/** 0 for the case's `input`, then 1, 2 and so on for each of its further `turns` */
|
|
26
|
+
turn: number;
|
|
27
|
+
/** The turns of the case before this one, with the answers the target gave */
|
|
28
|
+
history: readonly EvalTurn[];
|
|
29
|
+
}
|
|
30
|
+
/** A tool result as the `retrieval` of a target reads it */
|
|
31
|
+
export interface RetrievalToolResult {
|
|
32
|
+
name: string;
|
|
33
|
+
result: unknown;
|
|
20
34
|
}
|
|
21
35
|
export interface EvalTarget {
|
|
22
36
|
agent?: unknown;
|
|
23
37
|
cogitator?: unknown;
|
|
38
|
+
/** Called once per turn of a case: with `input`, then with each of its further `turns` */
|
|
24
39
|
fn?: (input: string, context: EvalTargetContext) => Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Reads the passages a tool result of an agent target surfaced, into the case's `retrieved`,
|
|
42
|
+
* undefined for a result that holds none. Default: `ragSearchPassages`, which reads the
|
|
43
|
+
* results of the `rag_search` tool of `@cogitator-ai/rag`
|
|
44
|
+
*/
|
|
45
|
+
retrieval?: (toolResult: RetrievalToolResult) => Passage[] | undefined;
|
|
25
46
|
}
|
|
26
47
|
export interface EvalProgress {
|
|
27
48
|
completed: number;
|
|
@@ -38,7 +59,19 @@ export interface EvalSuiteOptions {
|
|
|
38
59
|
concurrency?: number;
|
|
39
60
|
timeout?: number;
|
|
40
61
|
retries?: number;
|
|
41
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Runs of each case, a positive integer, 1 by default. Each trial is scored on its own, the
|
|
64
|
+
* case result is the first trial that answered (the first when all errored) with every trial in
|
|
65
|
+
* `trials`, and the aggregate of a metric
|
|
66
|
+
* takes the mean of a case's scored trials as the case's score. A case counts as errored in
|
|
67
|
+
* `stats.errors` when every trial errored, and `latency()`, `cost()` and `tokenUsage()` see
|
|
68
|
+
* every trial. Use `passAtK()` to score how often a case passes
|
|
69
|
+
*/
|
|
70
|
+
trials?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Called after each case, once all its trials ran. A callback that throws is reported as a
|
|
73
|
+
* process warning
|
|
74
|
+
*/
|
|
42
75
|
onProgress?: (progress: EvalProgress) => void;
|
|
43
76
|
}
|
|
44
77
|
export interface EvalRunOptions {
|
|
@@ -53,9 +86,11 @@ export interface EvalRunOptions {
|
|
|
53
86
|
/** The run-wide figures of a finished suite run, every field filled in */
|
|
54
87
|
export type EvalSuiteStats = Required<EvalStats>;
|
|
55
88
|
export interface EvalSuiteResult {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
/**
|
|
90
|
+
* One result per case, in dataset order. With several trials it is the first trial that
|
|
91
|
+
* answered, or the first when every trial errored, with every trial in its `trials`
|
|
92
|
+
*/
|
|
93
|
+
results: ScoredCaseResult[];
|
|
59
94
|
aggregated: Record<string, AggregatedMetric>;
|
|
60
95
|
assertions: AssertionResult[];
|
|
61
96
|
stats: EvalSuiteStats;
|
|
@@ -73,11 +108,16 @@ export declare class EvalSuite {
|
|
|
73
108
|
private readonly concurrency;
|
|
74
109
|
private readonly timeout;
|
|
75
110
|
private readonly retries;
|
|
111
|
+
private readonly trials;
|
|
76
112
|
private readonly onProgress?;
|
|
77
113
|
constructor(opts: EvalSuiteOptions);
|
|
78
114
|
private validateTarget;
|
|
115
|
+
/** A `passAtK()` metric needs the metric it reads and at least `k` trials */
|
|
116
|
+
private validatePassAtK;
|
|
79
117
|
/** Two metrics with one name would be aggregated, compared and reported as one */
|
|
80
118
|
private validateMetricNames;
|
|
119
|
+
/** The cases a run with `maxCases` goes through, in order */
|
|
120
|
+
cases(maxCases?: number): readonly EvalCase[];
|
|
81
121
|
run(options?: EvalRunOptions): Promise<EvalSuiteResult>;
|
|
82
122
|
private reportProgress;
|
|
83
123
|
private runCase;
|
|
@@ -89,14 +129,21 @@ export declare class EvalSuite {
|
|
|
89
129
|
*/
|
|
90
130
|
private executeCaseAttempt;
|
|
91
131
|
private executeFnTarget;
|
|
132
|
+
/**
|
|
133
|
+
* Runs the agent on each turn of the case. A case with further `turns` runs them all on one
|
|
134
|
+
* thread of its own, so the Cogitator's memory carries the conversation from turn to turn
|
|
135
|
+
*/
|
|
92
136
|
private executeAgentTarget;
|
|
93
137
|
private evaluateCaseMetrics;
|
|
94
138
|
/**
|
|
95
|
-
* A metric's score for one case.
|
|
96
|
-
*
|
|
97
|
-
* the
|
|
139
|
+
* A metric's score for one case. Only cases with an output are scored: a case whose every
|
|
140
|
+
* attempt failed is left unscored, so its empty output neither costs a judge call nor counts as
|
|
141
|
+
* a score of 0 (`stats.errors` and the `maxErrorRate` assertion catch those). A metric that
|
|
142
|
+
* throws, or whose score is not a finite number, scores 0 with the reason in `error`, so one
|
|
143
|
+
* bad metric neither drops the case nor poisons the aggregate with NaN.
|
|
98
144
|
*/
|
|
99
145
|
private scoreMetric;
|
|
146
|
+
/** One score per case and metric: the mean over the case's trials the metric scored */
|
|
100
147
|
private aggregateScores;
|
|
101
148
|
}
|
|
102
149
|
//# sourceMappingURL=eval-suite.d.ts.map
|
package/dist/eval-suite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eval-suite.d.ts","sourceRoot":"","sources":["../src/eval-suite.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"eval-suite.d.ts","sourceRoot":"","sources":["../src/eval-suite.ts"],"names":[],"mappings":"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"}
|
package/dist/eval-suite.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { writeFileSync } from 'node:fs';
|
|
2
3
|
import { EvalSuiteConfigSchema, JudgeConfigSchema } from './schema.js';
|
|
3
4
|
import { nonFiniteScoreError } from './metrics/types.js';
|
|
5
|
+
import { isPassAtKMetric } from './metrics/statistical.js';
|
|
6
|
+
import { ragSearchPassages } from './metrics/retrieval.js';
|
|
4
7
|
import { bindJudgeContext, judgeContextFor } from './metrics/llm-judge.js';
|
|
5
|
-
import { aggregate } from './stats/index.js';
|
|
8
|
+
import { aggregate, mean } from './stats/index.js';
|
|
6
9
|
import { report } from './reporters/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* A turn of a multi-turn case failed: the error of the turn, with the turns run so far (the
|
|
12
|
+
* failed one last) and what they spent
|
|
13
|
+
*/
|
|
14
|
+
class TurnFailure extends Error {
|
|
15
|
+
turns;
|
|
16
|
+
usage;
|
|
17
|
+
constructor(cause, turns, usage) {
|
|
18
|
+
super(errorMessage(cause), { cause });
|
|
19
|
+
this.turns = turns;
|
|
20
|
+
this.usage = usage;
|
|
21
|
+
this.name = 'TurnFailure';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** What the user says to the target, turn by turn: the case's `input`, then its `turns` */
|
|
25
|
+
function turnInputs(evalCase) {
|
|
26
|
+
return [evalCase.input, ...(evalCase.turns ?? [])];
|
|
27
|
+
}
|
|
7
28
|
export function isLLMMetric(m) {
|
|
8
29
|
return 'requiresJudge' in m && m.requiresJudge === true;
|
|
9
30
|
}
|
|
@@ -67,6 +88,25 @@ async function waitAtMost(promise, ms) {
|
|
|
67
88
|
clearTimeout(timer);
|
|
68
89
|
}
|
|
69
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The aggregate of a metric no case was scored on, every case having errored: its figures are
|
|
93
|
+
* NaN, so a threshold, a regression check or a baseline over it fails instead of passing on
|
|
94
|
+
* nothing
|
|
95
|
+
*/
|
|
96
|
+
function unscored(name) {
|
|
97
|
+
return {
|
|
98
|
+
name,
|
|
99
|
+
mean: Number.NaN,
|
|
100
|
+
median: Number.NaN,
|
|
101
|
+
min: Number.NaN,
|
|
102
|
+
max: Number.NaN,
|
|
103
|
+
stdDev: Number.NaN,
|
|
104
|
+
p50: Number.NaN,
|
|
105
|
+
p95: Number.NaN,
|
|
106
|
+
p99: Number.NaN,
|
|
107
|
+
count: 0,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
70
110
|
export class EvalSuite {
|
|
71
111
|
dataset;
|
|
72
112
|
target;
|
|
@@ -76,12 +116,14 @@ export class EvalSuite {
|
|
|
76
116
|
concurrency;
|
|
77
117
|
timeout;
|
|
78
118
|
retries;
|
|
119
|
+
trials;
|
|
79
120
|
onProgress;
|
|
80
121
|
constructor(opts) {
|
|
81
122
|
const config = EvalSuiteConfigSchema.parse({
|
|
82
123
|
concurrency: opts.concurrency,
|
|
83
124
|
timeout: opts.timeout,
|
|
84
125
|
retries: opts.retries,
|
|
126
|
+
trials: opts.trials,
|
|
85
127
|
});
|
|
86
128
|
this.dataset = opts.dataset;
|
|
87
129
|
this.target = opts.target;
|
|
@@ -90,10 +132,12 @@ export class EvalSuite {
|
|
|
90
132
|
this.concurrency = config.concurrency;
|
|
91
133
|
this.timeout = config.timeout;
|
|
92
134
|
this.retries = config.retries;
|
|
135
|
+
this.trials = config.trials;
|
|
93
136
|
this.onProgress = opts.onProgress;
|
|
94
137
|
this.validateTarget();
|
|
95
138
|
const rawMetrics = opts.metrics ?? [];
|
|
96
139
|
this.validateMetricNames(rawMetrics);
|
|
140
|
+
this.validatePassAtK(rawMetrics);
|
|
97
141
|
const hasLLMMetrics = rawMetrics.some(isLLMMetric);
|
|
98
142
|
if (hasLLMMetrics && !opts.judge) {
|
|
99
143
|
throw new Error('LLM metrics require a judge config');
|
|
@@ -123,6 +167,24 @@ export class EvalSuite {
|
|
|
123
167
|
if (hasCogitator && !hasAgent) {
|
|
124
168
|
throw new Error('Cogitator target requires agent instance');
|
|
125
169
|
}
|
|
170
|
+
if (hasFn && this.target.retrieval !== undefined) {
|
|
171
|
+
throw new Error('Target retrieval reads tool results, so it needs an agent target, not fn');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** A `passAtK()` metric needs the metric it reads and at least `k` trials */
|
|
175
|
+
validatePassAtK(metrics) {
|
|
176
|
+
const names = new Set(metrics.map((m) => m.metricName));
|
|
177
|
+
for (const statMetric of this.statisticalMetrics) {
|
|
178
|
+
if (!isPassAtKMetric(statMetric))
|
|
179
|
+
continue;
|
|
180
|
+
const { metric, k } = statMetric.passAtK;
|
|
181
|
+
if (!names.has(metric)) {
|
|
182
|
+
throw new Error(`${statMetric.metricName} reads the metric '${metric}', which the suite does not have: add it to metrics`);
|
|
183
|
+
}
|
|
184
|
+
if (k > this.trials) {
|
|
185
|
+
throw new Error(`${statMetric.metricName} needs at least ${k} trials per case, the suite runs ${this.trials}: set trials to ${k} or more`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
126
188
|
}
|
|
127
189
|
/** Two metrics with one name would be aggregated, compared and reported as one */
|
|
128
190
|
validateMetricNames(metrics) {
|
|
@@ -137,6 +199,10 @@ export class EvalSuite {
|
|
|
137
199
|
seen.add(name);
|
|
138
200
|
}
|
|
139
201
|
}
|
|
202
|
+
/** The cases a run with `maxCases` goes through, in order */
|
|
203
|
+
cases(maxCases) {
|
|
204
|
+
return this.dataset.cases.slice(0, maxCases);
|
|
205
|
+
}
|
|
140
206
|
async run(options = {}) {
|
|
141
207
|
const { maxCases, signal } = options;
|
|
142
208
|
if (maxCases !== undefined && (!Number.isInteger(maxCases) || maxCases < 1)) {
|
|
@@ -144,37 +210,45 @@ export class EvalSuite {
|
|
|
144
210
|
}
|
|
145
211
|
signal?.throwIfAborted();
|
|
146
212
|
const suiteStart = Date.now();
|
|
147
|
-
const cases = this.
|
|
213
|
+
const cases = this.cases(maxCases);
|
|
148
214
|
const total = cases.length;
|
|
149
|
-
const
|
|
150
|
-
|
|
215
|
+
const trials = this.trials;
|
|
216
|
+
const trialResults = cases.map(() => new Array(trials));
|
|
217
|
+
const trialsLeft = cases.map(() => trials);
|
|
218
|
+
const runs = total * trials;
|
|
219
|
+
let nextRun = 0;
|
|
151
220
|
let completed = 0;
|
|
152
221
|
const worker = async () => {
|
|
153
|
-
while (
|
|
154
|
-
const
|
|
222
|
+
while (nextRun < runs && !signal?.aborted) {
|
|
223
|
+
const run = nextRun++;
|
|
224
|
+
const i = Math.floor(run / trials);
|
|
155
225
|
const evalCase = cases[i];
|
|
156
|
-
|
|
226
|
+
trialResults[i][run % trials] = await this.runCase(evalCase, signal);
|
|
227
|
+
if (--trialsLeft[i] > 0)
|
|
228
|
+
continue;
|
|
157
229
|
completed++;
|
|
158
230
|
this.reportProgress({ completed, total, currentCase: evalCase });
|
|
159
231
|
}
|
|
160
232
|
};
|
|
161
|
-
await Promise.all(Array.from({ length: Math.min(this.concurrency,
|
|
233
|
+
await Promise.all(Array.from({ length: Math.min(this.concurrency, runs) }, worker));
|
|
162
234
|
signal?.throwIfAborted();
|
|
235
|
+
const orderedResults = trialResults.map((caseTrials) => trials === 1 ? caseTrials[0] : { ...leadTrial(caseTrials), trials: caseTrials });
|
|
236
|
+
const allTrials = orderedResults.flatMap(trialsOf);
|
|
163
237
|
const aggregated = this.aggregateScores(orderedResults);
|
|
164
238
|
for (const statMetric of this.statisticalMetrics) {
|
|
165
|
-
const score = statMetric(orderedResults);
|
|
239
|
+
const score = statMetric(isPassAtKMetric(statMetric) ? orderedResults : allTrials);
|
|
166
240
|
aggregated[score.name] = {
|
|
167
241
|
name: score.name,
|
|
168
242
|
...aggregate(score.values ?? [score.score]),
|
|
169
243
|
...(score.metadata && { metadata: score.metadata }),
|
|
170
244
|
};
|
|
171
245
|
}
|
|
172
|
-
const targetCost =
|
|
173
|
-
const judgeCost =
|
|
246
|
+
const targetCost = allTrials.reduce((sum, r) => sum + (r.usage?.cost ?? 0), 0);
|
|
247
|
+
const judgeCost = allTrials.reduce((sum, r) => sum + r.scores.reduce((s, score) => s + (score.usage?.cost ?? 0), 0), 0);
|
|
174
248
|
const stats = {
|
|
175
249
|
total,
|
|
176
250
|
errors: orderedResults.filter((r) => r.error !== undefined).length,
|
|
177
|
-
metricErrors:
|
|
251
|
+
metricErrors: allTrials.reduce((n, r) => n + r.scores.filter((s) => s.error !== undefined).length, 0),
|
|
178
252
|
duration: Date.now() - suiteStart,
|
|
179
253
|
cost: targetCost + judgeCost,
|
|
180
254
|
targetCost,
|
|
@@ -188,13 +262,7 @@ export class EvalSuite {
|
|
|
188
262
|
stats,
|
|
189
263
|
report: (type, options) => {
|
|
190
264
|
report({
|
|
191
|
-
results: orderedResults.map(
|
|
192
|
-
case: { input: r.case.input, expected: r.case.expected },
|
|
193
|
-
output: r.output,
|
|
194
|
-
duration: r.duration,
|
|
195
|
-
...(r.error !== undefined && { error: r.error }),
|
|
196
|
-
scores: r.scores,
|
|
197
|
-
})),
|
|
265
|
+
results: orderedResults.map(reportedCase),
|
|
198
266
|
aggregated,
|
|
199
267
|
assertions: assertionResults,
|
|
200
268
|
stats,
|
|
@@ -234,13 +302,14 @@ export class EvalSuite {
|
|
|
234
302
|
}
|
|
235
303
|
async runCase(evalCase, signal) {
|
|
236
304
|
const caseResult = await this.executeCase(evalCase, signal);
|
|
237
|
-
if (signal?.aborted)
|
|
305
|
+
if (signal?.aborted || caseResult.error !== undefined)
|
|
238
306
|
return { ...caseResult, scores: [] };
|
|
239
307
|
return { ...caseResult, scores: await this.evaluateCaseMetrics(caseResult) };
|
|
240
308
|
}
|
|
241
309
|
async executeCase(evalCase, signal) {
|
|
242
310
|
const start = Date.now();
|
|
243
311
|
let lastError;
|
|
312
|
+
let lastTurns;
|
|
244
313
|
let usage;
|
|
245
314
|
let attempts = 0;
|
|
246
315
|
for (let attempt = 0; attempt <= this.retries && !signal?.aborted; attempt++) {
|
|
@@ -252,6 +321,7 @@ export class EvalSuite {
|
|
|
252
321
|
}
|
|
253
322
|
usage = addUsage(usage, outcome.usage);
|
|
254
323
|
lastError = outcome.error;
|
|
324
|
+
lastTurns = outcome.turns;
|
|
255
325
|
}
|
|
256
326
|
return {
|
|
257
327
|
case: evalCase,
|
|
@@ -260,6 +330,7 @@ export class EvalSuite {
|
|
|
260
330
|
error: errorMessage(signal?.aborted ? signal.reason : lastError),
|
|
261
331
|
attempts,
|
|
262
332
|
...(usage && { usage }),
|
|
333
|
+
...(lastTurns && { turns: lastTurns }),
|
|
263
334
|
};
|
|
264
335
|
}
|
|
265
336
|
/**
|
|
@@ -280,10 +351,12 @@ export class EvalSuite {
|
|
|
280
351
|
try {
|
|
281
352
|
const first = await Promise.race([settled, abort.aborted]);
|
|
282
353
|
if (first !== 'aborted' && (first.ok || !signal.aborted))
|
|
283
|
-
return first;
|
|
354
|
+
return failureDetails(first);
|
|
284
355
|
const late = await waitAtMost(settled, this.timeout);
|
|
285
|
-
const
|
|
286
|
-
|
|
356
|
+
const lateOutcome = late && failureDetails(late);
|
|
357
|
+
const usage = lateOutcome?.ok ? lateOutcome.result.usage : lateOutcome?.usage;
|
|
358
|
+
const turns = lateOutcome && !lateOutcome.ok ? lateOutcome.turns : undefined;
|
|
359
|
+
return { ok: false, error: signal.reason, ...(usage && { usage }), ...(turns && { turns }) };
|
|
287
360
|
}
|
|
288
361
|
finally {
|
|
289
362
|
clearTimeout(timer);
|
|
@@ -292,31 +365,105 @@ export class EvalSuite {
|
|
|
292
365
|
}
|
|
293
366
|
async executeFnTarget(evalCase, context) {
|
|
294
367
|
const start = Date.now();
|
|
295
|
-
const
|
|
296
|
-
|
|
368
|
+
const inputs = turnInputs(evalCase);
|
|
369
|
+
const multiTurn = inputs.length > 1;
|
|
370
|
+
const turns = [];
|
|
371
|
+
for (const [turn, input] of inputs.entries()) {
|
|
372
|
+
if (turn > 0 && context.signal.aborted) {
|
|
373
|
+
throw new TurnFailure(context.signal.reason, [...turns], undefined);
|
|
374
|
+
}
|
|
375
|
+
const history = turns.map(({ input, output }) => ({ input, output }));
|
|
376
|
+
try {
|
|
377
|
+
const output = await this.target.fn(input, { ...context, turn, history });
|
|
378
|
+
turns.push({ input, output });
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (!multiTurn)
|
|
382
|
+
throw error;
|
|
383
|
+
throw new TurnFailure(error, [...turns, { input, output: '', error: errorMessage(error) }], undefined);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
case: evalCase,
|
|
388
|
+
output: turns[turns.length - 1].output,
|
|
389
|
+
duration: Date.now() - start,
|
|
390
|
+
...(multiTurn && { turns }),
|
|
391
|
+
};
|
|
297
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Runs the agent on each turn of the case. A case with further `turns` runs them all on one
|
|
395
|
+
* thread of its own, so the Cogitator's memory carries the conversation from turn to turn
|
|
396
|
+
*/
|
|
298
397
|
async executeAgentTarget(evalCase, signal) {
|
|
299
398
|
const start = Date.now();
|
|
300
399
|
const cogitator = this.target.cogitator;
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
const
|
|
400
|
+
const retrieval = this.target.retrieval ?? ragSearchPassages;
|
|
401
|
+
const inputs = turnInputs(evalCase);
|
|
402
|
+
const threadId = inputs.length > 1 ? `eval-${randomUUID()}` : undefined;
|
|
403
|
+
const turns = [];
|
|
404
|
+
const toolCalls = [];
|
|
405
|
+
const toolResults = [];
|
|
406
|
+
const retrieved = [];
|
|
407
|
+
let reportsToolCalls = false;
|
|
408
|
+
let usage;
|
|
409
|
+
const flags = {};
|
|
410
|
+
for (const input of inputs) {
|
|
411
|
+
if (turns.length > 0 && signal.aborted) {
|
|
412
|
+
throw new TurnFailure(signal.reason, [...turns], usage);
|
|
413
|
+
}
|
|
414
|
+
const turnResults = [];
|
|
415
|
+
let runResult;
|
|
416
|
+
try {
|
|
417
|
+
runResult = await cogitator.run(this.target.agent, {
|
|
418
|
+
input,
|
|
419
|
+
context: evalCase.context,
|
|
420
|
+
signal,
|
|
421
|
+
...(threadId !== undefined && { threadId }),
|
|
422
|
+
onToolResult: (toolResult) => turnResults.push(toolResultOf(toolResult)),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
if (threadId === undefined)
|
|
427
|
+
throw error;
|
|
428
|
+
throw new TurnFailure(error, [...turns, { input, output: '', error: errorMessage(error) }], usage);
|
|
429
|
+
}
|
|
430
|
+
const output = typeof runResult.output === 'string' ? runResult.output : '';
|
|
431
|
+
const turnCalls = Array.isArray(runResult.toolCalls)
|
|
432
|
+
? runResult.toolCalls
|
|
433
|
+
: undefined;
|
|
434
|
+
if (turnCalls)
|
|
435
|
+
reportsToolCalls = true;
|
|
436
|
+
toolCalls.push(...(turnCalls ?? []));
|
|
437
|
+
toolResults.push(...turnResults);
|
|
438
|
+
for (const toolResult of turnResults) {
|
|
439
|
+
if (toolResult.error !== undefined)
|
|
440
|
+
continue;
|
|
441
|
+
retrieved.push(...(retrieval({ name: toolResult.name, result: toolResult.result }) ?? []));
|
|
442
|
+
}
|
|
443
|
+
usage = addUsage(usage, targetUsageOf(runResult.usage));
|
|
444
|
+
if (runResult.truncated === true)
|
|
445
|
+
flags.truncated = true;
|
|
446
|
+
if (runResult.iterationLimitReached === true)
|
|
447
|
+
flags.iterationLimitReached = true;
|
|
448
|
+
if (runResult.status === 'paused')
|
|
449
|
+
flags.paused = true;
|
|
450
|
+
turns.push({
|
|
451
|
+
input,
|
|
452
|
+
output,
|
|
453
|
+
...(turnCalls && turnCalls.length > 0 && { toolCalls: turnCalls }),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
308
457
|
case: evalCase,
|
|
309
|
-
output:
|
|
310
|
-
duration,
|
|
458
|
+
output: turns[turns.length - 1].output,
|
|
459
|
+
duration: Date.now() - start,
|
|
460
|
+
...(usage && { usage }),
|
|
461
|
+
...(reportsToolCalls && { toolCalls }),
|
|
462
|
+
...(toolResults.length > 0 && { toolResults }),
|
|
463
|
+
...(retrieved.length > 0 && { retrieved }),
|
|
464
|
+
...(threadId !== undefined && { turns }),
|
|
465
|
+
...flags,
|
|
311
466
|
};
|
|
312
|
-
const usage = targetUsageOf(runResult.usage);
|
|
313
|
-
if (usage) {
|
|
314
|
-
result.usage = usage;
|
|
315
|
-
}
|
|
316
|
-
if (runResult.toolCalls) {
|
|
317
|
-
result.toolCalls = runResult.toolCalls;
|
|
318
|
-
}
|
|
319
|
-
return result;
|
|
320
467
|
}
|
|
321
468
|
async evaluateCaseMetrics(result) {
|
|
322
469
|
if (this.boundMetrics.length === 0)
|
|
@@ -324,9 +471,11 @@ export class EvalSuite {
|
|
|
324
471
|
return Promise.all(this.boundMetrics.map((m) => this.scoreMetric(m, result)));
|
|
325
472
|
}
|
|
326
473
|
/**
|
|
327
|
-
* A metric's score for one case.
|
|
328
|
-
*
|
|
329
|
-
* the
|
|
474
|
+
* A metric's score for one case. Only cases with an output are scored: a case whose every
|
|
475
|
+
* attempt failed is left unscored, so its empty output neither costs a judge call nor counts as
|
|
476
|
+
* a score of 0 (`stats.errors` and the `maxErrorRate` assertion catch those). A metric that
|
|
477
|
+
* throws, or whose score is not a finite number, scores 0 with the reason in `error`, so one
|
|
478
|
+
* bad metric neither drops the case nor poisons the aggregate with NaN.
|
|
330
479
|
*/
|
|
331
480
|
async scoreMetric(metric, result) {
|
|
332
481
|
try {
|
|
@@ -352,23 +501,75 @@ export class EvalSuite {
|
|
|
352
501
|
return { name: metric.metricName, score: 0, details: `metric error: ${error}`, error };
|
|
353
502
|
}
|
|
354
503
|
}
|
|
504
|
+
/** One score per case and metric: the mean over the case's trials the metric scored */
|
|
355
505
|
aggregateScores(results) {
|
|
356
506
|
const scoresByMetric = new Map();
|
|
507
|
+
const push = (map, name, value) => {
|
|
508
|
+
const values = map.get(name);
|
|
509
|
+
if (values)
|
|
510
|
+
values.push(value);
|
|
511
|
+
else
|
|
512
|
+
map.set(name, [value]);
|
|
513
|
+
};
|
|
357
514
|
for (const r of results) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
scoresByMetric.set(s.name, arr);
|
|
363
|
-
}
|
|
364
|
-
arr.push(s.score);
|
|
515
|
+
const caseScores = new Map();
|
|
516
|
+
for (const trial of trialsOf(r)) {
|
|
517
|
+
for (const s of trial.scores)
|
|
518
|
+
push(caseScores, s.name, s.score);
|
|
365
519
|
}
|
|
520
|
+
for (const [name, values] of caseScores)
|
|
521
|
+
push(scoresByMetric, name, mean(values));
|
|
366
522
|
}
|
|
367
523
|
const aggregated = {};
|
|
368
524
|
for (const [name, values] of scoresByMetric) {
|
|
369
|
-
aggregated[name] = { name, ...aggregate(values) };
|
|
525
|
+
aggregated[name] = { name, ...aggregate(values), count: values.length };
|
|
526
|
+
}
|
|
527
|
+
if (results.length > 0 && results.flatMap(trialsOf).every((r) => r.error !== undefined)) {
|
|
528
|
+
for (const metric of this.boundMetrics) {
|
|
529
|
+
aggregated[metric.metricName] ??= unscored(metric.metricName);
|
|
530
|
+
}
|
|
370
531
|
}
|
|
371
532
|
return aggregated;
|
|
372
533
|
}
|
|
373
534
|
}
|
|
535
|
+
function trialsOf(result) {
|
|
536
|
+
return result.trials ?? [result];
|
|
537
|
+
}
|
|
538
|
+
function toolResultOf(toolResult) {
|
|
539
|
+
return {
|
|
540
|
+
callId: toolResult.callId,
|
|
541
|
+
name: toolResult.name,
|
|
542
|
+
...(toolResult.result !== undefined && { result: toolResult.result }),
|
|
543
|
+
...(toolResult.error !== undefined && { error: toolResult.error }),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** The turns and usage a failed attempt leaves, when a turn of a multi-turn case failed */
|
|
547
|
+
function failureDetails(settled) {
|
|
548
|
+
if (settled.ok || !(settled.error instanceof TurnFailure))
|
|
549
|
+
return settled;
|
|
550
|
+
const { turns, usage } = settled.error;
|
|
551
|
+
return { ok: false, error: settled.error, turns, ...(usage && { usage }) };
|
|
552
|
+
}
|
|
553
|
+
/** A case result as the reporters get it, the agent's data and the trials kept */
|
|
554
|
+
/** The trial a case is reported by: the first that answered, the first when every trial errored */
|
|
555
|
+
function leadTrial(trials) {
|
|
556
|
+
return trials.find((trial) => trial.error === undefined) ?? trials[0];
|
|
557
|
+
}
|
|
558
|
+
function reportedCase(r) {
|
|
559
|
+
return {
|
|
560
|
+
case: { input: r.case.input, expected: r.case.expected },
|
|
561
|
+
output: r.output,
|
|
562
|
+
duration: r.duration,
|
|
563
|
+
...(r.error !== undefined && { error: r.error }),
|
|
564
|
+
...(r.truncated && { truncated: true }),
|
|
565
|
+
...(r.iterationLimitReached && { iterationLimitReached: true }),
|
|
566
|
+
...(r.paused && { paused: true }),
|
|
567
|
+
...(r.toolCalls && { toolCalls: r.toolCalls }),
|
|
568
|
+
...(r.toolResults && { toolResults: r.toolResults }),
|
|
569
|
+
...(r.retrieved && { retrieved: r.retrieved }),
|
|
570
|
+
...(r.turns && { turns: r.turns }),
|
|
571
|
+
scores: r.scores,
|
|
572
|
+
...(r.trials && { trials: r.trials.map(reportedCase) }),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
374
575
|
//# sourceMappingURL=eval-suite.js.map
|