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,320 @@
1
+ """
2
+ Database Auto-Creation System
3
+
4
+ This module automatically creates databases if they don't exist, supporting
5
+ PostgreSQL and MySQL with proper error handling and security validation.
6
+ """
7
+
8
+ import ssl
9
+ from typing import Dict
10
+ from urllib.parse import parse_qs, urlparse
11
+
12
+ from loguru import logger
13
+ from sqlalchemy import create_engine, text
14
+ from sqlalchemy.exc import OperationalError, ProgrammingError
15
+
16
+ from .connection_utils import DatabaseConnectionUtils
17
+
18
+
19
+ class DatabaseAutoCreator:
20
+ """Handles automatic database creation for PostgreSQL and MySQL"""
21
+
22
+ def __init__(self, schema_init: bool = True):
23
+ """
24
+ Initialize database auto-creator.
25
+
26
+ Args:
27
+ schema_init: Whether to enable automatic database creation
28
+ """
29
+ self.schema_init = schema_init
30
+ self.utils = DatabaseConnectionUtils()
31
+
32
+ def ensure_database_exists(self, connection_string: str) -> str:
33
+ """
34
+ Ensure target database exists, creating it if necessary.
35
+
36
+ Args:
37
+ connection_string: Original database connection string
38
+
39
+ Returns:
40
+ Connection string to use (may be unchanged if creation not needed)
41
+
42
+ Raises:
43
+ DatabaseCreationError: If database creation fails
44
+ """
45
+ if not self.schema_init:
46
+ logger.debug("Auto-creation disabled, using original connection string")
47
+ return connection_string
48
+
49
+ try:
50
+ # Parse connection string
51
+ components = self.utils.parse_connection_string(connection_string)
52
+
53
+ # SQLite doesn't need database creation
54
+ if not components["needs_creation"]:
55
+ logger.debug(
56
+ f"Database engine {components['engine']} auto-creates, no action needed"
57
+ )
58
+ return connection_string
59
+
60
+ # Validate database name
61
+ if not self.utils.validate_database_name(components["database"]):
62
+ raise ValueError(f"Invalid database name: {components['database']}")
63
+
64
+ # Check if database exists
65
+ if self._database_exists(components):
66
+ logger.debug(f"Database '{components['database']}' already exists")
67
+ return connection_string
68
+
69
+ # Create database
70
+ self._create_database(components)
71
+ logger.info(f"Successfully created database '{components['database']}'")
72
+ return connection_string
73
+
74
+ except Exception as e:
75
+ logger.error(f"Database auto-creation failed: {e}")
76
+ # Don't raise exception - let the original connection attempt proceed
77
+ # This allows graceful degradation if user has manual setup
78
+ return connection_string
79
+
80
+ def _database_exists(self, components: Dict[str, str]) -> bool:
81
+ """Check if target database exists."""
82
+ try:
83
+ engine = components["engine"]
84
+
85
+ if engine == "postgresql":
86
+ return self._postgresql_database_exists(components)
87
+ elif engine == "mysql":
88
+ return self._mysql_database_exists(components)
89
+ else:
90
+ logger.warning(f"Database existence check not supported for {engine}")
91
+ return False
92
+
93
+ except Exception as e:
94
+ logger.error(f"Failed to check database existence: {e}")
95
+ return False
96
+
97
+ def _postgresql_database_exists(self, components: Dict[str, str]) -> bool:
98
+ """Check if PostgreSQL database exists."""
99
+ try:
100
+ # Connect to postgres system database
101
+ engine = create_engine(components["default_url"])
102
+
103
+ with engine.connect() as conn:
104
+ result = conn.execute(
105
+ text("SELECT 1 FROM pg_database WHERE datname = :dbname"),
106
+ {"dbname": components["database"]},
107
+ )
108
+ exists = result.fetchone() is not None
109
+
110
+ engine.dispose()
111
+ return exists
112
+
113
+ except Exception as e:
114
+ logger.error(f"PostgreSQL database existence check failed: {e}")
115
+ return False
116
+
117
+ def _get_mysql_connect_args(self, original_url: str) -> Dict:
118
+ """Get MySQL connection arguments with SSL support for system database connections."""
119
+ connect_args = {"charset": "utf8mb4"}
120
+
121
+ # Parse original URL for SSL parameters
122
+ parsed = urlparse(original_url)
123
+ if parsed.query:
124
+ query_params = parse_qs(parsed.query)
125
+
126
+ # Handle SSL parameters for PyMySQL - same logic as sqlalchemy_manager
127
+ if any(key in query_params for key in ["ssl", "ssl_disabled"]):
128
+ if query_params.get("ssl", ["false"])[0].lower() == "true":
129
+ # Enable SSL with secure configuration for required secure transport
130
+ connect_args["ssl"] = {
131
+ "ssl_disabled": False,
132
+ "check_hostname": False,
133
+ "verify_mode": ssl.CERT_NONE,
134
+ }
135
+ # Also add ssl_disabled=False for PyMySQL
136
+ connect_args["ssl_disabled"] = False
137
+ elif query_params.get("ssl_disabled", ["true"])[0].lower() == "false":
138
+ # Enable SSL with secure configuration for required secure transport
139
+ connect_args["ssl"] = {
140
+ "ssl_disabled": False,
141
+ "check_hostname": False,
142
+ "verify_mode": ssl.CERT_NONE,
143
+ }
144
+ # Also add ssl_disabled=False for PyMySQL
145
+ connect_args["ssl_disabled"] = False
146
+
147
+ return connect_args
148
+
149
+ def _mysql_database_exists(self, components: Dict[str, str]) -> bool:
150
+ """Check if MySQL database exists."""
151
+ try:
152
+ # Connect to mysql system database with SSL support
153
+ connect_args = self._get_mysql_connect_args(components["original_url"])
154
+ engine = create_engine(components["default_url"], connect_args=connect_args)
155
+
156
+ with engine.connect() as conn:
157
+ result = conn.execute(
158
+ text(
159
+ "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :dbname"
160
+ ),
161
+ {"dbname": components["database"]},
162
+ )
163
+ exists = result.fetchone() is not None
164
+
165
+ engine.dispose()
166
+ return exists
167
+
168
+ except ModuleNotFoundError as e:
169
+ if "mysql" in str(e).lower():
170
+ logger.error(f"MySQL database existence check failed: {e}")
171
+ error_msg = (
172
+ "❌ MySQL driver not found for database existence check. Install one of:\n"
173
+ "- pip install mysql-connector-python\n"
174
+ "- pip install PyMySQL\n"
175
+ "- pip install memorisdk[mysql]"
176
+ )
177
+ logger.error(error_msg)
178
+ return False
179
+ except Exception as e:
180
+ logger.error(f"MySQL database existence check failed: {e}")
181
+ return False
182
+
183
+ def _create_database(self, components: Dict[str, str]) -> None:
184
+ """Create the target database."""
185
+ engine = components["engine"]
186
+
187
+ if engine == "postgresql":
188
+ self._create_postgresql_database(components)
189
+ elif engine == "mysql":
190
+ self._create_mysql_database(components)
191
+ else:
192
+ raise ValueError(f"Database creation not supported for {engine}")
193
+
194
+ def _create_postgresql_database(self, components: Dict[str, str]) -> None:
195
+ """Create PostgreSQL database."""
196
+ try:
197
+ logger.info(f"Creating PostgreSQL database '{components['database']}'...")
198
+
199
+ # Connect to postgres system database
200
+ engine = create_engine(components["default_url"])
201
+
202
+ # PostgreSQL requires autocommit for CREATE DATABASE
203
+ with engine.connect() as conn:
204
+ # Set autocommit mode
205
+ conn = conn.execution_options(isolation_level="AUTOCOMMIT")
206
+
207
+ # Create database (can't use parameters for database name)
208
+ # Database name is already validated, so this is safe
209
+ conn.execute(text(f'CREATE DATABASE "{components["database"]}"'))
210
+
211
+ engine.dispose()
212
+ logger.info(
213
+ f"PostgreSQL database '{components['database']}' created successfully"
214
+ )
215
+
216
+ except (OperationalError, ProgrammingError) as e:
217
+ error_msg = str(e)
218
+ if "already exists" in error_msg.lower():
219
+ logger.info(
220
+ f"PostgreSQL database '{components['database']}' already exists"
221
+ )
222
+ return
223
+ elif "permission denied" in error_msg.lower():
224
+ raise PermissionError(
225
+ f"Insufficient permissions to create database '{components['database']}'"
226
+ )
227
+ else:
228
+ raise RuntimeError(f"Failed to create PostgreSQL database: {e}")
229
+
230
+ except Exception as e:
231
+ raise RuntimeError(f"Unexpected error creating PostgreSQL database: {e}")
232
+
233
+ def _create_mysql_database(self, components: Dict[str, str]) -> None:
234
+ """Create MySQL database."""
235
+ try:
236
+ logger.info(f"Creating MySQL database '{components['database']}'...")
237
+
238
+ # Connect to mysql system database with SSL support
239
+ connect_args = self._get_mysql_connect_args(components["original_url"])
240
+ engine = create_engine(components["default_url"], connect_args=connect_args)
241
+
242
+ with engine.connect() as conn:
243
+ # Create database (can't use parameters for database name)
244
+ # Database name is already validated, so this is safe
245
+ conn.execute(
246
+ text(
247
+ f'CREATE DATABASE `{components["database"]}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'
248
+ )
249
+ )
250
+ conn.commit()
251
+
252
+ engine.dispose()
253
+ logger.info(
254
+ f"MySQL database '{components['database']}' created successfully"
255
+ )
256
+
257
+ except ModuleNotFoundError as e:
258
+ if "mysql" in str(e).lower():
259
+ error_msg = (
260
+ "❌ MySQL driver not found for database creation. Install one of:\n"
261
+ "- pip install mysql-connector-python\n"
262
+ "- pip install PyMySQL\n"
263
+ "- pip install memorisdk[mysql]"
264
+ )
265
+ logger.error(error_msg)
266
+ raise RuntimeError(error_msg)
267
+ else:
268
+ raise RuntimeError(f"Missing dependency for database creation: {e}")
269
+ except (OperationalError, ProgrammingError) as e:
270
+ error_msg = str(e)
271
+ if "database exists" in error_msg.lower():
272
+ logger.info(f"MySQL database '{components['database']}' already exists")
273
+ return
274
+ elif "access denied" in error_msg.lower():
275
+ raise PermissionError(
276
+ f"Insufficient permissions to create database '{components['database']}'"
277
+ )
278
+ else:
279
+ raise RuntimeError(f"Failed to create MySQL database: {e}")
280
+
281
+ except Exception as e:
282
+ raise RuntimeError(f"Unexpected error creating MySQL database: {e}")
283
+
284
+ def get_database_info(self, connection_string: str) -> Dict[str, str]:
285
+ """
286
+ Get detailed information about database from connection string.
287
+
288
+ Args:
289
+ connection_string: Database connection URL
290
+
291
+ Returns:
292
+ Dictionary with database information
293
+ """
294
+ try:
295
+ components = self.utils.parse_connection_string(connection_string)
296
+
297
+ info = {
298
+ "engine": components["engine"],
299
+ "database": components["database"],
300
+ "host": components["host"],
301
+ "port": components["port"],
302
+ "needs_creation": components["needs_creation"],
303
+ "auto_creation_enabled": self.schema_init,
304
+ }
305
+
306
+ # Add existence check if auto-creation is enabled
307
+ if self.schema_init and components["needs_creation"]:
308
+ info["exists"] = self._database_exists(components)
309
+
310
+ return info
311
+
312
+ except Exception as e:
313
+ logger.error(f"Failed to get database info: {e}")
314
+ return {"error": str(e)}
315
+
316
+
317
+ class DatabaseCreationError(Exception):
318
+ """Raised when database creation fails"""
319
+
320
+ pass
@@ -0,0 +1,207 @@
1
+ """
2
+ Database Connection Utilities for Auto-Creation System
3
+
4
+ This module handles parsing connection strings, creating databases automatically,
5
+ and managing multi-database scenarios for memori instances.
6
+ """
7
+
8
+ import re
9
+ from typing import Dict, Tuple
10
+ from urllib.parse import urlparse
11
+
12
+ from loguru import logger
13
+
14
+
15
+ class DatabaseConnectionUtils:
16
+ """Utilities for parsing and managing database connections"""
17
+
18
+ # Default system databases for each engine
19
+ DEFAULT_DATABASES = {
20
+ "postgresql": "postgres",
21
+ "mysql": "mysql",
22
+ "sqlite": None, # SQLite doesn't need default DB
23
+ }
24
+
25
+ @classmethod
26
+ def parse_connection_string(cls, connection_string: str) -> Dict[str, str]:
27
+ """
28
+ Parse database connection string and extract components.
29
+
30
+ Args:
31
+ connection_string: Database connection URL
32
+
33
+ Returns:
34
+ Dictionary with parsed components
35
+
36
+ Example:
37
+ mysql://root:pass@localhost:3306/memori_dev
38
+ -> {
39
+ 'engine': 'mysql',
40
+ 'user': 'root',
41
+ 'password': 'pass',
42
+ 'host': 'localhost',
43
+ 'port': 3306,
44
+ 'database': 'memori_dev',
45
+ 'base_url': 'mysql://root:pass@localhost:3306',
46
+ 'default_url': 'mysql://root:pass@localhost:3306/mysql'
47
+ }
48
+ """
49
+ try:
50
+ parsed = urlparse(connection_string)
51
+
52
+ # Extract components
53
+ engine = parsed.scheme.split("+")[
54
+ 0
55
+ ].lower() # Handle postgresql+psycopg2, mysql+pymysql
56
+ driver = parsed.scheme.split("+")[1] if "+" in parsed.scheme else None
57
+ user = parsed.username or ""
58
+ password = parsed.password or ""
59
+ host = parsed.hostname or "localhost"
60
+ port = parsed.port
61
+ database = parsed.path.lstrip("/") if parsed.path else ""
62
+
63
+ # Build base URL without database, preserving the driver
64
+ auth = f"{user}:{password}@" if user or password else ""
65
+ if user and not password:
66
+ auth = f"{user}@"
67
+
68
+ port_str = f":{port}" if port else ""
69
+ scheme = f"{engine}+{driver}" if driver else engine
70
+ base_url = f"{scheme}://{auth}{host}{port_str}"
71
+
72
+ # Create default database URL for system operations
73
+ default_db = cls.DEFAULT_DATABASES.get(engine)
74
+
75
+ # Preserve query parameters (especially SSL settings) for system database connections
76
+ query_string = f"?{parsed.query}" if parsed.query else ""
77
+ default_url = (
78
+ f"{base_url}/{default_db}{query_string}"
79
+ if default_db
80
+ else f"{base_url}{query_string}"
81
+ )
82
+
83
+ return {
84
+ "engine": engine,
85
+ "user": user,
86
+ "password": password,
87
+ "host": host,
88
+ "port": port,
89
+ "database": database,
90
+ "base_url": base_url,
91
+ "default_url": default_url,
92
+ "original_url": connection_string,
93
+ "needs_creation": engine
94
+ in ["postgresql", "mysql"], # SQLite auto-creates
95
+ }
96
+
97
+ except Exception as e:
98
+ logger.error(f"Failed to parse connection string: {e}")
99
+ raise ValueError(f"Invalid connection string format: {connection_string}")
100
+
101
+ @classmethod
102
+ def build_connection_string(
103
+ cls, components: Dict[str, str], target_database: str
104
+ ) -> str:
105
+ """
106
+ Build connection string with specific database name.
107
+
108
+ Args:
109
+ components: Parsed connection components
110
+ target_database: Database name to connect to
111
+
112
+ Returns:
113
+ Complete connection string
114
+ """
115
+ return f"{components['base_url']}/{target_database}"
116
+
117
+ @classmethod
118
+ def validate_database_name(cls, database_name: str) -> bool:
119
+ """
120
+ Validate database name for security and compatibility.
121
+
122
+ Args:
123
+ database_name: Name to validate
124
+
125
+ Returns:
126
+ True if valid, False otherwise
127
+ """
128
+ if not database_name:
129
+ return False
130
+
131
+ # Basic SQL injection prevention
132
+ if any(
133
+ char in database_name.lower()
134
+ for char in [";", "'", '"', "\\", "/", "*", "?", "<", ">", "|"]
135
+ ):
136
+ return False
137
+
138
+ # Check length (most databases have limits)
139
+ if len(database_name) > 64: # MySQL limit
140
+ return False
141
+
142
+ # Must start with letter or underscore, contain only alphanumeric, underscore, hyphen
143
+ if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_-]*$", database_name):
144
+ return False
145
+
146
+ # Reserved words check (only for database creation, not connection)
147
+ # Note: 'postgres' is a valid system database to connect to in PostgreSQL
148
+ reserved_words = ["mysql", "information_schema", "performance_schema", "sys"]
149
+ if database_name.lower() in reserved_words:
150
+ return False
151
+
152
+ return True
153
+
154
+ @classmethod
155
+ def generate_database_name(
156
+ cls, base_name: str = "memori", suffix: str = None, prefix: str = None
157
+ ) -> str:
158
+ """
159
+ Generate a valid database name with optional prefix/suffix.
160
+
161
+ Args:
162
+ base_name: Base database name
163
+ suffix: Optional suffix (e.g., "dev", "prod", "test")
164
+ prefix: Optional prefix (e.g., "company", "project")
165
+
166
+ Returns:
167
+ Generated database name
168
+
169
+ Examples:
170
+ generate_database_name() -> "memori"
171
+ generate_database_name(suffix="dev") -> "memori_dev"
172
+ generate_database_name(prefix="acme", suffix="prod") -> "acme_memori_prod"
173
+ """
174
+ parts = []
175
+
176
+ if prefix:
177
+ parts.append(prefix)
178
+
179
+ parts.append(base_name)
180
+
181
+ if suffix:
182
+ parts.append(suffix)
183
+
184
+ database_name = "_".join(parts)
185
+
186
+ if not cls.validate_database_name(database_name):
187
+ raise ValueError(f"Generated database name is invalid: {database_name}")
188
+
189
+ return database_name
190
+
191
+ @classmethod
192
+ def extract_database_info(cls, connection_string: str) -> Tuple[str, str, bool]:
193
+ """
194
+ Extract database engine, name, and creation requirement.
195
+
196
+ Args:
197
+ connection_string: Database connection URL
198
+
199
+ Returns:
200
+ Tuple of (engine, database_name, needs_creation)
201
+ """
202
+ components = cls.parse_connection_string(connection_string)
203
+ return (
204
+ components["engine"],
205
+ components["database"],
206
+ components["needs_creation"],
207
+ )