mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
b0ddcf31e4
* ci: add path-gated AI agent integration tests workflow Runs integration_tests/ai_agent_tests against real LLM providers (Anthropic/OpenAI/Google) only when AI-agent backend code or the tests change, since runs make paid LLM calls. Adds a conftest fixture that skips provider-parametrized cases whose API keys are absent, so CI exercises only the providers it has secrets for. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: add path-gated ai_evals global-mode smoke workflow Runs the global AI chat eval (global-test1) across one cheap model per provider (Anthropic/OpenAI/Google/DeepSeek) only when the eval harness or copilot chat code change, since runs make paid LLM calls. Builds Windmill CE from source as the AI proxy; global tools/drafts run in the Vitest bridge. Gates on the deterministic draft pipeline (run succeeded + produced a draft + used write_script), not the variable LLM judge score. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run AI smokes on PR ready-for-review instead of every push Switch the pull_request trigger from `synchronize` (every commit) to `ready_for_review`, with a job guard skipping draft PRs, so the paid LLM runs only fire when a PR is marked ready to merge (plus push-to-main and manual dispatch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ai_evals): lazily load cli mode so non-cli evals skip the cli toolchain The entrypoint eagerly imported modes/cli, which pulls the wmill CLI guidance modules and their JSR deps (@cliffy/*). Global/flow/script/app runs then crashed with "Cannot find module '@cliffy/ansi/colors'" when the cli workspace deps were not installed. Import createCliModeRunner dynamically inside runCliBenchmark instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ai_agent): raise low max_completion_tokens to OpenAI's 16 minimum OpenAI's /v1/responses rejects max_output_tokens < 16 with a 400, failing test_low_max_tokens for openai. 16 still exercises a truncated response. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run ai_evals workflow on Node 22 for the frontend undici 8.x dep The Vitest bridge loads frontend/node_modules/undici@8.x, which requires Node >=22.19; Node 20 failed with "webidl.util.markAsUncloneable is not a function" when loading vitest.config.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ai_evals): run frontend evals autonomously + give global-test1 more turns Frontend evals (flow/script/app/global) ran the production chat prompt, which assumes an interactive human — so cheaper models burned their turn budget asking for confirmation, waiting for approval, or presenting a plan, sometimes hitting maxTurns without producing a draft. Append a shared autonomy note in baseEvalRunner (the path all frontend modes share, mirroring cli mode): act directly on clear requests; only ask on genuinely ambiguous ones (preserving the askUserQuestion cases). Also raise global-test1's maxTurns 8 -> 10 so a model that over-explores still converges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(ai_evals): watch draft/prompt deps outside copilot/ The global eval runs production frontend code in-process, so the smoke's behavior depends on files outside frontend/src/lib/components/copilot/**: the draft model (userDraft.svelte.ts, userDraftDbSyncer.svelte.ts), script inference (infer.ts), and the chat system prompts ($system_prompts -> system_prompts/auto-generated). Add them to both push and PR path filters so a change there actually triggers the smoke that gates on draft production. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: skip direct provider tests without credentials * feat: add ai evals skip judge flag * fix: simplify ai evals ci gate * fix: simplify ai evals smoke gate * fix: handle ai eval workflow triggers --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import { loadSelectedCases } from "../../core/cases";
|
|
import { resolveBackendValidationSettings } from "../../core/backendValidation";
|
|
import {
|
|
formatRunModelLabel,
|
|
getFrontendEvalModel,
|
|
resolveEvalModel,
|
|
} from "../../core/models";
|
|
import { buildRunResult } from "../../core/results";
|
|
import { runSuite } from "../../core/runSuite";
|
|
import type { BenchmarkRunResult, ModeRunner } from "../../core/types";
|
|
import { resolveWindmillBackendSettings } from "../../core/windmillBackendSettings";
|
|
import { emitFrontendBenchmarkProgress } from "./progress";
|
|
import { DEFAULT_JUDGE_MODEL } from "../../core/judge";
|
|
|
|
export type FrontendBenchmarkMode = "flow" | "app" | "script" | "global";
|
|
|
|
export async function runFrontendBenchmarkFromEnv(): Promise<BenchmarkRunResult> {
|
|
const mode = parseMode(process.env.WMILL_FRONTEND_AI_EVAL_MODE);
|
|
const caseIds = parseOptionalJsonStringArray(
|
|
process.env.WMILL_FRONTEND_AI_EVAL_CASE_IDS,
|
|
);
|
|
const runs = parsePositiveInteger(
|
|
process.env.WMILL_FRONTEND_AI_EVAL_RUNS,
|
|
"WMILL_FRONTEND_AI_EVAL_RUNS",
|
|
);
|
|
const emitProgress = process.env.WMILL_FRONTEND_AI_EVAL_PROGRESS === "1";
|
|
const verbose = process.env.WMILL_FRONTEND_AI_EVAL_VERBOSE === "1";
|
|
const executionOnly =
|
|
process.env.WMILL_FRONTEND_AI_EVAL_EXECUTION_ONLY === "1";
|
|
const judgeModel =
|
|
process.env.WMILL_FRONTEND_AI_EVAL_SKIP_JUDGE === "1" || executionOnly
|
|
? null
|
|
: DEFAULT_JUDGE_MODEL;
|
|
const model = resolveEvalModel(
|
|
mode,
|
|
process.env.WMILL_FRONTEND_AI_EVAL_MODEL,
|
|
);
|
|
const backendValidation = resolveBackendValidationSettings({
|
|
evalMode: mode,
|
|
requestedMode: process.env.WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION,
|
|
});
|
|
const backendSettings = resolveWindmillBackendSettings();
|
|
|
|
const selectedCases = await loadSelectedCases(mode, caseIds);
|
|
const modeRunner = await getModeRunner(
|
|
mode,
|
|
getFrontendEvalModel(model),
|
|
backendValidation,
|
|
backendSettings,
|
|
);
|
|
const runModel = formatRunModelLabel(mode, model);
|
|
const caseResults = await runSuite({
|
|
modeRunner,
|
|
cases: selectedCases,
|
|
runs,
|
|
runModel,
|
|
judgeModel,
|
|
executionOnly,
|
|
concurrency: verbose ? 1 : undefined,
|
|
verbose,
|
|
onProgress: emitProgress
|
|
? (event) => emitFrontendBenchmarkProgress(event)
|
|
: undefined,
|
|
});
|
|
|
|
return buildRunResult({
|
|
mode,
|
|
runs,
|
|
runModel,
|
|
judgeModel,
|
|
caseResults,
|
|
});
|
|
}
|
|
|
|
async function getModeRunner(
|
|
mode: FrontendBenchmarkMode,
|
|
model: ReturnType<typeof getFrontendEvalModel>,
|
|
backendValidation: ReturnType<typeof resolveBackendValidationSettings>,
|
|
backendSettings: ReturnType<typeof resolveWindmillBackendSettings>,
|
|
): Promise<ModeRunner<any, any, any>> {
|
|
switch (mode) {
|
|
case "flow": {
|
|
const { createFlowModeRunner } = await import("../../modes/flow");
|
|
return createFlowModeRunner(model, backendValidation, backendSettings);
|
|
}
|
|
case "app": {
|
|
const { createAppModeRunner } = await import("../../modes/app");
|
|
return createAppModeRunner(model, backendSettings);
|
|
}
|
|
case "script": {
|
|
const { createScriptModeRunner } = await import("../../modes/script");
|
|
return createScriptModeRunner(
|
|
model,
|
|
backendValidation,
|
|
backendSettings,
|
|
);
|
|
}
|
|
case "global": {
|
|
const { createGlobalModeRunner } = await import("../../modes/global");
|
|
return createGlobalModeRunner(model, backendSettings);
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseMode(value: string | undefined): FrontendBenchmarkMode {
|
|
if (
|
|
value === "flow" ||
|
|
value === "app" ||
|
|
value === "script" ||
|
|
value === "global"
|
|
) {
|
|
return value;
|
|
}
|
|
throw new Error(`Unsupported frontend benchmark mode: ${String(value)}`);
|
|
}
|
|
|
|
function parseOptionalJsonStringArray(value: string | undefined): string[] {
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
const parsed = JSON.parse(value) as unknown;
|
|
if (
|
|
!Array.isArray(parsed) ||
|
|
parsed.some((entry) => typeof entry !== "string")
|
|
) {
|
|
throw new Error(
|
|
"WMILL_FRONTEND_AI_EVAL_CASE_IDS must be a JSON string array",
|
|
);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function parsePositiveInteger(
|
|
value: string | undefined,
|
|
envName: string,
|
|
): number {
|
|
const parsed = Number(value);
|
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
throw new Error(`${envName} must be a positive integer`);
|
|
}
|
|
return parsed;
|
|
}
|