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.
- memori/__init__.py +24 -8
- memori/agents/conscious_agent.py +252 -414
- memori/agents/memory_agent.py +487 -224
- memori/agents/retrieval_agent.py +491 -68
- memori/config/memory_manager.py +323 -0
- memori/core/conversation.py +393 -0
- memori/core/database.py +386 -371
- memori/core/memory.py +1683 -532
- 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 +700 -0
- memori/database/sqlalchemy_manager.py +888 -0
- memori/integrations/__init__.py +36 -11
- memori/integrations/litellm_integration.py +340 -6
- memori/integrations/openai_integration.py +506 -240
- memori/tools/memory_tool.py +94 -4
- 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.1.dist-info}/METADATA +56 -23
- memorisdk-2.0.1.dist-info/RECORD +66 -0
- memori/scripts/llm_text.py +0 -50
- 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.1.dist-info}/WHEEL +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-1.0.2.dist-info → memorisdk-2.0.1.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()}"'
|