memorisdk 1.0.1__py3-none-any.whl → 2.0.0__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 (46) hide show
  1. memori/__init__.py +24 -8
  2. memori/agents/conscious_agent.py +252 -414
  3. memori/agents/memory_agent.py +487 -224
  4. memori/agents/retrieval_agent.py +416 -60
  5. memori/config/memory_manager.py +323 -0
  6. memori/core/conversation.py +393 -0
  7. memori/core/database.py +386 -371
  8. memori/core/memory.py +1676 -534
  9. memori/core/providers.py +217 -0
  10. memori/database/adapters/__init__.py +10 -0
  11. memori/database/adapters/mysql_adapter.py +331 -0
  12. memori/database/adapters/postgresql_adapter.py +291 -0
  13. memori/database/adapters/sqlite_adapter.py +229 -0
  14. memori/database/auto_creator.py +320 -0
  15. memori/database/connection_utils.py +207 -0
  16. memori/database/connectors/base_connector.py +283 -0
  17. memori/database/connectors/mysql_connector.py +240 -18
  18. memori/database/connectors/postgres_connector.py +277 -4
  19. memori/database/connectors/sqlite_connector.py +178 -3
  20. memori/database/models.py +400 -0
  21. memori/database/queries/base_queries.py +1 -1
  22. memori/database/queries/memory_queries.py +91 -2
  23. memori/database/query_translator.py +222 -0
  24. memori/database/schema_generators/__init__.py +7 -0
  25. memori/database/schema_generators/mysql_schema_generator.py +215 -0
  26. memori/database/search/__init__.py +8 -0
  27. memori/database/search/mysql_search_adapter.py +255 -0
  28. memori/database/search/sqlite_search_adapter.py +180 -0
  29. memori/database/search_service.py +548 -0
  30. memori/database/sqlalchemy_manager.py +839 -0
  31. memori/integrations/__init__.py +36 -11
  32. memori/integrations/litellm_integration.py +340 -6
  33. memori/integrations/openai_integration.py +506 -240
  34. memori/utils/input_validator.py +395 -0
  35. memori/utils/pydantic_models.py +138 -36
  36. memori/utils/query_builder.py +530 -0
  37. memori/utils/security_audit.py +594 -0
  38. memori/utils/security_integration.py +339 -0
  39. memori/utils/transaction_manager.py +547 -0
  40. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/METADATA +144 -34
  41. memorisdk-2.0.0.dist-info/RECORD +67 -0
  42. memorisdk-1.0.1.dist-info/RECORD +0 -44
  43. memorisdk-1.0.1.dist-info/entry_points.txt +0 -2
  44. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
  45. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,839 @@
1
+ """
2
+ SQLAlchemy-based database manager for Memori v2.0
3
+ Replaces the existing database.py with cross-database compatibility
4
+ """
5
+
6
+ import importlib.util
7
+ import json
8
+ import ssl
9
+ import uuid
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+ from urllib.parse import parse_qs, urlparse
14
+
15
+ from loguru import logger
16
+ from sqlalchemy import create_engine, func, text
17
+ from sqlalchemy.exc import SQLAlchemyError
18
+ from sqlalchemy.orm import sessionmaker
19
+
20
+ from ..utils.exceptions import DatabaseError
21
+ from ..utils.pydantic_models import (
22
+ ProcessedLongTermMemory,
23
+ )
24
+ from .auto_creator import DatabaseAutoCreator
25
+ from .models import (
26
+ Base,
27
+ ChatHistory,
28
+ LongTermMemory,
29
+ ShortTermMemory,
30
+ )
31
+ from .query_translator import QueryParameterTranslator
32
+ from .search_service import SearchService
33
+
34
+
35
+ class SQLAlchemyDatabaseManager:
36
+ """SQLAlchemy-based database manager with cross-database support"""
37
+
38
+ def __init__(
39
+ self, database_connect: str, template: str = "basic", schema_init: bool = True
40
+ ):
41
+ self.database_connect = database_connect
42
+ self.template = template
43
+ self.schema_init = schema_init
44
+
45
+ # Initialize database auto-creator
46
+ self.auto_creator = DatabaseAutoCreator(schema_init)
47
+
48
+ # Ensure database exists (create if necessary)
49
+ self.database_connect = self.auto_creator.ensure_database_exists(
50
+ database_connect
51
+ )
52
+
53
+ # Parse connection string and create engine
54
+ self.engine = self._create_engine(self.database_connect)
55
+ self.database_type = self.engine.dialect.name
56
+
57
+ # Create session factory
58
+ self.SessionLocal = sessionmaker(bind=self.engine)
59
+
60
+ # Initialize search service
61
+ self._search_service = None
62
+
63
+ # Initialize query parameter translator for cross-database compatibility
64
+ self.query_translator = QueryParameterTranslator(self.database_type)
65
+
66
+ logger.info(f"Initialized SQLAlchemy database manager for {self.database_type}")
67
+
68
+ def _validate_database_dependencies(self, database_connect: str):
69
+ """Validate that required database drivers are installed"""
70
+ if database_connect.startswith("mysql:") or database_connect.startswith(
71
+ "mysql+"
72
+ ):
73
+ # Check for MySQL drivers
74
+ mysql_drivers = []
75
+
76
+ if (
77
+ "mysqlconnector" in database_connect
78
+ or "mysql+mysqlconnector" in database_connect
79
+ ):
80
+ if importlib.util.find_spec("mysql.connector") is not None:
81
+ mysql_drivers.append("mysql-connector-python")
82
+
83
+ if "pymysql" in database_connect:
84
+ if importlib.util.find_spec("pymysql") is not None:
85
+ mysql_drivers.append("PyMySQL")
86
+
87
+ # If using generic mysql:// try both drivers
88
+ if database_connect.startswith("mysql://"):
89
+ if importlib.util.find_spec("mysql.connector") is not None:
90
+ mysql_drivers.append("mysql-connector-python")
91
+ if importlib.util.find_spec("pymysql") is not None:
92
+ mysql_drivers.append("PyMySQL")
93
+
94
+ if not mysql_drivers:
95
+ error_msg = (
96
+ "❌ No MySQL driver found. Install one of the following:\n\n"
97
+ "Option 1 (Recommended): pip install mysql-connector-python\n"
98
+ "Option 2: pip install PyMySQL\n"
99
+ "Option 3: pip install memorisdk[mysql]\n\n"
100
+ "Then update your connection string:\n"
101
+ "- For mysql-connector-python: mysql+mysqlconnector://user:pass@host:port/db\n"
102
+ "- For PyMySQL: mysql+pymysql://user:pass@host:port/db"
103
+ )
104
+ raise DatabaseError(error_msg)
105
+
106
+ elif database_connect.startswith("postgresql:") or database_connect.startswith(
107
+ "postgresql+"
108
+ ):
109
+ # Check for PostgreSQL drivers
110
+ if (
111
+ importlib.util.find_spec("psycopg2") is None
112
+ and importlib.util.find_spec("asyncpg") is None
113
+ ):
114
+ error_msg = (
115
+ "❌ No PostgreSQL driver found. Install one of the following:\n\n"
116
+ "Option 1 (Recommended): pip install psycopg2-binary\n"
117
+ "Option 2: pip install memorisdk[postgres]\n\n"
118
+ "Then use connection string: postgresql://user:pass@host:port/db"
119
+ )
120
+ raise DatabaseError(error_msg)
121
+
122
+ def _create_engine(self, database_connect: str):
123
+ """Create SQLAlchemy engine with appropriate configuration"""
124
+ try:
125
+ # Validate database driver dependencies first
126
+ self._validate_database_dependencies(database_connect)
127
+ # Parse connection string
128
+ if database_connect.startswith("sqlite:"):
129
+ # Ensure directory exists for SQLite
130
+ if ":///" in database_connect:
131
+ db_path = database_connect.replace("sqlite:///", "")
132
+ db_dir = Path(db_path).parent
133
+ db_dir.mkdir(parents=True, exist_ok=True)
134
+
135
+ # SQLite-specific configuration
136
+ engine = create_engine(
137
+ database_connect,
138
+ json_serializer=json.dumps,
139
+ json_deserializer=json.loads,
140
+ echo=False,
141
+ # SQLite-specific options
142
+ connect_args={
143
+ "check_same_thread": False, # Allow multiple threads
144
+ },
145
+ )
146
+
147
+ elif database_connect.startswith("mysql:") or database_connect.startswith(
148
+ "mysql+"
149
+ ):
150
+ # MySQL-specific configuration
151
+ connect_args = {"charset": "utf8mb4"}
152
+
153
+ # Parse URL for SSL parameters
154
+ parsed = urlparse(database_connect)
155
+ if parsed.query:
156
+ query_params = parse_qs(parsed.query)
157
+
158
+ # Handle SSL parameters for PyMySQL - enforce secure transport
159
+ if any(key in query_params for key in ["ssl", "ssl_disabled"]):
160
+ if query_params.get("ssl", ["false"])[0].lower() == "true":
161
+ # Enable SSL with secure configuration for required secure transport
162
+ connect_args["ssl"] = {
163
+ "ssl_disabled": False,
164
+ "check_hostname": False,
165
+ "verify_mode": ssl.CERT_NONE,
166
+ }
167
+ # Also add ssl_disabled=False for PyMySQL
168
+ connect_args["ssl_disabled"] = False
169
+ elif (
170
+ query_params.get("ssl_disabled", ["true"])[0].lower()
171
+ == "false"
172
+ ):
173
+ # Enable SSL with secure configuration for required secure transport
174
+ connect_args["ssl"] = {
175
+ "ssl_disabled": False,
176
+ "check_hostname": False,
177
+ "verify_mode": ssl.CERT_NONE,
178
+ }
179
+ # Also add ssl_disabled=False for PyMySQL
180
+ connect_args["ssl_disabled"] = False
181
+
182
+ # Different args for different MySQL drivers
183
+ if "pymysql" in database_connect:
184
+ # PyMySQL-specific arguments
185
+ connect_args.update(
186
+ {
187
+ "charset": "utf8mb4",
188
+ "autocommit": False,
189
+ }
190
+ )
191
+ elif (
192
+ "mysqlconnector" in database_connect
193
+ or "mysql+mysqlconnector" in database_connect
194
+ ):
195
+ # MySQL Connector/Python-specific arguments
196
+ connect_args.update(
197
+ {
198
+ "charset": "utf8mb4",
199
+ "use_pure": True,
200
+ }
201
+ )
202
+
203
+ engine = create_engine(
204
+ database_connect,
205
+ json_serializer=json.dumps,
206
+ json_deserializer=json.loads,
207
+ echo=False,
208
+ connect_args=connect_args,
209
+ pool_pre_ping=True, # Validate connections
210
+ pool_recycle=3600, # Recycle connections every hour
211
+ )
212
+
213
+ elif database_connect.startswith(
214
+ "postgresql:"
215
+ ) or database_connect.startswith("postgresql+"):
216
+ # PostgreSQL-specific configuration
217
+ engine = create_engine(
218
+ database_connect,
219
+ json_serializer=json.dumps,
220
+ json_deserializer=json.loads,
221
+ echo=False,
222
+ pool_pre_ping=True,
223
+ pool_recycle=3600,
224
+ )
225
+
226
+ else:
227
+ raise DatabaseError(f"Unsupported database type: {database_connect}")
228
+
229
+ # Test connection
230
+ with engine.connect() as conn:
231
+ conn.execute(text("SELECT 1"))
232
+
233
+ return engine
234
+
235
+ except DatabaseError:
236
+ # Re-raise our custom database errors with helpful messages
237
+ raise
238
+ except ModuleNotFoundError as e:
239
+ if "mysql" in str(e).lower():
240
+ error_msg = (
241
+ "❌ MySQL driver not found. Install one of the following:\n\n"
242
+ "Option 1 (Recommended): pip install mysql-connector-python\n"
243
+ "Option 2: pip install PyMySQL\n"
244
+ "Option 3: pip install memorisdk[mysql]\n\n"
245
+ f"Original error: {e}"
246
+ )
247
+ raise DatabaseError(error_msg)
248
+ elif "psycopg" in str(e).lower() or "postgresql" in str(e).lower():
249
+ error_msg = (
250
+ "❌ PostgreSQL driver not found. Install one of the following:\n\n"
251
+ "Option 1 (Recommended): pip install psycopg2-binary\n"
252
+ "Option 2: pip install memorisdk[postgres]\n\n"
253
+ f"Original error: {e}"
254
+ )
255
+ raise DatabaseError(error_msg)
256
+ else:
257
+ raise DatabaseError(f"Missing required dependency: {e}")
258
+ except SQLAlchemyError as e:
259
+ error_msg = f"Database connection failed: {e}\n\nCheck your connection string and ensure the database server is running."
260
+ raise DatabaseError(error_msg)
261
+ except Exception as e:
262
+ raise DatabaseError(f"Failed to create database engine: {e}")
263
+
264
+ def initialize_schema(self):
265
+ """Initialize database schema"""
266
+ try:
267
+ # Create all tables
268
+ Base.metadata.create_all(bind=self.engine)
269
+
270
+ # Setup database-specific features
271
+ self._setup_database_features()
272
+
273
+ logger.info(
274
+ f"Database schema initialized successfully for {self.database_type}"
275
+ )
276
+
277
+ except Exception as e:
278
+ logger.error(f"Failed to initialize schema: {e}")
279
+ raise DatabaseError(f"Failed to initialize schema: {e}")
280
+
281
+ def _setup_database_features(self):
282
+ """Setup database-specific features like full-text search"""
283
+ try:
284
+ with self.engine.connect() as conn:
285
+ if self.database_type == "sqlite":
286
+ self._setup_sqlite_fts(conn)
287
+ elif self.database_type == "mysql":
288
+ self._setup_mysql_fulltext(conn)
289
+ elif self.database_type == "postgresql":
290
+ self._setup_postgresql_fts(conn)
291
+
292
+ conn.commit()
293
+
294
+ except Exception as e:
295
+ logger.warning(f"Failed to setup database-specific features: {e}")
296
+
297
+ def _setup_sqlite_fts(self, conn):
298
+ """Setup SQLite FTS5"""
299
+ try:
300
+ # Create FTS5 virtual table
301
+ conn.execute(
302
+ text(
303
+ """
304
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_search_fts USING fts5(
305
+ memory_id,
306
+ memory_type,
307
+ namespace,
308
+ searchable_content,
309
+ summary,
310
+ category_primary,
311
+ content='',
312
+ contentless_delete=1
313
+ )
314
+ """
315
+ )
316
+ )
317
+
318
+ # Create triggers
319
+ conn.execute(
320
+ text(
321
+ """
322
+ CREATE TRIGGER IF NOT EXISTS short_term_memory_fts_insert AFTER INSERT ON short_term_memory
323
+ BEGIN
324
+ INSERT INTO memory_search_fts(memory_id, memory_type, namespace, searchable_content, summary, category_primary)
325
+ VALUES (NEW.memory_id, 'short_term', NEW.namespace, NEW.searchable_content, NEW.summary, NEW.category_primary);
326
+ END
327
+ """
328
+ )
329
+ )
330
+
331
+ conn.execute(
332
+ text(
333
+ """
334
+ CREATE TRIGGER IF NOT EXISTS long_term_memory_fts_insert AFTER INSERT ON long_term_memory
335
+ BEGIN
336
+ INSERT INTO memory_search_fts(memory_id, memory_type, namespace, searchable_content, summary, category_primary)
337
+ VALUES (NEW.memory_id, 'long_term', NEW.namespace, NEW.searchable_content, NEW.summary, NEW.category_primary);
338
+ END
339
+ """
340
+ )
341
+ )
342
+
343
+ logger.info("SQLite FTS5 setup completed")
344
+
345
+ except Exception as e:
346
+ logger.warning(f"SQLite FTS5 setup failed: {e}")
347
+
348
+ def _setup_mysql_fulltext(self, conn):
349
+ """Setup MySQL FULLTEXT indexes"""
350
+ try:
351
+ # Create FULLTEXT indexes
352
+ conn.execute(
353
+ text(
354
+ "ALTER TABLE short_term_memory ADD FULLTEXT INDEX ft_short_term_search (searchable_content, summary)"
355
+ )
356
+ )
357
+ conn.execute(
358
+ text(
359
+ "ALTER TABLE long_term_memory ADD FULLTEXT INDEX ft_long_term_search (searchable_content, summary)"
360
+ )
361
+ )
362
+
363
+ logger.info("MySQL FULLTEXT indexes setup completed")
364
+
365
+ except Exception as e:
366
+ logger.warning(
367
+ f"MySQL FULLTEXT setup failed (indexes may already exist): {e}"
368
+ )
369
+
370
+ def _setup_postgresql_fts(self, conn):
371
+ """Setup PostgreSQL full-text search"""
372
+ try:
373
+ # Add tsvector columns
374
+ conn.execute(
375
+ text(
376
+ "ALTER TABLE short_term_memory ADD COLUMN IF NOT EXISTS search_vector tsvector"
377
+ )
378
+ )
379
+ conn.execute(
380
+ text(
381
+ "ALTER TABLE long_term_memory ADD COLUMN IF NOT EXISTS search_vector tsvector"
382
+ )
383
+ )
384
+
385
+ # Create GIN indexes
386
+ conn.execute(
387
+ text(
388
+ "CREATE INDEX IF NOT EXISTS idx_short_term_search_vector ON short_term_memory USING GIN(search_vector)"
389
+ )
390
+ )
391
+ conn.execute(
392
+ text(
393
+ "CREATE INDEX IF NOT EXISTS idx_long_term_search_vector ON long_term_memory USING GIN(search_vector)"
394
+ )
395
+ )
396
+
397
+ # Create update functions and triggers
398
+ conn.execute(
399
+ text(
400
+ """
401
+ CREATE OR REPLACE FUNCTION update_short_term_search_vector() RETURNS trigger AS $$
402
+ BEGIN
403
+ NEW.search_vector := to_tsvector('english', COALESCE(NEW.searchable_content, '') || ' ' || COALESCE(NEW.summary, ''));
404
+ RETURN NEW;
405
+ END
406
+ $$ LANGUAGE plpgsql;
407
+ """
408
+ )
409
+ )
410
+
411
+ conn.execute(
412
+ text(
413
+ """
414
+ DROP TRIGGER IF EXISTS update_short_term_search_vector_trigger ON short_term_memory;
415
+ CREATE TRIGGER update_short_term_search_vector_trigger
416
+ BEFORE INSERT OR UPDATE ON short_term_memory
417
+ FOR EACH ROW EXECUTE FUNCTION update_short_term_search_vector();
418
+ """
419
+ )
420
+ )
421
+
422
+ logger.info("PostgreSQL FTS setup completed")
423
+
424
+ except Exception as e:
425
+ logger.warning(f"PostgreSQL FTS setup failed: {e}")
426
+
427
+ def _get_search_service(self) -> SearchService:
428
+ """Get search service instance with fresh session"""
429
+ # Always create a new session to avoid stale connections
430
+ session = self.SessionLocal()
431
+ return SearchService(session, self.database_type)
432
+
433
+ def store_chat_history(
434
+ self,
435
+ chat_id: str,
436
+ user_input: str,
437
+ ai_output: str,
438
+ model: str,
439
+ timestamp: datetime,
440
+ session_id: str,
441
+ namespace: str = "default",
442
+ tokens_used: int = 0,
443
+ metadata: Optional[Dict[str, Any]] = None,
444
+ ):
445
+ """Store chat history"""
446
+ with self.SessionLocal() as session:
447
+ try:
448
+ chat_history = ChatHistory(
449
+ chat_id=chat_id,
450
+ user_input=user_input,
451
+ ai_output=ai_output,
452
+ model=model,
453
+ timestamp=timestamp,
454
+ session_id=session_id,
455
+ namespace=namespace,
456
+ tokens_used=tokens_used,
457
+ metadata_json=metadata or {},
458
+ )
459
+
460
+ session.merge(chat_history) # Use merge for INSERT OR REPLACE behavior
461
+ session.commit()
462
+
463
+ except SQLAlchemyError as e:
464
+ session.rollback()
465
+ raise DatabaseError(f"Failed to store chat history: {e}")
466
+
467
+ def get_chat_history(
468
+ self,
469
+ namespace: str = "default",
470
+ session_id: Optional[str] = None,
471
+ limit: int = 10,
472
+ ) -> List[Dict[str, Any]]:
473
+ """Get chat history with optional session filtering"""
474
+ with self.SessionLocal() as session:
475
+ try:
476
+ query = session.query(ChatHistory).filter(
477
+ ChatHistory.namespace == namespace
478
+ )
479
+
480
+ if session_id:
481
+ query = query.filter(ChatHistory.session_id == session_id)
482
+
483
+ results = (
484
+ query.order_by(ChatHistory.timestamp.desc()).limit(limit).all()
485
+ )
486
+
487
+ # Convert to dictionaries
488
+ return [
489
+ {
490
+ "chat_id": result.chat_id,
491
+ "user_input": result.user_input,
492
+ "ai_output": result.ai_output,
493
+ "model": result.model,
494
+ "timestamp": result.timestamp,
495
+ "session_id": result.session_id,
496
+ "namespace": result.namespace,
497
+ "tokens_used": result.tokens_used,
498
+ "metadata": result.metadata_json or {},
499
+ }
500
+ for result in results
501
+ ]
502
+
503
+ except SQLAlchemyError as e:
504
+ raise DatabaseError(f"Failed to get chat history: {e}")
505
+
506
+ def store_long_term_memory_enhanced(
507
+ self, memory: ProcessedLongTermMemory, chat_id: str, namespace: str = "default"
508
+ ) -> str:
509
+ """Store a ProcessedLongTermMemory with enhanced schema"""
510
+ memory_id = str(uuid.uuid4())
511
+
512
+ with self.SessionLocal() as session:
513
+ try:
514
+ long_term_memory = LongTermMemory(
515
+ memory_id=memory_id,
516
+ original_chat_id=chat_id,
517
+ processed_data=memory.model_dump(mode="json"),
518
+ importance_score=memory.importance_score,
519
+ category_primary=memory.classification.value,
520
+ retention_type="long_term",
521
+ namespace=namespace,
522
+ created_at=datetime.now(),
523
+ searchable_content=memory.content,
524
+ summary=memory.summary,
525
+ novelty_score=0.5,
526
+ relevance_score=0.5,
527
+ actionability_score=0.5,
528
+ classification=memory.classification.value,
529
+ memory_importance=memory.importance.value,
530
+ topic=memory.topic,
531
+ entities_json=memory.entities,
532
+ keywords_json=memory.keywords,
533
+ is_user_context=memory.is_user_context,
534
+ is_preference=memory.is_preference,
535
+ is_skill_knowledge=memory.is_skill_knowledge,
536
+ is_current_project=memory.is_current_project,
537
+ promotion_eligible=memory.promotion_eligible,
538
+ duplicate_of=memory.duplicate_of,
539
+ supersedes_json=memory.supersedes,
540
+ related_memories_json=memory.related_memories,
541
+ confidence_score=memory.confidence_score,
542
+ extraction_timestamp=memory.extraction_timestamp,
543
+ classification_reason=memory.classification_reason,
544
+ processed_for_duplicates=False,
545
+ conscious_processed=False,
546
+ )
547
+
548
+ session.add(long_term_memory)
549
+ session.commit()
550
+
551
+ logger.debug(f"Stored enhanced long-term memory {memory_id}")
552
+ return memory_id
553
+
554
+ except SQLAlchemyError as e:
555
+ session.rollback()
556
+ logger.error(f"Failed to store enhanced long-term memory: {e}")
557
+ raise DatabaseError(f"Failed to store enhanced long-term memory: {e}")
558
+
559
+ def search_memories(
560
+ self,
561
+ query: str,
562
+ namespace: str = "default",
563
+ category_filter: Optional[List[str]] = None,
564
+ limit: int = 10,
565
+ ) -> List[Dict[str, Any]]:
566
+ """Search memories using the cross-database search service"""
567
+ try:
568
+ search_service = self._get_search_service()
569
+ try:
570
+ results = search_service.search_memories(
571
+ query, namespace, category_filter, limit
572
+ )
573
+ logger.debug(f"Search for '{query}' returned {len(results)} results")
574
+ return results
575
+ finally:
576
+ # Ensure session is properly closed
577
+ search_service.session.close()
578
+
579
+ except Exception as e:
580
+ logger.error(f"Memory search failed for query '{query}': {e}")
581
+ # Return empty list instead of raising exception to avoid breaking auto_ingest
582
+ return []
583
+
584
+ def get_memory_stats(self, namespace: str = "default") -> Dict[str, Any]:
585
+ """Get comprehensive memory statistics"""
586
+ with self.SessionLocal() as session:
587
+ try:
588
+ stats = {}
589
+
590
+ # Basic counts
591
+ stats["chat_history_count"] = (
592
+ session.query(ChatHistory)
593
+ .filter(ChatHistory.namespace == namespace)
594
+ .count()
595
+ )
596
+
597
+ stats["short_term_count"] = (
598
+ session.query(ShortTermMemory)
599
+ .filter(ShortTermMemory.namespace == namespace)
600
+ .count()
601
+ )
602
+
603
+ stats["long_term_count"] = (
604
+ session.query(LongTermMemory)
605
+ .filter(LongTermMemory.namespace == namespace)
606
+ .count()
607
+ )
608
+
609
+ # Category breakdown
610
+ categories = {}
611
+
612
+ # Short-term categories
613
+ short_categories = (
614
+ session.query(
615
+ ShortTermMemory.category_primary,
616
+ func.count(ShortTermMemory.memory_id).label("count"),
617
+ )
618
+ .filter(ShortTermMemory.namespace == namespace)
619
+ .group_by(ShortTermMemory.category_primary)
620
+ .all()
621
+ )
622
+
623
+ for cat, count in short_categories:
624
+ categories[cat] = categories.get(cat, 0) + count
625
+
626
+ # Long-term categories
627
+ long_categories = (
628
+ session.query(
629
+ LongTermMemory.category_primary,
630
+ func.count(LongTermMemory.memory_id).label("count"),
631
+ )
632
+ .filter(LongTermMemory.namespace == namespace)
633
+ .group_by(LongTermMemory.category_primary)
634
+ .all()
635
+ )
636
+
637
+ for cat, count in long_categories:
638
+ categories[cat] = categories.get(cat, 0) + count
639
+
640
+ stats["memories_by_category"] = categories
641
+
642
+ # Average importance
643
+ short_avg = (
644
+ session.query(func.avg(ShortTermMemory.importance_score))
645
+ .filter(ShortTermMemory.namespace == namespace)
646
+ .scalar()
647
+ or 0
648
+ )
649
+
650
+ long_avg = (
651
+ session.query(func.avg(LongTermMemory.importance_score))
652
+ .filter(LongTermMemory.namespace == namespace)
653
+ .scalar()
654
+ or 0
655
+ )
656
+
657
+ total_memories = stats["short_term_count"] + stats["long_term_count"]
658
+ if total_memories > 0:
659
+ # Weight averages by count
660
+ total_avg = (
661
+ (short_avg * stats["short_term_count"])
662
+ + (long_avg * stats["long_term_count"])
663
+ ) / total_memories
664
+ stats["average_importance"] = float(total_avg) if total_avg else 0.0
665
+ else:
666
+ stats["average_importance"] = 0.0
667
+
668
+ # Database info
669
+ stats["database_type"] = self.database_type
670
+ stats["database_url"] = (
671
+ self.database_connect.split("@")[-1]
672
+ if "@" in self.database_connect
673
+ else self.database_connect
674
+ )
675
+
676
+ return stats
677
+
678
+ except SQLAlchemyError as e:
679
+ raise DatabaseError(f"Failed to get memory stats: {e}")
680
+
681
+ def clear_memory(
682
+ self, namespace: str = "default", memory_type: Optional[str] = None
683
+ ):
684
+ """Clear memory data"""
685
+ with self.SessionLocal() as session:
686
+ try:
687
+ if memory_type == "short_term":
688
+ session.query(ShortTermMemory).filter(
689
+ ShortTermMemory.namespace == namespace
690
+ ).delete()
691
+ elif memory_type == "long_term":
692
+ session.query(LongTermMemory).filter(
693
+ LongTermMemory.namespace == namespace
694
+ ).delete()
695
+ elif memory_type == "chat_history":
696
+ session.query(ChatHistory).filter(
697
+ ChatHistory.namespace == namespace
698
+ ).delete()
699
+ else: # Clear all
700
+ session.query(ShortTermMemory).filter(
701
+ ShortTermMemory.namespace == namespace
702
+ ).delete()
703
+ session.query(LongTermMemory).filter(
704
+ LongTermMemory.namespace == namespace
705
+ ).delete()
706
+ session.query(ChatHistory).filter(
707
+ ChatHistory.namespace == namespace
708
+ ).delete()
709
+
710
+ session.commit()
711
+
712
+ except SQLAlchemyError as e:
713
+ session.rollback()
714
+ raise DatabaseError(f"Failed to clear memory: {e}")
715
+
716
+ def execute_with_translation(self, query: str, parameters: Dict[str, Any] = None):
717
+ """
718
+ Execute a query with automatic parameter translation for cross-database compatibility.
719
+
720
+ Args:
721
+ query: SQL query string
722
+ parameters: Query parameters
723
+
724
+ Returns:
725
+ Query result
726
+ """
727
+ if parameters:
728
+ translated_params = self.query_translator.translate_parameters(parameters)
729
+ else:
730
+ translated_params = {}
731
+
732
+ with self.engine.connect() as conn:
733
+ result = conn.execute(text(query), translated_params)
734
+ conn.commit()
735
+ return result
736
+
737
+ def _get_connection(self):
738
+ """
739
+ Compatibility method for legacy code that expects raw database connections.
740
+
741
+ Returns a context manager that provides a SQLAlchemy connection with
742
+ automatic parameter translation support.
743
+
744
+ This is used by memory.py for direct SQL queries.
745
+ """
746
+ from contextlib import contextmanager
747
+
748
+ @contextmanager
749
+ def connection_context():
750
+ class TranslatingConnection:
751
+ """Wrapper that adds parameter translation to SQLAlchemy connections"""
752
+
753
+ def __init__(self, conn, translator):
754
+ self._conn = conn
755
+ self._translator = translator
756
+
757
+ def execute(self, query, parameters=None):
758
+ """Execute query with automatic parameter translation"""
759
+ if parameters:
760
+ # Handle both text() queries and raw strings
761
+ if hasattr(query, "text"):
762
+ # SQLAlchemy text() object
763
+ translated_params = self._translator.translate_parameters(
764
+ parameters
765
+ )
766
+ return self._conn.execute(query, translated_params)
767
+ else:
768
+ # Raw string query
769
+ translated_params = self._translator.translate_parameters(
770
+ parameters
771
+ )
772
+ return self._conn.execute(
773
+ text(str(query)), translated_params
774
+ )
775
+ else:
776
+ return self._conn.execute(query)
777
+
778
+ def commit(self):
779
+ """Commit transaction"""
780
+ return self._conn.commit()
781
+
782
+ def rollback(self):
783
+ """Rollback transaction"""
784
+ return self._conn.rollback()
785
+
786
+ def close(self):
787
+ """Close connection"""
788
+ return self._conn.close()
789
+
790
+ def fetchall(self):
791
+ """Compatibility method for cursor-like usage"""
792
+ # This is for backwards compatibility with code that expects cursor.fetchall()
793
+ return []
794
+
795
+ def scalar(self):
796
+ """Compatibility method for cursor-like usage"""
797
+ return None
798
+
799
+ def __getattr__(self, name):
800
+ """Delegate unknown attributes to the underlying connection"""
801
+ return getattr(self._conn, name)
802
+
803
+ conn = self.engine.connect()
804
+ try:
805
+ yield TranslatingConnection(conn, self.query_translator)
806
+ finally:
807
+ conn.close()
808
+
809
+ return connection_context()
810
+
811
+ def close(self):
812
+ """Close database connections"""
813
+ if self._search_service and hasattr(self._search_service, "session"):
814
+ self._search_service.session.close()
815
+
816
+ if hasattr(self, "engine"):
817
+ self.engine.dispose()
818
+
819
+ def get_database_info(self) -> Dict[str, Any]:
820
+ """Get database information and capabilities"""
821
+ base_info = {
822
+ "database_type": self.database_type,
823
+ "database_url": (
824
+ self.database_connect.split("@")[-1]
825
+ if "@" in self.database_connect
826
+ else self.database_connect
827
+ ),
828
+ "driver": self.engine.dialect.driver,
829
+ "server_version": getattr(self.engine.dialect, "server_version_info", None),
830
+ "supports_fulltext": True, # Assume true for SQLAlchemy managed connections
831
+ "auto_creation_enabled": self.enable_auto_creation,
832
+ }
833
+
834
+ # Add auto-creation specific information
835
+ if hasattr(self, "auto_creator"):
836
+ creation_info = self.auto_creator.get_database_info(self.database_connect)
837
+ base_info.update(creation_info)
838
+
839
+ return base_info