hard007ik commited on
Commit
64ffb1b
·
verified ·
1 Parent(s): 00fb2d7

Upload folder using huggingface_hub

Browse files
Blog.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏬 ShopManagerEng: Training LLMs to Run a Business
2
+
3
+ Welcome to **ShopManagerEng**, a Reinforcement Learning (RL) environment designed to test and train Large Language Models (LLMs) on complex, multi-step business operations.
4
+
5
+ While LLMs have become incredibly adept at chatting and writing code, evaluating their ability to make strategic, long-term decisions in a dynamic environment remains a challenge. ShopManagerEng tackles this by putting the AI in the shoes of a jewelry shop manager. Dont get into name 'Eng',just typos mistake of 'Env'.
6
+
7
+ This blog post breaks down how the environment works, its architecture, and how it can be used to train smarter, more strategic agents.
8
+
9
+ ---
10
+
11
+ ## 🎮 The Environment: A 3-Phase Business Simulation
12
+
13
+ ShopManagerEng is not a simple Q&A benchmark. It is a continuous simulation where the agent must manage inventory, respond to market fluctuations, and negotiate with customers. Every "episode" (a full game loop) consists of three distinct phases:
14
+
15
+ ### Phase 1: 📈 The Market (Supply)
16
+ The agent starts with a limited budget and must decide when to buy raw gold.
17
+ * **The Catch:** Gold prices fluctuate. The agent can observe price trends and decide to "wait" for a better price or "buy" immediately.
18
+ * **Real-World Noise:** The environment supports a "real" market mode that pulls live gold prices from Yahoo Finance, testing the agent against real-world economic volatility.
19
+
20
+ ### Phase 2: 🏭 The Warehouse (Production)
21
+ Once gold is acquired, the agent must decide what to craft: Rings, Necklaces, or Bracelets.
22
+ * **The Catch:** The agent must balance raw material costs, labor costs, and market demand. It receives a "demand forecast" (which includes noise) and must make production choices that maximize potential profit without running out of cash or gold.
23
+
24
+ ### Phase 3: 🤝 The Showroom (Sales)
25
+ The final phase tests the agent's negotiation skills.
26
+ * **The Catch:** Customers make initial offers. The agent can accept, reject, or counter-offer over a maximum of 5 rounds. Push too hard, and the customer walks away (resulting in zero profit). Settle too early, and the agent leaves money on the table.
27
+
28
+ The agent's final score is a cumulative reward based on how well it navigated all three phases.
29
+
30
+ ---
31
+
32
+ ## 🏗️ Architecture Under the Hood
33
+
34
+ ShopManagerEng is built on a robust Client-Server architecture, making it highly scalable for Reinforcement Learning tasks.
35
+
36
+ 1. **The Server (`/server/`):** The core simulation engine runs as a standalone web application (often deployed via Docker to a Hugging Face Space). It tracks the hidden "true" state of the world, handles state transitions, fetches real market data, and manages an SQLite database to log inventory and invoices.
37
+ 2. **The Client (`client.py` & `models.py`):** Provides a clean, typed Python interface. Agents use this client to send actions (e.g., `{"market_action": "buy", "gold_qty": 2.0}`) and receive structured observations in return.
38
+ 3. **The AI Player (`inference.py`):** A script demonstrating how to plug an LLM (like Llama 3) into the environment. It dynamically builds text prompts explaining the current state (e.g., *"Gold is $2000/oz and trending down. You have $1000. Buy or wait?"*) and parses the LLM's text output back into valid game actions.
39
+ 4. **The Training Suite (`/training/`):** The real power of this project lies in its RL training capabilities. Using algorithms like GRPO (Group Relative Policy Optimization), the framework can run massive batches of episodes, evaluate the agent using custom reward functions, and iteratively update the model's weights to forge a master negotiator and supply-chain manager.
40
+
41
+ ### 🛠️ Technical Stack & Data Flow
42
+
43
+ * **FastAPI & OpenEnv:** The server is powered by FastAPI, leveraging the `openenv` framework to standardize interactions. This ensures the environment can be easily hosted (e.g., on Hugging Face Spaces) and queried by agents remotely.
44
+ * **Pydantic Models:** All actions and observations are strongly typed using Pydantic (`models.py`). This guarantees that agents send properly formatted JSON payloads (like `target_price_usd` or `inventory_urgent` flags) and receive structured state data.
45
+ * **State Persistence:** A built-in SQLite store (`sqlite_store.py`) tracks the complex state across episodes, maintaining ledgers for cash, gold (in troy ounces and grams), and specific inventory items (rings, necklaces, bracelets).
46
+ * **yfinance Integration:** For the "real" market mode, the environment dynamically fetches live GC=F (Gold Futures) ticker data via `market_data.py`, injecting true market volatility into the simulation rather than relying purely on synthetic random walks.
47
+ * **GRPO Training:** The training pipeline (`train_jewelry_grpo.py`) utilizes Group Relative Policy Optimization, an advanced RL technique designed to stabilize training and improve sample efficiency when teaching LLMs complex, multi-step tasks. Custom reward functions (`rewards.py`) evaluate and guide the model's behavior.
48
+
49
+ ---
50
+
51
+ ## 🚀 Why This Matters
52
+
53
+ As we move towards autonomous AI agents, we need benchmarks that go beyond static knowledge retrieval. ShopManagerEng evaluates an agent's ability to:
54
+ * **Plan Long-Term:** A bad purchase in Phase 1 ruins the negotiation in Phase 3.
55
+ * **Handle Uncertainty:** Dealing with noisy demand forecasts and volatile live markets.
56
+ * **Negotiate:** Understanding margins and customer psychology.
57
+
58
+ Whether you are testing the out-of-the-box reasoning of a new foundational model or fine-tuning a specialized RL agent, ShopManagerEng provides a rich, complex sandbox to push AI capabilities forward.
Dockerfile CHANGED
@@ -59,6 +59,11 @@ FROM ${BASE_IMAGE}
59
 
60
  WORKDIR /app
61
 
 
 
 
 
 
62
  # Copy the virtual environment from builder
63
  COPY --from=builder /app/env/.venv /app/.venv
64
 
@@ -71,11 +76,16 @@ ENV PATH="/app/.venv/bin:$PATH"
71
  # Set PYTHONPATH so imports work correctly
72
  ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
 
 
 
 
74
  # Health check
75
  HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
  CMD curl -f http://localhost:8000/health || exit 1
77
 
78
- # Run the FastAPI server
79
- # The module path is constructed to work with the /app/env structure
 
 
80
  ENV ENABLE_WEB_INTERFACE=true
81
- CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
 
59
 
60
  WORKDIR /app
61
 
62
+ # Install curl (for health checks in start.sh) and streamlit
63
+ RUN apt-get update && \
64
+ apt-get install -y --no-install-recommends curl && \
65
+ rm -rf /var/lib/apt/lists/*
66
+
67
  # Copy the virtual environment from builder
68
  COPY --from=builder /app/env/.venv /app/.venv
69
 
 
76
  # Set PYTHONPATH so imports work correctly
77
  ENV PYTHONPATH="/app/env:$PYTHONPATH"
78
 
79
+ # Install streamlit into the existing venv
80
+ RUN pip install --no-cache-dir streamlit
81
+
82
  # Health check
83
  HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
84
  CMD curl -f http://localhost:8000/health || exit 1
85
 
86
+ # Make startup script executable
87
+ RUN chmod +x /app/env/start.sh
88
+
89
+ # Run both API server (background) + Streamlit UI (foreground)
90
  ENV ENABLE_WEB_INTERFACE=true
91
+ CMD ["/app/env/start.sh"]
README.md CHANGED
@@ -5,12 +5,15 @@ colorFrom: green
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
- app_port: 8000
9
  base_path: /web
10
  tags:
11
  - openenv
12
  ---
13
 
 
 
 
14
  # Jewelry Shop Manager — RL Environment
15
 
16
  A reinforcement learning environment simulating a **jewelry shop management** pipeline. An AI agent navigates three sequential phases — buying raw materials, selecting products to craft based on demand, and negotiating sales — to maximize profit.
 
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
9
  base_path: /web
10
  tags:
11
  - openenv
12
  ---
13
 
14
+ Link of the environment: https://huggingface.co/spaces/hard007ik/ShopManagerEng
15
+ Link of Blog.md: https://huggingface.co/spaces/hard007ik/ShopManagerEng/tree/main/Blog.md
16
+
17
  # Jewelry Shop Manager — RL Environment
18
 
19
  A reinforcement learning environment simulating a **jewelry shop management** pipeline. An AI agent navigates three sequential phases — buying raw materials, selecting products to craft based on demand, and negotiating sales — to maximize profit.
inference.py CHANGED
@@ -71,7 +71,8 @@ SYSTEM_PROMPT = textwrap.dedent(
71
  Respond: "ring", "necklace", or "bracelet".
72
 
73
  ## Phase 3: SHOWROOM (negotiate)
74
- The customer makes an offer; if you counter, they raise it ~5% per round,
 
75
  up to 5 rounds. After 5 rounds with no acceptance, the customer leaves
76
  (no phase-3 reward). Reject also gives 0 phase-3 reward.
77
  Respond: "I accept" or a counter like "How about $X?". NEVER explicitly reject.
 
71
  Respond: "ring", "necklace", or "bracelet".
72
 
73
  ## Phase 3: SHOWROOM (negotiate)
74
+ you makes an offer; if customer counter by telling less price from your offer, you can drop price about ~3-5% per round but make sure to not sell when loss is happening also bring max profit,
75
+ if customer says less price then your first told price then you have to say the price that lesser than the price you told before but more that the customer told price
76
  up to 5 rounds. After 5 rounds with no acceptance, the customer leaves
77
  (no phase-3 reward). Reject also gives 0 phase-3 reward.
78
  Respond: "I accept" or a counter like "How about $X?". NEVER explicitly reject.
pyproject.toml CHANGED
@@ -21,6 +21,7 @@ dependencies = [
21
  "yfinance>=0.2.40",
22
  "python-dotenv>=1.0.0",
23
  "requests>=2.28.0",
 
24
  ]
25
 
26
  [project.optional-dependencies]
@@ -29,6 +30,26 @@ dev = [
29
  "pytest-cov>=4.0.0",
30
  ]
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  [project.scripts]
33
  # Server entry point - enables running via: uv run --project . server
34
  # or: python -m ShopManagerEng.server.app
@@ -36,5 +57,5 @@ server = "ShopManagerEng.server.app:main"
36
 
37
  [tool.setuptools]
38
  include-package-data = true
39
- packages = ["ShopManagerEng", "ShopManagerEng.server"]
40
- package-dir = { "ShopManagerEng" = ".", "ShopManagerEng.server" = "server" }
 
21
  "yfinance>=0.2.40",
22
  "python-dotenv>=1.0.0",
23
  "requests>=2.28.0",
24
+ "streamlit>=1.30.0",
25
  ]
26
 
27
  [project.optional-dependencies]
 
30
  "pytest-cov>=4.0.0",
31
  ]
32
 
33
+ # GRPO training stack. Install only on a GPU host (vLLM is GPU-only).
34
+ # pip install -e '.[train]'
35
+ # Mirrors openenv-course Module 5 versions. trl>=0.17 is the cutoff that
36
+ # introduced trl.experimental.openenv.generate_rollout_completions.
37
+ train = [
38
+ "trl>=0.17.0",
39
+ "transformers>=4.46.0",
40
+ "datasets>=2.20.0",
41
+ "accelerate>=1.0.0",
42
+ # Pin to TRL's supported window. vllm 0.19+ changes the sampling-logprob
43
+ # path and breaks GRPO importance sampling (ratio collapses to ~0,
44
+ # grad_norm goes to 0, model stops learning). Keep <=0.18.0 until TRL
45
+ # bumps its supported range.
46
+ "vllm>=0.11.0,<=0.18.0",
47
+ "trackio",
48
+ "torch>=2.4.0",
49
+ # Local plotting of loss + reward curves (hackathon submission evidence).
50
+ "matplotlib>=3.7",
51
+ ]
52
+
53
  [project.scripts]
54
  # Server entry point - enables running via: uv run --project . server
55
  # or: python -m ShopManagerEng.server.app
 
57
 
58
  [tool.setuptools]
59
  include-package-data = true
60
+ packages = ["ShopManagerEng", "ShopManagerEng.server", "ShopManagerEng.training"]
61
+ package-dir = { "ShopManagerEng" = ".", "ShopManagerEng.server" = "server", "ShopManagerEng.training" = "training" }
rollout_baseline.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ No-TRL baseline: run random vs simple heuristic policies; log mean return.
4
+ Run local server first: uv run server
5
+ Then: SHOPMANAGER_MARKET_MODE=synthetic SHOPMANAGER_TRAIN_BASE_URL=http://127.0.0.1:8000 python rollout_baseline.py
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ import os
12
+ import random
13
+ import sys
14
+ from pathlib import Path
15
+ from statistics import fmean, pstdev
16
+ from typing import List, Optional
17
+
18
+ ROOT = Path(__file__).resolve().parent
19
+ if str(ROOT) not in sys.path:
20
+ sys.path.insert(0, str(ROOT))
21
+
22
+ from client import JewelryShopEnv
23
+ from models import JewelryAction, PRODUCT_CATALOG
24
+
25
+
26
+ def _heuristic_action(obs) -> JewelryAction:
27
+ ph = obs.phase
28
+ if ph == "market":
29
+ g = float(obs.gold_price or 0.0) or 1.0
30
+ need = 1.0
31
+ if obs.cash >= need * g + 10:
32
+ return JewelryAction(
33
+ market_action="buy", gold_qty=need, target_price_usd=obs.gold_price
34
+ )
35
+ return JewelryAction(market_action="wait")
36
+ if ph == "warehouse":
37
+ dem = obs.demand or {"ring": 0.5, "necklace": 0.3, "bracelet": 0.2}
38
+ for name in sorted(dem, key=lambda k: dem.get(k, 0), reverse=True):
39
+ gneed = float(PRODUCT_CATALOG[name]["gold_oz"])
40
+ lab = float(PRODUCT_CATALOG[name]["labor"])
41
+ if obs.gold_oz + 1e-9 >= gneed and obs.cash + 1e-9 >= lab:
42
+ return JewelryAction(product_choice=name)
43
+ return JewelryAction(product_choice="ring")
44
+ if ph == "showroom":
45
+ if (
46
+ obs.current_offer
47
+ and obs.cost_basis > 0
48
+ and (float(obs.current_offer) / float(obs.cost_basis)) >= 1.15
49
+ ) or (getattr(obs, "negotiation_round", 0) and int(obs.negotiation_round) >= 3):
50
+ return JewelryAction(message="I accept")
51
+ off = float(obs.current_offer or 0.0)
52
+ return JewelryAction(
53
+ message=f"How about ${off * 1.08:.2f}?" if off else "I need a better offer"
54
+ )
55
+ return JewelryAction()
56
+
57
+
58
+ def _random_action(obs) -> JewelryAction:
59
+ if obs.phase == "market":
60
+ if random.random() < 0.35:
61
+ return JewelryAction(
62
+ market_action="buy", gold_qty=round(random.uniform(0.1, 1.2), 2)
63
+ )
64
+ return JewelryAction(market_action="wait")
65
+ if obs.phase == "warehouse":
66
+ return JewelryAction(product_choice=random.choice(["ring", "necklace", "bracelet"]))
67
+ return JewelryAction(
68
+ message=random.choice(
69
+ [
70
+ "I accept",
71
+ f"How about ${float(obs.current_offer or 0) * 1.1:.0f}?",
72
+ ]
73
+ )
74
+ )
75
+
76
+
77
+ async def one_episode(base: str, policy: str, seed: Optional[int], max_steps: int) -> float:
78
+ """
79
+ Run one episode under the given policy and return the trajectory return,
80
+ which is the env's cumulative reward (sum of per-step partials, in [0, 1]).
81
+ """
82
+ if seed is not None:
83
+ random.seed(seed)
84
+ env = JewelryShopEnv(base_url=base)
85
+ r = await env.reset(seed=seed, episode_id=None)
86
+ o = r.observation
87
+ for _ in range(max_steps):
88
+ if r.done:
89
+ break
90
+ if policy == "heuristic":
91
+ a = _heuristic_action(o)
92
+ else:
93
+ a = _random_action(o)
94
+ r = await env.step(a)
95
+ o = r.observation
96
+ try:
97
+ await env.close()
98
+ except Exception: # noqa: BLE001
99
+ pass
100
+ # Authoritative trajectory return from the server (in [0, 1]).
101
+ return float(getattr(o, "cumulative_reward", 0.0))
102
+
103
+
104
+ def main() -> None:
105
+ p = argparse.ArgumentParser()
106
+ p.add_argument("--episodes", type=int, default=20)
107
+ p.add_argument("--max-steps", type=int, default=25)
108
+ p.add_argument(
109
+ "--base-url",
110
+ default=os.environ.get("SHOPMANAGER_TRAIN_BASE_URL", "http://127.0.0.1:8000"),
111
+ )
112
+ p.add_argument("--policies", nargs="+", default=["heuristic", "random"])
113
+ p.add_argument("--out", type=Path, default=Path("rollout_metrics.txt"))
114
+ args = p.parse_args()
115
+ base = str(args.base_url)
116
+ all_lines: List[str] = [f"base_url={base}", f"episodes={args.episodes} max_steps={args.max_steps}", ""]
117
+
118
+ for name in args.policies:
119
+ scores: List[float] = []
120
+ for epi in range(args.episodes):
121
+ sc = asyncio.run(
122
+ one_episode(base, name, seed=epi, max_steps=int(args.max_steps)) # type: ignore[misc] # noqa: E501
123
+ )
124
+ scores.append(sc)
125
+ m = fmean(scores) if scores else 0.0
126
+ sd = pstdev(scores) if len(scores) > 1 else 0.0
127
+ line = f"{name}: mean={m:.4f} std={sd:.4f} scores={scores!s}"
128
+ all_lines.append(line)
129
+ print(line)
130
+ text = "\n".join(all_lines) + "\n"
131
+ try:
132
+ args.out.write_text(text, encoding="utf-8")
133
+ print(f"Wrote {args.out}", flush=True)
134
+ except OSError as err:
135
+ print("Could not write out file:", err, flush=True)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
start.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Start the OpenEnv API server in the background (port 8000)
5
+ cd /app/env
6
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 &
7
+ API_PID=$!
8
+
9
+ # Wait for the API to be ready
10
+ echo "[start] Waiting for API server on port 8000..."
11
+ for i in $(seq 1 30); do
12
+ if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
13
+ echo "[start] API server ready."
14
+ break
15
+ fi
16
+ sleep 1
17
+ done
18
+
19
+ # Start Streamlit UI (port 7860 — HF Spaces default)
20
+ echo "[start] Launching Streamlit UI on port 7860..."
21
+ exec streamlit run ui.py \
22
+ --server.port 7860 \
23
+ --server.address 0.0.0.0 \
24
+ --server.headless true \
25
+ --browser.gatherUsageStats false \
26
+ --theme.primaryColor "#7c3aed" \
27
+ --theme.backgroundColor "#0f0c29" \
28
+ --theme.secondaryBackgroundColor "#302b63" \
29
+ --theme.textColor "#e2e8f0"
test_env_smoke.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone, no-LLM, no-server smoke tests for the JewelryShop env.
2
+
3
+ Run from inside the ShopManagerEng folder so `models` / `server` import.
4
+
5
+ Usage:
6
+ cd ShopManagerEng
7
+ SHOPMANAGER_MARKET_MODE=synthetic python test_env_smoke.py # default: A+B
8
+ SHOPMANAGER_MARKET_MODE=real python test_env_smoke.py live # C only
9
+ python test_env_smoke.py all # all three
10
+
11
+ Why a script: putting `f'gold_price=${o.gold_price}/oz'` inside `bash -c "..."`
12
+ makes bash try to expand `${o.gold_price}` as a shell variable and crash with
13
+ `bad substitution`. Running it from a .py file removes that whole class of bugs.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import sys
20
+
21
+ from server.ShopManagerEng_environment import JewelryShopEnvironment
22
+ from models import JewelryAction
23
+
24
+
25
+ def test_reward_path() -> None:
26
+ """A. Synthetic episode end-to-end, prints per-step partial + cumulative."""
27
+ print("\n=== A. reward path (synthetic) ===")
28
+ os.environ["SHOPMANAGER_MARKET_MODE"] = "synthetic"
29
+ e = JewelryShopEnvironment()
30
+ o = e.reset(seed=42, task_id="market_timing", starting_cash=10000.0)
31
+ print(
32
+ f" reset: phase={o.phase} cash=${o.cash} gold={o.gold_oz}oz "
33
+ f"price=${o.gold_price} weights={o.weights}"
34
+ )
35
+
36
+ actions = [
37
+ JewelryAction(market_action="buy", gold_qty=1.0),
38
+ JewelryAction(product_choice="ring"),
39
+ JewelryAction(message="I accept"),
40
+ ]
41
+ for i, a in enumerate(actions, 1):
42
+ o = e.step(a)
43
+ print(
44
+ f" step {i}: phase={o.phase} reward={o.reward:.4f} "
45
+ f"cum={o.cumulative_reward:.4f} done={o.done}"
46
+ )
47
+ print(f" FINAL cum={o.cumulative_reward:.4f} (must be in [0, 1])")
48
+
49
+
50
+ def test_bounce_loop() -> None:
51
+ """B. Warehouse cannot craft -> agent is bounced back to MARKET."""
52
+ print("\n=== B. bounce loop (warehouse -> market) ===")
53
+ os.environ["SHOPMANAGER_MARKET_MODE"] = "synthetic"
54
+ e = JewelryShopEnvironment()
55
+ o = e.reset(seed=42, task_id="profit_negotiator", starting_cash=10000.0)
56
+
57
+ o = e.step(JewelryAction(market_action="buy", gold_qty=0.2))
58
+ print(f" bought 0.2oz: phase={o.phase}, gold={o.gold_oz}oz")
59
+
60
+ o = e.step(JewelryAction(product_choice="ring"))
61
+ print(
62
+ f" tried ring: phase={o.phase} reentries={o.market_reentries}"
63
+ f"/{o.max_market_reentries} urgent={o.inventory_urgent} "
64
+ f"cannot_wait={o.cannot_wait}"
65
+ )
66
+ assert o.phase == "market", "expected bounce back to market"
67
+ assert o.inventory_urgent is True, "expected urgent flag"
68
+
69
+ o = e.step(JewelryAction(market_action="wait"))
70
+ print(f" tried wait while urgent: phase={o.phase} (should still be market)")
71
+ assert o.phase == "market", "wait should be blocked when urgent"
72
+
73
+ o = e.step(JewelryAction(market_action="buy", gold_qty=1.0))
74
+ print(f" bought 1.0oz more: phase={o.phase} gold={o.gold_oz}oz")
75
+
76
+ o = e.step(JewelryAction(product_choice="ring"))
77
+ print(f" craft ring: phase={o.phase} cum={o.cumulative_reward:.4f}")
78
+
79
+ o = e.step(JewelryAction(message="I accept"))
80
+ print(f" FINAL cum={o.cumulative_reward:.4f}")
81
+
82
+
83
+ def test_live_quote() -> None:
84
+ """C. Real mode: live yfinance gold price (needs network)."""
85
+ print("\n=== C. live yfinance quote (real mode) ===")
86
+ os.environ["SHOPMANAGER_MARKET_MODE"] = "real"
87
+ e = JewelryShopEnvironment()
88
+ o = e.reset(seed=0, task_id="market_timing", starting_cash=10000.0)
89
+ print(f" gold_price=${o.gold_price}/oz source={o.gold_price_source}")
90
+ print(f" market_mode={o.market_mode}")
91
+ print(f" history(last 5)={o.gold_price_history[-5:]}")
92
+
93
+
94
+ def main(argv: list[str]) -> None:
95
+ arg = (argv[1] if len(argv) > 1 else "default").lower()
96
+
97
+ if arg in ("default", "ab"):
98
+ test_reward_path()
99
+ test_bounce_loop()
100
+ elif arg == "live":
101
+ test_live_quote()
102
+ elif arg == "all":
103
+ test_reward_path()
104
+ test_bounce_loop()
105
+ test_live_quote()
106
+ elif arg == "a":
107
+ test_reward_path()
108
+ elif arg == "b":
109
+ test_bounce_loop()
110
+ elif arg == "c":
111
+ test_live_quote()
112
+ else:
113
+ print(f"unknown arg: {arg}. use one of: a / b / c / ab / live / all")
114
+ sys.exit(2)
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main(sys.argv)
train_jewelry_grpo.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GRPO training entry point for the JewelryShop OpenEnv (Module 5 style).
2
+
3
+ Canonical TRL >= 0.17 + OpenEnv pattern:
4
+
5
+ 1. Persistent sync env client (one WebSocket reused across rollouts)
6
+ 2. rollout_func(prompts, trainer) (returns prompt_ids/completion_ids/logprobs + rewards)
7
+ 3. Pure reward_funcs(completions, ...) (read kwargs the rollout puts there)
8
+ 4. GRPOTrainer(..., rollout_func=...) (NO `environment_factory`)
9
+
10
+ Design notes:
11
+ - Reward shaping: ONE primary reward (`reward_total = obs.cumulative_reward`)
12
+ drives gradients; per-phase rewards (market/warehouse/showroom) are exposed
13
+ for monitoring with weight 0 in the GRPO advantage.
14
+ - Tasks: dataset rows embed [TASK=<task_id>] which the rollout extracts so each
15
+ episode trains against a specific per-phase weight profile from openenv.yaml.
16
+ - Smoke: --smoke imports everything, builds the rollout func, opens the env
17
+ and does a single reset() round-trip — no GPU and no model weights needed.
18
+ Use this to validate wiring before paying for a GPU.
19
+
20
+ Quick local smoke (no GPU, no model load):
21
+ python train_jewelry_grpo.py --smoke
22
+
23
+ Full local quick check (CPU; slow, but verifies trainer.train() starts):
24
+ TRAIN_MODEL=Qwen/Qwen3-0.6B python train_jewelry_grpo.py --quick
25
+
26
+ Cloud (HF Jobs) — see README/TRAINING for the exact `hf jobs run` command.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import os
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ # Make `from ShopManagerEng...` imports work whether you launch this from inside
36
+ # the package directory or one level up.
37
+ ROOT = Path(__file__).resolve().parent
38
+ PARENT = ROOT.parent
39
+ if str(PARENT) not in sys.path:
40
+ sys.path.insert(0, str(PARENT))
41
+
42
+ try:
43
+ from ShopManagerEng.client import JewelryShopEnv
44
+ from ShopManagerEng.training.plotting import (
45
+ build_metrics_callback,
46
+ save_training_artifacts,
47
+ upload_training_artifacts_to_hub,
48
+ )
49
+ from ShopManagerEng.training.prompts import SYSTEM_PROMPT
50
+ from ShopManagerEng.training.rewards import (
51
+ ALL_REWARDS,
52
+ REWARD_WEIGHTS_MONITOR_ONLY,
53
+ )
54
+ from ShopManagerEng.training.rollout import VALID_TASKS, build_rollout_func
55
+ except ImportError: # script-style invocation from inside the folder
56
+ from client import JewelryShopEnv # type: ignore
57
+ from training.plotting import ( # type: ignore
58
+ build_metrics_callback,
59
+ save_training_artifacts,
60
+ upload_training_artifacts_to_hub,
61
+ )
62
+ from training.prompts import SYSTEM_PROMPT # type: ignore
63
+ from training.rewards import ( # type: ignore
64
+ ALL_REWARDS,
65
+ REWARD_WEIGHTS_MONITOR_ONLY,
66
+ )
67
+ from training.rollout import VALID_TASKS, build_rollout_func # type: ignore
68
+
69
+
70
+ def _build_dataset(dataset_size: int):
71
+ """Cycle through the 3 graded tasks; embed task id in prompt for the rollout."""
72
+ from datasets import Dataset
73
+
74
+ rows = []
75
+ for i in range(dataset_size):
76
+ task_id = VALID_TASKS[i % len(VALID_TASKS)]
77
+ rows.append(
78
+ {
79
+ "prompt": (
80
+ f"[TASK={task_id}] Manage a jewelry shop episode end-to-end. "
81
+ f"Maximize the {task_id} task reward."
82
+ ),
83
+ "task_id": task_id,
84
+ }
85
+ )
86
+ return Dataset.from_list(rows)
87
+
88
+
89
+ def _resolve_precision():
90
+ """CPU/GPU autodetect; mirror the well-tested defaults."""
91
+ try:
92
+ import torch
93
+ has_cuda = bool(torch.cuda.is_available())
94
+ except Exception:
95
+ has_cuda = False
96
+ if has_cuda:
97
+ return {"bf16": True}
98
+ return {"use_cpu": True, "bf16": False, "fp16": False}
99
+
100
+
101
+ def main() -> None:
102
+ ap = argparse.ArgumentParser()
103
+ ap.add_argument(
104
+ "--model",
105
+ default=os.environ.get("TRAIN_MODEL", "Qwen/Qwen3-1.7B"),
106
+ help="HF model id (default: Qwen/Qwen3-1.7B; matches openenv-course Module 5).",
107
+ )
108
+ ap.add_argument(
109
+ "--env-url",
110
+ default=os.environ.get(
111
+ "ENV_URL", "https://hard007ik-shopmanagereng.hf.space"
112
+ ),
113
+ help="Base URL of the running OpenEnv server (Space or http://127.0.0.1:8000).",
114
+ )
115
+ ap.add_argument(
116
+ "--output-dir",
117
+ default=os.environ.get("TRAIN_OUTPUT_DIR", "shopmanager-grpo-out"),
118
+ )
119
+ ap.add_argument("--dataset-size", type=int, default=300)
120
+ ap.add_argument("--num-generations", type=int, default=2)
121
+ ap.add_argument("--per-device-batch", type=int, default=1)
122
+ ap.add_argument("--grad-accum", type=int, default=32)
123
+ ap.add_argument("--max-completion-length", type=int, default=64)
124
+ # NOTE: --max-prompt-length intentionally removed. Recent TRL versions
125
+ # (>=0.20-ish) dropped `max_prompt_length` from GRPOConfig and now infer
126
+ # it from the tokenizer / model context window. Passing it back errors
127
+ # with `TypeError: unexpected keyword argument 'max_prompt_length'`.
128
+ ap.add_argument("--max-turns", type=int, default=15)
129
+ ap.add_argument("--lr", type=float, default=5e-6)
130
+ ap.add_argument("--warmup-steps", type=int, default=10)
131
+ ap.add_argument("--max-steps", type=int, default=-1, help="-1 = epoch-bounded.")
132
+ ap.add_argument("--epochs", type=int, default=1)
133
+ ap.add_argument(
134
+ "--vllm-gpu-mem",
135
+ type=float,
136
+ default=0.3,
137
+ help="Fraction of GPU mem reserved for vLLM. Lower if OOM.",
138
+ )
139
+ ap.add_argument("--push-to-hub", action="store_true")
140
+ ap.add_argument(
141
+ "--hub-repo-id",
142
+ default=None,
143
+ help="HF model repo (user/model-name) for weight push + training-artifact upload. "
144
+ "If omitted, artifact upload uses {whoami}/{basename of --output-dir}.",
145
+ )
146
+ ap.add_argument(
147
+ "--report-to",
148
+ default=os.environ.get("TRAIN_REPORT_TO", "trackio"),
149
+ help="trackio | wandb | none",
150
+ )
151
+ ap.add_argument(
152
+ "--smoke",
153
+ action="store_true",
154
+ help="No training. Imports, env connect, one reset() — validates wiring.",
155
+ )
156
+ ap.add_argument(
157
+ "--quick",
158
+ action="store_true",
159
+ help="CPU-friendly tiny run (1 step, num_generations=2, max_completion=32).",
160
+ )
161
+ args = ap.parse_args()
162
+
163
+ if args.smoke:
164
+ # No transformers / vllm load: just import + connect + reset, then
165
+ # also exercise the plotting pipeline on a fake log_history so the
166
+ # submission-artifact path is proven before we burn a GPU on it.
167
+ env = JewelryShopEnv(base_url=args.env_url)
168
+ sync_env = env.sync()
169
+ sync_env.connect()
170
+ try:
171
+ r = sync_env.reset(task_id=VALID_TASKS[0])
172
+ print(f"[SMOKE] connected to {args.env_url}")
173
+ print(
174
+ f"[SMOKE] reset OK: phase={r.observation.phase}, "
175
+ f"done={r.done}, cumulative_reward="
176
+ f"{getattr(r.observation, 'cumulative_reward', 0)}"
177
+ )
178
+ print(f"[SMOKE] system prompt loaded ({len(SYSTEM_PROMPT)} chars)")
179
+ print("[SMOKE] reward funcs:", [f.__name__ for f in ALL_REWARDS])
180
+ print("[SMOKE] reward weights:", REWARD_WEIGHTS_MONITOR_ONLY)
181
+ finally:
182
+ try:
183
+ sync_env.close()
184
+ except Exception:
185
+ pass
186
+
187
+ # Prove the plotting pipeline works (writes PNG + CSV + JSON to
188
+ # output_dir using a synthetic, monotonic log). This is what the
189
+ # real run will produce — same code path.
190
+ fake_history = []
191
+ for step in range(1, 21):
192
+ fake_history.append(
193
+ {
194
+ "step": step,
195
+ "loss": 1.0 / step,
196
+ "reward": 0.05 * step,
197
+ "rewards/reward_total": min(0.05 * step, 1.0),
198
+ "rewards/reward_market": min(0.02 * step, 0.6),
199
+ "rewards/reward_warehouse": min(0.015 * step, 0.6),
200
+ "rewards/reward_showroom": min(0.018 * step, 0.6),
201
+ }
202
+ )
203
+ summary = save_training_artifacts(
204
+ fake_history,
205
+ args.output_dir,
206
+ run_config={"smoke": True, "model": args.model, "env_url": args.env_url},
207
+ )
208
+ print(
209
+ f"[SMOKE] plot pipeline OK -> {args.output_dir}/loss_curve.png, "
210
+ f"{args.output_dir}/reward_curve.png, "
211
+ f"{args.output_dir}/reward_total_curve.png"
212
+ )
213
+ print(
214
+ f"[SMOKE] summary peak_reward_total={summary['reward_total']['max']:.3f} "
215
+ f"final_loss={summary['loss']['final']:.4f}"
216
+ )
217
+ print("[SMOKE] OK — wiring is sane.")
218
+ return
219
+
220
+ # ── Heavy imports only past the smoke gate ──
221
+ from transformers import AutoTokenizer
222
+ from trl import GRPOConfig, GRPOTrainer
223
+
224
+ tokenizer = AutoTokenizer.from_pretrained(args.model)
225
+ if tokenizer.pad_token is None:
226
+ tokenizer.pad_token = tokenizer.eos_token
227
+
228
+ env = JewelryShopEnv(base_url=args.env_url)
229
+ sync_env = env.sync()
230
+ sync_env.connect()
231
+ print(f"[TRAIN] env: {args.env_url}")
232
+ print(f"[TRAIN] model: {args.model}")
233
+
234
+ rollout_func = build_rollout_func(
235
+ sync_env=sync_env,
236
+ tokenizer=tokenizer,
237
+ system_prompt=SYSTEM_PROMPT,
238
+ max_turns=args.max_turns,
239
+ model_name=args.model,
240
+ )
241
+
242
+ if args.quick:
243
+ # Override to tiny CPU-friendly numbers.
244
+ args.dataset_size = 4
245
+ args.num_generations = 2
246
+ args.per_device_batch = 2
247
+ args.grad_accum = 1
248
+ args.max_completion_length = 32
249
+ args.max_steps = 1
250
+ args.warmup_steps = 0
251
+
252
+ dataset = _build_dataset(args.dataset_size)
253
+ precision = _resolve_precision()
254
+ use_cpu = precision.get("use_cpu", False)
255
+
256
+ grpo_config = GRPOConfig(
257
+ output_dir=args.output_dir,
258
+ num_train_epochs=args.epochs,
259
+ learning_rate=args.lr,
260
+ gradient_accumulation_steps=args.grad_accum,
261
+ per_device_train_batch_size=args.per_device_batch,
262
+ warmup_steps=args.warmup_steps,
263
+ num_generations=args.num_generations,
264
+ max_completion_length=args.max_completion_length,
265
+ # `max_prompt_length` removed: not accepted by recent TRL GRPOConfig.
266
+ # vLLM is the canonical generation backend on GPU; turn off on CPU smoke.
267
+ use_vllm=not use_cpu,
268
+ vllm_mode="colocate" if not use_cpu else None,
269
+ vllm_gpu_memory_utilization=args.vllm_gpu_mem if not use_cpu else None,
270
+ gradient_checkpointing=True,
271
+ gradient_checkpointing_kwargs={"use_reentrant": False},
272
+ reward_weights=REWARD_WEIGHTS_MONITOR_ONLY,
273
+ report_to=args.report_to if args.report_to != "none" else "none",
274
+ logging_steps=1,
275
+ save_steps=20,
276
+ push_to_hub=args.push_to_hub,
277
+ max_steps=args.max_steps if args.max_steps > 0 else -1,
278
+ **precision,
279
+ )
280
+
281
+ print(f"[TRAIN] device={'cpu' if use_cpu else 'gpu'} precision={precision}")
282
+ print(f"[TRAIN] dataset_size={args.dataset_size} num_generations={args.num_generations}")
283
+
284
+ trainer = GRPOTrainer(
285
+ model=args.model,
286
+ processing_class=tokenizer,
287
+ reward_funcs=list(ALL_REWARDS),
288
+ train_dataset=dataset,
289
+ args=grpo_config,
290
+ rollout_func=rollout_func,
291
+ )
292
+
293
+ # Persist loss + reward plots into output_dir every N steps + at the end.
294
+ # This is the hackathon "evidence you actually trained" artifact set.
295
+ trainer.add_callback(build_metrics_callback(args.output_dir, snapshot_every=5))
296
+
297
+ run_config = {
298
+ "model": args.model,
299
+ "env_url": args.env_url,
300
+ "dataset_size": args.dataset_size,
301
+ "num_generations": args.num_generations,
302
+ "per_device_batch": args.per_device_batch,
303
+ "grad_accum": args.grad_accum,
304
+ "max_completion_length": args.max_completion_length,
305
+ "max_turns": args.max_turns,
306
+ "lr": args.lr,
307
+ "warmup_steps": args.warmup_steps,
308
+ "max_steps": args.max_steps,
309
+ "epochs": args.epochs,
310
+ "vllm_gpu_mem": args.vllm_gpu_mem,
311
+ "reward_weights": REWARD_WEIGHTS_MONITOR_ONLY,
312
+ "precision": precision,
313
+ }
314
+
315
+ try:
316
+ trainer.train()
317
+ finally:
318
+ try:
319
+ sync_env.close()
320
+ except Exception:
321
+ pass
322
+ # Always persist whatever metrics we have, even on a crash mid-run.
323
+ try:
324
+ summary = save_training_artifacts(
325
+ list(trainer.state.log_history or []),
326
+ args.output_dir,
327
+ run_config=run_config,
328
+ )
329
+ print(
330
+ f"[ARTIFACTS] wrote loss/reward plots + metrics to {args.output_dir}\n"
331
+ f"[ARTIFACTS] final loss={summary['loss']['final']:.4f} "
332
+ f"max_reward_total={summary['reward_total']['max']:.4f} "
333
+ f"final_reward_total={summary['reward_total']['final']:.4f}"
334
+ )
335
+ except Exception as exc:
336
+ print(f"[ARTIFACTS] failed to save metrics: {exc}")
337
+
338
+ trainer.save_model(args.output_dir)
339
+ if args.push_to_hub:
340
+ trainer.push_to_hub()
341
+ # Default Hub push only ships weights/tokenizer; upload plots + metrics explicitly.
342
+ try:
343
+ from huggingface_hub import whoami
344
+
345
+ user = whoami().get("name") or whoami().get("preferred_username", "user")
346
+ rid = (
347
+ args.hub_repo_id
348
+ or getattr(trainer.args, "hub_model_id", None)
349
+ or f"{user}/{Path(args.output_dir).name}"
350
+ )
351
+ uploaded = upload_training_artifacts_to_hub(args.output_dir, rid)
352
+ if uploaded:
353
+ print(
354
+ f"[HUB] training artifacts ({len(uploaded)} files) -> "
355
+ f"https://huggingface.co/{rid}/tree/main/training_artifacts"
356
+ )
357
+ except Exception as exc: # noqa: BLE001
358
+ print(f"[HUB] training artifact upload failed: {exc}")
359
+ print(f"[DONE] saved to {args.output_dir}")
360
+
361
+
362
+ if __name__ == "__main__":
363
+ main()
training/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """ShopManagerEng training package.
2
+
3
+ Course-style (Module 5) GRPO training scaffolding for the JewelryShop env.
4
+ Uses TRL's `rollout_func=...` entry point with vLLM colocate generation.
5
+ """
training/parse_action.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Parse free-form model text into a typed JewelryAction.
2
+
3
+ Mirrors inference.py:get_action_from_text so the action surface during
4
+ training matches what was used during evaluation.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Tuple
9
+
10
+ try:
11
+ from ..models import JewelryAction
12
+ except ImportError:
13
+ from models import JewelryAction
14
+
15
+
16
+ def parse_model_text_to_action(phase: str, text: str) -> Tuple[JewelryAction, str]:
17
+ """Return (action, normalised_text) for the current phase.
18
+
19
+ Robust against typical LLM output noise: backticks, quotes, leading/trailing
20
+ whitespace. Falls back to safe defaults so a single bad token never breaks
21
+ the rollout.
22
+ """
23
+ text = (text or "").strip().replace("`", "").strip(" \t\n\r\"'")
24
+
25
+ if phase == "market":
26
+ lower = text.lower()
27
+ if lower.startswith("buy"):
28
+ qty_str = lower.replace("buy", "").strip()
29
+ try:
30
+ qty = float(qty_str)
31
+ except ValueError:
32
+ qty = 1.0
33
+ return JewelryAction(market_action="buy", gold_qty=qty), f"buy {qty}"
34
+ if "wait" in lower:
35
+ return JewelryAction(market_action="wait"), "wait"
36
+ try:
37
+ qty = float(text)
38
+ return JewelryAction(market_action="buy", gold_qty=qty), f"buy {qty}"
39
+ except ValueError:
40
+ return JewelryAction(market_action="wait"), "wait"
41
+
42
+ if phase == "warehouse":
43
+ lower = text.lower()
44
+ for product in ("necklace", "bracelet", "ring"):
45
+ if product in lower:
46
+ return JewelryAction(product_choice=product), product
47
+ return JewelryAction(product_choice="ring"), "ring"
48
+
49
+ if phase == "showroom":
50
+ return JewelryAction(message=text), text
51
+
52
+ return JewelryAction(), text
training/plotting.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Persist training metrics + loss/reward plots to disk.
2
+
3
+ Why this exists: the hackathon submission asks for "evidence you actually
4
+ trained — at minimum loss and reward plots from a real run." Since we run as
5
+ a script (not a notebook), nothing renders automatically. This module:
6
+
7
+ * Snapshots ``trainer.state.log_history`` every N steps via a TrainerCallback
8
+ (so a crashed run still leaves partial evidence behind), and
9
+ * Dumps a final set of artifacts (CSV, JSON, PNGs) after ``trainer.train()``.
10
+
11
+ All artifacts land in the trainer's ``output_dir`` so they ride back to the
12
+ Hugging Face Hub when ``push_to_hub=True``.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import csv
17
+ import json
18
+ import logging
19
+ from pathlib import Path
20
+ from typing import Any, Dict, Iterable, List, Optional
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # Reward keys we track. TRL logs reward functions under "rewards/<func_name>"
26
+ # (and a single-scalar "reward" = sum of weighted rewards).
27
+ PRIMARY_REWARD_KEY = "rewards/reward_total"
28
+ PHASE_REWARD_KEYS = (
29
+ "rewards/reward_market",
30
+ "rewards/reward_warehouse",
31
+ "rewards/reward_showroom",
32
+ )
33
+ LOSS_KEY = "loss"
34
+ STEP_KEY = "step"
35
+
36
+
37
+ def _flatten_log_history(log_history: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
38
+ """Make sure every row carries a `step` field even when TRL omits it on epoch logs."""
39
+ cleaned: List[Dict[str, Any]] = []
40
+ last_step = 0
41
+ for row in log_history:
42
+ step = row.get("step", row.get("global_step", last_step))
43
+ last_step = step or last_step
44
+ merged = {"step": last_step, **{k: v for k, v in row.items() if k != "step"}}
45
+ cleaned.append(merged)
46
+ return cleaned
47
+
48
+
49
+ def _series(rows: List[Dict[str, Any]], key: str) -> List[tuple]:
50
+ """Return ``[(step, value), ...]`` for the given metric key."""
51
+ out: List[tuple] = []
52
+ for r in rows:
53
+ if key in r and r[key] is not None:
54
+ try:
55
+ out.append((int(r["step"]), float(r[key])))
56
+ except (TypeError, ValueError):
57
+ continue
58
+ return out
59
+
60
+
61
+ def _save_csv(rows: List[Dict[str, Any]], path: Path) -> None:
62
+ if not rows:
63
+ return
64
+ columns: List[str] = []
65
+ seen = set()
66
+ for r in rows:
67
+ for k in r.keys():
68
+ if k not in seen:
69
+ seen.add(k)
70
+ columns.append(k)
71
+ with path.open("w", newline="") as f:
72
+ writer = csv.DictWriter(f, fieldnames=columns)
73
+ writer.writeheader()
74
+ writer.writerows(rows)
75
+
76
+
77
+ def _save_json(rows: List[Dict[str, Any]], path: Path) -> None:
78
+ with path.open("w") as f:
79
+ json.dump(rows, f, indent=2, default=str)
80
+
81
+
82
+ def _try_plot(
83
+ series: Iterable[tuple],
84
+ title: str,
85
+ ylabel: str,
86
+ out_path: Path,
87
+ *,
88
+ label: Optional[str] = None,
89
+ ) -> bool:
90
+ """Draw a single-series line plot. Silently no-ops if matplotlib is missing."""
91
+ try:
92
+ import matplotlib
93
+
94
+ matplotlib.use("Agg")
95
+ import matplotlib.pyplot as plt
96
+ except Exception as exc:
97
+ logger.warning("matplotlib unavailable, skipping %s (%s)", out_path.name, exc)
98
+ return False
99
+
100
+ pts = list(series)
101
+ if not pts:
102
+ logger.warning("no data for %s, skipping plot", out_path.name)
103
+ return False
104
+ xs, ys = zip(*pts)
105
+ fig, ax = plt.subplots(figsize=(8, 4.5))
106
+ ax.plot(xs, ys, marker="o", linewidth=1.5, label=label or ylabel)
107
+ ax.set_xlabel("training step")
108
+ ax.set_ylabel(ylabel)
109
+ ax.set_title(title)
110
+ ax.grid(True, alpha=0.3)
111
+ if label:
112
+ ax.legend(loc="best")
113
+ fig.tight_layout()
114
+ fig.savefig(out_path, dpi=120)
115
+ plt.close(fig)
116
+ return True
117
+
118
+
119
+ def _try_plot_multi(
120
+ name_to_series: Dict[str, Iterable[tuple]],
121
+ title: str,
122
+ ylabel: str,
123
+ out_path: Path,
124
+ ) -> bool:
125
+ """Draw a multi-series line plot."""
126
+ try:
127
+ import matplotlib
128
+
129
+ matplotlib.use("Agg")
130
+ import matplotlib.pyplot as plt
131
+ except Exception as exc:
132
+ logger.warning("matplotlib unavailable, skipping %s (%s)", out_path.name, exc)
133
+ return False
134
+
135
+ fig, ax = plt.subplots(figsize=(8.5, 5))
136
+ drew_any = False
137
+ for label, pts in name_to_series.items():
138
+ pts = list(pts)
139
+ if not pts:
140
+ continue
141
+ xs, ys = zip(*pts)
142
+ ax.plot(xs, ys, marker="o", linewidth=1.3, label=label)
143
+ drew_any = True
144
+ if not drew_any:
145
+ plt.close(fig)
146
+ logger.warning("no data for %s, skipping plot", out_path.name)
147
+ return False
148
+ ax.set_xlabel("training step")
149
+ ax.set_ylabel(ylabel)
150
+ ax.set_title(title)
151
+ ax.grid(True, alpha=0.3)
152
+ ax.legend(loc="best")
153
+ fig.tight_layout()
154
+ fig.savefig(out_path, dpi=120)
155
+ plt.close(fig)
156
+ return True
157
+
158
+
159
+ def _summary_stats(series: List[tuple]) -> Dict[str, float]:
160
+ if not series:
161
+ return {"final": 0.0, "max": 0.0, "min": 0.0, "mean": 0.0, "n": 0}
162
+ ys = [v for _, v in series]
163
+ return {
164
+ "final": float(ys[-1]),
165
+ "max": float(max(ys)),
166
+ "min": float(min(ys)),
167
+ "mean": float(sum(ys) / len(ys)),
168
+ "n": len(ys),
169
+ }
170
+
171
+
172
+ def save_training_artifacts(
173
+ log_history: List[Dict[str, Any]],
174
+ output_dir: str | Path,
175
+ *,
176
+ run_config: Optional[Dict[str, Any]] = None,
177
+ ) -> Dict[str, Any]:
178
+ """Write metrics + loss/reward plots into ``output_dir``.
179
+
180
+ Returns the summary dict that was also written to ``training_summary.json``.
181
+ """
182
+ out = Path(output_dir)
183
+ out.mkdir(parents=True, exist_ok=True)
184
+
185
+ rows = _flatten_log_history(log_history)
186
+ _save_csv(rows, out / "metrics.csv")
187
+ _save_json(rows, out / "metrics.json")
188
+
189
+ loss_series = _series(rows, LOSS_KEY)
190
+ total_reward_series = _series(rows, PRIMARY_REWARD_KEY)
191
+ # Some TRL versions log a flat "reward" scalar in addition. Prefer the
192
+ # named primary; fall back to "reward" if the named one is empty.
193
+ if not total_reward_series:
194
+ total_reward_series = _series(rows, "reward")
195
+
196
+ phase_series = {
197
+ "market": _series(rows, "rewards/reward_market"),
198
+ "warehouse": _series(rows, "rewards/reward_warehouse"),
199
+ "showroom": _series(rows, "rewards/reward_showroom"),
200
+ }
201
+
202
+ _try_plot(
203
+ loss_series,
204
+ title="Training loss (GRPO)",
205
+ ylabel="loss",
206
+ out_path=out / "loss_curve.png",
207
+ label="loss",
208
+ )
209
+ _try_plot(
210
+ total_reward_series,
211
+ title="Reward (total) — env cumulative_reward in [0, 1]",
212
+ ylabel="reward",
213
+ out_path=out / "reward_total_curve.png",
214
+ label="reward_total",
215
+ )
216
+ _try_plot_multi(
217
+ {
218
+ "reward_total": total_reward_series,
219
+ **{f"reward_{k}": v for k, v in phase_series.items()},
220
+ },
221
+ title="Rewards over training",
222
+ ylabel="reward",
223
+ out_path=out / "reward_curve.png",
224
+ )
225
+
226
+ summary: Dict[str, Any] = {
227
+ "loss": _summary_stats(loss_series),
228
+ "reward_total": _summary_stats(total_reward_series),
229
+ "reward_market": _summary_stats(phase_series["market"]),
230
+ "reward_warehouse": _summary_stats(phase_series["warehouse"]),
231
+ "reward_showroom": _summary_stats(phase_series["showroom"]),
232
+ "n_log_rows": len(rows),
233
+ "output_dir": str(out.resolve()),
234
+ }
235
+ if run_config is not None:
236
+ summary["run_config"] = run_config
237
+
238
+ with (out / "training_summary.json").open("w") as f:
239
+ json.dump(summary, f, indent=2, default=str)
240
+
241
+ logger.info("Wrote training artifacts to %s", out.resolve())
242
+ return summary
243
+
244
+
245
+ def build_metrics_callback(output_dir: str | Path, snapshot_every: int = 5):
246
+ """Return a TrainerCallback that snapshots metrics every N steps + on end.
247
+
248
+ Imported lazily so this module can be inspected on a machine without
249
+ transformers installed (e.g. for the local --smoke run).
250
+ """
251
+ from transformers.trainer_callback import TrainerCallback
252
+
253
+ out = Path(output_dir)
254
+
255
+ class MetricsSaverCallback(TrainerCallback):
256
+ """Persist metrics CSV/JSON + plots periodically and at the end."""
257
+
258
+ def __init__(self) -> None:
259
+ self._last_snapshot_step = -1
260
+
261
+ def _snapshot(self, state) -> None:
262
+ try:
263
+ save_training_artifacts(list(state.log_history or []), out)
264
+ except Exception as exc: # never let plotting kill training
265
+ logger.warning("metrics snapshot failed: %s", exc)
266
+
267
+ def on_log(self, args, state, control, **kwargs):
268
+ step = int(getattr(state, "global_step", 0) or 0)
269
+ if step <= 0:
270
+ return control
271
+ if (step - self._last_snapshot_step) >= max(snapshot_every, 1):
272
+ self._snapshot(state)
273
+ self._last_snapshot_step = step
274
+ return control
275
+
276
+ def on_train_end(self, args, state, control, **kwargs):
277
+ self._snapshot(state)
278
+ return control
279
+
280
+ return MetricsSaverCallback()
281
+
282
+
283
+ def upload_training_artifacts_to_hub(
284
+ output_dir: str | Path,
285
+ repo_id: str,
286
+ *,
287
+ path_in_repo: str = "training_artifacts",
288
+ ) -> list[str]:
289
+ """Upload small evidence files to the same model repo (PNGs, CSV, JSON).
290
+
291
+ ``GRPOTrainer.push_to_hub`` typically uploads weights/tokenizer only; this
292
+ adds ``metrics.csv``, ``loss_curve.png``, and related files under
293
+ ``path_in_repo/`` on the Hub so they survive ephemeral cloud jobs.
294
+ """
295
+ from huggingface_hub import HfApi, create_repo
296
+
297
+ out = Path(output_dir)
298
+ if not out.is_dir():
299
+ return []
300
+
301
+ create_repo(repo_id, repo_type="model", exist_ok=True)
302
+ api = HfApi()
303
+ names = (
304
+ "metrics.csv",
305
+ "metrics.json",
306
+ "loss_curve.png",
307
+ "reward_curve.png",
308
+ "reward_total_curve.png",
309
+ "training_summary.json",
310
+ )
311
+ prefix = path_in_repo.strip("/")
312
+ uploaded: list[str] = []
313
+ for name in names:
314
+ path = out / name
315
+ if not path.is_file():
316
+ continue
317
+ dest = f"{prefix}/{name}" if prefix else name
318
+ api.upload_file(
319
+ path_or_fileobj=str(path),
320
+ path_in_repo=dest,
321
+ repo_id=repo_id,
322
+ repo_type="model",
323
+ )
324
+ uploaded.append(dest)
325
+ return uploaded
training/prompts.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """System prompt + per-turn user-prompt builder for the JewelryShop env.
2
+
3
+ Logic mirrors `inference.py`'s `build_user_prompt` so that whatever the model
4
+ saw during inference evaluation it also sees during training. Kept as a plain
5
+ sync function (no asyncio) so it composes cleanly with TRL's rollout_func.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import textwrap
11
+ from typing import List
12
+
13
+
14
+ SYSTEM_PROMPT = textwrap.dedent(
15
+ """
16
+ You are an expert agent running a jewelry shop. The episode runs in 3 phases
17
+ and may loop back to MARKET if the warehouse runs out of gold. The episode
18
+ reward is the SUM of per-step partial rewards across the whole episode and
19
+ is bounded in [0, 1]. Each task weights the phases differently:
20
+ - market_timing -> phase 1 = 0.6, phase 2 = 0.2, phase 3 = 0.2
21
+ - demand_crafter -> phase 1 = 0.2, phase 2 = 0.6, phase 3 = 0.2
22
+ - profit_negotiator -> phase 1 = 0.2, phase 2 = 0.2, phase 3 = 0.6
23
+
24
+ ## Phase 1: MARKET (buy / wait)
25
+ Two modes:
26
+ - synthetic mode: gold price moves randomly each WAIT step within a round cap.
27
+ - real mode: gold price comes from a live source (yfinance: GC=F),
28
+ no round cap; WAIT just refreshes the live quote.
29
+ Coordination from the warehouse:
30
+ - inventory_urgent=True / cannot_wait=True means you MUST buy now;
31
+ WAIT will be blocked. Submit "buy X.XX" with an affordable troy-oz qty.
32
+ Behavior:
33
+ - If you can wait, observe the price trend in gold_price_history before buying.
34
+ - Reserve cash for labor (ring=$200, necklace=$300, bracelet=$100).
35
+ - Respond: "buy X.XX" (troy oz of gold) or "wait".
36
+
37
+ ## Phase 2: WAREHOUSE (choose product)
38
+ You see two demand fields:
39
+ - demand : the TRUE per-product demand for THIS episode (ground truth).
40
+ - demand_forecast : a NOISY signal you can also lean on for planning.
41
+ Products: ring (1oz + $200), necklace (2oz + $300), bracelet (0.5oz + $100).
42
+ If you don't have enough gold to craft your choice, the env may BOUNCE you back
43
+ to MARKET to buy more (up to max_market_reentries times). After max bounces or
44
+ when truly broke, the customer leaves and the episode ends.
45
+ Respond: "ring", "necklace", or "bracelet".
46
+
47
+ ## Phase 3: SHOWROOM (negotiate)
48
+ The customer makes an offer; if you counter, they raise it ~5% per round,
49
+ up to 5 rounds. After 5 rounds with no acceptance, the customer leaves
50
+ (no phase-3 reward). Reject also gives 0 phase-3 reward.
51
+ Respond: "I accept" or a counter like "How about $X?". NEVER explicitly reject.
52
+
53
+ CRITICAL: Respond with ONLY the action value. No explanations.
54
+ """
55
+ ).strip()
56
+
57
+
58
+ def build_user_prompt(step: int, obs, last_reward: float, history: List[str]) -> str:
59
+ """Format a single observation into a user prompt the LLM sees this turn.
60
+
61
+ Mirrors inference.py:build_user_prompt so the model sees the same input shape
62
+ during training and at evaluation time.
63
+ """
64
+ history_block = "\n".join(history[-4:]) if history else "None"
65
+
66
+ if obs.phase == "market":
67
+ prices = getattr(obs, "gold_price_history", []) or []
68
+ trend = ""
69
+ if len(prices) >= 2:
70
+ if prices[-1] < prices[-2]:
71
+ trend = "FALLING (might keep dropping, consider waiting)"
72
+ else:
73
+ trend = "RISING (buy now before it gets more expensive)"
74
+
75
+ if getattr(obs, "cannot_wait", False):
76
+ trend = (
77
+ "URGENT: inventory needs gold now — you cannot wait; buy at the current "
78
+ "live quote with an affordable gold_qty (troy oz)."
79
+ )
80
+
81
+ max_rounds = getattr(obs, "max_market_rounds", None)
82
+ rounds_left = (max_rounds - getattr(obs, "market_round", 0)) if max_rounds else None
83
+
84
+ reserve = 300.0
85
+ gold_price = getattr(obs, "gold_price", 0) or 0
86
+ cash = getattr(obs, "cash", 0) or 0
87
+ if gold_price > 0:
88
+ raw_qty = (cash - reserve) / gold_price
89
+ suggested_qty = max(math.floor(raw_qty * 100) / 100, 0.01)
90
+ else:
91
+ suggested_qty = 1.0
92
+
93
+ rl = "unlimited" if rounds_left is None else str(rounds_left)
94
+ phase_hint = (
95
+ f"Price: ${gold_price}/oz ({getattr(obs, 'gold_price_source', '') or 'n/a'}). "
96
+ f"Price history: {prices}. Trend: {trend}. "
97
+ f"Rounds / waits so far: {getattr(obs, 'market_round', 0)}; cap: {rl}. "
98
+ f"Gold on hand: {getattr(obs, 'gold_oz', 0)} troy oz "
99
+ f"(~{getattr(obs, 'gold_grams', 0):.2f} g). "
100
+ f"If buying, suggested qty: {suggested_qty} oz (reserves $300 for labor). "
101
+ f"Respond: 'buy {suggested_qty}' or 'wait'"
102
+ )
103
+
104
+ elif obs.phase == "warehouse":
105
+ demand = getattr(obs, "demand", {}) or {}
106
+ forecast = getattr(obs, "demand_forecast", {}) or {}
107
+ best_product = max(demand, key=demand.get) if demand else "ring"
108
+ phase_hint = (
109
+ f"Demand (episode): ring={demand.get('ring', 0):.0%}, "
110
+ f"necklace={demand.get('necklace', 0):.0%}, "
111
+ f"bracelet={demand.get('bracelet', 0):.0%}. "
112
+ f"Forecast (noisy): ring={forecast.get('ring', 0):.0%}, "
113
+ f"necklace={forecast.get('necklace', 0):.0%}, "
114
+ f"bracelet={forecast.get('bracelet', 0):.0%}. "
115
+ f"Highest demand: {best_product}. "
116
+ f"You have {getattr(obs, 'gold_oz', 0)}oz gold and "
117
+ f"${getattr(obs, 'cash', 0)} cash. "
118
+ f"Respond with EXACTLY: {best_product}"
119
+ )
120
+
121
+ elif obs.phase == "showroom":
122
+ cost_basis = getattr(obs, "cost_basis", 0) or 0
123
+ current_offer = getattr(obs, "current_offer", 0) or 0
124
+ negotiation_round = getattr(obs, "negotiation_round", 0) or 0
125
+
126
+ margin = ""
127
+ if current_offer and cost_basis > 0:
128
+ margin_pct = ((current_offer - cost_basis) / cost_basis) * 100
129
+ margin = f"Margin: {margin_pct:+.1f}%. "
130
+
131
+ should_accept = negotiation_round >= 4 or (
132
+ current_offer and cost_basis > 0 and current_offer > cost_basis * 1.3
133
+ )
134
+
135
+ if should_accept:
136
+ phase_hint = (
137
+ f"Cost: ${cost_basis}. Offer: ${current_offer}. {margin}"
138
+ f"Round {negotiation_round}/5. "
139
+ f"Respond with EXACTLY: I accept"
140
+ )
141
+ else:
142
+ counter_msgs = [
143
+ "I need a better price for this quality piece",
144
+ "That's too low, this craftsmanship deserves more",
145
+ f"How about ${round(cost_basis * 1.4, 2)}?",
146
+ f"I can't go below ${round(cost_basis * 1.3, 2)}",
147
+ ]
148
+ msg = counter_msgs[min(negotiation_round, len(counter_msgs) - 1)]
149
+ phase_hint = (
150
+ f"Cost: ${cost_basis}. Offer: ${current_offer}. {margin}"
151
+ f"Round {negotiation_round}/5. "
152
+ f"DO NOT ACCEPT. Counter-offer. "
153
+ f"Respond with EXACTLY: {msg}"
154
+ )
155
+ else:
156
+ phase_hint = ""
157
+
158
+ return textwrap.dedent(
159
+ f"""
160
+ Step: {step} | Phase: {obs.phase} | Last reward: {last_reward:.2f}
161
+ Cash: ${getattr(obs, 'cash', 0)} | Gold: {getattr(obs, 'gold_oz', 0)}oz | Rings: {getattr(obs, 'inventory', {})}
162
+ Gold Price: ${getattr(obs, 'gold_price', 0)}/oz
163
+ Env Message: {getattr(obs, 'message', '')}
164
+
165
+ {phase_hint}
166
+
167
+ History: {history_block}
168
+ """
169
+ ).strip()
training/rewards.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reward functions consumed by GRPOTrainer.
2
+
3
+ Design choice: ONE primary reward (``reward_total``) drives advantages, while
4
+ the three per-phase rewards are exposed for *monitoring only* via TRL's logged
5
+ reward metrics. Their config weight should be set to 0.0 to avoid double
6
+ counting the cumulative phase sum.
7
+
8
+ If you actually want phase-level shaping in the gradient, change the
9
+ GRPOConfig ``reward_weights`` to e.g. [1.0, 0.2, 0.2, 0.2].
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, List
14
+
15
+
16
+ def _pull(kwargs: dict, key: str, n: int) -> List[float]:
17
+ vals = kwargs.get(key)
18
+ if not vals:
19
+ return [0.0] * n
20
+ return [float(v) for v in vals]
21
+
22
+
23
+ def reward_total(completions: List[Any], **kwargs) -> List[float]:
24
+ """Authoritative trajectory return: env's cumulative_reward in [0, 1]."""
25
+ return _pull(kwargs, "total_reward", len(completions))
26
+
27
+
28
+ def reward_market(completions: List[Any], **kwargs) -> List[float]:
29
+ """Sum of per-step partials emitted while phase == 'market'. Monitoring."""
30
+ return _pull(kwargs, "market_reward", len(completions))
31
+
32
+
33
+ def reward_warehouse(completions: List[Any], **kwargs) -> List[float]:
34
+ """Sum of per-step partials emitted while phase == 'warehouse'. Monitoring."""
35
+ return _pull(kwargs, "warehouse_reward", len(completions))
36
+
37
+
38
+ def reward_showroom(completions: List[Any], **kwargs) -> List[float]:
39
+ """Sum of per-step partials emitted while phase == 'showroom'. Monitoring."""
40
+ return _pull(kwargs, "showroom_reward", len(completions))
41
+
42
+
43
+ # Convenience tuple for single-import use
44
+ ALL_REWARDS = (reward_total, reward_market, reward_warehouse, reward_showroom)
45
+
46
+ # Matching weights so only `reward_total` contributes to the GRPO advantage.
47
+ # Plug this straight into GRPOConfig(reward_weights=REWARD_WEIGHTS_MONITOR_ONLY).
48
+ REWARD_WEIGHTS_MONITOR_ONLY = [1.0, 0.0, 0.0, 0.0]
training/rollout.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Module 5 (TRL OpenEnv Wordle) style rollout for ShopManagerEng.
2
+
3
+ Two public symbols:
4
+
5
+ * ``rollout_once(...)`` — plays a single multi-turn jewelry-shop episode
6
+ against an already-connected sync env client and returns the per-episode
7
+ signals TRL/GRPO needs.
8
+ * ``build_rollout_func(...)`` — closure factory that returns the
9
+ ``rollout_func(prompts, trainer=None)`` callable handed to ``GRPOTrainer``.
10
+
11
+ The pattern (canonical for OpenEnv + TRL >= 0.17):
12
+
13
+ sync_env = env.sync(); sync_env.connect() # one persistent WS
14
+ trainer = GRPOTrainer(..., rollout_func=rollout_func)
15
+ trainer.train()
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from typing import Any, Callable, Dict, List, Optional
21
+
22
+ try:
23
+ from .parse_action import parse_model_text_to_action
24
+ from .prompts import build_user_prompt
25
+ except ImportError:
26
+ from training.parse_action import parse_model_text_to_action
27
+ from training.prompts import build_user_prompt
28
+
29
+
30
+ # Set of valid task ids supported by openenv.yaml; first one is the default.
31
+ VALID_TASKS = ("market_timing", "demand_crafter", "profit_negotiator")
32
+ _TASK_RE = re.compile(r"\[TASK=(\w+)\]")
33
+
34
+
35
+ def extract_task_id(prompt_text: str, default: str = VALID_TASKS[0]) -> str:
36
+ """Pull the [TASK=...] tag the dataset embeds, or fall back to the default."""
37
+ m = _TASK_RE.search(prompt_text or "")
38
+ if not m:
39
+ return default
40
+ candidate = m.group(1)
41
+ return candidate if candidate in VALID_TASKS else default
42
+
43
+
44
+ def _apply_chat_template(tokenizer, messages, model_name: str = "") -> str:
45
+ """Apply chat template, opting out of Qwen3 'thinking' mode when applicable."""
46
+ template_kwargs: Dict[str, Any] = {
47
+ "add_generation_prompt": True,
48
+ "tokenize": False,
49
+ }
50
+ # Qwen3 family supports the `enable_thinking` switch — disable it for short
51
+ # action outputs. Other models silently ignore unknown kwargs in newer
52
+ # transformers; older ones may raise, hence the lower() guard.
53
+ if "qwen3" in (model_name or "").lower():
54
+ template_kwargs["enable_thinking"] = False
55
+ return tokenizer.apply_chat_template(messages, **template_kwargs)
56
+
57
+
58
+ def rollout_once(
59
+ *,
60
+ trainer,
61
+ sync_env,
62
+ tokenizer,
63
+ dataset_prompt: str,
64
+ system_prompt: str,
65
+ max_turns: int,
66
+ model_name: str = "",
67
+ ) -> Dict[str, Any]:
68
+ """Play one full jewelry-shop episode and return per-episode signals.
69
+
70
+ Returns the dict shape TRL's GRPO loop expects: ``prompt_ids``,
71
+ ``completion_ids``, ``logprobs`` for **a single** vLLM forward (the **last
72
+ environment turn** in the episode) plus reward signals for reward
73
+ functions.
74
+
75
+ We **do not** concatenate multiple turns into one list. In ``GRPOTrainer``,
76
+ each batch row is ``cat(prompt_ids, completion_ids)``; vLLM's per-token
77
+ ``logprobs`` must be for **that** exact sequence, or the importance-sampling
78
+ ratio (vLLM vs reference forward) collapses. Multi-turn play still runs in
79
+ the environment; the policy gradient is applied to the **last** action's
80
+ tokens, while ``total_reward`` remains the full episode return for GRPO
81
+ group advantages.
82
+ """
83
+ # Late import: trl.experimental.openenv only exists for trl >= 0.17.
84
+ from trl.experimental.openenv import generate_rollout_completions
85
+
86
+ task_id = extract_task_id(dataset_prompt)
87
+ result = sync_env.reset(task_id=task_id)
88
+ obs = result.observation
89
+
90
+ # One (prompt_ids, completion_ids, logprobs) per vLLM call; last turn only
91
+ # is returned to TRL (see module docstring).
92
+ turn_traces: List[Dict[str, Any]] = []
93
+
94
+ history: List[str] = []
95
+ last_reward = 0.0
96
+ phase_rewards = {"market": 0.0, "warehouse": 0.0, "showroom": 0.0}
97
+
98
+ for turn in range(1, max_turns + 1):
99
+ if result.done:
100
+ break
101
+
102
+ user_prompt = build_user_prompt(turn, obs, last_reward, history)
103
+ messages = [
104
+ {"role": "system", "content": system_prompt},
105
+ {"role": "user", "content": user_prompt},
106
+ ]
107
+ prompt_text = _apply_chat_template(tokenizer, messages, model_name=model_name)
108
+
109
+ rollout_outputs = generate_rollout_completions(trainer, [prompt_text])[0]
110
+ p_ids = rollout_outputs["prompt_ids"]
111
+ c_ids = rollout_outputs["completion_ids"]
112
+ lps = rollout_outputs["logprobs"]
113
+ p_list = p_ids.tolist() if hasattr(p_ids, "tolist") else list(p_ids)
114
+ c_list = c_ids.tolist() if hasattr(c_ids, "tolist") else list(c_ids)
115
+ turn_traces.append(
116
+ {
117
+ "prompt_ids": p_list,
118
+ "completion_ids": c_list,
119
+ "logprobs": [float(x) for x in lps],
120
+ }
121
+ )
122
+
123
+ completion_text = rollout_outputs.get("text") or tokenizer.decode(
124
+ rollout_outputs["completion_ids"], skip_special_tokens=True
125
+ )
126
+
127
+ current_phase = obs.phase
128
+ action, raw_action_str = parse_model_text_to_action(current_phase, completion_text)
129
+
130
+ result = sync_env.step(action)
131
+ obs = result.observation
132
+ step_reward = float(result.reward or 0.0)
133
+ last_reward = step_reward
134
+
135
+ if current_phase in phase_rewards:
136
+ phase_rewards[current_phase] += step_reward
137
+
138
+ history.append(
139
+ f"Step {turn} ({current_phase}): {raw_action_str!r} -> reward {step_reward:+.2f}"
140
+ )
141
+
142
+ total_reward = float(getattr(obs, "cumulative_reward", sum(phase_rewards.values())))
143
+ total_reward = max(0.0, min(total_reward, 1.0))
144
+
145
+ if not turn_traces:
146
+ raise ValueError(
147
+ "rollout_once produced no vLLM turns (max_turns too low or env ended "
148
+ "before the first action)."
149
+ )
150
+ last = turn_traces[-1]
151
+
152
+ return {
153
+ "prompt_ids": last["prompt_ids"],
154
+ "completion_ids": last["completion_ids"],
155
+ "logprobs": last["logprobs"],
156
+ "total_reward": total_reward,
157
+ "market_reward": float(phase_rewards["market"]),
158
+ "warehouse_reward": float(phase_rewards["warehouse"]),
159
+ "showroom_reward": float(phase_rewards["showroom"]),
160
+ }
161
+
162
+
163
+ def build_rollout_func(
164
+ *,
165
+ sync_env,
166
+ tokenizer,
167
+ system_prompt: str,
168
+ max_turns: int = 15,
169
+ model_name: str = "",
170
+ ) -> Callable[..., Dict[str, List]]:
171
+ """Return ``rollout_func(prompts, trainer=None)`` closing over the env client.
172
+
173
+ A fresh episode is run for each prompt; the same persistent ``sync_env``
174
+ is reused across all prompts (single WebSocket session — matches Module 5).
175
+ """
176
+
177
+ def rollout_func(prompts: List[str], trainer=None) -> Dict[str, List]:
178
+ episode_prompt_ids: List[List[int]] = []
179
+ episode_completion_ids: List[List[int]] = []
180
+ episode_logprobs: List[List[float]] = []
181
+ total_rewards: List[float] = []
182
+ market_rewards: List[float] = []
183
+ warehouse_rewards: List[float] = []
184
+ showroom_rewards: List[float] = []
185
+
186
+ for prompt_text in prompts:
187
+ ep = rollout_once(
188
+ trainer=trainer,
189
+ sync_env=sync_env,
190
+ tokenizer=tokenizer,
191
+ dataset_prompt=prompt_text,
192
+ system_prompt=system_prompt,
193
+ max_turns=max_turns,
194
+ model_name=model_name,
195
+ )
196
+ episode_prompt_ids.append(ep["prompt_ids"])
197
+ episode_completion_ids.append(ep["completion_ids"])
198
+ episode_logprobs.append(ep["logprobs"])
199
+ total_rewards.append(ep["total_reward"])
200
+ market_rewards.append(ep["market_reward"])
201
+ warehouse_rewards.append(ep["warehouse_reward"])
202
+ showroom_rewards.append(ep["showroom_reward"])
203
+
204
+ return {
205
+ "prompt_ids": episode_prompt_ids,
206
+ "completion_ids": episode_completion_ids,
207
+ "logprobs": episode_logprobs,
208
+ "total_reward": total_rewards,
209
+ "market_reward": market_rewards,
210
+ "warehouse_reward": warehouse_rewards,
211
+ "showroom_reward": showroom_rewards,
212
+ }
213
+
214
+ return rollout_func
training/training_artifacts_v1/loss_curve.png ADDED
training/training_artifacts_v1/metrics.csv ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ step,loss,grad_norm,learning_rate,num_tokens,completions/mean_length,completions/min_length,completions/max_length,completions/clipped_ratio,completions/mean_terminated_length,completions/min_terminated_length,completions/max_terminated_length,rewards/reward_total/mean,rewards/reward_total/std,rewards/reward_market/mean,rewards/reward_market/std,rewards/reward_warehouse/mean,rewards/reward_warehouse/std,rewards/reward_showroom/mean,rewards/reward_showroom/std,reward,reward_std,frac_reward_zero_std,sampling/sampling_logp_difference/mean,sampling/sampling_logp_difference/max,sampling/importance_sampling_ratio/min,sampling/importance_sampling_ratio/mean,sampling/importance_sampling_ratio/max,entropy,clip_ratio/low_mean,clip_ratio/low_min,clip_ratio/high_mean,clip_ratio/high_max,clip_ratio/region_mean,step_time,epoch,train_runtime,train_samples_per_second,train_steps_per_second,total_flos,train_loss
2
+ 1,-0.009,12.235088348388672,0.0,27758.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7793656587600708,0.12745577096939087,0.25,0.13440430164337158,0.42500001192092896,0.20160645246505737,0.10436563193798065,0.069697305560112,0.7793656587600708,0.12745577096939087,0.0,0.005470390431582928,0.22214925289154053,0.8007970452308655,0.9949374794960022,1.076386570930481,0.028439456821217846,0.0,0.0,0.0,0.0,0.0,19.228480510413647,0.05555555555555555,,,,,
3
+ 2,0.0723,60.551937103271484,5.000000000000001e-07,55324.0,3.25,3.0,7.0,0.0,3.25,3.0,7.0,0.719434380531311,0.1505809724330902,0.30000001192092896,0.17597654461860657,0.30000001192092896,0.17597654461860657,0.11943437159061432,0.07055392116308212,0.719434380531311,0.1505809724330902,0.0,0.01085888221859932,0.28092825412750244,0.8382877111434937,1.0260276794433594,1.324359655380249,0.036137547835437545,0.0,0.0,0.0,0.0,0.0,17.92062332853675,0.1111111111111111,,,,,
4
+ 3,0.1153,232.934814453125,1.0000000000000002e-06,83000.0,3.5,3.0,7.0,0.0,3.5,3.0,7.0,0.7885687351226807,0.1279384195804596,0.32500001788139343,0.18837162852287292,0.375,0.20160646736621857,0.08856874704360962,0.07010025531053543,0.7885687351226807,0.1279384344816208,0.0,0.0251829382032156,0.6850378513336182,0.5226751565933228,1.0358223915100098,1.9838452339172363,0.03248842835137111,0.0,0.0,0.0,0.0,0.0,17.184778176248074,0.16666666666666666,,,,,
5
+ 4,0.0243,13.620709419250488,1.5e-06,110708.0,3.125,3.0,7.0,0.0,3.125,3.0,7.0,0.7762374877929688,0.13016164302825928,0.32499998807907104,0.18837164342403412,0.3500000238418579,0.1967477649450302,0.10123749077320099,0.06825561076402664,0.7762374877929688,0.13016162812709808,0.0,0.007013080175966024,0.2646750509738922,0.7674550414085388,0.9837819337844849,1.0260045528411865,0.03743034108288157,0.0,0.0,0.0,0.0,0.0,17.90316915512085,0.2222222222222222,,,,,
6
+ 5,0.0004,2.0591225624084473,2.0000000000000003e-06,138452.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7784374952316284,0.12858590483665466,0.30000001192092896,0.17597654461860657,0.375,0.20160646736621857,0.10343749821186066,0.06380020827054977,0.7784374952316284,0.12858593463897705,0.0,0.001465568202547729,0.05673474818468094,0.9749767780303955,1.0017430782318115,1.058376669883728,0.008188390799773515,0.0,0.0,0.0,0.0,0.0,17.8210555203259,0.2777777777777778,,,,,
7
+ 6,0.0915,31.611696243286133,2.5e-06,166252.0,3.125,3.0,7.0,0.0,3.125,3.0,7.0,0.7915937900543213,0.12960509955883026,0.375,0.20160646736621857,0.32500001788139343,0.18837162852287292,0.09159374982118607,0.0639258474111557,0.7915937900543213,0.12960509955883026,0.0,0.008641102351248264,0.8236088752746582,0.9935171008110046,1.0399717092514038,2.278707504272461,0.007084679029730978,0.0,0.0,0.0,0.0,0.0,18.056264080107212,0.3333333333333333,,,,,
8
+ 7,-0.0,0.281630277633667,3e-06,193950.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7600874900817871,0.13816162943840027,0.32500001788139343,0.18837162852287292,0.32500001788139343,0.18837162852287292,0.11008749902248383,0.07028691470623016,0.7600874900817871,0.13816164433956146,0.0,3.554227441782132e-05,0.0027569520752876997,0.997247576713562,0.9999052286148071,1.0000724792480469,0.0005298306713825696,0.0,0.0,0.0,0.0,0.0,17.43799263238907,0.3888888888888889,,,,,
9
+ 8,0.0,0.00018881642608903348,3.5e-06,221677.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7622687816619873,0.1372590959072113,0.4000000059604645,0.20320022106170654,0.25,0.13440430164337158,0.11226874589920044,0.07360353320837021,0.7622687816619873,0.1372590959072113,0.0,2.545601773817907e-07,1.5496943888138048e-06,0.9999986886978149,1.000000238418579,1.0000016689300537,4.1391018498870835e-05,0.0,0.0,0.0,0.0,0.0,17.406394600868225,0.4444444444444444,,,,,
10
+ 9,0.0,0.00015632262511644512,4.000000000000001e-06,249549.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.775946855545044,0.137176051735878,0.375,0.20160646736621857,0.30000001192092896,0.17597654461860657,0.10094687342643738,0.058497413992881775,0.775946855545044,0.1371760368347168,0.0,2.918131087881193e-07,3.099441755693988e-06,0.9999978542327881,1.0,1.000001311302185,4.173239403826301e-05,0.0,0.0,0.0,0.0,0.0,18.514019537717104,0.5,,,,,
11
+ 10,0.0,9.833038348006085e-05,4.5e-06,277377.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7337374687194824,0.15055809915065765,0.2750000059604645,0.1586231142282486,0.3500000238418579,0.1967477649450302,0.10873749852180481,0.06778524816036224,0.7337374687194824,0.15055811405181885,0.0,2.719489771152439e-07,1.9073031580774114e-06,0.9999985694885254,0.9999998807907104,1.0000019073486328,4.224909940830912e-05,0.0,0.0,0.0,0.0,0.0,18.26371632888913,0.5555555555555556,,,,,
12
+ 11,-0.0,0.00015879125567153096,5e-06,305332.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7567968368530273,0.13728150725364685,0.375,0.20160646736621857,0.2750000059604645,0.1586231142282486,0.10679687559604645,0.07404065877199173,0.7567968368530273,0.13728150725364685,0.0,3.067227396513772e-07,2.6226434783893637e-06,0.9999973773956299,0.9999994039535522,1.0000019073486328,4.7876037001515215e-05,0.0,0.0,0.0,0.0,0.0,19.4472255371511,0.6111111111111112,,,,,
13
+ 12,-0.0,0.00039661259506829083,4.3750000000000005e-06,333165.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7081500291824341,0.1421954482793808,0.30000001192092896,0.17597654461860657,0.2750000059604645,0.1586231142282486,0.13315001130104065,0.07370516657829285,0.7081500291824341,0.142195463180542,0.0,4.768499479723687e-07,5.602954843197949e-06,0.9999943971633911,0.9999988675117493,1.0000016689300537,5.9777358274004655e-05,0.0,0.0,0.0,0.0,0.0,18.45015063509345,0.6666666666666666,,,,,
14
+ 13,0.0,0.0005282312049530447,3.7500000000000005e-06,361006.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7762781381607056,0.13823458552360535,0.32500001788139343,0.18837162852287292,0.3500000238418579,0.1967477649450302,0.10127812623977661,0.06158862262964249,0.7762781381607056,0.13823460042476654,0.0,6.830008487668238e-07,5.60290391149465e-06,0.9999943971633911,0.9999983310699463,1.0000022649765015,7.182803437899565e-05,0.0,0.0,0.0,0.0,0.0,17.955969959497452,0.7222222222222222,,,,,
15
+ 14,-0.0,0.000535853614564985,3.125e-06,388862.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7628874778747559,0.1295449286699295,0.30000001192092896,0.17597654461860657,0.3499999940395355,0.19674775004386902,0.11288750171661377,0.073255755007267,0.7628874778747559,0.1295449435710907,0.0,1.1113726259281975e-06,2.0979605324100703e-05,0.9999922513961792,1.0000004768371582,1.0000211000442505,9.972968496185786e-05,0.0,0.0,0.0,0.0,0.0,18.99697282537818,0.7777777777777778,,,,,
16
+ 15,0.0,0.005312301218509674,2.5e-06,416636.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7899656295776367,0.1296130269765854,0.3499999940395355,0.19674775004386902,0.3500000238418579,0.1967477649450302,0.08996562659740448,0.05463240668177605,0.7899656295776367,0.12961304187774658,0.0,4.004845322924666e-06,4.589592936099507e-05,0.9999337792396545,0.9999885559082031,1.0000029802322388,0.0001807671503684105,0.0,0.0,0.0,0.0,0.0,19.184648096561432,0.8333333333333334,,,,,
17
+ 16,-0.0,0.005995223298668861,1.8750000000000003e-06,444544.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.789996862411499,0.12048782408237457,0.375,0.20160646736621857,0.32039374113082886,0.18316024541854858,0.09460312128067017,0.062435995787382126,0.789996862411499,0.12048781663179398,0.0,4.866625204158481e-06,2.9087463190080598e-05,0.999959409236908,0.9999855160713196,1.0000004768371582,0.00019320984370096994,0.0,0.0,0.0,0.0,0.0,19.476148523390293,0.8888888888888888,,,,,
18
+ 17,0.0,0.011378168128430843,1.25e-06,472481.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.8168656826019287,0.09262391924858093,0.375,0.20160646736621857,0.3498094081878662,0.19690066576004028,0.09205625206232071,0.07046373933553696,0.8168656826019287,0.09262390434741974,0.0,5.950785180175444e-06,4.7328881919384e-05,0.9999284744262695,0.9999822974205017,1.000001072883606,0.00021975090658088448,0.0,0.0,0.0,0.0,0.0,19.344543006271124,0.9444444444444444,,,,,
19
+ 18,0.0,0.018683306872844696,6.25e-07,500266.0,3.0,3.0,3.0,0.0,3.0,3.0,3.0,0.7750625014305115,0.14084643125534058,0.3500000238418579,0.1967477649450302,0.32499998807907104,0.18837164342403412,0.10006250441074371,0.05640558898448944,0.7750625014305115,0.14084644615650177,0.0625,1.1303089195280336e-05,0.00027322862297296524,0.999584436416626,0.9999662637710571,0.9999997615814209,0.000320568448614722,0.0,0.0,0.0,0.0,0.0,17.66909484937787,1.0,,,,,
20
+ 18,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,1.0,406.352,0.738,0.044,0.0,0.016376476217475202
training/training_artifacts_v1/metrics.json ADDED
@@ -0,0 +1,695 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "step": 1,
4
+ "loss": -0.009,
5
+ "grad_norm": 12.235088348388672,
6
+ "learning_rate": 0.0,
7
+ "num_tokens": 27758.0,
8
+ "completions/mean_length": 3.0,
9
+ "completions/min_length": 3.0,
10
+ "completions/max_length": 3.0,
11
+ "completions/clipped_ratio": 0.0,
12
+ "completions/mean_terminated_length": 3.0,
13
+ "completions/min_terminated_length": 3.0,
14
+ "completions/max_terminated_length": 3.0,
15
+ "rewards/reward_total/mean": 0.7793656587600708,
16
+ "rewards/reward_total/std": 0.12745577096939087,
17
+ "rewards/reward_market/mean": 0.25,
18
+ "rewards/reward_market/std": 0.13440430164337158,
19
+ "rewards/reward_warehouse/mean": 0.42500001192092896,
20
+ "rewards/reward_warehouse/std": 0.20160645246505737,
21
+ "rewards/reward_showroom/mean": 0.10436563193798065,
22
+ "rewards/reward_showroom/std": 0.069697305560112,
23
+ "reward": 0.7793656587600708,
24
+ "reward_std": 0.12745577096939087,
25
+ "frac_reward_zero_std": 0.0,
26
+ "sampling/sampling_logp_difference/mean": 0.005470390431582928,
27
+ "sampling/sampling_logp_difference/max": 0.22214925289154053,
28
+ "sampling/importance_sampling_ratio/min": 0.8007970452308655,
29
+ "sampling/importance_sampling_ratio/mean": 0.9949374794960022,
30
+ "sampling/importance_sampling_ratio/max": 1.076386570930481,
31
+ "entropy": 0.028439456821217846,
32
+ "clip_ratio/low_mean": 0.0,
33
+ "clip_ratio/low_min": 0.0,
34
+ "clip_ratio/high_mean": 0.0,
35
+ "clip_ratio/high_max": 0.0,
36
+ "clip_ratio/region_mean": 0.0,
37
+ "step_time": 19.228480510413647,
38
+ "epoch": 0.05555555555555555
39
+ },
40
+ {
41
+ "step": 2,
42
+ "loss": 0.0723,
43
+ "grad_norm": 60.551937103271484,
44
+ "learning_rate": 5.000000000000001e-07,
45
+ "num_tokens": 55324.0,
46
+ "completions/mean_length": 3.25,
47
+ "completions/min_length": 3.0,
48
+ "completions/max_length": 7.0,
49
+ "completions/clipped_ratio": 0.0,
50
+ "completions/mean_terminated_length": 3.25,
51
+ "completions/min_terminated_length": 3.0,
52
+ "completions/max_terminated_length": 7.0,
53
+ "rewards/reward_total/mean": 0.719434380531311,
54
+ "rewards/reward_total/std": 0.1505809724330902,
55
+ "rewards/reward_market/mean": 0.30000001192092896,
56
+ "rewards/reward_market/std": 0.17597654461860657,
57
+ "rewards/reward_warehouse/mean": 0.30000001192092896,
58
+ "rewards/reward_warehouse/std": 0.17597654461860657,
59
+ "rewards/reward_showroom/mean": 0.11943437159061432,
60
+ "rewards/reward_showroom/std": 0.07055392116308212,
61
+ "reward": 0.719434380531311,
62
+ "reward_std": 0.1505809724330902,
63
+ "frac_reward_zero_std": 0.0,
64
+ "sampling/sampling_logp_difference/mean": 0.01085888221859932,
65
+ "sampling/sampling_logp_difference/max": 0.28092825412750244,
66
+ "sampling/importance_sampling_ratio/min": 0.8382877111434937,
67
+ "sampling/importance_sampling_ratio/mean": 1.0260276794433594,
68
+ "sampling/importance_sampling_ratio/max": 1.324359655380249,
69
+ "entropy": 0.036137547835437545,
70
+ "clip_ratio/low_mean": 0.0,
71
+ "clip_ratio/low_min": 0.0,
72
+ "clip_ratio/high_mean": 0.0,
73
+ "clip_ratio/high_max": 0.0,
74
+ "clip_ratio/region_mean": 0.0,
75
+ "step_time": 17.92062332853675,
76
+ "epoch": 0.1111111111111111
77
+ },
78
+ {
79
+ "step": 3,
80
+ "loss": 0.1153,
81
+ "grad_norm": 232.934814453125,
82
+ "learning_rate": 1.0000000000000002e-06,
83
+ "num_tokens": 83000.0,
84
+ "completions/mean_length": 3.5,
85
+ "completions/min_length": 3.0,
86
+ "completions/max_length": 7.0,
87
+ "completions/clipped_ratio": 0.0,
88
+ "completions/mean_terminated_length": 3.5,
89
+ "completions/min_terminated_length": 3.0,
90
+ "completions/max_terminated_length": 7.0,
91
+ "rewards/reward_total/mean": 0.7885687351226807,
92
+ "rewards/reward_total/std": 0.1279384195804596,
93
+ "rewards/reward_market/mean": 0.32500001788139343,
94
+ "rewards/reward_market/std": 0.18837162852287292,
95
+ "rewards/reward_warehouse/mean": 0.375,
96
+ "rewards/reward_warehouse/std": 0.20160646736621857,
97
+ "rewards/reward_showroom/mean": 0.08856874704360962,
98
+ "rewards/reward_showroom/std": 0.07010025531053543,
99
+ "reward": 0.7885687351226807,
100
+ "reward_std": 0.1279384344816208,
101
+ "frac_reward_zero_std": 0.0,
102
+ "sampling/sampling_logp_difference/mean": 0.0251829382032156,
103
+ "sampling/sampling_logp_difference/max": 0.6850378513336182,
104
+ "sampling/importance_sampling_ratio/min": 0.5226751565933228,
105
+ "sampling/importance_sampling_ratio/mean": 1.0358223915100098,
106
+ "sampling/importance_sampling_ratio/max": 1.9838452339172363,
107
+ "entropy": 0.03248842835137111,
108
+ "clip_ratio/low_mean": 0.0,
109
+ "clip_ratio/low_min": 0.0,
110
+ "clip_ratio/high_mean": 0.0,
111
+ "clip_ratio/high_max": 0.0,
112
+ "clip_ratio/region_mean": 0.0,
113
+ "step_time": 17.184778176248074,
114
+ "epoch": 0.16666666666666666
115
+ },
116
+ {
117
+ "step": 4,
118
+ "loss": 0.0243,
119
+ "grad_norm": 13.620709419250488,
120
+ "learning_rate": 1.5e-06,
121
+ "num_tokens": 110708.0,
122
+ "completions/mean_length": 3.125,
123
+ "completions/min_length": 3.0,
124
+ "completions/max_length": 7.0,
125
+ "completions/clipped_ratio": 0.0,
126
+ "completions/mean_terminated_length": 3.125,
127
+ "completions/min_terminated_length": 3.0,
128
+ "completions/max_terminated_length": 7.0,
129
+ "rewards/reward_total/mean": 0.7762374877929688,
130
+ "rewards/reward_total/std": 0.13016164302825928,
131
+ "rewards/reward_market/mean": 0.32499998807907104,
132
+ "rewards/reward_market/std": 0.18837164342403412,
133
+ "rewards/reward_warehouse/mean": 0.3500000238418579,
134
+ "rewards/reward_warehouse/std": 0.1967477649450302,
135
+ "rewards/reward_showroom/mean": 0.10123749077320099,
136
+ "rewards/reward_showroom/std": 0.06825561076402664,
137
+ "reward": 0.7762374877929688,
138
+ "reward_std": 0.13016162812709808,
139
+ "frac_reward_zero_std": 0.0,
140
+ "sampling/sampling_logp_difference/mean": 0.007013080175966024,
141
+ "sampling/sampling_logp_difference/max": 0.2646750509738922,
142
+ "sampling/importance_sampling_ratio/min": 0.7674550414085388,
143
+ "sampling/importance_sampling_ratio/mean": 0.9837819337844849,
144
+ "sampling/importance_sampling_ratio/max": 1.0260045528411865,
145
+ "entropy": 0.03743034108288157,
146
+ "clip_ratio/low_mean": 0.0,
147
+ "clip_ratio/low_min": 0.0,
148
+ "clip_ratio/high_mean": 0.0,
149
+ "clip_ratio/high_max": 0.0,
150
+ "clip_ratio/region_mean": 0.0,
151
+ "step_time": 17.90316915512085,
152
+ "epoch": 0.2222222222222222
153
+ },
154
+ {
155
+ "step": 5,
156
+ "loss": 0.0004,
157
+ "grad_norm": 2.0591225624084473,
158
+ "learning_rate": 2.0000000000000003e-06,
159
+ "num_tokens": 138452.0,
160
+ "completions/mean_length": 3.0,
161
+ "completions/min_length": 3.0,
162
+ "completions/max_length": 3.0,
163
+ "completions/clipped_ratio": 0.0,
164
+ "completions/mean_terminated_length": 3.0,
165
+ "completions/min_terminated_length": 3.0,
166
+ "completions/max_terminated_length": 3.0,
167
+ "rewards/reward_total/mean": 0.7784374952316284,
168
+ "rewards/reward_total/std": 0.12858590483665466,
169
+ "rewards/reward_market/mean": 0.30000001192092896,
170
+ "rewards/reward_market/std": 0.17597654461860657,
171
+ "rewards/reward_warehouse/mean": 0.375,
172
+ "rewards/reward_warehouse/std": 0.20160646736621857,
173
+ "rewards/reward_showroom/mean": 0.10343749821186066,
174
+ "rewards/reward_showroom/std": 0.06380020827054977,
175
+ "reward": 0.7784374952316284,
176
+ "reward_std": 0.12858593463897705,
177
+ "frac_reward_zero_std": 0.0,
178
+ "sampling/sampling_logp_difference/mean": 0.001465568202547729,
179
+ "sampling/sampling_logp_difference/max": 0.05673474818468094,
180
+ "sampling/importance_sampling_ratio/min": 0.9749767780303955,
181
+ "sampling/importance_sampling_ratio/mean": 1.0017430782318115,
182
+ "sampling/importance_sampling_ratio/max": 1.058376669883728,
183
+ "entropy": 0.008188390799773515,
184
+ "clip_ratio/low_mean": 0.0,
185
+ "clip_ratio/low_min": 0.0,
186
+ "clip_ratio/high_mean": 0.0,
187
+ "clip_ratio/high_max": 0.0,
188
+ "clip_ratio/region_mean": 0.0,
189
+ "step_time": 17.8210555203259,
190
+ "epoch": 0.2777777777777778
191
+ },
192
+ {
193
+ "step": 6,
194
+ "loss": 0.0915,
195
+ "grad_norm": 31.611696243286133,
196
+ "learning_rate": 2.5e-06,
197
+ "num_tokens": 166252.0,
198
+ "completions/mean_length": 3.125,
199
+ "completions/min_length": 3.0,
200
+ "completions/max_length": 7.0,
201
+ "completions/clipped_ratio": 0.0,
202
+ "completions/mean_terminated_length": 3.125,
203
+ "completions/min_terminated_length": 3.0,
204
+ "completions/max_terminated_length": 7.0,
205
+ "rewards/reward_total/mean": 0.7915937900543213,
206
+ "rewards/reward_total/std": 0.12960509955883026,
207
+ "rewards/reward_market/mean": 0.375,
208
+ "rewards/reward_market/std": 0.20160646736621857,
209
+ "rewards/reward_warehouse/mean": 0.32500001788139343,
210
+ "rewards/reward_warehouse/std": 0.18837162852287292,
211
+ "rewards/reward_showroom/mean": 0.09159374982118607,
212
+ "rewards/reward_showroom/std": 0.0639258474111557,
213
+ "reward": 0.7915937900543213,
214
+ "reward_std": 0.12960509955883026,
215
+ "frac_reward_zero_std": 0.0,
216
+ "sampling/sampling_logp_difference/mean": 0.008641102351248264,
217
+ "sampling/sampling_logp_difference/max": 0.8236088752746582,
218
+ "sampling/importance_sampling_ratio/min": 0.9935171008110046,
219
+ "sampling/importance_sampling_ratio/mean": 1.0399717092514038,
220
+ "sampling/importance_sampling_ratio/max": 2.278707504272461,
221
+ "entropy": 0.007084679029730978,
222
+ "clip_ratio/low_mean": 0.0,
223
+ "clip_ratio/low_min": 0.0,
224
+ "clip_ratio/high_mean": 0.0,
225
+ "clip_ratio/high_max": 0.0,
226
+ "clip_ratio/region_mean": 0.0,
227
+ "step_time": 18.056264080107212,
228
+ "epoch": 0.3333333333333333
229
+ },
230
+ {
231
+ "step": 7,
232
+ "loss": -0.0,
233
+ "grad_norm": 0.281630277633667,
234
+ "learning_rate": 3e-06,
235
+ "num_tokens": 193950.0,
236
+ "completions/mean_length": 3.0,
237
+ "completions/min_length": 3.0,
238
+ "completions/max_length": 3.0,
239
+ "completions/clipped_ratio": 0.0,
240
+ "completions/mean_terminated_length": 3.0,
241
+ "completions/min_terminated_length": 3.0,
242
+ "completions/max_terminated_length": 3.0,
243
+ "rewards/reward_total/mean": 0.7600874900817871,
244
+ "rewards/reward_total/std": 0.13816162943840027,
245
+ "rewards/reward_market/mean": 0.32500001788139343,
246
+ "rewards/reward_market/std": 0.18837162852287292,
247
+ "rewards/reward_warehouse/mean": 0.32500001788139343,
248
+ "rewards/reward_warehouse/std": 0.18837162852287292,
249
+ "rewards/reward_showroom/mean": 0.11008749902248383,
250
+ "rewards/reward_showroom/std": 0.07028691470623016,
251
+ "reward": 0.7600874900817871,
252
+ "reward_std": 0.13816164433956146,
253
+ "frac_reward_zero_std": 0.0,
254
+ "sampling/sampling_logp_difference/mean": 3.554227441782132e-05,
255
+ "sampling/sampling_logp_difference/max": 0.0027569520752876997,
256
+ "sampling/importance_sampling_ratio/min": 0.997247576713562,
257
+ "sampling/importance_sampling_ratio/mean": 0.9999052286148071,
258
+ "sampling/importance_sampling_ratio/max": 1.0000724792480469,
259
+ "entropy": 0.0005298306713825696,
260
+ "clip_ratio/low_mean": 0.0,
261
+ "clip_ratio/low_min": 0.0,
262
+ "clip_ratio/high_mean": 0.0,
263
+ "clip_ratio/high_max": 0.0,
264
+ "clip_ratio/region_mean": 0.0,
265
+ "step_time": 17.43799263238907,
266
+ "epoch": 0.3888888888888889
267
+ },
268
+ {
269
+ "step": 8,
270
+ "loss": 0.0,
271
+ "grad_norm": 0.00018881642608903348,
272
+ "learning_rate": 3.5e-06,
273
+ "num_tokens": 221677.0,
274
+ "completions/mean_length": 3.0,
275
+ "completions/min_length": 3.0,
276
+ "completions/max_length": 3.0,
277
+ "completions/clipped_ratio": 0.0,
278
+ "completions/mean_terminated_length": 3.0,
279
+ "completions/min_terminated_length": 3.0,
280
+ "completions/max_terminated_length": 3.0,
281
+ "rewards/reward_total/mean": 0.7622687816619873,
282
+ "rewards/reward_total/std": 0.1372590959072113,
283
+ "rewards/reward_market/mean": 0.4000000059604645,
284
+ "rewards/reward_market/std": 0.20320022106170654,
285
+ "rewards/reward_warehouse/mean": 0.25,
286
+ "rewards/reward_warehouse/std": 0.13440430164337158,
287
+ "rewards/reward_showroom/mean": 0.11226874589920044,
288
+ "rewards/reward_showroom/std": 0.07360353320837021,
289
+ "reward": 0.7622687816619873,
290
+ "reward_std": 0.1372590959072113,
291
+ "frac_reward_zero_std": 0.0,
292
+ "sampling/sampling_logp_difference/mean": 2.545601773817907e-07,
293
+ "sampling/sampling_logp_difference/max": 1.5496943888138048e-06,
294
+ "sampling/importance_sampling_ratio/min": 0.9999986886978149,
295
+ "sampling/importance_sampling_ratio/mean": 1.000000238418579,
296
+ "sampling/importance_sampling_ratio/max": 1.0000016689300537,
297
+ "entropy": 4.1391018498870835e-05,
298
+ "clip_ratio/low_mean": 0.0,
299
+ "clip_ratio/low_min": 0.0,
300
+ "clip_ratio/high_mean": 0.0,
301
+ "clip_ratio/high_max": 0.0,
302
+ "clip_ratio/region_mean": 0.0,
303
+ "step_time": 17.406394600868225,
304
+ "epoch": 0.4444444444444444
305
+ },
306
+ {
307
+ "step": 9,
308
+ "loss": 0.0,
309
+ "grad_norm": 0.00015632262511644512,
310
+ "learning_rate": 4.000000000000001e-06,
311
+ "num_tokens": 249549.0,
312
+ "completions/mean_length": 3.0,
313
+ "completions/min_length": 3.0,
314
+ "completions/max_length": 3.0,
315
+ "completions/clipped_ratio": 0.0,
316
+ "completions/mean_terminated_length": 3.0,
317
+ "completions/min_terminated_length": 3.0,
318
+ "completions/max_terminated_length": 3.0,
319
+ "rewards/reward_total/mean": 0.775946855545044,
320
+ "rewards/reward_total/std": 0.137176051735878,
321
+ "rewards/reward_market/mean": 0.375,
322
+ "rewards/reward_market/std": 0.20160646736621857,
323
+ "rewards/reward_warehouse/mean": 0.30000001192092896,
324
+ "rewards/reward_warehouse/std": 0.17597654461860657,
325
+ "rewards/reward_showroom/mean": 0.10094687342643738,
326
+ "rewards/reward_showroom/std": 0.058497413992881775,
327
+ "reward": 0.775946855545044,
328
+ "reward_std": 0.1371760368347168,
329
+ "frac_reward_zero_std": 0.0,
330
+ "sampling/sampling_logp_difference/mean": 2.918131087881193e-07,
331
+ "sampling/sampling_logp_difference/max": 3.099441755693988e-06,
332
+ "sampling/importance_sampling_ratio/min": 0.9999978542327881,
333
+ "sampling/importance_sampling_ratio/mean": 1.0,
334
+ "sampling/importance_sampling_ratio/max": 1.000001311302185,
335
+ "entropy": 4.173239403826301e-05,
336
+ "clip_ratio/low_mean": 0.0,
337
+ "clip_ratio/low_min": 0.0,
338
+ "clip_ratio/high_mean": 0.0,
339
+ "clip_ratio/high_max": 0.0,
340
+ "clip_ratio/region_mean": 0.0,
341
+ "step_time": 18.514019537717104,
342
+ "epoch": 0.5
343
+ },
344
+ {
345
+ "step": 10,
346
+ "loss": 0.0,
347
+ "grad_norm": 9.833038348006085e-05,
348
+ "learning_rate": 4.5e-06,
349
+ "num_tokens": 277377.0,
350
+ "completions/mean_length": 3.0,
351
+ "completions/min_length": 3.0,
352
+ "completions/max_length": 3.0,
353
+ "completions/clipped_ratio": 0.0,
354
+ "completions/mean_terminated_length": 3.0,
355
+ "completions/min_terminated_length": 3.0,
356
+ "completions/max_terminated_length": 3.0,
357
+ "rewards/reward_total/mean": 0.7337374687194824,
358
+ "rewards/reward_total/std": 0.15055809915065765,
359
+ "rewards/reward_market/mean": 0.2750000059604645,
360
+ "rewards/reward_market/std": 0.1586231142282486,
361
+ "rewards/reward_warehouse/mean": 0.3500000238418579,
362
+ "rewards/reward_warehouse/std": 0.1967477649450302,
363
+ "rewards/reward_showroom/mean": 0.10873749852180481,
364
+ "rewards/reward_showroom/std": 0.06778524816036224,
365
+ "reward": 0.7337374687194824,
366
+ "reward_std": 0.15055811405181885,
367
+ "frac_reward_zero_std": 0.0,
368
+ "sampling/sampling_logp_difference/mean": 2.719489771152439e-07,
369
+ "sampling/sampling_logp_difference/max": 1.9073031580774114e-06,
370
+ "sampling/importance_sampling_ratio/min": 0.9999985694885254,
371
+ "sampling/importance_sampling_ratio/mean": 0.9999998807907104,
372
+ "sampling/importance_sampling_ratio/max": 1.0000019073486328,
373
+ "entropy": 4.224909940830912e-05,
374
+ "clip_ratio/low_mean": 0.0,
375
+ "clip_ratio/low_min": 0.0,
376
+ "clip_ratio/high_mean": 0.0,
377
+ "clip_ratio/high_max": 0.0,
378
+ "clip_ratio/region_mean": 0.0,
379
+ "step_time": 18.26371632888913,
380
+ "epoch": 0.5555555555555556
381
+ },
382
+ {
383
+ "step": 11,
384
+ "loss": -0.0,
385
+ "grad_norm": 0.00015879125567153096,
386
+ "learning_rate": 5e-06,
387
+ "num_tokens": 305332.0,
388
+ "completions/mean_length": 3.0,
389
+ "completions/min_length": 3.0,
390
+ "completions/max_length": 3.0,
391
+ "completions/clipped_ratio": 0.0,
392
+ "completions/mean_terminated_length": 3.0,
393
+ "completions/min_terminated_length": 3.0,
394
+ "completions/max_terminated_length": 3.0,
395
+ "rewards/reward_total/mean": 0.7567968368530273,
396
+ "rewards/reward_total/std": 0.13728150725364685,
397
+ "rewards/reward_market/mean": 0.375,
398
+ "rewards/reward_market/std": 0.20160646736621857,
399
+ "rewards/reward_warehouse/mean": 0.2750000059604645,
400
+ "rewards/reward_warehouse/std": 0.1586231142282486,
401
+ "rewards/reward_showroom/mean": 0.10679687559604645,
402
+ "rewards/reward_showroom/std": 0.07404065877199173,
403
+ "reward": 0.7567968368530273,
404
+ "reward_std": 0.13728150725364685,
405
+ "frac_reward_zero_std": 0.0,
406
+ "sampling/sampling_logp_difference/mean": 3.067227396513772e-07,
407
+ "sampling/sampling_logp_difference/max": 2.6226434783893637e-06,
408
+ "sampling/importance_sampling_ratio/min": 0.9999973773956299,
409
+ "sampling/importance_sampling_ratio/mean": 0.9999994039535522,
410
+ "sampling/importance_sampling_ratio/max": 1.0000019073486328,
411
+ "entropy": 4.7876037001515215e-05,
412
+ "clip_ratio/low_mean": 0.0,
413
+ "clip_ratio/low_min": 0.0,
414
+ "clip_ratio/high_mean": 0.0,
415
+ "clip_ratio/high_max": 0.0,
416
+ "clip_ratio/region_mean": 0.0,
417
+ "step_time": 19.4472255371511,
418
+ "epoch": 0.6111111111111112
419
+ },
420
+ {
421
+ "step": 12,
422
+ "loss": -0.0,
423
+ "grad_norm": 0.00039661259506829083,
424
+ "learning_rate": 4.3750000000000005e-06,
425
+ "num_tokens": 333165.0,
426
+ "completions/mean_length": 3.0,
427
+ "completions/min_length": 3.0,
428
+ "completions/max_length": 3.0,
429
+ "completions/clipped_ratio": 0.0,
430
+ "completions/mean_terminated_length": 3.0,
431
+ "completions/min_terminated_length": 3.0,
432
+ "completions/max_terminated_length": 3.0,
433
+ "rewards/reward_total/mean": 0.7081500291824341,
434
+ "rewards/reward_total/std": 0.1421954482793808,
435
+ "rewards/reward_market/mean": 0.30000001192092896,
436
+ "rewards/reward_market/std": 0.17597654461860657,
437
+ "rewards/reward_warehouse/mean": 0.2750000059604645,
438
+ "rewards/reward_warehouse/std": 0.1586231142282486,
439
+ "rewards/reward_showroom/mean": 0.13315001130104065,
440
+ "rewards/reward_showroom/std": 0.07370516657829285,
441
+ "reward": 0.7081500291824341,
442
+ "reward_std": 0.142195463180542,
443
+ "frac_reward_zero_std": 0.0,
444
+ "sampling/sampling_logp_difference/mean": 4.768499479723687e-07,
445
+ "sampling/sampling_logp_difference/max": 5.602954843197949e-06,
446
+ "sampling/importance_sampling_ratio/min": 0.9999943971633911,
447
+ "sampling/importance_sampling_ratio/mean": 0.9999988675117493,
448
+ "sampling/importance_sampling_ratio/max": 1.0000016689300537,
449
+ "entropy": 5.9777358274004655e-05,
450
+ "clip_ratio/low_mean": 0.0,
451
+ "clip_ratio/low_min": 0.0,
452
+ "clip_ratio/high_mean": 0.0,
453
+ "clip_ratio/high_max": 0.0,
454
+ "clip_ratio/region_mean": 0.0,
455
+ "step_time": 18.45015063509345,
456
+ "epoch": 0.6666666666666666
457
+ },
458
+ {
459
+ "step": 13,
460
+ "loss": 0.0,
461
+ "grad_norm": 0.0005282312049530447,
462
+ "learning_rate": 3.7500000000000005e-06,
463
+ "num_tokens": 361006.0,
464
+ "completions/mean_length": 3.0,
465
+ "completions/min_length": 3.0,
466
+ "completions/max_length": 3.0,
467
+ "completions/clipped_ratio": 0.0,
468
+ "completions/mean_terminated_length": 3.0,
469
+ "completions/min_terminated_length": 3.0,
470
+ "completions/max_terminated_length": 3.0,
471
+ "rewards/reward_total/mean": 0.7762781381607056,
472
+ "rewards/reward_total/std": 0.13823458552360535,
473
+ "rewards/reward_market/mean": 0.32500001788139343,
474
+ "rewards/reward_market/std": 0.18837162852287292,
475
+ "rewards/reward_warehouse/mean": 0.3500000238418579,
476
+ "rewards/reward_warehouse/std": 0.1967477649450302,
477
+ "rewards/reward_showroom/mean": 0.10127812623977661,
478
+ "rewards/reward_showroom/std": 0.06158862262964249,
479
+ "reward": 0.7762781381607056,
480
+ "reward_std": 0.13823460042476654,
481
+ "frac_reward_zero_std": 0.0,
482
+ "sampling/sampling_logp_difference/mean": 6.830008487668238e-07,
483
+ "sampling/sampling_logp_difference/max": 5.60290391149465e-06,
484
+ "sampling/importance_sampling_ratio/min": 0.9999943971633911,
485
+ "sampling/importance_sampling_ratio/mean": 0.9999983310699463,
486
+ "sampling/importance_sampling_ratio/max": 1.0000022649765015,
487
+ "entropy": 7.182803437899565e-05,
488
+ "clip_ratio/low_mean": 0.0,
489
+ "clip_ratio/low_min": 0.0,
490
+ "clip_ratio/high_mean": 0.0,
491
+ "clip_ratio/high_max": 0.0,
492
+ "clip_ratio/region_mean": 0.0,
493
+ "step_time": 17.955969959497452,
494
+ "epoch": 0.7222222222222222
495
+ },
496
+ {
497
+ "step": 14,
498
+ "loss": -0.0,
499
+ "grad_norm": 0.000535853614564985,
500
+ "learning_rate": 3.125e-06,
501
+ "num_tokens": 388862.0,
502
+ "completions/mean_length": 3.0,
503
+ "completions/min_length": 3.0,
504
+ "completions/max_length": 3.0,
505
+ "completions/clipped_ratio": 0.0,
506
+ "completions/mean_terminated_length": 3.0,
507
+ "completions/min_terminated_length": 3.0,
508
+ "completions/max_terminated_length": 3.0,
509
+ "rewards/reward_total/mean": 0.7628874778747559,
510
+ "rewards/reward_total/std": 0.1295449286699295,
511
+ "rewards/reward_market/mean": 0.30000001192092896,
512
+ "rewards/reward_market/std": 0.17597654461860657,
513
+ "rewards/reward_warehouse/mean": 0.3499999940395355,
514
+ "rewards/reward_warehouse/std": 0.19674775004386902,
515
+ "rewards/reward_showroom/mean": 0.11288750171661377,
516
+ "rewards/reward_showroom/std": 0.073255755007267,
517
+ "reward": 0.7628874778747559,
518
+ "reward_std": 0.1295449435710907,
519
+ "frac_reward_zero_std": 0.0,
520
+ "sampling/sampling_logp_difference/mean": 1.1113726259281975e-06,
521
+ "sampling/sampling_logp_difference/max": 2.0979605324100703e-05,
522
+ "sampling/importance_sampling_ratio/min": 0.9999922513961792,
523
+ "sampling/importance_sampling_ratio/mean": 1.0000004768371582,
524
+ "sampling/importance_sampling_ratio/max": 1.0000211000442505,
525
+ "entropy": 9.972968496185786e-05,
526
+ "clip_ratio/low_mean": 0.0,
527
+ "clip_ratio/low_min": 0.0,
528
+ "clip_ratio/high_mean": 0.0,
529
+ "clip_ratio/high_max": 0.0,
530
+ "clip_ratio/region_mean": 0.0,
531
+ "step_time": 18.99697282537818,
532
+ "epoch": 0.7777777777777778
533
+ },
534
+ {
535
+ "step": 15,
536
+ "loss": 0.0,
537
+ "grad_norm": 0.005312301218509674,
538
+ "learning_rate": 2.5e-06,
539
+ "num_tokens": 416636.0,
540
+ "completions/mean_length": 3.0,
541
+ "completions/min_length": 3.0,
542
+ "completions/max_length": 3.0,
543
+ "completions/clipped_ratio": 0.0,
544
+ "completions/mean_terminated_length": 3.0,
545
+ "completions/min_terminated_length": 3.0,
546
+ "completions/max_terminated_length": 3.0,
547
+ "rewards/reward_total/mean": 0.7899656295776367,
548
+ "rewards/reward_total/std": 0.1296130269765854,
549
+ "rewards/reward_market/mean": 0.3499999940395355,
550
+ "rewards/reward_market/std": 0.19674775004386902,
551
+ "rewards/reward_warehouse/mean": 0.3500000238418579,
552
+ "rewards/reward_warehouse/std": 0.1967477649450302,
553
+ "rewards/reward_showroom/mean": 0.08996562659740448,
554
+ "rewards/reward_showroom/std": 0.05463240668177605,
555
+ "reward": 0.7899656295776367,
556
+ "reward_std": 0.12961304187774658,
557
+ "frac_reward_zero_std": 0.0,
558
+ "sampling/sampling_logp_difference/mean": 4.004845322924666e-06,
559
+ "sampling/sampling_logp_difference/max": 4.589592936099507e-05,
560
+ "sampling/importance_sampling_ratio/min": 0.9999337792396545,
561
+ "sampling/importance_sampling_ratio/mean": 0.9999885559082031,
562
+ "sampling/importance_sampling_ratio/max": 1.0000029802322388,
563
+ "entropy": 0.0001807671503684105,
564
+ "clip_ratio/low_mean": 0.0,
565
+ "clip_ratio/low_min": 0.0,
566
+ "clip_ratio/high_mean": 0.0,
567
+ "clip_ratio/high_max": 0.0,
568
+ "clip_ratio/region_mean": 0.0,
569
+ "step_time": 19.184648096561432,
570
+ "epoch": 0.8333333333333334
571
+ },
572
+ {
573
+ "step": 16,
574
+ "loss": -0.0,
575
+ "grad_norm": 0.005995223298668861,
576
+ "learning_rate": 1.8750000000000003e-06,
577
+ "num_tokens": 444544.0,
578
+ "completions/mean_length": 3.0,
579
+ "completions/min_length": 3.0,
580
+ "completions/max_length": 3.0,
581
+ "completions/clipped_ratio": 0.0,
582
+ "completions/mean_terminated_length": 3.0,
583
+ "completions/min_terminated_length": 3.0,
584
+ "completions/max_terminated_length": 3.0,
585
+ "rewards/reward_total/mean": 0.789996862411499,
586
+ "rewards/reward_total/std": 0.12048782408237457,
587
+ "rewards/reward_market/mean": 0.375,
588
+ "rewards/reward_market/std": 0.20160646736621857,
589
+ "rewards/reward_warehouse/mean": 0.32039374113082886,
590
+ "rewards/reward_warehouse/std": 0.18316024541854858,
591
+ "rewards/reward_showroom/mean": 0.09460312128067017,
592
+ "rewards/reward_showroom/std": 0.062435995787382126,
593
+ "reward": 0.789996862411499,
594
+ "reward_std": 0.12048781663179398,
595
+ "frac_reward_zero_std": 0.0,
596
+ "sampling/sampling_logp_difference/mean": 4.866625204158481e-06,
597
+ "sampling/sampling_logp_difference/max": 2.9087463190080598e-05,
598
+ "sampling/importance_sampling_ratio/min": 0.999959409236908,
599
+ "sampling/importance_sampling_ratio/mean": 0.9999855160713196,
600
+ "sampling/importance_sampling_ratio/max": 1.0000004768371582,
601
+ "entropy": 0.00019320984370096994,
602
+ "clip_ratio/low_mean": 0.0,
603
+ "clip_ratio/low_min": 0.0,
604
+ "clip_ratio/high_mean": 0.0,
605
+ "clip_ratio/high_max": 0.0,
606
+ "clip_ratio/region_mean": 0.0,
607
+ "step_time": 19.476148523390293,
608
+ "epoch": 0.8888888888888888
609
+ },
610
+ {
611
+ "step": 17,
612
+ "loss": 0.0,
613
+ "grad_norm": 0.011378168128430843,
614
+ "learning_rate": 1.25e-06,
615
+ "num_tokens": 472481.0,
616
+ "completions/mean_length": 3.0,
617
+ "completions/min_length": 3.0,
618
+ "completions/max_length": 3.0,
619
+ "completions/clipped_ratio": 0.0,
620
+ "completions/mean_terminated_length": 3.0,
621
+ "completions/min_terminated_length": 3.0,
622
+ "completions/max_terminated_length": 3.0,
623
+ "rewards/reward_total/mean": 0.8168656826019287,
624
+ "rewards/reward_total/std": 0.09262391924858093,
625
+ "rewards/reward_market/mean": 0.375,
626
+ "rewards/reward_market/std": 0.20160646736621857,
627
+ "rewards/reward_warehouse/mean": 0.3498094081878662,
628
+ "rewards/reward_warehouse/std": 0.19690066576004028,
629
+ "rewards/reward_showroom/mean": 0.09205625206232071,
630
+ "rewards/reward_showroom/std": 0.07046373933553696,
631
+ "reward": 0.8168656826019287,
632
+ "reward_std": 0.09262390434741974,
633
+ "frac_reward_zero_std": 0.0,
634
+ "sampling/sampling_logp_difference/mean": 5.950785180175444e-06,
635
+ "sampling/sampling_logp_difference/max": 4.7328881919384e-05,
636
+ "sampling/importance_sampling_ratio/min": 0.9999284744262695,
637
+ "sampling/importance_sampling_ratio/mean": 0.9999822974205017,
638
+ "sampling/importance_sampling_ratio/max": 1.000001072883606,
639
+ "entropy": 0.00021975090658088448,
640
+ "clip_ratio/low_mean": 0.0,
641
+ "clip_ratio/low_min": 0.0,
642
+ "clip_ratio/high_mean": 0.0,
643
+ "clip_ratio/high_max": 0.0,
644
+ "clip_ratio/region_mean": 0.0,
645
+ "step_time": 19.344543006271124,
646
+ "epoch": 0.9444444444444444
647
+ },
648
+ {
649
+ "step": 18,
650
+ "loss": 0.0,
651
+ "grad_norm": 0.018683306872844696,
652
+ "learning_rate": 6.25e-07,
653
+ "num_tokens": 500266.0,
654
+ "completions/mean_length": 3.0,
655
+ "completions/min_length": 3.0,
656
+ "completions/max_length": 3.0,
657
+ "completions/clipped_ratio": 0.0,
658
+ "completions/mean_terminated_length": 3.0,
659
+ "completions/min_terminated_length": 3.0,
660
+ "completions/max_terminated_length": 3.0,
661
+ "rewards/reward_total/mean": 0.7750625014305115,
662
+ "rewards/reward_total/std": 0.14084643125534058,
663
+ "rewards/reward_market/mean": 0.3500000238418579,
664
+ "rewards/reward_market/std": 0.1967477649450302,
665
+ "rewards/reward_warehouse/mean": 0.32499998807907104,
666
+ "rewards/reward_warehouse/std": 0.18837164342403412,
667
+ "rewards/reward_showroom/mean": 0.10006250441074371,
668
+ "rewards/reward_showroom/std": 0.05640558898448944,
669
+ "reward": 0.7750625014305115,
670
+ "reward_std": 0.14084644615650177,
671
+ "frac_reward_zero_std": 0.0625,
672
+ "sampling/sampling_logp_difference/mean": 1.1303089195280336e-05,
673
+ "sampling/sampling_logp_difference/max": 0.00027322862297296524,
674
+ "sampling/importance_sampling_ratio/min": 0.999584436416626,
675
+ "sampling/importance_sampling_ratio/mean": 0.9999662637710571,
676
+ "sampling/importance_sampling_ratio/max": 0.9999997615814209,
677
+ "entropy": 0.000320568448614722,
678
+ "clip_ratio/low_mean": 0.0,
679
+ "clip_ratio/low_min": 0.0,
680
+ "clip_ratio/high_mean": 0.0,
681
+ "clip_ratio/high_max": 0.0,
682
+ "clip_ratio/region_mean": 0.0,
683
+ "step_time": 17.66909484937787,
684
+ "epoch": 1.0
685
+ },
686
+ {
687
+ "step": 18,
688
+ "train_runtime": 406.352,
689
+ "train_samples_per_second": 0.738,
690
+ "train_steps_per_second": 0.044,
691
+ "total_flos": 0.0,
692
+ "train_loss": 0.016376476217475202,
693
+ "epoch": 1.0
694
+ }
695
+ ]
training/training_artifacts_v1/reward_curve.png ADDED
training/training_artifacts_v1/reward_total_curve.png ADDED
training/training_artifacts_v1/training_summary.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "loss": {
3
+ "final": 0.0,
4
+ "max": 0.1153,
5
+ "min": -0.009,
6
+ "mean": 0.01637777777777778,
7
+ "n": 18
8
+ },
9
+ "reward_total": {
10
+ "final": 0.7750625014305115,
11
+ "max": 0.8168656826019287,
12
+ "min": 0.7081500291824341,
13
+ "mean": 0.7689822945329878,
14
+ "n": 18
15
+ },
16
+ "reward_market": {
17
+ "final": 0.0,
18
+ "max": 0.0,
19
+ "min": 0.0,
20
+ "mean": 0.0,
21
+ "n": 0
22
+ },
23
+ "reward_warehouse": {
24
+ "final": 0.0,
25
+ "max": 0.0,
26
+ "min": 0.0,
27
+ "mean": 0.0,
28
+ "n": 0
29
+ },
30
+ "reward_showroom": {
31
+ "final": 0.0,
32
+ "max": 0.0,
33
+ "min": 0.0,
34
+ "mean": 0.0,
35
+ "n": 0
36
+ },
37
+ "n_log_rows": 19,
38
+ "output_dir": "/workspace/shopmanager-grpo-qwen3",
39
+ "run_config": {
40
+ "model": "Qwen/Qwen3-1.7B",
41
+ "env_url": "https://hard007ik-shopmanagereng.hf.space",
42
+ "dataset_size": 300,
43
+ "num_generations": 2,
44
+ "per_device_batch": 1,
45
+ "grad_accum": 32,
46
+ "max_completion_length": 64,
47
+ "max_turns": 15,
48
+ "lr": 5e-06,
49
+ "warmup_steps": 10,
50
+ "max_steps": -1,
51
+ "epochs": 1,
52
+ "vllm_gpu_mem": 0.3,
53
+ "reward_weights": [
54
+ 1.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0
58
+ ],
59
+ "precision": {
60
+ "bf16": true
61
+ }
62
+ }
63
+ }
ui.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Streamlit UI for ShopManagerEng — Interactive Jewelry Shop Demo.
3
+
4
+ An AI heuristic agent automatically plays through each episode.
5
+ Users press "New Episode" and watch the agent navigate all 3 phases.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import random
11
+ import time
12
+ from pathlib import Path
13
+
14
+ import streamlit as st
15
+
16
+ # ── Ensure imports resolve ──────────────────────────────────────────────────
17
+ ROOT = Path(__file__).resolve().parent
18
+ if str(ROOT) not in sys.path:
19
+ sys.path.insert(0, str(ROOT))
20
+
21
+ os.environ.setdefault("SHOPMANAGER_MARKET_MODE", "synthetic")
22
+
23
+ from server.ShopManagerEng_environment import JewelryShopEnvironment
24
+ from models import JewelryAction, PRODUCT_CATALOG
25
+
26
+
27
+ # ── Page config ─────────────────────────────────────────────────────────────
28
+ st.set_page_config(
29
+ page_title="ShopManagerEng — Jewelry Shop RL",
30
+ page_icon="💎",
31
+ layout="wide",
32
+ initial_sidebar_state="expanded",
33
+ )
34
+
35
+ # ── CSS: clean light-theme styling ──────────────────────────────────────────
36
+ st.markdown("""
37
+ <style>
38
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap');
39
+ html, body, [class*="st-"] { font-family: 'Inter', sans-serif; }
40
+
41
+ .hero-title {
42
+ font-size: 36px; font-weight: 800;
43
+ background: linear-gradient(135deg, #7c3aed, #3b82f6, #10b981);
44
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
45
+ margin-bottom: 2px;
46
+ }
47
+ .hero-sub { font-size: 15px; color: #64748b; margin-bottom: 20px; }
48
+
49
+ .metric-card {
50
+ background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px;
51
+ padding: 14px 18px; text-align: center; min-height: 90px;
52
+ }
53
+ .metric-card .label { font-size: 12px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
54
+ .metric-card .value { font-size: 24px; font-weight: 800; color: #1e293b; margin-top: 4px; }
55
+
56
+ .phase-box {
57
+ background: #f1f5f9; border: 1px solid #cbd5e1; border-radius: 12px;
58
+ padding: 18px; margin-bottom: 12px;
59
+ }
60
+ .phase-box.active { border: 2px solid #7c3aed; background: #f5f3ff; }
61
+ .phase-box h4 { margin: 0 0 6px; color: #1e293b; }
62
+ .phase-box p { margin: 0; color: #475569; font-size: 14px; }
63
+
64
+ .env-msg {
65
+ background: #eff6ff; border-left: 4px solid #3b82f6;
66
+ border-radius: 0 10px 10px 0; padding: 12px 16px;
67
+ margin: 10px 0; font-size: 13px; color: #1e40af; line-height: 1.5;
68
+ word-wrap: break-word;
69
+ }
70
+
71
+ .step-row {
72
+ padding: 8px 12px; margin: 4px 0; border-radius: 8px;
73
+ font-size: 13px; color: #334155;
74
+ }
75
+ .step-row.market { background: #fef3c7; border-left: 3px solid #f59e0b; }
76
+ .step-row.warehouse { background: #dbeafe; border-left: 3px solid #3b82f6; }
77
+ .step-row.showroom { background: #d1fae5; border-left: 3px solid #10b981; }
78
+
79
+ .reward-big {
80
+ text-align: center; padding: 24px;
81
+ background: linear-gradient(135deg, #f5f3ff, #eff6ff);
82
+ border: 2px solid #7c3aed; border-radius: 16px;
83
+ }
84
+ .reward-big .score { font-size: 52px; font-weight: 800; color: #7c3aed; }
85
+ .reward-big .label { font-size: 14px; color: #64748b; }
86
+
87
+ .catalog-card {
88
+ background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px;
89
+ padding: 16px; text-align: center;
90
+ }
91
+ .catalog-card h4 { margin: 0 0 8px; color: #1e293b; }
92
+ .catalog-card p { margin: 2px 0; color: #475569; font-size: 13px; }
93
+
94
+ .demand-bar {
95
+ background: #e2e8f0; border-radius: 6px; height: 10px; overflow: hidden; margin-top: 4px;
96
+ }
97
+ .demand-fill { height: 100%; border-radius: 6px; }
98
+ </style>
99
+ """, unsafe_allow_html=True)
100
+
101
+
102
+ # ── Heuristic Agent ─────────────────────────────────────────────────────────
103
+ def heuristic_action(obs):
104
+ """Simple rule-based agent that plays through all 3 phases."""
105
+ if obs.phase == "market":
106
+ price = float(obs.gold_price or 300)
107
+ # Buy if we have enough cash for at least 1 oz
108
+ if obs.cash >= price + 10:
109
+ return JewelryAction(
110
+ market_action="buy",
111
+ gold_qty=1.0,
112
+ target_price_usd=obs.gold_price,
113
+ )
114
+ return JewelryAction(market_action="wait")
115
+
116
+ if obs.phase == "warehouse":
117
+ # Pick the highest-demand product we can afford
118
+ demand = obs.demand or {"ring": 0.5, "necklace": 0.3, "bracelet": 0.2}
119
+ for name in sorted(demand, key=lambda k: demand.get(k, 0), reverse=True):
120
+ spec = PRODUCT_CATALOG[name]
121
+ if obs.gold_oz + 1e-8 >= spec["gold_oz"] and obs.cash >= spec["labor"]:
122
+ return JewelryAction(product_choice=name)
123
+ return JewelryAction(product_choice="ring")
124
+
125
+ if obs.phase == "showroom":
126
+ # Accept if margin > 15% or after round 3
127
+ if (
128
+ obs.current_offer
129
+ and obs.cost_basis > 0
130
+ and float(obs.current_offer) / float(obs.cost_basis) >= 1.15
131
+ ) or (obs.negotiation_round and int(obs.negotiation_round) >= 3):
132
+ return JewelryAction(message="I accept")
133
+ offer = float(obs.current_offer or 0)
134
+ if offer:
135
+ return JewelryAction(message=f"How about ${offer * 1.08:.2f}?")
136
+ return JewelryAction(message="I need a better offer")
137
+
138
+ return JewelryAction()
139
+
140
+
141
+ # ── Session state ───────────────────────────────────────────────────────────
142
+ if "episode_steps" not in st.session_state:
143
+ st.session_state.episode_steps = None
144
+ st.session_state.final_reward = None
145
+ st.session_state.episode_count = 0
146
+
147
+
148
+ def run_episode(task_id):
149
+ """Run a full episode with the heuristic agent and return step logs."""
150
+ env = JewelryShopEnvironment()
151
+ seed = random.randint(0, 99999)
152
+ obs = env.reset(seed=seed, market_mode="synthetic", task_id=task_id)
153
+
154
+ steps = [{
155
+ "step": 0, "phase": obs.phase, "action": "reset",
156
+ "msg": obs.message, "reward": 0.0,
157
+ "cash": obs.cash, "gold_oz": obs.gold_oz,
158
+ "gold_price": obs.gold_price,
159
+ "cumulative": float(obs.cumulative_reward),
160
+ }]
161
+
162
+ for i in range(1, 20):
163
+ if obs.done:
164
+ break
165
+ action = heuristic_action(obs)
166
+
167
+ # Describe the action in human terms
168
+ if obs.phase == "market":
169
+ act_str = f"BUY {action.gold_qty} oz" if action.market_action == "buy" else "WAIT"
170
+ elif obs.phase == "warehouse":
171
+ act_str = f"CRAFT {action.product_choice}"
172
+ else:
173
+ act_str = action.message or "..."
174
+
175
+ obs = env.step(action)
176
+ steps.append({
177
+ "step": i, "phase": obs.phase, "action": act_str,
178
+ "msg": obs.message, "reward": float(obs.reward),
179
+ "cash": obs.cash, "gold_oz": obs.gold_oz,
180
+ "gold_price": getattr(obs, "gold_price", 0),
181
+ "product": getattr(obs, "product_for_sale", None),
182
+ "offer": float(obs.current_offer) if obs.current_offer else None,
183
+ "cost_basis": float(obs.cost_basis) if obs.cost_basis else None,
184
+ "cumulative": float(obs.cumulative_reward),
185
+ })
186
+
187
+ return steps, float(obs.cumulative_reward)
188
+
189
+
190
+ # ── Sidebar ─────────────────────────────────────────────────────────────────
191
+ with st.sidebar:
192
+ st.markdown("### 💎 ShopManagerEng")
193
+ st.markdown("---")
194
+
195
+ task = st.selectbox(
196
+ "🎯 Task Profile",
197
+ ["profit_negotiator", "market_timing", "demand_crafter"],
198
+ index=0,
199
+ )
200
+ weights = {
201
+ "profit_negotiator": "Showroom 60% · Market 20% · Warehouse 20%",
202
+ "market_timing": "Market 60% · Warehouse 20% · Showroom 20%",
203
+ "demand_crafter": "Warehouse 60% · Market 20% · Showroom 20%",
204
+ }
205
+ st.caption(f"**Weights:** {weights[task]}")
206
+
207
+ st.markdown("---")
208
+ if st.button("🚀 New Episode", use_container_width=True, type="primary"):
209
+ steps, reward = run_episode(task)
210
+ st.session_state.episode_steps = steps
211
+ st.session_state.final_reward = reward
212
+ st.session_state.episode_count += 1
213
+ st.rerun()
214
+
215
+ st.markdown("---")
216
+ st.markdown("#### How It Works")
217
+ st.markdown("""
218
+ An AI **heuristic agent** automatically plays
219
+ through all 3 phases of the jewelry shop:
220
+
221
+ 1. 📈 **Market** — Buy gold at the right price
222
+ 2. 🏭 **Warehouse** — Craft the most demanded product
223
+ 3. 🤝 **Showroom** — Negotiate the best sale price
224
+
225
+ Press **🚀 New Episode** to watch the agent play!
226
+ """)
227
+
228
+ if st.session_state.final_reward is not None:
229
+ st.markdown("---")
230
+ reward = st.session_state.final_reward
231
+ color = "#10b981" if reward >= 0.6 else "#f59e0b" if reward >= 0.4 else "#ef4444"
232
+ st.markdown(f"**Final Score:** :{'green' if reward >= 0.6 else 'orange' if reward >= 0.4 else 'red'}[{reward:.4f}]")
233
+ st.metric("Episodes Played", st.session_state.episode_count)
234
+
235
+
236
+ # ── Main area ───────────────────────────────────────────────────────────────
237
+ st.markdown('<p class="hero-title">💎 Jewelry Shop Manager</p>', unsafe_allow_html=True)
238
+ st.markdown('<p class="hero-sub">An RL environment for training LLMs on multi-step business decisions</p>', unsafe_allow_html=True)
239
+
240
+
241
+ # ── No episode yet — show welcome ──────────────────────────────────────────
242
+ if st.session_state.episode_steps is None:
243
+ st.info("👋 **Welcome!** Press **🚀 New Episode** in the sidebar to watch the AI agent play through the jewelry shop simulation.")
244
+
245
+ st.markdown("### 📦 Product Catalog")
246
+ cols = st.columns(3)
247
+ items = [("💍 Ring", "ring"), ("📿 Necklace", "necklace"), ("⌚ Bracelet", "bracelet")]
248
+ for i, (icon_name, key) in enumerate(items):
249
+ spec = PRODUCT_CATALOG[key]
250
+ with cols[i]:
251
+ st.markdown(f"""
252
+ <div class="catalog-card">
253
+ <h4>{icon_name}</h4>
254
+ <p>🪙 Gold: {spec['gold_oz']} oz</p>
255
+ <p>🔧 Labor: ${spec['labor']:.0f}</p>
256
+ <p>📊 Base demand: {spec['base_demand']:.0%}</p>
257
+ </div>
258
+ """, unsafe_allow_html=True)
259
+
260
+ st.markdown("### 🧠 Three Business Phases")
261
+ c1, c2, c3 = st.columns(3)
262
+ with c1:
263
+ st.markdown("""
264
+ <div class="phase-box">
265
+ <h4>📈 Phase 1: Market</h4>
266
+ <p>Buy raw gold at the best price. Prices fluctuate — time your purchase wisely!</p>
267
+ </div>
268
+ """, unsafe_allow_html=True)
269
+ with c2:
270
+ st.markdown("""
271
+ <div class="phase-box">
272
+ <h4>🏭 Phase 2: Warehouse</h4>
273
+ <p>Craft a product (ring, necklace, bracelet) that matches market demand.</p>
274
+ </div>
275
+ """, unsafe_allow_html=True)
276
+ with c3:
277
+ st.markdown("""
278
+ <div class="phase-box">
279
+ <h4>🤝 Phase 3: Showroom</h4>
280
+ <p>Negotiate with a customer over 5 rounds to maximize your selling price.</p>
281
+ </div>
282
+ """, unsafe_allow_html=True)
283
+
284
+
285
+ # ── Episode results ─────────────────────────────────────────────────────────
286
+ else:
287
+ steps = st.session_state.episode_steps
288
+ reward = st.session_state.final_reward
289
+
290
+ # ── Score banner ────────────────────────────────────────────────────────
291
+ if reward >= 0.8:
292
+ grade, grade_emoji = "Excellent", "🏆"
293
+ elif reward >= 0.6:
294
+ grade, grade_emoji = "Good", "👍"
295
+ elif reward >= 0.4:
296
+ grade, grade_emoji = "Fair", "😐"
297
+ else:
298
+ grade, grade_emoji = "Poor", "😬"
299
+
300
+ st.markdown(f"""
301
+ <div class="reward-big">
302
+ <div class="label">{grade_emoji} Episode #{st.session_state.episode_count} — {grade}</div>
303
+ <div class="score">{reward:.4f}</div>
304
+ <div class="label">cumulative reward (out of 1.0)</div>
305
+ </div>
306
+ """, unsafe_allow_html=True)
307
+
308
+ st.markdown("")
309
+
310
+ # ── Summary metrics ─────────────────────────────────────────────────────
311
+ last = steps[-1]
312
+ m1, m2, m3, m4 = st.columns(4)
313
+ with m1:
314
+ st.markdown(f"""<div class="metric-card"><div class="label">Steps</div><div class="value">{len(steps)-1}</div></div>""", unsafe_allow_html=True)
315
+ with m2:
316
+ st.markdown(f"""<div class="metric-card"><div class="label">Final Cash</div><div class="value">${last['cash']:,.0f}</div></div>""", unsafe_allow_html=True)
317
+ with m3:
318
+ gold_price = steps[0].get("gold_price", 0)
319
+ st.markdown(f"""<div class="metric-card"><div class="label">Gold Price</div><div class="value">${gold_price:,.0f}/oz</div></div>""", unsafe_allow_html=True)
320
+ with m4:
321
+ product = None
322
+ for s in steps:
323
+ if s.get("product"):
324
+ product = s["product"]
325
+ st.markdown(f"""<div class="metric-card"><div class="label">Product</div><div class="value">{(product or 'N/A').title()}</div></div>""", unsafe_allow_html=True)
326
+
327
+ st.markdown("---")
328
+
329
+ # ── Step-by-step log ────────────────────────────────────────────────────
330
+ st.markdown("### 📋 Agent Decision Log")
331
+
332
+ for s in steps:
333
+ phase = s.get("phase", "market")
334
+ icon = {"market": "📈", "warehouse": "🏭", "showroom": "🤝"}.get(phase, "⬜")
335
+ step_num = s["step"]
336
+ action = s.get("action", "")
337
+ rw = s.get("reward", 0)
338
+ cum = s.get("cumulative", 0)
339
+
340
+ rw_badge = f" · reward: `{rw:.4f}`" if rw else ""
341
+ cum_str = f" · cumulative: `{cum:.4f}`" if cum else ""
342
+
343
+ # Use pure markdown — no raw HTML divs
344
+ st.markdown(f"**Step {step_num}** {icon} **{action}**{rw_badge}{cum_str}")
345
+
346
+ # Show environment message
347
+ if s.get("msg"):
348
+ st.caption(s["msg"])
349
+
350
+ st.divider()
351
+
352
+ # ── Reward progression chart ────────────────────────────────────────────
353
+ st.markdown("---")
354
+ st.markdown("### 📊 Cumulative Reward Over Steps")
355
+ chart_data = [s["cumulative"] for s in steps]
356
+ st.line_chart(chart_data, use_container_width=True, height=200)
357
+
358
+ st.info("Press **🚀 New Episode** in the sidebar to run another episode with a different seed!")
uv.lock CHANGED
The diff for this file is too large to render. See raw diff