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.

Files changed (74) hide show
  1. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/METADATA +2 -1
  2. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/RECORD +72 -55
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +156 -65
  5. memos/api/context/context.py +147 -0
  6. memos/api/context/dependencies.py +90 -0
  7. memos/api/product_models.py +5 -1
  8. memos/api/routers/product_router.py +54 -26
  9. memos/configs/graph_db.py +49 -1
  10. memos/configs/internet_retriever.py +6 -0
  11. memos/configs/mem_os.py +5 -0
  12. memos/configs/mem_reader.py +9 -0
  13. memos/configs/mem_scheduler.py +18 -4
  14. memos/configs/mem_user.py +58 -0
  15. memos/graph_dbs/base.py +9 -1
  16. memos/graph_dbs/factory.py +2 -0
  17. memos/graph_dbs/nebular.py +1364 -0
  18. memos/graph_dbs/neo4j.py +4 -4
  19. memos/log.py +1 -1
  20. memos/mem_cube/utils.py +13 -6
  21. memos/mem_os/core.py +140 -30
  22. memos/mem_os/main.py +1 -1
  23. memos/mem_os/product.py +266 -152
  24. memos/mem_os/utils/format_utils.py +314 -67
  25. memos/mem_reader/simple_struct.py +13 -5
  26. memos/mem_scheduler/base_scheduler.py +220 -250
  27. memos/mem_scheduler/general_scheduler.py +193 -73
  28. memos/mem_scheduler/modules/base.py +5 -5
  29. memos/mem_scheduler/modules/dispatcher.py +6 -9
  30. memos/mem_scheduler/modules/misc.py +81 -16
  31. memos/mem_scheduler/modules/monitor.py +52 -41
  32. memos/mem_scheduler/modules/rabbitmq_service.py +9 -7
  33. memos/mem_scheduler/modules/retriever.py +108 -191
  34. memos/mem_scheduler/modules/scheduler_logger.py +255 -0
  35. memos/mem_scheduler/mos_for_test_scheduler.py +16 -19
  36. memos/mem_scheduler/schemas/__init__.py +0 -0
  37. memos/mem_scheduler/schemas/general_schemas.py +43 -0
  38. memos/mem_scheduler/schemas/message_schemas.py +148 -0
  39. memos/mem_scheduler/schemas/monitor_schemas.py +329 -0
  40. memos/mem_scheduler/utils/__init__.py +0 -0
  41. memos/mem_scheduler/utils/filter_utils.py +176 -0
  42. memos/mem_scheduler/utils/misc_utils.py +61 -0
  43. memos/mem_user/factory.py +94 -0
  44. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  45. memos/mem_user/mysql_user_manager.py +500 -0
  46. memos/mem_user/persistent_factory.py +96 -0
  47. memos/mem_user/user_manager.py +4 -4
  48. memos/memories/activation/item.py +4 -0
  49. memos/memories/textual/base.py +1 -1
  50. memos/memories/textual/general.py +35 -91
  51. memos/memories/textual/item.py +5 -33
  52. memos/memories/textual/tree.py +13 -7
  53. memos/memories/textual/tree_text_memory/organize/conflict.py +4 -2
  54. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +47 -43
  55. memos/memories/textual/tree_text_memory/organize/reorganizer.py +8 -5
  56. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  57. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
  58. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  59. memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -23
  60. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -15
  61. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  62. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  63. memos/memos_tools/dinding_report_bot.py +422 -0
  64. memos/memos_tools/notification_service.py +44 -0
  65. memos/memos_tools/notification_utils.py +96 -0
  66. memos/settings.py +3 -1
  67. memos/templates/mem_reader_prompts.py +2 -1
  68. memos/templates/mem_scheduler_prompts.py +41 -7
  69. memos/templates/mos_prompts.py +87 -0
  70. memos/mem_scheduler/modules/schemas.py +0 -328
  71. memos/mem_scheduler/utils.py +0 -75
  72. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
  73. {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
  74. {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.modules.schemas import (
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
- ScheduleMessageItem,
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.debug(f"Messages {messages} assigned to {QUERY_LABEL} handler.")
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._validate_messages(messages=messages, label=QUERY_LABEL)
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
- self.process_session_turn(
56
- queries=[msg.content for msg in messages],
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=messages[0].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.debug(f"Messages {messages} assigned to {ANSWER_LABEL} handler.")
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._validate_messages(messages=messages, label=ANSWER_LABEL)
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 acivation memories
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.debug(f"Messages {messages} assigned to {ADD_LABEL} handler.")
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._validate_messages(messages=messages, label=ADD_LABEL)
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
- 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
201
+ # for status update
202
+ self._set_current_context_from_message(msg=messages[0])
107
203
 
108
- # for status update
109
- self._set_current_context_from_message(msg=messages[0])
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
- # 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
- )
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
- # 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
- )
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
- query_history: list[str] | None = None,
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
- working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
159
- text_working_memory: list[str] = [w_m.memory for w_m in working_memory]
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=query_history, text_working_memory=text_working_memory
263
+ q_list=queries, text_working_memory=text_working_memory
162
264
  )
163
265
 
164
- if intent_result["trigger_retrieval"]:
165
- missing_evidences = intent_result["missing_evidences"]
166
- num_evidence = len(missing_evidences)
167
- k_per_evidence = max(1, top_k // max(1, num_evidence))
168
- new_candidates = []
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}")
175
- new_candidates.extend(results)
176
-
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,
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
- logger.debug(f"size of new_order_working_memory: {len(new_order_working_memory)}")
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.modules.schemas import BASE_DIR
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._current_mem_cube_id: str | None = None
21
- self._current_mem_cube: GeneralMemCube | None = None
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._current_mem_cube
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._current_mem_cube = value
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.modules.schemas import ScheduleMessageItem
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=3, enable_parallel_dispatch=False):
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)} messages to {label} handler.")
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
- # 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
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 threading
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 = True, timeout: float | None = None) -> None:
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
- 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)
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)