From 08a2ceaddf92583b6f121ae547a65d32317d191c Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Mon, 17 Nov 2025 16:06:58 +0200 Subject: [PATCH] feat(core): Support attaching tools to custom builder agents (no-changelog) (#21550) --- .../frontend/@n8n/i18n/src/locales/en.json | 1 + .../features/ai/chatHub/ChatAgentsView.vue | 38 +- .../src/features/ai/chatHub/ChatView.vue | 64 ++- .../chatHub/components/AgentEditorModal.vue | 119 +++--- .../ai/chatHub/components/ChatPrompt.vue | 104 +---- .../ai/chatHub/components/ToolsSelector.vue | 403 +++--------------- .../chatHub/components/ToolsSelectorModal.vue | 378 ++++++++++++++++ .../src/features/ai/chatHub/constants.ts | 3 + .../features/ai/chatHub/module.descriptor.ts | 34 +- 9 files changed, 603 insertions(+), 541 deletions(-) create mode 100644 packages/frontend/editor-ui/src/features/ai/chatHub/components/ToolsSelectorModal.vue diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 2ddb23a5ba1..8c034f61430 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -308,6 +308,7 @@ "chatHub.agent.editor.systemPrompt.label": "System Prompt", "chatHub.agent.editor.systemPrompt.placeholder": "Enter system prompt", "chatHub.agent.editor.model.label": "Model", + "chatHub.agent.editor.tools.label": "Tools", "chatHub.agent.editor.loading": "Loading agent...", "chatHub.agent.editor.saving": "Saving...", "chatHub.agent.editor.save": "Save", diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue index 1fb7c6defd7..7b8e331c544 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatAgentsView.vue @@ -14,7 +14,6 @@ import { } from '@n8n/design-system'; import { computed, ref, watch } from 'vue'; import { useUIStore } from '@/app/stores/ui.store'; -import AgentEditorModal from '@/features/ai/chatHub/components/AgentEditorModal.vue'; import ChatAgentCard from '@/features/ai/chatHub/components/ChatAgentCard.vue'; import { useChatCredentials } from '@/features/ai/chatHub/composables/useChatCredentials'; import { useUsersStore } from '@/features/settings/users/users.store'; @@ -23,7 +22,7 @@ import { filterAndSortAgents, stringifyModel } from '@/features/ai/chatHub/chat. import type { ChatAgentFilter } from '@/features/ai/chatHub/chat.types'; import { useChatHubSidebarState } from '@/features/ai/chatHub/composables/useChatHubSidebarState'; import { useMediaQuery } from '@vueuse/core'; -import { MOBILE_MEDIA_QUERY } from '@/features/ai/chatHub/constants'; +import { AGENT_EDITOR_MODAL_KEY, MOBILE_MEDIA_QUERY } from '@/features/ai/chatHub/constants'; import { useRouter } from 'vue-router'; const chatStore = useChatStore(); @@ -35,8 +34,6 @@ const sidebar = useChatHubSidebarState(); const router = useRouter(); const isMobileDevice = useMediaQuery(MOBILE_MEDIA_QUERY); -const editingAgentId = ref(undefined); - const agentFilter = ref({ search: '', provider: '', @@ -65,8 +62,12 @@ const sortOptions = [ function handleCreateAgent() { chatStore.currentEditingAgent = null; - editingAgentId.value = undefined; - uiStore.openModal('agentEditor'); + uiStore.openModalWithData({ + name: AGENT_EDITOR_MODAL_KEY, + data: { + credentials: credentialsByProvider, + }, + }); } async function handleEditAgent(model: ChatHubConversationModel) { @@ -85,22 +86,19 @@ async function handleEditAgent(model: ChatHubConversationModel) { if (model.provider === 'custom-agent') { try { await chatStore.fetchCustomAgent(model.agentId); - editingAgentId.value = model.agentId; - uiStore.openModal('agentEditor'); + uiStore.openModalWithData({ + name: AGENT_EDITOR_MODAL_KEY, + data: { + agentId: model.agentId, + credentials: credentialsByProvider, + }, + }); } catch (error) { toast.showError(error, 'Failed to load agent'); } } } -function handleCloseAgentEditor() { - editingAgentId.value = undefined; -} - -async function handleAgentCreatedOrUpdated() { - editingAgentId.value = undefined; -} - async function handleDeleteAgent(agentId: string) { const confirmed = await message.confirm( 'Are you sure you want to delete this agent?', @@ -201,14 +199,6 @@ watch( /> - - const currentConversationTitle = computed(() => currentConversation.value?.title); const readyToShowMessages = computed(() => chatStore.agentsReady); +// TODO: This also depends on the model, not all base LLM models support tools. +const canSelectTools = computed( + () => + selectedModel.value?.model.provider !== 'custom-agent' && + selectedModel.value?.model.provider !== 'n8n', +); + const { arrivedState, measure } = useScroll(scrollContainerRef, { throttle: 100, offset: { bottom: 100 }, @@ -199,8 +205,6 @@ const isMissingSelectedCredential = computed(() => !credentialsForSelectedProvid const editingMessageId = ref(); const didSubmitInCurrentSession = ref(false); -const editingAgentId = ref(undefined); -const isToolsSelectorOpen = ref(false); function scrollToBottom(smooth: boolean) { scrollContainerRef.value?.scrollTo({ @@ -316,7 +320,7 @@ function onSubmit(message: string) { message, selectedModel.value.model, credentialsForSelectedProvider.value, - selectedTools.value, + canSelectTools.value ? selectedTools.value : [], ); inputRef.value?.setText(''); @@ -405,12 +409,7 @@ function handleConfigureModel() { headerRef.value?.openModelSelector(); } -function handleConfigureTools() { - isToolsSelectorOpen.value = true; - uiStore.openModal('toolsSelector'); -} - -async function onUpdateTools(newTools: INode[]) { +async function handleUpdateTools(newTools: INode[]) { toolsSelection.value = newTools; defaultTools.value = newTools; @@ -426,8 +425,15 @@ async function onUpdateTools(newTools: INode[]) { async function handleEditAgent(agentId: string) { try { await chatStore.fetchCustomAgent(agentId); - editingAgentId.value = agentId; - uiStore.openModal('agentEditor'); + + uiStore.openModalWithData({ + name: AGENT_EDITOR_MODAL_KEY, + data: { + agentId, + credentials: credentialsByProvider, + onCreateCustomAgent: handleSelectModel, + }, + }); } catch (error) { toast.showError(error, 'Failed to load agent'); } @@ -435,12 +441,13 @@ async function handleEditAgent(agentId: string) { function openNewAgentCreator() { chatStore.currentEditingAgent = null; - editingAgentId.value = undefined; - uiStore.openModal('agentEditor'); -} - -function closeAgentEditor() { - editingAgentId.value = undefined; + uiStore.openModalWithData({ + name: AGENT_EDITOR_MODAL_KEY, + data: { + credentials: credentialsByProvider, + onCreateCustomAgent: handleSelectModel, + }, + }); } function handleOpenWorkflow(workflowId: string) { @@ -472,20 +479,6 @@ function handleOpenWorkflow(workflowId: string) { @open-workflow="handleOpenWorkflow" /> - - - - diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue index a6c0c56e9fe..e9e2fea7fc3 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue @@ -4,27 +4,27 @@ import { useMessage } from '@/app/composables/useMessage'; import { useToast } from '@/app/composables/useToast'; import { useChatStore } from '@/features/ai/chatHub/chat.store'; import ModelSelector from '@/features/ai/chatHub/components/ModelSelector.vue'; -import { useUIStore } from '@/app/stores/ui.store'; import type { ChatHubProvider, ChatModelDto } from '@n8n/api-types'; import { N8nButton, N8nHeading, N8nInput, N8nInputLabel } from '@n8n/design-system'; import { useI18n } from '@n8n/i18n'; import { assert } from '@n8n/utils/assert'; import { createEventBus } from '@n8n/utils/event-bus'; -import { computed, ref, watch } from 'vue'; +import { computed, onMounted, ref } from 'vue'; import type { CredentialsMap } from '../chat.types'; +import type { INode } from 'n8n-workflow'; +import ToolsSelector from './ToolsSelector.vue'; const props = defineProps<{ - credentials: CredentialsMap; - agentId?: string; -}>(); - -const emit = defineEmits<{ - createCustomAgent: [agent: ChatModelDto]; - close: []; + modalName: string; + data: { + agentId?: string; + credentials: CredentialsMap; + onClose?: () => void; + onCreateCustomAgent?: (selection: ChatModelDto) => void; + }; }>(); const chatStore = useChatStore(); -const uiStore = useUIStore(); const i18n = useI18n(); const toast = useToast(); const message = useMessage(); @@ -36,10 +36,11 @@ const systemPrompt = ref(''); const selectedModel = ref(null); const isSaving = ref(false); const isDeleting = ref(false); +const tools = ref([]); const agentSelectedCredentials = ref({}); -const isEditMode = computed(() => !!props.agentId); +const isEditMode = computed(() => !!props.data.agentId); const title = computed(() => isEditMode.value ? i18n.baseText('chatHub.agent.editor.title.edit') @@ -61,7 +62,7 @@ const isValid = computed(() => { const agentMergedCredentials = computed((): CredentialsMap => { return { - ...props.credentials, + ...props.data.credentials, ...agentSelectedCredentials.value, }; }); @@ -75,33 +76,18 @@ function loadAgent() { description.value = customAgent.description ?? ''; systemPrompt.value = customAgent.systemPrompt; selectedModel.value = chatStore.getAgent(customAgent) ?? null; + tools.value = customAgent.tools || []; if (customAgent.credentialId) { agentSelectedCredentials.value[customAgent.provider] = customAgent.credentialId; } } -function resetForm() { - name.value = ''; - description.value = ''; - systemPrompt.value = ''; - selectedModel.value = null; - agentSelectedCredentials.value = {}; -} - -// Watch for modal opening -watch( - () => uiStore.modalsById.agentEditor?.open, - (isOpen) => { - if (isOpen) { - if (props.agentId) { - loadAgent(); - } else { - resetForm(); - } - } - }, -); +onMounted(() => { + if (props.data.agentId) { + loadAgent(); + } +}); function onCredentialSelected(provider: ChatHubProvider, credentialId: string) { agentSelectedCredentials.value = { @@ -136,18 +122,18 @@ async function onSave() { systemPrompt: systemPrompt.value.trim(), ...model, credentialId, - tools: [], + tools: tools.value, }; - if (isEditMode.value && props.agentId) { - await chatStore.updateCustomAgent(props.agentId, payload, props.credentials); + if (isEditMode.value && props.data.agentId) { + await chatStore.updateCustomAgent(props.data.agentId, payload, props.data.credentials); toast.showMessage({ title: i18n.baseText('chatHub.agent.editor.success.update'), type: 'success', }); } else { - const agent = await chatStore.createCustomAgent(payload, props.credentials); - emit('createCustomAgent', agent); + const agent = await chatStore.createCustomAgent(payload, props.data.credentials); + props.data.onCreateCustomAgent?.(agent); toast.showMessage({ title: i18n.baseText('chatHub.agent.editor.success.create'), @@ -165,7 +151,7 @@ async function onSave() { } async function onDelete() { - if (!isEditMode.value || !props.agentId || isDeleting.value) return; + if (!isEditMode.value || !props.data.agentId || isDeleting.value) return; const confirmed = await message.confirm( i18n.baseText('chatHub.agent.editor.delete.confirm.message'), @@ -181,12 +167,12 @@ async function onDelete() { isDeleting.value = true; try { - await chatStore.deleteCustomAgent(props.agentId, props.credentials); + await chatStore.deleteCustomAgent(props.data.agentId, props.data.credentials); toast.showMessage({ title: i18n.baseText('chatHub.agent.editor.success.delete'), type: 'success', }); - emit('close'); + props.data.onClose?.(); modalBus.value.emit('close'); } catch (error) { const errorMessage = error instanceof Error ? error.message : ''; @@ -195,11 +181,15 @@ async function onDelete() { isDeleting.value = false; } } + +function onSelectTools(newTools: INode[]) { + tools.value = newTools; +}