mdb-engine 0.7.3__py3-none-any.whl → 0.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdb_engine/__init__.py +1 -1
- mdb_engine/cli/main.py +1 -1
- mdb_engine/core/engine.py +3 -2
- mdb_engine/core/service_initialization.py +5 -4
- mdb_engine/core/types.py +2 -2
- mdb_engine/dependencies.py +3 -3
- mdb_engine/memory/README.md +145 -21
- mdb_engine/memory/__init__.py +9 -0
- mdb_engine/memory/base.py +194 -0
- mdb_engine/memory/service.py +318 -162
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.4.dist-info}/METADATA +1 -1
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.4.dist-info}/RECORD +16 -15
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.4.dist-info}/WHEEL +0 -0
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.4.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.4.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.7.3.dist-info → mdb_engine-0.7.4.dist-info}/top_level.txt +0 -0
mdb_engine/__init__.py
CHANGED
mdb_engine/cli/main.py
CHANGED
mdb_engine/core/engine.py
CHANGED
|
@@ -910,13 +910,14 @@ class MongoDBEngine:
|
|
|
910
910
|
|
|
911
911
|
def get_memory_service(self, slug: str) -> Any | None:
|
|
912
912
|
"""
|
|
913
|
-
Get
|
|
913
|
+
Get memory service for an app (returns BaseMemoryService instance).
|
|
914
914
|
|
|
915
915
|
Args:
|
|
916
916
|
slug: App slug
|
|
917
917
|
|
|
918
918
|
Returns:
|
|
919
|
-
|
|
919
|
+
BaseMemoryService instance (currently Mem0MemoryService) if memory is enabled
|
|
920
|
+
for this app, None otherwise
|
|
920
921
|
|
|
921
922
|
Example:
|
|
922
923
|
```python
|
|
@@ -63,10 +63,10 @@ class ServiceInitializer:
|
|
|
63
63
|
self, slug: str, memory_config: dict[str, Any] | None
|
|
64
64
|
) -> None:
|
|
65
65
|
"""
|
|
66
|
-
Initialize
|
|
66
|
+
Initialize memory service for an app (defaults to Mem0 implementation).
|
|
67
67
|
|
|
68
68
|
Memory support is OPTIONAL - only processes if dependencies are available.
|
|
69
|
-
|
|
69
|
+
Currently uses Mem0.ai which handles embeddings and LLM via environment variables (.env).
|
|
70
70
|
|
|
71
71
|
Args:
|
|
72
72
|
slug: App slug
|
|
@@ -356,13 +356,14 @@ class ServiceInitializer:
|
|
|
356
356
|
|
|
357
357
|
def get_memory_service(self, slug: str) -> Any | None:
|
|
358
358
|
"""
|
|
359
|
-
Get
|
|
359
|
+
Get memory service for an app (returns BaseMemoryService instance).
|
|
360
360
|
|
|
361
361
|
Args:
|
|
362
362
|
slug: App slug
|
|
363
363
|
|
|
364
364
|
Returns:
|
|
365
|
-
|
|
365
|
+
BaseMemoryService instance (currently Mem0MemoryService) if memory is enabled
|
|
366
|
+
for this app, None otherwise
|
|
366
367
|
"""
|
|
367
368
|
try:
|
|
368
369
|
service = self._memory_services.get(slug)
|
mdb_engine/core/types.py
CHANGED
|
@@ -11,9 +11,9 @@ This module is part of MDB_ENGINE - MongoDB Engine.
|
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Literal, TypedDict
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from ..memory import
|
|
14
|
+
from ..memory import BaseMemoryService
|
|
15
15
|
else:
|
|
16
|
-
|
|
16
|
+
BaseMemoryService = Any
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
# ============================================================================
|
mdb_engine/dependencies.py
CHANGED
|
@@ -33,7 +33,7 @@ if TYPE_CHECKING:
|
|
|
33
33
|
from .core.engine import MongoDBEngine
|
|
34
34
|
from .database.scoped_wrapper import ScopedMongoWrapper
|
|
35
35
|
from .embeddings.service import EmbeddingService
|
|
36
|
-
from .memory
|
|
36
|
+
from .memory import BaseMemoryService
|
|
37
37
|
|
|
38
38
|
logger = logging.getLogger(__name__)
|
|
39
39
|
|
|
@@ -121,8 +121,8 @@ async def get_embedding_service(request: Request) -> "EmbeddingService":
|
|
|
121
121
|
raise HTTPException(503, f"Failed to initialize embedding service: {e}") from e
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
async def get_memory_service(request: Request) -> Optional["
|
|
125
|
-
"""Get the
|
|
124
|
+
async def get_memory_service(request: Request) -> Optional["BaseMemoryService"]:
|
|
125
|
+
"""Get the memory service if configured (defaults to Mem0 implementation)."""
|
|
126
126
|
engine = getattr(request.app.state, "engine", None)
|
|
127
127
|
slug = getattr(request.app.state, "app_slug", None)
|
|
128
128
|
if not engine or not slug:
|
mdb_engine/memory/README.md
CHANGED
|
@@ -2,9 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
Mem0.ai integration for intelligent memory management in MDB_ENGINE applications. Provides semantic memory storage, retrieval, and inference capabilities with MongoDB integration.
|
|
4
4
|
|
|
5
|
+
## 🎉 What's New
|
|
6
|
+
|
|
7
|
+
### Extensible Architecture (Latest)
|
|
8
|
+
|
|
9
|
+
**Base Class Pattern for Future Extensibility!**
|
|
10
|
+
|
|
11
|
+
The memory service now uses an abstract base class pattern, enabling future memory provider implementations while maintaining backward compatibility:
|
|
12
|
+
|
|
13
|
+
- **🏗️ BaseMemoryService**: Abstract base class defining the memory service interface
|
|
14
|
+
- **🔌 Provider Extensibility**: Easy to add new memory providers (LangChain, custom implementations, etc.)
|
|
15
|
+
- **✅ Backward Compatible**: All existing code continues to work without changes
|
|
16
|
+
- **📝 Type Safety**: Better IDE support and type checking with abstract base class
|
|
17
|
+
- **🎯 Consistent API**: All memory providers implement the same interface
|
|
18
|
+
|
|
19
|
+
### v0.7.4 Enhancements
|
|
20
|
+
|
|
21
|
+
**Enhanced Mem0 Integration - Production Ready!**
|
|
22
|
+
|
|
23
|
+
- **🔧 Hybrid Update Pattern**: Content updates via Mem0 (triggers re-embedding), metadata updates via direct MongoDB (full control, no API limitations)
|
|
24
|
+
- **📊 Direct MongoDB Access**: Reliable data retrieval directly from MongoDB, bypassing Mem0 API inconsistencies
|
|
25
|
+
- **🏷️ Full Metadata Support**: Update any metadata field without restrictions - not limited by Mem0's API
|
|
26
|
+
- **✅ Correct Mem0 Structure**: Properly handles Mem0's MongoDB structure (`_id` as document ID, `payload` for memory data)
|
|
27
|
+
- **🛡️ Robust Error Handling**: Specific exception handling with proper KeyboardInterrupt/SystemExit propagation
|
|
28
|
+
- **🔍 Reliable Returns**: Always returns normalized documents fetched directly from MongoDB (guaranteed structure)
|
|
29
|
+
|
|
30
|
+
> 📖 **Want to understand why we use manual MongoDB access?** See the [Mem0 Implementation Guide](../../docs/guides/MEM0_IMPLEMENTATION.md) for detailed explanations of our architectural decisions, Mem0's MongoDB structure, and things to watch out for.
|
|
31
|
+
|
|
5
32
|
## Features
|
|
6
33
|
|
|
7
|
-
- **
|
|
34
|
+
- **Extensible Architecture**: Base class pattern allows for multiple memory provider implementations
|
|
35
|
+
- **Mem0 Integration**: Default implementation using Mem0.ai for intelligent memory management
|
|
8
36
|
- **MongoDB Storage**: Built-in MongoDB vector store integration
|
|
9
37
|
- **Auto-Detection**: Automatically detects OpenAI or Azure OpenAI from environment variables
|
|
10
38
|
- **Semantic Search**: Vector-based semantic memory search
|
|
@@ -67,7 +95,7 @@ Enable memory service in your `manifest.json`:
|
|
|
67
95
|
### Basic Usage
|
|
68
96
|
|
|
69
97
|
```python
|
|
70
|
-
from mdb_engine.memory import
|
|
98
|
+
from mdb_engine.memory import BaseMemoryService # Base class for type hints
|
|
71
99
|
from mdb_engine.core import MongoDBEngine
|
|
72
100
|
|
|
73
101
|
# Initialize engine
|
|
@@ -75,7 +103,8 @@ engine = MongoDBEngine(mongo_uri="...", db_name="...")
|
|
|
75
103
|
await engine.initialize()
|
|
76
104
|
|
|
77
105
|
# Get memory service (automatically configured from manifest)
|
|
78
|
-
|
|
106
|
+
# Returns BaseMemoryService instance (currently Mem0MemoryService)
|
|
107
|
+
memory_service: BaseMemoryService = engine.get_memory_service("my_app")
|
|
79
108
|
|
|
80
109
|
# Add memory
|
|
81
110
|
memory = await memory_service.add(
|
|
@@ -163,14 +192,17 @@ results = await memory_service.search(
|
|
|
163
192
|
|
|
164
193
|
### Get Memories
|
|
165
194
|
|
|
166
|
-
Retrieve memories for a user:
|
|
195
|
+
Retrieve memories for a user. The service automatically normalizes Mem0's MongoDB structure (`_id`, `payload`) into a consistent API format:
|
|
167
196
|
|
|
168
197
|
```python
|
|
169
198
|
# Get all memories
|
|
170
199
|
all_memories = await memory_service.get_all(user_id="user123")
|
|
200
|
+
# Returns normalized format: [{"id": "...", "memory": "...", "metadata": {...}, ...}]
|
|
171
201
|
|
|
172
202
|
# Get specific memory
|
|
173
203
|
memory = await memory_service.get(memory_id="memory_123", user_id="user123")
|
|
204
|
+
# Returns normalized format: {"id": "...", "memory": "...", "metadata": {...}, ...}
|
|
205
|
+
# Note: memory_id can be either Mem0's _id or the normalized id field
|
|
174
206
|
|
|
175
207
|
# Get memories with filters
|
|
176
208
|
memories = await memory_service.get_all(
|
|
@@ -179,9 +211,16 @@ memories = await memory_service.get_all(
|
|
|
179
211
|
)
|
|
180
212
|
```
|
|
181
213
|
|
|
214
|
+
**Note**: The service handles Mem0's internal MongoDB structure (`_id` as document ID, `payload` containing memory data) automatically. All methods return normalized documents with consistent `id`, `memory`, `text`, and `metadata` fields.
|
|
215
|
+
|
|
182
216
|
### Update Memory
|
|
183
217
|
|
|
184
|
-
Update existing memories
|
|
218
|
+
Update existing memories using a **hybrid approach** that combines Mem0's embedding capabilities with direct MongoDB control:
|
|
219
|
+
|
|
220
|
+
**Architecture:**
|
|
221
|
+
- **Content Updates**: Routed via Mem0 (triggers automatic re-embedding)
|
|
222
|
+
- **Metadata Updates**: Routed via direct PyMongo (full control, no API limitations)
|
|
223
|
+
- **Return Value**: Always fetched from MongoDB (guaranteed correct structure)
|
|
185
224
|
|
|
186
225
|
```python
|
|
187
226
|
# Update memory content and metadata
|
|
@@ -200,14 +239,21 @@ updated = memory_service.update(
|
|
|
200
239
|
metadata={"updated": True}
|
|
201
240
|
)
|
|
202
241
|
|
|
203
|
-
# Update only metadata (content unchanged)
|
|
242
|
+
# Update only metadata (content unchanged) - FULLY SUPPORTED
|
|
243
|
+
updated = memory_service.update(
|
|
244
|
+
memory_id="memory_123",
|
|
245
|
+
user_id="user123",
|
|
246
|
+
metadata={"category": "updated", "priority": "high"}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Update only content (no metadata changes)
|
|
204
250
|
updated = memory_service.update(
|
|
205
251
|
memory_id="memory_123",
|
|
206
252
|
user_id="user123",
|
|
207
|
-
|
|
253
|
+
memory="Updated content only"
|
|
208
254
|
)
|
|
209
255
|
|
|
210
|
-
#
|
|
256
|
+
# Using 'data' parameter
|
|
211
257
|
updated = memory_service.update(
|
|
212
258
|
memory_id="memory_123",
|
|
213
259
|
user_id="user123",
|
|
@@ -217,12 +263,18 @@ updated = memory_service.update(
|
|
|
217
263
|
```
|
|
218
264
|
|
|
219
265
|
**Key Features:**
|
|
266
|
+
- **Hybrid Architecture**: Mem0 handles embeddings, MongoDB handles data persistence
|
|
267
|
+
- **Full Metadata Support**: Update any metadata field (not limited by Mem0 API)
|
|
220
268
|
- **Preserves Memory ID**: The original memory ID is maintained
|
|
221
269
|
- **Preserves Creation Timestamp**: `created_at` is not modified
|
|
222
270
|
- **Updates Timestamp**: `updated_at` is automatically set to current time
|
|
223
|
-
- **Recomputes Embeddings**: If content changes, the embedding vector is automatically recomputed
|
|
224
|
-
- **
|
|
271
|
+
- **Recomputes Embeddings**: If content changes, the embedding vector is automatically recomputed via Mem0
|
|
272
|
+
- **Reliable Returns**: Always returns the actual document from MongoDB (not Mem0's response format)
|
|
225
273
|
- **Partial Updates**: Can update content only, metadata only, or both
|
|
274
|
+
- **Security**: Validates user_id ownership before allowing updates
|
|
275
|
+
- **Mem0 Structure Aware**: Correctly handles Mem0's MongoDB structure (`_id` as document ID, `payload` for memory data)
|
|
276
|
+
- **Direct MongoDB Access**: Uses PyMongo for reliable data operations, ensuring consistency
|
|
277
|
+
- **Normalized Responses**: All methods return consistent document structure regardless of Mem0's internal format
|
|
226
278
|
|
|
227
279
|
### Delete Memory
|
|
228
280
|
|
|
@@ -336,25 +388,93 @@ for memory in insights:
|
|
|
336
388
|
print(f"Insights: {memory.get('insights')}")
|
|
337
389
|
```
|
|
338
390
|
|
|
391
|
+
## Architecture
|
|
392
|
+
|
|
393
|
+
### Base Class Pattern
|
|
394
|
+
|
|
395
|
+
The memory service uses an abstract base class pattern for extensibility:
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
from mdb_engine.memory import BaseMemoryService, MemoryServiceError
|
|
399
|
+
|
|
400
|
+
# BaseMemoryService defines the interface
|
|
401
|
+
# Mem0MemoryService implements it (default provider)
|
|
402
|
+
# Future providers can inherit from BaseMemoryService
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Benefits:**
|
|
406
|
+
- **Type Safety**: Use `BaseMemoryService` for type hints
|
|
407
|
+
- **Extensibility**: Easy to add new providers (LangChain, custom, etc.)
|
|
408
|
+
- **Consistency**: All providers implement the same interface
|
|
409
|
+
- **Backward Compatible**: Existing code works without changes
|
|
410
|
+
|
|
411
|
+
### Creating Custom Memory Providers
|
|
412
|
+
|
|
413
|
+
To create a custom memory provider, inherit from `BaseMemoryService`:
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
from mdb_engine.memory import BaseMemoryService, MemoryServiceError
|
|
417
|
+
|
|
418
|
+
class CustomMemoryService(BaseMemoryService):
|
|
419
|
+
"""Custom memory service implementation."""
|
|
420
|
+
|
|
421
|
+
def __init__(self, mongo_uri: str, db_name: str, app_slug: str, config: dict | None = None):
|
|
422
|
+
# Initialize your custom implementation
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
def add(self, messages, user_id=None, metadata=None, **kwargs):
|
|
426
|
+
# Implement add method
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
# Implement all other abstract methods...
|
|
430
|
+
def get_all(self, user_id=None, limit=100, filters=None, **kwargs):
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
def search(self, query, user_id=None, limit=5, filters=None, **kwargs):
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
def get(self, memory_id, user_id=None, **kwargs):
|
|
437
|
+
pass
|
|
438
|
+
|
|
439
|
+
def delete(self, memory_id, user_id=None, **kwargs):
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
def delete_all(self, user_id=None, **kwargs):
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
def update(self, memory_id, user_id=None, memory=None, metadata=None, **kwargs):
|
|
446
|
+
pass
|
|
447
|
+
```
|
|
448
|
+
|
|
339
449
|
## API Reference
|
|
340
450
|
|
|
451
|
+
### BaseMemoryService
|
|
452
|
+
|
|
453
|
+
Abstract base class for all memory service implementations. Defines the standard interface.
|
|
454
|
+
|
|
341
455
|
### Mem0MemoryService
|
|
342
456
|
|
|
457
|
+
Default implementation using Mem0.ai. Inherits from `BaseMemoryService`.
|
|
458
|
+
|
|
343
459
|
#### Initialization
|
|
344
460
|
|
|
345
461
|
```python
|
|
346
462
|
Mem0MemoryService(
|
|
347
463
|
mongo_uri: str,
|
|
348
464
|
db_name: str,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
465
|
+
app_slug: str,
|
|
466
|
+
config: dict = None # Optional configuration
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Or use the factory function
|
|
470
|
+
from mdb_engine.memory import get_memory_service
|
|
471
|
+
|
|
472
|
+
memory_service = get_memory_service(
|
|
473
|
+
mongo_uri="...",
|
|
474
|
+
db_name="...",
|
|
475
|
+
app_slug="...",
|
|
476
|
+
config={...},
|
|
477
|
+
provider="mem0" # Default, future providers can be specified here
|
|
358
478
|
)
|
|
359
479
|
```
|
|
360
480
|
|
|
@@ -513,15 +633,19 @@ knowledge = await memory_service.search(
|
|
|
513
633
|
## Error Handling
|
|
514
634
|
|
|
515
635
|
```python
|
|
516
|
-
from mdb_engine.memory import Mem0MemoryServiceError
|
|
636
|
+
from mdb_engine.memory import MemoryServiceError, Mem0MemoryServiceError
|
|
517
637
|
|
|
518
638
|
try:
|
|
519
639
|
memory = await memory_service.add(
|
|
520
640
|
messages=[{"role": "user", "content": "Test"}],
|
|
521
641
|
user_id="user123"
|
|
522
642
|
)
|
|
523
|
-
except
|
|
643
|
+
except MemoryServiceError as e:
|
|
644
|
+
# Base exception for all memory service errors
|
|
524
645
|
print(f"Memory service error: {e}")
|
|
646
|
+
except Mem0MemoryServiceError as e:
|
|
647
|
+
# Specific exception for Mem0 implementation
|
|
648
|
+
print(f"Mem0 memory service error: {e}")
|
|
525
649
|
except (ValueError, TypeError, ConnectionError) as e:
|
|
526
650
|
print(f"Configuration or connection error: {e}")
|
|
527
651
|
```
|
mdb_engine/memory/__init__.py
CHANGED
|
@@ -14,16 +14,25 @@ Key Features:
|
|
|
14
14
|
- **Optional LLM Inference**: Can leverage LLM service for automatic memory
|
|
15
15
|
extraction (set infer: false to disable)
|
|
16
16
|
- **Graph Support**: Optional knowledge graph construction for entity relationships
|
|
17
|
+
- **Extensible Architecture**: Base class allows for future memory provider implementations
|
|
17
18
|
|
|
18
19
|
Dependencies:
|
|
19
20
|
pip install mem0ai
|
|
20
21
|
"""
|
|
21
22
|
|
|
23
|
+
# Import base classes
|
|
24
|
+
from .base import BaseMemoryService, MemoryServiceError
|
|
25
|
+
|
|
22
26
|
# Import service components (mem0 import is lazy within service.py)
|
|
23
27
|
from .service import Mem0MemoryService, Mem0MemoryServiceError, get_memory_service
|
|
24
28
|
|
|
25
29
|
__all__ = [
|
|
30
|
+
# Base classes (for extensibility)
|
|
31
|
+
"BaseMemoryService",
|
|
32
|
+
"MemoryServiceError",
|
|
33
|
+
# Mem0 implementation (default)
|
|
26
34
|
"Mem0MemoryService",
|
|
27
35
|
"Mem0MemoryServiceError",
|
|
36
|
+
# Factory function
|
|
28
37
|
"get_memory_service",
|
|
29
38
|
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Memory Service Interface
|
|
3
|
+
|
|
4
|
+
Abstract base class for memory service implementations.
|
|
5
|
+
This allows for extensibility with different memory providers (Mem0, LangChain, custom, etc.)
|
|
6
|
+
while maintaining a consistent API.
|
|
7
|
+
|
|
8
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MemoryServiceError(Exception):
|
|
16
|
+
"""Base exception for all memory service errors."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseMemoryService(ABC):
|
|
22
|
+
"""
|
|
23
|
+
Abstract base class for memory service implementations.
|
|
24
|
+
|
|
25
|
+
This class defines the interface that all memory service implementations must follow.
|
|
26
|
+
Concrete implementations (e.g., Mem0MemoryService) inherit from this class and
|
|
27
|
+
implement the abstract methods.
|
|
28
|
+
|
|
29
|
+
All memory operations are scoped per user_id for safety and data isolation.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def add(
|
|
34
|
+
self,
|
|
35
|
+
messages: str | list[dict[str, str]],
|
|
36
|
+
user_id: str | None = None,
|
|
37
|
+
metadata: dict[str, Any] | None = None,
|
|
38
|
+
bucket_id: str | None = None,
|
|
39
|
+
bucket_type: str | None = None,
|
|
40
|
+
raw_content: str | None = None,
|
|
41
|
+
**kwargs,
|
|
42
|
+
) -> list[dict[str, Any]]:
|
|
43
|
+
"""
|
|
44
|
+
Add memories with user scoping and metadata convenience.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
messages: Memory content as a string or list of message dicts
|
|
48
|
+
user_id: User ID for scoping (optional but recommended)
|
|
49
|
+
metadata: Additional metadata to store with the memory
|
|
50
|
+
bucket_id: Bucket ID for organizing memories
|
|
51
|
+
bucket_type: Type of bucket (e.g., "general", "file", "conversation")
|
|
52
|
+
raw_content: Raw content to store alongside extracted facts
|
|
53
|
+
**kwargs: Additional provider-specific arguments
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of created memory objects with their IDs and metadata
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_all(
|
|
62
|
+
self,
|
|
63
|
+
user_id: str | None = None,
|
|
64
|
+
limit: int = 100,
|
|
65
|
+
filters: dict[str, Any] | None = None,
|
|
66
|
+
**kwargs,
|
|
67
|
+
) -> list[dict[str, Any]]:
|
|
68
|
+
"""
|
|
69
|
+
Get all memories with optional filtering.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
user_id: User ID to filter memories (optional)
|
|
73
|
+
limit: Maximum number of memories to return
|
|
74
|
+
filters: Additional filters to apply (provider-specific)
|
|
75
|
+
**kwargs: Additional provider-specific arguments
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of memory objects
|
|
79
|
+
"""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def search(
|
|
84
|
+
self,
|
|
85
|
+
query: str,
|
|
86
|
+
user_id: str | None = None,
|
|
87
|
+
limit: int = 5,
|
|
88
|
+
filters: dict[str, Any] | None = None,
|
|
89
|
+
**kwargs,
|
|
90
|
+
) -> list[dict[str, Any]]:
|
|
91
|
+
"""
|
|
92
|
+
Perform semantic search across memories.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
query: Search query string
|
|
96
|
+
user_id: User ID to scope search (optional)
|
|
97
|
+
limit: Maximum number of results to return
|
|
98
|
+
filters: Additional metadata filters to apply
|
|
99
|
+
**kwargs: Additional provider-specific arguments
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of memory objects matching the query, ordered by relevance
|
|
103
|
+
"""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
@abstractmethod
|
|
107
|
+
def get(
|
|
108
|
+
self,
|
|
109
|
+
memory_id: str,
|
|
110
|
+
user_id: str | None = None,
|
|
111
|
+
**kwargs,
|
|
112
|
+
) -> dict[str, Any] | None:
|
|
113
|
+
"""
|
|
114
|
+
Get a single memory by ID.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
memory_id: Unique identifier for the memory
|
|
118
|
+
user_id: User ID for security scoping (optional)
|
|
119
|
+
**kwargs: Additional provider-specific arguments
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Memory object if found, None otherwise
|
|
123
|
+
"""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def delete(
|
|
128
|
+
self,
|
|
129
|
+
memory_id: str,
|
|
130
|
+
user_id: str | None = None,
|
|
131
|
+
**kwargs,
|
|
132
|
+
) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Delete a single memory by ID.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
memory_id: Unique identifier for the memory to delete
|
|
138
|
+
user_id: User ID for security scoping (optional)
|
|
139
|
+
**kwargs: Additional provider-specific arguments
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if deletion was successful, False otherwise
|
|
143
|
+
"""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def delete_all(
|
|
148
|
+
self,
|
|
149
|
+
user_id: str | None = None,
|
|
150
|
+
**kwargs,
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Delete all memories for a user.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
user_id: User ID whose memories should be deleted (optional)
|
|
157
|
+
**kwargs: Additional provider-specific arguments
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if deletion was successful, False otherwise
|
|
161
|
+
"""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def update(
|
|
166
|
+
self,
|
|
167
|
+
memory_id: str,
|
|
168
|
+
user_id: str | None = None,
|
|
169
|
+
memory: str | None = None,
|
|
170
|
+
data: str | dict[str, Any] | None = None,
|
|
171
|
+
messages: str | list[dict[str, str]] | None = None,
|
|
172
|
+
metadata: dict[str, Any] | None = None,
|
|
173
|
+
**kwargs,
|
|
174
|
+
) -> dict[str, Any] | None:
|
|
175
|
+
"""
|
|
176
|
+
Update an existing memory's content and/or metadata.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
memory_id: Unique identifier for the memory to update (required)
|
|
180
|
+
user_id: User ID for security scoping (optional)
|
|
181
|
+
memory: New memory content as a string (optional)
|
|
182
|
+
data: Alternative parameter for content (string or dict) (optional)
|
|
183
|
+
messages: Alternative way to provide content as messages (optional)
|
|
184
|
+
metadata: Metadata updates (optional)
|
|
185
|
+
**kwargs: Additional provider-specific arguments
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Updated memory object if successful, None if memory not found
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
MemoryServiceError: If update operation fails
|
|
192
|
+
ValueError: If memory_id is invalid or empty
|
|
193
|
+
"""
|
|
194
|
+
pass
|
mdb_engine/memory/service.py
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Mem0 Memory Service Implementation
|
|
3
3
|
Production-ready wrapper for Mem0.ai with strict metadata schema for MongoDB.
|
|
4
|
+
|
|
5
|
+
v0.7.4: Enhanced with hybrid update pattern and direct MongoDB access for reliable
|
|
6
|
+
memory operations. Properly handles Mem0's MongoDB structure (_id, payload).
|
|
4
7
|
"""
|
|
5
8
|
|
|
6
9
|
import logging
|
|
7
10
|
import os
|
|
8
11
|
import tempfile
|
|
12
|
+
from datetime import datetime
|
|
9
13
|
from typing import Any
|
|
10
14
|
|
|
15
|
+
from .base import BaseMemoryService, MemoryServiceError
|
|
16
|
+
|
|
17
|
+
# Required: Direct PyMongo access
|
|
18
|
+
try:
|
|
19
|
+
from pymongo import MongoClient
|
|
20
|
+
from pymongo.errors import (
|
|
21
|
+
ConfigurationError,
|
|
22
|
+
ConnectionFailure,
|
|
23
|
+
InvalidURI,
|
|
24
|
+
PyMongoError,
|
|
25
|
+
ServerSelectionTimeoutError,
|
|
26
|
+
)
|
|
27
|
+
except ImportError:
|
|
28
|
+
MongoClient = None
|
|
29
|
+
ConnectionFailure = None
|
|
30
|
+
ConfigurationError = None
|
|
31
|
+
ServerSelectionTimeoutError = None
|
|
32
|
+
InvalidURI = None
|
|
33
|
+
PyMongoError = None
|
|
34
|
+
|
|
11
35
|
# Set MEM0_DIR environment variable early to avoid permission issues
|
|
12
36
|
if "MEM0_DIR" not in os.environ:
|
|
13
37
|
mem0_dir = os.path.join(tempfile.gettempdir(), ".mem0")
|
|
@@ -39,23 +63,29 @@ def _check_mem0_available():
|
|
|
39
63
|
logger = logging.getLogger(__name__)
|
|
40
64
|
|
|
41
65
|
|
|
42
|
-
class Mem0MemoryServiceError(
|
|
66
|
+
class Mem0MemoryServiceError(MemoryServiceError):
|
|
67
|
+
"""Exception raised by Mem0MemoryService operations."""
|
|
68
|
+
|
|
43
69
|
pass
|
|
44
70
|
|
|
45
71
|
|
|
46
|
-
class Mem0MemoryService:
|
|
72
|
+
class Mem0MemoryService(BaseMemoryService):
|
|
47
73
|
"""
|
|
48
74
|
Production-ready Mem0 Memory Service with MongoDB integration.
|
|
49
75
|
|
|
50
76
|
Features:
|
|
77
|
+
- Hybrid update pattern: Mem0 for embeddings, MongoDB for data persistence
|
|
78
|
+
- Full metadata support via direct MongoDB access
|
|
51
79
|
- In-place memory updates preserving IDs and timestamps
|
|
52
80
|
- Automatic embedding recomputation on content changes
|
|
53
81
|
- Knowledge graph support (if enabled in Mem0 config)
|
|
54
82
|
- Comprehensive error handling and logging
|
|
55
|
-
-
|
|
83
|
+
- Reliable return values fetched directly from MongoDB
|
|
56
84
|
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
Update Architecture:
|
|
86
|
+
- Content updates routed via Mem0 (triggers re-embedding)
|
|
87
|
+
- Metadata updates routed via direct PyMongo (full control)
|
|
88
|
+
- Final result always fetched from MongoDB (guaranteed structure)
|
|
59
89
|
"""
|
|
60
90
|
|
|
61
91
|
def __init__(
|
|
@@ -77,6 +107,26 @@ class Mem0MemoryService:
|
|
|
77
107
|
self.collection_name = (config or {}).get("collection_name", f"{app_slug}_memories")
|
|
78
108
|
self.infer = (config or {}).get("infer", True)
|
|
79
109
|
|
|
110
|
+
# ---------------------------------------------------------
|
|
111
|
+
# 1. SETUP DIRECT MONGODB ACCESS (The "Backdoor")
|
|
112
|
+
# ---------------------------------------------------------
|
|
113
|
+
if MongoClient is None:
|
|
114
|
+
raise Mem0MemoryServiceError("pymongo is required. pip install pymongo")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
self._client = MongoClient(mongo_uri)
|
|
118
|
+
self._db = self._client[db_name]
|
|
119
|
+
self.memories_collection = self._db[self.collection_name]
|
|
120
|
+
logger.info(f"✅ Direct MongoDB connection established for {self.collection_name}")
|
|
121
|
+
except BaseException as e:
|
|
122
|
+
# MongoDB connection may raise various exceptions. We catch BaseException
|
|
123
|
+
# (not Exception) to ensure we always raise Mem0MemoryServiceError for
|
|
124
|
+
# consistent error handling, but we re-raise KeyboardInterrupt and SystemExit
|
|
125
|
+
# to allow proper shutdown.
|
|
126
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
127
|
+
raise
|
|
128
|
+
raise Mem0MemoryServiceError(f"Failed to connect to MongoDB directly: {e}") from e
|
|
129
|
+
|
|
80
130
|
# Ensure GOOGLE_API_KEY is set for mem0 compatibility
|
|
81
131
|
# (mem0 expects GOOGLE_API_KEY, not GEMINI_API_KEY)
|
|
82
132
|
# This ensures we use the DIRECT Gemini API
|
|
@@ -437,8 +487,50 @@ class Mem0MemoryService:
|
|
|
437
487
|
return []
|
|
438
488
|
|
|
439
489
|
def get(self, memory_id: str, user_id: str | None = None, **kwargs) -> dict[str, Any]:
|
|
490
|
+
"""
|
|
491
|
+
Get memory by ID using direct MongoDB access for reliability.
|
|
492
|
+
|
|
493
|
+
Mem0 stores memories with _id as the MongoDB document ID.
|
|
494
|
+
Memory content and metadata are stored in the 'payload' field.
|
|
495
|
+
"""
|
|
440
496
|
try:
|
|
441
|
-
|
|
497
|
+
# Mem0 uses _id as the MongoDB document ID
|
|
498
|
+
doc = self.memories_collection.find_one({"_id": memory_id})
|
|
499
|
+
if doc:
|
|
500
|
+
# Extract payload (where Mem0 stores the actual memory data)
|
|
501
|
+
payload = doc.get("payload", {})
|
|
502
|
+
|
|
503
|
+
# Build normalized memory document
|
|
504
|
+
memory_doc = {
|
|
505
|
+
"id": str(doc["_id"]), # Convert _id to id for API consistency
|
|
506
|
+
"memory": payload.get("memory") or payload.get("text"),
|
|
507
|
+
"text": payload.get("text") or payload.get("memory"),
|
|
508
|
+
"metadata": payload.get("metadata", {}),
|
|
509
|
+
"user_id": payload.get("user_id") or payload.get("metadata", {}).get("user_id"),
|
|
510
|
+
"created_at": payload.get("created_at"),
|
|
511
|
+
"updated_at": payload.get("updated_at"),
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Add any other payload fields
|
|
515
|
+
for key, value in payload.items():
|
|
516
|
+
if key not in [
|
|
517
|
+
"memory",
|
|
518
|
+
"text",
|
|
519
|
+
"metadata",
|
|
520
|
+
"user_id",
|
|
521
|
+
"created_at",
|
|
522
|
+
"updated_at",
|
|
523
|
+
]:
|
|
524
|
+
memory_doc[key] = value
|
|
525
|
+
|
|
526
|
+
# Optional: Filter by user_id if provided
|
|
527
|
+
if user_id:
|
|
528
|
+
doc_user_id = memory_doc.get("user_id")
|
|
529
|
+
if doc_user_id and str(doc_user_id) != str(user_id):
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
return memory_doc
|
|
533
|
+
return None
|
|
442
534
|
except (
|
|
443
535
|
ValueError,
|
|
444
536
|
TypeError,
|
|
@@ -448,7 +540,19 @@ class Mem0MemoryService:
|
|
|
448
540
|
RuntimeError,
|
|
449
541
|
KeyError,
|
|
450
542
|
):
|
|
451
|
-
|
|
543
|
+
# Fallback to Mem0 if direct access fails
|
|
544
|
+
try:
|
|
545
|
+
return self.memory.get(memory_id, **kwargs)
|
|
546
|
+
except (
|
|
547
|
+
ValueError,
|
|
548
|
+
TypeError,
|
|
549
|
+
ConnectionError,
|
|
550
|
+
OSError,
|
|
551
|
+
AttributeError,
|
|
552
|
+
RuntimeError,
|
|
553
|
+
KeyError,
|
|
554
|
+
):
|
|
555
|
+
return None
|
|
452
556
|
|
|
453
557
|
def delete(self, memory_id: str, user_id: str | None = None, **kwargs) -> bool:
|
|
454
558
|
try:
|
|
@@ -491,15 +595,27 @@ class Mem0MemoryService:
|
|
|
491
595
|
**kwargs,
|
|
492
596
|
) -> dict[str, Any] | None:
|
|
493
597
|
"""
|
|
494
|
-
|
|
598
|
+
Robust Hybrid Update Pattern:
|
|
599
|
+
|
|
600
|
+
This method uses a hybrid approach that combines Mem0's embedding capabilities
|
|
601
|
+
with direct MongoDB control for maximum flexibility and reliability.
|
|
602
|
+
|
|
603
|
+
**Architecture:**
|
|
604
|
+
1. **Content Updates** → Routed via Mem0 (triggers automatic re-embedding)
|
|
605
|
+
2. **Metadata Updates** → Routed via direct PyMongo (full control, no API limitations)
|
|
606
|
+
3. **Return Value** → Always fetched from MongoDB (guaranteed correct structure)
|
|
607
|
+
|
|
608
|
+
**Why Hybrid?**
|
|
609
|
+
- Mem0's update() API doesn't support metadata parameter
|
|
610
|
+
- Mem0's return values can be inconsistent (dict, list, or status messages)
|
|
611
|
+
- Direct MongoDB access gives us full control over data persistence
|
|
612
|
+
- We use Mem0 purely as an "embedding utility" for content changes
|
|
495
613
|
|
|
496
614
|
Updates the memory content and/or metadata while preserving:
|
|
497
615
|
- Original memory ID (never changes)
|
|
498
616
|
- Creation timestamp (created_at) - preserved
|
|
499
617
|
- Other existing fields - preserved unless explicitly updated
|
|
500
618
|
|
|
501
|
-
If content is updated, the embedding vector is automatically recomputed.
|
|
502
|
-
|
|
503
619
|
Args:
|
|
504
620
|
memory_id: The ID of the memory to update (required)
|
|
505
621
|
user_id: The user ID who owns the memory (for scoping and security)
|
|
@@ -508,12 +624,13 @@ class Mem0MemoryService:
|
|
|
508
624
|
Can be a string or dict with 'memory'/'text'/'content' key.
|
|
509
625
|
messages: Alternative way to provide content as messages (optional).
|
|
510
626
|
Can be a string or list of dicts with 'content' key.
|
|
511
|
-
metadata: Metadata updates (
|
|
512
|
-
|
|
627
|
+
metadata: Metadata updates (FULLY SUPPORTED via direct MongoDB).
|
|
628
|
+
Can update any metadata field, not limited by Mem0 API.
|
|
513
629
|
**kwargs: Additional arguments passed to Mem0 operations
|
|
514
630
|
|
|
515
631
|
Returns:
|
|
516
|
-
Updated memory object with same ID,
|
|
632
|
+
Updated memory object with same ID, fetched directly from MongoDB,
|
|
633
|
+
or None if memory not found
|
|
517
634
|
|
|
518
635
|
Raises:
|
|
519
636
|
Mem0MemoryServiceError: If update operation fails
|
|
@@ -521,7 +638,7 @@ class Mem0MemoryService:
|
|
|
521
638
|
|
|
522
639
|
Example:
|
|
523
640
|
```python
|
|
524
|
-
# Update content and metadata
|
|
641
|
+
# Update content and metadata (hybrid approach)
|
|
525
642
|
updated = memory_service.update(
|
|
526
643
|
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
527
644
|
user_id="user123",
|
|
@@ -529,78 +646,164 @@ class Mem0MemoryService:
|
|
|
529
646
|
metadata={"category": "technical", "updated": True}
|
|
530
647
|
)
|
|
531
648
|
|
|
532
|
-
# Update only metadata (content unchanged)
|
|
649
|
+
# Update only metadata (content unchanged) - FULLY SUPPORTED
|
|
533
650
|
updated = memory_service.update(
|
|
534
651
|
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
535
652
|
user_id="user123",
|
|
536
|
-
metadata={"category": "updated"}
|
|
653
|
+
metadata={"category": "updated", "priority": "high"}
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Update only content (no metadata)
|
|
657
|
+
updated = memory_service.update(
|
|
658
|
+
memory_id="04f78986-dfad-46fe-8381-034bbee9a2fc",
|
|
659
|
+
user_id="user123",
|
|
660
|
+
memory="Updated content only"
|
|
537
661
|
)
|
|
538
662
|
```
|
|
539
663
|
"""
|
|
540
664
|
if not memory_id or not isinstance(memory_id, str) or not memory_id.strip():
|
|
541
665
|
raise ValueError("memory_id is required and must be a non-empty string")
|
|
542
666
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
normalized_metadata = self._normalize_metadata_input(metadata, data)
|
|
667
|
+
# 1. Normalize Inputs
|
|
668
|
+
normalized_memory = self._normalize_content_input(memory, data, messages)
|
|
669
|
+
normalized_metadata = self._normalize_metadata_input(metadata, data)
|
|
547
670
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
671
|
+
# 2. Check Existence (Fast check via ID)
|
|
672
|
+
# Mem0 uses _id as the MongoDB document ID
|
|
673
|
+
existing = self.memories_collection.find_one(
|
|
674
|
+
{"_id": memory_id}, {"_id": 1, "payload.user_id": 1, "payload.metadata.user_id": 1}
|
|
675
|
+
)
|
|
676
|
+
if not existing:
|
|
677
|
+
logger.warning(f"Memory {memory_id} not found.")
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
# Optional: Security Scope Check
|
|
681
|
+
# Check user_id in payload (Mem0 stores it there)
|
|
682
|
+
if user_id:
|
|
683
|
+
payload = existing.get("payload", {})
|
|
684
|
+
existing_user_id = payload.get("user_id") or payload.get("metadata", {}).get("user_id")
|
|
685
|
+
if existing_user_id and str(existing_user_id) != str(user_id):
|
|
686
|
+
logger.warning(f"Unauthorized update attempt for {memory_id} by {user_id}")
|
|
554
687
|
return None
|
|
555
688
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
# - In-place updates preserving memory ID
|
|
559
|
-
# - Automatic embedding recomputation
|
|
560
|
-
# - Metadata merging
|
|
561
|
-
# - User scoping
|
|
562
|
-
# - Knowledge graph updates (if enabled)
|
|
563
|
-
# - Relationship management
|
|
564
|
-
if not hasattr(self.memory, "update") or not callable(self.memory.update):
|
|
565
|
-
raise Mem0MemoryServiceError(
|
|
566
|
-
"Mem0 update method not available. "
|
|
567
|
-
"Please ensure you're using a compatible version of mem0ai "
|
|
568
|
-
"that supports updates. Install with: pip install --upgrade mem0ai"
|
|
569
|
-
)
|
|
689
|
+
# Use _id directly (Mem0's format)
|
|
690
|
+
actual_id = memory_id
|
|
570
691
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
692
|
+
try:
|
|
693
|
+
# -------------------------------------------------
|
|
694
|
+
# STEP A: Content Update (Via Mem0 for Vectors)
|
|
695
|
+
# -------------------------------------------------
|
|
696
|
+
if normalized_memory:
|
|
697
|
+
logger.info(f"📝 Updating content for {actual_id} (triggering re-embedding)")
|
|
698
|
+
# We use Mem0 here specifically because it handles the embedding logic.
|
|
699
|
+
# We do NOT care what it returns.
|
|
700
|
+
try:
|
|
701
|
+
# Use the actual_id (which Mem0 recognizes)
|
|
702
|
+
self.memory.update(memory_id=actual_id, data=normalized_memory)
|
|
703
|
+
except BaseException as e:
|
|
704
|
+
# Mem0 is a third-party library that may raise any exception.
|
|
705
|
+
# We catch BaseException (not Exception) to ensure we always raise
|
|
706
|
+
# Mem0MemoryServiceError for consistent error handling, but we
|
|
707
|
+
# re-raise KeyboardInterrupt and SystemExit to allow proper shutdown.
|
|
708
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
709
|
+
raise
|
|
710
|
+
# If Mem0 fails (e.g. LLM rate limit, API error), we should abort
|
|
711
|
+
# or fall back to just text update without vector (risky for search).
|
|
712
|
+
logger.exception(f"Mem0 embedding update failed: {e}")
|
|
713
|
+
raise Mem0MemoryServiceError(f"Content update failed: {e}") from e
|
|
714
|
+
|
|
715
|
+
# -------------------------------------------------
|
|
716
|
+
# STEP B: Metadata Update (Direct PyMongo)
|
|
717
|
+
# -------------------------------------------------
|
|
718
|
+
if normalized_metadata:
|
|
719
|
+
logger.info(f"🏷️ Updating metadata for {actual_id}")
|
|
720
|
+
|
|
721
|
+
# Ensure user_id is in metadata for consistency
|
|
722
|
+
if user_id:
|
|
723
|
+
normalized_metadata["user_id"] = str(user_id)
|
|
724
|
+
|
|
725
|
+
# Mem0 stores everything in the 'payload' field
|
|
726
|
+
# We need to update payload.metadata and payload.user_id
|
|
727
|
+
update_fields = {}
|
|
728
|
+
|
|
729
|
+
# Handle metadata updates (nested under payload.metadata)
|
|
730
|
+
for k, v in normalized_metadata.items():
|
|
731
|
+
if k == "user_id":
|
|
732
|
+
# user_id can be at payload.user_id or payload.metadata.user_id
|
|
733
|
+
update_fields["payload.user_id"] = v
|
|
734
|
+
update_fields["payload.metadata.user_id"] = v
|
|
735
|
+
else:
|
|
736
|
+
update_fields[f"payload.metadata.{k}"] = v
|
|
737
|
+
|
|
738
|
+
# Add timestamp to payload
|
|
739
|
+
update_fields["payload.updated_at"] = datetime.utcnow().isoformat()
|
|
740
|
+
|
|
741
|
+
# Execute Atomic Update - Mem0 uses _id
|
|
742
|
+
self.memories_collection.update_one({"_id": actual_id}, {"$set": update_fields})
|
|
743
|
+
|
|
744
|
+
# -------------------------------------------------
|
|
745
|
+
# STEP C: Return The Truth (Direct DB Fetch)
|
|
746
|
+
# -------------------------------------------------
|
|
747
|
+
# We completely ignore Mem0's return value (which might be {"message": "ok"})
|
|
748
|
+
# and fetch the actual document from the database.
|
|
749
|
+
final_doc_raw = self.memories_collection.find_one({"_id": actual_id})
|
|
750
|
+
|
|
751
|
+
if final_doc_raw:
|
|
752
|
+
# Extract payload (where Mem0 stores the actual memory data)
|
|
753
|
+
payload = final_doc_raw.get("payload", {})
|
|
754
|
+
|
|
755
|
+
# Build normalized memory document (same format as get() method)
|
|
756
|
+
final_doc = {
|
|
757
|
+
"id": str(final_doc_raw["_id"]), # Convert _id to id for API consistency
|
|
758
|
+
"memory": payload.get("memory") or payload.get("text"),
|
|
759
|
+
"text": payload.get("text") or payload.get("memory"),
|
|
760
|
+
"metadata": payload.get("metadata", {}),
|
|
761
|
+
"user_id": payload.get("user_id") or payload.get("metadata", {}).get("user_id"),
|
|
762
|
+
"created_at": payload.get("created_at"),
|
|
763
|
+
"updated_at": payload.get("updated_at"),
|
|
764
|
+
}
|
|
578
765
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
766
|
+
# Add any other payload fields
|
|
767
|
+
for key, value in payload.items():
|
|
768
|
+
if key not in [
|
|
769
|
+
"memory",
|
|
770
|
+
"text",
|
|
771
|
+
"metadata",
|
|
772
|
+
"user_id",
|
|
773
|
+
"created_at",
|
|
774
|
+
"updated_at",
|
|
775
|
+
]:
|
|
776
|
+
final_doc[key] = value
|
|
777
|
+
|
|
778
|
+
# Ensure date objects are serialized if your downstream expects strings
|
|
779
|
+
if isinstance(final_doc.get("created_at"), datetime):
|
|
780
|
+
final_doc["created_at"] = final_doc["created_at"].isoformat()
|
|
781
|
+
if isinstance(final_doc.get("updated_at"), datetime):
|
|
782
|
+
final_doc["updated_at"] = final_doc["updated_at"].isoformat()
|
|
783
|
+
else:
|
|
784
|
+
final_doc = None
|
|
585
785
|
|
|
586
786
|
logger.info(
|
|
587
|
-
f"Successfully updated memory {memory_id}
|
|
787
|
+
f"Successfully updated memory {memory_id}",
|
|
588
788
|
extra={
|
|
589
789
|
"memory_id": memory_id,
|
|
590
790
|
"content_updated": bool(normalized_memory),
|
|
591
791
|
"metadata_updated": bool(normalized_metadata),
|
|
592
792
|
},
|
|
593
793
|
)
|
|
594
|
-
return
|
|
794
|
+
return final_doc
|
|
595
795
|
|
|
596
796
|
except ValueError:
|
|
597
797
|
# Re-raise validation errors as-is
|
|
598
798
|
raise
|
|
599
|
-
except
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
799
|
+
except BaseException as e:
|
|
800
|
+
# Catch any unexpected errors during update. We catch BaseException
|
|
801
|
+
# (not Exception) to ensure we always raise Mem0MemoryServiceError for
|
|
802
|
+
# consistent error handling, but we re-raise KeyboardInterrupt and SystemExit
|
|
803
|
+
# to allow proper shutdown.
|
|
804
|
+
if isinstance(e, KeyboardInterrupt | SystemExit):
|
|
805
|
+
raise
|
|
806
|
+
logger.exception(f"Critical error during memory update for {memory_id}")
|
|
604
807
|
raise Mem0MemoryServiceError(f"Update failed: {e}") from e
|
|
605
808
|
|
|
606
809
|
def _normalize_content_input(
|
|
@@ -609,122 +812,45 @@ class Mem0MemoryService:
|
|
|
609
812
|
data: str | dict[str, Any] | None,
|
|
610
813
|
messages: str | list[dict[str, str]] | None,
|
|
611
814
|
) -> str | None:
|
|
612
|
-
"""
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
Priority: memory > data > messages
|
|
616
|
-
"""
|
|
617
|
-
# Already have memory content
|
|
618
|
-
if memory:
|
|
815
|
+
"""Normalize content input from various parameter formats."""
|
|
816
|
+
if memory is not None:
|
|
619
817
|
if not isinstance(memory, str):
|
|
620
|
-
raise TypeError("memory parameter must be a string")
|
|
818
|
+
raise TypeError(f"memory parameter must be a string, got {type(memory).__name__}")
|
|
621
819
|
return memory.strip() if memory.strip() else None
|
|
622
|
-
|
|
623
|
-
# Check data parameter
|
|
624
|
-
if data:
|
|
820
|
+
if data is not None:
|
|
625
821
|
if isinstance(data, str):
|
|
626
822
|
return data.strip() if data.strip() else None
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
# Check messages parameter
|
|
633
|
-
if messages:
|
|
823
|
+
if isinstance(data, dict):
|
|
824
|
+
return data.get("memory") or data.get("text") or data.get("content")
|
|
825
|
+
raise TypeError(f"data parameter must be a string or dict, got {type(data).__name__}")
|
|
826
|
+
if messages is not None:
|
|
634
827
|
if isinstance(messages, str):
|
|
635
828
|
return messages.strip() if messages.strip() else None
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if isinstance(content, str) and content.strip():
|
|
642
|
-
content_parts.append(content.strip())
|
|
643
|
-
if content_parts:
|
|
644
|
-
return " ".join(content_parts)
|
|
645
|
-
|
|
829
|
+
if isinstance(messages, list):
|
|
830
|
+
return " ".join([m.get("content", "") for m in messages if isinstance(m, dict)])
|
|
831
|
+
raise TypeError(
|
|
832
|
+
f"messages parameter must be a string or list, got {type(messages).__name__}"
|
|
833
|
+
)
|
|
646
834
|
return None
|
|
647
835
|
|
|
648
836
|
def _normalize_metadata_input(
|
|
649
837
|
self, metadata: dict[str, Any] | None, data: dict[str, Any] | None
|
|
650
838
|
) -> dict[str, Any] | None:
|
|
651
839
|
"""Normalize metadata input, extracting from data dict if needed."""
|
|
652
|
-
if metadata is not None and not isinstance(metadata, dict):
|
|
653
|
-
raise TypeError("metadata must be a dict or None")
|
|
654
|
-
|
|
655
|
-
# If metadata provided directly, use it
|
|
656
840
|
if metadata is not None:
|
|
841
|
+
if not isinstance(metadata, dict):
|
|
842
|
+
raise TypeError(f"metadata parameter must be a dict, got {type(metadata).__name__}")
|
|
657
843
|
return metadata
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
844
|
+
if data is not None and isinstance(data, dict):
|
|
845
|
+
metadata_from_data = data.get("metadata")
|
|
846
|
+
if metadata_from_data is not None and not isinstance(metadata_from_data, dict):
|
|
847
|
+
raise TypeError(
|
|
848
|
+
f"metadata in data parameter must be a dict, "
|
|
849
|
+
f"got {type(metadata_from_data).__name__}"
|
|
850
|
+
)
|
|
851
|
+
return metadata_from_data
|
|
665
852
|
return None
|
|
666
853
|
|
|
667
|
-
def _update_via_mem0(
|
|
668
|
-
self,
|
|
669
|
-
memory_id: str,
|
|
670
|
-
user_id: str | None,
|
|
671
|
-
memory: str | None,
|
|
672
|
-
metadata: dict[str, Any] | None,
|
|
673
|
-
**kwargs,
|
|
674
|
-
) -> dict[str, Any] | None:
|
|
675
|
-
"""
|
|
676
|
-
Update memory using Mem0's built-in update method.
|
|
677
|
-
|
|
678
|
-
Note: Mem0's update() only supports content updates (text parameter).
|
|
679
|
-
Metadata updates are not supported by the Mem0 API.
|
|
680
|
-
|
|
681
|
-
Args:
|
|
682
|
-
memory_id: Memory ID to update
|
|
683
|
-
user_id: User ID for scoping (not used in update call)
|
|
684
|
-
memory: New memory content (normalized)
|
|
685
|
-
metadata: Metadata to merge (ignored - not supported by Mem0 update API)
|
|
686
|
-
**kwargs: Additional arguments passed to Mem0
|
|
687
|
-
|
|
688
|
-
Returns:
|
|
689
|
-
Updated memory dict or None if not found
|
|
690
|
-
"""
|
|
691
|
-
# Filter out user_id from kwargs to prevent passing it as a direct parameter
|
|
692
|
-
filtered_kwargs = {k: v for k, v in kwargs.items() if k != "user_id"}
|
|
693
|
-
|
|
694
|
-
logger.debug(
|
|
695
|
-
f"Calling mem0.update() for memory_id={memory_id}",
|
|
696
|
-
extra={
|
|
697
|
-
"memory_id": memory_id,
|
|
698
|
-
"has_content": bool(memory),
|
|
699
|
-
"has_metadata": bool(metadata),
|
|
700
|
-
"user_id": user_id,
|
|
701
|
-
},
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
if metadata:
|
|
705
|
-
logger.warning(
|
|
706
|
-
f"Metadata update requested for memory {memory_id} but Mem0 update() "
|
|
707
|
-
f"does not support metadata parameter. Metadata will not be updated."
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
update_kwargs = {"memory_id": memory_id}
|
|
711
|
-
if memory:
|
|
712
|
-
update_kwargs["data"] = memory
|
|
713
|
-
update_kwargs.update(filtered_kwargs)
|
|
714
|
-
|
|
715
|
-
result = self.memory.update(**update_kwargs)
|
|
716
|
-
|
|
717
|
-
# Note: Mem0's update() doesn't support metadata parameter
|
|
718
|
-
# Metadata updates would require direct MongoDB access, which we avoid
|
|
719
|
-
|
|
720
|
-
# Normalize result to dict
|
|
721
|
-
if isinstance(result, dict):
|
|
722
|
-
return result
|
|
723
|
-
elif isinstance(result, list) and len(result) > 0:
|
|
724
|
-
return result[0] if isinstance(result[0], dict) else None
|
|
725
|
-
else:
|
|
726
|
-
return self.get(memory_id=memory_id)
|
|
727
|
-
|
|
728
854
|
def _normalize_result(self, result: Any) -> list[dict[str, Any]]:
|
|
729
855
|
"""Normalize Mem0's return type (dict vs list)."""
|
|
730
856
|
if result is None:
|
|
@@ -740,5 +866,35 @@ class Mem0MemoryService:
|
|
|
740
866
|
return []
|
|
741
867
|
|
|
742
868
|
|
|
743
|
-
def get_memory_service(
|
|
744
|
-
|
|
869
|
+
def get_memory_service(
|
|
870
|
+
mongo_uri: str,
|
|
871
|
+
db_name: str,
|
|
872
|
+
app_slug: str,
|
|
873
|
+
config: dict[str, Any] | None = None,
|
|
874
|
+
provider: str = "mem0",
|
|
875
|
+
) -> BaseMemoryService:
|
|
876
|
+
"""
|
|
877
|
+
Factory function to create a memory service instance.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
mongo_uri: MongoDB connection URI
|
|
881
|
+
db_name: Database name
|
|
882
|
+
app_slug: Application slug for scoping
|
|
883
|
+
config: Memory service configuration dictionary
|
|
884
|
+
provider: Memory provider to use (default: "mem0")
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
BaseMemoryService instance (concrete implementation based on provider)
|
|
888
|
+
|
|
889
|
+
Raises:
|
|
890
|
+
ValueError: If provider is not supported
|
|
891
|
+
Mem0MemoryServiceError: If Mem0 provider fails to initialize
|
|
892
|
+
"""
|
|
893
|
+
if provider == "mem0":
|
|
894
|
+
return Mem0MemoryService(mongo_uri, db_name, app_slug, config)
|
|
895
|
+
else:
|
|
896
|
+
raise ValueError(
|
|
897
|
+
f"Unsupported memory provider: {provider}. "
|
|
898
|
+
f"Supported providers: mem0. "
|
|
899
|
+
f"Future providers can be added by implementing BaseMemoryService."
|
|
900
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
mdb_engine/README.md,sha256=T3EFGcPopY9LslYW3lxgG3hohWkAOmBNbYG0FDMUJiY,3502
|
|
2
|
-
mdb_engine/__init__.py,sha256=
|
|
2
|
+
mdb_engine/__init__.py,sha256=OkNHx17I4f8jI5IAJGK0HbSTrZrUVnl1QlvtPZLri-I,3097
|
|
3
3
|
mdb_engine/config.py,sha256=DTAyxfKB8ogyI0v5QR9Y-SJOgXQr_eDBCKxNBSqEyLc,7269
|
|
4
4
|
mdb_engine/constants.py,sha256=eaotvW57TVOg7rRbLziGrVNoP7adgw_G9iVByHezc_A,7837
|
|
5
|
-
mdb_engine/dependencies.py,sha256=
|
|
5
|
+
mdb_engine/dependencies.py,sha256=F2_hbtK-lyeqB_iPeJVCh8jFbHi7sx0fduCUCNCKoUc,14128
|
|
6
6
|
mdb_engine/exceptions.py,sha256=NkBSdTBNP9bVTtI6w6mAoEfeVZDq-Bg7vCF2L26ZDZo,8442
|
|
7
7
|
mdb_engine/auth/ARCHITECTURE.md,sha256=JXZsjEIpNz4Szk18KaOiEabhEuz1pmYWXQRN-EJEHDM,4076
|
|
8
8
|
mdb_engine/auth/README.md,sha256=IVlUOat96V3yM6wk4T-u4GxJ4-WF05eBSJBlvIexyBo,37285
|
|
@@ -35,7 +35,7 @@ mdb_engine/auth/utils.py,sha256=YkexCo0xV37mpOJUI32cntRHVOUUS7r19TIMPWHcgpA,2734
|
|
|
35
35
|
mdb_engine/auth/websocket_sessions.py,sha256=7eFNagY2K3Rp1x7d_cO5JcpT-DrYkc__cmVhl6pAC2M,15081
|
|
36
36
|
mdb_engine/auth/websocket_tickets.py,sha256=VoIArcnQBtYqXRMs-5m7NSvCJB1dEeHrLl7j7yG-H-A,9887
|
|
37
37
|
mdb_engine/cli/__init__.py,sha256=PANRi4THmL34d1mawlqxIrnuItXMdqoMTq5Z1zHd7rM,301
|
|
38
|
-
mdb_engine/cli/main.py,sha256=
|
|
38
|
+
mdb_engine/cli/main.py,sha256=4etDTeg4DIPQQ5HdWIwcpg9FWLx21NXF6f2fj4xG8gQ,811
|
|
39
39
|
mdb_engine/cli/utils.py,sha256=bNRGJgdzxUjXAOVe1aoxWJ5M_IqtAE-eW4pfAkwiDDM,2760
|
|
40
40
|
mdb_engine/cli/commands/__init__.py,sha256=ZSzMhKdV9ILD5EbOSxDV9nURHo1e4bQ0c8AWpqsTEqM,115
|
|
41
41
|
mdb_engine/cli/commands/generate.py,sha256=VEcn7qNQkIPQlLEK3oXUBgYMwD-G0FyomXQcWTtKsbs,17304
|
|
@@ -48,13 +48,13 @@ mdb_engine/core/app_registration.py,sha256=w7wjrlNQsEekuDQkG6HM4Z5t00E65baEzncnh
|
|
|
48
48
|
mdb_engine/core/app_secrets.py,sha256=bo-syg9UUATibNyXEZs-0TTYWG-JaY-2S0yNSGA12n0,10524
|
|
49
49
|
mdb_engine/core/connection.py,sha256=XnwuPG34pJ7kJGJ84T0mhj1UZ6_CLz_9qZf6NRYGIS8,8346
|
|
50
50
|
mdb_engine/core/encryption.py,sha256=RZ5LPF5g28E3ZBn6v1IMw_oas7u9YGFtBcEj8lTi9LM,7515
|
|
51
|
-
mdb_engine/core/engine.py,sha256=
|
|
51
|
+
mdb_engine/core/engine.py,sha256=tzR5Gh5wTv9ACPYF7Z7aaAUInbF0l0Dtj2HrMB4VFes,185326
|
|
52
52
|
mdb_engine/core/index_management.py,sha256=9-r7MIy3JnjQ35sGqsbj8K_I07vAUWtAVgSWC99lJcE,5555
|
|
53
53
|
mdb_engine/core/manifest.py,sha256=D3OGRjm1It8dmS3IMoxHckHUerhs5PtcQgPz8o5ZL9w,140785
|
|
54
54
|
mdb_engine/core/ray_integration.py,sha256=csAgICl2g7Plqy4N49MJU1Ca-lr8KZznAouXMMRwhG8,13706
|
|
55
55
|
mdb_engine/core/seeding.py,sha256=c5IhdwlqUf_4Q5FFTAhPLaHPaUr_Txo3z_DUwZmWsFs,6421
|
|
56
|
-
mdb_engine/core/service_initialization.py,sha256=
|
|
57
|
-
mdb_engine/core/types.py,sha256=
|
|
56
|
+
mdb_engine/core/service_initialization.py,sha256=LVCbc911YA07cTdy4UbPC7eho3L_Ub_8aX-HIp4MHq4,15145
|
|
57
|
+
mdb_engine/core/types.py,sha256=MAFAqIR-Oj8CF24jlVnOQevrlXOcME3npIgRAzfZXcA,11254
|
|
58
58
|
mdb_engine/database/README.md,sha256=-31mVxBeVQaYsF3AD1-gQbD2NCYVcPjdFoA6sZ6b02Y,19354
|
|
59
59
|
mdb_engine/database/__init__.py,sha256=rrc3eZFli3K2zrvVdDbMBi8YkmoHYzP6JNT0AUBE5VU,981
|
|
60
60
|
mdb_engine/database/abstraction.py,sha256=H6f2WYY80r3onqN6s139uDSyG9W_QpadaoQ84hJuG1E,23438
|
|
@@ -74,9 +74,10 @@ mdb_engine/indexes/README.md,sha256=r7duq-1vtqHhBk1cwoBMYYh_dfTzxiaQaPE3mLB_3JQ,
|
|
|
74
74
|
mdb_engine/indexes/__init__.py,sha256=9QFJ6qo_yD26dZcKyKjj-hhesFpaomBt-mWTtYTQvqc,613
|
|
75
75
|
mdb_engine/indexes/helpers.py,sha256=tJHqDm18jKLrW-P2ofmnUnaf4_-V5xDLjqP_WU8W9MM,4190
|
|
76
76
|
mdb_engine/indexes/manager.py,sha256=ekrYBfKD-GOBpZMjXIZWSQ7UPLgrD5BINYzVteWkSI8,32508
|
|
77
|
-
mdb_engine/memory/README.md,sha256=
|
|
78
|
-
mdb_engine/memory/__init__.py,sha256=
|
|
79
|
-
mdb_engine/memory/
|
|
77
|
+
mdb_engine/memory/README.md,sha256=3NqI-Sm8Cy-4lsU1wAWxSg92XpRFK19OfDU_dLo3go0,20581
|
|
78
|
+
mdb_engine/memory/__init__.py,sha256=VEB8PuNBB6q1mSlIYXv9xWt14YmxdiSV5D9hrgmMW94,1328
|
|
79
|
+
mdb_engine/memory/base.py,sha256=bI1Rl14mEAsRSMKe8_3jxzrs-N8LGdYWPwXXRziRDLs,5759
|
|
80
|
+
mdb_engine/memory/service.py,sha256=9uReFG5FaTIs7Fl6Kz7sXES4Uj_4ao7bgcfs32Nvd9U,37164
|
|
80
81
|
mdb_engine/observability/README.md,sha256=CMgQaC1H8ESmCitfbhJifz6-XoXH_FPNE4MvuZ-oFas,13085
|
|
81
82
|
mdb_engine/observability/__init__.py,sha256=jjLsrW6Gy2ayrbfLrgHsDB61NxWWkYLHwv0q-N3fxjA,1213
|
|
82
83
|
mdb_engine/observability/health.py,sha256=ORjxF_rHYA9vCoxUqel5p0n9g3PLmHsHQn68Q402b6g,9212
|
|
@@ -91,9 +92,9 @@ mdb_engine/routing/__init__.py,sha256=reupjHi_RTc2ZBA4AH5XzobAmqy4EQIsfSUcTkFknU
|
|
|
91
92
|
mdb_engine/routing/websockets.py,sha256=w0m1XMpXKPpPrEBcZNIj8B8PwWeTNrseUZE1P8cipdM,53441
|
|
92
93
|
mdb_engine/utils/__init__.py,sha256=lDxQSGqkV4fVw5TWIk6FA6_eey_ZnEtMY0fir3cpAe8,236
|
|
93
94
|
mdb_engine/utils/mongo.py,sha256=Oqtv4tQdpiiZzrilGLEYQPo8Vmh8WsTQypxQs8Of53s,3369
|
|
94
|
-
mdb_engine-0.7.
|
|
95
|
-
mdb_engine-0.7.
|
|
96
|
-
mdb_engine-0.7.
|
|
97
|
-
mdb_engine-0.7.
|
|
98
|
-
mdb_engine-0.7.
|
|
99
|
-
mdb_engine-0.7.
|
|
95
|
+
mdb_engine-0.7.4.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
96
|
+
mdb_engine-0.7.4.dist-info/METADATA,sha256=T67AUeMaWC1ybxcQRAbPlYgKv9S5yWOJpWsIGCJVyrI,18843
|
|
97
|
+
mdb_engine-0.7.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
98
|
+
mdb_engine-0.7.4.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
|
|
99
|
+
mdb_engine-0.7.4.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
|
|
100
|
+
mdb_engine-0.7.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|