Skip to content

Commit

Permalink
fix(editor): Add missing node parameter values to AI Assistant request (
Browse files Browse the repository at this point in the history
  • Loading branch information
MiloradFilipovic authored Sep 17, 2024
1 parent a3335e0 commit d65ade4
Show file tree
Hide file tree
Showing 7 changed files with 672 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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();
Expand Down
37 changes: 37 additions & 0 deletions packages/editor-ui/src/composables/useWorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,42 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflowId, node, path, isFullPath);
}

/**
* Returns a copy of provided node parameters with added resolvedExpressionValue
* @param nodeParameters
* @returns
*/
function getNodeParametersWithResolvedExpressions(
nodeParameters: INodeParameters,
): INodeParameters {
function recurse(currentObj: INodeParameters, currentPath: string): INodeParameters {
const newObj: INodeParameters = {};
for (const key in currentObj) {
const value = currentObj[key as keyof typeof currentObj];
const path = currentPath ? `${currentPath}.${key}` : key;
if (typeof value === 'object' && value !== null) {
newObj[key] = recurse(value as INodeParameters, path);
} else if (typeof value === 'string' && String(value).startsWith('=')) {
// Resolve the expression if it is one
let resolved;
try {
resolved = resolveExpression(value, undefined, { isForCredential: false });
} catch (error) {
resolved = `Error in expression: "${error.message}"`;
}
newObj[key] = {
value,
resolvedExpressionValue: String(resolved),
};
} else {
newObj[key] = value;
}
}
return newObj;
}
return recurse(nodeParameters, '');
}

function resolveExpression(
expression: string,
siblingParameters: INodeParameters = {},
Expand Down Expand Up @@ -1159,5 +1195,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
getWorkflowProjectRole,
promptSaveUnsavedWorkflowChanges,
initState,
getNodeParametersWithResolvedExpressions,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ vi.mock('vue-router', () => ({
name: ENABLED_VIEWS[0],
}),
),
useRouter: vi.fn(),
RouterLink: vi.fn(),
}));

Expand Down
18 changes: 15 additions & 3 deletions packages/editor-ui/src/stores/assistant.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,7 +27,8 @@ import {
getNodeAuthOptions,
getReferencedNodes,
getNodesSchemas,
pruneNodeProperties,
processNodeForAssistant,
isNodeReferencingInputData,
} from '@/utils/nodeTypesUtils';
import { useNodeTypesStore } from './nodeTypes.store';
import { usePostHog } from './posthog.store';
Expand Down Expand Up @@ -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();

Expand All @@ -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,
},
Expand Down
5 changes: 3 additions & 2 deletions packages/editor-ui/src/types/assistant.types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -21,6 +21,7 @@ export namespace ChatRequest {
stack?: string;
};
node: INode;
nodeInputData?: IDataObject;
}

export interface InitErrorHelper extends ErrorContext, WorkflowContext {
Expand Down
Loading

0 comments on commit d65ade4

Please sign in to comment.