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.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +491 -68
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1683 -532
- memori/core/providers.py +217 -0
- memori/database/adapters/__init__.py +10 -0
- memori/database/adapters/mysql_adapter.py +331 -0
- memori/database/adapters/postgresql_adapter.py +291 -0
- memori/database/adapters/sqlite_adapter.py +229 -0
- memori/database/auto_creator.py +320 -0
- memori/database/connection_utils.py +207 -0
- memori/database/connectors/base_connector.py +283 -0
- memori/database/connectors/mysql_connector.py +240 -18
- memori/database/connectors/postgres_connector.py +277 -4
- memori/database/connectors/sqlite_connector.py +178 -3
- memori/database/models.py +400 -0
- memori/database/queries/base_queries.py +1 -1
- memori/database/queries/memory_queries.py +91 -2
- memori/database/query_translator.py +222 -0
- memori/database/schema_generators/__init__.py +7 -0
- memori/database/schema_generators/mysql_schema_generator.py +215 -0
- memori/database/search/__init__.py +8 -0
- memori/database/search/mysql_search_adapter.py +255 -0
- memori/database/search/sqlite_search_adapter.py +180 -0
- memori/database/search_service.py +700 -0
- memori/database/sqlalchemy_manager.py +888 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- memori/tools/memory_tool.py +94 -4
- memori/utils/input_validator.py +395 -0
- memori/utils/pydantic_models.py +138 -36
- memori/utils/query_builder.py +530 -0
- memori/utils/security_audit.py +594 -0
- memori/utils/security_integration.py +339 -0
- memori/utils/transaction_manager.py +547 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/METADATA +56 -23
- memorisdk-2.0.1.dist-info/RECORD +66 -0
- memori/scripts/llm_text.py +0 -50
- memorisdk-1.0.2.dist-info/RECORD +0 -44
- memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
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
|
|
316
|
-
self, memory:
|
|
336
|
+
def store_long_term_memory_enhanced(
|
|
337
|
+
self, memory: ProcessedLongTermMemory, chat_id: str, namespace: str = "default"
|
|
317
338
|
) -> str:
|
|
318
|
-
"""Store a
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
328
|
-
cursor = conn.cursor()
|
|
400
|
+
operations.append(insert_operation)
|
|
329
401
|
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
348
|
-
logger.debug(f"Stored memory {memory_id}
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
608
|
+
# Build parameterized query - avoid string concatenation
|
|
609
|
+
params = [fts_query, namespace]
|
|
643
610
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
683
|
-
|
|
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
|
-
|
|
722
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
843
|
-
|
|
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
|
-
|
|
878
|
-
|
|
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
|
)
|