memorisdk 2.0.0__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 +1 -1
- memori/agents/retrieval_agent.py +79 -12
- memori/core/memory.py +61 -17
- memori/database/search_service.py +201 -49
- memori/database/sqlalchemy_manager.py +62 -13
- memori/tools/memory_tool.py +94 -4
- {memorisdk-2.0.0.dist-info → memorisdk-2.0.1.dist-info}/METADATA +15 -9
- {memorisdk-2.0.0.dist-info → memorisdk-2.0.1.dist-info}/RECORD +11 -12
- memori/scripts/llm_text.py +0 -50
- {memorisdk-2.0.0.dist-info → memorisdk-2.0.1.dist-info}/WHEEL +0 -0
- {memorisdk-2.0.0.dist-info → memorisdk-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {memorisdk-2.0.0.dist-info → memorisdk-2.0.1.dist-info}/top_level.txt +0 -0
memori/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ Professional-grade memory layer with comprehensive error handling, configuration
|
|
|
5
5
|
management, and modular architecture for production AI systems.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "2.0.
|
|
8
|
+
__version__ = "2.0.1"
|
|
9
9
|
__author__ = "Harshal More"
|
|
10
10
|
__email__ = "harshalmore2468@gmail.com"
|
|
11
11
|
|
memori/agents/retrieval_agent.py
CHANGED
|
@@ -411,32 +411,96 @@ Be strategic and comprehensive in your search planning."""
|
|
|
411
411
|
|
|
412
412
|
# This would need to be implemented in the database manager
|
|
413
413
|
# For now, get all memories and filter by category
|
|
414
|
+
logger.debug(
|
|
415
|
+
f"Searching memories by categories: {categories} in namespace: {namespace}"
|
|
416
|
+
)
|
|
414
417
|
all_results = db_manager.search_memories(
|
|
415
418
|
query="", namespace=namespace, limit=limit * 3
|
|
416
419
|
)
|
|
417
420
|
|
|
421
|
+
logger.debug(
|
|
422
|
+
f"Retrieved {len(all_results)} total results for category filtering"
|
|
423
|
+
)
|
|
424
|
+
|
|
418
425
|
filtered_results = []
|
|
419
|
-
for result in all_results:
|
|
426
|
+
for i, result in enumerate(all_results):
|
|
427
|
+
logger.debug(f"Processing result {i+1}/{len(all_results)}: {type(result)}")
|
|
428
|
+
|
|
420
429
|
# Extract category from processed_data if it's stored as JSON
|
|
421
430
|
try:
|
|
422
|
-
|
|
431
|
+
memory_category = None
|
|
432
|
+
|
|
433
|
+
# Check processed_data field first
|
|
434
|
+
if "processed_data" in result and result["processed_data"]:
|
|
423
435
|
processed_data = result["processed_data"]
|
|
436
|
+
logger.debug(
|
|
437
|
+
f"Found processed_data: {type(processed_data)} - {str(processed_data)[:100]}..."
|
|
438
|
+
)
|
|
439
|
+
|
|
424
440
|
# Handle both dict and JSON string formats
|
|
425
441
|
if isinstance(processed_data, str):
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
442
|
+
try:
|
|
443
|
+
processed_data = json.loads(processed_data)
|
|
444
|
+
except json.JSONDecodeError as je:
|
|
445
|
+
logger.debug(f"JSON decode error for processed_data: {je}")
|
|
446
|
+
continue
|
|
447
|
+
|
|
448
|
+
if isinstance(processed_data, dict):
|
|
449
|
+
# Try multiple possible category locations
|
|
450
|
+
category_paths = [
|
|
451
|
+
["category", "primary_category"],
|
|
452
|
+
["category"],
|
|
453
|
+
["primary_category"],
|
|
454
|
+
["metadata", "category"],
|
|
455
|
+
["classification", "category"],
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
for path in category_paths:
|
|
459
|
+
temp_data = processed_data
|
|
460
|
+
try:
|
|
461
|
+
for key in path:
|
|
462
|
+
temp_data = temp_data.get(key, {})
|
|
463
|
+
if isinstance(temp_data, str) and temp_data:
|
|
464
|
+
memory_category = temp_data
|
|
465
|
+
logger.debug(
|
|
466
|
+
f"Found category via path {path}: {memory_category}"
|
|
467
|
+
)
|
|
468
|
+
break
|
|
469
|
+
except (AttributeError, TypeError):
|
|
470
|
+
continue
|
|
471
|
+
else:
|
|
472
|
+
logger.debug(
|
|
473
|
+
f"processed_data is not a dict after parsing: {type(processed_data)}"
|
|
474
|
+
)
|
|
475
|
+
continue
|
|
429
476
|
|
|
430
|
-
|
|
431
|
-
|
|
477
|
+
# Fallback: check direct category field
|
|
478
|
+
if not memory_category and "category" in result and result["category"]:
|
|
479
|
+
memory_category = result["category"]
|
|
480
|
+
logger.debug(f"Found category via direct field: {memory_category}")
|
|
481
|
+
|
|
482
|
+
# Check if the found category matches any of our target categories
|
|
483
|
+
if memory_category:
|
|
484
|
+
logger.debug(
|
|
485
|
+
f"Comparing memory category '{memory_category}' against target categories {categories}"
|
|
432
486
|
)
|
|
433
487
|
if memory_category in categories:
|
|
434
488
|
filtered_results.append(result)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
489
|
+
logger.debug(f"✓ Category match found: {memory_category}")
|
|
490
|
+
else:
|
|
491
|
+
logger.debug(
|
|
492
|
+
f"✗ Category mismatch: {memory_category} not in {categories}"
|
|
493
|
+
)
|
|
494
|
+
else:
|
|
495
|
+
logger.debug("No category found in result")
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.debug(f"Error processing result {i+1}: {e}")
|
|
438
499
|
continue
|
|
439
500
|
|
|
501
|
+
logger.debug(
|
|
502
|
+
f"Category filtering complete: {len(filtered_results)} results match categories {categories}"
|
|
503
|
+
)
|
|
440
504
|
return filtered_results[:limit]
|
|
441
505
|
|
|
442
506
|
def _detect_structured_output_support(self) -> bool:
|
|
@@ -821,8 +885,11 @@ Be strategic and comprehensive in your search planning."""
|
|
|
821
885
|
"""
|
|
822
886
|
# This is a compatibility method that uses the database manager directly
|
|
823
887
|
# We'll need the database manager to be injected or passed
|
|
824
|
-
# For now, return empty list and log the issue
|
|
825
|
-
logger.warning(
|
|
888
|
+
# For now, return empty list and log the issue with parameters
|
|
889
|
+
logger.warning(
|
|
890
|
+
f"search_memories called without database manager: query='{query}', "
|
|
891
|
+
f"max_results={max_results}, namespace='{namespace}'"
|
|
892
|
+
)
|
|
826
893
|
return []
|
|
827
894
|
|
|
828
895
|
|
memori/core/memory.py
CHANGED
|
@@ -1036,15 +1036,38 @@ class Memori:
|
|
|
1036
1036
|
self._in_context_retrieval = True
|
|
1037
1037
|
|
|
1038
1038
|
logger.debug(
|
|
1039
|
-
f"Auto-ingest: Starting context retrieval for query: '{user_input[:50]}...'"
|
|
1039
|
+
f"Auto-ingest: Starting context retrieval for query: '{user_input[:50]}...' in namespace: '{self.namespace}'"
|
|
1040
1040
|
)
|
|
1041
1041
|
|
|
1042
1042
|
# Always try direct database search first as it's more reliable
|
|
1043
1043
|
logger.debug("Auto-ingest: Using direct database search (primary method)")
|
|
1044
|
-
|
|
1045
|
-
|
|
1044
|
+
logger.debug(
|
|
1045
|
+
f"Auto-ingest: Database manager type: {type(self.db_manager).__name__}"
|
|
1046
1046
|
)
|
|
1047
1047
|
|
|
1048
|
+
try:
|
|
1049
|
+
results = self.db_manager.search_memories(
|
|
1050
|
+
query=user_input, namespace=self.namespace, limit=5
|
|
1051
|
+
)
|
|
1052
|
+
logger.debug(
|
|
1053
|
+
f"Auto-ingest: Database search returned {len(results) if results else 0} results"
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
if results:
|
|
1057
|
+
for i, result in enumerate(
|
|
1058
|
+
results[:3]
|
|
1059
|
+
): # Log first 3 results for debugging
|
|
1060
|
+
logger.debug(
|
|
1061
|
+
f"Auto-ingest: Result {i+1}: {type(result)} with keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}"
|
|
1062
|
+
)
|
|
1063
|
+
except Exception as db_search_e:
|
|
1064
|
+
logger.error(f"Auto-ingest: Database search failed: {db_search_e}")
|
|
1065
|
+
logger.debug(
|
|
1066
|
+
f"Auto-ingest: Database search error details: {type(db_search_e).__name__}: {str(db_search_e)}",
|
|
1067
|
+
exc_info=True,
|
|
1068
|
+
)
|
|
1069
|
+
results = []
|
|
1070
|
+
|
|
1048
1071
|
if results:
|
|
1049
1072
|
logger.debug(
|
|
1050
1073
|
f"Auto-ingest: Direct database search returned {len(results)} results"
|
|
@@ -1085,8 +1108,12 @@ class Memori:
|
|
|
1085
1108
|
)
|
|
1086
1109
|
|
|
1087
1110
|
except Exception as search_error:
|
|
1088
|
-
logger.
|
|
1089
|
-
f"Auto-ingest: Search engine failed
|
|
1111
|
+
logger.error(
|
|
1112
|
+
f"Auto-ingest: Search engine failed for query '{user_input[:50]}...': {search_error}"
|
|
1113
|
+
)
|
|
1114
|
+
logger.debug(
|
|
1115
|
+
f"Auto-ingest: Search engine error details: {type(search_error).__name__}: {str(search_error)}",
|
|
1116
|
+
exc_info=True,
|
|
1090
1117
|
)
|
|
1091
1118
|
else:
|
|
1092
1119
|
logger.debug("Auto-ingest: No search engine available")
|
|
@@ -1095,22 +1122,39 @@ class Memori:
|
|
|
1095
1122
|
logger.debug(
|
|
1096
1123
|
"Auto-ingest: All search methods returned 0 results, using recent memories fallback"
|
|
1097
1124
|
)
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
namespace=self.namespace,
|
|
1101
|
-
limit=3,
|
|
1125
|
+
logger.debug(
|
|
1126
|
+
f"Auto-ingest: Attempting fallback search in namespace '{self.namespace}'"
|
|
1102
1127
|
)
|
|
1103
1128
|
|
|
1104
|
-
|
|
1129
|
+
try:
|
|
1130
|
+
fallback_results = self.db_manager.search_memories(
|
|
1131
|
+
query="", # Empty query to get recent memories
|
|
1132
|
+
namespace=self.namespace,
|
|
1133
|
+
limit=3,
|
|
1134
|
+
)
|
|
1105
1135
|
logger.debug(
|
|
1106
|
-
f"Auto-ingest: Fallback returned {len(fallback_results)}
|
|
1136
|
+
f"Auto-ingest: Fallback search returned {len(fallback_results) if fallback_results else 0} results"
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
if fallback_results:
|
|
1140
|
+
logger.debug(
|
|
1141
|
+
f"Auto-ingest: Fallback returned {len(fallback_results)} recent memories"
|
|
1142
|
+
)
|
|
1143
|
+
# Add search metadata to fallback results
|
|
1144
|
+
for result in fallback_results:
|
|
1145
|
+
if isinstance(result, dict):
|
|
1146
|
+
result["retrieval_method"] = "recent_memories_fallback"
|
|
1147
|
+
result["retrieval_query"] = user_input
|
|
1148
|
+
return fallback_results
|
|
1149
|
+
else:
|
|
1150
|
+
logger.debug("Auto-ingest: Fallback search returned no results")
|
|
1151
|
+
|
|
1152
|
+
except Exception as fallback_e:
|
|
1153
|
+
logger.error(f"Auto-ingest: Fallback search failed: {fallback_e}")
|
|
1154
|
+
logger.debug(
|
|
1155
|
+
f"Auto-ingest: Fallback error details: {type(fallback_e).__name__}: {str(fallback_e)}",
|
|
1156
|
+
exc_info=True,
|
|
1107
1157
|
)
|
|
1108
|
-
# Add search metadata to fallback results
|
|
1109
|
-
for result in fallback_results:
|
|
1110
|
-
if isinstance(result, dict):
|
|
1111
|
-
result["retrieval_method"] = "recent_memories_fallback"
|
|
1112
|
-
result["retrieval_query"] = user_input
|
|
1113
|
-
return fallback_results
|
|
1114
1158
|
|
|
1115
1159
|
logger.debug(
|
|
1116
1160
|
"Auto-ingest: All retrieval methods failed, returning empty context"
|
|
@@ -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,17 +111,41 @@ 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,
|
|
@@ -117,8 +158,22 @@ class SearchService:
|
|
|
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 []
|
|
@@ -185,6 +268,14 @@ class SearchService:
|
|
|
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 []
|
|
@@ -268,6 +365,14 @@ class SearchService:
|
|
|
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 []
|
|
@@ -351,19 +466,42 @@ class SearchService:
|
|
|
351
466
|
search_short_term: bool,
|
|
352
467
|
search_long_term: bool,
|
|
353
468
|
) -> List[Dict[str, Any]]:
|
|
354
|
-
"""Fallback LIKE-based search"""
|
|
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,6 +584,9 @@ 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(
|
|
@@ -425,10 +425,31 @@ class SQLAlchemyDatabaseManager:
|
|
|
425
425
|
logger.warning(f"PostgreSQL FTS setup failed: {e}")
|
|
426
426
|
|
|
427
427
|
def _get_search_service(self) -> SearchService:
|
|
428
|
-
"""Get search service instance with fresh session"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
"""Get search service instance with fresh session and proper error handling"""
|
|
429
|
+
try:
|
|
430
|
+
if not self.SessionLocal:
|
|
431
|
+
logger.error("SessionLocal not available for search service")
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
# Always create a new session to avoid stale connections
|
|
435
|
+
session = self.SessionLocal()
|
|
436
|
+
if not session:
|
|
437
|
+
logger.error("Failed to create database session")
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
search_service = SearchService(session, self.database_type)
|
|
441
|
+
logger.debug(
|
|
442
|
+
f"Created new search service instance for database type: {self.database_type}"
|
|
443
|
+
)
|
|
444
|
+
return search_service
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.error(f"Failed to create search service: {e}")
|
|
448
|
+
logger.debug(
|
|
449
|
+
f"Search service creation error: {type(e).__name__}: {str(e)}",
|
|
450
|
+
exc_info=True,
|
|
451
|
+
)
|
|
452
|
+
return None
|
|
432
453
|
|
|
433
454
|
def store_chat_history(
|
|
434
455
|
self,
|
|
@@ -564,23 +585,51 @@ class SQLAlchemyDatabaseManager:
|
|
|
564
585
|
limit: int = 10,
|
|
565
586
|
) -> List[Dict[str, Any]]:
|
|
566
587
|
"""Search memories using the cross-database search service"""
|
|
588
|
+
search_service = None
|
|
567
589
|
try:
|
|
590
|
+
logger.debug(
|
|
591
|
+
f"Starting memory search for query '{query}' in namespace '{namespace}' with category_filter={category_filter}"
|
|
592
|
+
)
|
|
568
593
|
search_service = self._get_search_service()
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
594
|
+
|
|
595
|
+
if not search_service:
|
|
596
|
+
logger.error("Failed to create search service instance")
|
|
597
|
+
return []
|
|
598
|
+
|
|
599
|
+
results = search_service.search_memories(
|
|
600
|
+
query, namespace, category_filter, limit
|
|
601
|
+
)
|
|
602
|
+
logger.debug(f"Search for '{query}' returned {len(results)} results")
|
|
603
|
+
|
|
604
|
+
# Validate results structure
|
|
605
|
+
if not isinstance(results, list):
|
|
606
|
+
logger.warning(
|
|
607
|
+
f"Search service returned unexpected type: {type(results)}, converting to list"
|
|
572
608
|
)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
# Ensure session is properly closed
|
|
577
|
-
search_service.session.close()
|
|
609
|
+
results = list(results) if results else []
|
|
610
|
+
|
|
611
|
+
return results
|
|
578
612
|
|
|
579
613
|
except Exception as e:
|
|
580
|
-
logger.error(
|
|
614
|
+
logger.error(
|
|
615
|
+
f"Memory search failed for query '{query}' in namespace '{namespace}': {e}"
|
|
616
|
+
)
|
|
617
|
+
logger.debug(
|
|
618
|
+
f"Search error details: {type(e).__name__}: {str(e)}", exc_info=True
|
|
619
|
+
)
|
|
581
620
|
# Return empty list instead of raising exception to avoid breaking auto_ingest
|
|
582
621
|
return []
|
|
583
622
|
|
|
623
|
+
finally:
|
|
624
|
+
# Ensure session is properly closed, even if an exception occurred
|
|
625
|
+
if search_service and hasattr(search_service, "session"):
|
|
626
|
+
try:
|
|
627
|
+
if search_service.session:
|
|
628
|
+
logger.debug("Closing search service session")
|
|
629
|
+
search_service.session.close()
|
|
630
|
+
except Exception as session_e:
|
|
631
|
+
logger.warning(f"Error closing search service session: {session_e}")
|
|
632
|
+
|
|
584
633
|
def get_memory_stats(self, namespace: str = "default") -> Dict[str, Any]:
|
|
585
634
|
"""Get comprehensive memory statistics"""
|
|
586
635
|
with self.SessionLocal() as session:
|
memori/tools/memory_tool.py
CHANGED
|
@@ -73,11 +73,24 @@ class MemoryTool:
|
|
|
73
73
|
|
|
74
74
|
# Use retrieval agent for intelligent search
|
|
75
75
|
try:
|
|
76
|
+
logger.debug(
|
|
77
|
+
f"Attempting to import MemorySearchEngine for query: '{query}'"
|
|
78
|
+
)
|
|
76
79
|
from ..agents.retrieval_agent import MemorySearchEngine
|
|
77
80
|
|
|
81
|
+
logger.debug("Successfully imported MemorySearchEngine")
|
|
82
|
+
|
|
78
83
|
# Create search engine if not already initialized
|
|
79
84
|
if not hasattr(self, "_search_engine"):
|
|
80
|
-
|
|
85
|
+
if (
|
|
86
|
+
hasattr(self.memori, "provider_config")
|
|
87
|
+
and self.memori.provider_config
|
|
88
|
+
):
|
|
89
|
+
self._search_engine = MemorySearchEngine(
|
|
90
|
+
provider_config=self.memori.provider_config
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
self._search_engine = MemorySearchEngine()
|
|
81
94
|
|
|
82
95
|
# Execute search using retrieval agent
|
|
83
96
|
results = self._search_engine.execute_search(
|
|
@@ -88,18 +101,62 @@ class MemoryTool:
|
|
|
88
101
|
)
|
|
89
102
|
|
|
90
103
|
if not results:
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"Primary search returned no results for query: '{query}', trying fallback search"
|
|
106
|
+
)
|
|
107
|
+
# Try fallback direct database search
|
|
108
|
+
try:
|
|
109
|
+
fallback_results = self.memori.db_manager.search_memories(
|
|
110
|
+
query=query, namespace=self.memori.namespace, limit=5
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if fallback_results:
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"Fallback search found {len(fallback_results)} results"
|
|
116
|
+
)
|
|
117
|
+
results = fallback_results
|
|
118
|
+
else:
|
|
119
|
+
logger.warning(
|
|
120
|
+
f"Both primary and fallback search returned no results for query: '{query}'"
|
|
121
|
+
)
|
|
122
|
+
return f"No relevant memories found for query: '{query}'"
|
|
123
|
+
|
|
124
|
+
except Exception as fallback_e:
|
|
125
|
+
logger.error(
|
|
126
|
+
f"Fallback search also failed for query '{query}': {fallback_e}"
|
|
127
|
+
)
|
|
128
|
+
return f"No relevant memories found for query: '{query}'"
|
|
129
|
+
|
|
130
|
+
# Ensure we have results to format
|
|
131
|
+
if not results:
|
|
132
|
+
logger.warning(
|
|
133
|
+
f"No results available for formatting for query: '{query}'"
|
|
134
|
+
)
|
|
91
135
|
return f"No relevant memories found for query: '{query}'"
|
|
92
136
|
|
|
93
137
|
# Format results as a readable string
|
|
138
|
+
logger.debug(
|
|
139
|
+
f"Starting to format {len(results)} results for query: '{query}'"
|
|
140
|
+
)
|
|
94
141
|
formatted_output = f"🔍 Memory Search Results for: '{query}'\n\n"
|
|
95
142
|
|
|
96
143
|
for i, result in enumerate(results, 1):
|
|
97
144
|
try:
|
|
145
|
+
logger.debug(
|
|
146
|
+
f"Formatting result {i}: type={type(result)}, keys={list(result.keys()) if isinstance(result, dict) else 'not-dict'}"
|
|
147
|
+
)
|
|
148
|
+
|
|
98
149
|
# Try to parse processed data for better formatting
|
|
99
150
|
if "processed_data" in result:
|
|
100
151
|
import json
|
|
101
152
|
|
|
102
|
-
|
|
153
|
+
if isinstance(result["processed_data"], dict):
|
|
154
|
+
processed_data = result["processed_data"]
|
|
155
|
+
elif isinstance(result["processed_data"], str):
|
|
156
|
+
processed_data = json.loads(result["processed_data"])
|
|
157
|
+
else:
|
|
158
|
+
raise ValueError("Error, wrong 'processed_data' format")
|
|
159
|
+
|
|
103
160
|
summary = processed_data.get("summary", "")
|
|
104
161
|
category = processed_data.get("category", {}).get(
|
|
105
162
|
"primary_category", ""
|
|
@@ -124,34 +181,63 @@ class MemoryTool:
|
|
|
124
181
|
|
|
125
182
|
formatted_output += "\n"
|
|
126
183
|
|
|
127
|
-
except Exception:
|
|
184
|
+
except Exception as format_e:
|
|
185
|
+
logger.warning(f"Error formatting result {i}: {format_e}")
|
|
128
186
|
# Fallback formatting
|
|
129
187
|
content = result.get(
|
|
130
188
|
"searchable_content", "Memory content available"
|
|
131
189
|
)[:100]
|
|
132
190
|
formatted_output += f"{i}. {content}...\n\n"
|
|
133
191
|
|
|
192
|
+
logger.debug(
|
|
193
|
+
f"Successfully formatted results, output length: {len(formatted_output)}"
|
|
194
|
+
)
|
|
134
195
|
return formatted_output.strip()
|
|
135
196
|
|
|
136
|
-
except ImportError:
|
|
197
|
+
except ImportError as import_e:
|
|
198
|
+
logger.warning(
|
|
199
|
+
f"Failed to import MemorySearchEngine for query '{query}': {import_e}"
|
|
200
|
+
)
|
|
137
201
|
# Fallback to original search methods if retrieval agent is not available
|
|
202
|
+
logger.debug(
|
|
203
|
+
f"Using ImportError fallback search methods for query: '{query}'"
|
|
204
|
+
)
|
|
205
|
+
|
|
138
206
|
# Try different search strategies based on query content
|
|
139
207
|
if any(word in query.lower() for word in ["name", "who am i", "about me"]):
|
|
208
|
+
logger.debug(
|
|
209
|
+
f"Trying essential conversations for personal query: '{query}'"
|
|
210
|
+
)
|
|
140
211
|
# Personal information query - try essential conversations first
|
|
141
212
|
essential_result = self._get_essential_conversations()
|
|
142
213
|
if essential_result.get("count", 0) > 0:
|
|
214
|
+
logger.debug(
|
|
215
|
+
f"Essential conversations found {essential_result.get('count', 0)} results"
|
|
216
|
+
)
|
|
143
217
|
return self._format_dict_to_string(essential_result)
|
|
144
218
|
|
|
145
219
|
# General search
|
|
220
|
+
logger.debug(f"Trying general search for query: '{query}'")
|
|
146
221
|
search_result = self._search_memories(query=query, limit=10)
|
|
222
|
+
logger.debug(
|
|
223
|
+
f"General search returned results_count: {search_result.get('results_count', 0)}"
|
|
224
|
+
)
|
|
147
225
|
if search_result.get("results_count", 0) > 0:
|
|
148
226
|
return self._format_dict_to_string(search_result)
|
|
149
227
|
|
|
150
228
|
# Fallback to context retrieval
|
|
229
|
+
logger.debug(f"Trying context retrieval fallback for query: '{query}'")
|
|
151
230
|
context_result = self._retrieve_context(query=query, limit=5)
|
|
231
|
+
logger.debug(
|
|
232
|
+
f"Context retrieval returned context_count: {context_result.get('context_count', 0)}"
|
|
233
|
+
)
|
|
152
234
|
return self._format_dict_to_string(context_result)
|
|
153
235
|
|
|
154
236
|
except Exception as e:
|
|
237
|
+
logger.error(
|
|
238
|
+
f"Unexpected error in memory tool execute for query '{query}': {e}",
|
|
239
|
+
exc_info=True,
|
|
240
|
+
)
|
|
155
241
|
return f"Error searching memories: {str(e)}"
|
|
156
242
|
|
|
157
243
|
def _format_dict_to_string(self, result_dict: Dict[str, Any]) -> str:
|
|
@@ -293,6 +379,8 @@ class MemoryTool:
|
|
|
293
379
|
|
|
294
380
|
def _get_stats(self, **kwargs) -> Dict[str, Any]:
|
|
295
381
|
"""Get memory and integration statistics"""
|
|
382
|
+
# kwargs can be used for future filtering options
|
|
383
|
+
_ = kwargs # Mark as intentionally unused
|
|
296
384
|
try:
|
|
297
385
|
memory_stats = self.memori.get_memory_stats()
|
|
298
386
|
integration_stats = self.memori.get_integration_stats()
|
|
@@ -348,6 +436,8 @@ class MemoryTool:
|
|
|
348
436
|
|
|
349
437
|
def _trigger_analysis(self, **kwargs) -> Dict[str, Any]:
|
|
350
438
|
"""Trigger conscious agent analysis"""
|
|
439
|
+
# kwargs can be used for future analysis options
|
|
440
|
+
_ = kwargs # Mark as intentionally unused
|
|
351
441
|
try:
|
|
352
442
|
if hasattr(self.memori, "trigger_conscious_analysis"):
|
|
353
443
|
self.memori.trigger_conscious_analysis()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memorisdk
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: The Open-Source Memory Layer for AI Agents & Multi-Agent Systems
|
|
5
5
|
Author-email: GibsonAI Team <noc@gibsonai.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -458,19 +458,25 @@ memori/
|
|
|
458
458
|
- **[Memory Retrieval](./memory_retrival_example.py)** - Function calling with memory tools
|
|
459
459
|
- **[Advanced Config](./examples/advanced_config.py)** - Production configuration
|
|
460
460
|
- **[Interactive Demo](./memori_example.py)** - Live conscious ingestion showcase
|
|
461
|
+
- **[Simple Multi-User](./examples/multiple-users/simple_multiuser.py)** - Basic demonstration of user memory isolation with namespaces
|
|
462
|
+
- **[FastAPI Multi-User App](./examples/multiple-users/fastapi_multiuser_app.py)** - Full-featured REST API with Swagger UI for testing multi-user functionality
|
|
461
463
|
|
|
462
464
|
## Framework Integrations
|
|
463
465
|
|
|
464
466
|
Memori works seamlessly with popular AI frameworks:
|
|
465
467
|
|
|
466
|
-
| Framework | Description | Example |
|
|
467
|
-
|
|
468
|
-
|
|
|
469
|
-
|
|
|
470
|
-
|
|
|
471
|
-
|
|
|
472
|
-
|
|
|
473
|
-
|
|
|
468
|
+
| Framework | Description | Example |
|
|
469
|
+
|-----------|-------------|---------|
|
|
470
|
+
| [AgentOps](./examples/integrations/agentops_example.py) | Track and monitor Memori memory operations with comprehensive observability | Memory operation tracking with AgentOps analytics |
|
|
471
|
+
| [Agno](./examples/integrations/agno_example.py) | Memory-enhanced agent framework integration with persistent conversations | Simple chat agent with memory search |
|
|
472
|
+
| [AWS Strands](./examples/integrations/aws_strands_example.py) | Professional development coach with Strands SDK and persistent memory | Career coaching agent with goal tracking |
|
|
473
|
+
| [Azure AI Foundry](./examples/integrations/azure_ai_foundry_example.py) | Azure AI Foundry agents with persistent memory across conversations | Enterprise AI agents with Azure integration |
|
|
474
|
+
| [CamelAI](./examples/integrations/camelai_example.py) | Multi-agent communication framework with automatic memory recording and retrieval | Memory-enhanced chat agents with conversation continuity |
|
|
475
|
+
| [CrewAI](./examples/integrations/crewai_example.py) | Multi-agent system with shared memory across agent interactions | Collaborative agents with memory |
|
|
476
|
+
| [Digital Ocean AI](./examples/integrations/digital_ocean_example.py) | Memory-enhanced customer support using Digital Ocean's AI platform | Customer support assistant with conversation history |
|
|
477
|
+
| [LangChain](./examples/integrations/langchain_example.py) | Enterprise-grade agent framework with advanced memory integration | AI assistant with LangChain tools and memory |
|
|
478
|
+
| [OpenAI Agent](./examples/integrations/openai_agent_example.py) | Memory-enhanced OpenAI Agent with function calling and user preference tracking | Interactive assistant with memory search and user info storage |
|
|
479
|
+
| [Swarms](./examples/integrations/swarms_example.py) | Multi-agent system framework with persistent memory capabilities | Memory-enhanced Swarms agents with auto/conscious ingestion |
|
|
474
480
|
|
|
475
481
|
## Interactive Demos
|
|
476
482
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
memori/__init__.py,sha256=
|
|
1
|
+
memori/__init__.py,sha256=1PEpzV75fNiOmS6aIsDAQOpkPzdW2xq64beYADfaG70,3676
|
|
2
2
|
memori/agents/__init__.py,sha256=9M3IG5R10FfVgT8tUzBZ2pZ0SypSpYkFfhtyvMyeTpE,261
|
|
3
3
|
memori/agents/conscious_agent.py,sha256=NQ-dEHRXutrkwo2ssPZ0ct7uHXU2gDa-PQ0d74mUFvk,13459
|
|
4
4
|
memori/agents/memory_agent.py,sha256=tBwkNMtlVDlQ88jATjuL96vQHPfU_HOz_XXOB3vd710,23552
|
|
5
|
-
memori/agents/retrieval_agent.py,sha256=
|
|
5
|
+
memori/agents/retrieval_agent.py,sha256=FvGApAzljhubFvyMTM0rCs-JpROg34i0aOFeYh3Ru6s,40222
|
|
6
6
|
memori/config/__init__.py,sha256=tQAxopgOsea02u9iId-ocOY86nWWNGC3rvt3AOFcLn8,295
|
|
7
7
|
memori/config/manager.py,sha256=xi8d8xW3obyV6v9UHDG6idSAymge8yWxGW11e2mI0nQ,10388
|
|
8
8
|
memori/config/memory_manager.py,sha256=hxwkMpUaOkOcnnvgpHxR4PT6iHNj8yNda-o9OpM0Y-g,11020
|
|
@@ -10,15 +10,15 @@ memori/config/settings.py,sha256=nrrWD4hwdbtYlIPtJFHgGyMudGP-hz9sA-KBW_7ZZbE,972
|
|
|
10
10
|
memori/core/__init__.py,sha256=jvhHn-KL3bzRHs11-4B0BCKH6gkAf6Gf_G59If8fD0M,157
|
|
11
11
|
memori/core/conversation.py,sha256=l1SRLyIrKrcSzqEdqnqLwSqYW6rXtGeH1Fhs7YBI0XU,15825
|
|
12
12
|
memori/core/database.py,sha256=RkBelmgDm_6WoTKISPTz_WYVsPvnVB7NKJuRFWOLbZM,40044
|
|
13
|
-
memori/core/memory.py,sha256=
|
|
13
|
+
memori/core/memory.py,sha256=Wg1K8XUxGHwgkVi5nRDY7PhfVSRhAGMGN8xd8KnuQVg,105518
|
|
14
14
|
memori/core/providers.py,sha256=Ix8CEb_kw0Qur1E3_mxydh3RvUpyNy5qGAKK4wuGK6Y,7032
|
|
15
15
|
memori/database/__init__.py,sha256=kMLxwfRfTVvw0oV1kl9v-Dkyqm6ggcsMV6hltqdrN3k,189
|
|
16
16
|
memori/database/auto_creator.py,sha256=pQUKV9hO-wM-vocr-_2I-1kwCofd3z8-KpkHAxLREaM,12686
|
|
17
17
|
memori/database/connection_utils.py,sha256=iAxVvwP-_0UZwmc_y_GOs3-26YlPTmot6_bY8crIPxQ,6741
|
|
18
18
|
memori/database/models.py,sha256=jeC5JiVXjxzk3ABt7BeXfkAYOpyjcQ3OqnuLkfIIRyc,14832
|
|
19
19
|
memori/database/query_translator.py,sha256=oe_BCPamtT-LBBUKChbM0jK4rjI4ZDZtVp7ZUGnpDqs,7026
|
|
20
|
-
memori/database/search_service.py,sha256=
|
|
21
|
-
memori/database/sqlalchemy_manager.py,sha256=
|
|
20
|
+
memori/database/search_service.py,sha256=9J6rqCY9R0cxJRssbii73s49VvZbMgdAFFJgasoaj9A,26902
|
|
21
|
+
memori/database/sqlalchemy_manager.py,sha256=KvVv3TcBxB-FTiL1YWolRlklDca-YCfdxCWaDhAHILw,34974
|
|
22
22
|
memori/database/adapters/__init__.py,sha256=lSvRPZXJoKTlj4iI8kW5I45OXjKZh8oFb6vqDpJ69sQ,366
|
|
23
23
|
memori/database/adapters/mysql_adapter.py,sha256=WOe-huYmrt-aeDn0YACnH3F5bhSTjG0SePjl4hThd-s,12944
|
|
24
24
|
memori/database/adapters/postgresql_adapter.py,sha256=tGBGoBo340bLluhb9DytflDdVnhkjbgaCY2SvvoUpfA,11358
|
|
@@ -45,9 +45,8 @@ memori/integrations/__init__.py,sha256=jlTI3TRBpclXXiqbigSUVqpfN9t64Q9B17EhlN543
|
|
|
45
45
|
memori/integrations/anthropic_integration.py,sha256=IJtqPYTcMaYsXKWUabR4XDMpCabg9qMK7d-8T9A1CcY,8169
|
|
46
46
|
memori/integrations/litellm_integration.py,sha256=UWC5huyUMasfZDBzH7MxAVbTXoo70qvyGcrYZI7Gx_Y,12407
|
|
47
47
|
memori/integrations/openai_integration.py,sha256=iQKitY1JY_i-YldcAeePnVdYc6Ns0Gmk_zIoMV302RA,19977
|
|
48
|
-
memori/scripts/llm_text.py,sha256=HDkuGMb527yupSbA3syBFA9WHBPnP2lVqEKW6trIZtU,1603
|
|
49
48
|
memori/tools/__init__.py,sha256=0KPbWAFYmvEleacrby4RzhJGW5GPdFiXN6RWwFrbqf4,200
|
|
50
|
-
memori/tools/memory_tool.py,sha256=
|
|
49
|
+
memori/tools/memory_tool.py,sha256=g4H774npQoYWB8vnTY5xdLrLbEq5i1lK8FDm8MfFtVU,25077
|
|
51
50
|
memori/utils/__init__.py,sha256=e3AN4KfomQBQDsr53HwfvOeTtI3QZMzGQMYpRp8l6ow,1757
|
|
52
51
|
memori/utils/exceptions.py,sha256=JGLo2S8ElG3gBjD4aJeVPWNsNB9OLPYAYzCdKfiEW74,12136
|
|
53
52
|
memori/utils/helpers.py,sha256=_lpGSsI2UkMyYUY6X9k_VEpACvyxwY51TzgYVPZTeBk,13059
|
|
@@ -60,8 +59,8 @@ memori/utils/security_audit.py,sha256=BB9IwDdAiUgmuwjm8rEyHbthfTHr1sipcNlPdFpBs_
|
|
|
60
59
|
memori/utils/security_integration.py,sha256=TORuhOPEAQ1PRkt7xcBHhI1UbCd4LVLowdiv2UItwCY,13078
|
|
61
60
|
memori/utils/transaction_manager.py,sha256=ITSF1Gba4cEBJXW1woDyomhOsdfL8vwfGckqbYqAGok,18755
|
|
62
61
|
memori/utils/validators.py,sha256=2btILpEh2yBS_6rytp2B2epFxMT4876SibS0yNj6bKI,11287
|
|
63
|
-
memorisdk-2.0.
|
|
64
|
-
memorisdk-2.0.
|
|
65
|
-
memorisdk-2.0.
|
|
66
|
-
memorisdk-2.0.
|
|
67
|
-
memorisdk-2.0.
|
|
62
|
+
memorisdk-2.0.1.dist-info/licenses/LICENSE,sha256=gyrDaYsSODngoYE1l68l_UfjppS-oYDrf1MvY1JGhgE,10430
|
|
63
|
+
memorisdk-2.0.1.dist-info/METADATA,sha256=sqUK-ZDWt9bYhI5VtLlTLCabZuNdem9ZPHO-JM6S308,18803
|
|
64
|
+
memorisdk-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
65
|
+
memorisdk-2.0.1.dist-info/top_level.txt,sha256=Nm3ad0isbJYBzTEce-O_gmkAEiTbAbyilgAhRt8IoGA,7
|
|
66
|
+
memorisdk-2.0.1.dist-info/RECORD,,
|
memori/scripts/llm_text.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def is_text_file(file_path):
|
|
5
|
-
try:
|
|
6
|
-
with open(file_path, encoding="utf-8") as f:
|
|
7
|
-
f.read()
|
|
8
|
-
return True
|
|
9
|
-
except (UnicodeDecodeError, OSError):
|
|
10
|
-
return False
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def ingest_folder_to_txt(
|
|
14
|
-
input_path, output_file="ingested_data.txt", exclude_dirs=None
|
|
15
|
-
):
|
|
16
|
-
if exclude_dirs is None:
|
|
17
|
-
exclude_dirs = [".git", "node_modules", "__pycache__"]
|
|
18
|
-
|
|
19
|
-
with open(output_file, "w", encoding="utf-8") as out_f:
|
|
20
|
-
for root, dirs, files in os.walk(input_path):
|
|
21
|
-
# Filter out excluded directories
|
|
22
|
-
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
23
|
-
|
|
24
|
-
for file in files:
|
|
25
|
-
file_path = os.path.join(root, file)
|
|
26
|
-
|
|
27
|
-
# Skip binary files or the output file itself
|
|
28
|
-
if not is_text_file(file_path) or os.path.abspath(
|
|
29
|
-
file_path
|
|
30
|
-
) == os.path.abspath(output_file):
|
|
31
|
-
continue
|
|
32
|
-
|
|
33
|
-
try:
|
|
34
|
-
with open(file_path, encoding="utf-8") as in_f:
|
|
35
|
-
content = in_f.read()
|
|
36
|
-
|
|
37
|
-
relative_path = os.path.relpath(file_path, input_path)
|
|
38
|
-
out_f.write(f"\n### FILE: {relative_path} ###\n")
|
|
39
|
-
out_f.write(content + "\n")
|
|
40
|
-
|
|
41
|
-
except Exception as e:
|
|
42
|
-
print(f"Skipping {file_path}: {e}")
|
|
43
|
-
|
|
44
|
-
print(f"\n✅ Ingested data written to: {output_file}")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# ---- Run from CLI or script ----
|
|
48
|
-
if __name__ == "__main__":
|
|
49
|
-
folder_path = input("Enter the path to the folder: ").strip()
|
|
50
|
-
ingest_folder_to_txt(folder_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|