mdb-engine 0.1.6__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/README.md +144 -0
- mdb_engine/__init__.py +37 -0
- mdb_engine/auth/README.md +631 -0
- mdb_engine/auth/__init__.py +128 -0
- mdb_engine/auth/casbin_factory.py +199 -0
- mdb_engine/auth/casbin_models.py +46 -0
- mdb_engine/auth/config_defaults.py +71 -0
- mdb_engine/auth/config_helpers.py +213 -0
- mdb_engine/auth/cookie_utils.py +158 -0
- mdb_engine/auth/decorators.py +350 -0
- mdb_engine/auth/dependencies.py +747 -0
- mdb_engine/auth/helpers.py +64 -0
- mdb_engine/auth/integration.py +578 -0
- mdb_engine/auth/jwt.py +225 -0
- mdb_engine/auth/middleware.py +241 -0
- mdb_engine/auth/oso_factory.py +323 -0
- mdb_engine/auth/provider.py +570 -0
- mdb_engine/auth/restrictions.py +271 -0
- mdb_engine/auth/session_manager.py +477 -0
- mdb_engine/auth/token_lifecycle.py +213 -0
- mdb_engine/auth/token_store.py +289 -0
- mdb_engine/auth/users.py +1516 -0
- mdb_engine/auth/utils.py +614 -0
- mdb_engine/cli/__init__.py +13 -0
- mdb_engine/cli/commands/__init__.py +7 -0
- mdb_engine/cli/commands/generate.py +105 -0
- mdb_engine/cli/commands/migrate.py +83 -0
- mdb_engine/cli/commands/show.py +70 -0
- mdb_engine/cli/commands/validate.py +63 -0
- mdb_engine/cli/main.py +41 -0
- mdb_engine/cli/utils.py +92 -0
- mdb_engine/config.py +217 -0
- mdb_engine/constants.py +160 -0
- mdb_engine/core/README.md +542 -0
- mdb_engine/core/__init__.py +42 -0
- mdb_engine/core/app_registration.py +392 -0
- mdb_engine/core/connection.py +243 -0
- mdb_engine/core/engine.py +749 -0
- mdb_engine/core/index_management.py +162 -0
- mdb_engine/core/manifest.py +2793 -0
- mdb_engine/core/seeding.py +179 -0
- mdb_engine/core/service_initialization.py +355 -0
- mdb_engine/core/types.py +413 -0
- mdb_engine/database/README.md +522 -0
- mdb_engine/database/__init__.py +31 -0
- mdb_engine/database/abstraction.py +635 -0
- mdb_engine/database/connection.py +387 -0
- mdb_engine/database/scoped_wrapper.py +1721 -0
- mdb_engine/embeddings/README.md +184 -0
- mdb_engine/embeddings/__init__.py +62 -0
- mdb_engine/embeddings/dependencies.py +193 -0
- mdb_engine/embeddings/service.py +759 -0
- mdb_engine/exceptions.py +167 -0
- mdb_engine/indexes/README.md +651 -0
- mdb_engine/indexes/__init__.py +21 -0
- mdb_engine/indexes/helpers.py +145 -0
- mdb_engine/indexes/manager.py +895 -0
- mdb_engine/memory/README.md +451 -0
- mdb_engine/memory/__init__.py +30 -0
- mdb_engine/memory/service.py +1285 -0
- mdb_engine/observability/README.md +515 -0
- mdb_engine/observability/__init__.py +42 -0
- mdb_engine/observability/health.py +296 -0
- mdb_engine/observability/logging.py +161 -0
- mdb_engine/observability/metrics.py +297 -0
- mdb_engine/routing/README.md +462 -0
- mdb_engine/routing/__init__.py +73 -0
- mdb_engine/routing/websockets.py +813 -0
- mdb_engine/utils/__init__.py +7 -0
- mdb_engine-0.1.6.dist-info/METADATA +213 -0
- mdb_engine-0.1.6.dist-info/RECORD +75 -0
- mdb_engine-0.1.6.dist-info/WHEEL +5 -0
- mdb_engine-0.1.6.dist-info/entry_points.txt +2 -0
- mdb_engine-0.1.6.dist-info/licenses/LICENSE +661 -0
- mdb_engine-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Initial Data Seeding
|
|
3
|
+
|
|
4
|
+
Provides utilities for seeding initial data into collections based on manifest configuration.
|
|
5
|
+
|
|
6
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, Dict, List
|
|
12
|
+
|
|
13
|
+
from pymongo.errors import (ConnectionFailure, OperationFailure,
|
|
14
|
+
ServerSelectionTimeoutError)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def seed_initial_data(
|
|
20
|
+
db, app_slug: str, initial_data: Dict[str, List[Dict[str, Any]]]
|
|
21
|
+
) -> Dict[str, int]:
|
|
22
|
+
"""
|
|
23
|
+
Seed initial data into collections.
|
|
24
|
+
|
|
25
|
+
This function:
|
|
26
|
+
1. Checks if each collection is empty
|
|
27
|
+
2. Only seeds if collection is empty (idempotent)
|
|
28
|
+
3. Tracks seeded collections in metadata collection
|
|
29
|
+
4. Handles datetime conversion for seed data
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
db: Database wrapper (ScopedMongoWrapper or AppDB)
|
|
33
|
+
app_slug: App slug identifier
|
|
34
|
+
initial_data: Dictionary mapping collection names to arrays of documents
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dictionary mapping collection names to number of documents inserted
|
|
38
|
+
"""
|
|
39
|
+
results = {}
|
|
40
|
+
|
|
41
|
+
# Check metadata collection to see if we've already seeded
|
|
42
|
+
# Use a non-underscore name to avoid ScopedMongoWrapper attribute access restrictions
|
|
43
|
+
metadata_collection_name = "app_seeding_metadata"
|
|
44
|
+
# Use getattr to access collection (works with ScopedMongoWrapper and AppDB)
|
|
45
|
+
metadata_collection = getattr(db, metadata_collection_name)
|
|
46
|
+
|
|
47
|
+
# Get existing seeding metadata
|
|
48
|
+
seeding_metadata = await metadata_collection.find_one({"app_slug": app_slug})
|
|
49
|
+
seeded_collections = (
|
|
50
|
+
set(seeding_metadata.get("seeded_collections", []))
|
|
51
|
+
if seeding_metadata
|
|
52
|
+
else set()
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for collection_name, documents in initial_data.items():
|
|
56
|
+
try:
|
|
57
|
+
# Check if already seeded
|
|
58
|
+
if collection_name in seeded_collections:
|
|
59
|
+
logger.debug(
|
|
60
|
+
f"Collection '{collection_name}' already seeded for {app_slug}, skipping"
|
|
61
|
+
)
|
|
62
|
+
results[collection_name] = 0
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Get collection
|
|
66
|
+
collection = getattr(db, collection_name)
|
|
67
|
+
|
|
68
|
+
# Check if collection is empty (idempotent check)
|
|
69
|
+
count = await collection.count_documents({})
|
|
70
|
+
if count > 0:
|
|
71
|
+
logger.info(
|
|
72
|
+
f"Collection '{collection_name}' is not empty "
|
|
73
|
+
f"({count} documents) for {app_slug}, "
|
|
74
|
+
f"skipping seed to avoid duplicates"
|
|
75
|
+
)
|
|
76
|
+
# Mark as seeded even though we didn't insert (collection already has data)
|
|
77
|
+
seeded_collections.add(collection_name)
|
|
78
|
+
results[collection_name] = 0
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Prepare documents for insertion
|
|
82
|
+
# Convert datetime strings to datetime objects if needed
|
|
83
|
+
prepared_docs = []
|
|
84
|
+
for doc in documents:
|
|
85
|
+
prepared_doc = doc.copy()
|
|
86
|
+
|
|
87
|
+
# Convert datetime strings to datetime objects
|
|
88
|
+
for key, value in prepared_doc.items():
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
# Try to parse ISO format datetime strings
|
|
91
|
+
try:
|
|
92
|
+
# Check if it looks like a datetime string
|
|
93
|
+
if "T" in value and (
|
|
94
|
+
"Z" in value or "+" in value or "-" in value[-6:]
|
|
95
|
+
):
|
|
96
|
+
# Try parsing as ISO format
|
|
97
|
+
from dateutil.parser import parse as parse_date
|
|
98
|
+
|
|
99
|
+
prepared_doc[key] = parse_date(value)
|
|
100
|
+
except (ValueError, ImportError):
|
|
101
|
+
# Not a datetime string or dateutil not available, keep as string
|
|
102
|
+
pass
|
|
103
|
+
elif isinstance(value, dict) and "$date" in value:
|
|
104
|
+
# MongoDB extended JSON format
|
|
105
|
+
try:
|
|
106
|
+
from dateutil.parser import parse as parse_date
|
|
107
|
+
|
|
108
|
+
prepared_doc[key] = parse_date(value["$date"])
|
|
109
|
+
except (ValueError, ImportError):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# Add created_at if not present and document doesn't have timestamp fields
|
|
113
|
+
if (
|
|
114
|
+
"created_at" not in prepared_doc
|
|
115
|
+
and "date_created" not in prepared_doc
|
|
116
|
+
):
|
|
117
|
+
prepared_doc["created_at"] = datetime.utcnow()
|
|
118
|
+
|
|
119
|
+
prepared_docs.append(prepared_doc)
|
|
120
|
+
|
|
121
|
+
# Insert documents
|
|
122
|
+
if prepared_docs:
|
|
123
|
+
result = await collection.insert_many(prepared_docs)
|
|
124
|
+
inserted_count = len(result.inserted_ids)
|
|
125
|
+
results[collection_name] = inserted_count
|
|
126
|
+
|
|
127
|
+
# Mark as seeded
|
|
128
|
+
seeded_collections.add(collection_name)
|
|
129
|
+
|
|
130
|
+
logger.info(
|
|
131
|
+
f"✅ Seeded {inserted_count} document(s) into collection "
|
|
132
|
+
f"'{collection_name}' for {app_slug}"
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
results[collection_name] = 0
|
|
136
|
+
logger.warning(
|
|
137
|
+
f"No documents to seed for collection '{collection_name}' in {app_slug}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
except (
|
|
141
|
+
OperationFailure,
|
|
142
|
+
ConnectionFailure,
|
|
143
|
+
ServerSelectionTimeoutError,
|
|
144
|
+
Exception,
|
|
145
|
+
) as e:
|
|
146
|
+
# Type 2: Recoverable - log error and continue with other collections
|
|
147
|
+
logger.exception(
|
|
148
|
+
f"Failed to seed collection '{collection_name}' for {app_slug}: {e}"
|
|
149
|
+
)
|
|
150
|
+
results[collection_name] = 0
|
|
151
|
+
|
|
152
|
+
# Update seeding metadata
|
|
153
|
+
try:
|
|
154
|
+
if seeding_metadata:
|
|
155
|
+
await metadata_collection.update_one(
|
|
156
|
+
{"app_slug": app_slug},
|
|
157
|
+
{"$set": {"seeded_collections": list(seeded_collections)}},
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
await metadata_collection.insert_one(
|
|
161
|
+
{
|
|
162
|
+
"app_slug": app_slug,
|
|
163
|
+
"seeded_collections": list(seeded_collections),
|
|
164
|
+
"created_at": datetime.utcnow(),
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
except (
|
|
168
|
+
OperationFailure,
|
|
169
|
+
ConnectionFailure,
|
|
170
|
+
ServerSelectionTimeoutError,
|
|
171
|
+
ValueError,
|
|
172
|
+
TypeError,
|
|
173
|
+
KeyError,
|
|
174
|
+
) as e:
|
|
175
|
+
logger.warning(
|
|
176
|
+
f"Failed to update seeding metadata for {app_slug}: {e}", exc_info=True
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return results
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service initialization for MongoDB Engine.
|
|
3
|
+
|
|
4
|
+
This module handles initialization of optional services:
|
|
5
|
+
- Memory service (Mem0)
|
|
6
|
+
- WebSocket endpoints
|
|
7
|
+
- Observability (health checks, metrics, logging)
|
|
8
|
+
- Data seeding
|
|
9
|
+
|
|
10
|
+
This module is part of MDB_ENGINE - MongoDB Engine.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from pymongo.errors import (ConnectionFailure, OperationFailure,
|
|
17
|
+
ServerSelectionTimeoutError)
|
|
18
|
+
|
|
19
|
+
from ..database import ScopedMongoWrapper
|
|
20
|
+
from ..observability import get_logger as get_contextual_logger
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
contextual_logger = get_contextual_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ServiceInitializer:
|
|
27
|
+
"""
|
|
28
|
+
Manages initialization of optional services for apps.
|
|
29
|
+
|
|
30
|
+
Handles memory service, WebSocket endpoints, observability, and data seeding.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
mongo_uri: str,
|
|
36
|
+
db_name: str,
|
|
37
|
+
get_scoped_db_fn: Callable[[str], ScopedMongoWrapper],
|
|
38
|
+
) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Initialize the service initializer.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
mongo_uri: MongoDB connection URI
|
|
44
|
+
db_name: Database name
|
|
45
|
+
get_scoped_db_fn: Function to get scoped database wrapper
|
|
46
|
+
"""
|
|
47
|
+
self.mongo_uri = mongo_uri
|
|
48
|
+
self.db_name = db_name
|
|
49
|
+
self.get_scoped_db_fn = get_scoped_db_fn
|
|
50
|
+
self._memory_services: Dict[str, Any] = {}
|
|
51
|
+
self._websocket_configs: Dict[str, Dict[str, Any]] = {}
|
|
52
|
+
|
|
53
|
+
async def initialize_memory_service(
|
|
54
|
+
self, slug: str, memory_config: Dict[str, Any]
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Initialize Mem0 memory service for an app.
|
|
58
|
+
|
|
59
|
+
Memory support is OPTIONAL - only processes if dependencies are available.
|
|
60
|
+
mem0 handles embeddings and LLM via environment variables (.env).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
slug: App slug
|
|
64
|
+
memory_config: Memory configuration from manifest (already validated)
|
|
65
|
+
"""
|
|
66
|
+
# Try to import Memory service (optional dependency)
|
|
67
|
+
try:
|
|
68
|
+
from ..memory import Mem0MemoryService, Mem0MemoryServiceError
|
|
69
|
+
except ImportError as e:
|
|
70
|
+
contextual_logger.warning(
|
|
71
|
+
f"Memory configuration found for app '{slug}' but "
|
|
72
|
+
f"dependencies are not available: {e}. "
|
|
73
|
+
f"Memory support will be disabled for this app. Install with: "
|
|
74
|
+
f"pip install mem0ai"
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
contextual_logger.info(
|
|
79
|
+
f"Initializing Mem0 memory service for app '{slug}'",
|
|
80
|
+
extra={
|
|
81
|
+
"app_slug": slug,
|
|
82
|
+
"collection_name": memory_config.get(
|
|
83
|
+
"collection_name", f"{slug}_memories"
|
|
84
|
+
),
|
|
85
|
+
"enable_graph": memory_config.get("enable_graph", False),
|
|
86
|
+
"embedding_model_dims": memory_config.get("embedding_model_dims", 1536),
|
|
87
|
+
"infer": memory_config.get("infer", True),
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Extract memory config (exclude 'enabled')
|
|
93
|
+
service_config = {
|
|
94
|
+
k: v
|
|
95
|
+
for k, v in memory_config.items()
|
|
96
|
+
if k != "enabled"
|
|
97
|
+
and k
|
|
98
|
+
in [
|
|
99
|
+
"collection_name",
|
|
100
|
+
"embedding_model_dims",
|
|
101
|
+
"enable_graph",
|
|
102
|
+
"infer",
|
|
103
|
+
"async_mode",
|
|
104
|
+
"embedding_model",
|
|
105
|
+
"chat_model",
|
|
106
|
+
"temperature",
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Set default collection name if not provided
|
|
111
|
+
if "collection_name" not in service_config:
|
|
112
|
+
service_config["collection_name"] = f"{slug}_memories"
|
|
113
|
+
else:
|
|
114
|
+
# Ensure collection name is prefixed with app slug
|
|
115
|
+
collection_name = service_config["collection_name"]
|
|
116
|
+
if not collection_name.startswith(f"{slug}_"):
|
|
117
|
+
service_config["collection_name"] = f"{slug}_{collection_name}"
|
|
118
|
+
contextual_logger.info(
|
|
119
|
+
f"Prefixed memory collection name: "
|
|
120
|
+
f"'{collection_name}' -> "
|
|
121
|
+
f"'{service_config['collection_name']}'",
|
|
122
|
+
extra={
|
|
123
|
+
"app_slug": slug,
|
|
124
|
+
"original": collection_name,
|
|
125
|
+
"prefixed": service_config["collection_name"],
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Create Memory service with MongoDB integration
|
|
130
|
+
memory_service = Mem0MemoryService(
|
|
131
|
+
mongo_uri=self.mongo_uri,
|
|
132
|
+
db_name=self.db_name,
|
|
133
|
+
app_slug=slug,
|
|
134
|
+
config=service_config,
|
|
135
|
+
)
|
|
136
|
+
self._memory_services[slug] = memory_service
|
|
137
|
+
|
|
138
|
+
contextual_logger.info(
|
|
139
|
+
f"Mem0 memory service initialized for app '{slug}'",
|
|
140
|
+
extra={"app_slug": slug},
|
|
141
|
+
)
|
|
142
|
+
except Mem0MemoryServiceError as e:
|
|
143
|
+
contextual_logger.error(
|
|
144
|
+
f"Failed to initialize memory service for app '{slug}': {e}",
|
|
145
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
146
|
+
exc_info=True,
|
|
147
|
+
)
|
|
148
|
+
except (ImportError, AttributeError, TypeError, ValueError) as e:
|
|
149
|
+
contextual_logger.error(
|
|
150
|
+
f"Error initializing memory service for app '{slug}': {e}",
|
|
151
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
152
|
+
exc_info=True,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def register_websockets(
|
|
156
|
+
self, slug: str, websockets_config: Dict[str, Any]
|
|
157
|
+
) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Register WebSocket endpoints for an app.
|
|
160
|
+
|
|
161
|
+
WebSocket support is OPTIONAL - only processes if dependencies are available.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
slug: App slug
|
|
165
|
+
websockets_config: WebSocket configuration from manifest
|
|
166
|
+
"""
|
|
167
|
+
# Try to import WebSocket support (optional dependency)
|
|
168
|
+
try:
|
|
169
|
+
from ..routing.websockets import get_websocket_manager
|
|
170
|
+
except ImportError as e:
|
|
171
|
+
contextual_logger.warning(
|
|
172
|
+
f"WebSocket configuration found for app '{slug}' but "
|
|
173
|
+
f"dependencies are not available: {e}. "
|
|
174
|
+
f"WebSocket support will be disabled for this app. "
|
|
175
|
+
f"Install FastAPI with WebSocket support."
|
|
176
|
+
)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
contextual_logger.info(
|
|
180
|
+
f"Registering WebSocket endpoints for app '{slug}'",
|
|
181
|
+
extra={"app_slug": slug, "endpoint_count": len(websockets_config)},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Store WebSocket configuration for later route registration
|
|
185
|
+
self._websocket_configs[slug] = websockets_config
|
|
186
|
+
|
|
187
|
+
# Pre-initialize WebSocket managers
|
|
188
|
+
for endpoint_name, endpoint_config in websockets_config.items():
|
|
189
|
+
path = endpoint_config.get("path", f"/{endpoint_name}")
|
|
190
|
+
try:
|
|
191
|
+
await get_websocket_manager(slug)
|
|
192
|
+
except (ImportError, AttributeError, RuntimeError) as e:
|
|
193
|
+
contextual_logger.warning(
|
|
194
|
+
f"Could not initialize WebSocket manager for {slug}: {e}"
|
|
195
|
+
)
|
|
196
|
+
continue
|
|
197
|
+
contextual_logger.debug(
|
|
198
|
+
f"Configured WebSocket endpoint '{endpoint_name}' at path '{path}'",
|
|
199
|
+
extra={"app_slug": slug, "endpoint": endpoint_name, "path": path},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def seed_initial_data(
|
|
203
|
+
self, slug: str, initial_data: Dict[str, List[Dict[str, Any]]]
|
|
204
|
+
) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Seed initial data into collections for an app.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
slug: App slug
|
|
210
|
+
initial_data: Dictionary mapping collection names to arrays of documents
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
from .seeding import seed_initial_data
|
|
214
|
+
|
|
215
|
+
db = self.get_scoped_db_fn(slug)
|
|
216
|
+
results = await seed_initial_data(db, slug, initial_data)
|
|
217
|
+
|
|
218
|
+
total_inserted = sum(results.values())
|
|
219
|
+
if total_inserted > 0:
|
|
220
|
+
contextual_logger.info(
|
|
221
|
+
f"Seeded initial data for app '{slug}'",
|
|
222
|
+
extra={
|
|
223
|
+
"app_slug": slug,
|
|
224
|
+
"collections_seeded": len(
|
|
225
|
+
[c for c, count in results.items() if count > 0]
|
|
226
|
+
),
|
|
227
|
+
"total_documents": total_inserted,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
contextual_logger.debug(
|
|
232
|
+
f"No initial data seeded for app '{slug}' "
|
|
233
|
+
f"(collections already had data or were empty)",
|
|
234
|
+
extra={"app_slug": slug},
|
|
235
|
+
)
|
|
236
|
+
except (
|
|
237
|
+
OperationFailure,
|
|
238
|
+
ConnectionFailure,
|
|
239
|
+
ServerSelectionTimeoutError,
|
|
240
|
+
ValueError,
|
|
241
|
+
TypeError,
|
|
242
|
+
) as e:
|
|
243
|
+
contextual_logger.error(
|
|
244
|
+
f"Failed to seed initial data for app '{slug}': {e}",
|
|
245
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
246
|
+
exc_info=True,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
async def setup_observability(
|
|
250
|
+
self, slug: str, manifest: Dict[str, Any], observability_config: Dict[str, Any]
|
|
251
|
+
) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Set up observability features (health checks, metrics, logging) from manifest.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
slug: App slug
|
|
257
|
+
manifest: Full manifest dictionary
|
|
258
|
+
observability_config: Observability configuration from manifest
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
# Set up health checks
|
|
262
|
+
health_config = observability_config.get("health_checks", {})
|
|
263
|
+
if health_config.get("enabled", True):
|
|
264
|
+
endpoint = health_config.get("endpoint", "/health")
|
|
265
|
+
contextual_logger.info(
|
|
266
|
+
f"Health checks configured for {slug}",
|
|
267
|
+
extra={
|
|
268
|
+
"endpoint": endpoint,
|
|
269
|
+
"interval_seconds": health_config.get("interval_seconds", 30),
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Set up metrics
|
|
274
|
+
metrics_config = observability_config.get("metrics", {})
|
|
275
|
+
if metrics_config.get("enabled", True):
|
|
276
|
+
contextual_logger.info(
|
|
277
|
+
f"Metrics collection configured for {slug}",
|
|
278
|
+
extra={
|
|
279
|
+
"operation_metrics": metrics_config.get(
|
|
280
|
+
"collect_operation_metrics", True
|
|
281
|
+
),
|
|
282
|
+
"performance_metrics": metrics_config.get(
|
|
283
|
+
"collect_performance_metrics", True
|
|
284
|
+
),
|
|
285
|
+
"custom_metrics": metrics_config.get("custom_metrics", []),
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Set up logging
|
|
290
|
+
logging_config = observability_config.get("logging", {})
|
|
291
|
+
if logging_config:
|
|
292
|
+
log_level = logging_config.get("level", "INFO")
|
|
293
|
+
log_format = logging_config.get("format", "json")
|
|
294
|
+
contextual_logger.info(
|
|
295
|
+
f"Logging configured for {slug}",
|
|
296
|
+
extra={
|
|
297
|
+
"level": log_level,
|
|
298
|
+
"format": log_format,
|
|
299
|
+
"include_request_id": logging_config.get(
|
|
300
|
+
"include_request_id", True
|
|
301
|
+
),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
except (ImportError, AttributeError, TypeError, ValueError, KeyError) as e:
|
|
306
|
+
contextual_logger.warning(
|
|
307
|
+
f"Could not set up observability for {slug}: {e}", exc_info=True
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def get_websocket_config(self, slug: str) -> Optional[Dict[str, Any]]:
|
|
311
|
+
"""
|
|
312
|
+
Get WebSocket configuration for an app.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
slug: App slug
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
WebSocket configuration dict or None if not configured
|
|
319
|
+
"""
|
|
320
|
+
return self._websocket_configs.get(slug)
|
|
321
|
+
|
|
322
|
+
def get_memory_service(self, slug: str) -> Optional[Any]:
|
|
323
|
+
"""
|
|
324
|
+
Get Mem0 memory service for an app.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
slug: App slug
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Mem0MemoryService instance if memory is enabled for this app, None otherwise
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
service = self._memory_services.get(slug)
|
|
334
|
+
if service is not None:
|
|
335
|
+
# Quick health check - ensure it has the memory attribute
|
|
336
|
+
if not hasattr(service, "memory"):
|
|
337
|
+
contextual_logger.warning(
|
|
338
|
+
f"Memory service for '{slug}' is missing 'memory' "
|
|
339
|
+
f"attribute, returning None",
|
|
340
|
+
extra={"app_slug": slug},
|
|
341
|
+
)
|
|
342
|
+
return None
|
|
343
|
+
return service
|
|
344
|
+
except (KeyError, AttributeError, TypeError) as e:
|
|
345
|
+
contextual_logger.error(
|
|
346
|
+
f"Error retrieving memory service for '{slug}': {e}",
|
|
347
|
+
exc_info=True,
|
|
348
|
+
extra={"app_slug": slug, "error": str(e)},
|
|
349
|
+
)
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
def clear_services(self) -> None:
|
|
353
|
+
"""Clear all service state."""
|
|
354
|
+
self._memory_services.clear()
|
|
355
|
+
self._websocket_configs.clear()
|