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
@@ -0,0 +1,700 @@
1
+ """
2
+ SQLAlchemy-based search service for Memori v2.0
3
+ Provides cross-database full-text search capabilities
4
+ """
5
+
6
+ from datetime import datetime
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from loguru import logger
10
+ from sqlalchemy import and_, desc, or_, text
11
+ from sqlalchemy.orm import Session
12
+
13
+ from .models import LongTermMemory, ShortTermMemory
14
+
15
+
16
+ class SearchService:
17
+ """Cross-database search service using SQLAlchemy"""
18
+
19
+ def __init__(self, session: Session, database_type: str):
20
+ self.session = session
21
+ self.database_type = database_type
22
+
23
+ def search_memories(
24
+ self,
25
+ query: str,
26
+ namespace: str = "default",
27
+ category_filter: Optional[List[str]] = None,
28
+ limit: int = 10,
29
+ memory_types: Optional[List[str]] = None,
30
+ ) -> List[Dict[str, Any]]:
31
+ """
32
+ Search memories across different database backends
33
+
34
+ Args:
35
+ query: Search query string
36
+ namespace: Memory namespace
37
+ category_filter: List of categories to filter by
38
+ limit: Maximum number of results
39
+ memory_types: Types of memory to search ('short_term', 'long_term', or both)
40
+
41
+ Returns:
42
+ List of memory dictionaries with search metadata
43
+ """
44
+ logger.debug(
45
+ f"SearchService.search_memories called - query: '{query}', namespace: '{namespace}', database: {self.database_type}, limit: {limit}"
46
+ )
47
+
48
+ if not query or not query.strip():
49
+ logger.debug("Empty query provided, returning recent memories")
50
+ return self._get_recent_memories(
51
+ namespace, category_filter, limit, memory_types
52
+ )
53
+
54
+ results = []
55
+
56
+ # Determine which memory types to search
57
+ search_short_term = not memory_types or "short_term" in memory_types
58
+ search_long_term = not memory_types or "long_term" in memory_types
59
+
60
+ logger.debug(
61
+ f"Memory types to search - short_term: {search_short_term}, long_term: {search_long_term}, categories: {category_filter}"
62
+ )
63
+
64
+ try:
65
+ # Try database-specific full-text search first
66
+ if self.database_type == "sqlite":
67
+ logger.debug("Using SQLite FTS5 search strategy")
68
+ results = self._search_sqlite_fts(
69
+ query,
70
+ namespace,
71
+ category_filter,
72
+ limit,
73
+ search_short_term,
74
+ search_long_term,
75
+ )
76
+ elif self.database_type == "mysql":
77
+ logger.debug("Using MySQL FULLTEXT search strategy")
78
+ results = self._search_mysql_fulltext(
79
+ query,
80
+ namespace,
81
+ category_filter,
82
+ limit,
83
+ search_short_term,
84
+ search_long_term,
85
+ )
86
+ elif self.database_type == "postgresql":
87
+ logger.debug("Using PostgreSQL FTS search strategy")
88
+ results = self._search_postgresql_fts(
89
+ query,
90
+ namespace,
91
+ category_filter,
92
+ limit,
93
+ search_short_term,
94
+ search_long_term,
95
+ )
96
+
97
+ logger.debug(f"Primary search strategy returned {len(results)} results")
98
+
99
+ # If no results or full-text search failed, fall back to LIKE search
100
+ if not results:
101
+ logger.debug(
102
+ "Primary search returned no results, falling back to LIKE search"
103
+ )
104
+ results = self._search_like_fallback(
105
+ query,
106
+ namespace,
107
+ category_filter,
108
+ limit,
109
+ search_short_term,
110
+ search_long_term,
111
+ )
112
+
113
+ except Exception as e:
114
+ logger.error(
115
+ f"Full-text search failed for query '{query}' in namespace '{namespace}': {e}"
116
+ )
117
+ logger.debug(
118
+ f"Full-text search error details: {type(e).__name__}: {str(e)}",
119
+ exc_info=True,
120
+ )
121
+ logger.warning(f"Falling back to LIKE search for query '{query}'")
122
+ try:
123
+ results = self._search_like_fallback(
124
+ query,
125
+ namespace,
126
+ category_filter,
127
+ limit,
128
+ search_short_term,
129
+ search_long_term,
130
+ )
131
+ logger.debug(f"LIKE fallback search returned {len(results)} results")
132
+ except Exception as fallback_e:
133
+ logger.error(
134
+ f"LIKE fallback search also failed for query '{query}': {fallback_e}"
135
+ )
136
+ results = []
137
+
138
+ final_results = self._rank_and_limit_results(results, limit)
139
+ logger.debug(
140
+ f"SearchService completed - returning {len(final_results)} final results after ranking and limiting"
141
+ )
142
+
143
+ if final_results:
144
+ logger.debug(
145
+ f"Top result: memory_id={final_results[0].get('memory_id')}, score={final_results[0].get('composite_score', 0):.3f}, strategy={final_results[0].get('search_strategy')}"
146
+ )
147
+
148
+ return final_results
149
+
150
+ def _search_sqlite_fts(
151
+ self,
152
+ query: str,
153
+ namespace: str,
154
+ category_filter: Optional[List[str]],
155
+ limit: int,
156
+ search_short_term: bool,
157
+ search_long_term: bool,
158
+ ) -> List[Dict[str, Any]]:
159
+ """Search using SQLite FTS5"""
160
+ try:
161
+ logger.debug(
162
+ f"SQLite FTS search starting for query: '{query}' in namespace: '{namespace}'"
163
+ )
164
+
165
+ # Use parameters to validate search scope
166
+ if not search_short_term and not search_long_term:
167
+ logger.debug("No memory types specified for search, defaulting to both")
168
+ search_short_term = search_long_term = True
169
+
170
+ logger.debug(
171
+ f"Search scope - short_term: {search_short_term}, long_term: {search_long_term}"
172
+ )
173
+
174
+ # Build FTS query
175
+ fts_query = f'"{query.strip()}"'
176
+ logger.debug(f"FTS query built: {fts_query}")
177
+
178
+ # Build category filter
179
+ category_clause = ""
180
+ params = {"fts_query": fts_query, "namespace": namespace}
181
+
182
+ if category_filter:
183
+ category_placeholders = ",".join(
184
+ [f":cat_{i}" for i in range(len(category_filter))]
185
+ )
186
+ category_clause = (
187
+ f"AND fts.category_primary IN ({category_placeholders})"
188
+ )
189
+ for i, cat in enumerate(category_filter):
190
+ params[f"cat_{i}"] = cat
191
+ logger.debug(f"Category filter applied: {category_filter}")
192
+
193
+ # SQLite FTS5 search query with COALESCE to handle NULL values
194
+ sql_query = f"""
195
+ SELECT
196
+ fts.memory_id,
197
+ fts.memory_type,
198
+ fts.category_primary,
199
+ COALESCE(
200
+ CASE
201
+ WHEN fts.memory_type = 'short_term' THEN st.processed_data
202
+ WHEN fts.memory_type = 'long_term' THEN lt.processed_data
203
+ END,
204
+ '{{}}'
205
+ ) as processed_data,
206
+ COALESCE(
207
+ CASE
208
+ WHEN fts.memory_type = 'short_term' THEN st.importance_score
209
+ WHEN fts.memory_type = 'long_term' THEN lt.importance_score
210
+ ELSE 0.5
211
+ END,
212
+ 0.5
213
+ ) as importance_score,
214
+ COALESCE(
215
+ CASE
216
+ WHEN fts.memory_type = 'short_term' THEN st.created_at
217
+ WHEN fts.memory_type = 'long_term' THEN lt.created_at
218
+ END,
219
+ datetime('now')
220
+ ) as created_at,
221
+ COALESCE(fts.summary, '') as summary,
222
+ COALESCE(rank, 0.0) as search_score,
223
+ 'sqlite_fts5' as search_strategy
224
+ FROM memory_search_fts fts
225
+ LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
226
+ LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
227
+ WHERE memory_search_fts MATCH :fts_query AND fts.namespace = :namespace
228
+ {category_clause}
229
+ ORDER BY search_score, importance_score DESC
230
+ LIMIT {limit}
231
+ """
232
+
233
+ logger.debug(f"Executing SQLite FTS query with params: {params}")
234
+ result = self.session.execute(text(sql_query), params)
235
+ rows = [dict(row) for row in result]
236
+ logger.debug(f"SQLite FTS search returned {len(rows)} results")
237
+
238
+ # Log details of first result for debugging
239
+ if rows:
240
+ logger.debug(
241
+ f"Sample result: memory_id={rows[0].get('memory_id')}, type={rows[0].get('memory_type')}, score={rows[0].get('search_score')}"
242
+ )
243
+
244
+ return rows
245
+
246
+ except Exception as e:
247
+ logger.error(
248
+ f"SQLite FTS5 search failed for query '{query}' in namespace '{namespace}': {e}"
249
+ )
250
+ logger.debug(
251
+ f"SQLite FTS5 error details: {type(e).__name__}: {str(e)}",
252
+ exc_info=True,
253
+ )
254
+ # Roll back the transaction to recover from error state
255
+ self.session.rollback()
256
+ return []
257
+
258
+ def _search_mysql_fulltext(
259
+ self,
260
+ query: str,
261
+ namespace: str,
262
+ category_filter: Optional[List[str]],
263
+ limit: int,
264
+ search_short_term: bool,
265
+ search_long_term: bool,
266
+ ) -> List[Dict[str, Any]]:
267
+ """Search using MySQL FULLTEXT"""
268
+ results = []
269
+
270
+ try:
271
+ # Apply limit proportionally between memory types
272
+ short_limit = (
273
+ limit // 2 if search_short_term and search_long_term else limit
274
+ )
275
+ long_limit = (
276
+ limit - short_limit if search_short_term and search_long_term else limit
277
+ )
278
+
279
+ # Search short-term memory if requested
280
+ if search_short_term:
281
+ short_query = self.session.query(ShortTermMemory).filter(
282
+ ShortTermMemory.namespace == namespace
283
+ )
284
+
285
+ # Add FULLTEXT search
286
+ fulltext_condition = text(
287
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE)"
288
+ ).params(query=query)
289
+ short_query = short_query.filter(fulltext_condition)
290
+
291
+ # Add category filter
292
+ if category_filter:
293
+ short_query = short_query.filter(
294
+ ShortTermMemory.category_primary.in_(category_filter)
295
+ )
296
+
297
+ # Add relevance score and limit
298
+ short_results = self.session.execute(
299
+ short_query.statement.add_columns(
300
+ text(
301
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE) as search_score"
302
+ ).params(query=query),
303
+ text("'short_term' as memory_type"),
304
+ text("'mysql_fulltext' as search_strategy"),
305
+ ).limit(short_limit)
306
+ ).fetchall()
307
+
308
+ results.extend([dict(row) for row in short_results])
309
+
310
+ # Search long-term memory if requested
311
+ if search_long_term:
312
+ long_query = self.session.query(LongTermMemory).filter(
313
+ LongTermMemory.namespace == namespace
314
+ )
315
+
316
+ # Add FULLTEXT search
317
+ fulltext_condition = text(
318
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE)"
319
+ ).params(query=query)
320
+ long_query = long_query.filter(fulltext_condition)
321
+
322
+ # Add category filter
323
+ if category_filter:
324
+ long_query = long_query.filter(
325
+ LongTermMemory.category_primary.in_(category_filter)
326
+ )
327
+
328
+ # Add relevance score and limit
329
+ long_results = self.session.execute(
330
+ long_query.statement.add_columns(
331
+ text(
332
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE) as search_score"
333
+ ).params(query=query),
334
+ text("'long_term' as memory_type"),
335
+ text("'mysql_fulltext' as search_strategy"),
336
+ ).limit(long_limit)
337
+ ).fetchall()
338
+
339
+ results.extend([dict(row) for row in long_results])
340
+
341
+ return results
342
+
343
+ except Exception as e:
344
+ logger.error(
345
+ f"MySQL FULLTEXT search failed for query '{query}' in namespace '{namespace}': {e}"
346
+ )
347
+ logger.debug(
348
+ f"MySQL FULLTEXT error details: {type(e).__name__}: {str(e)}",
349
+ exc_info=True,
350
+ )
351
+ # Roll back the transaction to recover from error state
352
+ self.session.rollback()
353
+ return []
354
+
355
+ def _search_postgresql_fts(
356
+ self,
357
+ query: str,
358
+ namespace: str,
359
+ category_filter: Optional[List[str]],
360
+ limit: int,
361
+ search_short_term: bool,
362
+ search_long_term: bool,
363
+ ) -> List[Dict[str, Any]]:
364
+ """Search using PostgreSQL tsvector"""
365
+ results = []
366
+
367
+ try:
368
+ # Apply limit proportionally between memory types
369
+ short_limit = (
370
+ limit // 2 if search_short_term and search_long_term else limit
371
+ )
372
+ long_limit = (
373
+ limit - short_limit if search_short_term and search_long_term else limit
374
+ )
375
+
376
+ # Prepare query for tsquery - handle spaces and special characters
377
+ # Convert simple query to tsquery format (join words with &)
378
+ tsquery_text = " & ".join(query.split())
379
+
380
+ # Search short-term memory if requested
381
+ if search_short_term:
382
+ short_query = self.session.query(ShortTermMemory).filter(
383
+ ShortTermMemory.namespace == namespace
384
+ )
385
+
386
+ # Add tsvector search
387
+ ts_query = text(
388
+ "search_vector @@ to_tsquery('english', :query)"
389
+ ).params(query=tsquery_text)
390
+ short_query = short_query.filter(ts_query)
391
+
392
+ # Add category filter
393
+ if category_filter:
394
+ short_query = short_query.filter(
395
+ ShortTermMemory.category_primary.in_(category_filter)
396
+ )
397
+
398
+ # Add relevance score and limit
399
+ short_results = self.session.execute(
400
+ short_query.statement.add_columns(
401
+ text(
402
+ "ts_rank(search_vector, to_tsquery('english', :query)) as search_score"
403
+ ).params(query=tsquery_text),
404
+ text("'short_term' as memory_type"),
405
+ text("'postgresql_fts' as search_strategy"),
406
+ )
407
+ .order_by(text("search_score DESC"))
408
+ .limit(short_limit)
409
+ ).fetchall()
410
+
411
+ results.extend([dict(row) for row in short_results])
412
+
413
+ # Search long-term memory if requested
414
+ if search_long_term:
415
+ long_query = self.session.query(LongTermMemory).filter(
416
+ LongTermMemory.namespace == namespace
417
+ )
418
+
419
+ # Add tsvector search
420
+ ts_query = text(
421
+ "search_vector @@ to_tsquery('english', :query)"
422
+ ).params(query=tsquery_text)
423
+ long_query = long_query.filter(ts_query)
424
+
425
+ # Add category filter
426
+ if category_filter:
427
+ long_query = long_query.filter(
428
+ LongTermMemory.category_primary.in_(category_filter)
429
+ )
430
+
431
+ # Add relevance score and limit
432
+ long_results = self.session.execute(
433
+ long_query.statement.add_columns(
434
+ text(
435
+ "ts_rank(search_vector, to_tsquery('english', :query)) as search_score"
436
+ ).params(query=tsquery_text),
437
+ text("'long_term' as memory_type"),
438
+ text("'postgresql_fts' as search_strategy"),
439
+ )
440
+ .order_by(text("search_score DESC"))
441
+ .limit(long_limit)
442
+ ).fetchall()
443
+
444
+ results.extend([dict(row) for row in long_results])
445
+
446
+ return results
447
+
448
+ except Exception as e:
449
+ logger.error(
450
+ f"PostgreSQL FTS search failed for query '{query}' in namespace '{namespace}': {e}"
451
+ )
452
+ logger.debug(
453
+ f"PostgreSQL FTS error details: {type(e).__name__}: {str(e)}",
454
+ exc_info=True,
455
+ )
456
+ # Roll back the transaction to recover from error state
457
+ self.session.rollback()
458
+ return []
459
+
460
+ def _search_like_fallback(
461
+ self,
462
+ query: str,
463
+ namespace: str,
464
+ category_filter: Optional[List[str]],
465
+ limit: int,
466
+ search_short_term: bool,
467
+ search_long_term: bool,
468
+ ) -> List[Dict[str, Any]]:
469
+ """Fallback LIKE-based search with improved flexibility"""
470
+ logger.debug(
471
+ f"Starting LIKE fallback search for query: '{query}' in namespace: '{namespace}'"
472
+ )
473
+ results = []
474
+
475
+ # Create multiple search patterns for better matching
476
+ search_patterns = [
477
+ f"%{query}%", # Original full query
478
+ ]
479
+
480
+ # Add individual word patterns for better matching
481
+ words = query.strip().split()
482
+ if len(words) > 1:
483
+ for word in words:
484
+ if len(word) > 2: # Skip very short words
485
+ search_patterns.append(f"%{word}%")
486
+
487
+ logger.debug(f"LIKE search patterns: {search_patterns}")
488
+
489
+ # Search short-term memory
490
+ if search_short_term:
491
+ # Build OR conditions for all search patterns
492
+ search_conditions = []
493
+ for pattern in search_patterns:
494
+ search_conditions.extend(
495
+ [
496
+ ShortTermMemory.searchable_content.like(pattern),
497
+ ShortTermMemory.summary.like(pattern),
498
+ ]
499
+ )
500
+
501
+ short_query = self.session.query(ShortTermMemory).filter(
502
+ and_(
503
+ ShortTermMemory.namespace == namespace,
504
+ or_(*search_conditions),
505
+ )
506
+ )
507
+
508
+ if category_filter:
509
+ short_query = short_query.filter(
510
+ ShortTermMemory.category_primary.in_(category_filter)
511
+ )
512
+
513
+ short_results = (
514
+ short_query.order_by(
515
+ desc(ShortTermMemory.importance_score),
516
+ desc(ShortTermMemory.created_at),
517
+ )
518
+ .limit(limit)
519
+ .all()
520
+ )
521
+
522
+ logger.debug(f"LIKE fallback found {len(short_results)} short-term results")
523
+
524
+ for result in short_results:
525
+ memory_dict = {
526
+ "memory_id": result.memory_id,
527
+ "memory_type": "short_term",
528
+ "processed_data": result.processed_data,
529
+ "importance_score": result.importance_score,
530
+ "created_at": result.created_at,
531
+ "summary": result.summary,
532
+ "category_primary": result.category_primary,
533
+ "search_score": 0.4, # Fixed score for LIKE search
534
+ "search_strategy": f"{self.database_type}_like_fallback",
535
+ }
536
+ results.append(memory_dict)
537
+
538
+ # Search long-term memory
539
+ if search_long_term:
540
+ # Build OR conditions for all search patterns
541
+ search_conditions = []
542
+ for pattern in search_patterns:
543
+ search_conditions.extend(
544
+ [
545
+ LongTermMemory.searchable_content.like(pattern),
546
+ LongTermMemory.summary.like(pattern),
547
+ ]
548
+ )
549
+
550
+ long_query = self.session.query(LongTermMemory).filter(
551
+ and_(
552
+ LongTermMemory.namespace == namespace,
553
+ or_(*search_conditions),
554
+ )
555
+ )
556
+
557
+ if category_filter:
558
+ long_query = long_query.filter(
559
+ LongTermMemory.category_primary.in_(category_filter)
560
+ )
561
+
562
+ long_results = (
563
+ long_query.order_by(
564
+ desc(LongTermMemory.importance_score),
565
+ desc(LongTermMemory.created_at),
566
+ )
567
+ .limit(limit)
568
+ .all()
569
+ )
570
+
571
+ logger.debug(f"LIKE fallback found {len(long_results)} long-term results")
572
+
573
+ for result in long_results:
574
+ memory_dict = {
575
+ "memory_id": result.memory_id,
576
+ "memory_type": "long_term",
577
+ "processed_data": result.processed_data,
578
+ "importance_score": result.importance_score,
579
+ "created_at": result.created_at,
580
+ "summary": result.summary,
581
+ "category_primary": result.category_primary,
582
+ "search_score": 0.4, # Fixed score for LIKE search
583
+ "search_strategy": f"{self.database_type}_like_fallback",
584
+ }
585
+ results.append(memory_dict)
586
+
587
+ logger.debug(
588
+ f"LIKE fallback search completed, returning {len(results)} total results"
589
+ )
590
+ return results
591
+
592
+ def _get_recent_memories(
593
+ self,
594
+ namespace: str,
595
+ category_filter: Optional[List[str]],
596
+ limit: int,
597
+ memory_types: Optional[List[str]],
598
+ ) -> List[Dict[str, Any]]:
599
+ """Get recent memories when no search query is provided"""
600
+ results = []
601
+
602
+ search_short_term = not memory_types or "short_term" in memory_types
603
+ search_long_term = not memory_types or "long_term" in memory_types
604
+
605
+ # Get recent short-term memories
606
+ if search_short_term:
607
+ short_query = self.session.query(ShortTermMemory).filter(
608
+ ShortTermMemory.namespace == namespace
609
+ )
610
+
611
+ if category_filter:
612
+ short_query = short_query.filter(
613
+ ShortTermMemory.category_primary.in_(category_filter)
614
+ )
615
+
616
+ short_results = (
617
+ short_query.order_by(desc(ShortTermMemory.created_at))
618
+ .limit(limit // 2)
619
+ .all()
620
+ )
621
+
622
+ for result in short_results:
623
+ memory_dict = {
624
+ "memory_id": result.memory_id,
625
+ "memory_type": "short_term",
626
+ "processed_data": result.processed_data,
627
+ "importance_score": result.importance_score,
628
+ "created_at": result.created_at,
629
+ "summary": result.summary,
630
+ "category_primary": result.category_primary,
631
+ "search_score": 1.0,
632
+ "search_strategy": "recent_memories",
633
+ }
634
+ results.append(memory_dict)
635
+
636
+ # Get recent long-term memories
637
+ if search_long_term:
638
+ long_query = self.session.query(LongTermMemory).filter(
639
+ LongTermMemory.namespace == namespace
640
+ )
641
+
642
+ if category_filter:
643
+ long_query = long_query.filter(
644
+ LongTermMemory.category_primary.in_(category_filter)
645
+ )
646
+
647
+ long_results = (
648
+ long_query.order_by(desc(LongTermMemory.created_at))
649
+ .limit(limit // 2)
650
+ .all()
651
+ )
652
+
653
+ for result in long_results:
654
+ memory_dict = {
655
+ "memory_id": result.memory_id,
656
+ "memory_type": "long_term",
657
+ "processed_data": result.processed_data,
658
+ "importance_score": result.importance_score,
659
+ "created_at": result.created_at,
660
+ "summary": result.summary,
661
+ "category_primary": result.category_primary,
662
+ "search_score": 1.0,
663
+ "search_strategy": "recent_memories",
664
+ }
665
+ results.append(memory_dict)
666
+
667
+ return results
668
+
669
+ def _rank_and_limit_results(
670
+ self, results: List[Dict[str, Any]], limit: int
671
+ ) -> List[Dict[str, Any]]:
672
+ """Rank and limit search results"""
673
+ # Calculate composite score
674
+ for result in results:
675
+ search_score = result.get("search_score", 0.4)
676
+ importance_score = result.get("importance_score", 0.5)
677
+ recency_score = self._calculate_recency_score(result.get("created_at"))
678
+
679
+ # Weighted composite score
680
+ result["composite_score"] = (
681
+ search_score * 0.5 + importance_score * 0.3 + recency_score * 0.2
682
+ )
683
+
684
+ # Sort by composite score and limit
685
+ results.sort(key=lambda x: x.get("composite_score", 0), reverse=True)
686
+ return results[:limit]
687
+
688
+ def _calculate_recency_score(self, created_at) -> float:
689
+ """Calculate recency score (0-1, newer = higher)"""
690
+ try:
691
+ if not created_at:
692
+ return 0.0
693
+
694
+ if isinstance(created_at, str):
695
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
696
+
697
+ days_old = (datetime.now() - created_at).days
698
+ return max(0, 1 - (days_old / 30)) # Full score for recent, 0 after 30 days
699
+ except:
700
+ return 0.0