bug-triage-env / client.py
Siteshcodes's picture
v2.0: multi-step episodes, procedural bugs, semantic grading, sessions, 71 tests
703aa57
# client.py — Single source of truth for environment client
import os
import requests
from typing import Optional, List
from model import TriageAction, TriageObservation, BugReport
class StepResult:
"""Result returned by env.step()."""
def __init__(self, observation: TriageObservation, reward: float,
done: bool, info: dict):
self.observation = observation
self.reward = reward
self.done = done
self.info = info
def _parse_observation(data: dict) -> TriageObservation:
"""Parse a JSON dict into a TriageObservation."""
bug_data = data["bug_report"]
try:
bug = BugReport.model_validate(bug_data)
except Exception:
bug = BugReport(**bug_data)
return TriageObservation(
bug_report=bug,
task_id=data.get("task_id", "easy"),
score=data.get("score", 0.0),
feedback=data.get("feedback", ""),
done=data.get("done", False),
reward=data.get("reward", 0.0),
body_visible=data.get("body_visible", False),
comments_visible=data.get("comments_visible", False),
logs_visible=data.get("logs_visible", False),
similar_visible=data.get("similar_visible", False),
steps_taken=data.get("steps_taken", 0),
max_steps=data.get("max_steps", 6),
)
class BugTriageClient:
"""HTTP client for the Bug Triage Environment server."""
def __init__(self, base_url: Optional[str] = None):
self.base_url = (
base_url
or os.getenv("ENV_BASE_URL", "https://siteshcodes-bug-triage-env.hf.space")
).rstrip("/")
self.session = requests.Session()
self.session.headers.update({"Content-Type": "application/json"})
self._session_id: Optional[str] = None
@property
def session_id(self) -> Optional[str]:
return self._session_id
def reset(self, task_id: str = "easy", seed: int = None) -> TriageObservation:
"""Start a new episode. Stores session_id for subsequent step() calls."""
payload = {"task_id": task_id}
if seed is not None:
payload["seed"] = seed
if self._session_id:
payload["session_id"] = self._session_id
response = self.session.post(
f"{self.base_url}/reset", json=payload, timeout=30,
)
response.raise_for_status()
data = response.json()
self._session_id = data.get("session_id")
obs_data = data.get("observation", data)
return _parse_observation(obs_data)
def step(self, action: TriageAction) -> StepResult:
"""Send an action (investigation or submit) and get the result."""
try:
action_dict = action.model_dump()
except AttributeError:
action_dict = action.dict()
payload = {"action": action_dict}
if self._session_id:
payload["session_id"] = self._session_id
response = self.session.post(
f"{self.base_url}/step", json=payload, timeout=30,
)
response.raise_for_status()
data = response.json()
obs_data = data.get("observation", data)
obs = _parse_observation(obs_data)
reward = data.get("reward", obs.reward) or 0.0
reward = float(reward)
# Update session_id if server returned one
if "session_id" in data:
self._session_id = data["session_id"]
return StepResult(
observation=obs,
reward=reward,
done=data.get("done", obs.done),
info=data.get("info", {}),
)
def investigate(self, action_type: str) -> StepResult:
"""Shortcut for investigation actions."""
action = TriageAction(action_type=action_type)
return self.step(action)
def submit(self, priority: str, labels: List[str] = None,
assigned_team: str = "backend", milestone: str = "backlog",
reasoning: str = "") -> StepResult:
"""Shortcut for submitting the final triage decision."""
action = TriageAction(
action_type="submit",
priority=priority,
labels=labels or ["bug"],
assigned_team=assigned_team,
milestone=milestone,
reasoning=reasoning,
)
return self.step(action)
def state(self) -> dict:
"""Get current environment state."""
params = {}
if self._session_id:
params["session_id"] = self._session_id
response = self.session.get(
f"{self.base_url}/state", params=params, timeout=30,
)
response.raise_for_status()
return response.json()
def close(self):
self.session.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()