Spaces:
Sleeping
Sleeping
| """ | |
| Inference Script for OptiChain-Env | |
| =================================== | |
| MANDATORY HACKATHON CONFIGURATION | |
| - Reads API_BASE_URL, MODEL_NAME, HF_TOKEN from environment. | |
| - Uses OpenAI Python client for all LLM calls. | |
| - Emits structured [START], [STEP], [END] logs for automated scoring. | |
| """ | |
| import os | |
| import logging | |
| from dotenv import load_dotenv | |
| from openai import OpenAI | |
| from env.core import SupplyChainEnv, EXPEDITE_SURCHARGE | |
| from env.schemas import SupplyChainAction, SupplyChainObservation, PurchaseOrder | |
| logger = logging.getLogger(__name__) | |
| # Load environment variables for local testing | |
| load_dotenv() | |
| # ================================================================= | |
| # MANDATORY HACKATHON CONFIGURATION | |
| # Set these in your .env or HF Space secrets before running. | |
| # API_BASE_URL — LLM endpoint (OpenAI-compatible) | |
| # MODEL_NAME — model identifier | |
| # HF_TOKEN — your Hugging Face / API key | |
| # ================================================================= | |
| API_BASE_URL = os.environ.get("API_BASE_URL") or "http://localhost:11434/v1" | |
| API_KEY = os.environ.get("HF_TOKEN") or os.environ.get("API_KEY") or "ollama" | |
| MODEL_NAME = os.environ.get("MODEL_NAME") or "llama3.1:8b" | |
| # Inference hyper-parameters | |
| ANALYST_TEMPERATURE = 0.1 # slight randomness for strategic reasoning | |
| EXECUTOR_TEMPERATURE = 0.0 # deterministic JSON formatting | |
| MAX_TOKENS = 512 # cap token usage per call | |
| # Lazy client — created on first LLM call so the server can boot | |
| # before API secrets are injected (e.g. HF Space cold start). | |
| _client: OpenAI | None = None | |
| def _get_client() -> OpenAI: | |
| """Return a cached OpenAI client, re-reading env vars on first call.""" | |
| global _client | |
| if _client is None: | |
| _client = OpenAI( | |
| base_url=os.environ.get("API_BASE_URL") or API_BASE_URL, | |
| api_key=os.environ.get("HF_TOKEN") or os.environ.get("API_KEY") or API_KEY, | |
| ) | |
| return _client | |
| # ================================================================= | |
| def get_agent_action(obs: SupplyChainObservation) -> tuple[SupplyChainAction, str]: | |
| """ | |
| Multi-agent pipeline: Analyst reasons about the market, Executor formats the JSON. | |
| Returns: (SupplyChainAction, reasoning_string) | |
| """ | |
| # ========================================================= | |
| # 1. EXTRACT RAW TELEMETRY FOR THE LLM (No Python Math) | |
| # ========================================================= | |
| wh = obs.warehouse_status[0] | |
| current_stock = wh.current_stock | |
| incoming_shipments = sum(wh.incoming_shipments.values()) | |
| total_inventory_pos = current_stock + incoming_shipments | |
| days_remaining = obs.total_days - obs.current_day | |
| sales_yesterday = wh.sales_yesterday | |
| lost_yesterday = wh.lost_sales_yesterday | |
| # Format the pipeline so the LLM knows exactly when stock arrives | |
| pipeline_str = ", ".join([f"{qty} units in {days} days" for days, qty in wh.incoming_shipments.items() if qty > 0]) | |
| if not pipeline_str: | |
| pipeline_str = "No incoming shipments." | |
| # ========================================================= | |
| # 🤖 AGENT 1: THE ANALYST (Strategic Decision Maker) | |
| # ========================================================= | |
| analyst_prompt = ( | |
| "You are an Elite Supply Chain Optimizer. Your performance is graded on 'Inventory Efficiency' (Newsvendor Logic).\n\n" | |
| "=== THE GOLDEN RULES FOR A 1.0 SCORE ===\n" | |
| "1. OVERAGE IS FAILURE: Ending any day with unsold stock kills your score. Aim for JIT (Just-In-Time) delivery.\n" | |
| "2. UNDERAGE IS FAILURE: Missing a customer sale kills your score. Maintain a minimal safety buffer.\n" | |
| "3. PIPELINE MATH: Your 'Total Inventory Position' = Current Stock + All units in Pipeline.\n" | |
| "4. TARGET FORMULA: Aim for a Total Inventory Position = (Predicted Daily Demand) * (Lead Time + 1).\n\n" | |
| "=== UNIT ECONOMICS & LEAD TIMES ===\n" | |
| "- Standard: $800 cost | 2-day lead time (4 days during crisis).\n" | |
| "- Expedited: $900 cost | 1-day lead time.\n" | |
| "- Holding Cost: $2/unit/day | Stockout Penalty: $100/unit.\n\n" | |
| "=== STRATEGIC MANDATES ===\n" | |
| "- PREDICTIVE BUFFER: Analyze 'Yesterday's Performance'. If demand was 12, assume today is 12. Add a +2 unit safety buffer only.\n" | |
| "- BUDGET CHECK: You MUST multiply (Order Quantity * Unit Cost). This result MUST be less than your current Cash Balance.\n" | |
| "- CRISIS ADAPTATION: During a shipping crisis (4-day delay), use Expedited (1-day) to stay lean and responsive.\n" | |
| "- HORIZON AWARENESS: The simulation ends on Day 30. Any stock arriving after Day 30 is a total financial loss and results in a 0.0 efficiency score. " | |
| "Calculate 'Days Remaining' vs 'Lead Time' to decide when to stop ordering. Your goal is to have EXACTLY zero stock on Day 30.\n\n" | |
| "=== REQUIRED OUTPUT FORMAT ===\n" | |
| "Begin with a 'Step-by-Step Math' paragraph: Calculate Predicted Demand, Current Inventory Position, and identify if an order will arrive before Day 30.\n" | |
| "End your response with these EXACT tags:\n" | |
| "ORDER_QUANTITY: [number]\n" | |
| "EXPEDITE: [true/false]" | |
| ) | |
| # Build order feedback line so the LLM knows if its last order was rejected | |
| if obs.last_order_rejected > 0: | |
| order_feedback = ( | |
| f"LAST ORDER: REJECTED {obs.last_order_rejected} units (insufficient funds). " | |
| f"Only {obs.last_order_accepted} units were accepted. Reduce your order size!" | |
| ) | |
| elif obs.last_order_accepted > 0: | |
| order_feedback = f"LAST ORDER: Accepted {obs.last_order_accepted} units." | |
| else: | |
| order_feedback = "LAST ORDER: No order placed." | |
| analyst_context = ( | |
| f"=== CURRENT STATUS ===\n" | |
| f"DAY: {obs.current_day} of {obs.total_days} ({days_remaining} days remaining)\n" | |
| f"MARKET SIGNAL: {obs.market_trend_signal}\n" | |
| f"YESTERDAY'S PERFORMANCE: Sold {sales_yesterday}, Missed {lost_yesterday} sales.\n" | |
| f"{order_feedback}\n" | |
| f"CASH BALANCE: ${obs.cash_balance:.2f}\n" | |
| f"CURRENT STOCK: {current_stock} units\n" | |
| f"PIPELINE: {pipeline_str}\n" | |
| f"TOTAL INVENTORY POSITION: {total_inventory_pos} units\n\n" | |
| "Write your reasoning and final decision:" | |
| ) | |
| try: | |
| resp = _get_client().chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": analyst_prompt}, | |
| {"role": "user", "content": analyst_context}, | |
| ], | |
| temperature=ANALYST_TEMPERATURE, | |
| max_tokens=MAX_TOKENS, | |
| timeout=30, | |
| ) | |
| strategic_plan = resp.choices[0].message.content or "" | |
| except Exception as exc: | |
| logger.error("Analyst agent failed: %s", exc, exc_info=True) | |
| strategic_plan = f"Analyst unavailable ({exc}).\nORDER_QUANTITY: 0\nEXPEDITE: false" | |
| # ========================================================= | |
| # 🤖 AGENT 2: THE EXECUTOR (Strict JSON Formatter) | |
| # ========================================================= | |
| executor_prompt = ( | |
| "You are a strict Data Parsing API. Read the Analyst's plan, locate the 'ORDER_QUANTITY' and 'EXPEDITE' values, " | |
| "and output ONLY valid JSON using this exact schema:\n" | |
| "{\n" | |
| ' "orders": [\n' | |
| ' {"product_id": "SKU-LAPTOP", "quantity": <INT>, "expedite_shipping": <BOOL>}\n' | |
| " ]\n" | |
| "}\n" | |
| "If ORDER_QUANTITY is 0, output: {\"orders\": []}\n" | |
| "Do not output markdown blocks or any other text." | |
| ) | |
| try: | |
| resp = _get_client().chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": executor_prompt}, | |
| {"role": "user", "content": f"ANALYST PLAN:\n{strategic_plan}"}, | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=EXECUTOR_TEMPERATURE, | |
| max_tokens=MAX_TOKENS, | |
| timeout=30, | |
| ) | |
| action = SupplyChainAction.model_validate_json(resp.choices[0].message.content or "{}") | |
| except Exception as exc: | |
| logger.error("Executor agent failed: %s", exc, exc_info=True) | |
| action = SupplyChainAction(orders=[]) | |
| # ================================================================= | |
| # PYTHON GUARDRAILS — clip LLM output to what's affordable AND useful | |
| # Three caps applied in order: | |
| # 1. Burn-down: don't order if inventory already covers remaining demand | |
| # 2. Demand cap: never hold more than remaining_days × max_daily_demand | |
| # 3. Affordability: never exceed cash balance | |
| # 4. Lead-time: don't order if delivery arrives after episode ends | |
| # ================================================================= | |
| is_spike = "Black Friday" in obs.market_trend_signal | |
| is_crisis = "crisis" in obs.market_trend_signal.lower() or "delay" in obs.market_trend_signal.lower() | |
| # Estimate max daily demand from the task context | |
| max_daily_demand = 45 if is_spike else (20 if is_crisis else 12) | |
| lead_time = 1 if is_crisis else 2 | |
| max_useful = max(0, days_remaining * max_daily_demand - total_inventory_pos) | |
| clipped_orders = [] | |
| remaining_cash = obs.cash_balance | |
| for order in action.orders: | |
| unit_cost = 900 if order.expedite_shipping else 800 | |
| max_affordable = int(remaining_cash // unit_cost) if remaining_cash > 0 else 0 | |
| qty = order.quantity | |
| qty = min(qty, max_useful) # demand cap | |
| qty = min(qty, max_affordable) # affordability cap | |
| # Lead-time gate: no point ordering if it arrives after Day 30 | |
| if days_remaining <= lead_time: | |
| qty = 0 | |
| if qty > 0: | |
| remaining_cash -= qty * unit_cost | |
| max_useful -= qty # reduce remaining headroom | |
| clipped_orders.append(PurchaseOrder( | |
| product_id=order.product_id, | |
| quantity=qty, | |
| expedite_shipping=order.expedite_shipping, | |
| )) | |
| action = SupplyChainAction(orders=clipped_orders) | |
| return action, strategic_plan or "" | |
| BENCHMARK = "optichain-inventory-v1" | |
| SUCCESS_THRESHOLD = 0.1 # grader score >= 0.5 counts as success | |
| def _log_start(task: str, env_name: str, model: str) -> None: | |
| print(f"[START] task={task} env={env_name} model={model}", flush=True) | |
| def _log_step(step: int, action_str: str, reward: float, done: bool, error: str | None) -> None: | |
| error_val = error if error else "null" | |
| print( | |
| f"[STEP] step={step} action={action_str} reward={reward:.2f} " | |
| f"done={str(done).lower()} error={error_val}", | |
| flush=True, | |
| ) | |
| def _log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None: | |
| rewards_str = ",".join(f"{r:.2f}" for r in rewards) | |
| print( | |
| f"[END] success={str(success).lower()} steps={steps} " | |
| f"score={score:.2f} rewards={rewards_str}", | |
| flush=True, | |
| ) | |
| def main(): | |
| """ | |
| Full CLI evaluation loop. | |
| Emits [START], [STEP], [END] structured logs required by the hackathon scorer. | |
| """ | |
| env = SupplyChainEnv() | |
| tasks = ["task_01_easy", "task_02_medium", "task_03_hard"] | |
| for task_id in tasks: | |
| _log_start(task=task_id, env_name=BENCHMARK, model=MODEL_NAME) | |
| obs = env.reset(task_id=task_id) | |
| step = 0 | |
| rewards: list[float] = [] | |
| error: str | None = None | |
| try: | |
| while not obs.done: | |
| step += 1 | |
| action, _ = get_agent_action(obs) | |
| # Compact action string: e.g. "SKU-LAPTOP x10 std" | |
| if action.orders: | |
| o = action.orders[0] | |
| mode = "exp" if o.expedite_shipping else "std" | |
| action_str = f"{o.product_id}x{o.quantity}{mode}" | |
| else: | |
| action_str = "no_order" | |
| obs = env.step(action) | |
| rewards.append(obs.reward) | |
| _log_step( | |
| step=step, | |
| action_str=action_str, | |
| reward=obs.reward, | |
| done=obs.done, | |
| error=error, | |
| ) | |
| except Exception as exc: | |
| error = str(exc) | |
| logger.error("Episode error on %s: %s", task_id, exc, exc_info=True) | |
| score = env.get_grader_score() | |
| success = score >= SUCCESS_THRESHOLD | |
| _log_end(success=success, steps=step, score=score, rewards=rewards) | |
| if __name__ == "__main__": | |
| main() |