mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-28 15:27:03 +02:00
381 lines
9.2 KiB
TypeScript
381 lines
9.2 KiB
TypeScript
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
|
|
import type { BaseChatMemory } from '@langchain/classic/memory';
|
|
import { autoSaveHighlightedDataProperty } from 'n8n-nodes-base/dist/utils/highlightedData';
|
|
import {
|
|
limitWaitTimeOption,
|
|
sendAndWaitWebhooksDescription,
|
|
} from 'n8n-nodes-base/dist/utils/sendAndWait/descriptions';
|
|
import {
|
|
SEND_AND_WAIT_WAITING_TOOLTIP,
|
|
sendAndWaitWebhook,
|
|
} from 'n8n-nodes-base/dist/utils/sendAndWait/utils';
|
|
import {
|
|
CHAT_TRIGGER_NODE_TYPE,
|
|
CHAT_WAIT_USER_REPLY,
|
|
FREE_TEXT_CHAT_RESPONSE_TYPE,
|
|
NodeConnectionTypes,
|
|
NodeOperationError,
|
|
SEND_AND_WAIT_OPERATION,
|
|
getHighlightedInputKey,
|
|
getHighlightedResponseKey,
|
|
} from 'n8n-workflow';
|
|
import type {
|
|
IExecuteFunctions,
|
|
INodeExecutionData,
|
|
INodeTypeDescription,
|
|
INodeType,
|
|
NodeTypeAndVersion,
|
|
INode,
|
|
IDataObject,
|
|
} from 'n8n-workflow';
|
|
|
|
import {
|
|
configureInputs,
|
|
configureWaitTillDate,
|
|
getChatMessage,
|
|
getSendAndWaitPropertiesForChatNode,
|
|
} from './util';
|
|
|
|
export class Chat implements INodeType {
|
|
description: INodeTypeDescription = {
|
|
usableAsTool: true,
|
|
displayName: 'Chat',
|
|
name: 'chat',
|
|
icon: 'node:chat-trigger',
|
|
iconColor: 'black',
|
|
group: ['input'],
|
|
version: [1, 1.1, 1.2, 1.3],
|
|
defaultVersion: 1.3,
|
|
description: 'Send a message into the chat',
|
|
defaults: {
|
|
name: 'Chat',
|
|
},
|
|
builderHint: {
|
|
relatedNodes: [
|
|
{
|
|
nodeType: '@n8n/n8n-nodes-langchain.chatTrigger',
|
|
relationHint:
|
|
'Required trigger for this node to work - must set responseMode to "responseNodes"',
|
|
},
|
|
],
|
|
},
|
|
codex: {
|
|
categories: ['Core Nodes', 'HITL'],
|
|
subcategories: {
|
|
HITL: ['Human in the Loop'],
|
|
},
|
|
alias: ['human', 'wait', 'hitl', 'respond', 'approve', 'confirm', 'send', 'message'],
|
|
resources: {
|
|
primaryDocumentation: [
|
|
{
|
|
url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.respondtochat/',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
inputs: `={{ (${configureInputs})($parameter) }}`,
|
|
outputs: [NodeConnectionTypes.Main],
|
|
waitingNodeTooltip: SEND_AND_WAIT_WAITING_TOOLTIP,
|
|
webhooks: sendAndWaitWebhooksDescription,
|
|
properties: [
|
|
{
|
|
displayName:
|
|
"Verify you're using a chat trigger with the 'Response Mode' option set to 'Using Response Nodes'",
|
|
name: 'generalNotice',
|
|
type: 'notice',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'operation',
|
|
type: 'options',
|
|
default: 'send',
|
|
noDataExpression: true,
|
|
options: [
|
|
{
|
|
name: 'Send Message',
|
|
value: 'send',
|
|
action: 'Send a message',
|
|
},
|
|
{
|
|
name: 'Send and Wait for Response',
|
|
value: SEND_AND_WAIT_OPERATION,
|
|
action: 'Send message and wait for response',
|
|
},
|
|
],
|
|
displayOptions: {
|
|
show: {
|
|
'@version': [{ _cnd: { gte: 1.1 } }],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Message',
|
|
name: 'message',
|
|
type: 'string',
|
|
default: '',
|
|
required: true,
|
|
typeOptions: {
|
|
rows: 4,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Wait for User Reply',
|
|
name: CHAT_WAIT_USER_REPLY,
|
|
type: 'boolean',
|
|
default: true,
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
'@version': [{ _cnd: { lt: 1.1 } }],
|
|
},
|
|
},
|
|
},
|
|
...getSendAndWaitPropertiesForChatNode(),
|
|
{
|
|
displayName: 'Options',
|
|
name: 'options',
|
|
type: 'collection',
|
|
placeholder: 'Add Option',
|
|
default: {},
|
|
displayOptions: {
|
|
hide: {
|
|
'@tool': [true],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
displayName: 'Add Memory Input Connection',
|
|
name: 'memoryConnection',
|
|
type: 'boolean',
|
|
default: false,
|
|
displayOptions: {
|
|
hide: {
|
|
'/responseType': ['approval'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
...limitWaitTimeOption,
|
|
displayOptions: {
|
|
show: {
|
|
[`/${CHAT_WAIT_USER_REPLY}`]: [true],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
...limitWaitTimeOption,
|
|
displayOptions: {
|
|
show: {
|
|
'/operation': [SEND_AND_WAIT_OPERATION],
|
|
},
|
|
},
|
|
},
|
|
autoSaveHighlightedDataProperty,
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Options',
|
|
name: 'options',
|
|
type: 'collection',
|
|
placeholder: 'Add Option',
|
|
default: {},
|
|
options: [limitWaitTimeOption, autoSaveHighlightedDataProperty],
|
|
displayOptions: {
|
|
show: {
|
|
'@tool': [true],
|
|
[`/${CHAT_WAIT_USER_REPLY}`]: [true],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Options',
|
|
name: 'options',
|
|
type: 'collection',
|
|
placeholder: 'Add Option',
|
|
default: {},
|
|
options: [limitWaitTimeOption, autoSaveHighlightedDataProperty],
|
|
displayOptions: {
|
|
show: {
|
|
'@tool': [true],
|
|
'/operation': [SEND_AND_WAIT_OPERATION],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
webhook = sendAndWaitWebhook;
|
|
|
|
async onMessage(
|
|
context: IExecuteFunctions,
|
|
data: INodeExecutionData,
|
|
): Promise<INodeExecutionData[][]> {
|
|
const options = context.getNodeParameter('options', 0, {}) as {
|
|
memoryConnection?: boolean;
|
|
};
|
|
|
|
const nodeVersion = context.getNode().typeVersion;
|
|
let waitForReply;
|
|
if (nodeVersion >= 1.1) {
|
|
const operation = context.getNodeParameter('operation', 0, 'sendMessage');
|
|
waitForReply = operation === SEND_AND_WAIT_OPERATION;
|
|
} else {
|
|
waitForReply = context.getNodeParameter(CHAT_WAIT_USER_REPLY, 0, true) as boolean;
|
|
}
|
|
|
|
if (!waitForReply) {
|
|
// return original message instead of input data
|
|
if (nodeVersion >= 1.3) return [[data]];
|
|
|
|
const inputData = context.getInputData();
|
|
return [inputData];
|
|
}
|
|
|
|
const message = data.json?.chatInput as string;
|
|
if (context.getNodeParameter('options.autoSaveHighlightedData', 0, true) !== false) {
|
|
context.customData.set(getHighlightedInputKey(context.getNode().name), message);
|
|
}
|
|
|
|
if (options.memoryConnection) {
|
|
const memory = (await context.getInputConnectionData(NodeConnectionTypes.AiMemory, 0)) as
|
|
| BaseChatMemory
|
|
| undefined;
|
|
|
|
if (memory && message) {
|
|
await memory.chatHistory.addUserMessage(message);
|
|
}
|
|
}
|
|
|
|
if (nodeVersion < 1.1) {
|
|
return [[data]];
|
|
}
|
|
|
|
const responseType = context.getNodeParameter(
|
|
'responseType',
|
|
0,
|
|
FREE_TEXT_CHAT_RESPONSE_TYPE,
|
|
) as string;
|
|
const isFreeText = responseType === FREE_TEXT_CHAT_RESPONSE_TYPE;
|
|
|
|
if (nodeVersion <= 1.1) {
|
|
return [
|
|
[
|
|
{
|
|
...data,
|
|
json: {
|
|
// put everything under the `data` key to be consistent
|
|
// with other HITL nodes
|
|
data: {
|
|
...data.json,
|
|
// if the response type is not "Free Text" and the
|
|
// user has typed something - we assume it's
|
|
// disapproval
|
|
approved: isFreeText ? undefined : false,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
];
|
|
}
|
|
|
|
let nestedData: IDataObject = {};
|
|
if (typeof data.json.data === 'object') {
|
|
nestedData = {
|
|
...data.json.data,
|
|
};
|
|
}
|
|
|
|
// if the response type is not "Free Text" and the
|
|
// user has typed something - we assume it's
|
|
// disapproval
|
|
if (!isFreeText) {
|
|
nestedData.approved = false;
|
|
}
|
|
|
|
return [
|
|
[
|
|
{
|
|
...data,
|
|
json: {
|
|
// for v1.2+, don't nest under the `data` key so that Chat
|
|
// node can be connected to the AI Agent directly
|
|
// (it expects `$json.chatInput` field)
|
|
...data.json,
|
|
data: Object.keys(nestedData).length > 0 ? nestedData : undefined,
|
|
},
|
|
},
|
|
],
|
|
];
|
|
}
|
|
|
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
const connectedNodes = this.getParentNodes(this.getNode().name, {
|
|
includeNodeParameters: true,
|
|
});
|
|
|
|
let chatTrigger: INode | NodeTypeAndVersion | undefined | null = connectedNodes.find(
|
|
(node) => node.type === CHAT_TRIGGER_NODE_TYPE && !node.disabled,
|
|
);
|
|
|
|
if (!chatTrigger) {
|
|
try {
|
|
// try to get chat trigger from workflow if node working as a tool
|
|
chatTrigger = this.getChatTrigger();
|
|
} catch (error) {}
|
|
}
|
|
|
|
if (!chatTrigger) {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
'Workflow must be started from a chat trigger node',
|
|
);
|
|
}
|
|
|
|
const parameters = chatTrigger.parameters as {
|
|
mode?: 'hostedChat' | 'webhook';
|
|
options: { responseMode: 'lastNode' | 'responseNodes' | 'streaming' | 'responseNode' };
|
|
};
|
|
|
|
if (parameters.mode === 'webhook') {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
'"Embedded chat" is not supported, change the "Mode" in the chat trigger node to the "Hosted Chat"',
|
|
);
|
|
}
|
|
|
|
if (parameters.options.responseMode !== 'responseNodes') {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
'"Response Mode" in the chat trigger node must be set to "Using Response Nodes"',
|
|
);
|
|
}
|
|
|
|
const message = getChatMessage(this);
|
|
const options = this.getNodeParameter('options', 0, {}) as {
|
|
memoryConnection?: boolean;
|
|
};
|
|
|
|
if (this.getNodeParameter('options.autoSaveHighlightedData', 0, true) !== false) {
|
|
const responseText = typeof message === 'string' ? message : message.text;
|
|
this.customData.set(getHighlightedResponseKey(this.getNode().name), responseText);
|
|
}
|
|
|
|
if (options.memoryConnection) {
|
|
const memory = (await this.getInputConnectionData(NodeConnectionTypes.AiMemory, 0)) as
|
|
| BaseChatMemory
|
|
| undefined;
|
|
|
|
if (memory) {
|
|
const text = typeof message === 'string' ? message : message.text;
|
|
await memory.chatHistory.addAIMessage(text);
|
|
}
|
|
}
|
|
|
|
const waitTill = configureWaitTillDate(this);
|
|
|
|
await this.putExecutionToWait(waitTill);
|
|
return [[{ json: {}, sendMessage: message }]];
|
|
}
|
|
}
|