add api config
This commit is contained in:
parent
57b8620b93
commit
8d3a374257
|
|
@ -38,5 +38,8 @@
|
|||
**/*.key
|
||||
**/api.json
|
||||
|
||||
## uploads
|
||||
**/uploads
|
||||
|
||||
## compressed
|
||||
**/*.tgz
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { CSVLoader } from 'langchain/document_loaders/fs/csv'
|
||||
import { getBlob } from '../../../src/utils'
|
||||
|
||||
class Csv_DocumentLoaders implements INode {
|
||||
label: string
|
||||
|
|
@ -48,11 +49,8 @@ class Csv_DocumentLoaders implements INode {
|
|||
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
|
||||
const csvFileBase64 = nodeData.inputs?.csvFile as string
|
||||
const columnName = nodeData.inputs?.columnName as string
|
||||
const splitDataURI = csvFileBase64.split(',')
|
||||
splitDataURI.pop()
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
|
||||
const blob = new Blob([bf])
|
||||
const blob = new Blob(getBlob(csvFileBase64))
|
||||
const loader = new CSVLoader(blob, columnName.trim().length === 0 ? undefined : columnName.trim())
|
||||
|
||||
if (textSplitter) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { DocxLoader } from 'langchain/document_loaders/fs/docx'
|
||||
import { getBlob } from '../../../src/utils'
|
||||
|
||||
class Docx_DocumentLoaders implements INode {
|
||||
label: string
|
||||
|
|
@ -39,11 +40,8 @@ class Docx_DocumentLoaders implements INode {
|
|||
async init(nodeData: INodeData): Promise<any> {
|
||||
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
|
||||
const docxFileBase64 = nodeData.inputs?.docxFile as string
|
||||
const splitDataURI = docxFileBase64.split(',')
|
||||
splitDataURI.pop()
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
|
||||
const blob = new Blob([bf])
|
||||
const blob = new Blob(getBlob(docxFileBase64))
|
||||
const loader = new DocxLoader(blob)
|
||||
|
||||
if (textSplitter) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { JSONLoader } from 'langchain/document_loaders/fs/json'
|
||||
import { getBlob } from '../../../src/utils'
|
||||
|
||||
class Json_DocumentLoaders implements INode {
|
||||
label: string
|
||||
|
|
@ -48,9 +49,6 @@ class Json_DocumentLoaders implements INode {
|
|||
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
|
||||
const jsonFileBase64 = nodeData.inputs?.jsonFile as string
|
||||
const pointersName = nodeData.inputs?.pointersName as string
|
||||
const splitDataURI = jsonFileBase64.split(',')
|
||||
splitDataURI.pop()
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
|
||||
let pointers: string[] = []
|
||||
if (pointersName) {
|
||||
|
|
@ -58,7 +56,7 @@ class Json_DocumentLoaders implements INode {
|
|||
pointers = outputString.split(',').map((pointer) => '/' + pointer.trim())
|
||||
}
|
||||
|
||||
const blob = new Blob([bf])
|
||||
const blob = new Blob(getBlob(jsonFileBase64))
|
||||
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined)
|
||||
|
||||
if (textSplitter) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { PDFLoader } from 'langchain/document_loaders/fs/pdf'
|
||||
import { getBlob } from '../../../src/utils'
|
||||
|
||||
class Pdf_DocumentLoaders implements INode {
|
||||
label: string
|
||||
|
|
@ -57,10 +58,7 @@ class Pdf_DocumentLoaders implements INode {
|
|||
const pdfFileBase64 = nodeData.inputs?.pdfFile as string
|
||||
const usage = nodeData.inputs?.usage as string
|
||||
|
||||
const splitDataURI = pdfFileBase64.split(',')
|
||||
splitDataURI.pop()
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
const blob = new Blob([bf])
|
||||
const blob = new Blob(getBlob(pdfFileBase64))
|
||||
|
||||
if (usage === 'perFile') {
|
||||
// @ts-ignore
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { TextLoader } from 'langchain/document_loaders/fs/text'
|
||||
import { getBlob } from '../../../src/utils'
|
||||
|
||||
class Text_DocumentLoaders implements INode {
|
||||
label: string
|
||||
|
|
@ -39,11 +40,8 @@ class Text_DocumentLoaders implements INode {
|
|||
async init(nodeData: INodeData): Promise<any> {
|
||||
const textSplitter = nodeData.inputs?.textSplitter as TextSplitter
|
||||
const txtFileBase64 = nodeData.inputs?.txtFile as string
|
||||
const splitDataURI = txtFileBase64.split(',')
|
||||
splitDataURI.pop()
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
|
||||
const blob = new Blob([bf])
|
||||
const blob = new Blob(getBlob(txtFileBase64))
|
||||
const loader = new TextLoader(blob)
|
||||
|
||||
if (textSplitter) {
|
||||
|
|
|
|||
|
|
@ -149,3 +149,27 @@ export const getInputVariables = (paramValue: string): string[] => {
|
|||
}
|
||||
return inputVariables
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blob
|
||||
* @param {string} fileBase64Str
|
||||
* @returns {Buffer[]}
|
||||
*/
|
||||
export const getBlob = (fileBase64Str: string) => {
|
||||
let bufferArray: Buffer[] = []
|
||||
let files: string[] = []
|
||||
|
||||
if (fileBase64Str.startsWith('[') && fileBase64Str.endsWith(']')) {
|
||||
files = JSON.parse(fileBase64Str)
|
||||
} else {
|
||||
files = [fileBase64Str]
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const splitDataURI = file.split(',')
|
||||
splitDataURI.pop()
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
bufferArray.push(bf)
|
||||
}
|
||||
return bufferArray
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,19 +329,7 @@ export class App {
|
|||
})
|
||||
}
|
||||
|
||||
async processPrediction(req: Request, res: Response, isInternal = false) {
|
||||
try {
|
||||
const chatflowid = req.params.id
|
||||
const incomingInput: IncomingInput = req.body
|
||||
|
||||
let nodeToExecuteData: INodeData
|
||||
|
||||
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
|
||||
id: chatflowid
|
||||
})
|
||||
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
|
||||
|
||||
if (!isInternal) {
|
||||
async validateKey(req: Request, res: Response, chatflow: ChatFlow) {
|
||||
const chatFlowApiKeyId = chatflow.apikeyid
|
||||
const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
|
||||
|
||||
|
|
@ -307,14 +343,32 @@ export class App {
|
|||
}
|
||||
}
|
||||
|
||||
/* Check if:
|
||||
async processPrediction(req: Request, res: Response, isInternal = false) {
|
||||
try {
|
||||
const chatflowid = req.params.id
|
||||
const incomingInput: IncomingInput = req.body
|
||||
|
||||
let nodeToExecuteData: INodeData
|
||||
|
||||
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
|
||||
id: chatflowid
|
||||
})
|
||||
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
|
||||
|
||||
if (!isInternal) {
|
||||
await this.validateKey(req, res, chatflow)
|
||||
}
|
||||
|
||||
/* 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import client from './client'
|
||||
|
||||
const getConfig = (id) => client.get(`/flow-config/${id}`)
|
||||
|
||||
export default {
|
||||
getConfig
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { FormControlLabel, Checkbox } from '@mui/material'
|
||||
|
||||
export const CheckboxInput = ({ value, label, onChange, disabled = false }) => {
|
||||
const [myValue, setMyValue] = useState(value)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControlLabel
|
||||
sx={{ mt: 1, width: '100%' }}
|
||||
size='small'
|
||||
control={
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
checked={myValue}
|
||||
onChange={(event) => {
|
||||
setMyValue(event.target.checked)
|
||||
onChange(event.target.checked)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxInput.propTypes = {
|
||||
value: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
|
@ -22,9 +22,12 @@ import cURLSVG from 'assets/images/cURL.svg'
|
|||
// API
|
||||
import apiKeyApi from 'api/apikey'
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
import configApi from 'api/config'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
import { CheckboxInput } from 'ui-component/checkbox/Checkbox'
|
||||
import { TableViewOnly } from 'ui-component/table/Table'
|
||||
|
||||
function TabPanel(props) {
|
||||
const { children, value, index, ...other } = props
|
||||
|
|
@ -54,6 +57,66 @@ function a11yProps(index) {
|
|||
}
|
||||
}
|
||||
|
||||
const unshiftFiles = (configData) => {
|
||||
const filesConfig = configData.find((config) => config.name === 'files')
|
||||
if (filesConfig) {
|
||||
configData = configData.filter((config) => config.name !== 'files')
|
||||
configData.unshift(filesConfig)
|
||||
}
|
||||
return configData
|
||||
}
|
||||
|
||||
const getFormDataExamplesForJS = (configData) => {
|
||||
let finalStr = ''
|
||||
configData = unshiftFiles(configData)
|
||||
const loop = Math.min(configData.length, 4)
|
||||
for (let i = 0; i < loop; i += 1) {
|
||||
const config = configData[i]
|
||||
let exampleVal = `"example"`
|
||||
if (config.type === 'string') exampleVal = `"example"`
|
||||
else if (config.type === 'boolean') exampleVal = `true`
|
||||
else if (config.type === 'number') exampleVal = `1`
|
||||
else if (config.name === 'files') exampleVal = `input.files[0]`
|
||||
finalStr += `formData.append("${config.name}", ${exampleVal})\n`
|
||||
}
|
||||
return finalStr
|
||||
}
|
||||
|
||||
const getFormDataExamplesForPython = (configData) => {
|
||||
let finalStr = ''
|
||||
configData = unshiftFiles(configData)
|
||||
const loop = Math.min(configData.length, 4)
|
||||
for (let i = 0; i < loop; i += 1) {
|
||||
const config = configData[i]
|
||||
let exampleVal = `"example"`
|
||||
if (config.type === 'string') exampleVal = `"example"`
|
||||
else if (config.type === 'boolean') exampleVal = `true`
|
||||
else if (config.type === 'number') exampleVal = `1`
|
||||
else if (config.name === 'files') exampleVal = `('example${config.type}', open('example${config.type}', 'rb'))`
|
||||
finalStr += `\n "${config.name}": ${exampleVal}`
|
||||
if (i === loop - 1) finalStr += `\n`
|
||||
}
|
||||
return finalStr
|
||||
}
|
||||
|
||||
const getFormDataExamplesForCurl = (configData) => {
|
||||
let finalStr = ''
|
||||
configData = unshiftFiles(configData)
|
||||
const loop = Math.min(configData.length, 4)
|
||||
for (let i = 0; i < loop; i += 1) {
|
||||
const config = configData[i]
|
||||
let exampleVal = `example`
|
||||
if (config.type === 'string') exampleVal = `example`
|
||||
else if (config.type === 'boolean') exampleVal = `true`
|
||||
else if (config.type === 'number') exampleVal = `1`
|
||||
else if (config.name === 'files') exampleVal = `@/home/user1/Desktop/example${config.type}`
|
||||
finalStr += `\n -F "${config.name}=${exampleVal}"`
|
||||
if (i === loop - 1) finalStr += `)\n`
|
||||
else finalStr += ` \\`
|
||||
}
|
||||
return finalStr
|
||||
}
|
||||
|
||||
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const navigate = useNavigate()
|
||||
|
|
@ -64,9 +127,18 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
|
|||
const [apiKeys, setAPIKeys] = useState([])
|
||||
const [chatflowApiKeyId, setChatflowApiKeyId] = useState('')
|
||||
const [selectedApiKey, setSelectedApiKey] = useState({})
|
||||
const [checkboxVal, setCheckbox] = useState(false)
|
||||
|
||||
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
|
||||
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||
const getConfigApi = useApi(configApi.getConfig)
|
||||
|
||||
const onCheckBoxChanged = (newVal) => {
|
||||
setCheckbox(newVal)
|
||||
if (newVal) {
|
||||
getConfigApi.request(dialogProps.chatflowid)
|
||||
}
|
||||
}
|
||||
|
||||
const onApiKeySelected = (keyValue) => {
|
||||
if (keyValue === 'addnewkey') {
|
||||
|
|
@ -106,7 +178,7 @@ output = query({
|
|||
})
|
||||
`
|
||||
} else if (codeLang === 'JavaScript') {
|
||||
return `async function query(data) {
|
||||
return `async function query() {
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
|
|
@ -190,6 +262,147 @@ output = query({
|
|||
return pythonSVG
|
||||
}
|
||||
|
||||
const getConfigCode = (codeLang, configData) => {
|
||||
if (codeLang === 'Python') {
|
||||
return `import requests
|
||||
form_data = {${getFormDataExamplesForPython(configData)}}
|
||||
|
||||
def setConfig():
|
||||
response = requests.post("${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", files=form_data)
|
||||
return response.json()
|
||||
|
||||
def query(payload):
|
||||
response = requests.post("${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", json=payload)
|
||||
return response.json()
|
||||
|
||||
# Set initial config
|
||||
config = setConfig()
|
||||
|
||||
# Run prediction with config
|
||||
output = query({
|
||||
"question": "Hey, how are you?",
|
||||
"overrideConfig": config
|
||||
})
|
||||
`
|
||||
} else if (codeLang === 'JavaScript') {
|
||||
return `let formData = new FormData();
|
||||
${getFormDataExamplesForJS(configData)}
|
||||
async function setConfig() {
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}",
|
||||
{
|
||||
method: "POST",
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
const config = await response.json();
|
||||
return config; //Returns a config object
|
||||
}
|
||||
|
||||
async function query(config) {
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
"question": "Hey, how are you?",
|
||||
"overrideConfig": config
|
||||
},
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Set initial config
|
||||
const config = await setConfig()
|
||||
|
||||
// Run prediction with config
|
||||
const res = await query(config)
|
||||
`
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `CONFIG=$(curl ${baseURL}/api/v1/flow-config/${dialogProps.chatflowid} \\
|
||||
-X POST \\${getFormDataExamplesForCurl(configData)}
|
||||
curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-d '{"question": "Hey, how are you?", "overrideConfig": $CONFIG}'`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getConfigCodeWithAuthorization = (codeLang, configData) => {
|
||||
if (codeLang === 'Python') {
|
||||
return `import requests
|
||||
form_data = {${getFormDataExamplesForPython(configData)}}
|
||||
headers = {"Authorization": "Bearer ${selectedApiKey?.apiKey}"}
|
||||
|
||||
def setConfig():
|
||||
response = requests.post("${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", headers=headers, files=form_data)
|
||||
return response.json()
|
||||
|
||||
def query(payload):
|
||||
response = requests.post("${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", headers=headers, json=payload)
|
||||
return response.json()
|
||||
|
||||
# Set initial config
|
||||
config = setConfig()
|
||||
|
||||
# Run prediction with config
|
||||
output = query({
|
||||
"question": "Hey, how are you?",
|
||||
"overrideConfig": config
|
||||
})
|
||||
`
|
||||
} else if (codeLang === 'JavaScript') {
|
||||
return `let formData = new FormData();
|
||||
${getFormDataExamplesForJS(configData)}
|
||||
async function setConfig() {
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}",
|
||||
{
|
||||
headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" },
|
||||
method: "POST",
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
const config = await response.json();
|
||||
return config; //Returns a config object
|
||||
}
|
||||
|
||||
async function query(config) {
|
||||
const response = await fetch(
|
||||
"${baseURL}/api/v1/prediction/${dialogProps.chatflowid}",
|
||||
{
|
||||
headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" },
|
||||
method: "POST",
|
||||
body: {
|
||||
"question": "Hey, how are you?",
|
||||
"overrideConfig": config
|
||||
},
|
||||
}
|
||||
);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Set initial config
|
||||
const config = await setConfig()
|
||||
|
||||
// Run prediction with config
|
||||
const res = await query(config)
|
||||
`
|
||||
} else if (codeLang === 'cURL') {
|
||||
return `CONFIG=$(curl ${baseURL}/api/v1/flow-config/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"\\${getFormDataExamplesForCurl(configData)}
|
||||
curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\
|
||||
-X POST \\
|
||||
-H "Authorization: Bearer ${selectedApiKey?.apiKey}"
|
||||
-d '{"question": "Hey, how are you?", "overrideConfig": $CONFIG}'`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllAPIKeysApi.data) {
|
||||
const options = [
|
||||
|
|
@ -219,7 +432,9 @@ output = query({
|
|||
}, [dialogProps, getAllAPIKeysApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) getAllAPIKeysApi.request()
|
||||
if (show) {
|
||||
getAllAPIKeysApi.request()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [show])
|
||||
|
|
@ -277,6 +492,23 @@ output = query({
|
|||
showLineNumbers={false}
|
||||
wrapLines
|
||||
/>
|
||||
<CheckboxInput label='Show Input Config' value={checkboxVal} onChange={onCheckBoxChanged} />
|
||||
{checkboxVal && getConfigApi.data && getConfigApi.data.length > 0 && (
|
||||
<>
|
||||
<TableViewOnly rows={getConfigApi.data} columns={Object.keys(getConfigApi.data[0])} />
|
||||
<CopyBlock
|
||||
theme={atomOneDark}
|
||||
text={
|
||||
chatflowApiKeyId
|
||||
? getConfigCodeWithAuthorization(codeLang, getConfigApi.data)
|
||||
: getConfigCode(codeLang, getConfigApi.data)
|
||||
}
|
||||
language={getLang(codeLang)}
|
||||
showLineNumbers={false}
|
||||
wrapLines
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</TabPanel>
|
||||
))}
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ export const File = ({ value, fileType, onChange, disabled = false }) => {
|
|||
|
||||
const [myValue, setMyValue] = useState(value ?? '')
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const handleFileUpload = async (e) => {
|
||||
if (!e.target.files) return
|
||||
|
||||
if (e.target.files.length === 1) {
|
||||
const file = e.target.files[0]
|
||||
const { name } = file
|
||||
|
||||
|
|
@ -29,6 +30,28 @@ export const File = ({ value, fileType, onChange, disabled = false }) => {
|
|||
onChange(value)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
} else if (e.target.files.length > 0) {
|
||||
let files = Array.from(e.target.files).map((file) => {
|
||||
const reader = new FileReader()
|
||||
const { name } = file
|
||||
|
||||
return new Promise((resolve) => {
|
||||
reader.onload = (evt) => {
|
||||
if (!evt?.target?.result) {
|
||||
return
|
||||
}
|
||||
const { result } = evt.target
|
||||
const value = result + `,filename:${name}`
|
||||
resolve(value)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
})
|
||||
|
||||
const res = await Promise.all(files)
|
||||
setMyValue(JSON.stringify(res))
|
||||
onChange(JSON.stringify(res))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -51,7 +74,7 @@ export const File = ({ value, fileType, onChange, disabled = false }) => {
|
|||
sx={{ marginRight: '1rem' }}
|
||||
>
|
||||
{'Upload File'}
|
||||
<input type='file' accept={fileType} hidden onChange={(e) => handleFileUpload(e)} />
|
||||
<input type='file' multiple accept={fileType} hidden onChange={(e) => handleFileUpload(e)} />
|
||||
</Button>
|
||||
</FormControl>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper } from '@mui/material'
|
||||
|
||||
export const TableViewOnly = ({ columns, rows }) => {
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((col, index) => (
|
||||
<TableCell key={index}>{col.charAt(0).toUpperCase() + col.slice(1)}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index) => (
|
||||
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{Object.keys(row).map((key, index) => (
|
||||
<TableCell key={index}>{row[key]}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
TableViewOnly.propTypes = {
|
||||
rows: PropTypes.array,
|
||||
columns: PropTypes.array
|
||||
}
|
||||
|
|
@ -206,9 +206,20 @@ export const convertDateStringToDateObject = (dateString) => {
|
|||
}
|
||||
|
||||
export const getFileName = (fileBase64) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export const getFolderName = (base64ArrayStr) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue