dbx-tools-graphiti 0.6.111__tar.gz
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.
- dbx_tools_graphiti-0.6.111/PKG-INFO +119 -0
- dbx_tools_graphiti-0.6.111/README.md +111 -0
- dbx_tools_graphiti-0.6.111/pyproject.toml +22 -0
- dbx_tools_graphiti-0.6.111/pyproject.toml.orig +24 -0
- dbx_tools_graphiti-0.6.111/src/dbx_tools/graphiti/__init__.py +5 -0
- dbx_tools_graphiti-0.6.111/src/dbx_tools/graphiti/__main__.py +5 -0
- dbx_tools_graphiti-0.6.111/src/dbx_tools/graphiti/cli.py +51 -0
- dbx_tools_graphiti-0.6.111/src/dbx_tools/graphiti/config.yaml +41 -0
- dbx_tools_graphiti-0.6.111/src/dbx_tools/graphiti/runtime.py +318 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dbx-tools-graphiti
|
|
3
|
+
Version: 0.6.111
|
|
4
|
+
Summary: Native Graphiti MCP and Neo4j launcher with mise-managed prerequisites
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Project-URL: Source, https://github.com/reggie-db/dbx-tools/tree/main/packages/py/graphiti
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# `dbx-tools-graphiti`
|
|
10
|
+
|
|
11
|
+
Native launcher for [Graphiti](https://github.com/getzep/graphiti) with a local
|
|
12
|
+
Neo4j backend. It runs both services directly as host processes; it does not use
|
|
13
|
+
Docker, Podman, or another container runtime.
|
|
14
|
+
|
|
15
|
+
Install from PyPI:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add dbx-tools-graphiti
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install the current `main` branch:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv add "dbx-tools-graphiti @ git+https://github.com/reggie-db/dbx-tools.git@main#subdirectory=packages/py/graphiti"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Key features
|
|
28
|
+
|
|
29
|
+
- launches upstream Graphiti's HTTP MCP server at `http://127.0.0.1:8000/mcp/`;
|
|
30
|
+
- runs Neo4j Community 5.26 as a native background process;
|
|
31
|
+
- provisions Java 21 and `uv` through `mise use -g` only when absent;
|
|
32
|
+
- pins Graphiti and Neo4j versions for repeatable local environments;
|
|
33
|
+
- caches downloads, Python dependencies, Neo4j data, credentials, and logs;
|
|
34
|
+
- supports foreground or background operation without vendoring Graphiti code.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
`mise` must already be installed. The launcher handles Java and `uv` itself.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export OPENAI_API_KEY=...
|
|
42
|
+
uv run dbx-graphiti start
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The first run downloads about 120 MB of Neo4j plus the pinned Graphiti release,
|
|
46
|
+
creates Graphiti's `uv` environment, generates a local Neo4j password, starts
|
|
47
|
+
Neo4j, and then runs Graphiti in the foreground. Later runs reuse all of it.
|
|
48
|
+
|
|
49
|
+
For background operation:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run dbx-graphiti up
|
|
53
|
+
uv run dbx-graphiti status
|
|
54
|
+
uv run dbx-graphiti down
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| Command | Behavior |
|
|
60
|
+
| -------- | ----------------------------------------------------------------------------- |
|
|
61
|
+
| `setup` | Provision tools and populate the local cache without starting services. |
|
|
62
|
+
| `start` | Start Neo4j, then run Graphiti in the foreground. This is the default. |
|
|
63
|
+
| `up` | Start both services in the background. |
|
|
64
|
+
| `down` | Stop the managed Graphiti and Neo4j processes. |
|
|
65
|
+
| `status` | Print process state and the MCP URL as JSON. |
|
|
66
|
+
| `env` | Print resolved Neo4j connection settings as JSON. Treat its output as secret. |
|
|
67
|
+
|
|
68
|
+
Arguments after `start` or `up` are forwarded to upstream Graphiti. For example:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run dbx-graphiti start -- --port 9000 --group-id my-agent
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Provisioning and caching
|
|
75
|
+
|
|
76
|
+
The package deliberately keeps orchestration separate from Graphiti itself:
|
|
77
|
+
|
|
78
|
+
1. It checks `mise where java@21` and `mise where uv@0.11`.
|
|
79
|
+
2. A missing tool is installed globally with `mise use -g --yes`.
|
|
80
|
+
3. Neo4j Community `5.26.12` is downloaded from `dist.neo4j.org` and unpacked.
|
|
81
|
+
4. Graphiti `v0.29.3` is downloaded from its GitHub release tag.
|
|
82
|
+
5. `uv sync --project <checkout>/mcp_server` creates the upstream environment.
|
|
83
|
+
6. A generated Neo4j password is stored with mode `0600` and supplied to
|
|
84
|
+
Graphiti through its documented environment variables.
|
|
85
|
+
|
|
86
|
+
The cache root is:
|
|
87
|
+
|
|
88
|
+
- macOS: `~/Library/Application Support/dbx-tools/graphiti`
|
|
89
|
+
- Linux: `${XDG_DATA_HOME:-~/.local/share}/dbx-tools/graphiti`
|
|
90
|
+
- Windows: `%LOCALAPPDATA%/dbx-tools/graphiti`
|
|
91
|
+
|
|
92
|
+
Set `DBX_GRAPHITI_HOME` to override it. Removing the directory clears the
|
|
93
|
+
download cache and permanently removes the local graph data.
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
The packaged default fixes the database provider to Neo4j and otherwise follows
|
|
98
|
+
upstream Graphiti environment names. Common settings are:
|
|
99
|
+
|
|
100
|
+
| Variable | Default |
|
|
101
|
+
| ------------------- | ------------------------ |
|
|
102
|
+
| `OPENAI_API_KEY` | Required |
|
|
103
|
+
| `MODEL_NAME` | `gpt-4.1-mini` |
|
|
104
|
+
| `EMBEDDER_MODEL` | `text-embedding-3-small` |
|
|
105
|
+
| `GRAPHITI_GROUP_ID` | `main` |
|
|
106
|
+
| `GRAPHITI_HOST` | `127.0.0.1` |
|
|
107
|
+
| `GRAPHITI_PORT` | `8000` |
|
|
108
|
+
| `NEO4J_URI` | `bolt://127.0.0.1:7687` |
|
|
109
|
+
| `NEO4J_DATABASE` | `neo4j` |
|
|
110
|
+
|
|
111
|
+
Explicit `NEO4J_*` values override generated defaults, which lets the Graphiti
|
|
112
|
+
process use an existing Neo4j server. The launcher still manages its local
|
|
113
|
+
Neo4j process; use upstream Graphiti directly if lifecycle ownership belongs to
|
|
114
|
+
an external database administrator.
|
|
115
|
+
|
|
116
|
+
Graphiti owns MCP tools, graph behavior, LLM calls, embeddings, and migrations.
|
|
117
|
+
This package owns only repeatable installation, configuration, and process
|
|
118
|
+
lifecycle. See the [upstream MCP server documentation](https://github.com/getzep/graphiti/tree/main/mcp_server)
|
|
119
|
+
for its complete API and provider configuration.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# `dbx-tools-graphiti`
|
|
2
|
+
|
|
3
|
+
Native launcher for [Graphiti](https://github.com/getzep/graphiti) with a local
|
|
4
|
+
Neo4j backend. It runs both services directly as host processes; it does not use
|
|
5
|
+
Docker, Podman, or another container runtime.
|
|
6
|
+
|
|
7
|
+
Install from PyPI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv add dbx-tools-graphiti
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install the current `main` branch:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add "dbx-tools-graphiti @ git+https://github.com/reggie-db/dbx-tools.git@main#subdirectory=packages/py/graphiti"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Key features
|
|
20
|
+
|
|
21
|
+
- launches upstream Graphiti's HTTP MCP server at `http://127.0.0.1:8000/mcp/`;
|
|
22
|
+
- runs Neo4j Community 5.26 as a native background process;
|
|
23
|
+
- provisions Java 21 and `uv` through `mise use -g` only when absent;
|
|
24
|
+
- pins Graphiti and Neo4j versions for repeatable local environments;
|
|
25
|
+
- caches downloads, Python dependencies, Neo4j data, credentials, and logs;
|
|
26
|
+
- supports foreground or background operation without vendoring Graphiti code.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
`mise` must already be installed. The launcher handles Java and `uv` itself.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export OPENAI_API_KEY=...
|
|
34
|
+
uv run dbx-graphiti start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The first run downloads about 120 MB of Neo4j plus the pinned Graphiti release,
|
|
38
|
+
creates Graphiti's `uv` environment, generates a local Neo4j password, starts
|
|
39
|
+
Neo4j, and then runs Graphiti in the foreground. Later runs reuse all of it.
|
|
40
|
+
|
|
41
|
+
For background operation:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv run dbx-graphiti up
|
|
45
|
+
uv run dbx-graphiti status
|
|
46
|
+
uv run dbx-graphiti down
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
| Command | Behavior |
|
|
52
|
+
| -------- | ----------------------------------------------------------------------------- |
|
|
53
|
+
| `setup` | Provision tools and populate the local cache without starting services. |
|
|
54
|
+
| `start` | Start Neo4j, then run Graphiti in the foreground. This is the default. |
|
|
55
|
+
| `up` | Start both services in the background. |
|
|
56
|
+
| `down` | Stop the managed Graphiti and Neo4j processes. |
|
|
57
|
+
| `status` | Print process state and the MCP URL as JSON. |
|
|
58
|
+
| `env` | Print resolved Neo4j connection settings as JSON. Treat its output as secret. |
|
|
59
|
+
|
|
60
|
+
Arguments after `start` or `up` are forwarded to upstream Graphiti. For example:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv run dbx-graphiti start -- --port 9000 --group-id my-agent
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Provisioning and caching
|
|
67
|
+
|
|
68
|
+
The package deliberately keeps orchestration separate from Graphiti itself:
|
|
69
|
+
|
|
70
|
+
1. It checks `mise where java@21` and `mise where uv@0.11`.
|
|
71
|
+
2. A missing tool is installed globally with `mise use -g --yes`.
|
|
72
|
+
3. Neo4j Community `5.26.12` is downloaded from `dist.neo4j.org` and unpacked.
|
|
73
|
+
4. Graphiti `v0.29.3` is downloaded from its GitHub release tag.
|
|
74
|
+
5. `uv sync --project <checkout>/mcp_server` creates the upstream environment.
|
|
75
|
+
6. A generated Neo4j password is stored with mode `0600` and supplied to
|
|
76
|
+
Graphiti through its documented environment variables.
|
|
77
|
+
|
|
78
|
+
The cache root is:
|
|
79
|
+
|
|
80
|
+
- macOS: `~/Library/Application Support/dbx-tools/graphiti`
|
|
81
|
+
- Linux: `${XDG_DATA_HOME:-~/.local/share}/dbx-tools/graphiti`
|
|
82
|
+
- Windows: `%LOCALAPPDATA%/dbx-tools/graphiti`
|
|
83
|
+
|
|
84
|
+
Set `DBX_GRAPHITI_HOME` to override it. Removing the directory clears the
|
|
85
|
+
download cache and permanently removes the local graph data.
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
The packaged default fixes the database provider to Neo4j and otherwise follows
|
|
90
|
+
upstream Graphiti environment names. Common settings are:
|
|
91
|
+
|
|
92
|
+
| Variable | Default |
|
|
93
|
+
| ------------------- | ------------------------ |
|
|
94
|
+
| `OPENAI_API_KEY` | Required |
|
|
95
|
+
| `MODEL_NAME` | `gpt-4.1-mini` |
|
|
96
|
+
| `EMBEDDER_MODEL` | `text-embedding-3-small` |
|
|
97
|
+
| `GRAPHITI_GROUP_ID` | `main` |
|
|
98
|
+
| `GRAPHITI_HOST` | `127.0.0.1` |
|
|
99
|
+
| `GRAPHITI_PORT` | `8000` |
|
|
100
|
+
| `NEO4J_URI` | `bolt://127.0.0.1:7687` |
|
|
101
|
+
| `NEO4J_DATABASE` | `neo4j` |
|
|
102
|
+
|
|
103
|
+
Explicit `NEO4J_*` values override generated defaults, which lets the Graphiti
|
|
104
|
+
process use an existing Neo4j server. The launcher still manages its local
|
|
105
|
+
Neo4j process; use upstream Graphiti directly if lifecycle ownership belongs to
|
|
106
|
+
an external database administrator.
|
|
107
|
+
|
|
108
|
+
Graphiti owns MCP tools, graph behavior, LLM calls, embeddings, and migrations.
|
|
109
|
+
This package owns only repeatable installation, configuration, and process
|
|
110
|
+
lifecycle. See the [upstream MCP server documentation](https://github.com/getzep/graphiti/tree/main/mcp_server)
|
|
111
|
+
for its complete API and provider configuration.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dbx-tools-graphiti"
|
|
3
|
+
version = "0.6.111"
|
|
4
|
+
description = "Native Graphiti MCP and Neo4j launcher with mise-managed prerequisites"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[project.urls]
|
|
10
|
+
Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/graphiti"
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
dbx-graphiti = "dbx_tools.graphiti.cli:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[tool.uv.build-backend]
|
|
20
|
+
module-name = "dbx_tools.graphiti"
|
|
21
|
+
module-root = "src"
|
|
22
|
+
namespace = true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ~~ Generated by projen. To modify, edit .projenrc.js and run "bunx projen".
|
|
2
|
+
|
|
3
|
+
[project]
|
|
4
|
+
name = "dbx-tools-graphiti"
|
|
5
|
+
version = "0.6.111"
|
|
6
|
+
description = "Native Graphiti MCP and Neo4j launcher with mise-managed prerequisites"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [ ]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/graphiti"
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
dbx-graphiti = "dbx_tools.graphiti.cli:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = [ "uv_build>=0.11.28,<0.12.0" ]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
[tool.uv.build-backend]
|
|
22
|
+
module-name = "dbx_tools.graphiti"
|
|
23
|
+
module-root = "src"
|
|
24
|
+
namespace = true
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Command-line interface for the native Graphiti stack."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from .runtime import Runtime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="dbx-graphiti",
|
|
16
|
+
description="Run Graphiti MCP with a local native Neo4j backend (no containers).",
|
|
17
|
+
)
|
|
18
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
19
|
+
subparsers.add_parser("setup", help="Install pinned native prerequisites")
|
|
20
|
+
start = subparsers.add_parser("start", help="Start Neo4j and Graphiti in the foreground")
|
|
21
|
+
start.add_argument("graphiti_args", nargs=argparse.REMAINDER)
|
|
22
|
+
up = subparsers.add_parser("up", help="Start Neo4j and Graphiti in the background")
|
|
23
|
+
up.add_argument("graphiti_args", nargs=argparse.REMAINDER)
|
|
24
|
+
subparsers.add_parser("down", help="Stop Graphiti and Neo4j")
|
|
25
|
+
subparsers.add_parser("status", help="Show native process status")
|
|
26
|
+
subparsers.add_parser("env", help="Print resolved connection settings")
|
|
27
|
+
|
|
28
|
+
parsed = parser.parse_args(argv)
|
|
29
|
+
runtime = Runtime()
|
|
30
|
+
command = parsed.command or "start"
|
|
31
|
+
if command == "setup":
|
|
32
|
+
runtime.setup()
|
|
33
|
+
print(f"Graphiti is ready under {runtime.paths.root}")
|
|
34
|
+
elif command in {"start", "up"}:
|
|
35
|
+
if not os.getenv("OPENAI_API_KEY"):
|
|
36
|
+
parser.error("OPENAI_API_KEY is required by the default Graphiti configuration")
|
|
37
|
+
extra_args = getattr(parsed, "graphiti_args", [])
|
|
38
|
+
if extra_args[:1] == ["--"]:
|
|
39
|
+
extra_args = extra_args[1:]
|
|
40
|
+
result = runtime.start(foreground=command == "start", extra_args=extra_args)
|
|
41
|
+
if command == "up":
|
|
42
|
+
print(f"Graphiti started with PID {result}; MCP: http://127.0.0.1:8000/mcp/")
|
|
43
|
+
elif result:
|
|
44
|
+
raise SystemExit(result)
|
|
45
|
+
elif command == "down":
|
|
46
|
+
runtime.stop()
|
|
47
|
+
elif command == "status":
|
|
48
|
+
print(json.dumps(runtime.status(), indent=2))
|
|
49
|
+
elif command == "env":
|
|
50
|
+
state = runtime.read_state()
|
|
51
|
+
print(json.dumps(runtime.connection_settings(str(state["neo4j_password"])), indent=2))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
server:
|
|
2
|
+
transport: "http"
|
|
3
|
+
host: ${GRAPHITI_HOST:127.0.0.1}
|
|
4
|
+
port: ${GRAPHITI_PORT:8000}
|
|
5
|
+
|
|
6
|
+
llm:
|
|
7
|
+
provider: "openai"
|
|
8
|
+
model: ${MODEL_NAME:gpt-4.1-mini}
|
|
9
|
+
max_tokens: 4096
|
|
10
|
+
structured_output_mode: ${LLM_STRUCTURED_OUTPUT_MODE:json_schema}
|
|
11
|
+
providers:
|
|
12
|
+
openai:
|
|
13
|
+
api_key: ${OPENAI_API_KEY}
|
|
14
|
+
api_url: ${OPENAI_API_URL:https://api.openai.com/v1}
|
|
15
|
+
organization_id: ${OPENAI_ORGANIZATION_ID:}
|
|
16
|
+
|
|
17
|
+
embedder:
|
|
18
|
+
provider: "openai"
|
|
19
|
+
model: ${EMBEDDER_MODEL:text-embedding-3-small}
|
|
20
|
+
dimensions: 1536
|
|
21
|
+
providers:
|
|
22
|
+
openai:
|
|
23
|
+
api_key: ${OPENAI_API_KEY}
|
|
24
|
+
api_url: ${OPENAI_API_URL:https://api.openai.com/v1}
|
|
25
|
+
organization_id: ${OPENAI_ORGANIZATION_ID:}
|
|
26
|
+
|
|
27
|
+
database:
|
|
28
|
+
provider: "neo4j"
|
|
29
|
+
providers:
|
|
30
|
+
neo4j:
|
|
31
|
+
uri: ${NEO4J_URI:bolt://127.0.0.1:7687}
|
|
32
|
+
username: ${NEO4J_USER:neo4j}
|
|
33
|
+
password: ${NEO4J_PASSWORD}
|
|
34
|
+
database: ${NEO4J_DATABASE:neo4j}
|
|
35
|
+
use_parallel_runtime: false
|
|
36
|
+
|
|
37
|
+
graphiti:
|
|
38
|
+
group_id: ${GRAPHITI_GROUP_ID:main}
|
|
39
|
+
episode_id_prefix: ${EPISODE_ID_PREFIX:}
|
|
40
|
+
user_id: ${USER_ID:mcp_user}
|
|
41
|
+
entity_types: []
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Installation and native process lifecycle for Graphiti and Neo4j."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import shutil
|
|
9
|
+
import signal
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tarfile
|
|
13
|
+
import time
|
|
14
|
+
import urllib.request
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from importlib import resources
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
GRAPHITI_VERSION = "0.29.3"
|
|
20
|
+
NEO4J_VERSION = "5.26.12"
|
|
21
|
+
JAVA_VERSION = "21"
|
|
22
|
+
UV_VERSION = "0.11"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def default_data_dir() -> Path:
|
|
26
|
+
"""Return the per-user directory containing downloads, data, and logs."""
|
|
27
|
+
override = os.getenv("DBX_GRAPHITI_HOME")
|
|
28
|
+
if override:
|
|
29
|
+
return Path(override).expanduser()
|
|
30
|
+
if sys.platform == "darwin":
|
|
31
|
+
return Path.home() / "Library" / "Application Support" / "dbx-tools" / "graphiti"
|
|
32
|
+
if os.name == "nt":
|
|
33
|
+
return (
|
|
34
|
+
Path(os.getenv("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
35
|
+
/ "dbx-tools"
|
|
36
|
+
/ "graphiti"
|
|
37
|
+
)
|
|
38
|
+
return (
|
|
39
|
+
Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
40
|
+
/ "dbx-tools"
|
|
41
|
+
/ "graphiti"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class RuntimePaths:
|
|
47
|
+
"""Resolved filesystem layout for one Graphiti installation."""
|
|
48
|
+
|
|
49
|
+
root: Path
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def default(cls) -> RuntimePaths:
|
|
53
|
+
return cls(default_data_dir())
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def graphiti(self) -> Path:
|
|
57
|
+
return self.root / "graphiti" / GRAPHITI_VERSION
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def neo4j(self) -> Path:
|
|
61
|
+
return self.root / "neo4j" / NEO4J_VERSION
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def neo4j_data(self) -> Path:
|
|
65
|
+
return self.root / "data" / "neo4j"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def state(self) -> Path:
|
|
69
|
+
return self.root / "state.json"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def log(self) -> Path:
|
|
73
|
+
return self.root / "graphiti.log"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Runtime:
|
|
77
|
+
"""Provision and run a pinned native Graphiti stack."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, paths: RuntimePaths | None = None) -> None:
|
|
80
|
+
self.paths = paths or RuntimePaths.default()
|
|
81
|
+
|
|
82
|
+
def setup(self) -> None:
|
|
83
|
+
self._require_mise()
|
|
84
|
+
self._ensure_mise_tool("java", JAVA_VERSION)
|
|
85
|
+
self._ensure_mise_tool("uv", UV_VERSION)
|
|
86
|
+
self.paths.root.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
self._install_neo4j()
|
|
88
|
+
self._install_graphiti()
|
|
89
|
+
self._ensure_state()
|
|
90
|
+
|
|
91
|
+
def start(self, *, foreground: bool = True, extra_args: list[str] | None = None) -> int:
|
|
92
|
+
self.setup()
|
|
93
|
+
state = self.read_state()
|
|
94
|
+
self._start_neo4j(state["neo4j_password"])
|
|
95
|
+
command = self.graphiti_command(extra_args or [])
|
|
96
|
+
environment = self.environment(state["neo4j_password"])
|
|
97
|
+
if foreground:
|
|
98
|
+
return subprocess.call(command, cwd=self.paths.graphiti / "mcp_server", env=environment)
|
|
99
|
+
with self.paths.log.open("ab") as output:
|
|
100
|
+
process = subprocess.Popen(
|
|
101
|
+
command,
|
|
102
|
+
cwd=self.paths.graphiti / "mcp_server",
|
|
103
|
+
env=environment,
|
|
104
|
+
stdout=output,
|
|
105
|
+
stderr=subprocess.STDOUT,
|
|
106
|
+
start_new_session=True,
|
|
107
|
+
)
|
|
108
|
+
state["graphiti_pid"] = process.pid
|
|
109
|
+
self._write_state(state)
|
|
110
|
+
return process.pid
|
|
111
|
+
|
|
112
|
+
def stop(self) -> None:
|
|
113
|
+
state = self.read_state(required=False)
|
|
114
|
+
pid = state.pop("graphiti_pid", None)
|
|
115
|
+
if pid and _is_running(pid):
|
|
116
|
+
os.kill(pid, signal.SIGTERM)
|
|
117
|
+
if (self.paths.neo4j / "bin" / "neo4j").exists():
|
|
118
|
+
self._neo4j_command("stop", check=False)
|
|
119
|
+
if state:
|
|
120
|
+
self._write_state(state)
|
|
121
|
+
|
|
122
|
+
def status(self) -> dict[str, object]:
|
|
123
|
+
state = self.read_state(required=False)
|
|
124
|
+
pid = state.get("graphiti_pid")
|
|
125
|
+
neo4j_running = False
|
|
126
|
+
if (self.paths.neo4j / "bin" / "neo4j").exists():
|
|
127
|
+
result = self._neo4j_command("status", check=False, capture_output=True)
|
|
128
|
+
neo4j_running = result.returncode == 0
|
|
129
|
+
return {
|
|
130
|
+
"home": str(self.paths.root),
|
|
131
|
+
"graphiti": "running" if isinstance(pid, int) and _is_running(pid) else "stopped",
|
|
132
|
+
"graphiti_pid": pid,
|
|
133
|
+
"neo4j": "running" if neo4j_running else "stopped",
|
|
134
|
+
"mcp_url": "http://127.0.0.1:8000/mcp/",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def graphiti_command(self, extra_args: list[str]) -> list[str]:
|
|
138
|
+
config = resources.files("dbx_tools.graphiti").joinpath("config.yaml")
|
|
139
|
+
return [
|
|
140
|
+
"mise",
|
|
141
|
+
"exec",
|
|
142
|
+
f"uv@{UV_VERSION}",
|
|
143
|
+
"--",
|
|
144
|
+
"uv",
|
|
145
|
+
"run",
|
|
146
|
+
"--project",
|
|
147
|
+
str(self.paths.graphiti / "mcp_server"),
|
|
148
|
+
"python",
|
|
149
|
+
str(self.paths.graphiti / "mcp_server" / "main.py"),
|
|
150
|
+
"--config",
|
|
151
|
+
str(config),
|
|
152
|
+
"--database-provider",
|
|
153
|
+
"neo4j",
|
|
154
|
+
*extra_args,
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
def environment(self, password: str) -> dict[str, str]:
|
|
158
|
+
environment = os.environ.copy()
|
|
159
|
+
environment.setdefault("NEO4J_URI", "bolt://127.0.0.1:7687")
|
|
160
|
+
environment.setdefault("NEO4J_USER", "neo4j")
|
|
161
|
+
environment.setdefault("NEO4J_PASSWORD", password)
|
|
162
|
+
environment.setdefault("NEO4J_DATABASE", "neo4j")
|
|
163
|
+
return environment
|
|
164
|
+
|
|
165
|
+
def connection_settings(self, password: str) -> dict[str, str]:
|
|
166
|
+
"""Return only the Neo4j settings callers need to connect."""
|
|
167
|
+
environment = self.environment(password)
|
|
168
|
+
return {
|
|
169
|
+
name: environment[name]
|
|
170
|
+
for name in ("NEO4J_URI", "NEO4J_USER", "NEO4J_PASSWORD", "NEO4J_DATABASE")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
def _require_mise(self) -> None:
|
|
174
|
+
if not shutil.which("mise"):
|
|
175
|
+
raise RuntimeError("mise is required; install it from https://mise.jdx.dev")
|
|
176
|
+
|
|
177
|
+
def _ensure_mise_tool(self, tool: str, version: str) -> None:
|
|
178
|
+
result = subprocess.run(
|
|
179
|
+
["mise", "where", f"{tool}@{version}"],
|
|
180
|
+
stdout=subprocess.DEVNULL,
|
|
181
|
+
stderr=subprocess.DEVNULL,
|
|
182
|
+
check=False,
|
|
183
|
+
)
|
|
184
|
+
if result.returncode:
|
|
185
|
+
subprocess.run(["mise", "use", "-g", "--yes", f"{tool}@{version}"], check=True)
|
|
186
|
+
|
|
187
|
+
def _install_neo4j(self) -> None:
|
|
188
|
+
if (self.paths.neo4j / "bin" / "neo4j").exists():
|
|
189
|
+
return
|
|
190
|
+
archive = self.paths.root / f"neo4j-community-{NEO4J_VERSION}-unix.tar.gz"
|
|
191
|
+
url = f"https://dist.neo4j.org/neo4j-community-{NEO4J_VERSION}-unix.tar.gz"
|
|
192
|
+
_download(url, archive)
|
|
193
|
+
target_parent = self.paths.neo4j.parent
|
|
194
|
+
target_parent.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
with tarfile.open(archive, "r:gz") as bundle:
|
|
196
|
+
_extract_archive(bundle, target_parent)
|
|
197
|
+
extracted = target_parent / f"neo4j-community-{NEO4J_VERSION}"
|
|
198
|
+
extracted.rename(self.paths.neo4j)
|
|
199
|
+
archive.unlink()
|
|
200
|
+
|
|
201
|
+
def _install_graphiti(self) -> None:
|
|
202
|
+
if (self.paths.graphiti / "mcp_server" / "main.py").exists():
|
|
203
|
+
return
|
|
204
|
+
archive = self.paths.root / f"graphiti-{GRAPHITI_VERSION}.tar.gz"
|
|
205
|
+
url = f"https://github.com/getzep/graphiti/archive/refs/tags/v{GRAPHITI_VERSION}.tar.gz"
|
|
206
|
+
_download(url, archive)
|
|
207
|
+
target_parent = self.paths.graphiti.parent
|
|
208
|
+
target_parent.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
with tarfile.open(archive, "r:gz") as bundle:
|
|
210
|
+
_extract_archive(bundle, target_parent)
|
|
211
|
+
extracted = target_parent / f"graphiti-{GRAPHITI_VERSION}"
|
|
212
|
+
extracted.rename(self.paths.graphiti)
|
|
213
|
+
archive.unlink()
|
|
214
|
+
subprocess.run(
|
|
215
|
+
[
|
|
216
|
+
"mise",
|
|
217
|
+
"exec",
|
|
218
|
+
f"uv@{UV_VERSION}",
|
|
219
|
+
"--",
|
|
220
|
+
"uv",
|
|
221
|
+
"sync",
|
|
222
|
+
"--project",
|
|
223
|
+
str(self.paths.graphiti / "mcp_server"),
|
|
224
|
+
],
|
|
225
|
+
check=True,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def _ensure_state(self) -> None:
|
|
229
|
+
if self.paths.state.exists():
|
|
230
|
+
return
|
|
231
|
+
password = secrets.token_urlsafe(24)
|
|
232
|
+
self._write_state({"neo4j_password": password})
|
|
233
|
+
subprocess.run(
|
|
234
|
+
self._mise_java_command(
|
|
235
|
+
self.paths.neo4j / "bin" / "neo4j-admin",
|
|
236
|
+
"dbms",
|
|
237
|
+
"set-initial-password",
|
|
238
|
+
password,
|
|
239
|
+
),
|
|
240
|
+
env=self._neo4j_environment(),
|
|
241
|
+
check=True,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _start_neo4j(self, password: str) -> None:
|
|
245
|
+
del password
|
|
246
|
+
result = self._neo4j_command("status", check=False)
|
|
247
|
+
if result.returncode:
|
|
248
|
+
self._neo4j_command("start")
|
|
249
|
+
deadline = time.monotonic() + 60
|
|
250
|
+
while time.monotonic() < deadline:
|
|
251
|
+
result = self._neo4j_command("status", check=False)
|
|
252
|
+
if result.returncode == 0:
|
|
253
|
+
return
|
|
254
|
+
time.sleep(1)
|
|
255
|
+
raise RuntimeError("Neo4j did not become ready within 60 seconds")
|
|
256
|
+
|
|
257
|
+
def _neo4j_command(
|
|
258
|
+
self,
|
|
259
|
+
action: str,
|
|
260
|
+
*,
|
|
261
|
+
check: bool = False,
|
|
262
|
+
capture_output: bool = False,
|
|
263
|
+
) -> subprocess.CompletedProcess[str]:
|
|
264
|
+
return subprocess.run(
|
|
265
|
+
self._mise_java_command(self.paths.neo4j / "bin" / "neo4j", action),
|
|
266
|
+
env=self._neo4j_environment(),
|
|
267
|
+
text=True,
|
|
268
|
+
check=check,
|
|
269
|
+
capture_output=capture_output,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _mise_java_command(self, executable: Path, *arguments: str) -> list[str]:
|
|
273
|
+
return ["mise", "exec", f"java@{JAVA_VERSION}", "--", str(executable), *arguments]
|
|
274
|
+
|
|
275
|
+
def _neo4j_environment(self) -> dict[str, str]:
|
|
276
|
+
environment = os.environ.copy()
|
|
277
|
+
environment["NEO4J_HOME"] = str(self.paths.neo4j)
|
|
278
|
+
environment["NEO4J_CONF"] = str(self.paths.neo4j / "conf")
|
|
279
|
+
environment["NEO4J_server_directories_data"] = str(self.paths.neo4j_data)
|
|
280
|
+
environment["NEO4J_server_default__listen__address"] = "127.0.0.1"
|
|
281
|
+
return environment
|
|
282
|
+
|
|
283
|
+
def read_state(self, *, required: bool = True) -> dict[str, object]:
|
|
284
|
+
if not self.paths.state.exists():
|
|
285
|
+
if required:
|
|
286
|
+
raise RuntimeError("Graphiti is not set up; run `dbx-graphiti setup`")
|
|
287
|
+
return {}
|
|
288
|
+
return json.loads(self.paths.state.read_text())
|
|
289
|
+
|
|
290
|
+
def _write_state(self, state: dict[str, object]) -> None:
|
|
291
|
+
self.paths.root.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
self.paths.state.write_text(json.dumps(state, indent=2) + "\n")
|
|
293
|
+
self.paths.state.chmod(0o600)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _download(url: str, target: Path) -> None:
|
|
297
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
temporary = target.with_suffix(f"{target.suffix}.part")
|
|
299
|
+
with urllib.request.urlopen(url) as response, temporary.open("wb") as output:
|
|
300
|
+
shutil.copyfileobj(response, output)
|
|
301
|
+
temporary.replace(target)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _extract_archive(bundle: tarfile.TarFile, destination: Path) -> None:
|
|
305
|
+
destination = destination.resolve()
|
|
306
|
+
for member in bundle.getmembers():
|
|
307
|
+
extracted = (destination / member.name).resolve()
|
|
308
|
+
if destination not in extracted.parents and extracted != destination:
|
|
309
|
+
raise RuntimeError(f"Archive member escapes destination: {member.name}")
|
|
310
|
+
bundle.extractall(destination)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _is_running(pid: int) -> bool:
|
|
314
|
+
try:
|
|
315
|
+
os.kill(pid, 0)
|
|
316
|
+
except OSError:
|
|
317
|
+
return False
|
|
318
|
+
return True
|