UNPKG

@n8n-plus/n8n-plus

Version:

n8n Workflow Automation Tool (plus edition)

840 lines 43.4 kB
"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