commandagi 0.3.0__py3-none-any.whl

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.
commandagi/lerobot.py ADDED
@@ -0,0 +1,211 @@
1
+ """Record CommandAGI sim episodes as a LeRobot-style dataset — observation (camera + joint state),
2
+ action, and timestamps per step, written to disk in the LeRobot on-disk layout (parquet per episode
3
+ + frames + meta/info.json) so it loads into imitation/VLA training pipelines.
4
+
5
+ Two ways to use it:
6
+
7
+ 1. Wrap a Gym env — every ``step`` is recorded automatically::
8
+
9
+ from commandagi.gym_env import CommandAGIEnv
10
+ from commandagi.lerobot import RecordingWrapper
11
+ env = RecordingWrapper(CommandAGIEnv(world), root="./datasets/pick", task="pick up the cube")
12
+ obs, _ = env.reset()
13
+ for _ in range(200):
14
+ obs, *_ = env.step(policy(obs))
15
+ env.save() # flush the dataset to ./datasets/pick
16
+
17
+ 2. Hand-drive and log directly with :class:`EpisodeRecorder` (e.g. while teleoperating in Sim Studio).
18
+
19
+ This writes the LeRobot v2.1/3.0 directory shape (``meta/info.json``, ``data/chunk-000/episode_*.parquet``,
20
+ ``images/observation.image/episode_*/frame_*.jpg``). Requires ``pyarrow`` + ``numpy`` + ``pillow``
21
+ (``pip install "commandagi[lerobot]"``). Stats consolidation (``meta/stats.json``) is left to the
22
+ ``lerobot`` tooling; everything needed to compute it is on disk.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import io
27
+ import json
28
+ import time
29
+ from pathlib import Path
30
+ from typing import Any, Optional
31
+
32
+ try:
33
+ import numpy as np
34
+ import pyarrow as pa
35
+ import pyarrow.parquet as pq
36
+ from PIL import Image
37
+ except ImportError as e: # pragma: no cover
38
+ raise ImportError("commandagi.lerobot needs `pyarrow`, `numpy`, `pillow` — pip install 'commandagi[lerobot]'") from e
39
+
40
+ CODEBASE_VERSION = "v2.1"
41
+
42
+
43
+ class EpisodeRecorder:
44
+ """Accumulates steps for one or more episodes and writes a LeRobot-layout dataset.
45
+
46
+ Each step is ``(state, action, image)`` where ``state``/``action`` are 1-D float arrays and
47
+ ``image`` is an HxWx3 uint8 array (optional). Call :meth:`add` per step, :meth:`end_episode` to
48
+ close the current episode, and :meth:`save` to write everything out.
49
+ """
50
+
51
+ def __init__(self, root: str, *, fps: float = 10.0, task: str = "", image_key: str = "observation.image"):
52
+ self.root = Path(root)
53
+ self.fps = float(fps)
54
+ self.task = task
55
+ self.image_key = image_key
56
+ self._ep: list[dict[str, Any]] = [] # steps in the current episode
57
+ self._episodes: list[list[dict[str, Any]]] = [] # completed episodes
58
+ self._state_dim: Optional[int] = None
59
+ self._action_dim: Optional[int] = None
60
+ self._img_shape: Optional[tuple[int, int, int]] = None
61
+ self._t0 = time.time()
62
+
63
+ def start_episode(self) -> None:
64
+ if self._ep:
65
+ self.end_episode()
66
+ self._ep = []
67
+ self._t0 = time.time()
68
+
69
+ def add(self, state, action, image=None, *, reward: float = 0.0) -> None:
70
+ state = np.asarray(state, dtype=np.float32).reshape(-1)
71
+ action = np.asarray(action, dtype=np.float32).reshape(-1)
72
+ self._state_dim = self._state_dim or state.shape[0]
73
+ self._action_dim = self._action_dim or action.shape[0]
74
+ step: dict[str, Any] = {
75
+ "observation.state": state,
76
+ "action": action,
77
+ "timestamp": time.time() - self._t0,
78
+ "reward": float(reward),
79
+ }
80
+ if image is not None:
81
+ img = np.asarray(image, dtype=np.uint8)
82
+ self._img_shape = self._img_shape or tuple(img.shape) # type: ignore[assignment]
83
+ step["_image"] = img
84
+ self._ep.append(step)
85
+
86
+ def end_episode(self) -> None:
87
+ if self._ep:
88
+ self._episodes.append(self._ep)
89
+ self._ep = []
90
+
91
+ def save(self) -> Path:
92
+ """Write the dataset (all completed episodes + the current one) to disk; returns the root."""
93
+ self.end_episode()
94
+ if not self._episodes:
95
+ raise ValueError("nothing to save — record at least one step")
96
+ (self.root / "data" / "chunk-000").mkdir(parents=True, exist_ok=True)
97
+ (self.root / "meta").mkdir(parents=True, exist_ok=True)
98
+
99
+ total_frames = 0
100
+ global_index = 0
101
+ for ep_idx, steps in enumerate(self._episodes):
102
+ cols: dict[str, list] = {
103
+ "observation.state": [], "action": [], "timestamp": [], "reward": [],
104
+ "frame_index": [], "episode_index": [], "index": [], "next.done": [],
105
+ }
106
+ has_image = any("_image" in s for s in steps)
107
+ if has_image:
108
+ cols[self.image_key] = []
109
+ img_dir = self.root / "images" / self.image_key / f"episode_{ep_idx:06d}"
110
+ img_dir.mkdir(parents=True, exist_ok=True)
111
+ for f_idx, s in enumerate(steps):
112
+ cols["observation.state"].append(s["observation.state"].tolist())
113
+ cols["action"].append(s["action"].tolist())
114
+ cols["timestamp"].append(float(s["timestamp"]))
115
+ cols["reward"].append(float(s["reward"]))
116
+ cols["frame_index"].append(f_idx)
117
+ cols["episode_index"].append(ep_idx)
118
+ cols["index"].append(global_index)
119
+ cols["next.done"].append(f_idx == len(steps) - 1)
120
+ if has_image:
121
+ rel = f"images/{self.image_key}/episode_{ep_idx:06d}/frame_{f_idx:06d}.jpg"
122
+ if "_image" in s:
123
+ Image.fromarray(s["_image"]).save(self.root / rel, format="JPEG", quality=90)
124
+ cols[self.image_key].append(rel)
125
+ global_index += 1
126
+ total_frames += len(steps)
127
+ pq.write_table(pa.table(cols), self.root / "data" / "chunk-000" / f"episode_{ep_idx:06d}.parquet")
128
+
129
+ self._write_info(total_frames)
130
+ self._write_episodes_meta()
131
+ self._write_tasks_meta()
132
+ return self.root
133
+
134
+ def _features(self) -> dict:
135
+ feats: dict[str, Any] = {
136
+ "observation.state": {"dtype": "float32", "shape": [self._state_dim or 0], "names": None},
137
+ "action": {"dtype": "float32", "shape": [self._action_dim or 0], "names": None},
138
+ "timestamp": {"dtype": "float32", "shape": [1], "names": None},
139
+ "reward": {"dtype": "float32", "shape": [1], "names": None},
140
+ "frame_index": {"dtype": "int64", "shape": [1], "names": None},
141
+ "episode_index": {"dtype": "int64", "shape": [1], "names": None},
142
+ "index": {"dtype": "int64", "shape": [1], "names": None},
143
+ "next.done": {"dtype": "bool", "shape": [1], "names": None},
144
+ }
145
+ if self._img_shape is not None:
146
+ feats[self.image_key] = {"dtype": "image", "shape": list(self._img_shape), "names": ["height", "width", "channel"]}
147
+ return feats
148
+
149
+ def _write_info(self, total_frames: int) -> None:
150
+ info = {
151
+ "codebase_version": CODEBASE_VERSION,
152
+ "robot_type": "commandagi-sim",
153
+ "total_episodes": len(self._episodes),
154
+ "total_frames": total_frames,
155
+ "total_tasks": 1,
156
+ "total_chunks": 1,
157
+ "chunks_size": 1000,
158
+ "fps": self.fps,
159
+ "splits": {"train": f"0:{len(self._episodes)}"},
160
+ "data_path": "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet",
161
+ "features": self._features(),
162
+ }
163
+ (self.root / "meta" / "info.json").write_text(json.dumps(info, indent=2))
164
+
165
+ def _write_episodes_meta(self) -> None:
166
+ lines = []
167
+ for ep_idx, steps in enumerate(self._episodes):
168
+ lines.append(json.dumps({"episode_index": ep_idx, "tasks": [self.task] if self.task else [], "length": len(steps)}))
169
+ (self.root / "meta" / "episodes.jsonl").write_text("\n".join(lines) + "\n")
170
+
171
+ def _write_tasks_meta(self) -> None:
172
+ (self.root / "meta" / "tasks.jsonl").write_text(json.dumps({"task_index": 0, "task": self.task}) + "\n")
173
+
174
+
175
+ class RecordingWrapper:
176
+ """Wrap a :class:`commandagi.gym_env.CommandAGIEnv` so every ``reset``/``step`` is logged.
177
+
178
+ The observation's ``qpos``+``qvel`` become ``observation.state``; ``image`` (if present) is the
179
+ camera frame; the action passed to ``step`` is recorded verbatim. Call :meth:`save` when done.
180
+ """
181
+
182
+ def __init__(self, env, root: str, *, task: str = "", fps: Optional[float] = None):
183
+ self.env = env
184
+ self.recorder = EpisodeRecorder(root, fps=fps or (1.0 / getattr(env, "control_dt", 0.1)), task=task)
185
+ self.action_space = env.action_space
186
+ self.observation_space = env.observation_space
187
+
188
+ @staticmethod
189
+ def _state(obs: dict) -> np.ndarray:
190
+ return np.concatenate([np.asarray(obs.get("qpos", []), dtype=np.float32), np.asarray(obs.get("qvel", []), dtype=np.float32)])
191
+
192
+ def reset(self, **kw):
193
+ obs, info = self.env.reset(**kw)
194
+ self.recorder.start_episode()
195
+ self._last_obs = obs
196
+ return obs, info
197
+
198
+ def step(self, action):
199
+ obs, reward, terminated, truncated, info = self.env.step(action)
200
+ # Record the transition: the observation we acted on + the action + the reward received.
201
+ self.recorder.add(self._state(self._last_obs), action, self._last_obs.get("image"), reward=reward)
202
+ self._last_obs = obs
203
+ if terminated or truncated:
204
+ self.recorder.end_episode()
205
+ return obs, reward, terminated, truncated, info
206
+
207
+ def save(self):
208
+ return self.recorder.save()
209
+
210
+ def close(self):
211
+ self.env.close()
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: commandagi
3
+ Version: 0.3.0
4
+ Summary: Python SDK for CommandAGI — launch cloud computers and 3D robot simulations and control them programmatically.
5
+ Project-URL: Homepage, https://commandagi.com
6
+ Project-URL: Documentation, https://github.com/CommandAGI/commandagi/blob/main/docs/ROBOT_DEVELOPER_API.md
7
+ Author: CommandAGI
8
+ License: MIT
9
+ Keywords: agents,computer-use,physics,reinforcement-learning,robotics,simulation
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: requests>=2.28
12
+ Requires-Dist: websocket-client>=1.5
13
+ Provides-Extra: gym
14
+ Requires-Dist: gymnasium>=0.29; extra == 'gym'
15
+ Requires-Dist: numpy>=1.23; extra == 'gym'
16
+ Requires-Dist: pillow>=9.0; extra == 'gym'
17
+ Provides-Extra: lerobot
18
+ Requires-Dist: numpy>=1.23; extra == 'lerobot'
19
+ Requires-Dist: pillow>=9.0; extra == 'lerobot'
20
+ Requires-Dist: pyarrow>=14.0; extra == 'lerobot'
21
+ Provides-Extra: vision
22
+ Requires-Dist: numpy>=1.23; extra == 'vision'
23
+ Requires-Dist: pillow>=9.0; extra == 'vision'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # CommandAGI Python SDK
27
+
28
+ Launch real cloud **computers** and **3D robot simulations** and control them from Python — stream
29
+ the robot's camera, send actions, run episodes. No agent required: you drive.
30
+
31
+ ```bash
32
+ pip install commandagi # + `pip install commandagi[vision]` for numpy frames
33
+ ```
34
+
35
+ ## Robot testing in a 3D world
36
+
37
+ ```python
38
+ from commandagi import CommandAGI
39
+
40
+ cagi = CommandAGI(api_key="cagi_...") # or set COMMANDAGI_API_KEY
41
+
42
+ with cagi.launch("simulation/warehouse") as world:
43
+ obs = world.observe() # JPEG bytes from the robot's head camera
44
+ for _ in range(20):
45
+ obs = world.step("turn", dir="left") # act, then get the next frame
46
+ world.reset() # robot back to the episode start
47
+ # leaving the block stops the world and releases the cloud VM
48
+ ```
49
+
50
+ `launch()` provisions a real GCE VM running a 3D physics world, waits until it's streaming, and gives
51
+ you a `World`. Built-in scenes: `simulation/warehouse`, `simulation/house-on-fire`,
52
+ `simulation/school` (a mobile robot in each).
53
+
54
+ ### The control vocabulary
55
+
56
+ | World kind | actions |
57
+ | ----------- | ------------------------------------------------------------------------- |
58
+ | robot / sim | `move(speed)`, `back(speed)`, `turn(dir, rate)`, `stop`, `reset` |
59
+ | computer | `click(x, y)`, `type(text)`, `key(key)`, `move(x, y)`, `scroll(x, y, dy)` |
60
+
61
+ ```python
62
+ world.act("move", speed=0.8) # fire-and-forget
63
+ obs = world.step("move", speed=0.8) # act + return the next observation (settles 0.8s)
64
+ obs = world.observe(fresh=True) # wait for a frame newer than now
65
+ arr = world.observe_array() # HxWx3 uint8 numpy (needs commandagi[vision])
66
+ for frame in world.stream(): # live generator of frames
67
+ ...
68
+ ```
69
+
70
+ ## Simulator instances (morphology-agnostic robots)
71
+
72
+ The simulator is **morphology-agnostic**: a robot is just a set of named actuators and sites, driven
73
+ by one small **generic** control vocabulary — no `drive`/`gripper`, just `ctrl` / `actuator` / `ik`
74
+ / `trajectory` / `describe`. Spin up your own instance, choose who can watch or add robots, and
75
+ populate it with one or many robots on a single session.
76
+
77
+ ```python
78
+ from commandagi import CommandAGI
79
+
80
+ cagi = CommandAGI(api_key="cagi_...")
81
+
82
+ sim = cagi.launch_sim(scene="the-matrix", visibility="private", title="demo")
83
+ print("instance:", sim.id, "session:", sim.session_id)
84
+
85
+ # Who can do what:
86
+ sim.grant("user_teammate", capability="viewer") # may watch the stream
87
+ sim.grant("user_buddy", capability="operator") # may also launch robots into the world
88
+
89
+ # Add robots (each becomes a embodiment on sim.session_id):
90
+ rover = sim.join_robot(kind="rover") # -> {robotId, embodimentId, sessionId}
91
+ arm = sim.join_robot(kind="arm")
92
+
93
+ cagi.sims() # list instances visible to you
94
+ cagi.get_sim(sim.id) # rehydrate a SimInstance
95
+ sim.view() # instance metadata + attached embodiments
96
+ sim.stop() # release it (or use `with cagi.launch_sim(...) as sim:`)
97
+ ```
98
+
99
+ ### Generic robot control
100
+
101
+ `World` exposes the morphology-agnostic vocabulary (address a specific robot in a multi-robot embodiment
102
+ with `robot_id`):
103
+
104
+ ```python
105
+ world = cagi.connect_world(sim.session_id, rover["embodimentId"], kind="robot")
106
+
107
+ desc = world.describe() # actuators, sites, objects (best-effort)
108
+ world.ctrl({"left_wheel": 1.0, "right_wheel": 1.0}) # set actuator targets directly
109
+ world.actuator("left_wheel", 0.0) # one named actuator
110
+ world.ik(target=[0.3, 0.0, 0.4], site="ee", relative=False) # inverse kinematics to a point
111
+ world.trajectory([{"left_wheel": 1.0}, {"left_wheel": 0.0}]) # follow waypoints
112
+ frame = world.observe() # camera frame, as before
113
+ ```
114
+
115
+ > `describe()` is best-effort: the runtime answers a describe request over the session channel, but
116
+ > there is currently no synchronous describe HTTP endpoint — if nothing echoes back it returns `{}`,
117
+ > and the autonomous agent also obtains descriptions server-side via `/agent/robot-act`.
118
+
119
+ ## Autonomous agents over many robots
120
+
121
+ One agent can drive **many** robots in a single session. `RobotAgent` loops perceive → reason → act:
122
+ each step it gathers every embodiment's description + a fresh camera frame, calls `/agent/robot-act` with
123
+ all embodiments, and applies the returned tool calls (`ctrl`/`actuator`/`ik`/`trajectory`) back to the
124
+ addressed embodiment — until a `done` call or `max_steps`.
125
+
126
+ ```python
127
+ from commandagi import CommandAGI
128
+ from commandagi.agent import RobotAgent, attach_robots
129
+
130
+ cagi = CommandAGI(api_key="cagi_...")
131
+ sim = cagi.launch_sim(scene="warehouse")
132
+
133
+ embodiments = attach_robots(cagi, sim, kinds=["rover", "arm"]) # two robots, one session
134
+
135
+ with RobotAgent(cagi, sim.session_id, embodiments, goal="bring the red box to the arm") as agent:
136
+ result = agent.run(max_steps=25) # blocks; prints reasoning + applied calls each step
137
+ print("done:", result["done"], "in", result["steps"], "steps")
138
+
139
+ sim.stop()
140
+ ```
141
+
142
+ A full runnable script lives in [`examples/sim_agent.py`](examples/sim_agent.py).
143
+
144
+ ## Computers too
145
+
146
+ ```python
147
+ with cagi.launch("computer/software-engineer") as pc:
148
+ pc.act("type", text="hello")
149
+ pc.act("key", key="Return")
150
+ screenshot = pc.observe() # PNG bytes of the live Ubuntu desktop
151
+ ```
152
+
153
+ ## Auth
154
+
155
+ Create an API key with an `operator` scope (dashboard → API keys, or `POST /me/api-keys`). Pass it to
156
+ `CommandAGI(api_key=...)` or set `COMMANDAGI_API_KEY`. Point at another environment with
157
+ `COMMANDAGI_BASE_URL` (e.g. `https://api-dev.commandagi.com`).
158
+
159
+ Full HTTP + WebSocket reference (what the SDK wraps): [`docs/platform/ROBOT_DEVELOPER_API.md`](../../docs/platform/ROBOT_DEVELOPER_API.md).
@@ -0,0 +1,9 @@
1
+ commandagi/__init__.py,sha256=aF679wR7Fa0-7Xg8swPgQZ0PKB7LlyMgiwEiWUWknw0,697
2
+ commandagi/agent.py,sha256=hmnyWYQ9Ma7gZYQVNmXQKv-3YC70FQs_3KjpWXkSaq8,7737
3
+ commandagi/bridge.py,sha256=yS225aHQtYTsLEAz8nAxwG-qqUpidtqcbbh-deflwfU,5260
4
+ commandagi/client.py,sha256=LeWwaqe7zjX86Onr_OtHCgxgNiIryxEPIu1RAugJHpQ,24608
5
+ commandagi/gym_env.py,sha256=a9jKDGr5qI-e49E5Mm-EvGMRlTRvPQKErYsg2yCWv5s,6405
6
+ commandagi/lerobot.py,sha256=b-hP3MXnGLBuOoE0DsWvS_iLYAx3-V3oojttHRSKJdc,9488
7
+ commandagi-0.3.0.dist-info/METADATA,sha256=rAVYv4ChUdWxbutZ2DP9dokFJGZCe0LtmBF6qGySobY,6860
8
+ commandagi-0.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ commandagi-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any