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
memos/mem_os/core.py
ADDED
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from threading import Lock
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from memos.configs.mem_os import MOSConfig
|
|
11
|
+
from memos.context.context import ContextThreadPoolExecutor
|
|
12
|
+
from memos.llms.factory import LLMFactory
|
|
13
|
+
from memos.log import get_logger
|
|
14
|
+
from memos.mem_cube.general import GeneralMemCube
|
|
15
|
+
from memos.mem_reader.factory import MemReaderFactory
|
|
16
|
+
from memos.mem_scheduler.general_scheduler import GeneralScheduler
|
|
17
|
+
from memos.mem_scheduler.scheduler_factory import SchedulerFactory
|
|
18
|
+
from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
|
|
19
|
+
from memos.mem_scheduler.schemas.task_schemas import (
|
|
20
|
+
ADD_TASK_LABEL,
|
|
21
|
+
ANSWER_TASK_LABEL,
|
|
22
|
+
MEM_READ_TASK_LABEL,
|
|
23
|
+
PREF_ADD_TASK_LABEL,
|
|
24
|
+
QUERY_TASK_LABEL,
|
|
25
|
+
)
|
|
26
|
+
from memos.mem_user.user_manager import UserManager, UserRole
|
|
27
|
+
from memos.memories.activation.item import ActivationMemoryItem
|
|
28
|
+
from memos.memories.parametric.item import ParametricMemoryItem
|
|
29
|
+
from memos.memories.textual.item import TextualMemoryItem, TextualMemoryMetadata
|
|
30
|
+
from memos.memos_tools.thread_safe_dict_segment import OptimizedThreadSafeDict
|
|
31
|
+
from memos.templates.mos_prompts import QUERY_REWRITING_PROMPT
|
|
32
|
+
from memos.types import ChatHistory, MessageList, MOSSearchResult
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MOSCore:
|
|
39
|
+
"""
|
|
40
|
+
The MOSCore (Memory Operating System Core) class manages multiple MemCube objects and their operations.
|
|
41
|
+
It provides methods for creating, searching, updating, and deleting MemCubes, supporting multi-user scenarios.
|
|
42
|
+
MOSCore acts as an operating system layer for handling and orchestrating MemCube instances.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, config: MOSConfig, user_manager: UserManager | None = None):
|
|
46
|
+
self.config = config
|
|
47
|
+
self.user_id = config.user_id
|
|
48
|
+
self.session_id = config.session_id
|
|
49
|
+
self.chat_llm = LLMFactory.from_config(config.chat_model)
|
|
50
|
+
self.mem_reader = MemReaderFactory.from_config(config.mem_reader)
|
|
51
|
+
self.chat_history_manager: dict[str, ChatHistory] = {}
|
|
52
|
+
# use thread safe dict for multi-user product-server scenario
|
|
53
|
+
self.mem_cubes: OptimizedThreadSafeDict[str, GeneralMemCube] = (
|
|
54
|
+
OptimizedThreadSafeDict() if user_manager is not None else {}
|
|
55
|
+
)
|
|
56
|
+
self._register_chat_history()
|
|
57
|
+
|
|
58
|
+
# Use provided user_manager or create a new one
|
|
59
|
+
if user_manager is not None:
|
|
60
|
+
self.user_manager = user_manager
|
|
61
|
+
else:
|
|
62
|
+
self.user_manager = UserManager(user_id=self.user_id if self.user_id else "root")
|
|
63
|
+
|
|
64
|
+
# Validate user exists
|
|
65
|
+
if not self.user_manager.validate_user(self.user_id):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"User '{self.user_id}' does not exist or is inactive. Please create user first."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Initialize mem_scheduler
|
|
71
|
+
self._mem_scheduler_lock = Lock()
|
|
72
|
+
self.enable_mem_scheduler = self.config.get("enable_mem_scheduler", False)
|
|
73
|
+
if self.enable_mem_scheduler:
|
|
74
|
+
self._mem_scheduler = self._initialize_mem_scheduler()
|
|
75
|
+
self._mem_scheduler.mem_cubes = self.mem_cubes
|
|
76
|
+
self._mem_scheduler.mem_reader = self.mem_reader
|
|
77
|
+
else:
|
|
78
|
+
self._mem_scheduler: GeneralScheduler = None
|
|
79
|
+
|
|
80
|
+
logger.info(f"MOS initialized for user: {self.user_id}")
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def mem_scheduler(self) -> GeneralScheduler:
|
|
84
|
+
"""Lazy-loaded property for memory scheduler."""
|
|
85
|
+
if self.enable_mem_scheduler and self._mem_scheduler is None:
|
|
86
|
+
self._initialize_mem_scheduler()
|
|
87
|
+
self._mem_scheduler.mem_cubes = self.mem_cubes
|
|
88
|
+
return self._mem_scheduler
|
|
89
|
+
|
|
90
|
+
@mem_scheduler.setter
|
|
91
|
+
def mem_scheduler(self, value: GeneralScheduler | None) -> None:
|
|
92
|
+
"""Setter for memory scheduler with validation.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
value: GeneralScheduler instance or None to disable
|
|
96
|
+
Raises:
|
|
97
|
+
TypeError: If value is neither GeneralScheduler nor None
|
|
98
|
+
"""
|
|
99
|
+
with self._mem_scheduler_lock:
|
|
100
|
+
if value is not None and not isinstance(value, GeneralScheduler):
|
|
101
|
+
raise TypeError(f"Expected GeneralScheduler or None, got {type(value)}")
|
|
102
|
+
|
|
103
|
+
self._mem_scheduler = value
|
|
104
|
+
self._mem_scheduler.mem_cubes = self.mem_cubes
|
|
105
|
+
|
|
106
|
+
if value:
|
|
107
|
+
logger.info("Memory scheduler manually set")
|
|
108
|
+
else:
|
|
109
|
+
logger.debug("Memory scheduler cleared")
|
|
110
|
+
|
|
111
|
+
def _initialize_mem_scheduler(self) -> GeneralScheduler:
|
|
112
|
+
"""Initialize the memory scheduler on first access."""
|
|
113
|
+
if not self.config.enable_mem_scheduler:
|
|
114
|
+
logger.debug("Memory scheduler is disabled in config")
|
|
115
|
+
self._mem_scheduler = None
|
|
116
|
+
return self._mem_scheduler
|
|
117
|
+
elif not hasattr(self.config, "mem_scheduler"):
|
|
118
|
+
logger.error("Config of Memory scheduler is not available")
|
|
119
|
+
self._mem_scheduler = None
|
|
120
|
+
return self._mem_scheduler
|
|
121
|
+
else:
|
|
122
|
+
logger.info("Initializing memory scheduler...")
|
|
123
|
+
scheduler_config = self.config.mem_scheduler
|
|
124
|
+
self._mem_scheduler = SchedulerFactory.from_config(scheduler_config)
|
|
125
|
+
# Validate required components
|
|
126
|
+
if not hasattr(self.mem_reader, "llm"):
|
|
127
|
+
raise AttributeError(
|
|
128
|
+
f"Memory reader of type {type(self.mem_reader).__name__} "
|
|
129
|
+
"missing required 'llm' attribute"
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
# Configure scheduler general_modules
|
|
133
|
+
self._mem_scheduler.initialize_modules(
|
|
134
|
+
chat_llm=self.chat_llm,
|
|
135
|
+
process_llm=self.mem_reader.llm,
|
|
136
|
+
db_engine=self.user_manager.engine,
|
|
137
|
+
)
|
|
138
|
+
self._mem_scheduler.start()
|
|
139
|
+
return self._mem_scheduler
|
|
140
|
+
|
|
141
|
+
def mem_scheduler_on(self) -> bool:
|
|
142
|
+
if not self.config.enable_mem_scheduler or self._mem_scheduler is None:
|
|
143
|
+
logger.error("Cannot start scheduler: disabled in configuration")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
self._mem_scheduler.start()
|
|
147
|
+
logger.info("Memory scheduler service started")
|
|
148
|
+
return True
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Failed to start scheduler: {e!s}")
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def mem_scheduler_off(self) -> bool:
|
|
154
|
+
if not self.config.enable_mem_scheduler:
|
|
155
|
+
logger.error("Cannot stop scheduler: disabled in configuration")
|
|
156
|
+
|
|
157
|
+
if self._mem_scheduler is None:
|
|
158
|
+
logger.warning("No scheduler instance to stop")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
self._mem_scheduler.stop()
|
|
163
|
+
logger.info("Memory scheduler service stopped")
|
|
164
|
+
return True
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to stop scheduler: {e!s}")
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
def mem_reorganizer_on(self) -> bool:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def mem_reorganizer_off(self) -> bool:
|
|
173
|
+
"""temporally implement"""
|
|
174
|
+
for mem_cube in self.mem_cubes.values():
|
|
175
|
+
logger.info(f"try to close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
176
|
+
if mem_cube.text_mem and mem_cube.text_mem.is_reorganize:
|
|
177
|
+
logger.info(f"close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
178
|
+
mem_cube.text_mem.memory_manager.close()
|
|
179
|
+
mem_cube.text_mem.memory_manager.wait_reorganizer()
|
|
180
|
+
|
|
181
|
+
def mem_reorganizer_wait(self) -> bool:
|
|
182
|
+
for mem_cube in self.mem_cubes.values():
|
|
183
|
+
logger.info(f"try to close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
184
|
+
if mem_cube.text_mem and mem_cube.text_mem.is_reorganize:
|
|
185
|
+
logger.info(f"close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
186
|
+
mem_cube.text_mem.memory_manager.wait_reorganizer()
|
|
187
|
+
|
|
188
|
+
def _register_chat_history(
|
|
189
|
+
self, user_id: str | None = None, session_id: str | None = None
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Initialize chat history with user ID."""
|
|
192
|
+
self.chat_history_manager[user_id] = ChatHistory(
|
|
193
|
+
user_id=user_id if user_id is not None else self.user_id,
|
|
194
|
+
session_id=session_id if session_id is not None else self.session_id,
|
|
195
|
+
created_at=datetime.now(timezone.utc),
|
|
196
|
+
total_messages=0,
|
|
197
|
+
chat_history=[],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _validate_user_exists(self, user_id: str) -> None:
|
|
201
|
+
"""Validate user exists and is active.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
user_id (str): The user ID to validate.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ValueError: If user doesn't exist or is inactive.
|
|
208
|
+
"""
|
|
209
|
+
if not self.user_manager.validate_user(user_id):
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"User '{user_id}' does not exist or is inactive. Please register the user first."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _validate_cube_access(self, user_id: str, cube_id: str) -> None:
|
|
215
|
+
"""Validate user has access to the cube.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
user_id (str): The user ID to validate.
|
|
219
|
+
cube_id (str): The cube ID to validate.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
ValueError: If user doesn't have access to the cube.
|
|
223
|
+
"""
|
|
224
|
+
# First validate user exists
|
|
225
|
+
self._validate_user_exists(user_id)
|
|
226
|
+
|
|
227
|
+
# Then validate cube access
|
|
228
|
+
if not self.user_manager.validate_user_cube_access(user_id, cube_id):
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"User '{user_id}' does not have access to cube '{cube_id}'. Please register the cube first or request access."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _get_all_documents(self, path: str) -> list[str]:
|
|
234
|
+
"""Get all documents from path.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
path (str): The path to get documents.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
list[str]: The list of documents.
|
|
241
|
+
"""
|
|
242
|
+
documents = []
|
|
243
|
+
|
|
244
|
+
path_obj = Path(path)
|
|
245
|
+
doc_extensions = {".txt", ".pdf", ".json", ".md", ".ppt", ".pptx"}
|
|
246
|
+
for file_path in path_obj.rglob("*"):
|
|
247
|
+
if file_path.is_file() and (file_path.suffix.lower() in doc_extensions):
|
|
248
|
+
documents.append(str(file_path))
|
|
249
|
+
return documents
|
|
250
|
+
|
|
251
|
+
def chat(self, query: str, user_id: str | None = None, base_prompt: str | None = None) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Chat with the MOS.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
query (str): The user's query.
|
|
257
|
+
user_id (str, optional): The user ID for the chat session. Defaults to the user ID from the config.
|
|
258
|
+
base_prompt (str, optional): A custom base prompt to use for the chat.
|
|
259
|
+
It can be a template string with a `{memories}` placeholder.
|
|
260
|
+
If not provided, a default prompt is used.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
str: The response from the MOS.
|
|
264
|
+
"""
|
|
265
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
266
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
267
|
+
user_cube_ids = [cube.cube_id for cube in accessible_cubes]
|
|
268
|
+
if target_user_id not in self.chat_history_manager:
|
|
269
|
+
self._register_chat_history(target_user_id)
|
|
270
|
+
|
|
271
|
+
chat_history = self.chat_history_manager[target_user_id]
|
|
272
|
+
|
|
273
|
+
if self.config.enable_textual_memory and self.mem_cubes:
|
|
274
|
+
memories_all = []
|
|
275
|
+
for mem_cube_id, mem_cube in self.mem_cubes.items():
|
|
276
|
+
if mem_cube_id not in user_cube_ids:
|
|
277
|
+
continue
|
|
278
|
+
if not mem_cube.text_mem:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# submit message to scheduler
|
|
282
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
283
|
+
message_item = ScheduleMessageItem(
|
|
284
|
+
user_id=target_user_id,
|
|
285
|
+
mem_cube_id=mem_cube_id,
|
|
286
|
+
label=QUERY_TASK_LABEL,
|
|
287
|
+
content=query,
|
|
288
|
+
timestamp=datetime.utcnow(),
|
|
289
|
+
)
|
|
290
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
291
|
+
|
|
292
|
+
memories = mem_cube.text_mem.search(
|
|
293
|
+
query,
|
|
294
|
+
top_k=self.config.top_k,
|
|
295
|
+
info={
|
|
296
|
+
"user_id": target_user_id,
|
|
297
|
+
"session_id": self.session_id,
|
|
298
|
+
"chat_history": chat_history.chat_history,
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
memories_all.extend(memories)
|
|
302
|
+
logger.info(f"🧠 [Memory] Searched memories:\n{self._str_memories(memories_all)}\n")
|
|
303
|
+
system_prompt = self._build_system_prompt(memories_all, base_prompt=base_prompt)
|
|
304
|
+
else:
|
|
305
|
+
system_prompt = self._build_system_prompt(base_prompt=base_prompt)
|
|
306
|
+
current_messages = [
|
|
307
|
+
{"role": "system", "content": system_prompt},
|
|
308
|
+
*chat_history.chat_history,
|
|
309
|
+
{"role": "user", "content": query},
|
|
310
|
+
]
|
|
311
|
+
past_key_values = None
|
|
312
|
+
|
|
313
|
+
if self.config.enable_activation_memory:
|
|
314
|
+
if self.config.chat_model.backend not in ["huggingface", "huggingface_singleton"]:
|
|
315
|
+
logger.error(
|
|
316
|
+
"Activation memory only used for huggingface backend. Skipping activation memory."
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
# TODO this only one cubes
|
|
320
|
+
for mem_cube_id, mem_cube in self.mem_cubes.items():
|
|
321
|
+
if mem_cube_id not in user_cube_ids:
|
|
322
|
+
continue
|
|
323
|
+
if mem_cube.act_mem:
|
|
324
|
+
kv_cache = next(iter(mem_cube.act_mem.get_all()), None)
|
|
325
|
+
past_key_values = (
|
|
326
|
+
kv_cache.memory if (kv_cache and hasattr(kv_cache, "memory")) else None
|
|
327
|
+
)
|
|
328
|
+
break
|
|
329
|
+
# Generate response
|
|
330
|
+
response = self.chat_llm.generate(current_messages, past_key_values=past_key_values)
|
|
331
|
+
else:
|
|
332
|
+
response = self.chat_llm.generate(current_messages)
|
|
333
|
+
logger.info(f"🤖 [Assistant] {response}\n")
|
|
334
|
+
chat_history.chat_history.append({"role": "user", "content": query})
|
|
335
|
+
chat_history.chat_history.append({"role": "assistant", "content": response})
|
|
336
|
+
self.chat_history_manager[user_id] = chat_history
|
|
337
|
+
|
|
338
|
+
# submit message to scheduler
|
|
339
|
+
for accessible_mem_cube in accessible_cubes:
|
|
340
|
+
mem_cube_id = accessible_mem_cube.cube_id
|
|
341
|
+
mem_cube = self.mem_cubes[mem_cube_id]
|
|
342
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
343
|
+
message_item = ScheduleMessageItem(
|
|
344
|
+
user_id=target_user_id,
|
|
345
|
+
mem_cube_id=mem_cube_id,
|
|
346
|
+
label=ANSWER_TASK_LABEL,
|
|
347
|
+
content=response,
|
|
348
|
+
timestamp=datetime.utcnow(),
|
|
349
|
+
)
|
|
350
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
351
|
+
|
|
352
|
+
return response
|
|
353
|
+
|
|
354
|
+
def _build_system_prompt(
|
|
355
|
+
self,
|
|
356
|
+
memories: list[TextualMemoryItem] | list[str] | None = None,
|
|
357
|
+
base_prompt: str | None = None,
|
|
358
|
+
**kwargs,
|
|
359
|
+
) -> str:
|
|
360
|
+
"""Build system prompt with optional memories context."""
|
|
361
|
+
if base_prompt is None:
|
|
362
|
+
base_prompt = (
|
|
363
|
+
"You are a knowledgeable and helpful AI assistant. "
|
|
364
|
+
"You have access to conversation memories that help you provide more personalized responses. "
|
|
365
|
+
"Use the memories to understand the user's context, preferences, and past interactions. "
|
|
366
|
+
"If memories are provided, reference them naturally when relevant, but don't explicitly mention having memories."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
memory_context = ""
|
|
370
|
+
if memories:
|
|
371
|
+
memory_list = []
|
|
372
|
+
for i, memory in enumerate(memories, 1):
|
|
373
|
+
if isinstance(memory, TextualMemoryItem):
|
|
374
|
+
text_memory = memory.memory
|
|
375
|
+
else:
|
|
376
|
+
if not isinstance(memory, str):
|
|
377
|
+
logger.error("Unexpected memory type.")
|
|
378
|
+
text_memory = memory
|
|
379
|
+
memory_list.append(f"{i}. {text_memory}")
|
|
380
|
+
memory_context = "\n".join(memory_list)
|
|
381
|
+
|
|
382
|
+
if "{memories}" in base_prompt:
|
|
383
|
+
return base_prompt.format(memories=memory_context)
|
|
384
|
+
elif memories:
|
|
385
|
+
# For backward compatibility, append memories if no placeholder is found
|
|
386
|
+
memory_context_with_header = "\n\n## Memories:\n" + memory_context
|
|
387
|
+
return base_prompt + memory_context_with_header
|
|
388
|
+
return base_prompt
|
|
389
|
+
|
|
390
|
+
def _str_memories(
|
|
391
|
+
self, memories: list[TextualMemoryItem], mode: Literal["concise", "full"] = "full"
|
|
392
|
+
) -> str:
|
|
393
|
+
"""Format memories for display."""
|
|
394
|
+
if not memories:
|
|
395
|
+
return "No memories."
|
|
396
|
+
if mode == "concise":
|
|
397
|
+
return "\n".join(f"{i + 1}. {memory.memory}" for i, memory in enumerate(memories))
|
|
398
|
+
elif mode == "full":
|
|
399
|
+
return "\n".join(f"{i + 1}. {memory}" for i, memory in enumerate(memories))
|
|
400
|
+
|
|
401
|
+
def clear_messages(self, user_id: str | None = None) -> None:
|
|
402
|
+
"""Clear chat history."""
|
|
403
|
+
user_id = user_id if user_id is not None else self.user_id
|
|
404
|
+
self._register_chat_history(user_id)
|
|
405
|
+
|
|
406
|
+
def create_user(
|
|
407
|
+
self, user_id: str, role: UserRole = UserRole.USER, user_name: str | None = None
|
|
408
|
+
) -> str:
|
|
409
|
+
"""Create a new user.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
user_name (str): Name of the user.
|
|
413
|
+
role (UserRole): Role of the user.
|
|
414
|
+
user_id (str, optional): Custom user ID.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
str: The created user ID.
|
|
418
|
+
"""
|
|
419
|
+
if not user_name:
|
|
420
|
+
user_name = user_id
|
|
421
|
+
return self.user_manager.create_user(user_name, role, user_id)
|
|
422
|
+
|
|
423
|
+
def list_users(self) -> list:
|
|
424
|
+
"""List all active users.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
list: List of user information dictionaries.
|
|
428
|
+
"""
|
|
429
|
+
users = self.user_manager.list_users()
|
|
430
|
+
return [
|
|
431
|
+
{
|
|
432
|
+
"user_id": user.user_id,
|
|
433
|
+
"user_name": user.user_name,
|
|
434
|
+
"role": user.role.value,
|
|
435
|
+
"created_at": user.created_at.isoformat(),
|
|
436
|
+
"is_active": user.is_active,
|
|
437
|
+
}
|
|
438
|
+
for user in users
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
def create_cube_for_user(
|
|
442
|
+
self,
|
|
443
|
+
cube_name: str,
|
|
444
|
+
owner_id: str,
|
|
445
|
+
cube_path: str | None = None,
|
|
446
|
+
cube_id: str | None = None,
|
|
447
|
+
) -> str:
|
|
448
|
+
"""Create a new cube for the current user.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
cube_name (str): Name of the cube.
|
|
452
|
+
cube_path (str, optional): Path to the cube.
|
|
453
|
+
cube_id (str, optional): Custom cube ID.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
str: The created cube ID.
|
|
457
|
+
"""
|
|
458
|
+
return self.user_manager.create_cube(cube_name, owner_id, cube_path, cube_id)
|
|
459
|
+
|
|
460
|
+
def register_mem_cube(
|
|
461
|
+
self,
|
|
462
|
+
mem_cube_name_or_path: str | GeneralMemCube,
|
|
463
|
+
mem_cube_id: str | None = None,
|
|
464
|
+
user_id: str | None = None,
|
|
465
|
+
) -> None:
|
|
466
|
+
"""
|
|
467
|
+
Register a MemCube with the MOS.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
mem_cube_name_or_path (str): The name or path of the MemCube to register.
|
|
471
|
+
mem_cube_id (str, optional): The identifier for the MemCube. If not provided, a default ID is used.
|
|
472
|
+
"""
|
|
473
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
474
|
+
self._validate_user_exists(target_user_id)
|
|
475
|
+
|
|
476
|
+
if mem_cube_id is None:
|
|
477
|
+
if isinstance(mem_cube_name_or_path, GeneralMemCube):
|
|
478
|
+
mem_cube_id = f"cube_{target_user_id}"
|
|
479
|
+
else:
|
|
480
|
+
mem_cube_id = mem_cube_name_or_path
|
|
481
|
+
|
|
482
|
+
if mem_cube_id in self.mem_cubes:
|
|
483
|
+
logger.info(f"MemCube with ID {mem_cube_id} already in MOS, skip install.")
|
|
484
|
+
else:
|
|
485
|
+
if isinstance(mem_cube_name_or_path, GeneralMemCube):
|
|
486
|
+
self.mem_cubes[mem_cube_id] = mem_cube_name_or_path
|
|
487
|
+
logger.info(f"register new cube {mem_cube_id} for user {target_user_id}")
|
|
488
|
+
elif os.path.exists(mem_cube_name_or_path):
|
|
489
|
+
mem_cube_obj = GeneralMemCube.init_from_dir(mem_cube_name_or_path)
|
|
490
|
+
self.mem_cubes[mem_cube_id] = mem_cube_obj
|
|
491
|
+
else:
|
|
492
|
+
logger.warning(
|
|
493
|
+
f"MemCube {mem_cube_name_or_path} does not exist, try to init from remote repo."
|
|
494
|
+
)
|
|
495
|
+
mem_cube_obj = GeneralMemCube.init_from_remote_repo(mem_cube_name_or_path)
|
|
496
|
+
self.mem_cubes[mem_cube_id] = mem_cube_obj
|
|
497
|
+
# Check if cube already exists in database
|
|
498
|
+
existing_cube = self.user_manager.get_cube(mem_cube_id)
|
|
499
|
+
|
|
500
|
+
# check the embedder is it consistent with MOSConfig
|
|
501
|
+
if hasattr(
|
|
502
|
+
self.mem_cubes[mem_cube_id].text_mem.config, "embedder"
|
|
503
|
+
) and self.config.mem_reader.config.embedder != (
|
|
504
|
+
cube_embedder := self.mem_cubes[mem_cube_id].text_mem.config.embedder
|
|
505
|
+
):
|
|
506
|
+
logger.warning(
|
|
507
|
+
f"Cube Embedder is not consistent with MOSConfig for cube: {mem_cube_id}, will use Cube Embedder: {cube_embedder}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if existing_cube:
|
|
511
|
+
# Cube exists, just add user to cube if not already associated
|
|
512
|
+
if not self.user_manager.validate_user_cube_access(target_user_id, mem_cube_id):
|
|
513
|
+
success = self.user_manager.add_user_to_cube(target_user_id, mem_cube_id)
|
|
514
|
+
if success:
|
|
515
|
+
logger.info(f"User {target_user_id} added to existing cube {mem_cube_id}")
|
|
516
|
+
else:
|
|
517
|
+
logger.error(f"Failed to add user {target_user_id} to cube {mem_cube_id}")
|
|
518
|
+
else:
|
|
519
|
+
logger.info(f"User {target_user_id} already has access to cube {mem_cube_id}")
|
|
520
|
+
else:
|
|
521
|
+
# Cube doesn't exist, create it
|
|
522
|
+
self.create_cube_for_user(
|
|
523
|
+
cube_name=mem_cube_name_or_path
|
|
524
|
+
if not isinstance(mem_cube_name_or_path, GeneralMemCube)
|
|
525
|
+
else mem_cube_id,
|
|
526
|
+
owner_id=target_user_id,
|
|
527
|
+
cube_id=mem_cube_id,
|
|
528
|
+
cube_path=mem_cube_name_or_path
|
|
529
|
+
if not isinstance(mem_cube_name_or_path, GeneralMemCube)
|
|
530
|
+
else "init",
|
|
531
|
+
)
|
|
532
|
+
logger.info(f"register new cube {mem_cube_id} for user {target_user_id}")
|
|
533
|
+
|
|
534
|
+
def unregister_mem_cube(self, mem_cube_id: str, user_id: str | None = None) -> None:
|
|
535
|
+
"""
|
|
536
|
+
Unregister a MemCube by its identifier.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
mem_cube_id (str): The identifier of the MemCube to unregister.
|
|
540
|
+
"""
|
|
541
|
+
if mem_cube_id in self.mem_cubes:
|
|
542
|
+
del self.mem_cubes[mem_cube_id]
|
|
543
|
+
else:
|
|
544
|
+
raise ValueError(f"MemCube with ID {mem_cube_id} does not exist.")
|
|
545
|
+
|
|
546
|
+
def search(
|
|
547
|
+
self,
|
|
548
|
+
query: str,
|
|
549
|
+
user_id: str | None = None,
|
|
550
|
+
install_cube_ids: list[str] | None = None,
|
|
551
|
+
top_k: int | None = None,
|
|
552
|
+
mode: Literal["fast", "fine"] = "fast",
|
|
553
|
+
internet_search: bool = False,
|
|
554
|
+
moscube: bool = False,
|
|
555
|
+
session_id: str | None = None,
|
|
556
|
+
**kwargs,
|
|
557
|
+
) -> MOSSearchResult:
|
|
558
|
+
"""
|
|
559
|
+
Search for textual memories across all registered MemCubes.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
query (str): The search query.
|
|
563
|
+
user_id (str, optional): The identifier of the user to search for.
|
|
564
|
+
If None, the default user is used.
|
|
565
|
+
install_cube_ids (list[str], optional): The list of MemCube IDs to install.
|
|
566
|
+
If None, all MemCube for the user is used.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
MemoryResult: A dictionary containing the search results.
|
|
570
|
+
"""
|
|
571
|
+
target_session_id = session_id if session_id is not None else self.session_id
|
|
572
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
573
|
+
|
|
574
|
+
self._validate_user_exists(target_user_id)
|
|
575
|
+
# Get all cubes accessible by the target user
|
|
576
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
577
|
+
user_cube_ids = [cube.cube_id for cube in accessible_cubes]
|
|
578
|
+
|
|
579
|
+
logger.info(
|
|
580
|
+
f"User {target_user_id} has access to {len(user_cube_ids)} cubes: {user_cube_ids}"
|
|
581
|
+
)
|
|
582
|
+
if target_user_id not in self.chat_history_manager:
|
|
583
|
+
self._register_chat_history(target_user_id)
|
|
584
|
+
chat_history = self.chat_history_manager[target_user_id]
|
|
585
|
+
|
|
586
|
+
# Create search filter if session_id is provided
|
|
587
|
+
search_filter = None
|
|
588
|
+
if session_id is not None:
|
|
589
|
+
search_filter = {"session_id": session_id}
|
|
590
|
+
|
|
591
|
+
result: MOSSearchResult = {
|
|
592
|
+
"text_mem": [],
|
|
593
|
+
"act_mem": [],
|
|
594
|
+
"para_mem": [],
|
|
595
|
+
"pref_mem": [],
|
|
596
|
+
}
|
|
597
|
+
if install_cube_ids is None:
|
|
598
|
+
install_cube_ids = user_cube_ids
|
|
599
|
+
# create exist dict in mem_cubes and avoid one search slow
|
|
600
|
+
tmp_mem_cubes = {}
|
|
601
|
+
time_start_cube_get = time.time()
|
|
602
|
+
for mem_cube_id in install_cube_ids:
|
|
603
|
+
if mem_cube_id in self.mem_cubes:
|
|
604
|
+
tmp_mem_cubes[mem_cube_id] = self.mem_cubes.get(mem_cube_id)
|
|
605
|
+
logger.info(
|
|
606
|
+
f"time search: transform cube time user_id: {target_user_id} time is: {time.time() - time_start_cube_get}"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
for mem_cube_id, mem_cube in tmp_mem_cubes.items():
|
|
610
|
+
# Define internal functions for parallel search execution
|
|
611
|
+
def search_textual_memory(cube_id, cube):
|
|
612
|
+
if (
|
|
613
|
+
(cube_id in install_cube_ids)
|
|
614
|
+
and (cube.text_mem is not None)
|
|
615
|
+
and self.config.enable_textual_memory
|
|
616
|
+
):
|
|
617
|
+
time_start = time.time()
|
|
618
|
+
memories = cube.text_mem.search(
|
|
619
|
+
query,
|
|
620
|
+
top_k=top_k if top_k else self.config.top_k,
|
|
621
|
+
mode=mode,
|
|
622
|
+
manual_close_internet=not internet_search,
|
|
623
|
+
info={
|
|
624
|
+
"user_id": target_user_id,
|
|
625
|
+
"session_id": target_session_id,
|
|
626
|
+
"chat_history": chat_history.chat_history,
|
|
627
|
+
},
|
|
628
|
+
moscube=moscube,
|
|
629
|
+
search_filter=search_filter,
|
|
630
|
+
)
|
|
631
|
+
search_time_end = time.time()
|
|
632
|
+
logger.info(
|
|
633
|
+
f"🧠 [Memory] Searched memories from {cube_id}:\n{self._str_memories(memories)}\n"
|
|
634
|
+
)
|
|
635
|
+
logger.info(
|
|
636
|
+
f"time search graph: search graph time user_id: {target_user_id} time is: {search_time_end - time_start}"
|
|
637
|
+
)
|
|
638
|
+
return {"cube_id": cube_id, "memories": memories}
|
|
639
|
+
return None
|
|
640
|
+
|
|
641
|
+
def search_preference_memory(cube_id, cube):
|
|
642
|
+
if (
|
|
643
|
+
(cube_id in install_cube_ids)
|
|
644
|
+
and (cube.pref_mem is not None)
|
|
645
|
+
and self.config.enable_preference_memory
|
|
646
|
+
):
|
|
647
|
+
time_start = time.time()
|
|
648
|
+
memories = cube.pref_mem.search(
|
|
649
|
+
query,
|
|
650
|
+
top_k=top_k if top_k else self.config.top_k,
|
|
651
|
+
info={
|
|
652
|
+
"user_id": target_user_id,
|
|
653
|
+
"session_id": self.session_id,
|
|
654
|
+
"chat_history": chat_history.chat_history,
|
|
655
|
+
},
|
|
656
|
+
)
|
|
657
|
+
search_time_end = time.time()
|
|
658
|
+
logger.info(
|
|
659
|
+
f"🧠 [Memory] Searched preferences from {cube_id}:\n{self._str_memories(memories)}\n"
|
|
660
|
+
)
|
|
661
|
+
logger.info(
|
|
662
|
+
f"time search pref: search pref time user_id: {target_user_id} time is: {search_time_end - time_start}"
|
|
663
|
+
)
|
|
664
|
+
return {"cube_id": cube_id, "memories": memories}
|
|
665
|
+
return None
|
|
666
|
+
|
|
667
|
+
# Execute both search functions in parallel
|
|
668
|
+
with ContextThreadPoolExecutor(max_workers=2) as executor:
|
|
669
|
+
text_future = executor.submit(search_textual_memory, mem_cube_id, mem_cube)
|
|
670
|
+
pref_future = executor.submit(search_preference_memory, mem_cube_id, mem_cube)
|
|
671
|
+
|
|
672
|
+
# Wait for both tasks to complete and collect results
|
|
673
|
+
text_result = text_future.result()
|
|
674
|
+
pref_result = pref_future.result()
|
|
675
|
+
|
|
676
|
+
# Add results to the main result dictionary
|
|
677
|
+
if text_result is not None:
|
|
678
|
+
result["text_mem"].append(text_result)
|
|
679
|
+
if pref_result is not None:
|
|
680
|
+
result["pref_mem"].append(pref_result)
|
|
681
|
+
|
|
682
|
+
return result
|
|
683
|
+
|
|
684
|
+
def add(
|
|
685
|
+
self,
|
|
686
|
+
messages: MessageList | None = None,
|
|
687
|
+
memory_content: str | None = None,
|
|
688
|
+
doc_path: str | None = None,
|
|
689
|
+
mem_cube_id: str | None = None,
|
|
690
|
+
user_id: str | None = None,
|
|
691
|
+
session_id: str | None = None,
|
|
692
|
+
task_id: str | None = None, # New: Add task_id parameter
|
|
693
|
+
**kwargs,
|
|
694
|
+
) -> None:
|
|
695
|
+
"""
|
|
696
|
+
Add textual memories to a MemCube.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
messages (Union[MessageList, str]): The path to a document or a list of messages.
|
|
700
|
+
memory_content (str, optional): The content of the memory to add.
|
|
701
|
+
doc_path (str, optional): The path to the document associated with the memory.
|
|
702
|
+
mem_cube_id (str, optional): The identifier of the MemCube to add the memories to.
|
|
703
|
+
If None, the default MemCube for the user is used.
|
|
704
|
+
user_id (str, optional): The identifier of the user to add the memories to.
|
|
705
|
+
If None, the default user is used.
|
|
706
|
+
session_id (str, optional): session_id
|
|
707
|
+
"""
|
|
708
|
+
# user input messages
|
|
709
|
+
assert (messages is not None) or (memory_content is not None) or (doc_path is not None), (
|
|
710
|
+
"messages_or_doc_path or memory_content or doc_path must be provided."
|
|
711
|
+
)
|
|
712
|
+
# TODO: asure that session_id is a valid string
|
|
713
|
+
time_start = time.time()
|
|
714
|
+
|
|
715
|
+
target_session_id = session_id if session_id else self.session_id
|
|
716
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
717
|
+
if mem_cube_id is None:
|
|
718
|
+
# Try to find a default cube for the user
|
|
719
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
720
|
+
if not accessible_cubes:
|
|
721
|
+
raise ValueError(
|
|
722
|
+
f"No accessible cubes found for user '{target_user_id}'. Please register a cube first."
|
|
723
|
+
)
|
|
724
|
+
mem_cube_id = accessible_cubes[0].cube_id # TODO not only first
|
|
725
|
+
else:
|
|
726
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
727
|
+
logger.info(
|
|
728
|
+
f"time add: get mem_cube_id time user_id: {target_user_id} time is: {time.time() - time_start}"
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
if mem_cube_id not in self.mem_cubes:
|
|
732
|
+
raise ValueError(f"MemCube '{mem_cube_id}' is not loaded. Please register.")
|
|
733
|
+
|
|
734
|
+
sync_mode = self.mem_cubes[mem_cube_id].text_mem.mode
|
|
735
|
+
if sync_mode == "async":
|
|
736
|
+
assert self.mem_scheduler is not None, (
|
|
737
|
+
"Mem-Scheduler must be working when use asynchronous memory adding."
|
|
738
|
+
)
|
|
739
|
+
logger.debug(f"Mem-reader mode is: {sync_mode}")
|
|
740
|
+
|
|
741
|
+
def process_textual_memory():
|
|
742
|
+
if (
|
|
743
|
+
(messages is not None)
|
|
744
|
+
and self.config.enable_textual_memory
|
|
745
|
+
and self.mem_cubes[mem_cube_id].text_mem
|
|
746
|
+
):
|
|
747
|
+
if self.mem_cubes[mem_cube_id].config.text_mem.backend != "tree_text":
|
|
748
|
+
add_memory = []
|
|
749
|
+
metadata = TextualMemoryMetadata(
|
|
750
|
+
user_id=target_user_id, session_id=target_session_id, source="conversation"
|
|
751
|
+
)
|
|
752
|
+
for message in messages:
|
|
753
|
+
add_memory.append(
|
|
754
|
+
TextualMemoryItem(memory=message["content"], metadata=metadata)
|
|
755
|
+
)
|
|
756
|
+
self.mem_cubes[mem_cube_id].text_mem.add(add_memory)
|
|
757
|
+
else:
|
|
758
|
+
messages_list = [messages]
|
|
759
|
+
memories = self.mem_reader.get_memory(
|
|
760
|
+
messages_list,
|
|
761
|
+
type="chat",
|
|
762
|
+
info={"user_id": target_user_id, "session_id": target_session_id},
|
|
763
|
+
mode="fast" if sync_mode == "async" else "fine",
|
|
764
|
+
)
|
|
765
|
+
memories_flatten = [m for m_list in memories for m in m_list]
|
|
766
|
+
mem_ids: list[str] = self.mem_cubes[mem_cube_id].text_mem.add(memories_flatten)
|
|
767
|
+
logger.info(
|
|
768
|
+
f"Added memory user {target_user_id} to memcube {mem_cube_id}: {mem_ids}"
|
|
769
|
+
)
|
|
770
|
+
# submit messages for scheduler
|
|
771
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
772
|
+
if sync_mode == "async":
|
|
773
|
+
message_item = ScheduleMessageItem(
|
|
774
|
+
user_id=target_user_id,
|
|
775
|
+
mem_cube_id=mem_cube_id,
|
|
776
|
+
label=MEM_READ_TASK_LABEL,
|
|
777
|
+
content=json.dumps(mem_ids),
|
|
778
|
+
timestamp=datetime.utcnow(),
|
|
779
|
+
task_id=task_id,
|
|
780
|
+
)
|
|
781
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
782
|
+
else:
|
|
783
|
+
message_item = ScheduleMessageItem(
|
|
784
|
+
user_id=target_user_id,
|
|
785
|
+
mem_cube_id=mem_cube_id,
|
|
786
|
+
label=ADD_TASK_LABEL,
|
|
787
|
+
content=json.dumps(mem_ids),
|
|
788
|
+
timestamp=datetime.utcnow(),
|
|
789
|
+
task_id=task_id,
|
|
790
|
+
)
|
|
791
|
+
logger.info(
|
|
792
|
+
f"[DIAGNOSTIC] core.add: Submitting message to scheduler: {message_item.model_dump_json(indent=2)}"
|
|
793
|
+
)
|
|
794
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
795
|
+
|
|
796
|
+
def process_preference_memory():
|
|
797
|
+
if (
|
|
798
|
+
(messages is not None)
|
|
799
|
+
and self.config.enable_preference_memory
|
|
800
|
+
and self.mem_cubes[mem_cube_id].pref_mem
|
|
801
|
+
):
|
|
802
|
+
messages_list = [messages]
|
|
803
|
+
if sync_mode == "sync":
|
|
804
|
+
pref_memories = self.mem_cubes[mem_cube_id].pref_mem.get_memory(
|
|
805
|
+
messages_list,
|
|
806
|
+
type="chat",
|
|
807
|
+
info={
|
|
808
|
+
"user_id": target_user_id,
|
|
809
|
+
"session_id": self.session_id,
|
|
810
|
+
"mem_cube_id": mem_cube_id,
|
|
811
|
+
},
|
|
812
|
+
)
|
|
813
|
+
pref_ids = self.mem_cubes[mem_cube_id].pref_mem.add(pref_memories)
|
|
814
|
+
logger.info(
|
|
815
|
+
f"Added preferences user {target_user_id} to memcube {mem_cube_id}: {pref_ids}"
|
|
816
|
+
)
|
|
817
|
+
elif sync_mode == "async":
|
|
818
|
+
assert self.mem_scheduler is not None, (
|
|
819
|
+
"Mem-Scheduler must be working when use asynchronous memory adding."
|
|
820
|
+
)
|
|
821
|
+
message_item = ScheduleMessageItem(
|
|
822
|
+
user_id=target_user_id,
|
|
823
|
+
session_id=target_session_id,
|
|
824
|
+
mem_cube_id=mem_cube_id,
|
|
825
|
+
label=PREF_ADD_TASK_LABEL,
|
|
826
|
+
content=json.dumps(messages_list),
|
|
827
|
+
timestamp=datetime.utcnow(),
|
|
828
|
+
)
|
|
829
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
830
|
+
|
|
831
|
+
# Execute both memory processing functions in parallel
|
|
832
|
+
with ContextThreadPoolExecutor(max_workers=2) as executor:
|
|
833
|
+
text_future = executor.submit(process_textual_memory)
|
|
834
|
+
pref_future = executor.submit(process_preference_memory)
|
|
835
|
+
|
|
836
|
+
# Wait for both tasks to complete
|
|
837
|
+
text_future.result()
|
|
838
|
+
pref_future.result()
|
|
839
|
+
|
|
840
|
+
# user profile
|
|
841
|
+
if (
|
|
842
|
+
(memory_content is not None)
|
|
843
|
+
and self.config.enable_textual_memory
|
|
844
|
+
and self.mem_cubes[mem_cube_id].text_mem
|
|
845
|
+
):
|
|
846
|
+
if self.mem_cubes[mem_cube_id].config.text_mem.backend != "tree_text":
|
|
847
|
+
metadata = TextualMemoryMetadata(
|
|
848
|
+
user_id=target_user_id, session_id=target_session_id, source="conversation"
|
|
849
|
+
)
|
|
850
|
+
self.mem_cubes[mem_cube_id].text_mem.add(
|
|
851
|
+
[TextualMemoryItem(memory=memory_content, metadata=metadata)]
|
|
852
|
+
)
|
|
853
|
+
else:
|
|
854
|
+
messages_list = [
|
|
855
|
+
[{"role": "user", "content": memory_content}]
|
|
856
|
+
] # for only user-str input and convert message
|
|
857
|
+
|
|
858
|
+
memories = self.mem_reader.get_memory(
|
|
859
|
+
messages_list,
|
|
860
|
+
type="chat",
|
|
861
|
+
info={"user_id": target_user_id, "session_id": target_session_id},
|
|
862
|
+
mode="fast" if sync_mode == "async" else "fine",
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
mem_ids = []
|
|
866
|
+
for mem in memories:
|
|
867
|
+
mem_id_list: list[str] = self.mem_cubes[mem_cube_id].text_mem.add(mem)
|
|
868
|
+
logger.info(
|
|
869
|
+
f"Added memory user {target_user_id} to memcube {mem_cube_id}: {mem_id_list}"
|
|
870
|
+
)
|
|
871
|
+
mem_ids.extend(mem_id_list)
|
|
872
|
+
|
|
873
|
+
# submit messages for scheduler
|
|
874
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
875
|
+
if sync_mode == "async":
|
|
876
|
+
message_item = ScheduleMessageItem(
|
|
877
|
+
user_id=target_user_id,
|
|
878
|
+
mem_cube_id=mem_cube_id,
|
|
879
|
+
label=MEM_READ_TASK_LABEL,
|
|
880
|
+
content=json.dumps(mem_ids),
|
|
881
|
+
timestamp=datetime.utcnow(),
|
|
882
|
+
)
|
|
883
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
884
|
+
else:
|
|
885
|
+
message_item = ScheduleMessageItem(
|
|
886
|
+
user_id=target_user_id,
|
|
887
|
+
mem_cube_id=mem_cube_id,
|
|
888
|
+
label=ADD_TASK_LABEL,
|
|
889
|
+
content=json.dumps(mem_ids),
|
|
890
|
+
timestamp=datetime.utcnow(),
|
|
891
|
+
)
|
|
892
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
893
|
+
|
|
894
|
+
# user doc input
|
|
895
|
+
if (
|
|
896
|
+
(doc_path is not None)
|
|
897
|
+
and self.config.enable_textual_memory
|
|
898
|
+
and self.mem_cubes[mem_cube_id].text_mem
|
|
899
|
+
):
|
|
900
|
+
documents = self._get_all_documents(doc_path)
|
|
901
|
+
doc_memories = self.mem_reader.get_memory(
|
|
902
|
+
documents,
|
|
903
|
+
type="doc",
|
|
904
|
+
info={"user_id": target_user_id, "session_id": target_session_id},
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
mem_ids = []
|
|
908
|
+
for mem in doc_memories:
|
|
909
|
+
mem_id_list: list[str] = self.mem_cubes[mem_cube_id].text_mem.add(mem)
|
|
910
|
+
mem_ids.extend(mem_id_list)
|
|
911
|
+
|
|
912
|
+
# submit messages for scheduler
|
|
913
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
914
|
+
message_item = ScheduleMessageItem(
|
|
915
|
+
user_id=target_user_id,
|
|
916
|
+
mem_cube_id=mem_cube_id,
|
|
917
|
+
label=ADD_TASK_LABEL,
|
|
918
|
+
content=json.dumps(mem_ids),
|
|
919
|
+
timestamp=datetime.utcnow(),
|
|
920
|
+
)
|
|
921
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
922
|
+
|
|
923
|
+
logger.info(f"Add memory to {mem_cube_id} successfully")
|
|
924
|
+
|
|
925
|
+
def get(
|
|
926
|
+
self, mem_cube_id: str, memory_id: str, user_id: str | None = None
|
|
927
|
+
) -> TextualMemoryItem | ActivationMemoryItem | ParametricMemoryItem:
|
|
928
|
+
"""
|
|
929
|
+
Get a textual memory from a MemCube.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
mem_cube_id (str): The identifier of the MemCube to get the memory from.
|
|
933
|
+
memory_id (str): The identifier of the memory to get.
|
|
934
|
+
user_id (str, optional): The identifier of the user to get the memory from.
|
|
935
|
+
If None, the default user is used.
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
Union[TextualMemoryItem, ActivationMemoryItem, ParametricMemoryItem]: The requested memory item.
|
|
939
|
+
"""
|
|
940
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
941
|
+
# Validate user has access to this cube
|
|
942
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
943
|
+
if mem_cube_id is None:
|
|
944
|
+
# Try to find a default cube for the user
|
|
945
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
946
|
+
if not accessible_cubes:
|
|
947
|
+
raise ValueError(
|
|
948
|
+
f"No accessible cubes found for user '{target_user_id}'. Please register a cube first."
|
|
949
|
+
)
|
|
950
|
+
mem_cube_id = accessible_cubes[0].cube_id # TODO not only first
|
|
951
|
+
else:
|
|
952
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
953
|
+
|
|
954
|
+
assert mem_cube_id in self.mem_cubes, (
|
|
955
|
+
f"MemCube with ID {mem_cube_id} does not exist. please regiester"
|
|
956
|
+
)
|
|
957
|
+
return self.mem_cubes[mem_cube_id].text_mem.get(memory_id)
|
|
958
|
+
|
|
959
|
+
def get_all(
|
|
960
|
+
self, mem_cube_id: str | None = None, user_id: str | None = None
|
|
961
|
+
) -> MOSSearchResult:
|
|
962
|
+
"""
|
|
963
|
+
Get all textual memories from a MemCube.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
mem_cube_id (str, optional): The identifier of the MemCube to get the memories from.
|
|
967
|
+
If None, all MemCube for the user is used.
|
|
968
|
+
user_id (str, optional): The identifier of the user to get the memories from.
|
|
969
|
+
If None, the default user is used.
|
|
970
|
+
|
|
971
|
+
Returns:
|
|
972
|
+
MemoryResult: A dictionary containing the search results.
|
|
973
|
+
"""
|
|
974
|
+
result: MOSSearchResult = {"para_mem": [], "act_mem": [], "text_mem": []}
|
|
975
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
976
|
+
# Validate user has access to this cube
|
|
977
|
+
if mem_cube_id is None:
|
|
978
|
+
# Try to find a default cube for the user
|
|
979
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
980
|
+
if not accessible_cubes:
|
|
981
|
+
raise ValueError(
|
|
982
|
+
f"No accessible cubes found for user '{target_user_id}'. Please register a cube first."
|
|
983
|
+
)
|
|
984
|
+
mem_cube_id = accessible_cubes[0].cube_id # TODO not only first
|
|
985
|
+
else:
|
|
986
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
987
|
+
if self.config.enable_textual_memory and self.mem_cubes[mem_cube_id].text_mem:
|
|
988
|
+
result["text_mem"].append(
|
|
989
|
+
{"cube_id": mem_cube_id, "memories": self.mem_cubes[mem_cube_id].text_mem.get_all()}
|
|
990
|
+
)
|
|
991
|
+
if self.config.enable_activation_memory and self.mem_cubes[mem_cube_id].act_mem:
|
|
992
|
+
result["act_mem"].append(
|
|
993
|
+
{"cube_id": mem_cube_id, "memories": self.mem_cubes[mem_cube_id].act_mem.get_all()}
|
|
994
|
+
)
|
|
995
|
+
return result
|
|
996
|
+
|
|
997
|
+
def update(
|
|
998
|
+
self,
|
|
999
|
+
mem_cube_id: str,
|
|
1000
|
+
memory_id: str,
|
|
1001
|
+
text_memory_item: TextualMemoryItem | dict[str, Any],
|
|
1002
|
+
user_id: str | None = None,
|
|
1003
|
+
) -> None:
|
|
1004
|
+
"""
|
|
1005
|
+
Update a textual memory in a MemCube by text_memory_id and text_memory_id.
|
|
1006
|
+
|
|
1007
|
+
Args:
|
|
1008
|
+
mem_cube_id (str): The identifier of the MemCube to update the memory in.
|
|
1009
|
+
memory_id (str): The identifier of the textual memory to update.
|
|
1010
|
+
text_memory_item (TextualMemoryItem | dict[str, Any]): The updated textual memory item.
|
|
1011
|
+
"""
|
|
1012
|
+
assert mem_cube_id in self.mem_cubes, (
|
|
1013
|
+
f"MemCube with ID {mem_cube_id} does not exist. please regiester"
|
|
1014
|
+
)
|
|
1015
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
1016
|
+
# Validate user has access to this cube
|
|
1017
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
1018
|
+
if mem_cube_id is None:
|
|
1019
|
+
# Try to find a default cube for the user
|
|
1020
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
1021
|
+
if not accessible_cubes:
|
|
1022
|
+
raise ValueError(
|
|
1023
|
+
f"No accessible cubes found for user '{target_user_id}'. Please register a cube first."
|
|
1024
|
+
)
|
|
1025
|
+
mem_cube_id = accessible_cubes[0].cube_id # TODO not only first
|
|
1026
|
+
else:
|
|
1027
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
1028
|
+
if self.mem_cubes[mem_cube_id].config.text_mem.backend != "tree_text":
|
|
1029
|
+
self.mem_cubes[mem_cube_id].text_mem.update(memory_id, memories=text_memory_item)
|
|
1030
|
+
logger.info(f"MemCube {mem_cube_id} updated memory {memory_id}")
|
|
1031
|
+
else:
|
|
1032
|
+
logger.warning(
|
|
1033
|
+
f" {self.mem_cubes[mem_cube_id].config.text_mem.backend} does not support update memory"
|
|
1034
|
+
)
|
|
1035
|
+
|
|
1036
|
+
def delete(self, mem_cube_id: str, memory_id: str, user_id: str | None = None) -> None:
|
|
1037
|
+
"""
|
|
1038
|
+
Delete a textual memory from a MemCube by memory_id.
|
|
1039
|
+
|
|
1040
|
+
Args:
|
|
1041
|
+
mem_cube_id (str): The identifier of the MemCube to delete the memory from.
|
|
1042
|
+
memory_id (str): The identifier of the memory to delete.
|
|
1043
|
+
"""
|
|
1044
|
+
assert mem_cube_id in self.mem_cubes, (
|
|
1045
|
+
f"MemCube with ID {mem_cube_id} does not exist. please regiester"
|
|
1046
|
+
)
|
|
1047
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
1048
|
+
# Validate user has access to this cube
|
|
1049
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
1050
|
+
if mem_cube_id is None:
|
|
1051
|
+
# Try to find a default cube for the user
|
|
1052
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
1053
|
+
if not accessible_cubes:
|
|
1054
|
+
raise ValueError(
|
|
1055
|
+
f"No accessible cubes found for user '{target_user_id}'. Please register a cube first."
|
|
1056
|
+
)
|
|
1057
|
+
mem_cube_id = accessible_cubes[0].cube_id # TODO not only first
|
|
1058
|
+
else:
|
|
1059
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
1060
|
+
self.mem_cubes[mem_cube_id].text_mem.delete(memory_id)
|
|
1061
|
+
logger.info(f"MemCube {mem_cube_id} deleted memory {memory_id}")
|
|
1062
|
+
|
|
1063
|
+
def delete_all(self, mem_cube_id: str | None = None, user_id: str | None = None) -> None:
|
|
1064
|
+
"""
|
|
1065
|
+
Delete all textual memories from a MemCube for user.
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
mem_cube_id (str): The identifier of the MemCube to delete the memories from.
|
|
1069
|
+
"""
|
|
1070
|
+
assert mem_cube_id in self.mem_cubes, (
|
|
1071
|
+
f"MemCube with ID {mem_cube_id} does not exist. please regiester"
|
|
1072
|
+
)
|
|
1073
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
1074
|
+
# Validate user has access to this cube
|
|
1075
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
1076
|
+
if mem_cube_id is None:
|
|
1077
|
+
# Try to find a default cube for the user
|
|
1078
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
1079
|
+
if not accessible_cubes:
|
|
1080
|
+
raise ValueError(
|
|
1081
|
+
f"No accessible cubes found for user '{target_user_id}'. Please register a cube first."
|
|
1082
|
+
)
|
|
1083
|
+
mem_cube_id = accessible_cubes[0].cube_id # TODO not only first
|
|
1084
|
+
else:
|
|
1085
|
+
self._validate_cube_access(target_user_id, mem_cube_id)
|
|
1086
|
+
self.mem_cubes[mem_cube_id].text_mem.delete_all()
|
|
1087
|
+
logger.info(f"MemCube {mem_cube_id} deleted all memories")
|
|
1088
|
+
|
|
1089
|
+
def dump(
|
|
1090
|
+
self, dump_dir: str, user_id: str | None = None, mem_cube_id: str | None = None
|
|
1091
|
+
) -> None:
|
|
1092
|
+
"""Dump the MemCube to a dictionary.
|
|
1093
|
+
Args:
|
|
1094
|
+
dump_dir (str): The directory to dump the MemCube to.
|
|
1095
|
+
user_id (str, optional): The identifier of the user to dump the MemCube from.
|
|
1096
|
+
If None, the default user is used.
|
|
1097
|
+
mem_cube_id (str, optional): The identifier of the MemCube to dump.
|
|
1098
|
+
If None, the default MemCube for the user is used.
|
|
1099
|
+
"""
|
|
1100
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
1101
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
1102
|
+
if not mem_cube_id:
|
|
1103
|
+
mem_cube_id = accessible_cubes[0].cube_id
|
|
1104
|
+
if mem_cube_id not in self.mem_cubes:
|
|
1105
|
+
raise ValueError(f"MemCube with ID {mem_cube_id} does not exist. please regiester")
|
|
1106
|
+
self.mem_cubes[mem_cube_id].dump(dump_dir)
|
|
1107
|
+
logger.info(f"MemCube {mem_cube_id} dumped to {dump_dir}")
|
|
1108
|
+
|
|
1109
|
+
def load(
|
|
1110
|
+
self,
|
|
1111
|
+
load_dir: str,
|
|
1112
|
+
user_id: str | None = None,
|
|
1113
|
+
mem_cube_id: str | None = None,
|
|
1114
|
+
memory_types: list[Literal["text_mem", "act_mem", "para_mem", "pref_mem"]] | None = None,
|
|
1115
|
+
) -> None:
|
|
1116
|
+
"""Dump the MemCube to a dictionary.
|
|
1117
|
+
Args:
|
|
1118
|
+
load_dir (str): The directory to load the MemCube from.
|
|
1119
|
+
user_id (str, optional): The identifier of the user to load the MemCube from.
|
|
1120
|
+
If None, the default user is used.
|
|
1121
|
+
mem_cube_id (str, optional): The identifier of the MemCube to load.
|
|
1122
|
+
If None, the default MemCube for the user is used.
|
|
1123
|
+
"""
|
|
1124
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
1125
|
+
accessible_cubes = self.user_manager.get_user_cubes(target_user_id)
|
|
1126
|
+
if not mem_cube_id:
|
|
1127
|
+
mem_cube_id = accessible_cubes[0].cube_id
|
|
1128
|
+
if mem_cube_id not in self.mem_cubes:
|
|
1129
|
+
raise ValueError(f"MemCube with ID {mem_cube_id} does not exist. please regiester")
|
|
1130
|
+
self.mem_cubes[mem_cube_id].load(load_dir, memory_types=memory_types)
|
|
1131
|
+
logger.info(f"MemCube {mem_cube_id} loaded from {load_dir}")
|
|
1132
|
+
|
|
1133
|
+
def get_user_info(self) -> dict[str, Any]:
|
|
1134
|
+
"""Get current user information including accessible cubes.
|
|
1135
|
+
TODO: maybe input user_id
|
|
1136
|
+
Returns:
|
|
1137
|
+
dict: User information and accessible cubes.
|
|
1138
|
+
"""
|
|
1139
|
+
user = self.user_manager.get_user(self.user_id)
|
|
1140
|
+
if not user:
|
|
1141
|
+
return {}
|
|
1142
|
+
|
|
1143
|
+
accessible_cubes = self.user_manager.get_user_cubes(self.user_id)
|
|
1144
|
+
|
|
1145
|
+
return {
|
|
1146
|
+
"user_id": user.user_id,
|
|
1147
|
+
"user_name": user.user_name,
|
|
1148
|
+
"role": user.role.value if hasattr(user.role, "value") else user.role,
|
|
1149
|
+
"created_at": user.created_at.isoformat(),
|
|
1150
|
+
"accessible_cubes": [
|
|
1151
|
+
{
|
|
1152
|
+
"cube_id": cube.cube_id,
|
|
1153
|
+
"cube_name": cube.cube_name,
|
|
1154
|
+
"cube_path": cube.cube_path,
|
|
1155
|
+
"owner_id": cube.owner_id,
|
|
1156
|
+
"is_loaded": cube.cube_id in self.mem_cubes,
|
|
1157
|
+
}
|
|
1158
|
+
for cube in accessible_cubes
|
|
1159
|
+
],
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
def share_cube_with_user(self, cube_id: str, target_user_id: str) -> bool:
|
|
1163
|
+
"""Share a cube with another user.
|
|
1164
|
+
|
|
1165
|
+
Args:
|
|
1166
|
+
cube_id (str): The cube ID to share.
|
|
1167
|
+
target_user_id (str): The user ID to share with.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
bool: True if successful, False otherwise.
|
|
1171
|
+
"""
|
|
1172
|
+
# Validate current user has access to this cube
|
|
1173
|
+
self._validate_cube_access(cube_id, target_user_id)
|
|
1174
|
+
|
|
1175
|
+
# Validate target user exists
|
|
1176
|
+
if not self.user_manager.validate_user(target_user_id):
|
|
1177
|
+
raise ValueError(f"Target user '{target_user_id}' does not exist or is inactive.")
|
|
1178
|
+
|
|
1179
|
+
return self.user_manager.add_user_to_cube(target_user_id, cube_id)
|
|
1180
|
+
|
|
1181
|
+
def get_query_rewrite(self, query: str, user_id: str | None = None):
|
|
1182
|
+
"""
|
|
1183
|
+
Rewrite user's query according the context.
|
|
1184
|
+
Args:
|
|
1185
|
+
query (str): The search query that needs rewriting.
|
|
1186
|
+
user_id(str, optional): The identifier of the user that the query belongs to.
|
|
1187
|
+
If None, the default user is used.
|
|
1188
|
+
|
|
1189
|
+
Returns:
|
|
1190
|
+
str: query after rewriting process.
|
|
1191
|
+
"""
|
|
1192
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
1193
|
+
chat_history = self.chat_history_manager[target_user_id]
|
|
1194
|
+
|
|
1195
|
+
dialogue = "————{}".format("\n————".join(chat_history.chat_history))
|
|
1196
|
+
user_prompt = QUERY_REWRITING_PROMPT.format(dialogue=dialogue, query=query)
|
|
1197
|
+
messages = {"role": "user", "content": user_prompt}
|
|
1198
|
+
rewritten_result = self.chat_llm.generate(messages=messages)
|
|
1199
|
+
rewritten_result = json.loads(rewritten_result)
|
|
1200
|
+
if rewritten_result.get("former_dialogue_related", False):
|
|
1201
|
+
rewritten_query = rewritten_result.get("rewritten_question")
|
|
1202
|
+
return rewritten_query if len(rewritten_query) > 0 else query
|
|
1203
|
+
return query
|