sequence-ai 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.
- sequence_ai/__init__.py +192 -0
- sequence_ai/_config.py +72 -0
- sequence_ai/_policy.py +603 -0
- sequence_ai/_transport.py +201 -0
- sequence_ai/cli.py +250 -0
- sequence_ai/errors.py +96 -0
- sequence_ai/py.typed +0 -0
- sequence_ai/types.py +221 -0
- sequence_ai-0.1.0.dist-info/METADATA +267 -0
- sequence_ai-0.1.0.dist-info/RECORD +13 -0
- sequence_ai-0.1.0.dist-info/WHEEL +4 -0
- sequence_ai-0.1.0.dist-info/entry_points.txt +3 -0
- sequence_ai-0.1.0.dist-info/licenses/LICENSE +202 -0
sequence_ai/__init__.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sequence_ai — the Python client for the Sequences robot inference API.
|
|
3
|
+
|
|
4
|
+
── In one sentence ───────────────────────────────────────────────────
|
|
5
|
+
`connect()` opens one HTTP connection and hands back a `Policy`; `run()` drives a bounded
|
|
6
|
+
control loop against it, keeping the connection open and the action buffer full for the life
|
|
7
|
+
of the `with` block.
|
|
8
|
+
|
|
9
|
+
import sequence_ai
|
|
10
|
+
|
|
11
|
+
with sequence_ai.connect(model="pi05-droid") as policy:
|
|
12
|
+
out = policy.run(
|
|
13
|
+
observe=robot.read_observation,
|
|
14
|
+
act=robot.apply_action,
|
|
15
|
+
validate=robot.is_safe,
|
|
16
|
+
hold=robot.hold,
|
|
17
|
+
max_actions=250,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
── What it does for you ──────────────────────────────────────────────
|
|
21
|
+
Four things, each of them work a loop written straight against the HTTP endpoints has to get
|
|
22
|
+
right before it behaves properly on a robot:
|
|
23
|
+
|
|
24
|
+
keeps the connection open 184.9 ms of handshake otherwise paid on every call,
|
|
25
|
+
60% of a bare request (see _transport.py)
|
|
26
|
+
refills before the buffer a synchronous refill stops the arm once per chunk, for a
|
|
27
|
+
empties full round trip — 39% duty cycle on the fastest models
|
|
28
|
+
(see _policy.run)
|
|
29
|
+
classifies errors `warming` with a retry_after_s is "wait 118 s"; a plain
|
|
30
|
+
Unavailable is "stop" — opposite instructions (see errors)
|
|
31
|
+
one driver per buffer two loops on one handle interleave and both report
|
|
32
|
+
success (see Policy._driving)
|
|
33
|
+
|
|
34
|
+
None of the four is guesswork. Each is a defect that existed in this client, was measured, and
|
|
35
|
+
was fixed; the cited modules carry the measurements.
|
|
36
|
+
|
|
37
|
+
── If your controller is not Python ──────────────────────────────────
|
|
38
|
+
Everything here is a plain HTTP call to a documented, public endpoint, and it stays that way.
|
|
39
|
+
There is no private control plane and nothing this package can reach that you cannot.
|
|
40
|
+
|
|
41
|
+
curl https://api.generalsequences.com/v1/act \\
|
|
42
|
+
-H "Authorization: Bearer $SEQUENCES_API_KEY" \\
|
|
43
|
+
-d '{"model":"...","observation":{...}}'
|
|
44
|
+
|
|
45
|
+
Keeping that door open is deliberate: a vendor SDK that is the only supported way in gives the
|
|
46
|
+
vendor a lock and excludes every team on a language it does not ship, and robots are not a
|
|
47
|
+
Python-only industry — the controller is very often C++. But it is a door, not the front
|
|
48
|
+
entrance. The four items above become your client's problem, and each is documented precisely
|
|
49
|
+
enough to reimplement at https://generalsequences.com/docs/python/.
|
|
50
|
+
|
|
51
|
+
── Safety ────────────────────────────────────────────────────────────
|
|
52
|
+
An action returned by any model is model output, not a safe robot command. This library does
|
|
53
|
+
not check joint limits, reachability, collisions, velocity, or anything else. Bounds
|
|
54
|
+
checking, a watchdog and an e-stop belong between this library and your motors, and
|
|
55
|
+
`run(validate=..., hold=...)` is where to attach them.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
from typing import Any
|
|
61
|
+
|
|
62
|
+
from ._policy import Policy
|
|
63
|
+
from ._transport import DEFAULT_BASE_URL, Transport
|
|
64
|
+
from .errors import (
|
|
65
|
+
AuthError,
|
|
66
|
+
ChunkExhausted,
|
|
67
|
+
InvalidRequest,
|
|
68
|
+
OutOfCredit,
|
|
69
|
+
SequencesError,
|
|
70
|
+
Unavailable,
|
|
71
|
+
)
|
|
72
|
+
from .types import (
|
|
73
|
+
ActionChunk,
|
|
74
|
+
ImageFrame,
|
|
75
|
+
Model,
|
|
76
|
+
Observation,
|
|
77
|
+
Prediction,
|
|
78
|
+
Proprioception,
|
|
79
|
+
RunOutcome,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"connect",
|
|
84
|
+
"models",
|
|
85
|
+
"Policy",
|
|
86
|
+
"Observation",
|
|
87
|
+
"ImageFrame",
|
|
88
|
+
"Proprioception",
|
|
89
|
+
"ActionChunk",
|
|
90
|
+
"Prediction",
|
|
91
|
+
"RunOutcome",
|
|
92
|
+
"Model",
|
|
93
|
+
"SequencesError",
|
|
94
|
+
"AuthError",
|
|
95
|
+
"OutOfCredit",
|
|
96
|
+
"InvalidRequest",
|
|
97
|
+
"Unavailable",
|
|
98
|
+
"ChunkExhausted",
|
|
99
|
+
"DEFAULT_BASE_URL",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
__version__ = "0.1.0"
|
|
103
|
+
|
|
104
|
+
# Short ids are accepted as well as full ones, because `pi05-droid` is what people actually
|
|
105
|
+
# type and `accounts/sequences/models/pi05-droid` is what the API expects. Expanding it here
|
|
106
|
+
# rather than making the caller remember the prefix.
|
|
107
|
+
_PREFIX = "accounts/sequences/models/"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def connect(
|
|
111
|
+
model: str,
|
|
112
|
+
*,
|
|
113
|
+
api_key: str | None = None,
|
|
114
|
+
base_url: str | None = None,
|
|
115
|
+
control_hz: float | None = None,
|
|
116
|
+
timeout_s: float = 30.0,
|
|
117
|
+
) -> Policy:
|
|
118
|
+
"""
|
|
119
|
+
Open a connection to one model.
|
|
120
|
+
|
|
121
|
+
model either `pi05-droid` or the full `accounts/sequences/models/pi05-droid`
|
|
122
|
+
api_key defaults to $SEQUENCES_API_KEY
|
|
123
|
+
base_url defaults to $SEQUENCES_BASE_URL, then the production gateway
|
|
124
|
+
control_hz play actions out at this rate instead of the rate the chunk declares
|
|
125
|
+
timeout_s per-request timeout
|
|
126
|
+
|
|
127
|
+
Use it as a context manager. The connection is what makes this worth calling at all, and
|
|
128
|
+
a `Policy` that is never closed leaks a socket:
|
|
129
|
+
|
|
130
|
+
with sequence_ai.connect(model="pi05-droid") as policy:
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
Nothing is sent at connect time — no session is opened on the server and nothing is
|
|
134
|
+
billed. The first request happens on the first `predict()`. This is deliberately unlike
|
|
135
|
+
a session-based API where opening the handle already costs money; here the handle is
|
|
136
|
+
purely local.
|
|
137
|
+
"""
|
|
138
|
+
return Policy(
|
|
139
|
+
Transport(api_key=api_key, base_url=base_url, timeout_s=timeout_s),
|
|
140
|
+
model=_full_id(model),
|
|
141
|
+
control_hz=control_hz,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def models(
|
|
146
|
+
*,
|
|
147
|
+
endpoint: str | None = None,
|
|
148
|
+
api_key: str | None = None,
|
|
149
|
+
base_url: str | None = None,
|
|
150
|
+
) -> list[Model]:
|
|
151
|
+
"""
|
|
152
|
+
The live catalogue. `endpoint="act"` for action models, `"chat"` for vision-language.
|
|
153
|
+
|
|
154
|
+
Fetched from the server every call rather than shipped as a constant in this package: a
|
|
155
|
+
hard-coded list goes stale the moment a model is added, and a client that disagrees with
|
|
156
|
+
the server about what exists is worse than one that has to make a request to find out.
|
|
157
|
+
"""
|
|
158
|
+
# No key required: the catalogue is a public endpoint, and making people sign up
|
|
159
|
+
# before they can see what is on offer is a reason to close the tab.
|
|
160
|
+
t = Transport(api_key=api_key, base_url=base_url, require_key=False)
|
|
161
|
+
try:
|
|
162
|
+
path = "/v1/models" + (f"?endpoint={endpoint}" if endpoint else "")
|
|
163
|
+
body = t.get(path)
|
|
164
|
+
return [_to_model(d) for d in body.get("data", [])]
|
|
165
|
+
finally:
|
|
166
|
+
t.close()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _full_id(model: str) -> str:
|
|
170
|
+
return model if "/" in model else _PREFIX + model
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _to_model(d: dict[str, Any]) -> Model:
|
|
174
|
+
"""
|
|
175
|
+
Map a catalogue row, keeping the untouched dict in `.raw`.
|
|
176
|
+
|
|
177
|
+
Only the fields a caller needs to *construct a request* are lifted into attributes; the
|
|
178
|
+
rest — pricing, licence, latency, tags — stay in `.raw`. Lifting everything would mean
|
|
179
|
+
this function has to change every time the catalogue grows a field, and forgetting to
|
|
180
|
+
update it would silently drop data the server sent.
|
|
181
|
+
"""
|
|
182
|
+
return Model(
|
|
183
|
+
id=d["id"],
|
|
184
|
+
endpoint=d.get("endpoint", ""),
|
|
185
|
+
architecture=d.get("architecture", ""),
|
|
186
|
+
robot=d.get("robot"),
|
|
187
|
+
action_dim=d.get("action_dim"),
|
|
188
|
+
action_horizon=d.get("action_horizon"),
|
|
189
|
+
control_hz=d.get("control_hz"),
|
|
190
|
+
camera_views=tuple(d.get("camera_views") or ()),
|
|
191
|
+
raw=d,
|
|
192
|
+
)
|
sequence_ai/_config.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Where the API key lives when it is not in the environment.
|
|
3
|
+
|
|
4
|
+
Resolution order, first hit wins:
|
|
5
|
+
1. the `api_key=` argument
|
|
6
|
+
2. $SEQUENCES_API_KEY
|
|
7
|
+
3. ~/.config/sequence-ai/credentials
|
|
8
|
+
|
|
9
|
+
WHY a file at all, when an env var already works: a robot controller is usually started by
|
|
10
|
+
systemd or a launch file, not from the shell that ran `export`. Telling someone to put a
|
|
11
|
+
secret into a unit file is worse advice than writing it once to a mode-0600 file in their
|
|
12
|
+
home directory.
|
|
13
|
+
|
|
14
|
+
WHY the env var still wins over the file: CI and containers set env vars, and a stale file
|
|
15
|
+
left behind on a shared machine silently overriding an explicitly-set variable is the kind
|
|
16
|
+
of surprise that costs an afternoon.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import stat
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# XDG if set, otherwise the conventional location. Not ~/.sequence-ai: dotfile clutter in
|
|
26
|
+
# the home directory is what XDG exists to stop.
|
|
27
|
+
def config_dir() -> Path:
|
|
28
|
+
base = os.environ.get("XDG_CONFIG_HOME")
|
|
29
|
+
return Path(base) / "sequence-ai" if base else Path.home() / ".config" / "sequence-ai"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def credentials_path() -> Path:
|
|
33
|
+
return config_dir() / "credentials"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_key() -> str | None:
|
|
37
|
+
"""The stored key, or None. Never raises — a missing or unreadable file just means 'no key'."""
|
|
38
|
+
p = credentials_path()
|
|
39
|
+
try:
|
|
40
|
+
for line in p.read_text().splitlines():
|
|
41
|
+
line = line.strip()
|
|
42
|
+
if line and not line.startswith("#"):
|
|
43
|
+
return line
|
|
44
|
+
except OSError:
|
|
45
|
+
return None
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def save_key(key: str) -> Path:
|
|
50
|
+
"""
|
|
51
|
+
Write the key, readable only by its owner.
|
|
52
|
+
|
|
53
|
+
chmod is applied **after** writing rather than relying on the umask: a permissive umask
|
|
54
|
+
would otherwise leave the file world-readable for the moment between creation and the
|
|
55
|
+
chmod, and on a shared machine that moment is enough.
|
|
56
|
+
"""
|
|
57
|
+
d = config_dir()
|
|
58
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
p = credentials_path()
|
|
60
|
+
p.write_text(key.strip() + "\n")
|
|
61
|
+
p.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600
|
|
62
|
+
return p
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def clear_key() -> bool:
|
|
66
|
+
"""Remove the stored key. Returns whether there was one."""
|
|
67
|
+
p = credentials_path()
|
|
68
|
+
try:
|
|
69
|
+
p.unlink()
|
|
70
|
+
return True
|
|
71
|
+
except OSError:
|
|
72
|
+
return False
|