mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(agent): expose KnowledgeFS configuration entry points
This commit is contained in:
parent
1e7017772a
commit
d3d77bb2c8
@ -1391,7 +1391,7 @@ describe('useAgentConfigureSync', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should toast metadata filtering model error when publishing with automatic metadata filtering and no model', async () => {
|
||||
it('requires legacy knowledge migration before validating its old retrieval settings', async () => {
|
||||
const { result, store } = renderUseAgentConfigureSync({
|
||||
currentModel: configuredModel,
|
||||
})
|
||||
@ -1416,9 +1416,7 @@ describe('useAgentConfigureSync', () => {
|
||||
|
||||
expect(composerPutMutationFn).not.toHaveBeenCalled()
|
||||
expect(publishAgentMutationFn).not.toHaveBeenCalled()
|
||||
expect(toastMock.error).toHaveBeenCalledWith(
|
||||
'agentV2.agentDetail.configure.knowledgeRetrieval.validation.metadataModelRequired',
|
||||
)
|
||||
expect(toastMock.error).toHaveBeenCalledWith('agentV2.agentDetail.configure.knowledgeFs.legacy')
|
||||
})
|
||||
|
||||
it('should expose publishing status from the publish mutation while publish is pending', async () => {
|
||||
|
||||
@ -4,13 +4,17 @@ import type { AgentTool } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { createStore, Provider as JotaiProvider } from 'jotai'
|
||||
import { queryClientAtom } from 'jotai-tanstack-query'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
|
||||
import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge'
|
||||
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
|
||||
import { agentComposerToolsAtom } from '@/features/agent-v2/agent-composer/store-modules/tools'
|
||||
import { render } from '@/test/console/render'
|
||||
import {
|
||||
createConsoleQueryClient,
|
||||
renderWithConsoleQuery as render,
|
||||
} from '@/test/console/query-data'
|
||||
import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { AgentPromptEditor } from '../orchestrate/prompt-editor'
|
||||
import { AgentPromptSlashMenu } from '../orchestrate/prompt-editor/slash'
|
||||
@ -261,6 +265,8 @@ const renderAgentPromptEditor = (
|
||||
draftOverrides: Partial<typeof defaultAgentSoulConfigFormState> = {},
|
||||
) => {
|
||||
const store = createStore()
|
||||
const queryClient = createConsoleQueryClient()
|
||||
store.set(queryClientAtom, queryClient)
|
||||
seedRegisteredConsoleStateFixture(store)
|
||||
store.set(agentComposerDraftAtom, {
|
||||
...promptEditorDraft,
|
||||
@ -272,6 +278,7 @@ const renderAgentPromptEditor = (
|
||||
<JotaiProvider store={store}>
|
||||
<AgentPromptEditor />
|
||||
</JotaiProvider>,
|
||||
{ queryClient, systemFeatures: { agent_knowledge_fs_enabled: false } },
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@ -0,0 +1,194 @@
|
||||
import type { PromptEditorProps } from '@/app/components/base/prompt-editor'
|
||||
import { zKnowledgeFsSpaceListResponse } from '@dify/contracts/api/console/knowledge-fs/zod.gen'
|
||||
import { act, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import {
|
||||
createConsoleQueryClient,
|
||||
renderWithConsoleQuery as render,
|
||||
seedSystemFeatures,
|
||||
} from '@/test/console/query-data'
|
||||
import { AgentOrchestratePanel } from '../index'
|
||||
|
||||
// Keep the panel, KnowledgeFS dialog, prompt commands and action registration real.
|
||||
// Independent sections and Lexical selection are outside this entry-point contract.
|
||||
vi.mock('../model-config/field', () => ({ AgentModelField: () => null }))
|
||||
vi.mock('../skills', () => ({ AgentSkills: () => null }))
|
||||
vi.mock('../files', () => ({ AgentFiles: () => null }))
|
||||
vi.mock('../tools', () => ({ AgentTools: () => null }))
|
||||
vi.mock('../advanced', () => ({ AgentAdvancedSettings: () => null }))
|
||||
vi.mock('@/app/components/base/prompt-editor', () => ({
|
||||
default: (props: PromptEditorProps) => (
|
||||
<textarea
|
||||
aria-labelledby={props['aria-labelledby']}
|
||||
value={props.value}
|
||||
readOnly={!props.editable}
|
||||
onChange={(event) => props.onChange?.(event.target.value)}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContextSelector: (selector: (state: { enableSkill: boolean }) => unknown) =>
|
||||
selector({ enableSkill: false }),
|
||||
}))
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => ({ data: [] }),
|
||||
useAllCustomTools: () => ({ data: [] }),
|
||||
useAllWorkflowTools: () => ({ data: [] }),
|
||||
useAllMCPTools: () => ({ data: [] }),
|
||||
}))
|
||||
vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }) }))
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en_US',
|
||||
useDocLink: () => () => 'https://docs.example.com',
|
||||
}))
|
||||
|
||||
const SPACE = '00000000-0000-4000-8000-000000000001'
|
||||
const binding = { id: 'docs', controlSpaceId: SPACE, name: 'Product manual' }
|
||||
|
||||
function setup({ enabled = true, workflow = false, readOnly = false, configured = false } = {}) {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
const options = consoleQuery.knowledgeFs.spaces.get.infiniteOptions({
|
||||
input: (pageParam) => ({ query: { limit: 50, page: pageParam } }),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
})
|
||||
queryClient.setQueryData(options.queryKey, {
|
||||
pages: [
|
||||
zKnowledgeFsSpaceListResponse.parse({
|
||||
data: [
|
||||
{
|
||||
control_space_id: SPACE,
|
||||
knowledge_space_id: SPACE,
|
||||
created_at: '2026-09-07T00:00:00Z',
|
||||
updated_at: '2026-09-07T00:00:00Z',
|
||||
linked_apps: 0,
|
||||
owner_account_id: 'account',
|
||||
permission_keys: ['knowledge_space_read', 'knowledge_space_query'],
|
||||
resource_version: 1,
|
||||
state: 'active',
|
||||
technical_status: 'available',
|
||||
visibility: 'only_me',
|
||||
technical_summary: {
|
||||
knowledge_space_id: SPACE,
|
||||
name: 'Product manual',
|
||||
description: 'Product documentation',
|
||||
revision: 1,
|
||||
slug: 'product-manual',
|
||||
},
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
limit: 50,
|
||||
has_more: false,
|
||||
}),
|
||||
],
|
||||
pageParams: [1],
|
||||
})
|
||||
return render(
|
||||
<AgentComposerProvider
|
||||
initialDraft={{
|
||||
...defaultAgentSoulConfigFormState,
|
||||
knowledgeRetrievals: configured ? [binding] : [],
|
||||
}}
|
||||
>
|
||||
<AgentOrchestratePanel
|
||||
agentId="agent-1"
|
||||
{...(workflow ? { appId: 'workflow-1', nodeId: 'node-1' } : {})}
|
||||
textGenerationModelList={[]}
|
||||
onSelectModel={vi.fn()}
|
||||
showHeader={false}
|
||||
showPublishBar={false}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</AgentComposerProvider>,
|
||||
{ queryClient, systemFeatures: { agent_knowledge_fs_enabled: enabled } },
|
||||
)
|
||||
}
|
||||
|
||||
async function confirmSpace(user: ReturnType<typeof userEvent.setup>) {
|
||||
const dialog = await screen.findByRole('dialog', { name: /knowledgeFs.title/ })
|
||||
await user.click(within(dialog).getByRole('checkbox', { name: 'Product manual' }))
|
||||
await user.click(within(dialog).getByRole('button', { name: /operation.confirm/ }))
|
||||
await waitFor(() => expect(dialog).not.toBeInTheDocument())
|
||||
}
|
||||
|
||||
describe('KnowledgeFS entry points in the real Agent configuration panel', () => {
|
||||
it.each([false, true])(
|
||||
'can add a knowledge space from the panel (workflow=%s)',
|
||||
async (workflow) => {
|
||||
const user = userEvent.setup()
|
||||
setup({ workflow })
|
||||
|
||||
expect(await screen.findByRole('heading', { name: /knowledgeFs.title/ })).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: /knowledgeFs.add/ }))
|
||||
await confirmSpace(user)
|
||||
|
||||
expect(screen.getByText('Product manual')).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: /knowledgeFs.add/ }))
|
||||
const dialog = await screen.findByRole('dialog', { name: /knowledgeFs.title/ })
|
||||
expect(within(dialog).getByRole('checkbox', { name: 'Product manual' })).toHaveAttribute(
|
||||
'aria-checked',
|
||||
'true',
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
it('adds a KnowledgeFS binding from the prompt menu and inserts its reference', async () => {
|
||||
const user = userEvent.setup()
|
||||
setup()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /prompt.insert.label/ }))
|
||||
await user.click(screen.getByRole('button', { name: /knowledgeRetrieval.label/ }))
|
||||
await user.click(screen.getByRole('button', { name: /knowledgeRetrieval.add/ }))
|
||||
await confirmSpace(user)
|
||||
|
||||
expect(
|
||||
screen.getByRole<HTMLTextAreaElement>('textbox', { name: /prompt.label/ }).value,
|
||||
).toMatch(/^\[§knowledge:[0-9a-f-]+:Product manual§\] $/)
|
||||
expect(screen.queryByRole('dialog', { name: /prompt.insert.label/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Product manual')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('explains unavailable runtime and follows capability updates without losing bindings', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { queryClient } = setup({ enabled: false, configured: true })
|
||||
|
||||
expect(screen.getByText('Product manual')).toBeInTheDocument()
|
||||
expect(screen.getByRole('status')).toHaveTextContent(/knowledgeFs.runtimeUnavailable/)
|
||||
expect(screen.getByRole('button', { name: /knowledgeFs.add/ })).toBeDisabled()
|
||||
await user.click(screen.getByRole('button', { name: /prompt.insert.label/ }))
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /knowledgeRetrieval.label/ }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
seedSystemFeatures(queryClient, { agent_knowledge_fs_enabled: true })
|
||||
})
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('button', { name: /knowledgeFs.add/ })).toBeEnabled(),
|
||||
)
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /knowledgeRetrieval.label/ }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('Product manual')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a bound knowledge space without editing controls in a read-only Workflow panel', async () => {
|
||||
setup({ workflow: true, readOnly: true, configured: true })
|
||||
|
||||
expect(await screen.findByRole('heading', { name: /knowledgeFs.title/ })).toBeInTheDocument()
|
||||
expect(screen.getByText('Product manual')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /knowledgeFs.add/ })).toBeDisabled()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /knowledgeRetrieval.edit/ }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /knowledgeRetrieval.remove/ }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /prompt.insert.label/ })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -18,7 +18,6 @@ import {
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ENABLE_AGENT_KNOWLEDGE_RETRIEVAL } from '@/features/agent-v2/agent-detail/configure/feature-flags'
|
||||
import { AgentOrchestrateAddActionsProvider } from './add-actions'
|
||||
import { AgentAdvancedSettings } from './advanced'
|
||||
import { AgentOrchestrateBottomActions } from './bottom-actions'
|
||||
@ -173,7 +172,7 @@ export function AgentOrchestratePanel({
|
||||
<AgentSkills />
|
||||
<AgentFiles />
|
||||
<AgentTools />
|
||||
{ENABLE_AGENT_KNOWLEDGE_RETRIEVAL && <AgentKnowledgeRetrieval />}
|
||||
<AgentKnowledgeRetrieval />
|
||||
<AgentAdvancedSettings />
|
||||
</AgentBuildDraftChangedKeysProvider>
|
||||
</AgentOrchestrateAddActionsProvider>
|
||||
|
||||
@ -45,10 +45,8 @@ import {
|
||||
addProviderToolsAtom,
|
||||
agentComposerToolsAtom,
|
||||
} from '@/features/agent-v2/agent-composer/store-modules/tools'
|
||||
import {
|
||||
ENABLE_AGENT_CLI_TOOLS,
|
||||
ENABLE_AGENT_KNOWLEDGE_RETRIEVAL,
|
||||
} from '@/features/agent-v2/agent-detail/configure/feature-flags'
|
||||
import { ENABLE_AGENT_CLI_TOOLS } from '@/features/agent-v2/agent-detail/configure/feature-flags'
|
||||
import { agentKnowledgeFsEnabledAtom } from '@/features/system-features/state'
|
||||
import { useAgentOrchestrateAddActions } from '../add-actions-context'
|
||||
import { AgentConfigureTipContent } from '../common/tip-content'
|
||||
import {
|
||||
@ -444,6 +442,7 @@ export function AgentPromptEditor() {
|
||||
const addProviderTools = useSetAtom(addProviderToolsAtom)
|
||||
const { getConfiguredToolIcon } = useAgentPromptToolIconResolver()
|
||||
const retrievals = useAtomValue(agentComposerKnowledgeRetrievalsAtom)
|
||||
const knowledgeEnabled = useAtomValue(agentKnowledgeFsEnabledAtom)
|
||||
const addActions = useAgentOrchestrateAddActions()
|
||||
const promptTip = t(($) => $['agentDetail.configure.prompt.tip'])
|
||||
const promptPlaceholder = (
|
||||
@ -1011,7 +1010,7 @@ export function AgentPromptEditor() {
|
||||
label: t(($) => $['agentDetail.configure.tools.label']),
|
||||
icon: 'i-ri-box-3-line',
|
||||
},
|
||||
...(ENABLE_AGENT_KNOWLEDGE_RETRIEVAL
|
||||
...(knowledgeEnabled
|
||||
? [
|
||||
{
|
||||
key: 'knowledge' as const,
|
||||
|
||||
@ -83,7 +83,11 @@ const isCliToolItem = (
|
||||
const isKnowledgeRetrievalItem = (
|
||||
item: AgentOrchestrateAddedItem,
|
||||
): item is AgentKnowledgeRetrievalItem =>
|
||||
'queryMode' in item || 'customQuery' in item || 'selectedDatasets' in item || 'nameKey' in item
|
||||
'controlSpaceId' in item ||
|
||||
'queryMode' in item ||
|
||||
'customQuery' in item ||
|
||||
'selectedDatasets' in item ||
|
||||
'nameKey' in item
|
||||
|
||||
export function AgentPromptSlashMenu({
|
||||
view,
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export const ENABLE_AGENT_CLI_TOOLS = false
|
||||
export const ENABLE_AGENT_CONTENT_MODERATION = false
|
||||
export const ENABLE_AGENT_KNOWLEDGE_RETRIEVAL = false
|
||||
export const ENABLE_AGENT_SECRET_ENV_VARIABLES = false
|
||||
|
||||
Loading…
Reference in New Issue
Block a user