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:
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type IExecuteFunctions,
|
||||
type INodeType,
|
||||
type INodeTypeBaseDescription,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { listSearch, loadOptions } from '../methods';
|
||||
import { router } from './actions/router';
|
||||
import { configureNodeInputs } from '../helpers/description';
|
||||
|
||||
import * as assistant from './actions/assistant';
|
||||
import * as audio from './actions/audio';
|
||||
import * as file from './actions/file';
|
||||
import * as image from './actions/image';
|
||||
import * as text from './actions/text';
|
||||
|
||||
export class OpenAiV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8],
|
||||
defaults: {
|
||||
name: 'OpenAI',
|
||||
},
|
||||
inputs: `={{(${configureNodeInputs})($parameter.resource, $parameter.operation, $parameter.hideTools, $parameter.memory ?? undefined)}}`,
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'openAiApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: 'Assistant',
|
||||
value: 'assistant',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
value: 'image',
|
||||
},
|
||||
{
|
||||
name: 'Audio',
|
||||
value: 'audio',
|
||||
},
|
||||
{
|
||||
name: 'File',
|
||||
value: 'file',
|
||||
},
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
...assistant.description,
|
||||
...audio.description,
|
||||
...file.description,
|
||||
...image.description,
|
||||
...text.description,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
methods = {
|
||||
listSearch,
|
||||
loadOptions,
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
return await router.call(this);
|
||||
}
|
||||
}
|
||||
Vendored
+290
@@ -0,0 +1,290 @@
|
||||
import type {
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError, updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
modelRLC('modelSearch'),
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The name of the assistant. The maximum length is 256 characters.',
|
||||
placeholder: 'e.g. My Assistant',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The description of the assistant. The maximum length is 512 characters.',
|
||||
placeholder: 'e.g. My personal assistant',
|
||||
},
|
||||
{
|
||||
displayName: 'Instructions',
|
||||
name: 'instructions',
|
||||
type: 'string',
|
||||
description:
|
||||
'The system instructions that the assistant uses. The maximum length is 32768 characters.',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Code Interpreter',
|
||||
name: 'codeInterpreter',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to enable the code interpreter that allows the assistants to write and run Python code in a sandboxed execution environment, find more <a href="https://platform.openai.com/docs/assistants/tools/code-interpreter" target="_blank">here</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'Knowledge Retrieval',
|
||||
name: 'knowledgeRetrieval',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to augments the assistant with knowledge from outside its model, such as proprietary product information or documents, find more <a href="https://platform.openai.com/docs/assistants/tools/knowledge-retrieval" target="_blank">here</a>',
|
||||
},
|
||||
//we want to display Files selector only when codeInterpreter true or knowledgeRetrieval true or both
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
|
||||
displayName: 'Files',
|
||||
name: 'file_ids',
|
||||
type: 'multiOptions',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
|
||||
description:
|
||||
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant. You can use expression to pass file IDs as an array or comma-separated string.',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getFiles',
|
||||
},
|
||||
default: [],
|
||||
hint: "Add more files by using the 'Upload a File' operation",
|
||||
displayOptions: {
|
||||
show: {
|
||||
codeInterpreter: [true],
|
||||
},
|
||||
hide: {
|
||||
knowledgeRetrieval: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
|
||||
displayName: 'Files',
|
||||
name: 'file_ids',
|
||||
type: 'multiOptions',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
|
||||
description:
|
||||
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getFiles',
|
||||
},
|
||||
default: [],
|
||||
hint: "Add more files by using the 'Upload a File' operation",
|
||||
displayOptions: {
|
||||
show: {
|
||||
knowledgeRetrieval: [true],
|
||||
},
|
||||
hide: {
|
||||
codeInterpreter: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
|
||||
displayName: 'Files',
|
||||
name: 'file_ids',
|
||||
type: 'multiOptions',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
|
||||
description:
|
||||
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getFiles',
|
||||
},
|
||||
default: [],
|
||||
hint: "Add more files by using the 'Upload a File' operation",
|
||||
displayOptions: {
|
||||
show: {
|
||||
knowledgeRetrieval: [true],
|
||||
codeInterpreter: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Add custom n8n tools when you <i>message</i> your assistant (rather than when creating it)',
|
||||
name: 'noticeTools',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Output Randomness (Temperature)',
|
||||
name: 'temperature',
|
||||
default: 1,
|
||||
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
|
||||
description:
|
||||
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. We generally recommend altering this or temperature but not both.',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
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: 'Fail if Assistant Already Exists',
|
||||
name: 'failIfExists',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to fail an operation if the assistant with the same name already exists',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['create'],
|
||||
resource: ['assistant'],
|
||||
},
|
||||
};
|
||||
|
||||
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 name = this.getNodeParameter('name', i) as string;
|
||||
const assistantDescription = this.getNodeParameter('description', i) as string;
|
||||
const instructions = this.getNodeParameter('instructions', i) as string;
|
||||
const codeInterpreter = this.getNodeParameter('codeInterpreter', i) as boolean;
|
||||
const knowledgeRetrieval = this.getNodeParameter('knowledgeRetrieval', i) as boolean;
|
||||
let file_ids = this.getNodeParameter('file_ids', i, []) as string[] | string;
|
||||
if (typeof file_ids === 'string') {
|
||||
file_ids = file_ids.split(',').map((file_id) => file_id.trim());
|
||||
}
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
if (options.failIfExists) {
|
||||
const assistants: string[] = [];
|
||||
|
||||
let has_more = true;
|
||||
let after: string | undefined;
|
||||
|
||||
do {
|
||||
const response = (await apiRequest.call(this, 'GET', '/assistants', {
|
||||
headers: {
|
||||
'OpenAI-Beta': 'assistants=v2',
|
||||
},
|
||||
qs: {
|
||||
limit: 100,
|
||||
after,
|
||||
},
|
||||
})) as { data: IDataObject[]; has_more: boolean; last_id: string };
|
||||
|
||||
for (const assistant of response.data || []) {
|
||||
assistants.push(assistant.name as string);
|
||||
}
|
||||
|
||||
has_more = response.has_more;
|
||||
|
||||
if (has_more) {
|
||||
after = response.last_id;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (has_more);
|
||||
|
||||
if (assistants.includes(name)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`An assistant with the same name '${name}' already exists`,
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (file_ids.length > 20) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'The maximum number of files that can be attached to the assistant is 20',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
const body: IDataObject = {
|
||||
model,
|
||||
name,
|
||||
description: assistantDescription,
|
||||
instructions,
|
||||
};
|
||||
|
||||
const tools = [];
|
||||
|
||||
if (codeInterpreter) {
|
||||
tools.push({
|
||||
type: 'code_interpreter',
|
||||
});
|
||||
body.tool_resources = {
|
||||
...((body.tool_resources as object) ?? {}),
|
||||
code_interpreter: {
|
||||
file_ids,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (knowledgeRetrieval) {
|
||||
tools.push({
|
||||
type: 'file_search',
|
||||
});
|
||||
body.tool_resources = {
|
||||
...((body.tool_resources as object) ?? {}),
|
||||
file_search: {
|
||||
vector_stores: [
|
||||
{
|
||||
file_ids,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (tools.length) {
|
||||
body.tools = tools;
|
||||
}
|
||||
|
||||
const response = await apiRequest.call(this, 'POST', '/assistants', {
|
||||
body,
|
||||
headers: {
|
||||
'OpenAI-Beta': 'assistants=v2',
|
||||
},
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v1/actions/assistant/deleteAssistant.operation.ts
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { assistantRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [assistantRLC];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['deleteAssistant'],
|
||||
resource: ['assistant'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const assistantId = this.getNodeParameter('assistantId', i, '', { extractValue: true }) as string;
|
||||
|
||||
const response = await apiRequest.call(this, 'DELETE', `/assistants/${assistantId}`, {
|
||||
headers: {
|
||||
'OpenAI-Beta': 'assistants=v2',
|
||||
},
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as create from './create.operation';
|
||||
import * as deleteAssistant from './deleteAssistant.operation';
|
||||
import * as list from './list.operation';
|
||||
import * as message from './message.operation';
|
||||
import * as update from './update.operation';
|
||||
|
||||
export { create, deleteAssistant, message, list, update };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Create an Assistant',
|
||||
value: 'create',
|
||||
action: 'Create an assistant',
|
||||
description: 'Create a new assistant',
|
||||
},
|
||||
{
|
||||
name: 'Delete an Assistant',
|
||||
value: 'deleteAssistant',
|
||||
action: 'Delete an assistant',
|
||||
description: 'Delete an assistant from the account',
|
||||
},
|
||||
{
|
||||
name: 'List Assistants',
|
||||
value: 'list',
|
||||
action: 'List assistants',
|
||||
description: 'List assistants in the organization',
|
||||
},
|
||||
{
|
||||
name: 'Message an Assistant',
|
||||
value: 'message',
|
||||
action: 'Message an assistant',
|
||||
description: 'Send messages to an assistant',
|
||||
},
|
||||
{
|
||||
name: 'Update an Assistant',
|
||||
value: 'update',
|
||||
action: 'Update an assistant',
|
||||
description: 'Update an existing assistant',
|
||||
},
|
||||
],
|
||||
default: 'message',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['assistant'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
...create.description,
|
||||
...deleteAssistant.description,
|
||||
...message.description,
|
||||
...list.description,
|
||||
...update.description,
|
||||
];
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['list'],
|
||||
resource: ['assistant'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
let has_more = true;
|
||||
let after: string | undefined;
|
||||
|
||||
do {
|
||||
const response = await apiRequest.call(this, 'GET', '/assistants', {
|
||||
headers: {
|
||||
'OpenAI-Beta': 'assistants=v2',
|
||||
},
|
||||
qs: {
|
||||
limit: 100,
|
||||
after,
|
||||
},
|
||||
});
|
||||
|
||||
for (const assistant of response.data || []) {
|
||||
try {
|
||||
assistant.created_at = new Date(assistant.created_at * 1000).toISOString();
|
||||
} catch (error) {}
|
||||
|
||||
returnData.push({ json: assistant, pairedItem: { item: i } });
|
||||
}
|
||||
|
||||
has_more = response.has_more;
|
||||
|
||||
if (has_more) {
|
||||
after = response.last_id as string;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (has_more);
|
||||
|
||||
const simplify = this.getNodeParameter('simplify', i) as boolean;
|
||||
|
||||
if (simplify) {
|
||||
return returnData.map((item) => {
|
||||
const { id, name, model } = item.json;
|
||||
return {
|
||||
json: {
|
||||
id,
|
||||
name,
|
||||
model,
|
||||
},
|
||||
pairedItem: { item: i },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
Vendored
+318
@@ -0,0 +1,318 @@
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { AgentExecutor } from '@langchain/classic/agents';
|
||||
import type { OpenAIToolType } from '@langchain/classic/dist/experimental/openai_assistant/schema';
|
||||
import { OpenAIAssistantRunnable } from '@langchain/classic/experimental/openai_assistant';
|
||||
import type { BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import omit from 'lodash/omit';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
ApplicationError,
|
||||
NodeConnectionTypes,
|
||||
NodeOperationError,
|
||||
updateDisplayOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { OpenAI as OpenAIClient } from 'openai';
|
||||
|
||||
import { promptTypeOptionsDeprecated } from '@utils/descriptions';
|
||||
import { getConnectedTools, getPromptInputByType, mergeCustomHeaders } from '@utils/helpers';
|
||||
import { getTracingConfig } from '@utils/tracing';
|
||||
|
||||
import { formatToOpenAIAssistantTool, getChatMessages } from '../../../helpers/utils';
|
||||
import { assistantRLC } from '../descriptions';
|
||||
import { getProxyAgent } from '@n8n/ai-utilities';
|
||||
import { Container } from '@n8n/di';
|
||||
import { AiConfig } from '@n8n/config';
|
||||
import { checkDomainRestrictions } from '@utils/checkDomainRestrictions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
assistantRLC,
|
||||
{
|
||||
...promptTypeOptionsDeprecated,
|
||||
name: 'prompt',
|
||||
},
|
||||
{
|
||||
displayName: 'Prompt (User Message)',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. Hello, how can you help me?',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
prompt: ['define'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Memory',
|
||||
name: 'memory',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Use memory connector',
|
||||
value: 'connector',
|
||||
description: 'Connect one of the supported memory nodes',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Use thread ID',
|
||||
value: 'threadId',
|
||||
description: 'Specify the ID of the thread to continue',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.6 } }],
|
||||
},
|
||||
},
|
||||
default: 'connector',
|
||||
},
|
||||
{
|
||||
displayName: 'Thread ID',
|
||||
name: 'threadId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '',
|
||||
description: 'The ID of the thread to continue, a new thread will be created if not specified',
|
||||
hint: 'If the thread ID is empty or undefined a new thread will be created and included in the response',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.6 } }],
|
||||
memory: ['threadId'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Connect your own custom n8n tools to this node on the canvas',
|
||||
name: 'noticeTools',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
description: 'Additional options to add',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Base URL',
|
||||
name: 'baseURL',
|
||||
default: 'https://api.openai.com/v1',
|
||||
description: 'Override the default base URL for the API',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'@version': [{ _cnd: { gte: 1.8 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Max Retries',
|
||||
name: 'maxRetries',
|
||||
default: 2,
|
||||
description: 'Maximum number of retries to attempt',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
displayName: 'Timeout',
|
||||
name: 'timeout',
|
||||
default: 10000,
|
||||
description: 'Maximum amount of time a request is allowed to take in milliseconds',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
displayName: 'Preserve Original Tools',
|
||||
name: 'preserveOriginalTools',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to preserve the original tools of the assistant after the execution of this node, otherwise the tools will be replaced with the connected tools, if any, default is true',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.3 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['message'],
|
||||
resource: ['assistant'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
const mapChatMessageToThreadMessage = (
|
||||
message: BaseMessage,
|
||||
): OpenAIClient.Beta.Threads.ThreadCreateParams.Message => ({
|
||||
role: message._getType() === 'ai' ? 'assistant' : 'user',
|
||||
content: message.content.toString(),
|
||||
});
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const credentials = await this.getCredentials('openAiApi');
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
const input = getPromptInputByType({
|
||||
ctx: this,
|
||||
i,
|
||||
inputKey: 'text',
|
||||
promptTypeKey: 'prompt',
|
||||
});
|
||||
|
||||
const assistantId = this.getNodeParameter('assistantId', i, '', { extractValue: true }) as string;
|
||||
|
||||
const options = this.getNodeParameter('options', i, {}) as {
|
||||
baseURL?: string;
|
||||
maxRetries: number;
|
||||
timeout: number;
|
||||
preserveOriginalTools?: boolean;
|
||||
};
|
||||
|
||||
if (options.baseURL) {
|
||||
checkDomainRestrictions(this, credentials, options.baseURL);
|
||||
}
|
||||
|
||||
const baseURL = (options.baseURL ?? credentials.url) as string;
|
||||
const { openAiDefaultHeaders } = Container.get(AiConfig);
|
||||
const defaultHeaders = mergeCustomHeaders(credentials, openAiDefaultHeaders ?? {});
|
||||
const timeout = options.timeout;
|
||||
|
||||
const client = new OpenAIClient({
|
||||
apiKey: credentials.apiKey as string,
|
||||
maxRetries: options.maxRetries ?? 2,
|
||||
timeout: timeout ?? 10000,
|
||||
baseURL,
|
||||
fetchOptions: {
|
||||
dispatcher: getProxyAgent(baseURL, {
|
||||
headersTimeout: timeout,
|
||||
bodyTimeout: timeout,
|
||||
}),
|
||||
},
|
||||
defaultHeaders,
|
||||
});
|
||||
|
||||
const agent = new OpenAIAssistantRunnable({ assistantId, client, asAgent: true });
|
||||
|
||||
const tools = await getConnectedTools(this, nodeVersion > 1, false);
|
||||
let assistantTools;
|
||||
|
||||
if (tools.length) {
|
||||
const transformedConnectedTools = tools?.map(formatToOpenAIAssistantTool) ?? [];
|
||||
const nativeToolsParsed: OpenAIToolType = [];
|
||||
|
||||
assistantTools = (await client.beta.assistants.retrieve(assistantId)).tools;
|
||||
|
||||
const useCodeInterpreter = assistantTools.some((tool) => tool.type === 'code_interpreter');
|
||||
if (useCodeInterpreter) {
|
||||
nativeToolsParsed.push({
|
||||
type: 'code_interpreter',
|
||||
});
|
||||
}
|
||||
|
||||
const useRetrieval = assistantTools.some((tool) => tool.type === 'file_search');
|
||||
if (useRetrieval) {
|
||||
nativeToolsParsed.push({
|
||||
type: 'file_search',
|
||||
});
|
||||
}
|
||||
|
||||
await client.beta.assistants.update(assistantId, {
|
||||
tools: [...nativeToolsParsed, ...transformedConnectedTools],
|
||||
});
|
||||
}
|
||||
|
||||
const agentExecutor = AgentExecutor.fromAgentAndTools({
|
||||
agent,
|
||||
tools: tools ?? [],
|
||||
});
|
||||
|
||||
const useMemoryConnector =
|
||||
nodeVersion >= 1.6 && this.getNodeParameter('memory', i) === 'connector';
|
||||
const memory =
|
||||
useMemoryConnector || nodeVersion < 1.6
|
||||
? ((await this.getInputConnectionData(NodeConnectionTypes.AiMemory, 0)) as
|
||||
| BufferWindowMemory
|
||||
| undefined)
|
||||
: undefined;
|
||||
|
||||
const threadId =
|
||||
nodeVersion >= 1.6 && !useMemoryConnector
|
||||
? (this.getNodeParameter('threadId', i) as string)
|
||||
: undefined;
|
||||
|
||||
const chainValues: IDataObject = {
|
||||
content: input,
|
||||
signal: this.getExecutionCancelSignal(),
|
||||
timeout: options.timeout ?? 10000,
|
||||
};
|
||||
let thread: OpenAIClient.Beta.Threads.Thread;
|
||||
if (memory) {
|
||||
const chatMessages = await getChatMessages(memory);
|
||||
|
||||
// Construct a new thread from the chat history to map the memory
|
||||
if (chatMessages.length) {
|
||||
const first32Messages = chatMessages.slice(0, 32);
|
||||
// There is a undocumented limit of 32 messages per thread when creating a thread with messages
|
||||
const mappedMessages: OpenAIClient.Beta.Threads.ThreadCreateParams.Message[] =
|
||||
first32Messages.map(mapChatMessageToThreadMessage);
|
||||
|
||||
thread = await client.beta.threads.create({ messages: mappedMessages });
|
||||
const overLimitMessages = chatMessages.slice(32).map(mapChatMessageToThreadMessage);
|
||||
|
||||
// Send the remaining messages that exceed the limit of 32 sequentially
|
||||
for (const message of overLimitMessages) {
|
||||
await client.beta.threads.messages.create(thread.id, message);
|
||||
}
|
||||
|
||||
chainValues.threadId = thread.id;
|
||||
}
|
||||
} else if (threadId) {
|
||||
chainValues.threadId = threadId;
|
||||
}
|
||||
|
||||
let filteredResponse: IDataObject = {};
|
||||
try {
|
||||
const response = await agentExecutor.withConfig(getTracingConfig(this)).invoke(chainValues);
|
||||
if (memory) {
|
||||
await memory.saveContext({ input }, { output: response.output });
|
||||
|
||||
if (response.threadId && response.runId) {
|
||||
const threadRun = await client.beta.threads.runs.retrieve(response.runId, {
|
||||
thread_id: response.threadId,
|
||||
});
|
||||
response.usage = threadRun.usage;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options.preserveOriginalTools !== false &&
|
||||
nodeVersion >= 1.3 &&
|
||||
(assistantTools ?? [])?.length
|
||||
) {
|
||||
await client.beta.assistants.update(assistantId, {
|
||||
tools: assistantTools,
|
||||
});
|
||||
}
|
||||
// Remove configuration properties and runId added by Langchain that are not relevant to the user
|
||||
filteredResponse = omit(response, ['signal', 'timeout', 'content', 'runId']) as IDataObject;
|
||||
} catch (error) {
|
||||
if (!(error instanceof ApplicationError)) {
|
||||
throw new NodeOperationError(this.getNode(), error.message, { itemIndex: i });
|
||||
}
|
||||
}
|
||||
|
||||
return [{ json: filteredResponse, pairedItem: { item: i } }];
|
||||
}
|
||||
Vendored
+246
@@ -0,0 +1,246 @@
|
||||
import type {
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { ApplicationError, NodeOperationError, updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { assistantRLC, modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
assistantRLC,
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Code Interpreter',
|
||||
name: 'codeInterpreter',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to enable the code interpreter that allows the assistants to write and run Python code in a sandboxed execution environment, find more <a href="https://platform.openai.com/docs/assistants/tools/code-interpreter" target="_blank">here</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The description of the assistant. The maximum length is 512 characters.',
|
||||
placeholder: 'e.g. My personal assistant',
|
||||
},
|
||||
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
|
||||
displayName: 'Files',
|
||||
name: 'file_ids',
|
||||
type: 'multiOptions',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
|
||||
description:
|
||||
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant. You can use expression to pass file IDs as an array or comma-separated string.',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getFiles',
|
||||
},
|
||||
default: [],
|
||||
hint: "Add more files by using the 'Upload a File' operation, any existing files not selected here will be removed.",
|
||||
},
|
||||
{
|
||||
displayName: 'Instructions',
|
||||
name: 'instructions',
|
||||
type: 'string',
|
||||
description:
|
||||
'The system instructions that the assistant uses. The maximum length is 32768 characters.',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Knowledge Retrieval',
|
||||
name: 'knowledgeRetrieval',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to augments the assistant with knowledge from outside its model, such as proprietary product information or documents, find more <a href="https://platform.openai.com/docs/assistants/tools/knowledge-retrieval" target="_blank">here</a>',
|
||||
},
|
||||
{ ...modelRLC('modelSearch'), required: false },
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The name of the assistant. The maximum length is 256 characters.',
|
||||
placeholder: 'e.g. My Assistant',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Remove All Custom Tools (Functions)',
|
||||
name: 'removeCustomTools',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to remove all custom tools (functions) from the assistant',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Output Randomness (Temperature)',
|
||||
name: 'temperature',
|
||||
default: 1,
|
||||
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
|
||||
description:
|
||||
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. We generally recommend altering this or temperature but not both.',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
resource: ['assistant'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
function getFileIds(file_ids: unknown): string[] {
|
||||
if (Array.isArray(file_ids)) {
|
||||
return file_ids;
|
||||
}
|
||||
|
||||
if (typeof file_ids === 'string') {
|
||||
return file_ids.split(',').map((file_id) => file_id.trim());
|
||||
}
|
||||
|
||||
throw new ApplicationError('Invalid file_ids type');
|
||||
}
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const assistantId = this.getNodeParameter('assistantId', i, '', { extractValue: true }) as string;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const {
|
||||
modelId,
|
||||
name,
|
||||
instructions,
|
||||
codeInterpreter,
|
||||
knowledgeRetrieval,
|
||||
file_ids,
|
||||
removeCustomTools,
|
||||
temperature,
|
||||
topP,
|
||||
} = options;
|
||||
|
||||
const assistantDescription = options.description as string;
|
||||
|
||||
const body: IDataObject = {};
|
||||
|
||||
if (file_ids) {
|
||||
const files = getFileIds(file_ids);
|
||||
if (files.length > 20) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'The maximum number of files that can be attached to the assistant is 20',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
body.tool_resources = {
|
||||
...((body.tool_resources as object) ?? {}),
|
||||
code_interpreter: {
|
||||
file_ids: files,
|
||||
},
|
||||
// updating file_ids for file_search directly is not supported by OpenAI API
|
||||
// only updating vector_store_ids for file_search is supported
|
||||
// support for this to be added as part of ADO-2968
|
||||
// https://platform.openai.com/docs/api-reference/assistants/modifyAssistant
|
||||
};
|
||||
}
|
||||
|
||||
if (modelId) {
|
||||
body.model = this.getNodeParameter('options.modelId', i, '', { extractValue: true }) as string;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
body.name = name;
|
||||
}
|
||||
|
||||
if (assistantDescription) {
|
||||
body.description = assistantDescription;
|
||||
}
|
||||
|
||||
if (instructions) {
|
||||
body.instructions = instructions;
|
||||
}
|
||||
|
||||
if (temperature) {
|
||||
body.temperature = temperature;
|
||||
}
|
||||
|
||||
if (topP) {
|
||||
body.topP = topP;
|
||||
}
|
||||
|
||||
let tools =
|
||||
((
|
||||
await apiRequest.call(this, 'GET', `/assistants/${assistantId}`, {
|
||||
headers: {
|
||||
'OpenAI-Beta': 'assistants=v2',
|
||||
},
|
||||
})
|
||||
).tools as IDataObject[]) || [];
|
||||
|
||||
if (codeInterpreter && !tools.find((tool) => tool.type === 'code_interpreter')) {
|
||||
tools.push({
|
||||
type: 'code_interpreter',
|
||||
});
|
||||
}
|
||||
|
||||
if (codeInterpreter === false && tools.find((tool) => tool.type === 'code_interpreter')) {
|
||||
tools = tools.filter((tool) => tool.type !== 'code_interpreter');
|
||||
}
|
||||
|
||||
if (knowledgeRetrieval && !tools.find((tool) => tool.type === 'file_search')) {
|
||||
tools.push({
|
||||
type: 'file_search',
|
||||
});
|
||||
}
|
||||
|
||||
if (knowledgeRetrieval === false && tools.find((tool) => tool.type === 'file_search')) {
|
||||
tools = tools.filter((tool) => tool.type !== 'file_search');
|
||||
}
|
||||
|
||||
if (removeCustomTools) {
|
||||
tools = tools.filter((tool) => tool.type !== 'function');
|
||||
}
|
||||
|
||||
body.tools = tools;
|
||||
|
||||
const response = await apiRequest.call(this, 'POST', `/assistants/${assistantId}`, {
|
||||
body,
|
||||
headers: {
|
||||
'OpenAI-Beta': 'assistants=v2',
|
||||
},
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+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 },
|
||||
},
|
||||
];
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import type { INodeProperties } 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 assistantRLC: INodeProperties = {
|
||||
displayName: 'Assistant',
|
||||
name: 'assistantId',
|
||||
type: 'resourceLocator',
|
||||
description:
|
||||
'Assistant to respond to the message. You can add, modify or remove assistants in the <a href="https://platform.openai.com/playground?mode=assistant" target="_blank">playground</a>.',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'assistantSearch',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. asst_abc123',
|
||||
},
|
||||
],
|
||||
};
|
||||
+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,
|
||||
];
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
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',
|
||||
options: [
|
||||
{
|
||||
name: 'Any [Default]',
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
name: 'Assistants',
|
||||
value: 'assistants',
|
||||
},
|
||||
{
|
||||
name: 'Fine-Tune',
|
||||
value: 'fine-tune',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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 },
|
||||
}));
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
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: 'assistants',
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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 || 'assistants');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
import type {
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
...modelRLC('imageModelSearch'),
|
||||
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.4 } }] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Text Input',
|
||||
name: 'text',
|
||||
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[]> {
|
||||
let model = 'gpt-4-vision-preview';
|
||||
if (this.getNode().typeVersion >= 1.4) {
|
||||
model = this.getNodeParameter('modelId', i, 'gpt-4o', { extractValue: true }) as string;
|
||||
}
|
||||
|
||||
const text = this.getNodeParameter('text', i, '') as string;
|
||||
const inputType = this.getNodeParameter('inputType', i) as string;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
|
||||
const content: IDataObject[] = [
|
||||
{
|
||||
type: 'text',
|
||||
text,
|
||||
},
|
||||
];
|
||||
|
||||
const detail = (options.detail as string) || 'auto';
|
||||
|
||||
if (inputType === 'url') {
|
||||
const imageUrls = (this.getNodeParameter('imageUrls', i) as string)
|
||||
.split(',')
|
||||
.map((url) => url.trim());
|
||||
|
||||
for (const url of imageUrls) {
|
||||
content.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url,
|
||||
detail,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i)
|
||||
.split(',')
|
||||
.map((propertyName) => propertyName.trim());
|
||||
|
||||
for (const propertyName of binaryPropertyName) {
|
||||
const binaryData = this.helpers.assertBinaryData(i, propertyName);
|
||||
|
||||
let fileBase64;
|
||||
if (binaryData.id) {
|
||||
const chunkSize = 256 * 1024;
|
||||
const stream = await this.helpers.getBinaryStream(binaryData.id, chunkSize);
|
||||
const buffer = await this.helpers.binaryToBuffer(stream);
|
||||
fileBase64 = buffer.toString('base64');
|
||||
} else {
|
||||
fileBase64 = binaryData.data;
|
||||
}
|
||||
|
||||
if (!binaryData) {
|
||||
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
|
||||
}
|
||||
|
||||
content.push({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: `data:${binaryData.mimeType};base64,${fileBase64}`,
|
||||
detail,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
model,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content,
|
||||
},
|
||||
],
|
||||
max_tokens: (options.maxTokens as number) || 300,
|
||||
};
|
||||
|
||||
let response = await apiRequest.call(this, 'POST', '/chat/completions', { body });
|
||||
|
||||
const simplify = this.getNodeParameter('simplify', i) as boolean;
|
||||
|
||||
if (simplify && response.choices) {
|
||||
response = { content: response.choices[0].message.content };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
json: response,
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
}
|
||||
+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,37 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as analyze from './analyze.operation';
|
||||
import * as generate from './generate.operation';
|
||||
|
||||
export { generate, analyze };
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
default: 'generate',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['image'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...generate.description,
|
||||
...analyze.description,
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { AllEntities } from 'n8n-workflow';
|
||||
|
||||
type NodeMap = {
|
||||
assistant: 'message' | 'create' | 'deleteAssistant' | 'list' | 'update';
|
||||
audio: 'generate' | 'transcribe' | 'translate';
|
||||
file: 'upload' | 'deleteFile' | 'list';
|
||||
image: 'generate' | 'analyze';
|
||||
text: 'message' | 'classify';
|
||||
};
|
||||
|
||||
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,88 @@
|
||||
import {
|
||||
NodeOperationError,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
NodeApiError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as assistant from './assistant';
|
||||
import * as audio from './audio';
|
||||
import * as file from './file';
|
||||
import * as image from './image';
|
||||
import type { OpenAiType } from './node.type';
|
||||
import * as text from './text';
|
||||
import { getCustomErrorMessage } from '../../helpers/error-handling';
|
||||
|
||||
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 'assistant':
|
||||
execute = assistant[openAiTypeData.operation].execute;
|
||||
break;
|
||||
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;
|
||||
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];
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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 options = this.getNodeParameter('options', i);
|
||||
const 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 },
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as classify from './classify.operation';
|
||||
import * as message from './message.operation';
|
||||
|
||||
export { classify, message };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Message a Model',
|
||||
value: 'message',
|
||||
action: 'Message a model',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-excess-final-period
|
||||
description: 'Create a completion with GPT 3, 4, etc.',
|
||||
},
|
||||
{
|
||||
name: 'Classify Text for Violations',
|
||||
value: 'classify',
|
||||
action: 'Classify text for violations',
|
||||
description: 'Check whether content complies with usage policies',
|
||||
},
|
||||
],
|
||||
default: 'message',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['text'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
...classify.description,
|
||||
...message.description,
|
||||
];
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import _omit from 'lodash/omit';
|
||||
import type {
|
||||
INodeProperties,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { getConnectedTools } from '@utils/helpers';
|
||||
|
||||
import { MODELS_NOT_SUPPORT_FUNCTION_CALLS } from '../../../helpers/constants';
|
||||
import type { ChatCompletion } from '../../../helpers/interfaces';
|
||||
import { formatToOpenAIAssistantTool } from '../../../helpers/utils';
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
modelRLC('modelSearch'),
|
||||
{
|
||||
displayName: 'Messages',
|
||||
name: 'messages',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Message',
|
||||
default: { values: [{ content: '' }] },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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: 'Output Content as JSON',
|
||||
name: 'jsonOutput',
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether to attempt to return the response in JSON format. Compatible with GPT-4 Turbo and all GPT-3.5 Turbo models newer than gpt-3.5-turbo-1106.',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'Hide Tools',
|
||||
name: 'hideTools',
|
||||
type: 'hidden',
|
||||
default: 'hide',
|
||||
displayOptions: {
|
||||
show: {
|
||||
modelId: MODELS_NOT_SUPPORT_FUNCTION_CALLS,
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Connect your own custom n8n tools to this node on the canvas',
|
||||
name: 'noticeTools',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
hideTools: ['hide'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Frequency Penalty',
|
||||
name: 'frequency_penalty',
|
||||
default: 0,
|
||||
typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
|
||||
description:
|
||||
"Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim",
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
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: 'Number of Completions',
|
||||
name: 'n',
|
||||
default: 1,
|
||||
description:
|
||||
'How many completions to generate for each prompt. Note: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop.',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
displayName: 'Presence Penalty',
|
||||
name: 'presence_penalty',
|
||||
default: 0,
|
||||
typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
|
||||
description:
|
||||
"Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics",
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Randomness (Temperature)',
|
||||
name: 'temperature',
|
||||
default: 1,
|
||||
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
|
||||
description:
|
||||
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. We generally recommend altering this or temperature but not both.',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
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: 'Reasoning Effort',
|
||||
name: 'reasoning_effort',
|
||||
default: 'medium',
|
||||
description:
|
||||
'Controls the amount of reasoning tokens to use. A value of "low" will favor speed and economical token usage, "high" will favor more complete reasoning at the cost of more tokens generated and slower responses.',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
description: 'Favors speed and economical token usage',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 'medium',
|
||||
description: 'Balance between speed and reasoning accuracy',
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
description:
|
||||
'Favors more complete reasoning at the cost of more tokens generated and slower responses',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
// reasoning_effort is only available on o1, o1-versioned, or on o3-mini and beyond, and gpt-5 models. Not on o1-mini or other GPT-models.
|
||||
'/modelId': [{ _cnd: { regex: '(^o1([-\\d]+)?$)|(^o[3-9].*)|(^gpt-5.*)' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.5 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
operation: ['message'],
|
||||
resource: ['text'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
const model = this.getNodeParameter('modelId', i, '', { extractValue: true });
|
||||
let messages = this.getNodeParameter('messages.values', i, []) as IDataObject[];
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const jsonOutput = this.getNodeParameter('jsonOutput', i, false) as boolean;
|
||||
const maxToolsIterations =
|
||||
nodeVersion >= 1.5 ? (this.getNodeParameter('options.maxToolsIterations', i, 15) as number) : 0;
|
||||
|
||||
const abortSignal = this.getExecutionCancelSignal();
|
||||
|
||||
if (options.maxTokens !== undefined) {
|
||||
options.max_completion_tokens = options.maxTokens;
|
||||
delete options.maxTokens;
|
||||
}
|
||||
|
||||
if (options.topP !== undefined) {
|
||||
options.top_p = options.topP;
|
||||
delete options.topP;
|
||||
}
|
||||
|
||||
let response_format;
|
||||
if (jsonOutput) {
|
||||
response_format = { type: 'json_object' };
|
||||
messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a helpful assistant designed to output JSON.',
|
||||
},
|
||||
...messages,
|
||||
];
|
||||
}
|
||||
|
||||
const hideTools = this.getNodeParameter('hideTools', i, '') as string;
|
||||
|
||||
let tools;
|
||||
let externalTools: Tool[] = [];
|
||||
|
||||
if (hideTools !== 'hide') {
|
||||
const enforceUniqueNames = nodeVersion > 1;
|
||||
externalTools = await getConnectedTools(this, enforceUniqueNames, false);
|
||||
}
|
||||
|
||||
if (externalTools.length) {
|
||||
tools = externalTools.length ? externalTools?.map(formatToOpenAIAssistantTool) : undefined;
|
||||
}
|
||||
|
||||
const body: IDataObject = {
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
response_format,
|
||||
..._omit(options, ['maxToolsIterations']),
|
||||
};
|
||||
|
||||
let response = (await apiRequest.call(this, 'POST', '/chat/completions', {
|
||||
body,
|
||||
})) as ChatCompletion;
|
||||
|
||||
if (!response) return [];
|
||||
|
||||
let currentIteration = 1;
|
||||
let toolCalls = response?.choices[0]?.message?.tool_calls;
|
||||
|
||||
while (toolCalls?.length) {
|
||||
// Break the loop if the max iterations is reached or the execution is canceled
|
||||
if (
|
||||
abortSignal?.aborted ||
|
||||
(maxToolsIterations > 0 && currentIteration >= maxToolsIterations)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
messages.push(response.choices[0].message);
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
const functionName = toolCall.function.name;
|
||||
const functionArgs = toolCall.function.arguments;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
messages.push({
|
||||
tool_call_id: toolCall.id,
|
||||
role: 'tool',
|
||||
content: functionResponse,
|
||||
});
|
||||
}
|
||||
|
||||
response = (await apiRequest.call(this, 'POST', '/chat/completions', {
|
||||
body,
|
||||
})) as ChatCompletion;
|
||||
|
||||
toolCalls = response.choices[0].message.tool_calls;
|
||||
currentIteration += 1;
|
||||
}
|
||||
|
||||
if (response_format) {
|
||||
response.choices = response.choices.map((choice) => {
|
||||
try {
|
||||
choice.message.content = JSON.parse(choice.message.content);
|
||||
} catch (error) {}
|
||||
return choice;
|
||||
});
|
||||
}
|
||||
|
||||
const simplify = this.getNodeParameter('simplify', i) as boolean;
|
||||
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
if (simplify) {
|
||||
for (const entry of response.choices) {
|
||||
returnData.push({
|
||||
json: entry,
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
returnData.push({ json: response, pairedItem: { item: i } });
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
Reference in New Issue
Block a user