inspect-robots-agent 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.
- inspect_robots_agent/__init__.py +28 -0
- inspect_robots_agent/_llm.py +186 -0
- inspect_robots_agent/_png.py +50 -0
- inspect_robots_agent/_tools.py +282 -0
- inspect_robots_agent/policy.py +211 -0
- inspect_robots_agent/py.typed +0 -0
- inspect_robots_agent-0.1.0.dist-info/METADATA +73 -0
- inspect_robots_agent-0.1.0.dist-info/RECORD +10 -0
- inspect_robots_agent-0.1.0.dist-info/WHEEL +4 -0
- inspect_robots_agent-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""inspect-robots-agent — frontier LLMs as first-class Inspect Robots policies.
|
|
2
|
+
|
|
3
|
+
An LLM behind any OpenAI-compatible API (OpenRouter, OpenAI, local
|
|
4
|
+
vLLM/Ollama, Anthropic's compat endpoint) drives whatever embodiment it is
|
|
5
|
+
paired with: each tool call becomes one smooth, approver-checked action
|
|
6
|
+
chunk. Registered as the policy ``agent``::
|
|
7
|
+
|
|
8
|
+
inspect-robots "pick up the cube" --policy agent \
|
|
9
|
+
-P model=anthropic/claude-fable-5 --embodiment cubepick
|
|
10
|
+
|
|
11
|
+
or programmatically::
|
|
12
|
+
|
|
13
|
+
from inspect_robots import eval
|
|
14
|
+
from inspect_robots_agent import LLMAgentPolicy
|
|
15
|
+
|
|
16
|
+
eval("my-task", LLMAgentPolicy(model="openai/gpt-5.2"), "cubepick")
|
|
17
|
+
|
|
18
|
+
The policy is discovered via the ``inspect_robots.policies`` entry point, so
|
|
19
|
+
it shows up in ``inspect-robots list policies`` without being imported first.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from inspect_robots_agent.policy import AgentPolicyConfig, LLMAgentPolicy, agent_policy
|
|
25
|
+
|
|
26
|
+
__all__ = ["AgentPolicyConfig", "LLMAgentPolicy", "agent_policy"]
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""OpenAI-compatible chat client + provider resolution (plan 0008 §4a).
|
|
2
|
+
|
|
3
|
+
No provider SDKs: one ``httpx`` client speaking the chat-completions wire
|
|
4
|
+
format covers OpenRouter, OpenAI, local vLLM/Ollama, and Anthropic's
|
|
5
|
+
OpenAI-compat endpoint — the same "speak the protocol, don't import the
|
|
6
|
+
package" doctrine as the xpolicylab plugin.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from inspect_robots.errors import ConfigError
|
|
19
|
+
|
|
20
|
+
ENV_MODEL = "INSPECT_ROBOTS_MODEL"
|
|
21
|
+
|
|
22
|
+
_ANTHROPIC_BASE = "https://api.anthropic.com/v1"
|
|
23
|
+
_OPENAI_BASE = "https://api.openai.com/v1"
|
|
24
|
+
_OPENROUTER_BASE = "https://openrouter.ai/api/v1"
|
|
25
|
+
_OPENROUTER_KEY = "OPENROUTER_API_KEY"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Provider:
|
|
30
|
+
"""A resolved OpenAI-compatible endpoint: where, with which key, which model."""
|
|
31
|
+
|
|
32
|
+
base_url: str
|
|
33
|
+
api_key: str
|
|
34
|
+
model: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_provider(
|
|
38
|
+
model: str | None,
|
|
39
|
+
base_url: str | None,
|
|
40
|
+
api_key_env: str | None,
|
|
41
|
+
env: Mapping[str, str],
|
|
42
|
+
) -> Provider:
|
|
43
|
+
"""The key/base-url ladder (plan 0008 §4a); first match wins.
|
|
44
|
+
|
|
45
|
+
1. Explicit ``base_url`` — any OpenAI-compatible endpoint; the key comes
|
|
46
|
+
from ``api_key_env`` (default ``OPENROUTER_API_KEY``), and a missing
|
|
47
|
+
key is allowed (local vLLM/Ollama endpoints are typically keyless).
|
|
48
|
+
2. ``anthropic/*`` model + ``ANTHROPIC_API_KEY`` — the Anthropic compat
|
|
49
|
+
endpoint (provider prefix stripped from the model id).
|
|
50
|
+
3. ``openai/*`` model + ``OPENAI_API_KEY`` — OpenAI (prefix stripped).
|
|
51
|
+
4. ``OPENROUTER_API_KEY`` — OpenRouter, which takes the full
|
|
52
|
+
``provider/model`` string.
|
|
53
|
+
|
|
54
|
+
Anything else is a guided [`ConfigError`][inspect_robots.errors.ConfigError]
|
|
55
|
+
naming the fixes — never a traceback at the user.
|
|
56
|
+
"""
|
|
57
|
+
if not model:
|
|
58
|
+
raise ConfigError(
|
|
59
|
+
"no model configured for the agent policy.\n"
|
|
60
|
+
f"fix: pass -P model=provider/model (e.g. anthropic/claude-fable-5) "
|
|
61
|
+
f"or set ${ENV_MODEL}"
|
|
62
|
+
)
|
|
63
|
+
if base_url:
|
|
64
|
+
key_env = api_key_env or _OPENROUTER_KEY
|
|
65
|
+
return Provider(base_url=base_url.rstrip("/"), api_key=env.get(key_env, ""), model=model)
|
|
66
|
+
provider_prefix, _, bare_model = model.partition("/")
|
|
67
|
+
if provider_prefix == "anthropic" and (key := env.get("ANTHROPIC_API_KEY")):
|
|
68
|
+
return Provider(base_url=_ANTHROPIC_BASE, api_key=key, model=bare_model)
|
|
69
|
+
if provider_prefix == "openai" and (key := env.get("OPENAI_API_KEY")):
|
|
70
|
+
return Provider(base_url=_OPENAI_BASE, api_key=key, model=bare_model)
|
|
71
|
+
if key := env.get(_OPENROUTER_KEY):
|
|
72
|
+
return Provider(base_url=_OPENROUTER_BASE, api_key=key, model=model)
|
|
73
|
+
raise ConfigError(
|
|
74
|
+
f"no API key found for model {model!r}.\n"
|
|
75
|
+
f"fix: set ${_OPENROUTER_KEY} (works for any model), or the provider's "
|
|
76
|
+
"key ($ANTHROPIC_API_KEY for anthropic/*, $OPENAI_API_KEY for openai/*), "
|
|
77
|
+
"or pass -P base_url=... (+ -P api_key_env=NAME) for a custom endpoint"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class ToolCall:
|
|
83
|
+
"""One tool invocation the model asked for; ``arguments`` is raw JSON text."""
|
|
84
|
+
|
|
85
|
+
id: str
|
|
86
|
+
name: str
|
|
87
|
+
arguments: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class AssistantMessage:
|
|
92
|
+
"""The parsed ``choices[0].message`` of a chat completion."""
|
|
93
|
+
|
|
94
|
+
content: str | None
|
|
95
|
+
tool_calls: tuple[ToolCall, ...]
|
|
96
|
+
|
|
97
|
+
def raw(self) -> dict[str, Any]:
|
|
98
|
+
"""The wire-format dict to append back onto the conversation."""
|
|
99
|
+
message: dict[str, Any] = {"role": "assistant", "content": self.content}
|
|
100
|
+
if self.tool_calls:
|
|
101
|
+
message["tool_calls"] = [
|
|
102
|
+
{
|
|
103
|
+
"id": c.id,
|
|
104
|
+
"type": "function",
|
|
105
|
+
"function": {"name": c.name, "arguments": c.arguments},
|
|
106
|
+
}
|
|
107
|
+
for c in self.tool_calls
|
|
108
|
+
]
|
|
109
|
+
return message
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ChatClient:
|
|
113
|
+
"""Blocking chat-completions client with bounded retry on transient failures.
|
|
114
|
+
|
|
115
|
+
Retries 429/5xx and transport errors with exponential backoff; a 4xx is
|
|
116
|
+
our request's fault and fails immediately. Persistent failure raises
|
|
117
|
+
``RuntimeError``, which the rollout wraps as ``PolicyError``.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
provider: Provider,
|
|
123
|
+
*,
|
|
124
|
+
timeout_s: float = 120.0,
|
|
125
|
+
max_retries: int = 3,
|
|
126
|
+
backoff_s: float = 1.0,
|
|
127
|
+
transport: httpx.BaseTransport | None = None,
|
|
128
|
+
):
|
|
129
|
+
self._provider = provider
|
|
130
|
+
self._max_retries = max_retries
|
|
131
|
+
self._backoff_s = backoff_s
|
|
132
|
+
headers = {}
|
|
133
|
+
if provider.api_key:
|
|
134
|
+
headers["Authorization"] = f"Bearer {provider.api_key}"
|
|
135
|
+
self._http = httpx.Client(
|
|
136
|
+
base_url=provider.base_url,
|
|
137
|
+
headers=headers,
|
|
138
|
+
timeout=timeout_s,
|
|
139
|
+
transport=transport,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def complete(
|
|
143
|
+
self,
|
|
144
|
+
messages: list[dict[str, Any]],
|
|
145
|
+
tools: list[dict[str, Any]],
|
|
146
|
+
temperature: float | None = None,
|
|
147
|
+
) -> AssistantMessage:
|
|
148
|
+
body: dict[str, Any] = {"model": self._provider.model, "messages": messages}
|
|
149
|
+
if tools:
|
|
150
|
+
body["tools"] = tools
|
|
151
|
+
if temperature is not None:
|
|
152
|
+
body["temperature"] = temperature
|
|
153
|
+
|
|
154
|
+
last_error = "unknown error"
|
|
155
|
+
for attempt in range(self._max_retries):
|
|
156
|
+
try:
|
|
157
|
+
response = self._http.post("/chat/completions", json=body)
|
|
158
|
+
except httpx.TransportError as exc:
|
|
159
|
+
last_error = str(exc)
|
|
160
|
+
else:
|
|
161
|
+
if response.status_code == 200:
|
|
162
|
+
return _parse_message(response.json())
|
|
163
|
+
last_error = f"HTTP {response.status_code}: {response.text[:500]}"
|
|
164
|
+
if response.status_code not in (429,) and response.status_code < 500:
|
|
165
|
+
# A 4xx is our request's fault; retrying cannot help.
|
|
166
|
+
raise RuntimeError(f"LLM request rejected — {last_error}")
|
|
167
|
+
if attempt + 1 < self._max_retries:
|
|
168
|
+
time.sleep(self._backoff_s * 2**attempt)
|
|
169
|
+
raise RuntimeError(f"LLM request failed after {self._max_retries} attempts — {last_error}")
|
|
170
|
+
|
|
171
|
+
def close(self) -> None:
|
|
172
|
+
self._http.close()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse_message(payload: dict[str, Any]) -> AssistantMessage:
|
|
176
|
+
message = payload["choices"][0]["message"]
|
|
177
|
+
calls = tuple(
|
|
178
|
+
ToolCall(
|
|
179
|
+
id=str(c["id"]),
|
|
180
|
+
name=str(c["function"]["name"]),
|
|
181
|
+
arguments=str(c["function"]["arguments"]),
|
|
182
|
+
)
|
|
183
|
+
for c in message.get("tool_calls") or []
|
|
184
|
+
)
|
|
185
|
+
content = message.get("content")
|
|
186
|
+
return AssistantMessage(content=content if content is None else str(content), tool_calls=calls)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Minimal stdlib PNG encoder for camera frames (no Pillow dependency).
|
|
2
|
+
|
|
3
|
+
LLM APIs take images as base64 data URLs; this encodes an ``(H, W, C)``
|
|
4
|
+
uint8/float array as an uncompressed-filter PNG using only zlib + struct.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import struct
|
|
11
|
+
import zlib
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
|
|
17
|
+
_COLOR_TYPE_BY_CHANNELS = {1: 0, 3: 2, 4: 6}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _chunk(tag: bytes, data: bytes) -> bytes:
|
|
21
|
+
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def encode_png(image: npt.NDArray[Any]) -> bytes:
|
|
25
|
+
"""Encode an ``(H, W)`` or ``(H, W, {1,3,4})`` array as PNG bytes.
|
|
26
|
+
|
|
27
|
+
Float arrays are assumed normalized to [0, 1] and scaled; everything else
|
|
28
|
+
is cast to uint8.
|
|
29
|
+
"""
|
|
30
|
+
arr = np.asarray(image)
|
|
31
|
+
if np.issubdtype(arr.dtype, np.floating):
|
|
32
|
+
arr = (np.clip(arr, 0.0, 1.0) * 255.0).round()
|
|
33
|
+
arr = np.ascontiguousarray(arr.astype(np.uint8))
|
|
34
|
+
if arr.ndim == 2:
|
|
35
|
+
arr = arr[:, :, np.newaxis]
|
|
36
|
+
height, width, channels = arr.shape
|
|
37
|
+
color_type = _COLOR_TYPE_BY_CHANNELS[channels]
|
|
38
|
+
header = struct.pack(">IIBBBBB", width, height, 8, color_type, 0, 0, 0)
|
|
39
|
+
raw = b"".join(b"\x00" + arr[row].tobytes() for row in range(height))
|
|
40
|
+
return (
|
|
41
|
+
b"\x89PNG\r\n\x1a\n"
|
|
42
|
+
+ _chunk(b"IHDR", header)
|
|
43
|
+
+ _chunk(b"IDAT", zlib.compress(raw))
|
|
44
|
+
+ _chunk(b"IEND", b"")
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def png_data_url(image: npt.NDArray[Any]) -> str:
|
|
49
|
+
"""The ``data:image/png;base64,...`` form LLM APIs accept inline."""
|
|
50
|
+
return "data:image/png;base64," + base64.b64encode(encode_png(image)).decode("ascii")
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""The agent's tool surface over a bound action space (plan 0008 §4b/§4c).
|
|
2
|
+
|
|
3
|
+
Tools are generated from the embodiment's spaces — the plugin never knows
|
|
4
|
+
what a "YAM" or an "arm" is. The control mode picks the motion tool:
|
|
5
|
+
|
|
6
|
+
- Absolute-target modes (``joint_pos``, ``eef_abs_pose``): ``move_joints``
|
|
7
|
+
with named partial targets, linearly interpolated from the current
|
|
8
|
+
observed state; hold-still repeats the current state.
|
|
9
|
+
- Displacement modes (``eef_delta_pos``, ``eef_delta_pose``,
|
|
10
|
+
``joint_delta``): ``move_by`` splits the requested displacement evenly
|
|
11
|
+
across the chunk's steps; hold-still is all zeros.
|
|
12
|
+
|
|
13
|
+
Tool mistakes (unknown label, non-finite value, bad duration, malformed
|
|
14
|
+
JSON) come back as structured error strings the LLM sees and can correct —
|
|
15
|
+
never exceptions. Unsupported configurations fail at build (= ``bind``)
|
|
16
|
+
time with a clear message.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import numpy.typing as npt
|
|
27
|
+
|
|
28
|
+
from inspect_robots.spaces import Box, ObservationSpace
|
|
29
|
+
from inspect_robots.types import Action, ActionChunk, Observation
|
|
30
|
+
|
|
31
|
+
_ABSOLUTE_MODES = frozenset({"joint_pos", "eef_abs_pose"})
|
|
32
|
+
_DISPLACEMENT_MODES = frozenset({"eef_delta_pos", "eef_delta_pose", "joint_delta"})
|
|
33
|
+
_POSE_MODES = frozenset({"eef_abs_pose", "eef_delta_pose"})
|
|
34
|
+
_SAFE_ROT = frozenset({"none", "rot6d"})
|
|
35
|
+
|
|
36
|
+
_FALLBACK_HZ = 10.0
|
|
37
|
+
_MAX_DURATION_S = 10.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ToolsetError(Exception):
|
|
41
|
+
"""An action space / observation space this toolset cannot drive."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ToolResult:
|
|
46
|
+
"""One executed tool call: an action chunk to play, or an error for the LLM.
|
|
47
|
+
|
|
48
|
+
Exactly one of ``chunk``/``error`` is set. ``note`` is the human/LLM-facing
|
|
49
|
+
confirmation text for successful calls.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
chunk: ActionChunk | None = None
|
|
53
|
+
error: str | None = None
|
|
54
|
+
note: str = ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Toolset:
|
|
58
|
+
"""Schemas + execution for one bound embodiment. Built via ``build_toolset``."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
absolute: bool,
|
|
64
|
+
labels: tuple[str, ...],
|
|
65
|
+
state_key: str | None,
|
|
66
|
+
control_hz: float | None,
|
|
67
|
+
bounds_text: str,
|
|
68
|
+
):
|
|
69
|
+
self._absolute = absolute
|
|
70
|
+
self._labels = labels
|
|
71
|
+
self._index_by_label = {label: i for i, label in enumerate(labels)}
|
|
72
|
+
self._state_key = state_key
|
|
73
|
+
self._hz = control_hz
|
|
74
|
+
self._move_tool = "move_joints" if absolute else "move_by"
|
|
75
|
+
self._bounds_text = bounds_text
|
|
76
|
+
|
|
77
|
+
# -- schemas ---------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def schemas(self) -> list[dict[str, Any]]:
|
|
80
|
+
"""OpenAI-format tool definitions for this embodiment."""
|
|
81
|
+
if self._absolute:
|
|
82
|
+
move_description = (
|
|
83
|
+
"Move to absolute joint/dimension targets, smoothly interpolated "
|
|
84
|
+
"from the current pose over duration_s. Unnamed dimensions hold "
|
|
85
|
+
"their current value. " + self._bounds_text
|
|
86
|
+
)
|
|
87
|
+
values_key = "targets"
|
|
88
|
+
else:
|
|
89
|
+
move_description = (
|
|
90
|
+
"Move BY the given displacement per dimension, split evenly over "
|
|
91
|
+
"duration_s. Unnamed dimensions do not move. " + self._bounds_text
|
|
92
|
+
)
|
|
93
|
+
values_key = "deltas"
|
|
94
|
+
move = {
|
|
95
|
+
"type": "function",
|
|
96
|
+
"function": {
|
|
97
|
+
"name": self._move_tool,
|
|
98
|
+
"description": move_description,
|
|
99
|
+
"parameters": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
values_key: {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"description": (
|
|
105
|
+
"Map of dimension name to value. Valid names: "
|
|
106
|
+
+ ", ".join(self._labels)
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
"duration_s": {
|
|
110
|
+
"type": "number",
|
|
111
|
+
"description": f"Motion duration in seconds (0 < d <= {_MAX_DURATION_S})",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
"required": [values_key, "duration_s"],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
done = {
|
|
119
|
+
"type": "function",
|
|
120
|
+
"function": {
|
|
121
|
+
"name": "done",
|
|
122
|
+
"description": "Declare the task finished. The trial ends; a scorer judges success.",
|
|
123
|
+
"parameters": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {"summary": {"type": "string"}},
|
|
126
|
+
"required": ["summary"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
give_up = {
|
|
131
|
+
"type": "function",
|
|
132
|
+
"function": {
|
|
133
|
+
"name": "give_up",
|
|
134
|
+
"description": "Stop trying; the task cannot be completed. The trial ends.",
|
|
135
|
+
"parameters": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {"reason": {"type": "string"}},
|
|
138
|
+
"required": ["reason"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
return [move, done, give_up]
|
|
143
|
+
|
|
144
|
+
# -- execution ---------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def execute(self, call: Any, observation: Observation) -> ToolResult:
|
|
147
|
+
"""Turn one tool call into an ActionChunk, or an error string for the LLM."""
|
|
148
|
+
try:
|
|
149
|
+
arguments = json.loads(call.arguments)
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
return ToolResult(error=f"arguments for {call.name} are not valid JSON")
|
|
152
|
+
if not isinstance(arguments, dict):
|
|
153
|
+
return ToolResult(error=f"arguments for {call.name} must be a JSON object")
|
|
154
|
+
if call.name in ("done", "give_up"):
|
|
155
|
+
return self._stop(call.name, arguments, observation)
|
|
156
|
+
if call.name != self._move_tool:
|
|
157
|
+
return ToolResult(
|
|
158
|
+
error=f"unknown tool {call.name!r}; available: {self._move_tool}, done, give_up"
|
|
159
|
+
)
|
|
160
|
+
return self._move(arguments, observation)
|
|
161
|
+
|
|
162
|
+
def _current_state(self, observation: Observation) -> npt.NDArray[np.float64]:
|
|
163
|
+
if self._state_key is None:
|
|
164
|
+
return np.zeros(len(self._labels))
|
|
165
|
+
return np.asarray(observation.state[self._state_key], dtype=np.float64)
|
|
166
|
+
|
|
167
|
+
def _stop(self, name: str, arguments: dict[str, Any], observation: Observation) -> ToolResult:
|
|
168
|
+
# Hold still per control mode: repeat the pose (absolute) or move by
|
|
169
|
+
# nothing (displacement), flagged for rollout's policy-stop channel.
|
|
170
|
+
data = self._current_state(observation) if self._absolute else np.zeros(len(self._labels))
|
|
171
|
+
detail = str(arguments.get("summary") or arguments.get("reason") or "")
|
|
172
|
+
action = Action(
|
|
173
|
+
data=data,
|
|
174
|
+
meta={"request_stop": True, "stop_reason": name, "stop_detail": detail},
|
|
175
|
+
)
|
|
176
|
+
return ToolResult(
|
|
177
|
+
chunk=ActionChunk(actions=[action], control_hz=self._hz),
|
|
178
|
+
note=f"{name}: {detail}",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _move(self, arguments: dict[str, Any], observation: Observation) -> ToolResult:
|
|
182
|
+
values_key = "targets" if self._absolute else "deltas"
|
|
183
|
+
values = arguments.get(values_key)
|
|
184
|
+
duration = arguments.get("duration_s")
|
|
185
|
+
if not isinstance(values, dict) or not values:
|
|
186
|
+
return ToolResult(error=f"{values_key} must be a non-empty object of name: value")
|
|
187
|
+
if not isinstance(duration, (int, float)) or isinstance(duration, bool):
|
|
188
|
+
return ToolResult(error="duration_s must be a number")
|
|
189
|
+
if not 0 < float(duration) <= _MAX_DURATION_S:
|
|
190
|
+
return ToolResult(
|
|
191
|
+
error=f"duration_s must be in (0, {_MAX_DURATION_S}] seconds, got {duration}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
vector = np.zeros(len(self._labels))
|
|
195
|
+
for label, raw in values.items():
|
|
196
|
+
index = self._index_by_label.get(str(label))
|
|
197
|
+
if index is None:
|
|
198
|
+
return ToolResult(
|
|
199
|
+
error=f"unknown dimension {label!r}; valid names: {', '.join(self._labels)}"
|
|
200
|
+
)
|
|
201
|
+
if isinstance(raw, bool) or not isinstance(raw, (int, float)) or not np.isfinite(raw):
|
|
202
|
+
return ToolResult(error=f"value for {label!r} must be a finite number, got {raw!r}")
|
|
203
|
+
vector[index] = float(raw)
|
|
204
|
+
|
|
205
|
+
hz = self._hz if self._hz is not None else _FALLBACK_HZ
|
|
206
|
+
steps = max(1, round(float(duration) * hz))
|
|
207
|
+
if self._absolute:
|
|
208
|
+
current = self._current_state(observation)
|
|
209
|
+
target = current.copy()
|
|
210
|
+
for label in values:
|
|
211
|
+
target[self._index_by_label[str(label)]] = vector[self._index_by_label[str(label)]]
|
|
212
|
+
fractions = np.linspace(1.0 / steps, 1.0, steps)
|
|
213
|
+
actions = [Action(data=current + (target - current) * f) for f in fractions]
|
|
214
|
+
else:
|
|
215
|
+
per_step = vector / steps
|
|
216
|
+
actions = [Action(data=per_step.copy()) for _ in range(steps)]
|
|
217
|
+
return ToolResult(
|
|
218
|
+
chunk=ActionChunk(actions=actions, control_hz=self._hz),
|
|
219
|
+
note=f"executing {self._move_tool} over {steps} steps ({duration}s)",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def build_toolset(
|
|
224
|
+
action_space: Box, observation_space: ObservationSpace, control_hz: float | None
|
|
225
|
+
) -> Toolset:
|
|
226
|
+
"""Validate a (action space, observation space) pairing and build its tools.
|
|
227
|
+
|
|
228
|
+
Raises [`ToolsetError`][inspect_robots_agent._tools.ToolsetError] with a
|
|
229
|
+
clear message for configurations the motion layer cannot drive — at bind
|
|
230
|
+
time, never mid-trial.
|
|
231
|
+
"""
|
|
232
|
+
semantics = action_space.semantics
|
|
233
|
+
if semantics is None:
|
|
234
|
+
raise ToolsetError(
|
|
235
|
+
"the embodiment's action space declares no semantics; the agent cannot "
|
|
236
|
+
"tell absolute targets from displacements"
|
|
237
|
+
)
|
|
238
|
+
mode = semantics.control_mode
|
|
239
|
+
if mode in _POSE_MODES and semantics.rotation_repr not in _SAFE_ROT:
|
|
240
|
+
raise ToolsetError(
|
|
241
|
+
f"rotation_repr {semantics.rotation_repr!r} cannot be driven per-dimension; "
|
|
242
|
+
f"only {sorted(_SAFE_ROT)} are supported"
|
|
243
|
+
)
|
|
244
|
+
if mode not in _ABSOLUTE_MODES | _DISPLACEMENT_MODES:
|
|
245
|
+
raise ToolsetError(f"control_mode {mode!r} is not supported by the agent policy yet")
|
|
246
|
+
|
|
247
|
+
absolute = mode in _ABSOLUTE_MODES
|
|
248
|
+
dim = action_space.dim
|
|
249
|
+
state_key: str | None = None
|
|
250
|
+
if absolute:
|
|
251
|
+
spec = observation_space.state
|
|
252
|
+
if spec is None:
|
|
253
|
+
raise ToolsetError(
|
|
254
|
+
"absolute-target control needs a StateSpec on the embodiment's "
|
|
255
|
+
"observation space to locate the proprioceptive reference"
|
|
256
|
+
)
|
|
257
|
+
matching = [f.key for f in spec.fields if f.shape == (dim,)]
|
|
258
|
+
if len(matching) != 1:
|
|
259
|
+
raise ToolsetError(
|
|
260
|
+
f"absolute-target control needs exactly one state field with shape "
|
|
261
|
+
f"({dim},); found {matching or 'none'}"
|
|
262
|
+
)
|
|
263
|
+
state_key = matching[0]
|
|
264
|
+
|
|
265
|
+
labels = semantics.dim_labels or tuple(str(i) for i in range(dim))
|
|
266
|
+
low, high = action_space.low, action_space.high
|
|
267
|
+
if low is not None and high is not None:
|
|
268
|
+
pairs = ", ".join(
|
|
269
|
+
f"{label}: [{lo:.4g}, {hi:.4g}]"
|
|
270
|
+
for label, lo, hi in zip(labels, low.tolist(), high.tolist())
|
|
271
|
+
)
|
|
272
|
+
bounds_text = f"Per-dimension bounds: {pairs}."
|
|
273
|
+
else:
|
|
274
|
+
bounds_text = "The action space declares no bounds; move conservatively."
|
|
275
|
+
|
|
276
|
+
return Toolset(
|
|
277
|
+
absolute=absolute,
|
|
278
|
+
labels=labels,
|
|
279
|
+
state_key=state_key,
|
|
280
|
+
control_hz=control_hz,
|
|
281
|
+
bounds_text=bounds_text,
|
|
282
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""LLMAgentPolicy — a frontier LLM as a first-class Inspect Robots policy.
|
|
2
|
+
|
|
3
|
+
The conversation loop lives inside ``act()``: observation in (labeled state
|
|
4
|
+
text + camera frames), one validated tool call out, synthesized into an
|
|
5
|
+
open-loop ``ActionChunk`` by the motion layer. The LLM never sees raw
|
|
6
|
+
actuation, and every emitted action still passes the rollout's approver
|
|
7
|
+
chain — this module contains no safety-critical code path of its own
|
|
8
|
+
(plan 0008 §4b).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from inspect_robots.embodiment import EmbodimentInfo
|
|
22
|
+
from inspect_robots.policy import PolicyBase, PolicyConfig, PolicyInfo
|
|
23
|
+
from inspect_robots.scene import Scene
|
|
24
|
+
from inspect_robots.spaces import Box
|
|
25
|
+
from inspect_robots.types import ActionChunk, Observation
|
|
26
|
+
from inspect_robots_agent._llm import ENV_MODEL, ChatClient, ToolCall, resolve_provider
|
|
27
|
+
from inspect_robots_agent._png import png_data_url
|
|
28
|
+
from inspect_robots_agent._tools import Toolset, build_toolset
|
|
29
|
+
|
|
30
|
+
_MAX_CONSECUTIVE_FAILURES = 3
|
|
31
|
+
|
|
32
|
+
_SYSTEM_TEMPLATE = """You are controlling a real robot embodiment named {name!r} \
|
|
33
|
+
through tool calls. Each observation message gives you the current \
|
|
34
|
+
proprioceptive state and camera images. Work toward the user's goal in \
|
|
35
|
+
small, deliberate motions; re-check the observation after every motion. \
|
|
36
|
+
Safety approvers clamp out-of-bounds and too-fast actions below you. \
|
|
37
|
+
Respond with exactly one tool call per turn. When the goal is achieved call \
|
|
38
|
+
done; if it cannot be achieved call give_up. You have a budget of \
|
|
39
|
+
{budget} LLM calls for the whole trial."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class AgentPolicyConfig(PolicyConfig):
|
|
44
|
+
"""Inference-time configuration recorded in the eval log.
|
|
45
|
+
|
|
46
|
+
Extends the core ``PolicyConfig``; ``eval()`` serializes configs with
|
|
47
|
+
``dataclasses.asdict``, so these fields land in ``EvalSpec.policy_config``
|
|
48
|
+
for free.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
model: str | None = None
|
|
52
|
+
base_url: str | None = None
|
|
53
|
+
api_key_env: str | None = None
|
|
54
|
+
max_llm_calls: int = 50
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LLMAgentPolicy(PolicyBase):
|
|
58
|
+
"""Drives whatever embodiment it is bound to via LLM tool calls.
|
|
59
|
+
|
|
60
|
+
Embodiment-adaptive: ``bind()`` (called by ``eval()`` before the
|
|
61
|
+
compatibility check) adopts the embodiment's spaces and builds the tool
|
|
62
|
+
surface from them. Conversation state is per-trial (``reset``).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
model: str | None = None,
|
|
68
|
+
base_url: str | None = None,
|
|
69
|
+
api_key_env: str | None = None,
|
|
70
|
+
max_llm_calls: int = 50,
|
|
71
|
+
temperature: float | None = None,
|
|
72
|
+
transport: httpx.BaseTransport | None = None,
|
|
73
|
+
env: dict[str, str] | None = None,
|
|
74
|
+
):
|
|
75
|
+
environ = dict(os.environ) if env is None else env
|
|
76
|
+
provider = resolve_provider(
|
|
77
|
+
model=model or environ.get(ENV_MODEL),
|
|
78
|
+
base_url=base_url,
|
|
79
|
+
api_key_env=api_key_env,
|
|
80
|
+
env=environ,
|
|
81
|
+
)
|
|
82
|
+
if max_llm_calls < 1:
|
|
83
|
+
raise ValueError("max_llm_calls must be >= 1")
|
|
84
|
+
self._client = ChatClient(provider, transport=transport)
|
|
85
|
+
self._max_llm_calls = max_llm_calls
|
|
86
|
+
self._temperature = temperature
|
|
87
|
+
self.config = AgentPolicyConfig(
|
|
88
|
+
temperature=temperature,
|
|
89
|
+
model=provider.model,
|
|
90
|
+
base_url=provider.base_url,
|
|
91
|
+
api_key_env=api_key_env,
|
|
92
|
+
max_llm_calls=max_llm_calls,
|
|
93
|
+
)
|
|
94
|
+
# Placeholder until bind(); eval() always binds before compat/rollout.
|
|
95
|
+
self.info = PolicyInfo(name="agent", action_space=Box(shape=(1,)))
|
|
96
|
+
self._toolset: Toolset | None = None
|
|
97
|
+
self._embodiment_name = "(unbound)"
|
|
98
|
+
self._messages: list[dict[str, Any]] = []
|
|
99
|
+
self._calls_used = 0
|
|
100
|
+
|
|
101
|
+
# -- lifecycle ---------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def bind(self, embodiment_info: EmbodimentInfo) -> None:
|
|
104
|
+
"""Adopt the embodiment's spaces and build the tool surface from them."""
|
|
105
|
+
self._toolset = build_toolset(
|
|
106
|
+
embodiment_info.action_space,
|
|
107
|
+
embodiment_info.observation_space,
|
|
108
|
+
embodiment_info.control_hz,
|
|
109
|
+
)
|
|
110
|
+
self._embodiment_name = embodiment_info.name
|
|
111
|
+
self.info = PolicyInfo(
|
|
112
|
+
name="agent",
|
|
113
|
+
action_space=embodiment_info.action_space,
|
|
114
|
+
observation_space=embodiment_info.observation_space,
|
|
115
|
+
control_hz=embodiment_info.control_hz,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def reset(self, scene: Scene) -> None:
|
|
119
|
+
self._messages = [
|
|
120
|
+
{
|
|
121
|
+
"role": "system",
|
|
122
|
+
"content": _SYSTEM_TEMPLATE.format(
|
|
123
|
+
name=self._embodiment_name, budget=self._max_llm_calls
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
{"role": "user", "content": f"Goal: {scene.instruction}"},
|
|
127
|
+
]
|
|
128
|
+
self._calls_used = 0
|
|
129
|
+
|
|
130
|
+
# -- the loop ------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def act(self, observation: Observation) -> ActionChunk:
|
|
133
|
+
toolset = self._toolset
|
|
134
|
+
if toolset is None:
|
|
135
|
+
raise RuntimeError(
|
|
136
|
+
"LLMAgentPolicy.act() before bind(); run it through eval() or call "
|
|
137
|
+
"policy.bind(embodiment.info) first"
|
|
138
|
+
)
|
|
139
|
+
self._messages.append({"role": "user", "content": _observation_content(observation)})
|
|
140
|
+
failures = 0
|
|
141
|
+
while True:
|
|
142
|
+
if self._calls_used >= self._max_llm_calls:
|
|
143
|
+
return self._forced_give_up(toolset, observation, "LLM call budget exhausted")
|
|
144
|
+
message = self._client.complete(
|
|
145
|
+
self._messages, toolset.schemas(), temperature=self._temperature
|
|
146
|
+
)
|
|
147
|
+
self._calls_used += 1
|
|
148
|
+
self._messages.append(message.raw())
|
|
149
|
+
|
|
150
|
+
if not message.tool_calls:
|
|
151
|
+
failures += 1
|
|
152
|
+
if failures >= _MAX_CONSECUTIVE_FAILURES:
|
|
153
|
+
raise RuntimeError(f"LLM produced no tool call in {failures} consecutive turns")
|
|
154
|
+
self._messages.append(
|
|
155
|
+
{"role": "user", "content": "Respond with exactly one tool call."}
|
|
156
|
+
)
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
call, *extras = message.tool_calls
|
|
160
|
+
for extra in extras:
|
|
161
|
+
# Every tool_call id needs an answer on the wire; only the
|
|
162
|
+
# first call per turn is executed.
|
|
163
|
+
self._messages.append(
|
|
164
|
+
{
|
|
165
|
+
"role": "tool",
|
|
166
|
+
"tool_call_id": extra.id,
|
|
167
|
+
"content": "ignored: one tool call per turn",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
result = toolset.execute(call, observation)
|
|
171
|
+
self._messages.append(
|
|
172
|
+
{"role": "tool", "tool_call_id": call.id, "content": result.error or result.note}
|
|
173
|
+
)
|
|
174
|
+
if result.error:
|
|
175
|
+
failures += 1
|
|
176
|
+
if failures >= _MAX_CONSECUTIVE_FAILURES:
|
|
177
|
+
raise RuntimeError(f"LLM tool calls kept failing; last error: {result.error}")
|
|
178
|
+
continue
|
|
179
|
+
assert result.chunk is not None # execute() sets exactly one of chunk/error
|
|
180
|
+
return result.chunk
|
|
181
|
+
|
|
182
|
+
def _forced_give_up(self, toolset: Toolset, observation: Observation, why: str) -> ActionChunk:
|
|
183
|
+
synthetic = ToolCall(id="budget", name="give_up", arguments=json.dumps({"reason": why}))
|
|
184
|
+
result = toolset.execute(synthetic, observation)
|
|
185
|
+
assert result.chunk is not None
|
|
186
|
+
return result.chunk
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _observation_content(observation: Observation) -> list[dict[str, Any]]:
|
|
190
|
+
"""State as readable text plus camera frames as inline PNG data URLs."""
|
|
191
|
+
lines = ["Current observation."]
|
|
192
|
+
if observation.instruction:
|
|
193
|
+
lines.append(f"Instruction: {observation.instruction}")
|
|
194
|
+
for key, value in observation.state.items():
|
|
195
|
+
rounded = np.round(np.asarray(value, dtype=np.float64), 4).tolist()
|
|
196
|
+
lines.append(f"state[{key}]: {rounded}")
|
|
197
|
+
parts: list[dict[str, Any]] = [{"type": "text", "text": "\n".join(lines)}]
|
|
198
|
+
for name, image in observation.images.items():
|
|
199
|
+
parts.append({"type": "text", "text": f"camera {name!r}:"})
|
|
200
|
+
parts.append({"type": "image_url", "image_url": {"url": png_data_url(image)}})
|
|
201
|
+
return parts
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def agent_policy(**kwargs: Any) -> LLMAgentPolicy:
|
|
205
|
+
"""Factory the Inspect Robots registry calls (entry point ``agent``).
|
|
206
|
+
|
|
207
|
+
Accepts the same keyword arguments as
|
|
208
|
+
[`LLMAgentPolicy`][inspect_robots_agent.policy.LLMAgentPolicy]; the CLI
|
|
209
|
+
forwards ``-P key=value`` pairs here.
|
|
210
|
+
"""
|
|
211
|
+
return LLMAgentPolicy(**kwargs)
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inspect-robots-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM agent policy for Inspect Robots: frontier LLMs (Claude, GPT, anything OpenAI-compatible) drive any registered embodiment through tool calls.
|
|
5
|
+
Project-URL: Homepage, https://github.com/robocurve/inspect-robots/tree/main/plugins/inspect-robots-agent
|
|
6
|
+
Project-URL: Repository, https://github.com/robocurve/inspect-robots
|
|
7
|
+
Author: Inspect Robots contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: agent,evaluation,inspect_robots,llm,robotics,vla
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: inspect-robots>=0.4
|
|
19
|
+
Requires-Dist: numpy>=1.24
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# inspect-robots-agent
|
|
28
|
+
|
|
29
|
+
LLM agent policy for [Inspect Robots](https://github.com/robocurve/inspect-robots):
|
|
30
|
+
frontier LLMs (Claude, GPT, anything behind an OpenAI-compatible API) drive any
|
|
31
|
+
registered embodiment through tool calls, as a first-class `Policy` named
|
|
32
|
+
`agent`. The same policy runs ad-hoc instructions and scores on registered
|
|
33
|
+
tasks next to fine-tuned VLAs.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install inspect-robots inspect-robots-agent
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart (no hardware)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
45
|
+
|
|
46
|
+
inspect-robots "pick up the cube" --policy agent \
|
|
47
|
+
-P model=anthropic/claude-fable-5 --embodiment cubepick
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Model strings are OpenRouter-style `provider/model`, resolved from
|
|
51
|
+
`-P model=...` or `$INSPECT_ROBOTS_MODEL`. API keys come from the environment:
|
|
52
|
+
|
|
53
|
+
1. `-P base_url=...` (with `-P api_key_env=NAME`): any OpenAI-compatible endpoint
|
|
54
|
+
2. `anthropic/*` model with `ANTHROPIC_API_KEY`: the Anthropic compat endpoint
|
|
55
|
+
3. `openai/*` model with `OPENAI_API_KEY`: OpenAI
|
|
56
|
+
4. `OPENROUTER_API_KEY`: OpenRouter, any model string
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
Each LLM tool call becomes one smooth, open-loop action chunk: named partial
|
|
61
|
+
joint targets are interpolated at the embodiment's control rate
|
|
62
|
+
(`move_joints`), displacements are split across steps (`move_by`), and
|
|
63
|
+
`done`/`give_up` end the trial through the core's policy-stop channel. Every
|
|
64
|
+
action still passes the CLI's default safety approvers (bounds clamp plus
|
|
65
|
+
per-step delta limit); the plugin contains no safety-critical code path of
|
|
66
|
+
its own.
|
|
67
|
+
|
|
68
|
+
> **Warning:**
|
|
69
|
+
> Guardrails are on by default at the CLI. **Never pass `--disable-guardrails`
|
|
70
|
+
> on real hardware** unless you fully trust the policy and the rig.
|
|
71
|
+
|
|
72
|
+
Configuration knobs (all `-P key=value`): `model`, `base_url`, `api_key_env`,
|
|
73
|
+
`max_llm_calls`, `temperature`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
inspect_robots_agent/__init__.py,sha256=VXmY2Xr1USMOve2eqp3EhvOwV1ObhedCW_COezgg-oo,1021
|
|
2
|
+
inspect_robots_agent/_llm.py,sha256=Vm7yWbrCtJ9uQlGFiHQ-d6FfyuaNvBPJ1_VyiaVIJYM,6741
|
|
3
|
+
inspect_robots_agent/_png.py,sha256=ESKxe8iRc982L8Jv_ScZvT5iWNLwQEjE6Sp1sV6_gSs,1648
|
|
4
|
+
inspect_robots_agent/_tools.py,sha256=uoyuF_UgmWfsQid3IQY_rjYSd5BC-psLm6NOEruJ_sM,11555
|
|
5
|
+
inspect_robots_agent/policy.py,sha256=oFUAF4jjPFUd_C0t0-cMwRgN-7b-TRS-DJ19BkcoaWU,8629
|
|
6
|
+
inspect_robots_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
inspect_robots_agent-0.1.0.dist-info/METADATA,sha256=UsVAfnd2g670v9L78c5D89PjQL_QH8QB9jehvRlyhJ4,2934
|
|
8
|
+
inspect_robots_agent-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
9
|
+
inspect_robots_agent-0.1.0.dist-info/entry_points.txt,sha256=o8w6RAsKhdQlvxFprv2PF3tJeLcFg2kOuEMd2rPBp4Y,75
|
|
10
|
+
inspect_robots_agent-0.1.0.dist-info/RECORD,,
|