diff --git a/packages/frontend/editor-ui/src/features/assistant/builder.store.ts b/packages/frontend/editor-ui/src/features/assistant/builder.store.ts index 3dc773312a6..7cf950a72fb 100644 --- a/packages/frontend/editor-ui/src/features/assistant/builder.store.ts +++ b/packages/frontend/editor-ui/src/features/assistant/builder.store.ts @@ -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 ?? ''; diff --git a/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.test.ts b/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.test.ts index ac18d455b19..905199e7a23 100644 --- a/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.test.ts +++ b/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.test.ts @@ -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', () => { diff --git a/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.ts b/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.ts index 7b874d32ba1..4924ed43dc6 100644 --- a/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.ts +++ b/packages/frontend/editor-ui/src/features/assistant/composables/useAIAssistantHelpers.ts @@ -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; } diff --git a/packages/frontend/editor-ui/src/helpers/builderHelpers.ts b/packages/frontend/editor-ui/src/helpers/builderHelpers.ts index 977a6b5053f..4fc13cb8241 100644 --- a/packages/frontend/editor-ui/src/helpers/builderHelpers.ts +++ b/packages/frontend/editor-ui/src/helpers/builderHelpers.ts @@ -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) {