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,485 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import ssl
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from queue import Empty
|
|
9
|
+
|
|
10
|
+
from memos.configs.mem_scheduler import AuthConfig, RabbitMQConfig
|
|
11
|
+
from memos.context.context import ContextThread
|
|
12
|
+
from memos.dependency import require_python_package
|
|
13
|
+
from memos.log import get_logger
|
|
14
|
+
from memos.mem_scheduler.general_modules.base import BaseSchedulerModule
|
|
15
|
+
from memos.mem_scheduler.general_modules.misc import AutoDroppingQueue
|
|
16
|
+
from memos.mem_scheduler.schemas.general_schemas import DIRECT_EXCHANGE_TYPE, FANOUT_EXCHANGE_TYPE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RabbitMQSchedulerModule(BaseSchedulerModule):
|
|
23
|
+
@require_python_package(
|
|
24
|
+
import_name="pika",
|
|
25
|
+
install_command="pip install pika",
|
|
26
|
+
install_link="https://pika.readthedocs.io/en/stable/index.html",
|
|
27
|
+
)
|
|
28
|
+
def __init__(self):
|
|
29
|
+
"""
|
|
30
|
+
Initialize RabbitMQ connection settings.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.auth_config = None
|
|
34
|
+
|
|
35
|
+
# RabbitMQ settings
|
|
36
|
+
self.rabbitmq_config: RabbitMQConfig | None = None
|
|
37
|
+
self.rabbit_queue_name = "memos-scheduler"
|
|
38
|
+
self.rabbitmq_exchange_name = "memos-fanout" # Default, will be overridden by config
|
|
39
|
+
self.rabbitmq_exchange_type = FANOUT_EXCHANGE_TYPE # Default, will be overridden by config
|
|
40
|
+
self.rabbitmq_connection = None
|
|
41
|
+
self.rabbitmq_channel = None
|
|
42
|
+
|
|
43
|
+
# fixed params
|
|
44
|
+
self.rabbitmq_message_cache_max_size = 10 # Max 10 messages
|
|
45
|
+
self.rabbitmq_message_cache = AutoDroppingQueue(
|
|
46
|
+
maxsize=self.rabbitmq_message_cache_max_size
|
|
47
|
+
)
|
|
48
|
+
# Pending outgoing messages to avoid loss when connection is not ready
|
|
49
|
+
self.rabbitmq_publish_cache_max_size = 50
|
|
50
|
+
self.rabbitmq_publish_cache = AutoDroppingQueue(
|
|
51
|
+
maxsize=self.rabbitmq_publish_cache_max_size
|
|
52
|
+
)
|
|
53
|
+
self.rabbitmq_connection_attempts = 3 # Max retry attempts on connection failure
|
|
54
|
+
self.rabbitmq_retry_delay = 5 # Delay (seconds) between retries
|
|
55
|
+
self.rabbitmq_heartbeat = 60 # Heartbeat interval (seconds) for connectio
|
|
56
|
+
self.rabbitmq_conn_max_waiting_seconds = 30
|
|
57
|
+
self.rabbitmq_conn_sleep_seconds = 1
|
|
58
|
+
|
|
59
|
+
# Thread management
|
|
60
|
+
self._rabbitmq_io_loop_thread = None # For IOLoop execution
|
|
61
|
+
self._rabbitmq_stop_flag = False # Graceful shutdown flag
|
|
62
|
+
# Use RLock because publishing may trigger initialization, which also grabs the lock.
|
|
63
|
+
self._rabbitmq_lock = threading.RLock()
|
|
64
|
+
self._rabbitmq_initializing = False # Avoid duplicate concurrent initializations
|
|
65
|
+
|
|
66
|
+
def is_rabbitmq_connected(self) -> bool:
|
|
67
|
+
"""Check if RabbitMQ connection is alive"""
|
|
68
|
+
return (
|
|
69
|
+
self.rabbitmq_connection
|
|
70
|
+
and self.rabbitmq_connection.is_open
|
|
71
|
+
and self.rabbitmq_channel
|
|
72
|
+
and self.rabbitmq_channel.is_open
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def initialize_rabbitmq(
|
|
76
|
+
self, config: dict | None | RabbitMQConfig = None, config_path: str | Path | None = None
|
|
77
|
+
):
|
|
78
|
+
"""
|
|
79
|
+
Establish connection to RabbitMQ using pika.
|
|
80
|
+
"""
|
|
81
|
+
with self._rabbitmq_lock:
|
|
82
|
+
if self._rabbitmq_initializing:
|
|
83
|
+
logger.info(
|
|
84
|
+
"[DIAGNOSTIC] initialize_rabbitmq: initialization already in progress; skipping duplicate call."
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
self._rabbitmq_initializing = True
|
|
88
|
+
try:
|
|
89
|
+
# Skip remote initialization in CI/pytest unless explicitly enabled
|
|
90
|
+
enable_env = os.getenv("MEMOS_ENABLE_RABBITMQ", "").lower() == "true"
|
|
91
|
+
in_ci = os.getenv("CI", "").lower() == "true"
|
|
92
|
+
in_pytest = os.getenv("PYTEST_CURRENT_TEST") is not None
|
|
93
|
+
logger.info(
|
|
94
|
+
f"[DIAGNOSTIC] initialize_rabbitmq called. in_ci={in_ci}, in_pytest={in_pytest}, "
|
|
95
|
+
f"MEMOS_ENABLE_RABBITMQ={enable_env}, config_path={config_path}"
|
|
96
|
+
)
|
|
97
|
+
if (in_ci or in_pytest) and not enable_env:
|
|
98
|
+
logger.info(
|
|
99
|
+
"Skipping RabbitMQ initialization in CI/test environment. Set MEMOS_ENABLE_RABBITMQ=true to enable."
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if self.is_rabbitmq_connected():
|
|
104
|
+
logger.warning("RabbitMQ is already connected. Skipping initialization.")
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
from pika.adapters.select_connection import SelectConnection
|
|
108
|
+
|
|
109
|
+
if config is not None:
|
|
110
|
+
if isinstance(config, RabbitMQConfig):
|
|
111
|
+
self.rabbitmq_config = config
|
|
112
|
+
elif isinstance(config, dict):
|
|
113
|
+
self.rabbitmq_config = AuthConfig.from_dict(config).rabbitmq
|
|
114
|
+
else:
|
|
115
|
+
logger.error(f"Unsupported config type: {type(config)}")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
if config_path is not None and Path(config_path).exists():
|
|
120
|
+
self.auth_config = AuthConfig.from_local_config(config_path=config_path)
|
|
121
|
+
elif AuthConfig.default_config_exists():
|
|
122
|
+
self.auth_config = AuthConfig.from_local_config()
|
|
123
|
+
else:
|
|
124
|
+
self.auth_config = AuthConfig.from_local_env()
|
|
125
|
+
self.rabbitmq_config = self.auth_config.rabbitmq
|
|
126
|
+
|
|
127
|
+
if self.rabbitmq_config is None:
|
|
128
|
+
logger.error(
|
|
129
|
+
"Failed to load RabbitMQ configuration. Please check your config file or environment variables."
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Load exchange configuration from config
|
|
134
|
+
if self.rabbitmq_config:
|
|
135
|
+
if (
|
|
136
|
+
hasattr(self.rabbitmq_config, "exchange_name")
|
|
137
|
+
and self.rabbitmq_config.exchange_name
|
|
138
|
+
):
|
|
139
|
+
self.rabbitmq_exchange_name = self.rabbitmq_config.exchange_name
|
|
140
|
+
logger.info(f"Using configured exchange name: {self.rabbitmq_exchange_name}")
|
|
141
|
+
if (
|
|
142
|
+
hasattr(self.rabbitmq_config, "exchange_type")
|
|
143
|
+
and self.rabbitmq_config.exchange_type
|
|
144
|
+
):
|
|
145
|
+
self.rabbitmq_exchange_type = self.rabbitmq_config.exchange_type
|
|
146
|
+
logger.info(f"Using configured exchange type: {self.rabbitmq_exchange_type}")
|
|
147
|
+
|
|
148
|
+
env_exchange_name = os.getenv("MEMSCHEDULER_RABBITMQ_EXCHANGE_NAME")
|
|
149
|
+
env_exchange_type = os.getenv("MEMSCHEDULER_RABBITMQ_EXCHANGE_TYPE")
|
|
150
|
+
if env_exchange_name:
|
|
151
|
+
self.rabbitmq_exchange_name = env_exchange_name
|
|
152
|
+
logger.info(f"Using env exchange name override: {self.rabbitmq_exchange_name}")
|
|
153
|
+
if env_exchange_type:
|
|
154
|
+
self.rabbitmq_exchange_type = env_exchange_type
|
|
155
|
+
logger.info(f"Using env exchange type override: {self.rabbitmq_exchange_type}")
|
|
156
|
+
|
|
157
|
+
# Start connection process
|
|
158
|
+
parameters = self.get_rabbitmq_connection_param()
|
|
159
|
+
self.rabbitmq_connection = SelectConnection(
|
|
160
|
+
parameters,
|
|
161
|
+
on_open_callback=self.on_rabbitmq_connection_open,
|
|
162
|
+
on_open_error_callback=self.on_rabbitmq_connection_error,
|
|
163
|
+
on_close_callback=self.on_rabbitmq_connection_closed,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Start IOLoop in dedicated thread
|
|
167
|
+
self._io_loop_thread = ContextThread(
|
|
168
|
+
target=self.rabbitmq_connection.ioloop.start, daemon=True
|
|
169
|
+
)
|
|
170
|
+
self._io_loop_thread.start()
|
|
171
|
+
logger.info("RabbitMQ connection process started")
|
|
172
|
+
except Exception:
|
|
173
|
+
logger.error("Failed to initialize RabbitMQ connection", exc_info=True)
|
|
174
|
+
finally:
|
|
175
|
+
with self._rabbitmq_lock:
|
|
176
|
+
self._rabbitmq_initializing = False
|
|
177
|
+
|
|
178
|
+
def get_rabbitmq_queue_size(self) -> int:
|
|
179
|
+
"""Get the current number of messages in the queue.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
int: Number of messages in the queue.
|
|
183
|
+
Returns -1 if there's an error or no active connection.
|
|
184
|
+
"""
|
|
185
|
+
if self.rabbitmq_exchange_type != DIRECT_EXCHANGE_TYPE:
|
|
186
|
+
logger.warning("Queue size can only be checked for direct exchanges")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
with self._rabbitmq_lock:
|
|
190
|
+
if not self.is_rabbitmq_connected():
|
|
191
|
+
logger.warning("No active connection to check queue size")
|
|
192
|
+
return -1
|
|
193
|
+
|
|
194
|
+
# Declare queue passively (only checks existence, doesn't create)
|
|
195
|
+
# Using passive=True prevents accidental queue creation
|
|
196
|
+
result = self.rabbitmq_channel.queue_declare(
|
|
197
|
+
queue=self.rabbit_queue_name,
|
|
198
|
+
durable=True, # Match the original queue durability setting
|
|
199
|
+
passive=True, # Only check queue existence, don't create
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if result is None:
|
|
203
|
+
return 0
|
|
204
|
+
# Return the message count from the queue declaration result
|
|
205
|
+
return result.method.message_count
|
|
206
|
+
|
|
207
|
+
def get_rabbitmq_connection_param(self):
|
|
208
|
+
import pika
|
|
209
|
+
|
|
210
|
+
credentials = pika.PlainCredentials(
|
|
211
|
+
username=self.rabbitmq_config.user_name,
|
|
212
|
+
password=self.rabbitmq_config.password,
|
|
213
|
+
erase_on_connect=self.rabbitmq_config.erase_on_connect,
|
|
214
|
+
)
|
|
215
|
+
if self.rabbitmq_config.port == 5671:
|
|
216
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
217
|
+
context.check_hostname = False
|
|
218
|
+
context.verify_mode = False
|
|
219
|
+
return pika.ConnectionParameters(
|
|
220
|
+
host=self.rabbitmq_config.host_name,
|
|
221
|
+
port=self.rabbitmq_config.port,
|
|
222
|
+
virtual_host=self.rabbitmq_config.virtual_host,
|
|
223
|
+
credentials=credentials,
|
|
224
|
+
ssl_options=pika.SSLOptions(context),
|
|
225
|
+
connection_attempts=self.rabbitmq_connection_attempts,
|
|
226
|
+
retry_delay=self.rabbitmq_retry_delay,
|
|
227
|
+
heartbeat=self.rabbitmq_heartbeat,
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
return pika.ConnectionParameters(
|
|
231
|
+
host=self.rabbitmq_config.host_name,
|
|
232
|
+
port=self.rabbitmq_config.port,
|
|
233
|
+
virtual_host=self.rabbitmq_config.virtual_host,
|
|
234
|
+
credentials=credentials,
|
|
235
|
+
connection_attempts=self.rabbitmq_connection_attempts,
|
|
236
|
+
retry_delay=self.rabbitmq_retry_delay,
|
|
237
|
+
heartbeat=self.rabbitmq_heartbeat,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Connection lifecycle callbacks
|
|
241
|
+
def on_rabbitmq_connection_open(self, connection):
|
|
242
|
+
"""Called when connection is established."""
|
|
243
|
+
logger.info("[DIAGNOSTIC] RabbitMQ connection opened")
|
|
244
|
+
connection.channel(on_open_callback=self.on_rabbitmq_channel_open)
|
|
245
|
+
|
|
246
|
+
def on_rabbitmq_connection_error(self, connection, error):
|
|
247
|
+
"""Called if connection fails to open."""
|
|
248
|
+
logger.error(f"Connection failed: {error}")
|
|
249
|
+
self.rabbit_reconnect()
|
|
250
|
+
|
|
251
|
+
def on_rabbitmq_connection_closed(self, connection, reason):
|
|
252
|
+
"""Called when connection closes."""
|
|
253
|
+
logger.warning(f"Connection closed: {reason}")
|
|
254
|
+
if not self._rabbitmq_stop_flag:
|
|
255
|
+
self.rabbit_reconnect()
|
|
256
|
+
|
|
257
|
+
# Channel lifecycle callbacks
|
|
258
|
+
def on_rabbitmq_channel_open(self, channel):
|
|
259
|
+
"""Called when channel is ready."""
|
|
260
|
+
self.rabbitmq_channel = channel
|
|
261
|
+
logger.info("[DIAGNOSTIC] RabbitMQ channel opened")
|
|
262
|
+
|
|
263
|
+
# Setup exchange and queue
|
|
264
|
+
channel.exchange_declare(
|
|
265
|
+
exchange=self.rabbitmq_exchange_name,
|
|
266
|
+
exchange_type=self.rabbitmq_exchange_type,
|
|
267
|
+
durable=True,
|
|
268
|
+
callback=self.on_rabbitmq_exchange_declared,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def on_rabbitmq_exchange_declared(self, frame):
|
|
272
|
+
"""Called when exchange is ready."""
|
|
273
|
+
self.rabbitmq_channel.queue_declare(
|
|
274
|
+
queue=self.rabbit_queue_name, durable=True, callback=self.on_rabbitmq_queue_declared
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def on_rabbitmq_queue_declared(self, frame):
|
|
278
|
+
"""Called when queue is ready."""
|
|
279
|
+
self.rabbitmq_channel.queue_bind(
|
|
280
|
+
exchange=self.rabbitmq_exchange_name,
|
|
281
|
+
queue=self.rabbit_queue_name,
|
|
282
|
+
routing_key=self.rabbit_queue_name,
|
|
283
|
+
callback=self.on_rabbitmq_bind_ok,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def on_rabbitmq_bind_ok(self, frame):
|
|
287
|
+
"""Final setup step when bind is complete."""
|
|
288
|
+
logger.info("RabbitMQ setup completed")
|
|
289
|
+
# Flush any cached publish messages now that connection is ready
|
|
290
|
+
self._flush_cached_publish_messages()
|
|
291
|
+
|
|
292
|
+
def on_rabbitmq_message(self, channel, method, properties, body):
|
|
293
|
+
"""Handle incoming messages. Only for test."""
|
|
294
|
+
try:
|
|
295
|
+
print(f"Received message: {body.decode()}\n")
|
|
296
|
+
self.rabbitmq_message_cache.put({"properties": properties, "body": body})
|
|
297
|
+
print(f"message delivery_tag: {method.delivery_tag}\n")
|
|
298
|
+
channel.basic_ack(delivery_tag=method.delivery_tag)
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"Message handling failed: {e}", exc_info=True)
|
|
301
|
+
|
|
302
|
+
def wait_for_connection_ready(self):
|
|
303
|
+
start_time = time.time()
|
|
304
|
+
while not self.is_rabbitmq_connected():
|
|
305
|
+
delta_time = time.time() - start_time
|
|
306
|
+
if delta_time > self.rabbitmq_conn_max_waiting_seconds:
|
|
307
|
+
logger.error("Failed to start consuming: Connection timeout")
|
|
308
|
+
return False
|
|
309
|
+
self.rabbit_reconnect()
|
|
310
|
+
time.sleep(self.rabbitmq_conn_sleep_seconds) # Reduced frequency of checks
|
|
311
|
+
|
|
312
|
+
# Message handling
|
|
313
|
+
def rabbitmq_start_consuming(self):
|
|
314
|
+
"""Start consuming messages asynchronously."""
|
|
315
|
+
self.wait_for_connection_ready()
|
|
316
|
+
|
|
317
|
+
self.rabbitmq_channel.basic_consume(
|
|
318
|
+
queue=self.rabbit_queue_name,
|
|
319
|
+
on_message_callback=self.on_rabbitmq_message,
|
|
320
|
+
auto_ack=False,
|
|
321
|
+
)
|
|
322
|
+
logger.info("Started rabbitmq consuming messages")
|
|
323
|
+
|
|
324
|
+
def rabbitmq_publish_message(self, message: dict):
|
|
325
|
+
"""
|
|
326
|
+
Publish a message to RabbitMQ.
|
|
327
|
+
"""
|
|
328
|
+
import pika
|
|
329
|
+
|
|
330
|
+
exchange_name = self.rabbitmq_exchange_name
|
|
331
|
+
routing_key = self.rabbit_queue_name
|
|
332
|
+
label = message.get("label")
|
|
333
|
+
|
|
334
|
+
# Special handling for knowledgeBaseUpdate in local environment: always empty routing key
|
|
335
|
+
if label == "knowledgeBaseUpdate":
|
|
336
|
+
routing_key = ""
|
|
337
|
+
|
|
338
|
+
# Env override: apply to all message types when MEMSCHEDULER_RABBITMQ_EXCHANGE_NAME is set
|
|
339
|
+
env_exchange_name = os.getenv("MEMSCHEDULER_RABBITMQ_EXCHANGE_NAME")
|
|
340
|
+
env_routing_key = os.getenv("MEMSCHEDULER_RABBITMQ_ROUTING_KEY")
|
|
341
|
+
if env_exchange_name:
|
|
342
|
+
exchange_name = env_exchange_name
|
|
343
|
+
routing_key = (
|
|
344
|
+
env_routing_key if env_routing_key is not None and env_routing_key != "" else ""
|
|
345
|
+
)
|
|
346
|
+
logger.info(
|
|
347
|
+
f"[DIAGNOSTIC] Publishing {label} message with env exchange override. "
|
|
348
|
+
f"Exchange: {exchange_name}, Routing Key: '{routing_key}'."
|
|
349
|
+
)
|
|
350
|
+
logger.info(f" - Message Content: {json.dumps(message, indent=2, ensure_ascii=False)}")
|
|
351
|
+
elif label == "knowledgeBaseUpdate":
|
|
352
|
+
# Original diagnostic logging for knowledgeBaseUpdate if NOT in cloud env
|
|
353
|
+
logger.info(
|
|
354
|
+
f"[DIAGNOSTIC] Publishing knowledgeBaseUpdate message (Local Env). "
|
|
355
|
+
f"Current configured Exchange: {exchange_name}, Routing Key: '{routing_key}'."
|
|
356
|
+
)
|
|
357
|
+
logger.info(f" - Message Content: {json.dumps(message, indent=2, ensure_ascii=False)}")
|
|
358
|
+
|
|
359
|
+
with self._rabbitmq_lock:
|
|
360
|
+
logger.info(
|
|
361
|
+
f"[DIAGNOSTIC] rabbitmq_service.rabbitmq_publish_message invoked. "
|
|
362
|
+
f"is_connected={self.is_rabbitmq_connected()}, exchange={exchange_name}, "
|
|
363
|
+
f"routing_key='{routing_key}', label={label}"
|
|
364
|
+
)
|
|
365
|
+
if not self.is_rabbitmq_connected():
|
|
366
|
+
logger.error(
|
|
367
|
+
"[DIAGNOSTIC] Cannot publish - no active connection. Caching message for retry. "
|
|
368
|
+
f"connection_exists={bool(self.rabbitmq_connection)}, "
|
|
369
|
+
f"channel_exists={bool(self.rabbitmq_channel)}, "
|
|
370
|
+
f"config_loaded={self.rabbitmq_config is not None}"
|
|
371
|
+
)
|
|
372
|
+
self.rabbitmq_publish_cache.put(message)
|
|
373
|
+
# Best-effort to connect
|
|
374
|
+
self.initialize_rabbitmq(config=self.rabbitmq_config)
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
logger.info(
|
|
378
|
+
f"[DIAGNOSTIC] rabbitmq_service.rabbitmq_publish_message: Attempting to publish message. Exchange: {exchange_name}, Routing Key: {routing_key}, Message Content: {json.dumps(message, indent=2, ensure_ascii=False)}"
|
|
379
|
+
)
|
|
380
|
+
try:
|
|
381
|
+
self.rabbitmq_channel.basic_publish(
|
|
382
|
+
exchange=exchange_name,
|
|
383
|
+
routing_key=routing_key,
|
|
384
|
+
body=json.dumps(message),
|
|
385
|
+
properties=pika.BasicProperties(
|
|
386
|
+
delivery_mode=2, # Persistent
|
|
387
|
+
),
|
|
388
|
+
mandatory=True,
|
|
389
|
+
)
|
|
390
|
+
logger.debug(f"Published message: {message}")
|
|
391
|
+
return True
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.error(
|
|
394
|
+
"[DIAGNOSTIC] RabbitMQ publish error. label=%s item_id=%s exchange=%s "
|
|
395
|
+
"routing_key=%s error=%s",
|
|
396
|
+
label,
|
|
397
|
+
message.get("item_id"),
|
|
398
|
+
exchange_name,
|
|
399
|
+
routing_key,
|
|
400
|
+
e,
|
|
401
|
+
)
|
|
402
|
+
logger.error(f"Failed to publish message: {e}")
|
|
403
|
+
# Cache message for retry on next connection
|
|
404
|
+
self.rabbitmq_publish_cache.put(message)
|
|
405
|
+
self.rabbit_reconnect()
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
# Connection management
|
|
409
|
+
def rabbit_reconnect(self):
|
|
410
|
+
"""Schedule reconnection attempt."""
|
|
411
|
+
logger.info("Attempting to reconnect...")
|
|
412
|
+
if self.rabbitmq_connection and not self.rabbitmq_connection.is_closed:
|
|
413
|
+
self.rabbitmq_connection.ioloop.stop()
|
|
414
|
+
|
|
415
|
+
# Reset connection state
|
|
416
|
+
self.rabbitmq_channel = None
|
|
417
|
+
self.initialize_rabbitmq()
|
|
418
|
+
|
|
419
|
+
def rabbitmq_close(self):
|
|
420
|
+
"""Gracefully shutdown connection."""
|
|
421
|
+
with self._rabbitmq_lock:
|
|
422
|
+
self._rabbitmq_stop_flag = True
|
|
423
|
+
|
|
424
|
+
# Close channel if open
|
|
425
|
+
if self.rabbitmq_channel and self.rabbitmq_channel.is_open:
|
|
426
|
+
try:
|
|
427
|
+
self.rabbitmq_channel.close()
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.warning(f"Error closing channel: {e}")
|
|
430
|
+
|
|
431
|
+
# Close connection if open
|
|
432
|
+
if self.rabbitmq_connection:
|
|
433
|
+
if self.rabbitmq_connection.is_open:
|
|
434
|
+
try:
|
|
435
|
+
self.rabbitmq_connection.close()
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.warning(f"Error closing connection: {e}")
|
|
438
|
+
|
|
439
|
+
# Stop IOLoop if running
|
|
440
|
+
try:
|
|
441
|
+
self.rabbitmq_connection.ioloop.stop()
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.warning(f"Error stopping IOLoop: {e}")
|
|
444
|
+
|
|
445
|
+
# Wait for IOLoop thread to finish
|
|
446
|
+
if self._io_loop_thread and self._io_loop_thread.is_alive():
|
|
447
|
+
self._io_loop_thread.join(timeout=5)
|
|
448
|
+
if self._io_loop_thread.is_alive():
|
|
449
|
+
logger.warning("IOLoop thread did not terminate cleanly")
|
|
450
|
+
|
|
451
|
+
logger.info("RabbitMQ connection closed")
|
|
452
|
+
|
|
453
|
+
def _flush_cached_publish_messages(self):
|
|
454
|
+
"""Flush cached outgoing messages once connection is available."""
|
|
455
|
+
if self.rabbitmq_publish_cache.empty():
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
if not self.is_rabbitmq_connected():
|
|
459
|
+
logger.info(
|
|
460
|
+
"[DIAGNOSTIC] _flush_cached_publish_messages: connection still down; "
|
|
461
|
+
f"pending={self.rabbitmq_publish_cache.qsize()}"
|
|
462
|
+
)
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
drained: list[dict] = []
|
|
466
|
+
while True:
|
|
467
|
+
try:
|
|
468
|
+
drained.append(self.rabbitmq_publish_cache.get_nowait())
|
|
469
|
+
except Empty:
|
|
470
|
+
break
|
|
471
|
+
|
|
472
|
+
if not drained:
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
logger.info(
|
|
476
|
+
f"[DIAGNOSTIC] Flushing {len(drained)} cached RabbitMQ messages after reconnect."
|
|
477
|
+
)
|
|
478
|
+
for cached_msg in drained:
|
|
479
|
+
success = self.rabbitmq_publish_message(cached_msg)
|
|
480
|
+
if not success:
|
|
481
|
+
# Message already re-cached inside publish; avoid tight loop
|
|
482
|
+
logger.error(
|
|
483
|
+
"[DIAGNOSTIC] Failed to flush cached message; re-queued for next attempt."
|
|
484
|
+
)
|
|
485
|
+
break
|