shopmanager-train-code / rollout_baseline.py
hard007ik's picture
shop manage eng phase 2 first
048f186
Raw
History Blame Contribute Delete
4.94 kB
#!/usr/bin/env python3
"""
No-TRL baseline: run random vs simple heuristic policies; log mean return.
Run local server first: uv run server
Then: SHOPMANAGER_MARKET_MODE=synthetic SHOPMANAGER_TRAIN_BASE_URL=http://127.0.0.1:8000 python rollout_baseline.py
"""
from __future__ import annotations
import argparse
import asyncio
import os
import random
import sys
from pathlib import Path
from statistics import fmean, pstdev
from typing import List, Optional
ROOT = Path(__file__).resolve().parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from client import JewelryShopEnv
from models import JewelryAction, PRODUCT_CATALOG
def _heuristic_action(obs) -> JewelryAction:
ph = obs.phase
if ph == "market":
g = float(obs.gold_price or 0.0) or 1.0
need = 1.0
if obs.cash >= need * g + 10:
return JewelryAction(
market_action="buy", gold_qty=need, target_price_usd=obs.gold_price
)
return JewelryAction(market_action="wait")
if ph == "warehouse":
dem = obs.demand or {"ring": 0.5, "necklace": 0.3, "bracelet": 0.2}
for name in sorted(dem, key=lambda k: dem.get(k, 0), reverse=True):
gneed = float(PRODUCT_CATALOG[name]["gold_oz"])
lab = float(PRODUCT_CATALOG[name]["labor"])
if obs.gold_oz + 1e-9 >= gneed and obs.cash + 1e-9 >= lab:
return JewelryAction(product_choice=name)
return JewelryAction(product_choice="ring")
if ph == "showroom":
if (
obs.current_offer
and obs.cost_basis > 0
and (float(obs.current_offer) / float(obs.cost_basis)) >= 1.15
) or (getattr(obs, "negotiation_round", 0) and int(obs.negotiation_round) >= 3):
return JewelryAction(message="I accept")
off = float(obs.current_offer or 0.0)
return JewelryAction(
message=f"How about ${off * 1.08:.2f}?" if off else "I need a better offer"
)
return JewelryAction()
def _random_action(obs) -> JewelryAction:
if obs.phase == "market":
if random.random() < 0.35:
return JewelryAction(
market_action="buy", gold_qty=round(random.uniform(0.1, 1.2), 2)
)
return JewelryAction(market_action="wait")
if obs.phase == "warehouse":
return JewelryAction(product_choice=random.choice(["ring", "necklace", "bracelet"]))
return JewelryAction(
message=random.choice(
[
"I accept",
f"How about ${float(obs.current_offer or 0) * 1.1:.0f}?",
]
)
)
async def one_episode(base: str, policy: str, seed: Optional[int], max_steps: int) -> float:
"""
Run one episode under the given policy and return the trajectory return,
which is the env's cumulative reward (sum of per-step partials, in [0, 1]).
"""
if seed is not None:
random.seed(seed)
env = JewelryShopEnv(base_url=base)
r = await env.reset(seed=seed, episode_id=None)
o = r.observation
for _ in range(max_steps):
if r.done:
break
if policy == "heuristic":
a = _heuristic_action(o)
else:
a = _random_action(o)
r = await env.step(a)
o = r.observation
try:
await env.close()
except Exception: # noqa: BLE001
pass
# Authoritative trajectory return from the server (in [0, 1]).
return float(getattr(o, "cumulative_reward", 0.0))
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--episodes", type=int, default=20)
p.add_argument("--max-steps", type=int, default=25)
p.add_argument(
"--base-url",
default=os.environ.get("SHOPMANAGER_TRAIN_BASE_URL", "http://127.0.0.1:8000"),
)
p.add_argument("--policies", nargs="+", default=["heuristic", "random"])
p.add_argument("--out", type=Path, default=Path("rollout_metrics.txt"))
args = p.parse_args()
base = str(args.base_url)
all_lines: List[str] = [f"base_url={base}", f"episodes={args.episodes} max_steps={args.max_steps}", ""]
for name in args.policies:
scores: List[float] = []
for epi in range(args.episodes):
sc = asyncio.run(
one_episode(base, name, seed=epi, max_steps=int(args.max_steps)) # type: ignore[misc] # noqa: E501
)
scores.append(sc)
m = fmean(scores) if scores else 0.0
sd = pstdev(scores) if len(scores) > 1 else 0.0
line = f"{name}: mean={m:.4f} std={sd:.4f} scores={scores!s}"
all_lines.append(line)
print(line)
text = "\n".join(all_lines) + "\n"
try:
args.out.write_text(text, encoding="utf-8")
print(f"Wrote {args.out}", flush=True)
except OSError as err:
print("Could not write out file:", err, flush=True)
if __name__ == "__main__":
main()