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

Files changed (84) hide show
  1. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/METADATA +78 -49
  2. memoryos-0.2.1.dist-info/RECORD +152 -0
  3. memoryos-0.2.1.dist-info/entry_points.txt +3 -0
  4. memos/__init__.py +1 -1
  5. memos/api/config.py +471 -0
  6. memos/api/exceptions.py +28 -0
  7. memos/api/mcp_serve.py +502 -0
  8. memos/api/product_api.py +35 -0
  9. memos/api/product_models.py +159 -0
  10. memos/api/routers/__init__.py +1 -0
  11. memos/api/routers/product_router.py +358 -0
  12. memos/chunkers/sentence_chunker.py +8 -2
  13. memos/cli.py +113 -0
  14. memos/configs/embedder.py +27 -0
  15. memos/configs/graph_db.py +83 -2
  16. memos/configs/llm.py +48 -0
  17. memos/configs/mem_cube.py +1 -1
  18. memos/configs/mem_reader.py +4 -0
  19. memos/configs/mem_scheduler.py +91 -5
  20. memos/configs/memory.py +10 -4
  21. memos/dependency.py +52 -0
  22. memos/embedders/ark.py +92 -0
  23. memos/embedders/factory.py +4 -0
  24. memos/embedders/sentence_transformer.py +8 -2
  25. memos/embedders/universal_api.py +32 -0
  26. memos/graph_dbs/base.py +2 -2
  27. memos/graph_dbs/factory.py +2 -0
  28. memos/graph_dbs/item.py +46 -0
  29. memos/graph_dbs/neo4j.py +377 -101
  30. memos/graph_dbs/neo4j_community.py +300 -0
  31. memos/llms/base.py +9 -0
  32. memos/llms/deepseek.py +54 -0
  33. memos/llms/factory.py +10 -1
  34. memos/llms/hf.py +170 -13
  35. memos/llms/hf_singleton.py +114 -0
  36. memos/llms/ollama.py +4 -0
  37. memos/llms/openai.py +68 -1
  38. memos/llms/qwen.py +63 -0
  39. memos/llms/vllm.py +153 -0
  40. memos/mem_cube/general.py +77 -16
  41. memos/mem_cube/utils.py +102 -0
  42. memos/mem_os/core.py +131 -41
  43. memos/mem_os/main.py +93 -11
  44. memos/mem_os/product.py +1098 -35
  45. memos/mem_os/utils/default_config.py +352 -0
  46. memos/mem_os/utils/format_utils.py +1154 -0
  47. memos/mem_reader/simple_struct.py +13 -8
  48. memos/mem_scheduler/base_scheduler.py +467 -36
  49. memos/mem_scheduler/general_scheduler.py +125 -244
  50. memos/mem_scheduler/modules/base.py +9 -0
  51. memos/mem_scheduler/modules/dispatcher.py +68 -2
  52. memos/mem_scheduler/modules/misc.py +39 -0
  53. memos/mem_scheduler/modules/monitor.py +228 -49
  54. memos/mem_scheduler/modules/rabbitmq_service.py +317 -0
  55. memos/mem_scheduler/modules/redis_service.py +32 -22
  56. memos/mem_scheduler/modules/retriever.py +250 -23
  57. memos/mem_scheduler/modules/schemas.py +189 -7
  58. memos/mem_scheduler/mos_for_test_scheduler.py +143 -0
  59. memos/mem_scheduler/utils.py +51 -2
  60. memos/mem_user/persistent_user_manager.py +260 -0
  61. memos/memories/activation/item.py +25 -0
  62. memos/memories/activation/kv.py +10 -3
  63. memos/memories/activation/vllmkv.py +219 -0
  64. memos/memories/factory.py +2 -0
  65. memos/memories/textual/general.py +7 -5
  66. memos/memories/textual/item.py +3 -1
  67. memos/memories/textual/tree.py +14 -6
  68. memos/memories/textual/tree_text_memory/organize/conflict.py +198 -0
  69. memos/memories/textual/tree_text_memory/organize/manager.py +72 -23
  70. memos/memories/textual/tree_text_memory/organize/redundancy.py +193 -0
  71. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +233 -0
  72. memos/memories/textual/tree_text_memory/organize/reorganizer.py +606 -0
  73. memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
  74. memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
  75. memos/memories/textual/tree_text_memory/retrieve/searcher.py +6 -5
  76. memos/parsers/markitdown.py +8 -2
  77. memos/templates/mem_reader_prompts.py +105 -36
  78. memos/templates/mem_scheduler_prompts.py +96 -47
  79. memos/templates/tree_reorganize_prompts.py +223 -0
  80. memos/vec_dbs/base.py +12 -0
  81. memos/vec_dbs/qdrant.py +46 -20
  82. memoryos-0.1.13.dist-info/RECORD +0 -122
  83. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/LICENSE +0 -0
  84. {memoryos-0.1.13.dist-info → memoryos-0.2.1.dist-info}/WHEEL +0 -0
@@ -16,6 +16,7 @@ from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemory
16
16
  from memos.parsers.factory import ParserFactory
17
17
  from memos.templates.mem_reader_prompts import (
18
18
  SIMPLE_STRUCT_DOC_READER_PROMPT,
19
+ SIMPLE_STRUCT_MEM_READER_EXAMPLE,
19
20
  SIMPLE_STRUCT_MEM_READER_PROMPT,
20
21
  )
21
22
 
@@ -39,11 +40,11 @@ class SimpleStructMemReader(BaseMemReader, ABC):
39
40
  self.chunker = ChunkerFactory.from_config(config.chunker)
40
41
 
41
42
  def _process_chat_data(self, scene_data_info, info):
42
- prompt = (
43
- SIMPLE_STRUCT_MEM_READER_PROMPT.replace("${user_a}", "user")
44
- .replace("${user_b}", "assistant")
45
- .replace("${conversation}", "\n".join(scene_data_info))
43
+ prompt = SIMPLE_STRUCT_MEM_READER_PROMPT.replace(
44
+ "${conversation}", "\n".join(scene_data_info)
46
45
  )
46
+ if self.config.remove_prompt_example:
47
+ prompt = prompt.replace(SIMPLE_STRUCT_MEM_READER_EXAMPLE, "")
47
48
 
48
49
  messages = [{"role": "user", "content": prompt}]
49
50
 
@@ -207,15 +208,15 @@ class SimpleStructMemReader(BaseMemReader, ABC):
207
208
  for i, chunk_res in enumerate(processed_chunks):
208
209
  if chunk_res:
209
210
  node_i = TextualMemoryItem(
210
- memory=chunk_res["summary"],
211
+ memory=chunk_res["value"],
211
212
  metadata=TreeNodeTextualMemoryMetadata(
212
213
  user_id=info.get("user_id"),
213
214
  session_id=info.get("session_id"),
214
215
  memory_type="LongTermMemory",
215
216
  status="activated",
216
217
  tags=chunk_res["tags"],
217
- key="",
218
- embedding=self.embedder.embed([chunk_res["summary"]])[0],
218
+ key=chunk_res["key"],
219
+ embedding=self.embedder.embed([chunk_res["value"]])[0],
219
220
  usage=[],
220
221
  sources=[f"{scene_data_info['file']}_{i}"],
221
222
  background="",
@@ -228,7 +229,11 @@ class SimpleStructMemReader(BaseMemReader, ABC):
228
229
 
229
230
  def parse_json_result(self, response_text):
230
231
  try:
231
- response_text = response_text.replace("```", "").replace("json", "")
232
+ json_start = response_text.find("{")
233
+ response_text = response_text[json_start:]
234
+ response_text = response_text.replace("```", "").strip()
235
+ if response_text[-1] != "}":
236
+ response_text += "}"
232
237
  response_json = json.loads(response_text)
233
238
  return response_json
234
239
  except json.JSONDecodeError as e:
@@ -2,61 +2,283 @@ import queue
2
2
  import threading
3
3
  import time
4
4
 
5
- from abc import abstractmethod
6
- from queue import Queue
5
+ from datetime import datetime
6
+ from pathlib import Path
7
7
 
8
- from memos.configs.mem_scheduler import BaseSchedulerConfig
8
+ from memos.configs.mem_scheduler import AuthConfig, BaseSchedulerConfig
9
9
  from memos.llms.base import BaseLLM
10
10
  from memos.log import get_logger
11
+ from memos.mem_cube.general import GeneralMemCube
11
12
  from memos.mem_scheduler.modules.dispatcher import SchedulerDispatcher
13
+ from memos.mem_scheduler.modules.misc import AutoDroppingQueue as Queue
14
+ from memos.mem_scheduler.modules.monitor import SchedulerMonitor
15
+ from memos.mem_scheduler.modules.rabbitmq_service import RabbitMQSchedulerModule
12
16
  from memos.mem_scheduler.modules.redis_service import RedisSchedulerModule
17
+ from memos.mem_scheduler.modules.retriever import SchedulerRetriever
13
18
  from memos.mem_scheduler.modules.schemas import (
19
+ ACTIVATION_MEMORY_TYPE,
20
+ ADD_LABEL,
21
+ DEFAULT_ACT_MEM_DUMP_PATH,
14
22
  DEFAULT_CONSUME_INTERVAL_SECONDS,
15
23
  DEFAULT_THREAD__POOL_MAX_WORKERS,
24
+ LONG_TERM_MEMORY_TYPE,
25
+ NOT_INITIALIZED,
26
+ PARAMETER_MEMORY_TYPE,
27
+ QUERY_LABEL,
28
+ TEXT_MEMORY_TYPE,
29
+ USER_INPUT_TYPE,
30
+ WORKING_MEMORY_TYPE,
16
31
  ScheduleLogForWebItem,
17
32
  ScheduleMessageItem,
33
+ TreeTextMemory_SEARCH_METHOD,
18
34
  )
35
+ from memos.mem_scheduler.utils import transform_name_to_key
36
+ from memos.memories.activation.kv import KVCacheMemory
37
+ from memos.memories.activation.vllmkv import VLLMKVCacheItem, VLLMKVCacheMemory
38
+ from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
39
+ from memos.templates.mem_scheduler_prompts import MEMORY_ASSEMBLY_TEMPLATE
19
40
 
20
41
 
21
42
  logger = get_logger(__name__)
22
43
 
23
44
 
24
- class BaseScheduler(RedisSchedulerModule):
45
+ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule):
25
46
  """Base class for all mem_scheduler."""
26
47
 
27
48
  def __init__(self, config: BaseSchedulerConfig):
28
49
  """Initialize the scheduler with the given configuration."""
29
50
  super().__init__()
30
51
  self.config = config
52
+
53
+ # hyper-parameters
54
+ self.top_k = self.config.get("top_k", 5)
55
+ self.context_window_size = self.config.get("context_window_size", 5)
56
+ self.enable_act_memory_update = self.config.get("enable_act_memory_update", False)
57
+ self.act_mem_dump_path = self.config.get("act_mem_dump_path", DEFAULT_ACT_MEM_DUMP_PATH)
58
+ self.search_method = TreeTextMemory_SEARCH_METHOD
59
+
60
+ self.enable_parallel_dispatch = self.config.get("enable_parallel_dispatch", False)
31
61
  self.max_workers = self.config.get(
32
62
  "thread_pool_max_workers", DEFAULT_THREAD__POOL_MAX_WORKERS
33
63
  )
34
- self.retriever = None
35
- self.monitor = None
36
- self.enable_parallel_dispatch = self.config.get("enable_parallel_dispatch", False)
64
+
65
+ self.retriever: SchedulerRetriever | None = None
66
+ self.monitor: SchedulerMonitor | None = None
67
+
37
68
  self.dispatcher = SchedulerDispatcher(
38
69
  max_workers=self.max_workers, enable_parallel_dispatch=self.enable_parallel_dispatch
39
70
  )
40
71
 
41
- # message queue
42
- self.memos_message_queue: Queue[ScheduleMessageItem] = Queue()
43
- self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue()
72
+ # internal message queue
73
+ self.max_internal_messae_queue_size = 100
74
+ self.memos_message_queue: Queue[ScheduleMessageItem] = Queue(
75
+ maxsize=self.max_internal_messae_queue_size
76
+ )
77
+ self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue(
78
+ maxsize=self.max_internal_messae_queue_size
79
+ )
44
80
  self._consumer_thread = None # Reference to our consumer thread
45
81
  self._running = False
46
82
  self._consume_interval = self.config.get(
47
83
  "consume_interval_seconds", DEFAULT_CONSUME_INTERVAL_SECONDS
48
84
  )
49
85
 
50
- # others
86
+ # other attributes
87
+ self._context_lock = threading.Lock()
51
88
  self._current_user_id: str | None = None
89
+ self.auth_config_path: str | Path | None = self.config.get("auth_config_path", None)
90
+ self.auth_config = None
91
+ self.rabbitmq_config = None
92
+
93
+ def initialize_modules(self, chat_llm: BaseLLM, process_llm: BaseLLM | None = None):
94
+ if process_llm is None:
95
+ process_llm = chat_llm
96
+
97
+ # initialize submodules
98
+ self.chat_llm = chat_llm
99
+ self.process_llm = process_llm
100
+ self.monitor = SchedulerMonitor(process_llm=self.process_llm, config=self.config)
101
+ self.retriever = SchedulerRetriever(process_llm=self.process_llm, config=self.config)
102
+ self.retriever.log_working_memory_replacement = self.log_working_memory_replacement
103
+
104
+ # initialize with auth_cofig
105
+ if self.auth_config_path is not None and Path(self.auth_config_path).exists():
106
+ self.auth_config = AuthConfig.from_local_yaml(config_path=self.auth_config_path)
107
+ elif AuthConfig.default_config_exists():
108
+ self.auth_config = AuthConfig.from_local_yaml()
109
+ else:
110
+ self.auth_config = None
111
+
112
+ if self.auth_config is not None:
113
+ self.rabbitmq_config = self.auth_config.rabbitmq
114
+ self.initialize_rabbitmq(config=self.rabbitmq_config)
52
115
 
53
- @abstractmethod
54
- def initialize_modules(self, chat_llm: BaseLLM) -> None:
55
- """Initialize all necessary modules for the scheduler
116
+ logger.debug("GeneralScheduler has been initialized")
117
+
118
+ @property
119
+ def mem_cube(self) -> GeneralMemCube:
120
+ """The memory cube associated with this MemChat."""
121
+ return self._current_mem_cube
122
+
123
+ @mem_cube.setter
124
+ def mem_cube(self, value: GeneralMemCube) -> None:
125
+ """The memory cube associated with this MemChat."""
126
+ self._current_mem_cube = value
127
+ self.retriever.mem_cube = value
128
+
129
+ def _set_current_context_from_message(self, msg: ScheduleMessageItem) -> None:
130
+ """Update current user/cube context from the incoming message (thread-safe)."""
131
+ with self._context_lock:
132
+ self._current_user_id = msg.user_id
133
+ self._current_mem_cube_id = msg.mem_cube_id
134
+ self._current_mem_cube = msg.mem_cube
135
+
136
+ def _validate_messages(self, messages: list[ScheduleMessageItem], label: str):
137
+ """Validate if all messages match the expected label.
138
+
139
+ Args:
140
+ messages: List of message items to validate.
141
+ label: Expected message label (e.g., QUERY_LABEL/ANSWER_LABEL).
142
+
143
+ Returns:
144
+ bool: True if all messages passed validation, False if any failed.
145
+ """
146
+ for message in messages:
147
+ if not self._validate_message(message, label):
148
+ return False
149
+ logger.error("Message batch contains invalid labels, aborting processing")
150
+ return True
151
+
152
+ def _validate_message(self, message: ScheduleMessageItem, label: str):
153
+ """Validate if the message matches the expected label.
56
154
 
57
155
  Args:
58
- chat_llm: The LLM instance to be used for chat interactions
156
+ message: Incoming message item to validate.
157
+ label: Expected message label (e.g., QUERY_LABEL/ANSWER_LABEL).
158
+
159
+ Returns:
160
+ bool: True if validation passed, False otherwise.
161
+ """
162
+ if message.label != label:
163
+ logger.error(f"Handler validation failed: expected={label}, actual={message.label}")
164
+ return False
165
+ return True
166
+
167
+ def update_activation_memory(
168
+ self,
169
+ new_memories: list[str | TextualMemoryItem],
170
+ label: str,
171
+ user_id: str,
172
+ mem_cube_id: str,
173
+ mem_cube: GeneralMemCube,
174
+ ) -> None:
59
175
  """
176
+ Update activation memory by extracting KVCacheItems from new_memory (list of str),
177
+ add them to a KVCacheMemory instance, and dump to disk.
178
+ """
179
+ if len(new_memories) == 0:
180
+ logger.error("update_activation_memory: new_memory is empty.")
181
+ return
182
+ if isinstance(new_memories[0], TextualMemoryItem):
183
+ new_text_memories = [mem.memory for mem in new_memories]
184
+ elif isinstance(new_memories[0], str):
185
+ new_text_memories = new_memories
186
+ else:
187
+ logger.error("Not Implemented.")
188
+
189
+ try:
190
+ if isinstance(mem_cube.act_mem, VLLMKVCacheMemory):
191
+ act_mem: VLLMKVCacheMemory = mem_cube.act_mem
192
+ elif isinstance(mem_cube.act_mem, KVCacheMemory):
193
+ act_mem: KVCacheMemory = mem_cube.act_mem
194
+ else:
195
+ logger.error("Not Implemented.")
196
+ return
197
+
198
+ text_memory = MEMORY_ASSEMBLY_TEMPLATE.format(
199
+ memory_text="".join(
200
+ [
201
+ f"{i + 1}. {sentence.strip()}\n"
202
+ for i, sentence in enumerate(new_text_memories)
203
+ if sentence.strip() # Skip empty strings
204
+ ]
205
+ )
206
+ )
207
+
208
+ # huggingface or vllm kv cache
209
+ original_cache_items: list[VLLMKVCacheItem] = act_mem.get_all()
210
+ original_text_memories = []
211
+ if len(original_cache_items) > 0:
212
+ pre_cache_item: VLLMKVCacheItem = original_cache_items[-1]
213
+ original_text_memories = pre_cache_item.records.text_memories
214
+ act_mem.delete_all()
215
+
216
+ cache_item = act_mem.extract(text_memory)
217
+ cache_item.records.text_memories = new_text_memories
218
+
219
+ act_mem.add([cache_item])
220
+ act_mem.dump(self.act_mem_dump_path)
221
+
222
+ self.log_activation_memory_update(
223
+ original_text_memories=original_text_memories,
224
+ new_text_memories=new_text_memories,
225
+ label=label,
226
+ user_id=user_id,
227
+ mem_cube_id=mem_cube_id,
228
+ mem_cube=mem_cube,
229
+ )
230
+
231
+ except Exception as e:
232
+ logger.warning(f"MOS-based activation memory update failed: {e}", exc_info=True)
233
+
234
+ def update_activation_memory_periodically(
235
+ self,
236
+ interval_seconds: int,
237
+ label: str,
238
+ user_id: str,
239
+ mem_cube_id: str,
240
+ mem_cube: GeneralMemCube,
241
+ ):
242
+ new_activation_memories = []
243
+
244
+ if self.monitor.timed_trigger(
245
+ last_time=self.monitor._last_activation_mem_update_time,
246
+ interval_seconds=interval_seconds,
247
+ ):
248
+ logger.info(f"Updating activation memory for user {user_id} and mem_cube {mem_cube_id}")
249
+
250
+ self.monitor.update_memory_monitors(
251
+ user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
252
+ )
253
+
254
+ new_activation_memories = [
255
+ m.memory_text
256
+ for m in self.monitor.activation_memory_monitors[user_id][mem_cube_id].memories
257
+ ]
258
+
259
+ logger.info(
260
+ f"Collected {len(new_activation_memories)} new memory entries for processing"
261
+ )
262
+
263
+ self.update_activation_memory(
264
+ new_memories=new_activation_memories,
265
+ label=label,
266
+ user_id=user_id,
267
+ mem_cube_id=mem_cube_id,
268
+ mem_cube=mem_cube,
269
+ )
270
+
271
+ self.monitor._last_activation_mem_update_time = datetime.now()
272
+
273
+ logger.debug(
274
+ f"Activation memory update completed at {self.monitor._last_activation_mem_update_time}"
275
+ )
276
+ else:
277
+ logger.info(
278
+ f"Skipping update - {interval_seconds} second interval not yet reached. "
279
+ f"Last update time is {self.monitor._last_activation_mem_update_time} and now is"
280
+ f"{datetime.now()}"
281
+ )
60
282
 
61
283
  def submit_messages(self, messages: ScheduleMessageItem | list[ScheduleMessageItem]):
62
284
  """Submit multiple messages to the message queue."""
@@ -68,15 +290,185 @@ class BaseScheduler(RedisSchedulerModule):
68
290
  logger.info(f"Submitted message: {message.label} - {message.content}")
69
291
 
70
292
  def _submit_web_logs(self, messages: ScheduleLogForWebItem | list[ScheduleLogForWebItem]):
293
+ """Submit log messages to the web log queue and optionally to RabbitMQ.
294
+
295
+ Args:
296
+ messages: Single log message or list of log messages
297
+ """
71
298
  if isinstance(messages, ScheduleLogForWebItem):
72
299
  messages = [messages] # transform single message to list
73
300
 
74
301
  for message in messages:
75
302
  self._web_log_message_queue.put(message)
303
+ logger.info(f"Submitted Scheduling log for web: {message.log_content}")
304
+
305
+ if self.is_rabbitmq_connected():
306
+ logger.info("Submitted Scheduling log to rabbitmq")
307
+ self.rabbitmq_publish_message(message=message.to_dict())
308
+ logger.debug(f"{len(messages)} submitted. {self._web_log_message_queue.qsize()} in queue.")
309
+
310
+ def log_activation_memory_update(
311
+ self,
312
+ original_text_memories: list[str],
313
+ new_text_memories: list[str],
314
+ label: str,
315
+ user_id: str,
316
+ mem_cube_id: str,
317
+ mem_cube: GeneralMemCube,
318
+ ):
319
+ """Log changes when activation memory is updated.
320
+
321
+ Args:
322
+ original_text_memories: List of original memory texts
323
+ new_text_memories: List of new memory texts
324
+ """
325
+ original_set = set(original_text_memories)
326
+ new_set = set(new_text_memories)
327
+
328
+ # Identify changes
329
+ added_memories = list(new_set - original_set) # Present in new but not original
330
+
331
+ # recording messages
332
+ for mem in added_memories:
333
+ log_message_a = self.create_autofilled_log_item(
334
+ log_content=mem,
335
+ label=label,
336
+ from_memory_type=TEXT_MEMORY_TYPE,
337
+ to_memory_type=ACTIVATION_MEMORY_TYPE,
338
+ user_id=user_id,
339
+ mem_cube_id=mem_cube_id,
340
+ mem_cube=mem_cube,
341
+ )
342
+ log_message_b = self.create_autofilled_log_item(
343
+ log_content=mem,
344
+ label=label,
345
+ from_memory_type=ACTIVATION_MEMORY_TYPE,
346
+ to_memory_type=PARAMETER_MEMORY_TYPE,
347
+ user_id=user_id,
348
+ mem_cube_id=mem_cube_id,
349
+ mem_cube=mem_cube,
350
+ )
351
+ self._submit_web_logs(messages=[log_message_a, log_message_b])
76
352
  logger.info(
77
- f"Submitted Scheduling log for web: {message.log_title} - {message.log_content}"
353
+ f"{len(added_memories)} {LONG_TERM_MEMORY_TYPE} memorie(s) "
354
+ f"transformed to {WORKING_MEMORY_TYPE} memories."
78
355
  )
79
- logger.debug(f"{len(messages)} submitted. {self._web_log_message_queue.qsize()} in queue.")
356
+
357
+ def log_working_memory_replacement(
358
+ self,
359
+ original_memory: list[TextualMemoryItem],
360
+ new_memory: list[TextualMemoryItem],
361
+ user_id: str,
362
+ mem_cube_id: str,
363
+ mem_cube: GeneralMemCube,
364
+ ):
365
+ """Log changes when working memory is replaced."""
366
+ memory_type_map = {
367
+ transform_name_to_key(name=m.memory): m.metadata.memory_type
368
+ for m in original_memory + new_memory
369
+ }
370
+
371
+ original_text_memories = [m.memory for m in original_memory]
372
+ new_text_memories = [m.memory for m in new_memory]
373
+
374
+ # Convert to sets for efficient difference operations
375
+ original_set = set(original_text_memories)
376
+ new_set = set(new_text_memories)
377
+
378
+ # Identify changes
379
+ added_memories = list(new_set - original_set) # Present in new but not original
380
+
381
+ # recording messages
382
+ for mem in added_memories:
383
+ normalized_mem = transform_name_to_key(name=mem)
384
+ if normalized_mem not in memory_type_map:
385
+ logger.error(f"Memory text not found in type mapping: {mem[:50]}...")
386
+ # Get the memory type from the map, default to LONG_TERM_MEMORY_TYPE if not found
387
+ mem_type = memory_type_map.get(normalized_mem, LONG_TERM_MEMORY_TYPE)
388
+
389
+ if mem_type == WORKING_MEMORY_TYPE:
390
+ logger.warning(f"Memory already in working memory: {mem[:50]}...")
391
+ continue
392
+
393
+ log_message = self.create_autofilled_log_item(
394
+ log_content=mem,
395
+ label=QUERY_LABEL,
396
+ from_memory_type=mem_type,
397
+ to_memory_type=WORKING_MEMORY_TYPE,
398
+ user_id=user_id,
399
+ mem_cube_id=mem_cube_id,
400
+ mem_cube=mem_cube,
401
+ )
402
+ self._submit_web_logs(messages=log_message)
403
+ logger.info(
404
+ f"{len(added_memories)} {LONG_TERM_MEMORY_TYPE} memorie(s) "
405
+ f"transformed to {WORKING_MEMORY_TYPE} memories."
406
+ )
407
+
408
+ def log_adding_user_inputs(
409
+ self,
410
+ user_inputs: list[str],
411
+ user_id: str,
412
+ mem_cube_id: str,
413
+ mem_cube: GeneralMemCube,
414
+ ):
415
+ """Log changes when working memory is replaced."""
416
+
417
+ # recording messages
418
+ for input_str in user_inputs:
419
+ log_message = self.create_autofilled_log_item(
420
+ log_content=input_str,
421
+ label=ADD_LABEL,
422
+ from_memory_type=USER_INPUT_TYPE,
423
+ to_memory_type=TEXT_MEMORY_TYPE,
424
+ user_id=user_id,
425
+ mem_cube_id=mem_cube_id,
426
+ mem_cube=mem_cube,
427
+ )
428
+ self._submit_web_logs(messages=log_message)
429
+ logger.info(
430
+ f"{len(user_inputs)} {USER_INPUT_TYPE} memorie(s) "
431
+ f"transformed to {TEXT_MEMORY_TYPE} memories."
432
+ )
433
+
434
+ def create_autofilled_log_item(
435
+ self,
436
+ log_content: str,
437
+ label: str,
438
+ from_memory_type: str,
439
+ to_memory_type: str,
440
+ user_id: str,
441
+ mem_cube_id: str,
442
+ mem_cube: GeneralMemCube,
443
+ ) -> ScheduleLogForWebItem:
444
+ text_mem_base: TreeTextMemory = mem_cube.text_mem
445
+ current_memory_sizes = text_mem_base.get_current_memory_size()
446
+ current_memory_sizes = {
447
+ "long_term_memory_size": current_memory_sizes["LongTermMemory"],
448
+ "user_memory_size": current_memory_sizes["UserMemory"],
449
+ "working_memory_size": current_memory_sizes["WorkingMemory"],
450
+ "transformed_act_memory_size": NOT_INITIALIZED,
451
+ "parameter_memory_size": NOT_INITIALIZED,
452
+ }
453
+ memory_capacities = {
454
+ "long_term_memory_capacity": text_mem_base.memory_manager.memory_size["LongTermMemory"],
455
+ "user_memory_capacity": text_mem_base.memory_manager.memory_size["UserMemory"],
456
+ "working_memory_capacity": text_mem_base.memory_manager.memory_size["WorkingMemory"],
457
+ "transformed_act_memory_capacity": NOT_INITIALIZED,
458
+ "parameter_memory_capacity": NOT_INITIALIZED,
459
+ }
460
+
461
+ log_message = ScheduleLogForWebItem(
462
+ user_id=user_id,
463
+ mem_cube_id=mem_cube_id,
464
+ label=label,
465
+ from_memory_type=from_memory_type,
466
+ to_memory_type=to_memory_type,
467
+ log_content=log_content,
468
+ current_memory_sizes=current_memory_sizes,
469
+ memory_capacities=memory_capacities,
470
+ )
471
+ return log_message
80
472
 
81
473
  def get_web_log_messages(self) -> list[dict]:
82
474
  """
@@ -87,13 +479,12 @@ class BaseScheduler(RedisSchedulerModule):
87
479
  ready for JSON serialization. The list is ordered from oldest to newest.
88
480
  """
89
481
  messages = []
90
-
91
- # Process all items in the queue
92
- while not self._web_log_message_queue.empty():
93
- item = self._web_log_message_queue.get()
94
- # Convert the ScheduleLogForWebItem to a dictionary and ensure datetime is serialized
95
- item_dict = item.to_dict()
96
- messages.append(item_dict)
482
+ while True:
483
+ try:
484
+ item = self._web_log_message_queue.get_nowait() # 线程安全的 get
485
+ messages.append(item.to_dict())
486
+ except queue.Empty:
487
+ break
97
488
  return messages
98
489
 
99
490
  def _message_consumer(self) -> None:
@@ -133,32 +524,72 @@ class BaseScheduler(RedisSchedulerModule):
133
524
 
134
525
  def start(self) -> None:
135
526
  """
136
- Start the message consumer thread.
527
+ Start the message consumer thread and initialize dispatcher resources.
137
528
 
138
- Initializes and starts a daemon thread that will periodically
139
- check for and process messages from the queue.
529
+ Initializes and starts:
530
+ 1. Message consumer thread
531
+ 2. Dispatcher thread pool (if parallel dispatch enabled)
140
532
  """
141
- if self._consumer_thread is not None and self._consumer_thread.is_alive():
142
- logger.warning("Consumer thread is already running")
533
+ if self._running:
534
+ logger.warning("Memory Scheduler is already running")
143
535
  return
144
536
 
537
+ # Initialize dispatcher resources
538
+ if self.enable_parallel_dispatch:
539
+ logger.info(f"Initializing dispatcher thread pool with {self.max_workers} workers")
540
+
541
+ # Start consumer thread
145
542
  self._running = True
146
543
  self._consumer_thread = threading.Thread(
147
544
  target=self._message_consumer,
148
- daemon=True, # Allows program to exit even if thread is running
545
+ daemon=True,
149
546
  name="MessageConsumerThread",
150
547
  )
151
548
  self._consumer_thread.start()
152
549
  logger.info("Message consumer thread started")
153
550
 
154
551
  def stop(self) -> None:
155
- """Stop the consumer thread and clean up resources."""
156
- if self._consumer_thread is None or not self._running:
157
- logger.warning("Consumer thread is not running")
552
+ """Stop all scheduler components gracefully.
553
+
554
+ 1. Stops message consumer thread
555
+ 2. Shuts down dispatcher thread pool
556
+ 3. Cleans up resources
557
+ """
558
+ if not self._running:
559
+ logger.warning("Memory Scheduler is not running")
158
560
  return
561
+
562
+ # Signal consumer thread to stop
159
563
  self._running = False
160
- if self._consumer_thread.is_alive():
161
- self._consumer_thread.join(timeout=5.0) # Wait up to 5 seconds
564
+
565
+ # Wait for consumer thread
566
+ if self._consumer_thread and self._consumer_thread.is_alive():
567
+ self._consumer_thread.join(timeout=5.0)
162
568
  if self._consumer_thread.is_alive():
163
569
  logger.warning("Consumer thread did not stop gracefully")
164
- logger.info("Message consumer thread stopped")
570
+ else:
571
+ logger.info("Consumer thread stopped")
572
+
573
+ # Shutdown dispatcher
574
+ if hasattr(self, "dispatcher") and self.dispatcher:
575
+ logger.info("Shutting down dispatcher...")
576
+ self.dispatcher.shutdown()
577
+
578
+ # Clean up queues
579
+ self._cleanup_queues()
580
+ logger.info("Memory Scheduler stopped completely")
581
+
582
+ def _cleanup_queues(self) -> None:
583
+ """Ensure all queues are emptied and marked as closed."""
584
+ try:
585
+ while not self.memos_message_queue.empty():
586
+ self.memos_message_queue.get_nowait()
587
+ self.memos_message_queue.task_done()
588
+ except queue.Empty:
589
+ pass
590
+
591
+ try:
592
+ while not self._web_log_message_queue.empty():
593
+ self._web_log_message_queue.get_nowait()
594
+ except queue.Empty:
595
+ pass