servo-client 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.
Files changed (46) hide show
  1. servo/__init__.py +126 -0
  2. servo/client.py +236 -0
  3. servo/constants.py +79 -0
  4. servo/data_streaming.py +679 -0
  5. servo/errors.py +78 -0
  6. servo/execution/__init__.py +1 -0
  7. servo/execution/chunked_loop.py +396 -0
  8. servo/execution/policy.py +102 -0
  9. servo/execution/session.py +98 -0
  10. servo/execution/world.py +61 -0
  11. servo/hardware.py +699 -0
  12. servo/namespaces/__init__.py +1 -0
  13. servo/namespaces/adapters.py +26 -0
  14. servo/namespaces/billing.py +100 -0
  15. servo/namespaces/datasets.py +250 -0
  16. servo/namespaces/deployments.py +22 -0
  17. servo/namespaces/jobs.py +92 -0
  18. servo/namespaces/licenses.py +23 -0
  19. servo/namespaces/models.py +466 -0
  20. servo/namespaces/robots.py +106 -0
  21. servo/namespaces/secrets.py +43 -0
  22. servo/resources/__init__.py +1 -0
  23. servo/resources/adapter.py +79 -0
  24. servo/resources/base.py +123 -0
  25. servo/resources/billing.py +54 -0
  26. servo/resources/dataset.py +561 -0
  27. servo/resources/deployment.py +130 -0
  28. servo/resources/embodiment.py +41 -0
  29. servo/resources/hf_import.py +288 -0
  30. servo/resources/job.py +221 -0
  31. servo/resources/model.py +402 -0
  32. servo/resources/secret.py +38 -0
  33. servo/resources/upload.py +35 -0
  34. servo/resources/usage.py +75 -0
  35. servo/templates.py +47 -0
  36. servo/types.py +100 -0
  37. servo_client-0.1.0.dist-info/METADATA +24 -0
  38. servo_client-0.1.0.dist-info/RECORD +46 -0
  39. servo_client-0.1.0.dist-info/WHEEL +5 -0
  40. servo_client-0.1.0.dist-info/top_level.txt +2 -0
  41. servo_contracts/__init__.py +11 -0
  42. servo_contracts/errors.py +148 -0
  43. servo_contracts/inference.py +161 -0
  44. servo_contracts/observability.py +124 -0
  45. servo_contracts/resources.py +55 -0
  46. servo_contracts/templates.py +274 -0
servo/__init__.py ADDED
@@ -0,0 +1,126 @@
1
+ """Servo SDK — robot AI, made usable.
2
+
3
+ The public surface is one robot policy operations API:
4
+ - **Identity**: ``Model``, ``Checkpoint`` (which weights).
5
+ - **Hosting**: ``HostedDeployment``, ``EdgeExport`` (where it runs; never routes).
6
+ - **Execution**: ``Policy`` (``.act``), ``World`` (``.rollout``), ``Session`` (the control loop).
7
+
8
+ Everything is reached from ``servo.Servo``. ``servo.templates`` carries embodiment helpers;
9
+ ``servo.errors`` carries the full typed error surface. Modules can be used independently or
10
+ composed through the same policy and embodiment resources. Upstream frameworks are exposed as
11
+ integration adapters, not as application-facing control surfaces.
12
+ """
13
+
14
+ from servo import errors, templates # noqa: F401
15
+ from servo.client import Servo
16
+ from servo.constants import (
17
+ ActionSpace,
18
+ Compute,
19
+ PolicyAdapter,
20
+ RobotType,
21
+ )
22
+ from servo.errors import (
23
+ ActionSpaceIncompatible,
24
+ CapabilityNotAvailable,
25
+ DependencyUnavailable,
26
+ EmbodimentUnsupported,
27
+ ExternalDependencyError,
28
+ LicenseBlocked,
29
+ ModelNotServable,
30
+ NotFound,
31
+ NotHydrated,
32
+ NotReady,
33
+ ObservationInvalid,
34
+ PaymentRequired,
35
+ ServoError,
36
+ SurfaceUnsupported,
37
+ Unauthorized,
38
+ ValidationError,
39
+ )
40
+ from servo.execution.chunked_loop import ChunkedRunReport, ChunkRecord
41
+ from servo.execution.policy import Policy
42
+ from servo.execution.session import Session
43
+ from servo.execution.world import World
44
+ from servo.hardware import LocalRobot as ConnectedRobot
45
+ from servo.hardware import RobotRun
46
+ from servo.resources.dataset import Dataset, DatasetInspection
47
+ from servo.resources.deployment import EdgeExport, HostedDeployment
48
+ from servo.resources.embodiment import Embodiment as Robot
49
+ from servo.resources.hf_import import ModelImport, ModelInspection
50
+ from servo.resources.job import EvalResult, FineTuneResult, Job, SynthDataResult
51
+ from servo.resources.model import Checkpoint, CompatibilityReport, Model, RuntimeBinding
52
+ from servo.resources.adapter import Adapter, RuntimeEntry
53
+ from servo.resources.billing import BillingBalance, CheckoutSession
54
+ from servo.resources.secret import Secret
55
+ from servo.resources.upload import ModelUpload
56
+ from servo.resources.usage import Grant, UsageSummary
57
+ from servo.types import ActionPrediction, FrameRef, Observation, RolloutPrediction
58
+
59
+ __all__ = [
60
+ # Client + helpers
61
+ "Servo",
62
+ "templates",
63
+ "errors",
64
+ "Observation",
65
+ "ActionSpace",
66
+ "Compute",
67
+ "PolicyAdapter",
68
+ "RobotType",
69
+ # Identity
70
+ "Model",
71
+ "Checkpoint",
72
+ "Robot",
73
+ "ConnectedRobot",
74
+ "Adapter",
75
+ "RuntimeEntry",
76
+ # Data + import lifecycle
77
+ "Dataset",
78
+ "DatasetInspection",
79
+ "ModelInspection",
80
+ "ModelImport",
81
+ # Hosting
82
+ "HostedDeployment",
83
+ "EdgeExport",
84
+ # Jobs + results
85
+ "Job",
86
+ "FineTuneResult",
87
+ "EvalResult",
88
+ "SynthDataResult",
89
+ # Execution
90
+ "Policy",
91
+ "World",
92
+ "Session",
93
+ "RobotRun",
94
+ "ChunkedRunReport",
95
+ "ChunkRecord",
96
+ # Predictions
97
+ "ActionPrediction",
98
+ "RolloutPrediction",
99
+ "FrameRef",
100
+ # Operational
101
+ "UsageSummary",
102
+ "Grant",
103
+ "CompatibilityReport",
104
+ "RuntimeBinding",
105
+ "BillingBalance",
106
+ "CheckoutSession",
107
+ "Secret",
108
+ "ModelUpload",
109
+ # Errors (also under servo.errors)
110
+ "NotHydrated",
111
+ "NotReady",
112
+ "ServoError",
113
+ "SurfaceUnsupported",
114
+ "CapabilityNotAvailable",
115
+ "LicenseBlocked",
116
+ "EmbodimentUnsupported",
117
+ "ActionSpaceIncompatible",
118
+ "ExternalDependencyError",
119
+ "DependencyUnavailable",
120
+ "ValidationError",
121
+ "NotFound",
122
+ "Unauthorized",
123
+ "ModelNotServable",
124
+ "ObservationInvalid",
125
+ "PaymentRequired",
126
+ ]
servo/client.py ADDED
@@ -0,0 +1,236 @@
1
+ """Servo SDK client (006 redesign).
2
+
3
+ The ``Servo`` client is the single entry point: thin uniform namespaces (``sv.models`` etc.), the
4
+ execution factories (``sv.policy``/``sv.world``/``sv.session``), and ``sv.usage()``. Transport is
5
+ unchanged — HTTP to a running control plane by default, or the in-process ASGI ``app=`` test client.
6
+
7
+ Robot/embodiment normalization (FR-013/D8): every ``robot=`` accepts a registered robot handle,
8
+ id, or name and is resolved to an embodiment id via ``_embodiment_id`` before any request;
9
+ unknown/ambiguous names raise client-side.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ import httpx
17
+
18
+ from servo_contracts.errors import (
19
+ ActionSpaceIncompatible,
20
+ CapabilityNotAvailable,
21
+ DependencyUnavailable,
22
+ EmbodimentUnsupported,
23
+ ExternalDependencyError,
24
+ LicenseBlocked,
25
+ ModelNotServable,
26
+ NotFound,
27
+ ObservationInvalid,
28
+ PaymentRequired,
29
+ ServoError,
30
+ SurfaceUnsupported,
31
+ Unauthorized,
32
+ )
33
+ from servo.errors import NotReady, ValidationError
34
+ from servo.namespaces.datasets import Datasets
35
+ from servo.namespaces.adapters import Adapters
36
+ from servo.namespaces.billing import Billing
37
+ from servo.namespaces.deployments import Deployments
38
+ from servo.namespaces.robots import Robots
39
+ from servo.namespaces.jobs import Jobs
40
+ from servo.namespaces.licenses import Licenses
41
+ from servo.namespaces.models import Models
42
+ from servo.namespaces.secrets import Secrets
43
+ from servo.resources.usage import UsageSummary
44
+
45
+ if TYPE_CHECKING: # pragma: no cover
46
+ from servo.execution.policy import Policy
47
+ from servo.execution.session import Session
48
+ from servo.execution.world import World
49
+ from servo.resources.embodiment import Embodiment
50
+ from servo.resources.model import Checkpoint, Model
51
+
52
+ _ERROR_BY_CODE = {
53
+ "ObservationInvalid": ObservationInvalid,
54
+ "ActionSpaceIncompatible": ActionSpaceIncompatible,
55
+ "ModelNotServable": ModelNotServable,
56
+ "EmbodimentUnsupported": EmbodimentUnsupported,
57
+ "LicenseBlocked": LicenseBlocked,
58
+ "SurfaceUnsupported": SurfaceUnsupported,
59
+ "CapabilityNotAvailable": CapabilityNotAvailable,
60
+ "ExternalDependencyError": ExternalDependencyError,
61
+ "DependencyUnavailable": DependencyUnavailable,
62
+ "ValidationError": ValidationError,
63
+ "NotFound": NotFound,
64
+ "NotReady": NotReady,
65
+ "Unauthorized": Unauthorized,
66
+ "PaymentRequired": PaymentRequired,
67
+ }
68
+
69
+
70
+ def _raise_for_error(resp: httpx.Response) -> dict[str, Any]:
71
+ if resp.status_code < 400:
72
+ return resp.json() if resp.content else {}
73
+ try:
74
+ body = resp.json()
75
+ except Exception: # noqa: BLE001
76
+ resp.raise_for_status()
77
+ raise
78
+ code = body.get("code", "ServoError")
79
+ cls = _ERROR_BY_CODE.get(code, ServoError)
80
+ raise cls(body.get("message", "error"), detail=body.get("detail"))
81
+
82
+
83
+ class Servo:
84
+ """Typed Servo client. Pass ``app=`` to drive the ASGI app in-process (hermetic tests)."""
85
+
86
+ def __init__(
87
+ self,
88
+ base_url: str = "http://127.0.0.1:8000",
89
+ api_key: str = "dev",
90
+ app: Any | None = None,
91
+ http: Any | None = None,
92
+ timeout: float | None = 3600.0,
93
+ ):
94
+ self.api_key = api_key
95
+ headers = {"Authorization": f"Bearer {api_key}"}
96
+ if http is not None:
97
+ self._http = http
98
+ self._http.headers.update(headers)
99
+ elif app is not None:
100
+ from starlette.testclient import TestClient
101
+
102
+ self._http = TestClient(app)
103
+ self._http.headers.update(headers)
104
+ else:
105
+ # Some control-plane calls block on real GPU work (a synchronous finetune POST runs the
106
+ # Modal job inline and only returns when it finishes — minutes, not seconds). httpx's
107
+ # 5s default read-timeout would abort those mid-flight, so the SDK defaults to a generous
108
+ # 1h timeout. Override per-client with ``timeout=`` (``None`` disables it entirely).
109
+ self._http = httpx.Client(base_url=base_url, headers=headers, timeout=timeout)
110
+
111
+ # Namespaces (thin, uniform — all return typed handles).
112
+ self.robots = Robots(self)
113
+ self.adapters = Adapters(self)
114
+ self.models = Models(self)
115
+ self.datasets = Datasets(self)
116
+ self.data = self.datasets
117
+ self.deployments = Deployments(self)
118
+ self.jobs = Jobs(self)
119
+ self.licenses = Licenses(self)
120
+ self.billing = Billing(self)
121
+ self.secrets = Secrets(self)
122
+
123
+ self._embodiment_cache: dict[str, str] | None = None
124
+
125
+ # ---- execution factories ----
126
+ def policy(
127
+ self,
128
+ model_or_checkpoint: "Any",
129
+ embodiment: "Any",
130
+ *,
131
+ instruction: str | None = None,
132
+ deployment_id: str | None = None,
133
+ advisory: str | None = None,
134
+ ) -> "Policy":
135
+ """Bind a model/checkpoint + embodiment -> a :class:`Policy` (single-shot ``.act``)."""
136
+ from servo.execution.policy import Policy
137
+ from servo.resources.model import Checkpoint, Model
138
+
139
+ ref = (
140
+ model_or_checkpoint.id
141
+ if isinstance(model_or_checkpoint, (Model, Checkpoint))
142
+ else model_or_checkpoint
143
+ )
144
+ local_robot = embodiment if hasattr(embodiment, "run") and hasattr(embodiment, "embodiment") else None
145
+ emb_id = self._embodiment_id(embodiment)
146
+ return Policy(
147
+ self, ref, emb_id,
148
+ instruction=instruction, deployment_id=deployment_id, advisory=advisory,
149
+ robot=local_robot,
150
+ )
151
+
152
+ def world(self, model: "Any", embodiment: "Any" = None) -> "World":
153
+ """Bind a world model -> a :class:`World` (``.rollout``)."""
154
+ from servo.execution.world import World
155
+ from servo.resources.model import Checkpoint, Model
156
+
157
+ ref = model.id if isinstance(model, (Model, Checkpoint)) else model
158
+ emb_id = self._embodiment_id(embodiment) if embodiment is not None else None
159
+ return World(self, ref, embodiment_id=emb_id)
160
+
161
+ def session(self, policy: "Policy") -> "Session":
162
+ """Open the real-robot control loop for a :class:`Policy` -> a :class:`Session` (context mgr)."""
163
+ from servo.execution.session import Session
164
+
165
+ return Session(self, policy)
166
+
167
+ def usage(self) -> UsageSummary:
168
+ """Org usage telemetry summary -> a typed :class:`UsageSummary`."""
169
+ return UsageSummary(self._get("/v1/usage"))
170
+
171
+ # ---- embodiment normalization (FR-013 / D8) ----
172
+ def _embodiment_id(self, embodiment: "Any") -> str:
173
+ """Normalize ``Robot handle | id | name`` -> an embodiment id."""
174
+ from servo.resources.embodiment import Embodiment
175
+
176
+ if isinstance(embodiment, Embodiment):
177
+ return embodiment.id
178
+ local_embodiment = getattr(embodiment, "embodiment", None)
179
+ if isinstance(local_embodiment, Embodiment):
180
+ return local_embodiment.id
181
+ if not isinstance(embodiment, str):
182
+ raise ValidationError(
183
+ f"robot must be a Robot | id | name, got {type(embodiment).__name__}",
184
+ detail={"value": repr(embodiment)},
185
+ )
186
+ # An id passed directly (registered embodiment ids are emb_...) — accept as-is.
187
+ if embodiment.startswith("emb_"):
188
+ return embodiment
189
+ cache = self._embodiment_cache_get()
190
+ # id present in the cache (covers non-prefixed ids).
191
+ if embodiment in cache.values():
192
+ return embodiment
193
+ matches = [eid for name, eid in cache.items() if name == embodiment]
194
+ if len(matches) == 1:
195
+ return matches[0]
196
+ if len(matches) > 1:
197
+ raise ValidationError(
198
+ f"embodiment name '{embodiment}' is ambiguous ({len(matches)} matches)",
199
+ detail={"name": embodiment, "matches": matches},
200
+ )
201
+ # Unknown name: refresh once in case the cache is stale, then fail closed.
202
+ self._invalidate_embodiment_cache()
203
+ cache = self._embodiment_cache_get()
204
+ if embodiment in cache.values():
205
+ return embodiment
206
+ matches = [eid for name, eid in cache.items() if name == embodiment]
207
+ if len(matches) == 1:
208
+ return matches[0]
209
+ if len(matches) > 1:
210
+ raise ValidationError(
211
+ f"embodiment name '{embodiment}' is ambiguous ({len(matches)} matches)",
212
+ detail={"name": embodiment, "matches": matches},
213
+ )
214
+ raise ValidationError(
215
+ f"unknown robot '{embodiment}'; register it or pass a Robot handle",
216
+ detail={"name": embodiment},
217
+ )
218
+
219
+ def _embodiment_cache_get(self) -> dict[str, str]:
220
+ if self._embodiment_cache is None:
221
+ data = self._get("/v1/embodiments")["embodiments"]
222
+ self._embodiment_cache = {e["name"]: e["id"] for e in data}
223
+ return self._embodiment_cache
224
+
225
+ def _invalidate_embodiment_cache(self) -> None:
226
+ self._embodiment_cache = None
227
+
228
+ # ---- transport ----
229
+ def _get(self, path: str, params: dict | None = None) -> Any:
230
+ return _raise_for_error(self._http.get(path, params=params))
231
+
232
+ def _post(self, path: str, body: Any) -> Any:
233
+ return _raise_for_error(self._http.post(path, json=body))
234
+
235
+ def _delete(self, path: str) -> Any:
236
+ return _raise_for_error(self._http.delete(path))
servo/constants.py ADDED
@@ -0,0 +1,79 @@
1
+ """Public constants for Servo's typed SDK surface.
2
+
3
+ The constants are ``str`` enums, so callers can pass them anywhere the SDK accepts a string
4
+ while still getting autocomplete and a finite set of Servo-owned values.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+
11
+
12
+ class RobotType(StrEnum):
13
+ """Built-in robot types with Servo observation/action schemas."""
14
+
15
+ SO101 = "so101"
16
+ ALOHA = "aloha"
17
+ LIBERO = "libero"
18
+ DROID = "droid"
19
+
20
+
21
+ class ActionSpace(StrEnum):
22
+ JOINT_POSITION = "joint_position"
23
+ JOINT_VELOCITY = "joint_velocity"
24
+ EEF_DELTA = "eef_delta"
25
+ EEF_ABSOLUTE = "eef_absolute"
26
+
27
+
28
+ class PolicyAdapter(StrEnum):
29
+ """Servo policy integrations that inspect, validate, train, and serve checkpoints."""
30
+
31
+ OPENPI = "openpi"
32
+ OPENVLA = "openvla"
33
+ SMOLVLA = "smolvla"
34
+ GROOT = "gr00t"
35
+ MOLMOACT2 = "molmoact2"
36
+ STARVLA = "starvla"
37
+ VLA0 = "vla0"
38
+ HY_EMBODIED = "hy-embodied"
39
+ LINGBOT_VLA = "lingbot-vla"
40
+ X_VLA = "x-vla"
41
+ RDT = "rdt"
42
+ OCTO = "octo"
43
+ CUSTOM = "custom"
44
+
45
+ @classmethod
46
+ def native(cls) -> tuple["PolicyAdapter", ...]:
47
+ return (cls.OPENPI, cls.OPENVLA, cls.SMOLVLA, cls.GROOT)
48
+
49
+ @classmethod
50
+ def external(cls) -> tuple["PolicyAdapter", ...]:
51
+ return (
52
+ cls.MOLMOACT2,
53
+ cls.STARVLA,
54
+ cls.VLA0,
55
+ cls.HY_EMBODIED,
56
+ cls.LINGBOT_VLA,
57
+ cls.X_VLA,
58
+ cls.RDT,
59
+ cls.OCTO,
60
+ cls.CUSTOM,
61
+ )
62
+
63
+ @classmethod
64
+ def all(cls) -> tuple["PolicyAdapter", ...]:
65
+ return cls.native() + cls.external()
66
+
67
+
68
+ class Compute(StrEnum):
69
+ """Compute substrates Servo can provision for hosted deployment."""
70
+
71
+ MODAL = "modal"
72
+
73
+
74
+ __all__ = [
75
+ "ActionSpace",
76
+ "Compute",
77
+ "PolicyAdapter",
78
+ "RobotType",
79
+ ]