mirror of https://github.com/langgenius/dify.git
feat: add activation conditions for agent tools
- Implemented activation conditions for agent tools, allowing tools to be enabled based on specified conditions. - Introduced new components for managing conditions, including ConditionAdd, ConditionList, ConditionItem, and ConditionOperator. - Enhanced the AgentNode to process activation conditions when filtering tools. - Updated the ToolSelector to include activation condition management. - Added translations for new UI elements related to activation conditions. - Refactored utility functions to support condition operators and default values based on variable types.
This commit is contained in:
parent
775d2e14fc
commit
f263492c03
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from packaging.version import Version
|
||||
from pydantic import ValidationError
|
||||
|
|
@ -44,6 +44,8 @@ from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig
|
|||
from core.workflow.nodes.base.node import Node
|
||||
from core.workflow.nodes.base.variable_template_parser import VariableTemplateParser
|
||||
from core.workflow.runtime import VariablePool
|
||||
from core.workflow.utils.condition.entities import Condition
|
||||
from core.workflow.utils.condition.processor import ConditionProcessor
|
||||
from extensions.ext_database import db
|
||||
from factories import file_factory
|
||||
from factories.agent_factory import get_plugin_agent_strategy
|
||||
|
|
@ -206,6 +208,7 @@ class AgentNode(Node):
|
|||
|
||||
"""
|
||||
agent_parameters_dictionary = {parameter.name: parameter for parameter in agent_parameters}
|
||||
condition_processor = ConditionProcessor()
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
for parameter_name in node_data.agent_parameters:
|
||||
|
|
@ -243,7 +246,32 @@ class AgentNode(Node):
|
|||
value = parameter_value
|
||||
if parameter.type == "array[tools]":
|
||||
value = cast(list[dict[str, Any]], value)
|
||||
value = [tool for tool in value if tool.get("enabled", False)]
|
||||
filtered_tools: list[dict[str, Any]] = []
|
||||
for tool in value:
|
||||
activation_condition = tool.get("activation_condition")
|
||||
include_tool = True
|
||||
if activation_condition and activation_condition.get("enabled"):
|
||||
logical_operator = activation_condition.get("logical_operator", "and")
|
||||
if logical_operator not in {"and", "or"}:
|
||||
logical_operator = "and"
|
||||
try:
|
||||
conditions_raw = activation_condition.get("conditions", []) or []
|
||||
conditions = [Condition.model_validate(condition) for condition in conditions_raw]
|
||||
if conditions:
|
||||
_, _, include_tool = condition_processor.process_conditions(
|
||||
variable_pool=variable_pool,
|
||||
conditions=conditions,
|
||||
operator=cast(Literal["and", "or"], logical_operator),
|
||||
)
|
||||
else:
|
||||
include_tool = False
|
||||
except (ValidationError, ValueError):
|
||||
include_tool = False
|
||||
tool.pop("activation_condition", None)
|
||||
if include_tool:
|
||||
filtered_tools.append(tool)
|
||||
value = [tool for tool in filtered_tools if tool.get("enabled", False)]
|
||||
|
||||
value = self._filter_mcp_type_tool(strategy, value)
|
||||
for tool in value:
|
||||
if "schemas" in tool:
|
||||
|
|
|
|||
|
|
@ -202,10 +202,10 @@ const SimpleSelect: FC<ISelectProps> = ({
|
|||
setSelectedItem(defaultSelect)
|
||||
}, [defaultValue])
|
||||
|
||||
const listboxRef = useRef<HTMLDivElement>(null)
|
||||
const openRef = useRef(false)
|
||||
|
||||
return (
|
||||
<Listbox ref={listboxRef}
|
||||
<Listbox
|
||||
value={selectedItem}
|
||||
onChange={(value: Item) => {
|
||||
if (!disabled) {
|
||||
|
|
@ -214,83 +214,100 @@ const SimpleSelect: FC<ISelectProps> = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
|
||||
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
|
||||
{!renderTrigger && (
|
||||
<ListboxButton onClick={() => {
|
||||
onOpenChange?.(open)
|
||||
}} className={classNames(`flex h-full w-full items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
|
||||
<span className={classNames('system-sm-regular block truncate text-left text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
|
||||
: (selectedItem && !notClearable)
|
||||
? (
|
||||
<XMarkIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItem(null)
|
||||
onSelect({ name: '', value: '' })
|
||||
}}
|
||||
className="h-4 w-4 cursor-pointer text-text-quaternary"
|
||||
aria-hidden="false"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
open ? (
|
||||
<ChevronUpIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
)}
|
||||
{({ open }) => {
|
||||
if (openRef.current !== open) {
|
||||
openRef.current = open
|
||||
onOpenChange?.(open)
|
||||
}
|
||||
|
||||
{(!disabled) && (
|
||||
<ListboxOptions className={classNames('absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm', optionWrapClassName)}>
|
||||
{items.map((item: Item) => (
|
||||
<ListboxOption
|
||||
key={item.value}
|
||||
className={
|
||||
classNames(
|
||||
'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
optionClassName,
|
||||
)
|
||||
}
|
||||
value={item}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ /* active, */ selected }) => (
|
||||
<>
|
||||
{renderOption
|
||||
? renderOption({ item, selected })
|
||||
: (<>
|
||||
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
|
||||
{selected && !hideChecked && (
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
|
||||
)}
|
||||
>
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>)}
|
||||
</>
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
triggerPopupSameWidth
|
||||
>
|
||||
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
|
||||
<PortalToFollowElemTrigger asChild>
|
||||
{renderTrigger
|
||||
? <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>
|
||||
: (
|
||||
<ListboxButton className={classNames(`relative flex h-full w-full items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
|
||||
<span className={classNames('system-sm-regular block truncate text-left text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
{isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
|
||||
: (selectedItem && !notClearable)
|
||||
? (
|
||||
<XMarkIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelectedItem(null)
|
||||
onSelect({ name: '', value: '' })
|
||||
}}
|
||||
className="h-4 w-4 cursor-pointer text-text-quaternary"
|
||||
aria-hidden="false"
|
||||
/>
|
||||
)
|
||||
: (
|
||||
open ? (
|
||||
<ChevronUpIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className="h-4 w-4 text-text-quaternary group-hover/simple-select:text-text-secondary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</ListboxButton>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
||||
{(!disabled) && (
|
||||
<PortalToFollowElemContent className={classNames('z-10 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', optionWrapClassName)}>
|
||||
<ListboxOptions className='max-h-60 w-full overflow-auto px-1 py-1 text-base focus:outline-none sm:text-sm'>
|
||||
{items.map((item: Item) => (
|
||||
<ListboxOption
|
||||
key={item.value}
|
||||
className={
|
||||
classNames(
|
||||
'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover',
|
||||
optionClassName,
|
||||
)
|
||||
}
|
||||
value={item}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{renderOption
|
||||
? renderOption({ item, selected })
|
||||
: (<>
|
||||
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
|
||||
{selected && !hideChecked && (
|
||||
<span
|
||||
className={classNames(
|
||||
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
|
||||
)}
|
||||
>
|
||||
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</PortalToFollowElemContent>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}}
|
||||
</Listbox>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
AuthCategory,
|
||||
PluginAuthInAgent,
|
||||
} from '@/app/components/plugins/plugin-auth'
|
||||
import { AgentToolConditionEditor } from '@/app/components/workflow/nodes/agent/components/tool-condition'
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
|
|
@ -128,6 +129,7 @@ const ToolSelector: FC<Props> = ({
|
|||
description: tool.tool_description,
|
||||
},
|
||||
schemas: tool.paramSchemas,
|
||||
activation_condition: value?.activation_condition,
|
||||
}
|
||||
}
|
||||
const handleSelectTool = (tool: ToolDefaultValue) => {
|
||||
|
|
@ -150,6 +152,15 @@ const ToolSelector: FC<Props> = ({
|
|||
} as any)
|
||||
}
|
||||
|
||||
const handleActivationConditionChange = (condition?: ToolValue['activation_condition']) => {
|
||||
if (!value)
|
||||
return
|
||||
onSelect({
|
||||
...value,
|
||||
activation_condition: condition,
|
||||
} as any)
|
||||
}
|
||||
|
||||
// tool settings & params
|
||||
const currentToolSettings = useMemo(() => {
|
||||
if (!currentProvider) return []
|
||||
|
|
@ -266,7 +277,7 @@ const ToolSelector: FC<Props> = ({
|
|||
)}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
|
||||
<div className={cn('relative max-h-[642px] min-h-20 w-[480px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
|
||||
<>
|
||||
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div>
|
||||
{/* base form */}
|
||||
|
|
@ -391,6 +402,20 @@ const ToolSelector: FC<Props> = ({
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{value?.provider_name && nodeId && (
|
||||
<>
|
||||
<Divider className='my-1 w-full' />
|
||||
<div className='px-4 py-2'>
|
||||
<AgentToolConditionEditor
|
||||
value={value.activation_condition}
|
||||
onChange={handleActivationConditionChange}
|
||||
availableVars={nodeOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { PluginMeta } from '../../plugins/types'
|
||||
import type { AgentToolActivationCondition } from '@/app/components/workflow/nodes/agent/types'
|
||||
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ export type ToolValue = {
|
|||
enabled?: boolean
|
||||
extra?: Record<string, any>
|
||||
credential_id?: string
|
||||
activation_condition?: AgentToolActivationCondition
|
||||
}
|
||||
|
||||
export type DataSourceItem = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
|
||||
type Props = {
|
||||
variables: NodeOutPutVar[]
|
||||
onSelect: (valueSelector: ValueSelector, varItem: Var) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ConditionAdd = ({
|
||||
variables,
|
||||
onSelect,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleSelect = useCallback((valueSelector: ValueSelector, varItem: Var) => {
|
||||
onSelect(valueSelector, varItem)
|
||||
setOpen(false)
|
||||
}, [onSelect])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => !disabled && setOpen(!open)}>
|
||||
<Button
|
||||
size='small'
|
||||
disabled={disabled}
|
||||
>
|
||||
<RiAddLine className='mr-1 h-3.5 w-3.5' />
|
||||
{t('workflow.nodes.agent.toolCondition.addCondition')}
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
<VarReferenceVars
|
||||
vars={variables}
|
||||
isSupportFileVar
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionAdd
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ConditionInputProps = {
|
||||
disabled?: boolean
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
availableNodes: Node[]
|
||||
}
|
||||
const ConditionInput = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
availableNodes,
|
||||
}: ConditionInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
|
||||
return (
|
||||
<PromptEditor
|
||||
key={controlPromptEditorRerenderKey}
|
||||
compact
|
||||
value={value}
|
||||
placeholder={t('workflow.nodes.ifElse.enterValue') || ''}
|
||||
workflowVariableBlock={{
|
||||
show: true,
|
||||
variables: [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
showManageInputField: !!pipelineId,
|
||||
onManageInputField: () => setShowInputFieldPanel?.(true),
|
||||
}}
|
||||
onChange={onChange}
|
||||
editable={!disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionInput
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { produce } from 'immer'
|
||||
import ConditionVarSelector from './condition-var-selector'
|
||||
import ConditionOperator from './condition-operator'
|
||||
import {
|
||||
getConditionOperators,
|
||||
getDefaultValueByType,
|
||||
operatorNeedsValue,
|
||||
} from '../../utils'
|
||||
import type {
|
||||
AgentToolCondition,
|
||||
} from '../../types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
import ConditionInput from './condition-input'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
condition: AgentToolCondition
|
||||
availableVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
disabled?: boolean
|
||||
onChange: (condition: AgentToolCondition) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const ConditionItem = ({
|
||||
className,
|
||||
condition,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
disabled,
|
||||
onChange,
|
||||
onRemove,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const needsValue = operatorNeedsValue(condition.comparison_operator)
|
||||
const booleanOptions = useMemo(() => ([
|
||||
{ name: t('common.operation.yes'), value: true },
|
||||
{ name: t('common.operation.no'), value: false },
|
||||
]), [t])
|
||||
|
||||
const handleSelectVar = useCallback((valueSelector: ValueSelector, varItem: Var) => {
|
||||
const operators = getConditionOperators(varItem.type)
|
||||
const defaultOperator = operators[0]
|
||||
const nextCondition = produce(condition, (draft) => {
|
||||
draft.variable_selector = valueSelector
|
||||
draft.varType = varItem.type
|
||||
draft.comparison_operator = defaultOperator
|
||||
draft.value = operatorNeedsValue(defaultOperator) ? getDefaultValueByType(varItem.type) : undefined
|
||||
})
|
||||
onChange(nextCondition)
|
||||
}, [condition, onChange])
|
||||
|
||||
const handleOperatorChange = useCallback((operator: string) => {
|
||||
const nextCondition = produce(condition, (draft) => {
|
||||
draft.comparison_operator = operator
|
||||
if (operatorNeedsValue(operator))
|
||||
draft.value = draft.varType ? getDefaultValueByType(draft.varType) : ''
|
||||
else
|
||||
draft.value = undefined
|
||||
})
|
||||
onChange(nextCondition)
|
||||
}, [condition, onChange])
|
||||
|
||||
const handleValueChange = useCallback((value: string | boolean) => {
|
||||
const nextCondition = produce(condition, (draft) => {
|
||||
draft.value = value
|
||||
})
|
||||
onChange(nextCondition)
|
||||
}, [condition, onChange])
|
||||
|
||||
const handleTextValueChange = useCallback((value: string) => {
|
||||
handleValueChange(value)
|
||||
}, [handleValueChange])
|
||||
|
||||
const handleBooleanValueChange = useCallback((value: boolean) => {
|
||||
handleValueChange(value)
|
||||
}, [handleValueChange])
|
||||
|
||||
const renderValueInput = () => {
|
||||
if (!needsValue)
|
||||
return <div className='system-xs-regular text-text-tertiary'>{t('workflow.nodes.agent.toolCondition.noValueNeeded')}</div>
|
||||
|
||||
if (condition.varType === VarType.boolean) {
|
||||
return (
|
||||
<Select
|
||||
className='h-8'
|
||||
optionWrapClassName='w-32'
|
||||
value={condition.value as boolean | undefined}
|
||||
items={booleanOptions}
|
||||
onSelect={item => handleBooleanValueChange(Boolean(item.value))}
|
||||
disabled={disabled}
|
||||
hideChecked
|
||||
notClearable
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const normalizedValue = typeof condition.value === 'string' || typeof condition.value === 'number'
|
||||
? String(condition.value)
|
||||
: ''
|
||||
|
||||
if (condition.varType === VarType.number) {
|
||||
return (
|
||||
<Input
|
||||
value={normalizedValue}
|
||||
onChange={event => handleTextValueChange(event.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const textValue = typeof condition.value === 'string' ? condition.value : ''
|
||||
|
||||
return (
|
||||
<ConditionInput
|
||||
value={textValue}
|
||||
onChange={handleTextValueChange}
|
||||
disabled={disabled}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('mb-1 flex w-full last-of-type:mb-0', className)}>
|
||||
<div className='flex-1 rounded-lg bg-components-input-bg-normal'>
|
||||
<div className='flex items-center p-1'>
|
||||
<div className='w-0 grow'>
|
||||
<ConditionVarSelector
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
valueSelector={condition.variable_selector}
|
||||
varType={condition.varType}
|
||||
availableVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
onSelect={handleSelectVar}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-1 h-3 w-[1px] bg-divider-regular' />
|
||||
<ConditionOperator
|
||||
varType={condition.varType}
|
||||
value={condition.comparison_operator}
|
||||
onSelect={handleOperatorChange}
|
||||
disabled={disabled || !condition.variable_selector}
|
||||
/>
|
||||
</div>
|
||||
<div className='border-t border-divider-subtle px-3 py-2'>
|
||||
{renderValueInput()}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='ml-1 mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-lg text-text-tertiary transition-colors hover:bg-state-destructive-hover hover:text-text-destructive'
|
||||
onClick={onRemove}
|
||||
disabled={disabled}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionItem
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { RiLoopLeftLine } from '@remixicon/react'
|
||||
import { useMemo } from 'react'
|
||||
import ConditionItem from './condition-item'
|
||||
import type {
|
||||
AgentToolCondition,
|
||||
AgentToolConditionLogicalOperator,
|
||||
} from '../../types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
conditions: AgentToolCondition[]
|
||||
logicalOperator: AgentToolConditionLogicalOperator
|
||||
availableVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
disabled?: boolean
|
||||
onChange: (condition: AgentToolCondition) => void
|
||||
onRemove: (conditionId: string) => void
|
||||
onToggleLogicalOperator: () => void
|
||||
}
|
||||
|
||||
const ConditionList = ({
|
||||
conditions,
|
||||
logicalOperator,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
disabled,
|
||||
onChange,
|
||||
onRemove,
|
||||
onToggleLogicalOperator,
|
||||
}: Props) => {
|
||||
const hasMultiple = conditions.length > 1
|
||||
|
||||
const containerClassName = useMemo(() => cn('relative', hasMultiple && 'pl-[60px]'), [hasMultiple])
|
||||
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
{hasMultiple && (
|
||||
<div className='absolute bottom-0 left-0 top-0 w-[60px]'>
|
||||
<div className='absolute bottom-4 left-[46px] top-4 w-2.5 rounded-l-[8px] border border-r-0 border-divider-deep'></div>
|
||||
<div className='absolute right-0 top-1/2 h-[29px] w-4 -translate-y-1/2 bg-components-panel-bg'></div>
|
||||
<button
|
||||
type='button'
|
||||
className='absolute right-1 top-1/2 flex h-5 -translate-y-1/2 items-center gap-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-1.5 text-[10px] font-semibold uppercase text-text-accent-secondary shadow-xs disabled:cursor-not-allowed disabled:opacity-60'
|
||||
onClick={onToggleLogicalOperator}
|
||||
disabled={disabled}
|
||||
>
|
||||
{logicalOperator.toUpperCase()}
|
||||
<RiLoopLeftLine className='h-3 w-3' />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{conditions.map(condition => (
|
||||
<ConditionItem
|
||||
key={condition.id}
|
||||
className=''
|
||||
condition={condition}
|
||||
availableVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
onRemove={() => onRemove(condition.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionList
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import type { VarType } from '@/app/components/workflow/types'
|
||||
import { getConditionOperators } from '../../utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
varType?: VarType
|
||||
value?: string
|
||||
onSelect: (operator: string) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ConditionOperator = ({
|
||||
varType,
|
||||
value,
|
||||
onSelect,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const options = useMemo(() => {
|
||||
return getConditionOperators(varType).map((option) => {
|
||||
const key = `workflow.nodes.ifElse.comparisonOperator.${option}`
|
||||
const translated = t(key)
|
||||
return {
|
||||
value: option,
|
||||
label: translated === key ? option : translated,
|
||||
}
|
||||
})
|
||||
}, [t, varType])
|
||||
|
||||
const selectedOption = options.find(option => option.value === value)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => {
|
||||
if (!disabled)
|
||||
setOpen(v => !v)
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={cn('h-7 shrink-0 px-2', !selectedOption && 'opacity-50')}
|
||||
size='small'
|
||||
variant='ghost'
|
||||
disabled={disabled}
|
||||
>
|
||||
{selectedOption?.label ?? t('workflow.nodes.agent.toolCondition.operatorPlaceholder')}
|
||||
<RiArrowDownSLine className='ml-1 h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex h-7 cursor-pointer items-center rounded-lg px-3 py-1.5 text-[13px] font-medium text-text-secondary hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
onSelect(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionOperator
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
valueSelector?: ValueSelector
|
||||
varType?: VarType
|
||||
availableVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
onSelect: (valueSelector: ValueSelector, varItem: Var) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ConditionVarSelector = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
valueSelector,
|
||||
varType,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
onSelect,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleTriggerClick = useCallback(() => {
|
||||
if (disabled)
|
||||
return
|
||||
onOpenChange(!open)
|
||||
}, [disabled, onOpenChange, open])
|
||||
|
||||
const handleSelect = useCallback((selector: ValueSelector, varItem: Var) => {
|
||||
if (disabled)
|
||||
return
|
||||
onSelect(selector, varItem)
|
||||
onOpenChange(false)
|
||||
}, [disabled, onOpenChange, onSelect])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={(state) => {
|
||||
if (!disabled)
|
||||
onOpenChange(state)
|
||||
}}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTriggerClick}>
|
||||
<div className={cn('cursor-pointer', disabled && '!cursor-not-allowed opacity-60')}>
|
||||
{valueSelector && valueSelector.length > 0 ? (
|
||||
<VariableTag
|
||||
valueSelector={valueSelector}
|
||||
varType={varType ?? VarType.string}
|
||||
isShort
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
) : (
|
||||
<div className='inline-flex h-6 items-center rounded-md border border-dashed border-divider-subtle px-2 text-xs text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.toolCondition.selectVariable')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[296px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
|
||||
<VarReferenceVars
|
||||
vars={availableVars}
|
||||
isSupportFileVar
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionVarSelector)
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { produce } from 'immer'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import ConditionAdd from './condition-add'
|
||||
import ConditionList from './condition-list'
|
||||
import type {
|
||||
AgentToolActivationCondition,
|
||||
AgentToolCondition,
|
||||
} from '../../types'
|
||||
import { AgentToolConditionLogicalOperator } from '../../types'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
getConditionOperators,
|
||||
getDefaultValueByType,
|
||||
operatorNeedsValue,
|
||||
} from '../../utils'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value?: AgentToolActivationCondition
|
||||
onChange: (value: AgentToolActivationCondition | undefined) => void
|
||||
availableVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const AgentToolConditionEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const currentValue = useMemo<AgentToolActivationCondition>(() => value ?? ({
|
||||
enabled: false,
|
||||
logical_operator: AgentToolConditionLogicalOperator.And,
|
||||
conditions: [],
|
||||
}), [value])
|
||||
|
||||
const handleToggle = useCallback((state: boolean) => {
|
||||
const next = produce(currentValue, (draft) => {
|
||||
draft.enabled = state
|
||||
})
|
||||
onChange(next)
|
||||
}, [currentValue, onChange])
|
||||
|
||||
const handleAddCondition = useCallback((valueSelector: ValueSelector, varItem: Var) => {
|
||||
const operators = getConditionOperators(varItem.type)
|
||||
const defaultOperator = operators[0]
|
||||
const newCondition: AgentToolCondition = {
|
||||
id: uuid4(),
|
||||
varType: varItem.type ?? VarType.string,
|
||||
variable_selector: valueSelector,
|
||||
comparison_operator: defaultOperator,
|
||||
value: operatorNeedsValue(defaultOperator) ? getDefaultValueByType(varItem.type ?? VarType.string) : undefined,
|
||||
}
|
||||
const next = produce(currentValue, (draft) => {
|
||||
draft.enabled = true
|
||||
draft.conditions.push(newCondition)
|
||||
})
|
||||
onChange(next)
|
||||
}, [currentValue, onChange])
|
||||
|
||||
const handleConditionChange = useCallback((updated: AgentToolCondition) => {
|
||||
const next = produce(currentValue, (draft) => {
|
||||
const targetIndex = draft.conditions.findIndex(item => item.id === updated.id)
|
||||
if (targetIndex !== -1)
|
||||
draft.conditions[targetIndex] = updated
|
||||
})
|
||||
onChange(next)
|
||||
}, [currentValue, onChange])
|
||||
|
||||
const handleRemoveCondition = useCallback((conditionId: string) => {
|
||||
const next = produce(currentValue, (draft) => {
|
||||
draft.conditions = draft.conditions.filter(item => item.id !== conditionId)
|
||||
})
|
||||
onChange(next)
|
||||
}, [currentValue, onChange])
|
||||
|
||||
const handleToggleLogicalOperator = useCallback(() => {
|
||||
const next = produce(currentValue, (draft) => {
|
||||
draft.logical_operator = draft.logical_operator === AgentToolConditionLogicalOperator.And
|
||||
? AgentToolConditionLogicalOperator.Or
|
||||
: AgentToolConditionLogicalOperator.And
|
||||
})
|
||||
onChange(next)
|
||||
}, [currentValue, onChange])
|
||||
|
||||
const isEnabled = currentValue.enabled
|
||||
const hasConditions = currentValue.conditions.length > 0
|
||||
|
||||
return (
|
||||
<div className=''>
|
||||
<div className='flex items-start justify-between gap-3'>
|
||||
<div>
|
||||
<div className='system-sm-semibold text-text-primary'>{t('workflow.nodes.agent.toolCondition.title')}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{t('workflow.nodes.agent.toolCondition.description')}</div>
|
||||
</div>
|
||||
<Switch
|
||||
defaultValue={isEnabled}
|
||||
onChange={handleToggle}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isEnabled && (
|
||||
<div className='space-y-3'>
|
||||
<div className='rounded-[10px] bg-components-panel-bg px-3 py-2'>
|
||||
{hasConditions && (
|
||||
<div className='mb-2'>
|
||||
<ConditionList
|
||||
conditions={currentValue.conditions}
|
||||
logicalOperator={currentValue.logical_operator}
|
||||
availableVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
disabled={disabled}
|
||||
onChange={handleConditionChange}
|
||||
onRemove={handleRemoveCondition}
|
||||
onToggleLogicalOperator={handleToggleLogicalOperator}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(
|
||||
'flex items-center justify-between pr-[30px]',
|
||||
hasConditions && currentValue.conditions.length > 1 && 'ml-[60px]',
|
||||
!hasConditions && 'mt-1',
|
||||
)}>
|
||||
<ConditionAdd
|
||||
variables={availableVars}
|
||||
onSelect={handleAddCondition}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!hasConditions && (
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.toolCondition.addFirstCondition')}
|
||||
</div>
|
||||
)}
|
||||
{hasConditions && currentValue.conditions.length <= 1 && (
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('workflow.nodes.agent.toolCondition.singleConditionTip')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentToolConditionEditor
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as AgentToolConditionEditor } from './editor'
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { CommonNodeType, Memory } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType, Memory, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
import type { ToolVarInputs } from '../tool/types'
|
||||
import type { PluginMeta } from '@/app/components/plugins/types'
|
||||
|
||||
|
|
@ -18,3 +18,22 @@ export type AgentNodeType = CommonNodeType & {
|
|||
export enum AgentFeature {
|
||||
HISTORY_MESSAGES = 'history-messages',
|
||||
}
|
||||
|
||||
export enum AgentToolConditionLogicalOperator {
|
||||
And = 'and',
|
||||
Or = 'or',
|
||||
}
|
||||
|
||||
export type AgentToolCondition = {
|
||||
id: string
|
||||
varType: VarType
|
||||
variable_selector?: ValueSelector
|
||||
comparison_operator?: string
|
||||
value?: string | string[] | boolean
|
||||
}
|
||||
|
||||
export type AgentToolActivationCondition = {
|
||||
enabled: boolean
|
||||
logical_operator: AgentToolConditionLogicalOperator
|
||||
conditions: AgentToolCondition[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
const COMPARISON_OPERATOR_WITHOUT_VALUE = new Set([
|
||||
'empty',
|
||||
'not empty',
|
||||
'null',
|
||||
'not null',
|
||||
'exists',
|
||||
'not exists',
|
||||
])
|
||||
|
||||
export const getConditionOperators = (varType?: VarType): string[] => {
|
||||
switch (varType) {
|
||||
case VarType.number:
|
||||
return ['=', '≠', '>', '<', '≥', '≤']
|
||||
case VarType.boolean:
|
||||
return ['is', 'is not']
|
||||
case VarType.arrayString:
|
||||
case VarType.arrayNumber:
|
||||
case VarType.arrayBoolean:
|
||||
case VarType.array:
|
||||
case VarType.arrayAny:
|
||||
return ['contains', 'not contains', 'empty', 'not empty']
|
||||
case VarType.arrayFile:
|
||||
return ['contains', 'not contains', 'empty', 'not empty']
|
||||
case VarType.file:
|
||||
return ['exists', 'not exists']
|
||||
case VarType.object:
|
||||
return ['empty', 'not empty']
|
||||
case VarType.any:
|
||||
return ['is', 'is not', 'empty', 'not empty']
|
||||
default:
|
||||
return ['contains', 'not contains', 'start with', 'end with', 'is', 'is not', 'empty', 'not empty']
|
||||
}
|
||||
}
|
||||
|
||||
export const operatorNeedsValue = (operator?: string): boolean => {
|
||||
if (!operator)
|
||||
return false
|
||||
|
||||
return !COMPARISON_OPERATOR_WITHOUT_VALUE.has(operator)
|
||||
}
|
||||
|
||||
export const getDefaultValueByType = (varType: VarType): string | boolean => {
|
||||
if (varType === VarType.boolean)
|
||||
return true
|
||||
|
||||
return ''
|
||||
}
|
||||
|
|
@ -900,6 +900,17 @@ const translation = {
|
|||
notAuthorized: 'Not Authorized',
|
||||
model: 'model',
|
||||
toolbox: 'toolbox',
|
||||
toolCondition: {
|
||||
title: 'Activation Condition',
|
||||
description: 'When enabled, load this tool only when the conditions are met.',
|
||||
selectVariable: 'Select variable...',
|
||||
operatorPlaceholder: 'Select operator',
|
||||
noValueNeeded: 'No value is required for this operator.',
|
||||
addCondition: 'Add Condition',
|
||||
logicalOperator: 'Logical operator: {{value}}',
|
||||
singleConditionTip: 'Add at least one condition to start.',
|
||||
addFirstCondition: 'No conditions yet. Add your first condition.',
|
||||
},
|
||||
strategyNotSet: 'Agentic strategy Not Set',
|
||||
tools: 'Tools',
|
||||
maxIterations: 'Max Iterations',
|
||||
|
|
|
|||
|
|
@ -898,6 +898,17 @@ const translation = {
|
|||
},
|
||||
model: '模型',
|
||||
toolbox: '工具箱',
|
||||
toolCondition: {
|
||||
title: '启用条件',
|
||||
description: '开启后,根据条件加载该工具。',
|
||||
selectVariable: '选择变量...',
|
||||
operatorPlaceholder: '选择运算符',
|
||||
noValueNeeded: '该运算符无需填写值。',
|
||||
addCondition: '新增条件',
|
||||
logicalOperator: '当前逻辑:{{value}}',
|
||||
singleConditionTip: '请添加至少一个条件。',
|
||||
addFirstCondition: '暂未添加条件,请先新增一个条件。',
|
||||
},
|
||||
strategyNotSet: '代理策略未设置',
|
||||
configureModel: '配置模型',
|
||||
notAuthorized: '未授权',
|
||||
|
|
|
|||
Loading…
Reference in New Issue