first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
import type {
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Model',
|
||||
name: 'model',
|
||||
type: 'options',
|
||||
default: 'tts-1',
|
||||
options: [
|
||||
{
|
||||
name: 'TTS-1',
|
||||
value: 'tts-1',
|
||||
},
|
||||
{
|
||||
name: 'TTS-1-HD',
|
||||
value: 'tts-1-hd',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Text Input',
|
||||
name: 'input',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. The quick brown fox jumped over the lazy dog',
|
||||
description: 'The text to generate audio for. The maximum length is 4096 characters.',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Voice',
|
||||
name: 'voice',
|
||||
type: 'options',
|
||||
default: 'alloy',
|
||||
description: 'The voice to use when generating the audio',
|
||||
options: [
|
||||
{
|
||||
name: 'Alloy',
|
||||
value: 'alloy',
|
||||
},
|
||||
{
|
||||
name: 'Echo',
|
||||
value: 'echo',
|
||||
},
|
||||
{
|
||||
name: 'Fable',
|
||||
value: 'fable',
|
||||
},
|
||||
{
|
||||
name: 'Nova',
|
||||
value: 'nova',
|
||||
},
|
||||
{
|
||||
name: 'Onyx',
|
||||
value: 'onyx',
|
||||
},
|
||||
{
|
||||
name: 'Shimmer',
|
||||
value: 'shimmer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Response Format',
|
||||
name: 'response_format',
|
||||
type: 'options',
|
||||
default: 'mp3',
|
||||
options: [
|
||||
{
|
||||
name: 'MP3',
|
||||
value: 'mp3',
|
||||
},
|
||||
{
|
||||
name: 'OPUS',
|
||||
value: 'opus',
|
||||
},
|
||||
{
|
||||
name: 'AAC',
|
||||
value: 'aac',
|
||||
},
|
||||
{
|
||||
name: 'FLAC',
|
||||
value: 'flac',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Audio Speed',
|
||||
name: 'speed',
|
||||
type: 'number',
|
||||
default: 1,
|
||||
typeOptions: {
|
||||
minValue: 0.25,
|
||||
maxValue: 4,
|
||||
numberPrecision: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Put Output in Field',
|
||||
name: 'binaryPropertyOutput',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the output field to put the binary file data in',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['generate'],
|
||||
resource: ['audio'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('model', i) as string;
|
||||
const input = this.getNodeParameter('input', i) as string;
|
||||
const voice = this.getNodeParameter('voice', i) as string;
|
||||
let response_format = 'mp3';
|
||||
let speed = 1;
|
||||
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
if (options.response_format) {
|
||||
response_format = options.response_format as string;
|
||||
}
|
||||
|
||||
if (options.speed) {
|
||||
speed = options.speed as number;
|
||||
}
|
||||
|
||||
const body: IDataObject = {
|
||||
model,
|
||||
input,
|
||||
voice,
|
||||
response_format,
|
||||
speed,
|
||||
};
|
||||
|
||||
const option = {
|
||||
useStream: true,
|
||||
returnFullResponse: true,
|
||||
encoding: 'arraybuffer',
|
||||
json: false,
|
||||
};
|
||||
|
||||
const response = await apiRequest.call(this, 'POST', '/audio/speech', { body, option });
|
||||
|
||||
const binaryData = await this.helpers.prepareBinaryData(
|
||||
response,
|
||||
`audio.${response_format}`,
|
||||
`audio/${response_format}`,
|
||||
);
|
||||
|
||||
const binaryPropertyOutput = (options.binaryPropertyOutput as string) || 'data';
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {
|
||||
...binaryData,
|
||||
data: undefined,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
binary: {
|
||||
[binaryPropertyOutput]: binaryData,
|
||||
},
|
||||
};
|
||||
|
||||
return [newItem];
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as generate from './generate.operation';
|
||||
import * as transcribe from './transcribe.operation';
|
||||
import * as translate from './translate.operation';
|
||||
|
||||
export { generate, transcribe, translate };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Generate Audio',
|
||||
value: 'generate',
|
||||
action: 'Generate audio',
|
||||
description: 'Creates audio from a text prompt',
|
||||
},
|
||||
{
|
||||
name: 'Transcribe a Recording',
|
||||
value: 'transcribe',
|
||||
action: 'Transcribe a recording',
|
||||
description: 'Transcribes audio into text',
|
||||
},
|
||||
{
|
||||
name: 'Translate a Recording',
|
||||
value: 'translate',
|
||||
action: 'Translate a recording',
|
||||
description: 'Translates audio into text in English',
|
||||
},
|
||||
],
|
||||
default: 'generate',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['audio'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'OpenAI API limits the size of the audio file to 25 MB',
|
||||
name: 'fileSizeLimitNotice',
|
||||
type: 'notice',
|
||||
default: ' ',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['audio'],
|
||||
operation: ['translate', 'transcribe'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...generate.description,
|
||||
...transcribe.description,
|
||||
...translate.description,
|
||||
];
|
||||
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
import FormData from 'form-data';
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { getBinaryDataFile } from '../../../helpers/binary-data';
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description:
|
||||
'Name of the binary property which contains the audio file in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Language of the Audio File',
|
||||
name: 'language',
|
||||
type: 'string',
|
||||
description:
|
||||
'The language of the input audio. Supplying the input language in <a href="https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes" target="_blank">ISO-639-1</a> format will improve accuracy and latency.',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Randomness (Temperature)',
|
||||
name: 'temperature',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
numberPrecision: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['transcribe'],
|
||||
resource: ['audio'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = 'whisper-1';
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('model', model);
|
||||
|
||||
if (options.language) {
|
||||
formData.append('language', options.language);
|
||||
}
|
||||
|
||||
if (options.temperature) {
|
||||
formData.append('temperature', options.temperature.toString());
|
||||
}
|
||||
|
||||
const { filename, contentType, fileContent } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
binaryPropertyName,
|
||||
);
|
||||
formData.append('file', fileContent, {
|
||||
filename,
|
||||
contentType,
|
||||
});
|
||||
|
||||
const response = await apiRequest.call(this, 'POST', '/audio/transcriptions', {
|
||||
option: { formData },
|
||||
headers: formData.getHeaders(),
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import FormData from 'form-data';
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { getBinaryDataFile } from '../../../helpers/binary-data';
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
placeholder: 'e.g. data',
|
||||
description:
|
||||
'Name of the binary property which contains the audio file in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Output Randomness (Temperature)',
|
||||
name: 'temperature',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
numberPrecision: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['translate'],
|
||||
resource: ['audio'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = 'whisper-1';
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('model', model);
|
||||
|
||||
if (options.temperature) {
|
||||
formData.append('temperature', options.temperature.toString());
|
||||
}
|
||||
|
||||
const { filename, contentType, fileContent } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
binaryPropertyName,
|
||||
);
|
||||
formData.append('file', fileContent, {
|
||||
filename,
|
||||
contentType,
|
||||
});
|
||||
|
||||
const response = await apiRequest.call(this, 'POST', '/audio/translations', {
|
||||
option: { formData },
|
||||
headers: formData.getHeaders(),
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
Vendored
+107
@@ -0,0 +1,107 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { isObjectEmpty, jsonParse, updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { metadataProperty, textMessageProperties } from '../descriptions';
|
||||
import { formatInputMessages } from '../text/helpers/responses';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Messages',
|
||||
name: 'messages',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Message',
|
||||
default: { values: [{ type: 'text' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
description:
|
||||
"Role in shaping the model's response, it tells the model how it should behave and interact with the user",
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
description: 'Send a message as a user and get a response from the model',
|
||||
},
|
||||
{
|
||||
name: 'Assistant',
|
||||
value: 'assistant',
|
||||
description: 'Tell the model to adopt a specific tone or personality',
|
||||
},
|
||||
{
|
||||
name: 'System',
|
||||
value: 'system',
|
||||
description:
|
||||
"Usually used to set the model's behavior or context for the next user message",
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
{
|
||||
...textMessageProperties[0],
|
||||
displayOptions: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [metadataProperty],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['create'],
|
||||
resource: ['conversation'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const options = this.getNodeParameter('options', i, {}) as IDataObject;
|
||||
const messages = this.getNodeParameter('messages.values', i, []) as IDataObject[];
|
||||
|
||||
const body: IDataObject = {
|
||||
items: await formatInputMessages.call(this, i, messages),
|
||||
};
|
||||
|
||||
if (options.metadata) {
|
||||
const metadata = jsonParse(options.metadata as string, {
|
||||
errorMessage: 'Invalid JSON in metadata field',
|
||||
}) as IDataObject;
|
||||
if (!isObjectEmpty(metadata)) {
|
||||
body.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiRequest.call(this, 'POST', '/conversations', { body });
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Conversation ID',
|
||||
name: 'conversationId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'conv_1234567890',
|
||||
description: 'The ID of the conversation to retrieve',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['get'],
|
||||
resource: ['conversation'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const conversationId = this.getNodeParameter('conversationId', i, '') as string;
|
||||
|
||||
const response = await apiRequest.call(this, 'GET', `/conversations/${conversationId}`);
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as create from './create.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as remove from './remove.operation';
|
||||
import * as update from './update.operation';
|
||||
|
||||
export { create, get, remove, update };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
action: 'Create a conversation',
|
||||
description: 'Create a conversation',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
action: 'Get a conversation',
|
||||
description: 'Get a conversation',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
action: 'Remove a conversation',
|
||||
description: 'Remove a conversation',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
action: 'Update a conversation',
|
||||
description: 'Update a conversation',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['conversation'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...create.description,
|
||||
...remove.description,
|
||||
...update.description,
|
||||
...get.description,
|
||||
];
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Conversation ID',
|
||||
name: 'conversationId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'conv_1234567890',
|
||||
description: 'The ID of the conversation to delete',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['remove'],
|
||||
resource: ['conversation'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const conversationId = this.getNodeParameter('conversationId', i, '') as string;
|
||||
|
||||
const response = await apiRequest.call(this, 'DELETE', `/conversations/${conversationId}`);
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
import type {
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions, jsonParse } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { metadataProperty } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Conversation ID',
|
||||
name: 'conversationId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'conv_1234567890',
|
||||
description: 'The ID of the conversation to update',
|
||||
required: true,
|
||||
},
|
||||
{ ...metadataProperty, required: true },
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
resource: ['conversation'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const conversationId = this.getNodeParameter('conversationId', i, '') as string;
|
||||
const metadata = this.getNodeParameter('metadata', i, '') as string;
|
||||
|
||||
if (!conversationId) {
|
||||
throw new Error('Conversation ID is required');
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
throw new Error('Metadata is required');
|
||||
}
|
||||
|
||||
const body: IDataObject = {};
|
||||
|
||||
body.metadata = jsonParse(metadata, {
|
||||
errorMessage: 'Invalid JSON in metadata field',
|
||||
});
|
||||
|
||||
const response = await apiRequest.call(this, 'POST', `/conversations/${conversationId}`, {
|
||||
body,
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
import type { INodeProperties, INodePropertyCollection } from 'n8n-workflow';
|
||||
|
||||
export const modelRLC = (searchListMethod: string = 'modelSearch'): INodeProperties => ({
|
||||
displayName: 'Model',
|
||||
name: 'modelId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. gpt-4',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const metadataProperty: INodeProperties = {
|
||||
displayName: 'Metadata',
|
||||
name: 'metadata',
|
||||
type: 'json',
|
||||
description:
|
||||
'Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard. Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters.',
|
||||
default: '{}',
|
||||
};
|
||||
|
||||
const imageMessageProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Image Type',
|
||||
name: 'imageType',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
options: [
|
||||
{ name: 'Image URL', value: 'url' },
|
||||
{ name: 'File ID', value: 'fileId' },
|
||||
{ name: 'File Data', value: 'base64' },
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['image'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Image URL',
|
||||
name: 'imageUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. https://example.com/image.jpeg',
|
||||
description: 'URL of the image to be sent',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['image'],
|
||||
imageType: ['url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Image Data',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description: 'Name of the binary property which contains the image(s)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['image'],
|
||||
imageType: ['base64'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'File ID',
|
||||
name: 'fileId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'ID of the file to be sent',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['image'],
|
||||
imageType: ['fileId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Detail',
|
||||
name: 'imageDetail',
|
||||
type: 'options',
|
||||
default: 'auto',
|
||||
description: 'The detail level of the image to be sent to the model',
|
||||
options: [
|
||||
{ name: 'Auto', value: 'auto' },
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'High', value: 'high' },
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['image'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const textMessageProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
description: 'The content of the message to be send',
|
||||
default: '',
|
||||
placeholder: 'e.g. Hello, how can you help me?',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['text'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const fileMessageProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'File Type',
|
||||
name: 'fileType',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
options: [
|
||||
{ name: 'File URL', value: 'url' },
|
||||
{ name: 'File ID', value: 'fileId' },
|
||||
{ name: 'File Data', value: 'base64' },
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['file'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'File URL',
|
||||
name: 'fileUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. https://example.com/file.pdf',
|
||||
description: 'URL of the file to be sent. Accepts base64 encoded files as well.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['file'],
|
||||
fileType: ['url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'File ID',
|
||||
name: 'fileId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'ID of the file to be sent',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['file'],
|
||||
fileType: ['fileId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'File Data',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description: 'Name of the binary property which contains the file',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['file'],
|
||||
fileType: ['base64'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'File Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['file'],
|
||||
fileType: ['base64'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const messageOptions: INodePropertyCollection[] = [
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
default: 'text',
|
||||
options: [
|
||||
{ name: 'Text', value: 'text' },
|
||||
{ name: 'Image', value: 'image' },
|
||||
{ name: 'File', value: 'file' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
description:
|
||||
"Role in shaping the model's response, it tells the model how it should behave and interact with the user",
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
description: 'Send a message as a user and get a response from the model',
|
||||
},
|
||||
{
|
||||
name: 'Assistant',
|
||||
value: 'assistant',
|
||||
description: 'Tell the model to adopt a specific tone or personality',
|
||||
},
|
||||
{
|
||||
name: 'System',
|
||||
value: 'system',
|
||||
description:
|
||||
"Usually used to set the model's behavior or context for the next user message",
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
...textMessageProperties,
|
||||
...imageMessageProperties,
|
||||
...fileMessageProperties,
|
||||
],
|
||||
},
|
||||
];
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'File',
|
||||
name: 'fileId',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'fileSearch',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: 'file-[a-zA-Z0-9]',
|
||||
errorMessage: 'Not a valid File ID',
|
||||
},
|
||||
},
|
||||
],
|
||||
placeholder: 'e.g. file-1234567890',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['deleteFile'],
|
||||
resource: ['file'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const fileId = this.getNodeParameter('fileId', i, '', { extractValue: true });
|
||||
|
||||
const response = await apiRequest.call(this, 'DELETE', `/files/${fileId}`);
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as deleteFile from './deleteFile.operation';
|
||||
import * as list from './list.operation';
|
||||
import * as upload from './upload.operation';
|
||||
|
||||
export { upload, deleteFile, list };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Delete a File',
|
||||
value: 'deleteFile',
|
||||
action: 'Delete a file',
|
||||
description: 'Delete a file from the server',
|
||||
},
|
||||
{
|
||||
name: 'List Files',
|
||||
value: 'list',
|
||||
action: 'List files',
|
||||
description: "Returns a list of files that belong to the user's organization",
|
||||
},
|
||||
{
|
||||
name: 'Upload a File',
|
||||
value: 'upload',
|
||||
action: 'Upload a file',
|
||||
description: 'Upload a file that can be used across various endpoints',
|
||||
},
|
||||
],
|
||||
default: 'upload',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['file'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
...upload.description,
|
||||
...deleteFile.description,
|
||||
...list.description,
|
||||
];
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Purpose',
|
||||
name: 'purpose',
|
||||
type: 'options',
|
||||
default: 'any',
|
||||
description: 'Only return files with the given purpose',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: 'Any [Default]',
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
name: 'Assistants',
|
||||
value: 'assistants',
|
||||
},
|
||||
{
|
||||
name: 'Fine-Tune',
|
||||
value: 'fine-tune',
|
||||
},
|
||||
{
|
||||
name: 'Vision',
|
||||
value: 'vision',
|
||||
},
|
||||
{
|
||||
name: 'User Data',
|
||||
value: 'user_data',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['list'],
|
||||
resource: ['file'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const qs: IDataObject = {};
|
||||
|
||||
if (options.purpose && options.purpose !== 'any') {
|
||||
qs.purpose = options.purpose as string;
|
||||
}
|
||||
|
||||
const { data } = await apiRequest.call(this, 'GET', '/files', { qs });
|
||||
|
||||
return (data || []).map((file: IDataObject) => ({
|
||||
json: file,
|
||||
pairedItem: { item: i },
|
||||
}));
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import FormData from 'form-data';
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { getBinaryDataFile } from '../../../helpers/binary-data';
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
placeholder: 'e.g. data',
|
||||
description:
|
||||
'Name of the binary property which contains the file. The size of individual files can be a maximum of 512 MB or 2 million tokens for Assistants.',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Purpose',
|
||||
name: 'purpose',
|
||||
type: 'options',
|
||||
default: 'user_data',
|
||||
description:
|
||||
"The intended purpose of the uploaded file, the 'Fine-tuning' only supports .jsonl files",
|
||||
options: [
|
||||
{
|
||||
name: 'Assistants',
|
||||
value: 'assistants',
|
||||
},
|
||||
{
|
||||
name: 'Fine-Tune',
|
||||
value: 'fine-tune',
|
||||
},
|
||||
{
|
||||
name: 'Vision',
|
||||
value: 'vision',
|
||||
},
|
||||
{
|
||||
name: 'User Data',
|
||||
value: 'user_data',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['upload'],
|
||||
resource: ['file'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('purpose', options.purpose || 'user_data');
|
||||
|
||||
const { filename, contentType, fileContent } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
binaryPropertyName,
|
||||
);
|
||||
formData.append('file', fileContent, {
|
||||
filename,
|
||||
contentType,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiRequest.call(this, 'POST', '/files', {
|
||||
option: { formData },
|
||||
headers: formData.getHeaders(),
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
} catch (error) {
|
||||
if (
|
||||
error.message.includes('Bad request') &&
|
||||
error.description?.includes('Expected file to have JSONL format')
|
||||
) {
|
||||
throw new NodeOperationError(this.getNode(), 'The file content is not in JSONL format', {
|
||||
description:
|
||||
'Fine-tuning accepts only files in JSONL format, where every line is a valid JSON dictionary',
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import type { ResponseInputImage } from 'openai/resources/responses/responses';
|
||||
import type { ChatContent, ChatResponse, ChatResponseRequest } from '../../../helpers/interfaces';
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { modelRLC } from '../descriptions';
|
||||
import { getBinaryDataFile } from '../../../helpers/binary-data';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...modelRLC('imageModelSearch'),
|
||||
},
|
||||
{
|
||||
displayName: 'Text Input',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
placeholder: "e.g. What's in this image?",
|
||||
default: "What's in this image?",
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Type',
|
||||
name: 'inputType',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
options: [
|
||||
{
|
||||
name: 'Image URL(s)',
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
name: 'Binary File(s)',
|
||||
value: 'base64',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'URL(s)',
|
||||
name: 'imageUrls',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. https://example.com/image.jpeg',
|
||||
description: 'URL(s) of the image(s) to analyze, multiple URLs can be added separated by comma',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description: 'Name of the binary property which contains the image(s)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputType: ['base64'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to simplify the response or not',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Detail',
|
||||
name: 'detail',
|
||||
type: 'options',
|
||||
default: 'auto',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto',
|
||||
value: 'auto',
|
||||
description:
|
||||
'Model will look at the image input size and decide if it should use the low or high setting',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
description: 'Return faster responses and consume fewer tokens',
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
description: 'Return more detailed responses, consumes more tokens',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Length of Description (Max Tokens)',
|
||||
description: 'Fewer tokens will result in shorter, less detailed image description',
|
||||
name: 'maxTokens',
|
||||
type: 'number',
|
||||
default: 300,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['analyze'],
|
||||
resource: ['image'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const 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, {});
|
||||
|
||||
const content: ChatContent = [
|
||||
{
|
||||
type: 'input_text',
|
||||
text,
|
||||
},
|
||||
];
|
||||
|
||||
const detail = (options.detail as ResponseInputImage['detail']) || ('auto' as const);
|
||||
|
||||
if (inputType === 'url') {
|
||||
const imageUrls = (this.getNodeParameter('imageUrls', i) as string)
|
||||
.split(',')
|
||||
.map((url) => url.trim());
|
||||
|
||||
for (const url of imageUrls) {
|
||||
content.push({
|
||||
type: 'input_image',
|
||||
detail,
|
||||
image_url: url,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i)
|
||||
.split(',')
|
||||
.map((propertyName) => propertyName.trim());
|
||||
|
||||
for (const propertyName of binaryPropertyName) {
|
||||
const { fileContent, contentType } = await getBinaryDataFile(this, i, propertyName);
|
||||
const buffer = await this.helpers.binaryToBuffer(fileContent);
|
||||
const fileBase64 = buffer.toString('base64');
|
||||
|
||||
content.push({
|
||||
type: 'input_image',
|
||||
detail,
|
||||
image_url: `data:${contentType};base64,${fileBase64}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const body: ChatResponseRequest = {
|
||||
model,
|
||||
input: [
|
||||
{
|
||||
role: 'user',
|
||||
content,
|
||||
},
|
||||
],
|
||||
max_output_tokens: (options.maxTokens as number) || 300,
|
||||
};
|
||||
|
||||
const response = (await apiRequest.call(this, 'POST', '/responses', {
|
||||
body,
|
||||
})) as ChatResponse;
|
||||
|
||||
const simplify = this.getNodeParameter('simplify', i) as boolean;
|
||||
|
||||
if (simplify) {
|
||||
return [
|
||||
{
|
||||
json: response.output as unknown as IDataObject,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: response as unknown as IDataObject,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+456
@@ -0,0 +1,456 @@
|
||||
import FormData from 'form-data';
|
||||
import type {
|
||||
IBinaryData,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { getBinaryDataFile } from '../../../helpers/binary-data';
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
export const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Model',
|
||||
name: 'model',
|
||||
type: 'options',
|
||||
default: 'gpt-image-1',
|
||||
description: 'The model to use for image generation',
|
||||
options: [
|
||||
{
|
||||
name: 'DALL·E 2',
|
||||
value: 'dall-e-2',
|
||||
},
|
||||
{
|
||||
name: 'GPT Image 1',
|
||||
value: 'gpt-image-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'A text description of the desired image(s). Maximum 1000 characters for dall-e-2, 32000 characters for gpt-image-1.',
|
||||
placeholder: 'A beautiful sunset over mountains',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Images',
|
||||
name: 'images',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Image',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
multipleValueButtonText: 'Add Image',
|
||||
},
|
||||
default: { values: [{ binaryPropertyName: 'data' }] },
|
||||
description:
|
||||
'Add one or more binary fields to include images with your prompt. Each image should be a png, webp, or jpg file less than 50MB. You can provide up to 16 images.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Image',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Binary Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
description: 'The name of the binary field containing the image data',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Binary Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description:
|
||||
'Name of the binary property which contains the image. It should be a square png file less than 4MB.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Number of Images',
|
||||
name: 'n',
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: 'The number of images to generate. Must be between 1 and 10.',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Size',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
default: '1024x1024',
|
||||
description: 'The size of the generated images',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: '256x256',
|
||||
value: '256x256',
|
||||
},
|
||||
{
|
||||
name: '512x512',
|
||||
value: '512x512',
|
||||
},
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1536 (Portrait)',
|
||||
value: '1024x1536',
|
||||
},
|
||||
{
|
||||
name: '1536x1024 (Landscape)',
|
||||
value: '1536x1024',
|
||||
},
|
||||
{
|
||||
name: 'Auto',
|
||||
value: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'quality',
|
||||
type: 'options',
|
||||
default: 'auto',
|
||||
description: 'The quality of the image that will be generated',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: 'Auto',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
},
|
||||
{
|
||||
name: 'Standard',
|
||||
value: 'standard',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Response Format',
|
||||
name: 'responseFormat',
|
||||
type: 'options',
|
||||
default: 'url',
|
||||
description:
|
||||
'The format in which the generated images are returned. URLs are only valid for 60 minutes after generation.',
|
||||
options: [
|
||||
{
|
||||
name: 'URL',
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
name: 'Base64 JSON',
|
||||
value: 'b64_json',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Format',
|
||||
name: 'outputFormat',
|
||||
type: 'options',
|
||||
default: 'png',
|
||||
description:
|
||||
'The format in which the generated images are returned. Only supported for gpt-image-1.',
|
||||
options: [
|
||||
{
|
||||
name: 'PNG',
|
||||
value: 'png',
|
||||
},
|
||||
{
|
||||
name: 'JPEG',
|
||||
value: 'jpeg',
|
||||
},
|
||||
{
|
||||
name: 'WebP',
|
||||
value: 'webp',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Compression',
|
||||
name: 'outputCompression',
|
||||
type: 'number',
|
||||
default: 100,
|
||||
description:
|
||||
'The compression level (0-100%) for the generated images. Only supported for gpt-image-1 with webp or jpeg output formats.',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
outputFormat: ['webp', 'jpeg'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse',
|
||||
placeholder: 'user-12345',
|
||||
},
|
||||
{
|
||||
displayName: 'Background',
|
||||
name: 'background',
|
||||
type: 'options',
|
||||
default: 'auto',
|
||||
description:
|
||||
'Allows to set transparency for the background of the generated image(s). Only supported for gpt-image-1.',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
name: 'Transparent',
|
||||
value: 'transparent',
|
||||
},
|
||||
{
|
||||
name: 'Opaque',
|
||||
value: 'opaque',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Fidelity',
|
||||
name: 'inputFidelity',
|
||||
type: 'options',
|
||||
default: 'low',
|
||||
description:
|
||||
'Control how much effort the model will exert to match the style and features of input images. Only supported for gpt-image-1.',
|
||||
options: [
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Image Mask',
|
||||
name: 'imageMask',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the input field containing the binary file data to be processed',
|
||||
description:
|
||||
'Name of the binary property which contains the image. An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited. If there are multiple images provided, the mask will be applied on the first image. Must be a valid PNG file, less than 4MB, and have the same dimensions as image.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['edit'],
|
||||
resource: ['image'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('model', i);
|
||||
const prompt = this.getNodeParameter('prompt', i);
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const isGPTImage1 = model === 'gpt-image-1';
|
||||
const isDallE2 = model === 'dall-e-2';
|
||||
|
||||
const n = this.getNodeParameter('n', i, 1) as number;
|
||||
const size = this.getNodeParameter('size', i, '1024x1024') as string;
|
||||
const defaultResponseFormat = isGPTImage1 ? 'b64_json' : 'url';
|
||||
const responseFormat = this.getNodeParameter(
|
||||
'responseFormat',
|
||||
i,
|
||||
defaultResponseFormat,
|
||||
) as string;
|
||||
const quality = this.getNodeParameter('quality', i, 'auto') as string;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
if (isGPTImage1) {
|
||||
const imagesParam = this.getNodeParameter('images', i, {
|
||||
values: [{ binaryPropertyName: 'data' }],
|
||||
}) as { values: Array<{ binaryPropertyName: string | IBinaryData }> };
|
||||
|
||||
const imagesUi = imagesParam.values ?? [];
|
||||
const imageFieldNames = imagesUi.map((v) => v.binaryPropertyName).filter((n) => Boolean(n));
|
||||
|
||||
for (const fieldName of imageFieldNames) {
|
||||
const { fileContent, contentType, filename } = await getBinaryDataFile(this, i, fieldName);
|
||||
const buffer = await this.helpers.binaryToBuffer(fileContent);
|
||||
formData.append('image[]', buffer, {
|
||||
filename,
|
||||
contentType,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
||||
const { fileContent, contentType, filename } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
binaryPropertyName,
|
||||
);
|
||||
const buffer = await this.helpers.binaryToBuffer(fileContent);
|
||||
formData.append('image', buffer, {
|
||||
filename,
|
||||
contentType,
|
||||
});
|
||||
}
|
||||
|
||||
formData.append('prompt', prompt);
|
||||
formData.append('model', model);
|
||||
|
||||
if (n) {
|
||||
formData.append('n', n.toString());
|
||||
}
|
||||
if (size) {
|
||||
formData.append('size', size);
|
||||
}
|
||||
if (responseFormat && isDallE2) {
|
||||
formData.append('response_format', responseFormat);
|
||||
}
|
||||
if (options.user) {
|
||||
formData.append('user', options.user as string);
|
||||
}
|
||||
if (options.background && isGPTImage1) {
|
||||
formData.append('background', options.background as string);
|
||||
}
|
||||
if (options.inputFidelity && isGPTImage1) {
|
||||
formData.append('input_fidelity', options.inputFidelity as string);
|
||||
}
|
||||
if (options.outputFormat && isGPTImage1) {
|
||||
formData.append('output_format', options.outputFormat as string);
|
||||
}
|
||||
if (options.outputCompression !== undefined && options.outputCompression !== null) {
|
||||
formData.append('output_compression', String(Number(options.outputCompression)));
|
||||
}
|
||||
if (quality && isGPTImage1) {
|
||||
formData.append('quality', quality);
|
||||
}
|
||||
|
||||
if (options.imageMask && typeof options.imageMask === 'string') {
|
||||
const { fileContent, contentType, filename } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
options.imageMask,
|
||||
);
|
||||
const buffer = await this.helpers.binaryToBuffer(fileContent);
|
||||
formData.append('mask', buffer, {
|
||||
filename,
|
||||
contentType,
|
||||
});
|
||||
}
|
||||
|
||||
const response = (await apiRequest.call(this, 'POST', '/images/edits', {
|
||||
option: { formData },
|
||||
headers: formData.getHeaders(),
|
||||
})) as IDataObject;
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
if (responseFormat === 'url') {
|
||||
const data = (response.data as IDataObject[]) || [];
|
||||
const entries = data.map((entry) => ({
|
||||
json: entry,
|
||||
pairedItem: { item: i },
|
||||
}));
|
||||
Array.prototype.push.apply(returnData, entries);
|
||||
} else {
|
||||
for (const entry of (response.data as IDataObject[]) || []) {
|
||||
const binaryData = await this.helpers.prepareBinaryData(
|
||||
Buffer.from(entry.b64_json as string, 'base64'),
|
||||
'data',
|
||||
);
|
||||
returnData.push({
|
||||
json: Object.assign({}, binaryData, {
|
||||
data: undefined,
|
||||
}),
|
||||
binary: {
|
||||
data: binaryData,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
import type {
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Model',
|
||||
name: 'model',
|
||||
type: 'options',
|
||||
default: 'dall-e-3',
|
||||
description: 'The model to use for image generation',
|
||||
options: [
|
||||
{
|
||||
name: 'DALL·E 2',
|
||||
value: 'dall-e-2',
|
||||
},
|
||||
{
|
||||
name: 'DALL·E 3',
|
||||
value: 'dall-e-3',
|
||||
},
|
||||
{
|
||||
name: 'GPT Image 1',
|
||||
value: 'gpt-image-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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 for dall-e-2 and 4000 characters for dall-e-3.',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Number of Images',
|
||||
name: 'n',
|
||||
default: 1,
|
||||
description: 'Number of images to generate',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'dalleQuality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, HD creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'HD',
|
||||
value: 'hd',
|
||||
},
|
||||
{
|
||||
name: 'Standard',
|
||||
value: 'standard',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'standard',
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'quality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, High creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
default: 'medium',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '256x256',
|
||||
value: '256x256',
|
||||
},
|
||||
{
|
||||
name: '512x512',
|
||||
value: '512x512',
|
||||
},
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1792x1024',
|
||||
value: '1792x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1792',
|
||||
value: '1024x1792',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1536',
|
||||
value: '1024x1536',
|
||||
},
|
||||
{
|
||||
name: '1536x1024',
|
||||
value: '1536x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Style',
|
||||
name: 'style',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Natural',
|
||||
value: 'natural',
|
||||
description: 'Produce more natural looking images',
|
||||
},
|
||||
{
|
||||
name: 'Vivid',
|
||||
value: 'vivid',
|
||||
description: 'Lean towards generating hyper-real and dramatic images',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'vivid',
|
||||
},
|
||||
{
|
||||
displayName: 'Respond with Image URL(s)',
|
||||
name: 'returnImageUrls',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return image URL(s) instead of binary file(s)',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Put Output in Field',
|
||||
name: 'binaryPropertyOutput',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the output field to put the binary file data in',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnImageUrls: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['generate'],
|
||||
resource: ['image'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('model', i) as string;
|
||||
const prompt = this.getNodeParameter('prompt', i) as string;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
let response_format = 'b64_json';
|
||||
let binaryPropertyOutput = 'data';
|
||||
|
||||
if (options.returnImageUrls) {
|
||||
response_format = 'url';
|
||||
}
|
||||
|
||||
if (options.binaryPropertyOutput) {
|
||||
binaryPropertyOutput = options.binaryPropertyOutput as string;
|
||||
delete options.binaryPropertyOutput;
|
||||
}
|
||||
|
||||
if (options.dalleQuality) {
|
||||
options.quality = options.dalleQuality;
|
||||
delete options.dalleQuality;
|
||||
}
|
||||
|
||||
delete options.returnImageUrls;
|
||||
const body: IDataObject = {
|
||||
prompt,
|
||||
model,
|
||||
response_format: model !== 'gpt-image-1' ? response_format : undefined, // gpt-image-1 does not support response_format
|
||||
...options,
|
||||
};
|
||||
|
||||
const { data } = await apiRequest.call(this, 'POST', '/images/generations', { body });
|
||||
if (response_format === 'url') {
|
||||
return ((data as IDataObject[]) || []).map((entry) => ({
|
||||
json: entry,
|
||||
pairedItem: { item: i },
|
||||
}));
|
||||
} else {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (const entry of data) {
|
||||
const binaryData = await this.helpers.prepareBinaryData(
|
||||
Buffer.from(entry.b64_json as string, 'base64'),
|
||||
'data',
|
||||
);
|
||||
returnData.push({
|
||||
json: Object.assign({}, binaryData, {
|
||||
data: undefined,
|
||||
}),
|
||||
binary: {
|
||||
[binaryPropertyOutput]: binaryData,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as analyze from './analyze.operation';
|
||||
import * as generate from './generate.operation';
|
||||
import * as edit from './edit.operation';
|
||||
|
||||
export { generate, analyze, edit };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Analyze Image',
|
||||
value: 'analyze',
|
||||
action: 'Analyze image',
|
||||
description: 'Take in images and answer questions about them',
|
||||
},
|
||||
{
|
||||
name: 'Generate an Image',
|
||||
value: 'generate',
|
||||
action: 'Generate an image',
|
||||
description: 'Creates an image from a text prompt',
|
||||
},
|
||||
{
|
||||
name: 'Edit Image',
|
||||
value: 'edit',
|
||||
action: 'Edit image',
|
||||
description: 'Edit an image',
|
||||
},
|
||||
],
|
||||
default: 'generate',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['image'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...generate.description,
|
||||
...analyze.description,
|
||||
...edit.description,
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { AllEntities } from 'n8n-workflow';
|
||||
|
||||
type NodeMap = {
|
||||
audio: 'generate' | 'transcribe' | 'translate';
|
||||
file: 'upload' | 'deleteFile' | 'list';
|
||||
image: 'generate' | 'analyze' | 'edit';
|
||||
text: 'classify' | 'response';
|
||||
conversation: 'create' | 'get' | 'update' | 'remove';
|
||||
video: 'generate';
|
||||
};
|
||||
|
||||
export type OpenAiType = AllEntities<NodeMap>;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import * as audio from './audio';
|
||||
import { router } from './router';
|
||||
|
||||
describe('OpenAI router', () => {
|
||||
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
const mockAudio = jest.spyOn(audio.transcribe, 'execute');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle NodeApiError undefined error chaining', async () => {
|
||||
const errorNode: INode = {
|
||||
id: 'error-node-id',
|
||||
name: 'ErrorNode',
|
||||
type: 'test.error',
|
||||
typeVersion: 1,
|
||||
position: [100, 200],
|
||||
parameters: {},
|
||||
};
|
||||
const nodeApiError = new NodeApiError(
|
||||
errorNode,
|
||||
{ message: 'API error occurred', error: { error: { message: 'Rate limit exceeded' } } },
|
||||
{ itemIndex: 0 },
|
||||
);
|
||||
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((parameter) =>
|
||||
parameter === 'resource' ? 'audio' : 'transcribe',
|
||||
);
|
||||
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(errorNode);
|
||||
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
|
||||
|
||||
mockAudio.mockRejectedValue(nodeApiError);
|
||||
|
||||
await expect(router.call(mockExecuteFunctions)).rejects.toThrow(NodeApiError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
NodeApiError,
|
||||
NodeOperationError,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getCustomErrorMessage } from '../../helpers/error-handling';
|
||||
import type { OpenAiType } from './node.type';
|
||||
import * as audio from './audio';
|
||||
import * as conversation from './conversation';
|
||||
import * as file from './file';
|
||||
import * as image from './image';
|
||||
import * as text from './text';
|
||||
import * as video from './video';
|
||||
|
||||
export async function router(this: IExecuteFunctions) {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
const resource = this.getNodeParameter<OpenAiType>('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
const openAiTypeData = {
|
||||
resource,
|
||||
operation,
|
||||
} as OpenAiType;
|
||||
|
||||
let execute;
|
||||
switch (openAiTypeData.resource) {
|
||||
case 'audio':
|
||||
execute = audio[openAiTypeData.operation].execute;
|
||||
break;
|
||||
case 'file':
|
||||
execute = file[openAiTypeData.operation].execute;
|
||||
break;
|
||||
case 'image':
|
||||
execute = image[openAiTypeData.operation].execute;
|
||||
break;
|
||||
case 'text':
|
||||
execute = text[openAiTypeData.operation].execute;
|
||||
break;
|
||||
case 'conversation':
|
||||
execute = conversation[openAiTypeData.operation].execute;
|
||||
break;
|
||||
case 'video':
|
||||
execute = video[openAiTypeData.operation].execute;
|
||||
break;
|
||||
default:
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The operation "${operation}" is not supported!`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const responseData = await execute.call(this, i);
|
||||
|
||||
returnData.push(...responseData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error instanceof NodeApiError) {
|
||||
// If the error is a rate limit error, we want to handle it differently
|
||||
const errorCode: string | undefined = (error.cause as any)?.error?.error?.code;
|
||||
if (errorCode) {
|
||||
const customErrorMessage = getCustomErrorMessage(errorCode);
|
||||
if (customErrorMessage) {
|
||||
error.message = customErrorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
error.context = {
|
||||
itemIndex: i,
|
||||
};
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new NodeOperationError(this.getNode(), error, {
|
||||
itemIndex: i,
|
||||
description: error.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Text Input',
|
||||
name: 'input',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. Sample text goes here',
|
||||
description: 'The input text to classify if it is violates the moderation policy',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Use Stable Model',
|
||||
name: 'useStableModel',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to use the stable version of the model instead of the latest version, accuracy may be slightly lower',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { lt: 2.1 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['classify'],
|
||||
resource: ['text'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const input = this.getNodeParameter('input', i) as string;
|
||||
const version = this.getNode().typeVersion;
|
||||
let model = 'omni-moderation-latest';
|
||||
if (version < 2.1) {
|
||||
const options = this.getNodeParameter('options', i);
|
||||
model = options.useStableModel ? 'text-moderation-stable' : 'text-moderation-latest';
|
||||
}
|
||||
|
||||
const body = {
|
||||
input,
|
||||
model,
|
||||
};
|
||||
|
||||
const { results } = await apiRequest.call(this, 'POST', '/moderations', { body });
|
||||
|
||||
if (!results) return [];
|
||||
|
||||
const simplify = this.getNodeParameter('simplify', i) as boolean;
|
||||
|
||||
if (simplify && results) {
|
||||
return [
|
||||
{
|
||||
json: { flagged: results[0].flagged },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
json: results[0],
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
import type { OpenAIClient } from '@langchain/openai';
|
||||
import get from 'lodash/get';
|
||||
import isObject from 'lodash/isObject';
|
||||
import { isObjectEmpty, jsonParse, type IDataObject, type IExecuteFunctions } from 'n8n-workflow';
|
||||
import type { ResponseInputImage } from 'openai/resources/responses/responses';
|
||||
|
||||
import { getBinaryDataFile } from '../../../../helpers/binary-data';
|
||||
import type {
|
||||
ChatContent,
|
||||
ChatInputItem,
|
||||
ChatResponseRequest,
|
||||
} from '../../../../helpers/interfaces';
|
||||
|
||||
const toArray = (str: string) => str.split(',').map((e) => e.trim());
|
||||
|
||||
const removeEmptyProperties = <T>(rest: { [key: string]: any }): T => {
|
||||
return Object.keys(rest)
|
||||
.filter(
|
||||
(k) =>
|
||||
rest[k] !== '' && rest[k] !== undefined && !(isObject(rest[k]) && isObjectEmpty(rest[k])),
|
||||
)
|
||||
.reduce((a, k) => ({ ...a, [k]: rest[k] }), {}) as unknown as T;
|
||||
};
|
||||
|
||||
export async function formatInputMessages(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
messages: IDataObject[],
|
||||
) {
|
||||
return await Promise.all(
|
||||
messages.map<Promise<ChatInputItem>>(async (message) => {
|
||||
const role = message.role as ChatInputItem['role'];
|
||||
let content: ChatContent = [];
|
||||
if (message.type === 'text' || !message.type) {
|
||||
content = [{ type: 'input_text', text: message.content as string }];
|
||||
} else if (message.type === 'image') {
|
||||
const detail = (message.imageDetail as ResponseInputImage['detail']) || ('auto' as const);
|
||||
|
||||
if (message.imageType === 'base64') {
|
||||
const { fileContent, contentType } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
message.binaryPropertyName as string,
|
||||
);
|
||||
const buffer = await this.helpers.binaryToBuffer(fileContent);
|
||||
content = [
|
||||
{
|
||||
type: 'input_image',
|
||||
detail,
|
||||
image_url: `data:${contentType};base64,${buffer.toString('base64')}`,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
content = [
|
||||
{
|
||||
type: 'input_image',
|
||||
detail,
|
||||
...(message.imageType === 'url' && { image_url: message.imageUrl as string }),
|
||||
...(message.imageType === 'fileId' && { file_id: message.fileId as string }),
|
||||
},
|
||||
];
|
||||
}
|
||||
} else if (message.type === 'file') {
|
||||
if (message.fileType === 'base64') {
|
||||
const { fileContent, contentType } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
message.binaryPropertyName as string,
|
||||
);
|
||||
const buffer = await this.helpers.binaryToBuffer(fileContent);
|
||||
content = [
|
||||
{
|
||||
type: 'input_file',
|
||||
filename: message.fileName as string,
|
||||
file_data: `data:${contentType};base64,${buffer.toString('base64')}`,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
content = [
|
||||
{
|
||||
type: 'input_file',
|
||||
...(message.fileType === 'url' && { file_url: message.fileUrl as string }),
|
||||
...(message.fileType === 'fileId' && { file_id: message.fileId as string }),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return { role, content };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateRequestOptions {
|
||||
model: string;
|
||||
messages: IDataObject[];
|
||||
options: IDataObject;
|
||||
builtInTools?: IDataObject;
|
||||
tools?: OpenAIClient.Responses.FunctionTool[];
|
||||
}
|
||||
|
||||
export async function createRequest(
|
||||
this: IExecuteFunctions,
|
||||
i: number,
|
||||
{ model, messages, options, builtInTools, tools }: CreateRequestOptions,
|
||||
): Promise<ChatResponseRequest> {
|
||||
const body: ChatResponseRequest = {
|
||||
model,
|
||||
input: await formatInputMessages.call(this, i, messages),
|
||||
parallel_tool_calls: get(options, 'parallelToolCalls', true) as boolean,
|
||||
store: get(options, 'store', true) as boolean,
|
||||
instructions: options.instructions as string,
|
||||
max_output_tokens: options.maxTokens as number,
|
||||
previous_response_id: options.previousResponseId as string,
|
||||
prompt_cache_key: options.promptCacheKey as string,
|
||||
safety_identifier: options.safetyIdentifier as string,
|
||||
service_tier: options.serviceTier as ChatResponseRequest['service_tier'],
|
||||
temperature: options.temperature as number,
|
||||
top_p: options.topP as number,
|
||||
top_logprobs: options.topLogprobs as number,
|
||||
tools,
|
||||
max_tool_calls: options.maxToolCalls as number,
|
||||
background: get(options, 'backgroundMode.values.enabled', false) as boolean,
|
||||
};
|
||||
|
||||
if (options.truncation !== undefined) {
|
||||
body.truncation = !!options.truncation ? 'auto' : 'disabled';
|
||||
}
|
||||
|
||||
if (options.conversationId) {
|
||||
body.conversation = options.conversationId as string;
|
||||
}
|
||||
|
||||
if (Array.isArray(options.include) && options.include?.length) {
|
||||
body.include = options.include as ChatResponseRequest['include'];
|
||||
}
|
||||
|
||||
if (options.metadata) {
|
||||
body.metadata = jsonParse(options.metadata as string, {
|
||||
errorMessage: 'Failed to parse metadata',
|
||||
});
|
||||
}
|
||||
|
||||
if (options.promptConfig) {
|
||||
const prompt = get(options, 'promptConfig.promptOptions') as IDataObject;
|
||||
body.prompt = removeEmptyProperties({
|
||||
id: prompt.promptId,
|
||||
version: prompt.version,
|
||||
...(prompt.variables && {
|
||||
variables: jsonParse(prompt.variables as string, {
|
||||
errorMessage: 'Failed to parse prompt variables',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (options.reasoning) {
|
||||
const reasoning = get(options, 'reasoning.reasoningOptions') as IDataObject;
|
||||
body.reasoning = removeEmptyProperties({
|
||||
effort: reasoning.effort,
|
||||
summary: reasoning.summary && reasoning.summary !== 'none' ? reasoning.summary : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.textFormat) {
|
||||
const textOptions = get(options, 'textFormat.textOptions') as IDataObject;
|
||||
const textConfig: OpenAIClient.Responses.ResponseTextConfig = {
|
||||
verbosity: textOptions.verbosity as OpenAIClient.Responses.ResponseTextConfig['verbosity'],
|
||||
};
|
||||
if (textOptions.type === 'json_schema') {
|
||||
textConfig.format = {
|
||||
type: textOptions.type,
|
||||
name: textOptions.name as string,
|
||||
schema: jsonParse(textOptions.schema as string, {
|
||||
errorMessage: 'Failed to parse schema',
|
||||
}),
|
||||
};
|
||||
} else if (textOptions.type === 'json_object') {
|
||||
textConfig.format = {
|
||||
type: textOptions.type,
|
||||
};
|
||||
body.input = [
|
||||
{
|
||||
role: 'system',
|
||||
content: [
|
||||
{ type: 'input_text', text: 'You are a helpful assistant designed to output JSON.' },
|
||||
],
|
||||
},
|
||||
...body.input,
|
||||
];
|
||||
} else if (textOptions.type === 'text') {
|
||||
textConfig.format = {
|
||||
type: textOptions.type,
|
||||
};
|
||||
}
|
||||
|
||||
if (textConfig.format) {
|
||||
textConfig.format = removeEmptyProperties(textConfig.format);
|
||||
}
|
||||
|
||||
body.text = textConfig;
|
||||
}
|
||||
|
||||
if (builtInTools) {
|
||||
const newTools = body.tools ?? [];
|
||||
|
||||
const webSearchOptions = get(builtInTools, 'webSearch') as IDataObject | undefined;
|
||||
if (webSearchOptions) {
|
||||
let allowedDomains: string[] | undefined;
|
||||
const allowedDomainsRaw = get(webSearchOptions, 'allowedDomains', '') as string;
|
||||
if (allowedDomainsRaw) {
|
||||
allowedDomains = toArray(allowedDomainsRaw);
|
||||
}
|
||||
|
||||
let userLocation: OpenAIClient.Responses.WebSearchTool.UserLocation | undefined;
|
||||
if (webSearchOptions.country || webSearchOptions.city || webSearchOptions.region) {
|
||||
userLocation = {
|
||||
type: 'approximate',
|
||||
country: webSearchOptions.country as string,
|
||||
city: webSearchOptions.city as string,
|
||||
region: webSearchOptions.region as string,
|
||||
};
|
||||
}
|
||||
|
||||
newTools.push(
|
||||
removeEmptyProperties({
|
||||
type: 'web_search',
|
||||
search_context_size: get(webSearchOptions, 'searchContextSize', 'medium') as
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high',
|
||||
user_location: userLocation,
|
||||
...(allowedDomains && { filters: { allowed_domains: allowedDomains } }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (builtInTools.codeInterpreter) {
|
||||
newTools.push({
|
||||
type: 'code_interpreter',
|
||||
container: {
|
||||
type: 'auto',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (builtInTools.fileSearch) {
|
||||
const vectorStoreIds = get(builtInTools.fileSearch, 'vectorStoreIds', '[]') as string;
|
||||
const filters = get(builtInTools.fileSearch, 'filters', '{}') as string;
|
||||
newTools.push(
|
||||
removeEmptyProperties({
|
||||
type: 'file_search',
|
||||
vector_store_ids: jsonParse(vectorStoreIds, {
|
||||
errorMessage: 'Failed to parse vector store IDs',
|
||||
}),
|
||||
filters: filters
|
||||
? jsonParse(filters, { errorMessage: 'Failed to parse filters' })
|
||||
: undefined,
|
||||
max_num_results: get(builtInTools.fileSearch, 'maxResults') as number,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
body.tools = newTools;
|
||||
}
|
||||
|
||||
return await removeEmptyProperties(body);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as classify from './classify.operation';
|
||||
import * as response from './response.operation';
|
||||
|
||||
export { classify, response };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Message a Model',
|
||||
value: 'response',
|
||||
action: 'Message a model',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-excess-final-period, n8n-nodes-base/node-param-description-missing-final-period
|
||||
description: 'Generate a model response with GPT 3, 4, 5, etc. using Responses API',
|
||||
},
|
||||
{
|
||||
name: 'Classify Text for Violations',
|
||||
value: 'classify',
|
||||
action: 'Classify text for violations',
|
||||
description: 'Check whether content complies with usage policies',
|
||||
},
|
||||
],
|
||||
default: 'response',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['text'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
...classify.description,
|
||||
...response.description,
|
||||
];
|
||||
+723
@@ -0,0 +1,723 @@
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import { getConnectedTools } from '@utils/helpers';
|
||||
import get from 'lodash/get';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, NodeOperationError, updateDisplayOptions } from 'n8n-workflow';
|
||||
import { MODELS_NOT_SUPPORT_FUNCTION_CALLS } from '../../../helpers/constants';
|
||||
import type { ChatResponse } from '../../../helpers/interfaces';
|
||||
import { formatToOpenAIResponsesTool } from '../../../helpers/utils';
|
||||
import { pollUntilAvailable } from '../../../helpers/polling';
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { messageOptions, metadataProperty, modelRLC } from '../descriptions';
|
||||
import { createRequest } from './helpers/responses';
|
||||
|
||||
const jsonSchemaExample = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["message"]
|
||||
}`;
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
modelRLC('modelSearch'),
|
||||
{
|
||||
displayName: 'Messages',
|
||||
name: 'responses',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Message',
|
||||
default: { values: [{ type: 'text' }] },
|
||||
options: messageOptions,
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
{
|
||||
displayName: 'Hide Tools',
|
||||
name: 'hideTools',
|
||||
type: 'hidden',
|
||||
default: 'hide',
|
||||
displayOptions: {
|
||||
show: {
|
||||
modelId: MODELS_NOT_SUPPORT_FUNCTION_CALLS,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Connect your own custom n8n tools to this node on the canvas',
|
||||
name: 'noticeTools',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
hideTools: ['hide'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Built-in Tools',
|
||||
name: 'builtInTools',
|
||||
placeholder: 'Add Built-in Tool',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Web Search',
|
||||
name: 'webSearch',
|
||||
type: 'collection',
|
||||
default: { searchContextSize: 'medium' },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Search Context Size',
|
||||
description:
|
||||
'High level guidance for the amount of context window space to use for the search',
|
||||
name: 'searchContextSize',
|
||||
type: 'options',
|
||||
default: 'medium',
|
||||
options: [
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'Medium', value: 'medium' },
|
||||
{ name: 'High', value: 'high' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Web Search Allowed Domains',
|
||||
name: 'allowedDomains',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Comma-separated list of domains to search. Only domains in this list will be searched.',
|
||||
placeholder: 'e.g. google.com, wikipedia.org',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. US, GB',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. New York, London',
|
||||
},
|
||||
{
|
||||
displayName: 'Region',
|
||||
name: 'region',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. New York, London',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'File Search',
|
||||
name: 'fileSearch',
|
||||
type: 'collection',
|
||||
default: { vectorStoreIds: '[]' },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Vector Store IDs',
|
||||
name: 'vectorStoreIds',
|
||||
description:
|
||||
'The vector store IDs to use for the file search. Vector stores are managed via OpenAI Dashboard.',
|
||||
type: 'json',
|
||||
default: '[]',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'json',
|
||||
default: '{}',
|
||||
},
|
||||
{
|
||||
displayName: 'Max Results',
|
||||
name: 'maxResults',
|
||||
type: 'number',
|
||||
default: 1,
|
||||
typeOptions: { minValue: 1, maxValue: 50 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Code Interpreter',
|
||||
name: 'codeInterpreter',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to allow the model to execute code in a sandboxed environment',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Conversation ID',
|
||||
name: 'conversationId',
|
||||
default: '',
|
||||
description:
|
||||
'The conversation that this response belongs to. Input items and output items from this response are automatically added to this conversation after this response completes.',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
displayName: 'Include Additional Data',
|
||||
name: 'include',
|
||||
default: [],
|
||||
type: 'multiOptions',
|
||||
description: 'Specify additional output data to include in the model response',
|
||||
options: [
|
||||
{
|
||||
name: 'Code Interpreter Call Outputs',
|
||||
value: 'code_interpreter_call.outputs',
|
||||
},
|
||||
{
|
||||
name: 'Computer Call Output Image URL',
|
||||
value: 'computer_call_output.output.image_url',
|
||||
},
|
||||
{
|
||||
name: 'File Search Call Results',
|
||||
value: 'file_search_call.results',
|
||||
},
|
||||
{
|
||||
name: 'Message Input Image URL',
|
||||
value: 'message.input_image.image_url',
|
||||
},
|
||||
{
|
||||
name: 'Message Output Text Logprobs',
|
||||
value: 'message.output_text.logprobs',
|
||||
},
|
||||
{
|
||||
name: 'Reasoning Encrypted Content',
|
||||
value: 'reasoning.encrypted_content',
|
||||
},
|
||||
{
|
||||
name: 'Web Search Tool Call Sources',
|
||||
value: 'web_search_call.action.sources',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Instructions',
|
||||
name: 'instructions',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Instructions for the model to follow',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
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 32,768).',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
maxValue: 32768,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Tool Calls Iterations',
|
||||
name: 'maxToolsIterations',
|
||||
type: 'number',
|
||||
default: 15,
|
||||
description:
|
||||
'The maximum number of tool iteration cycles the LLM will run before stopping. A single iteration can contain multiple tool calls. Set to 0 for no limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Max Built-in Tool Calls',
|
||||
name: 'maxToolCalls',
|
||||
type: 'number',
|
||||
default: 15,
|
||||
description:
|
||||
'The maximum number of total calls to built-in tools that can be processed in a response. This maximum number applies across all built-in tool calls, not per individual tool. Any further attempts to call a tool by the model will be ignored.',
|
||||
},
|
||||
metadataProperty,
|
||||
{
|
||||
displayName: 'Parallel Tool Calls',
|
||||
name: 'parallelToolCalls',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to allow parallel tool calls. If true, the model can call multiple tools at once.',
|
||||
},
|
||||
{
|
||||
displayName: 'Previous Response ID',
|
||||
name: 'previousResponseId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
// TODO: add display options?
|
||||
description:
|
||||
'The ID of the previous response to continue from. Cannot be used in conjunction with Conversation ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'promptConfig',
|
||||
type: 'fixedCollection',
|
||||
description:
|
||||
'Configure the reusable prompt template configured via OpenAI Dashboard. <a href="https://platform.openai.com/docs/guides/prompt-engineering#reusable-prompts">Learn more</a>.',
|
||||
default: { promptOptions: [{ promptId: '' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'promptOptions',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Prompt ID',
|
||||
name: 'promptId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The unique identifier of the prompt template to use',
|
||||
},
|
||||
{
|
||||
displayName: 'Version',
|
||||
name: 'version',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Optional version of the prompt template',
|
||||
},
|
||||
{
|
||||
displayName: 'Variables',
|
||||
name: 'variables',
|
||||
type: 'json',
|
||||
default: '{}',
|
||||
description: 'Variables to be substituted into the prompt template',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Prompt Cache Key',
|
||||
name: 'promptCacheKey',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Used by OpenAI to cache responses for similar requests to optimize your cache hit rates',
|
||||
},
|
||||
{
|
||||
displayName: 'Reasoning',
|
||||
name: 'reasoning',
|
||||
type: 'fixedCollection',
|
||||
default: { reasoningOptions: [{ effort: 'medium', summary: 'none' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Reasoning',
|
||||
name: 'reasoningOptions',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Effort',
|
||||
name: 'effort',
|
||||
type: 'options',
|
||||
default: 'medium',
|
||||
// TODO: allow only high for gpt-5-pro
|
||||
options: [
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'Medium', value: 'medium' },
|
||||
{ name: 'High', value: 'high' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Summary',
|
||||
name: 'summary',
|
||||
type: 'options',
|
||||
default: 'auto',
|
||||
description:
|
||||
"A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process.",
|
||||
options: [
|
||||
{ name: 'None', value: 'none' },
|
||||
{ name: 'Auto', value: 'auto' },
|
||||
{ name: 'Concise', value: 'concise' },
|
||||
{ name: 'Detailed', value: 'detailed' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Safety Identifier',
|
||||
name: 'safetyIdentifier',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
"A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies. The IDs should be a string that uniquely identifies each user.",
|
||||
},
|
||||
{
|
||||
displayName: 'Service Tier',
|
||||
name: 'serviceTier',
|
||||
type: 'options',
|
||||
default: 'auto',
|
||||
description: 'The service tier to use for the request',
|
||||
options: [
|
||||
{ name: 'Auto', value: 'auto' },
|
||||
{ name: 'Flex', value: 'flex' },
|
||||
{ name: 'Default', value: 'default' },
|
||||
{ name: 'Priority', value: 'priority' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Store',
|
||||
name: 'store',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to store the generated model response for later retrieval via API',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Format',
|
||||
name: 'textFormat',
|
||||
type: 'fixedCollection',
|
||||
default: { textOptions: [{ type: 'text' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'textOptions',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
default: '',
|
||||
options: [
|
||||
{ name: 'Text', value: 'text' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'JSON Schema (recommended)', value: 'json_schema' },
|
||||
{ name: 'JSON Object', value: 'json_object' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Verbosity',
|
||||
name: 'verbosity',
|
||||
type: 'options',
|
||||
default: 'medium',
|
||||
options: [
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'Medium', value: 'medium' },
|
||||
{ name: 'High', value: 'high' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'my_schema',
|
||||
description:
|
||||
'The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['json_schema'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'All properties in the schema must be set to "required", when using "strict" mode.',
|
||||
name: 'requiredNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
strict: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Schema',
|
||||
name: 'schema',
|
||||
type: 'json',
|
||||
default: jsonSchemaExample,
|
||||
description: 'The schema of the response format',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['json_schema'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The description of the response format',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['json_schema'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Strict',
|
||||
name: 'strict',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to require that the AI will always generate responses that match the provided JSON Schema',
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['json_schema'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Top Logprobs',
|
||||
name: 'topLogprobs',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description:
|
||||
'An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated log probability',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Randomness (Temperature)',
|
||||
name: 'temperature',
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description:
|
||||
'What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend altering this or top_p but not both',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 2,
|
||||
numberPrecision: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Randomness (Top P)',
|
||||
name: 'topP',
|
||||
default: 1,
|
||||
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
|
||||
description:
|
||||
'An alternative to sampling with temperature, 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',
|
||||
},
|
||||
{
|
||||
displayName: 'Truncation',
|
||||
name: 'truncation',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
"Whether to truncate the input to the model's context window size. When disabled will throw a 400 error instead.",
|
||||
},
|
||||
{
|
||||
displayName: 'Background Mode',
|
||||
name: 'backgroundMode',
|
||||
type: 'fixedCollection',
|
||||
default: { values: [{ backgroundMode: true }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Bakground',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Background Mode',
|
||||
name: 'enabled',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to run the model in background mode. If true, the model will run in background mode.',
|
||||
},
|
||||
{
|
||||
displayName: 'Timeout',
|
||||
name: 'timeout',
|
||||
type: 'number',
|
||||
default: 300,
|
||||
description:
|
||||
'The timeout for the background mode in seconds. If 0, the timeout is infinite.',
|
||||
typeOptions: {
|
||||
minValue: 0,
|
||||
maxValue: 3600,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['response'],
|
||||
resource: ['text'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('modelId', i, '', { extractValue: true }) as string;
|
||||
const messages = this.getNodeParameter('responses.values', i, []) as IDataObject[];
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const maxToolsIterations = this.getNodeParameter('options.maxToolsIterations', i, 15) as number;
|
||||
const builtInTools = this.getNodeParameter('builtInTools', i, {}) as IDataObject;
|
||||
const abortSignal = this.getExecutionCancelSignal();
|
||||
|
||||
const hideTools = this.getNodeParameter('hideTools', i, '') as string;
|
||||
|
||||
let tools;
|
||||
let externalTools: Tool[] = [];
|
||||
|
||||
if (hideTools !== 'hide') {
|
||||
const enforceUniqueNames = true;
|
||||
externalTools = await getConnectedTools(this, enforceUniqueNames, false);
|
||||
}
|
||||
|
||||
if (externalTools.length) {
|
||||
tools = externalTools.length ? externalTools?.map(formatToOpenAIResponsesTool) : undefined;
|
||||
}
|
||||
|
||||
const body = await createRequest.call(this, i, {
|
||||
model,
|
||||
messages,
|
||||
options,
|
||||
tools,
|
||||
builtInTools,
|
||||
});
|
||||
let response = (await apiRequest.call(this, 'POST', '/responses', {
|
||||
body,
|
||||
})) as ChatResponse;
|
||||
|
||||
if (body.background) {
|
||||
const timeoutSeconds = get(options, 'backgroundMode.values.timeout', 300) as number;
|
||||
response = await pollUntilAvailable(
|
||||
this,
|
||||
async () => {
|
||||
return (await apiRequest.call(this, 'GET', `/responses/${response.id}`)) as ChatResponse;
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
throw new NodeOperationError(this.getNode(), 'Background mode error', {
|
||||
description: response.error.message,
|
||||
});
|
||||
}
|
||||
return response.status === 'completed';
|
||||
},
|
||||
timeoutSeconds,
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response) return [];
|
||||
|
||||
// reasoning models such as gpt5 include reasoning items that must be included in the request
|
||||
const isToolRelatedCall: (item: { type: string }) => boolean = (item) =>
|
||||
item.type === 'function_call' || item.type === 'reasoning';
|
||||
|
||||
let toolCalls = response.output.filter(isToolRelatedCall);
|
||||
|
||||
const hasFunctionCall = () => toolCalls.some((item) => item.type === 'function_call');
|
||||
|
||||
let currentIteration = 1;
|
||||
// make sure there's actually a function call to answer
|
||||
while (toolCalls.length && hasFunctionCall()) {
|
||||
if (abortSignal?.aborted || (maxToolsIterations > 0 && currentIteration > maxToolsIterations)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// if there's conversation, we don't need to include function_call or reasoning items in the request
|
||||
// if we include them, OpenAI will throw "Duplicate item with id" error
|
||||
if (!body.conversation) {
|
||||
body.input.push.apply(body.input, toolCalls);
|
||||
}
|
||||
|
||||
for (const item of toolCalls) {
|
||||
if (item.type === 'function_call') {
|
||||
const functionName = item.name;
|
||||
const functionArgs = item.arguments;
|
||||
const callId = item.call_id;
|
||||
|
||||
let functionResponse;
|
||||
for (const tool of externalTools ?? []) {
|
||||
if (tool.name === functionName) {
|
||||
const parsedArgs: { input: string } = jsonParse(functionArgs);
|
||||
const functionInput = parsedArgs.input ?? parsedArgs ?? functionArgs;
|
||||
functionResponse = await tool.invoke(functionInput);
|
||||
}
|
||||
|
||||
if (typeof functionResponse === 'object') {
|
||||
functionResponse = JSON.stringify(functionResponse);
|
||||
}
|
||||
}
|
||||
|
||||
body.input.push({
|
||||
type: 'function_call_output',
|
||||
call_id: callId,
|
||||
output: functionResponse,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
response = (await apiRequest.call(this, 'POST', '/responses', {
|
||||
body,
|
||||
})) as ChatResponse;
|
||||
toolCalls = response.output.filter(isToolRelatedCall);
|
||||
|
||||
currentIteration++;
|
||||
}
|
||||
|
||||
const formatType = get(body, 'text.format.type');
|
||||
if (formatType === 'json_object' || formatType === 'json_schema') {
|
||||
try {
|
||||
response.output = response.output.map((item) => {
|
||||
if (item.type === 'message') {
|
||||
item.content = item.content.map((content) => {
|
||||
if (content.type === 'output_text') {
|
||||
content.text = JSON.parse(content.text);
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const simplify = this.getNodeParameter('simplify', i) as boolean;
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
if (simplify) {
|
||||
const messages = response.output.filter((item) => item.type === 'message');
|
||||
returnData.push({
|
||||
json: {
|
||||
output: messages as unknown as IDataObject,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
} else {
|
||||
returnData.push({ json: response as unknown as IDataObject, pairedItem: { item: i } });
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import FormData from 'form-data';
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError, updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { getBinaryDataFile } from '../../../helpers/binary-data';
|
||||
import type { VideoJob } from '../../../helpers/interfaces';
|
||||
import { pollUntilAvailable } from '../../../helpers/polling';
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
modelRLC('videoModelSearch'),
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'string',
|
||||
default: 'A video of a cat playing with a ball',
|
||||
description: 'The prompt to generate a video from',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Seconds',
|
||||
name: 'seconds',
|
||||
type: 'number',
|
||||
default: 4,
|
||||
description: 'Clip duration in seconds',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Size',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
default: '1280x720',
|
||||
description:
|
||||
'Output resolution formatted as width x height. 1024x1792 and 1792x1024 are only supported by Sora 2 Pro.',
|
||||
options: [
|
||||
{ name: '720x1280', value: '720x1280' },
|
||||
{ name: '1280x720', value: '1280x720' },
|
||||
{ name: '1024x1792', value: '1024x1792' },
|
||||
{ name: '1792x1024', value: '1792x1024' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Reference',
|
||||
description: 'Optional image reference that guides generation',
|
||||
name: 'binaryPropertyNameReference',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
},
|
||||
{
|
||||
displayName: 'Wait Timeout',
|
||||
name: 'waitTime',
|
||||
type: 'number',
|
||||
default: 300,
|
||||
description: 'Time to wait for the video to be generated in seconds',
|
||||
typeOptions: {
|
||||
minValue: 5,
|
||||
maxValue: 7200,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Output Field Name',
|
||||
name: 'fileName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the output field to put the binary file data in',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['generate'],
|
||||
resource: ['video'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('modelId', i, '', { extractValue: true }) as string;
|
||||
const prompt = this.getNodeParameter('prompt', i) as string;
|
||||
const seconds = this.getNodeParameter('seconds', i) as number;
|
||||
const size = this.getNodeParameter('size', i) as string;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const waitSeconds = (options.waitTime as number) || 300;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('model', model);
|
||||
formData.append('prompt', prompt);
|
||||
formData.append('seconds', seconds.toString());
|
||||
formData.append('size', size);
|
||||
|
||||
if (options.binaryPropertyNameReference) {
|
||||
const { fileContent, contentType, filename } = await getBinaryDataFile(
|
||||
this,
|
||||
i,
|
||||
options.binaryPropertyNameReference as string,
|
||||
);
|
||||
const buffer = await this.helpers.binaryToBuffer(fileContent);
|
||||
formData.append('input_reference', buffer, {
|
||||
filename,
|
||||
contentType,
|
||||
});
|
||||
}
|
||||
|
||||
const response = (await apiRequest.call(this, 'POST', '/videos', {
|
||||
option: { formData },
|
||||
headers: formData.getHeaders(),
|
||||
})) as VideoJob;
|
||||
|
||||
const finalResponse = await pollUntilAvailable(
|
||||
this,
|
||||
async () => {
|
||||
return (await apiRequest.call(this, 'GET', `/videos/${response.id}`)) as VideoJob;
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
throw new NodeOperationError(this.getNode(), 'Error generating video', {
|
||||
description: response.error.message,
|
||||
itemIndex: i,
|
||||
});
|
||||
}
|
||||
return response.status === 'completed';
|
||||
},
|
||||
waitSeconds,
|
||||
10,
|
||||
);
|
||||
|
||||
const contentResponse = await apiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/videos/${finalResponse.id}/content`,
|
||||
{
|
||||
option: {
|
||||
useStream: true,
|
||||
resolveWithFullResponse: true,
|
||||
json: false,
|
||||
encoding: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const mimeType = contentResponse.headers['content-type'];
|
||||
|
||||
const binaryData = await this.helpers.prepareBinaryData(
|
||||
contentResponse.body,
|
||||
(options.fileName as string) || 'data',
|
||||
mimeType,
|
||||
);
|
||||
|
||||
return [
|
||||
{
|
||||
json: Object.assign({}, binaryData, {
|
||||
data: undefined,
|
||||
}),
|
||||
binary: {
|
||||
data: binaryData,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as generate from './generate.operation';
|
||||
|
||||
export { generate };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Generate',
|
||||
value: 'generate',
|
||||
action: 'Generate a video',
|
||||
description: 'Creates a video from a text prompt',
|
||||
},
|
||||
],
|
||||
default: 'generate',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['video'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...generate.description,
|
||||
];
|
||||
Reference in New Issue
Block a user