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.
@@ -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(Exception):
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
- - Backward compatibility with existing code
83
+ - Reliable return values fetched directly from MongoDB
56
84
 
57
- All operations go through Mem0's API to ensure proper state management,
58
- graph updates, and relationship handling.
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
- return self.memory.get(memory_id, **kwargs)
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
- return None
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
- Update an existing memory in-place with production-grade error handling.
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 (backward compatibility).
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 to merge with existing metadata (optional).
510
- Merged, not replaced - existing keys are preserved unless overridden.
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, or None if memory not found
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
- metadata={"category": "updated"}
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
- try:
543
- # Normalize data parameter (backward compatibility)
544
- normalized_memory = self._normalize_content_input(memory, data, messages)
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
- # Verify memory exists before attempting update
548
- existing_memory = self.get(memory_id=memory_id, user_id=user_id, **kwargs)
549
- if not existing_memory:
550
- logger.warning(
551
- f"Memory {memory_id} not found for update",
552
- extra={"memory_id": memory_id, "user_id": user_id},
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
- # Use Mem0's built-in update method
557
- # Mem0's Memory class update method handles:
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
- result = self._update_via_mem0(
572
- memory_id=memory_id,
573
- user_id=user_id,
574
- memory=normalized_memory,
575
- metadata=normalized_metadata,
576
- **kwargs,
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
- if result is None:
580
- logger.warning(
581
- f"Mem0 update returned None for memory {memory_id}",
582
- extra={"memory_id": memory_id, "user_id": user_id},
583
- )
584
- return None
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} using Mem0 update method",
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 result
794
+ return final_doc
595
795
 
596
796
  except ValueError:
597
797
  # Re-raise validation errors as-is
598
798
  raise
599
- except (AttributeError, TypeError, ValueError, KeyError) as e:
600
- logger.exception(
601
- f"Error updating memory {memory_id}",
602
- extra={"memory_id": memory_id, "user_id": user_id},
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
- Normalize content input from various parameter formats.
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
- elif isinstance(data, dict):
628
- content = data.get("memory") or data.get("text") or data.get("content")
629
- if content and isinstance(content, str):
630
- return content.strip() if content.strip() else None
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
- elif isinstance(messages, list):
637
- content_parts = []
638
- for msg in messages:
639
- if isinstance(msg, dict) and "content" in msg:
640
- content = msg["content"]
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
- # Check if metadata is in data dict
660
- if isinstance(data, dict) and "metadata" in data:
661
- data_metadata = data.get("metadata")
662
- if isinstance(data_metadata, dict):
663
- return data_metadata
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(mongo_uri, db_name, app_slug, config=None):
764
- return Mem0MemoryService(mongo_uri, db_name, app_slug, config)
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
+ )