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.
- memori/__init__.py +3 -3
- memori/agents/conscious_agent.py +289 -77
- memori/agents/memory_agent.py +19 -9
- memori/agents/retrieval_agent.py +59 -51
- memori/config/manager.py +7 -7
- memori/config/memory_manager.py +25 -25
- memori/config/settings.py +13 -6
- memori/core/conversation.py +15 -15
- memori/core/database.py +14 -13
- memori/core/memory.py +376 -105
- memori/core/providers.py +25 -25
- memori/database/__init__.py +11 -0
- memori/database/adapters/__init__.py +11 -0
- memori/database/adapters/mongodb_adapter.py +739 -0
- memori/database/adapters/mysql_adapter.py +8 -8
- memori/database/adapters/postgresql_adapter.py +6 -6
- memori/database/adapters/sqlite_adapter.py +6 -6
- memori/database/auto_creator.py +8 -9
- memori/database/connection_utils.py +5 -5
- memori/database/connectors/__init__.py +11 -0
- memori/database/connectors/base_connector.py +18 -19
- memori/database/connectors/mongodb_connector.py +654 -0
- memori/database/connectors/mysql_connector.py +13 -15
- memori/database/connectors/postgres_connector.py +12 -12
- memori/database/connectors/sqlite_connector.py +11 -11
- memori/database/models.py +2 -2
- memori/database/mongodb_manager.py +1484 -0
- memori/database/queries/base_queries.py +3 -4
- memori/database/queries/chat_queries.py +3 -5
- memori/database/queries/entity_queries.py +3 -5
- memori/database/queries/memory_queries.py +3 -5
- memori/database/query_translator.py +11 -11
- memori/database/schema_generators/__init__.py +11 -0
- memori/database/schema_generators/mongodb_schema_generator.py +666 -0
- memori/database/schema_generators/mysql_schema_generator.py +2 -4
- memori/database/search/__init__.py +11 -0
- memori/database/search/mongodb_search_adapter.py +653 -0
- memori/database/search/mysql_search_adapter.py +8 -8
- memori/database/search/sqlite_search_adapter.py +6 -6
- memori/database/search_service.py +17 -17
- memori/database/sqlalchemy_manager.py +10 -12
- memori/integrations/__init__.py +1 -1
- memori/integrations/anthropic_integration.py +1 -3
- memori/integrations/litellm_integration.py +23 -6
- memori/integrations/openai_integration.py +31 -3
- memori/tools/memory_tool.py +10 -9
- memori/utils/exceptions.py +58 -58
- memori/utils/helpers.py +11 -12
- memori/utils/input_validator.py +10 -12
- memori/utils/logging.py +4 -4
- memori/utils/pydantic_models.py +57 -57
- memori/utils/query_builder.py +20 -20
- memori/utils/security_audit.py +28 -28
- memori/utils/security_integration.py +9 -9
- memori/utils/transaction_manager.py +20 -19
- memori/utils/validators.py +6 -6
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/METADATA +23 -12
- memorisdk-2.1.1.dist-info/RECORD +71 -0
- memorisdk-2.0.1.dist-info/RECORD +0 -66
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/WHEEL +0 -0
- {memorisdk-2.0.1.dist-info → memorisdk-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {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)
|