Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow using Vector Stores directly as Tools #12311

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import { getConnectionHintNoticeField } from '@utils/sharedFields';

export class ToolVectorStore implements INodeType {
description: INodeTypeDescription = {
displayName: 'Vector Store Tool',
displayName: 'Vector Store Question Answer Tool',
name: 'toolVectorStore',
icon: 'fa:database',
group: ['transform'],
version: [1],
description: 'Retrieve context from vector store',
description: 'Answer questions with a vector store',
defaults: {
name: 'Vector Store Tool',
name: 'Answer questions with a vector store',
},
codex: {
categories: ['AI'],
Expand Down Expand Up @@ -59,20 +59,23 @@ export class ToolVectorStore implements INodeType {
properties: [
getConnectionHintNoticeField([NodeConnectionType.AiAgent]),
{
displayName: 'Name',
displayName: 'Data Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. company_knowledge_base',
placeholder: 'e.g. users_info',
validateType: 'string-alphanumeric',
description: 'Name of the vector store',
description:
'Name of the data in vector store. This will be used to fill this tool description: Useful for when you need to answer questions about [name]. Whenever you need information about [data description], you should ALWAYS use this. Input should be a fully formed question.',
},
{
displayName: 'Description',
displayName: 'Description of Data',
name: 'description',
type: 'string',
default: '',
placeholder: 'Retrieves data about [insert information about your data here]...',
placeholder: "[Describe your data here, e.g. a user's name, email, etc.]",
description:
'Describe the data in vector store. This will be used to fill this tool description: Useful for when you need to answer questions about [name]. Whenever you need information about [data description], you should ALWAYS use this. Input should be a fully formed question.',
typeOptions: {
rows: 3,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export class VectorStorePGVector extends createVectorStoreNode({
testedBy: 'postgresConnectionTest',
},
],
operationModes: ['load', 'insert', 'retrieve'],
operationModes: ['load', 'insert', 'retrieve', 'retrieve-as-tool'],
},
sharedFields,
insertFields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class VectorStorePinecone extends createVectorStoreNode({
required: true,
},
],
operationModes: ['load', 'insert', 'retrieve', 'update'],
operationModes: ['load', 'insert', 'retrieve', 'update', 'retrieve-as-tool'],
},
methods: { listSearch: { pineconeIndexSearch } },
retrieveFields,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class VectorStoreSupabase extends createVectorStoreNode({
required: true,
},
],
operationModes: ['load', 'insert', 'retrieve', 'update'],
operationModes: ['load', 'insert', 'retrieve', 'update', 'retrieve-as-tool'],
},
methods: {
listSearch: { supabaseTableNameSearch },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ import { N8nJsonLoader } from '@utils/N8nJsonLoader';
import { getConnectionHintNoticeField } from '@utils/sharedFields';

import { processDocument } from './processDocuments';
import { DynamicTool } from 'langchain/tools';

type NodeOperationMode = 'insert' | 'load' | 'retrieve' | 'update';
type NodeOperationMode = 'insert' | 'load' | 'retrieve' | 'update' | 'retrieve-as-tool';

const DEFAULT_OPERATION_MODES: NodeOperationMode[] = ['load', 'insert', 'retrieve'];
const DEFAULT_OPERATION_MODES: NodeOperationMode[] = [
'load',
'insert',
'retrieve',
'retrieve-as-tool',
];

interface NodeMeta {
displayName: string;
Expand Down Expand Up @@ -100,10 +106,16 @@ function getOperationModeOptions(args: VectorStoreNodeConstructorArgs): INodePro
action: 'Add documents to vector store',
},
{
name: 'Retrieve Documents (For Agent/Chain)',
name: 'Retrieve Documents (As Vector Store for AI Agent)',
value: 'retrieve',
description: 'Retrieve documents from vector store to be used with AI nodes',
action: 'Retrieve documents for AI processing',
description: 'Retrieve documents from vector store to be used as vector store with AI nodes',
action: 'Retrieve documents for AI processing as Vector Store',
},
{
name: 'Retrieve Documents (As Tool for AI Agent)',
value: 'retrieve-as-tool',
description: 'Retrieve documents from vector store to be used as tool with AI nodes',
action: 'Retrieve documents for AI processing as Tool',
},
{
name: 'Update Documents',
Expand Down Expand Up @@ -150,6 +162,10 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
const mode = parameters?.mode;
const inputs = [{ displayName: "Embedding", type: "${NodeConnectionType.AiEmbedding}", required: true, maxConnections: 1}]

if (mode === 'retrieve-as-tool') {
return inputs;
}

if (['insert', 'load', 'update'].includes(mode)) {
inputs.push({ displayName: "", type: "${NodeConnectionType.Main}"})
}
Expand All @@ -163,6 +179,11 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
outputs: `={{
((parameters) => {
const mode = parameters?.mode ?? 'retrieve';

if (mode === 'retrieve-as-tool') {
return [{ displayName: "Tool", type: "${NodeConnectionType.AiTool}"}]
}

if (mode === 'retrieve') {
return [{ displayName: "Vector Store", type: "${NodeConnectionType.AiVectorStore}"}]
}
Expand All @@ -186,6 +207,37 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
},
},
},
{
displayName: 'Name',
name: 'toolName',
type: 'string',
default: '',
required: true,
description: 'Name of the vector store',
placeholder: 'e.g. company_knowledge_base',
validateType: 'string-alphanumeric',
displayOptions: {
show: {
mode: ['retrieve-as-tool'],
},
},
},
{
displayName: 'Description',
name: 'toolDescription',
type: 'string',
default: '',
required: true,
typeOptions: { rows: 2 },
description:
'Explain to the LLM what this tool does, a good, specific description would allow LLMs to produce expected results much more often',
placeholder: `e.g. ${args.meta.description}`,
displayOptions: {
show: {
mode: ['retrieve-as-tool'],
},
},
},
...args.sharedFields,
...transformDescriptionForOperationMode(args.insertFields ?? [], 'insert'),
// Prompt and topK are always used for the load operation
Expand All @@ -211,7 +263,19 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
description: 'Number of top results to fetch from vector store',
displayOptions: {
show: {
mode: ['load'],
mode: ['load', 'retrieve-as-tool'],
},
},
},
{
displayName: 'Include Metadata',
name: 'includeDocumentMetadata',
type: 'boolean',
default: true,
description: 'Whether or not to include document metadata',
displayOptions: {
show: {
mode: ['load', 'retrieve-as-tool'],
},
},
},
Expand Down Expand Up @@ -268,10 +332,16 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
filter,
);

const includeDocumentMetadata = this.getNodeParameter(
'includeDocumentMetadata',
itemIndex,
true,
) as boolean;

const serializedDocs = docs.map(([doc, score]) => {
const document = {
metadata: doc.metadata,
pageContent: doc.pageContent,
...(includeDocumentMetadata ? { metadata: doc.metadata } : {}),
};

return {
Expand Down Expand Up @@ -378,12 +448,12 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>

throw new NodeOperationError(
this.getNode(),
'Only the "load" and "insert" operation modes are supported with execute',
'Only the "load", "update" and "insert" operation modes are supported with execute',
);
}

async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const mode = this.getNodeParameter('mode', 0) as 'load' | 'insert' | 'retrieve';
const mode = this.getNodeParameter('mode', 0) as NodeOperationMode;
const filter = getMetadataFiltersValues(this, itemIndex);
const embeddings = (await this.getInputConnectionData(
NodeConnectionType.AiEmbedding,
Expand All @@ -397,9 +467,54 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
};
}

if (mode === 'retrieve-as-tool') {
const toolDescription = this.getNodeParameter('toolDescription', itemIndex) as string;
const toolName = this.getNodeParameter('toolName', itemIndex) as string;
const topK = this.getNodeParameter('topK', itemIndex, 4) as number;
const includeDocumentMetadata = this.getNodeParameter(
'includeDocumentMetadata',
itemIndex,
true,
) as boolean;

const vectorStoreTool = new DynamicTool({
name: toolName,
description: toolDescription,
func: async (input) => {
const vectorStore = await args.getVectorStoreClient(
this,
filter,
embeddings,
itemIndex,
);
const embeddedPrompt = await embeddings.embedQuery(input);
const documents = await vectorStore.similaritySearchVectorWithScore(
embeddedPrompt,
topK,
filter,
);
return documents
.map((document) => {
if (includeDocumentMetadata) {
return { type: 'text', text: JSON.stringify(document[0]) };
}
return {
type: 'text',
text: JSON.stringify({ pageContent: document[0].pageContent }),
};
})
.filter((document) => !!document);
},
});

return {
response: logWrapper(vectorStoreTool, this),
};
}

throw new NodeOperationError(
this.getNode(),
'Only the "retrieve" operation mode is supported to supply data',
'Only the "retrieve" and "retrieve-as-tool" operation mode is supported to supply data',
);
}
};
Loading