This commit is contained in:
Ayush Shekhar 2026-05-09 06:30:27 +05:30 committed by GitHub
commit 76fa519091
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 147 additions and 0 deletions

View File

@ -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()
})
})

View File

@ -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<string, { attached: number, available: number }>()
// Group attached tools by provider_id
const attachedByProvider = new Map<string, Set<string>>()
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 = () => {
</Popover>
)}
</div>
{!item.isDeleted && outOfSyncProviders.has(item.provider_id) && (
<div className="ml-1 flex shrink-0 items-center">
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('toolOperationsOutOfSync', {
ns: 'tools',
available: outOfSyncProviders.get(item.provider_id)!.available,
attached: outOfSyncProviders.get(item.provider_id)!.attached,
})}
render={(
<button
type="button"
className="flex h-4 w-4 items-center justify-center rounded-sm outline-hidden hover:bg-black/5 focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
data-testid="tool-ops-out-of-sync"
>
<RiErrorWarningLine className="h-4 w-4 text-[#F79009]" />
</button>
)}
/>
<PopoverContent popupClassName="w-[240px] px-3 py-2 system-xs-regular text-text-tertiary">
{t('toolOperationsOutOfSync', {
ns: 'tools',
available: outOfSyncProviders.get(item.provider_id)!.available,
attached: outOfSyncProviders.get(item.provider_id)!.attached,
})}
</PopoverContent>
</Popover>
</div>
)}
</div>
<div className="ml-1 flex shrink-0 items-center">
{item.isDeleted && (

View File

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