UNPKG

n8n

Version:

n8n Workflow Automation Tool

213 lines 9.2 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.InstanceAiCompactionService = void 0; const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const di_1 = require("@n8n/di"); const instance_ai_1 = require("@n8n/instance-ai"); const context_limits_1 = require("../../modules/chat-hub/context-limits"); const typeorm_memory_storage_1 = require("./storage/typeorm-memory-storage"); const METADATA_KEY = 'instanceAiConversationSummary'; const DEFAULT_CONTEXT_WINDOW = 128_000; function estimateTokens(text) { return Math.ceil(text.length / 4); } function getContextWindowForModel(modelId) { const raw = typeof modelId === 'string' ? modelId : 'specificationVersion' in modelId ? `${modelId.provider.split('.')[0]}/${modelId.modelId}` : modelId.id; const slashIndex = raw.indexOf('/'); if (slashIndex < 0) { for (const providerModels of Object.values(context_limits_1.maxContextWindowTokens)) { if (providerModels[raw]) return providerModels[raw]; for (const [registryModel, tokens] of Object.entries(providerModels)) { if (tokens > 0 && registryModel.startsWith(raw)) return tokens; } } return DEFAULT_CONTEXT_WINDOW; } const provider = raw.slice(0, slashIndex); const model = raw.slice(slashIndex + 1); const providerModels = context_limits_1.maxContextWindowTokens[provider]; if (!providerModels) return DEFAULT_CONTEXT_WINDOW; if (providerModels[model]) return providerModels[model]; for (const [registryModel, tokens] of Object.entries(providerModels)) { if (tokens > 0 && registryModel.startsWith(model)) return tokens; } return DEFAULT_CONTEXT_WINDOW; } const FIXED_CONTEXT_OVERHEAD_TOKENS = 8_000; const MIN_UNSUMMARIZED_TOKENS = 500; let InstanceAiCompactionService = class InstanceAiCompactionService { constructor(logger, memoryStorage, globalConfig) { this.logger = logger; this.memoryStorage = memoryStorage; this.maxContextWindowTokensCap = globalConfig.instanceAi.maxContextWindowTokens; } async prepareCompactedContext(threadId, memory, modelId, lastMessages, compactionThreshold = 0.8, currentInput) { try { const recentTail = Math.max(0, lastMessages); const currentInputTokens = currentInput ? estimateTokens(currentInput.text) : 0; const { messages: allMessages } = await this.memoryStorage.listMessages({ threadId, perPage: false, orderBy: { field: 'createdAt', direction: 'ASC' }, }); const rawMessageTokens = allMessages.reduce((sum, m) => sum + estimateTokens(this.extractRawText(m)), 0); const totalTokens = FIXED_CONTEXT_OVERHEAD_TOKENS + rawMessageTokens + currentInputTokens; const modelContextWindow = getContextWindowForModel(modelId); const contextWindow = this.maxContextWindowTokensCap > 0 ? Math.min(modelContextWindow, this.maxContextWindowTokensCap) : modelContextWindow; const threshold = contextWindow * compactionThreshold; const thread = await memory.getThreadById({ threadId }); const existing = this.parseMetadata(thread?.metadata?.[METADATA_KEY]); if (allMessages.length <= recentTail) { return this.formatCachedSummaryBlock(existing); } if (totalTokens < threshold) { return this.formatCachedSummaryBlock(existing); } const prefixEnd = allMessages.length - recentTail; const prefix = allMessages.slice(0, prefixEnd); let unsummarizedStart = 0; if (existing?.upToMessageId) { const idx = prefix.findIndex((m) => m.id === existing.upToMessageId); if (idx >= 0) { unsummarizedStart = idx + 1; } } const unsummarizedSlice = prefix.slice(unsummarizedStart); const unsummarizedTokens = unsummarizedSlice.reduce((sum, m) => sum + estimateTokens(this.extractRawText(m)), 0); if (unsummarizedTokens < MIN_UNSUMMARIZED_TOKENS) { return this.formatCachedSummaryBlock(existing); } const messageBatch = this.extractHighSignalContent(unsummarizedSlice); if (messageBatch.length === 0) { return this.formatCachedSummaryBlock(existing); } const lastCompactedMessage = unsummarizedSlice[unsummarizedSlice.length - 1]; const summary = await (0, instance_ai_1.generateCompactionSummary)(modelId, { previousSummary: existing?.summary ?? null, messageBatch, }); const newMetadata = { version: (existing?.version ?? 0) + 1, upToMessageId: lastCompactedMessage.id, summary, updatedAt: new Date().toISOString(), }; await this.saveMetadata(threadId, memory, newMetadata); return this.formatSummaryBlock(summary); } catch (error) { this.logger.warn('Conversation compaction failed, continuing without summary', { threadId, error: error instanceof Error ? error.message : String(error), }); return null; } } formatCachedSummaryBlock(existing) { if (existing?.summary) { return this.formatSummaryBlock(existing.summary); } return null; } extractRawText(msg) { const content = msg.content; if (typeof content === 'string') return content; return JSON.stringify(content); } extractHighSignalContent(messages) { const result = []; for (const msg of messages) { if (msg.role !== 'user' && msg.role !== 'assistant') continue; const text = this.extractTextFromContent(msg.content); if (!text) continue; result.push({ role: msg.role, text }); } return result; } extractTextFromContent(content) { if (typeof content === 'string') return content; const inner = content?.content; if (typeof inner === 'string') return inner; if (Array.isArray(inner)) { const textParts = []; for (const part of inner) { if (typeof part === 'string') { textParts.push(part); } else if (isTextPart(part)) { textParts.push(part.text); } } return textParts.join('\n'); } return ''; } formatSummaryBlock(summary) { return `<conversation-summary>\n${summary}\n</conversation-summary>`; } parseMetadata(raw) { if (!raw || typeof raw !== 'object') return null; const obj = raw; if (typeof obj.version === 'number' && typeof obj.upToMessageId === 'string' && typeof obj.summary === 'string' && typeof obj.updatedAt === 'string') { return obj; } return null; } async saveMetadata(threadId, memory, metadata) { await (0, instance_ai_1.patchThread)(memory, { threadId, update: ({ metadata: currentMetadata }) => ({ metadata: { ...currentMetadata, [METADATA_KEY]: metadata, }, }), }); } }; exports.InstanceAiCompactionService = InstanceAiCompactionService; exports.InstanceAiCompactionService = InstanceAiCompactionService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, typeorm_memory_storage_1.TypeORMMemoryStorage, config_1.GlobalConfig]) ], InstanceAiCompactionService); function isTextPart(part) { return (typeof part === 'object' && part !== null && 'type' in part && part.type === 'text' && 'text' in part && typeof part.text === 'string'); } //# sourceMappingURL=compaction.service.js.map