Feature/add ability to upload file from chat (#3059)

add ability to upload file from chat
This commit is contained in:
Henry Heng
2024-08-25 13:22:48 +01:00
committed by GitHub
parent e8f5f07735
commit 66acd0c000
37 changed files with 1111 additions and 259 deletions
@@ -112,6 +112,7 @@ class CSV_Agents implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
base64String += fileData.toString('base64')
}
@@ -123,6 +124,7 @@ class CSV_Agents implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
base64String += splitDataURI.pop() ?? ''
@@ -89,7 +89,7 @@ class AWSChatBedrock_ChatModels implements INode {
name: 'allowImageUploads',
type: 'boolean',
description:
'Only works with claude-3-* models when image is being uploaded from chat. Compatible with LLMChain, Conversation Chain, ReAct Agent, and Conversational Agent',
'Only works with claude-3-* models when image is being uploaded from chat. Compatible with LLMChain, Conversation Chain, ReAct Agent, Conversational Agent, Tool Agent',
default: false,
optional: true
}
@@ -106,7 +106,7 @@ class AzureChatOpenAI_ChatModels implements INode {
name: 'allowImageUploads',
type: 'boolean',
description:
'Automatically uses gpt-4-vision-preview when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, and Conversational Agent',
'Automatically uses gpt-4-vision-preview when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, Conversational Agent, Tool Agent',
default: false,
optional: true
},
@@ -84,7 +84,7 @@ class ChatAnthropic_ChatModels implements INode {
name: 'allowImageUploads',
type: 'boolean',
description:
'Automatically uses claude-3-* models when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, and Conversational Agent',
'Automatically uses claude-3-* models when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, Conversational Agent, Tool Agent',
default: false,
optional: true
}
@@ -1,5 +1,5 @@
import { AnthropicInput, ChatAnthropic as LangchainChatAnthropic } from '@langchain/anthropic'
import { BaseLLMParams } from '@langchain/core/language_models/llms'
import { type BaseChatModelParams } from '@langchain/core/language_models/chat_models'
import { IVisionChatModal, IMultiModalOption } from '../../../src'
export class ChatAnthropic extends LangchainChatAnthropic implements IVisionChatModal {
@@ -8,8 +8,9 @@ export class ChatAnthropic extends LangchainChatAnthropic implements IVisionChat
multiModalOption: IMultiModalOption
id: string
constructor(id: string, fields: Partial<AnthropicInput> & BaseLLMParams & { anthropicApiKey?: string }) {
super(fields)
constructor(id: string, fields?: Partial<AnthropicInput> & BaseChatModelParams) {
// @ts-ignore
super(fields ?? {})
this.id = id
this.configuredModel = fields?.modelName || ''
this.configuredMaxToken = fields?.maxTokens ?? 2048
@@ -145,7 +145,7 @@ class GoogleGenerativeAI_ChatModels implements INode {
name: 'allowImageUploads',
type: 'boolean',
description:
'Automatically uses vision model when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, and Conversational Agent',
'Automatically uses vision model when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, Conversational Agent, Tool Agent',
default: false,
optional: true
}
@@ -115,7 +115,7 @@ class ChatOpenAI_ChatModels implements INode {
name: 'allowImageUploads',
type: 'boolean',
description:
'Automatically uses gpt-4-vision-preview when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, and Conversational Agent',
'Automatically uses gpt-4-vision-preview when image is being uploaded from chat. Only works with LLMChain, Conversation Chain, ReAct Agent, Conversational Agent, Tool Agent',
default: false,
optional: true
},
@@ -108,6 +108,7 @@ class Csv_DocumentLoaders implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
const loader = new CSVLoader(blob, columnName.trim().length === 0 ? undefined : columnName.trim())
@@ -127,6 +128,7 @@ class Csv_DocumentLoaders implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -83,6 +83,7 @@ class Docx_DocumentLoaders implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
const loader = new DocxLoader(blob)
@@ -103,6 +104,7 @@ class Docx_DocumentLoaders implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -0,0 +1,299 @@
import { omit } from 'lodash'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { TextSplitter } from 'langchain/text_splitter'
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { JSONLinesLoader, JSONLoader } from 'langchain/document_loaders/fs/json'
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
import { BaseDocumentLoader } from 'langchain/document_loaders/base'
import { Document } from '@langchain/core/documents'
import { getFileFromStorage } from '../../../src/storageUtils'
import { mapMimeTypeToExt } from '../../../src/utils'
class File_DocumentLoaders implements INode {
label: string
name: string
version: number
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'File Loader'
this.name = 'fileLoader'
this.version = 1.0
this.type = 'Document'
this.icon = 'file.svg'
this.category = 'Document Loaders'
this.description = `A generic file loader that can load txt, json, csv, docx, pdf, and other files`
this.baseClasses = [this.type]
this.inputs = [
{
label: 'File',
name: 'file',
type: 'file',
fileType: '*'
},
{
label: 'Text Splitter',
name: 'textSplitter',
type: 'TextSplitter',
optional: true
},
{
label: 'Pdf Usage',
name: 'pdfUsage',
type: 'options',
description: 'Only when loading PDF files',
options: [
{
label: 'One document per page',
name: 'perPage'
},
{
label: 'One document per file',
name: 'perFile'
}
],
default: 'perPage',
optional: true,
additionalParams: true
},
{
label: 'JSONL Pointer Extraction',
name: 'pointerName',
type: 'string',
description: 'Only when loading JSONL files',
placeholder: '<pointerName>',
optional: true,
additionalParams: true
},
{
label: 'Additional Metadata',
name: 'metadata',
type: 'json',
description: 'Additional metadata to be added to the extracted documents',
optional: true,
additionalParams: true
},
{
label: 'Omit Metadata Keys',
name: 'omitMetadataKeys',
type: 'string',
rows: 4,
description:
'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field',
placeholder: 'key1, key2, key3.nestedKey1',
optional: true,
additionalParams: true
}
]
}
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
const fileBase64 = nodeData.inputs?.file as string
const metadata = nodeData.inputs?.metadata
const pdfUsage = nodeData.inputs?.pdfUsage
const pointerName = nodeData.inputs?.pointerName as string
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
let omitMetadataKeys: string[] = []
if (_omitMetadataKeys) {
omitMetadataKeys = _omitMetadataKeys.split(',').map((key) => key.trim())
}
let files: string[] = []
const fileBlobs: { blob: Blob; ext: string }[] = []
//FILE-STORAGE::["CONTRIBUTING.md","LICENSE.md","README.md"]
const totalFiles = getOverrideFileInputs(nodeData) || fileBase64
if (totalFiles.startsWith('FILE-STORAGE::')) {
const fileName = totalFiles.replace('FILE-STORAGE::', '')
if (fileName.startsWith('[') && fileName.endsWith(']')) {
files = JSON.parse(fileName)
} else {
files = [fileName]
}
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
fileBlobs.push({ blob, ext: file.split('.').pop() || '' })
}
} else {
if (totalFiles.startsWith('[') && totalFiles.endsWith(']')) {
files = JSON.parse(totalFiles)
} else {
files = [totalFiles]
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const blob = new Blob([bf])
let extension = ''
// eslint-disable-next-line no-useless-escape
const match = file.match(/^data:([A-Za-z-+\/]+);base64,/)
if (!match) {
fileBlobs.push({
blob,
ext: extension
})
} else {
const mimeType = match[1]
fileBlobs.push({
blob,
ext: mapMimeTypeToExt(mimeType)
})
}
}
}
const loader = new MultiFileLoader(fileBlobs, {
json: (blob) => new JSONLoader(blob),
jsonl: (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()),
txt: (blob) => new TextLoader(blob),
csv: (blob) => new CSVLoader(blob),
xls: (blob) => new CSVLoader(blob),
xlsx: (blob) => new CSVLoader(blob),
docx: (blob) => new DocxLoader(blob),
doc: (blob) => new DocxLoader(blob),
pdf: (blob) =>
pdfUsage === 'perFile'
? // @ts-ignore
new PDFLoader(blob, { splitPages: false, pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js') })
: // @ts-ignore
new PDFLoader(blob, { pdfjs: () => import('pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js') }),
'': (blob) => new TextLoader(blob)
})
let docs = []
if (textSplitter) {
docs = await loader.load()
docs = await textSplitter.splitDocuments(docs)
} else {
docs = await loader.load()
}
if (metadata) {
const parsedMetadata = typeof metadata === 'object' ? metadata : JSON.parse(metadata)
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {
...parsedMetadata
}
: omit(
{
...doc.metadata,
...parsedMetadata
},
omitMetadataKeys
)
}))
} else {
docs = docs.map((doc) => ({
...doc,
metadata:
_omitMetadataKeys === '*'
? {}
: omit(
{
...doc.metadata
},
omitMetadataKeys
)
}))
}
return docs
}
}
const getOverrideFileInputs = (nodeData: INodeData) => {
const txtFileBase64 = nodeData.inputs?.txtFile as string
const pdfFileBase64 = nodeData.inputs?.pdfFile as string
const jsonFileBase64 = nodeData.inputs?.jsonFile as string
const csvFileBase64 = nodeData.inputs?.csvFile as string
const jsonlinesFileBase64 = nodeData.inputs?.jsonlinesFile as string
const docxFileBase64 = nodeData.inputs?.docxFile as string
const yamlFileBase64 = nodeData.inputs?.yamlFile as string
const removePrefix = (storageFile: string): string[] => {
const fileName = storageFile.replace('FILE-STORAGE::', '')
if (fileName.startsWith('[') && fileName.endsWith(']')) {
return JSON.parse(fileName)
}
return [fileName]
}
// If exists, combine all file inputs into an array
const files: string[] = []
if (txtFileBase64) {
files.push(...removePrefix(txtFileBase64))
}
if (pdfFileBase64) {
files.push(...removePrefix(pdfFileBase64))
}
if (jsonFileBase64) {
files.push(...removePrefix(jsonFileBase64))
}
if (csvFileBase64) {
files.push(...removePrefix(csvFileBase64))
}
if (jsonlinesFileBase64) {
files.push(...removePrefix(jsonlinesFileBase64))
}
if (docxFileBase64) {
files.push(...removePrefix(docxFileBase64))
}
if (yamlFileBase64) {
files.push(...removePrefix(yamlFileBase64))
}
return files.length ? `FILE-STORAGE::${JSON.stringify(files)}` : ''
}
interface LoadersMapping {
[extension: string]: (blob: Blob) => BaseDocumentLoader
}
class MultiFileLoader extends BaseDocumentLoader {
constructor(public fileBlobs: { blob: Blob; ext: string }[], public loaders: LoadersMapping) {
super()
if (Object.keys(loaders).length === 0) {
throw new Error('Must provide at least one loader')
}
}
public async load(): Promise<Document[]> {
const documents: Document[] = []
for (const fileBlob of this.fileBlobs) {
const loaderFactory = this.loaders[fileBlob.ext]
if (loaderFactory) {
const loader = loaderFactory(fileBlob.blob)
documents.push(...(await loader.load()))
} else {
throw new Error(`Error loading file`)
}
}
return documents
}
}
module.exports = { nodeClass: File_DocumentLoaders }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-files"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 3v4a1 1 0 0 0 1 1h4" /><path d="M18 17h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h4l5 5v7a2 2 0 0 1 -2 2z" /><path d="M16 17v2a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h2" /></svg>

After

Width:  |  Height:  |  Size: 505 B

@@ -3,7 +3,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { TextSplitter } from 'langchain/text_splitter'
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { DirectoryLoader } from 'langchain/document_loaders/fs/directory'
import { JSONLoader } from 'langchain/document_loaders/fs/json'
import { JSONLinesLoader, JSONLoader } from 'langchain/document_loaders/fs/json'
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
@@ -22,7 +22,7 @@ class Folder_DocumentLoaders implements INode {
constructor() {
this.label = 'Folder with Files'
this.name = 'folderFiles'
this.version = 2.0
this.version = 3.0
this.type = 'Document'
this.icon = 'folder.svg'
this.category = 'Document Loaders'
@@ -51,6 +51,7 @@ class Folder_DocumentLoaders implements INode {
label: 'Pdf Usage',
name: 'pdfUsage',
type: 'options',
description: 'Only when loading PDF files',
options: [
{
label: 'One document per page',
@@ -65,6 +66,15 @@ class Folder_DocumentLoaders implements INode {
optional: true,
additionalParams: true
},
{
label: 'JSONL Pointer Extraction',
name: 'pointerName',
type: 'string',
description: 'Only when loading JSONL files',
placeholder: '<pointerName>',
optional: true,
additionalParams: true
},
{
label: 'Additional Metadata',
name: 'metadata',
@@ -93,6 +103,7 @@ class Folder_DocumentLoaders implements INode {
const metadata = nodeData.inputs?.metadata
const recursive = nodeData.inputs?.recursive as boolean
const pdfUsage = nodeData.inputs?.pdfUsage
const pointerName = nodeData.inputs?.pointerName as string
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
let omitMetadataKeys: string[] = []
@@ -104,8 +115,12 @@ class Folder_DocumentLoaders implements INode {
folderPath,
{
'.json': (path) => new JSONLoader(path),
'.jsonl': (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()),
'.txt': (path) => new TextLoader(path),
'.csv': (path) => new CSVLoader(path),
'.xls': (path) => new CSVLoader(path),
'.xlsx': (path) => new CSVLoader(path),
'.doc': (path) => new DocxLoader(path),
'.docx': (path) => new DocxLoader(path),
'.pdf': (path) =>
pdfUsage === 'perFile'
@@ -99,6 +99,7 @@ class Json_DocumentLoaders implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined)
@@ -119,6 +120,7 @@ class Json_DocumentLoaders implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -93,6 +93,7 @@ class Jsonlines_DocumentLoaders implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
const loader = new JSONLinesLoader(blob, pointer)
@@ -113,6 +114,7 @@ class Jsonlines_DocumentLoaders implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -109,6 +109,7 @@ class Pdf_DocumentLoaders implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const bf = Buffer.from(fileData)
await this.extractDocs(usage, bf, legacyBuild, textSplitter, docs)
@@ -121,6 +122,7 @@ class Pdf_DocumentLoaders implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -101,6 +101,7 @@ class Text_DocumentLoaders implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
const loader = new TextLoader(blob)
@@ -121,6 +122,7 @@ class Text_DocumentLoaders implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -496,6 +496,7 @@ class UnstructuredFile_DocumentLoaders implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const loaderDocs = await loader.loadAndSplitBuffer(fileData, file)
docs.push(...loaderDocs)
@@ -508,6 +509,7 @@ class UnstructuredFile_DocumentLoaders implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
const filename = splitDataURI.pop()?.split(':')[1] ?? ''
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -209,11 +209,11 @@ class Supervisor_MultiAgents implements INode {
prompt = messages.prompt
multiModalMessageContent = messages.multiModalMessageContent
if (llm.bindTools === undefined) {
if ((llm as any).bindTools === undefined) {
throw new Error(`This agent only compatible with function calling models.`)
}
const modelWithTool = llm.bindTools([tool])
const modelWithTool = (llm as any).bindTools([tool])
const outputParser = new ToolCallingAgentOutputParser()
@@ -464,11 +464,11 @@ class Supervisor_MultiAgents implements INode {
prompt = messages.prompt
multiModalMessageContent = messages.multiModalMessageContent
if (llm.bindTools === undefined) {
if ((llm as any).bindTools === undefined) {
throw new Error(`This agent only compatible with function calling models.`)
}
const modelWithTool = llm.bindTools([tool])
const modelWithTool = (llm as any).bindTools([tool])
const outputParser = new ToolCallingAgentOutputParser()
@@ -4,7 +4,8 @@ import { Document } from '@langchain/core/documents'
import { MilvusLibArgs, Milvus } from '@langchain/community/vectorstores/milvus'
import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { howToUseFileUpload } from '../VectorStoreUtils'
interface InsertRow {
[x: string]: string | number[]
@@ -27,7 +28,7 @@ class Milvus_VectorStores implements INode {
constructor() {
this.label = 'Milvus'
this.name = 'milvus'
this.version = 1.0
this.version = 2.0
this.type = 'Milvus'
this.icon = 'milvus.svg'
this.category = 'Vector Stores'
@@ -64,6 +65,18 @@ class Milvus_VectorStores implements INode {
name: 'milvusCollection',
type: 'string'
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Milvus Text Field',
name: 'milvusTextField',
@@ -116,6 +129,7 @@ class Milvus_VectorStores implements INode {
// embeddings
const docs = nodeData.inputs?.document as Document[]
const embeddings = nodeData.inputs?.embeddings as Embeddings
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
// credential
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
@@ -135,6 +149,9 @@ class Milvus_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -158,8 +175,9 @@ class Milvus_VectorStores implements INode {
// server setup
const address = nodeData.inputs?.milvusServerUrl as string
const collectionName = nodeData.inputs?.milvusCollection as string
const milvusFilter = nodeData.inputs?.milvusFilter as string
const _milvusFilter = nodeData.inputs?.milvusFilter as string
const textField = nodeData.inputs?.milvusTextField as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
// embeddings
const embeddings = nodeData.inputs?.embeddings as Embeddings
@@ -186,6 +204,12 @@ class Milvus_VectorStores implements INode {
if (milvusUser) milVusArgs.username = milvusUser
if (milvusPassword) milVusArgs.password = milvusPassword
let milvusFilter = _milvusFilter
if (isFileUploadEnabled && options.chatId) {
if (milvusFilter) milvusFilter += ` OR ${FLOWISE_CHATID} == "${options.chatId}" OR NOT EXISTS(${FLOWISE_CHATID})`
else milvusFilter = `${FLOWISE_CHATID} == "${options.chatId}" OR NOT EXISTS(${FLOWISE_CHATID})`
}
const vectorStore = await Milvus.fromExistingCollection(embeddings, milVusArgs)
// Avoid Illegal Invocation
@@ -5,8 +5,8 @@ import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { VectorStore } from '@langchain/core/vectorstores'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, howToUseFileUpload, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { index } from '../../../src/indexing'
let pineconeClientSingleton: Pinecone
@@ -43,7 +43,7 @@ class Pinecone_VectorStores implements INode {
constructor() {
this.label = 'Pinecone'
this.name = 'pinecone'
this.version = 4.0
this.version = 5.0
this.type = 'Pinecone'
this.icon = 'pinecone.svg'
this.category = 'Vector Stores'
@@ -88,6 +88,18 @@ class Pinecone_VectorStores implements INode {
additionalParams: true,
optional: true
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Pinecone Text Key',
name: 'pineconeTextKey',
@@ -138,6 +150,7 @@ class Pinecone_VectorStores implements INode {
const embeddings = nodeData.inputs?.embeddings as Embeddings
const recordManager = nodeData.inputs?.recordManager
const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
@@ -150,6 +163,9 @@ class Pinecone_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -232,6 +248,7 @@ class Pinecone_VectorStores implements INode {
const pineconeMetadataFilter = nodeData.inputs?.pineconeMetadataFilter
const embeddings = nodeData.inputs?.embeddings as Embeddings
const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
@@ -250,6 +267,14 @@ class Pinecone_VectorStores implements INode {
const metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter)
obj.filter = metadatafilter
}
if (isFileUploadEnabled && options.chatId) {
obj.filter = obj.filter || {}
obj.filter.$or = [
...(obj.filter.$or || []),
{ [FLOWISE_CHATID]: { $eq: options.chatId } },
{ [FLOWISE_CHATID]: { $exists: false } }
]
}
const vectorStore = (await PineconeStore.fromExistingIndex(embeddings, obj)) as unknown as VectorStore
@@ -5,8 +5,9 @@ import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { TypeORMVectorStore, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
class Postgres_VectorStores implements INode {
label: string
@@ -25,7 +26,7 @@ class Postgres_VectorStores implements INode {
constructor() {
this.label = 'Postgres'
this.name = 'postgres'
this.version = 5.0
this.version = 6.0
this.type = 'Postgres'
this.icon = 'postgres.svg'
this.category = 'Vector Stores'
@@ -82,6 +83,18 @@ class Postgres_VectorStores implements INode {
additionalParams: true,
optional: true
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Additional Configuration',
name: 'additionalConfig',
@@ -132,6 +145,7 @@ class Postgres_VectorStores implements INode {
const embeddings = nodeData.inputs?.embeddings as Embeddings
const additionalConfig = nodeData.inputs?.additionalConfig as string
const recordManager = nodeData.inputs?.recordManager
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
let additionalConfiguration = {}
if (additionalConfig) {
@@ -161,6 +175,9 @@ class Postgres_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -268,11 +285,20 @@ class Postgres_VectorStores implements INode {
const topK = nodeData.inputs?.topK as string
const k = topK ? parseFloat(topK) : 4
const _pgMetadataFilter = nodeData.inputs?.pgMetadataFilter
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
let pgMetadataFilter: any
if (_pgMetadataFilter) {
pgMetadataFilter = typeof _pgMetadataFilter === 'object' ? _pgMetadataFilter : JSON.parse(_pgMetadataFilter)
}
if (isFileUploadEnabled && options.chatId) {
pgMetadataFilter = pgMetadataFilter || {}
pgMetadataFilter = {
...pgMetadataFilter,
[FLOWISE_CHATID]: options.chatId,
$notexists: FLOWISE_CHATID // special filter to check if the field does not exist
}
}
let additionalConfiguration = {}
if (additionalConfig) {
@@ -334,12 +360,20 @@ const similaritySearchVectorWithScore = async (
) => {
const embeddingString = `[${query.join(',')}]`
let _filter = '{}'
if (filter && typeof filter === 'object') _filter = JSON.stringify(filter)
let notExists = ''
if (filter && typeof filter === 'object') {
if (filter.$notexists) {
notExists = `OR NOT (metadata ? '${filter.$notexists}')`
delete filter.$notexists
}
_filter = JSON.stringify(filter)
}
const queryString = `
SELECT *, embedding <=> $1 as "_distance"
FROM ${tableName}
WHERE metadata @> $2
${notExists}
ORDER BY "_distance" ASC
LIMIT $3;`
@@ -6,8 +6,9 @@ import { Document } from '@langchain/core/documents'
import { QdrantVectorStore, QdrantLibArgs } from '@langchain/qdrant'
import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
type RetrieverConfig = Partial<VectorStoreRetrieverInput<QdrantVectorStore>>
type QdrantAddDocumentOptions = {
@@ -32,7 +33,7 @@ class Qdrant_VectorStores implements INode {
constructor() {
this.label = 'Qdrant'
this.name = 'qdrant'
this.version = 4.0
this.version = 5.0
this.type = 'Qdrant'
this.icon = 'qdrant.png'
this.category = 'Vector Stores'
@@ -78,6 +79,18 @@ class Qdrant_VectorStores implements INode {
name: 'qdrantCollection',
type: 'string'
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Vector Dimension',
name: 'qdrantVectorDimension',
@@ -188,6 +201,7 @@ class Qdrant_VectorStores implements INode {
const _batchSize = nodeData.inputs?.batchSize
const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content'
const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata'
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData)
@@ -204,6 +218,9 @@ class Qdrant_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -391,6 +408,7 @@ class Qdrant_VectorStores implements INode {
let queryFilter = nodeData.inputs?.qdrantFilter
const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content'
const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata'
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const k = topK ? parseFloat(topK) : 4
@@ -434,6 +452,25 @@ class Qdrant_VectorStores implements INode {
if (queryFilter) {
retrieverConfig.filter = typeof queryFilter === 'object' ? queryFilter : JSON.parse(queryFilter)
}
if (isFileUploadEnabled && options.chatId) {
retrieverConfig.filter = retrieverConfig.filter || {}
retrieverConfig.filter.should = Array.isArray(retrieverConfig.filter.should) ? retrieverConfig.filter.should : []
retrieverConfig.filter.should.push(
{
key: `metadata.${FLOWISE_CHATID}`,
match: {
value: options.chatId
}
},
{
is_empty: {
key: `metadata.${FLOWISE_CHATID}`
}
}
)
}
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, dbConfig)
@@ -1,12 +1,12 @@
import { flatten } from 'lodash'
import { IndexingResult, INode, INodeOutputsValue, INodeParams, INodeData, ICommonObject } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { UpstashVectorStore } from '@langchain/community/vectorstores/upstash'
import { Index as UpstashIndex } from '@upstash/vector'
import { index } from '../../../src/indexing'
import { resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { howToUseFileUpload, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
type UpstashVectorStoreParams = {
index: UpstashIndex
@@ -29,7 +29,7 @@ class Upstash_VectorStores implements INode {
constructor() {
this.label = 'Upstash Vector'
this.name = 'upstash'
this.version = 1.0
this.version = 2.0
this.type = 'Upstash'
this.icon = 'upstash.svg'
this.category = 'Vector Stores'
@@ -63,6 +63,18 @@ class Upstash_VectorStores implements INode {
description: 'Keep track of the record to prevent duplication',
optional: true
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Upstash Metadata Filter',
name: 'upstashMetadataFilter',
@@ -100,6 +112,7 @@ class Upstash_VectorStores implements INode {
const docs = nodeData.inputs?.document as Document[]
const embeddings = nodeData.inputs?.embeddings as Embeddings
const recordManager = nodeData.inputs?.recordManager
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData)
@@ -114,6 +127,9 @@ class Upstash_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -186,6 +202,7 @@ class Upstash_VectorStores implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const upstashMetadataFilter = nodeData.inputs?.upstashMetadataFilter
const embeddings = nodeData.inputs?.embeddings as Embeddings
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData)
@@ -203,6 +220,10 @@ class Upstash_VectorStores implements INode {
if (upstashMetadataFilter) {
obj.filter = upstashMetadataFilter
}
if (isFileUploadEnabled && options.chatId) {
if (upstashMetadataFilter) obj.filter += ` OR ${FLOWISE_CHATID} = "${options.chatId}" OR HAS NOT FIELD ${FLOWISE_CHATID}`
else obj.filter = `${FLOWISE_CHATID} = "${options.chatId}" OR HAS NOT FIELD ${FLOWISE_CHATID}`
}
const vectorStore = await UpstashVectorStore.fromExistingIndex(embeddings, obj)
@@ -194,6 +194,7 @@ class Vectara_VectorStores implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
vectaraFiles.push({ blob: blob, fileName: getFileName(file) })
@@ -206,6 +207,7 @@ class Vectara_VectorStores implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -83,3 +83,19 @@ export const addMMRInputParams = (inputs: any[]) => {
inputs.push(...mmrInputParams)
}
export const howToUseFileUpload = `
**File Upload**
This allows file upload on the chat. Uploaded files will be upserted on the fly to the vector store.
**Note:**
- You can only turn on file upload for one vector store at a time.
- At least one Document Loader node should be connected to the document input.
- Document Loader should be file types like PDF, DOCX, TXT, etc.
**How it works**
- Uploaded files will have the metadata updated with the chatId.
- This will allow the file to be associated with the chatId.
- When querying, metadata will be filtered by chatId to retrieve files associated with the chatId.
`
+66
View File
@@ -11,6 +11,8 @@ import { AIMessage, HumanMessage, BaseMessage } from '@langchain/core/messages'
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
export const FLOWISE_CHATID = 'flowise_chatId'
/*
* List of dependencies allowed to be import in vm2
*/
@@ -815,3 +817,67 @@ export const getVersion: () => Promise<{ version: string }> = async () => {
throw new Error('None of the package.json paths could be parsed')
}
/**
* Map MimeType to InputField
* @param {string} mimeType
* @returns {string}
*/
export const mapMimeTypeToInputField = (mimeType: string) => {
switch (mimeType) {
case 'text/plain':
return 'txtFile'
case 'application/pdf':
return 'pdfFile'
case 'application/json':
return 'jsonFile'
case 'text/csv':
return 'csvFile'
case 'application/json-lines':
case 'application/jsonl':
case 'text/jsonl':
return 'jsonlinesFile'
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return 'docxFile'
case 'application/vnd.yaml':
case 'application/x-yaml':
case 'text/vnd.yaml':
case 'text/x-yaml':
case 'text/yaml':
return 'yamlFile'
default:
return 'txtFile'
}
}
/**
* Map MimeType to Extension
* @param {string} mimeType
* @returns {string}
*/
export const mapMimeTypeToExt = (mimeType: string) => {
switch (mimeType) {
case 'text/plain':
return 'txt'
case 'application/pdf':
return 'pdf'
case 'application/json':
return 'json'
case 'text/csv':
return 'csv'
case 'application/json-lines':
case 'application/jsonl':
case 'text/jsonl':
return 'jsonl'
case 'application/msword':
return 'doc'
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return 'docx'
case 'application/vnd.ms-excel':
return 'xls'
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return 'xlsx'
default:
return ''
}
}