mirror of
https://github.com/Gen-Verse/OpenClaw-RL.git
synced 2026-08-14 00:47:53 +00:00
135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
"""Async HTTP client for swe_env_pool_server.
|
|
|
|
Used by generate_with_swe_remote.py (inside the RolloutManager) and standalone
|
|
scripts to interact with remote Docker containers via the pool server.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
|
|
class SweEnvClient:
|
|
def __init__(self, base_url: str | None = None):
|
|
self.base_url = (base_url or os.getenv("SWE_ENV_SERVER_URL", "http://localhost:18090")).rstrip("/")
|
|
self.default_max_retries = int(os.getenv("SWE_ENV_HTTP_MAX_RETRIES", "10"))
|
|
self.evaluate_max_retries = int(os.getenv("SWE_EVALUATE_MAX_RETRIES", "3"))
|
|
max_connections = int(os.getenv("SWE_ENV_HTTP_MAX_CONNECTIONS", "128"))
|
|
trust_env = os.getenv("SWE_ENV_HTTP_TRUST_ENV", "0").lower() in ("1", "true", "yes", "on")
|
|
default_timeout = float(os.getenv("SWE_ENV_HTTP_TIMEOUT", "0"))
|
|
self._client = httpx.AsyncClient(
|
|
limits=httpx.Limits(max_connections=max_connections),
|
|
timeout=httpx.Timeout(None) if default_timeout <= 0 else httpx.Timeout(default_timeout),
|
|
trust_env=trust_env,
|
|
)
|
|
|
|
async def _post(self, path: str, payload: dict[str, Any], max_retries: int,
|
|
request_timeout: float | None = None) -> dict[str, Any]:
|
|
url = f"{self.base_url}{path}"
|
|
retry_count = 0
|
|
req_kwargs: dict[str, Any] = {}
|
|
if request_timeout is not None:
|
|
req_kwargs["timeout"] = request_timeout
|
|
while retry_count < max_retries:
|
|
response = None
|
|
try:
|
|
response = await self._client.post(url, json=payload or {}, **req_kwargs)
|
|
response.raise_for_status()
|
|
content = await response.aread()
|
|
try:
|
|
output = json.loads(content)
|
|
except json.JSONDecodeError:
|
|
output = content.decode() if isinstance(content, bytes) else content
|
|
if not isinstance(output, dict):
|
|
raise RuntimeError(f"SWE request returned non-dict payload: {type(output).__name__}")
|
|
return output
|
|
except Exception:
|
|
retry_count += 1
|
|
if retry_count >= max_retries:
|
|
raise
|
|
await asyncio.sleep(1)
|
|
finally:
|
|
if response is not None:
|
|
await response.aclose()
|
|
|
|
raise RuntimeError(f"SWE request failed after retries: {url}")
|
|
|
|
async def allocate(self, image: str, instance_id: str = "") -> dict[str, Any]:
|
|
out = await self._post("/allocate", {"image": image, "instance_id": instance_id}, self.default_max_retries)
|
|
if not out.get("ok", False):
|
|
raise RuntimeError(f"SWE allocate failed: {out}")
|
|
return out
|
|
|
|
async def heartbeat(self, lease_id: str) -> None:
|
|
out = await self._post("/heartbeat", {"lease_id": lease_id}, self.default_max_retries)
|
|
if not out.get("ok", False):
|
|
raise RuntimeError(f"SWE heartbeat failed: {out}")
|
|
|
|
async def exec(
|
|
self,
|
|
lease_id: str,
|
|
command: str,
|
|
cwd: str = "/testbed",
|
|
timeout: int = 180,
|
|
env: dict | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Execute a command in the container. Returns {ok, returncode, output}."""
|
|
out = await self._post(
|
|
"/exec",
|
|
{
|
|
"lease_id": lease_id,
|
|
"command": command,
|
|
"cwd": cwd,
|
|
"timeout": timeout,
|
|
"env": env or {},
|
|
},
|
|
self.default_max_retries,
|
|
)
|
|
if not out.get("ok", False):
|
|
raise RuntimeError(f"SWE exec failed: {out}")
|
|
return out
|
|
|
|
async def diff(self, lease_id: str, cwd: str = "/testbed") -> str:
|
|
"""Get git diff from the container. Returns the patch string."""
|
|
out = await self._post("/diff", {"lease_id": lease_id, "cwd": cwd}, self.default_max_retries)
|
|
if not out.get("ok", False):
|
|
raise RuntimeError(f"SWE diff failed: {out}")
|
|
return out.get("patch", "")
|
|
|
|
async def evaluate(
|
|
self,
|
|
lease_id: str,
|
|
patch: str,
|
|
eval_script: str,
|
|
cwd: str = "/testbed",
|
|
timeout: int = 300,
|
|
) -> dict[str, Any]:
|
|
"""Apply patch + run eval script. Returns {ok, resolved, ...}."""
|
|
http_timeout = timeout + 120
|
|
out = await self._post(
|
|
"/evaluate",
|
|
{
|
|
"lease_id": lease_id,
|
|
"patch": patch,
|
|
"eval_script": eval_script,
|
|
"cwd": cwd,
|
|
"timeout": timeout,
|
|
},
|
|
self.evaluate_max_retries,
|
|
request_timeout=http_timeout,
|
|
)
|
|
if not out.get("ok", False):
|
|
raise RuntimeError(f"SWE evaluate failed: {out}")
|
|
return out
|
|
|
|
async def close(self, lease_id: str) -> None:
|
|
out = await self._post("/close", {"lease_id": lease_id}, self.default_max_retries)
|
|
if not out.get("ok", False):
|
|
raise RuntimeError(f"SWE close failed: {out}")
|
|
return None
|