Feature/Indexing (#1802)

* indexing

* fix for multiple files upsert

* fix default Postgres port

* fix SQLite node description

* add MySQLRecordManager node

* fix MySQL unique index

* add upsert history

* update jsx ui

* lint-fix

* update dialog details

* update llamaindex pinecone

---------

Co-authored-by: chungyau97 <chungyau97@gmail.com>
This commit is contained in:
Henry Heng
2024-04-02 23:47:19 +01:00
committed by GitHub
parent 957694a912
commit e422ce287b
67 changed files with 3006 additions and 246 deletions
+8
View File
@@ -93,6 +93,14 @@ export interface IVariable {
createdDate: Date
}
export interface IUpsertHistory {
id: string
chatflowid: string
result: string
flowData: string
date: Date
}
export interface IComponentNodes {
[key: string]: INode
}
@@ -19,7 +19,7 @@ const creatTool = async (req: Request, res: Response, next: NextFunction) => {
const deleteTool = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.params.id === 'undefined' || req.params.id === '') {
throw new Error(`Error: toolsController.updateTool - id not provided!`)
throw new Error(`Error: toolsController.deleteTool - id not provided!`)
}
const apiResponse = await toolsService.deleteTool(req.params.id)
if (apiResponse.executionError) {
@@ -0,0 +1,30 @@
import { Request, Response, NextFunction } from 'express'
import upsertHistoryService from '../../services/upsert-history'
const getAllUpsertHistory = async (req: Request, res: Response, next: NextFunction) => {
try {
const sortOrder = req.query?.order as string | undefined
const chatflowid = req.params?.id as string | undefined
const startDate = req.query?.startDate as string | undefined
const endDate = req.query?.endDate as string | undefined
const apiResponse = await upsertHistoryService.getAllUpsertHistory(sortOrder, chatflowid, startDate, endDate)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const patchDeleteUpsertHistory = async (req: Request, res: Response, next: NextFunction) => {
try {
const ids = req.body.ids ?? []
const apiResponse = await upsertHistoryService.patchDeleteUpsertHistory(ids)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
export default {
getAllUpsertHistory,
patchDeleteUpsertHistory
}
@@ -10,7 +10,7 @@ export class Assistant implements IAssistant {
@Column({ type: 'text' })
details: string
@Column({ type: 'uuid'})
@Column({ type: 'uuid' })
credential: string
@Column({ nullable: true })
@@ -34,11 +34,11 @@ export class ChatFlow implements IChatFlow {
@Column({ nullable: true, type: 'text' })
speechToText?: string
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@UpdateDateColumn()
updatedDate: Date
@@ -41,7 +41,7 @@ export class ChatMessage implements IChatMessage {
@Column({ type: 'varchar', nullable: true })
sessionId?: string
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
}
@@ -25,7 +25,7 @@ export class ChatMessageFeedback implements IChatMessageFeedback {
@Column({ nullable: true, type: 'text' })
content?: string
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
}
@@ -16,11 +16,11 @@ export class Credential implements ICredential {
@Column({ type: 'text' })
encryptedData: string
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@UpdateDateColumn()
updatedDate: Date
}
@@ -25,11 +25,11 @@ export class Tool implements ITool {
@Column({ nullable: true, type: 'text' })
func?: string
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@UpdateDateColumn()
updatedDate: Date
}
@@ -0,0 +1,22 @@
/* eslint-disable */
import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn } from 'typeorm'
import { IUpsertHistory } from '../../Interface'
@Entity()
export class UpsertHistory implements IUpsertHistory {
@PrimaryGeneratedColumn('uuid')
id: string
@Index()
@Column()
chatflowid: string
@Column()
result: string
@Column()
flowData: string
@CreateDateColumn()
date: Date
}
@@ -16,11 +16,11 @@ export class Variable implements IVariable {
@Column({ default: 'string', type: 'text' })
type: string
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
@Column({type:'timestamp'})
@Column({ type: 'timestamp' })
@UpdateDateColumn()
updatedDate: Date
}
@@ -5,6 +5,7 @@ import { Credential } from './Credential'
import { Tool } from './Tool'
import { Assistant } from './Assistant'
import { Variable } from './Variable'
import { UpsertHistory } from './UpsertHistory'
export const entities = {
ChatFlow,
@@ -13,5 +14,6 @@ export const entities = {
Credential,
Tool,
Assistant,
Variable
Variable,
UpsertHistory
}
@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddUpsertHistoryEntity1709814301358 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS \`upsert_history\` (
\`id\` varchar(36) NOT NULL,
\`chatflowid\` varchar(255) NOT NULL,
\`result\` text NOT NULL,
\`flowData\` text NOT NULL,
\`date\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`)
KEY \`IDX_a0b59fd66f6e48d2b198123cb6\` (\`chatflowid\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE upsert_history`)
}
}
@@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213626553 } from './1707213626553-AddFeedback'
export const mysqlMigrations = [
@@ -31,5 +32,6 @@ export const mysqlMigrations = [
AddFileUploadsToChatMessage1701788586491,
AddVariableEntity1699325775451,
AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358,
AddFeedback1707213626553
]
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddUpsertHistoryEntity1709814301358 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS upsert_history (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
"chatflowid" varchar NOT NULL,
"result" text NOT NULL,
"flowData" text NOT NULL,
"date" timestamp NOT NULL DEFAULT now(),
CONSTRAINT "PK_37327b22b6e246319bd5eeb0e88" PRIMARY KEY (id)
);`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE upsert_history`)
}
}
@@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213601923 } from './1707213601923-AddFeedback'
import { FieldTypes1710497452584 } from './1710497452584-FieldTypes'
@@ -32,6 +33,7 @@ export const postgresMigrations = [
AddFileUploadsToChatMessage1701788586491,
AddVariableEntity1699325775451,
AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358,
AddFeedback1707213601923,
FieldTypes1710497452584
]
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddUpsertHistoryEntity1709814301358 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS "upsert_history" ("id" varchar PRIMARY KEY NOT NULL, "chatflowid" varchar NOT NULL, "result" text NOT NULL, "flowData" text NOT NULL, "date" datetime NOT NULL DEFAULT (datetime('now')));`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE upsert_history`)
}
}
@@ -13,6 +13,7 @@ import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-Ad
import { AddFileUploadsToChatMessage1701788586491 } from './1701788586491-AddFileUploadsToChatMessage'
import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity'
import { AddSpeechToText1706364937060 } from './1706364937060-AddSpeechToText'
import { AddUpsertHistoryEntity1709814301358 } from './1709814301358-AddUpsertHistoryEntity'
import { AddFeedback1707213619308 } from './1707213619308-AddFeedback'
export const sqliteMigrations = [
@@ -31,5 +32,6 @@ export const sqliteMigrations = [
AddFileUploadsToChatMessage1701788586491,
AddVariableEntity1699325775451,
AddSpeechToText1706364937060,
AddUpsertHistoryEntity1709814301358,
AddFeedback1707213619308
]
+2
View File
@@ -35,6 +35,7 @@ import variablesRouter from './variables'
import vectorRouter from './vectors'
import verifyRouter from './verify'
import versionRouter from './versions'
import upsertHistoryRouter from './upsert-history'
const router = express.Router()
@@ -74,5 +75,6 @@ router.use('/variables', variablesRouter)
router.use('/vector', vectorRouter)
router.use('/verify', verifyRouter)
router.use('/version', versionRouter)
router.use('/upsert-history', upsertHistoryRouter)
export default router
@@ -0,0 +1,15 @@
import express from 'express'
import upsertHistoryController from '../../controllers/upsert-history'
const router = express.Router()
// CREATE
// READ
router.get('/:id', upsertHistoryController.getAllUpsertHistory)
// PATCH
router.patch('/', upsertHistoryController.patchDeleteUpsertHistory)
// DELETE
export default router
@@ -14,6 +14,9 @@ import logger from '../../utils/logger'
import { getStoragePath } from 'flowise-components'
import { IReactFlowObject } from '../../Interface'
import { utilGetUploadsConfig } from '../../utils/getUploadsConfig'
import { ChatMessage } from '../../database/entities/ChatMessage'
import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback'
import { UpsertHistory } from '../../database/entities/UpsertHistory'
// Check if chatflow valid for streaming
const checkIfChatflowIsValidForStreaming = async (chatflowId: string): Promise<any> => {
@@ -105,9 +108,18 @@ const deleteChatflow = async (chatflowId: string): Promise<any> => {
const appServer = getRunningExpressApp()
const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).delete({ id: chatflowId })
try {
// Delete all uploads corresponding to this chatflow
// Delete all uploads corresponding to this chatflow
const directory = path.join(getStoragePath(), chatflowId)
deleteFolderRecursive(directory)
// Delete all chat messages
await appServer.AppDataSource.getRepository(ChatMessage).delete({ chatflowid: chatflowId })
// Delete all chat feedback
await appServer.AppDataSource.getRepository(ChatMessageFeedback).delete({ chatflowid: chatflowId })
// Delete all upsert history
await appServer.AppDataSource.getRepository(UpsertHistory).delete({ chatflowid: chatflowId })
} catch (e) {
logger.error(`[server]: Error deleting file storage for chatflow ${chatflowId}: ${e}`)
}
@@ -0,0 +1,66 @@
import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { UpsertHistory } from '../../database/entities/UpsertHistory'
const getAllUpsertHistory = async (
sortOrder: string | undefined,
chatflowid: string | undefined,
startDate: string | undefined,
endDate: string | undefined
) => {
try {
const appServer = getRunningExpressApp()
const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => {
const date = new Date(dateTimeStr)
if (isNaN(date.getTime())) {
return undefined
}
setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999)
return date
}
let fromDate
if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start')
let toDate
if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end')
let upsertHistory = await appServer.AppDataSource.getRepository(UpsertHistory).find({
where: {
chatflowid,
...(fromDate && { date: MoreThanOrEqual(fromDate) }),
...(toDate && { date: LessThanOrEqual(toDate) })
},
order: {
date: sortOrder === 'DESC' ? 'DESC' : 'ASC'
}
})
upsertHistory = upsertHistory.map((hist) => {
return {
...hist,
result: hist.result ? JSON.parse(hist.result) : {},
flowData: hist.flowData ? JSON.parse(hist.flowData) : {}
}
})
return upsertHistory
} catch (error) {
throw new Error(`Error: upsertHistoryServices.getAllUpsertHistory - ${error}`)
}
}
const patchDeleteUpsertHistory = async (ids: string[] = []): Promise<any> => {
try {
const appServer = getRunningExpressApp()
const dbResponse = await appServer.AppDataSource.getRepository(UpsertHistory).delete(ids)
return dbResponse
} catch (error) {
throw new Error(`Error: upsertHistoryServices.patchUpsertHistory - ${error}`)
}
}
export default {
getAllUpsertHistory,
patchDeleteUpsertHistory
}
@@ -18,7 +18,7 @@ const deleteVariable = async (variableId: string): Promise<any> => {
const dbResponse = await appServer.AppDataSource.getRepository(Variable).delete({ id: variableId })
return dbResponse
} catch (error) {
throw new Error(`Error: variablesServices.createVariable - ${error}`)
throw new Error(`Error: variablesServices.deleteVariable - ${error}`)
}
}
@@ -36,7 +36,6 @@ import { utilAddChatMessage } from './addChatMesage'
* @param {Request} req
* @param {Server} socketIO
* @param {boolean} isInternal
* @param {boolean} isUpsert
*/
export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInternal: boolean = false): Promise<any> => {
try {
+86 -2
View File
@@ -237,6 +237,84 @@ export const getEndingNodes = (nodeDependencies: INodeDependencies, graph: INode
return endingNodeIds
}
/**
* Get file name from base64 string
* @param {string} fileBase64
*/
export const getFileName = (fileBase64: string): string => {
let fileNames = []
if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) {
const files = JSON.parse(fileBase64)
for (const file of files) {
const splitDataURI = file.split(',')
const filename = splitDataURI[splitDataURI.length - 1].split(':')[1]
fileNames.push(filename)
}
return fileNames.join(', ')
} else {
const splitDataURI = fileBase64.split(',')
const filename = splitDataURI[splitDataURI.length - 1].split(':')[1]
return filename
}
}
/**
* Save upsert flowData
* @param {INodeData} nodeData
* @param {Record<string, any>} upsertHistory
*/
export const saveUpsertFlowData = (nodeData: INodeData, upsertHistory: Record<string, any>): ICommonObject[] => {
const existingUpsertFlowData = upsertHistory['flowData'] ?? []
const paramValues: ICommonObject[] = []
for (const input in nodeData.inputs) {
const inputParam = nodeData.inputParams.find((inp) => inp.name === input)
if (!inputParam) continue
let paramValue: ICommonObject = {}
if (!nodeData.inputs[input]) {
continue
}
if (
typeof nodeData.inputs[input] === 'string' &&
nodeData.inputs[input].startsWith('{{') &&
nodeData.inputs[input].endsWith('}}')
) {
continue
}
// Get file name instead of the base64 string
if (nodeData.category === 'Document Loaders' && nodeData.inputParams.find((inp) => inp.name === input)?.type === 'file') {
paramValue = {
label: inputParam?.label,
name: inputParam?.name,
type: inputParam?.type,
value: getFileName(nodeData.inputs[input])
}
paramValues.push(paramValue)
continue
}
paramValue = {
label: inputParam?.label,
name: inputParam?.name,
type: inputParam?.type,
value: nodeData.inputs[input]
}
paramValues.push(paramValue)
}
const newFlowData = {
label: nodeData.label,
name: nodeData.name,
category: nodeData.category,
id: nodeData.id,
paramValues
}
existingUpsertFlowData.push(newFlowData)
return existingUpsertFlowData
}
/**
* Build langchain from start to end
* @param {string[]} startingNodeIds
@@ -272,6 +350,8 @@ export const buildFlow = async (
) => {
const flowNodes = cloneDeep(reactFlowNodes)
let upsertHistory: Record<string, any> = {}
// Create a Queue and add our initial node in it
const nodeQueue = [] as INodeQueue[]
const exploredNode = {} as IExploredNode
@@ -302,12 +382,15 @@ export const buildFlow = async (
let flowNodeData = cloneDeep(reactFlowNode.data)
if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig)
if (isUpsert) upsertHistory['flowData'] = saveUpsertFlowData(flowNodeData, upsertHistory)
const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question, chatHistory)
// TODO: Avoid processing Text Splitter + Doc Loader once Upsert & Load Existing Vector Nodes are deprecated
if (isUpsert && stopNodeId && nodeId === stopNodeId) {
logger.debug(`[server]: Upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, {
const indexResult = await newNodeInstance.vectorStoreMethods!['upsert']!.call(newNodeInstance, reactFlowNodeData, {
chatId,
sessionId,
chatflowid,
@@ -319,6 +402,7 @@ export const buildFlow = async (
dynamicVariables,
uploads
})
if (indexResult) upsertHistory['result'] = indexResult
logger.debug(`[server]: Finished upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`)
break
} else {
@@ -422,7 +506,7 @@ export const buildFlow = async (
flowNodes.push(flowNodes.splice(index, 1)[0])
}
}
return flowNodes
return isUpsert ? (upsertHistory as any) : flowNodes
}
/**
+25 -2
View File
@@ -1,5 +1,6 @@
import { Request, Response } from 'express'
import * as fs from 'fs'
import { cloneDeep, omit } from 'lodash'
import { ICommonObject } from 'flowise-components'
import telemetryService from '../services/telemetry'
import logger from '../utils/logger'
@@ -18,7 +19,14 @@ import { utilValidateKey } from './validateKey'
import { IncomingInput, INodeDirectedGraph, IReactFlowObject, chatType } from '../Interface'
import { ChatFlow } from '../database/entities/ChatFlow'
import { getRunningExpressApp } from '../utils/getRunningExpressApp'
import { UpsertHistory } from '../database/entities/UpsertHistory'
/**
* Upsert documents
* @param {Request} req
* @param {Response} res
* @param {boolean} isInternal
*/
export const upsertVector = async (req: Request, res: Response, isInternal: boolean = false) => {
try {
const appServer = getRunningExpressApp()
@@ -78,6 +86,8 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool
(node) =>
node.data.category === 'Vector Stores' && !node.data.label.includes('Upsert') && !node.data.label.includes('Load Existing')
)
// Check if multiple vector store nodes exist, and if stopNodeId is specified
if (vsNodes.length > 1 && !stopNodeId) {
return res.status(500).send('There are multiple vector nodes, please provide stopNodeId in body request')
} else if (vsNodes.length === 1 && !stopNodeId) {
@@ -99,7 +109,7 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool
const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId)
await buildFlow(
const upsertedResult = await buildFlow(
startingNodeIds,
nodes,
edges,
@@ -121,6 +131,19 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id))
await appServer.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig)
// Save to DB
if (upsertedResult['flowData'] && upsertedResult['result']) {
const result = cloneDeep(upsertedResult)
result['flowData'] = JSON.stringify(result['flowData'])
result['result'] = JSON.stringify(omit(result['result'], ['totalKeys', 'addedDocs']))
result.chatflowid = chatflowid
const newUpsertHistory = new UpsertHistory()
Object.assign(newUpsertHistory, result)
const upsertHistory = appServer.AppDataSource.getRepository(UpsertHistory).create(newUpsertHistory)
await appServer.AppDataSource.getRepository(UpsertHistory).save(upsertHistory)
}
await telemetryService.createEvent({
name: `vector_upserted`,
data: {
@@ -131,7 +154,7 @@ export const upsertVector = async (req: Request, res: Response, isInternal: bool
stopNodeId
}
})
return res.status(201).send('Successfully Upserted')
return res.status(201).json(upsertedResult['result'] ?? { result: 'Successfully Upserted' })
} catch (e: any) {
logger.error('[server]: Error:', e)
return res.status(500).send(e.message)