Files
patchy631--ai-engineering-hub/notebook-lm-clone/src/memory/memory_layer.py
T
2026-07-13 12:37:47 +08:00

326 lines
12 KiB
Python

import logging
import os
import time
from typing import Optional, Any, Dict, List
from dataclasses import dataclass
from datetime import datetime
from zep_cloud.client import Zep
from zep_crewai import ZepUserStorage
from crewai.memory.external.external_memory import ExternalMemory
from src.generation.rag import RAGResult
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class ConversationTurn:
"""Represents a single conversation turn with context"""
user_query: str
assistant_response: str
sources_used: List[Dict[str, Any]]
timestamp: str
session_id: str
class NotebookMemoryLayer:
def __init__(
self,
user_id: str,
session_id: str,
zep_api_key: Optional[str] = None,
mode: str = "summary",
indexing_wait_time: int = 10,
create_new_session: bool = False
):
self.user_id = user_id
self.session_id = session_id
self.indexing_wait_time = indexing_wait_time
self.zep_client = Zep(api_key=zep_api_key or os.getenv("ZEP_API_KEY"))
self._setup_user_and_session(create_new_session)
self.user_storage = ZepUserStorage(
client=self.zep_client,
user_id=self.user_id,
thread_id=self.session_id,
mode=mode,
)
self.external_memory = ExternalMemory(storage=self.user_storage)
logger.info(f"NotebookMemoryLayer initialized for user {user_id}, session {session_id}")
def _setup_user_and_session(self, create_new_session: bool):
try:
# Ensure user exists
try:
self.zep_client.user.get(self.user_id)
logger.info(f"Using existing user: {self.user_id}")
except:
self.zep_client.user.add(user_id=self.user_id)
logger.info(f"Created new user: {self.user_id}")
if create_new_session:
try:
self.zep_client.thread.delete(self.session_id)
logger.info(f"Deleted previous session: {self.session_id}")
except:
pass
self.zep_client.thread.create(thread_id=self.session_id, user_id=self.user_id)
logger.info(f"Created new session: {self.session_id}")
else:
# Try to use existing session, create if doesn't exist
try:
self.zep_client.thread.get(self.session_id)
logger.info(f"Using existing session: {self.session_id}")
except:
self.zep_client.thread.create(thread_id=self.session_id, user_id=self.user_id)
logger.info(f"Created session: {self.session_id}")
except Exception as e:
logger.error(f"Error setting up user/session: {str(e)}")
raise
def save_conversation_turn(
self,
rag_result: RAGResult,
user_metadata: Optional[Dict[str, Any]] = None,
assistant_metadata: Optional[Dict[str, Any]] = None
):
try:
user_meta = {
"type": "message",
"role": "user",
"timestamp": datetime.now().isoformat(),
"session_id": self.session_id,
**(user_metadata or {})
}
# Save user message
self.external_memory.save(
rag_result.query,
metadata=user_meta
)
assistant_meta = {
"type": "message",
"role": "assistant",
"timestamp": datetime.now().isoformat(),
"session_id": self.session_id,
"sources_count": len(rag_result.sources_used),
"retrieval_count": rag_result.retrieval_count,
"model_used": getattr(rag_result, 'model_name', 'unknown'),
"sources_summary": self._create_sources_summary(rag_result.sources_used),
**(assistant_metadata or {})
}
# Save assistant response
self.external_memory.save(
rag_result.response,
metadata=assistant_meta
)
self._save_source_context(rag_result.sources_used)
logger.info(f"Saved conversation turn with {len(rag_result.sources_used)} sources")
except Exception as e:
logger.error(f"Error saving conversation turn: {str(e)}")
raise
def _create_sources_summary(self, sources_used: List[Dict[str, Any]]) -> str:
if not sources_used:
return "No sources used"
source_files = list(set(source.get('source_file', 'Unknown') for source in sources_used))
source_types = list(set(source.get('source_type', 'unknown') for source in sources_used))
summary = f"{len(source_files)} files ({', '.join(source_types)}): {', '.join(source_files[:3])}"
if len(source_files) > 3:
summary += f" and {len(source_files) - 3} more"
return summary
def _save_source_context(self, sources_used: List[Dict[str, Any]]):
if not sources_used:
return
source_context = {
"referenced_documents": [],
"document_types": set(),
"key_topics_discussed": []
}
for source in sources_used:
doc_info = {
"file": source.get('source_file', 'Unknown'),
"type": source.get('source_type', 'unknown'),
"page": source.get('page_number'),
"relevance": source.get('relevance_score', 0)
}
source_context["referenced_documents"].append(doc_info)
source_context["document_types"].add(doc_info["type"])
source_context["document_types"] = list(source_context["document_types"])
self.external_memory.save(
f"Document sources referenced: {source_context}",
metadata={
"type": "source_context",
"category": "document_usage",
"session_id": self.session_id
}
)
def save_user_preferences(self, preferences: Dict[str, Any]):
try:
self.external_memory.save(
f"User preferences: {preferences}",
metadata={
"type": "preferences",
"category": "user_settings",
"timestamp": datetime.now().isoformat(),
"session_id": self.session_id
}
)
logger.info("User preferences saved to memory")
except Exception as e:
logger.error(f"Error saving preferences: {str(e)}")
def save_document_metadata(self, document_info: Dict[str, Any]):
try:
self.external_memory.save(
f"Document processed: {document_info}",
metadata={
"type": "document_metadata",
"category": "system_events",
"timestamp": datetime.now().isoformat(),
"session_id": self.session_id
}
)
logger.info(f"Document metadata saved: {document_info.get('name', 'Unknown')}")
except Exception as e:
logger.error(f"Error saving document metadata: {str(e)}")
def get_conversation_context(self) -> str:
try:
memory = self.zep_client.thread.get_user_context(thread_id=self.session_id)
return memory.context if memory.context else ""
except Exception as e:
logger.error(f"Error getting conversation context: {str(e)}")
return "No conversation context available"
def get_relevant_memory(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
try:
# Use Zep's semantic graph search on memory
results = self.zep_client.graph.search(
user_id=self.user_id,
query=query,
scope="episodes",
)
relevant_memories = []
for ep in results.episodes:
memory_info = {
"content": ep.content if ep.content else "",
"role": ep.role_type if ep.role_type else "unknown",
"relevance_score": ep.score if hasattr(ep, 'score') else 0,
"thread_id": ep.thread_id if ep.thread_id else None,
"session_id": ep.session_id if ep.session_id else None,
"timestamp": ep.created_at if ep.created_at else None,
}
relevant_memories.append(memory_info)
logger.info(f"Retrieved {len(relevant_memories)} relevant memories for query")
return relevant_memories
except Exception as e:
logger.error(f"Error getting relevant memory: {str(e)}")
return []
def wait_for_indexing(self):
logger.info(f"Waiting {self.indexing_wait_time}s for Zep indexing...")
time.sleep(self.indexing_wait_time)
def get_session_summary(self) -> Dict[str, Any]:
try:
messages = self.zep_client.thread.get(thread_id=self.session_id)
if not messages or not messages.messages:
return {"message_count": 0, "summary": "No messages in session"}
user_messages = [m for m in messages.messages if m.role == "user"]
assistant_messages = [m for m in messages.messages if m.role == "assistant"]
summary = {
"session_id": self.session_id,
"user_id": self.user_id,
"total_messages": len(messages.messages),
"user_messages": len(user_messages),
"assistant_messages": len(assistant_messages),
"context_available": bool(self.get_conversation_context()),
"last_interaction": messages.messages[0].created_at if messages.messages else None
}
return summary
except Exception as e:
logger.error(f"Error getting session summary: {str(e)}")
return {"error": str(e)}
def clear_session(self):
try:
self.zep_client.thread.delete(self.session_id)
self.zep_client.thread.create(thread_id=self.session_id, user_id=self.user_id)
logger.info(f"Session {self.session_id} cleared and recreated")
except Exception as e:
logger.error(f"Error clearing session: {str(e)}")
raise
if __name__ == "__main__":
from src.generation.rag import RAGGenerator, RAGResult
memory = NotebookMemoryLayer(
user_id="test_user",
session_id="test_session_123",
create_new_session=True
)
try:
mock_rag_result = RAGResult(
query="What are the main findings in the research?",
response="The research shows three key findings [1, 2]. First, the methodology was effective [1]. Second, the results were significant [2].",
sources_used=[
{"source_file": "research_paper.pdf", "source_type": "pdf", "page_number": 5},
{"source_file": "data_analysis.pdf", "source_type": "pdf", "page_number": 12}
],
retrieval_count=8
)
memory.save_conversation_turn(mock_rag_result)
memory.wait_for_indexing()
context = memory.get_conversation_context()
print(f"Conversation Context:\n{context}")
relevant = memory.get_relevant_memory("research findings")
print(f"\nRelevant Memories: {len(relevant)} found")
summary = memory.get_session_summary()
print(f"\nSession Summary: {summary}")
memory.save_user_preferences({
"response_length": "detailed",
"citation_style": "academic",
"preferred_sources": ["pdf", "web"]
})
print("Memory integration test completed successfully")
except Exception as e:
print(f"Error in memory integration test: {e}")