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.
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/METADATA +7 -1
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/RECORD +87 -64
- memos/__init__.py +1 -1
- memos/api/config.py +158 -69
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +101 -0
- memos/api/product_models.py +5 -1
- memos/api/routers/product_router.py +54 -26
- memos/configs/graph_db.py +49 -1
- memos/configs/internet_retriever.py +19 -0
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +54 -18
- memos/configs/mem_user.py +58 -0
- memos/graph_dbs/base.py +38 -3
- memos/graph_dbs/factory.py +2 -0
- memos/graph_dbs/nebular.py +1612 -0
- memos/graph_dbs/neo4j.py +18 -9
- memos/log.py +6 -1
- memos/mem_cube/utils.py +13 -6
- memos/mem_os/core.py +157 -37
- memos/mem_os/main.py +2 -2
- memos/mem_os/product.py +252 -201
- memos/mem_os/utils/default_config.py +1 -1
- memos/mem_os/utils/format_utils.py +281 -70
- memos/mem_os/utils/reference_utils.py +133 -0
- memos/mem_reader/simple_struct.py +13 -5
- memos/mem_scheduler/base_scheduler.py +239 -266
- memos/mem_scheduler/{modules → general_modules}/base.py +4 -5
- memos/mem_scheduler/{modules → general_modules}/dispatcher.py +57 -21
- memos/mem_scheduler/general_modules/misc.py +104 -0
- memos/mem_scheduler/{modules → general_modules}/rabbitmq_service.py +12 -10
- memos/mem_scheduler/{modules → general_modules}/redis_service.py +1 -1
- memos/mem_scheduler/general_modules/retriever.py +199 -0
- memos/mem_scheduler/general_modules/scheduler_logger.py +261 -0
- memos/mem_scheduler/general_scheduler.py +243 -80
- memos/mem_scheduler/monitors/__init__.py +0 -0
- memos/mem_scheduler/monitors/dispatcher_monitor.py +305 -0
- memos/mem_scheduler/{modules/monitor.py → monitors/general_monitor.py} +106 -57
- memos/mem_scheduler/mos_for_test_scheduler.py +23 -20
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +44 -0
- memos/mem_scheduler/schemas/message_schemas.py +149 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +337 -0
- memos/mem_scheduler/utils/__init__.py +0 -0
- memos/mem_scheduler/utils/filter_utils.py +176 -0
- memos/mem_scheduler/utils/misc_utils.py +102 -0
- memos/mem_user/factory.py +94 -0
- memos/mem_user/mysql_persistent_user_manager.py +271 -0
- memos/mem_user/mysql_user_manager.py +500 -0
- memos/mem_user/persistent_factory.py +96 -0
- memos/mem_user/user_manager.py +4 -4
- memos/memories/activation/item.py +5 -1
- memos/memories/activation/kv.py +20 -8
- memos/memories/textual/base.py +2 -2
- memos/memories/textual/general.py +36 -92
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +13 -7
- memos/memories/textual/tree_text_memory/organize/{conflict.py → handler.py} +34 -50
- memos/memories/textual/tree_text_memory/organize/manager.py +8 -96
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +49 -43
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +107 -142
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +229 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +11 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +15 -8
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +1 -1
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +191 -116
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +47 -15
- memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
- memos/memos_tools/dinding_report_bot.py +422 -0
- memos/memos_tools/lockfree_dict.py +120 -0
- memos/memos_tools/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +96 -0
- memos/memos_tools/thread_safe_dict.py +288 -0
- memos/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +4 -1
- memos/templates/mem_scheduler_prompts.py +62 -15
- memos/templates/mos_prompts.py +116 -0
- memos/templates/tree_reorganize_prompts.py +24 -17
- memos/utils.py +19 -0
- memos/mem_scheduler/modules/misc.py +0 -39
- memos/mem_scheduler/modules/retriever.py +0 -268
- memos/mem_scheduler/modules/schemas.py +0 -328
- memos/mem_scheduler/utils.py +0 -75
- memos/memories/textual/tree_text_memory/organize/redundancy.py +0 -193
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/LICENSE +0 -0
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/WHEEL +0 -0
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/entry_points.txt +0 -0
- /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.
|
|
9
|
-
from memos.mem_scheduler.
|
|
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.
|
|
20
|
-
|
|
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
|
|
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",
|
|
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 =
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
self.
|
|
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.
|
|
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
|
-
#
|
|
74
|
-
#
|
|
75
|
-
if
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
215
|
+
now = datetime.utcnow()
|
|
167
216
|
elapsed = (now - last_time).total_seconds()
|
|
168
217
|
if elapsed >= interval_seconds:
|
|
169
218
|
return True
|
|
170
|
-
logger.
|
|
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
|
-
|
|
212
|
-
sorted_text_memories = [m.memory_text for m in
|
|
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.
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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,
|
|
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
|