mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
fix: report unavailable agent tools in publish and workflow checks (#40077)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { AgentSoulDifyToolConfig } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { CommonNodeType, Node } from '../../types'
|
||||
import type { ChecklistItem } from '../use-checklist'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { zWorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps/zod.gen'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
@@ -18,6 +20,17 @@ import { useChecklist, useWorkflowRunValidation } from '../use-checklist'
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const toolServiceState = vi.hoisted(() => ({
|
||||
buildInTools: [] as ToolWithProvider[] | undefined,
|
||||
customTools: [] as ToolWithProvider[] | undefined,
|
||||
mcpTools: [] as ToolWithProvider[] | undefined,
|
||||
workflowTools: [] as ToolWithProvider[] | undefined,
|
||||
}))
|
||||
|
||||
const marketplacePluginState = vi.hoisted(() => ({
|
||||
label: undefined as Record<string, string> | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', async () => {
|
||||
const base = (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock()
|
||||
return {
|
||||
@@ -31,9 +44,32 @@ vi.mock('reactflow', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-tools', async () =>
|
||||
(await import('../../__tests__/service-mock-factory')).createToolServiceMock(),
|
||||
)
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => ({ data: toolServiceState.buildInTools }),
|
||||
useAllCustomTools: () => ({ data: toolServiceState.customTools }),
|
||||
useAllMCPTools: () => ({ data: toolServiceState.mcpTools }),
|
||||
useAllWorkflowTools: () => ({ data: toolServiceState.workflowTools }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useFetchPluginsInMarketPlaceByInfo: (infos: Array<{ organization: string; plugin: string }>) => ({
|
||||
data:
|
||||
infos.length > 0 && marketplacePluginState.label
|
||||
? {
|
||||
data: {
|
||||
list: infos.map(({ organization, plugin }) => ({
|
||||
plugin: {
|
||||
label: marketplacePluginState.label,
|
||||
labels: marketplacePluginState.label,
|
||||
name: plugin,
|
||||
plugin_id: `${organization}/${plugin}`,
|
||||
},
|
||||
})),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', async () =>
|
||||
(await import('../../__tests__/service-mock-factory')).createTriggerServiceMock(),
|
||||
@@ -163,6 +199,11 @@ beforeEach(() => {
|
||||
Object.keys(mockAvailableVarMap).forEach((k) => delete mockAvailableVarMap[k])
|
||||
mockModelProviders = []
|
||||
mockUsedVars = []
|
||||
toolServiceState.buildInTools = []
|
||||
toolServiceState.customTools = []
|
||||
toolServiceState.mcpTools = []
|
||||
toolServiceState.workflowTools = []
|
||||
marketplacePluginState.label = undefined
|
||||
setupNodesMap()
|
||||
})
|
||||
|
||||
@@ -183,11 +224,13 @@ function buildConnectedGraph() {
|
||||
}
|
||||
|
||||
function buildInlineAgentGraph({
|
||||
hasMissingFile,
|
||||
hasMissingSkill,
|
||||
difyTools = [],
|
||||
hasMissingFile = false,
|
||||
hasMissingSkill = false,
|
||||
}: {
|
||||
hasMissingFile: boolean
|
||||
hasMissingSkill: boolean
|
||||
difyTools?: AgentSoulDifyToolConfig[]
|
||||
hasMissingFile?: boolean
|
||||
hasMissingSkill?: boolean
|
||||
}) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -217,6 +260,9 @@ function buildInlineAgentGraph({
|
||||
{ name: 'Available Skill' },
|
||||
...(hasMissingSkill ? [{ is_missing: true, name: 'Missing Skill' }] : []),
|
||||
],
|
||||
tools: {
|
||||
dify_tools: difyTools,
|
||||
},
|
||||
},
|
||||
node_job: {},
|
||||
save_options: [],
|
||||
@@ -258,6 +304,45 @@ function buildInlineAgentGraph({
|
||||
}
|
||||
}
|
||||
|
||||
const credentialRequiredProvider = {
|
||||
id: 'google',
|
||||
name: 'google',
|
||||
author: 'Google',
|
||||
description: {
|
||||
en_US: 'Google tools.',
|
||||
zh_Hans: 'Google 工具。',
|
||||
},
|
||||
icon: 'https://example.com/google.svg',
|
||||
icon_dark: 'https://example.com/google-dark.svg',
|
||||
label: {
|
||||
en_US: 'Google Tools',
|
||||
zh_Hans: 'Google 工具',
|
||||
},
|
||||
type: CollectionType.builtIn,
|
||||
team_credentials: {
|
||||
api_key: {
|
||||
label: {
|
||||
en_US: 'API Key',
|
||||
zh_Hans: 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
en_US: 'Enter API key',
|
||||
zh_Hans: '输入 API Key',
|
||||
},
|
||||
required: true,
|
||||
type: 'secret-input',
|
||||
variable: 'api_key',
|
||||
},
|
||||
},
|
||||
is_team_authorization: false,
|
||||
allow_delete: false,
|
||||
labels: [],
|
||||
meta: {
|
||||
version: '0.0.1',
|
||||
},
|
||||
tools: [],
|
||||
} satisfies ToolWithProvider
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// useChecklist
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -343,6 +428,61 @@ describe('useChecklist', () => {
|
||||
expect(result.current).toEqual([])
|
||||
})
|
||||
|
||||
it('should report uninstalled tools from inline agents and open their configuration panel', async () => {
|
||||
marketplacePluginState.label = {
|
||||
en_US: 'Jina',
|
||||
}
|
||||
const { edges, nodeId, nodes, options } = buildInlineAgentGraph({
|
||||
difyTools: [
|
||||
{
|
||||
credential_type: 'unauthorized',
|
||||
plugin_id: 'langgenius/jina_tool',
|
||||
provider: 'langgenius/jina_tool/jina',
|
||||
provider_id: 'langgenius/jina_tool/jina',
|
||||
provider_type: 'plugin',
|
||||
tool_name: 'search',
|
||||
},
|
||||
],
|
||||
})
|
||||
const { result } = renderWorkflowHook(() => useChecklist(nodes, edges), options)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual([
|
||||
expect.objectContaining({
|
||||
id: nodeId,
|
||||
errorMessages: ['workflow.nodes.agent.toolNotInstallTooltip:{"tool":"Jina"}'],
|
||||
openInlineAgentPanel: true,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('should report unauthorized tools from inline agents and open their configuration panel', async () => {
|
||||
toolServiceState.buildInTools = [credentialRequiredProvider]
|
||||
const { edges, nodeId, nodes, options } = buildInlineAgentGraph({
|
||||
difyTools: [
|
||||
{
|
||||
credential_type: 'unauthorized',
|
||||
provider: 'google',
|
||||
provider_id: 'google',
|
||||
provider_type: 'builtin',
|
||||
tool_name: 'search',
|
||||
},
|
||||
],
|
||||
})
|
||||
const { result } = renderWorkflowHook(() => useChecklist(nodes, edges), options)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual([
|
||||
expect.objectContaining({
|
||||
id: nodeId,
|
||||
errorMessages: ['workflow.nodes.agent.toolNotAuthorizedTooltip:{"tool":"Google Tools"}'],
|
||||
openInlineAgentPanel: true,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass flow type to node validators', () => {
|
||||
const checkValid = vi.fn(() => ({ errorMessage: '' }))
|
||||
mockNodesMap[BlockEnum.LLM] = {
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
} from '../types'
|
||||
import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { AgentToolPublishIssue } from '@/features/agent-v2/agent-detail/configure/tool-provider-catalog'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@@ -31,6 +32,12 @@ import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { MAX_TREE_DEPTH } from '@/config'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions'
|
||||
import {
|
||||
createAgentToolProviderCatalog,
|
||||
getAgentToolPublishIssues,
|
||||
useAgentToolPresentation,
|
||||
} from '@/features/agent-v2/agent-detail/configure/tool-provider-catalog'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
@@ -148,6 +155,10 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const inlineAgentToolProviderCatalog = useMemo(
|
||||
() => createAgentToolProviderCatalog({ buildInTools, customTools, mcpTools, workflowTools }),
|
||||
[buildInTools, customTools, mcpTools, workflowTools],
|
||||
)
|
||||
const dataSourceList = useStore((s) => s.dataSourceList)
|
||||
const environmentVariables =
|
||||
useStore((s) => s.environmentVariables) ?? EMPTY_ENVIRONMENT_VARIABLES
|
||||
@@ -173,7 +184,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
),
|
||||
[nodes],
|
||||
)
|
||||
const inlineAgentMissingReferences = useQueries({
|
||||
const inlineAgentConfigurationIssues = useQueries({
|
||||
queries:
|
||||
!configsMap?.flowId ||
|
||||
(configsMap.flowType !== FlowType.appFlow && configsMap.flowType !== FlowType.snippet)
|
||||
@@ -202,9 +213,13 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
),
|
||||
),
|
||||
combine: (results) => {
|
||||
const missingReferences: Record<
|
||||
const issuesByNodeId: Record<
|
||||
string,
|
||||
{ hasMissingFiles: boolean; hasMissingSkills: boolean }
|
||||
{
|
||||
hasMissingFiles: boolean
|
||||
hasMissingSkills: boolean
|
||||
toolIssues: AgentToolPublishIssue[]
|
||||
}
|
||||
> = {}
|
||||
|
||||
results.forEach((result, index) => {
|
||||
@@ -214,17 +229,33 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
|
||||
const hasMissingFiles = agentSoul.config_files?.some((file) => file.is_missing === true)
|
||||
const hasMissingSkills = agentSoul.config_skills?.some((skill) => skill.is_missing === true)
|
||||
if (!hasMissingFiles && !hasMissingSkills) return
|
||||
const toolIssues = getAgentToolPublishIssues(
|
||||
agentSoulConfigToFormState(agentSoul).tools,
|
||||
inlineAgentToolProviderCatalog,
|
||||
)
|
||||
if (!hasMissingFiles && !hasMissingSkills && toolIssues.length === 0) return
|
||||
|
||||
missingReferences[nodeId] = {
|
||||
issuesByNodeId[nodeId] = {
|
||||
hasMissingFiles: !!hasMissingFiles,
|
||||
hasMissingSkills: !!hasMissingSkills,
|
||||
toolIssues,
|
||||
}
|
||||
})
|
||||
|
||||
return missingReferences
|
||||
return issuesByNodeId
|
||||
},
|
||||
})
|
||||
const inlineAgentIssueTools = useMemo(
|
||||
() =>
|
||||
Object.values(inlineAgentConfigurationIssues).flatMap((issues) =>
|
||||
issues.toolIssues.map((issue) => issue.tool),
|
||||
),
|
||||
[inlineAgentConfigurationIssues],
|
||||
)
|
||||
const inlineAgentToolPresentation = useAgentToolPresentation(
|
||||
inlineAgentIssueTools,
|
||||
inlineAgentToolProviderCatalog,
|
||||
)
|
||||
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||
const knowledgeBaseEmbeddingProviders = useMemo(() => {
|
||||
@@ -366,7 +397,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
})
|
||||
|
||||
const errorMessages: string[] = []
|
||||
const missingReferences = inlineAgentMissingReferences[node!.id]
|
||||
const inlineAgentIssues = inlineAgentConfigurationIssues[node!.id]
|
||||
|
||||
if (isPluginMissing) {
|
||||
errorMessages.push(t(($) => $['nodes.common.pluginNotInstalled'], { ns: 'workflow' }))
|
||||
@@ -398,14 +429,30 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
if (validationError) errorMessages.push(validationError)
|
||||
}
|
||||
|
||||
if (missingReferences?.hasMissingFiles)
|
||||
if (inlineAgentIssues?.hasMissingFiles)
|
||||
errorMessages.push(
|
||||
t(($) => $['agentDetail.configure.files.missing'], { ns: 'agentV2' }),
|
||||
)
|
||||
if (missingReferences?.hasMissingSkills)
|
||||
if (inlineAgentIssues?.hasMissingSkills)
|
||||
errorMessages.push(
|
||||
t(($) => $['agentDetail.configure.skills.missing'], { ns: 'agentV2' }),
|
||||
)
|
||||
for (const toolIssue of inlineAgentIssues?.toolIssues ?? []) {
|
||||
const toolName =
|
||||
inlineAgentToolPresentation.toolDisplayNameById.get(toolIssue.tool.id) ??
|
||||
toolIssue.tool.name
|
||||
errorMessages.push(
|
||||
toolIssue.type === 'uninstalled'
|
||||
? t(($) => $['nodes.agent.toolNotInstallTooltip'], {
|
||||
ns: 'workflow',
|
||||
tool: toolName,
|
||||
})
|
||||
: t(($) => $['nodes.agent.toolNotAuthorizedTooltip'], {
|
||||
ns: 'workflow',
|
||||
tool: toolName,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const availableVars = map[node!.id]!.availableVars
|
||||
let hasInvalidVar = false
|
||||
@@ -447,7 +494,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
pluginUniqueIdentifier: isPluginMissing
|
||||
? (node!.data as { plugin_unique_identifier?: string }).plugin_unique_identifier
|
||||
: undefined,
|
||||
...(missingReferences ? { openInlineAgentPanel: true } : {}),
|
||||
...(inlineAgentIssues ? { openInlineAgentPanel: true } : {}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -517,7 +564,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
t,
|
||||
map,
|
||||
modelProviders,
|
||||
inlineAgentMissingReferences,
|
||||
inlineAgentConfigurationIssues,
|
||||
inlineAgentToolPresentation.toolDisplayNameById,
|
||||
options?.flowType,
|
||||
])
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import type {
|
||||
} from '../form-state'
|
||||
import type { DraftFieldUpdate } from './utils'
|
||||
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import isEqual from 'fast-deep-equal'
|
||||
import { atom } from 'jotai'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { syncCliToolReferenceLabels } from '../reference-labels'
|
||||
import { agentComposerDraftAtom } from '../store'
|
||||
import { resolveDraftFieldUpdate } from './utils'
|
||||
@@ -37,6 +39,26 @@ export const agentComposerToolsAtom = atom(
|
||||
},
|
||||
)
|
||||
|
||||
export const agentComposerToolPresentationIdentitiesAtom = selectAtom(
|
||||
agentComposerToolsAtom,
|
||||
(tools) =>
|
||||
tools.flatMap((tool) => {
|
||||
if (tool.kind !== 'provider') return []
|
||||
|
||||
return [
|
||||
{
|
||||
kind: tool.kind,
|
||||
id: tool.id,
|
||||
name: tool.name,
|
||||
displayName: tool.displayName,
|
||||
pluginId: tool.pluginId,
|
||||
providerType: tool.providerType,
|
||||
},
|
||||
]
|
||||
}),
|
||||
isEqual,
|
||||
)
|
||||
|
||||
const toProviderToolAction = (tool: AgentProviderToolDefaultValue) => ({
|
||||
id: `${tool.provider_id}:${tool.tool_name}`,
|
||||
name: tool.tool_label || tool.title || tool.tool_name,
|
||||
|
||||
+164
@@ -1,7 +1,10 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { createStore, Provider as JotaiProvider } from 'jotai'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import {
|
||||
@@ -20,6 +23,17 @@ const toastMock = vi.hoisted(() => ({
|
||||
|
||||
const trackEventMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
const toolProviderState = vi.hoisted(() => ({
|
||||
builtInTools: [] as ToolWithProvider[] | undefined,
|
||||
customTools: [] as ToolWithProvider[] | undefined,
|
||||
mcpTools: [] as ToolWithProvider[] | undefined,
|
||||
workflowTools: [] as ToolWithProvider[] | undefined,
|
||||
}))
|
||||
|
||||
const marketplacePluginState = vi.hoisted(() => ({
|
||||
label: undefined as Record<string, string> | undefined,
|
||||
}))
|
||||
|
||||
const composerPutMutationFn = vi.hoisted(() =>
|
||||
vi.fn(
|
||||
async (variables: {
|
||||
@@ -139,6 +153,37 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: trackEventMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en_US',
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useFetchPluginsInMarketPlaceByInfo: (infos: Array<{ organization: string; plugin: string }>) => ({
|
||||
data:
|
||||
infos.length > 0 && marketplacePluginState.label
|
||||
? {
|
||||
data: {
|
||||
list: infos.map(({ organization, plugin }) => ({
|
||||
plugin: {
|
||||
label: marketplacePluginState.label,
|
||||
labels: marketplacePluginState.label,
|
||||
name: plugin,
|
||||
plugin_id: `${organization}/${plugin}`,
|
||||
},
|
||||
})),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => ({ data: toolProviderState.builtInTools }),
|
||||
useAllCustomTools: () => ({ data: toolProviderState.customTools }),
|
||||
useAllMCPTools: () => ({ data: toolProviderState.mcpTools }),
|
||||
useAllWorkflowTools: () => ({ data: toolProviderState.workflowTools }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
agent: {
|
||||
@@ -219,11 +264,55 @@ function renderUseAgentConfigureSync({
|
||||
}
|
||||
}
|
||||
|
||||
const credentialRequiredProvider = {
|
||||
id: 'google',
|
||||
name: 'google',
|
||||
author: 'Google',
|
||||
description: {
|
||||
en_US: 'Google tools.',
|
||||
zh_Hans: 'Google 工具。',
|
||||
},
|
||||
icon: 'https://example.com/google.svg',
|
||||
icon_dark: 'https://example.com/google-dark.svg',
|
||||
label: {
|
||||
en_US: 'Google Tools',
|
||||
zh_Hans: 'Google 工具',
|
||||
},
|
||||
type: CollectionType.builtIn,
|
||||
team_credentials: {
|
||||
api_key: {
|
||||
label: {
|
||||
en_US: 'API Key',
|
||||
zh_Hans: 'API Key',
|
||||
},
|
||||
placeholder: {
|
||||
en_US: 'Enter API key',
|
||||
zh_Hans: '输入 API Key',
|
||||
},
|
||||
required: true,
|
||||
type: 'secret-input',
|
||||
variable: 'api_key',
|
||||
},
|
||||
},
|
||||
is_team_authorization: false,
|
||||
allow_delete: false,
|
||||
labels: [],
|
||||
meta: {
|
||||
version: '0.0.1',
|
||||
},
|
||||
tools: [],
|
||||
} satisfies ToolWithProvider
|
||||
|
||||
describe('useAgentConfigureSync', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
composerPutRequestContexts.length = 0
|
||||
toolProviderState.builtInTools = []
|
||||
toolProviderState.customTools = []
|
||||
toolProviderState.mcpTools = []
|
||||
toolProviderState.workflowTools = []
|
||||
marketplacePluginState.label = undefined
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -880,6 +969,81 @@ describe('useAgentConfigureSync', () => {
|
||||
expect(toastMock.error).toHaveBeenCalledWith('common.modelProvider.selectModel')
|
||||
})
|
||||
|
||||
it('should toast and skip publish when a configured tool is not installed', async () => {
|
||||
marketplacePluginState.label = {
|
||||
en_US: 'Jina',
|
||||
}
|
||||
const { result, store } = renderUseAgentConfigureSync({
|
||||
currentModel: configuredModel,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
store.set(agentComposerDraftAtom, {
|
||||
...defaultAgentSoulConfigFormState,
|
||||
tools: [
|
||||
{
|
||||
id: 'langgenius/jina_tool/jina',
|
||||
kind: 'provider',
|
||||
name: 'langgenius/jina_tool/jina',
|
||||
iconClassName: 'i-custom-public-other-default-tool-icon',
|
||||
providerType: 'plugin',
|
||||
credentialType: 'unauthorized',
|
||||
credentialVariant: 'unauthorized',
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
} satisfies AgentSoulConfigFormState)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.publishDraft()
|
||||
})
|
||||
|
||||
expect(composerPutMutationFn).not.toHaveBeenCalled()
|
||||
expect(publishAgentMutationFn).not.toHaveBeenCalled()
|
||||
expect(trackEventMock).not.toHaveBeenCalled()
|
||||
expect(toastMock.error).toHaveBeenCalledWith(
|
||||
'workflow.nodes.agent.toolNotInstallTooltip:{"tool":"Jina"}',
|
||||
)
|
||||
})
|
||||
|
||||
it('should toast and skip publish when a configured tool is not authorized', async () => {
|
||||
toolProviderState.builtInTools = [credentialRequiredProvider]
|
||||
const { result, store } = renderUseAgentConfigureSync({
|
||||
currentModel: configuredModel,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
store.set(agentComposerDraftAtom, {
|
||||
...defaultAgentSoulConfigFormState,
|
||||
tools: [
|
||||
{
|
||||
id: 'google',
|
||||
kind: 'provider',
|
||||
name: 'google',
|
||||
displayName: 'Google Tools',
|
||||
iconClassName: 'i-custom-public-other-default-tool-icon',
|
||||
providerType: 'builtin',
|
||||
credentialType: 'unauthorized',
|
||||
credentialVariant: 'unauthorized',
|
||||
actions: [],
|
||||
},
|
||||
],
|
||||
} satisfies AgentSoulConfigFormState)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.publishDraft()
|
||||
})
|
||||
|
||||
expect(composerPutMutationFn).not.toHaveBeenCalled()
|
||||
expect(publishAgentMutationFn).not.toHaveBeenCalled()
|
||||
expect(trackEventMock).not.toHaveBeenCalled()
|
||||
expect(toastMock.error).toHaveBeenCalledWith(
|
||||
'workflow.nodes.agent.toolNotAuthorizedTooltip:{"tool":"Google Tools"}',
|
||||
)
|
||||
})
|
||||
|
||||
it('should keep default model fallback from leaving the local draft dirty after publish', async () => {
|
||||
const { result, store } = renderUseAgentConfigureSync({
|
||||
currentModel: configuredModel,
|
||||
|
||||
+27
-160
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { MarketplacePlugin } from '@dify/contracts/marketplace'
|
||||
import type { AgentOrchestrateAddActionOptions } from '../add-actions-context'
|
||||
import type { ToolSettingTarget } from './types'
|
||||
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
@@ -18,27 +17,25 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { parseToolProviderType } from '@/app/components/tools/provider-type'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { ToolPickerContent } from '@/app/components/workflow/block-selector/tool-picker'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import {
|
||||
addProviderToolsAtom,
|
||||
agentComposerToolsAtom,
|
||||
setProviderToolCredentialAtom,
|
||||
} from '@/features/agent-v2/agent-composer/store-modules/tools'
|
||||
import { ENABLE_AGENT_CLI_TOOLS } from '@/features/agent-v2/agent-detail/configure/feature-flags'
|
||||
import {
|
||||
useFetchPluginsInMarketPlaceByInfo,
|
||||
useInvalidateInstalledPluginList,
|
||||
} from '@/service/use-plugins'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidateAllBuiltInTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useInvalidateAllBuiltInTools } from '@/service/use-tools'
|
||||
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
||||
import {
|
||||
getAgentProviderPluginId,
|
||||
getAgentProviderToolDisplayName,
|
||||
getLocalizedText,
|
||||
getProviderCredentialType,
|
||||
getProviderCredentialVariant,
|
||||
useAgentToolPresentation,
|
||||
useAgentToolProviderCatalog,
|
||||
} from '../../../tool-provider-catalog'
|
||||
import { useRegisterAgentOrchestrateAddAction } from '../add-actions-context'
|
||||
import { ConfigureSectionAddButton } from '../common/add-button'
|
||||
import { ConfigureSectionEmpty } from '../common/empty'
|
||||
@@ -135,92 +132,6 @@ const AgentToolItem = memo(
|
||||
},
|
||||
)
|
||||
|
||||
function useAgentToolProviderMap() {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
return useMemo(() => {
|
||||
const providers = new Map<string, ToolWithProvider>()
|
||||
const resolvedProviderTypes = new Set<AgentProviderTool['providerType']>()
|
||||
const buildInToolList = Array.isArray(buildInTools) ? buildInTools : []
|
||||
const customToolList = Array.isArray(customTools) ? customTools : []
|
||||
const workflowToolList = Array.isArray(workflowTools) ? workflowTools : []
|
||||
const mcpToolList = Array.isArray(mcpTools) ? mcpTools : []
|
||||
const allProviders = [
|
||||
...buildInToolList,
|
||||
...customToolList,
|
||||
...workflowToolList,
|
||||
...mcpToolList,
|
||||
]
|
||||
|
||||
if (Array.isArray(buildInTools)) {
|
||||
resolvedProviderTypes.add(CollectionType.builtIn)
|
||||
resolvedProviderTypes.add('plugin')
|
||||
}
|
||||
if (Array.isArray(customTools)) resolvedProviderTypes.add(CollectionType.custom)
|
||||
if (Array.isArray(workflowTools)) resolvedProviderTypes.add(CollectionType.workflow)
|
||||
if (Array.isArray(mcpTools)) resolvedProviderTypes.add(CollectionType.mcp)
|
||||
|
||||
allProviders.forEach((provider) => {
|
||||
providers.set(provider.id, provider)
|
||||
providers.set(provider.name, provider)
|
||||
if (provider.plugin_id) {
|
||||
providers.set(provider.plugin_id, provider)
|
||||
providers.set(`${provider.plugin_id}/${provider.name}`, provider)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
providerById: providers,
|
||||
resolvedProviderTypes,
|
||||
}
|
||||
}, [buildInTools, customTools, workflowTools, mcpTools])
|
||||
}
|
||||
|
||||
function getLocalizedText(text: Partial<Record<string, string>> | undefined, language: string) {
|
||||
return text?.[language] ?? text?.en_US ?? text?.zh_Hans
|
||||
}
|
||||
|
||||
function getProviderPluginId(tool: AgentProviderTool) {
|
||||
if (tool.pluginId) return tool.pluginId
|
||||
|
||||
if (tool.providerType !== 'plugin' && tool.providerType !== CollectionType.builtIn) return ''
|
||||
|
||||
const providerIdSegments = tool.id.split('/')
|
||||
if (providerIdSegments.length !== 3) return ''
|
||||
|
||||
return providerIdSegments.slice(0, 2).join('/')
|
||||
}
|
||||
|
||||
function getProviderDisplayName(tool: AgentProviderTool) {
|
||||
const providerIdSegments = tool.name.split('/').filter(Boolean)
|
||||
return providerIdSegments.at(-1) ?? tool.name
|
||||
}
|
||||
|
||||
function getMarketplacePluginInfo(pluginId: string) {
|
||||
const [organization, plugin, ...remainingSegments] = pluginId.split('/')
|
||||
if (!organization || !plugin || remainingSegments.length > 0) return undefined
|
||||
|
||||
return {
|
||||
organization,
|
||||
plugin,
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderCredentialType(
|
||||
provider?: ToolWithProvider,
|
||||
): AgentProviderTool['credentialType'] {
|
||||
if (!provider) return undefined
|
||||
|
||||
if (Object.keys(provider.team_credentials ?? {}).length > 0) return 'api-key'
|
||||
|
||||
if (provider.type === CollectionType.builtIn && provider.allow_delete) return 'oauth2'
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getDisplayCredentialType(
|
||||
tool: AgentProviderTool,
|
||||
providerCredentialType: AgentProviderTool['credentialType'],
|
||||
@@ -233,27 +144,13 @@ function getDisplayCredentialType(
|
||||
return tool.credentialType ?? providerCredentialType
|
||||
}
|
||||
|
||||
function getProviderCredentialVariant(
|
||||
tool: AgentProviderTool,
|
||||
provider: ToolWithProvider,
|
||||
providerCredentialType: AgentProviderTool['credentialType'],
|
||||
) {
|
||||
if (!providerCredentialType) return 'none' as const
|
||||
|
||||
if (tool.credentialVariant !== 'none') return tool.credentialVariant
|
||||
|
||||
return tool.credentialId || provider.is_team_authorization
|
||||
? ('authorized' as const)
|
||||
: ('unauthorized' as const)
|
||||
}
|
||||
|
||||
function useDisplayTools(
|
||||
tools: AgentTool[],
|
||||
providerById: Map<string, ToolWithProvider>,
|
||||
resolvedProviderTypes: Set<AgentProviderTool['providerType']>,
|
||||
marketplacePluginById: Map<string, MarketplacePlugin>,
|
||||
toolPresentation: ReturnType<typeof useAgentToolPresentation>,
|
||||
) {
|
||||
const language = useGetLanguage()
|
||||
const { language, marketplacePluginById } = toolPresentation
|
||||
|
||||
return useMemo(() => {
|
||||
return tools.map((tool): DisplayAgentTool => {
|
||||
@@ -262,7 +159,7 @@ function useDisplayTools(
|
||||
const provider = providerById.get(tool.id) ?? providerById.get(tool.name)
|
||||
|
||||
if (!provider) {
|
||||
const providerPluginId = getProviderPluginId(tool)
|
||||
const providerPluginId = getAgentProviderPluginId(tool)
|
||||
const marketplacePlugin = marketplacePluginById.get(providerPluginId)
|
||||
|
||||
return {
|
||||
@@ -271,11 +168,11 @@ function useDisplayTools(
|
||||
pluginId: tool.pluginId ?? providerPluginId,
|
||||
pluginUniqueIdentifier:
|
||||
tool.pluginUniqueIdentifier ?? marketplacePlugin?.latest_package_identifier,
|
||||
displayName:
|
||||
tool.displayName ??
|
||||
getLocalizedText(marketplacePlugin?.label ?? marketplacePlugin?.labels, language) ??
|
||||
marketplacePlugin?.name ??
|
||||
getProviderDisplayName(tool),
|
||||
displayName: getAgentProviderToolDisplayName({
|
||||
language,
|
||||
marketplacePlugin,
|
||||
tool,
|
||||
}),
|
||||
icon:
|
||||
tool.icon ??
|
||||
(marketplacePlugin && providerPluginId
|
||||
@@ -292,7 +189,7 @@ function useDisplayTools(
|
||||
return {
|
||||
...tool,
|
||||
isInstalled: true,
|
||||
displayName: tool.displayName ?? getLocalizedText(provider.label, language) ?? tool.name,
|
||||
displayName: getAgentProviderToolDisplayName({ language, provider, tool }),
|
||||
icon: tool.icon ?? provider.icon,
|
||||
iconDark: tool.iconDark ?? provider.icon_dark,
|
||||
providerType: tool.providerType,
|
||||
@@ -378,7 +275,7 @@ function AddToolMenu({
|
||||
const { t } = useTranslation('agentV2')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [view, setView] = useState<AddToolMenuView>(addToolDefaultView)
|
||||
const { providerById } = useAgentToolProviderMap()
|
||||
const { providerById } = useAgentToolProviderCatalog()
|
||||
|
||||
const openToolPicker = useCallback(() => {
|
||||
setView('tool-picker')
|
||||
@@ -487,7 +384,7 @@ export function AgentTools() {
|
||||
const setProviderToolCredential = useSetAtom(setProviderToolCredentialAtom)
|
||||
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const { providerById, resolvedProviderTypes } = useAgentToolProviderMap()
|
||||
const { providerById, resolvedProviderTypes } = useAgentToolProviderCatalog()
|
||||
const tools = useAtomValue(agentComposerToolsAtom)
|
||||
const selectedTools = useSelectedProviderTools()
|
||||
const addTools = useSetAtom(addProviderToolsAtom)
|
||||
@@ -527,45 +424,15 @@ export function AgentTools() {
|
||||
() => (ENABLE_AGENT_CLI_TOOLS ? tools : tools.filter((tool) => tool.kind !== 'cli')),
|
||||
[tools],
|
||||
)
|
||||
const missingMarketplacePluginInfos = useMemo(() => {
|
||||
const pluginIds = new Set<string>()
|
||||
|
||||
visibleTools.forEach((tool) => {
|
||||
if (
|
||||
tool.kind !== 'provider' ||
|
||||
!resolvedProviderTypes.has(tool.providerType) ||
|
||||
providerById.has(tool.id) ||
|
||||
providerById.has(tool.name)
|
||||
)
|
||||
return
|
||||
|
||||
const pluginId = getProviderPluginId(tool)
|
||||
if (pluginId) pluginIds.add(pluginId)
|
||||
})
|
||||
|
||||
return Array.from(pluginIds).flatMap((pluginId) => {
|
||||
const info = getMarketplacePluginInfo(pluginId)
|
||||
return info ? [info] : []
|
||||
})
|
||||
}, [providerById, resolvedProviderTypes, visibleTools])
|
||||
const { data: missingMarketplacePluginsData } = useFetchPluginsInMarketPlaceByInfo(
|
||||
missingMarketplacePluginInfos,
|
||||
)
|
||||
const marketplacePluginById = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
(missingMarketplacePluginsData?.data.list ?? []).map(({ plugin }) => [
|
||||
plugin.plugin_id,
|
||||
plugin,
|
||||
]),
|
||||
),
|
||||
[missingMarketplacePluginsData],
|
||||
)
|
||||
const toolPresentation = useAgentToolPresentation(visibleTools, {
|
||||
providerById,
|
||||
resolvedProviderTypes,
|
||||
})
|
||||
const displayTools = useDisplayTools(
|
||||
visibleTools,
|
||||
providerById,
|
||||
resolvedProviderTypes,
|
||||
marketplacePluginById,
|
||||
toolPresentation,
|
||||
)
|
||||
/*
|
||||
* knip-ignore-start
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
'use client'
|
||||
|
||||
import type { MarketplacePlugin } from '@dify/contracts/marketplace'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { AgentProviderTool, AgentTool } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { useMemo } from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
type AgentToolPresentationProvider = Pick<
|
||||
AgentProviderTool,
|
||||
'kind' | 'id' | 'name' | 'displayName' | 'pluginId' | 'providerType'
|
||||
>
|
||||
type AgentToolPresentationSource = AgentTool | AgentToolPresentationProvider
|
||||
|
||||
export type AgentToolProviderCatalog = {
|
||||
providerById: Map<string, ToolWithProvider>
|
||||
resolvedProviderTypes: Set<AgentProviderTool['providerType']>
|
||||
}
|
||||
|
||||
export function createAgentToolProviderCatalog({
|
||||
buildInTools,
|
||||
customTools,
|
||||
mcpTools,
|
||||
workflowTools,
|
||||
}: {
|
||||
buildInTools?: ToolWithProvider[]
|
||||
customTools?: ToolWithProvider[]
|
||||
mcpTools?: ToolWithProvider[]
|
||||
workflowTools?: ToolWithProvider[]
|
||||
}): AgentToolProviderCatalog {
|
||||
const providers = new Map<string, ToolWithProvider>()
|
||||
const resolvedProviderTypes = new Set<AgentProviderTool['providerType']>()
|
||||
const buildInToolList = Array.isArray(buildInTools) ? buildInTools : []
|
||||
const customToolList = Array.isArray(customTools) ? customTools : []
|
||||
const workflowToolList = Array.isArray(workflowTools) ? workflowTools : []
|
||||
const mcpToolList = Array.isArray(mcpTools) ? mcpTools : []
|
||||
const allProviders = [...buildInToolList, ...customToolList, ...workflowToolList, ...mcpToolList]
|
||||
|
||||
if (Array.isArray(buildInTools)) {
|
||||
resolvedProviderTypes.add(CollectionType.builtIn)
|
||||
resolvedProviderTypes.add('plugin')
|
||||
}
|
||||
if (Array.isArray(customTools)) resolvedProviderTypes.add(CollectionType.custom)
|
||||
if (Array.isArray(workflowTools)) resolvedProviderTypes.add(CollectionType.workflow)
|
||||
if (Array.isArray(mcpTools)) resolvedProviderTypes.add(CollectionType.mcp)
|
||||
|
||||
allProviders.forEach((provider) => {
|
||||
providers.set(provider.id, provider)
|
||||
providers.set(provider.name, provider)
|
||||
if (provider.plugin_id) {
|
||||
providers.set(provider.plugin_id, provider)
|
||||
providers.set(`${provider.plugin_id}/${provider.name}`, provider)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
providerById: providers,
|
||||
resolvedProviderTypes,
|
||||
}
|
||||
}
|
||||
|
||||
export function useAgentToolProviderCatalog(): AgentToolProviderCatalog {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
return useMemo(
|
||||
() => createAgentToolProviderCatalog({ buildInTools, customTools, mcpTools, workflowTools }),
|
||||
[buildInTools, customTools, mcpTools, workflowTools],
|
||||
)
|
||||
}
|
||||
|
||||
export function getLocalizedText(
|
||||
text: Partial<Record<string, string>> | undefined,
|
||||
language: string,
|
||||
) {
|
||||
return text?.[language] ?? text?.en_US ?? text?.zh_Hans
|
||||
}
|
||||
|
||||
export function getAgentProviderPluginId(tool: AgentToolPresentationProvider) {
|
||||
if (tool.pluginId) return tool.pluginId
|
||||
|
||||
if (tool.providerType !== 'plugin' && tool.providerType !== CollectionType.builtIn) return ''
|
||||
|
||||
const providerIdSegments = tool.id.split('/')
|
||||
if (providerIdSegments.length !== 3) return ''
|
||||
|
||||
return providerIdSegments.slice(0, 2).join('/')
|
||||
}
|
||||
|
||||
function getMarketplacePluginInfo(pluginId: string) {
|
||||
const [organization, plugin, ...remainingSegments] = pluginId.split('/')
|
||||
if (!organization || !plugin || remainingSegments.length > 0) return undefined
|
||||
|
||||
return {
|
||||
organization,
|
||||
plugin,
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderFallbackDisplayName(tool: AgentToolPresentationProvider) {
|
||||
const providerIdSegments = tool.name.split('/').filter(Boolean)
|
||||
return providerIdSegments.at(-1) ?? tool.name
|
||||
}
|
||||
|
||||
export function getAgentProviderToolDisplayName({
|
||||
language,
|
||||
marketplacePlugin,
|
||||
provider,
|
||||
tool,
|
||||
}: {
|
||||
language: string
|
||||
marketplacePlugin?: MarketplacePlugin
|
||||
provider?: ToolWithProvider
|
||||
tool: AgentToolPresentationProvider
|
||||
}) {
|
||||
if (provider) return tool.displayName ?? getLocalizedText(provider.label, language) ?? tool.name
|
||||
|
||||
return (
|
||||
tool.displayName ??
|
||||
getLocalizedText(marketplacePlugin?.label ?? marketplacePlugin?.labels, language) ??
|
||||
marketplacePlugin?.name ??
|
||||
getProviderFallbackDisplayName(tool)
|
||||
)
|
||||
}
|
||||
|
||||
export function useAgentToolPresentation(
|
||||
tools: AgentToolPresentationSource[],
|
||||
{ providerById, resolvedProviderTypes }: AgentToolProviderCatalog,
|
||||
) {
|
||||
const language = useGetLanguage()
|
||||
const missingMarketplacePluginInfos = useMemo(() => {
|
||||
const pluginIds = new Set<string>()
|
||||
|
||||
tools.forEach((tool) => {
|
||||
if (
|
||||
tool.kind !== 'provider' ||
|
||||
!resolvedProviderTypes.has(tool.providerType) ||
|
||||
providerById.has(tool.id) ||
|
||||
providerById.has(tool.name)
|
||||
)
|
||||
return
|
||||
|
||||
const pluginId = getAgentProviderPluginId(tool)
|
||||
if (pluginId) pluginIds.add(pluginId)
|
||||
})
|
||||
|
||||
return Array.from(pluginIds).flatMap((pluginId) => {
|
||||
const info = getMarketplacePluginInfo(pluginId)
|
||||
return info ? [info] : []
|
||||
})
|
||||
}, [providerById, resolvedProviderTypes, tools])
|
||||
const { data: missingMarketplacePluginsData } = useFetchPluginsInMarketPlaceByInfo(
|
||||
missingMarketplacePluginInfos,
|
||||
)
|
||||
const marketplacePluginById = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
(missingMarketplacePluginsData?.data.list ?? []).map(({ plugin }) => [
|
||||
plugin.plugin_id,
|
||||
plugin,
|
||||
]),
|
||||
),
|
||||
[missingMarketplacePluginsData],
|
||||
)
|
||||
const toolDisplayNameById = useMemo(() => {
|
||||
const displayNames = new Map<string, string>()
|
||||
|
||||
tools.forEach((tool) => {
|
||||
if (tool.kind !== 'provider') return
|
||||
|
||||
const provider = providerById.get(tool.id) ?? providerById.get(tool.name)
|
||||
const marketplacePlugin = marketplacePluginById.get(getAgentProviderPluginId(tool))
|
||||
displayNames.set(
|
||||
tool.id,
|
||||
getAgentProviderToolDisplayName({ language, marketplacePlugin, provider, tool }),
|
||||
)
|
||||
})
|
||||
|
||||
return displayNames
|
||||
}, [language, marketplacePluginById, providerById, tools])
|
||||
|
||||
return {
|
||||
language,
|
||||
marketplacePluginById,
|
||||
toolDisplayNameById,
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderCredentialType(
|
||||
provider?: ToolWithProvider,
|
||||
): AgentProviderTool['credentialType'] {
|
||||
if (!provider) return undefined
|
||||
|
||||
if (Object.keys(provider.team_credentials ?? {}).length > 0) return 'api-key'
|
||||
|
||||
if (provider.type === CollectionType.builtIn && provider.allow_delete) return 'oauth2'
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getProviderCredentialVariant(
|
||||
tool: AgentProviderTool,
|
||||
provider: ToolWithProvider,
|
||||
providerCredentialType: AgentProviderTool['credentialType'],
|
||||
) {
|
||||
if (!providerCredentialType) return 'none' as const
|
||||
|
||||
if (tool.credentialVariant !== 'none') return tool.credentialVariant
|
||||
|
||||
return tool.credentialId || provider.is_team_authorization
|
||||
? ('authorized' as const)
|
||||
: ('unauthorized' as const)
|
||||
}
|
||||
|
||||
export type AgentToolPublishIssue = {
|
||||
type: 'uninstalled' | 'unauthorized'
|
||||
tool: AgentProviderTool
|
||||
}
|
||||
|
||||
export function getAgentToolPublishIssues(
|
||||
tools: AgentTool[],
|
||||
{ providerById, resolvedProviderTypes }: AgentToolProviderCatalog,
|
||||
): AgentToolPublishIssue[] {
|
||||
const issues: AgentToolPublishIssue[] = []
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool.kind !== 'provider') continue
|
||||
|
||||
const provider = providerById.get(tool.id) ?? providerById.get(tool.name)
|
||||
if (!provider) {
|
||||
if (resolvedProviderTypes.has(tool.providerType)) {
|
||||
issues.push({
|
||||
type: 'uninstalled',
|
||||
tool,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const providerCredentialType = getProviderCredentialType(provider)
|
||||
if (getProviderCredentialVariant(tool, provider, providerCredentialType) === 'unauthorized') {
|
||||
issues.push({
|
||||
type: 'unauthorized',
|
||||
tool,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
export function getAgentToolPublishIssue(
|
||||
tools: AgentTool[],
|
||||
catalog: AgentToolProviderCatalog,
|
||||
): AgentToolPublishIssue | undefined {
|
||||
return getAgentToolPublishIssues(tools, catalog)[0]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { mutationOptions, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import isEqual from 'fast-deep-equal'
|
||||
import { useSetAtom, useStore } from 'jotai'
|
||||
import { useAtomValue, useSetAtom, useStore } from 'jotai'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
@@ -22,7 +22,13 @@ import {
|
||||
agentComposerSavedDraftAtom,
|
||||
isAgentComposerDirtyAtom,
|
||||
} from '@/features/agent-v2/agent-composer/store'
|
||||
import { agentComposerToolPresentationIdentitiesAtom } from '@/features/agent-v2/agent-composer/store-modules/tools'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
getAgentToolPublishIssue,
|
||||
useAgentToolPresentation,
|
||||
useAgentToolProviderCatalog,
|
||||
} from './tool-provider-catalog'
|
||||
|
||||
const DRAFT_AUTOSAVE_WAIT = 5000
|
||||
|
||||
@@ -40,7 +46,11 @@ export function useAgentConfigureSync({
|
||||
enabled: boolean
|
||||
}) {
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { t: tWorkflow } = useTranslation('workflow')
|
||||
const getKnowledgeValidationMessage = useKnowledgeValidationMessage()
|
||||
const toolPresentationIdentities = useAtomValue(agentComposerToolPresentationIdentitiesAtom)
|
||||
const toolProviderCatalog = useAgentToolProviderCatalog()
|
||||
const toolPresentation = useAgentToolPresentation(toolPresentationIdentities, toolProviderCatalog)
|
||||
const queryClient = useQueryClient()
|
||||
const store = useStore()
|
||||
const setSavedDraft = useSetAtom(agentComposerSavedDraftAtom)
|
||||
@@ -355,6 +365,19 @@ export function useAgentConfigureSync({
|
||||
return
|
||||
}
|
||||
|
||||
const toolPublishIssue = getAgentToolPublishIssue(draft.tools, toolProviderCatalog)
|
||||
if (toolPublishIssue) {
|
||||
const toolName =
|
||||
toolPresentation.toolDisplayNameById.get(toolPublishIssue.tool.id) ??
|
||||
toolPublishIssue.tool.name
|
||||
toast.error(
|
||||
toolPublishIssue.type === 'uninstalled'
|
||||
? tWorkflow(($) => $['nodes.agent.toolNotInstallTooltip'], { tool: toolName })
|
||||
: tWorkflow(($) => $['nodes.agent.toolNotAuthorizedTooltip'], { tool: toolName }),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const knowledgeValidation = validateKnowledgeRetrievals(draft.knowledgeRetrievals)
|
||||
if (!knowledgeValidation.isValid) {
|
||||
toast.error(
|
||||
@@ -406,6 +429,9 @@ export function useAgentConfigureSync({
|
||||
runPublishTransaction,
|
||||
store,
|
||||
tCommon,
|
||||
toolProviderCatalog,
|
||||
toolPresentation.toolDisplayNameById,
|
||||
tWorkflow,
|
||||
])
|
||||
|
||||
return {
|
||||
|
||||
@@ -472,7 +472,7 @@
|
||||
"nodes.agent.task.mention": "Mention",
|
||||
"nodes.agent.task.placeholder": "Describe what this agent should do...",
|
||||
"nodes.agent.task.tooltip": "Additional prompt to help agent handle this very node. Use / to make explicit reference to variables.\nIf configured properly, you could be able to trust your agent to figure things out itself.",
|
||||
"nodes.agent.toolNotAuthorizedTooltip": "{{tool}} Not Authorized",
|
||||
"nodes.agent.toolNotAuthorizedTooltip": "{{tool}} is not authorized",
|
||||
"nodes.agent.toolNotInstallTooltip": "{{tool}} is not installed",
|
||||
"nodes.agent.toolbox": "toolbox",
|
||||
"nodes.agent.tools": "Tools",
|
||||
|
||||
Reference in New Issue
Block a user