puda 0.0.15__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.
- puda-0.0.15/PKG-INFO +337 -0
- puda-0.0.15/README.md +326 -0
- puda-0.0.15/pyproject.toml +31 -0
- puda-0.0.15/src/puda/__init__.py +19 -0
- puda-0.0.15/src/puda/command_service.py +850 -0
- puda-0.0.15/src/puda/edge_nats_client.py +1016 -0
- puda-0.0.15/src/puda/edge_runner.py +335 -0
- puda-0.0.15/src/puda/edge_updater.py +419 -0
- puda-0.0.15/src/puda/execution_state.py +89 -0
- puda-0.0.15/src/puda/models.py +96 -0
- puda-0.0.15/src/puda/run_manager.py +126 -0
- puda-0.0.15/src/puda/stream_subscriber.py +388 -0
puda-0.0.15/PKG-INFO
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: puda
|
|
3
|
+
Version: 0.0.15
|
|
4
|
+
Summary: Official Python SDK for PUDA, the runtime environment for Physical AI
|
|
5
|
+
Author: zhao
|
|
6
|
+
Author-email: zhao <20024592+agentzhao@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: nats-py>=2.12.0
|
|
8
|
+
Requires-Dist: pydantic>=2.12.5
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Puda Comms
|
|
13
|
+
|
|
14
|
+
A Python module for communication between machines and command services via NATS messaging. Provides client-side services for sending commands, machine-side clients for receiving commands, and data models for structured message exchange.
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
The `puda` module enables asynchronous, reliable communication between command services and machines using NATS (NATS JetStream for guaranteed delivery). It handles:
|
|
19
|
+
|
|
20
|
+
- **Command execution**: Send commands to machines and receive responses
|
|
21
|
+
- **Message routing**: Queue commands (sequential execution) and immediate commands (control operations)
|
|
22
|
+
- **State management**: Thread-safe execution state tracking for cancellation and locking
|
|
23
|
+
- **Connection management**: Automatic NATS connection handling with async context managers
|
|
24
|
+
|
|
25
|
+
## Components
|
|
26
|
+
|
|
27
|
+
The module consists of four main components:
|
|
28
|
+
|
|
29
|
+
### 1. Models (`models.py`)
|
|
30
|
+
|
|
31
|
+
Data models for structured message exchange. All models use Pydantic for validation and serialization.
|
|
32
|
+
|
|
33
|
+
#### Enums
|
|
34
|
+
|
|
35
|
+
##### `CommandResponseStatus`
|
|
36
|
+
Status of a command response:
|
|
37
|
+
- `SUCCESS`: Command executed successfully
|
|
38
|
+
- `ERROR`: Command execution failed
|
|
39
|
+
|
|
40
|
+
##### `CommandResponseCode`
|
|
41
|
+
Error codes for command responses:
|
|
42
|
+
- `COMMAND_CANCELLED`: Command was cancelled before completion
|
|
43
|
+
- `JSON_DECODE_ERROR`: Failed to decode JSON payload
|
|
44
|
+
- `EXECUTION_ERROR`: General execution error
|
|
45
|
+
- `EXECUTION_LOCKED`: Execution is locked (another command is running)
|
|
46
|
+
- `UNKNOWN_COMMAND`: Command name not recognized
|
|
47
|
+
- `PAUSE_ERROR`: Error occurred while pausing execution
|
|
48
|
+
- `RESUME_ERROR`: Error occurred while resuming execution
|
|
49
|
+
- `NO_EXECUTION`: No execution found
|
|
50
|
+
- `RUN_ID_MISMATCH`: Run ID doesn't match current execution
|
|
51
|
+
- `CANCEL_ERROR`: Error occurred while cancelling execution
|
|
52
|
+
- `MACHINE_PAUSED`: Machine is currently paused
|
|
53
|
+
|
|
54
|
+
##### `MessageType`
|
|
55
|
+
Type of NATS message:
|
|
56
|
+
- `COMMAND`: Command message sent to machine
|
|
57
|
+
- `RESPONSE`: Response message from machine
|
|
58
|
+
- `LOG`: Log message
|
|
59
|
+
- `ALERT`: Alert message
|
|
60
|
+
- `MEDIA`: Media message
|
|
61
|
+
|
|
62
|
+
##### `ImmediateCommand`
|
|
63
|
+
Command names for immediate/control commands:
|
|
64
|
+
- `PAUSE`: Pause the current execution
|
|
65
|
+
- `RESUME`: Resume a paused execution
|
|
66
|
+
- `CANCEL`: Cancel the current execution
|
|
67
|
+
|
|
68
|
+
#### Data Models
|
|
69
|
+
|
|
70
|
+
##### `CommandRequest`
|
|
71
|
+
Represents a command to be sent to a machine.
|
|
72
|
+
|
|
73
|
+
**Fields:**
|
|
74
|
+
- `name` (str): The command name to execute
|
|
75
|
+
- `machine_id` (str): Machine ID to send the command to (required)
|
|
76
|
+
- `params` (Dict[str, Any]): Command parameters (default: empty dict)
|
|
77
|
+
- `step_number` (int): Execution step number for tracking progress
|
|
78
|
+
- `version` (str): Command version (default: "1.0")
|
|
79
|
+
|
|
80
|
+
**Example:**
|
|
81
|
+
```python
|
|
82
|
+
command = CommandRequest(
|
|
83
|
+
name="attach_tip",
|
|
84
|
+
machine_id="first",
|
|
85
|
+
params={"deck_slot": "A3", "well_name": "G8"},
|
|
86
|
+
step_number=2,
|
|
87
|
+
version="1.0"
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
##### `CommandResponse`
|
|
92
|
+
Represents the result of a command execution.
|
|
93
|
+
|
|
94
|
+
**Fields:**
|
|
95
|
+
- `status` (CommandResponseStatus): Status of the command response (SUCCESS or ERROR)
|
|
96
|
+
- `completed_at` (str): ISO 8601 UTC timestamp (auto-generated)
|
|
97
|
+
- `code` (Optional[str]): Error code if status is ERROR
|
|
98
|
+
- `message` (Optional[str]): Human-readable error message
|
|
99
|
+
|
|
100
|
+
**Example:**
|
|
101
|
+
```python
|
|
102
|
+
response = CommandResponse(
|
|
103
|
+
status=CommandResponseStatus.SUCCESS,
|
|
104
|
+
completed_at="2026-01-20T02:00:46Z"
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Error Example:**
|
|
109
|
+
```python
|
|
110
|
+
error_response = CommandResponse(
|
|
111
|
+
status=CommandResponseStatus.ERROR,
|
|
112
|
+
code="EXECUTION_ERROR",
|
|
113
|
+
message="Failed to attach tip: deck_slot A3 not found",
|
|
114
|
+
completed_at="2026-01-20T02:00:46Z"
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
##### `MessageHeader`
|
|
119
|
+
Header metadata for NATS messages.
|
|
120
|
+
|
|
121
|
+
**Fields:**
|
|
122
|
+
- `message_type` (MessageType): Type of message (COMMAND, RESPONSE, LOG, etc.)
|
|
123
|
+
- `version` (str): Message version (default: "1.0")
|
|
124
|
+
- `timestamp` (str): ISO 8601 UTC timestamp (auto-generated)
|
|
125
|
+
- `user_id` (str): User ID who initiated the command
|
|
126
|
+
- `username` (str): Username who initiated the command
|
|
127
|
+
- `machine_id` (str): Identifier for the target machine
|
|
128
|
+
- `run_id` (Optional[str]): Unique identifier (UUID) for the run/workflow
|
|
129
|
+
|
|
130
|
+
**Example:**
|
|
131
|
+
```python
|
|
132
|
+
header = MessageHeader(
|
|
133
|
+
message_type=MessageType.RESPONSE,
|
|
134
|
+
version="1.0",
|
|
135
|
+
timestamp="2026-01-20T02:00:46Z",
|
|
136
|
+
user_id="user123",
|
|
137
|
+
username="John Doe",
|
|
138
|
+
machine_id="first",
|
|
139
|
+
run_id="092073e6-13d0-4756-8d99-eff1612a5a72"
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
##### `NATSMessage`
|
|
144
|
+
Complete NATS message structure combining header with optional command or response data.
|
|
145
|
+
|
|
146
|
+
**Fields:**
|
|
147
|
+
- `header` (MessageHeader): Message header (required)
|
|
148
|
+
- `command` (Optional[CommandRequest]): Command request (for command messages)
|
|
149
|
+
- `response` (Optional[CommandResponse]): Command response (for response messages)
|
|
150
|
+
|
|
151
|
+
**Structure:**
|
|
152
|
+
- For command messages: include `header` with `message_type=COMMAND` and `command` field
|
|
153
|
+
- For response messages: include `header` with `message_type=RESPONSE` and `response` field
|
|
154
|
+
|
|
155
|
+
**Complete Message Example:**
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"header": {
|
|
159
|
+
"message_type": "response",
|
|
160
|
+
"version": "1.0",
|
|
161
|
+
"timestamp": "2026-01-20T02:00:46Z",
|
|
162
|
+
"user_id": "user123",
|
|
163
|
+
"username": "John Doe",
|
|
164
|
+
"machine_id": "first",
|
|
165
|
+
"run_id": "092073e6-13d0-4756-8d99-eff1612a5a72"
|
|
166
|
+
},
|
|
167
|
+
"command": {
|
|
168
|
+
"name": "attach_tip",
|
|
169
|
+
"params": {
|
|
170
|
+
"deck_slot": "A3",
|
|
171
|
+
"well_name": "G8"
|
|
172
|
+
},
|
|
173
|
+
"step_number": 2,
|
|
174
|
+
"version": "1.0"
|
|
175
|
+
},
|
|
176
|
+
"response": {
|
|
177
|
+
"status": "success",
|
|
178
|
+
"completed_at": "2026-01-20T02:00:46Z",
|
|
179
|
+
"code": null,
|
|
180
|
+
"message": null
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 2. CommandService (`command_service.py`)
|
|
186
|
+
|
|
187
|
+
Client-side service for sending commands to machines via NATS. Handles:
|
|
188
|
+
- Connecting to NATS servers
|
|
189
|
+
- Sending commands to machines (queue or immediate)
|
|
190
|
+
- Waiting for and handling responses
|
|
191
|
+
- Managing command lifecycle (run_id, step_number, etc.)
|
|
192
|
+
- Automatic connection cleanup via async context manager
|
|
193
|
+
|
|
194
|
+
See [Sending Commands](#sending-commands) section for usage examples.
|
|
195
|
+
|
|
196
|
+
### 3. EdgeNatsClient (`machine_client.py`)
|
|
197
|
+
|
|
198
|
+
Basic default NATS client for generic machines. Handles commands, telemetry, and events following the `puda.{machine_id}.{category}.{sub_category}` pattern. Provides:
|
|
199
|
+
- Subscribing to command streams (queue and immediate) via JetStream with exactly-once delivery
|
|
200
|
+
- Processing incoming commands and sending command responses
|
|
201
|
+
- Publishing telemetry (core NATS, no JetStream)
|
|
202
|
+
- Publishing events (core NATS, fire-and-forget)
|
|
203
|
+
- Connection management and reconnection handling
|
|
204
|
+
|
|
205
|
+
**Note:** This is a generic client. Machine-specific methods should be implemented in the machine-edge client.
|
|
206
|
+
|
|
207
|
+
### 4. ExecutionState (`execution_state.py`)
|
|
208
|
+
|
|
209
|
+
Thread-safe state management for command execution. Provides:
|
|
210
|
+
- Execution lock to prevent concurrent commands
|
|
211
|
+
- Current task tracking for cancellation
|
|
212
|
+
- Run ID matching for cancel operations
|
|
213
|
+
- Thread-safe access to execution state
|
|
214
|
+
|
|
215
|
+
## Sending Commands
|
|
216
|
+
|
|
217
|
+
The `CommandService` provides a high-level interface for sending commands to machines via NATS. See [`tests/commands.py`](tests/commands.py) and [`tests/batch_commands.py`](tests/batch_commands.py) for complete examples.
|
|
218
|
+
|
|
219
|
+
### Recommended Usage: Async Context Manager
|
|
220
|
+
|
|
221
|
+
The recommended way to use `CommandService` is with an async context manager, which automatically handles connection and disconnection. See [`tests/commands.py`](tests/commands.py) for complete examples.
|
|
222
|
+
|
|
223
|
+
### Command Types
|
|
224
|
+
|
|
225
|
+
#### Queue Commands
|
|
226
|
+
|
|
227
|
+
Queue commands are regular commands that are executed in sequence. Use `send_queue_command()` for machine-specific operations.
|
|
228
|
+
|
|
229
|
+
**Note:** Available commands depend on the machine you are controlling. Different machines support different command sets (e.g., `first` machine supports commands like `load_deck`, `attach_tip`, `aspirate_from`, `dispense_to`, `drop_tip`, etc.).
|
|
230
|
+
|
|
231
|
+
Both `send_queue_command()`, `send_queue_commands()`, and `send_immediate_command()` accept an optional `timeout` parameter (default: 120 seconds):
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Single command (machine_id must be in CommandRequest)
|
|
235
|
+
reply = await service.send_queue_command(
|
|
236
|
+
request=request, # request.machine_id must be set
|
|
237
|
+
run_id=run_id,
|
|
238
|
+
user_id="user123",
|
|
239
|
+
username="John Doe",
|
|
240
|
+
timeout=60 # Wait up to 60 seconds
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Multiple commands (timeout applies to each command)
|
|
244
|
+
# Each command in the list must have machine_id set
|
|
245
|
+
reply = await service.send_queue_commands(
|
|
246
|
+
requests=commands, # Each CommandRequest must have machine_id
|
|
247
|
+
run_id=run_id,
|
|
248
|
+
user_id="user123",
|
|
249
|
+
username="John Doe",
|
|
250
|
+
timeout=60 # Wait up to 60 seconds per command
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Examples:**
|
|
255
|
+
|
|
256
|
+
See [`tests/commands.py`](tests/commands.py) for complete examples.
|
|
257
|
+
|
|
258
|
+
#### Immediate Commands
|
|
259
|
+
|
|
260
|
+
Immediate commands are control commands that interrupt or modify execution. Use `send_immediate_command()` for:
|
|
261
|
+
- `pause`: Pause the current execution
|
|
262
|
+
- `resume`: Resume a paused execution
|
|
263
|
+
- `cancel`: Cancel the current execution
|
|
264
|
+
|
|
265
|
+
**Examples:**
|
|
266
|
+
|
|
267
|
+
See [`tests/commands.py`](tests/commands.py) for complete examples.
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
### Sending Command Sequences
|
|
272
|
+
|
|
273
|
+
You can send multiple commands in sequence using `send_queue_commands()`, which sends commands one by one and waits for each response before sending the next. If any command fails or times out, it stops immediately and returns the error response.
|
|
274
|
+
|
|
275
|
+
**Loading Commands from JSON (Recommended for LLM-generated commands):**
|
|
276
|
+
|
|
277
|
+
When generating commands from an LLM or loading from external sources, you can store commands in a JSON file and load them. See [`tests/batch_commands.py`](tests/batch_commands.py) for a complete example.
|
|
278
|
+
|
|
279
|
+
### Error Handling
|
|
280
|
+
|
|
281
|
+
Always check the response status and handle errors appropriately:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
reply: NATSMessage = await service.send_queue_command(
|
|
285
|
+
request=request, # request.machine_id must be set
|
|
286
|
+
run_id=run_id,
|
|
287
|
+
user_id="user123",
|
|
288
|
+
username="John Doe"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if reply is None:
|
|
292
|
+
# Command timed out or failed to send
|
|
293
|
+
logger.error("Command failed or timed out")
|
|
294
|
+
elif reply.response is not None and reply.response.status == CommandResponseStatus.SUCCESS:
|
|
295
|
+
# Command succeeded
|
|
296
|
+
logger.info("Command completed successfully")
|
|
297
|
+
else:
|
|
298
|
+
# Command failed with error
|
|
299
|
+
logger.error("Command failed with code: %s, message: %s",
|
|
300
|
+
reply.response.code if reply.response else None,
|
|
301
|
+
reply.response.message if reply.response else None)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Configuration
|
|
305
|
+
|
|
306
|
+
#### NATS Server Configuration
|
|
307
|
+
|
|
308
|
+
The `CommandService` requires NATS server URLs to be specified explicitly. There are no default values. You must provide servers in one of two ways:
|
|
309
|
+
|
|
310
|
+
**Option 1: Via environment variable (comma-separated string)**
|
|
311
|
+
|
|
312
|
+
Set the `NATS_SERVERS` environment variable with comma-separated server URLs:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
export NATS_SERVERS="nats://192.168.50.201:4222,nats://192.168.50.201:4223,nats://192.168.50.201:4224"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Then parse it when creating a `CommandService`:
|
|
319
|
+
```python
|
|
320
|
+
import os
|
|
321
|
+
nats_servers = [s.strip() for s in os.getenv("NATS_SERVERS", "").split(",") if s.strip()]
|
|
322
|
+
service = CommandService(servers=nats_servers)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Option 2: Directly as a list**
|
|
326
|
+
|
|
327
|
+
Specify servers directly when creating a `CommandService`:
|
|
328
|
+
```python
|
|
329
|
+
service = CommandService(servers=["nats://192.168.50.201:4222", "nats://192.168.50.201:4223", "nats://192.168.50.201:4224"])
|
|
330
|
+
```
|
|
331
|
+
## Validation
|
|
332
|
+
|
|
333
|
+
All models use Pydantic for validation, ensuring:
|
|
334
|
+
- Type checking for all fields
|
|
335
|
+
- Required fields are present
|
|
336
|
+
- Default values are applied correctly
|
|
337
|
+
- JSON serialization/deserialization works correctly
|
puda-0.0.15/README.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# Puda Comms
|
|
2
|
+
|
|
3
|
+
A Python module for communication between machines and command services via NATS messaging. Provides client-side services for sending commands, machine-side clients for receiving commands, and data models for structured message exchange.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `puda` module enables asynchronous, reliable communication between command services and machines using NATS (NATS JetStream for guaranteed delivery). It handles:
|
|
8
|
+
|
|
9
|
+
- **Command execution**: Send commands to machines and receive responses
|
|
10
|
+
- **Message routing**: Queue commands (sequential execution) and immediate commands (control operations)
|
|
11
|
+
- **State management**: Thread-safe execution state tracking for cancellation and locking
|
|
12
|
+
- **Connection management**: Automatic NATS connection handling with async context managers
|
|
13
|
+
|
|
14
|
+
## Components
|
|
15
|
+
|
|
16
|
+
The module consists of four main components:
|
|
17
|
+
|
|
18
|
+
### 1. Models (`models.py`)
|
|
19
|
+
|
|
20
|
+
Data models for structured message exchange. All models use Pydantic for validation and serialization.
|
|
21
|
+
|
|
22
|
+
#### Enums
|
|
23
|
+
|
|
24
|
+
##### `CommandResponseStatus`
|
|
25
|
+
Status of a command response:
|
|
26
|
+
- `SUCCESS`: Command executed successfully
|
|
27
|
+
- `ERROR`: Command execution failed
|
|
28
|
+
|
|
29
|
+
##### `CommandResponseCode`
|
|
30
|
+
Error codes for command responses:
|
|
31
|
+
- `COMMAND_CANCELLED`: Command was cancelled before completion
|
|
32
|
+
- `JSON_DECODE_ERROR`: Failed to decode JSON payload
|
|
33
|
+
- `EXECUTION_ERROR`: General execution error
|
|
34
|
+
- `EXECUTION_LOCKED`: Execution is locked (another command is running)
|
|
35
|
+
- `UNKNOWN_COMMAND`: Command name not recognized
|
|
36
|
+
- `PAUSE_ERROR`: Error occurred while pausing execution
|
|
37
|
+
- `RESUME_ERROR`: Error occurred while resuming execution
|
|
38
|
+
- `NO_EXECUTION`: No execution found
|
|
39
|
+
- `RUN_ID_MISMATCH`: Run ID doesn't match current execution
|
|
40
|
+
- `CANCEL_ERROR`: Error occurred while cancelling execution
|
|
41
|
+
- `MACHINE_PAUSED`: Machine is currently paused
|
|
42
|
+
|
|
43
|
+
##### `MessageType`
|
|
44
|
+
Type of NATS message:
|
|
45
|
+
- `COMMAND`: Command message sent to machine
|
|
46
|
+
- `RESPONSE`: Response message from machine
|
|
47
|
+
- `LOG`: Log message
|
|
48
|
+
- `ALERT`: Alert message
|
|
49
|
+
- `MEDIA`: Media message
|
|
50
|
+
|
|
51
|
+
##### `ImmediateCommand`
|
|
52
|
+
Command names for immediate/control commands:
|
|
53
|
+
- `PAUSE`: Pause the current execution
|
|
54
|
+
- `RESUME`: Resume a paused execution
|
|
55
|
+
- `CANCEL`: Cancel the current execution
|
|
56
|
+
|
|
57
|
+
#### Data Models
|
|
58
|
+
|
|
59
|
+
##### `CommandRequest`
|
|
60
|
+
Represents a command to be sent to a machine.
|
|
61
|
+
|
|
62
|
+
**Fields:**
|
|
63
|
+
- `name` (str): The command name to execute
|
|
64
|
+
- `machine_id` (str): Machine ID to send the command to (required)
|
|
65
|
+
- `params` (Dict[str, Any]): Command parameters (default: empty dict)
|
|
66
|
+
- `step_number` (int): Execution step number for tracking progress
|
|
67
|
+
- `version` (str): Command version (default: "1.0")
|
|
68
|
+
|
|
69
|
+
**Example:**
|
|
70
|
+
```python
|
|
71
|
+
command = CommandRequest(
|
|
72
|
+
name="attach_tip",
|
|
73
|
+
machine_id="first",
|
|
74
|
+
params={"deck_slot": "A3", "well_name": "G8"},
|
|
75
|
+
step_number=2,
|
|
76
|
+
version="1.0"
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
##### `CommandResponse`
|
|
81
|
+
Represents the result of a command execution.
|
|
82
|
+
|
|
83
|
+
**Fields:**
|
|
84
|
+
- `status` (CommandResponseStatus): Status of the command response (SUCCESS or ERROR)
|
|
85
|
+
- `completed_at` (str): ISO 8601 UTC timestamp (auto-generated)
|
|
86
|
+
- `code` (Optional[str]): Error code if status is ERROR
|
|
87
|
+
- `message` (Optional[str]): Human-readable error message
|
|
88
|
+
|
|
89
|
+
**Example:**
|
|
90
|
+
```python
|
|
91
|
+
response = CommandResponse(
|
|
92
|
+
status=CommandResponseStatus.SUCCESS,
|
|
93
|
+
completed_at="2026-01-20T02:00:46Z"
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Error Example:**
|
|
98
|
+
```python
|
|
99
|
+
error_response = CommandResponse(
|
|
100
|
+
status=CommandResponseStatus.ERROR,
|
|
101
|
+
code="EXECUTION_ERROR",
|
|
102
|
+
message="Failed to attach tip: deck_slot A3 not found",
|
|
103
|
+
completed_at="2026-01-20T02:00:46Z"
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
##### `MessageHeader`
|
|
108
|
+
Header metadata for NATS messages.
|
|
109
|
+
|
|
110
|
+
**Fields:**
|
|
111
|
+
- `message_type` (MessageType): Type of message (COMMAND, RESPONSE, LOG, etc.)
|
|
112
|
+
- `version` (str): Message version (default: "1.0")
|
|
113
|
+
- `timestamp` (str): ISO 8601 UTC timestamp (auto-generated)
|
|
114
|
+
- `user_id` (str): User ID who initiated the command
|
|
115
|
+
- `username` (str): Username who initiated the command
|
|
116
|
+
- `machine_id` (str): Identifier for the target machine
|
|
117
|
+
- `run_id` (Optional[str]): Unique identifier (UUID) for the run/workflow
|
|
118
|
+
|
|
119
|
+
**Example:**
|
|
120
|
+
```python
|
|
121
|
+
header = MessageHeader(
|
|
122
|
+
message_type=MessageType.RESPONSE,
|
|
123
|
+
version="1.0",
|
|
124
|
+
timestamp="2026-01-20T02:00:46Z",
|
|
125
|
+
user_id="user123",
|
|
126
|
+
username="John Doe",
|
|
127
|
+
machine_id="first",
|
|
128
|
+
run_id="092073e6-13d0-4756-8d99-eff1612a5a72"
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
##### `NATSMessage`
|
|
133
|
+
Complete NATS message structure combining header with optional command or response data.
|
|
134
|
+
|
|
135
|
+
**Fields:**
|
|
136
|
+
- `header` (MessageHeader): Message header (required)
|
|
137
|
+
- `command` (Optional[CommandRequest]): Command request (for command messages)
|
|
138
|
+
- `response` (Optional[CommandResponse]): Command response (for response messages)
|
|
139
|
+
|
|
140
|
+
**Structure:**
|
|
141
|
+
- For command messages: include `header` with `message_type=COMMAND` and `command` field
|
|
142
|
+
- For response messages: include `header` with `message_type=RESPONSE` and `response` field
|
|
143
|
+
|
|
144
|
+
**Complete Message Example:**
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"header": {
|
|
148
|
+
"message_type": "response",
|
|
149
|
+
"version": "1.0",
|
|
150
|
+
"timestamp": "2026-01-20T02:00:46Z",
|
|
151
|
+
"user_id": "user123",
|
|
152
|
+
"username": "John Doe",
|
|
153
|
+
"machine_id": "first",
|
|
154
|
+
"run_id": "092073e6-13d0-4756-8d99-eff1612a5a72"
|
|
155
|
+
},
|
|
156
|
+
"command": {
|
|
157
|
+
"name": "attach_tip",
|
|
158
|
+
"params": {
|
|
159
|
+
"deck_slot": "A3",
|
|
160
|
+
"well_name": "G8"
|
|
161
|
+
},
|
|
162
|
+
"step_number": 2,
|
|
163
|
+
"version": "1.0"
|
|
164
|
+
},
|
|
165
|
+
"response": {
|
|
166
|
+
"status": "success",
|
|
167
|
+
"completed_at": "2026-01-20T02:00:46Z",
|
|
168
|
+
"code": null,
|
|
169
|
+
"message": null
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 2. CommandService (`command_service.py`)
|
|
175
|
+
|
|
176
|
+
Client-side service for sending commands to machines via NATS. Handles:
|
|
177
|
+
- Connecting to NATS servers
|
|
178
|
+
- Sending commands to machines (queue or immediate)
|
|
179
|
+
- Waiting for and handling responses
|
|
180
|
+
- Managing command lifecycle (run_id, step_number, etc.)
|
|
181
|
+
- Automatic connection cleanup via async context manager
|
|
182
|
+
|
|
183
|
+
See [Sending Commands](#sending-commands) section for usage examples.
|
|
184
|
+
|
|
185
|
+
### 3. EdgeNatsClient (`machine_client.py`)
|
|
186
|
+
|
|
187
|
+
Basic default NATS client for generic machines. Handles commands, telemetry, and events following the `puda.{machine_id}.{category}.{sub_category}` pattern. Provides:
|
|
188
|
+
- Subscribing to command streams (queue and immediate) via JetStream with exactly-once delivery
|
|
189
|
+
- Processing incoming commands and sending command responses
|
|
190
|
+
- Publishing telemetry (core NATS, no JetStream)
|
|
191
|
+
- Publishing events (core NATS, fire-and-forget)
|
|
192
|
+
- Connection management and reconnection handling
|
|
193
|
+
|
|
194
|
+
**Note:** This is a generic client. Machine-specific methods should be implemented in the machine-edge client.
|
|
195
|
+
|
|
196
|
+
### 4. ExecutionState (`execution_state.py`)
|
|
197
|
+
|
|
198
|
+
Thread-safe state management for command execution. Provides:
|
|
199
|
+
- Execution lock to prevent concurrent commands
|
|
200
|
+
- Current task tracking for cancellation
|
|
201
|
+
- Run ID matching for cancel operations
|
|
202
|
+
- Thread-safe access to execution state
|
|
203
|
+
|
|
204
|
+
## Sending Commands
|
|
205
|
+
|
|
206
|
+
The `CommandService` provides a high-level interface for sending commands to machines via NATS. See [`tests/commands.py`](tests/commands.py) and [`tests/batch_commands.py`](tests/batch_commands.py) for complete examples.
|
|
207
|
+
|
|
208
|
+
### Recommended Usage: Async Context Manager
|
|
209
|
+
|
|
210
|
+
The recommended way to use `CommandService` is with an async context manager, which automatically handles connection and disconnection. See [`tests/commands.py`](tests/commands.py) for complete examples.
|
|
211
|
+
|
|
212
|
+
### Command Types
|
|
213
|
+
|
|
214
|
+
#### Queue Commands
|
|
215
|
+
|
|
216
|
+
Queue commands are regular commands that are executed in sequence. Use `send_queue_command()` for machine-specific operations.
|
|
217
|
+
|
|
218
|
+
**Note:** Available commands depend on the machine you are controlling. Different machines support different command sets (e.g., `first` machine supports commands like `load_deck`, `attach_tip`, `aspirate_from`, `dispense_to`, `drop_tip`, etc.).
|
|
219
|
+
|
|
220
|
+
Both `send_queue_command()`, `send_queue_commands()`, and `send_immediate_command()` accept an optional `timeout` parameter (default: 120 seconds):
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# Single command (machine_id must be in CommandRequest)
|
|
224
|
+
reply = await service.send_queue_command(
|
|
225
|
+
request=request, # request.machine_id must be set
|
|
226
|
+
run_id=run_id,
|
|
227
|
+
user_id="user123",
|
|
228
|
+
username="John Doe",
|
|
229
|
+
timeout=60 # Wait up to 60 seconds
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Multiple commands (timeout applies to each command)
|
|
233
|
+
# Each command in the list must have machine_id set
|
|
234
|
+
reply = await service.send_queue_commands(
|
|
235
|
+
requests=commands, # Each CommandRequest must have machine_id
|
|
236
|
+
run_id=run_id,
|
|
237
|
+
user_id="user123",
|
|
238
|
+
username="John Doe",
|
|
239
|
+
timeout=60 # Wait up to 60 seconds per command
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Examples:**
|
|
244
|
+
|
|
245
|
+
See [`tests/commands.py`](tests/commands.py) for complete examples.
|
|
246
|
+
|
|
247
|
+
#### Immediate Commands
|
|
248
|
+
|
|
249
|
+
Immediate commands are control commands that interrupt or modify execution. Use `send_immediate_command()` for:
|
|
250
|
+
- `pause`: Pause the current execution
|
|
251
|
+
- `resume`: Resume a paused execution
|
|
252
|
+
- `cancel`: Cancel the current execution
|
|
253
|
+
|
|
254
|
+
**Examples:**
|
|
255
|
+
|
|
256
|
+
See [`tests/commands.py`](tests/commands.py) for complete examples.
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
### Sending Command Sequences
|
|
261
|
+
|
|
262
|
+
You can send multiple commands in sequence using `send_queue_commands()`, which sends commands one by one and waits for each response before sending the next. If any command fails or times out, it stops immediately and returns the error response.
|
|
263
|
+
|
|
264
|
+
**Loading Commands from JSON (Recommended for LLM-generated commands):**
|
|
265
|
+
|
|
266
|
+
When generating commands from an LLM or loading from external sources, you can store commands in a JSON file and load them. See [`tests/batch_commands.py`](tests/batch_commands.py) for a complete example.
|
|
267
|
+
|
|
268
|
+
### Error Handling
|
|
269
|
+
|
|
270
|
+
Always check the response status and handle errors appropriately:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
reply: NATSMessage = await service.send_queue_command(
|
|
274
|
+
request=request, # request.machine_id must be set
|
|
275
|
+
run_id=run_id,
|
|
276
|
+
user_id="user123",
|
|
277
|
+
username="John Doe"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if reply is None:
|
|
281
|
+
# Command timed out or failed to send
|
|
282
|
+
logger.error("Command failed or timed out")
|
|
283
|
+
elif reply.response is not None and reply.response.status == CommandResponseStatus.SUCCESS:
|
|
284
|
+
# Command succeeded
|
|
285
|
+
logger.info("Command completed successfully")
|
|
286
|
+
else:
|
|
287
|
+
# Command failed with error
|
|
288
|
+
logger.error("Command failed with code: %s, message: %s",
|
|
289
|
+
reply.response.code if reply.response else None,
|
|
290
|
+
reply.response.message if reply.response else None)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Configuration
|
|
294
|
+
|
|
295
|
+
#### NATS Server Configuration
|
|
296
|
+
|
|
297
|
+
The `CommandService` requires NATS server URLs to be specified explicitly. There are no default values. You must provide servers in one of two ways:
|
|
298
|
+
|
|
299
|
+
**Option 1: Via environment variable (comma-separated string)**
|
|
300
|
+
|
|
301
|
+
Set the `NATS_SERVERS` environment variable with comma-separated server URLs:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
export NATS_SERVERS="nats://192.168.50.201:4222,nats://192.168.50.201:4223,nats://192.168.50.201:4224"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Then parse it when creating a `CommandService`:
|
|
308
|
+
```python
|
|
309
|
+
import os
|
|
310
|
+
nats_servers = [s.strip() for s in os.getenv("NATS_SERVERS", "").split(",") if s.strip()]
|
|
311
|
+
service = CommandService(servers=nats_servers)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Option 2: Directly as a list**
|
|
315
|
+
|
|
316
|
+
Specify servers directly when creating a `CommandService`:
|
|
317
|
+
```python
|
|
318
|
+
service = CommandService(servers=["nats://192.168.50.201:4222", "nats://192.168.50.201:4223", "nats://192.168.50.201:4224"])
|
|
319
|
+
```
|
|
320
|
+
## Validation
|
|
321
|
+
|
|
322
|
+
All models use Pydantic for validation, ensuring:
|
|
323
|
+
- Type checking for all fields
|
|
324
|
+
- Required fields are present
|
|
325
|
+
- Default values are applied correctly
|
|
326
|
+
- JSON serialization/deserialization works correctly
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "puda"
|
|
3
|
+
version = "0.0.15"
|
|
4
|
+
description = "Official Python SDK for PUDA, the runtime environment for Physical AI"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "zhao", email = "20024592+agentzhao@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"nats-py>=2.12.0",
|
|
12
|
+
"pydantic>=2.12.5",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.ruff]
|
|
16
|
+
line-length = 100
|
|
17
|
+
|
|
18
|
+
[tool.ruff.lint.mccabe]
|
|
19
|
+
max-complexity = 10
|
|
20
|
+
|
|
21
|
+
[tool.uv.build-backend]
|
|
22
|
+
module-name = "puda"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
26
|
+
build-backend = "uv_build"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"ruff>=0.14.13",
|
|
31
|
+
]
|