add api config

This commit is contained in:
Henry
2023-05-04 18:44:51 +01:00
parent 57b8620b93
commit 8d3a374257
18 changed files with 589 additions and 59 deletions
+2
View File
@@ -54,12 +54,14 @@
"flowise-components": "*",
"flowise-ui": "*",
"moment-timezone": "^0.5.34",
"multer": "^1.4.5-lts.1",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.6"
},
"devDependencies": {
"@types/cors": "^2.8.12",
"@types/multer": "^1.4.7",
"concurrently": "^7.1.0",
"nodemon": "^2.0.15",
"oclif": "^3",
+4 -1
View File
@@ -1,3 +1,4 @@
import { ICommonObject } from 'flowise-components'
import { IActiveChatflows, INodeData, IReactFlowNode } from './Interface'
/**
@@ -12,13 +13,15 @@ export class ChatflowPool {
* @param {string} chatflowid
* @param {INodeData} endingNodeData
* @param {IReactFlowNode[]} startingNodes
* @param {ICommonObject} overrideConfig
*/
add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[]) {
add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) {
this.activeChatflows[chatflowid] = {
startingNodes,
endingNodeData,
inSync: true
}
if (overrideConfig) this.activeChatflows[chatflowid].overrideConfig = overrideConfig
}
/**
+10 -1
View File
@@ -1,4 +1,4 @@
import { INode, INodeData as INodeDataFromComponent, INodeParams } from 'flowise-components'
import { ICommonObject, INode, INodeData as INodeDataFromComponent, INodeParams } from 'flowise-components'
export type MessageType = 'apiMessage' | 'userMessage'
@@ -114,6 +114,7 @@ export interface IMessage {
export interface IncomingInput {
question: string
history: IMessage[]
overrideConfig?: ICommonObject
}
export interface IActiveChatflows {
@@ -121,5 +122,13 @@ export interface IActiveChatflows {
startingNodes: IReactFlowNode[]
endingNodeData: INodeData
inSync: boolean
overrideConfig?: ICommonObject
}
}
export interface IOverrideConfig {
node: string
label: string
name: string
type: string
}
+70 -15
View File
@@ -1,4 +1,5 @@
import express, { Request, Response } from 'express'
import multer from 'multer'
import path from 'path'
import cors from 'cors'
import http from 'http'
@@ -17,7 +18,10 @@ import {
addAPIKey,
updateAPIKey,
deleteAPIKey,
compareKeys
compareKeys,
mapMimeTypeToInputField,
findAvailableConfigs,
isSameOverrideConfig
} from './utils'
import { cloneDeep } from 'lodash'
import { getDataSource } from './DataSource'
@@ -25,6 +29,7 @@ import { NodesPool } from './NodesPool'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
import { ChatflowPool } from './ChatflowPool'
import { ICommonObject } from 'flowise-components'
export class App {
app: express.Application
@@ -66,6 +71,8 @@ export class App {
this.app.use(cors({ credentials: true, origin: 'http://localhost:8080' }))
}
const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` })
// ----------------------------------------
// Nodes
// ----------------------------------------
@@ -199,6 +206,47 @@ export class App {
return res.json(results)
})
// ----------------------------------------
// Configuration
// ----------------------------------------
this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
const flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
const nodes = parsedFlowData.nodes
const availableConfigs = findAvailableConfigs(nodes)
return res.json(availableConfigs)
})
this.app.post('/api/v1/flow-config/:id', upload.array('files'), async (req: Request, res: Response) => {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
await this.validateKey(req, res, chatflow)
const overrideConfig: ICommonObject = { ...req.body }
const files = req.files as any[]
if (!files || !files.length) return
for (const file of files) {
const fileData = fs.readFileSync(file.path, { encoding: 'base64' })
const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}`
const fileInputField = mapMimeTypeToInputField(file.mimetype)
if (overrideConfig[fileInputField]) {
overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String])
} else {
overrideConfig[fileInputField] = JSON.stringify([dataBase64String])
}
}
return res.json(overrideConfig)
})
// ----------------------------------------
// Prediction
// ----------------------------------------
@@ -281,6 +329,20 @@ export class App {
})
}
async validateKey(req: Request, res: Response, chatflow: ChatFlow) {
const chatFlowApiKeyId = chatflow.apikeyid
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
if (chatFlowApiKeyId && !authorizationHeader) return res.status(401).send(`Unauthorized`)
const suppliedKey = authorizationHeader.split(`Bearer `).pop()
if (chatFlowApiKeyId && suppliedKey) {
const keys = await getAPIKeys()
const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret
if (!compareKeys(apiSecret, suppliedKey)) return res.status(401).send(`Unauthorized`)
}
}
async processPrediction(req: Request, res: Response, isInternal = false) {
try {
const chatflowid = req.params.id
@@ -294,27 +356,19 @@ export class App {
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
if (!isInternal) {
const chatFlowApiKeyId = chatflow.apikeyid
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
if (chatFlowApiKeyId && !authorizationHeader) return res.status(401).send(`Unauthorized`)
const suppliedKey = authorizationHeader.split(`Bearer `).pop()
if (chatFlowApiKeyId && suppliedKey) {
const keys = await getAPIKeys()
const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret
if (!compareKeys(apiSecret, suppliedKey)) return res.status(401).send(`Unauthorized`)
}
await this.validateKey(req, res, chatflow)
}
/* Check if:
/* Don't rebuild the flow (to avoid duplicated upsert, recomputation) when all these conditions met:
* - Node Data already exists in pool
* - Still in sync (i.e the flow has not been modified since)
* - Existing overrideConfig and new overrideConfig are the same
* - Flow doesn't start with nodes that depend on incomingInput.question
***/
if (
Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
this.chatflowPool.activeChatflows[chatflowid].inSync &&
isSameOverrideConfig(this.chatflowPool.activeChatflows[chatflowid].overrideConfig, incomingInput.overrideConfig) &&
!isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes)
) {
nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData
@@ -359,7 +413,8 @@ export class App {
graph,
depthQueue,
this.nodesPool.componentNodes,
incomingInput.question
incomingInput.question,
incomingInput?.overrideConfig
)
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
@@ -369,7 +424,7 @@ export class App {
nodeToExecuteData = reactFlowNodeData
const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes)
this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig)
}
const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
+107 -4
View File
@@ -11,7 +11,8 @@ import {
IReactFlowEdge,
IReactFlowNode,
IVariableDict,
INodeData
INodeData,
IOverrideConfig
} from '../Interface'
import { cloneDeep, get } from 'lodash'
import { ICommonObject, getInputVariables } from 'flowise-components'
@@ -180,7 +181,8 @@ export const buildLangchain = async (
graph: INodeDirectedGraph,
depthQueue: IDepthQueue,
componentNodes: IComponentNodes,
question: string
question: string,
overrideConfig?: ICommonObject
) => {
const flowNodes = cloneDeep(reactFlowNodes)
@@ -208,7 +210,9 @@ export const buildLangchain = async (
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
const reactFlowNodeData: INodeData = resolveVariables(reactFlowNode.data, flowNodes, question)
let flowNodeData = cloneDeep(reactFlowNode.data)
if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig)
const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question)
flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question)
} catch (e: any) {
@@ -342,7 +346,24 @@ export const resolveVariables = (reactFlowNodeData: INodeData, reactFlowNodes: I
}
}
const paramsObj = (flowNodeData as any)[types]
const paramsObj = flowNodeData[types] ?? {}
getParamValues(paramsObj)
return flowNodeData
}
export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: ICommonObject) => {
const types = 'inputs'
const getParamValues = (paramsObj: ICommonObject) => {
for (const key in paramsObj) {
const paramValue: string = paramsObj[key]
paramsObj[key] = overrideConfig[key] ?? paramValue
}
}
const paramsObj = flowNodeData[types] ?? {}
getParamValues(paramsObj)
@@ -365,6 +386,24 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[]): boole
return false
}
/**
* Rebuild flow if new override config is provided
* @param {ICommonObject} existingOverrideConfig
* @param {ICommonObject} newOverrideConfig
* @returns {boolean}
*/
export const isSameOverrideConfig = (existingOverrideConfig?: ICommonObject, newOverrideConfig?: ICommonObject): boolean => {
if (
existingOverrideConfig &&
Object.keys(existingOverrideConfig).length &&
newOverrideConfig &&
Object.keys(newOverrideConfig).length &&
JSON.stringify(existingOverrideConfig) === JSON.stringify(newOverrideConfig)
)
return true
return false
}
/**
* Returns the api key path
* @returns {string}
@@ -480,3 +519,67 @@ export const deleteAPIKey = async (keyIdToDelete: string): Promise<ICommonObject
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(result), 'utf8')
return result
}
/**
* Map MimeType to InputField
* @param {string} mimeType
* @returns {Promise<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/vnd.openxmlformats-officedocument.wordprocessingml.document':
return 'docxFile'
default:
return ''
}
}
/**
* Find all available inpur params config
* @param {IReactFlowNode[]} reactFlowNodes
* @returns {Promise<IOverrideConfig[]>}
*/
export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[]) => {
const configs: IOverrideConfig[] = []
for (const flowNode of reactFlowNodes) {
for (const inputParam of flowNode.data.inputParams) {
let obj: IOverrideConfig
if (inputParam.type === 'password' || inputParam.type === 'options') {
obj = {
node: flowNode.data.label,
label: inputParam.label,
name: inputParam.name,
type: 'string'
}
} else if (inputParam.type === 'file') {
obj = {
node: flowNode.data.label,
label: inputParam.label,
name: 'files',
type: inputParam.fileType ?? inputParam.type
}
} else {
obj = {
node: flowNode.data.label,
label: inputParam.label,
name: inputParam.name,
type: inputParam.type
}
}
if (!configs.some((config) => JSON.stringify(config) === JSON.stringify(obj))) {
configs.push(obj)
}
}
}
return configs
}