From 601b0b0f5befcf69cdc07e1adb7bf6346a61b778 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 10 May 2023 01:06:53 +0100 Subject: [PATCH] removed flow-config endpoint and resort to just prediction endpoint --- packages/server/src/index.ts | 52 ++-- packages/server/src/utils/index.ts | 13 +- packages/ui/src/menu-items/settings.js | 11 +- packages/ui/src/store/constant.js | 1 + .../src/ui-component/dialog/APICodeDialog.js | 258 +++++++++++------- packages/ui/src/views/canvas/CanvasHeader.js | 72 +++-- packages/ui/src/views/canvas/index.js | 9 +- .../views/marketplaces/MarketplaceCanvas.js | 1 - 8 files changed, 255 insertions(+), 162 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index a168570d..99704e81 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -222,37 +222,12 @@ export class App { 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 // ---------------------------------------- // Send input message and get prediction result (External) - this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => { + this.app.post('/api/v1/prediction/:id', upload.array('files'), async (req: Request, res: Response) => { await this.processPrediction(req, res) }) @@ -346,7 +321,7 @@ export class App { async processPrediction(req: Request, res: Response, isInternal = false) { try { const chatflowid = req.params.id - const incomingInput: IncomingInput = req.body + let incomingInput: IncomingInput = req.body let nodeToExecuteData: INodeData @@ -359,6 +334,28 @@ export class App { await this.validateKey(req, res, chatflow) } + const files = (req.files as any[]) || [] + + if (files.length) { + const overrideConfig: ICommonObject = { ...req.body } + 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]) + } + } + incomingInput = { + question: req.body.question ?? 'hello', + overrideConfig, + history: [] + } + } + /* 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) @@ -378,7 +375,6 @@ export class App { nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData } else { /*** Get chatflows and prepare data ***/ - const flowData = chatflow.flowData const parsedFlowData: IReactFlowObject = JSON.parse(flowData) const nodes = parsedFlowData.nodes diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 1b4b41c6..8f668e12 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -353,6 +353,12 @@ export const resolveVariables = (reactFlowNodeData: INodeData, reactFlowNodes: I return flowNodeData } +/** + * Loop through each inputs and replace their value with override config values + * @param {INodeData} flowNodeData + * @param {ICommonObject} overrideConfig + * @returns {INodeData} + */ export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: ICommonObject) => { const types = 'inputs' @@ -560,12 +566,7 @@ export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[]) => { 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' - } + continue } else if (inputParam.type === 'file') { obj = { node: flowNode.data.label, diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 25fd7a55..77b9ebf5 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,8 +1,8 @@ // assets -import { IconTrash, IconFileUpload, IconFileExport } from '@tabler/icons' +import { IconTrash, IconFileUpload, IconFileExport, IconCopy } from '@tabler/icons' // constant -const icons = { IconTrash, IconFileUpload, IconFileExport } +const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -11,6 +11,13 @@ const settings = { title: '', type: 'group', children: [ + { + id: 'duplicateChatflow', + title: 'Duplicate Chatflow', + type: 'item', + url: '', + icon: icons.IconCopy + }, { id: 'loadChatflow', title: 'Load Chatflow', diff --git a/packages/ui/src/store/constant.js b/packages/ui/src/store/constant.js index 61acbd92..c3138257 100644 --- a/packages/ui/src/store/constant.js +++ b/packages/ui/src/store/constant.js @@ -4,3 +4,4 @@ export const drawerWidth = 260 export const appDrawerWidth = 320 export const maxScroll = 100000 export const baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000') +export const uiBaseURL = window.location.origin diff --git a/packages/ui/src/ui-component/dialog/APICodeDialog.js b/packages/ui/src/ui-component/dialog/APICodeDialog.js index f160c44d..b744b919 100644 --- a/packages/ui/src/ui-component/dialog/APICodeDialog.js +++ b/packages/ui/src/ui-component/dialog/APICodeDialog.js @@ -66,7 +66,7 @@ const unshiftFiles = (configData) => { return configData } -const getFormDataExamplesForJS = (configData) => { +const getConfigExamplesForJS = (configData, bodyType) => { let finalStr = '' configData = unshiftFiles(configData) const loop = Math.min(configData.length, 4) @@ -77,12 +77,13 @@ const getFormDataExamplesForJS = (configData) => { 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` + finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `formData.append("${config.name}", ${exampleVal})\n` + if (i === loop - 1 && bodyType !== 'json') `formData.append("question", "Hey, how are you?")\n` } return finalStr } -const getFormDataExamplesForPython = (configData) => { +const getConfigExamplesForPython = (configData, bodyType) => { let finalStr = '' configData = unshiftFiles(configData) const loop = Math.min(configData.length, 4) @@ -93,26 +94,26 @@ const getFormDataExamplesForPython = (configData) => { 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` + finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `\n "${config.name}": ${exampleVal},` + if (i === loop - 1 && bodyType !== 'json') finalStr += `\n "question": "Hey, how are you?"\n` } return finalStr } -const getFormDataExamplesForCurl = (configData) => { +const getConfigExamplesForCurl = (configData, bodyType) => { 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` + if (config.type === 'string') exampleVal = bodyType === 'json' ? `"example"` : `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 += ` \\` + finalStr += bodyType === 'json' ? `"${config.name}": ${exampleVal}` : `\n -F "${config.name}=${exampleVal}"` + if (i === loop - 1) finalStr += bodyType === 'json' ? ` }` : ` \\\n -F "question=Hey, how are you?"` + else finalStr += bodyType === 'json' ? `, ` : ` \\` } return finalStr } @@ -178,19 +179,21 @@ output = query({ }) ` } else if (codeLang === 'JavaScript') { - return `async function query() { + return `async function query(data) { const response = await fetch( "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", { method: "POST", - body: { - "question": "Hey, how are you?" - }, + body: data } ); const result = await response.json(); return result; } + +query({"question": "Hey, how are you?"}).then((response) => { + console.log(response); +}); ` } else if (codeLang === 'cURL') { return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ @@ -222,14 +225,16 @@ output = query({ { headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" }, method: "POST", - body: { - "question": "Hey, how are you?" - }, + body: data } ); const result = await response.json(); return result; } + +query({"question": "Hey, how are you?"}).then((response) => { + console.log(response); +}); ` } else if (codeLang === 'cURL') { return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ @@ -262,143 +267,190 @@ output = query({ return pythonSVG } - const getConfigCode = (codeLang, configData) => { + // ----------------------------CONFIG FORM DATA --------------------------// + + const getConfigCodeWithFormData = (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) +API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}" + +# use form data to upload files +form_data = {${getConfigExamplesForPython(configData, 'formData')}} + +def query(form_data): + response = requests.post(API_URL, 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 -}) +output = query(form_data) ` } 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) { + return `// use FormData to upload files +let formData = new FormData(); +${getConfigExamplesForJS(configData, 'formData')} +async function query(formData) { const response = await fetch( "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", { method: "POST", - body: { - "question": "Hey, how are you?", - "overrideConfig": config - }, + body: formData } ); const result = await response.json(); return result; } -// Set initial config -const config = await setConfig() - -// Run prediction with config -const res = await query(config) +query(formData).then((response) => { + console.log(response); +}); ` } 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 `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ + -X POST \\${getConfigExamplesForCurl(configData, 'formData')}` } return '' } - const getConfigCodeWithAuthorization = (codeLang, configData) => { + // ----------------------------CONFIG FORM DATA with AUTH--------------------------// + + const getConfigCodeWithFormDataWithAuth = (codeLang, configData) => { if (codeLang === 'Python') { return `import requests -form_data = {${getFormDataExamplesForPython(configData)}} + +API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}" headers = {"Authorization": "Bearer ${selectedApiKey?.apiKey}"} -def setConfig(): - response = requests.post("${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", headers=headers, files=form_data) +# use form data to upload files +form_data = {${getConfigExamplesForPython(configData, 'formData')}} + +def query(form_data): + response = requests.post(API_URL, 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 -}) +output = query(form_data) ` } else if (codeLang === 'JavaScript') { - return `let formData = new FormData(); -${getFormDataExamplesForJS(configData)} -async function setConfig() { + return `// use FormData to upload files +let formData = new FormData(); +${getConfigExamplesForJS(configData, 'formData')} +async function query(formData) { const response = await fetch( - "${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", + "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", { headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" }, method: "POST", body: formData } ); - const config = await response.json(); - return config; //Returns a config object + const result = await response.json(); + return result; } -async function query(config) { +query(formData).then((response) => { + console.log(response); +}); +` + } else if (codeLang === 'cURL') { + return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ + -X POST \\${getConfigExamplesForCurl(configData, 'formData')} \\ + -H "Authorization: Bearer ${selectedApiKey?.apiKey}"` + } + return '' + } + + // ----------------------------CONFIG JSON--------------------------// + + const getConfigCode = (codeLang, configData) => { + if (codeLang === 'Python') { + return `import requests + +API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}" + +def query(payload): + response = requests.post(API_URL, json=payload) + return response.json() + +output = query({ + "question": "Hey, how are you?", + "overrideConfig": {${getConfigExamplesForPython(configData, 'json')} + } +}) +` + } else if (codeLang === 'JavaScript') { + return `async function query(data) { 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 - }, + body: data } ); const result = await response.json(); return result; } -// Set initial config -const config = await setConfig() - -// Run prediction with config -const res = await query(config) +query({ + "question": "Hey, how are you?", + "overrideConfig": {${getConfigExamplesForJS(configData, 'json')} + } +}).then((response) => { + console.log(response); +}); ` } else if (codeLang === 'cURL') { - return `CONFIG=$(curl ${baseURL}/api/v1/flow-config/${dialogProps.chatflowid} \\ + return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ -X POST \\ - -H "Authorization: Bearer ${selectedApiKey?.apiKey}"\\${getFormDataExamplesForCurl(configData)} -curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ + -d '{"question": "Hey, how are you?", "overrideConfig": {${getConfigExamplesForCurl(configData, 'json')}}'` + } + return '' + } + + // ----------------------------CONFIG JSON with AUTH--------------------------// + + const getConfigCodeWithAuthorization = (codeLang, configData) => { + if (codeLang === 'Python') { + return `import requests + +API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}" +headers = {"Authorization": "Bearer ${selectedApiKey?.apiKey}"} + +def query(payload): + response = requests.post(API_URL, headers=headers, json=payload) + return response.json() + +output = query({ + "question": "Hey, how are you?", + "overrideConfig": {${getConfigExamplesForPython(configData, 'json')} + } +}) +` + } else if (codeLang === 'JavaScript') { + return `async function query(data) { + const response = await fetch( + "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", + { + headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" }, + method: "POST", + body: data + } + ); + const result = await response.json(); + return result; +} + +query({ + "question": "Hey, how are you?", + "overrideConfig": {${getConfigExamplesForJS(configData, 'json')} + } +}).then((response) => { + console.log(response); +}); +` + } else if (codeLang === 'cURL') { + return `curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ -X POST \\ - -H "Authorization: Bearer ${selectedApiKey?.apiKey}" - -d '{"question": "Hey, how are you?", "overrideConfig": $CONFIG}'` + -d '{"question": "Hey, how are you?", "overrideConfig": {${getConfigExamplesForCurl(configData, 'json')}}' \\ + -H "Authorization: Bearer ${selectedApiKey?.apiKey}"` } return '' } @@ -500,7 +552,11 @@ curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ theme={atomOneDark} text={ chatflowApiKeyId - ? getConfigCodeWithAuthorization(codeLang, getConfigApi.data) + ? dialogProps.isFormDataRequired + ? getConfigCodeWithFormDataWithAuth(codeLang, getConfigApi.data) + : getConfigCodeWithAuthorization(codeLang, getConfigApi.data) + : dialogProps.isFormDataRequired + ? getConfigCodeWithFormData(codeLang, getConfigApi.data) : getConfigCode(codeLang, getConfigApi.data) } language={getLang(codeLang)} diff --git a/packages/ui/src/views/canvas/CanvasHeader.js b/packages/ui/src/views/canvas/CanvasHeader.js index 70c6aa02..03bf61f2 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.js +++ b/packages/ui/src/views/canvas/CanvasHeader.js @@ -23,6 +23,7 @@ import useApi from 'hooks/useApi' // utils import { generateExportFlowData } from 'utils/genericHelper' +import { uiBaseURL } from 'store/constant' // ==============================|| CANVAS HEADER ||============================== // @@ -47,6 +48,13 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl if (setting === 'deleteChatflow') { handleDeleteFlow() + } else if (setting === 'duplicateChatflow') { + try { + localStorage.setItem('duplicatedFlowData', chatflow.flowData) + window.open(`${uiBaseURL}/canvas`, '_blank') + } catch (e) { + console.error(e) + } } else if (setting === 'exportChatflow') { try { const flowData = JSON.parse(chatflow.flowData) @@ -80,10 +88,26 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl } const onAPIDialogClick = () => { + let isFormDataRequired = false + + try { + const flowData = JSON.parse(chatflow.flowData) + const nodes = flowData.nodes + for (const node of nodes) { + if (node.data.inputParams.find((param) => param.type === 'file')) { + isFormDataRequired = true + break + } + } + } catch (e) { + console.error(e) + } + setAPIDialogProps({ title: 'Use this chatflow with API', chatflowid: chatflow.id, - chatflowApiKeyId: chatflow.apikeyid + chatflowApiKeyId: chatflow.apikeyid, + isFormDataRequired }) setAPIDialogOpen(true) } @@ -131,7 +155,9 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl } }} color='inherit' - onClick={() => navigate(-1)} + onClick={() => + window.history.state && window.history.state.idx > 0 ? navigate(-1) : navigate('/', { replace: true }) + } > @@ -231,26 +257,28 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl )} - - - - - + {chatflow?.id && ( + + + + + + )} { if (chatflowId) { getSpecificChatflowApi.request(chatflowId) } else { - setNodes([]) - setEdges([]) + if (localStorage.getItem('duplicatedFlowData')) { + handleLoadFlow(localStorage.getItem('duplicatedFlowData')) + setTimeout(() => localStorage.removeItem('duplicatedFlowData'), 0) + } else { + setNodes([]) + setEdges([]) + } dispatch({ type: SET_CHATFLOW, chatflow: { diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.js b/packages/ui/src/views/marketplaces/MarketplaceCanvas.js index 66ce0283..7ce29451 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.js +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.js @@ -46,7 +46,6 @@ const MarketplaceCanvas = () => { }, [flowData]) const onChatflowCopy = (flowData) => { - //navigator.clipboard.writeText(JSON.stringify(flowData)) const templateFlowData = JSON.stringify(flowData) navigate(`/canvas`, { state: { templateFlowData } }) }