memorisdk 1.0.2__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of memorisdk might be problematic. Click here for more details.

Files changed (46) hide show
  1. memori/__init__.py +24 -8
  2. memori/agents/conscious_agent.py +252 -414
  3. memori/agents/memory_agent.py +487 -224
  4. memori/agents/retrieval_agent.py +416 -60
  5. memori/config/memory_manager.py +323 -0
  6. memori/core/conversation.py +393 -0
  7. memori/core/database.py +386 -371
  8. memori/core/memory.py +1638 -531
  9. memori/core/providers.py +217 -0
  10. memori/database/adapters/__init__.py +10 -0
  11. memori/database/adapters/mysql_adapter.py +331 -0
  12. memori/database/adapters/postgresql_adapter.py +291 -0
  13. memori/database/adapters/sqlite_adapter.py +229 -0
  14. memori/database/auto_creator.py +320 -0
  15. memori/database/connection_utils.py +207 -0
  16. memori/database/connectors/base_connector.py +283 -0
  17. memori/database/connectors/mysql_connector.py +240 -18
  18. memori/database/connectors/postgres_connector.py +277 -4
  19. memori/database/connectors/sqlite_connector.py +178 -3
  20. memori/database/models.py +400 -0
  21. memori/database/queries/base_queries.py +1 -1
  22. memori/database/queries/memory_queries.py +91 -2
  23. memori/database/query_translator.py +222 -0
  24. memori/database/schema_generators/__init__.py +7 -0
  25. memori/database/schema_generators/mysql_schema_generator.py +215 -0
  26. memori/database/search/__init__.py +8 -0
  27. memori/database/search/mysql_search_adapter.py +255 -0
  28. memori/database/search/sqlite_search_adapter.py +180 -0
  29. memori/database/search_service.py +548 -0
  30. memori/database/sqlalchemy_manager.py +839 -0
  31. memori/integrations/__init__.py +36 -11
  32. memori/integrations/litellm_integration.py +340 -6
  33. memori/integrations/openai_integration.py +506 -240
  34. memori/utils/input_validator.py +395 -0
  35. memori/utils/pydantic_models.py +138 -36
  36. memori/utils/query_builder.py +530 -0
  37. memori/utils/security_audit.py +594 -0
  38. memori/utils/security_integration.py +339 -0
  39. memori/utils/transaction_manager.py +547 -0
  40. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/METADATA +44 -17
  41. memorisdk-2.0.0.dist-info/RECORD +67 -0
  42. memorisdk-1.0.2.dist-info/RECORD +0 -44
  43. memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
  44. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
  45. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
  46. {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,548 @@
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
+ if not query or not query.strip():
45
+ return self._get_recent_memories(
46
+ namespace, category_filter, limit, memory_types
47
+ )
48
+
49
+ results = []
50
+
51
+ # Determine which memory types to search
52
+ search_short_term = not memory_types or "short_term" in memory_types
53
+ search_long_term = not memory_types or "long_term" in memory_types
54
+
55
+ try:
56
+ # Try database-specific full-text search first
57
+ if self.database_type == "sqlite":
58
+ results = self._search_sqlite_fts(
59
+ query,
60
+ namespace,
61
+ category_filter,
62
+ limit,
63
+ search_short_term,
64
+ search_long_term,
65
+ )
66
+ elif self.database_type == "mysql":
67
+ results = self._search_mysql_fulltext(
68
+ query,
69
+ namespace,
70
+ category_filter,
71
+ limit,
72
+ search_short_term,
73
+ search_long_term,
74
+ )
75
+ elif self.database_type == "postgresql":
76
+ results = self._search_postgresql_fts(
77
+ query,
78
+ namespace,
79
+ category_filter,
80
+ limit,
81
+ search_short_term,
82
+ search_long_term,
83
+ )
84
+
85
+ # If no results or full-text search failed, fall back to LIKE search
86
+ if not results:
87
+ results = self._search_like_fallback(
88
+ query,
89
+ namespace,
90
+ category_filter,
91
+ limit,
92
+ search_short_term,
93
+ search_long_term,
94
+ )
95
+
96
+ except Exception as e:
97
+ logger.warning(f"Full-text search failed: {e}, falling back to LIKE search")
98
+ results = self._search_like_fallback(
99
+ query,
100
+ namespace,
101
+ category_filter,
102
+ limit,
103
+ search_short_term,
104
+ search_long_term,
105
+ )
106
+
107
+ return self._rank_and_limit_results(results, limit)
108
+
109
+ def _search_sqlite_fts(
110
+ self,
111
+ query: str,
112
+ namespace: str,
113
+ category_filter: Optional[List[str]],
114
+ limit: int,
115
+ search_short_term: bool,
116
+ search_long_term: bool,
117
+ ) -> List[Dict[str, Any]]:
118
+ """Search using SQLite FTS5"""
119
+ try:
120
+ # Build FTS query
121
+ fts_query = f'"{query.strip()}"'
122
+
123
+ # Build category filter
124
+ category_clause = ""
125
+ params = {"fts_query": fts_query, "namespace": namespace}
126
+
127
+ if category_filter:
128
+ category_placeholders = ",".join(
129
+ [f":cat_{i}" for i in range(len(category_filter))]
130
+ )
131
+ category_clause = (
132
+ f"AND fts.category_primary IN ({category_placeholders})"
133
+ )
134
+ for i, cat in enumerate(category_filter):
135
+ params[f"cat_{i}"] = cat
136
+
137
+ # SQLite FTS5 search query
138
+ sql_query = f"""
139
+ SELECT
140
+ fts.memory_id, fts.memory_type, fts.category_primary,
141
+ CASE
142
+ WHEN fts.memory_type = 'short_term' THEN st.processed_data
143
+ WHEN fts.memory_type = 'long_term' THEN lt.processed_data
144
+ END as processed_data,
145
+ CASE
146
+ WHEN fts.memory_type = 'short_term' THEN st.importance_score
147
+ WHEN fts.memory_type = 'long_term' THEN lt.importance_score
148
+ ELSE 0.5
149
+ END as importance_score,
150
+ CASE
151
+ WHEN fts.memory_type = 'short_term' THEN st.created_at
152
+ WHEN fts.memory_type = 'long_term' THEN lt.created_at
153
+ END as created_at,
154
+ fts.summary,
155
+ rank as search_score,
156
+ 'sqlite_fts5' as search_strategy
157
+ FROM memory_search_fts fts
158
+ LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
159
+ LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
160
+ WHERE memory_search_fts MATCH :fts_query AND fts.namespace = :namespace
161
+ {category_clause}
162
+ ORDER BY rank, importance_score DESC
163
+ LIMIT {limit}
164
+ """
165
+
166
+ result = self.session.execute(text(sql_query), params)
167
+ return [dict(row) for row in result]
168
+
169
+ except Exception as e:
170
+ logger.debug(f"SQLite FTS5 search failed: {e}")
171
+ # Roll back the transaction to recover from error state
172
+ self.session.rollback()
173
+ return []
174
+
175
+ def _search_mysql_fulltext(
176
+ self,
177
+ query: str,
178
+ namespace: str,
179
+ category_filter: Optional[List[str]],
180
+ limit: int,
181
+ search_short_term: bool,
182
+ search_long_term: bool,
183
+ ) -> List[Dict[str, Any]]:
184
+ """Search using MySQL FULLTEXT"""
185
+ results = []
186
+
187
+ try:
188
+ # Search short-term memory if requested
189
+ if search_short_term:
190
+ short_query = self.session.query(ShortTermMemory).filter(
191
+ ShortTermMemory.namespace == namespace
192
+ )
193
+
194
+ # Add FULLTEXT search
195
+ fulltext_condition = text(
196
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE)"
197
+ ).params(query=query)
198
+ short_query = short_query.filter(fulltext_condition)
199
+
200
+ # Add category filter
201
+ if category_filter:
202
+ short_query = short_query.filter(
203
+ ShortTermMemory.category_primary.in_(category_filter)
204
+ )
205
+
206
+ # Add relevance score
207
+ short_results = self.session.execute(
208
+ short_query.statement.add_columns(
209
+ text(
210
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE) as search_score"
211
+ ).params(query=query),
212
+ text("'short_term' as memory_type"),
213
+ text("'mysql_fulltext' as search_strategy"),
214
+ )
215
+ ).fetchall()
216
+
217
+ results.extend([dict(row) for row in short_results])
218
+
219
+ # Search long-term memory if requested
220
+ if search_long_term:
221
+ long_query = self.session.query(LongTermMemory).filter(
222
+ LongTermMemory.namespace == namespace
223
+ )
224
+
225
+ # Add FULLTEXT search
226
+ fulltext_condition = text(
227
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE)"
228
+ ).params(query=query)
229
+ long_query = long_query.filter(fulltext_condition)
230
+
231
+ # Add category filter
232
+ if category_filter:
233
+ long_query = long_query.filter(
234
+ LongTermMemory.category_primary.in_(category_filter)
235
+ )
236
+
237
+ # Add relevance score
238
+ long_results = self.session.execute(
239
+ long_query.statement.add_columns(
240
+ text(
241
+ "MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE) as search_score"
242
+ ).params(query=query),
243
+ text("'long_term' as memory_type"),
244
+ text("'mysql_fulltext' as search_strategy"),
245
+ )
246
+ ).fetchall()
247
+
248
+ results.extend([dict(row) for row in long_results])
249
+
250
+ return results
251
+
252
+ except Exception as e:
253
+ logger.debug(f"MySQL FULLTEXT search failed: {e}")
254
+ # Roll back the transaction to recover from error state
255
+ self.session.rollback()
256
+ return []
257
+
258
+ def _search_postgresql_fts(
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 PostgreSQL tsvector"""
268
+ results = []
269
+
270
+ try:
271
+ # Prepare query for tsquery - handle spaces and special characters
272
+ # Convert simple query to tsquery format (join words with &)
273
+ tsquery_text = " & ".join(query.split())
274
+
275
+ # Search short-term memory if requested
276
+ if search_short_term:
277
+ short_query = self.session.query(ShortTermMemory).filter(
278
+ ShortTermMemory.namespace == namespace
279
+ )
280
+
281
+ # Add tsvector search
282
+ ts_query = text(
283
+ "search_vector @@ to_tsquery('english', :query)"
284
+ ).params(query=tsquery_text)
285
+ short_query = short_query.filter(ts_query)
286
+
287
+ # Add category filter
288
+ if category_filter:
289
+ short_query = short_query.filter(
290
+ ShortTermMemory.category_primary.in_(category_filter)
291
+ )
292
+
293
+ # Add relevance score
294
+ short_results = self.session.execute(
295
+ short_query.statement.add_columns(
296
+ text(
297
+ "ts_rank(search_vector, to_tsquery('english', :query)) as search_score"
298
+ ).params(query=tsquery_text),
299
+ text("'short_term' as memory_type"),
300
+ text("'postgresql_fts' as search_strategy"),
301
+ ).order_by(text("search_score DESC"))
302
+ ).fetchall()
303
+
304
+ results.extend([dict(row) for row in short_results])
305
+
306
+ # Search long-term memory if requested
307
+ if search_long_term:
308
+ long_query = self.session.query(LongTermMemory).filter(
309
+ LongTermMemory.namespace == namespace
310
+ )
311
+
312
+ # Add tsvector search
313
+ ts_query = text(
314
+ "search_vector @@ to_tsquery('english', :query)"
315
+ ).params(query=tsquery_text)
316
+ long_query = long_query.filter(ts_query)
317
+
318
+ # Add category filter
319
+ if category_filter:
320
+ long_query = long_query.filter(
321
+ LongTermMemory.category_primary.in_(category_filter)
322
+ )
323
+
324
+ # Add relevance score
325
+ long_results = self.session.execute(
326
+ long_query.statement.add_columns(
327
+ text(
328
+ "ts_rank(search_vector, to_tsquery('english', :query)) as search_score"
329
+ ).params(query=tsquery_text),
330
+ text("'long_term' as memory_type"),
331
+ text("'postgresql_fts' as search_strategy"),
332
+ ).order_by(text("search_score DESC"))
333
+ ).fetchall()
334
+
335
+ results.extend([dict(row) for row in long_results])
336
+
337
+ return results
338
+
339
+ except Exception as e:
340
+ logger.debug(f"PostgreSQL FTS search failed: {e}")
341
+ # Roll back the transaction to recover from error state
342
+ self.session.rollback()
343
+ return []
344
+
345
+ def _search_like_fallback(
346
+ self,
347
+ query: str,
348
+ namespace: str,
349
+ category_filter: Optional[List[str]],
350
+ limit: int,
351
+ search_short_term: bool,
352
+ search_long_term: bool,
353
+ ) -> List[Dict[str, Any]]:
354
+ """Fallback LIKE-based search"""
355
+ results = []
356
+ search_pattern = f"%{query}%"
357
+
358
+ # Search short-term memory
359
+ if search_short_term:
360
+ short_query = self.session.query(ShortTermMemory).filter(
361
+ and_(
362
+ ShortTermMemory.namespace == namespace,
363
+ or_(
364
+ ShortTermMemory.searchable_content.like(search_pattern),
365
+ ShortTermMemory.summary.like(search_pattern),
366
+ ),
367
+ )
368
+ )
369
+
370
+ if category_filter:
371
+ short_query = short_query.filter(
372
+ ShortTermMemory.category_primary.in_(category_filter)
373
+ )
374
+
375
+ short_results = (
376
+ short_query.order_by(
377
+ desc(ShortTermMemory.importance_score),
378
+ desc(ShortTermMemory.created_at),
379
+ )
380
+ .limit(limit)
381
+ .all()
382
+ )
383
+
384
+ for result in short_results:
385
+ memory_dict = {
386
+ "memory_id": result.memory_id,
387
+ "memory_type": "short_term",
388
+ "processed_data": result.processed_data,
389
+ "importance_score": result.importance_score,
390
+ "created_at": result.created_at,
391
+ "summary": result.summary,
392
+ "category_primary": result.category_primary,
393
+ "search_score": 0.4, # Fixed score for LIKE search
394
+ "search_strategy": f"{self.database_type}_like_fallback",
395
+ }
396
+ results.append(memory_dict)
397
+
398
+ # Search long-term memory
399
+ if search_long_term:
400
+ long_query = self.session.query(LongTermMemory).filter(
401
+ and_(
402
+ LongTermMemory.namespace == namespace,
403
+ or_(
404
+ LongTermMemory.searchable_content.like(search_pattern),
405
+ LongTermMemory.summary.like(search_pattern),
406
+ ),
407
+ )
408
+ )
409
+
410
+ if category_filter:
411
+ long_query = long_query.filter(
412
+ LongTermMemory.category_primary.in_(category_filter)
413
+ )
414
+
415
+ long_results = (
416
+ long_query.order_by(
417
+ desc(LongTermMemory.importance_score),
418
+ desc(LongTermMemory.created_at),
419
+ )
420
+ .limit(limit)
421
+ .all()
422
+ )
423
+
424
+ for result in long_results:
425
+ memory_dict = {
426
+ "memory_id": result.memory_id,
427
+ "memory_type": "long_term",
428
+ "processed_data": result.processed_data,
429
+ "importance_score": result.importance_score,
430
+ "created_at": result.created_at,
431
+ "summary": result.summary,
432
+ "category_primary": result.category_primary,
433
+ "search_score": 0.4, # Fixed score for LIKE search
434
+ "search_strategy": f"{self.database_type}_like_fallback",
435
+ }
436
+ results.append(memory_dict)
437
+
438
+ return results
439
+
440
+ def _get_recent_memories(
441
+ self,
442
+ namespace: str,
443
+ category_filter: Optional[List[str]],
444
+ limit: int,
445
+ memory_types: Optional[List[str]],
446
+ ) -> List[Dict[str, Any]]:
447
+ """Get recent memories when no search query is provided"""
448
+ results = []
449
+
450
+ search_short_term = not memory_types or "short_term" in memory_types
451
+ search_long_term = not memory_types or "long_term" in memory_types
452
+
453
+ # Get recent short-term memories
454
+ if search_short_term:
455
+ short_query = self.session.query(ShortTermMemory).filter(
456
+ ShortTermMemory.namespace == namespace
457
+ )
458
+
459
+ if category_filter:
460
+ short_query = short_query.filter(
461
+ ShortTermMemory.category_primary.in_(category_filter)
462
+ )
463
+
464
+ short_results = (
465
+ short_query.order_by(desc(ShortTermMemory.created_at))
466
+ .limit(limit // 2)
467
+ .all()
468
+ )
469
+
470
+ for result in short_results:
471
+ memory_dict = {
472
+ "memory_id": result.memory_id,
473
+ "memory_type": "short_term",
474
+ "processed_data": result.processed_data,
475
+ "importance_score": result.importance_score,
476
+ "created_at": result.created_at,
477
+ "summary": result.summary,
478
+ "category_primary": result.category_primary,
479
+ "search_score": 1.0,
480
+ "search_strategy": "recent_memories",
481
+ }
482
+ results.append(memory_dict)
483
+
484
+ # Get recent long-term memories
485
+ if search_long_term:
486
+ long_query = self.session.query(LongTermMemory).filter(
487
+ LongTermMemory.namespace == namespace
488
+ )
489
+
490
+ if category_filter:
491
+ long_query = long_query.filter(
492
+ LongTermMemory.category_primary.in_(category_filter)
493
+ )
494
+
495
+ long_results = (
496
+ long_query.order_by(desc(LongTermMemory.created_at))
497
+ .limit(limit // 2)
498
+ .all()
499
+ )
500
+
501
+ for result in long_results:
502
+ memory_dict = {
503
+ "memory_id": result.memory_id,
504
+ "memory_type": "long_term",
505
+ "processed_data": result.processed_data,
506
+ "importance_score": result.importance_score,
507
+ "created_at": result.created_at,
508
+ "summary": result.summary,
509
+ "category_primary": result.category_primary,
510
+ "search_score": 1.0,
511
+ "search_strategy": "recent_memories",
512
+ }
513
+ results.append(memory_dict)
514
+
515
+ return results
516
+
517
+ def _rank_and_limit_results(
518
+ self, results: List[Dict[str, Any]], limit: int
519
+ ) -> List[Dict[str, Any]]:
520
+ """Rank and limit search results"""
521
+ # Calculate composite score
522
+ for result in results:
523
+ search_score = result.get("search_score", 0.4)
524
+ importance_score = result.get("importance_score", 0.5)
525
+ recency_score = self._calculate_recency_score(result.get("created_at"))
526
+
527
+ # Weighted composite score
528
+ result["composite_score"] = (
529
+ search_score * 0.5 + importance_score * 0.3 + recency_score * 0.2
530
+ )
531
+
532
+ # Sort by composite score and limit
533
+ results.sort(key=lambda x: x.get("composite_score", 0), reverse=True)
534
+ return results[:limit]
535
+
536
+ def _calculate_recency_score(self, created_at) -> float:
537
+ """Calculate recency score (0-1, newer = higher)"""
538
+ try:
539
+ if not created_at:
540
+ return 0.0
541
+
542
+ if isinstance(created_at, str):
543
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
544
+
545
+ days_old = (datetime.now() - created_at).days
546
+ return max(0, 1 - (days_old / 30)) # Full score for recent, 0 after 30 days
547
+ except:
548
+ return 0.0