| """ |
| mem0 client wrapper. |
| |
| Responsibility boundary (do not blur this): |
| - mem0 -> user preferences + long-term conversational memory |
| (e.g. "prefers beginner explanations", "likes step-by-step examples") |
| - ChromaDB -> DSA knowledge embeddings (rag/retriever.py) β NEVER touched here |
| - SQLite -> raw message/session transcripts (history/) β NEVER touched here |
| |
| mem0 runs in local/open-source mode: a local Chroma vector store (separate |
| collection from the knowledge base) + a local sentence-transformers |
| embedder (same model as the KB for consistency, configurable independently |
| if needed). |
| |
| Fact extraction (infer=True) would use an LLM to summarize what to |
| remember. To avoid depending on the (expensive, 4-bit-quantized) local |
| Mistral model just to log a preference, this wrapper uses infer=False by |
| default β messages are stored as-is and semantic search over them still |
| works for retrieval. Set infer=True if you want mem0 to run LLM-based |
| fact extraction using the same provider configured in llm/generate.py. |
| """ |
|
|
| import os |
|
|
| import config |
| from logs.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
| _MEM0_COLLECTION_NAME = "user_preferences_memory" |
|
|
|
|
| def _build_config_dict() -> dict: |
| os.makedirs(config.MEM0_LOCAL_STORAGE_DIR, exist_ok=True) |
| return { |
| "vector_store": { |
| "provider": "chroma", |
| "config": { |
| "collection_name": _MEM0_COLLECTION_NAME, |
| "path": os.path.join(config.MEM0_LOCAL_STORAGE_DIR, "chroma_db"), |
| }, |
| }, |
| "embedder": { |
| "provider": "huggingface", |
| "config": { |
| "model": f"sentence-transformers/{config.EMBEDDING_MODEL_NAME}", |
| }, |
| }, |
| "history_db_path": os.path.join(config.MEM0_LOCAL_STORAGE_DIR, "mem0_history.db"), |
| } |
|
|
|
|
| class Mem0Client: |
| """ |
| Thin wrapper around mem0.Memory scoped to a single responsibility: |
| user preferences and long-term conversational memory. |
| """ |
|
|
| def __init__(self, infer: bool = False): |
| self.infer = infer |
| self._memory = None |
| self._enabled = config.USE_MEM0 |
|
|
| if not self._enabled: |
| logger.info("USE_MEM0 is False β Mem0Client will be a no-op.") |
|
|
| def _get_memory(self): |
| if self._memory is None: |
| from mem0 import Memory |
|
|
| logger.info("Initializing mem0 (local chroma + huggingface embedder)...") |
| self._memory = Memory.from_config(_build_config_dict()) |
| return self._memory |
|
|
| def add_interaction(self, user_id: str, user_message: str, assistant_message: str = None) -> None: |
| """ |
| Store a turn of conversation for long-term memory purposes (NOT |
| transcript storage β that's history/service.py + SQLite). |
| |
| Only call this for things worth remembering long-term (preferences, |
| recurring topics of interest, stated skill level) β not every raw |
| message. The router/chat layer decides when this is worth calling; |
| this wrapper doesn't filter content itself. |
| """ |
| if not self._enabled: |
| return |
|
|
| messages = [{"role": "user", "content": user_message}] |
| if assistant_message: |
| messages.append({"role": "assistant", "content": assistant_message}) |
|
|
| try: |
| self._get_memory().add(messages, user_id=user_id, infer=self.infer) |
| except Exception: |
| logger.exception("mem0 add_interaction failed for user_id=%s", user_id) |
|
|
| def get_relevant_context(self, user_id: str, query: str, limit: int = 5) -> list: |
| """ |
| Returns a list of plain-text memory strings relevant to `query`, |
| ready to hand to llm/prompts.py's mem0_context parameter. |
| |
| Returns [] if mem0 is disabled, the user has no memories yet, or a |
| lookup error occurs β callers should treat that as "no memory |
| context available", not as an error condition. |
| """ |
| if not self._enabled: |
| return [] |
|
|
| try: |
| results = self._get_memory().search(query, user_id=user_id, limit=limit) |
| except Exception: |
| logger.exception("mem0 get_relevant_context failed for user_id=%s", user_id) |
| return [] |
|
|
| |
| |
| items = results.get("results", results) if isinstance(results, dict) else results |
| return [item.get("memory", "") for item in items if item.get("memory")] |
|
|
| def delete_all_for_user(self, user_id: str) -> None: |
| """Useful for account deletion / a 'forget me' feature.""" |
| if not self._enabled: |
| return |
| try: |
| self._get_memory().delete_all(user_id=user_id) |
| except Exception: |
| logger.exception("mem0 delete_all_for_user failed for user_id=%s", user_id) |
|
|
|
|
| _CLIENT_SINGLETON = None |
|
|
|
|
| def get_mem0_client() -> Mem0Client: |
| global _CLIENT_SINGLETON |
| if _CLIENT_SINGLETON is None: |
| _CLIENT_SINGLETON = Mem0Client() |
| return _CLIENT_SINGLETON |
|
|