From f263492c0353861a8c814d2e4d0db448a865fb9b Mon Sep 17 00:00:00 2001 From: GuanMu Date: Wed, 5 Nov 2025 08:37:14 +0000 Subject: [PATCH] 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. --- api/core/workflow/nodes/agent/agent_node.py | 32 ++- web/app/components/base/select/index.tsx | 171 ++++++++-------- .../tool-selector/index.tsx | 27 ++- .../workflow/block-selector/types.ts | 2 + .../tool-condition/condition-add.tsx | 68 +++++++ .../tool-condition/condition-input.tsx | 57 ++++++ .../tool-condition/condition-item.tsx | 182 ++++++++++++++++++ .../tool-condition/condition-list.tsx | 72 +++++++ .../tool-condition/condition-operator.tsx | 92 +++++++++ .../tool-condition/condition-var-selector.tsx | 97 ++++++++++ .../components/tool-condition/editor.tsx | 158 +++++++++++++++ .../agent/components/tool-condition/index.ts | 1 + .../components/workflow/nodes/agent/types.ts | 21 +- .../components/workflow/nodes/agent/utils.ts | 49 +++++ web/i18n/en-US/workflow.ts | 11 ++ web/i18n/zh-Hans/workflow.ts | 11 ++ 16 files changed, 970 insertions(+), 81 deletions(-) create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/condition-add.tsx create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/condition-input.tsx create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/condition-item.tsx create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/condition-list.tsx create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/condition-operator.tsx create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/condition-var-selector.tsx create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/editor.tsx create mode 100644 web/app/components/workflow/nodes/agent/components/tool-condition/index.ts create mode 100644 web/app/components/workflow/nodes/agent/utils.ts diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index 626ef1df7b..c20bb2cdac 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -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: diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index a1e8ac2724..61b9b5743e 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -202,10 +202,10 @@ const SimpleSelect: FC = ({ setSelectedItem(defaultSelect) }, [defaultValue]) - const listboxRef = useRef(null) + const openRef = useRef(false) return ( - { if (!disabled) { @@ -214,83 +214,100 @@ const SimpleSelect: FC = ({ } }} > - {({ open }) => ( -
- {renderTrigger && {renderTrigger(selectedItem)}} - {!renderTrigger && ( - { - 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)}> - {selectedItem?.name ?? localPlaceholder} - - {isLoading ? - : (selectedItem && !notClearable) - ? ( - { - e.stopPropagation() - setSelectedItem(null) - onSelect({ name: '', value: '' }) - }} - className="h-4 w-4 cursor-pointer text-text-quaternary" - aria-hidden="false" - /> - ) - : ( - open ? ( - - - )} + {({ open }) => { + if (openRef.current !== open) { + openRef.current = open + onOpenChange?.(open) + } - {(!disabled) && ( - - {items.map((item: Item) => ( - - {({ /* active, */ selected }) => ( - <> - {renderOption - ? renderOption({ item, selected }) - : (<> - {item.name} - {selected && !hideChecked && ( - - - )} - )} - + return ( + +
+ + {renderTrigger + ? {renderTrigger(selectedItem)} + : ( + + {selectedItem?.name ?? localPlaceholder} + + {isLoading ? + : (selectedItem && !notClearable) + ? ( + { + e.stopPropagation() + setSelectedItem(null) + onSelect({ name: '', value: '' }) + }} + className="h-4 w-4 cursor-pointer text-text-quaternary" + aria-hidden="false" + /> + ) + : ( + open ? ( + + )} - - ))} - - )} -
- )} + + + {(!disabled) && ( + + + {items.map((item: Item) => ( + + {({ selected }) => ( + <> + {renderOption + ? renderOption({ item, selected }) + : (<> + {item.name} + {selected && !hideChecked && ( + + + )} + )} + + )} + + ))} + + + )} +
+ + ) + }}
) } diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index d56d48d6d5..69b5fb0c5e 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -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 = ({ description: tool.tool_description, }, schemas: tool.paramSchemas, + activation_condition: value?.activation_condition, } } const handleSelectTool = (tool: ToolDefaultValue) => { @@ -150,6 +152,15 @@ const ToolSelector: FC = ({ } 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 = ({ )} -
+
<>
{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}
{/* base form */} @@ -391,6 +402,20 @@ const ToolSelector: FC = ({ )} )} + {value?.provider_name && nodeId && ( + <> + +
+ +
+ + )}
diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 48fbf6a500..aef5e2f7ad 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -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 credential_id?: string + activation_condition?: AgentToolActivationCondition } export type DataSourceItem = { diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/condition-add.tsx b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-add.tsx new file mode 100644 index 0000000000..cbc851cecb --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-add.tsx @@ -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 ( + + !disabled && setOpen(!open)}> + + + +
+ +
+
+
+ ) +} + +export default ConditionAdd diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/condition-input.tsx b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-input.tsx new file mode 100644 index 0000000000..683e2884cf --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-input.tsx @@ -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 ( + { + 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 diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/condition-item.tsx b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-item.tsx new file mode 100644 index 0000000000..656d4ac1e9 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-item.tsx @@ -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
{t('workflow.nodes.agent.toolCondition.noValueNeeded')}
+ + if (condition.varType === VarType.boolean) { + return ( + handleTextValueChange(event.target.value)} + disabled={disabled} + /> + ) + } + + const textValue = typeof condition.value === 'string' ? condition.value : '' + + return ( + + ) + } + + return ( +
+
+
+
+ +
+
+ +
+
+ {renderValueInput()} +
+
+ +
+ ) +} + +export default ConditionItem diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/condition-list.tsx b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-list.tsx new file mode 100644 index 0000000000..6d9a11d509 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-list.tsx @@ -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 ( +
+ {hasMultiple && ( +
+
+
+ +
+ )} + {conditions.map(condition => ( + onRemove(condition.id)} + /> + ))} +
+ ) +} + +export default ConditionList diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/condition-operator.tsx b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-operator.tsx new file mode 100644 index 0000000000..f99b1bb56c --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-operator.tsx @@ -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 ( + + { + if (!disabled) + setOpen(v => !v) + }} + > + + + +
+ {options.map(option => ( +
{ + onSelect(option.value) + setOpen(false) + }} + > + {option.label} +
+ ))} +
+
+
+ ) +} + +export default ConditionOperator diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/condition-var-selector.tsx b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-var-selector.tsx new file mode 100644 index 0000000000..3b115e9dbd --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/condition-var-selector.tsx @@ -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 ( + { + if (!disabled) + onOpenChange(state) + }} + placement='bottom-start' + offset={{ + mainAxis: 4, + crossAxis: 0, + }} + > + +
+ {valueSelector && valueSelector.length > 0 ? ( + + ) : ( +
+ {t('workflow.nodes.agent.toolCondition.selectVariable')} +
+ )} +
+
+ +
+ +
+
+
+ ) +} + +export default memo(ConditionVarSelector) diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/editor.tsx b/web/app/components/workflow/nodes/agent/components/tool-condition/editor.tsx new file mode 100644 index 0000000000..934b30be4b --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/editor.tsx @@ -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(() => 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 ( +
+
+
+
{t('workflow.nodes.agent.toolCondition.title')}
+
{t('workflow.nodes.agent.toolCondition.description')}
+
+ +
+ + {isEnabled && ( +
+
+ {hasConditions && ( +
+ +
+ )} +
1 && 'ml-[60px]', + !hasConditions && 'mt-1', + )}> + +
+
+ {!hasConditions && ( +
+ {t('workflow.nodes.agent.toolCondition.addFirstCondition')} +
+ )} + {hasConditions && currentValue.conditions.length <= 1 && ( +
+ {t('workflow.nodes.agent.toolCondition.singleConditionTip')} +
+ )} +
+ )} +
+ ) +} + +export default AgentToolConditionEditor diff --git a/web/app/components/workflow/nodes/agent/components/tool-condition/index.ts b/web/app/components/workflow/nodes/agent/components/tool-condition/index.ts new file mode 100644 index 0000000000..290befe038 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/components/tool-condition/index.ts @@ -0,0 +1 @@ +export { default as AgentToolConditionEditor } from './editor' diff --git a/web/app/components/workflow/nodes/agent/types.ts b/web/app/components/workflow/nodes/agent/types.ts index f163b3572a..5deb025ff0 100644 --- a/web/app/components/workflow/nodes/agent/types.ts +++ b/web/app/components/workflow/nodes/agent/types.ts @@ -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[] +} 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..53e656e54c --- /dev/null +++ b/web/app/components/workflow/nodes/agent/utils.ts @@ -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 '' +} diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index c59f4e9d6b..715b2a97b1 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -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', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 2d869083b7..056b3405a2 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -898,6 +898,17 @@ const translation = { }, model: '模型', toolbox: '工具箱', + toolCondition: { + title: '启用条件', + description: '开启后,根据条件加载该工具。', + selectVariable: '选择变量...', + operatorPlaceholder: '选择运算符', + noValueNeeded: '该运算符无需填写值。', + addCondition: '新增条件', + logicalOperator: '当前逻辑:{{value}}', + singleConditionTip: '请添加至少一个条件。', + addFirstCondition: '暂未添加条件,请先新增一个条件。', + }, strategyNotSet: '代理策略未设置', configureModel: '配置模型', notAuthorized: '未授权',