Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

105 lines
3.2 KiB
TypeScript

import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import type { INodeTypeDescription } from 'n8n-workflow';
import type { SimpleWorkflow } from '@/types/workflow';
import { DETERMINISTIC_CHECKS } from './checks';
import { LLM_CHECKS } from './llm-checks';
import type { BinaryCheck, BinaryCheckContext } from './types';
import type { EvaluationContext, Evaluator, Feedback } from '../../harness/harness-types';
export interface BinaryChecksEvaluatorOptions {
nodeTypes: INodeTypeDescription[];
llm?: BaseChatModel;
checks?: string[];
}
const EVALUATOR_NAME = 'binary-checks';
/**
* Create a binary-checks evaluator that runs deterministic and optional LLM checks.
*
* Each check emits one Feedback with score 0 or 1.
*/
export function createBinaryChecksEvaluator(
options: BinaryChecksEvaluatorOptions,
): Evaluator<EvaluationContext> {
const allChecks: BinaryCheck[] = [...DETERMINISTIC_CHECKS, ...(options.llm ? LLM_CHECKS : [])];
const allCheckNames = allChecks.map((c) => c.name);
let selectedChecks: BinaryCheck[];
if (options.checks && options.checks.length > 0) {
const validNames = new Set(allCheckNames);
const unrecognized = options.checks.filter((name) => !validNames.has(name));
for (const name of unrecognized) {
console.warn(`Warning: unrecognized check name "${name}" in --checks filter`);
}
selectedChecks = allChecks.filter((c) => options.checks!.includes(c.name));
if (selectedChecks.length === 0) {
throw new Error(
`No valid checks after filtering. Requested: ${options.checks.join(', ')}. Available: ${allCheckNames.join(', ')}`,
);
}
} else {
selectedChecks = allChecks;
}
return {
name: EVALUATOR_NAME,
async evaluate(workflow: SimpleWorkflow, ctx: EvaluationContext): Promise<Feedback[]> {
const checkCtx: BinaryCheckContext = {
prompt: ctx.prompt,
nodeTypes: options.nodeTypes,
annotations: ctx.annotations,
llm: options.llm,
// Intentionally omit llmCallLimiter: binary-checks LLM judges are small,
// cheap calls that should run in parallel, not throttled by the shared limiter.
timeoutMs: ctx.timeoutMs,
};
const results = await Promise.allSettled(
selectedChecks.map(async (check) => {
try {
const result = await check.run(workflow, checkCtx);
return { check, result };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { check, result: { pass: false, comment: `Error: ${message}` } };
}
}),
);
return results.map((settled, i) => {
if (settled.status === 'fulfilled') {
const { check, result } = settled.value;
return {
evaluator: EVALUATOR_NAME,
metric: check.name,
score: result.pass ? 1 : 0,
kind: 'metric' as const,
...(result.comment ? { comment: result.comment } : {}),
};
}
// Should not happen since we catch inside, but handle gracefully
const check = selectedChecks[i];
const reason =
settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
return {
evaluator: EVALUATOR_NAME,
metric: check.name,
score: 0,
kind: 'metric' as const,
comment: `Unexpected error: ${reason}`,
};
});
},
};
}