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