From ddbbddbd14da4ca140a85164136199714cb0e84e Mon Sep 17 00:00:00 2001 From: zhsama Date: Tue, 13 Jan 2026 16:40:34 +0800 Subject: [PATCH] refactor: Update variable syntax to support agent context markers Extend variable pattern matching to support both `#` and `@` markers, with `@` specifically used for agent context variables. Update regex patterns, text processing logic, and add sub-graph persistence for agent variable handling. --- .../components/app/overview/trigger-card.tsx | 1 - .../base/prompt-editor/constants.tsx | 7 +- .../plugins/workflow-variable-block/node.tsx | 7 +- .../sub-graph/hooks/use-sub-graph-init.ts | 147 ++++++++++-------- .../_base/components/form-input-item.tsx | 20 ++- .../components/workflow/nodes/_base/types.ts | 8 + .../mixed-variable-text-input/index.tsx | 48 ++++-- .../nodes/tool/use-single-run-form-params.ts | 2 +- web/config/index.spec.ts | 2 + web/config/index.ts | 2 +- 10 files changed, 155 insertions(+), 89 deletions(-) diff --git a/web/app/components/app/overview/trigger-card.tsx b/web/app/components/app/overview/trigger-card.tsx index a2d28606a1..c8f12745bd 100644 --- a/web/app/components/app/overview/trigger-card.tsx +++ b/web/app/components/app/overview/trigger-card.tsx @@ -14,7 +14,6 @@ import { BlockEnum } from '@/app/components/workflow/types' import { useAppContext } from '@/context/app-context' import { useDocLink } from '@/context/i18n' import { - useAppTriggers, useInvalidateAppTriggers, useUpdateTriggerStatus, diff --git a/web/app/components/base/prompt-editor/constants.tsx b/web/app/components/base/prompt-editor/constants.tsx index d6b8e9fcb4..9fcf445bfb 100644 --- a/web/app/components/base/prompt-editor/constants.tsx +++ b/web/app/components/base/prompt-editor/constants.tsx @@ -38,13 +38,16 @@ export const getInputVars = (text: string): ValueSelector[] => { if (!text || typeof text !== 'string') return [] - const allVars = text.match(/\{\{#([^#]*)#\}\}/g) + const allVars = text.match(/\{\{[@#]([^@#]*)[@#]\}\}/g) if (allVars && allVars?.length > 0) { // {{#context#}}, {{#query#}} is not input vars const inputVars = allVars .filter(item => item.includes('.')) .map((item) => { - const valueSelector = item.replace('{{#', '').replace('#}}', '').split('.') + const valueSelector = item + .replace(/^\{\{[@#]/, '') + .replace(/[@#]\}\}$/, '') + .split('.') if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0])) return valueSelector.slice(1) diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx index a241e75233..75ceb82f2d 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx @@ -2,6 +2,7 @@ import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical' import type { GetVarType, WorkflowVariableBlockType } from '../../types' import type { Var } from '@/app/components/workflow/types' import { DecoratorNode } from 'lexical' +import { BlockEnum } from '@/app/components/workflow/types' import WorkflowVariableBlockComponent from './component' export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap'] @@ -120,7 +121,11 @@ export class WorkflowVariableBlockNode extends DecoratorNode } getTextContent(): string { - return `{{#${this.getVariables().join('.')}#}}` + const variables = this.getVariables() + const node = this.getWorkflowNodesMap()?.[variables[0]] + const isAgentContextVariable = node?.type === BlockEnum.Agent && variables[variables.length - 1] === 'context' + const marker = isAgentContextVariable ? '@' : '#' + return `{{${marker}${variables.join('.')}${marker}}}` } } export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[], ragVariables?: Var[]): WorkflowVariableBlockNode { diff --git a/web/app/components/sub-graph/hooks/use-sub-graph-init.ts b/web/app/components/sub-graph/hooks/use-sub-graph-init.ts index ba6f391a83..68d1e1be20 100644 --- a/web/app/components/sub-graph/hooks/use-sub-graph-init.ts +++ b/web/app/components/sub-graph/hooks/use-sub-graph-init.ts @@ -1,86 +1,97 @@ import type { SubGraphProps } from '../types' import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' -import type { Edge, Node } from '@/app/components/workflow/types' +import type { Edge, Node, ValueSelector } from '@/app/components/workflow/types' import { useMemo } from 'react' import { BlockEnum, PromptRole } from '@/app/components/workflow/types' import { AppModeEnum } from '@/types/app' -const SUBGRAPH_SOURCE_NODE_ID = 'subgraph-source' -const SUBGRAPH_LLM_NODE_ID = 'subgraph-llm' +export const SUBGRAPH_SOURCE_NODE_ID = 'subgraph-source' +export const SUBGRAPH_LLM_NODE_ID = 'subgraph-llm' + +export const getSubGraphInitialNodes = ( + sourceVariable: ValueSelector, + agentName: string, +): Node[] => { + const sourceVarName = sourceVariable.length > 1 + ? sourceVariable.slice(1).join('.') + : 'output' + + const startNode: Node = { + id: SUBGRAPH_SOURCE_NODE_ID, + type: 'custom', + position: { x: 100, y: 150 }, + data: { + type: BlockEnum.Start, + title: `${agentName}: ${sourceVarName}`, + desc: 'Source variable from agent', + _connectedSourceHandleIds: ['source'], + _connectedTargetHandleIds: [], + variables: [], + }, + } + + const llmNode: Node = { + id: SUBGRAPH_LLM_NODE_ID, + type: 'custom', + position: { x: 450, y: 150 }, + data: { + type: BlockEnum.LLM, + title: 'LLM', + desc: 'Transform the output', + _connectedSourceHandleIds: [], + _connectedTargetHandleIds: ['target'], + model: { + provider: '', + name: '', + mode: AppModeEnum.CHAT, + completion_params: { + temperature: 0.7, + }, + }, + prompt_template: [{ + role: PromptRole.system, + text: '', + }], + context: { + enabled: false, + variable_selector: [], + }, + vision: { + enabled: false, + }, + }, + } + + return [startNode, llmNode] +} + +export const getSubGraphInitialEdges = (): Edge[] => { + return [ + { + id: `${SUBGRAPH_SOURCE_NODE_ID}-${SUBGRAPH_LLM_NODE_ID}`, + source: SUBGRAPH_SOURCE_NODE_ID, + sourceHandle: 'source', + target: SUBGRAPH_LLM_NODE_ID, + targetHandle: 'target', + type: 'custom', + data: { + sourceType: BlockEnum.Start, + targetType: BlockEnum.LLM, + }, + }, + ] +} export const useSubGraphInit = (props: SubGraphProps) => { const { sourceVariable, agentName } = props const initialNodes = useMemo((): Node[] => { - const sourceVarName = sourceVariable.length > 1 - ? sourceVariable.slice(1).join('.') - : 'output' - - const startNode: Node = { - id: SUBGRAPH_SOURCE_NODE_ID, - type: 'custom', - position: { x: 100, y: 150 }, - data: { - type: BlockEnum.Start, - title: `${agentName}: ${sourceVarName}`, - desc: 'Source variable from agent', - _connectedSourceHandleIds: ['source'], - _connectedTargetHandleIds: [], - variables: [], - }, - } - - const llmNode: Node = { - id: SUBGRAPH_LLM_NODE_ID, - type: 'custom', - position: { x: 450, y: 150 }, - data: { - type: BlockEnum.LLM, - title: 'LLM', - desc: 'Transform the output', - _connectedSourceHandleIds: [], - _connectedTargetHandleIds: ['target'], - model: { - provider: '', - name: '', - mode: AppModeEnum.CHAT, - completion_params: { - temperature: 0.7, - }, - }, - prompt_template: [{ - role: PromptRole.system, - text: '', - }], - context: { - enabled: false, - variable_selector: [], - }, - vision: { - enabled: false, - }, - }, - } - - return [startNode, llmNode] + return getSubGraphInitialNodes(sourceVariable, agentName) }, [sourceVariable, agentName]) const initialEdges = useMemo((): Edge[] => { - return [ - { - id: `${SUBGRAPH_SOURCE_NODE_ID}-${SUBGRAPH_LLM_NODE_ID}`, - source: SUBGRAPH_SOURCE_NODE_ID, - sourceHandle: 'source', - target: SUBGRAPH_LLM_NODE_ID, - targetHandle: 'target', - type: 'custom', - data: { - sourceType: BlockEnum.Start, - targetType: BlockEnum.LLM, - }, - }, - ] + return getSubGraphInitialEdges() }, []) return { diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx index 99789d8afe..661c69c4dc 100644 --- a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import type { ResourceVarInputs } from '../types' +import type { MentionConfig, ResourceVarInputs } from '../types' import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Event, Tool } from '@/app/components/tools/types' import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' @@ -233,13 +233,25 @@ const FormInputItem: FC = ({ } } - const handleValueChange = (newValue: any, newType?: VarKindType) => { + const handleValueChange = (newValue: any, newType?: VarKindType, mentionConfig?: MentionConfig | null) => { + const normalizedValue = isNumber ? Number.parseFloat(newValue) : newValue + const resolvedType = newType ?? (varInput?.type === VarKindType.mention ? VarKindType.mention : getVarKindType()) + const resolvedMentionConfig = resolvedType === VarKindType.mention + ? (mentionConfig ?? varInput?.mention_config ?? { + extractor_node_id: '', + output_selector: [], + null_strategy: 'use_default', + default_value: '', + }) + : undefined + onChange({ ...value, [variable]: { ...varInput, - type: newType ?? getVarKindType(), - value: isNumber ? Number.parseFloat(newValue) : newValue, + type: resolvedType, + value: normalizedValue, + mention_config: resolvedMentionConfig, }, }) } diff --git a/web/app/components/workflow/nodes/_base/types.ts b/web/app/components/workflow/nodes/_base/types.ts index 8f15c89881..f3c64656b5 100644 --- a/web/app/components/workflow/nodes/_base/types.ts +++ b/web/app/components/workflow/nodes/_base/types.ts @@ -8,10 +8,18 @@ export enum VarKindType { mention = 'mention', } +export type MentionConfig = { + extractor_node_id: string + output_selector: ValueSelector + null_strategy: 'raise_error' | 'use_default' + default_value: unknown +} + // Generic resource variable inputs export type ResourceVarInputs = Record // Base resource interface 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 d0beeabb0c..1f8ea1adb5 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 @@ -1,5 +1,5 @@ import type { AgentNode } from '@/app/components/base/prompt-editor/types' -import type { VarKindType } from '@/app/components/workflow/nodes/_base/types' +import type { MentionConfig, VarKindType } from '@/app/components/workflow/nodes/_base/types' import type { Node, NodeOutPutVar, @@ -13,6 +13,8 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import PromptEditor from '@/app/components/base/prompt-editor' +import { useSubGraphPersistence } from '@/app/components/sub-graph/hooks' +import { getSubGraphInitialEdges, getSubGraphInitialNodes } from '@/app/components/sub-graph/hooks/use-sub-graph-init' import { VarKindType as VarKindTypeEnum } from '@/app/components/workflow/nodes/_base/types' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' @@ -22,17 +24,24 @@ import AgentHeaderBar from './agent-header-bar' import Placeholder from './placeholder' /** - * Matches agent context variable syntax: {{#nodeId.context#}} - * Example: {{#agent-123.context#}} -> captures "agent-123" + * Matches agent context variable syntax: {{@nodeId.context@}} + * Example: {{@agent-123.context@}} -> captures "agent-123" */ -const AGENT_CONTEXT_VAR_PATTERN = /\{\{#([^.#]+)\.context#\}\}/g +const AGENT_CONTEXT_VAR_PATTERN = /\{\{[@#]([^.@#]+)\.context[@#]\}\}/g + +const DEFAULT_MENTION_CONFIG: MentionConfig = { + extractor_node_id: '', + output_selector: [], + null_strategy: 'use_default', + default_value: '', +} type MixedVariableTextInputProps = { readOnly?: boolean nodesOutputVars?: NodeOutPutVar[] availableNodes?: Node[] value?: string - onChange?: (text: string, type?: VarKindType) => void + onChange?: (text: string, type?: VarKindType, mentionConfig?: MentionConfig | null) => void showManageInputField?: boolean onManageInputField?: () => void disableVariableInsertion?: boolean @@ -57,6 +66,15 @@ const MixedVariableTextInput = ({ const setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey) const [isSubGraphModalOpen, setIsSubGraphModalOpen] = useState(false) + const { + loadSubGraphData, + updateSubGraphNodes, + clearSubGraphData, + } = useSubGraphPersistence({ + toolNodeId: toolNodeId || '', + paramKey: paramKey || '', + }) + const nodesByIdMap = useMemo(() => { return availableNodes.reduce((acc, node) => { acc[node.id] = node @@ -107,20 +125,28 @@ const MixedVariableTextInput = ({ return nodeId === agentNodeId ? '' : match }).trim() - onChange(valueWithoutAgentVars, VarKindTypeEnum.mixed) + onChange(valueWithoutAgentVars, VarKindTypeEnum.mixed, null) + if (toolNodeId && paramKey) + clearSubGraphData() setControlPromptEditorRerenderKey(Date.now()) - }, [detectedAgentFromValue?.nodeId, value, onChange, setControlPromptEditorRerenderKey]) + }, [clearSubGraphData, detectedAgentFromValue?.nodeId, onChange, paramKey, setControlPromptEditorRerenderKey, toolNodeId, value]) const handleAgentSelect = useCallback((agent: AgentNode) => { if (!onChange) return const valueWithoutTrigger = value.replace(/@$/, '') - const newValue = `{{#${agent.id}.context#}}${valueWithoutTrigger}` + const newValue = `{{@${agent.id}.context@}}${valueWithoutTrigger}` - onChange(newValue, VarKindTypeEnum.mention) + if (toolNodeId && paramKey && !loadSubGraphData()) { + const initialNodes = getSubGraphInitialNodes([agent.id, 'context'], agent.title) + const initialEdges = getSubGraphInitialEdges() + updateSubGraphNodes(initialNodes, initialEdges) + } + + onChange(newValue, VarKindTypeEnum.mention, DEFAULT_MENTION_CONFIG) setControlPromptEditorRerenderKey(Date.now()) - }, [value, onChange, setControlPromptEditorRerenderKey]) + }, [loadSubGraphData, onChange, paramKey, setControlPromptEditorRerenderKey, toolNodeId, updateSubGraphNodes, value]) const handleOpenSubGraphModal = useCallback(() => { setIsSubGraphModalOpen(true) @@ -179,7 +205,7 @@ const MixedVariableTextInput = ({ onSelect: handleAgentSelect, }} placeholder={} - onChange={onChange} + onChange={text => onChange?.(text)} /> {toolNodeId && detectedAgentFromValue && sourceVariable && ( (id, payload) const hadVarParams = Object.keys(inputs.tool_parameters) - .filter(key => inputs.tool_parameters[key].type !== VarType.constant) + .filter(key => ![VarType.constant, VarType.mention].includes(inputs.tool_parameters[key].type)) .map(k => inputs.tool_parameters[k]) const hadVarSettings = Object.keys(inputs.tool_configurations) diff --git a/web/config/index.spec.ts b/web/config/index.spec.ts index 7b1d91186d..e03ee92dfd 100644 --- a/web/config/index.spec.ts +++ b/web/config/index.spec.ts @@ -70,6 +70,8 @@ describe('config test', () => { // rag variables '{{#rag.1748945155129.a#}}', '{{#rag.shared.bbb#}}', + '{{@1749783300519.llm.a@}}', + '{{@sys.query@}}', ] vars.forEach((variable) => { expect(VAR_REGEX.test(variable)).toBe(true) diff --git a/web/config/index.ts b/web/config/index.ts index b804629048..812076404c 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -340,7 +340,7 @@ Thought: {{agent_scratchpad}} } export const VAR_REGEX - = /\{\{(#[\w-]{1,50}(\.\d+)?(\.[a-z_]\w{0,29}){1,10}#)\}\}/gi + = /\{\{([#@])[\w-]{1,50}(\.\d+)?(\.[a-z_]\w{0,29}){1,10}\1\}\}/gi export const resetReg = () => (VAR_REGEX.lastIndex = 0)