mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 23:01:11 +08:00
feat(agent-v2): wire workflow agent copy and roster save
This commit is contained in:
parent
673d84073b
commit
7ef6c13d0d
@ -298,6 +298,10 @@ class AgentComposerAgentResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
role: str | None = None
|
||||
icon_type: str | None = None
|
||||
icon: str | None = None
|
||||
icon_background: str | None = None
|
||||
scope: AgentScope
|
||||
status: AgentStatus
|
||||
active_config_snapshot_id: str | None = None
|
||||
|
||||
@ -1450,6 +1450,10 @@ class AgentComposerService:
|
||||
"id": agent.id,
|
||||
"name": agent.name,
|
||||
"description": agent.description,
|
||||
"role": agent.role,
|
||||
"icon_type": agent.icon_type,
|
||||
"icon": agent.icon,
|
||||
"icon_background": agent.icon_background,
|
||||
"scope": agent.scope.value,
|
||||
"status": agent.status.value,
|
||||
"active_config_snapshot_id": agent.active_config_snapshot_id,
|
||||
|
||||
@ -370,13 +370,27 @@ def test_serialize_workflow_state_changes_lock_and_save_options(monkeypatch: pyt
|
||||
node_id="node-1",
|
||||
node_job_config='{"workflow_prompt":"do work"}',
|
||||
)
|
||||
agent = Agent(id="agent-1", name="Analyst", description="", scope=AgentScope.ROSTER, status=AgentStatus.ACTIVE)
|
||||
agent = Agent(
|
||||
id="agent-1",
|
||||
name="Analyst",
|
||||
description="Clarifies tenders",
|
||||
role="Tender Analyst",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#F5F3FF",
|
||||
scope=AgentScope.ROSTER,
|
||||
status=AgentStatus.ACTIVE,
|
||||
)
|
||||
version = AgentConfigSnapshot(id="version-1", version=1, config_snapshot='{"prompt":{"system_prompt":"x"}}')
|
||||
monkeypatch.setattr(AgentComposerService, "calculate_impact", lambda **kwargs: {"workflow_node_count": 1})
|
||||
|
||||
state = AgentComposerService._serialize_workflow_state(binding=binding, agent=agent, version=version)
|
||||
|
||||
assert state["soul_lock"]["locked"] is True
|
||||
assert state["agent"]["role"] == "Tender Analyst"
|
||||
assert state["agent"]["icon_type"] == "emoji"
|
||||
assert state["agent"]["icon"] == "robot"
|
||||
assert state["agent"]["icon_background"] == "#F5F3FF"
|
||||
assert "save_as_new_version" in state["save_options"]
|
||||
assert state["agent_soul"]["app_features"] == {}
|
||||
# Stage 4 §10.1 (D-3): binding with no declared_outputs → response surfaces
|
||||
|
||||
@ -502,8 +502,12 @@ export type AgentConfigSnapshotSummaryResponse = {
|
||||
export type AgentComposerAgentResponse = {
|
||||
active_config_snapshot_id?: string | null
|
||||
description: string
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
id: string
|
||||
name: string
|
||||
role?: string | null
|
||||
scope: AgentScope
|
||||
status: AgentStatus
|
||||
}
|
||||
|
||||
@ -926,8 +926,12 @@ export const zAgentInviteOptionsResponse = z.object({
|
||||
export const zAgentComposerAgentResponse = z.object({
|
||||
active_config_snapshot_id: z.string().nullish(),
|
||||
description: z.string(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
role: z.string().nullish(),
|
||||
scope: zAgentScope,
|
||||
status: zAgentStatus,
|
||||
})
|
||||
|
||||
@ -1788,8 +1788,12 @@ export type AgentConfigSnapshotSummaryResponse = {
|
||||
export type AgentComposerAgentResponse = {
|
||||
active_config_snapshot_id?: string | null
|
||||
description: string
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
id: string
|
||||
name: string
|
||||
role?: string | null
|
||||
scope: AgentScope
|
||||
status: AgentStatus
|
||||
}
|
||||
|
||||
@ -2499,8 +2499,12 @@ export const zAgentStatus = z.enum(['active', 'archived'])
|
||||
export const zAgentComposerAgentResponse = z.object({
|
||||
active_config_snapshot_id: z.string().nullish(),
|
||||
description: z.string(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
role: z.string().nullish(),
|
||||
scope: zAgentScope,
|
||||
status: zAgentStatus,
|
||||
})
|
||||
|
||||
@ -14,6 +14,8 @@ const {
|
||||
mockInsertNodes,
|
||||
mockOrchestrateDrawerPanelProps,
|
||||
mockPromptEditorProps,
|
||||
mockCopyFromRosterMutate,
|
||||
mockCopyFromRosterState,
|
||||
mockCreateInlineAgentBinding,
|
||||
mockSetInputs,
|
||||
mockStoreState,
|
||||
@ -34,9 +36,14 @@ const {
|
||||
open: boolean
|
||||
}>,
|
||||
mockPromptEditorProps: [] as PromptEditorProps[],
|
||||
mockCopyFromRosterMutate: vi.fn(),
|
||||
mockCopyFromRosterState: {
|
||||
isPending: false,
|
||||
},
|
||||
mockCreateInlineAgentBinding: vi.fn(),
|
||||
mockSetInputs: vi.fn(),
|
||||
mockStoreState: {
|
||||
appId: 'app-1',
|
||||
openInlineAgentPanelNodeId: undefined as string | undefined,
|
||||
setOpenInlineAgentPanelNodeId: vi.fn(),
|
||||
},
|
||||
@ -80,6 +87,18 @@ vi.mock('@lexical/react/LexicalComposerContext', () => ({
|
||||
}],
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useMutation: () => ({
|
||||
isPending: mockCopyFromRosterState.isPending,
|
||||
mutate: mockCopyFromRosterMutate,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('lexical', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('lexical')>()
|
||||
|
||||
@ -165,6 +184,41 @@ vi.mock('../components/agent-orchestrate-drawer-panel', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../components/save-inline-agent-to-roster-dialog', () => ({
|
||||
SaveInlineAgentToRosterDialog: ({
|
||||
open,
|
||||
onSaved,
|
||||
}: {
|
||||
open: boolean
|
||||
onSaved: (binding: {
|
||||
agent_id?: string | null
|
||||
binding_type: 'inline_agent' | 'roster_agent'
|
||||
current_snapshot_id?: string | null
|
||||
id: string
|
||||
node_id: string
|
||||
workflow_id: string
|
||||
}) => void
|
||||
}) => open
|
||||
? (
|
||||
<div role="dialog" aria-label="save-inline-agent-to-roster">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSaved({
|
||||
id: 'binding-1',
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: 'saved-roster-agent',
|
||||
current_snapshot_id: 'saved-snapshot',
|
||||
workflow_id: 'workflow-1',
|
||||
node_id: 'agent-node',
|
||||
})}
|
||||
>
|
||||
Save inline agent to roster
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
|
||||
vi.mock('../../_base/hooks/use-available-var-list', () => ({
|
||||
default: () => ({
|
||||
availableVars: [{
|
||||
@ -215,7 +269,32 @@ describe('agent/panel', () => {
|
||||
vi.clearAllMocks()
|
||||
mockPromptEditorProps.length = 0
|
||||
mockOrchestrateDrawerPanelProps.length = 0
|
||||
mockStoreState.appId = 'app-1'
|
||||
mockStoreState.openInlineAgentPanelNodeId = undefined
|
||||
mockCopyFromRosterState.isPending = false
|
||||
mockCopyFromRosterMutate.mockImplementation((_variables, options?: {
|
||||
onSuccess?: (composerState: {
|
||||
binding: {
|
||||
agent_id: string
|
||||
binding_type: 'inline_agent'
|
||||
current_snapshot_id: string
|
||||
id: string
|
||||
node_id: string
|
||||
workflow_id: string
|
||||
}
|
||||
}) => void
|
||||
}) => {
|
||||
options?.onSuccess?.({
|
||||
binding: {
|
||||
id: 'binding-1',
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'inline-copy-agent',
|
||||
current_snapshot_id: 'inline-copy-snapshot',
|
||||
workflow_id: 'workflow-1',
|
||||
node_id: 'agent-node',
|
||||
},
|
||||
})
|
||||
})
|
||||
mockCreateInlineAgentBinding.mockImplementation(() => {})
|
||||
mockUseNodeCrud.mockImplementation((_id: string, data: AgentV2NodeType) => ({
|
||||
inputs: data,
|
||||
@ -304,6 +383,52 @@ describe('agent/panel', () => {
|
||||
expect(screen.queryByRole('dialog', { name: 'Nadia' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('copies a roster agent from the drawer into an inline agent for this node', () => {
|
||||
render(
|
||||
<AgentV2Panel
|
||||
id="agent-node"
|
||||
data={createData()}
|
||||
panelProps={panelProps}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.makeCopy' }))
|
||||
|
||||
expect(mockCopyFromRosterMutate).toHaveBeenCalledWith(
|
||||
{
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
node_id: 'agent-node',
|
||||
},
|
||||
body: {
|
||||
source_agent_id: 'agent-1',
|
||||
},
|
||||
},
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('agent-node')
|
||||
expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'agent-node',
|
||||
data: expect.objectContaining({
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'inline-copy-agent',
|
||||
current_snapshot_id: 'inline-copy-snapshot',
|
||||
},
|
||||
_openInlineAgentPanel: true,
|
||||
}),
|
||||
},
|
||||
expect.objectContaining({
|
||||
sync: true,
|
||||
notRefreshWhenSyncError: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('renders a required roster state when no roster agent is selected', () => {
|
||||
render(
|
||||
<AgentV2Panel
|
||||
@ -442,6 +567,45 @@ describe('agent/panel', () => {
|
||||
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens save-to-roster action from the inline drawer menu and rebinds to the saved roster agent', () => {
|
||||
mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
|
||||
render(
|
||||
<AgentV2Panel
|
||||
id="agent-node"
|
||||
data={createData({
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'inline-agent-1',
|
||||
current_snapshot_id: 'snapshot-1',
|
||||
},
|
||||
})}
|
||||
panelProps={panelProps}
|
||||
/>,
|
||||
)
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
|
||||
fireEvent.click(within(panel).getByRole('button', { name: 'workflow.nodes.agent.roster.more' }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'agentV2.roster.saveToRoster' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save inline agent to roster' }))
|
||||
|
||||
expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith(undefined)
|
||||
expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith(
|
||||
{
|
||||
id: 'agent-node',
|
||||
data: expect.objectContaining({
|
||||
agent_binding: {
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: 'saved-roster-agent',
|
||||
},
|
||||
}),
|
||||
},
|
||||
expect.objectContaining({
|
||||
sync: true,
|
||||
notRefreshWhenSyncError: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('does not show start from scratch for an existing inline agent binding', () => {
|
||||
render(
|
||||
<AgentV2Panel
|
||||
@ -482,7 +646,9 @@ describe('agent/panel', () => {
|
||||
|
||||
expect(mockUseWorkflowInlineAgentDetail).toHaveBeenCalledWith('agent-node', 'inline-agent-1')
|
||||
expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })).toBeInTheDocument()
|
||||
const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
|
||||
expect(panel).toBeInTheDocument()
|
||||
expect(within(panel).queryByRole('button', { name: 'workflow.nodes.agent.roster.more' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
|
||||
expect(mockOrchestrateDrawerPanelProps.at(-1)).toMatchObject({
|
||||
agentId: 'inline-agent-1',
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
import type { AgentComposerAgentResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { SaveInlineAgentToRosterDialog } from '../save-inline-agent-to-roster-dialog'
|
||||
|
||||
const mutationMock = vi.hoisted(() => ({
|
||||
isPending: false,
|
||||
mutate: vi.fn(),
|
||||
}))
|
||||
|
||||
const toastMock = vi.hoisted(() => ({
|
||||
success: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useMutation: () => ({
|
||||
isPending: mutationMock.isPending,
|
||||
mutate: mutationMock.mutate,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: toastMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon-picker', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
initialEmoji,
|
||||
onSelect,
|
||||
open,
|
||||
}: {
|
||||
initialEmoji?: { icon: string, background: string }
|
||||
onSelect: (payload: { type: 'emoji', icon: string, background: string }) => void
|
||||
open: boolean
|
||||
}) => open
|
||||
? (
|
||||
<div>
|
||||
<span>{`${initialEmoji?.icon}:${initialEmoji?.background}`}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect({ type: 'emoji', icon: '🧠', background: '#E0F2FE' })}
|
||||
>
|
||||
Select brain icon
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
byAppId: {
|
||||
workflows: {
|
||||
draft: {
|
||||
nodes: {
|
||||
byNodeId: {
|
||||
agentComposer: {
|
||||
saveToRoster: {
|
||||
post: {
|
||||
mutationOptions: vi.fn(() => ({})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const inlineAgent: AgentComposerAgentResponse = {
|
||||
active_config_snapshot_id: 'snapshot-1',
|
||||
description: 'Drafts tender clarifications.',
|
||||
icon: '🤖',
|
||||
icon_background: '#F5F3FF',
|
||||
icon_type: 'emoji',
|
||||
id: 'inline-agent-1',
|
||||
name: 'Inline Tender Agent',
|
||||
role: 'Tender Analyst',
|
||||
scope: 'workflow_only',
|
||||
status: 'active',
|
||||
}
|
||||
|
||||
const renderDialog = () => {
|
||||
const onOpenChange = vi.fn()
|
||||
const onSaved = vi.fn()
|
||||
|
||||
render(
|
||||
<SaveInlineAgentToRosterDialog
|
||||
appId="app-1"
|
||||
formKey={1}
|
||||
initialAgent={inlineAgent}
|
||||
nodeId="node-1"
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
onSaved={onSaved}
|
||||
/>,
|
||||
)
|
||||
|
||||
return { onOpenChange, onSaved }
|
||||
}
|
||||
|
||||
describe('SaveInlineAgentToRosterDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mutationMock.isPending = false
|
||||
})
|
||||
|
||||
it('initializes form fields from the inline agent metadata', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderDialog()
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'agentV2.roster.saveToRosterDialog.title' })
|
||||
expect(within(dialog).getByRole('textbox', { name: 'agentV2.roster.createForm.nameLabel' })).toHaveValue('Inline Tender Agent')
|
||||
expect(within(dialog).getByRole('textbox', { name: 'agentV2.roster.createForm.roleLabel' })).toHaveValue('Tender Analyst')
|
||||
expect(within(dialog).getByPlaceholderText('agentV2.roster.createForm.descriptionPlaceholder')).toHaveValue('Drafts tender clarifications.')
|
||||
|
||||
await user.click(within(dialog).getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mutationMock.mutate).toHaveBeenCalledWith({
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
node_id: 'node-1',
|
||||
},
|
||||
body: {
|
||||
variant: 'workflow',
|
||||
save_strategy: 'save_to_roster',
|
||||
new_agent_name: 'Inline Tender Agent',
|
||||
description: 'Drafts tender clarifications.',
|
||||
role: 'Tender Analyst',
|
||||
},
|
||||
}, expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}))
|
||||
const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1]
|
||||
expect(mutationOptions).not.toHaveProperty('onError')
|
||||
})
|
||||
|
||||
it('initializes the icon picker from the inline agent and submits changed icon fields', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderDialog()
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'agentV2.roster.saveToRosterDialog.title' })
|
||||
await user.click(within(dialog).getByRole('button', { name: 'agentV2.roster.saveToRosterForm.changeIcon' }))
|
||||
|
||||
expect(screen.getByText('🤖:#F5F3FF')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { hidden: true, name: 'Select brain icon' }))
|
||||
await user.click(within(dialog).getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(mutationMock.mutate).toHaveBeenCalledWith({
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
node_id: 'node-1',
|
||||
},
|
||||
body: {
|
||||
variant: 'workflow',
|
||||
save_strategy: 'save_to_roster',
|
||||
new_agent_name: 'Inline Tender Agent',
|
||||
description: 'Drafts tender clarifications.',
|
||||
role: 'Tender Analyst',
|
||||
icon_type: 'emoji',
|
||||
icon: '🧠',
|
||||
icon_background: '#E0F2FE',
|
||||
},
|
||||
}, expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}))
|
||||
})
|
||||
})
|
||||
@ -12,6 +12,12 @@ import {
|
||||
DrawerTitle,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import {
|
||||
Popover,
|
||||
@ -91,23 +97,30 @@ function AgentRosterDrawer({
|
||||
showAccessIcon = true,
|
||||
showConsoleLink = true,
|
||||
showDetailActions = true,
|
||||
isCopyPending = false,
|
||||
onMakeCopy,
|
||||
onSaveInlineToRoster,
|
||||
onClose,
|
||||
}: {
|
||||
agent: AgentRosterDisplayData
|
||||
children?: ReactNode
|
||||
isInlineSetup?: boolean
|
||||
isCopyPending?: boolean
|
||||
mode?: AgentRosterDrawerMode
|
||||
open: boolean
|
||||
portalContainerRef: RefObject<HTMLDivElement | null>
|
||||
showAccessIcon?: boolean
|
||||
showConsoleLink?: boolean
|
||||
showDetailActions?: boolean
|
||||
onMakeCopy?: () => void
|
||||
onSaveInlineToRoster?: () => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const isSetup = mode === 'setup'
|
||||
const title = isInlineSetup ? t(`${i18nPrefix}.roster.inlineSetup.name`, { ns: 'workflow' }) : agent.name
|
||||
const description = isSetup ? t(`${i18nPrefix}.roster.inlineSetup.description`, { ns: 'workflow' }) : agent.role
|
||||
const showInlineActions = isInlineSetup && !!onSaveInlineToRoster
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@ -160,16 +173,27 @@ function AgentRosterDrawer({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1 py-1">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(`${i18nPrefix}.roster.more`, { ns: 'workflow' })}
|
||||
className="flex size-6 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</button>
|
||||
<div className="flex h-3.5 items-start px-1">
|
||||
<div className="h-full w-px bg-divider-regular" />
|
||||
</div>
|
||||
{showInlineActions && (
|
||||
<>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t(`${i18nPrefix}.roster.more`, { ns: 'workflow' })}
|
||||
className="flex size-6 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-44 w-max">
|
||||
<DropdownMenuItem className="gap-2 whitespace-nowrap" onClick={onSaveInlineToRoster}>
|
||||
<span aria-hidden className="i-ri-inbox-archive-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span>{t('roster.saveToRoster', { ns: 'agentV2' })}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex h-3.5 items-start px-1">
|
||||
<div className="h-full w-px bg-divider-regular" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<DrawerCloseButton
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="size-6 rounded-md"
|
||||
@ -193,6 +217,8 @@ function AgentRosterDrawer({
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="min-w-0 flex-1 gap-1.5 px-3"
|
||||
loading={isCopyPending}
|
||||
onClick={onMakeCopy}
|
||||
>
|
||||
<span aria-hidden className="i-ri-file-copy-2-line size-4 shrink-0" />
|
||||
<span className="truncate">
|
||||
@ -222,6 +248,7 @@ export function AgentRosterField({
|
||||
agentId,
|
||||
canOpenPanel = true,
|
||||
isPanelOpen,
|
||||
isPanelCopyPending = false,
|
||||
isPending = false,
|
||||
isLoading = false,
|
||||
isInlineSetup = false,
|
||||
@ -230,13 +257,16 @@ export function AgentRosterField({
|
||||
showPanelDetailActions = true,
|
||||
portalContainerRef,
|
||||
onChange,
|
||||
onMakeCopy,
|
||||
onPanelOpenChange,
|
||||
onSaveInlineToRoster,
|
||||
onStartFromScratch,
|
||||
}: {
|
||||
agent?: AgentRosterDisplayData
|
||||
agentId?: string
|
||||
canOpenPanel?: boolean
|
||||
isPanelOpen?: boolean
|
||||
isPanelCopyPending?: boolean
|
||||
isLoading?: boolean
|
||||
isInlineSetup?: boolean
|
||||
isPending?: boolean
|
||||
@ -245,7 +275,9 @@ export function AgentRosterField({
|
||||
showPanelDetailActions?: boolean
|
||||
portalContainerRef: RefObject<HTMLDivElement | null>
|
||||
onChange: (agent: AgentRosterNodeData) => void
|
||||
onMakeCopy?: () => void
|
||||
onPanelOpenChange?: (open: boolean) => void
|
||||
onSaveInlineToRoster?: () => void
|
||||
onStartFromScratch?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
@ -358,6 +390,9 @@ export function AgentRosterField({
|
||||
portalContainerRef={portalContainerRef}
|
||||
showAccessIcon={!isInlineSetup}
|
||||
showDetailActions={showPanelDetailActions}
|
||||
isCopyPending={isPanelCopyPending}
|
||||
onMakeCopy={onMakeCopy}
|
||||
onSaveInlineToRoster={onSaveInlineToRoster}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
>
|
||||
{panelBody}
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentComposerAgentResponse, AgentComposerBindingResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { AgentFormValues, AgentIconSelection } from '@/features/agent-v2/roster/components/agent-form'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import { createAgentIconSelection, defaultAgentIcon } from '@/features/agent-v2/roster/components/agent-form'
|
||||
import { AgentFormFields } from '@/features/agent-v2/roster/components/agent-form-fields'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
type SaveInlineAgentToRosterDialogProps = {
|
||||
appId?: string
|
||||
formKey: number
|
||||
initialAgent?: AgentComposerAgentResponse | null
|
||||
nodeId: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSaved: (binding: AgentComposerBindingResponse) => void
|
||||
}
|
||||
|
||||
export function SaveInlineAgentToRosterDialog({
|
||||
appId,
|
||||
formKey,
|
||||
initialAgent,
|
||||
nodeId,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSaved,
|
||||
}: SaveInlineAgentToRosterDialogProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [name, setName] = useState(initialAgent?.name ?? '')
|
||||
const [description, setDescription] = useState(initialAgent?.description ?? '')
|
||||
const [role, setRole] = useState(initialAgent?.role ?? '')
|
||||
const [iconPickerOpen, setIconPickerOpen] = useState(false)
|
||||
const [isIconChanged, setIsIconChanged] = useState(false)
|
||||
const [agentIcon, setAgentIcon] = useState<AgentIconSelection>(() => initialAgent
|
||||
? createAgentIconSelection(initialAgent)
|
||||
: defaultAgentIcon)
|
||||
const saveToRosterMutation = useMutation(
|
||||
consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.saveToRoster.post.mutationOptions(),
|
||||
)
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (nextOpen) {
|
||||
setName(initialAgent?.name ?? '')
|
||||
setDescription(initialAgent?.description ?? '')
|
||||
setRole(initialAgent?.role ?? '')
|
||||
setIsIconChanged(false)
|
||||
setAgentIcon(initialAgent
|
||||
? createAgentIconSelection(initialAgent)
|
||||
: defaultAgentIcon)
|
||||
}
|
||||
else {
|
||||
setIconPickerOpen(false)
|
||||
}
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
const handleSubmit = (formValues: AgentFormValues) => {
|
||||
if (saveToRosterMutation.isPending)
|
||||
return
|
||||
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
const trimmedName = formValues.name?.trim() ?? ''
|
||||
const trimmedRole = formValues.role?.trim() ?? ''
|
||||
|
||||
saveToRosterMutation.mutate({
|
||||
params: {
|
||||
app_id: appId,
|
||||
node_id: nodeId,
|
||||
},
|
||||
body: {
|
||||
variant: 'workflow',
|
||||
save_strategy: 'save_to_roster',
|
||||
new_agent_name: trimmedName,
|
||||
description: formValues.description?.trim() ?? '',
|
||||
role: trimmedRole,
|
||||
...(isIconChanged
|
||||
? {
|
||||
icon_type: agentIcon.type,
|
||||
icon: agentIcon.type === 'image' ? agentIcon.fileId : agentIcon.icon,
|
||||
icon_background: agentIcon.type === 'emoji' ? agentIcon.background : undefined,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}, {
|
||||
onSuccess: (composerState) => {
|
||||
const binding = composerState.binding
|
||||
if (binding?.binding_type !== 'roster_agent' || !binding.agent_id)
|
||||
return
|
||||
|
||||
toast.success(t('roster.saveToRosterSuccess'))
|
||||
onSaved(binding)
|
||||
handleOpenChange(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange} disablePointerDismissal>
|
||||
<DialogContent className="flex max-h-[calc(100dvh-2rem)] w-130 flex-col overflow-hidden! p-0!">
|
||||
<DialogCloseButton />
|
||||
<div className="shrink-0 pt-6 pr-14 pb-3 pl-6">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t('roster.saveToRosterDialog.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{t('roster.saveToRosterDialog.description')}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<Form<AgentFormValues>
|
||||
key={formKey}
|
||||
className="min-h-0 flex-1"
|
||||
onFormSubmit={handleSubmit}
|
||||
>
|
||||
<AgentFormFields
|
||||
description={description}
|
||||
icon={agentIcon}
|
||||
iconAriaLabel={t('roster.saveToRosterForm.changeIcon')}
|
||||
name={name}
|
||||
role={role}
|
||||
onDescriptionChange={setDescription}
|
||||
onIconClick={() => setIconPickerOpen(true)}
|
||||
onNameChange={setName}
|
||||
onRoleChange={setRole}
|
||||
/>
|
||||
<div className="flex shrink-0 justify-end gap-2 px-6 pt-5 pb-6">
|
||||
<Button type="button" className="min-w-18" onClick={() => handleOpenChange(false)} disabled={saveToRosterMutation.isPending}>
|
||||
{tCommon('operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="min-w-18"
|
||||
loading={saveToRosterMutation.isPending}
|
||||
>
|
||||
{tCommon('operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AppIconPicker
|
||||
open={iconPickerOpen}
|
||||
initialEmoji={agentIcon.type === 'emoji'
|
||||
? { icon: agentIcon.icon, background: agentIcon.background }
|
||||
: undefined}
|
||||
onOpenChange={setIconPickerOpen}
|
||||
onSelect={(icon) => {
|
||||
setAgentIcon(icon)
|
||||
setIsIconChanged(true)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import type { AgentComposerBindingResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { AgentRosterNodeData } from '../../block-selector/types'
|
||||
import type { NodePanelProps } from '../../types'
|
||||
import type { AgentV2NodeType } from './types'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -10,12 +12,14 @@ import {
|
||||
} from '@/app/components/base/prompt-editor/plugins/agent-output-block/utils'
|
||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { AgentAdvancedSettings } from './components/agent-advanced-settings'
|
||||
import { AgentOrchestrateDrawerPanel } from './components/agent-orchestrate-drawer-panel'
|
||||
import { AgentOutputVariables } from './components/agent-output-variables'
|
||||
import { AgentRosterField } from './components/agent-roster-field'
|
||||
import { AgentTaskField } from './components/agent-task-field'
|
||||
import { SaveInlineAgentToRosterDialog } from './components/save-inline-agent-to-roster-dialog'
|
||||
import { useAgentRosterDetail, useCreateInlineAgentBinding, useWorkflowInlineAgentDetail } from './hooks'
|
||||
import { getAgentV2DeclaredOutputs } from './output-variables'
|
||||
import { hasValidInlineAgentBinding } from './types'
|
||||
@ -30,6 +34,8 @@ export function AgentV2Panel({
|
||||
const promptOutputNamesRef = useRef(extractAgentOutputNames(inputs.agent_task || ''))
|
||||
const [isRosterAgentPanelOpen, setIsRosterAgentPanelOpen] = useState(false)
|
||||
const [isInlineAgentPanelOpenedFromTrigger, setIsInlineAgentPanelOpenedFromTrigger] = useState(false)
|
||||
const [isSaveToRosterDialogOpen, setIsSaveToRosterDialogOpen] = useState(false)
|
||||
const [saveToRosterSessionKey, setSaveToRosterSessionKey] = useState(0)
|
||||
const { handleNodeDataUpdate, handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
const openInlineAgentPanelNodeId = useStore(state => state.openInlineAgentPanelNodeId)
|
||||
const setOpenInlineAgentPanelNodeId = useStore(state => state.setOpenInlineAgentPanelNodeId)
|
||||
@ -45,10 +51,17 @@ export function AgentV2Panel({
|
||||
const inlineAgentQuery = useWorkflowInlineAgentDetail(id, inlineAgentId)
|
||||
const { createInlineAgentBinding, isCreatingInlineAgent } = useCreateInlineAgentBinding()
|
||||
const inlineAgent = inlineAgentQuery.data?.agent
|
||||
const {
|
||||
isPending: isCopyingFromRoster,
|
||||
mutate: copyFromRoster,
|
||||
} = useMutation(
|
||||
consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.copyFromRoster.post.mutationOptions(),
|
||||
)
|
||||
const isAgentPanelOpen = isInlineAgentReady || isInlineAgentPending ? isInlineAgentPanelOpen : isRosterAgentPanelOpen
|
||||
const isInlineAgentLoading = isInlineAgentPending || (isInlineAgentReady && !inlineAgent)
|
||||
const isAgentBindingPending = isInlineAgentPending || isCreatingInlineAgent
|
||||
const canStartFromScratch = inputs.agent_binding?.binding_type !== 'inline_agent'
|
||||
const canSaveInlineToRoster = isInlineAgentReady && !!inlineAgent
|
||||
const displayedAgent = rosterAgentQuery.data ?? (isInlineAgentPending || isInlineAgentReady
|
||||
? {
|
||||
id: inlineAgentId ?? id,
|
||||
@ -113,6 +126,91 @@ export function AgentV2Panel({
|
||||
)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, inputs, setOpenInlineAgentPanelNodeId])
|
||||
|
||||
const handleMakeRosterCopy = useCallback(() => {
|
||||
if (!appId || !rosterAgentId || isCopyingFromRoster)
|
||||
return
|
||||
|
||||
copyFromRoster({
|
||||
params: {
|
||||
app_id: appId,
|
||||
node_id: id,
|
||||
},
|
||||
body: {
|
||||
source_agent_id: rosterAgentId,
|
||||
},
|
||||
}, {
|
||||
onSuccess: (composerState) => {
|
||||
const binding = composerState.binding
|
||||
if (
|
||||
binding?.binding_type !== 'inline_agent'
|
||||
|| !binding.agent_id
|
||||
|| !binding.current_snapshot_id
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsRosterAgentPanelOpen(false)
|
||||
setIsInlineAgentPanelOpenedFromTrigger(true)
|
||||
setOpenInlineAgentPanelNodeId(id)
|
||||
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster
|
||||
draft.agent_binding = {
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: binding.agent_id,
|
||||
current_snapshot_id: binding.current_snapshot_id,
|
||||
}
|
||||
draft._openInlineAgentPanel = true
|
||||
})
|
||||
inputsRef.current = newInputs
|
||||
handleNodeDataUpdateWithSyncDraft(
|
||||
{
|
||||
id,
|
||||
data: newInputs,
|
||||
},
|
||||
{
|
||||
sync: true,
|
||||
notRefreshWhenSyncError: true,
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
}, [appId, copyFromRoster, handleNodeDataUpdateWithSyncDraft, id, isCopyingFromRoster, rosterAgentId, setOpenInlineAgentPanelNodeId])
|
||||
|
||||
const handleSaveInlineToRosterOpen = useCallback(() => {
|
||||
setSaveToRosterSessionKey(key => key + 1)
|
||||
setIsSaveToRosterDialogOpen(true)
|
||||
}, [])
|
||||
|
||||
const handleInlineSavedToRoster = useCallback((binding: AgentComposerBindingResponse) => {
|
||||
if (binding.binding_type !== 'roster_agent' || !binding.agent_id)
|
||||
return
|
||||
|
||||
setOpenInlineAgentPanelNodeId(undefined)
|
||||
setIsInlineAgentPanelOpenedFromTrigger(false)
|
||||
setIsRosterAgentPanelOpen(true)
|
||||
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster
|
||||
delete draft._openInlineAgentPanel
|
||||
draft.agent_binding = {
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: binding.agent_id!,
|
||||
}
|
||||
})
|
||||
inputsRef.current = newInputs
|
||||
handleNodeDataUpdateWithSyncDraft(
|
||||
{
|
||||
id,
|
||||
data: newInputs,
|
||||
},
|
||||
{
|
||||
sync: true,
|
||||
notRefreshWhenSyncError: true,
|
||||
},
|
||||
)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, setOpenInlineAgentPanelNodeId])
|
||||
|
||||
const handleStartFromScratch = useCallback(() => {
|
||||
setIsRosterAgentPanelOpen(false)
|
||||
setIsInlineAgentPanelOpenedFromTrigger(false)
|
||||
@ -219,6 +317,7 @@ export function AgentV2Panel({
|
||||
canOpenPanel
|
||||
isInlineSetup={isInlineAgentReady || isInlineAgentPending}
|
||||
isLoading={isInlineAgentLoading}
|
||||
isPanelCopyPending={isCopyingFromRoster}
|
||||
isPanelOpen={isAgentPanelOpen}
|
||||
isPending={isAgentBindingPending}
|
||||
panelBody={isAgentPanelOpen && displayedAgent
|
||||
@ -237,9 +336,21 @@ export function AgentV2Panel({
|
||||
portalContainerRef={drawerPortalContainerRef}
|
||||
showPanelDetailActions={!isInlineAgentReady && !isInlineAgentPending}
|
||||
onChange={handleRosterChange}
|
||||
onMakeCopy={rosterAgentId ? handleMakeRosterCopy : undefined}
|
||||
onPanelOpenChange={handleAgentPanelOpenChange}
|
||||
onSaveInlineToRoster={canSaveInlineToRoster ? handleSaveInlineToRosterOpen : undefined}
|
||||
onStartFromScratch={canStartFromScratch ? handleStartFromScratch : undefined}
|
||||
/>
|
||||
<SaveInlineAgentToRosterDialog
|
||||
key={saveToRosterSessionKey}
|
||||
appId={appId}
|
||||
formKey={saveToRosterSessionKey}
|
||||
initialAgent={inlineAgent}
|
||||
nodeId={id}
|
||||
open={isSaveToRosterDialogOpen}
|
||||
onOpenChange={setIsSaveToRosterDialogOpen}
|
||||
onSaved={handleInlineSavedToRoster}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-disabled={isInlineAgentPending}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { AgentAppPartial } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
|
||||
export type AgentFormValues = {
|
||||
@ -19,7 +18,13 @@ export const defaultAgentIcon = {
|
||||
background: '#F5F3FF',
|
||||
} satisfies AppIconSelection
|
||||
|
||||
export const createAgentIconSelection = (agent: AgentAppPartial): AgentIconSelection => {
|
||||
type AgentIconSource = {
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
}
|
||||
|
||||
export const createAgentIconSelection = (agent: AgentIconSource): AgentIconSelection => {
|
||||
if (agent.icon_type === 'image' && agent.icon) {
|
||||
return {
|
||||
type: 'image',
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "البدء من الصفر",
|
||||
"roster.references.label": "سير العمل الذي يستخدم {{name}}",
|
||||
"roster.references.trigger": "سير العمل الذي يستخدم {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "البحث عن الوكلاء",
|
||||
"roster.searchPlaceholder": "ابحث عن الوكلاء بالاسم…",
|
||||
"roster.sources.agent_app": "تطبيق الوكيل",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Von Grund auf starten",
|
||||
"roster.references.label": "Workflows, die {{name}} verwenden",
|
||||
"roster.references.trigger": "Workflows, die {{name}} verwenden",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Agenten suchen",
|
||||
"roster.searchPlaceholder": "Agenten nach Namen suchen…",
|
||||
"roster.sources.agent_app": "Agent-App",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Start from Scratch",
|
||||
"roster.references.label": "Workflows using {{name}}",
|
||||
"roster.references.trigger": "Workflows using {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Search agents",
|
||||
"roster.searchPlaceholder": "Search agents by name…",
|
||||
"roster.sources.agent_app": "Agent app",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Empezar desde cero",
|
||||
"roster.references.label": "Flujos de trabajo que usan {{name}}",
|
||||
"roster.references.trigger": "Flujos de trabajo que usan {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Buscar agentes",
|
||||
"roster.searchPlaceholder": "Buscar agentes por nombre…",
|
||||
"roster.sources.agent_app": "Aplicación de agente",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "از صفر شروع کنید",
|
||||
"roster.references.label": "گردشهای کار با استفاده از {{name}}",
|
||||
"roster.references.trigger": "گردشهای کار با استفاده از {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "جستجوی عوامل",
|
||||
"roster.searchPlaceholder": "جستجوی عوامل بر اساس نام…",
|
||||
"roster.sources.agent_app": "برنامه عامل",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Partir de zéro",
|
||||
"roster.references.label": "Workflows utilisant {{name}}",
|
||||
"roster.references.trigger": "Workflows utilisant {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Rechercher des agents",
|
||||
"roster.searchPlaceholder": "Rechercher des agents par nom…",
|
||||
"roster.sources.agent_app": "Application agent",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "शुरुआत से आरंभ करें",
|
||||
"roster.references.label": "{{name}} का उपयोग करने वाले वर्कफ़्लो",
|
||||
"roster.references.trigger": "{{name}} का उपयोग करने वाले वर्कफ़्लो",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "एजेंट खोजें",
|
||||
"roster.searchPlaceholder": "नाम से एजेंट खोजें…",
|
||||
"roster.sources.agent_app": "एजेंट ऐप",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Mulai dari Awal",
|
||||
"roster.references.label": "Alur kerja yang menggunakan {{name}}",
|
||||
"roster.references.trigger": "Alur kerja yang menggunakan {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Cari agen",
|
||||
"roster.searchPlaceholder": "Cari agen berdasarkan nama…",
|
||||
"roster.sources.agent_app": "Aplikasi agen",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Inizia da zero",
|
||||
"roster.references.label": "Workflow che usano {{name}}",
|
||||
"roster.references.trigger": "Workflow che usano {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Cerca agenti",
|
||||
"roster.searchPlaceholder": "Cerca agenti per nome…",
|
||||
"roster.sources.agent_app": "App agente",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "ゼロから始める",
|
||||
"roster.references.label": "{{name}} を使用するワークフロー",
|
||||
"roster.references.trigger": "{{name}} を使用するワークフロー",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "エージェントを検索",
|
||||
"roster.searchPlaceholder": "エージェントを名前で検索…",
|
||||
"roster.sources.agent_app": "エージェントアプリ",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "처음부터 시작",
|
||||
"roster.references.label": "{{name}}을(를) 사용하는 워크플로",
|
||||
"roster.references.trigger": "{{name}}을(를) 사용하는 워크플로",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "에이전트 검색",
|
||||
"roster.searchPlaceholder": "이름으로 에이전트 검색…",
|
||||
"roster.sources.agent_app": "에이전트 앱",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Vanaf nul beginnen",
|
||||
"roster.references.label": "Workflows die {{name}} gebruiken",
|
||||
"roster.references.trigger": "Workflows die {{name}} gebruiken",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Agents zoeken",
|
||||
"roster.searchPlaceholder": "Agents op naam zoeken…",
|
||||
"roster.sources.agent_app": "Agent-app",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Zacznij od zera",
|
||||
"roster.references.label": "Workflow używające {{name}}",
|
||||
"roster.references.trigger": "Workflow używające {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Szukaj agentów",
|
||||
"roster.searchPlaceholder": "Wyszukaj agentów po nazwie…",
|
||||
"roster.sources.agent_app": "Aplikacja agenta",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Começar do zero",
|
||||
"roster.references.label": "Workflows que usam {{name}}",
|
||||
"roster.references.trigger": "Workflows que usam {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Pesquisar agentes",
|
||||
"roster.searchPlaceholder": "Pesquisar agentes pelo nome…",
|
||||
"roster.sources.agent_app": "Aplicativo agente",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Începeți de la zero",
|
||||
"roster.references.label": "Workflow-uri care folosesc {{name}}",
|
||||
"roster.references.trigger": "Workflow-uri care folosesc {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Căutați agenți",
|
||||
"roster.searchPlaceholder": "Căutați agenți după nume…",
|
||||
"roster.sources.agent_app": "Aplicație agent",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Начать с нуля",
|
||||
"roster.references.label": "Рабочие процессы, использующие {{name}}",
|
||||
"roster.references.trigger": "Рабочие процессы, использующие {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Поиск агентов",
|
||||
"roster.searchPlaceholder": "Поиск агентов по имени…",
|
||||
"roster.sources.agent_app": "Агентское приложение",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Začni iz nič",
|
||||
"roster.references.label": "Poteki dela, ki uporabljajo {{name}}",
|
||||
"roster.references.trigger": "Poteki dela, ki uporabljajo {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Iskanje agentov",
|
||||
"roster.searchPlaceholder": "Iskanje agentov po imenu…",
|
||||
"roster.sources.agent_app": "Aplikacija agenta",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "เริ่มต้นจากศูนย์",
|
||||
"roster.references.label": "เวิร์กโฟลว์ที่ใช้ {{name}}",
|
||||
"roster.references.trigger": "เวิร์กโฟลว์ที่ใช้ {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "ค้นหาตัวแทน",
|
||||
"roster.searchPlaceholder": "ค้นหาตัวแทนตามชื่อ…",
|
||||
"roster.sources.agent_app": "แอปตัวแทน",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Sıfırdan Başla",
|
||||
"roster.references.label": "{{name}} kullanan iş akışları",
|
||||
"roster.references.trigger": "{{name}} kullanan iş akışları",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Ajanları ara",
|
||||
"roster.searchPlaceholder": "Ajanları ada göre ara…",
|
||||
"roster.sources.agent_app": "Ajan uygulaması",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Почати з нуля",
|
||||
"roster.references.label": "Робочі процеси, що використовують {{name}}",
|
||||
"roster.references.trigger": "Робочі процеси, що використовують {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Пошук агентів",
|
||||
"roster.searchPlaceholder": "Пошук агентів за ім'ям…",
|
||||
"roster.sources.agent_app": "Агентний застосунок",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "Bắt đầu từ đầu",
|
||||
"roster.references.label": "Quy trình làm việc đang dùng {{name}}",
|
||||
"roster.references.trigger": "Quy trình làm việc đang dùng {{name}}",
|
||||
"roster.saveToRoster": "Save to Agent Console",
|
||||
"roster.saveToRosterDialog.description": "Save this inline setup as a reusable roster agent.",
|
||||
"roster.saveToRosterDialog.title": "Save to Agent Console",
|
||||
"roster.saveToRosterForm.changeIcon": "Change roster agent icon",
|
||||
"roster.saveToRosterSuccess": "Agent saved to roster.",
|
||||
"roster.searchLabel": "Tìm kiếm tác nhân",
|
||||
"roster.searchPlaceholder": "Tìm kiếm tác nhân theo tên…",
|
||||
"roster.sources.agent_app": "Ứng dụng tác nhân",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "从空白开始",
|
||||
"roster.references.label": "使用 {{name}} 的工作流",
|
||||
"roster.references.trigger": "使用 {{name}} 的工作流",
|
||||
"roster.saveToRoster": "保存到智能体控制台",
|
||||
"roster.saveToRosterDialog.description": "将此内联配置保存为可复用的 Roster 智能体。",
|
||||
"roster.saveToRosterDialog.title": "保存到智能体控制台",
|
||||
"roster.saveToRosterForm.changeIcon": "更换 Roster 智能体图标",
|
||||
"roster.saveToRosterSuccess": "智能体已保存到 Roster。",
|
||||
"roster.searchLabel": "搜索智能体",
|
||||
"roster.searchPlaceholder": "按名称搜索智能体…",
|
||||
"roster.sources.agent_app": "智能体应用",
|
||||
|
||||
@ -381,6 +381,11 @@
|
||||
"roster.nodeSelector.startFromScratch": "從空白開始",
|
||||
"roster.references.label": "使用 {{name}} 的工作流程",
|
||||
"roster.references.trigger": "使用 {{name}} 的工作流程",
|
||||
"roster.saveToRoster": "儲存到智慧體控制台",
|
||||
"roster.saveToRosterDialog.description": "將此內聯設定儲存為可重用的 Roster 智慧體。",
|
||||
"roster.saveToRosterDialog.title": "儲存到智慧體控制台",
|
||||
"roster.saveToRosterForm.changeIcon": "更換 Roster 智慧體圖示",
|
||||
"roster.saveToRosterSuccess": "智慧體已儲存到 Roster。",
|
||||
"roster.searchLabel": "搜尋智能體",
|
||||
"roster.searchPlaceholder": "依名稱搜尋智能體…",
|
||||
"roster.sources.agent_app": "智能體應用",
|
||||
|
||||
@ -46,6 +46,7 @@ const createApiBasedExtension = (overrides: Partial<ApiBasedExtensionResponse> =
|
||||
|
||||
type AgentMutationResponse = Parameters<NonNullable<ReturnType<typeof ConsoleQuery.agent.post.mutationOptions>['onSuccess']>>[0]
|
||||
type AgentComposerMutationResponse = Parameters<NonNullable<ReturnType<typeof ConsoleQuery.agent.byAgentId.composer.put.mutationOptions>['onSuccess']>>[0]
|
||||
type WorkflowAgentComposerMutationResponse = Parameters<NonNullable<ReturnType<typeof ConsoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.saveToRoster.post.mutationOptions>['onSuccess']>>[0]
|
||||
|
||||
const createAgent = (overrides: Partial<AgentMutationResponse> = {}): AgentMutationResponse => ({
|
||||
...overrides,
|
||||
@ -81,6 +82,40 @@ const createComposerState = (overrides: Partial<AgentComposerMutationResponse> =
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createWorkflowComposerState = (overrides: Partial<WorkflowAgentComposerMutationResponse> = {}): WorkflowAgentComposerMutationResponse => ({
|
||||
agent: {
|
||||
active_config_snapshot_id: 'snapshot-1',
|
||||
description: 'Agent description',
|
||||
id: 'agent-1',
|
||||
name: 'Agent',
|
||||
scope: 'roster',
|
||||
status: 'active',
|
||||
},
|
||||
agent_soul: {
|
||||
schema_version: 1,
|
||||
},
|
||||
binding: {
|
||||
agent_id: 'agent-1',
|
||||
binding_type: 'roster_agent',
|
||||
current_snapshot_id: 'snapshot-1',
|
||||
id: 'binding-1',
|
||||
node_id: 'node-1',
|
||||
workflow_id: 'workflow-1',
|
||||
},
|
||||
node_job: {
|
||||
mode: 'tell_agent_what_to_do',
|
||||
schema_version: 1,
|
||||
workflow_prompt: '',
|
||||
},
|
||||
save_options: ['node_job_only', 'save_as_new_agent'],
|
||||
soul_lock: {
|
||||
can_unlock: false,
|
||||
locked: true,
|
||||
},
|
||||
variant: 'workflow',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Scenario: base URL selection and warnings.
|
||||
describe('getBaseURL', () => {
|
||||
beforeEach(() => {
|
||||
@ -222,6 +257,104 @@ describe('consoleQuery agent mutation defaults', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should cache workflow composer state after copying a roster agent into an inline agent', async () => {
|
||||
const consoleQuery = await loadConsoleQuery()
|
||||
const queryClient = new QueryClient()
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
const composerState = createWorkflowComposerState({
|
||||
binding: {
|
||||
agent_id: 'inline-agent-1',
|
||||
binding_type: 'inline_agent',
|
||||
current_snapshot_id: 'inline-snapshot-1',
|
||||
id: 'binding-1',
|
||||
node_id: 'node-1',
|
||||
workflow_id: 'workflow-1',
|
||||
},
|
||||
})
|
||||
|
||||
const mutationOptions = consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.copyFromRoster.post.mutationOptions()
|
||||
await mutationOptions.onSuccess?.(
|
||||
composerState,
|
||||
{
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
node_id: 'node-1',
|
||||
},
|
||||
body: {
|
||||
source_agent_id: 'roster-agent-1',
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
createMutationContext(queryClient),
|
||||
)
|
||||
|
||||
expect(queryClient.getQueryData(consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.get.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
node_id: 'node-1',
|
||||
},
|
||||
},
|
||||
}))).toEqual(composerState)
|
||||
expect(invalidateQueries).not.toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).not.toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.inviteOptions.get.key(),
|
||||
})
|
||||
})
|
||||
|
||||
it('should cache workflow composer state and invalidate roster lists after saving inline agent to roster', async () => {
|
||||
const consoleQuery = await loadConsoleQuery()
|
||||
const queryClient = new QueryClient()
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
const composerState = createWorkflowComposerState()
|
||||
|
||||
const mutationOptions = consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.saveToRoster.post.mutationOptions()
|
||||
await mutationOptions.onSuccess?.(
|
||||
composerState,
|
||||
{
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
node_id: 'node-1',
|
||||
},
|
||||
body: {
|
||||
variant: 'workflow',
|
||||
save_strategy: 'save_to_roster',
|
||||
new_agent_name: 'Saved Agent',
|
||||
description: 'Agent description',
|
||||
role: 'Assistant',
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
createMutationContext(queryClient),
|
||||
)
|
||||
|
||||
expect(queryClient.getQueryData(consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.get.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
app_id: 'app-1',
|
||||
node_id: 'node-1',
|
||||
},
|
||||
},
|
||||
}))).toEqual(composerState)
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.inviteOptions.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.byAgentId.get.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it('should invalidate invite option lists after updating an agent', async () => {
|
||||
const consoleQuery = await loadConsoleQuery()
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
@ -347,6 +347,73 @@ export const consoleClient: JsonifiedClient<ContractRouterClient<typeof consoleR
|
||||
export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQueryUtils(consoleClient, {
|
||||
path: ['console'],
|
||||
experimental_defaults: {
|
||||
apps: {
|
||||
byAppId: {
|
||||
workflows: {
|
||||
draft: {
|
||||
nodes: {
|
||||
byNodeId: {
|
||||
agentComposer: {
|
||||
copyFromRoster: {
|
||||
post: {
|
||||
mutationOptions: {
|
||||
onSuccess: (composerState, variables, _onMutateResult, context) => {
|
||||
context.client.setQueryData(
|
||||
consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.get.queryKey({
|
||||
input: {
|
||||
params: variables.params,
|
||||
},
|
||||
}),
|
||||
composerState,
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saveToRoster: {
|
||||
post: {
|
||||
mutationOptions: {
|
||||
onSuccess: (composerState, variables, _onMutateResult, context) => {
|
||||
context.client.setQueryData(
|
||||
consoleQuery.apps.byAppId.workflows.draft.nodes.byNodeId.agentComposer.get.queryKey({
|
||||
input: {
|
||||
params: variables.params,
|
||||
},
|
||||
}),
|
||||
composerState,
|
||||
)
|
||||
context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.get.key(),
|
||||
})
|
||||
context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.inviteOptions.get.key(),
|
||||
})
|
||||
|
||||
const agentId = composerState.binding?.binding_type === 'roster_agent'
|
||||
? composerState.binding.agent_id
|
||||
: undefined
|
||||
if (agentId) {
|
||||
context.client.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.get.queryKey({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
post: {
|
||||
mutationOptions: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user