MemoryOS 1.0.0__py3-none-any.whl → 1.0.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.

Files changed (42) hide show
  1. {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/METADATA +2 -1
  2. {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/RECORD +42 -33
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +25 -0
  5. memos/api/context/context_thread.py +96 -0
  6. memos/api/context/dependencies.py +0 -11
  7. memos/api/middleware/request_context.py +94 -0
  8. memos/api/product_api.py +5 -1
  9. memos/api/product_models.py +16 -0
  10. memos/api/routers/product_router.py +39 -3
  11. memos/api/start_api.py +3 -0
  12. memos/configs/memory.py +13 -0
  13. memos/configs/reranker.py +18 -0
  14. memos/graph_dbs/base.py +4 -2
  15. memos/graph_dbs/nebular.py +215 -68
  16. memos/graph_dbs/neo4j.py +14 -12
  17. memos/graph_dbs/neo4j_community.py +6 -3
  18. memos/llms/vllm.py +2 -0
  19. memos/log.py +120 -8
  20. memos/mem_os/core.py +30 -2
  21. memos/mem_os/product.py +386 -146
  22. memos/mem_os/utils/reference_utils.py +20 -0
  23. memos/mem_reader/simple_struct.py +112 -43
  24. memos/mem_user/mysql_user_manager.py +4 -2
  25. memos/memories/textual/item.py +1 -1
  26. memos/memories/textual/tree.py +31 -1
  27. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +3 -1
  28. memos/memories/textual/tree_text_memory/retrieve/recall.py +53 -3
  29. memos/memories/textual/tree_text_memory/retrieve/searcher.py +74 -14
  30. memos/memories/textual/tree_text_memory/retrieve/utils.py +6 -4
  31. memos/memos_tools/notification_utils.py +46 -0
  32. memos/reranker/__init__.py +4 -0
  33. memos/reranker/base.py +24 -0
  34. memos/reranker/cosine_local.py +95 -0
  35. memos/reranker/factory.py +43 -0
  36. memos/reranker/http_bge.py +99 -0
  37. memos/reranker/noop.py +16 -0
  38. memos/templates/mem_reader_prompts.py +289 -40
  39. memos/templates/mos_prompts.py +133 -60
  40. {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/LICENSE +0 -0
  41. {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/WHEEL +0 -0
  42. {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,8 @@
1
+ from memos.memories.textual.item import (
2
+ TextualMemoryItem,
3
+ )
4
+
5
+
1
6
  def split_continuous_references(text: str) -> str:
2
7
  """
3
8
  Split continuous reference tags into individual reference tags.
@@ -131,3 +136,18 @@ def process_streaming_references_complete(text_buffer: str) -> tuple[str, str]:
131
136
  # No reference-like patterns found, process all text
132
137
  processed_text = split_continuous_references(text_buffer)
133
138
  return processed_text, ""
139
+
140
+
141
+ def prepare_reference_data(memories_list: list[TextualMemoryItem]) -> list[dict]:
142
+ # Prepare reference data
143
+ reference = []
144
+ for memories in memories_list:
145
+ memories_json = memories.model_dump()
146
+ memories_json["metadata"]["ref_id"] = f"{memories.id.split('-')[0]}"
147
+ memories_json["metadata"]["embedding"] = []
148
+ memories_json["metadata"]["sources"] = []
149
+ memories_json["metadata"]["memory"] = memories.memory
150
+ memories_json["metadata"]["id"] = memories.id
151
+ reference.append({"metadata": memories_json["metadata"]})
152
+
153
+ return reference
@@ -1,10 +1,14 @@
1
1
  import concurrent.futures
2
2
  import copy
3
3
  import json
4
+ import os
5
+ import re
4
6
 
5
7
  from abc import ABC
6
8
  from typing import Any
7
9
 
10
+ from tqdm import tqdm
11
+
8
12
  from memos import log
9
13
  from memos.chunkers import ChunkerFactory
10
14
  from memos.configs.mem_reader import SimpleStructMemReaderConfig
@@ -16,12 +20,79 @@ from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemory
16
20
  from memos.parsers.factory import ParserFactory
17
21
  from memos.templates.mem_reader_prompts import (
18
22
  SIMPLE_STRUCT_DOC_READER_PROMPT,
23
+ SIMPLE_STRUCT_DOC_READER_PROMPT_ZH,
19
24
  SIMPLE_STRUCT_MEM_READER_EXAMPLE,
25
+ SIMPLE_STRUCT_MEM_READER_EXAMPLE_ZH,
20
26
  SIMPLE_STRUCT_MEM_READER_PROMPT,
27
+ SIMPLE_STRUCT_MEM_READER_PROMPT_ZH,
21
28
  )
22
29
 
23
30
 
24
31
  logger = log.get_logger(__name__)
32
+ PROMPT_DICT = {
33
+ "chat": {
34
+ "en": SIMPLE_STRUCT_MEM_READER_PROMPT,
35
+ "zh": SIMPLE_STRUCT_MEM_READER_PROMPT_ZH,
36
+ "en_example": SIMPLE_STRUCT_MEM_READER_EXAMPLE,
37
+ "zh_example": SIMPLE_STRUCT_MEM_READER_EXAMPLE_ZH,
38
+ },
39
+ "doc": {"en": SIMPLE_STRUCT_DOC_READER_PROMPT, "zh": SIMPLE_STRUCT_DOC_READER_PROMPT_ZH},
40
+ }
41
+
42
+
43
+ def detect_lang(text):
44
+ try:
45
+ if not text or not isinstance(text, str):
46
+ return "en"
47
+ chinese_pattern = r"[\u4e00-\u9fff\u3400-\u4dbf\U00020000-\U0002a6df\U0002a700-\U0002b73f\U0002b740-\U0002b81f\U0002b820-\U0002ceaf\uf900-\ufaff]"
48
+ chinese_chars = re.findall(chinese_pattern, text)
49
+ if len(chinese_chars) / len(re.sub(r"[\s\d\W]", "", text)) > 0.3:
50
+ return "zh"
51
+ return "en"
52
+ except Exception:
53
+ return "en"
54
+
55
+
56
+ def _build_node(idx, message, info, scene_file, llm, parse_json_result, embedder):
57
+ # generate
58
+ raw = llm.generate(message)
59
+ if not raw:
60
+ return None
61
+
62
+ # parse_json_result
63
+ chunk_res = parse_json_result(raw)
64
+ if not chunk_res:
65
+ return None
66
+
67
+ value = chunk_res.get("value")
68
+ if not value:
69
+ return None
70
+
71
+ # embed
72
+ embedding = embedder.embed([value])[0]
73
+
74
+ # TextualMemoryItem
75
+ tags = chunk_res["tags"] if isinstance(chunk_res.get("tags"), list) else []
76
+ key = chunk_res.get("key", None)
77
+
78
+ node_i = TextualMemoryItem(
79
+ memory=value,
80
+ metadata=TreeNodeTextualMemoryMetadata(
81
+ user_id=info.get("user_id"),
82
+ session_id=info.get("session_id"),
83
+ memory_type="LongTermMemory",
84
+ status="activated",
85
+ tags=tags,
86
+ key=key,
87
+ embedding=embedding,
88
+ usage=[],
89
+ sources=[f"{scene_file}_{idx}"],
90
+ background="",
91
+ confidence=0.99,
92
+ type="fact",
93
+ ),
94
+ )
95
+ return node_i
25
96
 
26
97
 
27
98
  class SimpleStructMemReader(BaseMemReader, ABC):
@@ -40,11 +111,13 @@ class SimpleStructMemReader(BaseMemReader, ABC):
40
111
  self.chunker = ChunkerFactory.from_config(config.chunker)
41
112
 
42
113
  def _process_chat_data(self, scene_data_info, info):
43
- prompt = SIMPLE_STRUCT_MEM_READER_PROMPT.replace(
44
- "${conversation}", "\n".join(scene_data_info)
45
- )
114
+ lang = detect_lang("\n".join(scene_data_info))
115
+ template = PROMPT_DICT["chat"][lang]
116
+ examples = PROMPT_DICT["chat"][f"{lang}_example"]
117
+
118
+ prompt = template.replace("${conversation}", "\n".join(scene_data_info))
46
119
  if self.config.remove_prompt_example:
47
- prompt = prompt.replace(SIMPLE_STRUCT_MEM_READER_EXAMPLE, "")
120
+ prompt = prompt.replace(examples, "")
48
121
 
49
122
  messages = [{"role": "user", "content": prompt}]
50
123
 
@@ -180,7 +253,7 @@ class SimpleStructMemReader(BaseMemReader, ABC):
180
253
  elif type == "doc":
181
254
  for item in scene_data:
182
255
  try:
183
- if not isinstance(item, str):
256
+ if os.path.exists(item):
184
257
  parsed_text = parser.parse(item)
185
258
  results.append({"file": "pure_text", "text": parsed_text})
186
259
  else:
@@ -193,46 +266,42 @@ class SimpleStructMemReader(BaseMemReader, ABC):
193
266
 
194
267
  def _process_doc_data(self, scene_data_info, info):
195
268
  chunks = self.chunker.chunk(scene_data_info["text"])
196
- messages = [
197
- [
198
- {
199
- "role": "user",
200
- "content": SIMPLE_STRUCT_DOC_READER_PROMPT.replace("{chunk_text}", chunk.text),
201
- }
202
- ]
203
- for chunk in chunks
204
- ]
205
-
206
- processed_chunks = []
207
- with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
208
- futures = [executor.submit(self.llm.generate, message) for message in messages]
209
- for future in concurrent.futures.as_completed(futures):
210
- chunk_result = future.result()
211
- if chunk_result:
212
- processed_chunks.append(chunk_result)
269
+ messages = []
270
+ for chunk in chunks:
271
+ lang = detect_lang(chunk.text)
272
+ template = PROMPT_DICT["doc"][lang]
273
+ prompt = template.replace("{chunk_text}", chunk.text)
274
+ message = [{"role": "user", "content": prompt}]
275
+ messages.append(message)
213
276
 
214
- processed_chunks = [self.parse_json_result(r) for r in processed_chunks]
215
277
  doc_nodes = []
216
- for i, chunk_res in enumerate(processed_chunks):
217
- if chunk_res:
218
- node_i = TextualMemoryItem(
219
- memory=chunk_res["value"],
220
- metadata=TreeNodeTextualMemoryMetadata(
221
- user_id=info.get("user_id"),
222
- session_id=info.get("session_id"),
223
- memory_type="LongTermMemory",
224
- status="activated",
225
- tags=chunk_res["tags"] if type(chunk_res["tags"]) is list else [],
226
- key=chunk_res["key"],
227
- embedding=self.embedder.embed([chunk_res["value"]])[0],
228
- usage=[],
229
- sources=[f"{scene_data_info['file']}_{i}"],
230
- background="",
231
- confidence=0.99,
232
- type="fact",
233
- ),
234
- )
235
- doc_nodes.append(node_i)
278
+ scene_file = scene_data_info["file"]
279
+
280
+ with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
281
+ futures = {
282
+ executor.submit(
283
+ _build_node,
284
+ idx,
285
+ msg,
286
+ info,
287
+ scene_file,
288
+ self.llm,
289
+ self.parse_json_result,
290
+ self.embedder,
291
+ ): idx
292
+ for idx, msg in enumerate(messages)
293
+ }
294
+ total = len(futures)
295
+
296
+ for future in tqdm(
297
+ concurrent.futures.as_completed(futures), total=total, desc="Processing"
298
+ ):
299
+ try:
300
+ node = future.result()
301
+ if node:
302
+ doc_nodes.append(node)
303
+ except Exception as e:
304
+ tqdm.write(f"[ERROR] {e}")
236
305
  return doc_nodes
237
306
 
238
307
  def parse_json_result(self, response_text):
@@ -55,7 +55,9 @@ class User(Base):
55
55
 
56
56
  user_id = Column(String(255), primary_key=True, default=lambda: str(uuid.uuid4()))
57
57
  user_name = Column(String(255), unique=True, nullable=False)
58
- role = Column(String(20), default=UserRole.USER.value, nullable=False)
58
+ role = Column(
59
+ String(20), default=UserRole.USER.value, nullable=False
60
+ ) # for sqlite backend this is SQLEnum
59
61
  created_at = Column(DateTime, default=datetime.now, nullable=False)
60
62
  updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False)
61
63
  is_active = Column(Boolean, default=True, nullable=False)
@@ -65,7 +67,7 @@ class User(Base):
65
67
  owned_cubes = relationship("Cube", back_populates="owner", cascade="all, delete-orphan")
66
68
 
67
69
  def __repr__(self):
68
- return f"<User(user_id='{self.user_id}', user_name='{self.user_name}', role='{self.role.value}')>"
70
+ return f"<User(user_id='{self.user_id}', user_name='{self.user_name}', role='{self.role}')>"
69
71
 
70
72
 
71
73
  class Cube(Base):
@@ -33,7 +33,7 @@ class TextualMemoryMetadata(BaseModel):
33
33
  default=None,
34
34
  description="A numeric score (float between 0 and 100) indicating how certain you are about the accuracy or reliability of the memory.",
35
35
  )
36
- source: Literal["conversation", "retrieved", "web", "file"] | None = Field(
36
+ source: Literal["conversation", "retrieved", "web", "file", "system"] | None = Field(
37
37
  default=None, description="The origin of the memory"
38
38
  )
39
39
  tags: list[str] | None = Field(
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import Any
9
9
 
10
10
  from memos.configs.memory import TreeTextMemoryConfig
11
+ from memos.configs.reranker import RerankerConfigFactory
11
12
  from memos.embedders.factory import EmbedderFactory, OllamaEmbedder
12
13
  from memos.graph_dbs.factory import GraphStoreFactory, Neo4jGraphDB
13
14
  from memos.llms.factory import AzureLLM, LLMFactory, OllamaLLM, OpenAILLM
@@ -19,6 +20,7 @@ from memos.memories.textual.tree_text_memory.retrieve.internet_retriever_factory
19
20
  InternetRetrieverFactory,
20
21
  )
21
22
  from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher
23
+ from memos.reranker.factory import RerankerFactory
22
24
  from memos.types import MessageList
23
25
 
24
26
 
@@ -39,10 +41,33 @@ class TreeTextMemory(BaseTextMemory):
39
41
  )
40
42
  self.embedder: OllamaEmbedder = EmbedderFactory.from_config(config.embedder)
41
43
  self.graph_store: Neo4jGraphDB = GraphStoreFactory.from_config(config.graph_db)
44
+ if config.reranker is None:
45
+ default_cfg = RerankerConfigFactory.model_validate(
46
+ {
47
+ "backend": "cosine_local",
48
+ "config": {
49
+ "level_weights": {"topic": 1.0, "concept": 1.0, "fact": 1.0},
50
+ "level_field": "background",
51
+ },
52
+ }
53
+ )
54
+ self.reranker = RerankerFactory.from_config(default_cfg)
55
+ else:
56
+ self.reranker = RerankerFactory.from_config(config.reranker)
57
+
42
58
  self.is_reorganize = config.reorganize
43
59
 
44
60
  self.memory_manager: MemoryManager = MemoryManager(
45
- self.graph_store, self.embedder, self.extractor_llm, is_reorganize=self.is_reorganize
61
+ self.graph_store,
62
+ self.embedder,
63
+ self.extractor_llm,
64
+ memory_size=config.memory_size
65
+ or {
66
+ "WorkingMemory": 20,
67
+ "LongTermMemory": 1500,
68
+ "UserMemory": 480,
69
+ },
70
+ is_reorganize=self.is_reorganize,
46
71
  )
47
72
 
48
73
  # Create internet retriever if configured
@@ -96,6 +121,7 @@ class TreeTextMemory(BaseTextMemory):
96
121
  mode: str = "fast",
97
122
  memory_type: str = "All",
98
123
  manual_close_internet: bool = False,
124
+ moscube: bool = False,
99
125
  ) -> list[TextualMemoryItem]:
100
126
  """Search for memories based on a query.
101
127
  User query -> TaskGoalParser -> MemoryPathResolver ->
@@ -121,14 +147,18 @@ class TreeTextMemory(BaseTextMemory):
121
147
  self.dispatcher_llm,
122
148
  self.graph_store,
123
149
  self.embedder,
150
+ self.reranker,
124
151
  internet_retriever=None,
152
+ moscube=moscube,
125
153
  )
126
154
  else:
127
155
  searcher = Searcher(
128
156
  self.dispatcher_llm,
129
157
  self.graph_store,
130
158
  self.embedder,
159
+ self.reranker,
131
160
  internet_retriever=self.internet_retriever,
161
+ moscube=moscube,
132
162
  )
133
163
  return searcher.search(query, top_k, info, mode, memory_type)
134
164
 
@@ -218,7 +218,9 @@ class BochaAISearchRetriever:
218
218
  memory_items = []
219
219
  for read_item_i in read_items[0]:
220
220
  read_item_i.memory = (
221
- f"Title: {title}\nNewsTime: {publish_time}\nSummary: {summary}\n"
221
+ f"[Outer internet view] Title: {title}\nNewsTime:"
222
+ f" {publish_time}\nSummary:"
223
+ f" {summary}\n"
222
224
  f"Content: {read_item_i.memory}"
223
225
  )
224
226
  read_item_i.metadata.source = "web"
@@ -74,6 +74,51 @@ class GraphMemoryRetriever:
74
74
 
75
75
  return list(combined.values())
76
76
 
77
+ def retrieve_from_cube(
78
+ self,
79
+ top_k: int,
80
+ memory_scope: str,
81
+ query_embedding: list[list[float]] | None = None,
82
+ cube_name: str = "memos_cube01",
83
+ ) -> list[TextualMemoryItem]:
84
+ """
85
+ Perform hybrid memory retrieval:
86
+ - Run graph-based lookup from dispatch plan.
87
+ - Run vector similarity search from embedded query.
88
+ - Merge and return combined result set.
89
+
90
+ Args:
91
+ top_k (int): Number of candidates to return.
92
+ memory_scope (str): One of ['working', 'long_term', 'user'].
93
+ query_embedding(list of embedding): list of embedding of query
94
+ cube_name: specify cube_name
95
+
96
+ Returns:
97
+ list: Combined memory items.
98
+ """
99
+ if memory_scope not in ["WorkingMemory", "LongTermMemory", "UserMemory"]:
100
+ raise ValueError(f"Unsupported memory scope: {memory_scope}")
101
+
102
+ graph_results = self._vector_recall(
103
+ query_embedding, memory_scope, top_k, cube_name=cube_name
104
+ )
105
+
106
+ for result_i in graph_results:
107
+ result_i.metadata.memory_type = "OuterMemory"
108
+ # Merge and deduplicate by ID
109
+ combined = {item.id: item for item in graph_results}
110
+
111
+ graph_ids = {item.id for item in graph_results}
112
+ combined_ids = set(combined.keys())
113
+ lost_ids = graph_ids - combined_ids
114
+
115
+ if lost_ids:
116
+ print(
117
+ f"[DEBUG] The following nodes were in graph_results but missing in combined: {lost_ids}"
118
+ )
119
+
120
+ return list(combined.values())
121
+
77
122
  def _graph_recall(
78
123
  self, parsed_goal: ParsedTaskGoal, memory_scope: str
79
124
  ) -> list[TextualMemoryItem]:
@@ -134,7 +179,8 @@ class GraphMemoryRetriever:
134
179
  query_embedding: list[list[float]],
135
180
  memory_scope: str,
136
181
  top_k: int = 20,
137
- max_num: int = 5,
182
+ max_num: int = 3,
183
+ cube_name: str | None = None,
138
184
  ) -> list[TextualMemoryItem]:
139
185
  """
140
186
  # TODO: tackle with post-filter and pre-filter(5.18+) better.
@@ -144,7 +190,9 @@ class GraphMemoryRetriever:
144
190
 
145
191
  def search_single(vec):
146
192
  return (
147
- self.graph_store.search_by_embedding(vector=vec, top_k=top_k, scope=memory_scope)
193
+ self.graph_store.search_by_embedding(
194
+ vector=vec, top_k=top_k, scope=memory_scope, cube_name=cube_name
195
+ )
148
196
  or []
149
197
  )
150
198
 
@@ -159,6 +207,8 @@ class GraphMemoryRetriever:
159
207
 
160
208
  # Step 3: Extract matched IDs and retrieve full nodes
161
209
  unique_ids = set({r["id"] for r in all_matches})
162
- node_dicts = self.graph_store.get_nodes(list(unique_ids), include_embedding=True)
210
+ node_dicts = self.graph_store.get_nodes(
211
+ list(unique_ids), include_embedding=True, cube_name=cube_name
212
+ )
163
213
 
164
214
  return [TextualMemoryItem.from_dict(record) for record in node_dicts]
@@ -8,12 +8,12 @@ from memos.graph_dbs.factory import Neo4jGraphDB
8
8
  from memos.llms.factory import AzureLLM, OllamaLLM, OpenAILLM
9
9
  from memos.log import get_logger
10
10
  from memos.memories.textual.item import SearchedTreeNodeTextualMemoryMetadata, TextualMemoryItem
11
+ from memos.reranker.base import BaseReranker
11
12
  from memos.utils import timed
12
13
 
13
14
  from .internet_retriever_factory import InternetRetrieverFactory
14
15
  from .reasoner import MemoryReasoner
15
16
  from .recall import GraphMemoryRetriever
16
- from .reranker import MemoryReranker
17
17
  from .task_goal_parser import TaskGoalParser
18
18
 
19
19
 
@@ -26,18 +26,25 @@ class Searcher:
26
26
  dispatcher_llm: OpenAILLM | OllamaLLM | AzureLLM,
27
27
  graph_store: Neo4jGraphDB,
28
28
  embedder: OllamaEmbedder,
29
+ reranker: BaseReranker,
29
30
  internet_retriever: InternetRetrieverFactory | None = None,
31
+ moscube: bool = False,
30
32
  ):
31
33
  self.graph_store = graph_store
32
34
  self.embedder = embedder
33
35
 
34
36
  self.task_goal_parser = TaskGoalParser(dispatcher_llm)
35
37
  self.graph_retriever = GraphMemoryRetriever(self.graph_store, self.embedder)
36
- self.reranker = MemoryReranker(dispatcher_llm, self.embedder)
38
+ self.reranker = reranker
37
39
  self.reasoner = MemoryReasoner(dispatcher_llm)
38
40
 
39
41
  # Create internet retriever from config if provided
40
42
  self.internet_retriever = internet_retriever
43
+ self.moscube = moscube
44
+
45
+ self._usage_executor = concurrent.futures.ThreadPoolExecutor(
46
+ max_workers=4, thread_name_prefix="usage"
47
+ )
41
48
 
42
49
  @timed
43
50
  def search(
@@ -80,6 +87,12 @@ class Searcher:
80
87
  self._update_usage_history(final_results, info)
81
88
 
82
89
  logger.info(f"[SEARCH] Done. Total {len(final_results)} results.")
90
+ res_results = ""
91
+ for _num_i, result in enumerate(final_results):
92
+ res_results += "\n" + (
93
+ result.id + "|" + result.metadata.memory_type + "|" + result.memory
94
+ )
95
+ logger.info(f"[SEARCH] Results. {res_results}")
83
96
  return final_results
84
97
 
85
98
  @timed
@@ -101,9 +114,10 @@ class Searcher:
101
114
  context = list({node["memory"] for node in related_nodes})
102
115
 
103
116
  # optional: supplement context with internet knowledge
104
- if self.internet_retriever:
117
+ """if self.internet_retriever:
105
118
  extra = self.internet_retriever.retrieve_from_internet(query=query, top_k=3)
106
119
  context.extend(item.memory.partition("\nContent: ")[-1] for item in extra)
120
+ """
107
121
 
108
122
  # parse goal using LLM
109
123
  parsed_goal = self.task_goal_parser.parse(
@@ -157,6 +171,17 @@ class Searcher:
157
171
  memory_type,
158
172
  )
159
173
  )
174
+ if self.moscube:
175
+ tasks.append(
176
+ executor.submit(
177
+ self._retrieve_from_memcubes,
178
+ query,
179
+ parsed_goal,
180
+ query_embedding,
181
+ top_k,
182
+ "memos_cube01",
183
+ )
184
+ )
160
185
 
161
186
  results = []
162
187
  for t in tasks:
@@ -212,7 +237,26 @@ class Searcher:
212
237
  query=query,
213
238
  query_embedding=query_embedding[0],
214
239
  graph_results=results,
240
+ top_k=top_k,
241
+ parsed_goal=parsed_goal,
242
+ )
243
+
244
+ @timed
245
+ def _retrieve_from_memcubes(
246
+ self, query, parsed_goal, query_embedding, top_k, cube_name="memos_cube01"
247
+ ):
248
+ """Retrieve and rerank from LongTermMemory and UserMemory"""
249
+ results = self.graph_retriever.retrieve_from_cube(
250
+ query_embedding=query_embedding,
215
251
  top_k=top_k * 2,
252
+ memory_scope="LongTermMemory",
253
+ cube_name=cube_name,
254
+ )
255
+ return self.reranker.rerank(
256
+ query=query,
257
+ query_embedding=query_embedding[0],
258
+ graph_results=results,
259
+ top_k=top_k,
216
260
  parsed_goal=parsed_goal,
217
261
  )
218
262
 
@@ -271,14 +315,30 @@ class Searcher:
271
315
  def _update_usage_history(self, items, info):
272
316
  """Update usage history in graph DB"""
273
317
  now_time = datetime.now().isoformat()
274
- info.pop("chat_history", None)
275
- # `info` should be a serializable dict or string
276
- usage_record = json.dumps({"time": now_time, "info": info})
277
- for item in items:
278
- if (
279
- hasattr(item, "id")
280
- and hasattr(item, "metadata")
281
- and hasattr(item.metadata, "usage")
282
- ):
283
- item.metadata.usage.append(usage_record)
284
- self.graph_store.update_node(item.id, {"usage": item.metadata.usage})
318
+ info_copy = dict(info or {})
319
+ info_copy.pop("chat_history", None)
320
+ usage_record = json.dumps({"time": now_time, "info": info_copy})
321
+ payload = []
322
+ for it in items:
323
+ try:
324
+ item_id = getattr(it, "id", None)
325
+ md = getattr(it, "metadata", None)
326
+ if md is None:
327
+ continue
328
+ if not hasattr(md, "usage") or md.usage is None:
329
+ md.usage = []
330
+ md.usage.append(usage_record)
331
+ if item_id:
332
+ payload.append((item_id, list(md.usage)))
333
+ except Exception:
334
+ logger.exception("[USAGE] snapshot item failed")
335
+
336
+ if payload:
337
+ self._usage_executor.submit(self._update_usage_history_worker, payload, usage_record)
338
+
339
+ def _update_usage_history_worker(self, payload, usage_record: str):
340
+ try:
341
+ for item_id, usage_list in payload:
342
+ self.graph_store.update_node(item_id, {"usage": usage_list})
343
+ except Exception:
344
+ 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": [...],
@@ -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,4 @@
1
+ from .factory import RerankerFactory
2
+
3
+
4
+ __all__ = ["RerankerFactory"]