hud-python 0.3.5__py3-none-any.whl → 0.4.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.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -89
- hud/agents/__init__.py +17 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +379 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +354 -0
- hud/clients/fastmcp.py +202 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -420
- hud/tools/computer/hud.py +376 -334
- hud/tools/computer/openai.py +295 -292
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.0.dist-info/METADATA +474 -0
- hud_python-0.4.0.dist-info/RECORD +132 -0
- hud_python-0.4.0.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.5.dist-info/METADATA +0 -284
- hud_python-0.3.5.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
hud/env/local_docker_client.py
DELETED
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import contextlib
|
|
5
|
-
import io
|
|
6
|
-
import logging
|
|
7
|
-
import textwrap
|
|
8
|
-
import time
|
|
9
|
-
import uuid
|
|
10
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
import aiodocker
|
|
14
|
-
from aiohttp import ClientTimeout
|
|
15
|
-
|
|
16
|
-
AIODOCKER_AVAILABLE = True
|
|
17
|
-
except ImportError:
|
|
18
|
-
AIODOCKER_AVAILABLE = False
|
|
19
|
-
aiodocker = None # type: ignore
|
|
20
|
-
ClientTimeout = None # type: ignore
|
|
21
|
-
|
|
22
|
-
from hud.env.docker_client import DockerClient, EnvironmentStatus
|
|
23
|
-
from hud.utils import ExecuteResult
|
|
24
|
-
from hud.utils.common import directory_to_tar_bytes
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from pathlib import Path
|
|
28
|
-
|
|
29
|
-
from aiodocker.containers import DockerContainer
|
|
30
|
-
from aiodocker.stream import Stream
|
|
31
|
-
|
|
32
|
-
logger = logging.getLogger(__name__)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class LocalDockerClient(DockerClient):
|
|
36
|
-
"""
|
|
37
|
-
Docker-based environment client implementation.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
@classmethod
|
|
41
|
-
async def build_image(cls, build_context: Path) -> tuple[str, dict[str, Any]]:
|
|
42
|
-
"""
|
|
43
|
-
Build an image from a build context.
|
|
44
|
-
"""
|
|
45
|
-
logger.info("Building image from %s", build_context)
|
|
46
|
-
# Create a unique image tag
|
|
47
|
-
image_tag = f"hud-env-{uuid.uuid4().hex[:8]}"
|
|
48
|
-
|
|
49
|
-
# Initialize Docker client
|
|
50
|
-
if not AIODOCKER_AVAILABLE:
|
|
51
|
-
raise ImportError(
|
|
52
|
-
"aiodocker is required for LocalDockerClient. "
|
|
53
|
-
"Please install it with 'pip install aiodocker'"
|
|
54
|
-
)
|
|
55
|
-
docker_client = aiodocker.Docker() # type: ignore
|
|
56
|
-
|
|
57
|
-
# Create a tar file from the path
|
|
58
|
-
tar_bytes = directory_to_tar_bytes(build_context)
|
|
59
|
-
logger.info("generated tar file with size: %d KB", len(tar_bytes) // 1024)
|
|
60
|
-
|
|
61
|
-
# Build the image
|
|
62
|
-
build_stream = await docker_client.images.build(
|
|
63
|
-
fileobj=io.BytesIO(tar_bytes),
|
|
64
|
-
encoding="gzip",
|
|
65
|
-
tag=image_tag,
|
|
66
|
-
rm=True,
|
|
67
|
-
pull=True,
|
|
68
|
-
forcerm=True,
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
# Print build output
|
|
72
|
-
output = ""
|
|
73
|
-
for chunk in build_stream:
|
|
74
|
-
if "stream" in chunk:
|
|
75
|
-
logger.info(chunk["stream"])
|
|
76
|
-
output += chunk["stream"]
|
|
77
|
-
|
|
78
|
-
return image_tag, {"build_output": output}
|
|
79
|
-
|
|
80
|
-
@classmethod
|
|
81
|
-
async def create(
|
|
82
|
-
cls,
|
|
83
|
-
image: str,
|
|
84
|
-
host_config: dict[str, Any] | None = None,
|
|
85
|
-
) -> LocalDockerClient:
|
|
86
|
-
"""
|
|
87
|
-
Creates a Docker environment client from a image.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
image: The image to build the Docker image
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
DockerClient: An instance of the Docker environment client
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
# Initialize Docker client
|
|
97
|
-
if not AIODOCKER_AVAILABLE:
|
|
98
|
-
raise ImportError(
|
|
99
|
-
"aiodocker is required for LocalDockerClient. "
|
|
100
|
-
"Please install it with 'pip install aiodocker'"
|
|
101
|
-
)
|
|
102
|
-
docker_client = aiodocker.Docker() # type: ignore
|
|
103
|
-
|
|
104
|
-
# Default host config
|
|
105
|
-
if host_config is None:
|
|
106
|
-
host_config = {
|
|
107
|
-
"PublishAllPorts": True,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
# Create and start the container
|
|
111
|
-
container_config = {
|
|
112
|
-
"Image": image,
|
|
113
|
-
"Tty": True,
|
|
114
|
-
"OpenStdin": True,
|
|
115
|
-
"Cmd": None,
|
|
116
|
-
"HostConfig": host_config,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
container = await docker_client.containers.create(config=container_config)
|
|
120
|
-
await container.start()
|
|
121
|
-
|
|
122
|
-
# --------------------------------------------------
|
|
123
|
-
# Stream container logs while we wait for readiness
|
|
124
|
-
# --------------------------------------------------
|
|
125
|
-
async def _stream_logs() -> None:
|
|
126
|
-
try:
|
|
127
|
-
# .log() with follow=True -> async iterator of bytes/str
|
|
128
|
-
async for raw in container.log(stdout=True, stderr=True, follow=True):
|
|
129
|
-
if isinstance(raw, bytes):
|
|
130
|
-
raw = raw.decode(errors="replace")
|
|
131
|
-
logger.info("container %s | %s", container.id[:12], raw.rstrip())
|
|
132
|
-
except asyncio.CancelledError:
|
|
133
|
-
# task cancelled during cleanup - silently exit
|
|
134
|
-
return
|
|
135
|
-
except Exception:
|
|
136
|
-
logger.exception("error while streaming logs from %s", container.id[:12])
|
|
137
|
-
|
|
138
|
-
log_task: asyncio.Task | None = asyncio.create_task(_stream_logs())
|
|
139
|
-
|
|
140
|
-
inspection = await container.show()
|
|
141
|
-
if health_check_config := inspection["Config"].get("Healthcheck"):
|
|
142
|
-
# Using the interval as spinup deadline is a bit implicit - could
|
|
143
|
-
# consider adding explicitly to API if there's demand
|
|
144
|
-
window_usecs = health_check_config.get("Interval", int(30 * 1e9))
|
|
145
|
-
window_secs = window_usecs // 1_000_000
|
|
146
|
-
|
|
147
|
-
deadline = time.monotonic() + window_secs
|
|
148
|
-
logger.debug("Waiting for container %s to become healthy", container.id)
|
|
149
|
-
while True:
|
|
150
|
-
state = (await container.show())["State"]
|
|
151
|
-
if state.get("Health", {}).get("Status") == "healthy":
|
|
152
|
-
break
|
|
153
|
-
if state.get("Status") in {"exited", "dead"}:
|
|
154
|
-
raise RuntimeError("Container crashed before becoming healthy")
|
|
155
|
-
now = time.monotonic()
|
|
156
|
-
if now > deadline:
|
|
157
|
-
raise TimeoutError(f"{container.id} not healthy after {window_secs}s")
|
|
158
|
-
await asyncio.sleep(1)
|
|
159
|
-
logger.debug("Container %s is healthy", container.id)
|
|
160
|
-
else:
|
|
161
|
-
logger.debug("Container %s has no healthcheck, assuming ready", container.id)
|
|
162
|
-
|
|
163
|
-
# Stop the log stream now that the container is ready
|
|
164
|
-
if log_task is not None:
|
|
165
|
-
log_task.cancel()
|
|
166
|
-
with contextlib.suppress(Exception):
|
|
167
|
-
await log_task
|
|
168
|
-
log_task = None
|
|
169
|
-
|
|
170
|
-
# Return the controller instance
|
|
171
|
-
client = cls(docker_client, container.id)
|
|
172
|
-
# store the task so close() can cancel if it is still running
|
|
173
|
-
client._log_task = log_task # type: ignore[attr-defined]
|
|
174
|
-
return client
|
|
175
|
-
|
|
176
|
-
def __init__(self, docker_conn: aiodocker.Docker, container_id: str) -> None: # type: ignore
|
|
177
|
-
"""
|
|
178
|
-
Initialize the DockerClient.
|
|
179
|
-
|
|
180
|
-
Args:
|
|
181
|
-
docker_conn: Docker client connection
|
|
182
|
-
container_id: ID of the Docker container to control
|
|
183
|
-
"""
|
|
184
|
-
if not AIODOCKER_AVAILABLE:
|
|
185
|
-
raise ImportError(
|
|
186
|
-
"aiodocker is required for LocalDockerClient. "
|
|
187
|
-
"Please install it with 'pip install aiodocker'"
|
|
188
|
-
)
|
|
189
|
-
super().__init__()
|
|
190
|
-
|
|
191
|
-
# Store container ID instead of container object
|
|
192
|
-
self._container_id = container_id
|
|
193
|
-
|
|
194
|
-
# Docker client will be initialized when needed
|
|
195
|
-
self._docker = docker_conn
|
|
196
|
-
|
|
197
|
-
# Background task for streaming logs (may be None)
|
|
198
|
-
self._log_task: asyncio.Task | None = None
|
|
199
|
-
|
|
200
|
-
@property
|
|
201
|
-
def container_id(self) -> str:
|
|
202
|
-
"""Get the container ID."""
|
|
203
|
-
return self._container_id
|
|
204
|
-
|
|
205
|
-
@container_id.setter
|
|
206
|
-
def container_id(self, value: str) -> None:
|
|
207
|
-
"""Set the container ID."""
|
|
208
|
-
self._container_id = value
|
|
209
|
-
|
|
210
|
-
async def _get_container(self) -> DockerContainer:
|
|
211
|
-
"""Get the container object from aiodocker."""
|
|
212
|
-
return await self._docker.containers.get(self.container_id)
|
|
213
|
-
|
|
214
|
-
async def get_status(self) -> EnvironmentStatus:
|
|
215
|
-
"""
|
|
216
|
-
Get the current status of the Docker environment.
|
|
217
|
-
|
|
218
|
-
Returns:
|
|
219
|
-
EnvironmentStatus: The current status of the environment
|
|
220
|
-
"""
|
|
221
|
-
try:
|
|
222
|
-
container = await self._get_container()
|
|
223
|
-
container_data = await container.show()
|
|
224
|
-
|
|
225
|
-
# Check the container state
|
|
226
|
-
state = container_data.get("State", {})
|
|
227
|
-
status = state.get("Status", "").lower()
|
|
228
|
-
|
|
229
|
-
if status == "running":
|
|
230
|
-
return EnvironmentStatus.RUNNING
|
|
231
|
-
elif status == "created" or status == "starting":
|
|
232
|
-
return EnvironmentStatus.INITIALIZING
|
|
233
|
-
elif status in ["exited", "dead", "removing", "paused"]:
|
|
234
|
-
return EnvironmentStatus.COMPLETED
|
|
235
|
-
else:
|
|
236
|
-
# Any other state is considered an error
|
|
237
|
-
return EnvironmentStatus.ERROR
|
|
238
|
-
|
|
239
|
-
except Exception:
|
|
240
|
-
# If we can't connect to the container or there's any other error
|
|
241
|
-
return EnvironmentStatus.ERROR
|
|
242
|
-
|
|
243
|
-
async def execute(
|
|
244
|
-
self,
|
|
245
|
-
command: list[str],
|
|
246
|
-
*,
|
|
247
|
-
timeout: int | None = None, # noqa: ASYNC109
|
|
248
|
-
) -> ExecuteResult:
|
|
249
|
-
"""
|
|
250
|
-
Execute a command in the container.
|
|
251
|
-
|
|
252
|
-
Args:
|
|
253
|
-
command: Command to execute
|
|
254
|
-
workdir: Working directory for the command
|
|
255
|
-
|
|
256
|
-
Returns:
|
|
257
|
-
ExecuteResult: Result of the command execution
|
|
258
|
-
"""
|
|
259
|
-
container = await self._get_container()
|
|
260
|
-
|
|
261
|
-
exec_result = await container.exec(
|
|
262
|
-
cmd=command,
|
|
263
|
-
)
|
|
264
|
-
output: Stream = exec_result.start(timeout=ClientTimeout(timeout), detach=False) # type: ignore
|
|
265
|
-
|
|
266
|
-
stdout_data = bytearray()
|
|
267
|
-
stderr_data = bytearray()
|
|
268
|
-
|
|
269
|
-
while True:
|
|
270
|
-
message = await output.read_out()
|
|
271
|
-
if message is None:
|
|
272
|
-
break
|
|
273
|
-
if message.stream == 1: # stdout
|
|
274
|
-
stdout_data.extend(message.data)
|
|
275
|
-
elif message.stream == 2: # stderr
|
|
276
|
-
stderr_data.extend(message.data)
|
|
277
|
-
|
|
278
|
-
if "No module named 'hud_controller'" in stderr_data.decode():
|
|
279
|
-
if self._source_path is None:
|
|
280
|
-
message = textwrap.dedent("""\
|
|
281
|
-
Your environment is not set up correctly.
|
|
282
|
-
You are using a prebuilt image, so please ensure the following:
|
|
283
|
-
1. Your image cannot be a generic python image, it must contain a python package
|
|
284
|
-
called hud_controller.
|
|
285
|
-
""")
|
|
286
|
-
else:
|
|
287
|
-
message = textwrap.dedent("""\
|
|
288
|
-
Your environment is not set up correctly.
|
|
289
|
-
You are using a local controller, so please ensure the following:
|
|
290
|
-
1. Your package name is hud_controller
|
|
291
|
-
2. You installed the package in the Dockerfile.
|
|
292
|
-
3. The package is visible from the global python environment (no venv, conda, or uv)
|
|
293
|
-
""")
|
|
294
|
-
logger.error(message)
|
|
295
|
-
|
|
296
|
-
return ExecuteResult(
|
|
297
|
-
stdout=bytes(stdout_data),
|
|
298
|
-
stderr=bytes(stderr_data),
|
|
299
|
-
# TODO: Get the exit code from the output
|
|
300
|
-
exit_code=0,
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
async def get_archive(self, path: str) -> bytes:
|
|
304
|
-
"""
|
|
305
|
-
Get an archive of a path from the container.
|
|
306
|
-
|
|
307
|
-
Args:
|
|
308
|
-
path: Path in the container to archive
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
bytes: Tar archive containing the path contents
|
|
312
|
-
"""
|
|
313
|
-
container = await self._get_container()
|
|
314
|
-
|
|
315
|
-
tarfile = await container.get_archive(path)
|
|
316
|
-
# we know tarfile has fileobj BytesIO
|
|
317
|
-
# read the tarfile into a bytes object
|
|
318
|
-
fileobj = tarfile.fileobj
|
|
319
|
-
if not isinstance(fileobj, io.BytesIO):
|
|
320
|
-
raise TypeError("fileobj is not a BytesIO object")
|
|
321
|
-
return fileobj.getvalue()
|
|
322
|
-
|
|
323
|
-
async def put_archive(self, path: str, data: bytes) -> None:
|
|
324
|
-
"""
|
|
325
|
-
Put an archive of data at a path in the container.
|
|
326
|
-
|
|
327
|
-
Args:
|
|
328
|
-
path: Path in the container to extract the archive to
|
|
329
|
-
data: Bytes of the tar archive to extract
|
|
330
|
-
|
|
331
|
-
Returns:
|
|
332
|
-
bool: True if successful
|
|
333
|
-
"""
|
|
334
|
-
container = await self._get_container()
|
|
335
|
-
|
|
336
|
-
# Convert bytes to a file-like object for aiodocker
|
|
337
|
-
file_obj = io.BytesIO(data)
|
|
338
|
-
await container.put_archive(path=path, data=file_obj)
|
|
339
|
-
|
|
340
|
-
async def close(self) -> None:
|
|
341
|
-
"""
|
|
342
|
-
Close the Docker environment by stopping and removing the container.
|
|
343
|
-
"""
|
|
344
|
-
try:
|
|
345
|
-
container = await self._get_container()
|
|
346
|
-
await container.stop()
|
|
347
|
-
await container.delete()
|
|
348
|
-
except Exception as e:
|
|
349
|
-
# Log the error but don't raise it since this is cleanup
|
|
350
|
-
logger.warning("Error during Docker container cleanup: %s", e)
|
|
351
|
-
finally:
|
|
352
|
-
await self._docker.close()
|
|
353
|
-
|
|
354
|
-
# Cancel background log forwarding first (if still active)
|
|
355
|
-
if self._log_task is not None:
|
|
356
|
-
self._log_task.cancel()
|
|
357
|
-
with contextlib.suppress(Exception):
|
|
358
|
-
await self._log_task
|
hud/env/remote_client.py
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from base64 import b64decode
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
-
|
|
9
|
-
from hud.env.client import Client
|
|
10
|
-
from hud.exceptions import HudResponseError
|
|
11
|
-
from hud.server import make_request
|
|
12
|
-
from hud.settings import settings
|
|
13
|
-
from hud.types import EnvironmentStatus
|
|
14
|
-
from hud.utils import ExecuteResult
|
|
15
|
-
from hud.utils.config import FunctionConfig
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger("hud.env.remote_env_client")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class SetupRequest(BaseModel):
|
|
21
|
-
task_id: str | None = None
|
|
22
|
-
setup: FunctionConfig | None = None
|
|
23
|
-
config: dict[str, Any] | None = None
|
|
24
|
-
metadata: dict[str, Any] | None = None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class RemoteClient(Client):
|
|
28
|
-
"""
|
|
29
|
-
Remote environment client implementation.
|
|
30
|
-
|
|
31
|
-
Uses the HUD API to manage a remote environment.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
@classmethod
|
|
35
|
-
async def create(
|
|
36
|
-
cls,
|
|
37
|
-
*,
|
|
38
|
-
gym_id: str | None = None,
|
|
39
|
-
job_id: str | None = None,
|
|
40
|
-
task_id: str | None = None,
|
|
41
|
-
metadata: dict[str, Any] | None = None,
|
|
42
|
-
) -> tuple[RemoteClient, dict[str, Any]]:
|
|
43
|
-
"""
|
|
44
|
-
Creates a remote environment client from a dockerfile or gym_id.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
dockerfile: The dockerfile content to build the environment
|
|
48
|
-
gym_id: The gym_id of the environment to create
|
|
49
|
-
metadata: Metadata to associate with the environment
|
|
50
|
-
|
|
51
|
-
Returns:
|
|
52
|
-
A tuple containing the remote environment client and the build metadata
|
|
53
|
-
|
|
54
|
-
Raises:
|
|
55
|
-
HudResponseError: If the environment creation is successful but the response is invalid.
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
# Validate arguments
|
|
59
|
-
if metadata is None:
|
|
60
|
-
metadata = {}
|
|
61
|
-
|
|
62
|
-
request_data = {
|
|
63
|
-
# still named run_id for backwards compatibility
|
|
64
|
-
"run_id": job_id,
|
|
65
|
-
"metadata": metadata,
|
|
66
|
-
"gym_id": gym_id,
|
|
67
|
-
"task_id": task_id,
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
# Create a new environment via the HUD API
|
|
71
|
-
response = await make_request(
|
|
72
|
-
method="POST",
|
|
73
|
-
url=f"{settings.base_url}/v2/create_environment",
|
|
74
|
-
json=request_data,
|
|
75
|
-
api_key=settings.api_key,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
# Get the environment ID from the response
|
|
79
|
-
env_id = response.get("id")
|
|
80
|
-
if not env_id:
|
|
81
|
-
raise HudResponseError(
|
|
82
|
-
message="Failed to create remote environment: No ID returned in API response. "
|
|
83
|
-
"Please contact support if this issue persists.",
|
|
84
|
-
response_json=response,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Create the controller instance
|
|
88
|
-
controller = cls(env_id)
|
|
89
|
-
|
|
90
|
-
build_data = response.get("metadata", {})
|
|
91
|
-
|
|
92
|
-
if response.get("readme"):
|
|
93
|
-
logger.info("Gym created, see how to use it at %s", response.get("readme"))
|
|
94
|
-
|
|
95
|
-
return controller, build_data
|
|
96
|
-
|
|
97
|
-
def __init__(self, env_id: str) -> None:
|
|
98
|
-
"""
|
|
99
|
-
Initialize the RemoteClient.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
env_id: ID of the remote environment to control
|
|
103
|
-
"""
|
|
104
|
-
super().__init__()
|
|
105
|
-
self._env_id = env_id
|
|
106
|
-
|
|
107
|
-
@property
|
|
108
|
-
def env_id(self) -> str:
|
|
109
|
-
"""The ID of the remote environment."""
|
|
110
|
-
return self._env_id
|
|
111
|
-
|
|
112
|
-
async def get_status(self) -> EnvironmentStatus:
|
|
113
|
-
"""
|
|
114
|
-
Get the current status of the remote environment.
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
EnvironmentStatus: The current status of the environment
|
|
118
|
-
"""
|
|
119
|
-
try:
|
|
120
|
-
response = await make_request(
|
|
121
|
-
method="GET",
|
|
122
|
-
url=f"{settings.base_url}/v2/environments/{self.env_id}/state",
|
|
123
|
-
api_key=settings.api_key,
|
|
124
|
-
)
|
|
125
|
-
logger.debug("Environment status response: %s", response)
|
|
126
|
-
|
|
127
|
-
status = response.get("state", "").lower()
|
|
128
|
-
|
|
129
|
-
if status == "running":
|
|
130
|
-
return EnvironmentStatus.RUNNING
|
|
131
|
-
elif status == "initializing" or status == "pending":
|
|
132
|
-
return EnvironmentStatus.INITIALIZING
|
|
133
|
-
elif status == "completed" or status == "terminated":
|
|
134
|
-
return EnvironmentStatus.COMPLETED
|
|
135
|
-
else:
|
|
136
|
-
# Any other status is considered an error
|
|
137
|
-
logger.warning("Abnormal environment status response: %s", response)
|
|
138
|
-
return EnvironmentStatus.ERROR
|
|
139
|
-
|
|
140
|
-
except Exception:
|
|
141
|
-
# If we can't connect to the API or there's any other error
|
|
142
|
-
logger.info("(potentially transient) Error getting environment status")
|
|
143
|
-
return EnvironmentStatus.ERROR
|
|
144
|
-
|
|
145
|
-
async def execute(
|
|
146
|
-
self,
|
|
147
|
-
command: list[str],
|
|
148
|
-
*,
|
|
149
|
-
workdir: str | None = None,
|
|
150
|
-
timeout: float | None = None, # noqa: ASYNC109
|
|
151
|
-
) -> ExecuteResult:
|
|
152
|
-
"""
|
|
153
|
-
Execute a command in the environment.
|
|
154
|
-
No-op in some environments (like browser use).
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
command: Command to execute
|
|
158
|
-
workdir: Working directory for the command (ignored for remote environments)
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
ExecuteResult: Result of the command execution
|
|
162
|
-
"""
|
|
163
|
-
data = await make_request(
|
|
164
|
-
method="POST",
|
|
165
|
-
url=f"{settings.base_url}/v2/environments/{self.env_id}/execute",
|
|
166
|
-
json={
|
|
167
|
-
"command": command,
|
|
168
|
-
"workdir": workdir,
|
|
169
|
-
"timeout": timeout,
|
|
170
|
-
},
|
|
171
|
-
api_key=settings.api_key,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
return ExecuteResult(
|
|
175
|
-
stdout=b64decode(data["stdout"]),
|
|
176
|
-
stderr=b64decode(data["stderr"]),
|
|
177
|
-
exit_code=data["exit_code"],
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
async def invoke(self, config: FunctionConfig) -> tuple[Any, bytes, bytes]:
|
|
181
|
-
"""
|
|
182
|
-
Invoke a function in the environment.
|
|
183
|
-
"""
|
|
184
|
-
data = await make_request(
|
|
185
|
-
method="POST",
|
|
186
|
-
url=f"{settings.base_url}/v2/environments/{self.env_id}/invoke",
|
|
187
|
-
json=config.model_dump(),
|
|
188
|
-
api_key=settings.api_key,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
return data["result"], b64decode(data["stdout"]), b64decode(data["stderr"])
|
|
192
|
-
|
|
193
|
-
async def setup(self, setup_request: SetupRequest) -> dict[str, Any]:
|
|
194
|
-
"""
|
|
195
|
-
Setup the environment.
|
|
196
|
-
"""
|
|
197
|
-
return await make_request(
|
|
198
|
-
method="POST",
|
|
199
|
-
url=f"{settings.base_url}/v1/environments/{self.env_id}/reset",
|
|
200
|
-
json=setup_request.model_dump(),
|
|
201
|
-
api_key=settings.api_key,
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
async def close(self) -> None:
|
|
205
|
-
"""
|
|
206
|
-
Close the remote environment by making a request to the server.
|
|
207
|
-
"""
|
|
208
|
-
await make_request(
|
|
209
|
-
method="POST",
|
|
210
|
-
url=f"{settings.base_url}/v2/environments/{self.env_id}/close",
|
|
211
|
-
api_key=settings.api_key,
|
|
212
|
-
)
|