Spaces:
Sleeping
Sleeping
Download inference.py from Code-Father/store: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/spaces/Code-Father/store/resolve/main/inference.py
- Command line
-
hf download hf://spaces/Code-Father/store/inference.py
-
curl -L -o inference.py https://huggingface.co/spaces/Code-Father/store/resolve/main/inference.py
15.2 kB
| """ | |
| Inference script for OpenEnv Store environment. | |
| Uses an LLM (via OpenAI-compatible client) to play the kirana-store simulation. | |
| Emits structured [START], [STEP], [END] logs for evaluation scoring. | |
| Required env vars: | |
| API_BASE_URL – LLM API endpoint | |
| MODEL_NAME – model identifier | |
| HF_TOKEN – API key | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from typing import Any | |
| from openai import AsyncOpenAI | |
| from client import StoreEnv | |
| from models import ( | |
| OrderRequest, | |
| PriceUpdate, | |
| StoreAction, | |
| StoreObservation, | |
| dump_model, | |
| ) | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") | |
| MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen3.5-397B-A17B") | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| BENCHMARK = "store" | |
| TASKS: list[dict[str, Any]] = [ | |
| { | |
| "name": "store_short_run", | |
| "description": "Run the store profitably over 5 decision windows (15 days).", | |
| "max_steps": 5, | |
| "success_threshold": 0.5, | |
| }, | |
| { | |
| "name": "store_medium_run", | |
| "description": "Run the store profitably over 10 decision windows (30 days).", | |
| "max_steps": 10, | |
| "success_threshold": 0.55, | |
| }, | |
| { | |
| "name": "store_long_run", | |
| "description": ( | |
| "Run the store profitably over 15 decision windows (45 days). " | |
| "Requires sustained capital growth and careful long-term planning." | |
| ), | |
| "max_steps": 15, | |
| "success_threshold": 0.6, | |
| }, | |
| ] | |
| ENV_URL = os.environ.get("ENV_URL", "http://localhost:8000") | |
| 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: dict[str, Any] | str, | |
| reward: float, | |
| done: bool, | |
| error: str | None, | |
| ) -> None: | |
| done_str = "true" if done else "false" | |
| error_str = error if error else "null" | |
| print( | |
| f"[STEP] step={step} action={action} reward={reward:.2f} " | |
| f"done={done_str} error={error_str}", | |
| flush=True, | |
| ) | |
| def log_end( | |
| *, | |
| success: bool, | |
| steps: int, | |
| score: float, | |
| rewards: list[float], | |
| ) -> None: | |
| success_str = "true" if success else "false" | |
| rewards_str = ",".join(f"{r:.2f}" for r in rewards) | |
| print( | |
| f"[END] success={success_str} steps={steps} score={score:.2f} " | |
| f"rewards={rewards_str}", | |
| flush=True, | |
| ) | |
| def _format_observation(obs: StoreObservation) -> str: | |
| """Build a concise text summary of the observation for the LLM prompt.""" | |
| store = obs.store | |
| lines = [ | |
| "=== Store Snapshot ===", | |
| f"Day: {store.day}/{store.episode_horizon_days}", | |
| f"Bank Balance: ₹{store.bank_balance_inr:,.2f}", | |
| f"Customer Satisfaction: {store.customer_satisfaction}", | |
| f"Order Processing Capacity: {store.order_processing_capacity}", | |
| f"Daily Expense: ₹{store.current_daily_expense_inr:,.2f}", | |
| f"Cumulative Profit: ₹{store.cumulative_profit_inr:,.2f}", | |
| f"Active Recurring Cost: ₹{store.active_recurring_cost_inr:,.2f}", | |
| f"Active Action IDs: {store.active_action_ids}", | |
| "", | |
| "=== Current Stock (top items by value) ===", | |
| ] | |
| sorted_stock = sorted(obs.stock, key=lambda s: s.stock_value_inr, reverse=True) | |
| for item in sorted_stock[:15]: | |
| margin = ( | |
| ((item.selling_price_inr - item.buying_price_inr) / item.selling_price_inr) * 100 | |
| if item.selling_price_inr > 0 | |
| else 0 | |
| ) | |
| lines.append( | |
| f" {item.product_name}: qty={item.quantity_on_hand:.1f}, " | |
| f"buy=₹{item.buying_price_inr}, sell=₹{item.selling_price_inr}, " | |
| f"demand={item.demand_score}, margin={margin:.1f}%, " | |
| f"value=₹{item.stock_value_inr:.0f}" | |
| ) | |
| low_stock = [s for s in obs.stock if 0 < s.quantity_on_hand < 15] | |
| if low_stock: | |
| lines.append("") | |
| lines.append("=== Low Stock Alert ===") | |
| for item in low_stock: | |
| lines.append(f" {item.product_name}: qty={item.quantity_on_hand:.1f}") | |
| if obs.pending_orders: | |
| lines.append("") | |
| lines.append("=== Pending Orders ===") | |
| for po in obs.pending_orders: | |
| lines.append( | |
| f" {po.order_id}: {po.product_name} x{po.quantity} from {po.seller_name}, " | |
| f"arrives in {po.days_remaining}d" | |
| ) | |
| lines.append("") | |
| lines.append("=== Marketing Actions (inactive only) ===") | |
| for ma in obs.marketing_actions: | |
| if not ma.is_active: | |
| lines.append( | |
| f" ID {ma.action_id}: {ma.description} | " | |
| f"cost=₹{ma.cost_inr}, recurring=₹{ma.recurring_cost_inr}/day, " | |
| f"satisfaction_delta={ma.customer_satisfaction_delta:+d}, " | |
| f"capacity_delta={ma.order_processing_capacity_delta:+d}" | |
| ) | |
| if obs.metadata: | |
| rb = obs.metadata.get("reward_breakdown") | |
| if rb: | |
| lines.append("") | |
| lines.append("=== Last Reward Breakdown ===") | |
| lines.append(f" Starting Capital: ₹{rb.get('starting_capital_inr', 'N/A')}") | |
| lines.append(f" Ending Capital: ₹{rb.get('ending_capital_inr', 'N/A')}") | |
| lines.append(f" Difference: ₹{rb.get('difference_inr', 'N/A')}") | |
| lines.append(f" Recurring Costs: ₹{rb.get('recurring_costs_paid_inr', 'N/A')}") | |
| return "\n".join(lines) | |
| SYSTEM_PROMPT = """\ | |
| You are a store management AI agent controlling a neighborhood kirana (grocery) store in India. | |
| ## Simulation Mechanics | |
| Each step you submit ONE bundled action for the next 3-day decision window: | |
| 1. orders: list of supplier orders [{"product": "...", "seller": "Seller X", "quantity": N}] | |
| 2. prices: list of price updates [{"product": "...", "price": N}] (in-stock products only) | |
| 3. action_ids: list of marketing/operational action IDs to activate | |
| ## Ground-Truth Formulas | |
| DAILY ORDERS PROCESSED = order_processing_capacity × (customer_satisfaction / 100) | |
| For each in-stock product with quantity > 0: | |
| margin_pct = ((selling_price - buying_price) / selling_price) × 100 | |
| product_selling_score = max(0, demand_score - margin_pct × 3) | |
| TOTAL_SELLING_SCORE = sum of all product_selling_scores | |
| Each product's daily sales = (product_selling_score / TOTAL_SELLING_SCORE) × DAILY_ORDERS_PROCESSED | |
| (capped at available quantity) | |
| REWARD = 0.5 + (ending_capital - starting_capital - recurring_costs_6days) / starting_capital | |
| where capital = bank_balance + total_inventory_value | |
| CUSTOMER SATISFACTION UPDATE after price/stock changes: | |
| new_satisfaction = (1 + (score_after - score_before) / score_after) × base_satisfaction | |
| Capped at [1, 100]. | |
| ## Seller Profiles | |
| - Seller A: Cheapest on staples but OVERCOMMITS — deliveries often 1-2 days late | |
| - Seller B: Reliable baseline, matches quoted ETA consistently | |
| - Seller C: Quotes conservatively, often arrives EARLY — best for urgent fresh restocks | |
| - Seller D: Competitive on snacks/beverages but execution is uneven | |
| ## Key Constraints | |
| - Episode: 30 days, each step commits 3 days, reward horizon = 6 days | |
| - Bank balance < 0 → episode ends immediately | |
| - Marketing actions are DURABLE: one-time cost + permanent recurring daily cost | |
| - Starting cash is tight (₹100k) relative to inventory (~₹240k) | |
| ## Response Format | |
| You MUST respond with ONLY valid JSON: | |
| { | |
| "reasoning": "brief explanation of your decision", | |
| "orders": [{"product": "...", "seller": "Seller X", "quantity": N}, ...], | |
| "prices": [{"product": "...", "price": N}, ...], | |
| "action_ids": [N, ...] | |
| } | |
| No extra text outside JSON. Product names must exactly match the catalog. | |
| """ | |
| def _build_task_system_prompt(task: dict[str, Any]) -> str: | |
| max_steps = task["max_steps"] | |
| threshold = task["success_threshold"] | |
| goal = ( | |
| f"## Task: {task['name']}\n\n" | |
| f"You have {max_steps} decision windows. " | |
| f"Target average reward per step ≥ {threshold:.1f}.\n\n" | |
| "STRATEGY PRIORITIES:\n" | |
| "1. Activate zero-recurring marketing early: IDs 1, 10, 6 (+75 capacity, no daily cost)\n" | |
| "2. Restock high-demand cheap items: Biscuits (demand 90), Bottled Water (demand 89), " | |
| "Eggs (demand 84), Bread (demand 86) — use Seller B/C for reliability\n" | |
| "3. Use Seller A for bulk staples (Rice, Oil, Spices) only when stock cover is safe\n" | |
| "4. Protect anchor item prices: do NOT raise Milk/Bread/Eggs/Sugar/Bottled Water\n" | |
| "5. NEVER reorder Kitchen Appliances (dead stock, demand 28, margin ~35%)\n" | |
| "6. NEVER activate ID 15 (social ads: ₹1,200/day for +0 capacity)\n" | |
| "7. Keep bank_balance > ₹15k safety buffer at all times\n" | |
| "8. After foundations, consider WhatsApp ordering (ID 4) and delivery partner (ID 7)" | |
| ) | |
| return SYSTEM_PROMPT + f"\n\n{goal}" | |
| async def get_model_action( | |
| client: AsyncOpenAI, | |
| step: int, | |
| observation_text: str, | |
| last_reward: float, | |
| history: list[str], | |
| task_system_prompt: str, | |
| ) -> dict[str, Any]: | |
| """Query the LLM for the next action.""" | |
| history_text = "\n".join(history[-5:]) if history else "No history yet." | |
| user_msg = ( | |
| f"Step {step} | Last reward: {last_reward:+.4f}\n\n" | |
| f"Recent history:\n{history_text}\n\n" | |
| f"Current observation:\n{observation_text}\n\n" | |
| "Decide your next action. Respond with ONLY valid JSON." | |
| ) | |
| response = await client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": task_system_prompt}, | |
| {"role": "user", "content": user_msg}, | |
| ], | |
| temperature=0.3, | |
| max_tokens=4096, | |
| ) | |
| content = response.choices[0].message.content or "{}" | |
| content = content.strip() | |
| print(f"[DEBUG] Raw LLM response (first 500 chars): {content[:500]}", flush=True) | |
| think_end = content.find("</think>") | |
| if think_end != -1: | |
| content = content[think_end + len("</think>"):].strip() | |
| if content.startswith("```"): | |
| content = content.split("\n", 1)[-1] | |
| if content.endswith("```"): | |
| content = content[: -len("```")] | |
| content = content.strip() | |
| return json.loads(content) | |
| def _parse_action(raw: dict[str, Any]) -> StoreAction: | |
| """Convert the LLM JSON response into a StoreAction.""" | |
| orders = [ | |
| OrderRequest( | |
| product=o["product"], | |
| seller=o["seller"], | |
| quantity=int(o["quantity"]), | |
| ) | |
| for o in raw.get("orders", []) | |
| ] | |
| prices = [ | |
| PriceUpdate(product=p["product"], price=int(p["price"])) | |
| for p in raw.get("prices", []) | |
| ] or None | |
| action_ids = [int(a) for a in raw.get("action_ids", [])] | |
| return StoreAction(orders=orders, prices=prices, action_ids=action_ids) | |
| async def run_task( | |
| llm_client: AsyncOpenAI, | |
| task: dict[str, Any], | |
| ) -> float: | |
| """Run a single task episode and return the clamped score.""" | |
| task_name = task["name"] | |
| max_steps = task["max_steps"] | |
| threshold = task["success_threshold"] | |
| task_system_prompt = _build_task_system_prompt(task) | |
| history: list[str] = [] | |
| rewards: list[float] = [] | |
| steps_taken = 0 | |
| score = 0.0 | |
| success = False | |
| log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME) | |
| env = StoreEnv(base_url=ENV_URL) | |
| try: | |
| result = await env.reset() | |
| obs = result.observation | |
| obs_text = _format_observation(obs) | |
| last_reward = 0.0 | |
| for step in range(1, max_steps + 1): | |
| if obs.done: | |
| break | |
| raw_action: dict[str, Any] = {} | |
| action_str = "" | |
| error = None | |
| try: | |
| raw_action = await get_model_action( | |
| llm_client, | |
| step, | |
| obs_text, | |
| last_reward, | |
| history, | |
| task_system_prompt, | |
| ) | |
| action = _parse_action(raw_action) | |
| action_str = json.dumps(dump_model(action, exclude_none=True), separators=(",", ":")) | |
| except Exception as e: | |
| print(f"[DEBUG] Action parse error at step {step}: {e}", flush=True) | |
| action = StoreAction() | |
| action_str = "{}" | |
| error = str(e) | |
| try: | |
| result = await env.step(action) | |
| except Exception as e: | |
| print(f"[DEBUG] Step error at step {step}: {e}", flush=True) | |
| action = StoreAction() | |
| action_str = "{}" | |
| error = str(e) | |
| try: | |
| result = await env.step(action) | |
| except Exception as e2: | |
| print(f"[DEBUG] Pass-step also failed: {e2}", flush=True) | |
| error = str(e2) | |
| break | |
| obs = result.observation | |
| reward = result.reward or 0.0 | |
| done = result.done | |
| rewards.append(reward) | |
| steps_taken = step | |
| obs_text = _format_observation(obs) | |
| last_reward = reward | |
| log_step( | |
| step=step, | |
| action=action_str, | |
| reward=reward, | |
| done=done, | |
| error=error, | |
| ) | |
| reasoning = raw_action.get("reasoning", "") | |
| history.append( | |
| f"Step {step}: reward={reward:+.4f}, done={done}, " | |
| f"reasoning={reasoning!r}" | |
| ) | |
| if done: | |
| break | |
| score = sum(rewards) / (max_steps * 1.0) if max_steps > 0 else 0.0 | |
| score = min(max(score, 0.0), 1.0) | |
| success = score >= threshold | |
| finally: | |
| try: | |
| await env.close() | |
| except Exception as e: | |
| print(f"[DEBUG] env.close() error (container cleanup): {e}", flush=True) | |
| log_end(success=success, steps=steps_taken, score=score, rewards=rewards) | |
| return score | |
| async def main() -> None: | |
| llm_client = AsyncOpenAI( | |
| base_url=API_BASE_URL, | |
| api_key=HF_TOKEN, | |
| ) | |
| print(f"[DEBUG] Starting inference with model={MODEL_NAME}", flush=True) | |
| print(f"[DEBUG] API_BASE_URL={API_BASE_URL}", flush=True) | |
| print(f"[DEBUG] ENV_URL={ENV_URL}", flush=True) | |
| print(f"[DEBUG] Tasks: {[t['name'] for t in TASKS]}", flush=True) | |
| all_scores: list[float] = [] | |
| for task in TASKS: | |
| print(f"\n[DEBUG] === Running task: {task['name']} ===", flush=True) | |
| t0 = time.time() | |
| task_score = await run_task(llm_client, task) | |
| elapsed = time.time() - t0 | |
| all_scores.append(task_score) | |
| print( | |
| f"[DEBUG] Task {task['name']} completed: score={task_score:.4f}, " | |
| f"elapsed={elapsed:.1f}s", | |
| flush=True, | |
| ) | |
| avg_score = sum(all_scores) / len(all_scores) if all_scores else 0.0 | |
| print(f"\n[DEBUG] All task scores: {all_scores}", flush=True) | |
| print(f"[DEBUG] Average score: {avg_score:.4f}", flush=True) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |