diff --git a/packages/components/credentials/VoyageAIApi.credential.ts b/packages/components/credentials/VoyageAIApi.credential.ts new file mode 100644 index 00000000000..5965ce5998a --- /dev/null +++ b/packages/components/credentials/VoyageAIApi.credential.ts @@ -0,0 +1,32 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class VoyageAIApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Voyage AI API' + this.name = 'voyageAIApi' + this.version = 1.0 + this.description = + 'Refer to official guide on how to get an API Key' + this.inputs = [ + { + label: 'Voyage AI Endpoint', + name: 'endpoint', + type: 'string', + default: 'https://api.voyageai.com/v1/embeddings' + }, + { + label: 'Voyage AI API Key', + name: 'apiKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: VoyageAIApi } diff --git a/packages/components/nodes/embeddings/VoyageAIEmbedding/VoyageAIEmbedding.ts b/packages/components/nodes/embeddings/VoyageAIEmbedding/VoyageAIEmbedding.ts new file mode 100644 index 00000000000..82cfcecdc5e --- /dev/null +++ b/packages/components/nodes/embeddings/VoyageAIEmbedding/VoyageAIEmbedding.ts @@ -0,0 +1,84 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { VoyageEmbeddings, VoyageEmbeddingsParams } from 'langchain/embeddings/voyage' + +class VoyageAIEmbedding_Embeddings implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'VoyageAI Embeddings' + this.name = 'voyageAIEmbeddings' + this.version = 1.0 + this.type = 'VoyageAIEmbeddings' + this.icon = 'voyageai.png' + this.category = 'Embeddings' + this.description = 'Voyage AI API to generate embeddings for a given text' + this.baseClasses = [this.type, ...getBaseClasses(VoyageEmbeddings)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['voyageAIApi'] + } + this.inputs = [ + { + label: 'Model Name', + name: 'modelName', + type: 'options', + options: [ + { + label: 'voyage-2', + name: 'voyage-2', + description: 'Base generalist embedding model optimized for both latency and quality' + }, + { + label: 'voyage-code-2', + name: 'voyage-code-2', + description: 'Optimized for code retrieval' + }, + { + label: 'voyage-large-2', + name: 'voyage-large-2', + description: 'Powerful generalist embedding model' + }, + { + label: 'voyage-lite-02-instruct', + name: 'voyage-lite-02-instruct', + description: 'Instruction-tuned for classification, clustering, and sentence textual similarity tasks' + } + ], + default: 'voyage-2', + optional: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const modelName = nodeData.inputs?.modelName as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const voyageAiApiKey = getCredentialParam('apiKey', credentialData, nodeData) + const voyageAiEndpoint = getCredentialParam('endpoint', credentialData, nodeData) + + const obj: Partial & { apiKey?: string } = { + apiKey: voyageAiApiKey + } + + if (modelName) obj.modelName = modelName + + const model = new VoyageEmbeddings(obj) + if (voyageAiEndpoint) model.apiUrl = voyageAiEndpoint + return model + } +} + +module.exports = { nodeClass: VoyageAIEmbedding_Embeddings } diff --git a/packages/components/nodes/embeddings/VoyageAIEmbedding/voyageai.png b/packages/components/nodes/embeddings/VoyageAIEmbedding/voyageai.png new file mode 100644 index 00000000000..83ee56018f0 Binary files /dev/null and b/packages/components/nodes/embeddings/VoyageAIEmbedding/voyageai.png differ diff --git a/packages/components/nodes/retrievers/VoyageAIRetriever/VoyageAIRerank.ts b/packages/components/nodes/retrievers/VoyageAIRetriever/VoyageAIRerank.ts new file mode 100644 index 00000000000..fd43c0b656b --- /dev/null +++ b/packages/components/nodes/retrievers/VoyageAIRetriever/VoyageAIRerank.ts @@ -0,0 +1,52 @@ +import axios from 'axios' +import { Callbacks } from '@langchain/core/callbacks/manager' +import { Document } from '@langchain/core/documents' +import { BaseDocumentCompressor } from 'langchain/retrievers/document_compressors' + +export class VoyageAIRerank extends BaseDocumentCompressor { + private voyageAIAPIKey: any + private readonly VOYAGEAI_RERANK_API_URL = 'https://api.voyageai.com/v1/rerank' + private model: string = 'rerank-lite-1' + private readonly k: number + + constructor(voyageAIAPIKey: string, model: string, k: number) { + super() + this.voyageAIAPIKey = voyageAIAPIKey + this.model = model + this.k = k + } + async compressDocuments( + documents: Document>[], + query: string, + _?: Callbacks | undefined + ): Promise>[]> { + // avoid empty api call + if (documents.length === 0) { + return [] + } + const config = { + headers: { + Authorization: `Bearer ${this.voyageAIAPIKey}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + } + } + const data = { + model: this.model, + query: query, + documents: documents.map((doc) => doc.pageContent) + } + try { + let returnedDocs = await axios.post(this.VOYAGEAI_RERANK_API_URL, data, config) + const finalResults: Document>[] = [] + returnedDocs.data.results.forEach((result: any) => { + const doc = documents[result.index] + doc.metadata.relevance_score = result.relevance_score + finalResults.push(doc) + }) + return finalResults.splice(0, this.k) + } catch (error) { + return documents + } + } +} diff --git a/packages/components/nodes/retrievers/VoyageAIRetriever/VoyageAIRerankRetriever.ts b/packages/components/nodes/retrievers/VoyageAIRetriever/VoyageAIRerankRetriever.ts new file mode 100644 index 00000000000..b5687b4a809 --- /dev/null +++ b/packages/components/nodes/retrievers/VoyageAIRetriever/VoyageAIRerankRetriever.ts @@ -0,0 +1,129 @@ +import { BaseRetriever } from '@langchain/core/retrievers' +import { VectorStoreRetriever } from '@langchain/core/vectorstores' +import { ContextualCompressionRetriever } from 'langchain/retrievers/contextual_compression' +import { VoyageAIRerank } from './VoyageAIRerank' +import { getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class VoyageAIRerankRetriever_Retrievers implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + badge: string + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Voyage AI Rerank Retriever' + this.name = 'voyageAIRerankRetriever' + this.version = 1.0 + this.type = 'VoyageAIRerankRetriever' + this.icon = 'voyageai.png' + this.category = 'Retrievers' + this.badge = 'NEW' + this.description = 'Voyage AI Rerank indexes the documents from most to least semantically relevant to the query.' + this.baseClasses = [this.type, 'BaseRetriever'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['voyageAIApi'] + } + this.inputs = [ + { + label: 'Vector Store Retriever', + name: 'baseRetriever', + type: 'VectorStoreRetriever' + }, + { + label: 'Model Name', + name: 'model', + type: 'options', + options: [ + { + label: 'rerank-lite-1', + name: 'rerank-lite-1' + } + ], + default: 'rerank-lite-1', + optional: true + }, + { + label: 'Query', + name: 'query', + type: 'string', + description: 'Query to retrieve documents from retriever. If not specified, user question will be used', + optional: true, + acceptVariable: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to the TopK of the Base Retriever', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'Voyage AI Rerank Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Document', + name: 'document', + description: 'Array of document objects containing metadata and pageContent', + baseClasses: ['Document', 'json'] + }, + { + label: 'Text', + name: 'text', + description: 'Concatenated string from pageContent of documents', + baseClasses: ['string', 'json'] + } + ] + } + + async init(nodeData: INodeData, input: string, options: ICommonObject): Promise { + const baseRetriever = nodeData.inputs?.baseRetriever as BaseRetriever + const model = nodeData.inputs?.model as string + const query = nodeData.inputs?.query as string + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const voyageAiApiKey = getCredentialParam('apiKey', credentialData, nodeData) + const topK = nodeData.inputs?.topK as string + const k = topK ? parseFloat(topK) : (baseRetriever as VectorStoreRetriever).k ?? 4 + const output = nodeData.outputs?.output as string + + const voyageAICompressor = new VoyageAIRerank(voyageAiApiKey, model, k) + + const retriever = new ContextualCompressionRetriever({ + baseCompressor: voyageAICompressor, + baseRetriever: baseRetriever + }) + + if (output === 'retriever') return retriever + else if (output === 'document') return await retriever.getRelevantDocuments(query ? query : input) + else if (output === 'text') { + let finaltext = '' + + const docs = await retriever.getRelevantDocuments(query ? query : input) + + for (const doc of docs) finaltext += `${doc.pageContent}\n` + + return handleEscapeCharacters(finaltext, false) + } + + return retriever + } +} + +module.exports = { nodeClass: VoyageAIRerankRetriever_Retrievers } diff --git a/packages/components/nodes/retrievers/VoyageAIRetriever/voyageai.png b/packages/components/nodes/retrievers/VoyageAIRetriever/voyageai.png new file mode 100644 index 00000000000..83ee56018f0 Binary files /dev/null and b/packages/components/nodes/retrievers/VoyageAIRetriever/voyageai.png differ