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.

Files changed (92) hide show
  1. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/METADATA +7 -1
  2. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/RECORD +87 -64
  3. memos/__init__.py +1 -1
  4. memos/api/config.py +158 -69
  5. memos/api/context/context.py +147 -0
  6. memos/api/context/dependencies.py +101 -0
  7. memos/api/product_models.py +5 -1
  8. memos/api/routers/product_router.py +54 -26
  9. memos/configs/graph_db.py +49 -1
  10. memos/configs/internet_retriever.py +19 -0
  11. memos/configs/mem_os.py +5 -0
  12. memos/configs/mem_reader.py +9 -0
  13. memos/configs/mem_scheduler.py +54 -18
  14. memos/configs/mem_user.py +58 -0
  15. memos/graph_dbs/base.py +38 -3
  16. memos/graph_dbs/factory.py +2 -0
  17. memos/graph_dbs/nebular.py +1612 -0
  18. memos/graph_dbs/neo4j.py +18 -9
  19. memos/log.py +6 -1
  20. memos/mem_cube/utils.py +13 -6
  21. memos/mem_os/core.py +157 -37
  22. memos/mem_os/main.py +2 -2
  23. memos/mem_os/product.py +252 -201
  24. memos/mem_os/utils/default_config.py +1 -1
  25. memos/mem_os/utils/format_utils.py +281 -70
  26. memos/mem_os/utils/reference_utils.py +133 -0
  27. memos/mem_reader/simple_struct.py +13 -5
  28. memos/mem_scheduler/base_scheduler.py +239 -266
  29. memos/mem_scheduler/{modules → general_modules}/base.py +4 -5
  30. memos/mem_scheduler/{modules → general_modules}/dispatcher.py +57 -21
  31. memos/mem_scheduler/general_modules/misc.py +104 -0
  32. memos/mem_scheduler/{modules → general_modules}/rabbitmq_service.py +12 -10
  33. memos/mem_scheduler/{modules → general_modules}/redis_service.py +1 -1
  34. memos/mem_scheduler/general_modules/retriever.py +199 -0
  35. memos/mem_scheduler/general_modules/scheduler_logger.py +261 -0
  36. memos/mem_scheduler/general_scheduler.py +243 -80
  37. memos/mem_scheduler/monitors/__init__.py +0 -0
  38. memos/mem_scheduler/monitors/dispatcher_monitor.py +305 -0
  39. memos/mem_scheduler/{modules/monitor.py → monitors/general_monitor.py} +106 -57
  40. memos/mem_scheduler/mos_for_test_scheduler.py +23 -20
  41. memos/mem_scheduler/schemas/__init__.py +0 -0
  42. memos/mem_scheduler/schemas/general_schemas.py +44 -0
  43. memos/mem_scheduler/schemas/message_schemas.py +149 -0
  44. memos/mem_scheduler/schemas/monitor_schemas.py +337 -0
  45. memos/mem_scheduler/utils/__init__.py +0 -0
  46. memos/mem_scheduler/utils/filter_utils.py +176 -0
  47. memos/mem_scheduler/utils/misc_utils.py +102 -0
  48. memos/mem_user/factory.py +94 -0
  49. memos/mem_user/mysql_persistent_user_manager.py +271 -0
  50. memos/mem_user/mysql_user_manager.py +500 -0
  51. memos/mem_user/persistent_factory.py +96 -0
  52. memos/mem_user/user_manager.py +4 -4
  53. memos/memories/activation/item.py +5 -1
  54. memos/memories/activation/kv.py +20 -8
  55. memos/memories/textual/base.py +2 -2
  56. memos/memories/textual/general.py +36 -92
  57. memos/memories/textual/item.py +5 -33
  58. memos/memories/textual/tree.py +13 -7
  59. memos/memories/textual/tree_text_memory/organize/{conflict.py → handler.py} +34 -50
  60. memos/memories/textual/tree_text_memory/organize/manager.py +8 -96
  61. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +49 -43
  62. memos/memories/textual/tree_text_memory/organize/reorganizer.py +107 -142
  63. memos/memories/textual/tree_text_memory/retrieve/bochasearch.py +229 -0
  64. memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
  65. memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +11 -0
  66. memos/memories/textual/tree_text_memory/retrieve/recall.py +15 -8
  67. memos/memories/textual/tree_text_memory/retrieve/reranker.py +1 -1
  68. memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
  69. memos/memories/textual/tree_text_memory/retrieve/searcher.py +191 -116
  70. memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +47 -15
  71. memos/memories/textual/tree_text_memory/retrieve/utils.py +11 -7
  72. memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py +62 -58
  73. memos/memos_tools/dinding_report_bot.py +422 -0
  74. memos/memos_tools/lockfree_dict.py +120 -0
  75. memos/memos_tools/notification_service.py +44 -0
  76. memos/memos_tools/notification_utils.py +96 -0
  77. memos/memos_tools/thread_safe_dict.py +288 -0
  78. memos/settings.py +3 -1
  79. memos/templates/mem_reader_prompts.py +4 -1
  80. memos/templates/mem_scheduler_prompts.py +62 -15
  81. memos/templates/mos_prompts.py +116 -0
  82. memos/templates/tree_reorganize_prompts.py +24 -17
  83. memos/utils.py +19 -0
  84. memos/mem_scheduler/modules/misc.py +0 -39
  85. memos/mem_scheduler/modules/retriever.py +0 -268
  86. memos/mem_scheduler/modules/schemas.py +0 -328
  87. memos/mem_scheduler/utils.py +0 -75
  88. memos/memories/textual/tree_text_memory/organize/redundancy.py +0 -193
  89. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/LICENSE +0 -0
  90. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/WHEEL +0 -0
  91. {memoryos-0.2.1.dist-info → memoryos-1.0.0.dist-info}/entry_points.txt +0 -0
  92. /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
- EXTRACTION_RETRY_LOG.format(
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
- str_messages = json.dumps(messages)
56
- user_query = EXTRACTION_PROMPT_PART_1 + EXTRACTION_PROMPT_PART_2.format(
57
- messages=str_messages
52
+
53
+ str_messages = "\n".join(
54
+ [message["role"] + ":" + message["content"] for message in messages]
58
55
  )
59
- response = self.extractor_llm.generate([{"role": "user", "content": user_query}])
60
- raw_extracted_memories = json.loads(response)
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(**memory_dict) for memory_dict in raw_extracted_memories
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
- EXTRACTION_PROMPT_PART_1 = f"""You are a memory extractor. Your task is to extract memories from the given messages.
211
- * You will receive a list of messages, each with a role (user or assistant) and content.
212
- * Your job is to extract memories related to the user's long-term goals, interests, and emotional states.
213
- * Each memory should be a dictionary with the following keys:
214
- - "memory": The content of the memory (string). Rephrase the content if necessary.
215
- - "metadata": A dictionary containing additional information about the memory.
216
- * The metadata dictionary should include:
217
- - "type": The type of memory (string), e.g., "procedure", "fact", "event", "opinion", etc.
218
- - "memory_time": The time the memory occurred or refers to (string). Must be in standard `YYYY-MM-DD` format. Relative expressions such as "yesterday" or "tomorrow" are not allowed.
219
- - "source": The origin of the memory (string), e.g., `"conversation"`, `"retrieved"`, `"web"`, `"file"`.
220
- - "confidence": A numeric score (float between 0 and 100) indicating how certain you are about the accuracy or reliability of the memory.
221
- - "entities": A list of key entities (array of strings) mentioned in the memory, e.g., people, places, organizations, e.g., `["Alice", "Paris", "OpenAI"]`.
222
- - "tags": A list of keywords or thematic labels (array of strings) associated with the memory for categorization or retrieval, e.g., `["travel", "health", "project-x"]`.
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 {}
@@ -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: Literal["procedure", "fact", "event", "opinion", "topic", "reasoning"] | None = Field(
31
- default=None
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
- entities: list[str] | None = Field(
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:
@@ -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
- self.internet_retriever = None
121
- searcher = Searcher(
122
- self.dispatcher_llm,
123
- self.graph_store,
124
- self.embedder,
125
- internet_retriever=self.internet_retriever,
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
- CONFLICT_DETECTOR_PROMPT,
13
- CONFLICT_RESOLVER_PROMPT,
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 ConflictHandler:
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
- conflict_pairs = []
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": CONFLICT_DETECTOR_PROMPT.format(
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 "yes" in result.lower():
71
- conflict_pairs.append([memory, embedding_candidate])
72
- if len(conflict_pairs):
73
- conflict_text = "\n".join(
74
- f'"{pair[0].memory!s}" <==CONFLICT==> "{pair[1].memory!s}"'
75
- for pair in conflict_pairs
76
- )
77
- logger.warning(
78
- f"Detected {len(conflict_pairs)} conflicts for memory {memory.id}\n {conflict_text}"
79
- )
80
- return conflict_pairs
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": CONFLICT_RESOLVER_PROMPT.format(
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"Conflict between {memory_a.id} and {memory_b.id} could not be resolved. "
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 = datetime.fromisoformat(memory_a.metadata.updated_at)
137
- time_b = datetime.fromisoformat(memory_b.metadata.updated_at)
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": 10000,
43
- "UserMemory": 10000,
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
- embedding = memory.metadata.embedding
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
- merged_id, merged_text, merged_metadata.model_dump(exclude_none=True)
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="merge",
253
- before_node=[
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 merged_id
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
- nearest = self.graph_store.get_neighbors_by_tag(
50
- tags=node.metadata.tags,
51
- exclude_ids=exclude_ids,
52
- top_k=top_k,
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
- response_json = self._parse_json_result(response_text)
180
- if not response_json:
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(