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,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LadybugDB backend implementation for the Claude Code Memory Server.
|
|
3
|
+
|
|
4
|
+
This module provides the LadybugDB-specific implementation of the GraphBackend interface.
|
|
5
|
+
LadybugDB is a graph database that uses Cypher queries, similar to Kuzu.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Optional, List, Tuple, Dict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import real_ladybug as lb
|
|
15
|
+
LADYBUGDB_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
lb = None # type: ignore
|
|
18
|
+
LADYBUGDB_AVAILABLE = False
|
|
19
|
+
|
|
20
|
+
from .base import GraphBackend
|
|
21
|
+
from ..models import (
|
|
22
|
+
Memory,
|
|
23
|
+
MemoryType,
|
|
24
|
+
Relationship,
|
|
25
|
+
RelationshipType,
|
|
26
|
+
RelationshipProperties,
|
|
27
|
+
SearchQuery,
|
|
28
|
+
MemoryContext,
|
|
29
|
+
MemoryNode,
|
|
30
|
+
DatabaseConnectionError,
|
|
31
|
+
SchemaError,
|
|
32
|
+
ValidationError,
|
|
33
|
+
RelationshipError,
|
|
34
|
+
)
|
|
35
|
+
from ..config import Config
|
|
36
|
+
from datetime import datetime, timezone
|
|
37
|
+
import uuid
|
|
38
|
+
import json
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LadybugDBBackend(GraphBackend):
|
|
44
|
+
"""LadybugDB implementation of the GraphBackend interface."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, db_path: Optional[str] = None, graph_name: str = "memorygraph"):
|
|
47
|
+
"""
|
|
48
|
+
Initialize LadybugDB backend.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
db_path: Path to database file (defaults to LADYBUGDB_PATH env var or ~/.memorygraph/ladybugdb.db)
|
|
52
|
+
graph_name: Name of the graph database (defaults to 'memorygraph')
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ImportError: If real_ladybug package is not installed.
|
|
56
|
+
"""
|
|
57
|
+
if not LADYBUGDB_AVAILABLE:
|
|
58
|
+
raise ImportError(
|
|
59
|
+
"LadybugDB backend requires real_ladybug package. "
|
|
60
|
+
"Install it with: pip install real-ladybug"
|
|
61
|
+
)
|
|
62
|
+
if db_path is None:
|
|
63
|
+
db_path = os.getenv("LADYBUGDB_PATH")
|
|
64
|
+
if db_path is None:
|
|
65
|
+
# Default to ~/.memorygraph/ladybugdb.db
|
|
66
|
+
home = Path.home()
|
|
67
|
+
db_dir = home / ".memorygraph"
|
|
68
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
db_path = str(db_dir / "ladybugdb.db")
|
|
70
|
+
|
|
71
|
+
self.db_path = db_path
|
|
72
|
+
self.graph_name = graph_name
|
|
73
|
+
self.client = None
|
|
74
|
+
self.graph = None
|
|
75
|
+
self._connected = False
|
|
76
|
+
|
|
77
|
+
async def connect(self) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Establish connection to LadybugDB database.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if connection successful
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
DatabaseConnectionError: If connection fails
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
# Create LadybugDB database
|
|
89
|
+
self.client = lb.Database(self.db_path)
|
|
90
|
+
|
|
91
|
+
# Create connection for executing queries
|
|
92
|
+
self.graph = lb.Connection(self.client)
|
|
93
|
+
self._connected = True
|
|
94
|
+
|
|
95
|
+
logger.info(f"Successfully connected to LadybugDB at {self.db_path}")
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Failed to connect to LadybugDB: {e}")
|
|
100
|
+
raise DatabaseConnectionError(f"Failed to connect to LadybugDB: {e}")
|
|
101
|
+
|
|
102
|
+
async def disconnect(self) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Close the LadybugDB connection and clean up resources.
|
|
105
|
+
"""
|
|
106
|
+
if self.graph:
|
|
107
|
+
self.graph.close()
|
|
108
|
+
self.graph = None
|
|
109
|
+
if self.client:
|
|
110
|
+
self.client.close()
|
|
111
|
+
self.client = None
|
|
112
|
+
self._connected = False
|
|
113
|
+
logger.info("Disconnected from LadybugDB")
|
|
114
|
+
|
|
115
|
+
async def execute_query(
|
|
116
|
+
self,
|
|
117
|
+
query: str,
|
|
118
|
+
parameters: Optional[dict[str, Any]] = None,
|
|
119
|
+
write: bool = False,
|
|
120
|
+
) -> list[dict[str, Any]]:
|
|
121
|
+
"""
|
|
122
|
+
Execute a Cypher query and return results.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
query: Cypher query string
|
|
126
|
+
parameters: Query parameters
|
|
127
|
+
write: Whether this is a write operation
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of result dictionaries
|
|
131
|
+
"""
|
|
132
|
+
if not self._connected or not self.graph:
|
|
133
|
+
raise DatabaseConnectionError("Not connected to LadybugDB")
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Execute query using LadybugDB's connection
|
|
137
|
+
result = self.graph.execute(query)
|
|
138
|
+
|
|
139
|
+
# Convert result to list of dictionaries
|
|
140
|
+
# LadybugDB returns QueryResult with has_next()/get_next() methods
|
|
141
|
+
# get_next() returns the row data as a dictionary
|
|
142
|
+
rows = []
|
|
143
|
+
while result.has_next():
|
|
144
|
+
row_data = result.get_next()
|
|
145
|
+
rows.append(row_data)
|
|
146
|
+
|
|
147
|
+
return rows
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Query execution failed: {e}")
|
|
151
|
+
raise SchemaError(f"Query execution failed: {e}")
|
|
152
|
+
|
|
153
|
+
async def initialize_schema(self) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Initialize database schema including indexes and constraints.
|
|
156
|
+
|
|
157
|
+
This should be idempotent and safe to call multiple times.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
SchemaError: If schema initialization fails
|
|
161
|
+
"""
|
|
162
|
+
if not self._connected or not self.graph:
|
|
163
|
+
raise DatabaseConnectionError("Not connected to LadybugDB")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Create basic schema - indexes and constraints
|
|
167
|
+
# Note: LadybugDB Cypher syntax may vary, adjust as needed
|
|
168
|
+
schema_queries = [
|
|
169
|
+
"CREATE INDEX IF NOT EXISTS FOR (n:Memory) ON (n.id)",
|
|
170
|
+
"CREATE INDEX IF NOT EXISTS FOR (n:Memory) ON (n.type)",
|
|
171
|
+
"CREATE INDEX IF NOT EXISTS FOR (n:Memory) ON (n.created_at)",
|
|
172
|
+
"CREATE CONSTRAINT IF NOT EXISTS FOR (n:Memory) REQUIRE n.id IS UNIQUE",
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
for query in schema_queries:
|
|
176
|
+
await self.execute_query(query, write=True)
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Schema initialization failed: {e}")
|
|
180
|
+
raise SchemaError(f"Schema initialization failed: {e}")
|
|
181
|
+
|
|
182
|
+
async def health_check(self) -> dict[str, Any]:
|
|
183
|
+
"""
|
|
184
|
+
Check backend health and return status information.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dictionary with health check results
|
|
188
|
+
"""
|
|
189
|
+
health_info = {
|
|
190
|
+
"connected": self._connected,
|
|
191
|
+
"backend_type": "ladybugdb",
|
|
192
|
+
"backend_name": self.backend_name(),
|
|
193
|
+
"supports_fulltext_search": self.supports_fulltext_search(),
|
|
194
|
+
"supports_transactions": self.supports_transactions(),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if self._connected and self.graph:
|
|
198
|
+
try:
|
|
199
|
+
# Simple health check query
|
|
200
|
+
result = await self.execute_query("RETURN 'healthy' as status")
|
|
201
|
+
health_info["status"] = result[0]["status"] if result else "unknown"
|
|
202
|
+
health_info["healthy"] = True
|
|
203
|
+
except Exception as e:
|
|
204
|
+
health_info["healthy"] = False
|
|
205
|
+
health_info["error"] = str(e)
|
|
206
|
+
else:
|
|
207
|
+
health_info["healthy"] = False
|
|
208
|
+
health_info["error"] = "Not connected"
|
|
209
|
+
|
|
210
|
+
return health_info
|
|
211
|
+
|
|
212
|
+
def backend_name(self) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Return the name of this backend implementation.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Backend name
|
|
218
|
+
"""
|
|
219
|
+
return "ladybugdb"
|
|
220
|
+
|
|
221
|
+
def supports_fulltext_search(self) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Check if this backend supports full-text search.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if full-text search is supported
|
|
227
|
+
"""
|
|
228
|
+
# LadybugDB may support full-text search, but we'll be conservative
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
def supports_transactions(self) -> bool:
|
|
232
|
+
"""
|
|
233
|
+
Check if this backend supports ACID transactions.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if transactions are supported
|
|
237
|
+
"""
|
|
238
|
+
# LadybugDB likely supports transactions
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
# Additional methods would be implemented here following the GraphBackend interface
|
|
242
|
+
# For brevity, only the core methods are shown
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memgraph backend implementation for the Claude Code Memory Server.
|
|
3
|
+
|
|
4
|
+
This module provides Memgraph-specific implementation of the GraphBackend interface.
|
|
5
|
+
Memgraph uses the Bolt protocol and Cypher, so it can use the same driver as Neo4j
|
|
6
|
+
with some Cypher dialect adaptations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
from neo4j import AsyncGraphDatabase, AsyncDriver
|
|
14
|
+
from neo4j.exceptions import ServiceUnavailable, AuthError, Neo4jError
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
|
|
17
|
+
from .base import GraphBackend
|
|
18
|
+
from ..models import DatabaseConnectionError, SchemaError
|
|
19
|
+
from ..config import Config
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MemgraphBackend(GraphBackend):
|
|
25
|
+
"""Memgraph implementation of the GraphBackend interface."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
uri: Optional[str] = None,
|
|
30
|
+
user: str = "",
|
|
31
|
+
password: str = "",
|
|
32
|
+
database: str = "memgraph"
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize Memgraph backend.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
uri: Memgraph database URI (defaults to MEMORY_MEMGRAPH_URI env var)
|
|
39
|
+
user: Database username (Memgraph Community has no auth by default)
|
|
40
|
+
password: Database password (empty for Community Edition)
|
|
41
|
+
database: Database name (default: 'memgraph')
|
|
42
|
+
|
|
43
|
+
Note:
|
|
44
|
+
Memgraph Community Edition has no authentication by default.
|
|
45
|
+
Enterprise Edition supports authentication.
|
|
46
|
+
"""
|
|
47
|
+
self.uri = uri or os.getenv("MEMORY_MEMGRAPH_URI", "bolt://localhost:7687")
|
|
48
|
+
self.user = user or os.getenv("MEMORY_MEMGRAPH_USER", "")
|
|
49
|
+
self.password = password or os.getenv("MEMORY_MEMGRAPH_PASSWORD", "")
|
|
50
|
+
self.database = database
|
|
51
|
+
self.driver: Optional[AsyncDriver] = None
|
|
52
|
+
self._connected = False
|
|
53
|
+
|
|
54
|
+
async def connect(self) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Establish async connection to Memgraph database.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if connection successful
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
DatabaseConnectionError: If connection fails
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
# Memgraph uses same Bolt protocol as Neo4j
|
|
66
|
+
# Community Edition: auth is typically empty tuple or ("", "")
|
|
67
|
+
auth = (self.user, self.password) if self.user or self.password else None
|
|
68
|
+
|
|
69
|
+
self.driver = AsyncGraphDatabase.driver(
|
|
70
|
+
self.uri,
|
|
71
|
+
auth=auth,
|
|
72
|
+
max_connection_lifetime=30 * 60,
|
|
73
|
+
max_connection_pool_size=50,
|
|
74
|
+
connection_acquisition_timeout=30.0
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Verify connectivity
|
|
78
|
+
await self.driver.verify_connectivity()
|
|
79
|
+
self._connected = True
|
|
80
|
+
logger.info(f"Successfully connected to Memgraph at {self.uri}")
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
except ServiceUnavailable as e:
|
|
84
|
+
logger.error(f"Failed to connect to Memgraph: {e}")
|
|
85
|
+
raise DatabaseConnectionError(f"Failed to connect to Memgraph: {e}")
|
|
86
|
+
except AuthError as e:
|
|
87
|
+
logger.error(f"Authentication failed for Memgraph: {e}")
|
|
88
|
+
raise DatabaseConnectionError(f"Authentication failed for Memgraph: {e}")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Unexpected error connecting to Memgraph: {e}")
|
|
91
|
+
raise DatabaseConnectionError(f"Unexpected error connecting to Memgraph: {e}")
|
|
92
|
+
|
|
93
|
+
async def disconnect(self) -> None:
|
|
94
|
+
"""Close the database connection."""
|
|
95
|
+
if self.driver:
|
|
96
|
+
await self.driver.close()
|
|
97
|
+
self.driver = None
|
|
98
|
+
self._connected = False
|
|
99
|
+
logger.info("Memgraph connection closed")
|
|
100
|
+
|
|
101
|
+
async def execute_query(
|
|
102
|
+
self,
|
|
103
|
+
query: str,
|
|
104
|
+
parameters: Optional[dict[str, Any]] = None,
|
|
105
|
+
write: bool = False
|
|
106
|
+
) -> list[dict[str, Any]]:
|
|
107
|
+
"""
|
|
108
|
+
Execute a Cypher query and return results.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
query: The Cypher query string
|
|
112
|
+
parameters: Query parameters for parameterized queries
|
|
113
|
+
write: Whether this is a write operation (default: False)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of result records as dictionaries
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
DatabaseConnectionError: If not connected or query fails
|
|
120
|
+
"""
|
|
121
|
+
if not self._connected or not self.driver:
|
|
122
|
+
raise DatabaseConnectionError("Not connected to Memgraph. Call connect() first.")
|
|
123
|
+
|
|
124
|
+
params = parameters or {}
|
|
125
|
+
|
|
126
|
+
# Adapt Cypher for Memgraph dialect differences
|
|
127
|
+
adapted_query = self._adapt_cypher(query)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
async with self._session() as session:
|
|
131
|
+
# Memgraph doesn't distinguish between read/write transactions in the same way
|
|
132
|
+
# We'll use execute_write for both to ensure consistency
|
|
133
|
+
result = await session.execute_write(self._run_query_async, adapted_query, params)
|
|
134
|
+
return result
|
|
135
|
+
except Neo4jError as e:
|
|
136
|
+
logger.error(f"Query execution failed: {e}")
|
|
137
|
+
raise DatabaseConnectionError(f"Query execution failed: {e}")
|
|
138
|
+
|
|
139
|
+
@asynccontextmanager
|
|
140
|
+
async def _session(self):
|
|
141
|
+
"""Async context manager for Memgraph session."""
|
|
142
|
+
if not self.driver:
|
|
143
|
+
raise DatabaseConnectionError("Not connected to Memgraph. Call connect() first.")
|
|
144
|
+
|
|
145
|
+
session = self.driver.session()
|
|
146
|
+
try:
|
|
147
|
+
yield session
|
|
148
|
+
finally:
|
|
149
|
+
await session.close()
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
async def _run_query_async(tx, query: str, parameters: dict[str, Any]) -> list[dict[str, Any]]:
|
|
153
|
+
"""
|
|
154
|
+
Helper method to run a query within an async transaction.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
tx: Transaction object
|
|
158
|
+
query: Cypher query string
|
|
159
|
+
parameters: Query parameters
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of result records as dictionaries
|
|
163
|
+
"""
|
|
164
|
+
result = await tx.run(query, parameters)
|
|
165
|
+
records = await result.data()
|
|
166
|
+
return records
|
|
167
|
+
|
|
168
|
+
def _adapt_cypher(self, query: str) -> str:
|
|
169
|
+
"""
|
|
170
|
+
Adapt Cypher query for Memgraph dialect differences.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
query: Original Cypher query
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Adapted query for Memgraph
|
|
177
|
+
|
|
178
|
+
Note:
|
|
179
|
+
Main differences:
|
|
180
|
+
- FULLTEXT INDEX syntax is different
|
|
181
|
+
- Some constraint syntax differs
|
|
182
|
+
- CALL dbms.* procedures may not be available
|
|
183
|
+
"""
|
|
184
|
+
# Memgraph uses CREATE TEXT INDEX instead of CREATE FULLTEXT INDEX
|
|
185
|
+
# But it doesn't support fulltext the same way, so we skip it
|
|
186
|
+
if "CREATE FULLTEXT INDEX" in query:
|
|
187
|
+
logger.debug(f"Skipping fulltext index creation for Memgraph (not fully supported)")
|
|
188
|
+
return "RETURN 1" # No-op query
|
|
189
|
+
|
|
190
|
+
# Memgraph uses different constraint syntax pre-v2.11
|
|
191
|
+
# But modern Memgraph should support standard syntax
|
|
192
|
+
return query
|
|
193
|
+
|
|
194
|
+
async def initialize_schema(self) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Initialize database schema including indexes and constraints.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
SchemaError: If schema initialization fails
|
|
200
|
+
"""
|
|
201
|
+
logger.info("Initializing Memgraph schema for Claude Memory...")
|
|
202
|
+
|
|
203
|
+
# Create constraints (Memgraph syntax)
|
|
204
|
+
constraints = [
|
|
205
|
+
"CREATE CONSTRAINT ON (m:Memory) ASSERT m.id IS UNIQUE",
|
|
206
|
+
# Note: Relationship constraints may not be supported in all Memgraph versions
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
# Create indexes for performance
|
|
210
|
+
indexes = [
|
|
211
|
+
"CREATE INDEX ON :Memory(type)",
|
|
212
|
+
"CREATE INDEX ON :Memory(created_at)",
|
|
213
|
+
"CREATE INDEX ON :Memory(tags)",
|
|
214
|
+
"CREATE INDEX ON :Memory(importance)",
|
|
215
|
+
"CREATE INDEX ON :Memory(confidence)",
|
|
216
|
+
# Note: Memgraph doesn't support multi-property indexes the same way
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# Conditional multi-tenant indexes (Phase 1)
|
|
220
|
+
if Config.is_multi_tenant_mode():
|
|
221
|
+
multitenant_indexes = [
|
|
222
|
+
"CREATE INDEX ON :Memory(context_tenant_id)",
|
|
223
|
+
"CREATE INDEX ON :Memory(context_team_id)",
|
|
224
|
+
"CREATE INDEX ON :Memory(context_visibility)",
|
|
225
|
+
"CREATE INDEX ON :Memory(context_created_by)",
|
|
226
|
+
"CREATE INDEX ON :Memory(version)",
|
|
227
|
+
]
|
|
228
|
+
indexes.extend(multitenant_indexes)
|
|
229
|
+
logger.info("Multi-tenant mode enabled, adding tenant indexes")
|
|
230
|
+
|
|
231
|
+
# Execute schema creation
|
|
232
|
+
for constraint in constraints:
|
|
233
|
+
try:
|
|
234
|
+
await self.execute_query(constraint, write=True)
|
|
235
|
+
logger.debug(f"Created constraint: {constraint}")
|
|
236
|
+
except DatabaseConnectionError as e:
|
|
237
|
+
# Memgraph may not support all constraint types
|
|
238
|
+
if "already exists" not in str(e).lower() and "not supported" not in str(e).lower():
|
|
239
|
+
logger.warning(f"Failed to create constraint (may not be supported): {e}")
|
|
240
|
+
|
|
241
|
+
for index in indexes:
|
|
242
|
+
try:
|
|
243
|
+
await self.execute_query(index, write=True)
|
|
244
|
+
logger.debug(f"Created index: {index}")
|
|
245
|
+
except DatabaseConnectionError as e:
|
|
246
|
+
if "already exists" not in str(e).lower():
|
|
247
|
+
logger.warning(f"Failed to create index: {e}")
|
|
248
|
+
|
|
249
|
+
logger.info("Schema initialization completed")
|
|
250
|
+
|
|
251
|
+
async def health_check(self) -> dict[str, Any]:
|
|
252
|
+
"""
|
|
253
|
+
Check backend health and return status information.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dictionary with health check results
|
|
257
|
+
"""
|
|
258
|
+
health_info = {
|
|
259
|
+
"connected": self._connected,
|
|
260
|
+
"backend_type": "memgraph",
|
|
261
|
+
"uri": self.uri,
|
|
262
|
+
"database": self.database
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if self._connected:
|
|
266
|
+
try:
|
|
267
|
+
# Get basic node count
|
|
268
|
+
count_query = "MATCH (m:Memory) RETURN count(m) as count"
|
|
269
|
+
count_result = await self.execute_query(count_query, write=False)
|
|
270
|
+
if count_result:
|
|
271
|
+
health_info["statistics"] = {
|
|
272
|
+
"memory_count": count_result[0].get("count", 0)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Try to get Memgraph version (if available)
|
|
276
|
+
# Note: Memgraph may not have dbms.components()
|
|
277
|
+
health_info["version"] = "unknown"
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"Could not get detailed health info: {e}")
|
|
280
|
+
health_info["warning"] = str(e)
|
|
281
|
+
|
|
282
|
+
return health_info
|
|
283
|
+
|
|
284
|
+
def backend_name(self) -> str:
|
|
285
|
+
"""Return the name of this backend implementation."""
|
|
286
|
+
return "memgraph"
|
|
287
|
+
|
|
288
|
+
def supports_fulltext_search(self) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
Check if this backend supports full-text search.
|
|
291
|
+
|
|
292
|
+
Note:
|
|
293
|
+
Memgraph has limited full-text search support compared to Neo4j.
|
|
294
|
+
Text indexing is available but not full FULLTEXT INDEX functionality.
|
|
295
|
+
"""
|
|
296
|
+
return False # Limited support
|
|
297
|
+
|
|
298
|
+
def supports_transactions(self) -> bool:
|
|
299
|
+
"""Check if this backend supports ACID transactions."""
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
async def create(
|
|
304
|
+
cls,
|
|
305
|
+
uri: Optional[str] = None,
|
|
306
|
+
user: str = "",
|
|
307
|
+
password: str = "",
|
|
308
|
+
database: str = "memgraph"
|
|
309
|
+
) -> "MemgraphBackend":
|
|
310
|
+
"""
|
|
311
|
+
Factory method to create and connect to a Memgraph backend.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
uri: Memgraph database URI
|
|
315
|
+
user: Database username
|
|
316
|
+
password: Database password
|
|
317
|
+
database: Database name
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Connected MemgraphBackend instance
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
DatabaseConnectionError: If connection fails
|
|
324
|
+
"""
|
|
325
|
+
backend = cls(uri, user, password, database)
|
|
326
|
+
await backend.connect()
|
|
327
|
+
return backend
|