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,518 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.orm import declarative_base
|
|
7
|
+
|
|
8
|
+
from memos.log import get_logger
|
|
9
|
+
from memos.mem_scheduler.orm_modules.base_model import DatabaseError
|
|
10
|
+
from memos.mem_scheduler.schemas.api_schemas import (
|
|
11
|
+
APISearchHistoryManager,
|
|
12
|
+
)
|
|
13
|
+
from memos.mem_scheduler.utils.db_utils import get_utc_now
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
Base = declarative_base()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class APIRedisDBManager:
|
|
22
|
+
"""Redis-based database manager for any serializable object
|
|
23
|
+
|
|
24
|
+
This class handles persistence, synchronization, and locking
|
|
25
|
+
for any object that implements to_json/from_json methods using Redis as the backend storage.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Add orm_class attribute for compatibility
|
|
29
|
+
orm_class = None
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
user_id: str | None = None,
|
|
34
|
+
mem_cube_id: str | None = None,
|
|
35
|
+
obj: APISearchHistoryManager | None = None,
|
|
36
|
+
lock_timeout: int = 10,
|
|
37
|
+
redis_client=None,
|
|
38
|
+
redis_config: dict | None = None,
|
|
39
|
+
window_size: int = 5,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize the Redis database manager
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
user_id: Unique identifier for the user
|
|
45
|
+
mem_cube_id: Unique identifier for the memory cube
|
|
46
|
+
obj: Optional object instance to manage (must have to_json/from_json methods)
|
|
47
|
+
lock_timeout: Timeout in seconds for lock acquisition
|
|
48
|
+
redis_client: Redis client instance (optional)
|
|
49
|
+
redis_config: Redis configuration dictionary (optional)
|
|
50
|
+
"""
|
|
51
|
+
# Initialize Redis client
|
|
52
|
+
self.redis_client = redis_client
|
|
53
|
+
self.redis_config = redis_config or {}
|
|
54
|
+
|
|
55
|
+
if self.redis_client is None:
|
|
56
|
+
self._init_redis_client()
|
|
57
|
+
|
|
58
|
+
# Initialize base attributes without calling parent's init_manager
|
|
59
|
+
self.user_id = user_id
|
|
60
|
+
self.mem_cube_id = mem_cube_id
|
|
61
|
+
self.obj = obj
|
|
62
|
+
self.lock_timeout = lock_timeout
|
|
63
|
+
self.engine = None # Keep for compatibility but not used
|
|
64
|
+
self.SessionLocal = None # Not used for Redis
|
|
65
|
+
self.window_size = window_size
|
|
66
|
+
self.lock_key = f"{self._get_key_prefix()}:lock"
|
|
67
|
+
|
|
68
|
+
logger.info(
|
|
69
|
+
f"RedisDBManager initialized for user_id: {user_id}, mem_cube_id: {mem_cube_id}"
|
|
70
|
+
)
|
|
71
|
+
logger.info(f"Redis client: {type(self.redis_client).__name__}")
|
|
72
|
+
|
|
73
|
+
# Test Redis connection
|
|
74
|
+
try:
|
|
75
|
+
self.redis_client.ping()
|
|
76
|
+
logger.info("Redis connection successful")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.warning(f"Redis ping failed: {e}")
|
|
79
|
+
# Don't raise error here as it might be a mock client in tests
|
|
80
|
+
|
|
81
|
+
def _get_key_prefix(self) -> str:
|
|
82
|
+
"""Generate Redis key prefix for this user and memory cube
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Redis key prefix string
|
|
86
|
+
"""
|
|
87
|
+
return f"redis_api:{self.user_id}:{self.mem_cube_id}"
|
|
88
|
+
|
|
89
|
+
def _get_data_key(self) -> str:
|
|
90
|
+
"""Generate Redis key for storing serialized data
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Redis data key string
|
|
94
|
+
"""
|
|
95
|
+
return f"{self._get_key_prefix()}:data"
|
|
96
|
+
|
|
97
|
+
def _init_redis_client(self):
|
|
98
|
+
"""Initialize Redis client from config or environment"""
|
|
99
|
+
try:
|
|
100
|
+
import redis
|
|
101
|
+
except ImportError:
|
|
102
|
+
logger.error("Redis package not installed. Install with: pip install redis")
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
# Try to get Redis client from environment first
|
|
106
|
+
if not self.redis_client:
|
|
107
|
+
self.redis_client = APIRedisDBManager.load_redis_engine_from_env()
|
|
108
|
+
|
|
109
|
+
# If still no client, try from config
|
|
110
|
+
if not self.redis_client and self.redis_config:
|
|
111
|
+
redis_kwargs = {
|
|
112
|
+
"host": self.redis_config.get("host"),
|
|
113
|
+
"port": self.redis_config.get("port"),
|
|
114
|
+
"db": self.redis_config.get("db"),
|
|
115
|
+
"decode_responses": True,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if self.redis_config.get("password"):
|
|
119
|
+
redis_kwargs["password"] = self.redis_config["password"]
|
|
120
|
+
|
|
121
|
+
self.redis_client = redis.Redis(**redis_kwargs)
|
|
122
|
+
|
|
123
|
+
# Final fallback to localhost
|
|
124
|
+
if not self.redis_client:
|
|
125
|
+
logger.warning("No Redis configuration found, using localhost defaults")
|
|
126
|
+
self.redis_client = redis.Redis(
|
|
127
|
+
host="localhost", port=6379, db=0, decode_responses=True
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Test connection
|
|
131
|
+
if not self.redis_client.ping():
|
|
132
|
+
raise ConnectionError("Redis ping failed")
|
|
133
|
+
|
|
134
|
+
logger.info("Redis client initialized successfully")
|
|
135
|
+
|
|
136
|
+
def acquire_lock(self, block: bool = True, **kwargs) -> bool:
|
|
137
|
+
"""Acquire a distributed lock using Redis with atomic operations
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
block: Whether to block until lock is acquired
|
|
141
|
+
**kwargs: Additional filter criteria (ignored for Redis)
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if lock was acquired, False otherwise
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
now = get_utc_now()
|
|
148
|
+
|
|
149
|
+
# Use Redis SET with NX (only if not exists) and EX (expiry) for atomic lock acquisition
|
|
150
|
+
lock_value = f"{self._get_key_prefix()}:{now.timestamp()}"
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
result = self.redis_client.get(self.lock_key)
|
|
154
|
+
if result:
|
|
155
|
+
# Wait a bit before retrying
|
|
156
|
+
logger.info(
|
|
157
|
+
f"Waiting for Redis lock to be released for {self.user_id}/{self.mem_cube_id}"
|
|
158
|
+
)
|
|
159
|
+
if not block:
|
|
160
|
+
logger.warning(
|
|
161
|
+
f"Redis lock is held for {self.user_id}/{self.mem_cube_id}, cannot acquire"
|
|
162
|
+
)
|
|
163
|
+
return False
|
|
164
|
+
else:
|
|
165
|
+
time.sleep(0.1)
|
|
166
|
+
continue
|
|
167
|
+
else:
|
|
168
|
+
# Try to acquire lock atomically
|
|
169
|
+
result = self.redis_client.set(
|
|
170
|
+
self.lock_key,
|
|
171
|
+
lock_value,
|
|
172
|
+
ex=self.lock_timeout, # Set expiry in seconds
|
|
173
|
+
)
|
|
174
|
+
logger.info(f"Redis lock acquired for {self._get_key_prefix()}")
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
def release_locks(self, **kwargs):
|
|
178
|
+
# Delete the lock key to release the lock
|
|
179
|
+
result = self.redis_client.delete(self.lock_key)
|
|
180
|
+
|
|
181
|
+
# Redis DELETE returns the number of keys deleted (0 or 1)
|
|
182
|
+
if result > 0:
|
|
183
|
+
logger.info(f"Redis lock released for {self._get_key_prefix()}")
|
|
184
|
+
else:
|
|
185
|
+
logger.info(f"No Redis lock found to release for {self._get_key_prefix()}")
|
|
186
|
+
|
|
187
|
+
def merge_items(
|
|
188
|
+
self,
|
|
189
|
+
redis_data: str,
|
|
190
|
+
obj_instance: APISearchHistoryManager,
|
|
191
|
+
size_limit: int,
|
|
192
|
+
):
|
|
193
|
+
"""Merge Redis data with current object instance
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
redis_data: JSON string from Redis containing serialized APISearchHistoryManager
|
|
197
|
+
obj_instance: Current APISearchHistoryManager instance
|
|
198
|
+
size_limit: Maximum number of completed entries to keep
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
APISearchHistoryManager: Merged and synchronized manager instance
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
# Parse Redis data
|
|
205
|
+
redis_manager = APISearchHistoryManager.from_json(redis_data)
|
|
206
|
+
logger.debug(
|
|
207
|
+
f"Loaded Redis manager with {len(redis_manager.completed_entries)} completed and {len(redis_manager.running_item_ids)} running task IDs"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Create a new merged manager with the original window size from obj_instance
|
|
211
|
+
# Use size_limit only for limiting entries, not as window_size
|
|
212
|
+
original_window_size = obj_instance.window_size
|
|
213
|
+
merged_manager = APISearchHistoryManager(window_size=original_window_size)
|
|
214
|
+
|
|
215
|
+
# Merge completed entries - combine both sources and deduplicate by task_id
|
|
216
|
+
# Ensure all entries are APIMemoryHistoryEntryItem instances
|
|
217
|
+
from memos.mem_scheduler.schemas.api_schemas import APIMemoryHistoryEntryItem
|
|
218
|
+
|
|
219
|
+
all_completed = {}
|
|
220
|
+
|
|
221
|
+
# Add Redis completed entries
|
|
222
|
+
for entry in redis_manager.completed_entries:
|
|
223
|
+
if isinstance(entry, dict):
|
|
224
|
+
# Convert dict to APIMemoryHistoryEntryItem instance
|
|
225
|
+
try:
|
|
226
|
+
entry_obj = APIMemoryHistoryEntryItem(**entry)
|
|
227
|
+
task_id = entry_obj.item_id
|
|
228
|
+
all_completed[task_id] = entry_obj
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.warning(
|
|
231
|
+
f"Failed to convert dict entry to APIMemoryHistoryEntryItem: {e}"
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
else:
|
|
235
|
+
task_id = entry.item_id
|
|
236
|
+
all_completed[task_id] = entry
|
|
237
|
+
|
|
238
|
+
# Add current instance completed entries (these take priority if duplicated)
|
|
239
|
+
for entry in obj_instance.completed_entries:
|
|
240
|
+
if isinstance(entry, dict):
|
|
241
|
+
# Convert dict to APIMemoryHistoryEntryItem instance
|
|
242
|
+
try:
|
|
243
|
+
entry_obj = APIMemoryHistoryEntryItem(**entry)
|
|
244
|
+
task_id = entry_obj.item_id
|
|
245
|
+
all_completed[task_id] = entry_obj
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.warning(
|
|
248
|
+
f"Failed to convert dict entry to APIMemoryHistoryEntryItem: {e}"
|
|
249
|
+
)
|
|
250
|
+
continue
|
|
251
|
+
else:
|
|
252
|
+
task_id = entry.item_id
|
|
253
|
+
all_completed[task_id] = entry
|
|
254
|
+
|
|
255
|
+
# Sort by created_time and apply size limit
|
|
256
|
+
completed_list = list(all_completed.values())
|
|
257
|
+
|
|
258
|
+
def get_created_time(entry):
|
|
259
|
+
"""Helper function to safely extract created_time for sorting"""
|
|
260
|
+
from datetime import datetime
|
|
261
|
+
|
|
262
|
+
# All entries should now be APIMemoryHistoryEntryItem instances
|
|
263
|
+
return getattr(entry, "created_time", datetime.min)
|
|
264
|
+
|
|
265
|
+
completed_list.sort(key=get_created_time, reverse=True)
|
|
266
|
+
merged_manager.completed_entries = completed_list[:size_limit]
|
|
267
|
+
|
|
268
|
+
# Merge running task IDs - combine both sources and deduplicate
|
|
269
|
+
all_running_item_ids = set()
|
|
270
|
+
|
|
271
|
+
# Add Redis running task IDs
|
|
272
|
+
all_running_item_ids.update(redis_manager.running_item_ids)
|
|
273
|
+
|
|
274
|
+
# Add current instance running task IDs
|
|
275
|
+
all_running_item_ids.update(obj_instance.running_item_ids)
|
|
276
|
+
|
|
277
|
+
merged_manager.running_item_ids = list(all_running_item_ids)
|
|
278
|
+
|
|
279
|
+
logger.info(
|
|
280
|
+
f"Merged manager: {len(merged_manager.completed_entries)} completed, {len(merged_manager.running_item_ids)} running task IDs"
|
|
281
|
+
)
|
|
282
|
+
return merged_manager
|
|
283
|
+
|
|
284
|
+
def sync_with_redis(self, size_limit: int | None = None) -> None:
|
|
285
|
+
"""Synchronize data between Redis and the business object
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
size_limit: Optional maximum number of items to keep after synchronization
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
# Use window_size from the object if size_limit is not provided
|
|
292
|
+
if size_limit is None:
|
|
293
|
+
size_limit = self.window_size
|
|
294
|
+
|
|
295
|
+
# Acquire lock before operations
|
|
296
|
+
lock_status = self.acquire_lock(block=True)
|
|
297
|
+
if not lock_status:
|
|
298
|
+
logger.error("Failed to acquire Redis lock for synchronization")
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Load existing data from Redis
|
|
302
|
+
data_key = self._get_data_key()
|
|
303
|
+
redis_data = self.redis_client.get(data_key)
|
|
304
|
+
|
|
305
|
+
if redis_data:
|
|
306
|
+
# Merge Redis data with current object
|
|
307
|
+
merged_obj = self.merge_items(
|
|
308
|
+
redis_data=redis_data, obj_instance=self.obj, size_limit=size_limit
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Update the current object with merged data
|
|
312
|
+
self.obj = merged_obj
|
|
313
|
+
logger.info(
|
|
314
|
+
f"Successfully synchronized with Redis data for {self.user_id}/{self.mem_cube_id}"
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
logger.info(
|
|
318
|
+
f"No existing Redis data found for {self.user_id}/{self.mem_cube_id}, using current object"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Save the synchronized object back to Redis
|
|
322
|
+
self.save_to_db(self.obj)
|
|
323
|
+
|
|
324
|
+
self.release_locks()
|
|
325
|
+
|
|
326
|
+
def save_to_db(self, obj_instance: Any) -> None:
|
|
327
|
+
"""Save the current state of the business object to Redis
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
obj_instance: The object instance to save (must have to_json method)
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
data_key = self._get_data_key()
|
|
334
|
+
|
|
335
|
+
self.redis_client.set(data_key, obj_instance.to_json())
|
|
336
|
+
|
|
337
|
+
logger.info(f"Updated existing Redis record for {data_key}")
|
|
338
|
+
|
|
339
|
+
def load_from_db(self) -> Any | None:
|
|
340
|
+
data_key = self._get_data_key()
|
|
341
|
+
|
|
342
|
+
# Load from Redis
|
|
343
|
+
serialized_data = self.redis_client.get(data_key)
|
|
344
|
+
|
|
345
|
+
if not serialized_data:
|
|
346
|
+
logger.info(f"No Redis record found for {data_key}")
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
# Deserialize the business object using the actual object type
|
|
350
|
+
if hasattr(self, "obj_type") and self.obj_type is not None:
|
|
351
|
+
db_instance = self.obj_type.from_json(serialized_data)
|
|
352
|
+
else:
|
|
353
|
+
# Default to APISearchHistoryManager for this class
|
|
354
|
+
db_instance = APISearchHistoryManager.from_json(serialized_data)
|
|
355
|
+
|
|
356
|
+
logger.info(f"Successfully loaded object from Redis for {data_key} ")
|
|
357
|
+
|
|
358
|
+
return db_instance
|
|
359
|
+
|
|
360
|
+
@classmethod
|
|
361
|
+
def from_env(
|
|
362
|
+
cls,
|
|
363
|
+
user_id: str,
|
|
364
|
+
mem_cube_id: str,
|
|
365
|
+
obj: Any | None = None,
|
|
366
|
+
lock_timeout: int = 10,
|
|
367
|
+
env_file_path: str | None = None,
|
|
368
|
+
) -> "APIRedisDBManager":
|
|
369
|
+
"""Create RedisDBManager from environment variables
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
user_id: User identifier
|
|
373
|
+
mem_cube_id: Memory cube identifier
|
|
374
|
+
obj: Optional MemoryMonitorManager instance
|
|
375
|
+
lock_timeout: Lock timeout in seconds
|
|
376
|
+
env_file_path: Optional path to .env file
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
RedisDBManager instance
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
redis_client = APIRedisDBManager.load_redis_engine_from_env(env_file_path)
|
|
383
|
+
return cls(
|
|
384
|
+
user_id=user_id,
|
|
385
|
+
mem_cube_id=mem_cube_id,
|
|
386
|
+
obj=obj,
|
|
387
|
+
lock_timeout=lock_timeout,
|
|
388
|
+
redis_client=redis_client,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def close(self):
|
|
392
|
+
"""Close the Redis connection and clean up resources"""
|
|
393
|
+
try:
|
|
394
|
+
if hasattr(self.redis_client, "close"):
|
|
395
|
+
self.redis_client.close()
|
|
396
|
+
logger.info(
|
|
397
|
+
f"Redis connection closed for user_id: {self.user_id}, mem_cube_id: {self.mem_cube_id}"
|
|
398
|
+
)
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.warning(f"Error closing Redis connection: {e}")
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def load_redis_engine_from_env(env_file_path: str | None = None) -> Any:
|
|
404
|
+
"""Load Redis connection from environment variables
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
env_file_path: Path to .env file (optional, defaults to loading from current environment)
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Redis connection instance
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
DatabaseError: If required environment variables are missing or connection fails
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
import redis
|
|
417
|
+
except ImportError as e:
|
|
418
|
+
error_msg = "Redis package not installed. Install with: pip install redis"
|
|
419
|
+
logger.error(error_msg)
|
|
420
|
+
raise DatabaseError(error_msg) from e
|
|
421
|
+
|
|
422
|
+
# Load environment variables from file if provided
|
|
423
|
+
if env_file_path:
|
|
424
|
+
if os.path.exists(env_file_path):
|
|
425
|
+
from dotenv import load_dotenv
|
|
426
|
+
|
|
427
|
+
load_dotenv(env_file_path)
|
|
428
|
+
logger.info(f"Loaded environment variables from {env_file_path}")
|
|
429
|
+
else:
|
|
430
|
+
logger.warning(
|
|
431
|
+
f"Environment file not found: {env_file_path}, using current environment variables",
|
|
432
|
+
stack_info=True,
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
logger.info("Using current environment variables (no env_file_path provided)")
|
|
436
|
+
|
|
437
|
+
# Get Redis configuration from environment variables
|
|
438
|
+
redis_host = os.getenv("REDIS_HOST") or os.getenv("MEMSCHEDULER_REDIS_HOST")
|
|
439
|
+
redis_port_str = os.getenv("REDIS_PORT") or os.getenv("MEMSCHEDULER_REDIS_PORT")
|
|
440
|
+
redis_db_str = os.getenv("REDIS_DB") or os.getenv("MEMSCHEDULER_REDIS_DB")
|
|
441
|
+
redis_password = os.getenv("REDIS_PASSWORD") or os.getenv("MEMSCHEDULER_REDIS_PASSWORD")
|
|
442
|
+
|
|
443
|
+
# Check required environment variables
|
|
444
|
+
if not redis_host:
|
|
445
|
+
error_msg = (
|
|
446
|
+
"Missing required Redis environment variable: REDIS_HOST or MEMSCHEDULER_REDIS_HOST"
|
|
447
|
+
)
|
|
448
|
+
logger.error(error_msg)
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
# Parse port with validation
|
|
452
|
+
try:
|
|
453
|
+
redis_port = int(redis_port_str) if redis_port_str else 6379
|
|
454
|
+
except ValueError:
|
|
455
|
+
error_msg = f"Invalid REDIS_PORT value: {redis_port_str}. Must be a valid integer."
|
|
456
|
+
logger.error(error_msg)
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
# Parse database with validation
|
|
460
|
+
try:
|
|
461
|
+
redis_db = int(redis_db_str) if redis_db_str else 0
|
|
462
|
+
except ValueError:
|
|
463
|
+
error_msg = f"Invalid REDIS_DB value: {redis_db_str}. Must be a valid integer."
|
|
464
|
+
logger.error(error_msg)
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
# Optional timeout settings
|
|
468
|
+
socket_timeout = os.getenv(
|
|
469
|
+
"REDIS_SOCKET_TIMEOUT", os.getenv("MEMSCHEDULER_REDIS_TIMEOUT", None)
|
|
470
|
+
)
|
|
471
|
+
socket_connect_timeout = os.getenv(
|
|
472
|
+
"REDIS_SOCKET_CONNECT_TIMEOUT", os.getenv("MEMSCHEDULER_REDIS_CONNECT_TIMEOUT", None)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
# Build Redis connection parameters
|
|
477
|
+
redis_kwargs = {
|
|
478
|
+
"host": redis_host,
|
|
479
|
+
"port": redis_port,
|
|
480
|
+
"db": redis_db,
|
|
481
|
+
"decode_responses": True,
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if redis_password:
|
|
485
|
+
redis_kwargs["password"] = redis_password
|
|
486
|
+
|
|
487
|
+
if socket_timeout:
|
|
488
|
+
try:
|
|
489
|
+
redis_kwargs["socket_timeout"] = float(socket_timeout)
|
|
490
|
+
except ValueError:
|
|
491
|
+
logger.warning(
|
|
492
|
+
f"Invalid REDIS_SOCKET_TIMEOUT value: {socket_timeout}, ignoring"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if socket_connect_timeout:
|
|
496
|
+
try:
|
|
497
|
+
redis_kwargs["socket_connect_timeout"] = float(socket_connect_timeout)
|
|
498
|
+
except ValueError:
|
|
499
|
+
logger.warning(
|
|
500
|
+
f"Invalid REDIS_SOCKET_CONNECT_TIMEOUT value: {socket_connect_timeout}, ignoring"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Create Redis connection
|
|
504
|
+
redis_client = redis.Redis(**redis_kwargs)
|
|
505
|
+
|
|
506
|
+
# Test connection
|
|
507
|
+
if not redis_client.ping():
|
|
508
|
+
raise ConnectionError("Redis ping failed")
|
|
509
|
+
|
|
510
|
+
logger.info(
|
|
511
|
+
f"Successfully created Redis connection: {redis_host}:{redis_port}/{redis_db}"
|
|
512
|
+
)
|
|
513
|
+
return redis_client
|
|
514
|
+
|
|
515
|
+
except Exception as e:
|
|
516
|
+
error_msg = f"Failed to create Redis connection from environment variables: {e}"
|
|
517
|
+
logger.error(error_msg, stack_info=True)
|
|
518
|
+
raise DatabaseError(error_msg) from e
|