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.
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/METADATA +2 -1
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/RECORD +42 -33
- memos/__init__.py +1 -1
- memos/api/config.py +25 -0
- memos/api/context/context_thread.py +96 -0
- memos/api/context/dependencies.py +0 -11
- memos/api/middleware/request_context.py +94 -0
- memos/api/product_api.py +5 -1
- memos/api/product_models.py +16 -0
- memos/api/routers/product_router.py +39 -3
- memos/api/start_api.py +3 -0
- memos/configs/memory.py +13 -0
- memos/configs/reranker.py +18 -0
- memos/graph_dbs/base.py +4 -2
- memos/graph_dbs/nebular.py +215 -68
- memos/graph_dbs/neo4j.py +14 -12
- memos/graph_dbs/neo4j_community.py +6 -3
- memos/llms/vllm.py +2 -0
- memos/log.py +120 -8
- memos/mem_os/core.py +30 -2
- memos/mem_os/product.py +386 -146
- memos/mem_os/utils/reference_utils.py +20 -0
- memos/mem_reader/simple_struct.py +112 -43
- memos/mem_user/mysql_user_manager.py +4 -2
- memos/memories/textual/item.py +1 -1
- memos/memories/textual/tree.py +31 -1
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +3 -1
- memos/memories/textual/tree_text_memory/retrieve/recall.py +53 -3
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +74 -14
- memos/memories/textual/tree_text_memory/retrieve/utils.py +6 -4
- memos/memos_tools/notification_utils.py +46 -0
- memos/reranker/__init__.py +4 -0
- memos/reranker/base.py +24 -0
- memos/reranker/cosine_local.py +95 -0
- memos/reranker/factory.py +43 -0
- memos/reranker/http_bge.py +99 -0
- memos/reranker/noop.py +16 -0
- memos/templates/mem_reader_prompts.py +289 -40
- memos/templates/mos_prompts.py +133 -60
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/LICENSE +0 -0
- {memoryos-1.0.0.dist-info → memoryos-1.0.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
44
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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(
|
|
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
|
|
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):
|
memos/memories/textual/item.py
CHANGED
|
@@ -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(
|
memos/memories/textual/tree.py
CHANGED
|
@@ -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,
|
|
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:
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
275
|
-
|
|
276
|
-
usage_record = json.dumps({"time": now_time, "info":
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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,
|