MemoryOS 2.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. memoryos-2.0.3.dist-info/METADATA +418 -0
  2. memoryos-2.0.3.dist-info/RECORD +315 -0
  3. memoryos-2.0.3.dist-info/WHEEL +4 -0
  4. memoryos-2.0.3.dist-info/entry_points.txt +3 -0
  5. memoryos-2.0.3.dist-info/licenses/LICENSE +201 -0
  6. memos/__init__.py +20 -0
  7. memos/api/client.py +571 -0
  8. memos/api/config.py +1018 -0
  9. memos/api/context/dependencies.py +50 -0
  10. memos/api/exceptions.py +53 -0
  11. memos/api/handlers/__init__.py +62 -0
  12. memos/api/handlers/add_handler.py +158 -0
  13. memos/api/handlers/base_handler.py +194 -0
  14. memos/api/handlers/chat_handler.py +1401 -0
  15. memos/api/handlers/component_init.py +388 -0
  16. memos/api/handlers/config_builders.py +190 -0
  17. memos/api/handlers/feedback_handler.py +93 -0
  18. memos/api/handlers/formatters_handler.py +237 -0
  19. memos/api/handlers/memory_handler.py +316 -0
  20. memos/api/handlers/scheduler_handler.py +497 -0
  21. memos/api/handlers/search_handler.py +222 -0
  22. memos/api/handlers/suggestion_handler.py +117 -0
  23. memos/api/mcp_serve.py +614 -0
  24. memos/api/middleware/request_context.py +101 -0
  25. memos/api/product_api.py +38 -0
  26. memos/api/product_models.py +1206 -0
  27. memos/api/routers/__init__.py +1 -0
  28. memos/api/routers/product_router.py +477 -0
  29. memos/api/routers/server_router.py +394 -0
  30. memos/api/server_api.py +44 -0
  31. memos/api/start_api.py +433 -0
  32. memos/chunkers/__init__.py +4 -0
  33. memos/chunkers/base.py +24 -0
  34. memos/chunkers/charactertext_chunker.py +41 -0
  35. memos/chunkers/factory.py +24 -0
  36. memos/chunkers/markdown_chunker.py +62 -0
  37. memos/chunkers/sentence_chunker.py +54 -0
  38. memos/chunkers/simple_chunker.py +50 -0
  39. memos/cli.py +113 -0
  40. memos/configs/__init__.py +0 -0
  41. memos/configs/base.py +82 -0
  42. memos/configs/chunker.py +59 -0
  43. memos/configs/embedder.py +88 -0
  44. memos/configs/graph_db.py +236 -0
  45. memos/configs/internet_retriever.py +100 -0
  46. memos/configs/llm.py +151 -0
  47. memos/configs/mem_agent.py +54 -0
  48. memos/configs/mem_chat.py +81 -0
  49. memos/configs/mem_cube.py +105 -0
  50. memos/configs/mem_os.py +83 -0
  51. memos/configs/mem_reader.py +91 -0
  52. memos/configs/mem_scheduler.py +385 -0
  53. memos/configs/mem_user.py +70 -0
  54. memos/configs/memory.py +324 -0
  55. memos/configs/parser.py +38 -0
  56. memos/configs/reranker.py +18 -0
  57. memos/configs/utils.py +8 -0
  58. memos/configs/vec_db.py +80 -0
  59. memos/context/context.py +355 -0
  60. memos/dependency.py +52 -0
  61. memos/deprecation.py +262 -0
  62. memos/embedders/__init__.py +0 -0
  63. memos/embedders/ark.py +95 -0
  64. memos/embedders/base.py +106 -0
  65. memos/embedders/factory.py +29 -0
  66. memos/embedders/ollama.py +77 -0
  67. memos/embedders/sentence_transformer.py +49 -0
  68. memos/embedders/universal_api.py +51 -0
  69. memos/exceptions.py +30 -0
  70. memos/graph_dbs/__init__.py +0 -0
  71. memos/graph_dbs/base.py +274 -0
  72. memos/graph_dbs/factory.py +27 -0
  73. memos/graph_dbs/item.py +46 -0
  74. memos/graph_dbs/nebular.py +1794 -0
  75. memos/graph_dbs/neo4j.py +1942 -0
  76. memos/graph_dbs/neo4j_community.py +1058 -0
  77. memos/graph_dbs/polardb.py +5446 -0
  78. memos/hello_world.py +97 -0
  79. memos/llms/__init__.py +0 -0
  80. memos/llms/base.py +25 -0
  81. memos/llms/deepseek.py +13 -0
  82. memos/llms/factory.py +38 -0
  83. memos/llms/hf.py +443 -0
  84. memos/llms/hf_singleton.py +114 -0
  85. memos/llms/ollama.py +135 -0
  86. memos/llms/openai.py +222 -0
  87. memos/llms/openai_new.py +198 -0
  88. memos/llms/qwen.py +13 -0
  89. memos/llms/utils.py +14 -0
  90. memos/llms/vllm.py +218 -0
  91. memos/log.py +237 -0
  92. memos/mem_agent/base.py +19 -0
  93. memos/mem_agent/deepsearch_agent.py +391 -0
  94. memos/mem_agent/factory.py +36 -0
  95. memos/mem_chat/__init__.py +0 -0
  96. memos/mem_chat/base.py +30 -0
  97. memos/mem_chat/factory.py +21 -0
  98. memos/mem_chat/simple.py +200 -0
  99. memos/mem_cube/__init__.py +0 -0
  100. memos/mem_cube/base.py +30 -0
  101. memos/mem_cube/general.py +240 -0
  102. memos/mem_cube/navie.py +172 -0
  103. memos/mem_cube/utils.py +169 -0
  104. memos/mem_feedback/base.py +15 -0
  105. memos/mem_feedback/feedback.py +1192 -0
  106. memos/mem_feedback/simple_feedback.py +40 -0
  107. memos/mem_feedback/utils.py +230 -0
  108. memos/mem_os/client.py +5 -0
  109. memos/mem_os/core.py +1203 -0
  110. memos/mem_os/main.py +582 -0
  111. memos/mem_os/product.py +1608 -0
  112. memos/mem_os/product_server.py +455 -0
  113. memos/mem_os/utils/default_config.py +359 -0
  114. memos/mem_os/utils/format_utils.py +1403 -0
  115. memos/mem_os/utils/reference_utils.py +162 -0
  116. memos/mem_reader/__init__.py +0 -0
  117. memos/mem_reader/base.py +47 -0
  118. memos/mem_reader/factory.py +53 -0
  119. memos/mem_reader/memory.py +298 -0
  120. memos/mem_reader/multi_modal_struct.py +965 -0
  121. memos/mem_reader/read_multi_modal/__init__.py +43 -0
  122. memos/mem_reader/read_multi_modal/assistant_parser.py +311 -0
  123. memos/mem_reader/read_multi_modal/base.py +273 -0
  124. memos/mem_reader/read_multi_modal/file_content_parser.py +826 -0
  125. memos/mem_reader/read_multi_modal/image_parser.py +359 -0
  126. memos/mem_reader/read_multi_modal/multi_modal_parser.py +252 -0
  127. memos/mem_reader/read_multi_modal/string_parser.py +139 -0
  128. memos/mem_reader/read_multi_modal/system_parser.py +327 -0
  129. memos/mem_reader/read_multi_modal/text_content_parser.py +131 -0
  130. memos/mem_reader/read_multi_modal/tool_parser.py +210 -0
  131. memos/mem_reader/read_multi_modal/user_parser.py +218 -0
  132. memos/mem_reader/read_multi_modal/utils.py +358 -0
  133. memos/mem_reader/simple_struct.py +912 -0
  134. memos/mem_reader/strategy_struct.py +163 -0
  135. memos/mem_reader/utils.py +157 -0
  136. memos/mem_scheduler/__init__.py +0 -0
  137. memos/mem_scheduler/analyzer/__init__.py +0 -0
  138. memos/mem_scheduler/analyzer/api_analyzer.py +714 -0
  139. memos/mem_scheduler/analyzer/eval_analyzer.py +219 -0
  140. memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +571 -0
  141. memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
  142. memos/mem_scheduler/base_scheduler.py +1319 -0
  143. memos/mem_scheduler/general_modules/__init__.py +0 -0
  144. memos/mem_scheduler/general_modules/api_misc.py +137 -0
  145. memos/mem_scheduler/general_modules/base.py +80 -0
  146. memos/mem_scheduler/general_modules/init_components_for_scheduler.py +425 -0
  147. memos/mem_scheduler/general_modules/misc.py +313 -0
  148. memos/mem_scheduler/general_modules/scheduler_logger.py +389 -0
  149. memos/mem_scheduler/general_modules/task_threads.py +315 -0
  150. memos/mem_scheduler/general_scheduler.py +1495 -0
  151. memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
  152. memos/mem_scheduler/memory_manage_modules/memory_filter.py +306 -0
  153. memos/mem_scheduler/memory_manage_modules/retriever.py +547 -0
  154. memos/mem_scheduler/monitors/__init__.py +0 -0
  155. memos/mem_scheduler/monitors/dispatcher_monitor.py +366 -0
  156. memos/mem_scheduler/monitors/general_monitor.py +394 -0
  157. memos/mem_scheduler/monitors/task_schedule_monitor.py +254 -0
  158. memos/mem_scheduler/optimized_scheduler.py +410 -0
  159. memos/mem_scheduler/orm_modules/__init__.py +0 -0
  160. memos/mem_scheduler/orm_modules/api_redis_model.py +518 -0
  161. memos/mem_scheduler/orm_modules/base_model.py +729 -0
  162. memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
  163. memos/mem_scheduler/orm_modules/redis_model.py +699 -0
  164. memos/mem_scheduler/scheduler_factory.py +23 -0
  165. memos/mem_scheduler/schemas/__init__.py +0 -0
  166. memos/mem_scheduler/schemas/analyzer_schemas.py +52 -0
  167. memos/mem_scheduler/schemas/api_schemas.py +233 -0
  168. memos/mem_scheduler/schemas/general_schemas.py +55 -0
  169. memos/mem_scheduler/schemas/message_schemas.py +173 -0
  170. memos/mem_scheduler/schemas/monitor_schemas.py +406 -0
  171. memos/mem_scheduler/schemas/task_schemas.py +132 -0
  172. memos/mem_scheduler/task_schedule_modules/__init__.py +0 -0
  173. memos/mem_scheduler/task_schedule_modules/dispatcher.py +740 -0
  174. memos/mem_scheduler/task_schedule_modules/local_queue.py +247 -0
  175. memos/mem_scheduler/task_schedule_modules/orchestrator.py +74 -0
  176. memos/mem_scheduler/task_schedule_modules/redis_queue.py +1385 -0
  177. memos/mem_scheduler/task_schedule_modules/task_queue.py +162 -0
  178. memos/mem_scheduler/utils/__init__.py +0 -0
  179. memos/mem_scheduler/utils/api_utils.py +77 -0
  180. memos/mem_scheduler/utils/config_utils.py +100 -0
  181. memos/mem_scheduler/utils/db_utils.py +50 -0
  182. memos/mem_scheduler/utils/filter_utils.py +176 -0
  183. memos/mem_scheduler/utils/metrics.py +125 -0
  184. memos/mem_scheduler/utils/misc_utils.py +290 -0
  185. memos/mem_scheduler/utils/monitor_event_utils.py +67 -0
  186. memos/mem_scheduler/utils/status_tracker.py +229 -0
  187. memos/mem_scheduler/webservice_modules/__init__.py +0 -0
  188. memos/mem_scheduler/webservice_modules/rabbitmq_service.py +485 -0
  189. memos/mem_scheduler/webservice_modules/redis_service.py +380 -0
  190. memos/mem_user/factory.py +94 -0
  191. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  192. memos/mem_user/mysql_user_manager.py +502 -0
  193. memos/mem_user/persistent_factory.py +98 -0
  194. memos/mem_user/persistent_user_manager.py +260 -0
  195. memos/mem_user/redis_persistent_user_manager.py +225 -0
  196. memos/mem_user/user_manager.py +488 -0
  197. memos/memories/__init__.py +0 -0
  198. memos/memories/activation/__init__.py +0 -0
  199. memos/memories/activation/base.py +42 -0
  200. memos/memories/activation/item.py +56 -0
  201. memos/memories/activation/kv.py +292 -0
  202. memos/memories/activation/vllmkv.py +219 -0
  203. memos/memories/base.py +19 -0
  204. memos/memories/factory.py +42 -0
  205. memos/memories/parametric/__init__.py +0 -0
  206. memos/memories/parametric/base.py +19 -0
  207. memos/memories/parametric/item.py +11 -0
  208. memos/memories/parametric/lora.py +41 -0
  209. memos/memories/textual/__init__.py +0 -0
  210. memos/memories/textual/base.py +92 -0
  211. memos/memories/textual/general.py +236 -0
  212. memos/memories/textual/item.py +304 -0
  213. memos/memories/textual/naive.py +187 -0
  214. memos/memories/textual/prefer_text_memory/__init__.py +0 -0
  215. memos/memories/textual/prefer_text_memory/adder.py +504 -0
  216. memos/memories/textual/prefer_text_memory/config.py +106 -0
  217. memos/memories/textual/prefer_text_memory/extractor.py +221 -0
  218. memos/memories/textual/prefer_text_memory/factory.py +85 -0
  219. memos/memories/textual/prefer_text_memory/retrievers.py +177 -0
  220. memos/memories/textual/prefer_text_memory/spliter.py +132 -0
  221. memos/memories/textual/prefer_text_memory/utils.py +93 -0
  222. memos/memories/textual/preference.py +344 -0
  223. memos/memories/textual/simple_preference.py +161 -0
  224. memos/memories/textual/simple_tree.py +69 -0
  225. memos/memories/textual/tree.py +459 -0
  226. memos/memories/textual/tree_text_memory/__init__.py +0 -0
  227. memos/memories/textual/tree_text_memory/organize/__init__.py +0 -0
  228. memos/memories/textual/tree_text_memory/organize/handler.py +184 -0
  229. memos/memories/textual/tree_text_memory/organize/manager.py +518 -0
  230. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +238 -0
  231. memos/memories/textual/tree_text_memory/organize/reorganizer.py +622 -0
  232. memos/memories/textual/tree_text_memory/retrieve/__init__.py +0 -0
  233. memos/memories/textual/tree_text_memory/retrieve/advanced_searcher.py +364 -0
  234. memos/memories/textual/tree_text_memory/retrieve/bm25_util.py +186 -0
  235. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +419 -0
  236. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +270 -0
  237. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +102 -0
  238. memos/memories/textual/tree_text_memory/retrieve/reasoner.py +61 -0
  239. memos/memories/textual/tree_text_memory/retrieve/recall.py +497 -0
  240. memos/memories/textual/tree_text_memory/retrieve/reranker.py +111 -0
  241. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +16 -0
  242. memos/memories/textual/tree_text_memory/retrieve/retrieve_utils.py +472 -0
  243. memos/memories/textual/tree_text_memory/retrieve/searcher.py +848 -0
  244. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +135 -0
  245. memos/memories/textual/tree_text_memory/retrieve/utils.py +54 -0
  246. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +387 -0
  247. memos/memos_tools/dinding_report_bot.py +453 -0
  248. memos/memos_tools/lockfree_dict.py +120 -0
  249. memos/memos_tools/notification_service.py +44 -0
  250. memos/memos_tools/notification_utils.py +142 -0
  251. memos/memos_tools/singleton.py +174 -0
  252. memos/memos_tools/thread_safe_dict.py +310 -0
  253. memos/memos_tools/thread_safe_dict_segment.py +382 -0
  254. memos/multi_mem_cube/__init__.py +0 -0
  255. memos/multi_mem_cube/composite_cube.py +86 -0
  256. memos/multi_mem_cube/single_cube.py +874 -0
  257. memos/multi_mem_cube/views.py +54 -0
  258. memos/parsers/__init__.py +0 -0
  259. memos/parsers/base.py +15 -0
  260. memos/parsers/factory.py +21 -0
  261. memos/parsers/markitdown.py +28 -0
  262. memos/reranker/__init__.py +4 -0
  263. memos/reranker/base.py +25 -0
  264. memos/reranker/concat.py +103 -0
  265. memos/reranker/cosine_local.py +102 -0
  266. memos/reranker/factory.py +72 -0
  267. memos/reranker/http_bge.py +324 -0
  268. memos/reranker/http_bge_strategy.py +327 -0
  269. memos/reranker/noop.py +19 -0
  270. memos/reranker/strategies/__init__.py +4 -0
  271. memos/reranker/strategies/base.py +61 -0
  272. memos/reranker/strategies/concat_background.py +94 -0
  273. memos/reranker/strategies/concat_docsource.py +110 -0
  274. memos/reranker/strategies/dialogue_common.py +109 -0
  275. memos/reranker/strategies/factory.py +31 -0
  276. memos/reranker/strategies/single_turn.py +107 -0
  277. memos/reranker/strategies/singleturn_outmem.py +98 -0
  278. memos/settings.py +10 -0
  279. memos/templates/__init__.py +0 -0
  280. memos/templates/advanced_search_prompts.py +211 -0
  281. memos/templates/cloud_service_prompt.py +107 -0
  282. memos/templates/instruction_completion.py +66 -0
  283. memos/templates/mem_agent_prompts.py +85 -0
  284. memos/templates/mem_feedback_prompts.py +822 -0
  285. memos/templates/mem_reader_prompts.py +1096 -0
  286. memos/templates/mem_reader_strategy_prompts.py +238 -0
  287. memos/templates/mem_scheduler_prompts.py +626 -0
  288. memos/templates/mem_search_prompts.py +93 -0
  289. memos/templates/mos_prompts.py +403 -0
  290. memos/templates/prefer_complete_prompt.py +735 -0
  291. memos/templates/tool_mem_prompts.py +139 -0
  292. memos/templates/tree_reorganize_prompts.py +230 -0
  293. memos/types/__init__.py +34 -0
  294. memos/types/general_types.py +151 -0
  295. memos/types/openai_chat_completion_types/__init__.py +15 -0
  296. memos/types/openai_chat_completion_types/chat_completion_assistant_message_param.py +56 -0
  297. memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py +27 -0
  298. memos/types/openai_chat_completion_types/chat_completion_content_part_input_audio_param.py +23 -0
  299. memos/types/openai_chat_completion_types/chat_completion_content_part_param.py +43 -0
  300. memos/types/openai_chat_completion_types/chat_completion_content_part_refusal_param.py +16 -0
  301. memos/types/openai_chat_completion_types/chat_completion_content_part_text_param.py +16 -0
  302. memos/types/openai_chat_completion_types/chat_completion_message_custom_tool_call_param.py +27 -0
  303. memos/types/openai_chat_completion_types/chat_completion_message_function_tool_call_param.py +32 -0
  304. memos/types/openai_chat_completion_types/chat_completion_message_param.py +18 -0
  305. memos/types/openai_chat_completion_types/chat_completion_message_tool_call_union_param.py +15 -0
  306. memos/types/openai_chat_completion_types/chat_completion_system_message_param.py +36 -0
  307. memos/types/openai_chat_completion_types/chat_completion_tool_message_param.py +30 -0
  308. memos/types/openai_chat_completion_types/chat_completion_user_message_param.py +34 -0
  309. memos/utils.py +123 -0
  310. memos/vec_dbs/__init__.py +0 -0
  311. memos/vec_dbs/base.py +117 -0
  312. memos/vec_dbs/factory.py +23 -0
  313. memos/vec_dbs/item.py +50 -0
  314. memos/vec_dbs/milvus.py +654 -0
  315. memos/vec_dbs/qdrant.py +355 -0
@@ -0,0 +1,518 @@
1
+ import os
2
+ import time
3
+
4
+ from typing import Any
5
+
6
+ from sqlalchemy.orm import declarative_base
7
+
8
+ from memos.log import get_logger
9
+ from memos.mem_scheduler.orm_modules.base_model import DatabaseError
10
+ from memos.mem_scheduler.schemas.api_schemas import (
11
+ APISearchHistoryManager,
12
+ )
13
+ from memos.mem_scheduler.utils.db_utils import get_utc_now
14
+
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ Base = declarative_base()
19
+
20
+
21
+ class APIRedisDBManager:
22
+ """Redis-based database manager for any serializable object
23
+
24
+ This class handles persistence, synchronization, and locking
25
+ for any object that implements to_json/from_json methods using Redis as the backend storage.
26
+ """
27
+
28
+ # Add orm_class attribute for compatibility
29
+ orm_class = None
30
+
31
+ def __init__(
32
+ self,
33
+ user_id: str | None = None,
34
+ mem_cube_id: str | None = None,
35
+ obj: APISearchHistoryManager | None = None,
36
+ lock_timeout: int = 10,
37
+ redis_client=None,
38
+ redis_config: dict | None = None,
39
+ window_size: int = 5,
40
+ ):
41
+ """Initialize the Redis database manager
42
+
43
+ Args:
44
+ user_id: Unique identifier for the user
45
+ mem_cube_id: Unique identifier for the memory cube
46
+ obj: Optional object instance to manage (must have to_json/from_json methods)
47
+ lock_timeout: Timeout in seconds for lock acquisition
48
+ redis_client: Redis client instance (optional)
49
+ redis_config: Redis configuration dictionary (optional)
50
+ """
51
+ # Initialize Redis client
52
+ self.redis_client = redis_client
53
+ self.redis_config = redis_config or {}
54
+
55
+ if self.redis_client is None:
56
+ self._init_redis_client()
57
+
58
+ # Initialize base attributes without calling parent's init_manager
59
+ self.user_id = user_id
60
+ self.mem_cube_id = mem_cube_id
61
+ self.obj = obj
62
+ self.lock_timeout = lock_timeout
63
+ self.engine = None # Keep for compatibility but not used
64
+ self.SessionLocal = None # Not used for Redis
65
+ self.window_size = window_size
66
+ self.lock_key = f"{self._get_key_prefix()}:lock"
67
+
68
+ logger.info(
69
+ f"RedisDBManager initialized for user_id: {user_id}, mem_cube_id: {mem_cube_id}"
70
+ )
71
+ logger.info(f"Redis client: {type(self.redis_client).__name__}")
72
+
73
+ # Test Redis connection
74
+ try:
75
+ self.redis_client.ping()
76
+ logger.info("Redis connection successful")
77
+ except Exception as e:
78
+ logger.warning(f"Redis ping failed: {e}")
79
+ # Don't raise error here as it might be a mock client in tests
80
+
81
+ def _get_key_prefix(self) -> str:
82
+ """Generate Redis key prefix for this user and memory cube
83
+
84
+ Returns:
85
+ Redis key prefix string
86
+ """
87
+ return f"redis_api:{self.user_id}:{self.mem_cube_id}"
88
+
89
+ def _get_data_key(self) -> str:
90
+ """Generate Redis key for storing serialized data
91
+
92
+ Returns:
93
+ Redis data key string
94
+ """
95
+ return f"{self._get_key_prefix()}:data"
96
+
97
+ def _init_redis_client(self):
98
+ """Initialize Redis client from config or environment"""
99
+ try:
100
+ import redis
101
+ except ImportError:
102
+ logger.error("Redis package not installed. Install with: pip install redis")
103
+ raise
104
+
105
+ # Try to get Redis client from environment first
106
+ if not self.redis_client:
107
+ self.redis_client = APIRedisDBManager.load_redis_engine_from_env()
108
+
109
+ # If still no client, try from config
110
+ if not self.redis_client and self.redis_config:
111
+ redis_kwargs = {
112
+ "host": self.redis_config.get("host"),
113
+ "port": self.redis_config.get("port"),
114
+ "db": self.redis_config.get("db"),
115
+ "decode_responses": True,
116
+ }
117
+
118
+ if self.redis_config.get("password"):
119
+ redis_kwargs["password"] = self.redis_config["password"]
120
+
121
+ self.redis_client = redis.Redis(**redis_kwargs)
122
+
123
+ # Final fallback to localhost
124
+ if not self.redis_client:
125
+ logger.warning("No Redis configuration found, using localhost defaults")
126
+ self.redis_client = redis.Redis(
127
+ host="localhost", port=6379, db=0, decode_responses=True
128
+ )
129
+
130
+ # Test connection
131
+ if not self.redis_client.ping():
132
+ raise ConnectionError("Redis ping failed")
133
+
134
+ logger.info("Redis client initialized successfully")
135
+
136
+ def acquire_lock(self, block: bool = True, **kwargs) -> bool:
137
+ """Acquire a distributed lock using Redis with atomic operations
138
+
139
+ Args:
140
+ block: Whether to block until lock is acquired
141
+ **kwargs: Additional filter criteria (ignored for Redis)
142
+
143
+ Returns:
144
+ True if lock was acquired, False otherwise
145
+ """
146
+
147
+ now = get_utc_now()
148
+
149
+ # Use Redis SET with NX (only if not exists) and EX (expiry) for atomic lock acquisition
150
+ lock_value = f"{self._get_key_prefix()}:{now.timestamp()}"
151
+
152
+ while True:
153
+ result = self.redis_client.get(self.lock_key)
154
+ if result:
155
+ # Wait a bit before retrying
156
+ logger.info(
157
+ f"Waiting for Redis lock to be released for {self.user_id}/{self.mem_cube_id}"
158
+ )
159
+ if not block:
160
+ logger.warning(
161
+ f"Redis lock is held for {self.user_id}/{self.mem_cube_id}, cannot acquire"
162
+ )
163
+ return False
164
+ else:
165
+ time.sleep(0.1)
166
+ continue
167
+ else:
168
+ # Try to acquire lock atomically
169
+ result = self.redis_client.set(
170
+ self.lock_key,
171
+ lock_value,
172
+ ex=self.lock_timeout, # Set expiry in seconds
173
+ )
174
+ logger.info(f"Redis lock acquired for {self._get_key_prefix()}")
175
+ return True
176
+
177
+ def release_locks(self, **kwargs):
178
+ # Delete the lock key to release the lock
179
+ result = self.redis_client.delete(self.lock_key)
180
+
181
+ # Redis DELETE returns the number of keys deleted (0 or 1)
182
+ if result > 0:
183
+ logger.info(f"Redis lock released for {self._get_key_prefix()}")
184
+ else:
185
+ logger.info(f"No Redis lock found to release for {self._get_key_prefix()}")
186
+
187
+ def merge_items(
188
+ self,
189
+ redis_data: str,
190
+ obj_instance: APISearchHistoryManager,
191
+ size_limit: int,
192
+ ):
193
+ """Merge Redis data with current object instance
194
+
195
+ Args:
196
+ redis_data: JSON string from Redis containing serialized APISearchHistoryManager
197
+ obj_instance: Current APISearchHistoryManager instance
198
+ size_limit: Maximum number of completed entries to keep
199
+
200
+ Returns:
201
+ APISearchHistoryManager: Merged and synchronized manager instance
202
+ """
203
+
204
+ # Parse Redis data
205
+ redis_manager = APISearchHistoryManager.from_json(redis_data)
206
+ logger.debug(
207
+ f"Loaded Redis manager with {len(redis_manager.completed_entries)} completed and {len(redis_manager.running_item_ids)} running task IDs"
208
+ )
209
+
210
+ # Create a new merged manager with the original window size from obj_instance
211
+ # Use size_limit only for limiting entries, not as window_size
212
+ original_window_size = obj_instance.window_size
213
+ merged_manager = APISearchHistoryManager(window_size=original_window_size)
214
+
215
+ # Merge completed entries - combine both sources and deduplicate by task_id
216
+ # Ensure all entries are APIMemoryHistoryEntryItem instances
217
+ from memos.mem_scheduler.schemas.api_schemas import APIMemoryHistoryEntryItem
218
+
219
+ all_completed = {}
220
+
221
+ # Add Redis completed entries
222
+ for entry in redis_manager.completed_entries:
223
+ if isinstance(entry, dict):
224
+ # Convert dict to APIMemoryHistoryEntryItem instance
225
+ try:
226
+ entry_obj = APIMemoryHistoryEntryItem(**entry)
227
+ task_id = entry_obj.item_id
228
+ all_completed[task_id] = entry_obj
229
+ except Exception as e:
230
+ logger.warning(
231
+ f"Failed to convert dict entry to APIMemoryHistoryEntryItem: {e}"
232
+ )
233
+ continue
234
+ else:
235
+ task_id = entry.item_id
236
+ all_completed[task_id] = entry
237
+
238
+ # Add current instance completed entries (these take priority if duplicated)
239
+ for entry in obj_instance.completed_entries:
240
+ if isinstance(entry, dict):
241
+ # Convert dict to APIMemoryHistoryEntryItem instance
242
+ try:
243
+ entry_obj = APIMemoryHistoryEntryItem(**entry)
244
+ task_id = entry_obj.item_id
245
+ all_completed[task_id] = entry_obj
246
+ except Exception as e:
247
+ logger.warning(
248
+ f"Failed to convert dict entry to APIMemoryHistoryEntryItem: {e}"
249
+ )
250
+ continue
251
+ else:
252
+ task_id = entry.item_id
253
+ all_completed[task_id] = entry
254
+
255
+ # Sort by created_time and apply size limit
256
+ completed_list = list(all_completed.values())
257
+
258
+ def get_created_time(entry):
259
+ """Helper function to safely extract created_time for sorting"""
260
+ from datetime import datetime
261
+
262
+ # All entries should now be APIMemoryHistoryEntryItem instances
263
+ return getattr(entry, "created_time", datetime.min)
264
+
265
+ completed_list.sort(key=get_created_time, reverse=True)
266
+ merged_manager.completed_entries = completed_list[:size_limit]
267
+
268
+ # Merge running task IDs - combine both sources and deduplicate
269
+ all_running_item_ids = set()
270
+
271
+ # Add Redis running task IDs
272
+ all_running_item_ids.update(redis_manager.running_item_ids)
273
+
274
+ # Add current instance running task IDs
275
+ all_running_item_ids.update(obj_instance.running_item_ids)
276
+
277
+ merged_manager.running_item_ids = list(all_running_item_ids)
278
+
279
+ logger.info(
280
+ f"Merged manager: {len(merged_manager.completed_entries)} completed, {len(merged_manager.running_item_ids)} running task IDs"
281
+ )
282
+ return merged_manager
283
+
284
+ def sync_with_redis(self, size_limit: int | None = None) -> None:
285
+ """Synchronize data between Redis and the business object
286
+
287
+ Args:
288
+ size_limit: Optional maximum number of items to keep after synchronization
289
+ """
290
+
291
+ # Use window_size from the object if size_limit is not provided
292
+ if size_limit is None:
293
+ size_limit = self.window_size
294
+
295
+ # Acquire lock before operations
296
+ lock_status = self.acquire_lock(block=True)
297
+ if not lock_status:
298
+ logger.error("Failed to acquire Redis lock for synchronization")
299
+ return
300
+
301
+ # Load existing data from Redis
302
+ data_key = self._get_data_key()
303
+ redis_data = self.redis_client.get(data_key)
304
+
305
+ if redis_data:
306
+ # Merge Redis data with current object
307
+ merged_obj = self.merge_items(
308
+ redis_data=redis_data, obj_instance=self.obj, size_limit=size_limit
309
+ )
310
+
311
+ # Update the current object with merged data
312
+ self.obj = merged_obj
313
+ logger.info(
314
+ f"Successfully synchronized with Redis data for {self.user_id}/{self.mem_cube_id}"
315
+ )
316
+ else:
317
+ logger.info(
318
+ f"No existing Redis data found for {self.user_id}/{self.mem_cube_id}, using current object"
319
+ )
320
+
321
+ # Save the synchronized object back to Redis
322
+ self.save_to_db(self.obj)
323
+
324
+ self.release_locks()
325
+
326
+ def save_to_db(self, obj_instance: Any) -> None:
327
+ """Save the current state of the business object to Redis
328
+
329
+ Args:
330
+ obj_instance: The object instance to save (must have to_json method)
331
+ """
332
+
333
+ data_key = self._get_data_key()
334
+
335
+ self.redis_client.set(data_key, obj_instance.to_json())
336
+
337
+ logger.info(f"Updated existing Redis record for {data_key}")
338
+
339
+ def load_from_db(self) -> Any | None:
340
+ data_key = self._get_data_key()
341
+
342
+ # Load from Redis
343
+ serialized_data = self.redis_client.get(data_key)
344
+
345
+ if not serialized_data:
346
+ logger.info(f"No Redis record found for {data_key}")
347
+ return None
348
+
349
+ # Deserialize the business object using the actual object type
350
+ if hasattr(self, "obj_type") and self.obj_type is not None:
351
+ db_instance = self.obj_type.from_json(serialized_data)
352
+ else:
353
+ # Default to APISearchHistoryManager for this class
354
+ db_instance = APISearchHistoryManager.from_json(serialized_data)
355
+
356
+ logger.info(f"Successfully loaded object from Redis for {data_key} ")
357
+
358
+ return db_instance
359
+
360
+ @classmethod
361
+ def from_env(
362
+ cls,
363
+ user_id: str,
364
+ mem_cube_id: str,
365
+ obj: Any | None = None,
366
+ lock_timeout: int = 10,
367
+ env_file_path: str | None = None,
368
+ ) -> "APIRedisDBManager":
369
+ """Create RedisDBManager from environment variables
370
+
371
+ Args:
372
+ user_id: User identifier
373
+ mem_cube_id: Memory cube identifier
374
+ obj: Optional MemoryMonitorManager instance
375
+ lock_timeout: Lock timeout in seconds
376
+ env_file_path: Optional path to .env file
377
+
378
+ Returns:
379
+ RedisDBManager instance
380
+ """
381
+
382
+ redis_client = APIRedisDBManager.load_redis_engine_from_env(env_file_path)
383
+ return cls(
384
+ user_id=user_id,
385
+ mem_cube_id=mem_cube_id,
386
+ obj=obj,
387
+ lock_timeout=lock_timeout,
388
+ redis_client=redis_client,
389
+ )
390
+
391
+ def close(self):
392
+ """Close the Redis connection and clean up resources"""
393
+ try:
394
+ if hasattr(self.redis_client, "close"):
395
+ self.redis_client.close()
396
+ logger.info(
397
+ f"Redis connection closed for user_id: {self.user_id}, mem_cube_id: {self.mem_cube_id}"
398
+ )
399
+ except Exception as e:
400
+ logger.warning(f"Error closing Redis connection: {e}")
401
+
402
+ @staticmethod
403
+ def load_redis_engine_from_env(env_file_path: str | None = None) -> Any:
404
+ """Load Redis connection from environment variables
405
+
406
+ Args:
407
+ env_file_path: Path to .env file (optional, defaults to loading from current environment)
408
+
409
+ Returns:
410
+ Redis connection instance
411
+
412
+ Raises:
413
+ DatabaseError: If required environment variables are missing or connection fails
414
+ """
415
+ try:
416
+ import redis
417
+ except ImportError as e:
418
+ error_msg = "Redis package not installed. Install with: pip install redis"
419
+ logger.error(error_msg)
420
+ raise DatabaseError(error_msg) from e
421
+
422
+ # Load environment variables from file if provided
423
+ if env_file_path:
424
+ if os.path.exists(env_file_path):
425
+ from dotenv import load_dotenv
426
+
427
+ load_dotenv(env_file_path)
428
+ logger.info(f"Loaded environment variables from {env_file_path}")
429
+ else:
430
+ logger.warning(
431
+ f"Environment file not found: {env_file_path}, using current environment variables",
432
+ stack_info=True,
433
+ )
434
+ else:
435
+ logger.info("Using current environment variables (no env_file_path provided)")
436
+
437
+ # Get Redis configuration from environment variables
438
+ redis_host = os.getenv("REDIS_HOST") or os.getenv("MEMSCHEDULER_REDIS_HOST")
439
+ redis_port_str = os.getenv("REDIS_PORT") or os.getenv("MEMSCHEDULER_REDIS_PORT")
440
+ redis_db_str = os.getenv("REDIS_DB") or os.getenv("MEMSCHEDULER_REDIS_DB")
441
+ redis_password = os.getenv("REDIS_PASSWORD") or os.getenv("MEMSCHEDULER_REDIS_PASSWORD")
442
+
443
+ # Check required environment variables
444
+ if not redis_host:
445
+ error_msg = (
446
+ "Missing required Redis environment variable: REDIS_HOST or MEMSCHEDULER_REDIS_HOST"
447
+ )
448
+ logger.error(error_msg)
449
+ return None
450
+
451
+ # Parse port with validation
452
+ try:
453
+ redis_port = int(redis_port_str) if redis_port_str else 6379
454
+ except ValueError:
455
+ error_msg = f"Invalid REDIS_PORT value: {redis_port_str}. Must be a valid integer."
456
+ logger.error(error_msg)
457
+ return None
458
+
459
+ # Parse database with validation
460
+ try:
461
+ redis_db = int(redis_db_str) if redis_db_str else 0
462
+ except ValueError:
463
+ error_msg = f"Invalid REDIS_DB value: {redis_db_str}. Must be a valid integer."
464
+ logger.error(error_msg)
465
+ return None
466
+
467
+ # Optional timeout settings
468
+ socket_timeout = os.getenv(
469
+ "REDIS_SOCKET_TIMEOUT", os.getenv("MEMSCHEDULER_REDIS_TIMEOUT", None)
470
+ )
471
+ socket_connect_timeout = os.getenv(
472
+ "REDIS_SOCKET_CONNECT_TIMEOUT", os.getenv("MEMSCHEDULER_REDIS_CONNECT_TIMEOUT", None)
473
+ )
474
+
475
+ try:
476
+ # Build Redis connection parameters
477
+ redis_kwargs = {
478
+ "host": redis_host,
479
+ "port": redis_port,
480
+ "db": redis_db,
481
+ "decode_responses": True,
482
+ }
483
+
484
+ if redis_password:
485
+ redis_kwargs["password"] = redis_password
486
+
487
+ if socket_timeout:
488
+ try:
489
+ redis_kwargs["socket_timeout"] = float(socket_timeout)
490
+ except ValueError:
491
+ logger.warning(
492
+ f"Invalid REDIS_SOCKET_TIMEOUT value: {socket_timeout}, ignoring"
493
+ )
494
+
495
+ if socket_connect_timeout:
496
+ try:
497
+ redis_kwargs["socket_connect_timeout"] = float(socket_connect_timeout)
498
+ except ValueError:
499
+ logger.warning(
500
+ f"Invalid REDIS_SOCKET_CONNECT_TIMEOUT value: {socket_connect_timeout}, ignoring"
501
+ )
502
+
503
+ # Create Redis connection
504
+ redis_client = redis.Redis(**redis_kwargs)
505
+
506
+ # Test connection
507
+ if not redis_client.ping():
508
+ raise ConnectionError("Redis ping failed")
509
+
510
+ logger.info(
511
+ f"Successfully created Redis connection: {redis_host}:{redis_port}/{redis_db}"
512
+ )
513
+ return redis_client
514
+
515
+ except Exception as e:
516
+ error_msg = f"Failed to create Redis connection from environment variables: {e}"
517
+ logger.error(error_msg, stack_info=True)
518
+ raise DatabaseError(error_msg) from e