memorisdk 1.0.1__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 +1676 -534
- 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.1.dist-info → memorisdk-2.0.0.dist-info}/METADATA +144 -34
- memorisdk-2.0.0.dist-info/RECORD +67 -0
- memorisdk-1.0.1.dist-info/RECORD +0 -44
- memorisdk-1.0.1.dist-info/entry_points.txt +0 -2
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.1.dist-info → memorisdk-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL-specific search adapter with tsvector 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 PostgreSQLSearchAdapter(BaseSearchAdapter):
|
|
16
|
+
"""PostgreSQL-specific search implementation with tsvector and security measures"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, connector: BaseDatabaseConnector):
|
|
19
|
+
super().__init__(connector)
|
|
20
|
+
self.query_executor = DatabaseQueryExecutor(
|
|
21
|
+
connector, DatabaseDialect.POSTGRESQL
|
|
22
|
+
)
|
|
23
|
+
self._fts_available = None
|
|
24
|
+
|
|
25
|
+
def execute_fulltext_search(
|
|
26
|
+
self,
|
|
27
|
+
query: str,
|
|
28
|
+
namespace: str = "default",
|
|
29
|
+
category_filter: Optional[List[str]] = None,
|
|
30
|
+
limit: int = 10,
|
|
31
|
+
) -> List[Dict[str, Any]]:
|
|
32
|
+
"""Execute PostgreSQL full-text search with tsvector"""
|
|
33
|
+
try:
|
|
34
|
+
# Validate all parameters
|
|
35
|
+
validated = DatabaseInputValidator.validate_search_params(
|
|
36
|
+
query, namespace, category_filter, limit
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Check if FTS is available
|
|
40
|
+
if not self._check_fts_available():
|
|
41
|
+
logger.debug(
|
|
42
|
+
"PostgreSQL FTS not available, falling back to LIKE search"
|
|
43
|
+
)
|
|
44
|
+
return self.execute_fallback_search(
|
|
45
|
+
validated["query"],
|
|
46
|
+
validated["namespace"],
|
|
47
|
+
validated["category_filter"],
|
|
48
|
+
validated["limit"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Execute PostgreSQL tsvector search
|
|
52
|
+
return self.query_executor.execute_search(
|
|
53
|
+
validated["query"],
|
|
54
|
+
validated["namespace"],
|
|
55
|
+
validated["category_filter"],
|
|
56
|
+
validated["limit"],
|
|
57
|
+
use_fts=True,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
except ValidationError as e:
|
|
61
|
+
logger.error(f"Invalid search parameters: {e}")
|
|
62
|
+
return []
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"PostgreSQL FTS search failed: {e}")
|
|
65
|
+
return self.execute_fallback_search(
|
|
66
|
+
query, namespace, category_filter, limit
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def create_search_indexes(self) -> List[str]:
|
|
70
|
+
"""Create PostgreSQL-specific search indexes"""
|
|
71
|
+
indexes = []
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
with self.connector.get_connection() as conn:
|
|
75
|
+
cursor = conn.cursor()
|
|
76
|
+
|
|
77
|
+
# Create GIN indexes for full-text search
|
|
78
|
+
fts_indexes = [
|
|
79
|
+
"""
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_short_term_fts_gin
|
|
81
|
+
ON short_term_memory
|
|
82
|
+
USING gin(to_tsvector('english', searchable_content || ' ' || summary))
|
|
83
|
+
""",
|
|
84
|
+
"""
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_long_term_fts_gin
|
|
86
|
+
ON long_term_memory
|
|
87
|
+
USING gin(to_tsvector('english', searchable_content || ' ' || summary))
|
|
88
|
+
""",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for index_sql in fts_indexes:
|
|
92
|
+
try:
|
|
93
|
+
cursor.execute(index_sql)
|
|
94
|
+
indexes.append("gin_fts_index")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.warning(f"Failed to create FTS index: {e}")
|
|
97
|
+
|
|
98
|
+
# Create standard B-tree indexes for fallback search
|
|
99
|
+
standard_indexes = [
|
|
100
|
+
"CREATE INDEX IF NOT EXISTS idx_st_search_pg ON short_term_memory(namespace, category_primary, importance_score)",
|
|
101
|
+
"CREATE INDEX IF NOT EXISTS idx_lt_search_pg ON long_term_memory(namespace, category_primary, importance_score)",
|
|
102
|
+
"CREATE INDEX IF NOT EXISTS idx_st_content_pg ON short_term_memory USING gin(searchable_content gin_trgm_ops)",
|
|
103
|
+
"CREATE INDEX IF NOT EXISTS idx_lt_content_pg ON long_term_memory USING gin(searchable_content gin_trgm_ops)",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Enable trigram extension for better LIKE performance
|
|
107
|
+
try:
|
|
108
|
+
cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
|
|
109
|
+
logger.info("Enabled PostgreSQL pg_trgm extension")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.warning(f"Could not enable pg_trgm extension: {e}")
|
|
112
|
+
|
|
113
|
+
for index_sql in standard_indexes:
|
|
114
|
+
try:
|
|
115
|
+
cursor.execute(index_sql)
|
|
116
|
+
indexes.append(index_sql.split()[-1])
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.warning(f"Failed to create index: {e}")
|
|
119
|
+
|
|
120
|
+
conn.commit()
|
|
121
|
+
logger.info(f"Created {len(indexes)} PostgreSQL search indexes")
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Failed to create PostgreSQL search indexes: {e}")
|
|
125
|
+
|
|
126
|
+
return indexes
|
|
127
|
+
|
|
128
|
+
def translate_search_query(self, query: str) -> str:
|
|
129
|
+
"""Translate search query to PostgreSQL tsquery syntax"""
|
|
130
|
+
if not query or not query.strip():
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
# Sanitize input for tsquery
|
|
134
|
+
sanitized = query.strip()
|
|
135
|
+
|
|
136
|
+
# Remove potentially dangerous operators
|
|
137
|
+
dangerous_chars = ["!", "&", "|", "(", ")", "<", ">"]
|
|
138
|
+
for char in dangerous_chars:
|
|
139
|
+
sanitized = sanitized.replace(char, " ")
|
|
140
|
+
|
|
141
|
+
# Split into words and clean
|
|
142
|
+
words = [word.strip() for word in sanitized.split() if word.strip()]
|
|
143
|
+
|
|
144
|
+
if not words:
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
# Join words with AND operator (safer than allowing user operators)
|
|
148
|
+
return " & ".join(words)
|
|
149
|
+
|
|
150
|
+
def _check_fts_available(self) -> bool:
|
|
151
|
+
"""Check if PostgreSQL full-text search is available"""
|
|
152
|
+
if self._fts_available is not None:
|
|
153
|
+
return self._fts_available
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
with self.connector.get_connection() as conn:
|
|
157
|
+
cursor = conn.cursor()
|
|
158
|
+
# Test tsvector functionality
|
|
159
|
+
cursor.execute(
|
|
160
|
+
"SELECT to_tsvector('english', 'test') @@ plainto_tsquery('english', 'test')"
|
|
161
|
+
)
|
|
162
|
+
result = cursor.fetchone()
|
|
163
|
+
self._fts_available = result[0] if result else False
|
|
164
|
+
except Exception:
|
|
165
|
+
self._fts_available = False
|
|
166
|
+
|
|
167
|
+
return self._fts_available
|
|
168
|
+
|
|
169
|
+
def execute_fallback_search(
|
|
170
|
+
self,
|
|
171
|
+
query: str,
|
|
172
|
+
namespace: str = "default",
|
|
173
|
+
category_filter: Optional[List[str]] = None,
|
|
174
|
+
limit: int = 10,
|
|
175
|
+
) -> List[Dict[str, Any]]:
|
|
176
|
+
"""Execute LIKE-based fallback search for PostgreSQL"""
|
|
177
|
+
try:
|
|
178
|
+
return self.query_executor.execute_search(
|
|
179
|
+
query, namespace, category_filter, limit, use_fts=False
|
|
180
|
+
)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"PostgreSQL fallback search failed: {e}")
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
def optimize_database(self):
|
|
186
|
+
"""Perform PostgreSQL-specific optimizations"""
|
|
187
|
+
try:
|
|
188
|
+
with self.connector.get_connection() as conn:
|
|
189
|
+
cursor = conn.cursor()
|
|
190
|
+
|
|
191
|
+
# Update table statistics
|
|
192
|
+
cursor.execute("ANALYZE short_term_memory")
|
|
193
|
+
cursor.execute("ANALYZE long_term_memory")
|
|
194
|
+
|
|
195
|
+
# Reindex GIN indexes for FTS performance
|
|
196
|
+
try:
|
|
197
|
+
cursor.execute("REINDEX INDEX CONCURRENTLY idx_short_term_fts_gin")
|
|
198
|
+
cursor.execute("REINDEX INDEX CONCURRENTLY idx_long_term_fts_gin")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.debug(
|
|
201
|
+
f"Concurrent reindex failed, trying regular reindex: {e}"
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
cursor.execute("REINDEX INDEX idx_short_term_fts_gin")
|
|
205
|
+
cursor.execute("REINDEX INDEX idx_long_term_fts_gin")
|
|
206
|
+
except Exception as e2:
|
|
207
|
+
logger.warning(f"Reindex failed: {e2}")
|
|
208
|
+
|
|
209
|
+
conn.commit()
|
|
210
|
+
logger.info("PostgreSQL database optimization completed")
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.warning(f"PostgreSQL optimization failed: {e}")
|
|
214
|
+
|
|
215
|
+
def create_materialized_views(self):
|
|
216
|
+
"""Create materialized views for better search performance"""
|
|
217
|
+
try:
|
|
218
|
+
with self.connector.get_connection() as conn:
|
|
219
|
+
cursor = conn.cursor()
|
|
220
|
+
|
|
221
|
+
# Create materialized view for search optimization
|
|
222
|
+
mv_sql = """
|
|
223
|
+
CREATE MATERIALIZED VIEW IF NOT EXISTS memory_search_mv AS
|
|
224
|
+
SELECT
|
|
225
|
+
memory_id,
|
|
226
|
+
'short_term' as memory_type,
|
|
227
|
+
namespace,
|
|
228
|
+
category_primary,
|
|
229
|
+
searchable_content,
|
|
230
|
+
summary,
|
|
231
|
+
importance_score,
|
|
232
|
+
created_at,
|
|
233
|
+
to_tsvector('english', searchable_content || ' ' || summary) as search_vector
|
|
234
|
+
FROM short_term_memory
|
|
235
|
+
WHERE expires_at IS NULL OR expires_at > NOW()
|
|
236
|
+
|
|
237
|
+
UNION ALL
|
|
238
|
+
|
|
239
|
+
SELECT
|
|
240
|
+
memory_id,
|
|
241
|
+
'long_term' as memory_type,
|
|
242
|
+
namespace,
|
|
243
|
+
category_primary,
|
|
244
|
+
searchable_content,
|
|
245
|
+
summary,
|
|
246
|
+
importance_score,
|
|
247
|
+
created_at,
|
|
248
|
+
to_tsvector('english', searchable_content || ' ' || summary) as search_vector
|
|
249
|
+
FROM long_term_memory
|
|
250
|
+
|
|
251
|
+
ORDER BY importance_score DESC, created_at DESC
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
cursor.execute(mv_sql)
|
|
255
|
+
|
|
256
|
+
# Create index on materialized view
|
|
257
|
+
cursor.execute(
|
|
258
|
+
"""
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_memory_search_mv_fts
|
|
260
|
+
ON memory_search_mv
|
|
261
|
+
USING gin(search_vector)
|
|
262
|
+
"""
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
cursor.execute(
|
|
266
|
+
"""
|
|
267
|
+
CREATE INDEX IF NOT EXISTS idx_memory_search_mv_filter
|
|
268
|
+
ON memory_search_mv (namespace, category_primary, memory_type)
|
|
269
|
+
"""
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
conn.commit()
|
|
273
|
+
logger.info(
|
|
274
|
+
"Created PostgreSQL materialized view for search optimization"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.warning(f"Failed to create materialized view: {e}")
|
|
279
|
+
|
|
280
|
+
def refresh_materialized_views(self):
|
|
281
|
+
"""Refresh materialized views for up-to-date search results"""
|
|
282
|
+
try:
|
|
283
|
+
with self.connector.get_connection() as conn:
|
|
284
|
+
cursor = conn.cursor()
|
|
285
|
+
cursor.execute(
|
|
286
|
+
"REFRESH MATERIALIZED VIEW CONCURRENTLY memory_search_mv"
|
|
287
|
+
)
|
|
288
|
+
conn.commit()
|
|
289
|
+
logger.debug("Refreshed PostgreSQL materialized view")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.warning(f"Failed to refresh materialized view: {e}")
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite-specific search adapter with FTS5 support and proper security
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from ...utils.exceptions import ValidationError
|
|
11
|
+
from ...utils.input_validator import DatabaseInputValidator
|
|
12
|
+
from ...utils.query_builder import DatabaseDialect, DatabaseQueryExecutor
|
|
13
|
+
from ..connectors.base_connector import BaseDatabaseConnector, BaseSearchAdapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQLiteSearchAdapter(BaseSearchAdapter):
|
|
17
|
+
"""SQLite-specific search implementation with FTS5 and security measures"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, connector: BaseDatabaseConnector):
|
|
20
|
+
super().__init__(connector)
|
|
21
|
+
self.query_executor = DatabaseQueryExecutor(connector, DatabaseDialect.SQLITE)
|
|
22
|
+
self._fts_available = 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 SQLite FTS5 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 FTS is available
|
|
39
|
+
if not self._check_fts_available():
|
|
40
|
+
logger.debug("FTS not available, falling back to LIKE search")
|
|
41
|
+
return self.execute_fallback_search(
|
|
42
|
+
validated["query"],
|
|
43
|
+
validated["namespace"],
|
|
44
|
+
validated["category_filter"],
|
|
45
|
+
validated["limit"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Execute FTS search
|
|
49
|
+
return self.query_executor.execute_search(
|
|
50
|
+
validated["query"],
|
|
51
|
+
validated["namespace"],
|
|
52
|
+
validated["category_filter"],
|
|
53
|
+
validated["limit"],
|
|
54
|
+
use_fts=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
except ValidationError as e:
|
|
58
|
+
logger.error(f"Invalid search parameters: {e}")
|
|
59
|
+
return []
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"SQLite FTS search failed: {e}")
|
|
62
|
+
# Fallback to LIKE search on error
|
|
63
|
+
return self.execute_fallback_search(
|
|
64
|
+
query, namespace, category_filter, limit
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def create_search_indexes(self) -> List[str]:
|
|
68
|
+
"""Create SQLite-specific search indexes"""
|
|
69
|
+
indexes = []
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with self.connector.get_connection() as conn:
|
|
73
|
+
cursor = conn.cursor()
|
|
74
|
+
|
|
75
|
+
# Create FTS5 virtual table if not exists
|
|
76
|
+
fts_sql = """
|
|
77
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_search_fts USING fts5(
|
|
78
|
+
memory_id,
|
|
79
|
+
memory_type,
|
|
80
|
+
namespace,
|
|
81
|
+
searchable_content,
|
|
82
|
+
summary,
|
|
83
|
+
category_primary,
|
|
84
|
+
content='',
|
|
85
|
+
contentless_delete=1
|
|
86
|
+
)
|
|
87
|
+
"""
|
|
88
|
+
cursor.execute(fts_sql)
|
|
89
|
+
indexes.append("memory_search_fts")
|
|
90
|
+
|
|
91
|
+
# Create standard indexes for fallback search
|
|
92
|
+
standard_indexes = [
|
|
93
|
+
"CREATE INDEX IF NOT EXISTS idx_st_search ON short_term_memory(namespace, category_primary, importance_score)",
|
|
94
|
+
"CREATE INDEX IF NOT EXISTS idx_lt_search ON long_term_memory(namespace, category_primary, importance_score)",
|
|
95
|
+
"CREATE INDEX IF NOT EXISTS idx_st_content ON short_term_memory(searchable_content)",
|
|
96
|
+
"CREATE INDEX IF NOT EXISTS idx_lt_content ON long_term_memory(searchable_content)",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for index_sql in standard_indexes:
|
|
100
|
+
try:
|
|
101
|
+
cursor.execute(index_sql)
|
|
102
|
+
indexes.append(index_sql.split()[-1]) # Extract index name
|
|
103
|
+
except sqlite3.Error as e:
|
|
104
|
+
logger.warning(f"Failed to create index: {e}")
|
|
105
|
+
|
|
106
|
+
# Create triggers to maintain FTS index
|
|
107
|
+
triggers = [
|
|
108
|
+
"""
|
|
109
|
+
CREATE TRIGGER IF NOT EXISTS short_term_memory_fts_insert
|
|
110
|
+
AFTER INSERT ON short_term_memory
|
|
111
|
+
BEGIN
|
|
112
|
+
INSERT INTO memory_search_fts(memory_id, memory_type, namespace, searchable_content, summary, category_primary)
|
|
113
|
+
VALUES (NEW.memory_id, 'short_term', NEW.namespace, NEW.searchable_content, NEW.summary, NEW.category_primary);
|
|
114
|
+
END
|
|
115
|
+
""",
|
|
116
|
+
"""
|
|
117
|
+
CREATE TRIGGER IF NOT EXISTS long_term_memory_fts_insert
|
|
118
|
+
AFTER INSERT ON long_term_memory
|
|
119
|
+
BEGIN
|
|
120
|
+
INSERT INTO memory_search_fts(memory_id, memory_type, namespace, searchable_content, summary, category_primary)
|
|
121
|
+
VALUES (NEW.memory_id, 'long_term', NEW.namespace, NEW.searchable_content, NEW.summary, NEW.category_primary);
|
|
122
|
+
END
|
|
123
|
+
""",
|
|
124
|
+
"""
|
|
125
|
+
CREATE TRIGGER IF NOT EXISTS short_term_memory_fts_delete
|
|
126
|
+
AFTER DELETE ON short_term_memory
|
|
127
|
+
BEGIN
|
|
128
|
+
DELETE FROM memory_search_fts WHERE memory_id = OLD.memory_id AND memory_type = 'short_term';
|
|
129
|
+
END
|
|
130
|
+
""",
|
|
131
|
+
"""
|
|
132
|
+
CREATE TRIGGER IF NOT EXISTS long_term_memory_fts_delete
|
|
133
|
+
AFTER DELETE ON long_term_memory
|
|
134
|
+
BEGIN
|
|
135
|
+
DELETE FROM memory_search_fts WHERE memory_id = OLD.memory_id AND memory_type = 'long_term';
|
|
136
|
+
END
|
|
137
|
+
""",
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
for trigger_sql in triggers:
|
|
141
|
+
try:
|
|
142
|
+
cursor.execute(trigger_sql)
|
|
143
|
+
except sqlite3.Error as e:
|
|
144
|
+
logger.warning(f"Failed to create trigger: {e}")
|
|
145
|
+
|
|
146
|
+
conn.commit()
|
|
147
|
+
logger.info(f"Created {len(indexes)} SQLite search indexes")
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Failed to create SQLite search indexes: {e}")
|
|
151
|
+
|
|
152
|
+
return indexes
|
|
153
|
+
|
|
154
|
+
def translate_search_query(self, query: str) -> str:
|
|
155
|
+
"""Translate search query to SQLite FTS5 syntax"""
|
|
156
|
+
if not query or not query.strip():
|
|
157
|
+
return "*"
|
|
158
|
+
|
|
159
|
+
# Escape FTS5 special characters
|
|
160
|
+
sanitized = query.replace('"', '""') # Escape quotes
|
|
161
|
+
|
|
162
|
+
# Remove potentially dangerous FTS operators
|
|
163
|
+
dangerous_operators = ["AND", "OR", "NOT", "NEAR", "^"]
|
|
164
|
+
for op in dangerous_operators:
|
|
165
|
+
sanitized = sanitized.replace(op, "")
|
|
166
|
+
|
|
167
|
+
# Wrap in quotes for phrase search (safer than operators)
|
|
168
|
+
return f'"{sanitized.strip()}"'
|
|
169
|
+
|
|
170
|
+
def _check_fts_available(self) -> bool:
|
|
171
|
+
"""Check if FTS5 is available in SQLite"""
|
|
172
|
+
if self._fts_available is not None:
|
|
173
|
+
return self._fts_available
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
with self.connector.get_connection() as conn:
|
|
177
|
+
cursor = conn.cursor()
|
|
178
|
+
cursor.execute("CREATE VIRTUAL TABLE fts_test USING fts5(content)")
|
|
179
|
+
cursor.execute("DROP TABLE fts_test")
|
|
180
|
+
self._fts_available = True
|
|
181
|
+
except sqlite3.OperationalError:
|
|
182
|
+
self._fts_available = False
|
|
183
|
+
except Exception:
|
|
184
|
+
self._fts_available = False
|
|
185
|
+
|
|
186
|
+
return self._fts_available
|
|
187
|
+
|
|
188
|
+
def execute_fallback_search(
|
|
189
|
+
self,
|
|
190
|
+
query: str,
|
|
191
|
+
namespace: str = "default",
|
|
192
|
+
category_filter: Optional[List[str]] = None,
|
|
193
|
+
limit: int = 10,
|
|
194
|
+
) -> List[Dict[str, Any]]:
|
|
195
|
+
"""Execute LIKE-based fallback search for SQLite"""
|
|
196
|
+
try:
|
|
197
|
+
return self.query_executor.execute_search(
|
|
198
|
+
query, namespace, category_filter, limit, use_fts=False
|
|
199
|
+
)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f"SQLite fallback search failed: {e}")
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
def optimize_database(self):
|
|
205
|
+
"""Perform SQLite-specific optimizations"""
|
|
206
|
+
try:
|
|
207
|
+
with self.connector.get_connection() as conn:
|
|
208
|
+
cursor = conn.cursor()
|
|
209
|
+
|
|
210
|
+
# Analyze tables for better query planning
|
|
211
|
+
cursor.execute("ANALYZE")
|
|
212
|
+
|
|
213
|
+
# Optimize FTS index if available
|
|
214
|
+
if self._check_fts_available():
|
|
215
|
+
try:
|
|
216
|
+
cursor.execute(
|
|
217
|
+
"INSERT INTO memory_search_fts(memory_search_fts) VALUES('optimize')"
|
|
218
|
+
)
|
|
219
|
+
except sqlite3.Error as e:
|
|
220
|
+
logger.debug(f"FTS optimization skipped: {e}")
|
|
221
|
+
|
|
222
|
+
# Update table statistics
|
|
223
|
+
cursor.execute("PRAGMA optimize")
|
|
224
|
+
|
|
225
|
+
conn.commit()
|
|
226
|
+
logger.info("SQLite database optimization completed")
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.warning(f"SQLite optimization failed: {e}")
|