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()