feat(editor): Use node name as tool name at Vector Store retriever tool nodes (#15917)

This commit is contained in:
Jaakko Husso
2025-06-03 17:57:56 +03:00
committed by GitHub
parent b87d04e33b
commit a9f8b2d46a
6 changed files with 148 additions and 12 deletions

View File

@@ -127,6 +127,13 @@ exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] =
"displayName": "Name", "displayName": "Name",
"displayOptions": { "displayOptions": {
"show": { "show": {
"@version": [
{
"_cnd": {
"lte": 1.2,
},
},
],
"mode": [ "mode": [
"retrieve-as-tool", "retrieve-as-tool",
], ],
@@ -254,6 +261,7 @@ exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] =
1, 1,
1.1, 1.1,
1.2, 1.2,
1.3,
], ],
} }
`; `;

View File

@@ -95,15 +95,23 @@ describe('createVectorStoreNode', () => {
}); });
describe('retrieve-as-tool mode', () => { 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 // ARRANGE
const parameters: Record<string, NodeParameterValueType | object> = { const parameters: Record<string, NodeParameterValueType> = {
...DEFAULT_PARAMETERS, ...DEFAULT_PARAMETERS,
mode: 'retrieve-as-tool', mode: 'retrieve-as-tool',
description: 'tool description', description: 'tool description',
toolName: 'tool name', toolName: 'tool name',
includeDocumentMetadata: true, includeDocumentMetadata: true,
}; };
context.getNode.mockReturnValueOnce({
id: 'testNode',
typeVersion: 1.2,
name: 'Test Tool',
type: 'testVectorStore',
parameters,
position: [0, 0],
});
context.getNodeParameter.mockImplementation( context.getNodeParameter.mockImplementation(
(parameterName: string): NodeParameterValueType | object => parameters[parameterName], (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 // ARRANGE
const parameters: Record<string, NodeParameterValueType | object> = { const parameters: Record<string, NodeParameterValueType> = {
...DEFAULT_PARAMETERS, ...DEFAULT_PARAMETERS,
mode: 'retrieve-as-tool', mode: 'retrieve-as-tool',
description: 'tool description', description: 'tool description',
toolName: 'tool name', includeDocumentMetadata: true,
includeDocumentMetadata: false,
}; };
context.getNode.mockReturnValueOnce({
id: 'testNode',
typeVersion: 1.3,
name: 'Test Tool',
type: 'testVectorStore',
parameters,
position: [0, 0],
});
context.getNodeParameter.mockImplementation( context.getNodeParameter.mockImplementation(
(parameterName: string): NodeParameterValueType | object => parameters[parameterName], (parameterName: string): NodeParameterValueType | object => parameters[parameterName],
); );
@@ -151,7 +166,49 @@ describe('createVectorStoreNode', () => {
const output = await tool?.func(MOCK_SEARCH_VALUE); const output = await tool?.func(MOCK_SEARCH_VALUE);
// ASSERT // 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<string, NodeParameterValueType> = {
...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(tool?.description).toEqual(parameters.toolDescription);
expect(embeddings.embedQuery).toHaveBeenCalledWith(MOCK_SEARCH_VALUE); expect(embeddings.embedQuery).toHaveBeenCalledWith(MOCK_SEARCH_VALUE);
expect(vectorStore.similaritySearchVectorWithScore).toHaveBeenCalledWith( expect(vectorStore.similaritySearchVectorWithScore).toHaveBeenCalledWith(

View File

@@ -44,7 +44,8 @@ export const createVectorStoreNode = <T extends VectorStore = VectorStore>(
iconColor: args.meta.iconColor, iconColor: args.meta.iconColor,
group: ['transform'], group: ['transform'],
// 1.2 has changes to VectorStoreInMemory node. // 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: { defaults: {
name: args.meta.displayName, name: args.meta.displayName,
}, },
@@ -125,6 +126,7 @@ export const createVectorStoreNode = <T extends VectorStore = VectorStore>(
validateType: 'string-alphanumeric', validateType: 'string-alphanumeric',
displayOptions: { displayOptions: {
show: { show: {
'@version': [{ _cnd: { lte: 1.2 } }],
mode: ['retrieve-as-tool'], mode: ['retrieve-as-tool'],
}, },
}, },

View File

@@ -30,6 +30,14 @@ describe('Vector Store Operation Handlers', () => {
}; };
mockContext = mock<IExecuteFunctions & ISupplyDataFunctions>(); mockContext = mock<IExecuteFunctions & ISupplyDataFunctions>();
mockContext.getNode.mockReturnValue({
id: 'testNode',
typeVersion: 1.3,
name: 'Test Tool',
type: 'testVectorStore',
parameters: nodeParameters,
position: [0, 0],
});
mockContext.getNodeParameter.mockImplementation((parameterName, _itemIndex, fallbackValue) => { mockContext.getNodeParameter.mockImplementation((parameterName, _itemIndex, fallbackValue) => {
if (typeof parameterName !== 'string') return fallbackValue; if (typeof parameterName !== 'string') return fallbackValue;
return nodeParameters[parameterName] ?? fallbackValue; return nodeParameters[parameterName] ?? fallbackValue;
@@ -103,12 +111,29 @@ describe('Vector Store Operation Handlers', () => {
}); });
describe('handleRetrieveAsToolOperation', () => { 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); const result = await handleRetrieveAsToolOperation(mockContext, mockArgs, mockEmbeddings, 0);
expect(result).toHaveProperty('response'); expect(result).toHaveProperty('response');
expect(result.response).toHaveProperty('name', 'test_tool'); expect(result.response).toHaveProperty('name', 'test_tool');
expect(result.response).toHaveProperty('description', 'Test tool description'); 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');
});
}); });
}); });

View File

@@ -14,6 +14,7 @@ import { handleRetrieveAsToolOperation } from '../retrieveAsToolOperation';
// Mock the helper functions // Mock the helper functions
jest.mock('@utils/helpers', () => ({ jest.mock('@utils/helpers', () => ({
...jest.requireActual('@utils/helpers'),
getMetadataFiltersValues: jest.fn().mockReturnValue({ testFilter: 'value' }), getMetadataFiltersValues: jest.fn().mockReturnValue({ testFilter: 'value' }),
})); }));
@@ -37,6 +38,14 @@ describe('handleRetrieveAsToolOperation', () => {
}; };
mockContext = mock<ISupplyDataFunctions>(); mockContext = mock<ISupplyDataFunctions>();
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) => { mockContext.getNodeParameter.mockImplementation((parameterName, _itemIndex, fallbackValue) => {
if (typeof parameterName !== 'string') return fallbackValue; if (typeof parameterName !== 'string') return fallbackValue;
return nodeParameters[parameterName] ?? fallbackValue; return nodeParameters[parameterName] ?? fallbackValue;
@@ -70,7 +79,16 @@ describe('handleRetrieveAsToolOperation', () => {
jest.clearAllMocks(); 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( const result = (await handleRetrieveAsToolOperation(
mockContext, mockContext,
mockArgs, mockArgs,
@@ -89,6 +107,25 @@ describe('handleRetrieveAsToolOperation', () => {
expect(logWrapper).toHaveBeenCalledWith(expect.any(DynamicTool), mockContext); 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 () => { it('should create a tool that can search the vector store', async () => {
const result = await handleRetrieveAsToolOperation(mockContext, mockArgs, mockEmbeddings, 0); const result = await handleRetrieveAsToolOperation(mockContext, mockArgs, mockEmbeddings, 0);
const tool = result.response as DynamicTool; const tool = result.response as DynamicTool;

View File

@@ -3,7 +3,7 @@ import type { VectorStore } from '@langchain/core/vectorstores';
import { DynamicTool } from 'langchain/tools'; import { DynamicTool } from 'langchain/tools';
import type { ISupplyDataFunctions, SupplyData } from 'n8n-workflow'; import type { ISupplyDataFunctions, SupplyData } from 'n8n-workflow';
import { getMetadataFiltersValues } from '@utils/helpers'; import { getMetadataFiltersValues, nodeNameToToolName } from '@utils/helpers';
import { logWrapper } from '@utils/logWrapper'; import { logWrapper } from '@utils/logWrapper';
import type { VectorStoreNodeConstructorArgs } from '../types'; import type { VectorStoreNodeConstructorArgs } from '../types';
@@ -20,7 +20,14 @@ export async function handleRetrieveAsToolOperation<T extends VectorStore = Vect
): Promise<SupplyData> { ): Promise<SupplyData> {
// Get the tool configuration parameters // Get the tool configuration parameters
const toolDescription = context.getNodeParameter('toolDescription', itemIndex) as string; 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 topK = context.getNodeParameter('topK', itemIndex, 4) as number;
const includeDocumentMetadata = context.getNodeParameter( const includeDocumentMetadata = context.getNodeParameter(
'includeDocumentMetadata', 'includeDocumentMetadata',