Spaces:
Sleeping
Download README.md from RaoAditya/StoreManager: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/spaces/RaoAditya/StoreManager/resolve/main/README.md
- Command line
-
hf download hf://spaces/RaoAditya/StoreManager/README.md
-
curl -L -o README.md https://huggingface.co/spaces/RaoAditya/StoreManager/resolve/main/README.md
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_priceis never modified β it serves as a reference for how far prices have been cut.current_discount_pctreflects only the discount tier applied this step (used in pick-probability formula).- Prices reset to
base_selling_priceat 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_unitcharged upfront this stepholding_cost = quantity Γ cost_price Γ holding_cost_ratecharged 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