saketh1201 commited on
Commit
d97ff83
·
verified ·
1 Parent(s): 852bf33

Upload folder using huggingface_hub

Browse files
client.py CHANGED
@@ -22,9 +22,6 @@ class InventoryEnv(EnvClient[InventoryAction, InventoryObservation, InventorySta
22
  if action.delivery_method is not None:
23
  payload["delivery_method"] = action.delivery_method
24
 
25
- if action.upgrade_delivery is not None:
26
- payload["upgrade_delivery"] = action.upgrade_delivery
27
-
28
  if action.liquidate is not None:
29
  payload["liquidate"] = action.liquidate
30
 
 
22
  if action.delivery_method is not None:
23
  payload["delivery_method"] = action.delivery_method
24
 
 
 
 
25
  if action.liquidate is not None:
26
  payload["liquidate"] = action.liquidate
27
 
inference.py CHANGED
@@ -1,34 +1,42 @@
1
  """
2
- Inference Script Inventory Optimization Environment
3
- =======================================================
4
  Required env vars:
5
  API_BASE_URL The API endpoint for the LLM.
6
  MODEL_NAME The model identifier to use for inference.
7
- HF_TOKEN Your Hugging Face / API key.
 
 
 
 
8
  """
9
 
10
  import os
11
  import json
12
  import textwrap
13
 
 
 
 
14
  from openai import OpenAI
15
 
16
  from server.inventory_env import InventoryEnvironment
17
- from server.constants import EXTRA_INVENTORY_COST
18
  from models import InventoryAction
19
 
20
- from dotenv import load_dotenv
21
- load_dotenv()
22
-
23
  API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
24
- API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
25
  MODEL_NAME = os.getenv("MODEL_NAME")
26
  MAX_DAYS = 30
27
 
28
  SYSTEM_PROMPT = textwrap.dedent("""
29
- You are an inventory management AI agent. Each day you receive the current state
30
  of a retail store with 5 products: electronics, clothing, groceries, furniture, toys.
31
 
 
 
 
 
32
  Groceries are perishable (5-day shelf life). Other products don't expire.
33
 
34
  Product selling prices: electronics=$150, clothing=$40, groceries=$10, furniture=$200, toys=$25
@@ -37,16 +45,18 @@ You are an inventory management AI agent. Each day you receive the current state
37
  Shipping costs per unit: slow=$2 (5 days), medium=$5 (3 days), fast=$10 (1 day)
38
  Warehouse capacity: electronics=100, clothing=200, groceries=500, furniture=50, toys=300
39
 
40
- Events (like black_friday, christmas) boost demand when their countdown hits 0.
41
  Weekends (day%7 == 5 or 6) have 1.2x demand.
42
 
43
  CRITICAL STRATEGY:
44
- - You MUST restock products when inventory is low. If you don't buy, you run out of
45
- stock and miss sales. Missed sales = lost revenue = negative reward.
46
- - Check today's demand to estimate tomorrow's needs.
47
- - Do NOT overbuy when demand is low - unsold stock ties up cash, warehouse space and perishables expire.
48
  - Prioritize high-margin products: furniture ($70 profit), electronics ($50 profit).
49
- - Stock up BEFORE events hit (check event countdowns).
 
 
50
 
51
  Each day you must respond with a JSON action:
52
  {
@@ -60,11 +70,18 @@ You are an inventory management AI agent. Each day you receive the current state
60
  - liquidate: products and amounts to dispose of (no revenue, empty {} to skip)
61
  Use liquidate to free up warehouse space before a restock.
62
 
63
- You will see what demand occurred today AFTER it happened. Use this to spot trends
64
- and plan restocking. A negative reward means your last action was bad adjust.
 
 
 
 
 
 
 
 
65
 
66
- Do NOT buy more than you can afford. Do NOT buy on the last day.
67
- Respond with ONLY valid JSON, no explanation.
68
  """).strip()
69
 
70
 
@@ -89,8 +106,10 @@ def format_observation(obs):
89
  for event, days in obs.updated_events.items():
90
  if days > 0:
91
  event_lines.append(f" {event}: in {days} days")
92
- else:
93
  event_lines.append(f" {event}: ACTIVE NOW")
 
 
94
  events_text = "\n".join(event_lines) if event_lines else " None"
95
 
96
  # format deliveries
@@ -102,7 +121,7 @@ def format_observation(obs):
102
  delivery_lines.append(f" {product}: {qty} units arriving in {days_away} days")
103
  deliveries_text = "\n".join(delivery_lines) if delivery_lines else " None"
104
 
105
- # format demand (already happened today — feedback, not prediction)
106
  demand_lines = []
107
  for product, units in obs.demand_today.items():
108
  demand_lines.append(f" {product}: {units} units")
@@ -117,7 +136,7 @@ Last Step Reward: {obs.reward:.3f}
117
  Inventory:
118
  {inv_text}
119
 
120
- Demand That Occurred Today:
121
  {demand_text}
122
 
123
  Upcoming Events:
@@ -132,17 +151,42 @@ Respond with your action as JSON."""
132
 
133
 
134
  def parse_action(response_text):
135
- """Parse LLM response into InventoryAction."""
136
  try:
137
  text = response_text.strip()
138
- if text.startswith("```"):
139
- text = text.split("\n", 1)[1]
140
- text = text.rsplit("```", 1)[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  data = json.loads(text)
143
- return InventoryAction(**data)
144
- except Exception:
145
- print(response_text)
 
 
 
 
 
 
 
 
 
 
 
146
  return InventoryAction(
147
  buy_quantities={},
148
  delivery_method="slow",
@@ -150,6 +194,9 @@ def parse_action(response_text):
150
  )
151
 
152
 
 
 
 
153
  def run_task(client, task_name):
154
  """Run a single task and return total profit."""
155
  env = InventoryEnvironment(task_name)
@@ -159,6 +206,9 @@ def run_task(client, task_name):
159
  print(f"Task: {task_name.upper()} | Cash: ${obs.total_cash:.2f} | Days: {env.max_days}")
160
  print(f"{'=' * 50}")
161
 
 
 
 
162
  for day in range(1, env.max_days + 1):
163
  if obs.done:
164
  print("Episode ended early.")
@@ -166,16 +216,32 @@ def run_task(client, task_name):
166
 
167
  user_prompt = format_observation(obs)
168
 
169
- messages = [
170
- {"role": "system", "content": SYSTEM_PROMPT},
171
- {"role": "user", "content": user_prompt},
172
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  try:
175
  completion = client.chat.completions.create(
176
  model=MODEL_NAME,
177
  messages=messages,
178
- # temperature=0.2,
179
  max_completion_tokens=300,
180
  stream=False,
181
  )
@@ -184,6 +250,9 @@ def run_task(client, task_name):
184
  print(f" LLM request failed: {exc}. Skipping turn.")
185
  response_text = "{}"
186
 
 
 
 
187
  action = parse_action(response_text)
188
 
189
  print(f"Day {day}: buy={action.buy_quantities} delivery={action.delivery_method} liquidate={action.liquidate}")
@@ -199,6 +268,9 @@ def run_task(client, task_name):
199
  def main():
200
  from server.grader import grade_all, compute_baselines
201
 
 
 
 
202
  client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
203
 
204
  # print baselines first
@@ -225,4 +297,4 @@ def main():
225
 
226
 
227
  if __name__ == "__main__":
228
- main()
 
1
  """
2
+ Inference Script - Inventory Optimization Environment
3
+ =====================================================
4
  Required env vars:
5
  API_BASE_URL The API endpoint for the LLM.
6
  MODEL_NAME The model identifier to use for inference.
7
+ HF_TOKEN Hugging Face token (preferred for HF Router).
8
+
9
+ Supported key env vars (first non-empty wins): HF_TOKEN, API_KEY, OPENAI_API_KEY.
10
+ For non-OpenAI endpoints, a dummy key is used when no key is provided because
11
+ the OpenAI Python SDK requires a non-empty api_key argument.
12
  """
13
 
14
  import os
15
  import json
16
  import textwrap
17
 
18
+ from dotenv import load_dotenv
19
+ load_dotenv()
20
+
21
  from openai import OpenAI
22
 
23
  from server.inventory_env import InventoryEnvironment
24
+ from server.constants import EXTRA_INVENTORY_COST, EVENT_DURATION
25
  from models import InventoryAction
26
 
 
 
 
27
  API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
28
+ API_KEY = os.getenv("API_KEY") or os.getenv("HF_TOKEN")
29
  MODEL_NAME = os.getenv("MODEL_NAME")
30
  MAX_DAYS = 30
31
 
32
  SYSTEM_PROMPT = textwrap.dedent("""
33
+ You are an inventory management AI agent. Each day you receive the current state
34
  of a retail store with 5 products: electronics, clothing, groceries, furniture, toys.
35
 
36
+ You will be shown your decision history from recent days so you can learn from
37
+ past outcomes. Use this history to spot demand trends, identify what worked vs.
38
+ what didn't, and adjust your strategy accordingly.
39
+
40
  Groceries are perishable (5-day shelf life). Other products don't expire.
41
 
42
  Product selling prices: electronics=$150, clothing=$40, groceries=$10, furniture=$200, toys=$25
 
45
  Shipping costs per unit: slow=$2 (5 days), medium=$5 (3 days), fast=$10 (1 day)
46
  Warehouse capacity: electronics=100, clothing=200, groceries=500, furniture=50, toys=300
47
 
48
+ Events (like black_friday, christmas) boost demand when their countdown hits 0 and last for 2 days.
49
  Weekends (day%7 == 5 or 6) have 1.2x demand.
50
 
51
  CRITICAL STRATEGY:
52
+ - Review your history: if reward was negative, identify why and change approach.
53
+ - Track demand trends across days if a product's demand is rising, stock up early.
54
+ - You MUST restock products when inventory is low. Missed sales = lost revenue = negative reward.
55
+ - Do NOT overbuy when demand is low unsold stock ties up cash and perishables expire.
56
  - Prioritize high-margin products: furniture ($70 profit), electronics ($50 profit).
57
+ - Stock up BEFORE events hit (check event countdowns — order 3-5 days ahead using slow/medium shipping).
58
+ - When no events are approaching, slow shipping is often sufficient and saves significant cost.
59
+ - Near end of episode (last 2 days), stop buying — focus on selling remaining stock.
60
 
61
  Each day you must respond with a JSON action:
62
  {
 
70
  - liquidate: products and amounts to dispose of (no revenue, empty {} to skip)
71
  Use liquidate to free up warehouse space before a restock.
72
 
73
+ LEARNING FROM HISTORY:
74
+ - Compare your past buy quantities to the demand that followedwere you over or under?
75
+ - If you see repeated stockouts for a product, increase orders for it.
76
+ - If groceries expired, you overbought — reduce grocery orders or use faster shipping.
77
+ - A negative reward means your last action was bad — adjust immediately.
78
+
79
+ Before responding with JSON, briefly reason (2-3 lines max):
80
+ 1. What did I learn from recent history? What went wrong/right?
81
+ 2. What products need restocking vs. are overstocked?
82
+ 3. Are any events approaching?
83
 
84
+ Then output ONLY the final JSON action on the last line.
 
85
  """).strip()
86
 
87
 
 
106
  for event, days in obs.updated_events.items():
107
  if days > 0:
108
  event_lines.append(f" {event}: in {days} days")
109
+ elif -EVENT_DURATION < days <= 0:
110
  event_lines.append(f" {event}: ACTIVE NOW")
111
+ else:
112
+ event_lines.append(f" {event}: ended")
113
  events_text = "\n".join(event_lines) if event_lines else " None"
114
 
115
  # format deliveries
 
121
  delivery_lines.append(f" {product}: {qty} units arriving in {days_away} days")
122
  deliveries_text = "\n".join(delivery_lines) if delivery_lines else " None"
123
 
124
+ # format demand (yesterday's demand — feedback, not prediction)
125
  demand_lines = []
126
  for product, units in obs.demand_today.items():
127
  demand_lines.append(f" {product}: {units} units")
 
136
  Inventory:
137
  {inv_text}
138
 
139
+ Yesterday's Demand:
140
  {demand_text}
141
 
142
  Upcoming Events:
 
151
 
152
 
153
  def parse_action(response_text):
154
+ """Parse LLM response into InventoryAction. Extracts JSON even if surrounded by text."""
155
  try:
156
  text = response_text.strip()
157
+
158
+ # strip markdown code fences
159
+ if "```" in text:
160
+ parts = text.split("```")
161
+ for part in parts:
162
+ part = part.strip()
163
+ if part.startswith("json"):
164
+ part = part[4:].strip()
165
+ if part.startswith("{"):
166
+ text = part
167
+ break
168
+
169
+ # find the first { and last } to extract JSON
170
+ start = text.find("{")
171
+ end = text.rfind("}")
172
+ if start != -1 and end != -1 and end > start:
173
+ text = text[start:end + 1]
174
 
175
  data = json.loads(text)
176
+
177
+ # only keep valid fields
178
+ clean = {}
179
+ if "buy_quantities" in data:
180
+ clean["buy_quantities"] = data["buy_quantities"]
181
+ if "delivery_method" in data:
182
+ clean["delivery_method"] = data["delivery_method"]
183
+ if "liquidate" in data:
184
+ clean["liquidate"] = data["liquidate"]
185
+
186
+ return InventoryAction(**clean)
187
+ except Exception as e:
188
+ print(f" [DEBUG] Parse FAILED: {e}")
189
+ print(f" [DEBUG] Raw LLM response: {response_text[:500]}")
190
  return InventoryAction(
191
  buy_quantities={},
192
  delivery_method="slow",
 
194
  )
195
 
196
 
197
+ HISTORY_WINDOW = 15 # rolling window of past days to include in context
198
+
199
+
200
  def run_task(client, task_name):
201
  """Run a single task and return total profit."""
202
  env = InventoryEnvironment(task_name)
 
206
  print(f"Task: {task_name.upper()} | Cash: ${obs.total_cash:.2f} | Days: {env.max_days}")
207
  print(f"{'=' * 50}")
208
 
209
+ # Rolling history of (user_observation, assistant_response) pairs
210
+ history = []
211
+
212
  for day in range(1, env.max_days + 1):
213
  if obs.done:
214
  print("Episode ended early.")
 
216
 
217
  user_prompt = format_observation(obs)
218
 
219
+ # Build messages: system + history context + current observation
220
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
221
+
222
+ recent = history[-HISTORY_WINDOW:]
223
+ if recent:
224
+ # Tell the LLM it's about to see its past decisions and their outcomes
225
+ messages.append({
226
+ "role": "user",
227
+ "content": f"Here is your decision history from the last {len(recent)} day(s). "
228
+ "Use this to identify demand trends, adjust restocking, and avoid repeating mistakes.",
229
+ })
230
+ messages.append({
231
+ "role": "assistant",
232
+ "content": "Understood. I'll review my past decisions and their outcomes to make better choices today.",
233
+ })
234
+ for past_user, past_assistant in recent:
235
+ messages.append({"role": "user", "content": past_user})
236
+ messages.append({"role": "assistant", "content": past_assistant})
237
+
238
+ messages.append({"role": "user", "content": user_prompt})
239
 
240
  try:
241
  completion = client.chat.completions.create(
242
  model=MODEL_NAME,
243
  messages=messages,
244
+ temperature=0.0,
245
  max_completion_tokens=300,
246
  stream=False,
247
  )
 
250
  print(f" LLM request failed: {exc}. Skipping turn.")
251
  response_text = "{}"
252
 
253
+ # Save this turn to rolling history
254
+ history.append((user_prompt, response_text))
255
+
256
  action = parse_action(response_text)
257
 
258
  print(f"Day {day}: buy={action.buy_quantities} delivery={action.delivery_method} liquidate={action.liquidate}")
 
268
  def main():
269
  from server.grader import grade_all, compute_baselines
270
 
271
+ if not MODEL_NAME:
272
+ raise RuntimeError("MODEL_NAME is not set. Please export MODEL_NAME before running inference.")
273
+
274
  client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
275
 
276
  # print baselines first
 
297
 
298
 
299
  if __name__ == "__main__":
300
+ main()
models.py CHANGED
@@ -3,12 +3,20 @@ from __future__ import annotations
3
  from openenv.core.env_server import Action, Observation, State
4
  from typing import Literal, Dict, List, Optional
5
 
 
6
 
7
  class InventoryAction(Action):
8
  buy_quantities : Dict[str, int] = {}
9
- delivery_method : Literal["slow", "medium", "fast"] = "slow"
10
  liquidate : Dict[str, int] = {}
11
 
 
 
 
 
 
 
 
12
 
13
  class InventoryObservation(Observation):
14
  current_day : int
 
3
  from openenv.core.env_server import Action, Observation, State
4
  from typing import Literal, Dict, List, Optional
5
 
6
+ from pydantic import field_validator
7
 
8
  class InventoryAction(Action):
9
  buy_quantities : Dict[str, int] = {}
10
+ delivery_method : Literal["slow", "medium", "fast"] = "slow"
11
  liquidate : Dict[str, int] = {}
12
 
13
+ @field_validator("buy_quantities", "liquidate", mode="before")
14
+ @classmethod
15
+ def parse_dict_strings(cls, v):
16
+ if isinstance(v, str):
17
+ return json.loads(v)
18
+ return v
19
+
20
 
21
  class InventoryObservation(Observation):
22
  current_day : int
pyproject.toml CHANGED
@@ -15,4 +15,7 @@ dependencies = [
15
 
16
  [build-system]
17
  requires = ["setuptools>=61.0"]
18
- build-backend = "setuptools.build_meta"
 
 
 
 
15
 
16
  [build-system]
17
  requires = ["setuptools>=61.0"]
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [project.scripts]
21
+ server = "server.app:main"
scripts/validate-submission.sh ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # validate-submission.sh — OpenEnv Submission Validator
4
+ #
5
+ # Checks that your HF Space is live, Docker image builds, and openenv validate passes.
6
+ #
7
+ # Run:
8
+ # ./scripts/validate-submission.sh <ping_url> [repo_dir]
9
+ #
10
+ # Arguments:
11
+ # ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)
12
+ # repo_dir Path to your repo (default: current directory)
13
+ #
14
+
15
+ set -uo pipefail
16
+
17
+ DOCKER_BUILD_TIMEOUT=600
18
+ if [ -t 1 ]; then
19
+ RED='\033[0;31m'
20
+ GREEN='\033[0;32m'
21
+ YELLOW='\033[1;33m'
22
+ BOLD='\033[1m'
23
+ NC='\033[0m'
24
+ else
25
+ RED='' GREEN='' YELLOW='' BOLD='' NC=''
26
+ fi
27
+
28
+ run_with_timeout() {
29
+ local secs="$1"; shift
30
+ if command -v timeout &>/dev/null; then
31
+ timeout "$secs" "$@"
32
+ elif command -v gtimeout &>/dev/null; then
33
+ gtimeout "$secs" "$@"
34
+ else
35
+ "$@" &
36
+ local pid=$!
37
+ ( sleep "$secs" && kill "$pid" 2>/dev/null ) &
38
+ local watcher=$!
39
+ wait "$pid" 2>/dev/null
40
+ local rc=$?
41
+ kill "$watcher" 2>/dev/null
42
+ wait "$watcher" 2>/dev/null
43
+ return $rc
44
+ fi
45
+ }
46
+
47
+ portable_mktemp() {
48
+ local prefix="${1:-validate}"
49
+ mktemp "${TMPDIR:-/tmp}/${prefix}-XXXXXX" 2>/dev/null || mktemp
50
+ }
51
+
52
+ CLEANUP_FILES=()
53
+ cleanup() { rm -f "${CLEANUP_FILES[@]+"${CLEANUP_FILES[@]}"}"; }
54
+ trap cleanup EXIT
55
+
56
+ PING_URL="${1:-}"
57
+ REPO_DIR="${2:-.}"
58
+
59
+ if [ -z "$PING_URL" ]; then
60
+ printf "Usage: %s <ping_url> [repo_dir]\n" "$0"
61
+ printf "\n"
62
+ printf " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)\n"
63
+ printf " repo_dir Path to your repo (default: current directory)\n"
64
+ exit 1
65
+ fi
66
+
67
+ if ! REPO_DIR="$(cd "$REPO_DIR" 2>/dev/null && pwd)"; then
68
+ printf "Error: directory '%s' not found\n" "${2:-.}"
69
+ exit 1
70
+ fi
71
+ PING_URL="${PING_URL%/}"
72
+ export PING_URL
73
+ PASS=0
74
+
75
+ log() { printf "[%s] %b\n" "$(date -u +%H:%M:%S)" "$*"; }
76
+ pass() { log "${GREEN}PASSED${NC} -- $1"; PASS=$((PASS + 1)); }
77
+ fail() { log "${RED}FAILED${NC} -- $1"; }
78
+ hint() { printf " ${YELLOW}Hint:${NC} %b\n" "$1"; }
79
+ stop_at() {
80
+ printf "\n"
81
+ printf "${RED}${BOLD}Validation stopped at %s.${NC} Fix the above before continuing.\n" "$1"
82
+ exit 1
83
+ }
84
+
85
+ printf "\n"
86
+ printf "${BOLD}========================================${NC}\n"
87
+ printf "${BOLD} OpenEnv Submission Validator${NC}\n"
88
+ printf "${BOLD}========================================${NC}\n"
89
+ log "Repo: $REPO_DIR"
90
+ log "Ping URL: $PING_URL"
91
+ printf "\n"
92
+
93
+ log "${BOLD}Step 1/3: Pinging HF Space${NC} ($PING_URL/reset) ..."
94
+
95
+ CURL_OUTPUT=$(portable_mktemp "validate-curl")
96
+ CLEANUP_FILES+=("$CURL_OUTPUT")
97
+ HTTP_CODE=$(curl -s -o "$CURL_OUTPUT" -w "%{http_code}" -X POST \
98
+ -H "Content-Type: application/json" -d '{}' \
99
+ "$PING_URL/reset" --max-time 30 2>"$CURL_OUTPUT" || printf "000")
100
+
101
+ if [ "$HTTP_CODE" = "200" ]; then
102
+ pass "HF Space is live and responds to /reset"
103
+ elif [ "$HTTP_CODE" = "000" ]; then
104
+ fail "HF Space not reachable (connection failed or timed out)"
105
+ hint "Check your network connection and that the Space is running."
106
+ hint "Try: curl -s -o /dev/null -w '%%{http_code}' -X POST $PING_URL/reset"
107
+ stop_at "Step 1"
108
+ else
109
+ fail "HF Space /reset returned HTTP $HTTP_CODE (expected 200)"
110
+ hint "Make sure your Space is running and the URL is correct."
111
+ hint "Try opening $PING_URL in your browser first."
112
+ stop_at "Step 1"
113
+ fi
114
+
115
+ log "${BOLD}Step 2/3: Running docker build${NC} ..."
116
+
117
+ if ! command -v docker &>/dev/null; then
118
+ fail "docker command not found"
119
+ hint "Install Docker: https://docs.docker.com/get-docker/"
120
+ stop_at "Step 2"
121
+ fi
122
+
123
+ if [ -f "$REPO_DIR/Dockerfile" ]; then
124
+ DOCKER_CONTEXT="$REPO_DIR"
125
+ elif [ -f "$REPO_DIR/server/Dockerfile" ]; then
126
+ DOCKER_CONTEXT="$REPO_DIR/server"
127
+ else
128
+ fail "No Dockerfile found in repo root or server/ directory"
129
+ stop_at "Step 2"
130
+ fi
131
+
132
+ log " Found Dockerfile in $DOCKER_CONTEXT"
133
+
134
+ BUILD_OK=false
135
+ BUILD_OUTPUT=$(run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$DOCKER_CONTEXT" 2>&1) && BUILD_OK=true
136
+
137
+ if [ "$BUILD_OK" = true ]; then
138
+ pass "Docker build succeeded"
139
+ else
140
+ fail "Docker build failed (timeout=${DOCKER_BUILD_TIMEOUT}s)"
141
+ printf "%s\n" "$BUILD_OUTPUT" | tail -20
142
+ stop_at "Step 2"
143
+ fi
144
+
145
+ log "${BOLD}Step 3/3: Running openenv validate${NC} ..."
146
+
147
+ if ! command -v openenv &>/dev/null; then
148
+ fail "openenv command not found"
149
+ hint "Install it: pip install openenv-core"
150
+ stop_at "Step 3"
151
+ fi
152
+
153
+ VALIDATE_OK=false
154
+ VALIDATE_OUTPUT=$(cd "$REPO_DIR" && openenv validate 2>&1) && VALIDATE_OK=true
155
+
156
+ if [ "$VALIDATE_OK" = true ]; then
157
+ pass "openenv validate passed"
158
+ [ -n "$VALIDATE_OUTPUT" ] && log " $VALIDATE_OUTPUT"
159
+ else
160
+ fail "openenv validate failed"
161
+ printf "%s\n" "$VALIDATE_OUTPUT"
162
+ stop_at "Step 3"
163
+ fi
164
+
165
+ printf "\n"
166
+ printf "${BOLD}========================================${NC}\n"
167
+ printf "${GREEN}${BOLD} All 3/3 checks passed!${NC}\n"
168
+ printf "${GREEN}${BOLD} Your submission is ready to submit.${NC}\n"
169
+ printf "${BOLD}========================================${NC}\n"
170
+ printf "\n"
171
+
172
+ exit 0
server/grader.py CHANGED
@@ -32,47 +32,85 @@ def _run_heuristic(task_name):
32
  env = InventoryEnvironment(task_name)
33
  obs = env.reset()
34
 
 
 
 
35
  while not obs.done:
36
  buy = {}
37
- delivery = "medium"
38
  liquidate = {}
39
 
40
- # check if any event is imminent (within 3 days)
41
- event_soon = False
42
  for event, days in obs.updated_events.items():
43
- if 0 < days <= 3:
44
- event_soon = True
45
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  for product, (lo, hi) in task["base_demand"].items():
48
  avg_demand = (lo + hi) // 2
 
 
 
 
 
 
49
  current = sum(b[0] for b in obs.updated_inventory.get(product, []))
50
 
51
- if event_soon:
52
- # stock up 5 days' worth before events, use fast shipping
53
- target = avg_demand * 5
54
- delivery = "fast"
 
 
 
 
 
 
 
 
55
  else:
56
- # normal: keep 3 days' buffer
57
- target = avg_demand * 3
 
 
 
 
58
 
59
- if current < target:
60
- buy[product] = target - current
61
 
62
  # liquidate groceries about to expire (1 day left)
63
  for batch in obs.updated_inventory.get("groceries", []):
64
  if batch[1] is not None and batch[1] <= 1:
65
  liquidate["groceries"] = liquidate.get("groceries", 0) + batch[0]
66
 
67
- # don't buy on last 2 days
68
- if obs.current_day >= task["max_days"] - 2:
 
 
 
 
 
69
  buy = {}
70
 
71
  # don't buy more than cash allows (rough check)
72
  total_cost = sum(qty * (COST_PRICES[p] + SHIPPING_COST[delivery]) for p, qty in buy.items())
73
- if total_cost > obs.total_cash * 0.8:
74
- # scale down proportionally
75
- scale = (obs.total_cash * 0.8) / total_cost if total_cost > 0 else 0
76
  buy = {p: max(1, int(qty * scale)) for p, qty in buy.items()}
77
 
78
  action = InventoryAction(
 
32
  env = InventoryEnvironment(task_name)
33
  obs = env.reset()
34
 
35
+ # track recent demand to adapt ordering
36
+ demand_history = {}
37
+
38
  while not obs.done:
39
  buy = {}
 
40
  liquidate = {}
41
 
42
+ # determine nearest event distance
43
+ nearest_event_days = 999
44
  for event, days in obs.updated_events.items():
45
+ if 0 < days < nearest_event_days:
46
+ nearest_event_days = days
47
+
48
+ # pick shipping based on urgency
49
+ if nearest_event_days <= 2:
50
+ delivery = "fast"
51
+ elif nearest_event_days <= 5:
52
+ delivery = "medium"
53
+ else:
54
+ delivery = "slow"
55
+
56
+ # update demand history from observation
57
+ if obs.demand_today:
58
+ for product, units in obs.demand_today.items():
59
+ if product not in demand_history:
60
+ demand_history[product] = []
61
+ demand_history[product].append(units)
62
 
63
  for product, (lo, hi) in task["base_demand"].items():
64
  avg_demand = (lo + hi) // 2
65
+
66
+ # use recent demand if available (last 5 days)
67
+ if product in demand_history and len(demand_history[product]) >= 2:
68
+ recent = demand_history[product][-5:]
69
+ avg_demand = max(avg_demand, int(sum(recent) / len(recent)))
70
+
71
  current = sum(b[0] for b in obs.updated_inventory.get(product, []))
72
 
73
+ # count in-transit units
74
+ in_transit = 0
75
+ for d in obs.updated_deliveries:
76
+ for p, shipment in d.items():
77
+ if p == product:
78
+ in_transit += shipment[0]
79
+
80
+ available = current + in_transit
81
+
82
+ # how many days of stock to target
83
+ if nearest_event_days <= 5:
84
+ target = avg_demand * 6
85
  else:
86
+ target = avg_demand * 4
87
+
88
+ # prioritize high-margin products — order more aggressively
89
+ margin = BASE_PRICES[product] - COST_PRICES[product]
90
+ if margin >= 50: # electronics, furniture
91
+ target = int(target * 1.3)
92
 
93
+ if available < target:
94
+ buy[product] = target - available
95
 
96
  # liquidate groceries about to expire (1 day left)
97
  for batch in obs.updated_inventory.get("groceries", []):
98
  if batch[1] is not None and batch[1] <= 1:
99
  liquidate["groceries"] = liquidate.get("groceries", 0) + batch[0]
100
 
101
+ # stop buying when deliveries can't arrive in time
102
+ days_left = task["max_days"] - obs.current_day
103
+ if delivery == "slow" and days_left <= 5:
104
+ buy = {}
105
+ elif delivery == "medium" and days_left <= 3:
106
+ buy = {}
107
+ elif delivery == "fast" and days_left <= 1:
108
  buy = {}
109
 
110
  # don't buy more than cash allows (rough check)
111
  total_cost = sum(qty * (COST_PRICES[p] + SHIPPING_COST[delivery]) for p, qty in buy.items())
112
+ if total_cost > obs.total_cash * 0.85:
113
+ scale = (obs.total_cash * 0.85) / total_cost if total_cost > 0 else 0
 
114
  buy = {p: max(1, int(qty * scale)) for p, qty in buy.items()}
115
 
116
  action = InventoryAction(
server/inventory_env.py CHANGED
@@ -79,10 +79,9 @@ class InventoryEnvironment(Environment):
79
  day_cost = 0.0
80
  day_revenue = 0.0
81
 
82
- # 1. tick event countdowns
83
  for event_name in self.events:
84
- if self.events[event_name] > 0:
85
- self.events[event_name] -= 1
86
 
87
  # 2. remove expired groceries
88
  new_batches = []
@@ -232,9 +231,9 @@ class InventoryEnvironment(Environment):
232
  for product in demand:
233
  demand[product] = int(demand[product] * WEEKEND_MULTIPLIER)
234
 
235
- # active event multipliers
236
  for event_name, days in self.events.items():
237
- if days <= 0 and event_name in EVENT_EFFECTS:
238
  for product, mult in EVENT_EFFECTS[event_name].items():
239
  demand[product] = int(demand[product] * mult)
240
 
 
79
  day_cost = 0.0
80
  day_revenue = 0.0
81
 
82
+ # 1. tick event countdowns (keep ticking into negative to track active duration)
83
  for event_name in self.events:
84
+ self.events[event_name] -= 1
 
85
 
86
  # 2. remove expired groceries
87
  new_batches = []
 
231
  for product in demand:
232
  demand[product] = int(demand[product] * WEEKEND_MULTIPLIER)
233
 
234
+ # active event multipliers (only for EVENT_DURATION days after triggering)
235
  for event_name, days in self.events.items():
236
+ if -EVENT_DURATION < days <= 0 and event_name in EVENT_EFFECTS:
237
  for product, mult in EVENT_EFFECTS[event_name].items():
238
  demand[product] = int(demand[product] * mult)
239
 
uv.lock ADDED
The diff for this file is too large to render. See raw diff