mindgraph-sdk 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.
@@ -0,0 +1,25 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ environment: pypi
13
+ permissions:
14
+ id-token: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: '3.12'
21
+
22
+ - run: pip install build
23
+ - run: python -m build
24
+
25
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: mindgraph-sdk
3
+ Version: 0.1.0
4
+ Summary: Python client for the MindGraph Cloud API
5
+ License-Expression: MIT
6
+ Keywords: agent,ai,cognitive,knowledge-graph,mindgraph
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: httpx>=0.25
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest; extra == 'dev'
11
+ Requires-Dist: pytest-asyncio; extra == 'dev'
12
+ Requires-Dist: ruff; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # mindgraph
16
+
17
+ [![PyPI](https://img.shields.io/pypi/v/mindgraph)](https://pypi.org/project/mindgraph/)
18
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
19
+
20
+ Python client for the [MindGraph Cloud](https://mindgraph.cloud) API — a structured semantic memory graph for AI agents.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install mindgraph
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```python
31
+ from mindgraph import MindGraph
32
+
33
+ with MindGraph("https://api.mindgraph.cloud", api_key="mg_...") as graph:
34
+ # Add a node
35
+ node = graph.add_node(
36
+ label="User prefers dark mode",
37
+ node_type="Preference",
38
+ )
39
+
40
+ # Search
41
+ results = graph.search("what does the user prefer?")
42
+
43
+ # Connect knowledge
44
+ graph.add_link(
45
+ from_uid=node["uid"],
46
+ to_uid="user_abc",
47
+ edge_type="BelongsTo",
48
+ )
49
+ ```
50
+
51
+ ## API Reference
52
+
53
+ ### Constructor
54
+
55
+ ```python
56
+ MindGraph(base_url, *, api_key=None, jwt=None, timeout=30.0)
57
+ ```
58
+
59
+ Supports context manager protocol (`with` statement) for automatic cleanup.
60
+
61
+ ### Reality Layer
62
+
63
+ | Method | Description |
64
+ |--------|-------------|
65
+ | `ingest(**kwargs)` | Ingest a source, snippet, or observation |
66
+ | `entity(**kwargs)` | Create, alias, resolve, or merge entities |
67
+ | `find_or_create_entity(label, entity_type?, agent_id?)` | Convenience: create or find an entity by label |
68
+
69
+ ### Epistemic Layer
70
+
71
+ | Method | Description |
72
+ |--------|-------------|
73
+ | `argue(**kwargs)` | Construct a full argument: claim + evidence + warrant + edges |
74
+ | `inquire(**kwargs)` | Add hypothesis, theory, paradigm, anomaly, assumption, or question |
75
+ | `structure(**kwargs)` | Add concept, pattern, mechanism, model, analogy, theorem, etc. |
76
+
77
+ ### Intent Layer
78
+
79
+ | Method | Description |
80
+ |--------|-------------|
81
+ | `commit(**kwargs)` | Create a goal, project, or milestone |
82
+ | `deliberate(**kwargs)` | Open decisions, add options/constraints, resolve decisions |
83
+
84
+ ### Action Layer
85
+
86
+ | Method | Description |
87
+ |--------|-------------|
88
+ | `procedure(**kwargs)` | Build flows, add steps, affordances, and controls |
89
+ | `risk(**kwargs)` | Assess risk or retrieve existing assessments |
90
+
91
+ ### Memory Layer
92
+
93
+ | Method | Description |
94
+ |--------|-------------|
95
+ | `session(**kwargs)` | Open a session, record traces, or close a session |
96
+ | `distill(**kwargs)` | Create a summary that distills multiple source nodes |
97
+ | `memory_config(**kwargs)` | Set/get preferences and memory policies |
98
+
99
+ ### Agent Layer
100
+
101
+ | Method | Description |
102
+ |--------|-------------|
103
+ | `plan(**kwargs)` | Create tasks, plans, plan steps, update status |
104
+ | `governance(**kwargs)` | Create policies, set safety budgets, request/resolve approvals |
105
+ | `execution(**kwargs)` | Track execution lifecycle and register agents |
106
+
107
+ ### CRUD
108
+
109
+ | Method | Description |
110
+ |--------|-------------|
111
+ | `get_node(uid)` | Get a node by UID |
112
+ | `add_node(label, node_type?, props?, agent_id?)` | Add a generic node |
113
+ | `update_node(uid, **kwargs)` | Update node fields |
114
+ | `delete_node(uid)` | Tombstone a node and all connected edges |
115
+ | `add_link(from_uid, to_uid, edge_type, agent_id?)` | Add a typed edge |
116
+ | `get_edges(from_uid?, to_uid?)` | Get edges by source or target |
117
+
118
+ ### Search
119
+
120
+ | Method | Description |
121
+ |--------|-------------|
122
+ | `search(query, node_type?, layer?, limit?)` | Full-text search |
123
+ | `hybrid_search(query, k?, node_types?, layer?)` | BM25 + vector search with rank fusion |
124
+
125
+ ### Traversal
126
+
127
+ | Method | Description |
128
+ |--------|-------------|
129
+ | `reasoning_chain(uid, max_depth=5)` | Follow epistemic edges from a node |
130
+ | `neighborhood(uid, max_depth=1)` | Get all nodes within N hops |
131
+
132
+ ### Cross-cutting
133
+
134
+ | Method | Description |
135
+ |--------|-------------|
136
+ | `retrieve(**kwargs)` | Unified retrieval: text search, active goals, open questions, weak claims |
137
+ | `traverse(**kwargs)` | Graph traversal: chain, neighborhood, path, or subgraph |
138
+ | `evolve(**kwargs)` | Lifecycle mutations: update, tombstone, restore, decay, history |
139
+
140
+ ### Health & Stats
141
+
142
+ | Method | Description |
143
+ |--------|-------------|
144
+ | `health()` | Health check |
145
+ | `stats()` | Graph-wide statistics |
146
+
147
+ ## Error Handling
148
+
149
+ All methods raise `MindGraphError` on HTTP errors:
150
+
151
+ ```python
152
+ from mindgraph import MindGraphError
153
+
154
+ try:
155
+ graph.get_node("nonexistent")
156
+ except MindGraphError as e:
157
+ print(e.status, e.body)
158
+ ```
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,148 @@
1
+ # mindgraph
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/mindgraph)](https://pypi.org/project/mindgraph/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+
6
+ Python client for the [MindGraph Cloud](https://mindgraph.cloud) API — a structured semantic memory graph for AI agents.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install mindgraph
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ```python
17
+ from mindgraph import MindGraph
18
+
19
+ with MindGraph("https://api.mindgraph.cloud", api_key="mg_...") as graph:
20
+ # Add a node
21
+ node = graph.add_node(
22
+ label="User prefers dark mode",
23
+ node_type="Preference",
24
+ )
25
+
26
+ # Search
27
+ results = graph.search("what does the user prefer?")
28
+
29
+ # Connect knowledge
30
+ graph.add_link(
31
+ from_uid=node["uid"],
32
+ to_uid="user_abc",
33
+ edge_type="BelongsTo",
34
+ )
35
+ ```
36
+
37
+ ## API Reference
38
+
39
+ ### Constructor
40
+
41
+ ```python
42
+ MindGraph(base_url, *, api_key=None, jwt=None, timeout=30.0)
43
+ ```
44
+
45
+ Supports context manager protocol (`with` statement) for automatic cleanup.
46
+
47
+ ### Reality Layer
48
+
49
+ | Method | Description |
50
+ |--------|-------------|
51
+ | `ingest(**kwargs)` | Ingest a source, snippet, or observation |
52
+ | `entity(**kwargs)` | Create, alias, resolve, or merge entities |
53
+ | `find_or_create_entity(label, entity_type?, agent_id?)` | Convenience: create or find an entity by label |
54
+
55
+ ### Epistemic Layer
56
+
57
+ | Method | Description |
58
+ |--------|-------------|
59
+ | `argue(**kwargs)` | Construct a full argument: claim + evidence + warrant + edges |
60
+ | `inquire(**kwargs)` | Add hypothesis, theory, paradigm, anomaly, assumption, or question |
61
+ | `structure(**kwargs)` | Add concept, pattern, mechanism, model, analogy, theorem, etc. |
62
+
63
+ ### Intent Layer
64
+
65
+ | Method | Description |
66
+ |--------|-------------|
67
+ | `commit(**kwargs)` | Create a goal, project, or milestone |
68
+ | `deliberate(**kwargs)` | Open decisions, add options/constraints, resolve decisions |
69
+
70
+ ### Action Layer
71
+
72
+ | Method | Description |
73
+ |--------|-------------|
74
+ | `procedure(**kwargs)` | Build flows, add steps, affordances, and controls |
75
+ | `risk(**kwargs)` | Assess risk or retrieve existing assessments |
76
+
77
+ ### Memory Layer
78
+
79
+ | Method | Description |
80
+ |--------|-------------|
81
+ | `session(**kwargs)` | Open a session, record traces, or close a session |
82
+ | `distill(**kwargs)` | Create a summary that distills multiple source nodes |
83
+ | `memory_config(**kwargs)` | Set/get preferences and memory policies |
84
+
85
+ ### Agent Layer
86
+
87
+ | Method | Description |
88
+ |--------|-------------|
89
+ | `plan(**kwargs)` | Create tasks, plans, plan steps, update status |
90
+ | `governance(**kwargs)` | Create policies, set safety budgets, request/resolve approvals |
91
+ | `execution(**kwargs)` | Track execution lifecycle and register agents |
92
+
93
+ ### CRUD
94
+
95
+ | Method | Description |
96
+ |--------|-------------|
97
+ | `get_node(uid)` | Get a node by UID |
98
+ | `add_node(label, node_type?, props?, agent_id?)` | Add a generic node |
99
+ | `update_node(uid, **kwargs)` | Update node fields |
100
+ | `delete_node(uid)` | Tombstone a node and all connected edges |
101
+ | `add_link(from_uid, to_uid, edge_type, agent_id?)` | Add a typed edge |
102
+ | `get_edges(from_uid?, to_uid?)` | Get edges by source or target |
103
+
104
+ ### Search
105
+
106
+ | Method | Description |
107
+ |--------|-------------|
108
+ | `search(query, node_type?, layer?, limit?)` | Full-text search |
109
+ | `hybrid_search(query, k?, node_types?, layer?)` | BM25 + vector search with rank fusion |
110
+
111
+ ### Traversal
112
+
113
+ | Method | Description |
114
+ |--------|-------------|
115
+ | `reasoning_chain(uid, max_depth=5)` | Follow epistemic edges from a node |
116
+ | `neighborhood(uid, max_depth=1)` | Get all nodes within N hops |
117
+
118
+ ### Cross-cutting
119
+
120
+ | Method | Description |
121
+ |--------|-------------|
122
+ | `retrieve(**kwargs)` | Unified retrieval: text search, active goals, open questions, weak claims |
123
+ | `traverse(**kwargs)` | Graph traversal: chain, neighborhood, path, or subgraph |
124
+ | `evolve(**kwargs)` | Lifecycle mutations: update, tombstone, restore, decay, history |
125
+
126
+ ### Health & Stats
127
+
128
+ | Method | Description |
129
+ |--------|-------------|
130
+ | `health()` | Health check |
131
+ | `stats()` | Graph-wide statistics |
132
+
133
+ ## Error Handling
134
+
135
+ All methods raise `MindGraphError` on HTTP errors:
136
+
137
+ ```python
138
+ from mindgraph import MindGraphError
139
+
140
+ try:
141
+ graph.get_node("nonexistent")
142
+ except MindGraphError as e:
143
+ print(e.status, e.body)
144
+ ```
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,3 @@
1
+ from .client import MindGraph, MindGraphError
2
+
3
+ __all__ = ["MindGraph", "MindGraphError"]
@@ -0,0 +1,300 @@
1
+ """MindGraph Python client for the MindGraph Cloud API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+
10
+ class MindGraphError(Exception):
11
+ def __init__(self, message: str, status: int, body: Any = None):
12
+ super().__init__(message)
13
+ self.status = status
14
+ self.body = body
15
+
16
+
17
+ class MindGraph:
18
+ """Client for the MindGraph REST API."""
19
+
20
+ def __init__(
21
+ self,
22
+ base_url: str,
23
+ *,
24
+ api_key: str | None = None,
25
+ jwt: str | None = None,
26
+ timeout: float = 30.0,
27
+ ):
28
+ self.base_url = base_url.rstrip("/")
29
+ headers: dict[str, str] = {"Content-Type": "application/json"}
30
+ if api_key:
31
+ headers["Authorization"] = f"Bearer {api_key}"
32
+ elif jwt:
33
+ headers["Authorization"] = f"Bearer {jwt}"
34
+ self._client = httpx.Client(
35
+ base_url=self.base_url, headers=headers, timeout=timeout
36
+ )
37
+
38
+ def close(self) -> None:
39
+ self._client.close()
40
+
41
+ def __enter__(self) -> MindGraph:
42
+ return self
43
+
44
+ def __exit__(self, *args: Any) -> None:
45
+ self.close()
46
+
47
+ # ---- HTTP helpers ----
48
+
49
+ def _request(self, method: str, path: str, json: Any = None) -> Any:
50
+ resp = self._client.request(method, path, json=json)
51
+ if resp.status_code >= 400:
52
+ try:
53
+ body = resp.json()
54
+ except Exception:
55
+ body = resp.text
56
+ raise MindGraphError(
57
+ f"{method} {path} failed: {resp.status_code}",
58
+ resp.status_code,
59
+ body,
60
+ )
61
+ if not resp.content:
62
+ return None
63
+ return resp.json()
64
+
65
+ # ---- Health ----
66
+
67
+ def health(self) -> dict[str, str]:
68
+ return self._request("GET", "/health")
69
+
70
+ def stats(self) -> dict[str, Any]:
71
+ return self._request("GET", "/stats")
72
+
73
+ # ---- Reality Layer ----
74
+
75
+ def ingest(self, **kwargs: Any) -> Any:
76
+ return self._request("POST", "/reality/ingest", kwargs)
77
+
78
+ def entity(self, **kwargs: Any) -> Any:
79
+ return self._request("POST", "/reality/entity", kwargs)
80
+
81
+ def find_or_create_entity(
82
+ self,
83
+ label: str,
84
+ entity_type: str = "other",
85
+ agent_id: str | None = None,
86
+ ) -> dict[str, Any]:
87
+ body: dict[str, Any] = {
88
+ "action": "create",
89
+ "label": label,
90
+ "entity_type": entity_type,
91
+ }
92
+ if agent_id:
93
+ body["agent_id"] = agent_id
94
+ return self._request("POST", "/reality/entity", body)
95
+
96
+ # ---- Epistemic Layer ----
97
+
98
+ def argue(self, **kwargs: Any) -> Any:
99
+ return self._request("POST", "/epistemic/argument", kwargs)
100
+
101
+ def inquire(self, **kwargs: Any) -> Any:
102
+ return self._request("POST", "/epistemic/inquiry", kwargs)
103
+
104
+ def structure(self, **kwargs: Any) -> Any:
105
+ return self._request("POST", "/epistemic/structure", kwargs)
106
+
107
+ # ---- Intent Layer ----
108
+
109
+ def commit(self, **kwargs: Any) -> Any:
110
+ return self._request("POST", "/intent/commitment", kwargs)
111
+
112
+ def deliberate(self, **kwargs: Any) -> Any:
113
+ return self._request("POST", "/intent/deliberation", kwargs)
114
+
115
+ # ---- Action Layer ----
116
+
117
+ def procedure(self, **kwargs: Any) -> Any:
118
+ return self._request("POST", "/action/procedure", kwargs)
119
+
120
+ def risk(self, **kwargs: Any) -> Any:
121
+ return self._request("POST", "/action/risk", kwargs)
122
+
123
+ # ---- Memory Layer ----
124
+
125
+ def session(self, **kwargs: Any) -> Any:
126
+ return self._request("POST", "/memory/session", kwargs)
127
+
128
+ def distill(self, **kwargs: Any) -> Any:
129
+ return self._request("POST", "/memory/distill", kwargs)
130
+
131
+ def memory_config(self, **kwargs: Any) -> Any:
132
+ return self._request("POST", "/memory/config", kwargs)
133
+
134
+ # ---- Agent Layer ----
135
+
136
+ def plan(self, **kwargs: Any) -> Any:
137
+ return self._request("POST", "/agent/plan", kwargs)
138
+
139
+ def governance(self, **kwargs: Any) -> Any:
140
+ return self._request("POST", "/agent/governance", kwargs)
141
+
142
+ def execution(self, **kwargs: Any) -> Any:
143
+ return self._request("POST", "/agent/execution", kwargs)
144
+
145
+ # ---- Cross-cutting ----
146
+
147
+ def retrieve(self, **kwargs: Any) -> Any:
148
+ return self._request("POST", "/retrieve", kwargs)
149
+
150
+ def traverse(self, **kwargs: Any) -> Any:
151
+ return self._request("POST", "/traverse", kwargs)
152
+
153
+ def evolve(self, **kwargs: Any) -> Any:
154
+ return self._request("POST", "/evolve", kwargs)
155
+
156
+ # ---- Node CRUD ----
157
+
158
+ def get_node(self, uid: str) -> dict[str, Any]:
159
+ return self._request("GET", f"/node/{uid}")
160
+
161
+ def add_node(
162
+ self,
163
+ label: str,
164
+ node_type: str | None = None,
165
+ props: dict[str, Any] | None = None,
166
+ agent_id: str | None = None,
167
+ ) -> dict[str, Any]:
168
+ body: dict[str, Any] = {"label": label}
169
+ if node_type:
170
+ body["node_type"] = node_type
171
+ if props:
172
+ body["props"] = props
173
+ if agent_id:
174
+ body["agent_id"] = agent_id
175
+ return self._request("POST", "/node", body)
176
+
177
+ def update_node(
178
+ self, uid: str, **kwargs: Any
179
+ ) -> dict[str, Any]:
180
+ return self._request("PATCH", f"/node/{uid}", kwargs)
181
+
182
+ def delete_node(self, uid: str) -> None:
183
+ self._request("DELETE", f"/node/{uid}")
184
+
185
+ # ---- Edge CRUD ----
186
+
187
+ def add_link(
188
+ self,
189
+ from_uid: str,
190
+ to_uid: str,
191
+ edge_type: str,
192
+ agent_id: str | None = None,
193
+ ) -> Any:
194
+ body: dict[str, Any] = {
195
+ "from_uid": from_uid,
196
+ "to_uid": to_uid,
197
+ "edge_type": edge_type,
198
+ }
199
+ if agent_id:
200
+ body["agent_id"] = agent_id
201
+ return self._request("POST", "/link", body)
202
+
203
+ def get_edges(
204
+ self,
205
+ from_uid: str | None = None,
206
+ to_uid: str | None = None,
207
+ ) -> list[dict[str, Any]]:
208
+ params: dict[str, str] = {}
209
+ if from_uid:
210
+ params["from_uid"] = from_uid
211
+ if to_uid:
212
+ params["to_uid"] = to_uid
213
+ qs = "&".join(f"{k}={v}" for k, v in params.items())
214
+ return self._request("GET", f"/edges?{qs}")
215
+
216
+ # ---- Search ----
217
+
218
+ def search(
219
+ self,
220
+ query: str,
221
+ node_type: str | None = None,
222
+ layer: str | None = None,
223
+ limit: int | None = None,
224
+ ) -> list[dict[str, Any]]:
225
+ body: dict[str, Any] = {"query": query}
226
+ if node_type:
227
+ body["node_type"] = node_type
228
+ if layer:
229
+ body["layer"] = layer
230
+ if limit:
231
+ body["limit"] = limit
232
+ return self._request("POST", "/search", body)
233
+
234
+ def hybrid_search(
235
+ self,
236
+ query: str,
237
+ k: int | None = None,
238
+ node_types: list[str] | None = None,
239
+ layer: str | None = None,
240
+ ) -> list[dict[str, Any]]:
241
+ body: dict[str, Any] = {"action": "hybrid", "query": query}
242
+ if k:
243
+ body["k"] = k
244
+ if node_types:
245
+ body["node_types"] = node_types
246
+ if layer:
247
+ body["layer"] = layer
248
+ return self._request("POST", "/retrieve", body)
249
+
250
+ # ---- Traversal shortcuts ----
251
+
252
+ def reasoning_chain(self, uid: str, max_depth: int = 5) -> list[dict[str, Any]]:
253
+ return self._request(
254
+ "POST", "/traverse", {"action": "chain", "start_uid": uid, "max_depth": max_depth}
255
+ )
256
+
257
+ def neighborhood(self, uid: str, max_depth: int = 1) -> list[dict[str, Any]]:
258
+ return self._request(
259
+ "POST",
260
+ "/traverse",
261
+ {"action": "neighborhood", "start_uid": uid, "max_depth": max_depth},
262
+ )
263
+
264
+ # ---- Lifecycle shortcuts ----
265
+
266
+ def tombstone(
267
+ self, uid: str, reason: str | None = None, agent_id: str | None = None
268
+ ) -> Any:
269
+ body: dict[str, Any] = {"action": "tombstone", "uid": uid}
270
+ if reason:
271
+ body["reason"] = reason
272
+ if agent_id:
273
+ body["agent_id"] = agent_id
274
+ return self._request("POST", "/evolve", body)
275
+
276
+ def restore(self, uid: str, agent_id: str | None = None) -> Any:
277
+ body: dict[str, Any] = {"action": "restore", "uid": uid}
278
+ if agent_id:
279
+ body["agent_id"] = agent_id
280
+ return self._request("POST", "/evolve", body)
281
+
282
+ # ---- Management (Cloud only) ----
283
+
284
+ def signup(self, email: str, password: str) -> Any:
285
+ return self._request("POST", "/v1/auth/signup", {"email": email, "password": password})
286
+
287
+ def login(self, email: str, password: str) -> Any:
288
+ return self._request("POST", "/v1/auth/login", {"email": email, "password": password})
289
+
290
+ def create_api_key(self, name: str = "default") -> dict[str, Any]:
291
+ return self._request("POST", "/v1/api-keys", {"name": name})
292
+
293
+ def list_api_keys(self) -> dict[str, Any]:
294
+ return self._request("GET", "/v1/api-keys")
295
+
296
+ def revoke_api_key(self, key_id: str) -> None:
297
+ self._request("DELETE", f"/v1/api-keys/{key_id}")
298
+
299
+ def get_usage(self) -> Any:
300
+ return self._request("GET", "/v1/usage")
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mindgraph-sdk"
7
+ version = "0.1.0"
8
+ description = "Python client for the MindGraph Cloud API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ keywords = ["mindgraph", "knowledge-graph", "cognitive", "ai", "agent"]
13
+ dependencies = ["httpx>=0.25"]
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["mindgraph"]
17
+
18
+ [project.optional-dependencies]
19
+ dev = ["pytest", "pytest-asyncio", "ruff"]