mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
feat: hide add library skill in agent ui (#41250)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
960018253b
commit
a6fe278662
@ -852,4 +852,4 @@ DIFY_ENV_NACOS_REQUEST_TIMEOUT=10.0
|
||||
DIFY_ENV_NACOS_CONNECT_TIMEOUT=3.0
|
||||
|
||||
# skill entry
|
||||
ENABLE_SKILL=false
|
||||
ENABLE_SKILL=true
|
||||
|
||||
@ -1294,7 +1294,7 @@ class DataSetConfig(BaseSettings):
|
||||
class SkillConfig(BaseSettings):
|
||||
ENABLE_SKILL: bool = Field(
|
||||
description="Enable or disable Skill feature entry points",
|
||||
default=False,
|
||||
default=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -18209,7 +18209,7 @@ Flask blueprint initialization.
|
||||
| docs_processing | string, <br>**Default:** standard | | Yes |
|
||||
| documents_upload_quota | [LimitationModel](#limitationmodel) | | Yes |
|
||||
| education | [EducationModel](#educationmodel) | | Yes |
|
||||
| enable_skill | boolean | | Yes |
|
||||
| enable_skill | boolean, <br>**Default:** true | | Yes |
|
||||
| human_input_email_delivery_enabled | boolean | | Yes |
|
||||
| is_allow_transfer_workspace | boolean, <br>**Default:** true | | Yes |
|
||||
| knowledge_pipeline | [KnowledgePipeline](#knowledgepipeline) | | Yes |
|
||||
|
||||
@ -151,7 +151,7 @@ class PluginInstallationPermissionModel(FeatureResponseModel):
|
||||
class FeatureModel(FeatureResponseModel):
|
||||
billing: BillingModel = BillingModel()
|
||||
education: EducationModel = EducationModel()
|
||||
enable_skill: bool = False
|
||||
enable_skill: bool = True
|
||||
members: LimitationModel = LimitationModel(size=0, limit=1)
|
||||
apps: LimitationModel = LimitationModel(size=0, limit=10)
|
||||
vector_space: LimitationModel | None = LimitationModel(size=0, limit=5)
|
||||
|
||||
@ -6,7 +6,7 @@ from services.feature_service import FeatureService
|
||||
|
||||
|
||||
def test_skill_feature_is_disabled_by_default() -> None:
|
||||
assert FeatureModel().enable_skill is False
|
||||
assert FeatureModel().enable_skill is True
|
||||
|
||||
|
||||
def test_skill_feature_follows_env_config(config_overrides: Callable[..., None]) -> None:
|
||||
|
||||
@ -32,7 +32,7 @@ ENABLE_TRIAL_APP=false
|
||||
ENABLE_EXPLORE_BANNER=false
|
||||
ENABLE_LEARN_APP=true
|
||||
ENABLE_STEP_BY_STEP_TOUR=false
|
||||
ENABLE_SKILL=false
|
||||
ENABLE_SKILL=true
|
||||
RBAC_ENABLED=false
|
||||
ENABLE_LICENSE_EXPIRY_NOTICE=true
|
||||
CELERY_BROKER_URL=redis://:difyai123456@redis:6379/1
|
||||
|
||||
@ -103,7 +103,7 @@ export const zFeatureModel = z.object({
|
||||
docs_processing: z.string().default('standard'),
|
||||
documents_upload_quota: zLimitationModel.default({ limit: 50, size: 0 }),
|
||||
education: zEducationModel.default({ activated: false, enabled: false }),
|
||||
enable_skill: z.boolean().default(false),
|
||||
enable_skill: z.boolean().default(true),
|
||||
human_input_email_delivery_enabled: z.boolean().default(false),
|
||||
is_allow_transfer_workspace: z.boolean().default(true),
|
||||
knowledge_pipeline: zKnowledgePipeline.default({ publish_enabled: false }),
|
||||
|
||||
@ -199,6 +199,11 @@ vi.mock('@/context/workspace-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContextSelector: (selector: (state: { enableSkill: boolean }) => unknown) =>
|
||||
selector({ enableSkill: true }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useAllBuiltInTools: () => ({ data: mockBuiltInTools }),
|
||||
useAllCustomTools: () => ({ data: [] }),
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { createContext, use } from 'react'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { agentComposerFilesAtom } from '@/features/agent-v2/agent-composer/store-modules/files'
|
||||
import { agentComposerSkillsAtom } from '@/features/agent-v2/agent-composer/store-modules/skills'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
@ -40,16 +41,18 @@ export const useAgentConfigSkills = () => {
|
||||
|
||||
export const useAgentWorkspaceSkillBindings = () => {
|
||||
const { agentId } = useAgentConfigApiContext()
|
||||
const enableSkill = useProviderContextSelector((state) => state.enableSkill)
|
||||
|
||||
return useQuery(
|
||||
consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryOptions({
|
||||
return useQuery({
|
||||
...consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
enabled: enableSkill,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAgentConfigFiles = () => {
|
||||
|
||||
@ -38,6 +38,7 @@ import { Infotip } from '@/app/components/base/infotip'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge'
|
||||
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
|
||||
import {
|
||||
@ -421,6 +422,7 @@ function AgentPromptSelectionBridge({
|
||||
export function AgentPromptEditor() {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const readOnly = useAgentOrchestrateReadOnly()
|
||||
const enableSkill = useProviderContextSelector((state) => state.enableSkill)
|
||||
const [value, setValue] = useAtom(agentComposerPromptAtom)
|
||||
const { skills: embeddedSkills } = useAgentConfigSkills()
|
||||
const workspaceSkillBindingsQuery = useAgentWorkspaceSkillBindings()
|
||||
@ -1058,6 +1060,7 @@ export function AgentPromptEditor() {
|
||||
onAddFile={addActions.files}
|
||||
onAddKnowledge={addActions.knowledge}
|
||||
onAddSkill={addActions.skills}
|
||||
canAddWorkspaceSkill={enableSkill}
|
||||
knowledgeRetrievals={retrievals}
|
||||
onBack={returnToSlashMenuMain}
|
||||
onOpenCategory={handleOpenSlashMenuCategory}
|
||||
|
||||
@ -53,6 +53,7 @@ type AgentPromptSlashMenuProps = {
|
||||
onAddFile?: AgentOrchestrateAddAction
|
||||
onAddKnowledge?: AgentOrchestrateAddAction
|
||||
onAddSkill?: AgentOrchestrateAddAction
|
||||
canAddWorkspaceSkill?: boolean
|
||||
knowledgeRetrievals: AgentKnowledgeRetrievalItem[]
|
||||
onBack: () => void
|
||||
onOpenCategory: (view: Exclude<SlashMenuView, 'main'>) => void
|
||||
@ -95,6 +96,7 @@ export function AgentPromptSlashMenu({
|
||||
onAddFile,
|
||||
onAddKnowledge,
|
||||
onAddSkill,
|
||||
canAddWorkspaceSkill = true,
|
||||
knowledgeRetrievals,
|
||||
onBack,
|
||||
onOpenCategory,
|
||||
@ -215,11 +217,13 @@ export function AgentPromptSlashMenu({
|
||||
/>
|
||||
) : view === 'skills' ? (
|
||||
<div className="flex flex-col border-t border-divider-subtle p-1">
|
||||
<AgentPromptSkillAddButton
|
||||
icon="i-custom-vender-agent-v2-building-blocks"
|
||||
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
|
||||
onClick={() => handleAddFromFooter('library')}
|
||||
/>
|
||||
{canAddWorkspaceSkill && (
|
||||
<AgentPromptSkillAddButton
|
||||
icon="i-custom-vender-agent-v2-building-blocks"
|
||||
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
|
||||
onClick={() => handleAddFromFooter('library')}
|
||||
/>
|
||||
)}
|
||||
<AgentPromptSkillAddButton
|
||||
icon="i-ri-upload-cloud-2-line"
|
||||
label={t(($) => $['agentDetail.configure.skills.addMenu.upload.label'])}
|
||||
|
||||
@ -81,6 +81,9 @@ const mocks = vi.hoisted(() => ({
|
||||
fileUploadConfig: {
|
||||
skill_file_size_limit: 64,
|
||||
},
|
||||
providerContext: {
|
||||
enableSkill: true,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
@ -112,6 +115,11 @@ vi.mock('@/context/permission-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContextSelector: (selector: (state: { enableSkill: boolean }) => unknown) =>
|
||||
selector(mocks.providerContext),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
tags: {
|
||||
@ -342,6 +350,7 @@ function renderAgentSkills({
|
||||
describe('AgentSkills', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.providerContext.enableSkill = true
|
||||
mocks.fileUploadConfig.skill_file_size_limit = 64
|
||||
vi.stubGlobal('fetch', mocks.fetch)
|
||||
document.cookie = 'csrf_token=csrf-token; path=/'
|
||||
@ -732,9 +741,14 @@ describe('AgentSkills', () => {
|
||||
})
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
const addButton = screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.add/i,
|
||||
})
|
||||
expect(addButton).not.toHaveAttribute('data-popup-open')
|
||||
|
||||
await user.click(addButton)
|
||||
expect(addButton).toHaveAttribute('data-popup-open', '')
|
||||
|
||||
const workspaceMenuItem = screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
|
||||
})
|
||||
@ -770,6 +784,30 @@ describe('AgentSkills', () => {
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should hide workspace skill selection when skill is disabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.providerContext.enableSkill = false
|
||||
|
||||
renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState })
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.skills\.add/i }),
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.workspace\.label/i,
|
||||
}),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /agentV2\.agentDetail\.configure\.skills\.addMenu\.upload\.label/i,
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
expect(mocks.agentSkillBindingsQueryOptions).toHaveBeenCalledWith(expect.anything())
|
||||
expect(mocks.workspaceSkillsInfiniteOptions).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not replace existing workspace skill bindings before they finish loading', async () => {
|
||||
const user = userEvent.setup()
|
||||
let resolveBindings:
|
||||
|
||||
@ -31,6 +31,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SearchInput } from '@/app/components/base/search-input'
|
||||
import { SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import {
|
||||
agentComposerSkillsAtom,
|
||||
removeAgentSkillAtom,
|
||||
@ -466,6 +467,7 @@ export function AgentSkills() {
|
||||
const [isUploadOpen, setIsUploadOpen] = useState(false)
|
||||
const promptAddCallbackRef = useRef<AgentOrchestrateAddActionOptions['onAdded']>(undefined)
|
||||
const apiContext = useAgentConfigApiContext()
|
||||
const enableSkill = useProviderContextSelector((state) => state.enableSkill)
|
||||
const skills = useAtomValue(agentComposerSkillsAtom)
|
||||
const upsertAgentSkill = useSetAtom(upsertAgentSkillAtom)
|
||||
const removeAgentSkill = useSetAtom(removeAgentSkillAtom)
|
||||
@ -485,6 +487,7 @@ export function AgentSkills() {
|
||||
})
|
||||
const agentSkillBindingsQuery = useQuery({
|
||||
...agentSkillBindingsQueryOptions,
|
||||
enabled: enableSkill,
|
||||
})
|
||||
const hasLoadedAgentSkillBindings = agentSkillBindingsQuery.data !== undefined
|
||||
const { isPending: isReplacingAgentSkillBindings, mutate: replaceAgentSkillBindings } =
|
||||
@ -568,22 +571,25 @@ export function AgentSkills() {
|
||||
],
|
||||
)
|
||||
|
||||
const handlePromptAdd = useCallback((options?: AgentOrchestrateAddActionOptions) => {
|
||||
promptAddCallbackRef.current = options?.onAdded
|
||||
if (options?.skillSource === 'library') {
|
||||
setAddMenuView('workspace-selector')
|
||||
const handlePromptAdd = useCallback(
|
||||
(options?: AgentOrchestrateAddActionOptions) => {
|
||||
promptAddCallbackRef.current = options?.onAdded
|
||||
if (options?.skillSource === 'library' && enableSkill) {
|
||||
setAddMenuView('workspace-selector')
|
||||
setAddMenuOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (options?.skillSource === 'upload') {
|
||||
setIsUploadOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
setAddMenuView('menu')
|
||||
setAddMenuOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (options?.skillSource === 'upload') {
|
||||
setIsUploadOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
setAddMenuView('menu')
|
||||
setAddMenuOpen(true)
|
||||
}, [])
|
||||
},
|
||||
[enableSkill],
|
||||
)
|
||||
useRegisterAgentOrchestrateAddAction('skills', handlePromptAdd)
|
||||
|
||||
const handleAddMenuOpenChange = useCallback((open: boolean) => {
|
||||
@ -595,8 +601,10 @@ export function AgentSkills() {
|
||||
}, [])
|
||||
|
||||
const handleOpenWorkspaceSelector = useCallback(() => {
|
||||
if (!enableSkill) return
|
||||
|
||||
setAddMenuView('workspace-selector')
|
||||
}, [])
|
||||
}, [enableSkill])
|
||||
|
||||
const handleOpenUploadFromMenu = useCallback(() => {
|
||||
setAddMenuOpen(false)
|
||||
@ -615,6 +623,7 @@ export function AgentSkills() {
|
||||
const handleSelectWorkspaceSkill = useCallback(
|
||||
(skill: SkillResponse) => {
|
||||
if (
|
||||
!enableSkill ||
|
||||
!hasLoadedAgentSkillBindings ||
|
||||
!skill.latest_published_version_id ||
|
||||
boundSkillIds.includes(skill.id)
|
||||
@ -636,7 +645,7 @@ export function AgentSkills() {
|
||||
setAddMenuView('menu')
|
||||
})
|
||||
},
|
||||
[boundSkillIds, hasLoadedAgentSkillBindings, replaceWorkspaceSkillBindings, t],
|
||||
[boundSkillIds, enableSkill, hasLoadedAgentSkillBindings, replaceWorkspaceSkillBindings, t],
|
||||
)
|
||||
|
||||
const handleUploadOpenChange = useCallback((open: boolean) => {
|
||||
@ -714,7 +723,7 @@ export function AgentSkills() {
|
||||
aria-label={t(($) => $['agentDetail.configure.skills.add'])}
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="shrink-0 gap-1 px-2"
|
||||
className="shrink-0 gap-1 px-2 data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
<span aria-hidden className="i-ri-add-line size-3.5" />
|
||||
<span>{tCommon(($) => $['operation.add'])}</span>
|
||||
@ -732,14 +741,16 @@ export function AgentSkills() {
|
||||
>
|
||||
{addMenuView === 'menu' ? (
|
||||
<>
|
||||
<AgentSkillAddMenuItem
|
||||
iconClassName="i-custom-vender-agent-v2-building-blocks"
|
||||
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
|
||||
description={t(
|
||||
($) => $['agentDetail.configure.skills.addMenu.workspace.description'],
|
||||
)}
|
||||
onClick={handleOpenWorkspaceSelector}
|
||||
/>
|
||||
{enableSkill && (
|
||||
<AgentSkillAddMenuItem
|
||||
iconClassName="i-custom-vender-agent-v2-building-blocks"
|
||||
label={t(($) => $['agentDetail.configure.skills.addMenu.workspace.label'])}
|
||||
description={t(
|
||||
($) => $['agentDetail.configure.skills.addMenu.workspace.description'],
|
||||
)}
|
||||
onClick={handleOpenWorkspaceSelector}
|
||||
/>
|
||||
)}
|
||||
<AgentSkillAddMenuItem
|
||||
badge={t(($) => $['agentDetail.configure.skills.addMenu.upload.badge'])}
|
||||
iconClassName="i-ri-upload-cloud-2-line"
|
||||
@ -751,11 +762,15 @@ export function AgentSkills() {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<WorkspaceSkillSelector
|
||||
boundSkillIds={boundSkillIds}
|
||||
isBindingPending={!hasLoadedAgentSkillBindings || isReplacingAgentSkillBindings}
|
||||
onSelect={handleSelectWorkspaceSkill}
|
||||
/>
|
||||
enableSkill && (
|
||||
<WorkspaceSkillSelector
|
||||
boundSkillIds={boundSkillIds}
|
||||
isBindingPending={
|
||||
!hasLoadedAgentSkillBindings || isReplacingAgentSkillBindings
|
||||
}
|
||||
onSelect={handleSelectWorkspaceSkill}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@ -2366,7 +2366,6 @@ describe('SkillDetailPage', () => {
|
||||
})
|
||||
|
||||
it('updates and removes existing custom metadata from the manifest editor', async () => {
|
||||
const user = userEvent.setup()
|
||||
const content =
|
||||
'---\nname: github-actions-failure-debugging\ndescription: Guide for debugging failing GitHub Actions workflows.\nmetadata:\n display-name: Untitled skill\n owner: support\n---\n# GitHub Actions Failure Debugging\n'
|
||||
mocks.skillDetail = createSkillDetail({
|
||||
@ -2382,9 +2381,8 @@ describe('SkillDetailPage', () => {
|
||||
renderSkillDetailPage()
|
||||
|
||||
const ownerValue = await screen.findByRole('textbox', { name: 'owner value' })
|
||||
await user.clear(ownerValue)
|
||||
await user.type(ownerValue, 'success')
|
||||
await user.tab()
|
||||
fireEvent.change(ownerValue, { target: { value: 'success' } })
|
||||
fireEvent.blur(ownerValue)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
@ -2401,7 +2399,7 @@ describe('SkillDetailPage', () => {
|
||||
{ timeout: 2500 },
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Remove owner' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove owner' }))
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user