MemoryOS 0.0.1__py3-none-any.whl → 0.1.12__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.12.dist-info/METADATA +257 -0
- memoryos-0.1.12.dist-info/RECORD +117 -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/llm.py +71 -0
- memos/configs/mem_chat.py +81 -0
- memos/configs/mem_cube.py +89 -0
- memos/configs/mem_os.py +70 -0
- memos/configs/mem_reader.py +53 -0
- memos/configs/mem_scheduler.py +78 -0
- memos/configs/memory.py +190 -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 +12 -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 +478 -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 +289 -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/reasoner.py +64 -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 +166 -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/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/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.12.dist-info}/LICENSE +0 -0
- {memoryos-0.0.1.dist-info → memoryos-0.1.12.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from memos import log
|
|
9
|
+
from memos.chunkers import ChunkerFactory
|
|
10
|
+
from memos.configs.mem_reader import SimpleStructMemReaderConfig
|
|
11
|
+
from memos.configs.parser import ParserConfigFactory
|
|
12
|
+
from memos.embedders.factory import EmbedderFactory
|
|
13
|
+
from memos.llms.factory import LLMFactory
|
|
14
|
+
from memos.mem_reader.base import BaseMemReader
|
|
15
|
+
from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
|
|
16
|
+
from memos.parsers.factory import ParserFactory
|
|
17
|
+
from memos.templates.mem_reader_prompts import (
|
|
18
|
+
SIMPLE_STRUCT_DOC_READER_PROMPT,
|
|
19
|
+
SIMPLE_STRUCT_MEM_READER_PROMPT,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = log.get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SimpleStructMemReader(BaseMemReader, ABC):
|
|
27
|
+
"""Naive implementation of MemReader."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: SimpleStructMemReaderConfig):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the NaiveMemReader with configuration.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
config: Configuration object for the reader
|
|
35
|
+
"""
|
|
36
|
+
self.config = config
|
|
37
|
+
self.llm = LLMFactory.from_config(config.llm)
|
|
38
|
+
self.embedder = EmbedderFactory.from_config(config.embedder)
|
|
39
|
+
self.chunker = ChunkerFactory.from_config(config.chunker)
|
|
40
|
+
|
|
41
|
+
def _process_chat_data(self, scene_data_info, info):
|
|
42
|
+
prompt = (
|
|
43
|
+
SIMPLE_STRUCT_MEM_READER_PROMPT.replace("${user_a}", "user")
|
|
44
|
+
.replace("${user_b}", "assistant")
|
|
45
|
+
.replace("${conversation}", "\n".join(scene_data_info))
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
messages = [{"role": "user", "content": prompt}]
|
|
49
|
+
|
|
50
|
+
response_text = self.llm.generate(messages)
|
|
51
|
+
response_json = self.parse_json_result(response_text)
|
|
52
|
+
|
|
53
|
+
chat_read_nodes = []
|
|
54
|
+
for memory_i_raw in response_json.get("memory list", []):
|
|
55
|
+
node_i = TextualMemoryItem(
|
|
56
|
+
memory=memory_i_raw.get("value", ""),
|
|
57
|
+
metadata=TreeNodeTextualMemoryMetadata(
|
|
58
|
+
user_id=info.get("user_id"),
|
|
59
|
+
session_id=info.get("session_id"),
|
|
60
|
+
memory_type=memory_i_raw.get("memory_type", ""),
|
|
61
|
+
status="activated",
|
|
62
|
+
tags=memory_i_raw.get("tags", ""),
|
|
63
|
+
key=memory_i_raw.get("key", ""),
|
|
64
|
+
embedding=self.embedder.embed([memory_i_raw.get("value", "")])[0],
|
|
65
|
+
usage=[],
|
|
66
|
+
sources=scene_data_info,
|
|
67
|
+
background=response_json.get("summary", ""),
|
|
68
|
+
confidence=0.99,
|
|
69
|
+
type="fact",
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
chat_read_nodes.append(node_i)
|
|
73
|
+
|
|
74
|
+
return chat_read_nodes
|
|
75
|
+
|
|
76
|
+
def get_memory(
|
|
77
|
+
self, scene_data: list, type: str, info: dict[str, Any]
|
|
78
|
+
) -> list[list[TextualMemoryItem]]:
|
|
79
|
+
"""
|
|
80
|
+
Extract and classify memory content from scene_data.
|
|
81
|
+
For dictionaries: Use LLM to summarize pairs of Q&A
|
|
82
|
+
For file paths: Use chunker to split documents and LLM to summarize each chunk
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
scene_data: List of dialogue information or document paths
|
|
86
|
+
type: Type of scene_data: ['doc', 'chat']
|
|
87
|
+
info: Dictionary containing user_id and session_id.
|
|
88
|
+
Must be in format: {"user_id": "1111", "session_id": "2222"}
|
|
89
|
+
Optional parameters:
|
|
90
|
+
- topic_chunk_size: Size for large topic chunks (default: 1024)
|
|
91
|
+
- topic_chunk_overlap: Overlap for large topic chunks (default: 100)
|
|
92
|
+
- chunk_size: Size for small chunks (default: 256)
|
|
93
|
+
- chunk_overlap: Overlap for small chunks (default: 50)
|
|
94
|
+
Returns:
|
|
95
|
+
list[list[TextualMemoryItem]] containing memory content with summaries as keys and original text as values
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If scene_data is empty or if info dictionary is missing required fields
|
|
98
|
+
"""
|
|
99
|
+
if not scene_data:
|
|
100
|
+
raise ValueError("scene_data is empty")
|
|
101
|
+
|
|
102
|
+
# Validate info dictionary format
|
|
103
|
+
if not isinstance(info, dict):
|
|
104
|
+
raise ValueError("info must be a dictionary")
|
|
105
|
+
|
|
106
|
+
required_fields = {"user_id", "session_id"}
|
|
107
|
+
missing_fields = required_fields - set(info.keys())
|
|
108
|
+
if missing_fields:
|
|
109
|
+
raise ValueError(f"info dictionary is missing required fields: {missing_fields}")
|
|
110
|
+
|
|
111
|
+
if not all(isinstance(info[field], str) for field in required_fields):
|
|
112
|
+
raise ValueError("user_id and session_id must be strings")
|
|
113
|
+
|
|
114
|
+
list_scene_data_info = self.get_scene_data_info(scene_data, type)
|
|
115
|
+
|
|
116
|
+
memory_list = []
|
|
117
|
+
|
|
118
|
+
if type == "chat":
|
|
119
|
+
processing_func = self._process_chat_data
|
|
120
|
+
elif type == "doc":
|
|
121
|
+
processing_func = self._process_doc_data
|
|
122
|
+
else:
|
|
123
|
+
processing_func = self._process_doc_data
|
|
124
|
+
|
|
125
|
+
# Process Q&A pairs concurrently
|
|
126
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
127
|
+
futures = [
|
|
128
|
+
executor.submit(processing_func, scene_data_info, info)
|
|
129
|
+
for scene_data_info in list_scene_data_info
|
|
130
|
+
]
|
|
131
|
+
for future in concurrent.futures.as_completed(futures):
|
|
132
|
+
res_memory = future.result()
|
|
133
|
+
memory_list.append(res_memory)
|
|
134
|
+
|
|
135
|
+
return memory_list
|
|
136
|
+
|
|
137
|
+
def get_scene_data_info(self, scene_data: list, type: str) -> list[str]:
|
|
138
|
+
"""
|
|
139
|
+
Get raw information from scene_data.
|
|
140
|
+
If scene_data contains dictionaries, convert them to strings.
|
|
141
|
+
If scene_data contains file paths, parse them using the parser.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
scene_data: List of dialogue information or document paths
|
|
145
|
+
type: Type of scene data: ['doc', 'chat']
|
|
146
|
+
Returns:
|
|
147
|
+
List of strings containing the processed scene data
|
|
148
|
+
"""
|
|
149
|
+
results = []
|
|
150
|
+
parser_config = ParserConfigFactory.model_validate(
|
|
151
|
+
{
|
|
152
|
+
"backend": "markitdown",
|
|
153
|
+
"config": {},
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
parser = ParserFactory.from_config(parser_config)
|
|
157
|
+
|
|
158
|
+
if type == "chat":
|
|
159
|
+
for items in scene_data:
|
|
160
|
+
result = []
|
|
161
|
+
for item in items:
|
|
162
|
+
# Convert dictionary to string
|
|
163
|
+
if "chat_time" in item:
|
|
164
|
+
mem = item["role"] + ": " + f"[{item['chat_time']}]: " + item["content"]
|
|
165
|
+
result.append(mem)
|
|
166
|
+
else:
|
|
167
|
+
mem = item["role"] + ":" + item["content"]
|
|
168
|
+
result.append(mem)
|
|
169
|
+
if len(result) >= 10:
|
|
170
|
+
results.append(result)
|
|
171
|
+
context = copy.deepcopy(result[-2:])
|
|
172
|
+
result = context
|
|
173
|
+
if result:
|
|
174
|
+
results.append(result)
|
|
175
|
+
elif type == "doc":
|
|
176
|
+
for item in scene_data:
|
|
177
|
+
try:
|
|
178
|
+
parsed_text = parser.parse(item)
|
|
179
|
+
results.append({"file": item, "text": parsed_text})
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f"Error parsing file {item}: {e!s}")
|
|
182
|
+
|
|
183
|
+
return results
|
|
184
|
+
|
|
185
|
+
def _process_doc_data(self, scene_data_info, info):
|
|
186
|
+
chunks = self.chunker.chunk(scene_data_info["text"])
|
|
187
|
+
messages = [
|
|
188
|
+
[
|
|
189
|
+
{
|
|
190
|
+
"role": "user",
|
|
191
|
+
"content": SIMPLE_STRUCT_DOC_READER_PROMPT.replace("{chunk_text}", chunk.text),
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
for chunk in chunks
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
processed_chunks = []
|
|
198
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
|
199
|
+
futures = [executor.submit(self.llm.generate, message) for message in messages]
|
|
200
|
+
for future in concurrent.futures.as_completed(futures):
|
|
201
|
+
chunk_result = future.result()
|
|
202
|
+
if chunk_result:
|
|
203
|
+
processed_chunks.append(chunk_result)
|
|
204
|
+
|
|
205
|
+
processed_chunks = [self.parse_json_result(r) for r in processed_chunks]
|
|
206
|
+
doc_nodes = []
|
|
207
|
+
for i, chunk_res in enumerate(processed_chunks):
|
|
208
|
+
if chunk_res:
|
|
209
|
+
node_i = TextualMemoryItem(
|
|
210
|
+
memory=chunk_res["summary"],
|
|
211
|
+
metadata=TreeNodeTextualMemoryMetadata(
|
|
212
|
+
user_id=info.get("user_id"),
|
|
213
|
+
session_id=info.get("session_id"),
|
|
214
|
+
memory_type="LongTermMemory",
|
|
215
|
+
status="activated",
|
|
216
|
+
tags=chunk_res["tags"],
|
|
217
|
+
key="",
|
|
218
|
+
embedding=self.embedder.embed([chunk_res["summary"]])[0],
|
|
219
|
+
usage=[],
|
|
220
|
+
sources=[f"{scene_data_info['file']}_{i}"],
|
|
221
|
+
background="",
|
|
222
|
+
confidence=0.99,
|
|
223
|
+
type="fact",
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
doc_nodes.append(node_i)
|
|
227
|
+
return doc_nodes
|
|
228
|
+
|
|
229
|
+
def parse_json_result(self, response_text):
|
|
230
|
+
try:
|
|
231
|
+
response_text = response_text.replace("```", "").replace("json", "")
|
|
232
|
+
response_json = json.loads(response_text)
|
|
233
|
+
return response_json
|
|
234
|
+
except json.JSONDecodeError as e:
|
|
235
|
+
logger.warning(
|
|
236
|
+
f"Failed to parse LLM response as JSON: {e}\nRaw response:\n{response_text}"
|
|
237
|
+
)
|
|
238
|
+
return {}
|
|
239
|
+
|
|
240
|
+
def transform_memreader(self, data: dict) -> list[TextualMemoryItem]:
|
|
241
|
+
pass
|
|
File without changes
|
|
@@ -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")
|