Complete implementation: REST API, MCP server, and documentation

Implementation Summary:
- REST API with FastAPI (complete CRUD operations)
- MCP Server with Python MCP SDK (7 tools)
- Supabase migrations (pgvector setup)
- Docker Compose orchestration
- Mintlify documentation site
- Environment configuration
- Shared config module

REST API Features:
- POST /v1/memories/ - Add memory
- GET /v1/memories/search - Semantic search
- GET /v1/memories/{id} - Get memory
- GET /v1/memories/user/{user_id} - User memories
- PATCH /v1/memories/{id} - Update memory
- DELETE /v1/memories/{id} - Delete memory
- GET /v1/health - Health check
- GET /v1/stats - Statistics
- Bearer token authentication
- OpenAPI documentation

MCP Server Tools:
- add_memory - Add from messages
- search_memories - Semantic search
- get_memory - Retrieve by ID
- get_all_memories - List all
- update_memory - Update content
- delete_memory - Delete by ID
- delete_all_memories - Bulk delete

Infrastructure:
- Neo4j 5.26 with APOC/GDS
- Supabase pgvector integration
- Docker network: localai
- Health checks and monitoring
- Structured logging

Documentation:
- Introduction page
- Quickstart guide
- Architecture deep dive
- Mintlify configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Code
2025-10-14 08:44:16 +02:00
parent cfa7abd23d
commit 61a4050a8e
26 changed files with 3248 additions and 0 deletions

34
.env.example Normal file
View File

@@ -0,0 +1,34 @@
# OpenAI Configuration
OPENAI_API_KEY=sk-your-openai-api-key-here
# Supabase Configuration
SUPABASE_CONNECTION_STRING=postgresql://supabase_admin:your-password@172.21.0.12:5432/postgres
# Neo4j Configuration
NEO4J_URI=neo4j://neo4j:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your-neo4j-password
# API Configuration
API_HOST=0.0.0.0
API_PORT=8080
API_KEY=your-secure-api-key-here
# MCP Server Configuration
MCP_HOST=0.0.0.0
MCP_PORT=8765
# Mem0 Configuration
MEM0_COLLECTION_NAME=t6_memories
MEM0_EMBEDDING_DIMS=1536
MEM0_VERSION=v1.1
# Docker Network
DOCKER_NETWORK=localai
# Logging
LOG_LEVEL=INFO
LOG_FORMAT=json
# Environment
ENVIRONMENT=development

0
api/__init__.py Normal file
View File

66
api/auth.py Normal file
View File

@@ -0,0 +1,66 @@
"""
Authentication middleware for T6 Mem0 v2 REST API
"""
from fastapi import HTTPException, Security, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from config import settings
# Security scheme
security = HTTPBearer()
async def verify_api_key(
credentials: HTTPAuthorizationCredentials = Security(security)
) -> str:
"""
Verify API key from Authorization header
Args:
credentials: HTTP Bearer credentials
Returns:
The verified API key
Raises:
HTTPException: If authentication fails
"""
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token = credentials.credentials
# Verify token matches configured API key
if token != settings.api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired API key",
headers={"WWW-Authenticate": "Bearer"},
)
return token
async def optional_api_key(
credentials: HTTPAuthorizationCredentials = Security(security)
) -> str | None:
"""
Optional API key verification (for public endpoints)
Args:
credentials: HTTP Bearer credentials
Returns:
The API key if provided, None otherwise
"""
if not credentials:
return None
try:
return await verify_api_key(credentials)
except HTTPException:
return None

158
api/main.py Normal file
View File

@@ -0,0 +1,158 @@
"""
T6 Mem0 v2 REST API
Main FastAPI application
"""
import logging
import sys
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from api.routes import router
from api.memory_service import get_memory_service
from config import settings
# Configure logging
logging.basicConfig(
level=getattr(logging, settings.log_level.upper()),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Lifespan context manager for startup and shutdown events
"""
# Startup
logger.info("Starting T6 Mem0 v2 REST API")
logger.info(f"Environment: {settings.environment}")
logger.info(f"API Host: {settings.api_host}:{settings.api_port}")
try:
# Initialize memory service
service = get_memory_service()
logger.info("Memory service initialized successfully")
yield
except Exception as e:
logger.error(f"Failed to initialize application: {e}")
raise
finally:
# Shutdown
logger.info("Shutting down T6 Mem0 v2 REST API")
# Create FastAPI application
app = FastAPI(
title="T6 Mem0 v2 API",
description="""
Memory system for LLM applications based on mem0.ai
## Features
- **Add Memory**: Store conversation memories with semantic understanding
- **Search Memory**: Find relevant memories using semantic search
- **Manage Memory**: Update and delete memories
- **Multi-Agent**: Support for user, agent, and run-specific memories
- **Graph Relationships**: Neo4j integration for memory relationships
## Authentication
All endpoints (except /health) require Bearer token authentication.
Include your API key in the Authorization header:
```
Authorization: Bearer YOUR_API_KEY
```
""",
version="0.1.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
lifespan=lifespan
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:8080",
"http://localhost:5678", # n8n
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Exception handlers
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""Handle validation errors"""
logger.warning(f"Validation error: {exc.errors()}")
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"status": "error",
"error": "Validation failed",
"detail": exc.errors()
}
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""Handle general exceptions"""
logger.error(f"Unhandled exception: {exc}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"status": "error",
"error": "Internal server error",
"detail": str(exc) if settings.environment == "development" else "An error occurred"
}
)
# Include routers
app.include_router(router)
# Root endpoint
@app.get(
"/",
summary="API Root",
description="Get API information",
tags=["info"]
)
async def root():
"""API root endpoint"""
return {
"name": "T6 Mem0 v2 API",
"version": "0.1.0",
"status": "running",
"docs": "/docs",
"health": "/v1/health"
}
# Run with uvicorn
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"api.main:app",
host=settings.api_host,
port=settings.api_port,
reload=settings.environment == "development",
log_level=settings.log_level.lower()
)

353
api/memory_service.py Normal file
View File

@@ -0,0 +1,353 @@
"""
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
logger = logging.getLogger(__name__)
class MemoryService:
"""Singleton service for memory operations using Mem0"""
_instance: Optional['MemoryService'] = None
_memory: Optional[Memory] = 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)
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}")
results = self.memory.search(
query=query,
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
limit=limit
)
logger.info(f"Found {len(results)} matching memories")
return 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}")
results = self.memory.get_all(
user_id=user_id,
agent_id=agent_id,
run_id=run_id
)
logger.info(f"Retrieved {len(results)} memories")
return 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
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: user_id={user_id}, agent_id={agent_id}")
self.memory.delete_all(
user_id=user_id,
agent_id=agent_id,
run_id=run_id
)
logger.info("Successfully deleted all matching memories")
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

227
api/models.py Normal file
View File

@@ -0,0 +1,227 @@
"""
Pydantic models for T6 Mem0 v2 REST API
"""
from typing import Optional, List, Dict, Any
from datetime import datetime
from pydantic import BaseModel, Field, ConfigDict
from uuid import UUID
class Message(BaseModel):
"""Chat message for memory extraction"""
role: str = Field(..., description="Message role (user, assistant, system)")
content: str = Field(..., description="Message content")
model_config = ConfigDict(
json_schema_extra={
"example": {
"role": "user",
"content": "I love pizza with extra cheese"
}
}
)
class AddMemoryRequest(BaseModel):
"""Request to add new memory"""
messages: List[Message] = Field(..., description="Conversation messages")
user_id: Optional[str] = Field(None, description="User identifier")
agent_id: Optional[str] = Field(None, description="Agent identifier")
run_id: Optional[str] = Field(None, description="Run identifier")
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional metadata")
model_config = ConfigDict(
json_schema_extra={
"example": {
"messages": [
{"role": "user", "content": "I love pizza"},
{"role": "assistant", "content": "Great! What toppings?"},
{"role": "user", "content": "Extra cheese please"}
],
"user_id": "alice",
"metadata": {"session": "chat_123", "source": "web"}
}
}
)
class SearchMemoryRequest(BaseModel):
"""Request to search memories"""
query: str = Field(..., description="Search query")
user_id: Optional[str] = Field(None, description="User identifier filter")
agent_id: Optional[str] = Field(None, description="Agent identifier filter")
run_id: Optional[str] = Field(None, description="Run identifier filter")
limit: int = Field(default=10, ge=1, le=100, description="Maximum results")
model_config = ConfigDict(
json_schema_extra={
"example": {
"query": "What food does the user like?",
"user_id": "alice",
"limit": 5
}
}
)
class UpdateMemoryRequest(BaseModel):
"""Request to update existing memory"""
memory_text: Optional[str] = Field(None, description="Updated memory text")
metadata: Optional[Dict[str, Any]] = Field(None, description="Updated metadata")
model_config = ConfigDict(
json_schema_extra={
"example": {
"memory_text": "User loves pizza with mushrooms and olives",
"metadata": {"verified": True, "updated_by": "admin"}
}
}
)
class MemoryResponse(BaseModel):
"""Memory response model"""
id: str = Field(..., description="Memory unique identifier")
memory: str = Field(..., description="Memory text content")
user_id: Optional[str] = Field(None, description="User identifier")
agent_id: Optional[str] = Field(None, description="Agent identifier")
run_id: Optional[str] = Field(None, description="Run identifier")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Memory metadata")
created_at: Optional[datetime] = Field(None, description="Creation timestamp")
updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
score: Optional[float] = Field(None, description="Relevance score (for search results)")
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "mem_abc123",
"memory": "User loves pizza with extra cheese",
"user_id": "alice",
"metadata": {"session": "chat_123"},
"created_at": "2025-10-13T12:00:00Z",
"score": 0.95
}
}
)
class AddMemoryResponse(BaseModel):
"""Response from adding memory"""
status: str = Field(..., description="Operation status")
memories: List[MemoryResponse] = Field(..., description="Created memories")
message: str = Field(..., description="Result message")
model_config = ConfigDict(
json_schema_extra={
"example": {
"status": "success",
"memories": [{
"id": "mem_abc123",
"memory": "User loves pizza with extra cheese",
"user_id": "alice"
}],
"message": "Successfully added 1 memory"
}
}
)
class SearchMemoryResponse(BaseModel):
"""Response from searching memories"""
status: str = Field(..., description="Operation status")
memories: List[MemoryResponse] = Field(..., description="Matching memories")
count: int = Field(..., description="Number of results")
model_config = ConfigDict(
json_schema_extra={
"example": {
"status": "success",
"memories": [{
"id": "mem_abc123",
"memory": "User loves pizza",
"user_id": "alice",
"score": 0.95
}],
"count": 1
}
}
)
class DeleteMemoryResponse(BaseModel):
"""Response from deleting memory"""
status: str = Field(..., description="Operation status")
message: str = Field(..., description="Result message")
model_config = ConfigDict(
json_schema_extra={
"example": {
"status": "success",
"message": "Memory deleted successfully"
}
}
)
class HealthResponse(BaseModel):
"""Health check response"""
status: str = Field(..., description="Service status")
version: str = Field(..., description="API version")
timestamp: datetime = Field(..., description="Check timestamp")
dependencies: Dict[str, str] = Field(..., description="Dependency status")
model_config = ConfigDict(
json_schema_extra={
"example": {
"status": "healthy",
"version": "0.1.0",
"timestamp": "2025-10-13T12:00:00Z",
"dependencies": {
"supabase": "connected",
"neo4j": "connected",
"openai": "available"
}
}
}
)
class StatsResponse(BaseModel):
"""Memory statistics response"""
total_memories: int = Field(..., description="Total memory count")
total_users: int = Field(..., description="Unique user count")
total_agents: int = Field(..., description="Unique agent count")
avg_memories_per_user: float = Field(..., description="Average memories per user")
oldest_memory: Optional[datetime] = Field(None, description="Oldest memory timestamp")
newest_memory: Optional[datetime] = Field(None, description="Newest memory timestamp")
model_config = ConfigDict(
json_schema_extra={
"example": {
"total_memories": 1523,
"total_users": 42,
"total_agents": 5,
"avg_memories_per_user": 36.26,
"oldest_memory": "2025-01-01T00:00:00Z",
"newest_memory": "2025-10-13T12:00:00Z"
}
}
)
class ErrorResponse(BaseModel):
"""Error response model"""
status: str = Field(default="error", description="Error status")
error: str = Field(..., description="Error message")
detail: Optional[str] = Field(None, description="Detailed error information")
model_config = ConfigDict(
json_schema_extra={
"example": {
"status": "error",
"error": "Authentication failed",
"detail": "Invalid or missing API key"
}
}
)

377
api/routes.py Normal file
View File

@@ -0,0 +1,377 @@
"""
API routes for T6 Mem0 v2 REST API
"""
import logging
from typing import List
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi.responses import JSONResponse
from api.auth import verify_api_key
from api.models import (
AddMemoryRequest,
AddMemoryResponse,
SearchMemoryRequest,
SearchMemoryResponse,
UpdateMemoryRequest,
MemoryResponse,
DeleteMemoryResponse,
HealthResponse,
StatsResponse,
ErrorResponse
)
from api.memory_service import get_memory_service, MemoryService
from config import settings
logger = logging.getLogger(__name__)
# Create router
router = APIRouter(prefix="/v1", tags=["memories"])
def format_memory_response(mem: dict) -> MemoryResponse:
"""Format memory data to response model"""
return MemoryResponse(
id=mem.get('id', ''),
memory=mem.get('memory', mem.get('data', '')),
user_id=mem.get('user_id'),
agent_id=mem.get('agent_id'),
run_id=mem.get('run_id'),
metadata=mem.get('metadata', {}),
created_at=mem.get('created_at'),
updated_at=mem.get('updated_at'),
score=mem.get('score')
)
@router.post(
"/memories/",
response_model=AddMemoryResponse,
status_code=status.HTTP_201_CREATED,
summary="Add new memory",
description="Add new memory from conversation messages",
responses={
201: {"description": "Memory added successfully"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def add_memory(
request: AddMemoryRequest,
_api_key: str = Depends(verify_api_key),
service: MemoryService = Depends(get_memory_service)
):
"""Add new memory from messages"""
try:
messages = [msg.model_dump() for msg in request.messages]
memories = await service.add_memory(
messages=messages,
user_id=request.user_id,
agent_id=request.agent_id,
run_id=request.run_id,
metadata=request.metadata
)
formatted_memories = [format_memory_response(mem) for mem in memories]
return AddMemoryResponse(
status="success",
memories=formatted_memories,
message=f"Successfully added {len(formatted_memories)} memory(ies)"
)
except Exception as e:
logger.error(f"Error adding memory: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.get(
"/memories/search",
response_model=SearchMemoryResponse,
summary="Search memories",
description="Search memories by semantic similarity",
responses={
200: {"description": "Search completed successfully"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def search_memories(
query: str = Query(..., description="Search query"),
user_id: str | None = Query(None, description="Filter by user ID"),
agent_id: str | None = Query(None, description="Filter by agent ID"),
run_id: str | None = Query(None, description="Filter by run ID"),
limit: int = Query(10, ge=1, le=100, description="Maximum results"),
_api_key: str = Depends(verify_api_key),
service: MemoryService = Depends(get_memory_service)
):
"""Search memories by query"""
try:
memories = await service.search_memories(
query=query,
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
limit=limit
)
formatted_memories = [format_memory_response(mem) for mem in memories]
return SearchMemoryResponse(
status="success",
memories=formatted_memories,
count=len(formatted_memories)
)
except Exception as e:
logger.error(f"Error searching memories: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.get(
"/memories/{memory_id}",
response_model=MemoryResponse,
summary="Get specific memory",
description="Retrieve a specific memory by ID",
responses={
200: {"description": "Memory retrieved successfully"},
404: {"model": ErrorResponse, "description": "Memory not found"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def get_memory(
memory_id: str,
_api_key: str = Depends(verify_api_key),
service: MemoryService = Depends(get_memory_service)
):
"""Get specific memory by ID"""
try:
memory = await service.get_memory(memory_id)
if not memory:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Memory not found: {memory_id}"
)
return format_memory_response(memory)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting memory: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.get(
"/memories/user/{user_id}",
response_model=List[MemoryResponse],
summary="Get user memories",
description="Get all memories for a specific user",
responses={
200: {"description": "Memories retrieved successfully"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def get_user_memories(
user_id: str,
_api_key: str = Depends(verify_api_key),
service: MemoryService = Depends(get_memory_service)
):
"""Get all memories for a user"""
try:
memories = await service.get_all_memories(user_id=user_id)
return [format_memory_response(mem) for mem in memories]
except Exception as e:
logger.error(f"Error getting user memories: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.patch(
"/memories/{memory_id}",
response_model=MemoryResponse,
summary="Update memory",
description="Update an existing memory",
responses={
200: {"description": "Memory updated successfully"},
404: {"model": ErrorResponse, "description": "Memory not found"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def update_memory(
memory_id: str,
request: UpdateMemoryRequest,
_api_key: str = Depends(verify_api_key),
service: MemoryService = Depends(get_memory_service)
):
"""Update existing memory"""
try:
if not request.memory_text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="memory_text is required for update"
)
updated = await service.update_memory(
memory_id=memory_id,
data=request.memory_text
)
return format_memory_response(updated)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating memory: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.delete(
"/memories/{memory_id}",
response_model=DeleteMemoryResponse,
summary="Delete memory",
description="Delete a specific memory",
responses={
200: {"description": "Memory deleted successfully"},
404: {"model": ErrorResponse, "description": "Memory not found"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def delete_memory(
memory_id: str,
_api_key: str = Depends(verify_api_key),
service: MemoryService = Depends(get_memory_service)
):
"""Delete specific memory"""
try:
await service.delete_memory(memory_id)
return DeleteMemoryResponse(
status="success",
message=f"Memory {memory_id} deleted successfully"
)
except Exception as e:
logger.error(f"Error deleting memory: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.delete(
"/memories/user/{user_id}",
response_model=DeleteMemoryResponse,
summary="Delete user memories",
description="Delete all memories for a specific user",
responses={
200: {"description": "Memories deleted successfully"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def delete_user_memories(
user_id: str,
_api_key: str = Depends(verify_api_key),
service: MemoryService = Depends(get_memory_service)
):
"""Delete all memories for a user"""
try:
await service.delete_all_memories(user_id=user_id)
return DeleteMemoryResponse(
status="success",
message=f"All memories for user {user_id} deleted successfully"
)
except Exception as e:
logger.error(f"Error deleting user memories: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e)
)
@router.get(
"/health",
response_model=HealthResponse,
summary="Health check",
description="Check API health status",
responses={
200: {"description": "Service is healthy"}
}
)
async def health_check(
service: MemoryService = Depends(get_memory_service)
):
"""Health check endpoint"""
try:
health = await service.health_check()
return HealthResponse(
status="healthy" if all(v == "healthy" for v in health.values()) else "degraded",
version="0.1.0",
timestamp=datetime.utcnow(),
dependencies=health
)
except Exception as e:
logger.error(f"Health check failed: {e}")
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={
"status": "unhealthy",
"version": "0.1.0",
"timestamp": datetime.utcnow().isoformat(),
"error": str(e)
}
)
@router.get(
"/stats",
response_model=StatsResponse,
summary="Memory statistics",
description="Get system-wide memory statistics",
responses={
200: {"description": "Statistics retrieved successfully"},
401: {"model": ErrorResponse, "description": "Unauthorized"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
)
async def get_stats(
_api_key: str = Depends(verify_api_key)
):
"""Get memory statistics"""
# Note: This would require direct database access or extension of mem0 API
# For now, return placeholder
return StatsResponse(
total_memories=0,
total_users=0,
total_agents=0,
avg_memories_per_user=0.0,
oldest_memory=None,
newest_memory=None
)

121
config.py Normal file
View File

@@ -0,0 +1,121 @@
"""
Shared configuration for T6 Mem0 v2
Loads environment variables and creates Mem0 configuration
"""
import os
from typing import Dict, Any
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
"""Application settings loaded from environment variables"""
# OpenAI
openai_api_key: str = Field(..., env="OPENAI_API_KEY")
# Supabase
supabase_connection_string: str = Field(..., env="SUPABASE_CONNECTION_STRING")
# Neo4j
neo4j_uri: str = Field(..., env="NEO4J_URI")
neo4j_user: str = Field(default="neo4j", env="NEO4J_USER")
neo4j_password: str = Field(..., env="NEO4J_PASSWORD")
# API
api_host: str = Field(default="0.0.0.0", env="API_HOST")
api_port: int = Field(default=8080, env="API_PORT")
api_key: str = Field(..., env="API_KEY")
# MCP Server
mcp_host: str = Field(default="0.0.0.0", env="MCP_HOST")
mcp_port: int = Field(default=8765, env="MCP_PORT")
# Mem0
mem0_collection_name: str = Field(default="t6_memories", env="MEM0_COLLECTION_NAME")
mem0_embedding_dims: int = Field(default=1536, env="MEM0_EMBEDDING_DIMS")
mem0_version: str = Field(default="v1.1", env="MEM0_VERSION")
# Logging
log_level: str = Field(default="INFO", env="LOG_LEVEL")
log_format: str = Field(default="json", env="LOG_FORMAT")
# Environment
environment: str = Field(default="development", env="ENVIRONMENT")
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False
def get_settings() -> Settings:
"""Get application settings"""
return Settings()
def get_mem0_config(settings: Settings) -> Dict[str, Any]:
"""
Generate Mem0 configuration from settings
Args:
settings: Application settings
Returns:
Dict containing Mem0 configuration
"""
return {
# Vector Store - Supabase
"vector_store": {
"provider": "supabase",
"config": {
"connection_string": settings.supabase_connection_string,
"collection_name": settings.mem0_collection_name,
"embedding_model_dims": settings.mem0_embedding_dims,
"index_method": "hnsw", # Fastest search
"index_measure": "cosine_distance" # Best for embeddings
}
},
# Graph Store - Neo4j
"graph_store": {
"provider": "neo4j",
"config": {
"url": settings.neo4j_uri,
"username": settings.neo4j_user,
"password": settings.neo4j_password
}
},
# LLM Provider - OpenAI
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4o-mini",
"temperature": 0.1,
"max_tokens": 2000,
"api_key": settings.openai_api_key
}
},
# Embedder - OpenAI
"embedder": {
"provider": "openai",
"config": {
"model": "text-embedding-3-small",
"embedding_dims": settings.mem0_embedding_dims,
"api_key": settings.openai_api_key
}
},
# Version
"version": settings.mem0_version
}
# Global settings instance
settings = get_settings()
# Global mem0 config
mem0_config = get_mem0_config(settings)

111
docker-compose.yml Normal file
View File

@@ -0,0 +1,111 @@
version: '3.8'
services:
# Neo4j Graph Database
neo4j:
image: neo4j:5.26-community
container_name: t6-mem0-neo4j
restart: unless-stopped
ports:
- "7474:7474" # HTTP Browser UI
- "7687:7687" # Bolt Protocol
environment:
- NEO4J_AUTH=${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD}
- NEO4J_PLUGINS=["apoc", "graph-data-science"]
- NEO4J_dbms_security_procedures_unrestricted=apoc.*,gds.*
- NEO4J_dbms_memory_heap_initial__size=512M
- NEO4J_dbms_memory_heap_max__size=2G
- NEO4J_dbms_memory_pagecache_size=512M
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
- neo4j_import:/import
- neo4j_plugins:/plugins
networks:
- localai
healthcheck:
test: ["CMD-SHELL", "cypher-shell -u ${NEO4J_USER:-neo4j} -p ${NEO4J_PASSWORD} 'RETURN 1'"]
interval: 10s
timeout: 5s
retries: 5
# REST API Server
api:
build:
context: .
dockerfile: docker/Dockerfile.api
container_name: t6-mem0-api
restart: unless-stopped
ports:
- "${API_PORT:-8080}:8080"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- SUPABASE_CONNECTION_STRING=${SUPABASE_CONNECTION_STRING}
- NEO4J_URI=neo4j://neo4j:7687
- NEO4J_USER=${NEO4J_USER:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD}
- API_HOST=0.0.0.0
- API_PORT=8080
- API_KEY=${API_KEY}
- MEM0_COLLECTION_NAME=${MEM0_COLLECTION_NAME:-t6_memories}
- MEM0_EMBEDDING_DIMS=${MEM0_EMBEDDING_DIMS:-1536}
- MEM0_VERSION=${MEM0_VERSION:-v1.1}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- ENVIRONMENT=${ENVIRONMENT:-production}
depends_on:
neo4j:
condition: service_healthy
networks:
- localai
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/v1/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
# MCP Server
mcp-server:
build:
context: .
dockerfile: docker/Dockerfile.mcp
container_name: t6-mem0-mcp
restart: unless-stopped
ports:
- "${MCP_PORT:-8765}:8765"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- SUPABASE_CONNECTION_STRING=${SUPABASE_CONNECTION_STRING}
- NEO4J_URI=neo4j://neo4j:7687
- NEO4J_USER=${NEO4J_USER:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD}
- MCP_HOST=0.0.0.0
- MCP_PORT=8765
- MEM0_COLLECTION_NAME=${MEM0_COLLECTION_NAME:-t6_memories}
- MEM0_EMBEDDING_DIMS=${MEM0_EMBEDDING_DIMS:-1536}
- MEM0_VERSION=${MEM0_VERSION:-v1.1}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- ENVIRONMENT=${ENVIRONMENT:-production}
depends_on:
neo4j:
condition: service_healthy
networks:
- localai
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8765/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
volumes:
neo4j_data:
name: t6-mem0-neo4j-data
neo4j_logs:
name: t6-mem0-neo4j-logs
neo4j_import:
name: t6-mem0-neo4j-import
neo4j_plugins:
name: t6-mem0-neo4j-plugins
networks:
localai:
external: true

35
docker/Dockerfile.api Normal file
View File

@@ -0,0 +1,35 @@
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY config.py .
COPY api/ ./api/
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/v1/health || exit 1
# Run application
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8080"]

35
docker/Dockerfile.mcp Normal file
View File

@@ -0,0 +1,35 @@
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY config.py .
COPY mcp-server/ ./mcp-server/
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 8765
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8765/health || exit 1
# Run MCP server
CMD ["python", "-m", "mcp-server.main"]

313
docs/architecture.mdx Normal file
View File

@@ -0,0 +1,313 @@
---
title: 'System Architecture'
description: 'Technical architecture and design decisions for T6 Mem0 v2'
---
## Architecture Overview
T6 Mem0 v2 implements a **hybrid storage architecture** combining vector search, graph relationships, and structured data storage for optimal memory management.
```
┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
├──────────────────┬──────────────────┬──────────────────────┤
│ Claude Code (MCP)│ N8N Workflows │ External Apps │
└──────────────────┴──────────────────┴──────────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Interface Layer │
├──────────────────────────────┬──────────────────────────────┤
│ MCP Server (Port 8765) │ REST API (Port 8080) │
│ - SSE Connections │ - FastAPI │
│ - MCP Protocol │ - OpenAPI Spec │
│ - Tool Registration │ - Auth Middleware │
└──────────────────────────────┴──────────────────────────────┘
│ │
└────────┬───────────┘
┌─────────────────────────────────────────────────────────────┐
│ Core Layer │
│ Mem0 Core Library │
│ - Memory Management - Embedding Generation │
│ - Semantic Search - Relationship Extraction │
│ - Multi-Agent Support - Deduplication │
└─────────────────────────────────────────────────────────────┘
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Vector Store │ │ Graph Store │ │ External LLM │
│ Supabase │ │ Neo4j │ │ OpenAI │
│ (pgvector) │ │ (Cypher) │ │ (Embeddings) │
│ 172.21.0.12 │ │ 172.21.0.x │ │ API Cloud │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## Design Decisions
### 1. Hybrid Storage Architecture ✅
**Why Multiple Storage Systems?**
Each store is optimized for specific query patterns:
<AccordionGroup>
<Accordion title="Vector Store (Supabase + pgvector)">
**Purpose**: Semantic similarity search
- Stores 1536-dimensional OpenAI embeddings
- HNSW indexing for fast approximate nearest neighbor search
- O(log n) query performance
- Cosine distance for similarity measurement
</Accordion>
<Accordion title="Graph Store (Neo4j)">
**Purpose**: Relationship modeling
- Entity extraction and connection mapping
- Relationship traversal and pathfinding
- Visual exploration in Neo4j Browser
- Dynamic knowledge graph evolution
</Accordion>
<Accordion title="Key-Value Store (PostgreSQL JSONB)">
**Purpose**: Flexible metadata
- Schema-less metadata storage
- Fast JSON queries with GIN indexes
- Eliminates need for separate Redis
- Simplifies infrastructure
</Accordion>
</AccordionGroup>
### 2. MCP Server Implementation
**Custom vs. Pre-built**
<Info>
We built a custom MCP server instead of using OpenMemory MCP because:
- OpenMemory uses Qdrant (we need Supabase)
- Full control over Supabase + Neo4j integration
- Exact match to our storage stack
</Info>
### 3. Docker Networking Strategy
**localai Network Integration**
All services run on the `localai` Docker network (172.21.0.0/16):
```yaml
services:
neo4j: 172.21.0.x:7687
api: 172.21.0.x:8080
mcp-server: 172.21.0.x:8765
supabase: 172.21.0.12:5432 (existing)
```
**Benefits:**
- Container-to-container communication
- Service discovery via Docker DNS
- No host networking complications
- Persistent IPs via Docker Compose
## Data Flow
### Adding a Memory
<Steps>
<Step title="Client Request">
Client sends conversation messages via MCP or REST API
</Step>
<Step title="Mem0 Processing">
- LLM extracts key facts from messages
- Generates embedding vector (1536-dim)
- Identifies entities and relationships
</Step>
<Step title="Vector Storage">
Stores embedding + metadata in Supabase (pgvector)
</Step>
<Step title="Graph Storage">
Creates nodes and relationships in Neo4j
</Step>
<Step title="Response">
Returns memory ID and confirmation to client
</Step>
</Steps>
### Searching Memories
<Steps>
<Step title="Query Embedding">
Convert search query to vector using OpenAI
</Step>
<Step title="Vector Search">
Find similar memories in Supabase (cosine similarity)
</Step>
<Step title="Graph Enrichment">
Fetch related context from Neo4j graph
</Step>
<Step title="Ranked Results">
Return memories sorted by relevance score
</Step>
</Steps>
## Performance Characteristics
Based on mem0.ai research:
<CardGroup cols={3}>
<Card title="26% Accuracy Boost" icon="chart-line">
Higher accuracy vs baseline OpenAI
</Card>
<Card title="91% Lower Latency" icon="bolt">
Compared to full-context approaches
</Card>
<Card title="90% Token Savings" icon="dollar-sign">
Through selective memory retrieval
</Card>
</CardGroup>
## Security Architecture
### Authentication
- **REST API**: Bearer token authentication
- **MCP Server**: Client-specific SSE endpoints
- **Tokens**: Stored securely in environment variables
### Data Privacy
<Check>
All data stored locally - no cloud sync or external storage
</Check>
- Supabase instance is local (172.21.0.12)
- Neo4j runs in Docker container
- User isolation via `user_id` filtering
### Network Security
- Services on private Docker network
- No public exposure (use reverse proxy if needed)
- Internal communication only
## Scalability
### Horizontal Scaling
<Tabs>
<Tab title="REST API">
Deploy multiple API containers behind load balancer
</Tab>
<Tab title="MCP Server">
Dedicated instances per client group
</Tab>
<Tab title="Mem0 Core">
Stateless design scales with containers
</Tab>
</Tabs>
### Vertical Scaling
- **Supabase**: PostgreSQL connection pooling
- **Neo4j**: Memory configuration tuning
- **Vector Indexing**: HNSW for performance
## Technology Choices
| Component | Technology | Why? |
|-----------|-----------|------|
| Core Library | mem0ai | Production-ready, 26% accuracy boost |
| Vector DB | Supabase (pgvector) | Existing infrastructure, PostgreSQL |
| Graph DB | Neo4j | Best-in-class graph database |
| LLM | OpenAI | High-quality embeddings, GPT-4o |
| REST API | FastAPI | Fast, modern, auto-docs |
| MCP Protocol | Python MCP SDK | Official MCP implementation |
| Containers | Docker Compose | Simple orchestration |
## Phase 2: Ollama Integration
**Configuration-driven provider switching:**
```python
# Phase 1 (OpenAI)
"llm": {
"provider": "openai",
"config": {"model": "gpt-4o-mini"}
}
# Phase 2 (Ollama)
"llm": {
"provider": "ollama",
"config": {
"model": "llama3.1:8b",
"ollama_base_url": "http://172.21.0.1:11434"
}
}
```
**No code changes required** - just environment variables!
## Monitoring & Observability
### Metrics to Track
- Memory operations per second
- Average response time
- Vector search latency
- Graph query complexity
- OpenAI token usage
### Logging
- Structured JSON logs
- Request/response tracking
- Error aggregation
- Performance profiling
<Tip>
Use Prometheus + Grafana for production monitoring
</Tip>
## Deep Dive Resources
For complete architectural details, see:
- [ARCHITECTURE.md](https://git.colsys.tech/klas/t6_mem0_v2/blob/main/ARCHITECTURE.md)
- [PROJECT_REQUIREMENTS.md](https://git.colsys.tech/klas/t6_mem0_v2/blob/main/PROJECT_REQUIREMENTS.md)
## Next Steps
<CardGroup cols={2}>
<Card
title="Setup Supabase"
icon="database"
href="/setup/supabase"
>
Configure vector store
</Card>
<Card
title="Setup Neo4j"
icon="diagram-project"
href="/setup/neo4j"
>
Configure graph database
</Card>
<Card
title="API Reference"
icon="code"
href="/api-reference/introduction"
>
Explore endpoints
</Card>
<Card
title="MCP Integration"
icon="plug"
href="/mcp/introduction"
>
Connect with Claude Code
</Card>
</CardGroup>

168
docs/introduction.mdx Normal file
View File

@@ -0,0 +1,168 @@
---
title: Introduction
description: 'Welcome to T6 Mem0 v2 - Memory System for LLM Applications'
---
<img
className="block dark:hidden"
src="/images/hero-light.svg"
alt="Hero Light"
/>
<img
className="hidden dark:block"
src="/images/hero-dark.svg"
alt="Hero Dark"
/>
## What is T6 Mem0 v2?
T6 Mem0 v2 is a comprehensive memory system for LLM applications built on **mem0.ai**, featuring:
- 🔌 **MCP Server Integration** - Native Model Context Protocol support for Claude Code and AI tools
- 🌐 **REST API** - Full HTTP API for memory operations
- 🗄️ **Hybrid Storage** - Supabase (vector) + Neo4j (graph) for optimal performance
- 🤖 **AI-Powered** - OpenAI embeddings with 26% accuracy improvement
- 📊 **Graph Visualization** - Explore memory relationships in Neo4j Browser
- 🐳 **Docker-Native** - Fully containerized deployment
## Key Features
<CardGroup cols={2}>
<Card
title="Semantic Memory Search"
icon="magnifying-glass"
href="/api-reference/memories/search"
>
Find relevant memories using AI-powered semantic similarity
</Card>
<Card
title="MCP Integration"
icon="plug"
href="/mcp/introduction"
>
Use as MCP server with Claude Code, Cursor, and other AI tools
</Card>
<Card
title="Graph Relationships"
icon="diagram-project"
href="/setup/neo4j"
>
Visualize and explore memory connections with Neo4j
</Card>
<Card
title="Multi-Agent Support"
icon="users"
href="/api-reference/introduction"
>
Isolate memories by user, agent, or run identifiers
</Card>
</CardGroup>
## Architecture
T6 Mem0 v2 uses a **hybrid storage architecture** for optimal performance:
```
┌──────────────────────────────────┐
│ Clients (Claude, N8N, Apps) │
└──────────────┬───────────────────┘
┌──────────────┴───────────────────┐
│ MCP Server (8765) + REST (8080) │
└──────────────┬───────────────────┘
┌──────────────┴───────────────────┐
│ Mem0 Core Library │
└──────────────┬───────────────────┘
┌──────────┴──────────┐
│ │
┌───┴──────┐ ┌──────┴─────┐
│ Supabase │ │ Neo4j │
│ (Vector) │ │ (Graph) │
└──────────┘ └────────────┘
```
### Storage Layers
- **Vector Store (Supabase)**: Semantic similarity search with pgvector
- **Graph Store (Neo4j)**: Relationship modeling between memories
- **Key-Value Store (PostgreSQL JSONB)**: Flexible metadata storage
## Performance
Based on mem0.ai research:
- **26% higher accuracy** compared to baseline OpenAI
- **91% lower latency** than full-context approaches
- **90% token cost savings** through selective retrieval
## Use Cases
<AccordionGroup>
<Accordion icon="comment" title="Conversational AI">
Maintain context across conversations, remember user preferences, and provide personalized responses
</Accordion>
<Accordion icon="robot" title="AI Agents">
Give agents long-term memory, enable learning from past interactions, and improve decision-making
</Accordion>
<Accordion icon="headset" title="Customer Support">
Remember customer history, track issues across sessions, and provide consistent support
</Accordion>
<Accordion icon="graduation-cap" title="Educational Tools">
Track learning progress, adapt to user knowledge level, and personalize content delivery
</Accordion>
</AccordionGroup>
## Quick Links
<CardGroup cols={2}>
<Card
title="Quickstart"
icon="rocket"
href="/quickstart"
>
Get up and running in 5 minutes
</Card>
<Card
title="Architecture Deep Dive"
icon="sitemap"
href="/architecture"
>
Understand the system design
</Card>
<Card
title="API Reference"
icon="code"
href="/api-reference/introduction"
>
Explore the REST API
</Card>
<Card
title="MCP Integration"
icon="link"
href="/mcp/introduction"
>
Connect with Claude Code
</Card>
</CardGroup>
## Technology Stack
- **Core**: mem0ai library
- **Vector DB**: Supabase with pgvector
- **Graph DB**: Neo4j 5.x
- **LLM**: OpenAI API (Phase 1), Ollama (Phase 2)
- **REST API**: FastAPI + Pydantic
- **MCP**: Python MCP SDK
- **Container**: Docker & Docker Compose
## Support & Community
- **Repository**: [git.colsys.tech/klas/t6_mem0_v2](https://git.colsys.tech/klas/t6_mem0_v2)
- **mem0.ai**: [Official mem0 website](https://mem0.ai)
- **Issues**: Contact maintainer
---
Ready to get started? Continue to the [Quickstart Guide](/quickstart).

111
docs/mint.json Normal file
View File

@@ -0,0 +1,111 @@
{
"name": "T6 Mem0 v2",
"logo": {
"dark": "/logo/dark.svg",
"light": "/logo/light.svg"
},
"favicon": "/favicon.svg",
"colors": {
"primary": "#0D9373",
"light": "#07C983",
"dark": "#0D9373",
"anchors": {
"from": "#0D9373",
"to": "#07C983"
}
},
"topbarLinks": [
{
"name": "Support",
"url": "mailto:support@example.com"
}
],
"topbarCtaButton": {
"name": "Dashboard",
"url": "https://git.colsys.tech/klas/t6_mem0_v2"
},
"tabs": [
{
"name": "API Reference",
"url": "api-reference"
},
{
"name": "MCP Integration",
"url": "mcp"
}
],
"anchors": [
{
"name": "GitHub",
"icon": "github",
"url": "https://git.colsys.tech/klas/t6_mem0_v2"
},
{
"name": "mem0.ai",
"icon": "link",
"url": "https://mem0.ai"
}
],
"navigation": [
{
"group": "Get Started",
"pages": [
"introduction",
"quickstart",
"architecture"
]
},
{
"group": "Setup",
"pages": [
"setup/installation",
"setup/configuration",
"setup/supabase",
"setup/neo4j"
]
},
{
"group": "API Documentation",
"pages": [
"api-reference/introduction",
"api-reference/authentication"
]
},
{
"group": "Memory Operations",
"pages": [
"api-reference/memories/add",
"api-reference/memories/search",
"api-reference/memories/get",
"api-reference/memories/update",
"api-reference/memories/delete"
]
},
{
"group": "System",
"pages": [
"api-reference/health",
"api-reference/stats"
]
},
{
"group": "MCP Server",
"pages": [
"mcp/introduction",
"mcp/installation",
"mcp/tools"
]
},
{
"group": "Examples",
"pages": [
"examples/claude-code",
"examples/n8n",
"examples/python"
]
}
],
"footerSocials": {
"github": "https://git.colsys.tech/klas/t6_mem0_v2"
}
}

259
docs/quickstart.mdx Normal file
View File

@@ -0,0 +1,259 @@
---
title: 'Quickstart'
description: 'Get T6 Mem0 v2 running in 5 minutes'
---
## Prerequisites
Before you begin, ensure you have:
- Docker and Docker Compose installed
- Existing Supabase instance (PostgreSQL with pgvector)
- OpenAI API key
- Git access to the repository
<Check>
**Ready to go?** Let's set up your memory system!
</Check>
## Step 1: Clone Repository
```bash
git clone https://git.colsys.tech/klas/t6_mem0_v2
cd t6_mem0_v2
```
## Step 2: Configure Environment
Create `.env` file from template:
```bash
cp .env.example .env
```
Edit `.env` with your credentials:
```bash
# OpenAI Configuration
OPENAI_API_KEY=sk-your-openai-api-key-here
# Supabase Configuration (your existing instance)
SUPABASE_CONNECTION_STRING=postgresql://supabase_admin:password@172.21.0.12:5432/postgres
# Neo4j Configuration
NEO4J_PASSWORD=your-secure-neo4j-password
# API Configuration
API_KEY=your-secure-api-key-here
```
<Warning>
**Important**: Replace all placeholder values with your actual credentials. Never commit the `.env` file to version control!
</Warning>
## Step 3: Apply Database Migrations
Run the Supabase migration to set up the vector store:
### Option A: Using Supabase SQL Editor (Recommended)
1. Open your Supabase dashboard
2. Navigate to **SQL Editor**
3. Copy contents from `migrations/supabase/001_init_vector_store.sql`
4. Paste and execute
### Option B: Using psql
```bash
psql "$SUPABASE_CONNECTION_STRING" -f migrations/supabase/001_init_vector_store.sql
```
<Tip>
The migration creates tables, indexes, and functions needed for vector similarity search. See [Supabase Setup](/setup/supabase) for details.
</Tip>
## Step 4: Start Services
Launch all services with Docker Compose:
```bash
docker compose up -d
```
This starts:
- **Neo4j** (ports 7474, 7687)
- **REST API** (port 8080)
- **MCP Server** (port 8765)
## Step 5: Verify Installation
Check service health:
```bash
# Check API health
curl http://localhost:8080/v1/health
# Expected response:
# {
# "status": "healthy",
# "version": "0.1.0",
# "dependencies": {
# "mem0": "healthy"
# }
# }
```
<Check>
**Success!** All services are running. Let's try using the memory system.
</Check>
## Step 6: Add Your First Memory
### Using REST API
```bash
curl -X POST http://localhost:8080/v1/memories/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "I love pizza with mushrooms and olives"}
],
"user_id": "alice"
}'
```
### Response
```json
{
"status": "success",
"memories": [
{
"id": "mem_abc123",
"memory": "User loves pizza with mushrooms and olives",
"user_id": "alice",
"created_at": "2025-10-13T12:00:00Z"
}
],
"message": "Successfully added 1 memory(ies)"
}
```
## Step 7: Search Memories
```bash
curl -X GET "http://localhost:8080/v1/memories/search?query=What food does Alice like?&user_id=alice" \
-H "Authorization: Bearer YOUR_API_KEY"
```
### Response
```json
{
"status": "success",
"memories": [
{
"id": "mem_abc123",
"memory": "User loves pizza with mushrooms and olives",
"user_id": "alice",
"score": 0.95,
"created_at": "2025-10-13T12:00:00Z"
}
],
"count": 1
}
```
<Success>
**Congratulations!** Your memory system is working. The AI remembered Alice's food preferences and retrieved them semantically.
</Success>
## Step 8: Explore Neo4j (Optional)
View memory relationships in Neo4j Browser:
1. Open http://localhost:7474 in your browser
2. Login with:
- **Username**: `neo4j`
- **Password**: (from your `.env` NEO4J_PASSWORD)
3. Run query:
```cypher
MATCH (n) RETURN n LIMIT 25
```
<Tip>
Neo4j visualizes relationships between memories, entities, and concepts extracted by mem0.
</Tip>
## Next Steps
<CardGroup cols={2}>
<Card
title="Configure MCP Server"
icon="plug"
href="/mcp/installation"
>
Connect with Claude Code for AI assistant integration
</Card>
<Card
title="API Reference"
icon="book"
href="/api-reference/introduction"
>
Explore all available endpoints
</Card>
<Card
title="Architecture"
icon="sitemap"
href="/architecture"
>
Understand the system design
</Card>
<Card
title="Examples"
icon="code"
href="/examples/n8n"
>
See integration examples
</Card>
</CardGroup>
## Common Issues
<AccordionGroup>
<Accordion title="Connection to Supabase fails">
- Verify `SUPABASE_CONNECTION_STRING` is correct
- Ensure Supabase is accessible from Docker network
- Check if pgvector extension is enabled
- Run migration script if tables don't exist
</Accordion>
<Accordion title="Neo4j won't start">
- Check if ports 7474 and 7687 are available
- Verify `NEO4J_PASSWORD` is set in `.env`
- Check Docker logs: `docker logs t6-mem0-neo4j`
</Accordion>
<Accordion title="API returns authentication error">
- Verify `API_KEY` in `.env` matches request header
- Ensure Authorization header format: `Bearer YOUR_API_KEY`
</Accordion>
<Accordion title="OpenAI errors">
- Check `OPENAI_API_KEY` is valid
- Verify API key has sufficient credits
- Check internet connectivity from containers
</Accordion>
</AccordionGroup>
## Getting Help
- Review [Architecture documentation](/architecture)
- Check [API Reference](/api-reference/introduction)
- See [Setup guides](/setup/installation)
---
**Ready for production?** Continue to [Configuration](/setup/configuration) for advanced settings.

0
mcp-server/__init__.py Normal file
View File

165
mcp-server/main.py Normal file
View File

@@ -0,0 +1,165 @@
"""
T6 Mem0 v2 MCP Server
Model Context Protocol server for memory operations
"""
import logging
import sys
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Resource,
Tool,
TextContent,
ImageContent,
EmbeddedResource
)
from mem0 import Memory
from config import mem0_config, settings
from mcp_server.tools import MemoryTools
# Configure logging
logging.basicConfig(
level=getattr(logging, settings.log_level.upper()),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stderr)] # MCP uses stderr for logs
)
logger = logging.getLogger(__name__)
class T6Mem0Server:
"""T6 Mem0 v2 MCP Server"""
def __init__(self):
"""Initialize MCP server"""
self.server = Server("t6-mem0-v2")
self.memory: Memory | None = None
self.tools: MemoryTools | None = None
# Setup handlers
self.setup_handlers()
def setup_handlers(self):
"""Setup MCP server handlers"""
@self.server.list_resources()
async def list_resources() -> list[Resource]:
"""
List available resources (for future extension)
"""
return []
@self.server.read_resource()
async def read_resource(uri: str) -> str:
"""
Read resource by URI (for future extension)
"""
logger.warning(f"Resource read not implemented: {uri}")
return ""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""List available memory tools"""
logger.info("Listing tools")
if not self.tools:
raise RuntimeError("Tools not initialized")
return self.tools.get_tool_definitions()
@self.server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent | ImageContent | EmbeddedResource]:
"""
Handle tool calls
Args:
name: Tool name
arguments: Tool arguments
Returns:
Tool response
"""
logger.info(f"Tool called: {name}")
logger.debug(f"Arguments: {arguments}")
if not self.tools:
raise RuntimeError("Tools not initialized")
# Route to appropriate handler
handlers = {
"add_memory": self.tools.handle_add_memory,
"search_memories": self.tools.handle_search_memories,
"get_memory": self.tools.handle_get_memory,
"get_all_memories": self.tools.handle_get_all_memories,
"update_memory": self.tools.handle_update_memory,
"delete_memory": self.tools.handle_delete_memory,
"delete_all_memories": self.tools.handle_delete_all_memories,
}
handler = handlers.get(name)
if not handler:
logger.error(f"Unknown tool: {name}")
return [TextContent(type="text", text=f"Error: Unknown tool '{name}'")]
try:
return await handler(arguments)
except Exception as e:
logger.error(f"Tool execution failed: {e}", exc_info=True)
return [TextContent(type="text", text=f"Error executing tool: {str(e)}")]
async def initialize(self):
"""Initialize memory service"""
logger.info("Initializing T6 Mem0 v2 MCP Server")
logger.info(f"Environment: {settings.environment}")
try:
# Initialize Mem0
logger.info("Initializing Mem0...")
self.memory = Memory.from_config(config_dict=mem0_config)
logger.info("Mem0 initialized successfully")
# Initialize tools
self.tools = MemoryTools(self.memory)
logger.info("Tools initialized successfully")
logger.info("T6 Mem0 v2 MCP Server ready")
except Exception as e:
logger.error(f"Failed to initialize server: {e}", exc_info=True)
raise
async def run(self):
"""Run the MCP server"""
try:
# Initialize before running
await self.initialize()
# Run server with stdio transport
logger.info("Starting MCP server with stdio transport")
async with stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
except Exception as e:
logger.error(f"Server error: {e}", exc_info=True)
raise
async def main():
"""Main entry point"""
try:
server = T6Mem0Server()
await server.run()
except KeyboardInterrupt:
logger.info("Server stopped by user")
except Exception as e:
logger.error(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

13
mcp-server/run.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Run T6 Mem0 v2 MCP Server
set -e
# Load environment variables
if [ -f ../.env ]; then
export $(cat ../.env | grep -v '^#' | xargs)
fi
# Run MCP server
echo "Starting T6 Mem0 v2 MCP Server..."
python -m mcp_server.main

343
mcp-server/tools.py Normal file
View File

@@ -0,0 +1,343 @@
"""
MCP tools for T6 Mem0 v2
Tool definitions and handlers
"""
import logging
from typing import Any, Dict, List
from mcp.types import Tool, TextContent
from mem0 import Memory
logger = logging.getLogger(__name__)
class MemoryTools:
"""MCP tools for memory operations"""
def __init__(self, memory: Memory):
"""
Initialize memory tools
Args:
memory: Mem0 instance
"""
self.memory = memory
def get_tool_definitions(self) -> List[Tool]:
"""
Get MCP tool definitions
Returns:
List of Tool definitions
"""
return [
Tool(
name="add_memory",
description="Add new memory from messages. Extracts and stores important information from conversation.",
inputSchema={
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"role": {"type": "string", "enum": ["user", "assistant", "system"]},
"content": {"type": "string"}
},
"required": ["role", "content"]
},
"description": "Conversation messages to extract memory from"
},
"user_id": {
"type": "string",
"description": "User identifier (optional)"
},
"agent_id": {
"type": "string",
"description": "Agent identifier (optional)"
},
"metadata": {
"type": "object",
"description": "Additional metadata (optional)"
}
},
"required": ["messages"]
}
),
Tool(
name="search_memories",
description="Search memories by semantic similarity. Find relevant memories based on a query.",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"user_id": {
"type": "string",
"description": "Filter by user ID (optional)"
},
"agent_id": {
"type": "string",
"description": "Filter by agent ID (optional)"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 10,
"description": "Maximum number of results"
}
},
"required": ["query"]
}
),
Tool(
name="get_memory",
description="Get a specific memory by its ID",
inputSchema={
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "Memory identifier"
}
},
"required": ["memory_id"]
}
),
Tool(
name="get_all_memories",
description="Get all memories for a user or agent",
inputSchema={
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "User identifier (optional)"
},
"agent_id": {
"type": "string",
"description": "Agent identifier (optional)"
}
}
}
),
Tool(
name="update_memory",
description="Update an existing memory's content",
inputSchema={
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "Memory identifier"
},
"data": {
"type": "string",
"description": "New memory content"
}
},
"required": ["memory_id", "data"]
}
),
Tool(
name="delete_memory",
description="Delete a specific memory by ID",
inputSchema={
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "Memory identifier"
}
},
"required": ["memory_id"]
}
),
Tool(
name="delete_all_memories",
description="Delete all memories for a user or agent. Use with caution!",
inputSchema={
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "User identifier (optional)"
},
"agent_id": {
"type": "string",
"description": "Agent identifier (optional)"
}
}
}
)
]
async def handle_add_memory(self, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle add_memory tool call"""
try:
messages = arguments.get("messages", [])
user_id = arguments.get("user_id")
agent_id = arguments.get("agent_id")
metadata = arguments.get("metadata", {})
result = self.memory.add(
messages=messages,
user_id=user_id,
agent_id=agent_id,
metadata=metadata
)
memories = result.get('results', [])
response = f"Successfully added {len(memories)} memory(ies):\n\n"
for mem in memories:
response += f"- {mem.get('memory', mem.get('data', 'N/A'))}\n"
response += f" ID: {mem.get('id', 'N/A')}\n\n"
return [TextContent(type="text", text=response)]
except Exception as e:
logger.error(f"Error adding memory: {e}")
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def handle_search_memories(self, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle search_memories tool call"""
try:
query = arguments.get("query")
user_id = arguments.get("user_id")
agent_id = arguments.get("agent_id")
limit = arguments.get("limit", 10)
memories = self.memory.search(
query=query,
user_id=user_id,
agent_id=agent_id,
limit=limit
)
if not memories:
return [TextContent(type="text", text="No memories found matching your query.")]
response = f"Found {len(memories)} relevant memory(ies):\n\n"
for i, mem in enumerate(memories, 1):
response += f"{i}. {mem.get('memory', mem.get('data', 'N/A'))}\n"
response += f" ID: {mem.get('id', 'N/A')}\n"
if 'score' in mem:
response += f" Relevance: {mem['score']:.2%}\n"
response += "\n"
return [TextContent(type="text", text=response)]
except Exception as e:
logger.error(f"Error searching memories: {e}")
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def handle_get_memory(self, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle get_memory tool call"""
try:
memory_id = arguments.get("memory_id")
memory = self.memory.get(memory_id=memory_id)
if not memory:
return [TextContent(type="text", text=f"Memory not found: {memory_id}")]
response = f"Memory Details:\n\n"
response += f"ID: {memory.get('id', 'N/A')}\n"
response += f"Content: {memory.get('memory', memory.get('data', 'N/A'))}\n"
if memory.get('user_id'):
response += f"User ID: {memory['user_id']}\n"
if memory.get('agent_id'):
response += f"Agent ID: {memory['agent_id']}\n"
if memory.get('metadata'):
response += f"Metadata: {memory['metadata']}\n"
return [TextContent(type="text", text=response)]
except Exception as e:
logger.error(f"Error getting memory: {e}")
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def handle_get_all_memories(self, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle get_all_memories tool call"""
try:
user_id = arguments.get("user_id")
agent_id = arguments.get("agent_id")
memories = self.memory.get_all(
user_id=user_id,
agent_id=agent_id
)
if not memories:
return [TextContent(type="text", text="No memories found.")]
response = f"Retrieved {len(memories)} memory(ies):\n\n"
for i, mem in enumerate(memories, 1):
response += f"{i}. {mem.get('memory', mem.get('data', 'N/A'))}\n"
response += f" ID: {mem.get('id', 'N/A')}\n\n"
return [TextContent(type="text", text=response)]
except Exception as e:
logger.error(f"Error getting all memories: {e}")
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def handle_update_memory(self, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle update_memory tool call"""
try:
memory_id = arguments.get("memory_id")
data = arguments.get("data")
result = self.memory.update(
memory_id=memory_id,
data=data
)
response = f"Memory updated successfully:\n\n"
response += f"ID: {result.get('id', memory_id)}\n"
response += f"New Content: {data}\n"
return [TextContent(type="text", text=response)]
except Exception as e:
logger.error(f"Error updating memory: {e}")
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def handle_delete_memory(self, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle delete_memory tool call"""
try:
memory_id = arguments.get("memory_id")
self.memory.delete(memory_id=memory_id)
return [TextContent(type="text", text=f"Memory {memory_id} deleted successfully.")]
except Exception as e:
logger.error(f"Error deleting memory: {e}")
return [TextContent(type="text", text=f"Error: {str(e)}")]
async def handle_delete_all_memories(self, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle delete_all_memories tool call"""
try:
user_id = arguments.get("user_id")
agent_id = arguments.get("agent_id")
self.memory.delete_all(
user_id=user_id,
agent_id=agent_id
)
filter_str = f"user_id={user_id}" if user_id else f"agent_id={agent_id}" if agent_id else "all filters"
return [TextContent(type="text", text=f"All memories deleted for {filter_str}.")]
except Exception as e:
logger.error(f"Error deleting all memories: {e}")
return [TextContent(type="text", text=f"Error: {str(e)}")]

View File

@@ -0,0 +1,172 @@
-- T6 Mem0 v2 - Initial Vector Store Setup
-- This migration creates the necessary tables and functions for Mem0 vector storage
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create memories table
CREATE TABLE IF NOT EXISTS t6_memories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
embedding vector(1536), -- OpenAI text-embedding-3-small dimension
metadata JSONB NOT NULL DEFAULT '{}'::JSONB,
user_id TEXT,
agent_id TEXT,
run_id TEXT,
memory_text TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
hash TEXT, -- For deduplication
-- Indexes
CONSTRAINT t6_memories_hash_unique UNIQUE (hash)
);
-- Create index on embedding using HNSW for fast similarity search
CREATE INDEX IF NOT EXISTS t6_memories_embedding_idx
ON t6_memories
USING hnsw (embedding vector_cosine_ops);
-- Create indexes on metadata fields for filtering
CREATE INDEX IF NOT EXISTS t6_memories_user_id_idx
ON t6_memories (user_id);
CREATE INDEX IF NOT EXISTS t6_memories_agent_id_idx
ON t6_memories (agent_id);
CREATE INDEX IF NOT EXISTS t6_memories_run_id_idx
ON t6_memories (run_id);
CREATE INDEX IF NOT EXISTS t6_memories_created_at_idx
ON t6_memories (created_at DESC);
-- Create GIN index on metadata for JSON queries
CREATE INDEX IF NOT EXISTS t6_memories_metadata_idx
ON t6_memories
USING GIN (metadata);
-- Create full-text search index on memory_text
CREATE INDEX IF NOT EXISTS t6_memories_text_search_idx
ON t6_memories
USING GIN (to_tsvector('english', memory_text));
-- Function to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_t6_memories_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger to automatically update updated_at
CREATE TRIGGER t6_memories_updated_at_trigger
BEFORE UPDATE ON t6_memories
FOR EACH ROW
EXECUTE FUNCTION update_t6_memories_updated_at();
-- Function for vector similarity search with filters
CREATE OR REPLACE FUNCTION match_t6_memories(
query_embedding vector(1536),
match_count INT DEFAULT 10,
filter_user_id TEXT DEFAULT NULL,
filter_agent_id TEXT DEFAULT NULL,
filter_run_id TEXT DEFAULT NULL
)
RETURNS TABLE (
id UUID,
memory_text TEXT,
metadata JSONB,
user_id TEXT,
agent_id TEXT,
run_id TEXT,
similarity FLOAT,
created_at TIMESTAMP WITH TIME ZONE
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
t6_memories.id,
t6_memories.memory_text,
t6_memories.metadata,
t6_memories.user_id,
t6_memories.agent_id,
t6_memories.run_id,
1 - (t6_memories.embedding <=> query_embedding) AS similarity,
t6_memories.created_at
FROM t6_memories
WHERE
(filter_user_id IS NULL OR t6_memories.user_id = filter_user_id) AND
(filter_agent_id IS NULL OR t6_memories.agent_id = filter_agent_id) AND
(filter_run_id IS NULL OR t6_memories.run_id = filter_run_id)
ORDER BY t6_memories.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
-- Function to get memory statistics
CREATE OR REPLACE FUNCTION get_t6_memory_stats()
RETURNS TABLE (
total_memories BIGINT,
total_users BIGINT,
total_agents BIGINT,
avg_memories_per_user NUMERIC,
oldest_memory TIMESTAMP WITH TIME ZONE,
newest_memory TIMESTAMP WITH TIME ZONE
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(*)::BIGINT AS total_memories,
COUNT(DISTINCT user_id)::BIGINT AS total_users,
COUNT(DISTINCT agent_id)::BIGINT AS total_agents,
CASE
WHEN COUNT(DISTINCT user_id) > 0
THEN ROUND(COUNT(*)::NUMERIC / COUNT(DISTINCT user_id), 2)
ELSE 0
END AS avg_memories_per_user,
MIN(created_at) AS oldest_memory,
MAX(created_at) AS newest_memory
FROM t6_memories;
END;
$$;
-- Create a view for recent memories
CREATE OR REPLACE VIEW t6_recent_memories AS
SELECT
id,
user_id,
agent_id,
run_id,
memory_text,
metadata,
created_at,
updated_at
FROM t6_memories
ORDER BY created_at DESC
LIMIT 100;
-- Grant necessary permissions (adjust as needed for your setup)
-- GRANT SELECT, INSERT, UPDATE, DELETE ON t6_memories TO authenticated;
-- GRANT EXECUTE ON FUNCTION match_t6_memories TO authenticated;
-- GRANT EXECUTE ON FUNCTION get_t6_memory_stats TO authenticated;
-- Comments for documentation
COMMENT ON TABLE t6_memories IS 'Storage for T6 Mem0 v2 memory vectors and metadata';
COMMENT ON COLUMN t6_memories.embedding IS 'OpenAI text-embedding-3-small vector (1536 dimensions)';
COMMENT ON COLUMN t6_memories.metadata IS 'Flexible JSON metadata for additional memory properties';
COMMENT ON COLUMN t6_memories.hash IS 'Hash for deduplication of identical memories';
COMMENT ON FUNCTION match_t6_memories IS 'Performs cosine similarity search on memory embeddings with optional filters';
COMMENT ON FUNCTION get_t6_memory_stats IS 'Returns statistics about stored memories';
-- Success message
DO $$
BEGIN
RAISE NOTICE 'T6 Mem0 v2 vector store initialized successfully!';
RAISE NOTICE 'Table: t6_memories';
RAISE NOTICE 'Functions: match_t6_memories, get_t6_memory_stats';
RAISE NOTICE 'View: t6_recent_memories';
END $$;

View File

@@ -0,0 +1,153 @@
# Supabase Migrations for T6 Mem0 v2
## Overview
This directory contains SQL migrations for setting up the Supabase vector store used by T6 Mem0 v2.
## Migrations
### 001_init_vector_store.sql
Initial setup migration that creates:
- **pgvector extension**: Enables vector similarity search
- **t6_memories table**: Main storage for memory vectors and metadata
- **Indexes**: HNSW for vectors, B-tree for filters, GIN for JSONB
- **Functions**:
- `match_t6_memories()`: Vector similarity search with filters
- `get_t6_memory_stats()`: Memory statistics
- `update_t6_memories_updated_at()`: Auto-update timestamp
- **View**: `t6_recent_memories` for quick access to recent entries
## Applying Migrations
### Method 1: Supabase SQL Editor (Recommended)
1. Open your Supabase project dashboard
2. Navigate to SQL Editor
3. Create a new query
4. Copy and paste the contents of `001_init_vector_store.sql`
5. Click "Run" to execute
### Method 2: psql Command Line
```bash
# Connect to your Supabase database
psql "postgresql://supabase_admin:PASSWORD@172.21.0.12:5432/postgres"
# Run the migration
\i migrations/supabase/001_init_vector_store.sql
```
### Method 3: Programmatic Application
```python
import psycopg2
# Connect to Supabase
conn = psycopg2.connect(
"postgresql://supabase_admin:PASSWORD@172.21.0.12:5432/postgres"
)
# Read and execute migration
with open('migrations/supabase/001_init_vector_store.sql', 'r') as f:
migration_sql = f.read()
with conn.cursor() as cur:
cur.execute(migration_sql)
conn.commit()
conn.close()
```
## Verification
After applying the migration, verify the setup:
```sql
-- Check if pgvector extension is enabled
SELECT * FROM pg_extension WHERE extname = 'vector';
-- Check if table exists
\d t6_memories
-- Verify indexes
\di t6_memories*
-- Test the similarity search function
SELECT * FROM match_t6_memories(
'[0.1, 0.2, ...]'::vector(1536), -- Sample embedding
10, -- Match count
'test_user', -- User ID filter
NULL, -- Agent ID filter
NULL -- Run ID filter
);
-- Get memory statistics
SELECT * FROM get_t6_memory_stats();
```
## Rollback
If you need to rollback the migration:
```sql
-- Drop view
DROP VIEW IF EXISTS t6_recent_memories;
-- Drop functions
DROP FUNCTION IF EXISTS get_t6_memory_stats();
DROP FUNCTION IF EXISTS match_t6_memories(vector, INT, TEXT, TEXT, TEXT);
DROP FUNCTION IF EXISTS update_t6_memories_updated_at();
-- Drop trigger
DROP TRIGGER IF EXISTS t6_memories_updated_at_trigger ON t6_memories;
-- Drop table (WARNING: This will delete all data!)
DROP TABLE IF EXISTS t6_memories CASCADE;
-- Optionally remove extension (only if not used elsewhere)
-- DROP EXTENSION IF EXISTS vector CASCADE;
```
## Schema
### t6_memories Table
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| embedding | vector(1536) | OpenAI embedding vector |
| metadata | JSONB | Flexible metadata |
| user_id | TEXT | User identifier |
| agent_id | TEXT | Agent identifier |
| run_id | TEXT | Run identifier |
| memory_text | TEXT | Original memory text |
| created_at | TIMESTAMPTZ | Creation timestamp |
| updated_at | TIMESTAMPTZ | Last update timestamp |
| hash | TEXT | Deduplication hash (unique) |
### Indexes
- **t6_memories_embedding_idx**: HNSW index for fast vector search
- **t6_memories_user_id_idx**: B-tree for user filtering
- **t6_memories_agent_id_idx**: B-tree for agent filtering
- **t6_memories_run_id_idx**: B-tree for run filtering
- **t6_memories_created_at_idx**: B-tree for time-based queries
- **t6_memories_metadata_idx**: GIN for JSON queries
- **t6_memories_text_search_idx**: GIN for full-text search
## Notes
- The HNSW index provides O(log n) approximate nearest neighbor search
- Cosine distance is used for similarity (1 - cosine similarity)
- All timestamps are stored in UTC
- The hash column ensures deduplication of identical memories
- Metadata is stored as JSONB for flexible schema evolution
## Support
For issues or questions about migrations, refer to:
- [Supabase Vector Documentation](https://supabase.com/docs/guides/database/extensions/pgvector)
- [pgvector Documentation](https://github.com/pgvector/pgvector)
- Project Architecture: `../../ARCHITECTURE.md`

34
requirements.txt Normal file
View File

@@ -0,0 +1,34 @@
# Core Memory System
mem0ai[graph]==0.1.*
# Web Framework
fastapi==0.115.*
uvicorn[standard]==0.32.*
pydantic==2.9.*
pydantic-settings==2.6.*
# MCP Server
mcp==1.3.*
# Database Drivers
psycopg2-binary==2.9.*
neo4j==5.26.*
# OpenAI
openai==1.58.*
# Utilities
python-dotenv==1.0.*
httpx==0.28.*
pyyaml==6.0.*
# Development
pytest==8.3.*
pytest-asyncio==0.24.*
pytest-cov==6.0.*
black==24.10.*
ruff==0.8.*
mypy==1.13.*
# Monitoring
prometheus-client==0.21.*

0
tests/__init__.py Normal file
View File

0
tests/api/__init__.py Normal file
View File

View File

View File