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,548 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLAlchemy-based search service for Memori v2.0
|
|
3
|
+
Provides cross-database full-text search capabilities
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from sqlalchemy import and_, desc, or_, text
|
|
11
|
+
from sqlalchemy.orm import Session
|
|
12
|
+
|
|
13
|
+
from .models import LongTermMemory, ShortTermMemory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SearchService:
|
|
17
|
+
"""Cross-database search service using SQLAlchemy"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, session: Session, database_type: str):
|
|
20
|
+
self.session = session
|
|
21
|
+
self.database_type = database_type
|
|
22
|
+
|
|
23
|
+
def search_memories(
|
|
24
|
+
self,
|
|
25
|
+
query: str,
|
|
26
|
+
namespace: str = "default",
|
|
27
|
+
category_filter: Optional[List[str]] = None,
|
|
28
|
+
limit: int = 10,
|
|
29
|
+
memory_types: Optional[List[str]] = None,
|
|
30
|
+
) -> List[Dict[str, Any]]:
|
|
31
|
+
"""
|
|
32
|
+
Search memories across different database backends
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query: Search query string
|
|
36
|
+
namespace: Memory namespace
|
|
37
|
+
category_filter: List of categories to filter by
|
|
38
|
+
limit: Maximum number of results
|
|
39
|
+
memory_types: Types of memory to search ('short_term', 'long_term', or both)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of memory dictionaries with search metadata
|
|
43
|
+
"""
|
|
44
|
+
if not query or not query.strip():
|
|
45
|
+
return self._get_recent_memories(
|
|
46
|
+
namespace, category_filter, limit, memory_types
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
results = []
|
|
50
|
+
|
|
51
|
+
# Determine which memory types to search
|
|
52
|
+
search_short_term = not memory_types or "short_term" in memory_types
|
|
53
|
+
search_long_term = not memory_types or "long_term" in memory_types
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Try database-specific full-text search first
|
|
57
|
+
if self.database_type == "sqlite":
|
|
58
|
+
results = self._search_sqlite_fts(
|
|
59
|
+
query,
|
|
60
|
+
namespace,
|
|
61
|
+
category_filter,
|
|
62
|
+
limit,
|
|
63
|
+
search_short_term,
|
|
64
|
+
search_long_term,
|
|
65
|
+
)
|
|
66
|
+
elif self.database_type == "mysql":
|
|
67
|
+
results = self._search_mysql_fulltext(
|
|
68
|
+
query,
|
|
69
|
+
namespace,
|
|
70
|
+
category_filter,
|
|
71
|
+
limit,
|
|
72
|
+
search_short_term,
|
|
73
|
+
search_long_term,
|
|
74
|
+
)
|
|
75
|
+
elif self.database_type == "postgresql":
|
|
76
|
+
results = self._search_postgresql_fts(
|
|
77
|
+
query,
|
|
78
|
+
namespace,
|
|
79
|
+
category_filter,
|
|
80
|
+
limit,
|
|
81
|
+
search_short_term,
|
|
82
|
+
search_long_term,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# If no results or full-text search failed, fall back to LIKE search
|
|
86
|
+
if not results:
|
|
87
|
+
results = self._search_like_fallback(
|
|
88
|
+
query,
|
|
89
|
+
namespace,
|
|
90
|
+
category_filter,
|
|
91
|
+
limit,
|
|
92
|
+
search_short_term,
|
|
93
|
+
search_long_term,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning(f"Full-text search failed: {e}, falling back to LIKE search")
|
|
98
|
+
results = self._search_like_fallback(
|
|
99
|
+
query,
|
|
100
|
+
namespace,
|
|
101
|
+
category_filter,
|
|
102
|
+
limit,
|
|
103
|
+
search_short_term,
|
|
104
|
+
search_long_term,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return self._rank_and_limit_results(results, limit)
|
|
108
|
+
|
|
109
|
+
def _search_sqlite_fts(
|
|
110
|
+
self,
|
|
111
|
+
query: str,
|
|
112
|
+
namespace: str,
|
|
113
|
+
category_filter: Optional[List[str]],
|
|
114
|
+
limit: int,
|
|
115
|
+
search_short_term: bool,
|
|
116
|
+
search_long_term: bool,
|
|
117
|
+
) -> List[Dict[str, Any]]:
|
|
118
|
+
"""Search using SQLite FTS5"""
|
|
119
|
+
try:
|
|
120
|
+
# Build FTS query
|
|
121
|
+
fts_query = f'"{query.strip()}"'
|
|
122
|
+
|
|
123
|
+
# Build category filter
|
|
124
|
+
category_clause = ""
|
|
125
|
+
params = {"fts_query": fts_query, "namespace": namespace}
|
|
126
|
+
|
|
127
|
+
if category_filter:
|
|
128
|
+
category_placeholders = ",".join(
|
|
129
|
+
[f":cat_{i}" for i in range(len(category_filter))]
|
|
130
|
+
)
|
|
131
|
+
category_clause = (
|
|
132
|
+
f"AND fts.category_primary IN ({category_placeholders})"
|
|
133
|
+
)
|
|
134
|
+
for i, cat in enumerate(category_filter):
|
|
135
|
+
params[f"cat_{i}"] = cat
|
|
136
|
+
|
|
137
|
+
# SQLite FTS5 search query
|
|
138
|
+
sql_query = f"""
|
|
139
|
+
SELECT
|
|
140
|
+
fts.memory_id, fts.memory_type, fts.category_primary,
|
|
141
|
+
CASE
|
|
142
|
+
WHEN fts.memory_type = 'short_term' THEN st.processed_data
|
|
143
|
+
WHEN fts.memory_type = 'long_term' THEN lt.processed_data
|
|
144
|
+
END as processed_data,
|
|
145
|
+
CASE
|
|
146
|
+
WHEN fts.memory_type = 'short_term' THEN st.importance_score
|
|
147
|
+
WHEN fts.memory_type = 'long_term' THEN lt.importance_score
|
|
148
|
+
ELSE 0.5
|
|
149
|
+
END as importance_score,
|
|
150
|
+
CASE
|
|
151
|
+
WHEN fts.memory_type = 'short_term' THEN st.created_at
|
|
152
|
+
WHEN fts.memory_type = 'long_term' THEN lt.created_at
|
|
153
|
+
END as created_at,
|
|
154
|
+
fts.summary,
|
|
155
|
+
rank as search_score,
|
|
156
|
+
'sqlite_fts5' as search_strategy
|
|
157
|
+
FROM memory_search_fts fts
|
|
158
|
+
LEFT JOIN short_term_memory st ON fts.memory_id = st.memory_id AND fts.memory_type = 'short_term'
|
|
159
|
+
LEFT JOIN long_term_memory lt ON fts.memory_id = lt.memory_id AND fts.memory_type = 'long_term'
|
|
160
|
+
WHERE memory_search_fts MATCH :fts_query AND fts.namespace = :namespace
|
|
161
|
+
{category_clause}
|
|
162
|
+
ORDER BY rank, importance_score DESC
|
|
163
|
+
LIMIT {limit}
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
result = self.session.execute(text(sql_query), params)
|
|
167
|
+
return [dict(row) for row in result]
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.debug(f"SQLite FTS5 search failed: {e}")
|
|
171
|
+
# Roll back the transaction to recover from error state
|
|
172
|
+
self.session.rollback()
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
def _search_mysql_fulltext(
|
|
176
|
+
self,
|
|
177
|
+
query: str,
|
|
178
|
+
namespace: str,
|
|
179
|
+
category_filter: Optional[List[str]],
|
|
180
|
+
limit: int,
|
|
181
|
+
search_short_term: bool,
|
|
182
|
+
search_long_term: bool,
|
|
183
|
+
) -> List[Dict[str, Any]]:
|
|
184
|
+
"""Search using MySQL FULLTEXT"""
|
|
185
|
+
results = []
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
# Search short-term memory if requested
|
|
189
|
+
if search_short_term:
|
|
190
|
+
short_query = self.session.query(ShortTermMemory).filter(
|
|
191
|
+
ShortTermMemory.namespace == namespace
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Add FULLTEXT search
|
|
195
|
+
fulltext_condition = text(
|
|
196
|
+
"MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE)"
|
|
197
|
+
).params(query=query)
|
|
198
|
+
short_query = short_query.filter(fulltext_condition)
|
|
199
|
+
|
|
200
|
+
# Add category filter
|
|
201
|
+
if category_filter:
|
|
202
|
+
short_query = short_query.filter(
|
|
203
|
+
ShortTermMemory.category_primary.in_(category_filter)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Add relevance score
|
|
207
|
+
short_results = self.session.execute(
|
|
208
|
+
short_query.statement.add_columns(
|
|
209
|
+
text(
|
|
210
|
+
"MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE) as search_score"
|
|
211
|
+
).params(query=query),
|
|
212
|
+
text("'short_term' as memory_type"),
|
|
213
|
+
text("'mysql_fulltext' as search_strategy"),
|
|
214
|
+
)
|
|
215
|
+
).fetchall()
|
|
216
|
+
|
|
217
|
+
results.extend([dict(row) for row in short_results])
|
|
218
|
+
|
|
219
|
+
# Search long-term memory if requested
|
|
220
|
+
if search_long_term:
|
|
221
|
+
long_query = self.session.query(LongTermMemory).filter(
|
|
222
|
+
LongTermMemory.namespace == namespace
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Add FULLTEXT search
|
|
226
|
+
fulltext_condition = text(
|
|
227
|
+
"MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE)"
|
|
228
|
+
).params(query=query)
|
|
229
|
+
long_query = long_query.filter(fulltext_condition)
|
|
230
|
+
|
|
231
|
+
# Add category filter
|
|
232
|
+
if category_filter:
|
|
233
|
+
long_query = long_query.filter(
|
|
234
|
+
LongTermMemory.category_primary.in_(category_filter)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Add relevance score
|
|
238
|
+
long_results = self.session.execute(
|
|
239
|
+
long_query.statement.add_columns(
|
|
240
|
+
text(
|
|
241
|
+
"MATCH(searchable_content, summary) AGAINST(:query IN NATURAL LANGUAGE MODE) as search_score"
|
|
242
|
+
).params(query=query),
|
|
243
|
+
text("'long_term' as memory_type"),
|
|
244
|
+
text("'mysql_fulltext' as search_strategy"),
|
|
245
|
+
)
|
|
246
|
+
).fetchall()
|
|
247
|
+
|
|
248
|
+
results.extend([dict(row) for row in long_results])
|
|
249
|
+
|
|
250
|
+
return results
|
|
251
|
+
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.debug(f"MySQL FULLTEXT search failed: {e}")
|
|
254
|
+
# Roll back the transaction to recover from error state
|
|
255
|
+
self.session.rollback()
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
def _search_postgresql_fts(
|
|
259
|
+
self,
|
|
260
|
+
query: str,
|
|
261
|
+
namespace: str,
|
|
262
|
+
category_filter: Optional[List[str]],
|
|
263
|
+
limit: int,
|
|
264
|
+
search_short_term: bool,
|
|
265
|
+
search_long_term: bool,
|
|
266
|
+
) -> List[Dict[str, Any]]:
|
|
267
|
+
"""Search using PostgreSQL tsvector"""
|
|
268
|
+
results = []
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
# Prepare query for tsquery - handle spaces and special characters
|
|
272
|
+
# Convert simple query to tsquery format (join words with &)
|
|
273
|
+
tsquery_text = " & ".join(query.split())
|
|
274
|
+
|
|
275
|
+
# Search short-term memory if requested
|
|
276
|
+
if search_short_term:
|
|
277
|
+
short_query = self.session.query(ShortTermMemory).filter(
|
|
278
|
+
ShortTermMemory.namespace == namespace
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Add tsvector search
|
|
282
|
+
ts_query = text(
|
|
283
|
+
"search_vector @@ to_tsquery('english', :query)"
|
|
284
|
+
).params(query=tsquery_text)
|
|
285
|
+
short_query = short_query.filter(ts_query)
|
|
286
|
+
|
|
287
|
+
# Add category filter
|
|
288
|
+
if category_filter:
|
|
289
|
+
short_query = short_query.filter(
|
|
290
|
+
ShortTermMemory.category_primary.in_(category_filter)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Add relevance score
|
|
294
|
+
short_results = self.session.execute(
|
|
295
|
+
short_query.statement.add_columns(
|
|
296
|
+
text(
|
|
297
|
+
"ts_rank(search_vector, to_tsquery('english', :query)) as search_score"
|
|
298
|
+
).params(query=tsquery_text),
|
|
299
|
+
text("'short_term' as memory_type"),
|
|
300
|
+
text("'postgresql_fts' as search_strategy"),
|
|
301
|
+
).order_by(text("search_score DESC"))
|
|
302
|
+
).fetchall()
|
|
303
|
+
|
|
304
|
+
results.extend([dict(row) for row in short_results])
|
|
305
|
+
|
|
306
|
+
# Search long-term memory if requested
|
|
307
|
+
if search_long_term:
|
|
308
|
+
long_query = self.session.query(LongTermMemory).filter(
|
|
309
|
+
LongTermMemory.namespace == namespace
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Add tsvector search
|
|
313
|
+
ts_query = text(
|
|
314
|
+
"search_vector @@ to_tsquery('english', :query)"
|
|
315
|
+
).params(query=tsquery_text)
|
|
316
|
+
long_query = long_query.filter(ts_query)
|
|
317
|
+
|
|
318
|
+
# Add category filter
|
|
319
|
+
if category_filter:
|
|
320
|
+
long_query = long_query.filter(
|
|
321
|
+
LongTermMemory.category_primary.in_(category_filter)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Add relevance score
|
|
325
|
+
long_results = self.session.execute(
|
|
326
|
+
long_query.statement.add_columns(
|
|
327
|
+
text(
|
|
328
|
+
"ts_rank(search_vector, to_tsquery('english', :query)) as search_score"
|
|
329
|
+
).params(query=tsquery_text),
|
|
330
|
+
text("'long_term' as memory_type"),
|
|
331
|
+
text("'postgresql_fts' as search_strategy"),
|
|
332
|
+
).order_by(text("search_score DESC"))
|
|
333
|
+
).fetchall()
|
|
334
|
+
|
|
335
|
+
results.extend([dict(row) for row in long_results])
|
|
336
|
+
|
|
337
|
+
return results
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.debug(f"PostgreSQL FTS search failed: {e}")
|
|
341
|
+
# Roll back the transaction to recover from error state
|
|
342
|
+
self.session.rollback()
|
|
343
|
+
return []
|
|
344
|
+
|
|
345
|
+
def _search_like_fallback(
|
|
346
|
+
self,
|
|
347
|
+
query: str,
|
|
348
|
+
namespace: str,
|
|
349
|
+
category_filter: Optional[List[str]],
|
|
350
|
+
limit: int,
|
|
351
|
+
search_short_term: bool,
|
|
352
|
+
search_long_term: bool,
|
|
353
|
+
) -> List[Dict[str, Any]]:
|
|
354
|
+
"""Fallback LIKE-based search"""
|
|
355
|
+
results = []
|
|
356
|
+
search_pattern = f"%{query}%"
|
|
357
|
+
|
|
358
|
+
# Search short-term memory
|
|
359
|
+
if search_short_term:
|
|
360
|
+
short_query = self.session.query(ShortTermMemory).filter(
|
|
361
|
+
and_(
|
|
362
|
+
ShortTermMemory.namespace == namespace,
|
|
363
|
+
or_(
|
|
364
|
+
ShortTermMemory.searchable_content.like(search_pattern),
|
|
365
|
+
ShortTermMemory.summary.like(search_pattern),
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if category_filter:
|
|
371
|
+
short_query = short_query.filter(
|
|
372
|
+
ShortTermMemory.category_primary.in_(category_filter)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
short_results = (
|
|
376
|
+
short_query.order_by(
|
|
377
|
+
desc(ShortTermMemory.importance_score),
|
|
378
|
+
desc(ShortTermMemory.created_at),
|
|
379
|
+
)
|
|
380
|
+
.limit(limit)
|
|
381
|
+
.all()
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
for result in short_results:
|
|
385
|
+
memory_dict = {
|
|
386
|
+
"memory_id": result.memory_id,
|
|
387
|
+
"memory_type": "short_term",
|
|
388
|
+
"processed_data": result.processed_data,
|
|
389
|
+
"importance_score": result.importance_score,
|
|
390
|
+
"created_at": result.created_at,
|
|
391
|
+
"summary": result.summary,
|
|
392
|
+
"category_primary": result.category_primary,
|
|
393
|
+
"search_score": 0.4, # Fixed score for LIKE search
|
|
394
|
+
"search_strategy": f"{self.database_type}_like_fallback",
|
|
395
|
+
}
|
|
396
|
+
results.append(memory_dict)
|
|
397
|
+
|
|
398
|
+
# Search long-term memory
|
|
399
|
+
if search_long_term:
|
|
400
|
+
long_query = self.session.query(LongTermMemory).filter(
|
|
401
|
+
and_(
|
|
402
|
+
LongTermMemory.namespace == namespace,
|
|
403
|
+
or_(
|
|
404
|
+
LongTermMemory.searchable_content.like(search_pattern),
|
|
405
|
+
LongTermMemory.summary.like(search_pattern),
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if category_filter:
|
|
411
|
+
long_query = long_query.filter(
|
|
412
|
+
LongTermMemory.category_primary.in_(category_filter)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
long_results = (
|
|
416
|
+
long_query.order_by(
|
|
417
|
+
desc(LongTermMemory.importance_score),
|
|
418
|
+
desc(LongTermMemory.created_at),
|
|
419
|
+
)
|
|
420
|
+
.limit(limit)
|
|
421
|
+
.all()
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
for result in long_results:
|
|
425
|
+
memory_dict = {
|
|
426
|
+
"memory_id": result.memory_id,
|
|
427
|
+
"memory_type": "long_term",
|
|
428
|
+
"processed_data": result.processed_data,
|
|
429
|
+
"importance_score": result.importance_score,
|
|
430
|
+
"created_at": result.created_at,
|
|
431
|
+
"summary": result.summary,
|
|
432
|
+
"category_primary": result.category_primary,
|
|
433
|
+
"search_score": 0.4, # Fixed score for LIKE search
|
|
434
|
+
"search_strategy": f"{self.database_type}_like_fallback",
|
|
435
|
+
}
|
|
436
|
+
results.append(memory_dict)
|
|
437
|
+
|
|
438
|
+
return results
|
|
439
|
+
|
|
440
|
+
def _get_recent_memories(
|
|
441
|
+
self,
|
|
442
|
+
namespace: str,
|
|
443
|
+
category_filter: Optional[List[str]],
|
|
444
|
+
limit: int,
|
|
445
|
+
memory_types: Optional[List[str]],
|
|
446
|
+
) -> List[Dict[str, Any]]:
|
|
447
|
+
"""Get recent memories when no search query is provided"""
|
|
448
|
+
results = []
|
|
449
|
+
|
|
450
|
+
search_short_term = not memory_types or "short_term" in memory_types
|
|
451
|
+
search_long_term = not memory_types or "long_term" in memory_types
|
|
452
|
+
|
|
453
|
+
# Get recent short-term memories
|
|
454
|
+
if search_short_term:
|
|
455
|
+
short_query = self.session.query(ShortTermMemory).filter(
|
|
456
|
+
ShortTermMemory.namespace == namespace
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if category_filter:
|
|
460
|
+
short_query = short_query.filter(
|
|
461
|
+
ShortTermMemory.category_primary.in_(category_filter)
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
short_results = (
|
|
465
|
+
short_query.order_by(desc(ShortTermMemory.created_at))
|
|
466
|
+
.limit(limit // 2)
|
|
467
|
+
.all()
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
for result in short_results:
|
|
471
|
+
memory_dict = {
|
|
472
|
+
"memory_id": result.memory_id,
|
|
473
|
+
"memory_type": "short_term",
|
|
474
|
+
"processed_data": result.processed_data,
|
|
475
|
+
"importance_score": result.importance_score,
|
|
476
|
+
"created_at": result.created_at,
|
|
477
|
+
"summary": result.summary,
|
|
478
|
+
"category_primary": result.category_primary,
|
|
479
|
+
"search_score": 1.0,
|
|
480
|
+
"search_strategy": "recent_memories",
|
|
481
|
+
}
|
|
482
|
+
results.append(memory_dict)
|
|
483
|
+
|
|
484
|
+
# Get recent long-term memories
|
|
485
|
+
if search_long_term:
|
|
486
|
+
long_query = self.session.query(LongTermMemory).filter(
|
|
487
|
+
LongTermMemory.namespace == namespace
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if category_filter:
|
|
491
|
+
long_query = long_query.filter(
|
|
492
|
+
LongTermMemory.category_primary.in_(category_filter)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
long_results = (
|
|
496
|
+
long_query.order_by(desc(LongTermMemory.created_at))
|
|
497
|
+
.limit(limit // 2)
|
|
498
|
+
.all()
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
for result in long_results:
|
|
502
|
+
memory_dict = {
|
|
503
|
+
"memory_id": result.memory_id,
|
|
504
|
+
"memory_type": "long_term",
|
|
505
|
+
"processed_data": result.processed_data,
|
|
506
|
+
"importance_score": result.importance_score,
|
|
507
|
+
"created_at": result.created_at,
|
|
508
|
+
"summary": result.summary,
|
|
509
|
+
"category_primary": result.category_primary,
|
|
510
|
+
"search_score": 1.0,
|
|
511
|
+
"search_strategy": "recent_memories",
|
|
512
|
+
}
|
|
513
|
+
results.append(memory_dict)
|
|
514
|
+
|
|
515
|
+
return results
|
|
516
|
+
|
|
517
|
+
def _rank_and_limit_results(
|
|
518
|
+
self, results: List[Dict[str, Any]], limit: int
|
|
519
|
+
) -> List[Dict[str, Any]]:
|
|
520
|
+
"""Rank and limit search results"""
|
|
521
|
+
# Calculate composite score
|
|
522
|
+
for result in results:
|
|
523
|
+
search_score = result.get("search_score", 0.4)
|
|
524
|
+
importance_score = result.get("importance_score", 0.5)
|
|
525
|
+
recency_score = self._calculate_recency_score(result.get("created_at"))
|
|
526
|
+
|
|
527
|
+
# Weighted composite score
|
|
528
|
+
result["composite_score"] = (
|
|
529
|
+
search_score * 0.5 + importance_score * 0.3 + recency_score * 0.2
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Sort by composite score and limit
|
|
533
|
+
results.sort(key=lambda x: x.get("composite_score", 0), reverse=True)
|
|
534
|
+
return results[:limit]
|
|
535
|
+
|
|
536
|
+
def _calculate_recency_score(self, created_at) -> float:
|
|
537
|
+
"""Calculate recency score (0-1, newer = higher)"""
|
|
538
|
+
try:
|
|
539
|
+
if not created_at:
|
|
540
|
+
return 0.0
|
|
541
|
+
|
|
542
|
+
if isinstance(created_at, str):
|
|
543
|
+
created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
544
|
+
|
|
545
|
+
days_old = (datetime.now() - created_at).days
|
|
546
|
+
return max(0, 1 - (days_old / 30)) # Full score for recent, 0 after 30 days
|
|
547
|
+
except:
|
|
548
|
+
return 0.0
|