n8n
Version:
n8n Workflow Automation Tool
390 lines • 18.7 kB
JavaScript
"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