n8n
Version:
n8n Workflow Automation Tool
290 lines • 13.7 kB
JavaScript
;
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 backend_common_1 = require("@n8n/backend-common");
const di_1 = require("@n8n/di");
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 workflow_execute_additional_data_1 = require("../../../workflow-execute-additional-data");
const workflow_finder_service_1 = require("../../../workflows/workflow-finder.service");
const workflow_sdk_1 = require("@n8n/workflow-sdk");
const pin_data_generator_1 = require("./pin-data-generator");
const workflow_analysis_1 = require("./workflow-analysis");
const mock_handler_1 = require("./mock-handler");
const MAX_OUTPUT_ITEMS_PER_NODE = 10;
let EvalExecutionService = class EvalExecutionService {
constructor(workflowFinderService, nodeTypes, logger) {
this.workflowFinderService = workflowFinderService;
this.nodeTypes = nodeTypes;
this.logger = logger;
}
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`);
}
const hints = await this.analyzeWorkflow(workflowEntity, options.scenarioHints);
return await this.execute(workflowEntity, user, executionId, hints, options.scenarioHints);
}
async analyzeWorkflow(workflowEntity, scenarioHints) {
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);
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) {
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 ?? {},
});
additionalData.evalLlmMockHandler = this.createInterceptingHandler(mockHandler, nodeResults);
additionalData.hooks = new n8n_core_1.ExecutionLifecycleHooks('evaluation', executionId, workflowEntity);
const triggerPinData = this.buildTriggerPinData(startNode, hints.triggerContent);
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) {
const existing = nodeResults[startNode.name];
nodeResults[startNode.name] = {
output: null,
interceptedRequests: [],
executionMode: 'pinned',
...(existing?.configIssues ? { configIssues: existing.configIssues } : {}),
};
}
for (const nodeName of Object.keys(hints.bypassPinData)) {
const existing = nodeResults[nodeName];
nodeResults[nodeName] = {
output: null,
interceptedRequests: [],
executionMode: 'pinned',
...(existing?.configIssues ? { configIssues: existing.configIssues } : {}),
};
}
try {
const result = await this.runWorkflow(workflow, additionalData, executionData);
return this.buildResult(executionId, result, nodeResults, hints);
}
catch (error) {
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,
};
}
}
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] ??= {
output: null,
interceptedRequests: [],
executionMode: 'real',
});
entry.configIssues = issues.parameters;
}
}
}
buildTriggerPinData(startNode, triggerContent) {
if (Object.keys(triggerContent).length === 0)
return {};
return { [startNode.name]: [{ json: triggerContent }] };
}
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);
}
createInterceptingHandler(mockHandler, nodeResults) {
return async (requestOptions, node) => {
const entry = (nodeResults[node.name] ??= {
output: null,
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;
};
}
buildResult(executionId, result, nodeResults, hints) {
const errors = [];
const runData = result.data?.resultData?.runData ?? {};
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
const entry = (nodeResults[nodeName] ??= {
output: null,
interceptedRequests: [],
executionMode: 'real',
});
const lastRun = nodeRuns[nodeRuns.length - 1];
if (lastRun?.startTime) {
entry.startTime = lastRun.startTime;
}
if (lastRun?.data?.main) {
const flattened = lastRun.data.main.flat().filter(Boolean);
entry.outputCount = flattened.length;
const allOutputs = flattened.slice(0, MAX_OUTPUT_ITEMS_PER_NODE);
if (allOutputs.length > 0) {
entry.output = allOutputs;
}
}
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,
};
}
errorResult(executionId, message) {
return {
executionId,
success: false,
nodeResults: {},
errors: [message],
hints: {
globalContext: '',
triggerContent: {},
nodeHints: {},
warnings: [],
bypassPinData: {},
},
};
}
};
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])
], EvalExecutionService);
//# sourceMappingURL=execution.service.js.map