memorisdk 2.0.1__py3-none-any.whl → 2.1.1__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.

Potentially problematic release.


This version of memorisdk might be problematic. Click here for more details.

Files changed (62) hide show
  1. memori/__init__.py +3 -3
  2. memori/agents/conscious_agent.py +289 -77
  3. memori/agents/memory_agent.py +19 -9
  4. memori/agents/retrieval_agent.py +59 -51
  5. memori/config/manager.py +7 -7
  6. memori/config/memory_manager.py +25 -25
  7. memori/config/settings.py +13 -6
  8. memori/core/conversation.py +15 -15
  9. memori/core/database.py +14 -13
  10. memori/core/memory.py +376 -105
  11. memori/core/providers.py +25 -25
  12. memori/database/__init__.py +11 -0
  13. memori/database/adapters/__init__.py +11 -0
  14. memori/database/adapters/mongodb_adapter.py +739 -0
  15. memori/database/adapters/mysql_adapter.py +8 -8
  16. memori/database/adapters/postgresql_adapter.py +6 -6
  17. memori/database/adapters/sqlite_adapter.py +6 -6
  18. memori/database/auto_creator.py +8 -9
  19. memori/database/connection_utils.py +5 -5
  20. memori/database/connectors/__init__.py +11 -0
  21. memori/database/connectors/base_connector.py +18 -19
  22. memori/database/connectors/mongodb_connector.py +654 -0
  23. memori/database/connectors/mysql_connector.py +13 -15
  24. memori/database/connectors/postgres_connector.py +12 -12
  25. memori/database/connectors/sqlite_connector.py +11 -11
  26. memori/database/models.py +2 -2
  27. memori/database/mongodb_manager.py +1484 -0
  28. memori/database/queries/base_queries.py +3 -4
  29. memori/database/queries/chat_queries.py +3 -5
  30. memori/database/queries/entity_queries.py +3 -5
  31. memori/database/queries/memory_queries.py +3 -5
  32. memori/database/query_translator.py +11 -11
  33. memori/database/schema_generators/__init__.py +11 -0
  34. memori/database/schema_generators/mongodb_schema_generator.py +666 -0
  35. memori/database/schema_generators/mysql_schema_generator.py +2 -4
  36. memori/database/search/__init__.py +11 -0
  37. memori/database/search/mongodb_search_adapter.py +653 -0
  38. memori/database/search/mysql_search_adapter.py +8 -8
  39. memori/database/search/sqlite_search_adapter.py +6 -6
  40. memori/database/search_service.py +17 -17
  41. memori/database/sqlalchemy_manager.py +10 -12
  42. memori/integrations/__init__.py +1 -1
  43. memori/integrations/anthropic_integration.py +1 -3
  44. memori/integrations/litellm_integration.py +23 -6
  45. memori/integrations/openai_integration.py +31 -3
  46. memori/tools/memory_tool.py +10 -9
  47. memori/utils/exceptions.py +58 -58
  48. memori/utils/helpers.py +11 -12
  49. memori/utils/input_validator.py +10 -12
  50. memori/utils/logging.py +4 -4
  51. memori/utils/pydantic_models.py +57 -57
  52. memori/utils/query_builder.py +20 -20
  53. memori/utils/security_audit.py +28 -28
  54. memori/utils/security_integration.py +9 -9
  55. memori/utils/transaction_manager.py +20 -19
  56. memori/utils/validators.py +6 -6
  57. {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/METADATA +23 -12
  58. memorisdk-2.1.1.dist-info/RECORD +71 -0
  59. memorisdk-2.0.1.dist-info/RECORD +0 -66
  60. {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/WHEEL +0 -0
  61. {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/licenses/LICENSE +0 -0
  62. {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,654 @@
1
+ """
2
+ MongoDB connector for Memori
3
+ Provides MongoDB-specific implementation of the database connector interface
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from typing import TYPE_CHECKING, Any
10
+ from urllib.parse import urlparse
11
+
12
+ from loguru import logger
13
+
14
+ if TYPE_CHECKING:
15
+ from pymongo import MongoClient
16
+ from pymongo.collection import Collection
17
+ from pymongo.database import Database
18
+
19
+ try:
20
+ import pymongo # noqa: F401
21
+ from pymongo import MongoClient as _MongoClient
22
+ from pymongo.collection import Collection as _Collection
23
+ from pymongo.database import Database as _Database
24
+ from pymongo.errors import ConnectionFailure, OperationFailure # noqa: F401
25
+
26
+ PYMONGO_AVAILABLE = True
27
+ MongoClient = _MongoClient # type: ignore
28
+ Collection = _Collection # type: ignore
29
+ Database = _Database # type: ignore
30
+ except ImportError:
31
+ PYMONGO_AVAILABLE = False
32
+ MongoClient = None # type: ignore
33
+ Collection = None # type: ignore
34
+ Database = None # type: ignore
35
+
36
+ from ...utils.exceptions import DatabaseError
37
+ from .base_connector import BaseDatabaseConnector, DatabaseType
38
+
39
+
40
+ class MongoDBConnector(BaseDatabaseConnector):
41
+ """MongoDB database connector with Atlas Vector Search support"""
42
+
43
+ def __init__(self, connection_config):
44
+ """Initialize MongoDB connector"""
45
+ if not PYMONGO_AVAILABLE:
46
+ raise DatabaseError(
47
+ "pymongo is required for MongoDB support. Install with: pip install pymongo"
48
+ )
49
+
50
+ if isinstance(connection_config, str):
51
+ self.connection_string = connection_config
52
+ self.connection_config = {"connection_string": connection_config}
53
+ else:
54
+ self.connection_string = connection_config.get(
55
+ "connection_string", "mongodb://localhost:27017"
56
+ )
57
+
58
+ # Parse MongoDB connection string
59
+ self._parse_connection_string()
60
+
61
+ # MongoDB-specific settings
62
+ self.client = None
63
+ self.database = None
64
+ self._collections = {}
65
+
66
+ super().__init__(connection_config)
67
+
68
+ def _detect_database_type(self) -> DatabaseType:
69
+ """Detect database type from connection config"""
70
+ return DatabaseType.MONGODB
71
+
72
+ def _parse_connection_string(self):
73
+ """Parse MongoDB connection string to extract components"""
74
+ try:
75
+ # Handle MongoDB connection strings properly (including replica sets)
76
+ if self.connection_string.startswith(
77
+ "mongodb://"
78
+ ) or self.connection_string.startswith("mongodb+srv://"):
79
+ # For MongoDB URIs, extract database name and basic info for logging
80
+ # but let pymongo handle the full parsing
81
+ if "?" in self.connection_string:
82
+ uri_part, query_part = self.connection_string.split("?", 1)
83
+ else:
84
+ uri_part, query_part = self.connection_string, ""
85
+
86
+ # Extract database name from path
87
+ if "/" in uri_part:
88
+ path_part = uri_part.split("/")[-1]
89
+ self.database_name = path_part if path_part else "memori"
90
+ else:
91
+ self.database_name = "memori"
92
+
93
+ # Extract host info for logging
94
+ is_srv_uri = self.connection_string.startswith("mongodb+srv://")
95
+
96
+ if is_srv_uri:
97
+ # For SRV URIs, extract the service hostname
98
+ if "@" in uri_part:
99
+ srv_host = uri_part.split("@")[1].split("/")[0].split("?")[0]
100
+ else:
101
+ srv_host = (
102
+ uri_part.replace("mongodb+srv://", "")
103
+ .split("/")[0]
104
+ .split("?")[0]
105
+ )
106
+ self.host = srv_host
107
+ self.port = (
108
+ 27017 # SRV uses default port, actual ports resolved via DNS
109
+ )
110
+ else:
111
+ # Regular mongodb:// URI parsing
112
+ if "@" in uri_part:
113
+ host_part = uri_part.split("@")[1]
114
+ else:
115
+ host_part = uri_part.replace("mongodb://", "")
116
+
117
+ # Get first host for logging purposes
118
+ if "," in host_part:
119
+ first_host = host_part.split(",")[0].split("/")[0]
120
+ else:
121
+ first_host = host_part.split("/")[0]
122
+
123
+ if ":" in first_host:
124
+ self.host, port_str = first_host.split(":", 1)
125
+ try:
126
+ self.port = int(port_str)
127
+ except ValueError:
128
+ self.port = 27017
129
+ else:
130
+ self.host = first_host
131
+ self.port = 27017
132
+
133
+ # Extract auth info
134
+ parsed = urlparse(self.connection_string)
135
+ self.username = parsed.username
136
+ self.password = parsed.password
137
+
138
+ # Extract query parameters
139
+ self.options = {}
140
+ if query_part:
141
+ params = query_part.split("&")
142
+ for param in params:
143
+ if "=" in param:
144
+ key, value = param.split("=", 1)
145
+ self.options[key] = value
146
+ else:
147
+ # Fall back to urlparse for simple connection strings
148
+ parsed = urlparse(self.connection_string)
149
+ self.host = parsed.hostname or "localhost"
150
+ self.port = parsed.port or 27017
151
+ self.database_name = parsed.path.lstrip("/") or "memori"
152
+ self.username = parsed.username
153
+ self.password = parsed.password
154
+ self.options = {}
155
+
156
+ except Exception as e:
157
+ logger.warning(f"Failed to parse MongoDB connection string: {e}")
158
+ # Set defaults
159
+ self.host = "localhost"
160
+ self.port = 27017
161
+ self.database_name = "memori"
162
+ self.username = None
163
+ self.password = None
164
+ self.options = {}
165
+
166
+ def get_connection(self) -> MongoClient:
167
+ """Get MongoDB client connection with support for mongodb+srv DNS seedlist"""
168
+ if self.client is None:
169
+ try:
170
+ # Create MongoDB client with appropriate options
171
+ client_options = {
172
+ "serverSelectionTimeoutMS": 5000, # 5 second timeout
173
+ "connectTimeoutMS": 10000, # 10 second connect timeout
174
+ "socketTimeoutMS": 30000, # 30 second socket timeout
175
+ "maxPoolSize": 50, # Connection pool size
176
+ "retryWrites": True, # Enable retryable writes
177
+ }
178
+
179
+ # Special handling for mongodb+srv URIs
180
+ is_srv_uri = self.connection_string.startswith("mongodb+srv://")
181
+
182
+ if is_srv_uri:
183
+ # For mongodb+srv URIs, TLS is automatically enabled
184
+ # Don't set directConnection for SRV URIs as they use DNS seedlist discovery
185
+ logger.info(
186
+ "Using MongoDB Atlas DNS seedlist discovery (mongodb+srv)"
187
+ )
188
+
189
+ # Add modern SRV-specific options for 2025
190
+ srv_options = {
191
+ "srvMaxHosts": 0, # No limit on SRV hosts (default)
192
+ "srvServiceName": "mongodb", # Default service name
193
+ }
194
+ client_options.update(srv_options)
195
+ else:
196
+ # For standard mongodb:// URIs
197
+ # Handle replica sets vs single hosts
198
+ if "replicaSet" in self.options:
199
+ logger.info("Using MongoDB replica set connection")
200
+ elif "," in self.connection_string:
201
+ logger.info("Using MongoDB multiple host connection")
202
+ else:
203
+ logger.info("Using MongoDB single host connection")
204
+
205
+ # Add any additional options from connection string (these override defaults)
206
+ client_options.update(self.options)
207
+
208
+ # Never set directConnection for SRV URIs or replica sets
209
+ if is_srv_uri or "replicaSet" in self.options:
210
+ client_options.pop("directConnection", None)
211
+
212
+ logger.debug(f"MongoDB connection options: {client_options}")
213
+ self.client = MongoClient(self.connection_string, **client_options)
214
+
215
+ # Test connection with more detailed logging
216
+ self.client.admin.command("ping")
217
+
218
+ # Get server info for better logging
219
+ try:
220
+ server_info = self.client.server_info()
221
+ version = server_info.get("version", "unknown")
222
+ logger.info(
223
+ f"Connected to MongoDB {version} at {self.host}:{self.port}"
224
+ )
225
+
226
+ if is_srv_uri:
227
+ # Log DNS-resolved hosts for SRV connections
228
+ topology = self.client.topology_description
229
+ hosts = []
230
+ for server in topology.server_descriptions():
231
+ if hasattr(server, "address") and server.address:
232
+ if (
233
+ isinstance(server.address, tuple)
234
+ and len(server.address) >= 2
235
+ ):
236
+ hosts.append(
237
+ f"{server.address[0]}:{server.address[1]}"
238
+ )
239
+ else:
240
+ hosts.append(str(server.address))
241
+
242
+ if hosts:
243
+ logger.info(f"DNS resolved hosts: {', '.join(hosts)}")
244
+ else:
245
+ logger.info("DNS seedlist discovery completed successfully")
246
+ except Exception as e:
247
+ logger.warning(f"Could not get server info: {e}")
248
+ logger.info(f"Connected to MongoDB at {self.host}:{self.port}")
249
+
250
+ except Exception as e:
251
+ raise DatabaseError(f"Failed to connect to MongoDB: {e}")
252
+
253
+ return self.client
254
+
255
+ def get_database(self) -> Database:
256
+ """Get MongoDB database"""
257
+ if self.database is None:
258
+ client = self.get_connection()
259
+ self.database = client[self.database_name]
260
+ return self.database
261
+
262
+ def get_collection(self, collection_name: str) -> Collection:
263
+ """Get MongoDB collection with caching"""
264
+ if collection_name not in self._collections:
265
+ database = self.get_database()
266
+ self._collections[collection_name] = database[collection_name]
267
+ return self._collections[collection_name]
268
+
269
+ def execute_query(
270
+ self, query: str, params: list[Any] | None = None
271
+ ) -> list[dict[str, Any]]:
272
+ """
273
+ Execute a query-like operation in MongoDB
274
+ Note: MongoDB doesn't use SQL, so this is adapted for MongoDB operations
275
+ """
276
+ try:
277
+ # Parse the "query" as a JSON operation for MongoDB
278
+ # This is a compatibility layer for the base interface
279
+ if isinstance(query, str) and query.strip().startswith("{"):
280
+ # Treat as MongoDB operation
281
+ operation = json.loads(query)
282
+ collection_name = operation.get("collection", "memories")
283
+ operation_type = operation.get("operation", "find")
284
+ filter_doc = operation.get("filter", {})
285
+ options = operation.get("options", {})
286
+
287
+ collection = self.get_collection(collection_name)
288
+
289
+ if operation_type == "find":
290
+ cursor = collection.find(filter_doc, **options)
291
+ results = list(cursor)
292
+ # Convert ObjectId to string for JSON serialization
293
+ for result in results:
294
+ if "_id" in result:
295
+ result["_id"] = str(result["_id"])
296
+ return results
297
+ elif operation_type == "aggregate":
298
+ pipeline = operation.get("pipeline", [])
299
+ cursor = collection.aggregate(pipeline, **options)
300
+ results = list(cursor)
301
+ # Convert ObjectId to string for JSON serialization
302
+ for result in results:
303
+ if "_id" in result:
304
+ result["_id"] = str(result["_id"])
305
+ return results
306
+ else:
307
+ raise DatabaseError(
308
+ f"Unsupported MongoDB operation: {operation_type}"
309
+ )
310
+ else:
311
+ # Fallback: treat as a collection name and return all documents
312
+ collection = self.get_collection(query or "memories")
313
+ cursor = collection.find().limit(100) # Limit for safety
314
+ results = list(cursor)
315
+ # Convert ObjectId to string for JSON serialization
316
+ for result in results:
317
+ if "_id" in result:
318
+ result["_id"] = str(result["_id"])
319
+ return results
320
+
321
+ except Exception as e:
322
+ raise DatabaseError(f"Failed to execute MongoDB query: {e}")
323
+
324
+ def execute_insert(self, query: str, params: list[Any] | None = None) -> str:
325
+ """Execute an insert operation and return the inserted document ID"""
326
+ try:
327
+ if isinstance(query, str) and query.strip().startswith("{"):
328
+ # Parse as MongoDB insert operation
329
+ operation = json.loads(query)
330
+ collection_name = operation.get("collection", "memories")
331
+ document = operation.get("document", {})
332
+
333
+ collection = self.get_collection(collection_name)
334
+ result = collection.insert_one(document)
335
+ return str(result.inserted_id)
336
+ else:
337
+ raise DatabaseError("Invalid insert operation format for MongoDB")
338
+
339
+ except Exception as e:
340
+ raise DatabaseError(f"Failed to execute MongoDB insert: {e}")
341
+
342
+ def execute_update(self, query: str, params: list[Any] | None = None) -> int:
343
+ """Execute an update operation and return number of modified documents"""
344
+ try:
345
+ if isinstance(query, str) and query.strip().startswith("{"):
346
+ # Parse as MongoDB update operation
347
+ operation = json.loads(query)
348
+ collection_name = operation.get("collection", "memories")
349
+ filter_doc = operation.get("filter", {})
350
+ update_doc = operation.get("update", {})
351
+ options = operation.get("options", {})
352
+
353
+ collection = self.get_collection(collection_name)
354
+
355
+ if operation.get("update_many", False):
356
+ result = collection.update_many(filter_doc, update_doc, **options)
357
+ else:
358
+ result = collection.update_one(filter_doc, update_doc, **options)
359
+
360
+ return result.modified_count
361
+ else:
362
+ raise DatabaseError("Invalid update operation format for MongoDB")
363
+
364
+ except Exception as e:
365
+ raise DatabaseError(f"Failed to execute MongoDB update: {e}")
366
+
367
+ def execute_delete(self, query: str, params: list[Any] | None = None) -> int:
368
+ """Execute a delete operation and return number of deleted documents"""
369
+ try:
370
+ if isinstance(query, str) and query.strip().startswith("{"):
371
+ # Parse as MongoDB delete operation
372
+ operation = json.loads(query)
373
+ collection_name = operation.get("collection", "memories")
374
+ filter_doc = operation.get("filter", {})
375
+ options = operation.get("options", {})
376
+
377
+ collection = self.get_collection(collection_name)
378
+
379
+ if operation.get("delete_many", False):
380
+ result = collection.delete_many(filter_doc, **options)
381
+ else:
382
+ result = collection.delete_one(filter_doc, **options)
383
+
384
+ return result.deleted_count
385
+ else:
386
+ raise DatabaseError("Invalid delete operation format for MongoDB")
387
+
388
+ except Exception as e:
389
+ raise DatabaseError(f"Failed to execute MongoDB delete: {e}")
390
+
391
+ def execute_transaction(self, queries: list[tuple[str, list[Any] | None]]) -> bool:
392
+ """Execute multiple operations in a MongoDB transaction"""
393
+ try:
394
+ client = self.get_connection()
395
+
396
+ # Check if transactions are supported (requires replica set or sharded cluster)
397
+ try:
398
+ with client.start_session() as session:
399
+ with session.start_transaction():
400
+ for query, params in queries:
401
+ # Execute each operation within the transaction
402
+ if "insert" in query.lower():
403
+ self.execute_insert(query, params)
404
+ elif "update" in query.lower():
405
+ self.execute_update(query, params)
406
+ elif "delete" in query.lower():
407
+ self.execute_delete(query, params)
408
+
409
+ # Transaction commits automatically if no exception is raised
410
+ return True
411
+
412
+ except OperationFailure as e:
413
+ if "Transaction numbers" in str(e):
414
+ # Transactions not supported, execute operations individually
415
+ logger.warning(
416
+ "Transactions not supported, executing operations individually"
417
+ )
418
+ for query, params in queries:
419
+ if "insert" in query.lower():
420
+ self.execute_insert(query, params)
421
+ elif "update" in query.lower():
422
+ self.execute_update(query, params)
423
+ elif "delete" in query.lower():
424
+ self.execute_delete(query, params)
425
+ return True
426
+ else:
427
+ raise
428
+
429
+ except Exception as e:
430
+ logger.error(f"Transaction failed: {e}")
431
+ return False
432
+
433
+ def test_connection(self) -> bool:
434
+ """Test if the MongoDB connection is working"""
435
+ try:
436
+ client = self.get_connection()
437
+ # Ping the server
438
+ client.admin.command("ping")
439
+ return True
440
+ except Exception as e:
441
+ logger.error(f"MongoDB connection test failed: {e}")
442
+ return False
443
+
444
+ def initialize_schema(self, schema_sql: str | None = None):
445
+ """Initialize MongoDB collections and indexes"""
446
+ try:
447
+ from ..schema_generators.mongodb_schema_generator import (
448
+ MongoDBSchemaGenerator,
449
+ )
450
+
451
+ schema_generator = MongoDBSchemaGenerator()
452
+ database = self.get_database()
453
+
454
+ # Create collections with validation rules
455
+ collections_schema = schema_generator.generate_collections_schema()
456
+ for collection_name, schema in collections_schema.items():
457
+ if collection_name not in database.list_collection_names():
458
+ # Create collection with validation
459
+ database.create_collection(
460
+ collection_name,
461
+ validator=schema.get("validator"),
462
+ validationAction=schema.get("validationAction", "error"),
463
+ validationLevel=schema.get("validationLevel", "strict"),
464
+ )
465
+ logger.info(f"Created MongoDB collection: {collection_name}")
466
+
467
+ # Create indexes
468
+ indexes_schema = schema_generator.generate_indexes_schema()
469
+ for collection_name, indexes in indexes_schema.items():
470
+ collection = self.get_collection(collection_name)
471
+ for index in indexes:
472
+ try:
473
+ collection.create_index(
474
+ index["keys"],
475
+ name=index.get("name"),
476
+ unique=index.get("unique", False),
477
+ sparse=index.get("sparse", False),
478
+ background=True, # Create index in background
479
+ )
480
+ logger.debug(f"Created index on {collection_name}: {index}")
481
+ except Exception as e:
482
+ logger.warning(
483
+ f"Failed to create index on {collection_name}: {e}"
484
+ )
485
+
486
+ logger.info("MongoDB schema initialization completed")
487
+
488
+ except Exception as e:
489
+ logger.error(f"Failed to initialize MongoDB schema: {e}")
490
+ raise DatabaseError(f"Failed to initialize MongoDB schema: {e}")
491
+
492
+ def supports_full_text_search(self) -> bool:
493
+ """Check if MongoDB supports text search (always True for MongoDB)"""
494
+ return True
495
+
496
+ def supports_vector_search(self) -> bool:
497
+ """Check if MongoDB Atlas Vector Search is available"""
498
+ try:
499
+ # Check if this is MongoDB Atlas by looking for Atlas-specific features
500
+ client = self.get_connection()
501
+ build_info = client.admin.command("buildInfo")
502
+
503
+ # Atlas typically includes specific modules or version patterns
504
+ # This is a heuristic check - in production you might want to configure this explicitly
505
+ build_info.get("version", "")
506
+ modules = build_info.get("modules", [])
507
+
508
+ # Check if vector search is available (Atlas feature)
509
+ # This is a simplified check - Atlas vector search availability can be complex
510
+ return "atlas" in str(modules).lower() or self._is_atlas_connection()
511
+
512
+ except Exception:
513
+ return False
514
+
515
+ def _is_atlas_connection(self) -> bool:
516
+ """Heuristic to detect if this is an Atlas connection"""
517
+ return (
518
+ "mongodb.net" in self.connection_string.lower()
519
+ or "atlas" in self.connection_string.lower()
520
+ or "cluster" in self.connection_string.lower()
521
+ )
522
+
523
+ def create_full_text_index(
524
+ self, table: str, columns: list[str], index_name: str
525
+ ) -> str:
526
+ """Create MongoDB text index"""
527
+ try:
528
+ collection = self.get_collection(table)
529
+
530
+ # Create text index specification
531
+ index_spec = {}
532
+ for column in columns:
533
+ index_spec[column] = "text"
534
+
535
+ collection.create_index(
536
+ list(index_spec.items()), name=index_name, background=True
537
+ )
538
+
539
+ return f"Created text index '{index_name}' on collection '{table}'"
540
+
541
+ except Exception as e:
542
+ raise DatabaseError(f"Failed to create text index: {e}")
543
+
544
+ def create_vector_index(
545
+ self,
546
+ collection_name: str,
547
+ vector_field: str,
548
+ dimensions: int,
549
+ similarity: str = "cosine",
550
+ index_name: str | None = None,
551
+ ) -> str:
552
+ """Create MongoDB Atlas Vector Search index"""
553
+ try:
554
+ if not self.supports_vector_search():
555
+ raise DatabaseError(
556
+ "Vector search is not supported in this MongoDB deployment"
557
+ )
558
+
559
+ self.get_collection(collection_name)
560
+
561
+ # Vector search index specification for MongoDB Atlas
562
+
563
+ index_name = index_name or f"{vector_field}_vector_index"
564
+
565
+ # Note: Vector search indexes are typically created via Atlas UI or Atlas Admin API
566
+ # This is a placeholder for the actual implementation
567
+ logger.warning(
568
+ "Vector search indexes should be created via MongoDB Atlas UI or Admin API"
569
+ )
570
+
571
+ return f"Vector index specification created for '{collection_name}.{vector_field}'"
572
+
573
+ except Exception as e:
574
+ raise DatabaseError(f"Failed to create vector index: {e}")
575
+
576
+ def get_database_info(self) -> dict[str, Any]:
577
+ """Get MongoDB database information and capabilities"""
578
+ try:
579
+ client = self.get_connection()
580
+ database = self.get_database()
581
+
582
+ info = {}
583
+
584
+ # Server information
585
+ server_info = client.server_info()
586
+ info["version"] = server_info.get("version", "unknown")
587
+ info["database_type"] = self.database_type.value
588
+ info["database_name"] = self.database_name
589
+ info["connection_string"] = (
590
+ self.connection_string.replace(
591
+ f"{self.username}:{self.password}@", "***:***@"
592
+ )
593
+ if self.username and self.password
594
+ else self.connection_string
595
+ )
596
+
597
+ # Database stats
598
+ try:
599
+ stats = database.command("dbStats")
600
+ info["collections_count"] = stats.get("collections", 0)
601
+ info["data_size"] = stats.get("dataSize", 0)
602
+ info["storage_size"] = stats.get("storageSize", 0)
603
+ info["indexes_count"] = stats.get("indexes", 0)
604
+ except Exception:
605
+ pass
606
+
607
+ # Capabilities
608
+ info["full_text_search_support"] = True
609
+ info["vector_search_support"] = self.supports_vector_search()
610
+ info["transactions_support"] = self._check_transactions_support()
611
+
612
+ # Replica set information
613
+ try:
614
+ replica_config = client.admin.command("replSetGetStatus")
615
+ info["replica_set"] = replica_config.get("set", "Not in replica set")
616
+ except Exception:
617
+ info["replica_set"] = "Standalone"
618
+
619
+ return info
620
+
621
+ except Exception as e:
622
+ logger.warning(f"Could not get MongoDB database info: {e}")
623
+ return {
624
+ "database_type": self.database_type.value,
625
+ "version": "unknown",
626
+ "full_text_search_support": True,
627
+ "vector_search_support": False,
628
+ "error": str(e),
629
+ }
630
+
631
+ def _check_transactions_support(self) -> bool:
632
+ """Check if MongoDB deployment supports transactions"""
633
+ try:
634
+ client = self.get_connection()
635
+ with client.start_session() as session:
636
+ with session.start_transaction():
637
+ # Just test if we can start a transaction
638
+ pass
639
+ return True
640
+ except Exception:
641
+ return False
642
+
643
+ def close(self):
644
+ """Close MongoDB connection"""
645
+ if self.client:
646
+ self.client.close()
647
+ self.client = None
648
+ self.database = None
649
+ self._collections.clear()
650
+ logger.info("MongoDB connection closed")
651
+
652
+ def __del__(self):
653
+ """Cleanup when connector is destroyed"""
654
+ self.close()