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

Files changed (92) hide show
  1. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/METADATA +7 -1
  2. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/RECORD +87 -64
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +158 -69
  5. memos/api/context/context.py +147 -0
  6. memos/api/context/dependencies.py +101 -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 +19 -0
  11. memos/configs/mem_os.py +5 -0
  12. memos/configs/mem_reader.py +9 -0
  13. memos/configs/mem_scheduler.py +54 -18
  14. memos/configs/mem_user.py +58 -0
  15. memos/graph_dbs/base.py +38 -3
  16. memos/graph_dbs/factory.py +2 -0
  17. memos/graph_dbs/nebular.py +1612 -0
  18. memos/graph_dbs/neo4j.py +18 -9
  19. memos/log.py +6 -1
  20. memos/mem_cube/utils.py +13 -6
  21. memos/mem_os/core.py +157 -37
  22. memos/mem_os/main.py +2 -2
  23. memos/mem_os/product.py +252 -201
  24. memos/mem_os/utils/default_config.py +1 -1
  25. memos/mem_os/utils/format_utils.py +281 -70
  26. memos/mem_os/utils/reference_utils.py +133 -0
  27. memos/mem_reader/simple_struct.py +13 -5
  28. memos/mem_scheduler/base_scheduler.py +239 -266
  29. memos/mem_scheduler/{modules → general_modules}/base.py +4 -5
  30. memos/mem_scheduler/{modules → general_modules}/dispatcher.py +57 -21
  31. memos/mem_scheduler/general_modules/misc.py +104 -0
  32. memos/mem_scheduler/{modules → general_modules}/rabbitmq_service.py +12 -10
  33. memos/mem_scheduler/{modules → general_modules}/redis_service.py +1 -1
  34. memos/mem_scheduler/general_modules/retriever.py +199 -0
  35. memos/mem_scheduler/general_modules/scheduler_logger.py +261 -0
  36. memos/mem_scheduler/general_scheduler.py +243 -80
  37. memos/mem_scheduler/monitors/__init__.py +0 -0
  38. memos/mem_scheduler/monitors/dispatcher_monitor.py +305 -0
  39. memos/mem_scheduler/{modules/monitor.py → monitors/general_monitor.py} +106 -57
  40. memos/mem_scheduler/mos_for_test_scheduler.py +23 -20
  41. memos/mem_scheduler/schemas/__init__.py +0 -0
  42. memos/mem_scheduler/schemas/general_schemas.py +44 -0
  43. memos/mem_scheduler/schemas/message_schemas.py +149 -0
  44. memos/mem_scheduler/schemas/monitor_schemas.py +337 -0
  45. memos/mem_scheduler/utils/__init__.py +0 -0
  46. memos/mem_scheduler/utils/filter_utils.py +176 -0
  47. memos/mem_scheduler/utils/misc_utils.py +102 -0
  48. memos/mem_user/factory.py +94 -0
  49. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  50. memos/mem_user/mysql_user_manager.py +500 -0
  51. memos/mem_user/persistent_factory.py +96 -0
  52. memos/mem_user/user_manager.py +4 -4
  53. memos/memories/activation/item.py +5 -1
  54. memos/memories/activation/kv.py +20 -8
  55. memos/memories/textual/base.py +2 -2
  56. memos/memories/textual/general.py +36 -92
  57. memos/memories/textual/item.py +5 -33
  58. memos/memories/textual/tree.py +13 -7
  59. memos/memories/textual/tree_text_memory/organize/{conflict.py → handler.py} +34 -50
  60. memos/memories/textual/tree_text_memory/organize/manager.py +8 -96
  61. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +49 -43
  62. memos/memories/textual/tree_text_memory/organize/reorganizer.py +107 -142
  63. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +229 -0
  64. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  65. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +11 -0
  66. memos/memories/textual/tree_text_memory/retrieve/recall.py +15 -8
  67. memos/memories/textual/tree_text_memory/retrieve/reranker.py +1 -1
  68. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  69. memos/memories/textual/tree_text_memory/retrieve/searcher.py +191 -116
  70. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +47 -15
  71. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  72. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  73. memos/memos_tools/dinding_report_bot.py +422 -0
  74. memos/memos_tools/lockfree_dict.py +120 -0
  75. memos/memos_tools/notification_service.py +44 -0
  76. memos/memos_tools/notification_utils.py +96 -0
  77. memos/memos_tools/thread_safe_dict.py +288 -0
  78. memos/settings.py +3 -1
  79. memos/templates/mem_reader_prompts.py +4 -1
  80. memos/templates/mem_scheduler_prompts.py +62 -15
  81. memos/templates/mos_prompts.py +116 -0
  82. memos/templates/tree_reorganize_prompts.py +24 -17
  83. memos/utils.py +19 -0
  84. memos/mem_scheduler/modules/misc.py +0 -39
  85. memos/mem_scheduler/modules/retriever.py +0 -268
  86. memos/mem_scheduler/modules/schemas.py +0 -328
  87. memos/mem_scheduler/utils.py +0 -75
  88. memos/memories/textual/tree_text_memory/organize/redundancy.py +0 -193
  89. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/LICENSE +0 -0
  90. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/WHEEL +0 -0
  91. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/entry_points.txt +0 -0
  92. /memos/mem_scheduler/{modules → general_modules}/__init__.py +0 -0
@@ -0,0 +1,305 @@
1
+ import threading
2
+ import time
3
+
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from datetime import datetime
6
+ from time import perf_counter
7
+
8
+ from memos.configs.mem_scheduler import BaseSchedulerConfig
9
+ from memos.log import get_logger
10
+ from memos.mem_scheduler.general_modules.base import BaseSchedulerModule
11
+ from memos.mem_scheduler.general_modules.dispatcher import SchedulerDispatcher
12
+
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class SchedulerDispatcherMonitor(BaseSchedulerModule):
18
+ """Monitors and manages scheduling operations with LLM integration."""
19
+
20
+ def __init__(self, config: BaseSchedulerConfig):
21
+ super().__init__()
22
+ self.config: BaseSchedulerConfig = config
23
+
24
+ self.check_interval = self.config.get("dispatcher_monitor_check_interval", 60)
25
+ self.max_failures = self.config.get("dispatcher_monitor_max_failures", 2)
26
+
27
+ # Registry of monitored thread pools
28
+ self._pools: dict[str, dict] = {}
29
+ self._pool_lock = threading.Lock()
30
+
31
+ # thread pool monitor
32
+ self._monitor_thread: threading.Thread | None = None
33
+ self._running = False
34
+ self._restart_in_progress = False
35
+
36
+ # modules with thread pool
37
+ self.dispatcher: SchedulerDispatcher | None = None
38
+ self.dispatcher_pool_name = "dispatcher"
39
+
40
+ def initialize(self, dispatcher: SchedulerDispatcher):
41
+ self.dispatcher = dispatcher
42
+ self.register_pool(
43
+ name=self.dispatcher_pool_name,
44
+ executor=self.dispatcher.dispatcher_executor,
45
+ max_workers=self.dispatcher.max_workers,
46
+ restart_on_failure=True,
47
+ )
48
+
49
+ def register_pool(
50
+ self,
51
+ name: str,
52
+ executor: ThreadPoolExecutor,
53
+ max_workers: int,
54
+ restart_on_failure: bool = True,
55
+ ) -> bool:
56
+ """
57
+ Register a thread pool for monitoring.
58
+
59
+ Args:
60
+ name: Unique identifier for the pool
61
+ executor: ThreadPoolExecutor instance to monitor
62
+ max_workers: Expected maximum worker count
63
+ restart_on_failure: Whether to restart if pool fails
64
+
65
+ Returns:
66
+ bool: True if registration succeeded, False if pool already registered
67
+ """
68
+ with self._pool_lock:
69
+ if name in self._pools:
70
+ logger.warning(f"Thread pool '{name}' is already registered")
71
+ return False
72
+
73
+ self._pools[name] = {
74
+ "executor": executor,
75
+ "max_workers": max_workers,
76
+ "restart": restart_on_failure,
77
+ "failure_count": 0,
78
+ "last_active": datetime.utcnow(),
79
+ "healthy": True,
80
+ }
81
+ logger.info(f"Registered thread pool '{name}' for monitoring")
82
+ return True
83
+
84
+ def unregister_pool(self, name: str) -> bool:
85
+ """
86
+ Remove a thread pool from monitoring.
87
+
88
+ Args:
89
+ name: Identifier of the pool to remove
90
+
91
+ Returns:
92
+ bool: True if removal succeeded, False if pool not found
93
+ """
94
+ with self._pool_lock:
95
+ if name not in self._pools:
96
+ logger.warning(f"Thread pool '{name}' not found in registry")
97
+ return False
98
+
99
+ del self._pools[name]
100
+ logger.info(f"Unregistered thread pool '{name}'")
101
+ return True
102
+
103
+ def _monitor_loop(self) -> None:
104
+ """Main monitoring loop that periodically checks all registered pools."""
105
+ logger.info(f"Starting monitor loop with {self.check_interval} second interval")
106
+
107
+ while self._running:
108
+ time.sleep(self.check_interval)
109
+ try:
110
+ self._check_pools_health()
111
+ except Exception as e:
112
+ logger.error(f"Error during health check: {e!s}", exc_info=True)
113
+
114
+ logger.debug("Monitor loop exiting")
115
+
116
+ def start(self) -> bool:
117
+ """
118
+ Start the monitoring thread.
119
+
120
+ Returns:
121
+ bool: True if monitor started successfully, False if already running
122
+ """
123
+ if self._running:
124
+ logger.warning("Dispatcher Monitor is already running")
125
+ return False
126
+
127
+ self._running = True
128
+ self._monitor_thread = threading.Thread(
129
+ target=self._monitor_loop, name="threadpool_monitor", daemon=True
130
+ )
131
+ self._monitor_thread.start()
132
+ logger.info("Dispatcher Monitor monitor started")
133
+ return True
134
+
135
+ def stop(self) -> None:
136
+ """
137
+ Stop the monitoring thread and clean up all managed thread pools.
138
+ Ensures proper shutdown of all monitored executors.
139
+ """
140
+ if not self._running:
141
+ return
142
+
143
+ # Stop the monitoring loop
144
+ self._running = False
145
+ if self._monitor_thread and self._monitor_thread.is_alive():
146
+ self._monitor_thread.join(timeout=5)
147
+
148
+ # Shutdown all registered pools
149
+ with self._pool_lock:
150
+ for name, pool_info in self._pools.items():
151
+ executor = pool_info["executor"]
152
+ if not executor._shutdown: # pylint: disable=protected-access
153
+ try:
154
+ logger.info(f"Shutting down thread pool '{name}'")
155
+ executor.shutdown(wait=True, cancel_futures=True)
156
+ logger.info(f"Successfully shut down thread pool '{name}'")
157
+ except Exception as e:
158
+ logger.error(f"Error shutting down pool '{name}': {e!s}", exc_info=True)
159
+
160
+ # Clear the pool registry
161
+ self._pools.clear()
162
+ logger.info("Thread pool monitor and all pools stopped")
163
+
164
+ def _check_pools_health(self) -> None:
165
+ """Check health of all registered thread pools."""
166
+ for name, pool_info in list(self._pools.items()):
167
+ is_healthy, reason = self._check_pool_health(
168
+ pool_info=pool_info,
169
+ stuck_max_interval=4,
170
+ )
171
+ logger.info(f"Pool '{name}'. is_healthy: {is_healthy}. pool_info: {pool_info}")
172
+ with self._pool_lock:
173
+ if is_healthy:
174
+ pool_info["failure_count"] = 0
175
+ pool_info["healthy"] = True
176
+ return
177
+ else:
178
+ pool_info["failure_count"] += 1
179
+ pool_info["healthy"] = False
180
+ logger.warning(
181
+ f"Pool '{name}' unhealthy ({pool_info['failure_count']}/{self.max_failures}): {reason}"
182
+ )
183
+
184
+ if (
185
+ pool_info["failure_count"] >= self.max_failures
186
+ and pool_info["restart"]
187
+ and not self._restart_in_progress
188
+ ):
189
+ self._restart_pool(name, pool_info)
190
+
191
+ def _check_pool_health(self, pool_info: dict, stuck_max_interval=4) -> tuple[bool, str]:
192
+ """
193
+ Check health of a single thread pool.
194
+
195
+ Args:
196
+ pool_info: Dictionary containing pool configuration
197
+
198
+ Returns:
199
+ Tuple: (is_healthy, reason) where reason explains failure if not healthy
200
+ """
201
+ executor = pool_info["executor"]
202
+
203
+ # Check if executor is shutdown
204
+ if executor._shutdown: # pylint: disable=protected-access
205
+ return False, "Executor is shutdown"
206
+
207
+ # Check thread activity
208
+ active_threads = sum(
209
+ 1
210
+ for t in threading.enumerate()
211
+ if t.name.startswith(executor._thread_name_prefix) # pylint: disable=protected-access
212
+ )
213
+
214
+ # Check if no threads are active but should be
215
+ if active_threads == 0 and pool_info["max_workers"] > 0:
216
+ return False, "No active worker threads"
217
+
218
+ # Check if threads are stuck (no activity for 2 intervals)
219
+ time_delta = (datetime.utcnow() - pool_info["last_active"]).total_seconds()
220
+ if time_delta >= self.check_interval * stuck_max_interval:
221
+ return False, "No recent activity"
222
+
223
+ # If we got here, pool appears healthy
224
+ pool_info["last_active"] = datetime.utcnow()
225
+ return True, ""
226
+
227
+ def _restart_pool(self, name: str, pool_info: dict) -> None:
228
+ """
229
+ Attempt to restart a failed thread pool.
230
+
231
+ Args:
232
+ name: Name of the pool to restart
233
+ pool_info: Dictionary containing pool configuration
234
+ """
235
+ if self._restart_in_progress:
236
+ return
237
+
238
+ self._restart_in_progress = True
239
+ logger.warning(f"Attempting to restart thread pool '{name}'")
240
+
241
+ try:
242
+ old_executor = pool_info["executor"]
243
+ self.dispatcher.shutdown()
244
+
245
+ # Create new executor with same parameters
246
+ new_executor = ThreadPoolExecutor(
247
+ max_workers=pool_info["max_workers"],
248
+ thread_name_prefix=self.dispatcher.thread_name_prefix, # pylint: disable=protected-access
249
+ )
250
+ self.unregister_pool(name=self.dispatcher_pool_name)
251
+ self.dispatcher.dispatcher_executor = new_executor
252
+ self.register_pool(
253
+ name=self.dispatcher_pool_name,
254
+ executor=self.dispatcher.dispatcher_executor,
255
+ max_workers=self.dispatcher.max_workers,
256
+ restart_on_failure=True,
257
+ )
258
+
259
+ # Replace in registry
260
+ start_time = perf_counter()
261
+ with self._pool_lock:
262
+ pool_info["executor"] = new_executor
263
+ pool_info["failure_count"] = 0
264
+ pool_info["healthy"] = True
265
+ pool_info["last_active"] = datetime.utcnow()
266
+
267
+ elapsed_time = perf_counter() - start_time
268
+ if elapsed_time > 1:
269
+ logger.warning(f"Long lock wait: {elapsed_time:.3f}s")
270
+
271
+ # Shutdown old executor
272
+ try:
273
+ old_executor.shutdown(wait=False)
274
+ except Exception as e:
275
+ logger.error(f"Error shutting down old executor: {e!s}", exc_info=True)
276
+
277
+ logger.info(f"Successfully restarted thread pool '{name}'")
278
+ except Exception as e:
279
+ logger.error(f"Failed to restart pool '{name}': {e!s}", exc_info=True)
280
+ finally:
281
+ self._restart_in_progress = False
282
+
283
+ def get_status(self, name: str | None = None) -> dict:
284
+ """
285
+ Get status of monitored pools.
286
+
287
+ Args:
288
+ name: Optional specific pool name to check
289
+
290
+ Returns:
291
+ Dictionary of status information
292
+ """
293
+ with self._pool_lock:
294
+ if name:
295
+ return {name: self._pools.get(name, {}).copy()}
296
+ return {k: v.copy() for k, v in self._pools.items()}
297
+
298
+ def __enter__(self):
299
+ """Context manager entry point."""
300
+ self.start()
301
+ return self
302
+
303
+ def __exit__(self, exc_type, exc_val, exc_tb):
304
+ """Context manager exit point."""
305
+ self.stop()
@@ -1,29 +1,35 @@
1
1
  from datetime import datetime
2
+ from threading import Lock
2
3
  from typing import Any
3
4
 
4
5
  from memos.configs.mem_scheduler import BaseSchedulerConfig
5
6
  from memos.llms.base import BaseLLM
6
7
  from memos.log import get_logger
7
8
  from memos.mem_cube.general import GeneralMemCube
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.general_modules.base import BaseSchedulerModule
10
+ from memos.mem_scheduler.schemas.general_schemas import (
11
11
  DEFAULT_ACTIVATION_MEM_MONITOR_SIZE_LIMIT,
12
+ DEFAULT_WEIGHT_VECTOR_FOR_RANKING,
12
13
  DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT,
13
14
  MONITOR_ACTIVATION_MEMORY_TYPE,
14
15
  MONITOR_WORKING_MEMORY_TYPE,
15
16
  MemCubeID,
16
- MemoryMonitorManager,
17
17
  UserID,
18
18
  )
19
- from memos.mem_scheduler.utils import extract_json_dict
20
- from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
19
+ from memos.mem_scheduler.schemas.monitor_schemas import (
20
+ MemoryMonitorItem,
21
+ MemoryMonitorManager,
22
+ QueryMonitorItem,
23
+ QueryMonitorQueue,
24
+ )
25
+ from memos.mem_scheduler.utils.misc_utils import extract_json_dict
26
+ from memos.memories.textual.tree import TreeTextMemory
21
27
 
22
28
 
23
29
  logger = get_logger(__name__)
24
30
 
25
31
 
26
- class SchedulerMonitor(BaseSchedulerModule):
32
+ class SchedulerGeneralMonitor(BaseSchedulerModule):
27
33
  """Monitors and manages scheduling operations with LLM integration."""
28
34
 
29
35
  def __init__(self, process_llm: BaseLLM, config: BaseSchedulerConfig):
@@ -31,33 +37,79 @@ class SchedulerMonitor(BaseSchedulerModule):
31
37
 
32
38
  # hyper-parameters
33
39
  self.config: BaseSchedulerConfig = config
34
- self.act_mem_update_interval = self.config.get("act_mem_update_interval", 300)
40
+ self.act_mem_update_interval = self.config.get("act_mem_update_interval", 30)
41
+ self.query_trigger_interval = self.config.get("query_trigger_interval", 10)
35
42
 
36
43
  # Partial Retention Strategy
37
44
  self.partial_retention_number = 2
38
- self.working_mem_monitor_capacity = DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT
39
- self.activation_mem_monitor_capacity = DEFAULT_ACTIVATION_MEM_MONITOR_SIZE_LIMIT
45
+ self.working_mem_monitor_capacity = self.config.get(
46
+ "working_mem_monitor_capacity", DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT
47
+ )
48
+ self.activation_mem_monitor_capacity = self.config.get(
49
+ "activation_mem_monitor_capacity", DEFAULT_ACTIVATION_MEM_MONITOR_SIZE_LIMIT
50
+ )
40
51
 
41
52
  # attributes
42
- self.query_history = Queue(maxsize=self.config.context_window_size)
43
- self.intent_history = Queue(maxsize=self.config.context_window_size)
53
+ # recording query_messages
54
+ self.query_monitors: dict[UserID, dict[MemCubeID, QueryMonitorQueue[QueryMonitorItem]]] = {}
55
+
44
56
  self.working_memory_monitors: dict[UserID, dict[MemCubeID, MemoryMonitorManager]] = {}
45
57
  self.activation_memory_monitors: dict[UserID, dict[MemCubeID, MemoryMonitorManager]] = {}
46
58
 
47
59
  # Lifecycle monitor
48
- self._last_activation_mem_update_time = datetime.min
60
+ self.last_activation_mem_update_time = datetime.min
61
+ self.last_query_consume_time = datetime.min
49
62
 
63
+ self._register_lock = Lock()
50
64
  self._process_llm = process_llm
51
65
 
66
+ def extract_query_keywords(self, query: str) -> list:
67
+ """Extracts core keywords from a user query based on specific semantic rules."""
68
+ prompt_name = "query_keywords_extraction"
69
+ prompt = self.build_prompt(
70
+ template_name=prompt_name,
71
+ query=query,
72
+ )
73
+ llm_response = self._process_llm.generate([{"role": "user", "content": prompt}])
74
+ try:
75
+ # Parse JSON output from LLM response
76
+ keywords = extract_json_dict(llm_response)
77
+ assert isinstance(keywords, list)
78
+ except Exception as e:
79
+ logger.error(
80
+ f"Failed to parse keywords from LLM response: {llm_response}. Error: {e!s}"
81
+ )
82
+ keywords = [query]
83
+ return keywords
84
+
85
+ def register_query_monitor_if_not_exists(
86
+ self,
87
+ user_id: UserID | str,
88
+ mem_cube_id: MemCubeID | str,
89
+ ) -> None:
90
+ # First check (lock-free, fast path)
91
+ if user_id in self.query_monitors and mem_cube_id in self.query_monitors[user_id]:
92
+ return
93
+
94
+ # Second check (with lock, ensures uniqueness)
95
+ with self._register_lock:
96
+ if user_id not in self.query_monitors:
97
+ self.query_monitors[user_id] = {}
98
+ if mem_cube_id not in self.query_monitors[user_id]:
99
+ self.query_monitors[user_id][mem_cube_id] = QueryMonitorQueue(
100
+ maxsize=self.config.context_window_size
101
+ )
102
+
52
103
  def register_memory_manager_if_not_exists(
53
104
  self,
54
- user_id: str,
55
- mem_cube_id: str,
105
+ user_id: UserID | str,
106
+ mem_cube_id: MemCubeID | str,
56
107
  memory_monitors: dict[UserID, dict[MemCubeID, MemoryMonitorManager]],
57
108
  max_capacity: int,
58
109
  ) -> None:
59
110
  """
60
111
  Register a new MemoryMonitorManager for the given user and memory cube if it doesn't exist.
112
+ Thread-safe implementation using double-checked locking pattern.
61
113
 
62
114
  Checks if a MemoryMonitorManager already exists for the specified user_id and mem_cube_id.
63
115
  If not, creates a new MemoryMonitorManager with appropriate capacity settings and registers it.
@@ -65,14 +117,34 @@ class SchedulerMonitor(BaseSchedulerModule):
65
117
  Args:
66
118
  user_id: The ID of the user to associate with the memory manager
67
119
  mem_cube_id: The ID of the memory cube to monitor
120
+ memory_monitors: Dictionary storing existing memory monitor managers
121
+ max_capacity: Maximum capacity for the new memory monitor manager
122
+ lock: Threading lock to ensure safe concurrent access
68
123
 
69
124
  Note:
70
125
  This function will update the loose_max_working_memory_capacity based on the current
71
126
  WorkingMemory size plus partial retention number before creating a new manager.
72
127
  """
73
- # Check if a MemoryMonitorManager already exists for the current user_id and mem_cube_id
74
- # If doesn't exist, create and register a new one
75
- if (user_id not in memory_monitors) or (mem_cube_id not in memory_monitors[user_id]):
128
+ # First check (lock-free, fast path)
129
+ # Quickly verify existence without lock overhead
130
+ if user_id in memory_monitors and mem_cube_id in memory_monitors[user_id]:
131
+ logger.info(
132
+ f"MemoryMonitorManager already exists for user_id={user_id}, "
133
+ f"mem_cube_id={mem_cube_id} in the provided memory_monitors dictionary"
134
+ )
135
+ return
136
+
137
+ # Second check (with lock, ensures uniqueness)
138
+ # Acquire lock before modification and verify again to prevent race conditions
139
+ with self._register_lock:
140
+ # Re-check after acquiring lock, as another thread might have created it
141
+ if user_id in memory_monitors and mem_cube_id in memory_monitors[user_id]:
142
+ logger.info(
143
+ f"MemoryMonitorManager already exists for user_id={user_id}, "
144
+ f"mem_cube_id={mem_cube_id} in the provided memory_monitors dictionary"
145
+ )
146
+ return
147
+
76
148
  # Initialize MemoryMonitorManager with user ID, memory cube ID, and max capacity
77
149
  monitor_manager = MemoryMonitorManager(
78
150
  user_id=user_id, mem_cube_id=mem_cube_id, max_capacity=max_capacity
@@ -84,19 +156,16 @@ class SchedulerMonitor(BaseSchedulerModule):
84
156
  f"Registered new MemoryMonitorManager for user_id={user_id},"
85
157
  f" mem_cube_id={mem_cube_id} with max_capacity={max_capacity}"
86
158
  )
87
- else:
88
- logger.info(
89
- f"MemoryMonitorManager already exists for user_id={user_id}, "
90
- f"mem_cube_id={mem_cube_id} in the provided memory_monitors dictionary"
91
- )
92
159
 
93
- def update_memory_monitors(self, user_id: str, mem_cube_id: str, mem_cube: GeneralMemCube):
160
+ def update_working_memory_monitors(
161
+ self,
162
+ new_working_memory_monitors: list[MemoryMonitorItem],
163
+ user_id: str,
164
+ mem_cube_id: str,
165
+ mem_cube: GeneralMemCube,
166
+ ):
94
167
  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
-
168
+ assert isinstance(text_mem_base, TreeTextMemory)
100
169
  self.working_mem_monitor_capacity = min(
101
170
  DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT,
102
171
  (
@@ -105,17 +174,6 @@ class SchedulerMonitor(BaseSchedulerModule):
105
174
  ),
106
175
  )
107
176
 
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
177
  # register monitors
120
178
  self.register_memory_manager_if_not_exists(
121
179
  user_id=user_id,
@@ -124,14 +182,8 @@ class SchedulerMonitor(BaseSchedulerModule):
124
182
  max_capacity=self.working_mem_monitor_capacity,
125
183
  )
126
184
 
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
185
  self.working_memory_monitors[user_id][mem_cube_id].update_memories(
134
- text_working_memories=text_working_memory,
186
+ new_memory_monitors=new_working_memory_monitors,
135
187
  partial_retention_number=self.partial_retention_number,
136
188
  )
137
189
 
@@ -149,25 +201,22 @@ class SchedulerMonitor(BaseSchedulerModule):
149
201
  # Sort by importance_score in descending order and take top k
150
202
  top_k_memories = sorted(
151
203
  self.working_memory_monitors[user_id][mem_cube_id].memories,
152
- key=lambda m: m.get_score(),
204
+ key=lambda m: m.get_importance_score(weight_vector=DEFAULT_WEIGHT_VECTOR_FOR_RANKING),
153
205
  reverse=True,
154
206
  )[: self.activation_mem_monitor_capacity]
155
207
 
156
- # Extract just the text from these memories
157
- text_top_k_memories = [m.memory_text for m in top_k_memories]
158
-
159
208
  # Update the activation memory monitors with these important memories
160
209
  self.activation_memory_monitors[user_id][mem_cube_id].update_memories(
161
- text_working_memories=text_top_k_memories,
210
+ new_memory_monitors=top_k_memories,
162
211
  partial_retention_number=self.partial_retention_number,
163
212
  )
164
213
 
165
214
  def timed_trigger(self, last_time: datetime, interval_seconds: float) -> bool:
166
- now = datetime.now()
215
+ now = datetime.utcnow()
167
216
  elapsed = (now - last_time).total_seconds()
168
217
  if elapsed >= interval_seconds:
169
218
  return True
170
- logger.debug(f"Time trigger not ready, {elapsed:.1f}s elapsed (needs {interval_seconds}s)")
219
+ logger.info(f"Time trigger not ready, {elapsed:.1f}s elapsed (needs {interval_seconds}s)")
171
220
  return False
172
221
 
173
222
  def get_monitor_memories(
@@ -206,10 +255,10 @@ class SchedulerMonitor(BaseSchedulerModule):
206
255
  )
207
256
  return []
208
257
 
209
- manager = monitor_dict[user_id][mem_cube_id]
258
+ manager: MemoryMonitorManager = monitor_dict[user_id][mem_cube_id]
210
259
  # 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]]
260
+ sorted_memory_monitors = manager.get_sorted_mem_monitors(reverse=True)
261
+ sorted_text_memories = [m.memory_text for m in sorted_memory_monitors[:top_k]]
213
262
  return sorted_text_memories
214
263
 
215
264
  def get_monitors_info(self, user_id: str, mem_cube_id: str) -> dict[str, Any]:
@@ -3,12 +3,12 @@ from datetime import datetime
3
3
  from memos.configs.mem_os import MOSConfig
4
4
  from memos.log import get_logger
5
5
  from memos.mem_os.main import MOS
6
- from memos.mem_scheduler.modules.schemas import (
6
+ from memos.mem_scheduler.schemas.general_schemas import (
7
7
  ANSWER_LABEL,
8
8
  MONITOR_WORKING_MEMORY_TYPE,
9
9
  QUERY_LABEL,
10
- ScheduleMessageItem,
11
10
  )
11
+ from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
12
12
 
13
13
 
14
14
  logger = get_logger(__name__)
@@ -54,24 +54,20 @@ class MOSForTestScheduler(MOS):
54
54
  if not mem_cube.text_mem:
55
55
  continue
56
56
 
57
- # submit message to scheduler
58
- if self.enable_mem_scheduler and self.mem_scheduler is not None:
59
- message_item = ScheduleMessageItem(
60
- user_id=target_user_id,
61
- mem_cube_id=mem_cube_id,
62
- mem_cube=mem_cube,
63
- label=QUERY_LABEL,
64
- content=query,
65
- timestamp=datetime.now(),
66
- )
67
- self.mem_scheduler.submit_messages(messages=[message_item])
68
-
69
- self.mem_scheduler.monitor.register_memory_manager_if_not_exists(
70
- user_id=user_id,
57
+ message_item = ScheduleMessageItem(
58
+ user_id=target_user_id,
71
59
  mem_cube_id=mem_cube_id,
72
- memory_monitors=self.mem_scheduler.monitor.working_memory_monitors,
73
- max_capacity=self.mem_scheduler.monitor.working_mem_monitor_capacity,
60
+ mem_cube=mem_cube,
61
+ label=QUERY_LABEL,
62
+ content=query,
63
+ timestamp=datetime.now(),
74
64
  )
65
+ cur_working_memories = [m.memory for m in mem_cube.text_mem.get_working_memory()]
66
+ print(f"Working memories before schedule: {cur_working_memories}")
67
+
68
+ # --- force to run mem_scheduler ---
69
+ self.mem_scheduler.monitor.query_trigger_interval = 0
70
+ self.mem_scheduler._query_message_consumer(messages=[message_item])
75
71
 
76
72
  # from scheduler
77
73
  scheduler_memories = self.mem_scheduler.monitor.get_monitor_memories(
@@ -80,13 +76,21 @@ class MOSForTestScheduler(MOS):
80
76
  memory_type=MONITOR_WORKING_MEMORY_TYPE,
81
77
  top_k=topk_for_scheduler,
82
78
  )
79
+ print(f"Working memories after schedule: {scheduler_memories}")
83
80
  memories_all.extend(scheduler_memories)
84
81
 
85
82
  # from mem_cube
86
83
  memories = mem_cube.text_mem.search(
87
- query, top_k=self.config.top_k - topk_for_scheduler
84
+ query,
85
+ top_k=self.config.top_k - topk_for_scheduler,
86
+ info={
87
+ "user_id": target_user_id,
88
+ "session_id": self.session_id,
89
+ "chat_history": chat_history.chat_history,
90
+ },
88
91
  )
89
92
  text_memories = [m.memory for m in memories]
93
+ print(f"Search results with new working memories: {text_memories}")
90
94
  memories_all.extend(text_memories)
91
95
 
92
96
  memories_all = list(set(memories_all))
@@ -139,5 +143,4 @@ class MOSForTestScheduler(MOS):
139
143
  timestamp=datetime.now(),
140
144
  )
141
145
  self.mem_scheduler.submit_messages(messages=[message_item])
142
-
143
146
  return response
File without changes