inspect-robots 0.3.1__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,108 @@
1
+ """Inspect Robots — the Inspect AI for robotics.
2
+
3
+ An evaluation framework for VLA (vision-language-action) models. A benchmark is
4
+ defined once as a [`Task`][inspect_robots.task.Task] and run against any compatible
5
+ pairing of a [`Policy`][inspect_robots.policy.Policy] (the VLA) and an
6
+ [`Embodiment`][inspect_robots.embodiment.Embodiment] (a real robot or simulator).
7
+
8
+ The public API is everything exported here via ``__all__``. Anything not listed
9
+ (or prefixed with ``_``) is private and carries no stability guarantee.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from importlib.metadata import PackageNotFoundError, version
15
+
16
+ try:
17
+ __version__ = version("inspect_robots")
18
+ except PackageNotFoundError: # pragma: no cover - only hit in a non-installed tree
19
+ __version__ = "0.0.0+unknown"
20
+
21
+ from inspect_robots.embodiment import (
22
+ Embodiment,
23
+ EmbodimentBase,
24
+ EmbodimentInfo,
25
+ )
26
+ from inspect_robots.eval import eval, eval_set
27
+ from inspect_robots.log import (
28
+ EvalLog,
29
+ EvalResults,
30
+ EvalSpec,
31
+ EvalStats,
32
+ SceneResult,
33
+ read_eval_log,
34
+ )
35
+ from inspect_robots.policy import Policy, PolicyBase, PolicyConfig, PolicyInfo
36
+ from inspect_robots.registry import embodiment, policy, registered, resolve, scorer, sink, task
37
+ from inspect_robots.rollout import TrialRecord
38
+ from inspect_robots.scene import Scene, Target
39
+ from inspect_robots.scorer import (
40
+ Score,
41
+ Scorer,
42
+ episode_length,
43
+ min_distance_to_goal,
44
+ operator_scorer,
45
+ reached_goal_state,
46
+ success_at_end,
47
+ )
48
+ from inspect_robots.spaces import (
49
+ ActionSemantics,
50
+ Box,
51
+ CameraSpec,
52
+ ObservationSpace,
53
+ StateField,
54
+ StateSpec,
55
+ )
56
+ from inspect_robots.task import Epochs, Task
57
+ from inspect_robots.types import Action, ActionChunk, Observation, StepResult
58
+
59
+ # The public, stability-guaranteed API. Anything not listed here (or prefixed
60
+ # with ``_``) is private. Authoring a benchmark, policy, or embodiment should only
61
+ # need these names.
62
+ __all__ = [
63
+ "Action",
64
+ "ActionChunk",
65
+ "ActionSemantics",
66
+ "Box",
67
+ "CameraSpec",
68
+ "Embodiment",
69
+ "EmbodimentBase",
70
+ "EmbodimentInfo",
71
+ "Epochs",
72
+ "EvalLog",
73
+ "EvalResults",
74
+ "EvalSpec",
75
+ "EvalStats",
76
+ "Observation",
77
+ "ObservationSpace",
78
+ "Policy",
79
+ "PolicyBase",
80
+ "PolicyConfig",
81
+ "PolicyInfo",
82
+ "Scene",
83
+ "SceneResult",
84
+ "Score",
85
+ "Scorer",
86
+ "StateField",
87
+ "StateSpec",
88
+ "StepResult",
89
+ "Target",
90
+ "Task",
91
+ "TrialRecord",
92
+ "__version__",
93
+ "embodiment",
94
+ "episode_length",
95
+ "eval",
96
+ "eval_set",
97
+ "min_distance_to_goal",
98
+ "operator_scorer",
99
+ "policy",
100
+ "reached_goal_state",
101
+ "read_eval_log",
102
+ "registered",
103
+ "resolve",
104
+ "scorer",
105
+ "sink",
106
+ "success_at_end",
107
+ "task",
108
+ ]
@@ -0,0 +1,53 @@
1
+ """Register the in-tree builtin components with the registry.
2
+
3
+ Imported lazily by [`inspect_robots.registry`][inspect_robots.registry] on first
4
+ lookup, so importing ``inspect_robots`` stays cheap and free of import cycles.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from inspect_robots.logging import JsonLogSink, RerunSink
10
+ from inspect_robots.mock import CubePickEmbodiment, NoopPolicy, RandomPolicy, ScriptedPolicy
11
+ from inspect_robots.registry import embodiment, policy, scorer, sink, task
12
+ from inspect_robots.scene import Scene
13
+ from inspect_robots.scorer import (
14
+ episode_length,
15
+ min_distance_to_goal,
16
+ operator_scorer,
17
+ reached_goal_state,
18
+ success_at_end,
19
+ )
20
+ from inspect_robots.task import Task
21
+
22
+ # Embodiments
23
+ embodiment("cubepick")(CubePickEmbodiment)
24
+
25
+ # Policies
26
+ policy("scripted")(ScriptedPolicy)
27
+ policy("random")(RandomPolicy)
28
+ policy("noop")(NoopPolicy)
29
+
30
+ # Scorers
31
+ scorer("success_at_end")(success_at_end)
32
+ scorer("episode_length")(episode_length)
33
+ scorer("min_distance_to_goal")(min_distance_to_goal)
34
+ scorer("reached_goal_state")(reached_goal_state)
35
+ scorer("operator")(operator_scorer)
36
+
37
+ # Sinks
38
+ sink("json")(JsonLogSink)
39
+ sink("rerun")(RerunSink)
40
+
41
+
42
+ @task("cubepick-reach")
43
+ def _cubepick_reach(num_scenes: int = 4, max_steps: int = 80) -> Task:
44
+ """A simple reach benchmark over a handful of seeded cube layouts."""
45
+ return Task(
46
+ name="cubepick-reach",
47
+ scenes=[
48
+ Scene(id=f"scene-{i}", instruction="reach the cube", init_seed=i)
49
+ for i in range(num_scenes)
50
+ ],
51
+ scorer=success_at_end(),
52
+ max_steps=max_steps,
53
+ )
@@ -0,0 +1,153 @@
1
+ """User-level default components for the zero-config CLI (plan 0005).
2
+
3
+ ``inspect-robots "place the spoon on the plate"`` needs a policy and an
4
+ embodiment without flags. They come from, in order: explicit CLI flags
5
+ (handled in ``cli.py``), the ``INSPECT_ROBOTS_POLICY`` /
6
+ ``INSPECT_ROBOTS_EMBODIMENT`` environment variables, then the user config
7
+ file ``<config-home>/inspect-robots/config.ini`` (INI via stdlib
8
+ ``configparser`` — the core supports py3.10, which has no ``tomllib``).
9
+
10
+ There is deliberately **no project-local config file**: a checked-in
11
+ ``./inspect-robots.ini`` choosing which policy runs on the user's hardware
12
+ would be a trust footgun for a tool that moves physical robots.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import configparser
18
+ import os
19
+ from collections.abc import Mapping
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ ENV_POLICY = "INSPECT_ROBOTS_POLICY"
25
+ ENV_EMBODIMENT = "INSPECT_ROBOTS_EMBODIMENT"
26
+ ENV_SIM_EMBODIMENT = "INSPECT_ROBOTS_SIM_EMBODIMENT"
27
+
28
+ # Fallbacks for ad-hoc (instruction) runs when neither flag nor config decides.
29
+ ADHOC_SCORER_FALLBACK = "operator"
30
+ ADHOC_MAX_STEPS_FALLBACK = 300
31
+
32
+
33
+ def parse_value(text: str) -> Any:
34
+ """Best-effort scalar parse for ``k=v`` args (bool/int/float/None/str)."""
35
+ low = text.lower()
36
+ if low in ("true", "false"):
37
+ return low == "true"
38
+ if low in ("none", "null"):
39
+ return None
40
+ for caster in (int, float):
41
+ try:
42
+ return caster(text)
43
+ except ValueError:
44
+ continue
45
+ return text
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Defaults:
50
+ """Resolved user defaults, each with a human-readable source for the run header."""
51
+
52
+ policy: str | None = None
53
+ policy_source: str | None = None
54
+ embodiment: str | None = None
55
+ embodiment_source: str | None = None
56
+ # The sim counterpart --sim swaps in; real hardware is just the default
57
+ # `embodiment`. Args are kept separate: real-rig args (ports, camera
58
+ # serials) are wrong for a sim and vice versa.
59
+ sim_embodiment: str | None = None
60
+ sim_embodiment_source: str | None = None
61
+ scorer: str | None = None
62
+ max_steps: int | None = None
63
+ policy_args: dict[str, Any] = field(default_factory=dict)
64
+ embodiment_args: dict[str, Any] = field(default_factory=dict)
65
+ sim_embodiment_args: dict[str, Any] = field(default_factory=dict)
66
+
67
+
68
+ def _config_path(env: Mapping[str, str]) -> Path | None:
69
+ """The user config file location, derived from ``env`` only (testable)."""
70
+ if xdg := env.get("XDG_CONFIG_HOME"):
71
+ home = Path(xdg)
72
+ elif user_home := env.get("HOME"):
73
+ home = Path(user_home) / ".config"
74
+ else:
75
+ return None
76
+ return home / "inspect-robots" / "config.ini"
77
+
78
+
79
+ def _die(path: Path, problem: str) -> SystemExit:
80
+ return SystemExit(f"error in {path}: {problem}")
81
+
82
+
83
+ def _parse_args_section(parser: configparser.ConfigParser, section: str) -> dict[str, Any]:
84
+ """An ``[<kind>.args]`` section as parsed kwargs, with ``~`` paths expanded."""
85
+ if not parser.has_section(section):
86
+ return {}
87
+ out: dict[str, Any] = {}
88
+ for key, raw in parser.items(section):
89
+ value = parse_value(raw)
90
+ if isinstance(value, str) and value.startswith("~"):
91
+ # Checkpoint paths are the flagship use; a literal "~/..." string
92
+ # would fail silently deep inside a plugin.
93
+ value = os.path.expanduser(value)
94
+ out[key] = value
95
+ return out
96
+
97
+
98
+ def _read_config(path: Path) -> Defaults:
99
+ parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#"))
100
+ try:
101
+ with path.open(encoding="utf-8") as fh:
102
+ parser.read_file(fh)
103
+ except (configparser.Error, UnicodeDecodeError) as exc:
104
+ raise _die(path, f"malformed config: {exc}") from exc
105
+
106
+ source = str(path)
107
+ max_steps: int | None = None
108
+ if raw_steps := parser.get("defaults", "max_steps", fallback=None):
109
+ parsed = parse_value(raw_steps)
110
+ if not isinstance(parsed, int) or isinstance(parsed, bool) or parsed < 1:
111
+ raise _die(path, f"[defaults] max_steps must be an integer >= 1, got {raw_steps!r}")
112
+ max_steps = parsed
113
+
114
+ policy = parser.get("defaults", "policy", fallback=None)
115
+ embodiment = parser.get("defaults", "embodiment", fallback=None)
116
+ sim_embodiment = parser.get("defaults", "sim_embodiment", fallback=None)
117
+ return Defaults(
118
+ policy=policy,
119
+ policy_source=source if policy else None,
120
+ embodiment=embodiment,
121
+ embodiment_source=source if embodiment else None,
122
+ sim_embodiment=sim_embodiment,
123
+ sim_embodiment_source=source if sim_embodiment else None,
124
+ scorer=parser.get("defaults", "scorer", fallback=None),
125
+ max_steps=max_steps,
126
+ policy_args=_parse_args_section(parser, "policy.args"),
127
+ embodiment_args=_parse_args_section(parser, "embodiment.args"),
128
+ sim_embodiment_args=_parse_args_section(parser, "sim_embodiment.args"),
129
+ )
130
+
131
+
132
+ def load_defaults(env: Mapping[str, str]) -> Defaults:
133
+ """Load user defaults: environment variables override the config file.
134
+
135
+ ``env`` is injected (pass ``os.environ``) so tests never touch the real
136
+ home directory. A missing config file yields empty defaults; a malformed
137
+ or type-invalid one raises ``SystemExit`` naming the file — the
138
+ zero-config path must never print a traceback at the user.
139
+ """
140
+ from dataclasses import replace
141
+
142
+ path = _config_path(env)
143
+ defaults = _read_config(path) if path is not None and path.is_file() else Defaults()
144
+
145
+ if policy := env.get(ENV_POLICY):
146
+ defaults = replace(defaults, policy=policy, policy_source=f"${ENV_POLICY}")
147
+ if embodiment := env.get(ENV_EMBODIMENT):
148
+ defaults = replace(defaults, embodiment=embodiment, embodiment_source=f"${ENV_EMBODIMENT}")
149
+ if sim := env.get(ENV_SIM_EMBODIMENT):
150
+ defaults = replace(
151
+ defaults, sim_embodiment=sim, sim_embodiment_source=f"${ENV_SIM_EMBODIMENT}"
152
+ )
153
+ return defaults
@@ -0,0 +1,70 @@
1
+ """The Approver — a safety gate between policy output and the embodiment.
2
+
3
+ Every action passes through ``Approver.review`` before ``embodiment.step``. This
4
+ is the robotics analog of Inspect AI's ``ApprovalPolicy`` and is more
5
+ safety-critical: an approver may pass, clamp, or veto an action (a veto raises
6
+ [`SafetyAbort`][inspect_robots.errors.SafetyAbort]). In the tracer slice the default approver
7
+ passes everything through; clamping/operator approval land in rollout hardening.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import replace
13
+ from typing import Any, Protocol, runtime_checkable
14
+
15
+ import numpy as np
16
+
17
+ from inspect_robots.errors import SafetyAbort
18
+ from inspect_robots.spaces import Box
19
+ from inspect_robots.types import Action
20
+
21
+
22
+ @runtime_checkable
23
+ class Approver(Protocol):
24
+ """Reviews an action before it reaches the embodiment.
25
+
26
+ May return the action unchanged, return a modified (e.g. clamped) action, or
27
+ raise [`SafetyAbort`][inspect_robots.errors.SafetyAbort] to halt the eval.
28
+ """
29
+
30
+ def review(self, action: Action, store: dict[str, Any]) -> Action: ...
31
+
32
+
33
+ class AutoApprover:
34
+ """Approve every action unchanged (the permissive default)."""
35
+
36
+ def review(self, action: Action, store: dict[str, Any]) -> Action:
37
+ return action
38
+
39
+
40
+ class ClampApprover:
41
+ """Clamp actions to a box's ``low``/``high`` bounds before they reach hardware.
42
+
43
+ One-sided boxes are honored: a ``low``-only box clamps from below, a
44
+ ``high``-only box from above. A modified action is flagged via
45
+ ``action.meta["clamped"]`` so the rollout can record an approval event;
46
+ when nothing clamps, the *same* action object is returned (the rollout
47
+ detects modification by identity).
48
+
49
+ Non-finite values are the safety cases: ``NaN`` anywhere in the action
50
+ raises [`SafetyAbort`][inspect_robots.errors.SafetyAbort] — a NaN is a
51
+ poisonous value with no meaningful clamp, and it must never reach hardware.
52
+ ``±inf`` is *not* an abort: it clamps to the finite bound on that side like
53
+ any other out-of-range value (and passes through if that side is
54
+ unbounded).
55
+ """
56
+
57
+ def __init__(self, action_space: Box):
58
+ self._space = action_space
59
+
60
+ def review(self, action: Action, store: dict[str, Any]) -> Action:
61
+ data = np.asarray(action.data, dtype=np.float64)
62
+ if bool(np.isnan(data).any()):
63
+ raise SafetyAbort("ClampApprover: action contains NaN; refusing to pass it on")
64
+ low, high = self._space.low, self._space.high
65
+ if low is None and high is None:
66
+ return action
67
+ clamped = np.clip(data, low, high)
68
+ if np.array_equal(clamped, data):
69
+ return action
70
+ return replace(action, data=clamped, meta={**dict(action.meta), "clamped": True})