memgraph-context-graph 0.5.0__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.
- memgraph_context_graph/__init__.py +71 -0
- memgraph_context_graph/adapters/__init__.py +0 -0
- memgraph_context_graph/adapters/_identity.py +520 -0
- memgraph_context_graph/adapters/_opencode_plugin.js +112 -0
- memgraph_context_graph/adapters/_spec.py +441 -0
- memgraph_context_graph/adapters/antigravity_cli.py +152 -0
- memgraph_context_graph/adapters/claude.py +297 -0
- memgraph_context_graph/adapters/claude_code.py +216 -0
- memgraph_context_graph/adapters/claude_code_memory.py +181 -0
- memgraph_context_graph/adapters/codex.py +118 -0
- memgraph_context_graph/adapters/codex_memory.py +185 -0
- memgraph_context_graph/adapters/copilot_cli.py +236 -0
- memgraph_context_graph/adapters/cursor.py +189 -0
- memgraph_context_graph/adapters/grok.py +187 -0
- memgraph_context_graph/adapters/openai.py +240 -0
- memgraph_context_graph/adapters/opencode.py +168 -0
- memgraph_context_graph/cli.py +845 -0
- memgraph_context_graph/events.py +210 -0
- memgraph_context_graph/hooks/__init__.py +1 -0
- memgraph_context_graph/hooks/cli.py +184 -0
- memgraph_context_graph/hooks/runner.py +311 -0
- memgraph_context_graph/hooks/runtime_plugin.py +85 -0
- memgraph_context_graph/link.py +50 -0
- memgraph_context_graph/mcp_server.py +102 -0
- memgraph_context_graph/memory_backend.py +144 -0
- memgraph_context_graph/protocols.py +67 -0
- memgraph_context_graph/spaces.py +214 -0
- memgraph_context_graph/tools.py +130 -0
- memgraph_context_graph-0.5.0.dist-info/METADATA +718 -0
- memgraph_context_graph-0.5.0.dist-info/RECORD +33 -0
- memgraph_context_graph-0.5.0.dist-info/WHEEL +4 -0
- memgraph_context_graph-0.5.0.dist-info/entry_points.txt +11 -0
- memgraph_context_graph-0.5.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
"""Top-level CLI for memgraph-context-graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from importlib import metadata
|
|
13
|
+
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
|
|
18
|
+
_HELP = """usage: memgraph-context-graph <command> [options]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
bootstrap Install runtime dependencies and verify hook capture.
|
|
22
|
+
config Get or set persistent configuration values.
|
|
23
|
+
doctor Check runtime hook dependencies and Memgraph connectivity.
|
|
24
|
+
setup <runtime> Configure an agent runtime; --memory-backend context-graph|native
|
|
25
|
+
switches whose memory its model uses.
|
|
26
|
+
join <url> <login> Use an Enterprise Memgraph as <login> (edition = enterprise); --password or prompt.
|
|
27
|
+
hook <command> Configure or run command hooks.
|
|
28
|
+
mcp Serve the registered tools (recall, ...) over stdio MCP.
|
|
29
|
+
recall <question> Search your own past sessions; --json for the data.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
34
|
+
"""Dispatch top-level memgraph-context-graph commands."""
|
|
35
|
+
args = list(argv) if argv is not None else sys.argv[1:]
|
|
36
|
+
if not args:
|
|
37
|
+
print(_HELP)
|
|
38
|
+
return 2
|
|
39
|
+
if args[0] in {"-h", "--help"}:
|
|
40
|
+
print(_HELP)
|
|
41
|
+
return 0
|
|
42
|
+
if args[0] in {"--version", "version"}:
|
|
43
|
+
print(_package_version("memgraph-context-graph"))
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
command = args[0]
|
|
47
|
+
if command == "bootstrap":
|
|
48
|
+
return _bootstrap(args[1:])
|
|
49
|
+
|
|
50
|
+
if command == "config":
|
|
51
|
+
return _config(args[1:])
|
|
52
|
+
|
|
53
|
+
if command == "doctor":
|
|
54
|
+
return _doctor(args[1:])
|
|
55
|
+
|
|
56
|
+
if command == "mcp":
|
|
57
|
+
return _mcp(args[1:])
|
|
58
|
+
|
|
59
|
+
if command == "recall":
|
|
60
|
+
return _recall(args[1:])
|
|
61
|
+
|
|
62
|
+
if command == "join":
|
|
63
|
+
return _join(args[1:])
|
|
64
|
+
|
|
65
|
+
if command == "hook":
|
|
66
|
+
from memgraph_context_graph.hooks.cli import main as hook_main
|
|
67
|
+
|
|
68
|
+
return hook_main(args[1:])
|
|
69
|
+
|
|
70
|
+
if command == "setup":
|
|
71
|
+
from memgraph_context_graph.hooks.cli import main as hook_main
|
|
72
|
+
|
|
73
|
+
return hook_main(["init", *args[1:]])
|
|
74
|
+
|
|
75
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
76
|
+
print(_HELP)
|
|
77
|
+
return 2
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _config(argv: list[str]) -> int:
|
|
81
|
+
"""Get or set persistent config values in ~/.config/context-graph/config.toml."""
|
|
82
|
+
from memgraph_context_graph.adapters._identity import (
|
|
83
|
+
FALSY_VALUES,
|
|
84
|
+
MEMORY_BACKENDS,
|
|
85
|
+
TRUTHY_VALUES,
|
|
86
|
+
config_file_path,
|
|
87
|
+
load_config,
|
|
88
|
+
parse_bool_flag,
|
|
89
|
+
write_config,
|
|
90
|
+
)
|
|
91
|
+
from memgraph_context_graph.spaces import EDITION_COMMUNITY, EDITIONS
|
|
92
|
+
|
|
93
|
+
config_path = config_file_path()
|
|
94
|
+
|
|
95
|
+
_CONFIG_KEYS = {
|
|
96
|
+
"edition": "edition",
|
|
97
|
+
"identity.user_id": "user_id",
|
|
98
|
+
"memgraph.url": "memgraph_url",
|
|
99
|
+
"memgraph.user": "memgraph_user",
|
|
100
|
+
"memgraph.password": "memgraph_password",
|
|
101
|
+
"memgraph.database": "memgraph_database",
|
|
102
|
+
"llm.openai_api_key": "openai_api_key",
|
|
103
|
+
"llm.anthropic_api_key": "anthropic_api_key",
|
|
104
|
+
"reconcile.auto_reconcile": "auto_reconcile",
|
|
105
|
+
"recall.embedding_model": "embedding_model",
|
|
106
|
+
"ontology.path": "ontology_path",
|
|
107
|
+
"ontology.derive": "ontology_derive",
|
|
108
|
+
"github.token": "github_token",
|
|
109
|
+
"memory.backend": "memory_backend",
|
|
110
|
+
}
|
|
111
|
+
_SECRET_KEYS = {"memgraph.password", "llm.openai_api_key", "llm.anthropic_api_key", "github.token"}
|
|
112
|
+
_BOOL_KEYS = {"reconcile.auto_reconcile"}
|
|
113
|
+
|
|
114
|
+
if not argv or argv[0] in {"-h", "--help"}:
|
|
115
|
+
print("usage: memgraph-context-graph config set <key> <value>")
|
|
116
|
+
print(" memgraph-context-graph config get <key>")
|
|
117
|
+
print(" memgraph-context-graph config show")
|
|
118
|
+
print("")
|
|
119
|
+
print("Supported keys:")
|
|
120
|
+
for key in _CONFIG_KEYS:
|
|
121
|
+
print(f" {key}")
|
|
122
|
+
print(f"\nConfig file: {config_path}")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
action = argv[0]
|
|
126
|
+
|
|
127
|
+
if action == "show":
|
|
128
|
+
config = load_config()
|
|
129
|
+
print(f"# {config_path}")
|
|
130
|
+
print(f"edition = {config.edition or EDITION_COMMUNITY!r}")
|
|
131
|
+
print(f"identity.user_id = {config.user_id!r}")
|
|
132
|
+
print(f"memgraph.url = {config.memgraph_url!r}")
|
|
133
|
+
print(f"memgraph.user = {config.memgraph_user!r}")
|
|
134
|
+
print(f"memgraph.password = {'***' if config.memgraph_password else repr('')}")
|
|
135
|
+
print(f"memgraph.database = {config.memgraph_database!r}")
|
|
136
|
+
print(f"llm.openai_api_key = {'***' if config.openai_api_key else repr('')}")
|
|
137
|
+
print(f"llm.anthropic_api_key = {'***' if config.anthropic_api_key else repr('')}")
|
|
138
|
+
if config.auto_reconcile is None:
|
|
139
|
+
print("reconcile.auto_reconcile = unset (defaults to false)")
|
|
140
|
+
else:
|
|
141
|
+
print(f"reconcile.auto_reconcile = {'true' if config.auto_reconcile else 'false'}")
|
|
142
|
+
if config.embedding_model is None:
|
|
143
|
+
print("recall.embedding_model = unset (defaults to sessions-graph's model)")
|
|
144
|
+
else:
|
|
145
|
+
print(f"recall.embedding_model = {config.embedding_model!r}")
|
|
146
|
+
print(f"ontology.path = {config.ontology_path!r}" if config.ontology_path else "ontology.path = unset")
|
|
147
|
+
if config.ontology_derive is None:
|
|
148
|
+
print("ontology.derive = unset (defaults to extend)")
|
|
149
|
+
else:
|
|
150
|
+
print(f"ontology.derive = {config.ontology_derive!r}")
|
|
151
|
+
print(f"github.token = {'***' if config.github_token else 'unset'}")
|
|
152
|
+
print(
|
|
153
|
+
f"memory.backend = {config.memory_backend!r}"
|
|
154
|
+
if config.memory_backend
|
|
155
|
+
else "memory.backend = unset (native)"
|
|
156
|
+
)
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
if action == "set":
|
|
160
|
+
if len(argv) < 2:
|
|
161
|
+
print("usage: memgraph-context-graph config set <key> <value>", file=sys.stderr)
|
|
162
|
+
return 2
|
|
163
|
+
key = argv[1]
|
|
164
|
+
if key not in _CONFIG_KEYS:
|
|
165
|
+
print(f"Unknown config key: {key}", file=sys.stderr)
|
|
166
|
+
print(f"Supported keys: {', '.join(_CONFIG_KEYS)}", file=sys.stderr)
|
|
167
|
+
return 2
|
|
168
|
+
|
|
169
|
+
# Secrets: read from stdin if value not provided.
|
|
170
|
+
if len(argv) < 3:
|
|
171
|
+
if key in _SECRET_KEYS:
|
|
172
|
+
import getpass
|
|
173
|
+
|
|
174
|
+
value = getpass.getpass(f"Enter {key}: ")
|
|
175
|
+
else:
|
|
176
|
+
print(f"usage: memgraph-context-graph config set {key} <value>", file=sys.stderr)
|
|
177
|
+
return 2
|
|
178
|
+
else:
|
|
179
|
+
value = argv[2]
|
|
180
|
+
|
|
181
|
+
write_value: str | bool = value
|
|
182
|
+
if key in _BOOL_KEYS:
|
|
183
|
+
normalized = value.strip().lower()
|
|
184
|
+
if normalized not in TRUTHY_VALUES | FALSY_VALUES:
|
|
185
|
+
print(f"Invalid value for {key}: {value!r} (expected true/false)", file=sys.stderr)
|
|
186
|
+
return 2
|
|
187
|
+
write_value = parse_bool_flag(normalized)
|
|
188
|
+
if key == "edition" and value not in EDITIONS:
|
|
189
|
+
print(f"Invalid value for {key}: {value!r} (expected {'/'.join(sorted(EDITIONS))})", file=sys.stderr)
|
|
190
|
+
return 2
|
|
191
|
+
if key == "memory.backend" and value not in MEMORY_BACKENDS:
|
|
192
|
+
print(f"Invalid value for {key}: {value!r} (expected {'/'.join(sorted(MEMORY_BACKENDS))})", file=sys.stderr)
|
|
193
|
+
return 2
|
|
194
|
+
if key == "ontology.derive" and value not in {"extend", "off"}:
|
|
195
|
+
print(f"Invalid value for {key}: {value!r} (expected extend/off)", file=sys.stderr)
|
|
196
|
+
return 2
|
|
197
|
+
|
|
198
|
+
# The key/value pairing is only known at runtime (bool iff key is in
|
|
199
|
+
# _BOOL_KEYS, str otherwise); write_config's own parameters are precisely
|
|
200
|
+
# typed per key, so the checker can't verify this dynamic dispatch itself.
|
|
201
|
+
write_config(**cast("dict[str, Any]", {_CONFIG_KEYS[key]: write_value}))
|
|
202
|
+
if key in _SECRET_KEYS:
|
|
203
|
+
display_value = "***"
|
|
204
|
+
elif key in _BOOL_KEYS:
|
|
205
|
+
display_value = str(write_value).lower()
|
|
206
|
+
else:
|
|
207
|
+
display_value = repr(write_value)
|
|
208
|
+
print(f"Wrote {key} = {display_value} to {config_path}")
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
if action == "get":
|
|
212
|
+
if len(argv) < 2:
|
|
213
|
+
print("usage: memgraph-context-graph config get <key>", file=sys.stderr)
|
|
214
|
+
return 2
|
|
215
|
+
key = argv[1]
|
|
216
|
+
if key not in _CONFIG_KEYS:
|
|
217
|
+
print(f"Unknown config key: {key}", file=sys.stderr)
|
|
218
|
+
return 2
|
|
219
|
+
|
|
220
|
+
config = load_config()
|
|
221
|
+
attr = _CONFIG_KEYS[key]
|
|
222
|
+
value = getattr(config, attr)
|
|
223
|
+
if value is None:
|
|
224
|
+
print(f"{key} is not set in {config_path}", file=sys.stderr)
|
|
225
|
+
return 1
|
|
226
|
+
# Don't print secrets to stdout unless explicitly requested.
|
|
227
|
+
if key in _SECRET_KEYS:
|
|
228
|
+
print("***")
|
|
229
|
+
else:
|
|
230
|
+
print(value)
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
print(f"Unknown config action: {action}", file=sys.stderr)
|
|
234
|
+
return 2
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _mcp(argv: list[str]) -> int:
|
|
238
|
+
argparse.ArgumentParser(
|
|
239
|
+
prog="memgraph-context-graph mcp", description="Serve the registered tools over stdio MCP."
|
|
240
|
+
).parse_args(argv)
|
|
241
|
+
try:
|
|
242
|
+
from memgraph_context_graph.mcp_server import serve
|
|
243
|
+
except ImportError as exc:
|
|
244
|
+
print(f"memgraph-context-graph mcp needs the mcp extra: uv tool install 'memgraph-context-graph[mcp]' ({exc})",
|
|
245
|
+
file=sys.stderr) # fmt: skip
|
|
246
|
+
return 1
|
|
247
|
+
return serve()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _recall(argv: list[str]) -> int:
|
|
251
|
+
from memgraph_context_graph.adapters._identity import load_config
|
|
252
|
+
from memgraph_context_graph.tools import ToolError, load_tools
|
|
253
|
+
|
|
254
|
+
parser = argparse.ArgumentParser(
|
|
255
|
+
prog="memgraph-context-graph recall", description="Search your own past sessions, as the recall tool does."
|
|
256
|
+
)
|
|
257
|
+
parser.add_argument("question", nargs="+", help="What to look for, as you would ask it.")
|
|
258
|
+
parser.add_argument("--json", action="store_true", help="Print the turns and facts as JSON.")
|
|
259
|
+
args = parser.parse_args(argv)
|
|
260
|
+
|
|
261
|
+
tool = load_tools().get("recall")
|
|
262
|
+
if tool is None:
|
|
263
|
+
print("No recall tool is installed: it comes with sessions-graph[memgraph-context-graph].", file=sys.stderr)
|
|
264
|
+
return 1
|
|
265
|
+
try:
|
|
266
|
+
result = tool.call({"question": " ".join(args.question)}, load_config())
|
|
267
|
+
except ToolError as exc:
|
|
268
|
+
print(str(exc), file=sys.stderr)
|
|
269
|
+
return 1
|
|
270
|
+
print(json.dumps(result.structured, indent=2) if args.json else result.text)
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _join(argv: list[str]) -> int:
|
|
275
|
+
"""Point the config file at an Enterprise Memgraph as one login, then prove the login works."""
|
|
276
|
+
import getpass
|
|
277
|
+
|
|
278
|
+
from memgraph_context_graph.adapters._identity import config_file_path, reload_config, write_config
|
|
279
|
+
from memgraph_context_graph.spaces import EDITION_ENTERPRISE, SpaceError, spaces_for
|
|
280
|
+
|
|
281
|
+
parser = argparse.ArgumentParser(
|
|
282
|
+
prog="memgraph-context-graph join", description="Use an Enterprise Memgraph's personal and shared spaces."
|
|
283
|
+
)
|
|
284
|
+
parser.add_argument("url", help="The Memgraph Bolt URL, e.g. bolt://memgraph.corp:7687.")
|
|
285
|
+
parser.add_argument("login", help="Your Memgraph login; it is also who you are in Context Graph.")
|
|
286
|
+
parser.add_argument("--password", default=None, help="Prompted for when omitted.")
|
|
287
|
+
args = parser.parse_args(argv)
|
|
288
|
+
password = args.password if args.password is not None else getpass.getpass(f"Password for {args.login}: ")
|
|
289
|
+
|
|
290
|
+
write_config(
|
|
291
|
+
edition=EDITION_ENTERPRISE, memgraph_url=args.url, memgraph_user=args.login, memgraph_password=password
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
spaces = spaces_for(reload_config())
|
|
295
|
+
from memgraph_toolbox.api.memgraph import Memgraph
|
|
296
|
+
|
|
297
|
+
client = Memgraph(**spaces.personal().target.client_kwargs())
|
|
298
|
+
try:
|
|
299
|
+
(row,) = client.query("SHOW CURRENT DATABASE")
|
|
300
|
+
finally:
|
|
301
|
+
client.close()
|
|
302
|
+
shared = [space.name for space in spaces.readable() if space.shared]
|
|
303
|
+
except (SpaceError, ValueError) as exc:
|
|
304
|
+
print(f"Wrote {config_file_path()}, but the login doesn't work yet: {exc}", file=sys.stderr)
|
|
305
|
+
return 1
|
|
306
|
+
print(f"Wrote {config_file_path()}: edition = enterprise, login {args.login!r}.")
|
|
307
|
+
print(f"Personal space: {row['Current']}. Shared spaces: {', '.join(shared) or 'none yet'}.")
|
|
308
|
+
print("Next: memgraph-context-graph setup <runtime> (e.g. claude-code) if this machine isn't set up yet.")
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _bootstrap(argv: list[str]) -> int:
|
|
313
|
+
from memgraph_context_graph.hooks.runtime_plugin import load_runtime_plugins
|
|
314
|
+
|
|
315
|
+
parser = argparse.ArgumentParser(description="Install and verify Memgraph Context Graph runtime dependencies.")
|
|
316
|
+
parser.add_argument(
|
|
317
|
+
"--runtime",
|
|
318
|
+
choices=sorted(load_runtime_plugins()),
|
|
319
|
+
required=True,
|
|
320
|
+
help="Runtime hook adapter to bootstrap.",
|
|
321
|
+
)
|
|
322
|
+
parser.add_argument(
|
|
323
|
+
"--connector",
|
|
324
|
+
action="append",
|
|
325
|
+
default=None,
|
|
326
|
+
help="Graph connector to enable. Defaults to skills-graph.",
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument(
|
|
329
|
+
"--memgraph-host",
|
|
330
|
+
default=None,
|
|
331
|
+
help="Memgraph host. Defaults to MEMGRAPH_HOST or localhost.",
|
|
332
|
+
)
|
|
333
|
+
parser.add_argument(
|
|
334
|
+
"--memgraph-port",
|
|
335
|
+
type=int,
|
|
336
|
+
default=None,
|
|
337
|
+
help="Memgraph Bolt port. Defaults to MEMGRAPH_PORT or 7687.",
|
|
338
|
+
)
|
|
339
|
+
parser.add_argument(
|
|
340
|
+
"--no-reinstall",
|
|
341
|
+
action="store_true",
|
|
342
|
+
help="Do not pass --reinstall to uv tool install.",
|
|
343
|
+
)
|
|
344
|
+
args = parser.parse_args(argv)
|
|
345
|
+
|
|
346
|
+
connectors = args.connector or ["skills-graph"]
|
|
347
|
+
# Resolve Memgraph URL: MEMGRAPH_URL env takes priority (supports neo4j:// for HA).
|
|
348
|
+
# Falls back to host:port construction for backward compatibility.
|
|
349
|
+
memgraph_url_env = os.environ.get("MEMGRAPH_URL")
|
|
350
|
+
if memgraph_url_env:
|
|
351
|
+
memgraph_url = memgraph_url_env
|
|
352
|
+
# For connectivity check, we still need host:port — skip if using neo4j:// routing.
|
|
353
|
+
from urllib.parse import urlparse
|
|
354
|
+
|
|
355
|
+
parsed = urlparse(memgraph_url)
|
|
356
|
+
host = parsed.hostname or "localhost"
|
|
357
|
+
port = parsed.port or 7687
|
|
358
|
+
else:
|
|
359
|
+
host = args.memgraph_host if args.memgraph_host is not None else os.environ.get("MEMGRAPH_HOST", "localhost")
|
|
360
|
+
port = args.memgraph_port if args.memgraph_port is not None else int(os.environ.get("MEMGRAPH_PORT", "7687"))
|
|
361
|
+
memgraph_url = f"bolt://{host}:{port}"
|
|
362
|
+
|
|
363
|
+
uv = shutil.which("uv")
|
|
364
|
+
if not uv:
|
|
365
|
+
print("FAIL uv: not found on PATH", file=sys.stderr)
|
|
366
|
+
print("Install uv first:", file=sys.stderr)
|
|
367
|
+
print(" curl -LsSf https://astral.sh/uv/install.sh | sh", file=sys.stderr)
|
|
368
|
+
print(
|
|
369
|
+
"uv manages Python for the tool. If uv-managed Python downloads are blocked, install Python 3.10+.",
|
|
370
|
+
file=sys.stderr,
|
|
371
|
+
)
|
|
372
|
+
return 1
|
|
373
|
+
|
|
374
|
+
if not _memgraph_reachable(host, port):
|
|
375
|
+
print(f"FAIL memgraph: bolt://{host}:{port} is not reachable", file=sys.stderr)
|
|
376
|
+
print("Start Memgraph, then rerun bootstrap:", file=sys.stderr)
|
|
377
|
+
print(f" {_memgraph_command(port)}", file=sys.stderr)
|
|
378
|
+
print("Or point bootstrap at an existing instance:", file=sys.stderr)
|
|
379
|
+
print(
|
|
380
|
+
f" memgraph-context-graph bootstrap --runtime {args.runtime} "
|
|
381
|
+
f"--connector {' --connector '.join(connectors)} --memgraph-host <host> --memgraph-port <port>",
|
|
382
|
+
file=sys.stderr,
|
|
383
|
+
)
|
|
384
|
+
return 1
|
|
385
|
+
|
|
386
|
+
print(f"OK uv: {uv}")
|
|
387
|
+
print(f"OK memgraph: bolt://{host}:{port} reachable")
|
|
388
|
+
|
|
389
|
+
install_cmd = [uv, "tool", "install", "memgraph-context-graph[mcp]"]
|
|
390
|
+
for connector in connectors:
|
|
391
|
+
requirement = _connector_requirement(connector)
|
|
392
|
+
if requirement is None:
|
|
393
|
+
print(f"FAIL connector:{connector}: unsupported connector", file=sys.stderr)
|
|
394
|
+
return 1
|
|
395
|
+
install_cmd.extend(["--with", requirement])
|
|
396
|
+
if not args.no_reinstall:
|
|
397
|
+
install_cmd.append("--reinstall")
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
subprocess.run(install_cmd, check=True)
|
|
401
|
+
except subprocess.CalledProcessError as exc:
|
|
402
|
+
print(f"FAIL install: uv tool install exited with status {exc.returncode}", file=sys.stderr)
|
|
403
|
+
return exc.returncode
|
|
404
|
+
|
|
405
|
+
executable = shutil.which("memgraph-context-graph")
|
|
406
|
+
if not executable:
|
|
407
|
+
print("FAIL memgraph-context-graph: installed but not on PATH", file=sys.stderr)
|
|
408
|
+
print("uv usually installs tools into ~/.local/bin.", file=sys.stderr)
|
|
409
|
+
print("Add that directory to PATH, restart your shell, then rerun doctor.", file=sys.stderr)
|
|
410
|
+
return 1
|
|
411
|
+
|
|
412
|
+
print(f"OK memgraph-context-graph executable: {executable}")
|
|
413
|
+
|
|
414
|
+
# Write hook configuration file (auto-write + inform).
|
|
415
|
+
from memgraph_context_graph.adapters._identity import write_full_config
|
|
416
|
+
|
|
417
|
+
user_id = os.environ.get("MEMGRAPH_CONTEXT_GRAPH_USER_ID", "")
|
|
418
|
+
memgraph_user = os.environ.get("MEMGRAPH_USER", "")
|
|
419
|
+
memgraph_password = os.environ.get("MEMGRAPH_PASSWORD", "")
|
|
420
|
+
memgraph_database = os.environ.get("MEMGRAPH_DATABASE", "memgraph")
|
|
421
|
+
openai_api_key = os.environ.get("OPENAI_API_KEY", "")
|
|
422
|
+
anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
423
|
+
|
|
424
|
+
# auto_reconcile is deliberately NOT captured from SESSIONS_GRAPH_AUTO_RECONCILE
|
|
425
|
+
# here, unlike the keys above: those have a legitimate ambient-env source
|
|
426
|
+
# (people already have OPENAI_API_KEY/MEMGRAPH_PASSWORD exported for other
|
|
427
|
+
# tools), but nobody has this env var set for an unrelated reason -- it's
|
|
428
|
+
# only ever set via `config set reconcile.auto_reconcile`. write_full_config
|
|
429
|
+
# preserves the existing value when auto_reconcile isn't passed, so
|
|
430
|
+
# re-running bootstrap can't silently revert it to off.
|
|
431
|
+
config_path = write_full_config(
|
|
432
|
+
user_id=user_id,
|
|
433
|
+
memgraph_url=memgraph_url,
|
|
434
|
+
memgraph_user=memgraph_user,
|
|
435
|
+
memgraph_password=memgraph_password,
|
|
436
|
+
memgraph_database=memgraph_database,
|
|
437
|
+
openai_api_key=openai_api_key,
|
|
438
|
+
anthropic_api_key=anthropic_api_key,
|
|
439
|
+
)
|
|
440
|
+
print(f"OK config: wrote {config_path}")
|
|
441
|
+
if user_id:
|
|
442
|
+
print(f" identity.user_id = {user_id!r}")
|
|
443
|
+
else:
|
|
444
|
+
print(
|
|
445
|
+
" identity.user_id is empty — set it with: memgraph-context-graph config set identity.user_id <your-name>"
|
|
446
|
+
)
|
|
447
|
+
print(f" memgraph.url = {memgraph_url!r}")
|
|
448
|
+
if openai_api_key or anthropic_api_key:
|
|
449
|
+
found = ", ".join(
|
|
450
|
+
name
|
|
451
|
+
for name, value in (("llm.openai_api_key", openai_api_key), ("llm.anthropic_api_key", anthropic_api_key))
|
|
452
|
+
if value
|
|
453
|
+
)
|
|
454
|
+
print(f" {found} captured from environment")
|
|
455
|
+
else:
|
|
456
|
+
print(
|
|
457
|
+
" no LLM API key found in environment — sessions-graph reconciliation needs one; "
|
|
458
|
+
"set with: memgraph-context-graph config set llm.openai_api_key"
|
|
459
|
+
)
|
|
460
|
+
doctor_args = ["--runtime", args.runtime]
|
|
461
|
+
for connector in connectors:
|
|
462
|
+
doctor_args.extend(["--connector", connector])
|
|
463
|
+
return _doctor(doctor_args)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _doctor(argv: list[str]) -> int:
|
|
467
|
+
from memgraph_context_graph.hooks.runtime_plugin import load_runtime_plugins
|
|
468
|
+
|
|
469
|
+
parser = argparse.ArgumentParser(description="Check Memgraph Context Graph runtime hook setup.")
|
|
470
|
+
parser.add_argument(
|
|
471
|
+
"--runtime",
|
|
472
|
+
choices=sorted(load_runtime_plugins()),
|
|
473
|
+
default="claude-code",
|
|
474
|
+
help="Runtime hook adapter to smoke test.",
|
|
475
|
+
)
|
|
476
|
+
parser.add_argument(
|
|
477
|
+
"--connector",
|
|
478
|
+
action="append",
|
|
479
|
+
default=None,
|
|
480
|
+
help="Graph connector to enable. Defaults to skills-graph.",
|
|
481
|
+
)
|
|
482
|
+
parser.add_argument(
|
|
483
|
+
"--json",
|
|
484
|
+
action="store_true",
|
|
485
|
+
help="Print machine-readable JSON.",
|
|
486
|
+
)
|
|
487
|
+
args = parser.parse_args(argv)
|
|
488
|
+
|
|
489
|
+
connectors = args.connector or ["skills-graph"]
|
|
490
|
+
checks = [
|
|
491
|
+
_check_cli(),
|
|
492
|
+
_check_package("memgraph-context-graph"),
|
|
493
|
+
_check_config(),
|
|
494
|
+
]
|
|
495
|
+
# Every later check connects through the edition's space resolver; when the config can't
|
|
496
|
+
# produce one, the config check already says why, and the rest would only repeat it.
|
|
497
|
+
if _edition_resolves():
|
|
498
|
+
checks.append(_check_memgraph())
|
|
499
|
+
for connector in connectors:
|
|
500
|
+
checks.append(_check_connector(connector))
|
|
501
|
+
if any(connector.strip().replace("_", "-") == "sessions-graph" for connector in connectors):
|
|
502
|
+
checks.append(_check_embeddings())
|
|
503
|
+
checks.append(_check_mcp())
|
|
504
|
+
checks.append(_check_runtime(args.runtime, connectors))
|
|
505
|
+
if (memory_check := _check_memory_backend(args.runtime)) is not None:
|
|
506
|
+
checks.append(memory_check)
|
|
507
|
+
|
|
508
|
+
ok = all(check["ok"] for check in checks)
|
|
509
|
+
payload: _DoctorPayload = {"ok": ok, "checks": checks}
|
|
510
|
+
if args.json:
|
|
511
|
+
print(json.dumps(payload, indent=2))
|
|
512
|
+
else:
|
|
513
|
+
_print_doctor(payload)
|
|
514
|
+
return 0 if ok else 1
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class _CheckResult(TypedDict):
|
|
518
|
+
name: str
|
|
519
|
+
ok: bool
|
|
520
|
+
detail: str
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class _DoctorPayload(TypedDict):
|
|
524
|
+
ok: bool
|
|
525
|
+
checks: list[_CheckResult]
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _edition_resolves() -> bool:
|
|
529
|
+
from memgraph_context_graph.adapters._identity import load_config
|
|
530
|
+
from memgraph_context_graph.spaces import SpaceError, spaces_for
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
spaces_for(load_config())
|
|
534
|
+
except SpaceError:
|
|
535
|
+
return False
|
|
536
|
+
return True
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _check_config() -> _CheckResult:
|
|
540
|
+
from memgraph_context_graph.adapters._identity import config_file_path, load_config
|
|
541
|
+
|
|
542
|
+
path = config_file_path()
|
|
543
|
+
if not path.is_file():
|
|
544
|
+
return {
|
|
545
|
+
"name": "config",
|
|
546
|
+
"ok": False,
|
|
547
|
+
"detail": f"{path} not found — run: memgraph-context-graph bootstrap or memgraph-context-graph config set identity.user_id <name>",
|
|
548
|
+
}
|
|
549
|
+
from memgraph_context_graph.spaces import SpaceError, spaces_for
|
|
550
|
+
|
|
551
|
+
config = load_config()
|
|
552
|
+
try:
|
|
553
|
+
spaces = spaces_for(config)
|
|
554
|
+
except SpaceError as exc:
|
|
555
|
+
return {"name": "config", "ok": False, "detail": f"{path} — {exc}"}
|
|
556
|
+
user_id = spaces.user_id()
|
|
557
|
+
parts = [f"edition={spaces.edition}"]
|
|
558
|
+
if user_id:
|
|
559
|
+
parts.append(f"user_id={user_id!r}")
|
|
560
|
+
else:
|
|
561
|
+
parts.append("user_id=NOT SET")
|
|
562
|
+
parts.append(f"memgraph.url={spaces.personal().target.url!r}")
|
|
563
|
+
llm_key_set = bool(config.openai_api_key or config.anthropic_api_key)
|
|
564
|
+
parts.append(
|
|
565
|
+
"llm_key=SET" if llm_key_set else "llm_key=NOT SET (needed only for sessions-graph auto-reconciliation)"
|
|
566
|
+
)
|
|
567
|
+
auto_reconcile_on = bool(config.auto_reconcile)
|
|
568
|
+
parts.append(f"auto_reconcile={'ON' if auto_reconcile_on else 'OFF'}")
|
|
569
|
+
broken_auto_reconcile = auto_reconcile_on and not llm_key_set
|
|
570
|
+
if broken_auto_reconcile:
|
|
571
|
+
parts.append("WARNING: auto_reconcile is ON but no LLM key is set — reconciliation will fail")
|
|
572
|
+
ok = bool(user_id) and not broken_auto_reconcile
|
|
573
|
+
return {"name": "config", "ok": ok, "detail": f"{path} — {'; '.join(parts)}"}
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _check_memgraph() -> _CheckResult:
|
|
577
|
+
from memgraph_context_graph.adapters._identity import resolve_memgraph_env
|
|
578
|
+
|
|
579
|
+
env = resolve_memgraph_env()
|
|
580
|
+
url = env["MEMGRAPH_URL"]
|
|
581
|
+
try:
|
|
582
|
+
from memgraph_toolbox.api.memgraph import Memgraph
|
|
583
|
+
|
|
584
|
+
db = Memgraph(
|
|
585
|
+
url=env["MEMGRAPH_URL"],
|
|
586
|
+
username=env["MEMGRAPH_USER"],
|
|
587
|
+
password=env["MEMGRAPH_PASSWORD"],
|
|
588
|
+
database=env["MEMGRAPH_DATABASE"],
|
|
589
|
+
)
|
|
590
|
+
db.query("RETURN 1")
|
|
591
|
+
return {"name": "memgraph", "ok": True, "detail": f"{url} reachable"}
|
|
592
|
+
except Exception as exc:
|
|
593
|
+
return {"name": "memgraph", "ok": False, "detail": f"{url} — {type(exc).__name__}: {exc}"}
|
|
594
|
+
finally:
|
|
595
|
+
driver = getattr(getattr(locals().get("db", None), "driver", None), "driver", None)
|
|
596
|
+
if driver is not None:
|
|
597
|
+
driver.close()
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _check_cli() -> _CheckResult:
|
|
601
|
+
path = shutil.which("memgraph-context-graph") or sys.argv[0]
|
|
602
|
+
return {"name": "memgraph-context-graph executable", "ok": bool(path), "detail": path or "not found"}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _check_package(package_name: str) -> _CheckResult:
|
|
606
|
+
version = _package_version(package_name)
|
|
607
|
+
ok = version != "unknown"
|
|
608
|
+
return {"name": package_name, "ok": ok, "detail": version if ok else "not installed"}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _check_connector(connector_name: str) -> _CheckResult:
|
|
612
|
+
normalized = connector_name.strip().replace("-", "_")
|
|
613
|
+
if normalized == "skills_graph":
|
|
614
|
+
try:
|
|
615
|
+
from skills_graph import SkillGraph
|
|
616
|
+
|
|
617
|
+
graph = SkillGraph()
|
|
618
|
+
version = _package_version("skills-graph")
|
|
619
|
+
return {"name": "connector:skills-graph", "ok": True, "detail": f"installed={version}; memgraph=reachable"}
|
|
620
|
+
except Exception as exc:
|
|
621
|
+
return {"name": "connector:skills-graph", "ok": False, "detail": f"{type(exc).__name__}: {exc}"}
|
|
622
|
+
finally:
|
|
623
|
+
driver = getattr(getattr(locals().get("graph", None), "_db", None), "driver", None)
|
|
624
|
+
if driver is not None:
|
|
625
|
+
driver.close()
|
|
626
|
+
|
|
627
|
+
if normalized == "actions_graph":
|
|
628
|
+
try:
|
|
629
|
+
from actions_graph import ActionsGraph
|
|
630
|
+
|
|
631
|
+
graph = ActionsGraph()
|
|
632
|
+
version = _package_version("actions-graph")
|
|
633
|
+
return {"name": "connector:actions-graph", "ok": True, "detail": f"installed={version}; memgraph=reachable"}
|
|
634
|
+
except Exception as exc:
|
|
635
|
+
return {"name": "connector:actions-graph", "ok": False, "detail": f"{type(exc).__name__}: {exc}"}
|
|
636
|
+
finally:
|
|
637
|
+
driver = getattr(getattr(locals().get("graph", None), "_db", None), "driver", None)
|
|
638
|
+
if driver is not None:
|
|
639
|
+
driver.close()
|
|
640
|
+
|
|
641
|
+
if normalized == "sessions_graph":
|
|
642
|
+
try:
|
|
643
|
+
from sessions_graph import SessionsGraph
|
|
644
|
+
|
|
645
|
+
from memgraph_context_graph.adapters._identity import resolve_memgraph_env, resolve_user_id
|
|
646
|
+
|
|
647
|
+
env = resolve_memgraph_env()
|
|
648
|
+
graph = SessionsGraph(
|
|
649
|
+
url=env["MEMGRAPH_URL"],
|
|
650
|
+
username=env["MEMGRAPH_USER"],
|
|
651
|
+
password=env["MEMGRAPH_PASSWORD"],
|
|
652
|
+
database=env["MEMGRAPH_DATABASE"],
|
|
653
|
+
)
|
|
654
|
+
version = _package_version("sessions-graph")
|
|
655
|
+
user_id = resolve_user_id({})
|
|
656
|
+
if not user_id:
|
|
657
|
+
return {
|
|
658
|
+
"name": "connector:sessions-graph",
|
|
659
|
+
"ok": False,
|
|
660
|
+
"detail": "user_id not resolved — run: memgraph-context-graph config set identity.user_id <your-name>",
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
"name": "connector:sessions-graph",
|
|
664
|
+
"ok": True,
|
|
665
|
+
"detail": f"installed={version}; memgraph=reachable; user_id={user_id!r}",
|
|
666
|
+
}
|
|
667
|
+
except Exception as exc:
|
|
668
|
+
return {"name": "connector:sessions-graph", "ok": False, "detail": f"{type(exc).__name__}: {exc}"}
|
|
669
|
+
finally:
|
|
670
|
+
driver = getattr(getattr(locals().get("graph", None), "_db", None), "driver", None)
|
|
671
|
+
if driver is not None:
|
|
672
|
+
driver.close()
|
|
673
|
+
|
|
674
|
+
if normalized == "resources_graph":
|
|
675
|
+
try:
|
|
676
|
+
from memgraph_context_graph.adapters._identity import load_config, resolve_memgraph_env
|
|
677
|
+
from resources_graph import ResourcesGraph
|
|
678
|
+
|
|
679
|
+
env = resolve_memgraph_env()
|
|
680
|
+
graph = ResourcesGraph(
|
|
681
|
+
url=env["MEMGRAPH_URL"],
|
|
682
|
+
username=env["MEMGRAPH_USER"],
|
|
683
|
+
password=env["MEMGRAPH_PASSWORD"],
|
|
684
|
+
database=env["MEMGRAPH_DATABASE"],
|
|
685
|
+
)
|
|
686
|
+
version = _package_version("resources-graph")
|
|
687
|
+
# Touches are recorded without a token; the Sweep falls back to the user's gh login.
|
|
688
|
+
token = "set" if load_config().github_token else "unset (the Sweep uses the gh login)"
|
|
689
|
+
return {
|
|
690
|
+
"name": "connector:resources-graph",
|
|
691
|
+
"ok": True,
|
|
692
|
+
"detail": f"installed={version}; memgraph=reachable; github.token {token}",
|
|
693
|
+
}
|
|
694
|
+
except Exception as exc:
|
|
695
|
+
return {"name": "connector:resources-graph", "ok": False, "detail": f"{type(exc).__name__}: {exc}"}
|
|
696
|
+
finally:
|
|
697
|
+
driver = getattr(getattr(locals().get("graph", None), "_db", None), "driver", None)
|
|
698
|
+
if driver is not None:
|
|
699
|
+
driver.close()
|
|
700
|
+
|
|
701
|
+
else:
|
|
702
|
+
return {"name": f"connector:{connector_name}", "ok": False, "detail": "unsupported connector"}
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _check_embeddings() -> _CheckResult:
|
|
706
|
+
"""Whether Memgraph can embed for recall: MAGE's ``embeddings`` module, with the configured model loading.
|
|
707
|
+
|
|
708
|
+
The first run downloads the model inside Memgraph, so this can take a while once.
|
|
709
|
+
"""
|
|
710
|
+
from memgraph_context_graph.adapters._identity import resolve_embedding_model, resolve_memgraph_env
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
from sessions_graph.embeddings import DEFAULT_EMBEDDING_MODEL, check_available
|
|
714
|
+
|
|
715
|
+
from memgraph_toolbox.api.memgraph import Memgraph
|
|
716
|
+
except ImportError as exc:
|
|
717
|
+
return {"name": "embeddings", "ok": False, "detail": f"{type(exc).__name__}: {exc}"}
|
|
718
|
+
|
|
719
|
+
model = resolve_embedding_model() or DEFAULT_EMBEDDING_MODEL
|
|
720
|
+
env = resolve_memgraph_env()
|
|
721
|
+
db = None
|
|
722
|
+
try:
|
|
723
|
+
db = Memgraph(
|
|
724
|
+
url=env["MEMGRAPH_URL"],
|
|
725
|
+
username=env["MEMGRAPH_USER"],
|
|
726
|
+
password=env["MEMGRAPH_PASSWORD"],
|
|
727
|
+
database=env["MEMGRAPH_DATABASE"],
|
|
728
|
+
)
|
|
729
|
+
dimension = check_available(db, model)
|
|
730
|
+
return {"name": "embeddings", "ok": True, "detail": f"{model} ({dimension} dimensions) inside Memgraph"}
|
|
731
|
+
except Exception as exc:
|
|
732
|
+
return {
|
|
733
|
+
"name": "embeddings",
|
|
734
|
+
"ok": False,
|
|
735
|
+
"detail": f"{model} — {exc}. Recall needs Memgraph with MAGE (memgraph/memgraph-mage, 2 GiB or more); "
|
|
736
|
+
"without it, recall's vector lanes are off",
|
|
737
|
+
}
|
|
738
|
+
finally:
|
|
739
|
+
driver = getattr(getattr(db, "driver", None), "driver", None)
|
|
740
|
+
if driver is not None:
|
|
741
|
+
driver.close()
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _check_mcp() -> _CheckResult:
|
|
745
|
+
"""Whether ``memgraph-context-graph mcp`` can start and serves recall."""
|
|
746
|
+
from memgraph_context_graph.tools import load_tools
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
import mcp # noqa: F401 -- only whether the extra is installed
|
|
750
|
+
except ImportError:
|
|
751
|
+
return {
|
|
752
|
+
"name": "mcp",
|
|
753
|
+
"ok": False,
|
|
754
|
+
"detail": "the mcp extra is missing, so the recall tool can't be served — "
|
|
755
|
+
"run: uv tool install 'memgraph-context-graph[mcp]' --with 'sessions-graph[memgraph-context-graph]'",
|
|
756
|
+
}
|
|
757
|
+
names = sorted(load_tools())
|
|
758
|
+
if "recall" not in names:
|
|
759
|
+
return {"name": "mcp", "ok": False, "detail": f"no recall tool registered (tools: {names or 'none'})"}
|
|
760
|
+
return {"name": "mcp", "ok": True, "detail": f"serves {', '.join(names)}"}
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _check_memory_backend(runtime: str) -> _CheckResult | None:
|
|
764
|
+
"""Which memory the runtime's model uses; fails when both are on. None for runtimes that can't switch."""
|
|
765
|
+
from pathlib import Path
|
|
766
|
+
|
|
767
|
+
from memgraph_context_graph.adapters._identity import load_config
|
|
768
|
+
from memgraph_context_graph.hooks.runtime_plugin import get_runtime_plugin
|
|
769
|
+
from memgraph_context_graph.memory_backend import MemoryBackendError, native_memory_of
|
|
770
|
+
|
|
771
|
+
native = native_memory_of(get_runtime_plugin(runtime))
|
|
772
|
+
if native is None:
|
|
773
|
+
return None
|
|
774
|
+
try:
|
|
775
|
+
native_on = native.enabled(Path.home())
|
|
776
|
+
except MemoryBackendError as exc:
|
|
777
|
+
return {"name": "memory", "ok": False, "detail": str(exc)}
|
|
778
|
+
if not load_config().graph_memory:
|
|
779
|
+
return {"name": "memory", "ok": True, "detail": f"{runtime}'s built-in memory (memory.backend native)"}
|
|
780
|
+
if native_on:
|
|
781
|
+
return {
|
|
782
|
+
"name": "memory",
|
|
783
|
+
"ok": False,
|
|
784
|
+
"detail": f"memory.backend is context-graph but {runtime}'s built-in memory is still on, so the model "
|
|
785
|
+
f"has two memories — run: memgraph-context-graph setup {runtime} --memory-backend context-graph",
|
|
786
|
+
}
|
|
787
|
+
return {"name": "memory", "ok": True, "detail": f"Context Graph; {runtime}'s built-in memory is off"}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _check_runtime(runtime: str, connectors: list[str]) -> _CheckResult:
|
|
791
|
+
try:
|
|
792
|
+
from memgraph_context_graph.hooks.runner import create_link
|
|
793
|
+
from memgraph_context_graph.hooks.runtime_plugin import get_runtime_plugin
|
|
794
|
+
|
|
795
|
+
plugin = get_runtime_plugin(runtime)
|
|
796
|
+
link = create_link(connectors)
|
|
797
|
+
adapter = plugin.adapter_class(link)
|
|
798
|
+
adapter.handle_payload(dict(plugin.probe_payload))
|
|
799
|
+
return {"name": f"runtime:{runtime}", "ok": True, "detail": "strict hook smoke passed"}
|
|
800
|
+
except Exception as exc:
|
|
801
|
+
return {"name": f"runtime:{runtime}", "ok": False, "detail": f"{type(exc).__name__}: {exc}"}
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _print_doctor(payload: _DoctorPayload) -> None:
|
|
805
|
+
for check in payload["checks"]:
|
|
806
|
+
status = "OK" if check["ok"] else "FAIL"
|
|
807
|
+
print(f"{status} {check['name']}: {check['detail']}")
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _package_version(package_name: str) -> str:
|
|
811
|
+
try:
|
|
812
|
+
return metadata.version(package_name)
|
|
813
|
+
except metadata.PackageNotFoundError:
|
|
814
|
+
return "unknown"
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _memgraph_reachable(host: str, port: int) -> bool:
|
|
818
|
+
try:
|
|
819
|
+
with socket.create_connection((host, port), timeout=2):
|
|
820
|
+
return True
|
|
821
|
+
except OSError:
|
|
822
|
+
return False
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _memgraph_command(port: int) -> str:
|
|
826
|
+
published = f"{port}:7687"
|
|
827
|
+
# MAGE, not plain memgraph: sessions-graph embeds for recall with its embeddings module.
|
|
828
|
+
return f"docker run --rm -p {published} memgraph/memgraph-mage"
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _connector_requirement(connector: str) -> str | None:
|
|
832
|
+
normalized = connector.strip().replace("_", "-")
|
|
833
|
+
if normalized == "skills-graph":
|
|
834
|
+
return "skills-graph[memgraph-context-graph]"
|
|
835
|
+
if normalized == "actions-graph":
|
|
836
|
+
return "actions-graph[memgraph-context-graph]"
|
|
837
|
+
if normalized == "sessions-graph":
|
|
838
|
+
return "sessions-graph[memgraph-context-graph]"
|
|
839
|
+
if normalized == "resources-graph":
|
|
840
|
+
return "resources-graph[memgraph-context-graph]"
|
|
841
|
+
return None
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
if __name__ == "__main__":
|
|
845
|
+
raise SystemExit(main())
|