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.
@@ -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