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,965 @@
1
+ import concurrent.futures
2
+ import json
3
+ import re
4
+ import traceback
5
+
6
+ from typing import Any
7
+
8
+ from memos import log
9
+ from memos.configs.mem_reader import MultiModalStructMemReaderConfig
10
+ from memos.context.context import ContextThreadPoolExecutor
11
+ from memos.mem_reader.read_multi_modal import MultiModalParser, detect_lang
12
+ from memos.mem_reader.read_multi_modal.base import _derive_key
13
+ from memos.mem_reader.simple_struct import PROMPT_DICT, SimpleStructMemReader
14
+ from memos.mem_reader.utils import parse_json_result
15
+ from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
16
+ from memos.templates.mem_reader_prompts import MEMORY_MERGE_PROMPT_EN, MEMORY_MERGE_PROMPT_ZH
17
+ from memos.templates.tool_mem_prompts import TOOL_TRAJECTORY_PROMPT_EN, TOOL_TRAJECTORY_PROMPT_ZH
18
+ from memos.types import MessagesType
19
+ from memos.utils import timed
20
+
21
+
22
+ logger = log.get_logger(__name__)
23
+
24
+
25
+ class MultiModalStructMemReader(SimpleStructMemReader):
26
+ """Multimodal implementation of MemReader that inherits from
27
+ SimpleStructMemReader."""
28
+
29
+ def __init__(self, config: MultiModalStructMemReaderConfig):
30
+ """
31
+ Initialize the MultiModalStructMemReader with configuration.
32
+
33
+ Args:
34
+ config: Configuration object for the reader
35
+ """
36
+ from memos.configs.mem_reader import SimpleStructMemReaderConfig
37
+
38
+ # Extract direct_markdown_hostnames before converting to SimpleStructMemReaderConfig
39
+ direct_markdown_hostnames = getattr(config, "direct_markdown_hostnames", None)
40
+
41
+ # Create config_dict excluding direct_markdown_hostnames for SimpleStructMemReaderConfig
42
+ config_dict = config.model_dump(exclude_none=True)
43
+ config_dict.pop("direct_markdown_hostnames", None)
44
+
45
+ simple_config = SimpleStructMemReaderConfig(**config_dict)
46
+ super().__init__(simple_config)
47
+
48
+ # Initialize MultiModalParser for routing to different parsers
49
+ self.multi_modal_parser = MultiModalParser(
50
+ embedder=self.embedder,
51
+ llm=self.llm,
52
+ parser=None,
53
+ direct_markdown_hostnames=direct_markdown_hostnames,
54
+ )
55
+
56
+ def _split_large_memory_item(
57
+ self, item: TextualMemoryItem, max_tokens: int
58
+ ) -> list[TextualMemoryItem]:
59
+ """
60
+ Split a single memory item that exceeds max_tokens into multiple chunks.
61
+
62
+ Args:
63
+ item: TextualMemoryItem to split
64
+ max_tokens: Maximum tokens per chunk
65
+
66
+ Returns:
67
+ List of TextualMemoryItem chunks
68
+ """
69
+ item_text = item.memory or ""
70
+ if not item_text:
71
+ return [item]
72
+
73
+ item_tokens = self._count_tokens(item_text)
74
+ if item_tokens <= max_tokens:
75
+ return [item]
76
+
77
+ # Use chunker to split the text
78
+ try:
79
+ chunks = self.chunker.chunk(item_text)
80
+ split_items = []
81
+
82
+ for chunk in chunks:
83
+ # Chunk objects have a 'text' attribute
84
+ chunk_text = chunk.text
85
+ if not chunk_text or not chunk_text.strip():
86
+ continue
87
+
88
+ # Create a new memory item for each chunk, preserving original metadata
89
+ split_item = self._make_memory_item(
90
+ value=chunk_text,
91
+ info={
92
+ "user_id": item.metadata.user_id,
93
+ "session_id": item.metadata.session_id,
94
+ **(item.metadata.info or {}),
95
+ },
96
+ memory_type=item.metadata.memory_type,
97
+ tags=item.metadata.tags or [],
98
+ key=item.metadata.key,
99
+ sources=item.metadata.sources or [],
100
+ background=item.metadata.background or "",
101
+ )
102
+ split_items.append(split_item)
103
+
104
+ return split_items if split_items else [item]
105
+ except Exception as e:
106
+ logger.warning(
107
+ f"[MultiModalStruct] Failed to split large memory item: {e}. Returning original item."
108
+ )
109
+ return [item]
110
+
111
+ def _concat_multi_modal_memories(
112
+ self, all_memory_items: list[TextualMemoryItem], max_tokens=None, overlap=200
113
+ ) -> list[TextualMemoryItem]:
114
+ """
115
+ Aggregates memory items using sliding window logic similar to
116
+ `_iter_chat_windows` in simple_struct:
117
+ 1. Groups items into windows based on token count (max_tokens)
118
+ 2. Each window has overlap tokens for context continuity
119
+ 3. Aggregates items within each window into a single memory item
120
+ 4. Determines memory_type based on roles in each window
121
+ 5. Splits single large memory items that exceed max_tokens
122
+ """
123
+ if not all_memory_items:
124
+ return []
125
+
126
+ max_tokens = max_tokens or self.chat_window_max_tokens
127
+
128
+ # Split large memory items before processing
129
+ processed_items = []
130
+ for item in all_memory_items:
131
+ item_text = item.memory or ""
132
+ item_tokens = self._count_tokens(item_text)
133
+ if item_tokens > max_tokens:
134
+ # Split the large item into multiple chunks
135
+ split_items = self._split_large_memory_item(item, max_tokens)
136
+ processed_items.extend(split_items)
137
+ else:
138
+ processed_items.append(item)
139
+
140
+ # If only one item after processing, return as-is
141
+ if len(processed_items) == 1:
142
+ return processed_items
143
+
144
+ windows = []
145
+ buf_items = []
146
+ cur_text = ""
147
+
148
+ # Extract info from first item (all items should have same user_id, session_id)
149
+ first_item = processed_items[0]
150
+ info = {
151
+ "user_id": first_item.metadata.user_id,
152
+ "session_id": first_item.metadata.session_id,
153
+ **(first_item.metadata.info or {}),
154
+ }
155
+
156
+ for _idx, item in enumerate(processed_items):
157
+ item_text = item.memory or ""
158
+ # Ensure line ends with newline (same format as simple_struct)
159
+ line = item_text if item_text.endswith("\n") else f"{item_text}\n"
160
+
161
+ # Check if adding this item would exceed max_tokens (same logic as _iter_chat_windows)
162
+ # Note: After splitting large items, each item should be <= max_tokens,
163
+ # but we still check to handle edge cases
164
+ if self._count_tokens(cur_text + line) > max_tokens and cur_text:
165
+ # Yield current window
166
+ window = self._build_window_from_items(buf_items, info)
167
+ if window:
168
+ windows.append(window)
169
+
170
+ # Keep overlap: remove items until remaining tokens <= overlap
171
+ # (same logic as _iter_chat_windows)
172
+ while (
173
+ buf_items
174
+ and self._count_tokens("".join([it.memory or "" for it in buf_items])) > overlap
175
+ ):
176
+ buf_items.pop(0)
177
+ # Recalculate cur_text from remaining items
178
+ cur_text = "".join([it.memory or "" for it in buf_items])
179
+
180
+ # Add item to current window
181
+ buf_items.append(item)
182
+ # Recalculate cur_text from all items in buffer (same as _iter_chat_windows)
183
+ cur_text = "".join([it.memory or "" for it in buf_items])
184
+
185
+ # Yield final window if any items remain
186
+ if buf_items:
187
+ window = self._build_window_from_items(buf_items, info)
188
+ if window:
189
+ windows.append(window)
190
+
191
+ # Batch compute embeddings for all windows
192
+ if windows:
193
+ # Collect all valid windows that need embedding
194
+ valid_windows = [w for w in windows if w and w.memory]
195
+
196
+ if valid_windows:
197
+ # Collect all texts that need embedding
198
+ texts_to_embed = [w.memory for w in valid_windows]
199
+
200
+ # Batch compute all embeddings at once
201
+ try:
202
+ embeddings = self.embedder.embed(texts_to_embed)
203
+ # Fill embeddings back into memory items
204
+ for window, embedding in zip(valid_windows, embeddings, strict=True):
205
+ window.metadata.embedding = embedding
206
+ except Exception as e:
207
+ logger.error(f"[MultiModalStruct] Error batch computing embeddings: {e}")
208
+ # Fallback: compute embeddings individually
209
+ for window in valid_windows:
210
+ if window.memory:
211
+ try:
212
+ window.metadata.embedding = self.embedder.embed([window.memory])[0]
213
+ except Exception as e2:
214
+ logger.error(
215
+ f"[MultiModalStruct] Error computing embedding for item: {e2}"
216
+ )
217
+
218
+ return windows
219
+
220
+ def _build_window_from_items(
221
+ self, items: list[TextualMemoryItem], info: dict[str, Any]
222
+ ) -> TextualMemoryItem | None:
223
+ """
224
+ Build a single memory item from a window of items (similar to _build_fast_node).
225
+
226
+ Args:
227
+ items: List of TextualMemoryItem objects in the window
228
+ info: Dictionary containing user_id and session_id
229
+
230
+ Returns:
231
+ Aggregated TextualMemoryItem or None if no valid content
232
+ """
233
+ if not items:
234
+ return None
235
+
236
+ # Collect all memory texts and sources
237
+ memory_texts = []
238
+ all_sources = []
239
+ roles = set()
240
+ aggregated_file_ids: list[str] = []
241
+
242
+ for item in items:
243
+ if item.memory:
244
+ memory_texts.append(item.memory)
245
+
246
+ # Collect sources and extract roles
247
+ item_sources = item.metadata.sources or []
248
+ if not isinstance(item_sources, list):
249
+ item_sources = [item_sources]
250
+
251
+ for source in item_sources:
252
+ # Add source to all_sources
253
+ all_sources.append(source)
254
+
255
+ # Extract role from source
256
+ if hasattr(source, "role") and source.role:
257
+ roles.add(source.role)
258
+ elif isinstance(source, dict) and source.get("role"):
259
+ roles.add(source.get("role"))
260
+
261
+ # Aggregate file_ids from metadata
262
+ metadata = getattr(item, "metadata", None)
263
+ if metadata is not None:
264
+ item_file_ids = getattr(metadata, "file_ids", None)
265
+ if isinstance(item_file_ids, list):
266
+ for fid in item_file_ids:
267
+ if fid and fid not in aggregated_file_ids:
268
+ aggregated_file_ids.append(fid)
269
+
270
+ # Determine memory_type based on roles (same logic as simple_struct)
271
+ # UserMemory if only user role, else LongTermMemory
272
+ memory_type = "UserMemory" if roles == {"user"} else "LongTermMemory"
273
+
274
+ # Merge all memory texts (preserve the format from parser)
275
+ merged_text = "".join(memory_texts) if memory_texts else ""
276
+
277
+ if not merged_text.strip():
278
+ # If no text content, return None
279
+ return None
280
+
281
+ # Create aggregated memory item without embedding (will be computed in batch later)
282
+ extra_kwargs: dict[str, Any] = {}
283
+ if aggregated_file_ids:
284
+ extra_kwargs["file_ids"] = aggregated_file_ids
285
+
286
+ # Extract info fields
287
+ info_ = info.copy()
288
+ user_id = info_.pop("user_id", "")
289
+ session_id = info_.pop("session_id", "")
290
+
291
+ # Create memory item without embedding (set to None, will be filled in batch)
292
+ aggregated_item = TextualMemoryItem(
293
+ memory=merged_text,
294
+ metadata=TreeNodeTextualMemoryMetadata(
295
+ user_id=user_id,
296
+ session_id=session_id,
297
+ memory_type=memory_type,
298
+ status="activated",
299
+ tags=["mode:fast"],
300
+ key=_derive_key(merged_text),
301
+ embedding=None, # Will be computed in batch
302
+ usage=[],
303
+ sources=all_sources,
304
+ background="",
305
+ confidence=0.99,
306
+ type="fact",
307
+ info=info_,
308
+ **extra_kwargs,
309
+ ),
310
+ )
311
+
312
+ return aggregated_item
313
+
314
+ def _get_llm_response(
315
+ self,
316
+ mem_str: str,
317
+ custom_tags: list[str] | None = None,
318
+ sources: list | None = None,
319
+ prompt_type: str = "chat",
320
+ ) -> dict:
321
+ """
322
+ Override parent method to improve language detection by using actual text content
323
+ from sources instead of JSON-structured memory string.
324
+
325
+ Args:
326
+ mem_str: Memory string (may contain JSON structures)
327
+ custom_tags: Optional custom tags
328
+ sources: Optional list of SourceMessage objects to extract text content from
329
+ prompt_type: Type of prompt to use ("chat" or "doc")
330
+
331
+ Returns:
332
+ LLM response dictionary
333
+ """
334
+ # Determine language: prioritize lang from sources (set in fast mode),
335
+ # fallback to detecting from mem_str if sources don't have lang
336
+ lang = None
337
+
338
+ # First, try to get lang from sources (fast mode already set this)
339
+ if sources:
340
+ for source in sources:
341
+ if hasattr(source, "lang") and source.lang:
342
+ lang = source.lang
343
+ break
344
+ elif isinstance(source, dict) and source.get("lang"):
345
+ lang = source.get("lang")
346
+ break
347
+
348
+ # Fallback: detect language from mem_str if no lang from sources
349
+ if lang is None:
350
+ lang = detect_lang(mem_str)
351
+
352
+ # Select prompt template based on prompt_type
353
+ if prompt_type == "doc":
354
+ template = PROMPT_DICT["doc"][lang]
355
+ examples = "" # doc prompts don't have examples
356
+ prompt = template.replace("{chunk_text}", mem_str)
357
+ elif prompt_type == "general_string":
358
+ template = PROMPT_DICT["general_string"][lang]
359
+ examples = ""
360
+ prompt = template.replace("{chunk_text}", mem_str)
361
+ else:
362
+ template = PROMPT_DICT["chat"][lang]
363
+ examples = PROMPT_DICT["chat"][f"{lang}_example"]
364
+ prompt = template.replace("${conversation}", mem_str)
365
+
366
+ custom_tags_prompt = (
367
+ PROMPT_DICT["custom_tags"][lang].replace("{custom_tags}", str(custom_tags))
368
+ if custom_tags
369
+ else ""
370
+ )
371
+
372
+ # Replace custom_tags_prompt placeholder (different for doc vs chat)
373
+ if prompt_type in ["doc", "general_string"]:
374
+ prompt = prompt.replace("{custom_tags_prompt}", custom_tags_prompt)
375
+ else:
376
+ prompt = prompt.replace("${custom_tags_prompt}", custom_tags_prompt)
377
+
378
+ if self.config.remove_prompt_example and examples:
379
+ prompt = prompt.replace(examples, "")
380
+ messages = [{"role": "user", "content": prompt}]
381
+ try:
382
+ response_text = self.llm.generate(messages)
383
+ response_json = parse_json_result(response_text)
384
+ except Exception as e:
385
+ logger.error(f"[LLM] Exception during chat generation: {e}")
386
+ response_json = {
387
+ "memory list": [
388
+ {
389
+ "key": mem_str[:10],
390
+ "memory_type": "UserMemory",
391
+ "value": mem_str,
392
+ "tags": [],
393
+ }
394
+ ],
395
+ "summary": mem_str,
396
+ }
397
+ logger.info(f"[MultiModalFine] Task {messages}, Result {response_json}")
398
+ return response_json
399
+
400
+ def _determine_prompt_type(self, sources: list) -> str:
401
+ """
402
+ Determine prompt type based on sources.
403
+ """
404
+ if not sources:
405
+ return "chat"
406
+ prompt_type = "general_string"
407
+ for source in sources:
408
+ source_role = None
409
+ if hasattr(source, "role"):
410
+ source_role = source.role
411
+ elif isinstance(source, dict):
412
+ source_role = source.get("role")
413
+ if source_role in {"user", "assistant", "system", "tool"}:
414
+ prompt_type = "chat"
415
+
416
+ return prompt_type
417
+
418
+ def _get_maybe_merged_memory(
419
+ self,
420
+ extracted_memory_dict: dict,
421
+ mem_text: str,
422
+ sources: list,
423
+ **kwargs,
424
+ ) -> dict:
425
+ """
426
+ Check if extracted memory should be merged with similar existing memories.
427
+ If merge is needed, return merged memory dict with merged_from field.
428
+ Otherwise, return original memory dict.
429
+
430
+ Args:
431
+ extracted_memory_dict: The extracted memory dict from LLM response
432
+ mem_text: The memory text content
433
+ sources: Source messages for language detection
434
+ **kwargs: Additional parameters (merge_similarity_threshold, etc.)
435
+
436
+ Returns:
437
+ Memory dict (possibly merged) with merged_from field if merged
438
+ """
439
+ # If no graph_db or user_name, return original
440
+ if not self.graph_db or "user_name" not in kwargs:
441
+ return extracted_memory_dict
442
+ user_name = kwargs.get("user_name")
443
+
444
+ # Detect language
445
+ lang = "en"
446
+ if sources:
447
+ for source in sources:
448
+ if hasattr(source, "lang") and source.lang:
449
+ lang = source.lang
450
+ break
451
+ elif isinstance(source, dict) and source.get("lang"):
452
+ lang = source.get("lang")
453
+ break
454
+ if lang is None:
455
+ lang = detect_lang(mem_text)
456
+
457
+ # Search for similar memories
458
+ merge_threshold = kwargs.get("merge_similarity_threshold", 0.3)
459
+
460
+ try:
461
+ search_results = self.graph_db.search_by_embedding(
462
+ vector=self.embedder.embed(mem_text)[0],
463
+ top_k=20,
464
+ status="activated",
465
+ threshold=merge_threshold,
466
+ user_name=user_name,
467
+ filter={
468
+ "or": [
469
+ {"memory_type": "LongTermMemory"},
470
+ {"memory_type": "UserMemory"},
471
+ {"memory_type": "WorkingMemory"},
472
+ ]
473
+ },
474
+ )
475
+
476
+ if not search_results:
477
+ return extracted_memory_dict
478
+
479
+ # Get full memory details
480
+ similar_memory_ids = [r["id"] for r in search_results if r.get("id")]
481
+ similar_memories_list = [
482
+ self.graph_db.get_node(mem_id, include_embedding=False, user_name=user_name)
483
+ for mem_id in similar_memory_ids
484
+ ]
485
+
486
+ # Filter out None and mode:fast memories
487
+ filtered_similar = []
488
+ for mem in similar_memories_list:
489
+ if not mem:
490
+ continue
491
+ mem_metadata = mem.get("metadata", {})
492
+ tags = mem_metadata.get("tags", [])
493
+ if isinstance(tags, list) and "mode:fast" in tags:
494
+ continue
495
+ filtered_similar.append(
496
+ {
497
+ "id": mem.get("id"),
498
+ "memory": mem.get("memory", ""),
499
+ }
500
+ )
501
+ logger.info(
502
+ f"Valid similar memories for {mem_text} is "
503
+ f"{len(filtered_similar)}: {filtered_similar}"
504
+ )
505
+
506
+ if not filtered_similar:
507
+ return extracted_memory_dict
508
+
509
+ # Create a temporary TextualMemoryItem for merge check
510
+ temp_memory_item = TextualMemoryItem(
511
+ memory=mem_text,
512
+ metadata=TreeNodeTextualMemoryMetadata(
513
+ user_id="",
514
+ session_id="",
515
+ memory_type=extracted_memory_dict.get("memory_type", "LongTermMemory"),
516
+ status="activated",
517
+ tags=extracted_memory_dict.get("tags", []),
518
+ key=extracted_memory_dict.get("key", ""),
519
+ ),
520
+ )
521
+
522
+ # Try to merge with LLM
523
+ merge_result = self._merge_memories_with_llm(
524
+ temp_memory_item, filtered_similar, lang=lang
525
+ )
526
+
527
+ if merge_result:
528
+ # Return merged memory dict
529
+ merged_dict = extracted_memory_dict.copy()
530
+ merged_content = merge_result.get("value", mem_text)
531
+ merged_dict["value"] = merged_content
532
+ merged_from_ids = merge_result.get("merged_from", [])
533
+ merged_dict["merged_from"] = merged_from_ids
534
+ return merged_dict
535
+ else:
536
+ return extracted_memory_dict
537
+
538
+ except Exception as e:
539
+ logger.error(f"[MultiModalFine] Error in get_maybe_merged_memory: {e}")
540
+ # On error, return original
541
+ return extracted_memory_dict
542
+
543
+ def _merge_memories_with_llm(
544
+ self,
545
+ new_memory: TextualMemoryItem,
546
+ similar_memories: list[dict],
547
+ lang: str = "en",
548
+ ) -> dict | None:
549
+ """
550
+ Use LLM to merge new memory with similar existing memories.
551
+
552
+ Args:
553
+ new_memory: The newly extracted memory item
554
+ similar_memories: List of similar memories from graph_db (with id and memory fields)
555
+ lang: Language code ("en" or "zh")
556
+
557
+ Returns:
558
+ Merged memory dict with merged_from field, or None if no merge needed
559
+ """
560
+ if not similar_memories:
561
+ return None
562
+
563
+ # Build merge prompt using template
564
+ similar_memories_text = "\n".join(
565
+ [f"[{mem['id']}]: {mem['memory']}" for mem in similar_memories]
566
+ )
567
+
568
+ merge_prompt_template = MEMORY_MERGE_PROMPT_ZH if lang == "zh" else MEMORY_MERGE_PROMPT_EN
569
+ merge_prompt = merge_prompt_template.format(
570
+ new_memory=new_memory.memory,
571
+ similar_memories=similar_memories_text,
572
+ )
573
+
574
+ try:
575
+ response_text = self.llm.generate([{"role": "user", "content": merge_prompt}])
576
+ merge_result = parse_json_result(response_text)
577
+
578
+ if merge_result.get("should_merge", False):
579
+ return {
580
+ "value": merge_result.get("value", new_memory.memory),
581
+ "merged_from": merge_result.get(
582
+ "merged_from", [mem["id"] for mem in similar_memories]
583
+ ),
584
+ }
585
+ except Exception as e:
586
+ logger.error(f"[MultiModalFine] Error in merge LLM call: {e}")
587
+
588
+ return None
589
+
590
+ def _process_string_fine(
591
+ self,
592
+ fast_memory_items: list[TextualMemoryItem],
593
+ info: dict[str, Any],
594
+ custom_tags: list[str] | None = None,
595
+ **kwargs,
596
+ ) -> list[TextualMemoryItem]:
597
+ """
598
+ Process fast mode memory items through LLM to generate fine mode memories.
599
+ """
600
+ if not fast_memory_items:
601
+ return []
602
+
603
+ def _process_one_item(fast_item: TextualMemoryItem) -> list[TextualMemoryItem]:
604
+ """Process a single fast memory item and return a list of fine items."""
605
+ fine_items: list[TextualMemoryItem] = []
606
+
607
+ # Extract memory text (string content)
608
+ mem_str = fast_item.memory or ""
609
+ if not mem_str.strip():
610
+ return fine_items
611
+
612
+ sources = fast_item.metadata.sources or []
613
+ if not isinstance(sources, list):
614
+ sources = [sources]
615
+
616
+ # Extract file_ids from fast item metadata for propagation
617
+ metadata = getattr(fast_item, "metadata", None)
618
+ file_ids = getattr(metadata, "file_ids", None) if metadata is not None else None
619
+ file_ids = [fid for fid in file_ids if fid] if isinstance(file_ids, list) else []
620
+
621
+ # Build per-item info copy and kwargs for _make_memory_item
622
+ info_per_item = info.copy()
623
+ if file_ids and "file_id" not in info_per_item:
624
+ info_per_item["file_id"] = file_ids[0]
625
+ extra_kwargs: dict[str, Any] = {}
626
+ if file_ids:
627
+ extra_kwargs["file_ids"] = file_ids
628
+
629
+ # Determine prompt type based on sources
630
+ prompt_type = self._determine_prompt_type(sources)
631
+
632
+ # ========== Stage 1: Normal extraction (without reference) ==========
633
+ try:
634
+ resp = self._get_llm_response(mem_str, custom_tags, sources, prompt_type)
635
+ except Exception as e:
636
+ logger.error(f"[MultiModalFine] Error calling LLM: {e}")
637
+ return fine_items
638
+
639
+ if resp.get("memory list", []):
640
+ for m in resp.get("memory list", []):
641
+ try:
642
+ # Check and merge with similar memories if needed
643
+ m_maybe_merged = self._get_maybe_merged_memory(
644
+ extracted_memory_dict=m,
645
+ mem_text=m.get("value", ""),
646
+ sources=sources,
647
+ original_query=mem_str,
648
+ **kwargs,
649
+ )
650
+ # Normalize memory_type (same as simple_struct)
651
+ memory_type = (
652
+ m_maybe_merged.get("memory_type", "LongTermMemory")
653
+ .replace("长期记忆", "LongTermMemory")
654
+ .replace("用户记忆", "UserMemory")
655
+ )
656
+ node = self._make_memory_item(
657
+ value=m_maybe_merged.get("value", ""),
658
+ info=info_per_item,
659
+ memory_type=memory_type,
660
+ tags=m_maybe_merged.get("tags", []),
661
+ key=m_maybe_merged.get("key", ""),
662
+ sources=sources, # Preserve sources from fast item
663
+ background=resp.get("summary", ""),
664
+ **extra_kwargs,
665
+ )
666
+ # Add merged_from to info if present
667
+ if "merged_from" in m_maybe_merged:
668
+ node.metadata.info = node.metadata.info or {}
669
+ node.metadata.info["merged_from"] = m_maybe_merged["merged_from"]
670
+ fine_items.append(node)
671
+ except Exception as e:
672
+ logger.error(f"[MultiModalFine] parse error: {e}")
673
+ elif resp.get("value") and resp.get("key"):
674
+ try:
675
+ # Check and merge with similar memories if needed
676
+ resp_maybe_merged = self._get_maybe_merged_memory(
677
+ extracted_memory_dict=resp,
678
+ mem_text=resp.get("value", "").strip(),
679
+ sources=sources,
680
+ original_query=mem_str,
681
+ **kwargs,
682
+ )
683
+ node = self._make_memory_item(
684
+ value=resp_maybe_merged.get("value", "").strip(),
685
+ info=info_per_item,
686
+ memory_type="LongTermMemory",
687
+ tags=resp_maybe_merged.get("tags", []),
688
+ key=resp_maybe_merged.get("key", None),
689
+ sources=sources, # Preserve sources from fast item
690
+ background=resp.get("summary", ""),
691
+ **extra_kwargs,
692
+ )
693
+ # Add merged_from to info if present
694
+ if "merged_from" in resp_maybe_merged:
695
+ node.metadata.info = node.metadata.info or {}
696
+ node.metadata.info["merged_from"] = resp_maybe_merged["merged_from"]
697
+ fine_items.append(node)
698
+ except Exception as e:
699
+ logger.error(f"[MultiModalFine] parse error: {e}")
700
+
701
+ return fine_items
702
+
703
+ fine_memory_items: list[TextualMemoryItem] = []
704
+
705
+ with ContextThreadPoolExecutor(max_workers=30) as executor:
706
+ futures = [executor.submit(_process_one_item, item) for item in fast_memory_items]
707
+
708
+ for future in concurrent.futures.as_completed(futures):
709
+ try:
710
+ result = future.result()
711
+ if result:
712
+ fine_memory_items.extend(result)
713
+ except Exception as e:
714
+ logger.error(f"[MultiModalFine] worker error: {e}")
715
+
716
+ return fine_memory_items
717
+
718
+ def _get_llm_tool_trajectory_response(self, mem_str: str) -> dict:
719
+ """
720
+ Generete tool trajectory experience item by llm.
721
+ """
722
+ try:
723
+ lang = detect_lang(mem_str)
724
+ template = TOOL_TRAJECTORY_PROMPT_ZH if lang == "zh" else TOOL_TRAJECTORY_PROMPT_EN
725
+ prompt = template.replace("{messages}", mem_str)
726
+ rsp = self.llm.generate([{"role": "user", "content": prompt}])
727
+ rsp = rsp.replace("```json", "").replace("```", "")
728
+ return json.loads(rsp)
729
+ except Exception as e:
730
+ logger.error(f"[MultiModalFine] Error calling LLM for tool trajectory: {e}")
731
+ return []
732
+
733
+ def _process_tool_trajectory_fine(
734
+ self, fast_memory_items: list[TextualMemoryItem], info: dict[str, Any], **kwargs
735
+ ) -> list[TextualMemoryItem]:
736
+ """
737
+ Process tool trajectory memory items through LLM to generate fine mode memories.
738
+ """
739
+ if not fast_memory_items:
740
+ return []
741
+
742
+ fine_memory_items = []
743
+
744
+ for fast_item in fast_memory_items:
745
+ # Extract memory text (string content)
746
+ mem_str = fast_item.memory or ""
747
+ if not mem_str.strip() or (
748
+ "tool:" not in mem_str
749
+ and "[tool_calls]:" not in mem_str
750
+ and not re.search(r"<tool_schema>.*?</tool_schema>", mem_str, re.DOTALL)
751
+ ):
752
+ continue
753
+ try:
754
+ resp = self._get_llm_tool_trajectory_response(mem_str)
755
+ except Exception as e:
756
+ logger.error(f"[MultiModalFine] Error calling LLM for tool trajectory: {e}")
757
+ continue
758
+ for m in resp:
759
+ try:
760
+ # Normalize memory_type (same as simple_struct)
761
+ memory_type = "ToolTrajectoryMemory"
762
+
763
+ node = self._make_memory_item(
764
+ value=m.get("trajectory", ""),
765
+ info=info,
766
+ memory_type=memory_type,
767
+ correctness=m.get("correctness", ""),
768
+ experience=m.get("experience", ""),
769
+ tool_used_status=m.get("tool_used_status", []),
770
+ )
771
+ fine_memory_items.append(node)
772
+ except Exception as e:
773
+ logger.error(f"[MultiModalFine] parse error for tool trajectory: {e}")
774
+
775
+ return fine_memory_items
776
+
777
+ @timed
778
+ def _process_multi_modal_data(
779
+ self, scene_data_info: MessagesType, info, mode: str = "fine", **kwargs
780
+ ) -> list[TextualMemoryItem]:
781
+ """
782
+ Process multimodal data using MultiModalParser.
783
+
784
+ Args:
785
+ scene_data_info: MessagesType input
786
+ info: Dictionary containing user_id and session_id
787
+ mode: mem-reader mode, fast for quick process while fine for
788
+ better understanding via calling llm
789
+ **kwargs: Additional parameters (mode, etc.)
790
+ """
791
+ # Pop custom_tags from info (same as simple_struct.py)
792
+ # must pop here, avoid add to info, only used in sync fine mode
793
+ custom_tags = info.pop("custom_tags", None) if isinstance(info, dict) else None
794
+
795
+ # Use MultiModalParser to parse the scene data
796
+ # If it's a list, parse each item; otherwise parse as single message
797
+ if isinstance(scene_data_info, list):
798
+ # Parse each message in the list
799
+ all_memory_items = []
800
+ for msg in scene_data_info:
801
+ items = self.multi_modal_parser.parse(msg, info, mode="fast", **kwargs)
802
+ all_memory_items.extend(items)
803
+ else:
804
+ # Parse as single message
805
+ all_memory_items = self.multi_modal_parser.parse(
806
+ scene_data_info, info, mode="fast", **kwargs
807
+ )
808
+ fast_memory_items = self._concat_multi_modal_memories(all_memory_items)
809
+ if mode == "fast":
810
+ return fast_memory_items
811
+ else:
812
+ # Part A: call llm in parallel using thread pool
813
+ fine_memory_items = []
814
+
815
+ with ContextThreadPoolExecutor(max_workers=2) as executor:
816
+ future_string = executor.submit(
817
+ self._process_string_fine, fast_memory_items, info, custom_tags, **kwargs
818
+ )
819
+ future_tool = executor.submit(
820
+ self._process_tool_trajectory_fine, fast_memory_items, info, **kwargs
821
+ )
822
+
823
+ # Collect results
824
+ fine_memory_items_string_parser = future_string.result()
825
+ fine_memory_items_tool_trajectory_parser = future_tool.result()
826
+
827
+ fine_memory_items.extend(fine_memory_items_string_parser)
828
+ fine_memory_items.extend(fine_memory_items_tool_trajectory_parser)
829
+
830
+ # Part B: get fine multimodal items
831
+ for fast_item in fast_memory_items:
832
+ sources = fast_item.metadata.sources
833
+ for source in sources:
834
+ lang = getattr(source, "lang", "en")
835
+ items = self.multi_modal_parser.process_transfer(
836
+ source,
837
+ context_items=[fast_item],
838
+ custom_tags=custom_tags,
839
+ info=info,
840
+ lang=lang,
841
+ )
842
+ fine_memory_items.extend(items)
843
+ return fine_memory_items
844
+
845
+ @timed
846
+ def _process_transfer_multi_modal_data(
847
+ self, raw_node: TextualMemoryItem, custom_tags: list[str] | None = None, **kwargs
848
+ ) -> list[TextualMemoryItem]:
849
+ """
850
+ Process transfer for multimodal data.
851
+
852
+ Each source is processed independently by its corresponding parser,
853
+ which knows how to rebuild the original message and parse it in fine mode.
854
+ """
855
+ sources = raw_node.metadata.sources or []
856
+ if not sources:
857
+ logger.warning("[MultiModalStruct] No sources found in raw_node")
858
+ return []
859
+
860
+ # Extract info from raw_node (same as simple_struct.py)
861
+ info = {
862
+ "user_id": raw_node.metadata.user_id,
863
+ "session_id": raw_node.metadata.session_id,
864
+ **(raw_node.metadata.info or {}),
865
+ }
866
+
867
+ fine_memory_items = []
868
+ # Part A: call llm in parallel using thread pool
869
+ with ContextThreadPoolExecutor(max_workers=2) as executor:
870
+ future_string = executor.submit(
871
+ self._process_string_fine, [raw_node], info, custom_tags, **kwargs
872
+ )
873
+ future_tool = executor.submit(
874
+ self._process_tool_trajectory_fine, [raw_node], info, **kwargs
875
+ )
876
+
877
+ # Collect results
878
+ fine_memory_items_string_parser = future_string.result()
879
+ fine_memory_items_tool_trajectory_parser = future_tool.result()
880
+
881
+ fine_memory_items.extend(fine_memory_items_string_parser)
882
+ fine_memory_items.extend(fine_memory_items_tool_trajectory_parser)
883
+
884
+ # Part B: get fine multimodal items
885
+ for source in sources:
886
+ lang = getattr(source, "lang", "en")
887
+ items = self.multi_modal_parser.process_transfer(
888
+ source, context_items=[raw_node], info=info, custom_tags=custom_tags, lang=lang
889
+ )
890
+ fine_memory_items.extend(items)
891
+ return fine_memory_items
892
+
893
+ def get_scene_data_info(self, scene_data: list, type: str) -> list[list[Any]]:
894
+ """
895
+ Convert normalized MessagesType scenes into scene data info.
896
+ For MultiModalStructMemReader, this is a simplified version that returns the scenes as-is.
897
+
898
+ Args:
899
+ scene_data: List of MessagesType scenes
900
+ type: Type of scene_data: ['doc', 'chat']
901
+
902
+ Returns:
903
+ List of scene data info
904
+ """
905
+ # TODO: split messages
906
+ return scene_data
907
+
908
+ def _read_memory(
909
+ self,
910
+ messages: list[MessagesType],
911
+ type: str,
912
+ info: dict[str, Any],
913
+ mode: str = "fine",
914
+ **kwargs,
915
+ ) -> list[list[TextualMemoryItem]]:
916
+ list_scene_data_info = self.get_scene_data_info(messages, type)
917
+
918
+ memory_list = []
919
+ # Process Q&A pairs concurrently with context propagation
920
+ with ContextThreadPoolExecutor() as executor:
921
+ futures = [
922
+ executor.submit(
923
+ self._process_multi_modal_data, scene_data_info, info, mode=mode, **kwargs
924
+ )
925
+ for scene_data_info in list_scene_data_info
926
+ ]
927
+ for future in concurrent.futures.as_completed(futures):
928
+ try:
929
+ res_memory = future.result()
930
+ if res_memory is not None:
931
+ memory_list.append(res_memory)
932
+ except Exception as e:
933
+ logger.error(f"Task failed with exception: {e}")
934
+ logger.error(traceback.format_exc())
935
+ return memory_list
936
+
937
+ def fine_transfer_simple_mem(
938
+ self,
939
+ input_memories: list[TextualMemoryItem],
940
+ type: str,
941
+ custom_tags: list[str] | None = None,
942
+ **kwargs,
943
+ ) -> list[list[TextualMemoryItem]]:
944
+ if not input_memories:
945
+ return []
946
+
947
+ memory_list = []
948
+
949
+ # Process Q&A pairs concurrently with context propagation
950
+ with ContextThreadPoolExecutor() as executor:
951
+ futures = [
952
+ executor.submit(
953
+ self._process_transfer_multi_modal_data, scene_data_info, custom_tags, **kwargs
954
+ )
955
+ for scene_data_info in input_memories
956
+ ]
957
+ for future in concurrent.futures.as_completed(futures):
958
+ try:
959
+ res_memory = future.result()
960
+ if res_memory is not None:
961
+ memory_list.append(res_memory)
962
+ except Exception as e:
963
+ logger.error(f"Task failed with exception: {e}")
964
+ logger.error(traceback.format_exc())
965
+ return memory_list