test(coach): put the dialog scenarios to a real model nightly

The live evaluation now captures the tool calls a generation makes, with
their arguments, and hands them to the judge. runScenarioLive puts a
scenario to the model in the chat turn's own context (system prompt,
tool-mode and dialog addenda, DATA INVENTORY, EARLIER TABLES, transcript)
over the real tool catalogue, answers each call with a stand-in result,
and reads the reply through the turn's clarification and chip parsers.
runScenarioJudge grades each scenario a few times with the same
evaluateScenario the deterministic suite uses and reports a pass rate per
scenario; the server-only scenarios and checks are left out.

Like the existing judge it runs only with COACH_EVAL_API_KEY, from the
nightly workflow's non-blocking step, and never throws.
This commit is contained in:
Marc-André Bombeck committed 2026-09-27 13:07:25 +02:00
1 parent ec281c02e2
commit c2ab9cda11
6 files changed
+651 -8

No files matched your search

+25 -2
View File
@@ -10,7 +10,10 @@
* summary for the nightly log.
* 2. The LIVE JUDGE runs ONLY when `COACH_EVAL_API_KEY` is present. With the
* secret absent it no-ops with a clear line and exits 0 — the nightly
* workflow stays green and non-blocking.
* workflow stays green and non-blocking. The same key runs the dialog
* scenarios live: each is put to the model a few times and the tool
* calls (with their arguments) are graded, reported as a pass rate per
* scenario.
*
* Usage (nightly workflow / manual):
* COACH_EVAL_API_KEY=... pnpm dlx tsx scripts/run-coach-eval.ts
@@ -22,7 +25,7 @@ import {
} from "@/lib/ai/coach/eval/golden-cases";
import { captureDeterministic } from "@/lib/ai/coach/eval/run-case";
import { gradeSet } from "@/lib/ai/coach/eval/grade-groundedness";
import { runJudge } from "@/lib/ai/coach/eval/judge";
import { runJudge, runScenarioJudge } from "@/lib/ai/coach/eval/judge";
async function main() {
// Deterministic summary (the gate itself is the Vitest suite).
@@ -57,6 +60,26 @@ async function main() {
);
}
}
await reportScenarios();
}
/** The dialog scenarios, live: a pass rate per scenario, misses listed. */
async function reportScenarios() {
const run = await runScenarioJudge();
console.log(run.note);
for (const scenario of run.scenarios) {
console.log(
` ${scenario.passRate === 1 ? "PASS" : "MISS"} ${scenario.id} ${scenario.passed}/${scenario.runs}`,
);
scenario.misses.forEach((missed, i) => {
console.log(` run ${i + 1}: ${missed.join("; ")}`);
});
scenario.toolCalls.forEach((calls, i) => {
console.log(
` calls ${i + 1}: ${calls.map((c) => `${c.name}(${JSON.stringify(c.args)})`).join(", ") || "none"}`,
);
});
}
}
main().catch((err) => {