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/product.py
ADDED
|
@@ -0,0 +1,1608 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
from transformers import AutoTokenizer
|
|
13
|
+
|
|
14
|
+
from memos.configs.mem_cube import GeneralMemCubeConfig
|
|
15
|
+
from memos.configs.mem_os import MOSConfig
|
|
16
|
+
from memos.context.context import ContextThread
|
|
17
|
+
from memos.log import get_logger
|
|
18
|
+
from memos.mem_cube.general import GeneralMemCube
|
|
19
|
+
from memos.mem_os.core import MOSCore
|
|
20
|
+
from memos.mem_os.utils.format_utils import (
|
|
21
|
+
clean_json_response,
|
|
22
|
+
convert_graph_to_tree_forworkmem,
|
|
23
|
+
ensure_unique_tree_ids,
|
|
24
|
+
filter_nodes_by_tree_ids,
|
|
25
|
+
remove_embedding_recursive,
|
|
26
|
+
sort_children_by_memory_type,
|
|
27
|
+
)
|
|
28
|
+
from memos.mem_os.utils.reference_utils import (
|
|
29
|
+
prepare_reference_data,
|
|
30
|
+
process_streaming_references_complete,
|
|
31
|
+
)
|
|
32
|
+
from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
|
|
33
|
+
from memos.mem_scheduler.schemas.task_schemas import (
|
|
34
|
+
ANSWER_TASK_LABEL,
|
|
35
|
+
QUERY_TASK_LABEL,
|
|
36
|
+
)
|
|
37
|
+
from memos.mem_user.persistent_factory import PersistentUserManagerFactory
|
|
38
|
+
from memos.mem_user.user_manager import UserRole
|
|
39
|
+
from memos.memories.textual.item import (
|
|
40
|
+
TextualMemoryItem,
|
|
41
|
+
)
|
|
42
|
+
from memos.templates.mos_prompts import (
|
|
43
|
+
FURTHER_SUGGESTION_PROMPT,
|
|
44
|
+
SUGGESTION_QUERY_PROMPT_EN,
|
|
45
|
+
SUGGESTION_QUERY_PROMPT_ZH,
|
|
46
|
+
get_memos_prompt,
|
|
47
|
+
)
|
|
48
|
+
from memos.types import MessageList
|
|
49
|
+
from memos.utils import timed
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
logger = get_logger(__name__)
|
|
53
|
+
|
|
54
|
+
load_dotenv()
|
|
55
|
+
|
|
56
|
+
CUBE_PATH = os.getenv("MOS_CUBE_PATH", "/tmp/data/")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _short_id(mem_id: str) -> str:
|
|
60
|
+
return (mem_id or "").split("-")[0] if mem_id else ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _format_mem_block(memories_all, max_items: int = 20, max_chars_each: int = 320) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Modify TextualMemoryItem Format:
|
|
66
|
+
1:abcd :: [P] text...
|
|
67
|
+
2:ef01 :: [O] text...
|
|
68
|
+
sequence is [i:memId] i; [P]=PersonalMemory / [O]=OuterMemory
|
|
69
|
+
"""
|
|
70
|
+
if not memories_all:
|
|
71
|
+
return "(none)", "(none)"
|
|
72
|
+
|
|
73
|
+
lines_o = []
|
|
74
|
+
lines_p = []
|
|
75
|
+
for idx, m in enumerate(memories_all[:max_items], 1):
|
|
76
|
+
mid = _short_id(getattr(m, "id", "") or "")
|
|
77
|
+
mtype = getattr(getattr(m, "metadata", {}), "memory_type", None) or getattr(
|
|
78
|
+
m, "metadata", {}
|
|
79
|
+
).get("memory_type", "")
|
|
80
|
+
tag = "O" if "Outer" in str(mtype) else "P"
|
|
81
|
+
txt = (getattr(m, "memory", "") or "").replace("\n", " ").strip()
|
|
82
|
+
if len(txt) > max_chars_each:
|
|
83
|
+
txt = txt[: max_chars_each - 1] + "…"
|
|
84
|
+
mid = mid or f"mem_{idx}"
|
|
85
|
+
if tag == "O":
|
|
86
|
+
lines_o.append(f"[{idx}:{mid}] :: [{tag}] {txt}\n")
|
|
87
|
+
elif tag == "P":
|
|
88
|
+
lines_p.append(f"[{idx}:{mid}] :: [{tag}] {txt}")
|
|
89
|
+
return "\n".join(lines_o), "\n".join(lines_p)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class MOSProduct(MOSCore):
|
|
93
|
+
"""
|
|
94
|
+
The MOSProduct class inherits from MOSCore and manages multiple users.
|
|
95
|
+
Each user has their own configuration and cube access, but shares the same model instances.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
default_config: MOSConfig | None = None,
|
|
101
|
+
max_user_instances: int = 1,
|
|
102
|
+
default_cube_config: GeneralMemCubeConfig | None = None,
|
|
103
|
+
online_bot=None,
|
|
104
|
+
error_bot=None,
|
|
105
|
+
):
|
|
106
|
+
"""
|
|
107
|
+
Initialize MOSProduct with an optional default configuration.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
default_config (MOSConfig | None): Default configuration for new users
|
|
111
|
+
max_user_instances (int): Maximum number of user instances to keep in memory
|
|
112
|
+
default_cube_config (GeneralMemCubeConfig | None): Default cube configuration for loading cubes
|
|
113
|
+
online_bot: DingDing online_bot function or None if disabled
|
|
114
|
+
error_bot: DingDing error_bot function or None if disabled
|
|
115
|
+
"""
|
|
116
|
+
# Initialize with a root config for shared resources
|
|
117
|
+
if default_config is None:
|
|
118
|
+
# Create a minimal config for root user
|
|
119
|
+
root_config = MOSConfig(
|
|
120
|
+
user_id="root",
|
|
121
|
+
session_id="root_session",
|
|
122
|
+
chat_model=default_config.chat_model if default_config else None,
|
|
123
|
+
mem_reader=default_config.mem_reader if default_config else None,
|
|
124
|
+
enable_mem_scheduler=default_config.enable_mem_scheduler
|
|
125
|
+
if default_config
|
|
126
|
+
else False,
|
|
127
|
+
mem_scheduler=default_config.mem_scheduler if default_config else None,
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
root_config = default_config.model_copy(deep=True)
|
|
131
|
+
root_config.user_id = "root"
|
|
132
|
+
root_config.session_id = "root_session"
|
|
133
|
+
|
|
134
|
+
# Create persistent user manager BEFORE calling parent constructor
|
|
135
|
+
persistent_user_manager_client = PersistentUserManagerFactory.from_config(
|
|
136
|
+
config_factory=root_config.user_manager
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Initialize parent MOSCore with root config and persistent user manager
|
|
140
|
+
super().__init__(root_config, user_manager=persistent_user_manager_client)
|
|
141
|
+
|
|
142
|
+
# Product-specific attributes
|
|
143
|
+
self.default_config = default_config
|
|
144
|
+
self.default_cube_config = default_cube_config
|
|
145
|
+
self.max_user_instances = max_user_instances
|
|
146
|
+
self.online_bot = online_bot
|
|
147
|
+
self.error_bot = error_bot
|
|
148
|
+
|
|
149
|
+
# User-specific data structures
|
|
150
|
+
self.user_configs: dict[str, MOSConfig] = {}
|
|
151
|
+
self.user_cube_access: dict[str, set[str]] = {} # user_id -> set of cube_ids
|
|
152
|
+
self.user_chat_histories: dict[str, dict] = {}
|
|
153
|
+
|
|
154
|
+
# Note: self.user_manager is now the persistent user manager from parent class
|
|
155
|
+
# No need for separate global_user_manager as they are the same instance
|
|
156
|
+
|
|
157
|
+
# Initialize tiktoken for streaming
|
|
158
|
+
try:
|
|
159
|
+
# Use gpt2 encoding which is more stable and widely compatible
|
|
160
|
+
self.tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
|
|
161
|
+
logger.info("tokenizer initialized successfully for streaming")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.warning(
|
|
164
|
+
f"Failed to initialize tokenizer, will use character-based chunking: {e}"
|
|
165
|
+
)
|
|
166
|
+
self.tokenizer = None
|
|
167
|
+
|
|
168
|
+
# Restore user instances from persistent storage
|
|
169
|
+
self._restore_user_instances(default_cube_config=default_cube_config)
|
|
170
|
+
logger.info(f"User instances restored successfully, now user is {self.mem_cubes.keys()}")
|
|
171
|
+
|
|
172
|
+
def _restore_user_instances(
|
|
173
|
+
self, default_cube_config: GeneralMemCubeConfig | None = None
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Restore user instances from persistent storage after service restart.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
# Get all user configurations from persistent storage
|
|
182
|
+
user_configs = self.user_manager.list_user_configs(self.max_user_instances)
|
|
183
|
+
|
|
184
|
+
# Get the raw database records for sorting by updated_at
|
|
185
|
+
session = self.user_manager._get_session()
|
|
186
|
+
try:
|
|
187
|
+
from memos.mem_user.persistent_user_manager import UserConfig
|
|
188
|
+
|
|
189
|
+
db_configs = session.query(UserConfig).limit(self.max_user_instances).all()
|
|
190
|
+
# Create a mapping of user_id to updated_at timestamp
|
|
191
|
+
updated_at_map = {config.user_id: config.updated_at for config in db_configs}
|
|
192
|
+
|
|
193
|
+
# Sort by updated_at timestamp (most recent first) and limit by max_instances
|
|
194
|
+
sorted_configs = sorted(
|
|
195
|
+
user_configs.items(), key=lambda x: updated_at_map.get(x[0], ""), reverse=True
|
|
196
|
+
)[: self.max_user_instances]
|
|
197
|
+
finally:
|
|
198
|
+
session.close()
|
|
199
|
+
|
|
200
|
+
for user_id, config in sorted_configs:
|
|
201
|
+
if user_id != "root": # Skip root user
|
|
202
|
+
try:
|
|
203
|
+
# Store user config and cube access
|
|
204
|
+
self.user_configs[user_id] = config
|
|
205
|
+
self._load_user_cube_access(user_id)
|
|
206
|
+
|
|
207
|
+
# Pre-load all cubes for this user with default config
|
|
208
|
+
self._preload_user_cubes(user_id, default_cube_config)
|
|
209
|
+
|
|
210
|
+
logger.info(
|
|
211
|
+
f"Restored user configuration and pre-loaded cubes for {user_id}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"Failed to restore user configuration for {user_id}: {e}")
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Error during user instance restoration: {e}")
|
|
219
|
+
|
|
220
|
+
def _initialize_cube_from_default_config(
|
|
221
|
+
self, cube_id: str, user_id: str, default_config: GeneralMemCubeConfig
|
|
222
|
+
) -> GeneralMemCube | None:
|
|
223
|
+
"""
|
|
224
|
+
Initialize a cube from default configuration when cube path doesn't exist.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
cube_id (str): The cube ID to initialize.
|
|
228
|
+
user_id (str): The user ID for the cube.
|
|
229
|
+
default_config (GeneralMemCubeConfig): The default configuration to use.
|
|
230
|
+
"""
|
|
231
|
+
cube_config = default_config.model_copy(deep=True)
|
|
232
|
+
# Safely modify the graph_db user_name if it exists
|
|
233
|
+
if cube_config.text_mem.config.graph_db.config:
|
|
234
|
+
cube_config.text_mem.config.graph_db.config.user_name = (
|
|
235
|
+
f"memos{user_id.replace('-', '')}"
|
|
236
|
+
)
|
|
237
|
+
mem_cube = GeneralMemCube(config=cube_config)
|
|
238
|
+
return mem_cube
|
|
239
|
+
|
|
240
|
+
def _preload_user_cubes(
|
|
241
|
+
self, user_id: str, default_cube_config: GeneralMemCubeConfig | None = None
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Pre-load all cubes for a user into memory.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
user_id (str): The user ID to pre-load cubes for.
|
|
247
|
+
default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
# Get user's accessible cubes from persistent storage
|
|
251
|
+
accessible_cubes = self.user_manager.get_user_cubes(user_id)
|
|
252
|
+
|
|
253
|
+
for cube in accessible_cubes:
|
|
254
|
+
if cube.cube_id not in self.mem_cubes:
|
|
255
|
+
try:
|
|
256
|
+
if cube.cube_path and os.path.exists(cube.cube_path):
|
|
257
|
+
# Pre-load cube with all memory types and default config
|
|
258
|
+
self.register_mem_cube(
|
|
259
|
+
cube.cube_path,
|
|
260
|
+
cube.cube_id,
|
|
261
|
+
user_id,
|
|
262
|
+
memory_types=["act_mem"]
|
|
263
|
+
if self.config.enable_activation_memory
|
|
264
|
+
else [],
|
|
265
|
+
default_config=default_cube_config,
|
|
266
|
+
)
|
|
267
|
+
logger.info(f"Pre-loaded cube {cube.cube_id} for user {user_id}")
|
|
268
|
+
else:
|
|
269
|
+
logger.warning(
|
|
270
|
+
f"Cube path {cube.cube_path} does not exist for cube {cube.cube_id}, skipping pre-load"
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(
|
|
274
|
+
f"Failed to pre-load cube {cube.cube_id} for user {user_id}: {e}",
|
|
275
|
+
exc_info=True,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Error pre-loading cubes for user {user_id}: {e}", exc_info=True)
|
|
280
|
+
|
|
281
|
+
@timed
|
|
282
|
+
def _load_user_cubes(
|
|
283
|
+
self, user_id: str, default_cube_config: GeneralMemCubeConfig | None = None
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Load all cubes for a user into memory.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
user_id (str): The user ID to load cubes for.
|
|
289
|
+
default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
|
|
290
|
+
"""
|
|
291
|
+
# Get user's accessible cubes from persistent storage
|
|
292
|
+
accessible_cubes = self.user_manager.get_user_cubes(user_id)
|
|
293
|
+
|
|
294
|
+
for cube in accessible_cubes[:1]:
|
|
295
|
+
if cube.cube_id not in self.mem_cubes:
|
|
296
|
+
try:
|
|
297
|
+
if cube.cube_path and os.path.exists(cube.cube_path):
|
|
298
|
+
# Use MOSCore's register_mem_cube method directly with default config
|
|
299
|
+
# Only load act_mem since text_mem is stored in database
|
|
300
|
+
self.register_mem_cube(
|
|
301
|
+
cube.cube_path,
|
|
302
|
+
cube.cube_id,
|
|
303
|
+
user_id,
|
|
304
|
+
memory_types=["act_mem"],
|
|
305
|
+
default_config=default_cube_config,
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
logger.warning(
|
|
309
|
+
f"Cube path {cube.cube_path} does not exist for cube {cube.cube_id}, now init by default config"
|
|
310
|
+
)
|
|
311
|
+
cube_obj = self._initialize_cube_from_default_config(
|
|
312
|
+
cube_id=cube.cube_id,
|
|
313
|
+
user_id=user_id,
|
|
314
|
+
default_config=default_cube_config,
|
|
315
|
+
)
|
|
316
|
+
if cube_obj:
|
|
317
|
+
self.register_mem_cube(
|
|
318
|
+
cube_obj,
|
|
319
|
+
cube.cube_id,
|
|
320
|
+
user_id,
|
|
321
|
+
memory_types=[],
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
f"Failed to initialize default cube {cube.cube_id} for user {user_id}"
|
|
326
|
+
)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(f"Failed to load cube {cube.cube_id} for user {user_id}: {e}")
|
|
329
|
+
logger.info(f"load user {user_id} cubes successfully")
|
|
330
|
+
|
|
331
|
+
def _ensure_user_instance(self, user_id: str, max_instances: int | None = None) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Ensure user configuration exists, creating it if necessary.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
user_id (str): The user ID
|
|
337
|
+
max_instances (int): Maximum instances to keep in memory (overrides class default)
|
|
338
|
+
"""
|
|
339
|
+
if user_id in self.user_configs:
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
# Try to get config from persistent storage first
|
|
343
|
+
stored_config = self.user_manager.get_user_config(user_id)
|
|
344
|
+
if stored_config:
|
|
345
|
+
self.user_configs[user_id] = stored_config
|
|
346
|
+
self._load_user_cube_access(user_id)
|
|
347
|
+
else:
|
|
348
|
+
# Use default config
|
|
349
|
+
if not self.default_config:
|
|
350
|
+
raise ValueError(f"No configuration available for user {user_id}")
|
|
351
|
+
user_config = self.default_config.model_copy(deep=True)
|
|
352
|
+
user_config.user_id = user_id
|
|
353
|
+
user_config.session_id = f"{user_id}_session"
|
|
354
|
+
self.user_configs[user_id] = user_config
|
|
355
|
+
self._load_user_cube_access(user_id)
|
|
356
|
+
|
|
357
|
+
# Apply LRU eviction if needed
|
|
358
|
+
max_instances = max_instances or self.max_user_instances
|
|
359
|
+
if len(self.user_configs) > max_instances:
|
|
360
|
+
# Remove least recently used instance (excluding root)
|
|
361
|
+
user_ids = [uid for uid in self.user_configs if uid != "root"]
|
|
362
|
+
if user_ids:
|
|
363
|
+
oldest_user_id = user_ids[0]
|
|
364
|
+
del self.user_configs[oldest_user_id]
|
|
365
|
+
if oldest_user_id in self.user_cube_access:
|
|
366
|
+
del self.user_cube_access[oldest_user_id]
|
|
367
|
+
logger.info(f"Removed least recently used user configuration: {oldest_user_id}")
|
|
368
|
+
|
|
369
|
+
def _load_user_cube_access(self, user_id: str) -> None:
|
|
370
|
+
"""Load user's cube access permissions."""
|
|
371
|
+
try:
|
|
372
|
+
# Get user's accessible cubes from persistent storage
|
|
373
|
+
accessible_cubes = self.user_manager.get_user_cube_access(user_id)
|
|
374
|
+
self.user_cube_access[user_id] = set(accessible_cubes)
|
|
375
|
+
except Exception as e:
|
|
376
|
+
logger.warning(f"Failed to load cube access for user {user_id}: {e}")
|
|
377
|
+
self.user_cube_access[user_id] = set()
|
|
378
|
+
|
|
379
|
+
def _get_user_config(self, user_id: str) -> MOSConfig:
|
|
380
|
+
"""Get user configuration."""
|
|
381
|
+
if user_id not in self.user_configs:
|
|
382
|
+
self._ensure_user_instance(user_id)
|
|
383
|
+
return self.user_configs[user_id]
|
|
384
|
+
|
|
385
|
+
def _validate_user_cube_access(self, user_id: str, cube_id: str) -> None:
|
|
386
|
+
"""Validate user has access to the cube."""
|
|
387
|
+
if user_id not in self.user_cube_access:
|
|
388
|
+
self._load_user_cube_access(user_id)
|
|
389
|
+
|
|
390
|
+
if cube_id not in self.user_cube_access.get(user_id, set()):
|
|
391
|
+
raise ValueError(f"User '{user_id}' does not have access to cube '{cube_id}'")
|
|
392
|
+
|
|
393
|
+
def _validate_user_access(self, user_id: str, cube_id: str | None = None) -> None:
|
|
394
|
+
"""Validate user access using MOSCore's built-in validation."""
|
|
395
|
+
# Use MOSCore's built-in user validation
|
|
396
|
+
if cube_id:
|
|
397
|
+
self._validate_cube_access(user_id, cube_id)
|
|
398
|
+
else:
|
|
399
|
+
self._validate_user_exists(user_id)
|
|
400
|
+
|
|
401
|
+
def _create_user_config(self, user_id: str, config: MOSConfig) -> MOSConfig:
|
|
402
|
+
"""Create a new user configuration."""
|
|
403
|
+
# Create a copy of config with the specific user_id
|
|
404
|
+
user_config = config.model_copy(deep=True)
|
|
405
|
+
user_config.user_id = user_id
|
|
406
|
+
user_config.session_id = f"{user_id}_session"
|
|
407
|
+
|
|
408
|
+
# Save configuration to persistent storage
|
|
409
|
+
self.user_manager.save_user_config(user_id, user_config)
|
|
410
|
+
|
|
411
|
+
return user_config
|
|
412
|
+
|
|
413
|
+
def _get_or_create_user_config(
|
|
414
|
+
self, user_id: str, config: MOSConfig | None = None
|
|
415
|
+
) -> MOSConfig:
|
|
416
|
+
"""Get existing user config or create a new one."""
|
|
417
|
+
if user_id in self.user_configs:
|
|
418
|
+
return self.user_configs[user_id]
|
|
419
|
+
|
|
420
|
+
# Try to get config from persistent storage first
|
|
421
|
+
stored_config = self.user_manager.get_user_config(user_id)
|
|
422
|
+
if stored_config:
|
|
423
|
+
return self._create_user_config(user_id, stored_config)
|
|
424
|
+
|
|
425
|
+
# Use provided config or default config
|
|
426
|
+
user_config = config or self.default_config
|
|
427
|
+
if not user_config:
|
|
428
|
+
raise ValueError(f"No configuration provided for user {user_id}")
|
|
429
|
+
|
|
430
|
+
return self._create_user_config(user_id, user_config)
|
|
431
|
+
|
|
432
|
+
def _build_system_prompt(
|
|
433
|
+
self,
|
|
434
|
+
memories_all: list[TextualMemoryItem],
|
|
435
|
+
base_prompt: str | None = None,
|
|
436
|
+
tone: str = "friendly",
|
|
437
|
+
verbosity: str = "mid",
|
|
438
|
+
) -> str:
|
|
439
|
+
"""
|
|
440
|
+
Build custom system prompt for the user with memory references.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
user_id (str): The user ID.
|
|
444
|
+
memories (list[TextualMemoryItem]): The memories to build the system prompt.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
str: The custom system prompt.
|
|
448
|
+
"""
|
|
449
|
+
# Build base prompt
|
|
450
|
+
# Add memory context if available
|
|
451
|
+
now = datetime.now()
|
|
452
|
+
formatted_date = now.strftime("%Y-%m-%d (%A)")
|
|
453
|
+
sys_body = get_memos_prompt(
|
|
454
|
+
date=formatted_date, tone=tone, verbosity=verbosity, mode="base"
|
|
455
|
+
)
|
|
456
|
+
mem_block_o, mem_block_p = _format_mem_block(memories_all)
|
|
457
|
+
mem_block = mem_block_o + "\n" + mem_block_p
|
|
458
|
+
prefix = (base_prompt.strip() + "\n\n") if base_prompt else ""
|
|
459
|
+
return (
|
|
460
|
+
prefix
|
|
461
|
+
+ sys_body
|
|
462
|
+
+ "\n\n# Memories\n## PersonalMemory & OuterMemory (ordered)\n"
|
|
463
|
+
+ mem_block
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
def _build_base_system_prompt(
|
|
467
|
+
self,
|
|
468
|
+
base_prompt: str | None = None,
|
|
469
|
+
tone: str = "friendly",
|
|
470
|
+
verbosity: str = "mid",
|
|
471
|
+
mode: str = "enhance",
|
|
472
|
+
) -> str:
|
|
473
|
+
"""
|
|
474
|
+
Build base system prompt without memory references.
|
|
475
|
+
"""
|
|
476
|
+
now = datetime.now()
|
|
477
|
+
formatted_date = now.strftime("%Y-%m-%d (%A)")
|
|
478
|
+
sys_body = get_memos_prompt(date=formatted_date, tone=tone, verbosity=verbosity, mode=mode)
|
|
479
|
+
prefix = (base_prompt.strip() + "\n\n") if base_prompt else ""
|
|
480
|
+
return prefix + sys_body
|
|
481
|
+
|
|
482
|
+
def _build_memory_context(
|
|
483
|
+
self,
|
|
484
|
+
memories_all: list[TextualMemoryItem],
|
|
485
|
+
mode: str = "enhance",
|
|
486
|
+
) -> str:
|
|
487
|
+
"""
|
|
488
|
+
Build memory context to be included in user message.
|
|
489
|
+
"""
|
|
490
|
+
if not memories_all:
|
|
491
|
+
return ""
|
|
492
|
+
|
|
493
|
+
mem_block_o, mem_block_p = _format_mem_block(memories_all)
|
|
494
|
+
|
|
495
|
+
if mode == "enhance":
|
|
496
|
+
return (
|
|
497
|
+
"# Memories\n## PersonalMemory (ordered)\n"
|
|
498
|
+
+ mem_block_p
|
|
499
|
+
+ "\n## OuterMemory (ordered)\n"
|
|
500
|
+
+ mem_block_o
|
|
501
|
+
+ "\n\n"
|
|
502
|
+
)
|
|
503
|
+
else:
|
|
504
|
+
mem_block = mem_block_o + "\n" + mem_block_p
|
|
505
|
+
return "# Memories\n## PersonalMemory & OuterMemory (ordered)\n" + mem_block + "\n\n"
|
|
506
|
+
|
|
507
|
+
def _build_enhance_system_prompt(
|
|
508
|
+
self,
|
|
509
|
+
user_id: str,
|
|
510
|
+
memories_all: list[TextualMemoryItem],
|
|
511
|
+
tone: str = "friendly",
|
|
512
|
+
verbosity: str = "mid",
|
|
513
|
+
) -> str:
|
|
514
|
+
"""
|
|
515
|
+
Build enhance prompt for the user with memory references.
|
|
516
|
+
[DEPRECATED] Use _build_base_system_prompt and _build_memory_context instead.
|
|
517
|
+
"""
|
|
518
|
+
now = datetime.now()
|
|
519
|
+
formatted_date = now.strftime("%Y-%m-%d (%A)")
|
|
520
|
+
sys_body = get_memos_prompt(
|
|
521
|
+
date=formatted_date, tone=tone, verbosity=verbosity, mode="enhance"
|
|
522
|
+
)
|
|
523
|
+
mem_block_o, mem_block_p = _format_mem_block(memories_all)
|
|
524
|
+
return (
|
|
525
|
+
sys_body
|
|
526
|
+
+ "\n\n# Memories\n## PersonalMemory (ordered)\n"
|
|
527
|
+
+ mem_block_p
|
|
528
|
+
+ "\n## OuterMemory (ordered)\n"
|
|
529
|
+
+ mem_block_o
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
def _extract_references_from_response(self, response: str) -> tuple[str, list[dict]]:
|
|
533
|
+
"""
|
|
534
|
+
Extract reference information from the response and return clean text.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
response (str): The complete response text.
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
tuple[str, list[dict]]: A tuple containing:
|
|
541
|
+
- clean_text: Text with reference markers removed
|
|
542
|
+
- references: List of reference information
|
|
543
|
+
"""
|
|
544
|
+
import re
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
references = []
|
|
548
|
+
# Pattern to match [refid:memoriesID]
|
|
549
|
+
pattern = r"\[(\d+):([^\]]+)\]"
|
|
550
|
+
|
|
551
|
+
matches = re.findall(pattern, response)
|
|
552
|
+
for ref_number, memory_id in matches:
|
|
553
|
+
references.append({"memory_id": memory_id, "reference_number": int(ref_number)})
|
|
554
|
+
|
|
555
|
+
# Remove all reference markers from the text to get clean text
|
|
556
|
+
clean_text = re.sub(pattern, "", response)
|
|
557
|
+
|
|
558
|
+
# Clean up any extra whitespace that might be left after removing markers
|
|
559
|
+
clean_text = re.sub(r"\s+", " ", clean_text).strip()
|
|
560
|
+
|
|
561
|
+
return clean_text, references
|
|
562
|
+
except Exception as e:
|
|
563
|
+
logger.error(f"Error extracting references from response: {e}", exc_info=True)
|
|
564
|
+
return response, []
|
|
565
|
+
|
|
566
|
+
def _extract_struct_data_from_history(self, chat_data: list[dict]) -> dict:
|
|
567
|
+
"""
|
|
568
|
+
get struct message from chat-history
|
|
569
|
+
# TODO: @xcy make this more general
|
|
570
|
+
"""
|
|
571
|
+
system_content = ""
|
|
572
|
+
memory_content = ""
|
|
573
|
+
chat_history = []
|
|
574
|
+
|
|
575
|
+
for item in chat_data:
|
|
576
|
+
role = item.get("role")
|
|
577
|
+
content = item.get("content", "")
|
|
578
|
+
if role == "system":
|
|
579
|
+
parts = content.split("# Memories", 1)
|
|
580
|
+
system_content = parts[0].strip()
|
|
581
|
+
if len(parts) > 1:
|
|
582
|
+
memory_content = "# Memories" + parts[1].strip()
|
|
583
|
+
elif role in ("user", "assistant"):
|
|
584
|
+
chat_history.append({"role": role, "content": content})
|
|
585
|
+
|
|
586
|
+
if chat_history and chat_history[-1]["role"] == "assistant":
|
|
587
|
+
if len(chat_history) >= 2 and chat_history[-2]["role"] == "user":
|
|
588
|
+
chat_history = chat_history[:-2]
|
|
589
|
+
else:
|
|
590
|
+
chat_history = chat_history[:-1]
|
|
591
|
+
|
|
592
|
+
return {"system": system_content, "memory": memory_content, "chat_history": chat_history}
|
|
593
|
+
|
|
594
|
+
def _chunk_response_with_tiktoken(
|
|
595
|
+
self, response: str, chunk_size: int = 5
|
|
596
|
+
) -> Generator[str, None, None]:
|
|
597
|
+
"""
|
|
598
|
+
Chunk response using tiktoken for proper token-based streaming.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
response (str): The response text to chunk.
|
|
602
|
+
chunk_size (int): Number of tokens per chunk.
|
|
603
|
+
|
|
604
|
+
Yields:
|
|
605
|
+
str: Chunked text pieces.
|
|
606
|
+
"""
|
|
607
|
+
if self.tokenizer:
|
|
608
|
+
# Use tiktoken for proper token-based chunking
|
|
609
|
+
tokens = self.tokenizer.encode(response)
|
|
610
|
+
|
|
611
|
+
for i in range(0, len(tokens), chunk_size):
|
|
612
|
+
token_chunk = tokens[i : i + chunk_size]
|
|
613
|
+
chunk_text = self.tokenizer.decode(token_chunk)
|
|
614
|
+
yield chunk_text
|
|
615
|
+
else:
|
|
616
|
+
# Fallback to character-based chunking
|
|
617
|
+
char_chunk_size = chunk_size * 4 # Approximate character to token ratio
|
|
618
|
+
for i in range(0, len(response), char_chunk_size):
|
|
619
|
+
yield response[i : i + char_chunk_size]
|
|
620
|
+
|
|
621
|
+
def _send_message_to_scheduler(
|
|
622
|
+
self,
|
|
623
|
+
user_id: str,
|
|
624
|
+
mem_cube_id: str,
|
|
625
|
+
query: str,
|
|
626
|
+
label: str,
|
|
627
|
+
):
|
|
628
|
+
"""
|
|
629
|
+
Send message to scheduler.
|
|
630
|
+
args:
|
|
631
|
+
user_id: str,
|
|
632
|
+
mem_cube_id: str,
|
|
633
|
+
query: str,
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
if self.enable_mem_scheduler and (self.mem_scheduler is not None):
|
|
637
|
+
message_item = ScheduleMessageItem(
|
|
638
|
+
user_id=user_id,
|
|
639
|
+
mem_cube_id=mem_cube_id,
|
|
640
|
+
label=label,
|
|
641
|
+
content=query,
|
|
642
|
+
timestamp=datetime.utcnow(),
|
|
643
|
+
)
|
|
644
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
645
|
+
|
|
646
|
+
async def _post_chat_processing(
|
|
647
|
+
self,
|
|
648
|
+
user_id: str,
|
|
649
|
+
cube_id: str,
|
|
650
|
+
query: str,
|
|
651
|
+
full_response: str,
|
|
652
|
+
system_prompt: str,
|
|
653
|
+
time_start: float,
|
|
654
|
+
time_end: float,
|
|
655
|
+
speed_improvement: float,
|
|
656
|
+
current_messages: list,
|
|
657
|
+
) -> None:
|
|
658
|
+
"""
|
|
659
|
+
Asynchronous processing of logs, notifications and memory additions
|
|
660
|
+
"""
|
|
661
|
+
try:
|
|
662
|
+
logger.info(
|
|
663
|
+
f"user_id: {user_id}, cube_id: {cube_id}, current_messages: {current_messages}"
|
|
664
|
+
)
|
|
665
|
+
logger.info(f"user_id: {user_id}, cube_id: {cube_id}, full_response: {full_response}")
|
|
666
|
+
|
|
667
|
+
clean_response, extracted_references = self._extract_references_from_response(
|
|
668
|
+
full_response
|
|
669
|
+
)
|
|
670
|
+
struct_message = self._extract_struct_data_from_history(current_messages)
|
|
671
|
+
logger.info(f"Extracted {len(extracted_references)} references from response")
|
|
672
|
+
|
|
673
|
+
# Send chat report notifications asynchronously
|
|
674
|
+
if self.online_bot:
|
|
675
|
+
logger.info("Online Bot Open!")
|
|
676
|
+
try:
|
|
677
|
+
from memos.memos_tools.notification_utils import (
|
|
678
|
+
send_online_bot_notification_async,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Prepare notification data
|
|
682
|
+
chat_data = {"query": query, "user_id": user_id, "cube_id": cube_id}
|
|
683
|
+
chat_data.update(
|
|
684
|
+
{
|
|
685
|
+
"memory": struct_message["memory"],
|
|
686
|
+
"chat_history": struct_message["chat_history"],
|
|
687
|
+
"full_response": full_response,
|
|
688
|
+
}
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
system_data = {
|
|
692
|
+
"references": extracted_references,
|
|
693
|
+
"time_start": time_start,
|
|
694
|
+
"time_end": time_end,
|
|
695
|
+
"speed_improvement": speed_improvement,
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
emoji_config = {"chat": "💬", "system_info": "📊"}
|
|
699
|
+
|
|
700
|
+
await send_online_bot_notification_async(
|
|
701
|
+
online_bot=self.online_bot,
|
|
702
|
+
header_name="MemOS Chat Report",
|
|
703
|
+
sub_title_name="chat_with_references",
|
|
704
|
+
title_color="#00956D",
|
|
705
|
+
other_data1=chat_data,
|
|
706
|
+
other_data2=system_data,
|
|
707
|
+
emoji=emoji_config,
|
|
708
|
+
)
|
|
709
|
+
except Exception as e:
|
|
710
|
+
logger.warning(f"Failed to send chat notification (async): {e}")
|
|
711
|
+
|
|
712
|
+
self._send_message_to_scheduler(
|
|
713
|
+
user_id=user_id, mem_cube_id=cube_id, query=clean_response, label=ANSWER_TASK_LABEL
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
self.add(
|
|
717
|
+
user_id=user_id,
|
|
718
|
+
messages=[
|
|
719
|
+
{
|
|
720
|
+
"role": "user",
|
|
721
|
+
"content": query,
|
|
722
|
+
"chat_time": str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
"role": "assistant",
|
|
726
|
+
"content": clean_response, # Store clean text without reference markers
|
|
727
|
+
"chat_time": str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
mem_cube_id=cube_id,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
logger.info(f"Post-chat processing completed for user {user_id}")
|
|
734
|
+
|
|
735
|
+
except Exception as e:
|
|
736
|
+
logger.error(f"Error in post-chat processing for user {user_id}: {e}", exc_info=True)
|
|
737
|
+
|
|
738
|
+
def _start_post_chat_processing(
|
|
739
|
+
self,
|
|
740
|
+
user_id: str,
|
|
741
|
+
cube_id: str,
|
|
742
|
+
query: str,
|
|
743
|
+
full_response: str,
|
|
744
|
+
system_prompt: str,
|
|
745
|
+
time_start: float,
|
|
746
|
+
time_end: float,
|
|
747
|
+
speed_improvement: float,
|
|
748
|
+
current_messages: list,
|
|
749
|
+
) -> None:
|
|
750
|
+
"""
|
|
751
|
+
Asynchronous processing of logs, notifications and memory additions, handle synchronous and asynchronous environments
|
|
752
|
+
"""
|
|
753
|
+
logger.info("Start post_chat_processing...")
|
|
754
|
+
|
|
755
|
+
def run_async_in_thread():
|
|
756
|
+
"""Running asynchronous tasks in a new thread"""
|
|
757
|
+
try:
|
|
758
|
+
loop = asyncio.new_event_loop()
|
|
759
|
+
asyncio.set_event_loop(loop)
|
|
760
|
+
try:
|
|
761
|
+
loop.run_until_complete(
|
|
762
|
+
self._post_chat_processing(
|
|
763
|
+
user_id=user_id,
|
|
764
|
+
cube_id=cube_id,
|
|
765
|
+
query=query,
|
|
766
|
+
full_response=full_response,
|
|
767
|
+
system_prompt=system_prompt,
|
|
768
|
+
time_start=time_start,
|
|
769
|
+
time_end=time_end,
|
|
770
|
+
speed_improvement=speed_improvement,
|
|
771
|
+
current_messages=current_messages,
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
finally:
|
|
775
|
+
loop.close()
|
|
776
|
+
except Exception as e:
|
|
777
|
+
logger.error(
|
|
778
|
+
f"Error in thread-based post-chat processing for user {user_id}: {e}",
|
|
779
|
+
exc_info=True,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
try:
|
|
783
|
+
# Try to get the current event loop
|
|
784
|
+
asyncio.get_running_loop()
|
|
785
|
+
# Create task and store reference to prevent garbage collection
|
|
786
|
+
task = asyncio.create_task(
|
|
787
|
+
self._post_chat_processing(
|
|
788
|
+
user_id=user_id,
|
|
789
|
+
cube_id=cube_id,
|
|
790
|
+
query=query,
|
|
791
|
+
full_response=full_response,
|
|
792
|
+
system_prompt=system_prompt,
|
|
793
|
+
time_start=time_start,
|
|
794
|
+
time_end=time_end,
|
|
795
|
+
speed_improvement=speed_improvement,
|
|
796
|
+
current_messages=current_messages,
|
|
797
|
+
)
|
|
798
|
+
)
|
|
799
|
+
# Add exception handling for the background task
|
|
800
|
+
task.add_done_callback(
|
|
801
|
+
lambda t: logger.error(
|
|
802
|
+
f"Error in background post-chat processing for user {user_id}: {t.exception()}",
|
|
803
|
+
exc_info=True,
|
|
804
|
+
)
|
|
805
|
+
if t.exception()
|
|
806
|
+
else None
|
|
807
|
+
)
|
|
808
|
+
except RuntimeError:
|
|
809
|
+
# No event loop, run in a new thread with context propagation
|
|
810
|
+
thread = ContextThread(
|
|
811
|
+
target=run_async_in_thread,
|
|
812
|
+
name=f"PostChatProcessing-{user_id}",
|
|
813
|
+
# Set as a daemon thread to avoid blocking program exit
|
|
814
|
+
daemon=True,
|
|
815
|
+
)
|
|
816
|
+
thread.start()
|
|
817
|
+
|
|
818
|
+
def _filter_memories_by_threshold(
|
|
819
|
+
self,
|
|
820
|
+
memories: list[TextualMemoryItem],
|
|
821
|
+
threshold: float = 0.30,
|
|
822
|
+
min_num: int = 3,
|
|
823
|
+
memory_type: Literal["OuterMemory"] = "OuterMemory",
|
|
824
|
+
) -> list[TextualMemoryItem]:
|
|
825
|
+
"""
|
|
826
|
+
Filter memories by threshold and type, at least min_num memories for Non-OuterMemory.
|
|
827
|
+
Args:
|
|
828
|
+
memories: list[TextualMemoryItem],
|
|
829
|
+
threshold: float,
|
|
830
|
+
min_num: int,
|
|
831
|
+
memory_type: Literal["OuterMemory"],
|
|
832
|
+
Returns:
|
|
833
|
+
list[TextualMemoryItem]
|
|
834
|
+
"""
|
|
835
|
+
sorted_memories = sorted(memories, key=lambda m: m.metadata.relativity, reverse=True)
|
|
836
|
+
filtered_person = [m for m in memories if m.metadata.memory_type != memory_type]
|
|
837
|
+
filtered_outer = [m for m in memories if m.metadata.memory_type == memory_type]
|
|
838
|
+
filtered = []
|
|
839
|
+
per_memory_count = 0
|
|
840
|
+
for m in sorted_memories:
|
|
841
|
+
if m.metadata.relativity >= threshold:
|
|
842
|
+
if m.metadata.memory_type != memory_type:
|
|
843
|
+
per_memory_count += 1
|
|
844
|
+
filtered.append(m)
|
|
845
|
+
if len(filtered) < min_num:
|
|
846
|
+
filtered = filtered_person[:min_num] + filtered_outer[:min_num]
|
|
847
|
+
else:
|
|
848
|
+
if per_memory_count < min_num:
|
|
849
|
+
filtered += filtered_person[per_memory_count:min_num]
|
|
850
|
+
filtered_memory = sorted(filtered, key=lambda m: m.metadata.relativity, reverse=True)
|
|
851
|
+
return filtered_memory
|
|
852
|
+
|
|
853
|
+
def register_mem_cube(
|
|
854
|
+
self,
|
|
855
|
+
mem_cube_name_or_path_or_object: str | GeneralMemCube,
|
|
856
|
+
mem_cube_id: str | None = None,
|
|
857
|
+
user_id: str | None = None,
|
|
858
|
+
memory_types: list[Literal["text_mem", "act_mem", "para_mem"]] | None = None,
|
|
859
|
+
default_config: GeneralMemCubeConfig | None = None,
|
|
860
|
+
) -> None:
|
|
861
|
+
"""
|
|
862
|
+
Register a MemCube with the MOS.
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
mem_cube_name_or_path_or_object (str | GeneralMemCube): The name, path, or GeneralMemCube object to register.
|
|
866
|
+
mem_cube_id (str, optional): The identifier for the MemCube. If not provided, a default ID is used.
|
|
867
|
+
user_id (str, optional): The user ID to register the cube for.
|
|
868
|
+
memory_types (list[str], optional): List of memory types to load.
|
|
869
|
+
If None, loads all available memory types.
|
|
870
|
+
Options: ["text_mem", "act_mem", "para_mem"]
|
|
871
|
+
default_config (GeneralMemCubeConfig, optional): Default configuration for the cube.
|
|
872
|
+
"""
|
|
873
|
+
# Handle different input types
|
|
874
|
+
if isinstance(mem_cube_name_or_path_or_object, GeneralMemCube):
|
|
875
|
+
# Direct GeneralMemCube object provided
|
|
876
|
+
mem_cube = mem_cube_name_or_path_or_object
|
|
877
|
+
if mem_cube_id is None:
|
|
878
|
+
mem_cube_id = f"cube_{id(mem_cube)}" # Generate a unique ID
|
|
879
|
+
else:
|
|
880
|
+
# String path provided
|
|
881
|
+
mem_cube_name_or_path = mem_cube_name_or_path_or_object
|
|
882
|
+
if mem_cube_id is None:
|
|
883
|
+
mem_cube_id = mem_cube_name_or_path
|
|
884
|
+
|
|
885
|
+
if mem_cube_id in self.mem_cubes:
|
|
886
|
+
logger.info(f"MemCube with ID {mem_cube_id} already in MOS, skip install.")
|
|
887
|
+
return
|
|
888
|
+
|
|
889
|
+
# Create MemCube from path
|
|
890
|
+
time_start = time.time()
|
|
891
|
+
if os.path.exists(mem_cube_name_or_path):
|
|
892
|
+
mem_cube = GeneralMemCube.init_from_dir(
|
|
893
|
+
mem_cube_name_or_path, memory_types, default_config
|
|
894
|
+
)
|
|
895
|
+
logger.info(
|
|
896
|
+
f"time register_mem_cube: init_from_dir time is: {time.time() - time_start}"
|
|
897
|
+
)
|
|
898
|
+
else:
|
|
899
|
+
logger.warning(
|
|
900
|
+
f"MemCube {mem_cube_name_or_path} does not exist, try to init from remote repo."
|
|
901
|
+
)
|
|
902
|
+
mem_cube = GeneralMemCube.init_from_remote_repo(
|
|
903
|
+
mem_cube_name_or_path, memory_types=memory_types, default_config=default_config
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Register the MemCube
|
|
907
|
+
logger.info(
|
|
908
|
+
f"Registering MemCube {mem_cube_id} with cube config {mem_cube.config.model_dump(mode='json')}"
|
|
909
|
+
)
|
|
910
|
+
time_start = time.time()
|
|
911
|
+
self.mem_cubes[mem_cube_id] = mem_cube
|
|
912
|
+
time_end = time.time()
|
|
913
|
+
logger.info(f"time register_mem_cube: add mem_cube time is: {time_end - time_start}")
|
|
914
|
+
|
|
915
|
+
def user_register(
|
|
916
|
+
self,
|
|
917
|
+
user_id: str,
|
|
918
|
+
user_name: str | None = None,
|
|
919
|
+
config: MOSConfig | None = None,
|
|
920
|
+
interests: str | None = None,
|
|
921
|
+
default_mem_cube: GeneralMemCube | None = None,
|
|
922
|
+
default_cube_config: GeneralMemCubeConfig | None = None,
|
|
923
|
+
mem_cube_id: str | None = None,
|
|
924
|
+
) -> dict[str, str]:
|
|
925
|
+
"""Register a new user with configuration and default cube.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
user_id (str): The user ID for registration.
|
|
929
|
+
user_name (str): The user name for registration.
|
|
930
|
+
config (MOSConfig | None, optional): User-specific configuration. Defaults to None.
|
|
931
|
+
interests (str | None, optional): User interests as string. Defaults to None.
|
|
932
|
+
default_mem_cube (GeneralMemCube | None, optional): Default memory cube. Defaults to None.
|
|
933
|
+
default_cube_config (GeneralMemCubeConfig | None, optional): Default cube configuration. Defaults to None.
|
|
934
|
+
|
|
935
|
+
Returns:
|
|
936
|
+
dict[str, str]: Registration result with status and message.
|
|
937
|
+
"""
|
|
938
|
+
try:
|
|
939
|
+
# Use provided config or default config
|
|
940
|
+
user_config = config or self.default_config
|
|
941
|
+
if not user_config:
|
|
942
|
+
return {
|
|
943
|
+
"status": "error",
|
|
944
|
+
"message": "No configuration provided for user registration",
|
|
945
|
+
}
|
|
946
|
+
if not user_name:
|
|
947
|
+
user_name = user_id
|
|
948
|
+
|
|
949
|
+
# Create user with configuration using persistent user manager
|
|
950
|
+
self.user_manager.create_user_with_config(user_id, user_config, UserRole.USER, user_id)
|
|
951
|
+
|
|
952
|
+
# Create user configuration
|
|
953
|
+
user_config = self._create_user_config(user_id, user_config)
|
|
954
|
+
|
|
955
|
+
# Create a default cube for the user using MOSCore's methods
|
|
956
|
+
default_cube_name = f"{user_name}_{user_id}_default_cube"
|
|
957
|
+
mem_cube_name_or_path = os.path.join(CUBE_PATH, default_cube_name)
|
|
958
|
+
default_cube_id = self.create_cube_for_user(
|
|
959
|
+
cube_name=default_cube_name,
|
|
960
|
+
owner_id=user_id,
|
|
961
|
+
cube_path=mem_cube_name_or_path,
|
|
962
|
+
cube_id=mem_cube_id,
|
|
963
|
+
)
|
|
964
|
+
time_start = time.time()
|
|
965
|
+
if default_mem_cube:
|
|
966
|
+
try:
|
|
967
|
+
default_mem_cube.dump(mem_cube_name_or_path, memory_types=[])
|
|
968
|
+
except Exception as e:
|
|
969
|
+
logger.error(f"Failed to dump default cube: {e}")
|
|
970
|
+
time_end = time.time()
|
|
971
|
+
logger.info(f"time user_register: dump default cube time is: {time_end - time_start}")
|
|
972
|
+
# Register the default cube with MOS
|
|
973
|
+
self.register_mem_cube(
|
|
974
|
+
mem_cube_name_or_path_or_object=default_mem_cube,
|
|
975
|
+
mem_cube_id=default_cube_id,
|
|
976
|
+
user_id=user_id,
|
|
977
|
+
memory_types=["act_mem"] if self.config.enable_activation_memory else [],
|
|
978
|
+
default_config=default_cube_config, # use default cube config
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
# Add interests to the default cube if provided
|
|
982
|
+
if interests:
|
|
983
|
+
self.add(memory_content=interests, mem_cube_id=default_cube_id, user_id=user_id)
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
"status": "success",
|
|
987
|
+
"message": f"User {user_name} registered successfully with default cube {default_cube_id}",
|
|
988
|
+
"user_id": user_id,
|
|
989
|
+
"default_cube_id": default_cube_id,
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
except Exception as e:
|
|
993
|
+
return {"status": "error", "message": f"Failed to register user: {e!s}"}
|
|
994
|
+
|
|
995
|
+
def _get_further_suggestion(self, message: MessageList | None = None) -> list[str]:
|
|
996
|
+
"""Get further suggestion prompt."""
|
|
997
|
+
try:
|
|
998
|
+
dialogue_info = "\n".join([f"{msg['role']}: {msg['content']}" for msg in message[-2:]])
|
|
999
|
+
further_suggestion_prompt = FURTHER_SUGGESTION_PROMPT.format(dialogue=dialogue_info)
|
|
1000
|
+
message_list = [{"role": "system", "content": further_suggestion_prompt}]
|
|
1001
|
+
response = self.chat_llm.generate(message_list)
|
|
1002
|
+
clean_response = clean_json_response(response)
|
|
1003
|
+
response_json = json.loads(clean_response)
|
|
1004
|
+
return response_json["query"]
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
logger.error(f"Error getting further suggestion: {e}", exc_info=True)
|
|
1007
|
+
return []
|
|
1008
|
+
|
|
1009
|
+
def get_suggestion_query(
|
|
1010
|
+
self, user_id: str, language: str = "zh", message: MessageList | None = None
|
|
1011
|
+
) -> list[str]:
|
|
1012
|
+
"""Get suggestion query from LLM.
|
|
1013
|
+
Args:
|
|
1014
|
+
user_id (str): User ID.
|
|
1015
|
+
language (str): Language for suggestions ("zh" or "en").
|
|
1016
|
+
|
|
1017
|
+
Returns:
|
|
1018
|
+
list[str]: The suggestion query list.
|
|
1019
|
+
"""
|
|
1020
|
+
if message:
|
|
1021
|
+
further_suggestion = self._get_further_suggestion(message)
|
|
1022
|
+
return further_suggestion
|
|
1023
|
+
if language == "zh":
|
|
1024
|
+
suggestion_prompt = SUGGESTION_QUERY_PROMPT_ZH
|
|
1025
|
+
else: # English
|
|
1026
|
+
suggestion_prompt = SUGGESTION_QUERY_PROMPT_EN
|
|
1027
|
+
text_mem_result = super().search("my recently memories", user_id=user_id, top_k=3)[
|
|
1028
|
+
"text_mem"
|
|
1029
|
+
]
|
|
1030
|
+
if text_mem_result:
|
|
1031
|
+
memories = "\n".join([m.memory[:200] for m in text_mem_result[0]["memories"]])
|
|
1032
|
+
else:
|
|
1033
|
+
memories = ""
|
|
1034
|
+
message_list = [{"role": "system", "content": suggestion_prompt.format(memories=memories)}]
|
|
1035
|
+
response = self.chat_llm.generate(message_list)
|
|
1036
|
+
clean_response = clean_json_response(response)
|
|
1037
|
+
response_json = json.loads(clean_response)
|
|
1038
|
+
return response_json["query"]
|
|
1039
|
+
|
|
1040
|
+
def chat(
|
|
1041
|
+
self,
|
|
1042
|
+
query: str,
|
|
1043
|
+
user_id: str,
|
|
1044
|
+
cube_id: str | None = None,
|
|
1045
|
+
history: MessageList | None = None,
|
|
1046
|
+
base_prompt: str | None = None,
|
|
1047
|
+
internet_search: bool = False,
|
|
1048
|
+
moscube: bool = False,
|
|
1049
|
+
top_k: int = 10,
|
|
1050
|
+
threshold: float = 0.5,
|
|
1051
|
+
session_id: str | None = None,
|
|
1052
|
+
) -> str:
|
|
1053
|
+
"""
|
|
1054
|
+
Chat with LLM with memory references and complete response.
|
|
1055
|
+
"""
|
|
1056
|
+
self._load_user_cubes(user_id, self.default_cube_config)
|
|
1057
|
+
time_start = time.time()
|
|
1058
|
+
memories_result = super().search(
|
|
1059
|
+
query,
|
|
1060
|
+
user_id,
|
|
1061
|
+
install_cube_ids=[cube_id] if cube_id else None,
|
|
1062
|
+
top_k=top_k,
|
|
1063
|
+
mode="fine",
|
|
1064
|
+
internet_search=internet_search,
|
|
1065
|
+
moscube=moscube,
|
|
1066
|
+
session_id=session_id,
|
|
1067
|
+
)["text_mem"]
|
|
1068
|
+
|
|
1069
|
+
memories_list = []
|
|
1070
|
+
if memories_result:
|
|
1071
|
+
memories_list = memories_result[0]["memories"]
|
|
1072
|
+
memories_list = self._filter_memories_by_threshold(memories_list, threshold)
|
|
1073
|
+
new_memories_list = []
|
|
1074
|
+
for m in memories_list:
|
|
1075
|
+
m.metadata.embedding = []
|
|
1076
|
+
new_memories_list.append(m)
|
|
1077
|
+
memories_list = new_memories_list
|
|
1078
|
+
|
|
1079
|
+
system_prompt = super()._build_system_prompt(memories_list, base_prompt)
|
|
1080
|
+
if history is not None:
|
|
1081
|
+
# Use the provided history (even if it's empty)
|
|
1082
|
+
history_info = history[-20:]
|
|
1083
|
+
else:
|
|
1084
|
+
# Fall back to internal chat_history
|
|
1085
|
+
if user_id not in self.chat_history_manager:
|
|
1086
|
+
self._register_chat_history(user_id, session_id)
|
|
1087
|
+
history_info = self.chat_history_manager[user_id].chat_history[-20:]
|
|
1088
|
+
current_messages = [
|
|
1089
|
+
{"role": "system", "content": system_prompt},
|
|
1090
|
+
*history_info,
|
|
1091
|
+
{"role": "user", "content": query},
|
|
1092
|
+
]
|
|
1093
|
+
logger.info("Start to get final answer...")
|
|
1094
|
+
response = self.chat_llm.generate(current_messages)
|
|
1095
|
+
time_end = time.time()
|
|
1096
|
+
self._start_post_chat_processing(
|
|
1097
|
+
user_id=user_id,
|
|
1098
|
+
cube_id=cube_id,
|
|
1099
|
+
query=query,
|
|
1100
|
+
full_response=response,
|
|
1101
|
+
system_prompt=system_prompt,
|
|
1102
|
+
time_start=time_start,
|
|
1103
|
+
time_end=time_end,
|
|
1104
|
+
speed_improvement=0.0,
|
|
1105
|
+
current_messages=current_messages,
|
|
1106
|
+
)
|
|
1107
|
+
return response, memories_list
|
|
1108
|
+
|
|
1109
|
+
def chat_with_references(
|
|
1110
|
+
self,
|
|
1111
|
+
query: str,
|
|
1112
|
+
user_id: str,
|
|
1113
|
+
cube_id: str | None = None,
|
|
1114
|
+
history: MessageList | None = None,
|
|
1115
|
+
top_k: int = 20,
|
|
1116
|
+
internet_search: bool = False,
|
|
1117
|
+
moscube: bool = False,
|
|
1118
|
+
session_id: str | None = None,
|
|
1119
|
+
) -> Generator[str, None, None]:
|
|
1120
|
+
"""
|
|
1121
|
+
Chat with LLM with memory references and streaming output.
|
|
1122
|
+
|
|
1123
|
+
Args:
|
|
1124
|
+
query (str): Query string.
|
|
1125
|
+
user_id (str): User ID.
|
|
1126
|
+
cube_id (str, optional): Custom cube ID for user.
|
|
1127
|
+
history (MessageList, optional): Chat history.
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
Generator[str, None, None]: The response string generator with reference processing.
|
|
1131
|
+
"""
|
|
1132
|
+
|
|
1133
|
+
self._load_user_cubes(user_id, self.default_cube_config)
|
|
1134
|
+
time_start = time.time()
|
|
1135
|
+
memories_list = []
|
|
1136
|
+
yield f"data: {json.dumps({'type': 'status', 'data': '0'})}\n\n"
|
|
1137
|
+
memories_result = super().search(
|
|
1138
|
+
query,
|
|
1139
|
+
user_id,
|
|
1140
|
+
install_cube_ids=[cube_id] if cube_id else None,
|
|
1141
|
+
top_k=top_k,
|
|
1142
|
+
mode="fine",
|
|
1143
|
+
internet_search=internet_search,
|
|
1144
|
+
moscube=moscube,
|
|
1145
|
+
session_id=session_id,
|
|
1146
|
+
)["text_mem"]
|
|
1147
|
+
|
|
1148
|
+
yield f"data: {json.dumps({'type': 'status', 'data': '1'})}\n\n"
|
|
1149
|
+
search_time_end = time.time()
|
|
1150
|
+
logger.info(
|
|
1151
|
+
f"time chat: search text_mem time user_id: {user_id} time is: {search_time_end - time_start}"
|
|
1152
|
+
)
|
|
1153
|
+
self._send_message_to_scheduler(
|
|
1154
|
+
user_id=user_id, mem_cube_id=cube_id, query=query, label=QUERY_TASK_LABEL
|
|
1155
|
+
)
|
|
1156
|
+
if memories_result:
|
|
1157
|
+
memories_list = memories_result[0]["memories"]
|
|
1158
|
+
memories_list = self._filter_memories_by_threshold(memories_list)
|
|
1159
|
+
|
|
1160
|
+
reference = prepare_reference_data(memories_list)
|
|
1161
|
+
yield f"data: {json.dumps({'type': 'reference', 'data': reference})}\n\n"
|
|
1162
|
+
# Build custom system prompt with relevant memories)
|
|
1163
|
+
system_prompt = self._build_enhance_system_prompt(user_id, memories_list)
|
|
1164
|
+
# Get chat history
|
|
1165
|
+
if user_id not in self.chat_history_manager:
|
|
1166
|
+
self._register_chat_history(user_id, session_id)
|
|
1167
|
+
|
|
1168
|
+
chat_history = self.chat_history_manager[user_id]
|
|
1169
|
+
if history is not None:
|
|
1170
|
+
chat_history.chat_history = history[-20:]
|
|
1171
|
+
current_messages = [
|
|
1172
|
+
{"role": "system", "content": system_prompt},
|
|
1173
|
+
*chat_history.chat_history,
|
|
1174
|
+
{"role": "user", "content": query},
|
|
1175
|
+
]
|
|
1176
|
+
logger.info(
|
|
1177
|
+
f"user_id: {user_id}, cube_id: {cube_id}, current_system_prompt: {system_prompt}"
|
|
1178
|
+
)
|
|
1179
|
+
yield f"data: {json.dumps({'type': 'status', 'data': '2'})}\n\n"
|
|
1180
|
+
# Generate response with custom prompt
|
|
1181
|
+
past_key_values = None
|
|
1182
|
+
response_stream = None
|
|
1183
|
+
if self.config.enable_activation_memory:
|
|
1184
|
+
# Handle activation memory (copy MOSCore logic)
|
|
1185
|
+
for mem_cube_id, mem_cube in self.mem_cubes.items():
|
|
1186
|
+
if mem_cube.act_mem and mem_cube_id == cube_id:
|
|
1187
|
+
kv_cache = next(iter(mem_cube.act_mem.get_all()), None)
|
|
1188
|
+
past_key_values = (
|
|
1189
|
+
kv_cache.memory if (kv_cache and hasattr(kv_cache, "memory")) else None
|
|
1190
|
+
)
|
|
1191
|
+
if past_key_values is not None:
|
|
1192
|
+
logger.info("past_key_values is not None will apply to chat")
|
|
1193
|
+
else:
|
|
1194
|
+
logger.info("past_key_values is None will not apply to chat")
|
|
1195
|
+
break
|
|
1196
|
+
if self.config.chat_model.backend == "huggingface":
|
|
1197
|
+
response_stream = self.chat_llm.generate_stream(
|
|
1198
|
+
current_messages, past_key_values=past_key_values
|
|
1199
|
+
)
|
|
1200
|
+
elif self.config.chat_model.backend == "vllm":
|
|
1201
|
+
response_stream = self.chat_llm.generate_stream(current_messages)
|
|
1202
|
+
else:
|
|
1203
|
+
if self.config.chat_model.backend in ["huggingface", "vllm", "openai"]:
|
|
1204
|
+
response_stream = self.chat_llm.generate_stream(current_messages)
|
|
1205
|
+
else:
|
|
1206
|
+
response_stream = self.chat_llm.generate(current_messages)
|
|
1207
|
+
|
|
1208
|
+
time_end = time.time()
|
|
1209
|
+
chat_time_end = time.time()
|
|
1210
|
+
logger.info(
|
|
1211
|
+
f"time chat: chat time user_id: {user_id} time is: {chat_time_end - search_time_end}"
|
|
1212
|
+
)
|
|
1213
|
+
# Simulate streaming output with proper reference handling using tiktoken
|
|
1214
|
+
|
|
1215
|
+
# Initialize buffer for streaming
|
|
1216
|
+
buffer = ""
|
|
1217
|
+
full_response = ""
|
|
1218
|
+
token_count = 0
|
|
1219
|
+
# Use tiktoken for proper token-based chunking
|
|
1220
|
+
if self.config.chat_model.backend not in ["huggingface", "vllm", "openai"]:
|
|
1221
|
+
# For non-huggingface backends, we need to collect the full response first
|
|
1222
|
+
full_response_text = ""
|
|
1223
|
+
for chunk in response_stream:
|
|
1224
|
+
if chunk in ["<think>", "</think>"]:
|
|
1225
|
+
continue
|
|
1226
|
+
full_response_text += chunk
|
|
1227
|
+
response_stream = self._chunk_response_with_tiktoken(full_response_text, chunk_size=5)
|
|
1228
|
+
for chunk in response_stream:
|
|
1229
|
+
if chunk in ["<think>", "</think>"]:
|
|
1230
|
+
continue
|
|
1231
|
+
token_count += 1
|
|
1232
|
+
buffer += chunk
|
|
1233
|
+
full_response += chunk
|
|
1234
|
+
|
|
1235
|
+
# Process buffer to ensure complete reference tags
|
|
1236
|
+
processed_chunk, remaining_buffer = process_streaming_references_complete(buffer)
|
|
1237
|
+
|
|
1238
|
+
if processed_chunk:
|
|
1239
|
+
chunk_data = f"data: {json.dumps({'type': 'text', 'data': processed_chunk}, ensure_ascii=False)}\n\n"
|
|
1240
|
+
yield chunk_data
|
|
1241
|
+
buffer = remaining_buffer
|
|
1242
|
+
|
|
1243
|
+
# Process any remaining buffer
|
|
1244
|
+
if buffer:
|
|
1245
|
+
processed_chunk, remaining_buffer = process_streaming_references_complete(buffer)
|
|
1246
|
+
if processed_chunk:
|
|
1247
|
+
chunk_data = f"data: {json.dumps({'type': 'text', 'data': processed_chunk}, ensure_ascii=False)}\n\n"
|
|
1248
|
+
yield chunk_data
|
|
1249
|
+
|
|
1250
|
+
# set kvcache improve speed
|
|
1251
|
+
speed_improvement = round(float((len(system_prompt) / 2) * 0.0048 + 44.5), 1)
|
|
1252
|
+
total_time = round(float(time_end - time_start), 1)
|
|
1253
|
+
|
|
1254
|
+
yield f"data: {json.dumps({'type': 'time', 'data': {'total_time': total_time, 'speed_improvement': f'{speed_improvement}%'}})}\n\n"
|
|
1255
|
+
# get further suggestion
|
|
1256
|
+
current_messages.append({"role": "assistant", "content": full_response})
|
|
1257
|
+
further_suggestion = self._get_further_suggestion(current_messages)
|
|
1258
|
+
logger.info(f"further_suggestion: {further_suggestion}")
|
|
1259
|
+
yield f"data: {json.dumps({'type': 'suggestion', 'data': further_suggestion})}\n\n"
|
|
1260
|
+
yield f"data: {json.dumps({'type': 'end'})}\n\n"
|
|
1261
|
+
|
|
1262
|
+
# Asynchronous processing of logs, notifications and memory additions
|
|
1263
|
+
self._start_post_chat_processing(
|
|
1264
|
+
user_id=user_id,
|
|
1265
|
+
cube_id=cube_id,
|
|
1266
|
+
query=query,
|
|
1267
|
+
full_response=full_response,
|
|
1268
|
+
system_prompt=system_prompt,
|
|
1269
|
+
time_start=time_start,
|
|
1270
|
+
time_end=time_end,
|
|
1271
|
+
speed_improvement=speed_improvement,
|
|
1272
|
+
current_messages=current_messages,
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
def get_all(
|
|
1276
|
+
self,
|
|
1277
|
+
user_id: str,
|
|
1278
|
+
memory_type: Literal["text_mem", "act_mem", "param_mem", "para_mem"],
|
|
1279
|
+
mem_cube_ids: list[str] | None = None,
|
|
1280
|
+
) -> list[dict[str, Any]]:
|
|
1281
|
+
"""Get all memory items for a user.
|
|
1282
|
+
|
|
1283
|
+
Args:
|
|
1284
|
+
user_id (str): The ID of the user.
|
|
1285
|
+
cube_id (str | None, optional): The ID of the cube. Defaults to None.
|
|
1286
|
+
memory_type (Literal["text_mem", "act_mem", "param_mem"]): The type of memory to get.
|
|
1287
|
+
|
|
1288
|
+
Returns:
|
|
1289
|
+
list[dict[str, Any]]: A list of memory items with cube_id and memories structure.
|
|
1290
|
+
"""
|
|
1291
|
+
|
|
1292
|
+
# Load user cubes if not already loaded
|
|
1293
|
+
self._load_user_cubes(user_id, self.default_cube_config)
|
|
1294
|
+
time_start = time.time()
|
|
1295
|
+
memory_list = super().get_all(
|
|
1296
|
+
mem_cube_id=mem_cube_ids[0] if mem_cube_ids else None, user_id=user_id
|
|
1297
|
+
)[memory_type]
|
|
1298
|
+
get_all_time_end = time.time()
|
|
1299
|
+
logger.info(
|
|
1300
|
+
f"time get_all: get_all time user_id: {user_id} time is: {get_all_time_end - time_start}"
|
|
1301
|
+
)
|
|
1302
|
+
reformat_memory_list = []
|
|
1303
|
+
if memory_type == "text_mem":
|
|
1304
|
+
for memory in memory_list:
|
|
1305
|
+
memories = remove_embedding_recursive(memory["memories"])
|
|
1306
|
+
custom_type_ratios = {
|
|
1307
|
+
"WorkingMemory": 0.20,
|
|
1308
|
+
"LongTermMemory": 0.40,
|
|
1309
|
+
"UserMemory": 0.40,
|
|
1310
|
+
}
|
|
1311
|
+
tree_result, node_type_count = convert_graph_to_tree_forworkmem(
|
|
1312
|
+
memories, target_node_count=200, type_ratios=custom_type_ratios
|
|
1313
|
+
)
|
|
1314
|
+
# Ensure all node IDs are unique in the tree structure
|
|
1315
|
+
tree_result = ensure_unique_tree_ids(tree_result)
|
|
1316
|
+
memories_filtered = filter_nodes_by_tree_ids(tree_result, memories)
|
|
1317
|
+
children = tree_result["children"]
|
|
1318
|
+
children_sort = sort_children_by_memory_type(children)
|
|
1319
|
+
tree_result["children"] = children_sort
|
|
1320
|
+
memories_filtered["tree_structure"] = tree_result
|
|
1321
|
+
reformat_memory_list.append(
|
|
1322
|
+
{
|
|
1323
|
+
"cube_id": memory["cube_id"],
|
|
1324
|
+
"memories": [memories_filtered],
|
|
1325
|
+
"memory_statistics": node_type_count,
|
|
1326
|
+
}
|
|
1327
|
+
)
|
|
1328
|
+
elif memory_type == "act_mem":
|
|
1329
|
+
memories_list = []
|
|
1330
|
+
act_mem_params = self.mem_cubes[mem_cube_ids[0]].act_mem.get_all()
|
|
1331
|
+
if act_mem_params:
|
|
1332
|
+
memories_data = act_mem_params[0].model_dump()
|
|
1333
|
+
records = memories_data.get("records", [])
|
|
1334
|
+
for record in records["text_memories"]:
|
|
1335
|
+
memories_list.append(
|
|
1336
|
+
{
|
|
1337
|
+
"id": memories_data["id"],
|
|
1338
|
+
"text": record,
|
|
1339
|
+
"create_time": records["timestamp"],
|
|
1340
|
+
"size": random.randint(1, 20),
|
|
1341
|
+
"modify_times": 1,
|
|
1342
|
+
}
|
|
1343
|
+
)
|
|
1344
|
+
reformat_memory_list.append(
|
|
1345
|
+
{
|
|
1346
|
+
"cube_id": "xxxxxxxxxxxxxxxx" if not mem_cube_ids else mem_cube_ids[0],
|
|
1347
|
+
"memories": memories_list,
|
|
1348
|
+
}
|
|
1349
|
+
)
|
|
1350
|
+
elif memory_type == "para_mem":
|
|
1351
|
+
act_mem_params = self.mem_cubes[mem_cube_ids[0]].act_mem.get_all()
|
|
1352
|
+
logger.info(f"act_mem_params: {act_mem_params}")
|
|
1353
|
+
reformat_memory_list.append(
|
|
1354
|
+
{
|
|
1355
|
+
"cube_id": "xxxxxxxxxxxxxxxx" if not mem_cube_ids else mem_cube_ids[0],
|
|
1356
|
+
"memories": act_mem_params[0].model_dump(),
|
|
1357
|
+
}
|
|
1358
|
+
)
|
|
1359
|
+
make_format_time_end = time.time()
|
|
1360
|
+
logger.info(
|
|
1361
|
+
f"time get_all: make_format time user_id: {user_id} time is: {make_format_time_end - get_all_time_end}"
|
|
1362
|
+
)
|
|
1363
|
+
return reformat_memory_list
|
|
1364
|
+
|
|
1365
|
+
def _get_subgraph(
|
|
1366
|
+
self, query: str, mem_cube_id: str, user_id: str | None = None, top_k: int = 5
|
|
1367
|
+
) -> list[dict[str, Any]]:
|
|
1368
|
+
result = {"para_mem": [], "act_mem": [], "text_mem": []}
|
|
1369
|
+
if self.config.enable_textual_memory and self.mem_cubes[mem_cube_id].text_mem:
|
|
1370
|
+
result["text_mem"].append(
|
|
1371
|
+
{
|
|
1372
|
+
"cube_id": mem_cube_id,
|
|
1373
|
+
"memories": self.mem_cubes[mem_cube_id].text_mem.get_relevant_subgraph(
|
|
1374
|
+
query, top_k=top_k
|
|
1375
|
+
),
|
|
1376
|
+
}
|
|
1377
|
+
)
|
|
1378
|
+
return result
|
|
1379
|
+
|
|
1380
|
+
def get_subgraph(
|
|
1381
|
+
self,
|
|
1382
|
+
user_id: str,
|
|
1383
|
+
query: str,
|
|
1384
|
+
mem_cube_ids: list[str] | None = None,
|
|
1385
|
+
top_k: int = 20,
|
|
1386
|
+
) -> list[dict[str, Any]]:
|
|
1387
|
+
"""Get all memory items for a user.
|
|
1388
|
+
|
|
1389
|
+
Args:
|
|
1390
|
+
user_id (str): The ID of the user.
|
|
1391
|
+
cube_id (str | None, optional): The ID of the cube. Defaults to None.
|
|
1392
|
+
mem_cube_ids (list[str], optional): The IDs of the cubes. Defaults to None.
|
|
1393
|
+
|
|
1394
|
+
Returns:
|
|
1395
|
+
list[dict[str, Any]]: A list of memory items with cube_id and memories structure.
|
|
1396
|
+
"""
|
|
1397
|
+
|
|
1398
|
+
# Load user cubes if not already loaded
|
|
1399
|
+
self._load_user_cubes(user_id, self.default_cube_config)
|
|
1400
|
+
memory_list = self._get_subgraph(
|
|
1401
|
+
query=query, mem_cube_id=mem_cube_ids[0], user_id=user_id, top_k=top_k
|
|
1402
|
+
)["text_mem"]
|
|
1403
|
+
reformat_memory_list = []
|
|
1404
|
+
for memory in memory_list:
|
|
1405
|
+
memories = remove_embedding_recursive(memory["memories"])
|
|
1406
|
+
custom_type_ratios = {"WorkingMemory": 0.20, "LongTermMemory": 0.40, "UserMemory": 0.4}
|
|
1407
|
+
tree_result, node_type_count = convert_graph_to_tree_forworkmem(
|
|
1408
|
+
memories, target_node_count=150, type_ratios=custom_type_ratios
|
|
1409
|
+
)
|
|
1410
|
+
# Ensure all node IDs are unique in the tree structure
|
|
1411
|
+
tree_result = ensure_unique_tree_ids(tree_result)
|
|
1412
|
+
memories_filtered = filter_nodes_by_tree_ids(tree_result, memories)
|
|
1413
|
+
children = tree_result["children"]
|
|
1414
|
+
children_sort = sort_children_by_memory_type(children)
|
|
1415
|
+
tree_result["children"] = children_sort
|
|
1416
|
+
memories_filtered["tree_structure"] = tree_result
|
|
1417
|
+
reformat_memory_list.append(
|
|
1418
|
+
{
|
|
1419
|
+
"cube_id": memory["cube_id"],
|
|
1420
|
+
"memories": [memories_filtered],
|
|
1421
|
+
"memory_statistics": node_type_count,
|
|
1422
|
+
}
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
return reformat_memory_list
|
|
1426
|
+
|
|
1427
|
+
def search(
|
|
1428
|
+
self,
|
|
1429
|
+
query: str,
|
|
1430
|
+
user_id: str,
|
|
1431
|
+
install_cube_ids: list[str] | None = None,
|
|
1432
|
+
top_k: int = 10,
|
|
1433
|
+
mode: Literal["fast", "fine"] = "fast",
|
|
1434
|
+
session_id: str | None = None,
|
|
1435
|
+
):
|
|
1436
|
+
"""Search memories for a specific user."""
|
|
1437
|
+
|
|
1438
|
+
# Load user cubes if not already loaded
|
|
1439
|
+
time_start = time.time()
|
|
1440
|
+
self._load_user_cubes(user_id, self.default_cube_config)
|
|
1441
|
+
load_user_cubes_time_end = time.time()
|
|
1442
|
+
logger.info(
|
|
1443
|
+
f"time search: load_user_cubes time user_id: {user_id} time is: {load_user_cubes_time_end - time_start}"
|
|
1444
|
+
)
|
|
1445
|
+
search_result = super().search(
|
|
1446
|
+
query, user_id, install_cube_ids, top_k, mode=mode, session_id=session_id
|
|
1447
|
+
)
|
|
1448
|
+
search_time_end = time.time()
|
|
1449
|
+
logger.info(
|
|
1450
|
+
f"time search: search text_mem time user_id: {user_id} time is: {search_time_end - load_user_cubes_time_end}"
|
|
1451
|
+
)
|
|
1452
|
+
text_memory_list = search_result["text_mem"]
|
|
1453
|
+
reformat_memory_list = []
|
|
1454
|
+
for memory in text_memory_list:
|
|
1455
|
+
memories_list = []
|
|
1456
|
+
for data in memory["memories"]:
|
|
1457
|
+
memories = data.model_dump()
|
|
1458
|
+
memories["ref_id"] = f"[{memories['id'].split('-')[0]}]"
|
|
1459
|
+
memories["metadata"]["embedding"] = []
|
|
1460
|
+
memories["metadata"]["sources"] = []
|
|
1461
|
+
memories["metadata"]["ref_id"] = f"[{memories['id'].split('-')[0]}]"
|
|
1462
|
+
memories["metadata"]["id"] = memories["id"]
|
|
1463
|
+
memories["metadata"]["memory"] = memories["memory"]
|
|
1464
|
+
memories_list.append(memories)
|
|
1465
|
+
reformat_memory_list.append({"cube_id": memory["cube_id"], "memories": memories_list})
|
|
1466
|
+
logger.info(f"search memory list is : {reformat_memory_list}")
|
|
1467
|
+
search_result["text_mem"] = reformat_memory_list
|
|
1468
|
+
|
|
1469
|
+
pref_memory_list = search_result["pref_mem"]
|
|
1470
|
+
reformat_pref_memory_list = []
|
|
1471
|
+
for memory in pref_memory_list:
|
|
1472
|
+
memories_list = []
|
|
1473
|
+
for data in memory["memories"]:
|
|
1474
|
+
memories = data.model_dump()
|
|
1475
|
+
memories["ref_id"] = f"[{memories['id'].split('-')[0]}]"
|
|
1476
|
+
memories["metadata"]["embedding"] = []
|
|
1477
|
+
memories["metadata"]["sources"] = []
|
|
1478
|
+
memories["metadata"]["ref_id"] = f"[{memories['id'].split('-')[0]}]"
|
|
1479
|
+
memories["metadata"]["id"] = memories["id"]
|
|
1480
|
+
memories["metadata"]["memory"] = memories["memory"]
|
|
1481
|
+
memories_list.append(memories)
|
|
1482
|
+
reformat_pref_memory_list.append(
|
|
1483
|
+
{"cube_id": memory["cube_id"], "memories": memories_list}
|
|
1484
|
+
)
|
|
1485
|
+
search_result["pref_mem"] = reformat_pref_memory_list
|
|
1486
|
+
time_end = time.time()
|
|
1487
|
+
logger.info(
|
|
1488
|
+
f"time search: total time for user_id: {user_id} time is: {time_end - time_start}"
|
|
1489
|
+
)
|
|
1490
|
+
return search_result
|
|
1491
|
+
|
|
1492
|
+
def add(
|
|
1493
|
+
self,
|
|
1494
|
+
user_id: str,
|
|
1495
|
+
messages: MessageList | None = None,
|
|
1496
|
+
memory_content: str | None = None,
|
|
1497
|
+
doc_path: str | None = None,
|
|
1498
|
+
mem_cube_id: str | None = None,
|
|
1499
|
+
source: str | None = None,
|
|
1500
|
+
user_profile: bool = False,
|
|
1501
|
+
session_id: str | None = None,
|
|
1502
|
+
task_id: str | None = None, # Add task_id parameter
|
|
1503
|
+
):
|
|
1504
|
+
"""Add memory for a specific user."""
|
|
1505
|
+
|
|
1506
|
+
# Load user cubes if not already loaded
|
|
1507
|
+
self._load_user_cubes(user_id, self.default_cube_config)
|
|
1508
|
+
result = super().add(
|
|
1509
|
+
messages,
|
|
1510
|
+
memory_content,
|
|
1511
|
+
doc_path,
|
|
1512
|
+
mem_cube_id,
|
|
1513
|
+
user_id,
|
|
1514
|
+
session_id=session_id,
|
|
1515
|
+
task_id=task_id,
|
|
1516
|
+
)
|
|
1517
|
+
if user_profile:
|
|
1518
|
+
try:
|
|
1519
|
+
user_interests = memory_content.split("'userInterests': '")[1].split("', '")[0]
|
|
1520
|
+
user_interests = user_interests.replace(",", " ")
|
|
1521
|
+
user_profile_memories = self.mem_cubes[
|
|
1522
|
+
mem_cube_id
|
|
1523
|
+
].text_mem.internet_retriever.retrieve_from_internet(query=user_interests, top_k=5)
|
|
1524
|
+
for memory in user_profile_memories:
|
|
1525
|
+
self.mem_cubes[mem_cube_id].text_mem.add(memory)
|
|
1526
|
+
except Exception as e:
|
|
1527
|
+
logger.error(
|
|
1528
|
+
f"Failed to retrieve user profile: {e}, memory_content: {memory_content}"
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
return result
|
|
1532
|
+
|
|
1533
|
+
def list_users(self) -> list:
|
|
1534
|
+
"""List all registered users."""
|
|
1535
|
+
return self.user_manager.list_users()
|
|
1536
|
+
|
|
1537
|
+
def get_user_info(self, user_id: str) -> dict:
|
|
1538
|
+
"""Get user information including accessible cubes."""
|
|
1539
|
+
# Use MOSCore's built-in user validation
|
|
1540
|
+
# Validate user access
|
|
1541
|
+
self._validate_user_access(user_id)
|
|
1542
|
+
|
|
1543
|
+
result = super().get_user_info()
|
|
1544
|
+
|
|
1545
|
+
return result
|
|
1546
|
+
|
|
1547
|
+
def share_cube_with_user(self, cube_id: str, owner_user_id: str, target_user_id: str) -> bool:
|
|
1548
|
+
"""Share a cube with another user."""
|
|
1549
|
+
# Use MOSCore's built-in cube access validation
|
|
1550
|
+
self._validate_cube_access(owner_user_id, cube_id)
|
|
1551
|
+
|
|
1552
|
+
result = super().share_cube_with_user(cube_id, target_user_id)
|
|
1553
|
+
|
|
1554
|
+
return result
|
|
1555
|
+
|
|
1556
|
+
def clear_user_chat_history(self, user_id: str) -> None:
|
|
1557
|
+
"""Clear chat history for a specific user."""
|
|
1558
|
+
# Validate user access
|
|
1559
|
+
self._validate_user_access(user_id)
|
|
1560
|
+
|
|
1561
|
+
super().clear_messages(user_id)
|
|
1562
|
+
|
|
1563
|
+
def update_user_config(self, user_id: str, config: MOSConfig) -> bool:
|
|
1564
|
+
"""Update user configuration.
|
|
1565
|
+
|
|
1566
|
+
Args:
|
|
1567
|
+
user_id (str): The user ID.
|
|
1568
|
+
config (MOSConfig): The new configuration.
|
|
1569
|
+
|
|
1570
|
+
Returns:
|
|
1571
|
+
bool: True if successful, False otherwise.
|
|
1572
|
+
"""
|
|
1573
|
+
try:
|
|
1574
|
+
# Save to persistent storage
|
|
1575
|
+
success = self.user_manager.save_user_config(user_id, config)
|
|
1576
|
+
if success:
|
|
1577
|
+
# Update in-memory config
|
|
1578
|
+
self.user_configs[user_id] = config
|
|
1579
|
+
logger.info(f"Updated configuration for user {user_id}")
|
|
1580
|
+
|
|
1581
|
+
return success
|
|
1582
|
+
except Exception as e:
|
|
1583
|
+
logger.error(f"Failed to update user config for {user_id}: {e}")
|
|
1584
|
+
return False
|
|
1585
|
+
|
|
1586
|
+
def get_user_config(self, user_id: str) -> MOSConfig | None:
|
|
1587
|
+
"""Get user configuration.
|
|
1588
|
+
|
|
1589
|
+
Args:
|
|
1590
|
+
user_id (str): The user ID.
|
|
1591
|
+
|
|
1592
|
+
Returns:
|
|
1593
|
+
MOSConfig | None: The user's configuration or None if not found.
|
|
1594
|
+
"""
|
|
1595
|
+
return self.user_manager.get_user_config(user_id)
|
|
1596
|
+
|
|
1597
|
+
def get_active_user_count(self) -> int:
|
|
1598
|
+
"""Get the number of active user configurations in memory."""
|
|
1599
|
+
return len(self.user_configs)
|
|
1600
|
+
|
|
1601
|
+
def get_user_instance_info(self) -> dict[str, Any]:
|
|
1602
|
+
"""Get information about user configurations in memory."""
|
|
1603
|
+
return {
|
|
1604
|
+
"active_instances": len(self.user_configs),
|
|
1605
|
+
"max_instances": self.max_user_instances,
|
|
1606
|
+
"user_ids": list(self.user_configs.keys()),
|
|
1607
|
+
"lru_order": list(self.user_configs.keys()), # OrderedDict maintains insertion order
|
|
1608
|
+
}
|