""" Sprint Manager — OpenEnv Client This is what RL researchers import in their training code. It provides a clean typed interface to the environment server. Usage: import requests from client import SprintEnvClient, SprintAction client = SprintEnvClient(base_url="https://sejal-k-ai-sprint-manager.hf.space") obs = client.reset(task_name="easy_sprint") result = client.step(SprintAction(action_type="assign", task_id="T1", dev_id="dev1")) state = client.state() client.close() # Or as context manager: with SprintEnvClient(base_url="http://localhost:7860") as client: obs = client.reset(task_name="medium_sprint", seed=42) while not obs["done"]: result = client.step(SprintAction(action_type="skip")) obs = result["observation"] """ from __future__ import annotations import requests from typing import Optional, Any from sprint_env.models import SprintAction class StepResult: """Typed result from a step() call.""" def __init__(self, payload: dict): self.observation: dict = payload["observation"] self.reward: float = payload["reward"] self.done: bool = payload["done"] self.info: dict = payload.get("info", {}) def __repr__(self): return ( f"StepResult(reward={self.reward:+.2f}, done={self.done}, " f"day={self.observation.get('current_day')}, " f"completed={self.observation.get('tasks_completed')})" ) class SprintEnvClient: """ HTTP client for the Sprint Manager OpenEnv environment. Wraps the REST API into a clean Python interface. Use this in RL training loops, notebooks, or evaluation scripts. """ def __init__(self, base_url: str = "http://localhost:7860", timeout: int = 30): self.base_url = base_url.rstrip("/") self.timeout = timeout self._session = requests.Session() def reset( self, task_name: str = "easy_sprint", seed: Optional[int] = None, episode_id: Optional[str] = None, ) -> dict: """ Reset the environment and return initial observation. Args: task_name: One of "easy_sprint", "medium_sprint", "hard_sprint" seed: Random seed for reproducibility episode_id: Optional episode identifier Returns: Observation dict """ payload = {"task_name": task_name} if seed is not None: payload["seed"] = seed if episode_id is not None: payload["episode_id"] = episode_id resp = self._session.post( f"{self.base_url}/reset", json=payload, timeout=self.timeout ) resp.raise_for_status() return resp.json() def step(self, action: SprintAction) -> StepResult: """ Take one action and advance the sprint by one day. Args: action: SprintAction with action_type, task_id, dev_id, new_priority Returns: StepResult with observation, reward, done, info """ payload = {"action": action.model_dump()} resp = self._session.post( f"{self.base_url}/step", json=payload, timeout=self.timeout ) resp.raise_for_status() return StepResult(resp.json()) def state(self) -> dict: """Return the full current environment state.""" resp = self._session.get(f"{self.base_url}/state", timeout=self.timeout) resp.raise_for_status() return resp.json() def health(self) -> dict: """Check server health.""" resp = self._session.get(f"{self.base_url}/health", timeout=self.timeout) resp.raise_for_status() return resp.json() def list_tasks(self) -> list[dict]: """List all available sprint scenarios.""" resp = self._session.get(f"{self.base_url}/tasks", timeout=self.timeout) resp.raise_for_status() return resp.json()["tasks"] def close(self): """Close the HTTP session.""" self._session.close() def __enter__(self): return self def __exit__(self, *args): self.close() def __repr__(self): return f"SprintEnvClient(base_url='{self.base_url}')"