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 <hzj94@hotmail.com>
This commit is contained in:
Ilango
2024-11-13 23:51:59 +05:30
committed by GitHub
parent 3b72017a10
commit 537aa51ef8
16 changed files with 1053 additions and 289 deletions
@@ -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
+182 -119
View File
@@ -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<string, {label: string, nodeName: string }>} 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<string, { label: string; nodeName: string }>,
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<string, { label: string; nodeName: string }>
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<string, { source: ISeqAgentNode; toolNodes: ISeqAgentNode[] }> = {}
let bindModel: Record<string, any> = {}
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)}`)
+15 -5
View File
@@ -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
+61 -11
View File
@@ -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<INodeData> => {
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 }
}
}
+1 -1
View File
@@ -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,
@@ -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'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}>{dialogProps.title}</div>
<DialogTitle sx={{ fontSize: '1.25rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent>
<Tabs
@@ -115,7 +110,13 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
>
{CHATFLOW_CONFIGURATION_TABS.map((item, index) => (
<Tab
sx={{ minHeight: '40px', height: '40px', textAlign: 'left', display: 'flex', alignItems: 'start', mb: 1 }}
sx={{
minHeight: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
mb: 1
}}
key={index}
label={item.label}
{...a11yProps(index)}
@@ -124,12 +125,11 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
</Tabs>
{CHATFLOW_CONFIGURATION_TABS.map((item, index) => (
<TabPanel key={index} value={tabValue} index={index}>
{item.id === 'rateLimiting' && <RateLimit />}
{item.id === 'security' && <Security dialogProps={dialogProps} />}
{item.id === 'conversationStarters' ? <StarterPrompts dialogProps={dialogProps} /> : null}
{item.id === 'followUpPrompts' ? <FollowUpPrompts dialogProps={dialogProps} /> : null}
{item.id === 'speechToText' ? <SpeechToText dialogProps={dialogProps} /> : null}
{item.id === 'chatFeedback' ? <ChatFeedback dialogProps={dialogProps} /> : null}
{item.id === 'allowedDomains' ? <AllowedDomains dialogProps={dialogProps} /> : null}
{item.id === 'analyseChatflow' ? <AnalyseFlow dialogProps={dialogProps} /> : null}
{item.id === 'leads' ? <Leads dialogProps={dialogProps} /> : null}
{item.id === 'fileUpload' ? <FileUpload dialogProps={dialogProps} /> : null}
@@ -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 (
<>
<Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column'
}}
>
<Typography sx={{ mb: 1 }}>
Allowed Domains
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={'Your chatbot will only work when used from the following domains.'}
/>
</Typography>
</Box>
<List>
<Stack direction='column' spacing={2} sx={{ alignItems: 'start' }}>
<Typography variant='h3'>
Allowed Domains
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={'Your chatbot will only work when used from the following domains.'}
/>
</Typography>
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
<Stack direction='column' spacing={2}>
<Typography>Domains</Typography>
{inputFields.map((origin, index) => {
return (
<div key={index} style={{ display: 'flex', width: '100%' }}>
@@ -176,11 +170,9 @@ const AllowedDomains = ({ dialogProps }) => {
</div>
)
})}
</List>
</Box>
<Box sx={{ pt: 2, pb: 2 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<Typography sx={{ mb: 1 }}>
</Stack>
<Stack direction='column' spacing={1}>
<Typography>
Error Message
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
@@ -198,12 +190,12 @@ const AllowedDomains = ({ dialogProps }) => {
setErrorMessage(e.target.value)
}}
/>
</div>
</Box>
</Stack>
</Stack>
<StyledButton variant='contained' onClick={onSave}>
Save
</StyledButton>
</>
</Stack>
)
}
@@ -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.
<br />
Refer <a href='https://docs.flowiseai.com/using-flowise/uploads#files' target='_blank'>docs</a> for more details.`
const FileUpload = ({ dialogProps }) => {
const dispatch = useDispatch()
@@ -99,15 +101,41 @@ const FileUpload = ({ dialogProps }) => {
return (
<>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', mb: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Enable Full File Upload
<TooltipWithParser style={{ marginLeft: 10 }} title={message} />
</Typography>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
justifyContent: 'start',
gap: 3,
mb: 2
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
borderRadius: 10,
background: '#d8f3dc',
width: '100%',
padding: 10
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<IconBulb size={30} color='#2d6a4f' />
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>{parser(message)}</span>
</div>
</div>
<SwitchInput onChange={handleChange} value={fullFileUpload} />
<SwitchInput label='Enable Full File Upload' onChange={handleChange} value={fullFileUpload} />
</Box>
{/* TODO: Allow selection of allowed file types*/}
<StyledButton style={{ marginBottom: 10, marginTop: 10 }} variant='contained' onClick={onSave}>
Save
</StyledButton>
@@ -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 (
<TableContainer component={Paper}>
<Table size='small' sx={{ minWidth: 650, ...sx }} 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) => {
if (key !== 'id') {
return (
<TableCell key={index}>
{key === 'enabled' ? (
<SwitchInput onChange={(enabled) => handleChange(enabled, row)} value={row.enabled} />
) : (
row[key]
)}
</TableCell>
)
}
})}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
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) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
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 (
<Stack direction='column' spacing={2} sx={{ alignItems: 'start' }}>
<Typography variant='h3'>
Override Configuration
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={
'Enable or disable which properties of the flow configuration can be overridden. Refer to the <a href="https://docs.flowiseai.com/using-flowise/api#override-config" target="_blank">documentation</a> for more information.'
}
/>
</Typography>
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
<SwitchInput label='Enable Override Configuration' onChange={setOverrideConfigStatus} value={overrideConfigStatus} />
{overrideConfigStatus && (
<>
{nodeOverrides && nodeConfig && (
<Card sx={{ borderColor: theme.palette.primary[200] + 75, p: 2 }} variant='outlined'>
<Stack sx={{ mt: 1, mb: 2, ml: 1, alignItems: 'center' }} direction='row' spacing={2}>
<IconBox />
<Typography variant='h4'>Nodes</Typography>
</Stack>
<Stack direction='column'>
{Object.keys(nodeOverrides)
.sort()
.map((nodeLabel) => (
<Accordion
expanded={nodeConfigExpanded[nodeLabel] || false}
onChange={handleAccordionChange(nodeLabel)}
key={nodeLabel}
disableGutters
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${nodeLabel}`}
id={`nodes-accordian-header-${nodeLabel}`}
>
<Stack flexDirection='row' sx={{ gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant='h5'>{nodeLabel}</Typography>
{nodeConfig[nodeLabel].nodeIds.length > 0 &&
nodeConfig[nodeLabel].nodeIds.map((nodeId, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
width: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
padding: 5,
paddingLeft: 10,
paddingRight: 10
}}
>
<span
style={{
color: 'rgb(116,66,16)',
fontSize: '0.825rem'
}}
>
{nodeId}
</span>
</div>
))}
</Stack>
</AccordionSummary>
<AccordionDetails sx={{ p: 0 }}>
<OverrideConfigTable
rows={nodeOverrides[nodeLabel]}
columns={
nodeOverrides[nodeLabel].length > 0
? Object.keys(nodeOverrides[nodeLabel][0])
: []
}
onToggle={(property, status) =>
onNodeOverrideToggle(nodeLabel, property.name, status)
}
/>
</AccordionDetails>
</Accordion>
))}
</Stack>
</Card>
)}
{variableOverrides && (
<Card sx={{ borderColor: theme.palette.primary[200] + 75, p: 2 }} variant='outlined'>
<Stack sx={{ mt: 1, mb: 2, ml: 1, alignItems: 'center' }} direction='row' spacing={2}>
<IconVariable />
<Typography variant='h4'>Variables</Typography>
</Stack>
<OverrideConfigTable
rows={variableOverrides}
columns={['name', 'type', 'enabled']}
onToggle={(property, status) => onVariableOverrideToggle(property.id, status)}
/>
</Card>
)}
</>
)}
</Stack>
<StyledButton variant='contained' onClick={onOverrideConfigSave}>
Save
</StyledButton>
</Stack>
)
}
OverrideConfig.propTypes = {
dialogProps: PropTypes.object
}
export default OverrideConfig
@@ -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 (
<Box sx={{ pt: 2, pb: 2 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<Typography sx={{ mb: 1 }}>{fieldLabel}</Typography>
<OutlinedInput
id={fieldName}
type={fieldType}
fullWidth
value={message}
placeholder={placeholder}
name={fieldName}
size='small'
onChange={(e) => {
onTextChanged(e.target.value, fieldName)
}}
/>
</div>
</Box>
<Stack direction='column' spacing={1}>
<Typography>{fieldLabel}</Typography>
<OutlinedInput
id={fieldName}
type={fieldType}
fullWidth
value={message}
placeholder={placeholder}
name={fieldName}
size='small'
onChange={(e) => {
onTextChanged(e.target.value, fieldName)
}}
/>
</Stack>
)
}
return (
<>
{/*Rate Limit*/}
<Typography variant='h4' sx={{ mb: 1 }}>
<Stack direction='column' spacing={2} sx={{ alignItems: 'start' }}>
<Typography variant='h3'>
Rate Limit{' '}
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
style={{ marginLeft: 10 }}
title={
'Visit <a target="_blank" href="https://docs.flowiseai.com/rate-limit">Rate Limit Setup Guide</a> to set up Rate Limit correctly in your hosting environment.'
}
/>
</Typography>
<SwitchInput label='Enable Rate Limit' onChange={handleChange} value={rateLimitStatus} />
{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')}
</>
)}
<StyledButton
disabled={checkDisabled()}
style={{ marginBottom: 10, marginTop: 10 }}
variant='contained'
onClick={() => onSave()}
>
Save Changes
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
<SwitchInput label='Enable Rate Limit' onChange={handleChange} value={rateLimitStatus} />
{rateLimitStatus && (
<Stack direction='column' spacing={2} sx={{ width: '100%' }}>
{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')}
</Stack>
)}
</Stack>
<StyledButton disabled={checkDisabled()} variant='contained' onClick={() => onSave()} sx={{ width: 'auto' }}>
Save
</StyledButton>
</>
</Stack>
)
}
@@ -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 (
<Stack direction='column' divider={<Divider sx={{ my: 0.5, borderColor: theme.palette.grey[900] + 25 }} />} spacing={4}>
<RateLimit />
<AllowedDomains dialogProps={dialogProps} />
<OverrideConfig dialogProps={dialogProps} />
</Stack>
)
}
Security.propTypes = {
dialogProps: PropTypes.object
}
export default Security
@@ -248,9 +248,7 @@ const SpeechToText = ({ dialogProps }) => {
return (
<>
<Box fullWidth sx={{ mb: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant='h4' sx={{ mb: 1 }}>
Providers
</Typography>
<Typography>Providers</Typography>
<FormControl fullWidth>
<Select size='small' value={selectedProvider} onChange={handleProviderChange}>
<MenuItem value='none'>None</MenuItem>
+34 -5
View File
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper } from '@mui/material'
import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper, Chip } from '@mui/material'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
export const TableViewOnly = ({ columns, rows, sx }) => {
return (
@@ -9,16 +10,44 @@ export const TableViewOnly = ({ columns, rows, sx }) => {
<TableHead>
<TableRow>
{columns.map((col, index) => (
<TableCell key={index}>{col.charAt(0).toUpperCase() + col.slice(1)}</TableCell>
<TableCell key={index}>
{col === 'enabled' ? (
<>
Override
<TooltipWithParser
style={{ mb: 1, mt: 2, marginLeft: 10 }}
title={
'If enabled, this variable can be overridden in API calls and embeds. If disabled, any overrides will be ignored. To change this, go to Security settings in Chatflow Configuration.'
}
/>
</>
) : (
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>
))}
{Object.keys(row).map((key, index) => {
if (key !== 'id') {
return (
<TableCell key={index}>
{key === 'enabled' ? (
row[key] ? (
<Chip label='Enabled' color='primary' />
) : (
<Chip label='Disabled' />
)
) : (
row[key]
)}
</TableCell>
)
}
})}
</TableRow>
))}
</TableBody>
@@ -449,7 +449,7 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlo
onCancel={() => setFlowDialogOpen(false)}
onConfirm={onConfirmSaveName}
/>
<APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />
{apiDialogOpen && <APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />}
<ViewMessagesDialog
show={viewMessagesDialogOpen}
dialogProps={viewMessagesDialogProps}
+179 -51
View File
@@ -1,7 +1,7 @@
import { createPortal } from 'react-dom'
import { useNavigate } from 'react-router-dom'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import {
@@ -15,10 +15,12 @@ import {
AccordionSummary,
AccordionDetails,
Typography,
Stack
Stack,
Card
} from '@mui/material'
import { CopyBlock, atomOneDark } from 'react-code-blocks'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useTheme } from '@mui/material/styles'
// Project import
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
@@ -36,12 +38,13 @@ import cURLSVG from '@/assets/images/cURL.svg'
import EmbedSVG from '@/assets/images/embed.svg'
import ShareChatbotSVG from '@/assets/images/sharing.png'
import settingsSVG from '@/assets/images/settings.svg'
import { IconBulb } from '@tabler/icons-react'
import { IconBulb, IconBox, IconVariable } from '@tabler/icons-react'
// API
import apiKeyApi from '@/api/apikey'
import chatflowsApi from '@/api/chatflows'
import configApi from '@/api/config'
import variablesApi from '@/api/variables'
// Hooks
import useApi from '@/hooks/useApi'
@@ -83,6 +86,10 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const navigate = useNavigate()
const dispatch = useDispatch()
const theme = useTheme()
const chatflow = useSelector((state) => state.canvas.chatflow)
const apiConfig = chatflow?.apiConfig ? JSON.parse(chatflow.apiConfig) : {}
const overrideConfigStatus = apiConfig?.overrideConfig?.status !== undefined ? apiConfig.overrideConfig.status : false
const codes = ['Embed', 'Python', 'JavaScript', 'cURL', 'Share Chatbot']
const [value, setValue] = useState(0)
@@ -93,16 +100,20 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
const [checkboxVal, setCheckbox] = useState(false)
const [nodeConfig, setNodeConfig] = useState({})
const [nodeConfigExpanded, setNodeConfigExpanded] = useState({})
const [nodeOverrides, setNodeOverrides] = useState(apiConfig?.overrideConfig?.nodes ?? null)
const [variableOverrides, setVariableOverrides] = useState(apiConfig?.overrideConfig?.variables ?? [])
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
const getConfigApi = useApi(configApi.getConfig)
const getAllVariablesApi = useApi(variablesApi.getAllVariables)
const onCheckBoxChanged = (newVal) => {
setCheckbox(newVal)
if (newVal) {
getConfigApi.request(dialogProps.chatflowid)
getAllVariablesApi.request()
}
}
@@ -121,9 +132,12 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
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] = {
@@ -132,12 +146,23 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
}
}
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 })
}
}
})
@@ -145,8 +170,73 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
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 }
// If overrideConfigStatus is true, look for existing variable config
// Otherwise, create new default config
if (overrideConfigStatus) {
const existingVariable = variableOverrides?.find((existingParam) => existingParam.id === id)
if (existingVariable) {
if (!newVariables.some((variable) => variable.id === id)) {
newVariables.push({ ...existingVariable })
}
} else {
if (!newVariables.some((variable) => variable.id === id)) {
newVariables.push({ ...param, enabled: false })
}
}
} else {
// When no override config exists, create default values
if (!newVariables.some((variable) => variable.id === id)) {
newVariables.push({ ...param, enabled: false })
}
}
})
// If overrideConfigStatus is true, clean up any variables that no longer exist
if (overrideConfigStatus && 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 handleAccordionChange = (nodeLabel) => (event, isExpanded) => {
@@ -165,8 +255,16 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => {
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])
const handleChange = (event, newValue) => {
setValue(newValue)
}
@@ -625,55 +723,85 @@ formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")`
showLineNumbers={false}
wrapLines
/>
<CheckboxInput label='Show Input Config' value={checkboxVal} onChange={onCheckBoxChanged} />
<CheckboxInput label='Show Override Config' value={checkboxVal} onChange={onCheckBoxChanged} />
{checkboxVal && getConfigApi.data && getConfigApi.data.length > 0 && (
<>
{Object.keys(nodeConfig)
.sort()
.map((nodeLabel) => (
<Accordion
expanded={nodeConfigExpanded[nodeLabel] || false}
onChange={handleAccordionChange(nodeLabel)}
key={nodeLabel}
disableGutters
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${nodeLabel}`}
id={`nodes-accordian-header-${nodeLabel}`}
>
<Stack flexDirection='row' sx={{ gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant='h5'>{nodeLabel}</Typography>
{nodeConfig[nodeLabel].nodeIds.length > 0 &&
nodeConfig[nodeLabel].nodeIds.map((nodeId, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
width: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
padding: 5,
paddingLeft: 10,
paddingRight: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', fontSize: '0.825rem' }}>
{nodeId}
</span>
</div>
))}
</Stack>
</AccordionSummary>
<AccordionDetails>
<TableViewOnly
rows={nodeConfig[nodeLabel].params}
columns={Object.keys(nodeConfig[nodeLabel].params[0]).slice(-3)}
/>
</AccordionDetails>
</Accordion>
))}
<Typography sx={{ mt: 2, mb: 3 }}>
You can override existing input configuration of the chatflow with overrideConfig property.
</Typography>
<Stack direction='column' spacing={2} sx={{ width: '100%', my: 2 }}>
<Card sx={{ borderColor: theme.palette.primary[200] + 75, p: 2 }} variant='outlined'>
<Stack sx={{ mt: 1, mb: 2, ml: 1, alignItems: 'center' }} direction='row' spacing={2}>
<IconBox />
<Typography variant='h4'>Nodes</Typography>
</Stack>
{Object.keys(nodeConfig)
.sort()
.map((nodeLabel) => (
<Accordion
expanded={nodeConfigExpanded[nodeLabel] || false}
onChange={handleAccordionChange(nodeLabel)}
key={nodeLabel}
disableGutters
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${nodeLabel}`}
id={`nodes-accordian-header-${nodeLabel}`}
>
<Stack
flexDirection='row'
sx={{ gap: 2, alignItems: 'center', flexWrap: 'wrap' }}
>
<Typography variant='h5'>{nodeLabel}</Typography>
{nodeConfig[nodeLabel].nodeIds.length > 0 &&
nodeConfig[nodeLabel].nodeIds.map((nodeId, index) => (
<div
key={index}
style={{
display: 'flex',
flexDirection: 'row',
width: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
padding: 5,
paddingLeft: 10,
paddingRight: 10
}}
>
<span
style={{
color: 'rgb(116,66,16)',
fontSize: '0.825rem'
}}
>
{nodeId}
</span>
</div>
))}
</Stack>
</AccordionSummary>
<AccordionDetails>
<TableViewOnly
rows={nodeOverrides[nodeLabel]}
columns={
nodeOverrides[nodeLabel].length > 0
? Object.keys(nodeOverrides[nodeLabel][0])
: []
}
/>
</AccordionDetails>
</Accordion>
))}
</Card>
<Card sx={{ borderColor: theme.palette.primary[200] + 75, p: 2 }} variant='outlined'>
<Stack sx={{ mt: 1, mb: 2, ml: 1, alignItems: 'center' }} direction='row' spacing={2}>
<IconVariable />
<Typography variant='h4'>Variables</Typography>
</Stack>
<TableViewOnly rows={variableOverrides} columns={['name', 'type', 'enabled']} />
</Card>
</Stack>
<CopyBlock
theme={atomOneDark}
text={
@@ -140,9 +140,9 @@ const ShareChatbot = ({ isSessionMemory, isAgentCanvas }) => {
if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession
if (renderHTML) {
obj.overrideConfig.renderHTML = true
obj.renderHTML = true
} else {
obj.overrideConfig.renderHTML = false
obj.renderHTML = false
}
if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts