MemoryOS 0.2.1__py3-none-any.whl → 0.2.2__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 MemoryOS might be problematic. Click here for more details.

Files changed (74) hide show
  1. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/METADATA +2 -1
  2. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/RECORD +72 -55
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +156 -65
  5. memos/api/context/context.py +147 -0
  6. memos/api/context/dependencies.py +90 -0
  7. memos/api/product_models.py +5 -1
  8. memos/api/routers/product_router.py +54 -26
  9. memos/configs/graph_db.py +49 -1
  10. memos/configs/internet_retriever.py +6 -0
  11. memos/configs/mem_os.py +5 -0
  12. memos/configs/mem_reader.py +9 -0
  13. memos/configs/mem_scheduler.py +18 -4
  14. memos/configs/mem_user.py +58 -0
  15. memos/graph_dbs/base.py +9 -1
  16. memos/graph_dbs/factory.py +2 -0
  17. memos/graph_dbs/nebular.py +1364 -0
  18. memos/graph_dbs/neo4j.py +4 -4
  19. memos/log.py +1 -1
  20. memos/mem_cube/utils.py +13 -6
  21. memos/mem_os/core.py +140 -30
  22. memos/mem_os/main.py +1 -1
  23. memos/mem_os/product.py +266 -152
  24. memos/mem_os/utils/format_utils.py +314 -67
  25. memos/mem_reader/simple_struct.py +13 -5
  26. memos/mem_scheduler/base_scheduler.py +220 -250
  27. memos/mem_scheduler/general_scheduler.py +193 -73
  28. memos/mem_scheduler/modules/base.py +5 -5
  29. memos/mem_scheduler/modules/dispatcher.py +6 -9
  30. memos/mem_scheduler/modules/misc.py +81 -16
  31. memos/mem_scheduler/modules/monitor.py +52 -41
  32. memos/mem_scheduler/modules/rabbitmq_service.py +9 -7
  33. memos/mem_scheduler/modules/retriever.py +108 -191
  34. memos/mem_scheduler/modules/scheduler_logger.py +255 -0
  35. memos/mem_scheduler/mos_for_test_scheduler.py +16 -19
  36. memos/mem_scheduler/schemas/__init__.py +0 -0
  37. memos/mem_scheduler/schemas/general_schemas.py +43 -0
  38. memos/mem_scheduler/schemas/message_schemas.py +148 -0
  39. memos/mem_scheduler/schemas/monitor_schemas.py +329 -0
  40. memos/mem_scheduler/utils/__init__.py +0 -0
  41. memos/mem_scheduler/utils/filter_utils.py +176 -0
  42. memos/mem_scheduler/utils/misc_utils.py +61 -0
  43. memos/mem_user/factory.py +94 -0
  44. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  45. memos/mem_user/mysql_user_manager.py +500 -0
  46. memos/mem_user/persistent_factory.py +96 -0
  47. memos/mem_user/user_manager.py +4 -4
  48. memos/memories/activation/item.py +4 -0
  49. memos/memories/textual/base.py +1 -1
  50. memos/memories/textual/general.py +35 -91
  51. memos/memories/textual/item.py +5 -33
  52. memos/memories/textual/tree.py +13 -7
  53. memos/memories/textual/tree_text_memory/organize/conflict.py +4 -2
  54. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +47 -43
  55. memos/memories/textual/tree_text_memory/organize/reorganizer.py +8 -5
  56. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  57. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
  58. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  59. memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -23
  60. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -15
  61. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  62. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  63. memos/memos_tools/dinding_report_bot.py +422 -0
  64. memos/memos_tools/notification_service.py +44 -0
  65. memos/memos_tools/notification_utils.py +96 -0
  66. memos/settings.py +3 -1
  67. memos/templates/mem_reader_prompts.py +2 -1
  68. memos/templates/mem_scheduler_prompts.py +41 -7
  69. memos/templates/mos_prompts.py +87 -0
  70. memos/mem_scheduler/modules/schemas.py +0 -328
  71. memos/mem_scheduler/utils.py +0 -75
  72. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
  73. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
  74. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/entry_points.txt +0 -0
@@ -6,18 +6,23 @@ from memos.llms.base import BaseLLM
6
6
  from memos.log import get_logger
7
7
  from memos.mem_cube.general import GeneralMemCube
8
8
  from memos.mem_scheduler.modules.base import BaseSchedulerModule
9
- from memos.mem_scheduler.modules.misc import AutoDroppingQueue as Queue
10
- from memos.mem_scheduler.modules.schemas import (
9
+ from memos.mem_scheduler.schemas.general_schemas import (
11
10
  DEFAULT_ACTIVATION_MEM_MONITOR_SIZE_LIMIT,
11
+ DEFAULT_WEIGHT_VECTOR_FOR_RANKING,
12
12
  DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT,
13
13
  MONITOR_ACTIVATION_MEMORY_TYPE,
14
14
  MONITOR_WORKING_MEMORY_TYPE,
15
15
  MemCubeID,
16
- MemoryMonitorManager,
17
16
  UserID,
18
17
  )
19
- from memos.mem_scheduler.utils import extract_json_dict
20
- from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
18
+ from memos.mem_scheduler.schemas.monitor_schemas import (
19
+ MemoryMonitorItem,
20
+ MemoryMonitorManager,
21
+ QueryMonitorItem,
22
+ QueryMonitorQueue,
23
+ )
24
+ from memos.mem_scheduler.utils.misc_utils import extract_json_dict
25
+ from memos.memories.textual.tree import TreeTextMemory
21
26
 
22
27
 
23
28
  logger = get_logger(__name__)
@@ -31,7 +36,8 @@ class SchedulerMonitor(BaseSchedulerModule):
31
36
 
32
37
  # hyper-parameters
33
38
  self.config: BaseSchedulerConfig = config
34
- self.act_mem_update_interval = self.config.get("act_mem_update_interval", 300)
39
+ self.act_mem_update_interval = self.config.get("act_mem_update_interval", 30)
40
+ self.query_trigger_interval = self.config.get("query_trigger_interval", 10)
35
41
 
36
42
  # Partial Retention Strategy
37
43
  self.partial_retention_number = 2
@@ -39,16 +45,39 @@ class SchedulerMonitor(BaseSchedulerModule):
39
45
  self.activation_mem_monitor_capacity = DEFAULT_ACTIVATION_MEM_MONITOR_SIZE_LIMIT
40
46
 
41
47
  # attributes
42
- self.query_history = Queue(maxsize=self.config.context_window_size)
43
- self.intent_history = Queue(maxsize=self.config.context_window_size)
48
+ # recording query_messages
49
+ self.query_monitors: QueryMonitorQueue[QueryMonitorItem] = QueryMonitorQueue(
50
+ maxsize=self.config.context_window_size
51
+ )
52
+
44
53
  self.working_memory_monitors: dict[UserID, dict[MemCubeID, MemoryMonitorManager]] = {}
45
54
  self.activation_memory_monitors: dict[UserID, dict[MemCubeID, MemoryMonitorManager]] = {}
46
55
 
47
56
  # Lifecycle monitor
48
- self._last_activation_mem_update_time = datetime.min
57
+ self.last_activation_mem_update_time = datetime.min
58
+ self.last_query_consume_time = datetime.min
49
59
 
50
60
  self._process_llm = process_llm
51
61
 
62
+ def extract_query_keywords(self, query: str) -> list:
63
+ """Extracts core keywords from a user query based on specific semantic rules."""
64
+ prompt_name = "query_keywords_extraction"
65
+ prompt = self.build_prompt(
66
+ template_name=prompt_name,
67
+ query=query,
68
+ )
69
+ llm_response = self._process_llm.generate([{"role": "user", "content": prompt}])
70
+ try:
71
+ # Parse JSON output from LLM response
72
+ keywords = extract_json_dict(llm_response)
73
+ assert isinstance(keywords, list)
74
+ except Exception as e:
75
+ logger.error(
76
+ f"Failed to parse keywords from LLM response: {llm_response}. Error: {e!s}"
77
+ )
78
+ keywords = [query]
79
+ return keywords
80
+
52
81
  def register_memory_manager_if_not_exists(
53
82
  self,
54
83
  user_id: str,
@@ -90,13 +119,15 @@ class SchedulerMonitor(BaseSchedulerModule):
90
119
  f"mem_cube_id={mem_cube_id} in the provided memory_monitors dictionary"
91
120
  )
92
121
 
93
- def update_memory_monitors(self, user_id: str, mem_cube_id: str, mem_cube: GeneralMemCube):
122
+ def update_working_memory_monitors(
123
+ self,
124
+ new_working_memory_monitors: list[MemoryMonitorItem],
125
+ user_id: str,
126
+ mem_cube_id: str,
127
+ mem_cube: GeneralMemCube,
128
+ ):
94
129
  text_mem_base: TreeTextMemory = mem_cube.text_mem
95
-
96
- if not isinstance(text_mem_base, TreeTextMemory):
97
- logger.error("Not Implemented")
98
- return
99
-
130
+ assert isinstance(text_mem_base, TreeTextMemory)
100
131
  self.working_mem_monitor_capacity = min(
101
132
  DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT,
102
133
  (
@@ -105,17 +136,6 @@ class SchedulerMonitor(BaseSchedulerModule):
105
136
  ),
106
137
  )
107
138
 
108
- self.update_working_memory_monitors(
109
- user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
110
- )
111
-
112
- self.update_activation_memory_monitors(
113
- user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
114
- )
115
-
116
- def update_working_memory_monitors(
117
- self, user_id: str, mem_cube_id: str, mem_cube: GeneralMemCube
118
- ):
119
139
  # register monitors
120
140
  self.register_memory_manager_if_not_exists(
121
141
  user_id=user_id,
@@ -124,14 +144,8 @@ class SchedulerMonitor(BaseSchedulerModule):
124
144
  max_capacity=self.working_mem_monitor_capacity,
125
145
  )
126
146
 
127
- # === update working memory monitors ===
128
- # Retrieve current working memory content
129
- text_mem_base: TreeTextMemory = mem_cube.text_mem
130
- working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
131
- text_working_memory: list[str] = [w_m.memory for w_m in working_memory]
132
-
133
147
  self.working_memory_monitors[user_id][mem_cube_id].update_memories(
134
- text_working_memories=text_working_memory,
148
+ new_memory_monitors=new_working_memory_monitors,
135
149
  partial_retention_number=self.partial_retention_number,
136
150
  )
137
151
 
@@ -149,16 +163,13 @@ class SchedulerMonitor(BaseSchedulerModule):
149
163
  # Sort by importance_score in descending order and take top k
150
164
  top_k_memories = sorted(
151
165
  self.working_memory_monitors[user_id][mem_cube_id].memories,
152
- key=lambda m: m.get_score(),
166
+ key=lambda m: m.get_importance_score(weight_vector=DEFAULT_WEIGHT_VECTOR_FOR_RANKING),
153
167
  reverse=True,
154
168
  )[: self.activation_mem_monitor_capacity]
155
169
 
156
- # Extract just the text from these memories
157
- text_top_k_memories = [m.memory_text for m in top_k_memories]
158
-
159
170
  # Update the activation memory monitors with these important memories
160
171
  self.activation_memory_monitors[user_id][mem_cube_id].update_memories(
161
- text_working_memories=text_top_k_memories,
172
+ new_memory_monitors=top_k_memories,
162
173
  partial_retention_number=self.partial_retention_number,
163
174
  )
164
175
 
@@ -206,10 +217,10 @@ class SchedulerMonitor(BaseSchedulerModule):
206
217
  )
207
218
  return []
208
219
 
209
- manager = monitor_dict[user_id][mem_cube_id]
220
+ manager: MemoryMonitorManager = monitor_dict[user_id][mem_cube_id]
210
221
  # Sort memories by recording_count in descending order and return top_k items
211
- sorted_memories = sorted(manager.memories, key=lambda m: m.recording_count, reverse=True)
212
- sorted_text_memories = [m.memory_text for m in sorted_memories[:top_k]]
222
+ sorted_memory_monitors = manager.get_sorted_mem_monitors(reverse=True)
223
+ sorted_text_memories = [m.memory_text for m in sorted_memory_monitors[:top_k]]
213
224
  return sorted_text_memories
214
225
 
215
226
  def get_monitors_info(self, user_id: str, mem_cube_id: str) -> dict[str, Any]:
@@ -4,13 +4,13 @@ import threading
4
4
  import time
5
5
 
6
6
  from pathlib import Path
7
- from queue import Queue
8
7
 
9
8
  from memos.configs.mem_scheduler import AuthConfig, RabbitMQConfig
10
9
  from memos.dependency import require_python_package
11
10
  from memos.log import get_logger
12
11
  from memos.mem_scheduler.modules.base import BaseSchedulerModule
13
- from memos.mem_scheduler.modules.schemas import DIRECT_EXCHANGE_TYPE, FANOUT_EXCHANGE_TYPE
12
+ from memos.mem_scheduler.modules.misc import AutoDroppingQueue
13
+ from memos.mem_scheduler.schemas.general_schemas import DIRECT_EXCHANGE_TYPE, FANOUT_EXCHANGE_TYPE
14
14
 
15
15
 
16
16
  logger = get_logger(__name__)
@@ -38,7 +38,9 @@ class RabbitMQSchedulerModule(BaseSchedulerModule):
38
38
 
39
39
  # fixed params
40
40
  self.rabbitmq_message_cache_max_size = 10 # Max 10 messages
41
- self.rabbitmq_message_cache = Queue(maxsize=self.rabbitmq_message_cache_max_size)
41
+ self.rabbitmq_message_cache = AutoDroppingQueue(
42
+ maxsize=self.rabbitmq_message_cache_max_size
43
+ )
42
44
  self.rabbitmq_connection_attempts = 3 # Max retry attempts on connection failure
43
45
  self.rabbitmq_retry_delay = 5 # Delay (seconds) between retries
44
46
  self.rabbitmq_heartbeat = 60 # Heartbeat interval (seconds) for connectio
@@ -214,12 +216,12 @@ class RabbitMQSchedulerModule(BaseSchedulerModule):
214
216
  def on_rabbitmq_message(self, channel, method, properties, body):
215
217
  """Handle incoming messages. Only for test."""
216
218
  try:
217
- print(f"Received message: {body.decode()}")
218
- self.rabbitmq_message_cache.put_nowait({"properties": properties, "body": body})
219
- print(f"message delivery_tag: {method.delivery_tag}")
219
+ print(f"Received message: {body.decode()}\n")
220
+ self.rabbitmq_message_cache.put({"properties": properties, "body": body})
221
+ print(f"message delivery_tag: {method.delivery_tag}\n")
220
222
  channel.basic_ack(delivery_tag=method.delivery_tag)
221
223
  except Exception as e:
222
- logger.error(f"Message handling failed: {e}")
224
+ logger.error(f"Message handling failed: {e}", exc_info=True)
223
225
 
224
226
  def wait_for_connection_ready(self):
225
227
  start_time = time.time()
@@ -1,20 +1,19 @@
1
- import logging
2
-
3
1
  from memos.configs.mem_scheduler import BaseSchedulerConfig
4
- from memos.dependency import require_python_package
5
2
  from memos.llms.base import BaseLLM
6
3
  from memos.log import get_logger
7
4
  from memos.mem_cube.general import GeneralMemCube
8
5
  from memos.mem_scheduler.modules.base import BaseSchedulerModule
9
- from memos.mem_scheduler.modules.schemas import (
6
+ from memos.mem_scheduler.schemas.general_schemas import (
10
7
  TreeTextMemory_SEARCH_METHOD,
11
8
  )
12
- from memos.mem_scheduler.utils import (
13
- extract_json_dict,
14
- is_all_chinese,
15
- is_all_english,
9
+ from memos.mem_scheduler.utils.filter_utils import (
10
+ filter_similar_memories,
11
+ filter_too_short_memories,
16
12
  transform_name_to_key,
17
13
  )
14
+ from memos.mem_scheduler.utils.misc_utils import (
15
+ extract_json_dict,
16
+ )
18
17
  from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
19
18
 
20
19
 
@@ -25,19 +24,16 @@ class SchedulerRetriever(BaseSchedulerModule):
25
24
  def __init__(self, process_llm: BaseLLM, config: BaseSchedulerConfig):
26
25
  super().__init__()
27
26
 
28
- self.config: BaseSchedulerConfig = config
29
- self.process_llm = process_llm
30
-
31
27
  # hyper-parameters
32
28
  self.filter_similarity_threshold = 0.75
33
29
  self.filter_min_length_threshold = 6
34
30
 
35
- # log function callbacks
36
- self.log_working_memory_replacement = None
31
+ self.config: BaseSchedulerConfig = config
32
+ self.process_llm = process_llm
37
33
 
38
34
  def search(
39
35
  self, query: str, mem_cube: GeneralMemCube, top_k: int, method=TreeTextMemory_SEARCH_METHOD
40
- ):
36
+ ) -> list[TextualMemoryItem]:
41
37
  """Search in text memory with the given query.
42
38
 
43
39
  Args:
@@ -66,203 +62,124 @@ class SchedulerRetriever(BaseSchedulerModule):
66
62
  results = []
67
63
  return results
68
64
 
69
- @require_python_package(
70
- import_name="sklearn",
71
- install_command="pip install scikit-learn",
72
- install_link="https://scikit-learn.org/stable/install.html",
73
- )
74
- def filter_similar_memories(
75
- self, text_memories: list[str], similarity_threshold: float = 0.75
76
- ) -> list[str]:
65
+ def rerank_memories(
66
+ self,
67
+ queries: list[str],
68
+ original_memories: list[str],
69
+ top_k: int,
70
+ ) -> (list[str], bool):
77
71
  """
78
- Filters out low-quality or duplicate memories based on text similarity.
72
+ Rerank memories based on relevance to given queries using LLM.
79
73
 
80
74
  Args:
81
- text_memories: List of text memories to filter
82
- similarity_threshold: Threshold for considering memories duplicates (0.0-1.0)
83
- Higher values mean stricter filtering
75
+ queries: List of query strings to determine relevance
76
+ original_memories: List of memory strings to be reranked
77
+ top_k: Number of top memories to return after reranking
84
78
 
85
79
  Returns:
86
- List of filtered memories with duplicates removed
87
- """
88
- from sklearn.feature_extraction.text import TfidfVectorizer
89
- from sklearn.metrics.pairwise import cosine_similarity
90
-
91
- if not text_memories:
92
- logging.warning("Received empty memories list - nothing to filter")
93
- return []
94
-
95
- for idx in range(len(text_memories)):
96
- if not isinstance(text_memories[idx], str):
97
- logger.error(
98
- f"{text_memories[idx]} in memories is not a string,"
99
- f" and now has been transformed to be a string."
100
- )
101
- text_memories[idx] = str(text_memories[idx])
102
-
103
- try:
104
- # Step 1: Vectorize texts using TF-IDF
105
- vectorizer = TfidfVectorizer()
106
- tfidf_matrix = vectorizer.fit_transform(text_memories)
107
-
108
- # Step 2: Calculate pairwise similarity matrix
109
- similarity_matrix = cosine_similarity(tfidf_matrix)
110
-
111
- # Step 3: Identify duplicates
112
- to_keep = []
113
- removal_reasons = {}
114
-
115
- for current_idx in range(len(text_memories)):
116
- is_duplicate = False
117
-
118
- # Compare with already kept memories
119
- for kept_idx in to_keep:
120
- similarity_score = similarity_matrix[current_idx, kept_idx]
121
-
122
- if similarity_score > similarity_threshold:
123
- is_duplicate = True
124
- # Generate removal reason with sample text
125
- removal_reasons[current_idx] = (
126
- f"Memory too similar (score: {similarity_score:.2f}) to kept memory #{kept_idx}. "
127
- f"Kept: '{text_memories[kept_idx][:100]}...' | "
128
- f"Removed: '{text_memories[current_idx][:100]}...'"
129
- )
130
- logger.info(removal_reasons)
131
- break
132
-
133
- if not is_duplicate:
134
- to_keep.append(current_idx)
135
-
136
- # Return filtered memories
137
- return [text_memories[i] for i in sorted(to_keep)]
138
-
139
- except Exception as e:
140
- logging.error(f"Error filtering memories: {e!s}")
141
- return text_memories # Return original list if error occurs
142
-
143
- def filter_too_short_memories(
144
- self, text_memories: list[str], min_length_threshold: int = 20
145
- ) -> list[str]:
146
- """
147
- Filters out text memories that fall below the minimum length requirement.
148
- Handles both English (word count) and Chinese (character count) differently.
149
-
150
- Args:
151
- text_memories: List of text memories to be filtered
152
- min_length_threshold: Minimum length required to keep a memory.
153
- For English: word count, for Chinese: character count.
80
+ List of reranked memory strings (length <= top_k)
154
81
 
155
- Returns:
156
- List of filtered memories meeting the length requirement
82
+ Note:
83
+ If LLM reranking fails, falls back to original order (truncated to top_k)
157
84
  """
158
- if not text_memories:
159
- logging.debug("Empty memories list received in short memory filter")
160
- return []
85
+ success_flag = False
161
86
 
162
- filtered_memories = []
163
- removed_count = 0
87
+ logger.info(f"Starting memory reranking for {len(original_memories)} memories")
164
88
 
165
- for memory in text_memories:
166
- stripped_memory = memory.strip()
167
- if not stripped_memory: # Skip empty/whitespace memories
168
- removed_count += 1
169
- continue
170
-
171
- # Determine measurement method based on language
172
- if is_all_english(stripped_memory):
173
- length = len(stripped_memory.split()) # Word count for English
174
- elif is_all_chinese(stripped_memory):
175
- length = len(stripped_memory) # Character count for Chinese
176
- else:
177
- logger.debug(
178
- f"Mixed-language memory, using character count: {stripped_memory[:50]}..."
179
- )
180
- length = len(stripped_memory) # Default to character count
89
+ # Build LLM prompt for memory reranking
90
+ prompt = self.build_prompt(
91
+ "memory_reranking",
92
+ queries=[f"[0] {queries[0]}"],
93
+ current_order=[f"[{i}] {mem}" for i, mem in enumerate(original_memories)],
94
+ )
95
+ logger.debug(f"Generated reranking prompt: {prompt[:200]}...") # Log first 200 chars
181
96
 
182
- if length >= min_length_threshold:
183
- filtered_memories.append(memory)
184
- else:
185
- removed_count += 1
97
+ # Get LLM response
98
+ response = self.process_llm.generate([{"role": "user", "content": prompt}])
99
+ logger.debug(f"Received LLM response: {response[:200]}...") # Log first 200 chars
186
100
 
187
- if removed_count > 0:
101
+ try:
102
+ # Parse JSON response
103
+ response = extract_json_dict(response)
104
+ new_order = response["new_order"][:top_k]
105
+ text_memories_with_new_order = [original_memories[idx] for idx in new_order]
188
106
  logger.info(
189
- f"Filtered out {removed_count} short memories "
190
- f"(below {min_length_threshold} units). "
191
- f"Total remaining: {len(filtered_memories)}"
107
+ f"Successfully reranked memories. Returning top {len(text_memories_with_new_order)} items;"
108
+ f"Ranking reasoning: {response['reasoning']}"
192
109
  )
110
+ success_flag = True
111
+ except Exception as e:
112
+ logger.error(
113
+ f"Failed to rerank memories with LLM. Exception: {e}. Raw response: {response} ",
114
+ exc_info=True,
115
+ )
116
+ text_memories_with_new_order = original_memories[:top_k]
117
+ success_flag = False
118
+ return text_memories_with_new_order, success_flag
193
119
 
194
- return filtered_memories
195
-
196
- def replace_working_memory(
120
+ def process_and_rerank_memories(
197
121
  self,
198
122
  queries: list[str],
199
- user_id: str,
200
- mem_cube_id: str,
201
- mem_cube: GeneralMemCube,
202
123
  original_memory: list[TextualMemoryItem],
203
124
  new_memory: list[TextualMemoryItem],
204
125
  top_k: int = 10,
205
- ) -> None | list[TextualMemoryItem]:
206
- """Replace working memory with new memories after reranking."""
207
- memories_with_new_order = None
208
- text_mem_base = mem_cube.text_mem
209
- if isinstance(text_mem_base, TreeTextMemory):
210
- text_mem_base: TreeTextMemory = text_mem_base
211
- combined_memory = original_memory + new_memory
212
- memory_map = {
213
- transform_name_to_key(name=mem_obj.memory): mem_obj for mem_obj in combined_memory
214
- }
215
- combined_text_memory = [transform_name_to_key(name=m.memory) for m in combined_memory]
216
-
217
- # apply filters
218
- filtered_combined_text_memory = self.filter_similar_memories(
219
- text_memories=combined_text_memory,
220
- similarity_threshold=self.filter_similarity_threshold,
221
- )
222
-
223
- filtered_combined_text_memory = self.filter_too_short_memories(
224
- text_memories=filtered_combined_text_memory,
225
- min_length_threshold=self.filter_min_length_threshold,
226
- )
126
+ ) -> list[TextualMemoryItem] | None:
127
+ """
128
+ Process and rerank memory items by combining original and new memories,
129
+ applying filters, and then reranking based on relevance to queries.
227
130
 
228
- unique_memory = list(dict.fromkeys(filtered_combined_text_memory))
131
+ Args:
132
+ queries: List of query strings to rerank memories against
133
+ original_memory: List of original TextualMemoryItem objects
134
+ new_memory: List of new TextualMemoryItem objects to merge
135
+ top_k: Maximum number of memories to return after reranking
229
136
 
230
- try:
231
- prompt = self.build_prompt(
232
- "memory_reranking",
233
- queries=queries,
234
- current_order=unique_memory,
235
- staging_buffer=[],
137
+ Returns:
138
+ List of reranked TextualMemoryItem objects, or None if processing fails
139
+ """
140
+ # Combine original and new memories into a single list
141
+ combined_memory = original_memory + new_memory
142
+
143
+ # Create a mapping from normalized text to memory objects
144
+ memory_map = {
145
+ transform_name_to_key(name=mem_obj.memory): mem_obj for mem_obj in combined_memory
146
+ }
147
+
148
+ # Extract normalized text representations from all memory items
149
+ combined_text_memory = [m.memory for m in combined_memory]
150
+
151
+ # Apply similarity filter to remove overly similar memories
152
+ filtered_combined_text_memory = filter_similar_memories(
153
+ text_memories=combined_text_memory,
154
+ similarity_threshold=self.filter_similarity_threshold,
155
+ )
156
+
157
+ # Apply length filter to remove memories that are too short
158
+ filtered_combined_text_memory = filter_too_short_memories(
159
+ text_memories=filtered_combined_text_memory,
160
+ min_length_threshold=self.filter_min_length_threshold,
161
+ )
162
+
163
+ # Ensure uniqueness of memory texts using dictionary keys (preserves order)
164
+ unique_memory = list(dict.fromkeys(filtered_combined_text_memory))
165
+
166
+ # Rerank the filtered memories based on relevance to the queries
167
+ text_memories_with_new_order, success_flag = self.rerank_memories(
168
+ queries=queries,
169
+ original_memories=unique_memory,
170
+ top_k=top_k,
171
+ )
172
+
173
+ # Map reranked text entries back to their original memory objects
174
+ memories_with_new_order = []
175
+ for text in text_memories_with_new_order:
176
+ normalized_text = transform_name_to_key(name=text)
177
+ if normalized_text in memory_map: # Ensure correct key matching
178
+ memories_with_new_order.append(memory_map[normalized_text])
179
+ else:
180
+ logger.warning(
181
+ f"Memory text not found in memory map. text: {text};\n"
182
+ f"Keys of memory_map: {memory_map.keys()}"
236
183
  )
237
- response = self.process_llm.generate([{"role": "user", "content": prompt}])
238
- response = extract_json_dict(response)
239
- text_memories_with_new_order = response.get("new_order", [])[:top_k]
240
- except Exception as e:
241
- logger.error(f"Fail to rerank with LLM, Exeption: {e}.", exc_info=True)
242
- text_memories_with_new_order = unique_memory[:top_k]
243
-
244
- memories_with_new_order = []
245
- for text in text_memories_with_new_order:
246
- normalized_text = transform_name_to_key(name=text)
247
- if text in memory_map:
248
- memories_with_new_order.append(memory_map[normalized_text])
249
- else:
250
- logger.warning(
251
- f"Memory text not found in memory map. text: {text}; keys of memory_map: {memory_map.keys()}"
252
- )
253
-
254
- text_mem_base.replace_working_memory(memories_with_new_order)
255
- logger.info(
256
- f"The working memory has been replaced with {len(memories_with_new_order)} new memories."
257
- )
258
- self.log_working_memory_replacement(
259
- original_memory=original_memory,
260
- new_memory=memories_with_new_order,
261
- user_id=user_id,
262
- mem_cube_id=mem_cube_id,
263
- mem_cube=mem_cube,
264
- )
265
- else:
266
- logger.error("memory_base is not supported")
267
184
 
268
- return memories_with_new_order
185
+ return memories_with_new_order, success_flag