kiteml-cli 0.5.0__tar.gz → 0.7.0__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.
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/PKG-INFO +5 -3
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/README.md +4 -2
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/__init__.py +1 -1
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/evaluations.py +36 -7
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/twin.py +21 -11
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/mcp/server.py +23 -19
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/.gitignore +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/pyproject.toml +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/archive.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/auth.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/__init__.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/__init__.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/augment.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/auth_cmd.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/config_cmd.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/commands/rl.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/cli/main.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/client.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/config.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/mcp/__init__.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/mcp/common.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/output.py +0 -0
- {kiteml_cli-0.5.0 → kiteml_cli-0.7.0}/src/kite_cli/progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: kiteml-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Kite CLI and MCP server: train robot policies with reinforcement learning in simulation, augment LeRobot datasets, and build MuJoCo digital twins, from your terminal or an AI agent.
|
|
5
5
|
Project-URL: Homepage, https://kiteml.com
|
|
6
6
|
Project-URL: Documentation, https://docs.kiteml.com/tools/cli-and-mcp
|
|
@@ -138,7 +138,7 @@ dataset's folder in a public bucket (`s3://bucket/dataset/`).
|
|
|
138
138
|
Build a digital twin (beta, enabled per account):
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
|
-
kite twin create lerobot/svla_so101_pickplace --out ./twins # -> ./twins/twin_.../scene.xml
|
|
141
|
+
kite twin create lerobot/svla_so101_pickplace --robot so101 --out ./twins # -> ./twins/twin_.../scene.xml
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
Evaluate a policy on a Kite SDK spec, on Kite's machines (beta):
|
|
@@ -150,7 +150,9 @@ kite eval run specs/smoke.yaml --out ./runs # -> ./runs/eval_.../scorecard.js
|
|
|
150
150
|
|
|
151
151
|
A file too big to send, such as a checkpoint's weights, can come from the Hugging Face Hub or a public S3 bucket:
|
|
152
152
|
`--hf PATH=OWNER/NAME@COMMIT:FILE` or `--hf PATH=<the file's huggingface.co URL>`, and `--s3 PATH=s3://BUCKET/KEY`,
|
|
153
|
-
PATH being the path the spec names.
|
|
153
|
+
PATH being the path the spec names. A checkpoint of one of your Kite training runs goes as
|
|
154
|
+
`--training-run PATH=trn_...[@STEP]` (its latest without `@STEP`), PATH being the checkpoint folder the spec names:
|
|
155
|
+
Kite reads it from its own storage and publishes nothing.
|
|
154
156
|
|
|
155
157
|
Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
|
|
156
158
|
the full list.
|
|
@@ -96,7 +96,7 @@ dataset's folder in a public bucket (`s3://bucket/dataset/`).
|
|
|
96
96
|
Build a digital twin (beta, enabled per account):
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
kite twin create lerobot/svla_so101_pickplace --out ./twins # -> ./twins/twin_.../scene.xml
|
|
99
|
+
kite twin create lerobot/svla_so101_pickplace --robot so101 --out ./twins # -> ./twins/twin_.../scene.xml
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
Evaluate a policy on a Kite SDK spec, on Kite's machines (beta):
|
|
@@ -108,7 +108,9 @@ kite eval run specs/smoke.yaml --out ./runs # -> ./runs/eval_.../scorecard.js
|
|
|
108
108
|
|
|
109
109
|
A file too big to send, such as a checkpoint's weights, can come from the Hugging Face Hub or a public S3 bucket:
|
|
110
110
|
`--hf PATH=OWNER/NAME@COMMIT:FILE` or `--hf PATH=<the file's huggingface.co URL>`, and `--s3 PATH=s3://BUCKET/KEY`,
|
|
111
|
-
PATH being the path the spec names.
|
|
111
|
+
PATH being the path the spec names. A checkpoint of one of your Kite training runs goes as
|
|
112
|
+
`--training-run PATH=trn_...[@STEP]` (its latest without `@STEP`), PATH being the checkpoint folder the spec names:
|
|
113
|
+
Kite reads it from its own storage and publishes nothing.
|
|
112
114
|
|
|
113
115
|
Every command prints JSON (`{"ok": true, "data": ...}`) so scripts and agents can parse it. Run `kite --help` for
|
|
114
116
|
the full list.
|
|
@@ -17,7 +17,8 @@ SPEC is the file `kite run` takes (kite_spec_version "2"). The command sends its
|
|
|
17
17
|
names (`files_required`), and sends each from this disk: a file or folder Kite ships by its sha256 alone, anything
|
|
18
18
|
else inline (about 15 MB in all). A file on the Hugging Face Hub, such as a checkpoint's weights, goes as a
|
|
19
19
|
reference: `--hf PATH=OWNER/NAME@COMMIT:FILE`, PATH as the spec names it (a folder's file as FOLDER/FILE); an object
|
|
20
|
-
in a public S3 bucket as `--s3 PATH=s3://BUCKET/KEY
|
|
20
|
+
in a public S3 bucket as `--s3 PATH=s3://BUCKET/KEY`; a checkpoint of one of your Kite training runs as
|
|
21
|
+
`--training-run PATH=trn_...[@STEP]`, PATH being the folder the spec names (`policy.asset_dir`).
|
|
21
22
|
Where runs go: --out DIR, else $KITE_EVAL_RUNS_DIR, else ./runs (`~` works). Paths printed are absolute. A run
|
|
22
23
|
directory extracts to <out>/<run_id>/; the SDK's `kite verify <out>/<run_id> --evals DIR` checks it, DIR being the
|
|
23
24
|
evaluators the spec named (the run records where its job read them, not your folder).
|
|
@@ -55,12 +56,16 @@ _BODY_MAX = 24_000_000 # base64 characters in all, under the 32 MiB a request c
|
|
|
55
56
|
_ROUNDS = 64 # validate calls while the files are worked out: one per missing path or unshipped hash, at most
|
|
56
57
|
_HF = re.compile(r"(?P<repo>[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*)@(?P<revision>[0-9a-f]{40})"
|
|
57
58
|
r":(?P<path>.+)")
|
|
59
|
+
_TRN = re.compile(r"(?P<run>trn_[0-9A-HJKMNP-TV-Z]{26})(@(?P<step>[0-9]+))?") # a training run's id, as the API has it
|
|
58
60
|
|
|
59
61
|
_SET_HELP = "FIELD=VALUE, as `kite run --set` (repeatable)"
|
|
60
62
|
_HF_HELP = ("PATH=OWNER/NAME@COMMIT:FILE, or PATH=URL (https://huggingface.co/OWNER/NAME/blob/REVISION/FILE; Kite pins a "
|
|
61
63
|
"branch to its commit): the spec's PATH is that file of a public Hugging Face repo (repeatable)")
|
|
62
64
|
_S3_HELP = ("PATH=s3://BUCKET/KEY (or its https://BUCKET.s3.amazonaws.com/KEY URL): the spec's PATH is that object of a "
|
|
63
65
|
"public S3 bucket, pinned to the object it is when the run is created (repeatable)")
|
|
66
|
+
_TRN_HELP = ("PATH=trn_...[@STEP]: the spec's PATH (a folder, e.g. policy.asset_dir) is the pretrained_model/ folder "
|
|
67
|
+
"of one of your training runs' checkpoints, at STEP or its latest; read from Kite's storage, nothing is "
|
|
68
|
+
"published (repeatable)")
|
|
64
69
|
|
|
65
70
|
|
|
66
71
|
@app.command()
|
|
@@ -69,12 +74,13 @@ def validate(
|
|
|
69
74
|
set_: Optional[List[str]] = typer.Option(None, "--set", help=_SET_HELP),
|
|
70
75
|
hf: Optional[List[str]] = typer.Option(None, "--hf", help=_HF_HELP),
|
|
71
76
|
s3: Optional[List[str]] = typer.Option(None, "--s3", help=_S3_HELP),
|
|
77
|
+
training_run: Optional[List[str]] = typer.Option(None, "--training-run", help=_TRN_HELP),
|
|
72
78
|
kite_version: Optional[str] = typer.Option(None, "--kite-version", help="The Kite release you expect"),
|
|
73
79
|
):
|
|
74
80
|
"""Free dry run of a run: the spec plans, every file it names is sent, and the price. Creates nothing."""
|
|
75
81
|
try:
|
|
76
82
|
client = KiteClient(timeout=_SLOW_S)
|
|
77
|
-
_, validation, sent = request(client, spec, set_ or [],
|
|
83
|
+
_, validation, sent = request(client, spec, set_ or [], remote_refs(hf, s3, training_run), kite_version)
|
|
78
84
|
print_json({"ok": True, "data": validation, "files": sent})
|
|
79
85
|
except KiteAPIError as e:
|
|
80
86
|
print_error(e.message, code=e.code, details=e.details)
|
|
@@ -86,6 +92,7 @@ def run(
|
|
|
86
92
|
set_: Optional[List[str]] = typer.Option(None, "--set", help=_SET_HELP),
|
|
87
93
|
hf: Optional[List[str]] = typer.Option(None, "--hf", help=_HF_HELP),
|
|
88
94
|
s3: Optional[List[str]] = typer.Option(None, "--s3", help=_S3_HELP),
|
|
95
|
+
training_run: Optional[List[str]] = typer.Option(None, "--training-run", help=_TRN_HELP),
|
|
89
96
|
max_minutes: Optional[int] = typer.Option(
|
|
90
97
|
None, "--max-minutes", help="The run's cap, 1-720: what it reserves and the most it can cost "
|
|
91
98
|
"(default 60 on CPU, 120 on an L4)"),
|
|
@@ -106,7 +113,7 @@ def run(
|
|
|
106
113
|
key = idempotency_key or str(uuid.uuid4())
|
|
107
114
|
try:
|
|
108
115
|
client = KiteClient(timeout=_SLOW_S)
|
|
109
|
-
body, _, _ = request(client, spec, set_ or [],
|
|
116
|
+
body, _, _ = request(client, spec, set_ or [], remote_refs(hf, s3, training_run), kite_version)
|
|
110
117
|
options = {k: v for k, v in (("max_minutes", max_minutes), ("no_eval", no_eval),
|
|
111
118
|
("verify_determinism", verify_determinism)) if v}
|
|
112
119
|
body = {**body, **({"options": options} if options else {}), **({"label": label} if label else {})}
|
|
@@ -251,8 +258,8 @@ def resume(run_id: str = typer.Argument(..., help="Evaluation id (eval_...)")):
|
|
|
251
258
|
def request(client: KiteClient, spec: Path, overrides: List[str], remote: Dict[str, dict],
|
|
252
259
|
kite_version: Optional[str] = None) -> Tuple[dict, dict, Dict[str, str]]:
|
|
253
260
|
"""The request body for SPEC with every path it names, the validation that accepted it, and how each path went
|
|
254
|
-
(`sha256`, `inline`, `hf`, `s3`). `remote` maps the paths that are an `hf` file
|
|
255
|
-
reference ({"hf": …} or {"
|
|
261
|
+
(`sha256`, `inline`, `hf`, `s3`, `training_run`). `remote` maps the paths that are an `hf` file, an `s3` object
|
|
262
|
+
or a training run's checkpoint folder to their reference ({"hf": …}, {"s3": …} or {"training_run": …}).
|
|
256
263
|
|
|
257
264
|
The API names what it lacks: `files_required` lists the paths the spec names that the request does not carry,
|
|
258
265
|
and `file_unavailable` on `files.<path>` says a hash Kite does not ship. A path first goes by its hash alone, so
|
|
@@ -263,7 +270,8 @@ def request(client: KiteClient, spec: Path, overrides: List[str], remote: Dict[s
|
|
|
263
270
|
for key, ref in remote.items():
|
|
264
271
|
files[key] = dict(ref)
|
|
265
272
|
local = _local(base, key)
|
|
266
|
-
|
|
273
|
+
# pin the bytes this machine has: the Hub's file or the object must be the same (Kite pins a checkpoint's)
|
|
274
|
+
if local.is_file() and "training_run" not in ref:
|
|
267
275
|
files[key]["sha256"] = _file_sha256(local)
|
|
268
276
|
sent[key] = next(iter(ref))
|
|
269
277
|
body: dict = {"spec_yaml": text, "spec_path": spec_path, "files": files}
|
|
@@ -364,7 +372,8 @@ def _fits(key: str, raw_bytes: int, files: Dict[str, dict], biggest: List[Tuple[
|
|
|
364
372
|
hint = "".join(f", {posixpath.join(key, rel)} {p.stat().st_size >> 20} MB" for rel, p in biggest)
|
|
365
373
|
raise KiteAPIError(413, f"{key} is not a file Kite ships, and it is too big to send inline "
|
|
366
374
|
f"({raw_bytes >> 20} MB{hint}): put its large files on the Hugging Face Hub or in a public "
|
|
367
|
-
"S3 bucket and pass each with --hf PATH=OWNER/NAME@COMMIT:FILE or --s3 PATH=s3://BUCKET/KEY
|
|
375
|
+
"S3 bucket and pass each with --hf PATH=OWNER/NAME@COMMIT:FILE or --s3 PATH=s3://BUCKET/KEY (a "
|
|
376
|
+
"checkpoint Kite trained for you: --training-run PATH=trn_…@STEP)", code="file_too_large",
|
|
368
377
|
details={"path": key})
|
|
369
378
|
|
|
370
379
|
|
|
@@ -433,6 +442,26 @@ def s3_refs(pairs: List[str]) -> Dict[str, dict]:
|
|
|
433
442
|
return refs
|
|
434
443
|
|
|
435
444
|
|
|
445
|
+
def training_run_refs(pairs: List[str]) -> Dict[str, dict]:
|
|
446
|
+
"""PATH=trn_…[@STEP] pairs as {"training_run": id, "step": STEP} references, the step left out for the run's
|
|
447
|
+
latest checkpoint; the API checks the run is the caller's."""
|
|
448
|
+
refs: Dict[str, dict] = {}
|
|
449
|
+
for pair in pairs:
|
|
450
|
+
key, _, ref = pair.partition("=")
|
|
451
|
+
m = _TRN.fullmatch(ref.strip())
|
|
452
|
+
if not key.strip() or not m:
|
|
453
|
+
raise KiteAPIError(400, f"--training-run {pair!r}: expected PATH=trn_…[@STEP], a training run's id "
|
|
454
|
+
"(trn_ and 26 characters) and optionally the step of one of its checkpoints",
|
|
455
|
+
code="parameter_invalid", details={"param": "training_run"})
|
|
456
|
+
refs[key.strip()] = {"training_run": m["run"], **({"step": int(m["step"])} if m["step"] else {})}
|
|
457
|
+
return refs
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def remote_refs(hf: Optional[List[str]], s3: Optional[List[str]], training_run: Optional[List[str]]) -> Dict[str, dict]:
|
|
461
|
+
"""The --hf, --s3 and --training-run pairs as one map of path to reference."""
|
|
462
|
+
return {**hf_refs(hf or []), **s3_refs(s3 or []), **training_run_refs(training_run or [])}
|
|
463
|
+
|
|
464
|
+
|
|
436
465
|
# --- downloads ------------------------------------------------------------------------------------------------------
|
|
437
466
|
|
|
438
467
|
def _page(limit: int, after: Optional[str]) -> dict:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
MuJoCo scene out (about 100 minutes per twin).
|
|
3
3
|
|
|
4
4
|
kite twin validate lerobot/svla_so101_pickplace [--camera side] free: cameras, episodes, time and price
|
|
5
|
+
kite twin validate s3://my-bucket/my_dataset --robot so101 a dataset in a public S3 bucket
|
|
5
6
|
kite twin create lerobot/svla_so101_pickplace --wait --out ./twins -> ./twins/twin_.../scene.xml
|
|
6
7
|
kite twin status twin_01K... [--wait]
|
|
7
8
|
kite twin list [--limit 20] [--after twin_...]
|
|
@@ -10,7 +11,8 @@ MuJoCo scene out (about 100 minutes per twin).
|
|
|
10
11
|
kite twin resume twin_01K...
|
|
11
12
|
kite twin download twin_01K... [--out DIR] [--no-extract]
|
|
12
13
|
|
|
13
|
-
DATASET is a Hugging Face repo id
|
|
14
|
+
DATASET is a Hugging Face repo id or URL, which the API takes as {"source": {"type": "huggingface", "repo_id": DATASET}},
|
|
15
|
+
or an S3 link to the dataset's folder in a public bucket, taken as {"source": {"type": "s3", "uri": DATASET}}.
|
|
14
16
|
Where twins go: --out DIR, else $KITE_TWINS_DIR, else ./twins (`~` works). Paths printed are absolute.
|
|
15
17
|
|
|
16
18
|
A signed storage URL is fetched with a plain HTTP client, never with the Kite key; the API's own archive
|
|
@@ -21,6 +23,7 @@ and unpacked only inside <out>/<twin_id>/.
|
|
|
21
23
|
from __future__ import annotations
|
|
22
24
|
|
|
23
25
|
import os
|
|
26
|
+
import re
|
|
24
27
|
import uuid
|
|
25
28
|
from pathlib import Path, PurePosixPath
|
|
26
29
|
from typing import Optional
|
|
@@ -38,18 +41,21 @@ app = typer.Typer(no_args_is_help=True, help="Digital twins: one robot episode -
|
|
|
38
41
|
_TERMINAL = {"succeeded", "failed", "canceled"} # not canceling: the cancel was accepted, the build has not stopped
|
|
39
42
|
_POLL_S = 30.0
|
|
40
43
|
_SLOW_S = 180.0 # validate/create check the dataset server-side (the API allows the builder 120 s)
|
|
44
|
+
_DATASET_HELP = ("Hugging Face dataset id or URL, e.g. lerobot/svla_so101_pickplace, or an S3 link to the dataset's "
|
|
45
|
+
"folder in a public bucket, s3://BUCKET/PREFIX")
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
@app.command()
|
|
44
49
|
def validate(
|
|
45
|
-
dataset: str = typer.Argument(..., help=
|
|
50
|
+
dataset: str = typer.Argument(..., help=_DATASET_HELP),
|
|
46
51
|
camera: Optional[str] = typer.Option(None, "--camera", help="Substring of the camera that seeds the room"),
|
|
47
52
|
wrist_camera: Optional[str] = typer.Option(None, "--wrist-camera", help="Substring of the wrist camera"),
|
|
53
|
+
robot: Optional[str] = typer.Option(None, "--robot", help="Also check this robot can drive the dataset"),
|
|
48
54
|
):
|
|
49
55
|
"""Free dry run of a create: cameras, episodes, fps, and a time estimate. Creates nothing."""
|
|
50
56
|
try:
|
|
51
57
|
client = KiteClient(timeout=_SLOW_S)
|
|
52
|
-
body = _body(source=
|
|
58
|
+
body = _body(source=twin_source(dataset), camera=camera, wrist_camera=wrist_camera, robot=robot)
|
|
53
59
|
print_json(client.post("/v1/twins/validate", json_data=body))
|
|
54
60
|
except KiteAPIError as e:
|
|
55
61
|
print_error(e.message, code=e.code, details=e.details)
|
|
@@ -57,7 +63,8 @@ def validate(
|
|
|
57
63
|
|
|
58
64
|
@app.command()
|
|
59
65
|
def create(
|
|
60
|
-
dataset: str = typer.Argument(..., help=
|
|
66
|
+
dataset: str = typer.Argument(..., help=_DATASET_HELP),
|
|
67
|
+
robot: str = typer.Option(..., "--robot", help="The robot in the episode: so100, so101, koch, aloha, wx250s, wxai_biarm, piper, xarm7, panda, panda_ee_state, g1_dex3, vega_1p"),
|
|
61
68
|
name: Optional[str] = typer.Option(None, "--name", help="A short name for the twin"),
|
|
62
69
|
task: Optional[str] = typer.Option(None, "--task", help="Which objects to author: the ones the robot manipulates"),
|
|
63
70
|
prompt: Optional[str] = typer.Option(None, "--prompt", help="How to describe the room to the reconstructor"),
|
|
@@ -69,16 +76,16 @@ def create(
|
|
|
69
76
|
idempotency_key: Optional[str] = typer.Option(
|
|
70
77
|
None, "--idempotency-key", help="Safe retries: same key + body -> same twin (default: a generated key)"
|
|
71
78
|
),
|
|
72
|
-
wait: bool = typer.Option(False, "--wait", help="Poll until succeeded | failed | canceled (
|
|
79
|
+
wait: bool = typer.Option(False, "--wait", help="Poll until succeeded | failed | canceled (a few hours)"),
|
|
73
80
|
out: Optional[Path] = typer.Option(
|
|
74
81
|
None, "-o", "--out", help="When it succeeds, download, verify and unpack the scene into DIR (implies --wait); "
|
|
75
82
|
"default location for `kite twin download` is $KITE_TWINS_DIR, else ./twins"
|
|
76
83
|
),
|
|
77
84
|
):
|
|
78
|
-
"""Build a twin from one episode of a LeRobot dataset (queued;
|
|
79
|
-
|
|
85
|
+
"""Build a twin from one episode of a LeRobot dataset and its robot (queued; a few hours; 25,000 tokens, refunded
|
|
86
|
+
if it ends before authoring)."""
|
|
80
87
|
key = idempotency_key or str(uuid.uuid4())
|
|
81
|
-
body = _body(source=
|
|
88
|
+
body = _body(source=twin_source(dataset), robot=robot, name=name, task=task, prompt=prompt, camera=camera,
|
|
82
89
|
wrist_camera=wrist_camera, ceiling_m=ceiling_m)
|
|
83
90
|
try:
|
|
84
91
|
client = KiteClient(timeout=_SLOW_S)
|
|
@@ -177,7 +184,7 @@ def cancel(twin_id: str = typer.Argument(..., help="Twin id (twin_...)")):
|
|
|
177
184
|
@app.command()
|
|
178
185
|
def resume(twin_id: str = typer.Argument(..., help="Twin id (twin_...)")):
|
|
179
186
|
"""Re-queue a failed or canceled twin; only the stages that did not finish run again. Free while it's paid for;
|
|
180
|
-
a twin that was refunded is charged
|
|
187
|
+
a twin that was refunded is charged 25,000 tokens again."""
|
|
181
188
|
try:
|
|
182
189
|
print_json(KiteClient().post(f"/v1/twins/{twin_id}/resume"))
|
|
183
190
|
except KiteAPIError as e:
|
|
@@ -204,8 +211,11 @@ def _body(**fields) -> dict:
|
|
|
204
211
|
return {k: v for k, v in fields.items() if v not in (None, "")}
|
|
205
212
|
|
|
206
213
|
|
|
207
|
-
def
|
|
208
|
-
"""
|
|
214
|
+
def twin_source(dataset: str) -> dict:
|
|
215
|
+
"""DATASET as the API's `source`: an S3 link (s3://BUCKET/PREFIX, or its https://….amazonaws.com URL) is its
|
|
216
|
+
`uri`, anything else a Hugging Face `repo_id`. The API checks either."""
|
|
217
|
+
if re.match(r"(?i)\s*(s3://|https?://[^/]*amazonaws\.com/)", dataset):
|
|
218
|
+
return {"type": "s3", "uri": dataset.strip()}
|
|
209
219
|
return {"type": "huggingface", "repo_id": dataset}
|
|
210
220
|
|
|
211
221
|
|
|
@@ -51,11 +51,6 @@ def _client() -> KiteClient:
|
|
|
51
51
|
return KiteClient(api_url=os.environ.get("KITE_API_URL"), token=token, surface="kite-mcp")
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def _twin_source(dataset: str) -> dict:
|
|
55
|
-
"""The tools take `dataset`, the simplest thing to ask an agent for; the API takes it as a `source`."""
|
|
56
|
-
return {"type": "huggingface", "repo_id": dataset}
|
|
57
|
-
|
|
58
|
-
|
|
59
54
|
def _fetch_command(d: dict, fname: str, client: KiteClient) -> str:
|
|
60
55
|
"""The command that fetches a finished resource's archive. From Kite-Version 2026-10-12 `download.url` is this
|
|
61
56
|
API's path, which needs the key: ask it for the fresh signed link it redirects to, so an agent without the key
|
|
@@ -140,10 +135,10 @@ def _eval_request(args: dict, client: KiteClient) -> tuple[dict, dict | None]:
|
|
|
140
135
|
raise ValueError("`spec` only works with the local (stdio) Kite MCP server; send `spec_yaml` and `files`")
|
|
141
136
|
from pathlib import Path
|
|
142
137
|
|
|
143
|
-
from kite_cli.cli.commands.evaluations import
|
|
138
|
+
from kite_cli.cli.commands.evaluations import remote_refs, request
|
|
144
139
|
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
pairs = {m: [f"{k}={v}" for k, v in (args.get(m) or {}).items()] for m in ("hf", "s3", "training_runs")}
|
|
141
|
+
remote = remote_refs(pairs["hf"], pairs["s3"], pairs["training_runs"])
|
|
147
142
|
body, validation, _ = request(client, Path(args["spec"]), args.get("overrides") or [], remote, args.get("kite_version"))
|
|
148
143
|
return body, validation
|
|
149
144
|
|
|
@@ -204,14 +199,19 @@ _EVAL_SPEC = {
|
|
|
204
199
|
"spec": {"type": "string", "description": "A Kite SDK spec file on this machine, best as an absolute path (the local server only): every path it names is sent from this disk, a file Kite ships by its hash alone"},
|
|
205
200
|
"spec_yaml": {"type": "string", "description": "The spec's text, instead of `spec` (the hosted server): send `spec_path` and `files` with it"},
|
|
206
201
|
"spec_path": {"type": "string", "description": "With spec_yaml: its path in your workspace, e.g. specs/smoke.yaml. Paths in the spec are read relative to it"},
|
|
207
|
-
"files": {"type": "object", "description": "With spec_yaml: each path the spec names, as it names it, to where its bytes come from: {\"kind\": \"directory\", \"shipped\": \"evals\"} (a path Kite ships), {\"sha256\", \"kind\"} (a shipped file by hash), {\"text\"} or {\"base64\"} (a folder as a .tar), or {\"hf\": {\"repo\", \"revision\" (a full commit), \"path\"}} or {\"hf\": \"<the file's huggingface.co URL>\"} (a branch is pinned to its commit), or {\"s3\": \"s3://BUCKET/KEY\"} (an object in a public bucket, pinned to its ETag). A missing one is files_required, listing each"},
|
|
202
|
+
"files": {"type": "object", "description": "With spec_yaml: each path the spec names, as it names it, to where its bytes come from: {\"kind\": \"directory\", \"shipped\": \"evals\"} (a path Kite ships), {\"sha256\", \"kind\"} (a shipped file by hash), {\"text\"} or {\"base64\"} (a folder as a .tar), or {\"hf\": {\"repo\", \"revision\" (a full commit), \"path\"}} or {\"hf\": \"<the file's huggingface.co URL>\"} (a branch is pinned to its commit), or {\"s3\": \"s3://BUCKET/KEY\"} (an object in a public bucket, pinned to its ETag), or {\"training_run\": \"trn_...\", \"step\"} (a folder: the pretrained_model/ of one of your training runs' checkpoints, step left out for its latest; nothing is published). A missing one is files_required, listing each"},
|
|
208
203
|
"overrides": {"type": "array", "items": {"type": "string"}, "description": "FIELD=VALUE pairs, as `kite run --set`, e.g. episodes.count=20. Optional"},
|
|
209
204
|
"hf": {"type": "object", "description": "With spec: {PATH: \"OWNER/NAME@COMMIT:FILE\" or the file's huggingface.co URL}, the paths that are a public Hugging Face file (a checkpoint's weights). Optional"},
|
|
210
205
|
"s3": {"type": "object", "description": "With spec: {PATH: \"s3://BUCKET/KEY\"}, the paths that are an object in a public S3 bucket. Optional"},
|
|
206
|
+
"training_runs": {"type": "object", "description": "With spec: {PATH: \"trn_...\" or \"trn_...@STEP\"}, the folders (policy.asset_dir) that are the pretrained_model/ of one of your training runs' checkpoints, its latest without @STEP; read from Kite's storage, nothing is published. Optional"},
|
|
211
207
|
"kite_version": {"type": "string", "description": "The Kite release you expect; refused if it is not the one served. Optional"},
|
|
212
208
|
}
|
|
213
209
|
_EVAL_ID = {"run_id": {"type": "string", "description": "Evaluation id (eval_...)"}}
|
|
214
210
|
|
|
211
|
+
# The twin tools take `dataset`, the simplest thing to ask an agent for, and send it as the API's `source`.
|
|
212
|
+
_TWIN_DATASET = ("HF dataset id or URL, e.g. lerobot/svla_so101_pickplace (the API's source.repo_id), or an S3 link to "
|
|
213
|
+
"the dataset's folder in a public bucket, s3://BUCKET/PREFIX (source.uri)")
|
|
214
|
+
|
|
215
215
|
# ---------------------------------------------------------------------------
|
|
216
216
|
# Tool definitions — every tool has title + annotations per Claude directory
|
|
217
217
|
# ---------------------------------------------------------------------------
|
|
@@ -265,20 +265,22 @@ TOOLS = [
|
|
|
265
265
|
|
|
266
266
|
# Digital twins (public /v1 API): one robot episode -> an interactable MuJoCo scene, ~100 min each
|
|
267
267
|
Tool(name="kite_twin_validate", title="Validate Digital Twin Dataset",
|
|
268
|
-
description="Free check before building a twin: confirms a
|
|
268
|
+
description="Free check before building a twin: confirms a LeRobot dataset (on HuggingFace, or in a public S3 bucket) resolves and has camera video, says which camera will seed the room, and returns the episodes, fps, typical duration and price (estimate.tokens). Creates nothing. Call this first; errors name what to fix (dataset_not_found, dataset_private for an S3 bucket that is not public, camera_not_found with the available cameras, no_suitable_camera).",
|
|
269
269
|
inputSchema={"type": "object", "properties": {
|
|
270
|
-
"dataset": {"type": "string", "description":
|
|
271
|
-
"camera": {"type": "string", "description": "Substring of the camera that should seed the room. Optional"}
|
|
270
|
+
"dataset": {"type": "string", "description": _TWIN_DATASET},
|
|
271
|
+
"camera": {"type": "string", "description": "Substring of the camera that should seed the room. Optional"},
|
|
272
|
+
"robot": {"type": "string", "description": "Also check this robot can drive the dataset (see kite_twin_create). Optional"}},
|
|
272
273
|
"required": ["dataset"]}, annotations=_READ),
|
|
273
274
|
Tool(name="kite_twin_create", title="Create Digital Twin",
|
|
274
|
-
description="Build an interactable MuJoCo digital twin from one episode of a HuggingFace
|
|
275
|
+
description="Build an interactable MuJoCo digital twin from one episode of a LeRobot dataset, on HuggingFace or in a public S3 bucket: the room is reconstructed, the manipulated objects are authored with working joints, and every stage is reviewed by measurement. `dataset` and `robot` are required; the twin reports the dataset back as source.repo_id (source.uri for S3). Takes a few hours and costs 25,000 tokens, charged now and refunded in full if the build ends before authoring begins (insufficient_tokens when the balance is short); returns a queued twin (id twin_...) to poll with kite_twin_status.",
|
|
275
276
|
inputSchema={"type": "object", "properties": {
|
|
276
|
-
"dataset": {"type": "string", "description":
|
|
277
|
+
"dataset": {"type": "string", "description": _TWIN_DATASET},
|
|
278
|
+
"robot": {"type": "string", "description": "The robot in the episode: so100, so101, koch, aloha, wx250s, wxai_biarm, piper, xarm7, panda, panda_ee_state, g1_dex3 or vega_1p (robot_mismatch lists the ones that fit the dataset)"},
|
|
277
279
|
"name": {"type": "string", "description": "Short name for the twin. Optional"},
|
|
278
280
|
"task": {"type": "string", "description": "Which objects to author (the things the robot manipulates). Optional"},
|
|
279
281
|
"camera": {"type": "string", "description": "Substring of the camera that seeds the room. Optional"},
|
|
280
282
|
"idempotency_key": {"type": "string", "description": "Reuse the same key when retrying, so a retry never builds twice. Optional"}},
|
|
281
|
-
"required": ["dataset"]}, annotations=_WRITE),
|
|
283
|
+
"required": ["dataset", "robot"]}, annotations=_WRITE),
|
|
282
284
|
Tool(name="kite_twin_status", title="Get Digital Twin Status",
|
|
283
285
|
description="Get a twin's status (queued/processing/canceling/succeeded/failed/canceled), stage, progress, each finished stage's measurement, and a `next` instruction. canceling means a cancel was accepted but the build has not stopped yet; it becomes canceled. When succeeded, `download.url` is a direct link to the whole scene as one .tar.gz and `next` is the exact command to fetch and extract it (the folder contains scene.xml for MuJoCo).",
|
|
284
286
|
inputSchema={"type": "object", "properties": {
|
|
@@ -294,7 +296,7 @@ TOOLS = [
|
|
|
294
296
|
"twin_id": {"type": "string", "description": "Twin id (twin_...)"}},
|
|
295
297
|
"required": ["twin_id"]}, annotations=_DESTRUCTIVE),
|
|
296
298
|
Tool(name="kite_twin_resume", title="Resume Digital Twin",
|
|
297
|
-
description="Re-queue a failed or canceled twin. Every stage it finished is cached, so only what did not finish runs again. Free while the twin is paid for; one that was refunded (it
|
|
299
|
+
description="Re-queue a failed or canceled twin. Every stage it finished is cached, so only what did not finish runs again. Free while the twin is paid for; one that was refunded (it ended before authoring) is charged 25,000 tokens again.",
|
|
298
300
|
inputSchema={"type": "object", "properties": {
|
|
299
301
|
"twin_id": {"type": "string", "description": "Twin id (twin_...)"}},
|
|
300
302
|
"required": ["twin_id"]}, annotations=_WRITE),
|
|
@@ -464,12 +466,14 @@ async def _dispatch(name: str, args: dict, client: KiteClient) -> list[TextConte
|
|
|
464
466
|
return _ok(client.post(f"/v1/augmentations/{args['aug_id']}/cancel"))
|
|
465
467
|
|
|
466
468
|
# --- Digital twins (public /v1 API) ---
|
|
469
|
+
from kite_cli.cli.commands.twin import twin_source
|
|
470
|
+
|
|
467
471
|
if name == "kite_twin_validate":
|
|
468
|
-
body = {"source":
|
|
472
|
+
body = {"source": twin_source(args["dataset"]), **{k: args[k] for k in ("camera", "robot") if args.get(k)}}
|
|
469
473
|
return _ok(client.post("/v1/twins/validate", json_data=body))
|
|
470
474
|
if name == "kite_twin_create":
|
|
471
|
-
body = {"source":
|
|
472
|
-
**{k: args[k] for k in ("name", "task", "camera") if args.get(k)}}
|
|
475
|
+
body = {"source": twin_source(args["dataset"]),
|
|
476
|
+
**{k: args[k] for k in ("robot", "name", "task", "camera") if args.get(k)}}
|
|
473
477
|
headers = {"Idempotency-Key": args["idempotency_key"]} if args.get("idempotency_key") else None
|
|
474
478
|
return _ok(_twin_view(client.post("/v1/twins", json_data=body, extra_headers=headers), client))
|
|
475
479
|
if name == "kite_twin_status":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|