| """ |
| Streamlit UI for ShopManagerEng β Interactive Jewelry Shop Demo. |
| |
| An AI heuristic agent automatically plays through each episode. |
| Users press "New Episode" and watch the agent navigate all 3 phases. |
| """ |
|
|
| import os |
| import sys |
| import random |
| import time |
| from pathlib import Path |
|
|
| import streamlit as st |
|
|
| |
| ROOT = Path(__file__).resolve().parent |
| if str(ROOT) not in sys.path: |
| sys.path.insert(0, str(ROOT)) |
|
|
| os.environ.setdefault("SHOPMANAGER_MARKET_MODE", "synthetic") |
|
|
| from server.ShopManagerEng_environment import JewelryShopEnvironment |
| from models import JewelryAction, PRODUCT_CATALOG |
|
|
|
|
| |
| st.set_page_config( |
| page_title="ShopManagerEng β Jewelry Shop RL", |
| page_icon="π", |
| layout="wide", |
| initial_sidebar_state="expanded", |
| ) |
|
|
| |
| st.markdown(""" |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap'); |
| html, body, [class*="st-"] { font-family: 'Inter', sans-serif; } |
| |
| .hero-title { |
| font-size: 36px; font-weight: 800; |
| background: linear-gradient(135deg, #7c3aed, #3b82f6, #10b981); |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; |
| margin-bottom: 2px; |
| } |
| .hero-sub { font-size: 15px; color: #64748b; margin-bottom: 20px; } |
| |
| .metric-card { |
| background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; |
| padding: 14px 18px; text-align: center; min-height: 90px; |
| } |
| .metric-card .label { font-size: 12px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } |
| .metric-card .value { font-size: 24px; font-weight: 800; color: #1e293b; margin-top: 4px; } |
| |
| .phase-box { |
| background: #f1f5f9; border: 1px solid #cbd5e1; border-radius: 12px; |
| padding: 18px; margin-bottom: 12px; |
| } |
| .phase-box.active { border: 2px solid #7c3aed; background: #f5f3ff; } |
| .phase-box h4 { margin: 0 0 6px; color: #1e293b; } |
| .phase-box p { margin: 0; color: #475569; font-size: 14px; } |
| |
| .env-msg { |
| background: #eff6ff; border-left: 4px solid #3b82f6; |
| border-radius: 0 10px 10px 0; padding: 12px 16px; |
| margin: 10px 0; font-size: 13px; color: #1e40af; line-height: 1.5; |
| word-wrap: break-word; |
| } |
| |
| .step-row { |
| padding: 8px 12px; margin: 4px 0; border-radius: 8px; |
| font-size: 13px; color: #334155; |
| } |
| .step-row.market { background: #fef3c7; border-left: 3px solid #f59e0b; } |
| .step-row.warehouse { background: #dbeafe; border-left: 3px solid #3b82f6; } |
| .step-row.showroom { background: #d1fae5; border-left: 3px solid #10b981; } |
| |
| .reward-big { |
| text-align: center; padding: 24px; |
| background: linear-gradient(135deg, #f5f3ff, #eff6ff); |
| border: 2px solid #7c3aed; border-radius: 16px; |
| } |
| .reward-big .score { font-size: 52px; font-weight: 800; color: #7c3aed; } |
| .reward-big .label { font-size: 14px; color: #64748b; } |
| |
| .catalog-card { |
| background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; |
| padding: 16px; text-align: center; |
| } |
| .catalog-card h4 { margin: 0 0 8px; color: #1e293b; } |
| .catalog-card p { margin: 2px 0; color: #475569; font-size: 13px; } |
| |
| .demand-bar { |
| background: #e2e8f0; border-radius: 6px; height: 10px; overflow: hidden; margin-top: 4px; |
| } |
| .demand-fill { height: 100%; border-radius: 6px; } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
|
|
| |
| def heuristic_action(obs): |
| """Simple rule-based agent that plays through all 3 phases.""" |
| if obs.phase == "market": |
| price = float(obs.gold_price or 300) |
| |
| if obs.cash >= price + 10: |
| return JewelryAction( |
| market_action="buy", |
| gold_qty=1.0, |
| target_price_usd=obs.gold_price, |
| ) |
| return JewelryAction(market_action="wait") |
|
|
| if obs.phase == "warehouse": |
| |
| demand = obs.demand or {"ring": 0.5, "necklace": 0.3, "bracelet": 0.2} |
| for name in sorted(demand, key=lambda k: demand.get(k, 0), reverse=True): |
| spec = PRODUCT_CATALOG[name] |
| if obs.gold_oz + 1e-8 >= spec["gold_oz"] and obs.cash >= spec["labor"]: |
| return JewelryAction(product_choice=name) |
| return JewelryAction(product_choice="ring") |
|
|
| if obs.phase == "showroom": |
| |
| if ( |
| obs.current_offer |
| and obs.cost_basis > 0 |
| and float(obs.current_offer) / float(obs.cost_basis) >= 1.15 |
| ) or (obs.negotiation_round and int(obs.negotiation_round) >= 3): |
| return JewelryAction(message="I accept") |
| offer = float(obs.current_offer or 0) |
| if offer: |
| return JewelryAction(message=f"How about ${offer * 1.08:.2f}?") |
| return JewelryAction(message="I need a better offer") |
|
|
| return JewelryAction() |
|
|
|
|
| |
| if "episode_steps" not in st.session_state: |
| st.session_state.episode_steps = None |
| st.session_state.final_reward = None |
| st.session_state.episode_count = 0 |
|
|
|
|
| def run_episode(task_id): |
| """Run a full episode with the heuristic agent and return step logs.""" |
| env = JewelryShopEnvironment() |
| seed = random.randint(0, 99999) |
| obs = env.reset(seed=seed, market_mode="synthetic", task_id=task_id) |
|
|
| steps = [{ |
| "step": 0, "phase": obs.phase, "action": "reset", |
| "msg": obs.message, "reward": 0.0, |
| "cash": obs.cash, "gold_oz": obs.gold_oz, |
| "gold_price": obs.gold_price, |
| "cumulative": float(obs.cumulative_reward), |
| }] |
|
|
| for i in range(1, 20): |
| if obs.done: |
| break |
| action = heuristic_action(obs) |
|
|
| |
| if obs.phase == "market": |
| act_str = f"BUY {action.gold_qty} oz" if action.market_action == "buy" else "WAIT" |
| elif obs.phase == "warehouse": |
| act_str = f"CRAFT {action.product_choice}" |
| else: |
| act_str = action.message or "..." |
|
|
| obs = env.step(action) |
| steps.append({ |
| "step": i, "phase": obs.phase, "action": act_str, |
| "msg": obs.message, "reward": float(obs.reward), |
| "cash": obs.cash, "gold_oz": obs.gold_oz, |
| "gold_price": getattr(obs, "gold_price", 0), |
| "product": getattr(obs, "product_for_sale", None), |
| "offer": float(obs.current_offer) if obs.current_offer else None, |
| "cost_basis": float(obs.cost_basis) if obs.cost_basis else None, |
| "cumulative": float(obs.cumulative_reward), |
| }) |
|
|
| return steps, float(obs.cumulative_reward) |
|
|
|
|
| |
| with st.sidebar: |
| st.markdown("### π ShopManagerEng") |
| st.markdown("---") |
|
|
| task = st.selectbox( |
| "π― Task Profile", |
| ["profit_negotiator", "market_timing", "demand_crafter"], |
| index=0, |
| ) |
| weights = { |
| "profit_negotiator": "Showroom 60% Β· Market 20% Β· Warehouse 20%", |
| "market_timing": "Market 60% Β· Warehouse 20% Β· Showroom 20%", |
| "demand_crafter": "Warehouse 60% Β· Market 20% Β· Showroom 20%", |
| } |
| st.caption(f"**Weights:** {weights[task]}") |
|
|
| st.markdown("---") |
| if st.button("π New Episode", use_container_width=True, type="primary"): |
| steps, reward = run_episode(task) |
| st.session_state.episode_steps = steps |
| st.session_state.final_reward = reward |
| st.session_state.episode_count += 1 |
| st.rerun() |
|
|
| st.markdown("---") |
| st.markdown("#### How It Works") |
| st.markdown(""" |
| An AI **heuristic agent** automatically plays |
| through all 3 phases of the jewelry shop: |
| |
| 1. π **Market** β Buy gold at the right price |
| 2. π **Warehouse** β Craft the most demanded product |
| 3. π€ **Showroom** β Negotiate the best sale price |
| |
| Press **π New Episode** to watch the agent play! |
| """) |
|
|
| if st.session_state.final_reward is not None: |
| st.markdown("---") |
| reward = st.session_state.final_reward |
| color = "#10b981" if reward >= 0.6 else "#f59e0b" if reward >= 0.4 else "#ef4444" |
| st.markdown(f"**Final Score:** :{'green' if reward >= 0.6 else 'orange' if reward >= 0.4 else 'red'}[{reward:.4f}]") |
| st.metric("Episodes Played", st.session_state.episode_count) |
|
|
|
|
| |
| st.markdown('<p class="hero-title">π Jewelry Shop Manager</p>', unsafe_allow_html=True) |
| st.markdown('<p class="hero-sub">An RL environment for training LLMs on multi-step business decisions</p>', unsafe_allow_html=True) |
|
|
|
|
| |
| if st.session_state.episode_steps is None: |
| st.info("π **Welcome!** Press **π New Episode** in the sidebar to watch the AI agent play through the jewelry shop simulation.") |
|
|
| st.markdown("### π¦ Product Catalog") |
| cols = st.columns(3) |
| items = [("π Ring", "ring"), ("πΏ Necklace", "necklace"), ("β Bracelet", "bracelet")] |
| for i, (icon_name, key) in enumerate(items): |
| spec = PRODUCT_CATALOG[key] |
| with cols[i]: |
| st.markdown(f""" |
| <div class="catalog-card"> |
| <h4>{icon_name}</h4> |
| <p>πͺ Gold: {spec['gold_oz']} oz</p> |
| <p>π§ Labor: ${spec['labor']:.0f}</p> |
| <p>π Base demand: {spec['base_demand']:.0%}</p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.markdown("### π§ Three Business Phases") |
| c1, c2, c3 = st.columns(3) |
| with c1: |
| st.markdown(""" |
| <div class="phase-box"> |
| <h4>π Phase 1: Market</h4> |
| <p>Buy raw gold at the best price. Prices fluctuate β time your purchase wisely!</p> |
| </div> |
| """, unsafe_allow_html=True) |
| with c2: |
| st.markdown(""" |
| <div class="phase-box"> |
| <h4>π Phase 2: Warehouse</h4> |
| <p>Craft a product (ring, necklace, bracelet) that matches market demand.</p> |
| </div> |
| """, unsafe_allow_html=True) |
| with c3: |
| st.markdown(""" |
| <div class="phase-box"> |
| <h4>π€ Phase 3: Showroom</h4> |
| <p>Negotiate with a customer over 5 rounds to maximize your selling price.</p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| |
| else: |
| steps = st.session_state.episode_steps |
| reward = st.session_state.final_reward |
|
|
| |
| if reward >= 0.8: |
| grade, grade_emoji = "Excellent", "π" |
| elif reward >= 0.6: |
| grade, grade_emoji = "Good", "π" |
| elif reward >= 0.4: |
| grade, grade_emoji = "Fair", "π" |
| else: |
| grade, grade_emoji = "Poor", "π¬" |
|
|
| st.markdown(f""" |
| <div class="reward-big"> |
| <div class="label">{grade_emoji} Episode #{st.session_state.episode_count} β {grade}</div> |
| <div class="score">{reward:.4f}</div> |
| <div class="label">cumulative reward (out of 1.0)</div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.markdown("") |
|
|
| |
| last = steps[-1] |
| m1, m2, m3, m4 = st.columns(4) |
| with m1: |
| st.markdown(f"""<div class="metric-card"><div class="label">Steps</div><div class="value">{len(steps)-1}</div></div>""", unsafe_allow_html=True) |
| with m2: |
| 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) |
| with m3: |
| gold_price = steps[0].get("gold_price", 0) |
| 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) |
| with m4: |
| product = None |
| for s in steps: |
| if s.get("product"): |
| product = s["product"] |
| 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) |
|
|
| st.markdown("---") |
|
|
| |
| st.markdown("### π Agent Decision Log") |
|
|
| for s in steps: |
| phase = s.get("phase", "market") |
| icon = {"market": "π", "warehouse": "π", "showroom": "π€"}.get(phase, "β¬") |
| step_num = s["step"] |
| action = s.get("action", "") |
| rw = s.get("reward", 0) |
| cum = s.get("cumulative", 0) |
|
|
| rw_badge = f" Β· reward: `{rw:.4f}`" if rw else "" |
| cum_str = f" Β· cumulative: `{cum:.4f}`" if cum else "" |
|
|
| |
| st.markdown(f"**Step {step_num}** {icon} **{action}**{rw_badge}{cum_str}") |
|
|
| |
| if s.get("msg"): |
| st.caption(s["msg"]) |
|
|
| st.divider() |
|
|
| |
| st.markdown("---") |
| st.markdown("### π Cumulative Reward Over Steps") |
| chart_data = [s["cumulative"] for s in steps] |
| st.line_chart(chart_data, use_container_width=True, height=200) |
|
|
| st.info("Press **π New Episode** in the sidebar to run another episode with a different seed!") |
|
|