Skip to content

Commit 8abc88a

Browse files
authored
feat: add structure reorganizer and conflict resolver (#14)
1 parent e3031f5 commit 8abc88a

File tree

18 files changed

+1833
-19
lines changed

18 files changed

+1833
-19
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ evaluation/*tmp/
1111
evaluation/results
1212
evaluation/.env
1313
evaluation/configs/*
14+
**tree_textual_memory_locomo**
1415
.env
1516

1617
# Byte-compiled / optimized / DLL files
@@ -165,6 +166,7 @@ venv.bak/
165166
*.xlsx
166167
*.json
167168
*.pkl
169+
*.html
168170

169171
# but do not ignore docs/openapi.json
170172
!docs/openapi.json
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import uuid
2+
3+
from memos import log
4+
from memos.configs.embedder import EmbedderConfigFactory
5+
from memos.configs.graph_db import GraphDBConfigFactory
6+
from memos.configs.llm import LLMConfigFactory
7+
from memos.embedders.factory import EmbedderFactory
8+
from memos.graph_dbs.factory import GraphStoreFactory
9+
from memos.graph_dbs.item import GraphDBNode
10+
from memos.llms.factory import LLMFactory
11+
from memos.memories.textual.item import TreeNodeTextualMemoryMetadata
12+
from memos.memories.textual.tree_text_memory.organize.relation_reason_detector import (
13+
RelationAndReasoningDetector,
14+
)
15+
16+
17+
logger = log.get_logger(__name__)
18+
19+
# === Step 1: Initialize embedder ===
20+
embedder_config = EmbedderConfigFactory.model_validate(
21+
{
22+
"backend": "ollama",
23+
"config": {
24+
"model_name_or_path": "nomic-embed-text:latest",
25+
},
26+
}
27+
)
28+
embedder = EmbedderFactory.from_config(embedder_config)
29+
30+
# === Step 2: Initialize Neo4j GraphStore ===
31+
graph_config = GraphDBConfigFactory(
32+
backend="neo4j",
33+
config={
34+
"uri": "bolt://localhost:7687",
35+
"user": "neo4j",
36+
"password": "12345678",
37+
"db_name": "lucy4",
38+
"auto_create": True,
39+
},
40+
)
41+
graph_store = GraphStoreFactory.from_config(graph_config)
42+
43+
# === Step 3: Initialize LLM for pairwise relation detection ===
44+
# Step 1: Load LLM config and instantiate
45+
config = LLMConfigFactory.model_validate(
46+
{
47+
"backend": "ollama",
48+
"config": {
49+
"model_name_or_path": "qwen3:0.6b",
50+
"temperature": 0.7,
51+
"max_tokens": 1024,
52+
},
53+
}
54+
)
55+
llm = LLMFactory.from_config(config)
56+
57+
# === Step 4: Create a mock GraphDBNode to test relation detection ===
58+
59+
node_a = GraphDBNode(
60+
id=str(uuid.uuid4()),
61+
memory="Caroline faced increased workload stress during the project deadline.",
62+
metadata=TreeNodeTextualMemoryMetadata(
63+
memory_type="LongTermMemory",
64+
embedding=[0.1] * 10,
65+
key="Workload stress",
66+
tags=["stress", "workload"],
67+
type="fact",
68+
background="Project",
69+
confidence=0.95,
70+
updated_at="2024-06-28T09:00:00Z",
71+
),
72+
)
73+
74+
node_b = GraphDBNode(
75+
id=str(uuid.uuid4()),
76+
memory="After joining the support group, Caroline reported improved mental health.",
77+
metadata=TreeNodeTextualMemoryMetadata(
78+
memory_type="LongTermMemory",
79+
embedding=[0.1] * 10,
80+
key="Improved mental health",
81+
tags=["mental health", "support group"],
82+
type="fact",
83+
background="Personal follow-up",
84+
confidence=0.95,
85+
updated_at="2024-07-10T12:00:00Z",
86+
),
87+
)
88+
89+
node_c = GraphDBNode(
90+
id=str(uuid.uuid4()),
91+
memory="Peer support groups are effective in reducing stress for LGBTQ individuals.",
92+
metadata=TreeNodeTextualMemoryMetadata(
93+
memory_type="LongTermMemory",
94+
embedding=[0.1] * 10,
95+
key="Support group benefits",
96+
tags=["LGBTQ", "support group", "stress"],
97+
type="fact",
98+
background="General research",
99+
confidence=0.95,
100+
updated_at="2024-06-29T14:00:00Z",
101+
),
102+
)
103+
104+
# === D: Work pressure ➜ stress ===
105+
node_d = GraphDBNode(
106+
id=str(uuid.uuid4()),
107+
memory="Excessive work pressure increases stress levels among employees.",
108+
metadata=TreeNodeTextualMemoryMetadata(
109+
memory_type="LongTermMemory",
110+
embedding=[0.1] * 10,
111+
key="Work pressure impact",
112+
tags=["stress", "work pressure"],
113+
type="fact",
114+
background="Workplace study",
115+
confidence=0.9,
116+
updated_at="2024-06-15T08:00:00Z",
117+
),
118+
)
119+
120+
# === E: Stress ➜ poor sleep ===
121+
node_e = GraphDBNode(
122+
id=str(uuid.uuid4()),
123+
memory="High stress levels often result in poor sleep quality.",
124+
metadata=TreeNodeTextualMemoryMetadata(
125+
memory_type="LongTermMemory",
126+
embedding=[0.1] * 10,
127+
key="Stress and sleep",
128+
tags=["stress", "sleep"],
129+
type="fact",
130+
background="Health study",
131+
confidence=0.9,
132+
updated_at="2024-06-18T10:00:00Z",
133+
),
134+
)
135+
136+
# === F: Poor sleep ➜ low performance ===
137+
node_f = GraphDBNode(
138+
id=str(uuid.uuid4()),
139+
memory="Employees with poor sleep show reduced work performance.",
140+
metadata=TreeNodeTextualMemoryMetadata(
141+
memory_type="LongTermMemory",
142+
embedding=[0.1] * 10,
143+
key="Sleep and performance",
144+
tags=["sleep", "performance"],
145+
type="fact",
146+
background="HR report",
147+
confidence=0.9,
148+
updated_at="2024-06-20T12:00:00Z",
149+
),
150+
)
151+
152+
node = GraphDBNode(
153+
id="a88db9ce-3c77-4e83-8d61-aa9ef95c957e",
154+
memory="Caroline joined an LGBTQ support group to cope with work-related stress.",
155+
metadata=TreeNodeTextualMemoryMetadata(
156+
memory_type="LongTermMemory",
157+
embedding=embedder.embed(
158+
["Caroline joined an LGBTQ support group to cope with work-related stress."]
159+
)[0],
160+
key="Caroline LGBTQ stress",
161+
tags=["LGBTQ", "support group", "stress"],
162+
type="fact",
163+
background="Personal",
164+
confidence=0.95,
165+
updated_at="2024-07-01T10:00:00Z",
166+
),
167+
)
168+
169+
170+
for n in [node, node_a, node_b, node_c, node_d, node_e, node_f]:
171+
graph_store.add_node(n.id, n.memory, n.metadata.dict())
172+
173+
174+
# === Step 5: Initialize RelationDetector and run detection ===
175+
relation_detector = RelationAndReasoningDetector(
176+
graph_store=graph_store, llm=llm, embedder=embedder
177+
)
178+
179+
results = relation_detector.process_node(
180+
node=node,
181+
exclude_ids=[node.id], # Exclude self when searching for neighbors
182+
top_k=5,
183+
)
184+
185+
# === Step 6: Print detected relations ===
186+
print("\n=== Detected Global Relations ===")
187+
188+
189+
# === Step 6: Pretty-print detected results ===
190+
print("\n=== Detected Pairwise Relations ===")
191+
for rel in results["relations"]:
192+
print(f" Source ID: {rel['source_id']}")
193+
print(f" Target ID: {rel['target_id']}")
194+
print(f" Relation Type: {rel['relation_type']}")
195+
print("------")
196+
197+
print("\n=== Inferred Nodes ===")
198+
for node in results["inferred_nodes"]:
199+
print(f" New Fact: {node.memory}")
200+
print(f" Sources: {node.metadata.sources}")
201+
print("------")
202+
203+
print("\n=== Sequence Links (FOLLOWS) ===")
204+
for link in results["sequence_links"]:
205+
print(f" From: {link['from_id']} -> To: {link['to_id']}")
206+
print("------")
207+
208+
print("\n=== Aggregate Concepts ===")
209+
for agg in results["aggregate_nodes"]:
210+
print(f" Concept Key: {agg.metadata.key}")
211+
print(f" Concept Memory: {agg.memory}")
212+
print(f" Sources: {agg.metadata.sources}")
213+
print("------")

examples/core_memories/tree_textual_memory.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def embed_memory_item(memory: str) -> list[float]:
183183

184184
for m_list in memory:
185185
my_tree_textual_memory.add(m_list)
186+
my_tree_textual_memory.memory_manager.wait_reorganizer()
186187

187188
results = my_tree_textual_memory.search(
188189
"Talk about the user's childhood story?",
@@ -211,6 +212,7 @@ def embed_memory_item(memory: str) -> list[float]:
211212

212213
for m_list in doc_memory:
213214
my_tree_textual_memory.add(m_list)
215+
my_tree_textual_memory.memory_manager.wait_reorganizer()
214216

215217
results = my_tree_textual_memory.search(
216218
"Tell me about what memos consist of?",
@@ -222,6 +224,9 @@ def embed_memory_item(memory: str) -> list[float]:
222224
print(f"{i}'th similar result is: " + str(r["memory"]))
223225
print(f"Successfully search {len(results)} memories")
224226

227+
# close the synchronous thread in memory manager
228+
my_tree_textual_memory.memory_manager.close()
229+
225230

226231
# my_tree_textual_memory.dump
227232
my_tree_textual_memory.dump("tmp/my_tree_textual_memory")

examples/data/config/tree_config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@
3333
"auto_create": true,
3434
"embedding_dimension": 768
3535
}
36-
}
36+
},
37+
"reorganize": false
3738
}

poetry.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fastapi = {extras = ["all"], version = "^0.115.12"}
2626
sentence-transformers = "^4.1.0"
2727
sqlalchemy = "^2.0.41"
2828
redis = "^6.2.0"
29+
schedule = "^1.2.2"
2930

3031
[tool.poetry.group.dev]
3132
optional = false

src/memos/configs/memory.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ class TreeTextMemoryConfig(BaseTextMemoryConfig):
161161
description="Internet retriever configuration (optional)",
162162
)
163163

164+
reorganize: bool | None = Field(
165+
False,
166+
description="Optional description for this memory configuration.",
167+
)
168+
164169

165170
# ─── 3. Global Memory Config Factory ──────────────────────────────────────────
166171

src/memos/graph_dbs/item.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import uuid
2+
3+
from typing import Any, Literal
4+
5+
from pydantic import BaseModel, ConfigDict, Field, field_validator
6+
7+
from memos.memories.textual.item import TextualMemoryItem
8+
9+
10+
class GraphDBNode(TextualMemoryItem):
11+
pass
12+
13+
14+
class GraphDBEdge(BaseModel):
15+
"""Represents an edge in a graph database (corresponds to Neo4j relationship)."""
16+
17+
id: str = Field(
18+
default_factory=lambda: str(uuid.uuid4()), description="Unique identifier for the edge"
19+
)
20+
source: str = Field(..., description="Source node ID")
21+
target: str = Field(..., description="Target node ID")
22+
type: Literal["RELATED", "PARENT"] = Field(
23+
..., description="Relationship type (must be one of 'RELATED', 'PARENT')"
24+
)
25+
properties: dict[str, Any] | None = Field(
26+
default=None, description="Additional properties for the edge"
27+
)
28+
29+
model_config = ConfigDict(extra="forbid")
30+
31+
@field_validator("id")
32+
@classmethod
33+
def validate_id(cls, v):
34+
"""Validate that ID is a valid UUID."""
35+
if not isinstance(v, str) or not uuid.UUID(v, version=4):
36+
raise ValueError("ID must be a valid UUID string")
37+
return v
38+
39+
@classmethod
40+
def from_dict(cls, data: dict[str, Any]) -> "GraphDBEdge":
41+
"""Create GraphDBEdge from dictionary."""
42+
return cls(**data)
43+
44+
def to_dict(self) -> dict[str, Any]:
45+
"""Convert to dictionary format."""
46+
return self.model_dump(exclude_none=True)

0 commit comments

Comments
 (0)