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,1608 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import random
5
+ import time
6
+
7
+ from collections.abc import Generator
8
+ from datetime import datetime
9
+ from typing import Any, Literal
10
+
11
+ from dotenv import load_dotenv
12
+ from transformers import AutoTokenizer
13
+
14
+ from memos.configs.mem_cube import GeneralMemCubeConfig
15
+ from memos.configs.mem_os import MOSConfig
16
+ from memos.context.context import ContextThread
17
+ from memos.log import get_logger
18
+ from memos.mem_cube.general import GeneralMemCube
19
+ from memos.mem_os.core import MOSCore
20
+ from memos.mem_os.utils.format_utils import (
21
+ clean_json_response,
22
+ convert_graph_to_tree_forworkmem,
23
+ ensure_unique_tree_ids,
24
+ filter_nodes_by_tree_ids,
25
+ remove_embedding_recursive,
26
+ sort_children_by_memory_type,
27
+ )
28
+ from memos.mem_os.utils.reference_utils import (
29
+ prepare_reference_data,
30
+ process_streaming_references_complete,
31
+ )
32
+ from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
33
+ from memos.mem_scheduler.schemas.task_schemas import (
34
+ ANSWER_TASK_LABEL,
35
+ QUERY_TASK_LABEL,
36
+ )
37
+ from memos.mem_user.persistent_factory import PersistentUserManagerFactory
38
+ from memos.mem_user.user_manager import UserRole
39
+ from memos.memories.textual.item import (
40
+ TextualMemoryItem,
41
+ )
42
+ from memos.templates.mos_prompts import (
43
+ FURTHER_SUGGESTION_PROMPT,
44
+ SUGGESTION_QUERY_PROMPT_EN,
45
+ SUGGESTION_QUERY_PROMPT_ZH,
46
+ get_memos_prompt,
47
+ )
48
+ from memos.types import MessageList
49
+ from memos.utils import timed
50
+
51
+
52
+ logger = get_logger(__name__)
53
+
54
+ load_dotenv()
55
+
56
+ CUBE_PATH = os.getenv("MOS_CUBE_PATH", "/tmp/data/")
57
+
58
+
59
+ def _short_id(mem_id: str) -> str:
60
+ return (mem_id or "").split("-")[0] if mem_id else ""
61
+
62
+
63
+ def _format_mem_block(memories_all, max_items: int = 20, max_chars_each: int = 320) -> str:
64
+ """
65
+ Modify TextualMemoryItem Format:
66
+ 1:abcd :: [P] text...
67
+ 2:ef01 :: [O] text...
68
+ sequence is [i:memId] i; [P]=PersonalMemory / [O]=OuterMemory
69
+ """
70
+ if not memories_all:
71
+ return "(none)", "(none)"
72
+
73
+ lines_o = []
74
+ lines_p = []
75
+ for idx, m in enumerate(memories_all[:max_items], 1):
76
+ mid = _short_id(getattr(m, "id", "") or "")
77
+ mtype = getattr(getattr(m, "metadata", {}), "memory_type", None) or getattr(
78
+ m, "metadata", {}
79
+ ).get("memory_type", "")
80
+ tag = "O" if "Outer" in str(mtype) else "P"
81
+ txt = (getattr(m, "memory", "") or "").replace("\n", " ").strip()
82
+ if len(txt) > max_chars_each:
83
+ txt = txt[: max_chars_each - 1] + "…"
84
+ mid = mid or f"mem_{idx}"
85
+ if tag == "O":
86
+ lines_o.append(f"[{idx}:{mid}] :: [{tag}] {txt}\n")
87
+ elif tag == "P":
88
+ lines_p.append(f"[{idx}:{mid}] :: [{tag}] {txt}")
89
+ return "\n".join(lines_o), "\n".join(lines_p)
90
+
91
+
92
+ class MOSProduct(MOSCore):
93
+ """
94
+ The MOSProduct class inherits from MOSCore and manages multiple users.
95
+ Each user has their own configuration and cube access, but shares the same model instances.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ default_config: MOSConfig | None = None,
101
+ max_user_instances: int = 1,
102
+ default_cube_config: GeneralMemCubeConfig | None = None,
103
+ online_bot=None,
104
+ error_bot=None,
105
+ ):
106
+ """
107
+ Initialize MOSProduct with an optional default configuration.
108
+
109
+ Args:
110
+ default_config (MOSConfig | None): Default configuration for new users
111
+ max_user_instances (int): Maximum number of user instances to keep in memory
112
+ default_cube_config (GeneralMemCubeConfig | None): Default cube configuration for loading cubes
113
+ online_bot: DingDing online_bot function or None if disabled
114
+ error_bot: DingDing error_bot function or None if disabled
115
+ """
116
+ # Initialize with a root config for shared resources
117
+ if default_config is None:
118
+ # Create a minimal config for root user
119
+ root_config = MOSConfig(
120
+ user_id="root",
121
+ session_id="root_session",
122
+ chat_model=default_config.chat_model if default_config else None,
123
+ mem_reader=default_config.mem_reader if default_config else None,
124
+ enable_mem_scheduler=default_config.enable_mem_scheduler
125
+ if default_config
126
+ else False,
127
+ mem_scheduler=default_config.mem_scheduler if default_config else None,
128
+ )
129
+ else:
130
+ root_config = default_config.model_copy(deep=True)
131
+ root_config.user_id = "root"
132
+ root_config.session_id = "root_session"
133
+
134
+ # Create persistent user manager BEFORE calling parent constructor
135
+ persistent_user_manager_client = PersistentUserManagerFactory.from_config(
136
+ config_factory=root_config.user_manager
137
+ )
138
+
139
+ # Initialize parent MOSCore with root config and persistent user manager
140
+ super().__init__(root_config, user_manager=persistent_user_manager_client)
141
+
142
+ # Product-specific attributes
143
+ self.default_config = default_config
144
+ self.default_cube_config = default_cube_config
145
+ self.max_user_instances = max_user_instances
146
+ self.online_bot = online_bot
147
+ self.error_bot = error_bot
148
+
149
+ # User-specific data structures
150
+ self.user_configs: dict[str, MOSConfig] = {}
151
+ self.user_cube_access: dict[str, set[str]] = {} # user_id -> set of cube_ids
152
+ self.user_chat_histories: dict[str, dict] = {}
153
+
154
+ # Note: self.user_manager is now the persistent user manager from parent class
155
+ # No need for separate global_user_manager as they are the same instance
156
+
157
+ # Initialize tiktoken for streaming
158
+ try:
159
+ # Use gpt2 encoding which is more stable and widely compatible
160
+ self.tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
161
+ logger.info("tokenizer initialized successfully for streaming")
162
+ except Exception as e:
163
+ logger.warning(
164
+ f"Failed to initialize tokenizer, will use character-based chunking: {e}"
165
+ )
166
+ self.tokenizer = None
167
+
168
+ # Restore user instances from persistent storage
169
+ self._restore_user_instances(default_cube_config=default_cube_config)
170
+ logger.info(f"User instances restored successfully, now user is {self.mem_cubes.keys()}")
171
+
172
+ def _restore_user_instances(
173
+ self, default_cube_config: GeneralMemCubeConfig | None = None
174
+ ) -> None:
175
+ """Restore user instances from persistent storage after service restart.
176
+
177
+ Args:
178
+ default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
179
+ """
180
+ try:
181
+ # Get all user configurations from persistent storage
182
+ user_configs = self.user_manager.list_user_configs(self.max_user_instances)
183
+
184
+ # Get the raw database records for sorting by updated_at
185
+ session = self.user_manager._get_session()
186
+ try:
187
+ from memos.mem_user.persistent_user_manager import UserConfig
188
+
189
+ db_configs = session.query(UserConfig).limit(self.max_user_instances).all()
190
+ # Create a mapping of user_id to updated_at timestamp
191
+ updated_at_map = {config.user_id: config.updated_at for config in db_configs}
192
+
193
+ # Sort by updated_at timestamp (most recent first) and limit by max_instances
194
+ sorted_configs = sorted(
195
+ user_configs.items(), key=lambda x: updated_at_map.get(x[0], ""), reverse=True
196
+ )[: self.max_user_instances]
197
+ finally:
198
+ session.close()
199
+
200
+ for user_id, config in sorted_configs:
201
+ if user_id != "root": # Skip root user
202
+ try:
203
+ # Store user config and cube access
204
+ self.user_configs[user_id] = config
205
+ self._load_user_cube_access(user_id)
206
+
207
+ # Pre-load all cubes for this user with default config
208
+ self._preload_user_cubes(user_id, default_cube_config)
209
+
210
+ logger.info(
211
+ f"Restored user configuration and pre-loaded cubes for {user_id}"
212
+ )
213
+
214
+ except Exception as e:
215
+ logger.error(f"Failed to restore user configuration for {user_id}: {e}")
216
+
217
+ except Exception as e:
218
+ logger.error(f"Error during user instance restoration: {e}")
219
+
220
+ def _initialize_cube_from_default_config(
221
+ self, cube_id: str, user_id: str, default_config: GeneralMemCubeConfig
222
+ ) -> GeneralMemCube | None:
223
+ """
224
+ Initialize a cube from default configuration when cube path doesn't exist.
225
+
226
+ Args:
227
+ cube_id (str): The cube ID to initialize.
228
+ user_id (str): The user ID for the cube.
229
+ default_config (GeneralMemCubeConfig): The default configuration to use.
230
+ """
231
+ cube_config = default_config.model_copy(deep=True)
232
+ # Safely modify the graph_db user_name if it exists
233
+ if cube_config.text_mem.config.graph_db.config:
234
+ cube_config.text_mem.config.graph_db.config.user_name = (
235
+ f"memos{user_id.replace('-', '')}"
236
+ )
237
+ mem_cube = GeneralMemCube(config=cube_config)
238
+ return mem_cube
239
+
240
+ def _preload_user_cubes(
241
+ self, user_id: str, default_cube_config: GeneralMemCubeConfig | None = None
242
+ ) -> None:
243
+ """Pre-load all cubes for a user into memory.
244
+
245
+ Args:
246
+ user_id (str): The user ID to pre-load cubes for.
247
+ default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
248
+ """
249
+ try:
250
+ # Get user's accessible cubes from persistent storage
251
+ accessible_cubes = self.user_manager.get_user_cubes(user_id)
252
+
253
+ for cube in accessible_cubes:
254
+ if cube.cube_id not in self.mem_cubes:
255
+ try:
256
+ if cube.cube_path and os.path.exists(cube.cube_path):
257
+ # Pre-load cube with all memory types and default config
258
+ self.register_mem_cube(
259
+ cube.cube_path,
260
+ cube.cube_id,
261
+ user_id,
262
+ memory_types=["act_mem"]
263
+ if self.config.enable_activation_memory
264
+ else [],
265
+ default_config=default_cube_config,
266
+ )
267
+ logger.info(f"Pre-loaded cube {cube.cube_id} for user {user_id}")
268
+ else:
269
+ logger.warning(
270
+ f"Cube path {cube.cube_path} does not exist for cube {cube.cube_id}, skipping pre-load"
271
+ )
272
+ except Exception as e:
273
+ logger.error(
274
+ f"Failed to pre-load cube {cube.cube_id} for user {user_id}: {e}",
275
+ exc_info=True,
276
+ )
277
+
278
+ except Exception as e:
279
+ logger.error(f"Error pre-loading cubes for user {user_id}: {e}", exc_info=True)
280
+
281
+ @timed
282
+ def _load_user_cubes(
283
+ self, user_id: str, default_cube_config: GeneralMemCubeConfig | None = None
284
+ ) -> None:
285
+ """Load all cubes for a user into memory.
286
+
287
+ Args:
288
+ user_id (str): The user ID to load cubes for.
289
+ default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
290
+ """
291
+ # Get user's accessible cubes from persistent storage
292
+ accessible_cubes = self.user_manager.get_user_cubes(user_id)
293
+
294
+ for cube in accessible_cubes[:1]:
295
+ if cube.cube_id not in self.mem_cubes:
296
+ try:
297
+ if cube.cube_path and os.path.exists(cube.cube_path):
298
+ # Use MOSCore's register_mem_cube method directly with default config
299
+ # Only load act_mem since text_mem is stored in database
300
+ self.register_mem_cube(
301
+ cube.cube_path,
302
+ cube.cube_id,
303
+ user_id,
304
+ memory_types=["act_mem"],
305
+ default_config=default_cube_config,
306
+ )
307
+ else:
308
+ logger.warning(
309
+ f"Cube path {cube.cube_path} does not exist for cube {cube.cube_id}, now init by default config"
310
+ )
311
+ cube_obj = self._initialize_cube_from_default_config(
312
+ cube_id=cube.cube_id,
313
+ user_id=user_id,
314
+ default_config=default_cube_config,
315
+ )
316
+ if cube_obj:
317
+ self.register_mem_cube(
318
+ cube_obj,
319
+ cube.cube_id,
320
+ user_id,
321
+ memory_types=[],
322
+ )
323
+ else:
324
+ raise ValueError(
325
+ f"Failed to initialize default cube {cube.cube_id} for user {user_id}"
326
+ )
327
+ except Exception as e:
328
+ logger.error(f"Failed to load cube {cube.cube_id} for user {user_id}: {e}")
329
+ logger.info(f"load user {user_id} cubes successfully")
330
+
331
+ def _ensure_user_instance(self, user_id: str, max_instances: int | None = None) -> None:
332
+ """
333
+ Ensure user configuration exists, creating it if necessary.
334
+
335
+ Args:
336
+ user_id (str): The user ID
337
+ max_instances (int): Maximum instances to keep in memory (overrides class default)
338
+ """
339
+ if user_id in self.user_configs:
340
+ return
341
+
342
+ # Try to get config from persistent storage first
343
+ stored_config = self.user_manager.get_user_config(user_id)
344
+ if stored_config:
345
+ self.user_configs[user_id] = stored_config
346
+ self._load_user_cube_access(user_id)
347
+ else:
348
+ # Use default config
349
+ if not self.default_config:
350
+ raise ValueError(f"No configuration available for user {user_id}")
351
+ user_config = self.default_config.model_copy(deep=True)
352
+ user_config.user_id = user_id
353
+ user_config.session_id = f"{user_id}_session"
354
+ self.user_configs[user_id] = user_config
355
+ self._load_user_cube_access(user_id)
356
+
357
+ # Apply LRU eviction if needed
358
+ max_instances = max_instances or self.max_user_instances
359
+ if len(self.user_configs) > max_instances:
360
+ # Remove least recently used instance (excluding root)
361
+ user_ids = [uid for uid in self.user_configs if uid != "root"]
362
+ if user_ids:
363
+ oldest_user_id = user_ids[0]
364
+ del self.user_configs[oldest_user_id]
365
+ if oldest_user_id in self.user_cube_access:
366
+ del self.user_cube_access[oldest_user_id]
367
+ logger.info(f"Removed least recently used user configuration: {oldest_user_id}")
368
+
369
+ def _load_user_cube_access(self, user_id: str) -> None:
370
+ """Load user's cube access permissions."""
371
+ try:
372
+ # Get user's accessible cubes from persistent storage
373
+ accessible_cubes = self.user_manager.get_user_cube_access(user_id)
374
+ self.user_cube_access[user_id] = set(accessible_cubes)
375
+ except Exception as e:
376
+ logger.warning(f"Failed to load cube access for user {user_id}: {e}")
377
+ self.user_cube_access[user_id] = set()
378
+
379
+ def _get_user_config(self, user_id: str) -> MOSConfig:
380
+ """Get user configuration."""
381
+ if user_id not in self.user_configs:
382
+ self._ensure_user_instance(user_id)
383
+ return self.user_configs[user_id]
384
+
385
+ def _validate_user_cube_access(self, user_id: str, cube_id: str) -> None:
386
+ """Validate user has access to the cube."""
387
+ if user_id not in self.user_cube_access:
388
+ self._load_user_cube_access(user_id)
389
+
390
+ if cube_id not in self.user_cube_access.get(user_id, set()):
391
+ raise ValueError(f"User '{user_id}' does not have access to cube '{cube_id}'")
392
+
393
+ def _validate_user_access(self, user_id: str, cube_id: str | None = None) -> None:
394
+ """Validate user access using MOSCore's built-in validation."""
395
+ # Use MOSCore's built-in user validation
396
+ if cube_id:
397
+ self._validate_cube_access(user_id, cube_id)
398
+ else:
399
+ self._validate_user_exists(user_id)
400
+
401
+ def _create_user_config(self, user_id: str, config: MOSConfig) -> MOSConfig:
402
+ """Create a new user configuration."""
403
+ # Create a copy of config with the specific user_id
404
+ user_config = config.model_copy(deep=True)
405
+ user_config.user_id = user_id
406
+ user_config.session_id = f"{user_id}_session"
407
+
408
+ # Save configuration to persistent storage
409
+ self.user_manager.save_user_config(user_id, user_config)
410
+
411
+ return user_config
412
+
413
+ def _get_or_create_user_config(
414
+ self, user_id: str, config: MOSConfig | None = None
415
+ ) -> MOSConfig:
416
+ """Get existing user config or create a new one."""
417
+ if user_id in self.user_configs:
418
+ return self.user_configs[user_id]
419
+
420
+ # Try to get config from persistent storage first
421
+ stored_config = self.user_manager.get_user_config(user_id)
422
+ if stored_config:
423
+ return self._create_user_config(user_id, stored_config)
424
+
425
+ # Use provided config or default config
426
+ user_config = config or self.default_config
427
+ if not user_config:
428
+ raise ValueError(f"No configuration provided for user {user_id}")
429
+
430
+ return self._create_user_config(user_id, user_config)
431
+
432
+ def _build_system_prompt(
433
+ self,
434
+ memories_all: list[TextualMemoryItem],
435
+ base_prompt: str | None = None,
436
+ tone: str = "friendly",
437
+ verbosity: str = "mid",
438
+ ) -> str:
439
+ """
440
+ Build custom system prompt for the user with memory references.
441
+
442
+ Args:
443
+ user_id (str): The user ID.
444
+ memories (list[TextualMemoryItem]): The memories to build the system prompt.
445
+
446
+ Returns:
447
+ str: The custom system prompt.
448
+ """
449
+ # Build base prompt
450
+ # Add memory context if available
451
+ now = datetime.now()
452
+ formatted_date = now.strftime("%Y-%m-%d (%A)")
453
+ sys_body = get_memos_prompt(
454
+ date=formatted_date, tone=tone, verbosity=verbosity, mode="base"
455
+ )
456
+ mem_block_o, mem_block_p = _format_mem_block(memories_all)
457
+ mem_block = mem_block_o + "\n" + mem_block_p
458
+ prefix = (base_prompt.strip() + "\n\n") if base_prompt else ""
459
+ return (
460
+ prefix
461
+ + sys_body
462
+ + "\n\n# Memories\n## PersonalMemory & OuterMemory (ordered)\n"
463
+ + mem_block
464
+ )
465
+
466
+ def _build_base_system_prompt(
467
+ self,
468
+ base_prompt: str | None = None,
469
+ tone: str = "friendly",
470
+ verbosity: str = "mid",
471
+ mode: str = "enhance",
472
+ ) -> str:
473
+ """
474
+ Build base system prompt without memory references.
475
+ """
476
+ now = datetime.now()
477
+ formatted_date = now.strftime("%Y-%m-%d (%A)")
478
+ sys_body = get_memos_prompt(date=formatted_date, tone=tone, verbosity=verbosity, mode=mode)
479
+ prefix = (base_prompt.strip() + "\n\n") if base_prompt else ""
480
+ return prefix + sys_body
481
+
482
+ def _build_memory_context(
483
+ self,
484
+ memories_all: list[TextualMemoryItem],
485
+ mode: str = "enhance",
486
+ ) -> str:
487
+ """
488
+ Build memory context to be included in user message.
489
+ """
490
+ if not memories_all:
491
+ return ""
492
+
493
+ mem_block_o, mem_block_p = _format_mem_block(memories_all)
494
+
495
+ if mode == "enhance":
496
+ return (
497
+ "# Memories\n## PersonalMemory (ordered)\n"
498
+ + mem_block_p
499
+ + "\n## OuterMemory (ordered)\n"
500
+ + mem_block_o
501
+ + "\n\n"
502
+ )
503
+ else:
504
+ mem_block = mem_block_o + "\n" + mem_block_p
505
+ return "# Memories\n## PersonalMemory & OuterMemory (ordered)\n" + mem_block + "\n\n"
506
+
507
+ def _build_enhance_system_prompt(
508
+ self,
509
+ user_id: str,
510
+ memories_all: list[TextualMemoryItem],
511
+ tone: str = "friendly",
512
+ verbosity: str = "mid",
513
+ ) -> str:
514
+ """
515
+ Build enhance prompt for the user with memory references.
516
+ [DEPRECATED] Use _build_base_system_prompt and _build_memory_context instead.
517
+ """
518
+ now = datetime.now()
519
+ formatted_date = now.strftime("%Y-%m-%d (%A)")
520
+ sys_body = get_memos_prompt(
521
+ date=formatted_date, tone=tone, verbosity=verbosity, mode="enhance"
522
+ )
523
+ mem_block_o, mem_block_p = _format_mem_block(memories_all)
524
+ return (
525
+ sys_body
526
+ + "\n\n# Memories\n## PersonalMemory (ordered)\n"
527
+ + mem_block_p
528
+ + "\n## OuterMemory (ordered)\n"
529
+ + mem_block_o
530
+ )
531
+
532
+ def _extract_references_from_response(self, response: str) -> tuple[str, list[dict]]:
533
+ """
534
+ Extract reference information from the response and return clean text.
535
+
536
+ Args:
537
+ response (str): The complete response text.
538
+
539
+ Returns:
540
+ tuple[str, list[dict]]: A tuple containing:
541
+ - clean_text: Text with reference markers removed
542
+ - references: List of reference information
543
+ """
544
+ import re
545
+
546
+ try:
547
+ references = []
548
+ # Pattern to match [refid:memoriesID]
549
+ pattern = r"\[(\d+):([^\]]+)\]"
550
+
551
+ matches = re.findall(pattern, response)
552
+ for ref_number, memory_id in matches:
553
+ references.append({"memory_id": memory_id, "reference_number": int(ref_number)})
554
+
555
+ # Remove all reference markers from the text to get clean text
556
+ clean_text = re.sub(pattern, "", response)
557
+
558
+ # Clean up any extra whitespace that might be left after removing markers
559
+ clean_text = re.sub(r"\s+", " ", clean_text).strip()
560
+
561
+ return clean_text, references
562
+ except Exception as e:
563
+ logger.error(f"Error extracting references from response: {e}", exc_info=True)
564
+ return response, []
565
+
566
+ def _extract_struct_data_from_history(self, chat_data: list[dict]) -> dict:
567
+ """
568
+ get struct message from chat-history
569
+ # TODO: @xcy make this more general
570
+ """
571
+ system_content = ""
572
+ memory_content = ""
573
+ chat_history = []
574
+
575
+ for item in chat_data:
576
+ role = item.get("role")
577
+ content = item.get("content", "")
578
+ if role == "system":
579
+ parts = content.split("# Memories", 1)
580
+ system_content = parts[0].strip()
581
+ if len(parts) > 1:
582
+ memory_content = "# Memories" + parts[1].strip()
583
+ elif role in ("user", "assistant"):
584
+ chat_history.append({"role": role, "content": content})
585
+
586
+ if chat_history and chat_history[-1]["role"] == "assistant":
587
+ if len(chat_history) >= 2 and chat_history[-2]["role"] == "user":
588
+ chat_history = chat_history[:-2]
589
+ else:
590
+ chat_history = chat_history[:-1]
591
+
592
+ return {"system": system_content, "memory": memory_content, "chat_history": chat_history}
593
+
594
+ def _chunk_response_with_tiktoken(
595
+ self, response: str, chunk_size: int = 5
596
+ ) -> Generator[str, None, None]:
597
+ """
598
+ Chunk response using tiktoken for proper token-based streaming.
599
+
600
+ Args:
601
+ response (str): The response text to chunk.
602
+ chunk_size (int): Number of tokens per chunk.
603
+
604
+ Yields:
605
+ str: Chunked text pieces.
606
+ """
607
+ if self.tokenizer:
608
+ # Use tiktoken for proper token-based chunking
609
+ tokens = self.tokenizer.encode(response)
610
+
611
+ for i in range(0, len(tokens), chunk_size):
612
+ token_chunk = tokens[i : i + chunk_size]
613
+ chunk_text = self.tokenizer.decode(token_chunk)
614
+ yield chunk_text
615
+ else:
616
+ # Fallback to character-based chunking
617
+ char_chunk_size = chunk_size * 4 # Approximate character to token ratio
618
+ for i in range(0, len(response), char_chunk_size):
619
+ yield response[i : i + char_chunk_size]
620
+
621
+ def _send_message_to_scheduler(
622
+ self,
623
+ user_id: str,
624
+ mem_cube_id: str,
625
+ query: str,
626
+ label: str,
627
+ ):
628
+ """
629
+ Send message to scheduler.
630
+ args:
631
+ user_id: str,
632
+ mem_cube_id: str,
633
+ query: str,
634
+ """
635
+
636
+ if self.enable_mem_scheduler and (self.mem_scheduler is not None):
637
+ message_item = ScheduleMessageItem(
638
+ user_id=user_id,
639
+ mem_cube_id=mem_cube_id,
640
+ label=label,
641
+ content=query,
642
+ timestamp=datetime.utcnow(),
643
+ )
644
+ self.mem_scheduler.submit_messages(messages=[message_item])
645
+
646
+ async def _post_chat_processing(
647
+ self,
648
+ user_id: str,
649
+ cube_id: str,
650
+ query: str,
651
+ full_response: str,
652
+ system_prompt: str,
653
+ time_start: float,
654
+ time_end: float,
655
+ speed_improvement: float,
656
+ current_messages: list,
657
+ ) -> None:
658
+ """
659
+ Asynchronous processing of logs, notifications and memory additions
660
+ """
661
+ try:
662
+ logger.info(
663
+ f"user_id: {user_id}, cube_id: {cube_id}, current_messages: {current_messages}"
664
+ )
665
+ logger.info(f"user_id: {user_id}, cube_id: {cube_id}, full_response: {full_response}")
666
+
667
+ clean_response, extracted_references = self._extract_references_from_response(
668
+ full_response
669
+ )
670
+ struct_message = self._extract_struct_data_from_history(current_messages)
671
+ logger.info(f"Extracted {len(extracted_references)} references from response")
672
+
673
+ # Send chat report notifications asynchronously
674
+ if self.online_bot:
675
+ logger.info("Online Bot Open!")
676
+ try:
677
+ from memos.memos_tools.notification_utils import (
678
+ send_online_bot_notification_async,
679
+ )
680
+
681
+ # Prepare notification data
682
+ chat_data = {"query": query, "user_id": user_id, "cube_id": cube_id}
683
+ chat_data.update(
684
+ {
685
+ "memory": struct_message["memory"],
686
+ "chat_history": struct_message["chat_history"],
687
+ "full_response": full_response,
688
+ }
689
+ )
690
+
691
+ system_data = {
692
+ "references": extracted_references,
693
+ "time_start": time_start,
694
+ "time_end": time_end,
695
+ "speed_improvement": speed_improvement,
696
+ }
697
+
698
+ emoji_config = {"chat": "💬", "system_info": "📊"}
699
+
700
+ await send_online_bot_notification_async(
701
+ online_bot=self.online_bot,
702
+ header_name="MemOS Chat Report",
703
+ sub_title_name="chat_with_references",
704
+ title_color="#00956D",
705
+ other_data1=chat_data,
706
+ other_data2=system_data,
707
+ emoji=emoji_config,
708
+ )
709
+ except Exception as e:
710
+ logger.warning(f"Failed to send chat notification (async): {e}")
711
+
712
+ self._send_message_to_scheduler(
713
+ user_id=user_id, mem_cube_id=cube_id, query=clean_response, label=ANSWER_TASK_LABEL
714
+ )
715
+
716
+ self.add(
717
+ user_id=user_id,
718
+ messages=[
719
+ {
720
+ "role": "user",
721
+ "content": query,
722
+ "chat_time": str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
723
+ },
724
+ {
725
+ "role": "assistant",
726
+ "content": clean_response, # Store clean text without reference markers
727
+ "chat_time": str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
728
+ },
729
+ ],
730
+ mem_cube_id=cube_id,
731
+ )
732
+
733
+ logger.info(f"Post-chat processing completed for user {user_id}")
734
+
735
+ except Exception as e:
736
+ logger.error(f"Error in post-chat processing for user {user_id}: {e}", exc_info=True)
737
+
738
+ def _start_post_chat_processing(
739
+ self,
740
+ user_id: str,
741
+ cube_id: str,
742
+ query: str,
743
+ full_response: str,
744
+ system_prompt: str,
745
+ time_start: float,
746
+ time_end: float,
747
+ speed_improvement: float,
748
+ current_messages: list,
749
+ ) -> None:
750
+ """
751
+ Asynchronous processing of logs, notifications and memory additions, handle synchronous and asynchronous environments
752
+ """
753
+ logger.info("Start post_chat_processing...")
754
+
755
+ def run_async_in_thread():
756
+ """Running asynchronous tasks in a new thread"""
757
+ try:
758
+ loop = asyncio.new_event_loop()
759
+ asyncio.set_event_loop(loop)
760
+ try:
761
+ loop.run_until_complete(
762
+ self._post_chat_processing(
763
+ user_id=user_id,
764
+ cube_id=cube_id,
765
+ query=query,
766
+ full_response=full_response,
767
+ system_prompt=system_prompt,
768
+ time_start=time_start,
769
+ time_end=time_end,
770
+ speed_improvement=speed_improvement,
771
+ current_messages=current_messages,
772
+ )
773
+ )
774
+ finally:
775
+ loop.close()
776
+ except Exception as e:
777
+ logger.error(
778
+ f"Error in thread-based post-chat processing for user {user_id}: {e}",
779
+ exc_info=True,
780
+ )
781
+
782
+ try:
783
+ # Try to get the current event loop
784
+ asyncio.get_running_loop()
785
+ # Create task and store reference to prevent garbage collection
786
+ task = asyncio.create_task(
787
+ self._post_chat_processing(
788
+ user_id=user_id,
789
+ cube_id=cube_id,
790
+ query=query,
791
+ full_response=full_response,
792
+ system_prompt=system_prompt,
793
+ time_start=time_start,
794
+ time_end=time_end,
795
+ speed_improvement=speed_improvement,
796
+ current_messages=current_messages,
797
+ )
798
+ )
799
+ # Add exception handling for the background task
800
+ task.add_done_callback(
801
+ lambda t: logger.error(
802
+ f"Error in background post-chat processing for user {user_id}: {t.exception()}",
803
+ exc_info=True,
804
+ )
805
+ if t.exception()
806
+ else None
807
+ )
808
+ except RuntimeError:
809
+ # No event loop, run in a new thread with context propagation
810
+ thread = ContextThread(
811
+ target=run_async_in_thread,
812
+ name=f"PostChatProcessing-{user_id}",
813
+ # Set as a daemon thread to avoid blocking program exit
814
+ daemon=True,
815
+ )
816
+ thread.start()
817
+
818
+ def _filter_memories_by_threshold(
819
+ self,
820
+ memories: list[TextualMemoryItem],
821
+ threshold: float = 0.30,
822
+ min_num: int = 3,
823
+ memory_type: Literal["OuterMemory"] = "OuterMemory",
824
+ ) -> list[TextualMemoryItem]:
825
+ """
826
+ Filter memories by threshold and type, at least min_num memories for Non-OuterMemory.
827
+ Args:
828
+ memories: list[TextualMemoryItem],
829
+ threshold: float,
830
+ min_num: int,
831
+ memory_type: Literal["OuterMemory"],
832
+ Returns:
833
+ list[TextualMemoryItem]
834
+ """
835
+ sorted_memories = sorted(memories, key=lambda m: m.metadata.relativity, reverse=True)
836
+ filtered_person = [m for m in memories if m.metadata.memory_type != memory_type]
837
+ filtered_outer = [m for m in memories if m.metadata.memory_type == memory_type]
838
+ filtered = []
839
+ per_memory_count = 0
840
+ for m in sorted_memories:
841
+ if m.metadata.relativity >= threshold:
842
+ if m.metadata.memory_type != memory_type:
843
+ per_memory_count += 1
844
+ filtered.append(m)
845
+ if len(filtered) < min_num:
846
+ filtered = filtered_person[:min_num] + filtered_outer[:min_num]
847
+ else:
848
+ if per_memory_count < min_num:
849
+ filtered += filtered_person[per_memory_count:min_num]
850
+ filtered_memory = sorted(filtered, key=lambda m: m.metadata.relativity, reverse=True)
851
+ return filtered_memory
852
+
853
+ def register_mem_cube(
854
+ self,
855
+ mem_cube_name_or_path_or_object: str | GeneralMemCube,
856
+ mem_cube_id: str | None = None,
857
+ user_id: str | None = None,
858
+ memory_types: list[Literal["text_mem", "act_mem", "para_mem"]] | None = None,
859
+ default_config: GeneralMemCubeConfig | None = None,
860
+ ) -> None:
861
+ """
862
+ Register a MemCube with the MOS.
863
+
864
+ Args:
865
+ mem_cube_name_or_path_or_object (str | GeneralMemCube): The name, path, or GeneralMemCube object to register.
866
+ mem_cube_id (str, optional): The identifier for the MemCube. If not provided, a default ID is used.
867
+ user_id (str, optional): The user ID to register the cube for.
868
+ memory_types (list[str], optional): List of memory types to load.
869
+ If None, loads all available memory types.
870
+ Options: ["text_mem", "act_mem", "para_mem"]
871
+ default_config (GeneralMemCubeConfig, optional): Default configuration for the cube.
872
+ """
873
+ # Handle different input types
874
+ if isinstance(mem_cube_name_or_path_or_object, GeneralMemCube):
875
+ # Direct GeneralMemCube object provided
876
+ mem_cube = mem_cube_name_or_path_or_object
877
+ if mem_cube_id is None:
878
+ mem_cube_id = f"cube_{id(mem_cube)}" # Generate a unique ID
879
+ else:
880
+ # String path provided
881
+ mem_cube_name_or_path = mem_cube_name_or_path_or_object
882
+ if mem_cube_id is None:
883
+ mem_cube_id = mem_cube_name_or_path
884
+
885
+ if mem_cube_id in self.mem_cubes:
886
+ logger.info(f"MemCube with ID {mem_cube_id} already in MOS, skip install.")
887
+ return
888
+
889
+ # Create MemCube from path
890
+ time_start = time.time()
891
+ if os.path.exists(mem_cube_name_or_path):
892
+ mem_cube = GeneralMemCube.init_from_dir(
893
+ mem_cube_name_or_path, memory_types, default_config
894
+ )
895
+ logger.info(
896
+ f"time register_mem_cube: init_from_dir time is: {time.time() - time_start}"
897
+ )
898
+ else:
899
+ logger.warning(
900
+ f"MemCube {mem_cube_name_or_path} does not exist, try to init from remote repo."
901
+ )
902
+ mem_cube = GeneralMemCube.init_from_remote_repo(
903
+ mem_cube_name_or_path, memory_types=memory_types, default_config=default_config
904
+ )
905
+
906
+ # Register the MemCube
907
+ logger.info(
908
+ f"Registering MemCube {mem_cube_id} with cube config {mem_cube.config.model_dump(mode='json')}"
909
+ )
910
+ time_start = time.time()
911
+ self.mem_cubes[mem_cube_id] = mem_cube
912
+ time_end = time.time()
913
+ logger.info(f"time register_mem_cube: add mem_cube time is: {time_end - time_start}")
914
+
915
+ def user_register(
916
+ self,
917
+ user_id: str,
918
+ user_name: str | None = None,
919
+ config: MOSConfig | None = None,
920
+ interests: str | None = None,
921
+ default_mem_cube: GeneralMemCube | None = None,
922
+ default_cube_config: GeneralMemCubeConfig | None = None,
923
+ mem_cube_id: str | None = None,
924
+ ) -> dict[str, str]:
925
+ """Register a new user with configuration and default cube.
926
+
927
+ Args:
928
+ user_id (str): The user ID for registration.
929
+ user_name (str): The user name for registration.
930
+ config (MOSConfig | None, optional): User-specific configuration. Defaults to None.
931
+ interests (str | None, optional): User interests as string. Defaults to None.
932
+ default_mem_cube (GeneralMemCube | None, optional): Default memory cube. Defaults to None.
933
+ default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
934
+
935
+ Returns:
936
+ dict[str, str]: Registration result with status and message.
937
+ """
938
+ try:
939
+ # Use provided config or default config
940
+ user_config = config or self.default_config
941
+ if not user_config:
942
+ return {
943
+ "status": "error",
944
+ "message": "No configuration provided for user registration",
945
+ }
946
+ if not user_name:
947
+ user_name = user_id
948
+
949
+ # Create user with configuration using persistent user manager
950
+ self.user_manager.create_user_with_config(user_id, user_config, UserRole.USER, user_id)
951
+
952
+ # Create user configuration
953
+ user_config = self._create_user_config(user_id, user_config)
954
+
955
+ # Create a default cube for the user using MOSCore's methods
956
+ default_cube_name = f"{user_name}_{user_id}_default_cube"
957
+ mem_cube_name_or_path = os.path.join(CUBE_PATH, default_cube_name)
958
+ default_cube_id = self.create_cube_for_user(
959
+ cube_name=default_cube_name,
960
+ owner_id=user_id,
961
+ cube_path=mem_cube_name_or_path,
962
+ cube_id=mem_cube_id,
963
+ )
964
+ time_start = time.time()
965
+ if default_mem_cube:
966
+ try:
967
+ default_mem_cube.dump(mem_cube_name_or_path, memory_types=[])
968
+ except Exception as e:
969
+ logger.error(f"Failed to dump default cube: {e}")
970
+ time_end = time.time()
971
+ logger.info(f"time user_register: dump default cube time is: {time_end - time_start}")
972
+ # Register the default cube with MOS
973
+ self.register_mem_cube(
974
+ mem_cube_name_or_path_or_object=default_mem_cube,
975
+ mem_cube_id=default_cube_id,
976
+ user_id=user_id,
977
+ memory_types=["act_mem"] if self.config.enable_activation_memory else [],
978
+ default_config=default_cube_config, # use default cube config
979
+ )
980
+
981
+ # Add interests to the default cube if provided
982
+ if interests:
983
+ self.add(memory_content=interests, mem_cube_id=default_cube_id, user_id=user_id)
984
+
985
+ return {
986
+ "status": "success",
987
+ "message": f"User {user_name} registered successfully with default cube {default_cube_id}",
988
+ "user_id": user_id,
989
+ "default_cube_id": default_cube_id,
990
+ }
991
+
992
+ except Exception as e:
993
+ return {"status": "error", "message": f"Failed to register user: {e!s}"}
994
+
995
+ def _get_further_suggestion(self, message: MessageList | None = None) -> list[str]:
996
+ """Get further suggestion prompt."""
997
+ try:
998
+ dialogue_info = "\n".join([f"{msg['role']}: {msg['content']}" for msg in message[-2:]])
999
+ further_suggestion_prompt = FURTHER_SUGGESTION_PROMPT.format(dialogue=dialogue_info)
1000
+ message_list = [{"role": "system", "content": further_suggestion_prompt}]
1001
+ response = self.chat_llm.generate(message_list)
1002
+ clean_response = clean_json_response(response)
1003
+ response_json = json.loads(clean_response)
1004
+ return response_json["query"]
1005
+ except Exception as e:
1006
+ logger.error(f"Error getting further suggestion: {e}", exc_info=True)
1007
+ return []
1008
+
1009
+ def get_suggestion_query(
1010
+ self, user_id: str, language: str = "zh", message: MessageList | None = None
1011
+ ) -> list[str]:
1012
+ """Get suggestion query from LLM.
1013
+ Args:
1014
+ user_id (str): User ID.
1015
+ language (str): Language for suggestions ("zh" or "en").
1016
+
1017
+ Returns:
1018
+ list[str]: The suggestion query list.
1019
+ """
1020
+ if message:
1021
+ further_suggestion = self._get_further_suggestion(message)
1022
+ return further_suggestion
1023
+ if language == "zh":
1024
+ suggestion_prompt = SUGGESTION_QUERY_PROMPT_ZH
1025
+ else: # English
1026
+ suggestion_prompt = SUGGESTION_QUERY_PROMPT_EN
1027
+ text_mem_result = super().search("my recently memories", user_id=user_id, top_k=3)[
1028
+ "text_mem"
1029
+ ]
1030
+ if text_mem_result:
1031
+ memories = "\n".join([m.memory[:200] for m in text_mem_result[0]["memories"]])
1032
+ else:
1033
+ memories = ""
1034
+ message_list = [{"role": "system", "content": suggestion_prompt.format(memories=memories)}]
1035
+ response = self.chat_llm.generate(message_list)
1036
+ clean_response = clean_json_response(response)
1037
+ response_json = json.loads(clean_response)
1038
+ return response_json["query"]
1039
+
1040
+ def chat(
1041
+ self,
1042
+ query: str,
1043
+ user_id: str,
1044
+ cube_id: str | None = None,
1045
+ history: MessageList | None = None,
1046
+ base_prompt: str | None = None,
1047
+ internet_search: bool = False,
1048
+ moscube: bool = False,
1049
+ top_k: int = 10,
1050
+ threshold: float = 0.5,
1051
+ session_id: str | None = None,
1052
+ ) -> str:
1053
+ """
1054
+ Chat with LLM with memory references and complete response.
1055
+ """
1056
+ self._load_user_cubes(user_id, self.default_cube_config)
1057
+ time_start = time.time()
1058
+ memories_result = super().search(
1059
+ query,
1060
+ user_id,
1061
+ install_cube_ids=[cube_id] if cube_id else None,
1062
+ top_k=top_k,
1063
+ mode="fine",
1064
+ internet_search=internet_search,
1065
+ moscube=moscube,
1066
+ session_id=session_id,
1067
+ )["text_mem"]
1068
+
1069
+ memories_list = []
1070
+ if memories_result:
1071
+ memories_list = memories_result[0]["memories"]
1072
+ memories_list = self._filter_memories_by_threshold(memories_list, threshold)
1073
+ new_memories_list = []
1074
+ for m in memories_list:
1075
+ m.metadata.embedding = []
1076
+ new_memories_list.append(m)
1077
+ memories_list = new_memories_list
1078
+
1079
+ system_prompt = super()._build_system_prompt(memories_list, base_prompt)
1080
+ if history is not None:
1081
+ # Use the provided history (even if it's empty)
1082
+ history_info = history[-20:]
1083
+ else:
1084
+ # Fall back to internal chat_history
1085
+ if user_id not in self.chat_history_manager:
1086
+ self._register_chat_history(user_id, session_id)
1087
+ history_info = self.chat_history_manager[user_id].chat_history[-20:]
1088
+ current_messages = [
1089
+ {"role": "system", "content": system_prompt},
1090
+ *history_info,
1091
+ {"role": "user", "content": query},
1092
+ ]
1093
+ logger.info("Start to get final answer...")
1094
+ response = self.chat_llm.generate(current_messages)
1095
+ time_end = time.time()
1096
+ self._start_post_chat_processing(
1097
+ user_id=user_id,
1098
+ cube_id=cube_id,
1099
+ query=query,
1100
+ full_response=response,
1101
+ system_prompt=system_prompt,
1102
+ time_start=time_start,
1103
+ time_end=time_end,
1104
+ speed_improvement=0.0,
1105
+ current_messages=current_messages,
1106
+ )
1107
+ return response, memories_list
1108
+
1109
+ def chat_with_references(
1110
+ self,
1111
+ query: str,
1112
+ user_id: str,
1113
+ cube_id: str | None = None,
1114
+ history: MessageList | None = None,
1115
+ top_k: int = 20,
1116
+ internet_search: bool = False,
1117
+ moscube: bool = False,
1118
+ session_id: str | None = None,
1119
+ ) -> Generator[str, None, None]:
1120
+ """
1121
+ Chat with LLM with memory references and streaming output.
1122
+
1123
+ Args:
1124
+ query (str): Query string.
1125
+ user_id (str): User ID.
1126
+ cube_id (str, optional): Custom cube ID for user.
1127
+ history (MessageList, optional): Chat history.
1128
+
1129
+ Returns:
1130
+ Generator[str, None, None]: The response string generator with reference processing.
1131
+ """
1132
+
1133
+ self._load_user_cubes(user_id, self.default_cube_config)
1134
+ time_start = time.time()
1135
+ memories_list = []
1136
+ yield f"data: {json.dumps({'type': 'status', 'data': '0'})}\n\n"
1137
+ memories_result = super().search(
1138
+ query,
1139
+ user_id,
1140
+ install_cube_ids=[cube_id] if cube_id else None,
1141
+ top_k=top_k,
1142
+ mode="fine",
1143
+ internet_search=internet_search,
1144
+ moscube=moscube,
1145
+ session_id=session_id,
1146
+ )["text_mem"]
1147
+
1148
+ yield f"data: {json.dumps({'type': 'status', 'data': '1'})}\n\n"
1149
+ search_time_end = time.time()
1150
+ logger.info(
1151
+ f"time chat: search text_mem time user_id: {user_id} time is: {search_time_end - time_start}"
1152
+ )
1153
+ self._send_message_to_scheduler(
1154
+ user_id=user_id, mem_cube_id=cube_id, query=query, label=QUERY_TASK_LABEL
1155
+ )
1156
+ if memories_result:
1157
+ memories_list = memories_result[0]["memories"]
1158
+ memories_list = self._filter_memories_by_threshold(memories_list)
1159
+
1160
+ reference = prepare_reference_data(memories_list)
1161
+ yield f"data: {json.dumps({'type': 'reference', 'data': reference})}\n\n"
1162
+ # Build custom system prompt with relevant memories)
1163
+ system_prompt = self._build_enhance_system_prompt(user_id, memories_list)
1164
+ # Get chat history
1165
+ if user_id not in self.chat_history_manager:
1166
+ self._register_chat_history(user_id, session_id)
1167
+
1168
+ chat_history = self.chat_history_manager[user_id]
1169
+ if history is not None:
1170
+ chat_history.chat_history = history[-20:]
1171
+ current_messages = [
1172
+ {"role": "system", "content": system_prompt},
1173
+ *chat_history.chat_history,
1174
+ {"role": "user", "content": query},
1175
+ ]
1176
+ logger.info(
1177
+ f"user_id: {user_id}, cube_id: {cube_id}, current_system_prompt: {system_prompt}"
1178
+ )
1179
+ yield f"data: {json.dumps({'type': 'status', 'data': '2'})}\n\n"
1180
+ # Generate response with custom prompt
1181
+ past_key_values = None
1182
+ response_stream = None
1183
+ if self.config.enable_activation_memory:
1184
+ # Handle activation memory (copy MOSCore logic)
1185
+ for mem_cube_id, mem_cube in self.mem_cubes.items():
1186
+ if mem_cube.act_mem and mem_cube_id == cube_id:
1187
+ kv_cache = next(iter(mem_cube.act_mem.get_all()), None)
1188
+ past_key_values = (
1189
+ kv_cache.memory if (kv_cache and hasattr(kv_cache, "memory")) else None
1190
+ )
1191
+ if past_key_values is not None:
1192
+ logger.info("past_key_values is not None will apply to chat")
1193
+ else:
1194
+ logger.info("past_key_values is None will not apply to chat")
1195
+ break
1196
+ if self.config.chat_model.backend == "huggingface":
1197
+ response_stream = self.chat_llm.generate_stream(
1198
+ current_messages, past_key_values=past_key_values
1199
+ )
1200
+ elif self.config.chat_model.backend == "vllm":
1201
+ response_stream = self.chat_llm.generate_stream(current_messages)
1202
+ else:
1203
+ if self.config.chat_model.backend in ["huggingface", "vllm", "openai"]:
1204
+ response_stream = self.chat_llm.generate_stream(current_messages)
1205
+ else:
1206
+ response_stream = self.chat_llm.generate(current_messages)
1207
+
1208
+ time_end = time.time()
1209
+ chat_time_end = time.time()
1210
+ logger.info(
1211
+ f"time chat: chat time user_id: {user_id} time is: {chat_time_end - search_time_end}"
1212
+ )
1213
+ # Simulate streaming output with proper reference handling using tiktoken
1214
+
1215
+ # Initialize buffer for streaming
1216
+ buffer = ""
1217
+ full_response = ""
1218
+ token_count = 0
1219
+ # Use tiktoken for proper token-based chunking
1220
+ if self.config.chat_model.backend not in ["huggingface", "vllm", "openai"]:
1221
+ # For non-huggingface backends, we need to collect the full response first
1222
+ full_response_text = ""
1223
+ for chunk in response_stream:
1224
+ if chunk in ["<think>", "</think>"]:
1225
+ continue
1226
+ full_response_text += chunk
1227
+ response_stream = self._chunk_response_with_tiktoken(full_response_text, chunk_size=5)
1228
+ for chunk in response_stream:
1229
+ if chunk in ["<think>", "</think>"]:
1230
+ continue
1231
+ token_count += 1
1232
+ buffer += chunk
1233
+ full_response += chunk
1234
+
1235
+ # Process buffer to ensure complete reference tags
1236
+ processed_chunk, remaining_buffer = process_streaming_references_complete(buffer)
1237
+
1238
+ if processed_chunk:
1239
+ chunk_data = f"data: {json.dumps({'type': 'text', 'data': processed_chunk}, ensure_ascii=False)}\n\n"
1240
+ yield chunk_data
1241
+ buffer = remaining_buffer
1242
+
1243
+ # Process any remaining buffer
1244
+ if buffer:
1245
+ processed_chunk, remaining_buffer = process_streaming_references_complete(buffer)
1246
+ if processed_chunk:
1247
+ chunk_data = f"data: {json.dumps({'type': 'text', 'data': processed_chunk}, ensure_ascii=False)}\n\n"
1248
+ yield chunk_data
1249
+
1250
+ # set kvcache improve speed
1251
+ speed_improvement = round(float((len(system_prompt) / 2) * 0.0048 + 44.5), 1)
1252
+ total_time = round(float(time_end - time_start), 1)
1253
+
1254
+ yield f"data: {json.dumps({'type': 'time', 'data': {'total_time': total_time, 'speed_improvement': f'{speed_improvement}%'}})}\n\n"
1255
+ # get further suggestion
1256
+ current_messages.append({"role": "assistant", "content": full_response})
1257
+ further_suggestion = self._get_further_suggestion(current_messages)
1258
+ logger.info(f"further_suggestion: {further_suggestion}")
1259
+ yield f"data: {json.dumps({'type': 'suggestion', 'data': further_suggestion})}\n\n"
1260
+ yield f"data: {json.dumps({'type': 'end'})}\n\n"
1261
+
1262
+ # Asynchronous processing of logs, notifications and memory additions
1263
+ self._start_post_chat_processing(
1264
+ user_id=user_id,
1265
+ cube_id=cube_id,
1266
+ query=query,
1267
+ full_response=full_response,
1268
+ system_prompt=system_prompt,
1269
+ time_start=time_start,
1270
+ time_end=time_end,
1271
+ speed_improvement=speed_improvement,
1272
+ current_messages=current_messages,
1273
+ )
1274
+
1275
+ def get_all(
1276
+ self,
1277
+ user_id: str,
1278
+ memory_type: Literal["text_mem", "act_mem", "param_mem", "para_mem"],
1279
+ mem_cube_ids: list[str] | None = None,
1280
+ ) -> list[dict[str, Any]]:
1281
+ """Get all memory items for a user.
1282
+
1283
+ Args:
1284
+ user_id (str): The ID of the user.
1285
+ cube_id (str | None, optional): The ID of the cube. Defaults to None.
1286
+ memory_type (Literal["text_mem", "act_mem", "param_mem"]): The type of memory to get.
1287
+
1288
+ Returns:
1289
+ list[dict[str, Any]]: A list of memory items with cube_id and memories structure.
1290
+ """
1291
+
1292
+ # Load user cubes if not already loaded
1293
+ self._load_user_cubes(user_id, self.default_cube_config)
1294
+ time_start = time.time()
1295
+ memory_list = super().get_all(
1296
+ mem_cube_id=mem_cube_ids[0] if mem_cube_ids else None, user_id=user_id
1297
+ )[memory_type]
1298
+ get_all_time_end = time.time()
1299
+ logger.info(
1300
+ f"time get_all: get_all time user_id: {user_id} time is: {get_all_time_end - time_start}"
1301
+ )
1302
+ reformat_memory_list = []
1303
+ if memory_type == "text_mem":
1304
+ for memory in memory_list:
1305
+ memories = remove_embedding_recursive(memory["memories"])
1306
+ custom_type_ratios = {
1307
+ "WorkingMemory": 0.20,
1308
+ "LongTermMemory": 0.40,
1309
+ "UserMemory": 0.40,
1310
+ }
1311
+ tree_result, node_type_count = convert_graph_to_tree_forworkmem(
1312
+ memories, target_node_count=200, type_ratios=custom_type_ratios
1313
+ )
1314
+ # Ensure all node IDs are unique in the tree structure
1315
+ tree_result = ensure_unique_tree_ids(tree_result)
1316
+ memories_filtered = filter_nodes_by_tree_ids(tree_result, memories)
1317
+ children = tree_result["children"]
1318
+ children_sort = sort_children_by_memory_type(children)
1319
+ tree_result["children"] = children_sort
1320
+ memories_filtered["tree_structure"] = tree_result
1321
+ reformat_memory_list.append(
1322
+ {
1323
+ "cube_id": memory["cube_id"],
1324
+ "memories": [memories_filtered],
1325
+ "memory_statistics": node_type_count,
1326
+ }
1327
+ )
1328
+ elif memory_type == "act_mem":
1329
+ memories_list = []
1330
+ act_mem_params = self.mem_cubes[mem_cube_ids[0]].act_mem.get_all()
1331
+ if act_mem_params:
1332
+ memories_data = act_mem_params[0].model_dump()
1333
+ records = memories_data.get("records", [])
1334
+ for record in records["text_memories"]:
1335
+ memories_list.append(
1336
+ {
1337
+ "id": memories_data["id"],
1338
+ "text": record,
1339
+ "create_time": records["timestamp"],
1340
+ "size": random.randint(1, 20),
1341
+ "modify_times": 1,
1342
+ }
1343
+ )
1344
+ reformat_memory_list.append(
1345
+ {
1346
+ "cube_id": "xxxxxxxxxxxxxxxx" if not mem_cube_ids else mem_cube_ids[0],
1347
+ "memories": memories_list,
1348
+ }
1349
+ )
1350
+ elif memory_type == "para_mem":
1351
+ act_mem_params = self.mem_cubes[mem_cube_ids[0]].act_mem.get_all()
1352
+ logger.info(f"act_mem_params: {act_mem_params}")
1353
+ reformat_memory_list.append(
1354
+ {
1355
+ "cube_id": "xxxxxxxxxxxxxxxx" if not mem_cube_ids else mem_cube_ids[0],
1356
+ "memories": act_mem_params[0].model_dump(),
1357
+ }
1358
+ )
1359
+ make_format_time_end = time.time()
1360
+ logger.info(
1361
+ f"time get_all: make_format time user_id: {user_id} time is: {make_format_time_end - get_all_time_end}"
1362
+ )
1363
+ return reformat_memory_list
1364
+
1365
+ def _get_subgraph(
1366
+ self, query: str, mem_cube_id: str, user_id: str | None = None, top_k: int = 5
1367
+ ) -> list[dict[str, Any]]:
1368
+ result = {"para_mem": [], "act_mem": [], "text_mem": []}
1369
+ if self.config.enable_textual_memory and self.mem_cubes[mem_cube_id].text_mem:
1370
+ result["text_mem"].append(
1371
+ {
1372
+ "cube_id": mem_cube_id,
1373
+ "memories": self.mem_cubes[mem_cube_id].text_mem.get_relevant_subgraph(
1374
+ query, top_k=top_k
1375
+ ),
1376
+ }
1377
+ )
1378
+ return result
1379
+
1380
+ def get_subgraph(
1381
+ self,
1382
+ user_id: str,
1383
+ query: str,
1384
+ mem_cube_ids: list[str] | None = None,
1385
+ top_k: int = 20,
1386
+ ) -> list[dict[str, Any]]:
1387
+ """Get all memory items for a user.
1388
+
1389
+ Args:
1390
+ user_id (str): The ID of the user.
1391
+ cube_id (str | None, optional): The ID of the cube. Defaults to None.
1392
+ mem_cube_ids (list[str], optional): The IDs of the cubes. Defaults to None.
1393
+
1394
+ Returns:
1395
+ list[dict[str, Any]]: A list of memory items with cube_id and memories structure.
1396
+ """
1397
+
1398
+ # Load user cubes if not already loaded
1399
+ self._load_user_cubes(user_id, self.default_cube_config)
1400
+ memory_list = self._get_subgraph(
1401
+ query=query, mem_cube_id=mem_cube_ids[0], user_id=user_id, top_k=top_k
1402
+ )["text_mem"]
1403
+ reformat_memory_list = []
1404
+ for memory in memory_list:
1405
+ memories = remove_embedding_recursive(memory["memories"])
1406
+ custom_type_ratios = {"WorkingMemory": 0.20, "LongTermMemory": 0.40, "UserMemory": 0.4}
1407
+ tree_result, node_type_count = convert_graph_to_tree_forworkmem(
1408
+ memories, target_node_count=150, type_ratios=custom_type_ratios
1409
+ )
1410
+ # Ensure all node IDs are unique in the tree structure
1411
+ tree_result = ensure_unique_tree_ids(tree_result)
1412
+ memories_filtered = filter_nodes_by_tree_ids(tree_result, memories)
1413
+ children = tree_result["children"]
1414
+ children_sort = sort_children_by_memory_type(children)
1415
+ tree_result["children"] = children_sort
1416
+ memories_filtered["tree_structure"] = tree_result
1417
+ reformat_memory_list.append(
1418
+ {
1419
+ "cube_id": memory["cube_id"],
1420
+ "memories": [memories_filtered],
1421
+ "memory_statistics": node_type_count,
1422
+ }
1423
+ )
1424
+
1425
+ return reformat_memory_list
1426
+
1427
+ def search(
1428
+ self,
1429
+ query: str,
1430
+ user_id: str,
1431
+ install_cube_ids: list[str] | None = None,
1432
+ top_k: int = 10,
1433
+ mode: Literal["fast", "fine"] = "fast",
1434
+ session_id: str | None = None,
1435
+ ):
1436
+ """Search memories for a specific user."""
1437
+
1438
+ # Load user cubes if not already loaded
1439
+ time_start = time.time()
1440
+ self._load_user_cubes(user_id, self.default_cube_config)
1441
+ load_user_cubes_time_end = time.time()
1442
+ logger.info(
1443
+ f"time search: load_user_cubes time user_id: {user_id} time is: {load_user_cubes_time_end - time_start}"
1444
+ )
1445
+ search_result = super().search(
1446
+ query, user_id, install_cube_ids, top_k, mode=mode, session_id=session_id
1447
+ )
1448
+ search_time_end = time.time()
1449
+ logger.info(
1450
+ f"time search: search text_mem time user_id: {user_id} time is: {search_time_end - load_user_cubes_time_end}"
1451
+ )
1452
+ text_memory_list = search_result["text_mem"]
1453
+ reformat_memory_list = []
1454
+ for memory in text_memory_list:
1455
+ memories_list = []
1456
+ for data in memory["memories"]:
1457
+ memories = data.model_dump()
1458
+ memories["ref_id"] = f"[{memories['id'].split('-')[0]}]"
1459
+ memories["metadata"]["embedding"] = []
1460
+ memories["metadata"]["sources"] = []
1461
+ memories["metadata"]["ref_id"] = f"[{memories['id'].split('-')[0]}]"
1462
+ memories["metadata"]["id"] = memories["id"]
1463
+ memories["metadata"]["memory"] = memories["memory"]
1464
+ memories_list.append(memories)
1465
+ reformat_memory_list.append({"cube_id": memory["cube_id"], "memories": memories_list})
1466
+ logger.info(f"search memory list is : {reformat_memory_list}")
1467
+ search_result["text_mem"] = reformat_memory_list
1468
+
1469
+ pref_memory_list = search_result["pref_mem"]
1470
+ reformat_pref_memory_list = []
1471
+ for memory in pref_memory_list:
1472
+ memories_list = []
1473
+ for data in memory["memories"]:
1474
+ memories = data.model_dump()
1475
+ memories["ref_id"] = f"[{memories['id'].split('-')[0]}]"
1476
+ memories["metadata"]["embedding"] = []
1477
+ memories["metadata"]["sources"] = []
1478
+ memories["metadata"]["ref_id"] = f"[{memories['id'].split('-')[0]}]"
1479
+ memories["metadata"]["id"] = memories["id"]
1480
+ memories["metadata"]["memory"] = memories["memory"]
1481
+ memories_list.append(memories)
1482
+ reformat_pref_memory_list.append(
1483
+ {"cube_id": memory["cube_id"], "memories": memories_list}
1484
+ )
1485
+ search_result["pref_mem"] = reformat_pref_memory_list
1486
+ time_end = time.time()
1487
+ logger.info(
1488
+ f"time search: total time for user_id: {user_id} time is: {time_end - time_start}"
1489
+ )
1490
+ return search_result
1491
+
1492
+ def add(
1493
+ self,
1494
+ user_id: str,
1495
+ messages: MessageList | None = None,
1496
+ memory_content: str | None = None,
1497
+ doc_path: str | None = None,
1498
+ mem_cube_id: str | None = None,
1499
+ source: str | None = None,
1500
+ user_profile: bool = False,
1501
+ session_id: str | None = None,
1502
+ task_id: str | None = None, # Add task_id parameter
1503
+ ):
1504
+ """Add memory for a specific user."""
1505
+
1506
+ # Load user cubes if not already loaded
1507
+ self._load_user_cubes(user_id, self.default_cube_config)
1508
+ result = super().add(
1509
+ messages,
1510
+ memory_content,
1511
+ doc_path,
1512
+ mem_cube_id,
1513
+ user_id,
1514
+ session_id=session_id,
1515
+ task_id=task_id,
1516
+ )
1517
+ if user_profile:
1518
+ try:
1519
+ user_interests = memory_content.split("'userInterests': '")[1].split("', '")[0]
1520
+ user_interests = user_interests.replace(",", " ")
1521
+ user_profile_memories = self.mem_cubes[
1522
+ mem_cube_id
1523
+ ].text_mem.internet_retriever.retrieve_from_internet(query=user_interests, top_k=5)
1524
+ for memory in user_profile_memories:
1525
+ self.mem_cubes[mem_cube_id].text_mem.add(memory)
1526
+ except Exception as e:
1527
+ logger.error(
1528
+ f"Failed to retrieve user profile: {e}, memory_content: {memory_content}"
1529
+ )
1530
+
1531
+ return result
1532
+
1533
+ def list_users(self) -> list:
1534
+ """List all registered users."""
1535
+ return self.user_manager.list_users()
1536
+
1537
+ def get_user_info(self, user_id: str) -> dict:
1538
+ """Get user information including accessible cubes."""
1539
+ # Use MOSCore's built-in user validation
1540
+ # Validate user access
1541
+ self._validate_user_access(user_id)
1542
+
1543
+ result = super().get_user_info()
1544
+
1545
+ return result
1546
+
1547
+ def share_cube_with_user(self, cube_id: str, owner_user_id: str, target_user_id: str) -> bool:
1548
+ """Share a cube with another user."""
1549
+ # Use MOSCore's built-in cube access validation
1550
+ self._validate_cube_access(owner_user_id, cube_id)
1551
+
1552
+ result = super().share_cube_with_user(cube_id, target_user_id)
1553
+
1554
+ return result
1555
+
1556
+ def clear_user_chat_history(self, user_id: str) -> None:
1557
+ """Clear chat history for a specific user."""
1558
+ # Validate user access
1559
+ self._validate_user_access(user_id)
1560
+
1561
+ super().clear_messages(user_id)
1562
+
1563
+ def update_user_config(self, user_id: str, config: MOSConfig) -> bool:
1564
+ """Update user configuration.
1565
+
1566
+ Args:
1567
+ user_id (str): The user ID.
1568
+ config (MOSConfig): The new configuration.
1569
+
1570
+ Returns:
1571
+ bool: True if successful, False otherwise.
1572
+ """
1573
+ try:
1574
+ # Save to persistent storage
1575
+ success = self.user_manager.save_user_config(user_id, config)
1576
+ if success:
1577
+ # Update in-memory config
1578
+ self.user_configs[user_id] = config
1579
+ logger.info(f"Updated configuration for user {user_id}")
1580
+
1581
+ return success
1582
+ except Exception as e:
1583
+ logger.error(f"Failed to update user config for {user_id}: {e}")
1584
+ return False
1585
+
1586
+ def get_user_config(self, user_id: str) -> MOSConfig | None:
1587
+ """Get user configuration.
1588
+
1589
+ Args:
1590
+ user_id (str): The user ID.
1591
+
1592
+ Returns:
1593
+ MOSConfig | None: The user's configuration or None if not found.
1594
+ """
1595
+ return self.user_manager.get_user_config(user_id)
1596
+
1597
+ def get_active_user_count(self) -> int:
1598
+ """Get the number of active user configurations in memory."""
1599
+ return len(self.user_configs)
1600
+
1601
+ def get_user_instance_info(self) -> dict[str, Any]:
1602
+ """Get information about user configurations in memory."""
1603
+ return {
1604
+ "active_instances": len(self.user_configs),
1605
+ "max_instances": self.max_user_instances,
1606
+ "user_ids": list(self.user_configs.keys()),
1607
+ "lru_order": list(self.user_configs.keys()), # OrderedDict maintains insertion order
1608
+ }