memorisdk 2.0.0__py3-none-any.whl → 2.1.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 +3 -3
- memori/agents/conscious_agent.py +289 -77
- memori/agents/memory_agent.py +19 -9
- memori/agents/retrieval_agent.py +138 -63
- memori/config/manager.py +7 -7
- memori/config/memory_manager.py +25 -25
- memori/config/settings.py +13 -6
- memori/core/conversation.py +15 -15
- memori/core/database.py +14 -13
- memori/core/memory.py +438 -123
- memori/core/providers.py +25 -25
- memori/database/__init__.py +11 -0
- memori/database/adapters/__init__.py +11 -0
- memori/database/adapters/mongodb_adapter.py +739 -0
- memori/database/adapters/mysql_adapter.py +8 -8
- memori/database/adapters/postgresql_adapter.py +6 -6
- memori/database/adapters/sqlite_adapter.py +6 -6
- memori/database/auto_creator.py +8 -9
- memori/database/connection_utils.py +5 -5
- memori/database/connectors/__init__.py +11 -0
- memori/database/connectors/base_connector.py +18 -19
- memori/database/connectors/mongodb_connector.py +527 -0
- memori/database/connectors/mysql_connector.py +13 -15
- memori/database/connectors/postgres_connector.py +12 -12
- memori/database/connectors/sqlite_connector.py +11 -11
- memori/database/models.py +2 -2
- memori/database/mongodb_manager.py +1402 -0
- memori/database/queries/base_queries.py +3 -4
- memori/database/queries/chat_queries.py +3 -5
- memori/database/queries/entity_queries.py +3 -5
- memori/database/queries/memory_queries.py +3 -5
- memori/database/query_translator.py +11 -11
- memori/database/schema_generators/__init__.py +11 -0
- memori/database/schema_generators/mongodb_schema_generator.py +666 -0
- memori/database/schema_generators/mysql_schema_generator.py +2 -4
- memori/database/search/__init__.py +11 -0
- memori/database/search/mongodb_search_adapter.py +653 -0
- memori/database/search/mysql_search_adapter.py +8 -8
- memori/database/search/sqlite_search_adapter.py +6 -6
- memori/database/search_service.py +218 -66
- memori/database/sqlalchemy_manager.py +72 -25
- memori/integrations/__init__.py +1 -1
- memori/integrations/anthropic_integration.py +1 -3
- memori/integrations/litellm_integration.py +23 -6
- memori/integrations/openai_integration.py +31 -3
- memori/tools/memory_tool.py +104 -13
- memori/utils/exceptions.py +58 -58
- memori/utils/helpers.py +11 -12
- memori/utils/input_validator.py +10 -12
- memori/utils/logging.py +4 -4
- memori/utils/pydantic_models.py +57 -57
- memori/utils/query_builder.py +20 -20
- memori/utils/security_audit.py +28 -28
- memori/utils/security_integration.py +9 -9
- memori/utils/transaction_manager.py +20 -19
- memori/utils/validators.py +6 -6
- {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.dist-info}/METADATA +36 -20
- memorisdk-2.1.0.dist-info/RECORD +71 -0
- memori/scripts/llm_text.py +0 -50
- memorisdk-2.0.0.dist-info/RECORD +0 -67
- {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.dist-info}/WHEEL +0 -0
- {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-2.0.0.dist-info → memorisdk-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -3,7 +3,7 @@ MySQL FULLTEXT search adapter for Memori
|
|
|
3
3
|
Implements MySQL-specific search functionality
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
8
|
from loguru import logger
|
|
9
9
|
|
|
@@ -49,9 +49,9 @@ class MySQLSearchAdapter(BaseSearchAdapter):
|
|
|
49
49
|
self,
|
|
50
50
|
query: str,
|
|
51
51
|
namespace: str = "default",
|
|
52
|
-
category_filter:
|
|
52
|
+
category_filter: list[str] | None = None,
|
|
53
53
|
limit: int = 10,
|
|
54
|
-
) ->
|
|
54
|
+
) -> list[dict[str, Any]]:
|
|
55
55
|
"""Execute MySQL FULLTEXT search or fallback to LIKE search"""
|
|
56
56
|
if self.fulltext_available and query and query.strip():
|
|
57
57
|
try:
|
|
@@ -70,9 +70,9 @@ class MySQLSearchAdapter(BaseSearchAdapter):
|
|
|
70
70
|
self,
|
|
71
71
|
query: str,
|
|
72
72
|
namespace: str,
|
|
73
|
-
category_filter:
|
|
73
|
+
category_filter: list[str] | None,
|
|
74
74
|
limit: int,
|
|
75
|
-
) ->
|
|
75
|
+
) -> list[dict[str, Any]]:
|
|
76
76
|
"""Execute MySQL FULLTEXT search"""
|
|
77
77
|
results = []
|
|
78
78
|
translated_query = self.translate_search_query(query)
|
|
@@ -160,7 +160,7 @@ class MySQLSearchAdapter(BaseSearchAdapter):
|
|
|
160
160
|
except:
|
|
161
161
|
return 0.0
|
|
162
162
|
|
|
163
|
-
def create_search_indexes(self) ->
|
|
163
|
+
def create_search_indexes(self) -> list[str]:
|
|
164
164
|
"""Create MySQL FULLTEXT indexes"""
|
|
165
165
|
commands = [
|
|
166
166
|
# Create FULLTEXT index on short_term_memory
|
|
@@ -206,9 +206,9 @@ class MySQLSearchAdapter(BaseSearchAdapter):
|
|
|
206
206
|
self,
|
|
207
207
|
query: str,
|
|
208
208
|
namespace: str = "default",
|
|
209
|
-
category_filter:
|
|
209
|
+
category_filter: list[str] | None = None,
|
|
210
210
|
limit: int = 10,
|
|
211
|
-
) ->
|
|
211
|
+
) -> list[dict[str, Any]]:
|
|
212
212
|
"""Execute fallback LIKE-based search with MySQL syntax"""
|
|
213
213
|
results = []
|
|
214
214
|
|
|
@@ -4,7 +4,7 @@ Maintains existing FTS5 functionality
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import sqlite3
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
from loguru import logger
|
|
10
10
|
|
|
@@ -34,9 +34,9 @@ class SQLiteSearchAdapter(BaseSearchAdapter):
|
|
|
34
34
|
self,
|
|
35
35
|
query: str,
|
|
36
36
|
namespace: str = "default",
|
|
37
|
-
category_filter:
|
|
37
|
+
category_filter: list[str] | None = None,
|
|
38
38
|
limit: int = 10,
|
|
39
|
-
) ->
|
|
39
|
+
) -> list[dict[str, Any]]:
|
|
40
40
|
"""Execute FTS5 search or fallback to LIKE search"""
|
|
41
41
|
if self.fts5_available:
|
|
42
42
|
try:
|
|
@@ -53,9 +53,9 @@ class SQLiteSearchAdapter(BaseSearchAdapter):
|
|
|
53
53
|
self,
|
|
54
54
|
query: str,
|
|
55
55
|
namespace: str,
|
|
56
|
-
category_filter:
|
|
56
|
+
category_filter: list[str] | None,
|
|
57
57
|
limit: int,
|
|
58
|
-
) ->
|
|
58
|
+
) -> list[dict[str, Any]]:
|
|
59
59
|
"""Execute FTS5 search (adapted from original implementation)"""
|
|
60
60
|
# Build FTS query with category filter - handle empty queries
|
|
61
61
|
if query and query.strip():
|
|
@@ -119,7 +119,7 @@ class SQLiteSearchAdapter(BaseSearchAdapter):
|
|
|
119
119
|
|
|
120
120
|
return self.connector.execute_query(sql_query, params)
|
|
121
121
|
|
|
122
|
-
def create_search_indexes(self) ->
|
|
122
|
+
def create_search_indexes(self) -> list[str]:
|
|
123
123
|
"""Create FTS5 virtual table and triggers"""
|
|
124
124
|
if not self.fts5_available:
|
|
125
125
|
logger.warning("FTS5 not available, skipping FTS index creation")
|
|
@@ -4,7 +4,7 @@ Provides cross-database full-text search capabilities
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
from loguru import logger
|
|
10
10
|
from sqlalchemy import and_, desc, or_, text
|
|
@@ -24,10 +24,10 @@ class SearchService:
|
|
|
24
24
|
self,
|
|
25
25
|
query: str,
|
|
26
26
|
namespace: str = "default",
|
|
27
|
-
category_filter:
|
|
27
|
+
category_filter: list[str] | None = None,
|
|
28
28
|
limit: int = 10,
|
|
29
|
-
memory_types:
|
|
30
|
-
) ->
|
|
29
|
+
memory_types: list[str] | None = None,
|
|
30
|
+
) -> list[dict[str, Any]]:
|
|
31
31
|
"""
|
|
32
32
|
Search memories across different database backends
|
|
33
33
|
|
|
@@ -41,7 +41,12 @@ class SearchService:
|
|
|
41
41
|
Returns:
|
|
42
42
|
List of memory dictionaries with search metadata
|
|
43
43
|
"""
|
|
44
|
+
logger.debug(
|
|
45
|
+
f"SearchService.search_memories called - query: '{query}', namespace: '{namespace}', database: {self.database_type}, limit: {limit}"
|
|
46
|
+
)
|
|
47
|
+
|
|
44
48
|
if not query or not query.strip():
|
|
49
|
+
logger.debug("Empty query provided, returning recent memories")
|
|
45
50
|
return self._get_recent_memories(
|
|
46
51
|
namespace, category_filter, limit, memory_types
|
|
47
52
|
)
|
|
@@ -52,9 +57,14 @@ class SearchService:
|
|
|
52
57
|
search_short_term = not memory_types or "short_term" in memory_types
|
|
53
58
|
search_long_term = not memory_types or "long_term" in memory_types
|
|
54
59
|
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"Memory types to search - short_term: {search_short_term}, long_term: {search_long_term}, categories: {category_filter}"
|
|
62
|
+
)
|
|
63
|
+
|
|
55
64
|
try:
|
|
56
65
|
# Try database-specific full-text search first
|
|
57
66
|
if self.database_type == "sqlite":
|
|
67
|
+
logger.debug("Using SQLite FTS5 search strategy")
|
|
58
68
|
results = self._search_sqlite_fts(
|
|
59
69
|
query,
|
|
60
70
|
namespace,
|
|
@@ -64,6 +74,7 @@ class SearchService:
|
|
|
64
74
|
search_long_term,
|
|
65
75
|
)
|
|
66
76
|
elif self.database_type == "mysql":
|
|
77
|
+
logger.debug("Using MySQL FULLTEXT search strategy")
|
|
67
78
|
results = self._search_mysql_fulltext(
|
|
68
79
|
query,
|
|
69
80
|
namespace,
|
|
@@ -73,6 +84,7 @@ class SearchService:
|
|
|
73
84
|
search_long_term,
|
|
74
85
|
)
|
|
75
86
|
elif self.database_type == "postgresql":
|
|
87
|
+
logger.debug("Using PostgreSQL FTS search strategy")
|
|
76
88
|
results = self._search_postgresql_fts(
|
|
77
89
|
query,
|
|
78
90
|
namespace,
|
|
@@ -82,8 +94,13 @@ class SearchService:
|
|
|
82
94
|
search_long_term,
|
|
83
95
|
)
|
|
84
96
|
|
|
97
|
+
logger.debug(f"Primary search strategy returned {len(results)} results")
|
|
98
|
+
|
|
85
99
|
# If no results or full-text search failed, fall back to LIKE search
|
|
86
100
|
if not results:
|
|
101
|
+
logger.debug(
|
|
102
|
+
"Primary search returned no results, falling back to LIKE search"
|
|
103
|
+
)
|
|
87
104
|
results = self._search_like_fallback(
|
|
88
105
|
query,
|
|
89
106
|
namespace,
|
|
@@ -94,31 +111,69 @@ class SearchService:
|
|
|
94
111
|
)
|
|
95
112
|
|
|
96
113
|
except Exception as e:
|
|
97
|
-
logger.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
logger.error(
|
|
115
|
+
f"Full-text search failed for query '{query}' in namespace '{namespace}': {e}"
|
|
116
|
+
)
|
|
117
|
+
logger.debug(
|
|
118
|
+
f"Full-text search error details: {type(e).__name__}: {str(e)}",
|
|
119
|
+
exc_info=True,
|
|
120
|
+
)
|
|
121
|
+
logger.warning(f"Falling back to LIKE search for query '{query}'")
|
|
122
|
+
try:
|
|
123
|
+
results = self._search_like_fallback(
|
|
124
|
+
query,
|
|
125
|
+
namespace,
|
|
126
|
+
category_filter,
|
|
127
|
+
limit,
|
|
128
|
+
search_short_term,
|
|
129
|
+
search_long_term,
|
|
130
|
+
)
|
|
131
|
+
logger.debug(f"LIKE fallback search returned {len(results)} results")
|
|
132
|
+
except Exception as fallback_e:
|
|
133
|
+
logger.error(
|
|
134
|
+
f"LIKE fallback search also failed for query '{query}': {fallback_e}"
|
|
135
|
+
)
|
|
136
|
+
results = []
|
|
137
|
+
|
|
138
|
+
final_results = self._rank_and_limit_results(results, limit)
|
|
139
|
+
logger.debug(
|
|
140
|
+
f"SearchService completed - returning {len(final_results)} final results after ranking and limiting"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if final_results:
|
|
144
|
+
logger.debug(
|
|
145
|
+
f"Top result: memory_id={final_results[0].get('memory_id')}, score={final_results[0].get('composite_score', 0):.3f}, strategy={final_results[0].get('search_strategy')}"
|
|
105
146
|
)
|
|
106
147
|
|
|
107
|
-
return
|
|
148
|
+
return final_results
|
|
108
149
|
|
|
109
150
|
def _search_sqlite_fts(
|
|
110
151
|
self,
|
|
111
152
|
query: str,
|
|
112
153
|
namespace: str,
|
|
113
|
-
category_filter:
|
|
154
|
+
category_filter: list[str] | None,
|
|
114
155
|
limit: int,
|
|
115
156
|
search_short_term: bool,
|
|
116
157
|
search_long_term: bool,
|
|
117
|
-
) ->
|
|
158
|
+
) -> list[dict[str, Any]]:
|
|
118
159
|
"""Search using SQLite FTS5"""
|
|
119
160
|
try:
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"SQLite FTS search starting for query: '{query}' in namespace: '{namespace}'"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Use parameters to validate search scope
|
|
166
|
+
if not search_short_term and not search_long_term:
|
|
167
|
+
logger.debug("No memory types specified for search, defaulting to both")
|
|
168
|
+
search_short_term = search_long_term = True
|
|
169
|
+
|
|
170
|
+
logger.debug(
|
|
171
|
+
f"Search scope - short_term: {search_short_term}, long_term: {search_long_term}"
|
|
172
|
+
)
|
|
173
|
+
|
|
120
174
|
# Build FTS query
|
|
121
175
|
fts_query = f'"{query.strip()}"'
|
|
176
|
+
logger.debug(f"FTS query built: {fts_query}")
|
|
122
177
|
|
|
123
178
|
# Build category filter
|
|
124
179
|
category_clause = ""
|
|
@@ -133,41 +188,69 @@ class SearchService:
|
|
|
133
188
|
)
|
|
134
189
|
for i, cat in enumerate(category_filter):
|
|
135
190
|
params[f"cat_{i}"] = cat
|
|
191
|
+
logger.debug(f"Category filter applied: {category_filter}")
|
|
136
192
|
|
|
137
|
-
# SQLite FTS5 search query
|
|
193
|
+
# SQLite FTS5 search query with COALESCE to handle NULL values
|
|
138
194
|
sql_query = f"""
|
|
139
195
|
SELECT
|
|
140
|
-
fts.memory_id,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
196
|
+
fts.memory_id,
|
|
197
|
+
fts.memory_type,
|
|
198
|
+
fts.category_primary,
|
|
199
|
+
COALESCE(
|
|
200
|
+
CASE
|
|
201
|
+
WHEN fts.memory_type = 'short_term' THEN st.processed_data
|
|
202
|
+
WHEN fts.memory_type = 'long_term' THEN lt.processed_data
|
|
203
|
+
END,
|
|
204
|
+
'{{}}'
|
|
205
|
+
) as processed_data,
|
|
206
|
+
COALESCE(
|
|
207
|
+
CASE
|
|
208
|
+
WHEN fts.memory_type = 'short_term' THEN st.importance_score
|
|
209
|
+
WHEN fts.memory_type = 'long_term' THEN lt.importance_score
|
|
210
|
+
ELSE 0.5
|
|
211
|
+
END,
|
|
212
|
+
0.5
|
|
213
|
+
) as importance_score,
|
|
214
|
+
COALESCE(
|
|
215
|
+
CASE
|
|
216
|
+
WHEN fts.memory_type = 'short_term' THEN st.created_at
|
|
217
|
+
WHEN fts.memory_type = 'long_term' THEN lt.created_at
|
|
218
|
+
END,
|
|
219
|
+
datetime('now')
|
|
220
|
+
) as created_at,
|
|
221
|
+
COALESCE(fts.summary, '') as summary,
|
|
222
|
+
COALESCE(rank, 0.0) as search_score,
|
|
156
223
|
'sqlite_fts5' as search_strategy
|
|
157
224
|
FROM memory_search_fts fts
|
|
158
225
|
LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
|
|
159
226
|
LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
|
|
160
227
|
WHERE memory_search_fts MATCH :fts_query AND fts.namespace = :namespace
|
|
161
228
|
{category_clause}
|
|
162
|
-
ORDER BY
|
|
229
|
+
ORDER BY search_score, importance_score DESC
|
|
163
230
|
LIMIT {limit}
|
|
164
231
|
"""
|
|
165
232
|
|
|
233
|
+
logger.debug(f"Executing SQLite FTS query with params: {params}")
|
|
166
234
|
result = self.session.execute(text(sql_query), params)
|
|
167
|
-
|
|
235
|
+
rows = [dict(row) for row in result]
|
|
236
|
+
logger.debug(f"SQLite FTS search returned {len(rows)} results")
|
|
237
|
+
|
|
238
|
+
# Log details of first result for debugging
|
|
239
|
+
if rows:
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"Sample result: memory_id={rows[0].get('memory_id')}, type={rows[0].get('memory_type')}, score={rows[0].get('search_score')}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return rows
|
|
168
245
|
|
|
169
246
|
except Exception as e:
|
|
170
|
-
logger.
|
|
247
|
+
logger.error(
|
|
248
|
+
f"SQLite FTS5 search failed for query '{query}' in namespace '{namespace}': {e}"
|
|
249
|
+
)
|
|
250
|
+
logger.debug(
|
|
251
|
+
f"SQLite FTS5 error details: {type(e).__name__}: {str(e)}",
|
|
252
|
+
exc_info=True,
|
|
253
|
+
)
|
|
171
254
|
# Roll back the transaction to recover from error state
|
|
172
255
|
self.session.rollback()
|
|
173
256
|
return []
|
|
@@ -176,15 +259,23 @@ class SearchService:
|
|
|
176
259
|
self,
|
|
177
260
|
query: str,
|
|
178
261
|
namespace: str,
|
|
179
|
-
category_filter:
|
|
262
|
+
category_filter: list[str] | None,
|
|
180
263
|
limit: int,
|
|
181
264
|
search_short_term: bool,
|
|
182
265
|
search_long_term: bool,
|
|
183
|
-
) ->
|
|
266
|
+
) -> list[dict[str, Any]]:
|
|
184
267
|
"""Search using MySQL FULLTEXT"""
|
|
185
268
|
results = []
|
|
186
269
|
|
|
187
270
|
try:
|
|
271
|
+
# Apply limit proportionally between memory types
|
|
272
|
+
short_limit = (
|
|
273
|
+
limit // 2 if search_short_term and search_long_term else limit
|
|
274
|
+
)
|
|
275
|
+
long_limit = (
|
|
276
|
+
limit - short_limit if search_short_term and search_long_term else limit
|
|
277
|
+
)
|
|
278
|
+
|
|
188
279
|
# Search short-term memory if requested
|
|
189
280
|
if search_short_term:
|
|
190
281
|
short_query = self.session.query(ShortTermMemory).filter(
|
|
@@ -203,7 +294,7 @@ class SearchService:
|
|
|
203
294
|
ShortTermMemory.category_primary.in_(category_filter)
|
|
204
295
|
)
|
|
205
296
|
|
|
206
|
-
# Add relevance score
|
|
297
|
+
# Add relevance score and limit
|
|
207
298
|
short_results = self.session.execute(
|
|
208
299
|
short_query.statement.add_columns(
|
|
209
300
|
text(
|
|
@@ -211,7 +302,7 @@ class SearchService:
|
|
|
211
302
|
).params(query=query),
|
|
212
303
|
text("'short_term' as memory_type"),
|
|
213
304
|
text("'mysql_fulltext' as search_strategy"),
|
|
214
|
-
)
|
|
305
|
+
).limit(short_limit)
|
|
215
306
|
).fetchall()
|
|
216
307
|
|
|
217
308
|
results.extend([dict(row) for row in short_results])
|
|
@@ -234,7 +325,7 @@ class SearchService:
|
|
|
234
325
|
LongTermMemory.category_primary.in_(category_filter)
|
|
235
326
|
)
|
|
236
327
|
|
|
237
|
-
# Add relevance score
|
|
328
|
+
# Add relevance score and limit
|
|
238
329
|
long_results = self.session.execute(
|
|
239
330
|
long_query.statement.add_columns(
|
|
240
331
|
text(
|
|
@@ -242,7 +333,7 @@ class SearchService:
|
|
|
242
333
|
).params(query=query),
|
|
243
334
|
text("'long_term' as memory_type"),
|
|
244
335
|
text("'mysql_fulltext' as search_strategy"),
|
|
245
|
-
)
|
|
336
|
+
).limit(long_limit)
|
|
246
337
|
).fetchall()
|
|
247
338
|
|
|
248
339
|
results.extend([dict(row) for row in long_results])
|
|
@@ -250,7 +341,13 @@ class SearchService:
|
|
|
250
341
|
return results
|
|
251
342
|
|
|
252
343
|
except Exception as e:
|
|
253
|
-
logger.
|
|
344
|
+
logger.error(
|
|
345
|
+
f"MySQL FULLTEXT search failed for query '{query}' in namespace '{namespace}': {e}"
|
|
346
|
+
)
|
|
347
|
+
logger.debug(
|
|
348
|
+
f"MySQL FULLTEXT error details: {type(e).__name__}: {str(e)}",
|
|
349
|
+
exc_info=True,
|
|
350
|
+
)
|
|
254
351
|
# Roll back the transaction to recover from error state
|
|
255
352
|
self.session.rollback()
|
|
256
353
|
return []
|
|
@@ -259,15 +356,23 @@ class SearchService:
|
|
|
259
356
|
self,
|
|
260
357
|
query: str,
|
|
261
358
|
namespace: str,
|
|
262
|
-
category_filter:
|
|
359
|
+
category_filter: list[str] | None,
|
|
263
360
|
limit: int,
|
|
264
361
|
search_short_term: bool,
|
|
265
362
|
search_long_term: bool,
|
|
266
|
-
) ->
|
|
363
|
+
) -> list[dict[str, Any]]:
|
|
267
364
|
"""Search using PostgreSQL tsvector"""
|
|
268
365
|
results = []
|
|
269
366
|
|
|
270
367
|
try:
|
|
368
|
+
# Apply limit proportionally between memory types
|
|
369
|
+
short_limit = (
|
|
370
|
+
limit // 2 if search_short_term and search_long_term else limit
|
|
371
|
+
)
|
|
372
|
+
long_limit = (
|
|
373
|
+
limit - short_limit if search_short_term and search_long_term else limit
|
|
374
|
+
)
|
|
375
|
+
|
|
271
376
|
# Prepare query for tsquery - handle spaces and special characters
|
|
272
377
|
# Convert simple query to tsquery format (join words with &)
|
|
273
378
|
tsquery_text = " & ".join(query.split())
|
|
@@ -290,7 +395,7 @@ class SearchService:
|
|
|
290
395
|
ShortTermMemory.category_primary.in_(category_filter)
|
|
291
396
|
)
|
|
292
397
|
|
|
293
|
-
# Add relevance score
|
|
398
|
+
# Add relevance score and limit
|
|
294
399
|
short_results = self.session.execute(
|
|
295
400
|
short_query.statement.add_columns(
|
|
296
401
|
text(
|
|
@@ -298,7 +403,9 @@ class SearchService:
|
|
|
298
403
|
).params(query=tsquery_text),
|
|
299
404
|
text("'short_term' as memory_type"),
|
|
300
405
|
text("'postgresql_fts' as search_strategy"),
|
|
301
|
-
)
|
|
406
|
+
)
|
|
407
|
+
.order_by(text("search_score DESC"))
|
|
408
|
+
.limit(short_limit)
|
|
302
409
|
).fetchall()
|
|
303
410
|
|
|
304
411
|
results.extend([dict(row) for row in short_results])
|
|
@@ -321,7 +428,7 @@ class SearchService:
|
|
|
321
428
|
LongTermMemory.category_primary.in_(category_filter)
|
|
322
429
|
)
|
|
323
430
|
|
|
324
|
-
# Add relevance score
|
|
431
|
+
# Add relevance score and limit
|
|
325
432
|
long_results = self.session.execute(
|
|
326
433
|
long_query.statement.add_columns(
|
|
327
434
|
text(
|
|
@@ -329,7 +436,9 @@ class SearchService:
|
|
|
329
436
|
).params(query=tsquery_text),
|
|
330
437
|
text("'long_term' as memory_type"),
|
|
331
438
|
text("'postgresql_fts' as search_strategy"),
|
|
332
|
-
)
|
|
439
|
+
)
|
|
440
|
+
.order_by(text("search_score DESC"))
|
|
441
|
+
.limit(long_limit)
|
|
333
442
|
).fetchall()
|
|
334
443
|
|
|
335
444
|
results.extend([dict(row) for row in long_results])
|
|
@@ -337,7 +446,13 @@ class SearchService:
|
|
|
337
446
|
return results
|
|
338
447
|
|
|
339
448
|
except Exception as e:
|
|
340
|
-
logger.
|
|
449
|
+
logger.error(
|
|
450
|
+
f"PostgreSQL FTS search failed for query '{query}' in namespace '{namespace}': {e}"
|
|
451
|
+
)
|
|
452
|
+
logger.debug(
|
|
453
|
+
f"PostgreSQL FTS error details: {type(e).__name__}: {str(e)}",
|
|
454
|
+
exc_info=True,
|
|
455
|
+
)
|
|
341
456
|
# Roll back the transaction to recover from error state
|
|
342
457
|
self.session.rollback()
|
|
343
458
|
return []
|
|
@@ -346,24 +461,47 @@ class SearchService:
|
|
|
346
461
|
self,
|
|
347
462
|
query: str,
|
|
348
463
|
namespace: str,
|
|
349
|
-
category_filter:
|
|
464
|
+
category_filter: list[str] | None,
|
|
350
465
|
limit: int,
|
|
351
466
|
search_short_term: bool,
|
|
352
467
|
search_long_term: bool,
|
|
353
|
-
) ->
|
|
354
|
-
"""Fallback LIKE-based search"""
|
|
468
|
+
) -> list[dict[str, Any]]:
|
|
469
|
+
"""Fallback LIKE-based search with improved flexibility"""
|
|
470
|
+
logger.debug(
|
|
471
|
+
f"Starting LIKE fallback search for query: '{query}' in namespace: '{namespace}'"
|
|
472
|
+
)
|
|
355
473
|
results = []
|
|
356
|
-
|
|
474
|
+
|
|
475
|
+
# Create multiple search patterns for better matching
|
|
476
|
+
search_patterns = [
|
|
477
|
+
f"%{query}%", # Original full query
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
# Add individual word patterns for better matching
|
|
481
|
+
words = query.strip().split()
|
|
482
|
+
if len(words) > 1:
|
|
483
|
+
for word in words:
|
|
484
|
+
if len(word) > 2: # Skip very short words
|
|
485
|
+
search_patterns.append(f"%{word}%")
|
|
486
|
+
|
|
487
|
+
logger.debug(f"LIKE search patterns: {search_patterns}")
|
|
357
488
|
|
|
358
489
|
# Search short-term memory
|
|
359
490
|
if search_short_term:
|
|
491
|
+
# Build OR conditions for all search patterns
|
|
492
|
+
search_conditions = []
|
|
493
|
+
for pattern in search_patterns:
|
|
494
|
+
search_conditions.extend(
|
|
495
|
+
[
|
|
496
|
+
ShortTermMemory.searchable_content.like(pattern),
|
|
497
|
+
ShortTermMemory.summary.like(pattern),
|
|
498
|
+
]
|
|
499
|
+
)
|
|
500
|
+
|
|
360
501
|
short_query = self.session.query(ShortTermMemory).filter(
|
|
361
502
|
and_(
|
|
362
503
|
ShortTermMemory.namespace == namespace,
|
|
363
|
-
or_(
|
|
364
|
-
ShortTermMemory.searchable_content.like(search_pattern),
|
|
365
|
-
ShortTermMemory.summary.like(search_pattern),
|
|
366
|
-
),
|
|
504
|
+
or_(*search_conditions),
|
|
367
505
|
)
|
|
368
506
|
)
|
|
369
507
|
|
|
@@ -381,6 +519,8 @@ class SearchService:
|
|
|
381
519
|
.all()
|
|
382
520
|
)
|
|
383
521
|
|
|
522
|
+
logger.debug(f"LIKE fallback found {len(short_results)} short-term results")
|
|
523
|
+
|
|
384
524
|
for result in short_results:
|
|
385
525
|
memory_dict = {
|
|
386
526
|
"memory_id": result.memory_id,
|
|
@@ -397,13 +537,20 @@ class SearchService:
|
|
|
397
537
|
|
|
398
538
|
# Search long-term memory
|
|
399
539
|
if search_long_term:
|
|
540
|
+
# Build OR conditions for all search patterns
|
|
541
|
+
search_conditions = []
|
|
542
|
+
for pattern in search_patterns:
|
|
543
|
+
search_conditions.extend(
|
|
544
|
+
[
|
|
545
|
+
LongTermMemory.searchable_content.like(pattern),
|
|
546
|
+
LongTermMemory.summary.like(pattern),
|
|
547
|
+
]
|
|
548
|
+
)
|
|
549
|
+
|
|
400
550
|
long_query = self.session.query(LongTermMemory).filter(
|
|
401
551
|
and_(
|
|
402
552
|
LongTermMemory.namespace == namespace,
|
|
403
|
-
or_(
|
|
404
|
-
LongTermMemory.searchable_content.like(search_pattern),
|
|
405
|
-
LongTermMemory.summary.like(search_pattern),
|
|
406
|
-
),
|
|
553
|
+
or_(*search_conditions),
|
|
407
554
|
)
|
|
408
555
|
)
|
|
409
556
|
|
|
@@ -421,6 +568,8 @@ class SearchService:
|
|
|
421
568
|
.all()
|
|
422
569
|
)
|
|
423
570
|
|
|
571
|
+
logger.debug(f"LIKE fallback found {len(long_results)} long-term results")
|
|
572
|
+
|
|
424
573
|
for result in long_results:
|
|
425
574
|
memory_dict = {
|
|
426
575
|
"memory_id": result.memory_id,
|
|
@@ -435,15 +584,18 @@ class SearchService:
|
|
|
435
584
|
}
|
|
436
585
|
results.append(memory_dict)
|
|
437
586
|
|
|
587
|
+
logger.debug(
|
|
588
|
+
f"LIKE fallback search completed, returning {len(results)} total results"
|
|
589
|
+
)
|
|
438
590
|
return results
|
|
439
591
|
|
|
440
592
|
def _get_recent_memories(
|
|
441
593
|
self,
|
|
442
594
|
namespace: str,
|
|
443
|
-
category_filter:
|
|
595
|
+
category_filter: list[str] | None,
|
|
444
596
|
limit: int,
|
|
445
|
-
memory_types:
|
|
446
|
-
) ->
|
|
597
|
+
memory_types: list[str] | None,
|
|
598
|
+
) -> list[dict[str, Any]]:
|
|
447
599
|
"""Get recent memories when no search query is provided"""
|
|
448
600
|
results = []
|
|
449
601
|
|
|
@@ -515,8 +667,8 @@ class SearchService:
|
|
|
515
667
|
return results
|
|
516
668
|
|
|
517
669
|
def _rank_and_limit_results(
|
|
518
|
-
self, results:
|
|
519
|
-
) ->
|
|
670
|
+
self, results: list[dict[str, Any]], limit: int
|
|
671
|
+
) -> list[dict[str, Any]]:
|
|
520
672
|
"""Rank and limit search results"""
|
|
521
673
|
# Calculate composite score
|
|
522
674
|
for result in results:
|