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,406 @@
1
+ import json
2
+ import threading
3
+
4
+ from collections import Counter
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import ClassVar
8
+ from uuid import uuid4
9
+
10
+ from pydantic import BaseModel, Field, computed_field, field_validator
11
+
12
+ from memos.log import get_logger
13
+ from memos.mem_scheduler.general_modules.misc import AutoDroppingQueue, DictConversionMixin
14
+ from memos.mem_scheduler.schemas.general_schemas import (
15
+ DEFAULT_WEIGHT_VECTOR_FOR_RANKING,
16
+ NOT_INITIALIZED,
17
+ )
18
+ from memos.mem_scheduler.schemas.task_schemas import (
19
+ DEFAULT_MAX_QUERY_KEY_WORDS,
20
+ )
21
+ from memos.mem_scheduler.utils.filter_utils import transform_name_to_key
22
+ from memos.memories.textual.tree import TextualMemoryItem
23
+
24
+
25
+ logger = get_logger(__name__)
26
+
27
+ FILE_PATH = Path(__file__).absolute()
28
+ BASE_DIR = FILE_PATH.parent.parent.parent.parent.parent
29
+
30
+
31
+ # ============== Queries ==============
32
+ class QueryMonitorItem(BaseModel, DictConversionMixin):
33
+ item_id: str = Field(
34
+ description="Unique identifier for the query item", default_factory=lambda: str(uuid4())
35
+ )
36
+ user_id: str = Field(..., description="Required user identifier", min_length=1)
37
+ mem_cube_id: str = Field(..., description="Required memory cube identifier", min_length=1)
38
+ query_text: str = Field(
39
+ ...,
40
+ description="The actual user query text content",
41
+ min_length=1,
42
+ )
43
+ keywords: list[str] | None = Field(
44
+ default=None,
45
+ min_length=1, # If provided, shouldn't be empty
46
+ description="Semantic keywords extracted from the query text",
47
+ )
48
+ max_keywords: ClassVar[int] = DEFAULT_MAX_QUERY_KEY_WORDS
49
+
50
+ timestamp: datetime = Field(
51
+ default_factory=datetime.now, description="Timestamp indicating when query was submitted"
52
+ )
53
+
54
+ @field_validator("keywords", mode="before")
55
+ @classmethod
56
+ def validate_keywords(cls, v, values):
57
+ if v is None:
58
+ return None
59
+
60
+ if not isinstance(v, list):
61
+ raise ValueError("Keywords must be a list")
62
+
63
+ if len(v) > cls.max_keywords:
64
+ logger.warning(
65
+ f"Keywords list truncated from {len(v)} to {cls.max_keywords} items. "
66
+ f"Configure max_keywords class attribute to adjust this limit."
67
+ )
68
+ return v[: cls.max_keywords]
69
+ return v
70
+
71
+ @classmethod
72
+ def with_max_keywords(cls, limit: int):
73
+ """Create a new class with custom keywords limit."""
74
+ if not isinstance(limit, int) or limit <= 0:
75
+ raise ValueError("Max keywords limit must be positive integer")
76
+
77
+ return type(f"{cls.__name__}_MaxKeywords{limit}", (cls,), {"max_keywords": limit})
78
+
79
+
80
+ class QueryMonitorQueue(AutoDroppingQueue[QueryMonitorItem]):
81
+ """
82
+ A thread-safe queue for monitoring queries with timestamp and keyword tracking.
83
+ Each item is expected to be a dictionary containing:
84
+ """
85
+
86
+ def put(self, item: QueryMonitorItem, block: bool = True, timeout: float | None = 5.0) -> None:
87
+ """
88
+ Add a query item to the queue. Ensures the item is of correct type.
89
+
90
+ Args:
91
+ item: A QueryMonitorItem instance
92
+ """
93
+ if not isinstance(item, QueryMonitorItem):
94
+ raise ValueError("Item must be an instance of QueryMonitorItem")
95
+ logger.debug(
96
+ f"Thread {threading.get_ident()} acquired mutex. Timeout is set to {timeout} seconds"
97
+ )
98
+ super().put(item, block, timeout)
99
+
100
+ def get_queries_by_timestamp(
101
+ self, start_time: datetime, end_time: datetime
102
+ ) -> list[QueryMonitorItem]:
103
+ """
104
+ Retrieve queries added between the specified time range.
105
+ """
106
+ with self.mutex:
107
+ logger.debug(f"Thread {threading.get_ident()} acquired mutex.")
108
+ return [item for item in self.queue if start_time <= item.timestamp <= end_time]
109
+
110
+ def get_keywords_collections(self) -> Counter:
111
+ """
112
+ Generate a Counter containing keyword frequencies across all queries.
113
+
114
+ Returns:
115
+ Counter object with keyword counts
116
+ """
117
+ with self.mutex:
118
+ logger.debug(f"Thread {threading.get_ident()} acquired mutex.")
119
+ # Fix: Handle None keywords safely
120
+ all_keywords = [kw for item in self.queue if item.keywords for kw in item.keywords]
121
+ return Counter(all_keywords)
122
+
123
+ def get_queries_with_timesort(self, reverse: bool = True) -> list[str]:
124
+ """
125
+ Retrieve all queries sorted by timestamp.
126
+
127
+ Args:
128
+ reverse: If True, sort in descending order (newest first),
129
+ otherwise sort in ascending order (oldest first)
130
+
131
+ Returns:
132
+ List of query items sorted by timestamp
133
+ """
134
+ with self.mutex:
135
+ logger.debug(f"Thread {threading.get_ident()} acquired mutex.")
136
+ return [
137
+ monitor.query_text
138
+ for monitor in sorted(self.queue, key=lambda x: x.timestamp, reverse=reverse)
139
+ ]
140
+
141
+ def to_json(self) -> str:
142
+ """Serialize the queue to a JSON string.
143
+
144
+ Args:
145
+ item_serializer: Optional function to serialize individual items.
146
+ If not provided, items must be JSON-serializable.
147
+
148
+ Returns:
149
+ A JSON string representing the queue's content and maxsize.
150
+ """
151
+ with self.mutex:
152
+ serialized_items = [item.to_json() for item in self.queue]
153
+
154
+ data = {"maxsize": self.maxsize, "items": serialized_items}
155
+ return json.dumps(data, ensure_ascii=False, indent=2)
156
+
157
+ @classmethod
158
+ def from_json(cls, json_str: str) -> "QueryMonitorQueue":
159
+ """Create a new AutoDroppingQueue from a JSON string.
160
+
161
+ Args:
162
+ json_str: JSON string created by to_json()
163
+ item_deserializer: Optional function to reconstruct items from dicts.
164
+ If not provided, items are used as-is.
165
+
166
+ Returns:
167
+ A new AutoDroppingQueue instance with deserialized data.
168
+ """
169
+ data = json.loads(json_str)
170
+ maxsize = data.get("maxsize", 0)
171
+ item_strs = data.get("items", [])
172
+
173
+ queue = cls(maxsize=maxsize)
174
+
175
+ items = [QueryMonitorItem.from_json(json_str=item_str) for item_str in item_strs]
176
+
177
+ # Fix: Add error handling for put operations
178
+ for item in items:
179
+ try:
180
+ queue.put(item) # Use put() to respect maxsize and auto-drop behavior
181
+ except Exception as e:
182
+ logger.error(f"Failed to add item to queue: {e}")
183
+ # Continue with other items instead of failing completely
184
+
185
+ return queue
186
+
187
+
188
+ # ============== Memories ==============
189
+ class MemoryMonitorItem(BaseModel, DictConversionMixin):
190
+ """
191
+ Represents a memory item in the monitoring system.
192
+
193
+ Note: This class does NOT have a timestamp field, unlike QueryMonitorItem.
194
+ For sorting by recency, use sorting_score or importance_score instead.
195
+ """
196
+
197
+ item_id: str = Field(
198
+ description="Unique identifier for the memory item", default_factory=lambda: str(uuid4())
199
+ )
200
+ memory_text: str = Field(
201
+ ...,
202
+ description="The actual content of the memory",
203
+ min_length=1,
204
+ )
205
+ tree_memory_item: TextualMemoryItem | None = Field(
206
+ default=None, description="Optional textual memory item"
207
+ )
208
+ tree_memory_item_mapping_key: str = Field(
209
+ description="Key generated from memory_text using transform_name_to_key",
210
+ )
211
+ keywords_score: float = Field(
212
+ default=NOT_INITIALIZED,
213
+ description="The score generate by counting keywords in queries",
214
+ ge=NOT_INITIALIZED, # Minimum value of 0
215
+ )
216
+ sorting_score: float = Field(
217
+ default=NOT_INITIALIZED,
218
+ description="The score generate from rerank process",
219
+ ge=NOT_INITIALIZED, # Minimum value of 0
220
+ )
221
+ importance_score: float = Field(
222
+ default=NOT_INITIALIZED,
223
+ description="Numerical score representing the memory's importance",
224
+ ge=NOT_INITIALIZED, # Minimum value of 0
225
+ )
226
+ recording_count: int = Field(
227
+ default=1,
228
+ description="How many times this memory has been recorded",
229
+ ge=1,
230
+ )
231
+
232
+ @field_validator("tree_memory_item_mapping_key", mode="before")
233
+ def generate_mapping_key(cls, v, values): # noqa: N805
234
+ if v is None and "memory_text" in values:
235
+ return transform_name_to_key(values["memory_text"])
236
+ return v
237
+
238
+ def get_importance_score(self, weight_vector: list[float] | None = None) -> float:
239
+ return self._get_complex_importance_score(weight_vector=weight_vector)
240
+
241
+ def _get_complex_importance_score(self, weight_vector: list[float] | None = None) -> float:
242
+ """Calculate traditional importance score using existing logic"""
243
+ if weight_vector is None:
244
+ logger.warning("weight_vector of get_complex_score is None.")
245
+ weight_vector = DEFAULT_WEIGHT_VECTOR_FOR_RANKING
246
+
247
+ # Fix: Add proper validation for weight_vector
248
+ if not weight_vector or len(weight_vector) != 3 or abs(sum(weight_vector) - 1.0) > 1e-6:
249
+ raise ValueError("weight_vector must be provided, have length 3, and sum to 1.0")
250
+
251
+ # Fix: Handle uninitialized scores safely
252
+ sorting_score = self.sorting_score if self.sorting_score != NOT_INITIALIZED else 0.0
253
+ keywords_score = self.keywords_score if self.keywords_score != NOT_INITIALIZED else 0.0
254
+
255
+ normalized_keywords_score = min(keywords_score * weight_vector[1], 5)
256
+ normalized_recording_count_score = min(self.recording_count * weight_vector[2], 2)
257
+ self.importance_score = (
258
+ sorting_score * weight_vector[0]
259
+ + normalized_keywords_score * weight_vector[1]
260
+ + normalized_recording_count_score * weight_vector[2]
261
+ )
262
+ return self.importance_score
263
+
264
+
265
+ class MemoryMonitorManager(BaseModel, DictConversionMixin):
266
+ user_id: str = Field(..., description="Required user identifier", min_length=1)
267
+ mem_cube_id: str = Field(..., description="Required memory cube identifier", min_length=1)
268
+ memories: list[MemoryMonitorItem] = Field(
269
+ default_factory=list, description="Collection of memory items"
270
+ )
271
+ max_capacity: int | None = Field(
272
+ default=None, description="Maximum number of memories allowed (None for unlimited)", ge=1
273
+ )
274
+
275
+ @computed_field
276
+ @property
277
+ def memory_size(self) -> int:
278
+ """Automatically calculated count of memory items."""
279
+ return len(self.memories)
280
+
281
+ @property
282
+ def memories_mapping_dict(self) -> dict[str, MemoryMonitorItem]:
283
+ """
284
+ Generate a mapping dictionary for the memories in MemoryMonitorManager,
285
+ using tree_memory_item_mapping_key as the key and MemoryMonitorItem as the value.
286
+
287
+ Returns:
288
+ Dict[str, MemoryMonitorItem]: A dictionary where keys are
289
+ tree_memory_item_mapping_key values from MemoryMonitorItem,
290
+ and values are the corresponding MemoryMonitorItem objects.
291
+ """
292
+ mapping_dict = {
293
+ mem_item.tree_memory_item_mapping_key: mem_item for mem_item in self.memories
294
+ }
295
+
296
+ logger.debug(
297
+ f"Generated memories mapping dict for user_id={self.user_id}, "
298
+ f"mem_cube_id={self.mem_cube_id}, "
299
+ f"total_items={len(mapping_dict)}, "
300
+ f"source_memory_count={len(self.memories)}"
301
+ )
302
+ return mapping_dict
303
+
304
+ def get_sorted_mem_monitors(self, reverse=True) -> list[MemoryMonitorItem]:
305
+ """
306
+ Retrieve memory monitors sorted by their ranking score in descending order.
307
+
308
+ Returns:
309
+ list[MemoryMonitorItem]: Sorted list of memory monitor items.
310
+ """
311
+ return sorted(
312
+ self.memories,
313
+ key=lambda item: item.get_importance_score(
314
+ weight_vector=DEFAULT_WEIGHT_VECTOR_FOR_RANKING
315
+ ),
316
+ reverse=reverse,
317
+ )
318
+
319
+ def update_memories(
320
+ self, new_memory_monitors: list[MemoryMonitorItem], partial_retention_number: int
321
+ ) -> list[MemoryMonitorItem]: # Fix: Correct return type
322
+ """
323
+ Update memories based on monitor_working_memories.
324
+ """
325
+
326
+ # Validate partial_retention_number
327
+ if partial_retention_number < 0:
328
+ raise ValueError("partial_retention_number must be non-negative")
329
+
330
+ # Step 1: Update existing memories or add new ones
331
+ added_count = 0
332
+ memories_mapping_dict = self.memories_mapping_dict
333
+ new_mem_set = set()
334
+ for memory_monitor in new_memory_monitors:
335
+ if memory_monitor.tree_memory_item_mapping_key in memories_mapping_dict:
336
+ # Update existing memory
337
+ item: MemoryMonitorItem = memories_mapping_dict[
338
+ memory_monitor.tree_memory_item_mapping_key
339
+ ]
340
+ item.recording_count += 1
341
+ item.keywords_score = memory_monitor.keywords_score
342
+ item.sorting_score = memory_monitor.sorting_score
343
+ else:
344
+ # Add new memory
345
+ self.memories.append(memory_monitor)
346
+ added_count += 1
347
+
348
+ new_mem_set.add(memory_monitor.tree_memory_item_mapping_key)
349
+
350
+ # Step 2: Identify memories to remove
351
+ old_mem_monitor_list = []
352
+ for mem_monitor in self.memories:
353
+ if mem_monitor.tree_memory_item_mapping_key not in new_mem_set:
354
+ old_mem_monitor_list.append(mem_monitor)
355
+
356
+ # Sort memories by recording_count in descending order
357
+ sorted_old_mem_monitors = sorted(
358
+ old_mem_monitor_list,
359
+ key=lambda item: item.get_importance_score(
360
+ weight_vector=DEFAULT_WEIGHT_VECTOR_FOR_RANKING
361
+ ),
362
+ reverse=True,
363
+ )
364
+
365
+ # Fix: Add bounds checking to prevent IndexError
366
+ if partial_retention_number > len(sorted_old_mem_monitors):
367
+ partial_retention_number = len(sorted_old_mem_monitors)
368
+ logger.info(
369
+ f"partial_retention_number adjusted to {partial_retention_number} to match available old memories"
370
+ )
371
+
372
+ # Keep the top N old memories
373
+ memories_to_remove = sorted_old_mem_monitors[partial_retention_number:]
374
+ memories_to_change_score = sorted_old_mem_monitors[:partial_retention_number]
375
+
376
+ # Step 3: Remove identified memories and change the scores of left old memories
377
+ for memory in memories_to_remove:
378
+ self.memories.remove(memory)
379
+
380
+ for memory in memories_to_change_score:
381
+ memory.sorting_score = 0
382
+ memory.recording_count = 1
383
+ memory.keywords_score = 0
384
+
385
+ # Step 4: Enforce max_capacity if set
386
+ # Fix: Handle max_capacity safely
387
+ if self.max_capacity is not None:
388
+ sorted_memories = sorted(
389
+ self.memories,
390
+ key=lambda item: item.get_importance_score(
391
+ weight_vector=DEFAULT_WEIGHT_VECTOR_FOR_RANKING
392
+ ),
393
+ reverse=True,
394
+ )
395
+ # Keep only the top max_capacity memories
396
+ self.memories = sorted_memories[: self.max_capacity]
397
+
398
+ # Log the update result
399
+ logger.info(
400
+ f"Updated monitor manager for user {self.user_id}, mem_cube {self.mem_cube_id}: "
401
+ f"Total memories: {len(self.memories)}, "
402
+ f"Added/Updated: {added_count}, "
403
+ f"Removed: {len(memories_to_remove)} (excluding top {partial_retention_number} by recording_count)"
404
+ )
405
+
406
+ return self.memories
@@ -0,0 +1,132 @@
1
+ import os
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from pydantic import BaseModel, Field, computed_field
10
+
11
+ from memos.log import get_logger
12
+ from memos.mem_scheduler.general_modules.misc import DictConversionMixin
13
+ from memos.mem_scheduler.utils.db_utils import get_utc_now
14
+
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ FILE_PATH = Path(__file__).absolute()
19
+ BASE_DIR = FILE_PATH.parent.parent.parent.parent.parent
20
+
21
+
22
+ # ============== Schedule Task Definitaion ==============
23
+ class TaskPriorityLevel(Enum):
24
+ # priority top
25
+ LEVEL_1 = 1
26
+ LEVEL_2 = 2
27
+ LEVEL_3 = 3
28
+ # priority bottom
29
+
30
+
31
+ QUERY_TASK_LABEL = "query"
32
+ ANSWER_TASK_LABEL = "answer"
33
+ ADD_TASK_LABEL = "add"
34
+ MEM_READ_TASK_LABEL = "mem_read"
35
+ MEM_ORGANIZE_TASK_LABEL = "mem_organize"
36
+ MEM_UPDATE_TASK_LABEL = "mem_update"
37
+ MEM_ARCHIVE_TASK_LABEL = "mem_archive"
38
+ API_MIX_SEARCH_TASK_LABEL = "api_mix_search"
39
+ PREF_ADD_TASK_LABEL = "pref_add"
40
+ MEM_FEEDBACK_TASK_LABEL = "mem_feedback"
41
+
42
+ # Additional constants moved from general_schemas
43
+ DEFAULT_MAX_QUERY_KEY_WORDS = 1000
44
+ LONG_TERM_MEMORY_TYPE = "LongTermMemory"
45
+ USER_INPUT_TYPE = "UserInput"
46
+ NOT_APPLICABLE_TYPE = "NotApplicable"
47
+
48
+
49
+ # scheduler daemon defaults
50
+ # Interval in seconds for periodically releasing stale pending messages
51
+ DEFAULT_PENDING_REQUEUE_INTERVAL_SEC = 30.0
52
+
53
+ # Interval in seconds for refreshing cached Redis stream keys
54
+ DEFAULT_STREAM_KEYS_REFRESH_INTERVAL_SEC = 30.0
55
+
56
+ # Interval in seconds for batching and cleaning up deletions (xdel)
57
+ DEFAULT_DELETE_CLEANUP_INTERVAL_SEC = 30.0
58
+
59
+ # pending claim configuration
60
+ # Only claim pending messages whose idle time exceeds this threshold.
61
+ # Unit: milliseconds. Default: 1 hour.
62
+ DEFAULT_PENDING_CLAIM_MIN_IDLE_MS = 3_600_000
63
+
64
+
65
+ # Recency threshold for active streams
66
+ # Consider a stream "active" if its last message is within this window.
67
+ # Unit: seconds. Default: 1 hours.
68
+ DEFAULT_STREAM_RECENT_ACTIVE_SECONDS = 3_600.0
69
+
70
+
71
+ # Inactivity threshold for stream deletion
72
+ # Delete streams whose last message ID timestamp is older than this threshold.
73
+ # Unit: seconds. Default: 2 hour.
74
+ DEFAULT_STREAM_INACTIVITY_DELETE_SECONDS = 7_200.0
75
+
76
+
77
+ # task queue
78
+ DEFAULT_STREAM_KEY_PREFIX = os.getenv(
79
+ "MEMSCHEDULER_STREAM_KEY_PREFIX", "scheduler:messages:stream:v2.0"
80
+ )
81
+
82
+
83
+ # ============== Running Tasks ==============
84
+ class RunningTaskItem(BaseModel, DictConversionMixin):
85
+ """Data class for tracking running tasks in SchedulerDispatcher."""
86
+
87
+ item_id: str = Field(
88
+ description="Unique identifier for the task item", default_factory=lambda: str(uuid4())
89
+ )
90
+ user_id: str = Field(..., description="Required user identifier", min_length=1)
91
+ mem_cube_id: str = Field(..., description="Required memory cube identifier", min_length=1)
92
+ task_info: str = Field(..., description="Information about the task being executed")
93
+ task_name: str = Field(..., description="Name/type of the task handler")
94
+ start_time: datetime = Field(description="Task start time", default_factory=get_utc_now)
95
+ end_time: datetime | None = Field(default=None, description="Task completion time")
96
+ status: str = Field(default="running", description="Task status: running, completed, failed")
97
+ result: Any | None = Field(default=None, description="Task execution result")
98
+ error_message: str | None = Field(default=None, description="Error message if task failed")
99
+ messages: list[Any] | None = Field(
100
+ default=None, description="List of messages being processed by this task"
101
+ )
102
+
103
+ def mark_completed(self, result: Any | None = None) -> None:
104
+ """Mark task as completed with optional result."""
105
+ self.end_time = get_utc_now()
106
+ self.status = "completed"
107
+ self.result = result
108
+
109
+ def mark_failed(self, error_message: str) -> None:
110
+ """Mark task as failed with error message."""
111
+ self.end_time = get_utc_now()
112
+ self.status = "failed"
113
+ self.error_message = error_message
114
+
115
+ @computed_field
116
+ @property
117
+ def duration_seconds(self) -> float | None:
118
+ """Calculate task duration in seconds."""
119
+ if self.end_time:
120
+ return (self.end_time - self.start_time).total_seconds()
121
+ return None
122
+
123
+ def get_execution_info(self) -> str:
124
+ """Get formatted execution information for logging."""
125
+ duration = self.duration_seconds
126
+ duration_str = f"{duration:.2f}s" if duration else "ongoing"
127
+
128
+ return (
129
+ f"Task {self.task_name} (ID: {self.item_id[:8]}) "
130
+ f"for user {self.user_id}, cube {self.mem_cube_id} - "
131
+ f"Status: {self.status}, Duration: {duration_str}"
132
+ )
File without changes