sql-debug-env / client.py
aakashdandekar's picture
Initial commit of SQL Debug Environment
be62603
Raw History Blame Contribute Delete
5.81 kB
"""
SQL Debug Environment — OpenEnv-compliant async client.
Implements the standard EnvClient interface:
async with SQLDebugEnv(base_url="...") as client:
result = await client.reset()
result = await client.step(SQLFixAction(query="SELECT ..."))
Sync wrapper is also available:
with SQLDebugEnv(base_url="...").sync() as client:
result = client.reset()
result = client.step(SQLFixAction(query="SELECT ..."))
"""
import asyncio
from dataclasses import dataclass
from typing import Optional
import httpx
from models import Action, Observation, State
@dataclass
class StepResult:
"""Returned by step() and reset()."""
observation: Observation
reward: float
done: bool
success: bool
state: Optional[State] = None
class SQLDebugEnv:
"""
Async HTTP client for the SQL Debug Environment.
Compatible with the OpenEnv EnvClient interface.
Usage (async):
async with SQLDebugEnv(base_url="http://localhost:8000") as env:
result = await env.reset(task_group="easy_syntax_fix")
result = await env.step(SQLFixAction(query="SELECT 1"))
Usage (sync):
with SQLDebugEnv(base_url="http://localhost:8000").sync() as env:
result = env.reset()
result = env.step(SQLFixAction(query="SELECT 1"))
"""
def __init__(self, base_url: str = "http://localhost:8000", timeout: float = 30.0):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self._session_id: Optional[str] = None
self._client: Optional[httpx.AsyncClient] = None
#Context Manager
async def __aenter__(self):
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
return self
async def __aexit__(self, *args):
if self._client:
await self._client.aclose()
self._client = None
def sync(self) -> "SyncSQLDebugEnv":
"""Return a synchronous wrapper around this async client."""
return SyncSQLDebugEnv(self)
#Core OpenEnv API
async def reset(
self,
task_group: Optional[str] = None,
task_id: Optional[str] = None,
seed: Optional[int] = None,
) -> StepResult:
"""Initialize or restart an episode."""
payload = {
"session_id": self._session_id,
"task_group": task_group,
"task_id": task_id,
"seed": seed,
}
resp = await self._post("/reset", payload)
self._session_id = resp["session_id"]
obs = Observation(**resp["observation"])
state = State(**resp["state"]) if "state" in resp else None
return StepResult(observation=obs, reward=0.0, done=False, success=False, state=state)
async def step(self, action: Action) -> StepResult:
"""Submit a SQL fix attempt."""
if not self._session_id:
raise RuntimeError("No active session. Call reset() first.")
payload = {
"session_id": self._session_id,
"query": action.query,
"explanation": action.explanation or "",
}
resp = await self._post("/step", payload)
obs = Observation(**resp["observation"])
state = State(**resp["state"]) if "state" in resp else None
return StepResult(
observation=obs,
reward=resp["reward"],
done=resp["done"],
success=resp["success"],
state=state,
)
async def state(self) -> State:
"""Return current episode state without acting."""
if not self._session_id:
raise RuntimeError("No active session. Call reset() first.")
client = self._client or httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
try:
r = await client.get("/state", params={"session_id": self._session_id})
r.raise_for_status()
return State(**r.json())
finally:
if not self._client:
await client.aclose()
async def _post(self, endpoint: str, payload: dict) -> dict:
client = self._client
own_client = False
if client is None:
client = httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
own_client = True
try:
resp = await client.post(endpoint, json=payload)
resp.raise_for_status()
return resp.json()
finally:
if own_client:
await client.aclose()
@classmethod
def from_hub(cls, repo_id: str, port: int = 8000, **kwargs) -> "SQLDebugEnv":
"""
Connect to an environment hosted on Hugging Face Spaces.
Example:
env = SQLDebugEnv.from_hub("openenv/sql-debug-env")
"""
space_name = repo_id.replace("/", "-").lower()
base_url = f"https://{space_name}.hf.space"
return cls(base_url=base_url, **kwargs)
class SyncSQLDebugEnv:
"""Synchronous wrapper around SQLDebugEnv."""
def __init__(self, async_env: SQLDebugEnv):
self._env = async_env
self._loop: Optional[asyncio.AbstractEventLoop] = None
def __enter__(self):
self._loop = asyncio.new_event_loop()
self._loop.run_until_complete(self._env.__aenter__())
return self
def __exit__(self, *args):
self._loop.run_until_complete(self._env.__aexit__(*args))
self._loop.close()
def reset(self, **kwargs) -> StepResult:
return self._loop.run_until_complete(self._env.reset(**kwargs))
def step(self, action: Action) -> StepResult:
return self._loop.run_until_complete(self._env.step(action))
def state(self) -> State:
return self._loop.run_until_complete(self._env.state())
SQLFixAction = Action