feat(OpenAI Node): Add a node to work with OpenAI (#4932)

* feat(OpenAI Node): Add a node to work with OpenAI

* Added codex file for OpenAi node

* Minor tweaks to Operation Image.

* Minor tweaks to Resource Text.

* Minor copy modification to Image:Create.

* Removed "a Text" in Text operations names.

*  Connect Response Format parameter and other improvements

*  Add "filter" postReceiveAction

*  Rename operations and add spelling mistake again to example

*  Rename another operation

Co-authored-by: Jonathan Bennetts <jonathan.bennetts@gmail.com>
Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
Jan Oberhauser 2022-12-15 18:05:42 -06:00 committed by GitHub
parent 3028ad3c61
commit 7a984bb6b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 821 additions and 2 deletions

View file

@ -0,0 +1,50 @@
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class OpenAiApi implements ICredentialType {
name = 'openAiApi';
displayName = 'OpenAi';
documentationUrl = 'openAiApi';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
required: true,
default: '',
},
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'string',
default: '',
description:
"For users who belong to multiple organizations, you can set which organization is used for an API request. Usage from these API requests will count against the specified organization's subscription quota.",
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.apiKey}}',
'OpenAI-Organization': '={{$credentials.organizationId}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.openai.com',
url: '/v1/models',
},
};
}

View file

@ -0,0 +1,186 @@
import { INodeExecutionData, INodeProperties } from 'n8n-workflow';
export const imageOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['image'],
},
},
options: [
{
name: 'Create',
value: 'create',
action: 'Create an Image',
description: 'Create an image for a given text',
routing: {
request: {
method: 'POST',
url: '/v1/images/generations',
},
},
},
],
routing: {
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
],
},
},
default: 'create',
},
];
const createOperations: INodeProperties[] = [
{
displayName: 'Prompt',
name: 'prompt',
type: 'string',
placeholder: 'e.g. A cute cat eating a dinosaur',
description:
'A text description of the desired image(s). The maximum length is 1000 characters.',
displayOptions: {
show: {
resource: ['image'],
operation: ['create'],
},
},
default: '',
routing: {
send: {
type: 'body',
property: 'prompt',
},
},
},
{
displayName: 'Response Format',
name: 'responseFormat',
type: 'options',
default: 'binaryData',
description: 'The format in which to return the image(s)',
displayOptions: {
show: {
resource: ['image'],
operation: ['create'],
},
},
options: [
{
name: 'Binary Data',
value: 'binaryData',
},
{
name: 'Image Url',
value: 'imageUrl',
},
],
routing: {
send: {
type: 'body',
property: 'response_format',
value: '={{ $value === "imageUrl" ? "url" : "b64_json" }}',
},
output: {
postReceive: [
async function (items: INodeExecutionData[]): Promise<INodeExecutionData[]> {
if (this.getNode().parameters.responseFormat === 'imageUrl') {
return items;
}
const result: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
result.push({
json: {},
binary: {
data: await this.helpers.prepareBinaryData(
Buffer.from(items[i].json.b64_json as string, 'base64'),
'data',
),
},
} as INodeExecutionData);
}
return result;
},
],
},
},
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
description: 'Additional options to add',
type: 'collection',
default: {},
displayOptions: {
show: {
resource: ['image'],
operation: ['create'],
},
},
options: [
{
displayName: 'Number of Images',
name: 'n',
default: 1,
description: 'Number of images to generate',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 10,
},
routing: {
send: {
type: 'body',
property: 'n',
},
},
},
{
displayName: 'Resolution',
name: 'size',
type: 'options',
options: [
{
name: '256x256',
value: '256x256',
},
{
name: '512x512',
value: '512x512',
},
{
name: '1024x1024',
value: '1024x1024',
},
],
routing: {
send: {
type: 'body',
property: 'size',
},
},
default: '1024x1024',
},
],
},
];
export const imageFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* image:create */
/* -------------------------------------------------------------------------- */
...createOperations,
];

View file

@ -0,0 +1,19 @@
{
"node": "n8n-nodes-base.openAi",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Utility"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/openAiApi"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.openai/"
}
]
},
"alias": ["ChatGPT", "DallE"]
}

View file

@ -0,0 +1,54 @@
import { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { imageFields, imageOperations } from './ImageDescription';
import { textFields, textOperations } from './TextDescription';
export class OpenAi implements INodeType {
description: INodeTypeDescription = {
displayName: 'OpenAI',
name: 'openAi',
icon: 'file:openAi.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Open AI',
defaults: {
name: 'OpenAI',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'openAiApi',
required: true,
},
],
requestDefaults: {
baseURL: 'https://api.openai.com',
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Image',
value: 'image',
},
{
name: 'Text',
value: 'text',
},
],
default: 'text',
},
...imageOperations,
...imageFields,
...textOperations,
...textFields,
],
};
}

View file

@ -0,0 +1,472 @@
import { INodeExecutionData, INodeProperties } from 'n8n-workflow';
export const textOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['text'],
},
},
options: [
{
name: 'Complete',
value: 'complete',
action: 'Create a Completion',
description: 'Create one or more completions for a given text',
routing: {
request: {
method: 'POST',
url: '/v1/completions',
},
},
},
{
name: 'Edit',
value: 'edit',
action: 'Create an Edit',
description: 'Create an edited version for a given text',
routing: {
request: {
method: 'POST',
url: '/v1/edits',
},
},
},
{
name: 'Moderate',
value: 'moderate',
action: 'Create a Moderation',
description: "Classify if a text violates OpenAI's content policy",
routing: {
request: {
method: 'POST',
url: '/v1/moderations',
},
},
},
],
default: 'complete',
},
];
const completeOperations: INodeProperties[] = [
{
displayName: 'Model',
name: 'model',
type: 'options',
description:
'The model which will generate the completion. <a href="https://beta.openai.com/docs/models/overview">Learn more</a>.',
displayOptions: {
show: {
operation: ['complete'],
resource: ['text'],
},
},
typeOptions: {
loadOptions: {
routing: {
request: {
method: 'GET',
url: '/v1/models',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
{
type: 'filter',
properties: {
pass: "={{ !$responseItem.id.startsWith('audio-') && !['cushman:2020-05-03', 'davinci-if:3.0.0', 'davinci-instruct-beta:2.0.0', 'if'].includes($responseItem.id) && !$responseItem.id.includes('-edit-') && !$responseItem.id.endsWith(':001') }}",
},
},
{
type: 'setKeyValue',
properties: {
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased-id
name: '={{$responseItem.id}}',
value: '={{$responseItem.id}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
routing: {
send: {
type: 'body',
property: 'model',
},
},
default: 'text-davinci-003',
},
{
displayName: 'Prompt',
name: 'prompt',
type: 'string',
description: 'The prompt to generate completion(s) for',
placeholder: 'e.g. Say this is a test',
displayOptions: {
show: {
resource: ['text'],
operation: ['complete'],
},
},
default: '',
typeOptions: {
rows: 2,
},
routing: {
send: {
type: 'body',
property: 'prompt',
},
},
},
];
const editOperations: INodeProperties[] = [
{
displayName: 'Model',
name: 'model',
type: 'options',
description:
'The model which will generate the edited version. <a href="https://beta.openai.com/docs/models/overview">Learn more</a>.',
displayOptions: {
show: {
operation: ['edit'],
resource: ['text'],
},
},
options: [
{
name: 'code-davinci-edit-001',
value: 'code-davinci-edit-001',
},
{
name: 'text-davinci-edit-001',
value: 'text-davinci-edit-001',
},
],
routing: {
send: {
type: 'body',
property: 'model',
},
},
default: 'text-davinci-edit-001',
},
{
displayName: 'Input',
name: 'input',
type: 'string',
placeholder: 'e.g. What day of the wek is it?',
description: 'The input text to be edited',
displayOptions: {
show: {
resource: ['text'],
operation: ['edit'],
},
},
default: '',
routing: {
send: {
type: 'body',
property: 'input',
},
},
},
{
displayName: 'Instruction',
name: 'instruction',
type: 'string',
placeholder: 'e.g. Fix the spelling mistakes',
description: 'The instruction that tells the model how to edit the input text',
displayOptions: {
show: {
resource: ['text'],
operation: ['edit'],
},
},
default: '',
routing: {
send: {
type: 'body',
property: 'instruction',
},
},
},
];
const moderateOperations: INodeProperties[] = [
{
displayName: 'Model',
name: 'model',
type: 'options',
description:
'The model which will classify the text. <a href="https://beta.openai.com/docs/models/overview">Learn more</a>.',
displayOptions: {
show: {
resource: ['text'],
operation: ['moderate'],
},
},
options: [
{
name: 'text-moderation-stable',
value: 'text-moderation-stable',
},
{
name: 'text-moderation-latest',
value: 'text-moderation-latest',
},
],
routing: {
send: {
type: 'body',
property: 'model',
},
},
default: 'text-moderation-latest',
},
{
displayName: 'Input',
name: 'input',
type: 'string',
placeholder: 'e.g. I want to kill them',
description: 'The input text to classify',
displayOptions: {
show: {
resource: ['text'],
operation: ['moderate'],
},
},
default: '',
routing: {
send: {
type: 'body',
property: 'input',
},
},
},
{
displayName: 'Simplify',
name: 'simplifyOutput',
type: 'boolean',
default: true,
displayOptions: {
show: {
operation: ['moderate'],
resource: ['text'],
},
},
routing: {
output: {
postReceive: [
{
type: 'set',
enabled: '={{$value}}',
properties: {
value: '={{ { "data": $response.body.results } }}',
},
},
{
type: 'rootProperty',
enabled: '={{$value}}',
properties: {
property: 'data',
},
},
],
},
},
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];
const sharedOperations: INodeProperties[] = [
{
displayName: 'Simplify',
name: 'simplifyOutput',
type: 'boolean',
default: true,
displayOptions: {
show: {
operation: ['complete', 'edit'],
resource: ['text'],
},
},
routing: {
output: {
postReceive: [
{
type: 'set',
enabled: '={{$value}}',
properties: {
value: '={{ { "data": $response.body.choices } }}',
},
},
{
type: 'rootProperty',
enabled: '={{$value}}',
properties: {
property: 'data',
},
},
async function (items: INodeExecutionData[]): Promise<INodeExecutionData[]> {
if (this.getNode().parameters.simplifyOutput === false) {
return items;
}
return items.map((item) => {
return {
json: {
...item.json,
text: (item.json.text as string).trim(),
},
};
});
},
],
},
},
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
description: 'Additional options to add',
type: 'collection',
default: {},
displayOptions: {
show: {
operation: ['complete', 'edit'],
resource: ['text'],
},
},
options: [
{
displayName: 'Echo Prompt',
name: 'echo',
type: 'boolean',
description: 'Whether the prompt should be echo back in addition to the completion',
default: false,
displayOptions: {
show: {
'/operation': ['complete'],
},
},
routing: {
send: {
type: 'body',
property: 'echo',
},
},
},
{
displayName: 'Maximum Number of Tokens',
name: 'maxTokens',
default: 16,
description:
'The maximum number of tokens to generate in the completion. Most models have a context length of 2048 tokens (except for the newest models, which support 4096).',
type: 'number',
displayOptions: {
show: {
'/operation': ['complete'],
},
},
typeOptions: {
maxValue: 4096,
},
routing: {
send: {
type: 'body',
property: 'max_tokens',
},
},
},
{
displayName: 'Number of Completions',
name: 'n',
default: 1,
description:
'How many completions to generate for each prompt. Note: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop.',
type: 'number',
routing: {
send: {
type: 'body',
property: 'n',
},
},
},
{
displayName: 'Sampling Temperature',
name: 'temperature',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.',
type: 'number',
routing: {
send: {
type: 'body',
property: 'temperature',
},
},
},
{
displayName: 'Top P',
name: 'topP',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
type: 'number',
routing: {
send: {
type: 'body',
property: 'top_p',
},
},
},
],
},
];
export const textFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* text:complete */
/* -------------------------------------------------------------------------- */
...completeOperations,
/* -------------------------------------------------------------------------- */
/* text:edit */
/* -------------------------------------------------------------------------- */
...editOperations,
/* -------------------------------------------------------------------------- */
/* text:moderate */
/* -------------------------------------------------------------------------- */
...moderateOperations,
/* -------------------------------------------------------------------------- */
/* text:ALL */
/* -------------------------------------------------------------------------- */
...sharedOperations,
];

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="260px" viewBox="0 0 256 260" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<title>OpenAI</title>
<g>
<path d="M239.183914,106.202783 C245.054304,88.5242096 243.02228,69.1733805 233.607599,53.0998864 C219.451678,28.4588021 190.999703,15.7836129 163.213007,21.739505 C147.554077,4.32145883 123.794909,-3.42398554 100.87901,1.41873898 C77.9631105,6.26146349 59.3690093,22.9572536 52.0959621,45.2214219 C33.8436494,48.9644867 18.0901721,60.392749 8.86672513,76.5818033 C-5.443491,101.182962 -2.19544431,132.215255 16.8986662,153.320094 C11.0060865,170.990656 13.0197283,190.343991 22.4238231,206.422991 C36.5975553,231.072344 65.0680342,243.746566 92.8695738,237.783372 C105.235639,251.708249 123.001113,259.630942 141.623968,259.52692 C170.105359,259.552169 195.337611,241.165718 204.037777,214.045661 C222.28734,210.296356 238.038489,198.869783 247.267014,182.68528 C261.404453,158.127515 258.142494,127.262775 239.183914,106.202783 L239.183914,106.202783 Z M141.623968,242.541207 C130.255682,242.559177 119.243876,238.574642 110.519381,231.286197 L112.054146,230.416496 L163.724595,200.590881 C166.340648,199.056444 167.954321,196.256818 167.970781,193.224005 L167.970781,120.373788 L189.815614,133.010026 C190.034132,133.121423 190.186235,133.330564 190.224885,133.572774 L190.224885,193.940229 C190.168603,220.758427 168.442166,242.484864 141.623968,242.541207 Z M37.1575749,197.93062 C31.456498,188.086359 29.4094818,176.546984 31.3766237,165.342426 L32.9113895,166.263285 L84.6329973,196.088901 C87.2389349,197.618207 90.4682717,197.618207 93.0742093,196.088901 L156.255402,159.663793 L156.255402,184.885111 C156.243557,185.149771 156.111725,185.394602 155.89729,185.550176 L103.561776,215.733903 C80.3054953,229.131632 50.5924954,221.165435 37.1575749,197.93062 Z M23.5493181,85.3811273 C29.2899861,75.4733097 38.3511911,67.9162648 49.1287482,64.0478825 L49.1287482,125.438515 C49.0891492,128.459425 50.6965386,131.262556 53.3237748,132.754232 L116.198014,169.025864 L94.3531808,181.662102 C94.1132325,181.789434 93.8257461,181.789434 93.5857979,181.662102 L41.3526015,151.529534 C18.1419426,138.076098 10.1817681,108.385562 23.5493181,85.125333 L23.5493181,85.3811273 Z M203.0146,127.075598 L139.935725,90.4458545 L161.7294,77.8607748 C161.969348,77.7334434 162.256834,77.7334434 162.496783,77.8607748 L214.729979,108.044502 C231.032329,117.451747 240.437294,135.426109 238.871504,154.182739 C237.305714,172.939368 225.050719,189.105572 207.414262,195.67963 L207.414262,134.288998 C207.322521,131.276867 205.650697,128.535853 203.0146,127.075598 Z M224.757116,94.3850867 L223.22235,93.4642272 L171.60306,63.3828173 C168.981293,61.8443751 165.732456,61.8443751 163.110689,63.3828173 L99.9806554,99.8079259 L99.9806554,74.5866077 C99.9533004,74.3254088 100.071095,74.0701869 100.287609,73.9215426 L152.520805,43.7889738 C168.863098,34.3743518 189.174256,35.2529043 204.642579,46.0434841 C220.110903,56.8340638 227.949269,75.5923959 224.757116,94.1804513 L224.757116,94.3850867 Z M88.0606409,139.097931 L66.2158076,126.512851 C65.9950399,126.379091 65.8450965,126.154176 65.8065367,125.898945 L65.8065367,65.684966 C65.8314495,46.8285367 76.7500605,29.6846032 93.8270852,21.6883055 C110.90411,13.6920079 131.063833,16.2835462 145.5632,28.338998 L144.028434,29.2086986 L92.3579852,59.0343142 C89.7419327,60.5687513 88.1282597,63.3683767 88.1117998,66.4011901 L88.0606409,139.097931 Z M99.9294965,113.5185 L128.06687,97.3011417 L156.255402,113.5185 L156.255402,145.953218 L128.169187,162.170577 L99.9806554,145.953218 L99.9294965,113.5185 Z" fill="#000000"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -224,6 +224,7 @@
"dist/credentials/OdooApi.credentials.js",
"dist/credentials/OneSimpleApi.credentials.js",
"dist/credentials/OnfleetApi.credentials.js",
"dist/credentials/OpenAiApi.credentials.js",
"dist/credentials/OpenWeatherMapApi.credentials.js",
"dist/credentials/OrbitApi.credentials.js",
"dist/credentials/OuraApi.credentials.js",
@ -589,6 +590,7 @@
"dist/nodes/Notion/NotionTrigger.node.js",
"dist/nodes/Odoo/Odoo.node.js",
"dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
"dist/nodes/OpenAi/OpenAi.node.js",
"dist/nodes/OpenThesaurus/OpenThesaurus.node.js",
"dist/nodes/OpenWeatherMap/OpenWeatherMap.node.js",
"dist/nodes/Orbit/Orbit.node.js",

View file

@ -1296,6 +1296,7 @@ export type PostReceiveAction =
response: IN8nHttpFullResponse,
) => Promise<INodeExecutionData[]>)
| IPostReceiveBinaryData
| IPostReceiveFilter
| IPostReceiveLimit
| IPostReceiveRootProperty
| IPostReceiveSet
@ -1325,7 +1326,7 @@ export interface IPostReceiveBase {
type: string;
enabled?: boolean | string;
properties: {
[key: string]: string | number | IDataObject;
[key: string]: string | number | boolean | IDataObject;
};
errorMessage?: string;
}
@ -1337,6 +1338,13 @@ export interface IPostReceiveBinaryData extends IPostReceiveBase {
};
}
export interface IPostReceiveFilter extends IPostReceiveBase {
type: 'filter';
properties: {
pass: boolean | string;
};
}
export interface IPostReceiveLimit extends IPostReceiveBase {
type: 'limit';
properties: {

View file

@ -265,7 +265,6 @@ export class RoutingNode {
if (action.type === 'rootProperty') {
try {
return inputData.flatMap((item) => {
// let itemContent = item.json[action.properties.property];
let itemContent = get(item.json, action.properties.property);
if (!Array.isArray(itemContent)) {
@ -285,6 +284,28 @@ export class RoutingNode {
});
}
}
if (action.type === 'filter') {
const passValue = action.properties.pass;
inputData = inputData.filter((item) => {
// If the value is an expression resolve it
return this.getParameterValue(
passValue,
itemIndex,
runIndex,
executeSingleFunctions.getExecuteData(),
{
$response: responseData,
$responseItem: item.json,
$value: parameterValue,
$version: this.node.typeVersion,
},
false,
) as boolean;
});
return inputData;
}
if (action.type === 'limit') {
const maxResults = this.getParameterValue(
action.properties.maxResults,