From 48341a922e066c84c2206c448e8c870fe9da0710 Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 6 May 2026 04:07:47 +0530 Subject: [PATCH] feat: warn when agent app uses a subset of provider's available tool operations --- .../agent-tools/__tests__/index.spec.tsx | 86 +++++++++++++++++++ .../config/agent/agent-tools/index.tsx | 60 +++++++++++++ web/i18n/en-US/tools.json | 1 + 3 files changed, 147 insertions(+) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx index d668737a0a..9aaf5d1aa6 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx @@ -465,4 +465,90 @@ describe('AgentTools', () => { expect((getModelConfig().agentConfig.tools[0] as { isDeleted: boolean }).isDeleted).toBe(false) }) }) + + it('should show out-of-sync warning when attached operations are a subset of available', () => { + // Provider has 2 tools available (search + translate) + builtInTools = [ + createCollection({ + id: 'multi-op-provider', + name: 'vendor/multi-op-provider', + tools: [ + createToolDefinition({ name: 'op_a' }), + createToolDefinition({ name: 'op_b' }), + ], + }), + ] + + // But the app only attaches op_a + renderAgentTools([ + createAgentTool({ + provider_id: 'multi-op-provider', + provider_name: 'vendor/multi-op-provider', + tool_name: 'op_a', + tool_label: 'Operation A', + }), + ]) + + const warningIcon = screen.getByTestId('tool-ops-out-of-sync') + expect(warningIcon).toBeInTheDocument() + }) + + it('should not show out-of-sync warning when all operations are attached', () => { + // Provider has 2 tools available + builtInTools = [ + createCollection({ + id: 'full-provider', + name: 'vendor/full-provider', + tools: [ + createToolDefinition({ name: 'op_a' }), + createToolDefinition({ name: 'op_b' }), + ], + }), + ] + + // App attaches both operations + renderAgentTools([ + createAgentTool({ + provider_id: 'full-provider', + provider_name: 'vendor/full-provider', + tool_name: 'op_a', + tool_label: 'Operation A', + }), + createAgentTool({ + provider_id: 'full-provider', + provider_name: 'vendor/full-provider', + tool_name: 'op_b', + tool_label: 'Operation B', + }), + ]) + + const warningIcon = screen.queryByTestId('tool-ops-out-of-sync') + expect(warningIcon).not.toBeInTheDocument() + }) + + it('should not show out-of-sync warning when provider has only one operation', () => { + // Provider has exactly 1 tool + builtInTools = [ + createCollection({ + id: 'single-op-provider', + name: 'vendor/single-op-provider', + tools: [ + createToolDefinition({ name: 'only_op' }), + ], + }), + ] + + // App attaches that single operation + renderAgentTools([ + createAgentTool({ + provider_id: 'single-op-provider', + provider_name: 'vendor/single-op-provider', + tool_name: 'only_op', + tool_label: 'Only Operation', + }), + ]) + + const warningIcon = screen.queryByTestId('tool-ops-out-of-sync') + expect(warningIcon).not.toBeInTheDocument() + }) }) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index 3242bcdcf8..c2f0950062 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -12,6 +12,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too import { RiDeleteBinLine, RiEqualizer2Line, + RiErrorWarningLine, RiInformation2Line, } from '@remixicon/react' import copy from 'copy-to-clipboard' @@ -72,6 +73,35 @@ const AgentTools: FC = () => { collection, } }) + + // Detect providers where the app uses fewer operations than the provider exposes + const outOfSyncProviders = useMemo(() => { + const agentTools = modelConfig?.agentConfig?.tools as AgentTool[] || [] + const syncMap = new Map() + + // Group attached tools by provider_id + const attachedByProvider = new Map>() + for (const tool of agentTools) { + if (!attachedByProvider.has(tool.provider_id)) + attachedByProvider.set(tool.provider_id, new Set()) + attachedByProvider.get(tool.provider_id)!.add(tool.tool_name) + } + + // Compare against available operations in collectionList + for (const [providerId, attachedNames] of attachedByProvider) { + const collection = collectionList.find(c => + canFindTool(c.id, providerId), + ) + if (collection && collection.tools.length > attachedNames.size) { + syncMap.set(providerId, { + attached: attachedNames.size, + available: collection.tools.length, + }) + } + } + + return syncMap + }, [modelConfig?.agentConfig?.tools, collectionList]) const useSubscribe = useMittContextSelector(s => s.useSubscribe) const handleUpdateToolsWhenInstallToolSuccess = useCallback((installedPluginNames: string[]) => { const newModelConfig = produce(modelConfig, (draft) => { @@ -259,6 +289,36 @@ const AgentTools: FC = () => { )} + {!item.isDeleted && outOfSyncProviders.has(item.provider_id) && ( +
+ + + + + )} + /> + + {t('toolOperationsOutOfSync', { + ns: 'tools', + available: outOfSyncProviders.get(item.provider_id)!.available, + attached: outOfSyncProviders.get(item.provider_id)!.attached, + })} + + +
+ )}
{item.isDeleted && ( diff --git a/web/i18n/en-US/tools.json b/web/i18n/en-US/tools.json index 391e109317..3f59217cb6 100644 --- a/web/i18n/en-US/tools.json +++ b/web/i18n/en-US/tools.json @@ -206,6 +206,7 @@ "thought.using": "Using", "title": "Tools", "toolNameUsageTip": "Tool call name for agent reasoning and prompting", + "toolOperationsOutOfSync": "This tool has {{available}} operations available, but this app only uses {{attached}}. Add missing operations with the '+' button.", "toolRemoved": "Tool removed", "type.builtIn": "Tools", "type.custom": "Custom",