memorygraphMCP 0.11.7__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.
- memorygraph/__init__.py +50 -0
- memorygraph/__main__.py +12 -0
- memorygraph/advanced_tools.py +509 -0
- memorygraph/analytics/__init__.py +46 -0
- memorygraph/analytics/advanced_queries.py +727 -0
- memorygraph/backends/__init__.py +21 -0
- memorygraph/backends/base.py +179 -0
- memorygraph/backends/cloud.py +75 -0
- memorygraph/backends/cloud_backend.py +858 -0
- memorygraph/backends/factory.py +577 -0
- memorygraph/backends/falkordb_backend.py +749 -0
- memorygraph/backends/falkordblite_backend.py +746 -0
- memorygraph/backends/ladybugdb_backend.py +242 -0
- memorygraph/backends/memgraph_backend.py +327 -0
- memorygraph/backends/neo4j_backend.py +298 -0
- memorygraph/backends/sqlite_fallback.py +463 -0
- memorygraph/backends/turso.py +448 -0
- memorygraph/cli.py +743 -0
- memorygraph/cloud_database.py +297 -0
- memorygraph/config.py +295 -0
- memorygraph/database.py +933 -0
- memorygraph/graph_analytics.py +631 -0
- memorygraph/integration/__init__.py +69 -0
- memorygraph/integration/context_capture.py +426 -0
- memorygraph/integration/project_analysis.py +583 -0
- memorygraph/integration/workflow_tracking.py +492 -0
- memorygraph/intelligence/__init__.py +59 -0
- memorygraph/intelligence/context_retrieval.py +447 -0
- memorygraph/intelligence/entity_extraction.py +386 -0
- memorygraph/intelligence/pattern_recognition.py +420 -0
- memorygraph/intelligence/temporal.py +374 -0
- memorygraph/migration/__init__.py +27 -0
- memorygraph/migration/manager.py +579 -0
- memorygraph/migration/models.py +142 -0
- memorygraph/migration/scripts/__init__.py +17 -0
- memorygraph/migration/scripts/bitemporal_migration.py +595 -0
- memorygraph/migration/scripts/multitenancy_migration.py +452 -0
- memorygraph/migration_tools_module.py +146 -0
- memorygraph/models.py +684 -0
- memorygraph/proactive/__init__.py +46 -0
- memorygraph/proactive/outcome_learning.py +444 -0
- memorygraph/proactive/predictive.py +410 -0
- memorygraph/proactive/session_briefing.py +399 -0
- memorygraph/relationships.py +668 -0
- memorygraph/server.py +883 -0
- memorygraph/sqlite_database.py +1876 -0
- memorygraph/tools/__init__.py +59 -0
- memorygraph/tools/activity_tools.py +262 -0
- memorygraph/tools/memory_tools.py +315 -0
- memorygraph/tools/migration_tools.py +181 -0
- memorygraph/tools/relationship_tools.py +147 -0
- memorygraph/tools/search_tools.py +406 -0
- memorygraph/tools/temporal_tools.py +339 -0
- memorygraph/utils/__init__.py +10 -0
- memorygraph/utils/context_extractor.py +429 -0
- memorygraph/utils/error_handling.py +151 -0
- memorygraph/utils/export_import.py +425 -0
- memorygraph/utils/graph_algorithms.py +200 -0
- memorygraph/utils/pagination.py +149 -0
- memorygraph/utils/project_detection.py +133 -0
- memorygraphmcp-0.11.7.dist-info/METADATA +970 -0
- memorygraphmcp-0.11.7.dist-info/RECORD +65 -0
- memorygraphmcp-0.11.7.dist-info/WHEEL +4 -0
- memorygraphmcp-0.11.7.dist-info/entry_points.txt +2 -0
- memorygraphmcp-0.11.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Neo4j backend implementation for the Claude Code Memory Server.
|
|
3
|
+
|
|
4
|
+
This module provides the Neo4j-specific implementation of the GraphBackend interface,
|
|
5
|
+
wrapping the existing Neo4j connection and query logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from neo4j import AsyncGraphDatabase, AsyncDriver
|
|
13
|
+
from neo4j.exceptions import ServiceUnavailable, AuthError, Neo4jError
|
|
14
|
+
from contextlib import asynccontextmanager
|
|
15
|
+
|
|
16
|
+
from .base import GraphBackend
|
|
17
|
+
from ..models import DatabaseConnectionError, SchemaError
|
|
18
|
+
from ..config import Config
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Neo4jBackend(GraphBackend):
|
|
24
|
+
"""Neo4j implementation of the GraphBackend interface."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
uri: Optional[str] = None,
|
|
29
|
+
user: Optional[str] = None,
|
|
30
|
+
password: Optional[str] = None,
|
|
31
|
+
database: str = "neo4j"
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize Neo4j backend.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
uri: Neo4j database URI (defaults to MEMORY_NEO4J_URI or NEO4J_URI env var)
|
|
38
|
+
user: Database username (defaults to MEMORY_NEO4J_USER or NEO4J_USER env var)
|
|
39
|
+
password: Database password (defaults to MEMORY_NEO4J_PASSWORD or NEO4J_PASSWORD env var)
|
|
40
|
+
database: Database name (defaults to 'neo4j')
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
DatabaseConnectionError: If password is not provided
|
|
44
|
+
"""
|
|
45
|
+
self.uri = uri or os.getenv("MEMORY_NEO4J_URI") or os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
|
46
|
+
self.user = user or os.getenv("MEMORY_NEO4J_USER") or os.getenv("NEO4J_USER", "neo4j")
|
|
47
|
+
self.password = password or os.getenv("MEMORY_NEO4J_PASSWORD") or os.getenv("NEO4J_PASSWORD")
|
|
48
|
+
self.database = database
|
|
49
|
+
self.driver: Optional[AsyncDriver] = None
|
|
50
|
+
self._connected = False
|
|
51
|
+
|
|
52
|
+
if not self.password:
|
|
53
|
+
raise DatabaseConnectionError(
|
|
54
|
+
"Neo4j password must be provided via parameter or MEMORY_NEO4J_PASSWORD/NEO4J_PASSWORD env var"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def connect(self) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Establish async connection to Neo4j database.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
True if connection successful
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
DatabaseConnectionError: If connection fails
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
self.driver = AsyncGraphDatabase.driver(
|
|
69
|
+
self.uri,
|
|
70
|
+
auth=(self.user, self.password),
|
|
71
|
+
max_connection_lifetime=30 * 60, # 30 minutes
|
|
72
|
+
max_connection_pool_size=50,
|
|
73
|
+
connection_acquisition_timeout=30.0
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Verify connectivity
|
|
77
|
+
await self.driver.verify_connectivity()
|
|
78
|
+
self._connected = True
|
|
79
|
+
logger.info(f"Successfully connected to Neo4j at {self.uri}")
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
except ServiceUnavailable as e:
|
|
83
|
+
logger.error(f"Failed to connect to Neo4j: {e}")
|
|
84
|
+
raise DatabaseConnectionError(f"Failed to connect to Neo4j: {e}")
|
|
85
|
+
except AuthError as e:
|
|
86
|
+
logger.error(f"Authentication failed for Neo4j: {e}")
|
|
87
|
+
raise DatabaseConnectionError(f"Authentication failed for Neo4j: {e}")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Unexpected error connecting to Neo4j: {e}")
|
|
90
|
+
raise DatabaseConnectionError(f"Unexpected error connecting to Neo4j: {e}")
|
|
91
|
+
|
|
92
|
+
async def disconnect(self) -> None:
|
|
93
|
+
"""Close the database connection."""
|
|
94
|
+
if self.driver:
|
|
95
|
+
await self.driver.close()
|
|
96
|
+
self.driver = None
|
|
97
|
+
self._connected = False
|
|
98
|
+
logger.info("Neo4j connection closed")
|
|
99
|
+
|
|
100
|
+
async def execute_query(
|
|
101
|
+
self,
|
|
102
|
+
query: str,
|
|
103
|
+
parameters: Optional[dict[str, Any]] = None,
|
|
104
|
+
write: bool = False
|
|
105
|
+
) -> list[dict[str, Any]]:
|
|
106
|
+
"""
|
|
107
|
+
Execute a Cypher query and return results.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
query: The Cypher query string
|
|
111
|
+
parameters: Query parameters for parameterized queries
|
|
112
|
+
write: Whether this is a write operation (default: False)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of result records as dictionaries
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
DatabaseConnectionError: If not connected or query fails
|
|
119
|
+
"""
|
|
120
|
+
if not self._connected or not self.driver:
|
|
121
|
+
raise DatabaseConnectionError("Not connected to Neo4j. Call connect() first.")
|
|
122
|
+
|
|
123
|
+
params = parameters or {}
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
async with self._session() as session:
|
|
127
|
+
if write:
|
|
128
|
+
result = await session.execute_write(self._run_query_async, query, params)
|
|
129
|
+
else:
|
|
130
|
+
result = await session.execute_read(self._run_query_async, query, params)
|
|
131
|
+
return result
|
|
132
|
+
except Neo4jError as e:
|
|
133
|
+
logger.error(f"Query execution failed: {e}")
|
|
134
|
+
raise DatabaseConnectionError(f"Query execution failed: {e}")
|
|
135
|
+
|
|
136
|
+
@asynccontextmanager
|
|
137
|
+
async def _session(self):
|
|
138
|
+
"""Async context manager for Neo4j session."""
|
|
139
|
+
if not self.driver:
|
|
140
|
+
raise DatabaseConnectionError("Not connected to Neo4j. Call connect() first.")
|
|
141
|
+
|
|
142
|
+
session = self.driver.session(database=self.database)
|
|
143
|
+
try:
|
|
144
|
+
yield session
|
|
145
|
+
finally:
|
|
146
|
+
await session.close()
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
async def _run_query_async(tx, query: str, parameters: dict[str, Any]) -> list[dict[str, Any]]:
|
|
150
|
+
"""
|
|
151
|
+
Helper method to run a query within an async transaction.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
tx: Transaction object
|
|
155
|
+
query: Cypher query string
|
|
156
|
+
parameters: Query parameters
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of result records as dictionaries
|
|
160
|
+
"""
|
|
161
|
+
result = await tx.run(query, parameters)
|
|
162
|
+
records = await result.data()
|
|
163
|
+
return records
|
|
164
|
+
|
|
165
|
+
async def initialize_schema(self) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Initialize database schema including indexes and constraints.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
SchemaError: If schema initialization fails
|
|
171
|
+
"""
|
|
172
|
+
logger.info("Initializing Neo4j schema for Claude Memory...")
|
|
173
|
+
|
|
174
|
+
# Create constraints
|
|
175
|
+
constraints = [
|
|
176
|
+
"CREATE CONSTRAINT memory_id_unique IF NOT EXISTS FOR (m:Memory) REQUIRE m.id IS UNIQUE",
|
|
177
|
+
"CREATE CONSTRAINT relationship_id_unique IF NOT EXISTS FOR (r:RELATIONSHIP) REQUIRE r.id IS UNIQUE",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
# Create indexes for performance
|
|
181
|
+
indexes = [
|
|
182
|
+
"CREATE INDEX memory_type_index IF NOT EXISTS FOR (m:Memory) ON (m.type)",
|
|
183
|
+
"CREATE INDEX memory_created_at_index IF NOT EXISTS FOR (m:Memory) ON (m.created_at)",
|
|
184
|
+
"CREATE INDEX memory_tags_index IF NOT EXISTS FOR (m:Memory) ON (m.tags)",
|
|
185
|
+
"CREATE FULLTEXT INDEX memory_content_index IF NOT EXISTS FOR (m:Memory) ON EACH [m.title, m.content, m.summary]",
|
|
186
|
+
"CREATE INDEX memory_importance_index IF NOT EXISTS FOR (m:Memory) ON (m.importance)",
|
|
187
|
+
"CREATE INDEX memory_confidence_index IF NOT EXISTS FOR (m:Memory) ON (m.confidence)",
|
|
188
|
+
"CREATE INDEX memory_project_path_index IF NOT EXISTS FOR (m:Memory) ON (m.context_project_path)",
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
# Conditional multi-tenant indexes (Phase 1)
|
|
192
|
+
if Config.is_multi_tenant_mode():
|
|
193
|
+
multitenant_indexes = [
|
|
194
|
+
"CREATE INDEX memory_tenant_index IF NOT EXISTS FOR (m:Memory) ON (m.context_tenant_id)",
|
|
195
|
+
"CREATE INDEX memory_team_index IF NOT EXISTS FOR (m:Memory) ON (m.context_team_id)",
|
|
196
|
+
"CREATE INDEX memory_visibility_index IF NOT EXISTS FOR (m:Memory) ON (m.context_visibility)",
|
|
197
|
+
"CREATE INDEX memory_created_by_index IF NOT EXISTS FOR (m:Memory) ON (m.context_created_by)",
|
|
198
|
+
"CREATE INDEX memory_version_index IF NOT EXISTS FOR (m:Memory) ON (m.version)",
|
|
199
|
+
]
|
|
200
|
+
indexes.extend(multitenant_indexes)
|
|
201
|
+
logger.info("Multi-tenant mode enabled, adding tenant indexes")
|
|
202
|
+
|
|
203
|
+
# Execute schema creation
|
|
204
|
+
for constraint in constraints:
|
|
205
|
+
try:
|
|
206
|
+
await self.execute_query(constraint, write=True)
|
|
207
|
+
logger.debug(f"Created constraint: {constraint}")
|
|
208
|
+
except DatabaseConnectionError as e:
|
|
209
|
+
if "already exists" not in str(e).lower():
|
|
210
|
+
raise SchemaError(f"Failed to create constraint: {e}")
|
|
211
|
+
|
|
212
|
+
for index in indexes:
|
|
213
|
+
try:
|
|
214
|
+
await self.execute_query(index, write=True)
|
|
215
|
+
logger.debug(f"Created index: {index}")
|
|
216
|
+
except DatabaseConnectionError as e:
|
|
217
|
+
if "already exists" not in str(e).lower():
|
|
218
|
+
raise SchemaError(f"Failed to create index: {e}")
|
|
219
|
+
|
|
220
|
+
logger.info("Schema initialization completed")
|
|
221
|
+
|
|
222
|
+
async def health_check(self) -> dict[str, Any]:
|
|
223
|
+
"""
|
|
224
|
+
Check backend health and return status information.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Dictionary with health check results
|
|
228
|
+
"""
|
|
229
|
+
health_info = {
|
|
230
|
+
"connected": self._connected,
|
|
231
|
+
"backend_type": "neo4j",
|
|
232
|
+
"uri": self.uri,
|
|
233
|
+
"database": self.database
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if self._connected:
|
|
237
|
+
try:
|
|
238
|
+
# Try to get version and basic statistics
|
|
239
|
+
query = """
|
|
240
|
+
CALL dbms.components() YIELD name, versions, edition
|
|
241
|
+
RETURN name, versions[0] as version, edition
|
|
242
|
+
"""
|
|
243
|
+
result = await self.execute_query(query, write=False)
|
|
244
|
+
if result:
|
|
245
|
+
health_info["version"] = result[0].get("version", "unknown")
|
|
246
|
+
health_info["edition"] = result[0].get("edition", "unknown")
|
|
247
|
+
|
|
248
|
+
# Get basic node count
|
|
249
|
+
count_query = "MATCH (m:Memory) RETURN count(m) as count"
|
|
250
|
+
count_result = await self.execute_query(count_query, write=False)
|
|
251
|
+
if count_result:
|
|
252
|
+
health_info["statistics"] = {
|
|
253
|
+
"memory_count": count_result[0].get("count", 0)
|
|
254
|
+
}
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.warning(f"Could not get detailed health info: {e}")
|
|
257
|
+
health_info["warning"] = str(e)
|
|
258
|
+
|
|
259
|
+
return health_info
|
|
260
|
+
|
|
261
|
+
def backend_name(self) -> str:
|
|
262
|
+
"""Return the name of this backend implementation."""
|
|
263
|
+
return "neo4j"
|
|
264
|
+
|
|
265
|
+
def supports_fulltext_search(self) -> bool:
|
|
266
|
+
"""Check if this backend supports full-text search."""
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
def supports_transactions(self) -> bool:
|
|
270
|
+
"""Check if this backend supports ACID transactions."""
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
async def create(
|
|
275
|
+
cls,
|
|
276
|
+
uri: Optional[str] = None,
|
|
277
|
+
user: Optional[str] = None,
|
|
278
|
+
password: Optional[str] = None,
|
|
279
|
+
database: str = "neo4j"
|
|
280
|
+
) -> "Neo4jBackend":
|
|
281
|
+
"""
|
|
282
|
+
Factory method to create and connect to a Neo4j backend.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
uri: Neo4j database URI
|
|
286
|
+
user: Database username
|
|
287
|
+
password: Database password
|
|
288
|
+
database: Database name
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Connected Neo4jBackend instance
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
DatabaseConnectionError: If connection fails
|
|
295
|
+
"""
|
|
296
|
+
backend = cls(uri, user, password, database)
|
|
297
|
+
await backend.connect()
|
|
298
|
+
return backend
|