trial1 / project_client.py
priyaaaaaasharmaaaaa's picture
Upload trained LoRA adapter from Kaggle
35e9f20 verified
Raw
History Blame Contribute Delete
10.4 kB
"""
Project Manager β€” OpenEnv Client (Round 2)
Typed HTTP client for the multi-sprint /project/* endpoints.
Mirrors the R1 SprintEnvClient interface exactly, extended for R2.
Usage:
from project_client import ProjectEnvClient
client = ProjectEnvClient(base_url="https://sejal-k-ai-sprint-manager.hf.space")
obs = client.reset(task_name="project_easy", seed=42)
while not obs["done"]:
result = client.step({"action_type": "assign", "task_id": "T01", "dev_id": "dev1"})
obs = result.observation
client.close()
# Or as context manager:
with ProjectEnvClient() as client:
obs = client.reset("project_hard")
while not obs["done"]:
result = client.step({"action_type": "skip"})
obs = result.observation
"""
from __future__ import annotations
import requests
from typing import Any, Optional
# ── Typed step result ──────────────────────────────────────────────────────────
class ProjectStepResult:
"""
Typed result from a project step() call.
Extends R1 StepResult with R2-specific fields surfaced as properties.
"""
def __init__(self, payload: dict) -> None:
self.observation: dict = payload["observation"]
self.reward: float = payload["reward"]
self.done: bool = payload["done"]
self.info: dict = payload.get("info", {})
# ── R2 convenience accessors ──────────────────────────────────────────────
@property
def current_sprint(self) -> int:
return self.observation.get("current_sprint", 1)
@property
def current_day(self) -> int:
return self.observation.get("current_day", 1)
@property
def instruction_queue(self) -> list[dict]:
"""All instructions released up to current_day."""
return self.observation.get("instruction_queue", [])
@property
def active_instructions(self) -> list[dict]:
"""Released instructions not yet followed."""
return [i for i in self.instruction_queue if not i.get("followed", False)]
@property
def tech_debt(self) -> list[str]:
"""Task IDs that became tech debt at sprint boundaries."""
return self.observation.get("tech_debt", [])
@property
def sprint_rewards(self) -> list[float]:
"""Per-sprint reward history."""
return self.observation.get("sprint_rewards", [])
@property
def instruction_following_score(self) -> float:
return self.observation.get("instruction_following_score", 1.0)
@property
def tasks_completed(self) -> int:
return self.observation.get("tasks_completed", 0)
@property
def tasks_missed(self) -> int:
return self.observation.get("tasks_missed", 0)
@property
def cumulative_reward(self) -> float:
return self.observation.get("cumulative_reward", 0.0)
def __repr__(self) -> str:
return (
f"ProjectStepResult("
f"reward={self.reward:+.3f}, done={self.done}, "
f"day={self.current_day}/60, sprint={self.current_sprint}/6, "
f"completed={self.tasks_completed}, "
f"inst_score={self.instruction_following_score:.2f}, "
f"debt={len(self.tech_debt)})"
)
# ── Client ─────────────────────────────────────────────────────────────────────
class ProjectEnvClient:
"""
HTTP client for the R2 multi-sprint Project Manager environment.
Wraps the /project/* REST API into a clean typed Python interface.
Use this in RL training loops (train_llm.py), evaluation scripts
(evaluate_r2.py), and notebooks.
All endpoints mirror the R1 SprintEnvClient interface so training
code can swap between R1 and R2 with minimal changes.
"""
def __init__(
self,
base_url: str = "http://localhost:7860",
timeout: int = 60, # 60s β€” R2 steps can be slower than R1
) -> None:
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self._session = requests.Session()
self._prefix = "/project"
# ── Core API ──────────────────────────────────────────────────────────────
def reset(
self,
task_name: str = "project_easy",
seed: Optional[int] = None,
episode_id: Optional[str] = None,
) -> dict:
"""
Start a new 60-day multi-sprint episode.
Args:
task_name: "project_easy" | "project_medium" | "project_hard"
seed: Random seed for reproducibility
episode_id: Optional custom episode identifier
Returns:
Initial observation dict (includes current_sprint, instruction_queue,
tech_debt, sprint_rewards).
"""
payload: dict[str, Any] = {"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}{self._prefix}/reset",
json=payload,
timeout=self.timeout,
)
resp.raise_for_status()
return resp.json()
def step(self, action: dict[str, Any]) -> ProjectStepResult:
"""
Take one action and advance the project by one day.
Args:
action: dict with keys: action_type, task_id, dev_id,
new_priority, task_ids (for sprint_plan), notes.
Minimum required: {"action_type": "skip"}
Returns:
ProjectStepResult with observation, reward, done, info,
plus R2 convenience properties.
"""
payload = {"action": action}
resp = self._session.post(
f"{self.base_url}{self._prefix}/step",
json=payload,
timeout=self.timeout,
)
resp.raise_for_status()
return ProjectStepResult(resp.json())
def state(self) -> dict:
"""Return the full current internal state snapshot."""
resp = self._session.get(
f"{self.base_url}{self._prefix}/state",
timeout=self.timeout,
)
resp.raise_for_status()
return resp.json()
def health(self) -> dict:
"""
Check R2 server health.
Returns round=2 and current episode summary.
"""
resp = self._session.get(
f"{self.base_url}{self._prefix}/health",
timeout=self.timeout,
)
resp.raise_for_status()
return resp.json()
def list_tasks(self) -> list[dict]:
"""List all available R2 multi-sprint scenarios."""
resp = self._session.get(
f"{self.base_url}{self._prefix}/tasks",
timeout=self.timeout,
)
resp.raise_for_status()
return resp.json()["tasks"]
# ── Helpers ───────────────────────────────────────────────────────────────
def run_episode(
self,
task_name: str,
policy_fn, # Callable[[dict], dict]
seed: Optional[int] = None,
max_steps: int = 60,
verbose: bool = False,
) -> dict:
"""
Convenience: run a full episode with a policy function.
Args:
task_name: Scenario name
policy_fn: fn(observation: dict) -> action: dict
seed: Optional random seed
max_steps: Safety cap (default 60 = full project)
verbose: Print step summaries to stdout
Returns:
dict with keys: steps, cumulative_reward, final_score,
tasks_completed, tasks_missed, instruction_following_score,
tech_debt, sprint_rewards, done
"""
obs = self.reset(task_name=task_name, seed=seed)
total_reward = 0.0
steps = 0
for step_num in range(1, max_steps + 1):
if obs.get("done", False):
break
action = policy_fn(obs)
result = self.step(action)
obs = result.observation
total_reward += result.reward
steps += 1
if verbose:
print(
f" day={obs['current_day']-1:02d}/60 "
f"sprint={obs['current_sprint']}/6 "
f"done={obs['tasks_completed']} "
f"missed={obs['tasks_missed']} "
f"debt={len(obs['tech_debt'])} "
f"inst={obs['instruction_following_score']:.2f} "
f"reward={result.reward:+.3f}",
flush=True,
)
return {
"steps": steps,
"cumulative_reward": round(total_reward, 4),
"tasks_completed": obs.get("tasks_completed", 0),
"tasks_missed": obs.get("tasks_missed", 0),
"instruction_following_score": obs.get("instruction_following_score", 0.0),
"tech_debt": obs.get("tech_debt", []),
"sprint_rewards": obs.get("sprint_rewards", []),
"done": obs.get("done", False),
}
# ── Session lifecycle ─────────────────────────────────────────────────────
def close(self) -> None:
"""Close the underlying HTTP session."""
self._session.close()
def __enter__(self) -> "ProjectEnvClient":
return self
def __exit__(self, *args: Any) -> None:
self.close()
def __repr__(self) -> str:
return f"ProjectEnvClient(base_url='{self.base_url}', prefix='{self._prefix}')"