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,485 @@
1
+ import json
2
+ import os
3
+ import ssl
4
+ import threading
5
+ import time
6
+
7
+ from pathlib import Path
8
+ from queue import Empty
9
+
10
+ from memos.configs.mem_scheduler import AuthConfig, RabbitMQConfig
11
+ from memos.context.context import ContextThread
12
+ from memos.dependency import require_python_package
13
+ from memos.log import get_logger
14
+ from memos.mem_scheduler.general_modules.base import BaseSchedulerModule
15
+ from memos.mem_scheduler.general_modules.misc import AutoDroppingQueue
16
+ from memos.mem_scheduler.schemas.general_schemas import DIRECT_EXCHANGE_TYPE, FANOUT_EXCHANGE_TYPE
17
+
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class RabbitMQSchedulerModule(BaseSchedulerModule):
23
+ @require_python_package(
24
+ import_name="pika",
25
+ install_command="pip install pika",
26
+ install_link="https://pika.readthedocs.io/en/stable/index.html",
27
+ )
28
+ def __init__(self):
29
+ """
30
+ Initialize RabbitMQ connection settings.
31
+ """
32
+ super().__init__()
33
+ self.auth_config = None
34
+
35
+ # RabbitMQ settings
36
+ self.rabbitmq_config: RabbitMQConfig | None = None
37
+ self.rabbit_queue_name = "memos-scheduler"
38
+ self.rabbitmq_exchange_name = "memos-fanout" # Default, will be overridden by config
39
+ self.rabbitmq_exchange_type = FANOUT_EXCHANGE_TYPE # Default, will be overridden by config
40
+ self.rabbitmq_connection = None
41
+ self.rabbitmq_channel = None
42
+
43
+ # fixed params
44
+ self.rabbitmq_message_cache_max_size = 10 # Max 10 messages
45
+ self.rabbitmq_message_cache = AutoDroppingQueue(
46
+ maxsize=self.rabbitmq_message_cache_max_size
47
+ )
48
+ # Pending outgoing messages to avoid loss when connection is not ready
49
+ self.rabbitmq_publish_cache_max_size = 50
50
+ self.rabbitmq_publish_cache = AutoDroppingQueue(
51
+ maxsize=self.rabbitmq_publish_cache_max_size
52
+ )
53
+ self.rabbitmq_connection_attempts = 3 # Max retry attempts on connection failure
54
+ self.rabbitmq_retry_delay = 5 # Delay (seconds) between retries
55
+ self.rabbitmq_heartbeat = 60 # Heartbeat interval (seconds) for connectio
56
+ self.rabbitmq_conn_max_waiting_seconds = 30
57
+ self.rabbitmq_conn_sleep_seconds = 1
58
+
59
+ # Thread management
60
+ self._rabbitmq_io_loop_thread = None # For IOLoop execution
61
+ self._rabbitmq_stop_flag = False # Graceful shutdown flag
62
+ # Use RLock because publishing may trigger initialization, which also grabs the lock.
63
+ self._rabbitmq_lock = threading.RLock()
64
+ self._rabbitmq_initializing = False # Avoid duplicate concurrent initializations
65
+
66
+ def is_rabbitmq_connected(self) -> bool:
67
+ """Check if RabbitMQ connection is alive"""
68
+ return (
69
+ self.rabbitmq_connection
70
+ and self.rabbitmq_connection.is_open
71
+ and self.rabbitmq_channel
72
+ and self.rabbitmq_channel.is_open
73
+ )
74
+
75
+ def initialize_rabbitmq(
76
+ self, config: dict | None | RabbitMQConfig = None, config_path: str | Path | None = None
77
+ ):
78
+ """
79
+ Establish connection to RabbitMQ using pika.
80
+ """
81
+ with self._rabbitmq_lock:
82
+ if self._rabbitmq_initializing:
83
+ logger.info(
84
+ "[DIAGNOSTIC] initialize_rabbitmq: initialization already in progress; skipping duplicate call."
85
+ )
86
+ return
87
+ self._rabbitmq_initializing = True
88
+ try:
89
+ # Skip remote initialization in CI/pytest unless explicitly enabled
90
+ enable_env = os.getenv("MEMOS_ENABLE_RABBITMQ", "").lower() == "true"
91
+ in_ci = os.getenv("CI", "").lower() == "true"
92
+ in_pytest = os.getenv("PYTEST_CURRENT_TEST") is not None
93
+ logger.info(
94
+ f"[DIAGNOSTIC] initialize_rabbitmq called. in_ci={in_ci}, in_pytest={in_pytest}, "
95
+ f"MEMOS_ENABLE_RABBITMQ={enable_env}, config_path={config_path}"
96
+ )
97
+ if (in_ci or in_pytest) and not enable_env:
98
+ logger.info(
99
+ "Skipping RabbitMQ initialization in CI/test environment. Set MEMOS_ENABLE_RABBITMQ=true to enable."
100
+ )
101
+ return
102
+
103
+ if self.is_rabbitmq_connected():
104
+ logger.warning("RabbitMQ is already connected. Skipping initialization.")
105
+ return
106
+
107
+ from pika.adapters.select_connection import SelectConnection
108
+
109
+ if config is not None:
110
+ if isinstance(config, RabbitMQConfig):
111
+ self.rabbitmq_config = config
112
+ elif isinstance(config, dict):
113
+ self.rabbitmq_config = AuthConfig.from_dict(config).rabbitmq
114
+ else:
115
+ logger.error(f"Unsupported config type: {type(config)}")
116
+ return
117
+
118
+ else:
119
+ if config_path is not None and Path(config_path).exists():
120
+ self.auth_config = AuthConfig.from_local_config(config_path=config_path)
121
+ elif AuthConfig.default_config_exists():
122
+ self.auth_config = AuthConfig.from_local_config()
123
+ else:
124
+ self.auth_config = AuthConfig.from_local_env()
125
+ self.rabbitmq_config = self.auth_config.rabbitmq
126
+
127
+ if self.rabbitmq_config is None:
128
+ logger.error(
129
+ "Failed to load RabbitMQ configuration. Please check your config file or environment variables."
130
+ )
131
+ return
132
+
133
+ # Load exchange configuration from config
134
+ if self.rabbitmq_config:
135
+ if (
136
+ hasattr(self.rabbitmq_config, "exchange_name")
137
+ and self.rabbitmq_config.exchange_name
138
+ ):
139
+ self.rabbitmq_exchange_name = self.rabbitmq_config.exchange_name
140
+ logger.info(f"Using configured exchange name: {self.rabbitmq_exchange_name}")
141
+ if (
142
+ hasattr(self.rabbitmq_config, "exchange_type")
143
+ and self.rabbitmq_config.exchange_type
144
+ ):
145
+ self.rabbitmq_exchange_type = self.rabbitmq_config.exchange_type
146
+ logger.info(f"Using configured exchange type: {self.rabbitmq_exchange_type}")
147
+
148
+ env_exchange_name = os.getenv("MEMSCHEDULER_RABBITMQ_EXCHANGE_NAME")
149
+ env_exchange_type = os.getenv("MEMSCHEDULER_RABBITMQ_EXCHANGE_TYPE")
150
+ if env_exchange_name:
151
+ self.rabbitmq_exchange_name = env_exchange_name
152
+ logger.info(f"Using env exchange name override: {self.rabbitmq_exchange_name}")
153
+ if env_exchange_type:
154
+ self.rabbitmq_exchange_type = env_exchange_type
155
+ logger.info(f"Using env exchange type override: {self.rabbitmq_exchange_type}")
156
+
157
+ # Start connection process
158
+ parameters = self.get_rabbitmq_connection_param()
159
+ self.rabbitmq_connection = SelectConnection(
160
+ parameters,
161
+ on_open_callback=self.on_rabbitmq_connection_open,
162
+ on_open_error_callback=self.on_rabbitmq_connection_error,
163
+ on_close_callback=self.on_rabbitmq_connection_closed,
164
+ )
165
+
166
+ # Start IOLoop in dedicated thread
167
+ self._io_loop_thread = ContextThread(
168
+ target=self.rabbitmq_connection.ioloop.start, daemon=True
169
+ )
170
+ self._io_loop_thread.start()
171
+ logger.info("RabbitMQ connection process started")
172
+ except Exception:
173
+ logger.error("Failed to initialize RabbitMQ connection", exc_info=True)
174
+ finally:
175
+ with self._rabbitmq_lock:
176
+ self._rabbitmq_initializing = False
177
+
178
+ def get_rabbitmq_queue_size(self) -> int:
179
+ """Get the current number of messages in the queue.
180
+
181
+ Returns:
182
+ int: Number of messages in the queue.
183
+ Returns -1 if there's an error or no active connection.
184
+ """
185
+ if self.rabbitmq_exchange_type != DIRECT_EXCHANGE_TYPE:
186
+ logger.warning("Queue size can only be checked for direct exchanges")
187
+ return None
188
+
189
+ with self._rabbitmq_lock:
190
+ if not self.is_rabbitmq_connected():
191
+ logger.warning("No active connection to check queue size")
192
+ return -1
193
+
194
+ # Declare queue passively (only checks existence, doesn't create)
195
+ # Using passive=True prevents accidental queue creation
196
+ result = self.rabbitmq_channel.queue_declare(
197
+ queue=self.rabbit_queue_name,
198
+ durable=True, # Match the original queue durability setting
199
+ passive=True, # Only check queue existence, don't create
200
+ )
201
+
202
+ if result is None:
203
+ return 0
204
+ # Return the message count from the queue declaration result
205
+ return result.method.message_count
206
+
207
+ def get_rabbitmq_connection_param(self):
208
+ import pika
209
+
210
+ credentials = pika.PlainCredentials(
211
+ username=self.rabbitmq_config.user_name,
212
+ password=self.rabbitmq_config.password,
213
+ erase_on_connect=self.rabbitmq_config.erase_on_connect,
214
+ )
215
+ if self.rabbitmq_config.port == 5671:
216
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
217
+ context.check_hostname = False
218
+ context.verify_mode = False
219
+ return pika.ConnectionParameters(
220
+ host=self.rabbitmq_config.host_name,
221
+ port=self.rabbitmq_config.port,
222
+ virtual_host=self.rabbitmq_config.virtual_host,
223
+ credentials=credentials,
224
+ ssl_options=pika.SSLOptions(context),
225
+ connection_attempts=self.rabbitmq_connection_attempts,
226
+ retry_delay=self.rabbitmq_retry_delay,
227
+ heartbeat=self.rabbitmq_heartbeat,
228
+ )
229
+ else:
230
+ return pika.ConnectionParameters(
231
+ host=self.rabbitmq_config.host_name,
232
+ port=self.rabbitmq_config.port,
233
+ virtual_host=self.rabbitmq_config.virtual_host,
234
+ credentials=credentials,
235
+ connection_attempts=self.rabbitmq_connection_attempts,
236
+ retry_delay=self.rabbitmq_retry_delay,
237
+ heartbeat=self.rabbitmq_heartbeat,
238
+ )
239
+
240
+ # Connection lifecycle callbacks
241
+ def on_rabbitmq_connection_open(self, connection):
242
+ """Called when connection is established."""
243
+ logger.info("[DIAGNOSTIC] RabbitMQ connection opened")
244
+ connection.channel(on_open_callback=self.on_rabbitmq_channel_open)
245
+
246
+ def on_rabbitmq_connection_error(self, connection, error):
247
+ """Called if connection fails to open."""
248
+ logger.error(f"Connection failed: {error}")
249
+ self.rabbit_reconnect()
250
+
251
+ def on_rabbitmq_connection_closed(self, connection, reason):
252
+ """Called when connection closes."""
253
+ logger.warning(f"Connection closed: {reason}")
254
+ if not self._rabbitmq_stop_flag:
255
+ self.rabbit_reconnect()
256
+
257
+ # Channel lifecycle callbacks
258
+ def on_rabbitmq_channel_open(self, channel):
259
+ """Called when channel is ready."""
260
+ self.rabbitmq_channel = channel
261
+ logger.info("[DIAGNOSTIC] RabbitMQ channel opened")
262
+
263
+ # Setup exchange and queue
264
+ channel.exchange_declare(
265
+ exchange=self.rabbitmq_exchange_name,
266
+ exchange_type=self.rabbitmq_exchange_type,
267
+ durable=True,
268
+ callback=self.on_rabbitmq_exchange_declared,
269
+ )
270
+
271
+ def on_rabbitmq_exchange_declared(self, frame):
272
+ """Called when exchange is ready."""
273
+ self.rabbitmq_channel.queue_declare(
274
+ queue=self.rabbit_queue_name, durable=True, callback=self.on_rabbitmq_queue_declared
275
+ )
276
+
277
+ def on_rabbitmq_queue_declared(self, frame):
278
+ """Called when queue is ready."""
279
+ self.rabbitmq_channel.queue_bind(
280
+ exchange=self.rabbitmq_exchange_name,
281
+ queue=self.rabbit_queue_name,
282
+ routing_key=self.rabbit_queue_name,
283
+ callback=self.on_rabbitmq_bind_ok,
284
+ )
285
+
286
+ def on_rabbitmq_bind_ok(self, frame):
287
+ """Final setup step when bind is complete."""
288
+ logger.info("RabbitMQ setup completed")
289
+ # Flush any cached publish messages now that connection is ready
290
+ self._flush_cached_publish_messages()
291
+
292
+ def on_rabbitmq_message(self, channel, method, properties, body):
293
+ """Handle incoming messages. Only for test."""
294
+ try:
295
+ print(f"Received message: {body.decode()}\n")
296
+ self.rabbitmq_message_cache.put({"properties": properties, "body": body})
297
+ print(f"message delivery_tag: {method.delivery_tag}\n")
298
+ channel.basic_ack(delivery_tag=method.delivery_tag)
299
+ except Exception as e:
300
+ logger.error(f"Message handling failed: {e}", exc_info=True)
301
+
302
+ def wait_for_connection_ready(self):
303
+ start_time = time.time()
304
+ while not self.is_rabbitmq_connected():
305
+ delta_time = time.time() - start_time
306
+ if delta_time > self.rabbitmq_conn_max_waiting_seconds:
307
+ logger.error("Failed to start consuming: Connection timeout")
308
+ return False
309
+ self.rabbit_reconnect()
310
+ time.sleep(self.rabbitmq_conn_sleep_seconds) # Reduced frequency of checks
311
+
312
+ # Message handling
313
+ def rabbitmq_start_consuming(self):
314
+ """Start consuming messages asynchronously."""
315
+ self.wait_for_connection_ready()
316
+
317
+ self.rabbitmq_channel.basic_consume(
318
+ queue=self.rabbit_queue_name,
319
+ on_message_callback=self.on_rabbitmq_message,
320
+ auto_ack=False,
321
+ )
322
+ logger.info("Started rabbitmq consuming messages")
323
+
324
+ def rabbitmq_publish_message(self, message: dict):
325
+ """
326
+ Publish a message to RabbitMQ.
327
+ """
328
+ import pika
329
+
330
+ exchange_name = self.rabbitmq_exchange_name
331
+ routing_key = self.rabbit_queue_name
332
+ label = message.get("label")
333
+
334
+ # Special handling for knowledgeBaseUpdate in local environment: always empty routing key
335
+ if label == "knowledgeBaseUpdate":
336
+ routing_key = ""
337
+
338
+ # Env override: apply to all message types when MEMSCHEDULER_RABBITMQ_EXCHANGE_NAME is set
339
+ env_exchange_name = os.getenv("MEMSCHEDULER_RABBITMQ_EXCHANGE_NAME")
340
+ env_routing_key = os.getenv("MEMSCHEDULER_RABBITMQ_ROUTING_KEY")
341
+ if env_exchange_name:
342
+ exchange_name = env_exchange_name
343
+ routing_key = (
344
+ env_routing_key if env_routing_key is not None and env_routing_key != "" else ""
345
+ )
346
+ logger.info(
347
+ f"[DIAGNOSTIC] Publishing {label} message with env exchange override. "
348
+ f"Exchange: {exchange_name}, Routing Key: '{routing_key}'."
349
+ )
350
+ logger.info(f" - Message Content: {json.dumps(message, indent=2, ensure_ascii=False)}")
351
+ elif label == "knowledgeBaseUpdate":
352
+ # Original diagnostic logging for knowledgeBaseUpdate if NOT in cloud env
353
+ logger.info(
354
+ f"[DIAGNOSTIC] Publishing knowledgeBaseUpdate message (Local Env). "
355
+ f"Current configured Exchange: {exchange_name}, Routing Key: '{routing_key}'."
356
+ )
357
+ logger.info(f" - Message Content: {json.dumps(message, indent=2, ensure_ascii=False)}")
358
+
359
+ with self._rabbitmq_lock:
360
+ logger.info(
361
+ f"[DIAGNOSTIC] rabbitmq_service.rabbitmq_publish_message invoked. "
362
+ f"is_connected={self.is_rabbitmq_connected()}, exchange={exchange_name}, "
363
+ f"routing_key='{routing_key}', label={label}"
364
+ )
365
+ if not self.is_rabbitmq_connected():
366
+ logger.error(
367
+ "[DIAGNOSTIC] Cannot publish - no active connection. Caching message for retry. "
368
+ f"connection_exists={bool(self.rabbitmq_connection)}, "
369
+ f"channel_exists={bool(self.rabbitmq_channel)}, "
370
+ f"config_loaded={self.rabbitmq_config is not None}"
371
+ )
372
+ self.rabbitmq_publish_cache.put(message)
373
+ # Best-effort to connect
374
+ self.initialize_rabbitmq(config=self.rabbitmq_config)
375
+ return False
376
+
377
+ logger.info(
378
+ f"[DIAGNOSTIC] rabbitmq_service.rabbitmq_publish_message: Attempting to publish message. Exchange: {exchange_name}, Routing Key: {routing_key}, Message Content: {json.dumps(message, indent=2, ensure_ascii=False)}"
379
+ )
380
+ try:
381
+ self.rabbitmq_channel.basic_publish(
382
+ exchange=exchange_name,
383
+ routing_key=routing_key,
384
+ body=json.dumps(message),
385
+ properties=pika.BasicProperties(
386
+ delivery_mode=2, # Persistent
387
+ ),
388
+ mandatory=True,
389
+ )
390
+ logger.debug(f"Published message: {message}")
391
+ return True
392
+ except Exception as e:
393
+ logger.error(
394
+ "[DIAGNOSTIC] RabbitMQ publish error. label=%s item_id=%s exchange=%s "
395
+ "routing_key=%s error=%s",
396
+ label,
397
+ message.get("item_id"),
398
+ exchange_name,
399
+ routing_key,
400
+ e,
401
+ )
402
+ logger.error(f"Failed to publish message: {e}")
403
+ # Cache message for retry on next connection
404
+ self.rabbitmq_publish_cache.put(message)
405
+ self.rabbit_reconnect()
406
+ return False
407
+
408
+ # Connection management
409
+ def rabbit_reconnect(self):
410
+ """Schedule reconnection attempt."""
411
+ logger.info("Attempting to reconnect...")
412
+ if self.rabbitmq_connection and not self.rabbitmq_connection.is_closed:
413
+ self.rabbitmq_connection.ioloop.stop()
414
+
415
+ # Reset connection state
416
+ self.rabbitmq_channel = None
417
+ self.initialize_rabbitmq()
418
+
419
+ def rabbitmq_close(self):
420
+ """Gracefully shutdown connection."""
421
+ with self._rabbitmq_lock:
422
+ self._rabbitmq_stop_flag = True
423
+
424
+ # Close channel if open
425
+ if self.rabbitmq_channel and self.rabbitmq_channel.is_open:
426
+ try:
427
+ self.rabbitmq_channel.close()
428
+ except Exception as e:
429
+ logger.warning(f"Error closing channel: {e}")
430
+
431
+ # Close connection if open
432
+ if self.rabbitmq_connection:
433
+ if self.rabbitmq_connection.is_open:
434
+ try:
435
+ self.rabbitmq_connection.close()
436
+ except Exception as e:
437
+ logger.warning(f"Error closing connection: {e}")
438
+
439
+ # Stop IOLoop if running
440
+ try:
441
+ self.rabbitmq_connection.ioloop.stop()
442
+ except Exception as e:
443
+ logger.warning(f"Error stopping IOLoop: {e}")
444
+
445
+ # Wait for IOLoop thread to finish
446
+ if self._io_loop_thread and self._io_loop_thread.is_alive():
447
+ self._io_loop_thread.join(timeout=5)
448
+ if self._io_loop_thread.is_alive():
449
+ logger.warning("IOLoop thread did not terminate cleanly")
450
+
451
+ logger.info("RabbitMQ connection closed")
452
+
453
+ def _flush_cached_publish_messages(self):
454
+ """Flush cached outgoing messages once connection is available."""
455
+ if self.rabbitmq_publish_cache.empty():
456
+ return
457
+
458
+ if not self.is_rabbitmq_connected():
459
+ logger.info(
460
+ "[DIAGNOSTIC] _flush_cached_publish_messages: connection still down; "
461
+ f"pending={self.rabbitmq_publish_cache.qsize()}"
462
+ )
463
+ return
464
+
465
+ drained: list[dict] = []
466
+ while True:
467
+ try:
468
+ drained.append(self.rabbitmq_publish_cache.get_nowait())
469
+ except Empty:
470
+ break
471
+
472
+ if not drained:
473
+ return
474
+
475
+ logger.info(
476
+ f"[DIAGNOSTIC] Flushing {len(drained)} cached RabbitMQ messages after reconnect."
477
+ )
478
+ for cached_msg in drained:
479
+ success = self.rabbitmq_publish_message(cached_msg)
480
+ if not success:
481
+ # Message already re-cached inside publish; avoid tight loop
482
+ logger.error(
483
+ "[DIAGNOSTIC] Failed to flush cached message; re-queued for next attempt."
484
+ )
485
+ break