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
@@ -4,7 +4,8 @@ import { Document } from '@langchain/core/documents'
import { MilvusLibArgs, Milvus } from '@langchain/community/vectorstores/milvus'
import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { howToUseFileUpload } from '../VectorStoreUtils'
interface InsertRow {
[x: string]: string | number[]
@@ -27,7 +28,7 @@ class Milvus_VectorStores implements INode {
constructor() {
this.label = 'Milvus'
this.name = 'milvus'
this.version = 1.0
this.version = 2.0
this.type = 'Milvus'
this.icon = 'milvus.svg'
this.category = 'Vector Stores'
@@ -64,6 +65,18 @@ class Milvus_VectorStores implements INode {
name: 'milvusCollection',
type: 'string'
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Milvus Text Field',
name: 'milvusTextField',
@@ -116,6 +129,7 @@ class Milvus_VectorStores implements INode {
// embeddings
const docs = nodeData.inputs?.document as Document[]
const embeddings = nodeData.inputs?.embeddings as Embeddings
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
// credential
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
@@ -135,6 +149,9 @@ class Milvus_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -158,8 +175,9 @@ class Milvus_VectorStores implements INode {
// server setup
const address = nodeData.inputs?.milvusServerUrl as string
const collectionName = nodeData.inputs?.milvusCollection as string
const milvusFilter = nodeData.inputs?.milvusFilter as string
const _milvusFilter = nodeData.inputs?.milvusFilter as string
const textField = nodeData.inputs?.milvusTextField as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
// embeddings
const embeddings = nodeData.inputs?.embeddings as Embeddings
@@ -186,6 +204,12 @@ class Milvus_VectorStores implements INode {
if (milvusUser) milVusArgs.username = milvusUser
if (milvusPassword) milVusArgs.password = milvusPassword
let milvusFilter = _milvusFilter
if (isFileUploadEnabled && options.chatId) {
if (milvusFilter) milvusFilter += ` OR ${FLOWISE_CHATID} == "${options.chatId}" OR NOT EXISTS(${FLOWISE_CHATID})`
else milvusFilter = `${FLOWISE_CHATID} == "${options.chatId}" OR NOT EXISTS(${FLOWISE_CHATID})`
}
const vectorStore = await Milvus.fromExistingCollection(embeddings, milVusArgs)
// Avoid Illegal Invocation
@@ -5,8 +5,8 @@ import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { VectorStore } from '@langchain/core/vectorstores'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { addMMRInputParams, howToUseFileUpload, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { index } from '../../../src/indexing'
let pineconeClientSingleton: Pinecone
@@ -43,7 +43,7 @@ class Pinecone_VectorStores implements INode {
constructor() {
this.label = 'Pinecone'
this.name = 'pinecone'
this.version = 4.0
this.version = 5.0
this.type = 'Pinecone'
this.icon = 'pinecone.svg'
this.category = 'Vector Stores'
@@ -88,6 +88,18 @@ class Pinecone_VectorStores implements INode {
additionalParams: true,
optional: true
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Pinecone Text Key',
name: 'pineconeTextKey',
@@ -138,6 +150,7 @@ class Pinecone_VectorStores implements INode {
const embeddings = nodeData.inputs?.embeddings as Embeddings
const recordManager = nodeData.inputs?.recordManager
const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
@@ -150,6 +163,9 @@ class Pinecone_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -232,6 +248,7 @@ class Pinecone_VectorStores implements INode {
const pineconeMetadataFilter = nodeData.inputs?.pineconeMetadataFilter
const embeddings = nodeData.inputs?.embeddings as Embeddings
const pineconeTextKey = nodeData.inputs?.pineconeTextKey as string
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const pineconeApiKey = getCredentialParam('pineconeApiKey', credentialData, nodeData)
@@ -250,6 +267,14 @@ class Pinecone_VectorStores implements INode {
const metadatafilter = typeof pineconeMetadataFilter === 'object' ? pineconeMetadataFilter : JSON.parse(pineconeMetadataFilter)
obj.filter = metadatafilter
}
if (isFileUploadEnabled && options.chatId) {
obj.filter = obj.filter || {}
obj.filter.$or = [
...(obj.filter.$or || []),
{ [FLOWISE_CHATID]: { $eq: options.chatId } },
{ [FLOWISE_CHATID]: { $exists: false } }
]
}
const vectorStore = (await PineconeStore.fromExistingIndex(embeddings, obj)) as unknown as VectorStore
@@ -5,8 +5,9 @@ import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { TypeORMVectorStore, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
class Postgres_VectorStores implements INode {
label: string
@@ -25,7 +26,7 @@ class Postgres_VectorStores implements INode {
constructor() {
this.label = 'Postgres'
this.name = 'postgres'
this.version = 5.0
this.version = 6.0
this.type = 'Postgres'
this.icon = 'postgres.svg'
this.category = 'Vector Stores'
@@ -82,6 +83,18 @@ class Postgres_VectorStores implements INode {
additionalParams: true,
optional: true
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Additional Configuration',
name: 'additionalConfig',
@@ -132,6 +145,7 @@ class Postgres_VectorStores implements INode {
const embeddings = nodeData.inputs?.embeddings as Embeddings
const additionalConfig = nodeData.inputs?.additionalConfig as string
const recordManager = nodeData.inputs?.recordManager
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
let additionalConfiguration = {}
if (additionalConfig) {
@@ -161,6 +175,9 @@ class Postgres_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -268,11 +285,20 @@ class Postgres_VectorStores implements INode {
const topK = nodeData.inputs?.topK as string
const k = topK ? parseFloat(topK) : 4
const _pgMetadataFilter = nodeData.inputs?.pgMetadataFilter
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
let pgMetadataFilter: any
if (_pgMetadataFilter) {
pgMetadataFilter = typeof _pgMetadataFilter === 'object' ? _pgMetadataFilter : JSON.parse(_pgMetadataFilter)
}
if (isFileUploadEnabled && options.chatId) {
pgMetadataFilter = pgMetadataFilter || {}
pgMetadataFilter = {
...pgMetadataFilter,
[FLOWISE_CHATID]: options.chatId,
$notexists: FLOWISE_CHATID // special filter to check if the field does not exist
}
}
let additionalConfiguration = {}
if (additionalConfig) {
@@ -334,12 +360,20 @@ const similaritySearchVectorWithScore = async (
) => {
const embeddingString = `[${query.join(',')}]`
let _filter = '{}'
if (filter && typeof filter === 'object') _filter = JSON.stringify(filter)
let notExists = ''
if (filter && typeof filter === 'object') {
if (filter.$notexists) {
notExists = `OR NOT (metadata ? '${filter.$notexists}')`
delete filter.$notexists
}
_filter = JSON.stringify(filter)
}
const queryString = `
SELECT *, embedding <=> $1 as "_distance"
FROM ${tableName}
WHERE metadata @> $2
${notExists}
ORDER BY "_distance" ASC
LIMIT $3;`
@@ -6,8 +6,9 @@ import { Document } from '@langchain/core/documents'
import { QdrantVectorStore, QdrantLibArgs } from '@langchain/qdrant'
import { Embeddings } from '@langchain/core/embeddings'
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { index } from '../../../src/indexing'
import { howToUseFileUpload } from '../VectorStoreUtils'
type RetrieverConfig = Partial<VectorStoreRetrieverInput<QdrantVectorStore>>
type QdrantAddDocumentOptions = {
@@ -32,7 +33,7 @@ class Qdrant_VectorStores implements INode {
constructor() {
this.label = 'Qdrant'
this.name = 'qdrant'
this.version = 4.0
this.version = 5.0
this.type = 'Qdrant'
this.icon = 'qdrant.png'
this.category = 'Vector Stores'
@@ -78,6 +79,18 @@ class Qdrant_VectorStores implements INode {
name: 'qdrantCollection',
type: 'string'
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Vector Dimension',
name: 'qdrantVectorDimension',
@@ -188,6 +201,7 @@ class Qdrant_VectorStores implements INode {
const _batchSize = nodeData.inputs?.batchSize
const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content'
const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata'
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const qdrantApiKey = getCredentialParam('qdrantApiKey', credentialData, nodeData)
@@ -204,6 +218,9 @@ class Qdrant_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -391,6 +408,7 @@ class Qdrant_VectorStores implements INode {
let queryFilter = nodeData.inputs?.qdrantFilter
const contentPayloadKey = nodeData.inputs?.contentPayloadKey || 'content'
const metadataPayloadKey = nodeData.inputs?.metadataPayloadKey || 'metadata'
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const k = topK ? parseFloat(topK) : 4
@@ -434,6 +452,25 @@ class Qdrant_VectorStores implements INode {
if (queryFilter) {
retrieverConfig.filter = typeof queryFilter === 'object' ? queryFilter : JSON.parse(queryFilter)
}
if (isFileUploadEnabled && options.chatId) {
retrieverConfig.filter = retrieverConfig.filter || {}
retrieverConfig.filter.should = Array.isArray(retrieverConfig.filter.should) ? retrieverConfig.filter.should : []
retrieverConfig.filter.should.push(
{
key: `metadata.${FLOWISE_CHATID}`,
match: {
value: options.chatId
}
},
{
is_empty: {
key: `metadata.${FLOWISE_CHATID}`
}
}
)
}
const vectorStore = await QdrantVectorStore.fromExistingCollection(embeddings, dbConfig)
@@ -1,12 +1,12 @@
import { flatten } from 'lodash'
import { IndexingResult, INode, INodeOutputsValue, INodeParams, INodeData, ICommonObject } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { FLOWISE_CHATID, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { Embeddings } from '@langchain/core/embeddings'
import { Document } from '@langchain/core/documents'
import { UpstashVectorStore } from '@langchain/community/vectorstores/upstash'
import { Index as UpstashIndex } from '@upstash/vector'
import { index } from '../../../src/indexing'
import { resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
import { howToUseFileUpload, resolveVectorStoreOrRetriever } from '../VectorStoreUtils'
type UpstashVectorStoreParams = {
index: UpstashIndex
@@ -29,7 +29,7 @@ class Upstash_VectorStores implements INode {
constructor() {
this.label = 'Upstash Vector'
this.name = 'upstash'
this.version = 1.0
this.version = 2.0
this.type = 'Upstash'
this.icon = 'upstash.svg'
this.category = 'Vector Stores'
@@ -63,6 +63,18 @@ class Upstash_VectorStores implements INode {
description: 'Keep track of the record to prevent duplication',
optional: true
},
{
label: 'File Upload',
name: 'fileUpload',
description: 'Allow file upload on the chat',
hint: {
label: 'How to use',
value: howToUseFileUpload
},
type: 'boolean',
additionalParams: true,
optional: true
},
{
label: 'Upstash Metadata Filter',
name: 'upstashMetadataFilter',
@@ -100,6 +112,7 @@ class Upstash_VectorStores implements INode {
const docs = nodeData.inputs?.document as Document[]
const embeddings = nodeData.inputs?.embeddings as Embeddings
const recordManager = nodeData.inputs?.recordManager
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData)
@@ -114,6 +127,9 @@ class Upstash_VectorStores implements INode {
const finalDocs = []
for (let i = 0; i < flattenDocs.length; i += 1) {
if (flattenDocs[i] && flattenDocs[i].pageContent) {
if (isFileUploadEnabled && options.chatId) {
flattenDocs[i].metadata = { ...flattenDocs[i].metadata, [FLOWISE_CHATID]: options.chatId }
}
finalDocs.push(new Document(flattenDocs[i]))
}
}
@@ -186,6 +202,7 @@ class Upstash_VectorStores implements INode {
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const upstashMetadataFilter = nodeData.inputs?.upstashMetadataFilter
const embeddings = nodeData.inputs?.embeddings as Embeddings
const isFileUploadEnabled = nodeData.inputs?.fileUpload as boolean
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const UPSTASH_VECTOR_REST_URL = getCredentialParam('UPSTASH_VECTOR_REST_URL', credentialData, nodeData)
@@ -203,6 +220,10 @@ class Upstash_VectorStores implements INode {
if (upstashMetadataFilter) {
obj.filter = upstashMetadataFilter
}
if (isFileUploadEnabled && options.chatId) {
if (upstashMetadataFilter) obj.filter += ` OR ${FLOWISE_CHATID} = "${options.chatId}" OR HAS NOT FIELD ${FLOWISE_CHATID}`
else obj.filter = `${FLOWISE_CHATID} = "${options.chatId}" OR HAS NOT FIELD ${FLOWISE_CHATID}`
}
const vectorStore = await UpstashVectorStore.fromExistingIndex(embeddings, obj)
@@ -194,6 +194,7 @@ class Vectara_VectorStores implements INode {
const chatflowid = options.chatflowid
for (const file of files) {
if (!file) continue
const fileData = await getFileFromStorage(file, chatflowid)
const blob = new Blob([fileData])
vectaraFiles.push({ blob: blob, fileName: getFileName(file) })
@@ -206,6 +207,7 @@ class Vectara_VectorStores implements INode {
}
for (const file of files) {
if (!file) continue
const splitDataURI = file.split(',')
splitDataURI.pop()
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
@@ -83,3 +83,19 @@ export const addMMRInputParams = (inputs: any[]) => {
inputs.push(...mmrInputParams)
}
export const howToUseFileUpload = `
**File Upload**
This allows file upload on the chat. Uploaded files will be upserted on the fly to the vector store.
**Note:**
- You can only turn on file upload for one vector store at a time.
- At least one Document Loader node should be connected to the document input.
- Document Loader should be file types like PDF, DOCX, TXT, etc.
**How it works**
- Uploaded files will have the metadata updated with the chatId.
- This will allow the file to be associated with the chatId.
- When querying, metadata will be filtered by chatId to retrieve files associated with the chatId.
`