Skip to content

Commit 339c756

Browse files
authored
feat: add neo4j share-db example (#59)
* feat: add neo4j share-db example * feat: add Shared Database Multi-Tenant Mode * fix: neo4j auto create bug * fix: tree text memory reorganizer bug * feat: change 'RELATE_TO' to 'RELATE' * fix: database exist bug * feat: delete useless relation edge * feat: finish Shared Database Multi-Tenant Mode * feat: add share database multi tenant tree config * feat: add shared-db mode config * fix: file name bug in example tree text memory] * style: add user_name from config
1 parent 2012dd1 commit 339c756

File tree

10 files changed

+513
-234
lines changed

10 files changed

+513
-234
lines changed

examples/basic_modules/neo4j_example.py

Lines changed: 90 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@
88

99

1010
embedder_config = EmbedderConfigFactory.model_validate(
11-
{
12-
"backend": "sentence_transformer",
13-
"config": {
14-
"model_name_or_path": "nomic-ai/nomic-embed-text-v1.5",
15-
},
16-
}
11+
{"backend": "ollama", "config": {"model_name_or_path": "nomic-embed-text:latest"}}
1712
)
1813
embedder = EmbedderFactory.from_config(embedder_config)
1914

@@ -22,7 +17,7 @@ def embed_memory_item(memory: str) -> list[float]:
2217
return embedder.embed([memory])[0]
2318

2419

25-
def example_1_paper(db_name: str = "paper"):
20+
def example_multi_db(db_name: str = "paper"):
2621
# Step 1: Build factory config
2722
config = GraphDBConfigFactory(
2823
backend="neo4j",
@@ -33,6 +28,7 @@ def example_1_paper(db_name: str = "paper"):
3328
"db_name": db_name,
3429
"auto_create": True,
3530
"embedding_dimension": 768,
31+
"use_multi_db": True,
3632
},
3733
)
3834

@@ -68,7 +64,7 @@ def example_1_paper(db_name: str = "paper"):
6864
)
6965

7066
graph.add_node(
71-
id=topic.id, content=topic.memory, metadata=topic.metadata.model_dump(exclude_none=True)
67+
id=topic.id, memory=topic.memory, metadata=topic.metadata.model_dump(exclude_none=True)
7268
)
7369

7470
# Step 4: Define and write concept nodes
@@ -150,7 +146,7 @@ def example_1_paper(db_name: str = "paper"):
150146
for concept in concepts:
151147
graph.add_node(
152148
id=concept.id,
153-
content=concept.memory,
149+
memory=concept.memory,
154150
metadata=concept.metadata.model_dump(exclude_none=True),
155151
)
156152
graph.add_edge(source_id=concept.id, target_id=topic.id, type="RELATED")
@@ -259,113 +255,101 @@ def example_1_paper(db_name: str = "paper"):
259255
print(graph.get_node(node_i["id"]))
260256

261257

262-
def example_2_travel(db_name: str = "travel"):
263-
# Step 1: Build factory config
264-
config = GraphDBConfigFactory(
265-
backend="neo4j",
266-
config={
267-
"uri": "bolt://localhost:7687",
268-
"user": "neo4j",
269-
"password": "12345678",
270-
"db_name": db_name,
271-
"auto_create": True,
272-
"embedding_dimension": 768,
273-
},
274-
)
275-
276-
# Step 2: Instantiate the graph store
277-
graph = GraphStoreFactory.from_config(config)
278-
graph.clear()
279-
280-
# Step 3: Create topic node
281-
topic = TextualMemoryItem(
282-
memory="Travel",
283-
metadata=TreeNodeTextualMemoryMetadata(
284-
memory_type="LongTermMemory",
285-
hierarchy_level="topic",
286-
status="activated",
287-
visibility="public",
288-
embedding=embed_memory_item("Travel"),
289-
),
290-
)
291-
292-
graph.add_node(
293-
id=topic.id, content=topic.memory, metadata=topic.metadata.model_dump(exclude_none=True)
294-
)
295-
296-
concept1 = TextualMemoryItem(
297-
memory="Travel in Italy",
298-
metadata=TreeNodeTextualMemoryMetadata(
299-
memory_type="LongTermMemory",
300-
hierarchy_level="concept",
301-
status="activated",
302-
visibility="public",
303-
embedding=embed_memory_item("Travel in Italy"),
304-
),
305-
)
258+
def example_shared_db(db_name: str = "shared-traval-group"):
259+
"""
260+
Example: Single(Shared)-DB multi-tenant (logical isolation)
261+
Multiple users' data in the same Neo4j DB with user_name as a tag.
262+
"""
263+
# users
264+
user_list = ["travel_member_alice", "travel_member_bob"]
265+
266+
for user_name in user_list:
267+
# Step 1: Build factory config
268+
config = GraphDBConfigFactory(
269+
backend="neo4j",
270+
config={
271+
"uri": "bolt://localhost:7687",
272+
"user": "neo4j",
273+
"password": "12345678",
274+
"db_name": db_name,
275+
"user_name": user_name,
276+
"use_multi_db": False,
277+
"auto_create": True,
278+
"embedding_dimension": 768,
279+
},
280+
)
281+
# Step 2: Instantiate graph store
282+
graph = GraphStoreFactory.from_config(config)
283+
print(f"\n[INFO] Working in shared DB: {db_name}, for user: {user_name}")
284+
graph.clear()
285+
286+
# Step 3: Create topic node
287+
topic = TextualMemoryItem(
288+
memory=f"Travel notes for {user_name}",
289+
metadata=TreeNodeTextualMemoryMetadata(
290+
memory_type="LongTermMemory",
291+
hierarchy_level="topic",
292+
status="activated",
293+
visibility="public",
294+
embedding=embed_memory_item(f"Travel notes for {user_name}"),
295+
),
296+
)
306297

307-
graph.add_node(
308-
id=concept1.id,
309-
content=concept1.memory,
310-
metadata=concept1.metadata.model_dump(exclude_none=True),
311-
)
312-
graph.add_edge(source_id=topic.id, target_id=concept1.id, type="INCLUDE")
298+
graph.add_node(
299+
id=topic.id, memory=topic.memory, metadata=topic.metadata.model_dump(exclude_none=True)
300+
)
313301

314-
concept2 = TextualMemoryItem(
315-
memory="Traval plan",
316-
metadata=TreeNodeTextualMemoryMetadata(
317-
memory_type="LongTermMemory",
318-
hierarchy_level="concept",
319-
status="activated",
320-
visibility="public",
321-
embedding=embed_memory_item("Traval plan"),
322-
),
323-
)
302+
# Step 4: Add a concept for each user
303+
concept = TextualMemoryItem(
304+
memory=f"Itinerary plan for {user_name}",
305+
metadata=TreeNodeTextualMemoryMetadata(
306+
memory_type="LongTermMemory",
307+
hierarchy_level="concept",
308+
status="activated",
309+
visibility="public",
310+
embedding=embed_memory_item(f"Itinerary plan for {user_name}"),
311+
),
312+
)
324313

325-
graph.add_node(
326-
id=concept2.id,
327-
content=concept2.memory,
328-
metadata=concept2.metadata.model_dump(exclude_none=True),
329-
)
330-
graph.add_edge(source_id=concept1.id, target_id=concept2.id, type="INCLUDE")
314+
graph.add_node(
315+
id=concept.id,
316+
memory=concept.memory,
317+
metadata=concept.metadata.model_dump(exclude_none=True),
318+
)
331319

332-
fact1 = TextualMemoryItem(
333-
memory="10-Day Itinerary for Traveling in Italy",
334-
metadata=TreeNodeTextualMemoryMetadata(
335-
memory_type="WorkingMemory",
336-
key="Reward Components",
337-
value="Coverage gain, energy usage penalty, overlap penalty",
338-
hierarchy_level="fact",
339-
type="fact",
340-
memory_time="2024-01-01",
341-
source="file",
342-
sources=["paper://multi-uav-coverage/reward-details"],
343-
status="activated",
344-
confidence=90.0,
345-
tags=["reward", "overlap", "multi-agent"],
346-
entities=["coverage", "energy", "overlap"],
347-
visibility="public",
348-
embedding=embed_memory_item("10-Day Itinerary for Traveling in Italy"),
349-
updated_at=datetime.now().isoformat(),
350-
),
351-
)
320+
# Link concept to topic
321+
graph.add_edge(source_id=concept.id, target_id=topic.id, type="INCLUDE")
352322

353-
graph.add_node(
354-
id=fact1.id, content=fact1.memory, metadata=fact1.metadata.model_dump(exclude_none=True)
355-
)
356-
graph.add_edge(source_id=concept2.id, target_id=fact1.id, type="INCLUDE")
323+
print(f"[INFO] Added nodes for {user_name}")
357324

325+
# Step 5: Query and print ALL for verification
326+
print("\n=== Export entire DB (for verification, includes ALL users) ===")
327+
graph = GraphStoreFactory.from_config(config)
358328
all_graph_data = graph.export_graph()
359329
print(all_graph_data)
360330

361-
nodes = graph.search_by_embedding(vector=embed_memory_item("what does FT reflect?"), top_k=1)
362-
363-
for node_i in nodes:
364-
print(graph.get_node(node_i["id"]))
331+
# Step 6: Search for alice's data only
332+
print("\n=== Search for travel_member_alice ===")
333+
config_alice = GraphDBConfigFactory(
334+
backend="neo4j",
335+
config={
336+
"uri": "bolt://localhost:7687",
337+
"user": "neo4j",
338+
"password": "12345678",
339+
"db_name": db_name,
340+
"user_name": user_list[0],
341+
"embedding_dimension": 768,
342+
},
343+
)
344+
graph_alice = GraphStoreFactory.from_config(config_alice)
345+
nodes = graph_alice.search_by_embedding(vector=embed_memory_item("travel itinerary"), top_k=1)
346+
for node in nodes:
347+
print(graph_alice.get_node(node["id"]))
365348

366349

367350
if __name__ == "__main__":
368-
example_1_paper(db_name="paper")
351+
print("\n=== Example: Multi-DB ===")
352+
example_multi_db(db_name="paper")
369353

370-
if __name__ == "__main__":
371-
example_2_travel(db_name="traval")
354+
print("\n=== Example: Single-DB ===")
355+
example_shared_db(db_name="shared-traval-group11")

examples/core_memories/tree_textual_memory.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
13
from memos import log
24
from memos.configs.embedder import EmbedderConfigFactory
35
from memos.configs.mem_reader import SimpleStructMemReaderConfig
@@ -25,7 +27,9 @@ def embed_memory_item(memory: str) -> list[float]:
2527
return embedder.embed([memory])[0]
2628

2729

28-
tree_config = TreeTextMemoryConfig.from_json_file("examples/data/config/tree_config.json")
30+
tree_config = TreeTextMemoryConfig.from_json_file(
31+
"examples/data/config/tree_config_shared_database.json"
32+
)
2933
my_tree_textual_memory = TreeTextMemory(tree_config)
3034
my_tree_textual_memory.delete_all()
3135

@@ -185,6 +189,8 @@ def embed_memory_item(memory: str) -> list[float]:
185189
my_tree_textual_memory.add(m_list)
186190
my_tree_textual_memory.memory_manager.wait_reorganizer()
187191

192+
time.sleep(60)
193+
188194
results = my_tree_textual_memory.search(
189195
"Talk about the user's childhood story?",
190196
top_k=10,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"extractor_llm": {
3+
"backend": "ollama",
4+
"config": {
5+
"model_name_or_path": "qwen3:0.6b",
6+
"temperature": 0.0,
7+
"remove_think_prefix": true,
8+
"max_tokens": 8192
9+
}
10+
},
11+
"dispatcher_llm": {
12+
"backend": "ollama",
13+
"config": {
14+
"model_name_or_path": "qwen3:0.6b",
15+
"temperature": 0.0,
16+
"remove_think_prefix": true,
17+
"max_tokens": 8192
18+
}
19+
},
20+
"embedder": {
21+
"backend": "ollama",
22+
"config": {
23+
"model_name_or_path": "nomic-embed-text:latest"
24+
}
25+
},
26+
"graph_db": {
27+
"backend": "neo4j",
28+
"config": {
29+
"uri": "bolt://localhost:7687",
30+
"user": "neo4j",
31+
"password": "12345678",
32+
"db_name": "shared-tree-textual-memory",
33+
"user_name": "alice",
34+
"auto_create": true,
35+
"use_multi_db": false,
36+
"embedding_dimension": 768
37+
}
38+
},
39+
"reorganize": true
40+
}

src/memos/configs/graph_db.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,70 @@ class BaseGraphDBConfig(BaseConfig):
1414

1515

1616
class Neo4jGraphDBConfig(BaseGraphDBConfig):
17-
"""Neo4j-specific configuration."""
17+
"""
18+
Neo4j-specific configuration.
19+
20+
This config supports:
21+
1) Physical isolation (multi-db) — each user gets a dedicated Neo4j database.
22+
2) Logical isolation (single-db) — all users share one or more databases, but each node is tagged with `user_name`.
23+
24+
How to use:
25+
- If `use_multi_db=True`, then `db_name` should usually be the same as `user_name`.
26+
Each user gets a separate database for physical isolation.
27+
Example: db_name = "alice", user_name = None or "alice".
28+
29+
- If `use_multi_db=False`, then `db_name` is your shared database (e.g., "neo4j" or "shared_db").
30+
You must provide `user_name` to logically isolate each user's data.
31+
All nodes and queries must respect this tag.
32+
33+
Example configs:
34+
---
35+
# Physical isolation:
36+
db_name = "alice"
37+
use_multi_db = True
38+
user_name = None
39+
40+
# Logical isolation:
41+
db_name = "shared_db_student_group"
42+
use_multi_db = False
43+
user_name = "alice"
44+
"""
1845

1946
db_name: str = Field(..., description="The name of the target Neo4j database")
2047
auto_create: bool = Field(
21-
default=False, description="Whether to create the DB if it doesn't exist"
48+
default=False,
49+
description="If True, automatically create the target db_name in multi-db mode if it does not exist.",
50+
)
51+
52+
use_multi_db: bool = Field(
53+
default=True,
54+
description=(
55+
"If True: use Neo4j's multi-database feature for physical isolation; "
56+
"each user typically gets a separate database. "
57+
"If False: use a single shared database with logical isolation by user_name."
58+
),
2259
)
60+
61+
user_name: str | None = Field(
62+
default=None,
63+
description=(
64+
"Logical user or tenant ID for data isolation. "
65+
"Required if use_multi_db is False. "
66+
"All nodes must be tagged with this and all queries must filter by this."
67+
),
68+
)
69+
2370
embedding_dimension: int = Field(default=768, description="Dimension of vector embedding")
2471

72+
@model_validator(mode="after")
73+
def validate_config(self):
74+
"""Validate logical constraints to avoid misconfiguration."""
75+
if not self.use_multi_db and not self.user_name:
76+
raise ValueError(
77+
"In single-database mode (use_multi_db=False), `user_name` must be provided for logical isolation."
78+
)
79+
return self
80+
2581

2682
class GraphDBConfigFactory(BaseModel):
2783
backend: str = Field(..., description="Backend for graph database")

0 commit comments

Comments
 (0)