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
@@ -6,32 +6,143 @@ from typing import Any, Dict, List, Optional
6
6
 
7
7
  from loguru import logger
8
8
 
9
- from ...utils.exceptions import DatabaseError
9
+ from ...utils.exceptions import DatabaseError, ValidationError
10
+ from .base_connector import BaseDatabaseConnector, DatabaseType
10
11
 
11
12
 
12
- class PostgreSQLConnector:
13
+ class PostgreSQLConnector(BaseDatabaseConnector):
13
14
  """PostgreSQL database connector"""
14
15
 
15
- def __init__(self, connection_string: str):
16
+ def __init__(self, connection_config):
16
17
  """Initialize PostgreSQL connector"""
17
- self.connection_string = connection_string
18
+ if isinstance(connection_config, str):
19
+ self.connection_string = connection_config
20
+ else:
21
+ self.connection_string = self._build_connection_string(connection_config)
18
22
  self._psycopg2 = None
19
23
  self._setup_psycopg2()
24
+ self._ensure_database_exists()
25
+ super().__init__(connection_config)
20
26
 
21
27
  def _setup_psycopg2(self):
22
28
  """Setup psycopg2 connection"""
23
29
  try:
24
30
  import psycopg2
25
31
  import psycopg2.extras
32
+ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
26
33
 
27
34
  self._psycopg2 = psycopg2
28
35
  self._extras = psycopg2.extras
36
+ self.ISOLATION_LEVEL_AUTOCOMMIT = ISOLATION_LEVEL_AUTOCOMMIT
29
37
  except ImportError:
30
38
  raise DatabaseError(
31
39
  "psycopg2 is required for PostgreSQL support. "
32
40
  "Install it with: pip install psycopg2-binary"
33
41
  )
34
42
 
43
+ def _detect_database_type(self) -> DatabaseType:
44
+ """Detect database type from connection config"""
45
+ return DatabaseType.POSTGRESQL
46
+
47
+ def _build_connection_string(self, config: Dict[str, Any]) -> str:
48
+ """Build PostgreSQL connection string from config dict"""
49
+ try:
50
+ user = config.get("user", "postgres")
51
+ password = config.get("password", "")
52
+ host = config.get("host", "localhost")
53
+ port = config.get("port", 5432)
54
+ database = config.get("database")
55
+
56
+ if not database:
57
+ raise ValidationError("Database name is required")
58
+
59
+ if password:
60
+ return f"postgresql://{user}:{password}@{host}:{port}/{database}"
61
+ else:
62
+ return f"postgresql://{user}@{host}:{port}/{database}"
63
+ except Exception as e:
64
+ raise DatabaseError(f"Invalid PostgreSQL configuration: {e}")
65
+
66
+ def _parse_connection_string(self):
67
+ """Parse connection string to extract components"""
68
+ import re
69
+
70
+ # Parse PostgreSQL connection string
71
+ # Format: postgresql://user:password@host:port/database
72
+ # or: postgresql+psycopg2://user:password@host:port/database
73
+ pattern = r"postgresql(?:\+psycopg2)?://(?:([^:]+)(?::([^@]+))?@)?([^:/]+)(?::(\d+))?/(.+)"
74
+ match = re.match(pattern, self.connection_string)
75
+
76
+ if not match:
77
+ raise DatabaseError(
78
+ f"Invalid PostgreSQL connection string: {self.connection_string}"
79
+ )
80
+
81
+ user, password, host, port, database = match.groups()
82
+
83
+ return {
84
+ "user": user or "postgres",
85
+ "password": password or "",
86
+ "host": host or "localhost",
87
+ "port": port or "5432",
88
+ "database": database,
89
+ }
90
+
91
+ def _ensure_database_exists(self):
92
+ """Ensure the database exists, create if it doesn't"""
93
+ params = self._parse_connection_string()
94
+
95
+ try:
96
+ # Connect to default 'postgres' database to check/create target database
97
+ admin_conn_params = {
98
+ "host": params["host"],
99
+ "port": params["port"],
100
+ "user": params["user"],
101
+ "database": "postgres",
102
+ }
103
+
104
+ if params["password"]:
105
+ admin_conn_params["password"] = params["password"]
106
+
107
+ conn = self._psycopg2.connect(**admin_conn_params)
108
+ conn.set_isolation_level(self.ISOLATION_LEVEL_AUTOCOMMIT)
109
+ cursor = conn.cursor()
110
+
111
+ # Check if database exists
112
+ cursor.execute(
113
+ "SELECT 1 FROM pg_database WHERE datname = %s", (params["database"],)
114
+ )
115
+
116
+ if not cursor.fetchone():
117
+ # Database doesn't exist, create it
118
+ # Use SQL identifier for database name (can't use parameter substitution for DDL)
119
+ from psycopg2 import sql
120
+
121
+ cursor.execute(
122
+ sql.SQL("CREATE DATABASE {}").format(
123
+ sql.Identifier(params["database"])
124
+ )
125
+ )
126
+ logger.info(f"Created PostgreSQL database: {params['database']}")
127
+
128
+ # Set UTF-8 encoding
129
+ cursor.execute(
130
+ sql.SQL("ALTER DATABASE {} SET client_encoding TO 'UTF8'").format(
131
+ sql.Identifier(params["database"])
132
+ )
133
+ )
134
+
135
+ cursor.close()
136
+ conn.close()
137
+
138
+ except self._psycopg2.OperationalError as e:
139
+ # If we can't connect to postgres database, try connecting directly
140
+ # This might work if the target database already exists
141
+ logger.warning(f"Could not connect to postgres database: {e}")
142
+ logger.info("Attempting direct connection to target database...")
143
+ except Exception as e:
144
+ logger.warning(f"Could not ensure database exists: {e}")
145
+
35
146
  def get_connection(self):
36
147
  """Get PostgreSQL connection"""
37
148
  try:
@@ -156,3 +267,165 @@ class PostgreSQLConnector:
156
267
  except Exception as e:
157
268
  logger.error(f"Connection test failed: {e}")
158
269
  return False
270
+
271
+ def initialize_schema(self, schema_sql: Optional[str] = None):
272
+ """Initialize PostgreSQL database schema"""
273
+ try:
274
+ if not schema_sql:
275
+ # Use PostgreSQL-specific schema
276
+ try:
277
+ from ..schema_generators.postgresql_schema_generator import ( # type: ignore
278
+ PostgreSQLSchemaGenerator,
279
+ )
280
+
281
+ schema_generator = PostgreSQLSchemaGenerator()
282
+ schema_sql = schema_generator.generate_full_schema()
283
+ except ImportError:
284
+ # Fall back to None - let base functionality handle it
285
+ schema_sql = None
286
+
287
+ # Execute schema using transaction
288
+ with self.get_connection() as conn:
289
+ with conn.cursor() as cursor:
290
+ try:
291
+ # Split schema into individual statements
292
+ statements = self._split_postgresql_statements(schema_sql)
293
+
294
+ for statement in statements:
295
+ statement = statement.strip()
296
+ if statement and not statement.startswith("--"):
297
+ cursor.execute(statement)
298
+
299
+ conn.commit()
300
+ logger.info(
301
+ "PostgreSQL database schema initialized successfully"
302
+ )
303
+
304
+ except Exception as e:
305
+ conn.rollback()
306
+ logger.error(f"Failed to initialize PostgreSQL schema: {e}")
307
+ raise DatabaseError(f"Schema initialization failed: {e}")
308
+
309
+ except Exception as e:
310
+ logger.error(f"Failed to initialize PostgreSQL schema: {e}")
311
+ raise DatabaseError(f"Failed to initialize PostgreSQL schema: {e}")
312
+
313
+ def supports_full_text_search(self) -> bool:
314
+ """Check if PostgreSQL supports full-text search"""
315
+ try:
316
+ with self.get_connection() as conn:
317
+ with conn.cursor() as cursor:
318
+ # Test tsvector functionality
319
+ cursor.execute(
320
+ "SELECT to_tsvector('english', 'test') @@ plainto_tsquery('english', 'test')"
321
+ )
322
+ result = cursor.fetchone()
323
+ return result[0] if result else False
324
+ except Exception as e:
325
+ logger.warning(f"Could not determine PostgreSQL FTS support: {e}")
326
+ return False
327
+
328
+ def create_full_text_index(
329
+ self, table: str, columns: List[str], index_name: str
330
+ ) -> str:
331
+ """Create PostgreSQL GIN index for full-text search"""
332
+ # Validate inputs
333
+ try:
334
+ from ...utils.input_validator import InputValidator
335
+
336
+ table = InputValidator.sanitize_sql_identifier(table)
337
+ index_name = InputValidator.sanitize_sql_identifier(index_name)
338
+ for col in columns:
339
+ InputValidator.sanitize_sql_identifier(col)
340
+ except Exception as e:
341
+ raise DatabaseError(f"Invalid index parameters: {e}")
342
+
343
+ # Create tsvector expression
344
+ tsvector_expr = " || ' ' || ".join(columns)
345
+ return f"CREATE INDEX {index_name} ON {table} USING gin(to_tsvector('english', {tsvector_expr}))"
346
+
347
+ def get_database_info(self) -> Dict[str, Any]:
348
+ """Get PostgreSQL database information and capabilities"""
349
+ try:
350
+ with self.get_connection() as conn:
351
+ with conn.cursor(cursor_factory=self._extras.RealDictCursor) as cursor:
352
+ info = {}
353
+
354
+ # Basic version info
355
+ cursor.execute("SELECT version() as version")
356
+ version_result = cursor.fetchone()
357
+ info["version"] = (
358
+ version_result["version"] if version_result else "unknown"
359
+ )
360
+
361
+ # Database name
362
+ cursor.execute("SELECT current_database() as db_name")
363
+ db_result = cursor.fetchone()
364
+ info["database"] = db_result["db_name"] if db_result else "unknown"
365
+
366
+ # Extensions info
367
+ cursor.execute("SELECT extname FROM pg_extension")
368
+ extensions = cursor.fetchall()
369
+ info["extensions"] = (
370
+ [ext["extname"] for ext in extensions] if extensions else []
371
+ )
372
+
373
+ # Check for full-text search capabilities
374
+ info["database_type"] = self.database_type.value
375
+ info["fulltext_support"] = self.supports_full_text_search()
376
+
377
+ # Connection info
378
+ info["connection_string"] = (
379
+ self.connection_string.split("@")[1]
380
+ if "@" in self.connection_string
381
+ else "unknown"
382
+ )
383
+
384
+ return info
385
+
386
+ except Exception as e:
387
+ logger.warning(f"Could not get PostgreSQL database info: {e}")
388
+ return {
389
+ "database_type": self.database_type.value,
390
+ "version": "unknown",
391
+ "fulltext_support": False,
392
+ "error": str(e),
393
+ }
394
+
395
+ def _split_postgresql_statements(self, schema_sql: str) -> List[str]:
396
+ """Split SQL schema into individual statements handling PostgreSQL syntax"""
397
+ statements = []
398
+ current_statement = []
399
+ in_function = False
400
+
401
+ for line in schema_sql.split("\n"):
402
+ line = line.strip()
403
+
404
+ # Skip comments and empty lines
405
+ if not line or line.startswith("--"):
406
+ continue
407
+
408
+ # Track function/procedure boundaries
409
+ if line.upper().startswith(
410
+ ("CREATE FUNCTION", "CREATE OR REPLACE FUNCTION", "CREATE PROCEDURE")
411
+ ):
412
+ in_function = True
413
+ elif line.upper().startswith("$$") and in_function:
414
+ in_function = False
415
+
416
+ current_statement.append(line)
417
+
418
+ # PostgreSQL uses semicolon to end statements (except within functions)
419
+ if line.endswith(";") and not in_function:
420
+ statement = " ".join(current_statement)
421
+ if statement.strip():
422
+ statements.append(statement)
423
+ current_statement = []
424
+
425
+ # Add any remaining statement
426
+ if current_statement:
427
+ statement = " ".join(current_statement)
428
+ if statement.strip():
429
+ statements.append(statement)
430
+
431
+ return statements
@@ -9,15 +9,34 @@ from typing import Any, Dict, List, Optional
9
9
  from loguru import logger
10
10
 
11
11
  from ...utils.exceptions import DatabaseError
12
+ from ...utils.input_validator import InputValidator
13
+ from .base_connector import BaseDatabaseConnector, DatabaseType
12
14
 
13
15
 
14
- class SQLiteConnector:
16
+ class SQLiteConnector(BaseDatabaseConnector):
15
17
  """SQLite database connector with FTS5 support"""
16
18
 
17
- def __init__(self, db_path: str):
19
+ def __init__(self, connection_config):
18
20
  """Initialize SQLite connector"""
19
- self.db_path = db_path
21
+ if isinstance(connection_config, str):
22
+ self.db_path = self._parse_db_path(connection_config)
23
+ else:
24
+ self.db_path = connection_config.get("database", ":memory:")
20
25
  self._ensure_directory_exists()
26
+ super().__init__(connection_config)
27
+
28
+ def _detect_database_type(self) -> DatabaseType:
29
+ """Detect database type from connection config"""
30
+ return DatabaseType.SQLITE
31
+
32
+ def _parse_db_path(self, connection_string: str) -> str:
33
+ """Parse SQLite connection string to get database path"""
34
+ if connection_string.startswith("sqlite:///"):
35
+ return connection_string.replace("sqlite:///", "")
36
+ elif connection_string.startswith("sqlite://"):
37
+ return connection_string.replace("sqlite://", "")
38
+ else:
39
+ return connection_string
21
40
 
22
41
  def _ensure_directory_exists(self):
23
42
  """Ensure the database directory exists"""
@@ -146,3 +165,159 @@ class SQLiteConnector:
146
165
  except Exception as e:
147
166
  logger.error(f"Connection test failed: {e}")
148
167
  return False
168
+
169
+ def initialize_schema(self, schema_sql: Optional[str] = None):
170
+ """Initialize SQLite database schema"""
171
+ try:
172
+ if not schema_sql:
173
+ # Use SQLite-specific schema
174
+ try:
175
+ from ..schema_generators.sqlite_schema_generator import ( # type: ignore
176
+ SQLiteSchemaGenerator,
177
+ )
178
+
179
+ schema_generator = SQLiteSchemaGenerator()
180
+ schema_sql = schema_generator.generate_full_schema()
181
+ except ImportError:
182
+ # Fall back to None - let base functionality handle it
183
+ schema_sql = None
184
+
185
+ # Execute schema using transaction
186
+ with self.get_connection() as conn:
187
+ cursor = conn.cursor()
188
+
189
+ try:
190
+ # Split schema into individual statements
191
+ statements = self._split_sqlite_statements(schema_sql)
192
+
193
+ for statement in statements:
194
+ statement = statement.strip()
195
+ if statement and not statement.startswith("--"):
196
+ cursor.execute(statement)
197
+
198
+ conn.commit()
199
+ logger.info("SQLite database schema initialized successfully")
200
+
201
+ except Exception as e:
202
+ conn.rollback()
203
+ logger.error(f"Failed to initialize SQLite schema: {e}")
204
+ raise DatabaseError(f"Schema initialization failed: {e}")
205
+
206
+ except Exception as e:
207
+ logger.error(f"Failed to initialize SQLite schema: {e}")
208
+ raise DatabaseError(f"Failed to initialize SQLite schema: {e}")
209
+
210
+ def supports_full_text_search(self) -> bool:
211
+ """Check if SQLite supports FTS5"""
212
+ try:
213
+ with self.get_connection() as conn:
214
+ cursor = conn.cursor()
215
+ cursor.execute("CREATE VIRTUAL TABLE fts_test USING fts5(content)")
216
+ cursor.execute("DROP TABLE fts_test")
217
+ return True
218
+ except sqlite3.OperationalError:
219
+ return False
220
+ except Exception:
221
+ return False
222
+
223
+ def create_full_text_index(
224
+ self, table: str, columns: List[str], index_name: str
225
+ ) -> str:
226
+ """Create SQLite FTS5 virtual table"""
227
+ # Validate inputs
228
+ try:
229
+ table = InputValidator.sanitize_sql_identifier(table)
230
+ index_name = InputValidator.sanitize_sql_identifier(index_name)
231
+ for col in columns:
232
+ InputValidator.sanitize_sql_identifier(col)
233
+ except Exception as e:
234
+ raise DatabaseError(f"Invalid index parameters: {e}")
235
+
236
+ columns_str = ", ".join(columns)
237
+ return f"CREATE VIRTUAL TABLE {index_name} USING fts5({columns_str})"
238
+
239
+ def get_database_info(self) -> Dict[str, Any]:
240
+ """Get SQLite database information and capabilities"""
241
+ try:
242
+ with self.get_connection() as conn:
243
+ cursor = conn.cursor()
244
+
245
+ info = {}
246
+
247
+ # SQLite version
248
+ cursor.execute("SELECT sqlite_version() as version")
249
+ version_result = cursor.fetchone()
250
+ info["version"] = version_result[0] if version_result else "unknown"
251
+
252
+ # Database file info
253
+ info["database_file"] = self.db_path
254
+ info["database_type"] = self.database_type.value
255
+
256
+ # Check capabilities
257
+ info["fts5_support"] = self.supports_full_text_search()
258
+
259
+ # Pragma settings
260
+ cursor.execute("PRAGMA journal_mode")
261
+ journal_mode = cursor.fetchone()
262
+ info["journal_mode"] = journal_mode[0] if journal_mode else "unknown"
263
+
264
+ cursor.execute("PRAGMA synchronous")
265
+ synchronous = cursor.fetchone()
266
+ info["synchronous"] = synchronous[0] if synchronous else "unknown"
267
+
268
+ cursor.execute("PRAGMA cache_size")
269
+ cache_size = cursor.fetchone()
270
+ info["cache_size"] = cache_size[0] if cache_size else "unknown"
271
+
272
+ return info
273
+
274
+ except Exception as e:
275
+ logger.warning(f"Could not get SQLite database info: {e}")
276
+ return {
277
+ "database_type": self.database_type.value,
278
+ "version": "unknown",
279
+ "fts5_support": False,
280
+ "error": str(e),
281
+ }
282
+
283
+ def _split_sqlite_statements(self, schema_sql: str) -> List[str]:
284
+ """Split SQL schema into individual statements handling SQLite syntax"""
285
+ statements = []
286
+ current_statement = []
287
+ in_trigger = False
288
+
289
+ for line in schema_sql.split("\n"):
290
+ line = line.strip()
291
+
292
+ # Skip comments and empty lines
293
+ if not line or line.startswith("--"):
294
+ continue
295
+
296
+ # Track trigger boundaries
297
+ if line.upper().startswith("CREATE TRIGGER"):
298
+ in_trigger = True
299
+ elif line.upper() == "END;" and in_trigger:
300
+ current_statement.append(line)
301
+ statement = " ".join(current_statement)
302
+ if statement.strip():
303
+ statements.append(statement)
304
+ current_statement = []
305
+ in_trigger = False
306
+ continue
307
+
308
+ current_statement.append(line)
309
+
310
+ # SQLite uses semicolon to end statements (except within triggers)
311
+ if line.endswith(";") and not in_trigger:
312
+ statement = " ".join(current_statement)
313
+ if statement.strip():
314
+ statements.append(statement)
315
+ current_statement = []
316
+
317
+ # Add any remaining statement
318
+ if current_statement:
319
+ statement = " ".join(current_statement)
320
+ if statement.strip():
321
+ statements.append(statement)
322
+
323
+ return statements