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 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()