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 const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
base64String += fileData.toString('base64') base64String += fileData.toString('base64')
} }
@@ -123,6 +124,7 @@ class CSV_Agents implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
base64String += splitDataURI.pop() ?? '' base64String += splitDataURI.pop() ?? ''
@@ -89,7 +89,7 @@ class AWSChatBedrock_ChatModels implements INode {
name: 'allowImageUploads', name: 'allowImageUploads',
type: 'boolean', type: 'boolean',
description: 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, default: false,
optional: true optional: true
} }
@@ -106,7 +106,7 @@ class AzureChatOpenAI_ChatModels implements INode {
name: 'allowImageUploads', name: 'allowImageUploads',
type: 'boolean', type: 'boolean',
description: 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, default: false,
optional: true optional: true
}, },
@@ -84,7 +84,7 @@ class ChatAnthropic_ChatModels implements INode {
name: 'allowImageUploads', name: 'allowImageUploads',
type: 'boolean', type: 'boolean',
description: 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, default: false,
optional: true optional: true
} }
@@ -1,5 +1,5 @@
import { AnthropicInput, ChatAnthropic as LangchainChatAnthropic } from '@langchain/anthropic' 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' import { IVisionChatModal, IMultiModalOption } from '../../../src'
export class ChatAnthropic extends LangchainChatAnthropic implements IVisionChatModal { export class ChatAnthropic extends LangchainChatAnthropic implements IVisionChatModal {
@@ -8,8 +8,9 @@ export class ChatAnthropic extends LangchainChatAnthropic implements IVisionChat
multiModalOption: IMultiModalOption multiModalOption: IMultiModalOption
id: string id: string
constructor(id: string, fields: Partial<AnthropicInput> & BaseLLMParams & { anthropicApiKey?: string }) { constructor(id: string, fields?: Partial<AnthropicInput> & BaseChatModelParams) {
super(fields) // @ts-ignore
super(fields ?? {})
this.id = id this.id = id
this.configuredModel = fields?.modelName || '' this.configuredModel = fields?.modelName || ''
this.configuredMaxToken = fields?.maxTokens ?? 2048 this.configuredMaxToken = fields?.maxTokens ?? 2048
@@ -145,7 +145,7 @@ class GoogleGenerativeAI_ChatModels implements INode {
name: 'allowImageUploads', name: 'allowImageUploads',
type: 'boolean', type: 'boolean',
description: 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, default: false,
optional: true optional: true
} }
@@ -115,7 +115,7 @@ class ChatOpenAI_ChatModels implements INode {
name: 'allowImageUploads', name: 'allowImageUploads',
type: 'boolean', type: 'boolean',
description: 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, default: false,
optional: true optional: true
}, },
@@ -108,6 +108,7 @@ class Csv_DocumentLoaders implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData]) const blob = new Blob([fileData])
const loader = new CSVLoader(blob, columnName.trim().length === 0 ? undefined : columnName.trim()) 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) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -83,6 +83,7 @@ class Docx_DocumentLoaders implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData]) const blob = new Blob([fileData])
const loader = new DocxLoader(blob) const loader = new DocxLoader(blob)
@@ -103,6 +104,7 @@ class Docx_DocumentLoaders implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') 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 { TextSplitter } from 'langchain/text_splitter'
import { TextLoader } from 'langchain/document_loaders/fs/text' import { TextLoader } from 'langchain/document_loaders/fs/text'
import { DirectoryLoader } from 'langchain/document_loaders/fs/directory' 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 { CSVLoader } from '@langchain/community/document_loaders/fs/csv'
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf' import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx' import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'
@@ -22,7 +22,7 @@ class Folder_DocumentLoaders implements INode {
constructor() { constructor() {
this.label = 'Folder with Files' this.label = 'Folder with Files'
this.name = 'folderFiles' this.name = 'folderFiles'
this.version = 2.0 this.version = 3.0
this.type = 'Document' this.type = 'Document'
this.icon = 'folder.svg' this.icon = 'folder.svg'
this.category = 'Document Loaders' this.category = 'Document Loaders'
@@ -51,6 +51,7 @@ class Folder_DocumentLoaders implements INode {
label: 'Pdf Usage', label: 'Pdf Usage',
name: 'pdfUsage', name: 'pdfUsage',
type: 'options', type: 'options',
description: 'Only when loading PDF files',
options: [ options: [
{ {
label: 'One document per page', label: 'One document per page',
@@ -65,6 +66,15 @@ class Folder_DocumentLoaders implements INode {
optional: true, optional: true,
additionalParams: 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', label: 'Additional Metadata',
name: 'metadata', name: 'metadata',
@@ -93,6 +103,7 @@ class Folder_DocumentLoaders implements INode {
const metadata = nodeData.inputs?.metadata const metadata = nodeData.inputs?.metadata
const recursive = nodeData.inputs?.recursive as boolean const recursive = nodeData.inputs?.recursive as boolean
const pdfUsage = nodeData.inputs?.pdfUsage const pdfUsage = nodeData.inputs?.pdfUsage
const pointerName = nodeData.inputs?.pointerName as string
const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string
let omitMetadataKeys: string[] = [] let omitMetadataKeys: string[] = []
@@ -104,8 +115,12 @@ class Folder_DocumentLoaders implements INode {
folderPath, folderPath,
{ {
'.json': (path) => new JSONLoader(path), '.json': (path) => new JSONLoader(path),
'.jsonl': (blob) => new JSONLinesLoader(blob, '/' + pointerName.trim()),
'.txt': (path) => new TextLoader(path), '.txt': (path) => new TextLoader(path),
'.csv': (path) => new CSVLoader(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), '.docx': (path) => new DocxLoader(path),
'.pdf': (path) => '.pdf': (path) =>
pdfUsage === 'perFile' pdfUsage === 'perFile'
@@ -99,6 +99,7 @@ class Json_DocumentLoaders implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData]) const blob = new Blob([fileData])
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined) const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined)
@@ -119,6 +120,7 @@ class Json_DocumentLoaders implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -93,6 +93,7 @@ class Jsonlines_DocumentLoaders implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData]) const blob = new Blob([fileData])
const loader = new JSONLinesLoader(blob, pointer) const loader = new JSONLinesLoader(blob, pointer)
@@ -113,6 +114,7 @@ class Jsonlines_DocumentLoaders implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -109,6 +109,7 @@ class Pdf_DocumentLoaders implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const bf = Buffer.from(fileData) const bf = Buffer.from(fileData)
await this.extractDocs(usage, bf, legacyBuild, textSplitter, docs) await this.extractDocs(usage, bf, legacyBuild, textSplitter, docs)
@@ -121,6 +122,7 @@ class Pdf_DocumentLoaders implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -101,6 +101,7 @@ class Text_DocumentLoaders implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData]) const blob = new Blob([fileData])
const loader = new TextLoader(blob) const loader = new TextLoader(blob)
@@ -121,6 +122,7 @@ class Text_DocumentLoaders implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -496,6 +496,7 @@ class UnstructuredFile_DocumentLoaders implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const loaderDocs = await loader.loadAndSplitBuffer(fileData, file) const loaderDocs = await loader.loadAndSplitBuffer(fileData, file)
docs.push(...loaderDocs) docs.push(...loaderDocs)
@@ -508,6 +509,7 @@ class UnstructuredFile_DocumentLoaders implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
const filename = splitDataURI.pop()?.split(':')[1] ?? '' const filename = splitDataURI.pop()?.split(':')[1] ?? ''
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -209,11 +209,11 @@ class Supervisor_MultiAgents implements INode {
prompt = messages.prompt prompt = messages.prompt
multiModalMessageContent = messages.multiModalMessageContent multiModalMessageContent = messages.multiModalMessageContent
if (llm.bindTools === undefined) { if ((llm as any).bindTools === undefined) {
throw new Error(`This agent only compatible with function calling models.`) 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() const outputParser = new ToolCallingAgentOutputParser()
@@ -464,11 +464,11 @@ class Supervisor_MultiAgents implements INode {
prompt = messages.prompt prompt = messages.prompt
multiModalMessageContent = messages.multiModalMessageContent multiModalMessageContent = messages.multiModalMessageContent
if (llm.bindTools === undefined) { if ((llm as any).bindTools === undefined) {
throw new Error(`This agent only compatible with function calling models.`) 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() const outputParser = new ToolCallingAgentOutputParser()
@@ -4,7 +4,8 @@ import { Document } from '@langchain/core/documents'
import { MilvusLibArgs, Milvus } from '@langchain/community/vectorstores/milvus' import { MilvusLibArgs, Milvus } from '@langchain/community/vectorstores/milvus'
import { Embeddings } from '@langchain/core/embeddings' import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' 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 { interface InsertRow {
[x: string]: string | number[] [x: string]: string | number[]
@@ -27,7 +28,7 @@ class Milvus_VectorStores implements INode {
constructor() { constructor() {
this.label = 'Milvus' this.label = 'Milvus'
this.name = 'milvus' this.name = 'milvus'
this.version = 1.0 this.version = 2.0
this.type = 'Milvus' this.type = 'Milvus'
this.icon = 'milvus.svg' this.icon = 'milvus.svg'
this.category = 'Vector Stores' this.category = 'Vector Stores'
@@ -64,6 +65,18 @@ class Milvus_VectorStores implements INode {
name: 'milvusCollection', name: 'milvusCollection',
type: 'string' 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', label: 'Milvus Text Field',
name: 'milvusTextField', name: 'milvusTextField',
@@ -116,6 +129,7 @@ class Milvus_VectorStores implements INode {
// embeddings // embeddings
const docs = nodeData.inputs?.document as Document[] const docs = nodeData.inputs?.document as Document[]
const embeddings = nodeData.inputs?.embeddings as Embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
// credential // credential
const credentialData = await getCredentialData(nodeData.credential ?? '', options) const credentialData = await getCredentialData(nodeData.credential ?? '', options)
@@ -135,6 +149,9 @@ class Milvus_VectorStores implements INode {
const finalDocs = [] const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) { for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) { 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])) finalDocs.push(new Document(flattenDocs[i]))
} }
} }
@@ -158,8 +175,9 @@ class Milvus_VectorStores implements INode {
// server setup // server setup
const address = nodeData.inputs?.milvusServerUrl as string const address = nodeData.inputs?.milvusServerUrl as string
const collectionName = nodeData.inputs?.milvusCollection 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 textField = nodeData.inputs?.milvusTextField as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
// embeddings // embeddings
const embeddings = nodeData.inputs?.embeddings as Embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings
@@ -186,6 +204,12 @@ class Milvus_VectorStores implements INode {
if (milvusUser) milVusArgs.username = milvusUser if (milvusUser) milVusArgs.username = milvusUser
if (milvusPassword) milVusArgs.password = milvusPassword 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) const vectorStore = await Milvus.fromExistingCollection(embeddings, milVusArgs)
// Avoid Illegal Invocation // Avoid Illegal Invocation
@@ -5,8 +5,8 @@ import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents' import { Document } from '@langchain/core/documents'
import { VectorStore } from '@langchain/core/vectorstores' import { VectorStore } from '@langchain/core/vectorstores'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' 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 { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' import { addMMRInputParams, howToUseFileUpload, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { index } from '../../../src/indexing' import { index } from '../../../src/indexing'
let pineconeClientSingleton: Pinecone let pineconeClientSingleton: Pinecone
@@ -43,7 +43,7 @@ class Pinecone_VectorStores implements INode {
constructor() { constructor() {
this.label = 'Pinecone' this.label = 'Pinecone'
this.name = 'pinecone' this.name = 'pinecone'
this.version = 4.0 this.version = 5.0
this.type = 'Pinecone' this.type = 'Pinecone'
this.icon = 'pinecone.svg' this.icon = 'pinecone.svg'
this.category = 'Vector Stores' this.category = 'Vector Stores'
@@ -88,6 +88,18 @@ class Pinecone_VectorStores implements INode {
additionalParams: true, additionalParams: true,
optional: 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', label: 'Pinecone Text Key',
name: 'pineconeTextKey', name: 'pineconeTextKey',
@@ -138,6 +150,7 @@ class Pinecone_VectorStores implements INode {
const embeddings = nodeData.inputs?.embeddings as Embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings
const recordManager = nodeData.inputs?.recordManager const recordManager = nodeData.inputs?.recordManager
const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options) const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
@@ -150,6 +163,9 @@ class Pinecone_VectorStores implements INode {
const finalDocs = [] const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) { for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) { 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])) finalDocs.push(new Document(flattenDocs[i]))
} }
} }
@@ -232,6 +248,7 @@ class Pinecone_VectorStores implements INode {
const pineconeMetadataFilter = nodeData.inputs?.pineconeMetadataFilter const pineconeMetadataFilter = nodeData.inputs?.pineconeMetadataFilter
const embeddings = nodeData.inputs?.embeddings as Embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings
const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options) const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData) const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
@@ -250,6 +267,14 @@ class Pinecone_VectorStores implements INode {
const metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter) const metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter)
obj.filter = metadatafilter 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 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 { Document } from '@langchain/core/documents'
import { TypeORMVectorStore, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm' import { TypeORMVectorStore, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' 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 { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
class Postgres_VectorStores implements INode { class Postgres_VectorStores implements INode {
label: string label: string
@@ -25,7 +26,7 @@ class Postgres_VectorStores implements INode {
constructor() { constructor() {
this.label = 'Postgres' this.label = 'Postgres'
this.name = 'postgres' this.name = 'postgres'
this.version = 5.0 this.version = 6.0
this.type = 'Postgres' this.type = 'Postgres'
this.icon = 'postgres.svg' this.icon = 'postgres.svg'
this.category = 'Vector Stores' this.category = 'Vector Stores'
@@ -82,6 +83,18 @@ class Postgres_VectorStores implements INode {
additionalParams: true, additionalParams: true,
optional: 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', label: 'Additional Configuration',
name: 'additionalConfig', name: 'additionalConfig',
@@ -132,6 +145,7 @@ class Postgres_VectorStores implements INode {
const embeddings = nodeData.inputs?.embeddings as Embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings
const additionalConfig = nodeData.inputs?.additionalConfig as string const additionalConfig = nodeData.inputs?.additionalConfig as string
const recordManager = nodeData.inputs?.recordManager const recordManager = nodeData.inputs?.recordManager
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
let additionalConfiguration = {} let additionalConfiguration = {}
if (additionalConfig) { if (additionalConfig) {
@@ -161,6 +175,9 @@ class Postgres_VectorStores implements INode {
const finalDocs = [] const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) { for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) { 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])) finalDocs.push(new Document(flattenDocs[i]))
} }
} }
@@ -268,11 +285,20 @@ class Postgres_VectorStores implements INode {
const topK = nodeData.inputs?.topK as string const topK = nodeData.inputs?.topK as string
const k = topK ? parseFloat(topK) : 4 const k = topK ? parseFloat(topK) : 4
const _pgMetadataFilter = nodeData.inputs?.pgMetadataFilter const _pgMetadataFilter = nodeData.inputs?.pgMetadataFilter
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
let pgMetadataFilter: any let pgMetadataFilter: any
if (_pgMetadataFilter) { if (_pgMetadataFilter) {
pgMetadataFilter = typeof _pgMetadataFilter === 'object' ? _pgMetadataFilter : JSON.parse(_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 = {} let additionalConfiguration = {}
if (additionalConfig) { if (additionalConfig) {
@@ -334,12 +360,20 @@ const similaritySearchVectorWithScore = async (
) => { ) => {
const embeddingString = `[${query.join(',')}]` const embeddingString = `[${query.join(',')}]`
let _filter = '{}' 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 = ` const queryString = `
SELECT *, embedding <=> $1 as "_distance" SELECT *, embedding <=> $1 as "_distance"
FROM ${tableName} FROM ${tableName}
WHERE metadata @> $2 WHERE metadata @> $2
${notExists}
ORDER BY "_distance" ASC ORDER BY "_distance" ASC
LIMIT $3;` LIMIT $3;`
@@ -6,8 +6,9 @@ import { Document } from '@langchain/core/documents'
import { QdrantVectorStore, QdrantLibArgs } from '@langchain/qdrant' import { QdrantVectorStore, QdrantLibArgs } from '@langchain/qdrant'
import { Embeddings } from '@langchain/core/embeddings' import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface' 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 { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
type RetrieverConfig = Partial<VectorStoreRetrieverInput<QdrantVectorStore>> type RetrieverConfig = Partial<VectorStoreRetrieverInput<QdrantVectorStore>>
type QdrantAddDocumentOptions = { type QdrantAddDocumentOptions = {
@@ -32,7 +33,7 @@ class Qdrant_VectorStores implements INode {
constructor() { constructor() {
this.label = 'Qdrant' this.label = 'Qdrant'
this.name = 'qdrant' this.name = 'qdrant'
this.version = 4.0 this.version = 5.0
this.type = 'Qdrant' this.type = 'Qdrant'
this.icon = 'qdrant.png' this.icon = 'qdrant.png'
this.category = 'Vector Stores' this.category = 'Vector Stores'
@@ -78,6 +79,18 @@ class Qdrant_VectorStores implements INode {
name: 'qdrantCollection', name: 'qdrantCollection',
type: 'string' 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', label: 'Vector Dimension',
name: 'qdrantVectorDimension', name: 'qdrantVectorDimension',
@@ -188,6 +201,7 @@ class Qdrant_VectorStores implements INode {
const _batchSize = nodeData.inputs?.batchSize const _batchSize = nodeData.inputs?.batchSize
const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content' const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content'
const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata' const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata'
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options) const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData) const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData)
@@ -204,6 +218,9 @@ class Qdrant_VectorStores implements INode {
const finalDocs = [] const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) { for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) { 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])) finalDocs.push(new Document(flattenDocs[i]))
} }
} }
@@ -391,6 +408,7 @@ class Qdrant_VectorStores implements INode {
let queryFilter = nodeData.inputs?.qdrantFilter let queryFilter = nodeData.inputs?.qdrantFilter
const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content' const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content'
const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata' const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata'
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const k = topK ? parseFloat(topK) : 4 const k = topK ? parseFloat(topK) : 4
@@ -434,6 +452,25 @@ class Qdrant_VectorStores implements INode {
if (queryFilter) { if (queryFilter) {
retrieverConfig.filter = typeof queryFilter === 'object' ? queryFilter : JSON.parse(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) const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, dbConfig)
@@ -1,12 +1,12 @@
import { flatten } from 'lodash' import { flatten } from 'lodash'
import { IndexingResult, INode, INodeOutputsValue, INodeParams, INodeData, ICommonObject } from '../../../src/Interface' 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 { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents' import { Document } from '@langchain/core/documents'
import { UpstashVectorStore } from '@langchain/community/vectorstores/upstash' import { UpstashVectorStore } from '@langchain/community/vectorstores/upstash'
import { Index as UpstashIndex } from '@upstash/vector' import { Index as UpstashIndex } from '@upstash/vector'
import { index } from '../../../src/indexing' import { index } from '../../../src/indexing'
import { resolveVectorStoreOrRetriever } from '../VectorStoreUtils' import { howToUseFileUpload, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
type UpstashVectorStoreParams = { type UpstashVectorStoreParams = {
index: UpstashIndex index: UpstashIndex
@@ -29,7 +29,7 @@ class Upstash_VectorStores implements INode {
constructor() { constructor() {
this.label = 'Upstash Vector' this.label = 'Upstash Vector'
this.name = 'upstash' this.name = 'upstash'
this.version = 1.0 this.version = 2.0
this.type = 'Upstash' this.type = 'Upstash'
this.icon = 'upstash.svg' this.icon = 'upstash.svg'
this.category = 'Vector Stores' this.category = 'Vector Stores'
@@ -63,6 +63,18 @@ class Upstash_VectorStores implements INode {
description: 'Keep track of the record to prevent duplication', description: 'Keep track of the record to prevent duplication',
optional: 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: 'Upstash Metadata Filter', label: 'Upstash Metadata Filter',
name: 'upstashMetadataFilter', name: 'upstashMetadataFilter',
@@ -100,6 +112,7 @@ class Upstash_VectorStores implements INode {
const docs = nodeData.inputs?.document as Document[] const docs = nodeData.inputs?.document as Document[]
const embeddings = nodeData.inputs?.embeddings as Embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings
const recordManager = nodeData.inputs?.recordManager const recordManager = nodeData.inputs?.recordManager
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options) const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData) const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData)
@@ -114,6 +127,9 @@ class Upstash_VectorStores implements INode {
const finalDocs = [] const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) { for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) { 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])) finalDocs.push(new Document(flattenDocs[i]))
} }
} }
@@ -186,6 +202,7 @@ class Upstash_VectorStores implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> { async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const upstashMetadataFilter = nodeData.inputs?.upstashMetadataFilter const upstashMetadataFilter = nodeData.inputs?.upstashMetadataFilter
const embeddings = nodeData.inputs?.embeddings as Embeddings const embeddings = nodeData.inputs?.embeddings as Embeddings
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options) const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData) const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData)
@@ -203,6 +220,10 @@ class Upstash_VectorStores implements INode {
if (upstashMetadataFilter) { if (upstashMetadataFilter) {
obj.filter = 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) const vectorStore = await UpstashVectorStore.fromExistingIndex(embeddings, obj)
@@ -194,6 +194,7 @@ class Vectara_VectorStores implements INode {
const chatflowid = options.chatflowid const chatflowid = options.chatflowid
for (const file of files) { for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid) const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData]) const blob = new Blob([fileData])
vectaraFiles.push({ blob: blob, fileName: getFileName(file) }) vectaraFiles.push({ blob: blob, fileName: getFileName(file) })
@@ -206,6 +207,7 @@ class Vectara_VectorStores implements INode {
} }
for (const file of files) { for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',') const splitDataURI = file.split(',')
splitDataURI.pop() splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -83,3 +83,19 @@ export const addMMRInputParams = (inputs: any[]) => {
inputs.push(...mmrInputParams) 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 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 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 * 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') 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 ''
}
}
@@ -5,7 +5,7 @@
"nodes": [ "nodes": [
{ {
"width": 300, "width": 300,
"height": 513, "height": 511,
"id": "promptTemplate_0", "id": "promptTemplate_0",
"position": { "position": {
"x": 384.4880563109088, "x": 384.4880563109088,
@@ -65,7 +65,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 508, "height": 507,
"id": "llmChain_0", "id": "llmChain_0",
"position": { "position": {
"x": 770.4559230968546, "x": 770.4559230968546,
@@ -164,7 +164,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 670, "height": 669,
"id": "chatOpenAI_0", "id": "chatOpenAI_0",
"position": { "position": {
"x": 376.92707114970364, "x": 376.92707114970364,
@@ -343,7 +343,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 670, "height": 669,
"id": "chatOpenAI_1", "id": "chatOpenAI_1",
"position": { "position": {
"x": 2653.726672579251, "x": 2653.726672579251,
@@ -522,7 +522,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 508, "height": 507,
"id": "llmChain_1", "id": "llmChain_1",
"position": { "position": {
"x": 3089.9937691022837, "x": 3089.9937691022837,
@@ -621,7 +621,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 670, "height": 674,
"id": "customFunction_2", "id": "customFunction_2",
"position": { "position": {
"x": -19.95227863012829, "x": -19.95227863012829,
@@ -709,7 +709,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 670, "height": 674,
"id": "customFunction_1", "id": "customFunction_1",
"position": { "position": {
"x": 1887.4670208331604, "x": 1887.4670208331604,
@@ -797,7 +797,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 513, "height": 511,
"id": "promptTemplate_1", "id": "promptTemplate_1",
"position": { "position": {
"x": 2655.2632506040304, "x": 2655.2632506040304,
@@ -857,7 +857,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 305, "height": 304,
"id": "getVariable_1", "id": "getVariable_1",
"position": { "position": {
"x": 2272.8555266616872, "x": 2272.8555266616872,
@@ -919,7 +919,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 356, "height": 355,
"id": "setVariable_1", "id": "setVariable_1",
"position": { "position": {
"x": 1516.338224315744, "x": 1516.338224315744,
@@ -991,7 +991,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 757, "height": 765,
"id": "ifElseFunction_0", "id": "ifElseFunction_0",
"position": { "position": {
"x": 1147.8020838770517, "x": 1147.8020838770517,
@@ -1050,7 +1050,7 @@
"inputs": { "inputs": {
"functionInputVariables": "{\"sqlQuery\":\"{{llmChain_0.data.instance}}\"}", "functionInputVariables": "{\"sqlQuery\":\"{{llmChain_0.data.instance}}\"}",
"functionName": "IF SQL Query contains SELECT and WHERE", "functionName": "IF SQL Query contains SELECT and WHERE",
"ifFunction": "const sqlQuery = $sqlQuery.trim();\n\nif (sqlQuery.includes(\"SELECT\") && sqlQuery.includes(\"WHERE\")) {\n return sqlQuery;\n}", "ifFunction": "const sqlQuery = $sqlQuery.trim();\n\nconst regex = /SELECT\\s.*?(?:\\n|$)/gi;\n\n// Extracting the SQL part\nconst matches = sqlQuery.match(regex);\nconst cleanSql = matches ? matches[0].trim() : \"\";\n\nif (cleanSql.includes(\"SELECT\") && cleanSql.includes(\"WHERE\")) {\n return cleanSql;\n}",
"elseFunction": "return $sqlQuery;" "elseFunction": "return $sqlQuery;"
}, },
"outputAnchors": [ "outputAnchors": [
@@ -1092,7 +1092,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 513, "height": 511,
"id": "promptTemplate_2", "id": "promptTemplate_2",
"position": { "position": {
"x": 1193.7489579044463, "x": 1193.7489579044463,
@@ -1152,7 +1152,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 670, "height": 669,
"id": "chatOpenAI_2", "id": "chatOpenAI_2",
"position": { "position": {
"x": 1545.1023725538003, "x": 1545.1023725538003,
@@ -1331,7 +1331,7 @@
}, },
{ {
"width": 300, "width": 300,
"height": 508, "height": 507,
"id": "llmChain_2", "id": "llmChain_2",
"position": { "position": {
"x": 1914.509823868027, "x": 1914.509823868027,
+9 -1
View File
@@ -16,13 +16,21 @@ export class ChatflowPool {
* @param {IReactFlowNode[]} startingNodes * @param {IReactFlowNode[]} startingNodes
* @param {ICommonObject} overrideConfig * @param {ICommonObject} overrideConfig
*/ */
add(chatflowid: string, endingNodeData: INodeData | undefined, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { add(
chatflowid: string,
endingNodeData: INodeData | undefined,
startingNodes: IReactFlowNode[],
overrideConfig?: ICommonObject,
chatId?: string
) {
this.activeChatflows[chatflowid] = { this.activeChatflows[chatflowid] = {
startingNodes, startingNodes,
endingNodeData, endingNodeData,
inSync: true inSync: true
} }
if (overrideConfig) this.activeChatflows[chatflowid].overrideConfig = overrideConfig if (overrideConfig) this.activeChatflows[chatflowid].overrideConfig = overrideConfig
if (chatId) this.activeChatflows[chatflowid].chatId = chatId
logger.info(`[server]: Chatflow ${chatflowid} added into ChatflowPool`) logger.info(`[server]: Chatflow ${chatflowid} added into ChatflowPool`)
} }
+1
View File
@@ -231,6 +231,7 @@ export interface IActiveChatflows {
endingNodeData?: INodeData endingNodeData?: INodeData
inSync: boolean inSync: boolean
overrideConfig?: ICommonObject overrideConfig?: ICommonObject
chatId?: string
} }
} }
+1 -1
View File
@@ -14,6 +14,6 @@ router.post(
vectorsController.getRateLimiterMiddleware, vectorsController.getRateLimiterMiddleware,
vectorsController.upsertVectorMiddleware vectorsController.upsertVectorMiddleware
) )
router.post(['/internal-upsert/', '/internal-upsert/:id'], vectorsController.createInternalUpsert) router.post(['/internal-upsert/', '/internal-upsert/:id'], upload.array('files'), vectorsController.createInternalUpsert)
export default router export default router
+13 -4
View File
@@ -1,5 +1,12 @@
import { Request } from 'express' import { Request } from 'express'
import { IFileUpload, convertSpeechToText, ICommonObject, addSingleFileToStorage, addArrayFilesToStorage } from 'flowise-components' import {
IFileUpload,
convertSpeechToText,
ICommonObject,
addSingleFileToStorage,
addArrayFilesToStorage,
mapMimeTypeToInputField
} from 'flowise-components'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import { import {
IncomingInput, IncomingInput,
@@ -18,7 +25,6 @@ import { ChatFlow } from '../database/entities/ChatFlow'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { getRunningExpressApp } from '../utils/getRunningExpressApp'
import { import {
mapMimeTypeToInputField,
isFlowValidForStream, isFlowValidForStream,
buildFlow, buildFlow,
getTelemetryFlowObj, getTelemetryFlowObj,
@@ -32,7 +38,8 @@ import {
getMemorySessionId, getMemorySessionId,
isSameOverrideConfig, isSameOverrideConfig,
getEndingNodes, getEndingNodes,
constructGraphs constructGraphs,
isSameChatId
} from '../utils' } from '../utils'
import { validateChatflowAPIKey } from './validateKey' import { validateChatflowAPIKey } from './validateKey'
import { databaseEntities } from '.' import { databaseEntities } from '.'
@@ -201,6 +208,7 @@ export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInter
* - Node Data already exists in pool * - Node Data already exists in pool
* - Still in sync (i.e the flow has not been modified since) * - Still in sync (i.e the flow has not been modified since)
* - Existing overrideConfig and new overrideConfig are the same * - Existing overrideConfig and new overrideConfig are the same
* - Existing chatId and new chatId is the same
* - Flow doesn't start with/contain nodes that depend on incomingInput.question * - Flow doesn't start with/contain nodes that depend on incomingInput.question
***/ ***/
const isFlowReusable = () => { const isFlowReusable = () => {
@@ -209,6 +217,7 @@ export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInter
Object.prototype.hasOwnProperty.call(appServer.chatflowPool.activeChatflows, chatflowid) && Object.prototype.hasOwnProperty.call(appServer.chatflowPool.activeChatflows, chatflowid) &&
appServer.chatflowPool.activeChatflows[chatflowid].inSync && appServer.chatflowPool.activeChatflows[chatflowid].inSync &&
appServer.chatflowPool.activeChatflows[chatflowid].endingNodeData && appServer.chatflowPool.activeChatflows[chatflowid].endingNodeData &&
isSameChatId(appServer.chatflowPool.activeChatflows[chatflowid].chatId, chatId) &&
isSameOverrideConfig( isSameOverrideConfig(
isInternal, isInternal,
appServer.chatflowPool.activeChatflows[chatflowid].overrideConfig, appServer.chatflowPool.activeChatflows[chatflowid].overrideConfig,
@@ -338,7 +347,7 @@ export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInter
) )
nodeToExecuteData = reactFlowNodeData nodeToExecuteData = reactFlowNodeData
appServer.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) appServer.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig, chatId)
} }
logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`) logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
+76 -39
View File
@@ -2,14 +2,22 @@ import { StatusCodes } from 'http-status-codes'
import { INodeParams } from 'flowise-components' import { INodeParams } from 'flowise-components'
import { ChatFlow } from '../database/entities/ChatFlow' import { ChatFlow } from '../database/entities/ChatFlow'
import { getRunningExpressApp } from '../utils/getRunningExpressApp' import { getRunningExpressApp } from '../utils/getRunningExpressApp'
import { IUploadFileSizeAndTypes, IReactFlowNode } from '../Interface' import { IUploadFileSizeAndTypes, IReactFlowNode, IReactFlowEdge } from '../Interface'
import { InternalFlowiseError } from '../errors/internalFlowiseError' import { InternalFlowiseError } from '../errors/internalFlowiseError'
type IUploadConfig = {
isSpeechToTextEnabled: boolean
isImageUploadAllowed: boolean
isFileUploadAllowed: boolean
imgUploadSizeAndTypes: IUploadFileSizeAndTypes[]
fileUploadSizeAndTypes: IUploadFileSizeAndTypes[]
}
/** /**
* Method that checks if uploads are enabled in the chatflow * Method that checks if uploads are enabled in the chatflow
* @param {string} chatflowid * @param {string} chatflowid
*/ */
export const utilGetUploadsConfig = async (chatflowid: string): Promise<any> => { export const utilGetUploadsConfig = async (chatflowid: string): Promise<IUploadConfig> => {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
id: chatflowid id: chatflowid
@@ -18,21 +26,17 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<any> =>
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Chatflow ${chatflowid} not found`) throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Chatflow ${chatflowid} not found`)
} }
const uploadAllowedNodes = [
'llmChain',
'conversationChain',
'reactAgentChat',
'conversationalAgent',
'toolAgent',
'supervisor',
'seqStart'
]
const uploadProcessingNodes = ['chatOpenAI', 'chatAnthropic', 'awsChatBedrock', 'azureChatOpenAI', 'chatGoogleGenerativeAI']
const flowObj = JSON.parse(chatflow.flowData) const flowObj = JSON.parse(chatflow.flowData)
const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = [] const nodes: IReactFlowNode[] = flowObj.nodes
const edges: IReactFlowEdge[] = flowObj.edges
let isSpeechToTextEnabled = false let isSpeechToTextEnabled = false
let isImageUploadAllowed = false
let isFileUploadAllowed = false
/*
* Check for STT
*/
if (chatflow.speechToText) { if (chatflow.speechToText) {
const speechToTextProviders = JSON.parse(chatflow.speechToText) const speechToTextProviders = JSON.parse(chatflow.speechToText)
for (const provider in speechToTextProviders) { for (const provider in speechToTextProviders) {
@@ -46,39 +50,72 @@ export const utilGetUploadsConfig = async (chatflowid: string): Promise<any> =>
} }
} }
let isImageUploadAllowed = false /*
const nodes: IReactFlowNode[] = flowObj.nodes * Condition for isFileUploadAllowed
* 1.) vector store with fileUpload = true && connected to a document loader with fileType
*/
const fileUploadSizeAndTypes: IUploadFileSizeAndTypes[] = []
for (const node of nodes) {
if (node.data.category === 'Vector Stores' && node.data.inputs?.fileUpload) {
// Get the connected document loader node fileTypes
const sourceDocumentEdges = edges.filter(
(edge) => edge.target === node.id && edge.targetHandle === `${node.id}-input-document-Document`
)
for (const edge of sourceDocumentEdges) {
const sourceNode = nodes.find((node) => node.id === edge.source)
if (!sourceNode) continue
const fileType = sourceNode.data.inputParams.find((param) => param.type === 'file' && param.fileType)?.fileType
if (fileType) {
fileUploadSizeAndTypes.push({
fileTypes: fileType.split(', '),
maxUploadSize: 500
})
isFileUploadAllowed = true
}
}
break
}
}
/* /*
* Condition for isImageUploadAllowed * Condition for isImageUploadAllowed
* 1.) one of the uploadAllowedNodes exists * 1.) one of the imgUploadAllowedNodes exists
* 2.) one of the uploadProcessingNodes exists + allowImageUploads is ON * 2.) one of the imgUploadLLMNodes exists + allowImageUploads is ON
*/ */
if (!nodes.some((node) => uploadAllowedNodes.includes(node.data.name))) { const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = []
return { const imgUploadAllowedNodes = [
isSpeechToTextEnabled, 'llmChain',
isImageUploadAllowed: false, 'conversationChain',
imgUploadSizeAndTypes 'reactAgentChat',
} 'conversationalAgent',
'toolAgent',
'supervisor',
'seqStart'
]
const imgUploadLLMNodes = ['chatOpenAI', 'chatAnthropic', 'awsChatBedrock', 'azureChatOpenAI', 'chatGoogleGenerativeAI']
if (nodes.some((node) => imgUploadAllowedNodes.includes(node.data.name))) {
nodes.forEach((node: IReactFlowNode) => {
if (imgUploadLLMNodes.indexOf(node.data.name) > -1) {
// TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
node.data.inputParams.map((param: INodeParams) => {
if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
imgUploadSizeAndTypes.push({
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
maxUploadSize: 5
})
isImageUploadAllowed = true
}
})
}
})
} }
nodes.forEach((node: IReactFlowNode) => {
if (uploadProcessingNodes.indexOf(node.data.name) > -1) {
// TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
node.data.inputParams.map((param: INodeParams) => {
if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
imgUploadSizeAndTypes.push({
fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
maxUploadSize: 5
})
isImageUploadAllowed = true
}
})
}
})
return { return {
isSpeechToTextEnabled, isSpeechToTextEnabled,
isImageUploadAllowed, isImageUploadAllowed,
imgUploadSizeAndTypes isFileUploadAllowed,
imgUploadSizeAndTypes,
fileUploadSizeAndTypes
} }
} }
+8 -27
View File
@@ -1074,35 +1074,16 @@ export const isSameOverrideConfig = (
} }
/** /**
* Map MimeType to InputField * @param {string} existingChatId
* @param {string} mimeType * @param {string} newChatId
* @returns {Promise<string>} * @returns {boolean}
*/ */
export const mapMimeTypeToInputField = (mimeType: string) => { export const isSameChatId = (existingChatId?: string, newChatId?: string): boolean => {
switch (mimeType) { if (isEqual(existingChatId, newChatId)) {
case 'text/plain': return true
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'
} }
if (!existingChatId && !newChatId) return true
return false
} }
/** /**
+14 -7
View File
@@ -1,14 +1,13 @@
import { Request } from 'express' import { Request } from 'express'
import * as fs from 'fs' import * as fs from 'fs'
import { cloneDeep, omit } from 'lodash' import { cloneDeep, omit } from 'lodash'
import { ICommonObject, IMessage, addArrayFilesToStorage } from 'flowise-components' import { ICommonObject, IMessage, addArrayFilesToStorage, mapMimeTypeToInputField } from 'flowise-components'
import telemetryService from '../services/telemetry' import telemetryService from '../services/telemetry'
import logger from '../utils/logger' import logger from '../utils/logger'
import { import {
buildFlow, buildFlow,
constructGraphs, constructGraphs,
getAllConnectedNodes, getAllConnectedNodes,
mapMimeTypeToInputField,
findMemoryNode, findMemoryNode,
getMemorySessionId, getMemorySessionId,
getAppVersion, getAppVersion,
@@ -70,6 +69,9 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) =>
overrideConfig, overrideConfig,
stopNodeId: req.body.stopNodeId stopNodeId: req.body.stopNodeId
} }
if (req.body.chatId) {
incomingInput.chatId = req.body.chatId
}
} }
/*** Get chatflows and prepare data ***/ /*** Get chatflows and prepare data ***/
@@ -87,10 +89,15 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) =>
const memoryNode = findMemoryNode(nodes, edges) const memoryNode = findMemoryNode(nodes, edges)
let sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal) let sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal)
const vsNodes = nodes.filter( const vsNodes = nodes.filter((node) => node.data.category === 'Vector Stores')
(node) =>
node.data.category === 'Vector Stores' && !node.data.label.includes('Upsert') && !node.data.label.includes('Load Existing') // Get StopNodeId for vector store which has fielUpload
) const vsNodesWithFileUpload = vsNodes.filter((node) => node.data.inputs?.fileUpload)
if (vsNodesWithFileUpload.length > 1) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, 'Multiple vector store nodes with fileUpload enabled')
} else if (vsNodesWithFileUpload.length === 1 && !stopNodeId) {
stopNodeId = vsNodesWithFileUpload[0].data.id
}
// Check if multiple vector store nodes exist, and if stopNodeId is specified // Check if multiple vector store nodes exist, and if stopNodeId is specified
if (vsNodes.length > 1 && !stopNodeId) { if (vsNodes.length > 1 && !stopNodeId) {
@@ -138,7 +145,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) =>
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id)) const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id))
await appServer.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig) await appServer.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig, chatId)
// Save to DB // Save to DB
if (upsertedResult['flowData'] && upsertedResult['result']) { if (upsertedResult['flowData'] && upsertedResult['result']) {
+5
View File
@@ -1,11 +1,16 @@
import client from './client' import client from './client'
const upsertVectorStore = (id, input) => client.post(`/vector/internal-upsert/${id}`, input) const upsertVectorStore = (id, input) => client.post(`/vector/internal-upsert/${id}`, input)
const upsertVectorStoreWithFormData = (id, formData) =>
client.post(`/vector/internal-upsert/${id}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
const getUpsertHistory = (id, params = {}) => client.get(`/upsert-history/${id}`, { params: { order: 'DESC', ...params } }) const getUpsertHistory = (id, params = {}) => client.get(`/upsert-history/${id}`, { params: { order: 'DESC', ...params } })
const deleteUpsertHistory = (ids) => client.patch(`/upsert-history`, { ids }) const deleteUpsertHistory = (ids) => client.patch(`/upsert-history`, { ids })
export default { export default {
getUpsertHistory, getUpsertHistory,
upsertVectorStore, upsertVectorStore,
upsertVectorStoreWithFormData,
deleteUpsertHistory deleteUpsertHistory
} }
@@ -34,7 +34,7 @@ import userPNG from '@/assets/images/account.png'
import msgEmptySVG from '@/assets/images/message_empty.svg' import msgEmptySVG from '@/assets/images/message_empty.svg'
import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png' import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'
import multiagent_workerPNG from '@/assets/images/multiagent_worker.png' import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'
import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons-react' import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload, IconPaperclip } from '@tabler/icons-react'
// Project import // Project import
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
@@ -438,6 +438,59 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setSourceDialogOpen(true) setSourceDialogOpen(true)
} }
const renderFileUploads = (item, index) => {
if (item?.mime?.startsWith('image/')) {
return (
<Card
key={index}
sx={{
p: 0,
m: 0,
maxWidth: 128,
marginRight: '10px',
flex: '0 0 auto'
}}
>
<CardMedia component='img' image={item.data} sx={{ height: 64 }} alt={'preview'} style={messageImageStyle} />
</Card>
)
} else if (item?.mime?.startsWith('audio/')) {
return (
/* eslint-disable jsx-a11y/media-has-caption */
<audio controls='controls'>
Your browser does not support the &lt;audio&gt; tag.
<source src={item.data} type={item.mime} />
</audio>
)
} else {
return (
<Card
sx={{
display: 'inline-flex',
alignItems: 'center',
height: '48px',
width: 'max-content',
p: 2,
mr: 1,
flex: '0 0 auto',
backgroundColor: customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'
}}
variant='outlined'
>
<IconPaperclip size={20} />
<span
style={{
marginLeft: '5px',
color: customization.isDarkMode ? 'white' : 'inherit'
}}
>
{item.name}
</span>
</Card>
)
}
}
useEffect(() => { useEffect(() => {
const leadEmailFromChatMessages = chatMessages.filter((message) => message.type === 'userMessage' && message.leadEmail) const leadEmailFromChatMessages = chatMessages.filter((message) => message.type === 'userMessage' && message.leadEmail)
if (leadEmailFromChatMessages.length) { if (leadEmailFromChatMessages.length) {
@@ -855,37 +908,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
}} }}
> >
{message.fileUploads.map((item, index) => { {message.fileUploads.map((item, index) => {
return ( return <>{renderFileUploads(item, index)}</>
<>
{item.mime.startsWith('image/') ? (
<Card
key={index}
sx={{
p: 0,
m: 0,
maxWidth: 128,
marginRight: '10px',
flex: '0 0 auto'
}}
>
<CardMedia
component='img'
image={item.data}
sx={{ height: 64 }}
alt={'preview'}
style={messageImageStyle}
/>
</Card>
) : (
// eslint-disable-next-line jsx-a11y/media-has-caption
<audio controls='controls'>
Your browser does not support the &lt;audio&gt;
tag.
<source src={item.data} type={item.mime} />
</audio>
)}
</>
)
})} })}
</div> </div>
)} )}
+329 -105
View File
@@ -37,7 +37,8 @@ import {
IconTool, IconTool,
IconSquareFilled, IconSquareFilled,
IconDeviceSdCard, IconDeviceSdCard,
IconCheck IconCheck,
IconPaperclip
} from '@tabler/icons-react' } from '@tabler/icons-react'
import robotPNG from '@/assets/images/robot.png' import robotPNG from '@/assets/images/robot.png'
import userPNG from '@/assets/images/account.png' import userPNG from '@/assets/images/account.png'
@@ -64,6 +65,7 @@ import './ChatMessage.css'
import chatmessageApi from '@/api/chatmessage' import chatmessageApi from '@/api/chatmessage'
import chatflowsApi from '@/api/chatflows' import chatflowsApi from '@/api/chatflows'
import predictionApi from '@/api/prediction' import predictionApi from '@/api/prediction'
import vectorstoreApi from '@/api/vectorstore'
import chatmessagefeedbackApi from '@/api/chatmessagefeedback' import chatmessagefeedbackApi from '@/api/chatmessagefeedback'
import leadsApi from '@/api/lead' import leadsApi from '@/api/lead'
@@ -84,6 +86,71 @@ const messageImageStyle = {
objectFit: 'cover' objectFit: 'cover'
} }
const CardWithDeleteOverlay = ({ item, customization, onDelete }) => {
const [isHovered, setIsHovered] = useState(false)
const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{ position: 'relative', display: 'inline-block' }}
>
<Card
sx={{
display: 'inline-flex',
alignItems: 'center',
height: '48px',
width: 'max-content',
p: 2,
mr: 1,
flex: '0 0 auto',
transition: 'opacity 0.3s',
opacity: isHovered ? 1 : 1,
backgroundColor: isHovered ? 'rgba(0, 0, 0, 0.3)' : defaultBackgroundColor
}}
variant='outlined'
>
<IconPaperclip size={20} style={{ transition: 'filter 0.3s', filter: isHovered ? 'blur(2px)' : 'none' }} />
<span
style={{
marginLeft: '5px',
color: customization.isDarkMode ? 'white' : 'inherit',
transition: 'filter 0.3s',
filter: isHovered ? 'blur(2px)' : 'none'
}}
>
{item.name}
</span>
</Card>
{isHovered && (
<Button
onClick={() => onDelete(item)}
startIcon={<IconTrash color='white' size={22} />}
title='Remove attachment'
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'transparent'
}
}}
></Button>
)}
</div>
)
}
CardWithDeleteOverlay.propTypes = {
item: PropTypes.object,
customization: PropTypes.object,
onDelete: PropTypes.func
}
export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => { export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => {
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
@@ -111,6 +178,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
const [sourceDialogProps, setSourceDialogProps] = useState({}) const [sourceDialogProps, setSourceDialogProps] = useState({})
const [chatId, setChatId] = useState(uuidv4()) const [chatId, setChatId] = useState(uuidv4())
const [isMessageStopping, setIsMessageStopping] = useState(false) const [isMessageStopping, setIsMessageStopping] = useState(false)
const [uploadedFiles, setUploadedFiles] = useState([])
const [imageUploadAllowedTypes, setImageUploadAllowedTypes] = useState('')
const [fileUploadAllowedTypes, setFileUploadAllowedTypes] = useState('')
const inputRef = useRef(null) const inputRef = useRef(null)
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
@@ -134,8 +204,10 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
const [isLeadSaved, setIsLeadSaved] = useState(false) const [isLeadSaved, setIsLeadSaved] = useState(false)
// drag & drop and file input // drag & drop and file input
const imgUploadRef = useRef(null)
const fileUploadRef = useRef(null) const fileUploadRef = useRef(null)
const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false) const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false)
const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false)
const [isDragActive, setIsDragActive] = useState(false) const [isDragActive, setIsDragActive] = useState(false)
// recording // recording
@@ -158,6 +230,18 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
}) })
} }
if (constraints.isFileUploadAllowed) {
const fileExt = file.name.split('.').pop()
if (fileExt) {
constraints.fileUploadSizeAndTypes.map((allowed) => {
if (allowed.fileTypes.length === 1 && allowed.fileTypes[0] === '*') {
acceptFile = true
} else if (allowed.fileTypes.includes(`.${fileExt}`)) {
acceptFile = true
}
})
}
}
if (!acceptFile) { if (!acceptFile) {
alert(`Cannot upload file. Kindly check the allowed file types and maximum allowed size.`) alert(`Cannot upload file. Kindly check the allowed file types and maximum allowed size.`)
} }
@@ -165,12 +249,14 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
const handleDrop = async (e) => { const handleDrop = async (e) => {
if (!isChatFlowAvailableForUploads) { if (!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads) {
return return
} }
e.preventDefault() e.preventDefault()
setIsDragActive(false) setIsDragActive(false)
let files = [] let files = []
let uploadedFiles = []
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 0) {
for (const file of e.dataTransfer.files) { for (const file of e.dataTransfer.files) {
if (isFileAllowedForUpload(file) === false) { if (isFileAllowedForUpload(file) === false) {
@@ -178,6 +264,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
const reader = new FileReader() const reader = new FileReader()
const { name } = file const { name } = file
uploadedFiles.push(file)
files.push( files.push(
new Promise((resolve) => { new Promise((resolve) => {
reader.onload = (evt) => { reader.onload = (evt) => {
@@ -188,7 +275,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
let previewUrl let previewUrl
if (file.type.startsWith('audio/')) { if (file.type.startsWith('audio/')) {
previewUrl = audioUploadSVG previewUrl = audioUploadSVG
} else if (file.type.startsWith('image/')) { } else {
previewUrl = URL.createObjectURL(file) previewUrl = URL.createObjectURL(file)
} }
resolve({ resolve({
@@ -205,10 +292,12 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
const newFiles = await Promise.all(files) const newFiles = await Promise.all(files)
setUploadedFiles(uploadedFiles)
setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) setPreviews((prevPreviews) => [...prevPreviews, ...newFiles])
} }
if (e.dataTransfer.items) { if (e.dataTransfer.items) {
//TODO set files
for (const item of e.dataTransfer.items) { for (const item of e.dataTransfer.items) {
if (item.kind === 'string' && item.type.match('^text/uri-list')) { if (item.kind === 'string' && item.type.match('^text/uri-list')) {
item.getAsString((s) => { item.getAsString((s) => {
@@ -246,10 +335,12 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
return return
} }
let files = [] let files = []
let uploadedFiles = []
for (const file of event.target.files) { for (const file of event.target.files) {
if (isFileAllowedForUpload(file) === false) { if (isFileAllowedForUpload(file) === false) {
return return
} }
uploadedFiles.push(file)
const reader = new FileReader() const reader = new FileReader()
const { name } = file const { name } = file
files.push( files.push(
@@ -273,6 +364,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
const newFiles = await Promise.all(files) const newFiles = await Promise.all(files)
setUploadedFiles(uploadedFiles)
setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) setPreviews((prevPreviews) => [...prevPreviews, ...newFiles])
// 👇 reset file input // 👇 reset file input
event.target.value = null event.target.value = null
@@ -303,7 +395,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
const handleDrag = (e) => { const handleDrag = (e) => {
if (isChatFlowAvailableForUploads) { if (isChatFlowAvailableForImageUploads || isChatFlowAvailableForFileUploads) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
if (e.type === 'dragenter' || e.type === 'dragover') { if (e.type === 'dragenter' || e.type === 'dragover') {
@@ -343,11 +435,16 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
setPreviews(previews.filter((item) => item !== itemToDelete)) setPreviews(previews.filter((item) => item !== itemToDelete))
} }
const handleUploadClick = () => { const handleFileUploadClick = () => {
// 👇 open file input box on click of another element // 👇 open file input box on click of another element
fileUploadRef.current.click() fileUploadRef.current.click()
} }
const handleImageUploadClick = () => {
// 👇 open file input box on click of another element
imgUploadRef.current.click()
}
const clearPreviews = () => { const clearPreviews = () => {
// Revoke the data uris to avoid memory leaks // Revoke the data uris to avoid memory leaks
previews.forEach((file) => URL.revokeObjectURL(file.preview)) previews.forEach((file) => URL.revokeObjectURL(file.preview))
@@ -489,6 +586,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]) setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }])
setLoading(false) setLoading(false)
setUserInput('') setUserInput('')
setUploadedFiles([])
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus() inputRef.current?.focus()
}, 100) }, 100)
@@ -526,7 +624,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput
setLoading(true) setLoading(true)
const urls = previews.map((item) => { const uploads = previews.map((item) => {
return { return {
data: item.data, data: item.data,
type: item.type, type: item.type,
@@ -535,7 +633,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
}) })
clearPreviews() clearPreviews()
setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: urls }]) setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }])
// Send user question to Prediction Internal API // Send user question to Prediction Internal API
try { try {
@@ -543,11 +641,30 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
question: input, question: input,
chatId chatId
} }
if (urls && urls.length > 0) params.uploads = urls if (uploads && uploads.length > 0) params.uploads = uploads
if (leadEmail) params.leadEmail = leadEmail if (leadEmail) params.leadEmail = leadEmail
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
if (action) params.action = action if (action) params.action = action
if (uploadedFiles.length > 0) {
const formData = new FormData()
for (const file of uploadedFiles) {
formData.append('files', file)
}
formData.append('chatId', chatId)
const response = await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData)
if (!response.data) {
setMessages((prevMessages) => [...prevMessages, { message: 'Unable to upload documents', type: 'apiMessage' }])
} else {
// delay for vector store to be updated
const delay = (delayInms) => {
return new Promise((resolve) => setTimeout(resolve, delayInms))
}
await delay(2500) //TODO: check if embeddings can be retrieved using file name as metadata filter
}
}
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
if (response.data) { if (response.data) {
@@ -598,6 +715,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
setLocalStorageChatflow(chatflowid, data.chatId) setLocalStorageChatflow(chatflowid, data.chatId)
setLoading(false) setLoading(false)
setUserInput('') setUserInput('')
setUploadedFiles([])
setTimeout(() => { setTimeout(() => {
inputRef.current?.focus() inputRef.current?.focus()
scrollToBottom() scrollToBottom()
@@ -717,8 +835,11 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
// Get chatflow uploads capability // Get chatflow uploads capability
useEffect(() => { useEffect(() => {
if (getAllowChatFlowUploads.data) { if (getAllowChatFlowUploads.data) {
setIsChatFlowAvailableForUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false) setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false)
setIsChatFlowAvailableForFileUploads(getAllowChatFlowUploads.data?.isFileUploadAllowed ?? false)
setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false) setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false)
setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(','))
setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(','))
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllowChatFlowUploads.data]) }, [getAllowChatFlowUploads.data])
@@ -822,6 +943,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
return () => { return () => {
setUserInput('') setUserInput('')
setUploadedFiles([])
setLoading(false) setLoading(false)
setMessages([ setMessages([
{ {
@@ -965,6 +1087,105 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
) )
} }
const previewDisplay = (item) => {
if (item.mime.startsWith('image/')) {
return (
<ImageButton
focusRipple
style={{
width: '48px',
height: '48px',
marginRight: '10px',
flex: '0 0 auto'
}}
onClick={() => handleDeletePreview(item)}
>
<ImageSrc style={{ backgroundImage: `url(${item.data})` }} />
<ImageBackdrop className='MuiImageBackdrop-root' />
<ImageMarked className='MuiImageMarked-root'>
<IconTrash size={20} color='white' />
</ImageMarked>
</ImageButton>
)
} else if (item.mime.startsWith('audio/')) {
return (
<Card
sx={{
display: 'inline-flex',
alignItems: 'center',
height: '48px',
width: isDialog ? ps?.current?.offsetWidth / 4 : ps?.current?.offsetWidth / 2,
p: 0.5,
mr: 1,
backgroundColor: theme.palette.grey[500],
flex: '0 0 auto'
}}
variant='outlined'
>
<CardMedia component='audio' sx={{ color: 'transparent' }} controls src={item.data} />
<IconButton onClick={() => handleDeletePreview(item)} size='small'>
<IconTrash size={20} color='white' />
</IconButton>
</Card>
)
} else {
return <CardWithDeleteOverlay item={item} customization={customization} onDelete={() => handleDeletePreview(item)} />
}
}
const renderFileUploads = (item, index) => {
if (item?.mime?.startsWith('image/')) {
return (
<Card
key={index}
sx={{
p: 0,
m: 0,
maxWidth: 128,
marginRight: '10px',
flex: '0 0 auto'
}}
>
<CardMedia component='img' image={item.data} sx={{ height: 64 }} alt={'preview'} style={messageImageStyle} />
</Card>
)
} else if (item?.mime?.startsWith('audio/')) {
return (
/* eslint-disable jsx-a11y/media-has-caption */
<audio controls='controls'>
Your browser does not support the &lt;audio&gt; tag.
<source src={item.data} type={item.mime} />
</audio>
)
} else {
return (
<Card
sx={{
display: 'inline-flex',
alignItems: 'center',
height: '48px',
width: 'max-content',
p: 2,
mr: 1,
flex: '0 0 auto',
backgroundColor: customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'
}}
variant='outlined'
>
<IconPaperclip size={20} />
<span
style={{
marginLeft: '5px',
color: customization.isDarkMode ? 'white' : 'inherit'
}}
>
{item.name}
</span>
</Card>
)
}
}
return ( return (
<div onDragEnter={handleDrag}> <div onDragEnter={handleDrag}>
{isDragActive && ( {isDragActive && (
@@ -976,19 +1197,25 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
onDrop={handleDrop} onDrop={handleDrop}
/> />
)} )}
{isDragActive && getAllowChatFlowUploads.data?.isImageUploadAllowed && ( {isDragActive &&
<Box className='drop-overlay'> (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isFileAllowedForUpload) && (
<Typography variant='h2'>Drop here to upload</Typography> <Box className='drop-overlay'>
{getAllowChatFlowUploads.data.imgUploadSizeAndTypes.map((allowed) => { <Typography variant='h2'>Drop here to upload</Typography>
return ( {[
<> ...getAllowChatFlowUploads.data.imgUploadSizeAndTypes,
<Typography variant='subtitle1'>{allowed.fileTypes?.join(', ')}</Typography> ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes
<Typography variant='subtitle1'>Max Allowed Size: {allowed.maxUploadSize} MB</Typography> ].map((allowed) => {
</> return (
) <>
})} <Typography variant='subtitle1'>{allowed.fileTypes?.join(', ')}</Typography>
</Box> {allowed.maxUploadSize && (
)} <Typography variant='subtitle1'>Max Allowed Size: {allowed.maxUploadSize} MB</Typography>
)}
</>
)
})}
</Box>
)}
<div ref={ps} className={`${isDialog ? 'cloud-dialog' : 'cloud'}`}> <div ref={ps} className={`${isDialog ? 'cloud-dialog' : 'cloud'}`}>
<div id='messagelist' className={'messagelist'}> <div id='messagelist' className={'messagelist'}>
{messages && {messages &&
@@ -1038,36 +1265,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
}} }}
> >
{message.fileUploads.map((item, index) => { {message.fileUploads.map((item, index) => {
return ( return <>{renderFileUploads(item, index)}</>
<>
{item?.mime?.startsWith('image/') ? (
<Card
key={index}
sx={{
p: 0,
m: 0,
maxWidth: 128,
marginRight: '10px',
flex: '0 0 auto'
}}
>
<CardMedia
component='img'
image={item.data}
sx={{ height: 64 }}
alt={'preview'}
style={messageImageStyle}
/>
</Card>
) : (
// eslint-disable-next-line jsx-a11y/media-has-caption
<audio controls='controls'>
Your browser does not support the &lt;audio&gt; tag.
<source src={item.data} type={item.mime} />
</audio>
)}
</>
)
})} })}
</div> </div>
)} )}
@@ -1564,45 +1762,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
{previews && previews.length > 0 && ( {previews && previews.length > 0 && (
<Box sx={{ width: '100%', mb: 1.5, display: 'flex', alignItems: 'center' }}> <Box sx={{ width: '100%', mb: 1.5, display: 'flex', alignItems: 'center' }}>
{previews.map((item, index) => ( {previews.map((item, index) => (
<Fragment key={index}> <Fragment key={index}>{previewDisplay(item)}</Fragment>
{item.mime.startsWith('image/') ? (
<ImageButton
focusRipple
style={{
width: '48px',
height: '48px',
marginRight: '10px',
flex: '0 0 auto'
}}
onClick={() => handleDeletePreview(item)}
>
<ImageSrc style={{ backgroundImage: `url(${item.data})` }} />
<ImageBackdrop className='MuiImageBackdrop-root' />
<ImageMarked className='MuiImageMarked-root'>
<IconTrash size={20} color='white' />
</ImageMarked>
</ImageButton>
) : (
<Card
sx={{
display: 'inline-flex',
alignItems: 'center',
height: '48px',
width: isDialog ? ps?.current?.offsetWidth / 4 : ps?.current?.offsetWidth / 2,
p: 0.5,
mr: 1,
backgroundColor: theme.palette.grey[500],
flex: '0 0 auto'
}}
variant='outlined'
>
<CardMedia component='audio' sx={{ color: 'transparent' }} controls src={item.data} />
<IconButton onClick={() => handleDeletePreview(item)} size='small'>
<IconTrash size={20} color='white' />
</IconButton>
</Card>
)}
</Fragment>
))} ))}
</Box> </Box>
)} )}
@@ -1679,15 +1839,62 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
multiline={true} multiline={true}
maxRows={isDialog ? 7 : 2} maxRows={isDialog ? 7 : 2}
startAdornment={ startAdornment={
isChatFlowAvailableForUploads && ( <>
<InputAdornment position='start' sx={{ pl: 2 }}> {isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && (
<IconButton onClick={handleUploadClick} type='button' disabled={getInputDisabled()} edge='start'> <InputAdornment position='start' sx={{ ml: 2 }}>
<IconPhotoPlus <IconButton
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'} onClick={handleImageUploadClick}
/> type='button'
</IconButton> disabled={getInputDisabled()}
</InputAdornment> edge='start'
) >
<IconPhotoPlus
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
</InputAdornment>
)}
{!isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && (
<InputAdornment position='start' sx={{ ml: 2 }}>
<IconButton
onClick={handleFileUploadClick}
type='button'
disabled={getInputDisabled()}
edge='start'
>
<IconPaperclip
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
</InputAdornment>
)}
{isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && (
<InputAdornment position='start' sx={{ ml: 2 }}>
<IconButton
onClick={handleImageUploadClick}
type='button'
disabled={getInputDisabled()}
edge='start'
>
<IconPhotoPlus
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
<IconButton
sx={{ ml: 0 }}
onClick={handleFileUploadClick}
type='button'
disabled={getInputDisabled()}
edge='start'
>
<IconPaperclip
color={getInputDisabled() ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
</InputAdornment>
)}
{!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && <Box sx={{ pl: 1 }} />}
</>
} }
endAdornment={ endAdornment={
<> <>
@@ -1707,7 +1914,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
</InputAdornment> </InputAdornment>
)} )}
{!isAgentCanvas && ( {!isAgentCanvas && (
<InputAdornment position='end' sx={{ padding: '15px' }}> <InputAdornment position='end' sx={{ paddingRight: '15px' }}>
<IconButton type='submit' disabled={getInputDisabled()} edge='end'> <IconButton type='submit' disabled={getInputDisabled()} edge='end'>
{loading ? ( {loading ? (
<div> <div>
@@ -1727,7 +1934,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
{isAgentCanvas && ( {isAgentCanvas && (
<> <>
{!loading && ( {!loading && (
<InputAdornment position='end' sx={{ padding: '15px' }}> <InputAdornment position='end' sx={{ paddingRight: '15px' }}>
<IconButton type='submit' disabled={getInputDisabled()} edge='end'> <IconButton type='submit' disabled={getInputDisabled()} edge='end'>
<IconSend <IconSend
color={ color={
@@ -1765,8 +1972,25 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
</> </>
} }
/> />
{isChatFlowAvailableForUploads && ( {isChatFlowAvailableForImageUploads && (
<input style={{ display: 'none' }} multiple ref={fileUploadRef} type='file' onChange={handleFileChange} /> <input
style={{ display: 'none' }}
multiple
ref={imgUploadRef}
type='file'
onChange={handleFileChange}
accept={imageUploadAllowedTypes || '*'}
/>
)}
{isChatFlowAvailableForFileUploads && (
<input
style={{ display: 'none' }}
multiple
ref={fileUploadRef}
type='file'
onChange={handleFileChange}
accept={fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'}
/>
)} )}
</form> </form>
)} )}