mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-28 00:02:42 +00:00
* feat: read-only ai sessions on protected workspaces with a use, plan and build picker Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: address local review findings on the use, plan and build picker Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: address CI review findings on use mode and protected sessions Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: hold test-run forms and pending protection lookups to use mode Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * refactor: fetch rules once per send and dedupe helpers and prompt text Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: send role-locked operators to an admin and cover it with an eval Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat: count the work mode each session message runs in Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: regenerate the mcp session definitions after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat: rename build to use & build and let the chat ask to switch modes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: word a declined mode switch by the switch, not the mode Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 55445977dd05c25791388070182c77f4dbc20c0e This commit updates the EE repository reference after PR #915 was merged in windmill-ee-private. Previous ee-repo-ref: 7f7bf7003e356346ce74395af02c5bdf14f38781 New ee-repo-ref: 55445977dd05c25791388070182c77f4dbc20c0e Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
357 lines
11 KiB
TypeScript
357 lines
11 KiB
TypeScript
import { judgeOutput, DEFAULT_JUDGE_MODEL } from "./judge";
|
|
import type {
|
|
BenchmarkAttemptResult,
|
|
BenchmarkCaseResult,
|
|
BenchmarkCheck,
|
|
EvalCase,
|
|
FrontendBenchmarkProgressEvent,
|
|
ModeRunner,
|
|
} from "./types";
|
|
import {
|
|
validateAssistantExpectations,
|
|
validateToolExpectations,
|
|
} from "./validators";
|
|
|
|
export async function runSuite<TInitial, TExpected, TActual>(input: {
|
|
modeRunner: ModeRunner<TInitial, TExpected, TActual>;
|
|
cases: EvalCase[];
|
|
runs: number;
|
|
runModel: string | null;
|
|
judgeModel?: string | null;
|
|
executionOnly?: boolean;
|
|
concurrency?: number;
|
|
verbose?: boolean;
|
|
onProgress?: (event: FrontendBenchmarkProgressEvent) => void;
|
|
}): Promise<BenchmarkCaseResult[]> {
|
|
const judgeModel =
|
|
input.judgeModel === undefined ? DEFAULT_JUDGE_MODEL : input.judgeModel;
|
|
const concurrency = Math.max(1, input.concurrency ?? input.modeRunner.concurrency);
|
|
const results = new Array<BenchmarkCaseResult>(input.cases.length);
|
|
let cursor = 0;
|
|
|
|
if (input.modeRunner.mode !== "cli") {
|
|
input.onProgress?.({
|
|
type: "run-start",
|
|
surface: input.modeRunner.mode,
|
|
totalCases: input.cases.length,
|
|
runs: input.runs,
|
|
concurrency,
|
|
});
|
|
}
|
|
|
|
async function worker(): Promise<void> {
|
|
while (true) {
|
|
const caseIndex = cursor++;
|
|
if (caseIndex >= input.cases.length) {
|
|
return;
|
|
}
|
|
const evalCase = input.cases[caseIndex];
|
|
results[caseIndex] = {
|
|
id: evalCase.id,
|
|
prompt: evalCase.prompt,
|
|
initialPath: evalCase.initialPath,
|
|
expectedPath: evalCase.expectedPath,
|
|
attempts: await runCaseAttempts({
|
|
caseIndex,
|
|
evalCase,
|
|
runs: input.runs,
|
|
judgeModel,
|
|
judgeThreshold: input.modeRunner.judgeThreshold ?? 80,
|
|
executionOnly: input.executionOnly ?? false,
|
|
modeRunner: input.modeRunner,
|
|
totalCases: input.cases.length,
|
|
verbose: input.verbose ?? false,
|
|
onProgress: input.onProgress,
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
await Promise.all(
|
|
Array.from({ length: Math.min(concurrency, input.cases.length) }, () => worker())
|
|
);
|
|
|
|
return results;
|
|
}
|
|
|
|
async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
|
caseIndex: number;
|
|
evalCase: EvalCase;
|
|
runs: number;
|
|
judgeModel: string | null;
|
|
judgeThreshold: number;
|
|
executionOnly: boolean;
|
|
modeRunner: ModeRunner<TInitial, TExpected, TActual>;
|
|
totalCases: number;
|
|
verbose: boolean;
|
|
onProgress?: (event: FrontendBenchmarkProgressEvent) => void;
|
|
}): Promise<BenchmarkAttemptResult[]> {
|
|
const attempts: BenchmarkAttemptResult[] = [];
|
|
const surface = input.modeRunner.mode === "cli" ? null : input.modeRunner.mode;
|
|
|
|
for (let attempt = 1; attempt <= input.runs; attempt += 1) {
|
|
if (surface) {
|
|
input.onProgress?.({
|
|
type: "attempt-start",
|
|
surface,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
});
|
|
}
|
|
|
|
const startedAt = Date.now();
|
|
|
|
try {
|
|
const initial = await input.modeRunner.loadInitial(input.evalCase.initialPath);
|
|
const expected = input.executionOnly
|
|
? undefined
|
|
: await input.modeRunner.loadExpected(input.evalCase.expectedPath);
|
|
const run = await input.modeRunner.run(input.evalCase.prompt, initial, {
|
|
evalCase: input.evalCase,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
verbose: input.verbose,
|
|
onAssistantMessageStart: input.verbose && surface
|
|
? () =>
|
|
input.onProgress?.({
|
|
type: "assistant-message-start",
|
|
surface,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
})
|
|
: undefined,
|
|
onAssistantChunk: input.verbose && surface
|
|
? (chunk: string) =>
|
|
input.onProgress?.({
|
|
type: "assistant-chunk",
|
|
surface,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
chunk,
|
|
})
|
|
: undefined,
|
|
onAssistantMessageEnd: input.verbose && surface
|
|
? () =>
|
|
input.onProgress?.({
|
|
type: "assistant-message-end",
|
|
surface,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
})
|
|
: undefined,
|
|
onToolCall: input.verbose && surface
|
|
? ({ toolName, argumentsText }) =>
|
|
input.onProgress?.({
|
|
type: "tool-call",
|
|
surface,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
toolName,
|
|
argumentsText,
|
|
})
|
|
: undefined,
|
|
});
|
|
const checks: BenchmarkCheck[] = [
|
|
buildCheck("run succeeded", run.success, run.error),
|
|
];
|
|
if (!input.executionOnly) {
|
|
checks.push(
|
|
...input.modeRunner.validate({
|
|
evalCase: input.evalCase,
|
|
prompt: input.evalCase.prompt,
|
|
initial,
|
|
expected,
|
|
actual: run.actual,
|
|
run,
|
|
}),
|
|
...validateToolExpectations({
|
|
run,
|
|
toolExpect: input.evalCase.toolExpect,
|
|
}),
|
|
...validateAssistantExpectations({
|
|
run,
|
|
assistantExpect: input.evalCase.assistantExpect,
|
|
})
|
|
);
|
|
}
|
|
const artifactFiles = input.modeRunner.buildArtifacts?.(run.actual) ?? [];
|
|
|
|
if (
|
|
run.success &&
|
|
!input.executionOnly &&
|
|
input.modeRunner.backendValidate
|
|
) {
|
|
try {
|
|
const backendValidation = await input.modeRunner.backendValidate({
|
|
evalCase: input.evalCase,
|
|
prompt: input.evalCase.prompt,
|
|
initial,
|
|
expected,
|
|
actual: run.actual,
|
|
run,
|
|
context: {
|
|
evalCase: input.evalCase,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
verbose: input.verbose,
|
|
onAssistantMessageStart: undefined,
|
|
onAssistantChunk: undefined,
|
|
onAssistantMessageEnd: undefined,
|
|
},
|
|
});
|
|
|
|
if (backendValidation) {
|
|
checks.push(...backendValidation.checks);
|
|
artifactFiles.push(...(backendValidation.artifactFiles ?? []));
|
|
}
|
|
} catch (error) {
|
|
checks.push(
|
|
buildCheck(
|
|
"backend validation succeeded",
|
|
false,
|
|
error instanceof Error ? error.message : String(error)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
let judgeScore: number | null = null;
|
|
let judgeSummary: string | null = null;
|
|
|
|
if (
|
|
run.success &&
|
|
!input.executionOnly &&
|
|
input.judgeModel !== null &&
|
|
!input.evalCase.skipJudge
|
|
) {
|
|
const output = input.modeRunner.prepareJudgeActual
|
|
? input.modeRunner.prepareJudgeActual(run.actual)
|
|
: run.actual;
|
|
const judge = await judgeOutput({
|
|
mode: input.modeRunner.mode,
|
|
prompt: input.evalCase.prompt,
|
|
checklist: input.evalCase.judgeChecklist,
|
|
initial,
|
|
expected: input.modeRunner.mode === "cli" ? undefined : expected,
|
|
actual: input.evalCase.judgeAssistantText
|
|
? { assistantReply: run.assistantText, output }
|
|
: output,
|
|
model: input.judgeModel,
|
|
});
|
|
|
|
judgeScore = judge.success ? judge.score : null;
|
|
judgeSummary = judge.summary;
|
|
checks.push(buildCheck("judge succeeded", judge.success, judge.error));
|
|
checks.push(
|
|
buildCheck(
|
|
`judge score >= ${input.judgeThreshold}`,
|
|
(judgeScore ?? 0) >= input.judgeThreshold,
|
|
judge.success ? `score=${judgeScore}` : judge.error
|
|
)
|
|
);
|
|
}
|
|
|
|
const attemptResult: BenchmarkAttemptResult = {
|
|
attempt,
|
|
passed: checks.every((check) => check.passed),
|
|
durationMs: Date.now() - startedAt,
|
|
assistantMessageCount: run.assistantMessageCount,
|
|
toolCallCount: run.toolCallCount,
|
|
toolsUsed: uniqueStrings(run.toolsUsed),
|
|
toolCallDetails: run.toolCallDetails,
|
|
skillsInvoked: uniqueStrings(run.skillsInvoked),
|
|
checks,
|
|
judgeScore,
|
|
judgeSummary,
|
|
error: run.error ?? null,
|
|
tokenUsage: run.tokenUsage ?? null,
|
|
finalContextTokens: run.finalContextTokens ?? null,
|
|
artifactsPath: null,
|
|
artifactFiles,
|
|
};
|
|
|
|
if (surface) {
|
|
input.onProgress?.({
|
|
type: "attempt-finish",
|
|
surface,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
passed: attemptResult.passed,
|
|
durationMs: attemptResult.durationMs,
|
|
judgeScore: attemptResult.judgeScore,
|
|
error: attemptResult.error,
|
|
});
|
|
}
|
|
|
|
attempts.push(attemptResult);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const failedAttempt: BenchmarkAttemptResult = {
|
|
attempt,
|
|
passed: false,
|
|
durationMs: Date.now() - startedAt,
|
|
assistantMessageCount: 0,
|
|
toolCallCount: 0,
|
|
toolsUsed: [],
|
|
skillsInvoked: [],
|
|
checks: [buildCheck("run crashed", false, message)],
|
|
judgeScore: null,
|
|
judgeSummary: null,
|
|
error: message,
|
|
tokenUsage: null,
|
|
finalContextTokens: null,
|
|
};
|
|
if (surface) {
|
|
input.onProgress?.({
|
|
type: "attempt-finish",
|
|
surface,
|
|
caseId: input.evalCase.id,
|
|
caseNumber: input.caseIndex + 1,
|
|
totalCases: input.totalCases,
|
|
attempt,
|
|
runs: input.runs,
|
|
passed: false,
|
|
durationMs: failedAttempt.durationMs,
|
|
judgeScore: null,
|
|
error: message,
|
|
});
|
|
}
|
|
attempts.push(failedAttempt);
|
|
}
|
|
}
|
|
|
|
return attempts;
|
|
}
|
|
|
|
function buildCheck(name: string, passed: boolean, details?: string): BenchmarkCheck {
|
|
return details ? { name, passed, details } : { name, passed };
|
|
}
|
|
|
|
function uniqueStrings(values: string[]): string[] {
|
|
return [...new Set(values)];
|
|
}
|