memorisdk 1.0.2__py3-none-any.whl → 2.0.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 (48) 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 +491 -68
  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 +1683 -532
  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 +700 -0
  30. memori/database/sqlalchemy_manager.py +888 -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/tools/memory_tool.py +94 -4
  35. memori/utils/input_validator.py +395 -0
  36. memori/utils/pydantic_models.py +138 -36
  37. memori/utils/query_builder.py +530 -0
  38. memori/utils/security_audit.py +594 -0
  39. memori/utils/security_integration.py +339 -0
  40. memori/utils/transaction_manager.py +547 -0
  41. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/METADATA +56 -23
  42. memorisdk-2.0.1.dist-info/RECORD +66 -0
  43. memori/scripts/llm_text.py +0 -50
  44. memorisdk-1.0.2.dist-info/RECORD +0 -44
  45. memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
  46. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
  47. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
  48. {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/top_level.txt +0 -0
memori/core/database.py CHANGED
@@ -11,17 +11,24 @@ from typing import Any, Dict, List, Optional
11
11
 
12
12
  from loguru import logger
13
13
 
14
- from ..utils.exceptions import DatabaseError
15
- from ..utils.pydantic_models import MemoryCategoryType, ProcessedMemory, RetentionType
14
+ from ..utils.exceptions import DatabaseError, ValidationError
15
+ from ..utils.input_validator import DatabaseInputValidator, InputValidator
16
+ from ..utils.pydantic_models import (
17
+ ProcessedLongTermMemory,
18
+ ProcessedMemory,
19
+ RetentionType,
20
+ )
21
+ from ..utils.transaction_manager import TransactionManager, TransactionOperation
16
22
 
17
23
 
18
24
  class DatabaseManager:
19
- """Manages Pydantic-based memory storage with entity indexing and FTS search"""
25
+ """Manages Pydantic-based memory storage with streamlined schema and FTS search"""
20
26
 
21
27
  def __init__(self, database_connect: str, template: str = "basic"):
22
28
  self.database_connect = database_connect
23
29
  self.template = template
24
30
  self.db_path = self._parse_connection_string(database_connect)
31
+ self.transaction_manager = TransactionManager(self)
25
32
 
26
33
  def _parse_connection_string(self, connect_str: str) -> str:
27
34
  """Parse database connection string"""
@@ -209,22 +216,6 @@ class DatabaseManager:
209
216
  """
210
217
  )
211
218
 
212
- cursor.execute(
213
- """
214
- CREATE TABLE IF NOT EXISTS memory_entities (
215
- entity_id TEXT PRIMARY KEY,
216
- memory_id TEXT NOT NULL,
217
- memory_type TEXT NOT NULL,
218
- entity_type TEXT NOT NULL,
219
- entity_value TEXT NOT NULL,
220
- relevance_score REAL NOT NULL DEFAULT 0.5,
221
- entity_context TEXT,
222
- namespace TEXT NOT NULL DEFAULT 'default',
223
- created_at TIMESTAMP NOT NULL
224
- )
225
- """
226
- )
227
-
228
219
  conn.commit()
229
220
  logger.info("Basic database schema created")
230
221
 
@@ -240,7 +231,27 @@ class DatabaseManager:
240
231
  tokens_used: int = 0,
241
232
  metadata: Optional[Dict[str, Any]] = None,
242
233
  ):
243
- """Store chat history"""
234
+ """Store chat history with input validation"""
235
+ try:
236
+ # Validate and sanitize all inputs
237
+ validated_data = DatabaseInputValidator.validate_insert_params(
238
+ "chat_history",
239
+ {
240
+ "chat_id": chat_id,
241
+ "user_input": user_input,
242
+ "ai_output": ai_output,
243
+ "model": model,
244
+ "timestamp": timestamp,
245
+ "session_id": session_id,
246
+ "namespace": namespace,
247
+ "tokens_used": max(0, int(tokens_used)) if tokens_used else 0,
248
+ "metadata": metadata or {},
249
+ },
250
+ )
251
+ except (ValidationError, ValueError) as e:
252
+ logger.error(f"Invalid chat history data: {e}")
253
+ raise DatabaseError(f"Cannot store chat history: {e}")
254
+
244
255
  with self._get_connection() as conn:
245
256
  cursor = conn.cursor()
246
257
  cursor.execute(
@@ -250,15 +261,15 @@ class DatabaseManager:
250
261
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
251
262
  """,
252
263
  (
253
- chat_id,
254
- user_input,
255
- ai_output,
256
- model,
257
- timestamp,
258
- session_id,
259
- namespace,
260
- tokens_used,
261
- json.dumps(metadata or {}),
264
+ validated_data["chat_id"],
265
+ validated_data["user_input"],
266
+ validated_data["ai_output"],
267
+ validated_data["model"],
268
+ validated_data["timestamp"],
269
+ validated_data["session_id"],
270
+ validated_data["namespace"],
271
+ validated_data["tokens_used"],
272
+ validated_data["metadata"],
262
273
  ),
263
274
  )
264
275
  conn.commit()
@@ -270,6 +281,16 @@ class DatabaseManager:
270
281
  limit: int = 10,
271
282
  ) -> List[Dict[str, Any]]:
272
283
  """Get chat history with optional session filtering"""
284
+ try:
285
+ # Validate inputs
286
+ namespace = InputValidator.validate_namespace(namespace)
287
+ limit = InputValidator.validate_limit(limit)
288
+ if session_id:
289
+ session_id = InputValidator.validate_memory_id(session_id)
290
+ except ValidationError as e:
291
+ logger.error(f"Invalid chat history parameters: {e}")
292
+ return []
293
+
273
294
  with self._get_connection() as conn:
274
295
  cursor = conn.cursor()
275
296
 
@@ -312,46 +333,89 @@ class DatabaseManager:
312
333
 
313
334
  return result
314
335
 
315
- def store_processed_memory(
316
- self, memory: ProcessedMemory, chat_id: str, namespace: str = "default"
336
+ def store_long_term_memory_enhanced(
337
+ self, memory: ProcessedLongTermMemory, chat_id: str, namespace: str = "default"
317
338
  ) -> str:
318
- """Store a processed memory with entity indexing"""
319
-
320
- if not memory.should_store:
321
- logger.debug(f"Memory not stored: {memory.storage_reasoning}")
322
- return ""
323
-
324
- memory_id = str(uuid.uuid4())
325
- storage_location = self._determine_storage_location(memory)
339
+ """Store a ProcessedLongTermMemory with enhanced schema using transactions"""
340
+ try:
341
+ memory_id = str(uuid.uuid4())
342
+
343
+ # Validate inputs
344
+ chat_id = InputValidator.validate_memory_id(chat_id)
345
+ namespace = InputValidator.validate_namespace(namespace)
346
+
347
+ # Prepare operations for atomic execution
348
+ operations = []
349
+
350
+ # Main memory insert operation
351
+ insert_operation = TransactionOperation(
352
+ query="""
353
+ INSERT INTO long_term_memory (
354
+ memory_id, original_chat_id, processed_data, importance_score, category_primary,
355
+ retention_type, namespace, created_at, searchable_content, summary,
356
+ novelty_score, relevance_score, actionability_score, classification, memory_importance,
357
+ topic, entities_json, keywords_json, is_user_context, is_preference, is_skill_knowledge,
358
+ is_current_project, promotion_eligible, duplicate_of, supersedes_json, related_memories_json,
359
+ confidence_score, extraction_timestamp, classification_reason, processed_for_duplicates, conscious_processed
360
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
361
+ """,
362
+ params=[
363
+ memory_id,
364
+ chat_id,
365
+ json.dumps(memory.model_dump(mode="json")),
366
+ memory.importance_score,
367
+ memory.classification.value,
368
+ "long_term",
369
+ namespace,
370
+ datetime.now().isoformat(),
371
+ memory.content,
372
+ memory.summary,
373
+ 0.5,
374
+ 0.5,
375
+ 0.5, # novelty, relevance, actionability scores
376
+ memory.classification.value,
377
+ memory.importance.value,
378
+ memory.topic,
379
+ json.dumps(memory.entities),
380
+ json.dumps(memory.keywords),
381
+ memory.is_user_context,
382
+ memory.is_preference,
383
+ memory.is_skill_knowledge,
384
+ memory.is_current_project,
385
+ memory.promotion_eligible,
386
+ memory.duplicate_of,
387
+ json.dumps(memory.supersedes),
388
+ json.dumps(memory.related_memories),
389
+ memory.confidence_score,
390
+ memory.extraction_timestamp.isoformat(),
391
+ memory.classification_reason,
392
+ False, # processed_for_duplicates
393
+ False, # conscious_processed
394
+ ],
395
+ operation_type="insert",
396
+ table="long_term_memory",
397
+ expected_rows=1,
398
+ )
326
399
 
327
- with self._get_connection() as conn:
328
- cursor = conn.cursor()
400
+ operations.append(insert_operation)
329
401
 
330
- try:
331
- if storage_location == "short_term_memory":
332
- self._store_short_term_memory(
333
- cursor, memory_id, memory, chat_id, namespace
334
- )
335
- elif storage_location == "long_term_memory":
336
- self._store_long_term_memory(
337
- cursor, memory_id, memory, chat_id, namespace
338
- )
339
- elif storage_location == "rules_memory":
340
- self._store_rules_memory(cursor, memory_id, memory, namespace)
341
-
342
- # Store entities for indexing
343
- self._store_entities(
344
- cursor, memory_id, memory, storage_location, namespace
345
- )
402
+ # Execute all operations atomically
403
+ result = self.transaction_manager.execute_atomic_operations(operations)
346
404
 
347
- conn.commit()
348
- logger.debug(f"Stored memory {memory_id} in {storage_location}")
405
+ if result.success:
406
+ logger.debug(f"Stored enhanced long-term memory {memory_id}")
349
407
  return memory_id
408
+ else:
409
+ raise DatabaseError(
410
+ f"Failed to store enhanced long-term memory: {result.error_message}"
411
+ )
350
412
 
351
- except Exception as e:
352
- conn.rollback()
353
- logger.error(f"Failed to store memory: {e}")
354
- raise DatabaseError(f"Failed to store memory: {e}")
413
+ except ValidationError as e:
414
+ logger.error(f"Invalid memory data: {e}")
415
+ raise DatabaseError(f"Cannot store long-term memory: {e}")
416
+ except Exception as e:
417
+ logger.error(f"Failed to store enhanced long-term memory: {e}")
418
+ raise DatabaseError(f"Failed to store enhanced long-term memory: {e}")
355
419
 
356
420
  def _store_short_term_memory(
357
421
  self,
@@ -439,108 +503,6 @@ class DatabaseManager:
439
503
  ),
440
504
  )
441
505
 
442
- def _store_rules_memory(
443
- self,
444
- cursor: sqlite3.Cursor,
445
- memory_id: str,
446
- memory: ProcessedMemory,
447
- namespace: str,
448
- ):
449
- """Store rule-type memory in rules table"""
450
- # Ensure we have a valid timestamp (timezone-naive for SQLite compatibility)
451
- created_at = memory.timestamp
452
- if created_at is None:
453
- created_at = datetime.now()
454
- elif hasattr(created_at, "replace"):
455
- # Make timezone-naive if timezone-aware
456
- created_at = created_at.replace(tzinfo=None)
457
-
458
- cursor.execute(
459
- """
460
- INSERT INTO rules_memory
461
- (rule_id, rule_text, rule_type, priority, active, namespace,
462
- created_at, updated_at, processed_data)
463
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
464
- """,
465
- (
466
- memory_id,
467
- memory.summary,
468
- "rule",
469
- 5,
470
- True,
471
- namespace,
472
- created_at,
473
- created_at,
474
- memory.model_dump_json(),
475
- ),
476
- )
477
-
478
- def _store_entities(
479
- self,
480
- cursor: sqlite3.Cursor,
481
- memory_id: str,
482
- memory: ProcessedMemory,
483
- memory_type: str,
484
- namespace: str,
485
- ):
486
- """Store extracted entities for indexing"""
487
-
488
- # Simple entities (lists), In future we can make it to dynamically handle more entity types
489
-
490
- entity_mappings = [
491
- (memory.entities.people, "person"),
492
- (memory.entities.technologies, "technology"),
493
- (memory.entities.topics, "topic"),
494
- (memory.entities.skills, "skill"),
495
- (memory.entities.projects, "project"),
496
- (memory.entities.keywords, "keyword"),
497
- ]
498
-
499
- for entity_list, entity_type in entity_mappings:
500
- for entity_value in entity_list:
501
- entity_id = str(uuid.uuid4())
502
- cursor.execute(
503
- """
504
- INSERT INTO memory_entities
505
- (entity_id, memory_id, memory_type, entity_type, entity_value,
506
- relevance_score, namespace, created_at)
507
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
508
- """,
509
- (
510
- entity_id,
511
- memory_id,
512
- memory_type.replace("_memory", ""),
513
- entity_type,
514
- entity_value,
515
- 0.8,
516
- namespace,
517
- datetime.now(),
518
- ),
519
- )
520
-
521
- # Structured entities (with metadata)
522
- for structured_entity in memory.entities.structured_entities:
523
- entity_id = str(uuid.uuid4())
524
- cursor.execute(
525
- """
526
- INSERT INTO memory_entities
527
- (entity_id, memory_id, memory_type, entity_type, entity_value,
528
- relevance_score, entity_context, namespace, created_at)
529
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
530
- """,
531
- (
532
- entity_id,
533
- memory_id,
534
- memory_type.replace("_memory", ""),
535
- structured_entity.entity_type.value,
536
- structured_entity.value,
537
- structured_entity.relevance_score,
538
- structured_entity.context,
539
- namespace,
540
- datetime.now(),
541
- ),
542
- )
543
-
544
506
  def search_memories(
545
507
  self,
546
508
  query: str,
@@ -549,6 +511,20 @@ class DatabaseManager:
549
511
  limit: int = 10,
550
512
  ) -> List[Dict[str, Any]]:
551
513
  """Advanced memory search with hybrid approach: FTS + Entity + Category filtering"""
514
+ try:
515
+ # Validate and sanitize all input parameters
516
+ validated_params = DatabaseInputValidator.validate_search_params(
517
+ query, namespace, category_filter, limit
518
+ )
519
+ query = validated_params["query"]
520
+ namespace = validated_params["namespace"]
521
+ category_filter = validated_params["category_filter"]
522
+ limit = validated_params["limit"]
523
+
524
+ except ValidationError as e:
525
+ logger.error(f"Invalid search parameters: {e}")
526
+ return []
527
+
552
528
  all_results = []
553
529
 
554
530
  with self._get_connection() as conn:
@@ -564,14 +540,7 @@ class DatabaseManager:
564
540
  result["search_score"] = 1.0
565
541
  all_results.append(result)
566
542
 
567
- # 2. Entity-based search for better context matching
568
- entity_results = self._execute_entity_search(
569
- cursor, query, namespace, category_filter, limit
570
- )
571
- for result in entity_results:
572
- result["search_strategy"] = "entity_search"
573
- result["search_score"] = 0.8
574
- all_results.append(result)
543
+ # Note: Entity-based search removed in streamlined schema
575
544
 
576
545
  # 3. Category-based search if specified
577
546
  if category_filter:
@@ -625,116 +594,165 @@ class DatabaseManager:
625
594
  category_filter: Optional[List[str]],
626
595
  limit: int,
627
596
  ):
628
- """Execute FTS5 search"""
597
+ """Execute FTS5 search with proper parameterization"""
629
598
  try:
630
- # Build FTS query with category filter
631
- fts_query = f'"{query}"' if query else "*"
632
- category_clause = ""
633
- params = [fts_query, namespace]
634
-
635
- if category_filter:
636
- category_placeholders = ",".join("?" * len(category_filter))
637
- category_clause = (
638
- f"AND fts.category_primary IN ({category_placeholders})"
639
- )
640
- params.extend(category_filter)
599
+ # Sanitize and prepare FTS query
600
+ if query and query.strip():
601
+ # Escape FTS5 special characters and wrap in quotes for phrase search
602
+ sanitized_query = query.strip().replace('"', '""') # Escape quotes
603
+ fts_query = f'"{sanitized_query}"'
604
+ else:
605
+ # Use a simple match-all query for empty searches
606
+ fts_query = "*"
641
607
 
642
- params.append(limit)
608
+ # Build parameterized query - avoid string concatenation
609
+ params = [fts_query, namespace]
643
610
 
644
- cursor.execute(
645
- f"""
646
- SELECT
647
- fts.memory_id, fts.memory_type, fts.category_primary,
648
- CASE
649
- WHEN fts.memory_type = 'short_term' THEN st.processed_data
650
- WHEN fts.memory_type = 'long_term' THEN lt.processed_data
651
- WHEN fts.memory_type = 'rules' THEN r.processed_data
652
- END as processed_data,
653
- CASE
654
- WHEN fts.memory_type = 'short_term' THEN st.importance_score
655
- WHEN fts.memory_type = 'long_term' THEN lt.importance_score
656
- ELSE 0.5
657
- END as importance_score,
658
- CASE
659
- WHEN fts.memory_type = 'short_term' THEN st.created_at
660
- WHEN fts.memory_type = 'long_term' THEN lt.created_at
661
- WHEN fts.memory_type = 'rules' THEN r.created_at
662
- END as created_at,
663
- fts.summary,
664
- rank
665
- FROM memory_search_fts fts
666
- LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
667
- LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
668
- LEFT JOIN rules_memory r ON fts.memory_id = r.rule_id AND fts.memory_type = 'rules'
669
- WHERE memory_search_fts MATCH ? AND fts.namespace = ? {category_clause}
670
- ORDER BY rank, importance_score DESC
671
- LIMIT ?
672
- """,
673
- params,
674
- )
611
+ if category_filter and isinstance(category_filter, list):
612
+ # Validate category filter is a list of strings
613
+ sanitized_categories = [str(cat) for cat in category_filter if cat]
614
+ if sanitized_categories:
615
+ category_placeholders = ",".join("?" * len(sanitized_categories))
616
+ params.extend(sanitized_categories)
617
+ params.append(limit)
618
+
619
+ sql_query = f"""
620
+ SELECT
621
+ fts.memory_id, fts.memory_type, fts.category_primary,
622
+ CASE
623
+ WHEN fts.memory_type = 'short_term' THEN st.processed_data
624
+ WHEN fts.memory_type = 'long_term' THEN lt.processed_data
625
+ END as processed_data,
626
+ CASE
627
+ WHEN fts.memory_type = 'short_term' THEN st.importance_score
628
+ WHEN fts.memory_type = 'long_term' THEN lt.importance_score
629
+ ELSE 0.5
630
+ END as importance_score,
631
+ CASE
632
+ WHEN fts.memory_type = 'short_term' THEN st.created_at
633
+ WHEN fts.memory_type = 'long_term' THEN lt.created_at
634
+ END as created_at,
635
+ fts.summary,
636
+ rank
637
+ FROM memory_search_fts fts
638
+ LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
639
+ LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
640
+ WHERE memory_search_fts MATCH ? AND fts.namespace = ?
641
+ AND fts.category_primary IN ({category_placeholders})
642
+ ORDER BY rank, importance_score DESC
643
+ LIMIT ?
644
+ """
645
+ else:
646
+ # No valid categories, proceed without filter
647
+ params.append(limit)
648
+ sql_query = """
649
+ SELECT
650
+ fts.memory_id, fts.memory_type, fts.category_primary,
651
+ CASE
652
+ WHEN fts.memory_type = 'short_term' THEN st.processed_data
653
+ WHEN fts.memory_type = 'long_term' THEN lt.processed_data
654
+ END as processed_data,
655
+ CASE
656
+ WHEN fts.memory_type = 'short_term' THEN st.importance_score
657
+ WHEN fts.memory_type = 'long_term' THEN lt.importance_score
658
+ ELSE 0.5
659
+ END as importance_score,
660
+ CASE
661
+ WHEN fts.memory_type = 'short_term' THEN st.created_at
662
+ WHEN fts.memory_type = 'long_term' THEN lt.created_at
663
+ END as created_at,
664
+ fts.summary,
665
+ rank
666
+ FROM memory_search_fts fts
667
+ LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
668
+ LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
669
+ WHERE memory_search_fts MATCH ? AND fts.namespace = ?
670
+ ORDER BY rank, importance_score DESC
671
+ LIMIT ?
672
+ """
673
+ else:
674
+ params.append(limit)
675
+ sql_query = """
676
+ SELECT
677
+ fts.memory_id, fts.memory_type, fts.category_primary,
678
+ CASE
679
+ WHEN fts.memory_type = 'short_term' THEN st.processed_data
680
+ WHEN fts.memory_type = 'long_term' THEN lt.processed_data
681
+ END as processed_data,
682
+ CASE
683
+ WHEN fts.memory_type = 'short_term' THEN st.importance_score
684
+ WHEN fts.memory_type = 'long_term' THEN lt.importance_score
685
+ ELSE 0.5
686
+ END as importance_score,
687
+ CASE
688
+ WHEN fts.memory_type = 'short_term' THEN st.created_at
689
+ WHEN fts.memory_type = 'long_term' THEN lt.created_at
690
+ END as created_at,
691
+ fts.summary,
692
+ rank
693
+ FROM memory_search_fts fts
694
+ LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
695
+ LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
696
+ WHERE memory_search_fts MATCH ? AND fts.namespace = ?
697
+ ORDER BY rank, importance_score DESC
698
+ LIMIT ?
699
+ """
675
700
 
701
+ cursor.execute(sql_query, params)
676
702
  return [dict(row) for row in cursor.fetchall()]
677
703
 
678
704
  except sqlite3.OperationalError as e:
679
705
  logger.debug(f"FTS not available: {e}")
680
706
  return []
681
-
682
- def _execute_entity_search(
683
- self,
684
- cursor,
685
- query: str,
686
- namespace: str,
687
- category_filter: Optional[List[str]],
688
- limit: int,
689
- ):
690
- """Execute entity-based search"""
691
- category_clause = ""
692
- params = [f"%{query}%", namespace]
693
-
694
- if category_filter:
695
- category_placeholders = ",".join("?" * len(category_filter))
696
- category_clause = f"AND m.category_primary IN ({category_placeholders})"
697
- params.extend(category_filter)
698
-
699
- params.append(limit)
700
-
701
- cursor.execute(
702
- f"""
703
- SELECT DISTINCT m.memory_id, m.processed_data, m.importance_score, m.created_at,
704
- m.summary, m.category_primary, 'long_term' as memory_type,
705
- e.entity_type, e.entity_value, e.relevance_score
706
- FROM long_term_memory m
707
- JOIN memory_entities e ON m.memory_id = e.memory_id
708
- WHERE e.entity_value LIKE ? AND m.namespace = ? {category_clause}
709
- ORDER BY e.relevance_score DESC, m.importance_score DESC
710
- LIMIT ?
711
- """,
712
- params,
713
- )
714
-
715
- return [dict(row) for row in cursor.fetchall()]
707
+ except Exception as e:
708
+ logger.error(f"FTS search error: {e}")
709
+ return []
716
710
 
717
711
  def _execute_category_search(
718
712
  self, cursor, query: str, namespace: str, category_filter: List[str], limit: int
719
713
  ):
720
- """Execute category-based search"""
721
- category_placeholders = ",".join("?" * len(category_filter))
722
- params = [namespace] + category_filter + [f"%{query}%", f"%{query}%", limit]
714
+ """Execute category-based search with proper input validation"""
715
+ try:
716
+ # Input validation
717
+ if not isinstance(category_filter, list) or not category_filter:
718
+ return []
719
+
720
+ # Sanitize inputs
721
+ sanitized_query = str(query).strip() if query else ""
722
+ sanitized_namespace = str(namespace).strip()
723
+ sanitized_categories = [str(cat).strip() for cat in category_filter if cat]
724
+ sanitized_limit = max(1, min(int(limit), 1000)) # Limit between 1 and 1000
725
+
726
+ if not sanitized_categories:
727
+ return []
728
+
729
+ # Build parameterized query
730
+ category_placeholders = ",".join(["?"] * len(sanitized_categories))
731
+
732
+ # Use parameterized query with proper escaping
733
+ sql_query = f"""
734
+ SELECT memory_id, processed_data, importance_score, created_at, summary,
735
+ category_primary, 'long_term' as memory_type
736
+ FROM long_term_memory
737
+ WHERE namespace = ? AND category_primary IN ({category_placeholders})
738
+ AND (searchable_content LIKE ? OR summary LIKE ?)
739
+ ORDER BY importance_score DESC, created_at DESC
740
+ LIMIT ?
741
+ """
723
742
 
724
- cursor.execute(
725
- f"""
726
- SELECT memory_id, processed_data, importance_score, created_at, summary,
727
- category_primary, 'long_term' as memory_type
728
- FROM long_term_memory
729
- WHERE namespace = ? AND category_primary IN ({category_placeholders})
730
- AND (searchable_content LIKE ? OR summary LIKE ?)
731
- ORDER BY importance_score DESC, created_at DESC
732
- LIMIT ?
733
- """,
734
- params,
735
- )
743
+ # Build parameters safely
744
+ params = (
745
+ [sanitized_namespace]
746
+ + sanitized_categories
747
+ + [f"%{sanitized_query}%", f"%{sanitized_query}%", sanitized_limit]
748
+ )
749
+
750
+ cursor.execute(sql_query, params)
751
+ return [dict(row) for row in cursor.fetchall()]
736
752
 
737
- return [dict(row) for row in cursor.fetchall()]
753
+ except Exception as e:
754
+ logger.error(f"Category search error: {e}")
755
+ return []
738
756
 
739
757
  def _execute_like_search(
740
758
  self,
@@ -744,50 +762,104 @@ class DatabaseManager:
744
762
  category_filter: Optional[List[str]],
745
763
  limit: int,
746
764
  ):
747
- """Execute fallback LIKE search"""
748
- results = []
749
-
750
- # Search short-term memory
751
- category_clause = ""
752
- params = [namespace, f"%{query}%", f"%{query}%", datetime.now()]
753
-
754
- if category_filter:
755
- category_placeholders = ",".join("?" * len(category_filter))
756
- category_clause = f"AND category_primary IN ({category_placeholders})"
757
- params.extend(category_filter)
758
-
759
- params.append(limit)
760
-
761
- cursor.execute(
762
- f"""
763
- SELECT *, 'short_term' as memory_type FROM short_term_memory
764
- WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
765
- AND (expires_at IS NULL OR expires_at > ?) {category_clause}
766
- ORDER BY importance_score DESC, created_at DESC
767
- LIMIT ?
768
- """,
769
- params,
770
- )
771
- results.extend([dict(row) for row in cursor.fetchall()])
765
+ """Execute fallback LIKE search with proper input validation"""
766
+ try:
767
+ # Input validation and sanitization
768
+ sanitized_query = str(query).strip() if query else ""
769
+ sanitized_namespace = str(namespace).strip()
770
+ sanitized_limit = max(1, min(int(limit), 1000)) # Limit between 1 and 1000
771
+ current_timestamp = datetime.now()
772
+
773
+ results = []
774
+
775
+ # Validate and sanitize category filter
776
+ sanitized_categories = []
777
+ if category_filter and isinstance(category_filter, list):
778
+ sanitized_categories = [
779
+ str(cat).strip() for cat in category_filter if cat
780
+ ]
781
+
782
+ # Search short-term memory with parameterized query
783
+ if sanitized_categories:
784
+ category_placeholders = ",".join(["?"] * len(sanitized_categories))
785
+ short_term_sql = f"""
786
+ SELECT *, 'short_term' as memory_type FROM short_term_memory
787
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
788
+ AND (expires_at IS NULL OR expires_at > ?)
789
+ AND category_primary IN ({category_placeholders})
790
+ ORDER BY importance_score DESC, created_at DESC
791
+ LIMIT ?
792
+ """
793
+ short_term_params = (
794
+ [
795
+ sanitized_namespace,
796
+ f"%{sanitized_query}%",
797
+ f"%{sanitized_query}%",
798
+ current_timestamp,
799
+ ]
800
+ + sanitized_categories
801
+ + [sanitized_limit]
802
+ )
803
+ else:
804
+ short_term_sql = """
805
+ SELECT *, 'short_term' as memory_type FROM short_term_memory
806
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
807
+ AND (expires_at IS NULL OR expires_at > ?)
808
+ ORDER BY importance_score DESC, created_at DESC
809
+ LIMIT ?
810
+ """
811
+ short_term_params = [
812
+ sanitized_namespace,
813
+ f"%{sanitized_query}%",
814
+ f"%{sanitized_query}%",
815
+ current_timestamp,
816
+ sanitized_limit,
817
+ ]
818
+
819
+ cursor.execute(short_term_sql, short_term_params)
820
+ results.extend([dict(row) for row in cursor.fetchall()])
821
+
822
+ # Search long-term memory with parameterized query
823
+ if sanitized_categories:
824
+ category_placeholders = ",".join(["?"] * len(sanitized_categories))
825
+ long_term_sql = f"""
826
+ SELECT *, 'long_term' as memory_type FROM long_term_memory
827
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
828
+ AND category_primary IN ({category_placeholders})
829
+ ORDER BY importance_score DESC, created_at DESC
830
+ LIMIT ?
831
+ """
832
+ long_term_params = (
833
+ [
834
+ sanitized_namespace,
835
+ f"%{sanitized_query}%",
836
+ f"%{sanitized_query}%",
837
+ ]
838
+ + sanitized_categories
839
+ + [sanitized_limit]
840
+ )
841
+ else:
842
+ long_term_sql = """
843
+ SELECT *, 'long_term' as memory_type FROM long_term_memory
844
+ WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?)
845
+ ORDER BY importance_score DESC, created_at DESC
846
+ LIMIT ?
847
+ """
848
+ long_term_params = [
849
+ sanitized_namespace,
850
+ f"%{sanitized_query}%",
851
+ f"%{sanitized_query}%",
852
+ sanitized_limit,
853
+ ]
772
854
 
773
- # Search long-term memory
774
- params = [namespace, f"%{query}%", f"%{query}%"]
775
- if category_filter:
776
- params.extend(category_filter)
777
- params.append(limit)
855
+ cursor.execute(long_term_sql, long_term_params)
856
+ results.extend([dict(row) for row in cursor.fetchall()])
778
857
 
779
- cursor.execute(
780
- f"""
781
- SELECT *, 'long_term' as memory_type FROM long_term_memory
782
- WHERE namespace = ? AND (searchable_content LIKE ? OR summary LIKE ?) {category_clause}
783
- ORDER BY importance_score DESC, created_at DESC
784
- LIMIT ?
785
- """,
786
- params,
787
- )
788
- results.extend([dict(row) for row in cursor.fetchall()])
858
+ return results[:sanitized_limit] # Ensure final limit
789
859
 
790
- return results
860
+ except Exception as e:
861
+ logger.error(f"LIKE search error: {e}")
862
+ return []
791
863
 
792
864
  def _calculate_recency_score(self, created_at_str: str) -> float:
793
865
  """Calculate recency score (0-1, newer = higher)"""
@@ -801,47 +873,10 @@ class DatabaseManager:
801
873
  except:
802
874
  return 0.0
803
875
 
804
- def _search_by_entities(
805
- self, cursor: sqlite3.Cursor, query: str, namespace: str, limit: int
806
- ) -> List[Dict[str, Any]]:
807
- """Search memories by entity matching"""
808
- cursor.execute(
809
- """
810
- SELECT
811
- e.memory_id, e.memory_type, e.relevance_score,
812
- CASE
813
- WHEN e.memory_type = 'short_term' THEN st.processed_data
814
- WHEN e.memory_type = 'long_term' THEN lt.processed_data
815
- END as processed_data,
816
- CASE
817
- WHEN e.memory_type = 'short_term' THEN st.importance_score
818
- WHEN e.memory_type = 'long_term' THEN lt.importance_score
819
- END as importance_score,
820
- CASE
821
- WHEN e.memory_type = 'short_term' THEN st.category_primary
822
- WHEN e.memory_type = 'long_term' THEN lt.category_primary
823
- END as category_primary,
824
- CASE
825
- WHEN e.memory_type = 'short_term' THEN st.created_at
826
- WHEN e.memory_type = 'long_term' THEN lt.created_at
827
- END as created_at
828
- FROM memory_entities e
829
- LEFT JOIN short_term_memory st ON e.memory_id = st.memory_id AND e.memory_type = 'short_term'
830
- LEFT JOIN long_term_memory lt ON e.memory_id = lt.memory_id AND e.memory_type = 'long_term'
831
- WHERE e.namespace = ? AND e.entity_value LIKE ?
832
- ORDER BY e.relevance_score DESC, importance_score DESC
833
- LIMIT ?
834
- """,
835
- (namespace, f"%{query}%", limit),
836
- )
837
-
838
- return [dict(row) for row in cursor.fetchall()]
839
-
840
876
  def _determine_storage_location(self, memory: ProcessedMemory) -> str:
841
877
  """Determine where to store the memory based on its properties"""
842
- if memory.category.primary_category == MemoryCategoryType.rule:
843
- return "rules_memory"
844
- elif memory.importance.retention_type in [
878
+ # Note: rules_memory removed in streamlined schema - rules stored as long_term
879
+ if memory.importance.retention_type in [
845
880
  RetentionType.long_term,
846
881
  RetentionType.permanent,
847
882
  ]:
@@ -874,15 +909,9 @@ class DatabaseManager:
874
909
  )
875
910
  stats["long_term_count"] = cursor.fetchone()[0]
876
911
 
877
- cursor.execute(
878
- "SELECT COUNT(*) FROM rules_memory WHERE namespace = ?", (namespace,)
879
- )
880
- stats["rules_count"] = cursor.fetchone()[0]
881
-
882
- cursor.execute(
883
- "SELECT COUNT(*) FROM memory_entities WHERE namespace = ?", (namespace,)
884
- )
885
- stats["total_entities"] = cursor.fetchone()[0]
912
+ # Note: rules_memory and memory_entities tables removed in v2.0 streamlined schema
913
+ stats["rules_count"] = 0
914
+ stats["total_entities"] = 0
886
915
 
887
916
  # Category breakdown
888
917
  cursor.execute(
@@ -927,18 +956,10 @@ class DatabaseManager:
927
956
  cursor = conn.cursor()
928
957
 
929
958
  if memory_type == "short_term":
930
- cursor.execute(
931
- "DELETE FROM memory_entities WHERE namespace = ? AND memory_type = 'short_term'",
932
- (namespace,),
933
- )
934
959
  cursor.execute(
935
960
  "DELETE FROM short_term_memory WHERE namespace = ?", (namespace,)
936
961
  )
937
962
  elif memory_type == "long_term":
938
- cursor.execute(
939
- "DELETE FROM memory_entities WHERE namespace = ? AND memory_type = 'long_term'",
940
- (namespace,),
941
- )
942
963
  cursor.execute(
943
964
  "DELETE FROM long_term_memory WHERE namespace = ?", (namespace,)
944
965
  )
@@ -947,18 +968,12 @@ class DatabaseManager:
947
968
  "DELETE FROM chat_history WHERE namespace = ?", (namespace,)
948
969
  )
949
970
  else: # Clear all
950
- cursor.execute(
951
- "DELETE FROM memory_entities WHERE namespace = ?", (namespace,)
952
- )
953
971
  cursor.execute(
954
972
  "DELETE FROM short_term_memory WHERE namespace = ?", (namespace,)
955
973
  )
956
974
  cursor.execute(
957
975
  "DELETE FROM long_term_memory WHERE namespace = ?", (namespace,)
958
976
  )
959
- cursor.execute(
960
- "DELETE FROM rules_memory WHERE namespace = ?", (namespace,)
961
- )
962
977
  cursor.execute(
963
978
  "DELETE FROM chat_history WHERE namespace = ?", (namespace,)
964
979
  )