UNPKG

n8n

Version:

n8n Workflow Automation Tool

390 lines 18.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentChatBridge = void 0; const api_types_1 = require("@n8n/api-types"); const backend_common_1 = require("@n8n/backend-common"); const backend_network_1 = require("@n8n/backend-network"); const di_1 = require("@n8n/di"); const agent_chat_attachment_service_1 = require("../agent-chat-attachment.service"); const cache_service_1 = require("../../../services/cache/cache.service"); const agent_memory_scope_1 = require("../utils/agent-memory-scope"); const inbound_attachments_1 = require("../utils/inbound-attachments"); const agent_chat_integration_1 = require("./agent-chat-integration"); const agent_chat_hitl_resume_handler_1 = require("./agent-chat-hitl-resume-handler"); const agent_chat_message_context_1 = require("./agent-chat-message-context"); const agent_chat_stream_consumer_1 = require("./agent-chat-stream-consumer"); const agent_chat_suspension_cards_1 = require("./agent-chat-suspension-cards"); const callback_store_1 = require("./callback-store"); const integration_message_context_service_1 = require("./integration-message-context.service"); const discord_operations_1 = require("./platforms/discord-operations"); const types_1 = require("./types"); class AgentChatBridge { constructor(chat, agentId, agentService, componentMapper, logger, n8nProjectId, integration, messageContextStore, attachmentService, discordHttpClient) { this.chat = chat; this.agentId = agentId; this.agentService = agentService; this.componentMapper = componentMapper; this.logger = logger; this.n8nProjectId = n8nProjectId; this.integration = integration; this.attachmentService = attachmentService; this.discordHttpClient = discordHttpClient; this.integrationImpl = di_1.Container.get(agent_chat_integration_1.ChatIntegrationRegistry).get(integration.type); this.messageContextBridge = new agent_chat_message_context_1.AgentChatMessageContextBridge(messageContextStore, integration, agentId, logger); if (this.integrationImpl?.needsShortCallbackData) { this.callbackStore = new callback_store_1.CallbackStore(di_1.Container.get(cache_service_1.CacheService), di_1.Container.get(backend_common_1.LockService), `${agentId}:${integration.type}:${integration.credentialId}`); } const disableStreaming = this.integrationImpl?.disableStreaming ?? false; const actionToolNamePattern = new RegExp(`^${integration.type}(_\\d+)?_action$`); this.streamConsumer = new agent_chat_stream_consumer_1.AgentChatStreamConsumer({ disableStreaming, logger: this.logger, postErrorToThread: this.postErrorToThread.bind(this), handleSuspension: this.handleSuspension.bind(this), handleMessage: this.handleMessage.bind(this), isIntegrationActionTool: (toolName) => actionToolNamePattern.test(toolName), }); this.hitlResumeHandler = new agent_chat_hitl_resume_handler_1.AgentChatHitlResumeHandler({ agentId, projectId: n8nProjectId, integration, agentService, logger, callbackStore: this.callbackStore, deleteActionMessageBeforeResume: this.integrationImpl?.deleteActionMessageBeforeResume ?? true, formatActionDecisionMessage: (params) => this.integrationImpl?.formatActionDecisionMessage?.(params), settleActionMessage: this.integrationImpl?.settleActionMessage?.bind(this.integrationImpl), resolvePlatformThreadId: this.resolvePlatformThreadId.bind(this), toAgentThreadId: this.toAgentThreadId.bind(this), getPlatformAgentContext: this.getPlatformAgentContext.bind(this), messageContextBridge: this.messageContextBridge, streamConsumer: this.streamConsumer, createResumeExecutionContext: async (thread) => { const params = { chat: this.chat, thread, logger: this.logger, agentId: this.agentId, }; const resumeExecutionContext = await this.integrationImpl?.createResumeExecutionContext?.(params); if (resumeExecutionContext) return resumeExecutionContext; return {}; }, }); this.registerHandlers(); } static create(chat, agentId, agentService, componentMapper, logger, n8nProjectId, integration) { const agentExecutor = { async *executeForChatPublished({ memory, agentId: aid, message, attachments, integrationType, }) { yield* agentService.executeForChatPublished({ agentId: aid, projectId: n8nProjectId, message, attachments, memory: { threadId: memory.threadId.id, resourceId: memory.resourceId, ...(memory.resourceId !== undefined && { resourceId: memory.resourceId, }), }, integrationType, }); }, async *resumeForChat(config) { yield* agentService.resumeForChat(config); }, }; return new AgentChatBridge(chat, agentId, agentExecutor, componentMapper, logger, n8nProjectId, integration, di_1.Container.get(integration_message_context_service_1.IntegrationMessageContextService), di_1.Container.get(agent_chat_attachment_service_1.AgentChatAttachmentService), integration.type === 'discord' ? di_1.Container.get(backend_network_1.OutboundHttp).requests({ ssrf: 'disabled', }) : undefined); } registerHandlers() { this.chat.onNewMention(async (thread, message) => { try { if (!this.canUserAccess(message.author)) return; const shouldSubscribe = this.integrationImpl?.shouldSubscribeToNewMention?.({ thread, message }) ?? true; if (shouldSubscribe) { await thread.subscribe(); } await this.executeAndStream(thread, message, { isNewMention: true }); } catch (error) { await this.postErrorToThread(thread, error); } }); this.chat.onSubscribedMessage(async (thread, message) => { try { if (!this.canUserAccess(message.author)) return; await this.executeAndStream(thread, message, { isNewMention: false }); } catch (error) { await this.postErrorToThread(thread, error); } }); this.chat.onAction(async (event) => { try { if (!this.canUserAccess(event.user)) return; await this.hitlResumeHandler.handleAction(event); } catch (error) { await this.postErrorToThread(event.thread, error); } }); } canUserAccess(author) { return this.integrationImpl?.isUserAllowed?.(author, this.integration) ?? true; } resolvePlatformThreadId(thread) { return this.integrationImpl?.formatThreadId?.fromSdk(thread) ?? thread.id; } toAgentThreadId(platformThreadId) { return (0, types_1.toInternalThreadId)(`${this.agentId}:${platformThreadId}`); } getShortenCallback(metadata) { if (!this.callbackStore) return undefined; const store = this.callbackStore; return async (actionId, value, label) => { const key = await store.store(actionId, value, { ...metadata, ...(label !== undefined ? { label } : {}), }); return { id: key, value: '' }; }; } async executeAndStream(thread, message, options) { const { isNewMention } = options; const platformAgentContext = this.getPlatformAgentContext(); const text = this.prepareInboundText(message.text, platformAgentContext).trim(); const inboundAttachments = message.attachments ?? []; if (!text && inboundAttachments.length === 0) return; const platformThreadId = this.resolvePlatformThreadId(thread); const threadId = this.toAgentThreadId(platformThreadId); const resourceId = (0, agent_memory_scope_1.integrationMemoryResourceId)(this.integration.type, message.author.userId); const { attachments, attachmentNotes } = await this.storeInboundAttachments(inboundAttachments, threadId.id, resourceId); const statusRetry = new AbortController(); const replyExpectation = this.integrationImpl?.getReplyExpectation?.({ message, isNewMention, platformAgentContext, }) ?? 'required'; let statusHandle; let consumeStarted = false; try { const [bridgeExecutionContext, subject] = await Promise.all([ this.resolveBridgeExecutionContext(thread, message, platformAgentContext, statusRetry, isNewMention, replyExpectation), this.messageContextBridge.resolveSubject(message), ]); statusHandle = (0, agent_chat_integration_1.onceStatusHandle)(bridgeExecutionContext.statusHandle); await this.messageContextBridge.updateLatest(threadId.id, message.author.userId, thread, { messageId: message.id, interactingUserId: message.author.userId, ...bridgeExecutionContext.platformAgentContext, subject, replyExpectation, }); const textWithNotes = [text, ...attachmentNotes].filter(Boolean).join('\n'); const agentInput = bridgeExecutionContext.historyContext ? `${bridgeExecutionContext.historyContext}\n\n${textWithNotes}` : textWithNotes; const stream = this.agentService.executeForChatPublished({ agentId: this.agentId, projectId: this.n8nProjectId, message: agentInput, attachments: attachments.length > 0 ? attachments : undefined, memory: { threadId, resourceId, }, integrationType: this.integration.type, }); consumeStarted = true; await this.streamConsumer.consume(stream, thread, { forceBuffered: bridgeExecutionContext.forceBuffered, statusHandle, }); } catch (error) { if (!consumeStarted && attachments.length > 0) { await this.attachmentService?.deleteByIds(attachments.map((ref) => ref.id)).catch(() => { }); } throw error; } finally { statusRetry.abort(); await statusHandle?.clearBeforeResponse(); } } async storeInboundAttachments(inboundAttachments, threadId, resourceId) { const attachments = []; const attachmentNotes = []; if (!this.attachmentService || inboundAttachments.length === 0) { return { attachments, attachmentNotes }; } const skipped = inboundAttachments.slice(api_types_1.MAX_AGENT_CHAT_ATTACHMENTS_PER_MESSAGE); for (const attachment of skipped) { attachmentNotes.push(`[Attachment "${attachment.name ?? 'file'}" was not processed: too many attachments in one message]`); } for (const attachment of inboundAttachments.slice(0, api_types_1.MAX_AGENT_CHAT_ATTACHMENTS_PER_MESSAGE)) { const name = (attachment.name ?? 'attachment').slice(0, api_types_1.MAX_AGENT_CHAT_ATTACHMENT_FILENAME_LENGTH); try { if (attachment.size !== undefined && attachment.size > api_types_1.MAX_AGENT_CHAT_ATTACHMENT_SIZE_BYTES) { attachmentNotes.push(`[Attachment "${name}" was skipped: larger than ${api_types_1.MAX_AGENT_CHAT_ATTACHMENT_SIZE_MB} MB]`); continue; } const data = await this.fetchAttachmentData(attachment); if (!data || data.byteLength === 0) { attachmentNotes.push(`[Attachment "${name}" could not be downloaded]`); continue; } if (data.byteLength > api_types_1.MAX_AGENT_CHAT_ATTACHMENT_SIZE_BYTES) { attachmentNotes.push(`[Attachment "${name}" was skipped: larger than ${api_types_1.MAX_AGENT_CHAT_ATTACHMENT_SIZE_MB} MB]`); continue; } const mimeType = await (0, inbound_attachments_1.resolveInboundMimeType)(attachment.mimeType, data); const stored = await this.attachmentService.storeInbound({ agentId: this.agentId, projectId: this.n8nProjectId, threadId, resourceId, source: this.integration.type, fileName: name, mimeType, data, }); attachments.push({ id: stored.id, fileName: stored.fileName, mimeType: stored.mimeType, sizeBytes: stored.fileSizeBytes, }); } catch (error) { this.logger.warn('[AgentChatBridge] Failed to ingest attachment', { agentId: this.agentId, threadId, error: error instanceof Error ? error.message : String(error), }); attachmentNotes.push(`[Attachment "${name}" could not be processed]`); } } return { attachments, attachmentNotes }; } async fetchAttachmentData(attachment) { if (attachment.fetchData) return await attachment.fetchData(); if (Buffer.isBuffer(attachment.data)) return attachment.data; if (attachment.data) return Buffer.from(await attachment.data.arrayBuffer()); if (this.integration.type === 'discord' && attachment.url && this.discordHttpClient) { return await (0, discord_operations_1.downloadDiscordAttachment)(attachment.url, this.discordHttpClient); } return null; } async resolveBridgeExecutionContext(thread, message, platformAgentContext, statusRetry, isNewMention, replyExpectation) { return ((await this.integrationImpl?.createBridgeExecutionContext?.({ chat: this.chat, thread, message, logger: this.logger, agentId: this.agentId, statusRetry, isNewMention, replyExpectation, })) ?? { platformAgentContext }); } async handleSuspension(chunk, thread) { const { runId, toolCallId, suspendPayload } = chunk; if (!runId || !toolCallId) { this.logger.warn('[AgentChatBridge] Suspended chunk missing runId or toolCallId'); return 'failed'; } const cardPayload = (0, agent_chat_suspension_cards_1.buildSuspendCardPayload)(suspendPayload); if (!cardPayload) return 'skipped'; const callbackMetadata = { groupId: JSON.stringify([runId, toolCallId]), ...((0, agent_chat_suspension_cards_1.isApprovalSuspendPayload)(suspendPayload) ? { kind: 'approval' } : {}), }; try { const card = await this.componentMapper.toCard(cardPayload, runId, toolCallId, chunk.resumeSchema, this.getShortenCallback(callbackMetadata), this.integration.type); await thread.post({ card }); return 'posted'; } catch (error) { this.logger.error('[AgentChatBridge] Failed to post suspension card', { agentId: this.agentId, runId, toolCallId, error: error instanceof Error ? error.message : String(error), }); return 'failed'; } } async handleMessage(chunk, thread) { const agentMessage = chunk.message; if (!('content' in agentMessage) || !Array.isArray(agentMessage.content)) return false; const textParts = agentMessage.content .filter((part) => part.type === 'text' && 'text' in part) .map((part) => part.text); const textToPost = textParts.join(''); if (!textToPost.trim()) return false; try { await thread.post(textToPost); return true; } catch (error) { this.logger.error('[AgentChatBridge] Failed to post message chunk', { agentId: this.agentId, threadId: thread.id, error: error instanceof Error ? error.message : String(error), }); return false; } } getPlatformAgentContext() { return this.integrationImpl?.getPlatformAgentContext?.(this.chat) ?? {}; } prepareInboundText(text, context) { const trimmed = text?.trim() ?? ''; return this.integrationImpl?.prepareInboundText?.(trimmed, context) ?? trimmed; } async postErrorToThread(thread, error) { const message = error instanceof Error ? error.message : 'An unexpected error occurred'; this.logger.error('[AgentChatBridge] Error in handler', { agentId: this.agentId, threadId: thread?.id, error: message, }); try { if (!thread) { this.logger.warn("[AgentChatBridge] Couldn't post error message because thread is not set", { agentId: this.agentId, error: message, }); return; } await thread.post('⚠️ Something went wrong while processing your request. Please try again.'); } catch (postError) { this.logger.error('[AgentChatBridge] Failed to post error message', { agentId: this.agentId, error: postError instanceof Error ? postError.message : String(postError), }); } } } exports.AgentChatBridge = AgentChatBridge; //# sourceMappingURL=agent-chat-bridge.js.map