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,1319 @@
|
|
|
1
|
+
import multiprocessing
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Union
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.engine import Engine
|
|
13
|
+
|
|
14
|
+
from memos.configs.mem_scheduler import AuthConfig, BaseSchedulerConfig
|
|
15
|
+
from memos.context.context import (
|
|
16
|
+
ContextThread,
|
|
17
|
+
RequestContext,
|
|
18
|
+
get_current_context,
|
|
19
|
+
get_current_trace_id,
|
|
20
|
+
set_request_context,
|
|
21
|
+
)
|
|
22
|
+
from memos.llms.base import BaseLLM
|
|
23
|
+
from memos.log import get_logger
|
|
24
|
+
from memos.mem_cube.base import BaseMemCube
|
|
25
|
+
from memos.mem_cube.general import GeneralMemCube
|
|
26
|
+
from memos.mem_feedback.simple_feedback import SimpleMemFeedback
|
|
27
|
+
from memos.mem_scheduler.general_modules.init_components_for_scheduler import init_components
|
|
28
|
+
from memos.mem_scheduler.general_modules.misc import AutoDroppingQueue as Queue
|
|
29
|
+
from memos.mem_scheduler.general_modules.scheduler_logger import SchedulerLoggerModule
|
|
30
|
+
from memos.mem_scheduler.memory_manage_modules.retriever import SchedulerRetriever
|
|
31
|
+
from memos.mem_scheduler.monitors.dispatcher_monitor import SchedulerDispatcherMonitor
|
|
32
|
+
from memos.mem_scheduler.monitors.general_monitor import SchedulerGeneralMonitor
|
|
33
|
+
from memos.mem_scheduler.monitors.task_schedule_monitor import TaskScheduleMonitor
|
|
34
|
+
from memos.mem_scheduler.schemas.general_schemas import (
|
|
35
|
+
DEFAULT_ACT_MEM_DUMP_PATH,
|
|
36
|
+
DEFAULT_CONSUME_BATCH,
|
|
37
|
+
DEFAULT_CONSUME_INTERVAL_SECONDS,
|
|
38
|
+
DEFAULT_CONTEXT_WINDOW_SIZE,
|
|
39
|
+
DEFAULT_MAX_INTERNAL_MESSAGE_QUEUE_SIZE,
|
|
40
|
+
DEFAULT_MAX_WEB_LOG_QUEUE_SIZE,
|
|
41
|
+
DEFAULT_STARTUP_MODE,
|
|
42
|
+
DEFAULT_THREAD_POOL_MAX_WORKERS,
|
|
43
|
+
DEFAULT_TOP_K,
|
|
44
|
+
DEFAULT_USE_REDIS_QUEUE,
|
|
45
|
+
STARTUP_BY_PROCESS,
|
|
46
|
+
TreeTextMemory_SEARCH_METHOD,
|
|
47
|
+
)
|
|
48
|
+
from memos.mem_scheduler.schemas.message_schemas import (
|
|
49
|
+
ScheduleLogForWebItem,
|
|
50
|
+
ScheduleMessageItem,
|
|
51
|
+
)
|
|
52
|
+
from memos.mem_scheduler.schemas.monitor_schemas import MemoryMonitorItem
|
|
53
|
+
from memos.mem_scheduler.schemas.task_schemas import (
|
|
54
|
+
ADD_TASK_LABEL,
|
|
55
|
+
ANSWER_TASK_LABEL,
|
|
56
|
+
MEM_ARCHIVE_TASK_LABEL,
|
|
57
|
+
MEM_ORGANIZE_TASK_LABEL,
|
|
58
|
+
MEM_UPDATE_TASK_LABEL,
|
|
59
|
+
QUERY_TASK_LABEL,
|
|
60
|
+
TaskPriorityLevel,
|
|
61
|
+
)
|
|
62
|
+
from memos.mem_scheduler.task_schedule_modules.dispatcher import SchedulerDispatcher
|
|
63
|
+
from memos.mem_scheduler.task_schedule_modules.orchestrator import SchedulerOrchestrator
|
|
64
|
+
from memos.mem_scheduler.task_schedule_modules.task_queue import ScheduleTaskQueue
|
|
65
|
+
from memos.mem_scheduler.utils import metrics
|
|
66
|
+
from memos.mem_scheduler.utils.db_utils import get_utc_now
|
|
67
|
+
from memos.mem_scheduler.utils.filter_utils import (
|
|
68
|
+
transform_name_to_key,
|
|
69
|
+
)
|
|
70
|
+
from memos.mem_scheduler.utils.misc_utils import group_messages_by_user_and_mem_cube
|
|
71
|
+
from memos.mem_scheduler.utils.monitor_event_utils import emit_monitor_event, to_iso
|
|
72
|
+
from memos.mem_scheduler.utils.status_tracker import TaskStatusTracker
|
|
73
|
+
from memos.mem_scheduler.webservice_modules.rabbitmq_service import RabbitMQSchedulerModule
|
|
74
|
+
from memos.mem_scheduler.webservice_modules.redis_service import RedisSchedulerModule
|
|
75
|
+
from memos.memories.activation.kv import KVCacheMemory
|
|
76
|
+
from memos.memories.activation.vllmkv import VLLMKVCacheItem, VLLMKVCacheMemory
|
|
77
|
+
from memos.memories.textual.naive import NaiveTextMemory
|
|
78
|
+
from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
|
|
79
|
+
from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher
|
|
80
|
+
from memos.templates.mem_scheduler_prompts import MEMORY_ASSEMBLY_TEMPLATE
|
|
81
|
+
from memos.types.general_types import (
|
|
82
|
+
MemCubeID,
|
|
83
|
+
UserID,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if TYPE_CHECKING:
|
|
88
|
+
import redis
|
|
89
|
+
|
|
90
|
+
from memos.reranker.http_bge import HTTPBGEReranker
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
logger = get_logger(__name__)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLoggerModule):
|
|
97
|
+
"""Base class for all mem_scheduler."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, config: BaseSchedulerConfig):
|
|
100
|
+
"""Initialize the scheduler with the given configuration."""
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.config = config
|
|
103
|
+
|
|
104
|
+
# hyper-parameters
|
|
105
|
+
self.top_k = self.config.get("top_k", DEFAULT_TOP_K)
|
|
106
|
+
self.context_window_size = self.config.get(
|
|
107
|
+
"context_window_size", DEFAULT_CONTEXT_WINDOW_SIZE
|
|
108
|
+
)
|
|
109
|
+
self.enable_activation_memory = self.config.get("enable_activation_memory", False)
|
|
110
|
+
self.act_mem_dump_path = self.config.get("act_mem_dump_path", DEFAULT_ACT_MEM_DUMP_PATH)
|
|
111
|
+
self.search_method = self.config.get("search_method", TreeTextMemory_SEARCH_METHOD)
|
|
112
|
+
self.enable_parallel_dispatch = self.config.get("enable_parallel_dispatch", True)
|
|
113
|
+
self.thread_pool_max_workers = self.config.get(
|
|
114
|
+
"thread_pool_max_workers", DEFAULT_THREAD_POOL_MAX_WORKERS
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# startup mode configuration
|
|
118
|
+
self.scheduler_startup_mode = self.config.get(
|
|
119
|
+
"scheduler_startup_mode", DEFAULT_STARTUP_MODE
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# optional configs
|
|
123
|
+
self.disabled_handlers: list | None = self.config.get("disabled_handlers", None)
|
|
124
|
+
|
|
125
|
+
self.max_web_log_queue_size = self.config.get(
|
|
126
|
+
"max_web_log_queue_size", DEFAULT_MAX_WEB_LOG_QUEUE_SIZE
|
|
127
|
+
)
|
|
128
|
+
self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue(
|
|
129
|
+
maxsize=self.max_web_log_queue_size
|
|
130
|
+
)
|
|
131
|
+
self._consumer_thread = None # Reference to our consumer thread/process
|
|
132
|
+
self._consumer_process = None # Reference to our consumer process
|
|
133
|
+
self._running = False
|
|
134
|
+
self._consume_interval = self.config.get(
|
|
135
|
+
"consume_interval_seconds", DEFAULT_CONSUME_INTERVAL_SECONDS
|
|
136
|
+
)
|
|
137
|
+
self.consume_batch = self.config.get("consume_batch", DEFAULT_CONSUME_BATCH)
|
|
138
|
+
|
|
139
|
+
# message queue configuration
|
|
140
|
+
self.use_redis_queue = self.config.get("use_redis_queue", DEFAULT_USE_REDIS_QUEUE)
|
|
141
|
+
self.max_internal_message_queue_size = self.config.get(
|
|
142
|
+
"max_internal_message_queue_size", DEFAULT_MAX_INTERNAL_MESSAGE_QUEUE_SIZE
|
|
143
|
+
)
|
|
144
|
+
self.orchestrator = SchedulerOrchestrator()
|
|
145
|
+
|
|
146
|
+
self.searcher: Searcher | None = None
|
|
147
|
+
self.retriever: SchedulerRetriever | None = None
|
|
148
|
+
self.db_engine: Engine | None = None
|
|
149
|
+
self.monitor: SchedulerGeneralMonitor | None = None
|
|
150
|
+
self.dispatcher_monitor: SchedulerDispatcherMonitor | None = None
|
|
151
|
+
self.mem_reader = None # Will be set by MOSCore
|
|
152
|
+
self._status_tracker: TaskStatusTracker | None = None
|
|
153
|
+
self.metrics = metrics
|
|
154
|
+
self._monitor_thread = None
|
|
155
|
+
self.memos_message_queue = ScheduleTaskQueue(
|
|
156
|
+
use_redis_queue=self.use_redis_queue,
|
|
157
|
+
maxsize=self.max_internal_message_queue_size,
|
|
158
|
+
disabled_handlers=self.disabled_handlers,
|
|
159
|
+
orchestrator=self.orchestrator,
|
|
160
|
+
status_tracker=self._status_tracker,
|
|
161
|
+
)
|
|
162
|
+
self.dispatcher = SchedulerDispatcher(
|
|
163
|
+
config=self.config,
|
|
164
|
+
memos_message_queue=self.memos_message_queue,
|
|
165
|
+
max_workers=self.thread_pool_max_workers,
|
|
166
|
+
enable_parallel_dispatch=self.enable_parallel_dispatch,
|
|
167
|
+
status_tracker=self._status_tracker,
|
|
168
|
+
metrics=self.metrics,
|
|
169
|
+
submit_web_logs=self._submit_web_logs,
|
|
170
|
+
orchestrator=self.orchestrator,
|
|
171
|
+
)
|
|
172
|
+
# Task schedule monitor: initialize with underlying queue implementation
|
|
173
|
+
self.get_status_parallel = self.config.get("get_status_parallel", True)
|
|
174
|
+
self.task_schedule_monitor = TaskScheduleMonitor(
|
|
175
|
+
memos_message_queue=self.memos_message_queue.memos_message_queue,
|
|
176
|
+
dispatcher=self.dispatcher,
|
|
177
|
+
get_status_parallel=self.get_status_parallel,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# other attributes
|
|
181
|
+
self._context_lock = threading.Lock()
|
|
182
|
+
self.current_user_id: UserID | str | None = None
|
|
183
|
+
self.current_mem_cube_id: MemCubeID | str | None = None
|
|
184
|
+
self.current_mem_cube: BaseMemCube | None = None
|
|
185
|
+
|
|
186
|
+
self._mem_cubes: dict[str, BaseMemCube] = {}
|
|
187
|
+
self.auth_config_path: str | Path | None = self.config.get("auth_config_path", None)
|
|
188
|
+
self.auth_config = None
|
|
189
|
+
self.rabbitmq_config = None
|
|
190
|
+
self.feedback_server = None
|
|
191
|
+
|
|
192
|
+
def init_mem_cube(
|
|
193
|
+
self,
|
|
194
|
+
mem_cube: BaseMemCube,
|
|
195
|
+
searcher: Searcher | None = None,
|
|
196
|
+
feedback_server: SimpleMemFeedback | None = None,
|
|
197
|
+
):
|
|
198
|
+
if mem_cube is None:
|
|
199
|
+
logger.error("mem_cube is None, cannot initialize", stack_info=True)
|
|
200
|
+
self.mem_cube = mem_cube
|
|
201
|
+
self.text_mem: TreeTextMemory = self.mem_cube.text_mem
|
|
202
|
+
self.reranker: HTTPBGEReranker = getattr(self.text_mem, "reranker", None)
|
|
203
|
+
if searcher is None:
|
|
204
|
+
if hasattr(self.text_mem, "get_searcher"):
|
|
205
|
+
self.searcher: Searcher = self.text_mem.get_searcher(
|
|
206
|
+
manual_close_internet=os.getenv("ENABLE_INTERNET", "true").lower() == "false",
|
|
207
|
+
moscube=False,
|
|
208
|
+
process_llm=self.process_llm,
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
self.searcher = None
|
|
212
|
+
else:
|
|
213
|
+
self.searcher = searcher
|
|
214
|
+
self.feedback_server = feedback_server
|
|
215
|
+
|
|
216
|
+
def initialize_modules(
|
|
217
|
+
self,
|
|
218
|
+
chat_llm: BaseLLM,
|
|
219
|
+
process_llm: BaseLLM | None = None,
|
|
220
|
+
db_engine: Engine | None = None,
|
|
221
|
+
mem_reader=None,
|
|
222
|
+
redis_client: Union["redis.Redis", None] = None,
|
|
223
|
+
):
|
|
224
|
+
if process_llm is None:
|
|
225
|
+
process_llm = chat_llm
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
if redis_client and self.use_redis_queue:
|
|
229
|
+
self.status_tracker = TaskStatusTracker(redis_client)
|
|
230
|
+
if self.dispatcher:
|
|
231
|
+
self.dispatcher.status_tracker = self.status_tracker
|
|
232
|
+
if self.memos_message_queue:
|
|
233
|
+
# Use the setter to propagate to the inner queue (e.g. SchedulerRedisQueue)
|
|
234
|
+
self.memos_message_queue.set_status_tracker(self.status_tracker)
|
|
235
|
+
# initialize submodules
|
|
236
|
+
self.chat_llm = chat_llm
|
|
237
|
+
self.process_llm = process_llm
|
|
238
|
+
self.db_engine = db_engine
|
|
239
|
+
self.monitor = SchedulerGeneralMonitor(
|
|
240
|
+
process_llm=self.process_llm, config=self.config, db_engine=self.db_engine
|
|
241
|
+
)
|
|
242
|
+
self.db_engine = self.monitor.db_engine
|
|
243
|
+
self.dispatcher_monitor = SchedulerDispatcherMonitor(config=self.config)
|
|
244
|
+
self.retriever = SchedulerRetriever(process_llm=self.process_llm, config=self.config)
|
|
245
|
+
|
|
246
|
+
if mem_reader:
|
|
247
|
+
self.mem_reader = mem_reader
|
|
248
|
+
|
|
249
|
+
if self.enable_parallel_dispatch:
|
|
250
|
+
self.dispatcher_monitor.initialize(dispatcher=self.dispatcher)
|
|
251
|
+
self.dispatcher_monitor.start()
|
|
252
|
+
|
|
253
|
+
# initialize with auth_config
|
|
254
|
+
try:
|
|
255
|
+
if self.auth_config_path is not None and Path(self.auth_config_path).exists():
|
|
256
|
+
self.auth_config = AuthConfig.from_local_config(
|
|
257
|
+
config_path=self.auth_config_path
|
|
258
|
+
)
|
|
259
|
+
elif AuthConfig.default_config_exists():
|
|
260
|
+
self.auth_config = AuthConfig.from_local_config()
|
|
261
|
+
else:
|
|
262
|
+
self.auth_config = AuthConfig.from_local_env()
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
if self.auth_config is not None:
|
|
267
|
+
self.rabbitmq_config = self.auth_config.rabbitmq
|
|
268
|
+
if self.rabbitmq_config is not None:
|
|
269
|
+
self.initialize_rabbitmq(config=self.rabbitmq_config)
|
|
270
|
+
|
|
271
|
+
logger.debug("GeneralScheduler has been initialized")
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(f"Failed to initialize scheduler modules: {e}", exc_info=True)
|
|
274
|
+
# Clean up any partially initialized resources
|
|
275
|
+
self._cleanup_on_init_failure()
|
|
276
|
+
raise
|
|
277
|
+
|
|
278
|
+
def _cleanup_on_init_failure(self):
|
|
279
|
+
"""Clean up resources if initialization fails."""
|
|
280
|
+
try:
|
|
281
|
+
if hasattr(self, "dispatcher_monitor") and self.dispatcher_monitor is not None:
|
|
282
|
+
self.dispatcher_monitor.stop()
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.warning(f"Error during cleanup: {e}")
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def mem_cube(self) -> BaseMemCube:
|
|
288
|
+
"""The memory cube associated with this MemChat."""
|
|
289
|
+
if self.current_mem_cube is None:
|
|
290
|
+
logger.error("mem_cube is None when accessed", stack_info=True)
|
|
291
|
+
try:
|
|
292
|
+
self.components = init_components()
|
|
293
|
+
self.current_mem_cube: BaseMemCube = self.components["naive_mem_cube"]
|
|
294
|
+
except Exception:
|
|
295
|
+
logger.info(
|
|
296
|
+
"No environment available to initialize mem cube. Using fallback naive_mem_cube."
|
|
297
|
+
)
|
|
298
|
+
return self.current_mem_cube
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def status_tracker(self) -> TaskStatusTracker | None:
|
|
302
|
+
"""Lazy-initialized TaskStatusTracker.
|
|
303
|
+
|
|
304
|
+
If the tracker is None, attempt to initialize from the Redis client
|
|
305
|
+
available via RedisSchedulerModule. This mirrors the lazy pattern used
|
|
306
|
+
by `mem_cube` so downstream modules can safely access the tracker.
|
|
307
|
+
"""
|
|
308
|
+
if self._status_tracker is None and self.use_redis_queue:
|
|
309
|
+
try:
|
|
310
|
+
self._status_tracker = TaskStatusTracker(self.redis)
|
|
311
|
+
# Propagate to submodules when created lazily
|
|
312
|
+
if self.dispatcher:
|
|
313
|
+
self.dispatcher.status_tracker = self._status_tracker
|
|
314
|
+
if self.memos_message_queue:
|
|
315
|
+
self.memos_message_queue.set_status_tracker(self._status_tracker)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.warning(f"Failed to lazy-initialize status_tracker: {e}", exc_info=True)
|
|
318
|
+
|
|
319
|
+
return self._status_tracker
|
|
320
|
+
|
|
321
|
+
@status_tracker.setter
|
|
322
|
+
def status_tracker(self, value: TaskStatusTracker | None) -> None:
|
|
323
|
+
"""Setter that also propagates tracker to dependent modules."""
|
|
324
|
+
self._status_tracker = value
|
|
325
|
+
try:
|
|
326
|
+
if self.dispatcher:
|
|
327
|
+
self.dispatcher.status_tracker = value
|
|
328
|
+
if self.memos_message_queue and value is not None:
|
|
329
|
+
self.memos_message_queue.set_status_tracker(value)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.warning(f"Failed to propagate status_tracker: {e}", exc_info=True)
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def feedback_server(self) -> SimpleMemFeedback:
|
|
335
|
+
"""The memory cube associated with this MemChat."""
|
|
336
|
+
if self._feedback_server is None:
|
|
337
|
+
logger.error("feedback_server is None when accessed", stack_info=True)
|
|
338
|
+
try:
|
|
339
|
+
self.components = init_components()
|
|
340
|
+
self._feedback_server: SimpleMemFeedback = self.components["feedback_server"]
|
|
341
|
+
except Exception:
|
|
342
|
+
logger.info(
|
|
343
|
+
"No environment available to initialize feedback_server. Using fallback feedback_server."
|
|
344
|
+
)
|
|
345
|
+
return self._feedback_server
|
|
346
|
+
|
|
347
|
+
@feedback_server.setter
|
|
348
|
+
def feedback_server(self, value: SimpleMemFeedback) -> None:
|
|
349
|
+
self._feedback_server = value
|
|
350
|
+
|
|
351
|
+
@mem_cube.setter
|
|
352
|
+
def mem_cube(self, value: BaseMemCube) -> None:
|
|
353
|
+
"""The memory cube associated with this MemChat."""
|
|
354
|
+
self.current_mem_cube = value
|
|
355
|
+
self.retriever.mem_cube = value
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def mem_cubes(self) -> dict[str, BaseMemCube]:
|
|
359
|
+
"""All available memory cubes registered to the scheduler.
|
|
360
|
+
|
|
361
|
+
Setting this property will also initialize `current_mem_cube` if it is not
|
|
362
|
+
already set, following the initialization pattern used in component_init.py
|
|
363
|
+
(i.e., calling `init_mem_cube(...)`), without introducing circular imports.
|
|
364
|
+
"""
|
|
365
|
+
return self._mem_cubes
|
|
366
|
+
|
|
367
|
+
@mem_cubes.setter
|
|
368
|
+
def mem_cubes(self, value: dict[str, BaseMemCube]) -> None:
|
|
369
|
+
self._mem_cubes = value or {}
|
|
370
|
+
|
|
371
|
+
# Initialize current_mem_cube if not set yet and mem_cubes are available
|
|
372
|
+
try:
|
|
373
|
+
if self.current_mem_cube is None and self._mem_cubes:
|
|
374
|
+
selected_cube: BaseMemCube | None = None
|
|
375
|
+
|
|
376
|
+
# Prefer the cube matching current_mem_cube_id if provided
|
|
377
|
+
if self.current_mem_cube_id and self.current_mem_cube_id in self._mem_cubes:
|
|
378
|
+
selected_cube = self._mem_cubes[self.current_mem_cube_id]
|
|
379
|
+
else:
|
|
380
|
+
# Fall back to the first available cube deterministically
|
|
381
|
+
first_id, first_cube = next(iter(self._mem_cubes.items()))
|
|
382
|
+
self.current_mem_cube_id = first_id
|
|
383
|
+
selected_cube = first_cube
|
|
384
|
+
|
|
385
|
+
if selected_cube is not None:
|
|
386
|
+
# Use init_mem_cube to mirror component_init.py behavior
|
|
387
|
+
# This sets self.mem_cube (and retriever.mem_cube), text_mem, and searcher.
|
|
388
|
+
self.init_mem_cube(mem_cube=selected_cube)
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.warning(
|
|
391
|
+
f"Failed to initialize current_mem_cube from mem_cubes: {e}", exc_info=True
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def transform_working_memories_to_monitors(
|
|
395
|
+
self, query_keywords, memories: list[TextualMemoryItem]
|
|
396
|
+
) -> list[MemoryMonitorItem]:
|
|
397
|
+
"""
|
|
398
|
+
Convert a list of TextualMemoryItem objects into MemoryMonitorItem objects
|
|
399
|
+
with importance scores based on keyword matching.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
memories: List of TextualMemoryItem objects to be transformed.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List of MemoryMonitorItem objects with computed importance scores.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
result = []
|
|
409
|
+
mem_length = len(memories)
|
|
410
|
+
for idx, mem in enumerate(memories):
|
|
411
|
+
text_mem = mem.memory
|
|
412
|
+
mem_key = transform_name_to_key(name=text_mem)
|
|
413
|
+
|
|
414
|
+
# Calculate importance score based on keyword matches
|
|
415
|
+
keywords_score = 0
|
|
416
|
+
if query_keywords and text_mem:
|
|
417
|
+
for keyword, count in query_keywords.items():
|
|
418
|
+
keyword_count = text_mem.count(keyword)
|
|
419
|
+
if keyword_count > 0:
|
|
420
|
+
keywords_score += keyword_count * count
|
|
421
|
+
logger.debug(
|
|
422
|
+
f"Matched keyword '{keyword}' {keyword_count} times, added {keywords_score} to keywords_score"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# rank score
|
|
426
|
+
sorting_score = mem_length - idx
|
|
427
|
+
|
|
428
|
+
mem_monitor = MemoryMonitorItem(
|
|
429
|
+
memory_text=text_mem,
|
|
430
|
+
tree_memory_item=mem,
|
|
431
|
+
tree_memory_item_mapping_key=mem_key,
|
|
432
|
+
sorting_score=sorting_score,
|
|
433
|
+
keywords_score=keywords_score,
|
|
434
|
+
recording_count=1,
|
|
435
|
+
)
|
|
436
|
+
result.append(mem_monitor)
|
|
437
|
+
|
|
438
|
+
logger.info(f"Transformed {len(result)} memories to monitors")
|
|
439
|
+
return result
|
|
440
|
+
|
|
441
|
+
def replace_working_memory(
|
|
442
|
+
self,
|
|
443
|
+
user_id: UserID | str,
|
|
444
|
+
mem_cube_id: MemCubeID | str,
|
|
445
|
+
mem_cube: GeneralMemCube,
|
|
446
|
+
original_memory: list[TextualMemoryItem],
|
|
447
|
+
new_memory: list[TextualMemoryItem],
|
|
448
|
+
) -> None | list[TextualMemoryItem]:
|
|
449
|
+
"""Replace working memory with new memories after reranking."""
|
|
450
|
+
text_mem_base = mem_cube.text_mem
|
|
451
|
+
if isinstance(text_mem_base, TreeTextMemory):
|
|
452
|
+
text_mem_base: TreeTextMemory = text_mem_base
|
|
453
|
+
|
|
454
|
+
# process rerank memories with llm
|
|
455
|
+
query_db_manager = self.monitor.query_monitors[user_id][mem_cube_id]
|
|
456
|
+
# Sync with database to get latest query history
|
|
457
|
+
query_db_manager.sync_with_orm()
|
|
458
|
+
|
|
459
|
+
query_history = query_db_manager.obj.get_queries_with_timesort()
|
|
460
|
+
|
|
461
|
+
original_count = len(original_memory)
|
|
462
|
+
# Filter out memories tagged with "mode:fast"
|
|
463
|
+
filtered_original_memory = []
|
|
464
|
+
for origin_mem in original_memory:
|
|
465
|
+
if "mode:fast" not in origin_mem.metadata.tags:
|
|
466
|
+
filtered_original_memory.append(origin_mem)
|
|
467
|
+
else:
|
|
468
|
+
logger.debug(
|
|
469
|
+
f"Filtered out memory - ID: {getattr(origin_mem, 'id', 'unknown')}, Tags: {origin_mem.metadata.tags}"
|
|
470
|
+
)
|
|
471
|
+
# Calculate statistics
|
|
472
|
+
filtered_count = original_count - len(filtered_original_memory)
|
|
473
|
+
remaining_count = len(filtered_original_memory)
|
|
474
|
+
|
|
475
|
+
logger.info(
|
|
476
|
+
f"Filtering complete. Removed {filtered_count} memories with tag 'mode:fast'. Remaining memories: {remaining_count}"
|
|
477
|
+
)
|
|
478
|
+
original_memory = filtered_original_memory
|
|
479
|
+
|
|
480
|
+
memories_with_new_order, rerank_success_flag = (
|
|
481
|
+
self.retriever.process_and_rerank_memories(
|
|
482
|
+
queries=query_history,
|
|
483
|
+
original_memory=original_memory,
|
|
484
|
+
new_memory=new_memory,
|
|
485
|
+
top_k=self.top_k,
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Filter completely unrelated memories according to query_history
|
|
490
|
+
logger.info(f"Filtering memories based on query history: {len(query_history)} queries")
|
|
491
|
+
filtered_memories, filter_success_flag = self.retriever.filter_unrelated_memories(
|
|
492
|
+
query_history=query_history,
|
|
493
|
+
memories=memories_with_new_order,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if filter_success_flag:
|
|
497
|
+
logger.info(
|
|
498
|
+
f"Memory filtering completed successfully. "
|
|
499
|
+
f"Filtered from {len(memories_with_new_order)} to {len(filtered_memories)} memories"
|
|
500
|
+
)
|
|
501
|
+
memories_with_new_order = filtered_memories
|
|
502
|
+
else:
|
|
503
|
+
logger.warning(
|
|
504
|
+
"Memory filtering failed - keeping all memories as fallback. "
|
|
505
|
+
f"Original count: {len(memories_with_new_order)}"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Update working memory monitors
|
|
509
|
+
query_keywords = query_db_manager.obj.get_keywords_collections()
|
|
510
|
+
logger.info(
|
|
511
|
+
f"Processing {len(memories_with_new_order)} memories with {len(query_keywords)} query keywords"
|
|
512
|
+
)
|
|
513
|
+
new_working_memory_monitors = self.transform_working_memories_to_monitors(
|
|
514
|
+
query_keywords=query_keywords,
|
|
515
|
+
memories=memories_with_new_order,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if not rerank_success_flag:
|
|
519
|
+
for one in new_working_memory_monitors:
|
|
520
|
+
one.sorting_score = 0
|
|
521
|
+
|
|
522
|
+
logger.info(f"update {len(new_working_memory_monitors)} working_memory_monitors")
|
|
523
|
+
self.monitor.update_working_memory_monitors(
|
|
524
|
+
new_working_memory_monitors=new_working_memory_monitors,
|
|
525
|
+
user_id=user_id,
|
|
526
|
+
mem_cube_id=mem_cube_id,
|
|
527
|
+
mem_cube=mem_cube,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
mem_monitors: list[MemoryMonitorItem] = self.monitor.working_memory_monitors[user_id][
|
|
531
|
+
mem_cube_id
|
|
532
|
+
].obj.get_sorted_mem_monitors(reverse=True)
|
|
533
|
+
new_working_memories = [mem_monitor.tree_memory_item for mem_monitor in mem_monitors]
|
|
534
|
+
|
|
535
|
+
text_mem_base.replace_working_memory(memories=new_working_memories)
|
|
536
|
+
|
|
537
|
+
logger.info(
|
|
538
|
+
f"The working memory has been replaced with {len(memories_with_new_order)} new memories."
|
|
539
|
+
)
|
|
540
|
+
self.log_working_memory_replacement(
|
|
541
|
+
original_memory=original_memory,
|
|
542
|
+
new_memory=new_working_memories,
|
|
543
|
+
user_id=user_id,
|
|
544
|
+
mem_cube_id=mem_cube_id,
|
|
545
|
+
mem_cube=mem_cube,
|
|
546
|
+
log_func_callback=self._submit_web_logs,
|
|
547
|
+
)
|
|
548
|
+
elif isinstance(text_mem_base, NaiveTextMemory):
|
|
549
|
+
# For NaiveTextMemory, we populate the monitors with the new candidates so activation memory can pick them up
|
|
550
|
+
logger.info(
|
|
551
|
+
f"NaiveTextMemory: Updating working memory monitors with {len(new_memory)} candidates."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Use query keywords if available, otherwise just basic monitoring
|
|
555
|
+
query_db_manager = self.monitor.query_monitors[user_id][mem_cube_id]
|
|
556
|
+
query_db_manager.sync_with_orm()
|
|
557
|
+
query_keywords = query_db_manager.obj.get_keywords_collections()
|
|
558
|
+
|
|
559
|
+
new_working_memory_monitors = self.transform_working_memories_to_monitors(
|
|
560
|
+
query_keywords=query_keywords,
|
|
561
|
+
memories=new_memory,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
self.monitor.update_working_memory_monitors(
|
|
565
|
+
new_working_memory_monitors=new_working_memory_monitors,
|
|
566
|
+
user_id=user_id,
|
|
567
|
+
mem_cube_id=mem_cube_id,
|
|
568
|
+
mem_cube=mem_cube,
|
|
569
|
+
)
|
|
570
|
+
memories_with_new_order = new_memory
|
|
571
|
+
else:
|
|
572
|
+
logger.error("memory_base is not supported")
|
|
573
|
+
memories_with_new_order = new_memory
|
|
574
|
+
|
|
575
|
+
return memories_with_new_order
|
|
576
|
+
|
|
577
|
+
def update_activation_memory(
|
|
578
|
+
self,
|
|
579
|
+
new_memories: list[str | TextualMemoryItem],
|
|
580
|
+
label: str,
|
|
581
|
+
user_id: UserID | str,
|
|
582
|
+
mem_cube_id: MemCubeID | str,
|
|
583
|
+
mem_cube: GeneralMemCube,
|
|
584
|
+
) -> None:
|
|
585
|
+
"""
|
|
586
|
+
Update activation memory by extracting KVCacheItems from new_memory (list of str),
|
|
587
|
+
add them to a KVCacheMemory instance, and dump to disk.
|
|
588
|
+
"""
|
|
589
|
+
if len(new_memories) == 0:
|
|
590
|
+
logger.error("update_activation_memory: new_memory is empty.")
|
|
591
|
+
return
|
|
592
|
+
if isinstance(new_memories[0], TextualMemoryItem):
|
|
593
|
+
new_text_memories = [mem.memory for mem in new_memories]
|
|
594
|
+
elif isinstance(new_memories[0], str):
|
|
595
|
+
new_text_memories = new_memories
|
|
596
|
+
else:
|
|
597
|
+
logger.error("Not Implemented.")
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
if isinstance(mem_cube.act_mem, VLLMKVCacheMemory):
|
|
602
|
+
act_mem: VLLMKVCacheMemory = mem_cube.act_mem
|
|
603
|
+
elif isinstance(mem_cube.act_mem, KVCacheMemory):
|
|
604
|
+
act_mem: KVCacheMemory = mem_cube.act_mem
|
|
605
|
+
else:
|
|
606
|
+
logger.error("Not Implemented.")
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
new_text_memory = MEMORY_ASSEMBLY_TEMPLATE.format(
|
|
610
|
+
memory_text="".join(
|
|
611
|
+
[
|
|
612
|
+
f"{i + 1}. {sentence.strip()}\n"
|
|
613
|
+
for i, sentence in enumerate(new_text_memories)
|
|
614
|
+
if sentence.strip() # Skip empty strings
|
|
615
|
+
]
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# huggingface or vllm kv cache
|
|
620
|
+
original_cache_items: list[VLLMKVCacheItem] = act_mem.get_all()
|
|
621
|
+
original_text_memories = []
|
|
622
|
+
if len(original_cache_items) > 0:
|
|
623
|
+
pre_cache_item: VLLMKVCacheItem = original_cache_items[-1]
|
|
624
|
+
original_text_memories = pre_cache_item.records.text_memories
|
|
625
|
+
original_composed_text_memory = pre_cache_item.records.composed_text_memory
|
|
626
|
+
if original_composed_text_memory == new_text_memory:
|
|
627
|
+
logger.warning(
|
|
628
|
+
"Skipping memory update - new composition matches existing cache: %s",
|
|
629
|
+
new_text_memory[:50] + "..."
|
|
630
|
+
if len(new_text_memory) > 50
|
|
631
|
+
else new_text_memory,
|
|
632
|
+
)
|
|
633
|
+
return
|
|
634
|
+
act_mem.delete_all()
|
|
635
|
+
|
|
636
|
+
cache_item = act_mem.extract(new_text_memory)
|
|
637
|
+
cache_item.records.text_memories = new_text_memories
|
|
638
|
+
cache_item.records.timestamp = get_utc_now()
|
|
639
|
+
|
|
640
|
+
act_mem.add([cache_item])
|
|
641
|
+
act_mem.dump(self.act_mem_dump_path)
|
|
642
|
+
|
|
643
|
+
self.log_activation_memory_update(
|
|
644
|
+
original_text_memories=original_text_memories,
|
|
645
|
+
new_text_memories=new_text_memories,
|
|
646
|
+
label=label,
|
|
647
|
+
user_id=user_id,
|
|
648
|
+
mem_cube_id=mem_cube_id,
|
|
649
|
+
mem_cube=mem_cube,
|
|
650
|
+
log_func_callback=self._submit_web_logs,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
except Exception as e:
|
|
654
|
+
logger.error(f"MOS-based activation memory update failed: {e}", exc_info=True)
|
|
655
|
+
# Re-raise the exception if it's critical for the operation
|
|
656
|
+
# For now, we'll continue execution but this should be reviewed
|
|
657
|
+
|
|
658
|
+
def update_activation_memory_periodically(
|
|
659
|
+
self,
|
|
660
|
+
interval_seconds: int,
|
|
661
|
+
label: str,
|
|
662
|
+
user_id: UserID | str,
|
|
663
|
+
mem_cube_id: MemCubeID | str,
|
|
664
|
+
mem_cube: GeneralMemCube,
|
|
665
|
+
):
|
|
666
|
+
try:
|
|
667
|
+
if (
|
|
668
|
+
self.monitor.last_activation_mem_update_time == datetime.min
|
|
669
|
+
or self.monitor.timed_trigger(
|
|
670
|
+
last_time=self.monitor.last_activation_mem_update_time,
|
|
671
|
+
interval_seconds=interval_seconds,
|
|
672
|
+
)
|
|
673
|
+
):
|
|
674
|
+
logger.info(
|
|
675
|
+
f"Updating activation memory for user {user_id} and mem_cube {mem_cube_id}"
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
if (
|
|
679
|
+
user_id not in self.monitor.working_memory_monitors
|
|
680
|
+
or mem_cube_id not in self.monitor.working_memory_monitors[user_id]
|
|
681
|
+
or len(self.monitor.working_memory_monitors[user_id][mem_cube_id].obj.memories)
|
|
682
|
+
== 0
|
|
683
|
+
):
|
|
684
|
+
logger.warning(
|
|
685
|
+
"No memories found in working_memory_monitors, activation memory update is skipped"
|
|
686
|
+
)
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
self.monitor.update_activation_memory_monitors(
|
|
690
|
+
user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# Sync with database to get latest activation memories
|
|
694
|
+
activation_db_manager = self.monitor.activation_memory_monitors[user_id][
|
|
695
|
+
mem_cube_id
|
|
696
|
+
]
|
|
697
|
+
activation_db_manager.sync_with_orm()
|
|
698
|
+
new_activation_memories = [
|
|
699
|
+
m.memory_text for m in activation_db_manager.obj.memories
|
|
700
|
+
]
|
|
701
|
+
|
|
702
|
+
logger.info(
|
|
703
|
+
f"Collected {len(new_activation_memories)} new memory entries for processing"
|
|
704
|
+
)
|
|
705
|
+
# Print the content of each new activation memory
|
|
706
|
+
for i, memory in enumerate(new_activation_memories[:5], 1):
|
|
707
|
+
logger.info(
|
|
708
|
+
f"Part of New Activation Memorires | {i}/{len(new_activation_memories)}: {memory[:20]}"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
self.update_activation_memory(
|
|
712
|
+
new_memories=new_activation_memories,
|
|
713
|
+
label=label,
|
|
714
|
+
user_id=user_id,
|
|
715
|
+
mem_cube_id=mem_cube_id,
|
|
716
|
+
mem_cube=mem_cube,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
self.monitor.last_activation_mem_update_time = get_utc_now()
|
|
720
|
+
|
|
721
|
+
logger.debug(
|
|
722
|
+
f"Activation memory update completed at {self.monitor.last_activation_mem_update_time}"
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
else:
|
|
726
|
+
logger.info(
|
|
727
|
+
f"Skipping update - {interval_seconds} second interval not yet reached. "
|
|
728
|
+
f"Last update time is {self.monitor.last_activation_mem_update_time} and now is "
|
|
729
|
+
f"{get_utc_now()}"
|
|
730
|
+
)
|
|
731
|
+
except Exception as e:
|
|
732
|
+
logger.error(f"Error in update_activation_memory_periodically: {e}", exc_info=True)
|
|
733
|
+
|
|
734
|
+
def submit_messages(self, messages: ScheduleMessageItem | list[ScheduleMessageItem]):
|
|
735
|
+
"""Submit messages for processing, with priority-aware dispatch.
|
|
736
|
+
|
|
737
|
+
- LEVEL_1 tasks dispatch immediately to the appropriate handler.
|
|
738
|
+
- Lower-priority tasks are enqueued via the configured message queue.
|
|
739
|
+
"""
|
|
740
|
+
if isinstance(messages, ScheduleMessageItem):
|
|
741
|
+
messages = [messages]
|
|
742
|
+
|
|
743
|
+
if not messages:
|
|
744
|
+
return
|
|
745
|
+
|
|
746
|
+
current_trace_id = get_current_trace_id()
|
|
747
|
+
|
|
748
|
+
immediate_msgs: list[ScheduleMessageItem] = []
|
|
749
|
+
queued_msgs: list[ScheduleMessageItem] = []
|
|
750
|
+
|
|
751
|
+
for msg in messages:
|
|
752
|
+
# propagate request trace_id when available so monitor logs align with request logs
|
|
753
|
+
if current_trace_id:
|
|
754
|
+
msg.trace_id = current_trace_id
|
|
755
|
+
|
|
756
|
+
# basic metrics and status tracking
|
|
757
|
+
with suppress(Exception):
|
|
758
|
+
self.metrics.task_enqueued(user_id=msg.user_id, task_type=msg.label)
|
|
759
|
+
|
|
760
|
+
# ensure timestamp exists for monitoring
|
|
761
|
+
if getattr(msg, "timestamp", None) is None:
|
|
762
|
+
msg.timestamp = get_utc_now()
|
|
763
|
+
|
|
764
|
+
if self.status_tracker:
|
|
765
|
+
try:
|
|
766
|
+
self.status_tracker.task_submitted(
|
|
767
|
+
task_id=msg.item_id,
|
|
768
|
+
user_id=msg.user_id,
|
|
769
|
+
task_type=msg.label,
|
|
770
|
+
mem_cube_id=msg.mem_cube_id,
|
|
771
|
+
business_task_id=msg.task_id,
|
|
772
|
+
)
|
|
773
|
+
except Exception:
|
|
774
|
+
logger.warning("status_tracker.task_submitted failed", exc_info=True)
|
|
775
|
+
|
|
776
|
+
# honor disabled handlers
|
|
777
|
+
if self.disabled_handlers and msg.label in self.disabled_handlers:
|
|
778
|
+
logger.info(f"Skipping disabled handler: {msg.label} - {msg.content}")
|
|
779
|
+
continue
|
|
780
|
+
|
|
781
|
+
# decide priority path
|
|
782
|
+
task_priority = self.orchestrator.get_task_priority(task_label=msg.label)
|
|
783
|
+
if task_priority == TaskPriorityLevel.LEVEL_1:
|
|
784
|
+
immediate_msgs.append(msg)
|
|
785
|
+
else:
|
|
786
|
+
queued_msgs.append(msg)
|
|
787
|
+
|
|
788
|
+
# Dispatch high-priority tasks immediately
|
|
789
|
+
if immediate_msgs:
|
|
790
|
+
# emit enqueue events for consistency
|
|
791
|
+
for m in immediate_msgs:
|
|
792
|
+
emit_monitor_event(
|
|
793
|
+
"enqueue",
|
|
794
|
+
m,
|
|
795
|
+
{
|
|
796
|
+
"enqueue_ts": to_iso(getattr(m, "timestamp", None)),
|
|
797
|
+
"event_duration_ms": 0,
|
|
798
|
+
"total_duration_ms": 0,
|
|
799
|
+
},
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
# simulate dequeue for immediately dispatched messages so monitor logs stay complete
|
|
803
|
+
for m in immediate_msgs:
|
|
804
|
+
try:
|
|
805
|
+
now = time.time()
|
|
806
|
+
enqueue_ts_obj = getattr(m, "timestamp", None)
|
|
807
|
+
enqueue_epoch = None
|
|
808
|
+
if isinstance(enqueue_ts_obj, int | float):
|
|
809
|
+
enqueue_epoch = float(enqueue_ts_obj)
|
|
810
|
+
elif hasattr(enqueue_ts_obj, "timestamp"):
|
|
811
|
+
dt = enqueue_ts_obj
|
|
812
|
+
if dt.tzinfo is None:
|
|
813
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
814
|
+
enqueue_epoch = dt.timestamp()
|
|
815
|
+
|
|
816
|
+
queue_wait_ms = None
|
|
817
|
+
if enqueue_epoch is not None:
|
|
818
|
+
queue_wait_ms = max(0.0, now - enqueue_epoch) * 1000
|
|
819
|
+
|
|
820
|
+
object.__setattr__(m, "_dequeue_ts", now)
|
|
821
|
+
emit_monitor_event(
|
|
822
|
+
"dequeue",
|
|
823
|
+
m,
|
|
824
|
+
{
|
|
825
|
+
"enqueue_ts": to_iso(enqueue_ts_obj),
|
|
826
|
+
"dequeue_ts": datetime.fromtimestamp(now, tz=timezone.utc).isoformat(),
|
|
827
|
+
"queue_wait_ms": queue_wait_ms,
|
|
828
|
+
"event_duration_ms": queue_wait_ms,
|
|
829
|
+
"total_duration_ms": queue_wait_ms,
|
|
830
|
+
},
|
|
831
|
+
)
|
|
832
|
+
self.metrics.task_dequeued(user_id=m.user_id, task_type=m.label)
|
|
833
|
+
except Exception:
|
|
834
|
+
logger.debug("Failed to emit dequeue for immediate task", exc_info=True)
|
|
835
|
+
|
|
836
|
+
user_cube_groups = group_messages_by_user_and_mem_cube(immediate_msgs)
|
|
837
|
+
for user_id, cube_groups in user_cube_groups.items():
|
|
838
|
+
for mem_cube_id, user_cube_msgs in cube_groups.items():
|
|
839
|
+
label_groups: dict[str, list[ScheduleMessageItem]] = {}
|
|
840
|
+
for m in user_cube_msgs:
|
|
841
|
+
label_groups.setdefault(m.label, []).append(m)
|
|
842
|
+
|
|
843
|
+
for label, msgs_by_label in label_groups.items():
|
|
844
|
+
handler = self.dispatcher.handlers.get(
|
|
845
|
+
label, self.dispatcher._default_message_handler
|
|
846
|
+
)
|
|
847
|
+
self.dispatcher.execute_task(
|
|
848
|
+
user_id=user_id,
|
|
849
|
+
mem_cube_id=mem_cube_id,
|
|
850
|
+
task_label=label,
|
|
851
|
+
msgs=msgs_by_label,
|
|
852
|
+
handler_call_back=handler,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
# Enqueue lower-priority tasks
|
|
856
|
+
if queued_msgs:
|
|
857
|
+
self.memos_message_queue.submit_messages(messages=queued_msgs)
|
|
858
|
+
|
|
859
|
+
def _submit_web_logs(
|
|
860
|
+
self,
|
|
861
|
+
messages: ScheduleLogForWebItem | list[ScheduleLogForWebItem],
|
|
862
|
+
additional_log_info: str | None = None,
|
|
863
|
+
) -> None:
|
|
864
|
+
"""Submit log messages to the web log queue and optionally to RabbitMQ.
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
messages: Single log message or list of log messages
|
|
868
|
+
"""
|
|
869
|
+
if isinstance(messages, ScheduleLogForWebItem):
|
|
870
|
+
messages = [messages] # transform single message to list
|
|
871
|
+
|
|
872
|
+
for message in messages:
|
|
873
|
+
if self.rabbitmq_config is None:
|
|
874
|
+
return
|
|
875
|
+
try:
|
|
876
|
+
# Always call publish; the publisher now caches when offline and flushes after reconnect
|
|
877
|
+
logger.info(
|
|
878
|
+
f"[DIAGNOSTIC] base_scheduler._submit_web_logs: enqueue publish {message.model_dump_json(indent=2)}"
|
|
879
|
+
)
|
|
880
|
+
self.rabbitmq_publish_message(message=message.to_dict())
|
|
881
|
+
logger.info(
|
|
882
|
+
"[DIAGNOSTIC] base_scheduler._submit_web_logs: publish dispatched "
|
|
883
|
+
"item_id=%s task_id=%s label=%s",
|
|
884
|
+
message.item_id,
|
|
885
|
+
message.task_id,
|
|
886
|
+
message.label,
|
|
887
|
+
)
|
|
888
|
+
except Exception as e:
|
|
889
|
+
logger.error(
|
|
890
|
+
f"[DIAGNOSTIC] base_scheduler._submit_web_logs failed: {e}", exc_info=True
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
logger.debug(
|
|
894
|
+
f"{len(messages)} submitted. {self._web_log_message_queue.qsize()} in queue. additional_log_info: {additional_log_info}"
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def get_web_log_messages(self) -> list[dict]:
|
|
898
|
+
"""
|
|
899
|
+
Retrieve structured log messages from the queue and return JSON-serializable dicts.
|
|
900
|
+
"""
|
|
901
|
+
raw_items: list[ScheduleLogForWebItem] = []
|
|
902
|
+
while True:
|
|
903
|
+
try:
|
|
904
|
+
raw_items.append(self._web_log_message_queue.get_nowait())
|
|
905
|
+
except Exception:
|
|
906
|
+
break
|
|
907
|
+
|
|
908
|
+
def _map_label(label: str) -> str:
|
|
909
|
+
mapping = {
|
|
910
|
+
QUERY_TASK_LABEL: "addMessage",
|
|
911
|
+
ANSWER_TASK_LABEL: "addMessage",
|
|
912
|
+
ADD_TASK_LABEL: "addMemory",
|
|
913
|
+
MEM_UPDATE_TASK_LABEL: "updateMemory",
|
|
914
|
+
MEM_ORGANIZE_TASK_LABEL: "mergeMemory",
|
|
915
|
+
MEM_ARCHIVE_TASK_LABEL: "archiveMemory",
|
|
916
|
+
}
|
|
917
|
+
return mapping.get(label, label)
|
|
918
|
+
|
|
919
|
+
def _normalize_item(item: ScheduleLogForWebItem) -> dict:
|
|
920
|
+
data = item.to_dict()
|
|
921
|
+
data["label"] = _map_label(data.get("label"))
|
|
922
|
+
memcube_content = getattr(item, "memcube_log_content", None) or []
|
|
923
|
+
metadata = getattr(item, "metadata", None) or []
|
|
924
|
+
|
|
925
|
+
memcube_name = getattr(item, "memcube_name", None)
|
|
926
|
+
if not memcube_name and hasattr(self, "_map_memcube_name"):
|
|
927
|
+
memcube_name = self._map_memcube_name(item.mem_cube_id)
|
|
928
|
+
data["memcube_name"] = memcube_name
|
|
929
|
+
|
|
930
|
+
memory_len = getattr(item, "memory_len", None)
|
|
931
|
+
if memory_len is None:
|
|
932
|
+
if data["label"] == "mergeMemory":
|
|
933
|
+
memory_len = len([c for c in memcube_content if c.get("type") != "postMerge"])
|
|
934
|
+
elif memcube_content:
|
|
935
|
+
memory_len = len(memcube_content)
|
|
936
|
+
else:
|
|
937
|
+
memory_len = 1 if item.log_content else 0
|
|
938
|
+
|
|
939
|
+
data["memcube_log_content"] = memcube_content
|
|
940
|
+
data["memory_len"] = memory_len
|
|
941
|
+
|
|
942
|
+
def _with_memory_time(meta: dict) -> dict:
|
|
943
|
+
enriched = dict(meta)
|
|
944
|
+
if "memory_time" not in enriched:
|
|
945
|
+
enriched["memory_time"] = enriched.get("updated_at") or enriched.get(
|
|
946
|
+
"update_at"
|
|
947
|
+
)
|
|
948
|
+
return enriched
|
|
949
|
+
|
|
950
|
+
data["metadata"] = [_with_memory_time(m) for m in metadata]
|
|
951
|
+
data["log_title"] = ""
|
|
952
|
+
return data
|
|
953
|
+
|
|
954
|
+
return [_normalize_item(it) for it in raw_items]
|
|
955
|
+
|
|
956
|
+
def _message_consumer(self) -> None:
|
|
957
|
+
"""
|
|
958
|
+
Continuously checks the queue for messages and dispatches them.
|
|
959
|
+
|
|
960
|
+
Runs in a dedicated thread to process messages at regular intervals.
|
|
961
|
+
For Redis queue, this method starts the Redis listener.
|
|
962
|
+
"""
|
|
963
|
+
|
|
964
|
+
# Original local queue logic
|
|
965
|
+
while self._running: # Use a running flag for graceful shutdown
|
|
966
|
+
try:
|
|
967
|
+
# Check dispatcher thread pool status to avoid overloading
|
|
968
|
+
if self.enable_parallel_dispatch and self.dispatcher:
|
|
969
|
+
running_tasks = self.dispatcher.get_running_task_count()
|
|
970
|
+
if running_tasks >= self.dispatcher.max_workers:
|
|
971
|
+
# Thread pool is full, wait and retry
|
|
972
|
+
time.sleep(self._consume_interval)
|
|
973
|
+
continue
|
|
974
|
+
|
|
975
|
+
# Get messages in batches based on consume_batch setting
|
|
976
|
+
|
|
977
|
+
messages = self.memos_message_queue.get_messages(batch_size=self.consume_batch)
|
|
978
|
+
|
|
979
|
+
if messages:
|
|
980
|
+
now = time.time()
|
|
981
|
+
for msg in messages:
|
|
982
|
+
prev_context = get_current_context()
|
|
983
|
+
try:
|
|
984
|
+
# Set context for this message
|
|
985
|
+
msg_context = RequestContext(
|
|
986
|
+
trace_id=msg.trace_id,
|
|
987
|
+
user_name=msg.user_name,
|
|
988
|
+
)
|
|
989
|
+
set_request_context(msg_context)
|
|
990
|
+
|
|
991
|
+
enqueue_ts_obj = getattr(msg, "timestamp", None)
|
|
992
|
+
enqueue_epoch = None
|
|
993
|
+
if isinstance(enqueue_ts_obj, int | float):
|
|
994
|
+
enqueue_epoch = float(enqueue_ts_obj)
|
|
995
|
+
elif hasattr(enqueue_ts_obj, "timestamp"):
|
|
996
|
+
dt = enqueue_ts_obj
|
|
997
|
+
if dt.tzinfo is None:
|
|
998
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
999
|
+
enqueue_epoch = dt.timestamp()
|
|
1000
|
+
|
|
1001
|
+
queue_wait_ms = None
|
|
1002
|
+
if enqueue_epoch is not None:
|
|
1003
|
+
queue_wait_ms = max(0.0, now - enqueue_epoch) * 1000
|
|
1004
|
+
|
|
1005
|
+
# Avoid pydantic field enforcement by using object.__setattr__
|
|
1006
|
+
object.__setattr__(msg, "_dequeue_ts", now)
|
|
1007
|
+
emit_monitor_event(
|
|
1008
|
+
"dequeue",
|
|
1009
|
+
msg,
|
|
1010
|
+
{
|
|
1011
|
+
"enqueue_ts": to_iso(enqueue_ts_obj),
|
|
1012
|
+
"dequeue_ts": datetime.fromtimestamp(
|
|
1013
|
+
now, tz=timezone.utc
|
|
1014
|
+
).isoformat(),
|
|
1015
|
+
"queue_wait_ms": queue_wait_ms,
|
|
1016
|
+
"event_duration_ms": queue_wait_ms,
|
|
1017
|
+
"total_duration_ms": queue_wait_ms,
|
|
1018
|
+
},
|
|
1019
|
+
)
|
|
1020
|
+
self.metrics.task_dequeued(user_id=msg.user_id, task_type=msg.label)
|
|
1021
|
+
finally:
|
|
1022
|
+
# Restore the prior context of the consumer thread
|
|
1023
|
+
set_request_context(prev_context)
|
|
1024
|
+
try:
|
|
1025
|
+
import contextlib
|
|
1026
|
+
|
|
1027
|
+
with contextlib.suppress(Exception):
|
|
1028
|
+
if messages:
|
|
1029
|
+
self.dispatcher.on_messages_enqueued(messages)
|
|
1030
|
+
|
|
1031
|
+
self.dispatcher.dispatch(messages)
|
|
1032
|
+
except Exception as e:
|
|
1033
|
+
logger.error(f"Error dispatching messages: {e!s}")
|
|
1034
|
+
|
|
1035
|
+
# Sleep briefly to prevent busy waiting
|
|
1036
|
+
time.sleep(self._consume_interval) # Adjust interval as needed
|
|
1037
|
+
|
|
1038
|
+
except Exception as e:
|
|
1039
|
+
# Don't log error for "No messages available in Redis queue" as it's expected
|
|
1040
|
+
if "No messages available in Redis queue" not in str(e):
|
|
1041
|
+
logger.error(f"Unexpected error in message consumer: {e!s}", exc_info=True)
|
|
1042
|
+
time.sleep(self._consume_interval) # Prevent tight error loops
|
|
1043
|
+
|
|
1044
|
+
def _monitor_loop(self):
|
|
1045
|
+
while self._running:
|
|
1046
|
+
try:
|
|
1047
|
+
q_sizes = self.memos_message_queue.qsize()
|
|
1048
|
+
|
|
1049
|
+
if not isinstance(q_sizes, dict):
|
|
1050
|
+
continue
|
|
1051
|
+
|
|
1052
|
+
for stream_key, queue_length in q_sizes.items():
|
|
1053
|
+
# Skip aggregate keys like 'total_size'
|
|
1054
|
+
if stream_key == "total_size":
|
|
1055
|
+
continue
|
|
1056
|
+
|
|
1057
|
+
# Key format: ...:{user_id}:{mem_cube_id}:{task_label}
|
|
1058
|
+
# We want to extract user_id, which is the 3rd component from the end.
|
|
1059
|
+
parts = stream_key.split(":")
|
|
1060
|
+
if len(parts) >= 3:
|
|
1061
|
+
user_id = parts[-3]
|
|
1062
|
+
self.metrics.update_queue_length(queue_length, user_id)
|
|
1063
|
+
else:
|
|
1064
|
+
# Fallback for unexpected key formats (e.g. legacy or testing)
|
|
1065
|
+
# Try to use the key itself if it looks like a user_id (no colons)
|
|
1066
|
+
# or just log a warning?
|
|
1067
|
+
# For now, let's assume if it's not total_size and short, it might be a direct user_id key
|
|
1068
|
+
# (though that shouldn't happen with current queue implementations)
|
|
1069
|
+
if ":" not in stream_key:
|
|
1070
|
+
self.metrics.update_queue_length(queue_length, stream_key)
|
|
1071
|
+
|
|
1072
|
+
except Exception as e:
|
|
1073
|
+
logger.error(f"Error in metrics monitor loop: {e}", exc_info=True)
|
|
1074
|
+
|
|
1075
|
+
time.sleep(15) # 每 15 秒采样一次
|
|
1076
|
+
|
|
1077
|
+
def start(self) -> None:
|
|
1078
|
+
"""
|
|
1079
|
+
Start the message consumer thread/process and initialize dispatcher resources.
|
|
1080
|
+
|
|
1081
|
+
Initializes and starts:
|
|
1082
|
+
1. Message consumer thread or process (based on startup_mode)
|
|
1083
|
+
2. Dispatcher thread pool (if parallel dispatch enabled)
|
|
1084
|
+
"""
|
|
1085
|
+
# Initialize dispatcher resources
|
|
1086
|
+
if self.enable_parallel_dispatch:
|
|
1087
|
+
logger.info(
|
|
1088
|
+
f"Initializing dispatcher thread pool with {self.thread_pool_max_workers} workers"
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
self.start_consumer()
|
|
1092
|
+
self.start_background_monitor()
|
|
1093
|
+
|
|
1094
|
+
def start_background_monitor(self):
|
|
1095
|
+
if self._monitor_thread and self._monitor_thread.is_alive():
|
|
1096
|
+
return
|
|
1097
|
+
self._monitor_thread = ContextThread(
|
|
1098
|
+
target=self._monitor_loop, daemon=True, name="SchedulerMetricsMonitor"
|
|
1099
|
+
)
|
|
1100
|
+
self._monitor_thread.start()
|
|
1101
|
+
logger.info("Scheduler metrics monitor thread started.")
|
|
1102
|
+
|
|
1103
|
+
def start_consumer(self) -> None:
|
|
1104
|
+
"""
|
|
1105
|
+
Start only the message consumer thread/process.
|
|
1106
|
+
|
|
1107
|
+
This method can be used to restart the consumer after it has been stopped
|
|
1108
|
+
with stop_consumer(), without affecting other scheduler components.
|
|
1109
|
+
"""
|
|
1110
|
+
if self._running:
|
|
1111
|
+
logger.warning("Memory Scheduler consumer is already running")
|
|
1112
|
+
return
|
|
1113
|
+
|
|
1114
|
+
# Start consumer based on startup mode
|
|
1115
|
+
self._running = True
|
|
1116
|
+
|
|
1117
|
+
if self.scheduler_startup_mode == STARTUP_BY_PROCESS:
|
|
1118
|
+
# Start consumer process
|
|
1119
|
+
self._consumer_process = multiprocessing.Process(
|
|
1120
|
+
target=self._message_consumer,
|
|
1121
|
+
daemon=True,
|
|
1122
|
+
name="MessageConsumerProcess",
|
|
1123
|
+
)
|
|
1124
|
+
self._consumer_process.start()
|
|
1125
|
+
logger.info("Message consumer process started")
|
|
1126
|
+
else:
|
|
1127
|
+
# Default to thread mode
|
|
1128
|
+
self._consumer_thread = ContextThread(
|
|
1129
|
+
target=self._message_consumer,
|
|
1130
|
+
daemon=True,
|
|
1131
|
+
name="MessageConsumerThread",
|
|
1132
|
+
)
|
|
1133
|
+
self._consumer_thread.start()
|
|
1134
|
+
logger.info("Message consumer thread started")
|
|
1135
|
+
|
|
1136
|
+
def stop_consumer(self) -> None:
|
|
1137
|
+
"""Stop only the message consumer thread/process gracefully.
|
|
1138
|
+
|
|
1139
|
+
This method stops the consumer without affecting other components like
|
|
1140
|
+
dispatcher or monitors. Useful when you want to pause message processing
|
|
1141
|
+
while keeping other scheduler components running.
|
|
1142
|
+
"""
|
|
1143
|
+
if not self._running:
|
|
1144
|
+
logger.warning("Memory Scheduler consumer is not running")
|
|
1145
|
+
return
|
|
1146
|
+
|
|
1147
|
+
# Signal consumer thread/process to stop
|
|
1148
|
+
self._running = False
|
|
1149
|
+
|
|
1150
|
+
# Wait for consumer thread or process
|
|
1151
|
+
if self.scheduler_startup_mode == STARTUP_BY_PROCESS and self._consumer_process:
|
|
1152
|
+
if self._consumer_process.is_alive():
|
|
1153
|
+
self._consumer_process.join(timeout=5.0)
|
|
1154
|
+
if self._consumer_process.is_alive():
|
|
1155
|
+
logger.warning("Consumer process did not stop gracefully, terminating...")
|
|
1156
|
+
self._consumer_process.terminate()
|
|
1157
|
+
self._consumer_process.join(timeout=2.0)
|
|
1158
|
+
if self._consumer_process.is_alive():
|
|
1159
|
+
logger.error("Consumer process could not be terminated")
|
|
1160
|
+
else:
|
|
1161
|
+
logger.info("Consumer process terminated")
|
|
1162
|
+
else:
|
|
1163
|
+
logger.info("Consumer process stopped")
|
|
1164
|
+
self._consumer_process = None
|
|
1165
|
+
elif self._consumer_thread and self._consumer_thread.is_alive():
|
|
1166
|
+
self._consumer_thread.join(timeout=5.0)
|
|
1167
|
+
if self._consumer_thread.is_alive():
|
|
1168
|
+
logger.warning("Consumer thread did not stop gracefully")
|
|
1169
|
+
else:
|
|
1170
|
+
logger.info("Consumer thread stopped")
|
|
1171
|
+
self._consumer_thread = None
|
|
1172
|
+
|
|
1173
|
+
logger.info("Memory Scheduler consumer stopped")
|
|
1174
|
+
|
|
1175
|
+
def stop(self) -> None:
|
|
1176
|
+
"""Stop all scheduler components gracefully.
|
|
1177
|
+
|
|
1178
|
+
1. Stops message consumer thread/process
|
|
1179
|
+
2. Shuts down dispatcher thread pool
|
|
1180
|
+
3. Cleans up resources
|
|
1181
|
+
"""
|
|
1182
|
+
if not self._running:
|
|
1183
|
+
logger.warning("Memory Scheduler is not running")
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
# Stop consumer first
|
|
1187
|
+
self.stop_consumer()
|
|
1188
|
+
|
|
1189
|
+
if self._monitor_thread:
|
|
1190
|
+
self._monitor_thread.join(timeout=2.0)
|
|
1191
|
+
|
|
1192
|
+
# Shutdown dispatcher
|
|
1193
|
+
if self.dispatcher:
|
|
1194
|
+
logger.info("Shutting down dispatcher...")
|
|
1195
|
+
self.dispatcher.shutdown()
|
|
1196
|
+
|
|
1197
|
+
# Shutdown dispatcher_monitor
|
|
1198
|
+
if self.dispatcher_monitor:
|
|
1199
|
+
logger.info("Shutting down monitor...")
|
|
1200
|
+
self.dispatcher_monitor.stop()
|
|
1201
|
+
|
|
1202
|
+
@property
|
|
1203
|
+
def handlers(self) -> dict[str, Callable]:
|
|
1204
|
+
"""
|
|
1205
|
+
Access the dispatcher's handlers dictionary.
|
|
1206
|
+
|
|
1207
|
+
Returns:
|
|
1208
|
+
dict[str, Callable]: Dictionary mapping labels to handler functions
|
|
1209
|
+
"""
|
|
1210
|
+
if not self.dispatcher:
|
|
1211
|
+
logger.warning("Dispatcher is not initialized, returning empty handlers dict")
|
|
1212
|
+
return {}
|
|
1213
|
+
|
|
1214
|
+
return self.dispatcher.handlers
|
|
1215
|
+
|
|
1216
|
+
def register_handlers(
|
|
1217
|
+
self, handlers: dict[str, Callable[[list[ScheduleMessageItem]], None]]
|
|
1218
|
+
) -> None:
|
|
1219
|
+
"""
|
|
1220
|
+
Bulk register multiple handlers from a dictionary.
|
|
1221
|
+
|
|
1222
|
+
Args:
|
|
1223
|
+
handlers: Dictionary mapping labels to handler functions
|
|
1224
|
+
Format: {label: handler_callable}
|
|
1225
|
+
"""
|
|
1226
|
+
if not self.dispatcher:
|
|
1227
|
+
logger.warning("Dispatcher is not initialized, cannot register handlers")
|
|
1228
|
+
return
|
|
1229
|
+
|
|
1230
|
+
self.dispatcher.register_handlers(handlers)
|
|
1231
|
+
|
|
1232
|
+
def unregister_handlers(self, labels: list[str]) -> dict[str, bool]:
|
|
1233
|
+
"""
|
|
1234
|
+
Unregister handlers from the dispatcher by their labels.
|
|
1235
|
+
|
|
1236
|
+
Args:
|
|
1237
|
+
labels: List of labels to unregister handlers for
|
|
1238
|
+
|
|
1239
|
+
Returns:
|
|
1240
|
+
dict[str, bool]: Dictionary mapping each label to whether it was successfully unregistered
|
|
1241
|
+
"""
|
|
1242
|
+
if not self.dispatcher:
|
|
1243
|
+
logger.warning("Dispatcher is not initialized, cannot unregister handlers")
|
|
1244
|
+
return dict.fromkeys(labels, False)
|
|
1245
|
+
|
|
1246
|
+
return self.dispatcher.unregister_handlers(labels)
|
|
1247
|
+
|
|
1248
|
+
def get_running_tasks(self, filter_func: Callable | None = None) -> dict[str, dict]:
|
|
1249
|
+
if not self.dispatcher:
|
|
1250
|
+
logger.warning("Dispatcher is not initialized, returning empty tasks dict")
|
|
1251
|
+
return {}
|
|
1252
|
+
|
|
1253
|
+
running_tasks = self.dispatcher.get_running_tasks(filter_func=filter_func)
|
|
1254
|
+
|
|
1255
|
+
# Convert RunningTaskItem objects to dictionaries for easier consumption
|
|
1256
|
+
result = {}
|
|
1257
|
+
for task_id, task_item in running_tasks.items():
|
|
1258
|
+
result[task_id] = {
|
|
1259
|
+
"item_id": task_item.item_id,
|
|
1260
|
+
"user_id": task_item.user_id,
|
|
1261
|
+
"mem_cube_id": task_item.mem_cube_id,
|
|
1262
|
+
"task_info": task_item.task_info,
|
|
1263
|
+
"task_name": task_item.task_name,
|
|
1264
|
+
"start_time": task_item.start_time,
|
|
1265
|
+
"end_time": task_item.end_time,
|
|
1266
|
+
"status": task_item.status,
|
|
1267
|
+
"result": task_item.result,
|
|
1268
|
+
"error_message": task_item.error_message,
|
|
1269
|
+
"messages": task_item.messages,
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return result
|
|
1273
|
+
|
|
1274
|
+
def get_tasks_status(self):
|
|
1275
|
+
"""Delegate status collection to TaskScheduleMonitor."""
|
|
1276
|
+
return self.task_schedule_monitor.get_tasks_status()
|
|
1277
|
+
|
|
1278
|
+
def print_tasks_status(self, tasks_status: dict | None = None) -> None:
|
|
1279
|
+
"""Delegate pretty printing to TaskScheduleMonitor."""
|
|
1280
|
+
self.task_schedule_monitor.print_tasks_status(tasks_status=tasks_status)
|
|
1281
|
+
|
|
1282
|
+
def _gather_queue_stats(self) -> dict:
|
|
1283
|
+
"""Collect queue/dispatcher stats for reporting."""
|
|
1284
|
+
memos_message_queue = self.memos_message_queue.memos_message_queue
|
|
1285
|
+
stats: dict[str, int | float | str] = {}
|
|
1286
|
+
stats["use_redis_queue"] = bool(self.use_redis_queue)
|
|
1287
|
+
# local queue metrics
|
|
1288
|
+
if not self.use_redis_queue:
|
|
1289
|
+
try:
|
|
1290
|
+
stats["qsize"] = int(memos_message_queue.qsize())
|
|
1291
|
+
except Exception:
|
|
1292
|
+
stats["qsize"] = -1
|
|
1293
|
+
# unfinished_tasks if available
|
|
1294
|
+
try:
|
|
1295
|
+
stats["unfinished_tasks"] = int(
|
|
1296
|
+
getattr(memos_message_queue, "unfinished_tasks", 0) or 0
|
|
1297
|
+
)
|
|
1298
|
+
except Exception:
|
|
1299
|
+
stats["unfinished_tasks"] = -1
|
|
1300
|
+
stats["maxsize"] = int(self.max_internal_message_queue_size)
|
|
1301
|
+
try:
|
|
1302
|
+
maxsize = int(self.max_internal_message_queue_size) or 1
|
|
1303
|
+
qsize = int(stats.get("qsize", 0))
|
|
1304
|
+
stats["utilization"] = min(1.0, max(0.0, qsize / maxsize))
|
|
1305
|
+
except Exception:
|
|
1306
|
+
stats["utilization"] = 0.0
|
|
1307
|
+
# dispatcher stats
|
|
1308
|
+
try:
|
|
1309
|
+
d_stats = self.dispatcher.stats()
|
|
1310
|
+
stats.update(
|
|
1311
|
+
{
|
|
1312
|
+
"running": int(d_stats.get("running", 0)),
|
|
1313
|
+
"inflight": int(d_stats.get("inflight", 0)),
|
|
1314
|
+
"handlers": int(d_stats.get("handlers", 0)),
|
|
1315
|
+
}
|
|
1316
|
+
)
|
|
1317
|
+
except Exception:
|
|
1318
|
+
stats.update({"running": 0, "inflight": 0, "handlers": 0})
|
|
1319
|
+
return stats
|