UNPKG

permamind

Version:

An MCP server that provides an immortal memory layer for AI agents and clients

693 lines (692 loc) 27 kB
// VIP01Filter removed - using manual filters now import { event, fetchEvents, fetchEventsVIP01 } from "../relay.js"; // Constants for memory kinds const MEMORY_KINDS = { AI_MEMORY: "10", CONTACT_MAPPING: "31", CONTEXT_DOCUMENTATION: "50", MEMORY_CONTEXT: "40", MEMORY_RELATIONSHIP: "11", REASONING_CHAIN: "23", TOKEN_MAPPING: "30", }; // Validation utilities const isValidImportance = (importance) => importance >= 0 && importance <= 1; const isValidStrength = (strength) => strength >= 0 && strength <= 1; const isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0; const aiService = () => { return { addEnhanced: async (signer, hubId, memory) => { // Validate required fields if (!memory.content || !isNonEmptyString(memory.content)) { throw new Error("Memory content is required"); } if (!memory.p || !isNonEmptyString(memory.p)) { throw new Error("Memory p parameter is required"); } if (memory.importance !== undefined && !isValidImportance(memory.importance)) { throw new Error("Importance must be between 0 and 1"); } const tags = createAIMemoryTags(memory); try { await event(signer, hubId, tags); return JSON.stringify(tags); } catch (error) { // Error adding enhanced memory - silent for MCP compatibility return JSON.stringify(tags); } }, addMemoriesBatch: async (signer, hubId, memories, p) => { try { const results = []; for (const memory of memories) { memory.p = p; // Ensure p is set const result = await aiService().addEnhanced(signer, hubId, memory); results.push(result); } return results; } catch (e) { return [`Failed to add memories batch: ${e}`]; } }, addReasoningChain: async (signer, hubId, reasoning, p) => { try { // Validate inputs if (!isNonEmptyString(reasoning.chainId)) { throw new Error("Chain ID is required"); } if (!reasoning.steps || reasoning.steps.length === 0) { throw new Error("At least one reasoning step is required"); } if (!isNonEmptyString(p)) { throw new Error("P parameter is required"); } const tags = [ { name: "Kind", value: MEMORY_KINDS.REASONING_CHAIN }, { name: "chainId", value: reasoning.chainId }, { name: "steps", value: JSON.stringify(reasoning.steps) }, { name: "outcome", value: reasoning.outcome }, { name: "p", value: p }, ]; await event(signer, hubId, tags); return "Reasoning chain added successfully"; } catch (e) { throw new Error(`Failed to add reasoning chain: ${e}`); } }, createAIMemoryTags: createAIMemoryTags, createMemoryContext: async (signer, hubId, contextName, description, p) => { try { // Validate inputs if (!isNonEmptyString(contextName)) { throw new Error("Context name is required"); } if (!isNonEmptyString(p)) { throw new Error("P parameter is required"); } const tags = [ { name: "Kind", value: MEMORY_KINDS.MEMORY_CONTEXT }, { name: "contextName", value: contextName }, { name: "description", value: description }, { name: "p", value: p }, ]; await event(signer, hubId, tags); return "Memory context created successfully"; } catch (e) { throw new Error(`Failed to create memory context: ${e}`); } }, detectCircularReferences: async (hubId) => { try { const filter = { kinds: [MEMORY_KINDS.AI_MEMORY], limit: 1000, tags: { ai_type: ["link"] }, }; const _filters = JSON.stringify([filter]); const events = await fetchEvents(hubId, _filters); const links = new Map(); const visited = new Set(); const cycles = []; // Build adjacency list from memory links events.forEach((event) => { const fromId = event.from_memory_id; const toId = event.to_memory_id; if (fromId && toId) { if (!links.has(fromId)) links.set(fromId, new Set()); links.get(fromId).add(toId); } }); // DFS to detect cycles const dfs = (nodeId, path) => { if (path.has(nodeId)) { cycles.push(Array.from(path).join(" -> ") + " -> " + nodeId); return; } if (visited.has(nodeId)) return; visited.add(nodeId); path.add(nodeId); const neighbors = links.get(nodeId) || new Set(); neighbors.forEach((neighbor) => dfs(neighbor, path)); path.delete(nodeId); }; Array.from(links.keys()).forEach((nodeId) => { if (!visited.has(nodeId)) { dfs(nodeId, new Set()); } }); return cycles; } catch (error) { // Error detecting circular references - silent for MCP compatibility return []; } }, eventToAIMemory: eventToAIMemory, findShortestPath: async (hubId, fromId, toId) => { try { const filter = { kinds: [MEMORY_KINDS.AI_MEMORY], limit: 1000, tags: { ai_type: ["link"] }, }; const _filters = JSON.stringify([filter]); const events = await fetchEvents(hubId, _filters); const graph = new Map(); // Build adjacency list events.forEach((event) => { const from = event.from_memory_id; const to = event.to_memory_id; if (from && to) { if (!graph.has(from)) graph.set(from, []); graph.get(from).push(to); } }); // BFS to find shortest path const queue = [ { id: fromId, path: [fromId] }, ]; const visited = new Set(); while (queue.length > 0) { const { id, path } = queue.shift(); if (id === toId) { return path; } if (visited.has(id)) continue; visited.add(id); const neighbors = graph.get(id) || []; neighbors.forEach((neighbor) => { if (!visited.has(neighbor)) { queue.push({ id: neighbor, path: [...path, neighbor] }); } }); } return []; // No path found } catch (error) { // Error finding shortest path - silent for MCP compatibility return []; } }, getContextMemories: async (hubId, contextId) => { try { const filter = { kinds: [MEMORY_KINDS.AI_MEMORY], tags: { ai_context_id: [contextId] }, }; const _filters = JSON.stringify([filter]); const events = await fetchEvents(hubId, _filters); return events .filter((event) => typeof event === "object" && event !== null && "Content" in event) .map((event) => eventToAIMemory(event)); } catch { return []; } }, getMemoryAnalytics: async (hubId, p) => { try { const filter = { kinds: [MEMORY_KINDS.AI_MEMORY], }; if (p) { filter.tags = { p: [p] }; } const _filters = JSON.stringify([filter]); const events = await fetchEvents(hubId, _filters); const aiMemories = events .filter((event) => typeof event === "object" && event !== null && "Content" in event) .map((event) => eventToAIMemory(event)); return generateAnalytics(aiMemories); } catch { // Return default analytics on error return { accessPatterns: { mostAccessed: [], recentlyAccessed: [], unusedMemories: [], }, importanceDistribution: { high: 0, low: 0, medium: 0, }, memoryTypeDistribution: { context: 0, conversation: 0, enhancement: 0, knowledge: 0, performance: 0, procedure: 0, reasoning: 0, workflow: 0, }, totalMemories: 0, }; } }, getMemoryRelationships: async (hubId, memoryId) => { try { const filterParams = { kinds: [MEMORY_KINDS.AI_MEMORY], limit: 500, tags: { ai_type: ["link"] }, }; if (memoryId) { filterParams.tags.from_memory_id = [memoryId]; } const result = await fetchEventsVIP01(hubId, filterParams); if (!result || !result.events) { return []; } return result.events.map((event) => { const eventRecord = event; return { strength: parseFloat(eventRecord.link_strength || "0.5"), targetId: eventRecord.to_memory_id || "", type: (eventRecord.link_type || "references"), }; }); } catch (error) { // Error getting memory relationships - silent for MCP compatibility return []; } }, getReasoningChain: async (hubId, chainId) => { try { const filter = { kinds: [MEMORY_KINDS.REASONING_CHAIN], limit: 1, // Only need one reasoning chain tags: { chainId: [chainId] }, }; const result = await fetchEventsVIP01(hubId, filter); if (!result || !result.events || result.events.length === 0) return null; const event = result.events[0]; return { chainId: event.chainId, outcome: event.outcome || "", steps: JSON.parse(event.steps || "[]"), }; } catch { return null; } }, getRelationshipAnalytics: async (hubId) => { try { const links = await aiMemoryService.getMemoryRelationships(hubId); const totalLinks = links.length; const averageStrength = totalLinks > 0 ? links.reduce((sum, link) => sum + link.strength, 0) / totalLinks : 0; // Count relationship types const typeCount = new Map(); links.forEach((link) => { typeCount.set(link.type, (typeCount.get(link.type) || 0) + 1); }); const topRelationshipTypes = Array.from(typeCount.entries()) .map(([type, count]) => ({ count, type })) .sort((a, b) => b.count - a.count) .slice(0, 5); // Get events for connection analysis const eventsFilter = { kinds: [MEMORY_KINDS.AI_MEMORY], limit: 500, tags: { ai_type: ["link"] }, }; const _eventsFilters = JSON.stringify([eventsFilter]); const linkEvents = await fetchEvents(hubId, _eventsFilters); const strongestConnections = linkEvents .sort((a, b) => parseFloat(b.link_strength || "0") - parseFloat(a.link_strength || "0")) .slice(0, 10) .map((event) => ({ from: event.from_memory_id || "", strength: parseFloat(event.link_strength || "0"), to: event.to_memory_id || "", })); return { averageStrength, strongestConnections, topRelationshipTypes, totalLinks, }; } catch (error) { // Error getting relationship analytics - silent for MCP compatibility return { averageStrength: 0, strongestConnections: [], topRelationshipTypes: [], totalLinks: 0, }; } }, linkMemories: async (signer, hubId, sourceId, targetId, relationship) => { try { // Validate inputs if (!isNonEmptyString(sourceId)) { throw new Error("Source ID is required"); } if (!isNonEmptyString(targetId)) { throw new Error("Target ID is required"); } if (sourceId === targetId) { throw new Error("Self-referential relationships are not allowed"); } if (!isValidStrength(relationship.strength)) { throw new Error("Relationship strength must be between 0 and 1"); } const tags = [ { name: "Kind", value: MEMORY_KINDS.MEMORY_RELATIONSHIP }, { name: "sourceId", value: sourceId }, { name: "targetId", value: targetId }, { name: "relationshipType", value: relationship.type }, { name: "strength", value: relationship.strength.toString() }, ]; await event(signer, hubId, tags); return "Memory link created successfully"; } catch (e) { throw new Error(`Failed to link memories: ${e}`); } }, searchAdvanced: async (hubId, query, filters) => { try { // Build filter const filterParams = { kinds: [MEMORY_KINDS.AI_MEMORY], limit: 100, }; if (query) { filterParams.search = query; } // Build tags object for AI-specific filtering const tags = {}; if (filters?.memoryType) { tags.ai_type = [filters.memoryType]; } if (filters?.importanceThreshold) { // Note: This would require hub-side filtering support tags.ai_importance_min = [filters.importanceThreshold.toString()]; } if (filters?.sessionId) { tags.ai_session = [filters.sessionId]; } if (filters?.domain) { tags.ai_domain = [filters.domain]; } if (Object.keys(tags).length > 0) { filterParams.tags = tags; } // Add time range filtering if provided if (filters?.timeRange) { if (filters.timeRange.start) { filterParams.since = new Date(filters.timeRange.start).getTime(); } if (filters.timeRange.end) { filterParams.until = new Date(filters.timeRange.end).getTime(); } } const result = await fetchEventsVIP01(hubId, filterParams); if (!result || !result.events) { return []; } const aiMemories = result.events .filter((event) => typeof event === "object" && event !== null && "Content" in event) .map((event) => eventToAIMemory(event)) .filter((memory) => matchesFilters(memory, filters)); return rankMemoriesByRelevance(aiMemories); } catch (error) { throw new Error(`Failed to search memories: ${error}`); } }, }; }; // Helper functions function createAIMemoryTags(memory) { const tags = [ { name: "Kind", value: MEMORY_KINDS.AI_MEMORY }, { name: "Content", value: memory.content || "" }, { name: "p", value: memory.p || "" }, { name: "role", value: memory.role || "user" }, ]; // Add AI-specific tags if (memory.importance !== undefined) { tags.push({ name: "ai_importance", value: memory.importance.toString() }); } if (memory.memoryType) { tags.push({ name: "ai_type", value: memory.memoryType }); } if (memory.context) { tags.push({ name: "ai_context", value: JSON.stringify(memory.context) }); if (memory.context.sessionId) { tags.push({ name: "ai_session", value: memory.context.sessionId }); } if (memory.context.topic) { tags.push({ name: "ai_topic", value: memory.context.topic }); } if (memory.context.domain) { tags.push({ name: "ai_domain", value: memory.context.domain }); } } if (memory.metadata?.tags) { memory.metadata.tags.forEach((tag) => { tags.push({ name: "ai_tag", value: tag }); }); } // Add workflow-specific tags if this is a workflow memory const workflowMemory = memory; // Type assertion for workflow properties if (workflowMemory.workflowId) { tags.push({ name: "workflow_id", value: workflowMemory.workflowId, }); } if (workflowMemory.workflowVersion) { tags.push({ name: "workflow_version", value: workflowMemory.workflowVersion, }); } if (workflowMemory.stage) { tags.push({ name: "workflow_stage", value: workflowMemory.stage, }); } if (workflowMemory.performance) { tags.push({ name: "workflow_performance", value: JSON.stringify(workflowMemory.performance), }); } if (workflowMemory.enhancement) { tags.push({ name: "workflow_enhancement", value: JSON.stringify(workflowMemory.enhancement), }); } if (workflowMemory.dependencies && Array.isArray(workflowMemory.dependencies)) { workflowMemory.dependencies.forEach((dep) => { tags.push({ name: "workflow_dependency", value: dep }); }); } if (workflowMemory.capabilities && Array.isArray(workflowMemory.capabilities)) { workflowMemory.capabilities.forEach((cap) => { tags.push({ name: "workflow_capability", value: cap }); }); } if (workflowMemory.requirements && Array.isArray(workflowMemory.requirements)) { workflowMemory.requirements.forEach((req) => { tags.push({ name: "workflow_requirement", value: req }); }); } return tags; } function eventToAIMemory(event) { const baseMemory = { content: event.Content, id: event.Id, p: event.p, role: event.r || event.role || "user", timestamp: event.Timestamp, }; // Parse AI-specific fields with defaults const importance = parseFloat(event.ai_importance || "0.5"); const memoryType = event.ai_type || "conversation"; const context = event.ai_context ? (() => { try { return JSON.parse(event.ai_context); } catch { return {}; } })() : {}; // Add domain from event tags if available if (event.ai_domain) { context.domain = event.ai_domain; } const aiMemory = { ...baseMemory, context, importance, memoryType, metadata: { accessCount: 0, lastAccessed: new Date().toISOString(), tags: event.ai_tag ? Array.isArray(event.ai_tag) ? event.ai_tag : [event.ai_tag] : [], }, }; // Add workflow-specific properties if present const workflowMemory = aiMemory; if (event.workflow_id) { workflowMemory.workflowId = event.workflow_id; } if (event.workflow_version) { workflowMemory.workflowVersion = event.workflow_version; } if (event.workflow_stage) { workflowMemory.stage = event.workflow_stage; } if (event.workflow_performance) { try { workflowMemory.performance = JSON.parse(event.workflow_performance); } catch { // Ignore invalid JSON } } if (event.workflow_enhancement) { try { workflowMemory.enhancement = JSON.parse(event.workflow_enhancement); } catch { // Ignore invalid JSON } } // Handle arrays if (event.workflow_dependency) { const deps = Array.isArray(event.workflow_dependency) ? event.workflow_dependency : [event.workflow_dependency]; workflowMemory.dependencies = deps; } if (event.workflow_capability) { const caps = Array.isArray(event.workflow_capability) ? event.workflow_capability : [event.workflow_capability]; workflowMemory.capabilities = caps; } if (event.workflow_requirement) { const reqs = Array.isArray(event.workflow_requirement) ? event.workflow_requirement : [event.workflow_requirement]; workflowMemory.requirements = reqs; } return aiMemory; } function generateAnalytics(memories) { const memoryTypeDistribution = memories.reduce((acc, memory) => { acc[memory.memoryType] = (acc[memory.memoryType] || 0) + 1; return acc; }, {}); // Ensure all types are represented const typeDistribution = { context: memoryTypeDistribution.context || 0, conversation: memoryTypeDistribution.conversation || 0, enhancement: memoryTypeDistribution.enhancement || 0, knowledge: memoryTypeDistribution.knowledge || 0, performance: memoryTypeDistribution.performance || 0, procedure: memoryTypeDistribution.procedure || 0, reasoning: memoryTypeDistribution.reasoning || 0, workflow: memoryTypeDistribution.workflow || 0, }; const importanceDistribution = memories.reduce((acc, memory) => { if (memory.importance >= 0.7) acc.high++; else if (memory.importance >= 0.3) acc.medium++; else acc.low++; return acc; }, { high: 0, low: 0, medium: 0 }); // Sort by access count and recency for access patterns const sortedByAccess = [...memories].sort((a, b) => b.metadata.accessCount - a.metadata.accessCount); const sortedByRecency = [...memories].sort((a, b) => new Date(b.metadata.lastAccessed).getTime() - new Date(a.metadata.lastAccessed).getTime()); return { accessPatterns: { mostAccessed: sortedByAccess.slice(0, 10).map((m) => m.id), recentlyAccessed: sortedByRecency.slice(0, 10).map((m) => m.id), unusedMemories: memories .filter((m) => m.metadata.accessCount === 0) .map((m) => m.id), }, importanceDistribution, memoryTypeDistribution: typeDistribution, totalMemories: memories.length, }; } function matchesFilters(memory, filters) { if (!filters) return true; if (filters.memoryType && memory.memoryType !== filters.memoryType) { return false; } if (filters.importanceThreshold && memory.importance < filters.importanceThreshold) { return false; } if (filters.domain && memory.context.domain !== filters.domain) { return false; } if (filters.sessionId && memory.context.sessionId !== filters.sessionId) { return false; } if (filters.timeRange) { const memoryTime = new Date(memory.timestamp); const start = new Date(filters.timeRange.start); const end = new Date(filters.timeRange.end); if (memoryTime < start || memoryTime > end) { return false; } } return true; } function rankMemoriesByRelevance(memories) { return memories.sort((a, b) => { // Primary sort: importance score if (a.importance !== b.importance) { return b.importance - a.importance; } // Secondary sort: recency return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); }); } export const aiMemoryService = aiService(); // Export memory kinds for use in server export { MEMORY_KINDS };