| | """Memory Layer Implementation for LangGraph Agent System""" |
| | import os |
| | import time |
| | import hashlib |
| | import sqlite3 |
| | from typing import Optional, List, Dict, Any, Tuple |
| | from langchain_community.vectorstores import SupabaseVectorStore |
| | from langchain_huggingface import HuggingFaceEmbeddings |
| | from supabase.client import Client, create_client |
| | from langgraph.checkpoint.sqlite import SqliteSaver |
| | from langchain_core.messages import BaseMessage, HumanMessage |
| |
|
| |
|
| | |
| | TTL = 300 |
| | SIMILARITY_THRESHOLD = 0.85 |
| |
|
| |
|
| | class MemoryManager: |
| | """Manages short-term, long-term memory and checkpointing for the agent system""" |
| | |
| | def __init__(self): |
| | self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2") |
| | self.vector_store = None |
| | self.checkpointer = None |
| | self._sqlite_connection = None |
| | |
| | |
| | self.query_cache: Dict[str, Tuple[float, List]] = {} |
| | self.processed_tasks: set[str] = set() |
| | self.seen_hashes: set[str] = set() |
| | |
| | self._initialize_vector_store() |
| | self._initialize_checkpointer() |
| | |
| | def _initialize_vector_store(self) -> None: |
| | """Initialize Supabase vector store for long-term memory""" |
| | try: |
| | supabase_url = os.environ.get("SUPABASE_URL") |
| | supabase_key = os.environ.get("SUPABASE_SERVICE_KEY") |
| | |
| | if not supabase_url or not supabase_key: |
| | print("Warning: Supabase credentials not found, vector store will be disabled") |
| | return |
| | |
| | supabase: Client = create_client(supabase_url, supabase_key) |
| | self.vector_store = SupabaseVectorStore( |
| | client=supabase, |
| | embedding=self.embeddings, |
| | table_name="documents", |
| | query_name="match_documents_langchain", |
| | ) |
| | print("Vector store initialized successfully") |
| | except Exception as e: |
| | print(f"Warning: Could not initialize Supabase vector store: {e}") |
| | |
| | def _initialize_checkpointer(self) -> None: |
| | """Initialize SQLite checkpointer for short-term memory""" |
| | try: |
| | |
| | self._sqlite_connection = sqlite3.connect(":memory:", check_same_thread=False) |
| | self.checkpointer = SqliteSaver(self._sqlite_connection) |
| | print("Checkpointer initialized successfully") |
| | except Exception as e: |
| | print(f"Warning: Could not initialize checkpointer: {e}") |
| | |
| | def get_checkpointer(self) -> Optional[SqliteSaver]: |
| | """Get the checkpointer instance""" |
| | return self.checkpointer |
| | |
| | def close_checkpointer(self) -> None: |
| | """Close the checkpointer and its SQLite connection""" |
| | if self._sqlite_connection: |
| | try: |
| | self._sqlite_connection.close() |
| | print("SQLite connection closed") |
| | except Exception as e: |
| | print(f"Warning: Error closing SQLite connection: {e}") |
| | |
| | def similarity_search(self, query: str, k: int = 2) -> List[Any]: |
| | """Search for similar questions with caching""" |
| | if not self.vector_store: |
| | return [] |
| | |
| | |
| | q_hash = hashlib.sha256(query.encode()).hexdigest() |
| | now = time.time() |
| | |
| | if q_hash in self.query_cache and now - self.query_cache[q_hash][0] < TTL: |
| | print("Memory: Cache hit for similarity search") |
| | return self.query_cache[q_hash][1] |
| | |
| | try: |
| | print("Memory: Searching vector store for similar questions...") |
| | similar_questions = self.vector_store.similarity_search_with_relevance_scores(query, k=k) |
| | self.query_cache[q_hash] = (now, similar_questions) |
| | return similar_questions |
| | except Exception as e: |
| | print(f"Memory: Vector store search error – {e}") |
| | return [] |
| | |
| | def should_ingest(self, query: str) -> bool: |
| | """Determine if this query/answer should be ingested to long-term memory""" |
| | if not self.vector_store: |
| | return False |
| | |
| | similar_questions = self.similarity_search(query, k=1) |
| | top_score = similar_questions[0][1] if similar_questions else 0.0 |
| | return top_score < SIMILARITY_THRESHOLD |
| | |
| | def ingest_qa_pair(self, question: str, answer: str, attachments: str = "") -> None: |
| | """Store Q/A pair in long-term memory""" |
| | if not self.vector_store: |
| | print("Memory: Vector store not available for ingestion") |
| | return |
| | |
| | try: |
| | payload = f"Question:\n{question}\n\nAnswer:\n{answer}" |
| | if attachments: |
| | payload += f"\n\n{attachments}" |
| | |
| | hash_id = hashlib.sha256(payload.encode()).hexdigest() |
| | if hash_id in self.seen_hashes: |
| | print("Memory: Duplicate payload within session – skip") |
| | return |
| | |
| | self.seen_hashes.add(hash_id) |
| | self.vector_store.add_texts( |
| | [payload], |
| | metadatas=[{"hash_id": hash_id, "timestamp": time.time()}] |
| | ) |
| | print("Memory: Stored new Q/A pair in vector store") |
| | except Exception as e: |
| | print(f"Memory: Error while upserting – {e}") |
| | |
| | def get_similar_qa(self, query: str) -> Optional[str]: |
| | """Get similar Q/A for context""" |
| | similar_questions = self.similarity_search(query, k=1) |
| | if not similar_questions: |
| | return None |
| | |
| | example_doc = similar_questions[0][0] if isinstance(similar_questions[0], tuple) else similar_questions[0] |
| | return example_doc.page_content |
| | |
| | def add_processed_task(self, task_id: str) -> None: |
| | """Mark a task as processed to avoid re-downloading attachments""" |
| | self.processed_tasks.add(task_id) |
| | |
| | def is_task_processed(self, task_id: str) -> bool: |
| | """Check if a task has already been processed""" |
| | return task_id in self.processed_tasks |
| | |
| | def clear_session_cache(self) -> None: |
| | """Clear session-specific caches""" |
| | self.query_cache.clear() |
| | self.processed_tasks.clear() |
| | self.seen_hashes.clear() |
| | print("Memory: Session cache cleared") |
| |
|
| |
|
| | |
| | memory_manager = MemoryManager() |