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,1495 @@
1
+ import concurrent.futures
2
+ import contextlib
3
+ import json
4
+ import traceback
5
+
6
+ from memos.configs.mem_scheduler import GeneralSchedulerConfig
7
+ from memos.context.context import ContextThreadPoolExecutor
8
+ from memos.log import get_logger
9
+ from memos.mem_cube.general import GeneralMemCube
10
+ from memos.mem_scheduler.base_scheduler import BaseScheduler
11
+ from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
12
+ from memos.mem_scheduler.schemas.monitor_schemas import QueryMonitorItem
13
+ from memos.mem_scheduler.schemas.task_schemas import (
14
+ ADD_TASK_LABEL,
15
+ ANSWER_TASK_LABEL,
16
+ DEFAULT_MAX_QUERY_KEY_WORDS,
17
+ LONG_TERM_MEMORY_TYPE,
18
+ MEM_FEEDBACK_TASK_LABEL,
19
+ MEM_ORGANIZE_TASK_LABEL,
20
+ MEM_READ_TASK_LABEL,
21
+ MEM_UPDATE_TASK_LABEL,
22
+ NOT_APPLICABLE_TYPE,
23
+ PREF_ADD_TASK_LABEL,
24
+ QUERY_TASK_LABEL,
25
+ USER_INPUT_TYPE,
26
+ )
27
+ from memos.mem_scheduler.utils.filter_utils import (
28
+ is_all_chinese,
29
+ is_all_english,
30
+ transform_name_to_key,
31
+ )
32
+ from memos.mem_scheduler.utils.misc_utils import (
33
+ group_messages_by_user_and_mem_cube,
34
+ is_cloud_env,
35
+ )
36
+ from memos.memories.textual.item import TextualMemoryItem
37
+ from memos.memories.textual.naive import NaiveTextMemory
38
+ from memos.memories.textual.preference import PreferenceTextMemory
39
+ from memos.memories.textual.tree import TreeTextMemory
40
+ from memos.types import (
41
+ MemCubeID,
42
+ UserID,
43
+ )
44
+
45
+
46
+ logger = get_logger(__name__)
47
+
48
+
49
+ class GeneralScheduler(BaseScheduler):
50
+ def __init__(self, config: GeneralSchedulerConfig):
51
+ """Initialize the scheduler with the given configuration."""
52
+ super().__init__(config)
53
+
54
+ self.query_key_words_limit = self.config.get("query_key_words_limit", 20)
55
+
56
+ # register handlers
57
+ handlers = {
58
+ QUERY_TASK_LABEL: self._query_message_consumer,
59
+ ANSWER_TASK_LABEL: self._answer_message_consumer,
60
+ MEM_UPDATE_TASK_LABEL: self._memory_update_consumer,
61
+ ADD_TASK_LABEL: self._add_message_consumer,
62
+ MEM_READ_TASK_LABEL: self._mem_read_message_consumer,
63
+ MEM_ORGANIZE_TASK_LABEL: self._mem_reorganize_message_consumer,
64
+ PREF_ADD_TASK_LABEL: self._pref_add_message_consumer,
65
+ MEM_FEEDBACK_TASK_LABEL: self._mem_feedback_message_consumer,
66
+ }
67
+ self.dispatcher.register_handlers(handlers)
68
+
69
+ def long_memory_update_process(
70
+ self, user_id: str, mem_cube_id: str, messages: list[ScheduleMessageItem]
71
+ ):
72
+ mem_cube = self.mem_cube
73
+
74
+ # update query monitors
75
+ for msg in messages:
76
+ self.monitor.register_query_monitor_if_not_exists(
77
+ user_id=user_id, mem_cube_id=mem_cube_id
78
+ )
79
+
80
+ query = msg.content
81
+ query_keywords = self.monitor.extract_query_keywords(query=query)
82
+ logger.info(
83
+ f'Extracted keywords "{query_keywords}" from query "{query}" for user_id={user_id}'
84
+ )
85
+
86
+ if len(query_keywords) == 0:
87
+ stripped_query = query.strip()
88
+ # Determine measurement method based on language
89
+ if is_all_english(stripped_query):
90
+ words = stripped_query.split() # Word count for English
91
+ elif is_all_chinese(stripped_query):
92
+ words = stripped_query # Character count for Chinese
93
+ else:
94
+ logger.debug(
95
+ f"Mixed-language memory, using character count: {stripped_query[:50]}..."
96
+ )
97
+ words = stripped_query # Default to character count
98
+
99
+ query_keywords = list(set(words[: self.query_key_words_limit]))
100
+ logger.error(
101
+ f"Keyword extraction failed for query '{query}' (user_id={user_id}). Using fallback keywords: {query_keywords[:10]}... (truncated)",
102
+ exc_info=True,
103
+ )
104
+
105
+ item = QueryMonitorItem(
106
+ user_id=user_id,
107
+ mem_cube_id=mem_cube_id,
108
+ query_text=query,
109
+ keywords=query_keywords,
110
+ max_keywords=DEFAULT_MAX_QUERY_KEY_WORDS,
111
+ )
112
+
113
+ query_db_manager = self.monitor.query_monitors[user_id][mem_cube_id]
114
+ query_db_manager.obj.put(item=item)
115
+ # Sync with database after adding new item
116
+ query_db_manager.sync_with_orm()
117
+ logger.debug(
118
+ f"Queries in monitor for user_id={user_id}, mem_cube_id={mem_cube_id}: {query_db_manager.obj.get_queries_with_timesort()}"
119
+ )
120
+
121
+ queries = [msg.content for msg in messages]
122
+
123
+ # recall
124
+ cur_working_memory, new_candidates = self.process_session_turn(
125
+ queries=queries,
126
+ user_id=user_id,
127
+ mem_cube_id=mem_cube_id,
128
+ mem_cube=mem_cube,
129
+ top_k=self.top_k,
130
+ )
131
+ logger.info(
132
+ # Build the candidate preview string outside the f-string to avoid backslashes in expression
133
+ f"[long_memory_update_process] Processed {len(queries)} queries {queries} and retrieved {len(new_candidates)} "
134
+ f"new candidate memories for user_id={user_id}: "
135
+ + ("\n- " + "\n- ".join([f"{one.id}: {one.memory}" for one in new_candidates]))
136
+ )
137
+
138
+ # rerank
139
+ new_order_working_memory = self.replace_working_memory(
140
+ user_id=user_id,
141
+ mem_cube_id=mem_cube_id,
142
+ mem_cube=mem_cube,
143
+ original_memory=cur_working_memory,
144
+ new_memory=new_candidates,
145
+ )
146
+ logger.debug(
147
+ f"[long_memory_update_process] Final working memory size: {len(new_order_working_memory)} memories for user_id={user_id}"
148
+ )
149
+
150
+ old_memory_texts = "\n- " + "\n- ".join(
151
+ [f"{one.id}: {one.memory}" for one in cur_working_memory]
152
+ )
153
+ new_memory_texts = "\n- " + "\n- ".join(
154
+ [f"{one.id}: {one.memory}" for one in new_order_working_memory]
155
+ )
156
+
157
+ logger.info(
158
+ f"[long_memory_update_process] For user_id='{user_id}', mem_cube_id='{mem_cube_id}': "
159
+ f"Scheduler replaced working memory based on query history {queries}. "
160
+ f"Old working memory ({len(cur_working_memory)} items): {old_memory_texts}. "
161
+ f"New working memory ({len(new_order_working_memory)} items): {new_memory_texts}."
162
+ )
163
+
164
+ # update activation memories
165
+ logger.debug(
166
+ f"Activation memory update {'enabled' if self.enable_activation_memory else 'disabled'} "
167
+ f"(interval: {self.monitor.act_mem_update_interval}s)"
168
+ )
169
+ if self.enable_activation_memory:
170
+ self.update_activation_memory_periodically(
171
+ interval_seconds=self.monitor.act_mem_update_interval,
172
+ label=QUERY_TASK_LABEL,
173
+ user_id=user_id,
174
+ mem_cube_id=mem_cube_id,
175
+ mem_cube=self.mem_cube,
176
+ )
177
+
178
+ def _add_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
179
+ logger.info(f"Messages {messages} assigned to {ADD_TASK_LABEL} handler.")
180
+ # Process the query in a session turn
181
+ grouped_messages = group_messages_by_user_and_mem_cube(messages=messages)
182
+
183
+ self.validate_schedule_messages(messages=messages, label=ADD_TASK_LABEL)
184
+ try:
185
+ for user_id in grouped_messages:
186
+ for mem_cube_id in grouped_messages[user_id]:
187
+ batch = grouped_messages[user_id][mem_cube_id]
188
+ if not batch:
189
+ continue
190
+
191
+ # Process each message in the batch
192
+ for msg in batch:
193
+ prepared_add_items, prepared_update_items_with_original = (
194
+ self.log_add_messages(msg=msg)
195
+ )
196
+ logger.info(
197
+ f"prepared_add_items: {prepared_add_items};\n prepared_update_items_with_original: {prepared_update_items_with_original}"
198
+ )
199
+ # Conditional Logging: Knowledge Base (Cloud Service) vs. Playground/Default
200
+ cloud_env = is_cloud_env()
201
+
202
+ if cloud_env:
203
+ self.send_add_log_messages_to_cloud_env(
204
+ msg, prepared_add_items, prepared_update_items_with_original
205
+ )
206
+ else:
207
+ self.send_add_log_messages_to_local_env(
208
+ msg, prepared_add_items, prepared_update_items_with_original
209
+ )
210
+
211
+ except Exception as e:
212
+ logger.error(f"Error: {e}", exc_info=True)
213
+
214
+ def _memory_update_consumer(self, messages: list[ScheduleMessageItem]) -> None:
215
+ logger.info(f"Messages {messages} assigned to {MEM_UPDATE_TASK_LABEL} handler.")
216
+
217
+ grouped_messages = group_messages_by_user_and_mem_cube(messages=messages)
218
+
219
+ self.validate_schedule_messages(messages=messages, label=MEM_UPDATE_TASK_LABEL)
220
+
221
+ for user_id in grouped_messages:
222
+ for mem_cube_id in grouped_messages[user_id]:
223
+ batch = grouped_messages[user_id][mem_cube_id]
224
+ if not batch:
225
+ continue
226
+ # Process the whole batch once; no need to iterate per message
227
+ self.long_memory_update_process(
228
+ user_id=user_id, mem_cube_id=mem_cube_id, messages=batch
229
+ )
230
+
231
+ def _query_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
232
+ """
233
+ Process and handle query trigger messages from the queue.
234
+
235
+ Args:
236
+ messages: List of query messages to process
237
+ """
238
+ logger.info(f"Messages {messages} assigned to {QUERY_TASK_LABEL} handler.")
239
+
240
+ grouped_messages = group_messages_by_user_and_mem_cube(messages=messages)
241
+
242
+ self.validate_schedule_messages(messages=messages, label=QUERY_TASK_LABEL)
243
+
244
+ mem_update_messages = []
245
+ for user_id in grouped_messages:
246
+ for mem_cube_id in grouped_messages[user_id]:
247
+ batch = grouped_messages[user_id][mem_cube_id]
248
+ if not batch:
249
+ continue
250
+
251
+ for msg in batch:
252
+ try:
253
+ event = self.create_event_log(
254
+ label="addMessage",
255
+ from_memory_type=USER_INPUT_TYPE,
256
+ to_memory_type=NOT_APPLICABLE_TYPE,
257
+ user_id=msg.user_id,
258
+ mem_cube_id=msg.mem_cube_id,
259
+ mem_cube=self.mem_cube,
260
+ memcube_log_content=[
261
+ {
262
+ "content": f"[User] {msg.content}",
263
+ "ref_id": msg.item_id,
264
+ "role": "user",
265
+ }
266
+ ],
267
+ metadata=[],
268
+ memory_len=1,
269
+ memcube_name=self._map_memcube_name(msg.mem_cube_id),
270
+ )
271
+ event.task_id = msg.task_id
272
+ self._submit_web_logs([event])
273
+ except Exception:
274
+ logger.exception("Failed to record addMessage log for query")
275
+ # Re-submit the message with label changed to mem_update
276
+ update_msg = ScheduleMessageItem(
277
+ user_id=msg.user_id,
278
+ mem_cube_id=msg.mem_cube_id,
279
+ label=MEM_UPDATE_TASK_LABEL,
280
+ content=msg.content,
281
+ session_id=msg.session_id,
282
+ user_name=msg.user_name,
283
+ info=msg.info,
284
+ task_id=msg.task_id,
285
+ )
286
+ mem_update_messages.append(update_msg)
287
+
288
+ self.submit_messages(messages=mem_update_messages)
289
+
290
+ def _answer_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
291
+ """
292
+ Process and handle answer trigger messages from the queue.
293
+
294
+ Args:
295
+ messages: List of answer messages to process
296
+ """
297
+ logger.info(f"Messages {messages} assigned to {ANSWER_TASK_LABEL} handler.")
298
+ grouped_messages = group_messages_by_user_and_mem_cube(messages=messages)
299
+
300
+ self.validate_schedule_messages(messages=messages, label=ANSWER_TASK_LABEL)
301
+
302
+ for user_id in grouped_messages:
303
+ for mem_cube_id in grouped_messages[user_id]:
304
+ batch = grouped_messages[user_id][mem_cube_id]
305
+ if not batch:
306
+ continue
307
+ try:
308
+ for msg in batch:
309
+ event = self.create_event_log(
310
+ label="addMessage",
311
+ from_memory_type=USER_INPUT_TYPE,
312
+ to_memory_type=NOT_APPLICABLE_TYPE,
313
+ user_id=msg.user_id,
314
+ mem_cube_id=msg.mem_cube_id,
315
+ mem_cube=self.mem_cube,
316
+ memcube_log_content=[
317
+ {
318
+ "content": f"[Assistant] {msg.content}",
319
+ "ref_id": msg.item_id,
320
+ "role": "assistant",
321
+ }
322
+ ],
323
+ metadata=[],
324
+ memory_len=1,
325
+ memcube_name=self._map_memcube_name(msg.mem_cube_id),
326
+ )
327
+ event.task_id = msg.task_id
328
+ self._submit_web_logs([event])
329
+ except Exception:
330
+ logger.exception("Failed to record addMessage log for answer")
331
+
332
+ def log_add_messages(self, msg: ScheduleMessageItem):
333
+ try:
334
+ userinput_memory_ids = json.loads(msg.content)
335
+ except Exception as e:
336
+ logger.error(f"Error: {e}. Content: {msg.content}", exc_info=True)
337
+ userinput_memory_ids = []
338
+
339
+ # Prepare data for both logging paths, fetching original content for updates
340
+ prepared_add_items = []
341
+ prepared_update_items_with_original = []
342
+ missing_ids: list[str] = []
343
+
344
+ for memory_id in userinput_memory_ids:
345
+ try:
346
+ # This mem_item represents the NEW content that was just added/processed
347
+ mem_item: TextualMemoryItem | None = None
348
+ mem_item = self.mem_cube.text_mem.get(
349
+ memory_id=memory_id, user_name=msg.mem_cube_id
350
+ )
351
+ if mem_item is None:
352
+ raise ValueError(f"Memory {memory_id} not found after retries")
353
+ # Check if a memory with the same key already exists (determining if it's an update)
354
+ key = getattr(mem_item.metadata, "key", None) or transform_name_to_key(
355
+ name=mem_item.memory
356
+ )
357
+ exists = False
358
+ original_content = None
359
+ original_item_id = None
360
+
361
+ # Only check graph_store if a key exists and the text_mem has a graph_store
362
+ if key and hasattr(self.mem_cube.text_mem, "graph_store"):
363
+ candidates = self.mem_cube.text_mem.graph_store.get_by_metadata(
364
+ [
365
+ {"field": "key", "op": "=", "value": key},
366
+ {
367
+ "field": "memory_type",
368
+ "op": "=",
369
+ "value": mem_item.metadata.memory_type,
370
+ },
371
+ ]
372
+ )
373
+ if candidates:
374
+ exists = True
375
+ original_item_id = candidates[0]
376
+ # Crucial step: Fetch the original content for updates
377
+ # This `get` is for the *existing* memory that will be updated
378
+ original_mem_item = self.mem_cube.text_mem.get(
379
+ memory_id=original_item_id, user_name=msg.mem_cube_id
380
+ )
381
+ original_content = original_mem_item.memory
382
+
383
+ if exists:
384
+ prepared_update_items_with_original.append(
385
+ {
386
+ "new_item": mem_item,
387
+ "original_content": original_content,
388
+ "original_item_id": original_item_id,
389
+ }
390
+ )
391
+ else:
392
+ prepared_add_items.append(mem_item)
393
+
394
+ except Exception:
395
+ missing_ids.append(memory_id)
396
+ logger.debug(
397
+ f"This MemoryItem {memory_id} has already been deleted or an error occurred during preparation."
398
+ )
399
+
400
+ if missing_ids:
401
+ content_preview = (
402
+ msg.content[:200] + "..."
403
+ if isinstance(msg.content, str) and len(msg.content) > 200
404
+ else msg.content
405
+ )
406
+ logger.warning(
407
+ "Missing TextualMemoryItem(s) during add log preparation. "
408
+ "memory_ids=%s user_id=%s mem_cube_id=%s task_id=%s item_id=%s redis_msg_id=%s label=%s stream_key=%s content_preview=%s",
409
+ missing_ids,
410
+ msg.user_id,
411
+ msg.mem_cube_id,
412
+ msg.task_id,
413
+ msg.item_id,
414
+ getattr(msg, "redis_message_id", ""),
415
+ msg.label,
416
+ getattr(msg, "stream_key", ""),
417
+ content_preview,
418
+ )
419
+
420
+ if not prepared_add_items and not prepared_update_items_with_original:
421
+ logger.warning(
422
+ "No add/update items prepared; skipping addMemory/knowledgeBaseUpdate logs. "
423
+ "user_id=%s mem_cube_id=%s task_id=%s item_id=%s redis_msg_id=%s label=%s stream_key=%s missing_ids=%s",
424
+ msg.user_id,
425
+ msg.mem_cube_id,
426
+ msg.task_id,
427
+ msg.item_id,
428
+ getattr(msg, "redis_message_id", ""),
429
+ msg.label,
430
+ getattr(msg, "stream_key", ""),
431
+ missing_ids,
432
+ )
433
+ return prepared_add_items, prepared_update_items_with_original
434
+
435
+ def send_add_log_messages_to_local_env(
436
+ self, msg: ScheduleMessageItem, prepared_add_items, prepared_update_items_with_original
437
+ ):
438
+ # Existing: Playground/Default Logging
439
+ # Reconstruct add_content/add_meta/update_content/update_meta from prepared_items
440
+ # This ensures existing logging path continues to work with pre-existing data structures
441
+ add_content_legacy: list[dict] = []
442
+ add_meta_legacy: list[dict] = []
443
+ update_content_legacy: list[dict] = []
444
+ update_meta_legacy: list[dict] = []
445
+
446
+ for item in prepared_add_items:
447
+ key = getattr(item.metadata, "key", None) or transform_name_to_key(name=item.memory)
448
+ add_content_legacy.append({"content": f"{key}: {item.memory}", "ref_id": item.id})
449
+ add_meta_legacy.append(
450
+ {
451
+ "ref_id": item.id,
452
+ "id": item.id,
453
+ "key": item.metadata.key,
454
+ "memory": item.memory,
455
+ "memory_type": item.metadata.memory_type,
456
+ "status": item.metadata.status,
457
+ "confidence": item.metadata.confidence,
458
+ "tags": item.metadata.tags,
459
+ "updated_at": getattr(item.metadata, "updated_at", None)
460
+ or getattr(item.metadata, "update_at", None),
461
+ }
462
+ )
463
+
464
+ for item_data in prepared_update_items_with_original:
465
+ item = item_data["new_item"]
466
+ key = getattr(item.metadata, "key", None) or transform_name_to_key(name=item.memory)
467
+ update_content_legacy.append({"content": f"{key}: {item.memory}", "ref_id": item.id})
468
+ update_meta_legacy.append(
469
+ {
470
+ "ref_id": item.id,
471
+ "id": item.id,
472
+ "key": item.metadata.key,
473
+ "memory": item.memory,
474
+ "memory_type": item.metadata.memory_type,
475
+ "status": item.metadata.status,
476
+ "confidence": item.metadata.confidence,
477
+ "tags": item.metadata.tags,
478
+ "updated_at": getattr(item.metadata, "updated_at", None)
479
+ or getattr(item.metadata, "update_at", None),
480
+ }
481
+ )
482
+
483
+ events = []
484
+ if add_content_legacy:
485
+ event = self.create_event_log(
486
+ label="addMemory",
487
+ from_memory_type=USER_INPUT_TYPE,
488
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
489
+ user_id=msg.user_id,
490
+ mem_cube_id=msg.mem_cube_id,
491
+ mem_cube=self.mem_cube,
492
+ memcube_log_content=add_content_legacy,
493
+ metadata=add_meta_legacy,
494
+ memory_len=len(add_content_legacy),
495
+ memcube_name=self._map_memcube_name(msg.mem_cube_id),
496
+ )
497
+ event.task_id = msg.task_id
498
+ events.append(event)
499
+ if update_content_legacy:
500
+ event = self.create_event_log(
501
+ label="updateMemory",
502
+ from_memory_type=LONG_TERM_MEMORY_TYPE,
503
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
504
+ user_id=msg.user_id,
505
+ mem_cube_id=msg.mem_cube_id,
506
+ mem_cube=self.mem_cube,
507
+ memcube_log_content=update_content_legacy,
508
+ metadata=update_meta_legacy,
509
+ memory_len=len(update_content_legacy),
510
+ memcube_name=self._map_memcube_name(msg.mem_cube_id),
511
+ )
512
+ event.task_id = msg.task_id
513
+ events.append(event)
514
+ logger.info(f"send_add_log_messages_to_local_env: {len(events)}")
515
+ if events:
516
+ self._submit_web_logs(events, additional_log_info="send_add_log_messages_to_cloud_env")
517
+
518
+ def send_add_log_messages_to_cloud_env(
519
+ self, msg: ScheduleMessageItem, prepared_add_items, prepared_update_items_with_original
520
+ ):
521
+ """
522
+ Cloud logging path for add/update events.
523
+ """
524
+ kb_log_content: list[dict] = []
525
+ info = msg.info or {}
526
+
527
+ # Process added items
528
+ for item in prepared_add_items:
529
+ metadata = getattr(item, "metadata", None)
530
+ file_ids = getattr(metadata, "file_ids", None) if metadata else None
531
+ source_doc_id = file_ids[0] if isinstance(file_ids, list) and file_ids else None
532
+ kb_log_content.append(
533
+ {
534
+ "log_source": "KNOWLEDGE_BASE_LOG",
535
+ "trigger_source": info.get("trigger_source", "Messages"),
536
+ "operation": "ADD",
537
+ "memory_id": item.id,
538
+ "content": item.memory,
539
+ "original_content": None,
540
+ "source_doc_id": source_doc_id,
541
+ }
542
+ )
543
+
544
+ # Process updated items
545
+ for item_data in prepared_update_items_with_original:
546
+ item = item_data["new_item"]
547
+ metadata = getattr(item, "metadata", None)
548
+ file_ids = getattr(metadata, "file_ids", None) if metadata else None
549
+ source_doc_id = file_ids[0] if isinstance(file_ids, list) and file_ids else None
550
+ kb_log_content.append(
551
+ {
552
+ "log_source": "KNOWLEDGE_BASE_LOG",
553
+ "trigger_source": info.get("trigger_source", "Messages"),
554
+ "operation": "UPDATE",
555
+ "memory_id": item.id,
556
+ "content": item.memory,
557
+ "original_content": item_data.get("original_content"),
558
+ "source_doc_id": source_doc_id,
559
+ }
560
+ )
561
+
562
+ if kb_log_content:
563
+ logger.info(
564
+ f"[DIAGNOSTIC] general_scheduler.send_add_log_messages_to_cloud_env: Creating event log for KB update. Label: knowledgeBaseUpdate, user_id: {msg.user_id}, mem_cube_id: {msg.mem_cube_id}, task_id: {msg.task_id}. KB content: {json.dumps(kb_log_content, indent=2)}"
565
+ )
566
+ event = self.create_event_log(
567
+ label="knowledgeBaseUpdate",
568
+ from_memory_type=USER_INPUT_TYPE,
569
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
570
+ user_id=msg.user_id,
571
+ mem_cube_id=msg.mem_cube_id,
572
+ mem_cube=self.mem_cube,
573
+ memcube_log_content=kb_log_content,
574
+ metadata=None,
575
+ memory_len=len(kb_log_content),
576
+ memcube_name=self._map_memcube_name(msg.mem_cube_id),
577
+ )
578
+ event.log_content = f"Knowledge Base Memory Update: {len(kb_log_content)} changes."
579
+ event.task_id = msg.task_id
580
+ self._submit_web_logs([event])
581
+
582
+ def _mem_feedback_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
583
+ try:
584
+ if not messages:
585
+ return
586
+ message = messages[0]
587
+ mem_cube = self.mem_cube
588
+
589
+ user_id = message.user_id
590
+ mem_cube_id = message.mem_cube_id
591
+ content = message.content
592
+
593
+ try:
594
+ feedback_data = json.loads(content) if isinstance(content, str) else content
595
+ if not isinstance(feedback_data, dict):
596
+ logger.error(
597
+ f"Failed to decode feedback_data or it is not a dict: {feedback_data}"
598
+ )
599
+ return
600
+ except json.JSONDecodeError:
601
+ logger.error(f"Invalid JSON content for feedback message: {content}", exc_info=True)
602
+ return
603
+
604
+ task_id = feedback_data.get("task_id") or message.task_id
605
+ feedback_result = self.feedback_server.process_feedback(
606
+ user_id=user_id,
607
+ user_name=mem_cube_id,
608
+ session_id=feedback_data.get("session_id"),
609
+ chat_history=feedback_data.get("history", []),
610
+ retrieved_memory_ids=feedback_data.get("retrieved_memory_ids", []),
611
+ feedback_content=feedback_data.get("feedback_content"),
612
+ feedback_time=feedback_data.get("feedback_time"),
613
+ task_id=task_id,
614
+ info=feedback_data.get("info", None),
615
+ )
616
+
617
+ logger.info(
618
+ f"Successfully processed feedback for user_id={user_id}, mem_cube_id={mem_cube_id}"
619
+ )
620
+
621
+ cloud_env = is_cloud_env()
622
+ if cloud_env:
623
+ record = feedback_result.get("record") if isinstance(feedback_result, dict) else {}
624
+ add_records = record.get("add") if isinstance(record, dict) else []
625
+ update_records = record.get("update") if isinstance(record, dict) else []
626
+
627
+ def _extract_fields(mem_item):
628
+ mem_id = (
629
+ getattr(mem_item, "id", None)
630
+ if not isinstance(mem_item, dict)
631
+ else mem_item.get("id")
632
+ )
633
+ mem_memory = (
634
+ getattr(mem_item, "memory", None)
635
+ if not isinstance(mem_item, dict)
636
+ else mem_item.get("memory") or mem_item.get("text")
637
+ )
638
+ if mem_memory is None and isinstance(mem_item, dict):
639
+ mem_memory = mem_item.get("text")
640
+ original_content = (
641
+ getattr(mem_item, "origin_memory", None)
642
+ if not isinstance(mem_item, dict)
643
+ else mem_item.get("origin_memory")
644
+ or mem_item.get("old_memory")
645
+ or mem_item.get("original_content")
646
+ )
647
+ source_doc_id = None
648
+ if isinstance(mem_item, dict):
649
+ source_doc_id = mem_item.get("source_doc_id", None)
650
+
651
+ return mem_id, mem_memory, original_content, source_doc_id
652
+
653
+ kb_log_content: list[dict] = []
654
+
655
+ for mem_item in add_records or []:
656
+ mem_id, mem_memory, _, source_doc_id = _extract_fields(mem_item)
657
+ if mem_id and mem_memory:
658
+ kb_log_content.append(
659
+ {
660
+ "log_source": "KNOWLEDGE_BASE_LOG",
661
+ "trigger_source": "Feedback",
662
+ "operation": "ADD",
663
+ "memory_id": mem_id,
664
+ "content": mem_memory,
665
+ "original_content": None,
666
+ "source_doc_id": source_doc_id,
667
+ }
668
+ )
669
+ else:
670
+ logger.warning(
671
+ "Skipping malformed feedback add item. user_id=%s mem_cube_id=%s task_id=%s item=%s",
672
+ user_id,
673
+ mem_cube_id,
674
+ task_id,
675
+ mem_item,
676
+ stack_info=True,
677
+ )
678
+
679
+ for mem_item in update_records or []:
680
+ mem_id, mem_memory, original_content, source_doc_id = _extract_fields(mem_item)
681
+ if mem_id and mem_memory:
682
+ kb_log_content.append(
683
+ {
684
+ "log_source": "KNOWLEDGE_BASE_LOG",
685
+ "trigger_source": "Feedback",
686
+ "operation": "UPDATE",
687
+ "memory_id": mem_id,
688
+ "content": mem_memory,
689
+ "original_content": original_content,
690
+ "source_doc_id": source_doc_id,
691
+ }
692
+ )
693
+ else:
694
+ logger.warning(
695
+ "Skipping malformed feedback update item. user_id=%s mem_cube_id=%s task_id=%s item=%s",
696
+ user_id,
697
+ mem_cube_id,
698
+ task_id,
699
+ mem_item,
700
+ stack_info=True,
701
+ )
702
+
703
+ logger.info(f"[Feedback Scheduler] kb_log_content: {kb_log_content!s}")
704
+ if kb_log_content:
705
+ logger.info(
706
+ "[DIAGNOSTIC] general_scheduler._mem_feedback_message_consumer: Creating knowledgeBaseUpdate event for feedback. user_id=%s mem_cube_id=%s task_id=%s items=%s",
707
+ user_id,
708
+ mem_cube_id,
709
+ task_id,
710
+ len(kb_log_content),
711
+ )
712
+ event = self.create_event_log(
713
+ label="knowledgeBaseUpdate",
714
+ from_memory_type=USER_INPUT_TYPE,
715
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
716
+ user_id=user_id,
717
+ mem_cube_id=mem_cube_id,
718
+ mem_cube=mem_cube,
719
+ memcube_log_content=kb_log_content,
720
+ metadata=None,
721
+ memory_len=len(kb_log_content),
722
+ memcube_name=self._map_memcube_name(mem_cube_id),
723
+ )
724
+ event.log_content = (
725
+ f"Knowledge Base Memory Update: {len(kb_log_content)} changes."
726
+ )
727
+ event.task_id = task_id
728
+ self._submit_web_logs([event])
729
+ else:
730
+ logger.warning(
731
+ "No valid feedback content generated for web log. user_id=%s mem_cube_id=%s task_id=%s",
732
+ user_id,
733
+ mem_cube_id,
734
+ task_id,
735
+ stack_info=True,
736
+ )
737
+ else:
738
+ logger.info(
739
+ "Skipping web log for feedback. Not in a cloud environment (is_cloud_env=%s)",
740
+ cloud_env,
741
+ )
742
+
743
+ except Exception as e:
744
+ logger.error(f"Error processing feedbackMemory message: {e}", exc_info=True)
745
+
746
+ def _mem_read_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
747
+ logger.info(
748
+ f"[DIAGNOSTIC] general_scheduler._mem_read_message_consumer called. Received messages: {[msg.model_dump_json(indent=2) for msg in messages]}"
749
+ )
750
+ logger.info(f"Messages {messages} assigned to {MEM_READ_TASK_LABEL} handler.")
751
+
752
+ def process_message(message: ScheduleMessageItem):
753
+ try:
754
+ user_id = message.user_id
755
+ mem_cube_id = message.mem_cube_id
756
+ mem_cube = self.mem_cube
757
+ if mem_cube is None:
758
+ logger.error(
759
+ f"mem_cube is None for user_id={user_id}, mem_cube_id={mem_cube_id}, skipping processing",
760
+ stack_info=True,
761
+ )
762
+ return
763
+
764
+ content = message.content
765
+ user_name = message.user_name
766
+ info = message.info or {}
767
+
768
+ # Parse the memory IDs from content
769
+ mem_ids = json.loads(content) if isinstance(content, str) else content
770
+ if not mem_ids:
771
+ return
772
+
773
+ logger.info(
774
+ f"Processing mem_read for user_id={user_id}, mem_cube_id={mem_cube_id}, mem_ids={mem_ids}"
775
+ )
776
+
777
+ # Get the text memory from the mem_cube
778
+ text_mem = mem_cube.text_mem
779
+ if not isinstance(text_mem, TreeTextMemory):
780
+ logger.error(f"Expected TreeTextMemory but got {type(text_mem).__name__}")
781
+ return
782
+
783
+ # Use mem_reader to process the memories
784
+ self._process_memories_with_reader(
785
+ mem_ids=mem_ids,
786
+ user_id=user_id,
787
+ mem_cube_id=mem_cube_id,
788
+ text_mem=text_mem,
789
+ user_name=user_name,
790
+ custom_tags=info.get("custom_tags", None),
791
+ task_id=message.task_id,
792
+ info=info,
793
+ )
794
+
795
+ logger.info(
796
+ f"Successfully processed mem_read for user_id={user_id}, mem_cube_id={mem_cube_id}"
797
+ )
798
+
799
+ except Exception as e:
800
+ logger.error(f"Error processing mem_read message: {e}", stack_info=True)
801
+
802
+ with ContextThreadPoolExecutor(max_workers=min(8, len(messages))) as executor:
803
+ futures = [executor.submit(process_message, msg) for msg in messages]
804
+ for future in concurrent.futures.as_completed(futures):
805
+ try:
806
+ future.result()
807
+ except Exception as e:
808
+ logger.error(f"Thread task failed: {e}", stack_info=True)
809
+
810
+ def _process_memories_with_reader(
811
+ self,
812
+ mem_ids: list[str],
813
+ user_id: str,
814
+ mem_cube_id: str,
815
+ text_mem: TreeTextMemory,
816
+ user_name: str,
817
+ custom_tags: list[str] | None = None,
818
+ task_id: str | None = None,
819
+ info: dict | None = None,
820
+ ) -> None:
821
+ logger.info(
822
+ f"[DIAGNOSTIC] general_scheduler._process_memories_with_reader called. mem_ids: {mem_ids}, user_id: {user_id}, mem_cube_id: {mem_cube_id}, task_id: {task_id}"
823
+ )
824
+ """
825
+ Process memories using mem_reader for enhanced memory processing.
826
+
827
+ Args:
828
+ mem_ids: List of memory IDs to process
829
+ user_id: User ID
830
+ mem_cube_id: Memory cube ID
831
+ text_mem: Text memory instance
832
+ custom_tags: Optional list of custom tags for memory processing
833
+ """
834
+ kb_log_content: list[dict] = []
835
+ try:
836
+ # Get the mem_reader from the parent MOSCore
837
+ if not hasattr(self, "mem_reader") or self.mem_reader is None:
838
+ logger.warning(
839
+ "mem_reader not available in scheduler, skipping enhanced processing"
840
+ )
841
+ return
842
+
843
+ # Get the original memory items
844
+ memory_items = []
845
+ for mem_id in mem_ids:
846
+ try:
847
+ memory_item = text_mem.get(mem_id, user_name=user_name)
848
+ memory_items.append(memory_item)
849
+ except Exception as e:
850
+ logger.warning(
851
+ f"[_process_memories_with_reader] Failed to get memory {mem_id}: {e}"
852
+ )
853
+ continue
854
+
855
+ if not memory_items:
856
+ logger.warning("No valid memory items found for processing")
857
+ return
858
+
859
+ # parse working_binding ids from the *original* memory_items (the raw items created in /add)
860
+ # these still carry metadata.background with "[working_binding:...]" so we can know
861
+ # which WorkingMemory clones should be cleaned up later.
862
+ from memos.memories.textual.tree_text_memory.organize.manager import (
863
+ extract_working_binding_ids,
864
+ )
865
+
866
+ bindings_to_delete = extract_working_binding_ids(memory_items)
867
+ logger.info(
868
+ f"Extracted {len(bindings_to_delete)} working_binding ids to cleanup: {list(bindings_to_delete)}"
869
+ )
870
+
871
+ # Use mem_reader to process the memories
872
+ logger.info(f"Processing {len(memory_items)} memories with mem_reader")
873
+
874
+ # Extract memories using mem_reader
875
+ try:
876
+ processed_memories = self.mem_reader.fine_transfer_simple_mem(
877
+ memory_items,
878
+ type="chat",
879
+ custom_tags=custom_tags,
880
+ user_name=user_name,
881
+ )
882
+ except Exception as e:
883
+ logger.warning(f"{e}: Fail to transfer mem: {memory_items}")
884
+ processed_memories = []
885
+
886
+ if processed_memories and len(processed_memories) > 0:
887
+ # Flatten the results (mem_reader returns list of lists)
888
+ flattened_memories = []
889
+ for memory_list in processed_memories:
890
+ flattened_memories.extend(memory_list)
891
+
892
+ logger.info(f"mem_reader processed {len(flattened_memories)} enhanced memories")
893
+
894
+ # Add the enhanced memories back to the memory system
895
+ if flattened_memories:
896
+ enhanced_mem_ids = text_mem.add(flattened_memories, user_name=user_name)
897
+ logger.info(
898
+ f"Added {len(enhanced_mem_ids)} enhanced memories: {enhanced_mem_ids}"
899
+ )
900
+
901
+ # Mark merged_from memories as archived when provided in memory metadata
902
+ if self.mem_reader.graph_db:
903
+ for memory in flattened_memories:
904
+ merged_from = (memory.metadata.info or {}).get("merged_from")
905
+ if merged_from:
906
+ old_ids = (
907
+ merged_from
908
+ if isinstance(merged_from, (list | tuple | set))
909
+ else [merged_from]
910
+ )
911
+ for old_id in old_ids:
912
+ try:
913
+ self.mem_reader.graph_db.update_node(
914
+ str(old_id), {"status": "archived"}, user_name=user_name
915
+ )
916
+ logger.info(
917
+ f"[Scheduler] Archived merged_from memory: {old_id}"
918
+ )
919
+ except Exception as e:
920
+ logger.warning(
921
+ f"[Scheduler] Failed to archive merged_from memory {old_id}: {e}"
922
+ )
923
+ else:
924
+ # Check if any memory has merged_from but graph_db is unavailable
925
+ has_merged_from = any(
926
+ (m.metadata.info or {}).get("merged_from") for m in flattened_memories
927
+ )
928
+ if has_merged_from:
929
+ logger.warning(
930
+ "[Scheduler] merged_from provided but graph_db is unavailable; skip archiving."
931
+ )
932
+
933
+ # LOGGING BLOCK START
934
+ # This block is replicated from _add_message_consumer to ensure consistent logging
935
+ cloud_env = is_cloud_env()
936
+ if cloud_env:
937
+ # New: Knowledge Base Logging (Cloud Service)
938
+ kb_log_content = []
939
+ for item in flattened_memories:
940
+ metadata = getattr(item, "metadata", None)
941
+ file_ids = getattr(metadata, "file_ids", None) if metadata else None
942
+ source_doc_id = (
943
+ file_ids[0] if isinstance(file_ids, list) and file_ids else None
944
+ )
945
+ kb_log_content.append(
946
+ {
947
+ "log_source": "KNOWLEDGE_BASE_LOG",
948
+ "trigger_source": info.get("trigger_source", "Messages")
949
+ if info
950
+ else "Messages",
951
+ "operation": "ADD",
952
+ "memory_id": item.id,
953
+ "content": item.memory,
954
+ "original_content": None,
955
+ "source_doc_id": source_doc_id,
956
+ }
957
+ )
958
+ if kb_log_content:
959
+ logger.info(
960
+ f"[DIAGNOSTIC] general_scheduler._process_memories_with_reader: Creating event log for KB update. Label: knowledgeBaseUpdate, user_id: {user_id}, mem_cube_id: {mem_cube_id}, task_id: {task_id}. KB content: {json.dumps(kb_log_content, indent=2)}"
961
+ )
962
+ event = self.create_event_log(
963
+ label="knowledgeBaseUpdate",
964
+ from_memory_type=USER_INPUT_TYPE,
965
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
966
+ user_id=user_id,
967
+ mem_cube_id=mem_cube_id,
968
+ mem_cube=self.mem_cube,
969
+ memcube_log_content=kb_log_content,
970
+ metadata=None,
971
+ memory_len=len(kb_log_content),
972
+ memcube_name=self._map_memcube_name(mem_cube_id),
973
+ )
974
+ event.log_content = (
975
+ f"Knowledge Base Memory Update: {len(kb_log_content)} changes."
976
+ )
977
+ event.task_id = task_id
978
+ self._submit_web_logs([event])
979
+ else:
980
+ # Existing: Playground/Default Logging
981
+ add_content_legacy: list[dict] = []
982
+ add_meta_legacy: list[dict] = []
983
+ for item_id, item in zip(
984
+ enhanced_mem_ids, flattened_memories, strict=False
985
+ ):
986
+ key = getattr(item.metadata, "key", None) or transform_name_to_key(
987
+ name=item.memory
988
+ )
989
+ add_content_legacy.append(
990
+ {"content": f"{key}: {item.memory}", "ref_id": item_id}
991
+ )
992
+ add_meta_legacy.append(
993
+ {
994
+ "ref_id": item_id,
995
+ "id": item_id,
996
+ "key": item.metadata.key,
997
+ "memory": item.memory,
998
+ "memory_type": item.metadata.memory_type,
999
+ "status": item.metadata.status,
1000
+ "confidence": item.metadata.confidence,
1001
+ "tags": item.metadata.tags,
1002
+ "updated_at": getattr(item.metadata, "updated_at", None)
1003
+ or getattr(item.metadata, "update_at", None),
1004
+ }
1005
+ )
1006
+ if add_content_legacy:
1007
+ event = self.create_event_log(
1008
+ label="addMemory",
1009
+ from_memory_type=USER_INPUT_TYPE,
1010
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
1011
+ user_id=user_id,
1012
+ mem_cube_id=mem_cube_id,
1013
+ mem_cube=self.mem_cube,
1014
+ memcube_log_content=add_content_legacy,
1015
+ metadata=add_meta_legacy,
1016
+ memory_len=len(add_content_legacy),
1017
+ memcube_name=self._map_memcube_name(mem_cube_id),
1018
+ )
1019
+ event.task_id = task_id
1020
+ self._submit_web_logs([event])
1021
+ # LOGGING BLOCK END
1022
+ else:
1023
+ logger.info("No enhanced memories generated by mem_reader")
1024
+ else:
1025
+ logger.info("mem_reader returned no processed memories")
1026
+
1027
+ # build full delete list:
1028
+ # - original raw mem_ids (temporary fast memories)
1029
+ # - any bound working memories referenced by the enhanced memories
1030
+ delete_ids = list(mem_ids)
1031
+ if bindings_to_delete:
1032
+ delete_ids.extend(list(bindings_to_delete))
1033
+ # deduplicate
1034
+ delete_ids = list(dict.fromkeys(delete_ids))
1035
+ if delete_ids:
1036
+ try:
1037
+ text_mem.delete(delete_ids, user_name=user_name)
1038
+ logger.info(
1039
+ f"Delete raw/working mem_ids: {delete_ids} for user_name: {user_name}"
1040
+ )
1041
+ except Exception as e:
1042
+ logger.warning(f"Failed to delete some mem_ids {delete_ids}: {e}")
1043
+ else:
1044
+ logger.info("No mem_ids to delete (nothing to cleanup)")
1045
+
1046
+ text_mem.memory_manager.remove_and_refresh_memory(user_name=user_name)
1047
+ logger.info("Remove and Refresh Memories")
1048
+ logger.debug(f"Finished add {user_id} memory: {mem_ids}")
1049
+
1050
+ except Exception as exc:
1051
+ logger.error(
1052
+ f"Error in _process_memories_with_reader: {traceback.format_exc()}", exc_info=True
1053
+ )
1054
+ with contextlib.suppress(Exception):
1055
+ cloud_env = is_cloud_env()
1056
+ if cloud_env:
1057
+ if not kb_log_content:
1058
+ trigger_source = (
1059
+ info.get("trigger_source", "Messages") if info else "Messages"
1060
+ )
1061
+ kb_log_content = [
1062
+ {
1063
+ "log_source": "KNOWLEDGE_BASE_LOG",
1064
+ "trigger_source": trigger_source,
1065
+ "operation": "ADD",
1066
+ "memory_id": mem_id,
1067
+ "content": None,
1068
+ "original_content": None,
1069
+ "source_doc_id": None,
1070
+ }
1071
+ for mem_id in mem_ids
1072
+ ]
1073
+ event = self.create_event_log(
1074
+ label="knowledgeBaseUpdate",
1075
+ from_memory_type=USER_INPUT_TYPE,
1076
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
1077
+ user_id=user_id,
1078
+ mem_cube_id=mem_cube_id,
1079
+ mem_cube=self.mem_cube,
1080
+ memcube_log_content=kb_log_content,
1081
+ metadata=None,
1082
+ memory_len=len(kb_log_content),
1083
+ memcube_name=self._map_memcube_name(mem_cube_id),
1084
+ )
1085
+ event.log_content = f"Knowledge Base Memory Update failed: {exc!s}"
1086
+ event.task_id = task_id
1087
+ event.status = "failed"
1088
+ self._submit_web_logs([event])
1089
+
1090
+ def _mem_reorganize_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
1091
+ logger.info(f"Messages {messages} assigned to {MEM_ORGANIZE_TASK_LABEL} handler.")
1092
+
1093
+ def process_message(message: ScheduleMessageItem):
1094
+ try:
1095
+ user_id = message.user_id
1096
+ mem_cube_id = message.mem_cube_id
1097
+ mem_cube = self.mem_cube
1098
+ if mem_cube is None:
1099
+ logger.warning(
1100
+ f"mem_cube is None for user_id={user_id}, mem_cube_id={mem_cube_id}, skipping processing"
1101
+ )
1102
+ return
1103
+ content = message.content
1104
+ user_name = message.user_name
1105
+
1106
+ # Parse the memory IDs from content
1107
+ mem_ids = json.loads(content) if isinstance(content, str) else content
1108
+ if not mem_ids:
1109
+ return
1110
+
1111
+ logger.info(
1112
+ f"Processing mem_reorganize for user_id={user_id}, mem_cube_id={mem_cube_id}, mem_ids={mem_ids}"
1113
+ )
1114
+
1115
+ # Get the text memory from the mem_cube
1116
+ text_mem = mem_cube.text_mem
1117
+ if not isinstance(text_mem, TreeTextMemory):
1118
+ logger.error(f"Expected TreeTextMemory but got {type(text_mem).__name__}")
1119
+ return
1120
+
1121
+ # Use mem_reader to process the memories
1122
+ self._process_memories_with_reorganize(
1123
+ mem_ids=mem_ids,
1124
+ user_id=user_id,
1125
+ mem_cube_id=mem_cube_id,
1126
+ mem_cube=mem_cube,
1127
+ text_mem=text_mem,
1128
+ user_name=user_name,
1129
+ )
1130
+
1131
+ with contextlib.suppress(Exception):
1132
+ mem_items: list[TextualMemoryItem] = []
1133
+ for mid in mem_ids:
1134
+ with contextlib.suppress(Exception):
1135
+ mem_items.append(text_mem.get(mid, user_name=user_name))
1136
+ if len(mem_items) > 1:
1137
+ keys: list[str] = []
1138
+ memcube_content: list[dict] = []
1139
+ meta: list[dict] = []
1140
+ merged_target_ids: set[str] = set()
1141
+ with contextlib.suppress(Exception):
1142
+ if hasattr(text_mem, "graph_store"):
1143
+ for mid in mem_ids:
1144
+ edges = text_mem.graph_store.get_edges(
1145
+ mid, type="MERGED_TO", direction="OUT"
1146
+ )
1147
+ for edge in edges:
1148
+ target = (
1149
+ edge.get("to") or edge.get("dst") or edge.get("target")
1150
+ )
1151
+ if target:
1152
+ merged_target_ids.add(target)
1153
+ for item in mem_items:
1154
+ key = getattr(
1155
+ getattr(item, "metadata", {}), "key", None
1156
+ ) or transform_name_to_key(getattr(item, "memory", ""))
1157
+ keys.append(key)
1158
+ memcube_content.append(
1159
+ {"content": key or "(no key)", "ref_id": item.id, "type": "merged"}
1160
+ )
1161
+ meta.append(
1162
+ {
1163
+ "ref_id": item.id,
1164
+ "id": item.id,
1165
+ "key": key,
1166
+ "memory": item.memory,
1167
+ "memory_type": item.metadata.memory_type,
1168
+ "status": item.metadata.status,
1169
+ "confidence": item.metadata.confidence,
1170
+ "tags": item.metadata.tags,
1171
+ "updated_at": getattr(item.metadata, "updated_at", None)
1172
+ or getattr(item.metadata, "update_at", None),
1173
+ }
1174
+ )
1175
+ combined_key = keys[0] if keys else ""
1176
+ post_ref_id = None
1177
+ post_meta = {
1178
+ "ref_id": None,
1179
+ "id": None,
1180
+ "key": None,
1181
+ "memory": None,
1182
+ "memory_type": None,
1183
+ "status": None,
1184
+ "confidence": None,
1185
+ "tags": None,
1186
+ "updated_at": None,
1187
+ }
1188
+ if merged_target_ids:
1189
+ post_ref_id = next(iter(merged_target_ids))
1190
+ with contextlib.suppress(Exception):
1191
+ merged_item = text_mem.get(post_ref_id, user_name=user_name)
1192
+ combined_key = (
1193
+ getattr(getattr(merged_item, "metadata", {}), "key", None)
1194
+ or combined_key
1195
+ )
1196
+ post_meta = {
1197
+ "ref_id": post_ref_id,
1198
+ "id": post_ref_id,
1199
+ "key": getattr(
1200
+ getattr(merged_item, "metadata", {}), "key", None
1201
+ ),
1202
+ "memory": getattr(merged_item, "memory", None),
1203
+ "memory_type": getattr(
1204
+ getattr(merged_item, "metadata", {}), "memory_type", None
1205
+ ),
1206
+ "status": getattr(
1207
+ getattr(merged_item, "metadata", {}), "status", None
1208
+ ),
1209
+ "confidence": getattr(
1210
+ getattr(merged_item, "metadata", {}), "confidence", None
1211
+ ),
1212
+ "tags": getattr(
1213
+ getattr(merged_item, "metadata", {}), "tags", None
1214
+ ),
1215
+ "updated_at": getattr(
1216
+ getattr(merged_item, "metadata", {}), "updated_at", None
1217
+ )
1218
+ or getattr(
1219
+ getattr(merged_item, "metadata", {}), "update_at", None
1220
+ ),
1221
+ }
1222
+ if not post_ref_id:
1223
+ import hashlib
1224
+
1225
+ post_ref_id = f"merge-{hashlib.md5(''.join(sorted(mem_ids)).encode()).hexdigest()}"
1226
+ post_meta["ref_id"] = post_ref_id
1227
+ post_meta["id"] = post_ref_id
1228
+ if not post_meta.get("key"):
1229
+ post_meta["key"] = combined_key
1230
+ if not keys:
1231
+ keys = [item.id for item in mem_items]
1232
+ memcube_content.append(
1233
+ {
1234
+ "content": combined_key if combined_key else "(no key)",
1235
+ "ref_id": post_ref_id,
1236
+ "type": "postMerge",
1237
+ }
1238
+ )
1239
+ meta.append(post_meta)
1240
+ event = self.create_event_log(
1241
+ label="mergeMemory",
1242
+ from_memory_type=LONG_TERM_MEMORY_TYPE,
1243
+ to_memory_type=LONG_TERM_MEMORY_TYPE,
1244
+ user_id=user_id,
1245
+ mem_cube_id=mem_cube_id,
1246
+ mem_cube=mem_cube,
1247
+ memcube_log_content=memcube_content,
1248
+ metadata=meta,
1249
+ memory_len=len(keys),
1250
+ memcube_name=self._map_memcube_name(mem_cube_id),
1251
+ )
1252
+ self._submit_web_logs([event])
1253
+
1254
+ logger.info(
1255
+ f"Successfully processed mem_reorganize for user_id={user_id}, mem_cube_id={mem_cube_id}"
1256
+ )
1257
+
1258
+ except Exception as e:
1259
+ logger.error(f"Error processing mem_reorganize message: {e}", exc_info=True)
1260
+
1261
+ with ContextThreadPoolExecutor(max_workers=min(8, len(messages))) as executor:
1262
+ futures = [executor.submit(process_message, msg) for msg in messages]
1263
+ for future in concurrent.futures.as_completed(futures):
1264
+ try:
1265
+ future.result()
1266
+ except Exception as e:
1267
+ logger.error(f"Thread task failed: {e}", exc_info=True)
1268
+
1269
+ def _process_memories_with_reorganize(
1270
+ self,
1271
+ mem_ids: list[str],
1272
+ user_id: str,
1273
+ mem_cube_id: str,
1274
+ mem_cube: GeneralMemCube,
1275
+ text_mem: TreeTextMemory,
1276
+ user_name: str,
1277
+ ) -> None:
1278
+ """
1279
+ Process memories using mem_reorganize for enhanced memory processing.
1280
+
1281
+ Args:
1282
+ mem_ids: List of memory IDs to process
1283
+ user_id: User ID
1284
+ mem_cube_id: Memory cube ID
1285
+ mem_cube: Memory cube instance
1286
+ text_mem: Text memory instance
1287
+ """
1288
+ try:
1289
+ # Get the mem_reader from the parent MOSCore
1290
+ if not hasattr(self, "mem_reader") or self.mem_reader is None:
1291
+ logger.warning(
1292
+ "mem_reader not available in scheduler, skipping enhanced processing"
1293
+ )
1294
+ return
1295
+
1296
+ # Get the original memory items
1297
+ memory_items = []
1298
+ for mem_id in mem_ids:
1299
+ try:
1300
+ memory_item = text_mem.get(mem_id, user_name=user_name)
1301
+ memory_items.append(memory_item)
1302
+ except Exception as e:
1303
+ logger.warning(f"Failed to get memory {mem_id}: {e}|{traceback.format_exc()}")
1304
+ continue
1305
+
1306
+ if not memory_items:
1307
+ logger.warning("No valid memory items found for processing")
1308
+ return
1309
+
1310
+ # Use mem_reader to process the memories
1311
+ logger.info(f"Processing {len(memory_items)} memories with mem_reader")
1312
+ text_mem.memory_manager.remove_and_refresh_memory(user_name=user_name)
1313
+ logger.info("Remove and Refresh Memories")
1314
+ logger.debug(f"Finished add {user_id} memory: {mem_ids}")
1315
+
1316
+ except Exception:
1317
+ logger.error(
1318
+ f"Error in _process_memories_with_reorganize: {traceback.format_exc()}",
1319
+ exc_info=True,
1320
+ )
1321
+
1322
+ def _pref_add_message_consumer(self, messages: list[ScheduleMessageItem]) -> None:
1323
+ logger.info(f"Messages {messages} assigned to {PREF_ADD_TASK_LABEL} handler.")
1324
+
1325
+ def process_message(message: ScheduleMessageItem):
1326
+ try:
1327
+ mem_cube = self.mem_cube
1328
+ if mem_cube is None:
1329
+ logger.warning(
1330
+ f"mem_cube is None for user_id={message.user_id}, mem_cube_id={message.mem_cube_id}, skipping processing"
1331
+ )
1332
+ return
1333
+
1334
+ user_id = message.user_id
1335
+ session_id = message.session_id
1336
+ mem_cube_id = message.mem_cube_id
1337
+ content = message.content
1338
+ messages_list = json.loads(content)
1339
+ info = message.info or {}
1340
+
1341
+ logger.info(f"Processing pref_add for user_id={user_id}, mem_cube_id={mem_cube_id}")
1342
+
1343
+ # Get the preference memory from the mem_cube
1344
+ pref_mem = mem_cube.pref_mem
1345
+ if pref_mem is None:
1346
+ logger.warning(
1347
+ f"Preference memory not initialized for mem_cube_id={mem_cube_id}, "
1348
+ f"skipping pref_add processing"
1349
+ )
1350
+ return
1351
+ if not isinstance(pref_mem, PreferenceTextMemory):
1352
+ logger.error(
1353
+ f"Expected PreferenceTextMemory but got {type(pref_mem).__name__} "
1354
+ f"for mem_cube_id={mem_cube_id}"
1355
+ )
1356
+ return
1357
+
1358
+ # Use pref_mem.get_memory to process the memories
1359
+ pref_memories = pref_mem.get_memory(
1360
+ messages_list,
1361
+ type="chat",
1362
+ info={
1363
+ **info,
1364
+ "user_id": user_id,
1365
+ "session_id": session_id,
1366
+ "mem_cube_id": mem_cube_id,
1367
+ },
1368
+ )
1369
+ # Add pref_mem to vector db
1370
+ pref_ids = pref_mem.add(pref_memories)
1371
+
1372
+ logger.info(
1373
+ f"Successfully processed and add preferences for user_id={user_id}, mem_cube_id={mem_cube_id}, pref_ids={pref_ids}"
1374
+ )
1375
+
1376
+ except Exception as e:
1377
+ logger.error(f"Error processing pref_add message: {e}", exc_info=True)
1378
+
1379
+ with ContextThreadPoolExecutor(max_workers=min(8, len(messages))) as executor:
1380
+ futures = [executor.submit(process_message, msg) for msg in messages]
1381
+ for future in concurrent.futures.as_completed(futures):
1382
+ try:
1383
+ future.result()
1384
+ except Exception as e:
1385
+ logger.error(f"Thread task failed: {e}", exc_info=True)
1386
+
1387
+ def process_session_turn(
1388
+ self,
1389
+ queries: str | list[str],
1390
+ user_id: UserID | str,
1391
+ mem_cube_id: MemCubeID | str,
1392
+ mem_cube: GeneralMemCube,
1393
+ top_k: int = 10,
1394
+ ) -> tuple[list[TextualMemoryItem], list[TextualMemoryItem]] | None:
1395
+ """
1396
+ Process a dialog turn:
1397
+ - If q_list reaches window size, trigger retrieval;
1398
+ - Immediately switch to the new memory if retrieval is triggered.
1399
+ """
1400
+
1401
+ text_mem_base = mem_cube.text_mem
1402
+ if not isinstance(text_mem_base, TreeTextMemory):
1403
+ if isinstance(text_mem_base, NaiveTextMemory):
1404
+ logger.debug(
1405
+ f"NaiveTextMemory used for mem_cube_id={mem_cube_id}, processing session turn with simple search."
1406
+ )
1407
+ # Treat NaiveTextMemory similar to TreeTextMemory but with simpler logic
1408
+ # We will perform retrieval to get "working memory" candidates for activation memory
1409
+ # But we won't have a distinct "current working memory"
1410
+ cur_working_memory = []
1411
+ else:
1412
+ logger.warning(
1413
+ f"Not implemented! Expected TreeTextMemory but got {type(text_mem_base).__name__} "
1414
+ f"for mem_cube_id={mem_cube_id}, user_id={user_id}. "
1415
+ f"text_mem_base value: {text_mem_base}"
1416
+ )
1417
+ return [], []
1418
+ else:
1419
+ cur_working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory(
1420
+ user_name=mem_cube_id
1421
+ )
1422
+ cur_working_memory = cur_working_memory[:top_k]
1423
+
1424
+ logger.info(
1425
+ f"[process_session_turn] Processing {len(queries)} queries for user_id={user_id}, mem_cube_id={mem_cube_id}"
1426
+ )
1427
+
1428
+ text_working_memory: list[str] = [w_m.memory for w_m in cur_working_memory]
1429
+ intent_result = self.monitor.detect_intent(
1430
+ q_list=queries, text_working_memory=text_working_memory
1431
+ )
1432
+
1433
+ time_trigger_flag = False
1434
+ if self.monitor.timed_trigger(
1435
+ last_time=self.monitor.last_query_consume_time,
1436
+ interval_seconds=self.monitor.query_trigger_interval,
1437
+ ):
1438
+ time_trigger_flag = True
1439
+
1440
+ if (not intent_result["trigger_retrieval"]) and (not time_trigger_flag):
1441
+ logger.info(
1442
+ f"[process_session_turn] Query schedule not triggered for user_id={user_id}, mem_cube_id={mem_cube_id}. Intent_result: {intent_result}"
1443
+ )
1444
+ return
1445
+ elif (not intent_result["trigger_retrieval"]) and time_trigger_flag:
1446
+ logger.info(
1447
+ f"[process_session_turn] Query schedule forced to trigger due to time ticker for user_id={user_id}, mem_cube_id={mem_cube_id}"
1448
+ )
1449
+ intent_result["trigger_retrieval"] = True
1450
+ intent_result["missing_evidences"] = queries
1451
+ else:
1452
+ logger.info(
1453
+ f"[process_session_turn] Query schedule triggered for user_id={user_id}, mem_cube_id={mem_cube_id}. "
1454
+ f"Missing evidences: {intent_result['missing_evidences']}"
1455
+ )
1456
+
1457
+ missing_evidences = intent_result["missing_evidences"]
1458
+ num_evidence = len(missing_evidences)
1459
+ k_per_evidence = max(1, top_k // max(1, num_evidence))
1460
+ new_candidates = []
1461
+ for item in missing_evidences:
1462
+ logger.info(
1463
+ f"[process_session_turn] Searching for missing evidence: '{item}' with top_k={k_per_evidence} for user_id={user_id}"
1464
+ )
1465
+
1466
+ search_args = {}
1467
+ if isinstance(text_mem_base, NaiveTextMemory):
1468
+ # NaiveTextMemory doesn't support complex search args usually, but let's see
1469
+ # self.retriever.search calls mem_cube.text_mem.search
1470
+ # NaiveTextMemory.search takes query and top_k
1471
+ # SchedulerRetriever.search handles method dispatch
1472
+ # For NaiveTextMemory, we might need to bypass retriever or extend it
1473
+ # But let's try calling naive memory directly if retriever fails or doesn't support it
1474
+ try:
1475
+ results = text_mem_base.search(query=item, top_k=k_per_evidence)
1476
+ except Exception as e:
1477
+ logger.warning(f"NaiveTextMemory search failed: {e}")
1478
+ results = []
1479
+ else:
1480
+ results: list[TextualMemoryItem] = self.retriever.search(
1481
+ query=item,
1482
+ user_id=user_id,
1483
+ mem_cube_id=mem_cube_id,
1484
+ mem_cube=mem_cube,
1485
+ top_k=k_per_evidence,
1486
+ method=self.search_method,
1487
+ search_args=search_args,
1488
+ )
1489
+
1490
+ logger.info(
1491
+ f"[process_session_turn] Search results for missing evidence '{item}': "
1492
+ + ("\n- " + "\n- ".join([f"{one.id}: {one.memory}" for one in results]))
1493
+ )
1494
+ new_candidates.extend(results)
1495
+ return cur_working_memory, new_candidates