aindy-sdk 1.0.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,371 @@
1
+ Metadata-Version: 2.4
2
+ Name: aindy-sdk
3
+ Version: 1.0.0
4
+ Summary: Client SDK for the A.I.N.D.Y. runtime API
5
+ Author: Shawn Knight
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest==9.0.2; extra == "test"
14
+ Requires-Dist: pytest-cov==7.0.0; extra == "test"
15
+
16
+ # aindy-sdk
17
+
18
+ Universal interface SDK for the A.I.N.D.Y. runtime API.
19
+
20
+ `aindy-sdk` is the local+cloud bridge: the same client code works against a
21
+ locally-installed runtime (where the operator owns the infrastructure) and a
22
+ cloud-hosted runtime (where the provider owns the infrastructure). Switching
23
+ deployment context requires only a `base_url` change — no application code
24
+ changes.
25
+
26
+ Part of the Masterplan Infinite Weave ecosystem by Shawn Knight.
27
+ Runtime infrastructure: https://github.com/Masterplanner25/aindy-runtime
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install aindy-sdk
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.11+
38
+ - No external dependencies (stdlib only) for the API client
39
+ - Watcher subpackage optional dependencies: `psutil` (fallback window detection),
40
+ `pyobjc-framework-AppKit` + `pyobjc-framework-Quartz` (macOS window titles),
41
+ `xdotool` system package (Linux window detection)
42
+
43
+ ---
44
+
45
+ ## Getting started
46
+
47
+ ### 1. Start the runtime
48
+
49
+ Install and start `aindy-runtime` ([full instructions](https://github.com/Masterplanner25/aindy-runtime#quickstart)):
50
+
51
+ ```bash
52
+ git clone https://github.com/Masterplanner25/aindy-runtime.git
53
+ cd aindy-runtime
54
+ cp AINDY/.env.example AINDY/.env # set SECRET_KEY and OPENAI_API_KEY at minimum
55
+ docker compose up -d
56
+ ```
57
+
58
+ Wait until the server is ready:
59
+
60
+ ```bash
61
+ curl http://localhost:8000/ready # → {"status": "ok", ...}
62
+ ```
63
+
64
+ ### 2. Register and log in
65
+
66
+ ```bash
67
+ # Register
68
+ curl -s -X POST http://localhost:8000/auth/register \
69
+ -H "Content-Type: application/json" \
70
+ -d '{"email": "you@example.com", "password": "yourpassword", "display_name": "You"}' \
71
+ | python -m json.tool
72
+
73
+ # Log in — copy the access_token from the response
74
+ curl -s -X POST http://localhost:8000/auth/login \
75
+ -H "Content-Type: application/json" \
76
+ -d '{"email": "you@example.com", "password": "yourpassword"}' \
77
+ | python -m json.tool
78
+ ```
79
+
80
+ To promote your account to admin (required to create API keys via the CLI):
81
+
82
+ ```bash
83
+ # Using the CLI (no server restart needed)
84
+ aindy-runtime auth promote-admin you@example.com
85
+
86
+ # Or via env var (requires server restart)
87
+ # Add AINDY_BOOTSTRAP_ADMIN_EMAIL=you@example.com to AINDY/.env, then docker compose restart api
88
+ ```
89
+
90
+ ### 3. Create a Platform API key
91
+
92
+ Platform API keys start with `aindy_` and are shown only once — save the `key` field.
93
+
94
+ ```bash
95
+ JWT="<your-access_token>"
96
+
97
+ curl -s -X POST http://localhost:8000/platform/keys \
98
+ -H "Authorization: Bearer $JWT" \
99
+ -H "Content-Type: application/json" \
100
+ -d '{
101
+ "name": "my-app",
102
+ "scopes": ["memory.read", "memory.write", "flow.run", "event.emit"]
103
+ }' \
104
+ | python -m json.tool
105
+ ```
106
+
107
+ **Available scopes:**
108
+
109
+ | Scope | Grants access to |
110
+ |---|---|
111
+ | `memory.read` | Read and search memory nodes |
112
+ | `memory.write` | Write and delete memory nodes |
113
+ | `flow.run` | Trigger flow runs |
114
+ | `event.emit` | Emit events to the event bus |
115
+ | `syscall.*` | Full syscall registry access |
116
+ | `execution.read` | Read execution unit status |
117
+
118
+ ### 4. First call
119
+
120
+ ```python
121
+ from aindy_sdk import AINDYClient
122
+
123
+ client = AINDYClient(
124
+ base_url="http://localhost:8000",
125
+ api_key="aindy_your_platform_key",
126
+ )
127
+
128
+ result = client.memory.read("/memory/you/notes/**", limit=5)
129
+ print(result["data"]["nodes"])
130
+ ```
131
+
132
+ Run the full example:
133
+
134
+ ```bash
135
+ AINDY_API_KEY=aindy_... python examples/quickstart.py
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Usage
141
+
142
+ ```python
143
+ from aindy_sdk import AINDYClient
144
+
145
+ # Local runtime (operator owns infrastructure)
146
+ client = AINDYClient(
147
+ base_url="http://localhost:8000",
148
+ api_key="aindy_your_platform_key",
149
+ )
150
+
151
+ # Cloud-hosted — same code, different base_url
152
+ # client = AINDYClient(base_url="https://runtime.aindy.ai", api_key="...")
153
+
154
+ # Read memory
155
+ result = client.memory.read("/memory/shawn/entities/**", limit=5)
156
+ nodes = result["data"]["nodes"]
157
+
158
+ # Run a flow
159
+ analysis = client.flow.run("analyze_entities", {"data": nodes})
160
+
161
+ # Write memory
162
+ client.memory.write(
163
+ "/memory/shawn/insights/outcome",
164
+ analysis["data"].get("summary", ""),
165
+ tags=["sdk-demo"],
166
+ )
167
+
168
+ # Emit an event
169
+ client.events.emit("analysis.completed", {"node_count": len(nodes)})
170
+ ```
171
+
172
+ ---
173
+
174
+ ## API reference
175
+
176
+ All capabilities are accessed through namespaces on `AINDYClient`:
177
+
178
+ | Namespace | Description |
179
+ |---|---|
180
+ | `client.memory` | Read, write, and search memory nodes by MAS path or vector similarity |
181
+ | `client.flow` | Trigger flow runs and poll their status |
182
+ | `client.events` | Emit events to the runtime event bus |
183
+ | `client.nodus` | Compile and execute Nodus scripts |
184
+ | `client.syscalls` | List and inspect the syscall registry |
185
+ | `client.execution` | Introspect in-flight or completed execution units |
186
+ | `client.sandbox` | Query sandbox assurance posture |
187
+
188
+ Every method returns a **syscall envelope**:
189
+
190
+ ```python
191
+ {
192
+ "status": "success", # "success" or "error"
193
+ "data": { ... }, # payload — varies by call
194
+ "trace_id": "abc123...", # for log correlation
195
+ "duration_ms": 12, # server-side execution time in ms
196
+ "error": None, # error description when status == "error"
197
+ }
198
+ ```
199
+
200
+ ### Memory (`client.memory`)
201
+
202
+ ```python
203
+ # Read nodes at a MAS path (glob patterns supported)
204
+ result = client.memory.read("/memory/shawn/entities/**", limit=10)
205
+ nodes = result["data"]["nodes"]
206
+
207
+ # Write a node
208
+ result = client.memory.write(
209
+ "/memory/shawn/insights/daily-summary",
210
+ "Worked on SDK documentation today.",
211
+ tags=["insight", "daily"],
212
+ node_type="insight",
213
+ )
214
+ node_id = result["data"]["node"]["id"]
215
+
216
+ # Semantic similarity search
217
+ result = client.memory.search("recent project decisions", limit=5)
218
+
219
+ # Delete a node
220
+ client.memory.delete(node_id)
221
+ ```
222
+
223
+ ### Flow (`client.flow`)
224
+
225
+ ```python
226
+ # Trigger a flow run and get the result
227
+ result = client.flow.run("analyze_entities", {"data": nodes})
228
+ print(result["status"]) # "success" | "waiting" | "failed"
229
+
230
+ # For long-running flows: trigger and poll
231
+ result = client.flow.trigger("long_analysis", {"input": "..."})
232
+ run_id = result["data"]["run_id"]
233
+
234
+ import time
235
+ while True:
236
+ status = client.execution.get(run_id)
237
+ if status["data"]["status"] not in ("running", "waiting"):
238
+ break
239
+ time.sleep(2)
240
+ ```
241
+
242
+ ### Events (`client.events`)
243
+
244
+ ```python
245
+ # Emit an event (can trigger waiting flows)
246
+ result = client.events.emit(
247
+ "user.action.completed",
248
+ {"user_id": "u-123", "action": "document_saved"},
249
+ )
250
+ ```
251
+
252
+ ### Nodus (`client.nodus`)
253
+
254
+ ```python
255
+ # Execute a Nodus script
256
+ result = client.nodus.run_script(
257
+ script="""
258
+ let greeting = "Hello from Nodus"
259
+ set_state("message", greeting)
260
+ emit("script.hello", {text: greeting})
261
+ """,
262
+ input={},
263
+ )
264
+ print(result["nodus_status"]) # "complete" | "wait" | "error"
265
+ print(result["output_state"]) # dict of set_state() calls
266
+ print(result["events_emitted"]) # count of emit() calls
267
+ ```
268
+
269
+ ### Execution (`client.execution`)
270
+
271
+ ```python
272
+ run = client.execution.get("run-abc123")
273
+ status = run["data"]["status"] # "running" | "success" | "failed" | "waiting"
274
+ syscall_count = run["data"]["syscall_count"]
275
+ wall_time_ms = run["data"]["wall_time_ms"] # accumulated wall-clock time (includes I/O wait)
276
+ ```
277
+
278
+ ### Syscalls (`client.syscalls`)
279
+
280
+ ```python
281
+ registry = client.syscalls.list(version="v1")
282
+ print(registry["total_count"])
283
+ for action, spec in registry["syscalls"]["v1"].items():
284
+ deprecated = " [DEPRECATED]" if spec.get("deprecated") else ""
285
+ print(f"sys.v1.{action}{deprecated}")
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Error handling
291
+
292
+ The SDK maps HTTP status codes to typed exceptions so you can handle failure
293
+ cases explicitly rather than inspecting status codes:
294
+
295
+ ```python
296
+ from aindy_sdk import AINDYClient
297
+ from aindy_sdk.exceptions import (
298
+ AuthenticationError, # 401 — key missing, invalid, or expired
299
+ PermissionDeniedError, # 403 — key scope doesn't include this capability
300
+ NotFoundError, # 404 — resource doesn't exist
301
+ ValidationError, # 422 — bad request shape or schema violation
302
+ ResourceLimitError, # 429 — syscall count or wall_time_ms exceeded
303
+ ServerError, # 5xx — unexpected runtime error
304
+ NetworkError, # connection refused, timeout, or DNS failure
305
+ AINDYError, # base class — catches all of the above
306
+ )
307
+
308
+ client = AINDYClient(base_url="http://localhost:8000", api_key="aindy_...")
309
+
310
+ try:
311
+ result = client.memory.read("/memory/shawn/entities/**", limit=5)
312
+ except AuthenticationError:
313
+ print("Invalid API key — check AINDY_API_KEY")
314
+ except PermissionDeniedError as exc:
315
+ print(f"Key scope missing: {exc.message}")
316
+ # exc.response contains the full server error payload
317
+ except ValidationError as exc:
318
+ print(f"Bad request: {exc.message}")
319
+ except NetworkError as exc:
320
+ print(f"Server unreachable: {exc.cause}")
321
+ except AINDYError as exc:
322
+ print(f"Runtime error [{exc.status_code}]: {exc.message}")
323
+ ```
324
+
325
+ All typed errors carry:
326
+ - `.message` — human-readable description
327
+ - `.status_code` — HTTP status code (`None` for `NetworkError`)
328
+ - `.response` — raw server JSON dict
329
+
330
+ ---
331
+
332
+ ## Watcher
333
+
334
+ The SDK ships the A.I.N.D.Y. Watcher client process. The watcher runs on your
335
+ machine, observes the active OS window, classifies your activity, and emits
336
+ structured signals to the runtime for flow triggering and session tracking.
337
+
338
+ ```bash
339
+ # Start the watcher (requires AINDY_API_KEY and AINDY_WATCHER_API_URL)
340
+ python -m aindy_sdk.watcher.watcher
341
+
342
+ # Dry run — logs signals, no HTTP requests
343
+ python -m aindy_sdk.watcher.watcher --dry-run
344
+
345
+ # Custom poll interval and verbose logging
346
+ python -m aindy_sdk.watcher.watcher --poll-interval 10 --log-level DEBUG
347
+ ```
348
+
349
+ Environment variables:
350
+
351
+ | Variable | Default | Description |
352
+ |---|---|---|
353
+ | `AINDY_WATCHER_API_URL` | `http://localhost:8000` | Runtime base URL |
354
+ | `AINDY_API_KEY` | _(required)_ | Service API key (`X-API-Key` header) |
355
+ | `AINDY_WATCHER_DRY_RUN` | `false` | Log signals only, no HTTP POST |
356
+ | `AINDY_WATCHER_POLL_INTERVAL` | `5` | Seconds between window samples |
357
+ | `AINDY_WATCHER_FLUSH_INTERVAL` | `10` | Seconds between signal batch flushes |
358
+ | `AINDY_WATCHER_BATCH_SIZE` | `20` | Signals per POST batch |
359
+ | `AINDY_WATCHER_CONFIRMATION_DELAY` | `30` | Seconds of focus before `session_started` fires |
360
+ | `AINDY_WATCHER_DISTRACTION_TIMEOUT` | `60` | Seconds of off-task time before distraction state |
361
+ | `AINDY_WATCHER_RECOVERY_DELAY` | `30` | Seconds of focus to recover from distraction |
362
+ | `AINDY_WATCHER_HEARTBEAT_INTERVAL` | `300` | Seconds between heartbeat signals |
363
+ | `AINDY_WATCHER_LOG_LEVEL` | `INFO` | `DEBUG` \| `INFO` \| `WARNING` \| `ERROR` |
364
+
365
+ See `examples/watcher_demo.py` for a dry-run demonstration.
366
+
367
+ ---
368
+
369
+ ## Status
370
+
371
+ Beta. Part of the aindy-runtime 1.0.0 ecosystem.