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,255 @@
1
+ """
2
+ MySQL FULLTEXT search adapter for Memori
3
+ Implements MySQL-specific search functionality
4
+ """
5
+
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from loguru import logger
9
+
10
+ from ..connectors.base_connector import BaseDatabaseConnector, BaseSearchAdapter
11
+
12
+
13
+ class MySQLSearchAdapter(BaseSearchAdapter):
14
+ """MySQL-specific search adapter with FULLTEXT support"""
15
+
16
+ def __init__(self, connector: BaseDatabaseConnector):
17
+ super().__init__(connector)
18
+ self.fulltext_available = self._check_fulltext_support()
19
+
20
+ def _check_fulltext_support(self) -> bool:
21
+ """Check if FULLTEXT search is available and properly configured"""
22
+ try:
23
+ # Check if FULLTEXT indexes exist
24
+ check_query = """
25
+ SELECT COUNT(*) as index_count
26
+ FROM information_schema.STATISTICS
27
+ WHERE table_schema = DATABASE()
28
+ AND index_type = 'FULLTEXT'
29
+ AND table_name IN ('short_term_memory', 'long_term_memory')
30
+ """
31
+
32
+ result = self.connector.execute_query(check_query)
33
+ index_count = result[0]["index_count"] if result else 0
34
+
35
+ if index_count >= 2: # Expect FULLTEXT indexes on both tables
36
+ logger.info("MySQL FULLTEXT indexes found and available")
37
+ return True
38
+ else:
39
+ logger.warning(
40
+ "MySQL FULLTEXT indexes not found, will use LIKE fallback"
41
+ )
42
+ return False
43
+
44
+ except Exception as e:
45
+ logger.warning(f"Could not check MySQL FULLTEXT support: {e}")
46
+ return False
47
+
48
+ def execute_fulltext_search(
49
+ self,
50
+ query: str,
51
+ namespace: str = "default",
52
+ category_filter: Optional[List[str]] = None,
53
+ limit: int = 10,
54
+ ) -> List[Dict[str, Any]]:
55
+ """Execute MySQL FULLTEXT search or fallback to LIKE search"""
56
+ if self.fulltext_available and query and query.strip():
57
+ try:
58
+ return self._execute_mysql_fulltext_search(
59
+ query, namespace, category_filter, limit
60
+ )
61
+ except Exception as e:
62
+ logger.debug(
63
+ f"MySQL FULLTEXT search failed: {e}, falling back to LIKE search"
64
+ )
65
+
66
+ # Fallback to LIKE search
67
+ return self.execute_fallback_search(query, namespace, category_filter, limit)
68
+
69
+ def _execute_mysql_fulltext_search(
70
+ self,
71
+ query: str,
72
+ namespace: str,
73
+ category_filter: Optional[List[str]],
74
+ limit: int,
75
+ ) -> List[Dict[str, Any]]:
76
+ """Execute MySQL FULLTEXT search"""
77
+ results = []
78
+ translated_query = self.translate_search_query(query)
79
+
80
+ # Search short-term memory
81
+ category_clause = ""
82
+ params = [namespace, translated_query]
83
+
84
+ if category_filter:
85
+ category_placeholders = ",".join(["%s"] * len(category_filter))
86
+ category_clause = f"AND category_primary IN ({category_placeholders})"
87
+ params.extend(category_filter)
88
+
89
+ params.append(limit)
90
+
91
+ short_term_query = f"""
92
+ SELECT
93
+ *,
94
+ 'short_term' as memory_type,
95
+ 'mysql_fulltext' as search_strategy,
96
+ MATCH(searchable_content, summary) AGAINST(%s IN NATURAL LANGUAGE MODE) as relevance_score
97
+ FROM short_term_memory
98
+ WHERE namespace = %s
99
+ AND MATCH(searchable_content, summary) AGAINST(%s IN NATURAL LANGUAGE MODE)
100
+ {category_clause}
101
+ ORDER BY relevance_score DESC, importance_score DESC, created_at DESC
102
+ LIMIT %s
103
+ """
104
+
105
+ # Note: MySQL uses %s placeholders, need to adjust params
106
+ mysql_params = [translated_query, namespace, translated_query]
107
+ if category_filter:
108
+ mysql_params.extend(category_filter)
109
+ mysql_params.append(limit)
110
+
111
+ short_results = self.connector.execute_query(short_term_query, mysql_params)
112
+ results.extend(short_results)
113
+
114
+ # Search long-term memory
115
+ long_term_query = f"""
116
+ SELECT
117
+ *,
118
+ 'long_term' as memory_type,
119
+ 'mysql_fulltext' as search_strategy,
120
+ MATCH(searchable_content, summary) AGAINST(%s IN NATURAL LANGUAGE MODE) as relevance_score
121
+ FROM long_term_memory
122
+ WHERE namespace = %s
123
+ AND MATCH(searchable_content, summary) AGAINST(%s IN NATURAL LANGUAGE MODE)
124
+ {category_clause}
125
+ ORDER BY relevance_score DESC, importance_score DESC, created_at DESC
126
+ LIMIT %s
127
+ """
128
+
129
+ long_results = self.connector.execute_query(long_term_query, mysql_params)
130
+ results.extend(long_results)
131
+
132
+ # Sort combined results by relevance and limit
133
+ results.sort(
134
+ key=lambda x: (
135
+ x.get("relevance_score", 0) * 0.5
136
+ + x.get("importance_score", 0) * 0.3
137
+ + self._calculate_recency_score(x.get("created_at")) * 0.2
138
+ ),
139
+ reverse=True,
140
+ )
141
+
142
+ return results[:limit]
143
+
144
+ def _calculate_recency_score(self, created_at) -> float:
145
+ """Calculate recency score for MySQL datetime"""
146
+ try:
147
+ if not created_at:
148
+ return 0.0
149
+
150
+ from datetime import datetime
151
+
152
+ # Handle different datetime formats
153
+ if isinstance(created_at, str):
154
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
155
+ elif hasattr(created_at, "replace"):
156
+ created_at = created_at.replace(tzinfo=None)
157
+
158
+ days_old = (datetime.now() - created_at).days
159
+ return max(0, 1 - (days_old / 30))
160
+ except:
161
+ return 0.0
162
+
163
+ def create_search_indexes(self) -> List[str]:
164
+ """Create MySQL FULLTEXT indexes"""
165
+ commands = [
166
+ # Create FULLTEXT index on short_term_memory
167
+ """
168
+ ALTER TABLE short_term_memory
169
+ ADD FULLTEXT INDEX ft_short_term_search (searchable_content, summary)
170
+ """,
171
+ # Create FULLTEXT index on long_term_memory
172
+ """
173
+ ALTER TABLE long_term_memory
174
+ ADD FULLTEXT INDEX ft_long_term_search (searchable_content, summary)
175
+ """,
176
+ # Additional composite indexes for better performance
177
+ """
178
+ CREATE INDEX IF NOT EXISTS idx_short_term_namespace_category
179
+ ON short_term_memory(namespace, category_primary, importance_score)
180
+ """,
181
+ """
182
+ CREATE INDEX IF NOT EXISTS idx_long_term_namespace_category
183
+ ON long_term_memory(namespace, category_primary, importance_score)
184
+ """,
185
+ ]
186
+
187
+ return commands
188
+
189
+ def translate_search_query(self, query: str) -> str:
190
+ """Translate search query to MySQL FULLTEXT syntax"""
191
+ if not query or not query.strip():
192
+ return ""
193
+
194
+ # Clean and prepare the query for MySQL FULLTEXT
195
+ cleaned_query = query.strip()
196
+
197
+ # For basic natural language search, we can use the query as-is
198
+ # More sophisticated query translation could be added here:
199
+ # - Handle boolean operators (+, -, etc.)
200
+ # - Escape special characters
201
+ # - Add wildcard support
202
+
203
+ return cleaned_query
204
+
205
+ def execute_fallback_search(
206
+ self,
207
+ query: str,
208
+ namespace: str = "default",
209
+ category_filter: Optional[List[str]] = None,
210
+ limit: int = 10,
211
+ ) -> List[Dict[str, Any]]:
212
+ """Execute fallback LIKE-based search with MySQL syntax"""
213
+ results = []
214
+
215
+ # Search short-term memory
216
+ category_clause = ""
217
+ params = [namespace, f"%{query}%", f"%{query}%"]
218
+
219
+ if category_filter:
220
+ category_placeholders = ",".join(["%s"] * len(category_filter))
221
+ category_clause = f"AND category_primary IN ({category_placeholders})"
222
+ params.extend(category_filter)
223
+
224
+ params.append(limit)
225
+
226
+ # Use MySQL-specific placeholder syntax
227
+ mysql_params = [namespace, f"%{query}%", f"%{query}%"]
228
+ if category_filter:
229
+ mysql_params.extend(category_filter)
230
+ mysql_params.append(limit)
231
+
232
+ short_term_query = f"""
233
+ SELECT *, 'short_term' as memory_type, 'mysql_like_fallback' as search_strategy
234
+ FROM short_term_memory
235
+ WHERE namespace = %s AND (searchable_content LIKE %s OR summary LIKE %s)
236
+ {category_clause}
237
+ ORDER BY importance_score DESC, created_at DESC
238
+ LIMIT %s
239
+ """
240
+
241
+ results.extend(self.connector.execute_query(short_term_query, mysql_params))
242
+
243
+ # Search long-term memory
244
+ long_term_query = f"""
245
+ SELECT *, 'long_term' as memory_type, 'mysql_like_fallback' as search_strategy
246
+ FROM long_term_memory
247
+ WHERE namespace = %s AND (searchable_content LIKE %s OR summary LIKE %s)
248
+ {category_clause}
249
+ ORDER BY importance_score DESC, created_at DESC
250
+ LIMIT %s
251
+ """
252
+
253
+ results.extend(self.connector.execute_query(long_term_query, mysql_params))
254
+
255
+ return results[:limit]
@@ -0,0 +1,180 @@
1
+ """
2
+ SQLite FTS5 search adapter for Memori
3
+ Maintains existing FTS5 functionality
4
+ """
5
+
6
+ import sqlite3
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from loguru import logger
10
+
11
+ from ..connectors.base_connector import BaseDatabaseConnector, BaseSearchAdapter
12
+
13
+
14
+ class SQLiteSearchAdapter(BaseSearchAdapter):
15
+ """SQLite-specific search adapter with FTS5 support"""
16
+
17
+ def __init__(self, connector: BaseDatabaseConnector):
18
+ super().__init__(connector)
19
+ self.fts5_available = self._check_fts5_support()
20
+
21
+ def _check_fts5_support(self) -> bool:
22
+ """Check if FTS5 is supported in this SQLite installation"""
23
+ try:
24
+ with self.connector.get_connection() as conn:
25
+ cursor = conn.cursor()
26
+ cursor.execute("CREATE VIRTUAL TABLE fts_test USING fts5(content)")
27
+ cursor.execute("DROP TABLE fts_test")
28
+ return True
29
+ except sqlite3.OperationalError:
30
+ logger.warning("FTS5 not available, falling back to LIKE searches")
31
+ return False
32
+
33
+ def execute_fulltext_search(
34
+ self,
35
+ query: str,
36
+ namespace: str = "default",
37
+ category_filter: Optional[List[str]] = None,
38
+ limit: int = 10,
39
+ ) -> List[Dict[str, Any]]:
40
+ """Execute FTS5 search or fallback to LIKE search"""
41
+ if self.fts5_available:
42
+ try:
43
+ return self._execute_fts5_search(
44
+ query, namespace, category_filter, limit
45
+ )
46
+ except sqlite3.OperationalError as e:
47
+ logger.debug(f"FTS5 search failed: {e}, falling back to LIKE search")
48
+
49
+ # Fallback to LIKE search
50
+ return self.execute_fallback_search(query, namespace, category_filter, limit)
51
+
52
+ def _execute_fts5_search(
53
+ self,
54
+ query: str,
55
+ namespace: str,
56
+ category_filter: Optional[List[str]],
57
+ limit: int,
58
+ ) -> List[Dict[str, Any]]:
59
+ """Execute FTS5 search (adapted from original implementation)"""
60
+ # Build FTS query with category filter - handle empty queries
61
+ if query and query.strip():
62
+ fts_query = f'"{query.strip()}"'
63
+ else:
64
+ # Use a simple match-all query for empty searches
65
+ fts_query = "*"
66
+
67
+ category_clause = ""
68
+ params = [fts_query, namespace]
69
+
70
+ if category_filter:
71
+ category_placeholders = ",".join("?" * len(category_filter))
72
+ category_clause = (
73
+ "AND fts.category_primary IN (" + category_placeholders + ")"
74
+ )
75
+ params.extend(category_filter)
76
+
77
+ params.append(limit)
78
+
79
+ base_query = """
80
+ SELECT
81
+ fts.memory_id, fts.memory_type, fts.category_primary,
82
+ CASE
83
+ WHEN fts.memory_type = 'short_term' THEN st.processed_data
84
+ WHEN fts.memory_type = 'long_term' THEN lt.processed_data
85
+ END as processed_data,
86
+ CASE
87
+ WHEN fts.memory_type = 'short_term' THEN st.importance_score
88
+ WHEN fts.memory_type = 'long_term' THEN lt.importance_score
89
+ ELSE 0.5
90
+ END as importance_score,
91
+ CASE
92
+ WHEN fts.memory_type = 'short_term' THEN st.created_at
93
+ WHEN fts.memory_type = 'long_term' THEN lt.created_at
94
+ END as created_at,
95
+ fts.summary,
96
+ rank,
97
+ 'fts5_search' as search_strategy
98
+ FROM memory_search_fts fts
99
+ LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
100
+ LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
101
+ WHERE memory_search_fts MATCH ? AND fts.namespace = ?"""
102
+
103
+ if category_clause:
104
+ sql_query = (
105
+ base_query
106
+ + " "
107
+ + category_clause
108
+ + """
109
+ ORDER BY rank, importance_score DESC
110
+ LIMIT ?"""
111
+ )
112
+ else:
113
+ sql_query = (
114
+ base_query
115
+ + """
116
+ ORDER BY rank, importance_score DESC
117
+ LIMIT ?"""
118
+ )
119
+
120
+ return self.connector.execute_query(sql_query, params)
121
+
122
+ def create_search_indexes(self) -> List[str]:
123
+ """Create FTS5 virtual table and triggers"""
124
+ if not self.fts5_available:
125
+ logger.warning("FTS5 not available, skipping FTS index creation")
126
+ return []
127
+
128
+ commands = [
129
+ # Create FTS5 virtual table
130
+ """
131
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_search_fts USING fts5(
132
+ memory_id,
133
+ memory_type,
134
+ namespace,
135
+ searchable_content,
136
+ summary,
137
+ category_primary,
138
+ content='',
139
+ contentless_delete=1
140
+ )
141
+ """,
142
+ # Triggers to maintain FTS index
143
+ """
144
+ CREATE TRIGGER IF NOT EXISTS short_term_memory_fts_insert AFTER INSERT ON short_term_memory
145
+ BEGIN
146
+ INSERT INTO memory_search_fts(memory_id, memory_type, namespace, searchable_content, summary, category_primary)
147
+ VALUES (NEW.memory_id, 'short_term', NEW.namespace, NEW.searchable_content, NEW.summary, NEW.category_primary);
148
+ END
149
+ """,
150
+ """
151
+ CREATE TRIGGER IF NOT EXISTS long_term_memory_fts_insert AFTER INSERT ON long_term_memory
152
+ BEGIN
153
+ INSERT INTO memory_search_fts(memory_id, memory_type, namespace, searchable_content, summary, category_primary)
154
+ VALUES (NEW.memory_id, 'long_term', NEW.namespace, NEW.searchable_content, NEW.summary, NEW.category_primary);
155
+ END
156
+ """,
157
+ """
158
+ CREATE TRIGGER IF NOT EXISTS short_term_memory_fts_delete AFTER DELETE ON short_term_memory
159
+ BEGIN
160
+ DELETE FROM memory_search_fts WHERE memory_id = OLD.memory_id AND memory_type = 'short_term';
161
+ END
162
+ """,
163
+ """
164
+ CREATE TRIGGER IF NOT EXISTS long_term_memory_fts_delete AFTER DELETE ON long_term_memory
165
+ BEGIN
166
+ DELETE FROM memory_search_fts WHERE memory_id = OLD.memory_id AND memory_type = 'long_term';
167
+ END
168
+ """,
169
+ ]
170
+
171
+ return commands
172
+
173
+ def translate_search_query(self, query: str) -> str:
174
+ """Translate search query to FTS5 syntax"""
175
+ if not query or not query.strip():
176
+ return "*"
177
+
178
+ # For FTS5, wrap in quotes for exact phrase matching
179
+ # More sophisticated query translation could be added here
180
+ return f'"{query.strip()}"'