hard007ik's picture
Initial deployment of Streamlit frontend
9ae6197 verified
Raw
History Blame Contribute Delete
19.4 kB
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> &nbsp;|&nbsp;
Task: <b>{task_type}</b> &nbsp;|&nbsp;
Model: <b>{model_name.split('/')[-1]}</b>
</div>
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
main()