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,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
|