context-graph-spaces 0.1.0__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.
- context_graph_spaces-0.1.0/.gitignore +189 -0
- context_graph_spaces-0.1.0/CONTEXT.md +37 -0
- context_graph_spaces-0.1.0/LICENSE +21 -0
- context_graph_spaces-0.1.0/PKG-INFO +106 -0
- context_graph_spaces-0.1.0/README.md +85 -0
- context_graph_spaces-0.1.0/pyproject.toml +44 -0
- context_graph_spaces-0.1.0/src/context_graph_spaces/__init__.py +13 -0
- context_graph_spaces-0.1.0/src/context_graph_spaces/cli.py +123 -0
- context_graph_spaces-0.1.0/src/context_graph_spaces/provision.py +421 -0
- context_graph_spaces-0.1.0/src/context_graph_spaces/resolver.py +129 -0
- context_graph_spaces-0.1.0/src/context_graph_spaces/spec.py +157 -0
- context_graph_spaces-0.1.0/src/context_graph_spaces/worker.py +126 -0
- context_graph_spaces-0.1.0/tests/__init__.py +0 -0
- context_graph_spaces-0.1.0/tests/conftest.py +139 -0
- context_graph_spaces-0.1.0/tests/test_e2e_provision.py +157 -0
- context_graph_spaces-0.1.0/tests/test_e2e_recall.py +69 -0
- context_graph_spaces-0.1.0/tests/test_e2e_resolver.py +113 -0
- context_graph_spaces-0.1.0/tests/test_e2e_shared_memory.py +132 -0
- context_graph_spaces-0.1.0/tests/test_e2e_ship_gate.py +70 -0
- context_graph_spaces-0.1.0/tests/test_e2e_worker.py +159 -0
- context_graph_spaces-0.1.0/tests/test_spec.py +64 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# Ruff stuff:
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# PyPI configuration file
|
|
174
|
+
.pypirc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# VS Code
|
|
178
|
+
.vscode/
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Local agent hook configuration
|
|
182
|
+
/.codex/
|
|
183
|
+
/.claude/settings.local.json
|
|
184
|
+
|
|
185
|
+
# Project specific files
|
|
186
|
+
/enterprise-context/sic-agent/sic-scrapper/output/*
|
|
187
|
+
|
|
188
|
+
# deepeval writes run caches and telemetry here; build artifacts, not source.
|
|
189
|
+
.deepeval/
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Context Graph Spaces
|
|
2
|
+
|
|
3
|
+
The Enterprise edition of Context Graph: memory for many users on one shared,
|
|
4
|
+
licensed Memgraph, where Memgraph's own access control enforces the isolation
|
|
5
|
+
(map #501).
|
|
6
|
+
|
|
7
|
+
## Language
|
|
8
|
+
|
|
9
|
+
**Personal Space**:
|
|
10
|
+
A member's own database, `u_<login>`, and their main database. Hooks write all of the member's activity here. Only the member and nobody else can reach it.
|
|
11
|
+
_Avoid_: tenant, user database
|
|
12
|
+
|
|
13
|
+
**Shared Space**:
|
|
14
|
+
A database `space_<name>` that a set of members can read. Members may create and change only the labels and edge types the **Spaces Spec** allows; v1 allows curated memory only. A team or the whole org is just a Shared Space with different members.
|
|
15
|
+
_Avoid_: team space (a team is one use), workspace
|
|
16
|
+
|
|
17
|
+
**Spaces Spec**:
|
|
18
|
+
The admin's TOML file listing members and Shared Spaces. It is the single source of desired state. `context-graph-spaces plan` diffs Memgraph against it; `apply` makes Memgraph match.
|
|
19
|
+
_Avoid_: config (that's the member's own Context Graph config file)
|
|
20
|
+
|
|
21
|
+
**Space Membership**:
|
|
22
|
+
A `(:SpaceMembership {space, database})` node in a member's Personal Space, written by `apply`. It is how recall learns which Shared Spaces to read, because an unprivileged login can't list its own grants. Memgraph's grants remain the authority; a stale node only costs a permission error.
|
|
23
|
+
|
|
24
|
+
**Service Account**:
|
|
25
|
+
`cg_service`. Holds the worker role on every Shared Space and writes what members can't there, such as Chunks and entities from reconciliation.
|
|
26
|
+
|
|
27
|
+
**Owned Object**:
|
|
28
|
+
A database, role, user or tenant profile with a `u_`, `space_` or `cg_` prefix, or a user whose `u_<login>` database exists. The provisioner changes only owned objects.
|
|
29
|
+
|
|
30
|
+
## Relationships
|
|
31
|
+
|
|
32
|
+
- **Spaces Spec -> Memgraph**: `apply` creates databases, versioned roles (named by a hash of their grants), users, role assignments scoped to one database, tenant profiles and Space Memberships. Removing a member or a space revokes access; `--prune` also drops users and databases.
|
|
33
|
+
- **Context Graph config -> EnterpriseSpaces**: with `edition = "enterprise"`, memgraph-context-graph's `SpaceResolver` is this package's resolver, found through the `memgraph_context_graph.editions` entry point.
|
|
34
|
+
|
|
35
|
+
## Flagged ambiguities
|
|
36
|
+
|
|
37
|
+
- "Role": in this package, a Memgraph auth role (`space_payments_member_3fa2c1d0`). Not a message speaker role (`actions-graph`'s `MessageRole`).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Memgraph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: context-graph-spaces
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Enterprise edition of Context Graph: personal and shared memory spaces for many users on one Memgraph
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: memgraph-context-graph>=0.5.0
|
|
13
|
+
Requires-Dist: memgraph-toolbox
|
|
14
|
+
Requires-Dist: neo4j>=5.0
|
|
15
|
+
Requires-Dist: sessions-graph[reconciliation]>=0.6.0
|
|
16
|
+
Requires-Dist: tomli>=2.0
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=9.0.3; extra == 'test'
|
|
19
|
+
Requires-Dist: sessions-graph[memgraph-context-graph,reconciliation]>=0.8.0; extra == 'test'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# context-graph-spaces
|
|
23
|
+
|
|
24
|
+
The Enterprise edition of [Context Graph](../README.md): memory for many users on
|
|
25
|
+
one shared, licensed Memgraph. Each user gets a **personal space**, and teams get
|
|
26
|
+
**shared spaces**. Memgraph Enterprise enforces the isolation through
|
|
27
|
+
multi-tenancy, role-based access control and label- and property-based access
|
|
28
|
+
control. Our code doesn't filter.
|
|
29
|
+
|
|
30
|
+
The Community edition (one user, one database) doesn't need this package and
|
|
31
|
+
doesn't change when it's installed. A user's config file selects Enterprise
|
|
32
|
+
explicitly:
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
edition = "enterprise"
|
|
36
|
+
|
|
37
|
+
[memgraph]
|
|
38
|
+
url = "bolt://memgraph.corp:7687"
|
|
39
|
+
user = "ante" # your Memgraph login is your identity
|
|
40
|
+
password = "..."
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## For admins: provisioning
|
|
44
|
+
|
|
45
|
+
Spaces are declared in a spec file and applied idempotently:
|
|
46
|
+
|
|
47
|
+
```toml
|
|
48
|
+
members = ["ante", "bob"]
|
|
49
|
+
personal_memory_limit_mb = 2048
|
|
50
|
+
|
|
51
|
+
[spaces.payments]
|
|
52
|
+
members = ["ante", "bob"]
|
|
53
|
+
memory_limit_mb = 4096
|
|
54
|
+
# labels = [...] # what members may write; defaults to curated memory
|
|
55
|
+
# edge_types = [...]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
export MEMGRAPH_URL=bolt://memgraph.corp:7687 MEMGRAPH_USER=<admin> MEMGRAPH_PASSWORD=...
|
|
60
|
+
context-graph-spaces plan spaces.toml # what would change
|
|
61
|
+
context-graph-spaces apply spaces.toml # make it so; prints one-time setup lines for new logins
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
What `apply` maintains:
|
|
65
|
+
|
|
66
|
+
| Object | Name | Who |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Personal database | `u_<login>` | its owner (main database), plus nobody else |
|
|
69
|
+
| Shared database | `space_<name>` | members: read everything, write only the spec's labels |
|
|
70
|
+
| Roles | `cg_u_<login>_<hash>`, `space_<name>_member_<hash>`, `cg_worker_<hash>` | named after what they grant |
|
|
71
|
+
| Service login | `cg_service` | writes Chunks and entities in shared spaces |
|
|
72
|
+
| Tenant profiles | `cg_<mb>mb` | memory caps from the spec |
|
|
73
|
+
|
|
74
|
+
Removing someone from the spec revokes their access. Removing a space revokes
|
|
75
|
+
access to it. Users and databases are only dropped with `--prune`. Only objects
|
|
76
|
+
with the `u_`, `space_` and `cg_` prefixes are ever touched.
|
|
77
|
+
|
|
78
|
+
## The shared-space worker
|
|
79
|
+
|
|
80
|
+
Members can write only curated memory into a shared space. The derived graph
|
|
81
|
+
there (chunks, entities, relations, and the embeddings recall uses) is written by
|
|
82
|
+
one service login, `cg_service`. Run the worker next to `apply`, with the
|
|
83
|
+
password `apply` printed once:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export CG_SERVICE_PASSWORD=...
|
|
87
|
+
context-graph-spaces reconcile spaces.toml --watch # one pass every 30s
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Each pass, for each shared space, it does two things:
|
|
91
|
+
|
|
92
|
+
1. It removes what deleted files left behind. A member's delete can't touch chunks.
|
|
93
|
+
2. It reconciles changed memory files with GLiNER2 under the space's ontology.
|
|
94
|
+
No LLM is involved.
|
|
95
|
+
|
|
96
|
+
Personal spaces keep reconciling locally, as in Community.
|
|
97
|
+
|
|
98
|
+
## Tests
|
|
99
|
+
|
|
100
|
+
The tests run end-to-end against a licensed Memgraph. The licence is read from
|
|
101
|
+
`MEMGRAPH_ENTERPRISE_LICENSE` and `MEMGRAPH_ORGANIZATION_NAME` (the repo's `.env`
|
|
102
|
+
locally, secrets in CI):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
./scripts/dev-memgraph.sh test-enterprise
|
|
106
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# context-graph-spaces
|
|
2
|
+
|
|
3
|
+
The Enterprise edition of [Context Graph](../README.md): memory for many users on
|
|
4
|
+
one shared, licensed Memgraph. Each user gets a **personal space**, and teams get
|
|
5
|
+
**shared spaces**. Memgraph Enterprise enforces the isolation through
|
|
6
|
+
multi-tenancy, role-based access control and label- and property-based access
|
|
7
|
+
control. Our code doesn't filter.
|
|
8
|
+
|
|
9
|
+
The Community edition (one user, one database) doesn't need this package and
|
|
10
|
+
doesn't change when it's installed. A user's config file selects Enterprise
|
|
11
|
+
explicitly:
|
|
12
|
+
|
|
13
|
+
```toml
|
|
14
|
+
edition = "enterprise"
|
|
15
|
+
|
|
16
|
+
[memgraph]
|
|
17
|
+
url = "bolt://memgraph.corp:7687"
|
|
18
|
+
user = "ante" # your Memgraph login is your identity
|
|
19
|
+
password = "..."
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## For admins: provisioning
|
|
23
|
+
|
|
24
|
+
Spaces are declared in a spec file and applied idempotently:
|
|
25
|
+
|
|
26
|
+
```toml
|
|
27
|
+
members = ["ante", "bob"]
|
|
28
|
+
personal_memory_limit_mb = 2048
|
|
29
|
+
|
|
30
|
+
[spaces.payments]
|
|
31
|
+
members = ["ante", "bob"]
|
|
32
|
+
memory_limit_mb = 4096
|
|
33
|
+
# labels = [...] # what members may write; defaults to curated memory
|
|
34
|
+
# edge_types = [...]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export MEMGRAPH_URL=bolt://memgraph.corp:7687 MEMGRAPH_USER=<admin> MEMGRAPH_PASSWORD=...
|
|
39
|
+
context-graph-spaces plan spaces.toml # what would change
|
|
40
|
+
context-graph-spaces apply spaces.toml # make it so; prints one-time setup lines for new logins
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
What `apply` maintains:
|
|
44
|
+
|
|
45
|
+
| Object | Name | Who |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Personal database | `u_<login>` | its owner (main database), plus nobody else |
|
|
48
|
+
| Shared database | `space_<name>` | members: read everything, write only the spec's labels |
|
|
49
|
+
| Roles | `cg_u_<login>_<hash>`, `space_<name>_member_<hash>`, `cg_worker_<hash>` | named after what they grant |
|
|
50
|
+
| Service login | `cg_service` | writes Chunks and entities in shared spaces |
|
|
51
|
+
| Tenant profiles | `cg_<mb>mb` | memory caps from the spec |
|
|
52
|
+
|
|
53
|
+
Removing someone from the spec revokes their access. Removing a space revokes
|
|
54
|
+
access to it. Users and databases are only dropped with `--prune`. Only objects
|
|
55
|
+
with the `u_`, `space_` and `cg_` prefixes are ever touched.
|
|
56
|
+
|
|
57
|
+
## The shared-space worker
|
|
58
|
+
|
|
59
|
+
Members can write only curated memory into a shared space. The derived graph
|
|
60
|
+
there (chunks, entities, relations, and the embeddings recall uses) is written by
|
|
61
|
+
one service login, `cg_service`. Run the worker next to `apply`, with the
|
|
62
|
+
password `apply` printed once:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export CG_SERVICE_PASSWORD=...
|
|
66
|
+
context-graph-spaces reconcile spaces.toml --watch # one pass every 30s
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each pass, for each shared space, it does two things:
|
|
70
|
+
|
|
71
|
+
1. It removes what deleted files left behind. A member's delete can't touch chunks.
|
|
72
|
+
2. It reconciles changed memory files with GLiNER2 under the space's ontology.
|
|
73
|
+
No LLM is involved.
|
|
74
|
+
|
|
75
|
+
Personal spaces keep reconciling locally, as in Community.
|
|
76
|
+
|
|
77
|
+
## Tests
|
|
78
|
+
|
|
79
|
+
The tests run end-to-end against a licensed Memgraph. The licence is read from
|
|
80
|
+
`MEMGRAPH_ENTERPRISE_LICENSE` and `MEMGRAPH_ORGANIZATION_NAME` (the repo's `.env`
|
|
81
|
+
locally, secrets in CI):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
./scripts/dev-memgraph.sh test-enterprise
|
|
85
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "context-graph-spaces"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "The Enterprise edition of Context Graph: personal and shared memory spaces for many users on one Memgraph"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Programming Language :: Python :: 3.10",
|
|
10
|
+
"Programming Language :: Python :: 3.11",
|
|
11
|
+
"Programming Language :: Python :: 3.12",
|
|
12
|
+
"Programming Language :: Python :: 3.13",
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"memgraph-context-graph>=0.5.0",
|
|
16
|
+
"memgraph-toolbox",
|
|
17
|
+
# Shared spaces hold sessions-graph's memory files: apply sets up its schema there, and the
|
|
18
|
+
# worker reconciles them (the reconciliation extra brings unstructured2graph).
|
|
19
|
+
"sessions-graph[reconciliation]>=0.6.0",
|
|
20
|
+
"neo4j>=5.0",
|
|
21
|
+
# tomllib on 3.11+; tomli for 3.10, which ty checks against.
|
|
22
|
+
"tomli>=2.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = [
|
|
27
|
+
"pytest>=9.0.3",
|
|
28
|
+
"sessions-graph[memgraph-context-graph,reconciliation]>=0.8.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
context-graph-spaces = "context_graph_spaces.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.entry-points."memgraph_context_graph.editions"]
|
|
35
|
+
enterprise = "context_graph_spaces.resolver:EnterpriseSpaces"
|
|
36
|
+
|
|
37
|
+
[tool.uv.sources]
|
|
38
|
+
memgraph-context-graph = { workspace = true }
|
|
39
|
+
memgraph-toolbox = { workspace = true }
|
|
40
|
+
sessions-graph = { workspace = true }
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["hatchling"]
|
|
44
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Context Graph's Enterprise edition: personal and shared memory spaces for many users on one Memgraph.
|
|
2
|
+
|
|
3
|
+
Selected by ``edition = "enterprise"`` in the Context Graph config file, which
|
|
4
|
+
makes :class:`EnterpriseSpaces` the space resolver. The admin side provisions
|
|
5
|
+
spaces from a spec (:mod:`.spec`, :mod:`.provision`, ``context-graph-spaces
|
|
6
|
+
plan|apply``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .provision import Applied, Plan, apply, observed_state, plan
|
|
10
|
+
from .resolver import EnterpriseSpaces
|
|
11
|
+
from .spec import Spec, SpecError, load_spec
|
|
12
|
+
|
|
13
|
+
__all__ = ["Applied", "EnterpriseSpaces", "Plan", "Spec", "SpecError", "apply", "load_spec", "observed_state", "plan"]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""``context-graph-spaces``: the admin's commands for provisioning spaces from a spec.
|
|
2
|
+
|
|
3
|
+
The admin connection comes from flags, falling back to ``MEMGRAPH_URL`` /
|
|
4
|
+
``MEMGRAPH_USER`` / ``MEMGRAPH_PASSWORD``. This is an interactive admin tool,
|
|
5
|
+
not a hook, so the config-file-only rule for hooks doesn't apply; the admin's
|
|
6
|
+
login is never written anywhere.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
15
|
+
|
|
16
|
+
from .provision import SERVICE_ACCOUNT, Plan, Query, Step, apply, observed_state, plan
|
|
17
|
+
from .spec import SpecError, load_spec
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Sequence
|
|
21
|
+
|
|
22
|
+
from .spec import Spec
|
|
23
|
+
|
|
24
|
+
#: Where the reconcile worker finds the service login's password.
|
|
25
|
+
SERVICE_PASSWORD_ENV = "CG_SERVICE_PASSWORD"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
29
|
+
"""Run ``plan`` or ``apply``; returns the process exit code."""
|
|
30
|
+
parser = argparse.ArgumentParser(prog="context-graph-spaces", description=__doc__)
|
|
31
|
+
parser.add_argument("--url", default=os.environ.get("MEMGRAPH_URL", "bolt://localhost:7687"))
|
|
32
|
+
parser.add_argument("--user", default=os.environ.get("MEMGRAPH_USER", ""), help="An admin login.")
|
|
33
|
+
parser.add_argument("--password", default=os.environ.get("MEMGRAPH_PASSWORD", ""))
|
|
34
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
35
|
+
for name, text in (("plan", "Show what apply would change."), ("apply", "Make Memgraph match the spec.")):
|
|
36
|
+
command = commands.add_parser(name, help=text)
|
|
37
|
+
command.add_argument("spec", help="The spaces spec (TOML).")
|
|
38
|
+
command.add_argument("--prune", action="store_true", help="Also drop removed users and databases.")
|
|
39
|
+
reconcile = commands.add_parser("reconcile", help="Reconcile shared spaces' changed memory files, as cg_service.")
|
|
40
|
+
reconcile.add_argument("spec", help="The spaces spec (TOML): which shared spaces to reconcile.")
|
|
41
|
+
reconcile.add_argument("--watch", action="store_true", help="Keep going, one pass every --interval seconds.")
|
|
42
|
+
reconcile.add_argument("--interval", type=float, default=30.0)
|
|
43
|
+
args = parser.parse_args(argv)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
spec = load_spec(args.spec)
|
|
47
|
+
except SpecError as exc:
|
|
48
|
+
print(exc, file=sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
if args.command == "reconcile":
|
|
51
|
+
return _reconcile(spec, args.url, watch=args.watch, interval=args.interval)
|
|
52
|
+
query, close = admin_query(args.url, args.user, args.password)
|
|
53
|
+
try:
|
|
54
|
+
if args.command == "plan":
|
|
55
|
+
_print_plan(plan(spec, observed_state(query, spec.members), prune=args.prune))
|
|
56
|
+
return 0
|
|
57
|
+
applied = apply(spec, query, prune=args.prune)
|
|
58
|
+
finally:
|
|
59
|
+
close()
|
|
60
|
+
_print_steps(applied.steps, "Applied" if applied.steps else "Nothing to change; Memgraph matches the spec.")
|
|
61
|
+
_print_kept(applied.kept)
|
|
62
|
+
for login, password in sorted(applied.passwords.items()):
|
|
63
|
+
if login == SERVICE_ACCOUNT:
|
|
64
|
+
print(f"\nNew service login {login!r} for the shared-space reconcile worker; it isn't stored anywhere.")
|
|
65
|
+
print(f" Give the worker: {SERVICE_PASSWORD_ENV}='{password}'")
|
|
66
|
+
continue
|
|
67
|
+
print(f"\nNew login {login!r}. Hand this to them once; it isn't stored anywhere:")
|
|
68
|
+
print(f" memgraph-context-graph join {args.url} {login} --password='{password}'")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _reconcile(spec: Spec, url: str, *, watch: bool, interval: float) -> int:
|
|
73
|
+
"""Run the worker once, or every *interval* seconds with *watch*; exit 1 if the last pass had an error."""
|
|
74
|
+
import time
|
|
75
|
+
|
|
76
|
+
from .worker import reconcile_spaces
|
|
77
|
+
|
|
78
|
+
password = os.environ.get(SERVICE_PASSWORD_ENV)
|
|
79
|
+
if not password:
|
|
80
|
+
print(f"Set {SERVICE_PASSWORD_ENV} to {SERVICE_ACCOUNT}'s password (apply printed it once).", file=sys.stderr)
|
|
81
|
+
return 2
|
|
82
|
+
while True:
|
|
83
|
+
reports = reconcile_spaces(spec, url=url, password=password)
|
|
84
|
+
for report in reports:
|
|
85
|
+
if report.error:
|
|
86
|
+
print(f"{report.space}: not reconciled: {report.error}", file=sys.stderr)
|
|
87
|
+
continue
|
|
88
|
+
failed = "".join(f"\n failed {path}: {error}" for path, error in report.failed)
|
|
89
|
+
print(f"{report.space}: {len(report.completed)} reconciled, {report.pruned} leftovers removed{failed}")
|
|
90
|
+
if not watch:
|
|
91
|
+
return 1 if any(report.error or report.failed for report in reports) else 0
|
|
92
|
+
time.sleep(interval)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def admin_query(url: str, user: str, password: str) -> tuple[Query, Any]:
|
|
96
|
+
"""A :data:`~.provision.Query` over one admin driver, and the function that closes it."""
|
|
97
|
+
import neo4j
|
|
98
|
+
|
|
99
|
+
driver = neo4j.GraphDatabase.driver(url, auth=(user, password))
|
|
100
|
+
|
|
101
|
+
def query(statement: str, database: str | None, params: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
102
|
+
# Auth and multi-database DDL can't run in an explicit transaction, so each statement gets its own session.
|
|
103
|
+
with driver.session(database=database) as session:
|
|
104
|
+
# The driver's stubs want a LiteralString; admin DDL is built from names spec._NAME has validated.
|
|
105
|
+
return [record.data() for record in session.run(cast("Any", statement), params or {})]
|
|
106
|
+
|
|
107
|
+
return query, driver.close
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _print_plan(the_plan: Plan) -> None:
|
|
111
|
+
_print_steps(the_plan.steps, "Plan:" if the_plan.steps else "Nothing to change; Memgraph matches the spec.")
|
|
112
|
+
_print_kept(the_plan.kept)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _print_steps(steps: list[Step], heading: str) -> None:
|
|
116
|
+
print(heading)
|
|
117
|
+
for step in steps:
|
|
118
|
+
print(f" + {step.summary}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _print_kept(kept: list[str]) -> None:
|
|
122
|
+
if kept:
|
|
123
|
+
print(f"Kept (pass --prune to drop): {', '.join(kept)}")
|