first commit
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
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
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { writeFileSync } from 'fs';
|
||||
import pLimit from 'p-limit';
|
||||
import { join } from 'path';
|
||||
import pc from 'picocolors';
|
||||
|
||||
import { promptCategorizationChain } from '../src/chains/prompt-categorization';
|
||||
import { setupIntegrationLLM } from '../src/chains/test/integration/test-helpers';
|
||||
import { TechniqueDescription } from '../src/types/categorization';
|
||||
|
||||
// import { userPrompts } from '.prompts/100x3-prompts';
|
||||
|
||||
const userPrompts = [
|
||||
'Automate my business',
|
||||
'I want to build a workflow that generates a haiku from a wikipedia page.',
|
||||
];
|
||||
|
||||
interface CategorizationResult {
|
||||
index: number;
|
||||
prompt: string;
|
||||
promptPreview: string;
|
||||
techniques: string[];
|
||||
confidence: number;
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
async function categorizeAllPrompts() {
|
||||
// Get concurrency from environment or default to 10
|
||||
const DEFAULT_CONCURRENCY = 10;
|
||||
const parsedConcurrency = parseInt(process.env.CONCURRENCY ?? '', 10);
|
||||
const concurrency =
|
||||
!isNaN(parsedConcurrency) && parsedConcurrency >= 1 ? parsedConcurrency : DEFAULT_CONCURRENCY;
|
||||
|
||||
console.log(pc.blue(`\n🚀 Starting categorization of ${userPrompts.length} prompts...`));
|
||||
console.log(pc.dim(` Processing with concurrency=${concurrency}\n`));
|
||||
|
||||
// Setup LLM
|
||||
const llm = await setupIntegrationLLM();
|
||||
|
||||
const results: CategorizationResult[] = new Array(userPrompts.length).fill(
|
||||
null,
|
||||
) as CategorizationResult[];
|
||||
let completed = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Create concurrency limiter
|
||||
const limit = pLimit(concurrency);
|
||||
|
||||
// Process prompts in parallel with concurrency limit
|
||||
const processPrompt = async (prompt: string, i: number): Promise<void> => {
|
||||
const promptPreview = prompt.length > 80 ? prompt.substring(0, 80) + '...' : prompt;
|
||||
|
||||
try {
|
||||
const taskStartTime = Date.now();
|
||||
const result = await promptCategorizationChain(llm, prompt);
|
||||
const executionTime = Date.now() - taskStartTime;
|
||||
|
||||
results[i] = {
|
||||
index: i + 1,
|
||||
prompt,
|
||||
promptPreview,
|
||||
techniques: result.techniques,
|
||||
confidence: result.confidence ?? 0,
|
||||
executionTime,
|
||||
};
|
||||
|
||||
completed++;
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(
|
||||
pc.green(`✓ [${completed}/${userPrompts.length}]`) +
|
||||
pc.dim(` (${elapsed}s)`) +
|
||||
` ${promptPreview}\n Techniques: ${result.techniques.join(', ')}\n Confidence: ${((result.confidence ?? 0) * 100).toFixed(1)}%\n`,
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(pc.red(`✗ [${i + 1}/${userPrompts.length}] Error:`) + ` ${errorMessage}\n`);
|
||||
results[i] = {
|
||||
index: i + 1,
|
||||
prompt,
|
||||
promptPreview,
|
||||
techniques: [],
|
||||
confidence: 0,
|
||||
executionTime: 0,
|
||||
};
|
||||
completed++;
|
||||
}
|
||||
};
|
||||
|
||||
const promises = userPrompts.map(async (prompt, i) => {
|
||||
return await limit(async () => await processPrompt(prompt, i));
|
||||
});
|
||||
|
||||
// Wait for all promises to complete
|
||||
await Promise.all(promises);
|
||||
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
|
||||
// Calculate statistics
|
||||
const techniqueFrequency = new Map<string, number>();
|
||||
for (const result of results) {
|
||||
for (const technique of result.techniques) {
|
||||
techniqueFrequency.set(technique, (techniqueFrequency.get(technique) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const sortedFrequency = Array.from(techniqueFrequency.entries()).sort((a, b) => b[1] - a[1]);
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
|
||||
const outputDir = join(__dirname);
|
||||
|
||||
// Type guard to check if a string is a valid technique key
|
||||
const isValidTechnique = (technique: string): technique is keyof typeof TechniqueDescription => {
|
||||
return technique in TechniqueDescription;
|
||||
};
|
||||
|
||||
// Save technique frequency summary
|
||||
const summaryPath = join(outputDir, `categorization-summary-${timestamp}.md`);
|
||||
const summaryLines = [
|
||||
'# Prompt Categorization Summary',
|
||||
'',
|
||||
`**Date:** ${new Date().toISOString()}`,
|
||||
`**Total Prompts:** ${results.length}`,
|
||||
`**Total Time:** ${totalTime}s`,
|
||||
`**Concurrency:** ${concurrency} parallel executions`,
|
||||
`**Average Confidence:** ${((results.reduce((sum, r) => sum + r.confidence, 0) / results.length) * 100).toFixed(1)}%`,
|
||||
`**Average Execution Time:** ${(results.reduce((sum, r) => sum + r.executionTime, 0) / results.length).toFixed(0)}ms`,
|
||||
'',
|
||||
'## Technique Frequency',
|
||||
'',
|
||||
'| Rank | Technique | Used in | Description |',
|
||||
'|------|-----------|---------|-------------|',
|
||||
...sortedFrequency.map(([technique, count], index) => {
|
||||
const description = isValidTechnique(technique)
|
||||
? TechniqueDescription[technique]
|
||||
: 'Unknown technique';
|
||||
return `| ${index + 1} | \`${technique}\` | ${count} | ${description} |`;
|
||||
}),
|
||||
'',
|
||||
'## Detailed Results',
|
||||
'',
|
||||
...results.map((r) => {
|
||||
const preview =
|
||||
r.promptPreview.length > 100 ? r.promptPreview.substring(0, 100) + '...' : r.promptPreview;
|
||||
return [
|
||||
`### ${r.index}. ${preview}`,
|
||||
'',
|
||||
`**Techniques:** ${r.techniques.map((t) => `\`${t}\``).join(', ')}`,
|
||||
`**Confidence:** ${(r.confidence * 100).toFixed(1)}%`,
|
||||
`**Execution Time:** ${r.executionTime}ms`,
|
||||
'',
|
||||
].join('\n');
|
||||
}),
|
||||
];
|
||||
writeFileSync(summaryPath, summaryLines.join('\n'));
|
||||
|
||||
console.log(pc.green('\n✓ Categorization complete!\n'));
|
||||
console.log(`Results saved to: ${pc.dim(summaryPath)}\n`);
|
||||
}
|
||||
|
||||
// Run the script
|
||||
categorizeAllPrompts().catch((error) => {
|
||||
console.error(pc.red('\n✗ Error:'), error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import { basename, dirname, join } from 'path';
|
||||
import pc from 'picocolors';
|
||||
|
||||
import { mermaidStringify, type MermaidOptions } from '@/tools/utils/mermaid.utils';
|
||||
import type { WorkflowMetadata } from '@/types';
|
||||
import type { SimpleWorkflow } from '@/types/workflow';
|
||||
|
||||
// exported workflows (unlike templates) don't have a template ID - but the script doesn't need them to have it
|
||||
const templateId = 0;
|
||||
|
||||
/**
|
||||
* Type guard to check if value is a direct workflow format (nodes and connections at root)
|
||||
*/
|
||||
function isDirectWorkflowFormat(value: unknown): value is SimpleWorkflow & { name?: string } {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'nodes' in value &&
|
||||
'connections' in value &&
|
||||
Array.isArray((value as Record<string, unknown>).nodes)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if value is a WorkflowMetadata format (nested workflow object)
|
||||
*/
|
||||
function isWorkflowMetadataFormat(
|
||||
value: unknown,
|
||||
): value is { workflow: SimpleWorkflow; name?: string } {
|
||||
if (typeof value !== 'object' || value === null || !('workflow' in value)) {
|
||||
return false;
|
||||
}
|
||||
const workflow = (value as Record<string, unknown>).workflow;
|
||||
return isDirectWorkflowFormat(workflow);
|
||||
}
|
||||
|
||||
interface CliOptions {
|
||||
inputFile: string;
|
||||
outputFile?: string;
|
||||
includeNodeName: boolean;
|
||||
includeNodeType: boolean;
|
||||
includeNodeParameters: boolean;
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
console.log(`
|
||||
${pc.bold('Usage:')} workflow-to-mermaid <workflow.json> [options]
|
||||
|
||||
${pc.bold('Description:')}
|
||||
Converts a n8n workflow JSON file to a Mermaid flowchart diagram.
|
||||
By default, outputs to a markdown file with the same name in the same directory.
|
||||
|
||||
${pc.bold('Options:')}
|
||||
-o, --output <file> Output file path (default: same name as input with .md extension)
|
||||
--no-node-name Exclude node names from diagram
|
||||
--no-node-type Exclude node types from diagram comments
|
||||
--node-params Include node parameters in diagram comments
|
||||
-h, --help Show this help message
|
||||
|
||||
${pc.bold('Examples:')}
|
||||
workflow-to-mermaid my-workflow.json
|
||||
workflow-to-mermaid my-workflow.json -o output.md
|
||||
workflow-to-mermaid my-workflow.json --no-node-type --node-params
|
||||
`);
|
||||
}
|
||||
|
||||
interface ParseResult {
|
||||
options?: CliOptions;
|
||||
exitCode?: number;
|
||||
}
|
||||
|
||||
function parseArgs(args: string[]): ParseResult {
|
||||
const cliArgs = args.slice(2);
|
||||
|
||||
if (cliArgs.length === 0 || cliArgs.includes('-h') || cliArgs.includes('--help')) {
|
||||
printUsage();
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
|
||||
const options: CliOptions = {
|
||||
inputFile: '',
|
||||
includeNodeName: true,
|
||||
includeNodeType: true,
|
||||
includeNodeParameters: false,
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
while (i < cliArgs.length) {
|
||||
const arg = cliArgs[i];
|
||||
|
||||
if (arg === '-o' || arg === '--output') {
|
||||
i++;
|
||||
if (i >= cliArgs.length) {
|
||||
console.error(pc.red('Error: --output requires a file path'));
|
||||
return { exitCode: 1 };
|
||||
}
|
||||
options.outputFile = cliArgs[i];
|
||||
} else if (arg === '--no-node-name') {
|
||||
options.includeNodeName = false;
|
||||
} else if (arg === '--no-node-type') {
|
||||
options.includeNodeType = false;
|
||||
} else if (arg === '--node-params') {
|
||||
options.includeNodeParameters = true;
|
||||
} else if (arg.startsWith('-')) {
|
||||
console.error(pc.red(`Error: Unknown option: ${arg}`));
|
||||
printUsage();
|
||||
return { exitCode: 1 };
|
||||
} else if (!options.inputFile) {
|
||||
options.inputFile = arg;
|
||||
} else {
|
||||
console.error(pc.red(`Error: Unexpected argument: ${arg}`));
|
||||
printUsage();
|
||||
return { exitCode: 1 };
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!options.inputFile) {
|
||||
console.error(pc.red('Error: Input file is required'));
|
||||
printUsage();
|
||||
return { exitCode: 1 };
|
||||
}
|
||||
|
||||
return { options };
|
||||
}
|
||||
|
||||
function loadWorkflow(filePath: string): WorkflowMetadata {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const json: unknown = jsonParse(content);
|
||||
|
||||
// Handle both formats:
|
||||
// 1. Direct workflow format: { nodes: [...], connections: {...}, name?: string }
|
||||
// 2. WorkflowMetadata format: { workflow: { nodes: [...], connections: {...} }, name?: string }
|
||||
if (isDirectWorkflowFormat(json)) {
|
||||
const name = json.name ?? basename(filePath, '.json');
|
||||
return {
|
||||
templateId,
|
||||
name,
|
||||
workflow: {
|
||||
name,
|
||||
nodes: json.nodes,
|
||||
connections: json.connections,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isWorkflowMetadataFormat(json)) {
|
||||
const workflowName = json.workflow.name ?? basename(filePath, '.json');
|
||||
return {
|
||||
templateId,
|
||||
name: json.name ?? workflowName,
|
||||
workflow: {
|
||||
name: workflowName,
|
||||
nodes: json.workflow.nodes,
|
||||
connections: json.workflow.connections,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Invalid workflow format: expected nodes and connections at root or under workflow key',
|
||||
);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const result = parseArgs(process.argv);
|
||||
if (result.exitCode !== undefined) {
|
||||
process.exit(result.exitCode);
|
||||
}
|
||||
|
||||
const options = result.options!;
|
||||
|
||||
try {
|
||||
console.log(pc.blue(`\nLoading workflow from: ${options.inputFile}`));
|
||||
|
||||
const workflow = loadWorkflow(options.inputFile);
|
||||
|
||||
const mermaidOptions: MermaidOptions = {
|
||||
includeNodeName: options.includeNodeName,
|
||||
includeNodeType: options.includeNodeType,
|
||||
includeNodeParameters: options.includeNodeParameters,
|
||||
collectNodeConfigurations: false,
|
||||
};
|
||||
|
||||
console.log(
|
||||
pc.dim(
|
||||
` Options: name=${mermaidOptions.includeNodeName}, type=${mermaidOptions.includeNodeType}, params=${mermaidOptions.includeNodeParameters}`,
|
||||
),
|
||||
);
|
||||
|
||||
const mermaid = mermaidStringify(workflow, mermaidOptions);
|
||||
|
||||
// Determine output file path
|
||||
const outputFile =
|
||||
options.outputFile ??
|
||||
join(dirname(options.inputFile), basename(options.inputFile, '.json') + '.md');
|
||||
|
||||
// Write markdown file with mermaid content
|
||||
const markdownContent = `# ${workflow.name}\n\n${mermaid}\n`;
|
||||
writeFileSync(outputFile, markdownContent);
|
||||
|
||||
const nodeCount = workflow.workflow.nodes.filter(
|
||||
(n) => n.type !== 'n8n-nodes-base.stickyNote',
|
||||
).length;
|
||||
|
||||
console.log(pc.green('\n✓ Successfully converted workflow to Mermaid!'));
|
||||
console.log(pc.dim(` Nodes: ${nodeCount}`));
|
||||
console.log(pc.dim(` Output: ${outputFile}\n`));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(pc.red(`\n✗ Error: ${message}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user