-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add `OpenAI Assistant` node with support for both OpenAI tools and LC tools Github issue / Community forum post (link here to close automatically): - https://community.n8n.io/t/please-add-support-of-the-new-openai-features/32525/16 --------- Signed-off-by: Oleg Ivaniv <[email protected]> Co-authored-by: Jan Oberhauser <[email protected]>
- Loading branch information
1 parent
8d02f70
commit e5774cd
Showing
4 changed files
with
430 additions
and
11 deletions.
There are no files selected for viewing
352 changes: 352 additions & 0 deletions
352
packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,352 @@ | ||
import { AgentExecutor } from 'langchain/agents'; | ||
import { OpenAI as OpenAIClient } from 'openai'; | ||
import { OpenAIAssistantRunnable } from 'langchain/experimental/openai_assistant'; | ||
import { type Tool } from 'langchain/tools'; | ||
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 { formatToOpenAIAssistantTool } from './utils'; | ||
|
||
export class OpenAiAssistant implements INodeType { | ||
description: INodeTypeDescription = { | ||
displayName: 'OpenAI Assistant', | ||
name: 'openAiAssistant', | ||
icon: 'fa:robot', | ||
group: ['transform'], | ||
version: 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: 'Create 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, | ||
}, | ||
required: true, | ||
displayOptions: { | ||
show: { | ||
'/mode': ['new'], | ||
}, | ||
}, | ||
}, | ||
{ | ||
displayName: 'Model', | ||
name: 'model', | ||
type: 'options', | ||
description: | ||
'The model which will be used to power the assistant. <a href="https://beta.openai.com/docs/models/overview">Learn more</a>. 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. <a href="https://beta.openai.com/docs/assistants/overview">Learn more</a>.', | ||
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 }}', | ||
}, | ||
{ | ||
displayName: 'OpenAI Tools', | ||
name: 'nativeTools', | ||
type: 'multiOptions', | ||
default: [], | ||
options: [ | ||
{ | ||
name: 'Code Interpreter', | ||
value: 'code_interpreter', | ||
}, | ||
{ | ||
name: 'Retrieval', | ||
value: 'retrieval', | ||
}, | ||
], | ||
}, | ||
{ | ||
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<INodeExecutionData[][]> { | ||
const tools = (await this.getInputConnectionData(NodeConnectionType.AiTool, 0)) as Tool[]; | ||
const credentials = await this.getCredentials('openAiApi'); | ||
|
||
const items = this.getInputData(); | ||
const returnData: INodeExecutionData[] = []; | ||
|
||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { | ||
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.call({ | ||
content: input, | ||
signal: this.getExecutionCancelSignal(), | ||
timeout: options.timeout ?? 10000, | ||
}); | ||
|
||
returnData.push({ json: response }); | ||
} | ||
|
||
return this.prepareOutputData(returnData); | ||
} | ||
} |
Oops, something went wrong.