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
242 lines
7.4 KiB
TypeScript
242 lines
7.4 KiB
TypeScript
/**
|
|
* Artifact saving for v2 evaluation harness.
|
|
*
|
|
* Saves evaluation results to disk in JSON format for later analysis.
|
|
*/
|
|
|
|
import { createHash } from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
import { feedbackKey } from './feedback';
|
|
import type { ExampleResult, Feedback, RunSummary } from './harness-types';
|
|
import type { EvalLogger } from './logger';
|
|
import { selectScoringItems, calculateFiniteAverage } from './score-calculator';
|
|
import type { SimpleWorkflow } from '../../src/types/workflow';
|
|
|
|
/**
|
|
* Interface for saving evaluation artifacts to disk.
|
|
*/
|
|
export interface ArtifactSaver {
|
|
/** Save a single example result */
|
|
saveExample(result: ExampleResult): void;
|
|
/** Save the final summary */
|
|
saveSummary(summary: RunSummary, results: ExampleResult[]): void;
|
|
}
|
|
|
|
/**
|
|
* Options for creating an artifact saver.
|
|
*/
|
|
export interface ArtifactSaverOptions {
|
|
/** Directory to save artifacts to */
|
|
outputDir: string;
|
|
/** Logger for optional save logs */
|
|
logger: EvalLogger;
|
|
}
|
|
|
|
/**
|
|
* Create an artifact saver for persisting evaluation results to disk.
|
|
*
|
|
* Directory structure:
|
|
* ```
|
|
* outputDir/
|
|
* ├── example-001/
|
|
* │ ├── prompt.txt
|
|
* │ ├── workflow.json
|
|
* │ └── feedback.json
|
|
* ├── example-002/
|
|
* │ └── ...
|
|
* └── summary.json
|
|
* ```
|
|
*
|
|
* @param options - Configuration options
|
|
* @returns ArtifactSaver instance or null if outputDir is not provided
|
|
*/
|
|
export function createArtifactSaver(options: ArtifactSaverOptions): ArtifactSaver {
|
|
const { outputDir, logger } = options;
|
|
|
|
// Create output directory if it doesn't exist
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
return {
|
|
saveExample(result: ExampleResult): void {
|
|
const exampleDir = path.join(outputDir, getExampleDirName(result));
|
|
fs.mkdirSync(exampleDir, { recursive: true });
|
|
|
|
// Save prompt
|
|
fs.writeFileSync(path.join(exampleDir, 'prompt.txt'), result.prompt, 'utf-8');
|
|
|
|
// Save workflow if available
|
|
if (result.workflow) {
|
|
const workflowForExport = formatWorkflowForExport(result.workflow);
|
|
fs.writeFileSync(
|
|
path.join(exampleDir, 'workflow.json'),
|
|
JSON.stringify(workflowForExport, null, 2),
|
|
'utf-8',
|
|
);
|
|
}
|
|
|
|
// Save generated code if available (e.g., TypeScript SDK code from coding agent)
|
|
if (result.generatedCode) {
|
|
fs.writeFileSync(path.join(exampleDir, 'code.ts'), result.generatedCode, 'utf-8');
|
|
}
|
|
|
|
// Save feedback
|
|
const feedbackOutput = formatFeedbackForExport(result);
|
|
fs.writeFileSync(
|
|
path.join(exampleDir, 'feedback.json'),
|
|
JSON.stringify(feedbackOutput, null, 2),
|
|
'utf-8',
|
|
);
|
|
|
|
// Save error if present
|
|
if (result.error) {
|
|
fs.writeFileSync(path.join(exampleDir, 'error.txt'), result.error, 'utf-8');
|
|
}
|
|
|
|
logger.verbose(`Saved example ${result.index} to ${exampleDir}`);
|
|
},
|
|
|
|
saveSummary(summary: RunSummary, results: ExampleResult[]): void {
|
|
const summaryOutput = formatSummaryForExport(summary, results);
|
|
fs.writeFileSync(
|
|
path.join(outputDir, 'summary.json'),
|
|
JSON.stringify(summaryOutput, null, 2),
|
|
'utf-8',
|
|
);
|
|
|
|
logger.verbose(`Saved summary to ${path.join(outputDir, 'summary.json')}`);
|
|
},
|
|
};
|
|
}
|
|
|
|
function getExampleDirName(result: ExampleResult): string {
|
|
const index = String(result.index).padStart(3, '0');
|
|
const id = shortId(`${result.prompt}\n${result.index}`);
|
|
return `example-${index}-${id}`;
|
|
}
|
|
|
|
function shortId(input: string): string {
|
|
// Small deterministic id to avoid collisions when example folders are written concurrently
|
|
// and to keep folder names stable across reruns with the same prompts.
|
|
return createHash('md5').update(input).digest('hex').slice(0, 8);
|
|
}
|
|
|
|
/**
|
|
* Format a workflow for export (n8n-importable format).
|
|
*/
|
|
function formatWorkflowForExport(workflow: SimpleWorkflow): object {
|
|
return {
|
|
name: workflow.name ?? 'Generated Workflow',
|
|
nodes: workflow.nodes ?? [],
|
|
connections: workflow.connections ?? {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format feedback for export.
|
|
*/
|
|
function formatFeedbackForExport(result: ExampleResult): object {
|
|
// Group feedback by evaluator
|
|
const byEvaluator: Record<string, Feedback[]> = {};
|
|
for (const fb of result.feedback) {
|
|
const evaluator = fb.evaluator;
|
|
if (!byEvaluator[evaluator]) {
|
|
byEvaluator[evaluator] = [];
|
|
}
|
|
byEvaluator[evaluator].push(fb);
|
|
}
|
|
|
|
return {
|
|
index: result.index,
|
|
status: result.status,
|
|
durationMs: result.durationMs,
|
|
generationDurationMs: result.generationDurationMs,
|
|
evaluationDurationMs: result.evaluationDurationMs,
|
|
generationInputTokens: result.generationInputTokens,
|
|
generationOutputTokens: result.generationOutputTokens,
|
|
score: result.score,
|
|
// Include subgraph metrics if available
|
|
...(result.subgraphMetrics && {
|
|
subgraphMetrics: {
|
|
nodeCount: result.subgraphMetrics.nodeCount,
|
|
discoveryDurationMs: result.subgraphMetrics.discoveryDurationMs,
|
|
builderDurationMs: result.subgraphMetrics.builderDurationMs,
|
|
responderDurationMs: result.subgraphMetrics.responderDurationMs,
|
|
},
|
|
}),
|
|
evaluators: Object.entries(byEvaluator).map(([name, items]) => ({
|
|
name,
|
|
feedback: items.map((f) => ({
|
|
key: feedbackKey(f),
|
|
metric: f.metric,
|
|
score: f.score,
|
|
kind: f.kind,
|
|
...(f.comment ? { comment: f.comment } : {}),
|
|
})),
|
|
averageScore: calculateFiniteAverage(selectScoringItems(items)),
|
|
})),
|
|
allFeedback: result.feedback,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format summary for export.
|
|
*/
|
|
function formatSummaryForExport(summary: RunSummary, results: ExampleResult[]): object {
|
|
const resultsSorted = [...results].sort((a, b) => a.index - b.index);
|
|
|
|
// Calculate per-evaluator statistics
|
|
const evaluatorStats: Record<string, { scores: number[] }> = {};
|
|
for (const result of resultsSorted) {
|
|
const byEvaluator: Record<string, Feedback[]> = {};
|
|
for (const fb of result.feedback) {
|
|
const evaluator = fb.evaluator;
|
|
if (!byEvaluator[evaluator]) byEvaluator[evaluator] = [];
|
|
byEvaluator[evaluator].push(fb);
|
|
}
|
|
for (const [evaluator, items] of Object.entries(byEvaluator)) {
|
|
if (!evaluatorStats[evaluator]) {
|
|
evaluatorStats[evaluator] = { scores: [] };
|
|
}
|
|
const scoringItems = selectScoringItems(items);
|
|
const avg = calculateFiniteAverage(scoringItems);
|
|
evaluatorStats[evaluator].scores.push(avg);
|
|
}
|
|
}
|
|
|
|
const evaluatorAverages: Record<string, number> = {};
|
|
for (const [name, stats] of Object.entries(evaluatorStats)) {
|
|
evaluatorAverages[name] = stats.scores.reduce((a, b) => a + b, 0) / stats.scores.length;
|
|
}
|
|
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
totalExamples: summary.totalExamples,
|
|
passed: summary.passed,
|
|
failed: summary.failed,
|
|
errors: summary.errors,
|
|
passRate: summary.totalExamples > 0 ? summary.passed / summary.totalExamples : 0,
|
|
averageScore: summary.averageScore,
|
|
totalDurationMs: summary.totalDurationMs,
|
|
evaluatorAverages,
|
|
results: resultsSorted.map((r) => ({
|
|
index: r.index,
|
|
prompt: r.prompt.slice(0, 100) + (r.prompt.length > 100 ? '...' : ''),
|
|
status: r.status,
|
|
score: r.score,
|
|
durationMs: r.durationMs,
|
|
generationDurationMs: r.generationDurationMs,
|
|
generationInputTokens: r.generationInputTokens,
|
|
generationOutputTokens: r.generationOutputTokens,
|
|
...(r.subgraphMetrics && {
|
|
nodeCount: r.subgraphMetrics.nodeCount,
|
|
discoveryDurationMs: r.subgraphMetrics.discoveryDurationMs,
|
|
builderDurationMs: r.subgraphMetrics.builderDurationMs,
|
|
responderDurationMs: r.subgraphMetrics.responderDurationMs,
|
|
}),
|
|
...(r.error ? { error: r.error } : {}),
|
|
})),
|
|
};
|
|
}
|