StoreManager / tasks.py
RaoAditya's picture
Upload folder using huggingface_hub
4723a91 verified
Raw History Blame Contribute Delete
5.49 kB
# 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.
"""
Task configurations and graders for the Store Manager environment.
Four difficulty levels are defined, in strict increasing order of complexity:
- easy : 4 products, 20 steps β€” learn discount mechanics, no expiry pressure
- medium : 8 products, 28 steps β€” discount + zone placement (zone 1 is contested)
- hard : 10 products, 30 steps β€” discount + zone + expiry timing (most expire mid-episode)
- expert : 12 products, 25 steps β€” all three actions required (restock essential, aggressive expiry)
Each task has a fixed seed so grading is deterministic: the same agent always
produces the same episode given the same seed, and the score is computed as:
score = clamp(cumulative_profit / profit_target, 0.001, 0.999)
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict
@dataclass
class TaskConfig:
"""Configuration for one difficulty level of the Store Manager task."""
name: str
description: str
num_products: int
max_steps: int
num_customers: int
expiry_offset_min: int # min steps before a product expires
expiry_offset_max: int # max steps before a product expires
seed: int # fixed seed for deterministic episode generation
profit_target: float # cumulative profit at which grade == 1.0
# ── Canonical task registry ────────────────────────────────────────────────────
TASKS: Dict[str, TaskConfig] = {
"easy": TaskConfig(
name="easy",
description=(
"4 products, 20 steps, 15 customers/step. "
"Products expire well after the episode ends β€” no expiry pressure. "
"Focus on learning the discount mechanic to boost high-margin products. "
"Zone placement is available but low-stakes; restocking is unnecessary."
),
num_products=4,
max_steps=20,
num_customers=15,
expiry_offset_min=18,
expiry_offset_max=30,
seed=42,
profit_target=120.0,
),
"medium": TaskConfig(
name="medium",
description=(
"8 products, 28 steps, 12 customers/step. "
"Several products expire mid-episode (12-22 steps). "
"Zone 1 has only 2 slots for 8 competing products β€” "
"combine targeted discounts with zone placement to maximize clearance "
"before expiry. Restocking is rarely necessary."
),
num_products=8,
max_steps=28,
num_customers=12,
expiry_offset_min=12,
expiry_offset_max=22,
seed=200,
profit_target=200.0,
),
"hard": TaskConfig(
name="hard",
description=(
"10 products, 30 steps, 10 customers/step. "
"Most products expire in 8-18 steps β€” expiry management is critical. "
"Zone strategy (rotate zone 1 for near-expiry items) combined with "
"discounts drives clearance. Restock fast-sellers that run out early "
"to maintain revenue through the full episode."
),
num_products=10,
max_steps=30,
num_customers=10,
expiry_offset_min=8,
expiry_offset_max=18,
seed=456,
profit_target=240.0,
),
"expert": TaskConfig(
name="expert",
description=(
"12 products, 25 steps, 8 customers/step. "
"Aggressive expiry (4-10 steps) with limited initial stock β€” "
"all three actions must be coordinated: discount near-expiry items to "
"clear them fast, restock high-margin products before they stock out, "
"and continuously rotate zone 1 to prioritise the most urgent products."
),
num_products=12,
max_steps=25,
num_customers=8,
expiry_offset_min=4,
expiry_offset_max=10,
seed=789,
profit_target=190.0,
),
}
# ── Grader ─────────────────────────────────────────────────────────────────────
def grade(task_name: str, cumulative_profit: float) -> float:
"""
Return a deterministic score in (0.0, 1.0) exclusive for a completed episode.
Formula:
score = clamp(cumulative_profit / profit_target, 0.001, 0.999)
Args:
task_name: One of "easy", "medium", "hard", "expert" (or a custom string).
cumulative_profit: Total profit accumulated over the episode.
Returns:
Score strictly between 0.0 and 1.0 (exclusive).
"""
import math
_EPS = 0.001
try:
cp = float(cumulative_profit)
if not math.isfinite(cp):
cp = 0.0
except (TypeError, ValueError):
cp = 0.0
config = TASKS.get(task_name)
if config is None or config.profit_target <= 0:
return 1.0 - _EPS
return min(1.0 - _EPS, max(_EPS, cp / config.profit_target))
__all__ = ["TaskConfig", "TASKS", "grade"]