mdb-engine 0.7.0__tar.gz → 0.7.2__tar.gz
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-0.7.0/mdb_engine.egg-info → mdb_engine-0.7.2}/PKG-INFO +1 -1
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/__init__.py +7 -13
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/main.py +1 -1
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/engine.py +58 -4
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/service_initialization.py +63 -7
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/memory/README.md +34 -3
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/memory/service.py +284 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2/mdb_engine.egg-info}/PKG-INFO +1 -1
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/pyproject.toml +1 -1
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/setup.py +1 -1
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/LICENSE +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/MANIFEST.in +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/ARCHITECTURE.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/audit.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/base.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/casbin_factory.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/casbin_models.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/config_defaults.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/config_helpers.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/cookie_utils.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/csrf.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/decorators.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/dependencies.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/helpers.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/integration.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/jwt.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/middleware.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/oso_factory.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/provider.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/rate_limiter.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/restrictions.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/session_manager.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/shared_middleware.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/shared_users.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/token_lifecycle.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/token_store.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/users.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/utils.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/websocket_sessions.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/auth/websocket_tickets.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/commands/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/commands/generate.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/commands/migrate.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/commands/show.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/commands/validate.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/cli/utils.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/config.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/constants.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/app_registration.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/app_secrets.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/connection.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/encryption.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/index_management.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/manifest.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/ray_integration.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/seeding.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/core/types.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/database/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/database/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/database/abstraction.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/database/connection.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/database/query_validator.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/database/resource_limiter.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/database/scoped_wrapper.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/dependencies.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/di/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/di/container.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/di/providers.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/di/scopes.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/embeddings/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/embeddings/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/embeddings/dependencies.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/embeddings/service.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/exceptions.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/indexes/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/indexes/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/indexes/helpers.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/indexes/manager.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/memory/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/observability/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/observability/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/observability/health.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/observability/logging.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/observability/metrics.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/repositories/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/repositories/base.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/repositories/mongo.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/repositories/unit_of_work.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/routing/README.md +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/routing/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/routing/websockets.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/utils/__init__.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine/utils/mongo.py +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine.egg-info/SOURCES.txt +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine.egg-info/dependency_links.txt +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine.egg-info/entry_points.txt +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine.egg-info/requires.txt +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/mdb_engine.egg-info/top_level.txt +0 -0
- {mdb_engine-0.7.0 → mdb_engine-0.7.2}/setup.cfg +0 -0
|
@@ -82,19 +82,13 @@ from .repositories import Entity, MongoRepository, Repository, UnitOfWork
|
|
|
82
82
|
from .utils import clean_mongo_doc, clean_mongo_docs
|
|
83
83
|
|
|
84
84
|
__version__ = (
|
|
85
|
-
"0.
|
|
86
|
-
# -
|
|
87
|
-
# -
|
|
88
|
-
# -
|
|
89
|
-
# -
|
|
90
|
-
# -
|
|
91
|
-
# -
|
|
92
|
-
# - ENHANCED: WebSocket authentication with session key support
|
|
93
|
-
# - ENHANCED: CSRF middleware session key validation
|
|
94
|
-
# - ENHANCED: Multi-app WebSocket routing with session keys
|
|
95
|
-
# - BACKWARD COMPATIBLE: Cookie-based authentication fallback
|
|
96
|
-
# - UPDATED: All documentation for secure-by-default approach
|
|
97
|
-
# - COMPREHENSIVE: Unit and integration tests for session keys
|
|
85
|
+
"0.7.2" # Memory service update functionality
|
|
86
|
+
# - ADDED: Memory service update() method for in-place memory updates
|
|
87
|
+
# - ADDED: Support for updating memory content and metadata while preserving IDs
|
|
88
|
+
# - ADDED: Automatic embedding recomputation via Mem0's update method
|
|
89
|
+
# - ADDED: Comprehensive unit tests for memory update functionality (17 tests)
|
|
90
|
+
# - ENHANCED: Memory service now uses Mem0's native update method exclusively
|
|
91
|
+
# - REMOVED: Direct MongoDB update fallback (simplified implementation)
|
|
98
92
|
)
|
|
99
93
|
|
|
100
94
|
__all__ = [
|
|
@@ -36,6 +36,11 @@ from typing import TYPE_CHECKING, Any, Optional
|
|
|
36
36
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
37
37
|
from pymongo.errors import PyMongoError
|
|
38
38
|
|
|
39
|
+
try:
|
|
40
|
+
from openai import OpenAIError
|
|
41
|
+
except ImportError:
|
|
42
|
+
OpenAIError = RuntimeError
|
|
43
|
+
|
|
39
44
|
if TYPE_CHECKING:
|
|
40
45
|
from fastapi import FastAPI
|
|
41
46
|
|
|
@@ -2672,7 +2677,7 @@ class MongoDBEngine:
|
|
|
2672
2677
|
)
|
|
2673
2678
|
|
|
2674
2679
|
@asynccontextmanager
|
|
2675
|
-
async def lifespan(app: FastAPI):
|
|
2680
|
+
async def lifespan(app: FastAPI): # noqa: C901
|
|
2676
2681
|
"""Lifespan context manager for parent app."""
|
|
2677
2682
|
nonlocal mounted_apps, shared_user_pool_initialized
|
|
2678
2683
|
|
|
@@ -2911,11 +2916,60 @@ class MongoDBEngine:
|
|
|
2911
2916
|
child_app.add_middleware(middleware_class)
|
|
2912
2917
|
logger.debug(f"Added AppContextMiddleware to child app '{slug}'")
|
|
2913
2918
|
|
|
2914
|
-
# CRITICAL FIX: Register WebSocket routes on parent app BEFORE mounting
|
|
2915
|
-
# This ensures WebSocket routes are checked before mounted app routes
|
|
2916
|
-
# Mounted apps create catch-all routes that intercept /app-slug/* paths
|
|
2917
2919
|
await _register_websocket_routes(app, app_manifest_data, slug, path_prefix)
|
|
2918
2920
|
|
|
2921
|
+
memory_config = app_manifest_data.get("memory_config")
|
|
2922
|
+
if memory_config and memory_config.get("enabled", False):
|
|
2923
|
+
if engine._service_initializer: # noqa: SLF001
|
|
2924
|
+
try:
|
|
2925
|
+
await engine._service_initializer.initialize_memory_service( # noqa: SLF001
|
|
2926
|
+
slug, memory_config
|
|
2927
|
+
)
|
|
2928
|
+
logger.info(
|
|
2929
|
+
f"Memory service initialized for mounted app '{slug}' "
|
|
2930
|
+
f"in multi-app context"
|
|
2931
|
+
)
|
|
2932
|
+
except OpenAIError as e:
|
|
2933
|
+
logger.warning(
|
|
2934
|
+
f"Memory service initialization skipped for mounted app "
|
|
2935
|
+
f"'{slug}': OpenAI API error. {e}",
|
|
2936
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
2937
|
+
)
|
|
2938
|
+
except (
|
|
2939
|
+
ImportError,
|
|
2940
|
+
AttributeError,
|
|
2941
|
+
TypeError,
|
|
2942
|
+
ValueError,
|
|
2943
|
+
RuntimeError,
|
|
2944
|
+
ConnectionError,
|
|
2945
|
+
OSError,
|
|
2946
|
+
) as e:
|
|
2947
|
+
error_msg = str(e).lower()
|
|
2948
|
+
error_type = type(e).__name__
|
|
2949
|
+
is_api_key_error = (
|
|
2950
|
+
"api_key" in error_msg
|
|
2951
|
+
or "api key" in error_msg
|
|
2952
|
+
or "openai" in error_type.lower()
|
|
2953
|
+
)
|
|
2954
|
+
if is_api_key_error:
|
|
2955
|
+
logger.warning(
|
|
2956
|
+
f"Memory service initialization skipped for mounted app "
|
|
2957
|
+
f"'{slug}': Missing API key or configuration. {e}",
|
|
2958
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
2959
|
+
)
|
|
2960
|
+
else:
|
|
2961
|
+
logger.error(
|
|
2962
|
+
f"Failed to initialize memory service for mounted app "
|
|
2963
|
+
f"'{slug}': {e}",
|
|
2964
|
+
exc_info=True,
|
|
2965
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
2966
|
+
)
|
|
2967
|
+
else:
|
|
2968
|
+
logger.warning(
|
|
2969
|
+
f"Memory service requested for '{slug}' but "
|
|
2970
|
+
f"service_initializer is not available"
|
|
2971
|
+
)
|
|
2972
|
+
|
|
2919
2973
|
# Mount child app at path prefix (AFTER WebSocket routes are registered)
|
|
2920
2974
|
app.mount(path_prefix, child_app)
|
|
2921
2975
|
|
|
@@ -26,6 +26,11 @@ from ..observability import get_logger as get_contextual_logger
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
contextual_logger = get_contextual_logger(__name__)
|
|
28
28
|
|
|
29
|
+
try:
|
|
30
|
+
from openai import OpenAIError
|
|
31
|
+
except ImportError:
|
|
32
|
+
OpenAIError = RuntimeError
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
class ServiceInitializer:
|
|
31
36
|
"""
|
|
@@ -54,7 +59,9 @@ class ServiceInitializer:
|
|
|
54
59
|
self._memory_services: dict[str, Any] = {}
|
|
55
60
|
self._websocket_configs: dict[str, dict[str, Any]] = {}
|
|
56
61
|
|
|
57
|
-
async def initialize_memory_service(
|
|
62
|
+
async def initialize_memory_service(
|
|
63
|
+
self, slug: str, memory_config: dict[str, Any] | None
|
|
64
|
+
) -> None:
|
|
58
65
|
"""
|
|
59
66
|
Initialize Mem0 memory service for an app.
|
|
60
67
|
|
|
@@ -63,8 +70,17 @@ class ServiceInitializer:
|
|
|
63
70
|
|
|
64
71
|
Args:
|
|
65
72
|
slug: App slug
|
|
66
|
-
memory_config: Memory configuration from manifest (already validated)
|
|
73
|
+
memory_config: Memory configuration from manifest (already validated).
|
|
74
|
+
Can be None or empty dict to skip initialization.
|
|
67
75
|
"""
|
|
76
|
+
# Handle None or empty config
|
|
77
|
+
if not memory_config:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Check if memory is enabled (must be checked before import)
|
|
81
|
+
if not memory_config.get("enabled", False):
|
|
82
|
+
return
|
|
83
|
+
|
|
68
84
|
# Try to import Memory service (optional dependency)
|
|
69
85
|
try:
|
|
70
86
|
from ..memory import Mem0MemoryService, Mem0MemoryServiceError
|
|
@@ -145,12 +161,41 @@ class ServiceInitializer:
|
|
|
145
161
|
extra={"app_slug": slug, "error": str(e)},
|
|
146
162
|
exc_info=True,
|
|
147
163
|
)
|
|
148
|
-
except
|
|
149
|
-
contextual_logger.
|
|
150
|
-
f"
|
|
164
|
+
except OpenAIError as e:
|
|
165
|
+
contextual_logger.warning(
|
|
166
|
+
f"Memory service initialization skipped for app '{slug}': "
|
|
167
|
+
f"OpenAI API error. {e}",
|
|
151
168
|
extra={"app_slug": slug, "error": str(e)},
|
|
152
|
-
exc_info=True,
|
|
153
169
|
)
|
|
170
|
+
except (
|
|
171
|
+
ImportError,
|
|
172
|
+
AttributeError,
|
|
173
|
+
TypeError,
|
|
174
|
+
ValueError,
|
|
175
|
+
RuntimeError,
|
|
176
|
+
ConnectionError,
|
|
177
|
+
OSError,
|
|
178
|
+
) as e:
|
|
179
|
+
error_msg = str(e).lower()
|
|
180
|
+
error_type = type(e).__name__
|
|
181
|
+
is_api_key_error = (
|
|
182
|
+
"api_key" in error_msg
|
|
183
|
+
or "api key" in error_msg
|
|
184
|
+
or "openai" in error_type.lower()
|
|
185
|
+
or "openai" in error_msg
|
|
186
|
+
)
|
|
187
|
+
if is_api_key_error:
|
|
188
|
+
contextual_logger.warning(
|
|
189
|
+
f"Memory service initialization skipped for app '{slug}': "
|
|
190
|
+
f"Missing API key or configuration. {e}",
|
|
191
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
contextual_logger.error(
|
|
195
|
+
f"Error initializing memory service for app '{slug}': {e}",
|
|
196
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
197
|
+
exc_info=True,
|
|
198
|
+
)
|
|
154
199
|
|
|
155
200
|
async def register_websockets(self, slug: str, websockets_config: dict[str, Any]) -> None:
|
|
156
201
|
"""
|
|
@@ -330,7 +375,18 @@ class ServiceInitializer:
|
|
|
330
375
|
extra={"app_slug": slug},
|
|
331
376
|
)
|
|
332
377
|
return None
|
|
333
|
-
|
|
378
|
+
return service
|
|
379
|
+
|
|
380
|
+
# Service not found - check if it should be initialized but wasn't
|
|
381
|
+
# This can happen in multi-app context if initialization was missed
|
|
382
|
+
# Note: We can't do async initialization here, so we just log a warning
|
|
383
|
+
# The explicit initialization in create_multi_app should handle this
|
|
384
|
+
contextual_logger.debug(
|
|
385
|
+
f"Memory service not found for '{slug}' - "
|
|
386
|
+
f"it may not be initialized yet or memory is disabled",
|
|
387
|
+
extra={"app_slug": slug},
|
|
388
|
+
)
|
|
389
|
+
return None
|
|
334
390
|
except (KeyError, AttributeError, TypeError) as e:
|
|
335
391
|
contextual_logger.error(
|
|
336
392
|
f"Error retrieving memory service for '{slug}': {e}",
|
|
@@ -181,18 +181,49 @@ memories = await memory_service.get_all(
|
|
|
181
181
|
|
|
182
182
|
### Update Memory
|
|
183
183
|
|
|
184
|
-
Update existing memories:
|
|
184
|
+
Update existing memories in-place while preserving the original memory ID and creation timestamp:
|
|
185
185
|
|
|
186
186
|
```python
|
|
187
|
-
# Update memory
|
|
188
|
-
updated =
|
|
187
|
+
# Update memory content and metadata
|
|
188
|
+
updated = memory_service.update(
|
|
189
|
+
memory_id="memory_123",
|
|
190
|
+
user_id="user123",
|
|
191
|
+
memory="Updated memory content",
|
|
192
|
+
metadata={"updated": True, "category": "technical"}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Update using messages format
|
|
196
|
+
updated = memory_service.update(
|
|
189
197
|
memory_id="memory_123",
|
|
190
198
|
user_id="user123",
|
|
191
199
|
messages=[{"role": "user", "content": "Updated content"}],
|
|
192
200
|
metadata={"updated": True}
|
|
193
201
|
)
|
|
202
|
+
|
|
203
|
+
# Update only metadata (content unchanged)
|
|
204
|
+
updated = memory_service.update(
|
|
205
|
+
memory_id="memory_123",
|
|
206
|
+
user_id="user123",
|
|
207
|
+
metadata={"category": "updated"}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Backward compatibility: using 'data' parameter
|
|
211
|
+
updated = memory_service.update(
|
|
212
|
+
memory_id="memory_123",
|
|
213
|
+
user_id="user123",
|
|
214
|
+
data="Updated content",
|
|
215
|
+
metadata={"updated": True}
|
|
216
|
+
)
|
|
194
217
|
```
|
|
195
218
|
|
|
219
|
+
**Key Features:**
|
|
220
|
+
- **Preserves Memory ID**: The original memory ID is maintained
|
|
221
|
+
- **Preserves Creation Timestamp**: `created_at` is not modified
|
|
222
|
+
- **Updates Timestamp**: `updated_at` is automatically set to current time
|
|
223
|
+
- **Recomputes Embeddings**: If content changes, the embedding vector is automatically recomputed
|
|
224
|
+
- **Metadata Merging**: New metadata is merged with existing metadata (doesn't replace)
|
|
225
|
+
- **Partial Updates**: Can update content only, metadata only, or both
|
|
226
|
+
|
|
196
227
|
### Delete Memory
|
|
197
228
|
|
|
198
229
|
Remove memories:
|
|
@@ -44,6 +44,20 @@ class Mem0MemoryServiceError(Exception):
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class Mem0MemoryService:
|
|
47
|
+
"""
|
|
48
|
+
Production-ready Mem0 Memory Service with MongoDB integration.
|
|
49
|
+
|
|
50
|
+
Features:
|
|
51
|
+
- In-place memory updates preserving IDs and timestamps
|
|
52
|
+
- Automatic embedding recomputation on content changes
|
|
53
|
+
- Knowledge graph support (if enabled in Mem0 config)
|
|
54
|
+
- Comprehensive error handling and logging
|
|
55
|
+
- Backward compatibility with existing code
|
|
56
|
+
|
|
57
|
+
All operations go through Mem0's API to ensure proper state management,
|
|
58
|
+
graph updates, and relationship handling.
|
|
59
|
+
"""
|
|
60
|
+
|
|
47
61
|
def __init__(
|
|
48
62
|
self,
|
|
49
63
|
mongo_uri: str,
|
|
@@ -54,6 +68,9 @@ class Mem0MemoryService:
|
|
|
54
68
|
if not _check_mem0_available():
|
|
55
69
|
raise Mem0MemoryServiceError("Mem0 not installed. pip install mem0ai")
|
|
56
70
|
|
|
71
|
+
if not mongo_uri or not db_name or not app_slug:
|
|
72
|
+
raise Mem0MemoryServiceError("mongo_uri, db_name, and app_slug are required parameters")
|
|
73
|
+
|
|
57
74
|
self.mongo_uri = mongo_uri
|
|
58
75
|
self.db_name = db_name
|
|
59
76
|
self.app_slug = app_slug
|
|
@@ -461,6 +478,273 @@ class Mem0MemoryService:
|
|
|
461
478
|
):
|
|
462
479
|
return False
|
|
463
480
|
|
|
481
|
+
def update(
|
|
482
|
+
self,
|
|
483
|
+
memory_id: str,
|
|
484
|
+
user_id: str | None = None,
|
|
485
|
+
memory: str | None = None,
|
|
486
|
+
data: str | dict[str, Any] | None = None,
|
|
487
|
+
messages: str | list[dict[str, str]] | None = None,
|
|
488
|
+
metadata: dict[str, Any] | None = None,
|
|
489
|
+
**kwargs,
|
|
490
|
+
) -> dict[str, Any] | None:
|
|
491
|
+
"""
|
|
492
|
+
Update an existing memory in-place with production-grade error handling.
|
|
493
|
+
|
|
494
|
+
Updates the memory content and/or metadata while preserving:
|
|
495
|
+
- Original memory ID (never changes)
|
|
496
|
+
- Creation timestamp (created_at) - preserved
|
|
497
|
+
- Other existing fields - preserved unless explicitly updated
|
|
498
|
+
|
|
499
|
+
If content is updated, the embedding vector is automatically recomputed.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
memory_id: The ID of the memory to update (required)
|
|
503
|
+
user_id: The user ID who owns the memory (for scoping and security)
|
|
504
|
+
memory: New memory content as a string (optional)
|
|
505
|
+
data: Alternative parameter name for memory content (backward compatibility).
|
|
506
|
+
Can be a string or dict with 'memory'/'text'/'content' key.
|
|
507
|
+
messages: Alternative way to provide content as messages (optional).
|
|
508
|
+
Can be a string or list of dicts with 'content' key.
|
|
509
|
+
metadata: Metadata updates to merge with existing metadata (optional).
|
|
510
|
+
Merged, not replaced - existing keys are preserved unless overridden.
|
|
511
|
+
**kwargs: Additional arguments passed to Mem0 operations
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Updated memory object with same ID, or None if memory not found
|
|
515
|
+
|
|
516
|
+
Raises:
|
|
517
|
+
Mem0MemoryServiceError: If update operation fails
|
|
518
|
+
ValueError: If memory_id is invalid or empty
|
|
519
|
+
|
|
520
|
+
Example:
|
|
521
|
+
```python
|
|
522
|
+
# Update content and metadata
|
|
523
|
+
updated = memory_service.update(
|
|
524
|
+
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
525
|
+
user_id="user123",
|
|
526
|
+
memory="I love Python programming",
|
|
527
|
+
metadata={"category": "technical", "updated": True}
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Update only metadata (content unchanged)
|
|
531
|
+
updated = memory_service.update(
|
|
532
|
+
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
533
|
+
user_id="user123",
|
|
534
|
+
metadata={"category": "updated"}
|
|
535
|
+
)
|
|
536
|
+
```
|
|
537
|
+
"""
|
|
538
|
+
# Input validation
|
|
539
|
+
if not memory_id or not isinstance(memory_id, str) or not memory_id.strip():
|
|
540
|
+
raise ValueError("memory_id is required and must be a non-empty string")
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
# Normalize data parameter (backward compatibility)
|
|
544
|
+
normalized_memory = self._normalize_content_input(memory, data, messages)
|
|
545
|
+
normalized_metadata = self._normalize_metadata_input(metadata, data)
|
|
546
|
+
|
|
547
|
+
# Verify memory exists before attempting update
|
|
548
|
+
existing_memory = self.get(memory_id=memory_id, user_id=user_id, **kwargs)
|
|
549
|
+
if not existing_memory:
|
|
550
|
+
logger.warning(
|
|
551
|
+
f"Memory {memory_id} not found for update",
|
|
552
|
+
extra={"memory_id": memory_id, "user_id": user_id},
|
|
553
|
+
)
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
# Use Mem0's built-in update method
|
|
557
|
+
# Mem0's Memory class update method handles:
|
|
558
|
+
# - In-place updates preserving memory ID
|
|
559
|
+
# - Automatic embedding recomputation
|
|
560
|
+
# - Metadata merging
|
|
561
|
+
# - User scoping
|
|
562
|
+
# - Knowledge graph updates (if enabled)
|
|
563
|
+
# - Relationship management
|
|
564
|
+
if not hasattr(self.memory, "update") or not callable(self.memory.update):
|
|
565
|
+
raise Mem0MemoryServiceError(
|
|
566
|
+
"Mem0 update method not available. "
|
|
567
|
+
"Please ensure you're using a compatible version of mem0ai "
|
|
568
|
+
"that supports updates. Install with: pip install --upgrade mem0ai"
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
result = self._update_via_mem0(
|
|
572
|
+
memory_id=memory_id,
|
|
573
|
+
user_id=user_id,
|
|
574
|
+
memory=normalized_memory,
|
|
575
|
+
metadata=normalized_metadata,
|
|
576
|
+
**kwargs,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if result is None:
|
|
580
|
+
logger.warning(
|
|
581
|
+
f"Mem0 update returned None for memory {memory_id}",
|
|
582
|
+
extra={"memory_id": memory_id, "user_id": user_id},
|
|
583
|
+
)
|
|
584
|
+
return None
|
|
585
|
+
|
|
586
|
+
logger.info(
|
|
587
|
+
f"Successfully updated memory {memory_id} using Mem0 update method",
|
|
588
|
+
extra={
|
|
589
|
+
"memory_id": memory_id,
|
|
590
|
+
"content_updated": bool(normalized_memory),
|
|
591
|
+
"metadata_updated": bool(normalized_metadata),
|
|
592
|
+
},
|
|
593
|
+
)
|
|
594
|
+
return result
|
|
595
|
+
|
|
596
|
+
except ValueError:
|
|
597
|
+
# Re-raise validation errors as-is
|
|
598
|
+
raise
|
|
599
|
+
except (AttributeError, TypeError, ValueError, KeyError) as e:
|
|
600
|
+
logger.exception(
|
|
601
|
+
f"Error updating memory {memory_id}",
|
|
602
|
+
extra={"memory_id": memory_id, "user_id": user_id},
|
|
603
|
+
)
|
|
604
|
+
raise Mem0MemoryServiceError(f"Update failed: {e}") from e
|
|
605
|
+
|
|
606
|
+
def _normalize_content_input(
|
|
607
|
+
self,
|
|
608
|
+
memory: str | None,
|
|
609
|
+
data: str | dict[str, Any] | None,
|
|
610
|
+
messages: str | list[dict[str, str]] | None,
|
|
611
|
+
) -> str | None:
|
|
612
|
+
"""
|
|
613
|
+
Normalize content input from various parameter formats.
|
|
614
|
+
|
|
615
|
+
Priority: memory > data > messages
|
|
616
|
+
"""
|
|
617
|
+
# Already have memory content
|
|
618
|
+
if memory:
|
|
619
|
+
if not isinstance(memory, str):
|
|
620
|
+
raise TypeError("memory parameter must be a string")
|
|
621
|
+
return memory.strip() if memory.strip() else None
|
|
622
|
+
|
|
623
|
+
# Check data parameter
|
|
624
|
+
if data:
|
|
625
|
+
if isinstance(data, str):
|
|
626
|
+
return data.strip() if data.strip() else None
|
|
627
|
+
elif isinstance(data, dict):
|
|
628
|
+
content = data.get("memory") or data.get("text") or data.get("content")
|
|
629
|
+
if content and isinstance(content, str):
|
|
630
|
+
return content.strip() if content.strip() else None
|
|
631
|
+
|
|
632
|
+
# Check messages parameter
|
|
633
|
+
if messages:
|
|
634
|
+
if isinstance(messages, str):
|
|
635
|
+
return messages.strip() if messages.strip() else None
|
|
636
|
+
elif isinstance(messages, list):
|
|
637
|
+
content_parts = []
|
|
638
|
+
for msg in messages:
|
|
639
|
+
if isinstance(msg, dict) and "content" in msg:
|
|
640
|
+
content = msg["content"]
|
|
641
|
+
if isinstance(content, str) and content.strip():
|
|
642
|
+
content_parts.append(content.strip())
|
|
643
|
+
if content_parts:
|
|
644
|
+
return " ".join(content_parts)
|
|
645
|
+
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
def _normalize_metadata_input(
|
|
649
|
+
self, metadata: dict[str, Any] | None, data: dict[str, Any] | None
|
|
650
|
+
) -> dict[str, Any] | None:
|
|
651
|
+
"""Normalize metadata input, extracting from data dict if needed."""
|
|
652
|
+
if metadata is not None and not isinstance(metadata, dict):
|
|
653
|
+
raise TypeError("metadata must be a dict or None")
|
|
654
|
+
|
|
655
|
+
# If metadata provided directly, use it
|
|
656
|
+
if metadata is not None:
|
|
657
|
+
return metadata
|
|
658
|
+
|
|
659
|
+
# Check if metadata is in data dict
|
|
660
|
+
if isinstance(data, dict) and "metadata" in data:
|
|
661
|
+
data_metadata = data.get("metadata")
|
|
662
|
+
if isinstance(data_metadata, dict):
|
|
663
|
+
return data_metadata
|
|
664
|
+
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
def _update_via_mem0(
|
|
668
|
+
self,
|
|
669
|
+
memory_id: str,
|
|
670
|
+
user_id: str | None,
|
|
671
|
+
memory: str | None,
|
|
672
|
+
metadata: dict[str, Any] | None,
|
|
673
|
+
**kwargs,
|
|
674
|
+
) -> dict[str, Any] | None:
|
|
675
|
+
"""
|
|
676
|
+
Update memory using Mem0's built-in update method.
|
|
677
|
+
|
|
678
|
+
This is the primary update path. Mem0's update method handles:
|
|
679
|
+
- In-place updates preserving memory ID and created_at timestamp
|
|
680
|
+
- Automatic embedding recomputation when content changes
|
|
681
|
+
- Metadata merging
|
|
682
|
+
- User scoping for security
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
memory_id: Memory ID to update
|
|
686
|
+
user_id: User ID for scoping
|
|
687
|
+
memory: New memory content (normalized)
|
|
688
|
+
metadata: Metadata to merge (normalized)
|
|
689
|
+
**kwargs: Additional arguments passed to Mem0
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
Updated memory dict or None if not found
|
|
693
|
+
|
|
694
|
+
Raises:
|
|
695
|
+
Various exceptions from Mem0 if update fails
|
|
696
|
+
"""
|
|
697
|
+
# Build update parameters matching Mem0's API
|
|
698
|
+
# Mem0's update method signature:
|
|
699
|
+
# update(memory_id, text=None, metadata=None, user_id=None, **kwargs)
|
|
700
|
+
update_kwargs: dict[str, Any] = {"memory_id": memory_id}
|
|
701
|
+
|
|
702
|
+
# Add user_id for scoping (Mem0 supports this)
|
|
703
|
+
if user_id:
|
|
704
|
+
update_kwargs["user_id"] = str(user_id)
|
|
705
|
+
|
|
706
|
+
# Add text/content if provided
|
|
707
|
+
# Mem0 uses "text" parameter for content
|
|
708
|
+
if memory:
|
|
709
|
+
update_kwargs["text"] = memory
|
|
710
|
+
|
|
711
|
+
# Add metadata if provided
|
|
712
|
+
# Mem0 merges metadata automatically
|
|
713
|
+
if metadata is not None:
|
|
714
|
+
update_kwargs["metadata"] = metadata
|
|
715
|
+
|
|
716
|
+
# Pass through any additional kwargs
|
|
717
|
+
update_kwargs.update(kwargs)
|
|
718
|
+
|
|
719
|
+
logger.debug(
|
|
720
|
+
f"Calling mem0.update() for memory_id={memory_id}",
|
|
721
|
+
extra={
|
|
722
|
+
"memory_id": memory_id,
|
|
723
|
+
"has_content": bool(memory),
|
|
724
|
+
"has_metadata": bool(metadata),
|
|
725
|
+
"user_id": user_id,
|
|
726
|
+
},
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# Call Mem0's update method directly
|
|
730
|
+
# This handles all the complexity: embedding recomputation, ID preservation, etc.
|
|
731
|
+
result = self.memory.update(**update_kwargs)
|
|
732
|
+
|
|
733
|
+
# Normalize result format
|
|
734
|
+
# Mem0 may return dict, list, or other formats
|
|
735
|
+
if isinstance(result, dict):
|
|
736
|
+
return result
|
|
737
|
+
elif isinstance(result, list) and len(result) > 0:
|
|
738
|
+
# If list, return first item
|
|
739
|
+
return result[0] if isinstance(result[0], dict) else None
|
|
740
|
+
else:
|
|
741
|
+
# If result is None or unexpected format, return None to trigger fallback
|
|
742
|
+
logger.debug(
|
|
743
|
+
f"Mem0 update returned unexpected format: {type(result)}",
|
|
744
|
+
extra={"memory_id": memory_id},
|
|
745
|
+
)
|
|
746
|
+
return None
|
|
747
|
+
|
|
464
748
|
def _normalize_result(self, result: Any) -> list[dict[str, Any]]:
|
|
465
749
|
"""Normalize Mem0's return type (dict vs list)."""
|
|
466
750
|
if result is None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|