Spaces:
Sleeping
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
19.1 kB
| 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. | |
| ```json | |
| { "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. | |
| ```json | |
| { "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. | |
| ```json | |
| { "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`: | |
| ```json | |
| { | |
| "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 | |
| ```python | |
| 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 | |
| ```bash | |
| # 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`) | | |
| ```bash | |
| # 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 | |
| ```bash | |
| 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 | |
| ``` | |