From f44305af0d1e909e472af28595f0059cc3b23de4 Mon Sep 17 00:00:00 2001 From: zhsama Date: Mon, 19 Jan 2026 22:29:28 +0800 Subject: [PATCH] feat: add AssembleVariablesAlt icon and integrate into sub-graph components. --- .../line/general/AssembleVariablesAlt.json | 26 +++ .../line/general/AssembleVariablesAlt.tsx | 20 ++ .../icons/src/vender/line/general/index.ts | 1 + .../components/sub-graph-children.tsx | 54 +++-- .../sub-graph/components/sub-graph-main.tsx | 78 +++++--- web/app/components/sub-graph/index.tsx | 188 ++++++++++++------ web/app/components/sub-graph/types.ts | 32 ++- .../components/workflow/hooks-store/store.ts | 3 + web/app/components/workflow/index.tsx | 15 +- .../variable/var-reference-vars.tsx | 38 ++-- .../nodes/sub-graph-start/constants.ts | 1 + .../workflow/nodes/sub-graph-start/index.tsx | 60 ++++++ .../agent-header-bar.tsx | 8 +- .../mixed-variable-text-input/index.tsx | 34 +++- .../tool/components/sub-graph-modal/index.tsx | 156 ++++++++++----- .../sub-graph-modal/sub-graph-canvas.tsx | 34 +--- .../tool/components/sub-graph-modal/types.ts | 39 ++-- .../components/workflow/nodes/tool/node.tsx | 11 +- .../components/workflow/utils/elk-layout.ts | 2 + .../nodes/sub-graph-start/index.tsx | 60 ++++++ .../workflow/workflow-preview/index.tsx | 3 + 21 files changed, 611 insertions(+), 252 deletions(-) create mode 100644 web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.json create mode 100644 web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.tsx create mode 100644 web/app/components/workflow/nodes/sub-graph-start/constants.ts create mode 100644 web/app/components/workflow/nodes/sub-graph-start/index.tsx create mode 100644 web/app/components/workflow/workflow-preview/components/nodes/sub-graph-start/index.tsx diff --git a/web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.json b/web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.json new file mode 100644 index 0000000000..9823224134 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.json @@ -0,0 +1,26 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "12", + "height": "12", + "viewBox": "0 0 12 12", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M5.14286 5.14286V3.42857L8 5.71429L5.14286 8V6.28571H0V5.14286H5.14286ZM0.83303 7.42857H2.04658C2.72474 9.10389 4.36721 10.28571 6.28571 10.28571C8.81049 10.28571 10.85717 8.23903 10.85717 5.71429C10.85717 3.18956 8.81049 1.14285 6.28571 1.14285C4.36721 1.14285 2.72474 2.32467 2.04658 4H0.83303C1.56118 1.68165 3.72706 0 6.28571 0C9.4416 0 12 2.55837 12 5.71429C12 8.87014 9.4416 11.42854 6.28571 11.42854C3.72706 11.42854 1.56118 9.74691 0.83303 7.42857Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "AssembleVariablesAlt" +} diff --git a/web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.tsx b/web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.tsx new file mode 100644 index 0000000000..980d9fc2b1 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/AssembleVariablesAlt.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './AssembleVariablesAlt.json' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject> + }, +) => + +Icon.displayName = 'AssembleVariablesAlt' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/general/index.ts b/web/app/components/base/icons/src/vender/line/general/index.ts index 3ce1f86b62..90c37a6665 100644 --- a/web/app/components/base/icons/src/vender/line/general/index.ts +++ b/web/app/components/base/icons/src/vender/line/general/index.ts @@ -1,4 +1,5 @@ export { default as AssembleVariables } from './AssembleVariables' +export { default as AssembleVariablesAlt } from './AssembleVariablesAlt' export { default as AtSign } from './AtSign' export { default as Bookmark } from './Bookmark' export { default as Check } from './Check' diff --git a/web/app/components/sub-graph/components/sub-graph-children.tsx b/web/app/components/sub-graph/components/sub-graph-children.tsx index a8867d1963..4a18a66a1c 100644 --- a/web/app/components/sub-graph/components/sub-graph-children.tsx +++ b/web/app/components/sub-graph/components/sub-graph-children.tsx @@ -7,22 +7,28 @@ import { useShallow } from 'zustand/react/shallow' import { useIsChatMode, useWorkflowVariables } from '@/app/components/workflow/hooks' import Panel from '@/app/components/workflow/panel' import { useStore } from '@/app/components/workflow/store' -import { BlockEnum } from '@/app/components/workflow/types' import ConfigPanel from './config-panel' -type SubGraphChildrenProps = { - agentName: string - extractorNodeId: string - mentionConfig: MentionConfig - onMentionConfigChange: (config: MentionConfig) => void -} +type SubGraphChildrenProps + = | { + variant: 'agent' + title: string + extractorNodeId: string + mentionConfig: MentionConfig + onMentionConfigChange: (config: MentionConfig) => void + } + | { + variant: 'assemble' + title: string + extractorNodeId: string + } -const SubGraphChildren: FC = ({ - agentName, - extractorNodeId, - mentionConfig, - onMentionConfigChange, -}) => { +const SubGraphChildren: FC = (props) => { + const { + variant, + title, + extractorNodeId, + } = props const { getNodeAvailableVars } = useWorkflowVariables() const isChatMode = useIsChatMode() const nodePanelWidth = useStore(s => s.nodePanelWidth) @@ -32,7 +38,7 @@ const SubGraphChildren: FC = ({ })) const extractorNode = useReactFlowStore(useShallow((s) => { - return s.getNodes().find(node => node.data.type === BlockEnum.LLM) + return s.getNodes().find(node => node.id === extractorNodeId) })) const availableNodes = useMemo(() => { @@ -51,8 +57,10 @@ const SubGraphChildren: FC = ({ return vars.filter(item => item.nodeId === extractorNode.id) }, [extractorNode, getNodeAvailableVars, isChatMode]) + const agentProps = variant === 'agent' ? props : null + const panelRight = useMemo(() => { - if (selectedNode) + if (!agentProps || selectedNode) return null return ( @@ -62,17 +70,25 @@ const SubGraphChildren: FC = ({ style={{ width: `${nodePanelWidth}px` }} > ) - }, [agentName, availableNodes, availableVars, extractorNodeId, mentionConfig, nodePanelWidth, onMentionConfigChange, selectedNode]) + }, [agentProps, availableNodes, availableVars, extractorNodeId, nodePanelWidth, selectedNode, title]) + + if (variant === 'assemble') { + return ( + + ) + } return ( void + selectableNodeTypes?: BlockEnum[] onSave?: (nodes: Node[], edges: Edge[]) => void onSyncWorkflowDraft?: SyncWorkflowDraft } -const SubGraphMain: FC = ({ - nodes, - edges, - viewport, - agentName, - extractorNodeId, - configsMap, - mentionConfig, - onMentionConfigChange, - onSave, - onSyncWorkflowDraft, -}) => { +type SubGraphMainProps + = | (SubGraphMainBaseProps & { + variant: 'agent' + mentionConfig: MentionConfig + onMentionConfigChange: (config: MentionConfig) => void + }) + | (SubGraphMainBaseProps & { + variant: 'assemble' + }) + +const SubGraphMain: FC = (props) => { + const { + nodes, + edges, + viewport, + variant, + title, + extractorNodeId, + configsMap, + selectableNodeTypes, + onSave, + onSyncWorkflowDraft, + } = props const reactFlowStore = useStoreApi() const availableNodesMetaData = useAvailableNodesMetaData() const flowType = configsMap?.flowType ?? FlowType.appFlow @@ -76,32 +87,53 @@ const SubGraphMain: FC = ({ } }, [handleSyncSubGraphDraft, onSyncWorkflowDraft]) + const resolvedSelectableTypes = useMemo(() => { + if (selectableNodeTypes && selectableNodeTypes.length > 0) + return selectableNodeTypes + return variant === 'agent' ? [BlockEnum.LLM] : [BlockEnum.Code] + }, [selectableNodeTypes, variant]) + const hooksStore = useMemo(() => ({ interactionMode: 'subgraph', + subGraphSelectableNodeTypes: resolvedSelectableTypes, availableNodesMetaData, configsMap, fetchInspectVars, ...inspectVarsCrud, doSyncWorkflowDraft: handleSyncWorkflowDraft, syncWorkflowDraftWhenPageClose: handleSyncSubGraphDraft, - }), [availableNodesMetaData, configsMap, fetchInspectVars, handleSyncSubGraphDraft, handleSyncWorkflowDraft, inspectVarsCrud]) + }), [availableNodesMetaData, configsMap, fetchInspectVars, handleSyncSubGraphDraft, handleSyncWorkflowDraft, inspectVarsCrud, resolvedSelectableTypes]) + + const subGraphChildren = variant === 'agent' + ? ( + + ) + : ( + + ) return ( - + {subGraphChildren} ) } diff --git a/web/app/components/sub-graph/index.tsx b/web/app/components/sub-graph/index.tsx index c136cc929b..28adbac608 100644 --- a/web/app/components/sub-graph/index.tsx +++ b/web/app/components/sub-graph/index.tsx @@ -7,6 +7,7 @@ import { memo, useEffect, useMemo } from 'react' import WorkflowWithDefaultContext from '@/app/components/workflow' import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION } from '@/app/components/workflow/constants' import { WorkflowContextProvider } from '@/app/components/workflow/context' +import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants' import { useStore } from '@/app/components/workflow/store' import { BlockEnum, EditionType, isPromptMessageContext, PromptRole } from '@/app/components/workflow/types' import SubGraphMain from './components/sub-graph-main' @@ -18,7 +19,7 @@ const SUB_GRAPH_ENTRY_POSITION = { x: START_INITIAL_POSITION.x, y: 150, } -const SUB_GRAPH_LLM_POSITION = { +const SUB_GRAPH_EXTRACTOR_POSITION = { x: SUB_GRAPH_ENTRY_POSITION.x + NODE_WIDTH_X_OFFSET - SUB_GRAPH_EDGE_GAP, y: SUB_GRAPH_ENTRY_POSITION.y, } @@ -33,19 +34,19 @@ const SubGraphContent: FC = (props) => { const { toolNodeId, paramKey, - agentName, - agentNodeId, - mentionConfig, - onMentionConfigChange, - extractorNode, toolParamValue, parentAvailableNodes, parentAvailableVars, configsMap, + selectableNodeTypes, onSave, onSyncWorkflowDraft, } = props + const isAgentVariant = props.variant === 'agent' + const sourceTitle = isAgentVariant ? (props.agentName || '') : (props.title || '') + const resolvedAgentNodeId = isAgentVariant ? props.agentNodeId : '' + const setParentAvailableVars = useStore(state => state.setParentAvailableVars) const setParentAvailableNodes = useStore(state => state.setParentAvailableNodes) @@ -55,28 +56,47 @@ const SubGraphContent: FC = (props) => { }, [parentAvailableNodes, parentAvailableVars, setParentAvailableNodes, setParentAvailableVars]) const promptText = useMemo(() => { - if (!toolParamValue) + if (!isAgentVariant || !toolParamValue) return '' // Reason: escape agent id before building a regex pattern. - const escapedAgentId = agentNodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const escapedAgentId = resolvedAgentNodeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const leadingPattern = new RegExp(`^\\{\\{[@#]${escapedAgentId}\\.context[@#]\\}\\}`) return toolParamValue.replace(leadingPattern, '') - }, [agentNodeId, toolParamValue]) + }, [isAgentVariant, resolvedAgentNodeId, toolParamValue]) const startNode = useMemo(() => { + if (!isAgentVariant) { + return { + id: 'subgraph-source', + type: CUSTOM_SUB_GRAPH_START_NODE, + position: SUB_GRAPH_ENTRY_POSITION, + data: { + type: BlockEnum.Start, + title: sourceTitle, + desc: '', + selected: false, + iconType: 'assemble', + variables: [], + }, + selected: false, + selectable: false, + draggable: false, + connectable: false, + focusable: false, + deletable: false, + } + } + return { id: 'subgraph-source', - type: 'custom', + type: CUSTOM_SUB_GRAPH_START_NODE, position: SUB_GRAPH_ENTRY_POSITION, data: { type: BlockEnum.Start, - title: agentName, + title: sourceTitle, desc: '', - _connectedSourceHandleIds: ['source'], - _connectedTargetHandleIds: [], - _subGraphEntry: true, - _iconTypeOverride: BlockEnum.Agent, selected: false, + iconType: 'agent', variables: [], }, selected: false, @@ -86,65 +106,83 @@ const SubGraphContent: FC = (props) => { focusable: false, deletable: false, } - }, [agentName]) + }, [isAgentVariant, sourceTitle]) const extractorDisplayNode = useMemo(() => { - if (!extractorNode) - return null + if (isAgentVariant) { + const extractorNode = props.extractorNode + if (!extractorNode) + return null - const applyPromptText = (item: PromptItem) => { - if (item.edition_type === EditionType.jinja2) { - return { - ...item, - text: promptText, - jinja2_text: promptText, - } - } - return { ...item, text: promptText } - } - - const nextPromptTemplate = (() => { - const template = extractorNode.data.prompt_template - if (!Array.isArray(template)) - return applyPromptText(template as PromptItem) - - const userIndex = template.findIndex( - item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user, - ) - if (userIndex >= 0) { - return template.map((item, index) => { - if (index !== userIndex) - return item - return applyPromptText(item as PromptItem) - }) as PromptTemplateItem[] - } - - const useJinja = template.some( - item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2, - ) - const defaultUserPrompt: PromptItem = useJinja - ? { - role: PromptRole.user, + const applyPromptText = (item: PromptItem) => { + if (item.edition_type === EditionType.jinja2) { + return { + ...item, text: promptText, jinja2_text: promptText, - edition_type: EditionType.jinja2, } - : { role: PromptRole.user, text: promptText } - return [...template, defaultUserPrompt] as PromptTemplateItem[] - })() + } + return { ...item, text: promptText } + } + + const nextPromptTemplate = (() => { + const template = extractorNode.data.prompt_template + if (!Array.isArray(template)) + return applyPromptText(template as PromptItem) + + const userIndex = template.findIndex( + item => !isPromptMessageContext(item) && (item as PromptItem).role === PromptRole.user, + ) + if (userIndex >= 0) { + return template.map((item, index) => { + if (index !== userIndex) + return item + return applyPromptText(item as PromptItem) + }) as PromptTemplateItem[] + } + + const useJinja = template.some( + item => !isPromptMessageContext(item) && (item as PromptItem).edition_type === EditionType.jinja2, + ) + const defaultUserPrompt: PromptItem = useJinja + ? { + role: PromptRole.user, + text: promptText, + jinja2_text: promptText, + edition_type: EditionType.jinja2, + } + : { role: PromptRole.user, text: promptText } + return [...template, defaultUserPrompt] as PromptTemplateItem[] + })() + + return { + ...extractorNode, + hidden: false, + selected: false, + position: SUB_GRAPH_EXTRACTOR_POSITION, + data: { + ...extractorNode.data, + selected: false, + prompt_template: nextPromptTemplate, + }, + } + } + + const extractorNode = props.extractorNode + if (!extractorNode) + return null return { ...extractorNode, hidden: false, selected: false, - position: SUB_GRAPH_LLM_POSITION, + position: SUB_GRAPH_EXTRACTOR_POSITION, data: { ...extractorNode.data, selected: false, - prompt_template: nextPromptTemplate, }, } - }, [extractorNode, promptText]) + }, [isAgentVariant, promptText, props.extractorNode]) const nodesSource = useMemo(() => { if (!extractorDisplayNode) @@ -168,30 +206,54 @@ const SubGraphContent: FC = (props) => { selectable: false, data: { sourceType: BlockEnum.Start, - targetType: BlockEnum.LLM, + targetType: isAgentVariant ? BlockEnum.LLM : BlockEnum.Code, _isTemp: true, _isSubGraphTemp: true, }, }, ] - }, [extractorDisplayNode, startNode]) + }, [extractorDisplayNode, isAgentVariant, startNode]) const { nodes, edges } = useSubGraphNodes(nodesSource, edgesSource) + if (isAgentVariant) { + return ( + + + + ) + } + return ( diff --git a/web/app/components/sub-graph/types.ts b/web/app/components/sub-graph/types.ts index 2ffd7f91eb..94e3b4584d 100644 --- a/web/app/components/sub-graph/types.ts +++ b/web/app/components/sub-graph/types.ts @@ -1,8 +1,9 @@ import type { StateCreator } from 'zustand' import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store' import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types' +import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types' import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' -import type { Edge, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types' +import type { BlockEnum, Edge, Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types' export type SyncWorkflowDraftCallback = { onSuccess?: () => void @@ -15,23 +16,38 @@ export type SyncWorkflowDraft = ( callback?: SyncWorkflowDraftCallback, ) => Promise -export type SubGraphProps = { +export type SubGraphVariant = 'agent' | 'assemble' + +type BaseSubGraphProps = { toolNodeId: string paramKey: string - sourceVariable: ValueSelector - agentNodeId: string - agentName: string configsMap?: HooksStoreShape['configsMap'] - mentionConfig: MentionConfig - onMentionConfigChange: (config: MentionConfig) => void - extractorNode?: Node toolParamValue?: string parentAvailableNodes?: Node[] parentAvailableVars?: NodeOutPutVar[] + selectableNodeTypes?: BlockEnum[] onSave?: (nodes: Node[], edges: Edge[]) => void onSyncWorkflowDraft?: SyncWorkflowDraft } +export type AgentSubGraphProps = BaseSubGraphProps & { + variant: 'agent' + sourceVariable: ValueSelector + agentNodeId: string + agentName: string + mentionConfig: MentionConfig + onMentionConfigChange: (config: MentionConfig) => void + extractorNode?: Node +} + +export type AssembleSubGraphProps = BaseSubGraphProps & { + variant: 'assemble' + title: string + extractorNode?: Node +} + +export type SubGraphProps = AgentSubGraphProps | AssembleSubGraphProps + export type SubGraphSliceShape = { parentAvailableVars: NodeOutPutVar[] parentAvailableNodes: Node[] diff --git a/web/app/components/workflow/hooks-store/store.ts b/web/app/components/workflow/hooks-store/store.ts index 989c5e5063..716d77b7a7 100644 --- a/web/app/components/workflow/hooks-store/store.ts +++ b/web/app/components/workflow/hooks-store/store.ts @@ -46,6 +46,7 @@ export type CommonHooksFnMap = { handleWorkflowTriggerWebhookRunInWorkflow: (params: { nodeId: string }) => void handleWorkflowTriggerPluginRunInWorkflow: (nodeId?: string) => void handleWorkflowRunAllTriggersInWorkflow: (nodeIds: string[]) => void + subGraphSelectableNodeTypes?: BlockEnum[] availableNodesMetaData?: AvailableNodesMetaData getWorkflowRunAndTraceUrl: (runId?: string) => { runUrl: string, traceUrl: string } exportCheck?: () => Promise @@ -93,6 +94,7 @@ export const createHooksStore = ({ handleWorkflowTriggerWebhookRunInWorkflow = noop, handleWorkflowTriggerPluginRunInWorkflow = noop, handleWorkflowRunAllTriggersInWorkflow = noop, + subGraphSelectableNodeTypes, availableNodesMetaData = { nodes: [], }, @@ -136,6 +138,7 @@ export const createHooksStore = ({ handleWorkflowTriggerWebhookRunInWorkflow, handleWorkflowTriggerPluginRunInWorkflow, handleWorkflowRunAllTriggersInWorkflow, + subGraphSelectableNodeTypes, availableNodesMetaData, getWorkflowRunAndTraceUrl, exportCheck, diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index f0846f2996..185bd9c34a 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -89,6 +89,8 @@ import CustomIterationStartNode from './nodes/iteration-start' import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants' import CustomLoopStartNode from './nodes/loop-start' import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants' +import CustomSubGraphStartNode from './nodes/sub-graph-start' +import { CUSTOM_SUB_GRAPH_START_NODE } from './nodes/sub-graph-start/constants' import CustomNoteNode from './note-node' import { CUSTOM_NOTE_NODE } from './note-node/constants' import Operator from './operator' @@ -119,6 +121,7 @@ const nodeTypes = { [CUSTOM_NODE]: CustomNode, [CUSTOM_NOTE_NODE]: CustomNoteNode, [CUSTOM_SIMPLE_NODE]: CustomSimpleNode, + [CUSTOM_SUB_GRAPH_START_NODE]: CustomSubGraphStartNode, [CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode, [CUSTOM_LOOP_START_NODE]: CustomLoopStartNode, [CUSTOM_DATA_SOURCE_EMPTY_NODE]: CustomDataSourceEmptyNode, @@ -355,6 +358,7 @@ export const Workflow: FC = memo(({ const dataSourceList = useStore(s => s.dataSourceList) // buildInTools, customTools, workflowTools, mcpTools, dataSourceList const configsMap = useHooksStore(s => s.configsMap) + const subGraphSelectableNodeTypes = useHooksStore(s => s.subGraphSelectableNodeTypes) const [isLoadedVars, setIsLoadedVars] = useState(false) const [vars, setVars] = useState([]) useEffect(() => { @@ -393,12 +397,17 @@ export const Workflow: FC = memo(({ const handleNodeClickInMode = useCallback( (event, node) => { - if (isSubGraph && node.data.type !== BlockEnum.LLM) - return + if (isSubGraph) { + const allowTypes = subGraphSelectableNodeTypes?.length + ? subGraphSelectableNodeTypes + : [BlockEnum.LLM] + if (!allowTypes.includes(node.data.type)) + return + } handleNodeClick(event, node) }, - [handleNodeClick, isSubGraph], + [handleNodeClick, isSubGraph, subGraphSelectableNodeTypes], ) return ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 708ef3a77b..745b383305 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -351,6 +351,25 @@ const VarReferenceVars: FC = ({ ) } + { + showAssembleVariables && ( +
+ +
+ ) + } {filteredVars.length > 0 ? (
@@ -404,25 +423,6 @@ const VarReferenceVars: FC = ({ /> ) } - { - showAssembleVariables && ( -
- -
- ) - } ) } diff --git a/web/app/components/workflow/nodes/sub-graph-start/constants.ts b/web/app/components/workflow/nodes/sub-graph-start/constants.ts new file mode 100644 index 0000000000..4cb8c08038 --- /dev/null +++ b/web/app/components/workflow/nodes/sub-graph-start/constants.ts @@ -0,0 +1 @@ +export const CUSTOM_SUB_GRAPH_START_NODE = 'custom-sub-graph-start' diff --git a/web/app/components/workflow/nodes/sub-graph-start/index.tsx b/web/app/components/workflow/nodes/sub-graph-start/index.tsx new file mode 100644 index 0000000000..b5fff65994 --- /dev/null +++ b/web/app/components/workflow/nodes/sub-graph-start/index.tsx @@ -0,0 +1,60 @@ +import type { NodeProps } from 'reactflow' +import type { CommonNodeType } from '@/app/components/workflow/types' +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general' +import { Agent } from '@/app/components/base/icons/src/vender/workflow' +import Tooltip from '@/app/components/base/tooltip' +import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle' +import { cn } from '@/utils/classnames' + +type SubGraphStartNodeData = CommonNodeType<{ + tooltip?: string + iconType?: string +}> + +type IconComponent = typeof Agent + +const iconMap: Record = { + agent: Agent, + assemble: AssembleVariablesAlt, +} + +const SubGraphStartNode = ({ id, data }: NodeProps) => { + const { t } = useTranslation() + const iconType = data?.iconType || 'agent' + const Icon = iconMap[iconType] || Agent + const rawTitle = data?.title?.trim() || '' + const showTitle = iconType === 'agent' && !!rawTitle + const displayTitle = showTitle && (rawTitle.startsWith('@') ? rawTitle : `@${rawTitle}`) + const tooltip = data?.tooltip + || (iconType === 'assemble' ? t('blocks.start', { ns: 'workflow' }) : (data?.title || t('blocks.start', { ns: 'workflow' }))) + + return ( +
+ +
+ +
+
+ {showTitle && ( + + {displayTitle} + + )} + +
+ ) +} + +export default memo(SubGraphStartNode) diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/agent-header-bar.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/agent-header-bar.tsx index ac757431c5..b4445a747d 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/agent-header-bar.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/agent-header-bar.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react' import { memo } from 'react' import { useTranslation } from 'react-i18next' +import { AssembleVariables } from '@/app/components/base/icons/src/vender/line/general' import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle' import { Agent } from '@/app/components/base/icons/src/vender/workflow' import { cn } from '@/utils/classnames' @@ -34,8 +35,11 @@ const AgentHeaderBar: FC = ({ : 'border-components-panel-border-subtle bg-components-badge-white-to-dark', )} > -
- +
+ {showAtPrefix ? : }
{showAtPrefix && '@'} diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx index 33eb6ae3d8..1527a27178 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -36,9 +36,16 @@ import Placeholder from './placeholder' /** * Matches agent context variable syntax: {{@nodeId.context@}} - * Example: {{@agent-123.context@}} -> captures "agent-123" + * Example: {{@agent-123.context@}} */ -const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g +const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g +const AGENT_CONTEXT_VAR_PREFIX = '{{@' +const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}' +const getAgentNodeIdFromContextVar = (placeholder: string) => { + if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX)) + return '' + return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length) +} const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string) => { if (!toolNodeId || !paramKey) @@ -309,8 +316,9 @@ const MixedVariableTextInput = ({ const matches = text.matchAll(AGENT_CONTEXT_VAR_PATTERN) for (const match of matches) { - const variablePath = match[1] - const nodeId = variablePath.split('.')[0] + const nodeId = getAgentNodeIdFromContextVar(match[0]) + if (!nodeId) + continue const node = nodesByIdMap[nodeId] if (node && contextNodeIds.has(nodeId)) { return { @@ -461,8 +469,8 @@ const MixedVariableTextInput = ({ if (!agentNodeId || !onChange) return - const valueWithoutAgentVars = value.replace(AGENT_CONTEXT_VAR_PATTERN, (match, variablePath) => { - const nodeId = variablePath.split('.')[0] + const valueWithoutAgentVars = value.replace(AGENT_CONTEXT_VAR_PATTERN, (match) => { + const nodeId = getAgentNodeIdFromContextVar(match) return nodeId === agentNodeId ? '' : match }) @@ -552,6 +560,7 @@ const MixedVariableTextInput = ({ @@ -599,10 +608,21 @@ const MixedVariableTextInput = ({ }} /> )} - {toolNodeId && detectedAgentFromValue && sourceVariable && ( + {toolNodeId && paramKey && isAssembleValue && ( + )} + {toolNodeId && paramKey && !isAssembleValue && detectedAgentFromValue && sourceVariable && ( + = ({ - isOpen, - onClose, - toolNodeId, - paramKey, - sourceVariable, - agentName, - agentNodeId, -}) => { +const SubGraphModal: FC = (props) => { const { t } = useTranslation() + const { isOpen, onClose, variant, toolNodeId, paramKey } = props + const isAgentVariant = variant === 'agent' + const resolvedAgentNodeId = isAgentVariant ? props.agentNodeId : '' + const agentName = isAgentVariant ? props.agentName : '' + const assembleTitle = !isAgentVariant ? props.title : '' + const modalTitle = useMemo(() => { + const baseTitle = isAgentVariant + ? agentName + : (assembleTitle || t('nodes.tool.assembleVariables', { ns: 'workflow' })) + const prefix = isAgentVariant && baseTitle ? '@' : '' + return `${prefix}${baseTitle} ${t('subGraphModal.title', { ns: 'workflow' })}`.trim() + }, [agentName, assembleTitle, isAgentVariant, t]) const reactflowStore = useStoreApi() const workflowNodes = useWorkflowStore(state => state.nodes) const workflowEdges = useReactFlowStore(state => state.edges) @@ -41,13 +47,16 @@ const SubGraphModal: FC = ({ const extractorNodeId = `${toolNodeId}_ext_${paramKey}` const extractorNode = useMemo(() => { - return workflowNodes.find(node => node.id === extractorNodeId) as Node | undefined + return workflowNodes.find(node => node.id === extractorNodeId) as Node | undefined }, [extractorNodeId, workflowNodes]) const toolNode = useMemo(() => { return workflowNodes.find(node => node.id === toolNodeId) }, [toolNodeId, workflowNodes]) const toolParam = (toolNode?.data as ToolNodeType | undefined)?.tool_parameters?.[paramKey] const toolParamValue = toolParam?.value as string | undefined + const assemblePlaceholder = useMemo(() => { + return `{{#${toolNodeId}_ext_${paramKey}.result#}}` + }, [paramKey, toolNodeId]) const parentBeforeNodes = useMemo(() => { if (!isOpen) @@ -56,25 +65,28 @@ const SubGraphModal: FC = ({ }, [getBeforeNodesInSameBranch, isOpen, toolNodeId, workflowEdges, workflowNodes]) const parentContextNodes = useMemo(() => { - if (!parentBeforeNodes.length) + if (!parentBeforeNodes.length || !isAgentVariant) return [] return parentBeforeNodes.filter(node => node.data.type === BlockEnum.Agent || node.data.type === BlockEnum.LLM) - }, [parentBeforeNodes]) + }, [isAgentVariant, parentBeforeNodes]) - const parentContextNodeIds = useMemo(() => { - return parentContextNodes.map(node => node.id) - }, [parentContextNodes]) + const parentAvailableNodes = useMemo(() => { + if (!isOpen) + return [] + return isAgentVariant ? parentContextNodes : parentBeforeNodes + }, [isAgentVariant, isOpen, parentBeforeNodes, parentContextNodes]) const parentAvailableVars = useMemo(() => { - if (!parentContextNodeIds.length) + if (!parentAvailableNodes.length) return [] const vars = getNodeAvailableVars({ - beforeNodes: parentContextNodes, + beforeNodes: parentAvailableNodes, isChatMode, filterVar: () => true, }) - return vars.filter(nodeVar => parentContextNodeIds.includes(nodeVar.nodeId)) - }, [getNodeAvailableVars, isChatMode, parentContextNodeIds, parentContextNodes]) + const availableNodeIds = new Set(parentAvailableNodes.map(node => node.id)) + return vars.filter(nodeVar => availableNodeIds.has(nodeVar.nodeId)) + }, [getNodeAvailableVars, isChatMode, parentAvailableNodes]) const mentionConfig = useMemo(() => { const current = toolParam?.mention_config @@ -91,6 +103,9 @@ const SubGraphModal: FC = ({ }, [extractorNodeId, paramKey, toolParam?.mention_config]) const handleMentionConfigChange = useCallback((config: MentionConfig) => { + if (!isAgentVariant) + return + const { getNodes, setNodes } = reactflowStore.getState() const nextNodes = getNodes().map((node) => { if (node.id !== toolNodeId) @@ -118,10 +133,10 @@ const SubGraphModal: FC = ({ }) setNodes(nextNodes) handleSyncWorkflowDraft() - }, [handleSyncWorkflowDraft, paramKey, reactflowStore, toolNodeId]) + }, [handleSyncWorkflowDraft, isAgentVariant, paramKey, reactflowStore, toolNodeId]) useEffect(() => { - if (!toolParam || (toolParam.type && toolParam.type !== VarKindType.mention)) + if (!isAgentVariant || !toolParam || (toolParam.type && toolParam.type !== VarKindType.mention)) return const current = toolParam.mention_config @@ -132,7 +147,7 @@ const SubGraphModal: FC = ({ if (needsExtractor || needsNullStrategy || needsOutputSelector || needsDefaultValue) handleMentionConfigChange(mentionConfig) - }, [handleMentionConfigChange, mentionConfig, toolParam]) + }, [handleMentionConfigChange, isAgentVariant, mentionConfig, toolParam]) const getUserPromptText = useCallback((promptTemplate?: PromptTemplateItem[] | PromptItem) => { if (!promptTemplate) @@ -156,23 +171,46 @@ const SubGraphModal: FC = ({ // TODO: handle external workflow updates while sub-graph modal is open. const handleSave = useCallback((subGraphNodes: Node[]) => { - const extractorNodeData = subGraphNodes.find(node => node.id === extractorNodeId) as Node | undefined + const extractorNodeData = subGraphNodes.find(node => node.id === extractorNodeId) as Node | undefined if (!extractorNodeData) return - const userPromptText = getUserPromptText(extractorNodeData.data?.prompt_template) - const placeholder = `{{@${agentNodeId}.context@}}` - const nextValue = `${placeholder}${userPromptText}` + const ensureAssembleOutputs = (payload: CodeNodeType) => { + const outputs = payload.outputs || {} + if (outputs.result) + return payload + return { + ...payload, + outputs: { + ...outputs, + result: { + type: VarType.string, + children: null, + }, + }, + } + } + + const userPromptText = isAgentVariant + ? getUserPromptText((extractorNodeData.data as LLMNodeType).prompt_template) + : '' + const placeholder = isAgentVariant && resolvedAgentNodeId ? `{{@${resolvedAgentNodeId}.context@}}` : '' + const nextValue = isAgentVariant + ? `${placeholder}${userPromptText}` + : assemblePlaceholder const { getNodes, setNodes } = reactflowStore.getState() const nextNodes = getNodes().map((node) => { if (node.id === extractorNodeId) { + const nextData = isAgentVariant + ? extractorNodeData.data + : ensureAssembleOutputs(extractorNodeData.data as CodeNodeType) return { ...node, hidden: true, data: { ...node.data, - ...extractorNodeData.data, + ...nextData, parent_node_id: toolNodeId, }, } @@ -200,7 +238,7 @@ const SubGraphModal: FC = ({ }) setNodes(nextNodes) setControlPromptEditorRerenderKey(Date.now()) - }, [agentNodeId, extractorNodeId, getUserPromptText, paramKey, reactflowStore, setControlPromptEditorRerenderKey, toolNodeId]) + }, [assemblePlaceholder, extractorNodeId, getUserPromptText, isAgentVariant, paramKey, reactflowStore, resolvedAgentNodeId, setControlPromptEditorRerenderKey, toolNodeId]) return ( @@ -215,13 +253,12 @@ const SubGraphModal: FC = ({
- + {isAgentVariant + ? + : }
- @ - {agentName} - {' '} - {t('subGraphModal.title', { ns: 'workflow' })} + {modalTitle}
- + {variant === 'agent' + ? ( + | undefined} + toolParamValue={toolParamValue} + parentAvailableNodes={parentAvailableNodes} + parentAvailableVars={parentAvailableVars} + onSave={handleSave} + onSyncWorkflowDraft={doSyncWorkflowDraft} + /> + ) + : ( + | undefined} + toolParamValue={toolParamValue} + parentAvailableNodes={parentAvailableNodes} + parentAvailableVars={parentAvailableVars} + onSave={handleSave} + onSyncWorkflowDraft={doSyncWorkflowDraft} + /> + )}
diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx index c8f3b59708..a9e9e2565d 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/sub-graph-canvas.tsx @@ -4,40 +4,10 @@ import type { SubGraphCanvasProps } from './types' import { memo } from 'react' import SubGraph from '@/app/components/sub-graph' -const SubGraphCanvas: FC = ({ - toolNodeId, - paramKey, - sourceVariable, - agentNodeId, - agentName, - configsMap, - mentionConfig, - onMentionConfigChange, - extractorNode, - toolParamValue, - parentAvailableNodes, - parentAvailableVars, - onSave, - onSyncWorkflowDraft, -}) => { +const SubGraphCanvas: FC = (props) => { return (
- +
) } diff --git a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts index a6ed7b9a8f..8a29b402d1 100644 --- a/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts +++ b/web/app/components/workflow/nodes/tool/components/sub-graph-modal/types.ts @@ -1,34 +1,25 @@ -import type { SyncWorkflowDraft } from '@/app/components/sub-graph/types' -import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store' -import type { MentionConfig } from '@/app/components/workflow/nodes/_base/types' -import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' -import type { NodeOutPutVar, Edge as WorkflowEdge, Node as WorkflowNode } from '@/app/components/workflow/types' +import type { SubGraphProps } from '@/app/components/sub-graph/types' +import type { ValueSelector } from '@/app/components/workflow/types' -type WorkflowValueSelector = string[] - -export type SubGraphModalProps = { +type BaseSubGraphModalProps = { isOpen: boolean onClose: () => void toolNodeId: string paramKey: string - sourceVariable: WorkflowValueSelector +} + +type AgentSubGraphModalProps = BaseSubGraphModalProps & { + variant: 'agent' + sourceVariable: ValueSelector agentName: string agentNodeId: string } -export type SubGraphCanvasProps = { - toolNodeId: string - paramKey: string - sourceVariable: WorkflowValueSelector - agentNodeId: string - agentName: string - configsMap?: HooksStoreShape['configsMap'] - mentionConfig: MentionConfig - onMentionConfigChange: (config: MentionConfig) => void - extractorNode?: WorkflowNode - toolParamValue?: string - parentAvailableNodes?: WorkflowNode[] - parentAvailableVars?: NodeOutPutVar[] - onSave?: (nodes: WorkflowNode[], edges: WorkflowEdge[]) => void - onSyncWorkflowDraft?: SyncWorkflowDraft +type AssembleSubGraphModalProps = BaseSubGraphModalProps & { + variant: 'assemble' + title: string } + +export type SubGraphModalProps = AgentSubGraphModalProps | AssembleSubGraphModalProps + +export type SubGraphCanvasProps = SubGraphProps diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index 555bc1218f..0b20f30333 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -20,7 +20,14 @@ import { useStrategyProviders } from '@/service/use-strategy' import { cn } from '@/utils/classnames' import { VarType } from './types' -const AGENT_CONTEXT_VAR_PATTERN = /\{\{@([^.@#]+)\.context@\}\}/g +const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g +const AGENT_CONTEXT_VAR_PREFIX = '{{@' +const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}' +const getAgentNodeIdFromContextVar = (placeholder: string) => { + if (!placeholder.startsWith(AGENT_CONTEXT_VAR_PREFIX) || !placeholder.endsWith(AGENT_CONTEXT_VAR_SUFFIX)) + return '' + return placeholder.slice(AGENT_CONTEXT_VAR_PREFIX.length, -AGENT_CONTEXT_VAR_SUFFIX.length) +} type AgentCheckValidContext = { provider?: StrategyPluginDetail strategy?: StrategyDetail @@ -80,7 +87,7 @@ const Node: FC> = ({ return const matches = value.matchAll(AGENT_CONTEXT_VAR_PATTERN) for (const match of matches) { - const agentNodeId = match[1] + const agentNodeId = getAgentNodeIdFromContextVar(match[0]) if (!agentNodeId) continue const entryKey = `${paramKey}:${agentNodeId}` diff --git a/web/app/components/workflow/utils/elk-layout.ts b/web/app/components/workflow/utils/elk-layout.ts index c3b37c8f16..c0cf8543df 100644 --- a/web/app/components/workflow/utils/elk-layout.ts +++ b/web/app/components/workflow/utils/elk-layout.ts @@ -13,6 +13,7 @@ import { } from '@/app/components/workflow/constants' import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants' import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants' +import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants' import { BlockEnum, } from '@/app/components/workflow/types' @@ -442,6 +443,7 @@ const normaliseChildLayout = ( const startNode = nodes.find(node => node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_LOOP_START_NODE + || node.type === CUSTOM_SUB_GRAPH_START_NODE || node.data?.type === BlockEnum.LoopStart || node.data?.type === BlockEnum.IterationStart, ) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/sub-graph-start/index.tsx b/web/app/components/workflow/workflow-preview/components/nodes/sub-graph-start/index.tsx new file mode 100644 index 0000000000..aa0082b8c1 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/sub-graph-start/index.tsx @@ -0,0 +1,60 @@ +import type { NodeProps } from 'reactflow' +import type { CommonNodeType } from '@/app/components/workflow/types' +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { AssembleVariablesAlt } from '@/app/components/base/icons/src/vender/line/general' +import { Agent } from '@/app/components/base/icons/src/vender/workflow' +import Tooltip from '@/app/components/base/tooltip' +import { cn } from '@/utils/classnames' +import { NodeSourceHandle } from '../../node-handle' + +type SubGraphStartNodeData = CommonNodeType<{ + tooltip?: string + iconType?: string +}> + +type IconComponent = typeof Agent + +const iconMap: Record = { + agent: Agent, + assemble: AssembleVariablesAlt, +} + +const SubGraphStartNode = ({ id, data }: NodeProps) => { + const { t } = useTranslation() + const iconType = data?.iconType || 'agent' + const Icon = iconMap[iconType] || Agent + const rawTitle = data?.title?.trim() || '' + const showTitle = iconType === 'agent' && !!rawTitle + const displayTitle = showTitle && (rawTitle.startsWith('@') ? rawTitle : `@${rawTitle}`) + const tooltip = data?.tooltip + || (iconType === 'assemble' ? t('blocks.start', { ns: 'workflow' }) : (data?.title || t('blocks.start', { ns: 'workflow' }))) + + return ( +
+ +
+ +
+
+ {showTitle && ( + + {displayTitle} + + )} + +
+ ) +} + +export default memo(SubGraphStartNode) diff --git a/web/app/components/workflow/workflow-preview/index.tsx b/web/app/components/workflow/workflow-preview/index.tsx index 8f61c2cfb6..7e7b2c271c 100644 --- a/web/app/components/workflow/workflow-preview/index.tsx +++ b/web/app/components/workflow/workflow-preview/index.tsx @@ -29,6 +29,7 @@ import { import CustomConnectionLine from '@/app/components/workflow/custom-connection-line' import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants' import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants' +import { CUSTOM_SUB_GRAPH_START_NODE } from '@/app/components/workflow/nodes/sub-graph-start/constants' import { CUSTOM_NOTE_NODE } from '@/app/components/workflow/note-node/constants' import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants' import { @@ -40,6 +41,7 @@ import CustomEdge from './components/custom-edge' import CustomNode from './components/nodes' import IterationStartNode from './components/nodes/iteration-start' import LoopStartNode from './components/nodes/loop-start' +import SubGraphStartNode from './components/nodes/sub-graph-start' import CustomNoteNode from './components/note-node' import ZoomInOut from './components/zoom-in-out' import 'reactflow/dist/style.css' @@ -49,6 +51,7 @@ const nodeTypes = { [CUSTOM_NODE]: CustomNode, [CUSTOM_NOTE_NODE]: CustomNoteNode, [CUSTOM_SIMPLE_NODE]: CustomNode, + [CUSTOM_SUB_GRAPH_START_NODE]: SubGraphStartNode, [CUSTOM_ITERATION_START_NODE]: IterationStartNode, [CUSTOM_LOOP_START_NODE]: LoopStartNode, }