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,380 @@
1
+ import asyncio
2
+ import os
3
+ import subprocess
4
+ import time
5
+
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from memos.context.context import ContextThread
10
+ from memos.dependency import require_python_package
11
+ from memos.log import get_logger
12
+ from memos.mem_scheduler.general_modules.base import BaseSchedulerModule
13
+
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class RedisSchedulerModule(BaseSchedulerModule):
19
+ @require_python_package(
20
+ import_name="redis",
21
+ install_command="pip install redis",
22
+ install_link="https://redis.readthedocs.io/en/stable/",
23
+ )
24
+ def __init__(self):
25
+ """
26
+ intent_detector: Object used for intent recognition (such as the above IntentDetector)
27
+ scheduler: The actual scheduling module/interface object
28
+ trigger_intents: The types of intents that need to be triggered (list)
29
+ """
30
+ super().__init__()
31
+
32
+ # settings for redis
33
+ self.redis_host: str | None = None
34
+ self.redis_port: int | None = None
35
+ self.redis_db: int | None = None
36
+ self.redis_password: str | None = None
37
+ self.socket_timeout: float | None = None
38
+ self.socket_connect_timeout: float | None = None
39
+ self._redis_conn = None
40
+ self._local_redis_process = None
41
+ self.query_list_capacity = 1000
42
+
43
+ self._redis_listener_running = False
44
+ self._redis_listener_thread: ContextThread | None = None
45
+ self._redis_listener_loop: asyncio.AbstractEventLoop | None = None
46
+
47
+ @property
48
+ def redis(self) -> Any:
49
+ if self._redis_conn is None:
50
+ self.auto_initialize_redis()
51
+ return self._redis_conn
52
+
53
+ @redis.setter
54
+ def redis(self, value: Any) -> None:
55
+ self._redis_conn = value
56
+
57
+ def initialize_redis(
58
+ self,
59
+ redis_host: str = "localhost",
60
+ redis_port: int = 6379,
61
+ redis_db: int = 0,
62
+ redis_password: str | None = None,
63
+ socket_timeout: float | None = None,
64
+ socket_connect_timeout: float | None = None,
65
+ ):
66
+ import redis
67
+
68
+ self.redis_host = redis_host
69
+ self.redis_port = redis_port
70
+ self.redis_db = redis_db
71
+ self.redis_password = redis_password
72
+ self.socket_timeout = socket_timeout
73
+ self.socket_connect_timeout = socket_connect_timeout
74
+
75
+ try:
76
+ logger.debug(f"Connecting to Redis at {redis_host}:{redis_port}/{redis_db}")
77
+ redis_kwargs = {
78
+ "host": self.redis_host,
79
+ "port": self.redis_port,
80
+ "db": self.redis_db,
81
+ "password": redis_password,
82
+ "decode_responses": True,
83
+ }
84
+
85
+ # Add timeout parameters if provided
86
+ if socket_timeout is not None:
87
+ redis_kwargs["socket_timeout"] = socket_timeout
88
+ if socket_connect_timeout is not None:
89
+ redis_kwargs["socket_connect_timeout"] = socket_connect_timeout
90
+
91
+ self._redis_conn = redis.Redis(**redis_kwargs)
92
+ # test conn
93
+ if not self._redis_conn.ping():
94
+ logger.error("Redis connection failed")
95
+ except redis.ConnectionError as e:
96
+ self._redis_conn = None
97
+ logger.error(f"Redis connection error: {e}")
98
+ self._redis_conn.xtrim("user:queries:stream", self.query_list_capacity)
99
+ return self._redis_conn
100
+
101
+ @require_python_package(
102
+ import_name="redis",
103
+ install_command="pip install redis",
104
+ install_link="https://redis.readthedocs.io/en/stable/",
105
+ )
106
+ def auto_initialize_redis(self) -> bool:
107
+ """
108
+ Auto-initialize Redis with fallback strategies:
109
+ 1. Try to initialize from config
110
+ 2. Try to initialize from environment variables
111
+ 3. Try to start local Redis server as fallback
112
+
113
+ Returns:
114
+ bool: True if Redis connection is successfully established, False otherwise
115
+ """
116
+ # Skip remote initialization in CI/pytest unless explicitly enabled
117
+ enable_env = os.getenv("MEMOS_ENABLE_REDIS", "").lower() == "true"
118
+ in_ci = os.getenv("CI", "").lower() == "true"
119
+ in_pytest = os.getenv("PYTEST_CURRENT_TEST") is not None
120
+ if (in_ci or in_pytest) and not enable_env:
121
+ logger.info(
122
+ "Skipping Redis auto-initialization in CI/test environment. Set MEMOS_ENABLE_REDIS=true to enable."
123
+ )
124
+ return False
125
+
126
+ import redis
127
+
128
+ # Strategy 1: Try to initialize from config
129
+ if hasattr(self, "config") and hasattr(self.config, "redis_config"):
130
+ try:
131
+ redis_config = self.config.redis_config
132
+ logger.info("Attempting to initialize Redis from config")
133
+
134
+ self._redis_conn = redis.Redis(
135
+ host=redis_config.get("host", "localhost"),
136
+ port=redis_config.get("port", 6379),
137
+ db=redis_config.get("db", 0),
138
+ password=redis_config.get("password", None),
139
+ decode_responses=True,
140
+ )
141
+
142
+ # Test connection
143
+ if self._redis_conn.ping():
144
+ logger.info("Redis initialized successfully from config")
145
+ self.redis_host = redis_config.get("host", "localhost")
146
+ self.redis_port = redis_config.get("port", 6379)
147
+ self.redis_db = redis_config.get("db", 0)
148
+ self.redis_password = redis_config.get("password", None)
149
+ self.socket_timeout = redis_config.get("socket_timeout", None)
150
+ self.socket_connect_timeout = redis_config.get("socket_connect_timeout", None)
151
+ return True
152
+ else:
153
+ logger.warning("Redis config connection test failed")
154
+ self._redis_conn = None
155
+ except Exception as e:
156
+ logger.warning(f"Failed to initialize Redis from config: {e}")
157
+ self._redis_conn = None
158
+
159
+ # Strategy 2: Try to initialize from environment variables
160
+ try:
161
+ redis_host = os.getenv("MEMSCHEDULER_REDIS_HOST", "localhost")
162
+ redis_port = int(os.getenv("MEMSCHEDULER_REDIS_PORT", "6379"))
163
+ redis_db = int(os.getenv("MEMSCHEDULER_REDIS_DB", "0"))
164
+ redis_password = os.getenv("MEMSCHEDULER_REDIS_PASSWORD", None)
165
+ socket_timeout = os.getenv("MEMSCHEDULER_REDIS_TIMEOUT", None)
166
+ socket_connect_timeout = os.getenv("MEMSCHEDULER_REDIS_CONNECT_TIMEOUT", None)
167
+
168
+ logger.info(
169
+ f"Attempting to initialize Redis from environment variables: {redis_host}:{redis_port}"
170
+ )
171
+
172
+ redis_kwargs = {
173
+ "host": redis_host,
174
+ "port": redis_port,
175
+ "db": redis_db,
176
+ "password": redis_password,
177
+ "decode_responses": True,
178
+ }
179
+
180
+ # Add timeout parameters if provided
181
+ if socket_timeout is not None:
182
+ try:
183
+ redis_kwargs["socket_timeout"] = float(socket_timeout)
184
+ except ValueError:
185
+ logger.warning(
186
+ f"Invalid MEMSCHEDULER_REDIS_TIMEOUT value: {socket_timeout}, ignoring"
187
+ )
188
+
189
+ if socket_connect_timeout is not None:
190
+ try:
191
+ redis_kwargs["socket_connect_timeout"] = float(socket_connect_timeout)
192
+ except ValueError:
193
+ logger.warning(
194
+ f"Invalid MEMSCHEDULER_REDIS_CONNECT_TIMEOUT value: {socket_connect_timeout}, ignoring"
195
+ )
196
+
197
+ self._redis_conn = redis.Redis(**redis_kwargs)
198
+
199
+ # Test connection
200
+ if self._redis_conn.ping():
201
+ logger.info("Redis initialized successfully from environment variables")
202
+ self.redis_host = redis_host
203
+ self.redis_port = redis_port
204
+ self.redis_db = redis_db
205
+ self.redis_password = redis_password
206
+ self.socket_timeout = float(socket_timeout) if socket_timeout is not None else None
207
+ self.socket_connect_timeout = (
208
+ float(socket_connect_timeout) if socket_connect_timeout is not None else None
209
+ )
210
+ return True
211
+ else:
212
+ logger.warning("Redis environment connection test failed")
213
+ self._redis_conn = None
214
+ except Exception as e:
215
+ logger.warning(f"Failed to initialize Redis from environment variables: {e}")
216
+ self._redis_conn = None
217
+
218
+ # Strategy 3: Try to start local Redis server as fallback
219
+ try:
220
+ logger.warning(
221
+ "Attempting to start local Redis server as fallback (not recommended for production)"
222
+ )
223
+
224
+ # Try to start Redis server locally
225
+ self._local_redis_process = subprocess.Popen(
226
+ ["redis-server", "--port", "6379", "--daemonize", "no"],
227
+ stdout=subprocess.PIPE,
228
+ stderr=subprocess.PIPE,
229
+ preexec_fn=os.setsid if hasattr(os, "setsid") else None,
230
+ )
231
+
232
+ # Wait a moment for Redis to start
233
+ time.sleep(0.5)
234
+
235
+ # Try to connect to local Redis
236
+ self._redis_conn = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
237
+
238
+ # Test connection
239
+ if self._redis_conn.ping():
240
+ logger.warning("Local Redis server started and connected successfully")
241
+ logger.warning("WARNING: Using local Redis server - not suitable for production!")
242
+ self.redis_host = "localhost"
243
+ self.redis_port = 6379
244
+ self.redis_db = 0
245
+ self.redis_password = None
246
+ self.socket_timeout = None
247
+ self.socket_connect_timeout = None
248
+ return True
249
+ else:
250
+ logger.error("Local Redis server connection test failed")
251
+ self._cleanup_local_redis()
252
+ return False
253
+
254
+ except Exception as e:
255
+ logger.error(f"Failed to start local Redis server: {e}")
256
+ self._cleanup_local_redis()
257
+ return False
258
+
259
+ def _cleanup_local_redis(self):
260
+ """Clean up local Redis process if it exists"""
261
+ if self._local_redis_process:
262
+ try:
263
+ self._local_redis_process.terminate()
264
+ self._local_redis_process.wait(timeout=5)
265
+ logger.info("Local Redis process terminated")
266
+ except subprocess.TimeoutExpired:
267
+ logger.warning("Local Redis process did not terminate gracefully, killing it")
268
+ self._local_redis_process.kill()
269
+ self._local_redis_process.wait()
270
+ except Exception as e:
271
+ logger.error(f"Error cleaning up local Redis process: {e}")
272
+ finally:
273
+ self._local_redis_process = None
274
+
275
+ def _cleanup_redis_resources(self):
276
+ """Clean up Redis connection and local process"""
277
+ if self._redis_conn:
278
+ try:
279
+ self._redis_conn.close()
280
+ logger.info("Redis connection closed")
281
+ except Exception as e:
282
+ logger.error(f"Error closing Redis connection: {e}")
283
+ finally:
284
+ self._redis_conn = None
285
+
286
+ self._cleanup_local_redis()
287
+
288
+ def redis_add_message_stream(self, message: dict):
289
+ logger.debug(f"add_message_stream: {message}")
290
+ return self._redis_conn.xadd("user:queries:stream", message)
291
+
292
+ async def redis_consume_message_stream(self, message: dict):
293
+ logger.debug(f"consume_message_stream: {message}")
294
+
295
+ def _redis_run_listener_async(self, handler: Callable):
296
+ """Run the async listener in a separate thread"""
297
+ self._redis_listener_loop = asyncio.new_event_loop()
298
+ asyncio.set_event_loop(self._redis_listener_loop)
299
+
300
+ async def listener_wrapper():
301
+ try:
302
+ await self.__redis_listen_query_stream(handler)
303
+ except Exception as e:
304
+ logger.error(f"Listener thread error: {e}")
305
+ finally:
306
+ self._redis_listener_running = False
307
+
308
+ self._redis_listener_loop.run_until_complete(listener_wrapper())
309
+
310
+ async def __redis_listen_query_stream(
311
+ self, handler=None, last_id: str = "$", block_time: int = 2000
312
+ ):
313
+ """Internal async stream listener"""
314
+ import redis
315
+
316
+ self._redis_listener_running = True
317
+ while self._redis_listener_running:
318
+ try:
319
+ # Blocking read for new messages
320
+ messages = self.redis.xread(
321
+ {"user:queries:stream": last_id}, count=1, block=block_time
322
+ )
323
+
324
+ if messages:
325
+ for _, stream_messages in messages:
326
+ for message_id, message_data in stream_messages:
327
+ try:
328
+ print(f"deal with message_data {message_data}")
329
+ await handler(message_data)
330
+ last_id = message_id
331
+ except Exception as e:
332
+ logger.error(f"Error processing message {message_id}: {e}")
333
+
334
+ except redis.ConnectionError as e:
335
+ logger.error(f"Redis connection error: {e}")
336
+ await asyncio.sleep(5) # Wait before reconnecting
337
+ self._redis_conn = None # Force reconnection
338
+ except Exception as e:
339
+ logger.error(f"Unexpected error: {e}")
340
+ await asyncio.sleep(1)
341
+
342
+ def redis_start_listening(self, handler: Callable | None = None):
343
+ """Start the Redis stream listener in a background thread"""
344
+ if self._redis_listener_thread and self._redis_listener_thread.is_alive():
345
+ logger.warning("Listener is already running")
346
+ return
347
+
348
+ # Check Redis connection before starting listener
349
+ if self.redis is None:
350
+ logger.warning(
351
+ "Redis connection is None, attempting to auto-initialize before starting listener..."
352
+ )
353
+ if not self.auto_initialize_redis():
354
+ logger.error("Failed to initialize Redis connection, cannot start listener")
355
+ return
356
+
357
+ if handler is None:
358
+ handler = self.redis_consume_message_stream
359
+
360
+ self._redis_listener_thread = ContextThread(
361
+ target=self._redis_run_listener_async,
362
+ args=(handler,),
363
+ daemon=True,
364
+ name="RedisListenerThread",
365
+ )
366
+ self._redis_listener_thread.start()
367
+ logger.info("Started Redis stream listener thread")
368
+
369
+ def redis_stop_listening(self):
370
+ """Stop the listener thread gracefully"""
371
+ self._redis_listener_running = False
372
+ if self._redis_listener_thread and self._redis_listener_thread.is_alive():
373
+ self._redis_listener_thread.join(timeout=5.0)
374
+ if self._redis_listener_thread.is_alive():
375
+ logger.warning("Listener thread did not stop gracefully")
376
+ logger.info("Redis stream listener stopped")
377
+
378
+ def redis_close(self):
379
+ """Close Redis connection and clean up resources"""
380
+ self._cleanup_redis_resources()
@@ -0,0 +1,94 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from memos.configs.mem_user import UserManagerConfigFactory
4
+ from memos.mem_user.mysql_user_manager import MySQLUserManager
5
+ from memos.mem_user.user_manager import UserManager
6
+
7
+
8
+ class UserManagerFactory:
9
+ """Factory class for creating user manager instances."""
10
+
11
+ backend_to_class: ClassVar[dict[str, Any]] = {
12
+ "sqlite": UserManager,
13
+ "mysql": MySQLUserManager,
14
+ }
15
+
16
+ @classmethod
17
+ def from_config(
18
+ cls, config_factory: UserManagerConfigFactory
19
+ ) -> UserManager | MySQLUserManager:
20
+ """Create a user manager instance from configuration.
21
+
22
+ Args:
23
+ config_factory: Configuration factory containing backend and config
24
+
25
+ Returns:
26
+ User manager instance
27
+
28
+ Raises:
29
+ ValueError: If backend is not supported
30
+ """
31
+ backend = config_factory.backend
32
+ if backend not in cls.backend_to_class:
33
+ raise ValueError(f"Invalid user manager backend: {backend}")
34
+
35
+ user_manager_class = cls.backend_to_class[backend]
36
+ config = config_factory.config
37
+
38
+ # Use model_dump() to convert Pydantic model to dict and unpack as kwargs
39
+ return user_manager_class(**config.model_dump())
40
+
41
+ @classmethod
42
+ def create_sqlite(cls, db_path: str | None = None, user_id: str = "root") -> UserManager:
43
+ """Create SQLite user manager with default configuration.
44
+
45
+ Args:
46
+ db_path: Path to SQLite database file
47
+ user_id: Default user ID for initialization
48
+
49
+ Returns:
50
+ SQLite user manager instance
51
+ """
52
+ config_factory = UserManagerConfigFactory(
53
+ backend="sqlite", config={"db_path": db_path, "user_id": user_id}
54
+ )
55
+ return cls.from_config(config_factory)
56
+
57
+ @classmethod
58
+ def create_mysql(
59
+ cls,
60
+ user_id: str = "root",
61
+ host: str = "localhost",
62
+ port: int = 3306,
63
+ username: str = "root",
64
+ password: str = "",
65
+ database: str = "memos_users",
66
+ charset: str = "utf8mb4",
67
+ ) -> MySQLUserManager:
68
+ """Create MySQL user manager with specified configuration.
69
+
70
+ Args:
71
+ user_id: Default user ID for initialization
72
+ host: MySQL server host
73
+ port: MySQL server port
74
+ username: MySQL username
75
+ password: MySQL password
76
+ database: MySQL database name
77
+ charset: MySQL charset
78
+
79
+ Returns:
80
+ MySQL user manager instance
81
+ """
82
+ config_factory = UserManagerConfigFactory(
83
+ backend="mysql",
84
+ config={
85
+ "user_id": user_id,
86
+ "host": host,
87
+ "port": port,
88
+ "username": username,
89
+ "password": password,
90
+ "database": database,
91
+ "charset": charset,
92
+ },
93
+ )
94
+ return cls.from_config(config_factory)