fix(editor): Compact large ITaskDataConnections before sending to AI Builder (#20545)

This commit is contained in:
oleg 2025-10-09 13:07:57 +02:00 committed by GitHub
parent ac3efc5685
commit e58480f901
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 245 additions and 4 deletions

View File

@ -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 ?? '';

View File

@ -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', () => {

View File

@ -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;
}

View File

@ -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) {