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.
This commit is contained in:
zhsama
2026-01-13 16:40:34 +08:00
parent 9b961fb41e
commit ddbbddbd14
10 changed files with 155 additions and 89 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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<React.JSX.Element>
}
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 {

View File

@@ -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<StartNodeType> = {
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<LLMNodeType> = {
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<StartNodeType> = {
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<LLMNodeType> = {
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 {

View File

@@ -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<Props> = ({
}
}
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,
},
})
}

View File

@@ -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<string, {
type: VarKindType
value?: string | ValueSelector | any
mention_config?: MentionConfig
}>
// Base resource interface

View File

@@ -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={<Placeholder disableVariableInsertion={disableVariableInsertion} hasSelectedAgent={!!detectedAgentFromValue} />}
onChange={onChange}
onChange={text => onChange?.(text)}
/>
{toolNodeId && detectedAgentFromValue && sourceVariable && (
<SubGraphModal

View File

@@ -32,7 +32,7 @@ const useSingleRunFormParams = ({
const { inputs } = useNodeCrud<ToolNodeType>(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)

View File

@@ -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)

View File

@@ -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)