| import asyncio |
| import math |
| import os |
| import sys |
| import textwrap |
| from typing import List, Optional |
|
|
| from dotenv import load_dotenv |
| from openai import OpenAI |
|
|
| |
| 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", "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() |
|
|
|
|
| |
|
|
| 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) |
|
|
|
|
| |
|
|
| 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 |
| |
| 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: |
| |
| 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() |
|
|
|
|
| |
|
|
| 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"): |
| |
| 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: |
| 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) |
| |
| 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" |
|
|
|
|
|
|
| |
|
|
| 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 |
|
|
|
|
| |
|
|
| 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", "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()) |
|
|
|
|