11import json
22import re
3-
43from datetime import datetime
54
65from dateutil import parser
1110from memos .log import get_logger
1211from memos .memories .textual .item import TextualMemoryItem , TreeNodeTextualMemoryMetadata
1312from memos .templates .tree_reorganize_prompts import (
14- CONFLICT_DETECTOR_PROMPT ,
15- CONFLICT_RESOLVER_PROMPT ,
13+ MEMORY_RELATION_DETECTOR_PROMPT ,
14+ MEMORY_RELATION_RESOLVER_PROMPT ,
1615)
1716
18-
1917logger = get_logger (__name__ )
2018
2119
22- class ConflictHandler :
20+ class NodeHandler :
2321 EMBEDDING_THRESHOLD : float = 0.8 # Threshold for embedding similarity to consider conflict
2422
2523 def __init__ (self , graph_store : Neo4jGraphDB , llm : BaseLLM , embedder : BaseEmbedder ):
2624 self .graph_store = graph_store
2725 self .llm = llm
2826 self .embedder = embedder
2927
30- def detect (
31- self , memory : TextualMemoryItem , top_k : int = 5 , scope : str | None = None
32- ) -> list [tuple [TextualMemoryItem , TextualMemoryItem ]]:
33- """
34- Detect conflicts by finding the most similar items in the graph database based on embedding, then use LLM to judge conflict.
35- Args:
36- memory: The memory item (should have an embedding attribute or field).
37- top_k: Number of top similar nodes to retrieve.
38- scope: Optional memory type filter.
39- Returns:
40- List of conflict pairs (each pair is a tuple: (memory, candidate)).
41- """
28+ def detect (self , memory , top_k : int = 5 , scope = None ):
4229 # 1. Search for similar memories based on embedding
4330 embedding = memory .metadata .embedding
4431 embedding_candidates_info = self .graph_store .search_by_embedding (
45- embedding , top_k = top_k , scope = scope
32+ embedding , top_k = top_k , scope = scope , threshold = self . EMBEDDING_THRESHOLD
4633 )
4734 # 2. Filter based on similarity threshold
4835 embedding_candidates_ids = [
49- info ["id" ]
50- for info in embedding_candidates_info
51- 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
5237 ]
5338 # 3. Judge conflicts using LLM
5439 embedding_candidates = self .graph_store .get_nodes (embedding_candidates_ids )
55- conflict_pairs = []
40+ detected_relationships = []
5641 for embedding_candidate in embedding_candidates :
5742 embedding_candidate = TextualMemoryItem .from_dict (embedding_candidate )
5843 prompt = [
59- {
60- "role" : "system" ,
61- "content" : "You are a conflict detector for memory items." ,
62- },
6344 {
6445 "role" : "user" ,
65- "content" : CONFLICT_DETECTOR_PROMPT .format (
66- statement_1 = memory .memory ,
67- statement_2 = embedding_candidate .memory ,
46+ "content" : MEMORY_RELATION_DETECTOR_PROMPT .format (
47+ statement_1 = memory .memory , statement_2 = embedding_candidate .memory
6848 ),
69- },
49+ }
7050 ]
7151 result = self .llm .generate (prompt ).strip ()
72- if "yes" in result .lower ():
73- conflict_pairs .append ([memory , embedding_candidate ])
74- if len (conflict_pairs ):
75- conflict_text = "\n " .join (
76- f'"{ pair [0 ].memory !s} " <==CONFLICT==> "{ pair [1 ].memory !s} "'
77- for pair in conflict_pairs
78- )
79- logger .warning (
80- f"Detected { len (conflict_pairs )} conflicts for memory { memory .id } \n { conflict_text } "
81- )
82- 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
8367
84- def resolve (self , memory_a : TextualMemoryItem , memory_b : TextualMemoryItem ) -> None :
68+ def resolve (self , memory_a : TextualMemoryItem , memory_b : TextualMemoryItem , relation ) -> None :
8569 """
8670 Resolve detected conflicts between two memory items using LLM fusion.
8771 Args:
8872 memory_a: The first conflicting memory item.
8973 memory_b: The second conflicting memory item.
74+ relation: relation
9075 Returns:
9176 A fused TextualMemoryItem representing the resolved memory.
9277 """
@@ -96,13 +81,10 @@ def resolve(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem) -> N
9681 metadata_1 = memory_a .metadata .model_dump_json (include = metadata_for_resolve )
9782 metadata_2 = memory_b .metadata .model_dump_json (include = metadata_for_resolve )
9883 prompt = [
99- {
100- "role" : "system" ,
101- "content" : "" ,
102- },
10384 {
10485 "role" : "user" ,
105- "content" : CONFLICT_RESOLVER_PROMPT .format (
86+ "content" : MEMORY_RELATION_RESOLVER_PROMPT .format (
87+ relation = relation ,
10688 statement_1 = memory_a .memory ,
10789 metadata_1 = metadata_1 ,
10890 statement_2 = memory_b .memory ,
@@ -119,7 +101,7 @@ def resolve(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem) -> N
119101 # —————— 2.1 Can't resolve conflict, hard update by comparing timestamp ————
120102 if len (answer ) <= 10 and "no" in answer .lower ():
121103 logger .warning (
122- 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. "
123105 )
124106 self ._hard_update (memory_a , memory_b )
125107 # —————— 2.2 Conflict resolved, update metadata and memory ————
0 commit comments