MemoryOS 0.2.1__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.
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/METADATA +2 -1
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/RECORD +72 -55
- memos/__init__.py +1 -1
- memos/api/config.py +156 -65
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +90 -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 +6 -0
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +18 -4
- memos/configs/mem_user.py +58 -0
- memos/graph_dbs/base.py +9 -1
- memos/graph_dbs/factory.py +2 -0
- memos/graph_dbs/nebular.py +1364 -0
- memos/graph_dbs/neo4j.py +4 -4
- memos/log.py +1 -1
- memos/mem_cube/utils.py +13 -6
- memos/mem_os/core.py +140 -30
- memos/mem_os/main.py +1 -1
- memos/mem_os/product.py +266 -152
- memos/mem_os/utils/format_utils.py +314 -67
- memos/mem_reader/simple_struct.py +13 -5
- memos/mem_scheduler/base_scheduler.py +220 -250
- memos/mem_scheduler/general_scheduler.py +193 -73
- memos/mem_scheduler/modules/base.py +5 -5
- memos/mem_scheduler/modules/dispatcher.py +6 -9
- memos/mem_scheduler/modules/misc.py +81 -16
- memos/mem_scheduler/modules/monitor.py +52 -41
- memos/mem_scheduler/modules/rabbitmq_service.py +9 -7
- memos/mem_scheduler/modules/retriever.py +108 -191
- memos/mem_scheduler/modules/scheduler_logger.py +255 -0
- memos/mem_scheduler/mos_for_test_scheduler.py +16 -19
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +43 -0
- memos/mem_scheduler/schemas/message_schemas.py +148 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +329 -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 +61 -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 +4 -0
- memos/memories/textual/base.py +1 -1
- memos/memories/textual/general.py +35 -91
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +13 -7
- memos/memories/textual/tree_text_memory/organize/conflict.py +4 -2
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +47 -43
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +8 -5
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -23
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -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/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +96 -0
- memos/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +2 -1
- memos/templates/mem_scheduler_prompts.py +41 -7
- memos/templates/mos_prompts.py +87 -0
- memos/mem_scheduler/modules/schemas.py +0 -328
- memos/mem_scheduler/utils.py +0 -75
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/entry_points.txt +0 -0
|
@@ -4,12 +4,16 @@ from memos.configs.mem_scheduler import GeneralSchedulerConfig
|
|
|
4
4
|
from memos.log import get_logger
|
|
5
5
|
from memos.mem_cube.general import GeneralMemCube
|
|
6
6
|
from memos.mem_scheduler.base_scheduler import BaseScheduler
|
|
7
|
-
from memos.mem_scheduler.
|
|
7
|
+
from memos.mem_scheduler.schemas.general_schemas import (
|
|
8
8
|
ADD_LABEL,
|
|
9
9
|
ANSWER_LABEL,
|
|
10
|
+
DEFAULT_MAX_QUERY_KEY_WORDS,
|
|
10
11
|
QUERY_LABEL,
|
|
11
|
-
|
|
12
|
+
MemCubeID,
|
|
13
|
+
UserID,
|
|
12
14
|
)
|
|
15
|
+
from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
|
|
16
|
+
from memos.mem_scheduler.schemas.monitor_schemas import QueryMonitorItem
|
|
13
17
|
from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
|
|
14
18
|
|
|
15
19
|
|
|
@@ -29,6 +33,51 @@ class GeneralScheduler(BaseScheduler):
|
|
|
29
33
|
}
|
|
30
34
|
self.dispatcher.register_handlers(handlers)
|
|
31
35
|
|
|
36
|
+
# for evaluation
|
|
37
|
+
def search_for_eval(
|
|
38
|
+
self,
|
|
39
|
+
query: str,
|
|
40
|
+
user_id: UserID | str,
|
|
41
|
+
top_k: int,
|
|
42
|
+
) -> list[str]:
|
|
43
|
+
query_keywords = self.monitor.extract_query_keywords(query=query)
|
|
44
|
+
logger.info(f'Extract keywords "{query_keywords}" from query "{query}"')
|
|
45
|
+
|
|
46
|
+
item = QueryMonitorItem(
|
|
47
|
+
query_text=query,
|
|
48
|
+
keywords=query_keywords,
|
|
49
|
+
max_keywords=DEFAULT_MAX_QUERY_KEY_WORDS,
|
|
50
|
+
)
|
|
51
|
+
self.monitor.query_monitors.put(item=item)
|
|
52
|
+
logger.debug(
|
|
53
|
+
f"Queries in monitor are {self.monitor.query_monitors.get_queries_with_timesort()}."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
queries = [query]
|
|
57
|
+
|
|
58
|
+
# recall
|
|
59
|
+
cur_working_memory, new_candidates = self.process_session_turn(
|
|
60
|
+
queries=queries,
|
|
61
|
+
user_id=user_id,
|
|
62
|
+
mem_cube_id=self.current_mem_cube_id,
|
|
63
|
+
mem_cube=self.current_mem_cube,
|
|
64
|
+
top_k=self.top_k,
|
|
65
|
+
)
|
|
66
|
+
logger.info(f"Processed {queries} and get {len(new_candidates)} new candidate memories.")
|
|
67
|
+
|
|
68
|
+
# rerank
|
|
69
|
+
new_order_working_memory = self.replace_working_memory(
|
|
70
|
+
user_id=user_id,
|
|
71
|
+
mem_cube_id=self.current_mem_cube_id,
|
|
72
|
+
mem_cube=self.current_mem_cube,
|
|
73
|
+
original_memory=cur_working_memory,
|
|
74
|
+
new_memory=new_candidates,
|
|
75
|
+
)
|
|
76
|
+
new_order_working_memory = new_order_working_memory[:top_k]
|
|
77
|
+
logger.info(f"size of new_order_working_memory: {len(new_order_working_memory)}")
|
|
78
|
+
|
|
79
|
+
return [m.memory for m in new_order_working_memory]
|
|
80
|
+
|
|
32
81
|
def _query_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
|
|
33
82
|
"""
|
|
34
83
|
Process and handle query trigger messages from the queue.
|
|
@@ -36,12 +85,12 @@ class GeneralScheduler(BaseScheduler):
|
|
|
36
85
|
Args:
|
|
37
86
|
messages: List of query messages to process
|
|
38
87
|
"""
|
|
39
|
-
logger.
|
|
88
|
+
logger.info(f"Messages {messages} assigned to {QUERY_LABEL} handler.")
|
|
40
89
|
|
|
41
90
|
# Process the query in a session turn
|
|
42
91
|
grouped_messages = self.dispatcher.group_messages_by_user_and_cube(messages=messages)
|
|
43
92
|
|
|
44
|
-
self.
|
|
93
|
+
self.validate_schedule_messages(messages=messages, label=QUERY_LABEL)
|
|
45
94
|
|
|
46
95
|
for user_id in grouped_messages:
|
|
47
96
|
for mem_cube_id in grouped_messages[user_id]:
|
|
@@ -49,16 +98,50 @@ class GeneralScheduler(BaseScheduler):
|
|
|
49
98
|
if len(messages) == 0:
|
|
50
99
|
return
|
|
51
100
|
|
|
101
|
+
mem_cube = messages[0].mem_cube
|
|
102
|
+
|
|
52
103
|
# for status update
|
|
53
104
|
self._set_current_context_from_message(msg=messages[0])
|
|
54
105
|
|
|
55
|
-
|
|
56
|
-
|
|
106
|
+
# update query monitors
|
|
107
|
+
for msg in messages:
|
|
108
|
+
query = msg.content
|
|
109
|
+
query_keywords = self.monitor.extract_query_keywords(query=query)
|
|
110
|
+
logger.info(f'Extract keywords "{query_keywords}" from query "{query}"')
|
|
111
|
+
|
|
112
|
+
item = QueryMonitorItem(
|
|
113
|
+
query_text=query,
|
|
114
|
+
keywords=query_keywords,
|
|
115
|
+
max_keywords=DEFAULT_MAX_QUERY_KEY_WORDS,
|
|
116
|
+
)
|
|
117
|
+
self.monitor.query_monitors.put(item=item)
|
|
118
|
+
logger.debug(
|
|
119
|
+
f"Queries in monitor are {self.monitor.query_monitors.get_queries_with_timesort()}."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
queries = [msg.content for msg in messages]
|
|
123
|
+
|
|
124
|
+
# recall
|
|
125
|
+
cur_working_memory, new_candidates = self.process_session_turn(
|
|
126
|
+
queries=queries,
|
|
57
127
|
user_id=user_id,
|
|
58
128
|
mem_cube_id=mem_cube_id,
|
|
59
|
-
mem_cube=
|
|
129
|
+
mem_cube=mem_cube,
|
|
60
130
|
top_k=self.top_k,
|
|
61
131
|
)
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Processed {queries} and get {len(new_candidates)} new candidate memories."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# rerank
|
|
137
|
+
new_order_working_memory = self.replace_working_memory(
|
|
138
|
+
user_id=user_id,
|
|
139
|
+
mem_cube_id=mem_cube_id,
|
|
140
|
+
mem_cube=mem_cube,
|
|
141
|
+
original_memory=cur_working_memory,
|
|
142
|
+
new_memory=new_candidates,
|
|
143
|
+
)
|
|
144
|
+
logger.info(f"size of new_order_working_memory: {len(new_order_working_memory)}")
|
|
62
145
|
|
|
63
146
|
def _answer_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
|
|
64
147
|
"""
|
|
@@ -67,11 +150,11 @@ class GeneralScheduler(BaseScheduler):
|
|
|
67
150
|
Args:
|
|
68
151
|
messages: List of answer messages to process
|
|
69
152
|
"""
|
|
70
|
-
logger.
|
|
153
|
+
logger.info(f"Messages {messages} assigned to {ANSWER_LABEL} handler.")
|
|
71
154
|
# Process the query in a session turn
|
|
72
155
|
grouped_messages = self.dispatcher.group_messages_by_user_and_cube(messages=messages)
|
|
73
156
|
|
|
74
|
-
self.
|
|
157
|
+
self.validate_schedule_messages(messages=messages, label=ANSWER_LABEL)
|
|
75
158
|
|
|
76
159
|
for user_id in grouped_messages:
|
|
77
160
|
for mem_cube_id in grouped_messages[user_id]:
|
|
@@ -82,8 +165,18 @@ class GeneralScheduler(BaseScheduler):
|
|
|
82
165
|
# for status update
|
|
83
166
|
self._set_current_context_from_message(msg=messages[0])
|
|
84
167
|
|
|
85
|
-
# update
|
|
168
|
+
# update activation memories
|
|
86
169
|
if self.enable_act_memory_update:
|
|
170
|
+
if (
|
|
171
|
+
len(self.monitor.working_memory_monitors[user_id][mem_cube_id].memories)
|
|
172
|
+
== 0
|
|
173
|
+
):
|
|
174
|
+
self.initialize_working_memory_monitors(
|
|
175
|
+
user_id=user_id,
|
|
176
|
+
mem_cube_id=mem_cube_id,
|
|
177
|
+
mem_cube=messages[0].mem_cube,
|
|
178
|
+
)
|
|
179
|
+
|
|
87
180
|
self.update_activation_memory_periodically(
|
|
88
181
|
interval_seconds=self.monitor.act_mem_update_interval,
|
|
89
182
|
label=ANSWER_LABEL,
|
|
@@ -93,94 +186,121 @@ class GeneralScheduler(BaseScheduler):
|
|
|
93
186
|
)
|
|
94
187
|
|
|
95
188
|
def _add_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
|
|
96
|
-
logger.
|
|
189
|
+
logger.info(f"Messages {messages} assigned to {ADD_LABEL} handler.")
|
|
97
190
|
# Process the query in a session turn
|
|
98
191
|
grouped_messages = self.dispatcher.group_messages_by_user_and_cube(messages=messages)
|
|
99
192
|
|
|
100
|
-
self.
|
|
193
|
+
self.validate_schedule_messages(messages=messages, label=ADD_LABEL)
|
|
194
|
+
try:
|
|
195
|
+
for user_id in grouped_messages:
|
|
196
|
+
for mem_cube_id in grouped_messages[user_id]:
|
|
197
|
+
messages = grouped_messages[user_id][mem_cube_id]
|
|
198
|
+
if len(messages) == 0:
|
|
199
|
+
return
|
|
101
200
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
messages = grouped_messages[user_id][mem_cube_id]
|
|
105
|
-
if len(messages) == 0:
|
|
106
|
-
return
|
|
201
|
+
# for status update
|
|
202
|
+
self._set_current_context_from_message(msg=messages[0])
|
|
107
203
|
|
|
108
|
-
|
|
109
|
-
|
|
204
|
+
# submit logs
|
|
205
|
+
for msg in messages:
|
|
206
|
+
try:
|
|
207
|
+
userinput_memory_ids = json.loads(msg.content)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(f"Error: {e}. Content: {msg.content}", exc_info=True)
|
|
210
|
+
userinput_memory_ids = []
|
|
110
211
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
user_id=msg.user_id,
|
|
117
|
-
mem_cube_id=msg.mem_cube_id,
|
|
118
|
-
mem_cube=msg.mem_cube,
|
|
119
|
-
)
|
|
212
|
+
mem_cube = msg.mem_cube
|
|
213
|
+
for memory_id in userinput_memory_ids:
|
|
214
|
+
mem_item: TextualMemoryItem = mem_cube.text_mem.get(memory_id=memory_id)
|
|
215
|
+
mem_type = mem_item.metadata.memory_type
|
|
216
|
+
mem_content = mem_item.memory
|
|
120
217
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
218
|
+
self.log_adding_memory(
|
|
219
|
+
memory=mem_content,
|
|
220
|
+
memory_type=mem_type,
|
|
221
|
+
user_id=msg.user_id,
|
|
222
|
+
mem_cube_id=msg.mem_cube_id,
|
|
223
|
+
mem_cube=msg.mem_cube,
|
|
224
|
+
log_func_callback=self._submit_web_logs,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# update activation memories
|
|
228
|
+
if self.enable_act_memory_update:
|
|
229
|
+
self.update_activation_memory_periodically(
|
|
230
|
+
interval_seconds=self.monitor.act_mem_update_interval,
|
|
231
|
+
label=ADD_LABEL,
|
|
232
|
+
user_id=user_id,
|
|
233
|
+
mem_cube_id=mem_cube_id,
|
|
234
|
+
mem_cube=messages[0].mem_cube,
|
|
235
|
+
)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"Error: {e}", exc_info=True)
|
|
130
238
|
|
|
131
239
|
def process_session_turn(
|
|
132
240
|
self,
|
|
133
241
|
queries: str | list[str],
|
|
134
|
-
user_id: str,
|
|
135
|
-
mem_cube_id: str,
|
|
242
|
+
user_id: UserID | str,
|
|
243
|
+
mem_cube_id: MemCubeID | str,
|
|
136
244
|
mem_cube: GeneralMemCube,
|
|
137
245
|
top_k: int = 10,
|
|
138
|
-
|
|
139
|
-
) -> None:
|
|
246
|
+
) -> tuple[list[TextualMemoryItem], list[TextualMemoryItem]] | None:
|
|
140
247
|
"""
|
|
141
248
|
Process a dialog turn:
|
|
142
249
|
- If q_list reaches window size, trigger retrieval;
|
|
143
250
|
- Immediately switch to the new memory if retrieval is triggered.
|
|
144
251
|
"""
|
|
145
|
-
if isinstance(queries, str):
|
|
146
|
-
queries = [queries]
|
|
147
|
-
|
|
148
|
-
if query_history is None:
|
|
149
|
-
query_history = queries
|
|
150
|
-
else:
|
|
151
|
-
query_history.extend(queries)
|
|
152
252
|
|
|
153
253
|
text_mem_base = mem_cube.text_mem
|
|
154
254
|
if not isinstance(text_mem_base, TreeTextMemory):
|
|
155
255
|
logger.error("Not implemented!", exc_info=True)
|
|
156
256
|
return
|
|
157
257
|
|
|
158
|
-
|
|
159
|
-
|
|
258
|
+
logger.info(f"Processing {len(queries)} queries.")
|
|
259
|
+
|
|
260
|
+
cur_working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
|
|
261
|
+
text_working_memory: list[str] = [w_m.memory for w_m in cur_working_memory]
|
|
160
262
|
intent_result = self.monitor.detect_intent(
|
|
161
|
-
q_list=
|
|
263
|
+
q_list=queries, text_working_memory=text_working_memory
|
|
162
264
|
)
|
|
163
265
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
mem_cube_id
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
266
|
+
time_trigger_flag = False
|
|
267
|
+
if self.monitor.timed_trigger(
|
|
268
|
+
last_time=self.monitor.last_query_consume_time,
|
|
269
|
+
interval_seconds=self.monitor.query_trigger_interval,
|
|
270
|
+
):
|
|
271
|
+
time_trigger_flag = True
|
|
272
|
+
|
|
273
|
+
if (not intent_result["trigger_retrieval"]) and (not time_trigger_flag):
|
|
274
|
+
logger.info(f"Query schedule not triggered. Intent_result: {intent_result}")
|
|
275
|
+
return
|
|
276
|
+
elif (not intent_result["trigger_retrieval"]) and time_trigger_flag:
|
|
277
|
+
logger.info("Query schedule is forced to trigger due to time ticker")
|
|
278
|
+
intent_result["trigger_retrieval"] = True
|
|
279
|
+
intent_result["missing_evidences"] = queries
|
|
280
|
+
else:
|
|
281
|
+
logger.info(
|
|
282
|
+
f'Query schedule triggered for user "{user_id}" and mem_cube "{mem_cube_id}".'
|
|
283
|
+
f" Missing evidences: {intent_result['missing_evidences']}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
missing_evidences = intent_result["missing_evidences"]
|
|
287
|
+
num_evidence = len(missing_evidences)
|
|
288
|
+
k_per_evidence = max(1, top_k // max(1, num_evidence))
|
|
289
|
+
new_candidates = []
|
|
290
|
+
for item in missing_evidences:
|
|
291
|
+
logger.info(f"missing_evidences: {item}")
|
|
292
|
+
results: list[TextualMemoryItem] = self.retriever.search(
|
|
293
|
+
query=item, mem_cube=mem_cube, top_k=k_per_evidence, method=self.search_method
|
|
294
|
+
)
|
|
295
|
+
logger.info(
|
|
296
|
+
f"search results for {missing_evidences}: {[one.memory for one in results]}"
|
|
297
|
+
)
|
|
298
|
+
new_candidates.extend(results)
|
|
299
|
+
|
|
300
|
+
if len(new_candidates) == 0:
|
|
301
|
+
logger.warning(
|
|
302
|
+
f"As new_candidates is empty, new_candidates is set same to working_memory.\n"
|
|
303
|
+
f"time_trigger_flag: {time_trigger_flag}; intent_result: {intent_result}"
|
|
185
304
|
)
|
|
186
|
-
|
|
305
|
+
new_candidates = cur_working_memory
|
|
306
|
+
return cur_working_memory, new_candidates
|
|
@@ -3,7 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
from memos.llms.base import BaseLLM
|
|
4
4
|
from memos.log import get_logger
|
|
5
5
|
from memos.mem_cube.general import GeneralMemCube
|
|
6
|
-
from memos.mem_scheduler.
|
|
6
|
+
from memos.mem_scheduler.schemas.general_schemas import BASE_DIR
|
|
7
7
|
from memos.templates.mem_scheduler_prompts import PROMPT_MAPPING
|
|
8
8
|
|
|
9
9
|
|
|
@@ -17,8 +17,8 @@ class BaseSchedulerModule:
|
|
|
17
17
|
|
|
18
18
|
self._chat_llm = None
|
|
19
19
|
self._process_llm = None
|
|
20
|
-
self.
|
|
21
|
-
self.
|
|
20
|
+
self.current_mem_cube_id: str | None = None
|
|
21
|
+
self.current_mem_cube: GeneralMemCube | None = None
|
|
22
22
|
self.mem_cubes: dict[str, GeneralMemCube] = {}
|
|
23
23
|
|
|
24
24
|
def load_template(self, template_name: str) -> str:
|
|
@@ -75,9 +75,9 @@ class BaseSchedulerModule:
|
|
|
75
75
|
@property
|
|
76
76
|
def mem_cube(self) -> GeneralMemCube:
|
|
77
77
|
"""The memory cube associated with this MemChat."""
|
|
78
|
-
return self.
|
|
78
|
+
return self.current_mem_cube
|
|
79
79
|
|
|
80
80
|
@mem_cube.setter
|
|
81
81
|
def mem_cube(self, value: GeneralMemCube) -> None:
|
|
82
82
|
"""The memory cube associated with this MemChat."""
|
|
83
|
-
self.
|
|
83
|
+
self.current_mem_cube = value
|
|
@@ -4,7 +4,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
4
4
|
|
|
5
5
|
from memos.log import get_logger
|
|
6
6
|
from memos.mem_scheduler.modules.base import BaseSchedulerModule
|
|
7
|
-
from memos.mem_scheduler.
|
|
7
|
+
from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
logger = get_logger(__name__)
|
|
@@ -22,7 +22,7 @@ class SchedulerDispatcher(BaseSchedulerModule):
|
|
|
22
22
|
- Bulk handler registration
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
def __init__(self, max_workers=
|
|
25
|
+
def __init__(self, max_workers=30, enable_parallel_dispatch=False):
|
|
26
26
|
super().__init__()
|
|
27
27
|
# Main dispatcher thread pool
|
|
28
28
|
self.max_workers = max_workers
|
|
@@ -128,16 +128,13 @@ class SchedulerDispatcher(BaseSchedulerModule):
|
|
|
128
128
|
else:
|
|
129
129
|
handler = self.handlers[label]
|
|
130
130
|
# dispatch to different handler
|
|
131
|
-
logger.debug(f"Dispatch {len(msgs)}
|
|
131
|
+
logger.debug(f"Dispatch {len(msgs)} message(s) to {label} handler.")
|
|
132
132
|
if self.enable_parallel_dispatch and self.dispatcher_executor is not None:
|
|
133
133
|
# Capture variables in lambda to avoid loop variable issues
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
logger.debug(f"Dispatched {len(msgs)} messages as future task")
|
|
137
|
-
return future
|
|
134
|
+
self.dispatcher_executor.submit(handler, msgs)
|
|
135
|
+
logger.info(f"Dispatched {len(msgs)} message(s) as future task")
|
|
138
136
|
else:
|
|
139
137
|
handler(msgs)
|
|
140
|
-
return None
|
|
141
138
|
|
|
142
139
|
def join(self, timeout: float | None = None) -> bool:
|
|
143
140
|
"""Wait for all dispatched tasks to complete.
|
|
@@ -159,7 +156,7 @@ class SchedulerDispatcher(BaseSchedulerModule):
|
|
|
159
156
|
if self.dispatcher_executor is not None:
|
|
160
157
|
self.dispatcher_executor.shutdown(wait=True)
|
|
161
158
|
self._running = False
|
|
162
|
-
logger.info("Dispatcher has been shutdown")
|
|
159
|
+
logger.info("Dispatcher has been shutdown.")
|
|
163
160
|
|
|
164
161
|
def __enter__(self):
|
|
165
162
|
self._running = True
|
|
@@ -1,20 +1,85 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
2
|
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from datetime import datetime
|
|
3
5
|
from queue import Empty, Full, Queue
|
|
4
|
-
from typing import TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
5
7
|
|
|
8
|
+
from pydantic import field_serializer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pydantic import BaseModel
|
|
6
13
|
|
|
7
14
|
T = TypeVar("T")
|
|
8
15
|
|
|
16
|
+
BaseModelType = TypeVar("T", bound="BaseModel")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DictConversionMixin:
|
|
20
|
+
"""
|
|
21
|
+
Provides conversion functionality between Pydantic models and dictionaries,
|
|
22
|
+
including datetime serialization handling.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@field_serializer("timestamp", check_fields=False)
|
|
26
|
+
def serialize_datetime(self, dt: datetime | None, _info) -> str | None:
|
|
27
|
+
"""
|
|
28
|
+
Custom datetime serialization logic.
|
|
29
|
+
- Supports timezone-aware datetime objects
|
|
30
|
+
- Compatible with models without timestamp field (via check_fields=False)
|
|
31
|
+
"""
|
|
32
|
+
if dt is None:
|
|
33
|
+
return None
|
|
34
|
+
return dt.isoformat()
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Convert model instance to dictionary.
|
|
39
|
+
- Uses model_dump to ensure field consistency
|
|
40
|
+
- Prioritizes custom serializer for timestamp handling
|
|
41
|
+
"""
|
|
42
|
+
dump_data = self.model_dump()
|
|
43
|
+
if hasattr(self, "timestamp") and self.timestamp is not None:
|
|
44
|
+
dump_data["timestamp"] = self.serialize_datetime(self.timestamp, None)
|
|
45
|
+
return dump_data
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls: type[BaseModelType], data: dict) -> BaseModelType:
|
|
49
|
+
"""
|
|
50
|
+
Create model instance from dictionary.
|
|
51
|
+
- Automatically converts timestamp strings to datetime objects
|
|
52
|
+
"""
|
|
53
|
+
data_copy = data.copy() # Avoid modifying original dictionary
|
|
54
|
+
if "timestamp" in data_copy and isinstance(data_copy["timestamp"], str):
|
|
55
|
+
try:
|
|
56
|
+
data_copy["timestamp"] = datetime.fromisoformat(data_copy["timestamp"])
|
|
57
|
+
except ValueError:
|
|
58
|
+
# Handle invalid time formats - adjust as needed (e.g., log warning or set to None)
|
|
59
|
+
data_copy["timestamp"] = None
|
|
60
|
+
|
|
61
|
+
return cls(**data_copy)
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Convert to formatted JSON string.
|
|
66
|
+
- Used for user-friendly display in print() or str() calls
|
|
67
|
+
"""
|
|
68
|
+
return json.dumps(
|
|
69
|
+
self.to_dict(),
|
|
70
|
+
indent=4,
|
|
71
|
+
ensure_ascii=False,
|
|
72
|
+
default=lambda o: str(o), # Handle other non-serializable objects
|
|
73
|
+
)
|
|
74
|
+
|
|
9
75
|
|
|
10
76
|
class AutoDroppingQueue(Queue[T]):
|
|
11
77
|
"""A thread-safe queue that automatically drops the oldest item when full."""
|
|
12
78
|
|
|
13
79
|
def __init__(self, maxsize: int = 0):
|
|
14
80
|
super().__init__(maxsize=maxsize)
|
|
15
|
-
self._lock = threading.Lock() # Additional lock to prevent race conditions
|
|
16
81
|
|
|
17
|
-
def put(self, item: T, block: bool =
|
|
82
|
+
def put(self, item: T, block: bool = False, timeout: float | None = None) -> None:
|
|
18
83
|
"""Put an item into the queue.
|
|
19
84
|
|
|
20
85
|
If the queue is full, the oldest item will be automatically removed to make space.
|
|
@@ -25,15 +90,15 @@ class AutoDroppingQueue(Queue[T]):
|
|
|
25
90
|
block: Ignored (kept for compatibility with Queue interface)
|
|
26
91
|
timeout: Ignored (kept for compatibility with Queue interface)
|
|
27
92
|
"""
|
|
28
|
-
|
|
29
|
-
try
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
93
|
+
try:
|
|
94
|
+
# First try non-blocking put
|
|
95
|
+
super().put(item, block=block, timeout=timeout)
|
|
96
|
+
except Full:
|
|
97
|
+
with suppress(Empty):
|
|
98
|
+
self.get_nowait() # Remove oldest item
|
|
99
|
+
# Retry putting the new item
|
|
100
|
+
super().put(item, block=block, timeout=timeout)
|
|
101
|
+
|
|
102
|
+
def get_queue_content_without_pop(self) -> list[T]:
|
|
103
|
+
"""Return a copy of the queue's contents without modifying it."""
|
|
104
|
+
return list(self.queue)
|