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.
@@ -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(Exception):
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
- - Backward compatibility with existing code
86
+ - Reliable return values fetched directly from MongoDB
56
87
 
57
- All operations go through Mem0's API to ensure proper state management,
58
- graph updates, and relationship handling.
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
- return self.memory.get(memory_id, **kwargs)
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
- return None
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
- Update an existing memory in-place with production-grade error handling.
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 (optional, but NOT SUPPORTED by Mem0 update API).
512
- This parameter is accepted for API consistency but will be ignored.
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, or None if memory not found
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
- metadata={"category": "updated"}
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
- try:
544
- # Normalize data parameter (alternative to memory parameter)
545
- normalized_memory = self._normalize_content_input(memory, data, messages)
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
- 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
- )
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
- # 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
- )
786
+ # Use _id directly (Mem0's format)
787
+ actual_id = memory_id
570
788
 
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
- )
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
- 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
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} using Mem0 update method",
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 result
891
+ return final_doc
595
892
 
596
893
  except ValueError:
597
894
  # Re-raise validation errors as-is
598
895
  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
- )
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
- Normalize content input from various parameter formats.
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
- 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:
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
- 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
-
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
- # 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
-
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(mongo_uri, db_name, app_slug, config=None):
744
- return Mem0MemoryService(mongo_uri, db_name, app_slug, config)
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
+ )