From a9f8b2d46a2fc03633a2a6b8ca431a91e8e0cfce Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Tue, 3 Jun 2025 17:57:56 +0300 Subject: [PATCH] feat(editor): Use node name as tool name at Vector Store retriever tool nodes (#15917) --- .../createVectorStoreNode.test.ts.snap | 8 +++ .../createVectorStoreNode.test.ts | 71 +++++++++++++++++-- .../createVectorStoreNode.ts | 4 +- .../__tests__/operationHandlers.test.ts | 27 ++++++- .../__tests__/retrieveAsToolOperation.test.ts | 39 +++++++++- .../operations/retrieveAsToolOperation.ts | 11 ++- 6 files changed, 148 insertions(+), 12 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap index 52bded860f..52191d67a6 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap @@ -127,6 +127,13 @@ exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] = "displayName": "Name", "displayOptions": { "show": { + "@version": [ + { + "_cnd": { + "lte": 1.2, + }, + }, + ], "mode": [ "retrieve-as-tool", ], @@ -254,6 +261,7 @@ exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] = 1, 1.1, 1.2, + 1.3, ], } `; diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.test.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.test.ts index 0b6f45d526..40fc1d494d 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.test.ts @@ -95,15 +95,23 @@ describe('createVectorStoreNode', () => { }); describe('retrieve-as-tool mode', () => { - it('supplies DynamicTool that queries vector store and returns documents with metadata', async () => { + it('supplies DynamicTool that queries vector store and returns documents with metadata on version <= 1.2', async () => { // ARRANGE - const parameters: Record = { + const parameters: Record = { ...DEFAULT_PARAMETERS, mode: 'retrieve-as-tool', description: 'tool description', toolName: 'tool name', includeDocumentMetadata: true, }; + context.getNode.mockReturnValueOnce({ + id: 'testNode', + typeVersion: 1.2, + name: 'Test Tool', + type: 'testVectorStore', + parameters, + position: [0, 0], + }); context.getNodeParameter.mockImplementation( (parameterName: string): NodeParameterValueType | object => parameters[parameterName], ); @@ -130,15 +138,22 @@ describe('createVectorStoreNode', () => { ]); }); - it('supplies DynamicTool that queries vector store and returns documents without metadata', async () => { + it('supplies DynamicTool that queries vector store and returns documents with metadata on version > 1.2', async () => { // ARRANGE - const parameters: Record = { + const parameters: Record = { ...DEFAULT_PARAMETERS, mode: 'retrieve-as-tool', description: 'tool description', - toolName: 'tool name', - includeDocumentMetadata: false, + includeDocumentMetadata: true, }; + context.getNode.mockReturnValueOnce({ + id: 'testNode', + typeVersion: 1.3, + name: 'Test Tool', + type: 'testVectorStore', + parameters, + position: [0, 0], + }); context.getNodeParameter.mockImplementation( (parameterName: string): NodeParameterValueType | object => parameters[parameterName], ); @@ -151,7 +166,49 @@ describe('createVectorStoreNode', () => { const output = await tool?.func(MOCK_SEARCH_VALUE); // ASSERT - expect(tool?.getName()).toEqual(parameters.toolName); + expect(tool?.getName()).toEqual('Test_Tool'); + expect(tool?.description).toEqual(parameters.toolDescription); + expect(embeddings.embedQuery).toHaveBeenCalledWith(MOCK_SEARCH_VALUE); + expect(vectorStore.similaritySearchVectorWithScore).toHaveBeenCalledWith( + MOCK_EMBEDDED_SEARCH_VALUE, + parameters.topK, + parameters.filter, + ); + expect(output).toEqual([ + { type: 'text', text: JSON.stringify(MOCK_DOCUMENTS[0][0]) }, + { type: 'text', text: JSON.stringify(MOCK_DOCUMENTS[1][0]) }, + ]); + }); + + it('supplies DynamicTool that queries vector store and returns documents without metadata', async () => { + // ARRANGE + const parameters: Record = { + ...DEFAULT_PARAMETERS, + mode: 'retrieve-as-tool', + description: 'tool description', + includeDocumentMetadata: false, + }; + context.getNode.mockReturnValueOnce({ + id: 'testNode', + typeVersion: 1.3, + name: 'Test Tool', + type: 'testVectorStore', + parameters, + position: [0, 0], + }); + context.getNodeParameter.mockImplementation( + (parameterName: string): NodeParameterValueType | object => parameters[parameterName], + ); + + // ACT + const VectorStoreNodeType = createVectorStoreNode(vectorStoreNodeArgs); + const nodeType = new VectorStoreNodeType(); + const data = await nodeType.supplyData.call(context, 1); + const tool = (data.response as { logWrapped: DynamicTool }).logWrapped; + const output = await tool?.func(MOCK_SEARCH_VALUE); + + // ASSERT + expect(tool?.getName()).toEqual('Test_Tool'); expect(tool?.description).toEqual(parameters.toolDescription); expect(embeddings.embedQuery).toHaveBeenCalledWith(MOCK_SEARCH_VALUE); expect(vectorStore.similaritySearchVectorWithScore).toHaveBeenCalledWith( diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts index 1cb85b5268..31ea836f9e 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts @@ -44,7 +44,8 @@ export const createVectorStoreNode = ( iconColor: args.meta.iconColor, group: ['transform'], // 1.2 has changes to VectorStoreInMemory node. - version: [1, 1.1, 1.2], + // 1.3 drops `toolName` and uses node name as the tool name. + version: [1, 1.1, 1.2, 1.3], defaults: { name: args.meta.displayName, }, @@ -125,6 +126,7 @@ export const createVectorStoreNode = ( validateType: 'string-alphanumeric', displayOptions: { show: { + '@version': [{ _cnd: { lte: 1.2 } }], mode: ['retrieve-as-tool'], }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/operationHandlers.test.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/operationHandlers.test.ts index 2e886117ba..cf6f096bb7 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/operationHandlers.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/operationHandlers.test.ts @@ -30,6 +30,14 @@ describe('Vector Store Operation Handlers', () => { }; mockContext = mock(); + mockContext.getNode.mockReturnValue({ + id: 'testNode', + typeVersion: 1.3, + name: 'Test Tool', + type: 'testVectorStore', + parameters: nodeParameters, + position: [0, 0], + }); mockContext.getNodeParameter.mockImplementation((parameterName, _itemIndex, fallbackValue) => { if (typeof parameterName !== 'string') return fallbackValue; return nodeParameters[parameterName] ?? fallbackValue; @@ -103,12 +111,29 @@ describe('Vector Store Operation Handlers', () => { }); describe('handleRetrieveAsToolOperation', () => { - it('should return a tool with the correct name and description', async () => { + it('should return a tool with the correct name and description on version <= 1.2', async () => { + mockContext.getNode.mockReturnValueOnce({ + id: 'testNode', + typeVersion: 1.2, + name: 'Test Tool', + type: 'testVectorStore', + parameters: nodeParameters, + position: [0, 0], + }); + const result = await handleRetrieveAsToolOperation(mockContext, mockArgs, mockEmbeddings, 0); expect(result).toHaveProperty('response'); expect(result.response).toHaveProperty('name', 'test_tool'); expect(result.response).toHaveProperty('description', 'Test tool description'); }); + + it('should return a tool with the correct name and description on version > 1.2', async () => { + const result = await handleRetrieveAsToolOperation(mockContext, mockArgs, mockEmbeddings, 0); + + expect(result).toHaveProperty('response'); + expect(result.response).toHaveProperty('name', 'Test_Tool'); + expect(result.response).toHaveProperty('description', 'Test tool description'); + }); }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/retrieveAsToolOperation.test.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/retrieveAsToolOperation.test.ts index 90d9ea65a3..8699eb526c 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/retrieveAsToolOperation.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/__tests__/retrieveAsToolOperation.test.ts @@ -14,6 +14,7 @@ import { handleRetrieveAsToolOperation } from '../retrieveAsToolOperation'; // Mock the helper functions jest.mock('@utils/helpers', () => ({ + ...jest.requireActual('@utils/helpers'), getMetadataFiltersValues: jest.fn().mockReturnValue({ testFilter: 'value' }), })); @@ -37,6 +38,14 @@ describe('handleRetrieveAsToolOperation', () => { }; mockContext = mock(); + mockContext.getNode.mockReturnValue({ + id: 'testNode', + typeVersion: 1.3, + name: 'Test Knowledge Base', + type: 'testVectorStore', + parameters: nodeParameters, + position: [0, 0], + }); mockContext.getNodeParameter.mockImplementation((parameterName, _itemIndex, fallbackValue) => { if (typeof parameterName !== 'string') return fallbackValue; return nodeParameters[parameterName] ?? fallbackValue; @@ -70,7 +79,16 @@ describe('handleRetrieveAsToolOperation', () => { jest.clearAllMocks(); }); - it('should create a dynamic tool with the correct name and description', async () => { + it('should create a dynamic tool with the correct name and description on version <= 1.2', async () => { + mockContext.getNode.mockReturnValueOnce({ + id: 'testNode', + typeVersion: 1.2, + name: 'Test Knowledge Base', + type: 'testVectorStore', + parameters: nodeParameters, + position: [0, 0], + }); + const result = (await handleRetrieveAsToolOperation( mockContext, mockArgs, @@ -89,6 +107,25 @@ describe('handleRetrieveAsToolOperation', () => { expect(logWrapper).toHaveBeenCalledWith(expect.any(DynamicTool), mockContext); }); + it('should create a dynamic tool with the correct name and description on version > 1.2', async () => { + const result = (await handleRetrieveAsToolOperation( + mockContext, + mockArgs, + mockEmbeddings, + 0, + )) as { + response: DynamicTool; + }; + + expect(result).toHaveProperty('response'); + expect(result.response).toBeInstanceOf(DynamicTool); + expect(result.response.name).toBe('Test_Knowledge_Base'); + expect(result.response.description).toBe('Search the test knowledge base'); + + // Check logWrapper was called + expect(logWrapper).toHaveBeenCalledWith(expect.any(DynamicTool), mockContext); + }); + it('should create a tool that can search the vector store', async () => { const result = await handleRetrieveAsToolOperation(mockContext, mockArgs, mockEmbeddings, 0); const tool = result.response as DynamicTool; diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/retrieveAsToolOperation.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/retrieveAsToolOperation.ts index 92a5d4e6f6..f77c0c3ae2 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/retrieveAsToolOperation.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/operations/retrieveAsToolOperation.ts @@ -3,7 +3,7 @@ import type { VectorStore } from '@langchain/core/vectorstores'; import { DynamicTool } from 'langchain/tools'; import type { ISupplyDataFunctions, SupplyData } from 'n8n-workflow'; -import { getMetadataFiltersValues } from '@utils/helpers'; +import { getMetadataFiltersValues, nodeNameToToolName } from '@utils/helpers'; import { logWrapper } from '@utils/logWrapper'; import type { VectorStoreNodeConstructorArgs } from '../types'; @@ -20,7 +20,14 @@ export async function handleRetrieveAsToolOperation { // Get the tool configuration parameters const toolDescription = context.getNodeParameter('toolDescription', itemIndex) as string; - const toolName = context.getNodeParameter('toolName', itemIndex) as string; + + const node = context.getNode(); + const { typeVersion } = node; + const toolName = + typeVersion < 1.3 + ? (context.getNodeParameter('toolName', itemIndex) as string) + : nodeNameToToolName(node); + const topK = context.getNodeParameter('topK', itemIndex, 4) as number; const includeDocumentMetadata = context.getNodeParameter( 'includeDocumentMetadata',