dify
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { type FC, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type ModelBarProps = {
|
||||
provider: string
|
||||
model: string
|
||||
} | {}
|
||||
|
||||
const useAllModel = () => {
|
||||
const { data: textGeneration } = useModelList(ModelTypeEnum.textGeneration)
|
||||
const { data: moderation } = useModelList(ModelTypeEnum.moderation)
|
||||
const { data: rerank } = useModelList(ModelTypeEnum.rerank)
|
||||
const { data: speech2text } = useModelList(ModelTypeEnum.speech2text)
|
||||
const { data: textEmbedding } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
const { data: tts } = useModelList(ModelTypeEnum.tts)
|
||||
const models = useMemo(() => {
|
||||
return textGeneration
|
||||
.concat(moderation)
|
||||
.concat(rerank)
|
||||
.concat(speech2text)
|
||||
.concat(textEmbedding)
|
||||
.concat(tts)
|
||||
}, [textGeneration, moderation, rerank, speech2text, textEmbedding, tts])
|
||||
if (!textGeneration || !moderation || !rerank || !speech2text || !textEmbedding || !tts)
|
||||
return undefined
|
||||
return models
|
||||
}
|
||||
|
||||
export const ModelBar: FC<ModelBarProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const modelList = useAllModel()
|
||||
if (!('provider' in props)) {
|
||||
return <Tooltip
|
||||
popupContent={t('workflow.nodes.agent.modelNotSelected')}
|
||||
triggerMethod='hover'
|
||||
>
|
||||
<div className='relative'>
|
||||
<ModelSelector
|
||||
modelList={[]}
|
||||
triggerClassName='bg-workflow-block-parma-bg !h-6 !rounded-md'
|
||||
defaultModel={undefined}
|
||||
showDeprecatedWarnIcon={false}
|
||||
readonly
|
||||
deprecatedClassName='opacity-50'
|
||||
/>
|
||||
<Indicator color={'red'} className='absolute -right-0.5 -top-0.5' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
const modelInstalled = modelList?.some(
|
||||
provider => provider.provider === props.provider && provider.models.some(model => model.model === props.model))
|
||||
const showWarn = modelList && !modelInstalled
|
||||
return modelList && <Tooltip
|
||||
popupContent={t('workflow.nodes.agent.modelNotInstallTooltip')}
|
||||
triggerMethod='hover'
|
||||
disabled={!modelList || modelInstalled}
|
||||
>
|
||||
<div className='relative'>
|
||||
<ModelSelector
|
||||
modelList={modelList}
|
||||
triggerClassName='bg-workflow-block-parma-bg !h-6 !rounded-md'
|
||||
defaultModel={props}
|
||||
showDeprecatedWarnIcon={false}
|
||||
readonly
|
||||
deprecatedClassName='opacity-50'
|
||||
/>
|
||||
{showWarn && <Indicator color={'red'} className='absolute -right-0.5 -top-0.5' />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { memo, useMemo, useRef, useState } from 'react'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
|
||||
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
type Status = 'not-installed' | 'not-authorized' | undefined
|
||||
|
||||
export type ToolIconProps = {
|
||||
id: string
|
||||
providerName: string
|
||||
}
|
||||
|
||||
export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools
|
||||
const currentProvider = useMemo(() => {
|
||||
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])]
|
||||
return mergedTools.find((toolWithProvider) => {
|
||||
return toolWithProvider.name === providerName || toolWithProvider.id === providerName
|
||||
})
|
||||
}, [buildInTools, customTools, providerName, workflowTools, mcpTools])
|
||||
|
||||
const providerNameParts = providerName.split('/')
|
||||
const author = providerNameParts[0]
|
||||
const name = providerNameParts[1]
|
||||
const icon = useMemo(() => {
|
||||
if (!isDataReady) return ''
|
||||
if (currentProvider) return currentProvider.icon
|
||||
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
|
||||
return iconFromMarketPlace
|
||||
}, [author, currentProvider, name, isDataReady])
|
||||
const status: Status = useMemo(() => {
|
||||
if (!isDataReady) return undefined
|
||||
if (!currentProvider) return 'not-installed'
|
||||
if (currentProvider.is_team_authorization === false) return 'not-authorized'
|
||||
return undefined
|
||||
}, [currentProvider, isDataReady])
|
||||
const indicator = status === 'not-installed' ? 'red' : status === 'not-authorized' ? 'yellow' : undefined
|
||||
const notSuccess = (['not-installed', 'not-authorized'] as Array<Status>).includes(status)
|
||||
const { t } = useTranslation()
|
||||
const tooltip = useMemo(() => {
|
||||
if (!notSuccess) return undefined
|
||||
if (status === 'not-installed') return t('workflow.nodes.agent.toolNotInstallTooltip', { tool: name })
|
||||
if (status === 'not-authorized') return t('workflow.nodes.agent.toolNotAuthorizedTooltip', { tool: name })
|
||||
throw new Error('Unknown status')
|
||||
}, [name, notSuccess, status, t])
|
||||
const [iconFetchError, setIconFetchError] = useState(false)
|
||||
return <Tooltip
|
||||
triggerMethod='hover'
|
||||
popupContent={tooltip}
|
||||
disabled={!notSuccess}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative',
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="flex size-5 items-center justify-center overflow-hidden rounded-[6px] border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
|
||||
{(() => {
|
||||
if (iconFetchError || !icon)
|
||||
return <Group className="h-3 w-3 opacity-35" />
|
||||
if (typeof icon === 'string') {
|
||||
return <img
|
||||
src={icon}
|
||||
alt='tool icon'
|
||||
className={classNames(
|
||||
'size-3.5 h-full w-full object-cover',
|
||||
notSuccess && 'opacity-50',
|
||||
)}
|
||||
onError={() => setIconFetchError(true)}
|
||||
/>
|
||||
}
|
||||
if (typeof icon === 'object') {
|
||||
return <AppIcon
|
||||
className={classNames(
|
||||
'size-3.5 h-full w-full object-cover',
|
||||
notSuccess && 'opacity-50',
|
||||
)}
|
||||
icon={icon?.content}
|
||||
background={icon?.background}
|
||||
/>
|
||||
}
|
||||
return <Group className="h-3 w-3 opacity-35" />
|
||||
})()}
|
||||
</div>
|
||||
{indicator && <Indicator color={indicator} className="absolute -right-[1px] -top-[1px]" />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
})
|
||||
|
||||
ToolIcon.displayName = 'ToolIcon'
|
||||
155
dify/web/app/components/workflow/nodes/agent/default.ts
Normal file
155
dify/web/app/components/workflow/nodes/agent/default.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
|
||||
import { BlockEnum, type NodeDefault } from '../../types'
|
||||
import type { AgentNodeType } from './types'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { genNodeMetaData } from '../../utils'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
sort: 3,
|
||||
type: BlockEnum.Agent,
|
||||
})
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
|
||||
const nodeDefault: NodeDefault<AgentNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
tool_node_version: '2',
|
||||
},
|
||||
checkValid(payload, t, moreDataForCheckValid: {
|
||||
strategyProvider?: StrategyPluginDetail,
|
||||
strategy?: StrategyDetail
|
||||
language: string
|
||||
isReadyForCheckValid: boolean
|
||||
}) {
|
||||
const { strategy, language, isReadyForCheckValid } = moreDataForCheckValid
|
||||
if (!isReadyForCheckValid) {
|
||||
return {
|
||||
isValid: true,
|
||||
errorMessage: '',
|
||||
}
|
||||
}
|
||||
if (!strategy) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.nodes.agent.checkList.strategyNotSelected'),
|
||||
}
|
||||
}
|
||||
for (const param of strategy.parameters) {
|
||||
// single tool
|
||||
if (param.required && param.type === FormTypeEnum.toolSelector) {
|
||||
// no value
|
||||
const toolValue = payload.agent_parameters?.[param.name]?.value
|
||||
if (!toolValue) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
|
||||
}
|
||||
}
|
||||
// not enabled
|
||||
else if (!toolValue.enabled) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
|
||||
}
|
||||
}
|
||||
// check form of tool
|
||||
else {
|
||||
const schemas = toolValue.schemas || []
|
||||
const userSettings = toolValue.settings
|
||||
const reasoningConfig = toolValue.parameters
|
||||
const version = payload.version
|
||||
const toolNodeVersion = payload.tool_node_version
|
||||
const mergeVersion = version || toolNodeVersion
|
||||
schemas.forEach((schema: any) => {
|
||||
if (schema?.required) {
|
||||
if (schema.form === 'form' && !mergeVersion && !userSettings[schema.name]?.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
if (schema.form === 'form' && mergeVersion && !userSettings[schema.name]?.value.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
if (schema.form === 'llm' && !mergeVersion && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
if (schema.form === 'llm' && mergeVersion && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// multiple tools
|
||||
if (param.required && param.type === FormTypeEnum.multiToolSelector) {
|
||||
const tools = payload.agent_parameters?.[param.name]?.value || []
|
||||
// no value
|
||||
if (!tools.length) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
|
||||
}
|
||||
}
|
||||
// not enabled
|
||||
else if (tools.every((tool: any) => !tool.enabled)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
|
||||
}
|
||||
}
|
||||
// check form of tools
|
||||
else {
|
||||
const validState = {
|
||||
isValid: true,
|
||||
errorMessage: '',
|
||||
}
|
||||
for (const tool of tools) {
|
||||
const schemas = tool.schemas || []
|
||||
const userSettings = tool.settings
|
||||
const reasoningConfig = tool.parameters
|
||||
schemas.forEach((schema: any) => {
|
||||
if (schema?.required) {
|
||||
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
if (schema.form === 'llm' && reasoningConfig[schema.name]?.auto === 0 && !reasoningConfig[schema.name]?.value) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return validState
|
||||
}
|
||||
}
|
||||
// common params
|
||||
if (param.required && !(payload.agent_parameters?.[param.name]?.value || param.default)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
errorMessage: '',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
115
dify/web/app/components/workflow/nodes/agent/node.tsx
Normal file
115
dify/web/app/components/workflow/nodes/agent/node.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { type FC, memo, useMemo } from 'react'
|
||||
import type { NodeProps } from '../../types'
|
||||
import type { AgentNodeType } from './types'
|
||||
import { SettingItem } from '../_base/components/setting-item'
|
||||
import { Group, GroupLabel } from '../_base/components/group'
|
||||
import type { ToolIconProps } from './components/tool-icon'
|
||||
import { ToolIcon } from './components/tool-icon'
|
||||
import useConfig from './use-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { ModelBar } from './components/model-bar'
|
||||
|
||||
const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
|
||||
const { inputs, currentStrategy, currentStrategyStatus, pluginDetail } = useConfig(props.id, props.data)
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
const models = useMemo(() => {
|
||||
if (!inputs) return []
|
||||
// if selected, show in node
|
||||
// if required and not selected, show empty selector
|
||||
// if not required and not selected, show nothing
|
||||
const models = currentStrategy?.parameters
|
||||
.filter(param => param.type === FormTypeEnum.modelSelector)
|
||||
.reduce((acc, param) => {
|
||||
const item = inputs.agent_parameters?.[param.name]?.value
|
||||
if (!item) {
|
||||
if (param.required) {
|
||||
acc.push({ param: param.name })
|
||||
return acc
|
||||
}
|
||||
else { return acc }
|
||||
}
|
||||
acc.push({ provider: item.provider, model: item.model, param: param.name })
|
||||
return acc
|
||||
}, [] as Array<{ param: string } | { provider: string, model: string, param: string }>) || []
|
||||
return models
|
||||
}, [currentStrategy, inputs])
|
||||
|
||||
const tools = useMemo(() => {
|
||||
const tools: Array<ToolIconProps> = []
|
||||
currentStrategy?.parameters.forEach((param, i) => {
|
||||
if (param.type === FormTypeEnum.toolSelector) {
|
||||
const field = param.name
|
||||
const value = inputs.agent_parameters?.[field]?.value
|
||||
if (value) {
|
||||
tools.push({
|
||||
id: `${param.name}-${i}`,
|
||||
providerName: value.provider_name as any,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (param.type === FormTypeEnum.multiToolSelector) {
|
||||
const field = param.name
|
||||
const value = inputs.agent_parameters?.[field]?.value
|
||||
if (value) {
|
||||
(value as unknown as any[]).forEach((item, idx) => {
|
||||
tools.push({
|
||||
id: `${param.name}-${idx}`,
|
||||
providerName: item.provider_name,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return tools
|
||||
}, [currentStrategy?.parameters, inputs.agent_parameters])
|
||||
return <div className='mb-1 space-y-1 px-3'>
|
||||
{inputs.agent_strategy_name
|
||||
? <SettingItem
|
||||
label={t('workflow.nodes.agent.strategy.shortLabel')}
|
||||
status={
|
||||
currentStrategyStatus && !currentStrategyStatus.isExistInPlugin
|
||||
? 'error'
|
||||
: undefined
|
||||
}
|
||||
tooltip={
|
||||
(currentStrategyStatus && !currentStrategyStatus.isExistInPlugin)
|
||||
? t('workflow.nodes.agent.strategyNotInstallTooltip', {
|
||||
plugin: pluginDetail?.declaration.label
|
||||
? renderI18nObject(pluginDetail?.declaration.label)
|
||||
: undefined,
|
||||
strategy: inputs.agent_strategy_label,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{inputs.agent_strategy_label}
|
||||
</SettingItem>
|
||||
: <SettingItem label={t('workflow.nodes.agent.strategyNotSet')} />}
|
||||
{models.length > 0 && <Group
|
||||
label={<GroupLabel className='mt-1'>
|
||||
{t('workflow.nodes.agent.model')}
|
||||
</GroupLabel>}
|
||||
>
|
||||
{models.map((model) => {
|
||||
return <ModelBar
|
||||
{...model}
|
||||
key={model.param}
|
||||
/>
|
||||
})}
|
||||
</Group>}
|
||||
{tools.length > 0 && <Group label={<GroupLabel className='mt-1'>
|
||||
{t('workflow.nodes.agent.toolbox')}
|
||||
</GroupLabel>}>
|
||||
<div className='grid grid-cols-10 gap-0.5'>
|
||||
{tools.map((tool, i) => <ToolIcon {...tool} key={tool.id + i} />)}
|
||||
</div>
|
||||
</Group>}
|
||||
</div>
|
||||
}
|
||||
|
||||
AgentNode.displayName = 'AgentNode'
|
||||
|
||||
export default memo(AgentNode)
|
||||
133
dify/web/app/components/workflow/nodes/agent/panel.tsx
Normal file
133
dify/web/app/components/workflow/nodes/agent/panel.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type { NodePanelProps } from '../../types'
|
||||
import { AgentFeature, type AgentNodeType } from './types'
|
||||
import Field from '../_base/components/field'
|
||||
import { AgentStrategy } from '../_base/components/agent-strategy'
|
||||
import useConfig from './use-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import OutputVars, { VarItem } from '../_base/components/output-vars'
|
||||
import type { StrategyParamItem } from '@/app/components/plugins/types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { toType } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { useStore } from '../../store'
|
||||
import Split from '../_base/components/split'
|
||||
import MemoryConfig from '../_base/components/memory-config'
|
||||
const i18nPrefix = 'workflow.nodes.agent'
|
||||
|
||||
export function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema {
|
||||
return {
|
||||
...param as any,
|
||||
variable: param.name,
|
||||
show_on: [],
|
||||
type: toType(param.type),
|
||||
tooltip: param.help,
|
||||
}
|
||||
}
|
||||
|
||||
const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
|
||||
const {
|
||||
inputs,
|
||||
setInputs,
|
||||
currentStrategy,
|
||||
formData,
|
||||
onFormChange,
|
||||
isChatMode,
|
||||
availableNodesWithParent,
|
||||
availableVars,
|
||||
readOnly,
|
||||
outputSchema,
|
||||
handleMemoryChange,
|
||||
canChooseMCPTool,
|
||||
} = useConfig(props.id, props.data)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
|
||||
return <div className='my-2'>
|
||||
<Field
|
||||
required
|
||||
title={t('workflow.nodes.agent.strategy.label')}
|
||||
className='px-4 py-2'
|
||||
tooltip={t('workflow.nodes.agent.strategy.tooltip')} >
|
||||
<AgentStrategy
|
||||
strategy={inputs.agent_strategy_name ? {
|
||||
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
|
||||
agent_strategy_name: inputs.agent_strategy_name!,
|
||||
agent_strategy_label: inputs.agent_strategy_label!,
|
||||
agent_output_schema: inputs.output_schema,
|
||||
plugin_unique_identifier: inputs.plugin_unique_identifier!,
|
||||
meta: inputs.meta,
|
||||
} : undefined}
|
||||
onStrategyChange={(strategy) => {
|
||||
setInputs({
|
||||
...inputs,
|
||||
agent_strategy_provider_name: strategy?.agent_strategy_provider_name,
|
||||
agent_strategy_name: strategy?.agent_strategy_name,
|
||||
agent_strategy_label: strategy?.agent_strategy_label,
|
||||
output_schema: strategy!.agent_output_schema,
|
||||
plugin_unique_identifier: strategy!.plugin_unique_identifier,
|
||||
meta: strategy?.meta,
|
||||
})
|
||||
resetEditor(Date.now())
|
||||
}}
|
||||
formSchema={currentStrategy?.parameters?.map(strategyParamToCredientialForm) || []}
|
||||
formValue={formData}
|
||||
onFormValueChange={onFormChange}
|
||||
nodeOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
nodeId={props.id}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</Field>
|
||||
<div className='px-4 py-2'>
|
||||
{isChatMode && currentStrategy?.features?.includes(AgentFeature.HISTORY_MESSAGES) && (
|
||||
<>
|
||||
<Split />
|
||||
<MemoryConfig
|
||||
className='mt-4'
|
||||
readonly={readOnly}
|
||||
config={{ data: inputs.memory }}
|
||||
onChange={handleMemoryChange}
|
||||
canSetRoleName={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<OutputVars>
|
||||
<VarItem
|
||||
name='text'
|
||||
type='String'
|
||||
description={t(`${i18nPrefix}.outputVars.text`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='usage'
|
||||
type='object'
|
||||
description={t(`${i18nPrefix}.outputVars.usage`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='files'
|
||||
type='Array[File]'
|
||||
description={t(`${i18nPrefix}.outputVars.files.title`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='json'
|
||||
type='Array[Object]'
|
||||
description={t(`${i18nPrefix}.outputVars.json`)}
|
||||
/>
|
||||
{outputSchema.map(({ name, type, description }) => (
|
||||
<VarItem
|
||||
key={name}
|
||||
name={name}
|
||||
type={type}
|
||||
description={description}
|
||||
/>
|
||||
))}
|
||||
</OutputVars>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
AgentPanel.displayName = 'AgentPanel'
|
||||
|
||||
export default memo(AgentPanel)
|
||||
20
dify/web/app/components/workflow/nodes/agent/types.ts
Normal file
20
dify/web/app/components/workflow/nodes/agent/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { CommonNodeType, Memory } from '@/app/components/workflow/types'
|
||||
import type { ToolVarInputs } from '../tool/types'
|
||||
import type { PluginMeta } from '@/app/components/plugins/types'
|
||||
|
||||
export type AgentNodeType = CommonNodeType & {
|
||||
agent_strategy_provider_name?: string
|
||||
agent_strategy_name?: string
|
||||
agent_strategy_label?: string
|
||||
agent_parameters?: ToolVarInputs
|
||||
meta?: PluginMeta
|
||||
output_schema: Record<string, any>
|
||||
plugin_unique_identifier?: string
|
||||
memory?: Memory
|
||||
version?: string
|
||||
tool_node_version?: string
|
||||
}
|
||||
|
||||
export enum AgentFeature {
|
||||
HISTORY_MESSAGES = 'history-messages',
|
||||
}
|
||||
227
dify/web/app/components/workflow/nodes/agent/use-config.ts
Normal file
227
dify/web/app/components/workflow/nodes/agent/use-config.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useStrategyProviderDetail } from '@/service/use-strategy'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import type { AgentNodeType } from './types'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { type ToolVarInputs, VarType } from '../tool/types'
|
||||
import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
|
||||
import type { Memory, Var } from '../../types'
|
||||
import { VarType as VarKindType } from '../../types'
|
||||
import useAvailableVarList from '../_base/hooks/use-available-var-list'
|
||||
import { produce } from 'immer'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { isSupportMCP } from '@/utils/plugin-version-feature'
|
||||
import { generateAgentToolValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
|
||||
export type StrategyStatus = {
|
||||
plugin: {
|
||||
source: 'external' | 'marketplace'
|
||||
installed: boolean
|
||||
}
|
||||
isExistInPlugin: boolean
|
||||
}
|
||||
|
||||
export const useStrategyInfo = (
|
||||
strategyProviderName?: string,
|
||||
strategyName?: string,
|
||||
) => {
|
||||
const strategyProvider = useStrategyProviderDetail(
|
||||
strategyProviderName || '',
|
||||
{ retry: false },
|
||||
)
|
||||
const strategy = strategyProvider.data?.declaration.strategies.find(
|
||||
str => str.identity.name === strategyName,
|
||||
)
|
||||
const marketplace = useFetchPluginsInMarketPlaceByIds([strategyProviderName!], {
|
||||
retry: false,
|
||||
})
|
||||
const strategyStatus: StrategyStatus | undefined = useMemo(() => {
|
||||
if (strategyProvider.isLoading || marketplace.isLoading)
|
||||
return undefined
|
||||
const strategyExist = !!strategy
|
||||
const isPluginInstalled = !strategyProvider.isError
|
||||
const isInMarketplace = !!marketplace.data?.data.plugins.at(0)
|
||||
return {
|
||||
plugin: {
|
||||
source: isInMarketplace ? 'marketplace' : 'external',
|
||||
installed: isPluginInstalled,
|
||||
},
|
||||
isExistInPlugin: strategyExist,
|
||||
}
|
||||
}, [strategy, marketplace, strategyProvider.isError, strategyProvider.isLoading])
|
||||
const refetch = useCallback(() => {
|
||||
strategyProvider.refetch()
|
||||
marketplace.refetch()
|
||||
}, [marketplace, strategyProvider])
|
||||
return {
|
||||
strategyProvider,
|
||||
strategy,
|
||||
strategyStatus,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
|
||||
const useConfig = (id: string, payload: AgentNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<AgentNodeType>(id, payload)
|
||||
// variables
|
||||
const { handleVarListChange, handleAddVariable } = useVarList<AgentNodeType>({
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
const {
|
||||
strategyStatus: currentStrategyStatus,
|
||||
strategy: currentStrategy,
|
||||
strategyProvider,
|
||||
} = useStrategyInfo(
|
||||
inputs.agent_strategy_provider_name,
|
||||
inputs.agent_strategy_name,
|
||||
)
|
||||
const pluginId = inputs.agent_strategy_provider_name?.split('/').splice(0, 2).join('/')
|
||||
const pluginDetail = useCheckInstalled({
|
||||
pluginIds: [pluginId!],
|
||||
enabled: Boolean(pluginId),
|
||||
})
|
||||
const formData = useMemo(() => {
|
||||
const paramNameList = (currentStrategy?.parameters || []).map(item => item.name)
|
||||
const res = Object.fromEntries(
|
||||
Object.entries(inputs.agent_parameters || {}).filter(([name]) => paramNameList.includes(name)).map(([key, value]) => {
|
||||
return [key, value.value]
|
||||
}),
|
||||
)
|
||||
return res
|
||||
}, [inputs.agent_parameters, currentStrategy?.parameters])
|
||||
|
||||
const getParamVarType = useCallback((paramName: string) => {
|
||||
const isVariable = currentStrategy?.parameters.some(
|
||||
param => param.name === paramName && param.type === FormTypeEnum.any,
|
||||
)
|
||||
if (isVariable) return VarType.variable
|
||||
return VarType.constant
|
||||
}, [currentStrategy?.parameters])
|
||||
|
||||
const onFormChange = (value: Record<string, any>) => {
|
||||
const res: ToolVarInputs = {}
|
||||
Object.entries(value).forEach(([key, val]) => {
|
||||
res[key] = {
|
||||
type: getParamVarType(key),
|
||||
value: val,
|
||||
}
|
||||
})
|
||||
setInputs({
|
||||
...inputs,
|
||||
agent_parameters: res,
|
||||
})
|
||||
}
|
||||
|
||||
const formattingToolData = (data: any) => {
|
||||
const settingValues = generateAgentToolValue(data.settings, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form !== 'llm') as any))
|
||||
const paramValues = generateAgentToolValue(data.parameters, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form === 'llm') as any), true)
|
||||
const res = produce(data, (draft: any) => {
|
||||
draft.settings = settingValues
|
||||
draft.parameters = paramValues
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const formattingLegacyData = () => {
|
||||
if (inputs.version || inputs.tool_node_version)
|
||||
return inputs
|
||||
const newData = produce(inputs, (draft) => {
|
||||
const schemas = currentStrategy?.parameters || []
|
||||
Object.keys(draft.agent_parameters || {}).forEach((key) => {
|
||||
const targetSchema = schemas.find(schema => schema.name === key)
|
||||
if (targetSchema?.type === FormTypeEnum.toolSelector)
|
||||
draft.agent_parameters![key].value = formattingToolData(draft.agent_parameters![key].value)
|
||||
if (targetSchema?.type === FormTypeEnum.multiToolSelector)
|
||||
draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool))
|
||||
})
|
||||
draft.tool_node_version = '2'
|
||||
})
|
||||
return newData
|
||||
}
|
||||
|
||||
// formatting legacy data
|
||||
useEffect(() => {
|
||||
if (!currentStrategy)
|
||||
return
|
||||
const newData = formattingLegacyData()
|
||||
setInputs(newData)
|
||||
}, [currentStrategy])
|
||||
|
||||
// vars
|
||||
|
||||
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
|
||||
return [
|
||||
VarKindType.arrayObject,
|
||||
VarKindType.array,
|
||||
VarKindType.number,
|
||||
VarKindType.string,
|
||||
VarKindType.secret,
|
||||
VarKindType.arrayString,
|
||||
VarKindType.arrayNumber,
|
||||
VarKindType.file,
|
||||
VarKindType.arrayFile,
|
||||
].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const {
|
||||
availableVars,
|
||||
availableNodesWithParent,
|
||||
} = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: filterMemoryPromptVar,
|
||||
})
|
||||
|
||||
// single run
|
||||
|
||||
const outputSchema = useMemo(() => {
|
||||
const res: any[] = []
|
||||
if (!inputs.output_schema || !inputs.output_schema.properties)
|
||||
return []
|
||||
Object.keys(inputs.output_schema.properties).forEach((outputKey) => {
|
||||
const output = inputs.output_schema.properties[outputKey]
|
||||
res.push({
|
||||
name: outputKey,
|
||||
type: output.type === 'array'
|
||||
? `Array[${output.items?.type ? output.items.type.slice(0, 1).toLocaleUpperCase() + output.items.type.slice(1) : 'Unknown'}]`
|
||||
: `${output.type ? output.type.slice(0, 1).toLocaleUpperCase() + output.type.slice(1) : 'Unknown'}`,
|
||||
description: output.description,
|
||||
})
|
||||
})
|
||||
return res
|
||||
}, [inputs.output_schema])
|
||||
|
||||
const handleMemoryChange = useCallback((newMemory?: Memory) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.memory = newMemory
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
const isChatMode = useIsChatMode()
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
setInputs,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
currentStrategy,
|
||||
formData,
|
||||
onFormChange,
|
||||
currentStrategyStatus,
|
||||
strategyProvider: strategyProvider.data,
|
||||
pluginDetail: pluginDetail.data?.plugins.at(0),
|
||||
availableVars,
|
||||
availableNodesWithParent,
|
||||
outputSchema,
|
||||
handleMemoryChange,
|
||||
isChatMode,
|
||||
canChooseMCPTool: isSupportMCP(inputs.meta?.version),
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||
import { useMemo } from 'react'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import type { AgentNodeType } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import { useStrategyInfo } from './use-config'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import formatTracing from '@/app/components/workflow/run/utils/format-log'
|
||||
|
||||
type Params = {
|
||||
id: string,
|
||||
payload: AgentNodeType,
|
||||
runInputData: Record<string, any>
|
||||
runInputDataRef: RefObject<Record<string, any>>
|
||||
getInputVars: (textList: string[]) => InputVar[]
|
||||
setRunInputData: (data: Record<string, any>) => void
|
||||
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||
runResult: NodeTracing
|
||||
}
|
||||
const useSingleRunFormParams = ({
|
||||
id,
|
||||
payload,
|
||||
runInputData,
|
||||
getInputVars,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
}: Params) => {
|
||||
const { t } = useTranslation()
|
||||
const { inputs } = useNodeCrud<AgentNodeType>(id, payload)
|
||||
|
||||
const formData = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(inputs.agent_parameters || {}).map(([key, value]) => {
|
||||
return [key, value.value]
|
||||
}),
|
||||
)
|
||||
}, [inputs.agent_parameters])
|
||||
|
||||
const {
|
||||
strategy: currentStrategy,
|
||||
} = useStrategyInfo(
|
||||
inputs.agent_strategy_provider_name,
|
||||
inputs.agent_strategy_name,
|
||||
)
|
||||
|
||||
const allVarStrArr = (() => {
|
||||
const arr = currentStrategy?.parameters.filter(item => item.type === 'string').map((item) => {
|
||||
return formData[item.name]
|
||||
}) || []
|
||||
return arr
|
||||
})()
|
||||
|
||||
const varInputs = getInputVars?.(allVarStrArr)
|
||||
|
||||
const forms = useMemo(() => {
|
||||
const forms: FormProps[] = []
|
||||
|
||||
if (varInputs!.length > 0) {
|
||||
forms.push(
|
||||
{
|
||||
label: t('workflow.nodes.llm.singleRun.variable')!,
|
||||
inputs: varInputs!,
|
||||
values: runInputData,
|
||||
onChange: setRunInputData,
|
||||
},
|
||||
)
|
||||
}
|
||||
return forms
|
||||
}, [runInputData, setRunInputData, t, varInputs])
|
||||
|
||||
const nodeInfo = useMemo(() => {
|
||||
if (!runResult)
|
||||
return
|
||||
return formatTracing([runResult], t)[0]
|
||||
}, [runResult, t])
|
||||
|
||||
const getDependentVars = () => {
|
||||
return varInputs.map((item) => {
|
||||
// Guard against null/undefined variable to prevent app crash
|
||||
if (!item.variable || typeof item.variable !== 'string')
|
||||
return []
|
||||
|
||||
return item.variable.slice(1, -1).split('.')
|
||||
}).filter(arr => arr.length > 0)
|
||||
}
|
||||
|
||||
return {
|
||||
forms,
|
||||
nodeInfo,
|
||||
getDependentVars,
|
||||
}
|
||||
}
|
||||
|
||||
export default useSingleRunFormParams
|
||||
Reference in New Issue
Block a user