Skip to main content
Glama

Memory MCP Server

by win10ogod
short-term.js24 kB
/** * 短期记忆核心逻辑 * 基于关键词相关性、时间衰减和分数的动态记忆管理系统 */ import { extractKeywords } from '../nlp/jieba.js'; import { createContextSnapshot } from '../nlp/keywords.js'; import { normalizeModalities } from './modalities.js'; // --- 常量定义 --- const RELEVANCE_THRESHOLD = 5; // 激活相关记忆的阈值 const SCORE_INCREMENT_TOP = 5; // 高相关记忆分数增加值 const SCORE_INCREMENT_NEXT = 2; // 次相关记忆分数增加值 // -- 时间衰减 -- const TIME_DECAY_FACTOR_EXP = 2e-9; // 时间衰减因子(指数模型) const MAX_TIME_PENALTY = 15; // 时间衰减造成的最大分数惩罚值 // -- 随机选择权重 -- const BASE_RANDOM_WEIGHT = 1; // 基础权重 const RANDOM_WEIGHT_RECENCY_FACTOR = 1.7; // 近期性权重因子 const RANDOM_WEIGHT_SCORE_FACTOR = 1.1; // 分数权重因子 const MAX_SCORE_FOR_RANDOM_WEIGHT = 50; // 用于计算随机权重的最高分数上限 // -- 关键词提取 -- const KEYWORD_MIN_WEIGHT = 0.8; // 提取关键词的最低权重阈值 // -- 清理 -- const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000; // 清理间隔(1天) const MEMORY_TTL_MS = 365 * 24 * 60 * 60 * 1000; // 记忆最大存活时间(1年) const CLEANUP_MIN_SCORE_THRESHOLD = -5; // 清理时的最低分数阈值 const MIN_RETAINED_MEMORIES = 512; // 清理后最少保留的记忆数量 // -- 激活逻辑去重/过滤 -- const MIN_TIME_DIFFERENCE_ANY_MS = 10 * 60 * 1000; // 10分钟 const MIN_TIME_DIFFERENCE_SAME_CONVERSATION_MS = 20 * 60 * 1000; // 20分钟 const MAX_TOP_RELEVANT = 2; // 最多选几条最相关 const MAX_NEXT_RELEVANT = 1; // 最多选几条次相关 const MAX_RANDOM_FLASHBACK = 2; // 最多选几条随机 const TRANSCRIPT_KEYWORD_WEIGHT = 0.6; const MAX_TRANSCRIPT_KEYWORDS = 24; function getModalityEmbedding(modality) { if (!modality || typeof modality !== 'object') { return null; } const features = modality.features; if (!features || typeof features !== 'object') { return null; } const candidates = [features.embedding, features.vector, features.featureVector]; for (const candidate of candidates) { if (Array.isArray(candidate) && candidate.every(value => Number.isFinite(value))) { return candidate; } } return null; } function cosineSimilarity(vecA, vecB) { if (!Array.isArray(vecA) || !Array.isArray(vecB)) { return 0; } const length = Math.min(vecA.length, vecB.length); if (length === 0) { return 0; } let dot = 0; let magA = 0; let magB = 0; let usedDimensions = 0; for (let i = 0; i < length; i++) { const a = Number(vecA[i]); const b = Number(vecB[i]); if (!Number.isFinite(a) || !Number.isFinite(b)) { continue; } dot += a * b; magA += a * a; magB += b * b; usedDimensions++; } if (usedDimensions === 0 || magA === 0 || magB === 0) { return 0; } return dot / (Math.sqrt(magA) * Math.sqrt(magB)); } function collectModalityKeywords(modalities) { const collected = []; if (!Array.isArray(modalities)) { return collected; } for (const modality of modalities) { if (!modality || typeof modality !== 'object') { continue; } const keywordSources = []; if (Array.isArray(modality.keywords)) { keywordSources.push(...modality.keywords); } if (modality.features && typeof modality.features === 'object') { const featureKeywords = modality.features.keywords || modality.features.tags; if (Array.isArray(featureKeywords)) { keywordSources.push(...featureKeywords); } } for (const kw of keywordSources) { if (typeof kw === 'string') { collected.push({ word: kw, weight: 1 }); } else if (kw && typeof kw === 'object') { const word = typeof kw.word === 'string' ? kw.word : (typeof kw.term === 'string' ? kw.term : null); if (!word) { continue; } const weight = Number.isFinite(kw.weight) ? kw.weight : (Number.isFinite(kw.score) ? kw.score : 1); collected.push({ word, weight }); } } if (typeof modality.transcript === 'string' && modality.transcript.trim()) { const extracted = extractKeywords(modality.transcript, MAX_TRANSCRIPT_KEYWORDS); for (const kw of extracted) { collected.push({ word: kw.word, weight: kw.weight * TRANSCRIPT_KEYWORD_WEIGHT }); } } } return collected; } function calculateModalityVectorScore(memoryModalities, queryModalities) { if (!Array.isArray(memoryModalities) || !Array.isArray(queryModalities) || !memoryModalities.length || !queryModalities.length) { return 0; } let totalScore = 0; let comparisons = 0; for (const memoryModality of memoryModalities) { const memoryEmbedding = getModalityEmbedding(memoryModality); if (!memoryEmbedding) { continue; } for (const queryModality of queryModalities) { if (queryModality.type && memoryModality.type && queryModality.type !== memoryModality.type) { continue; } const queryEmbedding = getModalityEmbedding(queryModality); if (!queryEmbedding) { continue; } const similarity = cosineSimilarity(memoryEmbedding, queryEmbedding); if (Number.isFinite(similarity)) { totalScore += similarity; comparisons++; } } } if (comparisons === 0) { return 0; } return totalScore / comparisons; } /** * 短期记忆管理器 */ export class ShortTermMemoryManager { constructor() { this.memories = []; this.lastCleanupTime = Date.now(); } /** * 加载记忆数据 * @param {Array} memories - 记忆数组 */ loadMemories(memories) { if (!Array.isArray(memories)) { console.warn('[ShortTermMemory] Expected memories to be an array when loading, received:', typeof memories); this.memories = []; return; } const normalizedMemories = []; memories.forEach((mem, index) => { if (!mem || typeof mem !== 'object') { console.warn(`[ShortTermMemory] Skipping memory #${index}: invalid entry`); return; } const text = typeof mem.text === 'string' ? mem.text : ''; if (!text.trim()) { console.warn(`[ShortTermMemory] Skipping memory #${index}: missing text content`); return; } let timeStampSource = mem.time_stamp ?? mem.timestamp ?? mem.timeStamp; let parsedTimestamp = timeStampSource ? new Date(timeStampSource) : new Date(); if (!(parsedTimestamp instanceof Date) || Number.isNaN(parsedTimestamp.getTime())) { console.warn(`[ShortTermMemory] Memory #${index} has invalid timestamp, defaulting to current time`); parsedTimestamp = new Date(); } let keywords = []; if (Array.isArray(mem.keywords)) { keywords = mem.keywords .map((kw, kwIndex) => { if (!kw || typeof kw.word !== 'string') { console.warn(`[ShortTermMemory] Memory #${index} keyword #${kwIndex} is invalid, skipping`); return null; } const weight = Number.isFinite(kw.weight) ? kw.weight : 1; return { word: kw.word, weight }; }) .filter(Boolean); } else { console.warn(`[ShortTermMemory] Memory #${index} missing keywords array, defaulting to empty list`); } const score = Number.isFinite(mem.score) ? mem.score : 0; const conversationId = typeof mem.conversation_id === 'string' ? mem.conversation_id : 'default'; const modalities = normalizeModalities(mem.modalities ?? mem.attachments ?? []); normalizedMemories.push({ ...mem, text, keywords, score, conversation_id: conversationId, time_stamp: parsedTimestamp, modalities, attachments: modalities }); }); this.memories = normalizedMemories; } /** * 获取当前所有记忆 * @returns {Array} */ getMemories() { return this.memories; } /** * 提取消息的关键词 * @param {Array<{role: string, content: string}>} messages - 消息数组 * @param {Object} [weights] - 角色权重配置 * @returns {Promise<Array<{word: string, weight: number}>>} */ async extractMessageKeywords(messages, weights = {}) { const keywordMap = {}; for (const message of messages) { const text = message.content; if (!text || !text.trim()) continue; // 根据角色应用权重倍数 const multiplier = weights[message.role] || 1.0; const keywords = extractKeywords(text, 72); for (const kw of keywords) { keywordMap[kw.word] = kw.weight * multiplier + (keywordMap[kw.word] || 0); } } return Object.entries(keywordMap) .map(([word, weight]) => ({ word, weight })) .filter(kw => kw.weight >= KEYWORD_MIN_WEIGHT) .sort((a, b) => b.weight - a.weight) .slice(0, 72); } /** * 计算单个记忆的相关性分数 * @param {Object} memory - 记忆条目 * @param {Array<{word: string, weight: number}>} currentKeywords - 当前关键词 * @param {number} currentTimeStamp - 当前时间戳 * @returns {number} 相关性分数 */ calculateRelevance(memory, currentKeywords, currentTimeStamp, options = {}) { let relevanceScore = 0; const queryModalities = Array.isArray(options.queryModalities) ? options.queryModalities : normalizeModalities(options.attachments ?? []); const memoryModalities = Array.isArray(memory.modalities) ? memory.modalities : (Array.isArray(memory.attachments) ? normalizeModalities(memory.attachments) : []); // 关键词匹配分数 const memoryKeywordsArray = Array.isArray(memory.keywords) ? memory.keywords.filter(kw => kw && typeof kw.word === 'string') : []; const modalityKeywords = collectModalityKeywords(memoryModalities); const memoryKeywordMap = new Map(); for (const kw of [...memoryKeywordsArray, ...modalityKeywords]) { if (!kw || typeof kw.word !== 'string') { continue; } const baseWeight = Number.isFinite(kw.weight) ? kw.weight : 1; const existing = memoryKeywordMap.get(kw.word); if (existing) { existing.weight += baseWeight; } else { memoryKeywordMap.set(kw.word, { word: kw.word, weight: baseWeight }); } } let keywordMatchScore = 0; for (const currentKw of currentKeywords) { if (!currentKw || typeof currentKw.word !== 'string') { continue; } if (!memoryKeywordMap.has(currentKw.word)) { continue; } const memoryKw = memoryKeywordMap.get(currentKw.word); const currentWeight = Number.isFinite(currentKw.weight) ? currentKw.weight : 1; const memoryWeight = Number.isFinite(memoryKw?.weight) ? memoryKw.weight : 0; keywordMatchScore += currentWeight + memoryWeight; } relevanceScore += keywordMatchScore; // 时间衰减惩罚 const memoryTime = memory.time_stamp instanceof Date ? memory.time_stamp.getTime() : 0; const timeDiff = Math.max(0, currentTimeStamp - (Number.isFinite(memoryTime) ? memoryTime : 0)); const timePenalty = MAX_TIME_PENALTY * (1 - Math.exp(-timeDiff * TIME_DECAY_FACTOR_EXP)); relevanceScore -= timePenalty; // 加上记忆自身分数 const memoryScore = Number.isFinite(memory.score) ? memory.score : 0; relevanceScore += memoryScore; // 模态向量相似度 const vectorScore = calculateModalityVectorScore(memoryModalities, queryModalities); if (Number.isFinite(vectorScore) && vectorScore !== 0) { const vectorWeight = Number.isFinite(options.modalityVectorWeight) ? options.modalityVectorWeight : 10; relevanceScore += vectorScore * vectorWeight; } return relevanceScore; } /** * 加权随机选择一个项 * @template T * @param {T[]} items - 待选择项 * @param {number[]} weights - 权重 * @returns {T | null} */ selectOneWeightedRandom(items, weights) { if (!items?.length || items.length !== weights.length) { return null; } const totalWeight = weights.reduce((sum, w) => sum + Math.max(0, w), 0); if (totalWeight <= 0) { return items.length ? items[Math.floor(Math.random() * items.length)] : null; } const randomVal = Math.random() * totalWeight; let cumulativeWeight = 0; for (let i = 0; i < items.length; i++) { const weight = Math.max(0, weights[i]); cumulativeWeight += weight; if (randomVal <= cumulativeWeight) { return items[i]; } } return items[items.length - 1]; } /** * 搜索相关记忆 * @param {Array<{role: string, content: string}>} recentMessages - 最近的消息 * @param {string} conversationId - 当前对话 ID * @param {Object} [options] - 选项 * @returns {Promise<Object>} 搜索结果 */ async searchRelevantMemories(recentMessages, conversationId, options = {}) { const currentTimeStamp = Date.now(); const queryModalities = normalizeModalities(options.queryModalities ?? options.attachments ?? []); // 提取当前对话的关键词 const messageKeywords = await this.extractMessageKeywords(recentMessages, options.roleWeights); const modalityKeywords = collectModalityKeywords(queryModalities); const currentKeywords = [...messageKeywords, ...modalityKeywords]; const searchOptions = { ...options, queryModalities }; if ('attachments' in searchOptions) { delete searchOptions.attachments; } // 计算所有记忆的相关性 const scoredMemories = this.memories .map((mem, index) => ({ memory: mem, relevance: this.calculateRelevance(mem, currentKeywords, currentTimeStamp, searchOptions), index })) .filter(item => item.relevance >= RELEVANCE_THRESHOLD) .sort((a, b) => b.relevance - a.relevance); // 选择相关和次相关记忆 const selectedIndices = new Set(); const finalTopRelevant = []; const finalNextRelevant = []; const allSelectedRelevantMemories = []; for (const candidateMemory of scoredMemories) { if (finalTopRelevant.length >= MAX_TOP_RELEVANT && finalNextRelevant.length >= MAX_NEXT_RELEVANT) { break; } const isFromSameConversation = candidateMemory.memory.conversation_id === conversationId; const timeDiffSinceMemory = currentTimeStamp - candidateMemory.memory.time_stamp.getTime(); // 跳过同对话的近期记忆 if (isFromSameConversation && timeDiffSinceMemory < MIN_TIME_DIFFERENCE_SAME_CONVERSATION_MS) { continue; } // 检查是否与已选记忆时间过近 let isTooCloseToSelected = false; for (const selectedMem of allSelectedRelevantMemories) { if (Math.abs(candidateMemory.memory.time_stamp.getTime() - selectedMem.memory.time_stamp.getTime()) < MIN_TIME_DIFFERENCE_ANY_MS) { isTooCloseToSelected = true; break; } } if (isTooCloseToSelected) continue; // 分配到 Top 或 Next if (finalTopRelevant.length < MAX_TOP_RELEVANT) { finalTopRelevant.push(candidateMemory); allSelectedRelevantMemories.push(candidateMemory); selectedIndices.add(candidateMemory.index); } else if (finalNextRelevant.length < MAX_NEXT_RELEVANT) { finalNextRelevant.push(candidateMemory); allSelectedRelevantMemories.push(candidateMemory); selectedIndices.add(candidateMemory.index); } } // 随机闪回选择 const finalRandomFlashback = []; let availableForRandomPool = this.memories .map((mem, index) => ({ memory: mem, index })) .filter(item => !selectedIndices.has(item.index)); for (let i = 0; i < MAX_RANDOM_FLASHBACK && availableForRandomPool.length; i++) { const currentCandidates = availableForRandomPool.filter(candidate => { const isFromSameConversation = candidate.memory.conversation_id === conversationId; const timeDiffSinceMemory = currentTimeStamp - candidate.memory.time_stamp.getTime(); if (isFromSameConversation && timeDiffSinceMemory < MIN_TIME_DIFFERENCE_SAME_CONVERSATION_MS) { return false; } const allPreviouslySelected = [...allSelectedRelevantMemories, ...finalRandomFlashback]; for (const selectedItem of allPreviouslySelected) { if (Math.abs(candidate.memory.time_stamp.getTime() - selectedItem.memory.time_stamp.getTime()) < MIN_TIME_DIFFERENCE_ANY_MS) { return false; } } return true; }); if (!currentCandidates.length) break; // 计算权重 const weights = currentCandidates.map(item => { const ageFactor = Math.max(0, 1 - (currentTimeStamp - item.memory.time_stamp.getTime()) / MEMORY_TTL_MS); const cappedScore = Math.max(0, Math.min(item.memory.score, MAX_SCORE_FOR_RANDOM_WEIGHT)); const normalizedScoreFactor = MAX_SCORE_FOR_RANDOM_WEIGHT > 0 ? cappedScore / MAX_SCORE_FOR_RANDOM_WEIGHT : 0; const weight = BASE_RANDOM_WEIGHT + ageFactor * RANDOM_WEIGHT_RECENCY_FACTOR + normalizedScoreFactor * RANDOM_WEIGHT_SCORE_FACTOR; return Math.max(0, weight); }); const selectedRandomItem = this.selectOneWeightedRandom(currentCandidates, weights); if (selectedRandomItem) { selectedRandomItem.relevance = this.calculateRelevance(selectedRandomItem.memory, currentKeywords, currentTimeStamp, searchOptions); finalRandomFlashback.push(selectedRandomItem); availableForRandomPool = availableForRandomPool.filter(item => item.index !== selectedRandomItem.index); } else { break; } } // 强化被激活的记忆 finalTopRelevant.forEach(item => { const memoryToUpdate = this.memories[item.index]; if (memoryToUpdate) { memoryToUpdate.score = Math.min(memoryToUpdate.score + SCORE_INCREMENT_TOP, 100); } }); finalNextRelevant.forEach(item => { const memoryToUpdate = this.memories[item.index]; if (memoryToUpdate) { memoryToUpdate.score = Math.min(memoryToUpdate.score + SCORE_INCREMENT_NEXT, 100); } }); return { topRelevant: finalTopRelevant, nextRelevant: finalNextRelevant, randomFlashback: finalRandomFlashback }; } /** * 添加新记忆 * @param {Array<{role: string, content: string}>} messages - 消息数组 * @param {string} conversationId - 对话 ID * @param {Object} [options] - 选项 * @returns {Promise<boolean>} 是否成功添加 */ async addMemory(messages, conversationId, options = {}) { if (!messages || messages.length === 0) { return false; } // 提取关键词 const keywords = await this.extractMessageKeywords(messages, options.roleWeights); // 创建上下文快照 const contextSnapshot = createContextSnapshot(messages); if (!contextSnapshot.trim()) { console.warn('Skipping empty memory'); return false; } // 添加新记忆 const lastMessage = messages[messages.length - 1]; const modalities = normalizeModalities(options.modalities ?? options.attachments ?? []); this.memories.push({ time_stamp: new Date(lastMessage.timestamp || Date.now()), text: contextSnapshot, keywords: keywords, score: 0, conversation_id: conversationId, modalities, attachments: modalities }); return true; } /** * 删除匹配的记忆 * @param {string|RegExp} pattern - 匹配模式 * @returns {number} 删除的数量 */ deleteMemories(pattern) { const oldLength = this.memories.length; if (pattern instanceof RegExp) { this.memories = this.memories.filter(mem => !pattern.test(mem.text)); } else { this.memories = this.memories.filter(mem => !mem.text.includes(pattern)); } return oldLength - this.memories.length; } /** * 清理旧的或不相关的记忆 * @param {number} [currentTimeStamp] - 当前时间戳 * @returns {number} 清理的数量 */ cleanup(currentTimeStamp = Date.now()) { const initialCount = this.memories.length; const oneYearAgo = currentTimeStamp - MEMORY_TTL_MS; const passingMemories = []; const failingMemories = []; for (const mem of this.memories) { const relevance = this.calculateRelevance(mem, [], currentTimeStamp, {}); mem._relevance = relevance; if (mem.time_stamp.getTime() < oneYearAgo) { failingMemories.push(mem); } else if (relevance >= CLEANUP_MIN_SCORE_THRESHOLD) { passingMemories.push(mem); } else { failingMemories.push(mem); } } if (passingMemories.length >= MIN_RETAINED_MEMORIES) { this.memories = passingMemories; } else { const neededFromFailing = MIN_RETAINED_MEMORIES - passingMemories.length; failingMemories.sort((a, b) => b._relevance - a._relevance); const supplementaryMemories = failingMemories.slice(0, neededFromFailing); this.memories = [...passingMemories, ...supplementaryMemories]; } // 清理临时属性 for (const mem of this.memories) { delete mem._relevance; } this.lastCleanupTime = currentTimeStamp; const removed = initialCount - this.memories.length; if (removed > 0) { console.log(`[Memory] Cleanup: removed ${removed} entries, ${this.memories.length} remaining`); } return removed; } /** * 检查是否需要清理 * @returns {boolean} */ shouldCleanup() { return (Date.now() - this.lastCleanupTime) > CLEANUP_INTERVAL_MS; } /** * 获取记忆统计信息 * @returns {Object} */ getStats() { const now = Date.now(); const conversationCounts = {}; let totalScore = 0; let maxScore = -Infinity; let minScore = Infinity; for (const mem of this.memories) { conversationCounts[mem.conversation_id] = (conversationCounts[mem.conversation_id] || 0) + 1; totalScore += mem.score; maxScore = Math.max(maxScore, mem.score); minScore = Math.min(minScore, mem.score); } return { total: this.memories.length, conversationCounts, avgScore: this.memories.length > 0 ? totalScore / this.memories.length : 0, maxScore: this.memories.length > 0 ? maxScore : 0, minScore: this.memories.length > 0 ? minScore : 0, oldestMemory: this.memories.length > 0 ? Math.min(...this.memories.map(m => m.time_stamp.getTime())) : null, newestMemory: this.memories.length > 0 ? Math.max(...this.memories.map(m => m.time_stamp.getTime())) : null, lastCleanup: this.lastCleanupTime }; } /** * 获取最频繁的对话 ID * @returns {string|null} */ getMostFrequentConversation() { if (this.memories.length === 0) return null; const counts = {}; for (const mem of this.memories) { counts[mem.conversation_id] = (counts[mem.conversation_id] || 0) + 1; } let maxCount = 0; let mostFrequent = null; for (const [id, count] of Object.entries(counts)) { if (count > maxCount) { maxCount = count; mostFrequent = id; } } return mostFrequent; } } export default ShortTermMemoryManager;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/win10ogod/memory-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server