| """ |
| Inference Script — Personality-Driven Shopping Agent Environment |
| ================================================================ |
| MANDATORY |
| - Before submitting, ensure the following variables are defined: |
| API_BASE_URL The API endpoint for the LLM. |
| MODEL_NAME The model identifier to use for inference. |
| OPENAI_API_KEY Your OpenAI API key. |
| |
| STDOUT FORMAT |
| - [START] task=<task_name> env=<benchmark> model=<model_name> |
| - [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null> |
| - [END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn> |
| |
| Runs 3 tasks (easy → medium → hard) and produces baseline scores. |
| """ |
|
|
| import asyncio |
| import json |
| import os |
| import random |
| import textwrap |
| from typing import List, Optional |
|
|
| from dotenv import load_dotenv |
| from openai import OpenAI |
|
|
| from models import ShoppingAction |
| from shopping_env import ShoppingEnv |
| from memory_engine import load_profile |
|
|
| load_dotenv() |
|
|
| |
| |
| |
| API_KEY = ( |
| os.getenv("OPENAI_API_KEY") |
| or os.getenv("HF_TOKEN") |
| or os.getenv("API_KEY") |
| ) |
| API_BASE_URL = os.getenv( |
| "API_BASE_URL", |
| "https://api.openai.com/v1" if os.getenv("OPENAI_API_KEY") else "https://router.huggingface.co/v1", |
| ) |
| MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct") |
| RANDOM_SEED = int(os.getenv("BASELINE_RANDOM_SEED", "42")) |
|
|
| BENCHMARK = "shopping_agent" |
| TEMPERATURE = 0.7 |
| MAX_TOKENS = 300 |
|
|
| |
| TASKS = [ |
| {"name": "quick_pick", "query": "lip balm", "difficulty": "easy", "max_steps": 8, "product_count": 4}, |
| {"name": "smart_shop", "query": "earbuds", "difficulty": "medium", "max_steps": 12, "product_count": 8}, |
| {"name": "expert_deal", "query": "laptop backpack", "difficulty": "hard", "max_steps": 15, "product_count": 12}, |
| ] |
|
|
|
|
| |
| |
| |
| 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} " |
| f"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} " |
| f"score={score:.2f} rewards={rewards_str}", |
| flush=True, |
| ) |
|
|
|
|
| |
| |
| |
| def build_system_prompt(profile_text: str) -> str: |
| """Build the system prompt with the full user personality profile.""" |
| return textwrap.dedent(f"""\ |
| You are a shopping agent acting on behalf of a specific user. |
| Your goal is to buy the product that THIS USER would choose — not the |
| generically "best" product. |
| |
| {profile_text} |
| |
| ## Your Task |
| Given the current observation (products, personality profile, cart, history), |
| decide the BEST next action for THIS specific user. Think about: |
| - Would this user research more before buying? (check research_depth) |
| - Would this user pick the cheapest option? (check price_sensitivity) |
| - Would this user avoid risky products? (check risk_aversion) |
| - Would this user trust this brand? (check brand_trust) |
| - Would this user care about reviews? (check review_dependence) |
| |
| You MUST reply with **only valid JSON** matching this schema: |
| |
| {{ |
| "action_type": "search" | "view_item" | "compare" | "shortlist" | |
| "add_to_cart" | "remove_from_cart" | "buy" | "skip" | "ask_more", |
| "item_ids": ["p1", ...], // optional, list of product IDs |
| "search_query": "query text" // optional, only for search |
| }} |
| |
| Rules: |
| - Always think about what THIS USER would do based on their personality traits. |
| - Research depth matters: if research_depth > 0.7, view 3+ items and compare before buying. |
| - Risk aversion matters: if risk_aversion > 0.7, avoid products with <50 reviews or unknown sellers. |
| - The user's personality determines which product is "right", not just price or ratings. |
| - Do NOT output anything besides the JSON object. |
| """).strip() |
|
|
|
|
| |
| |
| |
| def build_user_prompt( |
| step: int, |
| obs_dict: dict, |
| last_reward: float, |
| history: List[str], |
| ) -> str: |
| history_block = "\n".join(history[-5:]) if history else "None" |
| mem_profile = obs_dict.get("memory_profile", {}) |
| obs_summary = { |
| "query": obs_dict.get("query", ""), |
| "personality_preferences": { |
| k: v for k, v in mem_profile.items() |
| if isinstance(v, (int, float)) and 0 <= v <= 1 |
| }, |
| "personality_conclusions": mem_profile.get("semantic_conclusions", [])[:4], |
| "products": [ |
| { |
| "id": p["id"], |
| "name": p["name"], |
| "price": p["price"], |
| "rating": p["rating"], |
| "brand": p["brand"], |
| "reviews": p["reviews"], |
| "refundable": p["refundable"], |
| "seller": p.get("seller", ""), |
| } |
| for p in obs_dict.get("candidate_products", []) |
| ], |
| "cart": obs_dict.get("cart", []), |
| "shortlisted": obs_dict.get("shortlisted", []), |
| "viewed": obs_dict.get("viewed_items", []), |
| "feedback": obs_dict.get("feedback", ""), |
| "step": obs_dict.get("step_number", step), |
| "max_steps": obs_dict.get("max_steps", 15), |
| } |
| return textwrap.dedent(f"""\ |
| Step {step} | Last reward: {last_reward:.2f} |
| |
| Observation: |
| {json.dumps(obs_summary, indent=2)} |
| |
| Recent history: |
| {history_block} |
| |
| Think about what this specific user would do based on their personality traits. |
| Reply with the next action as JSON.""") |
|
|
|
|
| def get_agent_action( |
| client: Optional[OpenAI], |
| system_prompt: str, |
| step: int, |
| obs_dict: dict, |
| last_reward: float, |
| history: List[str], |
| ) -> ShoppingAction: |
| if client is None: |
| return _fallback_action(obs_dict) |
| user_prompt = build_user_prompt(step, obs_dict, 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() |
| |
| if text.startswith("```"): |
| text = text.split("\n", 1)[-1] |
| if text.endswith("```"): |
| text = text.rsplit("```", 1)[0] |
| text = text.strip() |
| data = json.loads(text) |
| return ShoppingAction(**data) |
| except Exception as exc: |
| print(f"[DEBUG] LLM parse error: {exc}", flush=True) |
| return _fallback_action(obs_dict) |
|
|
|
|
| def _fallback_action(obs_dict: dict) -> ShoppingAction: |
| """ |
| Smart heuristic fallback when the LLM is unavailable. |
| Progresses through the shopping workflow based on current state. |
| """ |
| cart = obs_dict.get("cart", []) |
| shortlisted = obs_dict.get("shortlisted", []) |
| viewed = obs_dict.get("viewed_items", []) |
| compared = obs_dict.get("compared_sets", []) |
| products = obs_dict.get("candidate_products", []) |
|
|
| |
| if cart: |
| return ShoppingAction(action_type="buy", item_ids=list(cart)) |
|
|
| |
| if shortlisted: |
| return ShoppingAction(action_type="add_to_cart", item_ids=[shortlisted[0]]) |
|
|
| |
| if compared: |
| flat = [] |
| for s in compared: |
| flat.extend(s) |
| unique = list(dict.fromkeys(flat))[:2] |
| return ShoppingAction(action_type="shortlist", item_ids=unique) |
|
|
| |
| if len(viewed) >= 2: |
| return ShoppingAction(action_type="compare", item_ids=viewed[:3]) |
|
|
| |
| mid_range = [ |
| p["id"] for p in products |
| if p.get("price", 999) < 150 |
| and p.get("rating", 0) >= 3.5 |
| and p.get("reviews", 0) >= 50 |
| ][:3] |
| if mid_range: |
| return ShoppingAction(action_type="view_item", item_ids=mid_range) |
|
|
| |
| if products: |
| return ShoppingAction(action_type="view_item", item_ids=[products[0]["id"]]) |
|
|
| return ShoppingAction(action_type="skip") |
|
|
|
|
| |
| |
| |
| async def run_episode( |
| client: OpenAI, task: dict |
| ) -> tuple: |
| """Run a single task episode. Returns (success, steps, score, rewards).""" |
| task_name = task["name"] |
| query = task["query"] |
| max_steps = task["max_steps"] |
| product_count = task["product_count"] |
|
|
| env = ShoppingEnv(task_name=task_name) |
| env._max_steps = max_steps |
| history: List[str] = [] |
| rewards: List[float] = [] |
| steps_taken = 0 |
| score = 0.0 |
| success = False |
|
|
| |
| profile = load_profile() |
| profile_text = profile.to_prompt_text(category=query) |
| system_prompt = build_system_prompt(profile_text) |
|
|
| log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME) |
|
|
| try: |
| result = await env.reset(query=query, product_count=product_count) |
| obs_dict = result.observation.model_dump() |
| last_reward = 0.0 |
|
|
| for step_num in range(1, max_steps + 1): |
| if result.done: |
| break |
|
|
| action = get_agent_action( |
| client, system_prompt, step_num, obs_dict, last_reward, history |
| ) |
| result = await env.step(action) |
| reward = result.reward or 0.0 |
| done = result.done |
| error = None |
|
|
| rewards.append(reward) |
| steps_taken = step_num |
| obs_dict = result.observation.model_dump() |
| last_reward = reward |
|
|
| |
| ids_str = ",".join(action.item_ids) if action.item_ids else "" |
| if action.search_query: |
| action_str = f"{action.action_type}('{action.search_query}')" |
| elif ids_str: |
| action_str = f"{action.action_type}({ids_str})" |
| else: |
| action_str = action.action_type |
|
|
| log_step( |
| step=step_num, |
| action=action_str, |
| reward=reward, |
| done=done, |
| error=error, |
| ) |
| history.append( |
| f"Step {step_num}: {action_str} -> reward {reward:+.2f}" |
| ) |
|
|
| if done: |
| break |
|
|
| |
| |
| |
| if rewards: |
| buy_reward = max((r for r in rewards if r >= 0.3), default=0.0) |
| if buy_reward > 0: |
| score = buy_reward |
| else: |
| positive_rewards = sum(r for r in rewards if r > 0) |
| score = min(positive_rewards / 2.0, 0.3) |
| score = min(max(score, 0.0), 1.0) |
| success = score >= 0.5 |
|
|
| 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 success, steps_taken, score, rewards |
|
|
|
|
| |
| |
| |
| async def main() -> None: |
| random.seed(RANDOM_SEED) |
| client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY) if API_KEY else None |
|
|
| all_scores = {} |
| for task in TASKS: |
| _success, _steps, score, _rewards = await run_episode(client, task) |
| all_scores[task["name"]] = score |
| print(flush=True) |
|
|
| print("=" * 60, flush=True) |
| print("SUMMARY", flush=True) |
| for t, s in all_scores.items(): |
| status = "✅" if s >= 0.5 else "❌" |
| print(f" {status} {t:16s}: {s:.2f}", flush=True) |
| avg = sum(all_scores.values()) / len(all_scores) |
| print(f" {'average':19s}: {avg:.2f}", flush=True) |
| print("=" * 60, flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|