MemoryOS 1.0.0__py3-none-any.whl → 1.1.1__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-1.0.0.dist-info → memoryos-1.1.1.dist-info}/METADATA +8 -2
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/RECORD +92 -69
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/WHEEL +1 -1
- memos/__init__.py +1 -1
- memos/api/client.py +109 -0
- memos/api/config.py +35 -8
- memos/api/context/dependencies.py +15 -66
- memos/api/middleware/request_context.py +63 -0
- memos/api/product_api.py +5 -2
- memos/api/product_models.py +107 -16
- memos/api/routers/product_router.py +62 -19
- memos/api/start_api.py +13 -0
- memos/configs/graph_db.py +4 -0
- memos/configs/mem_scheduler.py +38 -3
- memos/configs/memory.py +13 -0
- memos/configs/reranker.py +18 -0
- memos/context/context.py +255 -0
- memos/embedders/factory.py +2 -0
- memos/graph_dbs/base.py +4 -2
- memos/graph_dbs/nebular.py +368 -223
- memos/graph_dbs/neo4j.py +49 -13
- memos/graph_dbs/neo4j_community.py +13 -3
- memos/llms/factory.py +2 -0
- memos/llms/openai.py +74 -2
- memos/llms/vllm.py +2 -0
- memos/log.py +128 -4
- memos/mem_cube/general.py +3 -1
- memos/mem_os/core.py +89 -23
- memos/mem_os/main.py +3 -6
- memos/mem_os/product.py +418 -154
- memos/mem_os/utils/reference_utils.py +20 -0
- memos/mem_reader/factory.py +2 -0
- memos/mem_reader/simple_struct.py +204 -82
- memos/mem_scheduler/analyzer/__init__.py +0 -0
- memos/mem_scheduler/analyzer/mos_for_test_scheduler.py +569 -0
- memos/mem_scheduler/analyzer/scheduler_for_eval.py +280 -0
- memos/mem_scheduler/base_scheduler.py +126 -56
- memos/mem_scheduler/general_modules/dispatcher.py +2 -2
- memos/mem_scheduler/general_modules/misc.py +99 -1
- memos/mem_scheduler/general_modules/scheduler_logger.py +17 -11
- memos/mem_scheduler/general_scheduler.py +40 -88
- memos/mem_scheduler/memory_manage_modules/__init__.py +5 -0
- memos/mem_scheduler/memory_manage_modules/memory_filter.py +308 -0
- memos/mem_scheduler/{general_modules → memory_manage_modules}/retriever.py +34 -7
- memos/mem_scheduler/monitors/dispatcher_monitor.py +9 -8
- memos/mem_scheduler/monitors/general_monitor.py +119 -39
- memos/mem_scheduler/optimized_scheduler.py +124 -0
- memos/mem_scheduler/orm_modules/__init__.py +0 -0
- memos/mem_scheduler/orm_modules/base_model.py +635 -0
- memos/mem_scheduler/orm_modules/monitor_models.py +261 -0
- memos/mem_scheduler/scheduler_factory.py +2 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +96 -29
- memos/mem_scheduler/utils/config_utils.py +100 -0
- memos/mem_scheduler/utils/db_utils.py +33 -0
- memos/mem_scheduler/utils/filter_utils.py +1 -1
- memos/mem_scheduler/webservice_modules/__init__.py +0 -0
- memos/mem_user/mysql_user_manager.py +4 -2
- memos/memories/activation/kv.py +2 -1
- memos/memories/textual/item.py +96 -17
- memos/memories/textual/naive.py +1 -1
- memos/memories/textual/tree.py +57 -3
- memos/memories/textual/tree_text_memory/organize/handler.py +4 -2
- memos/memories/textual/tree_text_memory/organize/manager.py +28 -14
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +1 -2
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +75 -23
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +10 -6
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -2
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +119 -21
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +172 -44
- memos/memories/textual/tree_text_memory/retrieve/utils.py +6 -4
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +5 -4
- memos/memos_tools/notification_utils.py +46 -0
- memos/memos_tools/singleton.py +174 -0
- memos/memos_tools/thread_safe_dict.py +22 -0
- memos/memos_tools/thread_safe_dict_segment.py +382 -0
- memos/parsers/factory.py +2 -0
- memos/reranker/__init__.py +4 -0
- memos/reranker/base.py +24 -0
- memos/reranker/concat.py +59 -0
- memos/reranker/cosine_local.py +96 -0
- memos/reranker/factory.py +48 -0
- memos/reranker/http_bge.py +312 -0
- memos/reranker/noop.py +16 -0
- memos/templates/mem_reader_prompts.py +289 -40
- memos/templates/mem_scheduler_prompts.py +242 -0
- memos/templates/mos_prompts.py +133 -60
- memos/types.py +4 -1
- memos/api/context/context.py +0 -147
- memos/mem_scheduler/mos_for_test_scheduler.py +0 -146
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info}/entry_points.txt +0 -0
- {memoryos-1.0.0.dist-info → memoryos-1.1.1.dist-info/licenses}/LICENSE +0 -0
- /memos/mem_scheduler/{general_modules → webservice_modules}/rabbitmq_service.py +0 -0
- /memos/mem_scheduler/{general_modules → webservice_modules}/redis_service.py +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
7
|
+
|
|
8
|
+
from memos.log import get_logger
|
|
9
|
+
from memos.mem_scheduler.general_scheduler import GeneralScheduler
|
|
10
|
+
from memos.mem_scheduler.schemas.general_schemas import (
|
|
11
|
+
DEFAULT_MAX_QUERY_KEY_WORDS,
|
|
12
|
+
UserID,
|
|
13
|
+
)
|
|
14
|
+
from memos.mem_scheduler.schemas.monitor_schemas import QueryMonitorItem
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from memos.memories.textual.tree import TextualMemoryItem
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SchedulerForEval(GeneralScheduler):
|
|
25
|
+
"""
|
|
26
|
+
A scheduler class that inherits from GeneralScheduler and provides evaluation-specific functionality.
|
|
27
|
+
This class extends GeneralScheduler with evaluation methods.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Class variable to store timing information for all instances
|
|
31
|
+
timer_cache: ClassVar[dict[str, dict[str, Any]]] = {}
|
|
32
|
+
|
|
33
|
+
def __init__(self, config):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the SchedulerForEval with the same configuration as GeneralScheduler.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: Configuration object for the scheduler
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(config)
|
|
41
|
+
# Initialize instance timer_cache
|
|
42
|
+
self.timer_cache = {}
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def time_it(func_name: str | None = None):
|
|
46
|
+
"""
|
|
47
|
+
Static method decorator to measure function execution time and store in timer_cache.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
func_name: Custom name for the function in timer_cache. If None, uses function.__name__
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def decorator(func):
|
|
54
|
+
@wraps(func)
|
|
55
|
+
def wrapper(self, *args, **kwargs):
|
|
56
|
+
# Get function name
|
|
57
|
+
name = func_name or func.__name__
|
|
58
|
+
|
|
59
|
+
# Start timing
|
|
60
|
+
start_time = time.time()
|
|
61
|
+
result = func(self, *args, **kwargs)
|
|
62
|
+
end_time = time.time()
|
|
63
|
+
|
|
64
|
+
# Calculate execution time
|
|
65
|
+
exec_time = end_time - start_time
|
|
66
|
+
|
|
67
|
+
# Format time as HH:MM:SS.mmm
|
|
68
|
+
hours = int(exec_time // 3600)
|
|
69
|
+
minutes = int((exec_time % 3600) // 60)
|
|
70
|
+
seconds = exec_time % 60
|
|
71
|
+
|
|
72
|
+
if hours > 0:
|
|
73
|
+
time_str = f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"
|
|
74
|
+
else:
|
|
75
|
+
time_str = f"{minutes:02d}:{seconds:06.3f}"
|
|
76
|
+
|
|
77
|
+
# Store in timer_cache
|
|
78
|
+
if not hasattr(self, "timer_cache"):
|
|
79
|
+
self.timer_cache = {}
|
|
80
|
+
|
|
81
|
+
self.timer_cache[name] = {
|
|
82
|
+
"time_str": time_str,
|
|
83
|
+
"seconds": exec_time,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.info(f"{name} executed in {time_str}")
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
return wrapper
|
|
90
|
+
|
|
91
|
+
return decorator
|
|
92
|
+
|
|
93
|
+
def get_timer_summary(self) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Get a summary of all timed functions.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Formatted string with timing information
|
|
99
|
+
"""
|
|
100
|
+
if not self.timer_cache:
|
|
101
|
+
return "No timing data available."
|
|
102
|
+
|
|
103
|
+
summary = "=== Timing Summary ===\n"
|
|
104
|
+
for func_name, data in self.timer_cache.items():
|
|
105
|
+
summary += f"{func_name}: {data['time_str']} (at {data['timestamp']})\n"
|
|
106
|
+
|
|
107
|
+
return summary
|
|
108
|
+
|
|
109
|
+
def clear_timer_cache(self):
|
|
110
|
+
"""Clear the timer cache."""
|
|
111
|
+
self.timer_cache.clear()
|
|
112
|
+
|
|
113
|
+
@time_it("update_working_memory")
|
|
114
|
+
def update_working_memory_for_eval(
|
|
115
|
+
self, query: str, user_id: UserID | str, top_k: int
|
|
116
|
+
) -> list[str]:
|
|
117
|
+
"""
|
|
118
|
+
Update working memory based on query and return the updated memory list.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
query: The query string
|
|
122
|
+
user_id: User identifier
|
|
123
|
+
top_k: Number of top memories to return
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
List of memory strings from updated working memory
|
|
127
|
+
"""
|
|
128
|
+
self.monitor.register_query_monitor_if_not_exists(
|
|
129
|
+
user_id=user_id, mem_cube_id=self.current_mem_cube_id
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
query_keywords = self.monitor.extract_query_keywords(query=query)
|
|
133
|
+
logger.info(f'Extract keywords "{query_keywords}" from query "{query}"')
|
|
134
|
+
|
|
135
|
+
item = QueryMonitorItem(
|
|
136
|
+
user_id=user_id,
|
|
137
|
+
mem_cube_id=self.current_mem_cube_id,
|
|
138
|
+
query_text=query,
|
|
139
|
+
keywords=query_keywords,
|
|
140
|
+
max_keywords=DEFAULT_MAX_QUERY_KEY_WORDS,
|
|
141
|
+
)
|
|
142
|
+
query_db_manager = self.monitor.query_monitors[user_id][self.current_mem_cube_id]
|
|
143
|
+
query_db_manager.obj.put(item=item)
|
|
144
|
+
# Sync with database after adding new item
|
|
145
|
+
query_db_manager.sync_with_orm()
|
|
146
|
+
logger.debug(f"Queries in monitor are {query_db_manager.obj.get_queries_with_timesort()}.")
|
|
147
|
+
|
|
148
|
+
queries = [query]
|
|
149
|
+
|
|
150
|
+
# recall
|
|
151
|
+
mem_cube = self.current_mem_cube
|
|
152
|
+
text_mem_base = mem_cube.text_mem
|
|
153
|
+
|
|
154
|
+
cur_working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
|
|
155
|
+
text_working_memory: list[str] = [w_m.memory for w_m in cur_working_memory]
|
|
156
|
+
intent_result = self.monitor.detect_intent(
|
|
157
|
+
q_list=queries, text_working_memory=text_working_memory
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if intent_result["trigger_retrieval"]:
|
|
161
|
+
missing_evidences = intent_result["missing_evidences"]
|
|
162
|
+
num_evidence = len(missing_evidences)
|
|
163
|
+
k_per_evidence = max(1, top_k // max(1, num_evidence))
|
|
164
|
+
new_candidates = []
|
|
165
|
+
for item in missing_evidences:
|
|
166
|
+
logger.info(f"missing_evidences: {item}")
|
|
167
|
+
results: list[TextualMemoryItem] = self.retriever.search(
|
|
168
|
+
query=item,
|
|
169
|
+
mem_cube=mem_cube,
|
|
170
|
+
top_k=k_per_evidence,
|
|
171
|
+
method=self.search_method,
|
|
172
|
+
)
|
|
173
|
+
logger.info(
|
|
174
|
+
f"search results for {missing_evidences}: {[one.memory for one in results]}"
|
|
175
|
+
)
|
|
176
|
+
new_candidates.extend(results)
|
|
177
|
+
logger.info(
|
|
178
|
+
f"missing_evidences: {missing_evidences} and get {len(new_candidates)} new candidate memories."
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
new_candidates = []
|
|
182
|
+
logger.info(f"intent_result: {intent_result}. not triggered")
|
|
183
|
+
|
|
184
|
+
# rerank
|
|
185
|
+
new_order_working_memory = self.replace_working_memory(
|
|
186
|
+
user_id=user_id,
|
|
187
|
+
mem_cube_id=self.current_mem_cube_id,
|
|
188
|
+
mem_cube=self.current_mem_cube,
|
|
189
|
+
original_memory=cur_working_memory,
|
|
190
|
+
new_memory=new_candidates,
|
|
191
|
+
)
|
|
192
|
+
new_order_working_memory = new_order_working_memory[:top_k]
|
|
193
|
+
logger.info(f"size of new_order_working_memory: {len(new_order_working_memory)}")
|
|
194
|
+
|
|
195
|
+
return [m.memory for m in new_order_working_memory]
|
|
196
|
+
|
|
197
|
+
@time_it("memory_answer_ability")
|
|
198
|
+
def evaluate_memory_answer_ability(
|
|
199
|
+
self, query: str, memory_texts: list[str], top_k: int = 100
|
|
200
|
+
) -> bool:
|
|
201
|
+
"""
|
|
202
|
+
Use LLM to evaluate whether the given memories can answer the query.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
query: The query string to evaluate
|
|
206
|
+
memory_texts: List of memory texts to check against
|
|
207
|
+
top_k: Maximum number of memories to consider for evaluation
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Boolean indicating whether the memories can answer the query
|
|
211
|
+
"""
|
|
212
|
+
# Limit the number of memories to evaluate
|
|
213
|
+
limited_memories = memory_texts[:top_k] if memory_texts else []
|
|
214
|
+
|
|
215
|
+
# Build prompt using the template
|
|
216
|
+
prompt = self.monitor.build_prompt(
|
|
217
|
+
template_name="memory_answer_ability_evaluation",
|
|
218
|
+
query=query,
|
|
219
|
+
memory_list="\n".join([f"- {memory}" for memory in limited_memories])
|
|
220
|
+
if limited_memories
|
|
221
|
+
else "No memories available",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Use the process LLM to generate response
|
|
225
|
+
response = self.monitor._process_llm.generate([{"role": "user", "content": prompt}])
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# Extract JSON response
|
|
229
|
+
from memos.mem_scheduler.utils.misc_utils import extract_json_dict
|
|
230
|
+
|
|
231
|
+
result = extract_json_dict(response)
|
|
232
|
+
|
|
233
|
+
# Validate response structure
|
|
234
|
+
if "result" in result:
|
|
235
|
+
logger.info(
|
|
236
|
+
f"Memory answer ability evaluation result: {result['result']}, reason: {result.get('reason', 'No reason provided')}"
|
|
237
|
+
)
|
|
238
|
+
return result["result"]
|
|
239
|
+
else:
|
|
240
|
+
logger.warning(f"Invalid response structure from LLM: {result}")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(
|
|
245
|
+
f"Failed to parse LLM response for memory answer ability evaluation: {response}. Error: {e}"
|
|
246
|
+
)
|
|
247
|
+
# Fallback: return False if we can't determine answer ability
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
@time_it("search_for_eval")
|
|
251
|
+
def search_for_eval(
|
|
252
|
+
self, query: str, user_id: UserID | str, top_k: int, scheduler_flag: bool = True
|
|
253
|
+
) -> list[str]:
|
|
254
|
+
"""
|
|
255
|
+
Original search_for_eval function refactored to use the new decomposed functions.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
query: The query string
|
|
259
|
+
user_id: User identifier
|
|
260
|
+
top_k: Number of top memories to return
|
|
261
|
+
scheduler_flag: Whether to update working memory or just evaluate
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Tuple of (memory_list, can_answer_boolean)
|
|
265
|
+
"""
|
|
266
|
+
if not scheduler_flag:
|
|
267
|
+
# Get current working memory without updating
|
|
268
|
+
mem_cube = self.current_mem_cube
|
|
269
|
+
text_mem_base = mem_cube.text_mem
|
|
270
|
+
cur_working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
|
|
271
|
+
text_working_memory: list[str] = [w_m.memory for w_m in cur_working_memory]
|
|
272
|
+
|
|
273
|
+
return text_working_memory
|
|
274
|
+
else:
|
|
275
|
+
# Update working memory and get the result
|
|
276
|
+
updated_memories = self.update_working_memory_for_eval(
|
|
277
|
+
query=query, user_id=user_id, top_k=top_k
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return updated_memories
|
|
@@ -5,16 +5,16 @@ import time
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from sqlalchemy.engine import Engine
|
|
9
|
+
|
|
8
10
|
from memos.configs.mem_scheduler import AuthConfig, BaseSchedulerConfig
|
|
9
11
|
from memos.llms.base import BaseLLM
|
|
10
12
|
from memos.log import get_logger
|
|
11
13
|
from memos.mem_cube.general import GeneralMemCube
|
|
12
14
|
from memos.mem_scheduler.general_modules.dispatcher import SchedulerDispatcher
|
|
13
15
|
from memos.mem_scheduler.general_modules.misc import AutoDroppingQueue as Queue
|
|
14
|
-
from memos.mem_scheduler.general_modules.rabbitmq_service import RabbitMQSchedulerModule
|
|
15
|
-
from memos.mem_scheduler.general_modules.redis_service import RedisSchedulerModule
|
|
16
|
-
from memos.mem_scheduler.general_modules.retriever import SchedulerRetriever
|
|
17
16
|
from memos.mem_scheduler.general_modules.scheduler_logger import SchedulerLoggerModule
|
|
17
|
+
from memos.mem_scheduler.memory_manage_modules.retriever import SchedulerRetriever
|
|
18
18
|
from memos.mem_scheduler.monitors.dispatcher_monitor import SchedulerDispatcherMonitor
|
|
19
19
|
from memos.mem_scheduler.monitors.general_monitor import SchedulerGeneralMonitor
|
|
20
20
|
from memos.mem_scheduler.schemas.general_schemas import (
|
|
@@ -33,6 +33,8 @@ from memos.mem_scheduler.schemas.monitor_schemas import MemoryMonitorItem
|
|
|
33
33
|
from memos.mem_scheduler.utils.filter_utils import (
|
|
34
34
|
transform_name_to_key,
|
|
35
35
|
)
|
|
36
|
+
from memos.mem_scheduler.webservice_modules.rabbitmq_service import RabbitMQSchedulerModule
|
|
37
|
+
from memos.mem_scheduler.webservice_modules.redis_service import RedisSchedulerModule
|
|
36
38
|
from memos.memories.activation.kv import KVCacheMemory
|
|
37
39
|
from memos.memories.activation.vllmkv import VLLMKVCacheItem, VLLMKVCacheMemory
|
|
38
40
|
from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
|
|
@@ -62,6 +64,7 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
62
64
|
)
|
|
63
65
|
|
|
64
66
|
self.retriever: SchedulerRetriever | None = None
|
|
67
|
+
self.db_engine: Engine | None = None
|
|
65
68
|
self.monitor: SchedulerGeneralMonitor | None = None
|
|
66
69
|
self.dispatcher_monitor: SchedulerDispatcherMonitor | None = None
|
|
67
70
|
self.dispatcher = SchedulerDispatcher(
|
|
@@ -70,12 +73,15 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
70
73
|
)
|
|
71
74
|
|
|
72
75
|
# internal message queue
|
|
73
|
-
self.
|
|
76
|
+
self.max_internal_message_queue_size = self.config.get(
|
|
77
|
+
"max_internal_message_queue_size", 100
|
|
78
|
+
)
|
|
74
79
|
self.memos_message_queue: Queue[ScheduleMessageItem] = Queue(
|
|
75
|
-
maxsize=self.
|
|
80
|
+
maxsize=self.max_internal_message_queue_size
|
|
76
81
|
)
|
|
82
|
+
self.max_web_log_queue_size = self.config.get("max_web_log_queue_size", 50)
|
|
77
83
|
self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue(
|
|
78
|
-
maxsize=self.
|
|
84
|
+
maxsize=self.max_web_log_queue_size
|
|
79
85
|
)
|
|
80
86
|
self._consumer_thread = None # Reference to our consumer thread
|
|
81
87
|
self._running = False
|
|
@@ -92,34 +98,57 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
92
98
|
self.auth_config = None
|
|
93
99
|
self.rabbitmq_config = None
|
|
94
100
|
|
|
95
|
-
def initialize_modules(
|
|
101
|
+
def initialize_modules(
|
|
102
|
+
self,
|
|
103
|
+
chat_llm: BaseLLM,
|
|
104
|
+
process_llm: BaseLLM | None = None,
|
|
105
|
+
db_engine: Engine | None = None,
|
|
106
|
+
):
|
|
96
107
|
if process_llm is None:
|
|
97
108
|
process_llm = chat_llm
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
try:
|
|
111
|
+
# initialize submodules
|
|
112
|
+
self.chat_llm = chat_llm
|
|
113
|
+
self.process_llm = process_llm
|
|
114
|
+
self.db_engine = db_engine
|
|
115
|
+
self.monitor = SchedulerGeneralMonitor(
|
|
116
|
+
process_llm=self.process_llm, config=self.config, db_engine=self.db_engine
|
|
117
|
+
)
|
|
118
|
+
self.db_engine = self.monitor.db_engine
|
|
119
|
+
self.dispatcher_monitor = SchedulerDispatcherMonitor(config=self.config)
|
|
120
|
+
self.retriever = SchedulerRetriever(process_llm=self.process_llm, config=self.config)
|
|
121
|
+
|
|
122
|
+
if self.enable_parallel_dispatch:
|
|
123
|
+
self.dispatcher_monitor.initialize(dispatcher=self.dispatcher)
|
|
124
|
+
self.dispatcher_monitor.start()
|
|
125
|
+
|
|
126
|
+
# initialize with auth_config
|
|
127
|
+
if self.auth_config_path is not None and Path(self.auth_config_path).exists():
|
|
128
|
+
self.auth_config = AuthConfig.from_local_config(config_path=self.auth_config_path)
|
|
129
|
+
elif AuthConfig.default_config_exists():
|
|
130
|
+
self.auth_config = AuthConfig.from_local_config()
|
|
131
|
+
else:
|
|
132
|
+
self.auth_config = AuthConfig.from_local_env()
|
|
105
133
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# initialize with auth_cofig
|
|
111
|
-
if self.auth_config_path is not None and Path(self.auth_config_path).exists():
|
|
112
|
-
self.auth_config = AuthConfig.from_local_config(config_path=self.auth_config_path)
|
|
113
|
-
elif AuthConfig.default_config_exists():
|
|
114
|
-
self.auth_config = AuthConfig.from_local_config()
|
|
115
|
-
else:
|
|
116
|
-
self.auth_config = None
|
|
134
|
+
if self.auth_config is not None:
|
|
135
|
+
self.rabbitmq_config = self.auth_config.rabbitmq
|
|
136
|
+
self.initialize_rabbitmq(config=self.rabbitmq_config)
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
logger.debug("GeneralScheduler has been initialized")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Failed to initialize scheduler modules: {e}", exc_info=True)
|
|
141
|
+
# Clean up any partially initialized resources
|
|
142
|
+
self._cleanup_on_init_failure()
|
|
143
|
+
raise
|
|
121
144
|
|
|
122
|
-
|
|
145
|
+
def _cleanup_on_init_failure(self):
|
|
146
|
+
"""Clean up resources if initialization fails."""
|
|
147
|
+
try:
|
|
148
|
+
if hasattr(self, "dispatcher_monitor") and self.dispatcher_monitor is not None:
|
|
149
|
+
self.dispatcher_monitor.stop()
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.warning(f"Error during cleanup: {e}")
|
|
123
152
|
|
|
124
153
|
@property
|
|
125
154
|
def mem_cube(self) -> GeneralMemCube:
|
|
@@ -200,8 +229,11 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
200
229
|
text_mem_base: TreeTextMemory = text_mem_base
|
|
201
230
|
|
|
202
231
|
# process rerank memories with llm
|
|
203
|
-
|
|
204
|
-
|
|
232
|
+
query_db_manager = self.monitor.query_monitors[user_id][mem_cube_id]
|
|
233
|
+
# Sync with database to get latest query history
|
|
234
|
+
query_db_manager.sync_with_orm()
|
|
235
|
+
|
|
236
|
+
query_history = query_db_manager.obj.get_queries_with_timesort()
|
|
205
237
|
memories_with_new_order, rerank_success_flag = (
|
|
206
238
|
self.retriever.process_and_rerank_memories(
|
|
207
239
|
queries=query_history,
|
|
@@ -211,8 +243,27 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
211
243
|
)
|
|
212
244
|
)
|
|
213
245
|
|
|
214
|
-
#
|
|
215
|
-
|
|
246
|
+
# Filter completely unrelated memories according to query_history
|
|
247
|
+
logger.info(f"Filtering memories based on query history: {len(query_history)} queries")
|
|
248
|
+
filtered_memories, filter_success_flag = self.retriever.filter_unrelated_memories(
|
|
249
|
+
query_history=query_history,
|
|
250
|
+
memories=memories_with_new_order,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if filter_success_flag:
|
|
254
|
+
logger.info(
|
|
255
|
+
f"Memory filtering completed successfully. "
|
|
256
|
+
f"Filtered from {len(memories_with_new_order)} to {len(filtered_memories)} memories"
|
|
257
|
+
)
|
|
258
|
+
memories_with_new_order = filtered_memories
|
|
259
|
+
else:
|
|
260
|
+
logger.warning(
|
|
261
|
+
"Memory filtering failed - keeping all memories as fallback. "
|
|
262
|
+
f"Original count: {len(memories_with_new_order)}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Update working memory monitors
|
|
266
|
+
query_keywords = query_db_manager.obj.get_keywords_collections()
|
|
216
267
|
logger.info(
|
|
217
268
|
f"Processing {len(memories_with_new_order)} memories with {len(query_keywords)} query keywords"
|
|
218
269
|
)
|
|
@@ -235,7 +286,7 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
235
286
|
|
|
236
287
|
mem_monitors: list[MemoryMonitorItem] = self.monitor.working_memory_monitors[user_id][
|
|
237
288
|
mem_cube_id
|
|
238
|
-
].get_sorted_mem_monitors(reverse=True)
|
|
289
|
+
].obj.get_sorted_mem_monitors(reverse=True)
|
|
239
290
|
new_working_memories = [mem_monitor.tree_memory_item for mem_monitor in mem_monitors]
|
|
240
291
|
|
|
241
292
|
text_mem_base.replace_working_memory(memories=new_working_memories)
|
|
@@ -278,6 +329,7 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
278
329
|
new_text_memories = new_memories
|
|
279
330
|
else:
|
|
280
331
|
logger.error("Not Implemented.")
|
|
332
|
+
return
|
|
281
333
|
|
|
282
334
|
try:
|
|
283
335
|
if isinstance(mem_cube.act_mem, VLLMKVCacheMemory):
|
|
@@ -333,7 +385,9 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
333
385
|
)
|
|
334
386
|
|
|
335
387
|
except Exception as e:
|
|
336
|
-
logger.
|
|
388
|
+
logger.error(f"MOS-based activation memory update failed: {e}", exc_info=True)
|
|
389
|
+
# Re-raise the exception if it's critical for the operation
|
|
390
|
+
# For now, we'll continue execution but this should be reviewed
|
|
337
391
|
|
|
338
392
|
def update_activation_memory_periodically(
|
|
339
393
|
self,
|
|
@@ -358,7 +412,8 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
358
412
|
if (
|
|
359
413
|
user_id not in self.monitor.working_memory_monitors
|
|
360
414
|
or mem_cube_id not in self.monitor.working_memory_monitors[user_id]
|
|
361
|
-
or len(self.monitor.working_memory_monitors[user_id][mem_cube_id].memories)
|
|
415
|
+
or len(self.monitor.working_memory_monitors[user_id][mem_cube_id].obj.memories)
|
|
416
|
+
== 0
|
|
362
417
|
):
|
|
363
418
|
logger.warning(
|
|
364
419
|
"No memories found in working_memory_monitors, activation memory update is skipped"
|
|
@@ -369,9 +424,13 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
369
424
|
user_id=user_id, mem_cube_id=mem_cube_id, mem_cube=mem_cube
|
|
370
425
|
)
|
|
371
426
|
|
|
427
|
+
# Sync with database to get latest activation memories
|
|
428
|
+
activation_db_manager = self.monitor.activation_memory_monitors[user_id][
|
|
429
|
+
mem_cube_id
|
|
430
|
+
]
|
|
431
|
+
activation_db_manager.sync_with_orm()
|
|
372
432
|
new_activation_memories = [
|
|
373
|
-
m.memory_text
|
|
374
|
-
for m in self.monitor.activation_memory_monitors[user_id][mem_cube_id].memories
|
|
433
|
+
m.memory_text for m in activation_db_manager.obj.memories
|
|
375
434
|
]
|
|
376
435
|
|
|
377
436
|
logger.info(
|
|
@@ -412,6 +471,11 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
412
471
|
messages = [messages] # transform single message to list
|
|
413
472
|
|
|
414
473
|
for message in messages:
|
|
474
|
+
if not isinstance(message, ScheduleMessageItem):
|
|
475
|
+
error_msg = f"Invalid message type: {type(message)}, expected ScheduleMessageItem"
|
|
476
|
+
logger.error(error_msg)
|
|
477
|
+
raise TypeError(error_msg)
|
|
478
|
+
|
|
415
479
|
self.memos_message_queue.put(message)
|
|
416
480
|
logger.info(f"Submitted message: {message.label} - {message.content}")
|
|
417
481
|
|
|
@@ -427,6 +491,11 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
427
491
|
messages = [messages] # transform single message to list
|
|
428
492
|
|
|
429
493
|
for message in messages:
|
|
494
|
+
if not isinstance(message, ScheduleLogForWebItem):
|
|
495
|
+
error_msg = f"Invalid message type: {type(message)}, expected ScheduleLogForWebItem"
|
|
496
|
+
logger.error(error_msg)
|
|
497
|
+
raise TypeError(error_msg)
|
|
498
|
+
|
|
430
499
|
self._web_log_message_queue.put(message)
|
|
431
500
|
message_info = message.debug_info()
|
|
432
501
|
logger.debug(f"Submitted Scheduling log for web: {message_info}")
|
|
@@ -461,25 +530,26 @@ class BaseScheduler(RabbitMQSchedulerModule, RedisSchedulerModule, SchedulerLogg
|
|
|
461
530
|
"""
|
|
462
531
|
while self._running: # Use a running flag for graceful shutdown
|
|
463
532
|
try:
|
|
464
|
-
#
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
533
|
+
# Get all available messages at once (thread-safe approach)
|
|
534
|
+
messages = []
|
|
535
|
+
while True:
|
|
536
|
+
try:
|
|
537
|
+
# Use get_nowait() directly without empty() check to avoid race conditions
|
|
538
|
+
message = self.memos_message_queue.get_nowait()
|
|
539
|
+
messages.append(message)
|
|
540
|
+
except queue.Empty:
|
|
541
|
+
# No more messages available
|
|
542
|
+
break
|
|
543
|
+
|
|
544
|
+
if messages:
|
|
545
|
+
try:
|
|
546
|
+
self.dispatcher.dispatch(messages)
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.error(f"Error dispatching messages: {e!s}")
|
|
549
|
+
finally:
|
|
550
|
+
# Mark all messages as processed
|
|
551
|
+
for _ in messages:
|
|
552
|
+
self.memos_message_queue.task_done()
|
|
483
553
|
|
|
484
554
|
# Sleep briefly to prevent busy waiting
|
|
485
555
|
time.sleep(self._consume_interval) # Adjust interval as needed
|
|
@@ -2,8 +2,8 @@ import concurrent
|
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
6
5
|
|
|
6
|
+
from memos.context.context import ContextThreadPoolExecutor
|
|
7
7
|
from memos.log import get_logger
|
|
8
8
|
from memos.mem_scheduler.general_modules.base import BaseSchedulerModule
|
|
9
9
|
from memos.mem_scheduler.schemas.message_schemas import ScheduleMessageItem
|
|
@@ -33,7 +33,7 @@ class SchedulerDispatcher(BaseSchedulerModule):
|
|
|
33
33
|
self.enable_parallel_dispatch = enable_parallel_dispatch
|
|
34
34
|
self.thread_name_prefix = "dispatcher"
|
|
35
35
|
if self.enable_parallel_dispatch:
|
|
36
|
-
self.dispatcher_executor =
|
|
36
|
+
self.dispatcher_executor = ContextThreadPoolExecutor(
|
|
37
37
|
max_workers=self.max_workers, thread_name_prefix=self.thread_name_prefix
|
|
38
38
|
)
|
|
39
39
|
else:
|