MemoryOS 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of MemoryOS might be problematic. Click here for more details.

Files changed (114) hide show
  1. {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/METADATA +67 -26
  2. memoryos-0.2.2.dist-info/RECORD +169 -0
  3. memoryos-0.2.2.dist-info/entry_points.txt +3 -0
  4. memos/__init__.py +1 -1
  5. memos/api/config.py +562 -0
  6. memos/api/context/context.py +147 -0
  7. memos/api/context/dependencies.py +90 -0
  8. memos/api/exceptions.py +28 -0
  9. memos/api/mcp_serve.py +502 -0
  10. memos/api/product_api.py +35 -0
  11. memos/api/product_models.py +163 -0
  12. memos/api/routers/__init__.py +1 -0
  13. memos/api/routers/product_router.py +386 -0
  14. memos/chunkers/sentence_chunker.py +8 -2
  15. memos/cli.py +113 -0
  16. memos/configs/embedder.py +27 -0
  17. memos/configs/graph_db.py +132 -3
  18. memos/configs/internet_retriever.py +6 -0
  19. memos/configs/llm.py +47 -0
  20. memos/configs/mem_cube.py +1 -1
  21. memos/configs/mem_os.py +5 -0
  22. memos/configs/mem_reader.py +9 -0
  23. memos/configs/mem_scheduler.py +107 -7
  24. memos/configs/mem_user.py +58 -0
  25. memos/configs/memory.py +5 -4
  26. memos/dependency.py +52 -0
  27. memos/embedders/ark.py +92 -0
  28. memos/embedders/factory.py +4 -0
  29. memos/embedders/sentence_transformer.py +8 -2
  30. memos/embedders/universal_api.py +32 -0
  31. memos/graph_dbs/base.py +11 -3
  32. memos/graph_dbs/factory.py +4 -0
  33. memos/graph_dbs/nebular.py +1364 -0
  34. memos/graph_dbs/neo4j.py +333 -124
  35. memos/graph_dbs/neo4j_community.py +300 -0
  36. memos/llms/base.py +9 -0
  37. memos/llms/deepseek.py +54 -0
  38. memos/llms/factory.py +10 -1
  39. memos/llms/hf.py +170 -13
  40. memos/llms/hf_singleton.py +114 -0
  41. memos/llms/ollama.py +4 -0
  42. memos/llms/openai.py +67 -1
  43. memos/llms/qwen.py +63 -0
  44. memos/llms/vllm.py +153 -0
  45. memos/log.py +1 -1
  46. memos/mem_cube/general.py +77 -16
  47. memos/mem_cube/utils.py +109 -0
  48. memos/mem_os/core.py +251 -51
  49. memos/mem_os/main.py +94 -12
  50. memos/mem_os/product.py +1220 -43
  51. memos/mem_os/utils/default_config.py +352 -0
  52. memos/mem_os/utils/format_utils.py +1401 -0
  53. memos/mem_reader/simple_struct.py +18 -10
  54. memos/mem_scheduler/base_scheduler.py +441 -40
  55. memos/mem_scheduler/general_scheduler.py +249 -248
  56. memos/mem_scheduler/modules/base.py +14 -5
  57. memos/mem_scheduler/modules/dispatcher.py +67 -4
  58. memos/mem_scheduler/modules/misc.py +104 -0
  59. memos/mem_scheduler/modules/monitor.py +240 -50
  60. memos/mem_scheduler/modules/rabbitmq_service.py +319 -0
  61. memos/mem_scheduler/modules/redis_service.py +32 -22
  62. memos/mem_scheduler/modules/retriever.py +167 -23
  63. memos/mem_scheduler/modules/scheduler_logger.py +255 -0
  64. memos/mem_scheduler/mos_for_test_scheduler.py +140 -0
  65. memos/mem_scheduler/schemas/__init__.py +0 -0
  66. memos/mem_scheduler/schemas/general_schemas.py +43 -0
  67. memos/mem_scheduler/{modules/schemas.py → schemas/message_schemas.py} +63 -61
  68. memos/mem_scheduler/schemas/monitor_schemas.py +329 -0
  69. memos/mem_scheduler/utils/__init__.py +0 -0
  70. memos/mem_scheduler/utils/filter_utils.py +176 -0
  71. memos/mem_scheduler/utils/misc_utils.py +61 -0
  72. memos/mem_user/factory.py +94 -0
  73. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  74. memos/mem_user/mysql_user_manager.py +500 -0
  75. memos/mem_user/persistent_factory.py +96 -0
  76. memos/mem_user/persistent_user_manager.py +260 -0
  77. memos/mem_user/user_manager.py +4 -4
  78. memos/memories/activation/item.py +29 -0
  79. memos/memories/activation/kv.py +10 -3
  80. memos/memories/activation/vllmkv.py +219 -0
  81. memos/memories/factory.py +2 -0
  82. memos/memories/textual/base.py +1 -1
  83. memos/memories/textual/general.py +43 -97
  84. memos/memories/textual/item.py +5 -33
  85. memos/memories/textual/tree.py +22 -12
  86. memos/memories/textual/tree_text_memory/organize/conflict.py +9 -5
  87. memos/memories/textual/tree_text_memory/organize/manager.py +26 -18
  88. memos/memories/textual/tree_text_memory/organize/redundancy.py +25 -44
  89. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +50 -48
  90. memos/memories/textual/tree_text_memory/organize/reorganizer.py +81 -56
  91. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  92. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
  93. memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
  94. memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
  95. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  96. memos/memories/textual/tree_text_memory/retrieve/searcher.py +52 -28
  97. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -15
  98. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  99. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  100. memos/memos_tools/dinding_report_bot.py +422 -0
  101. memos/memos_tools/notification_service.py +44 -0
  102. memos/memos_tools/notification_utils.py +96 -0
  103. memos/parsers/markitdown.py +8 -2
  104. memos/settings.py +3 -1
  105. memos/templates/mem_reader_prompts.py +66 -23
  106. memos/templates/mem_scheduler_prompts.py +126 -43
  107. memos/templates/mos_prompts.py +87 -0
  108. memos/templates/tree_reorganize_prompts.py +85 -30
  109. memos/vec_dbs/base.py +12 -0
  110. memos/vec_dbs/qdrant.py +46 -20
  111. memoryos-0.2.0.dist-info/RECORD +0 -128
  112. memos/mem_scheduler/utils.py +0 -26
  113. {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
  114. {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
@@ -1,7 +1,7 @@
1
1
  import concurrent.futures
2
2
  import copy
3
3
  import json
4
- import re
4
+
5
5
  from abc import ABC
6
6
  from typing import Any
7
7
 
@@ -16,8 +16,8 @@ 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_PROMPT,
20
19
  SIMPLE_STRUCT_MEM_READER_EXAMPLE,
20
+ SIMPLE_STRUCT_MEM_READER_PROMPT,
21
21
  )
22
22
 
23
23
 
@@ -58,9 +58,13 @@ class SimpleStructMemReader(BaseMemReader, ABC):
58
58
  metadata=TreeNodeTextualMemoryMetadata(
59
59
  user_id=info.get("user_id"),
60
60
  session_id=info.get("session_id"),
61
- memory_type=memory_i_raw.get("memory_type", ""),
61
+ memory_type=memory_i_raw.get("memory_type", "")
62
+ .replace("长期记忆", "LongTermMemory")
63
+ .replace("用户记忆", "UserMemory"),
62
64
  status="activated",
63
- tags=memory_i_raw.get("tags", ""),
65
+ tags=memory_i_raw.get("tags", [])
66
+ if type(memory_i_raw.get("tags", [])) is list
67
+ else [],
64
68
  key=memory_i_raw.get("key", ""),
65
69
  embedding=self.embedder.embed([memory_i_raw.get("value", "")])[0],
66
70
  usage=[],
@@ -176,8 +180,12 @@ class SimpleStructMemReader(BaseMemReader, ABC):
176
180
  elif type == "doc":
177
181
  for item in scene_data:
178
182
  try:
179
- parsed_text = parser.parse(item)
180
- results.append({"file": item, "text": parsed_text})
183
+ if not isinstance(item, str):
184
+ parsed_text = parser.parse(item)
185
+ results.append({"file": "pure_text", "text": parsed_text})
186
+ else:
187
+ parsed_text = item
188
+ results.append({"file": item, "text": parsed_text})
181
189
  except Exception as e:
182
190
  print(f"Error parsing file {item}: {e!s}")
183
191
 
@@ -208,15 +216,15 @@ class SimpleStructMemReader(BaseMemReader, ABC):
208
216
  for i, chunk_res in enumerate(processed_chunks):
209
217
  if chunk_res:
210
218
  node_i = TextualMemoryItem(
211
- memory=chunk_res["summary"],
219
+ memory=chunk_res["value"],
212
220
  metadata=TreeNodeTextualMemoryMetadata(
213
221
  user_id=info.get("user_id"),
214
222
  session_id=info.get("session_id"),
215
223
  memory_type="LongTermMemory",
216
224
  status="activated",
217
- tags=chunk_res["tags"],
218
- key="",
219
- embedding=self.embedder.embed([chunk_res["summary"]])[0],
225
+ tags=chunk_res["tags"] if type(chunk_res["tags"]) is list else [],
226
+ key=chunk_res["key"],
227
+ embedding=self.embedder.embed([chunk_res["value"]])[0],
220
228
  usage=[],
221
229
  sources=[f"{scene_data_info['file']}_{i}"],
222
230
  background="",
@@ -2,61 +2,413 @@ 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
13
- from memos.mem_scheduler.modules.schemas import (
17
+ from memos.mem_scheduler.modules.retriever import SchedulerRetriever
18
+ from memos.mem_scheduler.modules.scheduler_logger import SchedulerLoggerModule
19
+ from memos.mem_scheduler.schemas.general_schemas import (
20
+ DEFAULT_ACT_MEM_DUMP_PATH,
14
21
  DEFAULT_CONSUME_INTERVAL_SECONDS,
15
22
  DEFAULT_THREAD__POOL_MAX_WORKERS,
23
+ MemCubeID,
24
+ TreeTextMemory_SEARCH_METHOD,
25
+ UserID,
26
+ )
27
+ from memos.mem_scheduler.schemas.message_schemas import (
16
28
  ScheduleLogForWebItem,
17
29
  ScheduleMessageItem,
18
30
  )
31
+ from memos.mem_scheduler.schemas.monitor_schemas import MemoryMonitorItem
32
+ from memos.mem_scheduler.utils.filter_utils import (
33
+ transform_name_to_key,
34
+ )
35
+ from memos.memories.activation.kv import KVCacheMemory
36
+ from memos.memories.activation.vllmkv import VLLMKVCacheItem, VLLMKVCacheMemory
37
+ from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
38
+ from memos.templates.mem_scheduler_prompts import MEMORY_ASSEMBLY_TEMPLATE
19
39
 
20
40
 
21
41
  logger = get_logger(__name__)
22
42
 
23
43
 
24
- class BaseScheduler(RedisSchedulerModule):
44
+ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLoggerModule):
25
45
  """Base class for all mem_scheduler."""
26
46
 
27
47
  def __init__(self, config: BaseSchedulerConfig):
28
48
  """Initialize the scheduler with the given configuration."""
29
49
  super().__init__()
30
50
  self.config = config
51
+
52
+ # hyper-parameters
53
+ self.top_k = self.config.get("top_k", 10)
54
+ self.context_window_size = self.config.get("context_window_size", 5)
55
+ self.enable_act_memory_update = self.config.get("enable_act_memory_update", False)
56
+ self.act_mem_dump_path = self.config.get("act_mem_dump_path", DEFAULT_ACT_MEM_DUMP_PATH)
57
+ self.search_method = TreeTextMemory_SEARCH_METHOD
58
+ self.enable_parallel_dispatch = self.config.get("enable_parallel_dispatch", False)
31
59
  self.max_workers = self.config.get(
32
60
  "thread_pool_max_workers", DEFAULT_THREAD__POOL_MAX_WORKERS
33
61
  )
34
- self.retriever = None
35
- self.monitor = None
36
- self.enable_parallel_dispatch = self.config.get("enable_parallel_dispatch", False)
62
+
63
+ self.retriever: SchedulerRetriever | None = None
64
+ self.monitor: SchedulerMonitor | None = None
65
+
37
66
  self.dispatcher = SchedulerDispatcher(
38
67
  max_workers=self.max_workers, enable_parallel_dispatch=self.enable_parallel_dispatch
39
68
  )
40
69
 
41
- # message queue
42
- self.memos_message_queue: Queue[ScheduleMessageItem] = Queue()
43
- self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue()
70
+ # internal message queue
71
+ self.max_internal_messae_queue_size = 100
72
+ self.memos_message_queue: Queue[ScheduleMessageItem] = Queue(
73
+ maxsize=self.max_internal_messae_queue_size
74
+ )
75
+ self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue(
76
+ maxsize=self.max_internal_messae_queue_size
77
+ )
44
78
  self._consumer_thread = None # Reference to our consumer thread
45
79
  self._running = False
46
80
  self._consume_interval = self.config.get(
47
81
  "consume_interval_seconds", DEFAULT_CONSUME_INTERVAL_SECONDS
48
82
  )
49
83
 
50
- # others
51
- self._current_user_id: str | None = None
84
+ # other attributes
85
+ self._context_lock = threading.Lock()
86
+ self.current_user_id: UserID | str | None = None
87
+ self.auth_config_path: str | Path | None = self.config.get("auth_config_path", None)
88
+ self.auth_config = None
89
+ self.rabbitmq_config = None
90
+
91
+ def initialize_modules(self, chat_llm: BaseLLM, process_llm: BaseLLM | None = None):
92
+ if process_llm is None:
93
+ process_llm = chat_llm
94
+
95
+ # initialize submodules
96
+ self.chat_llm = chat_llm
97
+ self.process_llm = process_llm
98
+ self.monitor = SchedulerMonitor(process_llm=self.process_llm, config=self.config)
99
+ self.retriever = SchedulerRetriever(process_llm=self.process_llm, config=self.config)
52
100
 
53
- @abstractmethod
54
- def initialize_modules(self, chat_llm: BaseLLM) -> None:
55
- """Initialize all necessary modules for the scheduler
101
+ # initialize with auth_cofig
102
+ if self.auth_config_path is not None and Path(self.auth_config_path).exists():
103
+ self.auth_config = AuthConfig.from_local_yaml(config_path=self.auth_config_path)
104
+ elif AuthConfig.default_config_exists():
105
+ self.auth_config = AuthConfig.from_local_yaml()
106
+ else:
107
+ self.auth_config = None
108
+
109
+ if self.auth_config is not None:
110
+ self.rabbitmq_config = self.auth_config.rabbitmq
111
+ self.initialize_rabbitmq(config=self.rabbitmq_config)
112
+
113
+ logger.debug("GeneralScheduler has been initialized")
114
+
115
+ @property
116
+ def mem_cube(self) -> GeneralMemCube:
117
+ """The memory cube associated with this MemChat."""
118
+ return self.current_mem_cube
119
+
120
+ @mem_cube.setter
121
+ def mem_cube(self, value: GeneralMemCube) -> None:
122
+ """The memory cube associated with this MemChat."""
123
+ self.current_mem_cube = value
124
+ self.retriever.mem_cube = value
125
+
126
+ def _set_current_context_from_message(self, msg: ScheduleMessageItem) -> None:
127
+ """Update current user/cube context from the incoming message (thread-safe)."""
128
+ with self._context_lock:
129
+ self.current_user_id = msg.user_id
130
+ self.current_mem_cube_id = msg.mem_cube_id
131
+ self.current_mem_cube = msg.mem_cube
132
+
133
+ def transform_memories_to_monitors(
134
+ self, memories: list[TextualMemoryItem]
135
+ ) -> list[MemoryMonitorItem]:
136
+ """
137
+ Convert a list of TextualMemoryItem objects into MemoryMonitorItem objects
138
+ with importance scores based on keyword matching.
56
139
 
57
140
  Args:
58
- chat_llm: The LLM instance to be used for chat interactions
141
+ memories: List of TextualMemoryItem objects to be transformed.
142
+
143
+ Returns:
144
+ List of MemoryMonitorItem objects with computed importance scores.
145
+ """
146
+ query_keywords = self.monitor.query_monitors.get_keywords_collections()
147
+ logger.debug(
148
+ f"Processing {len(memories)} memories with {len(query_keywords)} query keywords"
149
+ )
150
+
151
+ result = []
152
+ mem_length = len(memories)
153
+ for idx, mem in enumerate(memories):
154
+ text_mem = mem.memory
155
+ mem_key = transform_name_to_key(name=text_mem)
156
+
157
+ # Calculate importance score based on keyword matches
158
+ keywords_score = 0
159
+ if query_keywords and text_mem:
160
+ for keyword, count in query_keywords.items():
161
+ keyword_count = text_mem.count(keyword)
162
+ if keyword_count > 0:
163
+ keywords_score += keyword_count * count
164
+ logger.debug(
165
+ f"Matched keyword '{keyword}' {keyword_count} times, added {keywords_score} to keywords_score"
166
+ )
167
+
168
+ # rank score
169
+ sorting_score = mem_length - idx
170
+
171
+ mem_monitor = MemoryMonitorItem(
172
+ memory_text=text_mem,
173
+ tree_memory_item=mem,
174
+ tree_memory_item_mapping_key=mem_key,
175
+ sorting_score=sorting_score,
176
+ keywords_score=keywords_score,
177
+ recording_count=1,
178
+ )
179
+ result.append(mem_monitor)
180
+
181
+ logger.debug(f"Transformed {len(result)} memories to monitors")
182
+ return result
183
+
184
+ def replace_working_memory(
185
+ self,
186
+ user_id: UserID | str,
187
+ mem_cube_id: MemCubeID | str,
188
+ mem_cube: GeneralMemCube,
189
+ original_memory: list[TextualMemoryItem],
190
+ new_memory: list[TextualMemoryItem],
191
+ ) -> None | list[TextualMemoryItem]:
192
+ """Replace working memory with new memories after reranking."""
193
+ text_mem_base = mem_cube.text_mem
194
+ if isinstance(text_mem_base, TreeTextMemory):
195
+ text_mem_base: TreeTextMemory = text_mem_base
196
+
197
+ # process rerank memories with llm
198
+ query_history = self.monitor.query_monitors.get_queries_with_timesort()
199
+ memories_with_new_order, rerank_success_flag = (
200
+ self.retriever.process_and_rerank_memories(
201
+ queries=query_history,
202
+ original_memory=original_memory,
203
+ new_memory=new_memory,
204
+ top_k=self.top_k,
205
+ )
206
+ )
207
+
208
+ # update working memory monitors
209
+ new_working_memory_monitors = self.transform_memories_to_monitors(
210
+ memories=memories_with_new_order
211
+ )
212
+
213
+ if not rerank_success_flag:
214
+ for one in new_working_memory_monitors:
215
+ one.sorting_score = 0
216
+
217
+ self.monitor.update_working_memory_monitors(
218
+ new_working_memory_monitors=new_working_memory_monitors,
219
+ user_id=user_id,
220
+ mem_cube_id=mem_cube_id,
221
+ mem_cube=mem_cube,
222
+ )
223
+
224
+ mem_monitors: list[MemoryMonitorItem] = self.monitor.working_memory_monitors[user_id][
225
+ mem_cube_id
226
+ ].get_sorted_mem_monitors(reverse=True)
227
+ new_working_memories = [mem_monitor.tree_memory_item for mem_monitor in mem_monitors]
228
+
229
+ text_mem_base.replace_working_memory(memories=new_working_memories)
230
+
231
+ logger.info(
232
+ f"The working memory has been replaced with {len(memories_with_new_order)} new memories."
233
+ )
234
+ self.log_working_memory_replacement(
235
+ original_memory=original_memory,
236
+ new_memory=new_working_memories,
237
+ user_id=user_id,
238
+ mem_cube_id=mem_cube_id,
239
+ mem_cube=mem_cube,
240
+ log_func_callback=self._submit_web_logs,
241
+ )
242
+ else:
243
+ logger.error("memory_base is not supported")
244
+ memories_with_new_order = new_memory
245
+
246
+ return memories_with_new_order
247
+
248
+ def initialize_working_memory_monitors(
249
+ self,
250
+ user_id: UserID | str,
251
+ mem_cube_id: MemCubeID | str,
252
+ mem_cube: GeneralMemCube,
253
+ ):
254
+ text_mem_base: TreeTextMemory = mem_cube.text_mem
255
+ working_memories = text_mem_base.get_working_memory()
256
+
257
+ working_memory_monitors = self.transform_memories_to_monitors(
258
+ memories=working_memories,
259
+ )
260
+ self.monitor.update_working_memory_monitors(
261
+ new_working_memory_monitors=working_memory_monitors,
262
+ user_id=user_id,
263
+ mem_cube_id=mem_cube_id,
264
+ mem_cube=mem_cube,
265
+ )
266
+
267
+ def update_activation_memory(
268
+ self,
269
+ new_memories: list[str | TextualMemoryItem],
270
+ label: str,
271
+ user_id: UserID | str,
272
+ mem_cube_id: MemCubeID | str,
273
+ mem_cube: GeneralMemCube,
274
+ ) -> None:
59
275
  """
276
+ Update activation memory by extracting KVCacheItems from new_memory (list of str),
277
+ add them to a KVCacheMemory instance, and dump to disk.
278
+ """
279
+ if len(new_memories) == 0:
280
+ logger.error("update_activation_memory: new_memory is empty.")
281
+ return
282
+ if isinstance(new_memories[0], TextualMemoryItem):
283
+ new_text_memories = [mem.memory for mem in new_memories]
284
+ elif isinstance(new_memories[0], str):
285
+ new_text_memories = new_memories
286
+ else:
287
+ logger.error("Not Implemented.")
288
+
289
+ try:
290
+ if isinstance(mem_cube.act_mem, VLLMKVCacheMemory):
291
+ act_mem: VLLMKVCacheMemory = mem_cube.act_mem
292
+ elif isinstance(mem_cube.act_mem, KVCacheMemory):
293
+ act_mem: KVCacheMemory = mem_cube.act_mem
294
+ else:
295
+ logger.error("Not Implemented.")
296
+ return
297
+
298
+ new_text_memory = MEMORY_ASSEMBLY_TEMPLATE.format(
299
+ memory_text="".join(
300
+ [
301
+ f"{i + 1}. {sentence.strip()}\n"
302
+ for i, sentence in enumerate(new_text_memories)
303
+ if sentence.strip() # Skip empty strings
304
+ ]
305
+ )
306
+ )
307
+
308
+ # huggingface or vllm kv cache
309
+ original_cache_items: list[VLLMKVCacheItem] = act_mem.get_all()
310
+ original_text_memories = []
311
+ if len(original_cache_items) > 0:
312
+ pre_cache_item: VLLMKVCacheItem = original_cache_items[-1]
313
+ original_text_memories = pre_cache_item.records.text_memories
314
+ original_composed_text_memory = pre_cache_item.records.composed_text_memory
315
+ if original_composed_text_memory == new_text_memory:
316
+ logger.warning(
317
+ "Skipping memory update - new composition matches existing cache: %s",
318
+ new_text_memory[:50] + "..."
319
+ if len(new_text_memory) > 50
320
+ else new_text_memory,
321
+ )
322
+ return
323
+ act_mem.delete_all()
324
+
325
+ cache_item = act_mem.extract(new_text_memory)
326
+ cache_item.records.text_memories = new_text_memories
327
+
328
+ act_mem.add([cache_item])
329
+ act_mem.dump(self.act_mem_dump_path)
330
+
331
+ self.log_activation_memory_update(
332
+ original_text_memories=original_text_memories,
333
+ new_text_memories=new_text_memories,
334
+ label=label,
335
+ user_id=user_id,
336
+ mem_cube_id=mem_cube_id,
337
+ mem_cube=mem_cube,
338
+ log_func_callback=self._submit_web_logs,
339
+ )
340
+
341
+ except Exception as e:
342
+ logger.warning(f"MOS-based activation memory update failed: {e}", exc_info=True)
343
+
344
+ def update_activation_memory_periodically(
345
+ self,
346
+ interval_seconds: int,
347
+ label: str,
348
+ user_id: UserID | str,
349
+ mem_cube_id: MemCubeID | str,
350
+ mem_cube: GeneralMemCube,
351
+ ):
352
+ try:
353
+ if (
354
+ self.monitor.last_activation_mem_update_time == datetime.min
355
+ or self.monitor.timed_trigger(
356
+ last_time=self.monitor.last_activation_mem_update_time,
357
+ interval_seconds=interval_seconds,
358
+ )
359
+ ):
360
+ logger.info(
361
+ f"Updating activation memory for user {user_id} and mem_cube {mem_cube_id}"
362
+ )
363
+
364
+ if (
365
+ user_id not in self.monitor.working_memory_monitors
366
+ or mem_cube_id not in self.monitor.working_memory_monitors[user_id]
367
+ or len(self.monitor.working_memory_monitors[user_id][mem_cube_id].memories) == 0
368
+ ):
369
+ logger.warning(
370
+ "No memories found in working_memory_monitors, initializing from current working_memories"
371
+ )
372
+ self.initialize_working_memory_monitors(
373
+ user_id=user_id,
374
+ mem_cube_id=mem_cube_id,
375
+ mem_cube=mem_cube,
376
+ )
377
+
378
+ self.monitor.update_activation_memory_monitors(
379
+ user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
380
+ )
381
+
382
+ new_activation_memories = [
383
+ m.memory_text
384
+ for m in self.monitor.activation_memory_monitors[user_id][mem_cube_id].memories
385
+ ]
386
+
387
+ logger.info(
388
+ f"Collected {len(new_activation_memories)} new memory entries for processing"
389
+ )
390
+
391
+ self.update_activation_memory(
392
+ new_memories=new_activation_memories,
393
+ label=label,
394
+ user_id=user_id,
395
+ mem_cube_id=mem_cube_id,
396
+ mem_cube=mem_cube,
397
+ )
398
+
399
+ self.monitor.last_activation_mem_update_time = datetime.now()
400
+
401
+ logger.debug(
402
+ f"Activation memory update completed at {self.monitor.last_activation_mem_update_time}"
403
+ )
404
+ else:
405
+ logger.info(
406
+ f"Skipping update - {interval_seconds} second interval not yet reached. "
407
+ f"Last update time is {self.monitor.last_activation_mem_update_time} and now is"
408
+ f"{datetime.now()}"
409
+ )
410
+ except Exception as e:
411
+ logger.error(f"Error: {e}", exc_info=True)
60
412
 
61
413
  def submit_messages(self, messages: ScheduleMessageItem | list[ScheduleMessageItem]):
62
414
  """Submit multiple messages to the message queue."""
@@ -67,15 +419,25 @@ class BaseScheduler(RedisSchedulerModule):
67
419
  self.memos_message_queue.put(message)
68
420
  logger.info(f"Submitted message: {message.label} - {message.content}")
69
421
 
70
- def _submit_web_logs(self, messages: ScheduleLogForWebItem | list[ScheduleLogForWebItem]):
422
+ def _submit_web_logs(
423
+ self, messages: ScheduleLogForWebItem | list[ScheduleLogForWebItem]
424
+ ) -> None:
425
+ """Submit log messages to the web log queue and optionally to RabbitMQ.
426
+
427
+ Args:
428
+ messages: Single log message or list of log messages
429
+ """
71
430
  if isinstance(messages, ScheduleLogForWebItem):
72
431
  messages = [messages] # transform single message to list
73
432
 
74
433
  for message in messages:
75
434
  self._web_log_message_queue.put(message)
76
- logger.info(
77
- f"Submitted Scheduling log for web: {message.log_title} - {message.log_content}"
78
- )
435
+ message_info = message.debug_info()
436
+ logger.debug(f"Submitted Scheduling log for web: {message_info}")
437
+
438
+ if self.is_rabbitmq_connected():
439
+ logger.info(f"Submitted Scheduling log to rabbitmq: {message_info}")
440
+ self.rabbitmq_publish_message(message=message.to_dict())
79
441
  logger.debug(f"{len(messages)} submitted. {self._web_log_message_queue.qsize()} in queue.")
80
442
 
81
443
  def get_web_log_messages(self) -> list[dict]:
@@ -87,13 +449,12 @@ class BaseScheduler(RedisSchedulerModule):
87
449
  ready for JSON serialization. The list is ordered from oldest to newest.
88
450
  """
89
451
  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)
452
+ while True:
453
+ try:
454
+ item = self._web_log_message_queue.get_nowait() # 线程安全的 get
455
+ messages.append(item.to_dict())
456
+ except queue.Empty:
457
+ break
97
458
  return messages
98
459
 
99
460
  def _message_consumer(self) -> None:
@@ -133,32 +494,72 @@ class BaseScheduler(RedisSchedulerModule):
133
494
 
134
495
  def start(self) -> None:
135
496
  """
136
- Start the message consumer thread.
497
+ Start the message consumer thread and initialize dispatcher resources.
137
498
 
138
- Initializes and starts a daemon thread that will periodically
139
- check for and process messages from the queue.
499
+ Initializes and starts:
500
+ 1. Message consumer thread
501
+ 2. Dispatcher thread pool (if parallel dispatch enabled)
140
502
  """
141
- if self._consumer_thread is not None and self._consumer_thread.is_alive():
142
- logger.warning("Consumer thread is already running")
503
+ if self._running:
504
+ logger.warning("Memory Scheduler is already running")
143
505
  return
144
506
 
507
+ # Initialize dispatcher resources
508
+ if self.enable_parallel_dispatch:
509
+ logger.info(f"Initializing dispatcher thread pool with {self.max_workers} workers")
510
+
511
+ # Start consumer thread
145
512
  self._running = True
146
513
  self._consumer_thread = threading.Thread(
147
514
  target=self._message_consumer,
148
- daemon=True, # Allows program to exit even if thread is running
515
+ daemon=True,
149
516
  name="MessageConsumerThread",
150
517
  )
151
518
  self._consumer_thread.start()
152
519
  logger.info("Message consumer thread started")
153
520
 
154
521
  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")
522
+ """Stop all scheduler components gracefully.
523
+
524
+ 1. Stops message consumer thread
525
+ 2. Shuts down dispatcher thread pool
526
+ 3. Cleans up resources
527
+ """
528
+ if not self._running:
529
+ logger.warning("Memory Scheduler is not running")
158
530
  return
531
+
532
+ # Signal consumer thread to stop
159
533
  self._running = False
160
- if self._consumer_thread.is_alive():
161
- self._consumer_thread.join(timeout=5.0) # Wait up to 5 seconds
534
+
535
+ # Wait for consumer thread
536
+ if self._consumer_thread and self._consumer_thread.is_alive():
537
+ self._consumer_thread.join(timeout=5.0)
162
538
  if self._consumer_thread.is_alive():
163
539
  logger.warning("Consumer thread did not stop gracefully")
164
- logger.info("Message consumer thread stopped")
540
+ else:
541
+ logger.info("Consumer thread stopped")
542
+
543
+ # Shutdown dispatcher
544
+ if hasattr(self, "dispatcher") and self.dispatcher:
545
+ logger.info("Shutting down dispatcher...")
546
+ self.dispatcher.shutdown()
547
+
548
+ # Clean up queues
549
+ self._cleanup_queues()
550
+ logger.info("Memory Scheduler stopped completely")
551
+
552
+ def _cleanup_queues(self) -> None:
553
+ """Ensure all queues are emptied and marked as closed."""
554
+ try:
555
+ while not self.memos_message_queue.empty():
556
+ self.memos_message_queue.get_nowait()
557
+ self.memos_message_queue.task_done()
558
+ except queue.Empty:
559
+ pass
560
+
561
+ try:
562
+ while not self._web_log_message_queue.empty():
563
+ self._web_log_message_queue.get_nowait()
564
+ except queue.Empty:
565
+ pass