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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,97 @@
import { z } from 'zod';
import type { SimpleWorkflow } from '../../../src/types/workflow';
// Violation schema
const violationSchema = z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
});
// Category score schema
const categoryScoreSchema = z.object({
violations: z.array(violationSchema),
score: z.number().min(0).max(1),
});
// Structural similarity schema (with applicable flag)
const structuralSimilaritySchema = z.object({
violations: z.array(violationSchema),
score: z.number().min(0).max(1),
applicable: z
.boolean()
.describe('Whether this category was evaluated (based on reference workflow availability)'),
});
const efficiencyScoreSchema = categoryScoreSchema.extend({
redundancyScore: z.number().min(0).max(1).describe('Score for avoiding redundant operations'),
pathOptimization: z.number().min(0).max(1).describe('Score for optimal execution paths'),
nodeCountEfficiency: z.number().min(0).max(1).describe('Score for using minimal nodes'),
});
const maintainabilityScoreSchema = categoryScoreSchema.extend({
nodeNamingQuality: z.number().min(0).max(1).describe('Score for descriptive node naming'),
workflowOrganization: z.number().min(0).max(1).describe('Score for logical workflow structure'),
modularity: z.number().min(0).max(1).describe('Score for reusable and modular components'),
});
const bestPracticesScoreSchema = categoryScoreSchema.extend({
techniques: z
.array(z.string())
.describe(
'Workflow techniques identified for this evaluation (e.g., chatbot, content-generation)',
)
.optional(),
});
// Main evaluation result schema
export const evaluationResultSchema = z.object({
overallScore: z
.number()
.min(0)
.max(1)
.describe('Weighted average score across all categories (0-1)'),
functionality: categoryScoreSchema,
connections: categoryScoreSchema,
expressions: categoryScoreSchema,
nodeConfiguration: categoryScoreSchema,
structuralSimilarity: structuralSimilaritySchema,
efficiency: efficiencyScoreSchema,
dataFlow: categoryScoreSchema,
maintainability: maintainabilityScoreSchema,
bestPractices: bestPracticesScoreSchema,
summary: z.string().describe('2-3 sentences summarizing main strengths and weaknesses'),
criticalIssues: z
.array(z.string())
.describe('List of issues that would prevent the workflow from functioning')
.optional(),
});
// Type exports
export type Violation = z.infer<typeof violationSchema>;
export type CategoryScore = z.infer<typeof categoryScoreSchema>;
export type EfficiencyScore = z.infer<typeof efficiencyScoreSchema>;
export type MaintainabilityScore = z.infer<typeof maintainabilityScoreSchema>;
export type BestPracticesScore = z.infer<typeof bestPracticesScoreSchema>;
export type EvaluationResult = z.infer<typeof evaluationResultSchema>;
// Test case schema for evaluation
export const testCaseSchema = z.object({
id: z.string(),
name: z.string(),
prompt: z.string(),
referenceWorkflows: z.array(z.custom<SimpleWorkflow>()).optional(),
});
export type TestCase = z.infer<typeof testCaseSchema>;
// Evaluation input schema
export const evaluationInputSchema = z.object({
userPrompt: z.string(),
generatedWorkflow: z.custom<SimpleWorkflow>(),
referenceWorkflows: z.array(z.custom<SimpleWorkflow>()).optional(),
preset: z.enum(['strict', 'standard', 'lenient']).optional(),
});
export type EvaluationInput = z.infer<typeof evaluationInputSchema>;
@@ -0,0 +1,57 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { SystemMessage } from '@langchain/core/messages';
import { ChatPromptTemplate, HumanMessagePromptTemplate } from '@langchain/core/prompts';
import type { Runnable, RunnableConfig } from '@langchain/core/runnables';
import { RunnableSequence } from '@langchain/core/runnables';
import { OperationalError } from 'n8n-workflow';
import type { z } from 'zod';
import type { EvaluationInput } from '../evaluation';
type EvaluatorChainInput = {
userPrompt: string;
generatedWorkflow: string;
referenceSection: string;
};
export function createEvaluatorChain<TResult extends Record<string, unknown>>(
llm: BaseChatModel,
schema: z.ZodType<TResult>,
systemPrompt: string,
humanTemplate: string,
): RunnableSequence<EvaluatorChainInput, TResult> {
if (!llm.bindTools) {
throw new OperationalError("LLM doesn't support binding tools");
}
const prompt = ChatPromptTemplate.fromMessages([
new SystemMessage(systemPrompt),
HumanMessagePromptTemplate.fromTemplate(humanTemplate),
]);
const llmWithStructuredOutput = llm.withStructuredOutput<TResult>(schema);
return RunnableSequence.from<EvaluatorChainInput, TResult>([prompt, llmWithStructuredOutput]);
}
export async function invokeEvaluatorChain<TResult>(
chain: Runnable<EvaluatorChainInput, TResult>,
input: EvaluationInput,
config?: RunnableConfig,
): Promise<TResult> {
const referenceSection =
input.referenceWorkflows && input.referenceWorkflows.length > 0
? `<reference_workflows>\n${JSON.stringify(input.referenceWorkflows, null, 2)}\n</reference_workflows>`
: '';
const result = await chain.invoke(
{
userPrompt: input.userPrompt,
generatedWorkflow: JSON.stringify(input.generatedWorkflow, null, 2),
referenceSection,
},
config,
);
return result;
}
@@ -0,0 +1,280 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { promptCategorizationChain } from '@/chains/prompt-categorization';
import { documentation } from '@/tools/best-practices';
import { createEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for best practices evaluation result
const bestPracticesResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
techniques: z
.array(z.string())
.describe(
'Workflow techniques identified for this evaluation (e.g., chatbot, content-generation)',
),
});
export type BestPracticesResult = z.infer<typeof bestPracticesResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing specifically on BEST PRACTICES ADHERENCE.
Your task is to evaluate whether a generated workflow follows the documented best practices for its workflow type(s).
## Your Role
Evaluate ONLY adherence to the provided best practices documentation. Focus on whether the workflow follows recommended patterns, avoids common pitfalls, and uses nodes correctly.
## Context-Aware Evaluation Philosophy
**CRITICAL**: Always consider what the user actually requested in their prompt. Do not penalize workflows for missing features or safeguards that were not part of the user's requirements.
- If the user asked for a simple workflow without mentioning production readiness, error handling, or rate limiting, these should NOT be critical violations
- Only mark something as critical if it would prevent the workflow from fulfilling the user's specific request
- Consider the scope and complexity implied by the user's prompt
## Evaluation Criteria
### Understanding Workflow Connections
n8n workflows can have multiple triggers and execution paths. When evaluating whether components are "connected," understand that n8n supports multiple connection methods beyond direct node-to-node data flow.
Valid Connection Methods:
1. **Direct Data Flow Connections**: Traditional node-to-node connections where data flows from source output to target input
- Example: HTTP Request → Set → Database
2. **AI-Specific Connections**: Special connection types for AI nodes, denoted with brackets like [ai_memory], [ai_tool], [ai_embedding]
- Memory nodes connected to multiple agents via [ai_memory] - enables agents to share conversation history
- Tools connected to agents via [ai_tool] - provides capabilities to agents
- Vector stores use [ai_embedding] and [ai_document] - for AI-powered data retrieval
**CRITICAL: AI sub-nodes are the SOURCE of ai_* connections, NOT the target.**
- Document Loader connects TO Vector Store (Document Loader is source, Vector Store is target)
- Embeddings connects TO Vector Store (Embeddings is source, Vector Store is target)
- Chat Model connects TO AI Agent (Chat Model is source, AI Agent is target)
In the connections JSON, this appears as:
\`\`\`json
"Document Loader": { "ai_document": [[{ "node": "Vector Store", ... }]] }
\`\`\`
This means the connection EXISTS - Document Loader provides ai_document capability TO Vector Store.
**NEVER flag "Vector Store missing Document Loader" if Document Loader has ai_document → Vector Store.**
3. **Shared Memory**: Multiple agents/workflows sharing the same memory node for context/data persistence
- Same Window Buffer Memory connected to both a scheduled agent AND a chat agent
- Both agents can access shared conversation history and context
4. **Vector Store Sharing**: Multiple workflows accessing the same vector store for data storage/retrieval
- Scheduled workflow writes documents to Vector Store
- Chat workflow queries the same Vector Store for information
- Both workflows effectively share data through the vector store
5. **Data Storage Sharing**: Multiple workflows reading/writing to the same persistent storage
- Database nodes (PostgreSQL, MongoDB, MySQL)
- Spreadsheet services (Google Sheets, Airtable)
- Data Tables (n8n's built-in storage)
- One workflow writes data, another workflow reads it
6. **Tool-based Connections**: Agents connected through tool nodes
- Agent Tool nodes allow one agent to invoke another agent
- Tools provide indirect connections between workflow components
7. **Loop Patterns (Split In Batches)**: Intentional cycles for batch processing
- Output 0 ("loop"): Fires for EACH batch - connect batch processing here
- Output 1 ("done"): Fires once after ALL iterations complete - connect final processing here
- Processing nodes loop BACK to Split In Batches input to continue the loop
- This circular connection is CORRECT and INTENTIONAL - it creates the batch processing loop
**NEVER flag as incorrect if:**
- Output 1 connects to processing nodes
- Processing nodes connect back to Split In Batches input (index 0)
- Output 0 connects to aggregation/final step
This is the standard n8n pattern for processing large datasets in batches.
8. **Shared Destination Pattern**: Multiple branches connecting to same node
- Multiple Switch/IF outputs can ALL connect to the same downstream node
- This is correct when all branches need the same final processing (e.g., save to database)
- Do NOT use Merge for this - Merge waits for all inputs, but only one branch executes per item
9. **Chat Trigger Auto-Response**: Chat Trigger handles responses automatically
- Chat Trigger (@n8n/n8n-nodes-langchain.chatTrigger) is BIDIRECTIONAL
- AI Agent output is automatically sent back to the chat interface
- There is NO main connection back to Chat Trigger - this is correct behavior
- **NEVER flag "AI Agent has no connection back to Chat Trigger"** - responses are built-in
10. **Document Loader Input Pattern**: Document Loaders read from context, not main connections
- Document Loaders have NO main input connections by design
- They read binary data/URLs from workflow context based on their configuration
- They OUTPUT via ai_document to Vector Store or other consumers
- **NEVER flag "Document Loader has no main input"** or "Trigger not connected to Document Loader"
Before assessing there is a missing connection as per best practices documentation (for example a chatbot
should be connected to data from other triggered components of the workflow) make sure that there is no
possible connection, check all possible connections, ESPECIALLY agent nodes (memory and tools could
create the necessary connections).
Critical Evaluation Rule:
Before marking components as "disconnected," verify they have NO connection method - not just no direct data flow connection.
### Evaluating Configuration and Fields
If a best practice states that certain configuration should be applied, for example disabling n8n attribution
check to see if that has been specified as part of the generated workflows configuration or its additional fields.
If a node of the correct type has these settings present, then it is likely NOT in violation of the practice.
## Violation Criteria
**Major (-20 to -40 points):**
- Not following recommended approaches that significantly impact reliability or performance FOR THE REQUESTED USE CASE
- Using non-recommended nodes when better alternatives are documented and relevant
- Missing important safeguards that the documentation warns about IF they're relevant to the user's request
- Ignoring service-specific considerations that would impact the user's stated goals
**Minor (-5 to -20 points):**
- Using less optimal patterns that are documented as pitfalls but don't break functionality
- Missing optional best practices that would improve the workflow (like error handling when not requested)
- Missing production-ready features when the user asked for a basic/simple workflow
- Small deviations from recommended approaches that don't impact the user's goals
- Missing rate limiting, memory management, or advanced error handling when not requested
## Scoring Instructions
1. Start with 100 points
2. Read the user prompt carefully to understand what they actually requested
3. Deduct points for each violation found based on severity AND relevance to the user's request
4. Score cannot go below 0
5. Convert to 0-1 scale by dividing by 100
## Important Context
- You will be provided with best practices documentation relevant to the workflow type(s)
- Focus on whether the workflow follows the documented recommendations RELEVANT to the user's request
- Consider the specific nodes used and their documented pitfalls
- Evaluate against common mistakes mentioned in the documentation
- DO NOT penalize for missing best practices that aren't relevant to what the user asked for
- DO NOT create arbitrary best practices - only evaluate against what's documented
- DO NOT mark optional features as critical violations when they weren't requested
`;
const humanTemplate = `Evaluate how well this workflow follows n8n best practices in the context of what the user requested.
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
<best_practices_documentation>
{bestPractices}
</best_practices_documentation>
{referenceSection}
IMPORTANT: First, analyze what the user actually requested in their prompt. Then evaluate the workflow against best practices that are relevant to that request.
- If the user requested a simple/basic workflow, do NOT mark missing error handling or rate limiting as critical
- Only mark violations as critical if they would prevent the core requested functionality from working
- Consider whether advanced features (error handling, rate limiting, memory management) were part of the user's requirements
Provide a best practices evaluation with score, violations (citing specific best practices and explaining why they matter for THIS use case), and brief analysis.`;
export function createBestPracticesEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, bestPracticesResultSchema, systemPrompt, humanTemplate);
}
/**
* Load relevant best practices documentation for the given user prompt
* Returns both the documentation string and the identified techniques
*/
async function loadRelevantBestPractices(
llm: BaseChatModel,
userPrompt: string,
): Promise<{ documentation: string; techniques: string[] }> {
try {
// Categorize the prompt to determine which techniques apply
const categorization = await promptCategorizationChain(llm, userPrompt);
// Load best practices for identified techniques
const relevantDocs: string[] = [];
for (const technique of categorization.techniques) {
const bestPractice = documentation[technique];
if (bestPractice) {
relevantDocs.push(
`## Best Practices for ${technique}\n\n${bestPractice.getDocumentation()}`,
);
}
}
if (relevantDocs.length === 0) {
return {
documentation:
'No specific best practices documentation available for this workflow type. Evaluate based on general n8n workflow principles.',
techniques: categorization.techniques,
};
}
return {
documentation: relevantDocs.join('\n\n---\n\n'),
techniques: categorization.techniques,
};
} catch (error) {
// If categorization fails, return a message indicating no specific best practices
return {
documentation:
'Unable to load specific best practices. Evaluate based on general n8n workflow principles.',
techniques: [],
};
}
}
type BestPracticesChainInput = {
userPrompt: string;
generatedWorkflow: string;
bestPractices: string;
referenceSection: string;
};
export async function evaluateBestPractices(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<BestPracticesResult> {
// Load relevant best practices documentation and identify techniques
const { documentation: bestPracticesDoc, techniques } = await loadRelevantBestPractices(
llm,
input.userPrompt,
);
// Prepare the reference section
const referenceSection =
input.referenceWorkflows && input.referenceWorkflows.length > 0
? `<reference_workflows>\n${JSON.stringify(input.referenceWorkflows, null, 2)}\n</reference_workflows>`
: '';
// Invoke the evaluator chain with best practices
const chain = createBestPracticesEvaluatorChain(llm);
const chainInput: BestPracticesChainInput = {
userPrompt: input.userPrompt,
generatedWorkflow: JSON.stringify(input.generatedWorkflow, null, 2),
bestPractices: bestPracticesDoc,
referenceSection,
};
const result = await chain.invoke(chainInput);
// Add the identified techniques to the result
return {
...result,
techniques,
};
}
@@ -0,0 +1,222 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { createEvaluatorChain, invokeEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for connections evaluation result
const connectionsResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
analysis: z.string().describe('Brief analysis of node connections and data flow'),
});
export type ConnectionsResult = z.infer<typeof connectionsResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing on node connections and data flow. Verify that connections follow n8n's sourcing rules, support the requested behaviour, and respect AI capability patterns.
<reading_n8n_connection_json>
The workflow JSON structure uses the outer key as the SOURCE node. This is critical for correct analysis.
Structure:
"connections": {
"SOURCE_NODE": {
"connection_type": [[{ "node": "TARGET_NODE", "type": "connection_type", "index": 0 }]]
}
}
Reading this JSON: SOURCE_NODE outputs to TARGET_NODE via connection_type.
Example:
"Form Trigger": {
"main": [[{ "node": "Set Node", "type": "main", "index": 0 }]]
}
This means Form Trigger connects TO Set Node (Form Trigger is source, Set Node is target). Data flows from Form Trigger to Set Node.
For AI capability connections:
"Text Splitter": {
"ai_textSplitter": [[{ "node": "Document Loader", "type": "ai_textSplitter", "index": 0 }]]
}
This means Text Splitter provides its capability TO Document Loader. Text Splitter is the source, Document Loader is the target. This is the correct direction for ai_* connections because sub-nodes provide capabilities to parent nodes.
</reading_n8n_connection_json>
<connection_model>
Main connections (type: main) carry runtime data between workflow nodes. They flow from data producers to data consumers, forming the primary execution path from trigger through processing to outputs.
AI capability connections (types: ai_languageModel, ai_memory, ai_tool, ai_document, ai_embedding, ai_textSplitter, ai_outputParser) let sub-nodes provide capabilities to parent nodes. The sub-node is always the source. For example, a Chat Model provides ai_languageModel capability to an AI Agent, so the connection goes Chat Model to AI Agent.
Capability-only nodes like Document Loader, Text Splitter, Embeddings, LLMs, Output Parsers, and Tool nodes exist purely to provide ai_* capabilities. They have no main inputs or outputs by design. Document Loader appearing without main connections is correct architecture, not a problem.
Hybrid nodes like Vector Store and AI Agent participate in both main data flow and ai_* capability networks simultaneously. A Vector Store in insert mode receives main data and also receives ai_document from Document Loader and ai_embedding from Embeddings.
</connection_model>
<rag_pipeline_architecture>
In RAG workflows, data flows through main connections while capabilities flow through ai_* connections:
Data Source connects to Vector Store via main (triggers the insert operation).
Document Loader connects to Vector Store via ai_document (provides document processing).
Text Splitter connects to Document Loader via ai_textSplitter (provides chunking).
Embeddings connects to Vector Store via ai_embedding (provides vectorization).
The Document Loader reads from workflow context based on its configuration. It does not receive data through main connections. This is intentional design.
Valid connection directions for RAG:
Text Splitter to Document Loader via ai_textSplitter
Document Loader to Vector Store via ai_document
Embeddings to Vector Store via ai_embedding
Language Model to AI Agent via ai_languageModel
Tool nodes to AI Agent via ai_tool
Memory nodes to AI Agent via ai_memory
</rag_pipeline_architecture>
<loop_and_multi_output_patterns>
## Split In Batches (Loop Node)
Split In Batches has TWO outputs with specific semantics:
- Output 0 ("done"): Fires ONCE after ALL batches complete. Connect aggregation/final processing here.
- Output 1 ("loop"): Fires for EACH batch. Connect processing nodes here.
Correct loop pattern:
1. Split In Batches output 1 → Processing Node(s) → Split In Batches INPUT (index 0)
2. Split In Batches output 0 → Next workflow step
CRITICAL: The loop-back connection goes to the node's INPUT (index 0), creating a cycle. This is CORRECT behavior.
Do NOT flag "Split In Batches connects to itself" or "circular connection" as an error - this IS the loop pattern.
Example correct connections JSON:
"Split In Batches": {
"main": [
[{ "node": "Aggregate", "type": "main", "index": 0 }], // Output 0 (done) → final step
[{ "node": "HTTP Request", "type": "main", "index": 0 }] // Output 1 (loop) → processing
]
},
"HTTP Request": {
"main": [
[{ "node": "Split In Batches", "type": "main", "index": 0 }] // Loop back to INPUT - CORRECT
]
}
## Switch and IF Nodes (Multi-Output)
Switch and IF nodes route data to different outputs:
- IF: Output 0 = true branch, Output 1 = false branch
- Switch: Outputs 0 to N-1 = case branches
SHARED DESTINATION PATTERN:
Multiple outputs can ALL connect to the same downstream node. This is valid when different branches need the same final processing:
Switch output 0 → Database
Switch output 1 → Database
Switch output 2 → Database
Do NOT flag multiple connections to the same target as redundant - it's the correct pattern for routing different cases to a shared destination without using Merge (which would wait forever since only one branch executes per item).
</loop_and_multi_output_patterns>
<chat_trigger_patterns>
## Chat Trigger and Chat Interface Nodes
Chat Trigger (@n8n/n8n-nodes-langchain.chatTrigger) is a BIDIRECTIONAL node that handles both input AND output automatically.
CRITICAL: Chat Trigger does NOT need a return connection from downstream nodes.
- Chat Trigger receives user messages and starts the workflow
- AI Agent or other nodes process the message
- The response is automatically sent back through Chat Trigger's built-in response mechanism
- There is NO "main" connection back to Chat Trigger - this is correct behavior
Valid chat workflow pattern:
Chat Trigger → AI Agent (with Chat Model via ai_languageModel)
The AI Agent's output is automatically routed back to the chat interface. Do NOT flag "AI Agent has no connection back to Chat Trigger" as an error.
## Node Positioning
Node positions (x, y coordinates) in the workflow JSON are for VISUAL LAYOUT ONLY.
- Position does NOT affect execution order
- Execution order is determined by connections, not positions
- A trigger at position [250, 450] executes before a node at [250, 300] if connected that way
- Do NOT flag node positioning as a connection or execution flow issue
</chat_trigger_patterns>
<document_loader_patterns>
## Document Loader Connection Rules
Document Loader nodes (@n8n/n8n-nodes-langchain.documentLoader*) are CAPABILITY-ONLY nodes.
CRITICAL rules for Document Loaders:
1. Document Loaders have NO main input connections - this is correct by design
2. Document Loaders provide ai_document capability to Vector Store or other consumers
3. Document Loaders read data from workflow context (binary data, URLs) based on their configuration
4. The data source is configured in the Document Loader's parameters, NOT passed via main connection
Valid pattern:
Form Trigger → Vector Store (main connection for triggering insert)
Document Loader → Vector Store (ai_document capability)
The Form Trigger does NOT connect to Document Loader. The Document Loader reads the binary data from workflow context automatically.
Do NOT flag these as errors:
- "Document Loader has no main input connection" - correct, it uses ai_document output only
- "Missing connection from Trigger to Document Loader" - incorrect expectation
- "Document Loader is disconnected" - check for ai_document connection instead
</document_loader_patterns>
<validation_process>
Work through these steps in your analysis:
1. Parse all connections from the JSON. For each entry, identify the source node (the JSON key) and the target node (the "node" field inside). Write each as: Source to Target via connection_type.
2. Identify capability-only nodes (Document Loader, Text Splitter, Embeddings, LLMs, Output Parsers, Tools, Memory). These nodes correctly have no main connections.
3. Verify the main execution path flows from trigger through processing nodes. Each non-capability node that processes data should have appropriate main connections.
4. Verify ai_* connections point from sub-nodes to parent nodes. The sub-node providing the capability should be the source (the JSON key).
5. For hybrid nodes, confirm they have both their required main connections and ai_* capability connections based on their mode.
</validation_process>
<scoring>
Start with 100 points and deduct for violations:
Critical violations (40-50 points): Breaks in main execution path where trigger or data source has no downstream connection. Missing mandatory main inputs for data processing nodes.
Major violations (15-25 points): Wrong connection type used. Hybrid nodes missing required connections for their configured mode. Data dependencies out of order.
Minor violations (5-10 points): Branches that should merge but remain isolated. Unused conditional branches without clear termination.
IMPORTANT - These are NOT violations:
- Capability-only nodes without main connections (correct design)
- Split In Batches loop-back connections (correct loop pattern)
- Multiple Switch/IF outputs connecting to the same destination (shared destination pattern)
- Chat Trigger with no return connection from AI Agent (auto-response is built-in)
- Document Loader with no main input (reads from workflow context, outputs via ai_document)
- Node positions not matching visual execution flow (positions are layout only)
Convert final score to 0-1 scale by dividing by 100.
</scoring>`;
const humanTemplate = `Evaluate the connections and data flow of this workflow:
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
{referenceSection}
Conduct your analysis in <analysis> tags, systematically parsing the connection JSON (remember: the JSON key is the SOURCE node, the "node" field is the TARGET). Then provide your evaluation with score, violations array, and brief analysis.`;
export function createConnectionsEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, connectionsResultSchema, systemPrompt, humanTemplate);
}
export async function evaluateConnections(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<ConnectionsResult> {
return await invokeEvaluatorChain(createConnectionsEvaluatorChain(llm), input);
}
@@ -0,0 +1,172 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { createEvaluatorChain, invokeEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for data flow evaluation result
const dataFlowResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
analysis: z.string().describe('Brief analysis of data flow and transformations'),
});
export type DataFlowResult = z.infer<typeof dataFlowResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing specifically on DATA FLOW and TRANSFORMATION ACCURACY.
Your task is to evaluate how accurately data is transformed and passed through the workflow.
## CRITICAL: Understanding n8n Data Flow Patterns
- **AI agents with tools handle data internally** - not visible in main flow
- **Vector stores are referenced by ID**, not direct connections
- **Memory nodes connect via ai_memory**, not main connections
- **Document loaders may process data via AI connections**
- **Focus on actual data corruption/loss, not architectural patterns**
## Data Transformation Accuracy (0-1)
### Evaluation Criteria:
**Score 1.0 - Perfect Transformations:**
- All data transformations preserve data integrity
- Field mappings are correct and complete
- Data types are properly handled (strings, numbers, arrays, objects)
- No data loss during transformations
- Proper handling of nested data structures
**Score 0.75 - Good Transformations:**
- Most transformations are correct
- Minor field naming inconsistencies that don't affect functionality
- Slight inefficiencies in data handling
**Score 0.5 - Adequate Transformations:**
- Core data transformations work but with issues
- Some data might be lost or incorrectly mapped
- Type conversions might have problems
**Score 0.25 - Poor Transformations:**
- Significant data transformation errors
- Important fields missing or incorrectly mapped
- Data structure problems
**Score 0.0 - Failed Transformations:**
- Critical data loss
- Completely incorrect transformations
- Would cause workflow to fail
## Common Transformation Patterns to Check:
### 1. JSON Data Handling
- Correct field extraction from nested objects
- Array manipulation (map, filter, reduce operations)
- Merging data from multiple sources
### 2. Type Conversions
- String to number conversions where needed
- Date formatting and parsing
- Boolean logic handling
- Array/object conversions
### 3. Data Aggregation
- Combining data from multiple nodes
- Proper use of Merge nodes
- Maintaining data relationships
- Handling one-to-many relationships
### 4. Data Filtering
- IF nodes with correct conditions
- Switch nodes with proper case handling
- Filter nodes for items filtering
- Filter operations on arrays
- Conditional data routing
### 5. Loop and Batch Processing (Split In Batches)
Split In Batches creates intentional loops for batch processing:
- Output 0 ("done"): Fires ONCE after all batches complete
- Output 1 ("loop"): Fires for EACH batch
Correct data flow pattern:
Split In Batches (output 1) → Processing nodes → Split In Batches (input) [LOOP BACK]
Split In Batches (output 0) → Aggregate/Final step [COMPLETION]
CRITICAL: The loop-back connection (Processing → Split In Batches input) is INTENTIONAL.
- Data accumulates across iterations
- Aggregate node on output 0 collects all processed items after loop completes
- Do NOT flag loop-back connections as "circular references" or "infinite loops"
## Violations to Identify:
**Critical (-30 to -40 points):**
- Complete data loss in transformations
- Wrong data types causing failures (e.g., string where number expected)
- Missing required data fields for downstream nodes
- **DO NOT penalize AI agent tool usage patterns**
- **DO NOT penalize Split In Batches loop-back connections** - these are intentional loops, not circular references
**Major (-10 to -20 points):**
- Partial data loss
- Incorrect field mappings affecting functionality
- Wrong assumptions about data structure
- Missing data validation
**Minor (-2 to -5 points):**
- Inefficient data transformations
- Unnecessary data duplication
- Minor field naming inconsistencies
- Missing optional data enrichment
## Special Considerations:
### DO NOT penalize for:
- Different but valid transformation approaches
- Field renaming that maintains data integrity
- Intermediate transformation steps for clarity
- Placeholder values where user didn't provide data
- Chat Trigger without return data flow (responses are automatic via built-in mechanism)
- Document Loader without main input (reads from workflow context, not main connections)
- Node positions not matching visual flow (positions are for layout only, not execution order)
### Context Awareness:
- Consider the user's intent for data transformation
- Some data loss might be intentional (filtering)
- Transformation complexity should match task requirements
- AI nodes might transform data implicitly
## Scoring Instructions
1. Evaluate transformation accuracy (0-1)
2. Identify specific violations
3. Overall score = transformation accuracy score
4. Provide examples of good/bad transformations in analysis
Focus on whether data flows correctly through the workflow and reaches its destination in the expected format.`;
const humanTemplate = `Evaluate the data flow and transformations in this workflow:
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
{referenceSection}
Provide a data flow evaluation with transformation accuracy score, violations, and analysis.`;
export function createDataFlowEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, dataFlowResultSchema, systemPrompt, humanTemplate);
}
export async function evaluateDataFlow(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<DataFlowResult> {
return await invokeEvaluatorChain(createDataFlowEvaluatorChain(llm), input);
}
@@ -0,0 +1,128 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { createEvaluatorChain, invokeEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for efficiency evaluation result
const efficiencyResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
redundancyScore: z.number().min(0).max(1).describe('Score for avoiding redundant operations'),
pathOptimization: z.number().min(0).max(1).describe('Score for optimal execution paths'),
nodeCountEfficiency: z.number().min(0).max(1).describe('Score for using minimal nodes'),
analysis: z.string().describe('Brief analysis of workflow efficiency'),
});
export type EfficiencyResult = z.infer<typeof efficiencyResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing specifically on WORKFLOW EFFICIENCY.
Your task is to evaluate the efficiency of the workflow across three key metrics.
## CRITICAL: Understanding n8n Efficiency Patterns
- **AI agents with tools + separate nodes is NOT always duplication**
- Agent tools are for AI-driven operations
- Separate nodes may handle different data or validation
- **Backup/fallback paths are intentional redundancy for reliability**
- **Some "redundancy" improves maintainability and debugging**
- **Focus on actual inefficiencies, not architectural choices**
## Efficiency Metrics
### 1. Redundancy Score (0-1)
Evaluate if the workflow avoids redundant operations:
- Check for duplicate operations that could be consolidated
- Look for or unnecessary data transformations
- Find redundant Set nodes that could be combined
- Score 1.0 = No redundancy, 0.0 = Highly redundant
**Violations for redundancy:**
- Critical: Same operation performed 3+ times unnecessarily
- Major: Clear duplication of logic or operations
- Minor: Small inefficiencies that could be optimized
### 2. Path Optimization (0-1)
Evaluate if the workflow uses optimal execution paths:
- Check if operations are in the most efficient order
- Identify paths that could be shortened or simplified
- Verify conditional logic doesn't create inefficient branches
- Score 1.0 = Optimal paths, 0.0 = Very inefficient paths
### 3. Node Count Efficiency (0-1)
Evaluate if the workflow uses the minimal number of nodes needed:
- Check if multiple operations could be combined into single nodes
- Look for unnecessary intermediate nodes
- Identify if simpler node types could achieve the same result
- Consider if the task complexity justifies the node count
- Score 1.0 = Minimal nodes for task, 0.0 = Excessive nodes
**Guidelines for node count:**
- Simple tasks (1-3 operations): 2-5 nodes expected
- Medium tasks (4-7 operations): 5-10 nodes expected
- Complex tasks (8+ operations): 10+ nodes acceptable
- Each node should have a clear purpose
**Violations for node count:**
- Critical: 2x+ more nodes than necessary
- Major: 50% more nodes than optimal
- Minor: A few extra nodes that could be consolidated
## Important Considerations
### DO NOT penalize for:
- Nodes required for proper error handling
- Necessary data validation steps
- Required authentication/setup nodes
- Legitimate use of multiple nodes for clarity/maintainability
- AI sub-nodes (they provide capabilities, not redundancy)
### Context Awareness:
- Consider the complexity of the user's request
- Some redundancy may be acceptable for reliability
- Clear separation of concerns can justify more nodes
## Scoring Instructions
1. Calculate each metric score (0-1)
2. Identify violations with point deductions
3. Overall score = average of the three metrics
4. Provide specific examples in the analysis
Focus on identifying clear inefficiencies, not micro-optimizations.`;
const humanTemplate = `Evaluate the efficiency of this workflow:
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
{referenceSection}
Provide an efficiency evaluation with individual metric scores, overall score, violations, and analysis.`;
export function createEfficiencyEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, efficiencyResultSchema, systemPrompt, humanTemplate);
}
export async function evaluateEfficiency(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<EfficiencyResult> {
const result = await invokeEvaluatorChain(createEfficiencyEvaluatorChain(llm), input);
// Ensure overall score is calculated as average of metrics
const avgScore =
(result.redundancyScore + result.pathOptimization + result.nodeCountEfficiency) / 3;
result.score = avgScore;
return result;
}
@@ -0,0 +1,130 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { createEvaluatorChain, invokeEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for expressions evaluation result
const expressionsResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
analysis: z.string().describe('Brief analysis of expression syntax and usage'),
});
export type ExpressionsResult = z.infer<typeof expressionsResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing specifically on EXPRESSION SYNTAX and CORRECTNESS.
Your task is to evaluate whether expressions correctly reference nodes and data using proper n8n syntax.
## Correct n8n Expression Syntax
### Modern Syntax (Preferred)
The correct n8n expression syntax uses \`{{ $('Node Name').item.json.field }}\` format
**Valid patterns:**
- Single item: \`={{ $('Node Name').item.json.fieldName }}\`
- All items: \`={{ $('Node Name').all() }}\`
- First/last item: \`={{ $('Node Name').first().json.field }}\` or \`={{ $('Node Name').last().json.field }}\`
- Array index: \`={{ $('Node Name').all()[0].json.fieldName }}\`
- Previous node: \`={{ $json.fieldName }}\` or \`={{ $input.item.json.field }}\`
- String with text: \`="Text prefix {{ expression }} text suffix"\`
- String with date: \`="Report - {{ $now.format('MMMM d, yyyy') }}"\`
### Special Tool Node Pattern
- Tool nodes (ending with "Tool") with ai_tool connections support $fromAI
- Format: \`={{ $fromAI('parameterName', 'description', 'type', defaultValue) }}\`
- This allows AI Agents to dynamically populate parameters
### Valid JavaScript in Expressions
- Array methods: \`={{ $json.items.map(item => item.name).join(', ') }}\`
- String operations: \`={{ $json.text.split(',').filter(x => x) }}\`
- Math operations: \`={{ Math.round($json.price * 1.2) }}\`
- Conditional logic: \`={{ $json.status === 'active' ? 'Yes' : 'No' }}\`
### Special n8n Variables
- **Item access helpers**: \`$json\`, \`$binary\`, \`$input.item\`, \`$input.all()\`, \`$input.first()\`, \`$input.last()\`, \`$input.params\`, \`$input.context.noItemsLeft\`
- **Cross-node helpers**: \`$('Node Name').item\`, \`.all(branchIndex?, runIndex?)\`, \`.first(...)\`, \`.last(...)\`, \`.params\`, \`.context\`, \`.itemMatching(currentNodeInputIndex)\`, \`$('Node Name').isExecuted\`
- **Execution metadata**: \`$workflow.id\`, \`$workflow.name\`, \`$workflow.active\`, \`$execution.id\`, \`$execution.mode\`, \`$execution.resumeUrl\`, \`$execution.customData\`, \`$runIndex\`, \`$prevNode.name\`, \`$prevNode.outputIndex\`, \`$prevNode.runIndex\`, \`$itemIndex\`, \`$nodeVersion\`, \`$version\`
- **Environment and variables**: \`$env\`, \`$vars\`, \`$secrets\`, \`$getWorkflowStaticData(type)\`
- **Utility helpers**: \`$evaluateExpression(expression, itemIndex?)\`, \`$ifEmpty(value, defaultValue)\`
- **Date and time**: \`$now\`, \`$today\`
- **HTTP node only**: \`$pageCount\`, \`$request\`, \`$response\`
- **Context awareness**: Some helpers exist only in specific nodes (Loop Over Items, HTTP Request, etc.); do not assume they should appear everywhere
## Important: The = Prefix
- REQUIRED for expressions: \`={{ expression }}\`
- REQUIRED for mixed text/expressions: \`="Text {{ expression }}"\`
- Optional for pure static text: \`"Hello World"\` or \`="Hello World"\`
## Evaluation Criteria
### DO NOT penalize:
- Alternative but functionally equivalent syntax variations
- Expression syntax that would work even if not optimal
- String concatenation in any valid form
- Simple = prefix for strings
- Any working expression format
### Check for these violations:
**Critical (-40 to -50 points):**
- Invalid JavaScript syntax causing runtime errors
- Referencing non-existent nodes or fields or npm modules
- Using $fromAI in non-tool nodes
- Unclosed brackets, syntax errors, malformed JSON
**Major (-20 to -25 points):**
- Missing required = prefix for expressions
- Referencing undefined variables or functions
- Wrong data paths preventing execution
**Minor (-5 to -10 points):**
- Inefficient but working expressions
- Outdated syntax (e.g., \`$node["NodeName"]\` instead of \`$('NodeName')\`)
- Style preferences that don't affect functionality
## Context Understanding
Consider the data flow context:
- Field names may differ between nodes
- Check if referenced fields exist in source nodes
- Consider field name transformations
- Minor naming mismatches are less severe if types match
## Scoring Instructions
1. Start with 100 points
2. Deduct points for each violation found based on severity
3. Score cannot go below 0
4. Convert to 0-1 scale by dividing by 100
Focus on whether expressions would execute successfully, not style preferences.`;
const humanTemplate = `Evaluate the expression syntax of this workflow:
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
{referenceSection}
Provide an expressions evaluation with score, violations, and brief analysis.`;
export function createExpressionsEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, expressionsResultSchema, systemPrompt, humanTemplate);
}
export async function evaluateExpressions(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<ExpressionsResult> {
return await invokeEvaluatorChain(createExpressionsEvaluatorChain(llm), input);
}
@@ -0,0 +1,183 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { createEvaluatorChain, invokeEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for functionality evaluation result
const functionalityResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
analysis: z.string().describe('Brief analysis of functionality implementation'),
});
export type FunctionalityResult = z.infer<typeof functionalityResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing specifically on FUNCTIONAL CORRECTNESS.
Your task is to evaluate whether a generated workflow correctly implements what the user EXPLICITLY requested.
## Your Role
Evaluate ONLY the functional aspects - whether the workflow achieves the intended goal and performs the requested operations.
## Evaluation Criteria
### DO NOT penalize for:
- Missing optimizations not requested by user
- Missing features that would be "nice to have" but weren't specified
- Alternative valid approaches to solve the same problem
- Style preferences or minor inefficiencies
### Check for these violations:
**Critical (-40 to -50 points):**
- Missing core functionality explicitly requested
- Incorrect operation logic that prevents the workflow from working
- Workflows missing a trigger node when they need to start automatically or by some external event
- Complete failure to address the user's main request
**Major (-15 to -25 points):**
- Missing explicitly required data transformations
- Incomplete implementation of requested features
- Using completely wrong node type for the task (e.g., Set node when IF node is clearly needed)
- Workflows that would fail immediately on first execution due to structural issues
- Missing important steps that were clearly specified
**Minor (-5 to -10 points):**
- Missing optional features explicitly mentioned by user
- Using less optimal but functional node choices
- Minor deviations from requested behavior that don't break functionality
## Scoring Instructions
1. Start with 100 points
2. Deduct points for each violation found based on severity
3. Score cannot go below 0
4. Convert to 0-1 scale by dividing by 100
## Important Context
- Focus on whether the workflow performs all EXPLICITLY requested operations
- Check if operations are in the correct logical sequence
- Verify it handles all scenarios mentioned in the user prompt
- Ensure data transformations are implemented as requested
- Remember: functional correctness is about meeting requirements, not perfection
## n8n RAG Pipeline Pattern (CRITICAL - Do Not Misunderstand)
**Document Loader is a CAPABILITY-ONLY sub-node. It NEVER receives main data flow.**
The Document Loader node:
- Has NO main input - it cannot and should not receive data via main connections
- ONLY connects via ai_document TO a Vector Store (Document Loader → Vector Store)
- Reads data from the workflow context (binary files, JSON) based on its dataType configuration
- Is a capability provider that tells Vector Store HOW to process documents
**CORRECT RAG Pipeline:**
\`\`\`
Data Source (Extract From File, HTTP Request, etc.)
│ [main]
Vector Store (insert mode) ◄──[ai_document]── Document Loader ◄──[ai_textSplitter]── Text Splitter
└──[ai_embedding]── Embeddings
\`\`\`
**THE FOLLOWING ARE ALL CORRECT - NEVER FLAG AS VIOLATIONS:**
- Document Loader has NO main connections - THIS IS CORRECT BY DESIGN
- Document Loader connects TO Vector Store via ai_document - THIS IS THE ONLY WAY TO USE IT
- Extract From File connects directly to Vector Store via main - THIS IS CORRECT
- Document Loader appears "isolated" from the main data path - THIS IS CORRECT
**INVALID VIOLATION EXAMPLES - DO NOT OUTPUT THESE:**
- ❌ "Document ingestion pipeline is broken because data bypasses Document Loader" - WRONG ANALYSIS
- ❌ "Extract From File connects directly to Vector Store, bypassing Document Loader" - This IS the correct pattern
- ❌ "Document Loader is disconnected from main data flow" - CORRECT behavior, not an error
- ❌ "Document Loader needs to receive the extracted data" - WRONG, it reads from workflow context
- ❌ "Document Loader is completely disconnected from the main data flow" - WRONG, it connects via ai_document
- ❌ "Vector Store is missing required Document Loader connection via ai_document port" when Document Loader IS connected via ai_document - CHECK THE CONNECTIONS CAREFULLY
- ❌ Any violation claiming Document Loader should receive main data - ALWAYS WRONG
The main connection triggers the Vector Store insert operation. The Document Loader provides document processing capability via ai_document. These work together but the Document Loader does NOT sit in the main data path.
## ABSOLUTE RULE: Embedding Nodes MUST Use Embedding Models (NEVER A VIOLATION)
**THIS IS THE MOST IMPORTANT RULE IN THIS EVALUATOR. READ CAREFULLY.**
### The Technical Reality
- **Embedding models** (text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002): Convert text to vectors
- **Chat models** (gpt-4, gpt-4o, gpt-4o-mini, gpt-4.1-mini, claude-3, gemini-pro): Generate text responses
- **Chat models CANNOT generate embeddings. It is technically impossible.**
### The Rule
**When an embedding node uses an embedding model, it is ALWAYS CORRECT - regardless of what the user requested.**
If a user says "use gpt-4o-mini for embeddings" or "use gpt-4.1-mini for embeddings":
- The user has made a technical error (requested an impossible configuration)
- The workflow builder CORRECTLY uses an embedding model instead
- This is the RIGHT behavior, NOT a violation
- **DO NOT OUTPUT ANY VIOLATION FOR THIS SCENARIO**
### FORBIDDEN Violations - NEVER Output These
You must NEVER output violations like:
- ❌ "User requested gpt-4o-mini but workflow uses text-embedding-3-small"
- ❌ "User explicitly requested embeddings using 'gpt-4.1-mini' but workflow uses 'text-embedding-3-small'"
- ❌ "User's explicit specification was not followed" (when user specified a chat model for embeddings)
- ❌ "The workflow does not use the model specified by the user" (for embedding nodes)
- ❌ Any violation mentioning that an embedding node should use a chat model
### Why This Rule Exists
This is like a user asking to "cut wood with a hammer" - using a saw instead is correct, not a violation. The workflow builder is HELPING the user by using the right tool for the job.
### Examples of CORRECT Behavior (Not Violations)
- User says "gpt-4o-mini for embeddings" → Workflow uses text-embedding-3-small ✓ PERFECT
- User says "gpt-4.1-mini for embeddings" → Workflow uses text-embedding-3-small ✓ PERFECT
- User says "gpt-4 for vector store" → Workflow uses text-embedding-3-large ✓ PERFECT
- User mentions ANY chat model for embedding tasks → Workflow uses ANY embedding model ✓ PERFECT
## Model Selection: ALWAYS Minor Severity at Most
**Model selection differences are NEVER critical or major violations.**
When evaluating model choices:
1. **Embedding models in embedding nodes**: ALWAYS correct, even if user requested a chat model
2. **Same family, different model**: Minor at most (e.g., user says gpt-4, workflow uses gpt-4o-mini)
3. **Same provider, different model**: Minor at most (e.g., user says claude-3-opus, workflow uses claude-3-sonnet)
4. **Different provider entirely**: Minor at most, unless user explicitly required a specific provider for a business reason
**Examples of CORRECT behavior (not violations):**
- User requests "gpt-4o-mini" → Workflow uses "gpt-4o" or "gpt-4" ✓
- User requests "claude" → Workflow uses any Anthropic model ✓
- User requests "OpenAI" → Workflow uses any OpenAI model ✓
- User mentions any model → Workflow uses a different but capable model ✓
**The workflow builder selects appropriate models. Model choice is a preference, not a functional requirement.**`;
const humanTemplate = `Evaluate the functional correctness of this workflow:
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
{referenceSection}
Provide a functionality evaluation with score, violations, and brief analysis.`;
export function createFunctionalityEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, functionalityResultSchema, systemPrompt, humanTemplate);
}
export async function evaluateFunctionality(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<FunctionalityResult> {
return await invokeEvaluatorChain(createFunctionalityEvaluatorChain(llm), input);
}
@@ -0,0 +1,15 @@
// Export all evaluator functions and types
export { evaluateFunctionality, type FunctionalityResult } from './functionality-evaluator';
export { evaluateConnections, type ConnectionsResult } from './connections-evaluator';
export { evaluateExpressions, type ExpressionsResult } from './expressions-evaluator';
export {
evaluateNodeConfiguration,
type NodeConfigurationResult,
} from './node-configuration-evaluator';
export { evaluateEfficiency, type EfficiencyResult } from './efficiency-evaluator';
export { evaluateDataFlow, type DataFlowResult } from './data-flow-evaluator';
export { evaluateMaintainability, type MaintainabilityResult } from './maintainability-evaluator';
export {
evaluateBestPractices,
type BestPracticesResult,
} from './best-practices-evaluator';
@@ -0,0 +1,230 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { createEvaluatorChain, invokeEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for maintainability evaluation result
const maintainabilityResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
nodeNamingQuality: z.number().min(0).max(1).describe('Score for descriptive node naming'),
workflowOrganization: z.number().min(0).max(1).describe('Score for logical workflow structure'),
modularity: z.number().min(0).max(1).describe('Score for reusable and modular components'),
analysis: z.string().describe('Brief analysis of workflow maintainability'),
});
export type MaintainabilityResult = z.infer<typeof maintainabilityResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing specifically on WORKFLOW MAINTAINABILITY.
Your task is to evaluate how maintainable and well-organized the workflow is.
## Maintainability Metrics
### 1. Node Naming Quality (0-1)
Evaluate the descriptiveness and consistency of node names:
**Score 1.0 - Excellent Naming:**
- All nodes have descriptive, clear names
- Names indicate the node's purpose/function
- Consistent naming convention throughout
- No generic names like "Set", "HTTP Request"
- Names help understand workflow at a glance
**Score 0.75 - Good Naming:**
- Most nodes well-named
- Some generic names but context is clear
- Generally consistent naming
**Score 0.5 - Adequate Naming:**
- Mix of good and poor names
- Some nodes hard to understand from names
- Inconsistent naming patterns
**Score 0.25 - Poor Naming:**
- Many generic or unclear names
- Difficult to understand node purposes
- No clear naming strategy
**Score 0.0 - Very Poor Naming:**
- All or most nodes have generic names
- Impossible to understand workflow from names
- Random or meaningless names
**Good Naming Examples:**
- "Fetch Customer Data from CRM"
- "Transform Order to Invoice Format"
- "Send Confirmation Email"
- "Validate User Input"
- "Check Inventory Availability"
**Poor Naming Examples:**
- "Set"
- "HTTP Request"
- "Node1"
- "Process Data"
- "Do Something"
### 2. Workflow Organization (0-1)
Evaluate the logical structure and layout:
**Score 1.0 - Excellent Organization:**
- Clear logical flow from start to finish
- Related nodes grouped together
- Proper separation of concerns
- Easy to follow data flow
- Clear section boundaries
**Score 0.75 - Good Organization:**
- Generally well-organized
- Most sections clear
- Minor improvements possible
**Score 0.5 - Adequate Organization:**
- Basic organization present
- Some confusion in flow
- Mixed concerns in places
**Score 0.25 - Poor Organization:**
- Confusing layout
- Hard to follow flow
- Mixed responsibilities
- No clear structure
**Score 0.0 - No Organization:**
- Chaotic structure
- Random node placement
- Impossible to follow
- No logical grouping
**Organization Patterns to Look For:**
- Input validation at the beginning
- Data transformation in the middle
- Output/notification at the end
- Error handling grouped together
- Related operations near each other
### 3. Modularity (0-1)
Evaluate reusability and component separation:
**Score 1.0 - Highly Modular:**
- Clear separation of concerns
- Reusable components/patterns
- Each node has single responsibility
- Could easily extract parts for reuse
- Workflow sections could be sub-workflows
**Score 0.75 - Good Modularity:**
- Most components well-separated
- Some reusable patterns
- Generally follows single responsibility
**Score 0.5 - Adequate Modularity:**
- Some modularity present
- Mixed responsibilities in places
- Limited reusability
**Score 0.25 - Poor Modularity:**
- Little separation of concerns
- Nodes doing too many things
- Hard to extract reusable parts
- Tightly coupled components
**Score 0.0 - No Modularity:**
- Everything mixed together
- No clear component boundaries
- Impossible to reuse parts
- Monolithic approach
**Modularity Indicators:**
- Each node does one thing well
- Data transformation separated from business logic
- Authentication separated from main flow
- Error handling is modular
- Could extract sections as sub-workflows
## Violations to Identify:
**Critical (-40 to -50 points):**
- Completely generic node naming throughout
- Chaotic organization making workflow unmaintainable
- No modularity - everything in single complex nodes
- Workflow would be impossible for another developer to understand
**Major (-15 to -25 points):**
- Many poorly named nodes
- Confusing organization in critical sections
- Poor separation of concerns
- Difficult to modify or extend
**Minor (-5 to -10 points):**
- Some generic node names
- Minor organization improvements needed
- Could be more modular
- Small maintainability issues
## Important Considerations:
### DO NOT penalize for:
- Simple workflows that don't need complex organization
- AI-generated placeholder names that are still descriptive
- Different but valid organizational approaches
- Prototypes or POC workflows
### Context Awareness:
- Simple workflows (2-5 nodes) need less organization
- Complex workflows (15+ nodes) need clear structure
- Template workflows should be extra maintainable
- Consider the workflow's purpose and audience
### Workflow Complexity vs Maintainability:
- Simple: Basic naming and organization acceptable
- Medium: Should have clear names and sections
- Complex: Must have excellent maintainability
## Scoring Instructions
1. Calculate node naming quality (0-1)
2. Calculate workflow organization (0-1)
3. Calculate modularity score (0-1)
4. Overall score = average of three metrics
5. Identify specific violations
6. Suggest improvements where applicable
Focus on aspects that would make the workflow easier to understand, modify, and maintain by other developers.`;
const humanTemplate = `Evaluate the maintainability of this workflow:
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
{referenceSection}
Provide a maintainability evaluation with naming, organization, and modularity scores, violations, and analysis.`;
export function createMaintainabilityEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, maintainabilityResultSchema, systemPrompt, humanTemplate);
}
export async function evaluateMaintainability(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<MaintainabilityResult> {
const result = await invokeEvaluatorChain(createMaintainabilityEvaluatorChain(llm), input);
// Ensure overall score is calculated as average of metrics
const avgScore = (result.nodeNamingQuality + result.workflowOrganization + result.modularity) / 3;
result.score = avgScore;
return result;
}
@@ -0,0 +1,187 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { z } from 'zod';
import { createEvaluatorChain, invokeEvaluatorChain } from './base';
import type { EvaluationInput } from '../evaluation';
// Schema for node configuration evaluation result
const nodeConfigurationResultSchema = z.object({
score: z.number().min(0).max(1),
violations: z.array(
z.object({
type: z.enum(['critical', 'major', 'minor']),
description: z.string(),
pointsDeducted: z.number().min(0),
}),
),
analysis: z.string().describe('Brief analysis of node parameter configuration'),
});
export type NodeConfigurationResult = z.infer<typeof nodeConfigurationResultSchema>;
const systemPrompt = `You are an expert n8n workflow evaluator focusing specifically on NODE CONFIGURATION and PARAMETERS.
Your task is to evaluate whether nodes are configured with correct parameters and settings.
## SCOPE: ONLY Evaluate Node Parameters
**YOUR SCOPE IS LIMITED TO:**
- Node parameter values (the "parameters" object inside each node)
- Whether parameter values match what the user requested
- Whether required parameters are present
- Whether parameter values are valid (correct types, valid JSON, etc.)
**DO NOT EVALUATE (these are handled by other evaluators):**
- Node connections (handled by Connections Evaluator)
- Whether nodes are connected to each other
- Missing ai_document, ai_embedding, ai_tool, ai_memory, or any other connection types
- Data flow between nodes
**NEVER OUTPUT VIOLATIONS ABOUT:**
- ❌ "missing Document Loader connection"
- ❌ "missing ai_document connection"
- ❌ "missing ai_embedding connection"
- ❌ "missing required connection"
- ❌ Any violation mentioning "connection" - that's not your job
If you see something that looks like a connection issue, IGNORE IT. Focus only on the parameters object.
## CRITICAL: Understanding n8n Credentials and Configuration
- **NEVER penalize nodes for missing credentials**
- **Credentials are ALWAYS configured at runtime through the n8n UI**
- **Empty "credentials": {} fields are NORMAL and EXPECTED**
- **Focus on actual parameter misconfiguration, not missing credentials**
## Valid Placeholder Patterns
### DO NOT penalize these patterns:
- \`<UNKNOWN>\` values when user didn't specify concrete values
- Empty strings ("") in configuration fields when not provided by user
- Empty strings in resource selectors (base/table/document IDs)
- Placeholder API keys like "YOUR_API_KEY" or similar patterns
- These are ALL valid user configuration points, not errors
**Important**: Empty string ("") and \`<UNKNOWN>\` are BOTH valid placeholders
### Special Tool Node Handling
- $fromAI expressions are VALID in ANY tool node (nodes ending with "Tool")
- Tool nodes connected via ai_tool allow AI Agents to populate parameters dynamically
- Format: \`{{ $fromAI('parameter', 'description') }}\` is correct and expected
- DO NOT penalize $fromAI in TOOL NODE parameters
## ABSOLUTE RULE: Embedding Nodes MUST Use Embedding Models (NEVER A VIOLATION)
**THIS IS THE MOST IMPORTANT RULE IN THIS EVALUATOR. READ CAREFULLY.**
### The Technical Reality
- **Embedding models** (text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002): Convert text to vectors
- **Chat models** (gpt-4, gpt-4o, gpt-4o-mini, gpt-4.1-mini, claude-3, etc.): Generate text
- **Chat models CANNOT generate embeddings. It is technically impossible.**
### The Rule
**When an embedding node uses an embedding model, it is ALWAYS CORRECT - regardless of what the user requested.**
If a user says "use gpt-4o-mini for embeddings" or "use gpt-4.1-mini for embeddings":
- The user has made a technical error (requested an impossible configuration)
- The workflow builder CORRECTLY uses an embedding model instead
- This is the RIGHT behavior, NOT a violation
- **DO NOT OUTPUT ANY VIOLATION FOR THIS SCENARIO**
### FORBIDDEN Violations - NEVER Output These
You must NEVER output violations like:
- ❌ "User requested gpt-4o-mini but workflow uses text-embedding-3-small"
- ❌ "User explicitly requested embeddings using 'gpt-4.1-mini' but workflow uses 'text-embedding-3-small'"
- ❌ "User's explicit specification was not followed" (when user specified a chat model for embeddings)
- ❌ "Embedding node uses wrong model"
- ❌ Any violation about embedding nodes not using chat models
### Examples of CORRECT Behavior (Not Violations)
- User says "gpt-4o-mini for embeddings" → Workflow uses text-embedding-3-small ✓ PERFECT
- User says "gpt-4.1-mini for embeddings" → Workflow uses text-embedding-3-small ✓ PERFECT
- User mentions ANY chat model for embedding tasks → Workflow uses ANY embedding model ✓ PERFECT
## General Model Selection Rules
**Model selection differences are NEVER critical or major violations. At most MINOR.**
Model choices are preferences, not requirements:
- Same provider, different model = MINOR at most (gpt-4 vs gpt-4o-mini)
- Different provider = MINOR at most (OpenAI vs Anthropic)
- Model selection is NEVER critical or major
**Examples of CORRECT behavior (not violations):**
- User says "gpt-4" → Workflow uses gpt-4o-mini ✓
- User says "claude" → Workflow uses any Anthropic model ✓
- User mentions model X → Workflow uses capable model Y ✓
## Evaluation Criteria
### Check for these violations:
**Critical (-30 to -40 points):** ONLY for actual breaking issues:
- Truly required parameters completely absent (not empty/placeholder):
- HTTP Request without URL (unless using $fromAI)
- Database operations without operation type specified
- Code node without any code
- Parameters with invalid values that would crash:
- Invalid JSON in JSON fields
- Non-numeric values in number-only fields
- Configuration that would cause runtime crash
- **NEVER penalize for missing credentials or API keys**
- **NEVER penalize for model selection choices**
**Major (-10 to -20 points):**
- Wrong operation mode when explicitly specified by user
- Significant deviation from requested behavior (NOT model choices)
- Missing resource/operation selection that prevents node from functioning
- **NOT model selection - model differences are minor at most**
**Minor (-2 to -5 points):**
- Suboptimal but working configurations
- Style preferences or minor inefficiencies
- Missing optional parameters that could improve functionality
- Model selection differences (if any - usually not worth flagging)
## Context-Aware Evaluation
### Compare Against User Request
- Only penalize incorrect values if user explicitly provided them
- If user didn't provide specific values, placeholders are expected
- Focus on structural correctness, not specific values
### Severity Guidelines:
- If user didn't provide email addresses, \`<UNKNOWN>\` is expected
- If user didn't specify API keys, placeholder values are valid
- If user didn't provide specific IDs or credentials, empty/placeholder values are correct
## Scoring Instructions
1. Start with 100 points
2. Deduct points for each violation found based on severity
3. Score cannot go below 0
4. Convert to 0-1 scale by dividing by 100
Focus on whether parameters are set correctly based on what the user actually specified.`;
const humanTemplate = `Evaluate the node configuration of this workflow:
<user_prompt>
{userPrompt}
</user_prompt>
<generated_workflow>
{generatedWorkflow}
</generated_workflow>
{referenceSection}
Provide a node configuration evaluation with score, violations, and brief analysis.`;
export function createNodeConfigurationEvaluatorChain(llm: BaseChatModel) {
return createEvaluatorChain(llm, nodeConfigurationResultSchema, systemPrompt, humanTemplate);
}
export async function evaluateNodeConfiguration(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<NodeConfigurationResult> {
return await invokeEvaluatorChain(createNodeConfigurationEvaluatorChain(llm), input);
}
@@ -0,0 +1,145 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import type { INodeTypeDescription } from 'n8n-workflow';
import type { SimpleWorkflow } from '@/types/workflow';
import type { EvaluationInput } from './evaluation';
import { evaluateWorkflow } from './workflow-evaluator';
import { runWithOptionalLimiter, withTimeout } from '../../harness/evaluation-helpers';
import type { EvaluationContext, Evaluator, Feedback } from '../../harness/harness-types';
const EVALUATOR_NAME = 'llm-judge';
/**
* Violation type from evaluation results.
*/
interface Violation {
type: string;
description: string;
pointsDeducted: number;
}
/**
* Format violations as a comment string.
*/
function formatViolations(violations: Violation[]): string {
if (!violations || violations.length === 0) return '';
return violations.map((v) => `[${v.type}] ${v.description}`).join('; ');
}
/**
* Create an LLM-as-judge evaluator that uses the existing evaluateWorkflow chain.
*
* @param llm - The LLM to use for evaluation
* @param _nodeTypes - Node type descriptions (unused but kept for interface compatibility)
* @returns An evaluator that produces feedback from LLM evaluation
*/
export function createLLMJudgeEvaluator(
llm: BaseChatModel,
_nodeTypes: INodeTypeDescription[],
): Evaluator<EvaluationContext> {
const fb = (
metric: string,
score: number,
kind: Feedback['kind'],
comment?: string,
): Feedback => ({
evaluator: EVALUATOR_NAME,
metric,
score,
kind,
...(comment ? { comment } : {}),
});
return {
name: EVALUATOR_NAME,
async evaluate(workflow: SimpleWorkflow, ctx: EvaluationContext): Promise<Feedback[]> {
const input: EvaluationInput = {
userPrompt: ctx.prompt,
generatedWorkflow: workflow,
};
const result = await runWithOptionalLimiter(async () => {
return await withTimeout({
promise: evaluateWorkflow(llm, input),
timeoutMs: ctx.timeoutMs,
label: 'llm-judge:evaluateWorkflow',
});
}, ctx.llmCallLimiter);
return [
// Core category scores
fb(
'functionality',
result.functionality.score,
'metric',
formatViolations(result.functionality.violations),
),
fb(
'connections',
result.connections.score,
'metric',
formatViolations(result.connections.violations),
),
fb(
'expressions',
result.expressions.score,
'metric',
formatViolations(result.expressions.violations),
),
fb(
'nodeConfiguration',
result.nodeConfiguration.score,
'metric',
formatViolations(result.nodeConfiguration.violations),
),
// Efficiency with sub-metrics
fb(
'efficiency',
result.efficiency.score,
'metric',
formatViolations(result.efficiency.violations),
),
fb('efficiency.redundancyScore', result.efficiency.redundancyScore, 'detail'),
fb('efficiency.pathOptimization', result.efficiency.pathOptimization, 'detail'),
fb('efficiency.nodeCountEfficiency', result.efficiency.nodeCountEfficiency, 'detail'),
// Data flow
fb(
'dataFlow',
result.dataFlow.score,
'metric',
formatViolations(result.dataFlow.violations),
),
// Maintainability with sub-metrics
fb(
'maintainability',
result.maintainability.score,
'metric',
formatViolations(result.maintainability.violations),
),
fb('maintainability.nodeNamingQuality', result.maintainability.nodeNamingQuality, 'detail'),
fb(
'maintainability.workflowOrganization',
result.maintainability.workflowOrganization,
'detail',
),
fb('maintainability.modularity', result.maintainability.modularity, 'detail'),
// Best practices adherence
fb(
'bestPractices',
result.bestPractices.score,
'metric',
formatViolations(result.bestPractices.violations),
),
// Overall score
fb('overallScore', result.overallScore, 'score', result.summary),
];
},
};
}
@@ -0,0 +1,309 @@
import type { EvaluationResult, CategoryScore } from './evaluation';
import {
calculateWeightedScore,
generateEvaluationSummary,
identifyCriticalIssues,
LLM_JUDGE_CATEGORY_WEIGHTS,
TOTAL_WEIGHT_WITHOUT_STRUCTURAL,
TOTAL_WEIGHT_WITH_STRUCTURAL,
} from './workflow-evaluator';
/**
* Creates a minimal category score for testing.
*/
function createCategoryScore(
score: number,
violations: CategoryScore['violations'] = [],
): CategoryScore {
return { score, violations };
}
/**
* Creates a complete evaluation result with all scores set to the same value.
*/
function createUniformResult(score: number): EvaluationResult {
return {
overallScore: 0,
functionality: createCategoryScore(score),
connections: createCategoryScore(score),
expressions: createCategoryScore(score),
nodeConfiguration: createCategoryScore(score),
efficiency: {
...createCategoryScore(score),
redundancyScore: score,
pathOptimization: score,
nodeCountEfficiency: score,
},
dataFlow: createCategoryScore(score),
maintainability: {
...createCategoryScore(score),
nodeNamingQuality: score,
workflowOrganization: score,
modularity: score,
},
bestPractices: createCategoryScore(score),
structuralSimilarity: {
score: 0,
violations: [],
applicable: false,
},
summary: '',
};
}
describe('workflow-evaluator', () => {
describe('calculateWeightedScore', () => {
it('should return 1.0 when all scores are perfect', () => {
const result = createUniformResult(1.0);
expect(calculateWeightedScore(result)).toBeCloseTo(1.0, 5);
});
it('should return 0 when all scores are zero', () => {
const result = createUniformResult(0);
expect(calculateWeightedScore(result)).toBe(0);
});
it('should return 0.5 when all scores are 0.5', () => {
const result = createUniformResult(0.5);
expect(calculateWeightedScore(result)).toBeCloseTo(0.5, 5);
});
it('should weight functionality at 25%', () => {
const result = createUniformResult(0);
result.functionality.score = 1.0;
const expected = LLM_JUDGE_CATEGORY_WEIGHTS.functionality / TOTAL_WEIGHT_WITHOUT_STRUCTURAL;
expect(calculateWeightedScore(result)).toBeCloseTo(expected, 5);
});
it('should weight connections at 15%', () => {
const result = createUniformResult(0);
result.connections.score = 1.0;
const expected = LLM_JUDGE_CATEGORY_WEIGHTS.connections / TOTAL_WEIGHT_WITHOUT_STRUCTURAL;
expect(calculateWeightedScore(result)).toBeCloseTo(expected, 5);
});
it('should include structural similarity when applicable', () => {
const result = createUniformResult(1.0);
result.structuralSimilarity = {
score: 0,
violations: [],
applicable: true,
};
// With structural similarity at 0, weighted sum = TOTAL_WEIGHT_WITHOUT_STRUCTURAL
const expected = TOTAL_WEIGHT_WITHOUT_STRUCTURAL / TOTAL_WEIGHT_WITH_STRUCTURAL;
expect(calculateWeightedScore(result)).toBeCloseTo(expected, 5);
});
it('should not include structural similarity when not applicable', () => {
const result = createUniformResult(1.0);
result.structuralSimilarity = {
score: 0.5,
violations: [],
applicable: false,
};
// Should still be 1.0 since structural similarity is not counted
expect(calculateWeightedScore(result)).toBeCloseTo(1.0, 5);
});
it('should handle mixed scores correctly', () => {
const result = createUniformResult(0);
result.functionality.score = 1.0;
result.connections.score = 0.8;
result.expressions.score = 0.6;
result.nodeConfiguration.score = 0.4;
result.efficiency.score = 0.2;
result.dataFlow.score = 0.0;
result.maintainability.score = 1.0;
result.bestPractices.score = 0.5;
const w = LLM_JUDGE_CATEGORY_WEIGHTS;
const weightedSum =
1.0 * w.functionality +
0.8 * w.connections +
0.6 * w.expressions +
0.4 * w.nodeConfiguration +
0.2 * w.efficiency +
0.0 * w.dataFlow +
1.0 * w.maintainability +
0.5 * w.bestPractices;
const expected = weightedSum / TOTAL_WEIGHT_WITHOUT_STRUCTURAL;
expect(calculateWeightedScore(result)).toBeCloseTo(expected, 5);
});
});
describe('generateEvaluationSummary', () => {
it('should list strengths for scores >= 0.8', () => {
const result = createUniformResult(0.9);
const summary = generateEvaluationSummary(result);
expect(summary).toContain('strong functional implementation');
expect(summary).toContain('well-connected nodes');
expect(summary).toContain('correct expression syntax');
expect(summary).toContain('well-configured nodes');
expect(summary).toContain('proper data flow');
expect(summary).toContain('efficient design');
expect(summary).toContain('maintainable structure');
expect(summary).toContain('follows best practices');
});
it('should list weaknesses for scores < 0.5', () => {
const result = createUniformResult(0.3);
const summary = generateEvaluationSummary(result);
expect(summary).toContain('functional gaps');
expect(summary).toContain('connection issues');
expect(summary).toContain('expression errors');
expect(summary).toContain('node configuration issues');
expect(summary).toContain('data flow problems');
expect(summary).toContain('inefficiencies');
expect(summary).toContain('poor maintainability');
expect(summary).toContain('deviates from best practices');
});
it('should not list scores between 0.5 and 0.8 as strengths or weaknesses', () => {
const result = createUniformResult(0.65);
const summary = generateEvaluationSummary(result);
// Should return the default message since no strengths or weaknesses
expect(summary).toBe(
'The workflow shows adequate implementation across all evaluated metrics.',
);
});
it('should handle mixed scores', () => {
const result = createUniformResult(0.65);
result.functionality.score = 0.9; // strength
result.connections.score = 0.3; // weakness
const summary = generateEvaluationSummary(result);
expect(summary).toContain('strong functional implementation');
expect(summary).toContain('connection issues');
expect(summary).not.toContain('adequate implementation');
});
it('should format summary with proper grammar', () => {
const result = createUniformResult(0.65);
result.functionality.score = 0.9;
result.connections.score = 0.9;
const summary = generateEvaluationSummary(result);
expect(summary).toMatch(/^The workflow demonstrates .+\.$/);
expect(summary).toContain(', '); // Multiple strengths should be comma-separated
});
it('should include "Key areas for improvement" for weaknesses', () => {
const result = createUniformResult(0.65);
result.functionality.score = 0.3;
const summary = generateEvaluationSummary(result);
expect(summary).toContain('Key areas for improvement include');
});
});
describe('identifyCriticalIssues', () => {
it('should return undefined when no critical violations exist', () => {
const result = createUniformResult(0.5);
result.functionality.violations = [
{ type: 'major', description: 'Some major issue', pointsDeducted: 20 },
{ type: 'minor', description: 'Some minor issue', pointsDeducted: 5 },
];
expect(identifyCriticalIssues(result)).toBeUndefined();
});
it('should extract critical violations from all categories', () => {
const result = createUniformResult(0.5);
result.functionality.violations = [
{ type: 'critical', description: 'Missing trigger', pointsDeducted: 50 },
];
result.connections.violations = [
{ type: 'critical', description: 'Disconnected node', pointsDeducted: 40 },
];
const issues = identifyCriticalIssues(result);
expect(issues).toHaveLength(2);
expect(issues).toContain('[functionality] Missing trigger');
expect(issues).toContain('[connections] Disconnected node');
});
it('should only include critical violations, not major or minor', () => {
const result = createUniformResult(0.5);
result.functionality.violations = [
{ type: 'critical', description: 'Critical issue', pointsDeducted: 50 },
{ type: 'major', description: 'Major issue', pointsDeducted: 20 },
{ type: 'minor', description: 'Minor issue', pointsDeducted: 5 },
];
const issues = identifyCriticalIssues(result);
expect(issues).toHaveLength(1);
expect(issues).toContain('[functionality] Critical issue');
});
it('should handle multiple critical violations in same category', () => {
const result = createUniformResult(0.5);
result.functionality.violations = [
{ type: 'critical', description: 'First critical', pointsDeducted: 50 },
{ type: 'critical', description: 'Second critical', pointsDeducted: 40 },
];
const issues = identifyCriticalIssues(result);
expect(issues).toHaveLength(2);
expect(issues).toContain('[functionality] First critical');
expect(issues).toContain('[functionality] Second critical');
});
it('should check all eight evaluation categories', () => {
const result = createUniformResult(0.5);
// Add a critical violation to each category
result.functionality.violations = [
{ type: 'critical', description: 'func issue', pointsDeducted: 50 },
];
result.connections.violations = [
{ type: 'critical', description: 'conn issue', pointsDeducted: 50 },
];
result.expressions.violations = [
{ type: 'critical', description: 'expr issue', pointsDeducted: 50 },
];
result.nodeConfiguration.violations = [
{ type: 'critical', description: 'config issue', pointsDeducted: 50 },
];
result.efficiency.violations = [
{ type: 'critical', description: 'eff issue', pointsDeducted: 50 },
];
result.dataFlow.violations = [
{ type: 'critical', description: 'flow issue', pointsDeducted: 50 },
];
result.maintainability.violations = [
{ type: 'critical', description: 'maint issue', pointsDeducted: 50 },
];
result.bestPractices.violations = [
{ type: 'critical', description: 'bp issue', pointsDeducted: 50 },
];
const issues = identifyCriticalIssues(result);
expect(issues).toHaveLength(8);
expect(issues).toContain('[functionality] func issue');
expect(issues).toContain('[connections] conn issue');
expect(issues).toContain('[expressions] expr issue');
expect(issues).toContain('[nodeConfiguration] config issue');
expect(issues).toContain('[efficiency] eff issue');
expect(issues).toContain('[dataFlow] flow issue');
expect(issues).toContain('[maintainability] maint issue');
expect(issues).toContain('[bestPractices] bp issue');
});
it('should return undefined for empty violations arrays', () => {
const result = createUniformResult(1.0);
expect(identifyCriticalIssues(result)).toBeUndefined();
});
});
});
@@ -0,0 +1,231 @@
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import type { EvaluationInput, EvaluationResult } from './evaluation';
import {
evaluateFunctionality,
evaluateConnections,
evaluateExpressions,
evaluateNodeConfiguration,
evaluateEfficiency,
evaluateDataFlow,
evaluateMaintainability,
evaluateBestPractices,
} from './evaluators';
/**
* Weights for each LLM-judge evaluation category used in overall score calculation.
*
* This is evaluator-internal weighting, and is independent from the harness-level
* cross-evaluator weighting in `evaluations/score-calculator.ts`.
* Exported for use in tests.
*/
export const LLM_JUDGE_CATEGORY_WEIGHTS = {
functionality: 0.25,
connections: 0.15,
expressions: 0.15,
nodeConfiguration: 0.15,
efficiency: 0.1,
dataFlow: 0.1,
maintainability: 0.05,
bestPractices: 0.1,
structuralSimilarity: 0.05,
} as const;
/**
* @deprecated Use `LLM_JUDGE_CATEGORY_WEIGHTS` (kept for backwards compatibility within the package).
*/
export const EVALUATION_WEIGHTS = LLM_JUDGE_CATEGORY_WEIGHTS;
/**
* Total weight when structural similarity is not applicable.
*/
export const TOTAL_WEIGHT_WITHOUT_STRUCTURAL =
LLM_JUDGE_CATEGORY_WEIGHTS.functionality +
LLM_JUDGE_CATEGORY_WEIGHTS.connections +
LLM_JUDGE_CATEGORY_WEIGHTS.expressions +
LLM_JUDGE_CATEGORY_WEIGHTS.nodeConfiguration +
LLM_JUDGE_CATEGORY_WEIGHTS.efficiency +
LLM_JUDGE_CATEGORY_WEIGHTS.dataFlow +
LLM_JUDGE_CATEGORY_WEIGHTS.maintainability +
LLM_JUDGE_CATEGORY_WEIGHTS.bestPractices;
/**
* Total weight when structural similarity is applicable.
*/
export const TOTAL_WEIGHT_WITH_STRUCTURAL =
TOTAL_WEIGHT_WITHOUT_STRUCTURAL + LLM_JUDGE_CATEGORY_WEIGHTS.structuralSimilarity;
/**
* Calculate weighted score for the overall evaluation
* @param result - Evaluation result with all category scores
* @returns Weighted overall score
*/
export function calculateWeightedScore(result: EvaluationResult): number {
const w = LLM_JUDGE_CATEGORY_WEIGHTS;
// Calculate weighted sum for all categories
const weightedSum =
result.functionality.score * w.functionality +
result.connections.score * w.connections +
result.expressions.score * w.expressions +
result.nodeConfiguration.score * w.nodeConfiguration +
result.efficiency.score * w.efficiency +
result.dataFlow.score * w.dataFlow +
result.maintainability.score * w.maintainability +
result.bestPractices.score * w.bestPractices +
(result.structuralSimilarity?.applicable
? result.structuralSimilarity.score * w.structuralSimilarity
: 0);
const totalWeight = result.structuralSimilarity?.applicable
? TOTAL_WEIGHT_WITH_STRUCTURAL
: TOTAL_WEIGHT_WITHOUT_STRUCTURAL;
return totalWeight > 0 ? weightedSum / totalWeight : 0;
}
/**
* Generates a summary of the evaluation results
* @param result - Complete evaluation result
* @returns Summary string describing strengths and weaknesses
*/
export function generateEvaluationSummary(result: EvaluationResult): string {
const strengths: string[] = [];
const weaknesses: string[] = [];
// Analyze core metrics
if (result.functionality.score >= 0.8) strengths.push('strong functional implementation');
else if (result.functionality.score < 0.5) weaknesses.push('functional gaps');
if (result.connections.score >= 0.8) strengths.push('well-connected nodes');
else if (result.connections.score < 0.5) weaknesses.push('connection issues');
if (result.expressions.score >= 0.8) strengths.push('correct expression syntax');
else if (result.expressions.score < 0.5) weaknesses.push('expression errors');
if (result.nodeConfiguration.score >= 0.8) strengths.push('well-configured nodes');
else if (result.nodeConfiguration.score < 0.5) weaknesses.push('node configuration issues');
if (result.dataFlow.score >= 0.8) strengths.push('proper data flow');
else if (result.dataFlow.score < 0.5) weaknesses.push('data flow problems');
// Analyze new metrics
if (result.efficiency.score >= 0.8) strengths.push('efficient design');
else if (result.efficiency.score < 0.5) weaknesses.push('inefficiencies');
if (result.maintainability.score >= 0.8) strengths.push('maintainable structure');
else if (result.maintainability.score < 0.5) weaknesses.push('poor maintainability');
if (result.bestPractices.score >= 0.8) strengths.push('follows best practices');
else if (result.bestPractices.score < 0.5) weaknesses.push('deviates from best practices');
// Create summary
let summary = '';
if (strengths.length > 0) {
summary += `The workflow demonstrates ${strengths.join(', ')}.`;
}
if (weaknesses.length > 0) {
summary += ` Key areas for improvement include ${weaknesses.join(', ')}.`;
}
if (summary === '') {
summary = 'The workflow shows adequate implementation across all evaluated metrics.';
}
return summary.trim();
}
/**
* Identifies critical issues from all evaluation categories
* @param result - Complete evaluation result
* @returns Array of critical issues if any
*/
export function identifyCriticalIssues(result: EvaluationResult): string[] | undefined {
const criticalIssues: string[] = [];
// Check all categories for critical violations
const categories = [
{ name: 'functionality', data: result.functionality },
{ name: 'connections', data: result.connections },
{ name: 'expressions', data: result.expressions },
{ name: 'nodeConfiguration', data: result.nodeConfiguration },
{ name: 'efficiency', data: result.efficiency },
{ name: 'dataFlow', data: result.dataFlow },
{ name: 'maintainability', data: result.maintainability },
{ name: 'bestPractices', data: result.bestPractices },
];
for (const category of categories) {
if (category.data) {
const criticalViolations = category.data.violations.filter((v) => v.type === 'critical');
criticalViolations.forEach((v) => {
criticalIssues.push(`[${category.name}] ${v.description}`);
});
}
}
return criticalIssues.length > 0 ? criticalIssues : undefined;
}
/**
* Main workflow evaluation function that orchestrates all evaluators
* Runs all evaluations in parallel for optimal performance
* @param llm - Language model to use for evaluation
* @param input - Evaluation input containing workflow and prompt
* @returns Complete evaluation result with all metrics
*/
export async function evaluateWorkflow(
llm: BaseChatModel,
input: EvaluationInput,
): Promise<EvaluationResult> {
// Run all evaluations in parallel
const [
functionality,
connections,
expressions,
nodeConfiguration,
efficiency,
dataFlow,
maintainability,
bestPractices,
] = await Promise.all([
// Core evaluations
evaluateFunctionality(llm, input),
evaluateConnections(llm, input),
evaluateExpressions(llm, input),
evaluateNodeConfiguration(llm, input),
evaluateEfficiency(llm, input),
evaluateDataFlow(llm, input),
evaluateMaintainability(llm, input),
evaluateBestPractices(llm, input),
]);
// Build the evaluation result
const evaluationResult: EvaluationResult = {
overallScore: 0, // Will be calculated below
functionality,
connections,
expressions,
nodeConfiguration,
efficiency,
dataFlow,
maintainability,
bestPractices,
structuralSimilarity: {
violations: [],
score: 0,
applicable: false, // TODO: Implement structural similarity if reference workflow provided
},
summary: '', // Will be generated below
};
// Calculate overall score
evaluationResult.overallScore = calculateWeightedScore(evaluationResult);
// Generate summary
evaluationResult.summary = generateEvaluationSummary(evaluationResult);
// Identify critical issues
evaluationResult.criticalIssues = identifyCriticalIssues(evaluationResult);
return evaluationResult;
}