feat: Add Memory system with Weaviate integration and MCP tools

MEMORY SYSTEM ARCHITECTURE:
- Weaviate-based memory storage (Thought, Message, Conversation collections)
- GPU embeddings with BAAI/bge-m3 (1024-dim, RTX 4070)
- 9 MCP tools for Claude Desktop integration

CORE MODULES (memory/):
- core/embedding_service.py: GPU embedder singleton with PyTorch
- schemas/memory_schemas.py: Weaviate schema definitions
- mcp/thought_tools.py: add_thought, search_thoughts, get_thought
- mcp/message_tools.py: add_message, get_messages, search_messages
- mcp/conversation_tools.py: get_conversation, search_conversations, list_conversations

FLASK TEMPLATES:
- conversation_view.html: Display single conversation with messages
- conversations.html: List all conversations with search
- memories.html: Browse and search thoughts

FEATURES:
- Semantic search across thoughts, messages, conversations
- Privacy levels (private, shared, public)
- Thought types (reflection, question, intuition, observation)
- Conversation categories with filtering
- Message ordering and role-based display

DATA (as of 2026-01-08):
- 102 Thoughts
- 377 Messages
- 12 Conversations

DOCUMENTATION:
- memory/README_MCP_TOOLS.md: Complete API reference and usage examples

All MCP tools tested and validated (see test_memory_mcp_tools.py in archive).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-08 18:08:13 +01:00
parent 187ba4854e
commit 2f34125ef6
13 changed files with 2145 additions and 0 deletions

56
memory/mcp/__init__.py Normal file
View File

@@ -0,0 +1,56 @@
"""
Memory MCP Tools Package.
Provides MCP tools for Memory system (Thoughts, Messages, Conversations).
"""
from memory.mcp.thought_tools import (
AddThoughtInput,
SearchThoughtsInput,
add_thought_handler,
search_thoughts_handler,
get_thought_handler,
)
from memory.mcp.message_tools import (
AddMessageInput,
GetMessagesInput,
SearchMessagesInput,
add_message_handler,
get_messages_handler,
search_messages_handler,
)
from memory.mcp.conversation_tools import (
GetConversationInput,
SearchConversationsInput,
ListConversationsInput,
get_conversation_handler,
search_conversations_handler,
list_conversations_handler,
)
__all__ = [
# Thought tools
"AddThoughtInput",
"SearchThoughtsInput",
"add_thought_handler",
"search_thoughts_handler",
"get_thought_handler",
# Message tools
"AddMessageInput",
"GetMessagesInput",
"SearchMessagesInput",
"add_message_handler",
"get_messages_handler",
"search_messages_handler",
# Conversation tools
"GetConversationInput",
"SearchConversationsInput",
"ListConversationsInput",
"get_conversation_handler",
"search_conversations_handler",
"list_conversations_handler",
]

View File

@@ -0,0 +1,208 @@
"""
Conversation MCP Tools - Handlers for conversation-related operations.
Provides tools for searching and retrieving conversations.
"""
import weaviate
from typing import Any, Dict
from pydantic import BaseModel, Field
from memory.core import get_embedder
class GetConversationInput(BaseModel):
"""Input for get_conversation tool."""
conversation_id: str = Field(..., description="Conversation identifier")
class SearchConversationsInput(BaseModel):
"""Input for search_conversations tool."""
query: str = Field(..., description="Search query text")
limit: int = Field(default=10, ge=1, le=50, description="Maximum results")
category_filter: str | None = Field(default=None, description="Filter by category")
class ListConversationsInput(BaseModel):
"""Input for list_conversations tool."""
limit: int = Field(default=20, ge=1, le=100, description="Maximum conversations")
category_filter: str | None = Field(default=None, description="Filter by category")
async def get_conversation_handler(input_data: GetConversationInput) -> Dict[str, Any]:
"""
Get a specific conversation by ID.
Args:
input_data: Query parameters.
Returns:
Dictionary with conversation data.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get collection
collection = client.collections.get("Conversation")
# Fetch by conversation_id
results = collection.query.fetch_objects(
filters=weaviate.classes.query.Filter.by_property("conversation_id").equal(input_data.conversation_id),
limit=1,
)
if not results.objects:
return {
"success": False,
"error": f"Conversation {input_data.conversation_id} not found",
}
obj = results.objects[0]
return {
"success": True,
"conversation_id": obj.properties['conversation_id'],
"category": obj.properties['category'],
"summary": obj.properties['summary'],
"timestamp_start": obj.properties['timestamp_start'],
"timestamp_end": obj.properties['timestamp_end'],
"participants": obj.properties['participants'],
"tags": obj.properties.get('tags', []),
"message_count": obj.properties['message_count'],
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}
async def search_conversations_handler(input_data: SearchConversationsInput) -> Dict[str, Any]:
"""
Search conversations using semantic similarity.
Args:
input_data: Search parameters.
Returns:
Dictionary with search results.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get embedder
embedder = get_embedder()
# Generate query vector
query_vector = embedder.embed_batch([input_data.query])[0]
# Get collection
collection = client.collections.get("Conversation")
# Build query
query_builder = collection.query.near_vector(
near_vector=query_vector.tolist(),
limit=input_data.limit,
)
# Apply category filter if provided
if input_data.category_filter:
query_builder = query_builder.where(
weaviate.classes.query.Filter.by_property("category").equal(input_data.category_filter)
)
# Execute search
results = query_builder.objects
# Format results
conversations = []
for obj in results:
conversations.append({
"conversation_id": obj.properties['conversation_id'],
"category": obj.properties['category'],
"summary": obj.properties['summary'],
"timestamp_start": obj.properties['timestamp_start'],
"timestamp_end": obj.properties['timestamp_end'],
"participants": obj.properties['participants'],
"message_count": obj.properties['message_count'],
})
return {
"success": True,
"query": input_data.query,
"results": conversations,
"count": len(conversations),
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}
async def list_conversations_handler(input_data: ListConversationsInput) -> Dict[str, Any]:
"""
List all conversations with filtering.
Args:
input_data: Query parameters.
Returns:
Dictionary with conversation list.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get collection
collection = client.collections.get("Conversation")
# Build query
if input_data.category_filter:
results = collection.query.fetch_objects(
filters=weaviate.classes.query.Filter.by_property("category").equal(input_data.category_filter),
limit=input_data.limit,
)
else:
results = collection.query.fetch_objects(
limit=input_data.limit,
)
# Format results
conversations = []
for obj in results.objects:
conversations.append({
"conversation_id": obj.properties['conversation_id'],
"category": obj.properties['category'],
"summary": obj.properties['summary'][:100] + "..." if len(obj.properties['summary']) > 100 else obj.properties['summary'],
"timestamp_start": obj.properties['timestamp_start'],
"message_count": obj.properties['message_count'],
"participants": obj.properties['participants'],
})
return {
"success": True,
"conversations": conversations,
"count": len(conversations),
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}

213
memory/mcp/message_tools.py Normal file
View File

@@ -0,0 +1,213 @@
"""
Message MCP Tools - Handlers for message-related operations.
Provides tools for adding, searching, and retrieving conversation messages.
"""
import weaviate
from datetime import datetime, timezone
from typing import Any, Dict
from pydantic import BaseModel, Field
from memory.core import get_embedder
class AddMessageInput(BaseModel):
"""Input for add_message tool."""
content: str = Field(..., description="Message content")
role: str = Field(..., description="Role: user, assistant, system")
conversation_id: str = Field(..., description="Conversation identifier")
order_index: int = Field(default=0, description="Position in conversation")
class GetMessagesInput(BaseModel):
"""Input for get_messages tool."""
conversation_id: str = Field(..., description="Conversation identifier")
limit: int = Field(default=50, ge=1, le=500, description="Maximum messages")
class SearchMessagesInput(BaseModel):
"""Input for search_messages tool."""
query: str = Field(..., description="Search query text")
limit: int = Field(default=10, ge=1, le=100, description="Maximum results")
conversation_id_filter: str | None = Field(default=None, description="Filter by conversation")
async def add_message_handler(input_data: AddMessageInput) -> Dict[str, Any]:
"""
Add a new message to Weaviate.
Args:
input_data: Message data to add.
Returns:
Dictionary with success status and message UUID.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get embedder
embedder = get_embedder()
# Generate vector for message content
vector = embedder.embed_batch([input_data.content])[0]
# Get collection
collection = client.collections.get("Message")
# Insert message
uuid = collection.data.insert(
properties={
"content": input_data.content,
"role": input_data.role,
"timestamp": datetime.now(timezone.utc).isoformat(),
"conversation_id": input_data.conversation_id,
"order_index": input_data.order_index,
"conversation": {
"conversation_id": input_data.conversation_id,
"category": "general", # Default
},
},
vector=vector.tolist()
)
return {
"success": True,
"uuid": str(uuid),
"content": input_data.content[:100] + "..." if len(input_data.content) > 100 else input_data.content,
"role": input_data.role,
"conversation_id": input_data.conversation_id,
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}
async def get_messages_handler(input_data: GetMessagesInput) -> Dict[str, Any]:
"""
Get all messages from a conversation.
Args:
input_data: Query parameters.
Returns:
Dictionary with messages in order.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get collection
collection = client.collections.get("Message")
# Fetch messages for conversation
results = collection.query.fetch_objects(
filters=weaviate.classes.query.Filter.by_property("conversation_id").equal(input_data.conversation_id),
limit=input_data.limit,
)
# Sort by order_index
messages = []
for obj in results.objects:
messages.append({
"uuid": str(obj.uuid),
"content": obj.properties['content'],
"role": obj.properties['role'],
"timestamp": obj.properties['timestamp'],
"order_index": obj.properties['order_index'],
})
# Sort by order_index
messages.sort(key=lambda m: m['order_index'])
return {
"success": True,
"conversation_id": input_data.conversation_id,
"messages": messages,
"count": len(messages),
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}
async def search_messages_handler(input_data: SearchMessagesInput) -> Dict[str, Any]:
"""
Search messages using semantic similarity.
Args:
input_data: Search parameters.
Returns:
Dictionary with search results.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get embedder
embedder = get_embedder()
# Generate query vector
query_vector = embedder.embed_batch([input_data.query])[0]
# Get collection
collection = client.collections.get("Message")
# Build query
query_builder = collection.query.near_vector(
near_vector=query_vector.tolist(),
limit=input_data.limit,
)
# Apply conversation filter if provided
if input_data.conversation_id_filter:
query_builder = query_builder.where(
weaviate.classes.query.Filter.by_property("conversation_id").equal(input_data.conversation_id_filter)
)
# Execute search
results = query_builder.objects
# Format results
messages = []
for obj in results:
messages.append({
"uuid": str(obj.uuid),
"content": obj.properties['content'],
"role": obj.properties['role'],
"timestamp": obj.properties['timestamp'],
"conversation_id": obj.properties['conversation_id'],
"order_index": obj.properties['order_index'],
})
return {
"success": True,
"query": input_data.query,
"results": messages,
"count": len(messages),
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}

203
memory/mcp/thought_tools.py Normal file
View File

@@ -0,0 +1,203 @@
"""
Thought MCP Tools - Handlers for thought-related operations.
Provides tools for adding, searching, and retrieving thoughts from Weaviate.
"""
import weaviate
from datetime import datetime, timezone
from typing import Any, Dict
from pydantic import BaseModel, Field
from memory.core import get_embedder
class AddThoughtInput(BaseModel):
"""Input for add_thought tool."""
content: str = Field(..., description="The thought content")
thought_type: str = Field(default="reflection", description="Type: reflection, question, intuition, observation, etc.")
trigger: str = Field(default="", description="What triggered this thought")
concepts: list[str] = Field(default_factory=list, description="Related concepts/tags")
privacy_level: str = Field(default="private", description="Privacy: private, shared, public")
class SearchThoughtsInput(BaseModel):
"""Input for search_thoughts tool."""
query: str = Field(..., description="Search query text")
limit: int = Field(default=10, ge=1, le=100, description="Maximum results")
thought_type_filter: str | None = Field(default=None, description="Filter by thought type")
async def add_thought_handler(input_data: AddThoughtInput) -> Dict[str, Any]:
"""
Add a new thought to Weaviate.
Args:
input_data: Thought data to add.
Returns:
Dictionary with success status and thought UUID.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get embedder
embedder = get_embedder()
# Generate vector for thought content
vector = embedder.embed_batch([input_data.content])[0]
# Get collection
collection = client.collections.get("Thought")
# Insert thought
uuid = collection.data.insert(
properties={
"content": input_data.content,
"thought_type": input_data.thought_type,
"timestamp": datetime.now(timezone.utc).isoformat(),
"trigger": input_data.trigger,
"concepts": input_data.concepts,
"privacy_level": input_data.privacy_level,
"emotional_state": "",
"context": "",
},
vector=vector.tolist()
)
return {
"success": True,
"uuid": str(uuid),
"content": input_data.content[:100] + "..." if len(input_data.content) > 100 else input_data.content,
"thought_type": input_data.thought_type,
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}
async def search_thoughts_handler(input_data: SearchThoughtsInput) -> Dict[str, Any]:
"""
Search thoughts using semantic similarity.
Args:
input_data: Search parameters.
Returns:
Dictionary with search results.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get embedder
embedder = get_embedder()
# Generate query vector
query_vector = embedder.embed_batch([input_data.query])[0]
# Get collection
collection = client.collections.get("Thought")
# Build query
query = collection.query.near_vector(
near_vector=query_vector.tolist(),
limit=input_data.limit,
)
# Apply thought_type filter if provided
if input_data.thought_type_filter:
query = query.where({
"path": ["thought_type"],
"operator": "Equal",
"valueText": input_data.thought_type_filter,
})
# Execute search
results = query.objects
# Format results
thoughts = []
for obj in results:
thoughts.append({
"uuid": str(obj.uuid),
"content": obj.properties['content'],
"thought_type": obj.properties['thought_type'],
"timestamp": obj.properties['timestamp'],
"trigger": obj.properties.get('trigger', ''),
"concepts": obj.properties.get('concepts', []),
})
return {
"success": True,
"query": input_data.query,
"results": thoughts,
"count": len(thoughts),
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}
async def get_thought_handler(uuid: str) -> Dict[str, Any]:
"""
Get a specific thought by UUID.
Args:
uuid: Thought UUID.
Returns:
Dictionary with thought data.
"""
try:
# Connect to Weaviate
client = weaviate.connect_to_local()
try:
# Get collection
collection = client.collections.get("Thought")
# Fetch by UUID
obj = collection.query.fetch_object_by_id(uuid)
if not obj:
return {
"success": False,
"error": f"Thought {uuid} not found",
}
return {
"success": True,
"uuid": str(obj.uuid),
"content": obj.properties['content'],
"thought_type": obj.properties['thought_type'],
"timestamp": obj.properties['timestamp'],
"trigger": obj.properties.get('trigger', ''),
"concepts": obj.properties.get('concepts', []),
"privacy_level": obj.properties.get('privacy_level', 'private'),
"emotional_state": obj.properties.get('emotional_state', ''),
"context": obj.properties.get('context', ''),
}
finally:
client.close()
except Exception as e:
return {
"success": False,
"error": str(e),
}