CoolFace
Apppublic

manzz05/kitchenflow-v2

sourceHugging Facemitupdated 5mo agoView on Hugging Face
1likes
inference.py271 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3inference.py — KitchenFlow-v1 Ghost Kitchen Dispatcher Baseline4===============================================================5Runs an LLM agent against all 3 tasks. Each step = 1 simulation minute.6 7Required environment variables:8    API_BASE_URL   LLM API endpoint (default: https://router.huggingface.co/v1)9    MODEL_NAME     Model identifier10    HF_TOKEN       HuggingFace / API key11 12Usage:13    python inference.py14    python inference.py --url http://localhost:786015    python inference.py --task T1_single_order_dispatch16 17Structured output format (required by validator):18    [START] task=TASK_ID19    [STEP] step=N reward=R score=S done=true/false20    [END] task=TASK_ID score=S steps=N21    Scores are strictly in (0, 1) — never 0.0 or 1.0.22"""23 24import argparse25import json26import os27import sys28import textwrap29import time30import urllib.request31import urllib.error32from typing import Optional33 34from openai import OpenAI35 36# ── Config ────────────────────────────────────────────────────────────────────37API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")38API_KEY      = os.getenv("HF_TOKEN") or os.getenv("API_KEY", "")39MODEL_NAME   = os.getenv("MODEL_NAME", "meta-llama/Llama-3.3-70B-Instruct")40ENV_URL      = os.getenv("ENV_URL", "http://localhost:7860")41 42TEMPERATURE = 0.143MAX_TOKENS  = 25644 45SYSTEM_PROMPT = textwrap.dedent("""46    You are the AI dispatcher for a ghost kitchen delivery hub.47    Every minute you receive a snapshot of active orders and must decide48    whether to summon a driver for each order.49 50    Physics:51    - Driver speed = 30 km/h / traffic_index  (in km per minute: 0.5 / traffic_index)52    - Driver ETA (minutes) = driver_dist_km / (0.5 / traffic_index)53    - Food cools at 1.8 degrees C/minute while waiting; perfect temp = 75 degrees C54    - Once summoned, a driver cannot be un-summoned55 56    Strategy:57    - Summon driver when: food_ready_min - current_time approximately equals driver_eta_min58    - i.e. dispatch so driver arrives just as food is bagged59    - food_ready_min approximately equals current_time + (1 - food_prep_progress) x prep_time_min60    - (prep_time is NOT shown -- infer it from food_prep_progress vs time)61 62    Reply with ONLY a valid JSON object:63    {"dispatch_decisions": {"ORD001": 0, "ORD002": 1}}64    Values: 0 = wait, 1 = summon driver now (ignored if already summoned)65    Include every active order ID in the response.66""").strip()67 68 69def build_prompt(obs: dict) -> str:70    lines = [71        f"MINUTE: {obs['time_min']} / {obs['max_time_min']}",72        f"TRAFFIC INDEX: {obs['traffic_index']} "73        f"(driver speed = {0.5 / obs['traffic_index']:.3f} km/min)",74        "",75        "ORDERS:",76    ]77    for o in obs["orders"]:78        if o["delivered"] or o["failed"]:79            lines.append(f"  {o['order_id']} [{o['status'].upper()}]")80            continue81 82        eta_str = f"ETA={o['driver_eta_min']}min" if o["driver_summoned"] else "not summoned"83        lines.append(84            f"  {o['order_id']} | {o['item_name']}"85            f" | prep={o['food_prep_progress']*100:.0f}%"86            f" | dist={o['driver_dist_km']:.2f}km ({eta_str})"87            f" | temp={o['food_temp_c']:.1f}C"88            f" | food_ready={'YES' if o['food_ready'] else 'no'}"89            f" | driver_arrived={'YES' if o['driver_arrived'] else 'no'}"90        )91        if o["driver_arrived"] and not o["food_ready"]:92            lines.append(f"    WARNING: DRIVER WAITING {o['minutes_driver_waited']}min")93        if o["food_ready"] and not o["driver_summoned"]:94            lines.append(f"    WARNING: FOOD READY but NO driver summoned")95 96    if obs.get("last_action_feedback") and obs.get("attempts", 0) > 1:97        lines += ["", f"LAST EVENT: {obs['last_action_feedback'][-200:]}"]98 99    lines += ["", "Your JSON decision (include all active order IDs):"]100    return "\n".join(lines)101 102 103def call_llm(client: OpenAI, prompt: str) -> dict:104    completion = client.chat.completions.create(105        model=MODEL_NAME,106        messages=[107            {"role": "system", "content": SYSTEM_PROMPT},108            {"role": "user",   "content": prompt},109        ],110        temperature=TEMPERATURE,111        max_tokens=MAX_TOKENS,112    )113    text = (completion.choices[0].message.content or "").strip()114    if text.startswith("```"):115        lines = text.splitlines()116        text  = "\n".join(l for l in lines if not l.strip().startswith("```")).strip()117    try:118        return json.loads(text)119    except json.JSONDecodeError:120        return {"dispatch_decisions": {}}121 122 123# ── Score clamper — validator requires strictly (0, 1), never 0.0 or 1.0 ──────124 125def _clamp(score: float) -> float:126    """Clamp score to strictly open interval (0, 1): never 0.0, never 1.0."""127    return max(0.001, min(0.999, float(score)))128 129 130# ── HTTP Client ───────────────────────────────────────────────────────────────131 132class EnvClient:133    def __init__(self, base_url: str):134        self._url = base_url.rstrip("/")135        self._episode_id: Optional[str] = None136 137    def _post(self, path: str, body: dict) -> dict:138        data = json.dumps(body).encode()139        req  = urllib.request.Request(140            f"{self._url}{path}", data=data,141            headers={"Content-Type": "application/json"},142        )143        try:144            with urllib.request.urlopen(req, timeout=30) as r:145                return json.loads(r.read())146        except urllib.error.HTTPError as e:147            raise RuntimeError(f"HTTP {e.code}: {e.read().decode()[:200]}") from e148 149    def _get(self, path: str) -> dict:150        with urllib.request.urlopen(f"{self._url}{path}", timeout=10) as r:151            return json.loads(r.read())152 153    def reset(self, task_id: Optional[str] = None) -> dict:154        body: dict = {}155        if task_id:156            body["task_id"] = task_id157        obs = self._post("/reset", body)158        self._episode_id = obs.get("episode_id")159        return obs160 161    def step(self, action: dict) -> dict:162        body: dict = {"action": action}163        if self._episode_id:164            body["episode_id"] = self._episode_id165        return self._post("/step", body)166 167    def tasks(self) -> list:168        try:169            return [t["task_id"] for t in self._get("/tasks").get("tasks", [])]170        except Exception:171            return [172                "T1_single_order_dispatch",173                "T2_multi_order_coordination",174                "T3_peak_hour_rush",175            ]176 177 178# ── Task runner ───────────────────────────────────────────────────────────────179 180def run_task(client: OpenAI, env: EnvClient, task_id: str) -> float:181    obs      = env.reset(task_id=task_id)182    max_mins = obs.get("max_time_min", 30)183 184    # Required structured output: START185    print(f"[START] task={task_id}", flush=True)186 187    best_score = 0.0188    step_num   = 0189 190    for minute in range(1, max_mins + 1):191        if obs.get("done", False):192            break193 194        step_num += 1195        prompt = build_prompt(obs)196 197        try:198            action_data = call_llm(client, prompt)199        except Exception:200            action_data = {"dispatch_decisions": {}}201 202        try:203            obs = env.step(action_data)204        except RuntimeError:205            score_out = _clamp(best_score)206            print(f"[STEP] step={step_num} reward=0.0010 score={score_out:.4f} done=true", flush=True)207            break208 209        reward     = obs.get("reward", 0.0)210        score      = _clamp(obs.get("score", 0.0))211        done       = obs.get("done", False)212        best_score = max(best_score, score)213 214        # Required structured output: STEP215        print(216            f"[STEP] step={step_num} reward={reward:.4f} score={score:.4f} "217            f"done={'true' if done else 'false'}",218            flush=True,219        )220 221        if done:222            best_score = max(best_score, score)223            break224 225    final_score = _clamp(best_score)226 227    # Required structured output: END228    print(f"[END] task={task_id} score={final_score:.4f} steps={step_num}", flush=True)229 230    return final_score231 232 233# ── Main ──────────────────────────────────────────────────────────────────────234 235def main():236    parser = argparse.ArgumentParser(description="KitchenFlow-v1 baseline agent")237    parser.add_argument("--url",  default=ENV_URL)238    parser.add_argument("--task", default=None)239    args = parser.parse_args()240 241    if not API_KEY:242        print("ERROR: Set HF_TOKEN environment variable", file=sys.stderr, flush=True)243        sys.exit(1)244 245    llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)246    env = EnvClient(args.url)247 248    task_ids = [args.task] if args.task else env.tasks()249    scores   = {}250    start    = time.time()251 252    for tid in task_ids:253        try:254            scores[tid] = run_task(llm, env, tid)255        except Exception as exc:256            # Still emit valid structured blocks even on error257            fallback = _clamp(0.001)258            print(f"[START] task={tid}", flush=True)259            print(f"[STEP] step=1 reward=0.0010 score={fallback:.4f} done=true", flush=True)260            print(f"[END] task={tid} score={fallback:.4f} steps=1", flush=True)261            scores[tid] = fallback262 263    elapsed = time.time() - start264    avg     = sum(scores.values()) / len(scores) if scores else 0.0265 266    print(f"\nAverage score: {avg:.4f} | Time: {elapsed:.1f}s", flush=True)267    sys.exit(0 if avg > 0 else 1)268 269 270if __name__ == "__main__":271    main()