robotrace-dev 0.1.0a2__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.
- robotrace_dev-0.1.0a2/.gitignore +50 -0
- robotrace_dev-0.1.0a2/CHANGELOG.md +120 -0
- robotrace_dev-0.1.0a2/LICENSE +21 -0
- robotrace_dev-0.1.0a2/PKG-INFO +265 -0
- robotrace_dev-0.1.0a2/README.md +226 -0
- robotrace_dev-0.1.0a2/pyproject.toml +149 -0
- robotrace_dev-0.1.0a2/src/robotrace/__init__.py +219 -0
- robotrace_dev-0.1.0a2/src/robotrace/_credentials.py +262 -0
- robotrace_dev-0.1.0a2/src/robotrace/_http.py +203 -0
- robotrace_dev-0.1.0a2/src/robotrace/_otel.py +153 -0
- robotrace_dev-0.1.0a2/src/robotrace/_version.py +9 -0
- robotrace_dev-0.1.0a2/src/robotrace/adapters/__init__.py +19 -0
- robotrace_dev-0.1.0a2/src/robotrace/adapters/ros2/__init__.py +62 -0
- robotrace_dev-0.1.0a2/src/robotrace/adapters/ros2/_classify.py +104 -0
- robotrace_dev-0.1.0a2/src/robotrace/adapters/ros2/_encode.py +717 -0
- robotrace_dev-0.1.0a2/src/robotrace/adapters/ros2/_scan.py +201 -0
- robotrace_dev-0.1.0a2/src/robotrace/adapters/ros2/_upload.py +211 -0
- robotrace_dev-0.1.0a2/src/robotrace/cli.py +509 -0
- robotrace_dev-0.1.0a2/src/robotrace/client.py +390 -0
- robotrace_dev-0.1.0a2/src/robotrace/episode.py +250 -0
- robotrace_dev-0.1.0a2/src/robotrace/errors.py +100 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnp
|
|
4
|
+
.pnp.*
|
|
5
|
+
.yarn/*
|
|
6
|
+
!.yarn/patches
|
|
7
|
+
!.yarn/plugins
|
|
8
|
+
!.yarn/releases
|
|
9
|
+
!.yarn/versions
|
|
10
|
+
|
|
11
|
+
# python
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
*$py.class
|
|
17
|
+
*.egg-info/
|
|
18
|
+
dist/
|
|
19
|
+
build/
|
|
20
|
+
.tox/
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
|
|
25
|
+
# next
|
|
26
|
+
.next/
|
|
27
|
+
out/
|
|
28
|
+
*.tsbuildinfo
|
|
29
|
+
next-env.d.ts
|
|
30
|
+
|
|
31
|
+
# env
|
|
32
|
+
.env
|
|
33
|
+
.env.local
|
|
34
|
+
.env.*.local
|
|
35
|
+
|
|
36
|
+
# misc
|
|
37
|
+
.DS_Store
|
|
38
|
+
*.pem
|
|
39
|
+
.vercel
|
|
40
|
+
.npm-cache/
|
|
41
|
+
*.log
|
|
42
|
+
npm-debug.log*
|
|
43
|
+
yarn-debug.log*
|
|
44
|
+
yarn-error.log*
|
|
45
|
+
.pnpm-debug.log*
|
|
46
|
+
|
|
47
|
+
# editor
|
|
48
|
+
.vscode/
|
|
49
|
+
.idea/
|
|
50
|
+
.cursor/.ai/
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `robotrace-dev` Python SDK
|
|
4
|
+
(import name: `robotrace`).
|
|
5
|
+
|
|
6
|
+
The SDK follows [Semantic Versioning](https://semver.org/). Pre-1.0
|
|
7
|
+
releases (`0.x`) may make breaking changes between minor versions.
|
|
8
|
+
Once we cut `1.0.0`, the [`log_episode`](./README.md#log_episode-the-sacred-call)
|
|
9
|
+
signature is **locked** per AGENTS.md — breakages require a major
|
|
10
|
+
bump and at least one minor of `DeprecationWarning` first.
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [0.1.0a2] — 2026-05-09
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **PyPI distribution name is `robotrace-dev`**, matching our
|
|
19
|
+
`robotrace.dev` domain. The un-hyphenated `robotrace` namespace on
|
|
20
|
+
PyPI was claimed in March 2026 by an unrelated robotics
|
|
21
|
+
observability project, and PyPI's typo-squat protector blocks any
|
|
22
|
+
single-edit-distance variant — including `robo-trace`, which we
|
|
23
|
+
tried first and PyPI rejected with `400 Bad Request`.
|
|
24
|
+
`robotrace-dev` clears the similarity threshold (Damerau-Levenshtein
|
|
25
|
+
≥ 3 from both `robotrace` and `robotrace-sdk`) and reads as the
|
|
26
|
+
obvious match for our domain. The *import* name is still
|
|
27
|
+
`robotrace` (no hyphen, no `-dev`) — same convention as
|
|
28
|
+
`pip install python-dateutil` → `import dateutil`. **No code
|
|
29
|
+
changes required** in your application; the install command is
|
|
30
|
+
`pip install robotrace-dev`.
|
|
31
|
+
- Earlier `0.1.0a*` releases were never published to PyPI, so
|
|
32
|
+
there's no in-the-wild upgrade path to worry about — `0.1.0a2` is
|
|
33
|
+
the first published release.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`robotrace-dev[otel]`** extra — opt-in OpenTelemetry trace
|
|
38
|
+
correlation. Pulls only `opentelemetry-api>=1.20` (~30 KB), not
|
|
39
|
+
the heavy `opentelemetry-sdk`. When `start_episode` is called
|
|
40
|
+
inside an active OTel span, the SDK reads the ambient context via
|
|
41
|
+
`opentelemetry.trace.get_current_span()` and attaches:
|
|
42
|
+
- `trace_id` (32-char lowercase hex)
|
|
43
|
+
- `span_id` (16-char lowercase hex)
|
|
44
|
+
- `traceparent` (W3C `00-<trace>-<span>-01` format)
|
|
45
|
+
to the create-episode payload. The server stores them under
|
|
46
|
+
`episodes.metadata.otel`; the portal renders a Tracing card on
|
|
47
|
+
the episode detail page with copy buttons and an optional
|
|
48
|
+
one-click "Open trace" deep-link via the
|
|
49
|
+
`NEXT_PUBLIC_TRACE_URL_TEMPLATE` env var (Datadog, Honeycomb,
|
|
50
|
+
Grafana Tempo, Jaeger).
|
|
51
|
+
- New `robotrace._otel` module with `capture_trace_context()`. Soft
|
|
52
|
+
imports — never raises if `opentelemetry` is missing or the active
|
|
53
|
+
span is invalid / unsampled.
|
|
54
|
+
- 7 new unit tests (`tests/test_otel.py`) covering: not-installed,
|
|
55
|
+
no-active-span, active-span happy path, unsampled flag, OTel
|
|
56
|
+
module misbehavior, public API exposure, and the `log_episode`
|
|
57
|
+
round-trip.
|
|
58
|
+
- The "sacred" `log_episode` / `start_episode` signature is
|
|
59
|
+
**unchanged** — OTel context is read implicitly. No new kwargs to
|
|
60
|
+
learn, no opt-in flag, no deprecation warnings on existing callers.
|
|
61
|
+
|
|
62
|
+
## [0.1.0a1] — 2026-05-08
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **`robotrace.adapters.ros2`** — read rosbag2 directories (sqlite3 +
|
|
67
|
+
mcap backends) and turn them into RoboTrace episodes without
|
|
68
|
+
needing an `rclpy` install. Three public verbs:
|
|
69
|
+
- `ros2.scan_bag(path) → BagSummary` — read-only introspection
|
|
70
|
+
with topic catalog, auto-classifier decisions, and bag duration.
|
|
71
|
+
- `ros2.encode_bag(path, output_dir) → EncodedBag` — writes
|
|
72
|
+
`video.mp4`, `sensors.npz`, `actions.npz` and returns the file
|
|
73
|
+
paths plus inferred `duration_s` / `fps`. No network.
|
|
74
|
+
- `ros2.upload_bag(path, **episode_kwargs) → Episode` — one-shot
|
|
75
|
+
scan + encode-to-tempdir + `start_episode` + `upload_*` +
|
|
76
|
+
`finalize`. The headline call.
|
|
77
|
+
- Auto-classification routes `sensor_msgs/Image` /
|
|
78
|
+
`CompressedImage` to `video`, `geometry_msgs/Twist*`,
|
|
79
|
+
`Wrench*`, `trajectory_msgs/JointTrajectory*`,
|
|
80
|
+
`control_msgs/JointJog`, and topics ending in `/cmd_*` /
|
|
81
|
+
`/command` to `actions`. Everything else lands in `sensors`.
|
|
82
|
+
Override per-slot via `video_topics=` / `sensor_topics=` /
|
|
83
|
+
`action_topics=` (empty list = exclude the slot).
|
|
84
|
+
- Multi-camera bags get tiled horizontally into a single
|
|
85
|
+
`video.mp4`; pass `canonical_video_topic=...` to pick one camera
|
|
86
|
+
instead. Frame rate inferred from the camera with the most
|
|
87
|
+
frames; falls back to 10 fps when timestamps are unusable.
|
|
88
|
+
- Built-in flatteners for `sensor_msgs/JointState`, `Imu`,
|
|
89
|
+
`geometry_msgs/Twist[Stamped]`, `Wrench[Stamped]`, `PoseStamped`,
|
|
90
|
+
`nav_msgs/Odometry`. Unknown message types fall through to a
|
|
91
|
+
generic dataclass walker that pulls every numeric leaf.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- Bumped `[project.optional-dependencies].ros2` from `[]` to
|
|
96
|
+
`["rosbags>=0.11,<0.12", "numpy>=1.26"]`. Pure Python — no real
|
|
97
|
+
ROS 2 install required to ingest bags.
|
|
98
|
+
- Image-topic encoding lives behind the existing `[video]` extra
|
|
99
|
+
(opencv-python). Install combo for camera bags is
|
|
100
|
+
`pip install 'robotrace-dev[ros2,video]'`; sensor-only bags can stick
|
|
101
|
+
with `[ros2]` and skip opencv entirely.
|
|
102
|
+
|
|
103
|
+
## [0.1.0a0] — 2026-05-02
|
|
104
|
+
|
|
105
|
+
First public alpha. Contract under iteration.
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
|
|
109
|
+
- `robotrace.init(...)`, `robotrace.start_episode(...)`,
|
|
110
|
+
`robotrace.log_episode(...)` top-level convenience.
|
|
111
|
+
- `robotrace.Client(...)` for explicit, multi-deployment use.
|
|
112
|
+
- `robotrace.Episode` handle with `upload_video` / `upload_sensors` /
|
|
113
|
+
`upload_actions` / `finalize` and context-manager auto-finalize
|
|
114
|
+
(clean exit → `ready`, exception → `failed`).
|
|
115
|
+
- Streaming PUT to Cloudflare R2 via signed URLs (no body buffering).
|
|
116
|
+
- Typed exception hierarchy (`RobotraceError` → `AuthError`,
|
|
117
|
+
`NotFoundError`, `ConflictError`, `ValidationError`, `ServerError`,
|
|
118
|
+
`TransportError`, `ConfigurationError`).
|
|
119
|
+
- `pytest`-based smoke tests pinning the wire format to
|
|
120
|
+
`/api/ingest/episode` and `/api/ingest/episode/{id}/finalize`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RoboTrace
|
|
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,265 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotrace-dev
|
|
3
|
+
Version: 0.1.0a2
|
|
4
|
+
Summary: RoboTrace — observability and evals for AI robots.
|
|
5
|
+
Project-URL: Homepage, https://robotrace.dev
|
|
6
|
+
Project-URL: Documentation, https://robotrace.dev/docs
|
|
7
|
+
Project-URL: Issues, https://github.com/robotrace/robotrace/issues
|
|
8
|
+
Author-email: RoboTrace <hello@robotrace.dev>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: evals,lerobot,observability,robotics,ros2,vla
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Provides-Extra: lerobot
|
|
28
|
+
Requires-Dist: lerobot; extra == 'lerobot'
|
|
29
|
+
Provides-Extra: numpy
|
|
30
|
+
Requires-Dist: numpy>=1.26; extra == 'numpy'
|
|
31
|
+
Provides-Extra: otel
|
|
32
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
|
|
33
|
+
Provides-Extra: ros2
|
|
34
|
+
Requires-Dist: numpy>=1.26; extra == 'ros2'
|
|
35
|
+
Requires-Dist: rosbags<0.12,>=0.11; extra == 'ros2'
|
|
36
|
+
Provides-Extra: video
|
|
37
|
+
Requires-Dist: opencv-python>=4.10; extra == 'video'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# robotrace-dev (Python SDK)
|
|
41
|
+
|
|
42
|
+
> The official Python SDK for [RoboTrace](https://robotrace.dev) —
|
|
43
|
+
> observability and evals for AI-powered robots.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install robotrace-dev==0.1.0a2
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Distribution name vs. import name.** PyPI distributes us as
|
|
50
|
+
> `robotrace-dev` (matching our `robotrace.dev` domain). The
|
|
51
|
+
> un-hyphenated `robotrace` PyPI namespace is held by an unrelated
|
|
52
|
+
> robotics project, and PyPI's typo-squat protector blocks any
|
|
53
|
+
> single-edit-distance variant (so `robo-trace` was rejected too).
|
|
54
|
+
> The *import* name stays `import robotrace` — same pattern as
|
|
55
|
+
> `pip install python-dateutil` → `import dateutil`.
|
|
56
|
+
|
|
57
|
+
> **Status:** alpha (`0.1.0a2`). The public API in this README is the
|
|
58
|
+
> shape we're iterating against; once we cut `1.0.0`, the
|
|
59
|
+
> [`log_episode`](#log_episode-the-sacred-call) signature is locked
|
|
60
|
+
> and breakages require a major bump (per `AGENTS.md` in the
|
|
61
|
+
> RoboTrace monorepo).
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
Mint an API key in your RoboTrace admin console
|
|
66
|
+
(**Admin → Clients → \<client\> → API access**), then:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import robotrace as rt
|
|
70
|
+
|
|
71
|
+
rt.init(
|
|
72
|
+
api_key="rt_…",
|
|
73
|
+
base_url="https://app.robotrace.dev", # or http://localhost:3000 in dev
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
rt.log_episode(
|
|
77
|
+
name="pick_and_place v3 morning warmup",
|
|
78
|
+
source="real",
|
|
79
|
+
robot="halcyon-bimanual-01",
|
|
80
|
+
policy_version="pap-v3.2.1",
|
|
81
|
+
env_version="halcyon-cell-rev4",
|
|
82
|
+
git_sha="abc1234",
|
|
83
|
+
seed=8124,
|
|
84
|
+
video="/tmp/run.mp4",
|
|
85
|
+
sensors="/tmp/sensors.bin",
|
|
86
|
+
actions="/tmp/actions.parquet",
|
|
87
|
+
duration_s=47.2,
|
|
88
|
+
fps=30,
|
|
89
|
+
metadata={"task": "pick_and_place", "scene": "tabletop"},
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The episode appears in `/admin/episodes` immediately, with the four
|
|
94
|
+
reproducibility fields (policy / env / git / seed) front-and-center
|
|
95
|
+
on the detail page.
|
|
96
|
+
|
|
97
|
+
### From environment variables
|
|
98
|
+
|
|
99
|
+
Same call without hardcoding the key:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export ROBOTRACE_API_KEY=rt_…
|
|
103
|
+
export ROBOTRACE_BASE_URL=https://app.robotrace.dev
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import robotrace as rt
|
|
108
|
+
|
|
109
|
+
# init() is optional when both env vars are set — the default
|
|
110
|
+
# client is constructed lazily on first use.
|
|
111
|
+
rt.log_episode(
|
|
112
|
+
name="…",
|
|
113
|
+
policy_version="…",
|
|
114
|
+
video="/tmp/run.mp4",
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## API
|
|
119
|
+
|
|
120
|
+
### `log_episode` — the sacred call
|
|
121
|
+
|
|
122
|
+
The one-shot entrypoint. Equivalent to `start_episode` → upload all
|
|
123
|
+
artifacts → `finalize`. Use this for the 95% case of "I have files
|
|
124
|
+
on disk, log them and move on."
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
rt.log_episode(
|
|
128
|
+
*,
|
|
129
|
+
# Identification
|
|
130
|
+
name: str | None = None,
|
|
131
|
+
source: Literal["real", "sim", "replay"] = "real",
|
|
132
|
+
robot: str | None = None,
|
|
133
|
+
|
|
134
|
+
# Reproducibility — load-bearing per AGENTS.md
|
|
135
|
+
policy_version: str | None = None,
|
|
136
|
+
env_version: str | None = None,
|
|
137
|
+
git_sha: str | None = None,
|
|
138
|
+
seed: int | None = None,
|
|
139
|
+
|
|
140
|
+
# Artifact paths (uploaded to object storage via signed PUT URLs)
|
|
141
|
+
video: str | Path | None = None,
|
|
142
|
+
sensors: str | Path | None = None,
|
|
143
|
+
actions: str | Path | None = None,
|
|
144
|
+
|
|
145
|
+
# Run details
|
|
146
|
+
duration_s: float | None = None,
|
|
147
|
+
fps: float | None = None,
|
|
148
|
+
metadata: Mapping[str, Any] | None = None,
|
|
149
|
+
|
|
150
|
+
# Final state
|
|
151
|
+
status: Literal["ready", "failed"] = "ready",
|
|
152
|
+
) -> Episode
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Returns the finalized `Episode`. On failure during upload the SDK
|
|
156
|
+
flips the run to `status="failed"` and re-raises so your program
|
|
157
|
+
sees what went wrong.
|
|
158
|
+
|
|
159
|
+
### `start_episode` — explicit lifecycle
|
|
160
|
+
|
|
161
|
+
When you want fine-grained control (stream uploads, defer finalize,
|
|
162
|
+
react to upload errors per-artifact), use `start_episode` and the
|
|
163
|
+
returned `Episode` handle:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
with rt.start_episode(
|
|
167
|
+
name="pick_and_place v3 morning warmup",
|
|
168
|
+
policy_version="pap-v3.2.1",
|
|
169
|
+
artifacts=["video", "sensors"], # only request the slots you'll fill
|
|
170
|
+
) as ep:
|
|
171
|
+
ep.upload_video("/tmp/run.mp4")
|
|
172
|
+
ep.upload_sensors("/tmp/sensors.bin")
|
|
173
|
+
# No explicit finalize — context manager handles it:
|
|
174
|
+
# • clean exit → status="ready"
|
|
175
|
+
# • exception → status="failed", with metadata.failure_reason set
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Or explicit:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
ep = rt.start_episode(name="…", policy_version="…", artifacts=["video"])
|
|
182
|
+
ep.upload_video("/tmp/run.mp4")
|
|
183
|
+
ep.finalize(status="ready", duration_s=47.2, fps=30)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `Client` — explicit instance
|
|
187
|
+
|
|
188
|
+
Skip the module-level default when you need multiple deployments at
|
|
189
|
+
once (e.g. shipping the same run to staging + production), or for
|
|
190
|
+
clean dependency injection in tests:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
with rt.Client(api_key="rt_…", base_url="https://…") as client:
|
|
194
|
+
client.log_episode(name="…", policy_version="…", video="…")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`Client` holds a connection pool — construct it once at process
|
|
198
|
+
startup, reuse across many episodes, and `close()` (or use as a
|
|
199
|
+
context manager) on shutdown.
|
|
200
|
+
|
|
201
|
+
## Errors
|
|
202
|
+
|
|
203
|
+
Every SDK error inherits from `robotrace.RobotraceError`. Catch by
|
|
204
|
+
type rather than parsing message strings:
|
|
205
|
+
|
|
206
|
+
| Exception | When |
|
|
207
|
+
| -------------------- | ------------------------------------------------------ |
|
|
208
|
+
| `ConfigurationError` | Missing `api_key` / `base_url`, file path doesn't exist |
|
|
209
|
+
| `TransportError` | Network / DNS / TLS / timeout |
|
|
210
|
+
| `AuthError` | 401 — bad / missing / revoked key |
|
|
211
|
+
| `NotFoundError` | 404 — episode id doesn't exist (or cross-tenant) |
|
|
212
|
+
| `ConflictError` | 409 — episode is archived, etc. |
|
|
213
|
+
| `ValidationError` | 400 — payload didn't pass server-side validation |
|
|
214
|
+
| `ServerError` | 5xx — flag for retries |
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from robotrace import RobotraceError, AuthError
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
rt.log_episode(...)
|
|
221
|
+
except AuthError:
|
|
222
|
+
# mint a fresh key and reload
|
|
223
|
+
raise
|
|
224
|
+
except RobotraceError:
|
|
225
|
+
# generic recovery / alert
|
|
226
|
+
raise
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Storage
|
|
230
|
+
|
|
231
|
+
Artifact uploads go to Cloudflare R2 via short-lived signed PUT URLs
|
|
232
|
+
the server mints for each call. The SDK streams from disk so memory
|
|
233
|
+
stays flat regardless of file size.
|
|
234
|
+
|
|
235
|
+
When the deployment hasn't wired R2 yet (`R2_ACCOUNT_ID` etc. are
|
|
236
|
+
blank), the create response has `storage="unconfigured"` and any
|
|
237
|
+
`upload_*` call raises `ConfigurationError` with a pointer to the
|
|
238
|
+
production setup checklist. Metadata-only runs still work — useful
|
|
239
|
+
for testing the SDK contract end-to-end before R2 is provisioned.
|
|
240
|
+
|
|
241
|
+
## Layout (current)
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
src/robotrace/
|
|
245
|
+
├── __init__.py # public API + module-level default client
|
|
246
|
+
├── _version.py
|
|
247
|
+
├── client.py # Client class
|
|
248
|
+
├── episode.py # Episode handle + UploadUrl + ArtifactKind
|
|
249
|
+
├── errors.py # RobotraceError + typed subclasses
|
|
250
|
+
└── _http.py # internal httpx wrapper
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
ROS 2 / LeRobot adapters land later under `src/robotrace/adapters/`.
|
|
254
|
+
|
|
255
|
+
## Contributing
|
|
256
|
+
|
|
257
|
+
The SDK lives in the [RoboTrace monorepo](https://github.com/robotrace/robotrace).
|
|
258
|
+
The web app at `apps/web` exposes the ingest API the SDK talks to —
|
|
259
|
+
coordinate breaking changes across both, and treat the
|
|
260
|
+
[`/api/ingest/episode`](https://robotrace.dev/docs/api/ingest)
|
|
261
|
+
contract as the boundary.
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# robotrace-dev (Python SDK)
|
|
2
|
+
|
|
3
|
+
> The official Python SDK for [RoboTrace](https://robotrace.dev) —
|
|
4
|
+
> observability and evals for AI-powered robots.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install robotrace-dev==0.1.0a2
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
> **Distribution name vs. import name.** PyPI distributes us as
|
|
11
|
+
> `robotrace-dev` (matching our `robotrace.dev` domain). The
|
|
12
|
+
> un-hyphenated `robotrace` PyPI namespace is held by an unrelated
|
|
13
|
+
> robotics project, and PyPI's typo-squat protector blocks any
|
|
14
|
+
> single-edit-distance variant (so `robo-trace` was rejected too).
|
|
15
|
+
> The *import* name stays `import robotrace` — same pattern as
|
|
16
|
+
> `pip install python-dateutil` → `import dateutil`.
|
|
17
|
+
|
|
18
|
+
> **Status:** alpha (`0.1.0a2`). The public API in this README is the
|
|
19
|
+
> shape we're iterating against; once we cut `1.0.0`, the
|
|
20
|
+
> [`log_episode`](#log_episode-the-sacred-call) signature is locked
|
|
21
|
+
> and breakages require a major bump (per `AGENTS.md` in the
|
|
22
|
+
> RoboTrace monorepo).
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
Mint an API key in your RoboTrace admin console
|
|
27
|
+
(**Admin → Clients → \<client\> → API access**), then:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import robotrace as rt
|
|
31
|
+
|
|
32
|
+
rt.init(
|
|
33
|
+
api_key="rt_…",
|
|
34
|
+
base_url="https://app.robotrace.dev", # or http://localhost:3000 in dev
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
rt.log_episode(
|
|
38
|
+
name="pick_and_place v3 morning warmup",
|
|
39
|
+
source="real",
|
|
40
|
+
robot="halcyon-bimanual-01",
|
|
41
|
+
policy_version="pap-v3.2.1",
|
|
42
|
+
env_version="halcyon-cell-rev4",
|
|
43
|
+
git_sha="abc1234",
|
|
44
|
+
seed=8124,
|
|
45
|
+
video="/tmp/run.mp4",
|
|
46
|
+
sensors="/tmp/sensors.bin",
|
|
47
|
+
actions="/tmp/actions.parquet",
|
|
48
|
+
duration_s=47.2,
|
|
49
|
+
fps=30,
|
|
50
|
+
metadata={"task": "pick_and_place", "scene": "tabletop"},
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The episode appears in `/admin/episodes` immediately, with the four
|
|
55
|
+
reproducibility fields (policy / env / git / seed) front-and-center
|
|
56
|
+
on the detail page.
|
|
57
|
+
|
|
58
|
+
### From environment variables
|
|
59
|
+
|
|
60
|
+
Same call without hardcoding the key:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export ROBOTRACE_API_KEY=rt_…
|
|
64
|
+
export ROBOTRACE_BASE_URL=https://app.robotrace.dev
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import robotrace as rt
|
|
69
|
+
|
|
70
|
+
# init() is optional when both env vars are set — the default
|
|
71
|
+
# client is constructed lazily on first use.
|
|
72
|
+
rt.log_episode(
|
|
73
|
+
name="…",
|
|
74
|
+
policy_version="…",
|
|
75
|
+
video="/tmp/run.mp4",
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API
|
|
80
|
+
|
|
81
|
+
### `log_episode` — the sacred call
|
|
82
|
+
|
|
83
|
+
The one-shot entrypoint. Equivalent to `start_episode` → upload all
|
|
84
|
+
artifacts → `finalize`. Use this for the 95% case of "I have files
|
|
85
|
+
on disk, log them and move on."
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
rt.log_episode(
|
|
89
|
+
*,
|
|
90
|
+
# Identification
|
|
91
|
+
name: str | None = None,
|
|
92
|
+
source: Literal["real", "sim", "replay"] = "real",
|
|
93
|
+
robot: str | None = None,
|
|
94
|
+
|
|
95
|
+
# Reproducibility — load-bearing per AGENTS.md
|
|
96
|
+
policy_version: str | None = None,
|
|
97
|
+
env_version: str | None = None,
|
|
98
|
+
git_sha: str | None = None,
|
|
99
|
+
seed: int | None = None,
|
|
100
|
+
|
|
101
|
+
# Artifact paths (uploaded to object storage via signed PUT URLs)
|
|
102
|
+
video: str | Path | None = None,
|
|
103
|
+
sensors: str | Path | None = None,
|
|
104
|
+
actions: str | Path | None = None,
|
|
105
|
+
|
|
106
|
+
# Run details
|
|
107
|
+
duration_s: float | None = None,
|
|
108
|
+
fps: float | None = None,
|
|
109
|
+
metadata: Mapping[str, Any] | None = None,
|
|
110
|
+
|
|
111
|
+
# Final state
|
|
112
|
+
status: Literal["ready", "failed"] = "ready",
|
|
113
|
+
) -> Episode
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Returns the finalized `Episode`. On failure during upload the SDK
|
|
117
|
+
flips the run to `status="failed"` and re-raises so your program
|
|
118
|
+
sees what went wrong.
|
|
119
|
+
|
|
120
|
+
### `start_episode` — explicit lifecycle
|
|
121
|
+
|
|
122
|
+
When you want fine-grained control (stream uploads, defer finalize,
|
|
123
|
+
react to upload errors per-artifact), use `start_episode` and the
|
|
124
|
+
returned `Episode` handle:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
with rt.start_episode(
|
|
128
|
+
name="pick_and_place v3 morning warmup",
|
|
129
|
+
policy_version="pap-v3.2.1",
|
|
130
|
+
artifacts=["video", "sensors"], # only request the slots you'll fill
|
|
131
|
+
) as ep:
|
|
132
|
+
ep.upload_video("/tmp/run.mp4")
|
|
133
|
+
ep.upload_sensors("/tmp/sensors.bin")
|
|
134
|
+
# No explicit finalize — context manager handles it:
|
|
135
|
+
# • clean exit → status="ready"
|
|
136
|
+
# • exception → status="failed", with metadata.failure_reason set
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Or explicit:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
ep = rt.start_episode(name="…", policy_version="…", artifacts=["video"])
|
|
143
|
+
ep.upload_video("/tmp/run.mp4")
|
|
144
|
+
ep.finalize(status="ready", duration_s=47.2, fps=30)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `Client` — explicit instance
|
|
148
|
+
|
|
149
|
+
Skip the module-level default when you need multiple deployments at
|
|
150
|
+
once (e.g. shipping the same run to staging + production), or for
|
|
151
|
+
clean dependency injection in tests:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
with rt.Client(api_key="rt_…", base_url="https://…") as client:
|
|
155
|
+
client.log_episode(name="…", policy_version="…", video="…")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`Client` holds a connection pool — construct it once at process
|
|
159
|
+
startup, reuse across many episodes, and `close()` (or use as a
|
|
160
|
+
context manager) on shutdown.
|
|
161
|
+
|
|
162
|
+
## Errors
|
|
163
|
+
|
|
164
|
+
Every SDK error inherits from `robotrace.RobotraceError`. Catch by
|
|
165
|
+
type rather than parsing message strings:
|
|
166
|
+
|
|
167
|
+
| Exception | When |
|
|
168
|
+
| -------------------- | ------------------------------------------------------ |
|
|
169
|
+
| `ConfigurationError` | Missing `api_key` / `base_url`, file path doesn't exist |
|
|
170
|
+
| `TransportError` | Network / DNS / TLS / timeout |
|
|
171
|
+
| `AuthError` | 401 — bad / missing / revoked key |
|
|
172
|
+
| `NotFoundError` | 404 — episode id doesn't exist (or cross-tenant) |
|
|
173
|
+
| `ConflictError` | 409 — episode is archived, etc. |
|
|
174
|
+
| `ValidationError` | 400 — payload didn't pass server-side validation |
|
|
175
|
+
| `ServerError` | 5xx — flag for retries |
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from robotrace import RobotraceError, AuthError
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
rt.log_episode(...)
|
|
182
|
+
except AuthError:
|
|
183
|
+
# mint a fresh key and reload
|
|
184
|
+
raise
|
|
185
|
+
except RobotraceError:
|
|
186
|
+
# generic recovery / alert
|
|
187
|
+
raise
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Storage
|
|
191
|
+
|
|
192
|
+
Artifact uploads go to Cloudflare R2 via short-lived signed PUT URLs
|
|
193
|
+
the server mints for each call. The SDK streams from disk so memory
|
|
194
|
+
stays flat regardless of file size.
|
|
195
|
+
|
|
196
|
+
When the deployment hasn't wired R2 yet (`R2_ACCOUNT_ID` etc. are
|
|
197
|
+
blank), the create response has `storage="unconfigured"` and any
|
|
198
|
+
`upload_*` call raises `ConfigurationError` with a pointer to the
|
|
199
|
+
production setup checklist. Metadata-only runs still work — useful
|
|
200
|
+
for testing the SDK contract end-to-end before R2 is provisioned.
|
|
201
|
+
|
|
202
|
+
## Layout (current)
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
src/robotrace/
|
|
206
|
+
├── __init__.py # public API + module-level default client
|
|
207
|
+
├── _version.py
|
|
208
|
+
├── client.py # Client class
|
|
209
|
+
├── episode.py # Episode handle + UploadUrl + ArtifactKind
|
|
210
|
+
├── errors.py # RobotraceError + typed subclasses
|
|
211
|
+
└── _http.py # internal httpx wrapper
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
ROS 2 / LeRobot adapters land later under `src/robotrace/adapters/`.
|
|
215
|
+
|
|
216
|
+
## Contributing
|
|
217
|
+
|
|
218
|
+
The SDK lives in the [RoboTrace monorepo](https://github.com/robotrace/robotrace).
|
|
219
|
+
The web app at `apps/web` exposes the ingest API the SDK talks to —
|
|
220
|
+
coordinate breaking changes across both, and treat the
|
|
221
|
+
[`/api/ingest/episode`](https://robotrace.dev/docs/api/ingest)
|
|
222
|
+
contract as the boundary.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT.
|