UNPKG

n8n

Version:

n8n Workflow Automation Tool

540 lines 20.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentChatBridge = void 0; const di_1 = require("@n8n/di"); const agent_chat_integration_1 = require("./agent-chat-integration"); const callback_store_1 = require("./callback-store"); const types_1 = require("./types"); class AgentChatBridge { constructor(chat, agentId, agentService, componentMapper, logger, n8nProjectId, integrationType) { this.chat = chat; this.agentId = agentId; this.agentService = agentService; this.componentMapper = componentMapper; this.logger = logger; this.n8nProjectId = n8nProjectId; this.integrationType = integrationType; this.activeResumedRuns = new Set(); this.richInteractionInputs = new Map(); this.integration = di_1.Container.get(agent_chat_integration_1.ChatIntegrationRegistry).get(integrationType); if (this.integration?.needsShortCallbackData) { this.callbackStore = new callback_store_1.CallbackStore(); } this.disableStreaming = this.integration?.disableStreaming ?? false; this.registerHandlers(); } static create(chat, agentId, agentService, componentMapper, logger, n8nProjectId, integrationType) { const agentExecutor = { async *executeForChatPublished({ memory, agentId: aid, message, integrationType }) { yield* agentService.executeForChatPublished({ agentId: aid, projectId: n8nProjectId, message, memory: { threadId: memory.threadId.id, resourceId: memory.resourceId }, integrationType, }); }, async *resumeForChat(config) { yield* agentService.resumeForChat(config); }, }; return new AgentChatBridge(chat, agentId, agentExecutor, componentMapper, logger, n8nProjectId, integrationType); } registerHandlers() { this.chat.onNewMention(async (thread, message) => { try { await thread.subscribe(); await this.executeAndStream(thread, message); } catch (error) { await this.postErrorToThread(thread, error); } }); this.chat.onSubscribedMessage(async (thread, message) => { try { await this.executeAndStream(thread, message); } catch (error) { await this.postErrorToThread(thread, error); } }); this.chat.onAction(async (event) => { try { await this.handleAction(event); } catch (error) { await this.postErrorToThread(event.thread, error); } }); } dispose() { this.callbackStore?.dispose(); } resolveThreadId(thread) { return (0, types_1.toInternalThreadId)(this.integration?.formatThreadId?.fromSdk(thread) ?? thread.id); } getShortenCallback() { if (!this.callbackStore) return undefined; const store = this.callbackStore; return async (actionId, value) => { const key = await store.store(actionId, value); return { id: key, value: '' }; }; } async executeAndStream(thread, message) { const text = message.text?.trim(); if (!text) return; const threadId = this.resolveThreadId(thread); const stream = this.agentService.executeForChatPublished({ agentId: this.agentId, projectId: this.n8nProjectId, message: text, memory: { threadId, resourceId: message.author.userId }, integrationType: this.integrationType, }); await this.consumeStream(stream, thread); } async consumeStream(stream, thread) { if (this.disableStreaming) { await this.consumeStreamBuffered(stream, thread); return; } const textStream = { yield: null, end: null, }; let streamingPost = null; const createTextIterable = () => { const queue = []; let done = false; let waiting = null; textStream.yield = (text) => { if (waiting) { const resolve = waiting; waiting = null; resolve({ value: text, done: false }); } else { queue.push(text); } }; textStream.end = () => { done = true; if (waiting) { const resolve = waiting; waiting = null; resolve({ value: '', done: true }); } }; return { [Symbol.asyncIterator]() { return { async next() { if (queue.length > 0) { return { value: queue.shift(), done: false }; } if (done) { return { value: '', done: true }; } return await new Promise((resolve) => { waiting = resolve; }); }, }; }, }; }; const startStreamingPost = () => { const iterable = createTextIterable(); streamingPost = thread.post(iterable).catch((postError) => { this.logger.error('[AgentChatBridge] Streaming post failed', { error: postError instanceof Error ? postError.message : String(postError), }); }); }; const endStreamingPost = async () => { if (textStream.end) { textStream.end(); textStream.end = null; textStream.yield = null; } if (streamingPost) { await streamingPost; streamingPost = null; } }; const ensureStreamingPost = () => { if (!streamingPost) startStreamingPost(); }; for await (const chunk of stream) { switch (chunk.type) { case 'text-delta': { const { delta } = chunk; ensureStreamingPost(); textStream.yield?.(delta); break; } case 'reasoning-delta': { const { delta } = chunk; ensureStreamingPost(); textStream.yield?.(`_${delta}_`); break; } case 'tool-call': this.stashRichInteractionInput(chunk); break; case 'tool-call-suspended': this.richInteractionInputs.delete(chunk.toolCallId); await endStreamingPost(); await this.handleSuspension(chunk, thread); break; case 'tool-result': if (this.isRichInteractionDisplayOnly(chunk)) { await endStreamingPost(); await this.handleDisplayOnly(chunk, thread); } else { this.richInteractionInputs.delete(chunk.toolCallId); } break; case 'message': await endStreamingPost(); await this.handleMessage(chunk, thread); break; case 'error': await endStreamingPost(); await this.postErrorToThread(thread, chunk.error); break; default: break; } } await endStreamingPost(); } async consumeStreamBuffered(stream, thread) { let buffer = ''; const flushBuffer = async () => { const text = buffer; buffer = ''; if (!text.trim()) return; try { await thread.post({ markdown: text }); } catch (postError) { await this.postErrorToThread(thread, postError); this.logger.error('[AgentChatBridge] Buffered post failed', { error: postError instanceof Error ? postError.message : String(postError), }); } }; for await (const chunk of stream) { switch (chunk.type) { case 'text-delta': buffer += chunk.delta; break; case 'reasoning-delta': buffer += `_${chunk.delta}_`; break; case 'tool-call': this.stashRichInteractionInput(chunk); break; case 'tool-call-suspended': this.richInteractionInputs.delete(chunk.toolCallId); await flushBuffer(); await this.handleSuspension(chunk, thread); break; case 'tool-result': if (this.isRichInteractionDisplayOnly(chunk)) { await flushBuffer(); await this.handleDisplayOnly(chunk, thread); } else { this.richInteractionInputs.delete(chunk.toolCallId); } break; case 'message': await flushBuffer(); await this.handleMessage(chunk, thread); break; case 'error': await flushBuffer(); await this.postErrorToThread(thread, chunk.error); break; default: break; } } await flushBuffer(); } async handleSuspension(chunk, thread) { const { runId, toolCallId, suspendPayload } = chunk; if (!runId || !toolCallId) { this.logger.warn('[AgentChatBridge] Suspended chunk missing runId or toolCallId'); return; } if (chunk.toolName === 'rich_interaction') { await this.handleRichInteraction(chunk, thread); return; } const payload = suspendPayload; const hasComponents = payload && 'components' in payload && Array.isArray(payload.components) && payload.components.length > 0; let cardPayload; if (hasComponents) { cardPayload = payload; } else { const message = payload && typeof payload === 'object' && 'message' in payload ? String(payload.message) : 'Action required — approve or deny?'; cardPayload = { title: message, components: [ { type: 'button', label: 'Approve', value: 'true', style: 'primary' }, { type: 'button', label: 'Deny', value: 'false', style: 'danger' }, ], }; } try { const card = await this.componentMapper.toCard(cardPayload, runId, toolCallId, chunk.resumeSchema, this.getShortenCallback(), this.integrationType); await thread.post({ card }); } catch (error) { this.logger.error('[AgentChatBridge] Failed to post suspension card', { agentId: this.agentId, runId, toolCallId, error: error instanceof Error ? error.message : String(error), }); } } async handleRichInteraction(chunk, thread) { const { runId, toolCallId, suspendPayload } = chunk; const payload = suspendPayload; if (!payload?.components?.length) { this.logger.warn('[AgentChatBridge] rich_interaction has no components'); return; } const riResumeSchema = { type: 'object', properties: { type: { type: 'string' }, value: { type: 'string' }, }, }; try { const card = await this.componentMapper.toCard(payload, runId, toolCallId, riResumeSchema, this.getShortenCallback(), this.integrationType); await thread.post(card); } catch (error) { this.logger.error('[AgentChatBridge] Failed to post rich interaction card', { error: error instanceof Error ? error.message : String(error), }); } } stashRichInteractionInput(chunk) { if (chunk.toolName !== 'rich_interaction') return; this.richInteractionInputs.set(chunk.toolCallId, chunk.input); } isRichInteractionDisplayOnly(chunk) { if (chunk.toolName !== 'rich_interaction') return false; const out = chunk.output; return (typeof out === 'object' && out !== null && 'displayOnly' in out && out.displayOnly === true); } async handleDisplayOnly(chunk, thread) { const { toolCallId } = chunk; const input = this.richInteractionInputs.get(toolCallId); this.richInteractionInputs.delete(toolCallId); const cardPayload = input; if (!cardPayload?.components?.length) { this.logger.warn('[AgentChatBridge] display-only rich_interaction has no components', { toolCallId, }); return; } const displayResumeSchema = { type: 'object', properties: { type: { type: 'string' }, value: { type: 'string' } }, }; try { const card = await this.componentMapper.toCard(cardPayload, '', toolCallId, displayResumeSchema, this.getShortenCallback(), this.integrationType); await thread.post({ card }); } catch (error) { this.logger.error('[AgentChatBridge] Failed to post display card', { error: error instanceof Error ? error.message : String(error), }); } } async handleMessage(chunk, thread) { const agentMessage = chunk.message; if (!('content' in agentMessage) || !Array.isArray(agentMessage.content)) return; const textParts = agentMessage.content .filter((part) => part.type === 'text' && 'text' in part) .map((part) => part.text); const textToPost = textParts.join(''); if (!textToPost.trim()) return; try { await thread.post(textToPost); } 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), }); } } parseActionId(actionId, value) { if (actionId.startsWith('ri-btn:')) { const parts = actionId.split(':'); if (parts.length < 4) { this.logger.warn('[AgentChatBridge] Malformed ri-btn action ID', { actionId }); return null; } let resumeData; try { resumeData = JSON.parse(value ?? ''); } catch { resumeData = { type: 'button', value }; } return { runId: parts[1], toolCallId: parts.slice(2, -1).join(':'), resumeData }; } if (actionId.startsWith('ri-sel:')) { const parts = actionId.split(':'); if (parts.length < 4) { this.logger.warn('[AgentChatBridge] Malformed ri-sel action ID', { actionId }); return null; } return { runId: parts[2], toolCallId: parts.slice(3).join(':'), resumeData: { type: 'select', id: parts[1], value }, }; } if (actionId.startsWith('resume:')) { const parts = actionId.split(':'); if (parts.length < 4) { this.logger.warn('[AgentChatBridge] Malformed action ID', { actionId }); return null; } let resumeData; try { resumeData = JSON.parse(value ?? ''); } catch { resumeData = { value }; } return { runId: parts[1], toolCallId: parts.slice(2, -1).join(':'), resumeData }; } return null; } async resolveCallbackData(actionId, value, thread) { if (!this.callbackStore) return { actionId, value }; const resolved = await this.callbackStore.resolve(actionId); if (!resolved) { this.logger.warn('[AgentChatBridge] Callback key not found or expired', { actionId }); await thread.post('This action is no longer available. The link may have expired or already been used.'); return null; } return { actionId: resolved.actionId, value: resolved.value }; } async cleanUpBeforeResume(event) { try { await event.adapter.deleteMessage(event.threadId, event.messageId); } catch (deleteError) { this.logger.warn('[AgentChatBridge] Failed to delete card message', { error: deleteError instanceof Error ? deleteError.message : String(deleteError), }); } const threadInternal = event.thread; if (threadInternal?._currentMessage?.raw) { const raw = threadInternal._currentMessage.raw; if (raw.team && typeof raw.team === 'object' && !raw.team_id) { raw.team_id = raw.team.id; } } } async executeResume(thread, runId, toolCallId, resumeData) { if (this.activeResumedRuns.has(runId)) { this.logger.warn('[AgentChatBridge] Run is already active', { runId, toolCallId }); await thread.post('This action has already been handled'); return; } this.activeResumedRuns.add(runId); try { const stream = this.agentService.resumeForChat({ agentId: this.agentId, projectId: this.n8nProjectId, runId, toolCallId, resumeData, integrationType: this.integrationType, }); await this.consumeStream(stream, thread); } finally { this.activeResumedRuns.delete(runId); } } async handleAction(event) { const { thread } = event; if (!thread) { this.logger.warn('[AgentChatBridge] Thread is not set for event', { threadId: event.threadId, actionId: event.actionId, }); return; } const callbackData = await this.resolveCallbackData(event.actionId, event.value, thread); if (!callbackData) return; const parsed = this.parseActionId(callbackData.actionId, callbackData.value); if (!parsed) return; await this.cleanUpBeforeResume(event); await this.executeResume(thread, parsed.runId, parsed.toolCallId, parsed.resumeData); } 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