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,518 @@
1
+ import re
2
+ import traceback
3
+ import uuid
4
+
5
+ from concurrent.futures import as_completed
6
+ from datetime import datetime
7
+
8
+ from memos.context.context import ContextThreadPoolExecutor
9
+ from memos.embedders.factory import OllamaEmbedder
10
+ from memos.graph_dbs.neo4j import Neo4jGraphDB
11
+ from memos.llms.factory import AzureLLM, OllamaLLM, OpenAILLM
12
+ from memos.log import get_logger
13
+ from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
14
+ from memos.memories.textual.tree_text_memory.organize.reorganizer import (
15
+ GraphStructureReorganizer,
16
+ QueueMessage,
17
+ )
18
+
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ def extract_working_binding_ids(mem_items: list[TextualMemoryItem]) -> set[str]:
24
+ """
25
+ Scan enhanced memory items for background hints like
26
+ "[working_binding:<uuid>]" and collect those working memory IDs.
27
+
28
+ We store the working<->long binding inside metadata.background when
29
+ initially adding memories in async mode, so we can later clean up
30
+ the temporary WorkingMemory nodes after mem_reader produces the
31
+ final LongTermMemory/UserMemory.
32
+
33
+ Args:
34
+ mem_items: list of TextualMemoryItem we just added (enhanced memories)
35
+
36
+ Returns:
37
+ A set of working memory IDs (as strings) that should be deleted.
38
+ """
39
+ bindings: set[str] = set()
40
+ pattern = re.compile(r"\[working_binding:([0-9a-fA-F-]{36})\]")
41
+ for item in mem_items:
42
+ try:
43
+ bg = getattr(item.metadata, "background", "") or ""
44
+ except Exception:
45
+ bg = ""
46
+ if not isinstance(bg, str):
47
+ continue
48
+ match = pattern.search(bg)
49
+ if match:
50
+ bindings.add(match.group(1))
51
+ return bindings
52
+
53
+
54
+ class MemoryManager:
55
+ def __init__(
56
+ self,
57
+ graph_store: Neo4jGraphDB,
58
+ embedder: OllamaEmbedder,
59
+ llm: OpenAILLM | OllamaLLM | AzureLLM,
60
+ memory_size: dict | None = None,
61
+ threshold: float | None = 0.80,
62
+ merged_threshold: float | None = 0.92,
63
+ is_reorganize: bool = False,
64
+ ):
65
+ self.graph_store = graph_store
66
+ self.embedder = embedder
67
+ self.memory_size = memory_size
68
+ self.current_memory_size = {
69
+ "WorkingMemory": 0,
70
+ "LongTermMemory": 0,
71
+ "UserMemory": 0,
72
+ }
73
+ if not memory_size:
74
+ self.memory_size = {
75
+ "WorkingMemory": 20,
76
+ "LongTermMemory": 1500,
77
+ "UserMemory": 480,
78
+ }
79
+ logger.info(f"MemorySize is {self.memory_size}")
80
+ self._threshold = threshold
81
+ self.is_reorganize = is_reorganize
82
+ self.reorganizer = GraphStructureReorganizer(
83
+ graph_store, llm, embedder, is_reorganize=is_reorganize
84
+ )
85
+ self._merged_threshold = merged_threshold
86
+
87
+ def add(
88
+ self,
89
+ memories: list[TextualMemoryItem],
90
+ user_name: str | None = None,
91
+ mode: str = "sync",
92
+ use_batch: bool = True,
93
+ ) -> list[str]:
94
+ """
95
+ Add new memories to different memory types.
96
+
97
+ Args:
98
+ memories: List of memory items to add.
99
+ user_name: Optional user name for the memories.
100
+ mode: "sync" to cleanup and refresh after adding, "async" to skip.
101
+ use_batch: If True, use batch database operations (more efficient for large batches).
102
+ If False, use parallel single-node operations (original behavior).
103
+
104
+ Returns:
105
+ List of added memory IDs.
106
+ """
107
+ added_ids: list[str] = []
108
+ if use_batch:
109
+ added_ids = self._add_memories_batch(memories, user_name)
110
+ else:
111
+ added_ids = self._add_memories_parallel(memories, user_name)
112
+
113
+ if mode == "sync":
114
+ self._cleanup_working_memory(user_name)
115
+ self._refresh_memory_size(user_name=user_name)
116
+
117
+ return added_ids
118
+
119
+ def _add_memories_parallel(
120
+ self, memories: list[TextualMemoryItem], user_name: str | None = None
121
+ ) -> list[str]:
122
+ """
123
+ Add memories using parallel single-node operations (original behavior).
124
+ """
125
+ added_ids: list[str] = []
126
+ with ContextThreadPoolExecutor(max_workers=10) as executor:
127
+ futures = {executor.submit(self._process_memory, m, user_name): m for m in memories}
128
+ for future in as_completed(futures, timeout=500):
129
+ try:
130
+ ids = future.result()
131
+ added_ids.extend(ids)
132
+ except Exception as e:
133
+ logger.exception("Memory processing error: ", exc_info=e)
134
+ logger.info(f"[MemoryManager: _add_memories_parallel] Added {len(added_ids)} memories")
135
+ return added_ids
136
+
137
+ def _add_memories_batch(
138
+ self, memories: list[TextualMemoryItem], user_name: str | None = None, batch_size: int = 5
139
+ ) -> list[str]:
140
+ """
141
+ Add memories using batch database operations (more efficient for large batches).
142
+
143
+ Args:
144
+ memories: List of memory items to add.
145
+ user_name: Optional user name for the memories.
146
+ batch_size: Number of nodes to insert per batch.
147
+
148
+ Returns:
149
+ List of added graph memory node IDs.
150
+ """
151
+ if not memories:
152
+ return []
153
+
154
+ added_ids: list[str] = []
155
+ working_nodes: list[dict] = []
156
+ graph_nodes: list[dict] = []
157
+ graph_node_ids: list[str] = []
158
+
159
+ for memory in memories:
160
+ working_id = str(uuid.uuid4())
161
+
162
+ if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"):
163
+ working_metadata = memory.metadata.model_copy(
164
+ update={"memory_type": "WorkingMemory"}
165
+ ).model_dump(exclude_none=True)
166
+ working_metadata["updated_at"] = datetime.now().isoformat()
167
+ working_nodes.append(
168
+ {
169
+ "id": working_id,
170
+ "memory": memory.memory,
171
+ "metadata": working_metadata,
172
+ }
173
+ )
174
+ if memory.metadata.memory_type in (
175
+ "LongTermMemory",
176
+ "UserMemory",
177
+ "ToolSchemaMemory",
178
+ "ToolTrajectoryMemory",
179
+ ):
180
+ graph_node_id = str(uuid.uuid4())
181
+ metadata_dict = memory.metadata.model_dump(exclude_none=True)
182
+ metadata_dict["updated_at"] = datetime.now().isoformat()
183
+
184
+ # Add working_binding for fast mode
185
+ tags = metadata_dict.get("tags") or []
186
+ if "mode:fast" in tags:
187
+ prev_bg = metadata_dict.get("background", "") or ""
188
+ binding_line = f"[working_binding:{working_id}] direct built from raw inputs"
189
+ metadata_dict["background"] = (
190
+ f"{prev_bg} || {binding_line}" if prev_bg else binding_line
191
+ )
192
+
193
+ graph_nodes.append(
194
+ {
195
+ "id": graph_node_id,
196
+ "memory": memory.memory,
197
+ "metadata": metadata_dict,
198
+ }
199
+ )
200
+ graph_node_ids.append(graph_node_id)
201
+ added_ids.append(graph_node_id)
202
+
203
+ def _submit_batches(nodes: list[dict], node_kind: str) -> None:
204
+ if not nodes:
205
+ return
206
+
207
+ max_workers = min(8, max(1, len(nodes) // max(1, batch_size)))
208
+ with ContextThreadPoolExecutor(max_workers=max_workers) as executor:
209
+ futures: list[tuple[int, int, object]] = []
210
+ for batch_index, i in enumerate(range(0, len(nodes), batch_size), start=1):
211
+ batch = nodes[i : i + batch_size]
212
+ fut = executor.submit(
213
+ self.graph_store.add_nodes_batch, batch, user_name=user_name
214
+ )
215
+ futures.append((batch_index, len(batch), fut))
216
+
217
+ for idx, size, fut in futures:
218
+ try:
219
+ fut.result()
220
+ except Exception as e:
221
+ logger.exception(
222
+ f"Batch add {node_kind} nodes error (batch {idx}, size {size}): ",
223
+ exc_info=e,
224
+ )
225
+
226
+ _submit_batches(working_nodes, "WorkingMemory")
227
+ _submit_batches(graph_nodes, "graph memory")
228
+
229
+ if graph_node_ids and self.is_reorganize:
230
+ self.reorganizer.add_message(QueueMessage(op="add", after_node=graph_node_ids))
231
+
232
+ return added_ids
233
+
234
+ def _cleanup_working_memory(self, user_name: str | None = None) -> None:
235
+ """
236
+ Remove oldest WorkingMemory nodes to keep within size limit.
237
+ """
238
+ try:
239
+ self.graph_store.remove_oldest_memory(
240
+ memory_type="WorkingMemory",
241
+ keep_latest=self.memory_size["WorkingMemory"],
242
+ user_name=user_name,
243
+ )
244
+ except Exception:
245
+ logger.warning(f"Remove WorkingMemory error: {traceback.format_exc()}")
246
+
247
+ def replace_working_memory(
248
+ self, memories: list[TextualMemoryItem], user_name: str | None = None
249
+ ) -> None:
250
+ """
251
+ Replace WorkingMemory
252
+ """
253
+ working_memory_top_k = memories[: self.memory_size["WorkingMemory"]]
254
+ with ContextThreadPoolExecutor(max_workers=8) as executor:
255
+ futures = [
256
+ executor.submit(
257
+ self._add_memory_to_db, memory, "WorkingMemory", user_name=user_name
258
+ )
259
+ for memory in working_memory_top_k
260
+ ]
261
+ for future in as_completed(futures, timeout=60):
262
+ try:
263
+ future.result()
264
+ except Exception as e:
265
+ logger.exception("Memory processing error: ", exc_info=e)
266
+
267
+ self.graph_store.remove_oldest_memory(
268
+ memory_type="WorkingMemory",
269
+ keep_latest=self.memory_size["WorkingMemory"],
270
+ user_name=user_name,
271
+ )
272
+ self._refresh_memory_size(user_name=user_name)
273
+
274
+ def get_current_memory_size(self, user_name: str | None = None) -> dict[str, int]:
275
+ """
276
+ Return the cached memory type counts.
277
+ """
278
+ self._refresh_memory_size(user_name=user_name)
279
+ return self.current_memory_size
280
+
281
+ def _refresh_memory_size(self, user_name: str | None = None) -> None:
282
+ """
283
+ Query the latest counts from the graph store and update internal state.
284
+ """
285
+ results = self.graph_store.get_grouped_counts(
286
+ group_fields=["memory_type"], user_name=user_name
287
+ )
288
+ self.current_memory_size = {
289
+ record["memory_type"]: int(record["count"]) for record in results
290
+ }
291
+ logger.info(f"[MemoryManager] Refreshed memory sizes: {self.current_memory_size}")
292
+
293
+ def _process_memory(self, memory: TextualMemoryItem, user_name: str | None = None):
294
+ """
295
+ Process and add memory to different memory types.
296
+
297
+ Behavior:
298
+ 1. Always create a WorkingMemory node from `memory` and get its node id.
299
+ 2. If `memory.metadata.memory_type` is "LongTermMemory" or "UserMemory",
300
+ also create a corresponding long/user node.
301
+ - In async mode, that long/user node's metadata will include
302
+ `working_binding` in `background` which records the WorkingMemory
303
+ node id created in step 1.
304
+ 3. Return ONLY the ids of the long/user nodes (NOT the working node id),
305
+ which preserves the previous external contract of `add()`.
306
+ """
307
+ ids: list[str] = []
308
+ futures = []
309
+
310
+ working_id = str(uuid.uuid4())
311
+
312
+ with ContextThreadPoolExecutor(max_workers=2, thread_name_prefix="mem") as ex:
313
+ if memory.metadata.memory_type not in ("ToolSchemaMemory", "ToolTrajectoryMemory"):
314
+ f_working = ex.submit(
315
+ self._add_memory_to_db, memory, "WorkingMemory", user_name, working_id
316
+ )
317
+ futures.append(("working", f_working))
318
+
319
+ if memory.metadata.memory_type in (
320
+ "LongTermMemory",
321
+ "UserMemory",
322
+ "ToolSchemaMemory",
323
+ "ToolTrajectoryMemory",
324
+ ):
325
+ f_graph = ex.submit(
326
+ self._add_to_graph_memory,
327
+ memory=memory,
328
+ memory_type=memory.metadata.memory_type,
329
+ user_name=user_name,
330
+ working_binding=working_id,
331
+ )
332
+ futures.append(("long", f_graph))
333
+
334
+ for kind, fut in futures:
335
+ try:
336
+ res = fut.result()
337
+ if kind != "working" and isinstance(res, str) and res:
338
+ ids.append(res)
339
+ except Exception:
340
+ logger.warning("Parallel memory processing failed:\n%s", traceback.format_exc())
341
+
342
+ return ids
343
+
344
+ def _add_memory_to_db(
345
+ self,
346
+ memory: TextualMemoryItem,
347
+ memory_type: str,
348
+ user_name: str | None = None,
349
+ forced_id: str | None = None,
350
+ ) -> str:
351
+ """
352
+ Add a single memory item to the graph store, with FIFO logic for WorkingMemory.
353
+ If forced_id is provided, use that as the node id.
354
+ """
355
+ metadata = memory.metadata.model_copy(update={"memory_type": memory_type}).model_dump(
356
+ exclude_none=True
357
+ )
358
+ metadata["updated_at"] = datetime.now().isoformat()
359
+ node_id = forced_id or str(uuid.uuid4())
360
+ working_memory = TextualMemoryItem(id=node_id, memory=memory.memory, metadata=metadata)
361
+ # Insert node into graph
362
+ self.graph_store.add_node(working_memory.id, working_memory.memory, metadata, user_name)
363
+ return node_id
364
+
365
+ def _add_to_graph_memory(
366
+ self,
367
+ memory: TextualMemoryItem,
368
+ memory_type: str,
369
+ user_name: str | None = None,
370
+ working_binding: str | None = None,
371
+ ):
372
+ """
373
+ Generalized method to add memory to a graph-based memory type (e.g., LongTermMemory, UserMemory).
374
+ """
375
+ node_id = str(uuid.uuid4())
376
+ # Step 2: Add new node to graph
377
+ metadata_dict = memory.metadata.model_dump(exclude_none=True)
378
+ tags = metadata_dict.get("tags") or []
379
+ if working_binding and ("mode:fast" in tags):
380
+ prev_bg = metadata_dict.get("background", "") or ""
381
+ binding_line = f"[working_binding:{working_binding}] direct built from raw inputs"
382
+ if prev_bg:
383
+ metadata_dict["background"] = prev_bg + " || " + binding_line
384
+ else:
385
+ metadata_dict["background"] = binding_line
386
+ self.graph_store.add_node(
387
+ node_id,
388
+ memory.memory,
389
+ metadata_dict,
390
+ user_name=user_name,
391
+ )
392
+ self.reorganizer.add_message(
393
+ QueueMessage(
394
+ op="add",
395
+ after_node=[node_id],
396
+ )
397
+ )
398
+ return node_id
399
+
400
+ def _inherit_edges(self, from_id: str, to_id: str) -> None:
401
+ """
402
+ Migrate all non-lineage edges from `from_id` to `to_id`,
403
+ and remove them from `from_id` after copying.
404
+ """
405
+ edges = self.graph_store.get_edges(from_id, type="ANY", direction="ANY")
406
+
407
+ for edge in edges:
408
+ if edge["type"] == "MERGED_TO":
409
+ continue # Keep lineage edges
410
+
411
+ new_from = to_id if edge["from"] == from_id else edge["from"]
412
+ new_to = to_id if edge["to"] == from_id else edge["to"]
413
+
414
+ if new_from == new_to:
415
+ continue
416
+
417
+ # Add edge to merged node if it doesn't already exist
418
+ if not self.graph_store.edge_exists(new_from, new_to, edge["type"], direction="ANY"):
419
+ self.graph_store.add_edge(new_from, new_to, edge["type"])
420
+
421
+ # Remove original edge if it involved the archived node
422
+ self.graph_store.delete_edge(edge["from"], edge["to"], edge["type"])
423
+
424
+ def _ensure_structure_path(
425
+ self, memory_type: str, metadata: TreeNodeTextualMemoryMetadata
426
+ ) -> str:
427
+ """
428
+ Ensure structural path exists (ROOT → ... → final node), return last node ID.
429
+
430
+ Args:
431
+ path: like ["hobby", "photography"]
432
+
433
+ Returns:
434
+ Final node ID of the structure path.
435
+ """
436
+ # Step 1: Try to find an existing memory node with content == tag
437
+ existing = self.graph_store.get_by_metadata(
438
+ [
439
+ {"field": "memory", "op": "=", "value": metadata.key},
440
+ {"field": "memory_type", "op": "=", "value": memory_type},
441
+ ]
442
+ )
443
+ if existing:
444
+ node_id = existing[0] # Use the first match
445
+ else:
446
+ # Step 2: If not found, create a new structure node
447
+ new_node = TextualMemoryItem(
448
+ memory=metadata.key,
449
+ metadata=TreeNodeTextualMemoryMetadata(
450
+ user_id=metadata.user_id,
451
+ session_id=metadata.session_id,
452
+ memory_type=memory_type,
453
+ status="activated",
454
+ tags=[],
455
+ key=metadata.key,
456
+ embedding=self.embedder.embed([metadata.key])[0],
457
+ usage=[],
458
+ sources=[],
459
+ confidence=0.99,
460
+ background="",
461
+ ),
462
+ )
463
+ self.graph_store.add_node(
464
+ id=new_node.id,
465
+ memory=new_node.memory,
466
+ metadata=new_node.metadata.model_dump(exclude_none=True),
467
+ )
468
+ self.reorganizer.add_message(
469
+ QueueMessage(
470
+ op="add",
471
+ after_node=[new_node.id],
472
+ )
473
+ )
474
+
475
+ node_id = new_node.id
476
+
477
+ # Step 3: Return this structure node ID as the parent_id
478
+ return node_id
479
+
480
+ def remove_and_refresh_memory(self, user_name: str | None = None):
481
+ self._cleanup_memories_if_needed(user_name=user_name)
482
+ self._refresh_memory_size(user_name=user_name)
483
+
484
+ def _cleanup_memories_if_needed(self, user_name: str | None = None) -> None:
485
+ """
486
+ Only clean up memories if we're close to or over the limit.
487
+ This reduces unnecessary database operations.
488
+ """
489
+ cleanup_threshold = 0.8 # Clean up when 80% full
490
+
491
+ logger.info(f"self.memory_size: {self.memory_size}")
492
+ for memory_type, limit in self.memory_size.items():
493
+ current_count = self.current_memory_size.get(memory_type, 0)
494
+ threshold = int(int(limit) * cleanup_threshold)
495
+
496
+ # Only clean up if we're at or above the threshold
497
+ if current_count >= threshold:
498
+ try:
499
+ self.graph_store.remove_oldest_memory(
500
+ memory_type=memory_type, keep_latest=limit, user_name=user_name
501
+ )
502
+ logger.debug(f"Cleaned up {memory_type}: {current_count} -> {limit}")
503
+ except Exception:
504
+ logger.warning(f"Remove {memory_type} error: {traceback.format_exc()}")
505
+
506
+ def wait_reorganizer(self):
507
+ """
508
+ Wait for the reorganizer to finish processing all messages.
509
+ """
510
+ logger.debug("Waiting for reorganizer to finish processing messages...")
511
+ self.reorganizer.wait_until_current_task_done()
512
+
513
+ def close(self):
514
+ self.wait_reorganizer()
515
+ self.reorganizer.stop()
516
+
517
+ def __del__(self):
518
+ self.close()