diff --git a/web/app/components/header/account-setting/model-provider-page/declarations.ts b/web/app/components/header/account-setting/model-provider-page/declarations.ts index 8f0adb1259..4500ebc1f7 100644 --- a/web/app/components/header/account-setting/model-provider-page/declarations.ts +++ b/web/app/components/header/account-setting/model-provider-page/declarations.ts @@ -17,6 +17,7 @@ export enum FormTypeEnum { file = 'file', modelSelector = 'model-selector', toolSelector = 'tool-selector', + multiToolSelector = 'array[tools]', appSelector = 'app-selector', } diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx index b7a9a69870..b357c2cb49 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx @@ -19,6 +19,7 @@ import Tooltip from '@/app/components/base/tooltip' import Radio from '@/app/components/base/radio' import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector' +import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import RadioE from '@/app/components/base/radio/ui' @@ -328,7 +329,35 @@ function Form< scope={scope} disabled={readonly} value={value[variable]} - onSelect={item => handleFormChange(variable, item as any)} /> + onSelect={item => handleFormChange(variable, item as any)} + onDelete={() => handleFormChange(variable, null as any)} + /> + {fieldMoreInfo?.(formSchema)} + {validating && changeKey === variable && } + + ) + } + + if (formSchema.type === FormTypeEnum.multiToolSelector) { + const { + variable, + label, + tooltip, + required, + scope, + } = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput) + + return ( +
+ handleFormChange(variable, item as any)} + /> {fieldMoreInfo?.(formSchema)} {validating && changeKey === variable && }
diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index d14c0d96be..57aa24bf84 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -7,7 +7,7 @@ import ActionList from './action-list' import ModelList from './model-list' import AgentStrategyList from './agent-strategy-list' import Drawer from '@/app/components/base/drawer' -import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector' +import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector' import type { PluginDetail } from '@/app/components/plugins/types' import cn from '@/utils/classnames' @@ -33,9 +33,6 @@ const PluginDetailPanel: FC = ({ console.log('tool change', val) setValue(val) } - const testDelete = () => { - setValue(undefined) - } if (!detail) return null @@ -64,10 +61,10 @@ const PluginDetailPanel: FC = ({ {!!detail.declaration.model && } {false && (
- testChange(item)} - onDelete={testDelete} +
)} diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx index bbf5a8a1ef..6adf26ee79 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx @@ -1,12 +1,148 @@ import React from 'react' +import { useTranslation } from 'react-i18next' +import { + RiAddLine, + RiArrowDropDownLine, + RiQuestionLine, +} from '@remixicon/react' +import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector' +import ActionButton from '@/app/components/base/action-button' +import Tooltip from '@/app/components/base/tooltip' +import Divider from '@/app/components/base/divider' +import type { ToolValue } from '@/app/components/plugins/plugin-detail-panel/tool-selector' +import cn from '@/utils/classnames' type Props = { - value: any[] + disabled?: boolean + value: ToolValue[] + label: string + required?: boolean + tooltip?: any + supportCollapse?: boolean + scope?: string + onChange: (value: ToolValue[]) => void } -const MultipleToolSelector = ({ value }: Props) => { +const MultipleToolSelector = ({ + disabled, + value, + label, + required, + tooltip, + supportCollapse, + scope, + onChange, +}: Props) => { + const { t } = useTranslation() + const enabledCount = value.filter(item => item.enabled).length + // collapse control + const [collapse, setCollapse] = React.useState(false) + const handleCollapse = () => { + if (supportCollapse) + setCollapse(!collapse) + } + + // add tool + const [open, setOpen] = React.useState(false) + const handleAdd = (val: ToolValue) => { + const newValue = [...value, val] + // deduplication + const deduplication = newValue.reduce((acc, cur) => { + if (!acc.find(item => item.provider_name === cur.provider_name && item.tool_name === cur.tool_name)) + acc.push(cur) + return acc + }, [] as ToolValue[]) + // update value + onChange(deduplication) + setOpen(false) + } + + // delete tool + const handleDelete = (index: number) => { + const newValue = [...value] + newValue.splice(index, 1) + onChange(newValue) + } + + // configure tool + const handleConfigure = (val: ToolValue, index: number) => { + const newValue = [...value] + newValue[index] = val + onChange(newValue) + } + return ( -
+ <> +
+
+
{label}
+ {required &&
*
} + {tooltip && ( + +
+
+ )} + {supportCollapse && ( +
+ +
+ )} +
+ {value.length > 0 && ( + <> +
+ {`${enabledCount}/${value.length}`} + {t('appDebug.agent.tools.enabled')} +
+ + + )} + {!disabled && ( + setOpen(!open)}> + + + )} +
+ {!collapse && ( + <> + + } + /> + {value.length === 0 && ( +
{t('plugin.detailPanel.toolSelector.empty')}
+ )} + {value.length > 0 && value.map((item, index) => ( +
+ handleConfigure(item, index)} + onDelete={() => handleDelete(index)} + supportEnableSwitch + /> +
+ ))} + + )} + ) } 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 3e7993f761..d21a0a1ded 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,16 +40,20 @@ import type { } from '@floating-ui/react' import cn from '@/utils/classnames' +export type ToolValue = { + provider_name: string + tool_name: string + parameters?: Record + enabled?: boolean + extra?: Record +} + type Props = { - value?: { - provider_name: string - tool_name: string - parameters?: Record - extra?: Record - } disabled?: boolean placement?: Placement offset?: OffsetOptions + scope?: string + value?: ToolValue onSelect: (tool: { provider_name: string tool_name: string @@ -57,8 +61,11 @@ type Props = { extra?: Record }) => void onDelete?: () => void + supportEnableSwitch?: boolean supportAddCustomTool?: boolean - scope?: string + trigger?: React.ReactNode + controlledState?: boolean + onControlledStateChange?: (state: boolean) => void } const ToolSelector: FC = ({ value, @@ -68,6 +75,10 @@ const ToolSelector: FC = ({ onSelect, onDelete, scope, + supportEnableSwitch, + trigger, + controlledState, + onControlledStateChange, }) => { const { t } = useTranslation() const [isShow, onShowChange] = useState(false) @@ -95,14 +106,13 @@ const ToolSelector: FC = ({ provider_name: tool.provider_id, tool_name: tool.tool_name, parameters: paramValues, + enabled: tool.is_team_authorization, extra: { description: '', }, } onSelect(toolValue) setIsShowChooseTool(false) - // if (tool.provider_type === CollectionType.builtIn && tool.is_team_authorization) - // onShowChange(false) } const handleDescriptionChange = (e: React.ChangeEvent) => { @@ -130,6 +140,13 @@ const ToolSelector: FC = ({ onSelect(toolValue as any) } + const handleEnabledChange = (state: boolean) => { + onSelect({ + ...value, + enabled: state, + } as any) + } + // authorization const { isCurrentWorkspaceManager } = useAppContext() const [isShowSettingAuth, setShowSettingAuth] = useState(false) @@ -152,14 +169,15 @@ const ToolSelector: FC = ({ - {!value?.provider_name && ( + {trigger} + {!trigger && !value?.provider_name && ( = ({ provider={currentProvider} /> )} - {value?.provider_name && ( + {!trigger && value?.provider_name && ( setShowSettingAuth(true)} - // uninstalled + // uninstalled TODO + // isError TODO errorTip={

{t('workflow.nodes.agent.pluginNotInstalled')}

{t('workflow.nodes.agent.pluginNotInstalledDesc')}

diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx index a20e0d6e6f..f2ff56d07b 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx @@ -77,7 +77,10 @@ const ToolItem = ({ )}
{ + e.stopPropagation() + onDelete?.() + }} onMouseOver={() => setIsDeleting(true)} onMouseLeave={() => setIsDeleting(false)} > @@ -85,11 +88,13 @@ const ToolItem = ({
{!isError && !uninstalled && !noAuth && showSwitch && ( - +
e.stopPropagation()}> + +
)} {!isError && !uninstalled && noAuth && ( - - + ) } diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index 7cd5bb6e8b..b35e3d89bc 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -36,8 +36,6 @@ type Props = { onShowRetryDetail?: (detail: NodeTracing[]) => void onShowAgentResultList?: (detail: AgentLogItemWithChildren[]) => void notShowIterationNav?: boolean - justShowIterationNavArrow?: boolean - justShowRetryNavArrow?: boolean } const NodePanel: FC = ({ @@ -50,7 +48,6 @@ const NodePanel: FC = ({ onShowRetryDetail, onShowAgentResultList, notShowIterationNav, - justShowIterationNavArrow, }) => { const [collapseState, doSetCollapseState] = useState(true) const setCollapseState = useCallback((state: boolean) => { @@ -138,7 +135,6 @@ const NodePanel: FC = ({ )} {isRetryNode && onShowRetryDetail && ( diff --git a/web/app/components/workflow/run/result-panel.tsx b/web/app/components/workflow/run/result-panel.tsx index e760f4daf8..ce86c73b9f 100644 --- a/web/app/components/workflow/run/result-panel.tsx +++ b/web/app/components/workflow/run/result-panel.tsx @@ -1,19 +1,20 @@ 'use client' import type { FC } from 'react' import { useTranslation } from 'react-i18next' -import { - RiArrowRightSLine, - RiRestartFill, -} from '@remixicon/react' import StatusPanel from './status' import MetaData from './meta' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' import type { NodeTracing } from '@/types/workflow' -import Button from '@/app/components/base/button' +import { BlockEnum } from '@/app/components/workflow/types' +import { hasRetryNode } from '@/app/components/workflow/utils' +import { IterationLogTrigger } from '@/app/components/workflow/run/iteration-log' +import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log' +import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log' type ResultPanelProps = { + nodeInfo?: NodeTracing inputs?: string process_data?: string outputs?: string @@ -28,11 +29,13 @@ type ResultPanelProps = { showSteps?: boolean exceptionCounts?: number execution_metadata?: any - retry_events?: NodeTracing[] - onShowRetryDetail?: (retries: NodeTracing[]) => void + handleShowIterationResultList?: (detail: NodeTracing[][], iterDurationMap: any) => void + onShowRetryDetail?: (detail: NodeTracing[]) => void + onShowAgentResultList?: () => void } const ResultPanel: FC = ({ + nodeInfo, inputs, process_data, outputs, @@ -46,10 +49,14 @@ const ResultPanel: FC = ({ showSteps, exceptionCounts, execution_metadata, - retry_events, + handleShowIterationResultList, onShowRetryDetail, + onShowAgentResultList, }) => { const { t } = useTranslation() + const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration + const isRetryNode = hasRetryNode(nodeInfo?.node_type) && nodeInfo?.retryDetail + const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent return (
@@ -62,23 +69,32 @@ const ResultPanel: FC = ({ exceptionCounts={exceptionCounts} />
- { - retry_events?.length && onShowRetryDetail && ( -
- -
- ) - } +
+ { + isIterationNode && handleShowIterationResultList && ( + + ) + } + { + isRetryNode && onShowRetryDetail && ( + + ) + } + { + isAgentNode && onShowAgentResultList && ( + + ) + } +
void - retryResultList: NodeTracing[] +export type SpecialResultPanelProps = { + showRetryDetail?: boolean + setShowRetryDetailFalse?: () => void + retryResultList?: NodeTracing[] - showIteratingDetail: boolean - setShowIteratingDetailFalse: () => void - iterationResultList: NodeTracing[][] - iterationResultDurationMap: IterationDurationMap + showIteratingDetail?: boolean + setShowIteratingDetailFalse?: () => void + iterationResultList?: NodeTracing[][] + iterationResultDurationMap?: IterationDurationMap - agentResultList: AgentLogItemWithChildren[] - setAgentResultList: (list: AgentLogItemWithChildren[]) => void + agentResultList?: AgentLogItemWithChildren[] + setAgentResultList?: (list: AgentLogItemWithChildren[]) => void } const SpecialResultPanel = ({ showRetryDetail, @@ -36,7 +36,7 @@ const SpecialResultPanel = ({ return ( <> { - showRetryDetail && ( + !!showRetryDetail && !!retryResultList?.length && setShowRetryDetailFalse && ( = ({ onShowIterationDetail={handleShowIterationResultList} onShowRetryDetail={handleShowRetryResultList} onShowAgentResultList={setAgentResultList} - justShowIterationNavArrow={true} - justShowRetryNavArrow={true} hideInfo={hideNodeInfo} hideProcessDetail={hideNodeProcessDetail} /> diff --git a/web/app/components/workflow/run/utils/format-log/agent/index.ts b/web/app/components/workflow/run/utils/format-log/agent/index.ts index 7bbe0aa57f..255dc16c5c 100644 --- a/web/app/components/workflow/run/utils/format-log/agent/index.ts +++ b/web/app/components/workflow/run/utils/format-log/agent/index.ts @@ -1,6 +1,8 @@ import { BlockEnum } from '@/app/components/workflow/types' import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow' +const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool] + const listToTree = (logs: AgentLogItem[]) => { if (!logs || logs.length === 0) return [] @@ -24,7 +26,7 @@ const listToTree = (logs: AgentLogItem[]) => { } const format = (list: NodeTracing[]): NodeTracing[] => { const result: NodeTracing[] = list.map((item) => { - if (item.node_type === BlockEnum.Agent && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0) + if (supportedAgentLogNodes.includes(item.node_type) && item.execution_metadata?.agent_log && item.execution_metadata?.agent_log.length > 0) item.agentLog = listToTree(item.execution_metadata.agent_log) return item diff --git a/web/app/components/workflow/run/utils/format-log/parallel/index.ts b/web/app/components/workflow/run/utils/format-log/parallel/index.ts index a341362e10..9501eec7ec 100644 --- a/web/app/components/workflow/run/utils/format-log/parallel/index.ts +++ b/web/app/components/workflow/run/utils/format-log/parallel/index.ts @@ -1,22 +1,25 @@ import { BlockEnum } from '@/app/components/workflow/types' import type { NodeTracing } from '@/types/workflow' -function printNodeStructure(node: NodeTracing, level: number) { - const indent = ' '.repeat(level) +function printNodeStructure(node: NodeTracing, depth: number) { + const indent = ' '.repeat(depth) console.log(`${indent}${node.title}`) if (node.parallelDetail?.children) { node.parallelDetail.children.forEach((child) => { - printNodeStructure(child, level + 1) + printNodeStructure(child, depth + 1) }) } } function addTitle({ - list, level, parallelNumRecord, + list, depth, belongParallelIndexInfo, }: { - list: NodeTracing[], level: number, parallelNumRecord: Record + list: NodeTracing[], + depth: number, + belongParallelIndexInfo?: string, }, t: any) { let branchIndex = 0 + const hasMoreThanOneParallel = list.filter(node => node.parallelDetail?.isParallelStartNode).length > 1 list.forEach((node) => { const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null const parallel_start_node_id = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null @@ -26,15 +29,20 @@ function addTitle({ return const isParallelStartNode = node.parallelDetail?.isParallelStartNode - if (isParallelStartNode) - parallelNumRecord.num++ - const letter = parallelNumRecord.num > 1 ? String.fromCharCode(64 + level) : '' - const parallelLevelInfo = `${parallelNumRecord.num}${letter}` + const parallelIndexLetter = (() => { + if (!isParallelStartNode || !hasMoreThanOneParallel) + return '' + + const index = 1 + list.filter(node => node.parallelDetail?.isParallelStartNode).findIndex(item => item.node_id === node.node_id) + return String.fromCharCode(64 + index) + })() + + const parallelIndexInfo = `${depth}${parallelIndexLetter}` if (isParallelStartNode) { node.parallelDetail!.isParallelStartNode = true - node.parallelDetail!.parallelTitle = `${t('workflow.common.parallel')}-${parallelLevelInfo}` + node.parallelDetail!.parallelTitle = `${t('workflow.common.parallel')}-${parallelIndexInfo}` } const isBrachStartNode = parallel_start_node_id === node.node_id @@ -47,14 +55,14 @@ function addTitle({ } } - node.parallelDetail!.branchTitle = `${t('workflow.common.branch')}-${parallelLevelInfo}-${branchLetter}` + node.parallelDetail!.branchTitle = `${t('workflow.common.branch')}-${belongParallelIndexInfo}-${branchLetter}` } if (node.parallelDetail?.children && node.parallelDetail.children.length > 0) { addTitle({ list: node.parallelDetail.children, - level: level + 1, - parallelNumRecord, + depth: depth + 1, + belongParallelIndexInfo: parallelIndexInfo, }, t) } }) @@ -70,7 +78,7 @@ const format = (list: NodeTracing[], t: any): NodeTracing[] => { const parallel_id = node.parallel_id ?? node.execution_metadata?.parallel_id ?? null const parent_parallel_id = node.parent_parallel_id ?? node.execution_metadata?.parent_parallel_id ?? null const branchStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null - const parent_parallel_start_node_id = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null + const parentParallelBranchStartNodeId = node.parent_parallel_start_node_id ?? node.execution_metadata?.parent_parallel_start_node_id ?? null const isNotInParallel = !parallel_id || node.node_type === BlockEnum.End if (isNotInParallel) return @@ -87,16 +95,24 @@ const format = (list: NodeTracing[], t: any): NodeTracing[] => { if (isRootLevel) return - const parentParallelStartNode = result.find(item => item.node_id === parent_parallel_start_node_id) - // append to parent parallel start node + const parentParallelStartNode = result.find(item => item.node_id === parentParallelBranchStartNodeId) + // append to parent parallel start node and after the same branch if (parentParallelStartNode) { if (!parentParallelStartNode?.parallelDetail) { parentParallelStartNode!.parallelDetail = { children: [], } } - if (parentParallelStartNode!.parallelDetail.children) - parentParallelStartNode!.parallelDetail.children.push(node) + if (parentParallelStartNode!.parallelDetail.children) { + const sameBranchNodesLastIndex = parentParallelStartNode.parallelDetail.children.findLastIndex((node) => { + const currStartNodeId = node.parallel_start_node_id ?? node.execution_metadata?.parallel_start_node_id ?? null + return currStartNodeId === parentParallelBranchStartNodeId + }) + if (sameBranchNodesLastIndex !== -1) + parentParallelStartNode!.parallelDetail.children.splice(sameBranchNodesLastIndex + 1, 0, node) + else + parentParallelStartNode!.parallelDetail.children.push(node) + } } return } @@ -144,14 +160,9 @@ const format = (list: NodeTracing[], t: any): NodeTracing[] => { // console.log(`----- p: ${now} end -----`) // }) - const parallelNumRecord: Record = { - num: 0, - } - addTitle({ list: filteredInParallelSubNodes, - level: 1, - parallelNumRecord, + depth: 1, }, t) return filteredInParallelSubNodes diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 38ec6d9be7..a706f8f042 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -73,6 +73,7 @@ const translation = { placeholder: 'Select a tool...', auth: 'AUTHORIZATION', settings: 'TOOL SETTINGS', + empty: 'Click the \'+\' button to add tools. You can add multiple tools.', }, configureApp: 'Configure App', configureModel: 'Configure model', diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index 8185f37b40..4b7c764d9f 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -73,6 +73,7 @@ const translation = { placeholder: '选择工具', auth: '授权', settings: '工具设置', + empty: '点击 "+" 按钮添加工具。您可以添加多个工具。', }, configureApp: '应用设置', configureModel: '模型设置',