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,1484 @@
1
+ """
2
+ MongoDB-based database manager for Memori v2.0
3
+ Provides MongoDB support parallel to SQLAlchemy with same interface
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from typing import TYPE_CHECKING, Any
12
+ from urllib.parse import urlparse
13
+
14
+ from loguru import logger
15
+
16
+ if TYPE_CHECKING:
17
+ from pymongo import MongoClient
18
+ from pymongo.collection import Collection
19
+ from pymongo.database import Database
20
+
21
+ try:
22
+ import pymongo # noqa: F401
23
+ from bson import ObjectId # noqa: F401
24
+ from pymongo import MongoClient as _MongoClient
25
+ from pymongo.collection import Collection as _Collection
26
+ from pymongo.database import Database as _Database
27
+ from pymongo.errors import ( # noqa: F401
28
+ ConnectionFailure,
29
+ DuplicateKeyError,
30
+ OperationFailure,
31
+ )
32
+
33
+ PYMONGO_AVAILABLE = True
34
+ MongoClient = _MongoClient # type: ignore
35
+ Collection = _Collection # type: ignore
36
+ Database = _Database # type: ignore
37
+ except ImportError:
38
+ PYMONGO_AVAILABLE = False
39
+ MongoClient = None # type: ignore
40
+ Collection = None # type: ignore
41
+ Database = None # type: ignore
42
+ logger.warning("pymongo not available - MongoDB support disabled")
43
+
44
+ from ..utils.exceptions import DatabaseError
45
+ from ..utils.pydantic_models import ProcessedLongTermMemory
46
+
47
+
48
+ class MongoDBDatabaseManager:
49
+ """MongoDB-based database manager with interface compatible with SQLAlchemy manager"""
50
+
51
+ # Constants for collection names
52
+ CHAT_HISTORY_COLLECTION = "chat_history"
53
+ SHORT_TERM_MEMORY_COLLECTION = "short_term_memory"
54
+ LONG_TERM_MEMORY_COLLECTION = "long_term_memory"
55
+
56
+ # Database type identifier for database-agnostic code
57
+ database_type = "mongodb"
58
+
59
+ def __init__(
60
+ self, database_connect: str, template: str = "basic", schema_init: bool = True
61
+ ):
62
+ if not PYMONGO_AVAILABLE:
63
+ raise DatabaseError(
64
+ "MongoDB support requires pymongo. Install with: pip install pymongo"
65
+ )
66
+
67
+ self.database_connect = database_connect
68
+ self.template = template
69
+ self.schema_init = schema_init
70
+
71
+ # Parse MongoDB connection string
72
+ self._parse_connection_string()
73
+
74
+ # Initialize MongoDB connection
75
+ self.client = None
76
+ self.database = None
77
+ self.database_type = "mongodb"
78
+
79
+ # Collection names (matching SQLAlchemy table names)
80
+ self.CHAT_HISTORY_COLLECTION = "chat_history"
81
+ self.SHORT_TERM_MEMORY_COLLECTION = "short_term_memory"
82
+ self.LONG_TERM_MEMORY_COLLECTION = "long_term_memory"
83
+
84
+ # Collections cache
85
+ self._collections = {}
86
+
87
+ logger.info(f"Initialized MongoDB database manager for {self.database_name}")
88
+
89
+ def _parse_connection_string(self):
90
+ """Parse MongoDB connection string to extract components"""
91
+ try:
92
+ # Handle both mongodb:// and mongodb+srv:// schemes
93
+ parsed = urlparse(self.database_connect)
94
+
95
+ # Extract host - handle SRV URIs differently
96
+ hostname = parsed.hostname
97
+ is_srv_uri = self.database_connect.startswith("mongodb+srv://")
98
+
99
+ if hostname and hostname != "localhost":
100
+ if is_srv_uri:
101
+ # For SRV URIs, don't try to resolve hostname directly
102
+ # PyMongo will handle SRV resolution internally
103
+ self.host = hostname
104
+ else:
105
+ # For regular mongodb:// URIs, check hostname resolution
106
+ import socket
107
+
108
+ try:
109
+ socket.gethostbyname(hostname)
110
+ self.host = hostname
111
+ except socket.gaierror:
112
+ logger.warning(
113
+ f"Cannot resolve hostname '{hostname}', falling back to localhost"
114
+ )
115
+ self.host = "localhost"
116
+ else:
117
+ self.host = hostname or "localhost"
118
+
119
+ self.port = parsed.port or 27017
120
+ self.database_name = parsed.path.lstrip("/") or "memori"
121
+ self.username = parsed.username
122
+ self.password = parsed.password
123
+
124
+ # Extract query parameters
125
+ self.options = {}
126
+ if parsed.query:
127
+ params = parsed.query.split("&")
128
+ for param in params:
129
+ if "=" in param:
130
+ key, value = param.split("=", 1)
131
+ self.options[key] = value
132
+
133
+ logger.debug(
134
+ f"Parsed MongoDB connection: {self.host}:{self.port}/{self.database_name}"
135
+ )
136
+
137
+ except Exception as e:
138
+ logger.warning(f"Failed to parse MongoDB connection string: {e}")
139
+ # Set defaults
140
+ self.host = "localhost"
141
+ self.port = 27017
142
+ self.database_name = "memori"
143
+ self.username = None
144
+ self.password = None
145
+ self.options = {}
146
+
147
+ def _get_client(self) -> MongoClient:
148
+ """Get MongoDB client connection with support for mongodb+srv DNS seedlist"""
149
+ if self.client is None:
150
+ try:
151
+ # Create MongoDB client with appropriate options
152
+ client_options = {
153
+ "serverSelectionTimeoutMS": 5000, # 5 second timeout
154
+ "connectTimeoutMS": 10000, # 10 second connect timeout
155
+ "socketTimeoutMS": 10000, # 10 second socket timeout
156
+ "maxPoolSize": 50, # Connection pool size
157
+ "retryWrites": True, # Enable retryable writes
158
+ }
159
+
160
+ # Check if this is a mongodb+srv URI for DNS seedlist discovery
161
+ is_srv_uri = self.database_connect.startswith("mongodb+srv://")
162
+
163
+ if is_srv_uri:
164
+ logger.info(
165
+ "Using MongoDB Atlas DNS seedlist discovery (mongodb+srv)"
166
+ )
167
+
168
+ # Add modern SRV-specific options for 2025
169
+ srv_options = {
170
+ "srvMaxHosts": 0, # No limit on SRV hosts (default)
171
+ "srvServiceName": "mongodb", # Default service name
172
+ }
173
+ client_options.update(srv_options)
174
+
175
+ # For SRV URIs, don't use fallback - they handle discovery automatically
176
+ # Add any additional options from connection string (these override defaults)
177
+ client_options.update(self.options)
178
+
179
+ # Never set directConnection for SRV URIs
180
+ client_options.pop("directConnection", None)
181
+
182
+ logger.debug(f"MongoDB+SRV connection options: {client_options}")
183
+ self.client = MongoClient(self.database_connect, **client_options)
184
+
185
+ # Test connection
186
+ self.client.admin.command("ping")
187
+
188
+ # Get server info and DNS-resolved hosts for better logging
189
+ try:
190
+ server_info = self.client.server_info()
191
+ version = server_info.get("version", "unknown")
192
+ logger.info(f"Connected to MongoDB Atlas {version}")
193
+
194
+ # Log DNS-resolved hosts for SRV connections
195
+ try:
196
+ topology = self.client.topology_description
197
+ hosts = []
198
+ for server in topology.server_descriptions():
199
+ try:
200
+ if hasattr(server, "address") and server.address:
201
+ if (
202
+ isinstance(server.address, tuple)
203
+ and len(server.address) >= 2
204
+ ):
205
+ hosts.append(
206
+ f"{server.address[0]}:{server.address[1]}"
207
+ )
208
+ else:
209
+ hosts.append(str(server.address))
210
+ except AttributeError:
211
+ # Some server descriptions might not have address attribute
212
+ continue
213
+
214
+ if hosts:
215
+ logger.info(
216
+ f"DNS resolved MongoDB Atlas hosts: {', '.join(hosts)}"
217
+ )
218
+ else:
219
+ logger.info(
220
+ "MongoDB Atlas DNS seedlist discovery completed successfully"
221
+ )
222
+ except Exception as e:
223
+ logger.debug(
224
+ f"Could not get Atlas server topology info: {e}"
225
+ )
226
+ except Exception as e:
227
+ logger.warning(f"Could not get Atlas server info: {e}")
228
+ logger.info("Connected to MongoDB Atlas successfully")
229
+
230
+ else:
231
+ # Legacy mongodb:// URI handling with fallbacks
232
+ # Add any additional options from connection string
233
+ client_options.update(self.options)
234
+
235
+ # Try original connection string first
236
+ try:
237
+ self.client = MongoClient(
238
+ self.database_connect, **client_options
239
+ )
240
+ # Test connection
241
+ self.client.admin.command("ping")
242
+ logger.info(
243
+ "Connected to MongoDB using original connection string"
244
+ )
245
+ except Exception as original_error:
246
+ logger.warning(f"Original connection failed: {original_error}")
247
+
248
+ # Try fallback with explicit host:port (only for non-SRV URIs)
249
+ fallback_uri = (
250
+ f"mongodb://{self.host}:{self.port}/{self.database_name}"
251
+ )
252
+ logger.info(f"Trying fallback connection: {fallback_uri}")
253
+
254
+ self.client = MongoClient(fallback_uri, **client_options)
255
+ # Test connection
256
+ self.client.admin.command("ping")
257
+ logger.info(
258
+ f"Connected to MongoDB at {self.host}:{self.port}/{self.database_name}"
259
+ )
260
+
261
+ except Exception as e:
262
+ error_msg = f"Failed to connect to MongoDB: {e}"
263
+ logger.error(error_msg)
264
+ logger.error("Please check that:")
265
+ logger.error("1. MongoDB is running")
266
+ logger.error("2. Connection string is correct")
267
+ logger.error("3. Network connectivity is available")
268
+ raise DatabaseError(error_msg)
269
+
270
+ return self.client
271
+
272
+ def _get_database(self) -> Database:
273
+ """Get MongoDB database with caching and creation if needed"""
274
+ if self.database is None:
275
+ client = self._get_client()
276
+ self.database = client[self.database_name]
277
+
278
+ # Ensure database exists by creating a dummy collection if needed
279
+ try:
280
+ # Try to get database stats - this will fail if DB doesn't exist
281
+ self.database.command("dbstats")
282
+ except Exception:
283
+ # Database doesn't exist, create it by creating a dummy collection
284
+ logger.info(f"Creating MongoDB database: {self.database_name}")
285
+ self.database.create_collection("_init")
286
+ # Remove the dummy collection
287
+ self.database.drop_collection("_init")
288
+ logger.info(f"Database {self.database_name} created successfully")
289
+
290
+ return self.database
291
+
292
+ def _get_collection(self, collection_name: str) -> Collection:
293
+ """Get MongoDB collection with caching"""
294
+ if collection_name not in self._collections:
295
+ database = self._get_database()
296
+ self._collections[collection_name] = database[collection_name]
297
+ return self._collections[collection_name]
298
+
299
+ def _convert_datetime_fields(self, document: dict[str, Any]) -> dict[str, Any]:
300
+ """Convert datetime strings to datetime objects"""
301
+ datetime_fields = [
302
+ "created_at",
303
+ "expires_at",
304
+ "last_accessed",
305
+ "extraction_timestamp",
306
+ "timestamp",
307
+ ]
308
+
309
+ for field in datetime_fields:
310
+ if field in document and document[field] is not None:
311
+ if isinstance(document[field], str):
312
+ try:
313
+ # Handle various ISO format variations
314
+ document[field] = datetime.fromisoformat(
315
+ document[field].replace("Z", "+00:00")
316
+ )
317
+ except:
318
+ document[field] = datetime.now(timezone.utc)
319
+ elif not isinstance(document[field], datetime):
320
+ document[field] = datetime.now(timezone.utc)
321
+
322
+ # Add created_at if missing
323
+ if "created_at" not in document:
324
+ document["created_at"] = datetime.now(timezone.utc)
325
+
326
+ return document
327
+
328
+ def _convert_to_dict(self, document: dict[str, Any]) -> dict[str, Any]:
329
+ """Convert MongoDB document to dictionary format compatible with SQLAlchemy results"""
330
+ if not document:
331
+ return {}
332
+
333
+ result = document.copy()
334
+
335
+ # Convert ObjectId to string
336
+ if "_id" in result:
337
+ result["_id"] = str(result["_id"])
338
+
339
+ # Convert datetime objects to ISO strings for compatibility
340
+ datetime_fields = [
341
+ "created_at",
342
+ "expires_at",
343
+ "last_accessed",
344
+ "extraction_timestamp",
345
+ "timestamp",
346
+ ]
347
+ for field in datetime_fields:
348
+ if field in result and isinstance(result[field], datetime):
349
+ result[field] = result[field].isoformat()
350
+
351
+ # Ensure JSON fields are properly handled
352
+ json_fields = [
353
+ "processed_data",
354
+ "entities_json",
355
+ "keywords_json",
356
+ "supersedes_json",
357
+ "related_memories_json",
358
+ "metadata_json",
359
+ ]
360
+ for field in json_fields:
361
+ if field in result and isinstance(result[field], str):
362
+ try:
363
+ result[field] = json.loads(result[field])
364
+ except:
365
+ pass # Keep as string if not valid JSON
366
+
367
+ return result
368
+
369
+ def initialize_schema(self):
370
+ """Initialize MongoDB collections and indexes"""
371
+ if not self.schema_init:
372
+ logger.info("Schema initialization disabled (schema_init=False)")
373
+ return
374
+
375
+ try:
376
+ database = self._get_database()
377
+ existing_collections = database.list_collection_names()
378
+
379
+ # Create collections if they don't exist
380
+ collections = [
381
+ self.CHAT_HISTORY_COLLECTION,
382
+ self.SHORT_TERM_MEMORY_COLLECTION,
383
+ self.LONG_TERM_MEMORY_COLLECTION,
384
+ ]
385
+
386
+ for collection_name in collections:
387
+ if collection_name not in existing_collections:
388
+ database.create_collection(collection_name)
389
+ logger.info(f"Created MongoDB collection: {collection_name}")
390
+
391
+ # Create indexes for performance
392
+ self._create_indexes()
393
+
394
+ logger.info("MongoDB schema initialized successfully")
395
+
396
+ except Exception as e:
397
+ logger.error(f"Failed to initialize MongoDB schema: {e}")
398
+ raise DatabaseError(f"Failed to initialize MongoDB schema: {e}")
399
+
400
+ def _create_indexes(self):
401
+ """Create essential indexes for performance"""
402
+ try:
403
+ # Chat history indexes
404
+ chat_collection = self._get_collection(self.CHAT_HISTORY_COLLECTION)
405
+ chat_collection.create_index([("chat_id", 1)], unique=True, background=True)
406
+ chat_collection.create_index(
407
+ [("namespace", 1), ("session_id", 1)], background=True
408
+ )
409
+ chat_collection.create_index([("timestamp", -1)], background=True)
410
+ chat_collection.create_index([("model", 1)], background=True)
411
+
412
+ # Short-term memory indexes
413
+ st_collection = self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION)
414
+ st_collection.create_index([("memory_id", 1)], unique=True, background=True)
415
+ st_collection.create_index(
416
+ [("namespace", 1), ("category_primary", 1), ("importance_score", -1)],
417
+ background=True,
418
+ )
419
+ st_collection.create_index([("expires_at", 1)], background=True)
420
+ st_collection.create_index([("created_at", -1)], background=True)
421
+ st_collection.create_index([("is_permanent_context", 1)], background=True)
422
+
423
+ # Enhanced text search index for short-term memory with weights
424
+ try:
425
+ # Check if text index already exists
426
+ existing_indexes = st_collection.list_indexes()
427
+ text_index_exists = any(
428
+ idx.get("name") == "text_search_index" for idx in existing_indexes
429
+ )
430
+
431
+ if not text_index_exists:
432
+ st_collection.create_index(
433
+ [
434
+ ("searchable_content", "text"),
435
+ ("summary", "text"),
436
+ ("topic", "text"),
437
+ ],
438
+ background=True, # Use background=True for non-blocking
439
+ weights={
440
+ "searchable_content": 10, # Highest weight for main content
441
+ "summary": 5, # Medium weight for summary
442
+ "topic": 3, # Lower weight for topic
443
+ },
444
+ name="text_search_index",
445
+ )
446
+ logger.info(
447
+ "Created enhanced text search index for short-term memory with weights"
448
+ )
449
+ else:
450
+ logger.debug(
451
+ "Text search index already exists for short-term memory"
452
+ )
453
+ except Exception as e:
454
+ logger.warning(f"Text index creation failed for short-term memory: {e}")
455
+
456
+ # Long-term memory indexes
457
+ lt_collection = self._get_collection(self.LONG_TERM_MEMORY_COLLECTION)
458
+ lt_collection.create_index([("memory_id", 1)], unique=True, background=True)
459
+ lt_collection.create_index(
460
+ [("namespace", 1), ("category_primary", 1), ("importance_score", -1)],
461
+ background=True,
462
+ )
463
+ lt_collection.create_index([("classification", 1)], background=True)
464
+ lt_collection.create_index([("topic", 1)], background=True)
465
+ lt_collection.create_index([("created_at", -1)], background=True)
466
+ lt_collection.create_index([("conscious_processed", 1)], background=True)
467
+ lt_collection.create_index(
468
+ [("processed_for_duplicates", 1)], background=True
469
+ )
470
+ lt_collection.create_index([("promotion_eligible", 1)], background=True)
471
+
472
+ # Enhanced text search index for long-term memory with weights
473
+ try:
474
+ # Check if text index already exists
475
+ existing_indexes = lt_collection.list_indexes()
476
+ text_index_exists = any(
477
+ idx.get("name") == "text_search_index" for idx in existing_indexes
478
+ )
479
+
480
+ if not text_index_exists:
481
+ lt_collection.create_index(
482
+ [
483
+ ("searchable_content", "text"),
484
+ ("summary", "text"),
485
+ ("topic", "text"),
486
+ ("classification_reason", "text"),
487
+ ],
488
+ background=True, # Use background=True for non-blocking
489
+ weights={
490
+ "searchable_content": 10, # Highest weight for main content
491
+ "summary": 8, # High weight for summary
492
+ "topic": 5, # Medium weight for topic
493
+ "classification_reason": 2, # Lower weight for reasoning
494
+ },
495
+ name="text_search_index",
496
+ )
497
+ logger.info(
498
+ "Created enhanced text search index for long-term memory with weights"
499
+ )
500
+ else:
501
+ logger.debug(
502
+ "Text search index already exists for long-term memory"
503
+ )
504
+ except Exception as e:
505
+ logger.warning(f"Text index creation failed for long-term memory: {e}")
506
+
507
+ # Verify text indexes are functional
508
+ self._verify_text_indexes()
509
+
510
+ logger.debug("MongoDB indexes created successfully")
511
+
512
+ except Exception as e:
513
+ logger.warning(f"Failed to create some MongoDB indexes: {e}")
514
+
515
+ def _verify_text_indexes(self):
516
+ """Verify that text indexes are functional by performing test searches"""
517
+ try:
518
+ # Test short-term memory text index
519
+ st_collection = self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION)
520
+ try:
521
+ # Perform a simple text search to verify index works
522
+ _ = st_collection.find_one({"$text": {"$search": "test"}})
523
+ logger.debug("Short-term memory text index verification successful")
524
+ except Exception as e:
525
+ logger.warning(
526
+ f"Short-term memory text index may not be functional: {e}"
527
+ )
528
+
529
+ # Test long-term memory text index
530
+ lt_collection = self._get_collection(self.LONG_TERM_MEMORY_COLLECTION)
531
+ try:
532
+ # Perform a simple text search to verify index works
533
+ _ = lt_collection.find_one({"$text": {"$search": "test"}})
534
+ logger.debug("Long-term memory text index verification successful")
535
+ except Exception as e:
536
+ logger.warning(
537
+ f"Long-term memory text index may not be functional: {e}"
538
+ )
539
+
540
+ # Check if text indexes exist
541
+ st_indexes = list(st_collection.list_indexes())
542
+ lt_indexes = list(lt_collection.list_indexes())
543
+
544
+ st_has_text_index = any(
545
+ "text" in idx.get("key", {}).values() for idx in st_indexes
546
+ )
547
+ lt_has_text_index = any(
548
+ "text" in idx.get("key", {}).values() for idx in lt_indexes
549
+ )
550
+
551
+ if st_has_text_index:
552
+ logger.info("Short-term memory collection has text index")
553
+ else:
554
+ logger.warning("Short-term memory collection missing text index")
555
+
556
+ if lt_has_text_index:
557
+ logger.info("Long-term memory collection has text index")
558
+ else:
559
+ logger.warning("Long-term memory collection missing text index")
560
+
561
+ except Exception as e:
562
+ logger.error(f"Text index verification failed: {e}")
563
+
564
+ def store_chat_history(
565
+ self,
566
+ chat_id: str,
567
+ user_input: str,
568
+ ai_output: str,
569
+ model: str,
570
+ timestamp: datetime,
571
+ session_id: str,
572
+ namespace: str = "default",
573
+ tokens_used: int = 0,
574
+ metadata: dict[str, Any] | None = None,
575
+ ):
576
+ """Store chat history in MongoDB"""
577
+ try:
578
+ collection = self._get_collection(self.CHAT_HISTORY_COLLECTION)
579
+
580
+ document = {
581
+ "chat_id": chat_id,
582
+ "user_input": user_input,
583
+ "ai_output": ai_output,
584
+ "model": model,
585
+ "timestamp": timestamp,
586
+ "session_id": session_id,
587
+ "namespace": namespace,
588
+ "tokens_used": tokens_used,
589
+ "metadata_json": metadata or {},
590
+ }
591
+
592
+ # Convert datetime fields
593
+ document = self._convert_datetime_fields(document)
594
+
595
+ # Use upsert (insert or update) for compatibility with SQLAlchemy behavior
596
+ collection.replace_one({"chat_id": chat_id}, document, upsert=True)
597
+
598
+ logger.debug(f"Stored chat history: {chat_id}")
599
+
600
+ except Exception as e:
601
+ logger.error(f"Failed to store chat history: {e}")
602
+ raise DatabaseError(f"Failed to store chat history: {e}")
603
+
604
+ def get_chat_history(
605
+ self,
606
+ namespace: str = "default",
607
+ session_id: str | None = None,
608
+ limit: int = 10,
609
+ ) -> list[dict[str, Any]]:
610
+ """Get chat history from MongoDB"""
611
+ try:
612
+ collection = self._get_collection(self.CHAT_HISTORY_COLLECTION)
613
+
614
+ # Build filter
615
+ filter_doc = {"namespace": namespace}
616
+ if session_id:
617
+ filter_doc["session_id"] = session_id
618
+
619
+ # Execute query
620
+ cursor = collection.find(filter_doc).sort("timestamp", -1).limit(limit)
621
+
622
+ results = []
623
+ for document in cursor:
624
+ results.append(self._convert_to_dict(document))
625
+
626
+ logger.debug(f"Retrieved {len(results)} chat history entries")
627
+ return results
628
+
629
+ except Exception as e:
630
+ logger.error(f"Failed to get chat history: {e}")
631
+ return []
632
+
633
+ def store_short_term_memory(
634
+ self,
635
+ memory_id: str,
636
+ processed_data: str,
637
+ importance_score: float,
638
+ category_primary: str,
639
+ retention_type: str,
640
+ namespace: str = "default",
641
+ expires_at: datetime | None = None,
642
+ searchable_content: str = "",
643
+ summary: str = "",
644
+ is_permanent_context: bool = False,
645
+ metadata: dict[str, Any] | None = None,
646
+ ):
647
+ """Store short-term memory in MongoDB"""
648
+ try:
649
+ collection = self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION)
650
+
651
+ document = {
652
+ "memory_id": memory_id,
653
+ "processed_data": processed_data,
654
+ "importance_score": importance_score,
655
+ "category_primary": category_primary,
656
+ "retention_type": retention_type,
657
+ "namespace": namespace,
658
+ "created_at": datetime.now(timezone.utc),
659
+ "expires_at": expires_at,
660
+ "searchable_content": searchable_content,
661
+ "summary": summary,
662
+ "is_permanent_context": is_permanent_context,
663
+ "metadata_json": metadata or {},
664
+ "access_count": 0,
665
+ "last_accessed": datetime.now(timezone.utc),
666
+ }
667
+
668
+ # Convert datetime fields
669
+ document = self._convert_datetime_fields(document)
670
+
671
+ # Use upsert (insert or update) for compatibility with SQLAlchemy behavior
672
+ collection.replace_one({"memory_id": memory_id}, document, upsert=True)
673
+
674
+ logger.debug(f"Stored short-term memory: {memory_id}")
675
+
676
+ except Exception as e:
677
+ logger.error(f"Failed to store short-term memory: {e}")
678
+ raise DatabaseError(f"Failed to store short-term memory: {e}")
679
+
680
+ def find_short_term_memory_by_id(
681
+ self,
682
+ memory_id: str,
683
+ namespace: str = "default",
684
+ ) -> dict[str, Any] | None:
685
+ """Find a specific short-term memory by memory_id"""
686
+ try:
687
+ collection = self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION)
688
+
689
+ # Find memory by memory_id and namespace
690
+ document = collection.find_one(
691
+ {"memory_id": memory_id, "namespace": namespace}
692
+ )
693
+
694
+ if document:
695
+ return self._convert_to_dict(document)
696
+ return None
697
+
698
+ except Exception as e:
699
+ logger.error(f"Failed to find short-term memory by ID {memory_id}: {e}")
700
+ return None
701
+
702
+ def get_short_term_memory(
703
+ self,
704
+ namespace: str = "default",
705
+ category_filter: str | None = None,
706
+ limit: int = 10,
707
+ include_expired: bool = False,
708
+ ) -> list[dict[str, Any]]:
709
+ """Get short-term memory from MongoDB"""
710
+ try:
711
+ collection = self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION)
712
+
713
+ # Build filter
714
+ filter_doc = {"namespace": namespace}
715
+
716
+ if category_filter:
717
+ filter_doc["category_primary"] = category_filter
718
+
719
+ if not include_expired:
720
+ current_time = datetime.now(timezone.utc)
721
+ filter_doc["$or"] = [
722
+ {"expires_at": {"$exists": False}},
723
+ {"expires_at": None},
724
+ {"expires_at": {"$gt": current_time}},
725
+ ]
726
+
727
+ # Execute query
728
+ cursor = (
729
+ collection.find(filter_doc)
730
+ .sort([("importance_score", -1), ("created_at", -1)])
731
+ .limit(limit)
732
+ )
733
+
734
+ results = []
735
+ for document in cursor:
736
+ results.append(self._convert_to_dict(document))
737
+
738
+ logger.debug(f"Retrieved {len(results)} short-term memory entries")
739
+ return results
740
+
741
+ except Exception as e:
742
+ logger.error(f"Failed to get short-term memory: {e}")
743
+ return []
744
+
745
+ def search_short_term_memory(
746
+ self,
747
+ query: str,
748
+ namespace: str = "default",
749
+ limit: int = 10,
750
+ ) -> list[dict[str, Any]]:
751
+ """Search short-term memory using MongoDB text search"""
752
+ try:
753
+ # Clean the query to remove common prefixes that interfere with search
754
+ cleaned_query = query.strip()
755
+
756
+ # Remove "User query:" prefix if present (this was causing search failures)
757
+ if cleaned_query.lower().startswith("user query:"):
758
+ cleaned_query = cleaned_query[11:].strip()
759
+ logger.debug(
760
+ f"Cleaned short-term search query from '{query}' to '{cleaned_query}'"
761
+ )
762
+
763
+ if not cleaned_query:
764
+ logger.debug(
765
+ "Empty query provided for short-term search, returning all short-term memories"
766
+ )
767
+ return self.get_short_term_memory(namespace=namespace, limit=limit)
768
+
769
+ collection = self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION)
770
+
771
+ current_time = datetime.now(timezone.utc)
772
+ search_filter = {
773
+ "$and": [
774
+ {"$text": {"$search": cleaned_query}}, # Use cleaned query
775
+ {"namespace": namespace},
776
+ {
777
+ "$or": [
778
+ {"expires_at": {"$exists": False}},
779
+ {"expires_at": None},
780
+ {"expires_at": {"$gt": current_time}},
781
+ ]
782
+ },
783
+ ]
784
+ }
785
+
786
+ logger.debug(
787
+ f"Executing short-term MongoDB text search with cleaned query '{cleaned_query}' and filter: {search_filter}"
788
+ )
789
+
790
+ # Execute MongoDB text search with text score projection
791
+ cursor = (
792
+ collection.find(search_filter, {"score": {"$meta": "textScore"}})
793
+ .sort(
794
+ [
795
+ ("score", {"$meta": "textScore"}),
796
+ ("importance_score", -1),
797
+ ("created_at", -1),
798
+ ]
799
+ )
800
+ .limit(limit)
801
+ )
802
+
803
+ results = []
804
+ for document in cursor:
805
+ memory = self._convert_to_dict(document)
806
+ memory["memory_type"] = "short_term"
807
+ memory["search_strategy"] = "mongodb_text"
808
+ # Preserve text search score
809
+ if "score" in document:
810
+ memory["text_score"] = document["score"]
811
+ results.append(memory)
812
+
813
+ logger.debug(
814
+ f"Short-term memory search returned {len(results)} results for query: '{query}'"
815
+ )
816
+ return results
817
+
818
+ except Exception as e:
819
+ logger.error(f"Short-term memory search failed: {e}")
820
+ return []
821
+
822
+ def update_short_term_memory_access(
823
+ self, memory_id: str, namespace: str = "default"
824
+ ):
825
+ """Update access count and last accessed time for short-term memory"""
826
+ try:
827
+ collection = self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION)
828
+
829
+ collection.update_one(
830
+ {"memory_id": memory_id, "namespace": namespace},
831
+ {
832
+ "$inc": {"access_count": 1},
833
+ "$set": {"last_accessed": datetime.now(timezone.utc)},
834
+ },
835
+ )
836
+
837
+ except Exception as e:
838
+ logger.debug(f"Failed to update short-term memory access: {e}")
839
+
840
+ def get_conscious_memories(
841
+ self,
842
+ namespace: str = "default",
843
+ processed_only: bool = False,
844
+ ) -> list[dict[str, Any]]:
845
+ """Get conscious-info labeled memories from long-term memory"""
846
+ try:
847
+ collection = self._get_collection(self.LONG_TERM_MEMORY_COLLECTION)
848
+
849
+ # Build filter for conscious-info classification
850
+ filter_doc = {"namespace": namespace, "classification": "conscious-info"}
851
+
852
+ if processed_only:
853
+ # Get only processed memories
854
+ filter_doc["conscious_processed"] = True
855
+ else:
856
+ # Get ALL conscious-info memories regardless of processed status
857
+ # This is the correct behavior for initial conscious ingestion
858
+ pass # No additional filter needed
859
+
860
+ # Execute query
861
+ cursor = collection.find(filter_doc).sort(
862
+ [("importance_score", -1), ("created_at", -1)]
863
+ )
864
+
865
+ results = []
866
+ for document in cursor:
867
+ results.append(self._convert_to_dict(document))
868
+
869
+ logger.debug(f"Retrieved {len(results)} conscious memories")
870
+ return results
871
+
872
+ except Exception as e:
873
+ logger.error(f"Failed to get conscious memories: {e}")
874
+ return []
875
+
876
+ def get_unprocessed_conscious_memories(
877
+ self,
878
+ namespace: str = "default",
879
+ ) -> list[dict[str, Any]]:
880
+ """Get unprocessed conscious-info labeled memories from long-term memory"""
881
+ try:
882
+ collection = self._get_collection(self.LONG_TERM_MEMORY_COLLECTION)
883
+
884
+ # Build filter for unprocessed conscious-info memories
885
+ filter_doc = {
886
+ "namespace": namespace,
887
+ "classification": "conscious-info",
888
+ "$or": [
889
+ {"conscious_processed": False},
890
+ {"conscious_processed": {"$exists": False}},
891
+ {"conscious_processed": None},
892
+ ],
893
+ }
894
+
895
+ # Execute query
896
+ cursor = collection.find(filter_doc).sort(
897
+ [("importance_score", -1), ("created_at", -1)]
898
+ )
899
+
900
+ results = []
901
+ for document in cursor:
902
+ results.append(self._convert_to_dict(document))
903
+
904
+ logger.debug(f"Retrieved {len(results)} unprocessed conscious memories")
905
+ return results
906
+
907
+ except Exception as e:
908
+ logger.error(f"Failed to get unprocessed conscious memories: {e}")
909
+ return []
910
+
911
+ def mark_conscious_memories_processed(
912
+ self, memory_ids: list[str], namespace: str = "default"
913
+ ):
914
+ """Mark conscious memories as processed"""
915
+ try:
916
+ collection = self._get_collection(self.LONG_TERM_MEMORY_COLLECTION)
917
+
918
+ # Update all memories in the list
919
+ result = collection.update_many(
920
+ {"memory_id": {"$in": memory_ids}, "namespace": namespace},
921
+ {"$set": {"conscious_processed": True}},
922
+ )
923
+
924
+ logger.debug(
925
+ f"Marked {result.modified_count} memories as conscious processed"
926
+ )
927
+
928
+ except Exception as e:
929
+ logger.error(f"Failed to mark conscious memories processed: {e}")
930
+
931
+ def store_long_term_memory_enhanced(
932
+ self, memory: ProcessedLongTermMemory, chat_id: str, namespace: str = "default"
933
+ ) -> str:
934
+ """Store a ProcessedLongTermMemory in MongoDB with enhanced schema"""
935
+ memory_id = str(uuid.uuid4())
936
+
937
+ try:
938
+ collection = self._get_collection(self.LONG_TERM_MEMORY_COLLECTION)
939
+
940
+ # Enrich searchable content with keywords and entities for better search
941
+ enriched_content_parts = [memory.content]
942
+
943
+ # Add summary for richer search content
944
+ if memory.summary and memory.summary.strip():
945
+ enriched_content_parts.append(memory.summary)
946
+
947
+ # Add keywords to searchable content
948
+ if memory.keywords:
949
+ keyword_text = " ".join(memory.keywords)
950
+ enriched_content_parts.append(keyword_text)
951
+
952
+ # Add entities to searchable content
953
+ if memory.entities:
954
+ entity_text = " ".join(memory.entities)
955
+ enriched_content_parts.append(entity_text)
956
+
957
+ # Create enriched searchable content
958
+ enriched_searchable_content = " ".join(enriched_content_parts)
959
+
960
+ # Convert Pydantic model to MongoDB document
961
+ document = {
962
+ "memory_id": memory_id,
963
+ "original_chat_id": chat_id,
964
+ "processed_data": memory.model_dump(mode="json"),
965
+ "importance_score": memory.importance_score,
966
+ "category_primary": memory.classification.value,
967
+ "retention_type": "long_term",
968
+ "namespace": namespace,
969
+ "created_at": datetime.now(timezone.utc),
970
+ "searchable_content": enriched_searchable_content,
971
+ "summary": memory.summary,
972
+ "novelty_score": 0.5,
973
+ "relevance_score": 0.5,
974
+ "actionability_score": 0.5,
975
+ "classification": memory.classification.value,
976
+ "memory_importance": memory.importance.value,
977
+ "topic": memory.topic,
978
+ "entities_json": memory.entities,
979
+ "keywords_json": memory.keywords,
980
+ "is_user_context": memory.is_user_context,
981
+ "is_preference": memory.is_preference,
982
+ "is_skill_knowledge": memory.is_skill_knowledge,
983
+ "is_current_project": memory.is_current_project,
984
+ "promotion_eligible": memory.promotion_eligible,
985
+ "duplicate_of": memory.duplicate_of,
986
+ "supersedes_json": memory.supersedes,
987
+ "related_memories_json": memory.related_memories,
988
+ "confidence_score": memory.confidence_score,
989
+ "extraction_timestamp": memory.extraction_timestamp,
990
+ "classification_reason": memory.classification_reason,
991
+ "processed_for_duplicates": False,
992
+ "conscious_processed": False, # Ensure new memories start as unprocessed
993
+ "access_count": 0,
994
+ }
995
+
996
+ # Convert datetime fields
997
+ document = self._convert_datetime_fields(document)
998
+
999
+ # Insert document
1000
+ collection.insert_one(document)
1001
+
1002
+ logger.debug(f"Stored enhanced long-term memory {memory_id}")
1003
+ return memory_id
1004
+
1005
+ except Exception as e:
1006
+ logger.error(f"Failed to store enhanced long-term memory: {e}")
1007
+ raise DatabaseError(f"Failed to store enhanced long-term memory: {e}")
1008
+
1009
+ def search_memories(
1010
+ self,
1011
+ query: str,
1012
+ namespace: str = "default",
1013
+ category_filter: list[str] | None = None,
1014
+ limit: int = 10,
1015
+ ) -> list[dict[str, Any]]:
1016
+ """Search memories using MongoDB text search with SQL-compatible interface"""
1017
+ try:
1018
+ logger.debug(
1019
+ f"MongoDB search_memories called: query='{query}', namespace='{namespace}', limit={limit}"
1020
+ )
1021
+
1022
+ # Handle empty queries consistently with SQL
1023
+ if not query or not query.strip():
1024
+ logger.debug(
1025
+ "Empty query provided, returning empty results for consistency"
1026
+ )
1027
+ return []
1028
+
1029
+ # Clean query (remove common problematic prefixes)
1030
+ cleaned_query = query.strip()
1031
+ if cleaned_query.lower().startswith("user query:"):
1032
+ cleaned_query = cleaned_query[11:].strip()
1033
+ logger.debug(f"Cleaned query from '{query}' to '{cleaned_query}'")
1034
+
1035
+ if not cleaned_query:
1036
+ return []
1037
+
1038
+ results = []
1039
+ collections_to_search = [
1040
+ (self.SHORT_TERM_MEMORY_COLLECTION, "short_term"),
1041
+ (self.LONG_TERM_MEMORY_COLLECTION, "long_term"),
1042
+ ]
1043
+
1044
+ # Search each collection
1045
+ for collection_name, memory_type in collections_to_search:
1046
+ collection = self._get_collection(collection_name)
1047
+
1048
+ try:
1049
+ # Build search filter
1050
+ search_filter: dict[str, Any] = {
1051
+ "$text": {"$search": cleaned_query},
1052
+ "namespace": namespace,
1053
+ }
1054
+
1055
+ # Add category filter if specified
1056
+ if category_filter:
1057
+ search_filter["category_primary"] = {"$in": category_filter}
1058
+
1059
+ # For short-term memories, exclude expired ones
1060
+ if memory_type == "short_term":
1061
+ current_time = datetime.now(timezone.utc)
1062
+ search_filter = {
1063
+ "$and": [
1064
+ {"$text": {"$search": cleaned_query}},
1065
+ {"namespace": namespace},
1066
+ {
1067
+ "$or": [
1068
+ {"expires_at": {"$exists": False}},
1069
+ {"expires_at": None},
1070
+ {"expires_at": {"$gt": current_time}},
1071
+ ]
1072
+ },
1073
+ ]
1074
+ }
1075
+ if category_filter:
1076
+ search_filter["$and"].append(
1077
+ {"category_primary": {"$in": category_filter}}
1078
+ )
1079
+
1080
+ # Execute search with standardized projection
1081
+ cursor = (
1082
+ collection.find(
1083
+ search_filter, {"score": {"$meta": "textScore"}}
1084
+ )
1085
+ .sort(
1086
+ [
1087
+ ("score", {"$meta": "textScore"}),
1088
+ ("importance_score", -1),
1089
+ ("created_at", -1),
1090
+ ]
1091
+ )
1092
+ .limit(limit)
1093
+ )
1094
+
1095
+ for document in cursor:
1096
+ memory = self._convert_to_dict(document)
1097
+
1098
+ # Standardize fields for SQL compatibility
1099
+ memory["memory_type"] = memory_type
1100
+ memory["search_strategy"] = "mongodb_text"
1101
+ memory["search_score"] = document.get(
1102
+ "score", 0.8
1103
+ ) # MongoDB text score
1104
+
1105
+ # Ensure all required fields are present
1106
+ if "importance_score" not in memory:
1107
+ memory["importance_score"] = 0.5
1108
+ if "created_at" not in memory:
1109
+ memory["created_at"] = datetime.now(
1110
+ timezone.utc
1111
+ ).isoformat()
1112
+
1113
+ results.append(memory)
1114
+
1115
+ except Exception as search_error:
1116
+ logger.error(
1117
+ f"MongoDB search failed for {collection_name}: {search_error}"
1118
+ )
1119
+ continue
1120
+
1121
+ # Sort results by search score for consistency
1122
+ results.sort(
1123
+ key=lambda x: (x.get("search_score", 0), x.get("importance_score", 0)),
1124
+ reverse=True,
1125
+ )
1126
+
1127
+ logger.debug(f"MongoDB search returned {len(results)} results")
1128
+ return results[:limit]
1129
+
1130
+ except Exception as e:
1131
+ logger.error(f"MongoDB search_memories failed: {e}")
1132
+ # Return empty list to maintain compatibility with SQL manager
1133
+ return []
1134
+
1135
+ def get_memory_stats(self, namespace: str = "default") -> dict[str, Any]:
1136
+ """Get comprehensive memory statistics"""
1137
+ try:
1138
+ database = self._get_database()
1139
+
1140
+ stats = {}
1141
+
1142
+ # Basic counts
1143
+ stats["chat_history_count"] = self._get_collection(
1144
+ self.CHAT_HISTORY_COLLECTION
1145
+ ).count_documents({"namespace": namespace})
1146
+
1147
+ stats["short_term_count"] = self._get_collection(
1148
+ self.SHORT_TERM_MEMORY_COLLECTION
1149
+ ).count_documents({"namespace": namespace})
1150
+
1151
+ stats["long_term_count"] = self._get_collection(
1152
+ self.LONG_TERM_MEMORY_COLLECTION
1153
+ ).count_documents({"namespace": namespace})
1154
+
1155
+ # Category breakdown for short-term memories
1156
+ short_categories = self._get_collection(
1157
+ self.SHORT_TERM_MEMORY_COLLECTION
1158
+ ).aggregate(
1159
+ [
1160
+ {"$match": {"namespace": namespace}},
1161
+ {"$group": {"_id": "$category_primary", "count": {"$sum": 1}}},
1162
+ ]
1163
+ )
1164
+
1165
+ categories = {}
1166
+ for doc in short_categories:
1167
+ categories[doc["_id"]] = doc["count"]
1168
+
1169
+ # Category breakdown for long-term memories
1170
+ long_categories = self._get_collection(
1171
+ self.LONG_TERM_MEMORY_COLLECTION
1172
+ ).aggregate(
1173
+ [
1174
+ {"$match": {"namespace": namespace}},
1175
+ {"$group": {"_id": "$category_primary", "count": {"$sum": 1}}},
1176
+ ]
1177
+ )
1178
+
1179
+ for doc in long_categories:
1180
+ categories[doc.get("_id", "unknown")] = (
1181
+ categories.get(doc.get("_id", "unknown"), 0) + doc["count"]
1182
+ )
1183
+
1184
+ stats["memories_by_category"] = categories
1185
+
1186
+ # Average importance scores
1187
+ short_avg_pipeline = [
1188
+ {"$match": {"namespace": namespace}},
1189
+ {
1190
+ "$group": {
1191
+ "_id": None,
1192
+ "avg_importance": {"$avg": "$importance_score"},
1193
+ }
1194
+ },
1195
+ ]
1196
+ short_avg_result = list(
1197
+ self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION).aggregate(
1198
+ short_avg_pipeline
1199
+ )
1200
+ )
1201
+ short_avg = short_avg_result[0]["avg_importance"] if short_avg_result else 0
1202
+
1203
+ long_avg_pipeline = [
1204
+ {"$match": {"namespace": namespace}},
1205
+ {
1206
+ "$group": {
1207
+ "_id": None,
1208
+ "avg_importance": {"$avg": "$importance_score"},
1209
+ }
1210
+ },
1211
+ ]
1212
+ long_avg_result = list(
1213
+ self._get_collection(self.LONG_TERM_MEMORY_COLLECTION).aggregate(
1214
+ long_avg_pipeline
1215
+ )
1216
+ )
1217
+ long_avg = long_avg_result[0]["avg_importance"] if long_avg_result else 0
1218
+
1219
+ total_memories = stats["short_term_count"] + stats["long_term_count"]
1220
+ if total_memories > 0:
1221
+ # Weight averages by count
1222
+ total_avg = (
1223
+ (short_avg * stats["short_term_count"])
1224
+ + (long_avg * stats["long_term_count"])
1225
+ ) / total_memories
1226
+ stats["average_importance"] = float(total_avg) if total_avg else 0.0
1227
+ else:
1228
+ stats["average_importance"] = 0.0
1229
+
1230
+ # Database info
1231
+ stats["database_type"] = self.database_type
1232
+ stats["database_url"] = (
1233
+ self.database_connect.split("@")[-1]
1234
+ if "@" in self.database_connect
1235
+ else self.database_connect
1236
+ )
1237
+
1238
+ # MongoDB-specific stats
1239
+ try:
1240
+ db_stats = database.command("dbStats")
1241
+ stats["storage_size"] = db_stats.get("storageSize", 0)
1242
+ stats["data_size"] = db_stats.get("dataSize", 0)
1243
+ stats["index_size"] = db_stats.get("indexSize", 0)
1244
+ stats["collections"] = db_stats.get("collections", 0)
1245
+ except Exception as e:
1246
+ logger.debug(f"Could not get database stats: {e}")
1247
+
1248
+ return stats
1249
+
1250
+ except Exception as e:
1251
+ logger.error(f"Failed to get memory stats: {e}")
1252
+ return {"error": str(e)}
1253
+
1254
+ def clear_memory(self, namespace: str = "default", memory_type: str | None = None):
1255
+ """Clear memory data"""
1256
+ try:
1257
+ if memory_type == "short_term":
1258
+ self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION).delete_many(
1259
+ {"namespace": namespace}
1260
+ )
1261
+ elif memory_type == "long_term":
1262
+ self._get_collection(self.LONG_TERM_MEMORY_COLLECTION).delete_many(
1263
+ {"namespace": namespace}
1264
+ )
1265
+ elif memory_type == "chat_history":
1266
+ self._get_collection(self.CHAT_HISTORY_COLLECTION).delete_many(
1267
+ {"namespace": namespace}
1268
+ )
1269
+ else: # Clear all
1270
+ self._get_collection(self.SHORT_TERM_MEMORY_COLLECTION).delete_many(
1271
+ {"namespace": namespace}
1272
+ )
1273
+ self._get_collection(self.LONG_TERM_MEMORY_COLLECTION).delete_many(
1274
+ {"namespace": namespace}
1275
+ )
1276
+ self._get_collection(self.CHAT_HISTORY_COLLECTION).delete_many(
1277
+ {"namespace": namespace}
1278
+ )
1279
+
1280
+ logger.info(
1281
+ f"Cleared {memory_type or 'all'} memory for namespace: {namespace}"
1282
+ )
1283
+
1284
+ except Exception as e:
1285
+ logger.error(f"Failed to clear memory: {e}")
1286
+ raise DatabaseError(f"Failed to clear memory: {e}")
1287
+
1288
+ def _get_connection(self):
1289
+ """
1290
+ Compatibility method for legacy code that expects raw database connections.
1291
+ Returns a MongoDB-compatible connection wrapper.
1292
+ """
1293
+ from contextlib import contextmanager
1294
+
1295
+ @contextmanager
1296
+ def connection_context():
1297
+ class MongoDBConnection:
1298
+ """Wrapper that provides SQLAlchemy-like interface for MongoDB"""
1299
+
1300
+ def __init__(self, manager):
1301
+ self.manager = manager
1302
+ self.database = manager._get_database()
1303
+
1304
+ def execute(self, query, parameters=None):
1305
+ """Execute query with parameter substitution"""
1306
+ try:
1307
+ # This is a compatibility shim for raw SQL-like queries
1308
+ # Convert basic queries to MongoDB operations
1309
+ if isinstance(query, str):
1310
+ # Handle common SQL-like patterns and convert to MongoDB
1311
+ if "SELECT" in query.upper():
1312
+ return self._handle_select_query(query, parameters)
1313
+ elif "INSERT" in query.upper():
1314
+ return self._handle_insert_query(query, parameters)
1315
+ elif "UPDATE" in query.upper():
1316
+ return self._handle_update_query(query, parameters)
1317
+ elif "DELETE" in query.upper():
1318
+ return self._handle_delete_query(query, parameters)
1319
+
1320
+ # Fallback for direct MongoDB operations
1321
+ return MockQueryResult([])
1322
+
1323
+ except Exception as e:
1324
+ logger.warning(f"Query execution failed: {e}")
1325
+ return MockQueryResult([])
1326
+
1327
+ def _handle_select_query(self, query, parameters):
1328
+ """Handle SELECT-like queries"""
1329
+ # Simple pattern matching for common queries
1330
+ if "short_term_memory" in query:
1331
+ collection = self.manager._get_collection(
1332
+ self.manager.SHORT_TERM_MEMORY_COLLECTION
1333
+ )
1334
+ filter_doc = {}
1335
+ if parameters:
1336
+ # Basic parameter substitution
1337
+ if "namespace" in parameters:
1338
+ filter_doc["namespace"] = parameters["namespace"]
1339
+
1340
+ cursor = (
1341
+ collection.find(filter_doc)
1342
+ .sort("created_at", -1)
1343
+ .limit(100)
1344
+ )
1345
+ results = [self.manager._convert_to_dict(doc) for doc in cursor]
1346
+ return MockQueryResult(results)
1347
+
1348
+ return MockQueryResult([])
1349
+
1350
+ def _handle_insert_query(self, _query, _parameters):
1351
+ """Handle INSERT-like queries"""
1352
+ # This is a compatibility shim - not fully implemented
1353
+ return MockQueryResult([])
1354
+
1355
+ def _handle_update_query(self, _query, _parameters):
1356
+ """Handle UPDATE-like queries"""
1357
+ # This is a compatibility shim - not fully implemented
1358
+ return MockQueryResult([])
1359
+
1360
+ def _handle_delete_query(self, _query, _parameters):
1361
+ """Handle DELETE-like queries"""
1362
+ # This is a compatibility shim - not fully implemented
1363
+ return MockQueryResult([])
1364
+
1365
+ def commit(self):
1366
+ """Commit transaction (no-op for MongoDB single operations)"""
1367
+ pass
1368
+
1369
+ def rollback(self):
1370
+ """Rollback transaction (no-op for MongoDB single operations)"""
1371
+ pass
1372
+
1373
+ def close(self):
1374
+ """Close connection (no-op, connection pooling handled by client)"""
1375
+ pass
1376
+
1377
+ def scalar(self):
1378
+ """Compatibility method"""
1379
+ return None
1380
+
1381
+ def fetchall(self):
1382
+ """Compatibility method"""
1383
+ return []
1384
+
1385
+ yield MongoDBConnection(self)
1386
+
1387
+ return connection_context()
1388
+
1389
+ def close(self):
1390
+ """Close MongoDB connection"""
1391
+ if self.client:
1392
+ self.client.close()
1393
+ self.client = None
1394
+ self.database = None
1395
+ self._collections.clear()
1396
+ logger.info("MongoDB connection closed")
1397
+
1398
+ def get_database_info(self) -> dict[str, Any]:
1399
+ """Get MongoDB database information and capabilities"""
1400
+ try:
1401
+ client = self._get_client()
1402
+ database = self._get_database()
1403
+
1404
+ info = {
1405
+ "database_type": self.database_type,
1406
+ "database_name": self.database_name,
1407
+ "connection_string": (
1408
+ self.database_connect.replace(
1409
+ f"{self.username}:{self.password}@", "***:***@"
1410
+ )
1411
+ if self.username and self.password
1412
+ else self.database_connect
1413
+ ),
1414
+ }
1415
+
1416
+ # Server information
1417
+ try:
1418
+ server_info = client.server_info()
1419
+ info["version"] = server_info.get("version", "unknown")
1420
+ info["driver"] = "pymongo"
1421
+ except Exception:
1422
+ info["version"] = "unknown"
1423
+ info["driver"] = "pymongo"
1424
+
1425
+ # Database stats
1426
+ try:
1427
+ stats = database.command("dbStats")
1428
+ info["collections_count"] = stats.get("collections", 0)
1429
+ info["data_size"] = stats.get("dataSize", 0)
1430
+ info["storage_size"] = stats.get("storageSize", 0)
1431
+ info["indexes_count"] = stats.get("indexes", 0)
1432
+ except Exception:
1433
+ pass
1434
+
1435
+ # Capabilities
1436
+ info["supports_fulltext"] = True
1437
+ info["auto_creation_enabled"] = (
1438
+ True # MongoDB creates collections automatically
1439
+ )
1440
+
1441
+ return info
1442
+
1443
+ except Exception as e:
1444
+ logger.warning(f"Could not get MongoDB database info: {e}")
1445
+ return {
1446
+ "database_type": self.database_type,
1447
+ "version": "unknown",
1448
+ "supports_fulltext": True,
1449
+ "error": str(e),
1450
+ }
1451
+
1452
+
1453
+ class MockQueryResult:
1454
+ """Mock query result for compatibility with SQLAlchemy-style code"""
1455
+
1456
+ def __init__(self, results):
1457
+ self.results = results
1458
+ self._index = 0
1459
+
1460
+ def fetchall(self):
1461
+ """Return all results"""
1462
+ return self.results
1463
+
1464
+ def fetchone(self):
1465
+ """Return one result"""
1466
+ if self._index < len(self.results):
1467
+ result = self.results[self._index]
1468
+ self._index += 1
1469
+ return result
1470
+ return None
1471
+
1472
+ def scalar(self):
1473
+ """Return scalar value"""
1474
+ if self.results:
1475
+ first_result = self.results[0]
1476
+ if isinstance(first_result, dict):
1477
+ # Return first value from dict
1478
+ return next(iter(first_result.values()))
1479
+ return first_result
1480
+ return None
1481
+
1482
+ def __iter__(self):
1483
+ """Make iterable"""
1484
+ return iter(self.results)