mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
add api config
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user