@n8n-plus/n8n-plus
Version:
n8n Workflow Automation Tool (plus edition)
840 lines • 43.4 kB
JavaScript
"use strict";
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.ChatHubService = void 0;
const api_types_1 = require("@n8n/api-types");
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const active_executions_1 = require("../../active-executions");
const bad_request_error_1 = require("../../errors/response-errors/bad-request.error");
const not_found_error_1 = require("../../errors/response-errors/not-found.error");
const execution_service_1 = require("../../executions/execution.service");
const workflow_execution_service_1 = require("../../workflows/workflow-execution.service");
const workflow_finder_service_1 = require("../../workflows/workflow-finder.service");
const chat_hub_agent_service_1 = require("./chat-hub-agent.service");
const chat_hub_credentials_service_1 = require("./chat-hub-credentials.service");
const chat_hub_workflow_service_1 = require("./chat-hub-workflow.service");
const chat_hub_attachment_service_1 = require("./chat-hub.attachment.service");
const chat_hub_constants_1 = require("./chat-hub.constants");
const chat_hub_models_service_1 = require("./chat-hub.models.service");
const chat_hub_settings_service_1 = require("./chat-hub.settings.service");
const chat_hub_types_1 = require("./chat-hub.types");
const chat_message_repository_1 = require("./chat-message.repository");
const chat_session_repository_1 = require("./chat-session.repository");
const stream_capturer_1 = require("./stream-capturer");
let ChatHubService = class ChatHubService {
constructor(logger, errorReporter, executionService, executionRepository, workflowExecutionService, workflowFinderService, workflowRepository, activeExecutions, sessionRepository, messageRepository, chatHubAgentService, chatHubCredentialsService, chatHubWorkflowService, chatHubModelsService, chatHubSettingsService, chatHubAttachmentService, instanceSettings, globalConfig) {
this.logger = logger;
this.errorReporter = errorReporter;
this.executionService = executionService;
this.executionRepository = executionRepository;
this.workflowExecutionService = workflowExecutionService;
this.workflowFinderService = workflowFinderService;
this.workflowRepository = workflowRepository;
this.activeExecutions = activeExecutions;
this.sessionRepository = sessionRepository;
this.messageRepository = messageRepository;
this.chatHubAgentService = chatHubAgentService;
this.chatHubCredentialsService = chatHubCredentialsService;
this.chatHubWorkflowService = chatHubWorkflowService;
this.chatHubModelsService = chatHubModelsService;
this.chatHubSettingsService = chatHubSettingsService;
this.chatHubAttachmentService = chatHubAttachmentService;
this.instanceSettings = instanceSettings;
this.globalConfig = globalConfig;
}
async deleteChatWorkflow(workflowId) {
await this.workflowRepository.delete(workflowId);
}
getErrorMessage(execution) {
if (execution.data.resultData.error) {
return execution.data.resultData.error.description ?? execution.data.resultData.error.message;
}
return undefined;
}
getAIOutput(execution, nodeName) {
const agent = execution.data.resultData.runData[nodeName];
if (!agent || !Array.isArray(agent) || agent.length === 0)
return undefined;
const runIndex = agent.length - 1;
const mainOutputs = agent[runIndex].data?.main;
if (mainOutputs && Array.isArray(mainOutputs)) {
for (const branch of mainOutputs) {
if (branch && Array.isArray(branch) && branch.length > 0 && branch[0].json?.output) {
if (typeof branch[0].json.output === 'string') {
return branch[0].json.output;
}
}
}
}
return undefined;
}
pickCredentialId(provider, credentials) {
if (provider === 'n8n' || provider === 'custom-agent') {
return null;
}
return credentials[api_types_1.PROVIDER_CREDENTIAL_TYPE_MAP[provider]]?.id ?? null;
}
async sendHumanMessage(res, user, payload) {
const { sessionId, messageId, message, model, credentials, previousMessageId, tools, attachments, timeZone, } = payload;
const tz = timeZone ?? this.globalConfig.generic.timezone;
const credentialId = this.getModelCredential(model, credentials);
let processedAttachments = [];
let executionData;
let workflowData;
let responseMode;
try {
const result = await this.messageRepository.manager.transaction(async (trx) => {
let session = await this.getChatSession(user, sessionId, trx);
session ??= await this.createChatSession(user, sessionId, model, credentialId, tools, payload.agentName, trx);
await this.ensurePreviousMessage(previousMessageId, sessionId, trx);
const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m]));
const history = this.buildMessageHistory(messages, previousMessageId);
processedAttachments = await this.chatHubAttachmentService.store(sessionId, messageId, attachments);
await this.saveHumanMessage(payload, processedAttachments, user, previousMessageId, model, undefined, trx);
return await this.prepareReplyWorkflow(user, sessionId, credentials, model, history, message, tools, processedAttachments, tz, trx);
});
executionData = result.executionData;
workflowData = result.workflowData;
responseMode = result.responseMode;
}
catch (error) {
if (processedAttachments.length > 0) {
try {
await this.chatHubAttachmentService.deleteAttachments(processedAttachments);
}
catch (error) {
this.errorReporter.warn(`Could not clean up ${processedAttachments.length} files`);
}
}
throw error;
}
await this.executeChatWorkflowWithCleanup(res, user, workflowData, executionData, sessionId, messageId, model, null, responseMode);
if (previousMessageId === null) {
await this.generateSessionTitle(user, sessionId, message, processedAttachments, credentials, model).catch((error) => {
this.logger.error(`Title generation failed: ${error}`);
});
}
}
async editMessage(res, user, payload) {
const { sessionId, editId, messageId, message, model, credentials, timeZone } = payload;
const tz = timeZone ?? this.globalConfig.generic.timezone;
let workflow;
let newStoredAttachments = [];
try {
workflow = await this.messageRepository.manager.transaction(async (trx) => {
const session = await this.getChatSession(user, sessionId, trx);
if (!session) {
throw new not_found_error_1.NotFoundError('Chat session not found');
}
const messageToEdit = await this.getChatMessage(session.id, editId, [], trx);
if (messageToEdit.type === 'ai') {
await this.messageRepository.updateChatMessage(editId, { content: payload.message }, trx);
return null;
}
if (messageToEdit.type === 'human') {
const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m]));
const history = this.buildMessageHistory(messages, messageToEdit.previousMessageId);
const revisionOfMessageId = messageToEdit.revisionOfMessageId ?? messageToEdit.id;
const originalAttachments = messageToEdit.attachments ?? [];
const keptAttachments = payload.keepAttachmentIndices.flatMap((index) => {
const attachment = originalAttachments[index];
return attachment ? [attachment] : [];
});
newStoredAttachments =
payload.newAttachments.length > 0
? await this.chatHubAttachmentService.store(sessionId, messageId, payload.newAttachments)
: [];
const attachments = [...keptAttachments, ...newStoredAttachments];
await this.saveHumanMessage(payload, attachments, user, messageToEdit.previousMessageId, model, revisionOfMessageId, trx);
return await this.prepareReplyWorkflow(user, sessionId, credentials, model, history, message, session.tools, attachments, tz, trx);
}
throw new bad_request_error_1.BadRequestError('Only human and AI messages can be edited');
});
}
catch (error) {
if (newStoredAttachments.length > 0) {
try {
await this.chatHubAttachmentService.deleteAttachments(newStoredAttachments);
}
catch (error) {
this.errorReporter.warn(`Could not clean up ${newStoredAttachments.length} files`);
}
}
throw error;
}
if (!workflow) {
return;
}
const { workflowData, executionData, responseMode } = workflow;
await this.executeChatWorkflowWithCleanup(res, user, workflowData, executionData, sessionId, messageId, model, null, responseMode);
}
async regenerateAIMessage(res, user, payload) {
const { sessionId, retryId, model, credentials, timeZone } = payload;
const tz = timeZone ?? this.globalConfig.generic.timezone;
const { workflow: { workflowData, executionData, responseMode }, retryOfMessageId, previousMessageId, } = await this.messageRepository.manager.transaction(async (trx) => {
const session = await this.getChatSession(user, sessionId, trx);
if (!session) {
throw new not_found_error_1.NotFoundError('Chat session not found');
}
const messageToRetry = await this.getChatMessage(session.id, retryId, [], trx);
if (messageToRetry.type !== 'ai') {
throw new bad_request_error_1.BadRequestError('Can only retry AI messages');
}
const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m]));
const history = this.buildMessageHistory(messages, messageToRetry.previousMessageId);
const lastHumanMessage = history.filter((m) => m.type === 'human').pop();
if (!lastHumanMessage) {
throw new bad_request_error_1.BadRequestError('No human message found to base the retry on');
}
const lastHumanMessageIndex = history.indexOf(lastHumanMessage);
if (lastHumanMessageIndex !== -1) {
history.splice(lastHumanMessageIndex + 1);
}
const retryOfMessageId = messageToRetry.retryOfMessageId ?? messageToRetry.id;
const message = lastHumanMessage ? lastHumanMessage.content : '';
const attachments = lastHumanMessage.attachments ?? [];
const workflow = await this.prepareReplyWorkflow(user, sessionId, credentials, model, history, message, session.tools, attachments, tz, trx);
return {
workflow,
previousMessageId: lastHumanMessage.id,
retryOfMessageId,
};
});
await this.executeChatWorkflowWithCleanup(res, user, workflowData, executionData, sessionId, previousMessageId, model, retryOfMessageId, responseMode);
}
async prepareReplyWorkflow(user, sessionId, credentials, model, history, message, tools, attachments, timeZone, trx) {
if (model.provider === 'n8n') {
return await this.prepareCustomAgentWorkflow(user, sessionId, model.workflowId, message, attachments);
}
if (model.provider === 'custom-agent') {
return await this.prepareChatAgentWorkflow(model.agentId, user, sessionId, history, message, attachments, timeZone, trx);
}
return await this.prepareBaseChatWorkflow(user, sessionId, credentials, model, history, message, undefined, tools, attachments, timeZone, trx);
}
async prepareBaseChatWorkflow(user, sessionId, credentials, model, history, message, systemMessage, tools, attachments, timeZone, trx) {
await this.chatHubSettingsService.ensureModelIsAllowed(model);
this.chatHubCredentialsService.findProviderCredential(model.provider, credentials);
const { id: projectId } = await this.chatHubCredentialsService.findPersonalProject(user, trx);
return await this.chatHubWorkflowService.createChatWorkflow(user.id, sessionId, projectId, history, message, attachments, credentials, model, systemMessage, tools, timeZone, trx);
}
async prepareChatAgentWorkflow(agentId, user, sessionId, history, message, attachments, timeZone, trx) {
const agent = await this.chatHubAgentService.getAgentById(agentId, user.id);
if (!agent) {
throw new bad_request_error_1.BadRequestError('Agent not found');
}
if (!agent.provider || !agent.model) {
throw new bad_request_error_1.BadRequestError('Provider or model not set for agent');
}
const credentialId = agent.credentialId;
if (!credentialId) {
throw new bad_request_error_1.BadRequestError('Credentials not set for agent');
}
const systemMessage = agent.systemPrompt + '\n\n' + this.chatHubWorkflowService.getSystemMessageMetadata(timeZone);
const model = {
provider: agent.provider,
model: agent.model,
};
const credentials = {
[api_types_1.PROVIDER_CREDENTIAL_TYPE_MAP[agent.provider]]: {
id: credentialId,
name: '',
},
};
const { tools } = agent;
return await this.prepareBaseChatWorkflow(user, sessionId, credentials, model, history, message, systemMessage, tools, attachments, timeZone, trx);
}
async prepareCustomAgentWorkflow(user, sessionId, workflowId, message, attachments) {
const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, user, ['workflow:execute-chat'], { includeTags: false, includeParentFolder: false, includeActiveVersion: true });
if (!workflow?.activeVersion) {
throw new bad_request_error_1.BadRequestError('Workflow not found');
}
const chatTriggers = workflow.activeVersion.nodes.filter((node) => node.type === n8n_workflow_1.CHAT_TRIGGER_NODE_TYPE);
if (chatTriggers.length !== 1) {
throw new bad_request_error_1.BadRequestError('Workflow must have exactly one chat trigger');
}
const chatTrigger = chatTriggers[0];
if (chatTrigger.typeVersion < chat_hub_constants_1.CHAT_TRIGGER_NODE_MIN_VERSION) {
throw new bad_request_error_1.BadRequestError('Chat Trigger node version is too old to support Chat. Please update the node.');
}
const chatTriggerParams = chat_hub_types_1.chatTriggerParamsShape.safeParse(chatTrigger.parameters).data;
if (!chatTriggerParams) {
throw new bad_request_error_1.BadRequestError('Chat Trigger node has invalid parameters');
}
if (!chatTriggerParams.availableInChat) {
throw new bad_request_error_1.BadRequestError('Chat Trigger node must be made available in Chat');
}
const responseMode = chatTriggerParams.options?.responseMode ?? 'streaming';
if (responseMode !== 'streaming') {
throw new bad_request_error_1.BadRequestError('Chat Trigger node response mode must be set to streaming to use the workflow on Chat');
}
const chatResponseNodes = workflow.activeVersion.nodes.filter((node) => node.type === n8n_workflow_1.RESPOND_TO_CHAT_NODE_TYPE);
if (chatResponseNodes.length > 0) {
throw new bad_request_error_1.BadRequestError('Respond to Chat nodes are not supported in custom agent workflows');
}
const agentNodes = workflow.activeVersion.nodes?.filter((node) => node.type === n8n_workflow_1.AGENT_LANGCHAIN_NODE_TYPE);
if (agentNodes.some((node) => node.typeVersion < chat_hub_constants_1.TOOLS_AGENT_NODE_MIN_VERSION)) {
throw new bad_request_error_1.BadRequestError('Agent node version is too old to support streaming responses. Please update the node.');
}
const nodeExecutionStack = this.chatHubWorkflowService.prepareExecutionData(chatTrigger, sessionId, message, attachments);
const executionData = (0, n8n_workflow_1.createRunExecutionData)({
executionData: {
nodeExecutionStack,
},
manualData: {
userId: user.id,
},
});
const workflowData = {
...workflow,
nodes: workflow.activeVersion.nodes,
connections: workflow.activeVersion.connections,
};
return {
workflowData,
executionData,
responseMode,
};
}
async ensurePreviousMessage(previousMessageId, sessionId, trx) {
if (!previousMessageId) {
return;
}
const previousMessage = await this.messageRepository.getOneById(previousMessageId, sessionId, [], trx);
if (!previousMessage) {
throw new bad_request_error_1.BadRequestError('The previous message does not exist in the session');
}
}
async stopGeneration(user, sessionId, messageId) {
await this.ensureConversation(user.id, sessionId);
const message = await this.getChatMessage(sessionId, messageId, [
'execution',
'execution.workflow',
]);
if (message.type !== 'ai') {
throw new bad_request_error_1.BadRequestError('Can only stop AI messages');
}
if (!message.executionId || !message.execution) {
throw new bad_request_error_1.BadRequestError('Message is not associated with a workflow execution');
}
if (message.status !== 'running') {
throw new bad_request_error_1.BadRequestError('Can only stop messages that are currently running');
}
await this.executionService.stop(message.execution.id, [message.execution.workflowId]);
await this.messageRepository.updateChatMessage(messageId, { status: 'cancelled' });
}
async executeChatWorkflow(res, user, workflowData, executionData, sessionId, previousMessageId, model, retryOfMessageId = null, executionMode = 'chat', responseMode) {
this.logger.debug(`Starting execution of workflow "${workflowData.name}" with ID ${workflowData.id}`);
if (responseMode !== 'streaming') {
throw new bad_request_error_1.BadRequestError(`Response mode "${responseMode}" is not supported yet.`);
}
let executionId = undefined;
const aggregator = (0, stream_capturer_1.createStructuredChunkAggregator)(previousMessageId, retryOfMessageId, {
onBegin: async (message) => {
await this.saveAIMessage({
...message,
sessionId,
executionId,
model,
retryOfMessageId,
});
},
onItem: (_message, _chunk) => {
},
onEnd: async (message) => {
await this.messageRepository.updateChatMessage(message.id, {
content: message.content,
status: message.status,
});
},
onError: async (message, _errorText) => {
await this.messageRepository.manager.transaction(async (trx) => {
await this.messageRepository.updateChatMessage(message.id, {
content: message.content,
}, trx);
const savedMessage = await this.messageRepository.getOneById(message.id, sessionId, [], trx);
if (savedMessage?.status === 'cancelled') {
return;
}
await this.messageRepository.updateChatMessage(message.id, {
status: 'error',
}, trx);
});
},
});
const transform = (text) => {
const trimmed = text.trim();
if (!trimmed)
return text;
let chunk = null;
try {
chunk = (0, n8n_workflow_1.jsonParse)(trimmed);
}
catch {
return text;
}
const message = aggregator.ingest(chunk);
const enriched = {
...chunk,
metadata: {
...chunk.metadata,
messageId: message.id,
previousMessageId: message.previousMessageId,
retryOfMessageId: message.retryOfMessageId,
executionId: executionId ? +executionId : null,
},
};
return (0, n8n_workflow_1.jsonStringify)(enriched) + '\n';
};
const stream = (0, stream_capturer_1.interceptResponseWrites)(res, transform);
stream.on('finish', aggregator.finalizeAll);
stream.on('close', aggregator.finalizeAll);
stream.writeHead(200, chat_hub_constants_1.JSONL_STREAM_HEADERS);
stream.flushHeaders();
const execution = await this.workflowExecutionService.executeChatWorkflow(workflowData, executionData, user, stream, true, executionMode);
executionId = execution.executionId;
if (!executionId) {
throw new n8n_workflow_1.OperationalError('There was a problem starting the chat execution.');
}
await this.waitForExecutionCompletion(executionId);
}
async waitForExecutionCompletion(executionId) {
if (this.instanceSettings.isMultiMain) {
return await this.waitForExecutionPoller(executionId);
}
else {
return await this.waitForExecutionPromise(executionId);
}
}
async waitForExecutionPoller(executionId) {
return await new Promise((resolve, reject) => {
const poller = setInterval(async () => {
try {
const result = await this.executionRepository.findSingleExecution(executionId, {
includeData: false,
unflattenData: false,
});
if (!result || chat_hub_constants_1.EXECUTION_FINISHED_STATUSES.includes(result.status)) {
this.logger.debug(`Execution ${executionId} finished with status ${result?.status ?? 'missing'}`);
clearInterval(poller);
resolve();
}
}
catch (error) {
this.logger.error(`Stopping polling for execution ${executionId} due to error.`);
clearInterval(poller);
if (error instanceof Error) {
this.logger.error(`Error while polling execution ${executionId}: ${error.message}`, {
error,
});
}
else {
this.logger.error(`Unknown error while polling execution ${executionId}`, { error });
}
if (error instanceof Error) {
reject(error);
}
else {
reject(new Error('Unknown error while polling execution status'));
}
}
}, chat_hub_constants_1.EXECUTION_POLL_INTERVAL);
});
}
async waitForExecutionPromise(executionId) {
try {
const result = await this.activeExecutions.getPostExecutePromise(executionId);
if (!result) {
throw new n8n_workflow_1.OperationalError('There was a problem executing the chat workflow.');
}
}
catch (error) {
if (error instanceof n8n_workflow_1.ManualExecutionCancelledError) {
return;
}
if (error instanceof Error) {
this.logger.error(`Error during chat workflow execution: ${error}`);
}
throw error;
}
}
async executeChatWorkflowWithCleanup(res, user, workflowData, executionData, sessionId, previousMessageId, model, retryOfMessageId, responseMode) {
try {
const executionMode = model.provider === 'n8n' ? 'webhook' : 'chat';
await this.executeChatWorkflow(res, user, workflowData, executionData, sessionId, previousMessageId, model, retryOfMessageId, executionMode, responseMode);
}
finally {
if (model.provider !== 'n8n') {
await this.deleteChatWorkflow(workflowData.id);
}
}
}
async generateSessionTitle(user, sessionId, humanMessage, attachments, credentials, model) {
const { executionData, workflowData } = await this.prepareTitleGenerationWorkflow(user, sessionId, humanMessage, attachments, credentials, model);
try {
const title = await this.runTitleWorkflowAndGetTitle(user, workflowData, executionData);
if (title) {
await this.sessionRepository.updateChatSession(sessionId, { title });
}
}
catch (error) {
if (error instanceof Error) {
this.logger.error(`Error during session title generation workflow execution: ${error}`);
}
throw error;
}
finally {
await this.deleteChatWorkflow(workflowData.id);
}
}
async prepareTitleGenerationWorkflow(user, sessionId, humanMessage, attachments, incomingCredentials, incomingModel) {
return await this.messageRepository.manager.transaction(async (trx) => {
const { resolvedCredentials, resolvedModel, credentialId, projectId } = await this.resolveCredentialsAndModelForTitle(user, incomingModel, incomingCredentials, trx);
if (!credentialId || !projectId) {
throw new bad_request_error_1.BadRequestError('Could not determine credentials for title generation');
}
this.logger.debug(`Using credential ID ${credentialId} for title generation in project ${projectId}, model ${(0, n8n_workflow_1.jsonStringify)(resolvedModel)}`);
return await this.chatHubWorkflowService.createTitleGenerationWorkflow(user.id, sessionId, projectId, humanMessage, attachments, resolvedCredentials, resolvedModel, trx);
});
}
async resolveCredentialsAndModelForTitle(user, model, credentials, trx) {
if (model.provider === 'n8n') {
return await this.resolveFromN8nWorkflow(user, model, trx);
}
if (model.provider === 'custom-agent') {
return await this.resolveFromCustomAgent(user, model, trx);
}
const credentialId = this.chatHubCredentialsService.findProviderCredential(model.provider, credentials);
const { id: projectId } = await this.chatHubCredentialsService.findPersonalProject(user, trx);
return {
resolvedCredentials: credentials,
resolvedModel: model,
credentialId,
projectId,
};
}
async resolveFromN8nWorkflow(user, { workflowId }, trx) {
const workflowEntity = await this.workflowFinderService.findWorkflowForUser(workflowId, user, ['workflow:execute-chat'], { includeTags: false, includeParentFolder: false, includeActiveVersion: true, em: trx });
if (!workflowEntity?.activeVersion) {
throw new bad_request_error_1.BadRequestError('Workflow not found for title generation');
}
const modelNodes = this.findSupportedLLMNodes(workflowEntity.activeVersion.nodes);
this.logger.debug(`Found ${modelNodes.length} LLM nodes in workflow ${workflowEntity.id} for title generation`);
if (modelNodes.length === 0) {
throw new bad_request_error_1.BadRequestError('No supported Model nodes found in workflow for title generation');
}
const modelNode = modelNodes[0];
const llmModel = modelNode.node.parameters?.model?.value;
if (!llmModel) {
throw new bad_request_error_1.BadRequestError(`No model set on Model node "${modelNode.node.name}" for title generation`);
}
if (typeof llmModel !== 'string' || llmModel.length === 0 || llmModel.startsWith('=')) {
throw new bad_request_error_1.BadRequestError(`Invalid model set on Model node "${modelNode.node.name}" for title generation`);
}
const llmCredentials = modelNode.node.credentials;
if (!llmCredentials) {
throw new bad_request_error_1.BadRequestError(`No credentials found on Model node "${modelNode.node.name}" for title generation`);
}
const { credentialId, projectId } = await this.chatHubCredentialsService.findWorkflowCredentialAndProject(modelNode.provider, llmCredentials, workflowId);
const resolvedModel = {
provider: modelNode.provider,
model: llmModel,
};
const resolvedCredentials = {
[api_types_1.PROVIDER_CREDENTIAL_TYPE_MAP[modelNode.provider]]: {
id: credentialId,
name: '',
},
};
return { resolvedCredentials, resolvedModel, credentialId, projectId };
}
findSupportedLLMNodes(nodes) {
return nodes.reduce((acc, node) => {
const supportedProvider = Object.entries(chat_hub_constants_1.PROVIDER_NODE_TYPE_MAP).find(([_provider, { name }]) => node.type === name);
if (supportedProvider) {
const [provider] = supportedProvider;
acc.push({ node, provider: provider });
}
return acc;
}, []);
}
async resolveFromCustomAgent(user, model, trx) {
const agent = await this.chatHubAgentService.getAgentById(model.agentId, user.id);
if (!agent) {
throw new bad_request_error_1.BadRequestError('Agent not found for title generation');
}
if (!agent.credentialId) {
throw new bad_request_error_1.BadRequestError('Credentials not set for agent');
}
const resolvedModel = {
provider: agent.provider,
model: agent.model,
};
const resolvedCredentials = {
[api_types_1.PROVIDER_CREDENTIAL_TYPE_MAP[agent.provider]]: {
id: agent.credentialId,
name: '',
},
};
const credentialId = this.chatHubCredentialsService.findProviderCredential(agent.provider, resolvedCredentials);
const { id: projectId } = await this.chatHubCredentialsService.findPersonalProject(user, trx);
return { resolvedCredentials, resolvedModel, credentialId, projectId };
}
async runTitleWorkflowAndGetTitle(user, workflowData, executionData) {
const { executionId } = await this.workflowExecutionService.executeChatWorkflow(workflowData, executionData, user, undefined, false, 'chat');
await this.waitForExecutionCompletion(executionId);
const execution = await this.executionRepository.findWithUnflattenedData(executionId, [
workflowData.id,
]);
if (!execution) {
throw new n8n_workflow_1.OperationalError(`Could not find execution with ID ${executionId}`);
}
if (!execution.status || execution.status !== 'success') {
const message = this.getErrorMessage(execution) ?? 'Failed to generate a response';
throw new n8n_workflow_1.OperationalError(message);
}
const title = this.getAIOutput(execution, chat_hub_constants_1.NODE_NAMES.TITLE_GENERATOR_AGENT);
return title ?? null;
}
async saveHumanMessage(payload, attachments, user, previousMessageId, model, revisionOfMessageId, trx) {
await this.messageRepository.createChatMessage({
id: payload.messageId,
sessionId: payload.sessionId,
type: 'human',
status: 'success',
content: payload.message,
previousMessageId,
revisionOfMessageId,
name: user.firstName || 'User',
attachments,
...model,
}, trx);
}
async saveAIMessage({ id, sessionId, executionId, previousMessageId, content, model, retryOfMessageId, status, }) {
await this.messageRepository.createChatMessage({
id,
sessionId,
previousMessageId,
executionId: executionId ? parseInt(executionId, 10) : null,
type: 'ai',
name: 'AI',
status,
content,
retryOfMessageId,
...model,
});
}
getModelCredential(model, credentials) {
const credentialId = model.provider !== 'n8n' ? this.pickCredentialId(model.provider, credentials) : null;
return credentialId;
}
async getChatSession(user, sessionId, trx) {
return await this.sessionRepository.getOneById(sessionId, user.id, trx);
}
async createChatSession(user, sessionId, model, credentialId, tools, agentName, trx) {
await this.ensureValidModel(user, model);
return await this.sessionRepository.createChatSession({
id: sessionId,
ownerId: user.id,
title: 'New Chat',
lastMessageAt: new Date(),
agentName,
tools,
credentialId,
...model,
}, trx);
}
async getChatMessage(sessionId, messageId, relations = [], trx) {
const message = await this.messageRepository.getOneById(messageId, sessionId, relations, trx);
if (!message) {
throw new not_found_error_1.NotFoundError('Chat message not found');
}
return message;
}
async getConversations(userId, limit, cursor) {
const sessions = await this.sessionRepository.getManyByUserId(userId, limit + 1, cursor);
const hasMore = sessions.length > limit;
const data = hasMore ? sessions.slice(0, limit) : sessions;
const nextCursor = hasMore ? data[data.length - 1].id : null;
return {
data: data.map((session) => this.convertSessionEntityToDto(session)),
nextCursor,
hasMore,
};
}
async ensureConversation(userId, sessionId, trx) {
const sessionExists = await this.sessionRepository.existsById(sessionId, userId, trx);
if (!sessionExists) {
throw new not_found_error_1.NotFoundError('Chat session not found');
}
}
async getConversation(userId, sessionId) {
const session = await this.sessionRepository.getOneById(sessionId, userId);
if (!session) {
throw new not_found_error_1.NotFoundError('Chat session not found');
}
const messages = session.messages ?? [];
return {
session: this.convertSessionEntityToDto(session),
conversation: {
messages: Object.fromEntries(messages.map((m) => [m.id, this.convertMessageToDto(m)])),
},
};
}
convertMessageToDto(message) {
return {
id: message.id,
sessionId: message.sessionId,
type: message.type,
name: message.name,
content: message.content,
provider: message.provider,
model: message.model,
workflowId: message.workflowId,
agentId: message.agentId,
executionId: message.executionId,
status: message.status,
createdAt: message.createdAt.toISOString(),
updatedAt: message.updatedAt.toISOString(),
previousMessageId: message.previousMessageId,
retryOfMessageId: message.retryOfMessageId,
revisionOfMessageId: message.revisionOfMessageId,
attachments: (message.attachments ?? []).map(({ fileName, mimeType }) => ({
fileName,
mimeType,
})),
};
}
buildMessageHistory(messages, lastMessageId) {
if (!lastMessageId)
return [];
const visited = new Set();
const historyIds = [];
let current = lastMessageId;
while (current && !visited.has(current)) {
historyIds.unshift(current);
visited.add(current);
current = messages[current]?.previousMessageId ?? null;
}
const history = historyIds.flatMap((id) => messages[id] ?? []);
return history;
}
async deleteAllSessions() {
await this.chatHubAttachmentService.deleteAll();
const result = await this.sessionRepository.deleteAll();
return result;
}
async updateSession(user, sessionId, updates) {
await this.ensureConversation(user.id, sessionId);
const sessionUpdates = {};
if (updates.agent) {
const model = updates.agent.model;
await this.ensureValidModel(user, model);
sessionUpdates.agentName = updates.agent.name;
sessionUpdates.provider = model.provider;
sessionUpdates.model = null;
sessionUpdates.credentialId = null;
sessionUpdates.agentId = null;
sessionUpdates.workflowId = null;
if (updates.agent.model.provider === 'n8n') {
sessionUpdates.workflowId = updates.agent.model.workflowId;
}
else if (updates.agent.model.provider === 'custom-agent') {
sessionUpdates.agentId = updates.agent.model.agentId;
}
else {
sessionUpdates.model = updates.agent.model.model;
}
}
if (updates.title !== undefined)
sessionUpdates.title = updates.title;
if (updates.credentialId !== undefined)
sessionUpdates.credentialId = updates.credentialId;
if (updates.tools !== undefined)
sessionUpdates.tools = updates.tools;
return await this.sessionRepository.updateChatSession(sessionId, sessionUpdates);
}
async deleteSession(userId, sessionId) {
await this.messageRepository.manager.transaction(async (trx) => {
await this.ensureConversation(userId, sessionId, trx);
await this.chatHubAttachmentService.deleteAllBySessionId(sessionId, trx);
await this.sessionRepository.deleteChatHubSession(sessionId, trx);
});
}
async ensureValidModel(user, model) {
if (model.provider === 'custom-agent') {
const agent = await this.chatHubAgentService.getAgentById(model.agentId, user.id);
if (!agent) {
throw new bad_request_error_1.BadRequestError('Agent not found for chat session initialization');
}
}
if (model.provider === 'n8n') {
const workflowEntity = await this.workflowFinderService.findWorkflowForUser(model.workflowId, user, ['workflow:execute-chat'], { includeTags: false, includeParentFolder: false, includeActiveVersion: true });
if (!workflowEntity?.activeVersion) {
throw new bad_request_error_1.BadRequestError('Workflow not found for chat session initialization');
}
const chatTrigger = workflowEntity.activeVersion.nodes?.find((node) => node.type === n8n_workflow_1.CHAT_TRIGGER_NODE_TYPE);
if (!chatTrigger) {
throw new bad_request_error_1.BadRequestError('Chat trigger not found in workflow for chat session initialization');
}
}
}
convertSessionEntityToDto(session) {
const agent = session.workflow
? this.chatHubModelsService.extractModelFromWorkflow(session.workflow, [])
: session.agent
? this.chatHubAgentService.convertAgentEntityToModel(session.agent)
: undefined;
return {
id: session.id,
title: session.title,
ownerId: session.ownerId,
lastMessageAt: session.lastMessageAt?.toISOString() ?? null,
credentialId: session.credentialId,
provider: session.provider,
model: session.model,
workflowId: session.workflowId,
agentId: session.agentId,
agentName: agent?.name ?? session.agentName ?? session.model ?? '',
agentIcon: agent?.icon ?? null,
createdAt: session.createdAt.toISOString(),
updatedAt: session.updatedAt.toISOString(),
tools: session.tools,
};
}
};
exports.ChatHubService = ChatHubService;
exports.ChatHubService = ChatHubService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
n8n_core_1.ErrorReporter,
execution_service_1.ExecutionService,
db_1.ExecutionRepository,
workflow_execution_service_1.WorkflowExecutionService,
workflow_finder_service_1.WorkflowFinderService,
db_1.WorkflowRepository,
active_executions_1.ActiveExecutions,
chat_session_repository_1.ChatHubSessionRepository,
chat_message_repository_1.ChatHubMessageRepository,
chat_hub_agent_service_1.ChatHubAgentService,
chat_hub_credentials_service_1.ChatHubCredentialsService,
chat_hub_workflow_service_1.ChatHubWorkflowService,
chat_hub_models_service_1.ChatHubModelsService,
chat_hub_settings_service_1.ChatHubSettingsService,
chat_hub_attachment_service_1.ChatHubAttachmentService,
n8n_core_1.InstanceSettings,
config_1.GlobalConfig])
], ChatHubService);
//# sourceMappingURL=chat-hub.service.js.map