stageflow 0.44.0 → 0.46.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/dist/agents/evalGate.js +1 -0
- package/dist/cli/evalCommand.d.ts +1 -1
- package/dist/cli/evalCommand.js +39 -2
- package/dist/cli/hostClient.d.ts +6 -0
- package/dist/cli/hostClient.js +4 -0
- package/dist/config/loadEval.d.ts +3 -1
- package/dist/config/loadEval.js +75 -7
- package/dist/config/validateEval.js +17 -1
- package/dist/evals/calibration.d.ts +21 -0
- package/dist/evals/calibration.js +69 -0
- package/dist/evals/calibrationSuggest.d.ts +29 -0
- package/dist/evals/calibrationSuggest.js +72 -0
- package/dist/evals/calibrator.d.ts +46 -0
- package/dist/evals/calibrator.js +148 -0
- package/dist/evals/compare.d.ts +1 -1
- package/dist/evals/compare.js +3 -0
- package/dist/evals/constants.d.ts +4 -2
- package/dist/evals/constants.js +4 -0
- package/dist/evals/engine/FakeEngine.d.ts +1 -0
- package/dist/evals/engine/FakeEngine.js +7 -2
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +2 -1
- package/dist/evals/engine/promptfoo/generate.d.ts +2 -0
- package/dist/evals/engine/promptfoo/generate.js +33 -16
- package/dist/evals/engine/promptfoo/judgeProviders.js +2 -1
- package/dist/evals/engine/promptfoo/normalize.d.ts +4 -1
- package/dist/evals/engine/promptfoo/normalize.js +16 -3
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +2 -0
- package/dist/evals/engine/promptfoo/rubricPrompt.js +10 -2
- package/dist/evals/errors.d.ts +7 -0
- package/dist/evals/errors.js +10 -0
- package/dist/evals/judgeProviders.d.ts +6 -0
- package/dist/evals/judgeProviders.js +14 -0
- package/dist/evals/judged.d.ts +7 -0
- package/dist/evals/judged.js +7 -0
- package/dist/evals/manager.d.ts +14 -8
- package/dist/evals/manager.js +63 -10
- package/dist/evals/plan.d.ts +2 -0
- package/dist/evals/plan.js +5 -4
- package/dist/evals/planInternalAgent.d.ts +1 -1
- package/dist/evals/planInternalAgent.js +2 -6
- package/dist/evals/read/api.d.ts +6 -0
- package/dist/evals/read/api.js +5 -1
- package/dist/evals/read/labelQueue.d.ts +45 -0
- package/dist/evals/read/labelQueue.js +83 -0
- package/dist/evals/stats.d.ts +40 -0
- package/dist/evals/stats.js +69 -0
- package/dist/evals/storeTypes.d.ts +42 -0
- package/dist/evals/types.d.ts +13 -0
- package/dist/evals/votes.d.ts +10 -0
- package/dist/evals/votes.js +60 -0
- package/dist/mcp/evalTools.js +39 -0
- package/dist/operatorAgent/profiles/guideConfirm.js +1 -1
- package/dist/operatorAgent/profiles/stageflowGuide.d.ts +1 -1
- package/dist/operatorAgent/profiles/stageflowGuide.js +8 -7
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/port.d.ts +13 -1
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +10 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +25 -0
- package/dist/runstore/sqlite/calibrationStore.d.ts +10 -0
- package/dist/runstore/sqlite/calibrationStore.js +106 -0
- package/dist/runstore/sqlite/evalStore.d.ts +5 -0
- package/dist/runstore/sqlite/evalStore.js +7 -0
- package/dist/runstore/sqlite/migrations/018-eval-grader-labels.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/018-eval-grader-labels.js +43 -0
- package/dist/runstore/sqlite/migrations/index.js +2 -0
- package/dist/server/evalRoutes.js +213 -3
- package/dist/toolkit/meta.js +2 -0
- package/dist/ui/assets/{MarkdownText-S5C_vemc.js → MarkdownText-BS2BBbCx.js} +1 -1
- package/dist/ui/assets/adapters-DPD-cAsy.js +17 -0
- package/dist/ui/assets/chat-BD1-VTT6.js +2 -0
- package/dist/ui/assets/chat-DXnQv9st.css +2 -0
- package/dist/ui/assets/main-AX5XNLtX.js +278 -0
- package/dist/ui/assets/{main-Del1GeLf.css → main-CFX0_n8J.css} +1 -1
- package/dist/ui/chat.html +3 -3
- package/dist/ui/index.html +3 -3
- package/package.json +1 -1
- package/skills/stageflow/references/control-surface.md +1 -1
- package/skills/stageflow/references/evals.md +1 -0
- package/dist/ui/assets/adapters-C0l4ruiR.js +0 -17
- package/dist/ui/assets/chat-CjnsmvqR.js +0 -2
- package/dist/ui/assets/chat-DFDd4UjS.css +0 -2
- package/dist/ui/assets/main-_yXVqcW9.js +0 -277
package/dist/evals/manager.js
CHANGED
|
@@ -10,19 +10,14 @@ import { scoreResult } from "./scoring.js";
|
|
|
10
10
|
import { computeStats } from "./stats.js";
|
|
11
11
|
import { toGradeRecords, toResultRecord } from "./statsAdapter.js";
|
|
12
12
|
import { isConversationTarget } from "./types.js";
|
|
13
|
+
import { isJudged } from "./judged.js";
|
|
14
|
+
import { measureCalibration } from "./calibrator.js";
|
|
15
|
+
import { EvalRunError } from "./errors.js";
|
|
16
|
+
export { CALIBRATION_DEFAULT_MAX_COST_USD } from "./calibrator.js";
|
|
17
|
+
export { EvalRunError };
|
|
13
18
|
export const EVAL_HOST_STOPPED_REASON = "Host stopped while the run was in progress";
|
|
14
19
|
export const EVAL_DEFAULT_ENGINE = "promptfoo";
|
|
15
20
|
export const EVAL_ORPHAN_SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
16
|
-
export class EvalRunError extends Error {
|
|
17
|
-
code;
|
|
18
|
-
findings;
|
|
19
|
-
constructor(code, message, findings) {
|
|
20
|
-
super(message);
|
|
21
|
-
this.code = code;
|
|
22
|
-
this.findings = findings;
|
|
23
|
-
this.name = "EvalRunError";
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
21
|
const key = (caseId, repeatIndex) => `${caseId}:${repeatIndex}`;
|
|
27
22
|
function isRecord(value) {
|
|
28
23
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -96,6 +91,7 @@ export class EvalRunManager {
|
|
|
96
91
|
reserved = false;
|
|
97
92
|
trying = false;
|
|
98
93
|
tryingRunId = null;
|
|
94
|
+
calibrationCancel = null;
|
|
99
95
|
constructor(deps) {
|
|
100
96
|
this.deps = deps;
|
|
101
97
|
this.store = deps.store;
|
|
@@ -542,6 +538,10 @@ export class EvalRunManager {
|
|
|
542
538
|
}
|
|
543
539
|
}
|
|
544
540
|
async cancel(evalRunId) {
|
|
541
|
+
if (this.calibrationCancel !== null && this.tryingRunId === evalRunId) {
|
|
542
|
+
this.calibrationCancel.abort();
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
545
|
const live = this.live;
|
|
546
546
|
if (live === undefined || live.evalRunId !== evalRunId) {
|
|
547
547
|
if ((await this.store.getEvalRun(evalRunId)) === null) {
|
|
@@ -676,6 +676,59 @@ export class EvalRunManager {
|
|
|
676
676
|
this.deps.logger?.warn("eval.try_session_cleanup_failed", safeMessage(err));
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
|
+
logEngine(level, message) {
|
|
680
|
+
if (level === "info")
|
|
681
|
+
this.deps.logger?.info?.("eval.engine", message);
|
|
682
|
+
else
|
|
683
|
+
this.deps.logger?.[level]("eval.engine", message);
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Re-judges the stored outputs of results the author labeled for one grader and compares the verdicts to the
|
|
687
|
+
* labels. No stage or agent runs: the engine's case callback replays the stored output. Nothing is added to the
|
|
688
|
+
* run list; the measurement is stored as a calibration.
|
|
689
|
+
*/
|
|
690
|
+
async calibrate(input) {
|
|
691
|
+
this.claim();
|
|
692
|
+
this.reserved = false;
|
|
693
|
+
this.trying = true;
|
|
694
|
+
const evalRunId = `${EVAL_RUN_ID_PREFIX}${randomBytes(6).toString("hex")}`;
|
|
695
|
+
this.tryingRunId = evalRunId;
|
|
696
|
+
const abort = new AbortController();
|
|
697
|
+
const cancel = new AbortController();
|
|
698
|
+
this.calibrationCancel = cancel;
|
|
699
|
+
const relay = () => cancel.abort();
|
|
700
|
+
if (input.signal?.aborted)
|
|
701
|
+
relay();
|
|
702
|
+
input.signal?.addEventListener("abort", relay, { once: true });
|
|
703
|
+
try {
|
|
704
|
+
const engine = this.engines(this.engineName);
|
|
705
|
+
const engineInfo = { name: engine.name, version: await engine.version(), nodeVersion: process.versions.node };
|
|
706
|
+
const built = await buildEvalPlan({
|
|
707
|
+
projectRoot: input.projectRoot,
|
|
708
|
+
evalPath: input.evalPath,
|
|
709
|
+
overrides: {},
|
|
710
|
+
evalRunId,
|
|
711
|
+
engine: engineInfo,
|
|
712
|
+
...(this.deps.isKnownModel ? { isKnownModel: this.deps.isKnownModel } : {}),
|
|
713
|
+
...this.agentsInput(),
|
|
714
|
+
});
|
|
715
|
+
if (!built.ok)
|
|
716
|
+
throw new EvalRunError("EVAL_INVALID", "Eval is invalid", built.findings);
|
|
717
|
+
const base = built.plan;
|
|
718
|
+
const found = base.graders.find((g) => g.id === input.graderId);
|
|
719
|
+
if (!found || !isJudged(found)) {
|
|
720
|
+
throw new EvalRunError("EVAL_INVALID", `Grader ${input.graderId} is not a judged grader of this eval`);
|
|
721
|
+
}
|
|
722
|
+
return await measureCalibration({ store: this.store, engine, log: (level, message) => this.logEngine(level, message) }, input, base, found, abort, cancel.signal);
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
input.signal?.removeEventListener("abort", relay);
|
|
726
|
+
this.calibrationCancel = null;
|
|
727
|
+
this.trying = false;
|
|
728
|
+
this.tryingRunId = null;
|
|
729
|
+
await this.cleanupWorkDir(evalRunId);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
679
732
|
async tryOne(input) {
|
|
680
733
|
this.claim();
|
|
681
734
|
this.reserved = false;
|
package/dist/evals/plan.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LoadedEval } from "../config/loadEval.js";
|
|
1
2
|
import { type AgentCatalogPort } from "../config/resolveAgentTarget.js";
|
|
2
3
|
import type { ValidationFinding } from "../config/validateCatalog.js";
|
|
3
4
|
import { type ConfigComponents, type ConfigSnapshot } from "./snapshot.js";
|
|
@@ -62,4 +63,5 @@ export type BuildEvalPlanResult = {
|
|
|
62
63
|
code: "EVAL_INVALID";
|
|
63
64
|
findings: ValidationFinding[];
|
|
64
65
|
};
|
|
66
|
+
export declare function evalDefinition(loaded: LoadedEval, projectRoot: string): Record<string, unknown>;
|
|
65
67
|
export declare function buildEvalPlan(input: BuildEvalPlanInput): Promise<BuildEvalPlanResult>;
|
package/dist/evals/plan.js
CHANGED
|
@@ -13,6 +13,7 @@ import { EVAL_LIMITS, SIMULATOR_PROMPT_VERSION } from "./constants.js";
|
|
|
13
13
|
import { buildComponents, buildSnapshot, computeFingerprint, } from "./snapshot.js";
|
|
14
14
|
import { planInternalAgent } from "./planInternalAgent.js";
|
|
15
15
|
import { isConversationTarget } from "./types.js";
|
|
16
|
+
import { isJudged, isJudgedType } from "./judged.js";
|
|
16
17
|
const SELECTION_KEYS = ["case_ids", "tags", "only_failed_from"];
|
|
17
18
|
const RUN_SIZE_CODES = new Set(["eval.large_run", "eval.too_large_run"]);
|
|
18
19
|
function posix(projectRoot, abs) {
|
|
@@ -106,10 +107,10 @@ function withOverrides(loaded, overrides) {
|
|
|
106
107
|
...(overrides.max_cost_usd !== undefined ? { max_cost_usd: overrides.max_cost_usd } : {}),
|
|
107
108
|
};
|
|
108
109
|
const graders = loaded.graders.map((grader) => {
|
|
109
|
-
if ((grader
|
|
110
|
+
if (!isJudged(grader) || overrides.judge === undefined)
|
|
110
111
|
return grader;
|
|
111
112
|
const raw = loaded.file.graders.find((g) => g.id === grader.id);
|
|
112
|
-
if (
|
|
113
|
+
if (raw !== undefined && isJudgedType(raw.type) && "judge" in raw && raw.judge !== undefined)
|
|
113
114
|
return grader;
|
|
114
115
|
const { hash: _old, ...rest } = { ...grader, judge: overrides.judge };
|
|
115
116
|
return { ...rest, hash: graderHash(rest) };
|
|
@@ -133,7 +134,7 @@ function selectCases(cases, overrides, failedIds) {
|
|
|
133
134
|
return cases.filter((c) => failedIds.has(c.id));
|
|
134
135
|
return [...cases];
|
|
135
136
|
}
|
|
136
|
-
function evalDefinition(loaded, projectRoot) {
|
|
137
|
+
export function evalDefinition(loaded, projectRoot) {
|
|
137
138
|
const { file } = loaded;
|
|
138
139
|
const paths = resolveEvalPaths(file, loaded.path);
|
|
139
140
|
const rel = (abs) => abs === undefined ? undefined : posix(projectRoot, abs);
|
|
@@ -508,7 +509,7 @@ async function buildAgentResult(a) {
|
|
|
508
509
|
});
|
|
509
510
|
const fingerprint = computeFingerprint(components);
|
|
510
511
|
const { overrides } = a;
|
|
511
|
-
const hasRubric = loaded.graders.some(
|
|
512
|
+
const hasRubric = loaded.graders.some(isJudged);
|
|
512
513
|
const plainDefinition = JSON.parse(JSON.stringify(definition));
|
|
513
514
|
const snapshot = buildSnapshot({
|
|
514
515
|
evalId: loaded.id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DatasetCase } from "../config/datasetTypes.js";
|
|
2
2
|
import { type LoadedEval } from "../config/loadEval.js";
|
|
3
3
|
import type { ValidationFinding } from "../config/validateCatalog.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type BuildEvalPlanInput, type BuildEvalPlanResult, type EvalOverrides } from "./plan.js";
|
|
5
5
|
type Dataset = {
|
|
6
6
|
path: string;
|
|
7
7
|
contentHash: string;
|
|
@@ -5,6 +5,7 @@ import { EVAL_LARGE_RUN, EVAL_TOO_LARGE_RUN, evalFinding } from "../config/valid
|
|
|
5
5
|
import { PACKAGE_VERSION } from "../package-meta.js";
|
|
6
6
|
import { sha256Hex } from "./canonical.js";
|
|
7
7
|
import { EVAL_LIMITS, SIMULATOR_PROMPT_VERSION } from "./constants.js";
|
|
8
|
+
import { evalDefinition } from "./plan.js";
|
|
8
9
|
import { buildComponents, buildSnapshot, computeFingerprint } from "./snapshot.js";
|
|
9
10
|
function posix(projectRoot, abs) {
|
|
10
11
|
return path.relative(projectRoot, abs).split(path.sep).join("/");
|
|
@@ -127,12 +128,7 @@ export function planInternalAgent(args) {
|
|
|
127
128
|
evalId: loaded.id,
|
|
128
129
|
evalPath: loaded.relPath,
|
|
129
130
|
evalHash: loaded.fileHash,
|
|
130
|
-
evalDefinition:
|
|
131
|
-
version: 1,
|
|
132
|
-
id: loaded.id,
|
|
133
|
-
target: { kind: "internal_agent", profile: profileId },
|
|
134
|
-
dataset: posix(projectRoot, paths.datasetPath),
|
|
135
|
-
},
|
|
131
|
+
evalDefinition: evalDefinition(loaded, projectRoot),
|
|
136
132
|
overrides: snapshotOverrides,
|
|
137
133
|
target: {
|
|
138
134
|
profile_id: profileId,
|
package/dist/evals/read/api.d.ts
CHANGED
|
@@ -159,6 +159,12 @@ export declare function buildResultDetail(store: RunStore, id: string, caseId: s
|
|
|
159
159
|
} | null;
|
|
160
160
|
tags: string[];
|
|
161
161
|
suggestion: import("./suggestions.js").WireSuggestion | null;
|
|
162
|
+
grader_labels: {
|
|
163
|
+
[k: string]: {
|
|
164
|
+
label: "fail" | "pass";
|
|
165
|
+
note: string | null;
|
|
166
|
+
};
|
|
167
|
+
};
|
|
162
168
|
}[];
|
|
163
169
|
}>;
|
|
164
170
|
export declare function loadEvalListItems(input: {
|
package/dist/evals/read/api.js
CHANGED
|
@@ -227,11 +227,12 @@ export async function buildResultsPage(store, id, input) {
|
|
|
227
227
|
}
|
|
228
228
|
export async function buildResultDetail(store, id, caseId) {
|
|
229
229
|
const row = await runByIdOr404(store, id);
|
|
230
|
-
const [cases, results, reviews, suggestions] = await Promise.all([
|
|
230
|
+
const [cases, results, reviews, suggestions, graderLabels] = await Promise.all([
|
|
231
231
|
store.getEvalCases(row.evalRunId),
|
|
232
232
|
store.getEvalResultsForCase(row.evalRunId, caseId),
|
|
233
233
|
store.getEvalReviews(row.evalRunId),
|
|
234
234
|
store.listEvalSuggestions(row.evalRunId),
|
|
235
|
+
store.listEvalGraderLabels(row.evalRunId),
|
|
235
236
|
]);
|
|
236
237
|
const caseRow = cases.find((x) => x.caseId === caseId);
|
|
237
238
|
if (!caseRow || results.length === 0) {
|
|
@@ -245,6 +246,9 @@ export async function buildResultDetail(store, id, caseId) {
|
|
|
245
246
|
.map((r) => ({
|
|
246
247
|
...resultToWire(r, reviewByKey.get(resultKey(r.caseId, r.repeatIndex)), row.targetKind === "agent" ? (row.targetRef.agent_id ?? "") : undefined),
|
|
247
248
|
suggestion: suggestionFor(suggestions, r.caseId, r.repeatIndex),
|
|
249
|
+
grader_labels: Object.fromEntries(graderLabels
|
|
250
|
+
.filter((l) => l.caseId === r.caseId && l.repeatIndex === r.repeatIndex)
|
|
251
|
+
.map((l) => [l.graderId, { label: l.label, note: l.note }])),
|
|
248
252
|
})),
|
|
249
253
|
};
|
|
250
254
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RunStore } from "../../runstore/port.js";
|
|
2
|
+
export type LabelCounts = {
|
|
3
|
+
pass: number;
|
|
4
|
+
fail: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Everything the console's labeling screen needs in one read: each result the judge graded for one grader, with the
|
|
8
|
+
* conversation, what the case expects, the judge's answer and the author's label so far. Results the judge errored on
|
|
9
|
+
* are left out (there is no verdict to compare with), and so are results that never ran.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildLabelQueue(store: RunStore, id: string, graderId: string): Promise<{
|
|
12
|
+
grader_id: string;
|
|
13
|
+
items: {
|
|
14
|
+
case: {
|
|
15
|
+
position: number;
|
|
16
|
+
};
|
|
17
|
+
result: import("./runs.js").WireResultDetail;
|
|
18
|
+
judge: {
|
|
19
|
+
passed: boolean;
|
|
20
|
+
score: number | null;
|
|
21
|
+
reason: string | null;
|
|
22
|
+
};
|
|
23
|
+
label: {
|
|
24
|
+
label: "fail" | "pass";
|
|
25
|
+
note: string | null;
|
|
26
|
+
} | null;
|
|
27
|
+
}[];
|
|
28
|
+
labeled_here: LabelCounts;
|
|
29
|
+
labeled_eval: LabelCounts;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* The label counts across the eval's runs, and which results of this run the judge graded that still have no label
|
|
33
|
+
* from you. No conversations: this is for a progress line and the Cases filter.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildLabelSummary(store: RunStore, id: string, graderId: string): Promise<{
|
|
36
|
+
grader_id: string;
|
|
37
|
+
labeled_eval: {
|
|
38
|
+
pass: number;
|
|
39
|
+
fail: number;
|
|
40
|
+
};
|
|
41
|
+
unlabeled: {
|
|
42
|
+
case_id: string;
|
|
43
|
+
repeat_index: number;
|
|
44
|
+
}[];
|
|
45
|
+
}>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resultKey } from "../statsTypes.js";
|
|
2
|
+
import { EvalApiError, runByIdOr404 } from "./api.js";
|
|
3
|
+
import { resultToWire } from "./runs.js";
|
|
4
|
+
import { isJudgedType } from "../judged.js";
|
|
5
|
+
/**
|
|
6
|
+
* Everything the console's labeling screen needs in one read: each result the judge graded for one grader, with the
|
|
7
|
+
* conversation, what the case expects, the judge's answer and the author's label so far. Results the judge errored on
|
|
8
|
+
* are left out (there is no verdict to compare with), and so are results that never ran.
|
|
9
|
+
*/
|
|
10
|
+
export async function buildLabelQueue(store, id, graderId) {
|
|
11
|
+
const row = await runByIdOr404(store, id);
|
|
12
|
+
const [cases, results, labels, evalLabels] = await Promise.all([
|
|
13
|
+
store.getEvalCases(row.evalRunId),
|
|
14
|
+
store.listEvalResults(row.evalRunId),
|
|
15
|
+
store.listEvalGraderLabels(row.evalRunId),
|
|
16
|
+
store.listEvalGraderLabelsForEval(row.projectRoot, row.evalId, graderId),
|
|
17
|
+
]);
|
|
18
|
+
const caseById = new Map(cases.map((c) => [c.caseId, c]));
|
|
19
|
+
const labelByKey = new Map(labels.filter((l) => l.graderId === graderId).map((l) => [resultKey(l.caseId, l.repeatIndex), l]));
|
|
20
|
+
const gradeOf = (grades) => grades.find((g) => g.graderId === graderId);
|
|
21
|
+
const known = results.some((r) => gradeOf(r.grades) !== undefined);
|
|
22
|
+
if (!known) {
|
|
23
|
+
throw new EvalApiError(404, "EVAL_GRADER_NOT_FOUND", `Grader ${graderId} has no grades in run ${id}`);
|
|
24
|
+
}
|
|
25
|
+
const judged = results.find((r) => {
|
|
26
|
+
const g = gradeOf(r.grades);
|
|
27
|
+
return g !== undefined && isJudgedType(g.graderType);
|
|
28
|
+
});
|
|
29
|
+
if (!judged) {
|
|
30
|
+
throw new EvalApiError(400, "invalid_request", `Grader ${graderId} is not judged by a model, so it has nothing to label`);
|
|
31
|
+
}
|
|
32
|
+
const items = [...results]
|
|
33
|
+
.sort((a, b) => (caseById.get(a.caseId)?.position ?? 0) - (caseById.get(b.caseId)?.position ?? 0) || a.repeatIndex - b.repeatIndex)
|
|
34
|
+
.flatMap((r) => {
|
|
35
|
+
const grade = gradeOf(r.grades);
|
|
36
|
+
const caseRow = caseById.get(r.caseId);
|
|
37
|
+
if (!grade || grade.status !== "graded" || !caseRow)
|
|
38
|
+
return [];
|
|
39
|
+
const label = labelByKey.get(resultKey(r.caseId, r.repeatIndex));
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
case: { ...caseRow.case, position: caseRow.position },
|
|
43
|
+
result: resultToWire(r, undefined),
|
|
44
|
+
judge: { passed: grade.passed === true, score: grade.score, reason: grade.reason },
|
|
45
|
+
label: label ? { label: label.label, note: label.note } : null,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
});
|
|
49
|
+
const count = (rows) => ({
|
|
50
|
+
pass: rows.filter((l) => l.label === "pass").length,
|
|
51
|
+
fail: rows.filter((l) => l.label === "fail").length,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
grader_id: graderId,
|
|
55
|
+
items,
|
|
56
|
+
labeled_here: count(labels.filter((l) => l.graderId === graderId)),
|
|
57
|
+
labeled_eval: count(evalLabels),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The label counts across the eval's runs, and which results of this run the judge graded that still have no label
|
|
62
|
+
* from you. No conversations: this is for a progress line and the Cases filter.
|
|
63
|
+
*/
|
|
64
|
+
export async function buildLabelSummary(store, id, graderId) {
|
|
65
|
+
const row = await runByIdOr404(store, id);
|
|
66
|
+
const [labels, runLabels, graded] = await Promise.all([
|
|
67
|
+
store.listEvalGraderLabelsForEval(row.projectRoot, row.evalId, graderId),
|
|
68
|
+
store.listEvalGraderLabels(row.evalRunId),
|
|
69
|
+
store.listGradedResultKeys(row.evalRunId, graderId),
|
|
70
|
+
]);
|
|
71
|
+
const labeled = new Set(runLabels.filter((l) => l.graderId === graderId).map((l) => resultKey(l.caseId, l.repeatIndex)));
|
|
72
|
+
const unlabeled = graded
|
|
73
|
+
.filter((r) => !labeled.has(resultKey(r.caseId, r.repeatIndex)))
|
|
74
|
+
.map((r) => ({ case_id: r.caseId, repeat_index: r.repeatIndex }));
|
|
75
|
+
return {
|
|
76
|
+
grader_id: graderId,
|
|
77
|
+
labeled_eval: {
|
|
78
|
+
pass: labels.filter((l) => l.label === "pass").length,
|
|
79
|
+
fail: labels.filter((l) => l.label === "fail").length,
|
|
80
|
+
},
|
|
81
|
+
unlabeled,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/evals/stats.d.ts
CHANGED
|
@@ -93,3 +93,43 @@ export declare function clusteredCi(center: number, perCase: number[], lo: numbe
|
|
|
93
93
|
export declare function buildCaseStats(results: ResultRecord[]): CaseStat[];
|
|
94
94
|
export declare function computeByGrader(graders: GraderRef[], grades: GradeRecord[], previousRun: PreviousRunLite | null | undefined): GraderStat[];
|
|
95
95
|
export declare function computeStats(input: ComputeStatsInput): Stats;
|
|
96
|
+
export declare const CALIBRATION_MIN_LABELS = 20;
|
|
97
|
+
export declare const CALIBRATION_MINORITY_SHARE = 0.2;
|
|
98
|
+
export interface CalibrationSample {
|
|
99
|
+
caseId: string;
|
|
100
|
+
repeatIndex: number;
|
|
101
|
+
label: boolean;
|
|
102
|
+
judge: boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface CalibrationStats {
|
|
105
|
+
n: number;
|
|
106
|
+
agreement: number | null;
|
|
107
|
+
cohen_kappa: number | null;
|
|
108
|
+
tpr: number | null;
|
|
109
|
+
tnr: number | null;
|
|
110
|
+
confusion: {
|
|
111
|
+
tp: number;
|
|
112
|
+
fp: number;
|
|
113
|
+
tn: number;
|
|
114
|
+
fn: number;
|
|
115
|
+
};
|
|
116
|
+
warnings: Array<"low_n" | "imbalanced" | "no_variance">;
|
|
117
|
+
}
|
|
118
|
+
/** Judge pass is the positive class; the human label is the truth. */
|
|
119
|
+
export declare function computeCalibration(samples: CalibrationSample[]): CalibrationStats;
|
|
120
|
+
/** A strict majority of valid votes passes; a tie fails. */
|
|
121
|
+
export declare function majorityPasses(passes: number, valid: number): boolean;
|
|
122
|
+
export declare function meanOf(values: readonly number[]): number;
|
|
123
|
+
/** Deterministic 70/30 split by a stable hash of the case id, so every repeat of a case lands on the same side. */
|
|
124
|
+
export declare function calibrationSplitOf(caseId: string): "dev" | "held_out";
|
|
125
|
+
export interface StabilityStats {
|
|
126
|
+
/** Judge calls per output. */
|
|
127
|
+
k: number;
|
|
128
|
+
/** Outputs with at least two valid votes. */
|
|
129
|
+
n: number;
|
|
130
|
+
/** Outputs whose valid votes were not all the same verdict. */
|
|
131
|
+
unstable: number;
|
|
132
|
+
flip_rate: number | null;
|
|
133
|
+
}
|
|
134
|
+
/** How often the judge changes its mind on the same output: the share of outputs with a split vote. */
|
|
135
|
+
export declare function computeStability(voteSets: boolean[][], k: number): StabilityStats;
|
package/dist/evals/stats.js
CHANGED
|
@@ -218,3 +218,72 @@ export function computeStats(input) {
|
|
|
218
218
|
...(hasAgentData ? { mean_turns: mean(turnValues), mean_tool_calls: mean(toolCallValues) } : {}),
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
|
+
export const CALIBRATION_MIN_LABELS = 20;
|
|
222
|
+
export const CALIBRATION_MINORITY_SHARE = 0.2;
|
|
223
|
+
/** Judge pass is the positive class; the human label is the truth. */
|
|
224
|
+
export function computeCalibration(samples) {
|
|
225
|
+
let tp = 0;
|
|
226
|
+
let fp = 0;
|
|
227
|
+
let tn = 0;
|
|
228
|
+
let fn = 0;
|
|
229
|
+
for (const s of samples) {
|
|
230
|
+
if (s.judge && s.label)
|
|
231
|
+
tp++;
|
|
232
|
+
else if (s.judge && !s.label)
|
|
233
|
+
fp++;
|
|
234
|
+
else if (!s.judge && !s.label)
|
|
235
|
+
tn++;
|
|
236
|
+
else
|
|
237
|
+
fn++;
|
|
238
|
+
}
|
|
239
|
+
const n = tp + fp + tn + fn;
|
|
240
|
+
const warnings = [];
|
|
241
|
+
if (n < CALIBRATION_MIN_LABELS)
|
|
242
|
+
warnings.push("low_n");
|
|
243
|
+
const labelPass = tp + fn;
|
|
244
|
+
const labelFail = fp + tn;
|
|
245
|
+
if (n > 0 && Math.min(labelPass, labelFail) / n < CALIBRATION_MINORITY_SHARE)
|
|
246
|
+
warnings.push("imbalanced");
|
|
247
|
+
const agreement = n > 0 ? (tp + tn) / n : null;
|
|
248
|
+
const judgePass = tp + fp;
|
|
249
|
+
const judgeFail = fn + tn;
|
|
250
|
+
const expected = n > 0 ? (judgePass * labelPass + judgeFail * labelFail) / (n * n) : 0;
|
|
251
|
+
let kappa = null;
|
|
252
|
+
if (n > 0 && agreement !== null) {
|
|
253
|
+
if (expected >= 1)
|
|
254
|
+
warnings.push("no_variance");
|
|
255
|
+
else
|
|
256
|
+
kappa = (agreement - expected) / (1 - expected);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
n,
|
|
260
|
+
agreement,
|
|
261
|
+
cohen_kappa: kappa,
|
|
262
|
+
tpr: labelPass > 0 ? tp / labelPass : null,
|
|
263
|
+
tnr: labelFail > 0 ? tn / labelFail : null,
|
|
264
|
+
confusion: { tp, fp, tn, fn },
|
|
265
|
+
warnings,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** A strict majority of valid votes passes; a tie fails. */
|
|
269
|
+
export function majorityPasses(passes, valid) {
|
|
270
|
+
return passes * 2 > valid;
|
|
271
|
+
}
|
|
272
|
+
export function meanOf(values) {
|
|
273
|
+
return values.reduce((total, v) => total + v, 0) / values.length;
|
|
274
|
+
}
|
|
275
|
+
/** Deterministic 70/30 split by a stable hash of the case id, so every repeat of a case lands on the same side. */
|
|
276
|
+
export function calibrationSplitOf(caseId) {
|
|
277
|
+
let h = 2166136261;
|
|
278
|
+
for (let i = 0; i < caseId.length; i++) {
|
|
279
|
+
h ^= caseId.charCodeAt(i);
|
|
280
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
281
|
+
}
|
|
282
|
+
return h % 10 < 7 ? "dev" : "held_out";
|
|
283
|
+
}
|
|
284
|
+
/** How often the judge changes its mind on the same output: the share of outputs with a split vote. */
|
|
285
|
+
export function computeStability(voteSets, k) {
|
|
286
|
+
const counted = voteSets.filter((votes) => votes.length >= 2);
|
|
287
|
+
const unstable = counted.filter((votes) => votes.some((v) => v !== votes[0])).length;
|
|
288
|
+
return { k, n: counted.length, unstable, flip_rate: counted.length > 0 ? unstable / counted.length : null };
|
|
289
|
+
}
|
|
@@ -205,3 +205,45 @@ export type EvalGraderVersionInput = {
|
|
|
205
205
|
graderId: string;
|
|
206
206
|
graderHash: string;
|
|
207
207
|
};
|
|
208
|
+
export type EvalGraderLabelRow = {
|
|
209
|
+
evalRunId: string;
|
|
210
|
+
caseId: string;
|
|
211
|
+
repeatIndex: number;
|
|
212
|
+
graderId: string;
|
|
213
|
+
label: EvalReviewLabel;
|
|
214
|
+
note: string | null;
|
|
215
|
+
createdAt: string;
|
|
216
|
+
updatedAt: string;
|
|
217
|
+
};
|
|
218
|
+
export type EvalGraderLabelPatch = {
|
|
219
|
+
label: EvalReviewLabel | null;
|
|
220
|
+
note?: string | null;
|
|
221
|
+
};
|
|
222
|
+
export type EvalCalibrationSplit = "all" | "dev" | "held_out";
|
|
223
|
+
export type EvalCalibrationDisagreement = {
|
|
224
|
+
/** The run the labeled result came from; absent on calibrations stored before this field existed. */
|
|
225
|
+
evalRunId?: string;
|
|
226
|
+
caseId: string;
|
|
227
|
+
repeatIndex: number;
|
|
228
|
+
label: EvalReviewLabel;
|
|
229
|
+
judge: EvalReviewLabel;
|
|
230
|
+
reason: string | null;
|
|
231
|
+
};
|
|
232
|
+
export type EvalCalibrationInput = {
|
|
233
|
+
projectRoot: string;
|
|
234
|
+
evalId: string;
|
|
235
|
+
graderId: string;
|
|
236
|
+
graderHash: string;
|
|
237
|
+
judge: string;
|
|
238
|
+
promptVersion: number;
|
|
239
|
+
split: EvalCalibrationSplit;
|
|
240
|
+
candidate: boolean;
|
|
241
|
+
stats: EvalJson;
|
|
242
|
+
disagreements: EvalCalibrationDisagreement[];
|
|
243
|
+
costUsd: number;
|
|
244
|
+
};
|
|
245
|
+
export type EvalCalibrationRow = Omit<EvalCalibrationInput, "stats"> & {
|
|
246
|
+
id: string;
|
|
247
|
+
stats: EvalJson;
|
|
248
|
+
createdAt: string;
|
|
249
|
+
};
|
package/dist/evals/types.d.ts
CHANGED
|
@@ -57,9 +57,18 @@ export type EvalTask = {
|
|
|
57
57
|
context?: string;
|
|
58
58
|
constraints?: string;
|
|
59
59
|
};
|
|
60
|
+
export type JudgeSettings = {
|
|
61
|
+
/** Sampling temperature for the judge call; unset leaves the provider default. */
|
|
62
|
+
temperature?: number;
|
|
63
|
+
/** The judge writes its reason before the verdict. */
|
|
64
|
+
reasoning?: boolean;
|
|
65
|
+
/** Judge calls per grade (odd, 1 to 5); the majority verdict wins. */
|
|
66
|
+
votes?: number;
|
|
67
|
+
};
|
|
60
68
|
export type EvalDefaults = {
|
|
61
69
|
repeats?: number;
|
|
62
70
|
judge?: string;
|
|
71
|
+
judge_settings?: JudgeSettings;
|
|
63
72
|
pass_threshold?: number;
|
|
64
73
|
max_cost_usd?: number;
|
|
65
74
|
concurrency?: number;
|
|
@@ -103,6 +112,7 @@ export type GraderConfig = (GraderCommon & {
|
|
|
103
112
|
scale?: "pass_fail" | "1_to_5";
|
|
104
113
|
pass_at?: number;
|
|
105
114
|
judge?: string;
|
|
115
|
+
judge_settings?: JudgeSettings;
|
|
106
116
|
examples?: RubricExample[];
|
|
107
117
|
}) | (GraderCommon & {
|
|
108
118
|
type: "code";
|
|
@@ -122,6 +132,7 @@ export type GraderConfig = (GraderCommon & {
|
|
|
122
132
|
}) | (GraderCommon & {
|
|
123
133
|
type: "goal_reached";
|
|
124
134
|
judge?: string;
|
|
135
|
+
judge_settings?: JudgeSettings;
|
|
125
136
|
guidance?: string;
|
|
126
137
|
});
|
|
127
138
|
export type EvalFile = {
|
|
@@ -178,6 +189,7 @@ export type ResolvedGrader = (ResolvedGraderCommon & {
|
|
|
178
189
|
scale: "pass_fail" | "1_to_5";
|
|
179
190
|
passAt: number;
|
|
180
191
|
judge: string;
|
|
192
|
+
judgeSettings?: JudgeSettings;
|
|
181
193
|
examples: RubricExample[];
|
|
182
194
|
}) | (ResolvedGraderCommon & {
|
|
183
195
|
type: "code";
|
|
@@ -197,6 +209,7 @@ export type ResolvedGrader = (ResolvedGraderCommon & {
|
|
|
197
209
|
}) | (ResolvedGraderCommon & {
|
|
198
210
|
type: "goal_reached";
|
|
199
211
|
judge: string;
|
|
212
|
+
judgeSettings?: JudgeSettings;
|
|
200
213
|
guidance?: string;
|
|
201
214
|
});
|
|
202
215
|
export type PlanCase = {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { NormalizedGrade } from "./types.js";
|
|
2
|
+
export declare const JUDGE_ERROR_CODE = "EVAL_JUDGE_ERROR";
|
|
3
|
+
/**
|
|
4
|
+
* One grade from several votes: the majority verdict for pass/fail (a tie fails), the mean raw score for 1 to 5.
|
|
5
|
+
* Errored votes are left out; if every vote errored the grade is an error. Tokens and cost add up over all votes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function combineVotes(grader: {
|
|
8
|
+
scale: "pass_fail" | "1_to_5";
|
|
9
|
+
passAt: number;
|
|
10
|
+
}, votes: readonly NormalizedGrade[]): NormalizedGrade;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { normalizeOneToFive } from "./scoring.js";
|
|
2
|
+
import { majorityPasses, meanOf } from "./stats.js";
|
|
3
|
+
export const JUDGE_ERROR_CODE = "EVAL_JUDGE_ERROR";
|
|
4
|
+
/**
|
|
5
|
+
* One grade from several votes: the majority verdict for pass/fail (a tie fails), the mean raw score for 1 to 5.
|
|
6
|
+
* Errored votes are left out; if every vote errored the grade is an error. Tokens and cost add up over all votes.
|
|
7
|
+
*/
|
|
8
|
+
export function combineVotes(grader, votes) {
|
|
9
|
+
const first = votes[0];
|
|
10
|
+
if (votes.length === 1)
|
|
11
|
+
return first;
|
|
12
|
+
const valid = votes.filter((g) => g.status === "graded" && g.passed !== undefined);
|
|
13
|
+
const sum = (pick) => votes.reduce((total, g) => total + (pick(g) ?? 0), 0);
|
|
14
|
+
const totals = {
|
|
15
|
+
judgeTokensIn: sum((g) => g.judgeTokensIn),
|
|
16
|
+
judgeTokensOut: sum((g) => g.judgeTokensOut),
|
|
17
|
+
judgeCostUsd: sum((g) => g.judgeCostUsd),
|
|
18
|
+
};
|
|
19
|
+
const priceUnknown = votes.some((g) => g.details?.judgeCostUnknown);
|
|
20
|
+
const record = votes.map((g) => ({
|
|
21
|
+
status: g.status,
|
|
22
|
+
...(g.passed !== undefined ? { passed: g.passed } : {}),
|
|
23
|
+
...(g.rawValue !== undefined ? { raw_value: g.rawValue } : {}),
|
|
24
|
+
...(g.reason !== undefined ? { reason: g.reason } : {}),
|
|
25
|
+
}));
|
|
26
|
+
const baseDetails = {
|
|
27
|
+
...(first.details ?? {}),
|
|
28
|
+
...(priceUnknown ? { judgeCostUnknown: true } : {}),
|
|
29
|
+
votes: record,
|
|
30
|
+
};
|
|
31
|
+
if (valid.length === 0) {
|
|
32
|
+
return { ...first, ...totals, status: "error", errorCode: first.errorCode ?? JUDGE_ERROR_CODE, details: baseDetails };
|
|
33
|
+
}
|
|
34
|
+
let passed;
|
|
35
|
+
let rawValue;
|
|
36
|
+
let score;
|
|
37
|
+
if (grader.scale === "1_to_5") {
|
|
38
|
+
rawValue = meanOf(valid.map((g) => g.rawValue ?? 0));
|
|
39
|
+
score = normalizeOneToFive(rawValue);
|
|
40
|
+
passed = rawValue >= grader.passAt;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
passed = majorityPasses(valid.filter((g) => g.passed === true).length, valid.length);
|
|
44
|
+
rawValue = passed ? 1 : 0;
|
|
45
|
+
score = rawValue;
|
|
46
|
+
}
|
|
47
|
+
const lead = valid.find((g) => g.passed === passed) ?? valid[0];
|
|
48
|
+
return {
|
|
49
|
+
graderId: first.graderId,
|
|
50
|
+
graderType: first.graderType,
|
|
51
|
+
status: "graded",
|
|
52
|
+
passed,
|
|
53
|
+
score,
|
|
54
|
+
rawValue,
|
|
55
|
+
...(lead.reason !== undefined ? { reason: lead.reason } : {}),
|
|
56
|
+
...(first.judgeModel !== undefined ? { judgeModel: first.judgeModel } : {}),
|
|
57
|
+
...totals,
|
|
58
|
+
details: baseDetails,
|
|
59
|
+
};
|
|
60
|
+
}
|