mdb-engine 0.2.3__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,6 @@
1
1
  """
2
2
  Mem0 Memory Service Implementation
3
-
4
- This module provides a wrapper around Mem0.ai for intelligent memory management.
5
- It integrates seamlessly with mdb-engine's MongoDB connection.
6
- mem0 handles embeddings and LLM via environment variables (.env).
3
+ Production-ready wrapper for Mem0.ai with strict metadata schema for MongoDB.
7
4
  """
8
5
 
9
6
  import logging
@@ -12,32 +9,21 @@ import tempfile
12
9
  from typing import Any
13
10
 
14
11
  # Set MEM0_DIR environment variable early to avoid permission issues
15
- # mem0 tries to create .mem0 directory at import time, so we set this before any import
16
12
  if "MEM0_DIR" not in os.environ:
17
- # Use /tmp/.mem0 which should be writable in most environments
18
13
  mem0_dir = os.path.join(tempfile.gettempdir(), ".mem0")
19
14
  try:
20
15
  os.makedirs(mem0_dir, exist_ok=True)
21
16
  os.environ["MEM0_DIR"] = mem0_dir
22
17
  except OSError:
23
- # Fallback: try user's home directory
24
- try:
25
- home_dir = os.path.expanduser("~")
26
- mem0_dir = os.path.join(home_dir, ".mem0")
27
- os.makedirs(mem0_dir, exist_ok=True)
28
- os.environ["MEM0_DIR"] = mem0_dir
29
- except OSError:
30
- # Last resort: current directory (may fail but won't crash import)
31
- os.environ["MEM0_DIR"] = os.path.join(os.getcwd(), ".mem0")
18
+ # Fallback: current directory
19
+ os.environ["MEM0_DIR"] = os.path.join(os.getcwd(), ".mem0")
32
20
 
33
- # Try to import mem0 (optional dependency)
34
- # Import is lazy to avoid permission issues at module load time
21
+ # Lazy Import
35
22
  MEM0_AVAILABLE = None
36
23
  Memory = None
37
24
 
38
25
 
39
26
  def _check_mem0_available():
40
- """Lazy check if mem0 is available."""
41
27
  global MEM0_AVAILABLE, Memory
42
28
  if MEM0_AVAILABLE is None:
43
29
  try:
@@ -47,284 +33,17 @@ def _check_mem0_available():
47
33
  except ImportError:
48
34
  MEM0_AVAILABLE = False
49
35
  Memory = None
50
- except OSError as e:
51
- logger.warning(f"Failed to set up mem0 directory: {e}. Memory features may be limited.")
52
- MEM0_AVAILABLE = False
53
- Memory = None
54
-
55
36
  return MEM0_AVAILABLE
56
37
 
57
38
 
58
39
  logger = logging.getLogger(__name__)
59
40
 
60
41
 
61
- def _detect_provider_from_env() -> str:
62
- """
63
- Detect provider from environment variables.
64
-
65
- Returns:
66
- "azure" if Azure OpenAI credentials are present, otherwise "openai"
67
- """
68
- if os.getenv("AZURE_OPENAI_API_KEY") and os.getenv("AZURE_OPENAI_ENDPOINT"):
69
- return "azure"
70
- elif os.getenv("OPENAI_API_KEY"):
71
- return "openai"
72
- else:
73
- # Default to openai if nothing is configured
74
- return "openai"
75
-
76
-
77
- def _detect_embedding_dimensions(model_name: str) -> int | None:
78
- """
79
- Auto-detect embedding dimensions from model name.
80
-
81
- Args:
82
- model_name: Embedding model name (e.g., "text-embedding-3-small")
83
-
84
- Returns:
85
- Number of dimensions, or None if unknown (should use config/default)
86
-
87
- Examples:
88
- >>> _detect_embedding_dimensions("text-embedding-3-small")
89
- 1536
90
- """
91
- # Normalize model name (remove provider prefix)
92
- normalized = model_name.lower()
93
- if "/" in normalized:
94
- normalized = normalized.split("/", 1)[1]
95
-
96
- # OpenAI models
97
- if "text-embedding-3-small" in normalized:
98
- return 1536
99
- elif "text-embedding-3-large" in normalized:
100
- return 3072
101
- elif "text-embedding-ada-002" in normalized or "ada-002" in normalized:
102
- return 1536
103
- elif "text-embedding-ada" in normalized:
104
- return 1536
105
-
106
- # Cohere models (common ones)
107
- if "embed-english-v3" in normalized:
108
- return 1024
109
- elif "embed-multilingual-v3" in normalized:
110
- return 1024
111
-
112
- # Unknown model - return None to use config/default
113
- return None
114
-
115
-
116
42
  class Mem0MemoryServiceError(Exception):
117
- """
118
- Base exception for all Mem0 Memory Service failures.
119
- """
120
-
121
43
  pass
122
44
 
123
45
 
124
- def _build_vector_store_config(
125
- db_name: str, collection_name: str, mongo_uri: str, embedding_model_dims: int
126
- ) -> dict[str, Any]:
127
- """Build vector store configuration for mem0."""
128
- return {
129
- "vector_store": {
130
- "provider": "mongodb",
131
- "config": {
132
- "db_name": db_name,
133
- "collection_name": collection_name,
134
- "mongo_uri": mongo_uri,
135
- "embedding_model_dims": embedding_model_dims,
136
- },
137
- }
138
- }
139
-
140
-
141
- def _build_embedder_config(provider: str, embedding_model: str, app_slug: str) -> dict[str, Any]:
142
- """Build embedder configuration for mem0."""
143
- clean_embedding_model = embedding_model.replace("azure/", "").replace("openai/", "")
144
- if provider == "azure":
145
- azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
146
- azure_api_key = os.getenv("AZURE_OPENAI_API_KEY")
147
- azure_api_version = os.getenv(
148
- "AZURE_OPENAI_API_VERSION",
149
- os.getenv("OPENAI_API_VERSION", "2024-02-15-preview"),
150
- )
151
-
152
- if not azure_endpoint or not azure_api_key:
153
- raise Mem0MemoryServiceError(
154
- "Azure OpenAI requires AZURE_OPENAI_ENDPOINT and "
155
- "AZURE_OPENAI_API_KEY environment variables"
156
- )
157
-
158
- config = {
159
- "provider": "azure_openai",
160
- "config": {
161
- "model": clean_embedding_model,
162
- "azure_kwargs": {
163
- "azure_deployment": clean_embedding_model,
164
- "api_version": azure_api_version,
165
- "azure_endpoint": azure_endpoint,
166
- "api_key": azure_api_key,
167
- },
168
- },
169
- }
170
- else:
171
- config = {
172
- "provider": "openai",
173
- "config": {"model": clean_embedding_model},
174
- }
175
-
176
- provider_name = "Azure OpenAI" if provider == "azure" else "OpenAI"
177
- logger.info(
178
- f"Configuring mem0 embedder ({provider_name}): "
179
- f"provider='{config['provider']}', "
180
- f"model='{clean_embedding_model}'",
181
- extra={
182
- "app_slug": app_slug,
183
- "embedding_model": embedding_model,
184
- "embedder_provider": config["provider"],
185
- "provider": provider,
186
- },
187
- )
188
- return config
189
-
190
-
191
- def _build_llm_config(
192
- provider: str, chat_model: str, temperature: float, app_slug: str
193
- ) -> dict[str, Any]:
194
- """Build LLM configuration for mem0."""
195
- clean_chat_model = chat_model.replace("azure/", "").replace("openai/", "")
196
- if provider == "azure":
197
- deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") or clean_chat_model
198
- clean_chat_model = deployment_name
199
-
200
- azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
201
- azure_api_key = os.getenv("AZURE_OPENAI_API_KEY")
202
- azure_api_version = os.getenv(
203
- "AZURE_OPENAI_API_VERSION",
204
- os.getenv("OPENAI_API_VERSION", "2024-02-15-preview"),
205
- )
206
-
207
- if not azure_endpoint or not azure_api_key:
208
- raise Mem0MemoryServiceError(
209
- "Azure OpenAI LLM requires AZURE_OPENAI_ENDPOINT and "
210
- "AZURE_OPENAI_API_KEY environment variables"
211
- )
212
-
213
- config = {
214
- "provider": "azure_openai",
215
- "config": {
216
- "model": clean_chat_model,
217
- "temperature": temperature,
218
- "azure_kwargs": {
219
- "azure_deployment": clean_chat_model,
220
- "api_version": azure_api_version,
221
- "azure_endpoint": azure_endpoint,
222
- "api_key": azure_api_key,
223
- },
224
- },
225
- }
226
- else:
227
- config = {
228
- "provider": "openai",
229
- "config": {"model": clean_chat_model, "temperature": temperature},
230
- }
231
-
232
- llm_provider_name = "Azure OpenAI" if provider == "azure" else "OpenAI"
233
- logger.info(
234
- f"Configuring mem0 LLM ({llm_provider_name}): "
235
- f"provider='{config['provider']}', "
236
- f"model='{clean_chat_model}'",
237
- extra={
238
- "app_slug": app_slug,
239
- "original_model": chat_model,
240
- "llm_provider": config["provider"],
241
- "llm_provider_type": provider,
242
- "temperature": temperature,
243
- },
244
- )
245
- return config
246
-
247
-
248
- def _initialize_memory_instance(mem0_config: dict[str, Any], app_slug: str) -> tuple:
249
- """Initialize Mem0 Memory instance and return (instance, init_method)."""
250
- logger.debug(
251
- "Initializing Mem0 Memory with config structure",
252
- extra={
253
- "app_slug": app_slug,
254
- "config_keys": list(mem0_config.keys()),
255
- "vector_store_provider": mem0_config.get("vector_store", {}).get("provider"),
256
- "embedder_provider": mem0_config.get("embedder", {}).get("provider"),
257
- "llm_provider": (
258
- mem0_config.get("llm", {}).get("provider") if mem0_config.get("llm") else None
259
- ),
260
- "full_config": mem0_config,
261
- },
262
- )
263
-
264
- init_method = None
265
- try:
266
- if hasattr(Memory, "from_config"):
267
- memory_instance = Memory.from_config(mem0_config)
268
- init_method = "Memory.from_config()"
269
- else:
270
- try:
271
- from mem0.config import Config
272
-
273
- config_obj = Config(**mem0_config)
274
- memory_instance = Memory(config_obj)
275
- init_method = "Memory(Config())"
276
- except (ImportError, TypeError) as config_error:
277
- logger.warning(
278
- f"Could not create Config object, trying dict: {config_error}",
279
- extra={"app_slug": app_slug},
280
- )
281
- memory_instance = Memory(mem0_config)
282
- init_method = "Memory(dict)"
283
- except (
284
- ImportError,
285
- AttributeError,
286
- TypeError,
287
- ValueError,
288
- RuntimeError,
289
- KeyError,
290
- ) as init_error:
291
- error_msg = str(init_error)
292
- logger.error(
293
- f"Failed to initialize Memory instance: {error_msg}",
294
- exc_info=True,
295
- extra={
296
- "app_slug": app_slug,
297
- "error": error_msg,
298
- "error_type": type(init_error).__name__,
299
- "config_keys": (
300
- list(mem0_config.keys()) if isinstance(mem0_config, dict) else "not_dict"
301
- ),
302
- },
303
- )
304
- raise Mem0MemoryServiceError(
305
- f"Failed to initialize Memory instance: {error_msg}. "
306
- f"Ensure mem0ai is installed and Azure OpenAI environment "
307
- f"variables are set correctly."
308
- ) from init_error
309
-
310
- return memory_instance, init_method
311
-
312
-
313
46
  class Mem0MemoryService:
314
- """
315
- Service for managing user memories using Mem0.ai.
316
-
317
- This service provides intelligent memory management that:
318
- - Stores and retrieves memories in MongoDB (using mdb-engine's connection)
319
- - Uses mem0's embedder for embeddings (configured via environment variables)
320
- - Optionally extracts memories from conversations (requires LLM if infer: true)
321
- - Retrieves relevant memories for context-aware responses
322
- - Optionally builds knowledge graphs for entity relationships
323
-
324
- Embeddings and LLM are configured via environment variables (.env) and mem0 handles
325
- provider routing automatically.
326
- """
327
-
328
47
  def __init__(
329
48
  self,
330
49
  mongo_uri: str,
@@ -332,897 +51,430 @@ class Mem0MemoryService:
332
51
  app_slug: str,
333
52
  config: dict[str, Any] | None = None,
334
53
  ):
335
- """
336
- Initialize Mem0 Memory Service.
337
-
338
- Args:
339
- mongo_uri: MongoDB connection URI
340
- db_name: Database name
341
- app_slug: App slug (used for collection naming)
342
- config: Optional memory configuration dict (from manifest.json
343
- memory_config)
344
- Can include: collection_name, enable_graph, infer,
345
- embedding_model, chat_model, temperature, etc.
346
- Note: embedding_model_dims is auto-detected by embedding a
347
- test string - no need to specify!
348
- Embeddings and LLM are configured via environment variables
349
- (.env).
350
-
351
- Raises:
352
- Mem0MemoryServiceError: If mem0 is not available or initialization fails
353
- """
354
- # Lazy check for mem0 availability
355
54
  if not _check_mem0_available():
356
- raise Mem0MemoryServiceError(
357
- "Mem0 dependencies not available. Install with: pip install mem0ai"
358
- )
55
+ raise Mem0MemoryServiceError("Mem0 not installed. pip install mem0ai")
359
56
 
360
57
  self.mongo_uri = mongo_uri
361
58
  self.db_name = db_name
362
59
  self.app_slug = app_slug
363
-
364
- # Extract config with defaults
365
60
  self.collection_name = (config or {}).get("collection_name", f"{app_slug}_memories")
366
- config_embedding_dims = (config or {}).get(
367
- "embedding_model_dims"
368
- ) # Optional - will be auto-detected
369
- self.enable_graph = (config or {}).get("enable_graph", False)
370
61
  self.infer = (config or {}).get("infer", True)
371
- self.async_mode = (config or {}).get("async_mode", True)
372
62
 
373
- # Get model names from config or environment
374
- # Default embedding model from config or env, fallback to common default
63
+ # Ensure GOOGLE_API_KEY is set for mem0 compatibility
64
+ # (mem0 expects GOOGLE_API_KEY, not GEMINI_API_KEY)
65
+ # This ensures we use the DIRECT Gemini API
66
+ # (generativelanguage.googleapis.com), NOT Vertex AI
67
+ if os.getenv("GEMINI_API_KEY") and not os.getenv("GOOGLE_API_KEY"):
68
+ os.environ["GOOGLE_API_KEY"] = os.getenv("GEMINI_API_KEY")
69
+ logger.info(
70
+ "Set GOOGLE_API_KEY from GEMINI_API_KEY for mem0 compatibility (direct Gemini API)"
71
+ )
72
+
73
+ # Verify we're NOT using Vertex AI (which would use GOOGLE_APPLICATION_CREDENTIALS)
74
+ if os.getenv("GOOGLE_APPLICATION_CREDENTIALS"):
75
+ logger.warning(
76
+ "GOOGLE_APPLICATION_CREDENTIALS is set - this would use Vertex AI, "
77
+ "not direct Gemini API"
78
+ )
79
+
80
+ # 1. Models & Config
375
81
  embedding_model = (config or {}).get("embedding_model") or os.getenv(
376
82
  "EMBEDDING_MODEL", "text-embedding-3-small"
377
83
  )
378
- chat_model = (
379
- (config or {}).get("chat_model")
380
- or os.getenv("CHAT_MODEL")
381
- or os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o")
382
- )
383
- temperature = (config or {}).get("temperature", float(os.getenv("LLM_TEMPERATURE", "0.0")))
384
-
385
- # Detect provider from environment variables
386
- provider = _detect_provider_from_env()
387
-
388
- # Verify required environment variables are set
389
- if provider == "azure":
390
- if not os.getenv("AZURE_OPENAI_API_KEY") or not os.getenv("AZURE_OPENAI_ENDPOINT"):
391
- raise Mem0MemoryServiceError(
392
- "Azure OpenAI provider requires AZURE_OPENAI_API_KEY and "
393
- "AZURE_OPENAI_ENDPOINT environment variables to be set."
394
- )
395
- else:
396
- if not os.getenv("OPENAI_API_KEY"):
397
- raise Mem0MemoryServiceError(
398
- "OpenAI provider requires OPENAI_API_KEY environment variable to be set."
399
- )
84
+ chat_model = (config or {}).get("chat_model") or os.getenv("CHAT_MODEL", "gpt-4o")
400
85
 
401
- try:
402
- # Detect embedding dimensions using model name (fallback method)
403
- detected_dims = _detect_embedding_dimensions(embedding_model)
404
- self.embedding_model_dims = (
405
- detected_dims if detected_dims is not None else (config_embedding_dims or 1536)
406
- )
86
+ # 2. Build Mem0 Configuration
87
+ embedding_dims = (config or {}).get(
88
+ "embedding_model_dims"
89
+ ) or 1536 # Default for text-embedding-3-small
90
+ mem0_config = {
91
+ "vector_store": {
92
+ "provider": "mongodb",
93
+ "config": {
94
+ "db_name": db_name,
95
+ "collection_name": self.collection_name,
96
+ "mongo_uri": mongo_uri,
97
+ "embedding_model_dims": embedding_dims,
98
+ },
99
+ },
100
+ "embedder": self._build_provider_config("embedder", embedding_model),
101
+ "llm": self._build_provider_config("llm", chat_model) if self.infer else None,
102
+ }
407
103
 
408
- # Build mem0 config with MongoDB as vector store
409
- mem0_config = _build_vector_store_config(
410
- self.db_name,
411
- self.collection_name,
412
- self.mongo_uri,
413
- self.embedding_model_dims,
104
+ # Add custom prompts to make fact extraction less restrictive (for document processing)
105
+ # The default mem0 prompts are too restrictive and filter out general facts
106
+ if self.infer:
107
+ # Long prompt string - using concatenation to avoid line length issues
108
+ fact_extraction_prompt = (
109
+ "You are a helpful assistant that extracts key facts, insights, "
110
+ "and information from documents and conversations.\n\n"
111
+ "Your task is to extract factual information, insights, and important details "
112
+ "from the provided content. Extract facts that would be useful for future "
113
+ "reference, including:\n"
114
+ "- Key concepts, definitions, and explanations\n"
115
+ "- Important dates, names, and entities\n"
116
+ "- Processes, procedures, and methodologies\n"
117
+ "- Insights, conclusions, and recommendations\n"
118
+ "- Relationships between concepts\n"
119
+ "- Any other factual information that would be valuable to remember\n\n"
120
+ 'Return your response as a JSON object with a "facts" array. '
121
+ "Each fact should be a clear, standalone statement.\n\n"
122
+ "Example:\n"
123
+ 'Input: "The Innovation Hub was established on August 14, 2024 by '
124
+ "David Vainchenker and Todd O'Brien. It focuses on experimental AI projects." + "\n"
125
+ 'Output: {{"facts": ["The Innovation Hub was established on August 14, 2024", '
126
+ '"The Innovation Hub was founded by David Vainchenker and Todd O\'Brien", '
127
+ '"The Innovation Hub focuses on experimental AI projects"]}}' + "\n\n"
128
+ "Now extract facts from the following content:"
414
129
  )
130
+ mem0_config["prompts"] = {"fact_extraction": fact_extraction_prompt}
415
131
 
416
- # Configure mem0 embedder
417
- mem0_config["embedder"] = _build_embedder_config(provider, embedding_model, app_slug)
418
-
419
- # Configure LLM for inference (if infer: true)
420
- if self.infer:
421
- mem0_config["llm"] = _build_llm_config(provider, chat_model, temperature, app_slug)
422
- except (ValueError, TypeError, KeyError, AttributeError, ImportError) as e:
423
- logger.exception(
424
- f"Failed to configure mem0: {e}",
425
- extra={"app_slug": app_slug, "error": str(e)},
426
- )
427
- raise Mem0MemoryServiceError(f"Failed to configure mem0: {e}") from e
132
+ # Filter None
133
+ mem0_config = {k: v for k, v in mem0_config.items() if v is not None}
428
134
 
429
- # Add graph store configuration if enabled
430
- if self.enable_graph:
431
- # Note: Graph store requires separate configuration (neo4j, memgraph, etc.)
432
- # For now, we just enable it - actual graph store config should come from manifest
433
- graph_config = (config or {}).get("graph_store")
434
- if graph_config:
435
- mem0_config["graph_store"] = graph_config
135
+ # 3. Initialize
136
+ try:
137
+ if hasattr(Memory, "from_config"):
138
+ self.memory = Memory.from_config(mem0_config)
436
139
  else:
437
- logger.warning(
438
- "Graph memory enabled but no graph_store config provided. "
439
- "Graph features will not work. Configure graph_store in manifest.json",
440
- extra={"app_slug": app_slug},
441
- )
140
+ self.memory = Memory(mem0_config)
141
+ logger.info(f" Mem0 Service active: {self.collection_name}")
142
+ except (
143
+ ValueError,
144
+ TypeError,
145
+ ConnectionError,
146
+ OSError,
147
+ AttributeError,
148
+ RuntimeError,
149
+ ) as e:
150
+ raise Mem0MemoryServiceError(f"Failed to init Mem0: {e}") from e
442
151
 
443
- try:
444
- # Initialize Mem0 Memory instance
445
- self.memory, init_method = _initialize_memory_instance(mem0_config, app_slug)
152
+ def _build_provider_config(self, component, model_name):
153
+ """
154
+ Build provider configuration for embeddings or LLM.
446
155
 
447
- # Verify the memory instance has required methods
448
- if not hasattr(self.memory, "get_all"):
449
- logger.warning(
450
- f"Memory instance missing 'get_all' method for app '{app_slug}'",
451
- extra={"app_slug": app_slug, "init_method": init_method},
156
+ For embeddings: Always use Azure OpenAI if available, otherwise OpenAI
157
+ For LLM: Detect provider from model name (gemini/google -> google_ai, else Azure/OpenAI)
158
+ """
159
+ clean_model = (
160
+ model_name.replace("azure/", "")
161
+ .replace("openai/", "")
162
+ .replace("google/", "")
163
+ .replace("gemini/", "")
164
+ )
165
+
166
+ # For embeddings, always prefer Azure if available
167
+ if component == "embedder":
168
+ provider = "azure_openai" if os.getenv("AZURE_OPENAI_API_KEY") else "openai"
169
+ cfg = {"provider": provider, "config": {"model": clean_model}}
170
+
171
+ if provider == "azure_openai":
172
+ # Support both AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME and AZURE_EMBEDDING_DEPLOYMENT
173
+ deployment_name = (
174
+ os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME")
175
+ or os.getenv("AZURE_EMBEDDING_DEPLOYMENT")
176
+ or clean_model
452
177
  )
453
- if not hasattr(self.memory, "add"):
454
- logger.warning(
455
- f"Memory instance missing 'add' method for app '{app_slug}'",
456
- extra={"app_slug": app_slug, "init_method": init_method},
178
+ # Use API version from env or default
179
+ api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-01")
180
+ cfg["config"]["azure_kwargs"] = {
181
+ "api_version": api_version,
182
+ "azure_deployment": deployment_name,
183
+ "azure_endpoint": os.getenv("AZURE_OPENAI_ENDPOINT"),
184
+ "api_key": os.getenv("AZURE_OPENAI_API_KEY"),
185
+ }
186
+ logger.info(
187
+ f"Using Azure OpenAI embedding provider with deployment: "
188
+ f"{deployment_name}, API version: {api_version}"
457
189
  )
458
-
459
- logger.info(
460
- f"Mem0 Memory Service initialized using {init_method} for app '{app_slug}'",
461
- extra={
462
- "app_slug": app_slug,
463
- "init_method": init_method,
464
- "collection_name": self.collection_name,
465
- "db_name": self.db_name,
466
- "enable_graph": self.enable_graph,
467
- "infer": self.infer,
468
- "has_get_all": hasattr(self.memory, "get_all"),
469
- "has_add": hasattr(self.memory, "add"),
470
- "embedder_provider": mem0_config.get("embedder", {}).get("provider"),
471
- "embedder_model": mem0_config.get("embedder", {})
472
- .get("config", {})
473
- .get("model"),
474
- "llm_provider": (
475
- mem0_config.get("llm", {}).get("provider") if self.infer else None
476
- ),
477
- "llm_model": (
478
- mem0_config.get("llm", {}).get("config", {}).get("model")
479
- if self.infer
480
- else None
481
- ),
190
+ return cfg
191
+
192
+ # For LLM, detect provider from model name or env vars
193
+ model_lower = model_name.lower()
194
+ # Mem0 uses "gemini" as provider name (not "google_ai" or "vertexai")
195
+ # GOOGLE_API_KEY should already be set in __init__ if GEMINI_API_KEY was provided
196
+ has_gemini_key = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
197
+ if "gemini" in model_lower or "google" in model_lower or has_gemini_key:
198
+ # Use Gemini provider for Mem0 (direct Gemini API, NOT Vertex AI)
199
+ provider = "gemini"
200
+ # Explicitly set API key in config to ensure direct Gemini API usage
201
+ api_key = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
202
+ cfg = {
203
+ "provider": provider,
204
+ "config": {
205
+ "model": clean_model,
206
+ "api_key": api_key, # Explicitly set to ensure direct API usage
482
207
  },
483
- )
484
- except (
485
- ImportError,
486
- AttributeError,
487
- TypeError,
488
- ValueError,
489
- RuntimeError,
490
- KeyError,
491
- ) as e:
492
- logger.error(
493
- f"Failed to initialize Mem0 Memory Service for app '{app_slug}': {e}",
494
- exc_info=True,
495
- extra={"app_slug": app_slug, "error": str(e)},
496
- )
497
- raise Mem0MemoryServiceError(f"Failed to initialize Mem0 Memory Service: {e}") from e
208
+ }
209
+ logger.info(f"Using Gemini LLM provider (direct API) with model: {clean_model}")
210
+ return cfg
211
+ else:
212
+ # Use Azure OpenAI if available, otherwise OpenAI
213
+ provider = "azure_openai" if os.getenv("AZURE_OPENAI_API_KEY") else "openai"
214
+ cfg = {"provider": provider, "config": {"model": clean_model}}
215
+
216
+ if provider == "azure_openai":
217
+ deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", clean_model)
218
+ # Use API version from env or default (match .env default)
219
+ api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2024-02-01")
220
+ cfg["config"]["azure_kwargs"] = {
221
+ "api_version": api_version,
222
+ "azure_deployment": deployment_name,
223
+ "azure_endpoint": os.getenv("AZURE_OPENAI_ENDPOINT"),
224
+ "api_key": os.getenv("AZURE_OPENAI_API_KEY"),
225
+ }
226
+ logger.info(
227
+ f"Using Azure OpenAI LLM provider with deployment: "
228
+ f"{deployment_name}, API version: {api_version}"
229
+ )
230
+ else:
231
+ logger.info(f"Using OpenAI LLM provider with model: {clean_model}")
232
+ return cfg
233
+
234
+ # --- Core Operations ---
498
235
 
499
236
  def add(
500
237
  self,
501
238
  messages: str | list[dict[str, str]],
502
239
  user_id: str | None = None,
503
240
  metadata: dict[str, Any] | None = None,
241
+ bucket_id: str | None = None,
242
+ bucket_type: str | None = None,
243
+ raw_content: str | None = None,
504
244
  **kwargs,
505
245
  ) -> list[dict[str, Any]]:
506
246
  """
507
- Add memories from messages or text.
508
-
509
- This method intelligently extracts memories from conversations
510
- and stores them in MongoDB. Memories are processed asynchronously
511
- by default for better performance.
247
+ Add memories with user scoping and metadata convenience.
248
+ All operations are scoped per user_id for safety.
249
+ bucket_id and bucket_type are stored in metadata for filtering convenience.
250
+ """
251
+ if isinstance(messages, str):
252
+ messages = [{"role": "user", "content": messages}]
512
253
 
513
- Args:
514
- messages: Either a string or list of message dicts with 'role' and 'content'
515
- user_id: Optional user ID to associate memories with
516
- metadata: Optional metadata dict (e.g., {"category": "preferences"})
517
- **kwargs: Additional mem0.add() parameters:
518
- - infer: Whether to infer memories (default: True)
519
- Note: async_mode is not a valid parameter for Mem0's add()
520
- method.
521
- Mem0 processes memories asynchronously by default.
522
- Graph features are configured at initialization via
523
- enable_graph in config, not per-add call.
254
+ # Merge metadata
255
+ final_metadata = dict(metadata) if metadata else {}
524
256
 
525
- Returns:
526
- List of memory events (each with 'id', 'event', 'data')
257
+ # CRITICAL: Database indexing relies on these fields being in metadata
258
+ if bucket_id:
259
+ final_metadata["bucket_id"] = bucket_id
260
+ final_metadata["context_id"] = bucket_id # Backwards compatibility
527
261
 
528
- Example:
529
- ```python
530
- memories = memory_service.add(
531
- messages=[
532
- {"role": "user", "content": "I love sci-fi movies"},
533
- {"role": "assistant", "content": "Noted! I'll remember that."}
534
- ],
535
- user_id="alice",
536
- metadata={"category": "preferences"}
537
- )
538
- ```
539
- """
540
- try:
541
- # Normalize messages format
542
- if isinstance(messages, str):
543
- messages = [{"role": "user", "content": messages}]
262
+ if bucket_type:
263
+ final_metadata["bucket_type"] = bucket_type
544
264
 
545
- # Prepare kwargs with defaults from config
546
- # async_mode is not a valid parameter for Mem0's add() method
547
- add_kwargs = {"infer": kwargs.pop("infer", self.infer), **kwargs}
548
- add_kwargs.pop("async_mode", None)
265
+ # Store raw_content in metadata if provided (metadata convenience)
266
+ if raw_content:
267
+ final_metadata["raw_content"] = raw_content
549
268
 
550
- # enable_graph is configured at initialization, not per-add call
551
- # Mem0 processes asynchronously by default
552
- # Log message content preview for debugging
553
- message_preview = []
554
- for i, msg in enumerate(messages[:5]): # Show first 5 messages
555
- if isinstance(msg, dict):
556
- role = msg.get("role", "unknown")
557
- content = msg.get("content", "")
558
- preview = content[:150] + "..." if len(content) > 150 else content
559
- message_preview.append(f"{i+1}. {role}: {preview}")
269
+ # Infer defaults to configured value unless overridden
270
+ infer = kwargs.pop("infer", self.infer)
560
271
 
561
- logger.info(
562
- f"🔵 CALLING mem0.add() - app_slug='{self.app_slug}', "
563
- f"user_id='{user_id}', messages={len(messages)}, "
564
- f"infer={add_kwargs.get('infer', 'N/A')}",
565
- extra={
566
- "app_slug": self.app_slug,
567
- "user_id": user_id,
568
- "collection_name": self.collection_name,
569
- "message_count": len(messages),
570
- "message_preview": "\n".join(message_preview),
571
- "infer": add_kwargs.get("infer"),
572
- "metadata": metadata or {},
573
- "add_kwargs": add_kwargs,
574
- },
272
+ try:
273
+ logger.debug(
274
+ f"Calling mem0.add() with infer={infer}, user_id={user_id}, bucket_id={bucket_id}"
575
275
  )
576
-
577
276
  result = self.memory.add(
578
277
  messages=messages,
579
- user_id=str(user_id), # Ensure string - mem0 might be strict about this
580
- metadata=metadata or {},
581
- **add_kwargs,
278
+ user_id=str(user_id) if user_id else None,
279
+ metadata=final_metadata,
280
+ infer=infer,
281
+ **kwargs,
582
282
  )
583
-
584
- # Normalize result format - mem0.add() may return different formats
585
- if isinstance(result, dict):
586
- # Some versions return {"results": [...]} or {"data": [...]}
587
- if "results" in result:
588
- result = result["results"]
589
- elif "data" in result:
590
- result = result["data"] if isinstance(result["data"], list) else []
591
- elif "memory" in result:
592
- # Single memory object
593
- result = [result]
594
-
595
- # Ensure result is always a list
596
- if not isinstance(result, list):
597
- result = [result] if result else []
598
-
599
- result_length = len(result) if isinstance(result, list) else 0
283
+ # Log raw result before normalization
600
284
  logger.debug(
601
- f"Raw result from mem0.add(): type={type(result)}, " f"length={result_length}",
602
- extra={
603
- "app_slug": self.app_slug,
604
- "user_id": user_id,
605
- "result_type": str(type(result)),
606
- "is_list": isinstance(result, list),
607
- "result_length": len(result) if isinstance(result, list) else 0,
608
- "result_sample": (
609
- result[0]
610
- if result and isinstance(result, list) and len(result) > 0
611
- else None
612
- ),
613
- },
285
+ f"mem0.add() raw result: type={type(result)}, "
286
+ f"value={str(result)[:500] if result else 'None'}"
614
287
  )
615
-
288
+ normalized = self._normalize_result(result)
616
289
  logger.info(
617
- f"Added {len(result)} memories for user '{user_id}'",
618
- extra={
619
- "app_slug": self.app_slug,
620
- "user_id": user_id,
621
- "message_count": len(messages),
622
- "memory_count": len(result) if isinstance(result, list) else 0,
623
- "memory_ids": (
624
- [m.get("id") or m.get("_id") for m in result if isinstance(m, dict)]
625
- if result
626
- else []
627
- ),
628
- "infer_enabled": add_kwargs.get("infer", False),
629
- "has_llm": (
630
- hasattr(self.memory, "llm") and self.memory.llm is not None
631
- if hasattr(self.memory, "llm")
632
- else False
633
- ),
634
- },
290
+ f"mem0.add() normalized to {len(normalized)} memories "
291
+ f"(raw result type: {type(result)})"
635
292
  )
636
-
637
- # If 0 memories and infer is enabled, log helpful info
638
- if len(result) == 0 and add_kwargs.get("infer", False):
639
- # Extract conversation content for analysis
640
- conversation_text = "\n".join(
641
- [
642
- f"{msg.get('role', 'unknown')}: {msg.get('content', '')[:100]}"
643
- for msg in messages[:5]
644
- ]
293
+ if not normalized and infer:
294
+ logger.warning(
295
+ f"⚠️ mem0.add() with infer=True returned empty result. Raw result: {result}"
645
296
  )
646
-
647
- logger.info(
648
- "ℹ️ mem0.add() returned 0 memories. This is normal if the "
649
- "conversation doesn't contain extractable facts. "
650
- "mem0 extracts personal preferences, facts, and details - "
651
- "not generic greetings or small talk. "
652
- "Try conversations like 'I love pizza' or 'I work as a "
653
- "software engineer' to see memories extracted.",
654
- extra={
655
- "app_slug": self.app_slug,
656
- "user_id": user_id,
657
- "message_count": len(messages),
658
- "infer": True,
659
- "has_llm": (
660
- hasattr(self.memory, "llm") and self.memory.llm is not None
661
- if hasattr(self.memory, "llm")
662
- else False
663
- ),
664
- "conversation_preview": conversation_text,
665
- },
297
+ # Try to understand why - check if it's a dict with empty results
298
+ if isinstance(result, dict):
299
+ logger.warning(f" Result dict keys: {list(result.keys())}")
300
+ if "results" in result:
301
+ logger.warning(f" result['results']: {result['results']}")
302
+ if "data" in result:
303
+ logger.warning(f" result['data']: {result['data']}")
304
+ return normalized
305
+ except (
306
+ ValueError,
307
+ TypeError,
308
+ ConnectionError,
309
+ OSError,
310
+ AttributeError,
311
+ RuntimeError,
312
+ KeyError,
313
+ ) as e:
314
+ error_msg = str(e)
315
+ # Handle rate limit errors gracefully - try storing without inference
316
+ if (
317
+ "429" in error_msg
318
+ or "RESOURCE_EXHAUSTED" in error_msg
319
+ or "rate limit" in error_msg.lower()
320
+ ):
321
+ logger.warning(
322
+ f"Rate limit hit during memory inference, storing without inference: "
323
+ f"{error_msg}"
666
324
  )
667
-
668
- return result
669
-
670
- except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
671
- logger.error(
672
- f"Failed to add memories: {e}",
673
- exc_info=True,
674
- extra={"app_slug": self.app_slug, "user_id": user_id, "error": str(e)},
675
- )
676
- raise Mem0MemoryServiceError(f"Failed to add memories: {e}") from e
325
+ # Retry without inference to at least store the raw content
326
+ try:
327
+ result = self.memory.add(
328
+ messages=messages,
329
+ user_id=str(user_id) if user_id else None,
330
+ metadata=final_metadata,
331
+ infer=False, # Disable inference to avoid rate limits
332
+ **kwargs,
333
+ )
334
+ logger.info("Successfully stored memory without inference due to rate limit")
335
+ return self._normalize_result(result)
336
+ except (
337
+ ValueError,
338
+ TypeError,
339
+ ConnectionError,
340
+ OSError,
341
+ AttributeError,
342
+ RuntimeError,
343
+ KeyError,
344
+ ) as retry_error:
345
+ logger.exception("Failed to store memory even without inference")
346
+ raise Mem0MemoryServiceError(
347
+ f"Add failed (rate limited, retry also failed): {retry_error}"
348
+ ) from retry_error
349
+ else:
350
+ logger.exception("Mem0 Add Failed")
351
+ raise Mem0MemoryServiceError(f"Add failed: {e}") from e
677
352
 
678
353
  def get_all(
679
354
  self,
680
355
  user_id: str | None = None,
681
- limit: int | None = None,
682
- retry_on_empty: bool = True,
683
- max_retries: int = 2,
684
- retry_delay: float = 0.5,
356
+ limit: int = 100,
357
+ filters: dict[str, Any] | None = None,
685
358
  **kwargs,
686
359
  ) -> list[dict[str, Any]]:
687
360
  """
688
- Get all memories for a user.
689
-
690
- Args:
691
- user_id: User ID to retrieve memories for
692
- limit: Optional limit on number of memories to return
693
- retry_on_empty: If True, retry if result is empty (handles async processing delay)
694
- max_retries: Maximum number of retries if result is empty
695
- retry_delay: Delay in seconds between retries
696
- **kwargs: Additional mem0.get_all() parameters
697
-
698
- Returns:
699
- List of memory dictionaries
361
+ Get all memories with direct database filtering.
700
362
  """
701
- import time
702
-
703
363
  try:
704
- # Verify memory instance is valid before calling
705
- if not hasattr(self, "memory") or self.memory is None:
706
- logger.error(
707
- f"Memory instance is None or missing for app '{self.app_slug}'",
708
- extra={"app_slug": self.app_slug, "user_id": user_id},
709
- )
710
- return []
711
-
712
- logger.info(
713
- f"🟢 CALLING mem0.get_all() - app_slug='{self.app_slug}', "
714
- f"user_id='{user_id}' (type: {type(user_id).__name__}), "
715
- f"collection='{self.collection_name}'",
716
- extra={
717
- "app_slug": self.app_slug,
718
- "user_id": user_id,
719
- "user_id_type": type(user_id).__name__,
720
- "user_id_repr": repr(user_id),
721
- "collection_name": self.collection_name,
722
- "limit": limit,
723
- "kwargs": kwargs,
724
- },
725
- )
726
-
727
- result = None
728
- attempt = 0
729
-
730
- while attempt <= max_retries:
731
- if attempt > 0:
732
- # Wait before retry to allow async processing to complete
733
- time.sleep(retry_delay * attempt) # Exponential backoff
734
- logger.debug(
735
- f"Retrying mem0.get_all (attempt {attempt + 1}/{max_retries + 1})",
736
- extra={
737
- "app_slug": self.app_slug,
738
- "user_id": user_id,
739
- "attempt": attempt + 1,
740
- },
741
- )
742
-
743
- # Call with safety - catch any exceptions from mem0
744
- try:
745
- logger.debug(
746
- f"🟢 EXECUTING: memory.get_all(user_id='{user_id}', "
747
- f"limit={limit}, kwargs={kwargs})",
748
- extra={
749
- "app_slug": self.app_slug,
750
- "user_id": user_id,
751
- "collection_name": self.collection_name,
752
- "attempt": attempt + 1,
753
- },
754
- )
755
- result = self.memory.get_all(
756
- user_id=str(user_id), limit=limit, **kwargs
757
- ) # Ensure string
758
- result_length = len(result) if isinstance(result, list | dict) else "N/A"
759
- logger.debug(
760
- f"🟢 RESULT RECEIVED: type={type(result).__name__}, "
761
- f"length={result_length}",
762
- extra={
763
- "app_slug": self.app_slug,
764
- "user_id": user_id,
765
- "result_type": type(result).__name__,
766
- "result_length": (
767
- len(result) if isinstance(result, list | dict) else 0
768
- ),
769
- "attempt": attempt + 1,
770
- },
771
- )
772
- except AttributeError as attr_error:
773
- logger.exception(
774
- f"Memory.get_all method not available: {attr_error}",
775
- extra={
776
- "app_slug": self.app_slug,
777
- "user_id": user_id,
778
- "error": str(attr_error),
779
- "attempt": attempt + 1,
780
- },
781
- )
782
- return [] # Return empty list instead of retrying
783
- # Type 4: Let other exceptions bubble up to framework handler
784
-
785
- logger.debug(
786
- f"Raw result from mem0.get_all (attempt {attempt + 1}): type={type(result)}",
787
- extra={
788
- "app_slug": self.app_slug,
789
- "user_id": user_id,
790
- "attempt": attempt + 1,
791
- "result_type": str(type(result)),
792
- "is_dict": isinstance(result, dict),
793
- "is_list": isinstance(result, list),
794
- "result_length": (len(result) if isinstance(result, list | dict) else 0),
795
- },
796
- )
797
-
798
- # Handle Mem0 v2 API response format: {"results": [...], "total": N}
799
- if isinstance(result, dict):
800
- if "results" in result:
801
- result = result["results"] # Extract results array
802
- logger.debug(
803
- "Extracted results from dict response",
804
- extra={
805
- "app_slug": self.app_slug,
806
- "user_id": user_id,
807
- "result_count": (len(result) if isinstance(result, list) else 0),
808
- },
809
- )
810
- elif "data" in result:
811
- # Alternative format: {"data": [...]}
812
- result = result["data"] if isinstance(result["data"], list) else []
813
-
814
- # Ensure result is always a list for backward compatibility
815
- if not isinstance(result, list):
816
- result = [result] if result else []
817
-
818
- # If we got results or retries are disabled, break
819
- if not retry_on_empty or len(result) > 0 or attempt >= max_retries:
820
- break
821
-
822
- attempt += 1
823
-
824
- logger.info(
825
- f"Retrieved {len(result)} memories for user '{user_id}' "
826
- f"(after {attempt + 1} attempt(s))",
827
- extra={
828
- "app_slug": self.app_slug,
829
- "user_id": user_id,
830
- "memory_count": len(result) if isinstance(result, list) else 0,
831
- "attempts": attempt + 1,
832
- "sample_memory": (
833
- result[0]
834
- if result and isinstance(result, list) and len(result) > 0
835
- else None
836
- ),
837
- },
838
- )
364
+ call_kwargs = {"limit": limit}
365
+ if user_id:
366
+ call_kwargs["user_id"] = str(user_id)
367
+ if filters:
368
+ call_kwargs["filters"] = filters # Passed to MongoDB $match
839
369
 
840
- return result
370
+ call_kwargs.update(kwargs)
841
371
 
842
- except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
843
- attempt_num = attempt + 1 if "attempt" in locals() and attempt is not None else 1
844
- logger.error(
845
- f"Failed to get memories: {e}",
846
- exc_info=True,
847
- extra={
848
- "app_slug": self.app_slug,
849
- "user_id": user_id,
850
- "error": str(e),
851
- "error_type": type(e).__name__,
852
- "attempt": attempt_num,
853
- },
854
- )
855
- raise Mem0MemoryServiceError(f"Failed to get memories: {e}") from e
372
+ return self._normalize_result(self.memory.get_all(**call_kwargs))
373
+ except (
374
+ ValueError,
375
+ TypeError,
376
+ ConnectionError,
377
+ OSError,
378
+ AttributeError,
379
+ RuntimeError,
380
+ KeyError,
381
+ ):
382
+ logger.exception("Mem0 get_all failed")
383
+ return []
856
384
 
857
385
  def search(
858
386
  self,
859
387
  query: str,
860
388
  user_id: str | None = None,
861
- limit: int | None = None,
862
- metadata: dict[str, Any] | None = None,
389
+ limit: int = 5,
863
390
  filters: dict[str, Any] | None = None,
864
391
  **kwargs,
865
392
  ) -> list[dict[str, Any]]:
866
393
  """
867
- Search for relevant memories using semantic search.
868
-
869
- Args:
870
- query: Search query string
871
- user_id: Optional user ID to scope search to
872
- limit: Optional limit on number of results
873
- metadata: Optional metadata dict to filter results
874
- (e.g., {"category": "travel"})
875
- Deprecated in favor of 'filters' parameter for Mem0 1.0.0+
876
- filters: Optional enhanced filters dict (Mem0 1.0.0+) with operators
877
- like {"category": {"eq": "travel"}}
878
- **kwargs: Additional mem0.search() parameters
879
-
880
- Returns:
881
- List of relevant memory dictionaries
882
-
883
- Example:
884
- ```python
885
- # Simple metadata filter (backward compatible)
886
- results = memory_service.search(
887
- query="What are my travel plans?",
888
- user_id="alice",
889
- metadata={"category": "travel"}
890
- )
891
-
892
- # Enhanced filters (Mem0 1.0.0+)
893
- results = memory_service.search(
894
- query="high priority tasks",
895
- user_id="alice",
896
- filters={
897
- "AND": [
898
- {"category": "work"},
899
- {"priority": {"gte": 5}}
900
- ]
901
- }
902
- )
903
- ```
904
- """
905
- try:
906
- # Build search kwargs
907
- search_kwargs = {"limit": limit, **kwargs}
908
-
909
- # Prefer 'filters' parameter (Mem0 1.0.0+) over 'metadata' (legacy)
910
- if filters is not None:
911
- search_kwargs["filters"] = filters
912
- elif metadata:
913
- # Backward compatibility: convert simple metadata to filters format
914
- # Try 'filters' first, fallback to 'metadata' if it fails
915
- search_kwargs["filters"] = metadata
916
-
917
- # Call search - try with filters first, fallback to metadata if needed
918
- try:
919
- result = self.memory.search(query=query, user_id=user_id, **search_kwargs)
920
- except (TypeError, ValueError) as e:
921
- # If filters parameter doesn't work, try with metadata (backward compatibility)
922
- if "filters" in search_kwargs and metadata:
923
- logger.debug(
924
- f"Filters parameter failed, trying metadata parameter: {e}",
925
- extra={"app_slug": self.app_slug, "user_id": user_id},
926
- )
927
- search_kwargs.pop("filters", None)
928
- search_kwargs["metadata"] = metadata
929
- result = self.memory.search(query=query, user_id=user_id, **search_kwargs)
930
- else:
931
- raise
932
-
933
- # Handle response format - search may return dict with "results" key
934
- if isinstance(result, dict):
935
- if "results" in result:
936
- result = result["results"]
937
- elif "data" in result:
938
- result = result["data"] if isinstance(result["data"], list) else []
939
-
940
- # Ensure result is always a list
941
- if not isinstance(result, list):
942
- result = [result] if result else []
943
-
944
- logger.debug(
945
- f"Searched memories for user '{user_id}'",
946
- extra={
947
- "app_slug": self.app_slug,
948
- "user_id": user_id,
949
- "query": query,
950
- "metadata_filter": metadata,
951
- "filters": filters,
952
- "result_count": len(result) if isinstance(result, list) else 0,
953
- },
954
- )
955
-
956
- return result
957
-
958
- except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
959
- logger.error(
960
- f"Failed to search memories: {e}",
961
- exc_info=True,
962
- extra={
963
- "app_slug": self.app_slug,
964
- "user_id": user_id,
965
- "query": query,
966
- "metadata": metadata,
967
- "filters": filters,
968
- "error": str(e),
969
- },
970
- )
971
- raise Mem0MemoryServiceError(f"Failed to search memories: {e}") from e
972
-
973
- def get(self, memory_id: str, user_id: str | None = None, **kwargs) -> dict[str, Any]:
974
- """
975
- Get a single memory by ID.
976
-
977
- Args:
978
- memory_id: Memory ID to retrieve
979
- user_id: Optional user ID for scoping
980
- **kwargs: Additional mem0.get() parameters
981
-
982
- Returns:
983
- Memory dictionary
984
-
985
- Example:
986
- ```python
987
- memory = memory_service.get(memory_id="mem_123", user_id="alice")
988
- ```
989
- """
990
- try:
991
- # Mem0's get() method doesn't accept user_id as a parameter
992
- # User scoping should be handled via metadata or filters if needed
993
- # For now, we just get by memory_id
994
- result = self.memory.get(memory_id=memory_id, **kwargs)
995
-
996
- # If user_id is provided, verify the memory belongs to that user
997
- # by checking metadata or user_id field in the result
998
- if user_id and isinstance(result, dict):
999
- result_user_id = result.get("user_id") or result.get("metadata", {}).get("user_id")
1000
- if result_user_id and result_user_id != user_id:
1001
- logger.warning(
1002
- f"Memory {memory_id} does not belong to user {user_id}",
1003
- extra={
1004
- "memory_id": memory_id,
1005
- "user_id": user_id,
1006
- "result_user_id": result_user_id,
1007
- },
1008
- )
1009
- raise Mem0MemoryServiceError(
1010
- f"Memory {memory_id} does not belong to user {user_id}"
1011
- )
1012
-
1013
- logger.debug(
1014
- f"Retrieved memory '{memory_id}' for user '{user_id}'",
1015
- extra={
1016
- "app_slug": self.app_slug,
1017
- "user_id": user_id,
1018
- "memory_id": memory_id,
1019
- },
1020
- )
1021
-
1022
- return result
1023
-
1024
- except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
1025
- logger.error(
1026
- f"Failed to get memory: {e}",
1027
- exc_info=True,
1028
- extra={
1029
- "app_slug": self.app_slug,
1030
- "user_id": user_id,
1031
- "memory_id": memory_id,
1032
- "error": str(e),
1033
- },
1034
- )
1035
- raise Mem0MemoryServiceError(f"Failed to get memory: {e}") from e
1036
-
1037
- def update(
1038
- self,
1039
- memory_id: str,
1040
- data: str | list[dict[str, str]],
1041
- user_id: str | None = None,
1042
- metadata: dict[str, Any] | None = None,
1043
- **kwargs,
1044
- ) -> dict[str, Any]:
394
+ Semantic search with metadata filters, scoped per user.
1045
395
  """
1046
- Update a memory by ID with new data.
1047
-
1048
- Args:
1049
- memory_id: Memory ID to update
1050
- data: New data (string or list of message dicts with 'role' and 'content')
1051
- user_id: Optional user ID for scoping
1052
- metadata: Optional metadata dict to update
1053
- **kwargs: Additional mem0.update() parameters
1054
-
1055
- Returns:
1056
- Updated memory dictionary
396
+ final_filters = filters or {}
1057
397
 
1058
- Example:
1059
- ```python
1060
- updated = memory_service.update(
1061
- memory_id="mem_123",
1062
- data="I am a software engineer using Python and FastAPI.",
1063
- user_id="bob"
1064
- )
1065
- ```
1066
- """
1067
398
  try:
1068
- # Normalize data format
1069
- if isinstance(data, str):
1070
- data = [{"role": "user", "content": data}]
399
+ call_kwargs = {"limit": limit}
400
+ if final_filters:
401
+ call_kwargs["filters"] = final_filters
1071
402
 
1072
- # Mem0's update() may not accept user_id directly
1073
- # Pass it in metadata if user_id is provided
1074
- update_metadata = metadata or {}
1075
- if user_id:
1076
- update_metadata["user_id"] = user_id
1077
-
1078
- # Try with user_id first, fall back without it if it fails
1079
- try:
1080
- result = self.memory.update(
1081
- memory_id=memory_id,
1082
- data=data,
1083
- user_id=user_id,
1084
- metadata=update_metadata,
1085
- **kwargs,
403
+ return self._normalize_result(
404
+ self.memory.search(
405
+ query=query, user_id=str(user_id) if user_id else None, **call_kwargs, **kwargs
1086
406
  )
1087
- except TypeError as e:
1088
- if "unexpected keyword argument 'user_id'" in str(e):
1089
- # Mem0 doesn't accept user_id, try without it
1090
- result = self.memory.update(
1091
- memory_id=memory_id,
1092
- data=data,
1093
- metadata=update_metadata,
1094
- **kwargs,
1095
- )
1096
- else:
1097
- raise
1098
-
1099
- logger.info(
1100
- f"Updated memory '{memory_id}' for user '{user_id}'",
1101
- extra={
1102
- "app_slug": self.app_slug,
1103
- "user_id": user_id,
1104
- "memory_id": memory_id,
1105
- },
1106
407
  )
408
+ except (
409
+ ValueError,
410
+ TypeError,
411
+ ConnectionError,
412
+ OSError,
413
+ AttributeError,
414
+ RuntimeError,
415
+ KeyError,
416
+ ):
417
+ logger.exception("Mem0 search failed")
418
+ return []
1107
419
 
1108
- return result
1109
-
1110
- except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
1111
- logger.error(
1112
- f"Failed to update memory: {e}",
1113
- exc_info=True,
1114
- extra={
1115
- "app_slug": self.app_slug,
1116
- "user_id": user_id,
1117
- "memory_id": memory_id,
1118
- "error": str(e),
1119
- },
1120
- )
1121
- raise Mem0MemoryServiceError(f"Failed to update memory: {e}") from e
420
+ def get(self, memory_id: str, user_id: str | None = None, **kwargs) -> dict[str, Any]:
421
+ try:
422
+ return self.memory.get(memory_id, **kwargs)
423
+ except (
424
+ ValueError,
425
+ TypeError,
426
+ ConnectionError,
427
+ OSError,
428
+ AttributeError,
429
+ RuntimeError,
430
+ KeyError,
431
+ ):
432
+ return None
1122
433
 
1123
434
  def delete(self, memory_id: str, user_id: str | None = None, **kwargs) -> bool:
1124
- """
1125
- Delete a memory by ID.
1126
-
1127
- Args:
1128
- memory_id: Memory ID to delete
1129
- user_id: Optional user ID for scoping
1130
- **kwargs: Additional mem0.delete() parameters
1131
-
1132
- Returns:
1133
- True if deletion was successful
1134
- """
1135
435
  try:
1136
- # Mem0's delete() may not accept user_id directly
1137
- # Try with user_id first, fall back without it if it fails
1138
- try:
1139
- result = self.memory.delete(memory_id=memory_id, user_id=user_id, **kwargs)
1140
- except TypeError as e:
1141
- if "unexpected keyword argument 'user_id'" in str(e):
1142
- # Mem0 doesn't accept user_id, try without it
1143
- # User scoping should be handled via metadata or filters
1144
- result = self.memory.delete(memory_id=memory_id, **kwargs)
1145
- else:
1146
- raise
1147
-
1148
- logger.info(
1149
- f"Deleted memory '{memory_id}' for user '{user_id}'",
1150
- extra={
1151
- "app_slug": self.app_slug,
1152
- "user_id": user_id,
1153
- "memory_id": memory_id,
1154
- },
1155
- )
1156
-
1157
- return result
1158
-
1159
- except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
1160
- logger.error(
1161
- f"Failed to delete memory: {e}",
1162
- exc_info=True,
1163
- extra={
1164
- "app_slug": self.app_slug,
1165
- "user_id": user_id,
1166
- "memory_id": memory_id,
1167
- "error": str(e),
1168
- },
1169
- )
1170
- raise Mem0MemoryServiceError(f"Failed to delete memory: {e}") from e
436
+ self.memory.delete(memory_id, **kwargs)
437
+ return True
438
+ except (
439
+ AttributeError,
440
+ ValueError,
441
+ RuntimeError,
442
+ KeyError,
443
+ TypeError,
444
+ ConnectionError,
445
+ OSError,
446
+ ):
447
+ return False
1171
448
 
1172
449
  def delete_all(self, user_id: str | None = None, **kwargs) -> bool:
1173
- """
1174
- Delete all memories for a user.
1175
-
1176
- Args:
1177
- user_id: User ID to delete all memories for
1178
- **kwargs: Additional mem0.delete_all() parameters
1179
-
1180
- Returns:
1181
- True if deletion was successful
1182
-
1183
- Example:
1184
- ```python
1185
- success = memory_service.delete_all(user_id="alice")
1186
- ```
1187
- """
1188
450
  try:
1189
- result = self.memory.delete_all(user_id=user_id, **kwargs)
1190
-
1191
- logger.info(
1192
- f"Deleted all memories for user '{user_id}'",
1193
- extra={"app_slug": self.app_slug, "user_id": user_id},
1194
- )
1195
-
451
+ self.memory.delete_all(user_id=user_id, **kwargs)
452
+ return True
453
+ except (
454
+ AttributeError,
455
+ ValueError,
456
+ RuntimeError,
457
+ KeyError,
458
+ TypeError,
459
+ ConnectionError,
460
+ OSError,
461
+ ):
462
+ return False
463
+
464
+ def _normalize_result(self, result: Any) -> list[dict[str, Any]]:
465
+ """Normalize Mem0's return type (dict vs list)."""
466
+ if result is None:
467
+ return []
468
+ if isinstance(result, dict):
469
+ if "results" in result:
470
+ return result["results"]
471
+ if "data" in result:
472
+ return result["data"]
473
+ return [result]
474
+ if isinstance(result, list):
1196
475
  return result
476
+ return []
1197
477
 
1198
- except (AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e:
1199
- logger.error(
1200
- f"Failed to delete all memories: {e}",
1201
- exc_info=True,
1202
- extra={"app_slug": self.app_slug, "user_id": user_id, "error": str(e)},
1203
- )
1204
- raise Mem0MemoryServiceError(f"Failed to delete all memories: {e}") from e
1205
-
1206
-
1207
- def get_memory_service(
1208
- mongo_uri: str, db_name: str, app_slug: str, config: dict[str, Any] | None = None
1209
- ) -> Mem0MemoryService:
1210
- """
1211
- Get or create a Mem0MemoryService instance (cached).
1212
-
1213
- Args:
1214
- mongo_uri: MongoDB connection URI
1215
- db_name: Database name
1216
- app_slug: App slug
1217
- config: Optional memory configuration dict
1218
-
1219
- Returns:
1220
- Mem0MemoryService instance
1221
- """
1222
- # Lazy check for mem0 availability
1223
- if not _check_mem0_available():
1224
- raise Mem0MemoryServiceError(
1225
- "Mem0 dependencies not available. Install with: pip install mem0ai"
1226
- )
1227
478
 
1228
- return Mem0MemoryService(mongo_uri=mongo_uri, db_name=db_name, app_slug=app_slug, config=config)
479
+ def get_memory_service(mongo_uri, db_name, app_slug, config=None):
480
+ return Mem0MemoryService(mongo_uri, db_name, app_slug, config)