Files
t6_mem0_v2/api/memory_service.py
Claude Code 1998bef6f4 Add MCP HTTP/SSE server and complete n8n integration
Major Changes:
- Implemented MCP HTTP/SSE transport server for n8n and web clients
- Created mcp_server/http_server.py with FastAPI for JSON-RPC 2.0 over HTTP
- Added health check endpoint (/health) for container monitoring
- Refactored mcp-server/ to mcp_server/ (Python module structure)
- Updated Dockerfile.mcp to run HTTP server with health checks

MCP Server Features:
- 7 memory tools exposed via MCP (add, search, get, update, delete)
- HTTP/SSE transport on port 8765 for n8n integration
- stdio transport for Claude Code integration
- JSON-RPC 2.0 protocol implementation
- CORS support for web clients

n8n Integration:
- Successfully tested with AI Agent workflows
- MCP Client Tool configuration documented
- Working webhook endpoint tested and verified
- System prompt optimized for automatic user_id usage

Documentation:
- Created comprehensive Mintlify documentation site
- Added docs/mcp/introduction.mdx - MCP server overview
- Added docs/mcp/installation.mdx - Installation guide
- Added docs/mcp/tools.mdx - Complete tool reference
- Added docs/examples/n8n.mdx - n8n integration guide
- Added docs/examples/claude-code.mdx - Claude Code setup
- Updated README.md with MCP HTTP server info
- Updated roadmap to mark Phase 1 as complete

Bug Fixes:
- Fixed synchronized delete operations across Supabase and Neo4j
- Updated memory_service.py with proper error handling
- Fixed Neo4j connection issues in delete operations

Configuration:
- Added MCP_HOST and MCP_PORT environment variables
- Updated .env.example with MCP server configuration
- Updated docker-compose.yml with MCP container health checks

Testing:
- Added test scripts for MCP HTTP endpoint verification
- Created test workflows in n8n
- Verified all 7 memory tools working correctly
- Tested synchronized operations across both stores

Version: 1.0.0
Status: Phase 1 Complete - Production Ready

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 13:56:41 +02:00

411 lines
12 KiB
Python

"""
Memory service wrapper for Mem0 core library
"""
import logging
from typing import List, Dict, Any, Optional
from mem0 import Memory
from config import mem0_config
from memory_cleanup import MemoryCleanup
logger = logging.getLogger(__name__)
class MemoryService:
"""Singleton service for memory operations using Mem0"""
_instance: Optional['MemoryService'] = None
_memory: Optional[Memory] = None
_cleanup: Optional[MemoryCleanup] = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
"""Initialize memory service with Mem0"""
if self._memory is None:
logger.info("Initializing Mem0 with configuration")
try:
self._memory = Memory.from_config(config_dict=mem0_config)
self._cleanup = MemoryCleanup(self._memory)
logger.info("Mem0 initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Mem0: {e}")
raise
@property
def memory(self) -> Memory:
"""Get Mem0 instance"""
if self._memory is None:
raise RuntimeError("MemoryService not initialized")
return self._memory
async def add_memory(
self,
messages: List[Dict[str, str]],
user_id: Optional[str] = None,
agent_id: Optional[str] = None,
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
"""
Add new memory from messages
Args:
messages: List of chat messages
user_id: User identifier
agent_id: Agent identifier
run_id: Run identifier
metadata: Additional metadata
Returns:
List of created memories
Raises:
Exception: If memory creation fails
"""
try:
logger.info(f"Adding memory for user_id={user_id}, agent_id={agent_id}")
result = self.memory.add(
messages=messages,
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
metadata=metadata or {}
)
logger.info(f"Successfully added {len(result.get('results', []))} memories")
return result.get('results', [])
except Exception as e:
logger.error(f"Failed to add memory: {e}")
raise
async def search_memories(
self,
query: str,
user_id: Optional[str] = None,
agent_id: Optional[str] = None,
run_id: Optional[str] = None,
limit: int = 10
) -> List[Dict[str, Any]]:
"""
Search memories by query
Args:
query: Search query
user_id: User identifier filter
agent_id: Agent identifier filter
run_id: Run identifier filter
limit: Maximum results
Returns:
List of matching memories with scores
Raises:
Exception: If search fails
"""
try:
logger.info(f"Searching memories: query='{query}', user_id={user_id}, limit={limit}")
result = self.memory.search(
query=query,
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
limit=limit
)
# In mem0 v0.1.118+, search returns dict with 'results' key
memories_list = result.get('results', []) if isinstance(result, dict) else result
# Handle both string and dict responses from mem0
formatted_results = []
for item in memories_list:
if isinstance(item, str):
# Convert string memory to dict format
formatted_results.append({
'id': '',
'memory': item,
'user_id': user_id,
'agent_id': agent_id,
'run_id': run_id,
'metadata': {},
'created_at': None,
'updated_at': None,
'score': None
})
elif isinstance(item, dict):
# Already in dict format
formatted_results.append(item)
else:
logger.warning(f"Unexpected memory format: {type(item)}")
logger.info(f"Found {len(formatted_results)} matching memories")
return formatted_results
except Exception as e:
logger.error(f"Failed to search memories: {e}")
raise
async def get_memory(
self,
memory_id: str
) -> Optional[Dict[str, Any]]:
"""
Get specific memory by ID
Args:
memory_id: Memory identifier
Returns:
Memory data or None if not found
Raises:
Exception: If retrieval fails
"""
try:
logger.info(f"Getting memory: id={memory_id}")
result = self.memory.get(memory_id=memory_id)
if result:
logger.info(f"Retrieved memory: {memory_id}")
else:
logger.warning(f"Memory not found: {memory_id}")
return result
except Exception as e:
logger.error(f"Failed to get memory: {e}")
raise
async def get_all_memories(
self,
user_id: Optional[str] = None,
agent_id: Optional[str] = None,
run_id: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
Get all memories for a user/agent/run
Args:
user_id: User identifier filter
agent_id: Agent identifier filter
run_id: Run identifier filter
Returns:
List of all matching memories
Raises:
Exception: If retrieval fails
"""
try:
logger.info(f"Getting all memories: user_id={user_id}, agent_id={agent_id}")
result = self.memory.get_all(
user_id=user_id,
agent_id=agent_id,
run_id=run_id
)
# In mem0 v0.1.118+, get_all returns dict with 'results' key
memories_list = result.get('results', []) if isinstance(result, dict) else result
# Handle both string and dict responses from mem0
formatted_results = []
for item in memories_list:
if isinstance(item, str):
# Convert string memory to dict format
formatted_results.append({
'id': '',
'memory': item,
'user_id': user_id,
'agent_id': agent_id,
'run_id': run_id,
'metadata': {},
'created_at': None,
'updated_at': None,
'score': None
})
elif isinstance(item, dict):
# Already in dict format
formatted_results.append(item)
else:
logger.warning(f"Unexpected memory format: {type(item)}")
logger.info(f"Retrieved {len(formatted_results)} memories")
return formatted_results
except Exception as e:
logger.error(f"Failed to get all memories: {e}")
raise
async def update_memory(
self,
memory_id: str,
data: str
) -> Dict[str, Any]:
"""
Update existing memory
Args:
memory_id: Memory identifier
data: Updated memory text
Returns:
Updated memory data
Raises:
Exception: If update fails
"""
try:
logger.info(f"Updating memory: id={memory_id}")
result = self.memory.update(
memory_id=memory_id,
data=data
)
logger.info(f"Successfully updated memory: {memory_id}")
return result
except Exception as e:
logger.error(f"Failed to update memory: {e}")
raise
async def delete_memory(
self,
memory_id: str
) -> bool:
"""
Delete memory by ID
Args:
memory_id: Memory identifier
Returns:
True if deleted successfully
Raises:
Exception: If deletion fails
"""
try:
logger.info(f"Deleting memory: id={memory_id}")
self.memory.delete(memory_id=memory_id)
logger.info(f"Successfully deleted memory: {memory_id}")
return True
except Exception as e:
logger.error(f"Failed to delete memory: {e}")
raise
async def delete_all_memories(
self,
user_id: Optional[str] = None,
agent_id: Optional[str] = None,
run_id: Optional[str] = None
) -> bool:
"""
Delete all memories for a user/agent/run
IMPORTANT: This uses synchronized deletion to ensure both
Supabase (vector store) and Neo4j (graph store) are cleaned up.
Args:
user_id: User identifier filter
agent_id: Agent identifier filter
run_id: Run identifier filter
Returns:
True if deleted successfully
Raises:
Exception: If deletion fails
"""
try:
logger.info(f"Deleting all memories (synchronized): user_id={user_id}, agent_id={agent_id}")
# Use synchronized deletion to clean up both Supabase and Neo4j
result = self._cleanup.delete_all_synchronized(
user_id=user_id,
agent_id=agent_id,
run_id=run_id
)
logger.info(f"Successfully deleted all matching memories: {result}")
return True
except Exception as e:
logger.error(f"Failed to delete all memories: {e}")
raise
async def get_memory_history(
self,
memory_id: str
) -> List[Dict[str, Any]]:
"""
Get history of a memory
Args:
memory_id: Memory identifier
Returns:
List of memory history entries
Raises:
Exception: If retrieval fails
"""
try:
logger.info(f"Getting memory history: id={memory_id}")
result = self.memory.history(memory_id=memory_id)
logger.info(f"Retrieved history for memory: {memory_id}")
return result
except Exception as e:
logger.error(f"Failed to get memory history: {e}")
raise
async def health_check(self) -> Dict[str, str]:
"""
Check health of memory service
Returns:
Dict with component health status
"""
health = {}
try:
# Test Mem0 access
self.memory
health['mem0'] = 'healthy'
except Exception as e:
logger.error(f"Mem0 health check failed: {e}")
health['mem0'] = 'unhealthy'
return health
# Global service instance
_memory_service: Optional[MemoryService] = None
def get_memory_service() -> MemoryService:
"""
Get or create memory service singleton
Returns:
MemoryService instance
"""
global _memory_service
if _memory_service is None:
_memory_service = MemoryService()
return _memory_service