diff --git a/packages/editor-ui/src/composables/__tests__/useWorkflowHelpers.spec.ts b/packages/editor-ui/src/composables/__tests__/useWorkflowHelpers.spec.ts index f50261a1aa..ac7d5addab 100644 --- a/packages/editor-ui/src/composables/__tests__/useWorkflowHelpers.spec.ts +++ b/packages/editor-ui/src/composables/__tests__/useWorkflowHelpers.spec.ts @@ -7,6 +7,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsEEStore } from '@/stores/workflows.ee.store'; import { useTagsStore } from '@/stores/tags.store'; import { createTestWorkflow } from '@/__tests__/mocks'; +import type { AssignmentCollectionValue } from 'n8n-workflow'; const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({ name: 'Duplicate webhook test', @@ -70,6 +71,163 @@ describe('useWorkflowHelpers', () => { vi.clearAllMocks(); }); + describe('getNodeParametersWithResolvedExpressions', () => { + it('should correctly detect and resolve expressions in a regular node ', () => { + const nodeParameters = { + curlImport: '', + method: 'GET', + url: '={{ $json.name }}', + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }; + const workflowHelpers = useWorkflowHelpers({ router }); + const resolvedParameters = + workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters); + expect(resolvedParameters.url).toHaveProperty('resolvedExpressionValue'); + }); + + it('should correctly detect and resolve expressions in a node with assignments (set node) ', () => { + const nodeParameters = { + mode: 'manual', + duplicateItem: false, + assignments: { + assignments: [ + { + id: '25d2d012-089b-424d-bfc6-642982a0711f', + name: 'date', + value: + "={{ DateTime.fromFormat('2023-12-12', 'dd/MM/yyyy').toISODate().plus({7, 'days' }) }}", + type: 'number', + }, + ], + }, + includeOtherFields: false, + options: {}, + }; + const workflowHelpers = useWorkflowHelpers({ router }); + const resolvedParameters = + workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters); + expect(resolvedParameters).toHaveProperty('assignments'); + const assignments = resolvedParameters.assignments as AssignmentCollectionValue; + expect(assignments).toHaveProperty('assignments'); + expect(assignments.assignments[0].value).toHaveProperty('resolvedExpressionValue'); + }); + + it('should correctly detect and resolve expressions in a node with filter component', () => { + const nodeParameters = { + mode: 'rules', + rules: { + values: [ + { + conditions: { + options: { + caseSensitive: true, + leftValue: '', + typeValidation: 'strict', + version: 2, + }, + conditions: [ + { + leftValue: "={{ $('Edit Fields 1').item.json.name }}", + rightValue: 12, + operator: { + type: 'number', + operation: 'equals', + }, + }, + ], + combinator: 'and', + }, + renameOutput: false, + }, + ], + }, + looseTypeValidation: false, + options: {}, + }; + const workflowHelpers = useWorkflowHelpers({ router }); + const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions( + nodeParameters, + ) as typeof nodeParameters; + expect(resolvedParameters).toHaveProperty('rules'); + expect(resolvedParameters.rules).toHaveProperty('values'); + expect(resolvedParameters.rules.values[0].conditions.conditions[0].leftValue).toHaveProperty( + 'resolvedExpressionValue', + ); + }); + it('should correctly detect and resolve expressions in a node with resource locator component', () => { + const nodeParameters = { + authentication: 'oAuth2', + resource: 'sheet', + operation: 'read', + documentId: { + __rl: true, + value: "={{ $('Edit Fields').item.json.document }}", + mode: 'id', + }, + sheetName: { + __rl: true, + value: "={{ $('Edit Fields').item.json.sheet }}", + mode: 'id', + }, + filtersUI: {}, + combineFilters: 'AND', + options: {}, + }; + const workflowHelpers = useWorkflowHelpers({ router }); + const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions( + nodeParameters, + ) as typeof nodeParameters; + expect(resolvedParameters.documentId.value).toHaveProperty('resolvedExpressionValue'); + expect(resolvedParameters.sheetName.value).toHaveProperty('resolvedExpressionValue'); + }); + it('should correctly detect and resolve expressions in a node with resource mapper component', () => { + const nodeParameters = { + authentication: 'oAuth2', + resource: 'sheet', + operation: 'read', + documentId: { + __rl: true, + value: '1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc', + mode: 'list', + cachedResultName: 'Mapping sheet', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc/edit?usp=drivesdk', + }, + sheetName: { + __rl: true, + value: 'gid=0', + mode: 'list', + cachedResultName: 'Users', + cachedResultUrl: + 'https://docs.google.com/spreadsheets/d/1BAjxEhlUu5tXDCMQcjqjguIZDFuct3FYkdo7flxl3yc/edit#gid=0', + }, + filtersUI: { + values: [ + { + lookupColumn: 'First name', + lookupValue: "={{ $('Edit Fields 1').item.json.userName }}", + }, + ], + }, + combineFilters: 'AND', + options: {}, + }; + const workflowHelpers = useWorkflowHelpers({ router }); + const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions( + nodeParameters, + ) as typeof nodeParameters; + expect(resolvedParameters.filtersUI.values[0].lookupValue).toHaveProperty( + 'resolvedExpressionValue', + ); + }); + }); + describe('saveAsNewWorkflow', () => { it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => { const workflow = getDuplicateTestWorkflow(); diff --git a/packages/editor-ui/src/composables/useWorkflowHelpers.ts b/packages/editor-ui/src/composables/useWorkflowHelpers.ts index fd203a9ecc..2c79cb2639 100644 --- a/packages/editor-ui/src/composables/useWorkflowHelpers.ts +++ b/packages/editor-ui/src/composables/useWorkflowHelpers.ts @@ -693,6 +693,42 @@ export function useWorkflowHelpers(options: { router: ReturnType ({ name: ENABLED_VIEWS[0], }), ), + useRouter: vi.fn(), RouterLink: vi.fn(), })); diff --git a/packages/editor-ui/src/stores/assistant.store.ts b/packages/editor-ui/src/stores/assistant.store.ts index 0bd883ad51..0ac0346e9c 100644 --- a/packages/editor-ui/src/stores/assistant.store.ts +++ b/packages/editor-ui/src/stores/assistant.store.ts @@ -17,7 +17,7 @@ import { useRoute } from 'vue-router'; import { useSettingsStore } from './settings.store'; import { assert } from '@/utils/assert'; import { useWorkflowsStore } from './workflows.store'; -import type { ICredentialType, INodeParameters } from 'n8n-workflow'; +import type { IDataObject, ICredentialType, INodeParameters } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import { ndvEventBus, codeNodeEditorEventBus } from '@/event-bus'; import { useNDVStore } from './ndv.store'; @@ -27,7 +27,8 @@ import { getNodeAuthOptions, getReferencedNodes, getNodesSchemas, - pruneNodeProperties, + processNodeForAssistant, + isNodeReferencingInputData, } from '@/utils/nodeTypesUtils'; import { useNodeTypesStore } from './nodeTypes.store'; import { usePostHog } from './posthog.store'; @@ -421,6 +422,16 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { 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(); @@ -435,7 +446,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => { firstName: usersStore.currentUser?.firstName ?? '', }, error: context.error, - node: pruneNodeProperties(context.node, ['position']), + node: processNodeForAssistant(context.node, ['position']), + nodeInputData, executionSchema: schemas, authType, }, diff --git a/packages/editor-ui/src/types/assistant.types.ts b/packages/editor-ui/src/types/assistant.types.ts index 69749cee73..1b6ea0a089 100644 --- a/packages/editor-ui/src/types/assistant.types.ts +++ b/packages/editor-ui/src/types/assistant.types.ts @@ -1,8 +1,8 @@ import type { Schema } from '@/Interface'; -import type { INode, INodeParameters } from 'n8n-workflow'; +import type { IDataObject, INode, INodeParameters } from 'n8n-workflow'; export namespace ChatRequest { - interface NodeExecutionSchema { + export interface NodeExecutionSchema { nodeName: string; schema: Schema; } @@ -21,6 +21,7 @@ export namespace ChatRequest { stack?: string; }; node: INode; + nodeInputData?: IDataObject; } export interface InitErrorHelper extends ErrorContext, WorkflowContext { diff --git a/packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts b/packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts new file mode 100644 index 0000000000..118095ed24 --- /dev/null +++ b/packages/editor-ui/src/utils/__tests__/nodeTypesUtils.spec.ts @@ -0,0 +1,383 @@ +import { describe, it, expect } from 'vitest'; +import { getReferencedNodes } from '../nodeTypesUtils'; +import type { INode } from 'n8n-workflow'; + +const referencedNodesTestCases: Array<{ caseName: string; node: INode; expected: string[] }> = [ + { + caseName: 'Should return an empty array if no referenced nodes', + node: { + parameters: { + curlImport: '', + method: 'GET', + url: 'https://httpbin.org/get1', + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: [], + }, + { + caseName: 'Should return an array of references for regular node', + node: { + parameters: { + authentication: 'oAuth2', + resource: 'sheet', + operation: 'read', + documentId: { + __rl: true, + value: "={{ $('Edit Fields').item.json.document }}", + mode: 'id', + }, + sheetName: { + __rl: true, + value: "={{ $('Edit Fields 2').item.json.sheet }}", + mode: 'id', + }, + filtersUI: {}, + combineFilters: 'AND', + options: {}, + }, + type: 'n8n-nodes-base.googleSheets', + typeVersion: 4.5, + position: [440, 0], + id: '9a95ad27-06cf-4076-af6b-52846a109a8b', + name: 'Google Sheets', + credentials: { + googleSheetsOAuth2Api: { + id: '8QEpi028oHDLXntS', + name: 'milorad@n8n.io', + }, + }, + }, + expected: ['Edit Fields', 'Edit Fields 2'], + }, + { + caseName: 'Should return an array of references for set node', + node: { + parameters: { + mode: 'manual', + duplicateItem: false, + assignments: { + assignments: [ + { + id: '135e0eb0-f412-430d-8990-731c57cf43ae', + name: 'document', + value: "={{ $('Edit Fields 2').item.json.document}}", + type: 'string', + }, + ], + }, + includeOtherFields: false, + options: {}, + }, + type: 'n8n-nodes-base.set', + typeVersion: 3.4, + position: [560, -140], + id: '7306745f-ba8c-451d-ae1a-c627f60fbdd3', + name: 'Edit Fields 2', + }, + expected: ['Edit Fields 2'], + }, + { + caseName: 'Should handle expressions with single quotes, double quotes and backticks', + node: { + parameters: { + authentication: 'oAuth2', + resource: 'sheet', + operation: 'read', + documentId: { + __rl: true, + value: "={{ $('Edit Fields').item.json.document }}", + mode: 'id', + }, + sheetName: { + __rl: true, + value: '={{ $("Edit Fields 2").item.json.sheet }}', + mode: 'id', + }, + rowName: { + __rl: true, + value: '={{ $(`Edit Fields 3`).item.json.row }}', + mode: 'id', + }, + filtersUI: {}, + combineFilters: 'AND', + options: {}, + }, + type: 'n8n-nodes-base.googleSheets', + typeVersion: 4.5, + position: [440, 0], + id: '9a95ad27-06cf-4076-af6b-52846a109a8b', + name: 'Google Sheets', + credentials: { + googleSheetsOAuth2Api: { + id: '8QEpi028oHDLXntS', + name: 'milorad@n8n.io', + }, + }, + }, + expected: ['Edit Fields', 'Edit Fields 2', 'Edit Fields 3'], + }, + { + caseName: 'Should only add one reference for each referenced node', + node: { + parameters: { + authentication: 'oAuth2', + resource: 'sheet', + operation: 'read', + documentId: { + __rl: true, + value: "={{ $('Edit Fields').item.json.document }}", + mode: 'id', + }, + sheetName: { + __rl: true, + value: "={{ $('Edit Fields').item.json.sheet }}", + mode: 'id', + }, + filtersUI: {}, + combineFilters: 'AND', + options: {}, + }, + type: 'n8n-nodes-base.googleSheets', + typeVersion: 4.5, + position: [440, 0], + id: '9a95ad27-06cf-4076-af6b-52846a109a8b', + name: 'Google Sheets', + credentials: { + googleSheetsOAuth2Api: { + id: '8QEpi028oHDLXntS', + name: 'milorad@n8n.io', + }, + }, + }, + expected: ['Edit Fields'], + }, + { + caseName: 'Should handle multiple node references in one expression', + node: { + parameters: { + curlImport: '', + method: 'GET', + url: "={{ $('Edit Fields').item.json.one }} {{ $('Edit Fields 2').item.json.two }} {{ $('Edit Fields').item.json.three }}", + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: ['Edit Fields', 'Edit Fields 2'], + }, + { + caseName: 'Should respect whitespace around node references', + node: { + parameters: { + curlImport: '', + method: 'GET', + url: "={{ $(' Edit Fields ').item.json.one }}", + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: [' Edit Fields '], + }, + { + caseName: 'Should ignore whitespace inside expressions', + node: { + parameters: { + curlImport: '', + method: 'GET', + url: "={{ $( 'Edit Fields' ).item.json.one }}", + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: ['Edit Fields'], + }, + { + caseName: 'Should ignore special characters in node references', + node: { + parameters: { + curlImport: '', + method: 'GET', + url: "={{ $( 'Ignore ' this' ).item.json.document }", + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: [], + }, + { + caseName: 'Should correctly detect node names that contain single quotes', + node: { + parameters: { + curlImport: '', + method: 'GET', + // In order to carry over backslashes to test function, the string needs to be double escaped + url: "={{ $('Edit \\'Fields\\' 2').item.json.name }}", + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: ["Edit 'Fields' 2"], + }, + { + caseName: 'Should correctly detect node names with inner backticks', + node: { + parameters: { + curlImport: '', + method: 'GET', + url: "={{ $('Edit `Fields` 2').item.json.name }}", + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: ['Edit `Fields` 2'], + }, + { + caseName: 'Should correctly detect node names with inner escaped backticks', + node: { + parameters: { + curlImport: '', + method: 'GET', + url: '={{ $(`Edit \\`Fields\\` 2`).item.json.name }}', + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: ['Edit `Fields` 2'], + }, + { + caseName: 'Should correctly detect node names with inner escaped double quotes', + node: { + parameters: { + curlImport: '', + method: 'GET', + // In order to carry over backslashes to test function, the string needs to be double escaped + url: '={{ $("Edit \\"Fields\\" 2").item.json.name }}', + authentication: 'none', + provideSslCertificates: false, + sendQuery: false, + sendHeaders: false, + sendBody: false, + options: {}, + infoMessage: '', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: ['Edit "Fields" 2'], + }, + { + caseName: 'Should not detect invalid expressions', + node: { + parameters: { + curlImport: '', + method: 'GET', + // String not closed properly + url: "={{ $('Edit ' fields').item.json.document }", + // Mixed quotes + url2: '{{ $("Edit \'Fields" 2").item.json.name }}', + url3: '{{ $("Edit `Fields" 2").item.json.name }}', + // Quotes not escaped + url4: '{{ $("Edit "Fields" 2").item.json.name }}', + url5: "{{ $('Edit 'Fields' 2').item.json.name }}", + url6: '{{ $(`Edit `Fields` 2`).item.json.name }}', + }, + type: 'n8n-nodes-base.httpRequest', + typeVersion: 4.2, + position: [220, 220], + id: 'edc36001-aee7-4052-b66e-cf127f4b6ea5', + name: 'HTTP Request', + }, + expected: [], + }, +]; + +describe.each(referencedNodesTestCases)('getReferencedNodes', (testCase) => { + const caseName = testCase.caseName; + it(`${caseName}`, () => { + expect(getReferencedNodes(testCase.node)).toEqual(testCase.expected); + }); +}); diff --git a/packages/editor-ui/src/utils/nodeTypesUtils.ts b/packages/editor-ui/src/utils/nodeTypesUtils.ts index 70fa803411..03f930c740 100644 --- a/packages/editor-ui/src/utils/nodeTypesUtils.ts +++ b/packages/editor-ui/src/utils/nodeTypesUtils.ts @@ -5,10 +5,10 @@ import type { ITemplatesNode, IVersionNode, NodeAuthenticationOption, - Schema, SimplifiedNodeType, } from '@/Interface'; import { useDataSchema } from '@/composables/useDataSchema'; +import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { CORE_NODES_CATEGORY, MAIN_AUTH_FIELD_NAME, @@ -20,20 +20,22 @@ 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 type { - AssignmentCollectionValue, - IDataObject, - INode, - INodeCredentialDescription, - INodeExecutionData, - INodeProperties, - INodeTypeDescription, - NodeParameterValueType, - ResourceMapperField, - Themed, +import { + deepCopy, + type IDataObject, + type INode, + type INodeCredentialDescription, + type INodeExecutionData, + type INodeProperties, + type INodeTypeDescription, + type NodeParameterValueType, + type ResourceMapperField, + type Themed, } from 'n8n-workflow'; +import { useRouter } from 'vue-router'; /* Constants and utility functions mainly used to get information about @@ -503,9 +505,9 @@ export const getNodeIconColor = ( /** Regular expression to extract the node names from the expressions in the template. - Example: $(expression) => expression + Supports single quotes, double quotes, and backticks. */ -const entityRegex = /\$\((['"])(.*?)\1\)/g; +const entityRegex = /\$\(\s*(\\?["'`])((?:\\.|(?!\1)[^\\])*)\1\s*\)/g; /** * Extract the node names from the expressions in the template. @@ -520,81 +522,89 @@ function extractNodeNames(template: string): string[] { } /** - * Extract the node names from the expressions in the node parameters. + * Unescape quotes in the string. Supports single quotes, double quotes, and backticks. */ -export function getReferencedNodes(node: INode): string[] { - const referencedNodes: string[] = []; - if (!node) { - return referencedNodes; - } - // Special case for code node - if (node.type === 'n8n-nodes-base.set' && node.parameters.assignments) { - const assignments = node.parameters.assignments as AssignmentCollectionValue; - if (assignments.assignments?.length) { - assignments.assignments.forEach((assignment) => { - if (assignment.name && assignment.value && String(assignment.value).startsWith('=')) { - const nodeNames = extractNodeNames(String(assignment.value)); - if (nodeNames.length) { - referencedNodes.push(...nodeNames); - } - } - }); - } - } else { - Object.values(node.parameters).forEach((value) => { - if (!value) { - return; - } - let strValue = String(value); - // Handle resource locator - if (typeof value === 'object' && 'value' in value) { - strValue = String(value.value); - } - if (strValue.startsWith('=')) { - const nodeNames = extractNodeNames(strValue); - if (nodeNames.length) { - referencedNodes.push(...nodeNames); - } - } - }); - } - return referencedNodes; +export function unescapeQuotes(str: string): string { + return str.replace(/\\(['"`])/g, '$1'); } /** - * Remove properties from a node based on the provided list of property names. - * Reruns a new node object with the properties removed. + * Extract the node names from the expressions in the node parameters. */ -export function pruneNodeProperties(node: INode, propsToRemove: string[]): INode { - const prunedNode = { ...node }; +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 prunedNode[key as keyof INode]; + delete nodeForLLM[key as keyof INode]; }); - return prunedNode; + 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 objects containing the node name and the schema + * @returns An array of NodeExecutionSchema objects */ export function getNodesSchemas(nodeNames: string[]) { - return nodeNames.map((name) => { + const schemas: ChatRequest.NodeExecutionSchema[] = []; + for (const name of nodeNames) { const node = useWorkflowsStore().getNodeByName(name); if (!node) { - return { - nodeName: name, - schema: {} as Schema, - }; + continue; } const { getSchemaForExecutionData, getInputDataWithPinned } = useDataSchema(); - const schema = getSchemaForExecutionData( - executionDataToJson(getInputDataWithPinned(node)), - true, - ); - return { + const schema = getSchemaForExecutionData(executionDataToJson(getInputDataWithPinned(node))); + schemas.push({ nodeName: node.name, schema, - }; - }); + }); + } + return schemas; }