| |
| """ |
| 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: |
| pass |
| |
| 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)) |
| ) |
| 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() |
|
|