openral-cli 0.1.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.
- openral_cli/__init__.py +6 -0
- openral_cli/_hf_publish.py +125 -0
- openral_cli/_rskill_doc_validator.py +562 -0
- openral_cli/_rskill_intel.py +334 -0
- openral_cli/_rskill_readme.py +217 -0
- openral_cli/_rskill_scaffolder.py +267 -0
- openral_cli/autodetect.py +464 -0
- openral_cli/check.py +397 -0
- openral_cli/collision.py +369 -0
- openral_cli/dataset.py +438 -0
- openral_cli/deploy_sim.py +2664 -0
- openral_cli/install.py +422 -0
- openral_cli/main.py +4108 -0
- openral_cli/prompt.py +128 -0
- openral_cli/py.typed +0 -0
- openral_cli/robot.py +301 -0
- openral_cli-0.1.0.dist-info/METADATA +33 -0
- openral_cli-0.1.0.dist-info/RECORD +20 -0
- openral_cli-0.1.0.dist-info/WHEEL +4 -0
- openral_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Scaffolder for new rSkill packages — backs ``ral skill new``.
|
|
2
|
+
|
|
3
|
+
Copies ``rskills/template/`` to a target directory and rewrites a small,
|
|
4
|
+
fixed set of placeholders (manifest ``name`` / ``license`` /
|
|
5
|
+
``embodiment_tags``), then re-validates the result through
|
|
6
|
+
`RSkillManifest.from_yaml` and `rSkill.from_yaml` so any
|
|
7
|
+
malformed scaffold fails at scaffold-time rather than lazily on first
|
|
8
|
+
load.
|
|
9
|
+
|
|
10
|
+
Used by:
|
|
11
|
+
|
|
12
|
+
- ``ral skill new <id>`` (Typer command in :mod:`openral_cli.main`).
|
|
13
|
+
- ``tools/rskill_scaffolder.py`` (standalone argparse wrapper).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import shutil
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import structlog
|
|
22
|
+
import yaml
|
|
23
|
+
from openral_core.exceptions import ROSConfigError
|
|
24
|
+
from openral_core.schemas import (
|
|
25
|
+
EmbodimentTag,
|
|
26
|
+
RSkillLicensePosture,
|
|
27
|
+
RSkillManifest,
|
|
28
|
+
)
|
|
29
|
+
from openral_rskill.loader import rSkill
|
|
30
|
+
from pydantic import ValidationError
|
|
31
|
+
|
|
32
|
+
from openral_cli._rskill_intel import RSkillFamily, RSkillPatch, family_defaults
|
|
33
|
+
|
|
34
|
+
log = structlog.get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
_TEMPLATE_DIR_NAME = "template"
|
|
37
|
+
_MANIFEST_FILENAME = "rskill.yaml"
|
|
38
|
+
_README_FILENAME = "README.md"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _default_template_dir() -> Path:
|
|
42
|
+
"""Resolve the in-tree ``rskills/template/`` directory.
|
|
43
|
+
|
|
44
|
+
Walks upward from this file's location until it finds a ``rskills/``
|
|
45
|
+
sibling, then returns ``rskills/template``. This makes the helper
|
|
46
|
+
work both when invoked from inside the installed CLI wheel and when
|
|
47
|
+
invoked via ``tools/rskill_scaffolder.py`` against an editable
|
|
48
|
+
workspace.
|
|
49
|
+
"""
|
|
50
|
+
for parent in Path(__file__).resolve().parents:
|
|
51
|
+
candidate = parent / "rskills" / _TEMPLATE_DIR_NAME
|
|
52
|
+
if candidate.is_dir():
|
|
53
|
+
return candidate
|
|
54
|
+
raise ROSConfigError(
|
|
55
|
+
"Could not locate rskills/template/ relative to "
|
|
56
|
+
f"{Path(__file__).resolve()}; pass template_dir= explicitly."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def scaffold_rskill(
|
|
61
|
+
rskill_id: str,
|
|
62
|
+
*,
|
|
63
|
+
out_dir: Path,
|
|
64
|
+
owner: str,
|
|
65
|
+
license_: RSkillLicensePosture,
|
|
66
|
+
embodiment_tag: EmbodimentTag,
|
|
67
|
+
family: RSkillFamily | None = None,
|
|
68
|
+
patch: RSkillPatch | None = None,
|
|
69
|
+
template_dir: Path | None = None,
|
|
70
|
+
overwrite: bool = False,
|
|
71
|
+
) -> Path:
|
|
72
|
+
"""Scaffold a new local rSkill package from ``rskills/template/``.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
rskill_id: Local skill id, convention ``<policy>-<task>`` (e.g.
|
|
76
|
+
``pi05-pick-cube``). Becomes the ``<repo>`` segment of the
|
|
77
|
+
generated manifest ``name`` field.
|
|
78
|
+
out_dir: Destination directory. The scaffold lands here directly
|
|
79
|
+
(no extra ``rskill_id`` segment is appended).
|
|
80
|
+
owner: HF Hub owner segment for the manifest ``name`` field
|
|
81
|
+
(e.g. ``"your-org"``).
|
|
82
|
+
license_: License posture to write into the manifest.
|
|
83
|
+
embodiment_tag: Canonical embodiment tag to declare in
|
|
84
|
+
``embodiment_tags``.
|
|
85
|
+
family: Optional :data:`RSkillFamily`. When set,
|
|
86
|
+
`openral_cli._rskill_intel.family_defaults` provides
|
|
87
|
+
the manifest baseline (``model_family``, ``chunk_size``,
|
|
88
|
+
``quantization``, latency budget, …) so a fresh ACT scaffold
|
|
89
|
+
doesn't ship pi0.5 numbers. ``None`` leaves the template
|
|
90
|
+
(currently pi0.5-shaped) as the baseline.
|
|
91
|
+
patch: Optional manifest patch to overlay on top of the family
|
|
92
|
+
defaults — typically built by
|
|
93
|
+
`openral_cli._rskill_intel.introspect_hf` from a
|
|
94
|
+
checkpoint's ``config.json``. Wins over both ``family``
|
|
95
|
+
defaults and template values, but loses to the rename /
|
|
96
|
+
license rewrite.
|
|
97
|
+
template_dir: Override the in-tree template location. Defaults
|
|
98
|
+
to ``rskills/template/`` relative to this file.
|
|
99
|
+
overwrite: If ``True``, an existing ``out_dir`` is removed
|
|
100
|
+
before scaffolding. If ``False`` (default), a pre-existing
|
|
101
|
+
``out_dir`` raises `ROSConfigError`.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The resolved ``out_dir`` path.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ROSConfigError: ``out_dir`` already exists and ``overwrite`` is
|
|
108
|
+
``False``; or the template directory cannot be located; or
|
|
109
|
+
the generated manifest fails Pydantic / loader validation
|
|
110
|
+
(a partial scaffold is removed before re-raising).
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> # from openral_cli._rskill_scaffolder import scaffold_rskill
|
|
114
|
+
>>> # scaffold_rskill(
|
|
115
|
+
>>> # "pi05-pick-cube",
|
|
116
|
+
>>> # out_dir=Path("rskills/pi05-pick-cube"),
|
|
117
|
+
>>> # owner="your-org",
|
|
118
|
+
>>> # license_=RSkillLicensePosture.APACHE_2_0,
|
|
119
|
+
>>> # embodiment_tag="franka_panda",
|
|
120
|
+
>>> # )
|
|
121
|
+
"""
|
|
122
|
+
resolved_out = out_dir.resolve()
|
|
123
|
+
src = (template_dir or _default_template_dir()).resolve()
|
|
124
|
+
|
|
125
|
+
if not (src / _MANIFEST_FILENAME).is_file():
|
|
126
|
+
raise ROSConfigError(f"template directory {src} is missing {_MANIFEST_FILENAME}")
|
|
127
|
+
|
|
128
|
+
if resolved_out.exists():
|
|
129
|
+
if not overwrite:
|
|
130
|
+
raise ROSConfigError(
|
|
131
|
+
f"refusing to overwrite existing path {resolved_out}; "
|
|
132
|
+
"pass overwrite=True (or --overwrite on the CLI) to replace it."
|
|
133
|
+
)
|
|
134
|
+
shutil.rmtree(resolved_out)
|
|
135
|
+
|
|
136
|
+
shutil.copytree(src, resolved_out)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
_rewrite_manifest(
|
|
140
|
+
resolved_out / _MANIFEST_FILENAME,
|
|
141
|
+
rskill_id=rskill_id,
|
|
142
|
+
owner=owner,
|
|
143
|
+
license_=license_,
|
|
144
|
+
embodiment_tag=embodiment_tag,
|
|
145
|
+
family=family,
|
|
146
|
+
patch=patch,
|
|
147
|
+
)
|
|
148
|
+
_rewrite_readme(
|
|
149
|
+
resolved_out / _README_FILENAME,
|
|
150
|
+
rskill_id=rskill_id,
|
|
151
|
+
owner=owner,
|
|
152
|
+
)
|
|
153
|
+
_validate_scaffold(resolved_out)
|
|
154
|
+
except (ROSConfigError, ValidationError):
|
|
155
|
+
# Clean up the partial scaffold so a re-run doesn't trip the
|
|
156
|
+
# "out_dir already exists" guard above.
|
|
157
|
+
shutil.rmtree(resolved_out, ignore_errors=True)
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
log.info(
|
|
161
|
+
"rskill_scaffolder.created",
|
|
162
|
+
rskill_id=rskill_id,
|
|
163
|
+
out_dir=str(resolved_out),
|
|
164
|
+
owner=owner,
|
|
165
|
+
license=license_.value,
|
|
166
|
+
embodiment_tag=embodiment_tag,
|
|
167
|
+
family=family,
|
|
168
|
+
from_hf=(patch or {}).get("weights_uri"),
|
|
169
|
+
)
|
|
170
|
+
return resolved_out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _rewrite_manifest(
|
|
174
|
+
manifest_path: Path,
|
|
175
|
+
*,
|
|
176
|
+
rskill_id: str,
|
|
177
|
+
owner: str,
|
|
178
|
+
license_: RSkillLicensePosture,
|
|
179
|
+
embodiment_tag: EmbodimentTag,
|
|
180
|
+
family: RSkillFamily | None,
|
|
181
|
+
patch: RSkillPatch | None,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Rewrite the scaffolded fields in ``rskill.yaml`` in place.
|
|
184
|
+
|
|
185
|
+
Application order (lowest → highest precedence):
|
|
186
|
+
|
|
187
|
+
1. The on-disk template values (currently a pi0.5-shaped baseline).
|
|
188
|
+
2. Family-aware defaults from
|
|
189
|
+
`openral_cli._rskill_intel.family_defaults` when
|
|
190
|
+
``family`` is set — overrides ``model_family`` /
|
|
191
|
+
``chunk_size`` / ``quantization`` / latency budget / etc.
|
|
192
|
+
3. The explicit ``patch`` (typically from ``--from-hf``
|
|
193
|
+
introspection) — overrides family defaults with checkpoint-side
|
|
194
|
+
facts (real chunk_size, real image-feature names, real state dim).
|
|
195
|
+
4. CLI-driven rename / license rewrite — always authoritative.
|
|
196
|
+
|
|
197
|
+
Round-trips through ``yaml.safe_load`` → mutate → ``yaml.safe_dump``
|
|
198
|
+
so inline template comments are stripped (documented in the
|
|
199
|
+
template header).
|
|
200
|
+
"""
|
|
201
|
+
raw = yaml.safe_load(manifest_path.read_text(encoding="utf-8"))
|
|
202
|
+
if not isinstance(raw, dict):
|
|
203
|
+
raise ROSConfigError(f"template manifest {manifest_path} did not parse as a mapping")
|
|
204
|
+
|
|
205
|
+
# Layer (2): family defaults.
|
|
206
|
+
if family is not None:
|
|
207
|
+
_apply_patch(raw, dict(family_defaults(family)))
|
|
208
|
+
|
|
209
|
+
# Layer (3): introspection patch.
|
|
210
|
+
if patch:
|
|
211
|
+
_apply_patch(raw, dict(patch))
|
|
212
|
+
|
|
213
|
+
# Layer (4): the always-authoritative CLI rewrite. Re-apply *after*
|
|
214
|
+
# the patch so a stale ``weights_uri`` left in the patch (e.g.
|
|
215
|
+
# placeholder template URL) never wins.
|
|
216
|
+
raw["name"] = f"{owner}/rskill-{rskill_id}"
|
|
217
|
+
raw["license"] = license_.value
|
|
218
|
+
raw["embodiment_tags"] = [embodiment_tag]
|
|
219
|
+
if not raw.get("weights_uri", "").startswith("hf://") or "TEMPLATE" in str(raw["weights_uri"]):
|
|
220
|
+
raw["weights_uri"] = f"hf://{owner}/{rskill_id}"
|
|
221
|
+
if "source_repo" in raw and (
|
|
222
|
+
not str(raw["source_repo"]).startswith("hf://") or "TEMPLATE" in str(raw["source_repo"])
|
|
223
|
+
):
|
|
224
|
+
raw["source_repo"] = f"hf://{owner}/{rskill_id}"
|
|
225
|
+
|
|
226
|
+
manifest_path.write_text(
|
|
227
|
+
yaml.safe_dump(raw, sort_keys=False, default_flow_style=False),
|
|
228
|
+
encoding="utf-8",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _apply_patch(raw: dict[str, object], patch: dict[str, object]) -> None:
|
|
233
|
+
"""Overlay ``patch`` keys onto ``raw``, dropping keys explicitly set to ``None``.
|
|
234
|
+
|
|
235
|
+
Setting a manifest key to ``None`` in a patch means "remove this
|
|
236
|
+
optional block from the template" (e.g. the ACT family clears
|
|
237
|
+
``min_vram_gb`` / ``n_action_steps`` since neither applies). Any
|
|
238
|
+
other value (including empty dicts / lists) overwrites in place.
|
|
239
|
+
"""
|
|
240
|
+
for key, value in patch.items():
|
|
241
|
+
if value is None:
|
|
242
|
+
raw.pop(key, None)
|
|
243
|
+
else:
|
|
244
|
+
raw[key] = value
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _rewrite_readme(readme_path: Path, *, rskill_id: str, owner: str) -> None:
|
|
248
|
+
"""Replace ``TEMPLATE_ORG`` / ``TEMPLATE_ID`` sentinels in the README."""
|
|
249
|
+
if not readme_path.is_file():
|
|
250
|
+
return
|
|
251
|
+
text = readme_path.read_text(encoding="utf-8")
|
|
252
|
+
text = text.replace("TEMPLATE_ORG/rskill-TEMPLATE_ID", f"{owner}/rskill-{rskill_id}")
|
|
253
|
+
text = text.replace("TEMPLATE_ORG", owner)
|
|
254
|
+
text = text.replace("TEMPLATE_ID", rskill_id)
|
|
255
|
+
readme_path.write_text(text, encoding="utf-8")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _validate_scaffold(scaffold_dir: Path) -> None:
|
|
259
|
+
"""Re-validate the generated manifest through the full loader path."""
|
|
260
|
+
manifest_path = scaffold_dir / _MANIFEST_FILENAME
|
|
261
|
+
# Pydantic-level validation first — gives the cleanest error message
|
|
262
|
+
# when the rewrite produced an invalid YAML shape.
|
|
263
|
+
RSkillManifest.from_yaml(str(manifest_path))
|
|
264
|
+
# Full rSkill loader path — runs the license guard + walks eval/*.json.
|
|
265
|
+
# An empty eval/ directory is OK (loader's _validate_eval_jsons
|
|
266
|
+
# early-returns on an empty glob).
|
|
267
|
+
rSkill.from_yaml(manifest_path)
|