diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue index dd45647672..bac1079d75 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -83,10 +83,10 @@ const i18n = useI18n(); const telemetry = useTelemetry(); onBeforeMount(async () => { - if (rootStore.defaultLocale === 'en') return; - uiStore.activeCredentialType = props.credentialType.name; + if (rootStore.defaultLocale === 'en') return; + const key = `n8n-nodes-base.credentials.${props.credentialType.name}`; if (i18n.exists(key)) return; diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index ba3eb19ff0..efbde8d1fb 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -420,6 +420,7 @@ async function beforeClose() { } if (!keepEditing) { + uiStore.activeCredentialType = null; return true; } else if (!requiredPropertiesFilled.value) { showValidationWarning.value = true; @@ -986,6 +987,7 @@ async function onAuthTypeChanged(type: string): Promise { const credentialsForType = getNodeCredentialForSelectedAuthType(activeNodeType.value, type); if (credentialsForType) { selectedCredential.value = credentialsForType.name; + uiStore.activeCredentialType = credentialsForType.name; resetCredentialData(); // Update current node auth type so credentials dropdown can be displayed properly updateNodeAuthType(ndvStore.activeNode, type); diff --git a/packages/editor-ui/src/components/Error/NodeErrorView.vue b/packages/editor-ui/src/components/Error/NodeErrorView.vue index aa0eac4c69..749cc203e4 100644 --- a/packages/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue @@ -23,6 +23,7 @@ import type { ChatRequest } from '@/types/assistant.types'; import InlineAskAssistantButton from 'n8n-design-system/components/InlineAskAssistantButton/InlineAskAssistantButton.vue'; import { useUIStore } from '@/stores/ui.store'; import { isCommunityPackageName } from '@/utils/nodeTypesUtils'; +import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers'; type Props = { // TODO: .node can be undefined @@ -34,6 +35,7 @@ const props = defineProps(); const clipboard = useClipboard(); const toast = useToast(); const i18n = useI18n(); +const assistantHelpers = useAIAssistantHelpers(); const nodeTypesStore = useNodeTypesStore(); const ndvStore = useNDVStore(); @@ -124,33 +126,11 @@ const isAskAssistantAvailable = computed(() => { const assistantAlreadyAsked = computed(() => { return assistantStore.isNodeErrorActive({ - error: simplifyErrorForAssistant(props.error), + error: assistantHelpers.simplifyErrorForAssistant(props.error), node: props.error.node || ndvStore.activeNode, }); }); -function simplifyErrorForAssistant( - error: NodeError | NodeApiError | NodeOperationError, -): ChatRequest.ErrorContext['error'] { - const simple: ChatRequest.ErrorContext['error'] = { - name: error.name, - message: error.message, - }; - if ('type' in error) { - simple.type = error.type; - } - if ('description' in error && error.description) { - simple.description = error.description; - } - if (error.stack) { - simple.stack = error.stack; - } - if ('lineNumber' in error) { - simple.lineNumber = error.lineNumber; - } - return simple; -} - function nodeVersionTag(nodeType: NodeError['node']): string { if (!nodeType || ('hidden' in nodeType && nodeType.hidden)) { return i18n.baseText('nodeSettings.deprecated'); diff --git a/packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts b/packages/editor-ui/src/composables/__tests__/useAIAssistantHelpers.test.ts similarity index 95% rename from packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts rename to packages/editor-ui/src/composables/__tests__/useAIAssistantHelpers.test.ts index 118095ed24..d04733336e 100644 --- a/packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useAIAssistantHelpers.test.ts @@ -1,6 +1,8 @@ import { describe, it, expect } from 'vitest'; -import { getReferencedNodes } from '../nodeTypesUtils'; import type { INode } from 'n8n-workflow'; +import { useAIAssistantHelpers } from '../useAIAssistantHelpers'; +import { createTestingPinia } from '@pinia/testing'; +import { setActivePinia } from 'pinia'; const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected: string[] }> = [ { @@ -376,8 +378,15 @@ const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected: ]; describe.each(referencedNodesTestCases)('getReferencedNodes', (testCase) => { + let aiAssistantHelpers: ReturnType; + + beforeEach(() => { + setActivePinia(createTestingPinia()); + aiAssistantHelpers = useAIAssistantHelpers(); + }); + const caseName = testCase.caseName; it(`${caseName}`, () => { - expect(getReferencedNodes(testCase.node)).toEqual(testCase.expected); + expect(aiAssistantHelpers.getReferencedNodes(testCase.node)).toEqual(testCase.expected); }); }); diff --git a/packages/editor-ui/src/composables/useAIAssistantHelpers.ts b/packages/editor-ui/src/composables/useAIAssistantHelpers.ts new file mode 100644 index 0000000000..a1c222ae48 --- /dev/null +++ b/packages/editor-ui/src/composables/useAIAssistantHelpers.ts @@ -0,0 +1,216 @@ +import type { IDataObject, NodeApiError, NodeError, NodeOperationError } from 'n8n-workflow'; +import { deepCopy, type INode } from 'n8n-workflow'; +import { useWorkflowHelpers } from './useWorkflowHelpers'; +import { useRouter } from 'vue-router'; +import { useNDVStore } from '@/stores/ndv.store'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import { executionDataToJson, getMainAuthField, getNodeAuthOptions } from '@/utils/nodeTypesUtils'; +import type { ChatRequest } from '@/types/assistant.types'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { useDataSchema } from './useDataSchema'; +import { VIEWS } from '@/constants'; +import { useI18n } from './useI18n'; + +const CANVAS_VIEWS = [VIEWS.NEW_WORKFLOW, VIEWS.WORKFLOW, VIEWS.EXECUTION_DEBUG]; +const EXECUTION_VIEWS = [VIEWS.EXECUTION_PREVIEW]; +const WORKFLOW_LIST_VIEWS = [VIEWS.WORKFLOWS, VIEWS.PROJECTS_WORKFLOWS]; +const CREDENTIALS_LIST_VIEWS = [VIEWS.CREDENTIALS, VIEWS.PROJECTS_CREDENTIALS]; + +export const useAIAssistantHelpers = () => { + const ndvStore = useNDVStore(); + const nodeTypesStore = useNodeTypesStore(); + const workflowsStore = useWorkflowsStore(); + + const workflowHelpers = useWorkflowHelpers({ router: useRouter() }); + const locale = useI18n(); + + /** + Regular expression to extract the node names from the expressions in the template. + Supports single quotes, double quotes, and backticks. + */ + const entityRegex = /\$\(\s*(\\?["'`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/g; + + /** + * Extract the node names from the expressions in the template. + */ + function extractNodeNames(template: string): string[] { + let matches; + const nodeNames: string[] = []; + while ((matches = entityRegex.exec(template)) !== null) { + nodeNames.push(matches[2]); + } + return nodeNames; + } + + /** + * Unescape quotes in the string. Supports single quotes, double quotes, and backticks. + */ + function unescapeQuotes(str: string): string { + return str.replace(/\\(['"`])/g, '$1'); + } + + /** + * Extract the node names from the expressions in the node parameters. + */ + function getReferencedNodes(node: INode): string[] { + const referencedNodes: Set = new Set(); + if (!node) { + return []; + } + // Go through all parameters and check if they contain expressions on any level + for (const key in node.parameters) { + let names: string[] = []; + if ( + node.parameters[key] && + typeof node.parameters[key] === 'object' && + Object.keys(node.parameters[key]).length + ) { + names = extractNodeNames(JSON.stringify(node.parameters[key])); + } else if (typeof node.parameters[key] === 'string' && node.parameters[key]) { + names = extractNodeNames(node.parameters[key]); + } + if (names.length) { + names + .map((name) => unescapeQuotes(name)) + .forEach((name) => { + referencedNodes.add(name); + }); + } + } + return referencedNodes.size ? Array.from(referencedNodes) : []; + } + + /** + * Processes node object before sending it to AI assistant + * - Removes unnecessary properties + * - Extracts expressions from the parameters and resolves them + * @param node original node object + * @param propsToRemove properties to remove from the node object + * @returns processed node + */ + function processNodeForAssistant(node: INode, propsToRemove: string[]): INode { + // Make a copy of the node object so we don't modify the original + const nodeForLLM = deepCopy(node); + propsToRemove.forEach((key) => { + delete nodeForLLM[key as keyof INode]; + }); + const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions( + nodeForLLM.parameters, + ); + nodeForLLM.parameters = resolvedParameters; + return nodeForLLM; + } + + function getNodeInfoForAssistant(node: INode): ChatRequest.NodeInfo { + if (!node) { + return {}; + } + // Get all referenced nodes and their schemas + const referencedNodeNames = getReferencedNodes(node); + const schemas = getNodesSchemas(referencedNodeNames); + + const nodeType = nodeTypesStore.getNodeType(node.type); + + // Get node credentials details for the ai assistant + let authType = undefined; + if (nodeType) { + const authField = getMainAuthField(nodeType); + const credentialInUse = node.parameters[authField?.name ?? '']; + const availableAuthOptions = getNodeAuthOptions(nodeType); + authType = availableAuthOptions.find((option) => option.value === credentialInUse); + } + let nodeInputData: { inputNodeName?: string; inputData?: IDataObject } | undefined = undefined; + const ndvInput = ndvStore.ndvInputData; + if (isNodeReferencingInputData(node) && ndvInput?.length) { + const inputData = ndvStore.ndvInputData[0].json; + const inputNodeName = ndvStore.input.nodeName; + nodeInputData = { + inputNodeName, + inputData, + }; + } + return { + authType, + schemas, + nodeInputData, + }; + } + + /** + * Simplify node error object for AI assistant + */ + function simplifyErrorForAssistant( + error: NodeError | NodeApiError | NodeOperationError, + ): ChatRequest.ErrorContext['error'] { + const simple: ChatRequest.ErrorContext['error'] = { + name: error.name, + message: error.message, + }; + if ('type' in error) { + simple.type = error.type; + } + if ('description' in error && error.description) { + simple.description = error.description; + } + if (error.stack) { + simple.stack = error.stack; + } + if ('lineNumber' in error) { + simple.lineNumber = error.lineNumber; + } + return simple; + } + + function isNodeReferencingInputData(node: INode): boolean { + const parametersString = JSON.stringify(node.parameters); + const references = ['$json', '$input', '$binary']; + return references.some((ref) => parametersString.includes(ref)); + } + + /** + * Get the schema for the referenced nodes as expected by the AI assistant + * @param nodeNames The names of the nodes to get the schema for + * @returns An array of NodeExecutionSchema objects + */ + function getNodesSchemas(nodeNames: string[]) { + const schemas: ChatRequest.NodeExecutionSchema[] = []; + for (const name of nodeNames) { + const node = workflowsStore.getNodeByName(name); + if (!node) { + continue; + } + const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema(); + const schema = getSchemaForExecutionData(executionDataToJson(getInputDataWithPinned(node))); + schemas.push({ + nodeName: node.name, + schema, + }); + } + return schemas; + } + + function getCurrentViewDescription(view: VIEWS) { + switch (true) { + case WORKFLOW_LIST_VIEWS.includes(view): + return locale.baseText('aiAssistant.prompts.currentView.workflowList'); + case CREDENTIALS_LIST_VIEWS.includes(view): + return locale.baseText('aiAssistant.prompts.currentView.credentialsList'); + case EXECUTION_VIEWS.includes(view): + return locale.baseText('aiAssistant.prompts.currentView.executionsView'); + case CANVAS_VIEWS.includes(view): + return locale.baseText('aiAssistant.prompts.currentView.workflowEditor'); + default: + return undefined; + } + } + + return { + processNodeForAssistant, + getNodeInfoForAssistant, + simplifyErrorForAssistant, + isNodeReferencingInputData, + getNodesSchemas, + getCurrentViewDescription, + getReferencedNodes, + }; +}; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 83a3f5792a..7dc37253b8 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -150,6 +150,10 @@ "aiAssistant.codeUpdated.message.body2": "node to see the changes", "aiAssistant.thinkingSteps.analyzingError": "Analyzing the error...", "aiAssistant.thinkingSteps.thinking": "Thinking...", + "aiAssistant.prompts.currentView.workflowList": "The user is currently looking at the list of workflows.", + "aiAssistant.prompts.currentView.credentialsList": "The user is currently looking at the list of credentials.", + "aiAssistant.prompts.currentView.executionsView": "The user is currently looking at the list of executions for the currently open workflow.", + "aiAssistant.prompts.currentView.workflowEditor": "The user is currently looking at the current workflow in n8n editor, without any specific node selected.", "banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your", "banners.confirmEmail.message.2": "email address.", "banners.confirmEmail.button": "Confirm email", diff --git a/packages/editor-ui/src/stores/assistant.store.ts b/packages/editor-ui/src/stores/assistant.store.ts index bb28382088..851018b5d5 100644 --- a/packages/editor-ui/src/stores/assistant.store.ts +++ b/packages/editor-ui/src/stores/assistant.store.ts @@ -5,6 +5,7 @@ import { STORES, AI_ASSISTANT_EXPERIMENT, PLACEHOLDER_EMPTY_WORKFLOW_ID, + CREDENTIAL_EDIT_MODAL_KEY, } from '@/constants'; import type { ChatRequest } from '@/types/assistant.types'; import type { ChatUI } from 'n8n-design-system/types/assistant'; @@ -17,26 +18,19 @@ import { useRoute } from 'vue-router'; import { useSettingsStore } from './settings.store'; import { assert } from '@/utils/assert'; import { useWorkflowsStore } from './workflows.store'; -import type { IDataObject, ICredentialType, INodeParameters } from 'n8n-workflow'; +import type { ICredentialType, INodeParameters, NodeError, INode } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus'; import { useNDVStore } from './ndv.store'; import type { IUpdateInformation } from '@/Interface'; -import { - getMainAuthField, - getNodeAuthOptions, - getReferencedNodes, - getNodesSchemas, - processNodeForAssistant, - isNodeReferencingInputData, -} from '@/utils/nodeTypesUtils'; -import { useNodeTypesStore } from './nodeTypes.store'; import { usePostHog } from './posthog.store'; import { useI18n } from '@/composables/useI18n'; import { useTelemetry } from '@/composables/useTelemetry'; import { useToast } from '@/composables/useToast'; import { useUIStore } from './ui.store'; import AiUpdatedCodeMessage from '@/components/AiUpdatedCodeMessage.vue'; +import { useCredentialsStore } from './credentials.store'; +import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers'; export const MAX_CHAT_WIDTH = 425; export const MIN_CHAT_WIDTH = 250; @@ -69,6 +63,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { const { getVariant } = usePostHog(); const locale = useI18n(); const telemetry = useTelemetry(); + const assistantHelpers = useAIAssistantHelpers(); const suggestions = ref<{ [suggestionId: string]: { @@ -355,10 +350,74 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { }); } + /** + * Gets information about the current view and active node to provide context to the assistant + */ + function getVisualContext(nodeInfo?: ChatRequest.NodeInfo): ChatRequest.UserContext | undefined { + if (chatSessionTask.value === 'error') { + return undefined; + } + const currentView = route.name as VIEWS; + const activeNode = workflowsStore.activeNode(); + const activeNodeForLLM = activeNode + ? assistantHelpers.processNodeForAssistant(activeNode, ['position']) + : null; + const activeModals = uiStore.activeModals; + const isCredentialModalActive = activeModals.includes(CREDENTIAL_EDIT_MODAL_KEY); + const activeCredential = isCredentialModalActive + ? useCredentialsStore().getCredentialTypeByName(uiStore.activeCredentialType ?? '') + : undefined; + const executionResult = workflowsStore.workflowExecutionData?.data?.resultData; + const isCurrentNodeExecuted = Boolean( + executionResult?.runData?.hasOwnProperty(activeNode?.name ?? ''), + ); + const currentNodeHasError = + executionResult?.error && + 'node' in executionResult.error && + executionResult.error.node?.name === activeNode?.name; + const nodeError = currentNodeHasError ? (executionResult.error as NodeError) : undefined; + const executionStatus = isCurrentNodeExecuted + ? { + status: nodeError ? 'error' : 'success', + error: nodeError ? assistantHelpers.simplifyErrorForAssistant(nodeError) : undefined, + } + : undefined; + return { + currentView: { + name: currentView, + description: assistantHelpers.getCurrentViewDescription(currentView), + }, + activeNodeInfo: { + node: activeNodeForLLM ?? undefined, + nodeIssues: !isCurrentNodeExecuted ? activeNode?.issues : undefined, + executionStatus, + nodeInputData: nodeInfo?.nodeInputData, + referencedNodes: nodeInfo?.schemas, + }, + activeCredentials: activeCredential + ? { + name: activeCredential?.name, + displayName: activeCredential?.displayName, + authType: nodeInfo?.authType?.name, + } + : undefined, + }; + } + async function initSupportChat(userMessage: string, credentialType?: ICredentialType) { - const id = getRandomId(); resetAssistantChat(); chatSessionTask.value = credentialType ? 'credentials' : 'support'; + const activeNode = workflowsStore.activeNode() as INode; + const nodeInfo = assistantHelpers.getNodeInfoForAssistant(activeNode); + // For the initial message, only provide visual context if the task is support + const visualContext = + chatSessionTask.value === 'support' ? getVisualContext(nodeInfo) : undefined; + + if (nodeInfo.authType && chatSessionTask.value === 'credentials') { + userMessage += ` I am using ${nodeInfo.authType.name}.`; + } + + const id = getRandomId(); chatSessionCredType.value = credentialType; addUserMessage(userMessage, id); addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.thinking')); @@ -371,6 +430,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { user: { firstName: usersStore.currentUser?.firstName ?? '', }, + context: visualContext, question: userMessage, }; if (credentialType) { @@ -413,29 +473,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { currentSessionActiveExecutionId.value = workflowsStore.activeExecutionId; } - // Get all referenced nodes and their schemas - const referencedNodeNames = getReferencedNodes(context.node); - const schemas = getNodesSchemas(referencedNodeNames); + const { authType, nodeInputData, schemas } = assistantHelpers.getNodeInfoForAssistant( + context.node, + ); - // Get node credentials details for the ai assistant - const nodeType = useNodeTypesStore().getNodeType(context.node.type); - let authType = undefined; - if (nodeType) { - const authField = getMainAuthField(nodeType); - const credentialInUse = context.node.parameters[authField?.name ?? '']; - const availableAuthOptions = getNodeAuthOptions(nodeType); - authType = availableAuthOptions.find((option) => option.value === credentialInUse); - } - let nodeInputData: { inputNodeName?: string; inputData?: IDataObject } | undefined = undefined; - const ndvInput = ndvStore.ndvInputData; - if (isNodeReferencingInputData(context.node) && ndvInput?.length) { - const inputData = ndvStore.ndvInputData[0].json; - const inputNodeName = ndvStore.input.nodeName; - nodeInputData = { - inputNodeName, - inputData, - }; - } addLoadingAssistantMessage(locale.baseText('aiAssistant.thinkingSteps.analyzingError')); openChat(); @@ -450,7 +491,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { firstName: usersStore.currentUser?.firstName ?? '', }, error: context.error, - node: processNodeForAssistant(context.node, ['position']), + node: assistantHelpers.processNodeForAssistant(context.node, ['position']), nodeInputData, executionSchema: schemas, authType, @@ -536,6 +577,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { ) { nodeExecutionStatus.value = 'not_executed'; } + const userContext = getVisualContext(); chatWithAssistant( rootStore.restApiContext, { @@ -544,6 +586,7 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { type: 'message', text: chatMessage.text, quickReplyType: chatMessage.quickReplyType, + context: userContext, }, sessionId: currentSessionId.value, }, diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index deb643174c..96554a6e91 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -303,6 +303,8 @@ export const useUIStore = defineStore(STORES.UI, () => { }, {}), ); + const activeModals = computed(() => modalStack.value.map((modalName) => modalName)); + const fakeDoorsByLocation = computed(() => fakeDoorFeatures.value.reduce((acc: { [uiLocation: string]: IFakeDoor }, fakeDoor) => { fakeDoor.uiLocations.forEach((uiLocation: IFakeDoorLocation) => { @@ -700,6 +702,7 @@ export const useUIStore = defineStore(STORES.UI, () => { setNotificationsForView, deleteNotificationsForView, resetLastInteractedWith, + activeModals, }; }); diff --git a/packages/editor-ui/src/types/assistant.types.ts b/packages/editor-ui/src/types/assistant.types.ts index 1b6ea0a089..4f07f1002d 100644 --- a/packages/editor-ui/src/types/assistant.types.ts +++ b/packages/editor-ui/src/types/assistant.types.ts @@ -1,5 +1,12 @@ -import type { Schema } from '@/Interface'; -import type { IDataObject, INode, INodeParameters } from 'n8n-workflow'; +import type { VIEWS } from '@/constants'; +import type { NodeAuthenticationOption, Schema } from '@/Interface'; +import type { + ICredentialType, + IDataObject, + INode, + INodeIssues, + INodeParameters, +} from 'n8n-workflow'; export namespace ChatRequest { export interface NodeExecutionSchema { @@ -39,6 +46,7 @@ export namespace ChatRequest { user: { firstName: string; }; + context?: UserContext; question: string; } @@ -69,6 +77,25 @@ export namespace ChatRequest { type: 'message'; text: string; quickReplyType?: string; + context?: UserContext; + } + + export interface UserContext { + activeNodeInfo?: { + node?: INode; + nodeIssues?: INodeIssues; + nodeInputData?: IDataObject; + referencedNodes?: NodeExecutionSchema[]; + executionStatus?: { + status: string; + error?: ErrorContext['error']; + }; + }; + activeCredentials?: Pick & { authType?: string }; + currentView?: { + name: VIEWS; + description?: string; + }; } export type RequestPayload = @@ -146,6 +173,15 @@ export namespace ChatRequest { sessionId?: string; messages: MessageResponse[]; } + + export interface NodeInfo { + authType?: NodeAuthenticationOption; + schemas?: NodeExecutionSchema[]; + nodeInputData?: { + inputNodeName?: string; + inputData?: IDataObject; + }; + } } export namespace ReplaceCodeRequest { diff --git a/packages/editor-ui/src/utils/nodeTypesUtils.ts b/packages/editor-ui/src/utils/nodeTypesUtils.ts index 03f930c740..91fe812e4e 100644 --- a/packages/editor-ui/src/utils/nodeTypesUtils.ts +++ b/packages/editor-ui/src/utils/nodeTypesUtils.ts @@ -7,8 +7,6 @@ import type { NodeAuthenticationOption, SimplifiedNodeType, } from '@/Interface'; -import { useDataSchema } from '@/composables/useDataSchema'; -import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { CORE_NODES_CATEGORY, MAIN_AUTH_FIELD_NAME, @@ -20,22 +18,18 @@ import { i18n as locale } from '@/plugins/i18n'; import { useCredentialsStore } from '@/stores/credentials.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; -import type { ChatRequest } from '@/types/assistant.types'; import { isResourceLocatorValue } from '@/utils/typeGuards'; import { isJsonKeyObject } from '@/utils/typesUtils'; -import { - deepCopy, - type IDataObject, - type INode, - type INodeCredentialDescription, - type INodeExecutionData, - type INodeProperties, - type INodeTypeDescription, - type NodeParameterValueType, - type ResourceMapperField, - type Themed, +import type { + IDataObject, + INodeCredentialDescription, + INodeExecutionData, + INodeProperties, + INodeTypeDescription, + NodeParameterValueType, + ResourceMapperField, + Themed, } from 'n8n-workflow'; -import { useRouter } from 'vue-router'; /* Constants and utility functions mainly used to get information about @@ -502,109 +496,3 @@ export const getNodeIconColor = ( } return nodeType?.defaults?.color?.toString(); }; - -/** - Regular expression to extract the node names from the expressions in the template. - Supports single quotes, double quotes, and backticks. -*/ -const entityRegex = /\$\(\s*(\\?["'`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/g; - -/** - * Extract the node names from the expressions in the template. - */ -function extractNodeNames(template: string): string[] { - let matches; - const nodeNames: string[] = []; - while ((matches = entityRegex.exec(template)) !== null) { - nodeNames.push(matches[2]); - } - return nodeNames; -} - -/** - * Unescape quotes in the string. Supports single quotes, double quotes, and backticks. - */ -export function unescapeQuotes(str: string): string { - return str.replace(/\\(['"`])/g, '$1'); -} - -/** - * Extract the node names from the expressions in the node parameters. - */ -export function getReferencedNodes(node: INode): string[] { - const referencedNodes: Set = new Set(); - if (!node) { - return []; - } - // Go through all parameters and check if they contain expressions on any level - for (const key in node.parameters) { - let names: string[] = []; - if ( - node.parameters[key] && - typeof node.parameters[key] === 'object' && - Object.keys(node.parameters[key]).length - ) { - names = extractNodeNames(JSON.stringify(node.parameters[key])); - } else if (typeof node.parameters[key] === 'string' && node.parameters[key]) { - names = extractNodeNames(node.parameters[key]); - } - if (names.length) { - names - .map((name) => unescapeQuotes(name)) - .forEach((name) => { - referencedNodes.add(name); - }); - } - } - return referencedNodes.size ? Array.from(referencedNodes) : []; -} - -/** - * Processes node object before sending it to AI assistant - * - Removes unnecessary properties - * - Extracts expressions from the parameters and resolves them - * @param node original node object - * @param propsToRemove properties to remove from the node object - * @returns processed node - */ -export function processNodeForAssistant(node: INode, propsToRemove: string[]): INode { - // Make a copy of the node object so we don't modify the original - const nodeForLLM = deepCopy(node); - propsToRemove.forEach((key) => { - delete nodeForLLM[key as keyof INode]; - }); - const workflowHelpers = useWorkflowHelpers({ router: useRouter() }); - const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions( - nodeForLLM.parameters, - ); - nodeForLLM.parameters = resolvedParameters; - return nodeForLLM; -} - -export function isNodeReferencingInputData(node: INode): boolean { - const parametersString = JSON.stringify(node.parameters); - const references = ['$json', '$input', '$binary']; - return references.some((ref) => parametersString.includes(ref)); -} - -/** - * Get the schema for the referenced nodes as expected by the AI assistant - * @param nodeNames The names of the nodes to get the schema for - * @returns An array of NodeExecutionSchema objects - */ -export function getNodesSchemas(nodeNames: string[]) { - const schemas: ChatRequest.NodeExecutionSchema[] = []; - for (const name of nodeNames) { - const node = useWorkflowsStore().getNodeByName(name); - if (!node) { - continue; - } - const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema(); - const schema = getSchemaForExecutionData(executionDataToJson(getInputDataWithPinned(node))); - schemas.push({ - nodeName: node.name, - schema, - }); - } - return schemas; -}