mdb-engine 0.7.3__py3-none-any.whl → 0.7.5__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 -1
- 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 +231 -27
- mdb_engine/memory/__init__.py +9 -0
- mdb_engine/memory/base.py +224 -0
- mdb_engine/memory/service.py +415 -162
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.5.dist-info}/METADATA +1 -1
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.5.dist-info}/RECORD +16 -15
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.5.dist-info}/WHEEL +0 -0
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.5.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.5.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.5.dist-info}/top_level.txt +0 -0
mdb_engine/memory/service.py
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
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).
|
|
7
|
+
|
|
8
|
+
v0.7.5: Added inject() method for manual memory insertion without LLM inference,
|
|
9
|
+
and enhanced delete functionality with comprehensive documentation.
|
|
4
10
|
"""
|
|
5
11
|
|
|
6
12
|
import logging
|
|
7
13
|
import os
|
|
8
14
|
import tempfile
|
|
15
|
+
from datetime import datetime
|
|
9
16
|
from typing import Any
|
|
10
17
|
|
|
18
|
+
from .base import BaseMemoryService, MemoryServiceError
|
|
19
|
+
|
|
20
|
+
# Required: Direct PyMongo access
|
|
21
|
+
try:
|
|
22
|
+
from pymongo import MongoClient
|
|
23
|
+
from pymongo.errors import (
|
|
24
|
+
ConfigurationError,
|
|
25
|
+
ConnectionFailure,
|
|
26
|
+
InvalidURI,
|
|
27
|
+
PyMongoError,
|
|
28
|
+
ServerSelectionTimeoutError,
|
|
29
|
+
)
|
|
30
|
+
except ImportError:
|
|
31
|
+
MongoClient = None
|
|
32
|
+
ConnectionFailure = None
|
|
33
|
+
ConfigurationError = None
|
|
34
|
+
ServerSelectionTimeoutError = None
|
|
35
|
+
InvalidURI = None
|
|
36
|
+
PyMongoError = None
|
|
37
|
+
|
|
11
38
|
# Set MEM0_DIR environment variable early to avoid permission issues
|
|
12
39
|
if "MEM0_DIR" not in os.environ:
|
|
13
40
|
mem0_dir = os.path.join(tempfile.gettempdir(), ".mem0")
|
|
@@ -39,23 +66,29 @@ def _check_mem0_available():
|
|
|
39
66
|
logger = logging.getLogger(__name__)
|
|
40
67
|
|
|
41
68
|
|
|
42
|
-
class Mem0MemoryServiceError(
|
|
69
|
+
class Mem0MemoryServiceError(MemoryServiceError):
|
|
70
|
+
"""Exception raised by Mem0MemoryService operations."""
|
|
71
|
+
|
|
43
72
|
pass
|
|
44
73
|
|
|
45
74
|
|
|
46
|
-
class Mem0MemoryService:
|
|
75
|
+
class Mem0MemoryService(BaseMemoryService):
|
|
47
76
|
"""
|
|
48
77
|
Production-ready Mem0 Memory Service with MongoDB integration.
|
|
49
78
|
|
|
50
79
|
Features:
|
|
80
|
+
- Hybrid update pattern: Mem0 for embeddings, MongoDB for data persistence
|
|
81
|
+
- Full metadata support via direct MongoDB access
|
|
51
82
|
- In-place memory updates preserving IDs and timestamps
|
|
52
83
|
- Automatic embedding recomputation on content changes
|
|
53
84
|
- Knowledge graph support (if enabled in Mem0 config)
|
|
54
85
|
- Comprehensive error handling and logging
|
|
55
|
-
-
|
|
86
|
+
- Reliable return values fetched directly from MongoDB
|
|
56
87
|
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
Update Architecture:
|
|
89
|
+
- Content updates routed via Mem0 (triggers re-embedding)
|
|
90
|
+
- Metadata updates routed via direct PyMongo (full control)
|
|
91
|
+
- Final result always fetched from MongoDB (guaranteed structure)
|
|
59
92
|
"""
|
|
60
93
|
|
|
61
94
|
def __init__(
|
|
@@ -77,6 +110,26 @@ class Mem0MemoryService:
|
|
|
77
110
|
self.collection_name = (config or {}).get("collection_name", f"{app_slug}_memories")
|
|
78
111
|
self.infer = (config or {}).get("infer", True)
|
|
79
112
|
|
|
113
|
+
# ---------------------------------------------------------
|
|
114
|
+
# 1. SETUP DIRECT MONGODB ACCESS (The "Backdoor")
|
|
115
|
+
# ---------------------------------------------------------
|
|
116
|
+
if MongoClient is None:
|
|
117
|
+
raise Mem0MemoryServiceError("pymongo is required. pip install pymongo")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
self._client = MongoClient(mongo_uri)
|
|
121
|
+
self._db = self._client[db_name]
|
|
122
|
+
self.memories_collection = self._db[self.collection_name]
|
|
123
|
+
logger.info(f"✅ Direct MongoDB connection established for {self.collection_name}")
|
|
124
|
+
except BaseException as e:
|
|
125
|
+
# MongoDB connection may raise various exceptions. We catch BaseException
|
|
126
|
+
# (not Exception) to ensure we always raise Mem0MemoryServiceError for
|
|
127
|
+
# consistent error handling, but we re-raise KeyboardInterrupt and SystemExit
|
|
128
|
+
# to allow proper shutdown.
|
|
129
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
130
|
+
raise
|
|
131
|
+
raise Mem0MemoryServiceError(f"Failed to connect to MongoDB directly: {e}") from e
|
|
132
|
+
|
|
80
133
|
# Ensure GOOGLE_API_KEY is set for mem0 compatibility
|
|
81
134
|
# (mem0 expects GOOGLE_API_KEY, not GEMINI_API_KEY)
|
|
82
135
|
# This ensures we use the DIRECT Gemini API
|
|
@@ -369,6 +422,100 @@ class Mem0MemoryService:
|
|
|
369
422
|
logger.exception("Mem0 Add Failed")
|
|
370
423
|
raise Mem0MemoryServiceError(f"Add failed: {e}") from e
|
|
371
424
|
|
|
425
|
+
def inject(
|
|
426
|
+
self,
|
|
427
|
+
memory: str | dict[str, Any],
|
|
428
|
+
user_id: str | None = None,
|
|
429
|
+
metadata: dict[str, Any] | None = None,
|
|
430
|
+
**kwargs,
|
|
431
|
+
) -> dict[str, Any]:
|
|
432
|
+
"""
|
|
433
|
+
Manually inject a memory without LLM inference.
|
|
434
|
+
|
|
435
|
+
This method allows direct insertion of memories without going through
|
|
436
|
+
the inference pipeline. Useful for manually adding facts, preferences,
|
|
437
|
+
or other structured data.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
memory: Memory content as a string or dict with memory/text/content key
|
|
441
|
+
user_id: User ID for scoping (optional but recommended)
|
|
442
|
+
metadata: Additional metadata to store with the memory
|
|
443
|
+
**kwargs: Additional provider-specific arguments
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Created memory object with ID and metadata
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
Mem0MemoryServiceError: If injection operation fails
|
|
450
|
+
ValueError: If memory content is invalid or empty
|
|
451
|
+
"""
|
|
452
|
+
# Normalize input: convert dict to string if needed
|
|
453
|
+
if isinstance(memory, dict):
|
|
454
|
+
# Extract memory content from dict (support multiple key formats)
|
|
455
|
+
memory_content = (
|
|
456
|
+
memory.get("memory") or memory.get("text") or memory.get("content") or str(memory)
|
|
457
|
+
)
|
|
458
|
+
if not memory_content or not isinstance(memory_content, str):
|
|
459
|
+
raise ValueError(
|
|
460
|
+
"Memory dict must contain 'memory', 'text', or 'content' key with string value"
|
|
461
|
+
)
|
|
462
|
+
# Merge any metadata from the dict
|
|
463
|
+
if "metadata" in memory and isinstance(memory["metadata"], dict):
|
|
464
|
+
final_metadata = dict(metadata) if metadata else {}
|
|
465
|
+
final_metadata.update(memory["metadata"])
|
|
466
|
+
metadata = final_metadata
|
|
467
|
+
elif isinstance(memory, str):
|
|
468
|
+
memory_content = memory.strip()
|
|
469
|
+
if not memory_content:
|
|
470
|
+
raise ValueError("Memory content cannot be empty")
|
|
471
|
+
else:
|
|
472
|
+
raise TypeError(f"Memory must be a string or dict, got {type(memory).__name__}")
|
|
473
|
+
|
|
474
|
+
# Convert to messages format for add() method
|
|
475
|
+
messages = [{"role": "user", "content": memory_content}]
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
# Call add() with infer=False to bypass LLM inference
|
|
479
|
+
logger.debug(
|
|
480
|
+
f"Injecting memory without inference for user_id={user_id}, "
|
|
481
|
+
f"memory_length={len(memory_content)}"
|
|
482
|
+
)
|
|
483
|
+
result = self.add(
|
|
484
|
+
messages=messages,
|
|
485
|
+
user_id=user_id,
|
|
486
|
+
metadata=metadata,
|
|
487
|
+
infer=False, # Explicitly disable inference
|
|
488
|
+
**kwargs,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Return the first created memory (normalized format)
|
|
492
|
+
if result and isinstance(result, list) and len(result) > 0:
|
|
493
|
+
injected_memory = result[0]
|
|
494
|
+
logger.info(
|
|
495
|
+
f"Successfully injected memory with id={injected_memory.get('id')} "
|
|
496
|
+
f"for user_id={user_id}"
|
|
497
|
+
)
|
|
498
|
+
return injected_memory
|
|
499
|
+
else:
|
|
500
|
+
# This shouldn't happen, but handle gracefully
|
|
501
|
+
logger.warning(
|
|
502
|
+
f"add() returned empty result for inject() call. "
|
|
503
|
+
f"user_id={user_id}, memory_length={len(memory_content)}"
|
|
504
|
+
)
|
|
505
|
+
raise Mem0MemoryServiceError("Failed to inject memory: add() returned empty result")
|
|
506
|
+
except (ValueError, TypeError):
|
|
507
|
+
# Re-raise validation errors as-is
|
|
508
|
+
raise
|
|
509
|
+
except (
|
|
510
|
+
ConnectionError,
|
|
511
|
+
OSError,
|
|
512
|
+
AttributeError,
|
|
513
|
+
RuntimeError,
|
|
514
|
+
KeyError,
|
|
515
|
+
) as e:
|
|
516
|
+
logger.exception("Mem0 inject failed")
|
|
517
|
+
raise Mem0MemoryServiceError(f"Inject failed: {e}") from e
|
|
518
|
+
|
|
372
519
|
def get_all(
|
|
373
520
|
self,
|
|
374
521
|
user_id: str | None = None,
|
|
@@ -437,8 +584,50 @@ class Mem0MemoryService:
|
|
|
437
584
|
return []
|
|
438
585
|
|
|
439
586
|
def get(self, memory_id: str, user_id: str | None = None, **kwargs) -> dict[str, Any]:
|
|
587
|
+
"""
|
|
588
|
+
Get memory by ID using direct MongoDB access for reliability.
|
|
589
|
+
|
|
590
|
+
Mem0 stores memories with _id as the MongoDB document ID.
|
|
591
|
+
Memory content and metadata are stored in the 'payload' field.
|
|
592
|
+
"""
|
|
440
593
|
try:
|
|
441
|
-
|
|
594
|
+
# Mem0 uses _id as the MongoDB document ID
|
|
595
|
+
doc = self.memories_collection.find_one({"_id": memory_id})
|
|
596
|
+
if doc:
|
|
597
|
+
# Extract payload (where Mem0 stores the actual memory data)
|
|
598
|
+
payload = doc.get("payload", {})
|
|
599
|
+
|
|
600
|
+
# Build normalized memory document
|
|
601
|
+
memory_doc = {
|
|
602
|
+
"id": str(doc["_id"]), # Convert _id to id for API consistency
|
|
603
|
+
"memory": payload.get("memory") or payload.get("text"),
|
|
604
|
+
"text": payload.get("text") or payload.get("memory"),
|
|
605
|
+
"metadata": payload.get("metadata", {}),
|
|
606
|
+
"user_id": payload.get("user_id") or payload.get("metadata", {}).get("user_id"),
|
|
607
|
+
"created_at": payload.get("created_at"),
|
|
608
|
+
"updated_at": payload.get("updated_at"),
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# Add any other payload fields
|
|
612
|
+
for key, value in payload.items():
|
|
613
|
+
if key not in [
|
|
614
|
+
"memory",
|
|
615
|
+
"text",
|
|
616
|
+
"metadata",
|
|
617
|
+
"user_id",
|
|
618
|
+
"created_at",
|
|
619
|
+
"updated_at",
|
|
620
|
+
]:
|
|
621
|
+
memory_doc[key] = value
|
|
622
|
+
|
|
623
|
+
# Optional: Filter by user_id if provided
|
|
624
|
+
if user_id:
|
|
625
|
+
doc_user_id = memory_doc.get("user_id")
|
|
626
|
+
if doc_user_id and str(doc_user_id) != str(user_id):
|
|
627
|
+
return None
|
|
628
|
+
|
|
629
|
+
return memory_doc
|
|
630
|
+
return None
|
|
442
631
|
except (
|
|
443
632
|
ValueError,
|
|
444
633
|
TypeError,
|
|
@@ -448,7 +637,19 @@ class Mem0MemoryService:
|
|
|
448
637
|
RuntimeError,
|
|
449
638
|
KeyError,
|
|
450
639
|
):
|
|
451
|
-
|
|
640
|
+
# Fallback to Mem0 if direct access fails
|
|
641
|
+
try:
|
|
642
|
+
return self.memory.get(memory_id, **kwargs)
|
|
643
|
+
except (
|
|
644
|
+
ValueError,
|
|
645
|
+
TypeError,
|
|
646
|
+
ConnectionError,
|
|
647
|
+
OSError,
|
|
648
|
+
AttributeError,
|
|
649
|
+
RuntimeError,
|
|
650
|
+
KeyError,
|
|
651
|
+
):
|
|
652
|
+
return None
|
|
452
653
|
|
|
453
654
|
def delete(self, memory_id: str, user_id: str | None = None, **kwargs) -> bool:
|
|
454
655
|
try:
|
|
@@ -491,15 +692,27 @@ class Mem0MemoryService:
|
|
|
491
692
|
**kwargs,
|
|
492
693
|
) -> dict[str, Any] | None:
|
|
493
694
|
"""
|
|
494
|
-
|
|
695
|
+
Robust Hybrid Update Pattern:
|
|
696
|
+
|
|
697
|
+
This method uses a hybrid approach that combines Mem0's embedding capabilities
|
|
698
|
+
with direct MongoDB control for maximum flexibility and reliability.
|
|
699
|
+
|
|
700
|
+
**Architecture:**
|
|
701
|
+
1. **Content Updates** → Routed via Mem0 (triggers automatic re-embedding)
|
|
702
|
+
2. **Metadata Updates** → Routed via direct PyMongo (full control, no API limitations)
|
|
703
|
+
3. **Return Value** → Always fetched from MongoDB (guaranteed correct structure)
|
|
704
|
+
|
|
705
|
+
**Why Hybrid?**
|
|
706
|
+
- Mem0's update() API doesn't support metadata parameter
|
|
707
|
+
- Mem0's return values can be inconsistent (dict, list, or status messages)
|
|
708
|
+
- Direct MongoDB access gives us full control over data persistence
|
|
709
|
+
- We use Mem0 purely as an "embedding utility" for content changes
|
|
495
710
|
|
|
496
711
|
Updates the memory content and/or metadata while preserving:
|
|
497
712
|
- Original memory ID (never changes)
|
|
498
713
|
- Creation timestamp (created_at) - preserved
|
|
499
714
|
- Other existing fields - preserved unless explicitly updated
|
|
500
715
|
|
|
501
|
-
If content is updated, the embedding vector is automatically recomputed.
|
|
502
|
-
|
|
503
716
|
Args:
|
|
504
717
|
memory_id: The ID of the memory to update (required)
|
|
505
718
|
user_id: The user ID who owns the memory (for scoping and security)
|
|
@@ -508,12 +721,13 @@ class Mem0MemoryService:
|
|
|
508
721
|
Can be a string or dict with 'memory'/'text'/'content' key.
|
|
509
722
|
messages: Alternative way to provide content as messages (optional).
|
|
510
723
|
Can be a string or list of dicts with 'content' key.
|
|
511
|
-
metadata: Metadata updates (
|
|
512
|
-
|
|
724
|
+
metadata: Metadata updates (FULLY SUPPORTED via direct MongoDB).
|
|
725
|
+
Can update any metadata field, not limited by Mem0 API.
|
|
513
726
|
**kwargs: Additional arguments passed to Mem0 operations
|
|
514
727
|
|
|
515
728
|
Returns:
|
|
516
|
-
Updated memory object with same ID,
|
|
729
|
+
Updated memory object with same ID, fetched directly from MongoDB,
|
|
730
|
+
or None if memory not found
|
|
517
731
|
|
|
518
732
|
Raises:
|
|
519
733
|
Mem0MemoryServiceError: If update operation fails
|
|
@@ -521,7 +735,7 @@ class Mem0MemoryService:
|
|
|
521
735
|
|
|
522
736
|
Example:
|
|
523
737
|
```python
|
|
524
|
-
# Update content and metadata
|
|
738
|
+
# Update content and metadata (hybrid approach)
|
|
525
739
|
updated = memory_service.update(
|
|
526
740
|
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
527
741
|
user_id="user123",
|
|
@@ -529,78 +743,164 @@ class Mem0MemoryService:
|
|
|
529
743
|
metadata={"category": "technical", "updated": True}
|
|
530
744
|
)
|
|
531
745
|
|
|
532
|
-
# Update only metadata (content unchanged)
|
|
746
|
+
# Update only metadata (content unchanged) - FULLY SUPPORTED
|
|
747
|
+
updated = memory_service.update(
|
|
748
|
+
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
749
|
+
user_id="user123",
|
|
750
|
+
metadata={"category": "updated", "priority": "high"}
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
# Update only content (no metadata)
|
|
533
754
|
updated = memory_service.update(
|
|
534
755
|
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
535
756
|
user_id="user123",
|
|
536
|
-
|
|
757
|
+
memory="Updated content only"
|
|
537
758
|
)
|
|
538
759
|
```
|
|
539
760
|
"""
|
|
540
761
|
if not memory_id or not isinstance(memory_id, str) or not memory_id.strip():
|
|
541
762
|
raise ValueError("memory_id is required and must be a non-empty string")
|
|
542
763
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
normalized_metadata = self._normalize_metadata_input(metadata, data)
|
|
764
|
+
# 1. Normalize Inputs
|
|
765
|
+
normalized_memory = self._normalize_content_input(memory, data, messages)
|
|
766
|
+
normalized_metadata = self._normalize_metadata_input(metadata, data)
|
|
547
767
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
768
|
+
# 2. Check Existence (Fast check via ID)
|
|
769
|
+
# Mem0 uses _id as the MongoDB document ID
|
|
770
|
+
existing = self.memories_collection.find_one(
|
|
771
|
+
{"_id": memory_id}, {"_id": 1, "payload.user_id": 1, "payload.metadata.user_id": 1}
|
|
772
|
+
)
|
|
773
|
+
if not existing:
|
|
774
|
+
logger.warning(f"Memory {memory_id} not found.")
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
# Optional: Security Scope Check
|
|
778
|
+
# Check user_id in payload (Mem0 stores it there)
|
|
779
|
+
if user_id:
|
|
780
|
+
payload = existing.get("payload", {})
|
|
781
|
+
existing_user_id = payload.get("user_id") or payload.get("metadata", {}).get("user_id")
|
|
782
|
+
if existing_user_id and str(existing_user_id) != str(user_id):
|
|
783
|
+
logger.warning(f"Unauthorized update attempt for {memory_id} by {user_id}")
|
|
554
784
|
return None
|
|
555
785
|
|
|
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
|
-
)
|
|
786
|
+
# Use _id directly (Mem0's format)
|
|
787
|
+
actual_id = memory_id
|
|
570
788
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
789
|
+
try:
|
|
790
|
+
# -------------------------------------------------
|
|
791
|
+
# STEP A: Content Update (Via Mem0 for Vectors)
|
|
792
|
+
# -------------------------------------------------
|
|
793
|
+
if normalized_memory:
|
|
794
|
+
logger.info(f"📝 Updating content for {actual_id} (triggering re-embedding)")
|
|
795
|
+
# We use Mem0 here specifically because it handles the embedding logic.
|
|
796
|
+
# We do NOT care what it returns.
|
|
797
|
+
try:
|
|
798
|
+
# Use the actual_id (which Mem0 recognizes)
|
|
799
|
+
self.memory.update(memory_id=actual_id, data=normalized_memory)
|
|
800
|
+
except BaseException as e:
|
|
801
|
+
# Mem0 is a third-party library that may raise any exception.
|
|
802
|
+
# We catch BaseException (not Exception) to ensure we always raise
|
|
803
|
+
# Mem0MemoryServiceError for consistent error handling, but we
|
|
804
|
+
# re-raise KeyboardInterrupt and SystemExit to allow proper shutdown.
|
|
805
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
806
|
+
raise
|
|
807
|
+
# If Mem0 fails (e.g. LLM rate limit, API error), we should abort
|
|
808
|
+
# or fall back to just text update without vector (risky for search).
|
|
809
|
+
logger.exception(f"Mem0 embedding update failed: {e}")
|
|
810
|
+
raise Mem0MemoryServiceError(f"Content update failed: {e}") from e
|
|
811
|
+
|
|
812
|
+
# -------------------------------------------------
|
|
813
|
+
# STEP B: Metadata Update (Direct PyMongo)
|
|
814
|
+
# -------------------------------------------------
|
|
815
|
+
if normalized_metadata:
|
|
816
|
+
logger.info(f"🏷️ Updating metadata for {actual_id}")
|
|
817
|
+
|
|
818
|
+
# Ensure user_id is in metadata for consistency
|
|
819
|
+
if user_id:
|
|
820
|
+
normalized_metadata["user_id"] = str(user_id)
|
|
821
|
+
|
|
822
|
+
# Mem0 stores everything in the 'payload' field
|
|
823
|
+
# We need to update payload.metadata and payload.user_id
|
|
824
|
+
update_fields = {}
|
|
825
|
+
|
|
826
|
+
# Handle metadata updates (nested under payload.metadata)
|
|
827
|
+
for k, v in normalized_metadata.items():
|
|
828
|
+
if k == "user_id":
|
|
829
|
+
# user_id can be at payload.user_id or payload.metadata.user_id
|
|
830
|
+
update_fields["payload.user_id"] = v
|
|
831
|
+
update_fields["payload.metadata.user_id"] = v
|
|
832
|
+
else:
|
|
833
|
+
update_fields[f"payload.metadata.{k}"] = v
|
|
834
|
+
|
|
835
|
+
# Add timestamp to payload
|
|
836
|
+
update_fields["payload.updated_at"] = datetime.utcnow().isoformat()
|
|
837
|
+
|
|
838
|
+
# Execute Atomic Update - Mem0 uses _id
|
|
839
|
+
self.memories_collection.update_one({"_id": actual_id}, {"$set": update_fields})
|
|
840
|
+
|
|
841
|
+
# -------------------------------------------------
|
|
842
|
+
# STEP C: Return The Truth (Direct DB Fetch)
|
|
843
|
+
# -------------------------------------------------
|
|
844
|
+
# We completely ignore Mem0's return value (which might be {"message": "ok"})
|
|
845
|
+
# and fetch the actual document from the database.
|
|
846
|
+
final_doc_raw = self.memories_collection.find_one({"_id": actual_id})
|
|
847
|
+
|
|
848
|
+
if final_doc_raw:
|
|
849
|
+
# Extract payload (where Mem0 stores the actual memory data)
|
|
850
|
+
payload = final_doc_raw.get("payload", {})
|
|
851
|
+
|
|
852
|
+
# Build normalized memory document (same format as get() method)
|
|
853
|
+
final_doc = {
|
|
854
|
+
"id": str(final_doc_raw["_id"]), # Convert _id to id for API consistency
|
|
855
|
+
"memory": payload.get("memory") or payload.get("text"),
|
|
856
|
+
"text": payload.get("text") or payload.get("memory"),
|
|
857
|
+
"metadata": payload.get("metadata", {}),
|
|
858
|
+
"user_id": payload.get("user_id") or payload.get("metadata", {}).get("user_id"),
|
|
859
|
+
"created_at": payload.get("created_at"),
|
|
860
|
+
"updated_at": payload.get("updated_at"),
|
|
861
|
+
}
|
|
578
862
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
863
|
+
# Add any other payload fields
|
|
864
|
+
for key, value in payload.items():
|
|
865
|
+
if key not in [
|
|
866
|
+
"memory",
|
|
867
|
+
"text",
|
|
868
|
+
"metadata",
|
|
869
|
+
"user_id",
|
|
870
|
+
"created_at",
|
|
871
|
+
"updated_at",
|
|
872
|
+
]:
|
|
873
|
+
final_doc[key] = value
|
|
874
|
+
|
|
875
|
+
# Ensure date objects are serialized if your downstream expects strings
|
|
876
|
+
if isinstance(final_doc.get("created_at"), datetime):
|
|
877
|
+
final_doc["created_at"] = final_doc["created_at"].isoformat()
|
|
878
|
+
if isinstance(final_doc.get("updated_at"), datetime):
|
|
879
|
+
final_doc["updated_at"] = final_doc["updated_at"].isoformat()
|
|
880
|
+
else:
|
|
881
|
+
final_doc = None
|
|
585
882
|
|
|
586
883
|
logger.info(
|
|
587
|
-
f"Successfully updated memory {memory_id}
|
|
884
|
+
f"Successfully updated memory {memory_id}",
|
|
588
885
|
extra={
|
|
589
886
|
"memory_id": memory_id,
|
|
590
887
|
"content_updated": bool(normalized_memory),
|
|
591
888
|
"metadata_updated": bool(normalized_metadata),
|
|
592
889
|
},
|
|
593
890
|
)
|
|
594
|
-
return
|
|
891
|
+
return final_doc
|
|
595
892
|
|
|
596
893
|
except ValueError:
|
|
597
894
|
# Re-raise validation errors as-is
|
|
598
895
|
raise
|
|
599
|
-
except
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
896
|
+
except BaseException as e:
|
|
897
|
+
# Catch any unexpected errors during update. We catch BaseException
|
|
898
|
+
# (not Exception) to ensure we always raise Mem0MemoryServiceError for
|
|
899
|
+
# consistent error handling, but we re-raise KeyboardInterrupt and SystemExit
|
|
900
|
+
# to allow proper shutdown.
|
|
901
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
902
|
+
raise
|
|
903
|
+
logger.exception(f"Critical error during memory update for {memory_id}")
|
|
604
904
|
raise Mem0MemoryServiceError(f"Update failed: {e}") from e
|
|
605
905
|
|
|
606
906
|
def _normalize_content_input(
|
|
@@ -609,122 +909,45 @@ class Mem0MemoryService:
|
|
|
609
909
|
data: str | dict[str, Any] | None,
|
|
610
910
|
messages: str | list[dict[str, str]] | None,
|
|
611
911
|
) -> str | None:
|
|
612
|
-
"""
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
Priority: memory > data > messages
|
|
616
|
-
"""
|
|
617
|
-
# Already have memory content
|
|
618
|
-
if memory:
|
|
912
|
+
"""Normalize content input from various parameter formats."""
|
|
913
|
+
if memory is not None:
|
|
619
914
|
if not isinstance(memory, str):
|
|
620
|
-
raise TypeError("memory parameter must be a string")
|
|
915
|
+
raise TypeError(f"memory parameter must be a string, got {type(memory).__name__}")
|
|
621
916
|
return memory.strip() if memory.strip() else None
|
|
622
|
-
|
|
623
|
-
# Check data parameter
|
|
624
|
-
if data:
|
|
917
|
+
if data is not None:
|
|
625
918
|
if isinstance(data, str):
|
|
626
919
|
return data.strip() if data.strip() else None
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
# Check messages parameter
|
|
633
|
-
if messages:
|
|
920
|
+
if isinstance(data, dict):
|
|
921
|
+
return data.get("memory") or data.get("text") or data.get("content")
|
|
922
|
+
raise TypeError(f"data parameter must be a string or dict, got {type(data).__name__}")
|
|
923
|
+
if messages is not None:
|
|
634
924
|
if isinstance(messages, str):
|
|
635
925
|
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
|
-
|
|
926
|
+
if isinstance(messages, list):
|
|
927
|
+
return " ".join([m.get("content", "") for m in messages if isinstance(m, dict)])
|
|
928
|
+
raise TypeError(
|
|
929
|
+
f"messages parameter must be a string or list, got {type(messages).__name__}"
|
|
930
|
+
)
|
|
646
931
|
return None
|
|
647
932
|
|
|
648
933
|
def _normalize_metadata_input(
|
|
649
934
|
self, metadata: dict[str, Any] | None, data: dict[str, Any] | None
|
|
650
935
|
) -> dict[str, Any] | None:
|
|
651
936
|
"""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
937
|
if metadata is not None:
|
|
938
|
+
if not isinstance(metadata, dict):
|
|
939
|
+
raise TypeError(f"metadata parameter must be a dict, got {type(metadata).__name__}")
|
|
657
940
|
return metadata
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
941
|
+
if data is not None and isinstance(data, dict):
|
|
942
|
+
metadata_from_data = data.get("metadata")
|
|
943
|
+
if metadata_from_data is not None and not isinstance(metadata_from_data, dict):
|
|
944
|
+
raise TypeError(
|
|
945
|
+
f"metadata in data parameter must be a dict, "
|
|
946
|
+
f"got {type(metadata_from_data).__name__}"
|
|
947
|
+
)
|
|
948
|
+
return metadata_from_data
|
|
665
949
|
return None
|
|
666
950
|
|
|
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
|
-
Note: Mem0's update() only supports content updates (text parameter).
|
|
679
|
-
Metadata updates are not supported by the Mem0 API.
|
|
680
|
-
|
|
681
|
-
Args:
|
|
682
|
-
memory_id: Memory ID to update
|
|
683
|
-
user_id: User ID for scoping (not used in update call)
|
|
684
|
-
memory: New memory content (normalized)
|
|
685
|
-
metadata: Metadata to merge (ignored - not supported by Mem0 update API)
|
|
686
|
-
**kwargs: Additional arguments passed to Mem0
|
|
687
|
-
|
|
688
|
-
Returns:
|
|
689
|
-
Updated memory dict or None if not found
|
|
690
|
-
"""
|
|
691
|
-
# Filter out user_id from kwargs to prevent passing it as a direct parameter
|
|
692
|
-
filtered_kwargs = {k: v for k, v in kwargs.items() if k != "user_id"}
|
|
693
|
-
|
|
694
|
-
logger.debug(
|
|
695
|
-
f"Calling mem0.update() for memory_id={memory_id}",
|
|
696
|
-
extra={
|
|
697
|
-
"memory_id": memory_id,
|
|
698
|
-
"has_content": bool(memory),
|
|
699
|
-
"has_metadata": bool(metadata),
|
|
700
|
-
"user_id": user_id,
|
|
701
|
-
},
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
if metadata:
|
|
705
|
-
logger.warning(
|
|
706
|
-
f"Metadata update requested for memory {memory_id} but Mem0 update() "
|
|
707
|
-
f"does not support metadata parameter. Metadata will not be updated."
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
update_kwargs = {"memory_id": memory_id}
|
|
711
|
-
if memory:
|
|
712
|
-
update_kwargs["data"] = memory
|
|
713
|
-
update_kwargs.update(filtered_kwargs)
|
|
714
|
-
|
|
715
|
-
result = self.memory.update(**update_kwargs)
|
|
716
|
-
|
|
717
|
-
# Note: Mem0's update() doesn't support metadata parameter
|
|
718
|
-
# Metadata updates would require direct MongoDB access, which we avoid
|
|
719
|
-
|
|
720
|
-
# Normalize result to dict
|
|
721
|
-
if isinstance(result, dict):
|
|
722
|
-
return result
|
|
723
|
-
elif isinstance(result, list) and len(result) > 0:
|
|
724
|
-
return result[0] if isinstance(result[0], dict) else None
|
|
725
|
-
else:
|
|
726
|
-
return self.get(memory_id=memory_id)
|
|
727
|
-
|
|
728
951
|
def _normalize_result(self, result: Any) -> list[dict[str, Any]]:
|
|
729
952
|
"""Normalize Mem0's return type (dict vs list)."""
|
|
730
953
|
if result is None:
|
|
@@ -740,5 +963,35 @@ class Mem0MemoryService:
|
|
|
740
963
|
return []
|
|
741
964
|
|
|
742
965
|
|
|
743
|
-
def get_memory_service(
|
|
744
|
-
|
|
966
|
+
def get_memory_service(
|
|
967
|
+
mongo_uri: str,
|
|
968
|
+
db_name: str,
|
|
969
|
+
app_slug: str,
|
|
970
|
+
config: dict[str, Any] | None = None,
|
|
971
|
+
provider: str = "mem0",
|
|
972
|
+
) -> BaseMemoryService:
|
|
973
|
+
"""
|
|
974
|
+
Factory function to create a memory service instance.
|
|
975
|
+
|
|
976
|
+
Args:
|
|
977
|
+
mongo_uri: MongoDB connection URI
|
|
978
|
+
db_name: Database name
|
|
979
|
+
app_slug: Application slug for scoping
|
|
980
|
+
config: Memory service configuration dictionary
|
|
981
|
+
provider: Memory provider to use (default: "mem0")
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
BaseMemoryService instance (concrete implementation based on provider)
|
|
985
|
+
|
|
986
|
+
Raises:
|
|
987
|
+
ValueError: If provider is not supported
|
|
988
|
+
Mem0MemoryServiceError: If Mem0 provider fails to initialize
|
|
989
|
+
"""
|
|
990
|
+
if provider == "mem0":
|
|
991
|
+
return Mem0MemoryService(mongo_uri, db_name, app_slug, config)
|
|
992
|
+
else:
|
|
993
|
+
raise ValueError(
|
|
994
|
+
f"Unsupported memory provider: {provider}. "
|
|
995
|
+
f"Supported providers: mem0. "
|
|
996
|
+
f"Future providers can be added by implementing BaseMemoryService."
|
|
997
|
+
)
|