hud-python 0.3.4__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.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -414
  87. hud/tools/computer/hud.py +376 -328
  88. hud/tools/computer/openai.py +295 -286
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.4.dist-info/METADATA +0 -284
  190. hud_python-0.3.4.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
@@ -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
- )