File size: 8,947 Bytes
aab74bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""

Data models for the Store Manager Environment.



The Store Manager environment simulates a retail store where an LLM agent

acts as the store manager, applying discounts to products to maximize profit

while managing inventory and handling product expiry.

"""

from __future__ import annotations

from typing import Dict, List, Literal, Optional

from openenv.core.env_server.types import Action, Observation, State
from pydantic import BaseModel, Field


class ProductState(BaseModel):
    """State of a single product in the store inventory."""

    product_id: int = Field(..., description="Unique integer identifier for the product")
    name: str = Field(..., description="Human-readable product name")
    cost_price: float = Field(..., description="Cost to the store per unit")
    selling_price: float = Field(..., description="Current selling price (after discount)")
    base_selling_price: float = Field(..., description="Original selling price before any discount")
    quantity: int = Field(..., description="Units currently in stock")
    expiry_step: int = Field(
        ..., description="Episode step number at which this product expires"
    )
    base_probability: float = Field(
        ..., description="Base pick-probability weight before discount"
    )
    current_discount_pct: float = Field(
        default=0.0, description="Active discount percentage this step (0/10/20/50)"
    )
    effective_pick_prob: float = Field(
        default=0.0,
        description="Normalised pick probability this step after applying discount (sums to 1 across all active products)",
    )
    is_active: bool = Field(
        default=True, description="False if the product is sold out or expired"
    )
    sales_history: List[Dict] = Field(
        default_factory=list,
        description="List of {step, discount_pct, units_sold} records",
    )
    # Zone / Shelf Placement fields
    zone: int = Field(
        default=2, description="Current shelf zone (1=premium/high-traffic, 2=standard, 3=back/low-traffic)"
    )
    # Restocking / Procurement fields
    holding_cost_rate: float = Field(
        default=0.02, description="Holding cost per unit per step as a fraction of cost_price"
    )
    restock_cost_per_unit: float = Field(
        default=0.0, description="Cost per unit when reordering (may exceed cost_price due to rush ordering)"
    )
    restock_lead_time: int = Field(
        default=2, description="Number of steps until a restock order is delivered"
    )
    pending_restock_quantity: int = Field(
        default=0, description="Units currently on order (not yet delivered)"
    )
    pending_restock_arrives_at_step: Optional[int] = Field(
        default=None, description="Step number when the pending restock order will arrive"
    )
    # Compounding popularity effect
    popularity_multiplier: float = Field(
        default=1.0,
        description=(
            "Cumulative pick-probability multiplier earned through repeated discounting. "
            "Starts at 1.0 and compounds up each time a discount action is applied: "
            "popularity_multiplier *= (1 + discount_pct / 100). "
            "Persists across steps within an episode; resets to 1.0 on reset()."
        ),
    )
    # Derived signal fields (recomputed each step — do not set manually)
    steps_until_expiry: int = Field(
        default=0,
        description=(
            "Steps remaining before this product expires (expiry_step - current_step). "
            "Near-expiry threshold: <= 3 steps. Below-cost discounting is permitted "
            "when steps_until_expiry <= 3; the price floor loosens progressively."
        ),
    )
    margin_per_unit: float = Field(
        default=0.0,
        description=(
            "Current per-unit profit margin: selling_price - cost_price. "
            "Positive = profitable. Zero = break-even. "
            "Negative = selling at a loss (only possible when steps_until_expiry <= 3)."
        ),
    )


class StoremanagerAction(Action):
    """Agent action: discount a product, restock inventory, or move a product to a different zone."""

    action_type: Literal["discount", "restock", "place"] = Field(
        default="discount",
        description="Type of action: 'discount' to apply a price discount, 'restock' to order more inventory, 'place' to move product to a different shelf zone",
    )
    product_id: int = Field(..., description="ID of the product to act on")
    discount_pct: Literal[0, 10, 20, 50] = Field(
        default=0, description="Discount percentage (used when action_type='discount'): 0, 10, 20, or 50"
    )
    restock_quantity: Optional[int] = Field(
        default=None, description="Number of units to order (used when action_type='restock'); must be > 0"
    )
    target_zone: Optional[Literal[1, 2, 3]] = Field(
        default=None, description="Destination zone (used when action_type='place'): 1=premium, 2=standard, 3=back"
    )


class StoremanagerObservation(Observation):
    """Full observation returned to the agent after each step."""

    inventory: List[ProductState] = Field(
        default_factory=list,
        description="Current state of all products (including inactive ones)",
    )
    current_step: int = Field(default=0, description="Current step number (0-indexed)")
    max_steps: int = Field(default=30, description="Total steps in this episode")
    steps_remaining: int = Field(
        default=30, description="Steps remaining in the episode"
    )
    cumulative_profit: float = Field(
        default=0.0, description="Total profit accumulated so far in the episode"
    )
    last_step_profit: float = Field(
        default=0.0, description="Net profit from the most recent step"
    )
    last_step_units_sold: int = Field(
        default=0, description="Total units sold across all products in the last step"
    )
    last_expired_products: List[str] = Field(
        default_factory=list,
        description="Names of products that expired at the end of the last step",
    )
    num_customers_per_step: int = Field(
        default=10, description="Fixed number of customers simulated per step"
    )
    task_name: str = Field(
        default="custom",
        description="Active difficulty task: easy / medium / hard / expert / custom",
    )
    error: Optional[str] = Field(
        default=None, description="Error message if the last action was invalid"
    )
    # Zone information
    zone_capacity: Dict[int, int] = Field(
        default_factory=lambda: {1: 2, 2: 4, 3: 99},
        description="Maximum number of products allowed per zone",
    )
    zone_occupancy: Dict[int, int] = Field(
        default_factory=lambda: {1: 0, 2: 0, 3: 0},
        description="Current number of active products in each zone",
    )
    zone_multipliers: Dict[int, float] = Field(
        default_factory=lambda: {1: 1.5, 2: 1.0, 3: 0.6},
        description="Pick-probability multiplier per zone",
    )
    # Step cost breakdown
    holding_cost_this_step: float = Field(
        default=0.0, description="Total holding cost charged this step across all products"
    )
    placement_cost_this_step: float = Field(
        default=0.0, description="Labor cost charged this step if a zone placement was made"
    )
    restock_cost_this_step: float = Field(
        default=0.0, description="Ordering cost charged this step if a restock order was placed"
    )
    unjustified_discount_penalty: float = Field(
        default=0.0,
        description=(
            "Extra penalty this step for selling below cost_price when steps_until_expiry >= 4. "
            "Equals the total per-unit loss on unjustified below-cost units sold. "
            "Zero when below-cost selling is justified by near-expiry urgency."
        ),
    )


class StoremanagerState(State):
    """Internal state snapshot for debugging and orchestration."""

    num_products: int = Field(default=8, description="Number of products in this episode")
    max_steps: int = Field(default=30, description="Episode length in steps")
    cumulative_profit: float = Field(default=0.0, description="Total episode profit so far")
    last_reward: float = Field(default=0.0, description="Reward from the last step")
    active_product_count: int = Field(
        default=0, description="Number of currently active (non-expired, in-stock) products"
    )
    inventory_snapshot: List[Dict] = Field(
        default_factory=list,
        description="Serialised inventory for state introspection",
    )