Skip to content

Commit

Permalink
Merge branch 'master' into ADO-1834-improve-touch-device-detection
Browse files Browse the repository at this point in the history
* master:
  fix: Fix typo with submitted (no-changelog) (#9662)
  refactor(core): Revamp crash recovery mechanism for main mode (#9613)
  fix(editor): Improve dragndrop of input pills with spaces (#9656)
  fix(editor): Indent on tabs in expression fields (#9659)
  fix(editor): Color node connections correctly in execution preview for nodes that have pinned data (#9669)
  fix(core): Fix optional chaining in continue on fail check (#9667)
  feat(OpenAI Node): Allow to select Image analyze model & improve types (#9660)
  • Loading branch information
MiloradFilipovic committed Jun 7, 2024
2 parents f8e1c50 + 67932c0 commit 11967f2
Show file tree
Hide file tree
Showing 37 changed files with 1,048 additions and 344 deletions.
3 changes: 2 additions & 1 deletion cypress/e2e/20-workflow-executions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ describe('Current Workflow Executions', () => {
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecution']);

cy.getByTestId('workflow-preview-iframe')
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body') // Access the body of the iframe document
.should('not.be.empty') // Ensure the body is not empty
Expand Down
65 changes: 65 additions & 0 deletions cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { v4 as uuid } from 'uuid';
import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { BACKEND_BASE_URL } from '../constants';

const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();

describe('ADO-2106 connections should be colored correctly for pinned data in executions preview', () => {
beforeEach(() => {
workflowPage.actions.visit();
});

beforeEach(() => {
cy.createFixtureWorkflow('Webhook_set_pinned.json', `Webhook set pinned ${uuid()}`);
workflowPage.actions.deselectAll();
workflowPage.getters.zoomToFitButton().click();

workflowPage.getters.getConnectionBetweenNodes('Webhook', 'Set').should('have.class', 'pinned');
});

it('should not color connections for pinned data nodes for production executions', () => {
workflowPage.actions.activateWorkflow();

// Execute the workflow
cy.request('POST', `${BACKEND_BASE_URL}/webhook/23fc3930-b8f9-41d9-89db-b647291a2201`, {
here: 'is some data',
}).then((response) => {
expect(response.status).to.eq(200);
});

executionsTab.actions.switchToExecutionsTab();

executionsTab.getters.successfulExecutionListItems().should('have.length', 1);

executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`)
.should('have.class', 'success')
.should('have.class', 'has-run')
.should('not.have.class', 'pinned');
});

it('should color connections for pinned data nodes for manual executions', () => {
workflowPage.actions.executeWorkflow();

executionsTab.actions.switchToExecutionsTab();

executionsTab.getters.successfulExecutionListItems().should('have.length', 1);

executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`)
.should('have.class', 'success')
.should('have.class', 'has-run')
.should('have.class', 'pinned');
});
});
67 changes: 67 additions & 0 deletions cypress/fixtures/Webhook_set_pinned.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"nodes": [
{
"parameters": {
"options": {}
},
"id": "bd816131-d8ad-4b4c-90d6-59fdab2e6307",
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [
720,
460
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "23fc3930-b8f9-41d9-89db-b647291a2201",
"options": {}
},
"id": "82fe0f6c-854a-4eb9-b311-d7b43025c047",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
460,
460
],
"webhookId": "23fc3930-b8f9-41d9-89db-b647291a2201"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Set",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Webhook": [
{
"headers": {
"host": "localhost:5678",
"content-length": "37",
"accept": "*/*",
"content-type": "application/json",
"accept-encoding": "gzip"
},
"params": {},
"query": {},
"body": {
"here": "be",
"dragons": true
},
"webhookUrl": "http://localhost:5678/webhook-test/23fc3930-b8f9-41d9-89db-b647291a2201",
"executionMode": "test"
}
]
}
}
1 change: 1 addition & 0 deletions cypress/pages/workflow-executions-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class WorkflowExecutionsTab extends BasePage {
executionPreviewId: () =>
this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-id"]'),
executionDebugButton: () => cy.getByTestId('execution-debug-button'),
workflowExecutionPreviewIframe: () => cy.getByTestId('workflow-preview-iframe'),
};
actions = {
toggleNodeEnabled: (nodeName: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { apiRequest } from '../../transport';
import { modelRLC } from '../descriptions';

const properties: INodeProperties[] = [
modelRLC,
modelRLC('modelSearch'),
{
displayName: 'Name',
name: 'name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const properties: INodeProperties[] = [
description:
'Whether to augments the assistant with knowledge from outside its model, such as proprietary product information or documents, find more <a href="https://platform.openai.com/docs/assistants/tools/knowledge-retrieval" target="_blank">here</a>',
},
{ ...modelRLC, required: false },
{ ...modelRLC('modelSearch'), required: false },
{
displayName: 'Name',
name: 'name',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { INodeProperties } from 'n8n-workflow';

export const modelRLC: INodeProperties = {
export const modelRLC = (searchListMethod: string = 'modelSearch'): INodeProperties => ({
displayName: 'Model',
name: 'modelId',
type: 'resourceLocator',
Expand All @@ -12,7 +12,7 @@ export const modelRLC: INodeProperties = {
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'modelSearch',
searchListMethod,
searchable: true,
},
},
Expand All @@ -23,7 +23,7 @@ export const modelRLC: INodeProperties = {
placeholder: 'e.g. gpt-4',
},
],
};
});

export const assistantRLC: INodeProperties = {
displayName: 'Assistant',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import type {
} from 'n8n-workflow';
import { updateDisplayOptions, NodeOperationError } from 'n8n-workflow';
import { apiRequest } from '../../transport';
import { modelRLC } from '../descriptions';

const properties: INodeProperties[] = [
{
...modelRLC('imageModelSearch'),
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.4 } }] } },
},
{
displayName: 'Text Input',
name: 'text',
Expand Down Expand Up @@ -123,7 +128,11 @@ const displayOptions = {
export const description = updateDisplayOptions(displayOptions, properties);

export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const model = 'gpt-4-vision-preview';
let model = 'gpt-4-vision-preview';
if (this.getNode().typeVersion >= 1.4) {
model = this.getNodeParameter('modelId', i, 'gpt-4o', { extractValue: true }) as string;
}

const text = this.getNodeParameter('text', i, '') as string;
const inputType = this.getNodeParameter('inputType', i) as string;
const options = this.getNodeParameter('options', i, {});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getConnectedTools } from '../../../../../utils/helpers';
import { MODELS_NOT_SUPPORT_FUNCTION_CALLS } from '../../helpers/constants';

const properties: INodeProperties[] = [
modelRLC,
modelRLC('modelSearch'),
{
displayName: 'Messages',
name: 'messages',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const versionDescription: INodeTypeDescription = {
name: 'openAi',
icon: { light: 'file:openAi.svg', dark: 'file:openAi.dark.svg' },
group: ['transform'],
version: [1, 1.1, 1.2, 1.3],
version: [1, 1.1, 1.2, 1.3, 1.4],
subtitle: `={{(${prettifyOperation})($parameter.resource, $parameter.operation)}}`,
description: 'Message an assistant or GPT, analyze images, generate audio, etc.',
defaults: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
INodeListSearchResult,
} from 'n8n-workflow';

import type { Model } from 'openai/resources/models';
import type { Assistant } from 'openai/resources/beta/assistants';
import { apiRequest } from '../transport';

export async function fileSearch(
Expand Down Expand Up @@ -38,55 +40,75 @@ export async function fileSearch(
}
}

export async function modelSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
let { data } = await apiRequest.call(this, 'GET', '/models');
const getModelSearch =
(filterCondition: (model: Model) => boolean) =>
async (ctx: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> => {
let { data } = (await apiRequest.call(ctx, 'GET', '/models')) as { data: Model[] };

data = data?.filter((model: IDataObject) => (model.id as string).startsWith('gpt-'));
data = data?.filter((model) => filterCondition(model));

let results: INodeListSearchItems[] = [];
let results: INodeListSearchItems[] = [];

if (filter) {
for (const model of data || []) {
if ((model.id as string)?.toLowerCase().includes(filter.toLowerCase())) {
results.push({
name: (model.id as string).toUpperCase(),
value: model.id as string,
});
if (filter) {
for (const model of data || []) {
if (model.id?.toLowerCase().includes(filter.toLowerCase())) {
results.push({
name: model.id.toUpperCase(),
value: model.id,
});
}
}
} else {
results = (data || []).map((model) => ({
name: model.id.toUpperCase(),
value: model.id,
}));
}
} else {
results = (data || []).map((model: IDataObject) => ({
name: (model.id as string).toUpperCase(),
value: model.id as string,
}));
}

results = results.sort((a, b) => a.name.localeCompare(b.name));
results = results.sort((a, b) => a.name.localeCompare(b.name));

return {
results,
return {
results,
};
};

export async function modelSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
return await getModelSearch((model) => model.id.startsWith('gpt-'))(this, filter);
}

export async function imageModelSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
return await getModelSearch(
(model) => model.id.includes('vision') || model.id.includes('gpt-4o'),
)(this, filter);
}

export async function assistantSearch(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const { data, has_more, last_id } = await apiRequest.call(this, 'GET', '/assistants', {
const { data, has_more, last_id } = (await apiRequest.call(this, 'GET', '/assistants', {
headers: {
'OpenAI-Beta': 'assistants=v2',
},
qs: {
limit: 100,
after: paginationToken,
},
});
})) as {
data: Assistant[];
has_more: boolean;
last_id: string;
first_id: string;
};

if (has_more === true) {
if (has_more) {
paginationToken = last_id;
} else {
paginationToken = undefined;
Expand All @@ -96,10 +118,10 @@ export async function assistantSearch(
const results: INodeListSearchItems[] = [];

for (const assistant of data || []) {
if ((assistant.name as string)?.toLowerCase().includes(filter.toLowerCase())) {
if (assistant.name?.toLowerCase().includes(filter.toLowerCase())) {
results.push({
name: assistant.name as string,
value: assistant.id as string,
name: assistant.name,
value: assistant.id,
});
}
}
Expand All @@ -109,9 +131,9 @@ export async function assistantSearch(
};
} else {
return {
results: (data || []).map((assistant: IDataObject) => ({
name: assistant.name as string,
value: assistant.id as string,
results: (data || []).map((assistant) => ({
name: assistant.name ?? assistant.id,
value: assistant.id,
})),
paginationToken,
};
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,14 @@ export const GENERIC_OAUTH2_CREDENTIALS_WITH_EDITABLE_SCOPE = [
'microsoftOAuth2Api',
'highLevelOAuth2Api',
];

export const ARTIFICIAL_TASK_DATA = {
main: [
[
{
json: { isArtificialRecoveredEventItem: true },
pairedItem: undefined,
},
],
],
};
Loading

0 comments on commit 11967f2

Please sign in to comment.