MemoryOS 0.2.0__py3-none-any.whl → 0.2.2__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.
Potentially problematic release.
This version of MemoryOS might be problematic. Click here for more details.
- {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/METADATA +67 -26
- memoryos-0.2.2.dist-info/RECORD +169 -0
- memoryos-0.2.2.dist-info/entry_points.txt +3 -0
- memos/__init__.py +1 -1
- memos/api/config.py +562 -0
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +90 -0
- memos/api/exceptions.py +28 -0
- memos/api/mcp_serve.py +502 -0
- memos/api/product_api.py +35 -0
- memos/api/product_models.py +163 -0
- memos/api/routers/__init__.py +1 -0
- memos/api/routers/product_router.py +386 -0
- memos/chunkers/sentence_chunker.py +8 -2
- memos/cli.py +113 -0
- memos/configs/embedder.py +27 -0
- memos/configs/graph_db.py +132 -3
- memos/configs/internet_retriever.py +6 -0
- memos/configs/llm.py +47 -0
- memos/configs/mem_cube.py +1 -1
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +107 -7
- memos/configs/mem_user.py +58 -0
- memos/configs/memory.py +5 -4
- memos/dependency.py +52 -0
- memos/embedders/ark.py +92 -0
- memos/embedders/factory.py +4 -0
- memos/embedders/sentence_transformer.py +8 -2
- memos/embedders/universal_api.py +32 -0
- memos/graph_dbs/base.py +11 -3
- memos/graph_dbs/factory.py +4 -0
- memos/graph_dbs/nebular.py +1364 -0
- memos/graph_dbs/neo4j.py +333 -124
- memos/graph_dbs/neo4j_community.py +300 -0
- memos/llms/base.py +9 -0
- memos/llms/deepseek.py +54 -0
- memos/llms/factory.py +10 -1
- memos/llms/hf.py +170 -13
- memos/llms/hf_singleton.py +114 -0
- memos/llms/ollama.py +4 -0
- memos/llms/openai.py +67 -1
- memos/llms/qwen.py +63 -0
- memos/llms/vllm.py +153 -0
- memos/log.py +1 -1
- memos/mem_cube/general.py +77 -16
- memos/mem_cube/utils.py +109 -0
- memos/mem_os/core.py +251 -51
- memos/mem_os/main.py +94 -12
- memos/mem_os/product.py +1220 -43
- memos/mem_os/utils/default_config.py +352 -0
- memos/mem_os/utils/format_utils.py +1401 -0
- memos/mem_reader/simple_struct.py +18 -10
- memos/mem_scheduler/base_scheduler.py +441 -40
- memos/mem_scheduler/general_scheduler.py +249 -248
- memos/mem_scheduler/modules/base.py +14 -5
- memos/mem_scheduler/modules/dispatcher.py +67 -4
- memos/mem_scheduler/modules/misc.py +104 -0
- memos/mem_scheduler/modules/monitor.py +240 -50
- memos/mem_scheduler/modules/rabbitmq_service.py +319 -0
- memos/mem_scheduler/modules/redis_service.py +32 -22
- memos/mem_scheduler/modules/retriever.py +167 -23
- memos/mem_scheduler/modules/scheduler_logger.py +255 -0
- memos/mem_scheduler/mos_for_test_scheduler.py +140 -0
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +43 -0
- memos/mem_scheduler/{modules/schemas.py → schemas/message_schemas.py} +63 -61
- memos/mem_scheduler/schemas/monitor_schemas.py +329 -0
- memos/mem_scheduler/utils/__init__.py +0 -0
- memos/mem_scheduler/utils/filter_utils.py +176 -0
- memos/mem_scheduler/utils/misc_utils.py +61 -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 +500 -0
- memos/mem_user/persistent_factory.py +96 -0
- memos/mem_user/persistent_user_manager.py +260 -0
- memos/mem_user/user_manager.py +4 -4
- memos/memories/activation/item.py +29 -0
- memos/memories/activation/kv.py +10 -3
- memos/memories/activation/vllmkv.py +219 -0
- memos/memories/factory.py +2 -0
- memos/memories/textual/base.py +1 -1
- memos/memories/textual/general.py +43 -97
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +22 -12
- memos/memories/textual/tree_text_memory/organize/conflict.py +9 -5
- memos/memories/textual/tree_text_memory/organize/manager.py +26 -18
- memos/memories/textual/tree_text_memory/organize/redundancy.py +25 -44
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +50 -48
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +81 -56
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +52 -28
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -15
- memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
- memos/memos_tools/dinding_report_bot.py +422 -0
- memos/memos_tools/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +96 -0
- memos/parsers/markitdown.py +8 -2
- memos/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +66 -23
- memos/templates/mem_scheduler_prompts.py +126 -43
- memos/templates/mos_prompts.py +87 -0
- memos/templates/tree_reorganize_prompts.py +85 -30
- memos/vec_dbs/base.py +12 -0
- memos/vec_dbs/qdrant.py +46 -20
- memoryos-0.2.0.dist-info/RECORD +0 -128
- memos/mem_scheduler/utils.py +0 -26
- {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
- {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
memos/mem_cube/utils.py
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
1
3
|
import subprocess
|
|
2
4
|
import tempfile
|
|
3
5
|
|
|
6
|
+
from memos.configs.mem_cube import GeneralMemCubeConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
4
11
|
|
|
5
12
|
def download_repo(repo: str, base_url: str, dir: str | None = None) -> str:
|
|
6
13
|
"""Download a repository from a remote source.
|
|
@@ -22,3 +29,105 @@ def download_repo(repo: str, base_url: str, dir: str | None = None) -> str:
|
|
|
22
29
|
subprocess.run(["git", "clone", repo_url, dir], check=True)
|
|
23
30
|
|
|
24
31
|
return dir
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def merge_config_with_default(
|
|
35
|
+
existing_config: GeneralMemCubeConfig, default_config: GeneralMemCubeConfig
|
|
36
|
+
) -> GeneralMemCubeConfig:
|
|
37
|
+
"""
|
|
38
|
+
Merge existing cube config with default config, preserving critical fields.
|
|
39
|
+
|
|
40
|
+
This method updates general configuration fields (like API keys, model parameters)
|
|
41
|
+
while preserving critical user-specific fields (like user_id, cube_id, graph_db settings).
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
existing_config (GeneralMemCubeConfig): The existing cube configuration loaded from file
|
|
45
|
+
default_config (GeneralMemCubeConfig): The default configuration to merge from
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
GeneralMemCubeConfig: Merged configuration
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Convert configs to dictionaries
|
|
52
|
+
existing_dict = existing_config.model_dump(mode="json")
|
|
53
|
+
default_dict = default_config.model_dump(mode="json")
|
|
54
|
+
|
|
55
|
+
logger.info(
|
|
56
|
+
f"Starting config merge for user {existing_config.user_id}, cube {existing_config.cube_id}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Define fields that should be preserved from existing config
|
|
60
|
+
preserve_fields = {"user_id", "cube_id", "config_filename", "model_schema"}
|
|
61
|
+
|
|
62
|
+
# Preserve graph_db from existing config if it exists, but merge some fields
|
|
63
|
+
preserved_graph_db = None
|
|
64
|
+
if "text_mem" in existing_dict and "text_mem" in default_dict:
|
|
65
|
+
existing_text_config = existing_dict["text_mem"].get("config", {})
|
|
66
|
+
default_text_config = default_dict["text_mem"].get("config", {})
|
|
67
|
+
|
|
68
|
+
if "graph_db" in existing_text_config and "graph_db" in default_text_config:
|
|
69
|
+
existing_graph_config = existing_text_config["graph_db"]["config"]
|
|
70
|
+
default_graph_config = default_text_config["graph_db"]["config"]
|
|
71
|
+
|
|
72
|
+
# Define graph_db fields to preserve (user-specific)
|
|
73
|
+
preserve_graph_fields = {
|
|
74
|
+
"auto_create",
|
|
75
|
+
"user_name",
|
|
76
|
+
"use_multi_db",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Create merged graph_db config
|
|
80
|
+
merged_graph_config = copy.deepcopy(existing_graph_config)
|
|
81
|
+
for key, value in default_graph_config.items():
|
|
82
|
+
if key not in preserve_graph_fields:
|
|
83
|
+
merged_graph_config[key] = value
|
|
84
|
+
logger.debug(
|
|
85
|
+
f"Updated graph_db field '{key}': {existing_graph_config.get(key)} -> {value}"
|
|
86
|
+
)
|
|
87
|
+
if not default_graph_config.get("use_multi_db", True):
|
|
88
|
+
# set original use_multi_db to False if default_graph_config.use_multi_db is False
|
|
89
|
+
if merged_graph_config.get("use_multi_db", True):
|
|
90
|
+
merged_graph_config["use_multi_db"] = False
|
|
91
|
+
merged_graph_config["user_name"] = merged_graph_config.get("db_name")
|
|
92
|
+
merged_graph_config["db_name"] = default_graph_config.get("db_name")
|
|
93
|
+
else:
|
|
94
|
+
logger.info("use_multi_db is already False, no need to change")
|
|
95
|
+
if "neo4j" not in default_text_config["graph_db"]["backend"]:
|
|
96
|
+
if "db_name" in merged_graph_config:
|
|
97
|
+
merged_graph_config.pop("db_name")
|
|
98
|
+
logger.info("neo4j is not supported, remove db_name")
|
|
99
|
+
else:
|
|
100
|
+
logger.info("db_name is not in merged_graph_config, no need to remove")
|
|
101
|
+
else:
|
|
102
|
+
if "space" in merged_graph_config:
|
|
103
|
+
merged_graph_config.pop("space")
|
|
104
|
+
logger.info("neo4j is not supported, remove db_name")
|
|
105
|
+
else:
|
|
106
|
+
logger.info("space is not in merged_graph_config, no need to remove")
|
|
107
|
+
preserved_graph_db = {
|
|
108
|
+
"backend": default_text_config["graph_db"]["backend"],
|
|
109
|
+
"config": merged_graph_config,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Use default config as base
|
|
113
|
+
merged_dict = copy.deepcopy(default_dict)
|
|
114
|
+
|
|
115
|
+
# Restore preserved fields from existing config
|
|
116
|
+
for field in preserve_fields:
|
|
117
|
+
if field in existing_dict:
|
|
118
|
+
merged_dict[field] = existing_dict[field]
|
|
119
|
+
logger.debug(f"Preserved field '{field}': {existing_dict[field]}")
|
|
120
|
+
|
|
121
|
+
# Restore graph_db if it was preserved
|
|
122
|
+
if preserved_graph_db and "text_mem" in merged_dict:
|
|
123
|
+
merged_dict["text_mem"]["config"]["graph_db"] = preserved_graph_db
|
|
124
|
+
logger.debug(f"Preserved graph_db with merged config: {preserved_graph_db}")
|
|
125
|
+
|
|
126
|
+
# Create new config from merged dictionary
|
|
127
|
+
merged_config = GeneralMemCubeConfig.model_validate(merged_dict)
|
|
128
|
+
|
|
129
|
+
logger.info(
|
|
130
|
+
f"Successfully merged cube config for user {merged_config.user_id}, cube {merged_config.cube_id}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return merged_config
|
memos/mem_os/core.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
3
|
+
import time
|
|
2
4
|
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from pathlib import Path
|
|
@@ -11,12 +13,18 @@ from memos.log import get_logger
|
|
|
11
13
|
from memos.mem_cube.general import GeneralMemCube
|
|
12
14
|
from memos.mem_reader.factory import MemReaderFactory
|
|
13
15
|
from memos.mem_scheduler.general_scheduler import GeneralScheduler
|
|
14
|
-
from memos.mem_scheduler.modules.schemas import ANSWER_LABEL, QUERY_LABEL, ScheduleMessageItem
|
|
15
16
|
from memos.mem_scheduler.scheduler_factory import SchedulerFactory
|
|
17
|
+
from memos.mem_scheduler.schemas.general_schemas import (
|
|
18
|
+
ADD_LABEL,
|
|
19
|
+
ANSWER_LABEL,
|
|
20
|
+
QUERY_LABEL,
|
|
21
|
+
)
|
|
22
|
+
from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
|
|
16
23
|
from memos.mem_user.user_manager import UserManager, UserRole
|
|
17
24
|
from memos.memories.activation.item import ActivationMemoryItem
|
|
18
25
|
from memos.memories.parametric.item import ParametricMemoryItem
|
|
19
26
|
from memos.memories.textual.item import TextualMemoryItem, TextualMemoryMetadata
|
|
27
|
+
from memos.templates.mos_prompts import QUERY_REWRITING_PROMPT
|
|
20
28
|
from memos.types import ChatHistory, MessageList, MOSSearchResult
|
|
21
29
|
|
|
22
30
|
|
|
@@ -30,7 +38,7 @@ class MOSCore:
|
|
|
30
38
|
MOSCore acts as an operating system layer for handling and orchestrating MemCube instances.
|
|
31
39
|
"""
|
|
32
40
|
|
|
33
|
-
def __init__(self, config: MOSConfig):
|
|
41
|
+
def __init__(self, config: MOSConfig, user_manager: UserManager | None = None):
|
|
34
42
|
self.config = config
|
|
35
43
|
self.user_id = config.user_id
|
|
36
44
|
self.session_id = config.session_id
|
|
@@ -39,7 +47,12 @@ class MOSCore:
|
|
|
39
47
|
self.mem_reader = MemReaderFactory.from_config(config.mem_reader)
|
|
40
48
|
self.chat_history_manager: dict[str, ChatHistory] = {}
|
|
41
49
|
self._register_chat_history()
|
|
42
|
-
|
|
50
|
+
|
|
51
|
+
# Use provided user_manager or create a new one
|
|
52
|
+
if user_manager is not None:
|
|
53
|
+
self.user_manager = user_manager
|
|
54
|
+
else:
|
|
55
|
+
self.user_manager = UserManager(user_id=self.user_id if self.user_id else "root")
|
|
43
56
|
|
|
44
57
|
# Validate user exists
|
|
45
58
|
if not self.user_manager.validate_user(self.user_id):
|
|
@@ -47,10 +60,15 @@ class MOSCore:
|
|
|
47
60
|
f"User '{self.user_id}' does not exist or is inactive. Please create user first."
|
|
48
61
|
)
|
|
49
62
|
|
|
50
|
-
#
|
|
63
|
+
# Initialize mem_scheduler
|
|
51
64
|
self._mem_scheduler_lock = Lock()
|
|
52
65
|
self.enable_mem_scheduler = self.config.get("enable_mem_scheduler", False)
|
|
53
|
-
self.
|
|
66
|
+
if self.enable_mem_scheduler:
|
|
67
|
+
self._mem_scheduler = self._initialize_mem_scheduler()
|
|
68
|
+
self._mem_scheduler.mem_cubes = self.mem_cubes
|
|
69
|
+
else:
|
|
70
|
+
self._mem_scheduler: GeneralScheduler = None
|
|
71
|
+
|
|
54
72
|
logger.info(f"MOS initialized for user: {self.user_id}")
|
|
55
73
|
|
|
56
74
|
@property
|
|
@@ -58,6 +76,7 @@ class MOSCore:
|
|
|
58
76
|
"""Lazy-loaded property for memory scheduler."""
|
|
59
77
|
if self.enable_mem_scheduler and self._mem_scheduler is None:
|
|
60
78
|
self._initialize_mem_scheduler()
|
|
79
|
+
self._mem_scheduler.mem_cubes = self.mem_cubes
|
|
61
80
|
return self._mem_scheduler
|
|
62
81
|
|
|
63
82
|
@mem_scheduler.setter
|
|
@@ -74,26 +93,43 @@ class MOSCore:
|
|
|
74
93
|
raise TypeError(f"Expected GeneralScheduler or None, got {type(value)}")
|
|
75
94
|
|
|
76
95
|
self._mem_scheduler = value
|
|
96
|
+
self._mem_scheduler.mem_cubes = self.mem_cubes
|
|
77
97
|
|
|
78
98
|
if value:
|
|
79
99
|
logger.info("Memory scheduler manually set")
|
|
80
100
|
else:
|
|
81
101
|
logger.debug("Memory scheduler cleared")
|
|
82
102
|
|
|
83
|
-
def _initialize_mem_scheduler(self):
|
|
103
|
+
def _initialize_mem_scheduler(self) -> GeneralScheduler:
|
|
84
104
|
"""Initialize the memory scheduler on first access."""
|
|
85
105
|
if not self.config.enable_mem_scheduler:
|
|
86
106
|
logger.debug("Memory scheduler is disabled in config")
|
|
87
107
|
self._mem_scheduler = None
|
|
108
|
+
return self._mem_scheduler
|
|
88
109
|
elif not hasattr(self.config, "mem_scheduler"):
|
|
89
110
|
logger.error("Config of Memory scheduler is not available")
|
|
90
111
|
self._mem_scheduler = None
|
|
112
|
+
return self._mem_scheduler
|
|
91
113
|
else:
|
|
92
114
|
logger.info("Initializing memory scheduler...")
|
|
93
115
|
scheduler_config = self.config.mem_scheduler
|
|
94
116
|
self._mem_scheduler = SchedulerFactory.from_config(scheduler_config)
|
|
95
|
-
|
|
117
|
+
# Validate required components
|
|
118
|
+
if not hasattr(self.mem_reader, "llm"):
|
|
119
|
+
raise AttributeError(
|
|
120
|
+
f"Memory reader of type {type(self.mem_reader).__name__} "
|
|
121
|
+
"missing required 'llm' attribute"
|
|
122
|
+
)
|
|
123
|
+
self._mem_scheduler.initialize_modules(
|
|
124
|
+
chat_llm=self.chat_llm, process_llm=self.chat_llm
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
# Configure scheduler modules
|
|
128
|
+
self._mem_scheduler.initialize_modules(
|
|
129
|
+
chat_llm=self.chat_llm, process_llm=self.mem_reader.llm
|
|
130
|
+
)
|
|
96
131
|
self._mem_scheduler.start()
|
|
132
|
+
return self._mem_scheduler
|
|
97
133
|
|
|
98
134
|
def mem_scheduler_on(self) -> bool:
|
|
99
135
|
if not self.config.enable_mem_scheduler or self._mem_scheduler is None:
|
|
@@ -123,6 +159,25 @@ class MOSCore:
|
|
|
123
159
|
logger.error(f"Failed to stop scheduler: {e!s}")
|
|
124
160
|
return False
|
|
125
161
|
|
|
162
|
+
def mem_reorganizer_on(self) -> bool:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
def mem_reorganizer_off(self) -> bool:
|
|
166
|
+
"""temporally implement"""
|
|
167
|
+
for mem_cube in self.mem_cubes.values():
|
|
168
|
+
logger.info(f"try to close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
169
|
+
if mem_cube.text_mem and mem_cube.text_mem.is_reorganize:
|
|
170
|
+
logger.info(f"close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
171
|
+
mem_cube.text_mem.memory_manager.close()
|
|
172
|
+
mem_cube.text_mem.memory_manager.wait_reorganizer()
|
|
173
|
+
|
|
174
|
+
def mem_reorganizer_wait(self) -> bool:
|
|
175
|
+
for mem_cube in self.mem_cubes.values():
|
|
176
|
+
logger.info(f"try to close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
177
|
+
if mem_cube.text_mem and mem_cube.text_mem.is_reorganize:
|
|
178
|
+
logger.info(f"close reorganizer for {mem_cube.text_mem.config.cube_id}")
|
|
179
|
+
mem_cube.text_mem.memory_manager.wait_reorganizer()
|
|
180
|
+
|
|
126
181
|
def _register_chat_history(self, user_id: str | None = None) -> None:
|
|
127
182
|
"""Initialize chat history with user ID."""
|
|
128
183
|
if user_id is None:
|
|
@@ -186,12 +241,16 @@ class MOSCore:
|
|
|
186
241
|
documents.append(str(file_path))
|
|
187
242
|
return documents
|
|
188
243
|
|
|
189
|
-
def chat(self, query: str, user_id: str | None = None) -> str:
|
|
244
|
+
def chat(self, query: str, user_id: str | None = None, base_prompt: str | None = None) -> str:
|
|
190
245
|
"""
|
|
191
246
|
Chat with the MOS.
|
|
192
247
|
|
|
193
248
|
Args:
|
|
194
249
|
query (str): The user's query.
|
|
250
|
+
user_id (str, optional): The user ID for the chat session. Defaults to the user ID from the config.
|
|
251
|
+
base_prompt (str, optional): A custom base prompt to use for the chat.
|
|
252
|
+
It can be a template string with a `{memories}` placeholder.
|
|
253
|
+
If not provided, a default prompt is used.
|
|
195
254
|
|
|
196
255
|
Returns:
|
|
197
256
|
str: The response from the MOS.
|
|
@@ -224,12 +283,20 @@ class MOSCore:
|
|
|
224
283
|
)
|
|
225
284
|
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
226
285
|
|
|
227
|
-
memories = mem_cube.text_mem.search(
|
|
286
|
+
memories = mem_cube.text_mem.search(
|
|
287
|
+
query,
|
|
288
|
+
top_k=self.config.top_k,
|
|
289
|
+
info={
|
|
290
|
+
"user_id": target_user_id,
|
|
291
|
+
"session_id": self.session_id,
|
|
292
|
+
"chat_history": chat_history.chat_history,
|
|
293
|
+
},
|
|
294
|
+
)
|
|
228
295
|
memories_all.extend(memories)
|
|
229
296
|
logger.info(f"🧠 [Memory] Searched memories:\n{self._str_memories(memories_all)}\n")
|
|
230
|
-
system_prompt = self._build_system_prompt(memories_all)
|
|
297
|
+
system_prompt = self._build_system_prompt(memories_all, base_prompt=base_prompt)
|
|
231
298
|
else:
|
|
232
|
-
system_prompt = self._build_system_prompt()
|
|
299
|
+
system_prompt = self._build_system_prompt(base_prompt=base_prompt)
|
|
233
300
|
current_messages = [
|
|
234
301
|
{"role": "system", "content": system_prompt},
|
|
235
302
|
*chat_history.chat_history,
|
|
@@ -261,8 +328,8 @@ class MOSCore:
|
|
|
261
328
|
self.chat_history_manager[user_id] = chat_history
|
|
262
329
|
|
|
263
330
|
# submit message to scheduler
|
|
264
|
-
|
|
265
|
-
mem_cube_id =
|
|
331
|
+
for accessible_mem_cube in accessible_cubes:
|
|
332
|
+
mem_cube_id = accessible_mem_cube.cube_id
|
|
266
333
|
mem_cube = self.mem_cubes[mem_cube_id]
|
|
267
334
|
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
268
335
|
message_item = ScheduleMessageItem(
|
|
@@ -277,20 +344,39 @@ class MOSCore:
|
|
|
277
344
|
|
|
278
345
|
return response
|
|
279
346
|
|
|
280
|
-
def _build_system_prompt(
|
|
347
|
+
def _build_system_prompt(
|
|
348
|
+
self,
|
|
349
|
+
memories: list[TextualMemoryItem] | list[str] | None = None,
|
|
350
|
+
base_prompt: str | None = None,
|
|
351
|
+
) -> str:
|
|
281
352
|
"""Build system prompt with optional memories context."""
|
|
282
|
-
base_prompt
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
353
|
+
if base_prompt is None:
|
|
354
|
+
base_prompt = (
|
|
355
|
+
"You are a knowledgeable and helpful AI assistant. "
|
|
356
|
+
"You have access to conversation memories that help you provide more personalized responses. "
|
|
357
|
+
"Use the memories to understand the user's context, preferences, and past interactions. "
|
|
358
|
+
"If memories are provided, reference them naturally when relevant, but don't explicitly mention having memories."
|
|
359
|
+
)
|
|
288
360
|
|
|
361
|
+
memory_context = ""
|
|
289
362
|
if memories:
|
|
290
|
-
|
|
363
|
+
memory_list = []
|
|
291
364
|
for i, memory in enumerate(memories, 1):
|
|
292
|
-
|
|
293
|
-
|
|
365
|
+
if isinstance(memory, TextualMemoryItem):
|
|
366
|
+
text_memory = memory.memory
|
|
367
|
+
else:
|
|
368
|
+
if not isinstance(memory, str):
|
|
369
|
+
logger.error("Unexpected memory type.")
|
|
370
|
+
text_memory = memory
|
|
371
|
+
memory_list.append(f"{i}. {text_memory}")
|
|
372
|
+
memory_context = "\n".join(memory_list)
|
|
373
|
+
|
|
374
|
+
if "{memories}" in base_prompt:
|
|
375
|
+
return base_prompt.format(memories=memory_context)
|
|
376
|
+
elif memories:
|
|
377
|
+
# For backward compatibility, append memories if no placeholder is found
|
|
378
|
+
memory_context_with_header = "\n\n## Memories:\n" + memory_context
|
|
379
|
+
return base_prompt + memory_context_with_header
|
|
294
380
|
return base_prompt
|
|
295
381
|
|
|
296
382
|
def _str_memories(
|
|
@@ -364,7 +450,10 @@ class MOSCore:
|
|
|
364
450
|
return self.user_manager.create_cube(cube_name, owner_id, cube_path, cube_id)
|
|
365
451
|
|
|
366
452
|
def register_mem_cube(
|
|
367
|
-
self,
|
|
453
|
+
self,
|
|
454
|
+
mem_cube_name_or_path: str | GeneralMemCube,
|
|
455
|
+
mem_cube_id: str | None = None,
|
|
456
|
+
user_id: str | None = None,
|
|
368
457
|
) -> None:
|
|
369
458
|
"""
|
|
370
459
|
Register a MemCube with the MOS.
|
|
@@ -377,12 +466,18 @@ class MOSCore:
|
|
|
377
466
|
self._validate_user_exists(target_user_id)
|
|
378
467
|
|
|
379
468
|
if mem_cube_id is None:
|
|
380
|
-
|
|
469
|
+
if isinstance(mem_cube_name_or_path, GeneralMemCube):
|
|
470
|
+
mem_cube_id = f"cube_{target_user_id}"
|
|
471
|
+
else:
|
|
472
|
+
mem_cube_id = mem_cube_name_or_path
|
|
381
473
|
|
|
382
474
|
if mem_cube_id in self.mem_cubes:
|
|
383
475
|
logger.info(f"MemCube with ID {mem_cube_id} already in MOS, skip install.")
|
|
384
476
|
else:
|
|
385
|
-
if
|
|
477
|
+
if isinstance(mem_cube_name_or_path, GeneralMemCube):
|
|
478
|
+
self.mem_cubes[mem_cube_id] = mem_cube_name_or_path
|
|
479
|
+
logger.info(f"register new cube {mem_cube_id} for user {target_user_id}")
|
|
480
|
+
elif os.path.exists(mem_cube_name_or_path):
|
|
386
481
|
self.mem_cubes[mem_cube_id] = GeneralMemCube.init_from_dir(mem_cube_name_or_path)
|
|
387
482
|
else:
|
|
388
483
|
logger.warning(
|
|
@@ -394,6 +489,14 @@ class MOSCore:
|
|
|
394
489
|
# Check if cube already exists in database
|
|
395
490
|
existing_cube = self.user_manager.get_cube(mem_cube_id)
|
|
396
491
|
|
|
492
|
+
# check the embedder is it consistent with MOSConfig
|
|
493
|
+
if self.config.mem_reader.config.embedder != (
|
|
494
|
+
cube_embedder := self.mem_cubes[mem_cube_id].text_mem.config.embedder
|
|
495
|
+
):
|
|
496
|
+
logger.warning(
|
|
497
|
+
f"Cube Embedder is not consistent with MOSConfig for cube: {mem_cube_id}, will use Cube Embedder: {cube_embedder}"
|
|
498
|
+
)
|
|
499
|
+
|
|
397
500
|
if existing_cube:
|
|
398
501
|
# Cube exists, just add user to cube if not already associated
|
|
399
502
|
if not self.user_manager.validate_user_cube_access(target_user_id, mem_cube_id):
|
|
@@ -407,10 +510,14 @@ class MOSCore:
|
|
|
407
510
|
else:
|
|
408
511
|
# Cube doesn't exist, create it
|
|
409
512
|
self.create_cube_for_user(
|
|
410
|
-
cube_name=mem_cube_name_or_path
|
|
513
|
+
cube_name=mem_cube_name_or_path
|
|
514
|
+
if not isinstance(mem_cube_name_or_path, GeneralMemCube)
|
|
515
|
+
else mem_cube_id,
|
|
411
516
|
owner_id=target_user_id,
|
|
412
517
|
cube_id=mem_cube_id,
|
|
413
|
-
cube_path=mem_cube_name_or_path
|
|
518
|
+
cube_path=mem_cube_name_or_path
|
|
519
|
+
if not isinstance(mem_cube_name_or_path, GeneralMemCube)
|
|
520
|
+
else "init",
|
|
414
521
|
)
|
|
415
522
|
logger.info(f"register new cube {mem_cube_id} for user {target_user_id}")
|
|
416
523
|
|
|
@@ -427,7 +534,13 @@ class MOSCore:
|
|
|
427
534
|
raise ValueError(f"MemCube with ID {mem_cube_id} does not exist.")
|
|
428
535
|
|
|
429
536
|
def search(
|
|
430
|
-
self,
|
|
537
|
+
self,
|
|
538
|
+
query: str,
|
|
539
|
+
user_id: str | None = None,
|
|
540
|
+
install_cube_ids: list[str] | None = None,
|
|
541
|
+
top_k: int | None = None,
|
|
542
|
+
mode: Literal["fast", "fine"] = "fast",
|
|
543
|
+
internet_search: bool = False,
|
|
431
544
|
) -> MOSSearchResult:
|
|
432
545
|
"""
|
|
433
546
|
Search for textual memories across all registered MemCubes.
|
|
@@ -451,6 +564,10 @@ class MOSCore:
|
|
|
451
564
|
logger.info(
|
|
452
565
|
f"User {target_user_id} has access to {len(user_cube_ids)} cubes: {user_cube_ids}"
|
|
453
566
|
)
|
|
567
|
+
if target_user_id not in self.chat_history_manager:
|
|
568
|
+
self._register_chat_history(target_user_id)
|
|
569
|
+
chat_history = self.chat_history_manager[target_user_id]
|
|
570
|
+
|
|
454
571
|
result: MOSSearchResult = {
|
|
455
572
|
"text_mem": [],
|
|
456
573
|
"act_mem": [],
|
|
@@ -464,20 +581,25 @@ class MOSCore:
|
|
|
464
581
|
and (mem_cube.text_mem is not None)
|
|
465
582
|
and self.config.enable_textual_memory
|
|
466
583
|
):
|
|
467
|
-
|
|
584
|
+
time_start = time.time()
|
|
585
|
+
memories = mem_cube.text_mem.search(
|
|
586
|
+
query,
|
|
587
|
+
top_k=top_k if top_k else self.config.top_k,
|
|
588
|
+
mode=mode,
|
|
589
|
+
manual_close_internet=not internet_search,
|
|
590
|
+
info={
|
|
591
|
+
"user_id": target_user_id,
|
|
592
|
+
"session_id": self.session_id,
|
|
593
|
+
"chat_history": chat_history.chat_history,
|
|
594
|
+
},
|
|
595
|
+
)
|
|
468
596
|
result["text_mem"].append({"cube_id": mem_cube_id, "memories": memories})
|
|
469
597
|
logger.info(
|
|
470
598
|
f"🧠 [Memory] Searched memories from {mem_cube_id}:\n{self._str_memories(memories)}\n"
|
|
471
599
|
)
|
|
472
|
-
|
|
473
|
-
(mem_cube_id in install_cube_ids)
|
|
474
|
-
and (mem_cube.act_mem is not None)
|
|
475
|
-
and self.config.enable_activation_memory
|
|
476
|
-
):
|
|
477
|
-
memories = mem_cube.act_mem.extract(query)
|
|
478
|
-
result["act_mem"].append({"cube_id": mem_cube_id, "memories": [memories]})
|
|
600
|
+
search_time_end = time.time()
|
|
479
601
|
logger.info(
|
|
480
|
-
f"
|
|
602
|
+
f"time search graph: search graph time user_id: {target_user_id} time is: {search_time_end - time_start}"
|
|
481
603
|
)
|
|
482
604
|
return result
|
|
483
605
|
|
|
@@ -501,6 +623,7 @@ class MOSCore:
|
|
|
501
623
|
user_id (str, optional): The identifier of the user to add the memories to.
|
|
502
624
|
If None, the default user is used.
|
|
503
625
|
"""
|
|
626
|
+
# user input messages
|
|
504
627
|
assert (messages is not None) or (memory_content is not None) or (doc_path is not None), (
|
|
505
628
|
"messages_or_doc_path or memory_content or doc_path must be provided."
|
|
506
629
|
)
|
|
@@ -540,8 +663,29 @@ class MOSCore:
|
|
|
540
663
|
type="chat",
|
|
541
664
|
info={"user_id": target_user_id, "session_id": self.session_id},
|
|
542
665
|
)
|
|
666
|
+
|
|
667
|
+
mem_ids = []
|
|
543
668
|
for mem in memories:
|
|
544
|
-
self.mem_cubes[mem_cube_id].text_mem.add(mem)
|
|
669
|
+
mem_id_list: list[str] = self.mem_cubes[mem_cube_id].text_mem.add(mem)
|
|
670
|
+
mem_ids.extend(mem_id_list)
|
|
671
|
+
logger.info(
|
|
672
|
+
f"Added memory user {target_user_id} to memcube {mem_cube_id}: {mem_id_list}"
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# submit messages for scheduler
|
|
676
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
677
|
+
mem_cube = self.mem_cubes[mem_cube_id]
|
|
678
|
+
message_item = ScheduleMessageItem(
|
|
679
|
+
user_id=target_user_id,
|
|
680
|
+
mem_cube_id=mem_cube_id,
|
|
681
|
+
mem_cube=mem_cube,
|
|
682
|
+
label=ADD_LABEL,
|
|
683
|
+
content=json.dumps(mem_ids),
|
|
684
|
+
timestamp=datetime.now(),
|
|
685
|
+
)
|
|
686
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
687
|
+
|
|
688
|
+
# user profile
|
|
545
689
|
if (
|
|
546
690
|
(memory_content is not None)
|
|
547
691
|
and self.config.enable_textual_memory
|
|
@@ -556,34 +700,66 @@ class MOSCore:
|
|
|
556
700
|
)
|
|
557
701
|
else:
|
|
558
702
|
messages_list = [
|
|
559
|
-
[
|
|
560
|
-
|
|
561
|
-
{
|
|
562
|
-
"role": "assistant",
|
|
563
|
-
"content": "",
|
|
564
|
-
}, # add by str to keep the format,assistant role is empty
|
|
565
|
-
]
|
|
566
|
-
]
|
|
703
|
+
[{"role": "user", "content": memory_content}]
|
|
704
|
+
] # for only user-str input and convert message
|
|
567
705
|
memories = self.mem_reader.get_memory(
|
|
568
706
|
messages_list,
|
|
569
707
|
type="chat",
|
|
570
708
|
info={"user_id": target_user_id, "session_id": self.session_id},
|
|
571
709
|
)
|
|
710
|
+
|
|
711
|
+
mem_ids = []
|
|
572
712
|
for mem in memories:
|
|
573
|
-
self.mem_cubes[mem_cube_id].text_mem.add(mem)
|
|
713
|
+
mem_id_list: list[str] = self.mem_cubes[mem_cube_id].text_mem.add(mem)
|
|
714
|
+
logger.info(
|
|
715
|
+
f"Added memory user {target_user_id} to memcube {mem_cube_id}: {mem_id_list}"
|
|
716
|
+
)
|
|
717
|
+
mem_ids.extend(mem_id_list)
|
|
718
|
+
|
|
719
|
+
# submit messages for scheduler
|
|
720
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
721
|
+
mem_cube = self.mem_cubes[mem_cube_id]
|
|
722
|
+
message_item = ScheduleMessageItem(
|
|
723
|
+
user_id=target_user_id,
|
|
724
|
+
mem_cube_id=mem_cube_id,
|
|
725
|
+
mem_cube=mem_cube,
|
|
726
|
+
label=ADD_LABEL,
|
|
727
|
+
content=json.dumps(mem_ids),
|
|
728
|
+
timestamp=datetime.now(),
|
|
729
|
+
)
|
|
730
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
731
|
+
|
|
732
|
+
# user doc input
|
|
574
733
|
if (
|
|
575
734
|
(doc_path is not None)
|
|
576
735
|
and self.config.enable_textual_memory
|
|
577
736
|
and self.mem_cubes[mem_cube_id].text_mem
|
|
578
737
|
):
|
|
579
738
|
documents = self._get_all_documents(doc_path)
|
|
580
|
-
|
|
739
|
+
doc_memories = self.mem_reader.get_memory(
|
|
581
740
|
documents,
|
|
582
741
|
type="doc",
|
|
583
742
|
info={"user_id": target_user_id, "session_id": self.session_id},
|
|
584
743
|
)
|
|
585
|
-
|
|
586
|
-
|
|
744
|
+
|
|
745
|
+
mem_ids = []
|
|
746
|
+
for mem in doc_memories:
|
|
747
|
+
mem_id_list: list[str] = self.mem_cubes[mem_cube_id].text_mem.add(mem)
|
|
748
|
+
mem_ids.extend(mem_id_list)
|
|
749
|
+
|
|
750
|
+
# submit messages for scheduler
|
|
751
|
+
if self.enable_mem_scheduler and self.mem_scheduler is not None:
|
|
752
|
+
mem_cube = self.mem_cubes[mem_cube_id]
|
|
753
|
+
message_item = ScheduleMessageItem(
|
|
754
|
+
user_id=target_user_id,
|
|
755
|
+
mem_cube_id=mem_cube_id,
|
|
756
|
+
mem_cube=mem_cube,
|
|
757
|
+
label=ADD_LABEL,
|
|
758
|
+
content=json.dumps(mem_ids),
|
|
759
|
+
timestamp=datetime.now(),
|
|
760
|
+
)
|
|
761
|
+
self.mem_scheduler.submit_messages(messages=[message_item])
|
|
762
|
+
|
|
587
763
|
logger.info(f"Add memory to {mem_cube_id} successfully")
|
|
588
764
|
|
|
589
765
|
def get(
|
|
@@ -817,3 +993,27 @@ class MOSCore:
|
|
|
817
993
|
raise ValueError(f"Target user '{target_user_id}' does not exist or is inactive.")
|
|
818
994
|
|
|
819
995
|
return self.user_manager.add_user_to_cube(target_user_id, cube_id)
|
|
996
|
+
|
|
997
|
+
def get_query_rewrite(self, query: str, user_id: str | None = None):
|
|
998
|
+
"""
|
|
999
|
+
Rewrite user's query according the context.
|
|
1000
|
+
Args:
|
|
1001
|
+
query (str): The search query that needs rewriting.
|
|
1002
|
+
user_id(str, optional): The identifier of the user that the query belongs to.
|
|
1003
|
+
If None, the default user is used.
|
|
1004
|
+
|
|
1005
|
+
Returns:
|
|
1006
|
+
str: query after rewriting process.
|
|
1007
|
+
"""
|
|
1008
|
+
target_user_id = user_id if user_id is not None else self.user_id
|
|
1009
|
+
chat_history = self.chat_history_manager[target_user_id]
|
|
1010
|
+
|
|
1011
|
+
dialogue = "————{}".format("\n————".join(chat_history.chat_history))
|
|
1012
|
+
user_prompt = QUERY_REWRITING_PROMPT.format(dialogue=dialogue, query=query)
|
|
1013
|
+
messages = {"role": "user", "content": user_prompt}
|
|
1014
|
+
rewritten_result = self.chat_llm.generate(messages=messages)
|
|
1015
|
+
rewritten_result = json.loads(rewritten_result)
|
|
1016
|
+
if rewritten_result.get("former_dialogue_related", False):
|
|
1017
|
+
rewritten_query = rewritten_result.get("rewritten_question")
|
|
1018
|
+
return rewritten_query if len(rewritten_query) > 0 else query
|
|
1019
|
+
return query
|