diff --git a/packages/components/nodes/chains/VectaraChain/VectaraChain.ts b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts new file mode 100644 index 000000000..a2fac534e --- /dev/null +++ b/packages/components/nodes/chains/VectaraChain/VectaraChain.ts @@ -0,0 +1,147 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src/utils' +import { VectorDBQAChain } from 'langchain/chains' +import { Document } from 'langchain/document' +import { VectaraStore } from 'langchain/vectorstores/vectara' +import fetch from 'node-fetch' + +class VectaraChain_Chains implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + baseClasses: string[] + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Vectara QA Chain' + this.name = 'vectaraQAChain' + this.version = 1.0 + this.type = 'VectaraQAChain' + this.icon = 'vectara.png' + this.category = 'Chains' + this.description = 'QA chain for Vectara' + this.baseClasses = [this.type, ...getBaseClasses(VectorDBQAChain)] + this.inputs = [ + { + label: 'Vectara Vector Store', + name: 'vectaraStore', + type: 'VectorStore' + } + ] + } + + async init(): Promise { + return null + } + + async run(nodeData: INodeData, input: string): Promise { + const vectorStore = nodeData.inputs?.vectaraStore as VectaraStore + const topK = (vectorStore as any)?.k ?? 4 + + const headers = await vectorStore.getJsonHeader() + const vectaraFilter = (vectorStore as any).vectaraFilter ?? {} + const corpusId: number[] = (vectorStore as any).corpusId ?? [] + const customerId = (vectorStore as any).customerId ?? '' + + const corpusKeys = corpusId.map((corpusId) => ({ + customerId, + corpusId, + metadataFilter: vectaraFilter?.filter ?? '', + lexicalInterpolationConfig: { lambda: vectaraFilter?.lambda ?? 0.025 } + })) + + let summarizerPromptName = 'vectara-experimental-summary-ext-2023-10-23-med' // can let user select + let responseLang = 'en' // can let user select + let maxSummarizedResults = 5 // can let user specify + + const data = { + query: [ + { + query: input, + start: 0, + numResults: topK, + contextConfig: { + sentencesAfter: vectaraFilter?.contextConfig?.sentencesAfter ?? 2, + sentencesBefore: vectaraFilter?.contextConfig?.sentencesBefore ?? 2 + }, + corpusKey: corpusKeys, + summary: [ + { + summarizerPromptName, + responseLang, + maxSummarizedResults + } + ] + } + ] + } + + try { + const response = await fetch(`https://api.vectara.io/v1/query`, { + method: 'POST', + headers: headers?.headers, + body: JSON.stringify(data) + }) + + if (response.status !== 200) { + throw new Error(`Vectara API returned status code ${response.status}`) + } + + const result = await response.json() + const responses = result.responseSet[0].response + const documents = result.responseSet[0].document + let summarizedText = '' + + for (let i = 0; i < responses.length; i += 1) { + const responseMetadata = responses[i].metadata + const documentMetadata = documents[responses[i].documentIndex].metadata + const combinedMetadata: Record = {} + + responseMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + documentMetadata.forEach((item: { name: string; value: unknown }) => { + combinedMetadata[item.name] = item.value + }) + + responses[i].metadata = combinedMetadata + } + + const summaryStatus = result.responseSet[0].summary[0].status + if (summaryStatus.length > 0 && summaryStatus[0].code === 'BAD_REQUEST') { + throw new Error( + `BAD REQUEST: Too much text for the summarizer to summarize. Please try reducing the number of search results to summarize, or the context of each result by adjusting the 'summary_num_sentences', and 'summary_num_results' parameters respectively.` + ) + } + + if ( + summaryStatus.length > 0 && + summaryStatus[0].code === 'NOT_FOUND' && + summaryStatus[0].statusDetail === 'Failed to retrieve summarizer.' + ) { + throw new Error(`BAD REQUEST: summarizer ${summarizerPromptName} is invalid for this account.`) + } + + summarizedText = result.responseSet[0].summary[0]?.text + + const sourceDocuments: Document[] = responses.map( + (response: { text: string; metadata: Record; score: number }) => + new Document({ + pageContent: response.text, + metadata: response.metadata + }) + ) + + return { text: summarizedText, sourceDocuments: sourceDocuments } + } catch (error) { + throw new Error(error) + } + } +} + +module.exports = { nodeClass: VectaraChain_Chains } diff --git a/packages/components/nodes/chains/VectaraChain/vectara.png b/packages/components/nodes/chains/VectaraChain/vectara.png new file mode 100644 index 000000000..a13a34e6b Binary files /dev/null and b/packages/components/nodes/chains/VectaraChain/vectara.png differ diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 239773a9a..616454a57 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -842,7 +842,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod let isValidChainOrAgent = false if (endingNodeData.category === 'Chains') { // Chains that are not available to stream - const blacklistChains = ['openApiChain'] + const blacklistChains = ['openApiChain', 'vectaraQAChain'] isValidChainOrAgent = !blacklistChains.includes(endingNodeData.name) } else if (endingNodeData.category === 'Agents') { // Agent that are available to stream diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 29a64155c..cadd4abd9 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -699,7 +699,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return ( { if (!message.sourceDocuments) return newSourceDocuments message.sourceDocuments.forEach((source) => { - if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { - visitedURLs.push(source.metadata.source) - newSourceDocuments.push(source) - } else if (!isValidURL(source.metadata.source)) { + if (source.metadata && source.metadata.source) { + if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) { + visitedURLs.push(source.metadata.source) + newSourceDocuments.push(source) + } else if (!isValidURL(source.metadata.source)) { + newSourceDocuments.push(source) + } + } else { newSourceDocuments.push(source) } }) diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 7cfd04740..7e805f7e9 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -379,7 +379,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { - const URL = isValidURL(source.metadata.source) + const URL = + source.metadata && source.metadata.source + ? isValidURL(source.metadata.source) + : undefined return (