MemoryOS 0.2.1__py3-none-any.whl → 1.0.0__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.2.1.dist-info → memoryos-1.0.0.dist-info}/METADATA +7 -1
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/RECORD +87 -64
- memos/__init__.py +1 -1
- memos/api/config.py +158 -69
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +101 -0
- memos/api/product_models.py +5 -1
- memos/api/routers/product_router.py +54 -26
- memos/configs/graph_db.py +49 -1
- memos/configs/internet_retriever.py +19 -0
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +54 -18
- memos/configs/mem_user.py +58 -0
- memos/graph_dbs/base.py +38 -3
- memos/graph_dbs/factory.py +2 -0
- memos/graph_dbs/nebular.py +1612 -0
- memos/graph_dbs/neo4j.py +18 -9
- memos/log.py +6 -1
- memos/mem_cube/utils.py +13 -6
- memos/mem_os/core.py +157 -37
- memos/mem_os/main.py +2 -2
- memos/mem_os/product.py +252 -201
- memos/mem_os/utils/default_config.py +1 -1
- memos/mem_os/utils/format_utils.py +281 -70
- memos/mem_os/utils/reference_utils.py +133 -0
- memos/mem_reader/simple_struct.py +13 -5
- memos/mem_scheduler/base_scheduler.py +239 -266
- memos/mem_scheduler/{modules → general_modules}/base.py +4 -5
- memos/mem_scheduler/{modules → general_modules}/dispatcher.py +57 -21
- memos/mem_scheduler/general_modules/misc.py +104 -0
- memos/mem_scheduler/{modules → general_modules}/rabbitmq_service.py +12 -10
- memos/mem_scheduler/{modules → general_modules}/redis_service.py +1 -1
- memos/mem_scheduler/general_modules/retriever.py +199 -0
- memos/mem_scheduler/general_modules/scheduler_logger.py +261 -0
- memos/mem_scheduler/general_scheduler.py +243 -80
- memos/mem_scheduler/monitors/__init__.py +0 -0
- memos/mem_scheduler/monitors/dispatcher_monitor.py +305 -0
- memos/mem_scheduler/{modules/monitor.py → monitors/general_monitor.py} +106 -57
- memos/mem_scheduler/mos_for_test_scheduler.py +23 -20
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +44 -0
- memos/mem_scheduler/schemas/message_schemas.py +149 -0
- memos/mem_scheduler/schemas/monitor_schemas.py +337 -0
- memos/mem_scheduler/utils/__init__.py +0 -0
- memos/mem_scheduler/utils/filter_utils.py +176 -0
- memos/mem_scheduler/utils/misc_utils.py +102 -0
- memos/mem_user/factory.py +94 -0
- memos/mem_user/mysql_persistent_user_manager.py +271 -0
- memos/mem_user/mysql_user_manager.py +500 -0
- memos/mem_user/persistent_factory.py +96 -0
- memos/mem_user/user_manager.py +4 -4
- memos/memories/activation/item.py +5 -1
- memos/memories/activation/kv.py +20 -8
- memos/memories/textual/base.py +2 -2
- memos/memories/textual/general.py +36 -92
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +13 -7
- memos/memories/textual/tree_text_memory/organize/{conflict.py → handler.py} +34 -50
- memos/memories/textual/tree_text_memory/organize/manager.py +8 -96
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +49 -43
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +107 -142
- memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +229 -0
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +11 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +15 -8
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +1 -1
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +191 -116
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +47 -15
- memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
- memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
- memos/memos_tools/dinding_report_bot.py +422 -0
- memos/memos_tools/lockfree_dict.py +120 -0
- memos/memos_tools/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +96 -0
- memos/memos_tools/thread_safe_dict.py +288 -0
- memos/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +4 -1
- memos/templates/mem_scheduler_prompts.py +62 -15
- memos/templates/mos_prompts.py +116 -0
- memos/templates/tree_reorganize_prompts.py +24 -17
- memos/utils.py +19 -0
- memos/mem_scheduler/modules/misc.py +0 -39
- memos/mem_scheduler/modules/retriever.py +0 -268
- memos/mem_scheduler/modules/schemas.py +0 -328
- memos/mem_scheduler/utils.py +0 -75
- memos/memories/textual/tree_text_memory/organize/redundancy.py +0 -193
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/LICENSE +0 -0
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/WHEEL +0 -0
- {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/entry_points.txt +0 -0
- /memos/mem_scheduler/{modules → general_modules}/__init__.py +0 -0
|
@@ -12,6 +12,7 @@ from memos.llms.factory import AzureLLM, LLMFactory, OllamaLLM, OpenAILLM
|
|
|
12
12
|
from memos.log import get_logger
|
|
13
13
|
from memos.memories.textual.base import BaseTextMemory
|
|
14
14
|
from memos.memories.textual.item import TextualMemoryItem
|
|
15
|
+
from memos.templates.mem_reader_prompts import SIMPLE_STRUCT_MEM_READER_PROMPT
|
|
15
16
|
from memos.types import MessageList
|
|
16
17
|
from memos.vec_dbs.factory import QdrantVecDB, VecDBFactory
|
|
17
18
|
from memos.vec_dbs.item import VecDBItem
|
|
@@ -36,11 +37,7 @@ class GeneralTextMemory(BaseTextMemory):
|
|
|
36
37
|
stop=stop_after_attempt(3),
|
|
37
38
|
retry=retry_if_exception_type(json.JSONDecodeError),
|
|
38
39
|
before_sleep=lambda retry_state: logger.warning(
|
|
39
|
-
|
|
40
|
-
error=retry_state.outcome.exception(),
|
|
41
|
-
attempt_number=retry_state.attempt_number,
|
|
42
|
-
max_attempt_number=3,
|
|
43
|
-
)
|
|
40
|
+
f"Extracting memory failed due to JSON decode error: {retry_state.outcome.exception()}, Attempt retry: {retry_state.attempt_number} / {3}"
|
|
44
41
|
),
|
|
45
42
|
)
|
|
46
43
|
def extract(self, messages: MessageList) -> list[TextualMemoryItem]:
|
|
@@ -52,14 +49,27 @@ class GeneralTextMemory(BaseTextMemory):
|
|
|
52
49
|
Returns:
|
|
53
50
|
List of TextualMemoryItem objects representing the extracted memories.
|
|
54
51
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
messages
|
|
52
|
+
|
|
53
|
+
str_messages = "\n".join(
|
|
54
|
+
[message["role"] + ":" + message["content"] for message in messages]
|
|
58
55
|
)
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
|
|
57
|
+
prompt = SIMPLE_STRUCT_MEM_READER_PROMPT.replace("${conversation}", str_messages)
|
|
58
|
+
messages = [{"role": "user", "content": prompt}]
|
|
59
|
+
response_text = self.extractor_llm.generate(messages)
|
|
60
|
+
response_json = self.parse_json_result(response_text)
|
|
61
|
+
|
|
61
62
|
extracted_memories = [
|
|
62
|
-
TextualMemoryItem(
|
|
63
|
+
TextualMemoryItem(
|
|
64
|
+
memory=memory_dict["value"],
|
|
65
|
+
metadata={
|
|
66
|
+
"key": memory_dict["key"],
|
|
67
|
+
"source": "conversation",
|
|
68
|
+
"tags": memory_dict["tags"],
|
|
69
|
+
"updated_at": datetime.now().isoformat(),
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
for memory_dict in response_json["memory list"]
|
|
63
73
|
]
|
|
64
74
|
|
|
65
75
|
return extracted_memories
|
|
@@ -104,7 +114,7 @@ class GeneralTextMemory(BaseTextMemory):
|
|
|
104
114
|
|
|
105
115
|
self.vector_db.update(memory_id, vec_db_item)
|
|
106
116
|
|
|
107
|
-
def search(self, query: str, top_k: int) -> list[TextualMemoryItem]:
|
|
117
|
+
def search(self, query: str, top_k: int, info=None, **kwargs) -> list[TextualMemoryItem]:
|
|
108
118
|
"""Search for memories based on a query.
|
|
109
119
|
Args:
|
|
110
120
|
query (str): The query to search for.
|
|
@@ -206,83 +216,17 @@ class GeneralTextMemory(BaseTextMemory):
|
|
|
206
216
|
"""Embed a single sentence."""
|
|
207
217
|
return self.embedder.embed([sentence])[0]
|
|
208
218
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
- "visibility": The accessibility scope of the memory (string), e.g., `"private"`, `"public"`, `"session"`, determining who or what contexts can access it.
|
|
224
|
-
- "updated_at": The timestamp of the last modification to the memory (string). Useful for tracking memory freshness or change history. Format: ISO 8601 or natural language.
|
|
225
|
-
* Current date and time is {datetime.now().isoformat()}.
|
|
226
|
-
* Only return the list of memories in JSON format.
|
|
227
|
-
* Do not include any explanations
|
|
228
|
-
* Do not include any extra text
|
|
229
|
-
* Do not include code blocks (```json```)
|
|
230
|
-
|
|
231
|
-
## Example
|
|
232
|
-
|
|
233
|
-
### Input
|
|
234
|
-
|
|
235
|
-
[
|
|
236
|
-
{{"role": "user", "content": "I plan to visit Paris next week."}},
|
|
237
|
-
{{"role": "assistant", "content": "Paris is a beautiful city with many attractions."}},
|
|
238
|
-
{{"role": "user", "content": "I love the Eiffel Tower."}},
|
|
239
|
-
{{"role": "assistant", "content": "The Eiffel Tower is a must-see landmark in Paris."}}
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
### Output
|
|
243
|
-
|
|
244
|
-
[
|
|
245
|
-
{{
|
|
246
|
-
"memory": "The user plans to visit Paris on 05-26-2025.",
|
|
247
|
-
"metadata": {{
|
|
248
|
-
"type": "event",
|
|
249
|
-
"memory_time": "2025-05-26",
|
|
250
|
-
"source": "conversation",
|
|
251
|
-
"confidence": 90.0,
|
|
252
|
-
"entities": ["Paris"],
|
|
253
|
-
"tags": ["travel", "plans"],
|
|
254
|
-
"visibility": "private",
|
|
255
|
-
"updated_at": "2025-05-19T00:00:00"
|
|
256
|
-
}}
|
|
257
|
-
}},
|
|
258
|
-
{{
|
|
259
|
-
"memory": "The user loves the Eiffel Tower.",
|
|
260
|
-
"metadata": {{
|
|
261
|
-
"type": "opinion",
|
|
262
|
-
"memory_time": "2025-05-19",
|
|
263
|
-
"source": "conversation",
|
|
264
|
-
"confidence": 100.0,
|
|
265
|
-
"entities": ["Eiffel Tower"],
|
|
266
|
-
"tags": ["opinions", "landmarks"],
|
|
267
|
-
"visibility": "session",
|
|
268
|
-
"updated_at": "2025-05-19T00:00:00"
|
|
269
|
-
}}
|
|
270
|
-
}}
|
|
271
|
-
]
|
|
272
|
-
|
|
273
|
-
"""
|
|
274
|
-
|
|
275
|
-
EXTRACTION_PROMPT_PART_2 = """
|
|
276
|
-
## Query
|
|
277
|
-
|
|
278
|
-
### Input
|
|
279
|
-
|
|
280
|
-
{messages}
|
|
281
|
-
|
|
282
|
-
### Output
|
|
283
|
-
|
|
284
|
-
"""
|
|
285
|
-
|
|
286
|
-
EXTRACTION_RETRY_LOG = """Extracting memory failed due to JSON decode error: {error},
|
|
287
|
-
Attempt retry: {attempt_number} / {max_attempt_number}
|
|
288
|
-
"""
|
|
219
|
+
def parse_json_result(self, response_text):
|
|
220
|
+
try:
|
|
221
|
+
json_start = response_text.find("{")
|
|
222
|
+
response_text = response_text[json_start:]
|
|
223
|
+
response_text = response_text.replace("```", "").strip()
|
|
224
|
+
if response_text[-1] != "}":
|
|
225
|
+
response_text += "}"
|
|
226
|
+
response_json = json.loads(response_text)
|
|
227
|
+
return response_json
|
|
228
|
+
except json.JSONDecodeError as e:
|
|
229
|
+
logger.warning(
|
|
230
|
+
f"Failed to parse LLM response as JSON: {e}\nRaw response:\n{response_text}"
|
|
231
|
+
)
|
|
232
|
+
return {}
|
memos/memories/textual/item.py
CHANGED
|
@@ -27,23 +27,14 @@ class TextualMemoryMetadata(BaseModel):
|
|
|
27
27
|
default="activated",
|
|
28
28
|
description="The status of the memory, e.g., 'activated', 'archived', 'deleted'.",
|
|
29
29
|
)
|
|
30
|
-
type:
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
memory_time: str | None = Field(
|
|
34
|
-
default=None,
|
|
35
|
-
description='The time the memory occurred or refers to. Must be in standard `YYYY-MM-DD` format. Relative expressions such as "yesterday" or "tomorrow" are not allowed.',
|
|
36
|
-
)
|
|
37
|
-
source: Literal["conversation", "retrieved", "web", "file"] | None = Field(
|
|
38
|
-
default=None, description="The origin of the memory"
|
|
39
|
-
)
|
|
30
|
+
type: str | None = Field(default=None)
|
|
31
|
+
key: str | None = Field(default=None, description="Memory key or title.")
|
|
40
32
|
confidence: float | None = Field(
|
|
41
33
|
default=None,
|
|
42
34
|
description="A numeric score (float between 0 and 100) indicating how certain you are about the accuracy or reliability of the memory.",
|
|
43
35
|
)
|
|
44
|
-
|
|
45
|
-
default=None,
|
|
46
|
-
description='A list of key entities mentioned in the memory, e.g., people, places, organizations, e.g., `["Alice", "Paris", "OpenAI"]`.',
|
|
36
|
+
source: Literal["conversation", "retrieved", "web", "file"] | None = Field(
|
|
37
|
+
default=None, description="The origin of the memory"
|
|
47
38
|
)
|
|
48
39
|
tags: list[str] | None = Field(
|
|
49
40
|
default=None,
|
|
@@ -59,23 +50,6 @@ class TextualMemoryMetadata(BaseModel):
|
|
|
59
50
|
|
|
60
51
|
model_config = ConfigDict(extra="allow")
|
|
61
52
|
|
|
62
|
-
@field_validator("memory_time")
|
|
63
|
-
@classmethod
|
|
64
|
-
def validate_memory_time(cls, v):
|
|
65
|
-
try:
|
|
66
|
-
if v:
|
|
67
|
-
datetime.strptime(v, "%Y-%m-%d")
|
|
68
|
-
except ValueError as e:
|
|
69
|
-
raise ValueError("Invalid date format. Use YYYY-MM-DD.") from e
|
|
70
|
-
return v
|
|
71
|
-
|
|
72
|
-
@field_validator("confidence")
|
|
73
|
-
@classmethod
|
|
74
|
-
def validate_confidence(cls, v):
|
|
75
|
-
if v is not None and (v < 0 or v > 100):
|
|
76
|
-
raise ValueError("Confidence must be between 0 and 100.")
|
|
77
|
-
return v
|
|
78
|
-
|
|
79
53
|
def __str__(self) -> str:
|
|
80
54
|
"""Pretty string representation of the metadata."""
|
|
81
55
|
meta = self.model_dump(exclude_none=True)
|
|
@@ -85,10 +59,9 @@ class TextualMemoryMetadata(BaseModel):
|
|
|
85
59
|
class TreeNodeTextualMemoryMetadata(TextualMemoryMetadata):
|
|
86
60
|
"""Extended metadata for structured memory, layered retrieval, and lifecycle tracking."""
|
|
87
61
|
|
|
88
|
-
memory_type: Literal["WorkingMemory", "LongTermMemory", "UserMemory"] = Field(
|
|
62
|
+
memory_type: Literal["WorkingMemory", "LongTermMemory", "UserMemory", "OuterMemory"] = Field(
|
|
89
63
|
default="WorkingMemory", description="Memory lifecycle type."
|
|
90
64
|
)
|
|
91
|
-
key: str | None = Field(default=None, description="Memory key or title.")
|
|
92
65
|
sources: list[str] | None = Field(
|
|
93
66
|
default=None, description="Multiple origins of the memory (e.g., URLs, notes)."
|
|
94
67
|
)
|
|
@@ -148,7 +121,6 @@ class TextualMemoryItem(BaseModel):
|
|
|
148
121
|
|
|
149
122
|
model_config = ConfigDict(extra="forbid")
|
|
150
123
|
|
|
151
|
-
@field_validator("id")
|
|
152
124
|
@classmethod
|
|
153
125
|
def validate_id(cls, v):
|
|
154
126
|
try:
|
memos/memories/textual/tree.py
CHANGED
|
@@ -117,13 +117,19 @@ class TreeTextMemory(BaseTextMemory):
|
|
|
117
117
|
logger.warning(
|
|
118
118
|
"Internet retriever is init by config , but this search set manual_close_internet is True and will close it"
|
|
119
119
|
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
searcher = Searcher(
|
|
121
|
+
self.dispatcher_llm,
|
|
122
|
+
self.graph_store,
|
|
123
|
+
self.embedder,
|
|
124
|
+
internet_retriever=None,
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
searcher = Searcher(
|
|
128
|
+
self.dispatcher_llm,
|
|
129
|
+
self.graph_store,
|
|
130
|
+
self.embedder,
|
|
131
|
+
internet_retriever=self.internet_retriever,
|
|
132
|
+
)
|
|
127
133
|
return searcher.search(query, top_k, info, mode, memory_type)
|
|
128
134
|
|
|
129
135
|
def get_relevant_subgraph(
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import re
|
|
3
|
-
|
|
4
3
|
from datetime import datetime
|
|
5
4
|
|
|
5
|
+
from dateutil import parser
|
|
6
|
+
|
|
6
7
|
from memos.embedders.base import BaseEmbedder
|
|
7
8
|
from memos.graph_dbs.neo4j import Neo4jGraphDB
|
|
8
9
|
from memos.llms.base import BaseLLM
|
|
9
10
|
from memos.log import get_logger
|
|
10
11
|
from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata
|
|
11
12
|
from memos.templates.tree_reorganize_prompts import (
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
MEMORY_RELATION_DETECTOR_PROMPT,
|
|
14
|
+
MEMORY_RELATION_RESOLVER_PROMPT,
|
|
14
15
|
)
|
|
15
16
|
|
|
16
|
-
|
|
17
17
|
logger = get_logger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class
|
|
20
|
+
class NodeHandler:
|
|
21
21
|
EMBEDDING_THRESHOLD: float = 0.8 # Threshold for embedding similarity to consider conflict
|
|
22
22
|
|
|
23
23
|
def __init__(self, graph_store: Neo4jGraphDB, llm: BaseLLM, embedder: BaseEmbedder):
|
|
@@ -25,66 +25,53 @@ class ConflictHandler:
|
|
|
25
25
|
self.llm = llm
|
|
26
26
|
self.embedder = embedder
|
|
27
27
|
|
|
28
|
-
def detect(
|
|
29
|
-
self, memory: TextualMemoryItem, top_k: int = 5, scope: str | None = None
|
|
30
|
-
) -> list[tuple[TextualMemoryItem, TextualMemoryItem]]:
|
|
31
|
-
"""
|
|
32
|
-
Detect conflicts by finding the most similar items in the graph database based on embedding, then use LLM to judge conflict.
|
|
33
|
-
Args:
|
|
34
|
-
memory: The memory item (should have an embedding attribute or field).
|
|
35
|
-
top_k: Number of top similar nodes to retrieve.
|
|
36
|
-
scope: Optional memory type filter.
|
|
37
|
-
Returns:
|
|
38
|
-
List of conflict pairs (each pair is a tuple: (memory, candidate)).
|
|
39
|
-
"""
|
|
28
|
+
def detect(self, memory, top_k: int = 5, scope=None):
|
|
40
29
|
# 1. Search for similar memories based on embedding
|
|
41
30
|
embedding = memory.metadata.embedding
|
|
42
31
|
embedding_candidates_info = self.graph_store.search_by_embedding(
|
|
43
|
-
embedding, top_k=top_k, scope=scope
|
|
32
|
+
embedding, top_k=top_k, scope=scope, threshold=self.EMBEDDING_THRESHOLD
|
|
44
33
|
)
|
|
45
34
|
# 2. Filter based on similarity threshold
|
|
46
35
|
embedding_candidates_ids = [
|
|
47
|
-
info["id"]
|
|
48
|
-
for info in embedding_candidates_info
|
|
49
|
-
if info["score"] >= self.EMBEDDING_THRESHOLD and info["id"] != memory.id
|
|
36
|
+
info["id"] for info in embedding_candidates_info if info["id"] != memory.id
|
|
50
37
|
]
|
|
51
38
|
# 3. Judge conflicts using LLM
|
|
52
39
|
embedding_candidates = self.graph_store.get_nodes(embedding_candidates_ids)
|
|
53
|
-
|
|
40
|
+
detected_relationships = []
|
|
54
41
|
for embedding_candidate in embedding_candidates:
|
|
55
42
|
embedding_candidate = TextualMemoryItem.from_dict(embedding_candidate)
|
|
56
43
|
prompt = [
|
|
57
|
-
{
|
|
58
|
-
"role": "system",
|
|
59
|
-
"content": "You are a conflict detector for memory items.",
|
|
60
|
-
},
|
|
61
44
|
{
|
|
62
45
|
"role": "user",
|
|
63
|
-
"content":
|
|
64
|
-
statement_1=memory.memory,
|
|
65
|
-
statement_2=embedding_candidate.memory,
|
|
46
|
+
"content": MEMORY_RELATION_DETECTOR_PROMPT.format(
|
|
47
|
+
statement_1=memory.memory, statement_2=embedding_candidate.memory
|
|
66
48
|
),
|
|
67
|
-
}
|
|
49
|
+
}
|
|
68
50
|
]
|
|
69
51
|
result = self.llm.generate(prompt).strip()
|
|
70
|
-
if "
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
52
|
+
if result == "contradictory":
|
|
53
|
+
logger.warning(
|
|
54
|
+
f'detected "{memory.memory}" <==CONFLICT==> "{embedding_candidate.memory}"'
|
|
55
|
+
)
|
|
56
|
+
detected_relationships.append([memory, embedding_candidate, "contradictory"])
|
|
57
|
+
elif result == "redundant":
|
|
58
|
+
logger.warning(
|
|
59
|
+
f'detected "{memory.memory}" <==REDUNDANT==> "{embedding_candidate.memory}"'
|
|
60
|
+
)
|
|
61
|
+
detected_relationships.append([memory, embedding_candidate, "redundant"])
|
|
62
|
+
elif result == "independent":
|
|
63
|
+
pass
|
|
64
|
+
else:
|
|
65
|
+
pass
|
|
66
|
+
return detected_relationships
|
|
81
67
|
|
|
82
|
-
def resolve(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem) -> None:
|
|
68
|
+
def resolve(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem, relation) -> None:
|
|
83
69
|
"""
|
|
84
70
|
Resolve detected conflicts between two memory items using LLM fusion.
|
|
85
71
|
Args:
|
|
86
72
|
memory_a: The first conflicting memory item.
|
|
87
73
|
memory_b: The second conflicting memory item.
|
|
74
|
+
relation: relation
|
|
88
75
|
Returns:
|
|
89
76
|
A fused TextualMemoryItem representing the resolved memory.
|
|
90
77
|
"""
|
|
@@ -94,13 +81,10 @@ class ConflictHandler:
|
|
|
94
81
|
metadata_1 = memory_a.metadata.model_dump_json(include=metadata_for_resolve)
|
|
95
82
|
metadata_2 = memory_b.metadata.model_dump_json(include=metadata_for_resolve)
|
|
96
83
|
prompt = [
|
|
97
|
-
{
|
|
98
|
-
"role": "system",
|
|
99
|
-
"content": "",
|
|
100
|
-
},
|
|
101
84
|
{
|
|
102
85
|
"role": "user",
|
|
103
|
-
"content":
|
|
86
|
+
"content": MEMORY_RELATION_RESOLVER_PROMPT.format(
|
|
87
|
+
relation=relation,
|
|
104
88
|
statement_1=memory_a.memory,
|
|
105
89
|
metadata_1=metadata_1,
|
|
106
90
|
statement_2=memory_b.memory,
|
|
@@ -117,7 +101,7 @@ class ConflictHandler:
|
|
|
117
101
|
# —————— 2.1 Can't resolve conflict, hard update by comparing timestamp ————
|
|
118
102
|
if len(answer) <= 10 and "no" in answer.lower():
|
|
119
103
|
logger.warning(
|
|
120
|
-
f"
|
|
104
|
+
f"{relation} between {memory_a.id} and {memory_b.id} could not be resolved. "
|
|
121
105
|
)
|
|
122
106
|
self._hard_update(memory_a, memory_b)
|
|
123
107
|
# —————— 2.2 Conflict resolved, update metadata and memory ————
|
|
@@ -133,8 +117,8 @@ class ConflictHandler:
|
|
|
133
117
|
"""
|
|
134
118
|
Hard update: compare updated_at, keep the newer one, overwrite the older one's metadata.
|
|
135
119
|
"""
|
|
136
|
-
time_a =
|
|
137
|
-
time_b =
|
|
120
|
+
time_a = parser.isoparse(memory_a.metadata.updated_at)
|
|
121
|
+
time_b = parser.isoparse(memory_b.metadata.updated_at)
|
|
138
122
|
|
|
139
123
|
newer_mem = memory_a if time_a >= time_b else memory_b
|
|
140
124
|
older_mem = memory_b if time_a >= time_b else memory_a
|
|
@@ -39,8 +39,8 @@ class MemoryManager:
|
|
|
39
39
|
if not memory_size:
|
|
40
40
|
self.memory_size = {
|
|
41
41
|
"WorkingMemory": 20,
|
|
42
|
-
"LongTermMemory":
|
|
43
|
-
"UserMemory":
|
|
42
|
+
"LongTermMemory": 1500,
|
|
43
|
+
"UserMemory": 480,
|
|
44
44
|
}
|
|
45
45
|
self._threshold = threshold
|
|
46
46
|
self.is_reorganize = is_reorganize
|
|
@@ -158,106 +158,18 @@ class MemoryManager:
|
|
|
158
158
|
- topic_summary_prefix: summary node id prefix if applicable
|
|
159
159
|
- enable_summary_link: whether to auto-link to a summary node
|
|
160
160
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
# Step 1: Find similar nodes for possible merging
|
|
164
|
-
similar_nodes = self.graph_store.search_by_embedding(
|
|
165
|
-
vector=embedding,
|
|
166
|
-
top_k=3,
|
|
167
|
-
scope=memory_type,
|
|
168
|
-
threshold=self._threshold,
|
|
169
|
-
status="activated",
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
if similar_nodes and similar_nodes[0]["score"] > self._merged_threshold:
|
|
173
|
-
return self._merge(memory, similar_nodes)
|
|
174
|
-
else:
|
|
175
|
-
node_id = str(uuid.uuid4())
|
|
176
|
-
# Step 2: Add new node to graph
|
|
177
|
-
self.graph_store.add_node(
|
|
178
|
-
node_id, memory.memory, memory.metadata.model_dump(exclude_none=True)
|
|
179
|
-
)
|
|
180
|
-
self.reorganizer.add_message(
|
|
181
|
-
QueueMessage(
|
|
182
|
-
op="add",
|
|
183
|
-
after_node=[node_id],
|
|
184
|
-
)
|
|
185
|
-
)
|
|
186
|
-
return node_id
|
|
187
|
-
|
|
188
|
-
def _merge(self, source_node: TextualMemoryItem, similar_nodes: list[dict]) -> str:
|
|
189
|
-
"""
|
|
190
|
-
TODO: Add node traceability support by optionally preserving source nodes and linking them with MERGED_FROM edges.
|
|
191
|
-
|
|
192
|
-
Merge the source memory into the most similar existing node (only one),
|
|
193
|
-
and establish a MERGED_FROM edge in the graph.
|
|
194
|
-
|
|
195
|
-
Parameters:
|
|
196
|
-
source_node: The new memory item (not yet in the graph)
|
|
197
|
-
similar_nodes: A list of dicts returned by search_by_embedding(), ordered by similarity
|
|
198
|
-
"""
|
|
199
|
-
original_node = similar_nodes[0]
|
|
200
|
-
original_id = original_node["id"]
|
|
201
|
-
original_data = self.graph_store.get_node(original_id)
|
|
202
|
-
|
|
203
|
-
target_text = original_data.get("memory", "")
|
|
204
|
-
merged_text = f"{target_text}\n⟵MERGED⟶\n{source_node.memory}"
|
|
205
|
-
|
|
206
|
-
original_meta = TreeNodeTextualMemoryMetadata(**original_data["metadata"])
|
|
207
|
-
source_meta = source_node.metadata
|
|
208
|
-
|
|
209
|
-
merged_key = source_meta.key or original_meta.key
|
|
210
|
-
merged_tags = list(set((original_meta.tags or []) + (source_meta.tags or [])))
|
|
211
|
-
merged_sources = list(set((original_meta.sources or []) + (source_meta.sources or [])))
|
|
212
|
-
merged_background = f"{original_meta.background}\n⟵MERGED⟶\n{source_meta.background}"
|
|
213
|
-
merged_embedding = self.embedder.embed([merged_text])[0]
|
|
214
|
-
|
|
215
|
-
original_conf = original_meta.confidence or 0.0
|
|
216
|
-
source_conf = source_meta.confidence or 0.0
|
|
217
|
-
merged_confidence = float((original_conf + source_conf) / 2)
|
|
218
|
-
merged_usage = list(set((original_meta.usage or []) + (source_meta.usage or [])))
|
|
219
|
-
|
|
220
|
-
# Create new merged node
|
|
221
|
-
merged_id = str(uuid.uuid4())
|
|
222
|
-
merged_metadata = source_meta.model_copy(
|
|
223
|
-
update={
|
|
224
|
-
"embedding": merged_embedding,
|
|
225
|
-
"updated_at": datetime.now().isoformat(),
|
|
226
|
-
"key": merged_key,
|
|
227
|
-
"tags": merged_tags,
|
|
228
|
-
"sources": merged_sources,
|
|
229
|
-
"background": merged_background,
|
|
230
|
-
"confidence": merged_confidence,
|
|
231
|
-
"usage": merged_usage,
|
|
232
|
-
}
|
|
233
|
-
)
|
|
234
|
-
|
|
161
|
+
node_id = str(uuid.uuid4())
|
|
162
|
+
# Step 2: Add new node to graph
|
|
235
163
|
self.graph_store.add_node(
|
|
236
|
-
|
|
164
|
+
node_id, memory.memory, memory.metadata.model_dump(exclude_none=True)
|
|
237
165
|
)
|
|
238
|
-
|
|
239
|
-
# Add traceability edges: both original and new point to merged node
|
|
240
|
-
self.graph_store.add_edge(original_id, merged_id, type="MERGED_TO")
|
|
241
|
-
self.graph_store.update_node(original_id, {"status": "archived"})
|
|
242
|
-
source_id = str(uuid.uuid4())
|
|
243
|
-
source_metadata = source_node.metadata.model_copy(update={"status": "archived"})
|
|
244
|
-
self.graph_store.add_node(source_id, source_node.memory, source_metadata.model_dump())
|
|
245
|
-
self.graph_store.add_edge(source_id, merged_id, type="MERGED_TO")
|
|
246
|
-
# After creating merged node and tracing lineage
|
|
247
|
-
self._inherit_edges(original_id, merged_id)
|
|
248
|
-
|
|
249
|
-
# log to reorganizer before updating the graph
|
|
250
166
|
self.reorganizer.add_message(
|
|
251
167
|
QueueMessage(
|
|
252
|
-
op="
|
|
253
|
-
|
|
254
|
-
original_id,
|
|
255
|
-
source_node.id,
|
|
256
|
-
],
|
|
257
|
-
after_node=[merged_id],
|
|
168
|
+
op="add",
|
|
169
|
+
after_node=[node_id],
|
|
258
170
|
)
|
|
259
171
|
)
|
|
260
|
-
return
|
|
172
|
+
return node_id
|
|
261
173
|
|
|
262
174
|
def _inherit_edges(self, from_id: str, to_id: str) -> None:
|
|
263
175
|
"""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import traceback
|
|
2
3
|
|
|
3
4
|
from memos.embedders.factory import OllamaEmbedder
|
|
4
5
|
from memos.graph_dbs.item import GraphDBNode
|
|
@@ -30,53 +31,59 @@ class RelationAndReasoningDetector:
|
|
|
30
31
|
3) Sequence links
|
|
31
32
|
4) Aggregate concepts
|
|
32
33
|
"""
|
|
33
|
-
if node.metadata.type == "reasoning":
|
|
34
|
-
logger.info(f"Skip reasoning for inferred node {node.id}")
|
|
35
|
-
return {
|
|
36
|
-
"relations": [],
|
|
37
|
-
"inferred_nodes": [],
|
|
38
|
-
"sequence_links": [],
|
|
39
|
-
"aggregate_nodes": [],
|
|
40
|
-
}
|
|
41
|
-
|
|
42
34
|
results = {
|
|
43
35
|
"relations": [],
|
|
44
36
|
"inferred_nodes": [],
|
|
45
37
|
"sequence_links": [],
|
|
46
38
|
"aggregate_nodes": [],
|
|
47
39
|
}
|
|
40
|
+
try:
|
|
41
|
+
if node.metadata.type == "reasoning":
|
|
42
|
+
logger.info(f"Skip reasoning for inferred node {node.id}")
|
|
43
|
+
return {
|
|
44
|
+
"relations": [],
|
|
45
|
+
"inferred_nodes": [],
|
|
46
|
+
"sequence_links": [],
|
|
47
|
+
"aggregate_nodes": [],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
nearest = self.graph_store.get_neighbors_by_tag(
|
|
51
|
+
tags=node.metadata.tags,
|
|
52
|
+
exclude_ids=exclude_ids,
|
|
53
|
+
top_k=top_k,
|
|
54
|
+
min_overlap=2,
|
|
55
|
+
)
|
|
56
|
+
nearest = [GraphDBNode(**cand_data) for cand_data in nearest]
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
# 1) Pairwise relations (including CAUSE/CONDITION/CONFLICT)
|
|
60
|
+
pairwise = self._detect_pairwise_causal_condition_relations(node, nearest)
|
|
61
|
+
results["relations"].extend(pairwise["relations"])
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
# 2) Inferred nodes (from causal/condition)
|
|
66
|
+
inferred = self._infer_fact_nodes_from_relations(pairwise)
|
|
67
|
+
results["inferred_nodes"].extend(inferred)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
3) Sequence (optional, if you have timestamps)
|
|
72
|
+
seq = self._detect_sequence_links(node, nearest)
|
|
73
|
+
results["sequence_links"].extend(seq)
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
# 4) Aggregate
|
|
78
|
+
agg = self._detect_aggregate_node_for_group(node, nearest, min_group_size=5)
|
|
79
|
+
if agg:
|
|
80
|
+
results["aggregate_nodes"].append(agg)
|
|
81
|
+
"""
|
|
48
82
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
min_overlap=2,
|
|
54
|
-
)
|
|
55
|
-
nearest = [GraphDBNode(**cand_data) for cand_data in nearest]
|
|
56
|
-
|
|
57
|
-
"""
|
|
58
|
-
# 1) Pairwise relations (including CAUSE/CONDITION/CONFLICT)
|
|
59
|
-
pairwise = self._detect_pairwise_causal_condition_relations(node, nearest)
|
|
60
|
-
results["relations"].extend(pairwise["relations"])
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
"""
|
|
64
|
-
# 2) Inferred nodes (from causal/condition)
|
|
65
|
-
inferred = self._infer_fact_nodes_from_relations(pairwise)
|
|
66
|
-
results["inferred_nodes"].extend(inferred)
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
"""
|
|
70
|
-
3) Sequence (optional, if you have timestamps)
|
|
71
|
-
seq = self._detect_sequence_links(node, nearest)
|
|
72
|
-
results["sequence_links"].extend(seq)
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
# 4) Aggregate
|
|
76
|
-
agg = self._detect_aggregate_node_for_group(node, nearest, min_group_size=5)
|
|
77
|
-
if agg:
|
|
78
|
-
results["aggregate_nodes"].append(agg)
|
|
79
|
-
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(
|
|
85
|
+
f"Error {e} while process struct reorganize: trace: {traceback.format_exc()}"
|
|
86
|
+
)
|
|
80
87
|
return results
|
|
81
88
|
|
|
82
89
|
def _detect_pairwise_causal_condition_relations(
|
|
@@ -176,10 +183,9 @@ class RelationAndReasoningDetector:
|
|
|
176
183
|
joined = "\n".join(f"- {n.memory}" for n in combined_nodes)
|
|
177
184
|
prompt = AGGREGATE_PROMPT.replace("{joined}", joined)
|
|
178
185
|
response_text = self._call_llm(prompt)
|
|
179
|
-
|
|
180
|
-
if not
|
|
186
|
+
summary = self._parse_json_result(response_text)
|
|
187
|
+
if not summary:
|
|
181
188
|
return None
|
|
182
|
-
summary = json.loads(response_text)
|
|
183
189
|
embedding = self.embedder.embed([summary["value"]])[0]
|
|
184
190
|
|
|
185
191
|
parent_node = GraphDBNode(
|