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.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +416 -60
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1638 -531
- memori/core/providers.py +217 -0
- memori/database/adapters/__init__.py +10 -0
- memori/database/adapters/mysql_adapter.py +331 -0
- memori/database/adapters/postgresql_adapter.py +291 -0
- memori/database/adapters/sqlite_adapter.py +229 -0
- memori/database/auto_creator.py +320 -0
- memori/database/connection_utils.py +207 -0
- memori/database/connectors/base_connector.py +283 -0
- memori/database/connectors/mysql_connector.py +240 -18
- memori/database/connectors/postgres_connector.py +277 -4
- memori/database/connectors/sqlite_connector.py +178 -3
- memori/database/models.py +400 -0
- memori/database/queries/base_queries.py +1 -1
- memori/database/queries/memory_queries.py +91 -2
- memori/database/query_translator.py +222 -0
- memori/database/schema_generators/__init__.py +7 -0
- memori/database/schema_generators/mysql_schema_generator.py +215 -0
- memori/database/search/__init__.py +8 -0
- memori/database/search/mysql_search_adapter.py +255 -0
- memori/database/search/sqlite_search_adapter.py +180 -0
- memori/database/search_service.py +548 -0
- memori/database/sqlalchemy_manager.py +839 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- memori/utils/input_validator.py +395 -0
- memori/utils/pydantic_models.py +138 -36
- memori/utils/query_builder.py +530 -0
- memori/utils/security_audit.py +594 -0
- memori/utils/security_integration.py +339 -0
- memori/utils/transaction_manager.py +547 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/METADATA +44 -17
- memorisdk-2.0.0.dist-info/RECORD +67 -0
- memorisdk-1.0.2.dist-info/RECORD +0 -44
- memorisdk-1.0.2.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
memori/core/providers.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider configuration for different LLM providers (OpenAI, Azure, custom)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProviderType(Enum):
|
|
13
|
+
"""Supported LLM provider types"""
|
|
14
|
+
|
|
15
|
+
OPENAI = "openai"
|
|
16
|
+
AZURE = "azure"
|
|
17
|
+
CUSTOM = "custom"
|
|
18
|
+
OPENAI_COMPATIBLE = "openai_compatible" # For OpenAI-compatible APIs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ProviderConfig:
|
|
23
|
+
"""
|
|
24
|
+
Configuration for LLM providers with support for OpenAI, Azure, and custom endpoints.
|
|
25
|
+
|
|
26
|
+
This class provides a unified interface for configuring different LLM providers
|
|
27
|
+
while maintaining backward compatibility with existing OpenAI-only configuration.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Common parameters
|
|
31
|
+
api_key: Optional[str] = None
|
|
32
|
+
api_type: Optional[str] = None # "openai", "azure", or custom
|
|
33
|
+
base_url: Optional[str] = None # Custom endpoint URL
|
|
34
|
+
timeout: Optional[float] = None
|
|
35
|
+
max_retries: Optional[int] = None
|
|
36
|
+
|
|
37
|
+
# Azure-specific parameters
|
|
38
|
+
azure_endpoint: Optional[str] = None
|
|
39
|
+
azure_deployment: Optional[str] = None
|
|
40
|
+
api_version: Optional[str] = None
|
|
41
|
+
azure_ad_token: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
# OpenAI-specific parameters
|
|
44
|
+
organization: Optional[str] = None
|
|
45
|
+
project: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
# Model configuration
|
|
48
|
+
model: Optional[str] = None # User can specify model, defaults to gpt-4o if not set
|
|
49
|
+
|
|
50
|
+
# Additional headers for custom providers
|
|
51
|
+
default_headers: Optional[Dict[str, str]] = None
|
|
52
|
+
default_query: Optional[Dict[str, Any]] = None
|
|
53
|
+
|
|
54
|
+
# HTTP client configuration
|
|
55
|
+
http_client: Optional[Any] = None
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_openai(
|
|
59
|
+
cls, api_key: Optional[str] = None, model: Optional[str] = None, **kwargs
|
|
60
|
+
):
|
|
61
|
+
"""Create configuration for standard OpenAI"""
|
|
62
|
+
return cls(api_key=api_key, api_type="openai", model=model, **kwargs)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_azure(
|
|
66
|
+
cls,
|
|
67
|
+
api_key: Optional[str] = None,
|
|
68
|
+
azure_endpoint: Optional[str] = None,
|
|
69
|
+
azure_deployment: Optional[str] = None,
|
|
70
|
+
api_version: Optional[str] = None,
|
|
71
|
+
model: Optional[str] = None,
|
|
72
|
+
**kwargs,
|
|
73
|
+
):
|
|
74
|
+
"""Create configuration for Azure OpenAI"""
|
|
75
|
+
return cls(
|
|
76
|
+
api_key=api_key,
|
|
77
|
+
api_type="azure",
|
|
78
|
+
azure_endpoint=azure_endpoint,
|
|
79
|
+
azure_deployment=azure_deployment,
|
|
80
|
+
api_version=api_version,
|
|
81
|
+
model=model,
|
|
82
|
+
**kwargs,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_custom(
|
|
87
|
+
cls,
|
|
88
|
+
base_url: str,
|
|
89
|
+
api_key: Optional[str] = None,
|
|
90
|
+
model: Optional[str] = None,
|
|
91
|
+
**kwargs,
|
|
92
|
+
):
|
|
93
|
+
"""Create configuration for custom OpenAI-compatible endpoints"""
|
|
94
|
+
return cls(
|
|
95
|
+
api_key=api_key, api_type="custom", base_url=base_url, model=model, **kwargs
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def get_openai_client_kwargs(self) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Get kwargs for OpenAI client initialization based on provider type.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dictionary of parameters to pass to OpenAI client constructor
|
|
104
|
+
"""
|
|
105
|
+
kwargs = {}
|
|
106
|
+
|
|
107
|
+
# Always include API key if provided
|
|
108
|
+
if self.api_key:
|
|
109
|
+
kwargs["api_key"] = self.api_key
|
|
110
|
+
|
|
111
|
+
if self.api_type == "azure":
|
|
112
|
+
# Azure OpenAI configuration
|
|
113
|
+
if self.azure_endpoint:
|
|
114
|
+
kwargs["azure_endpoint"] = self.azure_endpoint
|
|
115
|
+
if self.azure_deployment:
|
|
116
|
+
kwargs["azure_deployment"] = self.azure_deployment
|
|
117
|
+
if self.api_version:
|
|
118
|
+
kwargs["api_version"] = self.api_version
|
|
119
|
+
if self.azure_ad_token:
|
|
120
|
+
kwargs["azure_ad_token"] = self.azure_ad_token
|
|
121
|
+
# For Azure, we need to use AzureOpenAI client
|
|
122
|
+
kwargs["_use_azure_client"] = True
|
|
123
|
+
|
|
124
|
+
elif self.api_type == "custom" or self.api_type == "openai_compatible":
|
|
125
|
+
# Custom endpoint configuration
|
|
126
|
+
if self.base_url:
|
|
127
|
+
kwargs["base_url"] = self.base_url
|
|
128
|
+
|
|
129
|
+
elif self.api_type == "openai":
|
|
130
|
+
# Standard OpenAI configuration
|
|
131
|
+
if self.organization:
|
|
132
|
+
kwargs["organization"] = self.organization
|
|
133
|
+
if self.project:
|
|
134
|
+
kwargs["project"] = self.project
|
|
135
|
+
|
|
136
|
+
# Common parameters
|
|
137
|
+
if self.timeout:
|
|
138
|
+
kwargs["timeout"] = self.timeout
|
|
139
|
+
if self.max_retries:
|
|
140
|
+
kwargs["max_retries"] = self.max_retries
|
|
141
|
+
if self.default_headers:
|
|
142
|
+
kwargs["default_headers"] = self.default_headers
|
|
143
|
+
if self.default_query:
|
|
144
|
+
kwargs["default_query"] = self.default_query
|
|
145
|
+
if self.http_client:
|
|
146
|
+
kwargs["http_client"] = self.http_client
|
|
147
|
+
|
|
148
|
+
return kwargs
|
|
149
|
+
|
|
150
|
+
def create_client(self):
|
|
151
|
+
"""
|
|
152
|
+
Create the appropriate OpenAI client based on configuration.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
OpenAI or AzureOpenAI client instance
|
|
156
|
+
"""
|
|
157
|
+
import openai
|
|
158
|
+
|
|
159
|
+
kwargs = self.get_openai_client_kwargs()
|
|
160
|
+
|
|
161
|
+
# Check if we should use Azure client
|
|
162
|
+
if kwargs.pop("_use_azure_client", False):
|
|
163
|
+
# Use Azure OpenAI client
|
|
164
|
+
from openai import AzureOpenAI
|
|
165
|
+
|
|
166
|
+
return AzureOpenAI(**kwargs)
|
|
167
|
+
else:
|
|
168
|
+
# Use standard OpenAI client (works for OpenAI and custom endpoints)
|
|
169
|
+
return openai.OpenAI(**kwargs)
|
|
170
|
+
|
|
171
|
+
def create_async_client(self):
|
|
172
|
+
"""
|
|
173
|
+
Create the appropriate async OpenAI client based on configuration.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
AsyncOpenAI or AsyncAzureOpenAI client instance
|
|
177
|
+
"""
|
|
178
|
+
import openai
|
|
179
|
+
|
|
180
|
+
kwargs = self.get_openai_client_kwargs()
|
|
181
|
+
|
|
182
|
+
# Check if we should use Azure client
|
|
183
|
+
if kwargs.pop("_use_azure_client", False):
|
|
184
|
+
# Use Azure OpenAI async client
|
|
185
|
+
from openai import AsyncAzureOpenAI
|
|
186
|
+
|
|
187
|
+
return AsyncAzureOpenAI(**kwargs)
|
|
188
|
+
else:
|
|
189
|
+
# Use standard async OpenAI client
|
|
190
|
+
return openai.AsyncOpenAI(**kwargs)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def detect_provider_from_env() -> ProviderConfig:
|
|
194
|
+
"""
|
|
195
|
+
Create provider configuration from environment variables WITHOUT automatic detection.
|
|
196
|
+
|
|
197
|
+
This function ONLY uses standard OpenAI configuration by default.
|
|
198
|
+
It does NOT automatically detect or prioritize Azure or custom providers.
|
|
199
|
+
|
|
200
|
+
Only use specific providers if explicitly configured via Memori constructor parameters.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Standard OpenAI ProviderConfig instance (never auto-detects other providers)
|
|
204
|
+
"""
|
|
205
|
+
import os
|
|
206
|
+
|
|
207
|
+
# Get model from environment (optional, defaults to gpt-4o if not set)
|
|
208
|
+
model = os.getenv("OPENAI_MODEL") or os.getenv("LLM_MODEL") or "gpt-4o"
|
|
209
|
+
|
|
210
|
+
# ALWAYS default to standard OpenAI - no automatic detection
|
|
211
|
+
logger.info("Provider configuration: Using standard OpenAI (no auto-detection)")
|
|
212
|
+
return ProviderConfig.from_openai(
|
|
213
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
214
|
+
organization=os.getenv("OPENAI_ORGANIZATION"),
|
|
215
|
+
project=os.getenv("OPENAI_PROJECT"),
|
|
216
|
+
model=model,
|
|
217
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database adapters for different database backends
|
|
3
|
+
Provides database-specific implementations with proper security measures
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .mysql_adapter import MySQLSearchAdapter
|
|
7
|
+
from .postgresql_adapter import PostgreSQLSearchAdapter
|
|
8
|
+
from .sqlite_adapter import SQLiteSearchAdapter
|
|
9
|
+
|
|
10
|
+
__all__ = ["SQLiteSearchAdapter", "PostgreSQLSearchAdapter", "MySQLSearchAdapter"]
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MySQL-specific search adapter with FULLTEXT support and proper security
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from ...utils.exceptions import ValidationError
|
|
10
|
+
from ...utils.input_validator import DatabaseInputValidator
|
|
11
|
+
from ...utils.query_builder import DatabaseDialect, DatabaseQueryExecutor
|
|
12
|
+
from ..connectors.base_connector import BaseDatabaseConnector, BaseSearchAdapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MySQLSearchAdapter(BaseSearchAdapter):
|
|
16
|
+
"""MySQL-specific search implementation with FULLTEXT and security measures"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, connector: BaseDatabaseConnector):
|
|
19
|
+
super().__init__(connector)
|
|
20
|
+
self.query_executor = DatabaseQueryExecutor(connector, DatabaseDialect.MYSQL)
|
|
21
|
+
self._fts_available = None
|
|
22
|
+
self._mysql_version = None
|
|
23
|
+
|
|
24
|
+
def execute_fulltext_search(
|
|
25
|
+
self,
|
|
26
|
+
query: str,
|
|
27
|
+
namespace: str = "default",
|
|
28
|
+
category_filter: Optional[List[str]] = None,
|
|
29
|
+
limit: int = 10,
|
|
30
|
+
) -> List[Dict[str, Any]]:
|
|
31
|
+
"""Execute MySQL FULLTEXT search with proper validation"""
|
|
32
|
+
try:
|
|
33
|
+
# Validate all parameters
|
|
34
|
+
validated = DatabaseInputValidator.validate_search_params(
|
|
35
|
+
query, namespace, category_filter, limit
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Check if FULLTEXT is available
|
|
39
|
+
if not self._check_fts_available():
|
|
40
|
+
logger.debug(
|
|
41
|
+
"MySQL FULLTEXT not available, falling back to LIKE search"
|
|
42
|
+
)
|
|
43
|
+
return self.execute_fallback_search(
|
|
44
|
+
validated["query"],
|
|
45
|
+
validated["namespace"],
|
|
46
|
+
validated["category_filter"],
|
|
47
|
+
validated["limit"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Execute MySQL FULLTEXT search
|
|
51
|
+
return self.query_executor.execute_search(
|
|
52
|
+
validated["query"],
|
|
53
|
+
validated["namespace"],
|
|
54
|
+
validated["category_filter"],
|
|
55
|
+
validated["limit"],
|
|
56
|
+
use_fts=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
except ValidationError as e:
|
|
60
|
+
logger.error(f"Invalid search parameters: {e}")
|
|
61
|
+
return []
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"MySQL FULLTEXT search failed: {e}")
|
|
64
|
+
return self.execute_fallback_search(
|
|
65
|
+
query, namespace, category_filter, limit
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def create_search_indexes(self) -> List[str]:
|
|
69
|
+
"""Create MySQL-specific search indexes"""
|
|
70
|
+
indexes = []
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
with self.connector.get_connection() as conn:
|
|
74
|
+
cursor = conn.cursor()
|
|
75
|
+
|
|
76
|
+
# Create FULLTEXT indexes (requires InnoDB in MySQL 5.6+)
|
|
77
|
+
fulltext_indexes = [
|
|
78
|
+
"ALTER TABLE short_term_memory ADD FULLTEXT idx_st_fulltext (searchable_content, summary)",
|
|
79
|
+
"ALTER TABLE long_term_memory ADD FULLTEXT idx_lt_fulltext (searchable_content, summary)",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
for index_sql in fulltext_indexes:
|
|
83
|
+
try:
|
|
84
|
+
cursor.execute(index_sql)
|
|
85
|
+
indexes.append("fulltext_index")
|
|
86
|
+
logger.debug("Created FULLTEXT index")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.warning(f"Failed to create FULLTEXT index: {e}")
|
|
89
|
+
|
|
90
|
+
# Create standard indexes for fallback search
|
|
91
|
+
standard_indexes = [
|
|
92
|
+
"CREATE INDEX idx_st_search_mysql ON short_term_memory(namespace, category_primary, importance_score)",
|
|
93
|
+
"CREATE INDEX idx_lt_search_mysql ON long_term_memory(namespace, category_primary, importance_score)",
|
|
94
|
+
"CREATE INDEX idx_st_content_mysql ON short_term_memory(searchable_content(255))", # Prefix index
|
|
95
|
+
"CREATE INDEX idx_lt_content_mysql ON long_term_memory(searchable_content(255))",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
for index_sql in standard_indexes:
|
|
99
|
+
try:
|
|
100
|
+
cursor.execute(index_sql)
|
|
101
|
+
indexes.append(index_sql.split()[2]) # Extract index name
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.warning(f"Failed to create index: {e}")
|
|
104
|
+
|
|
105
|
+
conn.commit()
|
|
106
|
+
logger.info(f"Created {len(indexes)} MySQL search indexes")
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f"Failed to create MySQL search indexes: {e}")
|
|
110
|
+
|
|
111
|
+
return indexes
|
|
112
|
+
|
|
113
|
+
def translate_search_query(self, query: str) -> str:
|
|
114
|
+
"""Translate search query to MySQL FULLTEXT boolean syntax"""
|
|
115
|
+
if not query or not query.strip():
|
|
116
|
+
return ""
|
|
117
|
+
|
|
118
|
+
# Sanitize input for MySQL FULLTEXT
|
|
119
|
+
sanitized = query.strip()
|
|
120
|
+
|
|
121
|
+
# Remove potentially dangerous boolean operators (use phrase search instead)
|
|
122
|
+
dangerous_operators = ["+", "-", "~", "*", '"', "(", ")", "<", ">"]
|
|
123
|
+
for op in dangerous_operators:
|
|
124
|
+
sanitized = sanitized.replace(op, " ")
|
|
125
|
+
|
|
126
|
+
# Split into words and prepare for boolean mode
|
|
127
|
+
words = [
|
|
128
|
+
word.strip()
|
|
129
|
+
for word in sanitized.split()
|
|
130
|
+
if word.strip() and len(word) >= 3
|
|
131
|
+
] # MySQL ft_min_word_len
|
|
132
|
+
|
|
133
|
+
if not words:
|
|
134
|
+
return ""
|
|
135
|
+
|
|
136
|
+
# Use phrase search for safety (wrap in quotes)
|
|
137
|
+
return f'"{" ".join(words)}"'
|
|
138
|
+
|
|
139
|
+
def _check_fts_available(self) -> bool:
|
|
140
|
+
"""Check if MySQL FULLTEXT search is available"""
|
|
141
|
+
if self._fts_available is not None:
|
|
142
|
+
return self._fts_available
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
with self.connector.get_connection() as conn:
|
|
146
|
+
cursor = conn.cursor(dictionary=True)
|
|
147
|
+
|
|
148
|
+
# Check MySQL version
|
|
149
|
+
cursor.execute("SELECT VERSION() as version")
|
|
150
|
+
version_result = cursor.fetchone()
|
|
151
|
+
version = version_result["version"] if version_result else "0.0.0"
|
|
152
|
+
self._mysql_version = version
|
|
153
|
+
|
|
154
|
+
# Check if InnoDB supports FULLTEXT (MySQL 5.6+)
|
|
155
|
+
version_parts = [int(x.split("-")[0]) for x in version.split(".")[:2]]
|
|
156
|
+
version_ok = version_parts[0] > 5 or (
|
|
157
|
+
version_parts[0] == 5 and version_parts[1] >= 6
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Check if we have FULLTEXT indexes
|
|
161
|
+
cursor.execute(
|
|
162
|
+
"""
|
|
163
|
+
SELECT COUNT(*) as fulltext_count
|
|
164
|
+
FROM information_schema.STATISTICS
|
|
165
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
166
|
+
AND (TABLE_NAME = 'short_term_memory' OR TABLE_NAME = 'long_term_memory')
|
|
167
|
+
AND INDEX_TYPE = 'FULLTEXT'
|
|
168
|
+
"""
|
|
169
|
+
)
|
|
170
|
+
fulltext_result = cursor.fetchone()
|
|
171
|
+
has_fulltext = fulltext_result and fulltext_result["fulltext_count"] > 0
|
|
172
|
+
|
|
173
|
+
self._fts_available = version_ok and has_fulltext
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.debug(f"Error checking MySQL FULLTEXT availability: {e}")
|
|
177
|
+
self._fts_available = False
|
|
178
|
+
|
|
179
|
+
return self._fts_available
|
|
180
|
+
|
|
181
|
+
def execute_fallback_search(
|
|
182
|
+
self,
|
|
183
|
+
query: str,
|
|
184
|
+
namespace: str = "default",
|
|
185
|
+
category_filter: Optional[List[str]] = None,
|
|
186
|
+
limit: int = 10,
|
|
187
|
+
) -> List[Dict[str, Any]]:
|
|
188
|
+
"""Execute LIKE-based fallback search for MySQL"""
|
|
189
|
+
try:
|
|
190
|
+
return self.query_executor.execute_search(
|
|
191
|
+
query, namespace, category_filter, limit, use_fts=False
|
|
192
|
+
)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"MySQL fallback search failed: {e}")
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
def optimize_database(self):
|
|
198
|
+
"""Perform MySQL-specific optimizations"""
|
|
199
|
+
try:
|
|
200
|
+
with self.connector.get_connection() as conn:
|
|
201
|
+
cursor = conn.cursor()
|
|
202
|
+
|
|
203
|
+
# Analyze tables for better query planning
|
|
204
|
+
cursor.execute("ANALYZE TABLE short_term_memory")
|
|
205
|
+
cursor.execute("ANALYZE TABLE long_term_memory")
|
|
206
|
+
|
|
207
|
+
# Optimize FULLTEXT indexes
|
|
208
|
+
try:
|
|
209
|
+
cursor.execute("OPTIMIZE TABLE short_term_memory")
|
|
210
|
+
cursor.execute("OPTIMIZE TABLE long_term_memory")
|
|
211
|
+
logger.debug("Optimized MySQL FULLTEXT indexes")
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.warning(f"Table optimization failed: {e}")
|
|
214
|
+
|
|
215
|
+
conn.commit()
|
|
216
|
+
logger.info("MySQL database optimization completed")
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.warning(f"MySQL optimization failed: {e}")
|
|
220
|
+
|
|
221
|
+
def configure_fulltext_settings(self):
|
|
222
|
+
"""Configure MySQL FULLTEXT settings for optimal performance"""
|
|
223
|
+
try:
|
|
224
|
+
with self.connector.get_connection() as conn:
|
|
225
|
+
cursor = conn.cursor()
|
|
226
|
+
|
|
227
|
+
# Check current FULLTEXT settings
|
|
228
|
+
cursor.execute("SHOW VARIABLES LIKE 'ft_%'")
|
|
229
|
+
settings = cursor.fetchall()
|
|
230
|
+
|
|
231
|
+
current_settings = {}
|
|
232
|
+
if hasattr(cursor, "description") and cursor.description:
|
|
233
|
+
for row in settings:
|
|
234
|
+
if isinstance(row, (list, tuple)) and len(row) >= 2:
|
|
235
|
+
current_settings[row[0]] = row[1]
|
|
236
|
+
|
|
237
|
+
logger.debug(f"Current MySQL FULLTEXT settings: {current_settings}")
|
|
238
|
+
|
|
239
|
+
# Recommended settings (these require server restart or global privileges)
|
|
240
|
+
recommended_settings = {
|
|
241
|
+
"ft_min_word_len": "3", # Minimum word length
|
|
242
|
+
"ft_max_word_len": "84", # Maximum word length
|
|
243
|
+
"ft_boolean_syntax": '+ -><()~*:""&|', # Boolean operators
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for setting, value in recommended_settings.items():
|
|
247
|
+
if (
|
|
248
|
+
setting in current_settings
|
|
249
|
+
and current_settings[setting] != value
|
|
250
|
+
):
|
|
251
|
+
logger.info(
|
|
252
|
+
f"Consider setting {setting} = {value} (current: {current_settings.get(setting)})"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.debug(f"Could not check MySQL FULLTEXT settings: {e}")
|
|
257
|
+
|
|
258
|
+
def repair_fulltext_indexes(self):
|
|
259
|
+
"""Repair FULLTEXT indexes if they become corrupted"""
|
|
260
|
+
try:
|
|
261
|
+
with self.connector.get_connection() as conn:
|
|
262
|
+
cursor = conn.cursor()
|
|
263
|
+
|
|
264
|
+
# Check and repair tables with FULLTEXT indexes
|
|
265
|
+
tables_to_repair = ["short_term_memory", "long_term_memory"]
|
|
266
|
+
|
|
267
|
+
for table in tables_to_repair:
|
|
268
|
+
try:
|
|
269
|
+
cursor.execute(f"CHECK TABLE {table}")
|
|
270
|
+
cursor.execute(f"REPAIR TABLE {table}")
|
|
271
|
+
logger.debug(f"Repaired table {table}")
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.warning(f"Could not repair table {table}: {e}")
|
|
274
|
+
|
|
275
|
+
conn.commit()
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.warning(f"FULLTEXT index repair failed: {e}")
|
|
279
|
+
|
|
280
|
+
def get_fulltext_statistics(self) -> Dict[str, Any]:
|
|
281
|
+
"""Get statistics about FULLTEXT usage and performance"""
|
|
282
|
+
stats = {}
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
with self.connector.get_connection() as conn:
|
|
286
|
+
cursor = conn.cursor(dictionary=True)
|
|
287
|
+
|
|
288
|
+
# Get FULLTEXT index information
|
|
289
|
+
cursor.execute(
|
|
290
|
+
"""
|
|
291
|
+
SELECT
|
|
292
|
+
TABLE_NAME,
|
|
293
|
+
INDEX_NAME,
|
|
294
|
+
COLUMN_NAME,
|
|
295
|
+
INDEX_TYPE
|
|
296
|
+
FROM information_schema.STATISTICS
|
|
297
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
298
|
+
AND INDEX_TYPE = 'FULLTEXT'
|
|
299
|
+
ORDER BY TABLE_NAME, INDEX_NAME
|
|
300
|
+
"""
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
fulltext_indexes = cursor.fetchall()
|
|
304
|
+
stats["fulltext_indexes"] = fulltext_indexes
|
|
305
|
+
|
|
306
|
+
# Get table sizes
|
|
307
|
+
cursor.execute(
|
|
308
|
+
"""
|
|
309
|
+
SELECT
|
|
310
|
+
TABLE_NAME,
|
|
311
|
+
TABLE_ROWS,
|
|
312
|
+
DATA_LENGTH,
|
|
313
|
+
INDEX_LENGTH
|
|
314
|
+
FROM information_schema.TABLES
|
|
315
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
316
|
+
AND TABLE_NAME IN ('short_term_memory', 'long_term_memory')
|
|
317
|
+
"""
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
table_stats = cursor.fetchall()
|
|
321
|
+
stats["table_statistics"] = table_stats
|
|
322
|
+
|
|
323
|
+
# MySQL version and FULLTEXT variables
|
|
324
|
+
stats["mysql_version"] = self._mysql_version
|
|
325
|
+
stats["fts_available"] = self._fts_available
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.warning(f"Could not get FULLTEXT statistics: {e}")
|
|
329
|
+
stats["error"] = str(e)
|
|
330
|
+
|
|
331
|
+
return stats
|