""" 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": , "expedite_shipping": }\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()