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",
"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,
],
}
`;

View File

@@ -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<string, NodeParameterValueType | object> = {
const parameters: Record<string, NodeParameterValueType> = {
...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<string, NodeParameterValueType | object> = {
const parameters: Record<string, NodeParameterValueType> = {
...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<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(embeddings.embedQuery).toHaveBeenCalledWith(MOCK_SEARCH_VALUE);
expect(vectorStore.similaritySearchVectorWithScore).toHaveBeenCalledWith(

View File

@@ -44,7 +44,8 @@ export const createVectorStoreNode = <T extends VectorStore = VectorStore>(
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 = <T extends VectorStore = VectorStore>(
validateType: 'string-alphanumeric',
displayOptions: {
show: {
'@version': [{ _cnd: { lte: 1.2 } }],
mode: ['retrieve-as-tool'],
},
},

View File

@@ -30,6 +30,14 @@ describe('Vector Store Operation Handlers', () => {
};
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) => {
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');
});
});
});

View File

@@ -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<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) => {
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;

View File

@@ -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<T extends VectorStore = Vect
): Promise<SupplyData> {
// 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',