mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat: Add AI tool building capabilities (#7336)
Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/langchain-memory-chat/23733 --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Val <68596159+valya@users.noreply.github.com> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Deborah <deborah@starfallprojects.co.uk> Co-authored-by: Jesper Bylund <mail@jesperbylund.com> Co-authored-by: Jon <jonathan.bennetts@gmail.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Mason Geloso <Mason.geloso@gmail.com> Co-authored-by: Mason Geloso <hone@Masons-Mac-mini.local> Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import type { Document } from 'langchain/document';
|
||||
import type { Embeddings } from 'langchain/embeddings/base';
|
||||
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
|
||||
|
||||
export class MemoryVectorStoreManager {
|
||||
private static instance: MemoryVectorStoreManager | null = null;
|
||||
|
||||
private vectorStoreBuffer: Map<string, MemoryVectorStore>;
|
||||
|
||||
private embeddings: Embeddings;
|
||||
|
||||
private constructor(embeddings: Embeddings) {
|
||||
this.vectorStoreBuffer = new Map();
|
||||
this.embeddings = embeddings;
|
||||
}
|
||||
|
||||
public static getInstance(embeddings: Embeddings): MemoryVectorStoreManager {
|
||||
if (!MemoryVectorStoreManager.instance) {
|
||||
MemoryVectorStoreManager.instance = new MemoryVectorStoreManager(embeddings);
|
||||
}
|
||||
return MemoryVectorStoreManager.instance;
|
||||
}
|
||||
|
||||
public async getVectorStore(memoryKey: string): Promise<MemoryVectorStore> {
|
||||
let vectorStoreInstance = this.vectorStoreBuffer.get(memoryKey);
|
||||
|
||||
if (!vectorStoreInstance) {
|
||||
vectorStoreInstance = await MemoryVectorStore.fromExistingIndex(this.embeddings);
|
||||
this.vectorStoreBuffer.set(memoryKey, vectorStoreInstance);
|
||||
}
|
||||
|
||||
return vectorStoreInstance;
|
||||
}
|
||||
|
||||
public async addDocuments(
|
||||
memoryKey: string,
|
||||
documents: Document[],
|
||||
clearStore?: boolean,
|
||||
): Promise<void> {
|
||||
if (clearStore) {
|
||||
this.vectorStoreBuffer.delete(memoryKey);
|
||||
}
|
||||
const vectorStoreInstance = await this.getVectorStore(memoryKey);
|
||||
await vectorStoreInstance.addDocuments(documents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
|
||||
import type { VectorStore } from 'langchain/vectorstores/base';
|
||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
import type {
|
||||
INodeCredentialDescription,
|
||||
INodeProperties,
|
||||
INodeExecutionData,
|
||||
IExecuteFunctions,
|
||||
INodeTypeDescription,
|
||||
SupplyData,
|
||||
INodeType,
|
||||
ILoadOptionsFunctions,
|
||||
INodeListSearchResult,
|
||||
} from 'n8n-workflow';
|
||||
import type { Embeddings } from 'langchain/embeddings/base';
|
||||
import type { Document } from 'langchain/document';
|
||||
import { logWrapper } from '../../../utils/logWrapper';
|
||||
import type { N8nJsonLoader } from '../../../utils/N8nJsonLoader';
|
||||
import type { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader';
|
||||
import { getMetadataFiltersValues } from '../../../utils/helpers';
|
||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
||||
import { processDocument } from './processDocuments';
|
||||
|
||||
interface NodeMeta {
|
||||
displayName: string;
|
||||
name: string;
|
||||
description: string;
|
||||
docsUrl: string;
|
||||
icon: string;
|
||||
credentials?: INodeCredentialDescription[];
|
||||
}
|
||||
interface VectorStoreNodeConstructorArgs {
|
||||
meta: NodeMeta;
|
||||
methods?: {
|
||||
listSearch?: {
|
||||
[key: string]: (
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
) => Promise<INodeListSearchResult>;
|
||||
};
|
||||
};
|
||||
sharedFields: INodeProperties[];
|
||||
insertFields?: INodeProperties[];
|
||||
loadFields?: INodeProperties[];
|
||||
retrieveFields?: INodeProperties[];
|
||||
populateVectorStore: (
|
||||
context: IExecuteFunctions,
|
||||
embeddings: Embeddings,
|
||||
documents: Array<Document<Record<string, unknown>>>,
|
||||
itemIndex: number,
|
||||
) => Promise<void>;
|
||||
getVectorStoreClient: (
|
||||
context: IExecuteFunctions,
|
||||
filter: Record<string, never> | undefined,
|
||||
embeddings: Embeddings,
|
||||
itemIndex: number,
|
||||
) => Promise<VectorStore>;
|
||||
}
|
||||
|
||||
function transformDescriptionForOperationMode(
|
||||
fields: INodeProperties[],
|
||||
mode: 'insert' | 'load' | 'retrieve',
|
||||
) {
|
||||
return fields.map((field) => ({
|
||||
...field,
|
||||
displayOptions: { show: { mode: [mode] } },
|
||||
}));
|
||||
}
|
||||
export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
|
||||
class VectorStoreNodeType implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: args.meta.displayName,
|
||||
name: args.meta.name,
|
||||
description: args.meta.description,
|
||||
icon: args.meta.icon,
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
defaults: {
|
||||
name: args.meta.displayName,
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Vector Stores'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: args.meta.docsUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
credentials: args.meta.credentials,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
|
||||
inputs: `={{
|
||||
((parameters) => {
|
||||
const mode = parameters?.mode;
|
||||
const inputs = [{ displayName: "Embedding", type: "${NodeConnectionType.AiEmbedding}", required: true, maxConnections: 1}]
|
||||
|
||||
if (['insert', 'load'].includes(mode)) {
|
||||
inputs.push({ displayName: "", type: "${NodeConnectionType.Main}"})
|
||||
}
|
||||
|
||||
if (mode === 'insert') {
|
||||
inputs.push({ displayName: "Document", type: "${NodeConnectionType.AiDocument}", required: true, maxConnections: 1})
|
||||
}
|
||||
return inputs
|
||||
})($parameter)
|
||||
}}`,
|
||||
outputs: `={{
|
||||
((parameters) => {
|
||||
const mode = parameters?.mode ?? 'retrieve';
|
||||
if (mode === 'retrieve') {
|
||||
return [{ displayName: "Vector Store", type: "${NodeConnectionType.AiVectorStore}"}]
|
||||
}
|
||||
return [{ displayName: "", type: "${NodeConnectionType.Main}"}]
|
||||
})($parameter)
|
||||
}}`,
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
default: 'retrieve',
|
||||
options: [
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'load',
|
||||
description: 'Get many ranked documents from vector store for query',
|
||||
action: 'Get many ranked documents from vector store for query',
|
||||
},
|
||||
{
|
||||
name: 'Insert Documents',
|
||||
value: 'insert',
|
||||
description: 'Insert documents into vector store',
|
||||
action: 'Insert documents into vector store',
|
||||
},
|
||||
{
|
||||
name: 'Retrieve Documents (For Agent/Chain)',
|
||||
value: 'retrieve',
|
||||
description: 'Retrieve documents from vector store to be used with AI nodes',
|
||||
action: 'Retrieve documents from vector store to be used with AI nodes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...getConnectionHintNoticeField([NodeConnectionType.AiRetriever]),
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['retrieve'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...args.sharedFields,
|
||||
...transformDescriptionForOperationMode(args.insertFields ?? [], 'insert'),
|
||||
// Prompt and topK are always used for the load operation
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'Search prompt to retrieve matching documents from the vector store using similarity-based ranking',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['load'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'topK',
|
||||
type: 'number',
|
||||
default: 4,
|
||||
description: 'Number of top results to fetch from vector store',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['load'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...transformDescriptionForOperationMode(args.loadFields ?? [], 'load'),
|
||||
...transformDescriptionForOperationMode(args.retrieveFields ?? [], 'retrieve'),
|
||||
],
|
||||
};
|
||||
|
||||
methods = args.methods;
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const mode = this.getNodeParameter('mode', 0) as 'load' | 'insert' | 'retrieve';
|
||||
|
||||
const embeddings = (await this.getInputConnectionData(
|
||||
NodeConnectionType.AiEmbedding,
|
||||
0,
|
||||
)) as Embeddings;
|
||||
|
||||
if (mode === 'load') {
|
||||
const items = this.getInputData(0);
|
||||
|
||||
const resultData = [];
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const filter = getMetadataFiltersValues(this, itemIndex);
|
||||
const vectorStore = await args.getVectorStoreClient(
|
||||
this,
|
||||
// We'll pass filter to similaritySearchVectorWithScore instaed of getVectorStoreClient
|
||||
undefined,
|
||||
embeddings,
|
||||
itemIndex,
|
||||
);
|
||||
const prompt = this.getNodeParameter('prompt', itemIndex) as string;
|
||||
const topK = this.getNodeParameter('topK', itemIndex, 4) as number;
|
||||
|
||||
const embeddedPrompt = await embeddings.embedQuery(prompt);
|
||||
const docs = await vectorStore.similaritySearchVectorWithScore(
|
||||
embeddedPrompt,
|
||||
topK,
|
||||
filter,
|
||||
);
|
||||
|
||||
const serializedDocs = docs.map(([doc, score]) => {
|
||||
const document = {
|
||||
metadata: doc.metadata,
|
||||
pageContent: doc.pageContent,
|
||||
};
|
||||
|
||||
return {
|
||||
json: { document, score },
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
resultData.push(...serializedDocs);
|
||||
}
|
||||
|
||||
return this.prepareOutputData(resultData);
|
||||
}
|
||||
|
||||
if (mode === 'insert') {
|
||||
const items = this.getInputData();
|
||||
|
||||
const documentInput = (await this.getInputConnectionData(
|
||||
NodeConnectionType.AiDocument,
|
||||
0,
|
||||
)) as N8nJsonLoader | N8nBinaryLoader | Array<Document<Record<string, unknown>>>;
|
||||
|
||||
const resultData = [];
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const itemData = items[itemIndex];
|
||||
const { processedDocuments, serializedDocuments } = await processDocument(
|
||||
documentInput,
|
||||
itemData,
|
||||
itemIndex,
|
||||
);
|
||||
resultData.push(...serializedDocuments);
|
||||
|
||||
try {
|
||||
await args.populateVectorStore(this, embeddings, processedDocuments, itemIndex);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return this.prepareOutputData(resultData);
|
||||
}
|
||||
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Only the "load" and "insert" operation modes are supported with execute',
|
||||
);
|
||||
}
|
||||
|
||||
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const mode = this.getNodeParameter('mode', 0) as 'load' | 'insert' | 'retrieve';
|
||||
const filter = getMetadataFiltersValues(this, itemIndex);
|
||||
const embeddings = (await this.getInputConnectionData(
|
||||
NodeConnectionType.AiEmbedding,
|
||||
0,
|
||||
)) as Embeddings;
|
||||
|
||||
if (mode === 'retrieve') {
|
||||
const vectorStore = await args.getVectorStoreClient(this, filter, embeddings, itemIndex);
|
||||
return {
|
||||
response: logWrapper(vectorStore, this),
|
||||
};
|
||||
}
|
||||
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Only the "retrieve" operation mode is supported to supply data',
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const pineconeIndexRLC: INodeProperties = {
|
||||
displayName: 'Pinecone Index',
|
||||
name: 'pineconeIndex',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'pineconeIndexSearch',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const supabaseTableNameRLC: INodeProperties = {
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'supabaseTableNameSearch',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { IDataObject, ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
import { Pinecone } from '@pinecone-database/pinecone';
|
||||
|
||||
export async function pineconeIndexSearch(this: ILoadOptionsFunctions) {
|
||||
const credentials = await this.getCredentials('pineconeApi');
|
||||
|
||||
const client = new Pinecone({
|
||||
apiKey: credentials.apiKey as string,
|
||||
environment: credentials.environment as string,
|
||||
});
|
||||
|
||||
const indexes = await client.listIndexes();
|
||||
|
||||
const results = indexes.map((index) => ({
|
||||
name: index.name,
|
||||
value: index.name,
|
||||
}));
|
||||
|
||||
return { results };
|
||||
}
|
||||
|
||||
export async function supabaseTableNameSearch(this: ILoadOptionsFunctions) {
|
||||
const credentials = await this.getCredentials('supabaseApi');
|
||||
|
||||
const results = [];
|
||||
|
||||
const paths = (
|
||||
await this.helpers.requestWithAuthentication.call(this, 'supabaseApi', {
|
||||
headers: {
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
method: 'GET',
|
||||
uri: `${credentials.host}/rest/v1/`,
|
||||
json: true,
|
||||
})
|
||||
).paths as IDataObject;
|
||||
|
||||
for (const path of Object.keys(paths)) {
|
||||
//omit introspection path
|
||||
if (path === '/') continue;
|
||||
|
||||
results.push({
|
||||
name: path.replace('/', ''),
|
||||
value: path.replace('/', ''),
|
||||
});
|
||||
}
|
||||
|
||||
return { results };
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Document } from 'langchain/document';
|
||||
import type { INodeExecutionData } from 'n8n-workflow';
|
||||
import { N8nJsonLoader } from '../../../utils/N8nJsonLoader';
|
||||
import { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader';
|
||||
|
||||
export async function processDocuments(
|
||||
documentInput: N8nJsonLoader | N8nBinaryLoader | Array<Document<Record<string, unknown>>>,
|
||||
inputItems: INodeExecutionData[],
|
||||
) {
|
||||
let processedDocuments: Document[];
|
||||
|
||||
if (documentInput instanceof N8nJsonLoader || documentInput instanceof N8nBinaryLoader) {
|
||||
processedDocuments = await documentInput.processAll(inputItems);
|
||||
} else {
|
||||
processedDocuments = documentInput;
|
||||
}
|
||||
|
||||
const serializedDocuments = processedDocuments.map(({ metadata, pageContent }) => ({
|
||||
json: { metadata, pageContent },
|
||||
}));
|
||||
|
||||
return {
|
||||
processedDocuments,
|
||||
serializedDocuments,
|
||||
};
|
||||
}
|
||||
export async function processDocument(
|
||||
documentInput: N8nJsonLoader | N8nBinaryLoader | Array<Document<Record<string, unknown>>>,
|
||||
inputItem: INodeExecutionData,
|
||||
itemIndex: number,
|
||||
) {
|
||||
let processedDocuments: Document[];
|
||||
|
||||
if (documentInput instanceof N8nJsonLoader || documentInput instanceof N8nBinaryLoader) {
|
||||
processedDocuments = await documentInput.processItem(inputItem, itemIndex);
|
||||
} else {
|
||||
processedDocuments = documentInput;
|
||||
}
|
||||
|
||||
const serializedDocuments = processedDocuments.map(({ metadata, pageContent }) => ({
|
||||
json: { metadata, pageContent },
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
processedDocuments,
|
||||
serializedDocuments,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user