diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts index 109a090259..295060de21 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts @@ -28,7 +28,7 @@ export class ToolWorkflow extends VersionedNodeType { ], }, }, - defaultVersion: 2, + defaultVersion: 2.1, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { @@ -37,6 +37,7 @@ export class ToolWorkflow extends VersionedNodeType { 1.2: new ToolWorkflowV1(baseDescription), 1.3: new ToolWorkflowV1(baseDescription), 2: new ToolWorkflowV2(baseDescription), + 2.1: new ToolWorkflowV2(baseDescription), }; super(nodeVersions, baseDescription); } diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts index 98ca94cb1f..df682ce040 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.node.ts @@ -25,7 +25,9 @@ export class ToolWorkflowV2 implements INodeType { }; async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { - const workflowToolService = new WorkflowToolService(this); + const returnAllItems = this.getNode().typeVersion > 2; + + const workflowToolService = new WorkflowToolService(this, { returnAllItems }); const name = this.getNodeParameter('name', itemIndex) as string; const description = this.getNodeParameter('description', itemIndex) as string; diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts index 688000b1ec..6205e1d6d9 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts @@ -187,6 +187,66 @@ describe('WorkflowTool::WorkflowToolService', () => { expect(result.subExecutionId).toBe('test-execution'); }); + it('should successfully execute workflow and return first item of many', async () => { + const workflowInfo = { id: 'test-workflow' }; + const items: INodeExecutionData[] = []; + const workflowProxyMock = { + $execution: { id: 'exec-id' }, + $workflow: { id: 'workflow-id' }, + } as unknown as IWorkflowDataProxyData; + + const TEST_RESPONSE_1 = { msg: 'test response 1' }; + const TEST_RESPONSE_2 = { msg: 'test response 2' }; + + const mockResponse: ExecuteWorkflowData = { + data: [[{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]], + executionId: 'test-execution', + }; + + jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse); + + const result = await service['executeSubWorkflow']( + context, + workflowInfo, + items, + workflowProxyMock, + ); + + expect(result.response).toBe(TEST_RESPONSE_1); + expect(result.subExecutionId).toBe('test-execution'); + }); + + it('should successfully execute workflow and return all items', async () => { + const serviceWithReturnAllItems = new WorkflowToolService(context, { returnAllItems: true }); + const workflowInfo = { id: 'test-workflow' }; + const items: INodeExecutionData[] = []; + const workflowProxyMock = { + $execution: { id: 'exec-id' }, + $workflow: { id: 'workflow-id' }, + } as unknown as IWorkflowDataProxyData; + + const TEST_RESPONSE_1 = { msg: 'test response 1' }; + const TEST_RESPONSE_2 = { msg: 'test response 2' }; + + const mockResponse: ExecuteWorkflowData = { + data: [[{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]], + executionId: 'test-execution', + }; + + jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse); + + const result = await serviceWithReturnAllItems['executeSubWorkflow']( + context, + workflowInfo, + items, + workflowProxyMock, + undefined, + ); + + expect(result.response).toEqual([{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]); + expect(result.subExecutionId).toBe('test-execution'); + }); + it('should throw error when workflow execution fails', async () => { jest.spyOn(context, 'executeWorkflow').mockRejectedValueOnce(new Error('Execution failed')); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts index 8fc366084e..57a6d1fc84 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts @@ -1,7 +1,6 @@ import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools'; -import get from 'lodash/get'; -import isObject from 'lodash/isObject'; +import { isArray, isObject } from 'lodash'; import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions'; @@ -29,6 +28,10 @@ import { } from 'n8n-workflow'; import { z } from 'zod'; +function isNodeExecutionData(data: unknown): data is INodeExecutionData[] { + return isArray(data) && Boolean(data.length) && isObject(data[0]) && 'json' in data[0]; +} + /** Main class for creating the Workflow tool Processes the node parameters and creates AI Agent tool capable of executing n8n workflows @@ -43,10 +46,16 @@ export class WorkflowToolService { // Sub-workflow execution id, will be set after the sub-workflow is executed private subExecutionId: string | undefined; - constructor(private baseContext: ISupplyDataFunctions) { + private returnAllItems: boolean = false; + + constructor( + private baseContext: ISupplyDataFunctions, + options?: { returnAllItems: boolean }, + ) { const subWorkflowInputs = this.baseContext.getNode().parameters .workflowInputs as ResourceMapperValue; this.useSchema = (subWorkflowInputs?.schema ?? []).length > 0; + this.returnAllItems = options?.returnAllItems ?? false; } // Creates the tool based on the provided parameters @@ -65,7 +74,7 @@ export class WorkflowToolService { const toolHandler = async ( query: string | IDataObject, runManager?: CallbackManagerForToolRun, - ): Promise => { + ): Promise => { const localRunIndex = runIndex++; // We need to clone the context here to handle runIndex correctly // Otherwise the runIndex will be shared between different executions @@ -74,10 +83,23 @@ export class WorkflowToolService { runIndex: localRunIndex, inputData: [[{ json: { query } }]], }); + try { const response = await this.runFunction(context, query, itemIndex, runManager); + const processedResponse = this.handleToolResponse(response); + let responseData: INodeExecutionData[]; + if (isNodeExecutionData(response)) { + responseData = response; + } else { + const reParsedData = jsonParse(processedResponse, { + fallbackValue: { response: processedResponse }, + }); + + responseData = [{ json: reParsedData }]; + } + // Once the sub-workflow is executed, add the output data to the context // This will be used to link the sub-workflow execution in the parent workflow let metadata: ITaskMetadata | undefined; @@ -89,13 +111,11 @@ export class WorkflowToolService { }, }; } - const json = jsonParse(processedResponse, { - fallbackValue: { response: processedResponse }, - }); + void context.addOutputData( NodeConnectionType.AiTool, localRunIndex, - [[{ json }]], + [responseData], metadata, ); @@ -126,6 +146,14 @@ export class WorkflowToolService { return response.toString(); } + if (isNodeExecutionData(response)) { + return JSON.stringify( + response.map((item) => item.json), + null, + 2, + ); + } + if (isObject(response)) { return JSON.stringify(response, null, 2); } @@ -148,7 +176,7 @@ export class WorkflowToolService { items: INodeExecutionData[], workflowProxy: IWorkflowDataProxyData, runManager?: CallbackManagerForToolRun, - ): Promise<{ response: string; subExecutionId: string }> { + ): Promise<{ response: string | IDataObject | INodeExecutionData[]; subExecutionId: string }> { let receivedData: ExecuteWorkflowData; try { receivedData = await context.executeWorkflow(workflowInfo, items, runManager?.getChild(), { @@ -163,7 +191,12 @@ export class WorkflowToolService { throw new NodeOperationError(context.getNode(), error as Error); } - const response: string | undefined = get(receivedData, 'data[0][0].json') as string | undefined; + let response: IDataObject | INodeExecutionData[] | undefined; + if (this.returnAllItems) { + response = receivedData?.data?.[0]?.length ? receivedData.data[0] : undefined; + } else { + response = receivedData?.data?.[0]?.[0]?.json; + } if (response === undefined) { throw new NodeOperationError( context.getNode(), @@ -183,7 +216,7 @@ export class WorkflowToolService { query: string | IDataObject, itemIndex: number, runManager?: CallbackManagerForToolRun, - ): Promise { + ): Promise { const source = context.getNodeParameter('source', itemIndex) as string; const workflowProxy = context.getWorkflowDataProxy(0); @@ -304,7 +337,10 @@ export class WorkflowToolService { private async createStructuredTool( name: string, description: string, - func: (query: string | IDataObject, runManager?: CallbackManagerForToolRun) => Promise, + func: ( + query: string | IDataObject, + runManager?: CallbackManagerForToolRun, + ) => Promise, ): Promise { const collectedArguments = await this.extractFromAIParameters(); diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts index 46fb3d1677..cd56a0f5d7 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/versionDescription.ts @@ -12,7 +12,7 @@ export const versionDescription: INodeTypeDescription = { defaults: { name: 'Call n8n Workflow Tool', }, - version: [2], + version: [2, 2.1], inputs: [], outputs: [NodeConnectionType.AiTool], outputNames: ['Tool'],