memorisdk 2.0.1__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of memorisdk might be problematic. Click here for more details.
- memori/__init__.py +3 -3
- memori/agents/conscious_agent.py +289 -77
- memori/agents/memory_agent.py +19 -9
- memori/agents/retrieval_agent.py +59 -51
- memori/config/manager.py +7 -7
- memori/config/memory_manager.py +25 -25
- memori/config/settings.py +13 -6
- memori/core/conversation.py +15 -15
- memori/core/database.py +14 -13
- memori/core/memory.py +376 -105
- memori/core/providers.py +25 -25
- memori/database/__init__.py +11 -0
- memori/database/adapters/__init__.py +11 -0
- memori/database/adapters/mongodb_adapter.py +739 -0
- memori/database/adapters/mysql_adapter.py +8 -8
- memori/database/adapters/postgresql_adapter.py +6 -6
- memori/database/adapters/sqlite_adapter.py +6 -6
- memori/database/auto_creator.py +8 -9
- memori/database/connection_utils.py +5 -5
- memori/database/connectors/__init__.py +11 -0
- memori/database/connectors/base_connector.py +18 -19
- memori/database/connectors/mongodb_connector.py +654 -0
- memori/database/connectors/mysql_connector.py +13 -15
- memori/database/connectors/postgres_connector.py +12 -12
- memori/database/connectors/sqlite_connector.py +11 -11
- memori/database/models.py +2 -2
- memori/database/mongodb_manager.py +1484 -0
- memori/database/queries/base_queries.py +3 -4
- memori/database/queries/chat_queries.py +3 -5
- memori/database/queries/entity_queries.py +3 -5
- memori/database/queries/memory_queries.py +3 -5
- memori/database/query_translator.py +11 -11
- memori/database/schema_generators/__init__.py +11 -0
- memori/database/schema_generators/mongodb_schema_generator.py +666 -0
- memori/database/schema_generators/mysql_schema_generator.py +2 -4
- memori/database/search/__init__.py +11 -0
- memori/database/search/mongodb_search_adapter.py +653 -0
- memori/database/search/mysql_search_adapter.py +8 -8
- memori/database/search/sqlite_search_adapter.py +6 -6
- memori/database/search_service.py +17 -17
- memori/database/sqlalchemy_manager.py +10 -12
- memori/integrations/__init__.py +1 -1
- memori/integrations/anthropic_integration.py +1 -3
- memori/integrations/litellm_integration.py +23 -6
- memori/integrations/openai_integration.py +31 -3
- memori/tools/memory_tool.py +10 -9
- memori/utils/exceptions.py +58 -58
- memori/utils/helpers.py +11 -12
- memori/utils/input_validator.py +10 -12
- memori/utils/logging.py +4 -4
- memori/utils/pydantic_models.py +57 -57
- memori/utils/query_builder.py +20 -20
- memori/utils/security_audit.py +28 -28
- memori/utils/security_integration.py +9 -9
- memori/utils/transaction_manager.py +20 -19
- memori/utils/validators.py +6 -6
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/METADATA +23 -12
- memorisdk-2.1.1.dist-info/RECORD +71 -0
- memorisdk-2.0.1.dist-info/RECORD +0 -66
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/WHEEL +0 -0
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB adapter for Memori memory storage
|
|
3
|
+
Implements MongoDB-specific CRUD operations for memories
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import pymongo # noqa: F401
|
|
15
|
+
from bson import ObjectId # noqa: F401
|
|
16
|
+
from pymongo.collection import Collection # noqa: F401
|
|
17
|
+
from pymongo.errors import DuplicateKeyError, OperationFailure # noqa: F401
|
|
18
|
+
|
|
19
|
+
PYMONGO_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
PYMONGO_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
from ..connectors.mongodb_connector import MongoDBConnector
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MongoDBAdapter:
|
|
27
|
+
"""MongoDB-specific adapter for memory storage and retrieval"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, connector: MongoDBConnector):
|
|
30
|
+
"""Initialize MongoDB adapter"""
|
|
31
|
+
if not PYMONGO_AVAILABLE:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"pymongo is required for MongoDB support. Install with: pip install pymongo"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.connector = connector
|
|
37
|
+
self.database = connector.get_database()
|
|
38
|
+
|
|
39
|
+
# Collection names
|
|
40
|
+
self.CHAT_HISTORY_COLLECTION = "chat_history"
|
|
41
|
+
self.SHORT_TERM_MEMORY_COLLECTION = "short_term_memory"
|
|
42
|
+
self.LONG_TERM_MEMORY_COLLECTION = "long_term_memory"
|
|
43
|
+
|
|
44
|
+
# Initialize collections
|
|
45
|
+
self._initialize_collections()
|
|
46
|
+
|
|
47
|
+
def _initialize_collections(self):
|
|
48
|
+
"""Initialize MongoDB collections with proper indexes"""
|
|
49
|
+
try:
|
|
50
|
+
# Ensure collections exist
|
|
51
|
+
collections = [
|
|
52
|
+
self.CHAT_HISTORY_COLLECTION,
|
|
53
|
+
self.SHORT_TERM_MEMORY_COLLECTION,
|
|
54
|
+
self.LONG_TERM_MEMORY_COLLECTION,
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
existing_collections = self.database.list_collection_names()
|
|
58
|
+
for collection_name in collections:
|
|
59
|
+
if collection_name not in existing_collections:
|
|
60
|
+
self.database.create_collection(collection_name)
|
|
61
|
+
logger.info(f"Created MongoDB collection: {collection_name}")
|
|
62
|
+
|
|
63
|
+
# Create basic indexes
|
|
64
|
+
self._create_indexes()
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.warning(f"Failed to initialize MongoDB collections: {e}")
|
|
68
|
+
|
|
69
|
+
def _create_indexes(self):
|
|
70
|
+
"""Create essential indexes for performance"""
|
|
71
|
+
try:
|
|
72
|
+
# Chat history indexes
|
|
73
|
+
chat_collection = self.database[self.CHAT_HISTORY_COLLECTION]
|
|
74
|
+
chat_collection.create_index([("chat_id", 1)], unique=True, background=True)
|
|
75
|
+
chat_collection.create_index(
|
|
76
|
+
[("namespace", 1), ("session_id", 1)], background=True
|
|
77
|
+
)
|
|
78
|
+
chat_collection.create_index([("timestamp", -1)], background=True)
|
|
79
|
+
|
|
80
|
+
# Short-term memory indexes
|
|
81
|
+
st_collection = self.database[self.SHORT_TERM_MEMORY_COLLECTION]
|
|
82
|
+
st_collection.create_index([("memory_id", 1)], unique=True, background=True)
|
|
83
|
+
st_collection.create_index(
|
|
84
|
+
[("namespace", 1), ("category_primary", 1), ("importance_score", -1)],
|
|
85
|
+
background=True,
|
|
86
|
+
)
|
|
87
|
+
st_collection.create_index([("expires_at", 1)], background=True)
|
|
88
|
+
st_collection.create_index([("created_at", -1)], background=True)
|
|
89
|
+
st_collection.create_index(
|
|
90
|
+
[("searchable_content", "text"), ("summary", "text")], background=True
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Long-term memory indexes
|
|
94
|
+
lt_collection = self.database[self.LONG_TERM_MEMORY_COLLECTION]
|
|
95
|
+
lt_collection.create_index([("memory_id", 1)], unique=True, background=True)
|
|
96
|
+
lt_collection.create_index(
|
|
97
|
+
[("namespace", 1), ("category_primary", 1), ("importance_score", -1)],
|
|
98
|
+
background=True,
|
|
99
|
+
)
|
|
100
|
+
lt_collection.create_index([("classification", 1)], background=True)
|
|
101
|
+
lt_collection.create_index([("topic", 1)], background=True)
|
|
102
|
+
lt_collection.create_index([("created_at", -1)], background=True)
|
|
103
|
+
lt_collection.create_index(
|
|
104
|
+
[("searchable_content", "text"), ("summary", "text")], background=True
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
logger.debug("MongoDB indexes created successfully")
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.warning(f"Failed to create MongoDB indexes: {e}")
|
|
111
|
+
|
|
112
|
+
def _convert_memory_to_document(
|
|
113
|
+
self, memory_data: dict[str, Any]
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""Convert memory data to MongoDB document format"""
|
|
116
|
+
document = memory_data.copy()
|
|
117
|
+
|
|
118
|
+
# Ensure datetime fields are datetime objects
|
|
119
|
+
datetime_fields = [
|
|
120
|
+
"created_at",
|
|
121
|
+
"expires_at",
|
|
122
|
+
"last_accessed",
|
|
123
|
+
"extraction_timestamp",
|
|
124
|
+
]
|
|
125
|
+
for field in datetime_fields:
|
|
126
|
+
if field in document and document[field] is not None:
|
|
127
|
+
if isinstance(document[field], str):
|
|
128
|
+
try:
|
|
129
|
+
document[field] = datetime.fromisoformat(
|
|
130
|
+
document[field].replace("Z", "+00:00")
|
|
131
|
+
)
|
|
132
|
+
except:
|
|
133
|
+
document[field] = datetime.now(timezone.utc)
|
|
134
|
+
elif not isinstance(document[field], datetime):
|
|
135
|
+
document[field] = datetime.now(timezone.utc)
|
|
136
|
+
|
|
137
|
+
# Handle JSON fields that might be strings
|
|
138
|
+
json_fields = [
|
|
139
|
+
"processed_data",
|
|
140
|
+
"entities_json",
|
|
141
|
+
"keywords_json",
|
|
142
|
+
"supersedes_json",
|
|
143
|
+
"related_memories_json",
|
|
144
|
+
"metadata",
|
|
145
|
+
]
|
|
146
|
+
for field in json_fields:
|
|
147
|
+
if field in document and isinstance(document[field], str):
|
|
148
|
+
try:
|
|
149
|
+
document[field] = json.loads(document[field])
|
|
150
|
+
except:
|
|
151
|
+
pass # Keep as string if not valid JSON
|
|
152
|
+
|
|
153
|
+
# Ensure required fields have defaults
|
|
154
|
+
if "created_at" not in document:
|
|
155
|
+
document["created_at"] = datetime.now(timezone.utc)
|
|
156
|
+
if "importance_score" not in document:
|
|
157
|
+
document["importance_score"] = 0.5
|
|
158
|
+
if "access_count" not in document:
|
|
159
|
+
document["access_count"] = 0
|
|
160
|
+
if "namespace" not in document:
|
|
161
|
+
document["namespace"] = "default"
|
|
162
|
+
|
|
163
|
+
return document
|
|
164
|
+
|
|
165
|
+
def _convert_document_to_memory(self, document: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
"""Convert MongoDB document to memory format"""
|
|
167
|
+
if not document:
|
|
168
|
+
return {}
|
|
169
|
+
|
|
170
|
+
memory = document.copy()
|
|
171
|
+
|
|
172
|
+
# Convert ObjectId to string
|
|
173
|
+
if "_id" in memory:
|
|
174
|
+
memory["_id"] = str(memory["_id"])
|
|
175
|
+
|
|
176
|
+
# Convert datetime objects to ISO strings for JSON compatibility
|
|
177
|
+
datetime_fields = [
|
|
178
|
+
"created_at",
|
|
179
|
+
"expires_at",
|
|
180
|
+
"last_accessed",
|
|
181
|
+
"extraction_timestamp",
|
|
182
|
+
"timestamp",
|
|
183
|
+
]
|
|
184
|
+
for field in datetime_fields:
|
|
185
|
+
if field in memory and isinstance(memory[field], datetime):
|
|
186
|
+
memory[field] = memory[field].isoformat()
|
|
187
|
+
|
|
188
|
+
return memory
|
|
189
|
+
|
|
190
|
+
# Chat History Operations
|
|
191
|
+
def store_chat_interaction(
|
|
192
|
+
self,
|
|
193
|
+
chat_id: str,
|
|
194
|
+
user_input: str,
|
|
195
|
+
ai_output: str,
|
|
196
|
+
model: str,
|
|
197
|
+
session_id: str,
|
|
198
|
+
namespace: str = "default",
|
|
199
|
+
tokens_used: int = 0,
|
|
200
|
+
metadata: dict[str, Any] | None = None,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""Store a chat interaction in MongoDB"""
|
|
203
|
+
try:
|
|
204
|
+
collection = self.database[self.CHAT_HISTORY_COLLECTION]
|
|
205
|
+
|
|
206
|
+
document = {
|
|
207
|
+
"chat_id": chat_id,
|
|
208
|
+
"user_input": user_input,
|
|
209
|
+
"ai_output": ai_output,
|
|
210
|
+
"model": model,
|
|
211
|
+
"timestamp": datetime.now(timezone.utc),
|
|
212
|
+
"session_id": session_id,
|
|
213
|
+
"namespace": namespace,
|
|
214
|
+
"tokens_used": tokens_used,
|
|
215
|
+
"metadata": metadata or {},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
collection.insert_one(document)
|
|
219
|
+
logger.debug(f"Stored chat interaction: {chat_id}")
|
|
220
|
+
return chat_id
|
|
221
|
+
|
|
222
|
+
except DuplicateKeyError:
|
|
223
|
+
# Chat ID already exists, update instead
|
|
224
|
+
return self.update_chat_interaction(
|
|
225
|
+
chat_id, user_input, ai_output, model, tokens_used, metadata
|
|
226
|
+
)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"Failed to store chat interaction: {e}")
|
|
229
|
+
raise
|
|
230
|
+
|
|
231
|
+
def update_chat_interaction(
|
|
232
|
+
self,
|
|
233
|
+
chat_id: str,
|
|
234
|
+
user_input: str,
|
|
235
|
+
ai_output: str,
|
|
236
|
+
model: str,
|
|
237
|
+
tokens_used: int = 0,
|
|
238
|
+
metadata: dict[str, Any] | None = None,
|
|
239
|
+
) -> str:
|
|
240
|
+
"""Update an existing chat interaction"""
|
|
241
|
+
try:
|
|
242
|
+
collection = self.database[self.CHAT_HISTORY_COLLECTION]
|
|
243
|
+
|
|
244
|
+
update_doc = {
|
|
245
|
+
"$set": {
|
|
246
|
+
"user_input": user_input,
|
|
247
|
+
"ai_output": ai_output,
|
|
248
|
+
"model": model,
|
|
249
|
+
"tokens_used": tokens_used,
|
|
250
|
+
"timestamp": datetime.now(timezone.utc),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if metadata:
|
|
255
|
+
update_doc["$set"]["metadata"] = metadata
|
|
256
|
+
|
|
257
|
+
result = collection.update_one({"chat_id": chat_id}, update_doc)
|
|
258
|
+
|
|
259
|
+
if result.matched_count == 0:
|
|
260
|
+
raise ValueError(f"Chat interaction not found: {chat_id}")
|
|
261
|
+
|
|
262
|
+
logger.debug(f"Updated chat interaction: {chat_id}")
|
|
263
|
+
return chat_id
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Failed to update chat interaction: {e}")
|
|
267
|
+
raise
|
|
268
|
+
|
|
269
|
+
def get_chat_history(
|
|
270
|
+
self,
|
|
271
|
+
namespace: str = "default",
|
|
272
|
+
session_id: str | None = None,
|
|
273
|
+
limit: int = 100,
|
|
274
|
+
) -> list[dict[str, Any]]:
|
|
275
|
+
"""Retrieve chat history from MongoDB"""
|
|
276
|
+
try:
|
|
277
|
+
collection = self.database[self.CHAT_HISTORY_COLLECTION]
|
|
278
|
+
|
|
279
|
+
# Build filter
|
|
280
|
+
filter_doc = {"namespace": namespace}
|
|
281
|
+
if session_id:
|
|
282
|
+
filter_doc["session_id"] = session_id
|
|
283
|
+
|
|
284
|
+
# Execute query
|
|
285
|
+
cursor = collection.find(filter_doc).sort("timestamp", -1).limit(limit)
|
|
286
|
+
|
|
287
|
+
results = []
|
|
288
|
+
for document in cursor:
|
|
289
|
+
results.append(self._convert_document_to_memory(document))
|
|
290
|
+
|
|
291
|
+
logger.debug(f"Retrieved {len(results)} chat history entries")
|
|
292
|
+
return results
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.error(f"Failed to retrieve chat history: {e}")
|
|
296
|
+
return []
|
|
297
|
+
|
|
298
|
+
# Short-term Memory Operations
|
|
299
|
+
def store_short_term_memory(self, memory_data: dict[str, Any]) -> str:
|
|
300
|
+
"""Store short-term memory in MongoDB"""
|
|
301
|
+
try:
|
|
302
|
+
collection = self.database[self.SHORT_TERM_MEMORY_COLLECTION]
|
|
303
|
+
|
|
304
|
+
# Generate memory ID if not provided
|
|
305
|
+
if "memory_id" not in memory_data:
|
|
306
|
+
memory_data["memory_id"] = str(uuid4())
|
|
307
|
+
|
|
308
|
+
document = self._convert_memory_to_document(memory_data)
|
|
309
|
+
|
|
310
|
+
collection.insert_one(document)
|
|
311
|
+
logger.debug(f"Stored short-term memory: {memory_data['memory_id']}")
|
|
312
|
+
return memory_data["memory_id"]
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Failed to store short-term memory: {e}")
|
|
316
|
+
raise
|
|
317
|
+
|
|
318
|
+
def get_short_term_memories(
|
|
319
|
+
self,
|
|
320
|
+
namespace: str = "default",
|
|
321
|
+
category_filter: list[str] | None = None,
|
|
322
|
+
importance_threshold: float = 0.0,
|
|
323
|
+
limit: int = 100,
|
|
324
|
+
) -> list[dict[str, Any]]:
|
|
325
|
+
"""Retrieve short-term memories from MongoDB"""
|
|
326
|
+
try:
|
|
327
|
+
collection = self.database[self.SHORT_TERM_MEMORY_COLLECTION]
|
|
328
|
+
|
|
329
|
+
# Build filter
|
|
330
|
+
filter_doc: dict[str, Any] = {
|
|
331
|
+
"namespace": namespace,
|
|
332
|
+
"importance_score": {"$gte": importance_threshold},
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if category_filter:
|
|
336
|
+
filter_doc["category_primary"] = {"$in": category_filter}
|
|
337
|
+
|
|
338
|
+
# Include only non-expired memories
|
|
339
|
+
filter_doc["$or"] = [
|
|
340
|
+
{"expires_at": {"$exists": False}},
|
|
341
|
+
{"expires_at": None},
|
|
342
|
+
{"expires_at": {"$gt": datetime.now(timezone.utc)}},
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
# Execute query
|
|
346
|
+
cursor = (
|
|
347
|
+
collection.find(filter_doc)
|
|
348
|
+
.sort([("importance_score", -1), ("created_at", -1)])
|
|
349
|
+
.limit(limit)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
results = []
|
|
353
|
+
for document in cursor:
|
|
354
|
+
results.append(self._convert_document_to_memory(document))
|
|
355
|
+
|
|
356
|
+
logger.debug(f"Retrieved {len(results)} short-term memories")
|
|
357
|
+
return results
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.error(f"Failed to retrieve short-term memories: {e}")
|
|
361
|
+
return []
|
|
362
|
+
|
|
363
|
+
def update_short_term_memory(self, memory_id: str, updates: dict[str, Any]) -> bool:
|
|
364
|
+
"""Update a short-term memory"""
|
|
365
|
+
try:
|
|
366
|
+
collection = self.database[self.SHORT_TERM_MEMORY_COLLECTION]
|
|
367
|
+
|
|
368
|
+
# Convert updates to document format
|
|
369
|
+
update_doc = {"$set": self._convert_memory_to_document(updates)}
|
|
370
|
+
|
|
371
|
+
result = collection.update_one({"memory_id": memory_id}, update_doc)
|
|
372
|
+
|
|
373
|
+
success = result.matched_count > 0
|
|
374
|
+
if success:
|
|
375
|
+
logger.debug(f"Updated short-term memory: {memory_id}")
|
|
376
|
+
else:
|
|
377
|
+
logger.warning(f"Short-term memory not found: {memory_id}")
|
|
378
|
+
|
|
379
|
+
return success
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.error(f"Failed to update short-term memory: {e}")
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
def delete_short_term_memory(self, memory_id: str) -> bool:
|
|
386
|
+
"""Delete a short-term memory"""
|
|
387
|
+
try:
|
|
388
|
+
collection = self.database[self.SHORT_TERM_MEMORY_COLLECTION]
|
|
389
|
+
|
|
390
|
+
result = collection.delete_one({"memory_id": memory_id})
|
|
391
|
+
|
|
392
|
+
success = result.deleted_count > 0
|
|
393
|
+
if success:
|
|
394
|
+
logger.debug(f"Deleted short-term memory: {memory_id}")
|
|
395
|
+
else:
|
|
396
|
+
logger.warning(f"Short-term memory not found: {memory_id}")
|
|
397
|
+
|
|
398
|
+
return success
|
|
399
|
+
|
|
400
|
+
except Exception as e:
|
|
401
|
+
logger.error(f"Failed to delete short-term memory: {e}")
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
# Long-term Memory Operations
|
|
405
|
+
def store_long_term_memory(self, memory_data: dict[str, Any]) -> str:
|
|
406
|
+
"""Store long-term memory in MongoDB"""
|
|
407
|
+
try:
|
|
408
|
+
collection = self.database[self.LONG_TERM_MEMORY_COLLECTION]
|
|
409
|
+
|
|
410
|
+
# Generate memory ID if not provided
|
|
411
|
+
if "memory_id" not in memory_data:
|
|
412
|
+
memory_data["memory_id"] = str(uuid4())
|
|
413
|
+
|
|
414
|
+
document = self._convert_memory_to_document(memory_data)
|
|
415
|
+
|
|
416
|
+
collection.insert_one(document)
|
|
417
|
+
logger.debug(f"Stored long-term memory: {memory_data['memory_id']}")
|
|
418
|
+
return memory_data["memory_id"]
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"Failed to store long-term memory: {e}")
|
|
422
|
+
raise
|
|
423
|
+
|
|
424
|
+
def get_long_term_memories(
|
|
425
|
+
self,
|
|
426
|
+
namespace: str = "default",
|
|
427
|
+
category_filter: list[str] | None = None,
|
|
428
|
+
importance_threshold: float = 0.0,
|
|
429
|
+
classification_filter: list[str] | None = None,
|
|
430
|
+
limit: int = 100,
|
|
431
|
+
) -> list[dict[str, Any]]:
|
|
432
|
+
"""Retrieve long-term memories from MongoDB"""
|
|
433
|
+
try:
|
|
434
|
+
collection = self.database[self.LONG_TERM_MEMORY_COLLECTION]
|
|
435
|
+
|
|
436
|
+
# Build filter
|
|
437
|
+
filter_doc = {
|
|
438
|
+
"namespace": namespace,
|
|
439
|
+
"importance_score": {"$gte": importance_threshold},
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if category_filter:
|
|
443
|
+
filter_doc["category_primary"] = {"$in": category_filter}
|
|
444
|
+
|
|
445
|
+
if classification_filter:
|
|
446
|
+
filter_doc["classification"] = {"$in": classification_filter}
|
|
447
|
+
|
|
448
|
+
# Execute query
|
|
449
|
+
cursor = (
|
|
450
|
+
collection.find(filter_doc)
|
|
451
|
+
.sort([("importance_score", -1), ("created_at", -1)])
|
|
452
|
+
.limit(limit)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
results = []
|
|
456
|
+
for document in cursor:
|
|
457
|
+
results.append(self._convert_document_to_memory(document))
|
|
458
|
+
|
|
459
|
+
logger.debug(f"Retrieved {len(results)} long-term memories")
|
|
460
|
+
return results
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
logger.error(f"Failed to retrieve long-term memories: {e}")
|
|
464
|
+
return []
|
|
465
|
+
|
|
466
|
+
def update_long_term_memory(self, memory_id: str, updates: dict[str, Any]) -> bool:
|
|
467
|
+
"""Update a long-term memory"""
|
|
468
|
+
try:
|
|
469
|
+
collection = self.database[self.LONG_TERM_MEMORY_COLLECTION]
|
|
470
|
+
|
|
471
|
+
# Convert updates to document format
|
|
472
|
+
update_doc = {"$set": self._convert_memory_to_document(updates)}
|
|
473
|
+
|
|
474
|
+
result = collection.update_one({"memory_id": memory_id}, update_doc)
|
|
475
|
+
|
|
476
|
+
success = result.matched_count > 0
|
|
477
|
+
if success:
|
|
478
|
+
logger.debug(f"Updated long-term memory: {memory_id}")
|
|
479
|
+
else:
|
|
480
|
+
logger.warning(f"Long-term memory not found: {memory_id}")
|
|
481
|
+
|
|
482
|
+
return success
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.error(f"Failed to update long-term memory: {e}")
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
def delete_long_term_memory(self, memory_id: str) -> bool:
|
|
489
|
+
"""Delete a long-term memory"""
|
|
490
|
+
try:
|
|
491
|
+
collection = self.database[self.LONG_TERM_MEMORY_COLLECTION]
|
|
492
|
+
|
|
493
|
+
result = collection.delete_one({"memory_id": memory_id})
|
|
494
|
+
|
|
495
|
+
success = result.deleted_count > 0
|
|
496
|
+
if success:
|
|
497
|
+
logger.debug(f"Deleted long-term memory: {memory_id}")
|
|
498
|
+
else:
|
|
499
|
+
logger.warning(f"Long-term memory not found: {memory_id}")
|
|
500
|
+
|
|
501
|
+
return success
|
|
502
|
+
|
|
503
|
+
except Exception as e:
|
|
504
|
+
logger.error(f"Failed to delete long-term memory: {e}")
|
|
505
|
+
return False
|
|
506
|
+
|
|
507
|
+
# Search Operations
|
|
508
|
+
def search_memories(
|
|
509
|
+
self,
|
|
510
|
+
query: str,
|
|
511
|
+
namespace: str = "default",
|
|
512
|
+
memory_types: list[str] | None = None,
|
|
513
|
+
category_filter: list[str] | None = None,
|
|
514
|
+
limit: int = 10,
|
|
515
|
+
) -> list[dict[str, Any]]:
|
|
516
|
+
"""Search memories using MongoDB text search"""
|
|
517
|
+
try:
|
|
518
|
+
results = []
|
|
519
|
+
collections_to_search = []
|
|
520
|
+
|
|
521
|
+
# Determine which collections to search
|
|
522
|
+
if not memory_types or "short_term" in memory_types:
|
|
523
|
+
collections_to_search.append(
|
|
524
|
+
(self.SHORT_TERM_MEMORY_COLLECTION, "short_term")
|
|
525
|
+
)
|
|
526
|
+
if not memory_types or "long_term" in memory_types:
|
|
527
|
+
collections_to_search.append(
|
|
528
|
+
(self.LONG_TERM_MEMORY_COLLECTION, "long_term")
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
for collection_name, memory_type in collections_to_search:
|
|
532
|
+
collection = self.database[collection_name]
|
|
533
|
+
|
|
534
|
+
# Build search filter
|
|
535
|
+
search_filter: dict[str, Any] = {
|
|
536
|
+
"$text": {"$search": query},
|
|
537
|
+
"namespace": namespace,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if category_filter:
|
|
541
|
+
search_filter["category_primary"] = {"$in": category_filter}
|
|
542
|
+
|
|
543
|
+
# For short-term memories, exclude expired ones
|
|
544
|
+
if memory_type == "short_term":
|
|
545
|
+
search_filter["$or"] = [
|
|
546
|
+
{"expires_at": {"$exists": False}},
|
|
547
|
+
{"expires_at": None},
|
|
548
|
+
{"expires_at": {"$gt": datetime.now(timezone.utc)}},
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
# Execute text search
|
|
552
|
+
cursor = (
|
|
553
|
+
collection.find(search_filter, {"score": {"$meta": "textScore"}})
|
|
554
|
+
.sort([("score", {"$meta": "textScore"}), ("importance_score", -1)])
|
|
555
|
+
.limit(limit)
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
for document in cursor:
|
|
559
|
+
memory = self._convert_document_to_memory(document)
|
|
560
|
+
memory["memory_type"] = memory_type
|
|
561
|
+
memory["search_strategy"] = "mongodb_text"
|
|
562
|
+
results.append(memory)
|
|
563
|
+
|
|
564
|
+
# Sort all results by text score and importance
|
|
565
|
+
results.sort(
|
|
566
|
+
key=lambda x: (x.get("score", 0), x.get("importance_score", 0)),
|
|
567
|
+
reverse=True,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
logger.debug(f"MongoDB text search returned {len(results)} results")
|
|
571
|
+
return results[:limit]
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
logger.error(f"MongoDB text search failed: {e}")
|
|
575
|
+
return self._fallback_search(query, namespace, category_filter, limit)
|
|
576
|
+
|
|
577
|
+
def _fallback_search(
|
|
578
|
+
self,
|
|
579
|
+
query: str,
|
|
580
|
+
namespace: str = "default",
|
|
581
|
+
category_filter: list[str] | None = None,
|
|
582
|
+
limit: int = 10,
|
|
583
|
+
) -> list[dict[str, Any]]:
|
|
584
|
+
"""Fallback search using regex when text search fails"""
|
|
585
|
+
try:
|
|
586
|
+
results = []
|
|
587
|
+
collections_to_search = [
|
|
588
|
+
(self.SHORT_TERM_MEMORY_COLLECTION, "short_term"),
|
|
589
|
+
(self.LONG_TERM_MEMORY_COLLECTION, "long_term"),
|
|
590
|
+
]
|
|
591
|
+
|
|
592
|
+
# Create case-insensitive regex pattern
|
|
593
|
+
regex_pattern = {"$regex": query, "$options": "i"}
|
|
594
|
+
|
|
595
|
+
for collection_name, memory_type in collections_to_search:
|
|
596
|
+
collection = self.database[collection_name]
|
|
597
|
+
|
|
598
|
+
# Build search filter using regex
|
|
599
|
+
search_filter: dict[str, Any] = {
|
|
600
|
+
"$or": [
|
|
601
|
+
{"searchable_content": regex_pattern},
|
|
602
|
+
{"summary": regex_pattern},
|
|
603
|
+
],
|
|
604
|
+
"namespace": namespace,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if category_filter:
|
|
608
|
+
search_filter["category_primary"] = {"$in": category_filter}
|
|
609
|
+
|
|
610
|
+
# For short-term memories, exclude expired ones
|
|
611
|
+
if memory_type == "short_term":
|
|
612
|
+
search_filter["$and"] = [
|
|
613
|
+
search_filter.get("$and", []),
|
|
614
|
+
{
|
|
615
|
+
"$or": [
|
|
616
|
+
{"expires_at": {"$exists": False}},
|
|
617
|
+
{"expires_at": None},
|
|
618
|
+
{"expires_at": {"$gt": datetime.now(timezone.utc)}},
|
|
619
|
+
]
|
|
620
|
+
},
|
|
621
|
+
]
|
|
622
|
+
|
|
623
|
+
# Execute regex search
|
|
624
|
+
cursor = (
|
|
625
|
+
collection.find(search_filter)
|
|
626
|
+
.sort([("importance_score", -1), ("created_at", -1)])
|
|
627
|
+
.limit(limit)
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
for document in cursor:
|
|
631
|
+
memory = self._convert_document_to_memory(document)
|
|
632
|
+
memory["memory_type"] = memory_type
|
|
633
|
+
memory["search_strategy"] = "regex_fallback"
|
|
634
|
+
results.append(memory)
|
|
635
|
+
|
|
636
|
+
# Sort by importance score
|
|
637
|
+
results.sort(key=lambda x: x.get("importance_score", 0), reverse=True)
|
|
638
|
+
|
|
639
|
+
logger.debug(f"Regex fallback search returned {len(results)} results")
|
|
640
|
+
return results[:limit]
|
|
641
|
+
|
|
642
|
+
except Exception as e:
|
|
643
|
+
logger.error(f"Fallback search failed: {e}")
|
|
644
|
+
return []
|
|
645
|
+
|
|
646
|
+
# Batch Operations
|
|
647
|
+
def batch_store_memories(
|
|
648
|
+
self, memories: list[dict[str, Any]], memory_type: str = "short_term"
|
|
649
|
+
) -> list[str]:
|
|
650
|
+
"""Store multiple memories in batch"""
|
|
651
|
+
try:
|
|
652
|
+
if memory_type == "short_term":
|
|
653
|
+
collection = self.database[self.SHORT_TERM_MEMORY_COLLECTION]
|
|
654
|
+
elif memory_type == "long_term":
|
|
655
|
+
collection = self.database[self.LONG_TERM_MEMORY_COLLECTION]
|
|
656
|
+
else:
|
|
657
|
+
raise ValueError(f"Invalid memory type: {memory_type}")
|
|
658
|
+
|
|
659
|
+
# Prepare documents
|
|
660
|
+
documents = []
|
|
661
|
+
memory_ids = []
|
|
662
|
+
|
|
663
|
+
for memory_data in memories:
|
|
664
|
+
if "memory_id" not in memory_data:
|
|
665
|
+
memory_data["memory_id"] = str(uuid4())
|
|
666
|
+
|
|
667
|
+
memory_ids.append(memory_data["memory_id"])
|
|
668
|
+
documents.append(self._convert_memory_to_document(memory_data))
|
|
669
|
+
|
|
670
|
+
# Insert all documents
|
|
671
|
+
result = collection.insert_many(documents, ordered=False)
|
|
672
|
+
|
|
673
|
+
logger.info(
|
|
674
|
+
f"Batch stored {len(result.inserted_ids)} {memory_type} memories"
|
|
675
|
+
)
|
|
676
|
+
return memory_ids
|
|
677
|
+
|
|
678
|
+
except Exception as e:
|
|
679
|
+
logger.error(f"Batch store failed: {e}")
|
|
680
|
+
return []
|
|
681
|
+
|
|
682
|
+
def cleanup_expired_memories(self, namespace: str = "default") -> int:
|
|
683
|
+
"""Remove expired short-term memories"""
|
|
684
|
+
try:
|
|
685
|
+
collection = self.database[self.SHORT_TERM_MEMORY_COLLECTION]
|
|
686
|
+
|
|
687
|
+
# Delete expired memories
|
|
688
|
+
result = collection.delete_many(
|
|
689
|
+
{
|
|
690
|
+
"namespace": namespace,
|
|
691
|
+
"expires_at": {"$lt": datetime.now(timezone.utc)},
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
count = result.deleted_count
|
|
696
|
+
if count > 0:
|
|
697
|
+
logger.info(
|
|
698
|
+
f"Cleaned up {count} expired memories from namespace: {namespace}"
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
return count
|
|
702
|
+
|
|
703
|
+
except Exception as e:
|
|
704
|
+
logger.error(f"Failed to cleanup expired memories: {e}")
|
|
705
|
+
return 0
|
|
706
|
+
|
|
707
|
+
def get_memory_stats(self, namespace: str = "default") -> dict[str, Any]:
|
|
708
|
+
"""Get memory storage statistics"""
|
|
709
|
+
try:
|
|
710
|
+
stats = {
|
|
711
|
+
"namespace": namespace,
|
|
712
|
+
"short_term_count": 0,
|
|
713
|
+
"long_term_count": 0,
|
|
714
|
+
"chat_history_count": 0,
|
|
715
|
+
"total_size_bytes": 0,
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
# Count documents in each collection
|
|
719
|
+
stats["short_term_count"] = self.database[
|
|
720
|
+
self.SHORT_TERM_MEMORY_COLLECTION
|
|
721
|
+
].count_documents({"namespace": namespace})
|
|
722
|
+
|
|
723
|
+
stats["long_term_count"] = self.database[
|
|
724
|
+
self.LONG_TERM_MEMORY_COLLECTION
|
|
725
|
+
].count_documents({"namespace": namespace})
|
|
726
|
+
|
|
727
|
+
stats["chat_history_count"] = self.database[
|
|
728
|
+
self.CHAT_HISTORY_COLLECTION
|
|
729
|
+
].count_documents({"namespace": namespace})
|
|
730
|
+
|
|
731
|
+
# Get database stats
|
|
732
|
+
db_stats = self.database.command("dbStats")
|
|
733
|
+
stats["total_size_bytes"] = db_stats.get("dataSize", 0)
|
|
734
|
+
|
|
735
|
+
return stats
|
|
736
|
+
|
|
737
|
+
except Exception as e:
|
|
738
|
+
logger.error(f"Failed to get memory stats: {e}")
|
|
739
|
+
return {"error": str(e)}
|