MemoryOS 0.2.1__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.1.dist-info → memoryos-0.2.2.dist-info}/METADATA +2 -1
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/RECORD +72 -55
- memos/__init__.py +1 -1
- memos/api/config.py +156 -65
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +90 -0
- memos/api/product_models.py +5 -1
- memos/api/routers/product_router.py +54 -26
- memos/configs/graph_db.py +49 -1
- memos/configs/internet_retriever.py +6 -0
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +18 -4
- memos/configs/mem_user.py +58 -0
- memos/graph_dbs/base.py +9 -1
- memos/graph_dbs/factory.py +2 -0
- memos/graph_dbs/nebular.py +1364 -0
- memos/graph_dbs/neo4j.py +4 -4
- memos/log.py +1 -1
- memos/mem_cube/utils.py +13 -6
- memos/mem_os/core.py +140 -30
- memos/mem_os/main.py +1 -1
- memos/mem_os/product.py +266 -152
- memos/mem_os/utils/format_utils.py +314 -67
- memos/mem_reader/simple_struct.py +13 -5
- memos/mem_scheduler/base_scheduler.py +220 -250
- memos/mem_scheduler/general_scheduler.py +193 -73
- memos/mem_scheduler/modules/base.py +5 -5
- memos/mem_scheduler/modules/dispatcher.py +6 -9
- memos/mem_scheduler/modules/misc.py +81 -16
- memos/mem_scheduler/modules/monitor.py +52 -41
- memos/mem_scheduler/modules/rabbitmq_service.py +9 -7
- memos/mem_scheduler/modules/retriever.py +108 -191
- memos/mem_scheduler/modules/scheduler_logger.py +255 -0
- memos/mem_scheduler/mos_for_test_scheduler.py +16 -19
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +43 -0
- memos/mem_scheduler/schemas/message_schemas.py +148 -0
- 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/user_manager.py +4 -4
- memos/memories/activation/item.py +4 -0
- memos/memories/textual/base.py +1 -1
- memos/memories/textual/general.py +35 -91
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +13 -7
- memos/memories/textual/tree_text_memory/organize/conflict.py +4 -2
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +47 -43
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +8 -5
- 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/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +46 -23
- 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/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +2 -1
- memos/templates/mem_scheduler_prompts.py +41 -7
- memos/templates/mos_prompts.py +87 -0
- memos/mem_scheduler/modules/schemas.py +0 -328
- memos/mem_scheduler/utils.py +0 -75
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
- {memoryos-0.2.1.dist-info → memoryos-0.2.2.dist-info}/entry_points.txt +0 -0
|
@@ -6,18 +6,23 @@ from memos.llms.base import BaseLLM
|
|
|
6
6
|
from memos.log import get_logger
|
|
7
7
|
from memos.mem_cube.general import GeneralMemCube
|
|
8
8
|
from memos.mem_scheduler.modules.base import BaseSchedulerModule
|
|
9
|
-
from memos.mem_scheduler.
|
|
10
|
-
from memos.mem_scheduler.modules.schemas import (
|
|
9
|
+
from memos.mem_scheduler.schemas.general_schemas import (
|
|
11
10
|
DEFAULT_ACTIVATION_MEM_MONITOR_SIZE_LIMIT,
|
|
11
|
+
DEFAULT_WEIGHT_VECTOR_FOR_RANKING,
|
|
12
12
|
DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT,
|
|
13
13
|
MONITOR_ACTIVATION_MEMORY_TYPE,
|
|
14
14
|
MONITOR_WORKING_MEMORY_TYPE,
|
|
15
15
|
MemCubeID,
|
|
16
|
-
MemoryMonitorManager,
|
|
17
16
|
UserID,
|
|
18
17
|
)
|
|
19
|
-
from memos.mem_scheduler.
|
|
20
|
-
|
|
18
|
+
from memos.mem_scheduler.schemas.monitor_schemas import (
|
|
19
|
+
MemoryMonitorItem,
|
|
20
|
+
MemoryMonitorManager,
|
|
21
|
+
QueryMonitorItem,
|
|
22
|
+
QueryMonitorQueue,
|
|
23
|
+
)
|
|
24
|
+
from memos.mem_scheduler.utils.misc_utils import extract_json_dict
|
|
25
|
+
from memos.memories.textual.tree import TreeTextMemory
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
logger = get_logger(__name__)
|
|
@@ -31,7 +36,8 @@ class SchedulerMonitor(BaseSchedulerModule):
|
|
|
31
36
|
|
|
32
37
|
# hyper-parameters
|
|
33
38
|
self.config: BaseSchedulerConfig = config
|
|
34
|
-
self.act_mem_update_interval = self.config.get("act_mem_update_interval",
|
|
39
|
+
self.act_mem_update_interval = self.config.get("act_mem_update_interval", 30)
|
|
40
|
+
self.query_trigger_interval = self.config.get("query_trigger_interval", 10)
|
|
35
41
|
|
|
36
42
|
# Partial Retention Strategy
|
|
37
43
|
self.partial_retention_number = 2
|
|
@@ -39,16 +45,39 @@ class SchedulerMonitor(BaseSchedulerModule):
|
|
|
39
45
|
self.activation_mem_monitor_capacity = DEFAULT_ACTIVATION_MEM_MONITOR_SIZE_LIMIT
|
|
40
46
|
|
|
41
47
|
# attributes
|
|
42
|
-
|
|
43
|
-
self.
|
|
48
|
+
# recording query_messages
|
|
49
|
+
self.query_monitors: QueryMonitorQueue[QueryMonitorItem] = QueryMonitorQueue(
|
|
50
|
+
maxsize=self.config.context_window_size
|
|
51
|
+
)
|
|
52
|
+
|
|
44
53
|
self.working_memory_monitors: dict[UserID, dict[MemCubeID, MemoryMonitorManager]] = {}
|
|
45
54
|
self.activation_memory_monitors: dict[UserID, dict[MemCubeID, MemoryMonitorManager]] = {}
|
|
46
55
|
|
|
47
56
|
# Lifecycle monitor
|
|
48
|
-
self.
|
|
57
|
+
self.last_activation_mem_update_time = datetime.min
|
|
58
|
+
self.last_query_consume_time = datetime.min
|
|
49
59
|
|
|
50
60
|
self._process_llm = process_llm
|
|
51
61
|
|
|
62
|
+
def extract_query_keywords(self, query: str) -> list:
|
|
63
|
+
"""Extracts core keywords from a user query based on specific semantic rules."""
|
|
64
|
+
prompt_name = "query_keywords_extraction"
|
|
65
|
+
prompt = self.build_prompt(
|
|
66
|
+
template_name=prompt_name,
|
|
67
|
+
query=query,
|
|
68
|
+
)
|
|
69
|
+
llm_response = self._process_llm.generate([{"role": "user", "content": prompt}])
|
|
70
|
+
try:
|
|
71
|
+
# Parse JSON output from LLM response
|
|
72
|
+
keywords = extract_json_dict(llm_response)
|
|
73
|
+
assert isinstance(keywords, list)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(
|
|
76
|
+
f"Failed to parse keywords from LLM response: {llm_response}. Error: {e!s}"
|
|
77
|
+
)
|
|
78
|
+
keywords = [query]
|
|
79
|
+
return keywords
|
|
80
|
+
|
|
52
81
|
def register_memory_manager_if_not_exists(
|
|
53
82
|
self,
|
|
54
83
|
user_id: str,
|
|
@@ -90,13 +119,15 @@ class SchedulerMonitor(BaseSchedulerModule):
|
|
|
90
119
|
f"mem_cube_id={mem_cube_id} in the provided memory_monitors dictionary"
|
|
91
120
|
)
|
|
92
121
|
|
|
93
|
-
def
|
|
122
|
+
def update_working_memory_monitors(
|
|
123
|
+
self,
|
|
124
|
+
new_working_memory_monitors: list[MemoryMonitorItem],
|
|
125
|
+
user_id: str,
|
|
126
|
+
mem_cube_id: str,
|
|
127
|
+
mem_cube: GeneralMemCube,
|
|
128
|
+
):
|
|
94
129
|
text_mem_base: TreeTextMemory = mem_cube.text_mem
|
|
95
|
-
|
|
96
|
-
if not isinstance(text_mem_base, TreeTextMemory):
|
|
97
|
-
logger.error("Not Implemented")
|
|
98
|
-
return
|
|
99
|
-
|
|
130
|
+
assert isinstance(text_mem_base, TreeTextMemory)
|
|
100
131
|
self.working_mem_monitor_capacity = min(
|
|
101
132
|
DEFAULT_WORKING_MEM_MONITOR_SIZE_LIMIT,
|
|
102
133
|
(
|
|
@@ -105,17 +136,6 @@ class SchedulerMonitor(BaseSchedulerModule):
|
|
|
105
136
|
),
|
|
106
137
|
)
|
|
107
138
|
|
|
108
|
-
self.update_working_memory_monitors(
|
|
109
|
-
user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
self.update_activation_memory_monitors(
|
|
113
|
-
user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
def update_working_memory_monitors(
|
|
117
|
-
self, user_id: str, mem_cube_id: str, mem_cube: GeneralMemCube
|
|
118
|
-
):
|
|
119
139
|
# register monitors
|
|
120
140
|
self.register_memory_manager_if_not_exists(
|
|
121
141
|
user_id=user_id,
|
|
@@ -124,14 +144,8 @@ class SchedulerMonitor(BaseSchedulerModule):
|
|
|
124
144
|
max_capacity=self.working_mem_monitor_capacity,
|
|
125
145
|
)
|
|
126
146
|
|
|
127
|
-
# === update working memory monitors ===
|
|
128
|
-
# Retrieve current working memory content
|
|
129
|
-
text_mem_base: TreeTextMemory = mem_cube.text_mem
|
|
130
|
-
working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
|
|
131
|
-
text_working_memory: list[str] = [w_m.memory for w_m in working_memory]
|
|
132
|
-
|
|
133
147
|
self.working_memory_monitors[user_id][mem_cube_id].update_memories(
|
|
134
|
-
|
|
148
|
+
new_memory_monitors=new_working_memory_monitors,
|
|
135
149
|
partial_retention_number=self.partial_retention_number,
|
|
136
150
|
)
|
|
137
151
|
|
|
@@ -149,16 +163,13 @@ class SchedulerMonitor(BaseSchedulerModule):
|
|
|
149
163
|
# Sort by importance_score in descending order and take top k
|
|
150
164
|
top_k_memories = sorted(
|
|
151
165
|
self.working_memory_monitors[user_id][mem_cube_id].memories,
|
|
152
|
-
key=lambda m: m.
|
|
166
|
+
key=lambda m: m.get_importance_score(weight_vector=DEFAULT_WEIGHT_VECTOR_FOR_RANKING),
|
|
153
167
|
reverse=True,
|
|
154
168
|
)[: self.activation_mem_monitor_capacity]
|
|
155
169
|
|
|
156
|
-
# Extract just the text from these memories
|
|
157
|
-
text_top_k_memories = [m.memory_text for m in top_k_memories]
|
|
158
|
-
|
|
159
170
|
# Update the activation memory monitors with these important memories
|
|
160
171
|
self.activation_memory_monitors[user_id][mem_cube_id].update_memories(
|
|
161
|
-
|
|
172
|
+
new_memory_monitors=top_k_memories,
|
|
162
173
|
partial_retention_number=self.partial_retention_number,
|
|
163
174
|
)
|
|
164
175
|
|
|
@@ -206,10 +217,10 @@ class SchedulerMonitor(BaseSchedulerModule):
|
|
|
206
217
|
)
|
|
207
218
|
return []
|
|
208
219
|
|
|
209
|
-
manager = monitor_dict[user_id][mem_cube_id]
|
|
220
|
+
manager: MemoryMonitorManager = monitor_dict[user_id][mem_cube_id]
|
|
210
221
|
# Sort memories by recording_count in descending order and return top_k items
|
|
211
|
-
|
|
212
|
-
sorted_text_memories = [m.memory_text for m in
|
|
222
|
+
sorted_memory_monitors = manager.get_sorted_mem_monitors(reverse=True)
|
|
223
|
+
sorted_text_memories = [m.memory_text for m in sorted_memory_monitors[:top_k]]
|
|
213
224
|
return sorted_text_memories
|
|
214
225
|
|
|
215
226
|
def get_monitors_info(self, user_id: str, mem_cube_id: str) -> dict[str, Any]:
|
|
@@ -4,13 +4,13 @@ import threading
|
|
|
4
4
|
import time
|
|
5
5
|
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from queue import Queue
|
|
8
7
|
|
|
9
8
|
from memos.configs.mem_scheduler import AuthConfig, RabbitMQConfig
|
|
10
9
|
from memos.dependency import require_python_package
|
|
11
10
|
from memos.log import get_logger
|
|
12
11
|
from memos.mem_scheduler.modules.base import BaseSchedulerModule
|
|
13
|
-
from memos.mem_scheduler.modules.
|
|
12
|
+
from memos.mem_scheduler.modules.misc import AutoDroppingQueue
|
|
13
|
+
from memos.mem_scheduler.schemas.general_schemas import DIRECT_EXCHANGE_TYPE, FANOUT_EXCHANGE_TYPE
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
logger = get_logger(__name__)
|
|
@@ -38,7 +38,9 @@ class RabbitMQSchedulerModule(BaseSchedulerModule):
|
|
|
38
38
|
|
|
39
39
|
# fixed params
|
|
40
40
|
self.rabbitmq_message_cache_max_size = 10 # Max 10 messages
|
|
41
|
-
self.rabbitmq_message_cache =
|
|
41
|
+
self.rabbitmq_message_cache = AutoDroppingQueue(
|
|
42
|
+
maxsize=self.rabbitmq_message_cache_max_size
|
|
43
|
+
)
|
|
42
44
|
self.rabbitmq_connection_attempts = 3 # Max retry attempts on connection failure
|
|
43
45
|
self.rabbitmq_retry_delay = 5 # Delay (seconds) between retries
|
|
44
46
|
self.rabbitmq_heartbeat = 60 # Heartbeat interval (seconds) for connectio
|
|
@@ -214,12 +216,12 @@ class RabbitMQSchedulerModule(BaseSchedulerModule):
|
|
|
214
216
|
def on_rabbitmq_message(self, channel, method, properties, body):
|
|
215
217
|
"""Handle incoming messages. Only for test."""
|
|
216
218
|
try:
|
|
217
|
-
print(f"Received message: {body.decode()}")
|
|
218
|
-
self.rabbitmq_message_cache.
|
|
219
|
-
print(f"message delivery_tag: {method.delivery_tag}")
|
|
219
|
+
print(f"Received message: {body.decode()}\n")
|
|
220
|
+
self.rabbitmq_message_cache.put({"properties": properties, "body": body})
|
|
221
|
+
print(f"message delivery_tag: {method.delivery_tag}\n")
|
|
220
222
|
channel.basic_ack(delivery_tag=method.delivery_tag)
|
|
221
223
|
except Exception as e:
|
|
222
|
-
logger.error(f"Message handling failed: {e}")
|
|
224
|
+
logger.error(f"Message handling failed: {e}", exc_info=True)
|
|
223
225
|
|
|
224
226
|
def wait_for_connection_ready(self):
|
|
225
227
|
start_time = time.time()
|
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
1
|
from memos.configs.mem_scheduler import BaseSchedulerConfig
|
|
4
|
-
from memos.dependency import require_python_package
|
|
5
2
|
from memos.llms.base import BaseLLM
|
|
6
3
|
from memos.log import get_logger
|
|
7
4
|
from memos.mem_cube.general import GeneralMemCube
|
|
8
5
|
from memos.mem_scheduler.modules.base import BaseSchedulerModule
|
|
9
|
-
from memos.mem_scheduler.
|
|
6
|
+
from memos.mem_scheduler.schemas.general_schemas import (
|
|
10
7
|
TreeTextMemory_SEARCH_METHOD,
|
|
11
8
|
)
|
|
12
|
-
from memos.mem_scheduler.utils import (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
is_all_english,
|
|
9
|
+
from memos.mem_scheduler.utils.filter_utils import (
|
|
10
|
+
filter_similar_memories,
|
|
11
|
+
filter_too_short_memories,
|
|
16
12
|
transform_name_to_key,
|
|
17
13
|
)
|
|
14
|
+
from memos.mem_scheduler.utils.misc_utils import (
|
|
15
|
+
extract_json_dict,
|
|
16
|
+
)
|
|
18
17
|
from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
|
|
19
18
|
|
|
20
19
|
|
|
@@ -25,19 +24,16 @@ class SchedulerRetriever(BaseSchedulerModule):
|
|
|
25
24
|
def __init__(self, process_llm: BaseLLM, config: BaseSchedulerConfig):
|
|
26
25
|
super().__init__()
|
|
27
26
|
|
|
28
|
-
self.config: BaseSchedulerConfig = config
|
|
29
|
-
self.process_llm = process_llm
|
|
30
|
-
|
|
31
27
|
# hyper-parameters
|
|
32
28
|
self.filter_similarity_threshold = 0.75
|
|
33
29
|
self.filter_min_length_threshold = 6
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
self.
|
|
31
|
+
self.config: BaseSchedulerConfig = config
|
|
32
|
+
self.process_llm = process_llm
|
|
37
33
|
|
|
38
34
|
def search(
|
|
39
35
|
self, query: str, mem_cube: GeneralMemCube, top_k: int, method=TreeTextMemory_SEARCH_METHOD
|
|
40
|
-
):
|
|
36
|
+
) -> list[TextualMemoryItem]:
|
|
41
37
|
"""Search in text memory with the given query.
|
|
42
38
|
|
|
43
39
|
Args:
|
|
@@ -66,203 +62,124 @@ class SchedulerRetriever(BaseSchedulerModule):
|
|
|
66
62
|
results = []
|
|
67
63
|
return results
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
self, text_memories: list[str], similarity_threshold: float = 0.75
|
|
76
|
-
) -> list[str]:
|
|
65
|
+
def rerank_memories(
|
|
66
|
+
self,
|
|
67
|
+
queries: list[str],
|
|
68
|
+
original_memories: list[str],
|
|
69
|
+
top_k: int,
|
|
70
|
+
) -> (list[str], bool):
|
|
77
71
|
"""
|
|
78
|
-
|
|
72
|
+
Rerank memories based on relevance to given queries using LLM.
|
|
79
73
|
|
|
80
74
|
Args:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
queries: List of query strings to determine relevance
|
|
76
|
+
original_memories: List of memory strings to be reranked
|
|
77
|
+
top_k: Number of top memories to return after reranking
|
|
84
78
|
|
|
85
79
|
Returns:
|
|
86
|
-
List of
|
|
87
|
-
"""
|
|
88
|
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
89
|
-
from sklearn.metrics.pairwise import cosine_similarity
|
|
90
|
-
|
|
91
|
-
if not text_memories:
|
|
92
|
-
logging.warning("Received empty memories list - nothing to filter")
|
|
93
|
-
return []
|
|
94
|
-
|
|
95
|
-
for idx in range(len(text_memories)):
|
|
96
|
-
if not isinstance(text_memories[idx], str):
|
|
97
|
-
logger.error(
|
|
98
|
-
f"{text_memories[idx]} in memories is not a string,"
|
|
99
|
-
f" and now has been transformed to be a string."
|
|
100
|
-
)
|
|
101
|
-
text_memories[idx] = str(text_memories[idx])
|
|
102
|
-
|
|
103
|
-
try:
|
|
104
|
-
# Step 1: Vectorize texts using TF-IDF
|
|
105
|
-
vectorizer = TfidfVectorizer()
|
|
106
|
-
tfidf_matrix = vectorizer.fit_transform(text_memories)
|
|
107
|
-
|
|
108
|
-
# Step 2: Calculate pairwise similarity matrix
|
|
109
|
-
similarity_matrix = cosine_similarity(tfidf_matrix)
|
|
110
|
-
|
|
111
|
-
# Step 3: Identify duplicates
|
|
112
|
-
to_keep = []
|
|
113
|
-
removal_reasons = {}
|
|
114
|
-
|
|
115
|
-
for current_idx in range(len(text_memories)):
|
|
116
|
-
is_duplicate = False
|
|
117
|
-
|
|
118
|
-
# Compare with already kept memories
|
|
119
|
-
for kept_idx in to_keep:
|
|
120
|
-
similarity_score = similarity_matrix[current_idx, kept_idx]
|
|
121
|
-
|
|
122
|
-
if similarity_score > similarity_threshold:
|
|
123
|
-
is_duplicate = True
|
|
124
|
-
# Generate removal reason with sample text
|
|
125
|
-
removal_reasons[current_idx] = (
|
|
126
|
-
f"Memory too similar (score: {similarity_score:.2f}) to kept memory #{kept_idx}. "
|
|
127
|
-
f"Kept: '{text_memories[kept_idx][:100]}...' | "
|
|
128
|
-
f"Removed: '{text_memories[current_idx][:100]}...'"
|
|
129
|
-
)
|
|
130
|
-
logger.info(removal_reasons)
|
|
131
|
-
break
|
|
132
|
-
|
|
133
|
-
if not is_duplicate:
|
|
134
|
-
to_keep.append(current_idx)
|
|
135
|
-
|
|
136
|
-
# Return filtered memories
|
|
137
|
-
return [text_memories[i] for i in sorted(to_keep)]
|
|
138
|
-
|
|
139
|
-
except Exception as e:
|
|
140
|
-
logging.error(f"Error filtering memories: {e!s}")
|
|
141
|
-
return text_memories # Return original list if error occurs
|
|
142
|
-
|
|
143
|
-
def filter_too_short_memories(
|
|
144
|
-
self, text_memories: list[str], min_length_threshold: int = 20
|
|
145
|
-
) -> list[str]:
|
|
146
|
-
"""
|
|
147
|
-
Filters out text memories that fall below the minimum length requirement.
|
|
148
|
-
Handles both English (word count) and Chinese (character count) differently.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
text_memories: List of text memories to be filtered
|
|
152
|
-
min_length_threshold: Minimum length required to keep a memory.
|
|
153
|
-
For English: word count, for Chinese: character count.
|
|
80
|
+
List of reranked memory strings (length <= top_k)
|
|
154
81
|
|
|
155
|
-
|
|
156
|
-
|
|
82
|
+
Note:
|
|
83
|
+
If LLM reranking fails, falls back to original order (truncated to top_k)
|
|
157
84
|
"""
|
|
158
|
-
|
|
159
|
-
logging.debug("Empty memories list received in short memory filter")
|
|
160
|
-
return []
|
|
85
|
+
success_flag = False
|
|
161
86
|
|
|
162
|
-
|
|
163
|
-
removed_count = 0
|
|
87
|
+
logger.info(f"Starting memory reranking for {len(original_memories)} memories")
|
|
164
88
|
|
|
165
|
-
for memory
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if is_all_english(stripped_memory):
|
|
173
|
-
length = len(stripped_memory.split()) # Word count for English
|
|
174
|
-
elif is_all_chinese(stripped_memory):
|
|
175
|
-
length = len(stripped_memory) # Character count for Chinese
|
|
176
|
-
else:
|
|
177
|
-
logger.debug(
|
|
178
|
-
f"Mixed-language memory, using character count: {stripped_memory[:50]}..."
|
|
179
|
-
)
|
|
180
|
-
length = len(stripped_memory) # Default to character count
|
|
89
|
+
# Build LLM prompt for memory reranking
|
|
90
|
+
prompt = self.build_prompt(
|
|
91
|
+
"memory_reranking",
|
|
92
|
+
queries=[f"[0] {queries[0]}"],
|
|
93
|
+
current_order=[f"[{i}] {mem}" for i, mem in enumerate(original_memories)],
|
|
94
|
+
)
|
|
95
|
+
logger.debug(f"Generated reranking prompt: {prompt[:200]}...") # Log first 200 chars
|
|
181
96
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
removed_count += 1
|
|
97
|
+
# Get LLM response
|
|
98
|
+
response = self.process_llm.generate([{"role": "user", "content": prompt}])
|
|
99
|
+
logger.debug(f"Received LLM response: {response[:200]}...") # Log first 200 chars
|
|
186
100
|
|
|
187
|
-
|
|
101
|
+
try:
|
|
102
|
+
# Parse JSON response
|
|
103
|
+
response = extract_json_dict(response)
|
|
104
|
+
new_order = response["new_order"][:top_k]
|
|
105
|
+
text_memories_with_new_order = [original_memories[idx] for idx in new_order]
|
|
188
106
|
logger.info(
|
|
189
|
-
f"
|
|
190
|
-
f"
|
|
191
|
-
f"Total remaining: {len(filtered_memories)}"
|
|
107
|
+
f"Successfully reranked memories. Returning top {len(text_memories_with_new_order)} items;"
|
|
108
|
+
f"Ranking reasoning: {response['reasoning']}"
|
|
192
109
|
)
|
|
110
|
+
success_flag = True
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(
|
|
113
|
+
f"Failed to rerank memories with LLM. Exception: {e}. Raw response: {response} ",
|
|
114
|
+
exc_info=True,
|
|
115
|
+
)
|
|
116
|
+
text_memories_with_new_order = original_memories[:top_k]
|
|
117
|
+
success_flag = False
|
|
118
|
+
return text_memories_with_new_order, success_flag
|
|
193
119
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def replace_working_memory(
|
|
120
|
+
def process_and_rerank_memories(
|
|
197
121
|
self,
|
|
198
122
|
queries: list[str],
|
|
199
|
-
user_id: str,
|
|
200
|
-
mem_cube_id: str,
|
|
201
|
-
mem_cube: GeneralMemCube,
|
|
202
123
|
original_memory: list[TextualMemoryItem],
|
|
203
124
|
new_memory: list[TextualMemoryItem],
|
|
204
125
|
top_k: int = 10,
|
|
205
|
-
) ->
|
|
206
|
-
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if isinstance(text_mem_base, TreeTextMemory):
|
|
210
|
-
text_mem_base: TreeTextMemory = text_mem_base
|
|
211
|
-
combined_memory = original_memory + new_memory
|
|
212
|
-
memory_map = {
|
|
213
|
-
transform_name_to_key(name=mem_obj.memory): mem_obj for mem_obj in combined_memory
|
|
214
|
-
}
|
|
215
|
-
combined_text_memory = [transform_name_to_key(name=m.memory) for m in combined_memory]
|
|
216
|
-
|
|
217
|
-
# apply filters
|
|
218
|
-
filtered_combined_text_memory = self.filter_similar_memories(
|
|
219
|
-
text_memories=combined_text_memory,
|
|
220
|
-
similarity_threshold=self.filter_similarity_threshold,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
filtered_combined_text_memory = self.filter_too_short_memories(
|
|
224
|
-
text_memories=filtered_combined_text_memory,
|
|
225
|
-
min_length_threshold=self.filter_min_length_threshold,
|
|
226
|
-
)
|
|
126
|
+
) -> list[TextualMemoryItem] | None:
|
|
127
|
+
"""
|
|
128
|
+
Process and rerank memory items by combining original and new memories,
|
|
129
|
+
applying filters, and then reranking based on relevance to queries.
|
|
227
130
|
|
|
228
|
-
|
|
131
|
+
Args:
|
|
132
|
+
queries: List of query strings to rerank memories against
|
|
133
|
+
original_memory: List of original TextualMemoryItem objects
|
|
134
|
+
new_memory: List of new TextualMemoryItem objects to merge
|
|
135
|
+
top_k: Maximum number of memories to return after reranking
|
|
229
136
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
137
|
+
Returns:
|
|
138
|
+
List of reranked TextualMemoryItem objects, or None if processing fails
|
|
139
|
+
"""
|
|
140
|
+
# Combine original and new memories into a single list
|
|
141
|
+
combined_memory = original_memory + new_memory
|
|
142
|
+
|
|
143
|
+
# Create a mapping from normalized text to memory objects
|
|
144
|
+
memory_map = {
|
|
145
|
+
transform_name_to_key(name=mem_obj.memory): mem_obj for mem_obj in combined_memory
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Extract normalized text representations from all memory items
|
|
149
|
+
combined_text_memory = [m.memory for m in combined_memory]
|
|
150
|
+
|
|
151
|
+
# Apply similarity filter to remove overly similar memories
|
|
152
|
+
filtered_combined_text_memory = filter_similar_memories(
|
|
153
|
+
text_memories=combined_text_memory,
|
|
154
|
+
similarity_threshold=self.filter_similarity_threshold,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Apply length filter to remove memories that are too short
|
|
158
|
+
filtered_combined_text_memory = filter_too_short_memories(
|
|
159
|
+
text_memories=filtered_combined_text_memory,
|
|
160
|
+
min_length_threshold=self.filter_min_length_threshold,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Ensure uniqueness of memory texts using dictionary keys (preserves order)
|
|
164
|
+
unique_memory = list(dict.fromkeys(filtered_combined_text_memory))
|
|
165
|
+
|
|
166
|
+
# Rerank the filtered memories based on relevance to the queries
|
|
167
|
+
text_memories_with_new_order, success_flag = self.rerank_memories(
|
|
168
|
+
queries=queries,
|
|
169
|
+
original_memories=unique_memory,
|
|
170
|
+
top_k=top_k,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Map reranked text entries back to their original memory objects
|
|
174
|
+
memories_with_new_order = []
|
|
175
|
+
for text in text_memories_with_new_order:
|
|
176
|
+
normalized_text = transform_name_to_key(name=text)
|
|
177
|
+
if normalized_text in memory_map: # Ensure correct key matching
|
|
178
|
+
memories_with_new_order.append(memory_map[normalized_text])
|
|
179
|
+
else:
|
|
180
|
+
logger.warning(
|
|
181
|
+
f"Memory text not found in memory map. text: {text};\n"
|
|
182
|
+
f"Keys of memory_map: {memory_map.keys()}"
|
|
236
183
|
)
|
|
237
|
-
response = self.process_llm.generate([{"role": "user", "content": prompt}])
|
|
238
|
-
response = extract_json_dict(response)
|
|
239
|
-
text_memories_with_new_order = response.get("new_order", [])[:top_k]
|
|
240
|
-
except Exception as e:
|
|
241
|
-
logger.error(f"Fail to rerank with LLM, Exeption: {e}.", exc_info=True)
|
|
242
|
-
text_memories_with_new_order = unique_memory[:top_k]
|
|
243
|
-
|
|
244
|
-
memories_with_new_order = []
|
|
245
|
-
for text in text_memories_with_new_order:
|
|
246
|
-
normalized_text = transform_name_to_key(name=text)
|
|
247
|
-
if text in memory_map:
|
|
248
|
-
memories_with_new_order.append(memory_map[normalized_text])
|
|
249
|
-
else:
|
|
250
|
-
logger.warning(
|
|
251
|
-
f"Memory text not found in memory map. text: {text}; keys of memory_map: {memory_map.keys()}"
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
text_mem_base.replace_working_memory(memories_with_new_order)
|
|
255
|
-
logger.info(
|
|
256
|
-
f"The working memory has been replaced with {len(memories_with_new_order)} new memories."
|
|
257
|
-
)
|
|
258
|
-
self.log_working_memory_replacement(
|
|
259
|
-
original_memory=original_memory,
|
|
260
|
-
new_memory=memories_with_new_order,
|
|
261
|
-
user_id=user_id,
|
|
262
|
-
mem_cube_id=mem_cube_id,
|
|
263
|
-
mem_cube=mem_cube,
|
|
264
|
-
)
|
|
265
|
-
else:
|
|
266
|
-
logger.error("memory_base is not supported")
|
|
267
184
|
|
|
268
|
-
return memories_with_new_order
|
|
185
|
+
return memories_with_new_order, success_flag
|