MemoryOS 2.0.3__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.
Files changed (315) hide show
  1. memoryos-2.0.3.dist-info/METADATA +418 -0
  2. memoryos-2.0.3.dist-info/RECORD +315 -0
  3. memoryos-2.0.3.dist-info/WHEEL +4 -0
  4. memoryos-2.0.3.dist-info/entry_points.txt +3 -0
  5. memoryos-2.0.3.dist-info/licenses/LICENSE +201 -0
  6. memos/__init__.py +20 -0
  7. memos/api/client.py +571 -0
  8. memos/api/config.py +1018 -0
  9. memos/api/context/dependencies.py +50 -0
  10. memos/api/exceptions.py +53 -0
  11. memos/api/handlers/__init__.py +62 -0
  12. memos/api/handlers/add_handler.py +158 -0
  13. memos/api/handlers/base_handler.py +194 -0
  14. memos/api/handlers/chat_handler.py +1401 -0
  15. memos/api/handlers/component_init.py +388 -0
  16. memos/api/handlers/config_builders.py +190 -0
  17. memos/api/handlers/feedback_handler.py +93 -0
  18. memos/api/handlers/formatters_handler.py +237 -0
  19. memos/api/handlers/memory_handler.py +316 -0
  20. memos/api/handlers/scheduler_handler.py +497 -0
  21. memos/api/handlers/search_handler.py +222 -0
  22. memos/api/handlers/suggestion_handler.py +117 -0
  23. memos/api/mcp_serve.py +614 -0
  24. memos/api/middleware/request_context.py +101 -0
  25. memos/api/product_api.py +38 -0
  26. memos/api/product_models.py +1206 -0
  27. memos/api/routers/__init__.py +1 -0
  28. memos/api/routers/product_router.py +477 -0
  29. memos/api/routers/server_router.py +394 -0
  30. memos/api/server_api.py +44 -0
  31. memos/api/start_api.py +433 -0
  32. memos/chunkers/__init__.py +4 -0
  33. memos/chunkers/base.py +24 -0
  34. memos/chunkers/charactertext_chunker.py +41 -0
  35. memos/chunkers/factory.py +24 -0
  36. memos/chunkers/markdown_chunker.py +62 -0
  37. memos/chunkers/sentence_chunker.py +54 -0
  38. memos/chunkers/simple_chunker.py +50 -0
  39. memos/cli.py +113 -0
  40. memos/configs/__init__.py +0 -0
  41. memos/configs/base.py +82 -0
  42. memos/configs/chunker.py +59 -0
  43. memos/configs/embedder.py +88 -0
  44. memos/configs/graph_db.py +236 -0
  45. memos/configs/internet_retriever.py +100 -0
  46. memos/configs/llm.py +151 -0
  47. memos/configs/mem_agent.py +54 -0
  48. memos/configs/mem_chat.py +81 -0
  49. memos/configs/mem_cube.py +105 -0
  50. memos/configs/mem_os.py +83 -0
  51. memos/configs/mem_reader.py +91 -0
  52. memos/configs/mem_scheduler.py +385 -0
  53. memos/configs/mem_user.py +70 -0
  54. memos/configs/memory.py +324 -0
  55. memos/configs/parser.py +38 -0
  56. memos/configs/reranker.py +18 -0
  57. memos/configs/utils.py +8 -0
  58. memos/configs/vec_db.py +80 -0
  59. memos/context/context.py +355 -0
  60. memos/dependency.py +52 -0
  61. memos/deprecation.py +262 -0
  62. memos/embedders/__init__.py +0 -0
  63. memos/embedders/ark.py +95 -0
  64. memos/embedders/base.py +106 -0
  65. memos/embedders/factory.py +29 -0
  66. memos/embedders/ollama.py +77 -0
  67. memos/embedders/sentence_transformer.py +49 -0
  68. memos/embedders/universal_api.py +51 -0
  69. memos/exceptions.py +30 -0
  70. memos/graph_dbs/__init__.py +0 -0
  71. memos/graph_dbs/base.py +274 -0
  72. memos/graph_dbs/factory.py +27 -0
  73. memos/graph_dbs/item.py +46 -0
  74. memos/graph_dbs/nebular.py +1794 -0
  75. memos/graph_dbs/neo4j.py +1942 -0
  76. memos/graph_dbs/neo4j_community.py +1058 -0
  77. memos/graph_dbs/polardb.py +5446 -0
  78. memos/hello_world.py +97 -0
  79. memos/llms/__init__.py +0 -0
  80. memos/llms/base.py +25 -0
  81. memos/llms/deepseek.py +13 -0
  82. memos/llms/factory.py +38 -0
  83. memos/llms/hf.py +443 -0
  84. memos/llms/hf_singleton.py +114 -0
  85. memos/llms/ollama.py +135 -0
  86. memos/llms/openai.py +222 -0
  87. memos/llms/openai_new.py +198 -0
  88. memos/llms/qwen.py +13 -0
  89. memos/llms/utils.py +14 -0
  90. memos/llms/vllm.py +218 -0
  91. memos/log.py +237 -0
  92. memos/mem_agent/base.py +19 -0
  93. memos/mem_agent/deepsearch_agent.py +391 -0
  94. memos/mem_agent/factory.py +36 -0
  95. memos/mem_chat/__init__.py +0 -0
  96. memos/mem_chat/base.py +30 -0
  97. memos/mem_chat/factory.py +21 -0
  98. memos/mem_chat/simple.py +200 -0
  99. memos/mem_cube/__init__.py +0 -0
  100. memos/mem_cube/base.py +30 -0
  101. memos/mem_cube/general.py +240 -0
  102. memos/mem_cube/navie.py +172 -0
  103. memos/mem_cube/utils.py +169 -0
  104. memos/mem_feedback/base.py +15 -0
  105. memos/mem_feedback/feedback.py +1192 -0
  106. memos/mem_feedback/simple_feedback.py +40 -0
  107. memos/mem_feedback/utils.py +230 -0
  108. memos/mem_os/client.py +5 -0
  109. memos/mem_os/core.py +1203 -0
  110. memos/mem_os/main.py +582 -0
  111. memos/mem_os/product.py +1608 -0
  112. memos/mem_os/product_server.py +455 -0
  113. memos/mem_os/utils/default_config.py +359 -0
  114. memos/mem_os/utils/format_utils.py +1403 -0
  115. memos/mem_os/utils/reference_utils.py +162 -0
  116. memos/mem_reader/__init__.py +0 -0
  117. memos/mem_reader/base.py +47 -0
  118. memos/mem_reader/factory.py +53 -0
  119. memos/mem_reader/memory.py +298 -0
  120. memos/mem_reader/multi_modal_struct.py +965 -0
  121. memos/mem_reader/read_multi_modal/__init__.py +43 -0
  122. memos/mem_reader/read_multi_modal/assistant_parser.py +311 -0
  123. memos/mem_reader/read_multi_modal/base.py +273 -0
  124. memos/mem_reader/read_multi_modal/file_content_parser.py +826 -0
  125. memos/mem_reader/read_multi_modal/image_parser.py +359 -0
  126. memos/mem_reader/read_multi_modal/multi_modal_parser.py +252 -0
  127. memos/mem_reader/read_multi_modal/string_parser.py +139 -0
  128. memos/mem_reader/read_multi_modal/system_parser.py +327 -0
  129. memos/mem_reader/read_multi_modal/text_content_parser.py +131 -0
  130. memos/mem_reader/read_multi_modal/tool_parser.py +210 -0
  131. memos/mem_reader/read_multi_modal/user_parser.py +218 -0
  132. memos/mem_reader/read_multi_modal/utils.py +358 -0
  133. memos/mem_reader/simple_struct.py +912 -0
  134. memos/mem_reader/strategy_struct.py +163 -0
  135. memos/mem_reader/utils.py +157 -0
  136. memos/mem_scheduler/__init__.py +0 -0
  137. memos/mem_scheduler/analyzer/__init__.py +0 -0
  138. memos/mem_scheduler/analyzer/api_analyzer.py +714 -0
  139. memos/mem_scheduler/analyzer/eval_analyzer.py +219 -0
  140. memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +571 -0
  141. memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
  142. memos/mem_scheduler/base_scheduler.py +1319 -0
  143. memos/mem_scheduler/general_modules/__init__.py +0 -0
  144. memos/mem_scheduler/general_modules/api_misc.py +137 -0
  145. memos/mem_scheduler/general_modules/base.py +80 -0
  146. memos/mem_scheduler/general_modules/init_components_for_scheduler.py +425 -0
  147. memos/mem_scheduler/general_modules/misc.py +313 -0
  148. memos/mem_scheduler/general_modules/scheduler_logger.py +389 -0
  149. memos/mem_scheduler/general_modules/task_threads.py +315 -0
  150. memos/mem_scheduler/general_scheduler.py +1495 -0
  151. memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
  152. memos/mem_scheduler/memory_manage_modules/memory_filter.py +306 -0
  153. memos/mem_scheduler/memory_manage_modules/retriever.py +547 -0
  154. memos/mem_scheduler/monitors/__init__.py +0 -0
  155. memos/mem_scheduler/monitors/dispatcher_monitor.py +366 -0
  156. memos/mem_scheduler/monitors/general_monitor.py +394 -0
  157. memos/mem_scheduler/monitors/task_schedule_monitor.py +254 -0
  158. memos/mem_scheduler/optimized_scheduler.py +410 -0
  159. memos/mem_scheduler/orm_modules/__init__.py +0 -0
  160. memos/mem_scheduler/orm_modules/api_redis_model.py +518 -0
  161. memos/mem_scheduler/orm_modules/base_model.py +729 -0
  162. memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
  163. memos/mem_scheduler/orm_modules/redis_model.py +699 -0
  164. memos/mem_scheduler/scheduler_factory.py +23 -0
  165. memos/mem_scheduler/schemas/__init__.py +0 -0
  166. memos/mem_scheduler/schemas/analyzer_schemas.py +52 -0
  167. memos/mem_scheduler/schemas/api_schemas.py +233 -0
  168. memos/mem_scheduler/schemas/general_schemas.py +55 -0
  169. memos/mem_scheduler/schemas/message_schemas.py +173 -0
  170. memos/mem_scheduler/schemas/monitor_schemas.py +406 -0
  171. memos/mem_scheduler/schemas/task_schemas.py +132 -0
  172. memos/mem_scheduler/task_schedule_modules/__init__.py +0 -0
  173. memos/mem_scheduler/task_schedule_modules/dispatcher.py +740 -0
  174. memos/mem_scheduler/task_schedule_modules/local_queue.py +247 -0
  175. memos/mem_scheduler/task_schedule_modules/orchestrator.py +74 -0
  176. memos/mem_scheduler/task_schedule_modules/redis_queue.py +1385 -0
  177. memos/mem_scheduler/task_schedule_modules/task_queue.py +162 -0
  178. memos/mem_scheduler/utils/__init__.py +0 -0
  179. memos/mem_scheduler/utils/api_utils.py +77 -0
  180. memos/mem_scheduler/utils/config_utils.py +100 -0
  181. memos/mem_scheduler/utils/db_utils.py +50 -0
  182. memos/mem_scheduler/utils/filter_utils.py +176 -0
  183. memos/mem_scheduler/utils/metrics.py +125 -0
  184. memos/mem_scheduler/utils/misc_utils.py +290 -0
  185. memos/mem_scheduler/utils/monitor_event_utils.py +67 -0
  186. memos/mem_scheduler/utils/status_tracker.py +229 -0
  187. memos/mem_scheduler/webservice_modules/__init__.py +0 -0
  188. memos/mem_scheduler/webservice_modules/rabbitmq_service.py +485 -0
  189. memos/mem_scheduler/webservice_modules/redis_service.py +380 -0
  190. memos/mem_user/factory.py +94 -0
  191. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  192. memos/mem_user/mysql_user_manager.py +502 -0
  193. memos/mem_user/persistent_factory.py +98 -0
  194. memos/mem_user/persistent_user_manager.py +260 -0
  195. memos/mem_user/redis_persistent_user_manager.py +225 -0
  196. memos/mem_user/user_manager.py +488 -0
  197. memos/memories/__init__.py +0 -0
  198. memos/memories/activation/__init__.py +0 -0
  199. memos/memories/activation/base.py +42 -0
  200. memos/memories/activation/item.py +56 -0
  201. memos/memories/activation/kv.py +292 -0
  202. memos/memories/activation/vllmkv.py +219 -0
  203. memos/memories/base.py +19 -0
  204. memos/memories/factory.py +42 -0
  205. memos/memories/parametric/__init__.py +0 -0
  206. memos/memories/parametric/base.py +19 -0
  207. memos/memories/parametric/item.py +11 -0
  208. memos/memories/parametric/lora.py +41 -0
  209. memos/memories/textual/__init__.py +0 -0
  210. memos/memories/textual/base.py +92 -0
  211. memos/memories/textual/general.py +236 -0
  212. memos/memories/textual/item.py +304 -0
  213. memos/memories/textual/naive.py +187 -0
  214. memos/memories/textual/prefer_text_memory/__init__.py +0 -0
  215. memos/memories/textual/prefer_text_memory/adder.py +504 -0
  216. memos/memories/textual/prefer_text_memory/config.py +106 -0
  217. memos/memories/textual/prefer_text_memory/extractor.py +221 -0
  218. memos/memories/textual/prefer_text_memory/factory.py +85 -0
  219. memos/memories/textual/prefer_text_memory/retrievers.py +177 -0
  220. memos/memories/textual/prefer_text_memory/spliter.py +132 -0
  221. memos/memories/textual/prefer_text_memory/utils.py +93 -0
  222. memos/memories/textual/preference.py +344 -0
  223. memos/memories/textual/simple_preference.py +161 -0
  224. memos/memories/textual/simple_tree.py +69 -0
  225. memos/memories/textual/tree.py +459 -0
  226. memos/memories/textual/tree_text_memory/__init__.py +0 -0
  227. memos/memories/textual/tree_text_memory/organize/__init__.py +0 -0
  228. memos/memories/textual/tree_text_memory/organize/handler.py +184 -0
  229. memos/memories/textual/tree_text_memory/organize/manager.py +518 -0
  230. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +238 -0
  231. memos/memories/textual/tree_text_memory/organize/reorganizer.py +622 -0
  232. memos/memories/textual/tree_text_memory/retrieve/__init__.py +0 -0
  233. memos/memories/textual/tree_text_memory/retrieve/advanced_searcher.py +364 -0
  234. memos/memories/textual/tree_text_memory/retrieve/bm25_util.py +186 -0
  235. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +419 -0
  236. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +270 -0
  237. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +102 -0
  238. memos/memories/textual/tree_text_memory/retrieve/reasoner.py +61 -0
  239. memos/memories/textual/tree_text_memory/retrieve/recall.py +497 -0
  240. memos/memories/textual/tree_text_memory/retrieve/reranker.py +111 -0
  241. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +16 -0
  242. memos/memories/textual/tree_text_memory/retrieve/retrieve_utils.py +472 -0
  243. memos/memories/textual/tree_text_memory/retrieve/searcher.py +848 -0
  244. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +135 -0
  245. memos/memories/textual/tree_text_memory/retrieve/utils.py +54 -0
  246. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +387 -0
  247. memos/memos_tools/dinding_report_bot.py +453 -0
  248. memos/memos_tools/lockfree_dict.py +120 -0
  249. memos/memos_tools/notification_service.py +44 -0
  250. memos/memos_tools/notification_utils.py +142 -0
  251. memos/memos_tools/singleton.py +174 -0
  252. memos/memos_tools/thread_safe_dict.py +310 -0
  253. memos/memos_tools/thread_safe_dict_segment.py +382 -0
  254. memos/multi_mem_cube/__init__.py +0 -0
  255. memos/multi_mem_cube/composite_cube.py +86 -0
  256. memos/multi_mem_cube/single_cube.py +874 -0
  257. memos/multi_mem_cube/views.py +54 -0
  258. memos/parsers/__init__.py +0 -0
  259. memos/parsers/base.py +15 -0
  260. memos/parsers/factory.py +21 -0
  261. memos/parsers/markitdown.py +28 -0
  262. memos/reranker/__init__.py +4 -0
  263. memos/reranker/base.py +25 -0
  264. memos/reranker/concat.py +103 -0
  265. memos/reranker/cosine_local.py +102 -0
  266. memos/reranker/factory.py +72 -0
  267. memos/reranker/http_bge.py +324 -0
  268. memos/reranker/http_bge_strategy.py +327 -0
  269. memos/reranker/noop.py +19 -0
  270. memos/reranker/strategies/__init__.py +4 -0
  271. memos/reranker/strategies/base.py +61 -0
  272. memos/reranker/strategies/concat_background.py +94 -0
  273. memos/reranker/strategies/concat_docsource.py +110 -0
  274. memos/reranker/strategies/dialogue_common.py +109 -0
  275. memos/reranker/strategies/factory.py +31 -0
  276. memos/reranker/strategies/single_turn.py +107 -0
  277. memos/reranker/strategies/singleturn_outmem.py +98 -0
  278. memos/settings.py +10 -0
  279. memos/templates/__init__.py +0 -0
  280. memos/templates/advanced_search_prompts.py +211 -0
  281. memos/templates/cloud_service_prompt.py +107 -0
  282. memos/templates/instruction_completion.py +66 -0
  283. memos/templates/mem_agent_prompts.py +85 -0
  284. memos/templates/mem_feedback_prompts.py +822 -0
  285. memos/templates/mem_reader_prompts.py +1096 -0
  286. memos/templates/mem_reader_strategy_prompts.py +238 -0
  287. memos/templates/mem_scheduler_prompts.py +626 -0
  288. memos/templates/mem_search_prompts.py +93 -0
  289. memos/templates/mos_prompts.py +403 -0
  290. memos/templates/prefer_complete_prompt.py +735 -0
  291. memos/templates/tool_mem_prompts.py +139 -0
  292. memos/templates/tree_reorganize_prompts.py +230 -0
  293. memos/types/__init__.py +34 -0
  294. memos/types/general_types.py +151 -0
  295. memos/types/openai_chat_completion_types/__init__.py +15 -0
  296. memos/types/openai_chat_completion_types/chat_completion_assistant_message_param.py +56 -0
  297. memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py +27 -0
  298. memos/types/openai_chat_completion_types/chat_completion_content_part_input_audio_param.py +23 -0
  299. memos/types/openai_chat_completion_types/chat_completion_content_part_param.py +43 -0
  300. memos/types/openai_chat_completion_types/chat_completion_content_part_refusal_param.py +16 -0
  301. memos/types/openai_chat_completion_types/chat_completion_content_part_text_param.py +16 -0
  302. memos/types/openai_chat_completion_types/chat_completion_message_custom_tool_call_param.py +27 -0
  303. memos/types/openai_chat_completion_types/chat_completion_message_function_tool_call_param.py +32 -0
  304. memos/types/openai_chat_completion_types/chat_completion_message_param.py +18 -0
  305. memos/types/openai_chat_completion_types/chat_completion_message_tool_call_union_param.py +15 -0
  306. memos/types/openai_chat_completion_types/chat_completion_system_message_param.py +36 -0
  307. memos/types/openai_chat_completion_types/chat_completion_tool_message_param.py +30 -0
  308. memos/types/openai_chat_completion_types/chat_completion_user_message_param.py +34 -0
  309. memos/utils.py +123 -0
  310. memos/vec_dbs/__init__.py +0 -0
  311. memos/vec_dbs/base.py +117 -0
  312. memos/vec_dbs/factory.py +23 -0
  313. memos/vec_dbs/item.py +50 -0
  314. memos/vec_dbs/milvus.py +654 -0
  315. memos/vec_dbs/qdrant.py +355 -0
@@ -0,0 +1,1319 @@
1
+ import multiprocessing
2
+ import os
3
+ import threading
4
+ import time
5
+
6
+ from collections.abc import Callable
7
+ from contextlib import suppress
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Union
11
+
12
+ from sqlalchemy.engine import Engine
13
+
14
+ from memos.configs.mem_scheduler import AuthConfig, BaseSchedulerConfig
15
+ from memos.context.context import (
16
+ ContextThread,
17
+ RequestContext,
18
+ get_current_context,
19
+ get_current_trace_id,
20
+ set_request_context,
21
+ )
22
+ from memos.llms.base import BaseLLM
23
+ from memos.log import get_logger
24
+ from memos.mem_cube.base import BaseMemCube
25
+ from memos.mem_cube.general import GeneralMemCube
26
+ from memos.mem_feedback.simple_feedback import SimpleMemFeedback
27
+ from memos.mem_scheduler.general_modules.init_components_for_scheduler import init_components
28
+ from memos.mem_scheduler.general_modules.misc import AutoDroppingQueue as Queue
29
+ from memos.mem_scheduler.general_modules.scheduler_logger import SchedulerLoggerModule
30
+ from memos.mem_scheduler.memory_manage_modules.retriever import SchedulerRetriever
31
+ from memos.mem_scheduler.monitors.dispatcher_monitor import SchedulerDispatcherMonitor
32
+ from memos.mem_scheduler.monitors.general_monitor import SchedulerGeneralMonitor
33
+ from memos.mem_scheduler.monitors.task_schedule_monitor import TaskScheduleMonitor
34
+ from memos.mem_scheduler.schemas.general_schemas import (
35
+ DEFAULT_ACT_MEM_DUMP_PATH,
36
+ DEFAULT_CONSUME_BATCH,
37
+ DEFAULT_CONSUME_INTERVAL_SECONDS,
38
+ DEFAULT_CONTEXT_WINDOW_SIZE,
39
+ DEFAULT_MAX_INTERNAL_MESSAGE_QUEUE_SIZE,
40
+ DEFAULT_MAX_WEB_LOG_QUEUE_SIZE,
41
+ DEFAULT_STARTUP_MODE,
42
+ DEFAULT_THREAD_POOL_MAX_WORKERS,
43
+ DEFAULT_TOP_K,
44
+ DEFAULT_USE_REDIS_QUEUE,
45
+ STARTUP_BY_PROCESS,
46
+ TreeTextMemory_SEARCH_METHOD,
47
+ )
48
+ from memos.mem_scheduler.schemas.message_schemas import (
49
+ ScheduleLogForWebItem,
50
+ ScheduleMessageItem,
51
+ )
52
+ from memos.mem_scheduler.schemas.monitor_schemas import MemoryMonitorItem
53
+ from memos.mem_scheduler.schemas.task_schemas import (
54
+ ADD_TASK_LABEL,
55
+ ANSWER_TASK_LABEL,
56
+ MEM_ARCHIVE_TASK_LABEL,
57
+ MEM_ORGANIZE_TASK_LABEL,
58
+ MEM_UPDATE_TASK_LABEL,
59
+ QUERY_TASK_LABEL,
60
+ TaskPriorityLevel,
61
+ )
62
+ from memos.mem_scheduler.task_schedule_modules.dispatcher import SchedulerDispatcher
63
+ from memos.mem_scheduler.task_schedule_modules.orchestrator import SchedulerOrchestrator
64
+ from memos.mem_scheduler.task_schedule_modules.task_queue import ScheduleTaskQueue
65
+ from memos.mem_scheduler.utils import metrics
66
+ from memos.mem_scheduler.utils.db_utils import get_utc_now
67
+ from memos.mem_scheduler.utils.filter_utils import (
68
+ transform_name_to_key,
69
+ )
70
+ from memos.mem_scheduler.utils.misc_utils import group_messages_by_user_and_mem_cube
71
+ from memos.mem_scheduler.utils.monitor_event_utils import emit_monitor_event, to_iso
72
+ from memos.mem_scheduler.utils.status_tracker import TaskStatusTracker
73
+ from memos.mem_scheduler.webservice_modules.rabbitmq_service import RabbitMQSchedulerModule
74
+ from memos.mem_scheduler.webservice_modules.redis_service import RedisSchedulerModule
75
+ from memos.memories.activation.kv import KVCacheMemory
76
+ from memos.memories.activation.vllmkv import VLLMKVCacheItem, VLLMKVCacheMemory
77
+ from memos.memories.textual.naive import NaiveTextMemory
78
+ from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
79
+ from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher
80
+ from memos.templates.mem_scheduler_prompts import MEMORY_ASSEMBLY_TEMPLATE
81
+ from memos.types.general_types import (
82
+ MemCubeID,
83
+ UserID,
84
+ )
85
+
86
+
87
+ if TYPE_CHECKING:
88
+ import redis
89
+
90
+ from memos.reranker.http_bge import HTTPBGEReranker
91
+
92
+
93
+ logger = get_logger(__name__)
94
+
95
+
96
+ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLoggerModule):
97
+ """Base class for all mem_scheduler."""
98
+
99
+ def __init__(self, config: BaseSchedulerConfig):
100
+ """Initialize the scheduler with the given configuration."""
101
+ super().__init__()
102
+ self.config = config
103
+
104
+ # hyper-parameters
105
+ self.top_k = self.config.get("top_k", DEFAULT_TOP_K)
106
+ self.context_window_size = self.config.get(
107
+ "context_window_size", DEFAULT_CONTEXT_WINDOW_SIZE
108
+ )
109
+ self.enable_activation_memory = self.config.get("enable_activation_memory", False)
110
+ self.act_mem_dump_path = self.config.get("act_mem_dump_path", DEFAULT_ACT_MEM_DUMP_PATH)
111
+ self.search_method = self.config.get("search_method", TreeTextMemory_SEARCH_METHOD)
112
+ self.enable_parallel_dispatch = self.config.get("enable_parallel_dispatch", True)
113
+ self.thread_pool_max_workers = self.config.get(
114
+ "thread_pool_max_workers", DEFAULT_THREAD_POOL_MAX_WORKERS
115
+ )
116
+
117
+ # startup mode configuration
118
+ self.scheduler_startup_mode = self.config.get(
119
+ "scheduler_startup_mode", DEFAULT_STARTUP_MODE
120
+ )
121
+
122
+ # optional configs
123
+ self.disabled_handlers: list | None = self.config.get("disabled_handlers", None)
124
+
125
+ self.max_web_log_queue_size = self.config.get(
126
+ "max_web_log_queue_size", DEFAULT_MAX_WEB_LOG_QUEUE_SIZE
127
+ )
128
+ self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue(
129
+ maxsize=self.max_web_log_queue_size
130
+ )
131
+ self._consumer_thread = None # Reference to our consumer thread/process
132
+ self._consumer_process = None # Reference to our consumer process
133
+ self._running = False
134
+ self._consume_interval = self.config.get(
135
+ "consume_interval_seconds", DEFAULT_CONSUME_INTERVAL_SECONDS
136
+ )
137
+ self.consume_batch = self.config.get("consume_batch", DEFAULT_CONSUME_BATCH)
138
+
139
+ # message queue configuration
140
+ self.use_redis_queue = self.config.get("use_redis_queue", DEFAULT_USE_REDIS_QUEUE)
141
+ self.max_internal_message_queue_size = self.config.get(
142
+ "max_internal_message_queue_size", DEFAULT_MAX_INTERNAL_MESSAGE_QUEUE_SIZE
143
+ )
144
+ self.orchestrator = SchedulerOrchestrator()
145
+
146
+ self.searcher: Searcher | None = None
147
+ self.retriever: SchedulerRetriever | None = None
148
+ self.db_engine: Engine | None = None
149
+ self.monitor: SchedulerGeneralMonitor | None = None
150
+ self.dispatcher_monitor: SchedulerDispatcherMonitor | None = None
151
+ self.mem_reader = None # Will be set by MOSCore
152
+ self._status_tracker: TaskStatusTracker | None = None
153
+ self.metrics = metrics
154
+ self._monitor_thread = None
155
+ self.memos_message_queue = ScheduleTaskQueue(
156
+ use_redis_queue=self.use_redis_queue,
157
+ maxsize=self.max_internal_message_queue_size,
158
+ disabled_handlers=self.disabled_handlers,
159
+ orchestrator=self.orchestrator,
160
+ status_tracker=self._status_tracker,
161
+ )
162
+ self.dispatcher = SchedulerDispatcher(
163
+ config=self.config,
164
+ memos_message_queue=self.memos_message_queue,
165
+ max_workers=self.thread_pool_max_workers,
166
+ enable_parallel_dispatch=self.enable_parallel_dispatch,
167
+ status_tracker=self._status_tracker,
168
+ metrics=self.metrics,
169
+ submit_web_logs=self._submit_web_logs,
170
+ orchestrator=self.orchestrator,
171
+ )
172
+ # Task schedule monitor: initialize with underlying queue implementation
173
+ self.get_status_parallel = self.config.get("get_status_parallel", True)
174
+ self.task_schedule_monitor = TaskScheduleMonitor(
175
+ memos_message_queue=self.memos_message_queue.memos_message_queue,
176
+ dispatcher=self.dispatcher,
177
+ get_status_parallel=self.get_status_parallel,
178
+ )
179
+
180
+ # other attributes
181
+ self._context_lock = threading.Lock()
182
+ self.current_user_id: UserID | str | None = None
183
+ self.current_mem_cube_id: MemCubeID | str | None = None
184
+ self.current_mem_cube: BaseMemCube | None = None
185
+
186
+ self._mem_cubes: dict[str, BaseMemCube] = {}
187
+ self.auth_config_path: str | Path | None = self.config.get("auth_config_path", None)
188
+ self.auth_config = None
189
+ self.rabbitmq_config = None
190
+ self.feedback_server = None
191
+
192
+ def init_mem_cube(
193
+ self,
194
+ mem_cube: BaseMemCube,
195
+ searcher: Searcher | None = None,
196
+ feedback_server: SimpleMemFeedback | None = None,
197
+ ):
198
+ if mem_cube is None:
199
+ logger.error("mem_cube is None, cannot initialize", stack_info=True)
200
+ self.mem_cube = mem_cube
201
+ self.text_mem: TreeTextMemory = self.mem_cube.text_mem
202
+ self.reranker: HTTPBGEReranker = getattr(self.text_mem, "reranker", None)
203
+ if searcher is None:
204
+ if hasattr(self.text_mem, "get_searcher"):
205
+ self.searcher: Searcher = self.text_mem.get_searcher(
206
+ manual_close_internet=os.getenv("ENABLE_INTERNET", "true").lower() == "false",
207
+ moscube=False,
208
+ process_llm=self.process_llm,
209
+ )
210
+ else:
211
+ self.searcher = None
212
+ else:
213
+ self.searcher = searcher
214
+ self.feedback_server = feedback_server
215
+
216
+ def initialize_modules(
217
+ self,
218
+ chat_llm: BaseLLM,
219
+ process_llm: BaseLLM | None = None,
220
+ db_engine: Engine | None = None,
221
+ mem_reader=None,
222
+ redis_client: Union["redis.Redis", None] = None,
223
+ ):
224
+ if process_llm is None:
225
+ process_llm = chat_llm
226
+
227
+ try:
228
+ if redis_client and self.use_redis_queue:
229
+ self.status_tracker = TaskStatusTracker(redis_client)
230
+ if self.dispatcher:
231
+ self.dispatcher.status_tracker = self.status_tracker
232
+ if self.memos_message_queue:
233
+ # Use the setter to propagate to the inner queue (e.g. SchedulerRedisQueue)
234
+ self.memos_message_queue.set_status_tracker(self.status_tracker)
235
+ # initialize submodules
236
+ self.chat_llm = chat_llm
237
+ self.process_llm = process_llm
238
+ self.db_engine = db_engine
239
+ self.monitor = SchedulerGeneralMonitor(
240
+ process_llm=self.process_llm, config=self.config, db_engine=self.db_engine
241
+ )
242
+ self.db_engine = self.monitor.db_engine
243
+ self.dispatcher_monitor = SchedulerDispatcherMonitor(config=self.config)
244
+ self.retriever = SchedulerRetriever(process_llm=self.process_llm, config=self.config)
245
+
246
+ if mem_reader:
247
+ self.mem_reader = mem_reader
248
+
249
+ if self.enable_parallel_dispatch:
250
+ self.dispatcher_monitor.initialize(dispatcher=self.dispatcher)
251
+ self.dispatcher_monitor.start()
252
+
253
+ # initialize with auth_config
254
+ try:
255
+ if self.auth_config_path is not None and Path(self.auth_config_path).exists():
256
+ self.auth_config = AuthConfig.from_local_config(
257
+ config_path=self.auth_config_path
258
+ )
259
+ elif AuthConfig.default_config_exists():
260
+ self.auth_config = AuthConfig.from_local_config()
261
+ else:
262
+ self.auth_config = AuthConfig.from_local_env()
263
+ except Exception:
264
+ pass
265
+
266
+ if self.auth_config is not None:
267
+ self.rabbitmq_config = self.auth_config.rabbitmq
268
+ if self.rabbitmq_config is not None:
269
+ self.initialize_rabbitmq(config=self.rabbitmq_config)
270
+
271
+ logger.debug("GeneralScheduler has been initialized")
272
+ except Exception as e:
273
+ logger.error(f"Failed to initialize scheduler modules: {e}", exc_info=True)
274
+ # Clean up any partially initialized resources
275
+ self._cleanup_on_init_failure()
276
+ raise
277
+
278
+ def _cleanup_on_init_failure(self):
279
+ """Clean up resources if initialization fails."""
280
+ try:
281
+ if hasattr(self, "dispatcher_monitor") and self.dispatcher_monitor is not None:
282
+ self.dispatcher_monitor.stop()
283
+ except Exception as e:
284
+ logger.warning(f"Error during cleanup: {e}")
285
+
286
+ @property
287
+ def mem_cube(self) -> BaseMemCube:
288
+ """The memory cube associated with this MemChat."""
289
+ if self.current_mem_cube is None:
290
+ logger.error("mem_cube is None when accessed", stack_info=True)
291
+ try:
292
+ self.components = init_components()
293
+ self.current_mem_cube: BaseMemCube = self.components["naive_mem_cube"]
294
+ except Exception:
295
+ logger.info(
296
+ "No environment available to initialize mem cube. Using fallback naive_mem_cube."
297
+ )
298
+ return self.current_mem_cube
299
+
300
+ @property
301
+ def status_tracker(self) -> TaskStatusTracker | None:
302
+ """Lazy-initialized TaskStatusTracker.
303
+
304
+ If the tracker is None, attempt to initialize from the Redis client
305
+ available via RedisSchedulerModule. This mirrors the lazy pattern used
306
+ by `mem_cube` so downstream modules can safely access the tracker.
307
+ """
308
+ if self._status_tracker is None and self.use_redis_queue:
309
+ try:
310
+ self._status_tracker = TaskStatusTracker(self.redis)
311
+ # Propagate to submodules when created lazily
312
+ if self.dispatcher:
313
+ self.dispatcher.status_tracker = self._status_tracker
314
+ if self.memos_message_queue:
315
+ self.memos_message_queue.set_status_tracker(self._status_tracker)
316
+ except Exception as e:
317
+ logger.warning(f"Failed to lazy-initialize status_tracker: {e}", exc_info=True)
318
+
319
+ return self._status_tracker
320
+
321
+ @status_tracker.setter
322
+ def status_tracker(self, value: TaskStatusTracker | None) -> None:
323
+ """Setter that also propagates tracker to dependent modules."""
324
+ self._status_tracker = value
325
+ try:
326
+ if self.dispatcher:
327
+ self.dispatcher.status_tracker = value
328
+ if self.memos_message_queue and value is not None:
329
+ self.memos_message_queue.set_status_tracker(value)
330
+ except Exception as e:
331
+ logger.warning(f"Failed to propagate status_tracker: {e}", exc_info=True)
332
+
333
+ @property
334
+ def feedback_server(self) -> SimpleMemFeedback:
335
+ """The memory cube associated with this MemChat."""
336
+ if self._feedback_server is None:
337
+ logger.error("feedback_server is None when accessed", stack_info=True)
338
+ try:
339
+ self.components = init_components()
340
+ self._feedback_server: SimpleMemFeedback = self.components["feedback_server"]
341
+ except Exception:
342
+ logger.info(
343
+ "No environment available to initialize feedback_server. Using fallback feedback_server."
344
+ )
345
+ return self._feedback_server
346
+
347
+ @feedback_server.setter
348
+ def feedback_server(self, value: SimpleMemFeedback) -> None:
349
+ self._feedback_server = value
350
+
351
+ @mem_cube.setter
352
+ def mem_cube(self, value: BaseMemCube) -> None:
353
+ """The memory cube associated with this MemChat."""
354
+ self.current_mem_cube = value
355
+ self.retriever.mem_cube = value
356
+
357
+ @property
358
+ def mem_cubes(self) -> dict[str, BaseMemCube]:
359
+ """All available memory cubes registered to the scheduler.
360
+
361
+ Setting this property will also initialize `current_mem_cube` if it is not
362
+ already set, following the initialization pattern used in component_init.py
363
+ (i.e., calling `init_mem_cube(...)`), without introducing circular imports.
364
+ """
365
+ return self._mem_cubes
366
+
367
+ @mem_cubes.setter
368
+ def mem_cubes(self, value: dict[str, BaseMemCube]) -> None:
369
+ self._mem_cubes = value or {}
370
+
371
+ # Initialize current_mem_cube if not set yet and mem_cubes are available
372
+ try:
373
+ if self.current_mem_cube is None and self._mem_cubes:
374
+ selected_cube: BaseMemCube | None = None
375
+
376
+ # Prefer the cube matching current_mem_cube_id if provided
377
+ if self.current_mem_cube_id and self.current_mem_cube_id in self._mem_cubes:
378
+ selected_cube = self._mem_cubes[self.current_mem_cube_id]
379
+ else:
380
+ # Fall back to the first available cube deterministically
381
+ first_id, first_cube = next(iter(self._mem_cubes.items()))
382
+ self.current_mem_cube_id = first_id
383
+ selected_cube = first_cube
384
+
385
+ if selected_cube is not None:
386
+ # Use init_mem_cube to mirror component_init.py behavior
387
+ # This sets self.mem_cube (and retriever.mem_cube), text_mem, and searcher.
388
+ self.init_mem_cube(mem_cube=selected_cube)
389
+ except Exception as e:
390
+ logger.warning(
391
+ f"Failed to initialize current_mem_cube from mem_cubes: {e}", exc_info=True
392
+ )
393
+
394
+ def transform_working_memories_to_monitors(
395
+ self, query_keywords, memories: list[TextualMemoryItem]
396
+ ) -> list[MemoryMonitorItem]:
397
+ """
398
+ Convert a list of TextualMemoryItem objects into MemoryMonitorItem objects
399
+ with importance scores based on keyword matching.
400
+
401
+ Args:
402
+ memories: List of TextualMemoryItem objects to be transformed.
403
+
404
+ Returns:
405
+ List of MemoryMonitorItem objects with computed importance scores.
406
+ """
407
+
408
+ result = []
409
+ mem_length = len(memories)
410
+ for idx, mem in enumerate(memories):
411
+ text_mem = mem.memory
412
+ mem_key = transform_name_to_key(name=text_mem)
413
+
414
+ # Calculate importance score based on keyword matches
415
+ keywords_score = 0
416
+ if query_keywords and text_mem:
417
+ for keyword, count in query_keywords.items():
418
+ keyword_count = text_mem.count(keyword)
419
+ if keyword_count > 0:
420
+ keywords_score += keyword_count * count
421
+ logger.debug(
422
+ f"Matched keyword '{keyword}' {keyword_count} times, added {keywords_score} to keywords_score"
423
+ )
424
+
425
+ # rank score
426
+ sorting_score = mem_length - idx
427
+
428
+ mem_monitor = MemoryMonitorItem(
429
+ memory_text=text_mem,
430
+ tree_memory_item=mem,
431
+ tree_memory_item_mapping_key=mem_key,
432
+ sorting_score=sorting_score,
433
+ keywords_score=keywords_score,
434
+ recording_count=1,
435
+ )
436
+ result.append(mem_monitor)
437
+
438
+ logger.info(f"Transformed {len(result)} memories to monitors")
439
+ return result
440
+
441
+ def replace_working_memory(
442
+ self,
443
+ user_id: UserID | str,
444
+ mem_cube_id: MemCubeID | str,
445
+ mem_cube: GeneralMemCube,
446
+ original_memory: list[TextualMemoryItem],
447
+ new_memory: list[TextualMemoryItem],
448
+ ) -> None | list[TextualMemoryItem]:
449
+ """Replace working memory with new memories after reranking."""
450
+ text_mem_base = mem_cube.text_mem
451
+ if isinstance(text_mem_base, TreeTextMemory):
452
+ text_mem_base: TreeTextMemory = text_mem_base
453
+
454
+ # process rerank memories with llm
455
+ query_db_manager = self.monitor.query_monitors[user_id][mem_cube_id]
456
+ # Sync with database to get latest query history
457
+ query_db_manager.sync_with_orm()
458
+
459
+ query_history = query_db_manager.obj.get_queries_with_timesort()
460
+
461
+ original_count = len(original_memory)
462
+ # Filter out memories tagged with "mode:fast"
463
+ filtered_original_memory = []
464
+ for origin_mem in original_memory:
465
+ if "mode:fast" not in origin_mem.metadata.tags:
466
+ filtered_original_memory.append(origin_mem)
467
+ else:
468
+ logger.debug(
469
+ f"Filtered out memory - ID: {getattr(origin_mem, 'id', 'unknown')}, Tags: {origin_mem.metadata.tags}"
470
+ )
471
+ # Calculate statistics
472
+ filtered_count = original_count - len(filtered_original_memory)
473
+ remaining_count = len(filtered_original_memory)
474
+
475
+ logger.info(
476
+ f"Filtering complete. Removed {filtered_count} memories with tag 'mode:fast'. Remaining memories: {remaining_count}"
477
+ )
478
+ original_memory = filtered_original_memory
479
+
480
+ memories_with_new_order, rerank_success_flag = (
481
+ self.retriever.process_and_rerank_memories(
482
+ queries=query_history,
483
+ original_memory=original_memory,
484
+ new_memory=new_memory,
485
+ top_k=self.top_k,
486
+ )
487
+ )
488
+
489
+ # Filter completely unrelated memories according to query_history
490
+ logger.info(f"Filtering memories based on query history: {len(query_history)} queries")
491
+ filtered_memories, filter_success_flag = self.retriever.filter_unrelated_memories(
492
+ query_history=query_history,
493
+ memories=memories_with_new_order,
494
+ )
495
+
496
+ if filter_success_flag:
497
+ logger.info(
498
+ f"Memory filtering completed successfully. "
499
+ f"Filtered from {len(memories_with_new_order)} to {len(filtered_memories)} memories"
500
+ )
501
+ memories_with_new_order = filtered_memories
502
+ else:
503
+ logger.warning(
504
+ "Memory filtering failed - keeping all memories as fallback. "
505
+ f"Original count: {len(memories_with_new_order)}"
506
+ )
507
+
508
+ # Update working memory monitors
509
+ query_keywords = query_db_manager.obj.get_keywords_collections()
510
+ logger.info(
511
+ f"Processing {len(memories_with_new_order)} memories with {len(query_keywords)} query keywords"
512
+ )
513
+ new_working_memory_monitors = self.transform_working_memories_to_monitors(
514
+ query_keywords=query_keywords,
515
+ memories=memories_with_new_order,
516
+ )
517
+
518
+ if not rerank_success_flag:
519
+ for one in new_working_memory_monitors:
520
+ one.sorting_score = 0
521
+
522
+ logger.info(f"update {len(new_working_memory_monitors)} working_memory_monitors")
523
+ self.monitor.update_working_memory_monitors(
524
+ new_working_memory_monitors=new_working_memory_monitors,
525
+ user_id=user_id,
526
+ mem_cube_id=mem_cube_id,
527
+ mem_cube=mem_cube,
528
+ )
529
+
530
+ mem_monitors: list[MemoryMonitorItem] = self.monitor.working_memory_monitors[user_id][
531
+ mem_cube_id
532
+ ].obj.get_sorted_mem_monitors(reverse=True)
533
+ new_working_memories = [mem_monitor.tree_memory_item for mem_monitor in mem_monitors]
534
+
535
+ text_mem_base.replace_working_memory(memories=new_working_memories)
536
+
537
+ logger.info(
538
+ f"The working memory has been replaced with {len(memories_with_new_order)} new memories."
539
+ )
540
+ self.log_working_memory_replacement(
541
+ original_memory=original_memory,
542
+ new_memory=new_working_memories,
543
+ user_id=user_id,
544
+ mem_cube_id=mem_cube_id,
545
+ mem_cube=mem_cube,
546
+ log_func_callback=self._submit_web_logs,
547
+ )
548
+ elif isinstance(text_mem_base, NaiveTextMemory):
549
+ # For NaiveTextMemory, we populate the monitors with the new candidates so activation memory can pick them up
550
+ logger.info(
551
+ f"NaiveTextMemory: Updating working memory monitors with {len(new_memory)} candidates."
552
+ )
553
+
554
+ # Use query keywords if available, otherwise just basic monitoring
555
+ query_db_manager = self.monitor.query_monitors[user_id][mem_cube_id]
556
+ query_db_manager.sync_with_orm()
557
+ query_keywords = query_db_manager.obj.get_keywords_collections()
558
+
559
+ new_working_memory_monitors = self.transform_working_memories_to_monitors(
560
+ query_keywords=query_keywords,
561
+ memories=new_memory,
562
+ )
563
+
564
+ self.monitor.update_working_memory_monitors(
565
+ new_working_memory_monitors=new_working_memory_monitors,
566
+ user_id=user_id,
567
+ mem_cube_id=mem_cube_id,
568
+ mem_cube=mem_cube,
569
+ )
570
+ memories_with_new_order = new_memory
571
+ else:
572
+ logger.error("memory_base is not supported")
573
+ memories_with_new_order = new_memory
574
+
575
+ return memories_with_new_order
576
+
577
+ def update_activation_memory(
578
+ self,
579
+ new_memories: list[str | TextualMemoryItem],
580
+ label: str,
581
+ user_id: UserID | str,
582
+ mem_cube_id: MemCubeID | str,
583
+ mem_cube: GeneralMemCube,
584
+ ) -> None:
585
+ """
586
+ Update activation memory by extracting KVCacheItems from new_memory (list of str),
587
+ add them to a KVCacheMemory instance, and dump to disk.
588
+ """
589
+ if len(new_memories) == 0:
590
+ logger.error("update_activation_memory: new_memory is empty.")
591
+ return
592
+ if isinstance(new_memories[0], TextualMemoryItem):
593
+ new_text_memories = [mem.memory for mem in new_memories]
594
+ elif isinstance(new_memories[0], str):
595
+ new_text_memories = new_memories
596
+ else:
597
+ logger.error("Not Implemented.")
598
+ return
599
+
600
+ try:
601
+ if isinstance(mem_cube.act_mem, VLLMKVCacheMemory):
602
+ act_mem: VLLMKVCacheMemory = mem_cube.act_mem
603
+ elif isinstance(mem_cube.act_mem, KVCacheMemory):
604
+ act_mem: KVCacheMemory = mem_cube.act_mem
605
+ else:
606
+ logger.error("Not Implemented.")
607
+ return
608
+
609
+ new_text_memory = MEMORY_ASSEMBLY_TEMPLATE.format(
610
+ memory_text="".join(
611
+ [
612
+ f"{i + 1}. {sentence.strip()}\n"
613
+ for i, sentence in enumerate(new_text_memories)
614
+ if sentence.strip() # Skip empty strings
615
+ ]
616
+ )
617
+ )
618
+
619
+ # huggingface or vllm kv cache
620
+ original_cache_items: list[VLLMKVCacheItem] = act_mem.get_all()
621
+ original_text_memories = []
622
+ if len(original_cache_items) > 0:
623
+ pre_cache_item: VLLMKVCacheItem = original_cache_items[-1]
624
+ original_text_memories = pre_cache_item.records.text_memories
625
+ original_composed_text_memory = pre_cache_item.records.composed_text_memory
626
+ if original_composed_text_memory == new_text_memory:
627
+ logger.warning(
628
+ "Skipping memory update - new composition matches existing cache: %s",
629
+ new_text_memory[:50] + "..."
630
+ if len(new_text_memory) > 50
631
+ else new_text_memory,
632
+ )
633
+ return
634
+ act_mem.delete_all()
635
+
636
+ cache_item = act_mem.extract(new_text_memory)
637
+ cache_item.records.text_memories = new_text_memories
638
+ cache_item.records.timestamp = get_utc_now()
639
+
640
+ act_mem.add([cache_item])
641
+ act_mem.dump(self.act_mem_dump_path)
642
+
643
+ self.log_activation_memory_update(
644
+ original_text_memories=original_text_memories,
645
+ new_text_memories=new_text_memories,
646
+ label=label,
647
+ user_id=user_id,
648
+ mem_cube_id=mem_cube_id,
649
+ mem_cube=mem_cube,
650
+ log_func_callback=self._submit_web_logs,
651
+ )
652
+
653
+ except Exception as e:
654
+ logger.error(f"MOS-based activation memory update failed: {e}", exc_info=True)
655
+ # Re-raise the exception if it's critical for the operation
656
+ # For now, we'll continue execution but this should be reviewed
657
+
658
+ def update_activation_memory_periodically(
659
+ self,
660
+ interval_seconds: int,
661
+ label: str,
662
+ user_id: UserID | str,
663
+ mem_cube_id: MemCubeID | str,
664
+ mem_cube: GeneralMemCube,
665
+ ):
666
+ try:
667
+ if (
668
+ self.monitor.last_activation_mem_update_time == datetime.min
669
+ or self.monitor.timed_trigger(
670
+ last_time=self.monitor.last_activation_mem_update_time,
671
+ interval_seconds=interval_seconds,
672
+ )
673
+ ):
674
+ logger.info(
675
+ f"Updating activation memory for user {user_id} and mem_cube {mem_cube_id}"
676
+ )
677
+
678
+ if (
679
+ user_id not in self.monitor.working_memory_monitors
680
+ or mem_cube_id not in self.monitor.working_memory_monitors[user_id]
681
+ or len(self.monitor.working_memory_monitors[user_id][mem_cube_id].obj.memories)
682
+ == 0
683
+ ):
684
+ logger.warning(
685
+ "No memories found in working_memory_monitors, activation memory update is skipped"
686
+ )
687
+ return
688
+
689
+ self.monitor.update_activation_memory_monitors(
690
+ user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
691
+ )
692
+
693
+ # Sync with database to get latest activation memories
694
+ activation_db_manager = self.monitor.activation_memory_monitors[user_id][
695
+ mem_cube_id
696
+ ]
697
+ activation_db_manager.sync_with_orm()
698
+ new_activation_memories = [
699
+ m.memory_text for m in activation_db_manager.obj.memories
700
+ ]
701
+
702
+ logger.info(
703
+ f"Collected {len(new_activation_memories)} new memory entries for processing"
704
+ )
705
+ # Print the content of each new activation memory
706
+ for i, memory in enumerate(new_activation_memories[:5], 1):
707
+ logger.info(
708
+ f"Part of New Activation Memorires | {i}/{len(new_activation_memories)}: {memory[:20]}"
709
+ )
710
+
711
+ self.update_activation_memory(
712
+ new_memories=new_activation_memories,
713
+ label=label,
714
+ user_id=user_id,
715
+ mem_cube_id=mem_cube_id,
716
+ mem_cube=mem_cube,
717
+ )
718
+
719
+ self.monitor.last_activation_mem_update_time = get_utc_now()
720
+
721
+ logger.debug(
722
+ f"Activation memory update completed at {self.monitor.last_activation_mem_update_time}"
723
+ )
724
+
725
+ else:
726
+ logger.info(
727
+ f"Skipping update - {interval_seconds} second interval not yet reached. "
728
+ f"Last update time is {self.monitor.last_activation_mem_update_time} and now is "
729
+ f"{get_utc_now()}"
730
+ )
731
+ except Exception as e:
732
+ logger.error(f"Error in update_activation_memory_periodically: {e}", exc_info=True)
733
+
734
+ def submit_messages(self, messages: ScheduleMessageItem | list[ScheduleMessageItem]):
735
+ """Submit messages for processing, with priority-aware dispatch.
736
+
737
+ - LEVEL_1 tasks dispatch immediately to the appropriate handler.
738
+ - Lower-priority tasks are enqueued via the configured message queue.
739
+ """
740
+ if isinstance(messages, ScheduleMessageItem):
741
+ messages = [messages]
742
+
743
+ if not messages:
744
+ return
745
+
746
+ current_trace_id = get_current_trace_id()
747
+
748
+ immediate_msgs: list[ScheduleMessageItem] = []
749
+ queued_msgs: list[ScheduleMessageItem] = []
750
+
751
+ for msg in messages:
752
+ # propagate request trace_id when available so monitor logs align with request logs
753
+ if current_trace_id:
754
+ msg.trace_id = current_trace_id
755
+
756
+ # basic metrics and status tracking
757
+ with suppress(Exception):
758
+ self.metrics.task_enqueued(user_id=msg.user_id, task_type=msg.label)
759
+
760
+ # ensure timestamp exists for monitoring
761
+ if getattr(msg, "timestamp", None) is None:
762
+ msg.timestamp = get_utc_now()
763
+
764
+ if self.status_tracker:
765
+ try:
766
+ self.status_tracker.task_submitted(
767
+ task_id=msg.item_id,
768
+ user_id=msg.user_id,
769
+ task_type=msg.label,
770
+ mem_cube_id=msg.mem_cube_id,
771
+ business_task_id=msg.task_id,
772
+ )
773
+ except Exception:
774
+ logger.warning("status_tracker.task_submitted failed", exc_info=True)
775
+
776
+ # honor disabled handlers
777
+ if self.disabled_handlers and msg.label in self.disabled_handlers:
778
+ logger.info(f"Skipping disabled handler: {msg.label} - {msg.content}")
779
+ continue
780
+
781
+ # decide priority path
782
+ task_priority = self.orchestrator.get_task_priority(task_label=msg.label)
783
+ if task_priority == TaskPriorityLevel.LEVEL_1:
784
+ immediate_msgs.append(msg)
785
+ else:
786
+ queued_msgs.append(msg)
787
+
788
+ # Dispatch high-priority tasks immediately
789
+ if immediate_msgs:
790
+ # emit enqueue events for consistency
791
+ for m in immediate_msgs:
792
+ emit_monitor_event(
793
+ "enqueue",
794
+ m,
795
+ {
796
+ "enqueue_ts": to_iso(getattr(m, "timestamp", None)),
797
+ "event_duration_ms": 0,
798
+ "total_duration_ms": 0,
799
+ },
800
+ )
801
+
802
+ # simulate dequeue for immediately dispatched messages so monitor logs stay complete
803
+ for m in immediate_msgs:
804
+ try:
805
+ now = time.time()
806
+ enqueue_ts_obj = getattr(m, "timestamp", None)
807
+ enqueue_epoch = None
808
+ if isinstance(enqueue_ts_obj, int | float):
809
+ enqueue_epoch = float(enqueue_ts_obj)
810
+ elif hasattr(enqueue_ts_obj, "timestamp"):
811
+ dt = enqueue_ts_obj
812
+ if dt.tzinfo is None:
813
+ dt = dt.replace(tzinfo=timezone.utc)
814
+ enqueue_epoch = dt.timestamp()
815
+
816
+ queue_wait_ms = None
817
+ if enqueue_epoch is not None:
818
+ queue_wait_ms = max(0.0, now - enqueue_epoch) * 1000
819
+
820
+ object.__setattr__(m, "_dequeue_ts", now)
821
+ emit_monitor_event(
822
+ "dequeue",
823
+ m,
824
+ {
825
+ "enqueue_ts": to_iso(enqueue_ts_obj),
826
+ "dequeue_ts": datetime.fromtimestamp(now, tz=timezone.utc).isoformat(),
827
+ "queue_wait_ms": queue_wait_ms,
828
+ "event_duration_ms": queue_wait_ms,
829
+ "total_duration_ms": queue_wait_ms,
830
+ },
831
+ )
832
+ self.metrics.task_dequeued(user_id=m.user_id, task_type=m.label)
833
+ except Exception:
834
+ logger.debug("Failed to emit dequeue for immediate task", exc_info=True)
835
+
836
+ user_cube_groups = group_messages_by_user_and_mem_cube(immediate_msgs)
837
+ for user_id, cube_groups in user_cube_groups.items():
838
+ for mem_cube_id, user_cube_msgs in cube_groups.items():
839
+ label_groups: dict[str, list[ScheduleMessageItem]] = {}
840
+ for m in user_cube_msgs:
841
+ label_groups.setdefault(m.label, []).append(m)
842
+
843
+ for label, msgs_by_label in label_groups.items():
844
+ handler = self.dispatcher.handlers.get(
845
+ label, self.dispatcher._default_message_handler
846
+ )
847
+ self.dispatcher.execute_task(
848
+ user_id=user_id,
849
+ mem_cube_id=mem_cube_id,
850
+ task_label=label,
851
+ msgs=msgs_by_label,
852
+ handler_call_back=handler,
853
+ )
854
+
855
+ # Enqueue lower-priority tasks
856
+ if queued_msgs:
857
+ self.memos_message_queue.submit_messages(messages=queued_msgs)
858
+
859
+ def _submit_web_logs(
860
+ self,
861
+ messages: ScheduleLogForWebItem | list[ScheduleLogForWebItem],
862
+ additional_log_info: str | None = None,
863
+ ) -> None:
864
+ """Submit log messages to the web log queue and optionally to RabbitMQ.
865
+
866
+ Args:
867
+ messages: Single log message or list of log messages
868
+ """
869
+ if isinstance(messages, ScheduleLogForWebItem):
870
+ messages = [messages] # transform single message to list
871
+
872
+ for message in messages:
873
+ if self.rabbitmq_config is None:
874
+ return
875
+ try:
876
+ # Always call publish; the publisher now caches when offline and flushes after reconnect
877
+ logger.info(
878
+ f"[DIAGNOSTIC] base_scheduler._submit_web_logs: enqueue publish {message.model_dump_json(indent=2)}"
879
+ )
880
+ self.rabbitmq_publish_message(message=message.to_dict())
881
+ logger.info(
882
+ "[DIAGNOSTIC] base_scheduler._submit_web_logs: publish dispatched "
883
+ "item_id=%s task_id=%s label=%s",
884
+ message.item_id,
885
+ message.task_id,
886
+ message.label,
887
+ )
888
+ except Exception as e:
889
+ logger.error(
890
+ f"[DIAGNOSTIC] base_scheduler._submit_web_logs failed: {e}", exc_info=True
891
+ )
892
+
893
+ logger.debug(
894
+ f"{len(messages)} submitted. {self._web_log_message_queue.qsize()} in queue. additional_log_info: {additional_log_info}"
895
+ )
896
+
897
+ def get_web_log_messages(self) -> list[dict]:
898
+ """
899
+ Retrieve structured log messages from the queue and return JSON-serializable dicts.
900
+ """
901
+ raw_items: list[ScheduleLogForWebItem] = []
902
+ while True:
903
+ try:
904
+ raw_items.append(self._web_log_message_queue.get_nowait())
905
+ except Exception:
906
+ break
907
+
908
+ def _map_label(label: str) -> str:
909
+ mapping = {
910
+ QUERY_TASK_LABEL: "addMessage",
911
+ ANSWER_TASK_LABEL: "addMessage",
912
+ ADD_TASK_LABEL: "addMemory",
913
+ MEM_UPDATE_TASK_LABEL: "updateMemory",
914
+ MEM_ORGANIZE_TASK_LABEL: "mergeMemory",
915
+ MEM_ARCHIVE_TASK_LABEL: "archiveMemory",
916
+ }
917
+ return mapping.get(label, label)
918
+
919
+ def _normalize_item(item: ScheduleLogForWebItem) -> dict:
920
+ data = item.to_dict()
921
+ data["label"] = _map_label(data.get("label"))
922
+ memcube_content = getattr(item, "memcube_log_content", None) or []
923
+ metadata = getattr(item, "metadata", None) or []
924
+
925
+ memcube_name = getattr(item, "memcube_name", None)
926
+ if not memcube_name and hasattr(self, "_map_memcube_name"):
927
+ memcube_name = self._map_memcube_name(item.mem_cube_id)
928
+ data["memcube_name"] = memcube_name
929
+
930
+ memory_len = getattr(item, "memory_len", None)
931
+ if memory_len is None:
932
+ if data["label"] == "mergeMemory":
933
+ memory_len = len([c for c in memcube_content if c.get("type") != "postMerge"])
934
+ elif memcube_content:
935
+ memory_len = len(memcube_content)
936
+ else:
937
+ memory_len = 1 if item.log_content else 0
938
+
939
+ data["memcube_log_content"] = memcube_content
940
+ data["memory_len"] = memory_len
941
+
942
+ def _with_memory_time(meta: dict) -> dict:
943
+ enriched = dict(meta)
944
+ if "memory_time" not in enriched:
945
+ enriched["memory_time"] = enriched.get("updated_at") or enriched.get(
946
+ "update_at"
947
+ )
948
+ return enriched
949
+
950
+ data["metadata"] = [_with_memory_time(m) for m in metadata]
951
+ data["log_title"] = ""
952
+ return data
953
+
954
+ return [_normalize_item(it) for it in raw_items]
955
+
956
+ def _message_consumer(self) -> None:
957
+ """
958
+ Continuously checks the queue for messages and dispatches them.
959
+
960
+ Runs in a dedicated thread to process messages at regular intervals.
961
+ For Redis queue, this method starts the Redis listener.
962
+ """
963
+
964
+ # Original local queue logic
965
+ while self._running: # Use a running flag for graceful shutdown
966
+ try:
967
+ # Check dispatcher thread pool status to avoid overloading
968
+ if self.enable_parallel_dispatch and self.dispatcher:
969
+ running_tasks = self.dispatcher.get_running_task_count()
970
+ if running_tasks >= self.dispatcher.max_workers:
971
+ # Thread pool is full, wait and retry
972
+ time.sleep(self._consume_interval)
973
+ continue
974
+
975
+ # Get messages in batches based on consume_batch setting
976
+
977
+ messages = self.memos_message_queue.get_messages(batch_size=self.consume_batch)
978
+
979
+ if messages:
980
+ now = time.time()
981
+ for msg in messages:
982
+ prev_context = get_current_context()
983
+ try:
984
+ # Set context for this message
985
+ msg_context = RequestContext(
986
+ trace_id=msg.trace_id,
987
+ user_name=msg.user_name,
988
+ )
989
+ set_request_context(msg_context)
990
+
991
+ enqueue_ts_obj = getattr(msg, "timestamp", None)
992
+ enqueue_epoch = None
993
+ if isinstance(enqueue_ts_obj, int | float):
994
+ enqueue_epoch = float(enqueue_ts_obj)
995
+ elif hasattr(enqueue_ts_obj, "timestamp"):
996
+ dt = enqueue_ts_obj
997
+ if dt.tzinfo is None:
998
+ dt = dt.replace(tzinfo=timezone.utc)
999
+ enqueue_epoch = dt.timestamp()
1000
+
1001
+ queue_wait_ms = None
1002
+ if enqueue_epoch is not None:
1003
+ queue_wait_ms = max(0.0, now - enqueue_epoch) * 1000
1004
+
1005
+ # Avoid pydantic field enforcement by using object.__setattr__
1006
+ object.__setattr__(msg, "_dequeue_ts", now)
1007
+ emit_monitor_event(
1008
+ "dequeue",
1009
+ msg,
1010
+ {
1011
+ "enqueue_ts": to_iso(enqueue_ts_obj),
1012
+ "dequeue_ts": datetime.fromtimestamp(
1013
+ now, tz=timezone.utc
1014
+ ).isoformat(),
1015
+ "queue_wait_ms": queue_wait_ms,
1016
+ "event_duration_ms": queue_wait_ms,
1017
+ "total_duration_ms": queue_wait_ms,
1018
+ },
1019
+ )
1020
+ self.metrics.task_dequeued(user_id=msg.user_id, task_type=msg.label)
1021
+ finally:
1022
+ # Restore the prior context of the consumer thread
1023
+ set_request_context(prev_context)
1024
+ try:
1025
+ import contextlib
1026
+
1027
+ with contextlib.suppress(Exception):
1028
+ if messages:
1029
+ self.dispatcher.on_messages_enqueued(messages)
1030
+
1031
+ self.dispatcher.dispatch(messages)
1032
+ except Exception as e:
1033
+ logger.error(f"Error dispatching messages: {e!s}")
1034
+
1035
+ # Sleep briefly to prevent busy waiting
1036
+ time.sleep(self._consume_interval) # Adjust interval as needed
1037
+
1038
+ except Exception as e:
1039
+ # Don't log error for "No messages available in Redis queue" as it's expected
1040
+ if "No messages available in Redis queue" not in str(e):
1041
+ logger.error(f"Unexpected error in message consumer: {e!s}", exc_info=True)
1042
+ time.sleep(self._consume_interval) # Prevent tight error loops
1043
+
1044
+ def _monitor_loop(self):
1045
+ while self._running:
1046
+ try:
1047
+ q_sizes = self.memos_message_queue.qsize()
1048
+
1049
+ if not isinstance(q_sizes, dict):
1050
+ continue
1051
+
1052
+ for stream_key, queue_length in q_sizes.items():
1053
+ # Skip aggregate keys like 'total_size'
1054
+ if stream_key == "total_size":
1055
+ continue
1056
+
1057
+ # Key format: ...:{user_id}:{mem_cube_id}:{task_label}
1058
+ # We want to extract user_id, which is the 3rd component from the end.
1059
+ parts = stream_key.split(":")
1060
+ if len(parts) >= 3:
1061
+ user_id = parts[-3]
1062
+ self.metrics.update_queue_length(queue_length, user_id)
1063
+ else:
1064
+ # Fallback for unexpected key formats (e.g. legacy or testing)
1065
+ # Try to use the key itself if it looks like a user_id (no colons)
1066
+ # or just log a warning?
1067
+ # For now, let's assume if it's not total_size and short, it might be a direct user_id key
1068
+ # (though that shouldn't happen with current queue implementations)
1069
+ if ":" not in stream_key:
1070
+ self.metrics.update_queue_length(queue_length, stream_key)
1071
+
1072
+ except Exception as e:
1073
+ logger.error(f"Error in metrics monitor loop: {e}", exc_info=True)
1074
+
1075
+ time.sleep(15) # 每 15 秒采样一次
1076
+
1077
+ def start(self) -> None:
1078
+ """
1079
+ Start the message consumer thread/process and initialize dispatcher resources.
1080
+
1081
+ Initializes and starts:
1082
+ 1. Message consumer thread or process (based on startup_mode)
1083
+ 2. Dispatcher thread pool (if parallel dispatch enabled)
1084
+ """
1085
+ # Initialize dispatcher resources
1086
+ if self.enable_parallel_dispatch:
1087
+ logger.info(
1088
+ f"Initializing dispatcher thread pool with {self.thread_pool_max_workers} workers"
1089
+ )
1090
+
1091
+ self.start_consumer()
1092
+ self.start_background_monitor()
1093
+
1094
+ def start_background_monitor(self):
1095
+ if self._monitor_thread and self._monitor_thread.is_alive():
1096
+ return
1097
+ self._monitor_thread = ContextThread(
1098
+ target=self._monitor_loop, daemon=True, name="SchedulerMetricsMonitor"
1099
+ )
1100
+ self._monitor_thread.start()
1101
+ logger.info("Scheduler metrics monitor thread started.")
1102
+
1103
+ def start_consumer(self) -> None:
1104
+ """
1105
+ Start only the message consumer thread/process.
1106
+
1107
+ This method can be used to restart the consumer after it has been stopped
1108
+ with stop_consumer(), without affecting other scheduler components.
1109
+ """
1110
+ if self._running:
1111
+ logger.warning("Memory Scheduler consumer is already running")
1112
+ return
1113
+
1114
+ # Start consumer based on startup mode
1115
+ self._running = True
1116
+
1117
+ if self.scheduler_startup_mode == STARTUP_BY_PROCESS:
1118
+ # Start consumer process
1119
+ self._consumer_process = multiprocessing.Process(
1120
+ target=self._message_consumer,
1121
+ daemon=True,
1122
+ name="MessageConsumerProcess",
1123
+ )
1124
+ self._consumer_process.start()
1125
+ logger.info("Message consumer process started")
1126
+ else:
1127
+ # Default to thread mode
1128
+ self._consumer_thread = ContextThread(
1129
+ target=self._message_consumer,
1130
+ daemon=True,
1131
+ name="MessageConsumerThread",
1132
+ )
1133
+ self._consumer_thread.start()
1134
+ logger.info("Message consumer thread started")
1135
+
1136
+ def stop_consumer(self) -> None:
1137
+ """Stop only the message consumer thread/process gracefully.
1138
+
1139
+ This method stops the consumer without affecting other components like
1140
+ dispatcher or monitors. Useful when you want to pause message processing
1141
+ while keeping other scheduler components running.
1142
+ """
1143
+ if not self._running:
1144
+ logger.warning("Memory Scheduler consumer is not running")
1145
+ return
1146
+
1147
+ # Signal consumer thread/process to stop
1148
+ self._running = False
1149
+
1150
+ # Wait for consumer thread or process
1151
+ if self.scheduler_startup_mode == STARTUP_BY_PROCESS and self._consumer_process:
1152
+ if self._consumer_process.is_alive():
1153
+ self._consumer_process.join(timeout=5.0)
1154
+ if self._consumer_process.is_alive():
1155
+ logger.warning("Consumer process did not stop gracefully, terminating...")
1156
+ self._consumer_process.terminate()
1157
+ self._consumer_process.join(timeout=2.0)
1158
+ if self._consumer_process.is_alive():
1159
+ logger.error("Consumer process could not be terminated")
1160
+ else:
1161
+ logger.info("Consumer process terminated")
1162
+ else:
1163
+ logger.info("Consumer process stopped")
1164
+ self._consumer_process = None
1165
+ elif self._consumer_thread and self._consumer_thread.is_alive():
1166
+ self._consumer_thread.join(timeout=5.0)
1167
+ if self._consumer_thread.is_alive():
1168
+ logger.warning("Consumer thread did not stop gracefully")
1169
+ else:
1170
+ logger.info("Consumer thread stopped")
1171
+ self._consumer_thread = None
1172
+
1173
+ logger.info("Memory Scheduler consumer stopped")
1174
+
1175
+ def stop(self) -> None:
1176
+ """Stop all scheduler components gracefully.
1177
+
1178
+ 1. Stops message consumer thread/process
1179
+ 2. Shuts down dispatcher thread pool
1180
+ 3. Cleans up resources
1181
+ """
1182
+ if not self._running:
1183
+ logger.warning("Memory Scheduler is not running")
1184
+ return
1185
+
1186
+ # Stop consumer first
1187
+ self.stop_consumer()
1188
+
1189
+ if self._monitor_thread:
1190
+ self._monitor_thread.join(timeout=2.0)
1191
+
1192
+ # Shutdown dispatcher
1193
+ if self.dispatcher:
1194
+ logger.info("Shutting down dispatcher...")
1195
+ self.dispatcher.shutdown()
1196
+
1197
+ # Shutdown dispatcher_monitor
1198
+ if self.dispatcher_monitor:
1199
+ logger.info("Shutting down monitor...")
1200
+ self.dispatcher_monitor.stop()
1201
+
1202
+ @property
1203
+ def handlers(self) -> dict[str, Callable]:
1204
+ """
1205
+ Access the dispatcher's handlers dictionary.
1206
+
1207
+ Returns:
1208
+ dict[str, Callable]: Dictionary mapping labels to handler functions
1209
+ """
1210
+ if not self.dispatcher:
1211
+ logger.warning("Dispatcher is not initialized, returning empty handlers dict")
1212
+ return {}
1213
+
1214
+ return self.dispatcher.handlers
1215
+
1216
+ def register_handlers(
1217
+ self, handlers: dict[str, Callable[[list[ScheduleMessageItem]], None]]
1218
+ ) -> None:
1219
+ """
1220
+ Bulk register multiple handlers from a dictionary.
1221
+
1222
+ Args:
1223
+ handlers: Dictionary mapping labels to handler functions
1224
+ Format: {label: handler_callable}
1225
+ """
1226
+ if not self.dispatcher:
1227
+ logger.warning("Dispatcher is not initialized, cannot register handlers")
1228
+ return
1229
+
1230
+ self.dispatcher.register_handlers(handlers)
1231
+
1232
+ def unregister_handlers(self, labels: list[str]) -> dict[str, bool]:
1233
+ """
1234
+ Unregister handlers from the dispatcher by their labels.
1235
+
1236
+ Args:
1237
+ labels: List of labels to unregister handlers for
1238
+
1239
+ Returns:
1240
+ dict[str, bool]: Dictionary mapping each label to whether it was successfully unregistered
1241
+ """
1242
+ if not self.dispatcher:
1243
+ logger.warning("Dispatcher is not initialized, cannot unregister handlers")
1244
+ return dict.fromkeys(labels, False)
1245
+
1246
+ return self.dispatcher.unregister_handlers(labels)
1247
+
1248
+ def get_running_tasks(self, filter_func: Callable | None = None) -> dict[str, dict]:
1249
+ if not self.dispatcher:
1250
+ logger.warning("Dispatcher is not initialized, returning empty tasks dict")
1251
+ return {}
1252
+
1253
+ running_tasks = self.dispatcher.get_running_tasks(filter_func=filter_func)
1254
+
1255
+ # Convert RunningTaskItem objects to dictionaries for easier consumption
1256
+ result = {}
1257
+ for task_id, task_item in running_tasks.items():
1258
+ result[task_id] = {
1259
+ "item_id": task_item.item_id,
1260
+ "user_id": task_item.user_id,
1261
+ "mem_cube_id": task_item.mem_cube_id,
1262
+ "task_info": task_item.task_info,
1263
+ "task_name": task_item.task_name,
1264
+ "start_time": task_item.start_time,
1265
+ "end_time": task_item.end_time,
1266
+ "status": task_item.status,
1267
+ "result": task_item.result,
1268
+ "error_message": task_item.error_message,
1269
+ "messages": task_item.messages,
1270
+ }
1271
+
1272
+ return result
1273
+
1274
+ def get_tasks_status(self):
1275
+ """Delegate status collection to TaskScheduleMonitor."""
1276
+ return self.task_schedule_monitor.get_tasks_status()
1277
+
1278
+ def print_tasks_status(self, tasks_status: dict | None = None) -> None:
1279
+ """Delegate pretty printing to TaskScheduleMonitor."""
1280
+ self.task_schedule_monitor.print_tasks_status(tasks_status=tasks_status)
1281
+
1282
+ def _gather_queue_stats(self) -> dict:
1283
+ """Collect queue/dispatcher stats for reporting."""
1284
+ memos_message_queue = self.memos_message_queue.memos_message_queue
1285
+ stats: dict[str, int | float | str] = {}
1286
+ stats["use_redis_queue"] = bool(self.use_redis_queue)
1287
+ # local queue metrics
1288
+ if not self.use_redis_queue:
1289
+ try:
1290
+ stats["qsize"] = int(memos_message_queue.qsize())
1291
+ except Exception:
1292
+ stats["qsize"] = -1
1293
+ # unfinished_tasks if available
1294
+ try:
1295
+ stats["unfinished_tasks"] = int(
1296
+ getattr(memos_message_queue, "unfinished_tasks", 0) or 0
1297
+ )
1298
+ except Exception:
1299
+ stats["unfinished_tasks"] = -1
1300
+ stats["maxsize"] = int(self.max_internal_message_queue_size)
1301
+ try:
1302
+ maxsize = int(self.max_internal_message_queue_size) or 1
1303
+ qsize = int(stats.get("qsize", 0))
1304
+ stats["utilization"] = min(1.0, max(0.0, qsize / maxsize))
1305
+ except Exception:
1306
+ stats["utilization"] = 0.0
1307
+ # dispatcher stats
1308
+ try:
1309
+ d_stats = self.dispatcher.stats()
1310
+ stats.update(
1311
+ {
1312
+ "running": int(d_stats.get("running", 0)),
1313
+ "inflight": int(d_stats.get("inflight", 0)),
1314
+ "handlers": int(d_stats.get("handlers", 0)),
1315
+ }
1316
+ )
1317
+ except Exception:
1318
+ stats.update({"running": 0, "inflight": 0, "handlers": 0})
1319
+ return stats