shopmanager-train-code / inference.py
hard007ik's picture
inference updated for 3 task
28cbbf9
Raw
History Blame
11.8 kB
import asyncio
import math
import os
import sys
import textwrap
from typing import List, Optional
from dotenv import load_dotenv
from openai import OpenAI
# Add parent directory to path so ShopManagerEng is importable as a package
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ShopManagerEng.client import JewelryShopEnv
from ShopManagerEng.models import JewelryAction
load_dotenv()
IMAGE_NAME = os.getenv("IMAGE_NAME")
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
# MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.3-70B-Instruct")
TASK_NAME = os.getenv("JEWELRY_ENV_TASK", "jewelry-shop")
BENCHMARK = os.getenv("JEWELRY_ENV_BENCHMARK", "jewelry_shop_benchmark")
MAX_STEPS = 15
TEMPERATURE = 0.7
MAX_TOKENS = 150
SUCCESS_SCORE_THRESHOLD = 0.01
SYSTEM_PROMPT = textwrap.dedent(
"""
You are an expert agent running a jewelry shop. Maximize profit across 3 phases.
## Phase 1: MARKET (buy/wait)
Gold prices fluctuate Β±10% each round (up to 3 rounds).
- Analyze the price trend from the history.
- If the price DROPPED from the previous round, it might drop further β†’ consider waiting.
- If the price ROSE or you're on the last round β†’ buy now.
- Reserve enough cash for labor ($100-$300 depending on product).
- Respond: "buy X.XX" (to buy X.XX oz of gold) or "wait" (to see next price).
## Phase 2: WAREHOUSE (choose product)
You see demand levels for each product. Pick the HIGHEST demand product
that you can afford to craft (enough gold + cash for labor).
Products: ring (1oz + $200), necklace (2oz + $300), bracelet (0.5oz + $100).
- Respond: "ring", "necklace", or "bracelet"
## Phase 3: SHOWROOM (negotiate)
A customer offers a price. Your goal is to sell at maximum profit.
- Counter-offer to drive the price up (customer raises 5% each round, max 5 rounds).
- Accept when the offer is good (round >= 3 or offer > 1.3Γ— cost).
- NEVER reject.
- Respond: "I accept" or a counter like "How about $X?"
CRITICAL: Respond with ONLY the action value. No explanations.
"""
).strip()
# ── LOGGING ────────────────────────────────────
def log_start(task: str, env: str, model: str) -> None:
print(f"[START] task={task} env={env} model={model}", flush=True)
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
error_val = error if error else "null"
done_val = str(done).lower()
print(
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
flush=True,
)
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
# ── PROMPT BUILDING ────────────────────────────
def build_user_prompt(step: int, obs, last_reward: float, history: List[str]) -> str:
history_block = "\n".join(history[-4:]) if history else "None"
if obs.phase == "market":
prices = obs.gold_price_history
trend = ""
if len(prices) >= 2:
if prices[-1] < prices[-2]:
trend = "FALLING ↓ (might keep dropping, consider waiting)"
else:
trend = "RISING ↑ (buy now before it gets more expensive)"
rounds_left = obs.max_market_rounds - obs.market_round
# Suggest buy quantity that reserves $300 for labor (max labor cost)
reserve = 300.0
if obs.gold_price > 0:
raw_qty = (obs.cash - reserve) / obs.gold_price
suggested_qty = math.floor(raw_qty * 100) / 100
suggested_qty = max(suggested_qty, 0.01)
else:
suggested_qty = 1.0
phase_hint = (
f"Price history: {prices}. Trend: {trend}. "
f"Rounds left: {rounds_left}. "
f"If buying, suggested qty: {suggested_qty} oz (reserves $300 for labor). "
f"Respond: 'buy {suggested_qty}' or 'wait'"
)
elif obs.phase == "warehouse":
demand = obs.demand
best_product = max(demand, key=demand.get) if demand else "ring"
phase_hint = (
f"Demand: ring={demand.get('ring', 0):.0%}, "
f"necklace={demand.get('necklace', 0):.0%}, "
f"bracelet={demand.get('bracelet', 0):.0%}. "
f"Highest demand: {best_product}. "
f"You have {obs.gold_oz}oz gold and ${obs.cash} cash. "
f"Respond with EXACTLY: {best_product}"
)
elif obs.phase == "showroom":
margin = ""
if obs.current_offer and obs.cost_basis > 0:
margin_pct = ((obs.current_offer - obs.cost_basis) / obs.cost_basis) * 100
margin = f"Margin: {margin_pct:+.1f}%. "
should_accept = False
if obs.negotiation_round >= 4:
should_accept = True
if obs.current_offer and obs.cost_basis > 0 and obs.current_offer > obs.cost_basis * 1.3:
should_accept = True
if should_accept:
phase_hint = (
f"Cost: ${obs.cost_basis}. Offer: ${obs.current_offer}. {margin}"
f"Round {obs.negotiation_round}/5. "
f"Respond with EXACTLY: I accept"
)
else:
# Vary counter-offers per round
counter_msgs = [
"I need a better price for this quality piece",
"That's too low, this craftsmanship deserves more",
f"How about ${round(obs.cost_basis * 1.4, 2)}?",
f"I can't go below ${round(obs.cost_basis * 1.3, 2)}",
]
msg = counter_msgs[min(obs.negotiation_round, len(counter_msgs) - 1)]
phase_hint = (
f"Cost: ${obs.cost_basis}. Offer: ${obs.current_offer}. {margin}"
f"Round {obs.negotiation_round}/5. "
f"DO NOT ACCEPT. Counter-offer. "
f"Respond with EXACTLY: {msg}"
)
else:
phase_hint = ""
return textwrap.dedent(
f"""
Step: {step} | Phase: {obs.phase} | Last reward: {last_reward:.2f}
Cash: ${obs.cash} | Gold: {obs.gold_oz}oz | Rings: {obs.inventory}
Gold Price: ${obs.gold_price}/oz
Env Message: {obs.message}
{phase_hint}
History: {history_block}
"""
).strip()
# ── ACTION PARSING ─────────────────────────────
def get_action_from_text(phase: str, text: str) -> tuple[JewelryAction, str]:
text = text.strip().replace("`", "").strip(' \t\n\r"\'')
if phase == "market":
lower = text.lower()
if lower.startswith("buy"):
# Extract quantity from "buy 2.5" or "buy2.5"
qty_str = lower.replace("buy", "").strip()
try:
qty = float(qty_str)
except ValueError:
qty = 1.0
return JewelryAction(market_action="buy", gold_qty=qty), f"buy {qty}"
elif "wait" in lower:
return JewelryAction(market_action="wait"), "wait"
else:
# Try to parse as a number (assumed buy)
try:
qty = float(text)
return JewelryAction(market_action="buy", gold_qty=qty), f"buy {qty}"
except ValueError:
return JewelryAction(market_action="wait"), "wait"
elif phase == "warehouse":
lower = text.lower()
for product in ["necklace", "bracelet", "ring"]:
if product in lower:
return JewelryAction(product_choice=product), product
return JewelryAction(product_choice="ring"), "ring"
elif phase == "showroom":
return JewelryAction(message=text), text
return JewelryAction(), text
def get_model_action(client: OpenAI, step: int, obs, last_reward: float, history: List[str]) -> tuple[JewelryAction, str]:
user_prompt = build_user_prompt(step, obs, last_reward, history)
try:
completion = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS,
stream=False,
)
text = (completion.choices[0].message.content or "").strip()
return get_action_from_text(obs.phase, text)
except Exception as exc:
print(f"[DEBUG] Model request failed: {exc}", flush=True)
# Fallback actions
if obs.phase == "market":
return JewelryAction(market_action="buy", gold_qty=1.0), "buy 1.0"
elif obs.phase == "warehouse":
return JewelryAction(product_choice="ring"), "ring"
else:
return JewelryAction(message="I accept"), "I accept"
# ── SINGLE EPISODE RUNNER ──────────────────────
async def run_episode(client: OpenAI, task_name: str, env_name: str, base_url: str) -> float:
"""Run a single episode and return the final score."""
history: List[str] = []
rewards: List[float] = []
steps_taken = 0
score = 0.0
success = False
log_start(task=task_name, env=env_name, model=MODEL_NAME)
try:
env = JewelryShopEnv(base_url=base_url)
result = await env.reset()
obs = result.observation
last_reward = 0.0
for step in range(1, MAX_STEPS + 1):
if result.done:
break
action, raw_action_str = get_model_action(client, step, obs, last_reward, history)
current_phase = obs.phase
result = await env.step(action)
obs = result.observation
reward = result.reward or 0.0
done = result.done
error = None
rewards.append(reward)
steps_taken = step
last_reward = reward
log_step(step=step, action=raw_action_str.replace('\n', ' '), reward=reward, done=done, error=error)
history.append(f"Step {step} ({current_phase}): {raw_action_str!r} -> reward {reward:+.2f}")
if done:
break
if rewards:
score = rewards[-1]
else:
score = 0.0
score = min(max(score, 0.0), 1.0)
success = score >= SUCCESS_SCORE_THRESHOLD
finally:
try:
await env.close()
except Exception as e:
print(f"[DEBUG] env.close() error: {e}", flush=True)
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
return score
# ── MAIN ───────────────────────────────────────
TASKS = [
{"id": "market_timing", "env": "jewelry_shop_benchmark"},
{"id": "demand_crafter", "env": "jewelry_shop_benchmark"},
{"id": "profit_negotiator", "env": "jewelry_shop_benchmark"},
]
async def main() -> None:
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
# base_url = os.getenv("ENV_BASE_URL", "http://localhost:8000")
base_url = os.getenv("ENV_BASE_URL", "https://huggingface.co/spaces/hard007ik/ShopManagerEng")
for task in TASKS:
await run_episode(client, task["id"], task["env"], base_url)
if __name__ == "__main__":
asyncio.run(main())