MemoryOS 0.2.0__py3-none-any.whl → 0.2.1__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 (80) hide show
  1. {memoryos-0.2.0.dist-info → memoryos-0.2.1.dist-info}/METADATA +66 -26
  2. {memoryos-0.2.0.dist-info → memoryos-0.2.1.dist-info}/RECORD +80 -56
  3. memoryos-0.2.1.dist-info/entry_points.txt +3 -0
  4. memos/__init__.py +1 -1
  5. memos/api/config.py +471 -0
  6. memos/api/exceptions.py +28 -0
  7. memos/api/mcp_serve.py +502 -0
  8. memos/api/product_api.py +35 -0
  9. memos/api/product_models.py +159 -0
  10. memos/api/routers/__init__.py +1 -0
  11. memos/api/routers/product_router.py +358 -0
  12. memos/chunkers/sentence_chunker.py +8 -2
  13. memos/cli.py +113 -0
  14. memos/configs/embedder.py +27 -0
  15. memos/configs/graph_db.py +83 -2
  16. memos/configs/llm.py +47 -0
  17. memos/configs/mem_cube.py +1 -1
  18. memos/configs/mem_scheduler.py +91 -5
  19. memos/configs/memory.py +5 -4
  20. memos/dependency.py +52 -0
  21. memos/embedders/ark.py +92 -0
  22. memos/embedders/factory.py +4 -0
  23. memos/embedders/sentence_transformer.py +8 -2
  24. memos/embedders/universal_api.py +32 -0
  25. memos/graph_dbs/base.py +2 -2
  26. memos/graph_dbs/factory.py +2 -0
  27. memos/graph_dbs/neo4j.py +331 -122
  28. memos/graph_dbs/neo4j_community.py +300 -0
  29. memos/llms/base.py +9 -0
  30. memos/llms/deepseek.py +54 -0
  31. memos/llms/factory.py +10 -1
  32. memos/llms/hf.py +170 -13
  33. memos/llms/hf_singleton.py +114 -0
  34. memos/llms/ollama.py +4 -0
  35. memos/llms/openai.py +67 -1
  36. memos/llms/qwen.py +63 -0
  37. memos/llms/vllm.py +153 -0
  38. memos/mem_cube/general.py +77 -16
  39. memos/mem_cube/utils.py +102 -0
  40. memos/mem_os/core.py +131 -41
  41. memos/mem_os/main.py +93 -11
  42. memos/mem_os/product.py +1098 -35
  43. memos/mem_os/utils/default_config.py +352 -0
  44. memos/mem_os/utils/format_utils.py +1154 -0
  45. memos/mem_reader/simple_struct.py +5 -5
  46. memos/mem_scheduler/base_scheduler.py +467 -36
  47. memos/mem_scheduler/general_scheduler.py +125 -244
  48. memos/mem_scheduler/modules/base.py +9 -0
  49. memos/mem_scheduler/modules/dispatcher.py +68 -2
  50. memos/mem_scheduler/modules/misc.py +39 -0
  51. memos/mem_scheduler/modules/monitor.py +228 -49
  52. memos/mem_scheduler/modules/rabbitmq_service.py +317 -0
  53. memos/mem_scheduler/modules/redis_service.py +32 -22
  54. memos/mem_scheduler/modules/retriever.py +250 -23
  55. memos/mem_scheduler/modules/schemas.py +189 -7
  56. memos/mem_scheduler/mos_for_test_scheduler.py +143 -0
  57. memos/mem_scheduler/utils.py +51 -2
  58. memos/mem_user/persistent_user_manager.py +260 -0
  59. memos/memories/activation/item.py +25 -0
  60. memos/memories/activation/kv.py +10 -3
  61. memos/memories/activation/vllmkv.py +219 -0
  62. memos/memories/factory.py +2 -0
  63. memos/memories/textual/general.py +7 -5
  64. memos/memories/textual/tree.py +9 -5
  65. memos/memories/textual/tree_text_memory/organize/conflict.py +5 -3
  66. memos/memories/textual/tree_text_memory/organize/manager.py +26 -18
  67. memos/memories/textual/tree_text_memory/organize/redundancy.py +25 -44
  68. memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py +11 -13
  69. memos/memories/textual/tree_text_memory/organize/reorganizer.py +73 -51
  70. memos/memories/textual/tree_text_memory/retrieve/recall.py +0 -1
  71. memos/memories/textual/tree_text_memory/retrieve/reranker.py +2 -2
  72. memos/memories/textual/tree_text_memory/retrieve/searcher.py +6 -5
  73. memos/parsers/markitdown.py +8 -2
  74. memos/templates/mem_reader_prompts.py +65 -23
  75. memos/templates/mem_scheduler_prompts.py +96 -47
  76. memos/templates/tree_reorganize_prompts.py +85 -30
  77. memos/vec_dbs/base.py +12 -0
  78. memos/vec_dbs/qdrant.py +46 -20
  79. {memoryos-0.2.0.dist-info → memoryos-0.2.1.dist-info}/LICENSE +0 -0
  80. {memoryos-0.2.0.dist-info → memoryos-0.2.1.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(query, memory_type=memory_type)
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
117
  def count_nodes(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 count(n) AS count"
125
+
101
126
  with self.driver.session(database=self.db_name) as session:
102
- result = session.run(query, {"scope": scope}).single()
103
- return result["count"]
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()["count"]
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
- WITH n ORDER BY n.updated_at DESC
117
- SKIP {keep_latest}
118
- DETACH DELETE n
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 = f"""
166
- MATCH (n:Memory {{id: $id}})
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("MATCH (n:Memory {id: $id}) DETACH DELETE n", id=id)
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
- result = session.run("MATCH (n:Memory {id: $id}) RETURN n", id=id)
265
- record = result.single()
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
- query = "MATCH (n:Memory) WHERE n.id IN $ids RETURN n"
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, {"ids": ids})
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
- MATCH {pattern}
322
- WHERE {where_clause}
323
- RETURN a.id AS from_id, b.id AS to_id, type(r) AS type
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, id=id)
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
- query = """
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
- return list(session.run(query, id=id))
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
- status_clause = f", status: '{center_status}'" if center_status else ""
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
- MATCH (center:Memory {{id: $center_id{status_clause}}})
433
- OPTIONAL MATCH (center)-[r*1..{depth}]-(neighbor:Memory)
434
- WITH collect(DISTINCT center) AS centers,
435
- collect(DISTINCT neighbor) AS neighbors,
436
- collect(DISTINCT r) AS rels
437
- RETURN centers, neighbors, rels
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, {"center_id": center_id}).single()
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, params or {})
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
- # Step 1: Check if the database exists
673
- with self.driver.session(database="system") as session:
674
- result = session.run("SHOW DATABASES YIELD name RETURN name")
675
- db_names = [record["name"] for record in result]
676
- if self.db_name not in db_names:
677
- logger.info(f"[Skip] Database '{self.db_name}' does not exist.")
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("MATCH (n) DETACH DELETE n")
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
- node_result = session.run("MATCH (n:Memory) RETURN n")
702
- nodes = [_parse_node(dict(record["n"])) for record in node_result]
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
- MATCH (a:Memory)-[r]->(b:Memory)
707
- RETURN a.id AS source, b.id AS target, type(r) AS type
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
@@ -769,15 +923,22 @@ class Neo4jGraphDB(BaseGraphDB):
769
923
  if scope not in {"WorkingMemory", "LongTermMemory", "UserMemory"}:
770
924
  raise ValueError(f"Unsupported memory type scope: {scope}")
771
925
 
772
- query = """
773
- MATCH (n:Memory)
774
- WHERE n.memory_type = $scope
775
- RETURN n
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, {"scope": scope})
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
- query = """
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
- RETURN n.id AS id, n AS node
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, {"scope": scope})
798
- return [_parse_node({"id": record["id"], **dict(record["node"])}) for record in results]
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.db_name in ("system", "neo4j"):
806
- raise ValueError(f"Refusing to drop protected database: {self.db_name}")
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
- with self.driver.session(database="system") as session:
809
- session.run(f"DROP DATABASE {self.db_name} IF EXISTS")
810
- print(f"Database '{self.db_name}' has been dropped.")
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
- with self.driver.session(database="system") as session:
814
- session.run(f"CREATE DATABASE $db_name IF NOT EXISTS", db_name=self.db_name)
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="system") as session:
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, and updated_at fields.
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}