MemoryOS 0.0.1__py3-none-any.whl → 0.1.13__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.1.13.dist-info/METADATA +288 -0
- memoryos-0.1.13.dist-info/RECORD +122 -0
- memos/__init__.py +20 -1
- memos/api/start_api.py +420 -0
- memos/chunkers/__init__.py +4 -0
- memos/chunkers/base.py +24 -0
- memos/chunkers/factory.py +22 -0
- memos/chunkers/sentence_chunker.py +35 -0
- memos/configs/__init__.py +0 -0
- memos/configs/base.py +82 -0
- memos/configs/chunker.py +45 -0
- memos/configs/embedder.py +53 -0
- memos/configs/graph_db.py +45 -0
- memos/configs/internet_retriever.py +81 -0
- memos/configs/llm.py +71 -0
- memos/configs/mem_chat.py +81 -0
- memos/configs/mem_cube.py +89 -0
- memos/configs/mem_os.py +74 -0
- memos/configs/mem_reader.py +53 -0
- memos/configs/mem_scheduler.py +78 -0
- memos/configs/memory.py +195 -0
- memos/configs/parser.py +38 -0
- memos/configs/utils.py +8 -0
- memos/configs/vec_db.py +64 -0
- memos/deprecation.py +262 -0
- memos/embedders/__init__.py +0 -0
- memos/embedders/base.py +15 -0
- memos/embedders/factory.py +23 -0
- memos/embedders/ollama.py +74 -0
- memos/embedders/sentence_transformer.py +40 -0
- memos/exceptions.py +30 -0
- memos/graph_dbs/__init__.py +0 -0
- memos/graph_dbs/base.py +215 -0
- memos/graph_dbs/factory.py +21 -0
- memos/graph_dbs/neo4j.py +827 -0
- memos/hello_world.py +97 -0
- memos/llms/__init__.py +0 -0
- memos/llms/base.py +16 -0
- memos/llms/factory.py +25 -0
- memos/llms/hf.py +231 -0
- memos/llms/ollama.py +82 -0
- memos/llms/openai.py +34 -0
- memos/llms/utils.py +14 -0
- memos/log.py +78 -0
- memos/mem_chat/__init__.py +0 -0
- memos/mem_chat/base.py +30 -0
- memos/mem_chat/factory.py +21 -0
- memos/mem_chat/simple.py +200 -0
- memos/mem_cube/__init__.py +0 -0
- memos/mem_cube/base.py +29 -0
- memos/mem_cube/general.py +146 -0
- memos/mem_cube/utils.py +24 -0
- memos/mem_os/client.py +5 -0
- memos/mem_os/core.py +819 -0
- memos/mem_os/main.py +503 -0
- memos/mem_os/product.py +89 -0
- memos/mem_reader/__init__.py +0 -0
- memos/mem_reader/base.py +27 -0
- memos/mem_reader/factory.py +21 -0
- memos/mem_reader/memory.py +298 -0
- memos/mem_reader/simple_struct.py +241 -0
- memos/mem_scheduler/__init__.py +0 -0
- memos/mem_scheduler/base_scheduler.py +164 -0
- memos/mem_scheduler/general_scheduler.py +305 -0
- memos/mem_scheduler/modules/__init__.py +0 -0
- memos/mem_scheduler/modules/base.py +74 -0
- memos/mem_scheduler/modules/dispatcher.py +103 -0
- memos/mem_scheduler/modules/monitor.py +82 -0
- memos/mem_scheduler/modules/redis_service.py +146 -0
- memos/mem_scheduler/modules/retriever.py +41 -0
- memos/mem_scheduler/modules/schemas.py +146 -0
- memos/mem_scheduler/scheduler_factory.py +21 -0
- memos/mem_scheduler/utils.py +26 -0
- memos/mem_user/user_manager.py +488 -0
- memos/memories/__init__.py +0 -0
- memos/memories/activation/__init__.py +0 -0
- memos/memories/activation/base.py +42 -0
- memos/memories/activation/item.py +25 -0
- memos/memories/activation/kv.py +232 -0
- memos/memories/base.py +19 -0
- memos/memories/factory.py +34 -0
- memos/memories/parametric/__init__.py +0 -0
- memos/memories/parametric/base.py +19 -0
- memos/memories/parametric/item.py +11 -0
- memos/memories/parametric/lora.py +41 -0
- memos/memories/textual/__init__.py +0 -0
- memos/memories/textual/base.py +89 -0
- memos/memories/textual/general.py +286 -0
- memos/memories/textual/item.py +167 -0
- memos/memories/textual/naive.py +185 -0
- memos/memories/textual/tree.py +321 -0
- memos/memories/textual/tree_text_memory/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/organize/manager.py +305 -0
- memos/memories/textual/tree_text_memory/retrieve/__init__.py +0 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +263 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +89 -0
- memos/memories/textual/tree_text_memory/retrieve/reasoner.py +61 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +158 -0
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +111 -0
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +13 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +208 -0
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +68 -0
- memos/memories/textual/tree_text_memory/retrieve/utils.py +48 -0
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +335 -0
- memos/parsers/__init__.py +0 -0
- memos/parsers/base.py +15 -0
- memos/parsers/factory.py +19 -0
- memos/parsers/markitdown.py +22 -0
- memos/settings.py +8 -0
- memos/templates/__init__.py +0 -0
- memos/templates/mem_reader_prompts.py +98 -0
- memos/templates/mem_scheduler_prompts.py +65 -0
- memos/templates/mos_prompts.py +63 -0
- memos/types.py +55 -0
- memos/vec_dbs/__init__.py +0 -0
- memos/vec_dbs/base.py +105 -0
- memos/vec_dbs/factory.py +21 -0
- memos/vec_dbs/item.py +43 -0
- memos/vec_dbs/qdrant.py +292 -0
- memoryos-0.0.1.dist-info/METADATA +0 -53
- memoryos-0.0.1.dist-info/RECORD +0 -5
- {memoryos-0.0.1.dist-info → memoryos-0.1.13.dist-info}/LICENSE +0 -0
- {memoryos-0.0.1.dist-info → memoryos-0.1.13.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import queue
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from queue import Queue
|
|
7
|
+
|
|
8
|
+
from memos.configs.mem_scheduler import BaseSchedulerConfig
|
|
9
|
+
from memos.llms.base import BaseLLM
|
|
10
|
+
from memos.log import get_logger
|
|
11
|
+
from memos.mem_scheduler.modules.dispatcher import SchedulerDispatcher
|
|
12
|
+
from memos.mem_scheduler.modules.redis_service import RedisSchedulerModule
|
|
13
|
+
from memos.mem_scheduler.modules.schemas import (
|
|
14
|
+
DEFAULT_CONSUME_INTERVAL_SECONDS,
|
|
15
|
+
DEFAULT_THREAD__POOL_MAX_WORKERS,
|
|
16
|
+
ScheduleLogForWebItem,
|
|
17
|
+
ScheduleMessageItem,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseScheduler(RedisSchedulerModule):
|
|
25
|
+
"""Base class for all mem_scheduler."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: BaseSchedulerConfig):
|
|
28
|
+
"""Initialize the scheduler with the given configuration."""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.config = config
|
|
31
|
+
self.max_workers = self.config.get(
|
|
32
|
+
"thread_pool_max_workers", DEFAULT_THREAD__POOL_MAX_WORKERS
|
|
33
|
+
)
|
|
34
|
+
self.retriever = None
|
|
35
|
+
self.monitor = None
|
|
36
|
+
self.enable_parallel_dispatch = self.config.get("enable_parallel_dispatch", False)
|
|
37
|
+
self.dispatcher = SchedulerDispatcher(
|
|
38
|
+
max_workers=self.max_workers, enable_parallel_dispatch=self.enable_parallel_dispatch
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# message queue
|
|
42
|
+
self.memos_message_queue: Queue[ScheduleMessageItem] = Queue()
|
|
43
|
+
self._web_log_message_queue: Queue[ScheduleLogForWebItem] = Queue()
|
|
44
|
+
self._consumer_thread = None # Reference to our consumer thread
|
|
45
|
+
self._running = False
|
|
46
|
+
self._consume_interval = self.config.get(
|
|
47
|
+
"consume_interval_seconds", DEFAULT_CONSUME_INTERVAL_SECONDS
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# others
|
|
51
|
+
self._current_user_id: str | None = None
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def initialize_modules(self, chat_llm: BaseLLM) -> None:
|
|
55
|
+
"""Initialize all necessary modules for the scheduler
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
chat_llm: The LLM instance to be used for chat interactions
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def submit_messages(self, messages: ScheduleMessageItem | list[ScheduleMessageItem]):
|
|
62
|
+
"""Submit multiple messages to the message queue."""
|
|
63
|
+
if isinstance(messages, ScheduleMessageItem):
|
|
64
|
+
messages = [messages] # transform single message to list
|
|
65
|
+
|
|
66
|
+
for message in messages:
|
|
67
|
+
self.memos_message_queue.put(message)
|
|
68
|
+
logger.info(f"Submitted message: {message.label} - {message.content}")
|
|
69
|
+
|
|
70
|
+
def _submit_web_logs(self, messages: ScheduleLogForWebItem | list[ScheduleLogForWebItem]):
|
|
71
|
+
if isinstance(messages, ScheduleLogForWebItem):
|
|
72
|
+
messages = [messages] # transform single message to list
|
|
73
|
+
|
|
74
|
+
for message in messages:
|
|
75
|
+
self._web_log_message_queue.put(message)
|
|
76
|
+
logger.info(
|
|
77
|
+
f"Submitted Scheduling log for web: {message.log_title} - {message.log_content}"
|
|
78
|
+
)
|
|
79
|
+
logger.debug(f"{len(messages)} submitted. {self._web_log_message_queue.qsize()} in queue.")
|
|
80
|
+
|
|
81
|
+
def get_web_log_messages(self) -> list[dict]:
|
|
82
|
+
"""
|
|
83
|
+
Retrieves all web log messages from the queue and returns them as a list of JSON-serializable dictionaries.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List[dict]: A list of dictionaries representing ScheduleLogForWebItem objects,
|
|
87
|
+
ready for JSON serialization. The list is ordered from oldest to newest.
|
|
88
|
+
"""
|
|
89
|
+
messages = []
|
|
90
|
+
|
|
91
|
+
# Process all items in the queue
|
|
92
|
+
while not self._web_log_message_queue.empty():
|
|
93
|
+
item = self._web_log_message_queue.get()
|
|
94
|
+
# Convert the ScheduleLogForWebItem to a dictionary and ensure datetime is serialized
|
|
95
|
+
item_dict = item.to_dict()
|
|
96
|
+
messages.append(item_dict)
|
|
97
|
+
return messages
|
|
98
|
+
|
|
99
|
+
def _message_consumer(self) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Continuously checks the queue for messages and dispatches them.
|
|
102
|
+
|
|
103
|
+
Runs in a dedicated thread to process messages at regular intervals.
|
|
104
|
+
"""
|
|
105
|
+
while self._running: # Use a running flag for graceful shutdown
|
|
106
|
+
try:
|
|
107
|
+
# Check if queue has messages (non-blocking)
|
|
108
|
+
if not self.memos_message_queue.empty():
|
|
109
|
+
# Get all available messages at once
|
|
110
|
+
messages = []
|
|
111
|
+
while not self.memos_message_queue.empty():
|
|
112
|
+
try:
|
|
113
|
+
messages.append(self.memos_message_queue.get_nowait())
|
|
114
|
+
except queue.Empty:
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
if messages:
|
|
118
|
+
try:
|
|
119
|
+
self.dispatcher.dispatch(messages)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error dispatching messages: {e!s}")
|
|
122
|
+
finally:
|
|
123
|
+
# Mark all messages as processed
|
|
124
|
+
for _ in messages:
|
|
125
|
+
self.memos_message_queue.task_done()
|
|
126
|
+
|
|
127
|
+
# Sleep briefly to prevent busy waiting
|
|
128
|
+
time.sleep(self._consume_interval) # Adjust interval as needed
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Unexpected error in message consumer: {e!s}")
|
|
132
|
+
time.sleep(self._consume_interval) # Prevent tight error loops
|
|
133
|
+
|
|
134
|
+
def start(self) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Start the message consumer thread.
|
|
137
|
+
|
|
138
|
+
Initializes and starts a daemon thread that will periodically
|
|
139
|
+
check for and process messages from the queue.
|
|
140
|
+
"""
|
|
141
|
+
if self._consumer_thread is not None and self._consumer_thread.is_alive():
|
|
142
|
+
logger.warning("Consumer thread is already running")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
self._running = True
|
|
146
|
+
self._consumer_thread = threading.Thread(
|
|
147
|
+
target=self._message_consumer,
|
|
148
|
+
daemon=True, # Allows program to exit even if thread is running
|
|
149
|
+
name="MessageConsumerThread",
|
|
150
|
+
)
|
|
151
|
+
self._consumer_thread.start()
|
|
152
|
+
logger.info("Message consumer thread started")
|
|
153
|
+
|
|
154
|
+
def stop(self) -> None:
|
|
155
|
+
"""Stop the consumer thread and clean up resources."""
|
|
156
|
+
if self._consumer_thread is None or not self._running:
|
|
157
|
+
logger.warning("Consumer thread is not running")
|
|
158
|
+
return
|
|
159
|
+
self._running = False
|
|
160
|
+
if self._consumer_thread.is_alive():
|
|
161
|
+
self._consumer_thread.join(timeout=5.0) # Wait up to 5 seconds
|
|
162
|
+
if self._consumer_thread.is_alive():
|
|
163
|
+
logger.warning("Consumer thread did not stop gracefully")
|
|
164
|
+
logger.info("Message consumer thread stopped")
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
from memos.configs.mem_scheduler import GeneralSchedulerConfig
|
|
6
|
+
from memos.llms.base import BaseLLM
|
|
7
|
+
from memos.log import get_logger
|
|
8
|
+
from memos.mem_cube.general import GeneralMemCube
|
|
9
|
+
from memos.mem_scheduler.base_scheduler import BaseScheduler
|
|
10
|
+
from memos.mem_scheduler.modules.monitor import SchedulerMonitor
|
|
11
|
+
from memos.mem_scheduler.modules.retriever import SchedulerRetriever
|
|
12
|
+
from memos.mem_scheduler.modules.schemas import (
|
|
13
|
+
ANSWER_LABEL,
|
|
14
|
+
DEFAULT_ACT_MEM_DUMP_PATH,
|
|
15
|
+
DEFAULT_ACTIVATION_MEM_SIZE,
|
|
16
|
+
NOT_INITIALIZED,
|
|
17
|
+
QUERY_LABEL,
|
|
18
|
+
ScheduleLogForWebItem,
|
|
19
|
+
ScheduleMessageItem,
|
|
20
|
+
TextMemory_SEARCH_METHOD,
|
|
21
|
+
TreeTextMemory_SEARCH_METHOD,
|
|
22
|
+
)
|
|
23
|
+
from memos.memories.textual.tree import TextualMemoryItem, TreeTextMemory
|
|
24
|
+
from memos.templates.mem_scheduler_prompts import MEMORY_ASSEMBLY_TEMPLATE
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GeneralScheduler(BaseScheduler):
|
|
31
|
+
def __init__(self, config: GeneralSchedulerConfig):
|
|
32
|
+
"""Initialize the scheduler with the given configuration."""
|
|
33
|
+
super().__init__(config)
|
|
34
|
+
self.top_k = self.config.get("top_k", 10)
|
|
35
|
+
self.top_n = self.config.get("top_n", 5)
|
|
36
|
+
self.act_mem_update_interval = self.config.get("act_mem_update_interval", 300)
|
|
37
|
+
self.context_window_size = self.config.get("context_window_size", 5)
|
|
38
|
+
self.activation_mem_size = self.config.get(
|
|
39
|
+
"activation_mem_size", DEFAULT_ACTIVATION_MEM_SIZE
|
|
40
|
+
)
|
|
41
|
+
self.act_mem_dump_path = self.config.get("act_mem_dump_path", DEFAULT_ACT_MEM_DUMP_PATH)
|
|
42
|
+
self.search_method = TextMemory_SEARCH_METHOD
|
|
43
|
+
self._last_activation_mem_update_time = 0.0
|
|
44
|
+
self.query_list = []
|
|
45
|
+
|
|
46
|
+
# register handlers
|
|
47
|
+
handlers = {
|
|
48
|
+
QUERY_LABEL: self._query_message_consume,
|
|
49
|
+
ANSWER_LABEL: self._answer_message_consume,
|
|
50
|
+
}
|
|
51
|
+
self.dispatcher.register_handlers(handlers)
|
|
52
|
+
|
|
53
|
+
def initialize_modules(self, chat_llm: BaseLLM):
|
|
54
|
+
self.chat_llm = chat_llm
|
|
55
|
+
self.monitor = SchedulerMonitor(
|
|
56
|
+
chat_llm=self.chat_llm, activation_mem_size=self.activation_mem_size
|
|
57
|
+
)
|
|
58
|
+
self.retriever = SchedulerRetriever(chat_llm=self.chat_llm)
|
|
59
|
+
logger.debug("GeneralScheduler has been initialized")
|
|
60
|
+
|
|
61
|
+
def _answer_message_consume(self, messages: list[ScheduleMessageItem]) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Process and handle answer trigger messages from the queue.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
messages: List of answer messages to process
|
|
67
|
+
"""
|
|
68
|
+
# TODO: This handler is not ready yet
|
|
69
|
+
logger.debug(f"Messages {messages} assigned to {ANSWER_LABEL} handler.")
|
|
70
|
+
for msg in messages:
|
|
71
|
+
if msg.label is not ANSWER_LABEL:
|
|
72
|
+
logger.error(f"_answer_message_consume is not designed for {msg.label}")
|
|
73
|
+
continue
|
|
74
|
+
answer = msg.content
|
|
75
|
+
self._current_user_id = msg.user_id
|
|
76
|
+
self._current_mem_cube_id = msg.mem_cube_id
|
|
77
|
+
self._current_mem_cube = msg.mem_cube
|
|
78
|
+
|
|
79
|
+
# Get current activation memory items
|
|
80
|
+
current_activation_mem = [
|
|
81
|
+
item["memory"]
|
|
82
|
+
for item in self.monitor.activation_memory_freq_list
|
|
83
|
+
if item["memory"] is not None
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# Update memory frequencies based on the answer
|
|
87
|
+
# TODO: not implemented
|
|
88
|
+
self.monitor.activation_memory_freq_list = self.monitor.update_freq(
|
|
89
|
+
answer=answer, activation_memory_freq_list=self.monitor.activation_memory_freq_list
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Check if it's time to update activation memory
|
|
93
|
+
now = datetime.now()
|
|
94
|
+
if (now - self._last_activation_mem_update_time) >= timedelta(
|
|
95
|
+
seconds=self.act_mem_update_interval
|
|
96
|
+
):
|
|
97
|
+
# TODO: not implemented
|
|
98
|
+
self.update_activation_memory(current_activation_mem)
|
|
99
|
+
self._last_activation_mem_update_time = now
|
|
100
|
+
|
|
101
|
+
# recording messages
|
|
102
|
+
log_message = self.create_autofilled_log_item(
|
|
103
|
+
log_title="memos answer triggers scheduling...",
|
|
104
|
+
label=ANSWER_LABEL,
|
|
105
|
+
log_content="activation_memory has been updated",
|
|
106
|
+
)
|
|
107
|
+
self._submit_web_logs(messages=log_message)
|
|
108
|
+
|
|
109
|
+
def _query_message_consume(self, messages: list[ScheduleMessageItem]) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Process and handle query trigger messages from the queue.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
messages: List of query messages to process
|
|
115
|
+
"""
|
|
116
|
+
logger.debug(f"Messages {messages} assigned to {QUERY_LABEL} handler.")
|
|
117
|
+
for msg in messages:
|
|
118
|
+
if msg.label is not QUERY_LABEL:
|
|
119
|
+
logger.error(f"_query_message_consume is not designed for {msg.label}")
|
|
120
|
+
continue
|
|
121
|
+
# Process the query in a session turn
|
|
122
|
+
self._current_user_id = msg.user_id
|
|
123
|
+
self._current_mem_cube_id = msg.mem_cube_id
|
|
124
|
+
self._current_mem_cube = msg.mem_cube
|
|
125
|
+
self.process_session_turn(query=msg.content, top_k=self.top_k, top_n=self.top_n)
|
|
126
|
+
|
|
127
|
+
def process_session_turn(
|
|
128
|
+
self,
|
|
129
|
+
query: str,
|
|
130
|
+
top_k: int = 10,
|
|
131
|
+
top_n: int = 5,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Process a dialog turn:
|
|
135
|
+
- If q_list reaches window size, trigger retrieval;
|
|
136
|
+
- Immediately switch to the new memory if retrieval is triggered.
|
|
137
|
+
"""
|
|
138
|
+
q_list = [query]
|
|
139
|
+
self.query_list.append(query)
|
|
140
|
+
text_mem_base = self.mem_cube.text_mem
|
|
141
|
+
if isinstance(text_mem_base, TreeTextMemory):
|
|
142
|
+
working_memory: list[TextualMemoryItem] = text_mem_base.get_working_memory()
|
|
143
|
+
else:
|
|
144
|
+
logger.error("Not implemented!")
|
|
145
|
+
return
|
|
146
|
+
text_working_memory: list[str] = [w_m.memory for w_m in working_memory]
|
|
147
|
+
intent_result = self.monitor.detect_intent(
|
|
148
|
+
q_list=q_list, text_working_memory=text_working_memory
|
|
149
|
+
)
|
|
150
|
+
if intent_result["trigger_retrieval"]:
|
|
151
|
+
missing_evidence = intent_result["missing_evidence"]
|
|
152
|
+
num_evidence = len(missing_evidence)
|
|
153
|
+
k_per_evidence = max(1, top_k // max(1, num_evidence))
|
|
154
|
+
new_candidates = []
|
|
155
|
+
for item in missing_evidence:
|
|
156
|
+
logger.debug(f"missing_evidence: {item}")
|
|
157
|
+
results = self.search(query=item, top_k=k_per_evidence, method=self.search_method)
|
|
158
|
+
logger.debug(f"search results for {missing_evidence}: {results}")
|
|
159
|
+
new_candidates.extend(results)
|
|
160
|
+
|
|
161
|
+
# recording messages
|
|
162
|
+
log_message = self.create_autofilled_log_item(
|
|
163
|
+
log_title="user query triggers scheduling...",
|
|
164
|
+
label=QUERY_LABEL,
|
|
165
|
+
log_content=f"search new candidates for working memory: {len(new_candidates)}",
|
|
166
|
+
)
|
|
167
|
+
self._submit_web_logs(messages=log_message)
|
|
168
|
+
new_order_working_memory = self.replace_working_memory(
|
|
169
|
+
original_memory=working_memory, new_memory=new_candidates, top_k=top_k, top_n=top_n
|
|
170
|
+
)
|
|
171
|
+
self.update_activation_memory(new_order_working_memory)
|
|
172
|
+
|
|
173
|
+
def create_autofilled_log_item(
|
|
174
|
+
self, log_title: str, log_content: str, label: str
|
|
175
|
+
) -> ScheduleLogForWebItem:
|
|
176
|
+
# TODO: create the log iterm with real stats
|
|
177
|
+
text_mem_base: TreeTextMemory = self.mem_cube.text_mem
|
|
178
|
+
current_memory_sizes = {
|
|
179
|
+
"long_term_memory_size": NOT_INITIALIZED,
|
|
180
|
+
"user_memory_size": NOT_INITIALIZED,
|
|
181
|
+
"working_memory_size": NOT_INITIALIZED,
|
|
182
|
+
"transformed_act_memory_size": NOT_INITIALIZED,
|
|
183
|
+
"parameter_memory_size": NOT_INITIALIZED,
|
|
184
|
+
}
|
|
185
|
+
memory_capacities = {
|
|
186
|
+
"long_term_memory_capacity": text_mem_base.memory_manager.memory_size["LongTermMemory"],
|
|
187
|
+
"user_memory_capacity": text_mem_base.memory_manager.memory_size["UserMemory"],
|
|
188
|
+
"working_memory_capacity": text_mem_base.memory_manager.memory_size["WorkingMemory"],
|
|
189
|
+
"transformed_act_memory_capacity": NOT_INITIALIZED,
|
|
190
|
+
"parameter_memory_capacity": NOT_INITIALIZED,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
log_message = ScheduleLogForWebItem(
|
|
194
|
+
user_id=self._current_user_id,
|
|
195
|
+
mem_cube_id=self._current_mem_cube_id,
|
|
196
|
+
label=label,
|
|
197
|
+
log_title=log_title,
|
|
198
|
+
log_content=log_content,
|
|
199
|
+
current_memory_sizes=current_memory_sizes,
|
|
200
|
+
memory_capacities=memory_capacities,
|
|
201
|
+
)
|
|
202
|
+
return log_message
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def mem_cube(self) -> GeneralMemCube:
|
|
206
|
+
"""The memory cube associated with this MemChat."""
|
|
207
|
+
return self._current_mem_cube
|
|
208
|
+
|
|
209
|
+
@mem_cube.setter
|
|
210
|
+
def mem_cube(self, value: GeneralMemCube) -> None:
|
|
211
|
+
"""The memory cube associated with this MemChat."""
|
|
212
|
+
self._current_mem_cube = value
|
|
213
|
+
self.retriever.mem_cube = value
|
|
214
|
+
|
|
215
|
+
def replace_working_memory(
|
|
216
|
+
self,
|
|
217
|
+
original_memory: list[TextualMemoryItem],
|
|
218
|
+
new_memory: list[TextualMemoryItem],
|
|
219
|
+
top_k: int = 10,
|
|
220
|
+
top_n: int = 5,
|
|
221
|
+
) -> None | list[TextualMemoryItem]:
|
|
222
|
+
new_order_memory = None
|
|
223
|
+
text_mem_base = self.mem_cube.text_mem
|
|
224
|
+
if isinstance(text_mem_base, TreeTextMemory):
|
|
225
|
+
text_mem_base: TreeTextMemory = text_mem_base
|
|
226
|
+
combined_text_memory = [new_m.memory for new_m in original_memory] + [
|
|
227
|
+
new_m.memory for new_m in new_memory
|
|
228
|
+
]
|
|
229
|
+
combined_memory = original_memory + new_memory
|
|
230
|
+
memory_map = {mem_obj.memory: mem_obj for mem_obj in combined_memory}
|
|
231
|
+
|
|
232
|
+
unique_memory = list(dict.fromkeys(combined_text_memory))
|
|
233
|
+
prompt = self.build_prompt(
|
|
234
|
+
"memory_reranking", query="", current_order=unique_memory, staging_buffer=[]
|
|
235
|
+
)
|
|
236
|
+
response = self.chat_llm.generate([{"role": "user", "content": prompt}])
|
|
237
|
+
response = json.loads(response)
|
|
238
|
+
new_order_text_memory = response.get("new_order", [])[: top_n + top_k]
|
|
239
|
+
|
|
240
|
+
new_order_memory = []
|
|
241
|
+
for text in new_order_text_memory:
|
|
242
|
+
if text in memory_map:
|
|
243
|
+
new_order_memory.append(memory_map[text])
|
|
244
|
+
else:
|
|
245
|
+
logger.warning(
|
|
246
|
+
f"Memory text not found in memory map. text: {text}; memory_map: {memory_map}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
text_mem_base.replace_working_memory(new_order_memory[top_n:])
|
|
250
|
+
new_order_memory = new_order_memory[:top_n]
|
|
251
|
+
logger.info(
|
|
252
|
+
f"The working memory has been replaced with {len(new_order_memory)} new memories."
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
logger.error("memory_base is not supported")
|
|
256
|
+
|
|
257
|
+
return new_order_memory
|
|
258
|
+
|
|
259
|
+
def search(self, query: str, top_k: int, method=TreeTextMemory_SEARCH_METHOD):
|
|
260
|
+
text_mem_base = self.mem_cube.text_mem
|
|
261
|
+
if isinstance(text_mem_base, TreeTextMemory) and method == TextMemory_SEARCH_METHOD:
|
|
262
|
+
results_long_term = text_mem_base.search(
|
|
263
|
+
query=query, top_k=top_k, memory_type="LongTermMemory"
|
|
264
|
+
)
|
|
265
|
+
results_user = text_mem_base.search(query=query, top_k=top_k, memory_type="UserMemory")
|
|
266
|
+
results = results_long_term + results_user
|
|
267
|
+
else:
|
|
268
|
+
logger.error("Not implemented.")
|
|
269
|
+
results = None
|
|
270
|
+
return results
|
|
271
|
+
|
|
272
|
+
def update_activation_memory(self, new_memory: list[str | TextualMemoryItem]) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Update activation memory by extracting KVCacheItems from new_memory (list of str),
|
|
275
|
+
add them to a KVCacheMemory instance, and dump to disk.
|
|
276
|
+
"""
|
|
277
|
+
# TODO: The function of update activation memory is waiting to test
|
|
278
|
+
if len(new_memory) == 0:
|
|
279
|
+
logger.error("update_activation_memory: new_memory is empty.")
|
|
280
|
+
return
|
|
281
|
+
if isinstance(new_memory[0], TextualMemoryItem):
|
|
282
|
+
new_text_memory = [mem.memory for mem in new_memory]
|
|
283
|
+
elif isinstance(new_memory[0], str):
|
|
284
|
+
new_text_memory = new_memory
|
|
285
|
+
else:
|
|
286
|
+
logger.error("Not Implemented.")
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
act_mem = self.mem_cube.act_mem
|
|
290
|
+
|
|
291
|
+
text_memory = MEMORY_ASSEMBLY_TEMPLATE.format(
|
|
292
|
+
memory_text="".join(
|
|
293
|
+
[
|
|
294
|
+
f"{i + 1}. {sentence.strip()}\n"
|
|
295
|
+
for i, sentence in enumerate(new_text_memory)
|
|
296
|
+
if sentence.strip() # Skip empty strings
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
act_mem.delete_all()
|
|
301
|
+
cache_item = act_mem.extract(text_memory)
|
|
302
|
+
act_mem.add(cache_item)
|
|
303
|
+
act_mem.dump(self.act_mem_dump_path)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.warning(f"MOS-based activation memory update failed: {e}")
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from memos.llms.base import BaseLLM
|
|
4
|
+
from memos.log import get_logger
|
|
5
|
+
from memos.mem_cube.general import GeneralMemCube
|
|
6
|
+
from memos.mem_scheduler.modules.schemas import BASE_DIR
|
|
7
|
+
from memos.templates.mem_scheduler_prompts import PROMPT_MAPPING
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseSchedulerModule:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
"""Initialize the scheduler with the given configuration."""
|
|
16
|
+
self.base_dir = Path(BASE_DIR)
|
|
17
|
+
|
|
18
|
+
self._chat_llm = None
|
|
19
|
+
self._current_mem_cube_id: str | None = None
|
|
20
|
+
self._current_mem_cube: GeneralMemCube | None = None
|
|
21
|
+
self.mem_cubes: dict[str, GeneralMemCube] = {}
|
|
22
|
+
|
|
23
|
+
def load_template(self, template_name: str) -> str:
|
|
24
|
+
if template_name not in PROMPT_MAPPING:
|
|
25
|
+
logger.error("Prompt template is not found!")
|
|
26
|
+
prompt = PROMPT_MAPPING[template_name]
|
|
27
|
+
return prompt
|
|
28
|
+
|
|
29
|
+
def build_prompt(self, template_name: str, **kwargs) -> str:
|
|
30
|
+
template = self.load_template(template_name)
|
|
31
|
+
if not template:
|
|
32
|
+
raise FileNotFoundError(f"Prompt template `{template_name}` not found.")
|
|
33
|
+
return template.format(**kwargs)
|
|
34
|
+
|
|
35
|
+
def _build_system_prompt(self, memories: list | None = None) -> str:
|
|
36
|
+
"""Build system prompt with optional memories context."""
|
|
37
|
+
base_prompt = (
|
|
38
|
+
"You are a knowledgeable and helpful AI assistant. "
|
|
39
|
+
"You have access to conversation memories that help you provide more personalized responses. "
|
|
40
|
+
"Use the memories to understand the user's context, preferences, and past interactions. "
|
|
41
|
+
"If memories are provided, reference them naturally when relevant, but don't explicitly mention having memories."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if memories:
|
|
45
|
+
memory_context = "\n\n## Conversation Context:\n"
|
|
46
|
+
for i, memory in enumerate(memories, 1):
|
|
47
|
+
memory_context += f"{i}. {memory.memory}\n"
|
|
48
|
+
return base_prompt + memory_context
|
|
49
|
+
|
|
50
|
+
return base_prompt
|
|
51
|
+
|
|
52
|
+
def get_mem_cube(self, mem_cube_id: str) -> GeneralMemCube:
|
|
53
|
+
logger.error(f"mem_cube {mem_cube_id} does not exists.")
|
|
54
|
+
return self.mem_cubes.get(mem_cube_id, None)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def chat_llm(self) -> BaseLLM:
|
|
58
|
+
"""The memory cube associated with this MemChat."""
|
|
59
|
+
return self._chat_llm
|
|
60
|
+
|
|
61
|
+
@chat_llm.setter
|
|
62
|
+
def chat_llm(self, value: BaseLLM) -> None:
|
|
63
|
+
"""The memory cube associated with this MemChat."""
|
|
64
|
+
self._chat_llm = value
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def mem_cube(self) -> GeneralMemCube:
|
|
68
|
+
"""The memory cube associated with this MemChat."""
|
|
69
|
+
return self._current_mem_cube
|
|
70
|
+
|
|
71
|
+
@mem_cube.setter
|
|
72
|
+
def mem_cube(self, value: GeneralMemCube) -> None:
|
|
73
|
+
"""The memory cube associated with this MemChat."""
|
|
74
|
+
self._current_mem_cube = value
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
|
|
5
|
+
from memos.log import get_logger
|
|
6
|
+
from memos.mem_scheduler.modules.base import BaseSchedulerModule
|
|
7
|
+
from memos.mem_scheduler.modules.schemas import ScheduleMessageItem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SchedulerDispatcher(BaseSchedulerModule):
|
|
14
|
+
"""
|
|
15
|
+
Thread pool-based message dispatcher that routes messages to dedicated handlers
|
|
16
|
+
based on their labels.
|
|
17
|
+
|
|
18
|
+
Features:
|
|
19
|
+
- Dedicated thread pool per message label
|
|
20
|
+
- Batch message processing
|
|
21
|
+
- Graceful shutdown
|
|
22
|
+
- Bulk handler registration
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, max_workers=3, enable_parallel_dispatch=False):
|
|
26
|
+
super().__init__()
|
|
27
|
+
# Main dispatcher thread pool
|
|
28
|
+
self.max_workers = max_workers
|
|
29
|
+
# Only initialize thread pool if in parallel mode
|
|
30
|
+
self.enable_parallel_dispatch = enable_parallel_dispatch
|
|
31
|
+
if self.enable_parallel_dispatch:
|
|
32
|
+
self.dispatcher_executor = ThreadPoolExecutor(
|
|
33
|
+
max_workers=self.max_workers, thread_name_prefix="dispatcher"
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
self.dispatcher_executor = None
|
|
37
|
+
logger.info(f"enable_parallel_dispatch is set to {self.enable_parallel_dispatch}")
|
|
38
|
+
# Registered message handlers
|
|
39
|
+
self.handlers: dict[str, Callable] = {}
|
|
40
|
+
# Dispatcher running state
|
|
41
|
+
self._running = False
|
|
42
|
+
|
|
43
|
+
def register_handler(self, label: str, handler: Callable[[list[ScheduleMessageItem]], None]):
|
|
44
|
+
"""
|
|
45
|
+
Register a handler function for a specific message label.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
label: Message label to handle
|
|
49
|
+
handler: Callable that processes messages of this label
|
|
50
|
+
"""
|
|
51
|
+
self.handlers[label] = handler
|
|
52
|
+
|
|
53
|
+
def register_handlers(
|
|
54
|
+
self, handlers: dict[str, Callable[[list[ScheduleMessageItem]], None]]
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Bulk register multiple handlers from a dictionary.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
handlers: Dictionary mapping labels to handler functions
|
|
61
|
+
Format: {label: handler_callable}
|
|
62
|
+
"""
|
|
63
|
+
for label, handler in handlers.items():
|
|
64
|
+
if not isinstance(label, str):
|
|
65
|
+
logger.error(f"Invalid label type: {type(label)}. Expected str.")
|
|
66
|
+
continue
|
|
67
|
+
if not callable(handler):
|
|
68
|
+
logger.error(f"Handler for label '{label}' is not callable.")
|
|
69
|
+
continue
|
|
70
|
+
self.register_handler(label=label, handler=handler)
|
|
71
|
+
logger.info(f"Registered {len(handlers)} handlers in bulk")
|
|
72
|
+
|
|
73
|
+
def _default_message_handler(self, messages: list[ScheduleMessageItem]) -> None:
|
|
74
|
+
logger.debug(f"Using _default_message_handler to deal with messages: {messages}")
|
|
75
|
+
|
|
76
|
+
def dispatch(self, msg_list: list[ScheduleMessageItem]):
|
|
77
|
+
"""
|
|
78
|
+
Dispatch a list of messages to their respective handlers.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
msg_list: List of ScheduleMessageItem objects to process
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# Group messages by their labels
|
|
85
|
+
label_groups = defaultdict(list)
|
|
86
|
+
|
|
87
|
+
# Organize messages by label
|
|
88
|
+
for message in msg_list:
|
|
89
|
+
label_groups[message.label].append(message)
|
|
90
|
+
|
|
91
|
+
# Process each label group
|
|
92
|
+
for label, msgs in label_groups.items():
|
|
93
|
+
if label not in self.handlers:
|
|
94
|
+
logger.error(f"No handler registered for label: {label}")
|
|
95
|
+
handler = self._default_message_handler
|
|
96
|
+
else:
|
|
97
|
+
handler = self.handlers[label]
|
|
98
|
+
# dispatch to different handler
|
|
99
|
+
logger.debug(f"Dispatch {len(msgs)} messages to {label} handler.")
|
|
100
|
+
if self.enable_parallel_dispatch and self.dispatcher_executor is not None:
|
|
101
|
+
self.dispatcher_executor.submit(handler, msgs)
|
|
102
|
+
else:
|
|
103
|
+
handler(msgs) # Direct serial execution
|