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.
- memoryos-2.0.3.dist-info/METADATA +418 -0
- memoryos-2.0.3.dist-info/RECORD +315 -0
- memoryos-2.0.3.dist-info/WHEEL +4 -0
- memoryos-2.0.3.dist-info/entry_points.txt +3 -0
- memoryos-2.0.3.dist-info/licenses/LICENSE +201 -0
- memos/__init__.py +20 -0
- memos/api/client.py +571 -0
- memos/api/config.py +1018 -0
- memos/api/context/dependencies.py +50 -0
- memos/api/exceptions.py +53 -0
- memos/api/handlers/__init__.py +62 -0
- memos/api/handlers/add_handler.py +158 -0
- memos/api/handlers/base_handler.py +194 -0
- memos/api/handlers/chat_handler.py +1401 -0
- memos/api/handlers/component_init.py +388 -0
- memos/api/handlers/config_builders.py +190 -0
- memos/api/handlers/feedback_handler.py +93 -0
- memos/api/handlers/formatters_handler.py +237 -0
- memos/api/handlers/memory_handler.py +316 -0
- memos/api/handlers/scheduler_handler.py +497 -0
- memos/api/handlers/search_handler.py +222 -0
- memos/api/handlers/suggestion_handler.py +117 -0
- memos/api/mcp_serve.py +614 -0
- memos/api/middleware/request_context.py +101 -0
- memos/api/product_api.py +38 -0
- memos/api/product_models.py +1206 -0
- memos/api/routers/__init__.py +1 -0
- memos/api/routers/product_router.py +477 -0
- memos/api/routers/server_router.py +394 -0
- memos/api/server_api.py +44 -0
- memos/api/start_api.py +433 -0
- memos/chunkers/__init__.py +4 -0
- memos/chunkers/base.py +24 -0
- memos/chunkers/charactertext_chunker.py +41 -0
- memos/chunkers/factory.py +24 -0
- memos/chunkers/markdown_chunker.py +62 -0
- memos/chunkers/sentence_chunker.py +54 -0
- memos/chunkers/simple_chunker.py +50 -0
- memos/cli.py +113 -0
- memos/configs/__init__.py +0 -0
- memos/configs/base.py +82 -0
- memos/configs/chunker.py +59 -0
- memos/configs/embedder.py +88 -0
- memos/configs/graph_db.py +236 -0
- memos/configs/internet_retriever.py +100 -0
- memos/configs/llm.py +151 -0
- memos/configs/mem_agent.py +54 -0
- memos/configs/mem_chat.py +81 -0
- memos/configs/mem_cube.py +105 -0
- memos/configs/mem_os.py +83 -0
- memos/configs/mem_reader.py +91 -0
- memos/configs/mem_scheduler.py +385 -0
- memos/configs/mem_user.py +70 -0
- memos/configs/memory.py +324 -0
- memos/configs/parser.py +38 -0
- memos/configs/reranker.py +18 -0
- memos/configs/utils.py +8 -0
- memos/configs/vec_db.py +80 -0
- memos/context/context.py +355 -0
- memos/dependency.py +52 -0
- memos/deprecation.py +262 -0
- memos/embedders/__init__.py +0 -0
- memos/embedders/ark.py +95 -0
- memos/embedders/base.py +106 -0
- memos/embedders/factory.py +29 -0
- memos/embedders/ollama.py +77 -0
- memos/embedders/sentence_transformer.py +49 -0
- memos/embedders/universal_api.py +51 -0
- memos/exceptions.py +30 -0
- memos/graph_dbs/__init__.py +0 -0
- memos/graph_dbs/base.py +274 -0
- memos/graph_dbs/factory.py +27 -0
- memos/graph_dbs/item.py +46 -0
- memos/graph_dbs/nebular.py +1794 -0
- memos/graph_dbs/neo4j.py +1942 -0
- memos/graph_dbs/neo4j_community.py +1058 -0
- memos/graph_dbs/polardb.py +5446 -0
- memos/hello_world.py +97 -0
- memos/llms/__init__.py +0 -0
- memos/llms/base.py +25 -0
- memos/llms/deepseek.py +13 -0
- memos/llms/factory.py +38 -0
- memos/llms/hf.py +443 -0
- memos/llms/hf_singleton.py +114 -0
- memos/llms/ollama.py +135 -0
- memos/llms/openai.py +222 -0
- memos/llms/openai_new.py +198 -0
- memos/llms/qwen.py +13 -0
- memos/llms/utils.py +14 -0
- memos/llms/vllm.py +218 -0
- memos/log.py +237 -0
- memos/mem_agent/base.py +19 -0
- memos/mem_agent/deepsearch_agent.py +391 -0
- memos/mem_agent/factory.py +36 -0
- memos/mem_chat/__init__.py +0 -0
- memos/mem_chat/base.py +30 -0
- memos/mem_chat/factory.py +21 -0
- memos/mem_chat/simple.py +200 -0
- memos/mem_cube/__init__.py +0 -0
- memos/mem_cube/base.py +30 -0
- memos/mem_cube/general.py +240 -0
- memos/mem_cube/navie.py +172 -0
- memos/mem_cube/utils.py +169 -0
- memos/mem_feedback/base.py +15 -0
- memos/mem_feedback/feedback.py +1192 -0
- memos/mem_feedback/simple_feedback.py +40 -0
- memos/mem_feedback/utils.py +230 -0
- memos/mem_os/client.py +5 -0
- memos/mem_os/core.py +1203 -0
- memos/mem_os/main.py +582 -0
- memos/mem_os/product.py +1608 -0
- memos/mem_os/product_server.py +455 -0
- memos/mem_os/utils/default_config.py +359 -0
- memos/mem_os/utils/format_utils.py +1403 -0
- memos/mem_os/utils/reference_utils.py +162 -0
- memos/mem_reader/__init__.py +0 -0
- memos/mem_reader/base.py +47 -0
- memos/mem_reader/factory.py +53 -0
- memos/mem_reader/memory.py +298 -0
- memos/mem_reader/multi_modal_struct.py +965 -0
- memos/mem_reader/read_multi_modal/__init__.py +43 -0
- memos/mem_reader/read_multi_modal/assistant_parser.py +311 -0
- memos/mem_reader/read_multi_modal/base.py +273 -0
- memos/mem_reader/read_multi_modal/file_content_parser.py +826 -0
- memos/mem_reader/read_multi_modal/image_parser.py +359 -0
- memos/mem_reader/read_multi_modal/multi_modal_parser.py +252 -0
- memos/mem_reader/read_multi_modal/string_parser.py +139 -0
- memos/mem_reader/read_multi_modal/system_parser.py +327 -0
- memos/mem_reader/read_multi_modal/text_content_parser.py +131 -0
- memos/mem_reader/read_multi_modal/tool_parser.py +210 -0
- memos/mem_reader/read_multi_modal/user_parser.py +218 -0
- memos/mem_reader/read_multi_modal/utils.py +358 -0
- memos/mem_reader/simple_struct.py +912 -0
- memos/mem_reader/strategy_struct.py +163 -0
- memos/mem_reader/utils.py +157 -0
- memos/mem_scheduler/__init__.py +0 -0
- memos/mem_scheduler/analyzer/__init__.py +0 -0
- memos/mem_scheduler/analyzer/api_analyzer.py +714 -0
- memos/mem_scheduler/analyzer/eval_analyzer.py +219 -0
- memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +571 -0
- memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
- memos/mem_scheduler/base_scheduler.py +1319 -0
- memos/mem_scheduler/general_modules/__init__.py +0 -0
- memos/mem_scheduler/general_modules/api_misc.py +137 -0
- memos/mem_scheduler/general_modules/base.py +80 -0
- memos/mem_scheduler/general_modules/init_components_for_scheduler.py +425 -0
- memos/mem_scheduler/general_modules/misc.py +313 -0
- memos/mem_scheduler/general_modules/scheduler_logger.py +389 -0
- memos/mem_scheduler/general_modules/task_threads.py +315 -0
- memos/mem_scheduler/general_scheduler.py +1495 -0
- memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
- memos/mem_scheduler/memory_manage_modules/memory_filter.py +306 -0
- memos/mem_scheduler/memory_manage_modules/retriever.py +547 -0
- memos/mem_scheduler/monitors/__init__.py +0 -0
- memos/mem_scheduler/monitors/dispatcher_monitor.py +366 -0
- memos/mem_scheduler/monitors/general_monitor.py +394 -0
- memos/mem_scheduler/monitors/task_schedule_monitor.py +254 -0
- memos/mem_scheduler/optimized_scheduler.py +410 -0
- memos/mem_scheduler/orm_modules/__init__.py +0 -0
- memos/mem_scheduler/orm_modules/api_redis_model.py +518 -0
- memos/mem_scheduler/orm_modules/base_model.py +729 -0
- memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
- memos/mem_scheduler/orm_modules/redis_model.py +699 -0
- memos/mem_scheduler/scheduler_factory.py +23 -0
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/analyzer_schemas.py +52 -0
- memos/mem_scheduler/schemas/api_schemas.py +233 -0
- memos/mem_scheduler/schemas/general_schemas.py +55 -0
- memos/mem_scheduler/schemas/message_schemas.py +173 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +406 -0
- memos/mem_scheduler/schemas/task_schemas.py +132 -0
- memos/mem_scheduler/task_schedule_modules/__init__.py +0 -0
- memos/mem_scheduler/task_schedule_modules/dispatcher.py +740 -0
- memos/mem_scheduler/task_schedule_modules/local_queue.py +247 -0
- memos/mem_scheduler/task_schedule_modules/orchestrator.py +74 -0
- memos/mem_scheduler/task_schedule_modules/redis_queue.py +1385 -0
- memos/mem_scheduler/task_schedule_modules/task_queue.py +162 -0
- memos/mem_scheduler/utils/__init__.py +0 -0
- memos/mem_scheduler/utils/api_utils.py +77 -0
- memos/mem_scheduler/utils/config_utils.py +100 -0
- memos/mem_scheduler/utils/db_utils.py +50 -0
- memos/mem_scheduler/utils/filter_utils.py +176 -0
- memos/mem_scheduler/utils/metrics.py +125 -0
- memos/mem_scheduler/utils/misc_utils.py +290 -0
- memos/mem_scheduler/utils/monitor_event_utils.py +67 -0
- memos/mem_scheduler/utils/status_tracker.py +229 -0
- memos/mem_scheduler/webservice_modules/__init__.py +0 -0
- memos/mem_scheduler/webservice_modules/rabbitmq_service.py +485 -0
- memos/mem_scheduler/webservice_modules/redis_service.py +380 -0
- memos/mem_user/factory.py +94 -0
- memos/mem_user/mysql_persistent_user_manager.py +271 -0
- memos/mem_user/mysql_user_manager.py +502 -0
- memos/mem_user/persistent_factory.py +98 -0
- memos/mem_user/persistent_user_manager.py +260 -0
- memos/mem_user/redis_persistent_user_manager.py +225 -0
- memos/mem_user/user_manager.py +488 -0
- memos/memories/__init__.py +0 -0
- memos/memories/activation/__init__.py +0 -0
- memos/memories/activation/base.py +42 -0
- memos/memories/activation/item.py +56 -0
- memos/memories/activation/kv.py +292 -0
- memos/memories/activation/vllmkv.py +219 -0
- memos/memories/base.py +19 -0
- memos/memories/factory.py +42 -0
- memos/memories/parametric/__init__.py +0 -0
- memos/memories/parametric/base.py +19 -0
- memos/memories/parametric/item.py +11 -0
- memos/memories/parametric/lora.py +41 -0
- memos/memories/textual/__init__.py +0 -0
- memos/memories/textual/base.py +92 -0
- memos/memories/textual/general.py +236 -0
- memos/memories/textual/item.py +304 -0
- memos/memories/textual/naive.py +187 -0
- memos/memories/textual/prefer_text_memory/__init__.py +0 -0
- memos/memories/textual/prefer_text_memory/adder.py +504 -0
- memos/memories/textual/prefer_text_memory/config.py +106 -0
- memos/memories/textual/prefer_text_memory/extractor.py +221 -0
- memos/memories/textual/prefer_text_memory/factory.py +85 -0
- memos/memories/textual/prefer_text_memory/retrievers.py +177 -0
- memos/memories/textual/prefer_text_memory/spliter.py +132 -0
- memos/memories/textual/prefer_text_memory/utils.py +93 -0
- memos/memories/textual/preference.py +344 -0
- memos/memories/textual/simple_preference.py +161 -0
- memos/memories/textual/simple_tree.py +69 -0
- memos/memories/textual/tree.py +459 -0
- memos/memories/textual/tree_text_memory/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/handler.py +184 -0
- memos/memories/textual/tree_text_memory/organize/manager.py +518 -0
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +238 -0
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +622 -0
- memos/memories/textual/tree_text_memory/retrieve/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/retrieve/advanced_searcher.py +364 -0
- memos/memories/textual/tree_text_memory/retrieve/bm25_util.py +186 -0
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +419 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +270 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +102 -0
- memos/memories/textual/tree_text_memory/retrieve/reasoner.py +61 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +497 -0
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +111 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +16 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieve_utils.py +472 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +848 -0
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +135 -0
- memos/memories/textual/tree_text_memory/retrieve/utils.py +54 -0
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +387 -0
- memos/memos_tools/dinding_report_bot.py +453 -0
- memos/memos_tools/lockfree_dict.py +120 -0
- memos/memos_tools/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +142 -0
- memos/memos_tools/singleton.py +174 -0
- memos/memos_tools/thread_safe_dict.py +310 -0
- memos/memos_tools/thread_safe_dict_segment.py +382 -0
- memos/multi_mem_cube/__init__.py +0 -0
- memos/multi_mem_cube/composite_cube.py +86 -0
- memos/multi_mem_cube/single_cube.py +874 -0
- memos/multi_mem_cube/views.py +54 -0
- memos/parsers/__init__.py +0 -0
- memos/parsers/base.py +15 -0
- memos/parsers/factory.py +21 -0
- memos/parsers/markitdown.py +28 -0
- memos/reranker/__init__.py +4 -0
- memos/reranker/base.py +25 -0
- memos/reranker/concat.py +103 -0
- memos/reranker/cosine_local.py +102 -0
- memos/reranker/factory.py +72 -0
- memos/reranker/http_bge.py +324 -0
- memos/reranker/http_bge_strategy.py +327 -0
- memos/reranker/noop.py +19 -0
- memos/reranker/strategies/__init__.py +4 -0
- memos/reranker/strategies/base.py +61 -0
- memos/reranker/strategies/concat_background.py +94 -0
- memos/reranker/strategies/concat_docsource.py +110 -0
- memos/reranker/strategies/dialogue_common.py +109 -0
- memos/reranker/strategies/factory.py +31 -0
- memos/reranker/strategies/single_turn.py +107 -0
- memos/reranker/strategies/singleturn_outmem.py +98 -0
- memos/settings.py +10 -0
- memos/templates/__init__.py +0 -0
- memos/templates/advanced_search_prompts.py +211 -0
- memos/templates/cloud_service_prompt.py +107 -0
- memos/templates/instruction_completion.py +66 -0
- memos/templates/mem_agent_prompts.py +85 -0
- memos/templates/mem_feedback_prompts.py +822 -0
- memos/templates/mem_reader_prompts.py +1096 -0
- memos/templates/mem_reader_strategy_prompts.py +238 -0
- memos/templates/mem_scheduler_prompts.py +626 -0
- memos/templates/mem_search_prompts.py +93 -0
- memos/templates/mos_prompts.py +403 -0
- memos/templates/prefer_complete_prompt.py +735 -0
- memos/templates/tool_mem_prompts.py +139 -0
- memos/templates/tree_reorganize_prompts.py +230 -0
- memos/types/__init__.py +34 -0
- memos/types/general_types.py +151 -0
- memos/types/openai_chat_completion_types/__init__.py +15 -0
- memos/types/openai_chat_completion_types/chat_completion_assistant_message_param.py +56 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_image_param.py +27 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_input_audio_param.py +23 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_param.py +43 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_refusal_param.py +16 -0
- memos/types/openai_chat_completion_types/chat_completion_content_part_text_param.py +16 -0
- memos/types/openai_chat_completion_types/chat_completion_message_custom_tool_call_param.py +27 -0
- memos/types/openai_chat_completion_types/chat_completion_message_function_tool_call_param.py +32 -0
- memos/types/openai_chat_completion_types/chat_completion_message_param.py +18 -0
- memos/types/openai_chat_completion_types/chat_completion_message_tool_call_union_param.py +15 -0
- memos/types/openai_chat_completion_types/chat_completion_system_message_param.py +36 -0
- memos/types/openai_chat_completion_types/chat_completion_tool_message_param.py +30 -0
- memos/types/openai_chat_completion_types/chat_completion_user_message_param.py +34 -0
- memos/utils.py +123 -0
- memos/vec_dbs/__init__.py +0 -0
- memos/vec_dbs/base.py +117 -0
- memos/vec_dbs/factory.py +23 -0
- memos/vec_dbs/item.py +50 -0
- memos/vec_dbs/milvus.py +654 -0
- 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")
|