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:
Jan Oberhauser
2023-11-29 12:13:55 +01:00
committed by GitHub
parent dbfd617ace
commit 87def60979
243 changed files with 21526 additions and 321 deletions

View File

@@ -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);
}
}

View File

@@ -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',
);
}
};

View File

@@ -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',
},
],
};

View File

@@ -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 };
}

View File

@@ -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,
};
}