diff --git a/packages/cli/src/chat/__tests__/utils.test.ts b/packages/cli/src/chat/__tests__/utils.test.ts index 8fab34c7442..78843405311 100644 --- a/packages/cli/src/chat/__tests__/utils.test.ts +++ b/packages/cli/src/chat/__tests__/utils.test.ts @@ -146,6 +146,51 @@ describe('getMessage', () => { const result = getMessage(execution); expect(result).toBeUndefined(); }); + + it('should return message from the second output branch when first is empty', () => { + const execution = createMockExecution({}, undefined, [ + { + data: { + main: [ + [], // First output branch is empty + [ + { + json: { test: 'data' }, + sendMessage: 'Message from second branch', + }, + ], // Second output branch has the message + ], + }, + }, + ]); + const result = getMessage(execution); + expect(result).toBe('Message from second branch'); + }); + + it('should prioritize message from the first branch when multiple branches have messages', () => { + const execution = createMockExecution({}, undefined, [ + { + data: { + main: [ + [ + { + json: { test: 'data1' }, + sendMessage: 'Message from first branch', + }, + ], + [ + { + json: { test: 'data2' }, + sendMessage: 'Message from second branch', + }, + ], + ], + }, + }, + ]); + const result = getMessage(execution); + expect(result).toBe('Message from first branch'); + }); }); describe('getLastNodeExecuted', () => { diff --git a/packages/cli/src/chat/utils.ts b/packages/cli/src/chat/utils.ts index c189ac34b89..8662d775212 100644 --- a/packages/cli/src/chat/utils.ts +++ b/packages/cli/src/chat/utils.ts @@ -10,9 +10,18 @@ export function getMessage(execution: IExecutionResponse) { if (typeof lastNodeExecuted !== 'string') return undefined; const runIndex = execution.data.resultData.runData[lastNodeExecuted].length - 1; - const nodeExecutionData = - execution.data.resultData.runData[lastNodeExecuted][runIndex]?.data?.main?.[0]; - return nodeExecutionData?.[0] ? nodeExecutionData[0].sendMessage : undefined; + const mainOutputs = execution.data.resultData.runData[lastNodeExecuted][runIndex]?.data?.main; + + // Check all main output branches for a message + if (mainOutputs && Array.isArray(mainOutputs)) { + for (const branch of mainOutputs) { + if (branch && Array.isArray(branch) && branch.length > 0 && branch[0].sendMessage) { + return branch[0].sendMessage; + } + } + } + + return undefined; } /** diff --git a/packages/cli/src/webhooks/__tests__/webhook-last-node-response-extractor.test.ts b/packages/cli/src/webhooks/__tests__/webhook-last-node-response-extractor.test.ts index 83da033e476..d17568d760a 100644 --- a/packages/cli/src/webhooks/__tests__/webhook-last-node-response-extractor.test.ts +++ b/packages/cli/src/webhooks/__tests__/webhook-last-node-response-extractor.test.ts @@ -116,6 +116,47 @@ describe('extractWebhookLastNodeResponse', () => { }, }); }); + + it('should return data from second branch when first is empty', async () => { + const jsonData = { foo: 'bar', fromSecondBranch: true }; + lastNodeTaskData.data = { + main: [ + [], // First branch is empty + [{ json: jsonData }], // Second branch has data + ], + }; + + const result = await extractWebhookLastNodeResponse( + context, + 'firstEntryJson', + lastNodeTaskData, + ); + + expect(result).toEqual({ + ok: true, + result: { + type: 'static', + body: jsonData, + contentType: undefined, + }, + }); + }); + + it('should return error when all branches are empty', async () => { + lastNodeTaskData.data = { + main: [[], [], []], + }; + + const result = await extractWebhookLastNodeResponse( + context, + 'firstEntryJson', + lastNodeTaskData, + ); + + assert(!result.ok); + expect(result.error).toBeInstanceOf(OperationalError); + expect(result.error.message).toBe('No item to return was found'); + }); }); describe('responseDataType: firstEntryBinary', () => { @@ -288,6 +329,56 @@ describe('extractWebhookLastNodeResponse', () => { "The binary property 'nonExistentProperty' which should be returned does not exist", ); }); + + it('should return binary data from second branch when first is empty', async () => { + const binaryData: IBinaryData = { + data: Buffer.from('binary from second branch').toString(BINARY_ENCODING), + mimeType: 'text/plain', + }; + const nodeExecutionData: INodeExecutionData = { + json: {}, + binary: { data: binaryData }, + }; + lastNodeTaskData.data = { + main: [ + [], // First branch is empty + [nodeExecutionData], // Second branch has binary data + ], + }; + + context.evaluateSimpleWebhookDescriptionExpression.mockReturnValue('data'); + + const result = await extractWebhookLastNodeResponse( + context, + 'firstEntryBinary', + lastNodeTaskData, + ); + + expect(result).toEqual({ + ok: true, + result: { + type: 'static', + body: Buffer.from('binary from second branch'), + contentType: 'text/plain', + }, + }); + }); + + it('should return error when all branches are empty for binary', async () => { + lastNodeTaskData.data = { + main: [[], [], []], + }; + + const result = await extractWebhookLastNodeResponse( + context, + 'firstEntryBinary', + lastNodeTaskData, + ); + + assert(!result.ok); + expect(result.error).toBeInstanceOf(OperationalError); + expect(result.error.message).toBe('No item was found to return'); + }); }); describe('responseDataType: noData', () => { @@ -342,5 +433,68 @@ describe('extractWebhookLastNodeResponse', () => { }, }); }); + + it('should return all entries from second branch when first is empty', async () => { + const jsonData1 = { item: 1, fromSecondBranch: true }; + const jsonData2 = { item: 2, fromSecondBranch: true }; + const jsonData3 = { item: 3, fromSecondBranch: true }; + lastNodeTaskData.data = { + main: [ + [], // First branch is empty + [{ json: jsonData1 }, { json: jsonData2 }, { json: jsonData3 }], // Second branch has data + ], + }; + + const result = await extractWebhookLastNodeResponse(context, 'allEntries', lastNodeTaskData); + + expect(result).toEqual({ + ok: true, + result: { + type: 'static', + body: [jsonData1, jsonData2, jsonData3], + contentType: undefined, + }, + }); + }); + + it('should return entries from first non-empty branch only', async () => { + const branch2Data = { item: 'from-second' }; + const branch3Data = { item: 'from-third' }; + lastNodeTaskData.data = { + main: [ + [], // First branch is empty + [{ json: branch2Data }], // Second branch has data - this should be used + [{ json: branch3Data }], // Third branch also has data - should be ignored + ], + }; + + const result = await extractWebhookLastNodeResponse(context, 'allEntries', lastNodeTaskData); + + expect(result).toEqual({ + ok: true, + result: { + type: 'static', + body: [branch2Data], // Only data from second branch + contentType: undefined, + }, + }); + }); + + it('should return empty array when all branches are empty', async () => { + lastNodeTaskData.data = { + main: [[], [], []], + }; + + const result = await extractWebhookLastNodeResponse(context, 'allEntries', lastNodeTaskData); + + expect(result).toEqual({ + ok: true, + result: { + type: 'static', + body: [], + contentType: undefined, + }, + }); + }); }); }); diff --git a/packages/cli/src/webhooks/webhook-last-node-response-extractor.ts b/packages/cli/src/webhooks/webhook-last-node-response-extractor.ts index 4fafbddb725..e99a78b4b4e 100644 --- a/packages/cli/src/webhooks/webhook-last-node-response-extractor.ts +++ b/packages/cli/src/webhooks/webhook-last-node-response-extractor.ts @@ -50,17 +50,29 @@ export async function extractWebhookLastNodeResponse( } /** - * Extracts the JSON data of the first item of the last node + * Extracts the JSON data of the first item of the first non-empty branch of the last node */ function extractFirstEntryJsonFromTaskData( context: WebhookExecutionContext, lastNodeTaskData: ITaskData, ): Result { - if (lastNodeTaskData.data!.main[0]![0] === undefined) { + const mainOutputs = lastNodeTaskData.data?.main; + let firstItem: INodeExecutionData | undefined; + + if (mainOutputs && Array.isArray(mainOutputs)) { + for (const branch of mainOutputs) { + if (branch && Array.isArray(branch) && branch.length > 0) { + firstItem = branch[0]; + break; // Stop after processing the first non-empty branch + } + } + } + + if (firstItem === undefined) { return createResultError(new OperationalError('No item to return was found')); } - let lastNodeFirstJsonItem: unknown = lastNodeTaskData.data!.main[0]![0].json; + let lastNodeFirstJsonItem: unknown = firstItem.json; const responsePropertyName = context.evaluateSimpleWebhookDescriptionExpression('responsePropertyName'); @@ -82,14 +94,24 @@ function extractFirstEntryJsonFromTaskData( } /** - * Extracts the binary data of the first item of the last node + * Extracts the binary data of the first item of the first non-empty branch of the last node */ async function extractFirstEntryBinaryFromTaskData( context: WebhookExecutionContext, lastNodeTaskData: ITaskData, ): Promise> { - // Return the binary data of the first entry - const lastNodeFirstJsonItem: INodeExecutionData = lastNodeTaskData.data!.main[0]![0]; + const mainOutputs = lastNodeTaskData.data?.main; + let lastNodeFirstJsonItem: INodeExecutionData | undefined; + + if (mainOutputs && Array.isArray(mainOutputs)) { + for (const branch of mainOutputs) { + if (branch && Array.isArray(branch) && branch.length > 0) { + // Found a non-empty branch, take the first item from it + lastNodeFirstJsonItem = branch[0]; + break; // Stop after processing the first non-empty branch + } + } + } if (lastNodeFirstJsonItem === undefined) { return createResultError(new OperationalError('No item was found to return')); @@ -139,14 +161,24 @@ async function extractFirstEntryBinaryFromTaskData( } /** - * Extracts the JSON data of all the items of the last node + * Extracts the JSON data of all the items from the first non-empty branch of the last node */ function extractAllEntriesJsonFromTaskData( lastNodeTaskData: ITaskData, ): Result { const data: unknown[] = []; - for (const entry of lastNodeTaskData.data!.main[0]!) { - data.push(entry.json); + const mainOutputs = lastNodeTaskData.data?.main; + + if (mainOutputs && Array.isArray(mainOutputs)) { + for (const branch of mainOutputs) { + if (branch && Array.isArray(branch) && branch.length > 0) { + // Found a non-empty branch, extract all items from it + for (const entry of branch) { + data.push(entry.json); + } + break; // Stop after processing the first non-empty branch + } + } } return createResultOk({ diff --git a/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts b/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts index 06981a1ba77..45ebfa40359 100644 --- a/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts +++ b/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts @@ -1275,6 +1275,123 @@ describe('extractBotResponse', () => { const result = extractBotResponse(resultData, executionId); expect(result).toBeUndefined(); }); + + it('should extract response from second output branch when first is empty', () => { + const resultData: IRunExecutionData['resultData'] = { + lastNodeExecuted: 'nodeA', + runData: { + nodeA: [ + { + executionTime: 1, + startTime: 1, + executionIndex: 1, + source: [], + data: { + main: [ + [], // First output branch is empty + [{ json: { message: 'Response from second branch' } }], // Second branch has response + ], + }, + }, + ], + }, + }; + const executionId = 'test-exec-id'; + const result = extractBotResponse(resultData, executionId); + expect(result).toEqual({ + text: 'Response from second branch', + sender: 'bot', + id: executionId, + }); + }); + + it('should extract response from second branch when first has empty json', () => { + const resultData: IRunExecutionData['resultData'] = { + lastNodeExecuted: 'nodeA', + runData: { + nodeA: [ + { + executionTime: 1, + startTime: 1, + executionIndex: 1, + source: [], + data: { + main: [ + [{ json: {} }], // First branch has empty json object + [{ json: { text: 'Response from second branch' } }], // Second branch has response + ], + }, + }, + ], + }, + }; + const executionId = 'test-exec-id'; + const result = extractBotResponse(resultData, executionId); + expect(result).toEqual({ + text: 'Response from second branch', + sender: 'bot', + id: executionId, + }); + }); + + it('should extract response from first available branch when multiple exist', () => { + const resultData: IRunExecutionData['resultData'] = { + lastNodeExecuted: 'nodeA', + runData: { + nodeA: [ + { + executionTime: 1, + startTime: 1, + executionIndex: 1, + source: [], + data: { + main: [ + [], // First branch empty + [{ json: {} }], // Second branch has empty object + [{ json: { output: 'Response from third branch' } }], // Third branch has response + ], + }, + }, + ], + }, + }; + const executionId = 'test-exec-id'; + const result = extractBotResponse(resultData, executionId); + expect(result).toEqual({ + text: 'Response from third branch', + sender: 'bot', + id: executionId, + }); + }); + + it('should use response from first branch when multiple branches have valid text', () => { + const resultData: IRunExecutionData['resultData'] = { + lastNodeExecuted: 'nodeA', + runData: { + nodeA: [ + { + executionTime: 1, + startTime: 1, + executionIndex: 1, + source: [], + data: { + main: [ + [{ json: { text: 'First branch response' } }], + [{ json: { text: 'Second branch response' } }], + ], + }, + }, + ], + }, + }; + const executionId = 'test-exec-id'; + const result = extractBotResponse(resultData, executionId); + expect(result).toEqual({ + text: 'First branch response', + sender: 'bot', + id: executionId, + }); + }); }); describe(mergeStartData, () => { diff --git a/packages/frontend/editor-ui/src/features/logs/logs.utils.ts b/packages/frontend/editor-ui/src/features/logs/logs.utils.ts index 2d64069872d..aea92f260bb 100644 --- a/packages/frontend/editor-ui/src/features/logs/logs.utils.ts +++ b/packages/frontend/editor-ui/src/features/logs/logs.utils.ts @@ -502,8 +502,24 @@ export function extractBotResponse( if (get(nodeResponseData, 'error')) { responseMessage = '[ERROR: ' + get(nodeResponseData, 'error.message') + ']'; } else { - const responseData = get(nodeResponseData, 'data.main[0][0].json'); - const text = extractResponseText(responseData) ?? emptyText; + // Check all output branches for response data, not just the first one + const mainOutputs = get(nodeResponseData, 'data.main'); + let text: string | undefined; + + if (mainOutputs && Array.isArray(mainOutputs)) { + for (const branch of mainOutputs) { + if (branch?.[0]?.json) { + const responseData = branch[0].json; + text = extractResponseText(responseData); + if (text) { + break; // Found a valid response, stop searching + } + } + } + } + + // Fall back to emptyText if no valid response found + text = text ?? emptyText; if (!text) { return undefined;