diff --git a/packages/@n8n/nodes-langchain/credentials/WeaviateApi.credentials.ts b/packages/@n8n/nodes-langchain/credentials/WeaviateApi.credentials.ts new file mode 100644 index 0000000000..373b0310ca --- /dev/null +++ b/packages/@n8n/nodes-langchain/credentials/WeaviateApi.credentials.ts @@ -0,0 +1,143 @@ +import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow'; + +export class WeaviateApi implements ICredentialType { + name = 'weaviateApi'; + + displayName = 'Weaviate Credentials'; + + documentationUrl = 'https://docs.n8n.io/integrations/builtin/credentials/weaviate/'; + + properties: INodeProperties[] = [ + { + displayName: 'Connection Type', + name: 'connection_type', + type: 'options', + options: [ + { + name: 'Weaviate Cloud', + value: 'weaviate_cloud', + }, + { + name: 'Custom Connection', + value: 'custom_connection', + }, + ], + default: 'weaviate_cloud', + description: + 'Choose whether to connect to a Weaviate Cloud instance or a custom Weaviate instance.', + }, + { + displayName: 'Weaviate Cloud Endpoint', + name: 'weaviate_cloud_endpoint', + description: 'The Endpoint of a Weaviate Cloud instance.', + placeholder: 'https://your-cluster.weaviate.cloud', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + connection_type: ['weaviate_cloud'], + }, + }, + }, + { + displayName: 'Weaviate Api Key', + name: 'weaviate_api_key', + description: 'The API key for the Weaviate instance.', + type: 'string', + typeOptions: { password: true }, + default: '', + }, + { + displayName: 'Custom Connection HTTP Host', + name: 'custom_connection_http_host', + description: 'The host of your Weaviate instance.', + type: 'string', + required: true, + default: 'weaviate', + displayOptions: { + show: { + connection_type: ['custom_connection'], + }, + }, + }, + { + displayName: 'Custom Connection HTTP Port', + name: 'custom_connection_http_port', + description: 'The port of your Weaviate instance.', + type: 'number', + required: true, + default: 8080, + displayOptions: { + show: { + connection_type: ['custom_connection'], + }, + }, + }, + { + displayName: 'Custom Connection HTTP Secure', + name: 'custom_connection_http_secure', + description: 'Whether to use a secure connection for HTTP.', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + connection_type: ['custom_connection'], + }, + }, + }, + { + displayName: 'Custom Connection gRPC Host', + name: 'custom_connection_grpc_host', + description: 'The gRPC host of your Weaviate instance.', + type: 'string', + required: true, + default: 'weaviate', + displayOptions: { + show: { + connection_type: ['custom_connection'], + }, + }, + }, + { + displayName: 'Custom Connection gRPC Port', + name: 'custom_connection_grpc_port', + description: 'The gRPC port of your Weaviate instance.', + type: 'number', + required: true, + default: 50051, + displayOptions: { + show: { + connection_type: ['custom_connection'], + }, + }, + }, + { + displayName: 'Custom Connection gRPC Secure', + name: 'custom_connection_grpc_secure', + description: 'Whether to use a secure connection for gRPC.', + type: 'boolean', + required: true, + default: false, + displayOptions: { + show: { + connection_type: ['custom_connection'], + }, + }, + }, + ]; + + test: ICredentialTestRequest = { + request: { + baseURL: + '={{$credentials.weaviate_cloud_endpoint?$credentials.weaviate_cloud_endpoint.startsWith("http://") || $credentials.weaviate_cloud_endpoint.startsWith("https://")?$credentials.weaviate_cloud_endpoint:"https://" + $credentials.weaviate_cloud_endpoint:($credentials.custom_connection_http_secure ? "https" : "http") + "://" + $credentials.custom_connection_http_host + ":" + $credentials.custom_connection_http_port }}', + url: '/v1/nodes', + disableFollowRedirect: false, + headers: { + Authorization: + '={{$if($credentials.weaviate_api_key, "Bearer " + $credentials.weaviate_api_key, undefined)}}', + }, + }, + }; +} diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/VectorStoreWeaviate.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/VectorStoreWeaviate.node.ts new file mode 100644 index 0000000000..7efb1b195c --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/VectorStoreWeaviate.node.ts @@ -0,0 +1,271 @@ +import type { Embeddings } from '@langchain/core/embeddings'; +import { WeaviateStore } from '@langchain/weaviate'; +import type { WeaviateLibArgs } from '@langchain/weaviate'; +import type { + IDataObject, + INodeProperties, + INodePropertyCollection, + INodePropertyOptions, +} from 'n8n-workflow'; +import { type ProxiesParams, type TimeoutParams } from 'weaviate-client'; + +import type { WeaviateCompositeFilter, WeaviateCredential } from './Weaviate.utils'; +import { createWeaviateClient, parseCompositeFilter } from './Weaviate.utils'; +import { createVectorStoreNode } from '../shared/createVectorStoreNode/createVectorStoreNode'; +import { weaviateCollectionsSearch } from '../shared/createVectorStoreNode/methods/listSearch'; +import { weaviateCollectionRLC } from '../shared/descriptions'; + +class ExtendedWeaviateVectorStore extends WeaviateStore { + private static defaultFilter: WeaviateCompositeFilter; + + static async fromExistingCollection( + embeddings: Embeddings, + args: WeaviateLibArgs, + defaultFilter?: WeaviateCompositeFilter, + ): Promise { + if (defaultFilter) { + ExtendedWeaviateVectorStore.defaultFilter = defaultFilter; + } + return await super.fromExistingIndex(embeddings, args); + } + + async similaritySearchVectorWithScore(query: number[], k: number, filter?: IDataObject) { + filter = filter ?? ExtendedWeaviateVectorStore.defaultFilter; + if (filter) { + const composedFilter = parseCompositeFilter(filter as WeaviateCompositeFilter); + return await super.similaritySearchVectorWithScore(query, k, composedFilter); + } else { + return await super.similaritySearchVectorWithScore(query, k, undefined); + } + } +} + +const sharedFields: INodeProperties[] = [weaviateCollectionRLC]; + +const shared_options: Array = [ + { + displayName: 'Tenant Name', + name: 'tenant', + type: 'string', + default: undefined, + validateType: 'string', + description: 'Tenant Name. Collection must have been created with tenant support enabled.', + }, + { + displayName: 'Text Key', + name: 'textKey', + type: 'string', + default: 'text', + validateType: 'string', + description: 'The key in the document that contains the embedded text', + }, + { + displayName: 'Skip Init Checks', + name: 'skip_init_checks', + type: 'boolean', + default: false, + validateType: 'boolean', + description: 'Whether to skip init checks while instantiating the client', + }, + { + displayName: 'Init Timeout', + name: 'timeout_init', + type: 'number', + default: 2, + validateType: 'number', + description: 'Number of timeout seconds for initial checks', + }, + { + displayName: 'Insert Timeout', + name: 'timeout_insert', + type: 'number', + default: 90, + validateType: 'number', + description: 'Number of timeout seconds for inserts', + }, + { + displayName: 'Query Timeout', + name: 'timeout_query', + type: 'number', + default: 30, + validateType: 'number', + description: 'Number of timeout seconds for queries', + }, + { + displayName: 'GRPC Proxy', + name: 'proxy_grpc', + type: 'string', + default: undefined, + validateType: 'string', + description: 'Proxy to use for GRPC', + }, +]; + +const insertFields: INodeProperties[] = [ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + ...shared_options, + { + displayName: 'Clear Data', + name: 'clearStore', + type: 'boolean', + default: false, + description: 'Whether to clear the Collection/Tenant before inserting new data', + }, + ], + }, +]; + +const retrieveFields: INodeProperties[] = [ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Search Filters', + name: 'searchFilterJson', + type: 'json', + typeOptions: { + rows: 5, + }, + default: + '{\n "OR": [\n {\n "path": ["pdf_info_Author"],\n "operator": "Equal",\n "valueString": "Elis"\n },\n {\n "path": ["pdf_info_Author"],\n "operator": "Equal",\n "valueString": "Pinnacle"\n } \n ]\n}', + validateType: 'object', + description: + 'Filter pageContent or metadata using this filtering syntax', + }, + { + displayName: 'Metadata Keys', + name: 'metadataKeys', + type: 'string', + default: 'source,page', + validateType: 'string', + description: 'Select the metadata to retrieve along the content', + }, + ...shared_options, + ], + }, +]; + +export class VectorStoreWeaviate extends createVectorStoreNode({ + meta: { + displayName: 'Weaviate Vector Store', + name: 'vectorStoreWeaviate', + description: 'Work with your data in a Weaviate Cluster', + icon: 'file:weaviate.svg', + docsUrl: + 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoreweaviate/', + credentials: [ + { + name: 'weaviateApi', + required: true, + }, + ], + }, + methods: { + listSearch: { weaviateCollectionsSearch }, + }, + loadFields: retrieveFields, + insertFields, + sharedFields, + retrieveFields, + async getVectorStoreClient(context, filter, embeddings, itemIndex) { + const collection = context.getNodeParameter('weaviateCollection', itemIndex, '', { + extractValue: true, + }) as string; + + const options = context.getNodeParameter('options', itemIndex, {}) as { + tenant?: string; + textKey?: string; + timeout_init: number; + timeout_insert: number; + timeout_query: number; + skip_init_checks: boolean; + proxy_grpc: string; + metadataKeys?: string; + }; + // check if textKey is valid + + const credentials = await context.getCredentials('weaviateApi'); + + const timeout = { + query: options.timeout_query, + init: options.timeout_init, + insert: options.timeout_insert, + }; + + const proxies = { + grpc: options.proxy_grpc, + }; + + const client = await createWeaviateClient( + credentials as WeaviateCredential, + timeout as TimeoutParams, + proxies as ProxiesParams, + options.skip_init_checks as boolean, + ); + + const metadataKeys = options.metadataKeys ? options.metadataKeys.split(',') : []; + const config: WeaviateLibArgs = { + client, + indexName: collection, + tenant: options.tenant ? options.tenant : undefined, + textKey: options.textKey ? options.textKey : 'text', + metadataKeys: metadataKeys as string[] | undefined, + }; + + const validFilter = (filter && Object.keys(filter).length > 0 ? filter : undefined) as + | WeaviateCompositeFilter + | undefined; + return await ExtendedWeaviateVectorStore.fromExistingCollection( + embeddings, + config, + validFilter, + ); + }, + async populateVectorStore(context, embeddings, documents, itemIndex) { + const collectionName = context.getNodeParameter('weaviateCollection', itemIndex, '', { + extractValue: true, + }) as string; + + const options = context.getNodeParameter('options', itemIndex, {}) as { + tenant?: string; + textKey?: string; + clearStore?: boolean; + metadataKeys?: string; + }; + + const credentials = await context.getCredentials('weaviateApi'); + + const metadataKeys = options.metadataKeys ? options.metadataKeys.split(',') : []; + + const client = await createWeaviateClient(credentials as WeaviateCredential); + + const config: WeaviateLibArgs = { + client, + indexName: collectionName, + tenant: options.tenant ? options.tenant : undefined, + textKey: options.textKey ? options.textKey : 'text', + metadataKeys: metadataKeys as string[] | undefined, + }; + + if (options.clearStore) { + if (!options.tenant) { + await client.collections.delete(collectionName); + } else { + const collection = client.collections.get(collectionName); + await collection.tenants.remove([{ name: options.tenant }]); + } + } + + await WeaviateStore.fromDocuments(documents, embeddings, config); + }, +}) {} diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/Weaviate.utils.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/Weaviate.utils.ts new file mode 100644 index 0000000000..5651cf0827 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/Weaviate.utils.ts @@ -0,0 +1,141 @@ +import { OperationalError } from 'n8n-workflow'; +import type { + FilterValue, + GeoRangeFilter, + ProxiesParams, + TimeoutParams, + WeaviateClient, +} from 'weaviate-client'; +import weaviate, { Filters } from 'weaviate-client'; + +export type WeaviateCredential = { + weaviate_cloud_endpoint: string; + weaviate_api_key: string; + custom_connection_http_host: string; + custom_connection_http_port: number; + custom_connection_http_secure: boolean; + custom_connection_grpc_host: string; + custom_connection_grpc_port: number; + custom_connection_grpc_secure: boolean; +}; + +export async function createWeaviateClient( + credentials: WeaviateCredential, + timeout?: TimeoutParams, + proxies?: ProxiesParams, + skipInitChecks: boolean = false, +): Promise { + if (credentials.weaviate_cloud_endpoint) { + const weaviateClient: WeaviateClient = await weaviate.connectToWeaviateCloud( + credentials.weaviate_cloud_endpoint, + { + authCredentials: new weaviate.ApiKey(credentials.weaviate_api_key), + timeout, + skipInitChecks, + }, + ); + return weaviateClient; + } else { + const weaviateClient: WeaviateClient = await weaviate.connectToCustom({ + httpHost: credentials.custom_connection_http_host, + httpPort: credentials.custom_connection_http_port, + grpcHost: credentials.custom_connection_grpc_host, + grpcPort: credentials.custom_connection_grpc_port, + grpcSecure: credentials.custom_connection_grpc_secure, + httpSecure: credentials.custom_connection_http_secure, + authCredentials: credentials.weaviate_api_key + ? new weaviate.ApiKey(credentials.weaviate_api_key) + : undefined, + timeout, + proxies, + skipInitChecks, + }); + return weaviateClient; + } +} +type WeaviateFilterUnit = { + path: string[]; + operator: string; + valueString?: string; + valueTextArray?: string[]; + valueBoolean?: boolean; + valueNumber?: number; + valueGeoCoordinates?: GeoRangeFilter; +}; + +export type WeaviateCompositeFilter = { AND: WeaviateFilterUnit[] } | { OR: WeaviateFilterUnit[] }; + +function buildFilter(filter: WeaviateFilterUnit): FilterValue { + const { path, operator } = filter; + const property = weaviate.filter.byProperty(path[0]); + + switch (operator.toLowerCase()) { + case 'equal': + if (filter.valueString !== undefined) return property.equal(filter.valueString); + if (filter.valueNumber !== undefined) return property.equal(filter.valueNumber); + break; + + case 'like': + if (filter.valueString === undefined) { + throw new OperationalError("Missing 'valueString' for 'like' operator."); + } + return property.like(filter.valueString); + + case 'containsany': + if (filter.valueTextArray === undefined) { + throw new OperationalError("Missing 'valueTextArray' for 'containsAny' operator."); + } + return property.containsAny(filter.valueTextArray); + + case 'containsall': + if (filter.valueTextArray === undefined) { + throw new OperationalError("Missing 'valueTextArray' for 'containsAll' operator."); + } + return property.containsAll(filter.valueTextArray); + + case 'greaterthan': + if (filter.valueNumber === undefined) { + throw new OperationalError("Missing 'valueNumber' for 'greaterThan' operator."); + } + return property.greaterThan(filter.valueNumber); + + case 'lessthan': + if (filter.valueNumber === undefined) { + throw new OperationalError("Missing 'valueNumber' for 'lessThan' operator."); + } + return property.lessThan(filter.valueNumber); + + case 'isnull': + if (filter.valueBoolean === undefined) { + throw new OperationalError("Missing 'valueBoolean' for 'isNull' operator."); + } + return property.isNull(filter.valueBoolean); + + case 'withingeorange': + if (!filter.valueGeoCoordinates) { + throw new OperationalError("Missing 'valueGeoCoordinates' for 'withinGeoRange' operator."); + } + return property.withinGeoRange(filter.valueGeoCoordinates); + + default: + throw new OperationalError(`Unsupported operator: ${operator}`); + } + + throw new OperationalError(`No valid filter value provided for operator: ${operator}`); +} + +export function parseCompositeFilter( + filter: WeaviateCompositeFilter | WeaviateFilterUnit, +): FilterValue { + // Handle composite filters (AND/OR) + if (typeof filter === 'object' && ('AND' in filter || 'OR' in filter)) { + if ('AND' in filter) { + return Filters.and(...filter.AND.map(buildFilter)); + } else if ('OR' in filter) { + return Filters.or(...filter.OR.map(buildFilter)); + } + } + + // Handle individual filter units + return buildFilter(filter); +} diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/weaviate.svg b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/weaviate.svg new file mode 100644 index 0000000000..cb6ba0ad71 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreWeaviate/weaviate.svg @@ -0,0 +1,2 @@ + +qdrant \ No newline at end of file diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/methods/listSearch.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/methods/listSearch.ts index 8082c75a17..722809f677 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/methods/listSearch.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/methods/listSearch.ts @@ -4,6 +4,8 @@ import { ApplicationError, type IDataObject, type ILoadOptionsFunctions } from ' import type { QdrantCredential } from '../../../VectorStoreQdrant/Qdrant.utils'; import { createQdrantClient } from '../../../VectorStoreQdrant/Qdrant.utils'; +import type { WeaviateCredential } from '../../../VectorStoreWeaviate/Weaviate.utils'; +import { createWeaviateClient } from '../../../VectorStoreWeaviate/Weaviate.utils'; export async function pineconeIndexSearch(this: ILoadOptionsFunctions) { const credentials = await this.getCredentials('pineconeApi'); @@ -89,3 +91,18 @@ export async function milvusCollectionsSearch(this: ILoadOptionsFunctions) { return { results }; } + +export async function weaviateCollectionsSearch(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('weaviateApi'); + + const client = await createWeaviateClient(credentials as WeaviateCredential); + + const collections = await client.collections.listAll(); + + const results = collections.map((collection: { name: string }) => ({ + name: collection.name, + value: collection.name, + })); + + return { results }; +} diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts index d496c9e39d..f4456c30f2 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts @@ -91,3 +91,26 @@ export const milvusCollectionRLC: INodeProperties = { }, ], }; + +export const weaviateCollectionRLC: INodeProperties = { + displayName: 'Weaviate Collection', + name: 'weaviateCollection', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'weaviateCollectionsSearch', + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + }, + ], +}; diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index e0a5a43712..74ed11dc19 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -41,6 +41,7 @@ "dist/credentials/QdrantApi.credentials.js", "dist/credentials/SearXngApi.credentials.js", "dist/credentials/SerpApi.credentials.js", + "dist/credentials/WeaviateApi.credentials.js", "dist/credentials/WolframAlphaApi.credentials.js", "dist/credentials/XAiApi.credentials.js", "dist/credentials/XataApi.credentials.js", @@ -133,6 +134,7 @@ "dist/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.js", "dist/nodes/vector_store/VectorStoreSupabaseInsert/VectorStoreSupabaseInsert.node.js", "dist/nodes/vector_store/VectorStoreSupabaseLoad/VectorStoreSupabaseLoad.node.js", + "dist/nodes/vector_store/VectorStoreWeaviate/VectorStoreWeaviate.node.js", "dist/nodes/vector_store/VectorStoreZep/VectorStoreZep.node.js", "dist/nodes/vector_store/VectorStoreZepInsert/VectorStoreZepInsert.node.js", "dist/nodes/vector_store/VectorStoreZepLoad/VectorStoreZepLoad.node.js", @@ -178,6 +180,7 @@ "@langchain/qdrant": "0.1.2", "@langchain/redis": "0.1.1", "@langchain/textsplitters": "0.1.0", + "@langchain/weaviate": "0.2.0", "@modelcontextprotocol/sdk": "1.12.0", "@mozilla/readability": "0.6.0", "@n8n/client-oauth2": "workspace:*", @@ -216,6 +219,7 @@ "sqlite3": "5.1.7", "temp": "0.9.4", "tmp-promise": "3.0.3", + "weaviate-client": "3.6.2", "zod": "catalog:", "zod-to-json-schema": "3.23.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0677fb76e7..8ad631ddd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -912,6 +912,9 @@ importers: '@langchain/textsplitters': specifier: 0.1.0 version: 0.1.0(@langchain/core@0.3.59(openai@4.103.0(encoding@0.1.13)(ws@8.18.2)(zod@3.25.67))) + '@langchain/weaviate': + specifier: 0.2.0 + version: 0.2.0(@langchain/core@0.3.59(openai@4.103.0(encoding@0.1.13)(ws@8.18.2)(zod@3.25.67)))(encoding@0.1.13) '@modelcontextprotocol/sdk': specifier: 1.12.0 version: 1.12.0 @@ -1026,6 +1029,9 @@ importers: tmp-promise: specifier: 3.0.3 version: 3.0.3 + weaviate-client: + specifier: 3.6.2 + version: 3.6.2(encoding@0.1.13) zod: specifier: 3.25.67 version: 3.25.67