mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 01:07:04 +02:00
fix(editor): Compact large ITaskDataConnections before sending to AI Builder (#20545)
This commit is contained in:
parent
ac3efc5685
commit
e58480f901
|
|
@ -285,8 +285,16 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
|
|||
};
|
||||
|
||||
if (type === 'execution') {
|
||||
const resultData = JSON.stringify(workflowsStore.workflowExecutionData ?? {});
|
||||
const resultDataSizeKb = stringSizeInBytes(resultData) / 1024;
|
||||
let resultData = '{}';
|
||||
let resultDataSizeKb = 0;
|
||||
|
||||
try {
|
||||
resultData = JSON.stringify(workflowsStore.workflowExecutionData ?? {});
|
||||
resultDataSizeKb = stringSizeInBytes(resultData) / 1024;
|
||||
} catch (error) {
|
||||
// Handle circular structure errors gracefully
|
||||
console.warn('Failed to stringify execution data for telemetry:', error);
|
||||
}
|
||||
|
||||
trackingPayload.execution_data = resultDataSizeKb > 512 ? '{}' : resultData;
|
||||
trackingPayload.execution_status = executionStatus ?? '';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
deepCopy,
|
||||
type IDataObject,
|
||||
type ITaskDataConnections,
|
||||
type INode,
|
||||
type IRunExecutionData,
|
||||
type NodeConnectionType,
|
||||
|
|
@ -539,6 +541,11 @@ describe('Simplify assistant payloads', () => {
|
|||
aiAssistantHelpers = useAIAssistantHelpers();
|
||||
});
|
||||
|
||||
// Helper to create properly typed inputOverride objects
|
||||
const createInputOverride = (data: IDataObject): ITaskDataConnections => ({
|
||||
main: [[{ json: data }]],
|
||||
});
|
||||
|
||||
it('simplifyWorkflowForAssistant: Should remove unnecessary properties from workflow object', () => {
|
||||
const simplifiedWorkflow = aiAssistantHelpers.simplifyWorkflowForAssistant(testWorkflow);
|
||||
const removedProperties = [
|
||||
|
|
@ -563,6 +570,198 @@ describe('Simplify assistant payloads', () => {
|
|||
expect(simplifiedResultData.runData[nodeName][0]).not.toHaveProperty('data');
|
||||
}
|
||||
});
|
||||
|
||||
it('simplifyResultData: Should not modify inputOverride when compact is false', () => {
|
||||
const largeInputOverride = createInputOverride({ someData: 'x'.repeat(3000) });
|
||||
const executionData: IRunExecutionData['resultData'] = {
|
||||
runData: {
|
||||
TestNode: [
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780588,
|
||||
executionIndex: 0,
|
||||
executionTime: 4,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
inputOverride: largeInputOverride,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
pinData: {},
|
||||
};
|
||||
|
||||
const simplifiedResultData = aiAssistantHelpers.simplifyResultData(executionData);
|
||||
expect(simplifiedResultData.runData.TestNode[0].inputOverride).toEqual(largeInputOverride);
|
||||
});
|
||||
|
||||
it('simplifyResultData: Should not truncate small inputOverride when compact is true', () => {
|
||||
const smallInputOverride = createInputOverride({ someData: 'small data' });
|
||||
const executionData: IRunExecutionData['resultData'] = {
|
||||
runData: {
|
||||
TestNode: [
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780588,
|
||||
executionIndex: 0,
|
||||
executionTime: 4,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
inputOverride: smallInputOverride,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
pinData: {},
|
||||
};
|
||||
|
||||
const simplifiedResultData = aiAssistantHelpers.simplifyResultData(executionData, {
|
||||
compact: true,
|
||||
});
|
||||
expect(simplifiedResultData.runData.TestNode[0].inputOverride).toEqual(smallInputOverride);
|
||||
});
|
||||
|
||||
it('simplifyResultData: Should remove large inputOverride when compact is true', () => {
|
||||
const largeInputOverride = createInputOverride({ someData: 'x'.repeat(3000) });
|
||||
const executionData: IRunExecutionData['resultData'] = {
|
||||
runData: {
|
||||
TestNode: [
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780588,
|
||||
executionIndex: 0,
|
||||
executionTime: 4,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
inputOverride: largeInputOverride,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
pinData: {},
|
||||
};
|
||||
|
||||
const simplifiedResultData = aiAssistantHelpers.simplifyResultData(executionData, {
|
||||
compact: true,
|
||||
});
|
||||
|
||||
// Large inputOverride should be removed entirely to maintain type safety
|
||||
expect(simplifiedResultData.runData.TestNode[0].inputOverride).toBeUndefined();
|
||||
});
|
||||
|
||||
it('simplifyResultData: Should handle multiple nodes with different inputOverride sizes', () => {
|
||||
const smallInput = createInputOverride({ data: 'small' });
|
||||
const largeInput = createInputOverride({ data: 'x'.repeat(3000) });
|
||||
const executionData: IRunExecutionData['resultData'] = {
|
||||
runData: {
|
||||
SmallNode: [
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780588,
|
||||
executionIndex: 0,
|
||||
executionTime: 4,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
inputOverride: smallInput,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
LargeNode: [
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780589,
|
||||
executionIndex: 1,
|
||||
executionTime: 5,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
inputOverride: largeInput,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
NoInputNode: [
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780590,
|
||||
executionIndex: 2,
|
||||
executionTime: 3,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
pinData: {},
|
||||
};
|
||||
|
||||
const simplifiedResultData = aiAssistantHelpers.simplifyResultData(executionData, {
|
||||
compact: true,
|
||||
});
|
||||
|
||||
// Small input should not be removed
|
||||
expect(simplifiedResultData.runData.SmallNode[0].inputOverride).toEqual(smallInput);
|
||||
|
||||
// Large input should be removed entirely
|
||||
expect(simplifiedResultData.runData.LargeNode[0].inputOverride).toBeUndefined();
|
||||
|
||||
// Node without inputOverride should not have it added
|
||||
expect(simplifiedResultData.runData.NoInputNode[0]).not.toHaveProperty('inputOverride');
|
||||
});
|
||||
|
||||
it('simplifyResultData: Should handle multiple task data entries for the same node', () => {
|
||||
const largeInput1 = createInputOverride({ data: 'x'.repeat(3000) });
|
||||
const largeInput2 = createInputOverride({ data: 'y'.repeat(3000) });
|
||||
const executionData: IRunExecutionData['resultData'] = {
|
||||
runData: {
|
||||
TestNode: [
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780588,
|
||||
executionIndex: 0,
|
||||
executionTime: 4,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
inputOverride: largeInput1,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
hints: [],
|
||||
startTime: 1732882780589,
|
||||
executionIndex: 1,
|
||||
executionTime: 5,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
inputOverride: largeInput2,
|
||||
data: {
|
||||
main: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
pinData: {},
|
||||
};
|
||||
|
||||
const simplifiedResultData = aiAssistantHelpers.simplifyResultData(executionData, {
|
||||
compact: true,
|
||||
});
|
||||
|
||||
// Both entries should have inputOverride removed
|
||||
expect(simplifiedResultData.runData.TestNode[0].inputOverride).toBeUndefined();
|
||||
expect(simplifiedResultData.runData.TestNode[1].inputOverride).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trim Payload Size', () => {
|
||||
|
|
|
|||
|
|
@ -217,10 +217,15 @@ export const useAIAssistantHelpers = () => {
|
|||
/**
|
||||
* Prepare workflow execution result data for the AI assistant
|
||||
* by removing data from nodes
|
||||
* @param data The execution result data to simplify
|
||||
* @param options Options for simplification
|
||||
* @param options.compact If true, removes large inputOverride fields (> 2000 bytes)
|
||||
**/
|
||||
function simplifyResultData(
|
||||
data: IRunExecutionData['resultData'],
|
||||
options: { compact?: boolean } = {},
|
||||
): ChatRequest.ExecutionResultData {
|
||||
const { compact = false } = options;
|
||||
const simplifiedResultData: ChatRequest.ExecutionResultData = {
|
||||
runData: {},
|
||||
};
|
||||
|
|
@ -229,22 +234,49 @@ export const useAIAssistantHelpers = () => {
|
|||
if (data.error) {
|
||||
simplifiedResultData.error = data.error;
|
||||
}
|
||||
|
||||
// Early return if runData is not present
|
||||
if (!data.runData) {
|
||||
return simplifiedResultData;
|
||||
}
|
||||
|
||||
// Map runData, excluding the `data` field from ITaskData
|
||||
for (const key of Object.keys(data.runData)) {
|
||||
const taskDataArray = data.runData[key];
|
||||
simplifiedResultData.runData[key] = taskDataArray.map((taskData) => {
|
||||
const { data: taskDataContent, ...taskDataWithoutData } = taskData;
|
||||
const { data: _taskDataContent, ...taskDataWithoutData } = taskData;
|
||||
|
||||
// If compact mode is enabled, remove large inputOverride fields
|
||||
if (compact && taskDataWithoutData.inputOverride) {
|
||||
try {
|
||||
const inputOverrideStr = JSON.stringify(taskDataWithoutData.inputOverride);
|
||||
const sizeInBytes = new Blob([inputOverrideStr]).size;
|
||||
|
||||
// If too large, remove inputOverride entirely to maintain type safety
|
||||
if (sizeInBytes > 2000) {
|
||||
delete taskDataWithoutData.inputOverride;
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle circular references or non-serializable data
|
||||
// Remove the problematic field entirely
|
||||
delete taskDataWithoutData.inputOverride;
|
||||
}
|
||||
}
|
||||
|
||||
return taskDataWithoutData;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle lastNodeExecuted if it exists
|
||||
if (data.lastNodeExecuted) {
|
||||
simplifiedResultData.lastNodeExecuted = data.lastNodeExecuted;
|
||||
}
|
||||
|
||||
// Handle metadata if it exists
|
||||
if (data.metadata) {
|
||||
simplifiedResultData.metadata = data.metadata;
|
||||
}
|
||||
|
||||
return simplifiedResultData;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ export function createBuilderPayload(
|
|||
}
|
||||
|
||||
if (options.executionData) {
|
||||
workflowContext.executionData = assistantHelpers.simplifyResultData(options.executionData);
|
||||
workflowContext.executionData = assistantHelpers.simplifyResultData(options.executionData, {
|
||||
compact: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.nodesForSchema?.length) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user