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
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import concurrent.futures
|
|
2
1
|
import json
|
|
2
|
+
import traceback
|
|
3
3
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
|
|
6
|
+
from memos.context.context import ContextThreadPoolExecutor
|
|
6
7
|
from memos.embedders.factory import OllamaEmbedder
|
|
7
8
|
from memos.graph_dbs.factory import Neo4jGraphDB
|
|
8
9
|
from memos.llms.factory import AzureLLM, OllamaLLM, OpenAILLM
|
|
9
10
|
from memos.log import get_logger
|
|
10
11
|
from memos.memories.textual.item import SearchedTreeNodeTextualMemoryMetadata, TextualMemoryItem
|
|
12
|
+
from memos.reranker.base import BaseReranker
|
|
11
13
|
from memos.utils import timed
|
|
12
14
|
|
|
13
15
|
from .internet_retriever_factory import InternetRetrieverFactory
|
|
14
16
|
from .reasoner import MemoryReasoner
|
|
15
17
|
from .recall import GraphMemoryRetriever
|
|
16
|
-
from .reranker import MemoryReranker
|
|
17
18
|
from .task_goal_parser import TaskGoalParser
|
|
18
19
|
|
|
19
20
|
|
|
@@ -26,22 +27,33 @@ class Searcher:
|
|
|
26
27
|
dispatcher_llm: OpenAILLM | OllamaLLM | AzureLLM,
|
|
27
28
|
graph_store: Neo4jGraphDB,
|
|
28
29
|
embedder: OllamaEmbedder,
|
|
30
|
+
reranker: BaseReranker,
|
|
29
31
|
internet_retriever: InternetRetrieverFactory | None = None,
|
|
32
|
+
moscube: bool = False,
|
|
30
33
|
):
|
|
31
34
|
self.graph_store = graph_store
|
|
32
35
|
self.embedder = embedder
|
|
33
36
|
|
|
34
37
|
self.task_goal_parser = TaskGoalParser(dispatcher_llm)
|
|
35
38
|
self.graph_retriever = GraphMemoryRetriever(self.graph_store, self.embedder)
|
|
36
|
-
self.reranker =
|
|
39
|
+
self.reranker = reranker
|
|
37
40
|
self.reasoner = MemoryReasoner(dispatcher_llm)
|
|
38
41
|
|
|
39
42
|
# Create internet retriever from config if provided
|
|
40
43
|
self.internet_retriever = internet_retriever
|
|
44
|
+
self.moscube = moscube
|
|
45
|
+
|
|
46
|
+
self._usage_executor = ContextThreadPoolExecutor(max_workers=4, thread_name_prefix="usage")
|
|
41
47
|
|
|
42
48
|
@timed
|
|
43
49
|
def search(
|
|
44
|
-
self,
|
|
50
|
+
self,
|
|
51
|
+
query: str,
|
|
52
|
+
top_k: int,
|
|
53
|
+
info=None,
|
|
54
|
+
mode="fast",
|
|
55
|
+
memory_type="All",
|
|
56
|
+
search_filter: dict | None = None,
|
|
45
57
|
) -> list[TextualMemoryItem]:
|
|
46
58
|
"""
|
|
47
59
|
Search for memories based on a query.
|
|
@@ -56,6 +68,7 @@ class Searcher:
|
|
|
56
68
|
- 'fine': Uses a more detailed search process, invoking large models for higher precision, but slower performance.
|
|
57
69
|
memory_type (str): Type restriction for search.
|
|
58
70
|
['All', 'WorkingMemory', 'LongTermMemory', 'UserMemory']
|
|
71
|
+
search_filter (dict, optional): Optional metadata filters for search results.
|
|
59
72
|
Returns:
|
|
60
73
|
list[TextualMemoryItem]: List of matching memories.
|
|
61
74
|
"""
|
|
@@ -71,19 +84,27 @@ class Searcher:
|
|
|
71
84
|
else:
|
|
72
85
|
logger.debug(f"[SEARCH] Received info dict: {info}")
|
|
73
86
|
|
|
74
|
-
parsed_goal, query_embedding, context, query = self._parse_task(
|
|
87
|
+
parsed_goal, query_embedding, context, query = self._parse_task(
|
|
88
|
+
query, info, mode, search_filter=search_filter
|
|
89
|
+
)
|
|
75
90
|
results = self._retrieve_paths(
|
|
76
|
-
query, parsed_goal, query_embedding, info, top_k, mode, memory_type
|
|
91
|
+
query, parsed_goal, query_embedding, info, top_k, mode, memory_type, search_filter
|
|
77
92
|
)
|
|
78
93
|
deduped = self._deduplicate_results(results)
|
|
79
94
|
final_results = self._sort_and_trim(deduped, top_k)
|
|
80
95
|
self._update_usage_history(final_results, info)
|
|
81
96
|
|
|
82
97
|
logger.info(f"[SEARCH] Done. Total {len(final_results)} results.")
|
|
98
|
+
res_results = ""
|
|
99
|
+
for _num_i, result in enumerate(final_results):
|
|
100
|
+
res_results += "\n" + (
|
|
101
|
+
result.id + "|" + result.metadata.memory_type + "|" + result.memory
|
|
102
|
+
)
|
|
103
|
+
logger.info(f"[SEARCH] Results. {res_results}")
|
|
83
104
|
return final_results
|
|
84
105
|
|
|
85
106
|
@timed
|
|
86
|
-
def _parse_task(self, query, info, mode, top_k=5):
|
|
107
|
+
def _parse_task(self, query, info, mode, top_k=5, search_filter: dict | None = None):
|
|
87
108
|
"""Parse user query, do embedding search and create context"""
|
|
88
109
|
context = []
|
|
89
110
|
query_embedding = None
|
|
@@ -96,14 +117,30 @@ class Searcher:
|
|
|
96
117
|
# retrieve related nodes by embedding
|
|
97
118
|
related_nodes = [
|
|
98
119
|
self.graph_store.get_node(n["id"])
|
|
99
|
-
for n in self.graph_store.search_by_embedding(
|
|
120
|
+
for n in self.graph_store.search_by_embedding(
|
|
121
|
+
query_embedding, top_k=top_k, search_filter=search_filter
|
|
122
|
+
)
|
|
100
123
|
]
|
|
101
|
-
|
|
124
|
+
memories = []
|
|
125
|
+
for node in related_nodes:
|
|
126
|
+
try:
|
|
127
|
+
m = (
|
|
128
|
+
node.get("memory")
|
|
129
|
+
if isinstance(node, dict)
|
|
130
|
+
else (getattr(node, "memory", None))
|
|
131
|
+
)
|
|
132
|
+
if isinstance(m, str) and m:
|
|
133
|
+
memories.append(m)
|
|
134
|
+
except Exception:
|
|
135
|
+
logger.error(f"[SEARCH] Error during search: {traceback.format_exc()}")
|
|
136
|
+
continue
|
|
137
|
+
context = list(dict.fromkeys(memories))
|
|
102
138
|
|
|
103
139
|
# optional: supplement context with internet knowledge
|
|
104
|
-
if self.internet_retriever:
|
|
140
|
+
"""if self.internet_retriever:
|
|
105
141
|
extra = self.internet_retriever.retrieve_from_internet(query=query, top_k=3)
|
|
106
142
|
context.extend(item.memory.partition("\nContent: ")[-1] for item in extra)
|
|
143
|
+
"""
|
|
107
144
|
|
|
108
145
|
# parse goal using LLM
|
|
109
146
|
parsed_goal = self.task_goal_parser.parse(
|
|
@@ -121,10 +158,20 @@ class Searcher:
|
|
|
121
158
|
return parsed_goal, query_embedding, context, query
|
|
122
159
|
|
|
123
160
|
@timed
|
|
124
|
-
def _retrieve_paths(
|
|
161
|
+
def _retrieve_paths(
|
|
162
|
+
self,
|
|
163
|
+
query,
|
|
164
|
+
parsed_goal,
|
|
165
|
+
query_embedding,
|
|
166
|
+
info,
|
|
167
|
+
top_k,
|
|
168
|
+
mode,
|
|
169
|
+
memory_type,
|
|
170
|
+
search_filter: dict | None = None,
|
|
171
|
+
):
|
|
125
172
|
"""Run A/B/C retrieval paths in parallel"""
|
|
126
173
|
tasks = []
|
|
127
|
-
with
|
|
174
|
+
with ContextThreadPoolExecutor(max_workers=3) as executor:
|
|
128
175
|
tasks.append(
|
|
129
176
|
executor.submit(
|
|
130
177
|
self._retrieve_from_working_memory,
|
|
@@ -133,6 +180,7 @@ class Searcher:
|
|
|
133
180
|
query_embedding,
|
|
134
181
|
top_k,
|
|
135
182
|
memory_type,
|
|
183
|
+
search_filter,
|
|
136
184
|
)
|
|
137
185
|
)
|
|
138
186
|
tasks.append(
|
|
@@ -143,6 +191,7 @@ class Searcher:
|
|
|
143
191
|
query_embedding,
|
|
144
192
|
top_k,
|
|
145
193
|
memory_type,
|
|
194
|
+
search_filter,
|
|
146
195
|
)
|
|
147
196
|
)
|
|
148
197
|
tasks.append(
|
|
@@ -157,6 +206,17 @@ class Searcher:
|
|
|
157
206
|
memory_type,
|
|
158
207
|
)
|
|
159
208
|
)
|
|
209
|
+
if self.moscube:
|
|
210
|
+
tasks.append(
|
|
211
|
+
executor.submit(
|
|
212
|
+
self._retrieve_from_memcubes,
|
|
213
|
+
query,
|
|
214
|
+
parsed_goal,
|
|
215
|
+
query_embedding,
|
|
216
|
+
top_k,
|
|
217
|
+
"memos_cube01",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
160
220
|
|
|
161
221
|
results = []
|
|
162
222
|
for t in tasks:
|
|
@@ -168,14 +228,24 @@ class Searcher:
|
|
|
168
228
|
# --- Path A
|
|
169
229
|
@timed
|
|
170
230
|
def _retrieve_from_working_memory(
|
|
171
|
-
self,
|
|
231
|
+
self,
|
|
232
|
+
query,
|
|
233
|
+
parsed_goal,
|
|
234
|
+
query_embedding,
|
|
235
|
+
top_k,
|
|
236
|
+
memory_type,
|
|
237
|
+
search_filter: dict | None = None,
|
|
172
238
|
):
|
|
173
239
|
"""Retrieve and rerank from WorkingMemory"""
|
|
174
240
|
if memory_type not in ["All", "WorkingMemory"]:
|
|
175
241
|
logger.info(f"[PATH-A] '{query}'Skipped (memory_type does not match)")
|
|
176
242
|
return []
|
|
177
243
|
items = self.graph_retriever.retrieve(
|
|
178
|
-
query=query,
|
|
244
|
+
query=query,
|
|
245
|
+
parsed_goal=parsed_goal,
|
|
246
|
+
top_k=top_k,
|
|
247
|
+
memory_scope="WorkingMemory",
|
|
248
|
+
search_filter=search_filter,
|
|
179
249
|
)
|
|
180
250
|
return self.reranker.rerank(
|
|
181
251
|
query=query,
|
|
@@ -183,36 +253,79 @@ class Searcher:
|
|
|
183
253
|
graph_results=items,
|
|
184
254
|
top_k=top_k,
|
|
185
255
|
parsed_goal=parsed_goal,
|
|
256
|
+
search_filter=search_filter,
|
|
186
257
|
)
|
|
187
258
|
|
|
188
259
|
# --- Path B
|
|
189
260
|
@timed
|
|
190
261
|
def _retrieve_from_long_term_and_user(
|
|
191
|
-
self,
|
|
262
|
+
self,
|
|
263
|
+
query,
|
|
264
|
+
parsed_goal,
|
|
265
|
+
query_embedding,
|
|
266
|
+
top_k,
|
|
267
|
+
memory_type,
|
|
268
|
+
search_filter: dict | None = None,
|
|
192
269
|
):
|
|
193
270
|
"""Retrieve and rerank from LongTermMemory and UserMemory"""
|
|
194
271
|
results = []
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
272
|
+
tasks = []
|
|
273
|
+
|
|
274
|
+
with ContextThreadPoolExecutor(max_workers=2) as executor:
|
|
275
|
+
if memory_type in ["All", "LongTermMemory"]:
|
|
276
|
+
tasks.append(
|
|
277
|
+
executor.submit(
|
|
278
|
+
self.graph_retriever.retrieve,
|
|
279
|
+
query=query,
|
|
280
|
+
parsed_goal=parsed_goal,
|
|
281
|
+
query_embedding=query_embedding,
|
|
282
|
+
top_k=top_k * 2,
|
|
283
|
+
memory_scope="LongTermMemory",
|
|
284
|
+
search_filter=search_filter,
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
if memory_type in ["All", "UserMemory"]:
|
|
288
|
+
tasks.append(
|
|
289
|
+
executor.submit(
|
|
290
|
+
self.graph_retriever.retrieve,
|
|
291
|
+
query=query,
|
|
292
|
+
parsed_goal=parsed_goal,
|
|
293
|
+
query_embedding=query_embedding,
|
|
294
|
+
top_k=top_k * 2,
|
|
295
|
+
memory_scope="UserMemory",
|
|
296
|
+
search_filter=search_filter,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Collect results from all tasks
|
|
301
|
+
for task in tasks:
|
|
302
|
+
results.extend(task.result())
|
|
303
|
+
|
|
211
304
|
return self.reranker.rerank(
|
|
212
305
|
query=query,
|
|
213
306
|
query_embedding=query_embedding[0],
|
|
214
307
|
graph_results=results,
|
|
308
|
+
top_k=top_k,
|
|
309
|
+
parsed_goal=parsed_goal,
|
|
310
|
+
search_filter=search_filter,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
@timed
|
|
314
|
+
def _retrieve_from_memcubes(
|
|
315
|
+
self, query, parsed_goal, query_embedding, top_k, cube_name="memos_cube01"
|
|
316
|
+
):
|
|
317
|
+
"""Retrieve and rerank from LongTermMemory and UserMemory"""
|
|
318
|
+
results = self.graph_retriever.retrieve_from_cube(
|
|
319
|
+
query_embedding=query_embedding,
|
|
215
320
|
top_k=top_k * 2,
|
|
321
|
+
memory_scope="LongTermMemory",
|
|
322
|
+
cube_name=cube_name,
|
|
323
|
+
)
|
|
324
|
+
return self.reranker.rerank(
|
|
325
|
+
query=query,
|
|
326
|
+
query_embedding=query_embedding[0],
|
|
327
|
+
graph_results=results,
|
|
328
|
+
top_k=top_k,
|
|
216
329
|
parsed_goal=parsed_goal,
|
|
217
330
|
)
|
|
218
331
|
|
|
@@ -256,8 +369,7 @@ class Searcher:
|
|
|
256
369
|
final_items = []
|
|
257
370
|
for item, score in sorted_results:
|
|
258
371
|
meta_data = item.metadata.model_dump()
|
|
259
|
-
|
|
260
|
-
meta_data["relativity"] = score
|
|
372
|
+
meta_data["relativity"] = score
|
|
261
373
|
final_items.append(
|
|
262
374
|
TextualMemoryItem(
|
|
263
375
|
id=item.id,
|
|
@@ -271,14 +383,30 @@ class Searcher:
|
|
|
271
383
|
def _update_usage_history(self, items, info):
|
|
272
384
|
"""Update usage history in graph DB"""
|
|
273
385
|
now_time = datetime.now().isoformat()
|
|
274
|
-
info
|
|
275
|
-
|
|
276
|
-
usage_record = json.dumps({"time": now_time, "info":
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
386
|
+
info_copy = dict(info or {})
|
|
387
|
+
info_copy.pop("chat_history", None)
|
|
388
|
+
usage_record = json.dumps({"time": now_time, "info": info_copy})
|
|
389
|
+
payload = []
|
|
390
|
+
for it in items:
|
|
391
|
+
try:
|
|
392
|
+
item_id = getattr(it, "id", None)
|
|
393
|
+
md = getattr(it, "metadata", None)
|
|
394
|
+
if md is None:
|
|
395
|
+
continue
|
|
396
|
+
if not hasattr(md, "usage") or md.usage is None:
|
|
397
|
+
md.usage = []
|
|
398
|
+
md.usage.append(usage_record)
|
|
399
|
+
if item_id:
|
|
400
|
+
payload.append((item_id, list(md.usage)))
|
|
401
|
+
except Exception:
|
|
402
|
+
logger.exception("[USAGE] snapshot item failed")
|
|
403
|
+
|
|
404
|
+
if payload:
|
|
405
|
+
self._usage_executor.submit(self._update_usage_history_worker, payload, usage_record)
|
|
406
|
+
|
|
407
|
+
def _update_usage_history_worker(self, payload, usage_record: str):
|
|
408
|
+
try:
|
|
409
|
+
for item_id, usage_list in payload:
|
|
410
|
+
self.graph_store.update_node(item_id, {"usage": usage_list})
|
|
411
|
+
except Exception:
|
|
412
|
+
logger.exception("[USAGE] update usage failed")
|
|
@@ -8,18 +8,20 @@ You are a task parsing expert. Given a user task instruction, optional former co
|
|
|
8
8
|
5. Need for internet search: If the user's task instruction only involves objective facts or can be completed without introducing external knowledge, set "internet_search" to False. Otherwise, set it to True.
|
|
9
9
|
6. Memories: Provide 2–5 short semantic expansions or rephrasings of the rephrased/original user task instruction. These are used for improved embedding search coverage. Each should be clear, concise, and meaningful for retrieval.
|
|
10
10
|
|
|
11
|
-
Task description:
|
|
12
|
-
\"\"\"$task\"\"\"
|
|
13
|
-
|
|
14
11
|
Former conversation (if any):
|
|
15
12
|
\"\"\"
|
|
16
13
|
$conversation
|
|
17
14
|
\"\"\"
|
|
18
15
|
|
|
16
|
+
Task description(User Question):
|
|
17
|
+
\"\"\"$task\"\"\"
|
|
18
|
+
|
|
19
19
|
Context (if any):
|
|
20
20
|
\"\"\"$context\"\"\"
|
|
21
21
|
|
|
22
|
-
Return strictly in this JSON format
|
|
22
|
+
Return strictly in this JSON format, note that the
|
|
23
|
+
keys/tags/rephrased_instruction/memories should use the same language as the
|
|
24
|
+
input query:
|
|
23
25
|
{
|
|
24
26
|
"keys": [...],
|
|
25
27
|
"tags": [...],
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import uuid
|
|
5
5
|
|
|
6
|
-
from concurrent.futures import
|
|
6
|
+
from concurrent.futures import as_completed
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
|
+
from memos.context.context import ContextThreadPoolExecutor
|
|
11
12
|
from memos.embedders.factory import OllamaEmbedder
|
|
12
13
|
from memos.log import get_logger
|
|
13
14
|
from memos.mem_reader.base import BaseMemReader
|
|
14
|
-
from memos.memories.textual.item import TextualMemoryItem
|
|
15
|
+
from memos.memories.textual.item import SourceMessage, TextualMemoryItem
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
logger = get_logger(__name__)
|
|
@@ -150,7 +151,7 @@ class XinyuSearchRetriever:
|
|
|
150
151
|
# Convert to TextualMemoryItem format
|
|
151
152
|
memory_items: list[TextualMemoryItem] = []
|
|
152
153
|
|
|
153
|
-
with
|
|
154
|
+
with ContextThreadPoolExecutor(max_workers=8) as executor:
|
|
154
155
|
futures = [
|
|
155
156
|
executor.submit(self._process_result, result, query, parsed_goal, info)
|
|
156
157
|
for result in search_results
|
|
@@ -332,7 +333,7 @@ class XinyuSearchRetriever:
|
|
|
332
333
|
)
|
|
333
334
|
read_item_i.metadata.source = "web"
|
|
334
335
|
read_item_i.metadata.memory_type = "OuterMemory"
|
|
335
|
-
read_item_i.metadata.sources = [url] if url else []
|
|
336
|
+
read_item_i.metadata.sources = [SourceMessage(type="web", url=url)] if url else []
|
|
336
337
|
read_item_i.metadata.visibility = "public"
|
|
337
338
|
|
|
338
339
|
memory_items.append(read_item_i)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Notification utilities for MemOS product.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import logging
|
|
6
7
|
|
|
7
8
|
from collections.abc import Callable
|
|
@@ -51,6 +52,51 @@ def send_online_bot_notification(
|
|
|
51
52
|
logger.warning(f"Failed to send online bot notification: {e}")
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
async def send_online_bot_notification_async(
|
|
56
|
+
online_bot: Callable | None,
|
|
57
|
+
header_name: str,
|
|
58
|
+
sub_title_name: str,
|
|
59
|
+
title_color: str,
|
|
60
|
+
other_data1: dict[str, Any],
|
|
61
|
+
other_data2: dict[str, Any],
|
|
62
|
+
emoji: dict[str, str],
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Send notification via online_bot asynchronously if available.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
online_bot: The online_bot function or None
|
|
69
|
+
header_name: Header name for the report
|
|
70
|
+
sub_title_name: Subtitle for the report
|
|
71
|
+
title_color: Title color
|
|
72
|
+
other_data1: First data dict
|
|
73
|
+
other_data2: Second data dict
|
|
74
|
+
emoji: Emoji configuration dict
|
|
75
|
+
"""
|
|
76
|
+
if online_bot is None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Run the potentially blocking notification in a thread pool
|
|
81
|
+
loop = asyncio.get_event_loop()
|
|
82
|
+
await loop.run_in_executor(
|
|
83
|
+
None,
|
|
84
|
+
lambda: online_bot(
|
|
85
|
+
header_name=header_name,
|
|
86
|
+
sub_title_name=sub_title_name,
|
|
87
|
+
title_color=title_color,
|
|
88
|
+
other_data1=other_data1,
|
|
89
|
+
other_data2=other_data2,
|
|
90
|
+
emoji=emoji,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
logger.info(f"Online bot notification sent successfully (async): {header_name}")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning(f"Failed to send online bot notification (async): {e}")
|
|
98
|
+
|
|
99
|
+
|
|
54
100
|
def send_error_bot_notification(
|
|
55
101
|
error_bot: Callable | None,
|
|
56
102
|
err: str,
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Singleton decorator module for caching factory instances to avoid excessive memory usage
|
|
3
|
+
from repeated initialization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
|
+
from weakref import WeakValueDictionary
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FactorySingleton:
|
|
19
|
+
"""Factory singleton manager that caches instances based on configuration parameters"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
# Use weak reference dictionary for automatic cleanup when instances are no longer referenced
|
|
23
|
+
self._instances: dict[str, WeakValueDictionary] = {}
|
|
24
|
+
|
|
25
|
+
def _generate_cache_key(self, config: Any, *args, **kwargs) -> str:
|
|
26
|
+
"""Generate cache key based on configuration only (ignoring other parameters)"""
|
|
27
|
+
|
|
28
|
+
# Handle configuration objects - only use the config parameter
|
|
29
|
+
if hasattr(config, "model_dump"): # Pydantic model
|
|
30
|
+
config_data = config.model_dump()
|
|
31
|
+
elif hasattr(config, "dict"): # Legacy Pydantic model
|
|
32
|
+
config_data = config.dict()
|
|
33
|
+
elif isinstance(config, dict):
|
|
34
|
+
config_data = config
|
|
35
|
+
else:
|
|
36
|
+
# For other types, try to convert to string
|
|
37
|
+
config_data = str(config)
|
|
38
|
+
|
|
39
|
+
# Filter out time-related fields that shouldn't affect caching
|
|
40
|
+
filtered_config = self._filter_temporal_fields(config_data)
|
|
41
|
+
|
|
42
|
+
# Generate hash key based only on config
|
|
43
|
+
try:
|
|
44
|
+
cache_str = json.dumps(filtered_config, sort_keys=True, ensure_ascii=False, default=str)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
# If JSON serialization fails, convert the entire config to string
|
|
47
|
+
cache_str = str(filtered_config)
|
|
48
|
+
|
|
49
|
+
return hashlib.md5(cache_str.encode("utf-8")).hexdigest()
|
|
50
|
+
|
|
51
|
+
def _filter_temporal_fields(self, config_data: Any) -> Any:
|
|
52
|
+
"""Filter out temporal fields that shouldn't affect instance caching"""
|
|
53
|
+
if isinstance(config_data, dict):
|
|
54
|
+
filtered = {}
|
|
55
|
+
for key, value in config_data.items():
|
|
56
|
+
# Skip common temporal field names
|
|
57
|
+
if key.lower() in {
|
|
58
|
+
"created_at",
|
|
59
|
+
"updated_at",
|
|
60
|
+
"timestamp",
|
|
61
|
+
"time",
|
|
62
|
+
"date",
|
|
63
|
+
"created_time",
|
|
64
|
+
"updated_time",
|
|
65
|
+
"last_modified",
|
|
66
|
+
"modified_at",
|
|
67
|
+
"start_time",
|
|
68
|
+
"end_time",
|
|
69
|
+
"execution_time",
|
|
70
|
+
"run_time",
|
|
71
|
+
}:
|
|
72
|
+
continue
|
|
73
|
+
# Recursively filter nested dictionaries
|
|
74
|
+
filtered[key] = self._filter_temporal_fields(value)
|
|
75
|
+
return filtered
|
|
76
|
+
elif isinstance(config_data, list):
|
|
77
|
+
# Recursively filter lists
|
|
78
|
+
return [self._filter_temporal_fields(item) for item in config_data]
|
|
79
|
+
else:
|
|
80
|
+
# For primitive types, return as-is
|
|
81
|
+
return config_data
|
|
82
|
+
|
|
83
|
+
def get_or_create(self, factory_class: type, cache_key: str, creator_func: Callable) -> Any:
|
|
84
|
+
"""Get or create instance"""
|
|
85
|
+
class_name = factory_class.__name__
|
|
86
|
+
|
|
87
|
+
if class_name not in self._instances:
|
|
88
|
+
self._instances[class_name] = WeakValueDictionary()
|
|
89
|
+
|
|
90
|
+
class_cache = self._instances[class_name]
|
|
91
|
+
|
|
92
|
+
if cache_key in class_cache:
|
|
93
|
+
return class_cache[cache_key]
|
|
94
|
+
|
|
95
|
+
# Create new instance
|
|
96
|
+
instance = creator_func()
|
|
97
|
+
class_cache[cache_key] = instance
|
|
98
|
+
return instance
|
|
99
|
+
|
|
100
|
+
def clear_cache(self, factory_class: type | None = None):
|
|
101
|
+
"""Clear cache"""
|
|
102
|
+
if factory_class:
|
|
103
|
+
class_name = factory_class.__name__
|
|
104
|
+
if class_name in self._instances:
|
|
105
|
+
self._instances[class_name].clear()
|
|
106
|
+
else:
|
|
107
|
+
for cache in self._instances.values():
|
|
108
|
+
cache.clear()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Global singleton manager
|
|
112
|
+
_factory_singleton = FactorySingleton()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def singleton_factory(factory_class: type | str | None = None):
|
|
116
|
+
"""
|
|
117
|
+
Factory singleton decorator
|
|
118
|
+
|
|
119
|
+
Usage:
|
|
120
|
+
@singleton_factory()
|
|
121
|
+
def from_config(cls, config):
|
|
122
|
+
return SomeClass(config)
|
|
123
|
+
|
|
124
|
+
Or specify factory class:
|
|
125
|
+
@singleton_factory(EmbedderFactory)
|
|
126
|
+
def from_config(cls, config):
|
|
127
|
+
return SomeClass(config)
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
131
|
+
@wraps(func)
|
|
132
|
+
def wrapper(*args, **kwargs) -> T:
|
|
133
|
+
# Determine factory class and config parameter
|
|
134
|
+
target_factory_class = factory_class
|
|
135
|
+
config = None
|
|
136
|
+
|
|
137
|
+
# Simple logic: check if first parameter is a class or config
|
|
138
|
+
if args:
|
|
139
|
+
if hasattr(args[0], "__name__") and hasattr(args[0], "__module__"):
|
|
140
|
+
# First parameter is a class (cls), so this is a @classmethod
|
|
141
|
+
if target_factory_class is None:
|
|
142
|
+
target_factory_class = args[0]
|
|
143
|
+
config = args[1] if len(args) > 1 else None
|
|
144
|
+
else:
|
|
145
|
+
# First parameter is config, so this is a @staticmethod
|
|
146
|
+
if target_factory_class is None:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"Factory class must be explicitly specified for static methods"
|
|
149
|
+
)
|
|
150
|
+
if isinstance(target_factory_class, str):
|
|
151
|
+
# Convert string to a mock class for caching purposes
|
|
152
|
+
class MockFactoryClass:
|
|
153
|
+
__name__ = target_factory_class
|
|
154
|
+
|
|
155
|
+
target_factory_class = MockFactoryClass
|
|
156
|
+
config = args[0]
|
|
157
|
+
|
|
158
|
+
if config is None:
|
|
159
|
+
# If no configuration parameter, call original function directly
|
|
160
|
+
return func(*args, **kwargs)
|
|
161
|
+
|
|
162
|
+
# Generate cache key based only on config
|
|
163
|
+
cache_key = _factory_singleton._generate_cache_key(config)
|
|
164
|
+
|
|
165
|
+
# Function to create instance
|
|
166
|
+
def creator():
|
|
167
|
+
return func(*args, **kwargs)
|
|
168
|
+
|
|
169
|
+
# Get or create instance
|
|
170
|
+
return _factory_singleton.get_or_create(target_factory_class, cache_key, creator)
|
|
171
|
+
|
|
172
|
+
return wrapper
|
|
173
|
+
|
|
174
|
+
return decorator
|