#!/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()