StoreManager / README.md
RaoAditya's picture
Upload folder using huggingface_hub
aab74bd verified
|
Raw History Blame Contribute Delete
19.1 kB
metadata
title: Store Manager Environment
emoji: πŸ›’
colorFrom: blue
colorTo: yellow
sdk: docker
pinned: false
app_port: 8000
base_path: /web
tags:
  - openenv

Store Manager Environment

A retail store simulator where an LLM agent acts as the store manager. The agent observes full inventory state each step and chooses one action β€” discount a product, restock inventory, or move a product to a better shelf zone β€” to influence customer demand and maximize cumulative profit over a fixed episode horizon.


What Makes This Environment Interesting

This environment models three interconnected real-world retail mechanics that require the agent to reason about compounding consequences:

  • Discounts are permanent within an episode. Every discount cuts the current (already-discounted) price further. A 20% discount applied twice yields 36% off the original price. The agent must decide how aggressively to discount β€” deep cuts clear stock fast but permanently erode margin.
  • Restocking has a time lag. Stock doesn't arrive instantly. Each product has a restock_lead_time (1–3 steps). The agent must plan ahead β€” ordering too late leaves the shelf empty; ordering too much risks expiry penalties and inflated holding costs.
  • Zone placement directly multiplies pick probability. Zone 1 (premium) boosts visibility 1.5Γ—, but only holds 2 products. The agent must continuously rotate which products deserve premium placement.

These three mechanics combine into a rich multi-step decision problem where each action affects future state: a discount today changes tomorrow's revenue; a restock today affects holding costs for the next several steps; a zone move today costs $3 but can double a product's sell-through rate.


Task Overview

Component Description
Products N products, each with cost price, selling price, quantity, expiry step, shelf zone, and a base pick probability
Customers Fixed number of customers per step pick products according to a normalized probability distribution
Agent Action Exactly one of three action types per step: discount, restock, or place
Reward Normalized sigmoid reward in [0.0, 1.0] based on step profit vs. per-step target
Expiry penalty remaining_quantity Γ— cost_price charged when a product reaches its expiry step
Episode end After max_steps steps, or when all products are inactive

Pick Probability Formula

Customer pick probability combines zone placement and discount multiplier:

effective_weight[i] = base_probability[i] Γ— zone_multiplier[zone[i]] Γ— (1 + discount_pct[i] / 100)
pick_prob[i]        = effective_weight[i] / sum(effective_weight for all active products)

Actions

The agent submits exactly one action per step.

1. Discount

Reduce the selling price of a product. Discounts compound across steps within an episode β€” each discount cuts the current price further, not the original base price.

{ "action_type": "discount", "product_id": 2, "discount_pct": 20 }
Field Type Values
action_type string "discount"
product_id int any valid product ID
discount_pct int 0, 10, 20, or 50

Compounding example (base price $10.00):

Step Discount Selling Price Total off base
1 20% $8.00 βˆ’20%
2 20% $6.40 βˆ’36%
3 20% $5.12 βˆ’49%
4 50% $2.56 βˆ’74%
  • base_selling_price is never modified β€” it serves as a reference for how far prices have been cut.
  • current_discount_pct reflects only the discount tier applied this step (used in pick-probability formula).
  • Prices reset to base_selling_price at the start of each new episode (reset()).
  • A 0% discount leaves the price unchanged but still advances the step.

When to use: Apply deep discounts to near-expiry stock to clear inventory before the expiry penalty hits. Use light discounts on high-margin products to drive volume without destroying margin.


2. Restock

Place a purchase order for more units of a product. Stock arrives after restock_lead_time steps. The ordering cost is charged immediately this step.

{ "action_type": "restock", "product_id": 1, "restock_quantity": 15 }
Field Type Values
action_type string "restock"
product_id int any valid product ID
restock_quantity int any positive integer

How it works:

  • On restock: pending_restock_quantity = restock_quantity, pending_restock_arrives_at_step = current_step + restock_lead_time
  • When delivery step is reached: quantity += restock_quantity, pending fields cleared
  • Only one pending order per product at a time β€” wait for delivery before reordering

Costs:

  • restock_cost = restock_quantity Γ— restock_cost_per_unit charged upfront this step
  • holding_cost = quantity Γ— cost_price Γ— holding_cost_rate charged every step on all stock regardless of action

Per-product restock parameters (randomised at episode start):

Field Range Description
restock_lead_time 1–3 steps Steps until delivery
restock_cost_per_unit cost_price Γ— 1.00–1.25 Ordering cost per unit
holding_cost_rate 0.01–0.04 Fraction of cost_price charged per unit per step

Error cases: Invalid if restock_quantity ≀ 0 or product already has a pending order.

When to use: Replenish a fast-selling product before it stocks out, only when enough steps remain to sell through the new inventory before expiry.


3. Place (Zone Placement)

Move a product to a different shelf zone. Takes effect immediately. Charges a flat labor fee of $3.00 per move.

{ "action_type": "place", "product_id": 3, "target_zone": 1 }
Field Type Values
action_type string "place"
product_id int any valid product ID
target_zone int 1, 2, or 3

Zone multipliers and capacity:

Zone Location Pick Probability Multiplier Max Products
1 Premium / entrance / eye-level Γ—1.5 2
2 Standard shelves Γ—1.0 4
3 Back / low-traffic Γ—0.6 unlimited

Initial zone assignment at episode start: products fill zone 1 first (up to capacity 2), then zone 2, remainder in zone 3.

Error cases: Invalid if target_zone is not in {1, 2, 3}, product is already in target_zone, or target zone is at capacity.

When to use: Rotate zone 1 to whichever product most needs visibility β€” near-expiry items benefit most since clearance speed directly avoids penalty. Demote slow-movers to zone 3 to free up premium space.


Reward Function

Each step returns a normalized reward in [0.0, 1.0] using a sigmoid centred at break-even:

step_profit = step_revenue βˆ’ cost_of_goods_sold βˆ’ expiry_penalty
            βˆ’ holding_cost βˆ’ placement_cost βˆ’ restock_cost

x      = step_profit / per_step_target
reward = sigmoid(2x) = 1 / (1 + exp(βˆ’2x))
Step profit vs. target reward
Catastrophic loss (βˆ’βˆž) β†’ 0.0
Break-even (0) = 0.50
On target β‰ˆ 0.88
2Γ— target β‰ˆ 0.98

The raw per-step cost breakdown is available in metadata:

{
  "step_revenue": 45.20,
  "step_cost": 18.40,
  "expiry_penalty": 0.0,
  "holding_cost": 12.35,
  "placement_cost": 3.0,
  "restock_cost": 0.0,
  "step_profit": 11.45
}

Difficulty Tasks

Four pre-configured tasks with increasing complexity. Each task has a fixed seed for deterministic, reproducible grading.

Task Products Steps Customers/step Expiry range Profit target Dominant mechanic
easy 4 20 15 18–30 steps $120 Discount
medium 8 28 12 12–22 steps $200 Discount + zone placement
hard 10 30 10 8–18 steps $240 Discount + zone + expiry timing
expert 12 25 8 4–10 steps $190 All three actions coordinated

Task Descriptions

easy β€” Products expire well after the episode ends; no expiry pressure. The agent learns to identify high-margin products and apply targeted discounts to boost their pick probability.

medium β€” Several products expire mid-episode. Zone 1 has only 2 slots for 8 competing products β€” the agent must combine discounts with smart zone rotation to clear stock before expiry.

hard β€” Most products expire in 8–18 steps. Expiry management is critical: rotate zone 1 frequently to accelerate clearance, and consider restocking fast-sellers that run out early.

expert β€” Aggressive expiry (4–10 steps) with limited initial stock. All three actions must be coordinated: discount near-expiry items heavily, restock high-margin products before they stock out, and rotate zone 1 continuously.

Grading

score = clamp(cumulative_profit / profit_target, 0.0, 1.0)

Baseline Scores

Expected performance using gpt-4o-mini (deterministic, fixed seeds):

Task Profit Target Expected Profit Expected Score
easy $120 ~$140–180 ~0.90–1.0
medium $200 ~$160–220 ~0.80–1.0
hard $240 ~$180–260 ~0.75–1.0
expert $190 ~$100–170 ~0.50–0.90

Scores are fully reproducible. Re-run inference.py with the same model and seeds to verify.


Quick Start

from StoreManager import StoremanagerAction, StoremanagerEnv

with StoremanagerEnv(base_url="http://localhost:8000") as env:
    result = env.reset()
    print([p.name for p in result.observation.inventory])

    # Discount: compounds on current price each time applied
    result = env.step(StoremanagerAction(action_type="discount", product_id=0, discount_pct=20))
    print(f"Reward: {result.reward:.2f}")

    # Restock: stock arrives after restock_lead_time steps
    result = env.step(StoremanagerAction(action_type="restock", product_id=1, restock_quantity=10))
    print(f"Restock cost: {result.observation.restock_cost_this_step:.2f}")

    # Place: move product to premium zone (costs $3.00 labor)
    result = env.step(StoremanagerAction(action_type="place", product_id=2, target_zone=1))
    print(f"Placement cost: {result.observation.placement_cost_this_step:.2f}")
    print(f"Zone occupancy: {result.observation.zone_occupancy}")

Running Locally

# Install dependencies
uv sync

# Run dev server (auto-reload, default config)
uvicorn server.app:app --reload

# Run with a specific difficulty task baked in at startup
STORE_TASK=hard uvicorn server.app:app --reload

# Test environment logic directly (no server needed)
python server/StoreManager_environment.py

# Run the WebSocket test suite (server must be running)
python test_ws.py

Running the Inference Script

The inference.py script runs an LLM agent through all four tasks sequentially and reports a score for each.

Required Environment Variables

Variable Description
HF_TOKEN API key β€” your Hugging Face token or OpenAI API key
API_BASE_URL LLM API base URL (e.g. https://api-inference.huggingface.co/v1/)
MODEL_NAME Model identifier (e.g. meta-llama/Llama-3.1-8B-Instruct or gpt-4o-mini)
# Server must be running first
uvicorn server.app:app &

# Run with Hugging Face Inference API
HF_TOKEN=hf_... \
API_BASE_URL=https://api-inference.huggingface.co/v1/ \
MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct \
python inference.py

# Run with OpenAI (API_BASE_URL defaults to OpenAI when unset)
HF_TOKEN=sk-... MODEL_NAME=gpt-4o-mini python inference.py

# Override the environment server URL
HF_TOKEN=sk-... MODEL_NAME=gpt-4o-mini STORE_ENV_URL=http://localhost:8000 python inference.py

Structured Log Format

[START] {"task": "easy", "seed": 42, "model": "gpt-4o-mini", "max_steps": 20, "profit_target": 120.0}
[STEP]  {"task": "easy", "step": 1, "action": {"action_type": "discount", "product_id": 2, "discount_pct": 20}, "reward": 0.88, "cumulative_profit": 12.5, "holding_cost": 8.2, "placement_cost": 0, "restock_cost": 0}
...
[END]   {"task": "easy", "cumulative_profit": 145.3, "score": 1.0, "profit_target": 120.0}

A single server instance handles all four tasks β€” the task config is passed in the reset message, so no server restart is needed between tasks.


Action & Observation Reference

Action: StoremanagerAction

Field Type Default Description
action_type "discount" | "restock" | "place" "discount" Type of action
product_id int required ID of the product to act on
discount_pct 0 | 10 | 20 | 50 0 Discount tier (for discount actions)
restock_quantity int None Units to order (for restock actions)
target_zone 1 | 2 | 3 None Destination zone (for place actions)

Observation: StoremanagerObservation

Field Type Description
inventory List[ProductState] Full state of all products (including inactive)
current_step int Current step number (0 at reset, increments after each valid step)
max_steps int Total steps in this episode
steps_remaining int Steps left in the episode
cumulative_profit float Total profit accumulated so far
last_step_profit float Net profit from the most recent step
last_expired_products List[str] Names of products that expired this step
zone_capacity Dict[int, int] Max products per zone: {1: 2, 2: 4, 3: 99}
zone_occupancy Dict[int, int] Active products currently in each zone
zone_multipliers Dict[int, float] Pick-prob multiplier per zone: {1: 1.5, 2: 1.0, 3: 0.6}
holding_cost_this_step float Total holding cost charged this step
placement_cost_this_step float 3.0 if a place action was taken, else 0.0
restock_cost_this_step float Ordering cost if a restock was placed, else 0.0
task_name str Active task: easy / medium / hard / expert / custom
error str | None Error message if the action was invalid (step does not advance)
metadata dict Per-step breakdown: step_revenue, step_cost, expiry_penalty, holding_cost, placement_cost, restock_cost, step_profit

ProductState Fields (inside inventory)

Field Description
product_id Unique integer ID
name Product name (e.g. "Milk")
cost_price Per-unit cost to the store
selling_price Current selling price (decreases each time a discount is applied)
base_selling_price Original price β€” never modified, reference for total discount depth
quantity Units currently in stock
expiry_step Episode step at which this product expires
zone Current shelf zone: 1 (premium), 2 (standard), 3 (back)
effective_pick_prob Normalized pick probability this step (sums to 1.0 across all active products)
current_discount_pct Discount tier applied this step only (0 if no discount action taken)
holding_cost_rate Holding cost per unit per step as a fraction of cost_price
restock_cost_per_unit Cost to order one additional unit
restock_lead_time Steps until a placed order is delivered
pending_restock_quantity Units currently on order (not yet delivered)
pending_restock_arrives_at_step Step number when the pending order will arrive
is_active False if the product is sold out or expired

Error Handling

When an action is invalid, the step does not advance. The returned observation contains an error string and reflects the pre-action state. Common error cases:

Scenario Error
Discount on inactive / sold-out product "Product '...' is no longer active"
Restock with quantity ≀ 0 "restock_quantity must be a positive integer"
Restock when pending order exists "already has a pending restock order"
Place to same zone product is already in "is already in zone N"
Place to zone at capacity "Zone N is at capacity"
Invalid product_id "Invalid product_id"

Always check obs.get("error") before processing the observation.


Configuration

Control the server via environment variables:

Variable Default Description
STORE_TASK (none) Pre-load a task config at startup: easy, medium, hard, or expert
STORE_NUM_PRODUCTS 8 Number of products (overridden by STORE_TASK)
STORE_MAX_STEPS 30 Episode length in steps (overridden by STORE_TASK)
STORE_NUM_CUSTOMERS 10 Customers simulated per step (overridden by STORE_TASK)
STORE_MAX_CONCURRENT_ENVS 4 Max parallel WebSocket sessions

Task switching at runtime: A single server instance can serve all four tasks. Pass "task": "<name>" in the WebSocket reset message and the environment reconfigures immediately without a server restart.


Deploying to Hugging Face Spaces

openenv push
openenv push --repo-id my-org/store-manager --private

After deployment, your space includes:

  • Web Interface at /web
  • API Documentation at /docs
  • WebSocket at /ws β€” persistent session for low-latency agent interactions

Project Structure

StoreManager/
β”œβ”€β”€ __init__.py                       # Module exports
β”œβ”€β”€ models.py                         # Pydantic schemas: Action, Observation, State
β”œβ”€β”€ client.py                         # StoremanagerEnv WebSocket client
β”œβ”€β”€ tasks.py                          # Task configs (easy/medium/hard/expert) + grader
β”œβ”€β”€ inference.py                      # LLM agent runner with structured logs
β”œβ”€β”€ test_ws.py                        # WebSocket integration test suite (7 test groups)
β”œβ”€β”€ openenv.yaml                      # OpenEnv manifest
β”œβ”€β”€ pyproject.toml                    # Dependencies
└── server/
    β”œβ”€β”€ StoreManager_environment.py   # Core RL environment logic
    β”œβ”€β”€ app.py                        # FastAPI server (HTTP + WebSocket)
    └── Dockerfile                    # Container image