Spaces:
Sleeping
Sleeping
|
Download README.md from Code-Father/store: direct link, hf CLI and curl.
- Browser
- Download file 20.6 kB
-
https://huggingface.co/spaces/Code-Father/store/resolve/main/README.md
- Command line
-
hf download hf://spaces/Code-Father/store/README.md
-
curl -L -o README.md https://huggingface.co/spaces/Code-Father/store/resolve/main/README.md
20.6 kB
| title: Store Environment Server | |
| emoji: 🛒 | |
| colorFrom: green | |
| colorTo: yellow | |
| sdk: docker | |
| pinned: false | |
| app_port: 8000 | |
| base_path: /web | |
| tags: | |
| - openenv | |
| # Store Environment | |
| This repository now contains a CSV-backed neighborhood grocery simulation for | |
| OpenEnv. The environment is intentionally rooted in a kirana-store business | |
| loop: manage cash, stock, supplier orders, customer satisfaction, pricing, and | |
| durable operations upgrades over repeated decision windows. | |
| This README documents the current implementation, not the older echo-demo | |
| scaffold and not the earlier narrative plans. The runtime source of truth is: | |
| - `store/data/*.csv` | |
| - `store/models.py` | |
| - `store/server/store_environment.py` | |
| - `store/client.py` | |
| - `store/tests/*.py` | |
| If a note in `plan.md`, `plan2.md`, or `rules.md` conflicts with the code or the | |
| seed CSVs, this document follows the code and the current CSVs. | |
| ## At A Glance | |
| - Episode horizon: `30` simulated days by default | |
| - Decision window: `3` committed days per step | |
| - Reward lookahead: `6` simulated days per step | |
| - Action shape: one bundled action containing `orders`, `prices`, and | |
| `action_ids` | |
| - Observation shape: full store snapshot plus inventory, pending orders, market | |
| catalog, seller profiles, marketing catalog, and an action guide | |
| - State storage: in-memory per environment instance, seeded from CSV files on | |
| `reset()` | |
| - Reward form: `0.5 + normalized_difference` | |
| ## Seed Data | |
| The environment seeds itself from the CSV files in `store/data/`. | |
| ### Current seed snapshot | |
| - `store_state.csv`: 1 row | |
| - `stock.csv`: 30 rows | |
| - `market_offers.csv`: 100 rows | |
| - `seller_profiles.csv`: 4 rows | |
| - `pending_orders.csv`: 0 rows | |
| - `marketing_actions.csv`: 15 rows | |
| The opening store snapshot is currently: | |
| | Field | Value | | |
| |---|---:| | |
| | `day` | `1` | | |
| | `current_daily_expense_inr` | `3000` | | |
| | `bank_balance_inr` | `120000` | | |
| | `customer_satisfaction` | `70` | | |
| | `order_processing_capacity` | `500` | | |
| | `cumulative_profit_inr` | `0` | | |
| The current opening stock value is `237,192 INR`, which sits inside the intended | |
| `220k-260k INR` band. | |
| ## Files And Roles | |
| | File | Role | | |
| |---|---| | |
| | `store/models.py` | Action, observation, and data models | | |
| | `store/server/store_environment.py` | Core simulation engine | | |
| | `store/client.py` | Client wrapper for remote HTTP/WebSocket usage | | |
| | `store/server/app.py` | FastAPI app factory entrypoint | | |
| | `store/data/*.csv` | Seed data and catalog source of truth | | |
| | `store/tests/test_seed_csvs.py` | Seed integrity checks | | |
| | `store/tests/test_store_environment.py` | Behavioral simulation tests | | |
| ## Action Contract | |
| The agent sends one bundled action per decision window. | |
| ```python | |
| from store import OrderRequest, PriceUpdate, StoreAction | |
| action = StoreAction( | |
| orders=[ | |
| OrderRequest(product="Rice", seller="Seller A", quantity=25), | |
| OrderRequest(product="Bread", seller="Seller C", quantity=12), | |
| ], | |
| prices=[PriceUpdate(product="Milk", price=33)], | |
| action_ids=[7], | |
| ) | |
| ``` | |
| Passing is: | |
| ```python | |
| StoreAction() | |
| ``` | |
| ### Action fields | |
| | Field | Type | Meaning | | |
| |---|---|---| | |
| | `orders` | `list[OrderRequest]` | Supplier orders to place at the start of the step | | |
| | `prices` | `list[PriceUpdate] \| None` | Optional selling-price updates for stocked products | | |
| | `action_ids` | `list[int]` | Durable marketing or operations actions to activate | | |
| ### Nested action models | |
| | Model | Fields | | |
| |---|---| | |
| | `OrderRequest` | `product`, `seller`, `quantity` | | |
| | `PriceUpdate` | `product`, `price` | | |
| ### Validation rules for actions | |
| - `quantity` must be a positive integer. | |
| - `seller` must be one of `Seller A`, `Seller B`, `Seller C`, or `Seller D`. | |
| - `orders[*].product` must exist in `market_offers.csv`. | |
| - `prices[*].product` must already exist in current stock. | |
| - `prices[*].price` must be positive. | |
| - `action_ids` must exist in `marketing_actions.csv`. | |
| - Duplicate price updates for the same product are rejected. | |
| - Duplicate marketing action IDs in one step are rejected. | |
| - Already-active marketing actions cannot be activated again. | |
| ## Observation Contract | |
| `StoreObservation` is the agent-facing state. The OpenEnv transport `State` | |
| still only carries `episode_id` and `step_count`, but the business state lives in | |
| the observation payload. | |
| ### Top-level observation fields | |
| | Field | Meaning | | |
| |---|---| | |
| | `store` | Current top-level business snapshot | | |
| | `stock` | Current in-store inventory rows | | |
| | `pending_orders` | In-flight supplier orders | | |
| | `market` | Full supplier catalog with prices and ETAs | | |
| | `seller_profiles` | Seller behavior notes | | |
| | `marketing_actions` | Full action catalog plus active flags | | |
| | `available_action` | Agent guide showing the action shape | | |
| | `summary` | Human-readable step summary | | |
| | `reward` | Reward returned for the step | | |
| | `done` | Whether the episode has terminated | | |
| | `metadata` | Reward breakdown and simulation diagnostics | | |
| ### Store snapshot fields | |
| `store` is a `StoreSnapshot` with: | |
| - `day` | |
| - `current_daily_expense_inr` | |
| - `bank_balance_inr` | |
| - `customer_satisfaction` | |
| - `order_processing_capacity` | |
| - `cumulative_profit_inr` | |
| - `active_action_ids` | |
| - `active_recurring_cost_inr` | |
| - `episode_horizon_days` | |
| - `commit_window_days` | |
| - `reward_horizon_days` | |
| ### Available action guide | |
| Each observation includes an `available_action` block with: | |
| - `name`: always `action` | |
| - `description`: short instruction about the bundled step contract | |
| - `pass_action`: empty `StoreAction()` | |
| - `example_action`: a concrete non-empty example | |
| - `notes`: quick reminders about pass behavior, stock-only pricing, and durable | |
| marketing actions | |
| ## Step Lifecycle | |
| One call to `step()` performs a full decision-window update. | |
| ### High-level order of operations | |
| 1. Validate the bundle. | |
| 2. Read the current live state. | |
| 3. Compute starting capital. | |
| 4. Clone the live runtime state into two branches: | |
| - commit branch for the next `3` days of live state | |
| - reward branch for the `6`-day reward forecast | |
| 5. Apply the same action bundle to both branches once at the start of the | |
| window. | |
| 6. Update customer satisfaction once per decision window. | |
| 7. Simulate daily operations on both branches. | |
| 8. Commit only the `3`-day branch back to the live environment state. | |
| 9. Compute reward from the `6`-day branch. | |
| 10. Increment `step_count` and set `done` if the committed state crosses a | |
| terminal condition. | |
| ### What the action does at step start | |
| - Supplier orders are placed immediately and enter the pending queue. | |
| - The store pays the supplier cost immediately when the order is placed. | |
| - Price updates apply immediately to stocked items. | |
| - Marketing actions activate immediately and remain active for the rest of the | |
| episode. | |
| - Customer satisfaction is recomputed once for the decision window after the | |
| bundle is applied. | |
| ### What happens during simulated days | |
| For each simulated day: | |
| - Active recurring marketing cost is charged. | |
| - Demand is converted into sales. | |
| - Sales revenue is added to bank balance. | |
| - Daily operating expense is charged. | |
| - COGS is tracked in cumulative profit accounting. | |
| - Pending orders are decremented by one day. | |
| - Arriving orders are received into stock. | |
| - The day counter advances by one. | |
| ## Simulation Formulas | |
| The formulas below reflect the current implementation in | |
| `store/server/store_environment.py`. | |
| ### Starting capital | |
| ```text | |
| starting_capital = starting_bank_balance + total_inventory_value | |
| ``` | |
| `total_inventory_value` is the sum of `stock_value_inr` across current stock | |
| rows. | |
| ### Effective daily orders | |
| ```text | |
| effective_daily_orders = order_processing_capacity * (customer_satisfaction / 100) | |
| ``` | |
| This means the current satisfaction score is treated as a percentage scale. | |
| ### Product margin | |
| ```text | |
| margin_percent = ((selling_price_inr - buying_price_inr) / selling_price_inr) * 100 | |
| ``` | |
| The implementation uses the current selling price, not the buying price, as the | |
| denominator. | |
| ### Product selling score | |
| ```text | |
| product_selling_score = max(0, demand_score - margin_percent * 3) | |
| ``` | |
| This is computed separately for each stocked item with positive on-hand | |
| quantity. | |
| ### Total selling score | |
| ```text | |
| total_product_selling_score = sum(product_selling_score for each stocked item) | |
| ``` | |
| ### Daily sales allocation | |
| ```text | |
| desired_sales_for_item = effective_daily_orders * product_selling_score / total_product_selling_score | |
| actual_sales = min(quantity_on_hand, desired_sales_for_item) | |
| ``` | |
| Sales are proportional to selling score across stocked items. Because the model | |
| is proportional and not integer-rounded, `quantity_on_hand` can become | |
| fractional after simulated sales. | |
| ### Customer satisfaction update | |
| Before the window is simulated, the environment applies any marketing action | |
| delays to customer satisfaction. Then it recomputes a score-based multiplier. | |
| ```text | |
| adjusted_satisfaction = current_satisfaction + sum(customer_satisfaction_delta for activated actions) | |
| if after_score > 0: | |
| next_satisfaction = (1 + ((after_score - before_score) / after_score)) * adjusted_satisfaction | |
| else: | |
| next_satisfaction = adjusted_satisfaction | |
| next_satisfaction is then clamped to the range [1, 100] | |
| ``` | |
| `before_score` is the total selling score before the bundle is applied. | |
| `after_score` is the total selling score after price updates and before the | |
| simulated days begin. | |
| ### Order arrival | |
| When a pending order reaches `days_remaining <= 0`: | |
| - If the product already exists in stock, the order is merged into that row and | |
| the buying price becomes the weighted average of the existing and incoming | |
| units. | |
| - If the product does not exist in stock, a new stock row is created. | |
| - Newly created stock uses: | |
| ```text | |
| selling_price = max(buying_price + 1, ceil(buying_price * 1.18)) | |
| ``` | |
| The `1.18` markup is the implementation default for a newly introduced SKU. | |
| ### Reward | |
| Reward is computed from the `6`-day reward branch: | |
| ```text | |
| difference = | |
| (ending_inventory_value + ending_bank_balance) | |
| - (starting_inventory_value + starting_bank_balance) | |
| - recurring_costs_paid_over_6_days | |
| normalized_difference = difference / starting_capital | |
| reward = 0.5 + normalized_difference | |
| ``` | |
| If `starting_capital <= 0`, the implementation falls back to | |
| `normalized_difference = 0`. | |
| ### Reward metadata | |
| The step response includes reward diagnostics in `metadata`: | |
| - `reward_breakdown.starting_capital_inr` | |
| - `reward_breakdown.ending_capital_inr` | |
| - `reward_breakdown.difference_inr` | |
| - `reward_breakdown.normalized_difference` | |
| - `reward_breakdown.commit_window_days` | |
| - `reward_breakdown.reward_horizon_days` | |
| - `reward_breakdown.recurring_costs_paid_inr` | |
| - `commit_metrics` | |
| - `reward_metrics` | |
| - `done_reason` | |
| The code rounds the high-level reward breakdown to two decimals, except for | |
| `normalized_difference`, which is kept at six decimal places in metadata. | |
| ## Seller Behavior And ETA Sampling | |
| Supplier ETA is deterministic within an episode because the environment seeds | |
| its RNG from the reset count. Different resets get different seeds, but the same | |
| episode run is repeatable once started. | |
| ### Seller profiles | |
| | Seller | Behavior | ETA sampling | | |
| |---|---|---| | |
| | Seller A | Overcommits | `quoted + {0, 1, 2}` with delay-heavy weights | | |
| | Seller B | Strict | Uses quoted ETA exactly | | |
| | Seller C | Undercommits | `quoted + {-2, -1, 0}` with early-heavy weights | | |
| | Seller D | Variable | `quoted + {-1, 0, 1, 2}` with mixed weights | | |
| ### Actual ETA rules | |
| - The quoted ETA comes from the corresponding seller column in | |
| `market_offers.csv`. | |
| - Actual ETA is sampled once when the order is placed. | |
| - The sampled ETA is floored at `1`. | |
| - Each simulated day decrements `days_remaining` by `1`. | |
| - When `days_remaining <= 0`, the order is received. | |
| ### Exact weights in the current code | |
| - Seller A: offsets `[0, 1, 2]` with weights `[2, 4, 5]` | |
| - Seller B: offset `[0]` | |
| - Seller C: offsets `[-2, -1, 0]` with weights `[5, 4, 2]` | |
| - Seller D: offsets `[-1, 0, 1, 2]` with weights `[2, 4, 3, 2]` | |
| These weights are implementation details, not a business guarantee. | |
| ## Data Contract | |
| ### `store_state.csv` | |
| Current columns: | |
| - `day` | |
| - `current_daily_expense_inr` | |
| - `bank_balance_inr` | |
| - `customer_satisfaction` | |
| - `order_processing_capacity` | |
| - `cumulative_profit_inr` | |
| ### `stock.csv` | |
| Current columns: | |
| - `product_name` | |
| - `unit` | |
| - `buying_price_inr` | |
| - `selling_price_inr` | |
| - `demand_score` | |
| - `quantity_on_hand` | |
| - `stock_value_inr` | |
| The seed data includes 30 stocked products and the stored value must equal | |
| `buying_price_inr * quantity_on_hand` on each row. | |
| ### `market_offers.csv` | |
| Current columns: | |
| - `product_name` | |
| - `category` | |
| - `unit` | |
| - `demand_score` | |
| - `seller_a_price_inr` | |
| - `seller_a_etd_days` | |
| - `seller_b_price_inr` | |
| - `seller_b_etd_days` | |
| - `seller_c_price_inr` | |
| - `seller_c_etd_days` | |
| - `seller_d_price_inr` | |
| - `seller_d_etd_days` | |
| The current catalog contains 100 products. | |
| ### `seller_profiles.csv` | |
| Current columns: | |
| - `seller_name` | |
| - `timeline_behavior` | |
| - `reliability_note` | |
| - `default_etd_days` | |
| ### `pending_orders.csv` | |
| Current columns: | |
| - `order_id` | |
| - `product_name` | |
| - `quantity` | |
| - `seller_name` | |
| - `buy_price_inr` | |
| - `days_remaining` | |
| - `status` | |
| - `created_day` | |
| The seed file starts empty. | |
| ### `marketing_actions.csv` | |
| Current columns: | |
| - `action_id` | |
| - `description` | |
| - `customer_satisfaction_delta` | |
| - `cost_inr` | |
| - `recurring_cost_inr` | |
| - `order_processing_capacity_delta` | |
| The current catalog contains 15 durable action rows. | |
| ## Episode End Conditions | |
| The environment uses a default episode horizon of `30` days, but each step | |
| advances the live state by `3` days, so a full default episode lasts 10 | |
| decision windows. | |
| The episode ends when either: | |
| - committed bank balance drops below `0` | |
| - committed day exceeds `30` | |
| The current implementation checks `day > 30` after the commit branch is | |
| applied, so the tenth decision window takes the day counter to `31` and ends | |
| the episode. | |
| ## Practical Edge Cases | |
| ### Fractional inventory | |
| Sales are proportional, so on-hand inventory can become fractional. The models | |
| use floats for inventory fields for that reason. | |
| ### Empty sales mix | |
| If all stocked items have zero selling score or zero quantity, the sales loop | |
| returns zero revenue and zero COGS for that day. | |
| ### Zero or negative starting capital | |
| The reward math guards against division by zero by falling back to a zero | |
| normalized difference. | |
| ### Already-active marketing actions | |
| Once a marketing action is active, a later step that tries to re-activate it | |
| raises a validation error. | |
| ### Product pricing | |
| Price updates only apply to products already in stock. If a product is not in | |
| stock yet, you must wait for a supplier order to arrive before changing its | |
| price. | |
| ### New stock rows | |
| If a pending order arrives for a product not currently in stock, the environment | |
| creates a new stock row on arrival. This is rare in the seeded scenario, but the | |
| code supports it. | |
| ## How To Run | |
| The project is designed to run through `uv` so the declared dependencies are | |
| available. In this workspace, the global shell does not include `openenv-core` | |
| or `pytest`, so prefer the project environment. | |
| ### Install dependencies | |
| ```bash | |
| cd store | |
| uv sync | |
| ``` | |
| ### Start the server | |
| ```bash | |
| cd store | |
| uv run --project . server --port 8000 | |
| ``` | |
| ### Start the Gradio playground | |
| ```bash | |
| cd store | |
| uv run --project . gradio | |
| ``` | |
| By default the Gradio UI launches on `http://127.0.0.1:7860` and runs the | |
| environment in-process, so you do not need the FastAPI server running to play | |
| with the simulation manually. | |
| ### Validate the OpenEnv package | |
| ```bash | |
| cd store | |
| uv run --project . openenv validate . | |
| ``` | |
| ### Run the tests | |
| ```bash | |
| cd store | |
| uv run --project . python -m pytest | |
| ``` | |
| ## How To Play Manually | |
| You can interact with the simulation directly from Python without starting a | |
| network server: | |
| ```python | |
| from store import OrderRequest, PriceUpdate, StoreAction | |
| from store.server.store_environment import StoreEnvironment | |
| env = StoreEnvironment() | |
| obs = env.reset() | |
| print(obs.store) | |
| print(obs.summary) | |
| step = env.step( | |
| StoreAction( | |
| orders=[OrderRequest(product="Rice", seller="Seller A", quantity=25)], | |
| prices=[PriceUpdate(product="Milk", price=33)], | |
| action_ids=[7], | |
| ) | |
| ) | |
| print(step.reward) | |
| print(step.done) | |
| print(step.metadata["reward_breakdown"]) | |
| print(step.store.day) | |
| ``` | |
| ### Passing a step | |
| ```python | |
| env.step(StoreAction()) | |
| ``` | |
| ### Using the Gradio UI | |
| The Gradio frontend lives in `store/gradio_app.py` and is designed as a local | |
| playground for the same bundled action contract: | |
| - `Orders JSON`: JSON array of `{product, seller, quantity}` objects | |
| - `Price Updates JSON`: JSON array of `{product, price}` objects | |
| - `Action IDs`: either a JSON array like `[7, 12]`, a single integer, or a | |
| comma-separated string like `7, 12` | |
| UI controls: | |
| - `Reset Episode`: creates a fresh `StoreEnvironment` instance and resets it | |
| - `Submit Step`: validates the current inputs and executes one step | |
| - `Pass Step`: executes a real pass action immediately | |
| - `Load Example`: fills the inputs using the observation's bundled example action | |
| The UI displays: | |
| - current store snapshot | |
| - stock table | |
| - pending orders | |
| - market catalog | |
| - marketing action catalog | |
| - reward breakdown | |
| - raw observation metadata | |
| Invalid JSON or action validation errors are shown in the UI while preserving | |
| the current episode state so you can correct the inputs and try again. | |
| ### Inspecting the current state | |
| The most useful fields to inspect after each step are: | |
| - `obs.store` | |
| - `obs.stock` | |
| - `obs.pending_orders` | |
| - `obs.marketing_actions` | |
| - `obs.metadata["reward_breakdown"]` | |
| - `obs.metadata["done_reason"]` | |
| ## OpenEnv Client Usage | |
| If you want to connect to a running server, use `StoreEnv`: | |
| ```python | |
| from store import OrderRequest, PriceUpdate, StoreAction, StoreEnv | |
| with StoreEnv(base_url="http://localhost:8000") as client: | |
| reset_result = client.reset() | |
| print(reset_result.observation.store) | |
| step_result = client.step( | |
| StoreAction( | |
| orders=[OrderRequest(product="Rice", seller="Seller A", quantity=10)], | |
| prices=[PriceUpdate(product="Milk", price=33)], | |
| action_ids=[7], | |
| ) | |
| ) | |
| print(step_result.reward) | |
| print(step_result.observation.summary) | |
| ``` | |
| The client serializes the action bundle to JSON and reconstructs typed | |
| observations from the server response. | |
| ## Tests | |
| The current test suite includes: | |
| - `store/tests/test_seed_csvs.py` | |
| - `store/tests/test_store_environment.py` | |
| These tests cover: | |
| - CSV shape and seed integrity | |
| - opening store values | |
| - market catalog completeness | |
| - marketing action rows | |
| - reset behavior | |
| - 3-day commit semantics | |
| - 6-day reward forecasting | |
| - ETA sampling | |
| - customer satisfaction changes | |
| - durable marketing actions | |
| - episode termination | |
| - client serialization and parsing | |
| ### Useful test command | |
| ```bash | |
| cd store | |
| uv run --project . python -m pytest store/tests/test_seed_csvs.py store/tests/test_store_environment.py | |
| ``` | |
| ## Known Limitations | |
| - The simulation is deterministic aside from seller ETA sampling, which is | |
| seeded per reset. | |
| - Demand is not a full market model; it is based on stock-level selling score | |
| proportions. | |
| - There is no spoilage, shrinkage, theft, or seasonality model. | |
| - There is no supplier cancellation workflow. | |
| - Sales can become fractional because the simulation allocates demand | |
| proportionally. | |
| - The environment exposes the full market and marketing catalogs every step, so | |
| observations are intentionally verbose. | |
| - Reward is unbounded and can be greater than `1.0` or less than `0.0`. | |
| - `plan.md` and `plan2.md` are useful design notes, but the code and CSVs are | |
| the authoritative implementation. | |
| ## Assumptions | |
| - The agent-facing state is the observation payload, not the OpenEnv transport | |
| `State`. | |
| - CSV seed files are runtime truth and are re-read on every reset. | |
| - The bundled action is the only supported step input. | |
| - Marketing actions are durable and stay active once activated. | |
| - A 30-day horizon with 3-day windows is the intended default episode shape. | |
| - Current seed values in `store_state.csv` are intentional and should not be | |
| replaced with the older numbers from the narrative docs unless the code and | |
| tests are updated together. | |
| ## Packaging Notes | |
| The OpenEnv manifest is `store/openenv.yaml`, and the server entry point is | |
| `store.server.app:app` with the script entry point `server = "store.server.app:main"`. | |
| The current package also declares: | |
| - `openenv-core[core]>=0.2.2` | |
| - `pydantic>=2.0.0` | |
| ## Further Reading | |
| - `store/models.py` | |
| - `store/server/store_environment.py` | |
| - `store/tests/test_store_environment.py` | |
| - `store/tests/test_seed_csvs.py` | |