mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 00:54:06 -08:00
dcf12867b3
Fixes: - Refactor connection snapping when dragging and enable it also for non-main connection types - Fix propagation of errors from sub-nodes - Fix chat scrolling when sending/receiving messages - Prevent empty chat messages - Fix sub-node selected styles - Fix output names text overflow Usability improvements: - Auto-add manual chat trigger for agents & chain nodes - Various labels and description updates - Make the output parser input optional for Basic LLM Chain - Summarization Chain V2 with a simplified document loader & text chunking mode #### How to test the change: Example workflow showcasing different operation mode of the new summarization chain: [Summarization_V2.json](https://github.com/n8n-io/n8n/files/13599901/Summarization_V2.json) ## Issues fixed Include links to Github issue or Community forum post or **Linear ticket**: > Important in order to close automatically and provide context to reviewers - https://www.notion.so/n8n/David-Langchain-Posthog-notes-7a9294938420403095f4508f1a21d31d - https://linear.app/n8n/issue/N8N-7070/ux-fixes-batch - https://linear.app/n8n/issue/N8N-7071/ai-sub-node-bugs ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. > > *(internal)* You can use Slack commands to trigger [e2e tests](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#a39f9e5ba64a48b58a71d81c837e8227) or [deploy test instance](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#f6a177d32bde4b57ae2da0b8e454bfce) or [deploy early access version on Cloud](https://www.notion.so/n8n/Cloudbot-3dbe779836004972b7057bc989526998?pvs=4#fef2d36ab02247e1a0f65a74f6fb534e). --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Elias Meire <elias@meire.dev>
369 lines
9.1 KiB
TypeScript
369 lines
9.1 KiB
TypeScript
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: '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. <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: '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 <a href="https://platform.openai.com/playground" target="_blank">OpenAI website<a/>',
|
||
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<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);
|
||
}
|
||
}
|