UNPKG

n8n

Version:

n8n Workflow Automation Tool

281 lines • 11.8 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.InstanceAiMemoryService = 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 db_snapshot_storage_1 = require("./storage/db-snapshot-storage"); const not_found_error_1 = require("../../errors/response-errors/not-found.error"); const message_parser_1 = require("./message-parser"); const instance_ai_checkpoint_repository_1 = require("./repositories/instance-ai-checkpoint.repository"); const instance_ai_pending_confirmation_repository_1 = require("./repositories/instance-ai-pending-confirmation.repository"); const typeorm_agent_memory_1 = require("./storage/typeorm-agent-memory"); function isAgentMessageLike(value) { return (typeof value === 'object' && value !== null && typeof value.id === 'string' && 'role' in value); } function messageCreatedAtMs(message) { const at = message.createdAt; if (at instanceof Date) return at.getTime(); const parsed = new Date(at).getTime(); return Number.isNaN(parsed) ? 0 : parsed; } function mergeMessagesById(stored, extras) { if (extras.length === 0) return stored; const byId = new Map(); for (const message of stored) byId.set(message.id, message); for (const message of extras) if (!byId.has(message.id)) byId.set(message.id, message); return [...byId.values()].sort((a, b) => messageCreatedAtMs(a) - messageCreatedAtMs(b)); } let InstanceAiMemoryService = class InstanceAiMemoryService { constructor(logger, globalConfig, agentMemory, dbSnapshotStorage, checkpointRepository, pendingConfirmationRepository) { this.logger = logger; this.agentMemory = agentMemory; this.dbSnapshotStorage = dbSnapshotStorage; this.checkpointRepository = checkpointRepository; this.pendingConfirmationRepository = pendingConfirmationRepository; this.instanceAiConfig = globalConfig.instanceAi; } async listThreads(userId, page = 0, perPage = 100) { const result = await this.agentMemory.listThreads({ filter: { resourceId: userId }, perPage, page, orderBy: { field: 'updatedAt', direction: 'DESC' }, }); return { threads: result.threads.map((t) => this.toThreadInfo(t)), total: result.total, page: result.page, hasMore: result.hasMore, }; } async ensureThread(userId, threadId) { const existing = await this.agentMemory.getThread(threadId); if (existing) { if (existing.resourceId !== userId) { throw new Error(`Thread ${threadId} is not owned by user ${userId}`); } return { thread: this.toThreadInfo(existing), created: false, }; } const created = await this.agentMemory.saveThread({ id: threadId, resourceId: userId, title: '', }); return { thread: this.toThreadInfo(created), created: true, }; } async getThreadMessages(_userId, threadId, options) { const result = await this.agentMemory.listMessages({ threadId, limit: options?.limit ?? 50, page: options?.page ?? 0, }); return { threadId, messages: result.messages.map((m) => this.toThreadMessage(m)), }; } async getRichMessages(_userId, threadId, options) { const result = await this.agentMemory.listMessages({ threadId, limit: options?.limit ?? 50, page: options?.page ?? 0, }); let snapshots = await this.dbSnapshotStorage.getAll(threadId).catch((error) => { this.logger.warn('Failed to load agent tree snapshots', { threadId, error: error instanceof Error ? error.message : String(error), }); return []; }); if (options?.excludeRunIds?.length) { const excluded = new Set(options.excludeRunIds); snapshots = snapshots.filter((s) => !excluded.has(s.runId)); } const checkpointMessages = await this.loadInFlightCheckpointMessages(threadId); const storedMessages = mergeMessagesById(result.messages, checkpointMessages); const messages = (0, message_parser_1.parseStoredMessages)(storedMessages, snapshots); await this.flagExpiredConfirmations(messages); return { threadId, messages }; } async flagExpiredConfirmations(messages) { const requestIds = (0, message_parser_1.collectConfirmationRequestIds)(messages); if (requestIds.length === 0) return; try { const live = await this.pendingConfirmationRepository.findLiveRequestIds(requestIds, new Date()); (0, message_parser_1.markExpiredConfirmations)(messages, live); } catch (error) { this.logger.warn('Failed to flag expired confirmation cards', { error: error instanceof Error ? error.message : String(error), }); } } async loadInFlightCheckpointMessages(threadId) { let checkpoints; try { checkpoints = await this.checkpointRepository.findActiveByThreadId(threadId); } catch (error) { this.logger.warn('Failed to load in-flight checkpoint messages', { threadId, error: error instanceof Error ? error.message : String(error), }); return []; } const merged = []; const seen = new Set(); for (const checkpoint of checkpoints) { const stateMessages = checkpoint.state?.messageList?.messages ?? []; for (const candidate of stateMessages) { if (!isAgentMessageLike(candidate) || seen.has(candidate.id)) continue; seen.add(candidate.id); merged.push({ ...candidate, createdAt: candidate.createdAt instanceof Date ? candidate.createdAt : new Date(candidate.createdAt), }); } } return merged; } async getLatestRunSnapshot(threadId, options) { return await this.dbSnapshotStorage.getLatest(threadId, options); } async validateThreadOwnership(userId, threadId) { return (await this.checkThreadOwnership(userId, threadId)) === 'owned'; } async checkThreadOwnership(userId, threadId) { const thread = await this.agentMemory.getThread(threadId); if (!thread) return 'not_found'; return thread.resourceId === userId ? 'owned' : 'other_user'; } async deleteThread(threadId) { await this.agentMemory.deleteThreadsByResourceIdPrefix((0, instance_ai_1.createSubAgentResourceIdPrefix)(threadId)); await this.agentMemory.deleteThread(threadId); } async renameThread(threadId, title) { return await this.updateThread(threadId, { title }); } async updateThread(threadId, updates) { const updated = await (0, instance_ai_1.patchThread)(this.agentMemory, { threadId, update: ({ metadata }) => { const patch = { metadata: { ...metadata, ...updates.metadata }, }; if (updates.title !== undefined) { patch.title = updates.title; patch.metadata.titleRefined = true; } return patch; }, }); if (!updated) { throw new not_found_error_1.NotFoundError(`Thread ${threadId} not found`); } return this.toThreadInfo(updated); } async getThreadMetadata(userId, threadId) { const thread = await this.agentMemory.getThread(threadId); if (!thread || thread.resourceId !== userId) return undefined; return thread.metadata; } async cleanupExpiredThreads(onThreadDeleted) { const ttlDays = this.instanceAiConfig.threadTtlDays; if (!ttlDays || ttlDays <= 0) return 0; const cutoff = new Date(Date.now() - ttlDays * 24 * 60 * 60 * 1000); let deletedCount = 0; const perPage = 100; let hasMore = true; while (hasMore) { const result = await this.agentMemory.listThreads({ perPage, page: 0, orderBy: { field: 'updatedAt', direction: 'ASC' }, }); let deletedInPage = 0; for (const thread of result.threads) { if (thread.updatedAt < cutoff) { try { await onThreadDeleted?.(thread.id); await this.deleteThread(thread.id); deletedCount++; deletedInPage++; } catch (error) { this.logger.warn('Failed to delete expired thread', { threadId: thread.id, error: error instanceof Error ? error.message : String(error), }); } } } hasMore = deletedInPage > 0 && result.hasMore; } if (deletedCount > 0) { this.logger.info(`Cleaned up ${deletedCount} expired conversation threads (TTL: ${ttlDays} days)`); } return deletedCount; } toThreadInfo(thread) { return { id: thread.id, title: thread.title, resourceId: thread.resourceId, createdAt: thread.createdAt.toISOString(), updatedAt: thread.updatedAt.toISOString(), metadata: thread.metadata, }; } toThreadMessage(message) { return { id: message.id, role: 'role' in message ? message.role : 'custom', content: 'content' in message ? message.content : message.data, type: message.type, createdAt: message.createdAt.toISOString(), }; } }; exports.InstanceAiMemoryService = InstanceAiMemoryService; exports.InstanceAiMemoryService = InstanceAiMemoryService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, config_1.GlobalConfig, typeorm_agent_memory_1.TypeORMAgentMemory, db_snapshot_storage_1.DbSnapshotStorage, instance_ai_checkpoint_repository_1.InstanceAiCheckpointRepository, instance_ai_pending_confirmation_repository_1.InstanceAiPendingConfirmationRepository]) ], InstanceAiMemoryService); //# sourceMappingURL=instance-ai-memory.service.js.map