@contextableai/openclaw-memory-rebac 0.1.0

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.
@@ -0,0 +1,101 @@
1
+ ###############################################################################
2
+ # openclaw-memory-rebac — Graphiti FastAPI REST server stack (standalone)
3
+ #
4
+ # Services:
5
+ # neo4j — graph database
6
+ # graphiti — Graphiti FastAPI REST server with extended config
7
+ #
8
+ # Usage:
9
+ # cp .env.example .env # configure LLM, embedder, reranker
10
+ # docker compose up -d
11
+ #
12
+ # Graphiti endpoint: http://graphiti:8000 (internal to Docker network only)
13
+ #
14
+ # LLM Configuration:
15
+ # Each AI component (LLM, embedder, reranker) can point to a different
16
+ # service. See .env for all available configuration options.
17
+ ###############################################################################
18
+
19
+ name: openclaw-graphiti
20
+
21
+ services:
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Neo4j — graph store for Graphiti
25
+ # ---------------------------------------------------------------------------
26
+ neo4j:
27
+ image: neo4j:5.26.2
28
+ restart: unless-stopped
29
+ expose:
30
+ - "7687"
31
+ - "7474"
32
+ environment:
33
+ NEO4J_AUTH: ${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD:-graphiti_pw}
34
+ volumes:
35
+ - neo4j_data:/data
36
+ healthcheck:
37
+ test: ["CMD-SHELL", "wget -q --spider http://localhost:7474 || exit 1"]
38
+ interval: 10s
39
+ timeout: 10s
40
+ retries: 15
41
+ start_period: 30s
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Graphiti FastAPI REST server
45
+ # ---------------------------------------------------------------------------
46
+ graphiti:
47
+ build:
48
+ context: .
49
+ dockerfile: Dockerfile
50
+ restart: unless-stopped
51
+ expose:
52
+ - "8000"
53
+ environment:
54
+ # -- Graph database (Neo4j) --
55
+ NEO4J_URI: bolt://neo4j:7687
56
+ NEO4J_USER: ${NEO4J_USER:-neo4j}
57
+ NEO4J_PASSWORD: ${NEO4J_PASSWORD:-graphiti_pw}
58
+
59
+ # -- Graphiti concurrency --
60
+ # build_indices_and_constraints fires 29 index queries concurrently
61
+ # (default semaphore limit = 20). Neo4j can't handle that many
62
+ # concurrent DDL ops and drops connections. Serialize them.
63
+ SEMAPHORE_LIMIT: 3
64
+
65
+ # -- LLM (entity extraction) --
66
+ OPENAI_API_KEY: ${LLM_API_KEY:?Set LLM_API_KEY in .env}
67
+ OPENAI_BASE_URL: ${LLM_BASE_URL:-}
68
+ MODEL_NAME: ${LLM_MODEL:-gpt-4o-mini}
69
+
70
+ # -- Embedder --
71
+ EMBEDDING_MODEL_NAME: ${EMBEDDING_MODEL:-text-embedding-3-small}
72
+ EMBEDDING_BASE_URL: ${EMBEDDING_BASE_URL:-}
73
+ EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-}
74
+ EMBEDDING_DIM: ${EMBEDDING_DIM:-}
75
+
76
+ # -- Reranker / cross-encoder --
77
+ # Default: "bge" runs BAAI/bge-reranker-v2-m3 locally (no API needed)
78
+ # Set to "openai" to use a remote reranker API instead
79
+ RERANKER_PROVIDER: ${RERANKER_PROVIDER:-bge}
80
+ RERANKER_MODEL: ${RERANKER_MODEL:-}
81
+ RERANKER_BASE_URL: ${RERANKER_BASE_URL:-}
82
+ RERANKER_API_KEY: ${RERANKER_API_KEY:-}
83
+
84
+ # -- Additional provider keys (optional) --
85
+ ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
86
+ GOOGLE_API_KEY: ${GOOGLE_API_KEY:-}
87
+ GROQ_API_KEY: ${GROQ_API_KEY:-}
88
+ depends_on:
89
+ neo4j:
90
+ condition: service_healthy
91
+ extra_hosts:
92
+ - "host.docker.internal:host-gateway"
93
+ healthcheck:
94
+ test: ["CMD", "curl", "-sf", "http://localhost:8000/healthcheck"]
95
+ interval: 10s
96
+ timeout: 5s
97
+ retries: 5
98
+ start_period: 15s
99
+
100
+ volumes:
101
+ neo4j_data:
@@ -0,0 +1,141 @@
1
+ """
2
+ Extended Graphiti initialization with per-component client configuration.
3
+
4
+ Defines OpenClawGraphiti — a subclass of the base Graphiti class that:
5
+ 1. Properly forwards all constructor params (embedder, cross_encoder)
6
+ 2. Adds the CRUD methods the FastAPI routes require
7
+
8
+ We bypass ZepGraphiti because its __init__ only forwards (uri, user,
9
+ password, llm_client) to super(), silently dropping embedder and
10
+ cross_encoder. This causes all embedding calls to use the hardcoded
11
+ default model (text-embedding-3-small) instead of the configured one.
12
+ """
13
+
14
+ import logging
15
+
16
+ from graphiti_core import Graphiti
17
+ from graphiti_core.edges import EntityEdge
18
+ from graphiti_core.errors import (
19
+ EdgeNotFoundError,
20
+ GroupsEdgesNotFoundError,
21
+ NodeNotFoundError,
22
+ )
23
+ from graphiti_core.llm_client.config import LLMConfig
24
+ from graphiti_core.llm_client.openai_generic_client import OpenAIGenericClient
25
+ from graphiti_core.embedder.openai import OpenAIEmbedder, OpenAIEmbedderConfig
26
+ from graphiti_core.nodes import EntityNode, EpisodicNode
27
+
28
+ from fastapi import HTTPException
29
+
30
+ from graph_service.config_overlay import ExtendedSettings
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class OpenClawGraphiti(Graphiti):
36
+ """Graphiti subclass with server-specific CRUD methods.
37
+
38
+ Unlike ZepGraphiti, this properly forwards ALL constructor params
39
+ (including embedder and cross_encoder) to the base Graphiti class,
40
+ so they are wired into GraphitiClients at construction time.
41
+ """
42
+
43
+ async def get_entity_edge(self, uuid: str):
44
+ try:
45
+ edge = await EntityEdge.get_by_uuid(self.driver, uuid)
46
+ return edge
47
+ except EdgeNotFoundError as e:
48
+ raise HTTPException(status_code=404, detail=e.message) from e
49
+
50
+ async def delete_entity_edge(self, uuid: str):
51
+ try:
52
+ edge = await EntityEdge.get_by_uuid(self.driver, uuid)
53
+ await edge.delete(self.driver)
54
+ except EdgeNotFoundError as e:
55
+ raise HTTPException(status_code=404, detail=e.message) from e
56
+
57
+ async def delete_group(self, group_id: str):
58
+ try:
59
+ edges = await EntityEdge.get_by_group_ids(self.driver, [group_id])
60
+ except GroupsEdgesNotFoundError:
61
+ logger.warning(f"No edges found for group {group_id}")
62
+ edges = []
63
+
64
+ nodes = await EntityNode.get_by_group_ids(self.driver, [group_id])
65
+ episodes = await EpisodicNode.get_by_group_ids(self.driver, [group_id])
66
+
67
+ for edge in edges:
68
+ await edge.delete(self.driver)
69
+ for node in nodes:
70
+ await node.delete(self.driver)
71
+ for episode in episodes:
72
+ await episode.delete(self.driver)
73
+
74
+ async def delete_episodic_node(self, uuid: str):
75
+ try:
76
+ episode = await EpisodicNode.get_by_uuid(self.driver, uuid)
77
+ await episode.delete(self.driver)
78
+ except NodeNotFoundError as e:
79
+ raise HTTPException(status_code=404, detail=e.message) from e
80
+
81
+
82
+ def _create_reranker(settings: ExtendedSettings, llm_client):
83
+ """Create the appropriate reranker based on RERANKER_PROVIDER."""
84
+ if settings.reranker_provider == "openai":
85
+ from graphiti_core.cross_encoder.openai_reranker_client import OpenAIRerankerClient
86
+
87
+ reranker_config = LLMConfig(
88
+ api_key=settings.reranker_api_key or settings.openai_api_key,
89
+ base_url=settings.reranker_base_url or settings.openai_base_url,
90
+ )
91
+ if settings.reranker_model:
92
+ reranker_config.model = settings.reranker_model
93
+ return OpenAIRerankerClient(client=llm_client, config=reranker_config)
94
+
95
+ # Default: BGE reranker (runs locally via sentence-transformers, no API needed)
96
+ from graphiti_core.cross_encoder.bge_reranker_client import BGERerankerClient
97
+
98
+ return BGERerankerClient()
99
+
100
+
101
+ def create_graphiti(settings: ExtendedSettings) -> OpenClawGraphiti:
102
+ """Create an OpenClawGraphiti instance with per-component client configuration."""
103
+
104
+ # -- LLM client (entity extraction) --
105
+ llm_config = LLMConfig(
106
+ api_key=settings.openai_api_key,
107
+ base_url=settings.openai_base_url,
108
+ )
109
+ if settings.model_name:
110
+ llm_config.model = settings.model_name
111
+ llm_config.small_model = settings.model_name
112
+ llm_client = OpenAIGenericClient(config=llm_config)
113
+
114
+ # -- Embedder --
115
+ embedder_api_key = settings.embedding_api_key or settings.openai_api_key
116
+ embedder_base_url = settings.embedding_base_url or settings.openai_base_url
117
+ embedder_kwargs = {
118
+ "api_key": embedder_api_key,
119
+ "base_url": embedder_base_url,
120
+ }
121
+ if settings.embedding_model_name:
122
+ embedder_kwargs["embedding_model"] = settings.embedding_model_name
123
+ if settings.embedding_dim is not None:
124
+ embedder_kwargs["embedding_dim"] = settings.embedding_dim
125
+ embedder_config = OpenAIEmbedderConfig(**embedder_kwargs)
126
+ embedder = OpenAIEmbedder(config=embedder_config)
127
+
128
+ # -- Reranker / cross-encoder --
129
+ reranker = _create_reranker(settings, llm_client)
130
+
131
+ # -- Construct with all params forwarded to base Graphiti --
132
+ client = OpenClawGraphiti(
133
+ uri=settings.neo4j_uri,
134
+ user=settings.neo4j_user,
135
+ password=settings.neo4j_password,
136
+ llm_client=llm_client,
137
+ embedder=embedder,
138
+ cross_encoder=reranker,
139
+ )
140
+
141
+ return client
@@ -0,0 +1,222 @@
1
+ """
2
+ Custom startup that configures the Graphiti FastAPI server to use our
3
+ extended per-component embedder/reranker settings before launching uvicorn.
4
+
5
+ Uses FastAPI's app.dependency_overrides to replace get_graphiti —
6
+ the proper mechanism that works with Annotated[..., Depends()] captures.
7
+ """
8
+
9
+ import asyncio
10
+ import importlib
11
+ import logging
12
+
13
+ from graph_service.config_overlay import ExtendedSettings
14
+ from graph_service.graphiti_overlay import create_graphiti
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ MAX_RETRIES = 5
19
+ RETRY_BASE_DELAY = 3 # seconds, doubles each retry
20
+
21
+
22
+ def patch():
23
+ """
24
+ Override graph_service modules so the app uses ExtendedSettings
25
+ (with per-component embedder/reranker config) instead of the base
26
+ Settings class.
27
+ """
28
+ settings = ExtendedSettings()
29
+
30
+ # -- Patch Settings class on config module --
31
+ # The @lru_cache get_settings() does `return Settings()`.
32
+ # Replacing the class on the module + clearing cache makes it
33
+ # return our ExtendedSettings (which has all base fields plus extras).
34
+ config_mod = importlib.import_module("graph_service.config")
35
+ config_mod.Settings = ExtendedSettings
36
+ config_mod.get_settings.cache_clear()
37
+
38
+ # -- Create the singleton client ONCE (loads BGE reranker weights) --
39
+ # Used for both index initialization and per-request dependency injection.
40
+ # Upstream get_graphiti creates/closes a client per-request, but POST
41
+ # /messages queues background work via AsyncWorker that outlives the
42
+ # request scope. A process-lifetime singleton avoids "Driver closed" errors.
43
+ singleton_client = create_graphiti(settings)
44
+
45
+ # -- Patch initialize_graphiti on upstream modules --
46
+ # main.py does `from graph_service.zep_graphiti import initialize_graphiti`
47
+ # creating a local binding we must also replace.
48
+ zep_mod = importlib.import_module("graph_service.zep_graphiti")
49
+
50
+ async def patched_initialize_graphiti(s=None):
51
+ """Initialize graph DB indices with retry for Neo4j readiness."""
52
+ for attempt in range(1, MAX_RETRIES + 1):
53
+ try:
54
+ await singleton_client.build_indices_and_constraints()
55
+ logger.info("Graph indices built successfully (attempt %d)", attempt)
56
+ return
57
+ except Exception as e:
58
+ logger.warning(
59
+ "build_indices_and_constraints failed (attempt %d/%d): %s",
60
+ attempt, MAX_RETRIES, e,
61
+ )
62
+ if attempt == MAX_RETRIES:
63
+ logger.error("All %d attempts failed, giving up", MAX_RETRIES)
64
+ raise
65
+ delay = RETRY_BASE_DELAY * (2 ** (attempt - 1))
66
+ logger.info("Retrying in %ds...", delay)
67
+ await asyncio.sleep(delay)
68
+
69
+ zep_mod.initialize_graphiti = patched_initialize_graphiti
70
+
71
+ main_mod = importlib.import_module("graph_service.main")
72
+ main_mod.initialize_graphiti = patched_initialize_graphiti
73
+
74
+ # -- Override get_graphiti via FastAPI dependency_overrides --
75
+ # This is the proper mechanism: app.dependency_overrides replaces
76
+ # the function that Depends(get_graphiti) captured at import time.
77
+ # We must import the app AFTER patching initialize_graphiti above
78
+ # (the app's lifespan calls initialize_graphiti on startup).
79
+ from graph_service.main import app
80
+
81
+ original_get_graphiti = zep_mod.get_graphiti
82
+
83
+ async def patched_get_graphiti(settings_dep=None):
84
+ """Yield the long-lived Graphiti client."""
85
+ yield singleton_client
86
+
87
+ app.dependency_overrides[original_get_graphiti] = patched_get_graphiti
88
+
89
+ # -- Fix upstream AsyncWorker crash-on-error bug --
90
+ # The worker loop only catches CancelledError; any other exception from
91
+ # add_episode() kills the worker silently and no more jobs are processed.
92
+ ingest_mod = importlib.import_module("graph_service.routers.ingest")
93
+
94
+ async def resilient_worker(self):
95
+ """Worker loop with exception logging and recovery."""
96
+ while True:
97
+ try:
98
+ job = await self.queue.get()
99
+ logger.info("AsyncWorker processing job (queue size: %d)", self.queue.qsize())
100
+ await job()
101
+ except asyncio.CancelledError:
102
+ break
103
+ except Exception:
104
+ logger.exception("AsyncWorker job failed")
105
+
106
+ ingest_mod.AsyncWorker.worker = resilient_worker
107
+
108
+ # -- Fix Neo4j CypherTypeError for nested attribute maps --
109
+ # graphiti-core does `entity_data.update(node.attributes or {})` for nodes
110
+ # and `edge_data.update(edge.attributes or {})` for edges, merging raw
111
+ # LLM-extracted attributes directly into Neo4j properties.
112
+ # Some LLMs (e.g., qwen2.5 via Ollama) return nested dicts/lists for
113
+ # attributes, which Neo4j rejects: "Property values can only be of
114
+ # primitive types." Sanitize both entity nodes AND entity edges.
115
+ import json
116
+
117
+ bulk_mod = importlib.import_module("graphiti_core.utils.bulk_utils")
118
+ original_bulk_add = bulk_mod.add_nodes_and_edges_bulk
119
+
120
+ def _sanitize_attributes(attrs):
121
+ """Flatten non-primitive attribute values to JSON strings for Neo4j."""
122
+ if not attrs:
123
+ return attrs
124
+ sanitized = {}
125
+ for k, v in attrs.items():
126
+ if isinstance(v, (dict, list, set, tuple)):
127
+ sanitized[k] = json.dumps(v, default=str)
128
+ else:
129
+ sanitized[k] = v
130
+ return sanitized
131
+
132
+ async def patched_bulk_add(driver, episodic_nodes, episodic_edges,
133
+ entity_nodes, entity_edges, embedder):
134
+ for node in entity_nodes:
135
+ if node.attributes:
136
+ node.attributes = _sanitize_attributes(node.attributes)
137
+ for edge in entity_edges:
138
+ if edge.attributes:
139
+ edge.attributes = _sanitize_attributes(edge.attributes)
140
+ return await original_bulk_add(
141
+ driver, episodic_nodes, episodic_edges,
142
+ entity_nodes, entity_edges, embedder,
143
+ )
144
+
145
+ bulk_mod.add_nodes_and_edges_bulk = patched_bulk_add
146
+
147
+ # Also patch the local binding in graphiti.py (uses `from ... import`)
148
+ graphiti_mod = importlib.import_module("graphiti_core.graphiti")
149
+ graphiti_mod.add_nodes_and_edges_bulk = patched_bulk_add
150
+
151
+ # -- Fix TypeError in extract_edges when LLM returns None for node indices --
152
+ # graphiti-core does `if not (-1 < source_idx < len(nodes) and -1 < target_idx < len(nodes))`
153
+ # which crashes with TypeError if the LLM returns None for an index.
154
+ #
155
+ # Primary fix: filter bad edges at model-parse level so valid edges from the same
156
+ # episode are preserved. Only applies when the Edge model has int fields (old
157
+ # index-based validation); a no-op for newer name-based validation.
158
+ # Fallback: catch TypeError from the whole function (loses all edges for that episode).
159
+ edge_ops_mod = importlib.import_module(
160
+ "graphiti_core.utils.maintenance.edge_operations"
161
+ )
162
+ original_extract_edges = edge_ops_mod.extract_edges
163
+
164
+ try:
165
+ _ep_mod = importlib.import_module("graphiti_core.prompts.extract_edges")
166
+ _OrigExtractedEdges = _ep_mod.ExtractedEdges
167
+ _OrigEdge = _ep_mod.Edge
168
+
169
+ # Collect Optional[int] fields (present in old index-based Edge model)
170
+ _int_fields = []
171
+ for _fname, _finfo in _OrigEdge.model_fields.items():
172
+ _ann = _finfo.annotation
173
+ if _ann is int:
174
+ _int_fields.append(_fname)
175
+ elif hasattr(_ann, "__args__") and int in _ann.__args__:
176
+ _int_fields.append(_fname)
177
+
178
+ if _int_fields:
179
+ _orig_mv = _OrigExtractedEdges.model_validate
180
+
181
+ @classmethod # type: ignore[misc]
182
+ def _filtered_model_validate(cls, obj, *a, **kw):
183
+ result = _orig_mv.__func__(cls, obj, *a, **kw)
184
+ if result.edges:
185
+ valid = [e for e in result.edges if all(getattr(e, f) is not None for f in _int_fields)]
186
+ dropped = len(result.edges) - len(valid)
187
+ if dropped:
188
+ logger.warning("extract_edges: filtered %d edge(s) with None index fields", dropped)
189
+ result.edges = valid
190
+ return result
191
+
192
+ _OrigExtractedEdges.model_validate = _filtered_model_validate
193
+ logger.info("Patched ExtractedEdges.model_validate to filter None int fields: %s", _int_fields)
194
+ else:
195
+ logger.info("ExtractedEdges uses name-based validation — None-index filter not needed")
196
+ except Exception as _patch_err:
197
+ logger.warning("Could not patch ExtractedEdges for None-index filtering: %s", _patch_err)
198
+
199
+ # Fallback: catch any remaining TypeError from the whole function
200
+ async def safe_extract_edges(*args, **kwargs):
201
+ try:
202
+ return await original_extract_edges(*args, **kwargs)
203
+ except TypeError as e:
204
+ if "not supported between instances" in str(e):
205
+ logger.warning("extract_edges skipped due to LLM output issue: %s", e)
206
+ return []
207
+ raise
208
+
209
+ edge_ops_mod.extract_edges = safe_extract_edges
210
+ # Patch the local binding in graphiti.py
211
+ graphiti_mod.extract_edges = safe_extract_edges
212
+
213
+ return app
214
+
215
+
216
+ if __name__ == "__main__":
217
+ import uvicorn
218
+
219
+ app = patch()
220
+
221
+ port = ExtendedSettings().port
222
+ uvicorn.run(app, host="0.0.0.0", port=port)
@@ -0,0 +1,79 @@
1
+ ###############################################################################
2
+ # openclaw-memory-rebac — SpiceDB authorization stack (standalone)
3
+ #
4
+ # Services:
5
+ # postgres — SpiceDB backing store (PostgreSQL 16)
6
+ # spicedb-migrate — one-shot schema migration
7
+ # spicedb — authorization engine (gRPC :50051, HTTP :8080)
8
+ #
9
+ # Usage:
10
+ # cp .env.example .env # set SPICEDB_PRESHARED_KEY
11
+ # docker compose up -d
12
+ #
13
+ # SpiceDB endpoint: localhost:50051 (insecure by default)
14
+ ###############################################################################
15
+
16
+ name: openclaw-spicedb
17
+
18
+ services:
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # PostgreSQL — SpiceDB backing store
22
+ # ---------------------------------------------------------------------------
23
+ postgres:
24
+ image: postgres:16-alpine
25
+ restart: unless-stopped
26
+ environment:
27
+ POSTGRES_USER: spicedb
28
+ POSTGRES_PASSWORD: ${SPICEDB_POSTGRES_PASSWORD:-spicedb}
29
+ POSTGRES_DB: spicedb
30
+ volumes:
31
+ - graphiti_spicedb_postgres_data:/var/lib/postgresql/data
32
+ healthcheck:
33
+ test: ["CMD-SHELL", "pg_isready -U spicedb"]
34
+ interval: 5s
35
+ retries: 10
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # SpiceDB migration (one-shot)
39
+ # ---------------------------------------------------------------------------
40
+ spicedb-migrate:
41
+ image: authzed/spicedb:latest
42
+ command: migrate head
43
+ environment:
44
+ SPICEDB_DATASTORE_ENGINE: postgres
45
+ SPICEDB_DATASTORE_CONN_URI: "postgres://spicedb:${SPICEDB_POSTGRES_PASSWORD:-spicedb}@postgres:5432/spicedb?sslmode=disable"
46
+ depends_on:
47
+ postgres:
48
+ condition: service_healthy
49
+ restart: "no"
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # SpiceDB — authorization engine
53
+ # ---------------------------------------------------------------------------
54
+ spicedb:
55
+ image: authzed/spicedb:latest
56
+ command: serve
57
+ restart: unless-stopped
58
+ ports:
59
+ - "${SPICEDB_GRPC_PORT:-50051}:50051" # gRPC
60
+ - "${SPICEDB_HTTP_PORT:-8080}:8080" # HTTP metrics / healthz
61
+ environment:
62
+ SPICEDB_GRPC_PRESHARED_KEY: ${SPICEDB_PRESHARED_KEY:-dev_token}
63
+ SPICEDB_DATASTORE_ENGINE: postgres
64
+ SPICEDB_DATASTORE_CONN_URI: "postgres://spicedb:${SPICEDB_POSTGRES_PASSWORD:-spicedb}@postgres:5432/spicedb?sslmode=disable"
65
+ SPICEDB_GRPC_NO_TLS: "${SPICEDB_GRPC_NO_TLS:-true}"
66
+ depends_on:
67
+ spicedb-migrate:
68
+ condition: service_completed_successfully
69
+ postgres:
70
+ condition: service_healthy
71
+ healthcheck:
72
+ test: ["CMD", "grpc_health_probe", "-addr=localhost:50051"]
73
+ interval: 10s
74
+ timeout: 5s
75
+ retries: 5
76
+ start_period: 10s
77
+
78
+ volumes:
79
+ graphiti_spicedb_postgres_data: