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
@@ -1,27 +1,16 @@
1
1
  import json
2
2
 
3
- from datetime import datetime, timedelta
4
-
5
3
  from memos.configs.mem_scheduler import GeneralSchedulerConfig
6
- from memos.llms.base import BaseLLM
7
4
  from memos.log import get_logger
8
5
  from memos.mem_cube.general import GeneralMemCube
9
6
  from memos.mem_scheduler.base_scheduler import BaseScheduler
10
- from memos.mem_scheduler.modules.monitor import SchedulerMonitor
11
- from memos.mem_scheduler.modules.retriever import SchedulerRetriever
12
7
  from memos.mem_scheduler.modules.schemas import (
8
+ ADD_LABEL,
13
9
  ANSWER_LABEL,
14
- DEFAULT_ACT_MEM_DUMP_PATH,
15
- DEFAULT_ACTIVATION_MEM_SIZE,
16
- NOT_INITIALIZED,
17
10
  QUERY_LABEL,
18
- ScheduleLogForWebItem,
19
11
  ScheduleMessageItem,
20
- TextMemory_SEARCH_METHOD,
21
- TreeTextMemory_SEARCH_METHOD,
22
12
  )
23
13
  from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
24
- from memos.templates.mem_scheduler_prompts import MEMORY_ASSEMBLY_TEMPLATE
25
14
 
26
15
 
27
16
  logger = get_logger(__name__)
@@ -31,275 +20,167 @@ class GeneralScheduler(BaseScheduler):
31
20
  def __init__(self, config: GeneralSchedulerConfig):
32
21
  """Initialize the scheduler with the given configuration."""
33
22
  super().__init__(config)
34
- self.top_k = self.config.get("top_k", 10)
35
- self.top_n = self.config.get("top_n", 5)
36
- self.act_mem_update_interval = self.config.get("act_mem_update_interval", 300)
37
- self.context_window_size = self.config.get("context_window_size", 5)
38
- self.activation_mem_size = self.config.get(
39
- "activation_mem_size", DEFAULT_ACTIVATION_MEM_SIZE
40
- )
41
- self.act_mem_dump_path = self.config.get("act_mem_dump_path", DEFAULT_ACT_MEM_DUMP_PATH)
42
- self.search_method = TextMemory_SEARCH_METHOD
43
- self._last_activation_mem_update_time = 0.0
44
- self.query_list = []
45
23
 
46
24
  # register handlers
47
25
  handlers = {
48
- QUERY_LABEL: self._query_message_consume,
49
- ANSWER_LABEL: self._answer_message_consume,
26
+ QUERY_LABEL: self._query_message_consumer,
27
+ ANSWER_LABEL: self._answer_message_consumer,
28
+ ADD_LABEL: self._add_message_consumer,
50
29
  }
51
30
  self.dispatcher.register_handlers(handlers)
52
31
 
53
- def initialize_modules(self, chat_llm: BaseLLM):
54
- self.chat_llm = chat_llm
55
- self.monitor = SchedulerMonitor(
56
- chat_llm=self.chat_llm, activation_mem_size=self.activation_mem_size
57
- )
58
- self.retriever = SchedulerRetriever(chat_llm=self.chat_llm)
59
- logger.debug("GeneralScheduler has been initialized")
60
-
61
- def _answer_message_consume(self, messages: list[ScheduleMessageItem]) -> None:
32
+ def _query_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
62
33
  """
63
- Process and handle answer trigger messages from the queue.
34
+ Process and handle query trigger messages from the queue.
64
35
 
65
36
  Args:
66
- messages: List of answer messages to process
37
+ messages: List of query messages to process
67
38
  """
68
- # TODO: This handler is not ready yet
69
- logger.debug(f"Messages {messages} assigned to {ANSWER_LABEL} handler.")
70
- for msg in messages:
71
- if msg.label is not ANSWER_LABEL:
72
- logger.error(f"_answer_message_consume is not designed for {msg.label}")
73
- continue
74
- answer = msg.content
75
- self._current_user_id = msg.user_id
76
- self._current_mem_cube_id = msg.mem_cube_id
77
- self._current_mem_cube = msg.mem_cube
39
+ logger.debug(f"Messages {messages} assigned to {QUERY_LABEL} handler.")
78
40
 
79
- # Get current activation memory items
80
- current_activation_mem = [
81
- item["memory"]
82
- for item in self.monitor.activation_memory_freq_list
83
- if item["memory"] is not None
84
- ]
41
+ # Process the query in a session turn
42
+ grouped_messages = self.dispatcher.group_messages_by_user_and_cube(messages=messages)
85
43
 
86
- # Update memory frequencies based on the answer
87
- # TODO: not implemented
88
- self.monitor.activation_memory_freq_list = self.monitor.update_freq(
89
- answer=answer, activation_memory_freq_list=self.monitor.activation_memory_freq_list
90
- )
44
+ self._validate_messages(messages=messages, label=QUERY_LABEL)
91
45
 
92
- # Check if it's time to update activation memory
93
- now = datetime.now()
94
- if (now - self._last_activation_mem_update_time) >= timedelta(
95
- seconds=self.act_mem_update_interval
96
- ):
97
- # TODO: not implemented
98
- self.update_activation_memory(current_activation_mem)
99
- self._last_activation_mem_update_time = now
46
+ for user_id in grouped_messages:
47
+ for mem_cube_id in grouped_messages[user_id]:
48
+ messages = grouped_messages[user_id][mem_cube_id]
49
+ if len(messages) == 0:
50
+ return
100
51
 
101
- # recording messages
102
- log_message = self.create_autofilled_log_item(
103
- log_title="memos answer triggers scheduling...",
104
- label=ANSWER_LABEL,
105
- log_content="activation_memory has been updated",
106
- )
107
- self._submit_web_logs(messages=log_message)
52
+ # for status update
53
+ self._set_current_context_from_message(msg=messages[0])
108
54
 
109
- def _query_message_consume(self, messages: list[ScheduleMessageItem]) -> None:
55
+ self.process_session_turn(
56
+ queries=[msg.content for msg in messages],
57
+ user_id=user_id,
58
+ mem_cube_id=mem_cube_id,
59
+ mem_cube=messages[0].mem_cube,
60
+ top_k=self.top_k,
61
+ )
62
+
63
+ def _answer_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
110
64
  """
111
- Process and handle query trigger messages from the queue.
65
+ Process and handle answer trigger messages from the queue.
112
66
 
113
67
  Args:
114
- messages: List of query messages to process
68
+ messages: List of answer messages to process
115
69
  """
116
- logger.debug(f"Messages {messages} assigned to {QUERY_LABEL} handler.")
117
- for msg in messages:
118
- if msg.label is not QUERY_LABEL:
119
- logger.error(f"_query_message_consume is not designed for {msg.label}")
120
- continue
121
- # Process the query in a session turn
122
- self._current_user_id = msg.user_id
123
- self._current_mem_cube_id = msg.mem_cube_id
124
- self._current_mem_cube = msg.mem_cube
125
- self.process_session_turn(query=msg.content, top_k=self.top_k, top_n=self.top_n)
70
+ logger.debug(f"Messages {messages} assigned to {ANSWER_LABEL} handler.")
71
+ # Process the query in a session turn
72
+ grouped_messages = self.dispatcher.group_messages_by_user_and_cube(messages=messages)
73
+
74
+ self._validate_messages(messages=messages, label=ANSWER_LABEL)
75
+
76
+ for user_id in grouped_messages:
77
+ for mem_cube_id in grouped_messages[user_id]:
78
+ messages = grouped_messages[user_id][mem_cube_id]
79
+ if len(messages) == 0:
80
+ return
81
+
82
+ # for status update
83
+ self._set_current_context_from_message(msg=messages[0])
84
+
85
+ # update acivation memories
86
+ if self.enable_act_memory_update:
87
+ self.update_activation_memory_periodically(
88
+ interval_seconds=self.monitor.act_mem_update_interval,
89
+ label=ANSWER_LABEL,
90
+ user_id=user_id,
91
+ mem_cube_id=mem_cube_id,
92
+ mem_cube=messages[0].mem_cube,
93
+ )
94
+
95
+ def _add_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
96
+ logger.debug(f"Messages {messages} assigned to {ADD_LABEL} handler.")
97
+ # Process the query in a session turn
98
+ grouped_messages = self.dispatcher.group_messages_by_user_and_cube(messages=messages)
99
+
100
+ self._validate_messages(messages=messages, label=ADD_LABEL)
101
+
102
+ for user_id in grouped_messages:
103
+ for mem_cube_id in grouped_messages[user_id]:
104
+ messages = grouped_messages[user_id][mem_cube_id]
105
+ if len(messages) == 0:
106
+ return
107
+
108
+ # for status update
109
+ self._set_current_context_from_message(msg=messages[0])
110
+
111
+ # submit logs
112
+ for msg in messages:
113
+ user_inputs = json.loads(msg.content)
114
+ self.log_adding_user_inputs(
115
+ user_inputs=user_inputs,
116
+ user_id=msg.user_id,
117
+ mem_cube_id=msg.mem_cube_id,
118
+ mem_cube=msg.mem_cube,
119
+ )
120
+
121
+ # update acivation memories
122
+ if self.enable_act_memory_update:
123
+ self.update_activation_memory_periodically(
124
+ interval_seconds=self.monitor.act_mem_update_interval,
125
+ label=ADD_LABEL,
126
+ user_id=user_id,
127
+ mem_cube_id=mem_cube_id,
128
+ mem_cube=messages[0].mem_cube,
129
+ )
126
130
 
127
131
  def process_session_turn(
128
132
  self,
129
- query: str,
133
+ queries: str | list[str],
134
+ user_id: str,
135
+ mem_cube_id: str,
136
+ mem_cube: GeneralMemCube,
130
137
  top_k: int = 10,
131
- top_n: int = 5,
138
+ query_history: list[str] | None = None,
132
139
  ) -> None:
133
140
  """
134
141
  Process a dialog turn:
135
142
  - If q_list reaches window size, trigger retrieval;
136
143
  - Immediately switch to the new memory if retrieval is triggered.
137
144
  """
138
- q_list = [query]
139
- self.query_list.append(query)
140
- text_mem_base = self.mem_cube.text_mem
141
- if isinstance(text_mem_base, TreeTextMemory):
142
- working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
145
+ if isinstance(queries, str):
146
+ queries = [queries]
147
+
148
+ if query_history is None:
149
+ query_history = queries
143
150
  else:
144
- logger.error("Not implemented!")
151
+ query_history.extend(queries)
152
+
153
+ text_mem_base = mem_cube.text_mem
154
+ if not isinstance(text_mem_base, TreeTextMemory):
155
+ logger.error("Not implemented!", exc_info=True)
145
156
  return
157
+
158
+ working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
146
159
  text_working_memory: list[str] = [w_m.memory for w_m in working_memory]
147
160
  intent_result = self.monitor.detect_intent(
148
- q_list=q_list, text_working_memory=text_working_memory
161
+ q_list=query_history, text_working_memory=text_working_memory
149
162
  )
163
+
150
164
  if intent_result["trigger_retrieval"]:
151
- missing_evidence = intent_result["missing_evidence"]
152
- num_evidence = len(missing_evidence)
165
+ missing_evidences = intent_result["missing_evidences"]
166
+ num_evidence = len(missing_evidences)
153
167
  k_per_evidence = max(1, top_k // max(1, num_evidence))
154
168
  new_candidates = []
155
- for item in missing_evidence:
156
- logger.debug(f"missing_evidence: {item}")
157
- results = self.search(query=item, top_k=k_per_evidence, method=self.search_method)
158
- logger.debug(f"search results for {missing_evidence}: {results}")
169
+ for item in missing_evidences:
170
+ logger.debug(f"missing_evidences: {item}")
171
+ results = self.retriever.search(
172
+ query=item, mem_cube=mem_cube, top_k=k_per_evidence, method=self.search_method
173
+ )
174
+ logger.debug(f"search results for {missing_evidences}: {results}")
159
175
  new_candidates.extend(results)
160
176
 
161
- # recording messages
162
- log_message = self.create_autofilled_log_item(
163
- log_title="user query triggers scheduling...",
164
- label=QUERY_LABEL,
165
- log_content=f"search new candidates for working memory: {len(new_candidates)}",
166
- )
167
- self._submit_web_logs(messages=log_message)
168
- new_order_working_memory = self.replace_working_memory(
169
- original_memory=working_memory, new_memory=new_candidates, top_k=top_k, top_n=top_n
170
- )
171
- self.update_activation_memory(new_order_working_memory)
172
-
173
- def create_autofilled_log_item(
174
- self, log_title: str, log_content: str, label: str
175
- ) -> ScheduleLogForWebItem:
176
- # TODO: create the log iterm with real stats
177
- text_mem_base: TreeTextMemory = self.mem_cube.text_mem
178
- current_memory_sizes = {
179
- "long_term_memory_size": NOT_INITIALIZED,
180
- "user_memory_size": NOT_INITIALIZED,
181
- "working_memory_size": NOT_INITIALIZED,
182
- "transformed_act_memory_size": NOT_INITIALIZED,
183
- "parameter_memory_size": NOT_INITIALIZED,
184
- }
185
- memory_capacities = {
186
- "long_term_memory_capacity": text_mem_base.memory_manager.memory_size["LongTermMemory"],
187
- "user_memory_capacity": text_mem_base.memory_manager.memory_size["UserMemory"],
188
- "working_memory_capacity": text_mem_base.memory_manager.memory_size["WorkingMemory"],
189
- "transformed_act_memory_capacity": NOT_INITIALIZED,
190
- "parameter_memory_capacity": NOT_INITIALIZED,
191
- }
192
-
193
- log_message = ScheduleLogForWebItem(
194
- user_id=self._current_user_id,
195
- mem_cube_id=self._current_mem_cube_id,
196
- label=label,
197
- log_title=log_title,
198
- log_content=log_content,
199
- current_memory_sizes=current_memory_sizes,
200
- memory_capacities=memory_capacities,
201
- )
202
- return log_message
203
-
204
- @property
205
- def mem_cube(self) -> GeneralMemCube:
206
- """The memory cube associated with this MemChat."""
207
- return self._current_mem_cube
208
-
209
- @mem_cube.setter
210
- def mem_cube(self, value: GeneralMemCube) -> None:
211
- """The memory cube associated with this MemChat."""
212
- self._current_mem_cube = value
213
- self.retriever.mem_cube = value
214
-
215
- def replace_working_memory(
216
- self,
217
- original_memory: list[TextualMemoryItem],
218
- new_memory: list[TextualMemoryItem],
219
- top_k: int = 10,
220
- top_n: int = 5,
221
- ) -> None | list[TextualMemoryItem]:
222
- new_order_memory = None
223
- text_mem_base = self.mem_cube.text_mem
224
- if isinstance(text_mem_base, TreeTextMemory):
225
- text_mem_base: TreeTextMemory = text_mem_base
226
- combined_text_memory = [new_m.memory for new_m in original_memory] + [
227
- new_m.memory for new_m in new_memory
228
- ]
229
- combined_memory = original_memory + new_memory
230
- memory_map = {mem_obj.memory: mem_obj for mem_obj in combined_memory}
231
-
232
- unique_memory = list(dict.fromkeys(combined_text_memory))
233
- prompt = self.build_prompt(
234
- "memory_reranking", query="", current_order=unique_memory, staging_buffer=[]
235
- )
236
- response = self.chat_llm.generate([{"role": "user", "content": prompt}])
237
- response = json.loads(response)
238
- new_order_text_memory = response.get("new_order", [])[: top_n + top_k]
239
-
240
- new_order_memory = []
241
- for text in new_order_text_memory:
242
- if text in memory_map:
243
- new_order_memory.append(memory_map[text])
244
- else:
245
- logger.warning(
246
- f"Memory text not found in memory map. text: {text}; memory_map: {memory_map}"
247
- )
248
-
249
- text_mem_base.replace_working_memory(new_order_memory[top_n:])
250
- new_order_memory = new_order_memory[:top_n]
251
- logger.info(
252
- f"The working memory has been replaced with {len(new_order_memory)} new memories."
253
- )
254
- else:
255
- logger.error("memory_base is not supported")
256
-
257
- return new_order_memory
258
-
259
- def search(self, query: str, top_k: int, method=TreeTextMemory_SEARCH_METHOD):
260
- text_mem_base = self.mem_cube.text_mem
261
- if isinstance(text_mem_base, TreeTextMemory) and method == TextMemory_SEARCH_METHOD:
262
- results_long_term = text_mem_base.search(
263
- query=query, top_k=top_k, memory_type="LongTermMemory"
264
- )
265
- results_user = text_mem_base.search(query=query, top_k=top_k, memory_type="UserMemory")
266
- results = results_long_term + results_user
267
- else:
268
- logger.error("Not implemented.")
269
- results = None
270
- return results
271
-
272
- def update_activation_memory(self, new_memory: list[str | TextualMemoryItem]) -> None:
273
- """
274
- Update activation memory by extracting KVCacheItems from new_memory (list of str),
275
- add them to a KVCacheMemory instance, and dump to disk.
276
- """
277
- # TODO: The function of update activation memory is waiting to test
278
- if len(new_memory) == 0:
279
- logger.error("update_activation_memory: new_memory is empty.")
280
- return
281
- if isinstance(new_memory[0], TextualMemoryItem):
282
- new_text_memory = [mem.memory for mem in new_memory]
283
- elif isinstance(new_memory[0], str):
284
- new_text_memory = new_memory
285
- else:
286
- logger.error("Not Implemented.")
287
-
288
- try:
289
- act_mem = self.mem_cube.act_mem
290
-
291
- text_memory = MEMORY_ASSEMBLY_TEMPLATE.format(
292
- memory_text="".join(
293
- [
294
- f"{i + 1}. {sentence.strip()}\n"
295
- for i, sentence in enumerate(new_text_memory)
296
- if sentence.strip() # Skip empty strings
297
- ]
298
- )
177
+ new_order_working_memory = self.retriever.replace_working_memory(
178
+ queries=queries,
179
+ user_id=user_id,
180
+ mem_cube_id=mem_cube_id,
181
+ mem_cube=mem_cube,
182
+ original_memory=working_memory,
183
+ new_memory=new_candidates,
184
+ top_k=top_k,
299
185
  )
300
- act_mem.delete_all()
301
- cache_item = act_mem.extract(text_memory)
302
- act_mem.add(cache_item)
303
- act_mem.dump(self.act_mem_dump_path)
304
- except Exception as e:
305
- logger.warning(f"MOS-based activation memory update failed: {e}")
186
+ logger.debug(f"size of new_order_working_memory: {len(new_order_working_memory)}")
@@ -16,6 +16,7 @@ class BaseSchedulerModule:
16
16
  self.base_dir = Path(BASE_DIR)
17
17
 
18
18
  self._chat_llm = None
19
+ self._process_llm = None
19
20
  self._current_mem_cube_id: str | None = None
20
21
  self._current_mem_cube: GeneralMemCube | None = None
21
22
  self.mem_cubes: dict[str, GeneralMemCube] = {}
@@ -63,6 +64,14 @@ class BaseSchedulerModule:
63
64
  """The memory cube associated with this MemChat."""
64
65
  self._chat_llm = value
65
66
 
67
+ @property
68
+ def process_llm(self) -> BaseLLM:
69
+ return self._process_llm
70
+
71
+ @process_llm.setter
72
+ def process_llm(self, value: BaseLLM) -> None:
73
+ self._process_llm = value
74
+
66
75
  @property
67
76
  def mem_cube(self) -> GeneralMemCube:
68
77
  """The memory cube associated with this MemChat."""
@@ -73,6 +73,38 @@ class SchedulerDispatcher(BaseSchedulerModule):
73
73
  def _default_message_handler(self, messages: list[ScheduleMessageItem]) -> None:
74
74
  logger.debug(f"Using _default_message_handler to deal with messages: {messages}")
75
75
 
76
+ def group_messages_by_user_and_cube(
77
+ self, messages: list[ScheduleMessageItem]
78
+ ) -> dict[str, dict[str, list[ScheduleMessageItem]]]:
79
+ """
80
+ Groups messages into a nested dictionary structure first by user_id, then by mem_cube_id.
81
+
82
+ Args:
83
+ messages: List of ScheduleMessageItem objects to be grouped
84
+
85
+ Returns:
86
+ A nested dictionary with the structure:
87
+ {
88
+ "user_id_1": {
89
+ "mem_cube_id_1": [msg1, msg2, ...],
90
+ "mem_cube_id_2": [msg3, msg4, ...],
91
+ ...
92
+ },
93
+ "user_id_2": {
94
+ ...
95
+ },
96
+ ...
97
+ }
98
+ Where each msg is the original ScheduleMessageItem object
99
+ """
100
+ grouped_dict = defaultdict(lambda: defaultdict(list))
101
+
102
+ for msg in messages:
103
+ grouped_dict[msg.user_id][msg.mem_cube_id].append(msg)
104
+
105
+ # Convert defaultdict to regular dict for cleaner output
106
+ return {user_id: dict(cube_groups) for user_id, cube_groups in grouped_dict.items()}
107
+
76
108
  def dispatch(self, msg_list: list[ScheduleMessageItem]):
77
109
  """
78
110
  Dispatch a list of messages to their respective handlers.
@@ -98,6 +130,40 @@ class SchedulerDispatcher(BaseSchedulerModule):
98
130
  # dispatch to different handler
99
131
  logger.debug(f"Dispatch {len(msgs)} messages to {label} handler.")
100
132
  if self.enable_parallel_dispatch and self.dispatcher_executor is not None:
101
- self.dispatcher_executor.submit(handler, msgs)
133
+ # Capture variables in lambda to avoid loop variable issues
134
+ # TODO check this
135
+ future = self.dispatcher_executor.submit(handler, msgs)
136
+ logger.debug(f"Dispatched {len(msgs)} messages as future task")
137
+ return future
102
138
  else:
103
- handler(msgs) # Direct serial execution
139
+ handler(msgs)
140
+ return None
141
+
142
+ def join(self, timeout: float | None = None) -> bool:
143
+ """Wait for all dispatched tasks to complete.
144
+
145
+ Args:
146
+ timeout: Maximum time to wait in seconds. None means wait forever.
147
+
148
+ Returns:
149
+ bool: True if all tasks completed, False if timeout occurred.
150
+ """
151
+ if not self.enable_parallel_dispatch or self.dispatcher_executor is None:
152
+ return True # 串行模式无需等待
153
+
154
+ self.dispatcher_executor.shutdown(wait=True, timeout=timeout)
155
+ return True
156
+
157
+ def shutdown(self) -> None:
158
+ """Gracefully shutdown the dispatcher."""
159
+ if self.dispatcher_executor is not None:
160
+ self.dispatcher_executor.shutdown(wait=True)
161
+ self._running = False
162
+ logger.info("Dispatcher has been shutdown")
163
+
164
+ def __enter__(self):
165
+ self._running = True
166
+ return self
167
+
168
+ def __exit__(self, exc_type, exc_val, exc_tb):
169
+ self.shutdown()
@@ -0,0 +1,39 @@
1
+ import threading
2
+
3
+ from queue import Empty, Full, Queue
4
+ from typing import TypeVar
5
+
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class AutoDroppingQueue(Queue[T]):
11
+ """A thread-safe queue that automatically drops the oldest item when full."""
12
+
13
+ def __init__(self, maxsize: int = 0):
14
+ super().__init__(maxsize=maxsize)
15
+ self._lock = threading.Lock() # Additional lock to prevent race conditions
16
+
17
+ def put(self, item: T, block: bool = True, timeout: float | None = None) -> None:
18
+ """Put an item into the queue.
19
+
20
+ If the queue is full, the oldest item will be automatically removed to make space.
21
+ This operation is thread-safe.
22
+
23
+ Args:
24
+ item: The item to be put into the queue
25
+ block: Ignored (kept for compatibility with Queue interface)
26
+ timeout: Ignored (kept for compatibility with Queue interface)
27
+ """
28
+ with self._lock: # Ensure atomic operation
29
+ try:
30
+ # First try non-blocking put
31
+ super().put(item, block=False)
32
+ except Full:
33
+ # If queue is full, remove the oldest item
34
+ from contextlib import suppress
35
+
36
+ with suppress(Empty):
37
+ self.get_nowait() # Remove oldest item
38
+ # Retry putting the new item
39
+ super().put(item, block=False)