@n8n-plus/n8n-plus
Version:
n8n Workflow Automation Tool (plus edition)
688 lines (683 loc) • 28 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChatHubWorkflowService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const luxon_1 = require("luxon");
const n8n_workflow_1 = require("n8n-workflow");
const uuid_1 = require("uuid");
const constants_1 = require("../../constants");
const chat_hub_constants_1 = require("./chat-hub.constants");
const context_limits_1 = require("./context-limits");
const chat_hub_attachment_service_1 = require("./chat-hub.attachment.service");
let ChatHubWorkflowService = class ChatHubWorkflowService {
constructor(logger, workflowRepository, sharedWorkflowRepository, chatHubAttachmentService) {
this.logger = logger;
this.workflowRepository = workflowRepository;
this.sharedWorkflowRepository = sharedWorkflowRepository;
this.chatHubAttachmentService = chatHubAttachmentService;
}
async createChatWorkflow(userId, sessionId, projectId, history, humanMessage, attachments, credentials, model, systemMessage, tools, timeZone, trx) {
return await (0, db_1.withTransaction)(this.workflowRepository.manager, trx, async (em) => {
this.logger.debug(`Creating chat workflow for user ${userId} and session ${sessionId}, provider ${model.provider}`);
const { nodes, connections, executionData } = await this.buildChatWorkflow({
userId,
sessionId,
history,
humanMessage,
attachments,
credentials,
model,
systemMessage: systemMessage ?? this.getBaseSystemMessage(timeZone),
tools,
});
const newWorkflow = new db_1.WorkflowEntity();
newWorkflow.isArchived = true;
newWorkflow.versionId = (0, uuid_1.v4)();
newWorkflow.name = `Chat ${sessionId}`;
newWorkflow.active = false;
newWorkflow.activeVersionId = null;
newWorkflow.nodes = nodes;
newWorkflow.connections = connections;
newWorkflow.settings = {
executionOrder: 'v1',
};
const workflow = await em.save(newWorkflow);
await em.save(this.sharedWorkflowRepository.create({
role: 'workflow:owner',
projectId,
workflow,
}));
return {
workflowData: workflow,
executionData,
responseMode: 'streaming',
};
});
}
async createTitleGenerationWorkflow(userId, sessionId, projectId, humanMessage, attachments, credentials, model, trx) {
return await (0, db_1.withTransaction)(this.workflowRepository.manager, trx, async (em) => {
this.logger.debug(`Creating title generation workflow for user ${userId} and session ${sessionId}, provider ${model.provider}`);
const { nodes, connections, executionData } = this.buildTitleGenerationWorkflow(userId, sessionId, credentials, model, humanMessage, attachments);
const newWorkflow = new db_1.WorkflowEntity();
newWorkflow.isArchived = true;
newWorkflow.versionId = (0, uuid_1.v4)();
newWorkflow.name = `Chat ${sessionId} (Title Generation)`;
newWorkflow.active = false;
newWorkflow.activeVersionId = null;
newWorkflow.nodes = nodes;
newWorkflow.connections = connections;
newWorkflow.settings = {
executionOrder: 'v1',
saveDataSuccessExecution: 'all',
};
const workflow = await em.save(newWorkflow);
await em.save(this.sharedWorkflowRepository.create({
role: 'workflow:owner',
projectId,
workflow,
}));
return {
workflowData: workflow,
executionData,
};
});
}
prepareExecutionData(triggerNode, sessionId, message, attachments) {
return [
{
node: triggerNode,
data: {
main: [
[
{
json: {
sessionId,
action: 'sendMessage',
chatInput: message,
files: attachments.map(({ data, ...metadata }) => metadata),
},
binary: Object.fromEntries(attachments.map((attachment, index) => [`data${index}`, attachment])),
},
],
],
},
source: null,
},
];
}
parseInputModalities(options) {
const allowFileUploads = options?.allowFileUploads ?? false;
const allowedFilesMimeTypes = options?.allowedFilesMimeTypes;
if (!allowFileUploads) {
return ['text'];
}
if (!allowedFilesMimeTypes || allowedFilesMimeTypes === '*/*') {
return ['text', 'image', 'audio', 'video', 'file'];
}
const mimeTypes = allowedFilesMimeTypes.split(',').map((type) => type.trim());
const modalities = new Set(['text']);
for (const mimeType of mimeTypes) {
modalities.add(this.getMimeTypeModality(mimeType));
}
return Array.from(modalities);
}
getUniqueNodeName(originalName, existingNames) {
if (!existingNames.has(originalName)) {
return originalName;
}
let index = 1;
let uniqueName = `${originalName}${index}`;
while (existingNames.has(uniqueName)) {
index++;
uniqueName = `${originalName}${index}`;
}
return uniqueName;
}
async buildChatWorkflow({ userId, sessionId, history, humanMessage, attachments, credentials, model, systemMessage, tools, }) {
const chatTriggerNode = this.buildChatTriggerNode();
const toolsAgentNode = this.buildToolsAgentNode(model, systemMessage);
const modelNode = this.buildModelNode(credentials, model);
const memoryNode = this.buildMemoryNode(20);
const restoreMemoryNode = await this.buildRestoreMemoryNode(history, model);
const clearMemoryNode = this.buildClearMemoryNode();
const mergeNode = this.buildMergeNode();
const nodes = [
chatTriggerNode,
toolsAgentNode,
modelNode,
memoryNode,
restoreMemoryNode,
clearMemoryNode,
mergeNode,
];
const nodeNames = new Set(nodes.map((node) => node.name));
const distinctTools = tools.map((tool, i) => {
const position = [
700 + Math.floor(i / 3) * 60 + (i % 3) * 120,
300 + Math.floor(i / 3) * 120 - (i % 3) * 30,
];
const name = this.getUniqueNodeName(tool.name, nodeNames);
nodeNames.add(name);
return {
...tool,
name,
position,
};
});
nodes.push.apply(nodes, distinctTools);
const connections = {
[chat_hub_constants_1.NODE_NAMES.CHAT_TRIGGER]: {
[n8n_workflow_1.NodeConnectionTypes.Main]: [
[
{ node: chat_hub_constants_1.NODE_NAMES.RESTORE_CHAT_MEMORY, type: n8n_workflow_1.NodeConnectionTypes.Main, index: 0 },
{ node: chat_hub_constants_1.NODE_NAMES.MERGE, type: n8n_workflow_1.NodeConnectionTypes.Main, index: 0 },
],
],
},
[chat_hub_constants_1.NODE_NAMES.RESTORE_CHAT_MEMORY]: {
[n8n_workflow_1.NodeConnectionTypes.Main]: [
[{ node: chat_hub_constants_1.NODE_NAMES.MERGE, type: n8n_workflow_1.NodeConnectionTypes.Main, index: 1 }],
],
},
[chat_hub_constants_1.NODE_NAMES.MERGE]: {
[n8n_workflow_1.NodeConnectionTypes.Main]: [
[{ node: chat_hub_constants_1.NODE_NAMES.REPLY_AGENT, type: n8n_workflow_1.NodeConnectionTypes.Main, index: 0 }],
],
},
[chat_hub_constants_1.NODE_NAMES.CHAT_MODEL]: {
[n8n_workflow_1.NodeConnectionTypes.AiLanguageModel]: [
[{ node: chat_hub_constants_1.NODE_NAMES.REPLY_AGENT, type: n8n_workflow_1.NodeConnectionTypes.AiLanguageModel, index: 0 }],
],
},
[chat_hub_constants_1.NODE_NAMES.MEMORY]: {
[n8n_workflow_1.NodeConnectionTypes.AiMemory]: [
[
{ node: chat_hub_constants_1.NODE_NAMES.REPLY_AGENT, type: n8n_workflow_1.NodeConnectionTypes.AiMemory, index: 0 },
{ node: chat_hub_constants_1.NODE_NAMES.RESTORE_CHAT_MEMORY, type: n8n_workflow_1.NodeConnectionTypes.AiMemory, index: 0 },
{ node: chat_hub_constants_1.NODE_NAMES.CLEAR_CHAT_MEMORY, type: n8n_workflow_1.NodeConnectionTypes.AiMemory, index: 0 },
],
],
},
[chat_hub_constants_1.NODE_NAMES.REPLY_AGENT]: {
[n8n_workflow_1.NodeConnectionTypes.Main]: [
[
{
node: chat_hub_constants_1.NODE_NAMES.CLEAR_CHAT_MEMORY,
type: n8n_workflow_1.NodeConnectionTypes.Main,
index: 0,
},
],
],
},
...distinctTools.reduce((acc, tool) => {
acc[tool.name] = {
[n8n_workflow_1.NodeConnectionTypes.AiTool]: [
[
{
node: chat_hub_constants_1.NODE_NAMES.REPLY_AGENT,
type: n8n_workflow_1.NodeConnectionTypes.AiTool,
index: 0,
},
],
],
};
return acc;
}, {}),
};
const nodeExecutionStack = this.prepareExecutionData(chatTriggerNode, sessionId, humanMessage, attachments);
const executionData = (0, n8n_workflow_1.createRunExecutionData)({
executionData: {
nodeExecutionStack,
},
manualData: {
userId,
},
});
return { nodes, connections, executionData };
}
buildTitleGenerationWorkflow(userId, sessionId, credentials, model, humanMessage, attachments) {
const chatTriggerNode = this.buildChatTriggerNode();
const titleGeneratorAgentNode = this.buildTitleGeneratorAgentNode(humanMessage, attachments);
const modelNode = this.buildModelNode(credentials, model);
const nodes = [chatTriggerNode, titleGeneratorAgentNode, modelNode];
const connections = {
[chat_hub_constants_1.NODE_NAMES.CHAT_TRIGGER]: {
[n8n_workflow_1.NodeConnectionTypes.Main]: [
[{ node: chat_hub_constants_1.NODE_NAMES.TITLE_GENERATOR_AGENT, type: n8n_workflow_1.NodeConnectionTypes.Main, index: 0 }],
],
},
[chat_hub_constants_1.NODE_NAMES.CHAT_MODEL]: {
[n8n_workflow_1.NodeConnectionTypes.AiLanguageModel]: [
[
{
node: chat_hub_constants_1.NODE_NAMES.TITLE_GENERATOR_AGENT,
type: n8n_workflow_1.NodeConnectionTypes.AiLanguageModel,
index: 0,
},
],
],
},
};
const nodeExecutionStack = [
{
node: chatTriggerNode,
data: {
[n8n_workflow_1.NodeConnectionTypes.Main]: [
[
{
json: {
sessionId,
action: 'sendMessage',
chatInput: humanMessage,
},
},
],
],
},
source: null,
},
];
const executionData = (0, n8n_workflow_1.createRunExecutionData)({
executionData: {
nodeExecutionStack,
},
manualData: {
userId,
},
});
return { nodes, connections, executionData };
}
buildChatTriggerNode() {
return {
parameters: {},
type: n8n_workflow_1.CHAT_TRIGGER_NODE_TYPE,
typeVersion: 1.4,
position: [-448, -112],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.CHAT_TRIGGER,
webhookId: (0, uuid_1.v4)(),
};
}
getSystemMessageMetadata(timeZone) {
const now = constants_1.inE2ETests ? luxon_1.DateTime.fromISO('2025-01-15T12:00:00.000Z') : luxon_1.DateTime.now();
const isoTime = now.setZone(timeZone).toISO({ includeOffset: true });
return `The user's current local date and time is: ${isoTime} (timezone: ${timeZone}).
When you need to reference "now", use this date and time.
You can only produce text responses.
You cannot create, generate, edit, or display images, videos, or other non-text content.
If the user asks you to generate or edit an image (or other media), explain that you are not able to do that and, if helpful, describe in words what the image could look like or how they could create it using external tools.`;
}
getBaseSystemMessage(timeZone) {
return `You are a helpful assistant.
${this.getSystemMessageMetadata(timeZone)}`;
}
buildToolsAgentNode(model, systemMessage, enableStreaming = true) {
return {
parameters: {
promptType: 'define',
text: `={{ $('${chat_hub_constants_1.NODE_NAMES.CHAT_TRIGGER}').item.json.chatInput }}`,
options: {
enableStreaming,
maxTokensFromMemory: model.provider !== 'n8n' && model.provider !== 'custom-agent'
? (0, context_limits_1.getMaxContextWindowTokens)(model.provider, model.model)
: undefined,
systemMessage,
},
},
type: n8n_workflow_1.AGENT_LANGCHAIN_NODE_TYPE,
typeVersion: 3,
position: [608, 0],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.REPLY_AGENT,
};
}
buildModelNode(credentials, conversationModel) {
if (conversationModel.provider === 'n8n' || conversationModel.provider === 'custom-agent') {
throw new n8n_workflow_1.OperationalError('Custom agent workflows do not require a model node');
}
const { provider, model } = conversationModel;
const common = {
position: [608, 304],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.CHAT_MODEL,
credentials,
type: chat_hub_constants_1.PROVIDER_NODE_TYPE_MAP[provider].name,
typeVersion: chat_hub_constants_1.PROVIDER_NODE_TYPE_MAP[provider].version,
};
switch (provider) {
case 'openai':
return {
...common,
parameters: {
model: { __rl: true, mode: 'id', value: model },
options: {},
},
};
case 'anthropic':
return {
...common,
parameters: {
model: {
__rl: true,
mode: 'id',
value: model,
cachedResultName: model,
},
options: {},
},
};
case 'google':
return {
...common,
parameters: {
model: { __rl: true, mode: 'id', value: model },
options: {},
},
};
case 'azureOpenAi':
case 'azureEntraId':
return {
...common,
parameters: {
model,
options: {},
},
};
case 'ollama': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'awsBedrock': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'vercelAiGateway': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'xAiGrok': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'groq': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'openRouter': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'deepSeek': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'cohere': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
case 'mistralCloud': {
return {
...common,
parameters: {
model,
options: {},
},
};
}
default:
throw new n8n_workflow_1.OperationalError('Unsupported model provider');
}
}
buildMemoryNode(contextWindowLength) {
return {
parameters: {
sessionIdType: 'customKey',
sessionKey: `={{ $('${chat_hub_constants_1.NODE_NAMES.CHAT_TRIGGER}').item.json.sessionId }}`,
contextWindowLength,
},
type: n8n_workflow_1.MEMORY_BUFFER_WINDOW_NODE_TYPE,
typeVersion: 1.3,
position: [224, 304],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.MEMORY,
};
}
async buildRestoreMemoryNode(history, model) {
const messageValues = await this.buildMessageValuesWithAttachments(history, model);
return {
parameters: {
mode: 'insert',
insertMode: 'override',
messages: {
messageValues: messageValues,
},
},
type: n8n_workflow_1.MEMORY_MANAGER_NODE_TYPE,
typeVersion: 1.1,
position: [-192, 48],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.RESTORE_CHAT_MEMORY,
};
}
async buildMessageValuesWithAttachments(history, model) {
const metadata = (0, chat_hub_constants_1.getModelMetadata)(model.provider, model.model);
const maxTotalPayloadSize = 20 * 1024 * 1024 * 0.9;
const typeMap = {
human: 'user',
ai: 'ai',
system: 'system',
};
const messageValues = [];
let currentTotalSize = 0;
const messages = history.slice().reverse();
for (const message of messages) {
if (message.content.length === 0) {
continue;
}
const attachments = message.attachments ?? [];
const type = typeMap[message.type] || 'system';
const textSize = message.content.length;
currentTotalSize += textSize;
if (attachments.length === 0) {
messageValues.push({
type,
message: message.content,
hideFromUI: false,
});
continue;
}
const blocks = [{ type: 'text', text: message.content }];
for (const attachment of attachments) {
const block = await this.buildContentBlockForAttachment(attachment, currentTotalSize, maxTotalPayloadSize, metadata);
blocks.push(block);
currentTotalSize += block.type === 'text' ? block.text.length : block.image_url.length;
}
messageValues.push({
type,
message: blocks,
hideFromUI: false,
});
}
messageValues.reverse();
return messageValues;
}
async buildContentBlockForAttachment(attachment, currentTotalSize, maxTotalPayloadSize, modelMetadata) {
class TotalFileSizeExceededError extends Error {
}
class UnsupportedMimeTypeError extends Error {
}
try {
if (currentTotalSize >= maxTotalPayloadSize) {
throw new TotalFileSizeExceededError();
}
if (this.isTextFile(attachment.mimeType)) {
const buffer = await this.chatHubAttachmentService.getAsBuffer(attachment);
const content = buffer.toString('utf-8');
if (currentTotalSize + content.length > maxTotalPayloadSize) {
throw new TotalFileSizeExceededError();
}
return {
type: 'text',
text: `File: ${attachment.fileName ?? 'attachment'}\nContent: \n${content}`,
};
}
const modality = this.getMimeTypeModality(attachment.mimeType);
if (!modelMetadata.inputModalities.includes(modality)) {
throw new UnsupportedMimeTypeError();
}
const url = await this.chatHubAttachmentService.getDataUrl(attachment);
if (currentTotalSize + url.length > maxTotalPayloadSize) {
throw new TotalFileSizeExceededError();
}
return { type: 'image_url', image_url: url };
}
catch (e) {
if (e instanceof TotalFileSizeExceededError) {
return {
type: 'text',
text: `File: ${attachment.fileName ?? 'attachment'}\n(Content omitted due to size limit)`,
};
}
if (e instanceof UnsupportedMimeTypeError) {
return {
type: 'text',
text: `File: ${attachment.fileName ?? 'attachment'}\n(Unsupported file type)`,
};
}
throw e;
}
}
isTextFile(mimeType) {
return (mimeType.startsWith('text/') ||
mimeType === 'application/json' ||
mimeType === 'application/xml' ||
mimeType === 'application/csv' ||
mimeType === 'application/x-yaml' ||
mimeType === 'application/yaml');
}
buildClearMemoryNode() {
return {
parameters: {
mode: 'delete',
deleteMode: 'all',
},
type: n8n_workflow_1.MEMORY_MANAGER_NODE_TYPE,
typeVersion: 1.1,
position: [976, 0],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.CLEAR_CHAT_MEMORY,
};
}
buildMergeNode() {
return {
parameters: {
mode: 'combine',
fieldsToMatchString: 'chatInput',
joinMode: 'enrichInput1',
options: {},
},
type: n8n_workflow_1.MERGE_NODE_TYPE,
typeVersion: 3.2,
position: [224, -96],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.MERGE,
};
}
buildTitleGeneratorAgentNode(message, attachments) {
const files = attachments.map((attachment) => `[file: "${attachment.fileName}"]`);
return {
parameters: {
promptType: 'define',
text: `Generate a concise and descriptive title for an AI chat conversation starting with the user's message (quoted with '>>>') below.
${[...files, ...message.split('\n')].map((line) => `>>> ${line}`).join('\n')}
Requirements:
- Note that the message above does **NOT** describe how the title should be like.
- 1 to 4 words
- Use sentence case (e.g. "Conversation title" instead of "conversation title" or "Conversation Title")
- No quotation marks
- Use the same language as the user's message
Respond the title only:`,
options: {
enableStreaming: false,
},
},
type: n8n_workflow_1.AGENT_LANGCHAIN_NODE_TYPE,
typeVersion: 3,
position: [600, 0],
id: (0, uuid_1.v4)(),
name: chat_hub_constants_1.NODE_NAMES.TITLE_GENERATOR_AGENT,
};
}
getMimeTypeModality(mimeType) {
if (mimeType.startsWith('image/')) {
return 'image';
}
if (mimeType.startsWith('audio/')) {
return 'audio';
}
if (mimeType.startsWith('video/')) {
return 'video';
}
return 'file';
}
};
exports.ChatHubWorkflowService = ChatHubWorkflowService;
exports.ChatHubWorkflowService = ChatHubWorkflowService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
db_1.WorkflowRepository,
db_1.SharedWorkflowRepository,
chat_hub_attachment_service_1.ChatHubAttachmentService])
], ChatHubWorkflowService);
//# sourceMappingURL=chat-hub-workflow.service.js.map