diff --git a/web/app/components/base/input-number/index.tsx b/web/app/components/base/input-number/index.tsx new file mode 100644 index 0000000000..cccb9c8f41 --- /dev/null +++ b/web/app/components/base/input-number/index.tsx @@ -0,0 +1,86 @@ +import type { FC } from 'react' +import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react' +import Input, { type InputProps } from '../input' +import classNames from '@/utils/classnames' + +export type InputNumberProps = { + unit?: string + value?: number + onChange: (value?: number) => void + amount?: number + size?: 'sm' | 'md' + max?: number + min?: number + defaultValue?: number +} & Omit + +export const InputNumber: FC = (props) => { + const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, ...rest } = props + + const isValidValue = (v: number) => { + if (max && v > max) + return false + if (min && v < min) + return false + return true + } + + const inc = () => { + if (value === undefined) { + onChange(defaultValue) + return + } + const newValue = value + amount + if (!isValidValue(newValue)) + return + onChange(newValue) + } + const dec = () => { + if (value === undefined) { + onChange(defaultValue) + return + } + const newValue = value - amount + if (!isValidValue(newValue)) + return + onChange(newValue) + } + + return
+ { + if (e.target.value === '') + onChange(undefined) + + const parsed = Number(e.target.value) + if (Number.isNaN(parsed)) + return + + if (!isValidValue(parsed)) + return + onChange(parsed) + }} + /> + {unit &&
{unit}
} +
+ + +
+
+} diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index 1001e981c5..3656c42b3f 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -53,6 +53,8 @@ const getIcon = (type: BlockEnum, className: string) => { [BlockEnum.ParameterExtractor]: , [BlockEnum.DocExtractor]: , [BlockEnum.ListFilter]: , + // TODO: add icon for Agent + [BlockEnum.Agent]: , }[type] } const ICON_CONTAINER_BG_COLOR_MAP: Record = { @@ -73,6 +75,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record = { [BlockEnum.ParameterExtractor]: 'bg-util-colors-blue-blue-500', [BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500', [BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500', + [BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500', } const BlockIcon: FC = ({ type, diff --git a/web/app/components/workflow/block-selector/constants.tsx b/web/app/components/workflow/block-selector/constants.tsx index 2849288404..798e7ae3c5 100644 --- a/web/app/components/workflow/block-selector/constants.tsx +++ b/web/app/components/workflow/block-selector/constants.tsx @@ -84,6 +84,11 @@ export const BLOCKS: Block[] = [ type: BlockEnum.ListFilter, title: 'List Filter', }, + { + classification: BlockClassificationEnum.Default, + type: BlockEnum.Agent, + title: 'Agent', + }, ] export const BLOCK_CLASSIFICATIONS: string[] = [ diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 67a419a846..922caded51 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -18,8 +18,9 @@ import IterationDefault from './nodes/iteration/default' import DocExtractorDefault from './nodes/document-extractor/default' import ListFilterDefault from './nodes/list-operator/default' import IterationStartDefault from './nodes/iteration-start/default' +import AgentDefault from './nodes/agent/default' -interface NodesExtraData { +type NodesExtraData = { author: string about: string availablePrevNodes: BlockEnum[] @@ -200,7 +201,15 @@ export const NODES_EXTRA_DATA: Record = { getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes, checkValid: ListFilterDefault.checkValid, }, - + [BlockEnum.Agent]: { + author: 'Dify', + about: '', + availablePrevNodes: [], + availableNextNodes: [], + getAvailablePrevNodes: ListFilterDefault.getAvailablePrevNodes, + getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes, + checkValid: AgentDefault.checkValid, + }, } export const ALL_CHAT_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[] @@ -339,6 +348,12 @@ export const NODES_INITIAL_DATA = { desc: '', ...ListFilterDefault.defaultValue, }, + [BlockEnum.Agent]: { + type: BlockEnum.Agent, + title: '', + desc: '', + ...AgentDefault.defaultValue, + }, } export const MAX_ITERATION_PARALLEL_NUM = 10 export const MIN_ITERATION_PARALLEL_NUM = 1 diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx new file mode 100644 index 0000000000..e82e66776f --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -0,0 +1,86 @@ +import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' +import { useState } from 'react' +import AllTools from '../../../block-selector/all-tools' +import type { Strategy } from './agent-strategy' +import classNames from '@/utils/classnames' +import { RiArrowDownSLine, RiErrorWarningFill } from '@remixicon/react' +import { useAllBuiltInTools } from '@/service/use-tools' +import Tooltip from '@/app/components/base/tooltip' +import Link from 'next/link' +import { InstallPluginButton } from './install-plugin-button' + +const ExternalNotInstallWarn = () => { + // TODO: add i18n label + return +

This plugin is not installed

+

This plugin is installed from GitHub. Please go to Plugins to reinstall

+

+ Link to Plugins +

+ } + needsDelay + > +
+ +
+
+} + +export type AgentStrategySelectorProps = { + value?: Strategy, + onChange: (value?: Strategy) => void, +} + +export const AgentStrategySelector = (props: AgentStrategySelectorProps) => { + const { value, onChange } = props + const [open, setOpen] = useState(false) + const list = useAllBuiltInTools() + // TODO: should be replaced by real data + const isExternalInstalled = true + return + +
setOpen(true)}> + {list.data && coll, + )?.icon as string} + width={24} + height={24} + className='rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge' + alt='icon' + />} +

+ {value?.agent_strategy_name || 'Select agentic strategy'} +

+
+ e.preventDefault()} /> + {isExternalInstalled ? : } +
+
+
+ + {list.data && { + if (!tool) { + // TODO: should not be called, try it + return + } + onChange({ + agent_strategy_name: tool.title, + agent_strategy_provider_name: tool.provider_name, + agent_parameters: {}, + }) + }} + />} + +
+} diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx new file mode 100644 index 0000000000..0981f8b7d6 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -0,0 +1,39 @@ +import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { ToolVarInputs } from '../../tool/types' +import ListEmpty from '@/app/components/base/list-empty' +import { AgentStrategySelector } from './agent-strategy-selector' +import Link from 'next/link' + +export type Strategy = { + agent_strategy_provider_name: string + agent_strategy_name: string + agent_strategy_label?: string + agent_parameters?: ToolVarInputs +} + +export type AgentStrategyProps = { + strategy?: Strategy + onStrategyChange: (strategy?: Strategy) => void + formSchema: CredentialFormSchema[] + formValue: ToolVarInputs + onFormValueChange: (value: ToolVarInputs) => void +} + +export const AgentStrategy = (props: AgentStrategyProps) => { + const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange } = props + return
+ + { + strategy + ?
+ // TODO: list empty need a icon + : + After configuring the agentic strategy, this node will automatically load the remaining configurations. The strategy will affect the mechanism of multi-step tool reasoning.
+ Learn more +
} + /> + } + +} diff --git a/web/app/components/workflow/nodes/_base/components/group.tsx b/web/app/components/workflow/nodes/_base/components/group.tsx new file mode 100644 index 0000000000..ad2e3f0596 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/group.tsx @@ -0,0 +1,25 @@ +import classNames from '@/utils/classnames' +import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react' + +export type GroupLabelProps = ComponentProps<'div'> + +export const GroupLabel: FC = (props) => { + const { children, className, ...rest } = props + return
+ {children} +
+} + +export type Group = PropsWithChildren<{ + label: ReactNode +}> + +export const Group: FC = (props) => { + const { children, label } = props + return
+ {label} +
+ {children} +
+
+} diff --git a/web/app/components/workflow/nodes/_base/components/info-panel.tsx b/web/app/components/workflow/nodes/_base/components/info-panel.tsx index 1a06425fdc..e960931d49 100644 --- a/web/app/components/workflow/nodes/_base/components/info-panel.tsx +++ b/web/app/components/workflow/nodes/_base/components/info-panel.tsx @@ -1,10 +1,10 @@ 'use client' -import type { FC } from 'react' +import type { FC, ReactNode } from 'react' import React from 'react' -interface Props { +type Props = { title: string - content: string | JSX.Element + content: ReactNode } const InfoPanel: FC = ({ diff --git a/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx b/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx new file mode 100644 index 0000000000..08095b8e0b --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/install-plugin-button.tsx @@ -0,0 +1,15 @@ +import Button from '@/app/components/base/button' +import { RiInstallLine, RiLoader2Line } from '@remixicon/react' +import type { ComponentProps } from 'react' +import classNames from '@/utils/classnames' + +type InstallPluginButtonProps = Omit, 'children'> + +export const InstallPluginButton = (props: InstallPluginButtonProps) => { + const { loading, className, ...rest } = props + // TODO: add i18n label + return +} diff --git a/web/app/components/workflow/nodes/_base/components/setting-item.tsx b/web/app/components/workflow/nodes/_base/components/setting-item.tsx new file mode 100644 index 0000000000..865f445d38 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/setting-item.tsx @@ -0,0 +1,19 @@ +import Indicator from '@/app/components/header/indicator' +import type { ComponentProps, PropsWithChildren } from 'react' + +export type SettingItemProps = PropsWithChildren<{ + label: string + indicator?: ComponentProps['color'] +}> + +export const SettingItem = ({ label, children, indicator }: SettingItemProps) => { + return
+
+ {label} +
+
+ {children} +
+ {indicator && } +
+} diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index e3da18326b..7ad8bcd834 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -293,6 +293,11 @@ const formatItem = ( break } + case BlockEnum.Agent: { + res.vars = [] + break + } + case 'env': { res.vars = data.envList.map((env: EnvironmentVariable) => { return { diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx new file mode 100644 index 0000000000..aca1a75f5d --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -0,0 +1,34 @@ +import Tooltip from '@/app/components/base/tooltip' +import Indicator from '@/app/components/header/indicator' +import classNames from '@/utils/classnames' +import { useRef } from 'react' + +export type ToolIconProps = { + src: string + alt?: string + status?: 'error' | 'warning' + tooltip?: string +} + +export const ToolIcon = ({ src, status, tooltip, alt }: ToolIconProps) => { + const indicator = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined + const containerRef = useRef(null) + const notSuccess = (['error', 'warning'] as Array).includes(status) + return +
+ {alt} + {indicator && } +
+
+} diff --git a/web/app/components/workflow/nodes/agent/default.ts b/web/app/components/workflow/nodes/agent/default.ts new file mode 100644 index 0000000000..f5300cbe77 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/default.ts @@ -0,0 +1,33 @@ +import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '../../constants' +import type { NodeDefault } from '../../types' +import type { AgentNodeType } from './types' + +const nodeDefault: NodeDefault = { + defaultValue: { + max_iterations: 3, + }, + getAvailablePrevNodes(isChatMode) { + return isChatMode + ? ALL_CHAT_AVAILABLE_BLOCKS + : ALL_COMPLETION_AVAILABLE_BLOCKS + }, + getAvailableNextNodes(isChatMode) { + return isChatMode + ? ALL_CHAT_AVAILABLE_BLOCKS + : ALL_COMPLETION_AVAILABLE_BLOCKS + }, + checkValid(payload) { + let isValid = true + let errorMessages = '' + if (payload.type) { + isValid = true + errorMessages = '' + } + return { + isValid, + errorMessage: errorMessages, + } + }, +} + +export default nodeDefault diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx new file mode 100644 index 0000000000..3dc61ddb41 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -0,0 +1,47 @@ +import type { FC } from 'react' +import type { NodeProps } from '../../types' +import type { AgentNodeType } from './types' +import { SettingItem } from '../_base/components/setting-item' +import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' +import { Group, GroupLabel } from '../_base/components/group' +import { ToolIcon } from './components/tool-icon' + +const AgentNode: FC> = (props) => { + const strategySelected = true + return
+ {strategySelected + // TODO: add tooltip for this + ? + ReAct + + : } + + Model + }> + + + + + + Toolbox + }> +
+ + + +
+
+
+} + +export default AgentNode diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx new file mode 100644 index 0000000000..1e65cb99b8 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -0,0 +1,61 @@ +import type { FC } from 'react' +import type { NodePanelProps } from '../../types' +import type { AgentNodeType } from './types' +import Field from '../_base/components/field' +import { InputNumber } from '@/app/components/base/input-number' +import Slider from '@/app/components/base/slider' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { AgentStrategy } from '../_base/components/agent-strategy' + +const AgentPanel: FC> = (props) => { + const { inputs, setInputs } = useNodeCrud(props.id, props.data) + const [iter, setIter] = [inputs.max_iterations, (value: number) => { + setInputs({ + ...inputs, + max_iterations: value, + }) + }] + return <> + + { + setInputs({ + ...inputs, + agent_strategy_provider_name: strategy?.agent_strategy_provider_name, + agent_strategy_name: strategy?.agent_strategy_name, + agent_parameters: strategy?.agent_parameters, + }) + }} + formSchema={[]} + formValue={{}} + onFormValueChange={console.error} + /> + + + + + +
+ + +
+
+ +} + +export default AgentPanel diff --git a/web/app/components/workflow/nodes/agent/types.ts b/web/app/components/workflow/nodes/agent/types.ts new file mode 100644 index 0000000000..93a2e6f6fd --- /dev/null +++ b/web/app/components/workflow/nodes/agent/types.ts @@ -0,0 +1,11 @@ +import type { CommonNodeType } from '@/app/components/workflow/types' +import type { ToolVarInputs } from '../tool/types' + +export type AgentNodeType = CommonNodeType & { + max_iterations: number + agent_strategy_provider_name?: string + agent_strategy_name?: string + agent_strategy_label?: string + agent_parameters?: ToolVarInputs, + agent_configurations?: Record +} diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts new file mode 100644 index 0000000000..f0495bf773 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -0,0 +1,25 @@ +import useNodeCrud from '../_base/hooks/use-node-crud' +import useVarList from '../_base/hooks/use-var-list' +import type { AgentNodeType } from './types' +import { + useNodesReadOnly, +} from '@/app/components/workflow/hooks' + +const useConfig = (id: string, payload: AgentNodeType) => { + const { nodesReadOnly: readOnly } = useNodesReadOnly() + const { inputs, setInputs } = useNodeCrud(id, payload) + // variables + const { handleVarListChange, handleAddVariable } = useVarList({ + inputs, + setInputs, + }) + + return { + readOnly, + inputs, + handleVarListChange, + handleAddVariable, + } +} + +export default useConfig diff --git a/web/app/components/workflow/nodes/agent/utils.ts b/web/app/components/workflow/nodes/agent/utils.ts new file mode 100644 index 0000000000..25beff3eb2 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/utils.ts @@ -0,0 +1,5 @@ +import type { AgentNodeType } from './types' + +export const checkNodeValid = (payload: AgentNodeType) => { + return true +} diff --git a/web/app/components/workflow/nodes/constants.ts b/web/app/components/workflow/nodes/constants.ts index 82a21d9a58..dc202acc28 100644 --- a/web/app/components/workflow/nodes/constants.ts +++ b/web/app/components/workflow/nodes/constants.ts @@ -34,6 +34,8 @@ import DocExtractorNode from './document-extractor/node' import DocExtractorPanel from './document-extractor/panel' import ListFilterNode from './list-operator/node' import ListFilterPanel from './list-operator/panel' +import AgentNode from './agent/node' +import AgentPanel from './agent/panel' export const NodeComponentMap: Record> = { [BlockEnum.Start]: StartNode, @@ -54,6 +56,7 @@ export const NodeComponentMap: Record> = { [BlockEnum.Iteration]: IterationNode, [BlockEnum.DocExtractor]: DocExtractorNode, [BlockEnum.ListFilter]: ListFilterNode, + [BlockEnum.Agent]: AgentNode, } export const PanelComponentMap: Record> = { @@ -75,6 +78,7 @@ export const PanelComponentMap: Record> = { [BlockEnum.Iteration]: IterationPanel, [BlockEnum.DocExtractor]: DocExtractorPanel, [BlockEnum.ListFilter]: ListFilterPanel, + [BlockEnum.Agent]: AgentPanel, } export const CUSTOM_NODE_TYPE = 'custom' diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 6d0fabd90e..78bd650036 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -35,6 +35,7 @@ export enum BlockEnum { ListFilter = 'list-operator', IterationStart = 'iteration-start', Assigner = 'assigner', // is now named as VariableAssigner + Agent = 'agent', } export enum ControlMode { diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index cb17944c97..8ead4696ce 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -236,6 +236,7 @@ const translation = { 'parameter-extractor': 'Parameter Extractor', 'document-extractor': 'Doc Extractor', 'list-operator': 'List Operator', + 'agent': 'Agent', }, blocksAbout: { 'start': 'Define the initial parameters for launching a workflow', @@ -255,6 +256,7 @@ const translation = { 'parameter-extractor': 'Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.', 'document-extractor': 'Used to parse uploaded documents into text content that is easily understandable by LLM.', 'list-operator': 'Used to filter or sort array content.', + 'agent': 'TODO: add text here', }, operator: { zoomIn: 'Zoom In', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index b56a54db07..93276648e9 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -236,6 +236,7 @@ const translation = { 'parameter-extractor': '参数提取器', 'document-extractor': '文档提取器', 'list-operator': '列表操作', + 'agent': 'Agent', }, blocksAbout: { 'start': '定义一个 workflow 流程启动的初始参数', @@ -255,6 +256,7 @@ const translation = { 'parameter-extractor': '利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。', 'document-extractor': '用于将用户上传的文档解析为 LLM 便于理解的文本内容。', 'list-operator': '用于过滤或排序数组内容。', + 'agent': 'TODO: Agent', }, operator: { zoomIn: '放大',