memorisdk 2.0.0__py3-none-any.whl → 2.1.0__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.

Files changed (63) hide show
  1. memori/__init__.py +3 -3
  2. memori/agents/conscious_agent.py +289 -77
  3. memori/agents/memory_agent.py +19 -9
  4. memori/agents/retrieval_agent.py +138 -63
  5. memori/config/manager.py +7 -7
  6. memori/config/memory_manager.py +25 -25
  7. memori/config/settings.py +13 -6
  8. memori/core/conversation.py +15 -15
  9. memori/core/database.py +14 -13
  10. memori/core/memory.py +438 -123
  11. memori/core/providers.py +25 -25
  12. memori/database/__init__.py +11 -0
  13. memori/database/adapters/__init__.py +11 -0
  14. memori/database/adapters/mongodb_adapter.py +739 -0
  15. memori/database/adapters/mysql_adapter.py +8 -8
  16. memori/database/adapters/postgresql_adapter.py +6 -6
  17. memori/database/adapters/sqlite_adapter.py +6 -6
  18. memori/database/auto_creator.py +8 -9
  19. memori/database/connection_utils.py +5 -5
  20. memori/database/connectors/__init__.py +11 -0
  21. memori/database/connectors/base_connector.py +18 -19
  22. memori/database/connectors/mongodb_connector.py +527 -0
  23. memori/database/connectors/mysql_connector.py +13 -15
  24. memori/database/connectors/postgres_connector.py +12 -12
  25. memori/database/connectors/sqlite_connector.py +11 -11
  26. memori/database/models.py +2 -2
  27. memori/database/mongodb_manager.py +1402 -0
  28. memori/database/queries/base_queries.py +3 -4
  29. memori/database/queries/chat_queries.py +3 -5
  30. memori/database/queries/entity_queries.py +3 -5
  31. memori/database/queries/memory_queries.py +3 -5
  32. memori/database/query_translator.py +11 -11
  33. memori/database/schema_generators/__init__.py +11 -0
  34. memori/database/schema_generators/mongodb_schema_generator.py +666 -0
  35. memori/database/schema_generators/mysql_schema_generator.py +2 -4
  36. memori/database/search/__init__.py +11 -0
  37. memori/database/search/mongodb_search_adapter.py +653 -0
  38. memori/database/search/mysql_search_adapter.py +8 -8
  39. memori/database/search/sqlite_search_adapter.py +6 -6
  40. memori/database/search_service.py +218 -66
  41. memori/database/sqlalchemy_manager.py +72 -25
  42. memori/integrations/__init__.py +1 -1
  43. memori/integrations/anthropic_integration.py +1 -3
  44. memori/integrations/litellm_integration.py +23 -6
  45. memori/integrations/openai_integration.py +31 -3
  46. memori/tools/memory_tool.py +104 -13
  47. memori/utils/exceptions.py +58 -58
  48. memori/utils/helpers.py +11 -12
  49. memori/utils/input_validator.py +10 -12
  50. memori/utils/logging.py +4 -4
  51. memori/utils/pydantic_models.py +57 -57
  52. memori/utils/query_builder.py +20 -20
  53. memori/utils/security_audit.py +28 -28
  54. memori/utils/security_integration.py +9 -9
  55. memori/utils/transaction_manager.py +20 -19
  56. memori/utils/validators.py +6 -6
  57. {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.dist-info}/METADATA +36 -20
  58. memorisdk-2.1.0.dist-info/RECORD +71 -0
  59. memori/scripts/llm_text.py +0 -50
  60. memorisdk-2.0.0.dist-info/RECORD +0 -67
  61. {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.dist-info}/WHEEL +0 -0
  62. {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.dist-info}/licenses/LICENSE +0 -0
  63. {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.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)}