From 537aa51ef8b043012180f2bf7aad65e4f5be1f8e Mon Sep 17 00:00:00 2001 From: Ilango Date: Wed, 13 Nov 2024 23:51:59 +0530 Subject: [PATCH] Enable/disable variables in override configuration (#3467) * Add ability to enable/disable which variables can be overriden during external predictions * Remove duplicated code * Remove rate limit and allowed domains tab from chatflow configuration * Show tooltip in api code dialog for override config properties * Fix server crash when override config is not available * update UI for chatflow config security, file upload * Fix UI issues in security tab of chatflow configuration dialog * Fix override config options not updating when nodes change * Fix crash in api code dialog when overrideConfig is not available for a chatflow/agentflow. Also fix input config in api code dialog not updating when nodes change. * Refactor override config and add override config for variables * Update api code dialog - update how override config is read and show variable overrides * Update how node and variable overrides are read and resolved * Prevent api code dialog mounting on page load and only mount when api code dialog button is clicked. this should fix loading incorrect data. * Fix variables list not showing when overrideConfig is not available * add overrideconfig to agentflow and upsert vector * temporarily removed enable overrideconfig on upsert, fix linting issues --------- Co-authored-by: Henry --- .../src/services/openai-realtime/index.ts | 22 +- packages/server/src/utils/buildAgentGraph.ts | 301 +++++++----- packages/server/src/utils/buildChatflow.ts | 20 +- packages/server/src/utils/index.ts | 72 ++- packages/server/src/utils/upsertVector.ts | 2 +- .../dialog/ChatflowConfigurationDialog.jsx | 26 +- .../ui-component/extended/AllowedDomains.jsx | 44 +- .../src/ui-component/extended/FileUpload.jsx | 50 +- .../ui-component/extended/OverrideConfig.jsx | 430 ++++++++++++++++++ .../src/ui-component/extended/RateLimit.jsx | 70 ++- .../ui/src/ui-component/extended/Security.jsx | 26 ++ .../ui-component/extended/SpeechToText.jsx | 4 +- packages/ui/src/ui-component/table/Table.jsx | 39 +- packages/ui/src/views/canvas/CanvasHeader.jsx | 2 +- .../ui/src/views/chatflows/APICodeDialog.jsx | 230 +++++++--- .../ui/src/views/chatflows/ShareChatbot.jsx | 4 +- 16 files changed, 1053 insertions(+), 289 deletions(-) create mode 100644 packages/ui/src/ui-component/extended/OverrideConfig.jsx create mode 100644 packages/ui/src/ui-component/extended/Security.jsx diff --git a/packages/server/src/services/openai-realtime/index.ts b/packages/server/src/services/openai-realtime/index.ts index cdc02a11..dc48976a 100644 --- a/packages/server/src/services/openai-realtime/index.ts +++ b/packages/server/src/services/openai-realtime/index.ts @@ -1,7 +1,15 @@ import { StatusCodes } from 'http-status-codes' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' -import { buildFlow, constructGraphs, databaseEntities, getEndingNodes, getStartingNodes, resolveVariables } from '../../utils' +import { + buildFlow, + constructGraphs, + databaseEntities, + getAPIOverrideConfig, + getEndingNodes, + getStartingNodes, + resolveVariables +} from '../../utils' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { ChatFlow } from '../../database/entities/ChatFlow' import { IDepthQueue, IReactFlowNode } from '../../Interface' @@ -51,6 +59,8 @@ const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessag } startingNodeIds = [...new Set(startingNodeIds)] + const { nodeOverrides, variableOverrides, apiOverrideStatus } = getAPIOverrideConfig(chatflow) + const reactFlowNodes = await buildFlow({ startingNodeIds, reactFlowNodes: nodes, @@ -64,7 +74,10 @@ const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessag sessionId: chatId, chatflowid, apiMessageId, - appDataSource: appServer.AppDataSource + appDataSource: appServer.AppDataSource, + apiOverrideStatus, + nodeOverrides, + variableOverrides }) const nodeToExecute = @@ -77,13 +90,16 @@ const buildAndInitTool = async (chatflowid: string, _chatId?: string, _apiMessag } const flowDataObj: ICommonObject = { chatflowid, chatId } + const reactFlowNodeData: INodeData = await resolveVariables( appServer.AppDataSource, nodeToExecute.data, reactFlowNodes, '', [], - flowDataObj + flowDataObj, + '', + variableOverrides ) let nodeToExecuteData = reactFlowNodeData diff --git a/packages/server/src/utils/buildAgentGraph.ts b/packages/server/src/utils/buildAgentGraph.ts index 6acf641b..440ebb00 100644 --- a/packages/server/src/utils/buildAgentGraph.ts +++ b/packages/server/src/utils/buildAgentGraph.ts @@ -37,7 +37,8 @@ import { databaseEntities, getSessionChatHistory, getMemorySessionId, - clearSessionMemory + clearSessionMemory, + getAPIOverrideConfig } from '../utils' import { getRunningExpressApp } from './getRunningExpressApp' import { replaceInputsWithConfig, resolveVariables } from '.' @@ -111,6 +112,9 @@ export const buildAgentGraph = async ( ) } + /*** Get API Config ***/ + const { nodeOverrides, variableOverrides, apiOverrideStatus } = getAPIOverrideConfig(chatflow) + // Initialize nodes like ChatModels, Tools, etc. const reactFlowNodes: IReactFlowNode[] = await buildFlow({ startingNodeIds, @@ -121,17 +125,20 @@ export const buildAgentGraph = async ( depthQueue, componentNodes: appServer.nodesPool.componentNodes, question: incomingInput.question, + uploadedFilesContent, chatHistory, chatId, sessionId, chatflowid, appDataSource: appServer.AppDataSource, overrideConfig: incomingInput?.overrideConfig, + apiOverrideStatus, + nodeOverrides, + variableOverrides, cachePool: appServer.cachePool, isUpsert: false, uploads: incomingInput.uploads, - baseURL, - uploadedFilesContent + baseURL }) const options = { @@ -177,39 +184,39 @@ export const buildAgentGraph = async ( try { if (!seqAgentNodes.length) { - streamResults = await compileMultiAgentsGraph( + streamResults = await compileMultiAgentsGraph({ chatflow, mapNameToLabel, reactFlowNodes, - endingNodeIds, - appServer.nodesPool.componentNodes, + workerNodeIds: endingNodeIds, + componentNodes: appServer.nodesPool.componentNodes, options, startingNodeIds, - incomingInput.question, - incomingInput.history, + question: incomingInput.question, + prependHistoryMessages: incomingInput.history, chatHistory, - incomingInput?.overrideConfig, - sessionId || chatId, - seqAgentNodes.some((node) => node.data.inputs?.summarization), + overrideConfig: incomingInput?.overrideConfig, + threadId: sessionId || chatId, + summarization: seqAgentNodes.some((node) => node.data.inputs?.summarization), uploadedFilesContent - ) + }) } else { isSequential = true - streamResults = await compileSeqAgentsGraph( + streamResults = await compileSeqAgentsGraph({ depthQueue, chatflow, reactFlowNodes, - edges, - appServer.nodesPool.componentNodes, + reactFlowEdges: edges, + componentNodes: appServer.nodesPool.componentNodes, options, - incomingInput.question, - incomingInput.history, + question: incomingInput.question, + prependHistoryMessages: incomingInput.history, chatHistory, - incomingInput?.overrideConfig, - sessionId || chatId, - incomingInput.action, + overrideConfig: incomingInput?.overrideConfig, + threadId: sessionId || chatId, + action: incomingInput.action, uploadedFilesContent - ) + }) } if (streamResults) { @@ -367,7 +374,11 @@ export const buildAgentGraph = async ( // Map raw tool calls to used tools, to be shown on interrupted message const mappedToolCalls = lastMessageRaw.tool_calls.map((toolCall) => { - return { tool: toolCall.name, toolInput: toolCall.args, toolOutput: '' } + return { + tool: toolCall.name, + toolInput: toolCall.args, + toolOutput: '' + } }) // Emit the interrupt message to the client @@ -388,7 +399,11 @@ export const buildAgentGraph = async ( } finalAction = { id: uuidv4(), - mapping: { approve: approveButtonText, reject: rejectButtonText, toolCalls: lastMessageRaw.tool_calls }, + mapping: { + approve: approveButtonText, + reject: rejectButtonText, + toolCalls: lastMessageRaw.tool_calls + }, elements: [ { type: 'approve-button', label: approveButtonText }, { type: 'reject-button', label: rejectButtonText } @@ -446,37 +461,42 @@ export const buildAgentGraph = async ( } } -/** - * Compile Multi Agents Graph - * @param {IChatFlow} chatflow - * @param {Record} mapNameToLabel - * @param {IReactFlowNode[]} reactflowNodes - * @param {string[]} workerNodeIds - * @param {IComponentNodes} componentNodes - * @param {ICommonObject} options - * @param {string[]} startingNodeIds - * @param {string} question - * @param {ICommonObject} overrideConfig - * @param {string} threadId - * @param {boolean} summarization - * @param {string} uploadedFilesContent, - */ -const compileMultiAgentsGraph = async ( - chatflow: IChatFlow, - mapNameToLabel: Record, - reactflowNodes: IReactFlowNode[] = [], - workerNodeIds: string[], - componentNodes: IComponentNodes, - options: ICommonObject, - startingNodeIds: string[], - question: string, - prependHistoryMessages: IMessage[] = [], - chatHistory: IMessage[] = [], - overrideConfig?: ICommonObject, - threadId?: string, - summarization?: boolean, +type MultiAgentsGraphParams = { + chatflow: IChatFlow + mapNameToLabel: Record + reactFlowNodes: IReactFlowNode[] + workerNodeIds: string[] + componentNodes: IComponentNodes + options: ICommonObject + startingNodeIds: string[] + question: string + prependHistoryMessages?: IMessage[] + chatHistory?: IMessage[] + overrideConfig?: ICommonObject + threadId?: string + summarization?: boolean uploadedFilesContent?: string -) => { +} + +const compileMultiAgentsGraph = async (params: MultiAgentsGraphParams) => { + const { + chatflow, + mapNameToLabel, + reactFlowNodes, + workerNodeIds, + componentNodes, + options, + startingNodeIds, + prependHistoryMessages = [], + chatHistory = [], + overrideConfig = {}, + threadId, + summarization = false, + uploadedFilesContent + } = params + + let question = params.question + const appServer = getRunningExpressApp() const channels: ITeamState = { messages: { @@ -495,7 +515,10 @@ const compileMultiAgentsGraph = async ( channels }) - const workerNodes = reactflowNodes.filter((node) => workerNodeIds.includes(node.data.id)) + const workerNodes = reactFlowNodes.filter((node) => workerNodeIds.includes(node.data.id)) + + /*** Get API Config ***/ + const { nodeOverrides, variableOverrides, apiOverrideStatus } = getAPIOverrideConfig(chatflow) let supervisorWorkers: { [key: string]: IMultiAgentNode[] } = {} @@ -506,15 +529,16 @@ const compileMultiAgentsGraph = async ( const newNodeInstance = new nodeModule.nodeClass() let flowNodeData = cloneDeep(workerNode.data) - if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) + if (overrideConfig && apiOverrideStatus) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig, nodeOverrides) flowNodeData = await resolveVariables( appServer.AppDataSource, flowNodeData, - reactflowNodes, + reactFlowNodes, question, chatHistory, overrideConfig, - uploadedFilesContent + uploadedFilesContent, + variableOverrides ) try { @@ -536,7 +560,7 @@ const compileMultiAgentsGraph = async ( // Init supervisor nodes for (const supervisor in supervisorWorkers) { const supervisorInputLabel = mapNameToLabel[supervisor].label - const supervisorNode = reactflowNodes.find((node) => supervisorInputLabel === node.data.inputs?.supervisorName) + const supervisorNode = reactFlowNodes.find((node) => supervisorInputLabel === node.data.inputs?.supervisorName) if (!supervisorNode) continue const nodeInstanceFilePath = componentNodes[supervisorNode.data.name].filePath as string @@ -545,15 +569,16 @@ const compileMultiAgentsGraph = async ( let flowNodeData = cloneDeep(supervisorNode.data) - if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) + if (overrideConfig && apiOverrideStatus) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig, nodeOverrides) flowNodeData = await resolveVariables( appServer.AppDataSource, flowNodeData, - reactflowNodes, + reactFlowNodes, question, chatHistory, overrideConfig, - uploadedFilesContent + uploadedFilesContent, + variableOverrides ) if (flowNodeData.inputs) flowNodeData.inputs.workerNodes = supervisorWorkers[supervisor] @@ -598,7 +623,7 @@ const compileMultiAgentsGraph = async ( appServer.chatflowPool.add( `${chatflow.id}_${options.chatId}`, workflowGraph as any, - reactflowNodes.filter((node) => startingNodeIds.includes(node.id)), + reactFlowNodes.filter((node) => startingNodeIds.includes(node.id)), overrideConfig ) @@ -616,9 +641,17 @@ const compileMultiAgentsGraph = async ( if (prependHistoryMessages.length === chatHistory.length) { for (const message of prependHistoryMessages) { if (message.role === 'apiMessage' || message.type === 'apiMessage') { - prependMessages.push(new AIMessage({ content: message.message || message.content || '' })) + prependMessages.push( + new AIMessage({ + content: message.message || message.content || '' + }) + ) } else if (message.role === 'userMessage' || message.type === 'userMessage') { - prependMessages.push(new HumanMessage({ content: message.message || message.content || '' })) + prependMessages.push( + new HumanMessage({ + content: message.message || message.content || '' + }) + ) } } } @@ -629,7 +662,11 @@ const compileMultiAgentsGraph = async ( { messages: [...prependMessages, new HumanMessage({ content: finalQuestion })] }, - { recursionLimit: supervisorResult?.recursionLimit ?? 100, callbacks: [loggerHandler, ...callbacks], configurable: config } + { + recursionLimit: supervisorResult?.recursionLimit ?? 100, + callbacks: [loggerHandler, ...callbacks], + configurable: config + } ) } catch (e) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error initialize supervisor nodes - ${getErrorMessage(e)}`) @@ -637,35 +674,40 @@ const compileMultiAgentsGraph = async ( } } -/** - * Compile Seq Agents Graph - * @param {IDepthQueue} depthQueue - * @param {IChatFlow} chatflow - * @param {IReactFlowNode[]} reactflowNodes - * @param {IReactFlowEdge[]} reactflowEdges - * @param {IComponentNodes} componentNodes - * @param {ICommonObject} options - * @param {string} question - * @param {IMessage[]} chatHistory - * @param {ICommonObject} overrideConfig - * @param {string} threadId - * @param {IAction} action - */ -const compileSeqAgentsGraph = async ( - depthQueue: IDepthQueue, - chatflow: IChatFlow, - reactflowNodes: IReactFlowNode[] = [], - reactflowEdges: IReactFlowEdge[] = [], - componentNodes: IComponentNodes, - options: ICommonObject, - question: string, - prependHistoryMessages: IMessage[] = [], - chatHistory: IMessage[] = [], - overrideConfig?: ICommonObject, - threadId?: string, - action?: IAction, +type SeqAgentsGraphParams = { + depthQueue: IDepthQueue + chatflow: IChatFlow + reactFlowNodes: IReactFlowNode[] + reactFlowEdges: IReactFlowEdge[] + componentNodes: IComponentNodes + options: ICommonObject + question: string + prependHistoryMessages?: IMessage[] + chatHistory?: IMessage[] + overrideConfig?: ICommonObject + threadId?: string + action?: IAction uploadedFilesContent?: string -) => { +} + +const compileSeqAgentsGraph = async (params: SeqAgentsGraphParams) => { + const { + depthQueue, + chatflow, + reactFlowNodes, + reactFlowEdges, + componentNodes, + options, + prependHistoryMessages = [], + chatHistory = [], + overrideConfig = {}, + threadId, + action, + uploadedFilesContent + } = params + + let question = params.question + const appServer = getRunningExpressApp() let channels: ISeqAgentsState = { @@ -676,7 +718,7 @@ const compileSeqAgentsGraph = async ( } // Get state - const seqStateNode = reactflowNodes.find((node: IReactFlowNode) => node.data.name === 'seqState') + const seqStateNode = reactFlowNodes.find((node: IReactFlowNode) => node.data.name === 'seqState') if (seqStateNode) { channels = { ...seqStateNode.data.instance.node, @@ -690,13 +732,13 @@ const compileSeqAgentsGraph = async ( }) /*** Validate Graph ***/ - const startAgentNodes: IReactFlowNode[] = reactflowNodes.filter((node: IReactFlowNode) => node.data.name === 'seqStart') + const startAgentNodes: IReactFlowNode[] = reactFlowNodes.filter((node: IReactFlowNode) => node.data.name === 'seqStart') if (!startAgentNodes.length) throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, 'Start node not found') if (startAgentNodes.length > 1) throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, 'Graph should have only one start node') - const endAgentNodes: IReactFlowNode[] = reactflowNodes.filter((node: IReactFlowNode) => node.data.name === 'seqEnd') - const loopNodes: IReactFlowNode[] = reactflowNodes.filter((node: IReactFlowNode) => node.data.name === 'seqLoop') + const endAgentNodes: IReactFlowNode[] = reactFlowNodes.filter((node: IReactFlowNode) => node.data.name === 'seqEnd') + const loopNodes: IReactFlowNode[] = reactFlowNodes.filter((node: IReactFlowNode) => node.data.name === 'seqLoop') if (!endAgentNodes.length && !loopNodes.length) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, 'Graph should have at least one End/Loop node') } @@ -708,6 +750,7 @@ const compileSeqAgentsGraph = async ( let conditionalToolNodes: Record = {} let bindModel: Record = {} let interruptToolNodeNames = [] + const { nodeOverrides, variableOverrides, apiOverrideStatus } = getAPIOverrideConfig(chatflow) const initiateNode = async (node: IReactFlowNode) => { const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string @@ -715,15 +758,16 @@ const compileSeqAgentsGraph = async ( const newNodeInstance = new nodeModule.nodeClass() flowNodeData = cloneDeep(node.data) - if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) + if (overrideConfig && apiOverrideStatus) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig, nodeOverrides) flowNodeData = await resolveVariables( appServer.AppDataSource, flowNodeData, - reactflowNodes, + reactFlowNodes, question, chatHistory, overrideConfig, - uploadedFilesContent + uploadedFilesContent, + variableOverrides ) const seqAgentNode: ISeqAgentNode = await newNodeInstance.init(flowNodeData, question, options) @@ -740,16 +784,16 @@ const compileSeqAgentsGraph = async ( * 2.) With the interruptedRouteMapping object, avoid adding conditional edges to the Interrupted Agent for the nodes that are already interrupted by tools. It will be separately added from the function - agentInterruptToolFunc */ const processInterruptedRouteMapping = (conditionNodeId: string) => { - const conditionEdges = reactflowEdges.filter((edge) => edge.source === conditionNodeId) ?? [] + const conditionEdges = reactFlowEdges.filter((edge) => edge.source === conditionNodeId) ?? [] for (const conditionEdge of conditionEdges) { const nextNodeId = conditionEdge.target const conditionNodeOutputAnchorId = conditionEdge.sourceHandle - const nextNode = reactflowNodes.find((node) => node.id === nextNodeId) + const nextNode = reactFlowNodes.find((node) => node.id === nextNodeId) if (!nextNode) continue - const conditionNode = reactflowNodes.find((node) => node.id === conditionNodeId) + const conditionNode = reactFlowNodes.find((node) => node.id === conditionNodeId) if (!conditionNode) continue const outputAnchors = conditionNode?.data.outputAnchors @@ -780,13 +824,13 @@ const compileSeqAgentsGraph = async ( * } */ const prepareConditionalEdges = (nodeId: string, nodeInstance: ISeqAgentNode) => { - const conditionEdges = reactflowEdges.filter((edge) => edge.target === nodeId && edge.source.includes('seqCondition')) ?? [] + const conditionEdges = reactFlowEdges.filter((edge) => edge.target === nodeId && edge.source.includes('seqCondition')) ?? [] for (const conditionEdge of conditionEdges) { const conditionNodeId = conditionEdge.source const conditionNodeOutputAnchorId = conditionEdge.sourceHandle - const conditionNode = reactflowNodes.find((node) => node.id === conditionNodeId) + const conditionNode = reactFlowNodes.find((node) => node.id === conditionNodeId) const outputAnchors = conditionNode?.data.outputAnchors if (!outputAnchors || !outputAnchors.length || !outputAnchors[0].options) continue @@ -799,7 +843,10 @@ const compileSeqAgentsGraph = async ( if (Object.prototype.hasOwnProperty.call(conditionalEdges, conditionNodeId)) { conditionalEdges[conditionNodeId] = { ...conditionalEdges[conditionNodeId], - nodes: { ...conditionalEdges[conditionNodeId].nodes, [conditionOutputAnchorLabel]: nodeInstance.name } + nodes: { + ...conditionalEdges[conditionNodeId].nodes, + [conditionOutputAnchorLabel]: nodeInstance.name + } } } else { conditionalEdges[conditionNodeId] = { @@ -820,7 +867,10 @@ const compileSeqAgentsGraph = async ( if (Object.prototype.hasOwnProperty.call(conditionalToolNodes, predecessorAgent.id)) { const toolNodes = conditionalToolNodes[predecessorAgent.id].toolNodes toolNodes.push(toolNodeInstance) - conditionalToolNodes[predecessorAgent.id] = { source: predecessorAgent, toolNodes } + conditionalToolNodes[predecessorAgent.id] = { + source: predecessorAgent, + toolNodes + } } else { conditionalToolNodes[predecessorAgent.id] = { source: predecessorAgent, @@ -837,7 +887,7 @@ const compileSeqAgentsGraph = async ( /*** Start processing every Agent nodes ***/ for (const agentNodeId of getSortedDepthNodes(depthQueue)) { - const agentNode = reactflowNodes.find((node) => node.id === agentNodeId) + const agentNode = reactFlowNodes.find((node) => node.id === agentNodeId) if (!agentNode) continue const eligibleSeqNodes = ['seqAgent', 'seqEnd', 'seqLoop', 'seqToolNode', 'seqLLMNode'] @@ -859,8 +909,8 @@ const compileSeqAgentsGraph = async ( if (agentInstance.type === 'agent' && agentNode.data.inputs?.interrupt) { interruptToolNodeNames.push(agentInstance.agentInterruptToolNode.name) - const nextNodeId = reactflowEdges.find((edge) => edge.source === agentNode.id)?.target - const nextNode = reactflowNodes.find((node) => node.id === nextNodeId) + const nextNodeId = reactFlowEdges.find((edge) => edge.source === agentNode.id)?.target + const nextNode = reactFlowNodes.find((node) => node.id === nextNodeId) let nextNodeSeqAgentName = '' if (nextNodeId && nextNode) { @@ -950,11 +1000,11 @@ const compileSeqAgentsGraph = async ( /*** Add conditional edges to graph for condition nodes ***/ for (const conditionNodeId in conditionalEdges) { - const startConditionEdges = reactflowEdges.filter((edge) => edge.target === conditionNodeId) + const startConditionEdges = reactFlowEdges.filter((edge) => edge.target === conditionNodeId) if (!startConditionEdges.length) continue for (const startConditionEdge of startConditionEdges) { - const startConditionNode = reactflowNodes.find((node) => node.id === startConditionEdge.source) + const startConditionNode = reactFlowNodes.find((node) => node.id === startConditionEdge.source) if (!startConditionNode) continue seqGraph.addConditionalEdges( startConditionNode.data.instance.name, @@ -995,22 +1045,24 @@ const compileSeqAgentsGraph = async ( routeMessage ) } - /*** Add agentflow to pool ***/ ;(seqGraph as any).signal = options.signal appServer.chatflowPool.add( `${chatflow.id}_${options.chatId}`, seqGraph as any, - reactflowNodes.filter((node) => startAgentNodes.map((nd) => nd.id).includes(node.id)), + reactFlowNodes.filter((node) => startAgentNodes.map((nd) => nd.id).includes(node.id)), overrideConfig ) /*** Get memory ***/ - const startNode = reactflowNodes.find((node: IReactFlowNode) => node.data.name === 'seqStart') + const startNode = reactFlowNodes.find((node: IReactFlowNode) => node.data.name === 'seqStart') let memory = startNode?.data.instance?.checkpointMemory try { - const graph = seqGraph.compile({ checkpointer: memory, interruptBefore: interruptToolNodeNames as any }) + const graph = seqGraph.compile({ + checkpointer: memory, + interruptBefore: interruptToolNodeNames as any + }) const loggerHandler = new ConsoleCallbackHandler(logger) const callbacks = await additionalCallbacks(flowNodeData as any, options) @@ -1021,9 +1073,17 @@ const compileSeqAgentsGraph = async ( if (prependHistoryMessages.length === chatHistory.length) { for (const message of prependHistoryMessages) { if (message.role === 'apiMessage' || message.type === 'apiMessage') { - prependMessages.push(new AIMessage({ content: message.message || message.content || '' })) + prependMessages.push( + new AIMessage({ + content: message.message || message.content || '' + }) + ) } else if (message.role === 'userMessage' || message.type === 'userMessage') { - prependMessages.push(new HumanMessage({ content: message.message || message.content || '' })) + prependMessages.push( + new HumanMessage({ + content: message.message || message.content || '' + }) + ) } } } @@ -1047,7 +1107,10 @@ const compileSeqAgentsGraph = async ( }) } } - return await graph.stream(humanMsg, { callbacks: [loggerHandler, ...callbacks], configurable: config }) + return await graph.stream(humanMsg, { + callbacks: [loggerHandler, ...callbacks], + configurable: config + }) } catch (e) { logger.error('Error compile graph', e) throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error compile graph - ${getErrorMessage(e)}`) diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index b69c2ec7..1e61278b 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -42,7 +42,8 @@ import { isSameOverrideConfig, getEndingNodes, constructGraphs, - isSameChatId + isSameChatId, + getAPIOverrideConfig } from '../utils' import { validateChatflowAPIKey } from './validateKey' import { databaseEntities } from '.' @@ -346,15 +347,19 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) + /*** Get API Config ***/ + const { nodeOverrides, variableOverrides, apiOverrideStatus } = getAPIOverrideConfig(chatflow) + logger.debug(`[server]: Start building chatflow ${chatflowid}`) + /*** BFS to traverse from Starting Nodes to Ending Node ***/ const reactFlowNodes = await buildFlow({ startingNodeIds, reactFlowNodes: nodes, reactFlowEdges: edges, + apiMessageId, graph, depthQueue, - apiMessageId, componentNodes: appServer.nodesPool.componentNodes, question: incomingInput.question, uploadedFilesContent, @@ -364,6 +369,9 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals chatflowid, appDataSource: appServer.AppDataSource, overrideConfig: incomingInput?.overrideConfig, + apiOverrideStatus, + nodeOverrides, + variableOverrides, cachePool: appServer.cachePool, isUpsert: false, uploads: incomingInput.uploads, @@ -378,8 +386,9 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Node not found`) } - if (incomingInput.overrideConfig) { - nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig) + // Only override the config if its status is true + if (incomingInput.overrideConfig && apiOverrideStatus) { + nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig, nodeOverrides) } const flowData: ICommonObject = { @@ -398,7 +407,8 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals incomingInput.question, chatHistory, flowData, - uploadedFilesContent + uploadedFilesContent, + variableOverrides ) nodeToExecuteData = reactFlowNodeData diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index cc416366..4fe1ec71 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -2,6 +2,7 @@ import path from 'path' import fs from 'fs' import logger from './logger' import { + IChatFlow, IComponentCredentials, IComponentNodes, ICredentialDataDecrypted, @@ -434,6 +435,9 @@ type BuildFlowParams = { apiMessageId: string appDataSource: DataSource overrideConfig?: ICommonObject + apiOverrideStatus?: boolean + nodeOverrides?: ICommonObject + variableOverrides?: ICommonObject[] cachePool?: CachePool isUpsert?: boolean stopNodeId?: string @@ -462,6 +466,9 @@ export const buildFlow = async ({ chatflowid, appDataSource, overrideConfig, + apiOverrideStatus = false, + nodeOverrides = {}, + variableOverrides = [], cachePool, isUpsert, stopNodeId, @@ -509,7 +516,11 @@ export const buildFlow = async ({ const newNodeInstance = new nodeModule.nodeClass() let flowNodeData = cloneDeep(reactFlowNode.data) - if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) + + // Only override the config if its status is true + if (overrideConfig && apiOverrideStatus) { + flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig, nodeOverrides) + } if (isUpsert) upsertHistory['flowData'] = saveUpsertFlowData(flowNodeData, upsertHistory) @@ -520,7 +531,8 @@ export const buildFlow = async ({ question, chatHistory, flowData, - uploadedFilesContent + uploadedFilesContent, + variableOverrides ) if (isUpsert && stopNodeId && nodeId === stopNodeId) { @@ -713,13 +725,19 @@ export const clearSessionMemory = async ( } } -const getGlobalVariable = async (appDataSource: DataSource, overrideConfig?: ICommonObject) => { +const getGlobalVariable = async (appDataSource: DataSource, overrideConfig?: ICommonObject, variableOverrides?: ICommonObject[]) => { const variables = await appDataSource.getRepository(Variable).find() // override variables defined in overrideConfig // nodeData.inputs.vars is an Object, check each property and override the variable - if (overrideConfig?.vars) { + if (overrideConfig?.vars && variableOverrides) { for (const propertyName of Object.getOwnPropertyNames(overrideConfig.vars)) { + // Check if this variable is enabled for override + const override = variableOverrides.find((v) => v.name === propertyName) + if (!override?.enabled) { + continue // Skip this variable if it's not enabled for override + } + const foundVar = variables.find((v) => v.name === propertyName) if (foundVar) { // even if the variable was defined as runtime, we override it with static value @@ -776,7 +794,8 @@ export const getVariableValue = async ( chatHistory: IMessage[], isAcceptVariable = false, flowData?: ICommonObject, - uploadedFilesContent?: string + uploadedFilesContent?: string, + variableOverrides: ICommonObject[] = [] ) => { const isObject = typeof paramValue === 'object' const initialValue = (isObject ? JSON.stringify(paramValue) : paramValue) ?? '' @@ -818,7 +837,7 @@ export const getVariableValue = async ( } if (variableFullPath.startsWith('$vars.')) { - const vars = await getGlobalVariable(appDataSource, flowData) + const vars = await getGlobalVariable(appDataSource, flowData, variableOverrides) const variableValue = get(vars, variableFullPath.replace('$vars.', '')) if (variableValue) { variableDict[`{{${variableFullPath}}}`] = variableValue @@ -927,7 +946,8 @@ export const resolveVariables = async ( question: string, chatHistory: IMessage[], flowData?: ICommonObject, - uploadedFilesContent?: string + uploadedFilesContent?: string, + variableOverrides: ICommonObject[] = [] ): Promise => { let flowNodeData = cloneDeep(reactFlowNodeData) const types = 'inputs' @@ -946,7 +966,8 @@ export const resolveVariables = async ( chatHistory, undefined, flowData, - uploadedFilesContent + uploadedFilesContent, + variableOverrides ) resolvedInstances.push(resolvedInstance) } @@ -961,7 +982,8 @@ export const resolveVariables = async ( chatHistory, isAcceptVariable, flowData, - uploadedFilesContent + uploadedFilesContent, + variableOverrides ) paramsObj[key] = resolvedInstance } @@ -978,18 +1000,28 @@ export const resolveVariables = async ( * Loop through each inputs and replace their value with override config values * @param {INodeData} flowNodeData * @param {ICommonObject} overrideConfig + * @param {ICommonObject} nodeOverrides * @returns {INodeData} */ -export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: ICommonObject) => { +export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: ICommonObject, nodeOverrides: ICommonObject) => { const types = 'inputs' + const isParameterEnabled = (nodeType: string, paramName: string): boolean => { + if (!nodeOverrides[nodeType]) return false + const parameter = nodeOverrides[nodeType].find((param: any) => param.name === paramName) + return parameter?.enabled ?? false + } + const getParamValues = (inputsObj: ICommonObject) => { for (const config in overrideConfig) { // If overrideConfig[key] is object if (overrideConfig[config] && typeof overrideConfig[config] === 'object') { const nodeIds = Object.keys(overrideConfig[config]) if (nodeIds.includes(flowNodeData.id)) { - inputsObj[config] = overrideConfig[config][flowNodeData.id] + // Check if this parameter is enabled for this node type + if (isParameterEnabled(flowNodeData.label, config)) { + inputsObj[config] = overrideConfig[config][flowNodeData.id] + } continue } else if (nodeIds.some((nodeId) => nodeId.includes(flowNodeData.name))) { /* @@ -1001,6 +1033,11 @@ export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: } } + // Only proceed if the parameter is enabled for this node type + if (!isParameterEnabled(flowNodeData.label, config)) { + continue + } + let paramValue = inputsObj[config] const overrideConfigValue = overrideConfig[config] if (overrideConfigValue) { @@ -1600,3 +1637,16 @@ export const aMonthAgo = () => { date.setMonth(new Date().getMonth() - 1) return date } + +export const getAPIOverrideConfig = (chatflow: IChatFlow) => { + try { + const apiConfig = chatflow.apiConfig ? JSON.parse(chatflow.apiConfig) : {} + const nodeOverrides = apiConfig.overrideConfig && apiConfig.overrideConfig.nodes ? apiConfig.overrideConfig.nodes : {} + const variableOverrides = apiConfig.overrideConfig && apiConfig.overrideConfig.variables ? apiConfig.overrideConfig.variables : [] + const apiOverrideStatus = apiConfig.overrideConfig && apiConfig.overrideConfig.status ? apiConfig.overrideConfig.status : false + + return { nodeOverrides, variableOverrides, apiOverrideStatus } + } catch (error) { + return { nodeOverrides: {}, variableOverrides: [], apiOverrideStatus: false } + } +} diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts index 3342c60a..280abbd6 100644 --- a/packages/server/src/utils/upsertVector.ts +++ b/packages/server/src/utils/upsertVector.ts @@ -159,13 +159,13 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => startingNodeIds, reactFlowNodes: nodes, reactFlowEdges: edges, + apiMessageId, graph: filteredGraph, depthQueue, componentNodes: appServer.nodesPool.componentNodes, question: incomingInput.question, chatHistory, chatId, - apiMessageId, sessionId: sessionId ?? '', chatflowid, appDataSource: appServer.AppDataSource, diff --git a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx index 48d6f370..081f6c91 100644 --- a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx @@ -4,8 +4,7 @@ import { createPortal } from 'react-dom' import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/material' import { tabsClasses } from '@mui/material/Tabs' import SpeechToText from '@/ui-component/extended/SpeechToText' -import RateLimit from '@/ui-component/extended/RateLimit' -import AllowedDomains from '@/ui-component/extended/AllowedDomains' +import Security from '@/ui-component/extended/Security' import ChatFeedback from '@/ui-component/extended/ChatFeedback' import AnalyseFlow from '@/ui-component/extended/AnalyseFlow' import StarterPrompts from '@/ui-component/extended/StarterPrompts' @@ -15,8 +14,8 @@ import FileUpload from '@/ui-component/extended/FileUpload' const CHATFLOW_CONFIGURATION_TABS = [ { - label: 'Rate Limiting', - id: 'rateLimiting' + label: 'Security', + id: 'security' }, { label: 'Starter Prompts', @@ -34,10 +33,6 @@ const CHATFLOW_CONFIGURATION_TABS = [ label: 'Chat Feedback', id: 'chatFeedback' }, - { - label: 'Allowed Domains', - id: 'allowedDomains' - }, { label: 'Analyse Chatflow', id: 'analyseChatflow' @@ -94,8 +89,8 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description' > - -
{dialogProps.title}
+ + {dialogProps.title} { > {CHATFLOW_CONFIGURATION_TABS.map((item, index) => ( { {CHATFLOW_CONFIGURATION_TABS.map((item, index) => ( - {item.id === 'rateLimiting' && } + {item.id === 'security' && } {item.id === 'conversationStarters' ? : null} {item.id === 'followUpPrompts' ? : null} {item.id === 'speechToText' ? : null} {item.id === 'chatFeedback' ? : null} - {item.id === 'allowedDomains' ? : null} {item.id === 'analyseChatflow' ? : null} {item.id === 'leads' ? : null} {item.id === 'fileUpload' ? : null} diff --git a/packages/ui/src/ui-component/extended/AllowedDomains.jsx b/packages/ui/src/ui-component/extended/AllowedDomains.jsx index 8c6e70dc..b13d660d 100644 --- a/packages/ui/src/ui-component/extended/AllowedDomains.jsx +++ b/packages/ui/src/ui-component/extended/AllowedDomains.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' // material-ui -import { Button, IconButton, OutlinedInput, Box, List, InputAdornment, Typography } from '@mui/material' +import { Button, IconButton, OutlinedInput, Box, InputAdornment, Stack, Typography } from '@mui/material' import { IconX, IconTrash, IconPlus } from '@tabler/icons-react' // Project import @@ -118,23 +118,17 @@ const AllowedDomains = ({ dialogProps }) => { }, [dialogProps]) return ( - <> - - - - Allowed Domains - - - - + + + Allowed Domains + + + + + Domains {inputFields.map((origin, index) => { return (
@@ -176,11 +170,9 @@ const AllowedDomains = ({ dialogProps }) => {
) })} -
-
- -
- + + + Error Message { setErrorMessage(e.target.value) }} /> -
-
+ + Save - + ) } diff --git a/packages/ui/src/ui-component/extended/FileUpload.jsx b/packages/ui/src/ui-component/extended/FileUpload.jsx index b1ccb08c..d6fc8f02 100644 --- a/packages/ui/src/ui-component/extended/FileUpload.jsx +++ b/packages/ui/src/ui-component/extended/FileUpload.jsx @@ -2,14 +2,14 @@ import { useDispatch } from 'react-redux' import { useState, useEffect } from 'react' import PropTypes from 'prop-types' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' +import parser from 'html-react-parser' // material-ui -import { Button, Box, Typography } from '@mui/material' -import { IconX } from '@tabler/icons-react' +import { Button, Box } from '@mui/material' +import { IconX, IconBulb } from '@tabler/icons-react' // Project import import { StyledButton } from '@/ui-component/button/StyledButton' -import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' import { SwitchInput } from '@/ui-component/switch/Switch' // store @@ -18,7 +18,9 @@ import useNotifier from '@/utils/useNotifier' // API import chatflowsApi from '@/api/chatflows' -const message = `Allow files to be uploaded from the chat. Uploaded files will be parsed as string and sent to LLM. If File Upload is enabled on Vector Store as well, this will override and takes precedence.` +const message = `Uploaded files will be parsed as strings and sent to the LLM. If file upload is enabled on the Vector Store as well, this will override and take precedence. +
+Refer docs for more details.` const FileUpload = ({ dialogProps }) => { const dispatch = useDispatch() @@ -99,15 +101,41 @@ const FileUpload = ({ dialogProps }) => { return ( <> - -
- - Enable Full File Upload - - + +
+
+ + {parser(message)} +
- +
+ {/* TODO: Allow selection of allowed file types*/} Save diff --git a/packages/ui/src/ui-component/extended/OverrideConfig.jsx b/packages/ui/src/ui-component/extended/OverrideConfig.jsx new file mode 100644 index 00000000..ca352c91 --- /dev/null +++ b/packages/ui/src/ui-component/extended/OverrideConfig.jsx @@ -0,0 +1,430 @@ +import PropTypes from 'prop-types' +import { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + Accordion, + AccordionDetails, + AccordionSummary, + Button, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + Card +} from '@mui/material' +import { useTheme } from '@mui/material/styles' + +// Project import +import { StyledButton } from '@/ui-component/button/StyledButton' +import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' +import { SwitchInput } from '@/ui-component/switch/Switch' +import useNotifier from '@/utils/useNotifier' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, SET_CHATFLOW } from '@/store/actions' + +// Icons +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { IconX, IconBox, IconVariable } from '@tabler/icons-react' + +// API +import useApi from '@/hooks/useApi' +import chatflowsApi from '@/api/chatflows' +import configApi from '@/api/config' +import variablesApi from '@/api/variables' + +// utils + +const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => { + const handleChange = (enabled, row) => { + onToggle(row, enabled) + } + + return ( + + + + + {columns.map((col, index) => ( + {col.charAt(0).toUpperCase() + col.slice(1)} + ))} + + + + {rows.map((row, index) => ( + + {Object.keys(row).map((key, index) => { + if (key !== 'id') { + return ( + + {key === 'enabled' ? ( + handleChange(enabled, row)} value={row.enabled} /> + ) : ( + row[key] + )} + + ) + } + })} + + ))} + +
+
+ ) +} + +OverrideConfigTable.propTypes = { + rows: PropTypes.array, + columns: PropTypes.array, + sx: PropTypes.object, + onToggle: PropTypes.func +} + +const OverrideConfig = ({ dialogProps }) => { + const dispatch = useDispatch() + const chatflow = useSelector((state) => state.canvas.chatflow) + const chatflowid = chatflow.id + const apiConfig = chatflow.apiConfig ? JSON.parse(chatflow.apiConfig) : {} + + useNotifier() + const theme = useTheme() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [nodeConfig, setNodeConfig] = useState(null) + const [nodeConfigExpanded, setNodeConfigExpanded] = useState({}) + const [overrideConfigStatus, setOverrideConfigStatus] = useState( + apiConfig?.overrideConfig?.status !== undefined ? apiConfig.overrideConfig.status : false + ) + const [nodeOverrides, setNodeOverrides] = useState(apiConfig?.overrideConfig?.nodes !== undefined ? apiConfig.overrideConfig.nodes : {}) + const [variableOverrides, setVariableOverrides] = useState( + apiConfig?.overrideConfig?.variables !== undefined ? apiConfig.overrideConfig.variables : [] + ) + + const getConfigApi = useApi(configApi.getConfig) + const getAllVariablesApi = useApi(variablesApi.getAllVariables) + + const handleAccordionChange = (nodeLabel) => (event, isExpanded) => { + const accordianNodes = { ...nodeConfigExpanded } + accordianNodes[nodeLabel] = isExpanded + setNodeConfigExpanded(accordianNodes) + } + + const formatObj = () => { + const obj = { + overrideConfig: { status: overrideConfigStatus } + } + + if (overrideConfigStatus) { + obj.overrideConfig = { + ...obj.overrideConfig, + nodes: nodeOverrides, + variables: variableOverrides + } + } + + return obj + } + + const onNodeOverrideToggle = (node, property, status) => { + setNodeOverrides((prev) => { + const newConfig = { ...prev } + newConfig[node] = newConfig[node].map((item) => { + if (item.name === property) { + item.enabled = status + } + return item + }) + return newConfig + }) + } + + const onVariableOverrideToggle = (id, status) => { + setVariableOverrides((prev) => { + return prev.map((item) => { + if (item.id === id) { + item.enabled = status + } + return item + }) + }) + } + + const groupByNodeLabel = (nodes) => { + const result = {} + const newNodeOverrides = {} + const seenNodes = new Set() + + nodes.forEach((item) => { + const { node, nodeId, label, name, type } = item + seenNodes.add(node) + + if (!result[node]) { + result[node] = { + nodeIds: [], + params: [] + } + } + + if (!newNodeOverrides[node]) { + // If overrideConfigStatus is true, copy existing config for this node + newNodeOverrides[node] = overrideConfigStatus ? [...(nodeOverrides[node] || [])] : [] + } + + if (!result[node].nodeIds.includes(nodeId)) result[node].nodeIds.push(nodeId) + + const param = { label, name, type } + + if (!result[node].params.some((existingParam) => JSON.stringify(existingParam) === JSON.stringify(param))) { + result[node].params.push(param) + const paramExists = newNodeOverrides[node].some( + (existingParam) => existingParam.label === label && existingParam.name === name && existingParam.type === type + ) + if (!paramExists) { + newNodeOverrides[node].push({ ...param, enabled: false }) + } + } + }) + + // Sort the nodeIds array + for (const node in result) { + result[node].nodeIds.sort() + } + setNodeConfig(result) + + if (!overrideConfigStatus) { + setNodeOverrides(newNodeOverrides) + } else { + const updatedNodeOverrides = { ...nodeOverrides } + + Object.keys(updatedNodeOverrides).forEach((node) => { + if (!seenNodes.has(node)) { + delete updatedNodeOverrides[node] + } + }) + + seenNodes.forEach((node) => { + if (!updatedNodeOverrides[node]) { + updatedNodeOverrides[node] = newNodeOverrides[node] + } + }) + + setNodeOverrides(updatedNodeOverrides) + } + } + + const groupByVariableLabel = (variables) => { + const newVariables = [] + const seenVariables = new Set() + + variables.forEach((item) => { + const { id, name, type } = item + seenVariables.add(id) + + const param = { id, name, type } + const existingVariable = variableOverrides?.find((existingParam) => existingParam.id === id) + + if (existingVariable) { + if (!newVariables.some((existingVariable) => existingVariable.id === id)) { + newVariables.push({ ...existingVariable }) + } + } else { + if (!newVariables.some((existingVariable) => existingVariable.id === id)) { + newVariables.push({ ...param, enabled: false }) + } + } + }) + + if (variableOverrides) { + variableOverrides.forEach((existingVariable) => { + if (!seenVariables.has(existingVariable.id)) { + const index = newVariables.findIndex((newVariable) => newVariable.id === existingVariable.id) + if (index !== -1) { + newVariables.splice(index, 1) + } + } + }) + } + + setVariableOverrides(newVariables) + } + + const onOverrideConfigSave = async () => { + try { + const saveResp = await chatflowsApi.updateChatflow(chatflowid, { + apiConfig: JSON.stringify(formatObj()) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Override Configuration Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + enqueueSnackbar({ + message: `Failed to save Override Configuration: ${ + typeof error.response.data === 'object' ? error.response.data.message : error.response.data + }`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow) { + getConfigApi.request(dialogProps.chatflow.id) + getAllVariablesApi.request() + } + + return () => {} + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dialogProps]) + + useEffect(() => { + if (getConfigApi.data) { + groupByNodeLabel(getConfigApi.data) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getConfigApi.data]) + + useEffect(() => { + if (getAllVariablesApi.data) { + groupByVariableLabel(getAllVariablesApi.data) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllVariablesApi.data]) + + return ( + + + Override Configuration + documentation for more information.' + } + /> + + + + {overrideConfigStatus && ( + <> + {nodeOverrides && nodeConfig && ( + + + + Nodes + + + {Object.keys(nodeOverrides) + .sort() + .map((nodeLabel) => ( + + } + aria-controls={`nodes-accordian-${nodeLabel}`} + id={`nodes-accordian-header-${nodeLabel}`} + > + + {nodeLabel} + {nodeConfig[nodeLabel].nodeIds.length > 0 && + nodeConfig[nodeLabel].nodeIds.map((nodeId, index) => ( +
+ + {nodeId} + +
+ ))} +
+
+ + 0 + ? Object.keys(nodeOverrides[nodeLabel][0]) + : [] + } + onToggle={(property, status) => + onNodeOverrideToggle(nodeLabel, property.name, status) + } + /> + +
+ ))} +
+
+ )} + {variableOverrides && ( + + + + Variables + + onVariableOverrideToggle(property.id, status)} + /> + + )} + + )} +
+ + Save + +
+ ) +} + +OverrideConfig.propTypes = { + dialogProps: PropTypes.object +} + +export default OverrideConfig diff --git a/packages/ui/src/ui-component/extended/RateLimit.jsx b/packages/ui/src/ui-component/extended/RateLimit.jsx index 7591083e..32a49b9e 100644 --- a/packages/ui/src/ui-component/extended/RateLimit.jsx +++ b/packages/ui/src/ui-component/extended/RateLimit.jsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' import PropTypes from 'prop-types' -import { Box, Typography, Button, OutlinedInput } from '@mui/material' +import { Typography, Button, OutlinedInput, Stack } from '@mui/material' // Project import import { StyledButton } from '@/ui-component/button/StyledButton' @@ -126,55 +126,49 @@ const RateLimit = () => { const textField = (message, fieldName, fieldLabel, fieldType = 'string', placeholder = '') => { return ( - -
- {fieldLabel} - { - onTextChanged(e.target.value, fieldName) - }} - /> -
-
+ + {fieldLabel} + { + onTextChanged(e.target.value, fieldName) + }} + /> + ) } return ( - <> - {/*Rate Limit*/} - + + Rate Limit{' '} Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' } /> - - {rateLimitStatus && ( - <> - {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number', '5')} - {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number', '60')} - {textField(limitMsg, 'limitMsg', 'Limit Message', 'string', 'You have reached the quota')} - - )} - onSave()} - > - Save Changes + + + {rateLimitStatus && ( + + {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number', '5')} + {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number', '60')} + {textField(limitMsg, 'limitMsg', 'Limit Message', 'string', 'You have reached the quota')} + + )} + + onSave()} sx={{ width: 'auto' }}> + Save - + ) } diff --git a/packages/ui/src/ui-component/extended/Security.jsx b/packages/ui/src/ui-component/extended/Security.jsx new file mode 100644 index 00000000..b46847fc --- /dev/null +++ b/packages/ui/src/ui-component/extended/Security.jsx @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types' +import { Divider, Stack } from '@mui/material' +import { useTheme } from '@mui/material/styles' + +// Project import +import RateLimit from '@/ui-component/extended/RateLimit' +import AllowedDomains from '@/ui-component/extended/AllowedDomains' +import OverrideConfig from './OverrideConfig' + +const Security = ({ dialogProps }) => { + const theme = useTheme() + + return ( + } spacing={4}> + + + + + ) +} + +Security.propTypes = { + dialogProps: PropTypes.object +} + +export default Security diff --git a/packages/ui/src/ui-component/extended/SpeechToText.jsx b/packages/ui/src/ui-component/extended/SpeechToText.jsx index 8c55152d..59f9964b 100644 --- a/packages/ui/src/ui-component/extended/SpeechToText.jsx +++ b/packages/ui/src/ui-component/extended/SpeechToText.jsx @@ -248,9 +248,7 @@ const SpeechToText = ({ dialogProps }) => { return ( <> - - Providers - + Providers