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

308 lines
8.5 KiB
TypeScript

import { writeFileSync } from 'node:fs';
import type { ExampleResult, Feedback } from './harness-types';
import { DETERMINISTIC_CHECKS } from '../evaluators/binary-checks/checks';
import { LLM_CHECKS } from '../evaluators/binary-checks/llm-checks';
/**
* Fixed columns that appear first in the CSV (in order).
*/
const FIXED_COLUMNS = [
'prompt',
'overall_score',
'status',
'gen_latency_ms',
'gen_input_tokens',
'gen_output_tokens',
'node_count',
'discovery_latency_ms',
'builder_latency_ms',
'responder_latency_ms',
] as const;
/**
* LLM Judge metrics to include (in order).
* Each metric gets a score column and a _detail column.
*/
const LLM_JUDGE_METRICS = [
'functionality',
'connections',
'expressions',
'nodeConfiguration',
'efficiency',
'dataFlow',
'maintainability',
'bestPractices',
] as const;
/**
* Pairwise evaluator metrics.
*/
const PAIRWISE_METRICS = [
'pairwise_primary',
'pairwise_diagnostic',
'pairwise_judges_passed',
'pairwise_total_passes',
'pairwise_total_violations',
] as const;
/**
* Binary check names derived from the check registries.
* Order: deterministic checks first, then LLM checks.
*/
const BINARY_CHECK_NAMES = [
...DETERMINISTIC_CHECKS.map((c) => c.name),
...LLM_CHECKS.map((c) => c.name),
];
type EvaluationSuite = 'llm-judge' | 'pairwise' | 'binary-checks' | 'unknown';
/**
* Escape a value for CSV output.
* Wraps in quotes if contains comma, quote, or newline.
*/
function escapeCsvValue(value: string | number | undefined): string {
if (value === undefined || value === null) return '';
const str = String(value);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
}
/**
* Detect the evaluation suite from feedback.
*/
function detectSuite(feedback: Feedback[]): EvaluationSuite {
if (feedback.some((f) => f.evaluator === 'llm-judge')) {
return 'llm-judge';
}
if (feedback.some((f) => f.evaluator === 'pairwise')) {
return 'pairwise';
}
if (feedback.some((f) => f.evaluator === 'binary-checks')) {
return 'binary-checks';
}
return 'unknown';
}
/**
* Extract the detail text from feedback for a given evaluator and metric.
*/
function extractMetricDetail(feedback: Feedback[], evaluator: string, metric: string): string {
const item = feedback.find((f) => f.evaluator === evaluator && f.metric === metric && f.comment);
return item?.comment ?? '';
}
/**
* Extract the score for a given evaluator and metric.
*/
function extractMetricScore(
feedback: Feedback[],
evaluator: string,
metric: string,
): number | undefined {
const item = feedback.find((f) => f.evaluator === evaluator && f.metric === metric);
return item?.score;
}
/**
* Get the number of judges used in pairwise evaluation.
*/
function getJudgeCount(feedback: Feedback[]): number {
const judgeMetrics = feedback.filter(
(f) => f.evaluator === 'pairwise' && f.metric.startsWith('judge'),
);
return judgeMetrics.length;
}
/**
* Build CSV row for LLM Judge suite.
*/
function buildLlmJudgeRow(result: ExampleResult): string[] {
const row: string[] = [];
// Fixed columns
row.push(escapeCsvValue(result.prompt));
row.push(escapeCsvValue(result.score));
row.push(escapeCsvValue(result.status));
row.push(escapeCsvValue(result.generationDurationMs));
row.push(escapeCsvValue(result.generationInputTokens));
row.push(escapeCsvValue(result.generationOutputTokens));
row.push(escapeCsvValue(result.subgraphMetrics?.nodeCount));
row.push(escapeCsvValue(result.subgraphMetrics?.discoveryDurationMs));
row.push(escapeCsvValue(result.subgraphMetrics?.builderDurationMs));
row.push(escapeCsvValue(result.subgraphMetrics?.responderDurationMs));
// LLM Judge metric columns (score + detail pairs)
for (const metric of LLM_JUDGE_METRICS) {
row.push(escapeCsvValue(extractMetricScore(result.feedback, 'llm-judge', metric)));
row.push(escapeCsvValue(extractMetricDetail(result.feedback, 'llm-judge', metric)));
}
return row;
}
/**
* Build CSV row for Pairwise suite.
*/
function buildPairwiseRow(result: ExampleResult, judgeCount: number): string[] {
const row: string[] = [];
// Fixed columns
row.push(escapeCsvValue(result.prompt));
row.push(escapeCsvValue(result.score));
row.push(escapeCsvValue(result.status));
row.push(escapeCsvValue(result.generationDurationMs));
row.push(escapeCsvValue(result.generationInputTokens));
row.push(escapeCsvValue(result.generationOutputTokens));
row.push(escapeCsvValue(result.subgraphMetrics?.nodeCount));
row.push(escapeCsvValue(result.subgraphMetrics?.discoveryDurationMs));
row.push(escapeCsvValue(result.subgraphMetrics?.builderDurationMs));
row.push(escapeCsvValue(result.subgraphMetrics?.responderDurationMs));
// Pairwise metrics (scores only, no detail)
for (const metric of PAIRWISE_METRICS) {
row.push(escapeCsvValue(extractMetricScore(result.feedback, 'pairwise', metric)));
}
// Individual judge results (score + violation detail)
for (let i = 1; i <= judgeCount; i++) {
const judgeMetric = `judge${i}`;
row.push(escapeCsvValue(extractMetricScore(result.feedback, 'pairwise', judgeMetric)));
row.push(escapeCsvValue(extractMetricDetail(result.feedback, 'pairwise', judgeMetric)));
}
return row;
}
/**
* Build CSV row for binary-checks suite.
*/
function buildBinaryChecksRow(result: ExampleResult): string[] {
const row: string[] = [];
row.push(escapeCsvValue(result.prompt));
row.push(escapeCsvValue(result.status));
row.push(escapeCsvValue(result.generationDurationMs));
for (const checkName of BINARY_CHECK_NAMES) {
row.push(escapeCsvValue(extractMetricScore(result.feedback, 'binary-checks', checkName)));
}
return row;
}
/**
* Build CSV header for LLM Judge suite.
*/
function buildLlmJudgeHeader(): string[] {
const header: string[] = [...FIXED_COLUMNS];
for (const metric of LLM_JUDGE_METRICS) {
header.push(metric);
header.push(`${metric}_detail`);
}
return header;
}
/**
* Build CSV header for Pairwise suite.
*/
function buildPairwiseHeader(judgeCount: number): string[] {
const header: string[] = [...FIXED_COLUMNS];
// Pairwise metrics
for (const metric of PAIRWISE_METRICS) {
header.push(metric);
}
// Individual judge columns
for (let i = 1; i <= judgeCount; i++) {
header.push(`judge${i}`);
header.push(`judge${i}_detail`);
}
return header;
}
/**
* Build CSV header for binary-checks suite.
*/
function buildBinaryChecksHeader(): string[] {
return ['prompt', 'status', 'gen_latency_ms', ...BINARY_CHECK_NAMES];
}
export interface WriteResultsCsvOptions {
/** Explicitly specify the evaluation suite. If not provided, auto-detects from feedback. */
suite?: 'llm-judge' | 'pairwise' | 'binary-checks';
}
/**
* Write evaluation results to a CSV file.
* Results are sorted by prompt for consistent ordering across runs.
* Automatically detects evaluation suite and formats accordingly,
* unless explicitly specified via options.
*/
export function writeResultsCsv(
results: ExampleResult[],
outputPath: string,
options?: WriteResultsCsvOptions,
): void {
if (results.length === 0) {
writeFileSync(outputPath, '', 'utf-8');
return;
}
// Sort by prompt for consistent ordering
const sorted = [...results].sort((a, b) => a.prompt.localeCompare(b.prompt));
// Use explicit suite if provided, otherwise detect from feedback
let suite: EvaluationSuite;
if (options?.suite) {
suite = options.suite;
} else {
// Detect suite from first result with feedback (excluding runner errors)
const firstWithFeedback = sorted.find((r) =>
r.feedback.some((f) => f.evaluator !== 'runner' && f.evaluator !== 'programmatic'),
);
suite = firstWithFeedback ? detectSuite(firstWithFeedback.feedback) : 'unknown';
}
const lines: string[] = [];
if (suite === 'pairwise') {
// Determine max judge count across all results
const judgeCount = Math.max(...sorted.map((r) => getJudgeCount(r.feedback)), 0);
// Header
lines.push(buildPairwiseHeader(judgeCount).join(','));
// Data rows
for (const result of sorted) {
lines.push(buildPairwiseRow(result, judgeCount).join(','));
}
} else if (suite === 'binary-checks') {
// Header
lines.push(buildBinaryChecksHeader().join(','));
// Data rows
for (const result of sorted) {
lines.push(buildBinaryChecksRow(result).join(','));
}
} else {
// Default to LLM Judge format (also handles unknown)
// Header
lines.push(buildLlmJudgeHeader().join(','));
// Data rows
for (const result of sorted) {
lines.push(buildLlmJudgeRow(result).join(','));
}
}
// Write file (overwrites if exists)
writeFileSync(outputPath, lines.join('\n') + '\n', 'utf-8');
}