@n8n/n8n-nodes-langchain
Version:

827 lines (825 loc) • 27.5 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var ChatTrigger_node_exports = {};
__export(ChatTrigger_node_exports, {
ChatTrigger: () => ChatTrigger
});
module.exports = __toCommonJS(ChatTrigger_node_exports);
var import_pick = __toESM(require("lodash/pick"));
var import_n8n_workflow = require("n8n-workflow");
var a = __toESM(require("node:assert"));
var import_constants = require("./constants");
var import_GenericFunctions = require("./GenericFunctions");
var import_templates = require("./templates");
var import_types = require("./types");
const CHAT_TRIGGER_PATH_IDENTIFIER = "chat";
const allowFileUploadsOption = {
displayName: "Allow File Uploads",
name: "allowFileUploads",
type: "boolean",
default: false,
description: "Whether to allow file uploads in the chat"
};
const allowedFileMimeTypeOption = {
displayName: "Allowed File Mime Types",
name: "allowedFilesMimeTypes",
type: "string",
default: "*",
placeholder: "e.g. image/*, text/*, application/pdf",
description: 'Allowed file types for upload. Comma-separated list of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types" target="_blank">MIME types</a>.'
};
const respondToWebhookResponseMode = {
name: "Using 'Respond to Webhook' Node",
value: "responseNode",
description: "Response defined in that node"
};
const lastNodeResponseMode = {
name: "When Last Node Finishes",
value: "lastNode",
description: "Returns data of the last-executed node"
};
const streamingResponseMode = {
name: "Streaming",
value: "streaming",
description: "Streaming response from specified nodes (e.g. Agents)"
};
const respondNodesResponseMode = {
name: "Using Response Nodes",
value: "responseNodes",
description: "Send responses to the chat by using 'Respond to Chat' node"
};
const commonOptionsFields = [
// CORS parameters are only valid for when chat is used in hosted or webhook mode
{
displayName: "Allowed Origins (CORS)",
name: "allowedOrigins",
type: "string",
default: "*",
description: "Comma-separated list of URLs allowed for cross-origin non-preflight requests. Use * (default) to allow all origins.",
displayOptions: {
show: {
"/mode": ["hostedChat", "webhook"]
}
}
},
{
...allowFileUploadsOption,
displayOptions: {
show: {
"/mode": ["hostedChat"]
}
}
},
{
...allowedFileMimeTypeOption,
displayOptions: {
show: {
"/mode": ["hostedChat"]
}
}
},
{
displayName: "Input Placeholder",
name: "inputPlaceholder",
type: "string",
displayOptions: {
show: {
"/mode": ["hostedChat"]
}
},
default: "Type your question..",
placeholder: "e.g. Type your message here",
description: "Shown as placeholder text in the chat input field"
},
{
displayName: "Load Previous Session",
name: "loadPreviousSession",
type: "options",
options: [
{
name: "Off",
value: "notSupported",
description: "Loading messages of previous session is turned off"
},
{
name: "From Memory",
value: "memory",
description: "Load session messages from memory"
},
{
name: "Manually",
value: "manually",
description: "Manually return messages of session"
}
],
default: "notSupported",
description: "If loading messages of a previous session should be enabled"
},
{
displayName: "Require Button Click to Start Chat",
name: "showWelcomeScreen",
type: "boolean",
displayOptions: {
show: {
"/mode": ["hostedChat"]
}
},
default: false,
description: "Whether to show the welcome screen at the start of the chat"
},
{
displayName: "Start Conversation Button Text",
name: "getStarted",
type: "string",
displayOptions: {
show: {
showWelcomeScreen: [true],
"/mode": ["hostedChat"]
}
},
default: "New Conversation",
placeholder: "e.g. New Conversation",
description: "Shown as part of the welcome screen, in the middle of the chat window"
},
{
displayName: "Subtitle",
name: "subtitle",
type: "string",
displayOptions: {
show: {
"/mode": ["hostedChat"]
}
},
default: "Start a chat. We're here to help you 24/7.",
placeholder: "e.g. We're here for you",
description: "Shown at the top of the chat, under the title"
},
{
displayName: "Title",
name: "title",
type: "string",
displayOptions: {
show: {
"/mode": ["hostedChat"]
}
},
default: "Hi there! \u{1F44B}",
placeholder: "e.g. Welcome",
description: "Shown at the top of the chat"
},
{
displayName: "Custom Chat Styling",
name: "customCss",
type: "string",
typeOptions: {
rows: 10,
editor: "cssEditor"
},
displayOptions: {
show: {
"/mode": ["hostedChat"]
}
},
default: `
${import_constants.cssVariables}
/* You can override any class styles, too. Right-click inspect in Chat UI to find class to override. */
.chat-message {
max-width: 50%;
}
`.trim(),
description: "Override default styling of the public chat interface with CSS"
}
];
class ChatTrigger extends import_n8n_workflow.Node {
constructor() {
super(...arguments);
this.description = {
displayName: "Chat Trigger",
name: "chatTrigger",
icon: "fa:comments",
iconColor: "black",
group: ["trigger"],
version: [1, 1.1, 1.2, 1.3, 1.4],
defaultVersion: 1.4,
description: "Runs the workflow when an n8n generated webchat is submitted",
defaults: {
name: "When chat message received"
},
codex: {
categories: ["Core Nodes"],
resources: {
primaryDocumentation: [
{
url: "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.chattrigger/"
}
]
}
},
maxNodes: 1,
inputs: `={{ (() => {
if (!['hostedChat', 'webhook'].includes($parameter.mode)) {
return [];
}
if ($parameter.options?.loadPreviousSession !== 'memory') {
return [];
}
return [
{
displayName: 'Memory',
maxConnections: 1,
type: '${import_n8n_workflow.NodeConnectionTypes.AiMemory}',
required: true,
}
];
})() }}`,
outputs: [import_n8n_workflow.NodeConnectionTypes.Main],
credentials: [
{
// eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed
name: "httpBasicAuth",
required: true,
displayOptions: {
show: {
authentication: ["basicAuth"]
}
}
}
],
webhooks: [
{
name: "setup",
httpMethod: "GET",
responseMode: "onReceived",
path: CHAT_TRIGGER_PATH_IDENTIFIER,
ndvHideUrl: true
},
{
name: "default",
httpMethod: "POST",
responseMode: '={{$parameter.options?.["responseMode"] ?? ($parameter.availableInChat ? "streaming" : "lastNode") }}',
path: CHAT_TRIGGER_PATH_IDENTIFIER,
ndvHideMethod: true,
ndvHideUrl: "={{ !$parameter.public }}"
}
],
eventTriggerDescription: "Waiting for you to submit the chat",
activationMessage: "You can now make calls to your production chat URL.",
triggerPanel: false,
properties: [
/**
* @note If we change this property, also update it in ChatEmbedModal.vue
*/
{
displayName: "Make Chat Publicly Available",
name: "public",
type: "boolean",
default: false,
description: "Whether the chat should be publicly available or only accessible through the manual chat interface"
},
{
displayName: "Mode",
name: "mode",
type: "options",
options: [
{
name: "Hosted Chat",
value: "hostedChat",
description: "Chat on a page served by n8n"
},
{
name: "Embedded Chat",
value: "webhook",
description: "Chat through a widget embedded in another page, or by calling a webhook"
}
],
default: "hostedChat",
displayOptions: {
show: {
public: [true]
}
}
},
{
displayName: "Chat will be live at the URL above once this workflow is published. Live executions will show up in the \u2018executions\u2019 tab",
name: "hostedChatNotice",
type: "notice",
displayOptions: {
show: {
mode: ["hostedChat"],
public: [true]
}
},
default: ""
},
{
displayName: 'Follow the instructions <a href="https://www.npmjs.com/package/@n8n/chat" target="_blank">here</a> to embed chat in a webpage (or just call the webhook URL at the top of this section). Chat will be live once you publish this workflow',
name: "embeddedChatNotice",
type: "notice",
displayOptions: {
show: {
mode: ["webhook"],
public: [true]
}
},
default: ""
},
{
displayName: "Authentication",
name: "authentication",
type: "options",
displayOptions: {
show: {
public: [true]
}
},
options: [
{
name: "Basic Auth",
value: "basicAuth",
description: "Simple username and password (the same one for all users)"
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: "n8n User Auth",
value: "n8nUserAuth",
description: "Require user to be logged in with their n8n account"
},
{
name: "None",
value: "none"
}
],
default: "none",
description: "The way to authenticate"
},
{
displayName: "Initial Message(s)",
name: "initialMessages",
type: "string",
displayOptions: {
show: {
mode: ["hostedChat"],
public: [true]
}
},
typeOptions: {
rows: 3
},
default: "Hi there! \u{1F44B}\nMy name is Nathan. How can I assist you today?",
description: "Default messages shown at the start of the chat, one per line"
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
displayName: "Make Available in n8n Chat",
name: "availableInChat",
type: "boolean",
default: false,
noDataExpression: true,
description: "Whether to make the agent available in n8n Chat"
},
{
displayName: "Your Chat Trigger node is out of date. To update, delete this node and insert a new Chat Trigger node.",
name: "availableInChatNotice",
type: "notice",
displayOptions: {
show: {
availableInChat: [true],
"@version": [{ _cnd: { lt: 1.2 } }]
}
},
default: ""
},
{
displayName: 'Your n8n users will be able to use this agent in <a href="/home/chat/" target="_blank">Chat</a> once this workflow is published. Make sure to share this workflow with at least Project Chat User access to all users who should use it. Currently, only streaming response mode is supported.',
name: "availableInChatNotice",
type: "notice",
displayOptions: {
show: {
availableInChat: [true],
"@version": [{ _cnd: { gte: 1.2 } }]
}
},
default: ""
},
{
displayName: "Agent Icon",
name: "agentIcon",
type: "icon",
default: { type: "icon", value: "bot" },
noDataExpression: true,
description: "The icon of the agent on n8n Chat",
displayOptions: {
show: {
availableInChat: [true],
"@version": [{ _cnd: { gte: 1.2 } }]
}
}
},
{
displayName: "Agent Name",
name: "agentName",
type: "string",
default: "",
noDataExpression: true,
description: "The name of the agent on n8n Chat. Name of the workflow is used if left empty.",
displayOptions: {
show: {
availableInChat: [true],
"@version": [{ _cnd: { gte: 1.2 } }]
}
}
},
{
displayName: "Agent Description",
name: "agentDescription",
type: "string",
typeOptions: {
rows: 2
},
default: "",
noDataExpression: true,
description: "The description of the agent on n8n Chat",
displayOptions: {
show: {
availableInChat: [true],
"@version": [{ _cnd: { gte: 1.2 } }]
}
}
},
{
displayName: "Options",
name: "options",
type: "collection",
displayOptions: {
show: {
public: [false],
"@version": [1, 1.1]
}
},
placeholder: "Add Field",
default: {},
options: [allowFileUploadsOption, allowedFileMimeTypeOption]
},
// Options for versions 1.0 and 1.1 (without streaming)
{
displayName: "Options",
name: "options",
type: "collection",
displayOptions: {
show: {
mode: ["hostedChat", "webhook"],
public: [true],
"@version": [1, 1.1]
}
},
placeholder: "Add Field",
default: {},
options: [
...commonOptionsFields,
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [lastNodeResponseMode, respondToWebhookResponseMode],
default: "lastNode",
description: "When and how to respond to the webhook"
}
]
},
// Options for version 1.2 (with streaming)
{
displayName: "Options",
name: "options",
type: "collection",
displayOptions: {
show: {
mode: ["hostedChat", "webhook"],
public: [true],
"@version": [1.2]
}
},
placeholder: "Add Field",
default: {},
options: [
...commonOptionsFields,
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [lastNodeResponseMode, respondToWebhookResponseMode, streamingResponseMode],
default: "lastNode",
description: "When and how to respond to the webhook",
displayOptions: { show: { "/availableInChat": [false] } }
},
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [streamingResponseMode],
default: "streaming",
description: "When and how to respond to the webhook",
displayOptions: { show: { "/availableInChat": [true] } }
}
]
},
{
displayName: "Options",
name: "options",
type: "collection",
displayOptions: {
show: {
public: [false],
"@version": [{ _cnd: { gte: 1.3 } }]
}
},
placeholder: "Add Field",
default: {},
options: [
allowFileUploadsOption,
allowedFileMimeTypeOption,
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [lastNodeResponseMode, respondNodesResponseMode, streamingResponseMode],
default: "lastNode",
description: "When and how to respond to the chat",
displayOptions: { show: { "/availableInChat": [false] } }
},
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [streamingResponseMode],
default: "streaming",
description: "When and how to respond to the webhook",
displayOptions: { show: { "/availableInChat": [true] } }
}
]
},
{
displayName: "Options",
name: "options",
type: "collection",
displayOptions: {
show: {
mode: ["hostedChat", "webhook"],
public: [true],
"@version": [{ _cnd: { gte: 1.3 } }]
}
},
placeholder: "Add Field",
default: {},
options: [
...commonOptionsFields,
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [streamingResponseMode],
default: "streaming",
description: "When and how to respond to the webhook",
displayOptions: { show: { "/availableInChat": [true] } }
},
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [lastNodeResponseMode, streamingResponseMode, respondToWebhookResponseMode],
default: "lastNode",
description: "When and how to respond to the chat",
displayOptions: { show: { "/mode": ["webhook"], "/availableInChat": [false] } }
},
{
displayName: "Response Mode",
name: "responseMode",
type: "options",
options: [lastNodeResponseMode, streamingResponseMode, respondNodesResponseMode],
default: "lastNode",
description: "When and how to respond to the webhook",
displayOptions: { show: { "/mode": ["hostedChat"], "/availableInChat": [false] } }
}
]
}
]
};
}
async handleFormData(context) {
const req = context.getRequestObject();
a.ok(req.contentType === "multipart/form-data", "Expected multipart/form-data");
const options = context.getNodeParameter("options", {});
const { data, files } = req.body;
const returnItem = {
json: data
};
if (files && Object.keys(files).length) {
returnItem.json.files = [];
returnItem.binary = {};
const count = 0;
for (const fileKey of Object.keys(files)) {
const processedFiles = [];
if (Array.isArray(files[fileKey])) {
processedFiles.push(...files[fileKey]);
} else {
processedFiles.push(files[fileKey]);
}
let fileIndex = 0;
for (const file of processedFiles) {
let binaryPropertyName = "data";
if (binaryPropertyName.endsWith("[]")) {
binaryPropertyName = binaryPropertyName.slice(0, -2);
}
if (options.binaryPropertyName) {
binaryPropertyName = `${options.binaryPropertyName.toString()}${count}`;
}
const binaryFile = await context.nodeHelpers.copyBinaryFile(
file.filepath,
file.originalFilename ?? file.newFilename,
file.mimetype
);
const binaryKey = `${binaryPropertyName}${fileIndex}`;
const binaryInfo = {
...(0, import_pick.default)(binaryFile, ["fileName", "fileSize", "fileType", "mimeType", "fileExtension"]),
binaryKey
};
returnItem.binary = Object.assign(returnItem.binary ?? {}, {
[`${binaryKey}`]: binaryFile
});
returnItem.json.files = [
...returnItem.json.files,
binaryInfo
];
fileIndex += 1;
}
}
}
return returnItem;
}
async webhook(ctx) {
const res = ctx.getResponseObject();
const isPublic = ctx.getNodeParameter("public", false);
(0, import_n8n_workflow.assertParamIsBoolean)("public", isPublic, ctx.getNode());
const nodeMode = ctx.getNodeParameter("mode", "hostedChat");
(0, import_n8n_workflow.assertParamIsString)("mode", nodeMode, ctx.getNode());
if (!isPublic) {
res.status(404).end();
return {
noWebhookResponse: true
};
}
const availableInChat = ctx.getNodeParameter("availableInChat", false);
const options = ctx.getNodeParameter("options", {});
(0, import_n8n_workflow.validateNodeParameters)(
options,
{
getStarted: { type: "string" },
inputPlaceholder: { type: "string" },
loadPreviousSession: { type: "string" },
showWelcomeScreen: { type: "boolean" },
subtitle: { type: "string" },
title: { type: "string" },
allowFileUploads: { type: "boolean" },
allowedFilesMimeTypes: { type: "string" },
customCss: { type: "string" },
responseMode: { type: "string" }
},
ctx.getNode()
);
const loadPreviousSession = options.loadPreviousSession;
(0, import_types.assertValidLoadPreviousSessionOption)(loadPreviousSession, ctx.getNode());
const enableStreaming = availableInChat ? !options.responseMode || options.responseMode === "streaming" : options.responseMode === "streaming";
const req = ctx.getRequestObject();
const webhookName = ctx.getWebhookName();
const mode = ctx.getMode() === "manual" ? "test" : "production";
const bodyData = ctx.getBodyData() ?? {};
try {
await (0, import_GenericFunctions.validateAuth)(ctx);
} catch (error) {
if (error) {
res.writeHead(error.responseCode, {
"www-authenticate": 'Basic realm="Webhook"'
});
res.end(error.message);
return { noWebhookResponse: true };
}
throw error;
}
if (nodeMode === "hostedChat") {
if (webhookName === "setup") {
const webhookUrlRaw = ctx.getNodeWebhookUrl("default");
if (!webhookUrlRaw) {
throw new import_n8n_workflow.NodeOperationError(ctx.getNode(), "Default webhook url not set");
}
const webhookUrl = mode === "test" ? webhookUrlRaw.replace("/webhook", "/webhook-test") : webhookUrlRaw;
const authentication = ctx.getNodeParameter("authentication");
const initialMessagesRaw = ctx.getNodeParameter("initialMessages", "");
(0, import_n8n_workflow.assertParamIsString)("initialMessage", initialMessagesRaw, ctx.getNode());
const instanceId = ctx.getInstanceId();
const i18nConfig = {};
const keys = ["getStarted", "inputPlaceholder", "subtitle", "title"];
for (const key of keys) {
if (options[key] !== void 0) {
i18nConfig[key] = options[key];
}
}
const page = (0, import_templates.createPage)({
i18n: {
en: i18nConfig
},
showWelcomeScreen: options.showWelcomeScreen,
loadPreviousSession,
initialMessages: initialMessagesRaw,
webhookUrl,
mode,
instanceId,
authentication,
allowFileUploads: options.allowFileUploads,
allowedFilesMimeTypes: options.allowedFilesMimeTypes,
customCss: options.customCss,
enableStreaming
});
res.status(200).send(page).end();
return {
noWebhookResponse: true
};
}
}
if (bodyData.action === "loadPreviousSession") {
if (options?.loadPreviousSession === "memory") {
const memory = await ctx.getInputConnectionData(import_n8n_workflow.NodeConnectionTypes.AiMemory, 0);
const messages = (await memory?.chatHistory.getMessages() ?? []).filter((message) => !message?.additional_kwargs?.hideFromUI).map((message) => message?.toJSON());
return {
webhookResponse: { data: messages }
};
} else if (!options?.loadPreviousSession || options?.loadPreviousSession === "notSupported") {
return {
webhookResponse: { data: [] }
};
}
}
let returnData;
const webhookResponse = { status: 200 };
if (enableStreaming) {
res.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Cache-Control": "no-cache",
Connection: "keep-alive"
});
res.flushHeaders();
if (req.contentType === "multipart/form-data") {
returnData = [await this.handleFormData(ctx)];
} else {
returnData = [{ json: bodyData }];
}
return {
workflowData: [ctx.helpers.returnJsonArray(returnData)],
noWebhookResponse: true
};
}
if (req.contentType === "multipart/form-data") {
returnData = [await this.handleFormData(ctx)];
return {
webhookResponse,
workflowData: [returnData]
};
} else {
returnData = [{ json: bodyData }];
}
return {
webhookResponse,
workflowData: [ctx.helpers.returnJsonArray(returnData)]
};
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ChatTrigger
});
//# sourceMappingURL=ChatTrigger.node.js.map