UNPKG

n8n

Version:

n8n Workflow Automation Tool

285 lines 12.6 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WorkflowCompilerService = void 0; const di_1 = require("@n8n/di"); const n8n_workflow_1 = require("n8n-workflow"); const nanoid_1 = require("nanoid"); const evaluation_config_validator_1 = require("../evaluation-config-validator"); const llm_judge_provider_registry_1 = require("../llm-judge-provider-registry"); const RESERVED_PREFIX = '__eval_'; const TRIGGER_NAME = '__eval_trigger'; const NODE_STEP_X = 220; const MODEL_OFFSET_Y = 220; const METRIC_COLUMN_GAP = 440; const EXPRESSION_ROW_HEIGHT = 140; const LLM_JUDGE_ROW_HEIGHT = 380; let WorkflowCompilerService = class WorkflowCompilerService { constructor(providerRegistry) { this.providerRegistry = providerRegistry; } compile(workflow, config) { this.assertNoReservedNames(workflow); const entryNodeName = this.resolveEntryNode(workflow, config); const entryPos = this.positionOf(workflow, entryNodeName) ?? [0, 0]; const endPos = this.positionOf(workflow, config.endNodeName) ?? [ entryPos[0] + NODE_STEP_X, entryPos[1], ]; const replacedNodeName = this.findReplacedUpstreamNode(workflow, entryNodeName); const userTriggerEdge = this.findUserTriggerEdgeTo(workflow.connections, entryNodeName); const rightmostX = workflow.nodes.reduce((max, n) => Math.max(max, n.position?.[0] ?? 0), endPos[0]); const metricColumnX = rightmostX + METRIC_COLUMN_GAP; const metricRowYs = this.computeMetricRowYs(config.metrics, endPos[1]); const triggerNode = this.buildEvaluationTriggerNode(config, entryPos); const metricNodes = config.metrics.map((m, i) => this.buildMetricNode(m, metricColumnX, metricRowYs[i])); const chatModelNodes = config.metrics.flatMap((m, i) => this.buildChatModelNodeIfNeeded(m, metricColumnX, metricRowYs[i])); const userNodesOut = replacedNodeName ? workflow.nodes.map((n) => this.rewriteExpressionsOnNode(n, replacedNodeName)) : workflow.nodes; const metricNodesOut = replacedNodeName ? metricNodes.map((n) => this.rewriteExpressionsOnNode(n, replacedNodeName)) : metricNodes; const nodes = [...userNodesOut, triggerNode, ...metricNodesOut, ...chatModelNodes]; const connections = this.rewireConnections({ original: workflow.connections, userTriggerEdge, entryNodeName, metrics: config.metrics, endNodeName: config.endNodeName, }); return { ...workflow, nodes, connections }; } findReplacedUpstreamNode(workflow, entryNodeName) { const upstreamNames = new Set(); for (const [fromNode, conn] of Object.entries(workflow.connections)) { for (const bucket of conn.main ?? []) { for (const edge of bucket ?? []) { if (edge?.node === entryNodeName) upstreamNames.add(fromNode); } } } if (upstreamNames.size !== 1) return null; const [name] = upstreamNames; return name; } computeMetricRowYs(metrics, baseY) { const ys = []; let y = baseY; for (const m of metrics) { ys.push(y); y += m.type === 'llm_judge' ? LLM_JUDGE_ROW_HEIGHT : EXPRESSION_ROW_HEIGHT; } return ys; } rewriteExpressionsOnNode(node, fromName) { return { ...node, parameters: rewriteExpressionRefs(node.parameters, fromName), }; } assertNoReservedNames(workflow) { const offender = workflow.nodes.find((n) => n.name.startsWith(RESERVED_PREFIX)); if (offender) { throw new n8n_workflow_1.UserError(`Node name "${offender.name}" uses the reserved "${RESERVED_PREFIX}" prefix`); } } resolveEntryNode(workflow, config) { if (config.startNodeName) { return config.startNodeName; } const triggerNames = workflow.nodes.filter((n) => this.isTrigger(n)).map((n) => n.name); const downstream = new Set(); for (const tName of triggerNames) { for (const bucket of workflow.connections[tName]?.main ?? []) { for (const edge of bucket ?? []) downstream.add(edge.node); } } if (downstream.size === 0) { throw new n8n_workflow_1.UserError('Cannot determine entry node: workflow has no trigger with a downstream connection'); } if (downstream.size > 1) { throw new n8n_workflow_1.UserError('Cannot auto-determine entry node: workflow trigger has multiple downstream nodes; set startNodeName explicitly'); } return [...downstream][0]; } isTrigger(node) { return /trigger|webhook|manual/i.test(node.type); } findUserTriggerEdgeTo(connections, entryNodeName) { for (const [fromNode, conn] of Object.entries(connections)) { const buckets = conn.main ?? []; for (let bIdx = 0; bIdx < buckets.length; bIdx++) { const edges = buckets[bIdx] ?? []; for (let eIdx = 0; eIdx < edges.length; eIdx++) { if (edges[eIdx]?.node === entryNodeName) { return { fromNode, fromBucketIndex: bIdx, edgeIndex: eIdx }; } } } } throw new n8n_workflow_1.UserError(`No incoming connection to entry node "${entryNodeName}"; cannot inject evaluation trigger`); } positionOf(workflow, nodeName) { const node = workflow.nodes.find((n) => n.name === nodeName); return node?.position; } buildEvaluationTriggerNode(config, entryPos) { const datasetRef = config.datasetSource === 'data_table' ? config.datasetRef : undefined; return { id: (0, nanoid_1.nanoid)(), name: TRIGGER_NAME, type: n8n_workflow_1.EVALUATION_TRIGGER_NODE_TYPE, typeVersion: 4.7, position: [entryPos[0] - NODE_STEP_X, entryPos[1]], parameters: { source: 'dataTable', dataTableId: datasetRef?.dataTableId ?? '', }, }; } buildMetricNode(metric, x, y) { return { id: (0, nanoid_1.nanoid)(), name: `__eval_metric_${metric.id}`, type: n8n_workflow_1.EVALUATION_NODE_TYPE, typeVersion: 4.7, position: [x, y], parameters: this.buildMetricNodeParameters(metric), }; } buildMetricNodeParameters(metric) { if (metric.type === 'expression') { if (metric.config.outputType === 'boolean' && !(0, evaluation_config_validator_1.isCoercibleBooleanExpression)(metric.config.expression)) { throw new n8n_workflow_1.UserError(`Metric "${metric.name}" expression cannot be coerced into a boolean (multi-segment templates are not supported)`); } const value = metric.config.outputType === 'boolean' ? coerceBooleanExpression(metric.config.expression) : metric.config.expression; return { operation: 'setMetrics', metric: 'customMetrics', metrics: { assignments: [{ id: (0, nanoid_1.nanoid)(), name: metric.name, value, type: 'number' }], }, }; } const { preset, prompt, inputs } = metric.config; return { operation: 'setMetrics', metric: preset, prompt, actualAnswer: inputs.actualAnswer, ...(preset === 'correctness' ? { expectedAnswer: inputs.expectedAnswer ?? '' } : {}), ...(preset === 'helpfulness' ? { userQuery: inputs.userQuery ?? '' } : {}), options: { metricName: metric.name, }, }; } buildChatModelNodeIfNeeded(metric, metricX, metricY) { if (metric.type !== 'llm_judge') return []; return [ { id: (0, nanoid_1.nanoid)(), name: `__eval_model_${metric.id}`, type: metric.config.provider, typeVersion: 1, position: [metricX, metricY + MODEL_OFFSET_Y], credentials: this.credentialsForProvider(metric.config.provider, metric.config.credentialId), parameters: { model: metric.config.model }, }, ]; } credentialsForProvider(provider, credentialId) { const entry = this.providerRegistry.get(provider); if (!entry || entry.credentialTypes.length === 0) { throw new n8n_workflow_1.UserError(`Unsupported LLM judge provider "${provider}"`); } const credentialType = entry.credentialTypes[0].name; return { [credentialType]: { id: credentialId, name: '' } }; } rewireConnections(args) { const out = (0, n8n_workflow_1.deepCopy)(args.original); const { fromNode, fromBucketIndex, edgeIndex } = args.userTriggerEdge; const buckets = out[fromNode]?.main; if (buckets?.[fromBucketIndex]) { buckets[fromBucketIndex].splice(edgeIndex, 1); if (buckets.every((b) => (b ?? []).length === 0)) { delete out[fromNode]; } } out[TRIGGER_NAME] = { main: [[{ node: args.entryNodeName, type: 'main', index: 0 }]], }; const metricEdges = args.metrics.map((m) => ({ node: `__eval_metric_${m.id}`, type: 'main', index: 0, })); const existingEnd = out[args.endNodeName] ?? {}; const existingMain = existingEnd.main ?? []; const bucketCount = Math.max(existingMain.length, 1); out[args.endNodeName] = { ...existingEnd, main: Array.from({ length: bucketCount }, () => [...metricEdges]), }; for (const m of args.metrics) { if (m.type !== 'llm_judge') continue; const modelNode = `__eval_model_${m.id}`; const metricNode = `__eval_metric_${m.id}`; out[modelNode] = { [n8n_workflow_1.NodeConnectionTypes.AiLanguageModel]: [ [{ node: metricNode, type: n8n_workflow_1.NodeConnectionTypes.AiLanguageModel, index: 0 }], ], }; } return out; } }; exports.WorkflowCompilerService = WorkflowCompilerService; exports.WorkflowCompilerService = WorkflowCompilerService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [llm_judge_provider_registry_1.LlmJudgeProviderRegistry]) ], WorkflowCompilerService); function coerceBooleanExpression(expression) { const match = expression.match(/^=\{\{([\s\S]*)\}\}$/); const inner = match ? match[1].trim() : JSON.stringify(expression); return `={{ (${inner}) ? 1 : 0 }}`; } function rewriteExpressionRefs(value, fromName) { const escaped = fromName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const pattern = new RegExp(`\\$\\(\\s*(['"])${escaped}\\1\\s*\\)`, 'g'); const replacement = `$("${TRIGGER_NAME}")`; const walk = (v) => { if (typeof v === 'string') return v.replace(pattern, replacement); if (Array.isArray(v)) return v.map(walk); if (v && typeof v === 'object') { const out = {}; for (const [k, child] of Object.entries(v)) out[k] = walk(child); return out; } return v; }; return walk(value); } //# sourceMappingURL=workflow-compiler.service.js.map