Files
dify/web/app/components/workflow/nodes/tool/node.tsx

213 lines
8.7 KiB
TypeScript

import type { FC } from 'react'
import type { ToolNodeType } from './types'
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
import type { AgentNodeType } from '@/app/components/workflow/nodes/agent/types'
import type { CommonNodeType, NodeProps, Node as WorkflowNode } from '@/app/components/workflow/types'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import BlockIcon from '@/app/components/workflow/block-icon'
import { useNodesMetaData } from '@/app/components/workflow/hooks'
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'
type AgentCheckValidContext = {
provider?: StrategyPluginDetail
strategy?: StrategyDetail
language: string
isReadyForCheckValid: boolean
}
const Node: FC<NodeProps<ToolNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
const { data: strategyProviders } = useStrategyProviders()
const nodes = useNodes<CommonNodeType>()
const { tool_configurations, paramSchemas } = data
const toolConfigs = Object.keys(tool_configurations || {})
const {
isChecking,
isMissing,
uniqueIdentifier,
canInstall,
onInstallSuccess,
shouldDim,
} = useNodePluginInstallation(data)
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
const nodesById = useMemo(() => {
return nodes.reduce((acc, node) => {
acc[node.id] = node
return acc
}, {} as Record<string, WorkflowNode>)
}, [nodes])
const nestedNodeEntries = useMemo(() => {
const entries: Array<{ agentNodeId: string, extractorNodeId?: string, paramKey: string }> = []
const seen = new Set<string>()
const toolParams = data.tool_parameters || {}
Object.entries(toolParams).forEach(([paramKey, param]) => {
const value = param?.value
if (typeof value !== 'string')
return
const matches = value.matchAll(AGENT_CONTEXT_VAR_PATTERN)
for (const match of matches) {
const agentNodeId = getAgentNodeIdFromContextVar(match[0])
if (!agentNodeId)
continue
const entryKey = `${paramKey}:${agentNodeId}`
if (seen.has(entryKey))
continue
seen.add(entryKey)
entries.push({
agentNodeId,
paramKey,
extractorNodeId: param?.nested_node_config?.extractor_node_id
|| (param?.type === VarType.nested_node ? `${id}_ext_${paramKey}` : undefined),
})
}
})
return entries
}, [data.tool_parameters, id])
const referenceItems = useMemo(() => {
if (!nestedNodeEntries.length)
return []
const getNodeWarning = (node?: WorkflowNode) => {
if (!node)
return true
const validator = nodesMetaDataMap?.[node.data.type as BlockEnum]?.checkValid
if (!validator)
return false
let moreDataForCheckValid: AgentCheckValidContext | undefined
if (node.data.type === BlockEnum.Agent) {
const agentData = node.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === agentData.agent_strategy_provider_name)
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === agentData.agent_strategy_name)
moreDataForCheckValid = {
provider,
strategy,
language,
isReadyForCheckValid,
}
}
const { errorMessage } = validator(node.data, t, moreDataForCheckValid)
return Boolean(errorMessage)
}
return nestedNodeEntries.map(({ agentNodeId, extractorNodeId, paramKey }) => {
const agentNode = nodesById[agentNodeId]
const agentLabel = `@${agentNode?.data.title || agentNodeId}`
const agentWarning = getNodeWarning(agentNode)
const extractorWarning = extractorNodeId
? getNodeWarning(nodesById[extractorNodeId])
: false
const hasWarning = agentWarning || extractorWarning
return {
key: `${paramKey}-${agentNodeId}-${extractorNodeId || 'no-extractor'}`,
label: agentLabel,
type: BlockEnum.Agent,
hasWarning,
}
})
}, [nestedNodeEntries, nodesById, nodesMetaDataMap, strategyProviders, language, t])
const hasConfigs = toolConfigs.length > 0
const hasReferences = referenceItems.length > 0
if (!showInstallButton && !hasConfigs && !hasReferences)
return null
return (
<div className="relative mb-1 px-3 py-1">
{showInstallButton && (
<div className="pointer-events-auto absolute right-3 top-[-32px] z-40">
<InstallPluginButton
size="small"
className="!font-medium !text-text-accent"
extraIdentifiers={[
data.plugin_id,
data.provider_id,
data.provider_name,
].filter(Boolean) as string[]}
uniqueIdentifier={uniqueIdentifier!}
onSuccess={onInstallSuccess}
/>
</div>
)}
{hasConfigs && (
<div className="space-y-0.5" aria-disabled={shouldDim}>
{toolConfigs.map((key, index) => (
<div key={index} className="flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary">
<div title={key} className="max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary">
{key}
</div>
{typeof tool_configurations[key].value === 'string' && (
<div title={tool_configurations[key].value} className="w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary">
{paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value}
</div>
)}
{typeof tool_configurations[key].value === 'number' && (
<div title={Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} className="w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary">
{Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value}
</div>
)}
{typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && (
<div title={tool_configurations[key].model} className="w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary">
{tool_configurations[key].model}
</div>
)}
</div>
))}
</div>
)}
{hasReferences && (
<div className={cn('space-y-0.5', hasConfigs && 'mt-1')} aria-disabled={shouldDim}>
{referenceItems.map(item => (
<div
key={item.key}
className={cn(
'flex h-6 items-center justify-between space-x-1 rounded-md border px-1 text-xs font-normal text-text-secondary',
item.hasWarning
? 'border-text-warning-secondary bg-components-badge-status-light-warning-halo'
: 'border-transparent bg-workflow-block-parma-bg',
)}
>
<div className="flex min-w-0 items-center gap-1">
<BlockIcon
className="shrink-0"
type={item.type}
size="xs"
/>
<span title={item.label} className="truncate text-text-secondary system-xs-medium">
{item.label}
</span>
</div>
{item.hasWarning && (
<AlertTriangle className="h-3.5 w-3.5 text-text-warning-secondary" />
)}
</div>
))}
</div>
)}
</div>
)
}
export default React.memo(Node)