diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 02bc2ee14..f0677b046 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -101,7 +101,10 @@ export enum NodeInputKeyEnum { // if else condition = 'condition', - ifElseList = 'ifElseList' + ifElseList = 'ifElseList', + + // variable update + updateList = 'updateList' } export enum NodeOutputKeyEnum { diff --git a/packages/global/core/workflow/node/constant.ts b/packages/global/core/workflow/node/constant.ts index 2036f033c..c500771f2 100644 --- a/packages/global/core/workflow/node/constant.ts +++ b/packages/global/core/workflow/node/constant.ts @@ -112,7 +112,8 @@ export enum FlowNodeTypeEnum { tools = 'tools', stopTool = 'stopTool', lafModule = 'lafModule', - ifElseNode = 'ifElseNode' + ifElseNode = 'ifElseNode', + variableUpdate = 'variableUpdate' } export const EDGE_TYPE = 'default'; diff --git a/packages/global/core/workflow/runtime/constants.ts b/packages/global/core/workflow/runtime/constants.ts index b9d63a7fe..950c6de9b 100644 --- a/packages/global/core/workflow/runtime/constants.ts +++ b/packages/global/core/workflow/runtime/constants.ts @@ -9,7 +9,8 @@ export enum SseResponseEventEnum { toolCall = 'toolCall', // tool start toolParams = 'toolParams', // tool params return toolResponse = 'toolResponse', // tool response return - flowResponses = 'flowResponses' // sse response request + flowResponses = 'flowResponses', // sse response request + variables = 'variables' } export enum DispatchNodeResponseKeyEnum { diff --git a/packages/global/core/workflow/template/constants.ts b/packages/global/core/workflow/template/constants.ts index b3471b172..be909a9d2 100644 --- a/packages/global/core/workflow/template/constants.ts +++ b/packages/global/core/workflow/template/constants.ts @@ -18,10 +18,10 @@ import { PluginOutputModule } from './system/pluginOutput'; import { RunPluginModule } from './system/runPlugin'; import { AiQueryExtension } from './system/queryExtension'; -import type { FlowNodeTemplateType, nodeTemplateListType } from '../type'; -import { FlowNodeTemplateTypeEnum } from '../../workflow/constants'; +import type { FlowNodeTemplateType } from '../type'; import { lafModule } from './system/laf'; import { ifElseNode } from './system/ifElse/index'; +import { variableUpdateNode } from './system/variableUpdate'; /* app flow module templates */ export const appSystemModuleTemplates: FlowNodeTemplateType[] = [ @@ -39,7 +39,8 @@ export const appSystemModuleTemplates: FlowNodeTemplateType[] = [ HttpModule468, AiQueryExtension, lafModule, - ifElseNode + ifElseNode, + variableUpdateNode ]; /* plugin flow module templates */ export const pluginSystemModuleTemplates: FlowNodeTemplateType[] = [ @@ -57,7 +58,8 @@ export const pluginSystemModuleTemplates: FlowNodeTemplateType[] = [ HttpModule468, AiQueryExtension, lafModule, - ifElseNode + ifElseNode, + variableUpdateNode ]; /* all module */ @@ -81,5 +83,6 @@ export const moduleTemplatesFlat: FlowNodeTemplateType[] = [ RunPluginModule, AiQueryExtension, lafModule, - ifElseNode + ifElseNode, + variableUpdateNode ]; diff --git a/packages/global/core/workflow/template/system/variableUpdate/index.tsx b/packages/global/core/workflow/template/system/variableUpdate/index.tsx new file mode 100644 index 000000000..3871bc5fb --- /dev/null +++ b/packages/global/core/workflow/template/system/variableUpdate/index.tsx @@ -0,0 +1,35 @@ +import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '../../../node/constant'; +import { FlowNodeTemplateType } from '../../../type/index.d'; +import { + FlowNodeTemplateTypeEnum, + NodeInputKeyEnum, + WorkflowIOValueTypeEnum +} from '../../../constants'; +import { getHandleConfig } from '../../utils'; + +export const variableUpdateNode: FlowNodeTemplateType = { + id: FlowNodeTypeEnum.variableUpdate, + templateType: FlowNodeTemplateTypeEnum.tools, + flowNodeType: FlowNodeTypeEnum.variableUpdate, + sourceHandle: getHandleConfig(true, true, true, true), + targetHandle: getHandleConfig(true, true, true, true), + avatar: '/imgs/workflow/variable.png', + name: '变量更新', + intro: '可以更新指定节点的输出值和全局变量', + showStatus: true, + isTool: false, + inputs: [ + { + key: NodeInputKeyEnum.updateList, + valueType: WorkflowIOValueTypeEnum.any, + label: '', + renderTypeList: [FlowNodeInputTypeEnum.hidden], + editField: { + key: true, + valueType: true + }, + value: [] + } + ], + outputs: [] +}; diff --git a/packages/global/core/workflow/template/system/variableUpdate/type.d.ts b/packages/global/core/workflow/template/system/variableUpdate/type.d.ts new file mode 100644 index 000000000..fffe90565 --- /dev/null +++ b/packages/global/core/workflow/template/system/variableUpdate/type.d.ts @@ -0,0 +1,5 @@ +export type TUpdateListItem = { + variable?: ReferenceValueProps; + value?: ReferenceValueProps; + renderType?: FlowNodeInputTypeEnum.input | FlowNodeInputTypeEnum.reference; +}; diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index 8e1c4bd41..cb7551a2e 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -42,7 +42,8 @@ import { dispatchLafRequest } from './tools/runLaf'; import { dispatchIfElse } from './tools/runIfElse'; import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils'; -import { dispatchSystemConfig } from './init/systemConfiig'; +import { dispatchSystemConfig } from './init/systemConfig'; +import { dispatchUpdateVariable } from './tools/runUpdateVar'; const callbackMap: Record<`${FlowNodeTypeEnum}`, Function> = { [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, @@ -62,6 +63,7 @@ const callbackMap: Record<`${FlowNodeTypeEnum}`, Function> = { [FlowNodeTypeEnum.stopTool]: dispatchStopToolCall, [FlowNodeTypeEnum.lafModule]: dispatchLafRequest, [FlowNodeTypeEnum.ifElseNode]: dispatchIfElse, + [FlowNodeTypeEnum.variableUpdate]: dispatchUpdateVariable, // none [FlowNodeTypeEnum.systemConfig]: dispatchSystemConfig, @@ -355,7 +357,7 @@ export async function dispatchWorkFlow(data: Props): Promise) => { + const { variables } = props; + return variables; +}; diff --git a/packages/service/core/workflow/dispatch/init/systemConfiig.tsx b/packages/service/core/workflow/dispatch/init/systemConfiig.tsx deleted file mode 100644 index 56c074dcc..000000000 --- a/packages/service/core/workflow/dispatch/init/systemConfiig.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import type { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type/index.d'; -export type UserChatInputProps = ModuleDispatchProps<{ - [NodeInputKeyEnum.userChatInput]: string; -}>; - -export const dispatchSystemConfig = (props: Record) => { - const { variables } = props as UserChatInputProps; - return variables; -}; diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts new file mode 100644 index 000000000..a8e4f23b1 --- /dev/null +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -0,0 +1,53 @@ +import { NodeInputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; +import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; +import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils'; +import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; +import { ModuleDispatchProps } from '@fastgpt/global/core/workflow/type'; + +type Props = ModuleDispatchProps<{ + [NodeInputKeyEnum.updateList]: TUpdateListItem[]; +}>; + +export const dispatchUpdateVariable = async ( + props: Props +): Promise> => { + const { params, variables, runtimeNodes } = props; + + const { updateList } = params; + updateList.forEach((item) => { + const varNodeId = item.variable?.[0]; + const varKey = item.variable?.[1]; + + if (!varNodeId || !varKey) { + return; + } + let value = ''; + if (!item.value?.[0]) { + value = item.value?.[1]; + } else { + value = getReferenceVariableValue({ + value: item.value, + variables, + nodes: runtimeNodes + }); + } + if (varNodeId === VARIABLE_NODE_ID) { + variables[varKey] = value; + } else { + const node = runtimeNodes.find((node) => node.nodeId === varNodeId); + if (node) { + const output = node.outputs.find((output) => output.id === varKey); + if (output) { + output.value = value; + } + } + } + }); + + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + totalPoints: 0 + } + }; +}; diff --git a/packages/service/core/workflow/dispatch/type.d.ts b/packages/service/core/workflow/dispatch/type.d.ts index 8dcd7d474..88b6748b4 100644 --- a/packages/service/core/workflow/dispatch/type.d.ts +++ b/packages/service/core/workflow/dispatch/type.d.ts @@ -19,4 +19,5 @@ export type DispatchFlowResponse = { }; [DispatchNodeResponseKeyEnum.toolResponses]: ToolRunResponseItemType; [DispatchNodeResponseKeyEnum.assistantResponses]: AIChatItemValueItemType[]; + newVariables: Record; }; diff --git a/packages/service/core/workflow/dispatchV1/type.d.ts b/packages/service/core/workflow/dispatchV1/type.d.ts index 5fc6f0eb8..189505610 100644 --- a/packages/service/core/workflow/dispatchV1/type.d.ts +++ b/packages/service/core/workflow/dispatchV1/type.d.ts @@ -12,4 +12,5 @@ export type DispatchFlowResponse = { flowUsages: ChatNodeUsageType[]; [DispatchNodeResponseKeyEnum.toolResponses]: ToolRunResponseItemType; [DispatchNodeResponseKeyEnum.assistantResponses]: AIChatItemValueItemType[]; + newVariables: Record; }; diff --git a/projects/app/i18n/en/common.json b/projects/app/i18n/en/common.json index 893ff25e9..c6ce69d6a 100644 --- a/projects/app/i18n/en/common.json +++ b/projects/app/i18n/en/common.json @@ -1050,8 +1050,75 @@ "Intro placeholder": "If this plugin is invoked as a tool, this description will be used as the prompt." }, "shareChat": { - "Init Error": "Conversation initialization failed", - "Init History Error": "Chat history initialization failed" + "Init Error": "Init Chat Error", + "Init History Error": "Init History Error" + }, + "workflow": { + "Add variable": "Add", + "Can not delete node": "Can not delete the node", + "Change input type tip": "Changing the input type will empty the entered values, please confirm!", + "Check Failed": "Workflow verification fails. Check whether the node or connection is normal", + "Confirm stop debug": "Do you want to terminate debugging? Debugging information is not retained.", + "Copy node": "Copy node", + "Current workflow": "Current workflow", + "Custom inputs": "Inputs", + "Custom outputs": "Outputs", + "Custom variable": "Custom variable", + "Dataset quote": "Dataset quote", + "Debug": "Debug", + "Debug Node": "Workflow Debug", + "Failed": "Running failed", + "Not intro": "This node is not introduced", + "Run from here": "Run from here", + "Run result": "Run result", + "Running": "Running", + "Skipped": "Skipped", + "Stop debug": "Stop", + "Success": "Running success", + "Value type": "Type", + "Variable outputs": "Variables", + "chat": { + "Quote prompt": "Quote prompt" + }, + "debug": { + "Done": "Done", + "Hide result": "Hide result", + "Not result": "Not result", + "Run result": "", + "Show result": "Show result" + }, + "inputType": { + "JSON Editor": "JSON Editor", + "Manual input": "", + "Manual select": "Select", + "Reference": "Reference", + "Required": "Required", + "Select edit field": "Editable field", + "Select input default value": "Default value", + "Select input type": "Configurable input types", + "Select input type placeholder": "Please select a configurable input type", + "chat history": "History", + "dynamicTargetInput": "dynamic Target Input", + "input": "Input", + "number input": "number input", + "selectApp": "App Selector", + "selectDataset": "Dataset Selector", + "selectLLMModel": "Select Chat Model", + "switch": "Switch", + "target": "Target Data", + "textarea": "Textarea" + }, + "publish": { + "OnRevert version": "Click back to that version", + "OnRevert version confirm": "Are you sure to roll back the version?", + "histories": "Publiish histories" + }, + "tool": { + "Handle": "Tool handle", + "Select Tool": "Select Tool" + }, + "value": "Value", + "variable": "Variable" } }, "dataset": { diff --git a/projects/app/i18n/zh/common.json b/projects/app/i18n/zh/common.json index 974456b61..258e9137f 100644 --- a/projects/app/i18n/zh/common.json +++ b/projects/app/i18n/zh/common.json @@ -1160,7 +1160,9 @@ "tool": { "Handle": "工具连接器", "Select Tool": "选择工具" - } + }, + "value": "值", + "variable": "变量" } }, "dataset": { diff --git a/projects/app/src/components/ChatBox/index.tsx b/projects/app/src/components/ChatBox/index.tsx index 0664ff25e..0f725e0c2 100644 --- a/projects/app/src/components/ChatBox/index.tsx +++ b/projects/app/src/components/ChatBox/index.tsx @@ -91,6 +91,7 @@ type Props = OutLinkChatAuthProps & { onStartChat?: (e: StartChatFnProps) => Promise<{ responseText: string; [DispatchNodeResponseKeyEnum.nodeResponse]: ChatHistoryItemResType[]; + newVariables?: Record; isNewChat?: boolean; }>; onDelMessage?: (e: { contentId: string }) => void; @@ -462,6 +463,7 @@ const ChatBox = ( const { responseData, responseText, + newVariables, isNewChat = false } = await onStartChat({ chatList: newChatList, @@ -470,6 +472,7 @@ const ChatBox = ( generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }), variables }); + setValue('variables', newVariables || []); isNewChatReplace.current = isNewChat; @@ -549,6 +552,7 @@ const ChatBox = ( resetInputVal, setAudioPlayingChatId, setChatHistories, + setValue, splitText2Audio, startSegmentedAudio, t, diff --git a/projects/app/src/components/core/workflow/Flow/ChatTest.tsx b/projects/app/src/components/core/workflow/Flow/ChatTest.tsx index b8e38e53d..9fd976953 100644 --- a/projects/app/src/components/core/workflow/Flow/ChatTest.tsx +++ b/projects/app/src/components/core/workflow/Flow/ChatTest.tsx @@ -68,7 +68,7 @@ const ChatTest = ( const history = chatList.slice(-historyMaxLen - 2, -2); // 流请求,获取数据 - const { responseText, responseData } = await streamFetch({ + const { responseText, responseData, newVariables } = await streamFetch({ url: '/api/core/chat/chatTest', data: { history, @@ -84,7 +84,7 @@ const ChatTest = ( abortCtrl: controller }); - return { responseText, responseData }; + return { responseText, responseData, newVariables }; }, [app._id, app.name, edges, nodes] ); diff --git a/projects/app/src/components/core/workflow/Flow/index.tsx b/projects/app/src/components/core/workflow/Flow/index.tsx index 7076b363a..b08307bce 100644 --- a/projects/app/src/components/core/workflow/Flow/index.tsx +++ b/projects/app/src/components/core/workflow/Flow/index.tsx @@ -57,7 +57,8 @@ const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = { ), [FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')), - [FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')) + [FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')), + [FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')) }; const edgeTypes = { [EDGE_TYPE]: ButtonEdge diff --git a/projects/app/src/components/core/workflow/Flow/nodes/NodeVariableUpdate.tsx b/projects/app/src/components/core/workflow/Flow/nodes/NodeVariableUpdate.tsx new file mode 100644 index 000000000..7a8fef7a3 --- /dev/null +++ b/projects/app/src/components/core/workflow/Flow/nodes/NodeVariableUpdate.tsx @@ -0,0 +1,277 @@ +import React, { useCallback, useMemo } from 'react'; +import NodeCard from './render/NodeCard'; +import { NodeProps } from 'reactflow'; +import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type'; +import { useTranslation } from 'react-i18next'; +import { + Box, + Button, + Flex, + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, + Switch, + Textarea +} from '@chakra-ui/react'; +import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type'; +import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { useContextSelector } from 'use-context-selector'; +import { WorkflowContext } from '@/components/core/workflow/context'; +import { + FlowNodeInputMap, + FlowNodeInputTypeEnum +} from '@fastgpt/global/core/workflow/node/constant'; +import Container from '../components/Container'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; +import { SmallAddIcon } from '@chakra-ui/icons'; +import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io'; +import { ReferSelector, useReference } from './render/RenderInput/templates/Reference'; + +const NodeVariableUpdate = ({ data, selected }: NodeProps) => { + const { inputs = [], nodeId } = data; + const { t } = useTranslation(); + + const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); + const nodes = useContextSelector(WorkflowContext, (v) => v.nodes); + + const updateList = useMemo( + () => + (inputs.find((input) => input.key === NodeInputKeyEnum.updateList) + ?.value as TUpdateListItem[]) || [], + [inputs] + ); + + const onUpdateList = useCallback( + (value: TUpdateListItem[]) => { + const updateListInput = inputs.find((input) => input.key === NodeInputKeyEnum.updateList); + if (!updateListInput) return; + + onChangeNode({ + nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.updateList, + value: { + ...updateListInput, + value + } + }); + }, + [inputs, nodeId, onChangeNode] + ); + + const menuList = [ + { + renderType: FlowNodeInputTypeEnum.input, + icon: FlowNodeInputMap[FlowNodeInputTypeEnum.input].icon, + label: t('core.workflow.inputType.Manual input') + }, + { + renderType: FlowNodeInputTypeEnum.reference, + icon: FlowNodeInputMap[FlowNodeInputTypeEnum.reference].icon, + label: t('core.workflow.inputType.Reference') + } + ]; + + return ( + + + {updateList.map((updateItem, index) => { + const type = (() => { + const variable = updateItem.variable; + const variableNodeId = variable?.[0]; + const variableNode = nodes.find((node) => node.id === variableNodeId); + if (!variableNode) return 'any'; + const variableInput = variableNode.data.outputs.find( + (output) => output.id === variable?.[1] + ); + if (!variableInput) return 'any'; + return variableInput.valueType; + })(); + + const renderTypeData = menuList.find((item) => item.renderType === updateItem.renderType); + const handleUpdate = (newValue: any) => { + onUpdateList( + updateList.map((update, i) => + i === index ? { ...update, value: ['', newValue] } : update + ) + ); + }; + return ( + + + + {t('core.workflow.variable')} + { + onUpdateList( + updateList.map((update, i) => { + if (i === index) { + return { + ...update, + variable: value + }; + } + return update; + }) + ); + }} + /> + + { + onUpdateList(updateList.filter((_, i) => i !== index)); + }} + /> + + + + {t('core.workflow.value')} + item.renderType === updateItem.renderType)?.label + } + > + + + + {updateItem.renderType === FlowNodeInputTypeEnum.reference ? ( + + ) : ( + <> + {type === 'string' && ( +