dexos-lite 0.0.1__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.
Potentially problematic release.
This version of dexos-lite might be problematic. Click here for more details.
- dexos_lite/__init__.py +84 -0
- dexos_lite/cli.py +367 -0
- dexos_lite/dataset.py +600 -0
- dexos_lite/dataset_validation.py +237 -0
- dexos_lite/exceptions.py +157 -0
- dexos_lite/inference.py +134 -0
- dexos_lite/robot/__init__.py +42 -0
- dexos_lite/robot/aloha.py +54 -0
- dexos_lite/robot/base.py +418 -0
- dexos_lite/robot/controller/__init__.py +5 -0
- dexos_lite/robot/controller/aloha.py +264 -0
- dexos_lite/robot/controller/base.py +139 -0
- dexos_lite/robot/controller/mirror.py +139 -0
- dexos_lite/robot/mirror.py +58 -0
- dexos_lite/robot/sensor/__init__.py +40 -0
- dexos_lite/robot/sensor/base.py +223 -0
- dexos_lite/robot/sensor/realsense.py +266 -0
- dexos_lite/utils/__init__.py +24 -0
- dexos_lite/utils/action_smoothing.py +140 -0
- dexos_lite/utils/client.py +381 -0
- dexos_lite/utils/http.py +105 -0
- dexos_lite/utils/rotation.py +35 -0
- dexos_lite/utils/serialization.py +76 -0
- dexos_lite-0.0.1.dist-info/METADATA +224 -0
- dexos_lite-0.0.1.dist-info/RECORD +28 -0
- dexos_lite-0.0.1.dist-info/WHEEL +5 -0
- dexos_lite-0.0.1.dist-info/entry_points.txt +2 -0
- dexos_lite-0.0.1.dist-info/top_level.txt +1 -0
dexos_lite/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from dexos_lite.inference import infer_robot
|
|
2
|
+
from dexos_lite.dataset_validation import DEFAULT_DATASET_FORMAT, DatasetFormat
|
|
3
|
+
from dexos_lite.utils.client import ActionType, InferenceResult, DexosClient
|
|
4
|
+
from dexos_lite.utils.http import DexosResponse
|
|
5
|
+
from dexos_lite.exceptions import (
|
|
6
|
+
CliError,
|
|
7
|
+
CliExit,
|
|
8
|
+
DatasetDependencyError,
|
|
9
|
+
DatasetFormatError,
|
|
10
|
+
DatasetReadError,
|
|
11
|
+
DatasetUploadError,
|
|
12
|
+
DatasetValidationError,
|
|
13
|
+
DexosAPIError,
|
|
14
|
+
DexosClientError,
|
|
15
|
+
DexosConfigError,
|
|
16
|
+
DexosHTTPError,
|
|
17
|
+
DexosRequestError,
|
|
18
|
+
InferenceInterrupted,
|
|
19
|
+
RobotActionError,
|
|
20
|
+
RobotConfigError,
|
|
21
|
+
RobotConfigImportError,
|
|
22
|
+
RobotConfigNotFoundError,
|
|
23
|
+
RobotConfigTypeError,
|
|
24
|
+
RobotDependencyError,
|
|
25
|
+
RobotError,
|
|
26
|
+
RobotNotStartedError,
|
|
27
|
+
RobotTimeoutError,
|
|
28
|
+
RobotUnsupportedOperationError,
|
|
29
|
+
SensorDataError,
|
|
30
|
+
SensorDependencyError,
|
|
31
|
+
SensorError,
|
|
32
|
+
SensorImageFormatError,
|
|
33
|
+
SensorImageRequiredError,
|
|
34
|
+
SensorImageTooLargeError,
|
|
35
|
+
SensorNotFoundError,
|
|
36
|
+
SensorNotImplementedError,
|
|
37
|
+
SensorNotStartedError,
|
|
38
|
+
SensorParamError,
|
|
39
|
+
SensorUnsupportedError,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"InferenceResult",
|
|
44
|
+
"ActionType",
|
|
45
|
+
"DEFAULT_DATASET_FORMAT",
|
|
46
|
+
"CliError",
|
|
47
|
+
"CliExit",
|
|
48
|
+
"DatasetDependencyError",
|
|
49
|
+
"DatasetFormatError",
|
|
50
|
+
"DatasetReadError",
|
|
51
|
+
"DatasetUploadError",
|
|
52
|
+
"DatasetValidationError",
|
|
53
|
+
"DatasetFormat",
|
|
54
|
+
"DexosAPIError",
|
|
55
|
+
"DexosClient",
|
|
56
|
+
"DexosClientError",
|
|
57
|
+
"DexosConfigError",
|
|
58
|
+
"DexosHTTPError",
|
|
59
|
+
"DexosRequestError",
|
|
60
|
+
"DexosResponse",
|
|
61
|
+
"InferenceInterrupted",
|
|
62
|
+
"RobotActionError",
|
|
63
|
+
"RobotConfigError",
|
|
64
|
+
"RobotConfigImportError",
|
|
65
|
+
"RobotConfigNotFoundError",
|
|
66
|
+
"RobotConfigTypeError",
|
|
67
|
+
"RobotDependencyError",
|
|
68
|
+
"RobotError",
|
|
69
|
+
"RobotNotStartedError",
|
|
70
|
+
"RobotTimeoutError",
|
|
71
|
+
"RobotUnsupportedOperationError",
|
|
72
|
+
"SensorDataError",
|
|
73
|
+
"SensorDependencyError",
|
|
74
|
+
"SensorError",
|
|
75
|
+
"SensorImageFormatError",
|
|
76
|
+
"SensorImageRequiredError",
|
|
77
|
+
"SensorImageTooLargeError",
|
|
78
|
+
"SensorNotFoundError",
|
|
79
|
+
"SensorNotImplementedError",
|
|
80
|
+
"SensorNotStartedError",
|
|
81
|
+
"SensorParamError",
|
|
82
|
+
"SensorUnsupportedError",
|
|
83
|
+
"infer_robot",
|
|
84
|
+
]
|
dexos_lite/cli.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import signal
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import FrameType, TracebackType
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from dexos_lite.dataset import upload_dataset
|
|
15
|
+
from dexos_lite.dataset_validation import DEFAULT_DATASET_FORMAT, DatasetFormat
|
|
16
|
+
from dexos_lite.exceptions import CliError, CliExit, InferenceInterrupted
|
|
17
|
+
from dexos_lite.inference import infer_robot
|
|
18
|
+
from dexos_lite.robot import AlohaRobot, MirrorRobot, load_robot_config
|
|
19
|
+
from dexos_lite.utils.client import DexosClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_options = {}
|
|
26
|
+
ROBOT_TYPES = {
|
|
27
|
+
"aloha": AlohaRobot,
|
|
28
|
+
"mirror": MirrorRobot,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _InferenceInterruptHandler:
|
|
33
|
+
"""Translate ``Ctrl+C`` into a controlled inference-loop stop."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
"""Create a temporary SIGINT handler for the active inference loop."""
|
|
37
|
+
|
|
38
|
+
self.stop_requested = False
|
|
39
|
+
self._installed = False
|
|
40
|
+
self._previous_handler: Any = None
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> "_InferenceInterruptHandler":
|
|
43
|
+
"""Install the SIGINT handler.
|
|
44
|
+
|
|
45
|
+
:return: The active handler instance.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
if threading.current_thread() is not threading.main_thread():
|
|
49
|
+
return self
|
|
50
|
+
self._previous_handler = signal.getsignal(signal.SIGINT)
|
|
51
|
+
signal.signal(signal.SIGINT, self._handle_signal)
|
|
52
|
+
self._installed = True
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(
|
|
56
|
+
self,
|
|
57
|
+
exc_type: Optional[type[BaseException]],
|
|
58
|
+
exc: Optional[BaseException],
|
|
59
|
+
traceback: Optional[TracebackType],
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Restore the previous SIGINT handler.
|
|
62
|
+
|
|
63
|
+
:param exc_type: Exception type leaving the context.
|
|
64
|
+
:param exc: Exception leaving the context.
|
|
65
|
+
:param traceback: Traceback leaving the context.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
if self._installed:
|
|
69
|
+
signal.signal(signal.SIGINT, self._previous_handler)
|
|
70
|
+
|
|
71
|
+
def _handle_signal(self, signum: int, frame: Optional[FrameType]) -> None:
|
|
72
|
+
"""Stop the current inference loop immediately.
|
|
73
|
+
|
|
74
|
+
:param signum: Received signal number.
|
|
75
|
+
:param frame: Current interpreter frame.
|
|
76
|
+
:raises _InferenceInterrupted: Always raised to interrupt the active call.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
self.stop_requested = True
|
|
80
|
+
raise InferenceInterrupted
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _UploadProgress:
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
self._context: Any = None
|
|
86
|
+
self._bar: Any = None
|
|
87
|
+
self._last_uploaded = 0
|
|
88
|
+
|
|
89
|
+
def update(self, uploaded: int, total: int) -> None:
|
|
90
|
+
if self._bar is None:
|
|
91
|
+
self._context = click.progressbar(
|
|
92
|
+
length=total,
|
|
93
|
+
label="Uploading dataset",
|
|
94
|
+
show_eta=True,
|
|
95
|
+
show_percent=True,
|
|
96
|
+
show_pos=True,
|
|
97
|
+
file=click.get_text_stream("stderr"),
|
|
98
|
+
)
|
|
99
|
+
self._bar = self._context.__enter__()
|
|
100
|
+
self._last_uploaded = 0
|
|
101
|
+
|
|
102
|
+
delta = uploaded - self._last_uploaded
|
|
103
|
+
if delta > 0:
|
|
104
|
+
self._bar.update(delta)
|
|
105
|
+
self._last_uploaded = uploaded
|
|
106
|
+
|
|
107
|
+
def retry(self, completed_attempt: int, total_attempts: int, reason: str) -> None:
|
|
108
|
+
self.close()
|
|
109
|
+
next_attempt = completed_attempt + 1
|
|
110
|
+
click.echo(
|
|
111
|
+
(
|
|
112
|
+
f"Upload attempt {completed_attempt}/{total_attempts} failed; "
|
|
113
|
+
f"retrying attempt {next_attempt}/{total_attempts}: {reason}"
|
|
114
|
+
),
|
|
115
|
+
err=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
if self._context is None:
|
|
120
|
+
return
|
|
121
|
+
self._context.__exit__(None, None, None)
|
|
122
|
+
self._context = None
|
|
123
|
+
self._bar = None
|
|
124
|
+
self._last_uploaded = 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _upload_success_message(result: Dict[str, Any]) -> str:
|
|
128
|
+
details = []
|
|
129
|
+
dataset_id = result.get("dataset_id")
|
|
130
|
+
status = result.get("status")
|
|
131
|
+
if dataset_id:
|
|
132
|
+
details.append(f"dataset_id={dataset_id}")
|
|
133
|
+
if status:
|
|
134
|
+
details.append(f"status={status}")
|
|
135
|
+
suffix = f" ({', '.join(details)})" if details else ""
|
|
136
|
+
return f"Dataset upload succeeded{suffix}."
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _upload_failure_message(exc: Exception) -> str:
|
|
140
|
+
message = str(exc).strip() or exc.__class__.__name__
|
|
141
|
+
return f"Dataset upload failed: {message}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _prompt_inference_feedback() -> Optional[Dict[str, Any]]:
|
|
145
|
+
try:
|
|
146
|
+
score = click.prompt(
|
|
147
|
+
"Please evaluate this task on a scale of 0 to 10",
|
|
148
|
+
type=click.IntRange(0, 10),
|
|
149
|
+
)
|
|
150
|
+
message = click.prompt(
|
|
151
|
+
"Feedback message",
|
|
152
|
+
default="",
|
|
153
|
+
show_default=False,
|
|
154
|
+
)
|
|
155
|
+
except (click.Abort, EOFError, KeyboardInterrupt):
|
|
156
|
+
click.echo("feedback skipped", err=True)
|
|
157
|
+
return None
|
|
158
|
+
return {"score": score, "message": message}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _submit_inference_feedback(client: DexosClient) -> None:
|
|
162
|
+
feedback = _prompt_inference_feedback()
|
|
163
|
+
if feedback is None:
|
|
164
|
+
return
|
|
165
|
+
try:
|
|
166
|
+
client.feedback(
|
|
167
|
+
score=feedback["score"],
|
|
168
|
+
message=feedback["message"],
|
|
169
|
+
)
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
click.echo(f"feedback failed: {exc}", err=True)
|
|
172
|
+
return
|
|
173
|
+
click.echo("feedback submitted")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _reset_robot_to_home(robot: Any, *, raise_errors: bool = True) -> None:
|
|
177
|
+
try:
|
|
178
|
+
click.echo("resetting robot to home pose...")
|
|
179
|
+
robot.reset_to_home()
|
|
180
|
+
click.echo("robot reset to home pose")
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
if raise_errors:
|
|
183
|
+
raise
|
|
184
|
+
click.echo(f"reset_to_home failed: {exc}", err=True)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@click.group()
|
|
188
|
+
@click.option("--debug", is_flag=True, help="Enable debug mode.")
|
|
189
|
+
def cli(debug) -> None:
|
|
190
|
+
global _options
|
|
191
|
+
_options["debug"] = debug
|
|
192
|
+
if debug:
|
|
193
|
+
logging.basicConfig(
|
|
194
|
+
level=logging.ERROR,
|
|
195
|
+
format=(
|
|
196
|
+
"%(asctime)s | %(levelname)-8s | "
|
|
197
|
+
"%(name)s:%(funcName)s:%(lineno)d - %(message)s"
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
logging.getLogger("dexos_lite").setLevel(logging.INFO)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def safe_cli(): # pragma: no cover
|
|
204
|
+
try:
|
|
205
|
+
cli()
|
|
206
|
+
except Exception as e:
|
|
207
|
+
if _options.get("debug", False):
|
|
208
|
+
raise
|
|
209
|
+
else:
|
|
210
|
+
click.echo(f"\n[{type(e).__name__}] {e}", err=True)
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@cli.group()
|
|
215
|
+
def dataset() -> None:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@cli.group("robot")
|
|
220
|
+
def robot_cli() -> None:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataset.command("upload")
|
|
225
|
+
@click.argument("path", type=click.Path(exists=True, path_type=str))
|
|
226
|
+
@click.option("--api-key", envvar=DexosClient.api_key_env)
|
|
227
|
+
@click.option("--dataset-name")
|
|
228
|
+
@click.option(
|
|
229
|
+
"--dataset-format",
|
|
230
|
+
default=DEFAULT_DATASET_FORMAT.value,
|
|
231
|
+
show_default=True,
|
|
232
|
+
type=click.Choice([item.value for item in DatasetFormat]),
|
|
233
|
+
)
|
|
234
|
+
@click.option("--description", default="", show_default=True)
|
|
235
|
+
def upload_dataset_command(
|
|
236
|
+
path: str,
|
|
237
|
+
api_key: Optional[str],
|
|
238
|
+
dataset_name: Optional[str],
|
|
239
|
+
dataset_format: str,
|
|
240
|
+
description: str,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Upload a dataset using the configured CLI endpoint.
|
|
243
|
+
|
|
244
|
+
:param path: Dataset file or directory path.
|
|
245
|
+
:param api_key: API key from ``--api-key`` or ``DEXOS_API_KEY``.
|
|
246
|
+
:param dataset_name: Remote dataset name.
|
|
247
|
+
:param dataset_format: Dataset format name.
|
|
248
|
+
:param description: Optional dataset description.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
progress = _UploadProgress()
|
|
252
|
+
try:
|
|
253
|
+
result = upload_dataset(
|
|
254
|
+
path,
|
|
255
|
+
api_key=api_key,
|
|
256
|
+
dataset_name=dataset_name,
|
|
257
|
+
dataset_format=dataset_format,
|
|
258
|
+
description=description,
|
|
259
|
+
direct_upload_max_retries=3,
|
|
260
|
+
progress_callback=progress.update,
|
|
261
|
+
retry_callback=progress.retry,
|
|
262
|
+
)
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
progress.close()
|
|
265
|
+
click.echo(_upload_failure_message(exc), err=True)
|
|
266
|
+
raise CliExit(1) from None
|
|
267
|
+
finally:
|
|
268
|
+
progress.close()
|
|
269
|
+
|
|
270
|
+
click.echo(_upload_success_message(result))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@robot_cli.command("config")
|
|
274
|
+
@click.option(
|
|
275
|
+
"--robot-id",
|
|
276
|
+
required=True,
|
|
277
|
+
type=click.Choice(sorted(ROBOT_TYPES)),
|
|
278
|
+
help="Robot ID to generate config for.",
|
|
279
|
+
)
|
|
280
|
+
@click.option(
|
|
281
|
+
"--path",
|
|
282
|
+
"config_path",
|
|
283
|
+
default="config.py",
|
|
284
|
+
show_default=True,
|
|
285
|
+
type=click.Path(dir_okay=False, path_type=str),
|
|
286
|
+
help="Path for the generated config.",
|
|
287
|
+
)
|
|
288
|
+
@click.option("--force", is_flag=True, help="Overwrite an existing config file.")
|
|
289
|
+
def robot_config_command(robot_id: str, config_path: str, force: bool) -> None:
|
|
290
|
+
path = Path(config_path).expanduser()
|
|
291
|
+
if path.exists() and not force:
|
|
292
|
+
raise CliError(
|
|
293
|
+
f"config already exists: {path}. Use --force to overwrite it."
|
|
294
|
+
)
|
|
295
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
path.write_text(ROBOT_TYPES[robot_id].config_template, encoding="utf-8")
|
|
297
|
+
click.echo(f"Robot config written to {path}")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@robot_cli.command("infer")
|
|
301
|
+
@click.option("--api-key", envvar=DexosClient.api_key_env)
|
|
302
|
+
@click.option("--model-id", required=False, default="dm05", help="Model ID to use for inference.")
|
|
303
|
+
@click.option(
|
|
304
|
+
"--robot-config",
|
|
305
|
+
required=True,
|
|
306
|
+
help="Python config path, optionally path.py:robot.",
|
|
307
|
+
)
|
|
308
|
+
@click.option("--prompt", required=True, help="Text prompt sent to the inference API.")
|
|
309
|
+
@click.option(
|
|
310
|
+
"--time-limit",
|
|
311
|
+
default=600.0,
|
|
312
|
+
show_default="600 seconds",
|
|
313
|
+
type=click.FloatRange(min=0.0, min_open=True),
|
|
314
|
+
help="Maximum inference runtime in seconds.",
|
|
315
|
+
)
|
|
316
|
+
def infer_command(
|
|
317
|
+
api_key: Optional[str],
|
|
318
|
+
model_id: str,
|
|
319
|
+
robot_config: str,
|
|
320
|
+
prompt: Optional[str],
|
|
321
|
+
time_limit: float,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Run robot inference with the configured CLI endpoint.
|
|
324
|
+
|
|
325
|
+
:param api_key: API key from ``--api-key`` or ``DEXOS_API_KEY``.
|
|
326
|
+
:param model_id: Model ID to use for inference.
|
|
327
|
+
:param robot_config: Python config path, optionally ``path.py:robot``.
|
|
328
|
+
:param prompt: Text prompt sent to the inference API.
|
|
329
|
+
:param time_limit: Maximum inference runtime in seconds.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
client = DexosClient(api_key=api_key)
|
|
333
|
+
robot = load_robot_config(robot_config)
|
|
334
|
+
should_collect_feedback = False
|
|
335
|
+
interrupted = False
|
|
336
|
+
try:
|
|
337
|
+
robot.start()
|
|
338
|
+
_reset_robot_to_home(robot)
|
|
339
|
+
deadline = time.monotonic() + time_limit
|
|
340
|
+
with _InferenceInterruptHandler() as interrupt_handler:
|
|
341
|
+
while not interrupt_handler.stop_requested:
|
|
342
|
+
if time.monotonic() >= deadline:
|
|
343
|
+
click.echo("time limit reached")
|
|
344
|
+
should_collect_feedback = True
|
|
345
|
+
break
|
|
346
|
+
infer_robot(
|
|
347
|
+
robot,
|
|
348
|
+
client,
|
|
349
|
+
model_id=model_id,
|
|
350
|
+
prompt=prompt,
|
|
351
|
+
)
|
|
352
|
+
time.sleep(1)
|
|
353
|
+
click.echo("working...")
|
|
354
|
+
except KeyboardInterrupt:
|
|
355
|
+
click.echo("interrupted", err=True)
|
|
356
|
+
should_collect_feedback = True
|
|
357
|
+
interrupted = True
|
|
358
|
+
finally:
|
|
359
|
+
if interrupted:
|
|
360
|
+
_reset_robot_to_home(robot, raise_errors=False)
|
|
361
|
+
robot.close()
|
|
362
|
+
if should_collect_feedback:
|
|
363
|
+
_submit_inference_feedback(client)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == "__main__":
|
|
367
|
+
safe_cli()
|