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,313 @@
1
+ import json
2
+ import os
3
+
4
+ from contextlib import suppress
5
+ from datetime import datetime
6
+ from queue import Empty, Full, Queue
7
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
8
+
9
+ from dotenv import load_dotenv
10
+ from pydantic import field_serializer
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from pydantic import BaseModel
15
+
16
+ T = TypeVar("T")
17
+
18
+ BaseModelType = TypeVar("T", bound="BaseModel")
19
+
20
+
21
+ class EnvConfigMixin(Generic[T]):
22
+ """Abstract base class for environment variable configuration."""
23
+
24
+ ENV_PREFIX = "MEMSCHEDULER_"
25
+
26
+ @classmethod
27
+ def get_env_prefix(cls) -> str:
28
+ """Automatically generates environment variable prefix from class name.
29
+
30
+ Converts the class name to uppercase and appends an underscore.
31
+ If the class name ends with 'Config', that suffix is removed first.
32
+
33
+ Examples:
34
+ RabbitMQConfig -> "RABBITMQ_"
35
+ OpenAIConfig -> "OPENAI_"
36
+ GraphDBAuthConfig -> "GRAPHDBAUTH_"
37
+ """
38
+ class_name = cls.__name__
39
+ # Remove 'Config' suffix if present
40
+ if class_name.endswith("Config"):
41
+ class_name = class_name[:-6]
42
+ # Convert to uppercase and add trailing underscore
43
+
44
+ return f"{cls.ENV_PREFIX}{class_name.upper()}_"
45
+
46
+ @classmethod
47
+ def from_env(cls: type[T]) -> T:
48
+ """Creates a config instance from environment variables.
49
+
50
+ Reads all environment variables with the class-specific prefix and maps them
51
+ to corresponding configuration fields (converting to the appropriate types).
52
+
53
+ Returns:
54
+ An instance of the config class populated from environment variables.
55
+
56
+ Raises:
57
+ ValueError: If required environment variables are missing.
58
+ """
59
+ load_dotenv()
60
+
61
+ prefix = cls.get_env_prefix()
62
+ field_values = {}
63
+
64
+ for field_name, field_info in cls.model_fields.items():
65
+ env_var = f"{prefix}{field_name.upper()}"
66
+ field_type = field_info.annotation
67
+
68
+ if field_info.is_required() and env_var not in os.environ:
69
+ raise ValueError(f"Required environment variable {env_var} is missing")
70
+
71
+ if env_var in os.environ:
72
+ raw_value = os.environ[env_var]
73
+ field_values[field_name] = cls._parse_env_value(raw_value, field_type)
74
+ elif field_info.default is not None:
75
+ field_values[field_name] = field_info.default
76
+ else:
77
+ raise ValueError()
78
+ return cls(**field_values)
79
+
80
+ @classmethod
81
+ def _parse_env_value(cls, value: str, target_type: type) -> Any:
82
+ """Converts environment variable string to appropriate type."""
83
+ if target_type is bool:
84
+ return value.lower() in ("true", "1", "t", "y", "yes")
85
+ if target_type is int:
86
+ return int(value)
87
+ if target_type is float:
88
+ return float(value)
89
+ return value
90
+
91
+ @classmethod
92
+ def print_env_mapping(cls) -> None:
93
+ """Print the mapping between class fields and their corresponding environment variable names.
94
+
95
+ Displays each field's name, type, whether it's required, default value, and corresponding environment variable name.
96
+ """
97
+ prefix = cls.get_env_prefix()
98
+ print(f"\n=== {cls.__name__} Environment Variable Mapping ===")
99
+ print(f"Environment Variable Prefix: {prefix}")
100
+ print("-" * 60)
101
+
102
+ if not hasattr(cls, "model_fields"):
103
+ print("This class does not define model_fields, may not be a Pydantic model")
104
+ return
105
+
106
+ for field_name, field_info in cls.model_fields.items():
107
+ env_var = f"{prefix}{field_name.upper()}"
108
+ field_type = field_info.annotation
109
+ is_required = field_info.is_required()
110
+ default_value = field_info.default if field_info.default is not None else "None"
111
+
112
+ print(f"Field Name: {field_name}")
113
+ print(f" Environment Variable: {env_var}")
114
+ print(f" Type: {field_type}")
115
+ print(f" Required: {'Yes' if is_required else 'No'}")
116
+ print(f" Default Value: {default_value}")
117
+ print(f" Current Environment Value: {os.environ.get(env_var, 'Not Set')}")
118
+ print("-" * 40)
119
+
120
+
121
+ class DictConversionMixin:
122
+ """
123
+ Provides conversion functionality between Pydantic models and dictionaries,
124
+ including datetime serialization handling.
125
+ """
126
+
127
+ @field_serializer("timestamp", check_fields=False)
128
+ def serialize_datetime(self, dt: datetime | None, _info) -> str | None:
129
+ """
130
+ Custom timestamp serialization logic.
131
+ - Supports timezone-aware datetime objects
132
+ - Compatible with models without timestamp field (via check_fields=False)
133
+ """
134
+ if dt is None:
135
+ return None
136
+ return dt.isoformat()
137
+
138
+ def to_dict(self) -> dict:
139
+ """
140
+ Convert model instance to dictionary.
141
+ - Uses model_dump to ensure field consistency
142
+ - Prioritizes custom serializer for timestamp handling
143
+ """
144
+ dump_data = self.model_dump()
145
+ if hasattr(self, "timestamp") and self.timestamp is not None:
146
+ dump_data["timestamp"] = self.serialize_datetime(self.timestamp, None)
147
+ return dump_data
148
+
149
+ def to_json(self, **kwargs) -> str:
150
+ """
151
+ Convert model instance to a JSON string.
152
+ - Accepts the same kwargs as json.dumps (e.g., indent, ensure_ascii)
153
+ - Default settings make JSON human-readable and UTF-8 safe
154
+ """
155
+ return json.dumps(self.to_dict(), ensure_ascii=False, default=lambda o: str(o), **kwargs)
156
+
157
+ @classmethod
158
+ def from_json(cls: type[BaseModelType], json_str: str) -> BaseModelType:
159
+ """
160
+ Create model instance from a JSON string.
161
+ - Parses JSON into a dictionary and delegates to from_dict
162
+ """
163
+ try:
164
+ data = json.loads(json_str)
165
+ except json.JSONDecodeError as e:
166
+ raise ValueError(f"Invalid JSON string: {e}") from e
167
+ return cls.from_dict(data)
168
+
169
+ @classmethod
170
+ def from_dict(cls: type[BaseModelType], data: dict) -> BaseModelType:
171
+ """
172
+ Create model instance from dictionary.
173
+ - Automatically converts timestamp strings to datetime objects
174
+ """
175
+ data_copy = data.copy() # Avoid modifying original dictionary
176
+ if "timestamp" in data_copy and isinstance(data_copy["timestamp"], str):
177
+ try:
178
+ data_copy["timestamp"] = datetime.fromisoformat(data_copy["timestamp"])
179
+ except ValueError:
180
+ # Handle invalid time formats - adjust as needed (e.g., log warning or set to None)
181
+ data_copy["timestamp"] = None
182
+
183
+ return cls(**data_copy)
184
+
185
+ def __str__(self) -> str:
186
+ """
187
+ Convert to formatted JSON string.
188
+ - Used for user-friendly display in print() or str() calls
189
+ """
190
+ return json.dumps(
191
+ self.to_dict(),
192
+ indent=4,
193
+ ensure_ascii=False,
194
+ default=lambda o: str(o), # Handle other non-serializable objects
195
+ )
196
+
197
+
198
+ class AutoDroppingQueue(Queue[T]):
199
+ """A thread-safe queue that automatically drops the oldest item when full."""
200
+
201
+ def __init__(self, maxsize: int = 0):
202
+ # If maxsize <= 0, set to 0 (unlimited queue size)
203
+ if maxsize <= 0:
204
+ maxsize = 0
205
+ super().__init__(maxsize=maxsize)
206
+
207
+ def put(self, item: T, block: bool = False, timeout: float | None = None) -> None:
208
+ """Put an item into the queue.
209
+
210
+ If the queue is full, the oldest item will be automatically removed to make space.
211
+ IMPORTANT: When we drop an item we also call `task_done()` to keep
212
+ the internal `unfinished_tasks` counter consistent (the dropped task
213
+ will never be processed).
214
+
215
+ Args:
216
+ item: The item to be put into the queue
217
+ block: Ignored (kept for compatibility with Queue interface)
218
+ timeout: Ignored (kept for compatibility with Queue interface)
219
+ """
220
+ while True:
221
+ try:
222
+ # First try non-blocking put
223
+ super().put(item, block=block, timeout=timeout)
224
+ return
225
+ except Full:
226
+ # Remove the oldest item and mark it done to avoid leaking unfinished_tasks
227
+ with suppress(Empty):
228
+ _ = self.get_nowait()
229
+ # If the removed item had previously incremented unfinished_tasks,
230
+ # we must decrement here since it will never be processed.
231
+ with suppress(ValueError):
232
+ self.task_done()
233
+ # Continue loop to retry putting the item
234
+
235
+ def get(
236
+ self, block: bool = True, timeout: float | None = None, batch_size: int | None = None
237
+ ) -> list[T]:
238
+ """Get items from the queue.
239
+
240
+ Args:
241
+ block: Whether to block if no items are available (default: True)
242
+ timeout: Timeout in seconds for blocking operations (default: None)
243
+ batch_size: Number of items to retrieve (default: 1)
244
+
245
+ Returns:
246
+ List of items (always returns a list for consistency)
247
+
248
+ Raises:
249
+ Empty: If no items are available and block=False or timeout expires
250
+ """
251
+
252
+ if batch_size is None:
253
+ return super().get(block=block, timeout=timeout)
254
+ items = []
255
+ for _ in range(batch_size):
256
+ try:
257
+ items.append(super().get(block=block, timeout=timeout))
258
+ except Empty:
259
+ if not items and block:
260
+ # If we haven't gotten any items and we're blocking, re-raise Empty
261
+ raise
262
+ break
263
+ return items
264
+
265
+ def get_nowait(self, batch_size: int | None = None) -> list[T]:
266
+ """Get items from the queue without blocking.
267
+
268
+ Args:
269
+ batch_size: Number of items to retrieve (default: 1)
270
+
271
+ Returns:
272
+ List of items (always returns a list for consistency)
273
+ """
274
+ if batch_size is None:
275
+ return super().get_nowait()
276
+
277
+ items = []
278
+ for _ in range(batch_size):
279
+ try:
280
+ items.append(super().get_nowait())
281
+ except Empty:
282
+ break
283
+ return items
284
+
285
+ def get_queue_content_without_pop(self) -> list[T]:
286
+ """Return a copy of the queue's contents without modifying it."""
287
+ # Ensure a consistent snapshot by holding the mutex
288
+ with self.mutex:
289
+ return list(self.queue)
290
+
291
+ def qsize(self) -> int:
292
+ """Return the approximate size of the queue.
293
+
294
+ Returns:
295
+ Number of items currently in the queue
296
+ """
297
+ return super().qsize()
298
+
299
+ def clear(self) -> None:
300
+ """Remove all items from the queue.
301
+
302
+ This operation is thread-safe.
303
+ IMPORTANT: We also decrement `unfinished_tasks` by the number of
304
+ items cleared, since those tasks will never be processed.
305
+ """
306
+ with self.mutex:
307
+ dropped = len(self.queue)
308
+ self.queue.clear()
309
+ # Call task_done() outside of the mutex to avoid deadlocks because
310
+ # Queue.task_done() acquires the same condition bound to `self.mutex`.
311
+ for _ in range(dropped):
312
+ with suppress(ValueError):
313
+ self.task_done()
@@ -0,0 +1,389 @@
1
+ import hashlib
2
+
3
+ from collections.abc import Callable
4
+
5
+ from memos.log import get_logger
6
+ from memos.mem_cube.general import GeneralMemCube
7
+ from memos.mem_scheduler.general_modules.base import BaseSchedulerModule
8
+ from memos.mem_scheduler.schemas.general_schemas import (
9
+ ACTIVATION_MEMORY_TYPE,
10
+ NOT_INITIALIZED,
11
+ PARAMETER_MEMORY_TYPE,
12
+ TEXT_MEMORY_TYPE,
13
+ WORKING_MEMORY_TYPE,
14
+ )
15
+ from memos.mem_scheduler.schemas.message_schemas import (
16
+ ScheduleLogForWebItem,
17
+ ScheduleMessageItem,
18
+ )
19
+ from memos.mem_scheduler.schemas.task_schemas import (
20
+ ADD_TASK_LABEL,
21
+ MEM_ARCHIVE_TASK_LABEL,
22
+ MEM_UPDATE_TASK_LABEL,
23
+ USER_INPUT_TYPE,
24
+ )
25
+ from memos.mem_scheduler.utils.filter_utils import (
26
+ transform_name_to_key,
27
+ )
28
+ from memos.mem_scheduler.utils.misc_utils import log_exceptions
29
+ from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
30
+
31
+
32
+ logger = get_logger(__name__)
33
+
34
+
35
+ class SchedulerLoggerModule(BaseSchedulerModule):
36
+ def __init__(self):
37
+ """
38
+ Initialize RabbitMQ connection settings.
39
+ """
40
+ super().__init__()
41
+
42
+ @log_exceptions(logger=logger)
43
+ def create_autofilled_log_item(
44
+ self,
45
+ log_content: str,
46
+ label: str,
47
+ from_memory_type: str,
48
+ to_memory_type: str,
49
+ user_id: str,
50
+ mem_cube_id: str,
51
+ mem_cube: GeneralMemCube,
52
+ ) -> ScheduleLogForWebItem:
53
+ if mem_cube is None:
54
+ logger.error(
55
+ "mem_cube is None — this should not happen in production!", stack_info=True
56
+ )
57
+ text_mem_base: TreeTextMemory = mem_cube.text_mem
58
+
59
+ current_memory_sizes = {}
60
+ if hasattr(text_mem_base, "get_current_memory_size"):
61
+ current_memory_sizes = text_mem_base.get_current_memory_size(user_name=mem_cube_id)
62
+
63
+ current_memory_sizes = {
64
+ "long_term_memory_size": current_memory_sizes.get("LongTermMemory", 0),
65
+ "user_memory_size": current_memory_sizes.get("UserMemory", 0),
66
+ "working_memory_size": current_memory_sizes.get("WorkingMemory", 0),
67
+ "transformed_act_memory_size": NOT_INITIALIZED,
68
+ "parameter_memory_size": NOT_INITIALIZED,
69
+ }
70
+
71
+ memory_capacities = {
72
+ "long_term_memory_capacity": 0,
73
+ "user_memory_capacity": 0,
74
+ "working_memory_capacity": 0,
75
+ "transformed_act_memory_capacity": NOT_INITIALIZED,
76
+ "parameter_memory_capacity": NOT_INITIALIZED,
77
+ }
78
+
79
+ if hasattr(text_mem_base, "memory_manager") and hasattr(
80
+ text_mem_base.memory_manager, "memory_size"
81
+ ):
82
+ memory_capacities.update(
83
+ {
84
+ "long_term_memory_capacity": text_mem_base.memory_manager.memory_size.get(
85
+ "LongTermMemory", 0
86
+ ),
87
+ "user_memory_capacity": text_mem_base.memory_manager.memory_size.get(
88
+ "UserMemory", 0
89
+ ),
90
+ "working_memory_capacity": text_mem_base.memory_manager.memory_size.get(
91
+ "WorkingMemory", 0
92
+ ),
93
+ }
94
+ )
95
+
96
+ if hasattr(self, "monitor"):
97
+ if (
98
+ user_id in self.monitor.activation_memory_monitors
99
+ and mem_cube_id in self.monitor.activation_memory_monitors[user_id]
100
+ ):
101
+ activation_monitor = self.monitor.activation_memory_monitors[user_id][mem_cube_id]
102
+ transformed_act_memory_size = len(activation_monitor.obj.memories)
103
+ logger.info(
104
+ f'activation_memory_monitors currently has "{transformed_act_memory_size}" transformed memory size'
105
+ )
106
+ else:
107
+ transformed_act_memory_size = 0
108
+ logger.info(
109
+ f'activation_memory_monitors is not initialized for user "{user_id}" and mem_cube "{mem_cube_id}'
110
+ )
111
+ current_memory_sizes["transformed_act_memory_size"] = transformed_act_memory_size
112
+ current_memory_sizes["parameter_memory_size"] = 1
113
+
114
+ memory_capacities["transformed_act_memory_capacity"] = (
115
+ self.monitor.activation_mem_monitor_capacity
116
+ )
117
+ memory_capacities["parameter_memory_capacity"] = 1
118
+
119
+ log_message = ScheduleLogForWebItem(
120
+ user_id=user_id,
121
+ mem_cube_id=mem_cube_id,
122
+ label=label,
123
+ from_memory_type=from_memory_type,
124
+ to_memory_type=to_memory_type,
125
+ log_content=log_content,
126
+ current_memory_sizes=current_memory_sizes,
127
+ memory_capacities=memory_capacities,
128
+ )
129
+ return log_message
130
+
131
+ @log_exceptions(logger=logger)
132
+ def create_event_log(
133
+ self,
134
+ label: str,
135
+ from_memory_type: str,
136
+ to_memory_type: str,
137
+ user_id: str,
138
+ mem_cube_id: str,
139
+ mem_cube: GeneralMemCube,
140
+ memcube_log_content: list[dict],
141
+ metadata: list[dict],
142
+ memory_len: int,
143
+ memcube_name: str | None = None,
144
+ log_content: str | None = None,
145
+ ) -> ScheduleLogForWebItem:
146
+ item = self.create_autofilled_log_item(
147
+ log_content=log_content or "",
148
+ label=label,
149
+ from_memory_type=from_memory_type,
150
+ to_memory_type=to_memory_type,
151
+ user_id=user_id,
152
+ mem_cube_id=mem_cube_id,
153
+ mem_cube=mem_cube,
154
+ )
155
+ item.memcube_log_content = memcube_log_content
156
+ item.metadata = metadata
157
+ item.memory_len = memory_len
158
+ item.memcube_name = memcube_name or self._map_memcube_name(mem_cube_id)
159
+ return item
160
+
161
+ def _map_memcube_name(self, mem_cube_id: str) -> str:
162
+ x = mem_cube_id or ""
163
+ if "public" in x.lower():
164
+ return "PublicMemCube"
165
+ return "UserMemCube"
166
+
167
+ # TODO: Log output count is incorrect
168
+ @log_exceptions(logger=logger)
169
+ def log_working_memory_replacement(
170
+ self,
171
+ original_memory: list[TextualMemoryItem],
172
+ new_memory: list[TextualMemoryItem],
173
+ user_id: str,
174
+ mem_cube_id: str,
175
+ mem_cube: GeneralMemCube,
176
+ log_func_callback: Callable[[list[ScheduleLogForWebItem]], None],
177
+ ):
178
+ """Log changes when working memory is replaced."""
179
+ original_text_memories = [m.memory for m in original_memory]
180
+ new_text_memories = [m.memory for m in new_memory]
181
+ original_set = set(original_text_memories)
182
+ new_set = set(new_text_memories)
183
+ added_texts = []
184
+ for new_mem in new_set:
185
+ if new_mem not in original_set:
186
+ added_texts.append(new_mem)
187
+ memcube_content = []
188
+ meta = []
189
+ by_text = {m.memory: m for m in new_memory}
190
+ for t in added_texts:
191
+ itm = by_text.get(t)
192
+ if not itm:
193
+ continue
194
+ key_name = getattr(itm.metadata, "key", None) or itm.memory
195
+ k = transform_name_to_key(name=key_name)
196
+ memcube_content.append(
197
+ {
198
+ "content": f"[{itm.metadata.memory_type}→{WORKING_MEMORY_TYPE}] {k}: {itm.memory}",
199
+ "ref_id": itm.id,
200
+ }
201
+ )
202
+ meta.append(
203
+ {
204
+ "ref_id": itm.id,
205
+ "id": itm.id,
206
+ "key": itm.metadata.key,
207
+ "memory": itm.memory,
208
+ "memory_type": itm.metadata.memory_type,
209
+ "status": itm.metadata.status,
210
+ "confidence": itm.metadata.confidence,
211
+ "tags": itm.metadata.tags,
212
+ "updated_at": getattr(itm.metadata, "updated_at", None)
213
+ or getattr(itm.metadata, "update_at", None),
214
+ }
215
+ )
216
+ # Only create log if there are actual memory changes
217
+ if memcube_content:
218
+ ev = self.create_event_log(
219
+ label="scheduleMemory",
220
+ from_memory_type=TEXT_MEMORY_TYPE,
221
+ to_memory_type=WORKING_MEMORY_TYPE,
222
+ user_id=user_id,
223
+ mem_cube_id=mem_cube_id,
224
+ mem_cube=mem_cube,
225
+ memcube_log_content=memcube_content,
226
+ metadata=meta,
227
+ memory_len=len(memcube_content),
228
+ memcube_name=self._map_memcube_name(mem_cube_id),
229
+ )
230
+ log_func_callback([ev])
231
+
232
+ @log_exceptions(logger=logger)
233
+ def log_activation_memory_update(
234
+ self,
235
+ original_text_memories: list[str],
236
+ new_text_memories: list[str],
237
+ label: str,
238
+ user_id: str,
239
+ mem_cube_id: str,
240
+ mem_cube: GeneralMemCube,
241
+ log_func_callback: Callable[[list[ScheduleLogForWebItem]], None],
242
+ ):
243
+ """Log changes when activation memory is updated."""
244
+ original_set = set(original_text_memories)
245
+ new_set = set(new_text_memories)
246
+
247
+ added_memories = list(new_set - original_set)
248
+ memcube_content = []
249
+ meta = []
250
+ for mem in added_memories:
251
+ key = transform_name_to_key(mem)
252
+ ref_id = f"actparam-{hashlib.md5(mem.encode()).hexdigest()}"
253
+ memcube_content.append(
254
+ {
255
+ "content": f"[{ACTIVATION_MEMORY_TYPE}→{PARAMETER_MEMORY_TYPE}] {key}: {mem}",
256
+ "ref_id": ref_id,
257
+ }
258
+ )
259
+ meta.append(
260
+ {
261
+ "ref_id": ref_id,
262
+ "id": ref_id,
263
+ "key": key,
264
+ "memory": mem,
265
+ "memory_type": ACTIVATION_MEMORY_TYPE,
266
+ "status": None,
267
+ "confidence": None,
268
+ "tags": None,
269
+ "updated_at": None,
270
+ }
271
+ )
272
+ # Only create log if there are actual memory changes
273
+ if memcube_content:
274
+ ev = self.create_event_log(
275
+ label="scheduleMemory",
276
+ from_memory_type=ACTIVATION_MEMORY_TYPE,
277
+ to_memory_type=PARAMETER_MEMORY_TYPE,
278
+ user_id=user_id,
279
+ mem_cube_id=mem_cube_id,
280
+ mem_cube=mem_cube,
281
+ memcube_log_content=memcube_content,
282
+ metadata=meta,
283
+ memory_len=len(added_memories),
284
+ memcube_name=self._map_memcube_name(mem_cube_id),
285
+ )
286
+ log_func_callback([ev])
287
+
288
+ @log_exceptions(logger=logger)
289
+ def log_adding_memory(
290
+ self,
291
+ memory: str,
292
+ memory_type: str,
293
+ user_id: str,
294
+ mem_cube_id: str,
295
+ mem_cube: GeneralMemCube,
296
+ log_func_callback: Callable[[list[ScheduleLogForWebItem]], None],
297
+ ):
298
+ """Deprecated: legacy text log. Use create_event_log with structured fields instead."""
299
+ log_message = self.create_autofilled_log_item(
300
+ log_content=memory,
301
+ label=ADD_TASK_LABEL,
302
+ from_memory_type=USER_INPUT_TYPE,
303
+ to_memory_type=memory_type,
304
+ user_id=user_id,
305
+ mem_cube_id=mem_cube_id,
306
+ mem_cube=mem_cube,
307
+ )
308
+ log_func_callback([log_message])
309
+ logger.info(
310
+ f"{USER_INPUT_TYPE} memory for user {user_id} "
311
+ f"converted to {memory_type} memory in mem_cube {mem_cube_id}: {memory}"
312
+ )
313
+
314
+ @log_exceptions(logger=logger)
315
+ def log_updating_memory(
316
+ self,
317
+ memory: str,
318
+ memory_type: str,
319
+ user_id: str,
320
+ mem_cube_id: str,
321
+ mem_cube: GeneralMemCube,
322
+ log_func_callback: Callable[[list[ScheduleLogForWebItem]], None],
323
+ ):
324
+ """Deprecated: legacy text log. Use create_event_log with structured fields instead."""
325
+ log_message = self.create_autofilled_log_item(
326
+ log_content=memory,
327
+ label=MEM_UPDATE_TASK_LABEL,
328
+ from_memory_type=memory_type,
329
+ to_memory_type=memory_type,
330
+ user_id=user_id,
331
+ mem_cube_id=mem_cube_id,
332
+ mem_cube=mem_cube,
333
+ )
334
+ log_func_callback([log_message])
335
+
336
+ @log_exceptions(logger=logger)
337
+ def log_archiving_memory(
338
+ self,
339
+ memory: str,
340
+ memory_type: str,
341
+ user_id: str,
342
+ mem_cube_id: str,
343
+ mem_cube: GeneralMemCube,
344
+ log_func_callback: Callable[[list[ScheduleLogForWebItem]], None],
345
+ ):
346
+ """Deprecated: legacy text log. Use create_event_log with structured fields instead."""
347
+ log_message = self.create_autofilled_log_item(
348
+ log_content=memory,
349
+ label=MEM_ARCHIVE_TASK_LABEL,
350
+ from_memory_type=memory_type,
351
+ to_memory_type=memory_type,
352
+ user_id=user_id,
353
+ mem_cube_id=mem_cube_id,
354
+ mem_cube=mem_cube,
355
+ )
356
+ log_func_callback([log_message])
357
+
358
+ @log_exceptions(logger=logger)
359
+ def validate_schedule_message(self, message: ScheduleMessageItem, label: str):
360
+ """Validate if the message matches the expected label.
361
+
362
+ Args:
363
+ message: Incoming message item to validate.
364
+ label: Expected message label (e.g., QUERY_LABEL/ANSWER_LABEL).
365
+
366
+ Returns:
367
+ bool: True if validation passed, False otherwise.
368
+ """
369
+ if message.label != label:
370
+ logger.error(f"Handler validation failed: expected={label}, actual={message.label}")
371
+ return False
372
+ return True
373
+
374
+ @log_exceptions(logger=logger)
375
+ def validate_schedule_messages(self, messages: list[ScheduleMessageItem], label: str):
376
+ """Validate if all messages match the expected label.
377
+
378
+ Args:
379
+ messages: List of message items to validate.
380
+ label: Expected message label (e.g., QUERY_LABEL/ANSWER_LABEL).
381
+
382
+ Returns:
383
+ bool: True if all messages passed validation, False if any failed.
384
+ """
385
+ for message in messages:
386
+ if not self.validate_schedule_message(message, label):
387
+ logger.error("Message batch contains invalid labels, aborting processing")
388
+ return False
389
+ return True