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
105 lines
3.2 KiB
TypeScript
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}`,
|
|
};
|
|
});
|
|
},
|
|
};
|
|
}
|