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,497 @@
1
+ """
2
+ Scheduler handler for scheduler management functionality.
3
+
4
+ This module handles all scheduler-related operations including status checking,
5
+ waiting for idle state, and streaming progress updates.
6
+ """
7
+
8
+ import json
9
+ import time
10
+ import traceback
11
+
12
+ from collections import Counter
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ from fastapi import HTTPException
17
+ from fastapi.responses import StreamingResponse
18
+
19
+ # Imports for new implementation
20
+ from memos.api.product_models import (
21
+ AllStatusResponse,
22
+ AllStatusResponseData,
23
+ StatusResponse,
24
+ StatusResponseItem,
25
+ TaskQueueData,
26
+ TaskQueueResponse,
27
+ TaskSummary,
28
+ )
29
+ from memos.log import get_logger
30
+ from memos.mem_scheduler.base_scheduler import BaseScheduler
31
+ from memos.mem_scheduler.optimized_scheduler import OptimizedScheduler
32
+ from memos.mem_scheduler.utils.status_tracker import TaskStatusTracker
33
+
34
+
35
+ logger = get_logger(__name__)
36
+
37
+
38
+ def handle_scheduler_allstatus(
39
+ mem_scheduler: BaseScheduler,
40
+ status_tracker: TaskStatusTracker,
41
+ ) -> AllStatusResponse:
42
+ """
43
+ Get aggregated scheduler status metrics (no per-task payload).
44
+
45
+ Args:
46
+ mem_scheduler: The BaseScheduler instance.
47
+ status_tracker: The TaskStatusTracker instance.
48
+
49
+ Returns:
50
+ AllStatusResponse with aggregated status data.
51
+ """
52
+
53
+ def _summarize_tasks(task_details: list[dict[str, Any]]) -> TaskSummary:
54
+ """Aggregate counts by status for the provided task details (tracker data)."""
55
+ counter = Counter()
56
+ for detail in task_details:
57
+ status = detail.get("status")
58
+ if status:
59
+ counter[status] += 1
60
+
61
+ total = sum(counter.values())
62
+ return TaskSummary(
63
+ waiting=counter.get("waiting", 0),
64
+ in_progress=counter.get("in_progress", 0),
65
+ completed=counter.get("completed", 0),
66
+ pending=counter.get("pending", counter.get("waiting", 0)),
67
+ failed=counter.get("failed", 0),
68
+ cancelled=counter.get("cancelled", 0),
69
+ total=total,
70
+ )
71
+
72
+ def _aggregate_counts_from_redis(
73
+ tracker: TaskStatusTracker, max_age_seconds: float = 86400
74
+ ) -> TaskSummary | None:
75
+ """Stream status counts directly from Redis to avoid loading all task payloads."""
76
+ redis_client = getattr(tracker, "redis", None)
77
+ if not redis_client:
78
+ return None
79
+
80
+ counter = Counter()
81
+ now = datetime.now(timezone.utc).timestamp()
82
+
83
+ # Scan task_meta keys, then hscan each hash in batches
84
+ cursor: int | str = 0
85
+ while True:
86
+ cursor, keys = redis_client.scan(cursor=cursor, match="memos:task_meta:*", count=200)
87
+ for key in keys:
88
+ h_cursor: int | str = 0
89
+ while True:
90
+ h_cursor, fields = redis_client.hscan(key, cursor=h_cursor, count=500)
91
+ for value in fields.values():
92
+ try:
93
+ payload = json.loads(
94
+ value.decode("utf-8") if isinstance(value, bytes) else value
95
+ )
96
+ # Skip stale entries to reduce noise and load
97
+ ts = payload.get("submitted_at") or payload.get("started_at")
98
+ if ts:
99
+ try:
100
+ ts_dt = datetime.fromisoformat(ts)
101
+ ts_seconds = ts_dt.timestamp()
102
+ except Exception:
103
+ ts_seconds = None
104
+ if ts_seconds and (now - ts_seconds) > max_age_seconds:
105
+ continue
106
+ status = payload.get("status")
107
+ if status:
108
+ counter[status] += 1
109
+ except Exception:
110
+ continue
111
+ if h_cursor == 0 or h_cursor == "0":
112
+ break
113
+ if cursor == 0 or cursor == "0":
114
+ break
115
+
116
+ if not counter:
117
+ return TaskSummary() # Empty summary if nothing found
118
+
119
+ total = sum(counter.values())
120
+ return TaskSummary(
121
+ waiting=counter.get("waiting", 0),
122
+ in_progress=counter.get("in_progress", 0),
123
+ completed=counter.get("completed", 0),
124
+ pending=counter.get("pending", counter.get("waiting", 0)),
125
+ failed=counter.get("failed", 0),
126
+ cancelled=counter.get("cancelled", 0),
127
+ total=total,
128
+ )
129
+
130
+ try:
131
+ # Prefer streaming aggregation to avoid pulling all task payloads
132
+ all_tasks_summary = _aggregate_counts_from_redis(status_tracker)
133
+ if all_tasks_summary is None:
134
+ # Fallback: load all details then aggregate
135
+ global_tasks = status_tracker.get_all_tasks_global()
136
+ all_task_details: list[dict[str, Any]] = []
137
+ for _, tasks in global_tasks.items():
138
+ all_task_details.extend(tasks.values())
139
+ all_tasks_summary = _summarize_tasks(all_task_details)
140
+
141
+ # Scheduler view: assume tracker contains scheduler tasks; overlay queue monitor for live queue depth
142
+ sched_waiting = all_tasks_summary.waiting
143
+ sched_in_progress = all_tasks_summary.in_progress
144
+ sched_pending = all_tasks_summary.pending
145
+ sched_completed = all_tasks_summary.completed
146
+ sched_failed = all_tasks_summary.failed
147
+ sched_cancelled = all_tasks_summary.cancelled
148
+
149
+ # If queue monitor is available, prefer its live waiting/in_progress counts
150
+ if mem_scheduler.task_schedule_monitor:
151
+ queue_status_data = mem_scheduler.task_schedule_monitor.get_tasks_status() or {}
152
+ scheduler_waiting = 0
153
+ scheduler_in_progress = 0
154
+ scheduler_pending = 0
155
+ for key, value in queue_status_data.items():
156
+ if not key.startswith("scheduler:"):
157
+ continue
158
+ scheduler_in_progress += int(value.get("running", 0) or 0)
159
+ scheduler_pending += int(value.get("pending", value.get("remaining", 0)) or 0)
160
+ scheduler_waiting += int(value.get("remaining", 0) or 0)
161
+ sched_waiting = scheduler_waiting
162
+ sched_in_progress = scheduler_in_progress
163
+ sched_pending = scheduler_pending
164
+
165
+ scheduler_summary = TaskSummary(
166
+ waiting=sched_waiting,
167
+ in_progress=sched_in_progress,
168
+ pending=sched_pending,
169
+ completed=sched_completed,
170
+ failed=sched_failed,
171
+ cancelled=sched_cancelled,
172
+ total=sched_waiting
173
+ + sched_in_progress
174
+ + sched_completed
175
+ + sched_failed
176
+ + sched_cancelled,
177
+ )
178
+
179
+ return AllStatusResponse(
180
+ data=AllStatusResponseData(
181
+ scheduler_summary=scheduler_summary,
182
+ all_tasks_summary=all_tasks_summary,
183
+ )
184
+ )
185
+ except Exception as err:
186
+ logger.error(f"Failed to get full scheduler status: {traceback.format_exc()}")
187
+ raise HTTPException(status_code=500, detail="Failed to get full scheduler status") from err
188
+
189
+
190
+ def handle_scheduler_status(
191
+ user_id: str, status_tracker: TaskStatusTracker, task_id: str | None = None
192
+ ) -> StatusResponse:
193
+ """
194
+ Get scheduler running status for one or all tasks of a user.
195
+
196
+ Retrieves task statuses from the persistent TaskStatusTracker.
197
+
198
+ Args:
199
+ user_id: User ID to query for.
200
+ status_tracker: The TaskStatusTracker instance.
201
+ task_id: Optional Task ID to query. Can be either:
202
+ - business_task_id (will aggregate all related item statuses)
203
+ - item_id (will return single item status)
204
+
205
+ Returns:
206
+ StatusResponse with a list of task statuses.
207
+
208
+ Raises:
209
+ HTTPException: If a specific task is not found.
210
+ """
211
+ response_data: list[StatusResponseItem] = []
212
+
213
+ try:
214
+ if task_id:
215
+ # First try as business_task_id (aggregated query)
216
+ business_task_data = status_tracker.get_task_status_by_business_id(task_id, user_id)
217
+ if business_task_data:
218
+ response_data.append(
219
+ StatusResponseItem(task_id=task_id, status=business_task_data["status"])
220
+ )
221
+ else:
222
+ # Fallback: try as item_id (single item query)
223
+ item_task_data = status_tracker.get_task_status(task_id, user_id)
224
+ if not item_task_data:
225
+ raise HTTPException(
226
+ status_code=404, detail=f"Task {task_id} not found for user {user_id}"
227
+ )
228
+ response_data.append(
229
+ StatusResponseItem(task_id=task_id, status=item_task_data["status"])
230
+ )
231
+ else:
232
+ all_tasks = status_tracker.get_all_tasks_for_user(user_id)
233
+ # The plan returns an empty list, which is good.
234
+ # No need to check "if not all_tasks" explicitly before the list comprehension
235
+ response_data = [
236
+ StatusResponseItem(task_id=tid, status=t_data["status"])
237
+ for tid, t_data in all_tasks.items()
238
+ ]
239
+
240
+ return StatusResponse(data=response_data)
241
+ except HTTPException:
242
+ # Re-raise HTTPException directly to preserve its status code (e.g., 404)
243
+ raise
244
+ except Exception as err:
245
+ logger.error(f"Failed to get scheduler status for user {user_id}: {traceback.format_exc()}")
246
+ raise HTTPException(status_code=500, detail="Failed to get scheduler status") from err
247
+
248
+
249
+ def handle_task_queue_status(
250
+ user_id: str, mem_scheduler: OptimizedScheduler, task_id: str | None = None
251
+ ) -> TaskQueueResponse:
252
+ try:
253
+ queue_wrapper = getattr(mem_scheduler, "memos_message_queue", None)
254
+ if queue_wrapper is None:
255
+ raise HTTPException(status_code=503, detail="Scheduler queue is not available")
256
+
257
+ # Unwrap to the underlying queue if wrapped by ScheduleTaskQueue
258
+ queue = getattr(queue_wrapper, "memos_message_queue", queue_wrapper)
259
+
260
+ # Only support Redis-backed queue for now; try lazy init if not connected
261
+ redis_conn = getattr(queue, "_redis_conn", None)
262
+ if redis_conn is None:
263
+ try:
264
+ if hasattr(queue, "auto_initialize_redis"):
265
+ queue.auto_initialize_redis()
266
+ redis_conn = getattr(queue, "_redis_conn", None)
267
+ if redis_conn and hasattr(queue, "connect"):
268
+ queue.connect()
269
+ except Exception:
270
+ redis_conn = None
271
+
272
+ if redis_conn is None:
273
+ raise HTTPException(status_code=503, detail="Scheduler queue not connected to Redis")
274
+
275
+ # Use wrapper to list stream keys so it can adapt to local/redis queue
276
+ stream_keys = queue_wrapper.get_stream_keys()
277
+ # Filter by user_id; stream key format: {prefix}:{user_id}:{mem_cube_id}:{task_label}
278
+ user_stream_keys = [sk for sk in stream_keys if f":{user_id}:" in sk]
279
+
280
+ if not user_stream_keys:
281
+ raise HTTPException(
282
+ status_code=404, detail=f"No scheduler streams found for user {user_id}"
283
+ )
284
+
285
+ def _parse_user_id_from_stream(stream_key: str) -> str | None:
286
+ try:
287
+ parts = stream_key.split(":")
288
+ if len(parts) < 3:
289
+ return None
290
+ # prefix may contain multiple segments; user_id is the 2nd segment from the end - 1
291
+ return parts[-3]
292
+ except Exception:
293
+ return None
294
+
295
+ user_ids_present = {
296
+ uid for uid in (_parse_user_id_from_stream(sk) for sk in stream_keys) if uid
297
+ }
298
+
299
+ pending_total = 0
300
+ pending_detail: list[str] = []
301
+ remaining_total = 0
302
+ remaining_detail: list[str] = []
303
+
304
+ consumer_group = getattr(queue, "consumer_group", None) or "scheduler_group"
305
+ for sk in user_stream_keys:
306
+ try:
307
+ pending_info = redis_conn.xpending(sk, consumer_group)
308
+ pending_count = pending_info[0] if pending_info else 0
309
+ except Exception:
310
+ pending_count = 0
311
+ pending_total += pending_count
312
+ pending_detail.append(f"{sk}:{pending_count}")
313
+
314
+ try:
315
+ remaining_count = redis_conn.xlen(sk)
316
+ except Exception:
317
+ remaining_count = 0
318
+ remaining_total += remaining_count
319
+ remaining_detail.append(f"{sk}:{remaining_count}")
320
+
321
+ data = TaskQueueData(
322
+ user_id=user_id,
323
+ user_name=None,
324
+ mem_cube_id=None,
325
+ stream_keys=user_stream_keys,
326
+ users_count=len(user_ids_present),
327
+ pending_tasks_count=pending_total,
328
+ remaining_tasks_count=remaining_total,
329
+ pending_tasks_detail=pending_detail,
330
+ remaining_tasks_detail=remaining_detail,
331
+ )
332
+ return TaskQueueResponse(data=data)
333
+ except HTTPException:
334
+ # Re-raise HTTPException directly to preserve its status code (e.g., 404)
335
+ raise
336
+ except Exception as err:
337
+ logger.error(
338
+ f"Failed to get task queue status for user {user_id}: {traceback.format_exc()}"
339
+ )
340
+ raise HTTPException(status_code=500, detail="Failed to get scheduler status") from err
341
+
342
+
343
+ def handle_scheduler_wait(
344
+ user_name: str,
345
+ status_tracker: TaskStatusTracker,
346
+ timeout_seconds: float = 120.0,
347
+ poll_interval: float = 0.5,
348
+ ) -> dict[str, Any]:
349
+ """
350
+ Wait until the scheduler is idle for a specific user.
351
+
352
+ Blocks and polls the new /scheduler/status endpoint until no tasks are in
353
+ 'waiting' or 'in_progress' state, or until a timeout is reached.
354
+
355
+ Args:
356
+ user_name: User name to wait for.
357
+ status_tracker: The TaskStatusTracker instance.
358
+ timeout_seconds: Maximum wait time in seconds.
359
+ poll_interval: Polling interval in seconds.
360
+
361
+ Returns:
362
+ Dictionary with wait result and statistics.
363
+
364
+ Raises:
365
+ HTTPException: If wait operation fails.
366
+ """
367
+ start_time = time.time()
368
+ try:
369
+ while time.time() - start_time < timeout_seconds:
370
+ # Directly call the new, reliable status logic
371
+ status_response = handle_scheduler_status(
372
+ user_id=user_name, status_tracker=status_tracker
373
+ )
374
+
375
+ # System is idle if the data list is empty or no tasks are active
376
+ is_idle = not status_response.data or all(
377
+ task.status in ["completed", "failed", "cancelled"] for task in status_response.data
378
+ )
379
+
380
+ if is_idle:
381
+ return {
382
+ "message": "idle",
383
+ "data": {
384
+ "running_tasks": 0, # Kept for compatibility
385
+ "waited_seconds": round(time.time() - start_time, 3),
386
+ "timed_out": False,
387
+ "user_name": user_name,
388
+ },
389
+ }
390
+
391
+ time.sleep(poll_interval)
392
+
393
+ # Timeout occurred
394
+ final_status = handle_scheduler_status(user_id=user_name, status_tracker=status_tracker)
395
+ active_tasks = [t for t in final_status.data if t.status in ["waiting", "in_progress"]]
396
+
397
+ return {
398
+ "message": "timeout",
399
+ "data": {
400
+ "running_tasks": len(active_tasks), # A more accurate count of active tasks
401
+ "waited_seconds": round(time.time() - start_time, 3),
402
+ "timed_out": True,
403
+ "user_name": user_name,
404
+ },
405
+ }
406
+ except HTTPException:
407
+ # Re-raise HTTPException directly to preserve its status code
408
+ raise
409
+ except Exception as err:
410
+ logger.error(
411
+ f"Failed while waiting for scheduler for user {user_name}: {traceback.format_exc()}"
412
+ )
413
+ raise HTTPException(status_code=500, detail="Failed while waiting for scheduler") from err
414
+
415
+
416
+ def handle_scheduler_wait_stream(
417
+ user_name: str,
418
+ status_tracker: TaskStatusTracker,
419
+ timeout_seconds: float = 120.0,
420
+ poll_interval: float = 0.5,
421
+ instance_id: str = "",
422
+ ) -> StreamingResponse:
423
+ """
424
+ Stream scheduler progress via Server-Sent Events (SSE) using the new status endpoint.
425
+
426
+ Emits periodic heartbeat frames while tasks are active, then a final
427
+ status frame indicating idle or timeout.
428
+
429
+ Args:
430
+ user_name: User name to monitor.
431
+ status_tracker: The TaskStatusTracker instance.
432
+ timeout_seconds: Maximum stream duration in seconds.
433
+ poll_interval: Polling interval between updates.
434
+ instance_id: Instance ID for response.
435
+
436
+ Returns:
437
+ StreamingResponse with SSE formatted progress updates.
438
+ """
439
+
440
+ def event_generator():
441
+ start_time = time.time()
442
+ try:
443
+ while True:
444
+ elapsed = time.time() - start_time
445
+ if elapsed > timeout_seconds:
446
+ # Send timeout message and break
447
+ final_status = handle_scheduler_status(
448
+ user_id=user_name, status_tracker=status_tracker
449
+ )
450
+ active_tasks = [
451
+ t for t in final_status.data if t.status in ["waiting", "in_progress"]
452
+ ]
453
+ payload = {
454
+ "user_name": user_name,
455
+ "active_tasks": len(active_tasks),
456
+ "elapsed_seconds": round(elapsed, 3),
457
+ "status": "timeout",
458
+ "timed_out": True,
459
+ "instance_id": instance_id,
460
+ }
461
+ yield "data: " + json.dumps(payload, ensure_ascii=False) + "\n\n"
462
+ break
463
+
464
+ # Get status
465
+ status_response = handle_scheduler_status(
466
+ user_id=user_name, status_tracker=status_tracker
467
+ )
468
+ active_tasks = [
469
+ t for t in status_response.data if t.status in ["waiting", "in_progress"]
470
+ ]
471
+ num_active = len(active_tasks)
472
+
473
+ payload = {
474
+ "user_name": user_name,
475
+ "active_tasks": num_active,
476
+ "elapsed_seconds": round(elapsed, 3),
477
+ "status": "running" if num_active > 0 else "idle",
478
+ "instance_id": instance_id,
479
+ }
480
+ yield "data: " + json.dumps(payload, ensure_ascii=False) + "\n\n"
481
+
482
+ if num_active == 0:
483
+ break # Exit loop if idle
484
+
485
+ time.sleep(poll_interval)
486
+
487
+ except Exception as e:
488
+ err_payload = {
489
+ "status": "error",
490
+ "detail": "stream_failed",
491
+ "exception": str(e),
492
+ "user_name": user_name,
493
+ }
494
+ logger.error(f"Scheduler stream error for {user_name}: {traceback.format_exc()}")
495
+ yield "data: " + json.dumps(err_payload, ensure_ascii=False) + "\n\n"
496
+
497
+ return StreamingResponse(event_generator(), media_type="text/event-stream")