import { AgentExecutor } from 'langchain/agents'; import { OpenAI as OpenAIClient } from 'openai'; import { OpenAIAssistantRunnable } from 'langchain/experimental/openai_assistant'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; import type { OpenAIToolType } from 'langchain/dist/experimental/openai_assistant/schema'; import { getConnectedTools } from '../../../utils/helpers'; import { getTracingConfig } from '../../../utils/tracing'; import { formatToOpenAIAssistantTool } from './utils'; export class OpenAiAssistant implements INodeType { description: INodeTypeDescription = { displayName: 'OpenAI Assistant', name: 'openAiAssistant', hidden: true, icon: 'fa:robot', group: ['transform'], version: [1, 1.1], description: 'Utilizes Assistant API from Open AI.', subtitle: 'Open AI Assistant', defaults: { name: 'OpenAI Assistant', color: '#404040', }, codex: { alias: ['LangChain'], categories: ['AI'], subcategories: { AI: ['Agents'], }, resources: { primaryDocumentation: [ { url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.openaiassistant/', }, ], }, }, inputs: [ { type: NodeConnectionType.Main }, { type: NodeConnectionType.AiTool, displayName: 'Tools' }, ], outputs: [NodeConnectionType.Main], credentials: [ { name: 'openAiApi', required: true, }, ], requestDefaults: { ignoreHttpStatusErrors: true, baseURL: '={{ $parameter.options?.baseURL?.split("/").slice(0,-1).join("/") || "https://api.openai.com" }}', }, properties: [ { displayName: 'Operation', name: 'mode', type: 'options', noDataExpression: true, default: 'existing', options: [ { name: 'Use New Assistant', value: 'new', }, { name: 'Use Existing Assistant', value: 'existing', }, ], }, { displayName: 'Name', name: 'name', type: 'string', default: '', required: true, displayOptions: { show: { '/mode': ['new'], }, }, }, { displayName: 'Instructions', name: 'instructions', type: 'string', description: 'How the Assistant and model should behave or respond', default: '', typeOptions: { rows: 5, }, displayOptions: { show: { '/mode': ['new'], }, }, }, { displayName: 'Model', name: 'model', type: 'options', description: 'The model which will be used to power the assistant. Learn more. The Retrieval tool requires gpt-3.5-turbo-1106 and gpt-4-1106-preview models.', required: true, displayOptions: { show: { '/mode': ['new'], }, }, typeOptions: { loadOptions: { routing: { request: { method: 'GET', url: '={{ $parameter.options?.baseURL?.split("/").slice(-1).pop() || "v1" }}/models', }, output: { postReceive: [ { type: 'rootProperty', properties: { property: 'data', }, }, { type: 'filter', properties: { pass: "={{ $responseItem.id.startsWith('gpt-') && !$responseItem.id.includes('instruct') }}", }, }, { type: 'setKeyValue', properties: { name: '={{$responseItem.id}}', value: '={{$responseItem.id}}', }, }, { type: 'sort', properties: { key: 'name', }, }, ], }, }, }, }, routing: { send: { type: 'body', property: 'model', }, }, default: 'gpt-3.5-turbo-1106', }, { displayName: 'Assistant', name: 'assistantId', type: 'options', noDataExpression: true, displayOptions: { show: { '/mode': ['existing'], }, }, description: 'The assistant to use. Learn more.', typeOptions: { loadOptions: { routing: { request: { method: 'GET', headers: { 'OpenAI-Beta': 'assistants=v1', }, url: '={{ $parameter.options?.baseURL?.split("/").slice(-1).pop() || "v1" }}/assistants', }, output: { postReceive: [ { type: 'rootProperty', properties: { property: 'data', }, }, { type: 'setKeyValue', properties: { name: '={{$responseItem.name}}', value: '={{$responseItem.id}}', // eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string description: '={{$responseItem.model}}', }, }, { type: 'sort', properties: { key: 'name', }, }, ], }, }, }, }, routing: { send: { type: 'body', property: 'assistant', }, }, required: true, default: '', }, { displayName: 'Text', name: 'text', type: 'string', required: true, default: '={{ $json.chat_input }}', displayOptions: { show: { '@version': [1], }, }, }, { displayName: 'Text', name: 'text', type: 'string', required: true, default: '={{ $json.chatInput }}', displayOptions: { show: { '@version': [1.1], }, }, }, { displayName: 'OpenAI Tools', name: 'nativeTools', type: 'multiOptions', default: [], options: [ { name: 'Code Interpreter', value: 'code_interpreter', }, { name: 'Knowledge Retrieval', value: 'retrieval', }, ], }, { displayName: 'Connect your own custom tools to this node on the canvas', name: 'noticeTools', type: 'notice', default: '', }, { displayName: 'Upload files for retrieval using the OpenAI website', name: 'noticeTools', type: 'notice', typeOptions: { noticeTheme: 'info', }, displayOptions: { show: { '/nativeTools': ['retrieval'] } }, 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', }, { 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', }, ], }, ], }; async execute(this: IExecuteFunctions): Promise { const nodeVersion = this.getNode().typeVersion; const tools = await getConnectedTools(this, nodeVersion > 1); const credentials = await this.getCredentials('openAiApi'); const items = this.getInputData(); const returnData: INodeExecutionData[] = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { try { const input = this.getNodeParameter('text', itemIndex) as string; const assistantId = this.getNodeParameter('assistantId', itemIndex, '') as string; const nativeTools = this.getNodeParameter('nativeTools', itemIndex, []) as Array< 'code_interpreter' | 'retrieval' >; const options = this.getNodeParameter('options', itemIndex, {}) as { baseURL?: string; maxRetries: number; timeout: number; }; if (input === undefined) { throw new NodeOperationError(this.getNode(), 'The ‘text‘ parameter is empty.'); } const client = new OpenAIClient({ apiKey: credentials.apiKey as string, maxRetries: options.maxRetries ?? 2, timeout: options.timeout ?? 10000, baseURL: options.baseURL, }); let agent; const nativeToolsParsed: OpenAIToolType = nativeTools.map((tool) => ({ type: tool })); const transformedConnectedTools = tools?.map(formatToOpenAIAssistantTool) ?? []; const newTools = [...transformedConnectedTools, ...nativeToolsParsed]; // Existing agent, update tools with currently assigned if (assistantId) { agent = new OpenAIAssistantRunnable({ assistantId, client, asAgent: true }); await client.beta.assistants.update(assistantId, { tools: newTools, }); } else { const name = this.getNodeParameter('name', itemIndex, '') as string; const instructions = this.getNodeParameter('instructions', itemIndex, '') as string; const model = this.getNodeParameter('model', itemIndex, 'gpt-3.5-turbo-1106') as string; agent = await OpenAIAssistantRunnable.createAssistant({ model, client, instructions, name, tools: newTools, asAgent: true, }); } const agentExecutor = AgentExecutor.fromAgentAndTools({ agent, tools, }); const response = await agentExecutor.withConfig(getTracingConfig(this)).invoke({ content: input, signal: this.getExecutionCancelSignal(), timeout: options.timeout ?? 10000, }); returnData.push({ json: response }); } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: { item: itemIndex } }); continue; } throw error; } } return await this.prepareOutputData(returnData); } }