UNPKG

n8n

Version:

n8n Workflow Automation Tool

480 lines 22.5 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.EvalExecutionService = void 0; const api_types_1 = require("@n8n/api-types"); const backend_common_1 = require("@n8n/backend-common"); const di_1 = require("@n8n/di"); const workflow_sdk_1 = require("@n8n/workflow-sdk"); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const node_crypto_1 = require("node:crypto"); const node_types_1 = require("../../../node-types"); const posthog_1 = require("../../../posthog"); const workflow_execute_additional_data_1 = require("../../../workflow-execute-additional-data"); const workflow_finder_service_1 = require("../../../workflows/workflow-finder.service"); const eval_mocked_credentials_helper_1 = require("./eval-mocked-credentials-helper"); const llm_wire_server_1 = require("./llm-wire-server"); const mock_handler_1 = require("./mock-handler"); const pin_data_generator_1 = require("./pin-data-generator"); const proxy_loopback_1 = require("./proxy-loopback"); const workflow_analysis_1 = require("./workflow-analysis"); const MAX_OUTPUT_ITEMS_PER_BRANCH = 10; let EvalExecutionService = class EvalExecutionService { constructor(workflowFinderService, nodeTypes, logger, postHogClient, binaryDataService) { this.workflowFinderService = workflowFinderService; this.nodeTypes = nodeTypes; this.logger = logger; this.postHogClient = postHogClient; this.binaryDataService = binaryDataService; } async executeWithLlmMock(workflowId, user, options = {}) { const executionId = (0, node_crypto_1.randomUUID)(); const workflowEntity = await this.workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:execute', ]); if (!workflowEntity) { return this.errorResult(executionId, `Workflow ${workflowId} not found or not accessible`); } let partitioned; try { partitioned = (0, workflow_analysis_1.partitionAiRoots)(workflowEntity, options.pinNodes ?? []); } catch (error) { if (error instanceof n8n_workflow_1.UserError) { return this.errorResult(executionId, error.message); } throw error; } for (const entry of partitioned.autoPinned) { this.logger.debug(`[EvalMock] Auto-pinning AI root "${entry.root}" — sub-node "${entry.subNode}" (${entry.subNodeType}) is ${entry.reason}`); } let interceptionEnabled = false; let unpinNodes = partitioned.unpinNodes; if (unpinNodes.length > 0) { interceptionEnabled = await this.isInterceptionEnabled(user); if (!interceptionEnabled) { this.logger.warn('[EvalMock] Vendor SDK interception disabled by kill-switch — pinning all AI roots'); unpinNodes = []; } } const unpinSet = unpinNodes.length > 0 ? new Set(unpinNodes) : undefined; const hints = await this.analyzeWorkflow(workflowEntity, options.scenarioHints, unpinSet); const vendorLlmRouting = interceptionEnabled ? (0, workflow_analysis_1.buildVendorLlmRouting)(workflowEntity, unpinNodes) : undefined; return await this.execute(workflowEntity, user, executionId, hints, options.scenarioHints, interceptionEnabled, vendorLlmRouting); } async isInterceptionEnabled(user) { try { const flags = await this.postHogClient.getFeatureFlags(user); return flags?.[api_types_1.EVAL_VENDOR_SDK_INTERCEPTION_FLAG] !== false; } catch (error) { this.logger.warn('[EvalMock] Failed to resolve vendor-SDK interception flag', { error: error instanceof Error ? error.message : String(error), }); return false; } } async analyzeWorkflow(workflowEntity, scenarioHints, unpinSet) { const hintNodes = (0, workflow_analysis_1.identifyNodesForHints)(workflowEntity); const nodeNames = hintNodes.map((n) => n.name); this.logger.debug(`[EvalMock] Generating hints for ${nodeNames.length} nodes: ${nodeNames.join(', ')}`); const hints = await (0, workflow_analysis_1.generateMockHints)({ workflow: workflowEntity, nodeNames, scenarioHints, }); if (!hints.globalContext && nodeNames.length > 0) { this.logger.warn('[EvalMock] Phase 1 hint generation returned empty — mock responses will lack cross-node consistency'); } this.logger.debug(`[EvalMock] Phase 1 result — globalContext: ${hints.globalContext ? 'present' : 'EMPTY'}, triggerContent keys: ${JSON.stringify(Object.keys(hints.triggerContent))}, nodeHints: ${Object.keys(hints.nodeHints).join(', ')}`); const bypassNodes = (0, workflow_analysis_1.identifyNodesForPinData)(workflowEntity, unpinSet); const bypassNodeNames = bypassNodes.map((n) => n.name); if (bypassNodeNames.length > 0) { this.logger.debug(`[EvalMock] Generating pin data for ${bypassNodeNames.length} bypass nodes: ${bypassNodeNames.join(', ')}`); hints.bypassPinData = await this.generateBypassPinData(workflowEntity, bypassNodeNames, hints.globalContext, scenarioHints); this.logger.debug(`[EvalMock] Phase 1.5 result — pinned nodes: ${Object.keys(hints.bypassPinData).join(', ') || 'none'}`); } return hints; } async generateBypassPinData(workflowEntity, bypassNodeNames, globalContext, scenarioHints) { if (bypassNodeNames.length === 0) return {}; try { const dataDescription = [globalContext, scenarioHints].filter(Boolean).join('\n\n'); const result = await (0, pin_data_generator_1.generatePinData)({ workflow: workflowEntity, nodeNames: bypassNodeNames, instructions: dataDescription ? { dataDescription } : undefined, }); return (0, workflow_sdk_1.normalizePinData)(result); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); this.logger.error(`[EvalMock] Phase 1.5 pin data generation failed: ${errorMsg}`); return (0, workflow_sdk_1.normalizePinData)(Object.fromEntries(bypassNodeNames.map((nodeName) => [nodeName, [{ json: {} }]]))); } } async execute(workflowEntity, user, executionId, hints, scenarioHints, interceptionEnabled = false, vendorLlmRouting) { const nodeResults = {}; const workflow = this.buildWorkflow(workflowEntity); const startNode = this.findStartNode(workflow); if (!startNode) { return this.errorResult(executionId, 'No trigger or start node found in the workflow'); } const mockHandler = (0, mock_handler_1.createLlmMockHandler)({ scenarioHints, globalContext: hints.globalContext, nodeHints: hints.nodeHints, }); const additionalData = await (0, workflow_execute_additional_data_1.getBase)({ userId: user.id, workflowId: workflowEntity.id, workflowSettings: workflowEntity.settings ?? {}, }); let wireServer; let restoreNoProxy; let credentialsHelper; try { let serverUrl; if (interceptionEnabled) { wireServer = new llm_wire_server_1.LlmWireServer({ mockHandler, rootToSubNode: vendorLlmRouting?.rootToSubNode, onIntercept: (turn) => this.recordWireServerTurn(turn, nodeResults), logger: this.logger, }); serverUrl = await wireServer.start(); restoreNoProxy = (0, proxy_loopback_1.patchNoProxyForLoopback)(); this.logger.debug(`[EvalMock] Wire server listening at ${serverUrl}`); } credentialsHelper = new eval_mocked_credentials_helper_1.EvalMockedCredentialsHelper(additionalData.credentialsHelper, serverUrl, this.logger, vendorLlmRouting?.subNodeToRoot); additionalData.credentialsHelper = credentialsHelper; additionalData.evalLlmMockHandler = this.createInterceptingHandler(mockHandler, nodeResults); additionalData.hooks = new n8n_core_1.ExecutionLifecycleHooks('evaluation', executionId, workflowEntity); const binaryRequirement = (0, workflow_analysis_1.detectBinaryDependencies)(workflowEntity); const triggerPinData = this.buildTriggerPinData(startNode, hints.triggerContent, binaryRequirement); const pinData = { ...triggerPinData, ...hints.bypassPinData }; const pinDataNodeNames = Object.keys(pinData); this.checkNodeConfig(workflow, nodeResults, pinDataNodeNames); const executionData = this.buildExecutionData(startNode, pinData); if (Object.keys(triggerPinData).length > 0) { this.markNodeAsPinned(startNode.name, nodeResults); } for (const nodeName of Object.keys(hints.bypassPinData)) { this.markNodeAsPinned(nodeName, nodeResults); } const result = await this.runWorkflow(workflow, additionalData, executionData); return await this.buildResult(executionId, result, nodeResults, hints, credentialsHelper); } catch (error) { return this.buildPartialFailureResult(executionId, error, nodeResults, hints, credentialsHelper); } finally { if (restoreNoProxy) restoreNoProxy(); if (wireServer) { try { await wireServer.stop(); } catch (error) { this.logger.warn('[EvalMock] Wire server teardown failed', { error: error instanceof Error ? error.message : String(error), }); } } } } buildWorkflow(workflowEntity) { return new n8n_workflow_1.Workflow({ id: workflowEntity.id, name: workflowEntity.name, nodes: workflowEntity.nodes, connections: workflowEntity.connections, active: false, nodeTypes: this.nodeTypes, staticData: workflowEntity.staticData, settings: workflowEntity.settings ?? {}, }); } findStartNode(workflow) { return workflow.getStartNode() ?? this.findWebhookNode(workflow); } findWebhookNode(workflow) { return Object.values(workflow.nodes).find((node) => { if (node.disabled) return false; const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); return nodeType !== undefined && 'webhook' in nodeType; }); } checkNodeConfig(workflow, nodeResults, pinDataNodeNames) { for (const node of Object.values(workflow.nodes)) { if (node.disabled) continue; const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); if (!nodeType) continue; const issues = n8n_workflow_1.NodeHelpers.getNodeParametersIssues(nodeType.description.properties, node, nodeType.description, pinDataNodeNames); if (issues?.parameters && Object.keys(issues.parameters).length > 0) { const entry = (nodeResults[node.name] ??= { outputs: {}, outputCount: 0, iterationCount: 0, interceptedRequests: [], executionMode: 'real', }); entry.configIssues = issues.parameters; } } } buildTriggerPinData(startNode, triggerContent, binaryRequirement) { const classifyBinaryFileType = (contentType) => { const lc = contentType.toLowerCase(); if (lc.startsWith('image/')) return 'image'; if (lc.startsWith('audio/')) return 'audio'; if (lc.startsWith('video/')) return 'video'; if (lc === 'application/pdf') return 'pdf'; if (lc.startsWith('text/html')) return 'html'; if (lc === 'application/json' || lc.startsWith('text/json')) return 'json'; if (lc.startsWith('text/')) return 'text'; return undefined; }; if (Object.keys(triggerContent).length === 0 && !binaryRequirement) return {}; const item = { json: triggerContent }; if (binaryRequirement) { const bytes = (0, n8n_core_1.synthesizeBinaryFixture)(binaryRequirement.contentType, binaryRequirement.filename); const extension = binaryRequirement.filename.includes('.') ? binaryRequirement.filename.slice(binaryRequirement.filename.lastIndexOf('.') + 1) : 'bin'; const binary = { [binaryRequirement.propertyName]: { mimeType: binaryRequirement.contentType, fileName: binaryRequirement.filename, fileExtension: extension, fileType: classifyBinaryFileType(binaryRequirement.contentType), data: bytes.toString('base64'), }, }; item.binary = binary; } return { [startNode.name]: [item] }; } buildExecutionData(startNode, pinData) { return (0, n8n_workflow_1.createRunExecutionData)({ startData: {}, resultData: { pinData, runData: {} }, executionData: { contextData: {}, metadata: {}, nodeExecutionStack: [ { node: startNode, data: { main: [[{ json: {} }]] }, source: null, }, ], waitingExecution: {}, waitingExecutionSource: {}, }, }); } async runWorkflow(workflow, additionalData, executionData) { const workflowExecute = new n8n_core_1.WorkflowExecute(additionalData, 'evaluation', executionData); return await workflowExecute.processRunExecutionData(workflow); } recordWireServerTurn(turn, nodeResults) { const entry = (nodeResults[turn.rootName] ??= { outputs: {}, outputCount: 0, iterationCount: 0, interceptedRequests: [], executionMode: 'mocked', }); if (entry.executionMode !== 'pinned') { entry.executionMode = 'mocked'; } entry.interceptedRequests.push({ url: turn.url, method: turn.method, nodeType: turn.nodeType, requestBody: turn.requestBody, mockResponse: turn.mockResponse, }); this.logger.debug(`[EvalMock] Wire server intercepted ${turn.method} ${turn.url} attributed to root "${turn.rootName}"`); } createInterceptingHandler(mockHandler, nodeResults) { return async (requestOptions, node) => { const entry = (nodeResults[node.name] ??= { outputs: {}, outputCount: 0, iterationCount: 0, interceptedRequests: [], executionMode: 'mocked', }); entry.executionMode = 'mocked'; const response = await mockHandler(requestOptions, node); entry.interceptedRequests.push({ url: requestOptions.url, method: requestOptions.method ?? 'GET', nodeType: node.type, requestBody: requestOptions.body, mockResponse: response?.body, }); this.logger.debug(`[EvalMock] Intercepted ${requestOptions.method ?? 'GET'} ${requestOptions.url} from "${node.name}" (${node.type})`); return response; }; } markNodeAsPinned(nodeName, nodeResults) { const existing = nodeResults[nodeName]; nodeResults[nodeName] = { outputs: {}, outputCount: 0, iterationCount: 0, interceptedRequests: [], executionMode: 'pinned', ...(existing?.configIssues ? { configIssues: existing.configIssues } : {}), }; } buildPartialFailureResult(executionId, error, nodeResults, hints, credentialsHelper) { const message = error instanceof Error ? error.message : String(error); this.logger.error(`[EvalMock] Workflow execution failed: ${message}`); return { executionId, success: false, nodeResults, errors: [`Execution failed: ${message}`], hints, mockedCredentials: credentialsHelper?.mockedCredentials ?? [], rewrittenCredentials: credentialsHelper?.rewrittenCredentials ?? [], }; } async hydrateBinaryData(items) { return await Promise.all(items.map(async (item) => { if (!item.binary) return item; const hydratedBinary = {}; for (const [key, entry] of Object.entries(item.binary)) { if (entry.id) { try { const buffer = await this.binaryDataService.getAsBuffer(entry); hydratedBinary[key] = { ...entry, data: buffer.toString('base64') }; continue; } catch (error) { this.logger.warn(`[EvalMock] Failed to hydrate binary "${key}" (${entry.id}): ${error instanceof Error ? error.message : String(error)}`); } } hydratedBinary[key] = entry; } return { ...item, binary: hydratedBinary }; })); } async buildResult(executionId, result, nodeResults, hints, credentialsHelper) { const errors = []; const runData = result.data?.resultData?.runData ?? {}; for (const [nodeName, nodeRuns] of Object.entries(runData)) { const entry = (nodeResults[nodeName] ??= { outputs: {}, outputCount: 0, iterationCount: 0, interceptedRequests: [], executionMode: 'real', }); entry.iterationCount = nodeRuns.length; const firstErrorIdx = nodeRuns.findIndex((run) => run?.error !== undefined); if (firstErrorIdx !== -1) { entry.firstErrorIteration = firstErrorIdx; } const lastRun = nodeRuns[nodeRuns.length - 1]; if (lastRun?.startTime) { entry.startTime = lastRun.startTime; } if (lastRun?.data) { let totalCount = 0; let truncated = false; const outputs = {}; for (const [connectionType, branches] of Object.entries(lastRun.data)) { if (!Array.isArray(branches)) continue; outputs[connectionType] = await Promise.all(branches.map(async (branch) => { if (!Array.isArray(branch)) return []; totalCount += branch.length; let kept = branch; if (branch.length > MAX_OUTPUT_ITEMS_PER_BRANCH) { truncated = true; kept = branch.slice(0, MAX_OUTPUT_ITEMS_PER_BRANCH); } return await this.hydrateBinaryData(kept); })); } entry.outputs = outputs; entry.outputCount = totalCount; if (truncated) entry.truncated = true; } if (lastRun?.error) { errors.push(`Node "${nodeName}": ${lastRun.error.message}`); } } const executionError = result.data?.resultData?.error; if (executionError) { errors.push(`Workflow error: ${executionError.message}`); } return { executionId, success: executionError === undefined && errors.length === 0, nodeResults, errors, hints, mockedCredentials: credentialsHelper.mockedCredentials, rewrittenCredentials: credentialsHelper.rewrittenCredentials, }; } errorResult(executionId, message) { return { executionId, success: false, nodeResults: {}, errors: [message], hints: { globalContext: '', triggerContent: {}, nodeHints: {}, warnings: [], bypassPinData: {}, }, mockedCredentials: [], rewrittenCredentials: [], }; } }; exports.EvalExecutionService = EvalExecutionService; exports.EvalExecutionService = EvalExecutionService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [workflow_finder_service_1.WorkflowFinderService, node_types_1.NodeTypes, backend_common_1.Logger, posthog_1.PostHogClient, n8n_core_1.BinaryDataService]) ], EvalExecutionService); //# sourceMappingURL=execution.service.js.map