Spaces:
Sleeping
Sleeping
File size: 4,936 Bytes
64ffb1b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | #!/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()
|