diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 74e73b4176ded..b08c7cc70ae2f 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -1083,6 +1083,7 @@ export default defineComponent({ TelemetryHelpers.generateNodesGraph( workflowData as IWorkflowBase, this.workflowHelpers.getNodeTypes(), + { isCloudDeployment: this.settingsStore.isCloudDeployment }, ).nodeGraph, ), }; @@ -1991,6 +1992,7 @@ export default defineComponent({ TelemetryHelpers.generateNodesGraph( workflowData as IWorkflowBase, this.workflowHelpers.getNodeTypes(), + { isCloudDeployment: this.settingsStore.isCloudDeployment }, ).nodeGraph, ), }; @@ -2147,6 +2149,7 @@ export default defineComponent({ workflowData.meta && workflowData.meta.instanceId !== currInstanceId ? workflowData.meta.instanceId : '', + isCloudDeployment: this.settingsStore.isCloudDeployment, }, ).nodeGraph, ); diff --git a/packages/workflow/src/Constants.ts b/packages/workflow/src/Constants.ts index ecd604f8dc121..12f33a5d11306 100644 --- a/packages/workflow/src/Constants.ts +++ b/packages/workflow/src/Constants.ts @@ -6,18 +6,54 @@ export const LOG_LEVELS = ['silent', 'error', 'warn', 'info', 'debug', 'verbose' export const CODE_LANGUAGES = ['javaScript', 'python'] as const; export const CODE_EXECUTION_MODES = ['runOnceForAllItems', 'runOnceForEachItem'] as const; +// Arbitrary value to represent an empty credential value +export const CREDENTIAL_EMPTY_VALUE = + '__n8n_EMPTY_VALUE_7b1af746-3729-4c60-9b9b-e08eb29e58da' as const; + +export const FORM_TRIGGER_PATH_IDENTIFIER = 'n8n-form'; + +//n8n-nodes-base +export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote'; +export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp'; +export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest'; +export const WEBHOOK_NODE_TYPE = 'n8n-nodes-base.webhook'; +export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger'; +export const ERROR_TRIGGER_NODE_TYPE = 'n8n-nodes-base.errorTrigger'; +export const START_NODE_TYPE = 'n8n-nodes-base.start'; +export const EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE = 'n8n-nodes-base.executeWorkflowTrigger'; +export const CODE_NODE_TYPE = 'n8n-nodes-base.code'; +export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function'; +export const FUNCTION_ITEM_NODE_TYPE = 'n8n-nodes-base.functionItem'; + +export const STARTING_NODE_TYPES = [ + MANUAL_TRIGGER_NODE_TYPE, + EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, + ERROR_TRIGGER_NODE_TYPE, + START_NODE_TYPE, +]; + +export const SCRIPTING_NODE_TYPES = [FUNCTION_NODE_TYPE, FUNCTION_ITEM_NODE_TYPE, CODE_NODE_TYPE]; + /** * Nodes whose parameter values may refer to other nodes without expressions. * Their content may need to be updated when the referenced node is renamed. */ export const NODES_WITH_RENAMABLE_CONTENT = new Set([ - 'n8n-nodes-base.code', - 'n8n-nodes-base.function', - 'n8n-nodes-base.functionItem', + CODE_NODE_TYPE, + FUNCTION_NODE_TYPE, + FUNCTION_ITEM_NODE_TYPE, ]); -// Arbitrary value to represent an empty credential value -export const CREDENTIAL_EMPTY_VALUE = - '__n8n_EMPTY_VALUE_7b1af746-3729-4c60-9b9b-e08eb29e58da' as const; +//@n8n/n8n-nodes-langchain +export const MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualChatTrigger'; +export const AGENT_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent'; +export const OPENAI_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.openAi'; +export const CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE = + '@n8n/n8n-nodes-langchain.chainSummarization'; +export const CODE_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolCode'; +export const WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolWorkflow'; -export const FORM_TRIGGER_PATH_IDENTIFIER = 'n8n-form'; +export const LANGCHAIN_CUSTOM_TOOLS = [ + CODE_TOOL_LANGCHAIN_NODE_TYPE, + WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE, +]; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index e21cf3e73ebb2..54bad71b4b42c 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2238,6 +2238,7 @@ export interface INodeGraphItem { src_node_id?: string; src_instance_id?: string; agent?: string; //@n8n/n8n-nodes-langchain.agent + prompts?: IDataObject[] | IDataObject; //ai node's prompts, cloud only } export interface INodeNameIndex { diff --git a/packages/workflow/src/TelemetryHelpers.ts b/packages/workflow/src/TelemetryHelpers.ts index 999273d082031..5368d94ae8bb6 100644 --- a/packages/workflow/src/TelemetryHelpers.ts +++ b/packages/workflow/src/TelemetryHelpers.ts @@ -8,10 +8,18 @@ import type { INodesGraphResult, IWorkflowBase, INodeTypes, + IDataObject, } from './Interfaces'; import { ApplicationError } from './errors/application.error'; - -const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote'; +import { + AGENT_LANGCHAIN_NODE_TYPE, + CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE, + HTTP_REQUEST_NODE_TYPE, + LANGCHAIN_CUSTOM_TOOLS, + OPENAI_LANGCHAIN_NODE_TYPE, + STICKY_NODE_TYPE, + WEBHOOK_NODE_TYPE, +} from './Constants'; export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined { return workflow.nodes.find((node) => node.name === nodeName); @@ -95,6 +103,7 @@ export function generateNodesGraph( options?: { sourceInstanceId?: string; nodeIdMap?: { [curr: string]: string }; + isCloudDeployment?: boolean; }, ): INodesGraphResult { const nodeGraph: INodesGraph = { @@ -158,15 +167,15 @@ export function generateNodesGraph( nodeItem.src_node_id = options.nodeIdMap[node.id]; } - if (node.type === '@n8n/n8n-nodes-langchain.agent') { + if (node.type === AGENT_LANGCHAIN_NODE_TYPE) { nodeItem.agent = (node.parameters.agent as string) || 'conversationalAgent'; - } else if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion === 1) { + } else if (node.type === HTTP_REQUEST_NODE_TYPE && node.typeVersion === 1) { try { nodeItem.domain = new URL(node.parameters.url as string).hostname; } catch { nodeItem.domain = getDomainBase(node.parameters.url as string); } - } else if (node.type === 'n8n-nodes-base.httpRequest' && node.typeVersion > 1) { + } else if (node.type === HTTP_REQUEST_NODE_TYPE && node.typeVersion > 1) { const { authentication } = node.parameters as { authentication: string }; nodeItem.credential_type = { @@ -182,7 +191,7 @@ export function generateNodesGraph( nodeItem.domain_base = getDomainBase(url); nodeItem.domain_path = getDomainPath(url); nodeItem.method = node.parameters.requestMethod as string; - } else if (node.type === 'n8n-nodes-base.webhook') { + } else if (node.type === WEBHOOK_NODE_TYPE) { webhookNodeNames.push(node.name); } else { try { @@ -216,6 +225,58 @@ export function generateNodesGraph( } } + if (options?.isCloudDeployment === true) { + if (node.type === OPENAI_LANGCHAIN_NODE_TYPE) { + nodeItem.prompts = + (((node.parameters?.messages as IDataObject) || {}).values as IDataObject[]) || []; + } + + if (node.type === AGENT_LANGCHAIN_NODE_TYPE) { + const prompts: IDataObject = {}; + + if (node.parameters?.text) { + prompts.text = node.parameters.text as string; + } + const nodeOptions = node.parameters?.options as IDataObject; + + if (nodeOptions) { + const optionalMessagesKeys = [ + 'humanMessage', + 'systemMessage', + 'humanMessageTemplate', + 'prefix', + 'suffixChat', + 'suffix', + 'prefixPrompt', + 'suffixPrompt', + ]; + + for (const key of optionalMessagesKeys) { + if (nodeOptions[key]) { + prompts[key] = nodeOptions[key] as string; + } + } + } + + if (Object.keys(prompts).length) { + nodeItem.prompts = prompts; + } + } + + if (node.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) { + nodeItem.prompts = ( + (((node.parameters?.options as IDataObject) || {}) + .summarizationMethodAndPrompts as IDataObject) || {} + ).values as IDataObject; + } + + if (LANGCHAIN_CUSTOM_TOOLS.includes(node.type)) { + nodeItem.prompts = { + description: (node.parameters?.description as string) || '', + }; + } + } + nodeGraph.nodes[index.toString()] = nodeItem; nameIndices[node.name] = index.toString(); }); diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 5a05195c8d704..09dbae8c68148 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -52,7 +52,11 @@ import * as NodeHelpers from './NodeHelpers'; import * as ObservableObject from './ObservableObject'; import { RoutingNode } from './RoutingNode'; import { Expression } from './Expression'; -import { NODES_WITH_RENAMABLE_CONTENT } from './Constants'; +import { + MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE, + NODES_WITH_RENAMABLE_CONTENT, + STARTING_NODE_TYPES, +} from './Constants'; import { ApplicationError } from './errors/application.error'; function dedupe(arr: T[]): T[] { @@ -990,7 +994,7 @@ export class Workflow { nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); // TODO: Identify later differently - if (nodeType.description.name === '@n8n/n8n-nodes-langchain.manualChatTrigger') { + if (nodeType.description.name === MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE) { continue; } @@ -1002,20 +1006,13 @@ export class Workflow { } } - const startingNodeTypes = [ - 'n8n-nodes-base.manualTrigger', - 'n8n-nodes-base.executeWorkflowTrigger', - 'n8n-nodes-base.errorTrigger', - 'n8n-nodes-base.start', - ]; - const sortedNodeNames = Object.values(this.nodes) - .sort((a, b) => startingNodeTypes.indexOf(a.type) - startingNodeTypes.indexOf(b.type)) + .sort((a, b) => STARTING_NODE_TYPES.indexOf(a.type) - STARTING_NODE_TYPES.indexOf(b.type)) .map((n) => n.name); for (const nodeName of sortedNodeNames) { node = this.nodes[nodeName]; - if (startingNodeTypes.includes(node.type)) { + if (STARTING_NODE_TYPES.includes(node.type)) { if (node.disabled === true) { continue; } diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 3a88532df6158..a5f2bb7fde86d 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -28,6 +28,7 @@ import { augmentArray, augmentObject } from './AugmentObject'; import { deepCopy } from './utils'; import { getGlobalState } from './GlobalState'; import { ApplicationError } from './errors/application.error'; +import { SCRIPTING_NODE_TYPES } from './Constants'; export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator { return Boolean( @@ -35,12 +36,6 @@ export function isResourceLocatorValue(value: unknown): value is INodeParameterR ); } -const SCRIPTING_NODE_TYPES = [ - 'n8n-nodes-base.function', - 'n8n-nodes-base.functionItem', - 'n8n-nodes-base.code', -]; - const isScriptingNode = (nodeName: string, workflow: Workflow) => { const node = workflow.getNode(nodeName); diff --git a/packages/workflow/src/errors/node-api.error.ts b/packages/workflow/src/errors/node-api.error.ts index 75d75570d8874..0048c9c786f66 100644 --- a/packages/workflow/src/errors/node-api.error.ts +++ b/packages/workflow/src/errors/node-api.error.ts @@ -15,6 +15,7 @@ import { NodeError } from './abstract/node.error'; import { removeCircularRefs } from '../utils'; import type { ReportingOptions } from './application.error'; import { AxiosError } from 'axios'; +import { NO_OP_NODE_TYPE } from '../Constants'; export interface NodeOperationErrorOptions { message?: string; @@ -282,7 +283,7 @@ export class NodeApiError extends NodeError { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE; } - if (this.node.type === 'n8n-nodes-base.noOp' && this.message === UNKNOWN_ERROR_MESSAGE) { + if (this.node.type === NO_OP_NODE_TYPE && this.message === UNKNOWN_ERROR_MESSAGE) { this.message = `${UNKNOWN_ERROR_MESSAGE_CRED} - ${this.httpCode}`; } }