optichain-env / inference.py
Padmanava's picture
Upload folder using huggingface_hub
51615a4 verified
Raw
History Blame Contribute Delete
13 kB
"""
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()