Spaces:
Build error
Build error
| import streamlit as st | |
| import os | |
| import sys | |
| import time | |
| sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) | |
| from client import JewelryShopEnv | |
| from models import JewelryAction | |
| from inference import get_model_action | |
| from openai import OpenAI | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| st.set_page_config(page_title="Jewelry Shop AI Agent", page_icon="π", layout="wide", initial_sidebar_state="expanded") | |
| # ββ PREMIUM CSS ββ | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); | |
| :root { | |
| --bg-primary: #0F1117; | |
| --bg-card: rgba(30, 34, 50, 0.7); | |
| --bg-card-hover: rgba(40, 44, 65, 0.85); | |
| --border-glass: rgba(255,255,255,0.08); | |
| --accent-red: #FF4B6E; | |
| --accent-green: #00E09E; | |
| --accent-blue: #5B8DEF; | |
| --accent-gold: #FFD700; | |
| --accent-purple: #A78BFA; | |
| --text-primary: #F1F5F9; | |
| --text-muted: #94A3B8; | |
| } | |
| html, body, [class*="css"] { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .stApp { | |
| background: linear-gradient(135deg, #0F1117 0%, #1A1B2E 50%, #0F1117 100%); | |
| color: var(--text-primary); | |
| } | |
| /* Hide default streamlit elements */ | |
| #MainMenu, footer, header {visibility: hidden;} | |
| .hero-title { | |
| font-size: 2.4rem; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, #FFD700, #FF6B6B, #A78BFA); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 0.3rem; | |
| letter-spacing: -0.5px; | |
| } | |
| .hero-sub { | |
| color: var(--text-muted); | |
| font-size: 1rem; | |
| font-weight: 400; | |
| margin-bottom: 1.5rem; | |
| line-height: 1.6; | |
| } | |
| /* Glassmorphism metric cards */ | |
| .glass-card { | |
| background: var(--bg-card); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--border-glass); | |
| border-radius: 16px; | |
| padding: 20px 16px; | |
| text-align: center; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .glass-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; right: 0; | |
| height: 3px; | |
| border-radius: 16px 16px 0 0; | |
| } | |
| .glass-card:hover { | |
| background: var(--bg-card-hover); | |
| transform: translateY(-3px); | |
| box-shadow: 0 12px 40px rgba(0,0,0,0.3); | |
| } | |
| .glass-card.phase::before { background: linear-gradient(90deg, #FF4B6E, #FF6B8A); } | |
| .glass-card.cash::before { background: linear-gradient(90deg, #00E09E, #00C9A7); } | |
| .glass-card.gold::before { background: linear-gradient(90deg, #FFD700, #FFA500); } | |
| .glass-card.items::before { background: linear-gradient(90deg, #5B8DEF, #818CF8); } | |
| .glass-card.price::before { background: linear-gradient(90deg, #A78BFA, #C084FC); } | |
| .card-value { | |
| font-size: 1.6rem; | |
| font-weight: 700; | |
| margin: 8px 0 4px; | |
| } | |
| .card-label { | |
| font-size: 0.7rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| font-weight: 600; | |
| } | |
| .card-icon { font-size: 1.4rem; } | |
| .v-phase { color: #FF4B6E; } | |
| .v-cash { color: #00E09E; } | |
| .v-gold { color: #FFD700; } | |
| .v-items { color: #5B8DEF; } | |
| .v-price { color: #A78BFA; } | |
| /* Phase pipeline */ | |
| .phase-pipeline { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0; | |
| margin: 1.2rem 0; | |
| padding: 16px 24px; | |
| background: var(--bg-card); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid var(--border-glass); | |
| border-radius: 16px; | |
| } | |
| .phase-node { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 24px; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| font-size: 0.85rem; | |
| letter-spacing: 0.5px; | |
| background: rgba(255,255,255,0.04); | |
| border: 1px solid rgba(255,255,255,0.06); | |
| color: var(--text-muted); | |
| transition: all 0.3s ease; | |
| } | |
| .phase-node.active { | |
| color: white; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| } | |
| .phase-node.active.market { background: linear-gradient(135deg, #FF4B6E, #FF6B8A); border-color: #FF4B6E; } | |
| .phase-node.active.warehouse { background: linear-gradient(135deg, #5B8DEF, #818CF8); border-color: #5B8DEF; } | |
| .phase-node.active.showroom { background: linear-gradient(135deg, #00E09E, #00C9A7); border-color: #00E09E; } | |
| .phase-node.done { color: var(--accent-green); border-color: rgba(0,224,158,0.3); } | |
| .phase-arrow { color: var(--text-muted); font-size: 1.2rem; margin: 0 8px; } | |
| /* Log entries */ | |
| .log-entry { | |
| padding: 12px 16px; | |
| margin: 6px 0; | |
| border-radius: 12px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-glass); | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| font-size: 0.9rem; | |
| transition: all 0.2s ease; | |
| } | |
| .log-entry:hover { background: var(--bg-card-hover); } | |
| .log-badge { | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| font-size: 0.7rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| flex-shrink: 0; | |
| } | |
| .badge-market { background: rgba(255,75,110,0.15); color: #FF4B6E; } | |
| .badge-warehouse { background: rgba(91,141,239,0.15); color: #5B8DEF; } | |
| .badge-showroom { background: rgba(0,224,158,0.15); color: #00E09E; } | |
| .log-action-pill { | |
| background: rgba(255,215,0,0.12); | |
| color: #FFD700; | |
| padding: 3px 10px; | |
| border-radius: 6px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| } | |
| .log-reward { | |
| margin-left: auto; | |
| font-weight: 700; | |
| font-size: 0.85rem; | |
| } | |
| .reward-pos { color: #00E09E; } | |
| .reward-zero { color: var(--text-muted); } | |
| .reward-neg { color: #FF4B6E; } | |
| .log-msg { | |
| color: var(--text-muted); | |
| font-size: 0.82rem; | |
| padding: 6px 16px 6px 40px; | |
| font-style: italic; | |
| } | |
| /* Section headers */ | |
| .section-header { | |
| font-size: 1.1rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin: 1.5rem 0 0.8rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| /* Results card */ | |
| .results-card { | |
| background: var(--bg-card); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid var(--border-glass); | |
| border-radius: 16px; | |
| padding: 24px; | |
| text-align: center; | |
| } | |
| .results-card.success { border-color: rgba(0,224,158,0.4); } | |
| .results-card.fail { border-color: rgba(255,75,110,0.4); } | |
| .results-score { | |
| font-size: 3rem; | |
| font-weight: 800; | |
| } | |
| /* Sidebar styling */ | |
| section[data-testid="stSidebar"] { | |
| background: linear-gradient(180deg, #151829 0%, #1A1D30 100%); | |
| border-right: 1px solid var(--border-glass); | |
| } | |
| section[data-testid="stSidebar"] .stSelectbox label, | |
| section[data-testid="stSidebar"] .stTextInput label { | |
| color: var(--text-muted) !important; | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| /* Start button */ | |
| .stButton>button { | |
| background: linear-gradient(135deg, #FF4B6E 0%, #FF6B8A 50%, #A78BFA 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| padding: 12px 24px; | |
| font-weight: 700; | |
| font-size: 1rem; | |
| letter-spacing: 0.5px; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| width: 100%; | |
| box-shadow: 0 4px 15px rgba(255,75,110,0.3); | |
| } | |
| .stButton>button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(255,75,110,0.5); | |
| color: white; | |
| } | |
| /* Chart containers */ | |
| .chart-container { | |
| background: var(--bg-card); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid var(--border-glass); | |
| border-radius: 16px; | |
| padding: 16px; | |
| margin: 8px 0; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ HELPER FUNCTIONS ββ | |
| def render_phase_pipeline(current_phase, done): | |
| phases = ["market", "warehouse", "showroom"] | |
| icons = {"market": "π", "warehouse": "π", "showroom": "ποΈ"} | |
| labels = {"market": "MARKET", "warehouse": "WAREHOUSE", "showroom": "SHOWROOM"} | |
| current_idx = phases.index(current_phase) if current_phase in phases else 0 | |
| html = '<div class="phase-pipeline">' | |
| for i, p in enumerate(phases): | |
| cls = "phase-node" | |
| if done: | |
| cls += " done" | |
| elif i == current_idx: | |
| cls += f" active {p}" | |
| elif i < current_idx: | |
| cls += " done" | |
| html += f'<div class="{cls}">{icons[p]} {labels[p]}</div>' | |
| if i < len(phases) - 1: | |
| html += '<span class="phase-arrow">β</span>' | |
| html += '</div>' | |
| st.markdown(html, unsafe_allow_html=True) | |
| def render_metrics(obs): | |
| crafted = [f"{v} {k}" for k, v in obs.inventory.items() if v > 0] | |
| items_str = ", ".join(crafted) if crafted else "β" | |
| cols = st.columns(5) | |
| cards = [ | |
| ("phase", "π", str(obs.phase).upper(), "CURRENT PHASE", "v-phase"), | |
| ("cash", "π΅", f"${obs.cash:,.2f}", "CASH BALANCE", "v-cash"), | |
| ("gold", "πͺ", f"{obs.gold_oz:.2f} oz", "RAW GOLD", "v-gold"), | |
| ("items", "π", items_str, "CRAFTED ITEMS", "v-items"), | |
| ("price", "π", f"${obs.gold_price:.2f}", "GOLD PRICE/OZ", "v-price"), | |
| ] | |
| for col, (ctype, icon, value, label, vcls) in zip(cols, cards): | |
| with col: | |
| st.markdown(f''' | |
| <div class="glass-card {ctype}"> | |
| <div class="card-icon">{icon}</div> | |
| <div class="card-value {vcls}">{value}</div> | |
| <div class="card-label">{label}</div> | |
| </div>''', unsafe_allow_html=True) | |
| def format_log_entry(step, phase, action_str, reward, msg=""): | |
| badge_cls = f"badge-{phase}" | |
| if reward > 0: | |
| r_cls, r_sign = "reward-pos", "+" | |
| elif reward < 0: | |
| r_cls, r_sign = "reward-neg", "" | |
| else: | |
| r_cls, r_sign = "reward-zero", "" | |
| html = f''' | |
| <div class="log-entry"> | |
| <span style="color:var(--text-muted);font-weight:600;min-width:50px;">#{step}</span> | |
| <span class="log-badge {badge_cls}">{phase.upper()}</span> | |
| <span class="log-action-pill">{action_str}</span> | |
| <span class="log-reward {r_cls}">{r_sign}{reward:.3f}</span> | |
| </div>''' | |
| if msg: | |
| html += f'<div class="log-msg">π¬ {msg}</div>' | |
| return html | |
| # ββ MAIN APP ββ | |
| def main(): | |
| # ββ SIDEBAR ββ | |
| with st.sidebar: | |
| st.markdown(""" | |
| <div style="text-align:center;margin-bottom:1.5rem;"> | |
| <div style="font-size:2.5rem;">π</div> | |
| <div style="font-size:1.2rem;font-weight:700;background:linear-gradient(135deg,#FFD700,#FF6B6B); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent;">ShopManager AI</div> | |
| <div style="color:var(--text-muted);font-size:0.75rem;margin-top:4px;">Powered by GRPO Fine-tuning</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| hf_token = st.text_input("π HuggingFace Token", type="password", | |
| value=os.getenv("HF_TOKEN", ""), | |
| help="Your HF API token for model inference") | |
| models = [ | |
| "hard007ik/shopmanager-grpo-qwen3", | |
| "meta-llama/Llama-3.3-70B-Instruct", | |
| "Qwen/Qwen2.5-72B-Instruct", | |
| ] | |
| model_name = st.selectbox("π€ AI Model", models) | |
| env_url = st.text_input("π Environment URL", | |
| value="https://hard007ik-shopmanagereng.hf.space") | |
| task_type = st.selectbox("π― Task Focus", [ | |
| "market_timing", "demand_crafter", "profit_negotiator" | |
| ], help="Controls reward weighting across phases") | |
| task_labels = { | |
| "market_timing": "Market 60% Β· Craft 20% Β· Sell 20%", | |
| "demand_crafter": "Market 20% Β· Craft 60% Β· Sell 20%", | |
| "profit_negotiator": "Market 20% Β· Craft 20% Β· Sell 60%", | |
| } | |
| st.caption(task_labels[task_type]) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| start_btn = st.button("π Start New Episode", use_container_width=True) | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style="text-align:center;color:var(--text-muted);font-size:0.7rem;"> | |
| <div>Model: <b>shopmanager-grpo-qwen3</b></div> | |
| <div style="margin-top:4px;">Env: <b>HuggingFace Spaces</b></div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ HERO HEADER ββ | |
| st.markdown(""" | |
| <div class="hero-title">π Jewelry Shop AI Agent</div> | |
| <div class="hero-sub"> | |
| Watch your fine-tuned AI autonomously manage a jewelry business β | |
| buying gold at the right price, crafting high-demand products, and negotiating maximum profit with customers. | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ PLACEHOLDERS ββ | |
| pipeline_ph = st.empty() | |
| metrics_ph = st.empty() | |
| chart_cols_ph = st.empty() | |
| st.markdown('<div class="section-header">π Live Event Log</div>', unsafe_allow_html=True) | |
| log_ph = st.empty() | |
| result_ph = st.empty() | |
| # ββ IDLE STATE ββ | |
| if not start_btn: | |
| pipeline_ph.markdown(""" | |
| <div class="phase-pipeline"> | |
| <div class="phase-node">π MARKET</div> | |
| <span class="phase-arrow">β</span> | |
| <div class="phase-node">π WAREHOUSE</div> | |
| <span class="phase-arrow">β</span> | |
| <div class="phase-node">ποΈ SHOWROOM</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.info("π Configure settings in the sidebar and click **Start New Episode** to watch the AI run!") | |
| return | |
| # ββ VALIDATION ββ | |
| if not hf_token: | |
| st.error("π Please enter your HuggingFace API Token in the sidebar.") | |
| return | |
| # ββ INIT ENV ββ | |
| with st.spinner("π Connecting to environment server..."): | |
| try: | |
| env = JewelryShopEnv(base_url=env_url).sync() | |
| result = env.reset(task_id=task_type) | |
| obs = result.observation | |
| client = OpenAI(base_url="https://router.huggingface.co/v1", api_key=hf_token) | |
| except Exception as e: | |
| st.error(f"β Failed to connect: {e}") | |
| return | |
| # ββ TRACKING STATE ββ | |
| history = [] | |
| rewards_list = [] | |
| gold_prices = list(obs.gold_price_history) if obs.gold_price_history else [obs.gold_price] | |
| step_num = 1 | |
| done = result.done | |
| last_reward = result.reward or 0.0 | |
| log_html = "" | |
| phase_order = [] | |
| # Render initial state | |
| with pipeline_ph.container(): | |
| render_phase_pipeline(obs.phase, False) | |
| with metrics_ph.container(): | |
| render_metrics(obs) | |
| # ββ AUTOMATED EPISODE LOOP ββ | |
| while not done and step_num <= 15: | |
| # 1. Ask AI for action | |
| try: | |
| import inference | |
| orig_model = inference.MODEL_NAME | |
| inference.MODEL_NAME = model_name | |
| action, action_str = get_model_action( | |
| client=client, step=step_num, obs=obs, | |
| last_reward=last_reward, history=history | |
| ) | |
| inference.MODEL_NAME = orig_model | |
| except Exception as e: | |
| log_html += f'<div class="log-entry" style="border-color:#FF4B6E;"><span style="color:#FF4B6E;">β AI Error: {e}</span></div>' | |
| log_ph.markdown(log_html, unsafe_allow_html=True) | |
| break | |
| current_phase = obs.phase | |
| phase_order.append(current_phase) | |
| # 2. Step environment | |
| try: | |
| result = env.step(action) | |
| obs = result.observation | |
| reward = result.reward or 0.0 | |
| done = result.done | |
| rewards_list.append(reward) | |
| history.append(f"Step {step_num} ({current_phase}): '{action_str}' -> reward {reward:+.2f}") | |
| # Track gold prices | |
| if obs.gold_price_history: | |
| gold_prices = list(obs.gold_price_history) | |
| elif obs.gold_price and obs.gold_price not in gold_prices: | |
| gold_prices.append(obs.gold_price) | |
| except Exception as e: | |
| log_html += f'<div class="log-entry" style="border-color:#FF4B6E;"><span style="color:#FF4B6E;">β Env Error: {e}</span></div>' | |
| log_ph.markdown(log_html, unsafe_allow_html=True) | |
| break | |
| # 3. Update all displays | |
| log_html += format_log_entry(step_num, current_phase, action_str, reward, obs.message) | |
| log_ph.markdown(log_html, unsafe_allow_html=True) | |
| with pipeline_ph.container(): | |
| render_phase_pipeline(obs.phase if not done else "showroom", done) | |
| with metrics_ph.container(): | |
| render_metrics(obs) | |
| # 4. Update charts | |
| with chart_cols_ph.container(): | |
| c1, c2, c3 = st.columns(3) | |
| with c1: | |
| st.markdown('<div class="chart-container">', unsafe_allow_html=True) | |
| st.caption("π Gold Price Trend") | |
| if len(gold_prices) > 1: | |
| st.line_chart(gold_prices, height=180) | |
| else: | |
| st.info(f"Current: ${gold_prices[0]:.2f}/oz") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with c2: | |
| st.markdown('<div class="chart-container">', unsafe_allow_html=True) | |
| st.caption("π Demand Forecast") | |
| forecast = getattr(obs, "demand_forecast", {}) or {} | |
| if forecast: | |
| st.bar_chart(forecast, height=180) | |
| else: | |
| demand = getattr(obs, "demand", {}) or {} | |
| if demand: | |
| st.bar_chart(demand, height=180) | |
| else: | |
| st.info("No demand data yet") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with c3: | |
| st.markdown('<div class="chart-container">', unsafe_allow_html=True) | |
| st.caption("π Cumulative Reward") | |
| if rewards_list: | |
| cum_rewards = [] | |
| running = 0 | |
| for r in rewards_list: | |
| running += r | |
| cum_rewards.append(running) | |
| st.area_chart(cum_rewards, height=180) | |
| else: | |
| st.info("No rewards yet") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| last_reward = reward | |
| step_num += 1 | |
| time.sleep(1.0) | |
| # ββ CLEANUP ββ | |
| try: | |
| env.close() | |
| except Exception: | |
| pass | |
| # ββ FINAL RESULTS ββ | |
| score = float(getattr(obs, "cumulative_reward", sum(rewards_list) if rewards_list else 0.0)) | |
| score = min(max(score, 0.0), 1.0) | |
| success = score >= 0.01 | |
| with result_ph.container(): | |
| cls = "success" if success else "fail" | |
| color = "#00E09E" if success else "#FF4B6E" | |
| icon = "π" if success else "π" | |
| label = "SUCCESS" if success else "FAILED" | |
| st.markdown(f""" | |
| <div class="results-card {cls}" style="margin-top:2rem;"> | |
| <div style="font-size:2rem;">{icon}</div> | |
| <div style="color:{color};font-weight:700;font-size:0.9rem;letter-spacing:2px;margin:8px 0;">{label}</div> | |
| <div class="results-score" style="color:{color};">{score:.4f}</div> | |
| <div style="color:var(--text-muted);font-size:0.85rem;margin-top:8px;"> | |
| Steps: <b>{step_num - 1}</b> | | |
| Task: <b>{task_type}</b> | | |
| Model: <b>{model_name.split('/')[-1]}</b> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main() | |