mdb-engine 0.7.2__py3-none-any.whl → 0.7.4__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.
- mdb_engine/__init__.py +1 -9
- mdb_engine/cli/main.py +1 -1
- mdb_engine/core/engine.py +3 -2
- mdb_engine/core/service_initialization.py +5 -4
- mdb_engine/core/types.py +2 -2
- mdb_engine/dependencies.py +3 -3
- mdb_engine/memory/README.md +145 -21
- mdb_engine/memory/__init__.py +9 -0
- mdb_engine/memory/base.py +194 -0
- mdb_engine/memory/service.py +323 -187
- mdb_engine/routing/websockets.py +45 -6
- {mdb_engine-0.7.2.dist-info → mdb_engine-0.7.4.dist-info}/METADATA +1 -15
- {mdb_engine-0.7.2.dist-info → mdb_engine-0.7.4.dist-info}/RECORD +17 -16
- {mdb_engine-0.7.2.dist-info → mdb_engine-0.7.4.dist-info}/WHEEL +0 -0
- {mdb_engine-0.7.2.dist-info → mdb_engine-0.7.4.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.7.2.dist-info → mdb_engine-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.7.2.dist-info → mdb_engine-0.7.4.dist-info}/top_level.txt +0 -0
mdb_engine/memory/service.py
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Mem0 Memory Service Implementation
|
|
3
3
|
Production-ready wrapper for Mem0.ai with strict metadata schema for MongoDB.
|
|
4
|
+
|
|
5
|
+
v0.7.4: Enhanced with hybrid update pattern and direct MongoDB access for reliable
|
|
6
|
+
memory operations. Properly handles Mem0's MongoDB structure (_id, payload).
|
|
4
7
|
"""
|
|
5
8
|
|
|
6
9
|
import logging
|
|
7
10
|
import os
|
|
8
11
|
import tempfile
|
|
12
|
+
from datetime import datetime
|
|
9
13
|
from typing import Any
|
|
10
14
|
|
|
15
|
+
from .base import BaseMemoryService, MemoryServiceError
|
|
16
|
+
|
|
17
|
+
# Required: Direct PyMongo access
|
|
18
|
+
try:
|
|
19
|
+
from pymongo import MongoClient
|
|
20
|
+
from pymongo.errors import (
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
ConnectionFailure,
|
|
23
|
+
InvalidURI,
|
|
24
|
+
PyMongoError,
|
|
25
|
+
ServerSelectionTimeoutError,
|
|
26
|
+
)
|
|
27
|
+
except ImportError:
|
|
28
|
+
MongoClient = None
|
|
29
|
+
ConnectionFailure = None
|
|
30
|
+
ConfigurationError = None
|
|
31
|
+
ServerSelectionTimeoutError = None
|
|
32
|
+
InvalidURI = None
|
|
33
|
+
PyMongoError = None
|
|
34
|
+
|
|
11
35
|
# Set MEM0_DIR environment variable early to avoid permission issues
|
|
12
36
|
if "MEM0_DIR" not in os.environ:
|
|
13
37
|
mem0_dir = os.path.join(tempfile.gettempdir(), ".mem0")
|
|
@@ -39,23 +63,29 @@ def _check_mem0_available():
|
|
|
39
63
|
logger = logging.getLogger(__name__)
|
|
40
64
|
|
|
41
65
|
|
|
42
|
-
class Mem0MemoryServiceError(
|
|
66
|
+
class Mem0MemoryServiceError(MemoryServiceError):
|
|
67
|
+
"""Exception raised by Mem0MemoryService operations."""
|
|
68
|
+
|
|
43
69
|
pass
|
|
44
70
|
|
|
45
71
|
|
|
46
|
-
class Mem0MemoryService:
|
|
72
|
+
class Mem0MemoryService(BaseMemoryService):
|
|
47
73
|
"""
|
|
48
74
|
Production-ready Mem0 Memory Service with MongoDB integration.
|
|
49
75
|
|
|
50
76
|
Features:
|
|
77
|
+
- Hybrid update pattern: Mem0 for embeddings, MongoDB for data persistence
|
|
78
|
+
- Full metadata support via direct MongoDB access
|
|
51
79
|
- In-place memory updates preserving IDs and timestamps
|
|
52
80
|
- Automatic embedding recomputation on content changes
|
|
53
81
|
- Knowledge graph support (if enabled in Mem0 config)
|
|
54
82
|
- Comprehensive error handling and logging
|
|
55
|
-
-
|
|
83
|
+
- Reliable return values fetched directly from MongoDB
|
|
56
84
|
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
Update Architecture:
|
|
86
|
+
- Content updates routed via Mem0 (triggers re-embedding)
|
|
87
|
+
- Metadata updates routed via direct PyMongo (full control)
|
|
88
|
+
- Final result always fetched from MongoDB (guaranteed structure)
|
|
59
89
|
"""
|
|
60
90
|
|
|
61
91
|
def __init__(
|
|
@@ -77,6 +107,26 @@ class Mem0MemoryService:
|
|
|
77
107
|
self.collection_name = (config or {}).get("collection_name", f"{app_slug}_memories")
|
|
78
108
|
self.infer = (config or {}).get("infer", True)
|
|
79
109
|
|
|
110
|
+
# ---------------------------------------------------------
|
|
111
|
+
# 1. SETUP DIRECT MONGODB ACCESS (The "Backdoor")
|
|
112
|
+
# ---------------------------------------------------------
|
|
113
|
+
if MongoClient is None:
|
|
114
|
+
raise Mem0MemoryServiceError("pymongo is required. pip install pymongo")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
self._client = MongoClient(mongo_uri)
|
|
118
|
+
self._db = self._client[db_name]
|
|
119
|
+
self.memories_collection = self._db[self.collection_name]
|
|
120
|
+
logger.info(f"✅ Direct MongoDB connection established for {self.collection_name}")
|
|
121
|
+
except BaseException as e:
|
|
122
|
+
# MongoDB connection may raise various exceptions. We catch BaseException
|
|
123
|
+
# (not Exception) to ensure we always raise Mem0MemoryServiceError for
|
|
124
|
+
# consistent error handling, but we re-raise KeyboardInterrupt and SystemExit
|
|
125
|
+
# to allow proper shutdown.
|
|
126
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
127
|
+
raise
|
|
128
|
+
raise Mem0MemoryServiceError(f"Failed to connect to MongoDB directly: {e}") from e
|
|
129
|
+
|
|
80
130
|
# Ensure GOOGLE_API_KEY is set for mem0 compatibility
|
|
81
131
|
# (mem0 expects GOOGLE_API_KEY, not GEMINI_API_KEY)
|
|
82
132
|
# This ensures we use the DIRECT Gemini API
|
|
@@ -268,10 +318,13 @@ class Mem0MemoryService:
|
|
|
268
318
|
if isinstance(messages, str):
|
|
269
319
|
messages = [{"role": "user", "content": messages}]
|
|
270
320
|
|
|
271
|
-
# Merge metadata
|
|
272
321
|
final_metadata = dict(metadata) if metadata else {}
|
|
273
322
|
|
|
274
323
|
# CRITICAL: Database indexing relies on these fields being in metadata
|
|
324
|
+
# Include user_id in metadata ONLY if provided (supports non-SSO use cases)
|
|
325
|
+
if user_id:
|
|
326
|
+
final_metadata["user_id"] = str(user_id)
|
|
327
|
+
|
|
275
328
|
if bucket_id:
|
|
276
329
|
final_metadata["bucket_id"] = bucket_id
|
|
277
330
|
final_metadata["context_id"] = bucket_id # Backwards compatibility
|
|
@@ -279,7 +332,6 @@ class Mem0MemoryService:
|
|
|
279
332
|
if bucket_type:
|
|
280
333
|
final_metadata["bucket_type"] = bucket_type
|
|
281
334
|
|
|
282
|
-
# Store raw_content in metadata if provided (metadata convenience)
|
|
283
335
|
if raw_content:
|
|
284
336
|
final_metadata["raw_content"] = raw_content
|
|
285
337
|
|
|
@@ -435,8 +487,50 @@ class Mem0MemoryService:
|
|
|
435
487
|
return []
|
|
436
488
|
|
|
437
489
|
def get(self, memory_id: str, user_id: str | None = None, **kwargs) -> dict[str, Any]:
|
|
490
|
+
"""
|
|
491
|
+
Get memory by ID using direct MongoDB access for reliability.
|
|
492
|
+
|
|
493
|
+
Mem0 stores memories with _id as the MongoDB document ID.
|
|
494
|
+
Memory content and metadata are stored in the 'payload' field.
|
|
495
|
+
"""
|
|
438
496
|
try:
|
|
439
|
-
|
|
497
|
+
# Mem0 uses _id as the MongoDB document ID
|
|
498
|
+
doc = self.memories_collection.find_one({"_id": memory_id})
|
|
499
|
+
if doc:
|
|
500
|
+
# Extract payload (where Mem0 stores the actual memory data)
|
|
501
|
+
payload = doc.get("payload", {})
|
|
502
|
+
|
|
503
|
+
# Build normalized memory document
|
|
504
|
+
memory_doc = {
|
|
505
|
+
"id": str(doc["_id"]), # Convert _id to id for API consistency
|
|
506
|
+
"memory": payload.get("memory") or payload.get("text"),
|
|
507
|
+
"text": payload.get("text") or payload.get("memory"),
|
|
508
|
+
"metadata": payload.get("metadata", {}),
|
|
509
|
+
"user_id": payload.get("user_id") or payload.get("metadata", {}).get("user_id"),
|
|
510
|
+
"created_at": payload.get("created_at"),
|
|
511
|
+
"updated_at": payload.get("updated_at"),
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Add any other payload fields
|
|
515
|
+
for key, value in payload.items():
|
|
516
|
+
if key not in [
|
|
517
|
+
"memory",
|
|
518
|
+
"text",
|
|
519
|
+
"metadata",
|
|
520
|
+
"user_id",
|
|
521
|
+
"created_at",
|
|
522
|
+
"updated_at",
|
|
523
|
+
]:
|
|
524
|
+
memory_doc[key] = value
|
|
525
|
+
|
|
526
|
+
# Optional: Filter by user_id if provided
|
|
527
|
+
if user_id:
|
|
528
|
+
doc_user_id = memory_doc.get("user_id")
|
|
529
|
+
if doc_user_id and str(doc_user_id) != str(user_id):
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
return memory_doc
|
|
533
|
+
return None
|
|
440
534
|
except (
|
|
441
535
|
ValueError,
|
|
442
536
|
TypeError,
|
|
@@ -446,7 +540,19 @@ class Mem0MemoryService:
|
|
|
446
540
|
RuntimeError,
|
|
447
541
|
KeyError,
|
|
448
542
|
):
|
|
449
|
-
|
|
543
|
+
# Fallback to Mem0 if direct access fails
|
|
544
|
+
try:
|
|
545
|
+
return self.memory.get(memory_id, **kwargs)
|
|
546
|
+
except (
|
|
547
|
+
ValueError,
|
|
548
|
+
TypeError,
|
|
549
|
+
ConnectionError,
|
|
550
|
+
OSError,
|
|
551
|
+
AttributeError,
|
|
552
|
+
RuntimeError,
|
|
553
|
+
KeyError,
|
|
554
|
+
):
|
|
555
|
+
return None
|
|
450
556
|
|
|
451
557
|
def delete(self, memory_id: str, user_id: str | None = None, **kwargs) -> bool:
|
|
452
558
|
try:
|
|
@@ -489,29 +595,42 @@ class Mem0MemoryService:
|
|
|
489
595
|
**kwargs,
|
|
490
596
|
) -> dict[str, Any] | None:
|
|
491
597
|
"""
|
|
492
|
-
|
|
598
|
+
Robust Hybrid Update Pattern:
|
|
599
|
+
|
|
600
|
+
This method uses a hybrid approach that combines Mem0's embedding capabilities
|
|
601
|
+
with direct MongoDB control for maximum flexibility and reliability.
|
|
602
|
+
|
|
603
|
+
**Architecture:**
|
|
604
|
+
1. **Content Updates** → Routed via Mem0 (triggers automatic re-embedding)
|
|
605
|
+
2. **Metadata Updates** → Routed via direct PyMongo (full control, no API limitations)
|
|
606
|
+
3. **Return Value** → Always fetched from MongoDB (guaranteed correct structure)
|
|
607
|
+
|
|
608
|
+
**Why Hybrid?**
|
|
609
|
+
- Mem0's update() API doesn't support metadata parameter
|
|
610
|
+
- Mem0's return values can be inconsistent (dict, list, or status messages)
|
|
611
|
+
- Direct MongoDB access gives us full control over data persistence
|
|
612
|
+
- We use Mem0 purely as an "embedding utility" for content changes
|
|
493
613
|
|
|
494
614
|
Updates the memory content and/or metadata while preserving:
|
|
495
615
|
- Original memory ID (never changes)
|
|
496
616
|
- Creation timestamp (created_at) - preserved
|
|
497
617
|
- Other existing fields - preserved unless explicitly updated
|
|
498
618
|
|
|
499
|
-
If content is updated, the embedding vector is automatically recomputed.
|
|
500
|
-
|
|
501
619
|
Args:
|
|
502
620
|
memory_id: The ID of the memory to update (required)
|
|
503
621
|
user_id: The user ID who owns the memory (for scoping and security)
|
|
504
622
|
memory: New memory content as a string (optional)
|
|
505
|
-
data: Alternative parameter name for memory content
|
|
623
|
+
data: Alternative parameter name for memory content.
|
|
506
624
|
Can be a string or dict with 'memory'/'text'/'content' key.
|
|
507
625
|
messages: Alternative way to provide content as messages (optional).
|
|
508
626
|
Can be a string or list of dicts with 'content' key.
|
|
509
|
-
metadata: Metadata updates
|
|
510
|
-
|
|
627
|
+
metadata: Metadata updates (FULLY SUPPORTED via direct MongoDB).
|
|
628
|
+
Can update any metadata field, not limited by Mem0 API.
|
|
511
629
|
**kwargs: Additional arguments passed to Mem0 operations
|
|
512
630
|
|
|
513
631
|
Returns:
|
|
514
|
-
Updated memory object with same ID,
|
|
632
|
+
Updated memory object with same ID, fetched directly from MongoDB,
|
|
633
|
+
or None if memory not found
|
|
515
634
|
|
|
516
635
|
Raises:
|
|
517
636
|
Mem0MemoryServiceError: If update operation fails
|
|
@@ -519,7 +638,7 @@ class Mem0MemoryService:
|
|
|
519
638
|
|
|
520
639
|
Example:
|
|
521
640
|
```python
|
|
522
|
-
# Update content and metadata
|
|
641
|
+
# Update content and metadata (hybrid approach)
|
|
523
642
|
updated = memory_service.update(
|
|
524
643
|
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
525
644
|
user_id="user123",
|
|
@@ -527,80 +646,164 @@ class Mem0MemoryService:
|
|
|
527
646
|
metadata={"category": "technical", "updated": True}
|
|
528
647
|
)
|
|
529
648
|
|
|
530
|
-
# Update only metadata (content unchanged)
|
|
649
|
+
# Update only metadata (content unchanged) - FULLY SUPPORTED
|
|
650
|
+
updated = memory_service.update(
|
|
651
|
+
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
652
|
+
user_id="user123",
|
|
653
|
+
metadata={"category": "updated", "priority": "high"}
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Update only content (no metadata)
|
|
531
657
|
updated = memory_service.update(
|
|
532
658
|
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
533
659
|
user_id="user123",
|
|
534
|
-
|
|
660
|
+
memory="Updated content only"
|
|
535
661
|
)
|
|
536
662
|
```
|
|
537
663
|
"""
|
|
538
|
-
# Input validation
|
|
539
664
|
if not memory_id or not isinstance(memory_id, str) or not memory_id.strip():
|
|
540
665
|
raise ValueError("memory_id is required and must be a non-empty string")
|
|
541
666
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
normalized_metadata = self._normalize_metadata_input(metadata, data)
|
|
667
|
+
# 1. Normalize Inputs
|
|
668
|
+
normalized_memory = self._normalize_content_input(memory, data, messages)
|
|
669
|
+
normalized_metadata = self._normalize_metadata_input(metadata, data)
|
|
546
670
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
671
|
+
# 2. Check Existence (Fast check via ID)
|
|
672
|
+
# Mem0 uses _id as the MongoDB document ID
|
|
673
|
+
existing = self.memories_collection.find_one(
|
|
674
|
+
{"_id": memory_id}, {"_id": 1, "payload.user_id": 1, "payload.metadata.user_id": 1}
|
|
675
|
+
)
|
|
676
|
+
if not existing:
|
|
677
|
+
logger.warning(f"Memory {memory_id} not found.")
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
# Optional: Security Scope Check
|
|
681
|
+
# Check user_id in payload (Mem0 stores it there)
|
|
682
|
+
if user_id:
|
|
683
|
+
payload = existing.get("payload", {})
|
|
684
|
+
existing_user_id = payload.get("user_id") or payload.get("metadata", {}).get("user_id")
|
|
685
|
+
if existing_user_id and str(existing_user_id) != str(user_id):
|
|
686
|
+
logger.warning(f"Unauthorized update attempt for {memory_id} by {user_id}")
|
|
554
687
|
return None
|
|
555
688
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
# - In-place updates preserving memory ID
|
|
559
|
-
# - Automatic embedding recomputation
|
|
560
|
-
# - Metadata merging
|
|
561
|
-
# - User scoping
|
|
562
|
-
# - Knowledge graph updates (if enabled)
|
|
563
|
-
# - Relationship management
|
|
564
|
-
if not hasattr(self.memory, "update") or not callable(self.memory.update):
|
|
565
|
-
raise Mem0MemoryServiceError(
|
|
566
|
-
"Mem0 update method not available. "
|
|
567
|
-
"Please ensure you're using a compatible version of mem0ai "
|
|
568
|
-
"that supports updates. Install with: pip install --upgrade mem0ai"
|
|
569
|
-
)
|
|
689
|
+
# Use _id directly (Mem0's format)
|
|
690
|
+
actual_id = memory_id
|
|
570
691
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
692
|
+
try:
|
|
693
|
+
# -------------------------------------------------
|
|
694
|
+
# STEP A: Content Update (Via Mem0 for Vectors)
|
|
695
|
+
# -------------------------------------------------
|
|
696
|
+
if normalized_memory:
|
|
697
|
+
logger.info(f"📝 Updating content for {actual_id} (triggering re-embedding)")
|
|
698
|
+
# We use Mem0 here specifically because it handles the embedding logic.
|
|
699
|
+
# We do NOT care what it returns.
|
|
700
|
+
try:
|
|
701
|
+
# Use the actual_id (which Mem0 recognizes)
|
|
702
|
+
self.memory.update(memory_id=actual_id, data=normalized_memory)
|
|
703
|
+
except BaseException as e:
|
|
704
|
+
# Mem0 is a third-party library that may raise any exception.
|
|
705
|
+
# We catch BaseException (not Exception) to ensure we always raise
|
|
706
|
+
# Mem0MemoryServiceError for consistent error handling, but we
|
|
707
|
+
# re-raise KeyboardInterrupt and SystemExit to allow proper shutdown.
|
|
708
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
709
|
+
raise
|
|
710
|
+
# If Mem0 fails (e.g. LLM rate limit, API error), we should abort
|
|
711
|
+
# or fall back to just text update without vector (risky for search).
|
|
712
|
+
logger.exception(f"Mem0 embedding update failed: {e}")
|
|
713
|
+
raise Mem0MemoryServiceError(f"Content update failed: {e}") from e
|
|
714
|
+
|
|
715
|
+
# -------------------------------------------------
|
|
716
|
+
# STEP B: Metadata Update (Direct PyMongo)
|
|
717
|
+
# -------------------------------------------------
|
|
718
|
+
if normalized_metadata:
|
|
719
|
+
logger.info(f"🏷️ Updating metadata for {actual_id}")
|
|
720
|
+
|
|
721
|
+
# Ensure user_id is in metadata for consistency
|
|
722
|
+
if user_id:
|
|
723
|
+
normalized_metadata["user_id"] = str(user_id)
|
|
724
|
+
|
|
725
|
+
# Mem0 stores everything in the 'payload' field
|
|
726
|
+
# We need to update payload.metadata and payload.user_id
|
|
727
|
+
update_fields = {}
|
|
728
|
+
|
|
729
|
+
# Handle metadata updates (nested under payload.metadata)
|
|
730
|
+
for k, v in normalized_metadata.items():
|
|
731
|
+
if k == "user_id":
|
|
732
|
+
# user_id can be at payload.user_id or payload.metadata.user_id
|
|
733
|
+
update_fields["payload.user_id"] = v
|
|
734
|
+
update_fields["payload.metadata.user_id"] = v
|
|
735
|
+
else:
|
|
736
|
+
update_fields[f"payload.metadata.{k}"] = v
|
|
737
|
+
|
|
738
|
+
# Add timestamp to payload
|
|
739
|
+
update_fields["payload.updated_at"] = datetime.utcnow().isoformat()
|
|
740
|
+
|
|
741
|
+
# Execute Atomic Update - Mem0 uses _id
|
|
742
|
+
self.memories_collection.update_one({"_id": actual_id}, {"$set": update_fields})
|
|
743
|
+
|
|
744
|
+
# -------------------------------------------------
|
|
745
|
+
# STEP C: Return The Truth (Direct DB Fetch)
|
|
746
|
+
# -------------------------------------------------
|
|
747
|
+
# We completely ignore Mem0's return value (which might be {"message": "ok"})
|
|
748
|
+
# and fetch the actual document from the database.
|
|
749
|
+
final_doc_raw = self.memories_collection.find_one({"_id": actual_id})
|
|
750
|
+
|
|
751
|
+
if final_doc_raw:
|
|
752
|
+
# Extract payload (where Mem0 stores the actual memory data)
|
|
753
|
+
payload = final_doc_raw.get("payload", {})
|
|
754
|
+
|
|
755
|
+
# Build normalized memory document (same format as get() method)
|
|
756
|
+
final_doc = {
|
|
757
|
+
"id": str(final_doc_raw["_id"]), # Convert _id to id for API consistency
|
|
758
|
+
"memory": payload.get("memory") or payload.get("text"),
|
|
759
|
+
"text": payload.get("text") or payload.get("memory"),
|
|
760
|
+
"metadata": payload.get("metadata", {}),
|
|
761
|
+
"user_id": payload.get("user_id") or payload.get("metadata", {}).get("user_id"),
|
|
762
|
+
"created_at": payload.get("created_at"),
|
|
763
|
+
"updated_at": payload.get("updated_at"),
|
|
764
|
+
}
|
|
578
765
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
766
|
+
# Add any other payload fields
|
|
767
|
+
for key, value in payload.items():
|
|
768
|
+
if key not in [
|
|
769
|
+
"memory",
|
|
770
|
+
"text",
|
|
771
|
+
"metadata",
|
|
772
|
+
"user_id",
|
|
773
|
+
"created_at",
|
|
774
|
+
"updated_at",
|
|
775
|
+
]:
|
|
776
|
+
final_doc[key] = value
|
|
777
|
+
|
|
778
|
+
# Ensure date objects are serialized if your downstream expects strings
|
|
779
|
+
if isinstance(final_doc.get("created_at"), datetime):
|
|
780
|
+
final_doc["created_at"] = final_doc["created_at"].isoformat()
|
|
781
|
+
if isinstance(final_doc.get("updated_at"), datetime):
|
|
782
|
+
final_doc["updated_at"] = final_doc["updated_at"].isoformat()
|
|
783
|
+
else:
|
|
784
|
+
final_doc = None
|
|
585
785
|
|
|
586
786
|
logger.info(
|
|
587
|
-
f"Successfully updated memory {memory_id}
|
|
787
|
+
f"Successfully updated memory {memory_id}",
|
|
588
788
|
extra={
|
|
589
789
|
"memory_id": memory_id,
|
|
590
790
|
"content_updated": bool(normalized_memory),
|
|
591
791
|
"metadata_updated": bool(normalized_metadata),
|
|
592
792
|
},
|
|
593
793
|
)
|
|
594
|
-
return
|
|
794
|
+
return final_doc
|
|
595
795
|
|
|
596
796
|
except ValueError:
|
|
597
797
|
# Re-raise validation errors as-is
|
|
598
798
|
raise
|
|
599
|
-
except
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
799
|
+
except BaseException as e:
|
|
800
|
+
# Catch any unexpected errors during update. We catch BaseException
|
|
801
|
+
# (not Exception) to ensure we always raise Mem0MemoryServiceError for
|
|
802
|
+
# consistent error handling, but we re-raise KeyboardInterrupt and SystemExit
|
|
803
|
+
# to allow proper shutdown.
|
|
804
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
805
|
+
raise
|
|
806
|
+
logger.exception(f"Critical error during memory update for {memory_id}")
|
|
604
807
|
raise Mem0MemoryServiceError(f"Update failed: {e}") from e
|
|
605
808
|
|
|
606
809
|
def _normalize_content_input(
|
|
@@ -609,142 +812,45 @@ class Mem0MemoryService:
|
|
|
609
812
|
data: str | dict[str, Any] | None,
|
|
610
813
|
messages: str | list[dict[str, str]] | None,
|
|
611
814
|
) -> str | None:
|
|
612
|
-
"""
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
Priority: memory > data > messages
|
|
616
|
-
"""
|
|
617
|
-
# Already have memory content
|
|
618
|
-
if memory:
|
|
815
|
+
"""Normalize content input from various parameter formats."""
|
|
816
|
+
if memory is not None:
|
|
619
817
|
if not isinstance(memory, str):
|
|
620
|
-
raise TypeError("memory parameter must be a string")
|
|
818
|
+
raise TypeError(f"memory parameter must be a string, got {type(memory).__name__}")
|
|
621
819
|
return memory.strip() if memory.strip() else None
|
|
622
|
-
|
|
623
|
-
# Check data parameter
|
|
624
|
-
if data:
|
|
820
|
+
if data is not None:
|
|
625
821
|
if isinstance(data, str):
|
|
626
822
|
return data.strip() if data.strip() else None
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
# Check messages parameter
|
|
633
|
-
if messages:
|
|
823
|
+
if isinstance(data, dict):
|
|
824
|
+
return data.get("memory") or data.get("text") or data.get("content")
|
|
825
|
+
raise TypeError(f"data parameter must be a string or dict, got {type(data).__name__}")
|
|
826
|
+
if messages is not None:
|
|
634
827
|
if isinstance(messages, str):
|
|
635
828
|
return messages.strip() if messages.strip() else None
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if isinstance(content, str) and content.strip():
|
|
642
|
-
content_parts.append(content.strip())
|
|
643
|
-
if content_parts:
|
|
644
|
-
return " ".join(content_parts)
|
|
645
|
-
|
|
829
|
+
if isinstance(messages, list):
|
|
830
|
+
return " ".join([m.get("content", "") for m in messages if isinstance(m, dict)])
|
|
831
|
+
raise TypeError(
|
|
832
|
+
f"messages parameter must be a string or list, got {type(messages).__name__}"
|
|
833
|
+
)
|
|
646
834
|
return None
|
|
647
835
|
|
|
648
836
|
def _normalize_metadata_input(
|
|
649
837
|
self, metadata: dict[str, Any] | None, data: dict[str, Any] | None
|
|
650
838
|
) -> dict[str, Any] | None:
|
|
651
839
|
"""Normalize metadata input, extracting from data dict if needed."""
|
|
652
|
-
if metadata is not None and not isinstance(metadata, dict):
|
|
653
|
-
raise TypeError("metadata must be a dict or None")
|
|
654
|
-
|
|
655
|
-
# If metadata provided directly, use it
|
|
656
840
|
if metadata is not None:
|
|
841
|
+
if not isinstance(metadata, dict):
|
|
842
|
+
raise TypeError(f"metadata parameter must be a dict, got {type(metadata).__name__}")
|
|
657
843
|
return metadata
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
844
|
+
if data is not None and isinstance(data, dict):
|
|
845
|
+
metadata_from_data = data.get("metadata")
|
|
846
|
+
if metadata_from_data is not None and not isinstance(metadata_from_data, dict):
|
|
847
|
+
raise TypeError(
|
|
848
|
+
f"metadata in data parameter must be a dict, "
|
|
849
|
+
f"got {type(metadata_from_data).__name__}"
|
|
850
|
+
)
|
|
851
|
+
return metadata_from_data
|
|
665
852
|
return None
|
|
666
853
|
|
|
667
|
-
def _update_via_mem0(
|
|
668
|
-
self,
|
|
669
|
-
memory_id: str,
|
|
670
|
-
user_id: str | None,
|
|
671
|
-
memory: str | None,
|
|
672
|
-
metadata: dict[str, Any] | None,
|
|
673
|
-
**kwargs,
|
|
674
|
-
) -> dict[str, Any] | None:
|
|
675
|
-
"""
|
|
676
|
-
Update memory using Mem0's built-in update method.
|
|
677
|
-
|
|
678
|
-
This is the primary update path. Mem0's update method handles:
|
|
679
|
-
- In-place updates preserving memory ID and created_at timestamp
|
|
680
|
-
- Automatic embedding recomputation when content changes
|
|
681
|
-
- Metadata merging
|
|
682
|
-
- User scoping for security
|
|
683
|
-
|
|
684
|
-
Args:
|
|
685
|
-
memory_id: Memory ID to update
|
|
686
|
-
user_id: User ID for scoping
|
|
687
|
-
memory: New memory content (normalized)
|
|
688
|
-
metadata: Metadata to merge (normalized)
|
|
689
|
-
**kwargs: Additional arguments passed to Mem0
|
|
690
|
-
|
|
691
|
-
Returns:
|
|
692
|
-
Updated memory dict or None if not found
|
|
693
|
-
|
|
694
|
-
Raises:
|
|
695
|
-
Various exceptions from Mem0 if update fails
|
|
696
|
-
"""
|
|
697
|
-
# Build update parameters matching Mem0's API
|
|
698
|
-
# Mem0's update method signature:
|
|
699
|
-
# update(memory_id, text=None, metadata=None, user_id=None, **kwargs)
|
|
700
|
-
update_kwargs: dict[str, Any] = {"memory_id": memory_id}
|
|
701
|
-
|
|
702
|
-
# Add user_id for scoping (Mem0 supports this)
|
|
703
|
-
if user_id:
|
|
704
|
-
update_kwargs["user_id"] = str(user_id)
|
|
705
|
-
|
|
706
|
-
# Add text/content if provided
|
|
707
|
-
# Mem0 uses "text" parameter for content
|
|
708
|
-
if memory:
|
|
709
|
-
update_kwargs["text"] = memory
|
|
710
|
-
|
|
711
|
-
# Add metadata if provided
|
|
712
|
-
# Mem0 merges metadata automatically
|
|
713
|
-
if metadata is not None:
|
|
714
|
-
update_kwargs["metadata"] = metadata
|
|
715
|
-
|
|
716
|
-
# Pass through any additional kwargs
|
|
717
|
-
update_kwargs.update(kwargs)
|
|
718
|
-
|
|
719
|
-
logger.debug(
|
|
720
|
-
f"Calling mem0.update() for memory_id={memory_id}",
|
|
721
|
-
extra={
|
|
722
|
-
"memory_id": memory_id,
|
|
723
|
-
"has_content": bool(memory),
|
|
724
|
-
"has_metadata": bool(metadata),
|
|
725
|
-
"user_id": user_id,
|
|
726
|
-
},
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
# Call Mem0's update method directly
|
|
730
|
-
# This handles all the complexity: embedding recomputation, ID preservation, etc.
|
|
731
|
-
result = self.memory.update(**update_kwargs)
|
|
732
|
-
|
|
733
|
-
# Normalize result format
|
|
734
|
-
# Mem0 may return dict, list, or other formats
|
|
735
|
-
if isinstance(result, dict):
|
|
736
|
-
return result
|
|
737
|
-
elif isinstance(result, list) and len(result) > 0:
|
|
738
|
-
# If list, return first item
|
|
739
|
-
return result[0] if isinstance(result[0], dict) else None
|
|
740
|
-
else:
|
|
741
|
-
# If result is None or unexpected format, return None to trigger fallback
|
|
742
|
-
logger.debug(
|
|
743
|
-
f"Mem0 update returned unexpected format: {type(result)}",
|
|
744
|
-
extra={"memory_id": memory_id},
|
|
745
|
-
)
|
|
746
|
-
return None
|
|
747
|
-
|
|
748
854
|
def _normalize_result(self, result: Any) -> list[dict[str, Any]]:
|
|
749
855
|
"""Normalize Mem0's return type (dict vs list)."""
|
|
750
856
|
if result is None:
|
|
@@ -760,5 +866,35 @@ class Mem0MemoryService:
|
|
|
760
866
|
return []
|
|
761
867
|
|
|
762
868
|
|
|
763
|
-
def get_memory_service(
|
|
764
|
-
|
|
869
|
+
def get_memory_service(
|
|
870
|
+
mongo_uri: str,
|
|
871
|
+
db_name: str,
|
|
872
|
+
app_slug: str,
|
|
873
|
+
config: dict[str, Any] | None = None,
|
|
874
|
+
provider: str = "mem0",
|
|
875
|
+
) -> BaseMemoryService:
|
|
876
|
+
"""
|
|
877
|
+
Factory function to create a memory service instance.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
mongo_uri: MongoDB connection URI
|
|
881
|
+
db_name: Database name
|
|
882
|
+
app_slug: Application slug for scoping
|
|
883
|
+
config: Memory service configuration dictionary
|
|
884
|
+
provider: Memory provider to use (default: "mem0")
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
BaseMemoryService instance (concrete implementation based on provider)
|
|
888
|
+
|
|
889
|
+
Raises:
|
|
890
|
+
ValueError: If provider is not supported
|
|
891
|
+
Mem0MemoryServiceError: If Mem0 provider fails to initialize
|
|
892
|
+
"""
|
|
893
|
+
if provider == "mem0":
|
|
894
|
+
return Mem0MemoryService(mongo_uri, db_name, app_slug, config)
|
|
895
|
+
else:
|
|
896
|
+
raise ValueError(
|
|
897
|
+
f"Unsupported memory provider: {provider}. "
|
|
898
|
+
f"Supported providers: mem0. "
|
|
899
|
+
f"Future providers can be added by implementing BaseMemoryService."
|
|
900
|
+
)
|