@cogitator-ai/evals 0.11.0 → 0.13.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 +159 -40
- package/dist/calibrate-judge.d.ts +53 -0
- package/dist/calibrate-judge.d.ts.map +1 -0
- package/dist/calibrate-judge.js +114 -0
- package/dist/calibrate-judge.js.map +1 -0
- package/dist/comparison-stats.d.ts +104 -0
- package/dist/comparison-stats.d.ts.map +1 -0
- package/dist/comparison-stats.js +179 -0
- package/dist/comparison-stats.js.map +1 -0
- package/dist/datasets/dataset.d.ts.map +1 -1
- package/dist/datasets/dataset.js +3 -27
- package/dist/datasets/dataset.js.map +1 -1
- package/dist/eval-comparison.d.ts +30 -10
- package/dist/eval-comparison.d.ts.map +1 -1
- package/dist/eval-comparison.js +73 -103
- package/dist/eval-comparison.js.map +1 -1
- package/dist/eval-suite.d.ts +33 -0
- package/dist/eval-suite.d.ts.map +1 -1
- package/dist/eval-suite.js +81 -2
- package/dist/eval-suite.js.map +1 -1
- package/dist/index.d.ts +21 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/g-eval.d.ts +29 -0
- package/dist/metrics/g-eval.d.ts.map +1 -0
- package/dist/metrics/g-eval.js +89 -0
- package/dist/metrics/g-eval.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -0
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/judge-prompt.d.ts +31 -0
- package/dist/metrics/judge-prompt.d.ts.map +1 -0
- package/dist/metrics/judge-prompt.js +90 -0
- package/dist/metrics/judge-prompt.js.map +1 -0
- package/dist/metrics/llm-judge.d.ts +96 -9
- package/dist/metrics/llm-judge.d.ts.map +1 -1
- package/dist/metrics/llm-judge.js +265 -120
- package/dist/metrics/llm-judge.js.map +1 -1
- package/dist/metrics/types.d.ts +25 -0
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/types.js.map +1 -1
- package/dist/options.d.ts +4 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +9 -0
- package/dist/options.js.map +1 -0
- package/dist/pairwise.d.ts +53 -0
- package/dist/pairwise.d.ts.map +1 -0
- package/dist/pairwise.js +118 -0
- package/dist/pairwise.js.map +1 -0
- package/dist/reporters/comparison.d.ts +24 -0
- package/dist/reporters/comparison.d.ts.map +1 -0
- package/dist/reporters/comparison.js +290 -0
- package/dist/reporters/comparison.js.map +1 -0
- package/dist/reporters/format.d.ts +21 -0
- package/dist/reporters/format.d.ts.map +1 -1
- package/dist/reporters/format.js +31 -0
- package/dist/reporters/format.js.map +1 -1
- package/dist/reporters/github.d.ts.map +1 -1
- package/dist/reporters/github.js +1 -16
- package/dist/reporters/github.js.map +1 -1
- package/dist/reporters/html-format.d.ts +11 -0
- package/dist/reporters/html-format.d.ts.map +1 -0
- package/dist/reporters/html-format.js +111 -0
- package/dist/reporters/html-format.js.map +1 -0
- package/dist/reporters/html.d.ts +12 -0
- package/dist/reporters/html.d.ts.map +1 -0
- package/dist/reporters/html.js +119 -0
- package/dist/reporters/html.js.map +1 -0
- package/dist/reporters/index.d.ts +20 -6
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +15 -4
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/markdown.d.ts +15 -0
- package/dist/reporters/markdown.d.ts.map +1 -0
- package/dist/reporters/markdown.js +67 -0
- package/dist/reporters/markdown.js.map +1 -0
- package/dist/runs/compare.d.ts +103 -0
- package/dist/runs/compare.d.ts.map +1 -0
- package/dist/runs/compare.js +208 -0
- package/dist/runs/compare.js.map +1 -0
- package/dist/runs/index.d.ts +9 -0
- package/dist/runs/index.d.ts.map +1 -0
- package/dist/runs/index.js +5 -0
- package/dist/runs/index.js.map +1 -0
- package/dist/runs/json.d.ts +11 -0
- package/dist/runs/json.d.ts.map +1 -0
- package/dist/runs/json.js +58 -0
- package/dist/runs/json.js.map +1 -0
- package/dist/runs/run.d.ts +44 -0
- package/dist/runs/run.d.ts.map +1 -0
- package/dist/runs/run.js +136 -0
- package/dist/runs/run.js.map +1 -0
- package/dist/runs/schema.d.ts +504 -0
- package/dist/runs/schema.d.ts.map +1 -0
- package/dist/runs/schema.js +171 -0
- package/dist/runs/schema.js.map +1 -0
- package/dist/runs/store.d.ts +57 -0
- package/dist/runs/store.d.ts.map +1 -0
- package/dist/runs/store.js +146 -0
- package/dist/runs/store.js.map +1 -0
- package/dist/schema.d.ts +16 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +17 -0
- package/dist/schema.js.map +1 -1
- package/dist/stats/bootstrap.d.ts +22 -0
- package/dist/stats/bootstrap.d.ts.map +1 -0
- package/dist/stats/bootstrap.js +34 -0
- package/dist/stats/bootstrap.js.map +1 -0
- package/dist/stats/correlation.d.ts +11 -0
- package/dist/stats/correlation.d.ts.map +1 -0
- package/dist/stats/correlation.js +42 -0
- package/dist/stats/correlation.js.map +1 -0
- package/dist/stats/effect-size.d.ts +7 -0
- package/dist/stats/effect-size.d.ts.map +1 -0
- package/dist/stats/effect-size.js +17 -0
- package/dist/stats/effect-size.js.map +1 -0
- package/dist/stats/holm.d.ts +7 -0
- package/dist/stats/holm.d.ts.map +1 -0
- package/dist/stats/holm.js +21 -0
- package/dist/stats/holm.js.map +1 -0
- package/dist/stats/index.d.ts +10 -2
- package/dist/stats/index.d.ts.map +1 -1
- package/dist/stats/index.js +7 -1
- package/dist/stats/index.js.map +1 -1
- package/dist/stats/mcnemar.d.ts +9 -0
- package/dist/stats/mcnemar.d.ts.map +1 -1
- package/dist/stats/mcnemar.js +27 -13
- package/dist/stats/mcnemar.js.map +1 -1
- package/dist/stats/normal.d.ts +5 -0
- package/dist/stats/normal.d.ts.map +1 -0
- package/dist/stats/normal.js +19 -0
- package/dist/stats/normal.js.map +1 -0
- package/dist/stats/precision.d.ts +12 -0
- package/dist/stats/precision.d.ts.map +1 -0
- package/dist/stats/precision.js +29 -0
- package/dist/stats/precision.js.map +1 -0
- package/dist/stats/random.d.ts +6 -0
- package/dist/stats/random.d.ts.map +1 -0
- package/dist/stats/random.js +25 -0
- package/dist/stats/random.js.map +1 -0
- package/dist/stats/ranks.d.ts +5 -0
- package/dist/stats/ranks.d.ts.map +1 -0
- package/dist/stats/ranks.js +24 -0
- package/dist/stats/ranks.js.map +1 -0
- package/dist/stats/t-test.d.ts +4 -0
- package/dist/stats/t-test.d.ts.map +1 -1
- package/dist/stats/t-test.js +9 -3
- package/dist/stats/t-test.js.map +1 -1
- package/dist/stats/wilcoxon.d.ts +19 -0
- package/dist/stats/wilcoxon.d.ts.map +1 -0
- package/dist/stats/wilcoxon.js +70 -0
- package/dist/stats/wilcoxon.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,20 +13,21 @@ pnpm add @cogitator-ai/core # agent targets and LLM-as-judge metrics
|
|
|
13
13
|
pnpm add -D vitest # describeEval from @cogitator-ai/evals/vitest
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Website docs: [Evals](https://cogitator.app/docs/evals), [Datasets](https://cogitator.app/docs/evals/datasets), [Metrics](https://cogitator.app/docs/evals/metrics), [Assertions](https://cogitator.app/docs/evals/assertions), [Agents](https://cogitator.app/docs/evals/agents), [Comparison](https://cogitator.app/docs/evals/comparison), [Reporters](https://cogitator.app/docs/evals/reporters).
|
|
16
|
+
Website docs: [Evals](https://cogitator.app/docs/evals), [Datasets](https://cogitator.app/docs/evals/datasets), [Metrics](https://cogitator.app/docs/evals/metrics), [Assertions](https://cogitator.app/docs/evals/assertions), [Agents](https://cogitator.app/docs/evals/agents), [Comparison](https://cogitator.app/docs/evals/comparison), [Experiments](https://cogitator.app/docs/evals/experiments), [Reporters](https://cogitator.app/docs/evals/reporters).
|
|
17
17
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
20
20
|
- **EvalSuite** — Run datasets against agents or plain functions with configurable concurrency, timeouts, and retries
|
|
21
21
|
- **8 Deterministic Metrics** - exactMatch, contains, containsAll, containsAny, similarity, tokenF1, regex, jsonSchema (Zod)
|
|
22
|
-
- **
|
|
22
|
+
- **7 LLM-as-Judge Metrics** - faithfulness, relevance, coherence, helpfulness, contextRelevance, gEval, custom llmMetric with a free prompt or a discrete rubric. Prompts keep every untrusted value in escaped tags, `judge.samples` averages several verdicts, and `calibrateJudge()` checks a judge against labels
|
|
23
23
|
- **4 Statistical Metrics** - latency, cost, tokenUsage with full percentile breakdowns, and passAtK over trials
|
|
24
24
|
- **Custom Metrics** — `metric()` factory for anything domain-specific
|
|
25
25
|
- **Agent Evaluation** - `toolCalls()` and `trajectory()` against expected tool calls, `retrievalRecall()`, `retrievalPrecision()`, `retrievalMRR()` and the `contextRelevance()` judge against expected passages, multi-turn cases on one conversation
|
|
26
26
|
- **Trials and pass@k** - run each case several times and score how often it passes with `passAtK()`
|
|
27
27
|
- **Assertions** — threshold, noRegression, custom assertion with auto-detection of lower-is-better metrics
|
|
28
|
-
- **A/B Testing**
|
|
29
|
-
- **
|
|
28
|
+
- **A/B Testing** - EvalComparison with McNemar's test (exact for few discordant pairs), the paired t-test or Wilcoxon signed-rank, Holm's correction across metrics, bootstrap confidence intervals, effect sizes, latency and cost comparisons and pairwise judging
|
|
29
|
+
- **Experiment Tracking** - named runs with metadata saved as versioned run files, a `RunStore` directory of runs, `compareRuns()` with the A/B statistics, per-case regressions and improvements, and warnings when datasets, prompt versions, metrics or judges differ. Agent cases keep the `traceId` and versioned `prompt` of their runs
|
|
30
|
+
- **8 Reporters** - console (colored table), JSON, CSV, JUnit XML, Markdown (pull request comments), self-contained HTML, GitHub Actions annotations and job summary, CI (exit code on failure), plus `reportComparison()` for comparisons
|
|
30
31
|
- **Vitest** - `describeEval` from `@cogitator-ai/evals/vitest` runs a suite as a test file, one test per case plus one for the assertions, and `evalMatchers` from `@cogitator-ai/evals/matchers` (Vitest 5) adds `toHaveCalledTool`, `toFollowTrajectory`, `toHaveRetrieved` and `toScoreAtLeast`
|
|
31
32
|
- **Builder API** — Fluent `EvalBuilder` for composable eval pipelines
|
|
32
33
|
- **Baseline Workflow** — Save baselines, compare against them, catch regressions in CI
|
|
@@ -235,7 +236,8 @@ Metrics scored by an LLM judge (0.0 to 1.0). They require a `judge` config on th
|
|
|
235
236
|
| `coherence` | Logical structure and readability |
|
|
236
237
|
| `helpfulness` | Practical usefulness to the user |
|
|
237
238
|
| `contextRelevance` | Whether the retrieved passages fit the input |
|
|
238
|
-
| `
|
|
239
|
+
| `gEval` | G-Eval: evaluation steps, then a scale |
|
|
240
|
+
| `llmMetric` | Custom prompt or rubric, your criteria |
|
|
239
241
|
|
|
240
242
|
```typescript
|
|
241
243
|
import { faithfulness, relevance, llmMetric } from '@cogitator-ai/evals';
|
|
@@ -263,7 +265,43 @@ Dataset.from([{ input: 'Write the lede', context: { dossier: dossierText } }]);
|
|
|
263
265
|
|
|
264
266
|
When the run retrieved passages (see [Retrieval](#retrieval)), `faithfulness()` checks the response against them instead of `context`. `contextRelevance()` judges the input against the retrieved passages alone, or against `context` when nothing was retrieved, and scores 0 without a judge call when the case has neither.
|
|
265
267
|
|
|
266
|
-
The `judge` option is a `JudgeConfigInput`: `{ model, temperature?, maxTokens?, cogitator?, cache?, concurrency? }`, with `temperature` defaulting to `0`. `cache: true` reuses a verdict for the same model, settings and prompt in memory (a `JudgeCache` with `get` and `set` keeps them across runs), and `concurrency` caps the judge calls running at once.
|
|
268
|
+
The `judge` option is a `JudgeConfigInput`: `{ model, temperature?, maxTokens?, cogitator?, cache?, concurrency?, samples? }`, with `temperature` defaulting to `0`. `cache: true` reuses a verdict for the same model, settings and prompt in memory (a `JudgeCache` with `get` and `set` keeps them across runs), and `concurrency` caps the judge calls running at once.
|
|
269
|
+
|
|
270
|
+
`llmMetric` also takes a discrete `rubric` instead of a free prompt: the judge picks one level, and the score is normalized to 0 - 1 by the lowest and highest level. A rubric needs at least two levels with distinct scores, and a verdict that is not a level counts as a judge error.
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
llmMetric({
|
|
274
|
+
name: 'correctness',
|
|
275
|
+
prompt: 'Grade whether the response answers the question correctly.', // optional with a rubric
|
|
276
|
+
rubric: [
|
|
277
|
+
{ score: 1, description: 'wrong, or does not answer' },
|
|
278
|
+
{ score: 3, description: 'partly correct' },
|
|
279
|
+
{ score: 5, description: 'fully correct' },
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`gEval({ name, criteria, steps?, scale? })` follows G-Eval: the judge scores each case on `scale` (`[1, 5]` by default, normalized to 0 - 1) following evaluation steps. Without `steps`, the judge writes them from `criteria` once per metric instance, before the first case, and every later case reuses them. A failed generation scores the cases waiting on it as judge errors, and the next case tries again. The original G-Eval weights each score by the probability of its token, which not every provider exposes, so set `judge.samples` (with a `temperature` above 0) to average several verdicts instead.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
gEval({
|
|
288
|
+
name: 'clarity',
|
|
289
|
+
criteria: 'A beginner can follow the explanation, and it names the key idea first.',
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Every judge prompt keeps the untrusted values of a case (input, response, expected answer, context, retrieved passages, the turns of a conversation) inside XML-like tags such as `<input>` and `<response>`, with `<`, `>` and `&` escaped, so an answer cannot close its tag or open another, and the judge is told that tag contents are data to evaluate, never instructions. An answer that tells the judge to give it full marks is judged as content.
|
|
294
|
+
|
|
295
|
+
`judge.samples` asks the judge that many times per case and metric (1 by default) and scores the mean of the verdicts it could read. The score's `details` give the spread (`mean of 3 verdicts, spread 0.6..0.9`), `metadata.samples` each verdict, and `usage` the summed cost. With `cache`, each sample is cached under its own key.
|
|
296
|
+
|
|
297
|
+
`calibrateJudge(metric, examples, options?)` checks a judge against labels people gave: it scores each `{ input, output, expected?, context?, label }` (labels from 0 to 1) and returns `{ n, errors, pearson, spearman, mae, kappa, perCase }`, `kappa` being Cohen's kappa of pass and fail at `threshold` (0.5 by default). Pass a metric bound with `bindJudgeContext`, or `judge` with a `cogitator` to bind it. Examples the metric fails on are reported in `perCase` and left out of the figures.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const calibration = await calibrateJudge(correctness, labelled, {
|
|
301
|
+
judge: { model: 'openai/gpt-6.1-sol', cogitator: cog },
|
|
302
|
+
});
|
|
303
|
+
console.log(calibration.spearman, calibration.kappa);
|
|
304
|
+
```
|
|
267
305
|
|
|
268
306
|
The judge answers with `{ "score", "reasoning" }` JSON. A verdict in a code fence or with the score as a numeric string is read as well, and an answer without JSON falls back to its last labelled score (`Score: 0.4`, `score is 4/5`), never to a stray number in the prose. When no verdict can be read, or the judge call fails, the metric scores 0 with the reason in `score.error`. The judge's usage is reported on each score and counted in `stats.judgeCost`.
|
|
269
307
|
|
|
@@ -505,6 +543,30 @@ for (const [name, mc] of Object.entries(result.summary.metrics)) {
|
|
|
505
543
|
}
|
|
506
544
|
```
|
|
507
545
|
|
|
546
|
+
Binary metrics (every score 0 or 1) take McNemar's test, exact below 25 discordant pairs and chi-square with continuity correction from 25. Other metrics take the paired t-test, or Wilcoxon's signed-rank test with `test: 'wilcoxon'` (zero differences dropped, exact up to 20 pairs, normal approximation above). Paired differences are rounded to 12 significant digits, so float noise neither splits ties nor hides a zero. The winner follows the test's direction: the higher mean for McNemar's and the t-test, the larger rank sum for Wilcoxon's, which can disagree with `difference`, the mean difference. The p-values are corrected with Holm's method across the compared metrics: `pValue` is the test's own, `adjustedPValue` the corrected one, and `significant` means `adjustedPValue < alpha` (`alpha` 0.05 by default). Each metric also reports `difference` (the mean of challenger minus baseline), `ci` (a percentile bootstrap interval of the paired differences at `1 - alpha`, the same on every run with `seed`), `effectSize` (Cohen's d of the paired differences) and the `test` it ran.
|
|
547
|
+
|
|
548
|
+
`result.summary.latency` and `result.summary.cost` compare milliseconds and USD per case, paired over the cases both sides answered, where lower is better: means, `difference`, `pValue` and `winner`. `cost` is there only when both sides reported usage. Neither takes part in the metrics' vote on the overall winner.
|
|
549
|
+
|
|
550
|
+
With `pairwise`, the judge also reads both answers of each case and says which is better, twice, the second time with the answers swapped. Agreeing verdicts give the case to that side, disagreeing ones or a tie make it a tie, a side that errored loses the case without a judge call, and a case both sides errored on is left out. A judge failure counts as a tie and is reported.
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
const comparison = new EvalComparison({
|
|
554
|
+
dataset,
|
|
555
|
+
targets: { baseline, challenger },
|
|
556
|
+
metrics: [exactMatch()],
|
|
557
|
+
judge: { model: 'openai/gpt-6.1-sol', cogitator: cog },
|
|
558
|
+
pairwise: { criteria: 'Which answer is more accurate.' },
|
|
559
|
+
test: 'wilcoxon',
|
|
560
|
+
alpha: 0.05,
|
|
561
|
+
seed: 42,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const { pairwise } = await comparison.run();
|
|
565
|
+
// { name, wins: { baseline, challenger }, ties, cases, winRate, judgeErrors, cost, perCase }
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
`winRate` is the challenger's wins over the decided cases, NaN when none was decided.
|
|
569
|
+
|
|
508
570
|
Access full suite results via `result.baseline` and `result.challenger`. `comparison.run({ signal })` cancels both runs.
|
|
509
571
|
|
|
510
572
|
---
|
|
@@ -513,14 +575,16 @@ Access full suite results via `result.baseline` and `result.challenger`. `compar
|
|
|
513
575
|
|
|
514
576
|
Call `result.report()` after a suite run to output results.
|
|
515
577
|
|
|
516
|
-
| Reporter
|
|
517
|
-
|
|
|
518
|
-
| `console`
|
|
519
|
-
| `json`
|
|
520
|
-
| `csv`
|
|
521
|
-
| `junit`
|
|
522
|
-
| `github`
|
|
523
|
-
| `ci`
|
|
578
|
+
| Reporter | Output |
|
|
579
|
+
| ---------- | --------------------------------------------------------------------- |
|
|
580
|
+
| `console` | Colored table with metrics, assertions, summary |
|
|
581
|
+
| `json` | Writes `eval-report.json` (configurable path) |
|
|
582
|
+
| `csv` | Writes `eval-report.csv` (configurable path) |
|
|
583
|
+
| `junit` | Writes `eval-report.xml`, a JUnit report (configurable path) |
|
|
584
|
+
| `github` | GitHub Actions annotations, plus a job summary in Actions |
|
|
585
|
+
| `ci` | Compact verdict, sets `process.exitCode = 1` when an assertion fails |
|
|
586
|
+
| `markdown` | Writes `eval-report.md`, a compact summary for a pull request comment |
|
|
587
|
+
| `html` | Writes `eval-report.html`, a self-contained page for light and dark |
|
|
524
588
|
|
|
525
589
|
```typescript
|
|
526
590
|
const result = await suite.run();
|
|
@@ -531,9 +595,11 @@ result.report(['console', 'json', 'csv', 'junit'], { path: './reports/eval' });
|
|
|
531
595
|
result.report(['github', 'ci']);
|
|
532
596
|
```
|
|
533
597
|
|
|
534
|
-
The `path` is a base path: each file reporter adds its own extension (`.json`, `.csv`, `.xml`). `ci` runs last and does not end the process, so output flushes and later hooks still run before Node exits with code 1.
|
|
598
|
+
The `path` is a base path: each file reporter adds its own extension (`.json`, `.csv`, `.xml`, `.md`, `.html`). `ci` runs last and does not end the process, so output flushes and later hooks still run before Node exits with code 1.
|
|
535
599
|
|
|
536
600
|
- **junit** writes one `<testsuite name="eval">` with a `<testcase>` per case (a `<failure>` when it errored, its scores in `<system-out>`) and a `<testcase>` per assertion (a `<failure>` when it did not pass). Everything is XML-escaped, and characters XML 1.0 does not allow are replaced.
|
|
601
|
+
- **markdown** writes the totals (with the trials per case when there are several), each metric's mean and cases, the assertions with pass or fail, and the failures of every case and trial. Every value is escaped, so no answer breaks the tables.
|
|
602
|
+
- **html** writes one page with inline CSS and no scripts or external assets: totals, metrics, assertions and every case with its scores, status, turns, trials, trace id and prompt version. Every value is HTML-escaped.
|
|
537
603
|
- **github** prints `::error` for each failed assertion and errored case and `::warning` for each failed metric score. When `GITHUB_STEP_SUMMARY` is set it also appends a Markdown summary with the totals, the aggregated metrics (mean, p50, p95, cases scored) and the assertions.
|
|
538
604
|
|
|
539
605
|
Agent run flags (`truncated`, `iterationLimitReached`, `paused`) show up per case in the json, csv and junit reports, and each aggregated metric's `count` (cases it was scored on) in the console, json and github output. The json report keeps each case's `toolCalls`, `toolResults`, `retrieved`, `turns` and `trials`, and the console and github reports show the trials per case.
|
|
@@ -575,6 +641,41 @@ it('books a table', async () => {
|
|
|
575
641
|
|
|
576
642
|
---
|
|
577
643
|
|
|
644
|
+
## Experiment Tracking
|
|
645
|
+
|
|
646
|
+
Give a suite a `name` and JSON `metadata`, save each run as a run file, and compare runs later, across commits or prompt versions.
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { EvalSuite, RunStore, compareRuns, reportComparison } from '@cogitator-ai/evals';
|
|
650
|
+
|
|
651
|
+
const suite = new EvalSuite({
|
|
652
|
+
dataset,
|
|
653
|
+
target: { agent, cogitator },
|
|
654
|
+
metrics: [exactMatch(), faithfulness()],
|
|
655
|
+
judge: { model: 'google/gemini-3.5-flash-lite', cogitator },
|
|
656
|
+
name: 'support-agent',
|
|
657
|
+
metadata: { branch: process.env.GITHUB_HEAD_REF, commit: process.env.GITHUB_SHA },
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const result = await suite.run();
|
|
661
|
+
const store = new RunStore('./eval-runs');
|
|
662
|
+
const base = store.latest({ name: 'support-agent' });
|
|
663
|
+
store.save(result); // ./eval-runs/<runId>.json, as result.saveRun('./eval-runs') writes it
|
|
664
|
+
|
|
665
|
+
if (base) {
|
|
666
|
+
const comparison = compareRuns(base, result, { seed: 42 });
|
|
667
|
+
reportComparison(comparison, ['console', 'markdown'], { path: './reports/comparison' });
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
- **Run files.** `result.saveRun(target)` writes JSON with `format: 'cogitator.eval-run'` and `version: 1`: `runId`, `startedAt` and `finishedAt`, `name`, `metadata`, the dataset fingerprint (SHA-256 of the canonical JSON of the cases run), the config (metric names, judge model, temperature and samples, trials, concurrency, target kind, agent name and model), the prompt versions used, every case result with its `caseKey`, the aggregates, assertions and stats. A directory target (or one ending in `/`) gets `<runId>.json`. Functions, Cogitator instances and keys are never written. `result.toRun()` returns the same object, and `loadRun(path)` reads one back, validated with Zod.
|
|
672
|
+
- **Traces and prompts.** Agent cases keep the `traceId` of their run (`RunResult.trace.traceId`) and the versioned instructions in `prompt` (`{ key, versionId?, version?, abTest? }`, set when the Cogitator versions instructions with `cogitator.prompts`). Each turn of a multi-turn case keeps its own and the case keeps the last turn's, each trial its own.
|
|
673
|
+
- **RunStore.** `new RunStore(dir, { onSkip? })` keeps run files in a directory: `save(result | run)`, `list(filter?)` newest first with id, name, metadata, times and metric means, `load(id)`, and `latest(filter?)`, where a filter is `{ name }` or a predicate. `list()` and `latest()` skip `.json` entries they cannot use (a directory, a file that is not JSON, a run file of another version or edited by hand) and report each to `onSkip`, a process warning by default, while `load(id)` throws for them. A run is validated before it is written, so a hand-made `runId` cannot lead outside the directory.
|
|
674
|
+
- **compareRuns(base, head, { alpha?, test?, seed?, metrics? }).** Cases are matched by `caseKey` (SHA-256 of `{ input, turns, expected }`), the others listed in `added` and `removed`. Each metric both runs have gets the statistics of `EvalComparison`: means, `difference`, `ci`, `effectSize`, `pValue`, `adjustedPValue` (Holm across the compared metrics), `significant` and `winner` (`head`, `base` or `tie`), plus the `improved` and `regressed` cases. An errored side counts as 0, a case both runs errored on is dropped. `warnings` say when the datasets, the prompt versions, the metrics or the judge differ, and name a metric left out because no matched case was scored on it.
|
|
675
|
+
- **reportComparison(comparison, 'markdown' | 'html' | 'console', { path? }).** Renders a `compareRuns()` or `EvalComparison` result: the winner, a metric table with difference, CI, p and adjusted p and significance, the regressions and improvements, and the warnings. The default path is `eval-comparison`.
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
578
679
|
## Builder API
|
|
579
680
|
|
|
580
681
|
`EvalBuilder` provides a fluent interface for constructing eval suites.
|
|
@@ -657,7 +758,12 @@ result.report('ci');
|
|
|
657
758
|
| ---------------- | ------------------------------------------------------------------- |
|
|
658
759
|
| `EvalSuite` | Main evaluation runner |
|
|
659
760
|
| `EvalComparison` | A/B testing runner with statistical significance |
|
|
761
|
+
| `calibrateJudge` | Agreement of a judge metric with labelled examples |
|
|
660
762
|
| `EvalBuilder` | Fluent builder for EvalSuite |
|
|
763
|
+
| `compareRuns` | Compares two saved runs or suite results case by case |
|
|
764
|
+
| `RunStore` | Directory of run files: save, list, load, latest |
|
|
765
|
+
| `loadRun` | Reads and validates a run file |
|
|
766
|
+
| `caseKey` | The key a case is matched by across runs |
|
|
661
767
|
| `Dataset` | Immutable dataset with from/fromJsonl/fromCsv/filter/sample/shuffle |
|
|
662
768
|
| `loadJsonl` | Low-level JSONL file loader |
|
|
663
769
|
| `loadCsv` | Low-level CSV file loader |
|
|
@@ -680,7 +786,8 @@ result.report('ci');
|
|
|
680
786
|
| `coherence` | LLM Judge | Logical structure |
|
|
681
787
|
| `helpfulness` | LLM Judge | Practical usefulness |
|
|
682
788
|
| `contextRelevance` | LLM Judge | Relevance of the retrieved passages to the input |
|
|
683
|
-
| `
|
|
789
|
+
| `gEval` | LLM Judge | Evaluation steps from criteria, then a score on a scale |
|
|
790
|
+
| `llmMetric` | LLM Judge | Custom judge prompt or discrete rubric |
|
|
684
791
|
| `toolCalls` | Agent | Tool calls against `expectedToolCalls`, F1, recall or precision |
|
|
685
792
|
| `trajectory` | Agent | Sequence of tool names, exact, in-order or any-order |
|
|
686
793
|
| `retrievalRecall` | Agent | Share of `expectedPassages` retrieved |
|
|
@@ -704,37 +811,49 @@ result.report('ci');
|
|
|
704
811
|
|
|
705
812
|
### Reporters
|
|
706
813
|
|
|
707
|
-
| Export
|
|
708
|
-
|
|
|
709
|
-
| `report`
|
|
710
|
-
| `
|
|
711
|
-
| `
|
|
814
|
+
| Export | Description |
|
|
815
|
+
| -------------------------------------- | ------------------------------------------------ |
|
|
816
|
+
| `report` | Dispatch to one or more reporter types |
|
|
817
|
+
| `reportComparison` | Render a comparison as console, Markdown or HTML |
|
|
818
|
+
| `comparisonMarkdown`, `comparisonHtml` | The Markdown or HTML of a comparison as a string |
|
|
819
|
+
| `describeEval` (`/vitest` entry) | Run a suite as a Vitest describe block |
|
|
820
|
+
| `evalMatchers` (`/matchers` entry) | Matchers for `expect.extend()`, Vitest 5 |
|
|
712
821
|
|
|
713
|
-
`report` takes `console`, `json`, `csv`, `junit`, `github` and `
|
|
822
|
+
`report` takes `console`, `json`, `csv`, `junit`, `github`, `ci`, `markdown` and `html`. `describeEval` takes `DescribeEvalOptions` and returns a `DescribeEvalHandle`.
|
|
714
823
|
|
|
715
824
|
### Types
|
|
716
825
|
|
|
717
|
-
| Export | Description
|
|
718
|
-
| ------------------ |
|
|
719
|
-
| `JudgeConfigInput` | The `judge` option: model, settings, `cogitator`, `cache`, `concurrency`
|
|
720
|
-
| `JudgeCache` | Where verdicts are kept across runs: `get(key)` and `set(key, verdict)`
|
|
721
|
-
| `AggregatedMetric` | Aggregate of a metric, with `count` for the cases it covers
|
|
722
|
-
| `MetricComparison` | A metric of a comparison, with `n` for the cases it was compared on
|
|
723
|
-
| `ExpectedToolCall` | A tool call a case expects: `name` and optional `arguments`
|
|
724
|
-
| `Passage` | A retrieved passage: `id?`, `content`, `score?`, `source?`
|
|
725
|
-
| `EvalTurnResult` | A turn of a multi-turn case: `input`, `output`, `toolCalls?`, `error?`
|
|
826
|
+
| Export | Description |
|
|
827
|
+
| ------------------ | --------------------------------------------------------------------------------------------- |
|
|
828
|
+
| `JudgeConfigInput` | The `judge` option: model, settings, `cogitator`, `cache`, `concurrency` |
|
|
829
|
+
| `JudgeCache` | Where verdicts are kept across runs: `get(key)` and `set(key, verdict)` |
|
|
830
|
+
| `AggregatedMetric` | Aggregate of a metric, with `count` for the cases it covers |
|
|
831
|
+
| `MetricComparison` | A metric of a comparison, with `n` for the cases it was compared on |
|
|
832
|
+
| `ExpectedToolCall` | A tool call a case expects: `name` and optional `arguments` |
|
|
833
|
+
| `Passage` | A retrieved passage: `id?`, `content`, `score?`, `source?` |
|
|
834
|
+
| `EvalTurnResult` | A turn of a multi-turn case: `input`, `output`, `toolCalls?`, `traceId?`, `prompt?`, `error?` |
|
|
835
|
+
| `EvalPrompt` | Versioned instructions of an agent run: `key`, `versionId?`, `version?`, `abTest?` |
|
|
836
|
+
| `EvalRun` | A saved run, as `loadRun()` returns it |
|
|
837
|
+
| `RunComparison` | What `compareRuns()` returns |
|
|
726
838
|
|
|
727
839
|
### Statistics
|
|
728
840
|
|
|
729
|
-
| Export
|
|
730
|
-
|
|
|
731
|
-
| `pairedTTest`
|
|
732
|
-
| `
|
|
733
|
-
| `
|
|
734
|
-
| `
|
|
735
|
-
| `
|
|
736
|
-
| `
|
|
737
|
-
| `
|
|
841
|
+
| Export | Description |
|
|
842
|
+
| -------------------- | --------------------------------------------------------------- |
|
|
843
|
+
| `pairedTTest` | Paired t-test for continuous metric comparison |
|
|
844
|
+
| `wilcoxonSignedRank` | Wilcoxon signed-rank test, zeros dropped, exact up to 20 pairs |
|
|
845
|
+
| `mcnemarsTest` | McNemar's test (chi-square, continuity corrected) for binary |
|
|
846
|
+
| `mcnemarExact` | Exact McNemar's test, the binomial test of the discordant pairs |
|
|
847
|
+
| `holm` | Holm-Bonferroni adjusted p-values |
|
|
848
|
+
| `bootstrapCI` | Seeded percentile bootstrap confidence interval of the mean |
|
|
849
|
+
| `cohensD` | Cohen's d of paired differences (dz) |
|
|
850
|
+
| `pearson` | Pearson's correlation |
|
|
851
|
+
| `spearman` | Spearman's rank correlation |
|
|
852
|
+
| `mean` | Arithmetic mean |
|
|
853
|
+
| `median` | Median value |
|
|
854
|
+
| `stdDev` | Sample standard deviation |
|
|
855
|
+
| `percentile` | Arbitrary percentile |
|
|
856
|
+
| `aggregate` | Full stats: mean, median, min, max, stdDev, p50, p95, p99 |
|
|
738
857
|
|
|
739
858
|
### Agent Tools
|
|
740
859
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MetricFn } from './metrics/types.js';
|
|
2
|
+
import type { JudgeConfigInput } from './schema.js';
|
|
3
|
+
/** An answer labelled by a person, with the score the metric should give it, from 0 to 1 */
|
|
4
|
+
export interface CalibrationExample {
|
|
5
|
+
input: string;
|
|
6
|
+
output: string;
|
|
7
|
+
expected?: string;
|
|
8
|
+
context?: Record<string, unknown>;
|
|
9
|
+
label: number;
|
|
10
|
+
}
|
|
11
|
+
export interface CalibrateJudgeOptions {
|
|
12
|
+
/** Where a score or label counts as a pass for `kappa`, 0.5 by default */
|
|
13
|
+
threshold?: number;
|
|
14
|
+
/** Binds an unbound judge metric, run by `judge.cogitator` */
|
|
15
|
+
judge?: JudgeConfigInput;
|
|
16
|
+
/** Examples scored at once, 5 by default */
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface CalibrationCase {
|
|
20
|
+
/** Position of the example */
|
|
21
|
+
index: number;
|
|
22
|
+
label: number;
|
|
23
|
+
/** The metric's score, absent when it failed */
|
|
24
|
+
score?: number;
|
|
25
|
+
details?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface JudgeCalibration {
|
|
29
|
+
/** Examples the metric scored, those it failed on left out of the figures */
|
|
30
|
+
n: number;
|
|
31
|
+
/** Examples the metric failed on */
|
|
32
|
+
errors: number;
|
|
33
|
+
/** Pearson's correlation of scores and labels, NaN when either does not vary */
|
|
34
|
+
pearson: number;
|
|
35
|
+
/** Spearman's rank correlation of scores and labels */
|
|
36
|
+
spearman: number;
|
|
37
|
+
/** Mean absolute error of the scores against the labels */
|
|
38
|
+
mae: number;
|
|
39
|
+
/**
|
|
40
|
+
* Cohen's kappa of pass and fail, a score or label passing at `threshold` or more. NaN when
|
|
41
|
+
* agreement by chance is certain, every score and label being on one side
|
|
42
|
+
*/
|
|
43
|
+
kappa: number;
|
|
44
|
+
perCase: CalibrationCase[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* How well a metric, usually an LLM judge, agrees with labels people gave: it scores each
|
|
48
|
+
* labelled example and compares the scores with the labels. Bind a judge metric first with
|
|
49
|
+
* `bindJudgeContext`, or pass `judge` with a `cogitator`. Examples the metric fails on are
|
|
50
|
+
* reported and left out of the figures
|
|
51
|
+
*/
|
|
52
|
+
export declare function calibrateJudge(metric: MetricFn, examples: readonly CalibrationExample[], options?: CalibrateJudgeOptions): Promise<JudgeCalibration>;
|
|
53
|
+
//# sourceMappingURL=calibrate-judge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calibrate-judge.d.ts","sourceRoot":"","sources":["../src/calibrate-judge.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAkB,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAKjD,4FAA4F;AAC5F,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,CAAC,EAAE,MAAM,CAAC;IACV,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AA6ED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,QAAQ,EAChB,QAAQ,EAAE,SAAS,kBAAkB,EAAE,EACvC,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAyC3B"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { isLLMMetric } from './eval-suite.js';
|
|
3
|
+
import { nonFiniteScoreError } from './metrics/types.js';
|
|
4
|
+
import { bindJudgeContext, judgeContextFor } from './metrics/llm-judge.js';
|
|
5
|
+
import { JudgeConfigSchema } from './schema.js';
|
|
6
|
+
import { parseOptions } from './options.js';
|
|
7
|
+
import { pearson, spearman } from './stats/correlation.js';
|
|
8
|
+
import { mean } from './stats/percentiles.js';
|
|
9
|
+
const CalibrationExampleSchema = z.object({
|
|
10
|
+
input: z.string(),
|
|
11
|
+
output: z.string(),
|
|
12
|
+
expected: z.string().optional(),
|
|
13
|
+
context: z.record(z.string(), z.unknown()).optional(),
|
|
14
|
+
label: z.number().min(0).max(1),
|
|
15
|
+
});
|
|
16
|
+
const CalibrateJudgeOptionsSchema = z.object({
|
|
17
|
+
threshold: z.number().min(0).max(1).default(0.5),
|
|
18
|
+
concurrency: z.number().int().positive().default(5),
|
|
19
|
+
});
|
|
20
|
+
/** Cohen's kappa of two binary raters */
|
|
21
|
+
function binaryKappa(a, b) {
|
|
22
|
+
const n = a.length;
|
|
23
|
+
if (n === 0)
|
|
24
|
+
return Number.NaN;
|
|
25
|
+
let agree = 0;
|
|
26
|
+
let aPass = 0;
|
|
27
|
+
let bPass = 0;
|
|
28
|
+
for (let i = 0; i < n; i++) {
|
|
29
|
+
if (a[i] === b[i])
|
|
30
|
+
agree++;
|
|
31
|
+
if (a[i])
|
|
32
|
+
aPass++;
|
|
33
|
+
if (b[i])
|
|
34
|
+
bPass++;
|
|
35
|
+
}
|
|
36
|
+
const observed = agree / n;
|
|
37
|
+
const chance = (aPass / n) * (bPass / n) + (1 - aPass / n) * (1 - bPass / n);
|
|
38
|
+
return chance === 1 ? Number.NaN : (observed - chance) / (1 - chance);
|
|
39
|
+
}
|
|
40
|
+
function bindMetric(metric, judge) {
|
|
41
|
+
if (!isLLMMetric(metric))
|
|
42
|
+
return metric;
|
|
43
|
+
if (!judge) {
|
|
44
|
+
throw new Error('calibrateJudge needs options.judge to run an unbound judge metric');
|
|
45
|
+
}
|
|
46
|
+
const config = JudgeConfigSchema.parse(judge);
|
|
47
|
+
if (!config.cogitator) {
|
|
48
|
+
throw new Error('calibrateJudge needs judge.cogitator to run the judge');
|
|
49
|
+
}
|
|
50
|
+
return bindJudgeContext(metric, judgeContextFor(config.cogitator, config));
|
|
51
|
+
}
|
|
52
|
+
async function scoreExample(metric, example, index) {
|
|
53
|
+
const result = {
|
|
54
|
+
case: {
|
|
55
|
+
input: example.input,
|
|
56
|
+
...(example.expected !== undefined && { expected: example.expected }),
|
|
57
|
+
...(example.context !== undefined && { context: example.context }),
|
|
58
|
+
},
|
|
59
|
+
output: example.output,
|
|
60
|
+
duration: 0,
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
const scored = await metric(result);
|
|
64
|
+
const invalid = scored.error ?? nonFiniteScoreError(scored.score);
|
|
65
|
+
if (invalid !== undefined)
|
|
66
|
+
return { index, label: example.label, error: invalid };
|
|
67
|
+
return {
|
|
68
|
+
index,
|
|
69
|
+
label: example.label,
|
|
70
|
+
score: scored.score,
|
|
71
|
+
...(scored.details !== undefined && { details: scored.details }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
index,
|
|
77
|
+
label: example.label,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* How well a metric, usually an LLM judge, agrees with labels people gave: it scores each
|
|
84
|
+
* labelled example and compares the scores with the labels. Bind a judge metric first with
|
|
85
|
+
* `bindJudgeContext`, or pass `judge` with a `cogitator`. Examples the metric fails on are
|
|
86
|
+
* reported and left out of the figures
|
|
87
|
+
*/
|
|
88
|
+
export async function calibrateJudge(metric, examples, options = {}) {
|
|
89
|
+
const { threshold, concurrency } = parseOptions(CalibrateJudgeOptionsSchema, { threshold: options.threshold, concurrency: options.concurrency }, 'calibrateJudge');
|
|
90
|
+
const parsed = parseOptions(z.array(CalibrationExampleSchema), examples, 'calibrateJudge examples');
|
|
91
|
+
const bound = bindMetric(metric, options.judge);
|
|
92
|
+
const perCase = new Array(parsed.length);
|
|
93
|
+
let next = 0;
|
|
94
|
+
const worker = async () => {
|
|
95
|
+
while (next < parsed.length) {
|
|
96
|
+
const at = next++;
|
|
97
|
+
perCase[at] = await scoreExample(bound, parsed[at], at);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, parsed.length) }, worker));
|
|
101
|
+
const scored = perCase.filter((one) => one.score !== undefined);
|
|
102
|
+
const scores = scored.map((one) => one.score);
|
|
103
|
+
const labels = scored.map((one) => one.label);
|
|
104
|
+
return {
|
|
105
|
+
n: scored.length,
|
|
106
|
+
errors: perCase.length - scored.length,
|
|
107
|
+
pearson: pearson(scores, labels),
|
|
108
|
+
spearman: spearman(scores, labels),
|
|
109
|
+
mae: scored.length === 0 ? Number.NaN : mean(scored.map((one) => Math.abs(one.score - one.label))),
|
|
110
|
+
kappa: binaryKappa(scores.map((score) => score >= threshold), labels.map((label) => label >= threshold)),
|
|
111
|
+
perCase,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=calibrate-judge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calibrate-judge.js","sourceRoot":"","sources":["../src/calibrate-judge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { EvalComparisonConfig } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* The test a comparison ran: McNemar's (exact below 25 discordant pairs, chi-square with
|
|
4
|
+
* continuity correction from 25), the paired t-test, Wilcoxon signed-rank, or none with fewer
|
|
5
|
+
* than 2 cases
|
|
6
|
+
*/
|
|
7
|
+
export type ComparisonTest = 'mcnemar' | 'mcnemar-exact' | 't' | 'wilcoxon' | 'none';
|
|
8
|
+
export type ComparisonWinner = 'baseline' | 'challenger' | 'tie';
|
|
9
|
+
export interface MetricComparison {
|
|
10
|
+
/** Mean score over the compared cases, NaN when `n` is 0 */
|
|
11
|
+
baseline: number;
|
|
12
|
+
challenger: number;
|
|
13
|
+
/** Cases compared: those at least one side scored, an errored side counting as a 0 */
|
|
14
|
+
n: number;
|
|
15
|
+
/** Mean of the paired differences, challenger minus baseline, NaN when `n` is 0 */
|
|
16
|
+
difference: number;
|
|
17
|
+
/** Percentile bootstrap confidence interval of `difference` at `1 - alpha` */
|
|
18
|
+
ci: {
|
|
19
|
+
low: number;
|
|
20
|
+
high: number;
|
|
21
|
+
};
|
|
22
|
+
/** Cohen's d of the paired differences (dz), NaN with fewer than 2 cases */
|
|
23
|
+
effectSize: number;
|
|
24
|
+
test: ComparisonTest;
|
|
25
|
+
/** The test's own p-value */
|
|
26
|
+
pValue: number;
|
|
27
|
+
/** `pValue` after Holm's correction across the compared metrics */
|
|
28
|
+
adjustedPValue: number;
|
|
29
|
+
/** `adjustedPValue` is below `alpha` */
|
|
30
|
+
significant: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The higher side when significant, by the test's direction: the sign of `difference`, except
|
|
33
|
+
* for Wilcoxon's test, where it is the side with the larger rank sum and may differ from it
|
|
34
|
+
*/
|
|
35
|
+
winner: ComparisonWinner;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Latency or cost of the two sides, paired over the cases both answered, where lower is better.
|
|
39
|
+
* Not corrected for multiple comparisons and not part of the metrics' vote on the winner
|
|
40
|
+
*/
|
|
41
|
+
export interface ResourceComparison {
|
|
42
|
+
/** Mean per case, NaN when no case pairs up */
|
|
43
|
+
baseline: number;
|
|
44
|
+
challenger: number;
|
|
45
|
+
n: number;
|
|
46
|
+
/** Challenger minus baseline: below 0 when the challenger is cheaper or faster */
|
|
47
|
+
difference: number;
|
|
48
|
+
test: ComparisonTest;
|
|
49
|
+
pValue: number;
|
|
50
|
+
significant: boolean;
|
|
51
|
+
/** The lower side when significant, by the test's direction as for `MetricComparison.winner` */
|
|
52
|
+
winner: ComparisonWinner;
|
|
53
|
+
}
|
|
54
|
+
/** The statistical settings of a comparison */
|
|
55
|
+
export type ComparisonStatsConfig = Pick<EvalComparisonConfig, 'alpha' | 'test' | 'seed'>;
|
|
56
|
+
/** A case result as the comparison reads it: its error, its scores and its trials */
|
|
57
|
+
export interface ScoredTrial {
|
|
58
|
+
error?: string;
|
|
59
|
+
scores: ReadonlyArray<{
|
|
60
|
+
name: string;
|
|
61
|
+
score: number;
|
|
62
|
+
}>;
|
|
63
|
+
trials?: readonly ScoredTrial[];
|
|
64
|
+
}
|
|
65
|
+
/** The two sides of one case, either missing when that side has no such case */
|
|
66
|
+
export type CasePair<T extends ScoredTrial = ScoredTrial> = readonly [T | undefined, T | undefined];
|
|
67
|
+
/** How the score of a case moved from one side to the other */
|
|
68
|
+
export interface CaseChange {
|
|
69
|
+
/** Zero based position of the case in the pairs compared */
|
|
70
|
+
index: number;
|
|
71
|
+
/** The case input on one line, shortened */
|
|
72
|
+
input: string;
|
|
73
|
+
/** Score of the first side (the baseline, or the base run) */
|
|
74
|
+
before: number;
|
|
75
|
+
/** Score of the second side (the challenger, or the head run) */
|
|
76
|
+
after: number;
|
|
77
|
+
/** `after` minus `before` */
|
|
78
|
+
delta: number;
|
|
79
|
+
}
|
|
80
|
+
/** A score moves when it changes by more than this */
|
|
81
|
+
export declare const SCORE_EPSILON = 1e-9;
|
|
82
|
+
/**
|
|
83
|
+
* Compares each metric on its paired scores, then adjusts the p-values with Holm's correction
|
|
84
|
+
* across the metrics and decides each metric's winner
|
|
85
|
+
*/
|
|
86
|
+
export declare function compareMetrics(paired: ReadonlyMap<string, readonly [number[], number[]]>, config: ComparisonStatsConfig): Record<string, MetricComparison>;
|
|
87
|
+
export declare function compareResource(baseline: number[], challenger: number[], config: ComparisonStatsConfig): ResourceComparison;
|
|
88
|
+
/** The side more metrics name as their winner, a tie when as many name each */
|
|
89
|
+
export declare function determineOverallWinner(metrics: Record<string, Pick<MetricComparison, 'winner'>>): ComparisonWinner;
|
|
90
|
+
/** The names of the metrics any trial of the cases was scored on, in the order first seen */
|
|
91
|
+
export declare function scoredMetricNames(results: Iterable<ScoredTrial>): Set<string>;
|
|
92
|
+
/** A case's score of a metric: the mean over its trials the metric scored, none when it scored none */
|
|
93
|
+
export declare function caseScore(result: ScoredTrial | undefined, metricName: string): number | undefined;
|
|
94
|
+
/** The scores of a metric for each pair, side by side, as `pairedCaseScores` pairs them */
|
|
95
|
+
export declare function pairedScores(pairs: readonly CasePair[], metricName: string): [number[], number[]];
|
|
96
|
+
/**
|
|
97
|
+
* The cases whose score of a metric rose or fell from the first side to the second by more than
|
|
98
|
+
* `SCORE_EPSILON`, the largest moves first
|
|
99
|
+
*/
|
|
100
|
+
export declare function caseChanges(pairs: readonly CasePair[], metricName: string, inputOf: (index: number) => string): {
|
|
101
|
+
improved: CaseChange[];
|
|
102
|
+
regressed: CaseChange[];
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=comparison-stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparison-stats.d.ts","sourceRoot":"","sources":["../src/comparison-stats.ts"],"names":[],"mappings":"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"}
|