From 5a69d2a2f35cc86685b6eb42e870a2e3ed85c248 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Sat, 9 Aug 2025 10:28:14 +0300 Subject: [PATCH] feat(editor): Add pre-built agents experiment (#18124) --- .../nodes/agents/Agent/V1/AgentV1.node.ts | 14 + .../nodes/agents/Agent/V2/AgentV2.node.ts | 14 + .../createVectorStoreNode.test.ts.snap | 3 +- .../createVectorStoreNode.ts | 3 +- .../load-nodes-and-credentials.test.ts | 30 ++ .../cli/src/load-nodes-and-credentials.ts | 16 +- .../frontend/@n8n/i18n/src/locales/en.json | 2 + packages/frontend/editor-ui/src/Interface.ts | 9 +- .../ItemTypes/OpenTemplateItem.vue | 26 +- .../Node/NodeCreator/Modes/ActionsMode.vue | 23 +- .../Node/NodeCreator/Modes/NodesMode.vue | 33 +- .../NodeCreator/Renderers/ItemsRenderer.vue | 2 + .../__snapshots__/viewsData.spec.ts.snap | 6 +- .../src/components/Node/NodeCreator/utils.ts | 152 ++++++- .../Node/NodeCreator/viewsData.spec.ts | 16 +- .../components/Node/NodeCreator/viewsData.ts | 27 +- .../ParameterInputList.test.constants.ts | 3 +- .../src/components/ParameterInputList.vue | 67 ++- .../src/composables/useCalloutHelpers.test.ts | 176 +++++++- .../src/composables/useCalloutHelpers.ts | 166 +++++++- .../composables/useCanvasOperations.test.ts | 4 +- .../handlers/executionFinished.ts | 27 +- packages/frontend/editor-ui/src/constants.ts | 9 + .../easyAiWorkflowUtils.test.ts.snap | 143 ------- .../src/utils/easyAiWorkflowUtils.test.ts | 12 - .../src/utils/easyAiWorkflowUtils.ts | 352 ---------------- .../agents/email_triage_agent_with_gmail.json | 156 +++++++ .../agents/joke_agent_with_http_tool.json | 168 ++++++++ ...owledge_store_agent_with_google_drive.json | 284 +++++++++++++ ...k_management_agent_with_google_sheets.json | 388 ++++++++++++++++++ ...ssistant_agent_with_telegram_and_gcal.json | 307 ++++++++++++++ .../templates/samples/easy_ai_starter.json | 58 +++ .../utils/templates/samples/rag_starter.json | 263 ++++++++++++ .../src/utils/templates/typeGuards.ts | 11 + .../src/utils/templates/workflowSamples.ts | 132 ++++++ .../frontend/editor-ui/src/views/NodeView.vue | 39 +- .../editor-ui/src/views/WorkflowsView.vue | 20 +- .../Google/Calendar/GoogleCalendar.node.ts | 18 + .../Drive/v2/actions/versionDescription.ts | 20 +- .../nodes/Google/Gmail/v2/GmailV2.node.ts | 18 + .../Sheet/v2/actions/versionDescription.ts | 17 + .../nodes/HttpRequest/V3/Description.ts | 17 + .../nodes/Telegram/Telegram.node.ts | 18 + packages/workflow/src/interfaces.ts | 19 +- 44 files changed, 2635 insertions(+), 653 deletions(-) delete mode 100644 packages/frontend/editor-ui/src/utils/__snapshots__/easyAiWorkflowUtils.test.ts.snap delete mode 100644 packages/frontend/editor-ui/src/utils/easyAiWorkflowUtils.test.ts delete mode 100644 packages/frontend/editor-ui/src/utils/easyAiWorkflowUtils.ts create mode 100644 packages/frontend/editor-ui/src/utils/templates/samples/agents/email_triage_agent_with_gmail.json create mode 100644 packages/frontend/editor-ui/src/utils/templates/samples/agents/joke_agent_with_http_tool.json create mode 100644 packages/frontend/editor-ui/src/utils/templates/samples/agents/knowledge_store_agent_with_google_drive.json create mode 100644 packages/frontend/editor-ui/src/utils/templates/samples/agents/task_management_agent_with_google_sheets.json create mode 100644 packages/frontend/editor-ui/src/utils/templates/samples/agents/voice_assistant_agent_with_telegram_and_gcal.json create mode 100644 packages/frontend/editor-ui/src/utils/templates/samples/easy_ai_starter.json create mode 100644 packages/frontend/editor-ui/src/utils/templates/samples/rag_starter.json create mode 100644 packages/frontend/editor-ui/src/utils/templates/workflowSamples.ts diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V1/AgentV1.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V1/AgentV1.node.ts index 5005650973..d90a6b00ec 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V1/AgentV1.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V1/AgentV1.node.ts @@ -312,6 +312,20 @@ export class AgentV1 implements INodeType { }, }, }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Get started faster with our', + name: 'preBuiltAgentsCallout', + type: 'callout', + typeOptions: { + calloutAction: { + label: 'pre-built agents', + icon: 'bot', + type: 'openPreBuiltAgentsCollection', + }, + }, + default: '', + }, { displayName: "This node is using Agent that has been deprecated. Please switch to using 'Tools Agent' instead.", diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V2/AgentV2.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V2/AgentV2.node.ts index ea956a778f..6f1a664bcd 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V2/AgentV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V2/AgentV2.node.ts @@ -39,6 +39,20 @@ export class AgentV2 implements INodeType { type: 'callout', default: '', }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + displayName: 'Get started faster with our', + name: 'preBuiltAgentsCallout', + type: 'callout', + typeOptions: { + calloutAction: { + label: 'pre-built agents', + icon: 'bot', + type: 'openPreBuiltAgentsCollection', + }, + }, + default: '', + }, promptTypeOptions, { ...textFromPreviousNode, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap index 183c0c1045..da4fea5d25 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap @@ -86,7 +86,8 @@ exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] = "typeOptions": { "calloutAction": { "label": "RAG starter template", - "type": "openRagStarterTemplate", + "templateId": "rag-starter-template", + "type": "openSampleWorkflowTemplate", }, }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts index 7b2f6f00c2..7fff59b520 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode/createVectorStoreNode.ts @@ -32,7 +32,8 @@ const ragStarterCallout: INodeProperties = { typeOptions: { calloutAction: { label: 'RAG starter template', - type: 'openRagStarterTemplate', + type: 'openSampleWorkflowTemplate', + templateId: 'rag-starter-template', }, }, default: '', diff --git a/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts b/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts index d5e14b9263..7b88a0aad0 100644 --- a/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts +++ b/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts @@ -86,6 +86,36 @@ describe('LoadNodesAndCredentials', () => { expect(toolDescriptionProp?.default).toBe(fullNodeWrapper.description.description); }); + it('should add toolDescription property after callout property', () => { + fullNodeWrapper.description.properties = [ + { + displayName: 'Callout 1', + name: 'callout1', + type: 'callout', + default: '', + }, + { + displayName: 'Callout 2', + name: 'callout2', + type: 'callout', + default: '', + }, + { + displayName: 'Another', + name: 'another', + type: 'boolean', + default: true, + }, + ] satisfies INodeProperties[]; + + const result = instance.convertNodeToAiTool(fullNodeWrapper); + const toolDescriptionPropIndex = result.description.properties.findIndex( + (prop) => prop.name === 'toolDescription', + ); + expect(toolDescriptionPropIndex).toBe(2); + expect(result.description.properties).toHaveLength(4); + }); + it('should set codex categories correctly', () => { const result = instance.convertNodeToAiTool(fullNodeWrapper); expect(result.description.codex).toEqual({ diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index e397429f7a..fa37a34c21 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -188,6 +188,14 @@ export class LoadNodesAndCredentials { return isContainedWithin(nodeParentPath, filePath) ? filePath : undefined; } + findLastCalloutIndex(properties: INodeProperties[]): number { + for (let i = properties.length - 1; i >= 0; i--) { + if (properties[i].type === 'callout') return i; + } + + return -1; + } + getCustomDirectories(): string[] { const customDirectories = [this.instanceSettings.customExtensionDir]; @@ -486,12 +494,14 @@ export class LoadNodesAndCredentials { 'Explain to the LLM what this tool does, a good, specific description would allow LLMs to produce expected results much more often', }; - item.description.properties.unshift(descProp); + const lastCallout = this.findLastCalloutIndex(item.description.properties); + + item.description.properties.splice(lastCallout + 1, 0, descProp); // If node has resource or operation we can determine pre-populate tool description based on it - // so we add the descriptionType property as the first property + // so we add the descriptionType property as the first property after possible callout param(s). if (hasResource || hasOperation) { - item.description.properties.unshift(descriptionType); + item.description.properties.splice(lastCallout + 1, 0, descriptionType); descProp.displayOptions = { show: { diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index d42a5c153f..bdd80def48 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -1398,6 +1398,8 @@ "nodeCreator.nodeItem.triggerIconTitle": "Trigger Node", "nodeCreator.nodeItem.aiIconTitle": "LangChain AI Node", "nodeCreator.nodeItem.deprecated": "Deprecated", + "nodeCreator.preBuiltAgents.title": "Pre-built agents", + "nodeCreator.preBuiltAgents.description": "Get started faster with ready to go agents", "nodeCredentials.createNew": "Create new credential", "nodeCredentials.credentialFor": "Credential for {credentialType}", "nodeCredentials.credentialsLabel": "Credential to connect with", diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts index 3be9f0414d..70c1111f59 100644 --- a/packages/frontend/editor-ui/src/Interface.ts +++ b/packages/frontend/editor-ui/src/Interface.ts @@ -645,11 +645,13 @@ export interface LinkItemProps { } export interface OpenTemplateItemProps { - key: 'rag-starter-template'; + templateId: string; title: string; description: string; - icon: string; + nodes?: INodeTypeDescription[]; + icon?: string; tag?: NodeCreatorTag; + compact?: boolean; } export interface ActionTypeDescription extends SimplifiedNodeType { @@ -922,7 +924,8 @@ export type NodeCreatorOpenSource = | 'notice_error_message' | 'add_node_button' | 'add_evaluation_trigger_button' - | 'add_evaluation_node_button'; + | 'add_evaluation_node_button' + | 'templates_callout'; export interface INodeCreatorState { itemsFilter: string; diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue index 5761a637f4..5a5f3e4252 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/ItemTypes/OpenTemplateItem.vue @@ -9,15 +9,15 @@ defineProps();