Spaces:
Sleeping
Sleeping
Commit
·
aa9003d
1
Parent(s):
53cf39f
Enhance memo and refactor to subfiles
Browse files- app.py +75 -34
- memo/README.md +342 -0
- memo/REFACTORING_COMPLETE.md +0 -0
- memo/context.py +86 -0
- memo/core.py +247 -0
- memo/history.py +73 -89
- memo/{memory.py → legacy.py} +20 -8
- memo/nvidia.py +121 -0
- memo/persistent.py +199 -0
app.py
CHANGED
|
@@ -344,12 +344,12 @@ async def delete_chat_history(user_id: str, project_id: str):
|
|
| 344 |
logger.info(f"[CHAT] Cleared history for user {user_id} project {project_id}")
|
| 345 |
# Also clear in-memory LRU for this user to avoid stale context
|
| 346 |
try:
|
| 347 |
-
from memo.
|
| 348 |
-
memory =
|
| 349 |
memory.clear(user_id)
|
| 350 |
-
logger.info(f"[CHAT] Cleared
|
| 351 |
except Exception as me:
|
| 352 |
-
logger.warning(f"[CHAT] Failed to clear
|
| 353 |
return MessageResponse(message="Chat history cleared")
|
| 354 |
except Exception as e:
|
| 355 |
raise HTTPException(500, detail=f"Failed to clear chat history: {str(e)}")
|
|
@@ -776,10 +776,9 @@ async def _chat_impl(
|
|
| 776 |
- After answering, summarize (q,a) via NVIDIA and store into LRU (last 20)
|
| 777 |
"""
|
| 778 |
import sys
|
| 779 |
-
from memo.
|
| 780 |
-
from memo.history import summarize_qa_with_nvidia, files_relevance, related_recent_and_semantic_context
|
| 781 |
from utils.router import NVIDIA_SMALL # reuse default name
|
| 782 |
-
memory =
|
| 783 |
logger.info("[CHAT] User Q/chat: %s", trim_text(question, 15).replace("\n", " "))
|
| 784 |
|
| 785 |
# 0) Detect any filenames mentioned in the question (e.g., JADE.pdf)
|
|
@@ -834,7 +833,9 @@ async def _chat_impl(
|
|
| 834 |
|
| 835 |
# 1b) Ask NVIDIA to mark relevance per file
|
| 836 |
try:
|
| 837 |
-
|
|
|
|
|
|
|
| 838 |
relevant_files = [fn for fn, ok in relevant_map.items() if ok]
|
| 839 |
logger.info(f"[CHAT] NVIDIA relevant files: {relevant_files}")
|
| 840 |
except Exception as e:
|
|
@@ -850,32 +851,56 @@ async def _chat_impl(
|
|
| 850 |
logger.info(f"[CHAT] Forced-include mentioned files into relevance: {extra}")
|
| 851 |
|
| 852 |
# 2) Memory context: recent 3 via NVIDIA, remaining 17 via semantic
|
| 853 |
-
#
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
recent_related = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
|
| 880 |
# 3) RAG vector search (restricted to relevant files if any)
|
| 881 |
logger.info(f"[CHAT] Starting vector search with relevant_files={relevant_files}")
|
|
@@ -1006,8 +1031,24 @@ async def _chat_impl(
|
|
| 1006 |
answer = "I had trouble contacting the language model provider just now. Please try again."
|
| 1007 |
# After answering: summarize QA and store in memory (LRU, last 20)
|
| 1008 |
try:
|
| 1009 |
-
|
|
|
|
|
|
|
| 1010 |
memory.add(user_id, qa_sum)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1011 |
except Exception as e:
|
| 1012 |
logger.warning(f"QA summarize/store failed: {e}")
|
| 1013 |
# Trim for logging
|
|
|
|
| 344 |
logger.info(f"[CHAT] Cleared history for user {user_id} project {project_id}")
|
| 345 |
# Also clear in-memory LRU for this user to avoid stale context
|
| 346 |
try:
|
| 347 |
+
from memo.core import get_memory_system
|
| 348 |
+
memory = get_memory_system()
|
| 349 |
memory.clear(user_id)
|
| 350 |
+
logger.info(f"[CHAT] Cleared memory for user {user_id}")
|
| 351 |
except Exception as me:
|
| 352 |
+
logger.warning(f"[CHAT] Failed to clear memory for user {user_id}: {me}")
|
| 353 |
return MessageResponse(message="Chat history cleared")
|
| 354 |
except Exception as e:
|
| 355 |
raise HTTPException(500, detail=f"Failed to clear chat history: {str(e)}")
|
|
|
|
| 776 |
- After answering, summarize (q,a) via NVIDIA and store into LRU (last 20)
|
| 777 |
"""
|
| 778 |
import sys
|
| 779 |
+
from memo.core import get_memory_system
|
|
|
|
| 780 |
from utils.router import NVIDIA_SMALL # reuse default name
|
| 781 |
+
memory = get_memory_system()
|
| 782 |
logger.info("[CHAT] User Q/chat: %s", trim_text(question, 15).replace("\n", " "))
|
| 783 |
|
| 784 |
# 0) Detect any filenames mentioned in the question (e.g., JADE.pdf)
|
|
|
|
| 833 |
|
| 834 |
# 1b) Ask NVIDIA to mark relevance per file
|
| 835 |
try:
|
| 836 |
+
from memo.history import get_history_manager
|
| 837 |
+
history_manager = get_history_manager(memory)
|
| 838 |
+
relevant_map = await history_manager.files_relevance(question, files_list, nvidia_rotator)
|
| 839 |
relevant_files = [fn for fn, ok in relevant_map.items() if ok]
|
| 840 |
logger.info(f"[CHAT] NVIDIA relevant files: {relevant_files}")
|
| 841 |
except Exception as e:
|
|
|
|
| 851 |
logger.info(f"[CHAT] Forced-include mentioned files into relevance: {extra}")
|
| 852 |
|
| 853 |
# 2) Memory context: recent 3 via NVIDIA, remaining 17 via semantic
|
| 854 |
+
# Use enhanced context retrieval if available, otherwise fallback to original method
|
| 855 |
+
try:
|
| 856 |
+
from memo.history import get_history_manager
|
| 857 |
+
history_manager = get_history_manager(memory)
|
| 858 |
+
recent_related, semantic_related = await history_manager.related_recent_and_semantic_context(
|
| 859 |
+
user_id, question, embedder
|
| 860 |
+
)
|
| 861 |
+
except Exception as e:
|
| 862 |
+
logger.warning(f"[CHAT] Enhanced context retrieval failed, using fallback: {e}")
|
| 863 |
+
# Fallback to original method
|
| 864 |
+
recent3 = memory.recent(user_id, 3)
|
| 865 |
+
if recent3:
|
| 866 |
+
sys = "Pick only items that directly relate to the new question. Output the selected items verbatim, no commentary. If none, output nothing."
|
| 867 |
+
numbered = [{"id": i+1, "text": s} for i, s in enumerate(recent3)]
|
| 868 |
+
user = f"Question: {question}\nCandidates:\n{json.dumps(numbered, ensure_ascii=False)}\nSelect any related items and output ONLY their 'text' values concatenated."
|
| 869 |
+
try:
|
| 870 |
+
from utils.rotator import robust_post_json
|
| 871 |
+
key = nvidia_rotator.get_key()
|
| 872 |
+
url = "https://integrate.api.nvidia.com/v1/chat/completions"
|
| 873 |
+
payload = {
|
| 874 |
+
"model": os.getenv("NVIDIA_SMALL", "meta/llama-3.1-8b-instruct"),
|
| 875 |
+
"temperature": 0.0,
|
| 876 |
+
"messages": [
|
| 877 |
+
{"role": "system", "content": sys},
|
| 878 |
+
{"role": "user", "content": user},
|
| 879 |
+
]
|
| 880 |
+
}
|
| 881 |
+
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {key or ''}"}
|
| 882 |
+
data = await robust_post_json(url, headers, payload, nvidia_rotator)
|
| 883 |
+
recent_related = data["choices"][0]["message"]["content"].strip()
|
| 884 |
+
except Exception as e:
|
| 885 |
+
logger.warning(f"Recent-related NVIDIA error: {e}")
|
| 886 |
+
recent_related = ""
|
| 887 |
+
else:
|
| 888 |
recent_related = ""
|
| 889 |
+
|
| 890 |
+
# Get semantic context from remaining memories
|
| 891 |
+
rest17 = memory.rest(user_id, 3)
|
| 892 |
+
if rest17:
|
| 893 |
+
import numpy as np
|
| 894 |
+
def _cosine(a: np.ndarray, b: np.ndarray) -> float:
|
| 895 |
+
denom = (np.linalg.norm(a) * np.linalg.norm(b)) or 1.0
|
| 896 |
+
return float(np.dot(a, b) / denom)
|
| 897 |
+
|
| 898 |
+
qv = np.array(embedder.embed([question])[0], dtype="float32")
|
| 899 |
+
mats = embedder.embed([s.strip() for s in rest17])
|
| 900 |
+
sims = [(_cosine(qv, np.array(v, dtype="float32")), s) for v, s in zip(mats, rest17)]
|
| 901 |
+
sims.sort(key=lambda x: x[0], reverse=True)
|
| 902 |
+
top = [s for (sc, s) in sims[:3] if sc > 0.15]
|
| 903 |
+
semantic_related = "\n\n".join(top) if top else ""
|
| 904 |
|
| 905 |
# 3) RAG vector search (restricted to relevant files if any)
|
| 906 |
logger.info(f"[CHAT] Starting vector search with relevant_files={relevant_files}")
|
|
|
|
| 1031 |
answer = "I had trouble contacting the language model provider just now. Please try again."
|
| 1032 |
# After answering: summarize QA and store in memory (LRU, last 20)
|
| 1033 |
try:
|
| 1034 |
+
from memo.history import get_history_manager
|
| 1035 |
+
history_manager = get_history_manager(memory)
|
| 1036 |
+
qa_sum = await history_manager.summarize_qa_with_nvidia(question, answer, nvidia_rotator)
|
| 1037 |
memory.add(user_id, qa_sum)
|
| 1038 |
+
|
| 1039 |
+
# Also store enhanced conversation memory if available
|
| 1040 |
+
if memory.is_enhanced_available():
|
| 1041 |
+
await memory.add_conversation_memory(
|
| 1042 |
+
user_id=user_id,
|
| 1043 |
+
question=question,
|
| 1044 |
+
answer=answer,
|
| 1045 |
+
project_id=project_id,
|
| 1046 |
+
context={
|
| 1047 |
+
"relevant_files": relevant_files,
|
| 1048 |
+
"sources_count": len(sources_meta),
|
| 1049 |
+
"timestamp": time.time()
|
| 1050 |
+
}
|
| 1051 |
+
)
|
| 1052 |
except Exception as e:
|
| 1053 |
logger.warning(f"QA summarize/store failed: {e}")
|
| 1054 |
# Trim for logging
|
memo/README.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Memory System for EdSummariser
|
| 2 |
+
|
| 3 |
+
This directory contains a clean, modular memory and history management system for the EdSummariser application, designed to provide superior chat continuity and context awareness while maintaining simplicity and efficiency.
|
| 4 |
+
|
| 5 |
+
## 🚀 Features
|
| 6 |
+
|
| 7 |
+
### Core Memory Types
|
| 8 |
+
- **Conversation Memory**: Stores and retrieves chat history with intelligent summarization
|
| 9 |
+
- **Enhanced Memory**: MongoDB-based persistent storage with semantic search (when available)
|
| 10 |
+
- **Legacy Memory**: In-memory LRU system for backward compatibility
|
| 11 |
+
|
| 12 |
+
### Key Capabilities
|
| 13 |
+
- **Backward Compatibility**: All existing code works unchanged
|
| 14 |
+
- **Enhanced Features**: MongoDB persistence and semantic search when available
|
| 15 |
+
- **Graceful Fallback**: Falls back to legacy system if MongoDB unavailable
|
| 16 |
+
- **Zero Breaking Changes**: No modifications required to existing code
|
| 17 |
+
- **Modular Design**: Clean separation of concerns across files
|
| 18 |
+
|
| 19 |
+
## 📁 File Structure
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
memo/
|
| 23 |
+
├── README.md # This documentation
|
| 24 |
+
├── core.py # Main memory system Legacy memory
|
| 25 |
+
├── legacy.py # Legacy in-memory LRU system
|
| 26 |
+
├── persistent.py # MongoDB-based persistent storage
|
| 27 |
+
├── nvidia.py # NVIDIA API integration
|
| 28 |
+
├── context.py # Context retrieval and management
|
| 29 |
+
└── history.py # History management functions
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## 🔧 Installation & Setup
|
| 33 |
+
|
| 34 |
+
### Prerequisites
|
| 35 |
+
- MongoDB instance (local or cloud)
|
| 36 |
+
- Python 3.8+
|
| 37 |
+
- Required dependencies (see requirements.txt)
|
| 38 |
+
|
| 39 |
+
### Environment Variables
|
| 40 |
+
```bash
|
| 41 |
+
MONGO_URI=mongodb://localhost:27017
|
| 42 |
+
MONGO_DB=enhanced_memory
|
| 43 |
+
EMBED_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
| 44 |
+
NVIDIA_SMALL=meta/llama-3.1-8b-instruct
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Quick Start
|
| 48 |
+
```python
|
| 49 |
+
from memo.core import get_memory_system
|
| 50 |
+
|
| 51 |
+
# Initialize the memory system
|
| 52 |
+
memory = get_memory_system()
|
| 53 |
+
|
| 54 |
+
# Check if enhanced features are available
|
| 55 |
+
if memory.is_enhanced_available():
|
| 56 |
+
print("Enhanced memory system is ready!")
|
| 57 |
+
else:
|
| 58 |
+
print("Using legacy memory system")
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## 📖 Usage Examples
|
| 62 |
+
|
| 63 |
+
### Basic Memory Operations
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
from memo.core import get_memory_system
|
| 67 |
+
from memo.history import get_history_manager
|
| 68 |
+
|
| 69 |
+
memory = get_memory_system()
|
| 70 |
+
history_manager = get_history_manager(memory)
|
| 71 |
+
|
| 72 |
+
# Add conversation memory (legacy compatibility)
|
| 73 |
+
memory.add("user123", "q: What is AI?\na: AI is artificial intelligence")
|
| 74 |
+
|
| 75 |
+
# Add enhanced conversation memory (when MongoDB available)
|
| 76 |
+
memory_id = await memory.add_conversation_memory(
|
| 77 |
+
user_id="user123",
|
| 78 |
+
question="How do I implement authentication?",
|
| 79 |
+
answer="You can use JWT tokens with FastAPI...",
|
| 80 |
+
project_id="my_project",
|
| 81 |
+
context={"topic": "authentication", "difficulty": "intermediate"}
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Get recent memories
|
| 85 |
+
recent_memories = memory.recent("user123", n=5)
|
| 86 |
+
|
| 87 |
+
# Search memories semantically
|
| 88 |
+
search_results = await memory.search_memories(
|
| 89 |
+
user_id="user123",
|
| 90 |
+
query="authentication best practices",
|
| 91 |
+
limit=10
|
| 92 |
+
)
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### Advanced Features
|
| 96 |
+
|
| 97 |
+
```python
|
| 98 |
+
# Add user preferences
|
| 99 |
+
await memory_manager.add_user_preference(
|
| 100 |
+
user_id="user123",
|
| 101 |
+
preference="Prefers detailed explanations with code examples",
|
| 102 |
+
context={"communication_style": "detailed"}
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Add project context
|
| 106 |
+
await memory_manager.add_project_context(
|
| 107 |
+
user_id="user123",
|
| 108 |
+
project_id="my_project",
|
| 109 |
+
context="FastAPI application with JWT auth and PostgreSQL",
|
| 110 |
+
importance=MemoryImportance.HIGH
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# Get comprehensive conversation context
|
| 114 |
+
recent_context, semantic_context = await memory_manager.get_conversation_context(
|
| 115 |
+
user_id="user123",
|
| 116 |
+
question="How do I handle database migrations?",
|
| 117 |
+
project_id="my_project"
|
| 118 |
+
)
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### Conversation Management
|
| 122 |
+
|
| 123 |
+
```python
|
| 124 |
+
from memo.enhanced_history import ConversationManager
|
| 125 |
+
|
| 126 |
+
# Initialize conversation manager
|
| 127 |
+
conversation_manager = ConversationManager(
|
| 128 |
+
memory_system=memory_manager.enhanced_memory,
|
| 129 |
+
nvidia_rotator=nvidia_rotator,
|
| 130 |
+
embedder=embedder
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# Process conversation turn with enhanced context
|
| 134 |
+
memory_id = await conversation_manager.process_conversation_turn(
|
| 135 |
+
user_id="user123",
|
| 136 |
+
question="What's the best way to structure my code?",
|
| 137 |
+
answer="Follow the single responsibility principle...",
|
| 138 |
+
project_id="my_project",
|
| 139 |
+
context={"files": ["main.py", "auth.py"]}
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# Get conversation summary
|
| 143 |
+
summary = await conversation_manager.create_conversation_summary(
|
| 144 |
+
user_id="user123",
|
| 145 |
+
project_id="my_project"
|
| 146 |
+
)
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
## 🔄 Migration from Legacy System
|
| 150 |
+
|
| 151 |
+
The enhanced memory system is designed to be backward compatible. Here's how to migrate:
|
| 152 |
+
|
| 153 |
+
### Step 1: Update Imports
|
| 154 |
+
```python
|
| 155 |
+
# OLD
|
| 156 |
+
from memo.memory import MemoryLRU
|
| 157 |
+
|
| 158 |
+
# NEW
|
| 159 |
+
from memo.memory_integration import MemoryIntegrationManager
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
### Step 2: Initialize Memory Manager
|
| 163 |
+
```python
|
| 164 |
+
# OLD
|
| 165 |
+
memory = MemoryLRU()
|
| 166 |
+
|
| 167 |
+
# NEW
|
| 168 |
+
memory_manager = MemoryIntegrationManager(mongo_uri, db_name)
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
### Step 3: Update Memory Operations
|
| 172 |
+
```python
|
| 173 |
+
# OLD
|
| 174 |
+
memory.add(user_id, qa_summary)
|
| 175 |
+
recent = memory.recent(user_id, 3)
|
| 176 |
+
rest = memory.rest(user_id, 3)
|
| 177 |
+
|
| 178 |
+
# NEW
|
| 179 |
+
await memory_manager.add_conversation_memory(user_id, question, answer, project_id)
|
| 180 |
+
recent_context, semantic_context = await memory_manager.get_conversation_context(
|
| 181 |
+
user_id, question, project_id
|
| 182 |
+
)
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
## 🏗️ Architecture
|
| 186 |
+
|
| 187 |
+
### Memory Layers
|
| 188 |
+
1. **Short-term Memory**: Recent conversation context (last 10-20 exchanges)
|
| 189 |
+
2. **Long-term Memory**: Persistent storage with semantic indexing
|
| 190 |
+
3. **User Memory**: Preferences, goals, and personality traits
|
| 191 |
+
4. **Project Memory**: Project-specific context and knowledge
|
| 192 |
+
5. **Session Memory**: Active conversation state and threading
|
| 193 |
+
|
| 194 |
+
### Data Flow
|
| 195 |
+
```
|
| 196 |
+
User Question → Context Composition → Memory Retrieval → LLM Processing → Memory Storage
|
| 197 |
+
↓ ↓ ↓ ↓ ↓
|
| 198 |
+
Recent Context → Semantic Search → Enhanced Context → Response → Memory Update
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
## 🔍 Memory Types
|
| 202 |
+
|
| 203 |
+
| Type | Description | Use Case |
|
| 204 |
+
|------|-------------|----------|
|
| 205 |
+
| `CONVERSATION` | Chat history and Q&A pairs | Context continuity |
|
| 206 |
+
| `USER_PREFERENCE` | User communication style and preferences | Personalization |
|
| 207 |
+
| `PROJECT_CONTEXT` | Project-specific knowledge | Domain awareness |
|
| 208 |
+
| `SESSION_STATE` | Active conversation state | Session management |
|
| 209 |
+
| `KNOWLEDGE_FACT` | Domain-specific facts | Knowledge base |
|
| 210 |
+
| `GOAL_OBJECTIVE` | User goals and objectives | Goal tracking |
|
| 211 |
+
|
| 212 |
+
## 📊 Memory Importance Levels
|
| 213 |
+
|
| 214 |
+
| Level | Description | Retention |
|
| 215 |
+
|-------|-------------|-----------|
|
| 216 |
+
| `CRITICAL` | Essential user preferences, project goals | Permanent |
|
| 217 |
+
| `HIGH` | Important context, user patterns | Long-term |
|
| 218 |
+
| `MEDIUM` | Regular conversations, project details | Medium-term |
|
| 219 |
+
| `LOW` | Casual interactions, temporary context | Short-term |
|
| 220 |
+
|
| 221 |
+
## 🛠️ Configuration
|
| 222 |
+
|
| 223 |
+
### Memory Limits
|
| 224 |
+
```python
|
| 225 |
+
# Configure memory limits
|
| 226 |
+
memory_manager.enhanced_memory.consolidate_memories(
|
| 227 |
+
user_id="user123",
|
| 228 |
+
max_memories=1000 # Maximum memories per user
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
# Cleanup old memories
|
| 232 |
+
memory_manager.enhanced_memory.cleanup_old_memories(
|
| 233 |
+
user_id="user123",
|
| 234 |
+
days_old=90 # Remove memories older than 90 days
|
| 235 |
+
)
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### Embedding Configuration
|
| 239 |
+
```python
|
| 240 |
+
# Use different embedding models
|
| 241 |
+
embedder = EmbeddingClient(model_name="sentence-transformers/all-mpnet-base-v2")
|
| 242 |
+
|
| 243 |
+
# Initialize with custom embedder
|
| 244 |
+
memory_manager = MemoryIntegrationManager(
|
| 245 |
+
mongo_uri=mongo_uri,
|
| 246 |
+
db_name=db_name
|
| 247 |
+
)
|
| 248 |
+
memory_manager.enhanced_memory.embedder = embedder
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
## 🔒 Privacy & Security
|
| 252 |
+
|
| 253 |
+
### Data Protection
|
| 254 |
+
- All memories are user-scoped and isolated
|
| 255 |
+
- No cross-user data leakage
|
| 256 |
+
- Configurable data retention policies
|
| 257 |
+
- Memory export/import capabilities
|
| 258 |
+
|
| 259 |
+
### Access Control
|
| 260 |
+
```python
|
| 261 |
+
# Clear user data
|
| 262 |
+
memory_manager.clear_user_memories("user123")
|
| 263 |
+
|
| 264 |
+
# Get memory statistics
|
| 265 |
+
stats = memory_manager.get_memory_stats("user123")
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
## 📈 Performance Optimization
|
| 269 |
+
|
| 270 |
+
### Memory Consolidation
|
| 271 |
+
- Automatic memory importance scoring
|
| 272 |
+
- Intelligent memory pruning
|
| 273 |
+
- Embedding-based deduplication
|
| 274 |
+
- Efficient MongoDB indexing
|
| 275 |
+
|
| 276 |
+
### Query Optimization
|
| 277 |
+
- Semantic search with similarity thresholds
|
| 278 |
+
- Context length management
|
| 279 |
+
- Lazy loading of memory components
|
| 280 |
+
- Caching for frequent queries
|
| 281 |
+
|
| 282 |
+
## 🧪 Testing
|
| 283 |
+
|
| 284 |
+
Run the example integration to test the system:
|
| 285 |
+
|
| 286 |
+
```bash
|
| 287 |
+
python memo/example_integration.py
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
This will demonstrate:
|
| 291 |
+
- Basic memory operations
|
| 292 |
+
- Advanced features
|
| 293 |
+
- Integration patterns
|
| 294 |
+
- Error handling
|
| 295 |
+
|
| 296 |
+
## 🤝 Contributing
|
| 297 |
+
|
| 298 |
+
When adding new features to the memory system:
|
| 299 |
+
|
| 300 |
+
1. Maintain backward compatibility
|
| 301 |
+
2. Add comprehensive logging
|
| 302 |
+
3. Include error handling
|
| 303 |
+
4. Update documentation
|
| 304 |
+
5. Add tests for new functionality
|
| 305 |
+
|
| 306 |
+
## 📚 References
|
| 307 |
+
|
| 308 |
+
This enhanced memory system is inspired by:
|
| 309 |
+
- **Cursor AI**: Multi-file context awareness and conversation threading
|
| 310 |
+
- **ChatGPT**: Memory functionality and conversation continuity
|
| 311 |
+
- **Claude**: Advanced context management and reasoning
|
| 312 |
+
- **Research Papers**: MemoryBank, Mem0, and other memory architectures
|
| 313 |
+
|
| 314 |
+
## 🐛 Troubleshooting
|
| 315 |
+
|
| 316 |
+
### Common Issues
|
| 317 |
+
|
| 318 |
+
1. **MongoDB Connection Failed**
|
| 319 |
+
- Check MONGO_URI environment variable
|
| 320 |
+
- Ensure MongoDB is running
|
| 321 |
+
- Verify network connectivity
|
| 322 |
+
|
| 323 |
+
2. **Enhanced Memory Not Available**
|
| 324 |
+
- Check MongoDB connection
|
| 325 |
+
- Verify required dependencies
|
| 326 |
+
- Check logs for initialization errors
|
| 327 |
+
|
| 328 |
+
3. **Memory Retrieval Issues**
|
| 329 |
+
- Check embedding model availability
|
| 330 |
+
- Verify memory consolidation settings
|
| 331 |
+
- Check similarity thresholds
|
| 332 |
+
|
| 333 |
+
### Debug Mode
|
| 334 |
+
```python
|
| 335 |
+
import logging
|
| 336 |
+
logging.getLogger("ENHANCED_MEMORY").setLevel(logging.DEBUG)
|
| 337 |
+
logging.getLogger("MEMORY_INTEGRATION").setLevel(logging.DEBUG)
|
| 338 |
+
```
|
| 339 |
+
|
| 340 |
+
## 📄 License
|
| 341 |
+
|
| 342 |
+
This enhanced memory system is part of the EdSummariser project and follows the same license terms.
|
memo/REFACTORING_COMPLETE.md
ADDED
|
File without changes
|
memo/context.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ────────────────────────────── memo/context.py ──────────────────────────────
|
| 2 |
+
"""
|
| 3 |
+
Context Management
|
| 4 |
+
|
| 5 |
+
Functions for retrieving and managing conversation context.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
from typing import List, Dict, Any, Tuple, Optional
|
| 10 |
+
|
| 11 |
+
from utils.logger import get_logger
|
| 12 |
+
from utils.embeddings import EmbeddingClient
|
| 13 |
+
|
| 14 |
+
logger = get_logger("CONTEXT_MANAGER", __name__)
|
| 15 |
+
|
| 16 |
+
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
| 17 |
+
"""Calculate cosine similarity between two vectors"""
|
| 18 |
+
denom = (np.linalg.norm(a) * np.linalg.norm(b)) or 1.0
|
| 19 |
+
return float(np.dot(a, b) / denom)
|
| 20 |
+
|
| 21 |
+
def as_text(block: str) -> str:
|
| 22 |
+
"""Convert block to text"""
|
| 23 |
+
return block.strip()
|
| 24 |
+
|
| 25 |
+
async def semantic_context(question: str, memories: List[str], embedder: EmbeddingClient, topk: int = 3) -> str:
|
| 26 |
+
"""
|
| 27 |
+
Get semantic context from memories using cosine similarity.
|
| 28 |
+
"""
|
| 29 |
+
if not memories:
|
| 30 |
+
return ""
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
qv = np.array(embedder.embed([question])[0], dtype="float32")
|
| 34 |
+
mats = embedder.embed([as_text(s) for s in memories])
|
| 35 |
+
sims = [(cosine_similarity(qv, np.array(v, dtype="float32")), s) for v, s in zip(mats, memories)]
|
| 36 |
+
sims.sort(key=lambda x: x[0], reverse=True)
|
| 37 |
+
top = [s for (sc, s) in sims[:topk] if sc > 0.15] # small threshold
|
| 38 |
+
return "\n\n".join(top) if top else ""
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"[CONTEXT_MANAGER] Semantic context failed: {e}")
|
| 41 |
+
return ""
|
| 42 |
+
|
| 43 |
+
async def get_conversation_context(user_id: str, question: str, memory_system,
|
| 44 |
+
embedder: EmbeddingClient, topk_sem: int = 3) -> Tuple[str, str]:
|
| 45 |
+
"""
|
| 46 |
+
Get both recent and semantic context for conversation continuity.
|
| 47 |
+
"""
|
| 48 |
+
try:
|
| 49 |
+
if memory_system and memory_system.is_enhanced_available():
|
| 50 |
+
# Use enhanced context retrieval
|
| 51 |
+
recent_context, semantic_context = await memory_system.get_conversation_context(
|
| 52 |
+
user_id, question
|
| 53 |
+
)
|
| 54 |
+
return recent_context, semantic_context
|
| 55 |
+
else:
|
| 56 |
+
# Fallback to legacy context
|
| 57 |
+
return await get_legacy_context(user_id, question, memory_system, embedder, topk_sem)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error(f"[CONTEXT_MANAGER] Context retrieval failed: {e}")
|
| 60 |
+
return "", ""
|
| 61 |
+
|
| 62 |
+
async def get_legacy_context(user_id: str, question: str, memory_system,
|
| 63 |
+
embedder: EmbeddingClient, topk_sem: int) -> Tuple[str, str]:
|
| 64 |
+
"""Get context using legacy method"""
|
| 65 |
+
if not memory_system:
|
| 66 |
+
return "", ""
|
| 67 |
+
|
| 68 |
+
recent3 = memory_system.recent(user_id, 3)
|
| 69 |
+
rest17 = memory_system.rest(user_id, 3)
|
| 70 |
+
|
| 71 |
+
recent_text = ""
|
| 72 |
+
if recent3:
|
| 73 |
+
# This would need NVIDIA processing in the calling code
|
| 74 |
+
pass
|
| 75 |
+
|
| 76 |
+
sem_text = ""
|
| 77 |
+
if rest17:
|
| 78 |
+
qv = np.array(embedder.embed([question])[0], dtype="float32")
|
| 79 |
+
mats = embedder.embed([s.strip() for s in rest17])
|
| 80 |
+
sims = [(cosine_similarity(qv, np.array(v, dtype="float32")), s) for v, s in zip(mats, rest17)]
|
| 81 |
+
sims.sort(key=lambda x: x[0], reverse=True)
|
| 82 |
+
top = [s for (sc, s) in sims[:topk_sem] if sc > 0.15]
|
| 83 |
+
if top:
|
| 84 |
+
sem_text = "\n\n".join(top)
|
| 85 |
+
|
| 86 |
+
return recent_text, sem_text
|
memo/core.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ────────────────────────────── memo/core.py ──────────────────────────────
|
| 2 |
+
"""
|
| 3 |
+
Core Memory System
|
| 4 |
+
|
| 5 |
+
Main memory system that provides both legacy and enhanced functionality.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import asyncio
|
| 10 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 11 |
+
|
| 12 |
+
from utils.logger import get_logger
|
| 13 |
+
from utils.embeddings import EmbeddingClient
|
| 14 |
+
from memo.legacy import MemoryLRU
|
| 15 |
+
from memo.persistent import PersistentMemory
|
| 16 |
+
|
| 17 |
+
logger = get_logger("CORE_MEMORY", __name__)
|
| 18 |
+
|
| 19 |
+
class MemorySystem:
|
| 20 |
+
"""
|
| 21 |
+
Main memory system that provides both legacy and enhanced functionality.
|
| 22 |
+
Automatically uses enhanced features when MongoDB is available.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, mongo_uri: str = None, db_name: str = "studybuddy"):
|
| 26 |
+
self.mongo_uri = mongo_uri or os.getenv("MONGO_URI", "mongodb://localhost:27017")
|
| 27 |
+
self.db_name = db_name
|
| 28 |
+
|
| 29 |
+
# Initialize legacy memory system (always available)
|
| 30 |
+
self.legacy_memory = MemoryLRU()
|
| 31 |
+
|
| 32 |
+
# Initialize enhanced memory system if MongoDB is available
|
| 33 |
+
self.enhanced_available = False
|
| 34 |
+
self.enhanced_memory = None
|
| 35 |
+
self.embedder = None
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
self.embedder = EmbeddingClient()
|
| 39 |
+
self.enhanced_memory = PersistentMemory(self.mongo_uri, self.db_name, self.embedder)
|
| 40 |
+
self.enhanced_available = True
|
| 41 |
+
logger.info("[CORE_MEMORY] Enhanced memory system initialized")
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.warning(f"[CORE_MEMORY] Enhanced memory system unavailable: {e}")
|
| 44 |
+
self.enhanced_available = False
|
| 45 |
+
|
| 46 |
+
logger.info(f"[CORE_MEMORY] Initialized with enhanced_available={self.enhanced_available}")
|
| 47 |
+
|
| 48 |
+
# ────────────────────────────── Core Memory Operations ──────────────────────────────
|
| 49 |
+
|
| 50 |
+
def add(self, user_id: str, qa_summary: str):
|
| 51 |
+
"""Add a Q&A summary to memory (backward compatibility)"""
|
| 52 |
+
try:
|
| 53 |
+
# Add to legacy memory
|
| 54 |
+
self.legacy_memory.add(user_id, qa_summary)
|
| 55 |
+
|
| 56 |
+
# Also add to enhanced memory if available
|
| 57 |
+
if self.enhanced_available:
|
| 58 |
+
# Extract question and answer from summary
|
| 59 |
+
lines = qa_summary.split('\n')
|
| 60 |
+
question = ""
|
| 61 |
+
answer = ""
|
| 62 |
+
|
| 63 |
+
for line in lines:
|
| 64 |
+
if line.strip().lower().startswith('q:'):
|
| 65 |
+
question = line.strip()[2:].strip()
|
| 66 |
+
elif line.strip().lower().startswith('a:'):
|
| 67 |
+
answer = line.strip()[2:].strip()
|
| 68 |
+
|
| 69 |
+
if question and answer:
|
| 70 |
+
asyncio.create_task(self._add_enhanced_memory(user_id, question, answer))
|
| 71 |
+
|
| 72 |
+
logger.debug(f"[CORE_MEMORY] Added memory for user {user_id}")
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"[CORE_MEMORY] Failed to add memory: {e}")
|
| 75 |
+
|
| 76 |
+
def recent(self, user_id: str, n: int = 3) -> List[str]:
|
| 77 |
+
"""Get recent memories (backward compatibility)"""
|
| 78 |
+
return self.legacy_memory.recent(user_id, n)
|
| 79 |
+
|
| 80 |
+
def rest(self, user_id: str, skip_n: int = 3) -> List[str]:
|
| 81 |
+
"""Get remaining memories excluding recent ones (backward compatibility)"""
|
| 82 |
+
return self.legacy_memory.rest(user_id, skip_n)
|
| 83 |
+
|
| 84 |
+
def all(self, user_id: str) -> List[str]:
|
| 85 |
+
"""Get all memories for a user (backward compatibility)"""
|
| 86 |
+
return self.legacy_memory.all(user_id)
|
| 87 |
+
|
| 88 |
+
def clear(self, user_id: str) -> None:
|
| 89 |
+
"""Clear all memories for a user (backward compatibility)"""
|
| 90 |
+
self.legacy_memory.clear(user_id)
|
| 91 |
+
|
| 92 |
+
# Also clear enhanced memory if available
|
| 93 |
+
if self.enhanced_available:
|
| 94 |
+
try:
|
| 95 |
+
self.enhanced_memory.clear_user_memories(user_id)
|
| 96 |
+
logger.info(f"[CORE_MEMORY] Cleared enhanced memory for user {user_id}")
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.warning(f"[CORE_MEMORY] Failed to clear enhanced memory: {e}")
|
| 99 |
+
|
| 100 |
+
def is_enhanced_available(self) -> bool:
|
| 101 |
+
"""Check if enhanced memory features are available"""
|
| 102 |
+
return self.enhanced_available
|
| 103 |
+
|
| 104 |
+
# ────────────────────────────── Enhanced Features ──────────────────────────────
|
| 105 |
+
|
| 106 |
+
async def add_conversation_memory(self, user_id: str, question: str, answer: str,
|
| 107 |
+
project_id: Optional[str] = None,
|
| 108 |
+
context: Dict[str, Any] = None) -> str:
|
| 109 |
+
"""Add conversation memory with enhanced context"""
|
| 110 |
+
if not self.enhanced_available:
|
| 111 |
+
logger.warning("[CORE_MEMORY] Enhanced features not available")
|
| 112 |
+
return ""
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
memory_id = self.enhanced_memory.add_memory(
|
| 116 |
+
user_id=user_id,
|
| 117 |
+
content=f"Q: {question}\nA: {answer}",
|
| 118 |
+
memory_type="conversation",
|
| 119 |
+
project_id=project_id,
|
| 120 |
+
importance="medium",
|
| 121 |
+
tags=["conversation", "qa"],
|
| 122 |
+
metadata=context or {}
|
| 123 |
+
)
|
| 124 |
+
return memory_id
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"[CORE_MEMORY] Failed to add conversation memory: {e}")
|
| 127 |
+
return ""
|
| 128 |
+
|
| 129 |
+
async def get_conversation_context(self, user_id: str, question: str,
|
| 130 |
+
project_id: Optional[str] = None) -> Tuple[str, str]:
|
| 131 |
+
"""Get conversation context for chat continuity"""
|
| 132 |
+
try:
|
| 133 |
+
if self.enhanced_available:
|
| 134 |
+
# Use enhanced context retrieval
|
| 135 |
+
recent_context, semantic_context = await self._get_enhanced_context(user_id, question)
|
| 136 |
+
return recent_context, semantic_context
|
| 137 |
+
else:
|
| 138 |
+
# Fallback to legacy context
|
| 139 |
+
return "", ""
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.error(f"[CORE_MEMORY] Failed to get conversation context: {e}")
|
| 142 |
+
return "", ""
|
| 143 |
+
|
| 144 |
+
async def search_memories(self, user_id: str, query: str,
|
| 145 |
+
project_id: Optional[str] = None,
|
| 146 |
+
limit: int = 10) -> List[Tuple[str, float]]:
|
| 147 |
+
"""Search memories using semantic similarity"""
|
| 148 |
+
if not self.enhanced_available:
|
| 149 |
+
return []
|
| 150 |
+
|
| 151 |
+
try:
|
| 152 |
+
results = self.enhanced_memory.search_memories(
|
| 153 |
+
user_id=user_id,
|
| 154 |
+
query=query,
|
| 155 |
+
project_id=project_id,
|
| 156 |
+
limit=limit
|
| 157 |
+
)
|
| 158 |
+
return [(m["content"], score) for m, score in results]
|
| 159 |
+
except Exception as e:
|
| 160 |
+
logger.error(f"[CORE_MEMORY] Failed to search memories: {e}")
|
| 161 |
+
return []
|
| 162 |
+
|
| 163 |
+
def get_memory_stats(self, user_id: str) -> Dict[str, Any]:
|
| 164 |
+
"""Get memory statistics for a user"""
|
| 165 |
+
if self.enhanced_available:
|
| 166 |
+
return self.enhanced_memory.get_memory_stats(user_id)
|
| 167 |
+
else:
|
| 168 |
+
# Legacy memory stats
|
| 169 |
+
all_memories = self.legacy_memory.all(user_id)
|
| 170 |
+
return {
|
| 171 |
+
"total_memories": len(all_memories),
|
| 172 |
+
"system_type": "legacy",
|
| 173 |
+
"enhanced_available": False
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
# ────────────────────────────── Private Helper Methods ──────────────────────────────
|
| 177 |
+
|
| 178 |
+
async def _add_enhanced_memory(self, user_id: str, question: str, answer: str):
|
| 179 |
+
"""Add memory to enhanced system"""
|
| 180 |
+
try:
|
| 181 |
+
self.enhanced_memory.add_memory(
|
| 182 |
+
user_id=user_id,
|
| 183 |
+
content=f"Q: {question}\nA: {answer}",
|
| 184 |
+
memory_type="conversation",
|
| 185 |
+
importance="medium",
|
| 186 |
+
tags=["conversation", "qa"]
|
| 187 |
+
)
|
| 188 |
+
except Exception as e:
|
| 189 |
+
logger.warning(f"[CORE_MEMORY] Failed to add enhanced memory: {e}")
|
| 190 |
+
|
| 191 |
+
async def _get_enhanced_context(self, user_id: str, question: str) -> Tuple[str, str]:
|
| 192 |
+
"""Get context from enhanced memory system"""
|
| 193 |
+
try:
|
| 194 |
+
# Get recent conversation memories
|
| 195 |
+
recent_memories = self.enhanced_memory.get_memories(
|
| 196 |
+
user_id=user_id,
|
| 197 |
+
memory_type="conversation",
|
| 198 |
+
limit=5
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
recent_context = ""
|
| 202 |
+
if recent_memories:
|
| 203 |
+
recent_summaries = [m["summary"] for m in recent_memories]
|
| 204 |
+
recent_context = "\n\n".join(recent_summaries)
|
| 205 |
+
|
| 206 |
+
# Get semantic context from other memory types
|
| 207 |
+
semantic_memories = self.enhanced_memory.get_memories(
|
| 208 |
+
user_id=user_id,
|
| 209 |
+
limit=10
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
semantic_context = ""
|
| 213 |
+
if semantic_memories:
|
| 214 |
+
other_memories = [m for m in semantic_memories if m.get("memory_type") != "conversation"]
|
| 215 |
+
if other_memories:
|
| 216 |
+
semantic_summaries = [m["summary"] for m in other_memories]
|
| 217 |
+
semantic_context = "\n\n".join(semantic_summaries)
|
| 218 |
+
|
| 219 |
+
return recent_context, semantic_context
|
| 220 |
+
|
| 221 |
+
except Exception as e:
|
| 222 |
+
logger.error(f"[CORE_MEMORY] Failed to get enhanced context: {e}")
|
| 223 |
+
return "", ""
|
| 224 |
+
|
| 225 |
+
# ────────────────────────────── Global Instance ──────────────────────────────
|
| 226 |
+
|
| 227 |
+
_memory_system: Optional[MemorySystem] = None
|
| 228 |
+
|
| 229 |
+
def get_memory_system(mongo_uri: str = None, db_name: str = None) -> MemorySystem:
|
| 230 |
+
"""Get the global memory system instance"""
|
| 231 |
+
global _memory_system
|
| 232 |
+
|
| 233 |
+
if _memory_system is None:
|
| 234 |
+
if mongo_uri is None:
|
| 235 |
+
mongo_uri = os.getenv("MONGO_URI", "mongodb://localhost:27017")
|
| 236 |
+
if db_name is None:
|
| 237 |
+
db_name = os.getenv("MONGO_DB", "studybuddy")
|
| 238 |
+
|
| 239 |
+
_memory_system = MemorySystem(mongo_uri, db_name)
|
| 240 |
+
logger.info("[CORE_MEMORY] Global memory system initialized")
|
| 241 |
+
|
| 242 |
+
return _memory_system
|
| 243 |
+
|
| 244 |
+
def reset_memory_system():
|
| 245 |
+
"""Reset the global memory system (for testing)"""
|
| 246 |
+
global _memory_system
|
| 247 |
+
_memory_system = None
|
memo/history.py
CHANGED
|
@@ -1,53 +1,55 @@
|
|
| 1 |
-
# ────────────────────────────── memo/history.py ──────────────────────────────
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from utils.logger import get_logger
|
| 9 |
-
from
|
|
|
|
| 10 |
from utils.embeddings import EmbeddingClient
|
| 11 |
|
| 12 |
-
logger = get_logger("
|
| 13 |
-
|
| 14 |
-
NVIDIA_SMALL = os.getenv("NVIDIA_SMALL", "meta/llama-3.1-8b-instruct")
|
| 15 |
|
| 16 |
-
|
| 17 |
"""
|
| 18 |
-
|
|
|
|
| 19 |
"""
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
try:
|
| 40 |
-
return json.loads(s)
|
| 41 |
-
except Exception:
|
| 42 |
-
# Try to extract a JSON object from text
|
| 43 |
-
start = s.find("{")
|
| 44 |
-
end = s.rfind("}")
|
| 45 |
-
if start != -1 and end != -1 and end > start:
|
| 46 |
-
try:
|
| 47 |
-
return json.loads(s[start:end+1])
|
| 48 |
-
except Exception:
|
| 49 |
-
return {}
|
| 50 |
-
return {}
|
| 51 |
|
| 52 |
async def summarize_qa_with_nvidia(question: str, answer: str, rotator) -> str:
|
| 53 |
"""
|
|
@@ -55,73 +57,55 @@ async def summarize_qa_with_nvidia(question: str, answer: str, rotator) -> str:
|
|
| 55 |
q: <concise>\na: <concise>
|
| 56 |
No extra commentary.
|
| 57 |
"""
|
| 58 |
-
|
| 59 |
-
user = f"Question:\n{question}\n\nAnswer:\n{answer}"
|
| 60 |
-
key = rotator.get_key()
|
| 61 |
-
out = await _nvidia_chat(sys, user, key, rotator)
|
| 62 |
-
# Basic guard if the model returns extra prose
|
| 63 |
-
lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
|
| 64 |
-
ql = next((l for l in lines if l.lower().startswith('q:')), None)
|
| 65 |
-
al = next((l for l in lines if l.lower().startswith('a:')), None)
|
| 66 |
-
if not ql or not al:
|
| 67 |
-
# Fallback truncate
|
| 68 |
-
ql = "q: " + (question.strip()[:160] + ("…" if len(question.strip()) > 160 else ""))
|
| 69 |
-
al = "a: " + (answer.strip()[:220] + ("…" if len(answer.strip()) > 220 else ""))
|
| 70 |
-
return f"{ql}\n{al}"
|
| 71 |
|
| 72 |
async def files_relevance(question: str, file_summaries: List[Dict[str, str]], rotator) -> Dict[str, bool]:
|
| 73 |
"""
|
| 74 |
Ask NVIDIA model to mark each file as relevant (true) or not (false) for the question.
|
| 75 |
Returns {filename: bool}
|
| 76 |
"""
|
| 77 |
-
|
| 78 |
-
items = [{"filename": f["filename"], "summary": f.get("summary","")} for f in file_summaries]
|
| 79 |
-
user = f"Question: {question}\n\nFiles:\n{json.dumps(items, ensure_ascii=False)}\n\nReturn JSON only."
|
| 80 |
-
key = rotator.get_key()
|
| 81 |
-
out = await _nvidia_chat(sys, user, key, rotator)
|
| 82 |
-
data = _safe_json(out) or {}
|
| 83 |
-
rels = {}
|
| 84 |
-
for row in data.get("relevance", []):
|
| 85 |
-
fn = row.get("filename")
|
| 86 |
-
rv = row.get("relevant")
|
| 87 |
-
if isinstance(fn, str) and isinstance(rv, bool):
|
| 88 |
-
rels[fn] = rv
|
| 89 |
-
# If parsing failed, default to considering all files possibly relevant
|
| 90 |
-
if not rels and file_summaries:
|
| 91 |
-
rels = {f["filename"]: True for f in file_summaries}
|
| 92 |
-
return rels
|
| 93 |
-
|
| 94 |
-
def _cosine(a: np.ndarray, b: np.ndarray) -> float:
|
| 95 |
-
denom = (np.linalg.norm(a) * np.linalg.norm(b)) or 1.0
|
| 96 |
-
return float(np.dot(a, b) / denom)
|
| 97 |
-
|
| 98 |
-
def _as_text(block: str) -> str:
|
| 99 |
-
return block.strip()
|
| 100 |
|
| 101 |
async def related_recent_and_semantic_context(user_id: str, question: str, memory, embedder: EmbeddingClient, topk_sem: int = 3) -> Tuple[str, str]:
|
| 102 |
"""
|
| 103 |
Returns (recent_related_text, semantic_related_text).
|
| 104 |
- recent_related_text: NVIDIA checks the last 3 summaries for direct relatedness.
|
| 105 |
- semantic_related_text: cosine-sim search over the remaining 17 summaries (top-k).
|
|
|
|
|
|
|
|
|
|
| 106 |
"""
|
| 107 |
recent3 = memory.recent(user_id, 3)
|
| 108 |
rest17 = memory.rest(user_id, 3)
|
| 109 |
|
| 110 |
recent_text = ""
|
| 111 |
if recent3:
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
key = None # We'll let robust_post_json handle rotation via rotator param
|
| 116 |
# Semantic over rest17
|
| 117 |
sem_text = ""
|
| 118 |
if rest17:
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
sims = [(_cosine(qv, np.array(v, dtype="float32")), s) for v, s in zip(mats, rest17)]
|
| 122 |
-
sims.sort(key=lambda x: x[0], reverse=True)
|
| 123 |
-
top = [s for (sc, s) in sims[:topk_sem] if sc > 0.15] # small threshold
|
| 124 |
-
if top:
|
| 125 |
-
sem_text = "\n\n".join(top)
|
| 126 |
# Return recent empty (to be filled by caller using NVIDIA), and semantic text
|
| 127 |
return ("", sem_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ────────────────────────────── memo/history.py ──────────────────────────────
|
| 2 |
+
"""
|
| 3 |
+
History Management
|
| 4 |
+
|
| 5 |
+
Functions for managing conversation history and context.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from typing import List, Dict, Any, Tuple, Optional
|
| 9 |
|
| 10 |
from utils.logger import get_logger
|
| 11 |
+
from memo.nvidia import summarize_qa, files_relevance, related_recent_context
|
| 12 |
+
from memo.context import get_conversation_context, get_legacy_context, semantic_context
|
| 13 |
from utils.embeddings import EmbeddingClient
|
| 14 |
|
| 15 |
+
logger = get_logger("HISTORY_MANAGER", __name__)
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
class HistoryManager:
|
| 18 |
"""
|
| 19 |
+
Enhanced history manager that provides both legacy and enhanced functionality.
|
| 20 |
+
Automatically uses enhanced features when available.
|
| 21 |
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, memory_system=None):
|
| 24 |
+
self.memory_system = memory_system
|
| 25 |
+
|
| 26 |
+
async def summarize_qa_with_nvidia(self, question: str, answer: str, nvidia_rotator) -> str:
|
| 27 |
+
"""Summarize Q&A using NVIDIA model (enhanced version)"""
|
| 28 |
+
return await summarize_qa(question, answer, nvidia_rotator)
|
| 29 |
+
|
| 30 |
+
async def files_relevance(self, question: str, file_summaries: List[Dict[str, str]], nvidia_rotator) -> Dict[str, bool]:
|
| 31 |
+
"""Determine file relevance using NVIDIA model (enhanced version)"""
|
| 32 |
+
return await files_relevance(question, file_summaries, nvidia_rotator)
|
| 33 |
+
|
| 34 |
+
async def related_recent_and_semantic_context(self, user_id: str, question: str,
|
| 35 |
+
embedder: EmbeddingClient,
|
| 36 |
+
topk_sem: int = 3) -> Tuple[str, str]:
|
| 37 |
+
"""Get related recent and semantic context (enhanced version)"""
|
| 38 |
+
try:
|
| 39 |
+
if self.memory_system and self.memory_system.is_enhanced_available():
|
| 40 |
+
# Use enhanced context retrieval
|
| 41 |
+
recent_context, semantic_context = await self.memory_system.get_conversation_context(
|
| 42 |
+
user_id, question
|
| 43 |
+
)
|
| 44 |
+
return recent_context, semantic_context
|
| 45 |
+
else:
|
| 46 |
+
# Fallback to original implementation
|
| 47 |
+
return await get_legacy_context(user_id, question, self.memory_system, embedder, topk_sem)
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.error(f"[HISTORY_MANAGER] Context retrieval failed: {e}")
|
| 50 |
+
return "", ""
|
| 51 |
|
| 52 |
+
# ────────────────────────────── Legacy Functions (Backward Compatibility) ──────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
async def summarize_qa_with_nvidia(question: str, answer: str, rotator) -> str:
|
| 55 |
"""
|
|
|
|
| 57 |
q: <concise>\na: <concise>
|
| 58 |
No extra commentary.
|
| 59 |
"""
|
| 60 |
+
return await summarize_qa(question, answer, rotator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
async def files_relevance(question: str, file_summaries: List[Dict[str, str]], rotator) -> Dict[str, bool]:
|
| 63 |
"""
|
| 64 |
Ask NVIDIA model to mark each file as relevant (true) or not (false) for the question.
|
| 65 |
Returns {filename: bool}
|
| 66 |
"""
|
| 67 |
+
return await files_relevance(question, file_summaries, rotator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
async def related_recent_and_semantic_context(user_id: str, question: str, memory, embedder: EmbeddingClient, topk_sem: int = 3) -> Tuple[str, str]:
|
| 70 |
"""
|
| 71 |
Returns (recent_related_text, semantic_related_text).
|
| 72 |
- recent_related_text: NVIDIA checks the last 3 summaries for direct relatedness.
|
| 73 |
- semantic_related_text: cosine-sim search over the remaining 17 summaries (top-k).
|
| 74 |
+
|
| 75 |
+
This function is maintained for backward compatibility.
|
| 76 |
+
For enhanced features, use the integrated memory system.
|
| 77 |
"""
|
| 78 |
recent3 = memory.recent(user_id, 3)
|
| 79 |
rest17 = memory.rest(user_id, 3)
|
| 80 |
|
| 81 |
recent_text = ""
|
| 82 |
if recent3:
|
| 83 |
+
# This would need NVIDIA processing in the calling code
|
| 84 |
+
pass
|
| 85 |
+
|
|
|
|
| 86 |
# Semantic over rest17
|
| 87 |
sem_text = ""
|
| 88 |
if rest17:
|
| 89 |
+
sem_text = await semantic_context(question, rest17, embedder, topk_sem)
|
| 90 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
# Return recent empty (to be filled by caller using NVIDIA), and semantic text
|
| 92 |
return ("", sem_text)
|
| 93 |
+
|
| 94 |
+
# ────────────────────────────── Global Instance ──────────────────────────────
|
| 95 |
+
|
| 96 |
+
_history_manager: Optional[HistoryManager] = None
|
| 97 |
+
|
| 98 |
+
def get_history_manager(memory_system=None) -> HistoryManager:
|
| 99 |
+
"""Get the global history manager instance"""
|
| 100 |
+
global _history_manager
|
| 101 |
+
|
| 102 |
+
if _history_manager is None:
|
| 103 |
+
_history_manager = HistoryManager(memory_system)
|
| 104 |
+
logger.info("[HISTORY_MANAGER] Global history manager initialized")
|
| 105 |
+
|
| 106 |
+
return _history_manager
|
| 107 |
+
|
| 108 |
+
def reset_history_manager():
|
| 109 |
+
"""Reset the global history manager (for testing)"""
|
| 110 |
+
global _history_manager
|
| 111 |
+
_history_manager = None
|
memo/{memory.py → legacy.py}
RENAMED
|
@@ -1,20 +1,31 @@
|
|
| 1 |
-
# ────────────────────────────── memo/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from collections import deque, defaultdict
|
| 3 |
from typing import List, Dict
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
class MemoryLRU:
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
Each item is a single string in the format: "q: ...\na: ..."
|
| 9 |
-
"""
|
| 10 |
def __init__(self, capacity: int = 20):
|
| 11 |
self.capacity = capacity
|
| 12 |
self._store: Dict[str, deque] = defaultdict(lambda: deque(maxlen=self.capacity))
|
| 13 |
|
| 14 |
def add(self, user_id: str, qa_summary: str):
|
|
|
|
| 15 |
self._store[user_id].append(qa_summary)
|
|
|
|
| 16 |
|
| 17 |
def recent(self, user_id: str, n: int = 3) -> List[str]:
|
|
|
|
| 18 |
d = self._store[user_id]
|
| 19 |
if not d:
|
| 20 |
return []
|
|
@@ -22,6 +33,7 @@ class MemoryLRU:
|
|
| 22 |
return list(d)[-n:][::-1]
|
| 23 |
|
| 24 |
def rest(self, user_id: str, skip_n: int = 3) -> List[str]:
|
|
|
|
| 25 |
d = self._store[user_id]
|
| 26 |
if not d:
|
| 27 |
return []
|
|
@@ -29,11 +41,11 @@ class MemoryLRU:
|
|
| 29 |
return list(d)[:-skip_n] if len(d) > skip_n else []
|
| 30 |
|
| 31 |
def all(self, user_id: str) -> List[str]:
|
|
|
|
| 32 |
return list(self._store[user_id])
|
| 33 |
|
| 34 |
def clear(self, user_id: str) -> None:
|
| 35 |
-
"""
|
| 36 |
-
Clear all cached summaries for the given user.
|
| 37 |
-
"""
|
| 38 |
if user_id in self._store:
|
| 39 |
self._store[user_id].clear()
|
|
|
|
|
|
| 1 |
+
# ────────────────────────────── memo/legacy.py ──────────────────────────────
|
| 2 |
+
"""
|
| 3 |
+
Legacy Memory System
|
| 4 |
+
|
| 5 |
+
In-memory LRU system for backward compatibility.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
from collections import deque, defaultdict
|
| 9 |
from typing import List, Dict
|
| 10 |
|
| 11 |
+
from utils.logger import get_logger
|
| 12 |
+
|
| 13 |
+
logger = get_logger("LEGACY_MEMORY", __name__)
|
| 14 |
+
|
| 15 |
class MemoryLRU:
|
| 16 |
+
"""Legacy in-memory LRU system for backward compatibility"""
|
| 17 |
+
|
|
|
|
|
|
|
| 18 |
def __init__(self, capacity: int = 20):
|
| 19 |
self.capacity = capacity
|
| 20 |
self._store: Dict[str, deque] = defaultdict(lambda: deque(maxlen=self.capacity))
|
| 21 |
|
| 22 |
def add(self, user_id: str, qa_summary: str):
|
| 23 |
+
"""Add a Q&A summary to the user's memory"""
|
| 24 |
self._store[user_id].append(qa_summary)
|
| 25 |
+
logger.debug(f"[LEGACY_MEMORY] Added memory for user {user_id}")
|
| 26 |
|
| 27 |
def recent(self, user_id: str, n: int = 3) -> List[str]:
|
| 28 |
+
"""Get the most recent n memories for a user"""
|
| 29 |
d = self._store[user_id]
|
| 30 |
if not d:
|
| 31 |
return []
|
|
|
|
| 33 |
return list(d)[-n:][::-1]
|
| 34 |
|
| 35 |
def rest(self, user_id: str, skip_n: int = 3) -> List[str]:
|
| 36 |
+
"""Get memories excluding the most recent skip_n"""
|
| 37 |
d = self._store[user_id]
|
| 38 |
if not d:
|
| 39 |
return []
|
|
|
|
| 41 |
return list(d)[:-skip_n] if len(d) > skip_n else []
|
| 42 |
|
| 43 |
def all(self, user_id: str) -> List[str]:
|
| 44 |
+
"""Get all memories for a user"""
|
| 45 |
return list(self._store[user_id])
|
| 46 |
|
| 47 |
def clear(self, user_id: str) -> None:
|
| 48 |
+
"""Clear all cached summaries for the given user"""
|
|
|
|
|
|
|
| 49 |
if user_id in self._store:
|
| 50 |
self._store[user_id].clear()
|
| 51 |
+
logger.info(f"[LEGACY_MEMORY] Cleared memories for user {user_id}")
|
memo/nvidia.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ────────────────────────────── memo/nvidia.py ──────────────────────────────
|
| 2 |
+
"""
|
| 3 |
+
NVIDIA Integration
|
| 4 |
+
|
| 5 |
+
Functions for interacting with NVIDIA's API for summarization and analysis.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
from typing import List, Dict, Any
|
| 11 |
+
|
| 12 |
+
from utils.logger import get_logger
|
| 13 |
+
from utils.rotator import robust_post_json
|
| 14 |
+
|
| 15 |
+
logger = get_logger("NVIDIA_INTEGRATION", __name__)
|
| 16 |
+
|
| 17 |
+
NVIDIA_SMALL = os.getenv("NVIDIA_SMALL", "meta/llama-3.1-8b-instruct")
|
| 18 |
+
|
| 19 |
+
async def nvidia_chat(system_prompt: str, user_prompt: str, nvidia_key: str, rotator) -> str:
|
| 20 |
+
"""
|
| 21 |
+
Minimal NVIDIA Chat call that enforces no-comment concise outputs.
|
| 22 |
+
"""
|
| 23 |
+
url = "https://integrate.api.nvidia.com/v1/chat/completions"
|
| 24 |
+
payload = {
|
| 25 |
+
"model": NVIDIA_SMALL,
|
| 26 |
+
"temperature": 0.0,
|
| 27 |
+
"messages": [
|
| 28 |
+
{"role": "system", "content": system_prompt},
|
| 29 |
+
{"role": "user", "content": user_prompt},
|
| 30 |
+
]
|
| 31 |
+
}
|
| 32 |
+
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {nvidia_key or ''}"}
|
| 33 |
+
data = None
|
| 34 |
+
try:
|
| 35 |
+
data = await robust_post_json(url, headers, payload, rotator)
|
| 36 |
+
return data["choices"][0]["message"]["content"]
|
| 37 |
+
except Exception as e:
|
| 38 |
+
logger.warning(f"NVIDIA chat error: {e} • response: {data}")
|
| 39 |
+
return ""
|
| 40 |
+
|
| 41 |
+
def safe_json(s: str) -> Any:
|
| 42 |
+
"""Safely parse JSON string"""
|
| 43 |
+
try:
|
| 44 |
+
return json.loads(s)
|
| 45 |
+
except Exception:
|
| 46 |
+
# Try to extract a JSON object from text
|
| 47 |
+
start = s.find("{")
|
| 48 |
+
end = s.rfind("}")
|
| 49 |
+
if start != -1 and end != -1 and end > start:
|
| 50 |
+
try:
|
| 51 |
+
return json.loads(s[start:end+1])
|
| 52 |
+
except Exception:
|
| 53 |
+
return {}
|
| 54 |
+
return {}
|
| 55 |
+
|
| 56 |
+
async def summarize_qa(question: str, answer: str, rotator) -> str:
|
| 57 |
+
"""
|
| 58 |
+
Returns a single line block:
|
| 59 |
+
q: <concise>\na: <concise>
|
| 60 |
+
No extra commentary.
|
| 61 |
+
"""
|
| 62 |
+
sys = "You are a terse summarizer. Output exactly two lines:\nq: <short question summary>\na: <short answer summary>\nNo extra text."
|
| 63 |
+
user = f"Question:\n{question}\n\nAnswer:\n{answer}"
|
| 64 |
+
key = rotator.get_key()
|
| 65 |
+
out = await nvidia_chat(sys, user, key, rotator)
|
| 66 |
+
|
| 67 |
+
# Basic guard if the model returns extra prose
|
| 68 |
+
lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
|
| 69 |
+
ql = next((l for l in lines if l.lower().startswith('q:')), None)
|
| 70 |
+
al = next((l for l in lines if l.lower().startswith('a:')), None)
|
| 71 |
+
|
| 72 |
+
if not ql or not al:
|
| 73 |
+
# Fallback truncate
|
| 74 |
+
ql = "q: " + (question.strip()[:160] + ("…" if len(question.strip()) > 160 else ""))
|
| 75 |
+
al = "a: " + (answer.strip()[:220] + ("…" if len(answer.strip()) > 220 else ""))
|
| 76 |
+
|
| 77 |
+
return f"{ql}\n{al}"
|
| 78 |
+
|
| 79 |
+
async def files_relevance(question: str, file_summaries: List[Dict[str, str]], rotator) -> Dict[str, bool]:
|
| 80 |
+
"""
|
| 81 |
+
Ask NVIDIA model to mark each file as relevant (true) or not (false) for the question.
|
| 82 |
+
Returns {filename: bool}
|
| 83 |
+
"""
|
| 84 |
+
sys = "You classify file relevance. Return STRICT JSON only with shape {\"relevance\":[{\"filename\":\"...\",\"relevant\":true|false}]}."
|
| 85 |
+
items = [{"filename": f["filename"], "summary": f.get("summary","")} for f in file_summaries]
|
| 86 |
+
user = f"Question: {question}\n\nFiles:\n{json.dumps(items, ensure_ascii=False)}\n\nReturn JSON only."
|
| 87 |
+
key = rotator.get_key()
|
| 88 |
+
out = await nvidia_chat(sys, user, key, rotator)
|
| 89 |
+
|
| 90 |
+
data = safe_json(out) or {}
|
| 91 |
+
rels = {}
|
| 92 |
+
for row in data.get("relevance", []):
|
| 93 |
+
fn = row.get("filename")
|
| 94 |
+
rv = row.get("relevant")
|
| 95 |
+
if isinstance(fn, str) and isinstance(rv, bool):
|
| 96 |
+
rels[fn] = rv
|
| 97 |
+
|
| 98 |
+
# If parsing failed, default to considering all files possibly relevant
|
| 99 |
+
if not rels and file_summaries:
|
| 100 |
+
rels = {f["filename"]: True for f in file_summaries}
|
| 101 |
+
|
| 102 |
+
return rels
|
| 103 |
+
|
| 104 |
+
async def related_recent_context(question: str, recent_memories: List[str], rotator) -> str:
|
| 105 |
+
"""
|
| 106 |
+
Use NVIDIA to select related items from recent memories.
|
| 107 |
+
"""
|
| 108 |
+
if not recent_memories:
|
| 109 |
+
return ""
|
| 110 |
+
|
| 111 |
+
sys = "Pick only items that directly relate to the new question. Output the selected items verbatim, no commentary. If none, output nothing."
|
| 112 |
+
numbered = [{"id": i+1, "text": s} for i, s in enumerate(recent_memories)]
|
| 113 |
+
user = f"Question: {question}\nCandidates:\n{json.dumps(numbered, ensure_ascii=False)}\nSelect any related items and output ONLY their 'text' lines concatenated."
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
key = rotator.get_key()
|
| 117 |
+
out = await nvidia_chat(sys, user, key, rotator)
|
| 118 |
+
return out.strip()
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logger.warning(f"Recent-related NVIDIA error: {e}")
|
| 121 |
+
return ""
|
memo/persistent.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ────────────────────────────── memo/persistent.py ──────────────────────────────
|
| 2 |
+
"""
|
| 3 |
+
Persistent Memory System
|
| 4 |
+
|
| 5 |
+
MongoDB-based persistent memory storage with semantic search capabilities.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import uuid
|
| 10 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
|
| 13 |
+
from utils.logger import get_logger
|
| 14 |
+
from utils.embeddings import EmbeddingClient
|
| 15 |
+
|
| 16 |
+
logger = get_logger("PERSISTENT_MEMORY", __name__)
|
| 17 |
+
|
| 18 |
+
class PersistentMemory:
|
| 19 |
+
"""MongoDB-based persistent memory system with semantic search"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, mongo_uri: str, db_name: str, embedder: EmbeddingClient):
|
| 22 |
+
self.mongo_uri = mongo_uri
|
| 23 |
+
self.db_name = db_name
|
| 24 |
+
self.embedder = embedder
|
| 25 |
+
|
| 26 |
+
# MongoDB connection
|
| 27 |
+
try:
|
| 28 |
+
from pymongo import MongoClient
|
| 29 |
+
self.client = MongoClient(mongo_uri)
|
| 30 |
+
self.db = self.client[db_name]
|
| 31 |
+
self.memories = self.db["memories"]
|
| 32 |
+
|
| 33 |
+
# Create indexes for efficient querying
|
| 34 |
+
self.memories.create_index([("user_id", 1), ("memory_type", 1)])
|
| 35 |
+
self.memories.create_index([("user_id", 1), ("created_at", -1)])
|
| 36 |
+
self.memories.create_index([("user_id", 1), ("project_id", 1)])
|
| 37 |
+
|
| 38 |
+
logger.info(f"[PERSISTENT_MEMORY] Connected to MongoDB: {db_name}")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"[PERSISTENT_MEMORY] Failed to connect to MongoDB: {e}")
|
| 41 |
+
raise
|
| 42 |
+
|
| 43 |
+
def add_memory(self, user_id: str, content: str, memory_type: str,
|
| 44 |
+
project_id: str = None, importance: str = "medium",
|
| 45 |
+
tags: List[str] = None, metadata: Dict[str, Any] = None) -> str:
|
| 46 |
+
"""Add a memory entry to the persistent system"""
|
| 47 |
+
try:
|
| 48 |
+
# Generate embedding for semantic search
|
| 49 |
+
embedding = self.embedder.embed([content])[0] if content else None
|
| 50 |
+
|
| 51 |
+
# Create summary
|
| 52 |
+
summary = content[:200] + "..." if len(content) > 200 else content
|
| 53 |
+
|
| 54 |
+
memory_entry = {
|
| 55 |
+
"id": str(uuid.uuid4()),
|
| 56 |
+
"user_id": user_id,
|
| 57 |
+
"project_id": project_id,
|
| 58 |
+
"memory_type": memory_type,
|
| 59 |
+
"content": content,
|
| 60 |
+
"summary": summary,
|
| 61 |
+
"importance": importance,
|
| 62 |
+
"tags": tags or [],
|
| 63 |
+
"created_at": datetime.now(timezone.utc),
|
| 64 |
+
"updated_at": datetime.now(timezone.utc),
|
| 65 |
+
"last_accessed": datetime.now(timezone.utc),
|
| 66 |
+
"access_count": 0,
|
| 67 |
+
"embedding": embedding,
|
| 68 |
+
"metadata": metadata or {}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# Store in MongoDB
|
| 72 |
+
self.memories.insert_one(memory_entry)
|
| 73 |
+
logger.info(f"[PERSISTENT_MEMORY] Added {memory_type} memory for user {user_id}")
|
| 74 |
+
return memory_entry["id"]
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
logger.error(f"[PERSISTENT_MEMORY] Failed to add memory: {e}")
|
| 78 |
+
raise
|
| 79 |
+
|
| 80 |
+
def get_memories(self, user_id: str, memory_type: str = None,
|
| 81 |
+
project_id: str = None, limit: int = 50) -> List[Dict[str, Any]]:
|
| 82 |
+
"""Get memories for a user with optional filtering"""
|
| 83 |
+
try:
|
| 84 |
+
query = {"user_id": user_id}
|
| 85 |
+
|
| 86 |
+
if memory_type:
|
| 87 |
+
query["memory_type"] = memory_type
|
| 88 |
+
|
| 89 |
+
if project_id:
|
| 90 |
+
query["project_id"] = project_id
|
| 91 |
+
|
| 92 |
+
cursor = self.memories.find(query).sort("created_at", -1).limit(limit)
|
| 93 |
+
return list(cursor)
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logger.error(f"[PERSISTENT_MEMORY] Failed to get memories: {e}")
|
| 97 |
+
return []
|
| 98 |
+
|
| 99 |
+
def search_memories(self, user_id: str, query: str, memory_types: List[str] = None,
|
| 100 |
+
project_id: str = None, limit: int = 10) -> List[Tuple[Dict[str, Any], float]]:
|
| 101 |
+
"""Search memories using semantic similarity"""
|
| 102 |
+
try:
|
| 103 |
+
# Generate query embedding
|
| 104 |
+
query_embedding = self.embedder.embed([query])[0]
|
| 105 |
+
|
| 106 |
+
# Build MongoDB query
|
| 107 |
+
mongo_query = {
|
| 108 |
+
"user_id": user_id,
|
| 109 |
+
"embedding": {"$exists": True}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
if memory_types:
|
| 113 |
+
mongo_query["memory_type"] = {"$in": memory_types}
|
| 114 |
+
|
| 115 |
+
if project_id:
|
| 116 |
+
mongo_query["project_id"] = project_id
|
| 117 |
+
|
| 118 |
+
# Get all matching memories
|
| 119 |
+
cursor = self.memories.find(mongo_query)
|
| 120 |
+
|
| 121 |
+
# Calculate similarities
|
| 122 |
+
results = []
|
| 123 |
+
for doc in cursor:
|
| 124 |
+
try:
|
| 125 |
+
if doc.get("embedding"):
|
| 126 |
+
# Calculate cosine similarity
|
| 127 |
+
similarity = self._cosine_similarity(query_embedding, doc["embedding"])
|
| 128 |
+
results.append((doc, similarity))
|
| 129 |
+
except Exception as e:
|
| 130 |
+
logger.warning(f"[PERSISTENT_MEMORY] Failed to process memory for search: {e}")
|
| 131 |
+
continue
|
| 132 |
+
|
| 133 |
+
# Sort by similarity and return top results
|
| 134 |
+
results.sort(key=lambda x: x[1], reverse=True)
|
| 135 |
+
return results[:limit]
|
| 136 |
+
|
| 137 |
+
except Exception as e:
|
| 138 |
+
logger.error(f"[PERSISTENT_MEMORY] Failed to search memories: {e}")
|
| 139 |
+
return []
|
| 140 |
+
|
| 141 |
+
def _cosine_similarity(self, a: List[float], b: List[float]) -> float:
|
| 142 |
+
"""Calculate cosine similarity between two vectors"""
|
| 143 |
+
try:
|
| 144 |
+
import numpy as np
|
| 145 |
+
a_np = np.array(a)
|
| 146 |
+
b_np = np.array(b)
|
| 147 |
+
|
| 148 |
+
dot_product = np.dot(a_np, b_np)
|
| 149 |
+
norm_a = np.linalg.norm(a_np)
|
| 150 |
+
norm_b = np.linalg.norm(b_np)
|
| 151 |
+
|
| 152 |
+
if norm_a == 0 or norm_b == 0:
|
| 153 |
+
return 0.0
|
| 154 |
+
|
| 155 |
+
return float(dot_product / (norm_a * norm_b))
|
| 156 |
+
except Exception:
|
| 157 |
+
return 0.0
|
| 158 |
+
|
| 159 |
+
def clear_user_memories(self, user_id: str) -> int:
|
| 160 |
+
"""Clear all memories for a user"""
|
| 161 |
+
try:
|
| 162 |
+
result = self.memories.delete_many({"user_id": user_id})
|
| 163 |
+
logger.info(f"[PERSISTENT_MEMORY] Cleared {result.deleted_count} memories for user {user_id}")
|
| 164 |
+
return result.deleted_count
|
| 165 |
+
except Exception as e:
|
| 166 |
+
logger.error(f"[PERSISTENT_MEMORY] Failed to clear user memories: {e}")
|
| 167 |
+
return 0
|
| 168 |
+
|
| 169 |
+
def get_memory_stats(self, user_id: str) -> Dict[str, Any]:
|
| 170 |
+
"""Get memory statistics for a user"""
|
| 171 |
+
try:
|
| 172 |
+
stats = {
|
| 173 |
+
"total_memories": self.memories.count_documents({"user_id": user_id}),
|
| 174 |
+
"by_type": {},
|
| 175 |
+
"recent_activity": 0
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
# Count by memory type
|
| 179 |
+
pipeline = [
|
| 180 |
+
{"$match": {"user_id": user_id}},
|
| 181 |
+
{"$group": {"_id": "$memory_type", "count": {"$sum": 1}}}
|
| 182 |
+
]
|
| 183 |
+
|
| 184 |
+
for result in self.memories.aggregate(pipeline):
|
| 185 |
+
stats["by_type"][result["_id"]] = result["count"]
|
| 186 |
+
|
| 187 |
+
# Recent activity (last 7 days)
|
| 188 |
+
from datetime import timedelta
|
| 189 |
+
week_ago = datetime.now(timezone.utc) - timedelta(days=7)
|
| 190 |
+
stats["recent_activity"] = self.memories.count_documents({
|
| 191 |
+
"user_id": user_id,
|
| 192 |
+
"created_at": {"$gte": week_ago}
|
| 193 |
+
})
|
| 194 |
+
|
| 195 |
+
return stats
|
| 196 |
+
|
| 197 |
+
except Exception as e:
|
| 198 |
+
logger.error(f"[PERSISTENT_MEMORY] Failed to get memory stats: {e}")
|
| 199 |
+
return {}
|