diff --git a/packages/@n8n/ai-workflow-builder.ee/src/tools/get-node-examples.tool.ts b/packages/@n8n/ai-workflow-builder.ee/src/tools/get-node-examples.tool.ts index ffca06d479f..6ea57335151 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/tools/get-node-examples.tool.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/tools/get-node-examples.tool.ts @@ -92,47 +92,15 @@ interface WorkflowRetrievalResult { newTemplates: WorkflowMetadata[]; } -/** Options for getWorkflowsForNodeType */ -interface GetWorkflowsOptions { - nodeType: string; - logger?: Logger; - onProgress?: (message: string) => void; - /** Local cache of templates accumulated during batch processing */ - localCache?: WorkflowMetadata[]; -} - /** * Single retrieval function for getting workflows containing a specific node type. - * Checks local cache first, then state cache, then fetches from API if needed. + * Checks state cache first, then fetches from API if needed. */ -async function getWorkflowsForNodeType({ - nodeType, - logger, - onProgress, - localCache = [], -}: GetWorkflowsOptions): Promise { - // First check local cache (templates fetched earlier in the same batch) - const relevantFromLocal = localCache.filter((wf) => - wf.workflow.nodes.some((n) => n.type === nodeType), - ); - - if (relevantFromLocal.length > 0) { - const nodeConfigs = getNodeConfigurationsFromTemplates(relevantFromLocal, nodeType); - - logger?.debug('Found node configurations in local batch cache', { - nodeType, - configCount: nodeConfigs.length, - workflowCount: relevantFromLocal.length, - }); - - return { - workflows: relevantFromLocal, - nodeConfigs, - newTemplates: [], // Already in local cache, not "new" - }; - } - - // Then check state cache (templates from previous tool calls) +async function getWorkflowsForNodeType( + nodeType: string, + logger?: Logger, +): Promise { + // Check state cache (templates from previous tool calls) let stateCachedTemplates: WorkflowMetadata[] = []; try { const state = getWorkflowState(); @@ -162,8 +130,6 @@ async function getWorkflowsForNodeType({ } // No cached data, fetch from templates API - onProgress?.(`Fetching examples for ${nodeType}...`); - try { const result = await fetchWorkflowsFromTemplates( { nodes: nodeType, rows: 5 }, @@ -277,33 +243,28 @@ export function createGetNodeExamplesTool({ exampleType, logger }: CreateNodeExa reporter.start(validatedInput); - // Process all nodes and collect results - // Use localCache to accumulate templates during batch processing - // so subsequent nodes can benefit from earlier fetches - const allMessages: string[] = []; + // Fetch all node examples in parallel + reportProgress(reporter, `Fetching examples for ${nodes.length} node(s)...`); + + const results = await Promise.all( + nodes.map(async ({ nodeType }) => await getWorkflowsForNodeType(nodeType, logger)), + ); + + // Process results and format messages const allNewTemplates: WorkflowMetadata[] = []; let totalFound = 0; - for (const { nodeType, nodeVersion } of nodes) { - const result = await getWorkflowsForNodeType({ - nodeType, - logger, - onProgress: (msg: string) => reportProgress(reporter, msg), - localCache: allNewTemplates, // Pass accumulated templates - }); + const allMessages = nodes.map(({ nodeType, nodeVersion }, i) => { + const result = results[i]; - // Format based on example type - const message = - exampleType === 'configuration' - ? formatNodeConfigurationExamples(nodeType, result.nodeConfigs, nodeVersion) - : formatConnectionExamples(nodeType, result.workflows); - - allMessages.push(message); - // Add new templates to local cache for subsequent iterations allNewTemplates.push(...result.newTemplates); totalFound += exampleType === 'configuration' ? result.nodeConfigs.length : result.workflows.length; - } + + return exampleType === 'configuration' + ? formatNodeConfigurationExamples(nodeType, result.nodeConfigs, nodeVersion) + : formatConnectionExamples(nodeType, result.workflows); + }); const combinedMessage = allMessages.join('\n\n---\n\n'); const nodeTypes = nodes.map((n) => n.nodeType); diff --git a/packages/@n8n/ai-workflow-builder.ee/src/tools/test/get-node-examples.tool.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/tools/test/get-node-examples.tool.test.ts index 5a760c76234..f2e5ed367d9 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/tools/test/get-node-examples.tool.test.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/tools/test/get-node-examples.tool.test.ts @@ -285,32 +285,39 @@ describe('GetNodeExamplesTool', () => { }); }); - describe('batch processing with local cache', () => { - it('should use templates from earlier fetches for subsequent nodes in same batch', async () => { + describe('parallel batch processing', () => { + it('should fetch multiple nodes in parallel and combine results', async () => { const configTool = createGetNodeConfigurationExamplesTool().tool; const mockConfig = createToolConfig('get_node_configuration_examples', 'test-batch'); - // First fetch returns a workflow containing BOTH httpRequest AND code nodes - mockFetchWorkflowsFromTemplates.mockResolvedValueOnce( - createMockFetchResult([ - createMockWorkflow('Multi-Node Workflow', [ - createNode({ - id: 'http-1', - name: 'HTTP Request', - type: 'n8n-nodes-base.httpRequest', - typeVersion: 1, - parameters: { url: 'https://api.example.com' }, - }), - createNode({ - id: 'code-1', - name: 'Transform', - type: 'n8n-nodes-base.code', - typeVersion: 1, - parameters: { jsCode: 'return items.map(i => i);' }, - }), + // Set up separate responses for each node type (fetched in parallel) + mockFetchWorkflowsFromTemplates + .mockResolvedValueOnce( + createMockFetchResult([ + createMockWorkflow('HTTP Workflow', [ + createNode({ + id: 'http-1', + name: 'HTTP Request', + type: 'n8n-nodes-base.httpRequest', + typeVersion: 1, + parameters: { url: 'https://api.example.com' }, + }), + ]), ]), - ]), - ); + ) + .mockResolvedValueOnce( + createMockFetchResult([ + createMockWorkflow('Code Workflow', [ + createNode({ + id: 'code-1', + name: 'Transform', + type: 'n8n-nodes-base.code', + typeVersion: 1, + parameters: { jsCode: 'return items.map(i => i);' }, + }), + ]), + ]), + ); // Request examples for both nodes in one call const result = await configTool.invoke( @@ -330,8 +337,8 @@ describe('GetNodeExamplesTool', () => { expect(message).toContain('code'); expect(message).toContain('return items.map'); - // API should only be called ONCE - second node should use local cache - expect(mockFetchWorkflowsFromTemplates).toHaveBeenCalledTimes(1); + // API should be called for each node type (parallel fetching) + expect(mockFetchWorkflowsFromTemplates).toHaveBeenCalledTimes(2); }); });