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.
- mdb_engine/__init__.py +8 -1
- mdb_engine/auth/README.md +6 -0
- mdb_engine/auth/shared_middleware.py +330 -119
- mdb_engine/core/engine.py +417 -5
- mdb_engine/core/manifest.py +114 -0
- mdb_engine/embeddings/service.py +37 -8
- mdb_engine/memory/README.md +93 -2
- mdb_engine/memory/service.py +348 -1096
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- {mdb_engine-0.2.3.dist-info → mdb_engine-0.3.0.dist-info}/METADATA +193 -14
- {mdb_engine-0.2.3.dist-info → mdb_engine-0.3.0.dist-info}/RECORD +16 -15
- {mdb_engine-0.2.3.dist-info → mdb_engine-0.3.0.dist-info}/WHEEL +1 -1
- {mdb_engine-0.2.3.dist-info → mdb_engine-0.3.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.2.3.dist-info → mdb_engine-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.2.3.dist-info → mdb_engine-0.3.0.dist-info}/top_level.txt +0 -0
mdb_engine/memory/service.py
CHANGED
|
@@ -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:
|
|
24
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
374
|
-
#
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
#
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
152
|
+
def _build_provider_config(self, component, model_name):
|
|
153
|
+
"""
|
|
154
|
+
Build provider configuration for embeddings or LLM.
|
|
446
155
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
"
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
f"user_id=
|
|
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)
|
|
580
|
-
metadata=
|
|
581
|
-
|
|
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"
|
|
602
|
-
|
|
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"
|
|
618
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
"
|
|
650
|
-
|
|
651
|
-
"
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
682
|
-
|
|
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
|
|
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
|
-
|
|
705
|
-
if
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
370
|
+
call_kwargs.update(kwargs)
|
|
841
371
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1069
|
-
if
|
|
1070
|
-
|
|
399
|
+
call_kwargs = {"limit": limit}
|
|
400
|
+
if final_filters:
|
|
401
|
+
call_kwargs["filters"] = final_filters
|
|
1071
402
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
479
|
+
def get_memory_service(mongo_uri, db_name, app_slug, config=None):
|
|
480
|
+
return Mem0MemoryService(mongo_uri, db_name, app_slug, config)
|