MemoryOS 0.2.0__py3-none-any.whl → 0.2.2__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.0.dist-info → memoryos-0.2.2.dist-info}/METADATA +67 -26
- memoryos-0.2.2.dist-info/RECORD +169 -0
- memoryos-0.2.2.dist-info/entry_points.txt +3 -0
- memos/__init__.py +1 -1
- memos/api/config.py +562 -0
- memos/api/context/context.py +147 -0
- memos/api/context/dependencies.py +90 -0
- memos/api/exceptions.py +28 -0
- memos/api/mcp_serve.py +502 -0
- memos/api/product_api.py +35 -0
- memos/api/product_models.py +163 -0
- memos/api/routers/__init__.py +1 -0
- memos/api/routers/product_router.py +386 -0
- memos/chunkers/sentence_chunker.py +8 -2
- memos/cli.py +113 -0
- memos/configs/embedder.py +27 -0
- memos/configs/graph_db.py +132 -3
- memos/configs/internet_retriever.py +6 -0
- memos/configs/llm.py +47 -0
- memos/configs/mem_cube.py +1 -1
- memos/configs/mem_os.py +5 -0
- memos/configs/mem_reader.py +9 -0
- memos/configs/mem_scheduler.py +107 -7
- memos/configs/mem_user.py +58 -0
- memos/configs/memory.py +5 -4
- memos/dependency.py +52 -0
- memos/embedders/ark.py +92 -0
- memos/embedders/factory.py +4 -0
- memos/embedders/sentence_transformer.py +8 -2
- memos/embedders/universal_api.py +32 -0
- memos/graph_dbs/base.py +11 -3
- memos/graph_dbs/factory.py +4 -0
- memos/graph_dbs/nebular.py +1364 -0
- memos/graph_dbs/neo4j.py +333 -124
- memos/graph_dbs/neo4j_community.py +300 -0
- memos/llms/base.py +9 -0
- memos/llms/deepseek.py +54 -0
- memos/llms/factory.py +10 -1
- memos/llms/hf.py +170 -13
- memos/llms/hf_singleton.py +114 -0
- memos/llms/ollama.py +4 -0
- memos/llms/openai.py +67 -1
- memos/llms/qwen.py +63 -0
- memos/llms/vllm.py +153 -0
- memos/log.py +1 -1
- memos/mem_cube/general.py +77 -16
- memos/mem_cube/utils.py +109 -0
- memos/mem_os/core.py +251 -51
- memos/mem_os/main.py +94 -12
- memos/mem_os/product.py +1220 -43
- memos/mem_os/utils/default_config.py +352 -0
- memos/mem_os/utils/format_utils.py +1401 -0
- memos/mem_reader/simple_struct.py +18 -10
- memos/mem_scheduler/base_scheduler.py +441 -40
- memos/mem_scheduler/general_scheduler.py +249 -248
- memos/mem_scheduler/modules/base.py +14 -5
- memos/mem_scheduler/modules/dispatcher.py +67 -4
- memos/mem_scheduler/modules/misc.py +104 -0
- memos/mem_scheduler/modules/monitor.py +240 -50
- memos/mem_scheduler/modules/rabbitmq_service.py +319 -0
- memos/mem_scheduler/modules/redis_service.py +32 -22
- memos/mem_scheduler/modules/retriever.py +167 -23
- memos/mem_scheduler/modules/scheduler_logger.py +255 -0
- memos/mem_scheduler/mos_for_test_scheduler.py +140 -0
- memos/mem_scheduler/schemas/__init__.py +0 -0
- memos/mem_scheduler/schemas/general_schemas.py +43 -0
- memos/mem_scheduler/{modules/schemas.py → schemas/message_schemas.py} +63 -61
- memos/mem_scheduler/schemas/monitor_schemas.py +329 -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 +61 -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/persistent_user_manager.py +260 -0
- memos/mem_user/user_manager.py +4 -4
- memos/memories/activation/item.py +29 -0
- memos/memories/activation/kv.py +10 -3
- memos/memories/activation/vllmkv.py +219 -0
- memos/memories/factory.py +2 -0
- memos/memories/textual/base.py +1 -1
- memos/memories/textual/general.py +43 -97
- memos/memories/textual/item.py +5 -33
- memos/memories/textual/tree.py +22 -12
- memos/memories/textual/tree_text_memory/organize/conflict.py +9 -5
- memos/memories/textual/tree_text_memory/organize/manager.py +26 -18
- memos/memories/textual/tree_text_memory/organize/redundancy.py +25 -44
- memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +50 -48
- memos/memories/textual/tree_text_memory/organize/reorganizer.py +81 -56
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py +6 -3
- memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
- memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
- memos/memories/textual/tree_text_memory/retrieve/retrieval_mid_structs.py +2 -0
- memos/memories/textual/tree_text_memory/retrieve/searcher.py +52 -28
- memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +42 -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/notification_service.py +44 -0
- memos/memos_tools/notification_utils.py +96 -0
- memos/parsers/markitdown.py +8 -2
- memos/settings.py +3 -1
- memos/templates/mem_reader_prompts.py +66 -23
- memos/templates/mem_scheduler_prompts.py +126 -43
- memos/templates/mos_prompts.py +87 -0
- memos/templates/tree_reorganize_prompts.py +85 -30
- memos/vec_dbs/base.py +12 -0
- memos/vec_dbs/qdrant.py +46 -20
- memoryos-0.2.0.dist-info/RECORD +0 -128
- memos/mem_scheduler/utils.py +0 -26
- {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/LICENSE +0 -0
- {memoryos-0.2.0.dist-info → memoryos-0.2.2.dist-info}/WHEEL +0 -0
memos/graph_dbs/neo4j.py
CHANGED
|
@@ -3,9 +3,8 @@ import time
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Any, Literal
|
|
5
5
|
|
|
6
|
-
from neo4j import GraphDatabase
|
|
7
|
-
|
|
8
6
|
from memos.configs.graph_db import Neo4jGraphDBConfig
|
|
7
|
+
from memos.dependency import require_python_package
|
|
9
8
|
from memos.graph_dbs.base import BaseGraphDB
|
|
10
9
|
from memos.log import get_logger
|
|
11
10
|
|
|
@@ -13,17 +12,6 @@ from memos.log import get_logger
|
|
|
13
12
|
logger = get_logger(__name__)
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
def _parse_node(node_data: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
-
node = node_data.copy()
|
|
18
|
-
|
|
19
|
-
# Convert Neo4j datetime to string
|
|
20
|
-
for time_field in ("created_at", "updated_at"):
|
|
21
|
-
if time_field in node and hasattr(node[time_field], "isoformat"):
|
|
22
|
-
node[time_field] = node[time_field].isoformat()
|
|
23
|
-
|
|
24
|
-
return {"id": node.pop("id"), "memory": node.pop("memory", ""), "metadata": node}
|
|
25
|
-
|
|
26
|
-
|
|
27
15
|
def _compose_node(item: dict[str, Any]) -> tuple[str, str, dict[str, Any]]:
|
|
28
16
|
node_id = item["id"]
|
|
29
17
|
memory = item["memory"]
|
|
@@ -55,11 +43,37 @@ def _prepare_node_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
|
|
55
43
|
class Neo4jGraphDB(BaseGraphDB):
|
|
56
44
|
"""Neo4j-based implementation of a graph memory store."""
|
|
57
45
|
|
|
46
|
+
@require_python_package(
|
|
47
|
+
import_name="neo4j",
|
|
48
|
+
install_command="pip install neo4j",
|
|
49
|
+
install_link="https://neo4j.com/docs/python-manual/current/install/",
|
|
50
|
+
)
|
|
58
51
|
def __init__(self, config: Neo4jGraphDBConfig):
|
|
52
|
+
"""Neo4j-based implementation of a graph memory store.
|
|
53
|
+
|
|
54
|
+
Tenant Modes:
|
|
55
|
+
- use_multi_db = True:
|
|
56
|
+
Dedicated Database Mode (Multi-Database Multi-Tenant).
|
|
57
|
+
Each tenant or logical scope uses a separate Neo4j database.
|
|
58
|
+
`db_name` is the specific tenant database.
|
|
59
|
+
`user_name` can be None (optional).
|
|
60
|
+
|
|
61
|
+
- use_multi_db = False:
|
|
62
|
+
Shared Database Multi-Tenant Mode.
|
|
63
|
+
All tenants share a single Neo4j database.
|
|
64
|
+
`db_name` is the shared database.
|
|
65
|
+
`user_name` is required to isolate each tenant's data at the node level.
|
|
66
|
+
All node queries will enforce `user_name` in WHERE conditions and store it in metadata,
|
|
67
|
+
but it will be removed automatically before returning to external consumers.
|
|
68
|
+
"""
|
|
69
|
+
from neo4j import GraphDatabase
|
|
70
|
+
|
|
59
71
|
self.config = config
|
|
60
72
|
self.driver = GraphDatabase.driver(config.uri, auth=(config.user, config.password))
|
|
61
73
|
self.db_name = config.db_name
|
|
74
|
+
self.user_name = config.user_name
|
|
62
75
|
|
|
76
|
+
self.system_db_name = "system" if config.use_multi_db else config.db_name
|
|
63
77
|
if config.auto_create:
|
|
64
78
|
self._ensure_database_exists()
|
|
65
79
|
|
|
@@ -86,21 +100,38 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
86
100
|
query = """
|
|
87
101
|
MATCH (n:Memory)
|
|
88
102
|
WHERE n.memory_type = $memory_type
|
|
89
|
-
RETURN COUNT(n) AS count
|
|
90
103
|
"""
|
|
104
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
105
|
+
query += "\nAND n.user_name = $user_name"
|
|
106
|
+
query += "\nRETURN COUNT(n) AS count"
|
|
91
107
|
with self.driver.session(database=self.db_name) as session:
|
|
92
|
-
result = session.run(
|
|
108
|
+
result = session.run(
|
|
109
|
+
query,
|
|
110
|
+
{
|
|
111
|
+
"memory_type": memory_type,
|
|
112
|
+
"user_name": self.config.user_name if self.config.user_name else None,
|
|
113
|
+
},
|
|
114
|
+
)
|
|
93
115
|
return result.single()["count"]
|
|
94
116
|
|
|
95
|
-
def
|
|
117
|
+
def node_not_exist(self, scope: str) -> int:
|
|
96
118
|
query = """
|
|
97
119
|
MATCH (n:Memory)
|
|
98
120
|
WHERE n.memory_type = $scope
|
|
99
|
-
RETURN count(n) AS count
|
|
100
121
|
"""
|
|
122
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
123
|
+
query += "\nAND n.user_name = $user_name"
|
|
124
|
+
query += "\nRETURN n LIMIT 1"
|
|
125
|
+
|
|
101
126
|
with self.driver.session(database=self.db_name) as session:
|
|
102
|
-
result = session.run(
|
|
103
|
-
|
|
127
|
+
result = session.run(
|
|
128
|
+
query,
|
|
129
|
+
{
|
|
130
|
+
"scope": scope,
|
|
131
|
+
"user_name": self.config.user_name if self.config.user_name else None,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
return result.single() is None
|
|
104
135
|
|
|
105
136
|
def remove_oldest_memory(self, memory_type: str, keep_latest: int) -> None:
|
|
106
137
|
"""
|
|
@@ -113,14 +144,22 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
113
144
|
query = f"""
|
|
114
145
|
MATCH (n:Memory)
|
|
115
146
|
WHERE n.memory_type = '{memory_type}'
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
147
|
+
"""
|
|
148
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
149
|
+
query += f"\nAND n.user_name = '{self.config.user_name}'"
|
|
150
|
+
|
|
151
|
+
query += f"""
|
|
152
|
+
WITH n ORDER BY n.updated_at DESC
|
|
153
|
+
SKIP {keep_latest}
|
|
154
|
+
DETACH DELETE n
|
|
119
155
|
"""
|
|
120
156
|
with self.driver.session(database=self.db_name) as session:
|
|
121
157
|
session.run(query)
|
|
122
158
|
|
|
123
159
|
def add_node(self, id: str, memory: str, metadata: dict[str, Any]) -> None:
|
|
160
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
161
|
+
metadata["user_name"] = self.config.user_name
|
|
162
|
+
|
|
124
163
|
# Safely process metadata
|
|
125
164
|
metadata = _prepare_node_metadata(metadata)
|
|
126
165
|
|
|
@@ -162,10 +201,14 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
162
201
|
set_clauses.append("n += $fields") # Merge remaining fields
|
|
163
202
|
set_clause_str = ",\n ".join(set_clauses)
|
|
164
203
|
|
|
165
|
-
query =
|
|
166
|
-
MATCH (n:Memory {
|
|
167
|
-
SET {set_clause_str}
|
|
204
|
+
query = """
|
|
205
|
+
MATCH (n:Memory {id: $id})
|
|
168
206
|
"""
|
|
207
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
208
|
+
query += "\nWHERE n.user_name = $user_name"
|
|
209
|
+
params["user_name"] = self.config.user_name
|
|
210
|
+
|
|
211
|
+
query += f"\nSET {set_clause_str}"
|
|
169
212
|
|
|
170
213
|
with self.driver.session(database=self.db_name) as session:
|
|
171
214
|
session.run(query, **params)
|
|
@@ -176,8 +219,17 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
176
219
|
Args:
|
|
177
220
|
id: Node identifier to delete.
|
|
178
221
|
"""
|
|
222
|
+
query = "MATCH (n:Memory {id: $id})"
|
|
223
|
+
|
|
224
|
+
params = {"id": id}
|
|
225
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
226
|
+
query += " WHERE n.user_name = $user_name"
|
|
227
|
+
params["user_name"] = self.config.user_name
|
|
228
|
+
|
|
229
|
+
query += " DETACH DELETE n"
|
|
230
|
+
|
|
179
231
|
with self.driver.session(database=self.db_name) as session:
|
|
180
|
-
session.run(
|
|
232
|
+
session.run(query, **params)
|
|
181
233
|
|
|
182
234
|
# Edge (Relationship) Management
|
|
183
235
|
def add_edge(self, source_id: str, target_id: str, type: str) -> None:
|
|
@@ -188,15 +240,21 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
188
240
|
target_id: ID of the target node.
|
|
189
241
|
type: Relationship type (e.g., 'RELATE_TO', 'PARENT').
|
|
190
242
|
"""
|
|
243
|
+
query = """
|
|
244
|
+
MATCH (a:Memory {id: $source_id})
|
|
245
|
+
MATCH (b:Memory {id: $target_id})
|
|
246
|
+
"""
|
|
247
|
+
params = {"source_id": source_id, "target_id": target_id}
|
|
248
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
249
|
+
query += """
|
|
250
|
+
WHERE a.user_name = $user_name AND b.user_name = $user_name
|
|
251
|
+
"""
|
|
252
|
+
params["user_name"] = self.config.user_name
|
|
253
|
+
|
|
254
|
+
query += f"\nMERGE (a)-[:{type}]->(b)"
|
|
255
|
+
|
|
191
256
|
with self.driver.session(database=self.db_name) as session:
|
|
192
|
-
session.run(
|
|
193
|
-
f"""
|
|
194
|
-
MATCH (a:Memory {{id: $source_id}})
|
|
195
|
-
MATCH (b:Memory {{id: $target_id}})
|
|
196
|
-
MERGE (a)-[:{type}]->(b)
|
|
197
|
-
""",
|
|
198
|
-
{"source_id": source_id, "target_id": target_id},
|
|
199
|
-
)
|
|
257
|
+
session.run(query, params)
|
|
200
258
|
|
|
201
259
|
def delete_edge(self, source_id: str, target_id: str, type: str) -> None:
|
|
202
260
|
"""
|
|
@@ -206,12 +264,21 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
206
264
|
target_id: ID of the target node.
|
|
207
265
|
type: Relationship type to remove.
|
|
208
266
|
"""
|
|
267
|
+
query = f"""
|
|
268
|
+
MATCH (a:Memory {{id: $source}})
|
|
269
|
+
-[r:{type}]->
|
|
270
|
+
(b:Memory {{id: $target}})
|
|
271
|
+
"""
|
|
272
|
+
params = {"source": source_id, "target": target_id}
|
|
273
|
+
|
|
274
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
275
|
+
query += "\nWHERE a.user_name = $user_name AND b.user_name = $user_name"
|
|
276
|
+
params["user_name"] = self.config.user_name
|
|
277
|
+
|
|
278
|
+
query += "\nDELETE r"
|
|
279
|
+
|
|
209
280
|
with self.driver.session(database=self.db_name) as session:
|
|
210
|
-
session.run(
|
|
211
|
-
f"MATCH (a:Memory {{id: $source}})-[r:{type}]->(b:Memory {{id: $target}})\nDELETE r",
|
|
212
|
-
source=source_id,
|
|
213
|
-
target=target_id,
|
|
214
|
-
)
|
|
281
|
+
session.run(query, params)
|
|
215
282
|
|
|
216
283
|
def edge_exists(
|
|
217
284
|
self, source_id: str, target_id: str, type: str = "ANY", direction: str = "OUTGOING"
|
|
@@ -241,14 +308,18 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
241
308
|
raise ValueError(
|
|
242
309
|
f"Invalid direction: {direction}. Must be 'OUTGOING', 'INCOMING', or 'ANY'."
|
|
243
310
|
)
|
|
311
|
+
query = f"MATCH {pattern}"
|
|
312
|
+
params = {"source": source_id, "target": target_id}
|
|
313
|
+
|
|
314
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
315
|
+
query += "\nWHERE a.user_name = $user_name AND b.user_name = $user_name"
|
|
316
|
+
params["user_name"] = self.config.user_name
|
|
317
|
+
|
|
318
|
+
query += "\nRETURN r"
|
|
244
319
|
|
|
245
320
|
# Run the Cypher query
|
|
246
321
|
with self.driver.session(database=self.db_name) as session:
|
|
247
|
-
result = session.run(
|
|
248
|
-
f"MATCH {pattern} RETURN r",
|
|
249
|
-
source=source_id,
|
|
250
|
-
target=target_id,
|
|
251
|
-
)
|
|
322
|
+
result = session.run(query, params)
|
|
252
323
|
return result.single() is not None
|
|
253
324
|
|
|
254
325
|
# Graph Query & Reasoning
|
|
@@ -260,10 +331,17 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
260
331
|
Returns:
|
|
261
332
|
Dictionary of node fields, or None if not found.
|
|
262
333
|
"""
|
|
334
|
+
where_user = ""
|
|
335
|
+
params = {"id": id}
|
|
336
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
337
|
+
where_user = " AND n.user_name = $user_name"
|
|
338
|
+
params["user_name"] = self.config.user_name
|
|
339
|
+
|
|
340
|
+
query = f"MATCH (n:Memory) WHERE n.id = $id {where_user} RETURN n"
|
|
341
|
+
|
|
263
342
|
with self.driver.session(database=self.db_name) as session:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return _parse_node(dict(record["n"])) if record else None
|
|
343
|
+
record = session.run(query, params).single()
|
|
344
|
+
return self._parse_node(dict(record["n"])) if record else None
|
|
267
345
|
|
|
268
346
|
def get_nodes(self, ids: list[str]) -> list[dict[str, Any]]:
|
|
269
347
|
"""
|
|
@@ -280,10 +358,18 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
280
358
|
if not ids:
|
|
281
359
|
return []
|
|
282
360
|
|
|
283
|
-
|
|
361
|
+
where_user = ""
|
|
362
|
+
params = {"ids": ids}
|
|
363
|
+
|
|
364
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
365
|
+
where_user = " AND n.user_name = $user_name"
|
|
366
|
+
params["user_name"] = self.config.user_name
|
|
367
|
+
|
|
368
|
+
query = f"MATCH (n:Memory) WHERE n.id IN $ids{where_user} RETURN n"
|
|
369
|
+
|
|
284
370
|
with self.driver.session(database=self.db_name) as session:
|
|
285
|
-
results = session.run(query,
|
|
286
|
-
return [_parse_node(dict(record["n"])) for record in results]
|
|
371
|
+
results = session.run(query, params)
|
|
372
|
+
return [self._parse_node(dict(record["n"])) for record in results]
|
|
287
373
|
|
|
288
374
|
def get_edges(self, id: str, type: str = "ANY", direction: str = "ANY") -> list[dict[str, str]]:
|
|
289
375
|
"""
|
|
@@ -317,14 +403,20 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
317
403
|
else:
|
|
318
404
|
raise ValueError("Invalid direction. Must be 'OUTGOING', 'INCOMING', or 'ANY'.")
|
|
319
405
|
|
|
406
|
+
params = {"id": id}
|
|
407
|
+
|
|
408
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
409
|
+
where_clause += " AND a.user_name = $user_name AND b.user_name = $user_name"
|
|
410
|
+
params["user_name"] = self.config.user_name
|
|
411
|
+
|
|
320
412
|
query = f"""
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
413
|
+
MATCH {pattern}
|
|
414
|
+
WHERE {where_clause}
|
|
415
|
+
RETURN a.id AS from_id, b.id AS to_id, type(r) AS type
|
|
416
|
+
"""
|
|
325
417
|
|
|
326
418
|
with self.driver.session(database=self.db_name) as session:
|
|
327
|
-
result = session.run(query,
|
|
419
|
+
result = session.run(query, params)
|
|
328
420
|
edges = []
|
|
329
421
|
for record in result:
|
|
330
422
|
edges.append(
|
|
@@ -365,19 +457,7 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
365
457
|
Returns:
|
|
366
458
|
List of dicts with node details and overlap count.
|
|
367
459
|
"""
|
|
368
|
-
|
|
369
|
-
MATCH (n:Memory)
|
|
370
|
-
WHERE NOT n.id IN $exclude_ids
|
|
371
|
-
AND n.status = 'activated'
|
|
372
|
-
AND n.type <> 'reasoning'
|
|
373
|
-
AND n.memory_type <> 'WorkingMemory'
|
|
374
|
-
WITH n, [tag IN n.tags WHERE tag IN $tags] AS overlap_tags
|
|
375
|
-
WHERE size(overlap_tags) >= $min_overlap
|
|
376
|
-
RETURN n, size(overlap_tags) AS overlap_count
|
|
377
|
-
ORDER BY overlap_count DESC
|
|
378
|
-
LIMIT $top_k
|
|
379
|
-
"""
|
|
380
|
-
|
|
460
|
+
where_user = ""
|
|
381
461
|
params = {
|
|
382
462
|
"tags": tags,
|
|
383
463
|
"exclude_ids": exclude_ids,
|
|
@@ -385,18 +465,47 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
385
465
|
"top_k": top_k,
|
|
386
466
|
}
|
|
387
467
|
|
|
468
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
469
|
+
where_user = "AND n.user_name = $user_name"
|
|
470
|
+
params["user_name"] = self.config.user_name
|
|
471
|
+
|
|
472
|
+
query = f"""
|
|
473
|
+
MATCH (n:Memory)
|
|
474
|
+
WHERE NOT n.id IN $exclude_ids
|
|
475
|
+
AND n.status = 'activated'
|
|
476
|
+
AND n.type <> 'reasoning'
|
|
477
|
+
AND n.memory_type <> 'WorkingMemory'
|
|
478
|
+
{where_user}
|
|
479
|
+
WITH n, [tag IN n.tags WHERE tag IN $tags] AS overlap_tags
|
|
480
|
+
WHERE size(overlap_tags) >= $min_overlap
|
|
481
|
+
RETURN n, size(overlap_tags) AS overlap_count
|
|
482
|
+
ORDER BY overlap_count DESC
|
|
483
|
+
LIMIT $top_k
|
|
484
|
+
"""
|
|
485
|
+
|
|
388
486
|
with self.driver.session(database=self.db_name) as session:
|
|
389
487
|
result = session.run(query, params)
|
|
390
|
-
return [_parse_node(dict(record["n"])) for record in result]
|
|
488
|
+
return [self._parse_node(dict(record["n"])) for record in result]
|
|
489
|
+
|
|
490
|
+
def get_children_with_embeddings(self, id: str) -> list[dict[str, Any]]:
|
|
491
|
+
where_user = ""
|
|
492
|
+
params = {"id": id}
|
|
493
|
+
|
|
494
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
495
|
+
where_user = "AND p.user_name = $user_name AND c.user_name = $user_name"
|
|
496
|
+
params["user_name"] = self.config.user_name
|
|
497
|
+
|
|
498
|
+
query = f"""
|
|
499
|
+
MATCH (p:Memory)-[:PARENT]->(c:Memory)
|
|
500
|
+
WHERE p.id = $id {where_user}
|
|
501
|
+
RETURN c.id AS id, c.embedding AS embedding, c.memory AS memory
|
|
502
|
+
"""
|
|
391
503
|
|
|
392
|
-
def get_children_with_embeddings(self, id: str) -> list[str]:
|
|
393
|
-
query = """
|
|
394
|
-
MATCH (p:Memory)-[:PARENT]->(c:Memory)
|
|
395
|
-
WHERE p.id = $id
|
|
396
|
-
RETURN c.id AS id, c.embedding AS embedding, c.memory AS memory
|
|
397
|
-
"""
|
|
398
504
|
with self.driver.session(database=self.db_name) as session:
|
|
399
|
-
|
|
505
|
+
result = session.run(query, params)
|
|
506
|
+
return [
|
|
507
|
+
{"id": r["id"], "embedding": r["embedding"], "memory": r["memory"]} for r in result
|
|
508
|
+
]
|
|
400
509
|
|
|
401
510
|
def get_path(self, source_id: str, target_id: str, max_depth: int = 3) -> list[str]:
|
|
402
511
|
"""
|
|
@@ -427,16 +536,29 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
427
536
|
}
|
|
428
537
|
"""
|
|
429
538
|
with self.driver.session(database=self.db_name) as session:
|
|
430
|
-
|
|
539
|
+
params = {"center_id": center_id}
|
|
540
|
+
center_user_clause = ""
|
|
541
|
+
neighbor_user_clause = ""
|
|
542
|
+
|
|
543
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
544
|
+
center_user_clause = " AND center.user_name = $user_name"
|
|
545
|
+
neighbor_user_clause = " WHERE neighbor.user_name = $user_name"
|
|
546
|
+
params["user_name"] = self.config.user_name
|
|
547
|
+
status_clause = f" AND center.status = '{center_status}'" if center_status else ""
|
|
548
|
+
|
|
431
549
|
query = f"""
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
550
|
+
MATCH (center:Memory)
|
|
551
|
+
WHERE center.id = $center_id{status_clause}{center_user_clause}
|
|
552
|
+
|
|
553
|
+
OPTIONAL MATCH (center)-[r*1..{depth}]-(neighbor:Memory)
|
|
554
|
+
{neighbor_user_clause}
|
|
555
|
+
|
|
556
|
+
WITH collect(DISTINCT center) AS centers,
|
|
557
|
+
collect(DISTINCT neighbor) AS neighbors,
|
|
558
|
+
collect(DISTINCT r) AS rels
|
|
559
|
+
RETURN centers, neighbors, rels
|
|
438
560
|
"""
|
|
439
|
-
record = session.run(query,
|
|
561
|
+
record = session.run(query, params).single()
|
|
440
562
|
|
|
441
563
|
if not record:
|
|
442
564
|
return {"core_node": None, "neighbors": [], "edges": []}
|
|
@@ -445,8 +567,8 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
445
567
|
if not centers or centers[0] is None:
|
|
446
568
|
return {"core_node": None, "neighbors": [], "edges": []}
|
|
447
569
|
|
|
448
|
-
core_node = _parse_node(dict(centers[0]))
|
|
449
|
-
neighbors = [_parse_node(dict(n)) for n in record["neighbors"] if n]
|
|
570
|
+
core_node = self._parse_node(dict(centers[0]))
|
|
571
|
+
neighbors = [self._parse_node(dict(n)) for n in record["neighbors"] if n]
|
|
450
572
|
edges = []
|
|
451
573
|
for rel_chain in record["rels"]:
|
|
452
574
|
for rel in rel_chain:
|
|
@@ -508,6 +630,8 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
508
630
|
where_clauses.append("node.memory_type = $scope")
|
|
509
631
|
if status:
|
|
510
632
|
where_clauses.append("node.status = $status")
|
|
633
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
634
|
+
where_clauses.append("node.user_name = $user_name")
|
|
511
635
|
|
|
512
636
|
where_clause = ""
|
|
513
637
|
if where_clauses:
|
|
@@ -525,6 +649,8 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
525
649
|
parameters["scope"] = scope
|
|
526
650
|
if status:
|
|
527
651
|
parameters["status"] = status
|
|
652
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
653
|
+
parameters["user_name"] = self.config.user_name
|
|
528
654
|
|
|
529
655
|
with self.driver.session(database=self.db_name) as session:
|
|
530
656
|
result = session.run(query, parameters)
|
|
@@ -592,6 +718,10 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
592
718
|
else:
|
|
593
719
|
raise ValueError(f"Unsupported operator: {op}")
|
|
594
720
|
|
|
721
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
722
|
+
where_clauses.append("n.user_name = $user_name")
|
|
723
|
+
params["user_name"] = self.config.user_name
|
|
724
|
+
|
|
595
725
|
where_str = " AND ".join(where_clauses)
|
|
596
726
|
query = f"MATCH (n:Memory) WHERE {where_str} RETURN n.id AS id"
|
|
597
727
|
|
|
@@ -620,6 +750,20 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
620
750
|
if not group_fields:
|
|
621
751
|
raise ValueError("group_fields cannot be empty")
|
|
622
752
|
|
|
753
|
+
final_params = params.copy() if params else {}
|
|
754
|
+
|
|
755
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
756
|
+
user_clause = "n.user_name = $user_name"
|
|
757
|
+
final_params["user_name"] = self.config.user_name
|
|
758
|
+
if where_clause:
|
|
759
|
+
where_clause = where_clause.strip()
|
|
760
|
+
if where_clause.upper().startswith("WHERE"):
|
|
761
|
+
where_clause += f" AND {user_clause}"
|
|
762
|
+
else:
|
|
763
|
+
where_clause = f"WHERE {where_clause} AND {user_clause}"
|
|
764
|
+
else:
|
|
765
|
+
where_clause = f"WHERE {user_clause}"
|
|
766
|
+
|
|
623
767
|
# Force RETURN field AS field to guarantee key match
|
|
624
768
|
group_fields_cypher = ", ".join([f"n.{field} AS {field}" for field in group_fields])
|
|
625
769
|
|
|
@@ -630,7 +774,7 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
630
774
|
"""
|
|
631
775
|
|
|
632
776
|
with self.driver.session(database=self.db_name) as session:
|
|
633
|
-
result = session.run(query,
|
|
777
|
+
result = session.run(query, final_params)
|
|
634
778
|
return [
|
|
635
779
|
{**{field: record[field] for field in group_fields}, "count": record["count"]}
|
|
636
780
|
for record in result
|
|
@@ -669,17 +813,16 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
669
813
|
Clear the entire graph if the target database exists.
|
|
670
814
|
"""
|
|
671
815
|
try:
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return
|
|
816
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
817
|
+
query = "MATCH (n:Memory) WHERE n.user_name = $user_name DETACH DELETE n"
|
|
818
|
+
params = {"user_name": self.config.user_name}
|
|
819
|
+
else:
|
|
820
|
+
query = "MATCH (n) DETACH DELETE n"
|
|
821
|
+
params = {}
|
|
679
822
|
|
|
680
823
|
# Step 2: Clear the graph in that database
|
|
681
824
|
with self.driver.session(database=self.db_name) as session:
|
|
682
|
-
session.run(
|
|
825
|
+
session.run(query, params)
|
|
683
826
|
logger.info(f"Cleared all nodes from database '{self.db_name}'.")
|
|
684
827
|
|
|
685
828
|
except Exception as e:
|
|
@@ -698,14 +841,22 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
698
841
|
"""
|
|
699
842
|
with self.driver.session(database=self.db_name) as session:
|
|
700
843
|
# Export nodes
|
|
701
|
-
|
|
702
|
-
|
|
844
|
+
node_query = "MATCH (n:Memory)"
|
|
845
|
+
edge_query = "MATCH (a:Memory)-[r]->(b:Memory)"
|
|
846
|
+
params = {}
|
|
847
|
+
|
|
848
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
849
|
+
node_query += " WHERE n.user_name = $user_name"
|
|
850
|
+
edge_query += " WHERE a.user_name = $user_name AND b.user_name = $user_name"
|
|
851
|
+
params["user_name"] = self.config.user_name
|
|
852
|
+
|
|
853
|
+
node_result = session.run(f"{node_query} RETURN n", params)
|
|
854
|
+
nodes = [self._parse_node(dict(record["n"])) for record in node_result]
|
|
703
855
|
|
|
704
856
|
# Export edges
|
|
705
|
-
edge_result = session.run(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
""")
|
|
857
|
+
edge_result = session.run(
|
|
858
|
+
f"{edge_query} RETURN a.id AS source, b.id AS target, type(r) AS type", params
|
|
859
|
+
)
|
|
709
860
|
edges = [
|
|
710
861
|
{"source": record["source"], "target": record["target"], "type": record["type"]}
|
|
711
862
|
for record in edge_result
|
|
@@ -724,6 +875,9 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
724
875
|
for node in data.get("nodes", []):
|
|
725
876
|
id, memory, metadata = _compose_node(node)
|
|
726
877
|
|
|
878
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
879
|
+
metadata["user_name"] = self.config.user_name
|
|
880
|
+
|
|
727
881
|
metadata = _prepare_node_metadata(metadata)
|
|
728
882
|
|
|
729
883
|
# Merge node and set metadata
|
|
@@ -766,18 +920,25 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
766
920
|
Returns:
|
|
767
921
|
list[dict]: Full list of memory items under this scope.
|
|
768
922
|
"""
|
|
769
|
-
if scope not in {"WorkingMemory", "LongTermMemory", "UserMemory"}:
|
|
923
|
+
if scope not in {"WorkingMemory", "LongTermMemory", "UserMemory", "OuterMemory"}:
|
|
770
924
|
raise ValueError(f"Unsupported memory type scope: {scope}")
|
|
771
925
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
926
|
+
where_clause = "WHERE n.memory_type = $scope"
|
|
927
|
+
params = {"scope": scope}
|
|
928
|
+
|
|
929
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
930
|
+
where_clause += " AND n.user_name = $user_name"
|
|
931
|
+
params["user_name"] = self.config.user_name
|
|
932
|
+
|
|
933
|
+
query = f"""
|
|
934
|
+
MATCH (n:Memory)
|
|
935
|
+
{where_clause}
|
|
936
|
+
RETURN n
|
|
937
|
+
"""
|
|
777
938
|
|
|
778
939
|
with self.driver.session(database=self.db_name) as session:
|
|
779
|
-
results = session.run(query,
|
|
780
|
-
return [_parse_node(dict(record["n"])) for record in results]
|
|
940
|
+
results = session.run(query, params)
|
|
941
|
+
return [self._parse_node(dict(record["n"])) for record in results]
|
|
781
942
|
|
|
782
943
|
def get_structure_optimization_candidates(self, scope: str) -> list[dict]:
|
|
783
944
|
"""
|
|
@@ -785,37 +946,62 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
785
946
|
- Isolated nodes, nodes with empty background, or nodes with exactly one child.
|
|
786
947
|
- Plus: the child of any parent node that has exactly one child.
|
|
787
948
|
"""
|
|
788
|
-
|
|
789
|
-
MATCH (n:Memory)
|
|
949
|
+
where_clause = """
|
|
790
950
|
WHERE n.memory_type = $scope
|
|
791
951
|
AND n.status = 'activated'
|
|
792
952
|
AND NOT ( (n)-[:PARENT]->() OR ()-[:PARENT]->(n) )
|
|
793
|
-
|
|
794
|
-
|
|
953
|
+
"""
|
|
954
|
+
params = {"scope": scope}
|
|
955
|
+
|
|
956
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
957
|
+
where_clause += " AND n.user_name = $user_name"
|
|
958
|
+
params["user_name"] = self.config.user_name
|
|
959
|
+
|
|
960
|
+
query = f"""
|
|
961
|
+
MATCH (n:Memory)
|
|
962
|
+
{where_clause}
|
|
963
|
+
RETURN n.id AS id, n AS node
|
|
964
|
+
"""
|
|
795
965
|
|
|
796
966
|
with self.driver.session(database=self.db_name) as session:
|
|
797
|
-
results = session.run(query,
|
|
798
|
-
return [
|
|
967
|
+
results = session.run(query, params)
|
|
968
|
+
return [
|
|
969
|
+
self._parse_node({"id": record["id"], **dict(record["node"])}) for record in results
|
|
970
|
+
]
|
|
799
971
|
|
|
800
972
|
def drop_database(self) -> None:
|
|
801
973
|
"""
|
|
802
974
|
Permanently delete the entire database this instance is using.
|
|
803
975
|
WARNING: This operation is destructive and cannot be undone.
|
|
804
976
|
"""
|
|
805
|
-
if self.
|
|
806
|
-
|
|
977
|
+
if self.config.use_multi_db:
|
|
978
|
+
if self.db_name in ("system", "neo4j"):
|
|
979
|
+
raise ValueError(f"Refusing to drop protected database: {self.db_name}")
|
|
807
980
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
981
|
+
with self.driver.session(database=self.system_db_name) as session:
|
|
982
|
+
session.run(f"DROP DATABASE {self.db_name} IF EXISTS")
|
|
983
|
+
print(f"Database '{self.db_name}' has been dropped.")
|
|
984
|
+
else:
|
|
985
|
+
raise ValueError(
|
|
986
|
+
f"Refusing to drop protected database: {self.db_name} in "
|
|
987
|
+
f"Shared Database Multi-Tenant mode"
|
|
988
|
+
)
|
|
811
989
|
|
|
812
990
|
def _ensure_database_exists(self):
|
|
813
|
-
|
|
814
|
-
|
|
991
|
+
from neo4j.exceptions import ClientError
|
|
992
|
+
|
|
993
|
+
try:
|
|
994
|
+
with self.driver.session(database="system") as session:
|
|
995
|
+
session.run(f"CREATE DATABASE `{self.db_name}` IF NOT EXISTS")
|
|
996
|
+
except ClientError as e:
|
|
997
|
+
if "ExistingDatabaseFound" in str(e):
|
|
998
|
+
pass # Ignore, database already exists
|
|
999
|
+
else:
|
|
1000
|
+
raise
|
|
815
1001
|
|
|
816
1002
|
# Wait until the database is available
|
|
817
1003
|
for _ in range(10):
|
|
818
|
-
with self.driver.session(database=
|
|
1004
|
+
with self.driver.session(database=self.system_db_name) as session:
|
|
819
1005
|
result = session.run(
|
|
820
1006
|
"SHOW DATABASES YIELD name, currentStatus RETURN name, currentStatus"
|
|
821
1007
|
)
|
|
@@ -857,7 +1043,10 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
857
1043
|
|
|
858
1044
|
def _create_basic_property_indexes(self) -> None:
|
|
859
1045
|
"""
|
|
860
|
-
Create standard B-tree indexes on memory_type, created_at,
|
|
1046
|
+
Create standard B-tree indexes on memory_type, created_at,
|
|
1047
|
+
and updated_at fields.
|
|
1048
|
+
Create standard B-tree indexes on user_name when use Shared Database
|
|
1049
|
+
Multi-Tenant Mode
|
|
861
1050
|
"""
|
|
862
1051
|
try:
|
|
863
1052
|
with self.driver.session(database=self.db_name) as session:
|
|
@@ -878,6 +1067,15 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
878
1067
|
FOR (n:Memory) ON (n.updated_at)
|
|
879
1068
|
""")
|
|
880
1069
|
logger.debug("Index 'memory_updated_at_index' ensured.")
|
|
1070
|
+
|
|
1071
|
+
if not self.config.use_multi_db and self.config.user_name:
|
|
1072
|
+
session.run(
|
|
1073
|
+
"""
|
|
1074
|
+
CREATE INDEX memory_user_name_index IF NOT EXISTS
|
|
1075
|
+
FOR (n:Memory) ON (n.user_name)
|
|
1076
|
+
"""
|
|
1077
|
+
)
|
|
1078
|
+
logger.debug("Index 'memory_user_name_index' ensured.")
|
|
881
1079
|
except Exception as e:
|
|
882
1080
|
logger.warning(f"Failed to create basic property indexes: {e}")
|
|
883
1081
|
|
|
@@ -892,3 +1090,14 @@ class Neo4jGraphDB(BaseGraphDB):
|
|
|
892
1090
|
if record["name"] == index_name:
|
|
893
1091
|
return True
|
|
894
1092
|
return False
|
|
1093
|
+
|
|
1094
|
+
def _parse_node(self, node_data: dict[str, Any]) -> dict[str, Any]:
|
|
1095
|
+
node = node_data.copy()
|
|
1096
|
+
|
|
1097
|
+
# Convert Neo4j datetime to string
|
|
1098
|
+
for time_field in ("created_at", "updated_at"):
|
|
1099
|
+
if time_field in node and hasattr(node[time_field], "isoformat"):
|
|
1100
|
+
node[time_field] = node[time_field].isoformat()
|
|
1101
|
+
node.pop("user_name", None)
|
|
1102
|
+
|
|
1103
|
+
return {"id": node.pop("id"), "memory": node.pop("memory", ""), "metadata": node}
|