chore: add warning ui for agentic stragey

This commit is contained in:
AkaraChen 2024-12-24 14:20:15 +08:00
parent e2e2090e0c
commit 7c460eb6e7
22 changed files with 528 additions and 5 deletions

View File

@ -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<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'>
export const InputNumber: FC<InputNumberProps> = (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 <div className='flex'>
<Input {...rest}
// disable default controller
type='text'
className={classNames('rounded-r-none', className)}
value={value}
max={max}
min={min}
onChange={(e) => {
if (e.target.value === '')
onChange(undefined)
const parsed = Number(e.target.value)
if (Number.isNaN(parsed))
return
if (!isValidValue(parsed))
return
onChange(parsed)
}}
/>
{unit && <div className='flex items-center bg-components-input-bg-normal text-[13px] text-text-placeholder pr-2'>{unit}</div>}
<div className='flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs'>
<button onClick={inc} className={classNames(
size === 'sm' ? 'pt-1' : 'pt-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
)}>
<RiArrowUpSLine className='size-3' />
</button>
<button onClick={dec} className={classNames(
size === 'sm' ? 'pb-1' : 'pb-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
)}>
<RiArrowDownSLine className='size-3' />
</button>
</div>
</div>
}

View File

@ -53,6 +53,8 @@ const getIcon = (type: BlockEnum, className: string) => {
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
[BlockEnum.DocExtractor]: <DocsExtractor className={className} />,
[BlockEnum.ListFilter]: <ListFilter className={className} />,
// TODO: add icon for Agent
[BlockEnum.Agent]: <VariableX className={className} />,
}[type]
}
const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
@ -73,6 +75,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
[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<BlockIconProps> = ({
type,

View File

@ -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[] = [

View File

@ -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<BlockEnum, NodesExtraData> = {
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

View File

@ -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 <Tooltip
popupContent={<div className='space-y-1 text-xs'>
<h3 className='text-text-primary font-semibold'>This plugin is not installed</h3>
<p className='text-text-secondary tracking-tight'>This plugin is installed from GitHub. Please go to Plugins to reinstall</p>
<p>
<Link href={'/plugins'} className='text-text-accent tracking-tight'>Link to Plugins</Link>
</p>
</div>}
needsDelay
>
<div>
<RiErrorWarningFill className='text-text-destructive size-4' />
</div>
</Tooltip>
}
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 <PortalToFollowElem open={open} onOpenChange={setOpen}>
<PortalToFollowElemTrigger className='w-full'>
<div className='py-2 pl-3 pr-2 flex items-center rounded-lg bg-components-input-bg-normal w-full hover:bg-state-base-hover-alt' onClick={() => setOpen(true)}>
{list.data && <img
src={list.data.find(
coll => 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'
/>}
<p
className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'text-xs px-1')}
>
{value?.agent_strategy_name || 'Select agentic strategy'}
</p>
<div className='ml-auto flex items-center gap-1'>
<InstallPluginButton onClick={e => e.preventDefault()} />
{isExternalInstalled ? <ExternalNotInstallWarn /> : <RiArrowDownSLine className='size-4 text-text-tertiary' />}
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
{list.data && <AllTools
className='border-components-panel-border bg-components-panel-bg-blur'
searchText=''
tags={[]}
buildInTools={list.data}
customTools={[]}
workflowTools={[]}
onSelect={(_e, tool) => {
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: {},
})
}}
/>}
</PortalToFollowElemContent>
</PortalToFollowElem>
}

View File

@ -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 <div className='space-y-2'>
<AgentStrategySelector value={strategy} onChange={onStrategyChange} />
{
strategy
? <div></div>
// TODO: list empty need a icon
: <ListEmpty
title='Please configure agentic strategy.'
description={<div className='text-text-tertiary text-xs'>
After configuring the agentic strategy, this node will automatically load the remaining configurations. The strategy will affect the mechanism of multi-step tool reasoning. <br />
<Link href={'/'} className='text-text-accent-secondary'>Learn more</Link>
</div>}
/>
}
</div>
}

View File

@ -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<GroupLabelProps> = (props) => {
const { children, className, ...rest } = props
return <div {...rest} className={classNames('mb-1 system-2xs-medium-uppercase text-text-tertiary', className)}>
{children}
</div>
}
export type Group = PropsWithChildren<{
label: ReactNode
}>
export const Group: FC<Group> = (props) => {
const { children, label } = props
return <div className={classNames('py-1')}>
{label}
<div className='space-y-0.5'>
{children}
</div>
</div>
}

View File

@ -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<Props> = ({

View File

@ -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<ComponentProps<typeof Button>, 'children'>
export const InstallPluginButton = (props: InstallPluginButtonProps) => {
const { loading, className, ...rest } = props
// TODO: add i18n label
return <Button variant={'secondary'} disabled={loading} className={classNames('flex items-center', className)} {...rest}>
{loading ? 'Installing' : 'Install'}
{!loading ? <RiInstallLine className='size-4 ml-1' /> : <RiLoader2Line className='size-4 ml-1 animate-spin' />}
</Button>
}

View File

@ -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<typeof Indicator>['color']
}>
export const SettingItem = ({ label, children, indicator }: SettingItemProps) => {
return <div className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal relative'>
<div className='max-w-[100px] shrink-0 truncate text-xs font-medium text-text-tertiary uppercase'>
{label}
</div>
<div className='grow w-0 shrink-0 truncate text-right text-xs font-normal text-text-secondary'>
{children}
</div>
{indicator && <Indicator color={indicator} className='absolute -right-0.5 -top-0.5' />}
</div>
}

View File

@ -293,6 +293,11 @@ const formatItem = (
break
}
case BlockEnum.Agent: {
res.vars = []
break
}
case 'env': {
res.vars = data.envList.map((env: EnvironmentVariable) => {
return {

View File

@ -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<HTMLDivElement>(null)
const notSuccess = (['error', 'warning'] as Array<ToolIconProps['status']>).includes(status)
return <Tooltip triggerMethod='hover' popupContent={tooltip} disabled={!notSuccess}>
<div className={classNames(
'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative',
)}
ref={containerRef}
>
<img
src={src}
alt={alt}
className={classNames(
'w-full h-full max-w-5 max-h-5 object-cover rounded-[6px]',
notSuccess && 'opacity-50',
)}
/>
{indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />}
</div>
</Tooltip>
}

View File

@ -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<AgentNodeType> = {
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

View File

@ -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<NodeProps<AgentNodeType>> = (props) => {
const strategySelected = true
return <div className='mb-1 px-3 py-1 space-y-1'>
{strategySelected
// TODO: add tooltip for this
? <SettingItem label='Strategy' indicator='red'>
ReAct
</SettingItem>
: <SettingItem label='Agentic strategy Not Set' />}
<Group label={
<GroupLabel className='mt-1'>
Model
</GroupLabel>}>
<ModelSelector
modelList={[]}
readonly
/>
<ModelSelector
modelList={[]}
readonly
/>
<ModelSelector
modelList={[]}
readonly
/>
</Group>
<Group label={<GroupLabel className='mt-1'>
Toolbox
</GroupLabel>}>
<div className='grid grid-cols-10 gap-0.5'>
<ToolIcon src='/logo/logo.png' />
<ToolIcon src='/logo/logo.png' status='error' tooltip='Gmail Sender is not installed' />
<ToolIcon src='/logo/logo.png' status='warning' tooltip='DuckDuckGo AI Search Not Authorized' />
</div>
</Group>
</div>
}
export default AgentNode

View File

@ -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<NodePanelProps<AgentNodeType>> = (props) => {
const { inputs, setInputs } = useNodeCrud(props.id, props.data)
const [iter, setIter] = [inputs.max_iterations, (value: number) => {
setInputs({
...inputs,
max_iterations: value,
})
}]
return <>
<Field title={'Strategy'} className='px-4' >
<AgentStrategy
strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,
agent_strategy_name: inputs.agent_strategy_name!,
agent_parameters: inputs.agent_parameters,
} : undefined}
onStrategyChange={(strategy) => {
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}
/>
</Field>
<Field title={'tools'} className='px-4'>
</Field>
<Field title={'max iterations'} tooltip={'max iter'} inline className='px-4'>
<div className='flex w-[200px] items-center gap-3'>
<Slider value={iter} onChange={setIter} className='w-full' min={1} max={10} />
<InputNumber
value={iter}
// TODO: maybe empty, handle this
onChange={setIter as any}
defaultValue={3}
size='sm'
min={1}
max={10}
className='w-12'
placeholder=''
/>
</div>
</Field>
</>
}
export default AgentPanel

View File

@ -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<string, ToolVarInputs>
}

View File

@ -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<AgentNodeType>(id, payload)
// variables
const { handleVarListChange, handleAddVariable } = useVarList<AgentNodeType>({
inputs,
setInputs,
})
return {
readOnly,
inputs,
handleVarListChange,
handleAddVariable,
}
}
export default useConfig

View File

@ -0,0 +1,5 @@
import type { AgentNodeType } from './types'
export const checkNodeValid = (payload: AgentNodeType) => {
return true
}

View File

@ -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<string, ComponentType<any>> = {
[BlockEnum.Start]: StartNode,
@ -54,6 +56,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Iteration]: IterationNode,
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: AgentNode,
}
export const PanelComponentMap: Record<string, ComponentType<any>> = {
@ -75,6 +78,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Iteration]: IterationPanel,
[BlockEnum.DocExtractor]: DocExtractorPanel,
[BlockEnum.ListFilter]: ListFilterPanel,
[BlockEnum.Agent]: AgentPanel,
}
export const CUSTOM_NODE_TYPE = 'custom'

View File

@ -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 {

View File

@ -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',

View File

@ -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: '放大',