Code-Father commited on
Commit
7a2fcb3
·
1 Parent(s): b7a5bab

Infrance script fixed

Browse files
inference.py ADDED
@@ -0,0 +1,618 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference script for OpenEnv Store environment.
3
+
4
+ Uses an LLM (via OpenAI-compatible client) to play the kirana-store simulation.
5
+ Emits structured [START], [STEP], [END] logs for evaluation scoring.
6
+
7
+ Required env vars:
8
+ API_BASE_URL – LLM API endpoint
9
+ MODEL_NAME – model identifier
10
+ HF_TOKEN – API key
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ import os
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from dotenv import load_dotenv
24
+
25
+ load_dotenv(Path(__file__).resolve().parent / ".env")
26
+
27
+ from openai import AsyncOpenAI
28
+
29
+ from client import StoreEnv
30
+ from models import (
31
+ OrderRequest,
32
+ PriceUpdate,
33
+ StoreAction,
34
+ StoreObservation,
35
+ dump_model,
36
+ )
37
+
38
+ API_BASE_URL = os.environ["API_BASE_URL"]
39
+ MODEL_NAME = os.environ["MODEL_NAME"]
40
+ HF_TOKEN = os.environ["HF_TOKEN"]
41
+
42
+ BENCHMARK = "store"
43
+ MAX_STEPS = 10
44
+ MAX_TOTAL_REWARD = MAX_STEPS * 1.0
45
+ SUCCESS_SCORE_THRESHOLD = 0.5
46
+
47
+ TASKS: list[dict[str, Any]] = [
48
+ {
49
+ "name": "margin_aware_pricing",
50
+ "description": (
51
+ "Optimize selling prices using the ground-truth selling score formula: "
52
+ "selling_score = max(0, demand_score - margin_pct * 3). "
53
+ "The agent must identify which thin-margin SKUs (Milk, Bread, Eggs, "
54
+ "Bottled Water) are anchor items driving volume through high selling scores, "
55
+ "and resist the naive urge to raise every low-margin price. Meanwhile, "
56
+ "it should find genuinely over-margined or low-demand products where a "
57
+ "price cut boosts volume, or under-margined items where a raise barely "
58
+ "hurts score. Sugar is the key trap: thin margin but strong anchor role."
59
+ ),
60
+ "max_steps": MAX_STEPS,
61
+ "success_threshold": 0.5,
62
+ },
63
+ {
64
+ "name": "supplier_risk_arbitrage",
65
+ "description": (
66
+ "Navigate the seller reliability spectrum to restock fast-movers cheaply "
67
+ "without incurring late-delivery stockouts. Seller A offers Rice at 138, "
68
+ "Oil at 156, Spices at 345 — far below Seller B — but overcommits and "
69
+ "frequently delivers 1-2 days late. Seller C quotes conservatively on "
70
+ "Bread (31), Eggs (64), Curd (33), Vegetables (38) and often arrives "
71
+ "early. The agent must weigh price savings against delivery risk, use "
72
+ "Seller B as a reliable baseline, and avoid staking urgent restocks on "
73
+ "Seller A when stock cover is already thin (Biscuits at 40, Water at 48)."
74
+ ),
75
+ "max_steps": MAX_STEPS,
76
+ "success_threshold": 0.5,
77
+ },
78
+ {
79
+ "name": "inventory_and_capital_discipline",
80
+ "description": (
81
+ "Maintain service levels on high-demand fast-movers while avoiding "
82
+ "capital sinks. Starting cash is only ₹100k against ~₹240k inventory. "
83
+ "Kitchen Appliances (demand 28, buy 1800, qty 4 = ₹7200 locked) is "
84
+ "dead stock the agent should stop restocking. Biscuits (demand 90, qty 40) "
85
+ "and Bottled Water (demand 89, qty 48) are replenishment misses that need "
86
+ "immediate reorder. The agent must balance reorder urgency against cash "
87
+ "preservation — daily expenses are ₹3000, and bank balance going negative "
88
+ "ends the episode. Reward = 0.5 + (capital_change - recurring_costs) / "
89
+ "starting_capital, so wasted capital directly reduces the score."
90
+ ),
91
+ "max_steps": MAX_STEPS,
92
+ "success_threshold": 0.5,
93
+ },
94
+ {
95
+ "name": "marketing_roi_evaluation",
96
+ "description": (
97
+ "Select durable marketing actions that maximize order-processing capacity "
98
+ "and satisfaction per rupee of recurring cost. Cheap, effective picks: "
99
+ "Shelf placement (ID 1: ₹800 one-time, 0 recurring, +20 capacity), "
100
+ "WhatsApp ordering (ID 4: ₹1500, ₹100/day, +15 capacity), Staff training "
101
+ "(ID 10: ₹2500, 0 recurring, +25 capacity). Mid-game path: Local delivery "
102
+ "(ID 7), Website (ID 12), Rider (ID 13), Stockroom (ID 14). The trap is "
103
+ "Action 15 (social ads): ₹4000 upfront, ₹1200/day recurring, +0 capacity "
104
+ "— a weak-ROI money drain. The aspirational Action 11 (second branch: "
105
+ "₹750k) is unreachable on day-1 capital. The agent should evaluate total "
106
+ "cost of ownership, not just headline satisfaction boosts."
107
+ ),
108
+ "max_steps": MAX_STEPS,
109
+ "success_threshold": 0.45,
110
+ },
111
+ {
112
+ "name": "full_store_management",
113
+ "description": (
114
+ "Holistic 30-day store management combining all dimensions: pricing using "
115
+ "the selling-score formula, supplier selection balancing cost vs reliability, "
116
+ "inventory discipline on fast-movers and dead stock, and phased marketing "
117
+ "investments. The agent must sequence decisions across 10 three-day windows, "
118
+ "adapt to delivery arrivals and changing stock levels, and avoid both "
119
+ "bankruptcy (bank < 0) and stagnation (no growth). This is the integrative "
120
+ "test: partial credit for getting any single dimension right, full credit "
121
+ "for coherent multi-objective reasoning across the entire episode."
122
+ ),
123
+ "max_steps": MAX_STEPS,
124
+ "success_threshold": 0.5,
125
+ },
126
+ ]
127
+
128
+ ENV_URL = os.environ.get("ENV_URL", "http://localhost:8000")
129
+
130
+
131
+ def log_start(*, task: str, env: str, model: str) -> None:
132
+ print(
133
+ json.dumps(
134
+ {"type": "START", "task": task, "env": env, "model": model},
135
+ separators=(",", ":"),
136
+ ),
137
+ flush=True,
138
+ )
139
+
140
+
141
+ def log_step(
142
+ *,
143
+ step: int,
144
+ action: dict[str, Any] | str,
145
+ reward: float,
146
+ done: bool,
147
+ error: str | None,
148
+ ) -> None:
149
+ print(
150
+ json.dumps(
151
+ {
152
+ "type": "STEP",
153
+ "step": step,
154
+ "action": action,
155
+ "reward": reward,
156
+ "done": done,
157
+ "error": error,
158
+ },
159
+ separators=(",", ":"),
160
+ ),
161
+ flush=True,
162
+ )
163
+
164
+
165
+ def log_end(
166
+ *,
167
+ success: bool,
168
+ steps: int,
169
+ score: float,
170
+ rewards: list[float],
171
+ ) -> None:
172
+ print(
173
+ json.dumps(
174
+ {
175
+ "type": "END",
176
+ "success": success,
177
+ "steps": steps,
178
+ "score": score,
179
+ "rewards": rewards,
180
+ },
181
+ separators=(",", ":"),
182
+ ),
183
+ flush=True,
184
+ )
185
+
186
+
187
+ def _format_observation(obs: StoreObservation) -> str:
188
+ """Build a concise text summary of the observation for the LLM prompt."""
189
+ store = obs.store
190
+ lines = [
191
+ "=== Store Snapshot ===",
192
+ f"Day: {store.day}/{store.episode_horizon_days}",
193
+ f"Bank Balance: ₹{store.bank_balance_inr:,.2f}",
194
+ f"Customer Satisfaction: {store.customer_satisfaction}",
195
+ f"Order Processing Capacity: {store.order_processing_capacity}",
196
+ f"Daily Expense: ₹{store.current_daily_expense_inr:,.2f}",
197
+ f"Cumulative Profit: ₹{store.cumulative_profit_inr:,.2f}",
198
+ f"Active Recurring Cost: ₹{store.active_recurring_cost_inr:,.2f}",
199
+ f"Active Action IDs: {store.active_action_ids}",
200
+ "",
201
+ "=== Current Stock (top items by value) ===",
202
+ ]
203
+ sorted_stock = sorted(obs.stock, key=lambda s: s.stock_value_inr, reverse=True)
204
+ for item in sorted_stock[:15]:
205
+ margin = (
206
+ ((item.selling_price_inr - item.buying_price_inr) / item.selling_price_inr) * 100
207
+ if item.selling_price_inr > 0
208
+ else 0
209
+ )
210
+ lines.append(
211
+ f" {item.product_name}: qty={item.quantity_on_hand:.1f}, "
212
+ f"buy=₹{item.buying_price_inr}, sell=₹{item.selling_price_inr}, "
213
+ f"demand={item.demand_score}, margin={margin:.1f}%, "
214
+ f"value=₹{item.stock_value_inr:.0f}"
215
+ )
216
+
217
+ low_stock = [s for s in obs.stock if 0 < s.quantity_on_hand < 15]
218
+ if low_stock:
219
+ lines.append("")
220
+ lines.append("=== Low Stock Alert ===")
221
+ for item in low_stock:
222
+ lines.append(f" {item.product_name}: qty={item.quantity_on_hand:.1f}")
223
+
224
+ if obs.pending_orders:
225
+ lines.append("")
226
+ lines.append("=== Pending Orders ===")
227
+ for po in obs.pending_orders:
228
+ lines.append(
229
+ f" {po.order_id}: {po.product_name} x{po.quantity} from {po.seller_name}, "
230
+ f"arrives in {po.days_remaining}d"
231
+ )
232
+
233
+ lines.append("")
234
+ lines.append("=== Marketing Actions (inactive only) ===")
235
+ for ma in obs.marketing_actions:
236
+ if not ma.is_active:
237
+ lines.append(
238
+ f" ID {ma.action_id}: {ma.description} | "
239
+ f"cost=₹{ma.cost_inr}, recurring=₹{ma.recurring_cost_inr}/day, "
240
+ f"satisfaction_delta={ma.customer_satisfaction_delta:+d}, "
241
+ f"capacity_delta={ma.order_processing_capacity_delta:+d}"
242
+ )
243
+
244
+ if obs.metadata:
245
+ rb = obs.metadata.get("reward_breakdown")
246
+ if rb:
247
+ lines.append("")
248
+ lines.append("=== Last Reward Breakdown ===")
249
+ lines.append(f" Starting Capital: ₹{rb.get('starting_capital_inr', 'N/A')}")
250
+ lines.append(f" Ending Capital: ₹{rb.get('ending_capital_inr', 'N/A')}")
251
+ lines.append(f" Difference: ₹{rb.get('difference_inr', 'N/A')}")
252
+ lines.append(f" Recurring Costs: ₹{rb.get('recurring_costs_paid_inr', 'N/A')}")
253
+
254
+ return "\n".join(lines)
255
+
256
+
257
+ SYSTEM_PROMPT = """\
258
+ You are a store management AI agent controlling a neighborhood kirana (grocery) store in India.
259
+
260
+ ## Simulation Mechanics
261
+
262
+ Each step you submit ONE bundled action for the next 3-day decision window:
263
+ 1. orders: list of supplier orders [{"product": "...", "seller": "Seller X", "quantity": N}]
264
+ 2. prices: list of price updates [{"product": "...", "price": N}] (in-stock products only)
265
+ 3. action_ids: list of marketing/operational action IDs to activate
266
+
267
+ ## Ground-Truth Formulas
268
+
269
+ DAILY ORDERS PROCESSED = order_processing_capacity × (customer_satisfaction / 100)
270
+
271
+ For each in-stock product with quantity > 0:
272
+ margin_pct = ((selling_price - buying_price) / selling_price) × 100
273
+ product_selling_score = max(0, demand_score - margin_pct × 3)
274
+
275
+ TOTAL_SELLING_SCORE = sum of all product_selling_scores
276
+ Each product's daily sales = (product_selling_score / TOTAL_SELLING_SCORE) × DAILY_ORDERS_PROCESSED
277
+ (capped at available quantity)
278
+
279
+ REWARD = 0.5 + (ending_capital - starting_capital - recurring_costs_6days) / starting_capital
280
+ where capital = bank_balance + total_inventory_value
281
+
282
+ CUSTOMER SATISFACTION UPDATE after price/stock changes:
283
+ new_satisfaction = (1 + (score_after - score_before) / score_after) × base_satisfaction
284
+ Capped at [1, 100].
285
+
286
+ ## Seller Profiles
287
+ - Seller A: Cheapest on staples but OVERCOMMITS — deliveries often 1-2 days late
288
+ - Seller B: Reliable baseline, matches quoted ETA consistently
289
+ - Seller C: Quotes conservatively, often arrives EARLY — best for urgent fresh restocks
290
+ - Seller D: Competitive on snacks/beverages but execution is uneven
291
+
292
+ ## Key Constraints
293
+ - Episode: 30 days, each step commits 3 days, reward horizon = 6 days
294
+ - Bank balance < 0 → episode ends immediately
295
+ - Marketing actions are DURABLE: one-time cost + permanent recurring daily cost
296
+ - Starting cash is tight (₹100k) relative to inventory (~₹240k)
297
+
298
+ ## Response Format
299
+ You MUST respond with ONLY valid JSON:
300
+ {
301
+ "reasoning": "brief explanation of your decision",
302
+ "orders": [{"product": "...", "seller": "Seller X", "quantity": N}, ...],
303
+ "prices": [{"product": "...", "price": N}, ...],
304
+ "action_ids": [N, ...]
305
+ }
306
+ No extra text outside JSON. Product names must exactly match the catalog.
307
+ """
308
+
309
+
310
+ def _build_task_system_prompt(task: dict[str, Any]) -> str:
311
+ task_goal = {
312
+ "margin_aware_pricing": (
313
+ "## Task: Margin-Aware Pricing\n\n"
314
+ "Your PRIMARY goal is to optimize selling prices using the selling-score formula.\n\n"
315
+ "The formula selling_score = max(0, demand - margin% × 3) means:\n"
316
+ "- A product with demand 96 and margin 6% has score = 96 - 18 = 78 (strong seller)\n"
317
+ "- Raising that margin to 20% gives score = 96 - 60 = 36 (volume collapses)\n"
318
+ "- A product with demand 28 at any margin has negligible score anyway\n\n"
319
+ "KEY INSIGHT: Thin-margin anchor items (Milk, Bread, Eggs, Bottled Water) have "
320
+ "HIGH selling scores because margin% × 3 is small. Raising their price destroys "
321
+ "volume disproportionately. Sugar (demand 74, margin ~5%) is a deliberate anchor — "
322
+ "do NOT raise it aggressively.\n\n"
323
+ "WHAT TO DO:\n"
324
+ "- Calculate selling_score for every in-stock product before changing prices\n"
325
+ "- Only raise prices on items where demand is high enough to absorb the score loss\n"
326
+ "- Consider cutting prices on over-margined items (Coffee: ~22% margin, demand 62)\n"
327
+ "- Kitchen Appliances (demand 28, margin ~35%) sell almost nothing — pricing won't help\n"
328
+ "- Protect total_selling_score: it directly feeds daily order allocation"
329
+ ),
330
+ "supplier_risk_arbitrage": (
331
+ "## Task: Supplier Risk Arbitrage\n\n"
332
+ "Your PRIMARY goal is to restock fast-movers at lowest cost without stockouts.\n\n"
333
+ "SELLER BEHAVIOR (actual delivery = quoted_ETA + random offset):\n"
334
+ "- Seller A: offset from {0,+1,+2} with weights {2,4,5} — most likely +2 days late\n"
335
+ "- Seller B: offset = 0 always — perfectly reliable\n"
336
+ "- Seller C: offset from {-2,-1,0} with weights {5,4,2} — most likely 2 days early\n"
337
+ "- Seller D: offset from {-1,0,+1,+2} with weights {2,4,3,2} — unpredictable\n\n"
338
+ "KEY OPPORTUNITIES:\n"
339
+ "- Seller A: Rice 138 (vs B: 160), Oil 156 (vs B: 180), Spices 345 (vs B: 400)\n"
340
+ " → Big savings but expect delivery 3-4 days instead of quoted 2\n"
341
+ "- Seller C: Bread 31, Eggs 64, Curd 33, Vegetables 38 — arrives 3 days early\n"
342
+ " → Best for urgent perishable restocks despite higher quoted ETA\n\n"
343
+ "URGENT RESTOCKS NEEDED:\n"
344
+ "- Biscuits: qty 40, demand 90 — will sell out fast, reorder via Seller B or C\n"
345
+ "- Bottled Water: qty 48, demand 89 — thin cover, needs immediate replenishment\n\n"
346
+ "STRATEGY: Use Seller A for non-urgent bulk staples (Rice, Oil) where you can "
347
+ "absorb delivery slippage. Use Seller C for urgent perishable/fresh restocks. "
348
+ "Use Seller B as the safe default when timing matters."
349
+ ),
350
+ "inventory_and_capital_discipline": (
351
+ "## Task: Inventory & Capital Discipline\n\n"
352
+ "Your PRIMARY goal is to protect service levels on fast-movers while preserving capital.\n\n"
353
+ "STARTING POSITION:\n"
354
+ "- Bank: ₹100,000 | Inventory: ~₹240,000 | Daily expense: ₹3,000\n"
355
+ "- Only ~33 days of cash runway before expenses alone drain the bank\n"
356
+ "- Revenue from sales replenishes cash, but you must keep selling\n\n"
357
+ "DEAD STOCK TO AVOID:\n"
358
+ "- Kitchen Appliances: demand 28, buy ₹1800, qty 4 = ₹7,200 locked capital\n"
359
+ " Selling score ≈ max(0, 28 - 35%×3) ≈ 0 — these barely move. NEVER reorder.\n\n"
360
+ "REPLENISHMENT PRIORITIES (by demand × low stock):\n"
361
+ "- Biscuits: demand 90, qty 40, buy ₹20 → cheap to restock, huge demand\n"
362
+ "- Bottled Water: demand 89, qty 48, buy ₹18 → cheap and high demand\n"
363
+ "- Eggs: demand 84, qty 24 → running low\n"
364
+ "- Bread: demand 86, qty 36 → perishable, needs frequent restocking\n\n"
365
+ "CAPITAL MATH: Each order is paid upfront on ordering (bank -= buy_price × qty). "
366
+ "Revenue flows in daily from sales. Over-ordering locks capital in inventory "
367
+ "that may not sell fast enough, starving you of cash for expenses.\n\n"
368
+ "RULE: Keep bank_balance > ₹20k safety buffer at all times. Order in small "
369
+ "batches rather than bulk. Prioritize items with highest demand-to-cost ratio."
370
+ ),
371
+ "marketing_roi_evaluation": (
372
+ "## Task: Marketing ROI Evaluation\n\n"
373
+ "Your PRIMARY goal is to select marketing actions that maximize capacity and "
374
+ "satisfaction per rupee of TOTAL cost (upfront + recurring over remaining days).\n\n"
375
+ "ZERO-RECURRING QUICK WINS (activate early):\n"
376
+ "- ID 1: Shelf placement — ₹800, +20 capacity, +1 satisfaction, 0 recurring\n"
377
+ "- ID 6: Storage racks — ₹7,000, +30 capacity, 0 satisfaction, 0 recurring\n"
378
+ "- ID 10: Staff training — ₹2,500, +25 capacity, +2 satisfaction, 0 recurring\n"
379
+ "- ID 9: Clearance shelf — ₹500, +6 capacity, -2 satisfaction, 0 recurring\n\n"
380
+ "MODERATE-RECURRING GOOD VALUE:\n"
381
+ "- ID 4: WhatsApp ordering — ₹1,500 + ₹100/day, +15 capacity, +4 satisfaction\n"
382
+ "- ID 8: Referral rewards — ₹1,800 + ₹150/day, 0 capacity, +3 satisfaction\n\n"
383
+ "MID-GAME EXPANSION (expensive but transformative if cash allows):\n"
384
+ "- ID 7: Delivery partner — ₹5,000 + ₹600/day, +40 capacity\n"
385
+ "- ID 12: Website — ₹60,000 + ₹400/day, +60 capacity\n"
386
+ "- ID 13: Delivery rider — ₹45,000 + ₹900/day, +50 capacity\n\n"
387
+ "THE TRAP — DO NOT ACTIVATE:\n"
388
+ "- ID 15: Social ads — ₹4,000 + ₹1,200/day, +0 capacity, only +3 satisfaction\n"
389
+ " → Over 30 days this costs ₹40,000 total with ZERO capacity gain\n\n"
390
+ "UNREACHABLE:\n"
391
+ "- ID 11: Second branch — ₹750,000 upfront. You only have ₹100k. Skip it.\n\n"
392
+ "FORMULA IMPACT: More capacity → more daily_orders_processed → more revenue. "
393
+ "Recurring costs are subtracted from reward. Evaluate 6-day total cost of "
394
+ "ownership against the capacity gain."
395
+ ),
396
+ "full_store_management": (
397
+ "## Task: Full Store Management (Integrative)\n\n"
398
+ "Your PRIMARY goal is to run the store profitably across all 10 decision windows "
399
+ "by combining pricing, ordering, inventory management, and marketing.\n\n"
400
+ "PHASE 1 (Steps 1-3): Foundation\n"
401
+ "- Activate zero-recurring marketing: IDs 1, 10, 6 (total ₹10,300, +75 capacity)\n"
402
+ "- Restock urgent items: Biscuits and Bottled Water via Seller B or C\n"
403
+ "- Review prices: do NOT raise Milk/Bread/Eggs/Sugar — they're anchor items\n"
404
+ "- Consider cutting Coffee price to boost its selling score\n\n"
405
+ "PHASE 2 (Steps 4-6): Growth\n"
406
+ "- Activate WhatsApp ordering (ID 4) for +15 capacity at ₹100/day\n"
407
+ "- Use Seller A for bulk Rice/Oil/Spices — savings are ~15-20% vs Seller B\n"
408
+ "- Monitor delivery arrivals and restock perishables via Seller C as needed\n"
409
+ "- Evaluate delivery partner (ID 7) — +40 capacity but ₹600/day\n\n"
410
+ "PHASE 3 (Steps 7-10): Optimization\n"
411
+ "- Fine-tune prices based on observed stock depletion rates\n"
412
+ "- If profitable, invest in ID 12 (website) or ID 13 (rider) for big capacity\n"
413
+ "- NEVER activate ID 15 (social ads) — ₹1,200/day for +0 capacity is a trap\n"
414
+ "- NEVER reorder Kitchen Appliances — dead stock\n"
415
+ "- Keep bank > ₹15k at all times to avoid bankruptcy\n\n"
416
+ "REWARD OPTIMIZATION: Each step's reward = 0.5 + capital_growth_rate. "
417
+ "Capital = bank + inventory. Maximize revenue through high selling scores, "
418
+ "minimize costs through smart supplier selection and avoiding wasteful marketing."
419
+ ),
420
+ }
421
+ goal = task_goal.get(task["name"], "Maximize your score.")
422
+ return SYSTEM_PROMPT + f"\n\n{goal}"
423
+
424
+
425
+ async def get_model_action(
426
+ client: AsyncOpenAI,
427
+ step: int,
428
+ observation_text: str,
429
+ last_reward: float,
430
+ history: list[str],
431
+ task_system_prompt: str,
432
+ ) -> dict[str, Any]:
433
+ """Query the LLM for the next action."""
434
+ history_text = "\n".join(history[-5:]) if history else "No history yet."
435
+
436
+ user_msg = (
437
+ f"Step {step} | Last reward: {last_reward:+.4f}\n\n"
438
+ f"Recent history:\n{history_text}\n\n"
439
+ f"Current observation:\n{observation_text}\n\n"
440
+ "Decide your next action. Respond with ONLY valid JSON."
441
+ )
442
+
443
+ response = await client.chat.completions.create(
444
+ model=MODEL_NAME,
445
+ messages=[
446
+ {"role": "system", "content": task_system_prompt},
447
+ {"role": "user", "content": user_msg},
448
+ ],
449
+ temperature=0.3,
450
+ max_tokens=1024,
451
+ )
452
+
453
+ content = response.choices[0].message.content or "{}"
454
+ content = content.strip()
455
+ if content.startswith("```"):
456
+ content = content.split("\n", 1)[-1]
457
+ if content.endswith("```"):
458
+ content = content[: -len("```")]
459
+ content = content.strip()
460
+
461
+ return json.loads(content)
462
+
463
+
464
+ def _parse_action(raw: dict[str, Any]) -> StoreAction:
465
+ """Convert the LLM JSON response into a StoreAction."""
466
+ orders = [
467
+ OrderRequest(
468
+ product=o["product"],
469
+ seller=o["seller"],
470
+ quantity=int(o["quantity"]),
471
+ )
472
+ for o in raw.get("orders", [])
473
+ ]
474
+ prices = [
475
+ PriceUpdate(product=p["product"], price=int(p["price"]))
476
+ for p in raw.get("prices", [])
477
+ ] or None
478
+ action_ids = [int(a) for a in raw.get("action_ids", [])]
479
+ return StoreAction(orders=orders, prices=prices, action_ids=action_ids)
480
+
481
+
482
+ async def run_task(
483
+ llm_client: AsyncOpenAI,
484
+ task: dict[str, Any],
485
+ ) -> float:
486
+ """Run a single task episode and return the clamped score."""
487
+ task_name = task["name"]
488
+ max_steps = task["max_steps"]
489
+ threshold = task["success_threshold"]
490
+ task_system_prompt = _build_task_system_prompt(task)
491
+
492
+ history: list[str] = []
493
+ rewards: list[float] = []
494
+ steps_taken = 0
495
+ score = 0.0
496
+ success = False
497
+
498
+ log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)
499
+
500
+ env = StoreEnv(base_url=ENV_URL)
501
+
502
+ try:
503
+ result = await env.reset()
504
+ obs = result.observation
505
+ obs_text = _format_observation(obs)
506
+ last_reward = 0.0
507
+
508
+ for step in range(1, max_steps + 1):
509
+ if obs.done:
510
+ break
511
+
512
+ raw_action: dict[str, Any] = {}
513
+ action_str = ""
514
+ error = None
515
+
516
+ try:
517
+ raw_action = await get_model_action(
518
+ llm_client,
519
+ step,
520
+ obs_text,
521
+ last_reward,
522
+ history,
523
+ task_system_prompt,
524
+ )
525
+ action = _parse_action(raw_action)
526
+ action_str = json.dumps(dump_model(action, exclude_none=True), separators=(",", ":"))
527
+ except Exception as e:
528
+ print(f"[DEBUG] Action parse error at step {step}: {e}", flush=True)
529
+ action = StoreAction()
530
+ action_str = "{}"
531
+ error = str(e)
532
+
533
+ try:
534
+ result = await env.step(action)
535
+ except Exception as e:
536
+ print(f"[DEBUG] Step error at step {step}: {e}", flush=True)
537
+ action = StoreAction()
538
+ action_str = "{}"
539
+ error = str(e)
540
+ try:
541
+ result = await env.step(action)
542
+ except Exception as e2:
543
+ print(f"[DEBUG] Pass-step also failed: {e2}", flush=True)
544
+ error = str(e2)
545
+ break
546
+
547
+ obs = result.observation
548
+ reward = result.reward or 0.0
549
+ done = result.done
550
+
551
+ rewards.append(reward)
552
+ steps_taken = step
553
+ obs_text = _format_observation(obs)
554
+ last_reward = reward
555
+
556
+ log_step(
557
+ step=step,
558
+ action=action_str,
559
+ reward=reward,
560
+ done=done,
561
+ error=error,
562
+ )
563
+
564
+ reasoning = raw_action.get("reasoning", "")
565
+ history.append(
566
+ f"Step {step}: reward={reward:+.4f}, done={done}, "
567
+ f"reasoning={reasoning!r}"
568
+ )
569
+
570
+ if done:
571
+ break
572
+
573
+ score = sum(rewards) / (max_steps * 1.0) if max_steps > 0 else 0.0
574
+ score = min(max(score, 0.0), 1.0)
575
+ success = score >= threshold
576
+
577
+ finally:
578
+ try:
579
+ await env.close()
580
+ except Exception as e:
581
+ print(f"[DEBUG] env.close() error (container cleanup): {e}", flush=True)
582
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
583
+
584
+ return score
585
+
586
+
587
+ async def main() -> None:
588
+ llm_client = AsyncOpenAI(
589
+ base_url=API_BASE_URL,
590
+ api_key=HF_TOKEN,
591
+ )
592
+
593
+ print(f"[DEBUG] Starting inference with model={MODEL_NAME}", flush=True)
594
+ print(f"[DEBUG] API_BASE_URL={API_BASE_URL}", flush=True)
595
+ print(f"[DEBUG] ENV_URL={ENV_URL}", flush=True)
596
+ print(f"[DEBUG] Tasks: {[t['name'] for t in TASKS]}", flush=True)
597
+
598
+ all_scores: list[float] = []
599
+
600
+ for task in TASKS:
601
+ print(f"\n[DEBUG] === Running task: {task['name']} ===", flush=True)
602
+ t0 = time.time()
603
+ task_score = await run_task(llm_client, task)
604
+ elapsed = time.time() - t0
605
+ all_scores.append(task_score)
606
+ print(
607
+ f"[DEBUG] Task {task['name']} completed: score={task_score:.4f}, "
608
+ f"elapsed={elapsed:.1f}s",
609
+ flush=True,
610
+ )
611
+
612
+ avg_score = sum(all_scores) / len(all_scores) if all_scores else 0.0
613
+ print(f"\n[DEBUG] All task scores: {all_scores}", flush=True)
614
+ print(f"[DEBUG] Average score: {avg_score:.4f}", flush=True)
615
+
616
+
617
+ if __name__ == "__main__":
618
+ asyncio.run(main())
pyproject.toml CHANGED
@@ -14,20 +14,12 @@ version = "0.1.0"
14
  description = "Store environment for OpenEnv"
15
  requires-python = ">=3.10"
16
  dependencies = [
17
- # Core OpenEnv runtime (provides FastAPI server + HTTP client types)
18
- # install from github
19
- # "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
20
  "gradio>=6.0.0",
21
  "openenv-core[core]>=0.2.2",
22
  "pydantic>=2.0.0",
23
- # Environment-specific dependencies
24
- # Add all dependencies needed for your environment here
25
- # Examples:
26
- # "numpy>=1.19.0",
27
- # "torch>=2.0.0",
28
- # "gymnasium>=0.29.0",
29
- # "openspiel>=1.0.0",
30
- # "smolagents>=1.22.0,<2",
31
  ]
32
 
33
  [project.optional-dependencies]
 
14
  description = "Store environment for OpenEnv"
15
  requires-python = ">=3.10"
16
  dependencies = [
17
+ # Core OpenEnV runtime (provides FastAPI server + HTTP client types)
 
 
18
  "gradio>=6.0.0",
19
  "openenv-core[core]>=0.2.2",
20
  "pydantic>=2.0.0",
21
+ "openai>=1.0.0",
22
+ "python-dotenv>=1.0.0",
 
 
 
 
 
 
23
  ]
24
 
25
  [project.optional-dependencies]
scripts/validate-submission.sh ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # chmod +x scripts/validate-submission.sh
9
+ # ./scripts/validate-submission.sh <ping_url> [repo_dir]
10
+ #
11
+
12
+ set -uo pipefail
13
+
14
+ DOCKER_BUILD_TIMEOUT=600
15
+ if [ -t 1 ]; then
16
+ RED='\033[0;31m'
17
+ GREEN='\033[0;32m'
18
+ YELLOW='\033[1;33m'
19
+ BOLD='\033[1m'
20
+ NC='\033[0m'
21
+ else
22
+ RED='' GREEN='' YELLOW='' BOLD='' NC=''
23
+ fi
24
+
25
+ pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
26
+ fail() { echo -e "${RED}[FAIL]${NC} $1"; FAILED=1; }
27
+ warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
28
+ info() { echo -e "${BOLD}[INFO]${NC} $1"; }
29
+
30
+ FAILED=0
31
+
32
+ PING_URL="${1:-}"
33
+ REPO_DIR="${2:-.}"
34
+
35
+ if [ -z "$PING_URL" ]; then
36
+ echo "Usage: $0 <ping_url> [repo_dir]"
37
+ echo " ping_url Your HuggingFace Space URL (e.g. https://your-space.hf.space)"
38
+ echo " repo_dir Path to your repo (default: current directory)"
39
+ exit 1
40
+ fi
41
+
42
+ REPO_DIR="$(cd "$REPO_DIR" && pwd)"
43
+
44
+ echo ""
45
+ echo -e "${BOLD}=== OpenEnv Submission Validator ===${NC}"
46
+ echo ""
47
+
48
+ # ---------- Check 1: Required files ----------
49
+ info "Checking required files..."
50
+
51
+ for f in openenv.yaml Dockerfile inference.py; do
52
+ if [ -f "$REPO_DIR/$f" ]; then
53
+ pass "Found $f"
54
+ else
55
+ fail "Missing $f"
56
+ fi
57
+ done
58
+
59
+ # ---------- Check 2: openenv.yaml ----------
60
+ info "Checking openenv.yaml..."
61
+ if [ -f "$REPO_DIR/openenv.yaml" ]; then
62
+ if grep -q "spec_version" "$REPO_DIR/openenv.yaml"; then
63
+ pass "openenv.yaml has spec_version"
64
+ else
65
+ fail "openenv.yaml missing spec_version"
66
+ fi
67
+ if grep -q "name:" "$REPO_DIR/openenv.yaml"; then
68
+ pass "openenv.yaml has name field"
69
+ else
70
+ fail "openenv.yaml missing name field"
71
+ fi
72
+ fi
73
+
74
+ # ---------- Check 3: inference.py structure ----------
75
+ info "Checking inference.py structure..."
76
+ if [ -f "$REPO_DIR/inference.py" ]; then
77
+ for pattern in 'API_BASE_URL' 'MODEL_NAME' 'HF_TOKEN' 'OpenAI' '"START"' '"STEP"' '"END"'; do
78
+ if grep -q "$pattern" "$REPO_DIR/inference.py"; then
79
+ pass "inference.py references $pattern"
80
+ else
81
+ fail "inference.py missing reference to $pattern"
82
+ fi
83
+ done
84
+ fi
85
+
86
+ # ---------- Check 4: Environment variables ----------
87
+ info "Checking environment variables..."
88
+ for var in API_BASE_URL MODEL_NAME HF_TOKEN; do
89
+ if [ -n "${!var:-}" ]; then
90
+ pass "$var is set"
91
+ else
92
+ warn "$var is not set (required at runtime)"
93
+ fi
94
+ done
95
+
96
+ # ---------- Check 5: Docker build ----------
97
+ info "Checking Docker build..."
98
+ if command -v docker &>/dev/null; then
99
+ info "Running docker build (timeout ${DOCKER_BUILD_TIMEOUT}s)..."
100
+ if timeout "$DOCKER_BUILD_TIMEOUT" docker build -t openenv-store-test "$REPO_DIR" 2>&1; then
101
+ pass "Docker build succeeded"
102
+ else
103
+ fail "Docker build failed"
104
+ fi
105
+ else
106
+ warn "Docker not found — skipping Docker build check"
107
+ fi
108
+
109
+ # ---------- Check 6: HF Space ping ----------
110
+ info "Pinging HF Space at $PING_URL ..."
111
+ if command -v curl &>/dev/null; then
112
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$PING_URL/health" 2>/dev/null || echo "000")
113
+ if [ "$HTTP_CODE" = "200" ]; then
114
+ pass "HF Space /health returned 200"
115
+ else
116
+ fail "HF Space /health returned HTTP $HTTP_CODE (expected 200)"
117
+ fi
118
+
119
+ RESET_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 -X POST "$PING_URL/reset" 2>/dev/null || echo "000")
120
+ if [ "$RESET_CODE" = "200" ]; then
121
+ pass "HF Space /reset returned 200"
122
+ else
123
+ fail "HF Space /reset returned HTTP $RESET_CODE (expected 200)"
124
+ fi
125
+ else
126
+ warn "curl not found — skipping ping check"
127
+ fi
128
+
129
+ # ---------- Check 7: openenv validate ----------
130
+ info "Running openenv validate..."
131
+ if command -v openenv &>/dev/null; then
132
+ if (cd "$REPO_DIR" && openenv validate .); then
133
+ pass "openenv validate passed"
134
+ else
135
+ fail "openenv validate failed"
136
+ fi
137
+ elif command -v uv &>/dev/null && [ -f "$REPO_DIR/pyproject.toml" ]; then
138
+ if (cd "$REPO_DIR" && uv run --project . openenv validate .); then
139
+ pass "openenv validate passed (via uv)"
140
+ else
141
+ fail "openenv validate failed (via uv)"
142
+ fi
143
+ else
144
+ warn "Neither openenv nor uv found — skipping validate"
145
+ fi
146
+
147
+ # ---------- Summary ----------
148
+ echo ""
149
+ if [ "$FAILED" -eq 0 ]; then
150
+ echo -e "${GREEN}${BOLD}All checks passed!${NC}"
151
+ exit 0
152
+ else
153
+ echo -e "${RED}${BOLD}Some checks failed. Please fix the issues above before submitting.${NC}"
154
+ exit 1
155
+ fi
server/requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  openenv[core]>=0.2.0
2
  fastapi>=0.115.0
3
  uvicorn>=0.24.0
 
4
 
5
 
6
 
 
1
  openenv[core]>=0.2.0
2
  fastapi>=0.115.0
3
  uvicorn>=0.24.0
4
+ openai>=1.0.0
5
 
6
 
7
 
uv.lock CHANGED
@@ -1604,8 +1604,10 @@ version = "0.1.0"
1604
  source = { editable = "." }
1605
  dependencies = [
1606
  { name = "gradio" },
 
1607
  { name = "openenv-core", extra = ["core"] },
1608
  { name = "pydantic" },
 
1609
  ]
1610
 
1611
  [package.optional-dependencies]
@@ -1617,10 +1619,12 @@ dev = [
1617
  [package.metadata]
1618
  requires-dist = [
1619
  { name = "gradio", specifier = ">=6.0.0" },
 
1620
  { name = "openenv-core", extras = ["core"], specifier = ">=0.2.2" },
1621
  { name = "pydantic", specifier = ">=2.0.0" },
1622
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
1623
  { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },
 
1624
  ]
1625
  provides-extras = ["dev"]
1626
 
 
1604
  source = { editable = "." }
1605
  dependencies = [
1606
  { name = "gradio" },
1607
+ { name = "openai" },
1608
  { name = "openenv-core", extra = ["core"] },
1609
  { name = "pydantic" },
1610
+ { name = "python-dotenv" },
1611
  ]
1612
 
1613
  [package.optional-dependencies]
 
1619
  [package.metadata]
1620
  requires-dist = [
1621
  { name = "gradio", specifier = ">=6.0.0" },
1622
+ { name = "openai", specifier = ">=1.0.0" },
1623
  { name = "openenv-core", extras = ["core"], specifier = ">=0.2.2" },
1624
  { name = "pydantic", specifier = ">=2.0.0" },
1625
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
1626
  { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" },
1627
+ { name = "python-dotenv", specifier = ">=1.0.0" },
1628
  ]
1629
  provides-extras = ["dev"]
1630