From 54fce5e903c4b6a57b5daed9748bc4878cee187e Mon Sep 17 00:00:00 2001 From: zhsama Date: Tue, 27 Jan 2026 02:41:57 +0800 Subject: [PATCH] feat: Add @agent icon and implement agent alias variables in workflow inspector --- .../icons/src/vender/workflow/AtSign.json | 26 ++ .../base/icons/src/vender/workflow/AtSign.tsx | 20 ++ .../base/icons/src/vender/workflow/index.ts | 1 + .../sub-graph/components/sub-graph-main.tsx | 2 + .../hooks/inspect-vars-agent-alias.ts | 265 ++++++++++++++++++ .../hooks/use-fetch-workflow-inspect-vars.ts | 9 +- .../hooks/use-inspect-vars-crud-common.ts | 20 +- .../hooks/use-mixed-variable-extractor.ts | 10 +- .../components/workflow/nodes/tool/node.tsx | 9 +- .../workflow/utils/agent-context.ts | 9 + .../workflow/variable-inspect/group.tsx | 45 +-- .../workflow/variable-inspect/left.tsx | 4 +- .../workflow/variable-inspect/panel.tsx | 78 +++--- .../workflow/variable-inspect/right.tsx | 64 +++-- .../workflow/variable-inspect/utils.tsx | 32 +++ web/types/workflow.ts | 8 + 16 files changed, 492 insertions(+), 110 deletions(-) create mode 100644 web/app/components/base/icons/src/vender/workflow/AtSign.json create mode 100644 web/app/components/base/icons/src/vender/workflow/AtSign.tsx create mode 100644 web/app/components/workflow/hooks/inspect-vars-agent-alias.ts create mode 100644 web/app/components/workflow/utils/agent-context.ts diff --git a/web/app/components/base/icons/src/vender/workflow/AtSign.json b/web/app/components/base/icons/src/vender/workflow/AtSign.json new file mode 100644 index 0000000000..5ed6d9537f --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/AtSign.json @@ -0,0 +1,26 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "14", + "height": "14", + "viewBox": "0 0 14 14", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M11.6665 7.00008C11.6665 4.42275 9.57718 2.33341 6.99984 2.33341C4.42251 2.33341 2.33317 4.42275 2.33317 7.00008C2.33317 9.57742 4.42251 11.6667 6.99984 11.6667C7.95755 11.6667 8.8479 11.3782 9.58873 10.8834L10.2359 11.8542C9.30995 12.4728 8.19701 12.8334 6.99984 12.8334C3.77817 12.8334 1.1665 10.2217 1.1665 7.00008C1.1665 3.77842 3.77817 1.16675 6.99984 1.16675C10.2215 1.16675 12.8332 3.77842 12.8332 7.00008V7.87508C12.8332 9.00266 11.9191 9.91675 10.7915 9.91675C10.0891 9.91675 9.46944 9.56196 9.10205 9.02186C8.57145 9.57346 7.82572 9.91675 6.99984 9.91675C5.38901 9.91675 4.08317 8.6109 4.08317 7.00008C4.08317 5.38925 5.38901 4.08341 6.99984 4.08341C7.65655 4.08341 8.26258 4.30047 8.75013 4.66675H9.9165V7.87508C9.9165 8.35831 10.3083 8.75008 10.7915 8.75008C11.2747 8.75008 11.6665 8.35831 11.6665 7.87508V7.00008ZM6.99984 5.25008C6.03331 5.25008 5.24984 6.03356 5.24984 7.00008C5.24984 7.96661 6.03331 8.75008 6.99984 8.75008C7.96636 8.75008 8.74984 7.96661 8.74984 7.00008C8.74984 6.03356 7.96636 5.25008 6.99984 5.25008Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "AtSign" +} diff --git a/web/app/components/base/icons/src/vender/workflow/AtSign.tsx b/web/app/components/base/icons/src/vender/workflow/AtSign.tsx new file mode 100644 index 0000000000..c055644879 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/AtSign.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 './AtSign.json' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject> + }, +) => + +Icon.displayName = 'AtSign' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index 9a989881bd..5813a49ed7 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/web/app/components/base/icons/src/vender/workflow/index.ts @@ -3,6 +3,7 @@ export { default as Answer } from './Answer' export { default as ApiAggregate } from './ApiAggregate' export { default as Assigner } from './Assigner' export { default as Asterisk } from './Asterisk' +export { default as AtSign } from './AtSign' export { default as CalendarCheckLine } from './CalendarCheckLine' export { default as Code } from './Code' export { default as Datasource } from './Datasource' diff --git a/web/app/components/sub-graph/components/sub-graph-main.tsx b/web/app/components/sub-graph/components/sub-graph-main.tsx index c81e1dde62..52f1e64bbc 100644 --- a/web/app/components/sub-graph/components/sub-graph-main.tsx +++ b/web/app/components/sub-graph/components/sub-graph-main.tsx @@ -58,10 +58,12 @@ const SubGraphMain: FC = (props) => { const { fetchInspectVars } = useSetWorkflowVarsWithValue({ flowType, flowId, + interactionMode: InteractionMode.Subgraph, }) const inspectVarsCrud = useInspectVarsCrudCommon({ flowType, flowId, + interactionMode: InteractionMode.Subgraph, }) const handleSyncSubGraphDraft = useCallback(async () => { diff --git a/web/app/components/workflow/hooks/inspect-vars-agent-alias.ts b/web/app/components/workflow/hooks/inspect-vars-agent-alias.ts new file mode 100644 index 0000000000..0b970ab77e --- /dev/null +++ b/web/app/components/workflow/hooks/inspect-vars-agent-alias.ts @@ -0,0 +1,265 @@ +import type { Node } from '@/app/components/workflow/types' +import type { NodeWithVar, VarInInspect } from '@/types/workflow' +import { BlockEnum, VarType } from '@/app/components/workflow/types' +import { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context' +import { VarInInspectType } from '@/types/workflow' + +const buildAliasVarId = (mapping: AgentAliasMapping) => { + const outputPath = mapping.outputSelector.join('.') + return `alias:${mapping.parentNodeId}:${mapping.extractorNodeId}:${outputPath}` +} + +type AgentAliasMapping = { + parentNodeId: string + extractorNodeId: string + outputSelector: string[] + aliasName: string +} + +type ToolParameterShape = { + value?: unknown + nested_node_config?: { + extractor_node_id?: string + output_selector?: unknown + } +} + +type ToolNodeDataShape = { + tool_parameters?: Record +} + +type AliasSource = { + sourceVar: VarInInspect + matchedSelector: string[] + resolvedValue: unknown + resolvedValueFound: boolean + usedFallback: boolean +} + +const toSelectorKey = (selector: string[]) => selector.join('.') + +const resolveOutputSelector = (extractorNodeId: string, rawSelector?: unknown): string[] => { + if (!Array.isArray(rawSelector)) + return [] + if (rawSelector[0] === extractorNodeId) + return rawSelector.slice(1) + return rawSelector as string[] +} + +const collectAgentAliasMappings = (nodes: Node[]) => { + const nodesById = new Map(nodes.map(node => [node.id, node])) + const mappings: AgentAliasMapping[] = [] + const extractorNodeIds = new Set() + + nodes.forEach((node) => { + if (node.data.type !== BlockEnum.Tool) + return + const toolData = node.data as ToolNodeDataShape + const toolParams = toolData.tool_parameters || {} + Object.entries(toolParams).forEach(([paramKey, param]) => { + const value = param?.value + if (typeof value !== 'string') + return + const matches = Array.from(value.matchAll(AGENT_CONTEXT_VAR_PATTERN)) + if (!matches.length) + return + const agentNodeId = getAgentNodeIdFromContextVar(matches[0][0]) + if (!agentNodeId) + return + const extractorNodeId = param?.nested_node_config?.extractor_node_id || `${node.id}_ext_${paramKey}` + extractorNodeIds.add(extractorNodeId) + const resolvedOutputSelector = resolveOutputSelector(extractorNodeId, param?.nested_node_config?.output_selector) + const outputSelector = resolvedOutputSelector.length > 0 + ? resolvedOutputSelector + : ['structured_output', paramKey] + const agentNode = nodesById.get(agentNodeId) + const aliasName = `@${agentNode?.data.title || agentNodeId}` + + mappings.push({ + parentNodeId: node.id, + extractorNodeId, + outputSelector, + aliasName, + }) + }) + }) + + return { mappings, extractorNodeIds, nodesById } +} + +const findAliasSourceVar = (vars: VarInInspect[], extractorNodeId: string, outputSelector: string[]) => { + const selectorKey = toSelectorKey(outputSelector) + return vars.find((varItem) => { + const selector = Array.isArray(varItem.selector) ? varItem.selector : [] + if (selector[0] !== extractorNodeId) + return false + return toSelectorKey(selector.slice(1)) === selectorKey + }) +} + +const resolveNestedValue = (value: unknown, path: string[]): { found: boolean, value: unknown } => { + let current: unknown = value + for (const key of path) { + if (Array.isArray(current)) { + const index = Number.parseInt(key, 10) + if (!Number.isNaN(index) && index >= 0 && index < current.length) { + current = current[index] + continue + } + return { found: false, value: undefined } + } + if (current && typeof current === 'object') { + if (Object.prototype.hasOwnProperty.call(current, key)) { + current = (current as Record)[key] + continue + } + } + return { found: false, value: undefined } + } + return { found: true, value: current } +} + +const resolveAliasSourceVar = ( + vars: VarInInspect[], + extractorNodeId: string, + outputSelector: string[], +): AliasSource | undefined => { + const directMatch = findAliasSourceVar(vars, extractorNodeId, outputSelector) + if (directMatch) { + return { + sourceVar: directMatch, + matchedSelector: directMatch.selector as string[], + resolvedValue: directMatch.value, + resolvedValueFound: true, + usedFallback: false, + } + } + if (outputSelector.length === 0) + return undefined + const prefixKey = outputSelector[0] + const prefixVar = vars.find((varItem) => { + const selector = Array.isArray(varItem.selector) ? varItem.selector : [] + if (selector[0] !== extractorNodeId) + return false + return selector[1] === prefixKey && selector.length === 2 + }) + if (!prefixVar) + return undefined + if (outputSelector.length === 1) { + return { + sourceVar: prefixVar, + matchedSelector: prefixVar.selector as string[], + resolvedValue: prefixVar.value, + resolvedValueFound: true, + usedFallback: true, + } + } + const resolved = resolveNestedValue(prefixVar.value, outputSelector.slice(1)) + return { + sourceVar: prefixVar, + matchedSelector: prefixVar.selector as string[], + resolvedValue: resolved.value, + resolvedValueFound: resolved.found, + usedFallback: true, + } +} + +export const applyAgentSubgraphInspectVars = (nodesWithInspectVars: NodeWithVar[], allNodes: Node[]) => { + const hideExtractorNodes = true + const { mappings, extractorNodeIds, nodesById } = collectAgentAliasMappings(allNodes) + if (mappings.length === 0 && extractorNodeIds.size === 0) { + return nodesWithInspectVars + } + + const resultMap = new Map() + nodesWithInspectVars.forEach((node) => { + const isExtractorNode = extractorNodeIds.has(node.nodeId) + resultMap.set(node.nodeId, { + ...node, + vars: [...node.vars], + isHidden: hideExtractorNodes && isExtractorNode, + }) + }) + + const getOrCreateParentNode = (parentNodeId: string): NodeWithVar | undefined => { + const existing = resultMap.get(parentNodeId) + if (existing) + return existing + const parentNode = nodesById.get(parentNodeId) + if (!parentNode) + return undefined + return { + nodeId: parentNode.id, + nodeType: parentNode.data.type, + title: parentNode.data.title, + nodePayload: parentNode.data, + vars: [] as VarInInspect[], + isValueFetched: false, + isHidden: false, + } + } + + mappings.forEach((mapping) => { + const parent = getOrCreateParentNode(mapping.parentNodeId) + if (!parent) + return + const parentVars = parent.vars as VarInInspect[] + const aliasId = buildAliasVarId(mapping) + const upsertAliasVar = (aliasVar: VarInInspect, shouldOverwrite: boolean) => { + const existingIndex = parentVars.findIndex(varItem => varItem.id === aliasVar.id) + if (existingIndex === -1) + parentVars.unshift(aliasVar) + else if (shouldOverwrite) + parentVars[existingIndex] = { ...parentVars[existingIndex], ...aliasVar } + } + const placeholderAliasVar: VarInInspect = { + id: aliasId, + type: VarInInspectType.node, + name: mapping.aliasName, + description: '', + selector: [mapping.parentNodeId, mapping.aliasName], + value_type: VarType.any, + value: undefined, + edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, + aliasMeta: { + extractorNodeId: mapping.extractorNodeId, + outputSelector: mapping.outputSelector, + sourceVarId: aliasId, + }, + } + const extractorGroup = nodesWithInspectVars.find(node => node.nodeId === mapping.extractorNodeId) + if (!extractorGroup?.vars?.length) { + upsertAliasVar(placeholderAliasVar, false) + resultMap.set(mapping.parentNodeId, parent) + return + } + const resolved = resolveAliasSourceVar(extractorGroup.vars, mapping.extractorNodeId, mapping.outputSelector) + if (!resolved) { + upsertAliasVar(placeholderAliasVar, false) + resultMap.set(mapping.parentNodeId, parent) + return + } + const resolvedValue = resolved.resolvedValueFound ? resolved.resolvedValue : resolved.sourceVar.value + const aliasVar: VarInInspect = { + ...resolved.sourceVar, + id: aliasId, + name: mapping.aliasName, + selector: [mapping.parentNodeId, mapping.aliasName], + value: resolvedValue, + visible: true, + aliasMeta: { + extractorNodeId: mapping.extractorNodeId, + outputSelector: mapping.outputSelector, + sourceVarId: resolved.sourceVar.id, + }, + } + upsertAliasVar(aliasVar, true) + resultMap.set(mapping.parentNodeId, parent) + }) + + // TODO: handle assemble sub-graph output mapping. + return Array.from(resultMap.values()) +} diff --git a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts index 54c2c77d0d..f56fc25aae 100644 --- a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts +++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts @@ -4,6 +4,7 @@ import type { FlowType } from '@/types/common' import type { NodeWithVar, VarInInspect } from '@/types/workflow' import { useCallback } from 'react' import { useStoreApi } from 'reactflow' +import { InteractionMode } from '@/app/components/workflow' import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { @@ -16,15 +17,18 @@ import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@ import { fetchAllInspectVars } from '@/service/workflow' import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type' import { toNodeOutputVars } from '../nodes/_base/components/variable/utils' +import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias' type Params = { flowType: FlowType flowId: string + interactionMode?: InteractionMode } export const useSetWorkflowVarsWithValue = ({ flowType, flowId, + interactionMode, }: Params) => { const workflowStore = useWorkflowStore() const store = useStoreApi() @@ -94,7 +98,10 @@ export const useSetWorkflowVarsWithValue = ({ } return nodeWithVar }) - setNodesWithInspectVars(res) + const resolvedInteractionMode = interactionMode ?? InteractionMode.Default + const shouldApplyAlias = resolvedInteractionMode !== InteractionMode.Subgraph + const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(res, nodeArr) : res + setNodesWithInspectVars(nextNodes) } const fetchInspectVars = useCallback(async (params: { diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts index 316da71d9d..0159ff9c7d 100644 --- a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts @@ -5,6 +5,7 @@ import type { VarInInspect } from '@/types/workflow' import { produce } from 'immer' import { useCallback } from 'react' import { useStoreApi } from 'reactflow' +import { InteractionMode } from '@/app/components/workflow' import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' import { @@ -23,14 +24,17 @@ import { } from '@/service/use-tools' import { fetchNodeInspectVars } from '@/service/workflow' import { VarInInspectType } from '@/types/workflow' +import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias' type Params = { flowId: string flowType: FlowType + interactionMode?: InteractionMode } export const useInspectVarsCrudCommon = ({ flowId, flowType, + interactionMode, }: Params) => { const workflowStore = useWorkflowStore() const store = useStoreApi() @@ -108,6 +112,7 @@ export const useInspectVarsCrudCommon = ({ const fetchInspectVarValue = useCallback(async (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => { const { setNodeInspectVars, + setNodesWithInspectVars, dataSourceList, } = workflowStore.getState() const nodeId = selector[0] @@ -141,7 +146,13 @@ export const useInspectVarsCrudCommon = ({ } }) setNodeInspectVars(nodeId, varsWithSchemaType) - }, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools]) + const resolvedInteractionMode = interactionMode ?? InteractionMode.Default + if (resolvedInteractionMode !== InteractionMode.Subgraph) { + const { nodesWithInspectVars } = workflowStore.getState() + const nextNodes = applyAgentSubgraphInspectVars(nodesWithInspectVars, nodeArr) + setNodesWithInspectVars(nextNodes) + } + }, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools, interactionMode]) // after last run would call this const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => { @@ -169,9 +180,12 @@ export const useInspectVarsCrudCommon = ({ } } }) - setNodesWithInspectVars(nodes) + const resolvedInteractionMode = interactionMode ?? InteractionMode.Default + const shouldApplyAlias = resolvedInteractionMode !== InteractionMode.Subgraph + const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(nodes, allNodes) : nodes + setNodesWithInspectVars(nextNodes) handleCancelNodeSuccessStatus(nodeId) - }, [workflowStore, handleCancelNodeSuccessStatus]) + }, [workflowStore, handleCancelNodeSuccessStatus, interactionMode]) const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => { const { nodesWithInspectVars } = workflowStore.getState() diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts index d94882c0fb..3ce0df6323 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/hooks/use-mixed-variable-extractor.ts @@ -18,15 +18,7 @@ import { FlowType } from '@/types/common' // Constants -export const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g -const AGENT_CONTEXT_VAR_PREFIX = '{{@' -const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}' - -export const getAgentNodeIdFromContextVar = (placeholder: string): 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) -} +export { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context' export const buildAssemblePlaceholder = (toolNodeId?: string, paramKey?: string): string => { if (!toolNodeId || !paramKey) diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index 5bc8c7dfc4..32df8ffdda 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -15,19 +15,12 @@ import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation' import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' import { BlockEnum } from '@/app/components/workflow/types' +import { AGENT_CONTEXT_VAR_PATTERN, getAgentNodeIdFromContextVar } from '@/app/components/workflow/utils/agent-context' import { useGetLanguage } from '@/context/i18n' 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_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 diff --git a/web/app/components/workflow/utils/agent-context.ts b/web/app/components/workflow/utils/agent-context.ts new file mode 100644 index 0000000000..0116649a2e --- /dev/null +++ b/web/app/components/workflow/utils/agent-context.ts @@ -0,0 +1,9 @@ +export const AGENT_CONTEXT_VAR_PATTERN = /\{\{@[^.@#]+\.context@\}\}/g +const AGENT_CONTEXT_VAR_PREFIX = '{{@' +const AGENT_CONTEXT_VAR_SUFFIX = '.context@}}' + +export const getAgentNodeIdFromContextVar = (placeholder: string): 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) +} diff --git a/web/app/components/workflow/variable-inspect/group.tsx b/web/app/components/workflow/variable-inspect/group.tsx index dad8168d2b..23693ac1d8 100644 --- a/web/app/components/workflow/variable-inspect/group.tsx +++ b/web/app/components/workflow/variable-inspect/group.tsx @@ -11,6 +11,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' // import Button from '@/app/components/base/button' import ActionButton from '@/app/components/base/action-button' +import { AtSign } from '@/app/components/base/icons/src/vender/workflow' import Tooltip from '@/app/components/base/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' @@ -147,24 +148,32 @@ const Group = ({ {/* var item list */} {!isCollapsed && !nodeData?.isSingRunRunning && (
- {visibleVarList.length > 0 && visibleVarList.map(varItem => ( -
handleSelectVar(varItem, varType)} - > - -
{varItem.name}
-
{formatVarTypeLabel(varItem.value_type)}
-
- ))} + {visibleVarList.length > 0 && visibleVarList.map((varItem) => { + const isAgentAliasVar = typeof varItem.name === 'string' && varItem.name.startsWith('@') + const displayName = isAgentAliasVar ? varItem.name.slice(1) : varItem.name + return ( +
handleSelectVar(varItem, varType)} + > + {isAgentAliasVar + ? + : ( + + )} +
{displayName}
+
{formatVarTypeLabel(varItem.value_type)}
+
+ ) + })}
)} diff --git a/web/app/components/workflow/variable-inspect/left.tsx b/web/app/components/workflow/variable-inspect/left.tsx index e6638cb0f9..b5f49ffcca 100644 --- a/web/app/components/workflow/variable-inspect/left.tsx +++ b/web/app/components/workflow/variable-inspect/left.tsx @@ -36,6 +36,8 @@ const Left = ({ } = useCurrentVars() const { handleNodeSelect } = useNodesInteractions() + const visibleNodesWithInspectVars = nodesWithInspectVars.filter(node => !node.isHidden) + const showDivider = environmentVariables.length > 0 || conversationVars.length > 0 || systemVars.length > 0 const handleClearAll = () => { @@ -91,7 +93,7 @@ const Left = ({ )} {/* group nodes */} - {nodesWithInspectVars.length > 0 && nodesWithInspectVars.map(group => ( + {visibleNodesWithInspectVars.length > 0 && visibleNodesWithInspectVars.map(group => ( { @@ -59,23 +60,12 @@ const Panel: FC = () => { return if (currentFocusNodeId === VarInInspectType.environment) { const currentVar = environmentVariables.find(v => v.id === currentVarId) - const res = { + return { nodeId: VarInInspectType.environment, title: VarInInspectType.environment, nodeType: VarInInspectType.environment, + var: currentVar ? toEnvVarInInspect(currentVar) : undefined, } - if (currentVar) { - return { - ...res, - var: { - ...currentVar, - type: VarInInspectType.environment, - visible: true, - ...(currentVar.value_type === 'secret' ? { value: '******************' } : {}), - }, - } - } - return res } if (currentFocusNodeId === VarInInspectType.conversation) { const currentVar = conversationVars.find(v => v.id === currentVarId) @@ -83,15 +73,12 @@ const Panel: FC = () => { nodeId: VarInInspectType.conversation, title: VarInInspectType.conversation, nodeType: VarInInspectType.conversation, - } - if (currentVar) { - return { - ...res, - var: { - ...currentVar, - type: VarInInspectType.conversation, - }, - } + var: currentVar + ? { + ...currentVar, + type: VarInInspectType.conversation, + } + : undefined, } return res } @@ -101,15 +88,12 @@ const Panel: FC = () => { nodeId: VarInInspectType.system, title: VarInInspectType.system, nodeType: VarInInspectType.system, - } - if (currentVar) { - return { - ...res, - var: { - ...currentVar, - type: VarInInspectType.system, - }, - } + var: currentVar + ? { + ...currentVar, + type: VarInInspectType.system, + } + : undefined, } return res } @@ -124,22 +108,32 @@ const Panel: FC = () => { isSingRunRunning: targetNode.isSingRunRunning, isValueFetched: targetNode.isValueFetched, nodeData: targetNode.nodePayload, - ...(currentVar ? { var: currentVar } : {}), + var: currentVar, } }, [currentFocusNodeId, currentVarId, environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + const currentAliasMeta = useMemo(() => { + if (!currentFocusNodeId || !currentVarId) + return undefined + const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) + const targetVar = targetNode?.vars.find(v => v.id === currentVarId) + return targetVar?.aliasMeta + }, [currentFocusNodeId, currentVarId, nodesWithInspectVars]) + const fetchNodeId = currentAliasMeta?.extractorNodeId || currentFocusNodeId + const isCurrentNodeVarValueFetching = useMemo(() => { - if (!currentNodeInfo) + if (!fetchNodeId) return false - const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentNodeInfo.nodeId) + const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) if (!targetNode) return false return !targetNode.isValueFetched - }, [currentNodeInfo, nodesWithInspectVars]) + }, [fetchNodeId, nodesWithInspectVars]) const handleNodeVarSelect = useCallback((node: currentVarType) => { setCurrentFocusNodeId(node.nodeId) - setCurrentVarId(node.var.id) + if (node.var) + setCurrentVarId(node.var.id) }, [setCurrentFocusNodeId, setCurrentVarId]) const { isLoading, schemaTypeDefinitions } = useMatchSchemaType() @@ -150,12 +144,12 @@ const Panel: FC = () => { }, [eventEmitter]) useEffect(() => { - if (currentFocusNodeId && currentVarId && !isLoading) { - const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) + if (currentFocusNodeId && currentVarId && !isLoading && fetchNodeId) { + const targetNode = nodesWithInspectVars.find(node => node.nodeId === fetchNodeId) if (targetNode && !targetNode.isValueFetched) - fetchInspectVarValue([currentFocusNodeId], schemaTypeDefinitions!) + fetchInspectVarValue([fetchNodeId], schemaTypeDefinitions!) } - }, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading]) + }, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue, schemaTypeDefinitions, isLoading, fetchNodeId]) if (isListening) { return ( diff --git a/web/app/components/workflow/variable-inspect/right.tsx b/web/app/components/workflow/variable-inspect/right.tsx index 687d8a8361..b20349ade2 100644 --- a/web/app/components/workflow/variable-inspect/right.tsx +++ b/web/app/components/workflow/variable-inspect/right.tsx @@ -55,8 +55,14 @@ const Right = ({ const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) const toolIcon = useToolIcon(currentNodeVar?.nodeData) - const isTruncated = currentNodeVar?.var.is_truncated - const fullContent = currentNodeVar?.var.full_content + const currentVar = currentNodeVar?.var + const currentNodeType = currentNodeVar?.nodeType + const currentNodeTitle = currentNodeVar?.title + const currentNodeId = currentNodeVar?.nodeId + const isTruncated = !!currentVar?.is_truncated + const fullContent = currentVar?.full_content + const isAgentAliasVar = currentVar?.name?.startsWith('@') + const displayVarName = isAgentAliasVar ? currentVar?.name?.slice(1) : currentVar?.name const { resetConversationVar, @@ -65,15 +71,15 @@ const Right = ({ } = useCurrentVars() const handleValueChange = (varId: string, value: any) => { - if (!currentNodeVar) + if (!currentNodeVar || !currentVar) return editInspectVarValue(currentNodeVar.nodeId, varId, value) } const resetValue = () => { - if (!currentNodeVar) + if (!currentNodeVar || !currentVar) return - resetToLastRunVar(currentNodeVar.nodeId, currentNodeVar.var.id) + resetToLastRunVar(currentNodeVar.nodeId, currentVar.id) } const handleClose = () => { @@ -82,13 +88,13 @@ const Right = ({ } const handleClear = () => { - if (!currentNodeVar) + if (!currentNodeVar || !currentVar) return - resetConversationVar(currentNodeVar.var.id) + resetConversationVar(currentVar.id) } const getCopyContent = () => { - const value = currentNodeVar?.var.value + const value = currentVar?.value if (value === null || value === undefined) return '' @@ -160,8 +166,8 @@ const Right = ({ handleHidePromptGenerator() }, [setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator]) - const schemaType = currentNodeVar?.var.schemaType - const valueType = currentNodeVar?.var.value_type + const schemaType = currentVar?.schemaType + const valueType = currentVar?.value_type const valueTypeLabel = formatVarTypeLabel(valueType) const shouldShowSchemaType = !!schemaType && schemaType !== valueType @@ -178,32 +184,34 @@ const Right = ({ )}
- {currentNodeVar?.var && ( + {currentVar && ( <> { - [VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeVar.nodeType as VarInInspectType) && ( + currentNodeType + && [VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeType as VarInInspectType) && ( ) } - {currentNodeVar.nodeType !== VarInInspectType.environment - && currentNodeVar.nodeType !== VarInInspectType.conversation - && currentNodeVar.nodeType !== VarInInspectType.system + {currentNodeType + && currentNodeType !== VarInInspectType.environment + && currentNodeType !== VarInInspectType.conversation + && currentNodeType !== VarInInspectType.system && ( <> -
{currentNodeVar.title}
+
{currentNodeTitle}
/
)} -
{currentNodeVar.var.name}
+
{displayVarName}
{`${valueTypeLabel}${displaySchemaType}`} {isTruncated && ( @@ -221,7 +229,7 @@ const Right = ({ )}
- {currentNodeVar && ( + {currentVar && ( <> {canShowPromptGenerator && ( @@ -245,27 +253,27 @@ const Right = ({ )} - {!isTruncated && currentNodeVar.var.edited && ( + {!isTruncated && currentVar.edited && ( {t('debug.variableInspect.edited', { ns: 'workflow' })} )} - {!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type !== VarInInspectType.conversation && ( + {!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && ( )} - {!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type === VarInInspectType.conversation && ( + {!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && ( )} - {currentNodeVar.var.value_type !== 'secret' && ( + {currentVar.value_type !== 'secret' && ( )} @@ -277,16 +285,16 @@ const Right = ({
{/* content */}
- {!currentNodeVar?.var && } + {!currentVar && } {isValueFetching && (
)} - {currentNodeVar?.var && !isValueFetching && ( + {currentVar && currentNodeId && !isValueFetching && ( diff --git a/web/app/components/workflow/variable-inspect/utils.tsx b/web/app/components/workflow/variable-inspect/utils.tsx index bfeef37a59..a5e44e2767 100644 --- a/web/app/components/workflow/variable-inspect/utils.tsx +++ b/web/app/components/workflow/variable-inspect/utils.tsx @@ -1,4 +1,8 @@ +import type { EnvironmentVariable } from '@/app/components/workflow/types' +import type { VarInInspect } from '@/types/workflow' import { z } from 'zod' +import { VarType } from '@/app/components/workflow/types' +import { VarInInspectType } from '@/types/workflow' const arrayStringSchemaParttern = z.array(z.string()) const arrayNumberSchemaParttern = z.array(z.number()) @@ -10,6 +14,34 @@ type Json = Literal | { [key: string]: Json } | Json[] const jsonSchema: z.ZodType = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) const arrayJsonSchema: z.ZodType = z.lazy(() => z.array(jsonSchema)) +const toEnvVarType = (valueType: EnvironmentVariable['value_type']): VarInInspect['value_type'] => { + switch (valueType) { + case 'number': + return VarType.number + case 'secret': + return VarType.secret + default: + return VarType.string + } +} + +export const toEnvVarInInspect = (envVar: EnvironmentVariable): VarInInspect => { + const valueType = envVar.value_type + return { + id: envVar.id, + type: VarInInspectType.environment, + name: envVar.name, + description: envVar.description, + selector: [VarInInspectType.environment, envVar.name], + value_type: toEnvVarType(valueType), + value: valueType === 'secret' ? '******************' : envVar.value, + edited: false, + visible: true, + is_truncated: false, + full_content: { size_bytes: 0, download_url: '' }, + } +} + export const validateJSONSchema = (schema: any, type: string) => { if (type === 'array[string]') { const result = arrayStringSchemaParttern.safeParse(schema) diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 7898787f48..279a098195 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -477,6 +477,12 @@ export type FullContent = { download_url: string } +export type VarInInspectAliasMeta = { + extractorNodeId: string + outputSelector: string[] + sourceVarId: string +} + export type VarInInspect = { id: string type: VarInInspectType @@ -490,6 +496,7 @@ export type VarInInspect = { is_truncated: boolean full_content: FullContent schemaType?: string + aliasMeta?: VarInInspectAliasMeta } export type NodeWithVar = { @@ -500,4 +507,5 @@ export type NodeWithVar = { vars: VarInInspect[] isSingRunRunning?: boolean isValueFetched?: boolean + isHidden?: boolean }