mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
feat: Add @agent icon and implement agent alias variables in workflow
inspector
This commit is contained in:
265
web/app/components/workflow/hooks/inspect-vars-agent-alias.ts
Normal file
265
web/app/components/workflow/hooks/inspect-vars-agent-alias.ts
Normal file
@ -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<string, ToolParameterShape>
|
||||
}
|
||||
|
||||
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<string>()
|
||||
|
||||
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<string, unknown>)[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<string, NodeWithVar>()
|
||||
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())
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
9
web/app/components/workflow/utils/agent-context.ts
Normal file
9
web/app/components/workflow/utils/agent-context.ts
Normal file
@ -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)
|
||||
}
|
||||
@ -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 && (
|
||||
<div className="px-0.5">
|
||||
{visibleVarList.length > 0 && visibleVarList.map(varItem => (
|
||||
<div
|
||||
key={varItem.id}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover',
|
||||
varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
|
||||
)}
|
||||
onClick={() => handleSelectVar(varItem, varType)}
|
||||
>
|
||||
<VariableIconWithColor
|
||||
variableCategory={varType}
|
||||
isExceptionVariable={['error_type', 'error_message'].includes(varItem.name)}
|
||||
className="size-4"
|
||||
/>
|
||||
<div className="system-sm-medium grow truncate text-text-secondary">{varItem.name}</div>
|
||||
<div className="system-xs-regular shrink-0 text-text-tertiary">{formatVarTypeLabel(varItem.value_type)}</div>
|
||||
</div>
|
||||
))}
|
||||
{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 (
|
||||
<div
|
||||
key={varItem.id}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover',
|
||||
varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt',
|
||||
)}
|
||||
onClick={() => handleSelectVar(varItem, varType)}
|
||||
>
|
||||
{isAgentAliasVar
|
||||
? <AtSign className="size-4 shrink-0 text-util-colors-violet-violet-600" />
|
||||
: (
|
||||
<VariableIconWithColor
|
||||
variableCategory={varType}
|
||||
isExceptionVariable={['error_type', 'error_message'].includes(varItem.name)}
|
||||
className="size-4"
|
||||
/>
|
||||
)}
|
||||
<div className="system-sm-medium grow truncate text-text-secondary">{displayName}</div>
|
||||
<div className="system-xs-regular shrink-0 text-text-tertiary">{formatVarTypeLabel(varItem.value_type)}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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 = ({
|
||||
</div>
|
||||
)}
|
||||
{/* group nodes */}
|
||||
{nodesWithInspectVars.length > 0 && nodesWithInspectVars.map(group => (
|
||||
{visibleNodesWithInspectVars.length > 0 && visibleNodesWithInspectVars.map(group => (
|
||||
<Group
|
||||
key={group.nodeId}
|
||||
varType={VarInInspectType.node}
|
||||
|
||||
@ -19,14 +19,15 @@ import Empty from './empty'
|
||||
import Left from './left'
|
||||
import Listening from './listening'
|
||||
import Right from './right'
|
||||
import { toEnvVarInInspect } from './utils'
|
||||
|
||||
export type currentVarType = {
|
||||
nodeId: string
|
||||
nodeType: string
|
||||
title: string
|
||||
isValueFetched?: boolean
|
||||
var: VarInInspect
|
||||
nodeData: NodeProps['data']
|
||||
var?: VarInInspect
|
||||
nodeData?: NodeProps['data']
|
||||
}
|
||||
|
||||
const Panel: FC = () => {
|
||||
@ -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 (
|
||||
|
||||
@ -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 = ({
|
||||
</ActionButton>
|
||||
)}
|
||||
<div className="flex w-0 grow items-center gap-1">
|
||||
{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) && (
|
||||
<VariableIconWithColor
|
||||
variableCategory={currentNodeVar.nodeType as VarInInspectType}
|
||||
variableCategory={currentNodeType as VarInInspectType}
|
||||
className="size-4"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{currentNodeVar.nodeType !== VarInInspectType.environment
|
||||
&& currentNodeVar.nodeType !== VarInInspectType.conversation
|
||||
&& currentNodeVar.nodeType !== VarInInspectType.system
|
||||
{currentNodeType
|
||||
&& currentNodeType !== VarInInspectType.environment
|
||||
&& currentNodeType !== VarInInspectType.conversation
|
||||
&& currentNodeType !== VarInInspectType.system
|
||||
&& (
|
||||
<>
|
||||
<BlockIcon
|
||||
className="shrink-0"
|
||||
type={currentNodeVar.nodeType as BlockEnum}
|
||||
type={currentNodeType as BlockEnum}
|
||||
size="xs"
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeVar.title}</div>
|
||||
<div className="system-sm-regular shrink-0 text-text-secondary">{currentNodeTitle}</div>
|
||||
<div className="system-sm-regular shrink-0 text-text-quaternary">/</div>
|
||||
</>
|
||||
)}
|
||||
<div title={currentNodeVar.var.name} className="system-sm-semibold truncate text-text-secondary">{currentNodeVar.var.name}</div>
|
||||
<div title={displayVarName} className="system-sm-semibold truncate text-text-secondary">{displayVarName}</div>
|
||||
<div className="system-xs-medium ml-1 shrink-0 space-x-2 text-text-tertiary">
|
||||
<span>{`${valueTypeLabel}${displaySchemaType}`}</span>
|
||||
{isTruncated && (
|
||||
@ -221,7 +229,7 @@ const Right = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{currentNodeVar && (
|
||||
{currentVar && (
|
||||
<>
|
||||
{canShowPromptGenerator && (
|
||||
<Tooltip popupContent={t('generate.optimizePromptTooltip', { ns: 'appDebug' })}>
|
||||
@ -245,27 +253,27 @@ const Right = ({
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isTruncated && currentNodeVar.var.edited && (
|
||||
{!isTruncated && currentVar.edited && (
|
||||
<Badge>
|
||||
<span className="ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary"></span>
|
||||
<span className="system-2xs-semibold-uupercase">{t('debug.variableInspect.edited', { ns: 'workflow' })}</span>
|
||||
</Badge>
|
||||
)}
|
||||
{!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type !== VarInInspectType.conversation && (
|
||||
{!isTruncated && currentVar.edited && currentVar.type !== VarInInspectType.conversation && (
|
||||
<Tooltip popupContent={t('debug.variableInspect.reset', { ns: 'workflow' })}>
|
||||
<ActionButton onClick={resetValue}>
|
||||
<RiArrowGoBackLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isTruncated && currentNodeVar.var.edited && currentNodeVar.var.type === VarInInspectType.conversation && (
|
||||
{!isTruncated && currentVar.edited && currentVar.type === VarInInspectType.conversation && (
|
||||
<Tooltip popupContent={t('debug.variableInspect.resetConversationVar', { ns: 'workflow' })}>
|
||||
<ActionButton onClick={handleClear}>
|
||||
<RiArrowGoBackLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentNodeVar.var.value_type !== 'secret' && (
|
||||
{currentVar.value_type !== 'secret' && (
|
||||
<CopyFeedback content={getCopyContent()} />
|
||||
)}
|
||||
</>
|
||||
@ -277,16 +285,16 @@ const Right = ({
|
||||
</div>
|
||||
{/* content */}
|
||||
<div className="grow p-2">
|
||||
{!currentNodeVar?.var && <Empty />}
|
||||
{!currentVar && <Empty />}
|
||||
{isValueFetching && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
{currentNodeVar?.var && !isValueFetching && (
|
||||
{currentVar && currentNodeId && !isValueFetching && (
|
||||
<ValueContent
|
||||
key={`${currentNodeVar.nodeId}-${currentNodeVar.var.id}`}
|
||||
currentVar={currentNodeVar.var}
|
||||
key={`${currentNodeId}-${currentVar.id}`}
|
||||
currentVar={currentVar}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={!!isTruncated}
|
||||
/>
|
||||
|
||||
@ -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<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))
|
||||
const arrayJsonSchema: z.ZodType<Json[]> = 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)
|
||||
|
||||
Reference in New Issue
Block a user