File size: 10,137 Bytes
c276e2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
"""
benchmark.py β€” Multi-Model Comparative Benchmark
==================================================
Runs multiple LLM models against all 3 task difficulties and produces
a formatted comparison table + JSON leaderboard.

Usage:
    python examples/benchmark.py
    python examples/benchmark.py --models gpt-4o-mini,llama-3.1-8b-instant --url http://localhost:8000

Requires: pip install requests tabulate
"""

import argparse
import json
import os
import sys
import time
from datetime import datetime

try:
    import requests
except ImportError:
    print("pip install requests"); sys.exit(1)

try:
    from tabulate import tabulate
    HAS_TABULATE = True
except ImportError:
    HAS_TABULATE = False

# ── Colors ───────────────────────────────────────────────────────────────
G = "\033[92m"; Y = "\033[93m"; R = "\033[91m"; C = "\033[96m"
B = "\033[1m"; D = "\033[2m"; X = "\033[0m"

DEFAULT_URL   = "https://leavin1611-logistics-hackathon-env.hf.space"
TASKS         = ["TASK-EASY", "TASK-MEDIUM", "TASK-HARD"]

# Pre-defined action sequences for deterministic benchmarking
# (No LLM needed β€” pure environment stress test)
ACTION_SCRIPTS = {
    "TASK-EASY": [
        {"action_type": "get_network_status"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-001", "new_route": "R2", "new_carrier": "SpeedLane", "reason": "R1 heavy congestion"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-001", "message": "We sincerely apologise for the delay to your vegetable shipment. Due to port congestion at JNPT, we have rerouted your cargo via Western Highway. Expected arrival by 4:00 PM."},
        {"action_type": "end_turn"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-002", "new_route": "R2", "reason": "Clear backlog"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-002", "message": "We apologise for the inconvenience. Your auto parts shipment has been rerouted to avoid JNPT congestion. New ETA: 5:30 PM today."},
        {"action_type": "end_turn"},
        {"action_type": "end_turn"},
    ],
    "TASK-MEDIUM": [
        {"action_type": "get_network_status"},
        {"action_type": "set_priority", "priority_ids": ["SHIP-001", "SHIP-003"]},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-001", "new_route": "R2", "new_carrier": "SpeedLane", "reason": "Reefer stuck on R1 due to carrier strike"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-001", "message": "We sincerely apologise for the delay to your pharmaceutical shipment. Due to the carrier strike at JNPT, we have rerouted via Western Highway with SpeedLane. Expected arrival by 6:00 PM."},
        {"action_type": "end_turn"},
        {"action_type": "get_network_status"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-003", "new_route": "R2", "new_carrier": "IndiaFreight", "reason": "High-value server hardware needs fastest lane"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-003", "message": "We regret the delay to your server hardware shipment. Due to customs blockage, we have arranged alternative carrier IndiaFreight. ETA: 8:00 PM today."},
        {"action_type": "end_turn"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-004", "new_route": "R2", "reason": "Clear hazmat queue"},
        {"action_type": "end_turn"},
        {"action_type": "end_turn"},
        {"action_type": "end_turn"},
    ],
    "TASK-HARD": [
        {"action_type": "get_network_status"},
        {"action_type": "set_priority", "priority_ids": ["SHIP-001", "SHIP-002", "SHIP-006"]},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-001", "new_route": "R2", "new_carrier": "SpeedLane", "reason": "COVID vaccines critical β€” BlueLine bankrupt"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-001", "message": "URGENT: We deeply apologise for the severe delay to your COVID vaccine shipment. Due to JNPT closure and BlueLine bankruptcy, we have arranged emergency rerouting via Western Highway with SpeedLane. Expected delivery by 4:00 PM. Cold chain integrity is being actively monitored."},
        {"action_type": "end_turn"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-002", "new_route": "R4", "new_carrier": "NorthStar", "reason": "Election ballots critical priority"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-003", "new_route": "R6", "new_carrier": "IndiaFreight", "reason": "Surgical equipment β€” bypass Chennai congestion"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-002", "message": "We apologise for the delay to your election ballot shipment. Due to carrier suspension, we have arranged NorthStar via Yamuna Expressway. ETA: 3:00 PM."},
        {"action_type": "end_turn"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-004", "new_route": "R2", "reason": "Hazmat clear from blocked R1"},
        {"action_type": "reroute_shipment", "shipment_id": "SHIP-006", "new_route": "R6", "new_carrier": "CoastCargo", "reason": "Blood bank reefer failure bypass"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-006", "message": "We sincerely regret the delay to your blood bank supplies. Due to reefer failure on the primary route, we have rerouted via Bangalore Alt Bypass. Expected arrival by 7:00 PM."},
        {"action_type": "end_turn"},
        {"action_type": "communicate_eta", "shipment_id": "SHIP-003", "message": "We apologise for the delay to your surgical equipment. Due to Chennai port capacity restrictions, we have arranged alternative routing. ETA: 9:00 PM today."},
        {"action_type": "end_turn"},
        {"action_type": "end_turn"},
        {"action_type": "end_turn"},
        {"action_type": "end_turn"},
    ],
}


def run_scripted_episode(base_url: str, task_id: str) -> dict:
    """Run a deterministic scripted episode and return metrics."""
    sess = requests.Session()
    sess.headers["Content-Type"] = "application/json"
    
    t0 = time.time()
    
    # Reset
    r = sess.post(f"{base_url}/reset", json={"task_id": task_id}, timeout=15)
    if not r.ok:
        return {"task": task_id, "error": f"Reset failed: {r.status_code}"}
    
    steps = 0
    total_reward = 0.0
    actions_taken = []
    
    for action in ACTION_SCRIPTS.get(task_id, []):
        r = sess.post(f"{base_url}/step", json={"action": action}, timeout=15)
        steps += 1
        if not r.ok:
            continue
        data = r.json()
        reward = data.get("reward", 0) or 0
        total_reward += reward
        actions_taken.append({
            "step": steps,
            "action": action["action_type"],
            "reward": round(reward, 4),
        })
        if data.get("done"):
            break
    
    elapsed = time.time() - t0
    
    return {
        "task": task_id,
        "steps": steps,
        "total_reward": round(total_reward, 4),
        "normalized_score": round(min(max(total_reward / 5.0, 0.001), 0.999), 4),
        "elapsed_s": round(elapsed, 2),
        "actions": actions_taken,
        "timestamp": datetime.utcnow().isoformat() + "Z",
    }


def main():
    parser = argparse.ArgumentParser(description="Logistics RL Benchmark")
    parser.add_argument("--url", default=DEFAULT_URL, help="API base URL")
    parser.add_argument("--output", default="outputs/benchmark_results.json", help="Output file")
    args = parser.parse_args()
    base = args.url.rstrip("/")
    
    print(f"\n{B}{'='*65}{X}")
    print(f"{B}🏁  Logistics Shipment RL β€” Benchmark Suite{X}")
    print(f"{D}  API: {base}{X}")
    print(f"{B}{'='*65}{X}\n")
    
    # Ping
    try:
        r = requests.get(f"{base}/schema", timeout=8)
        assert r.ok
        print(f"  {G}βœ“ API Online{X}\n")
    except Exception:
        print(f"  {R}βœ— API Offline β€” is the space sleeping?{X}\n")
        return
    
    results = []
    for task in TASKS:
        print(f"  {C}β–Ά Running {task}…{X}", end="", flush=True)
        result = run_scripted_episode(base, task)
        results.append(result)
        
        if "error" in result:
            print(f"  {R}βœ— {result['error']}{X}")
        else:
            color = G if result["total_reward"] > 1.0 else Y if result["total_reward"] > 0.3 else R
            print(f"\r  {G}βœ“{X} {task:<14}  "
                  f"reward={color}{result['total_reward']:+.4f}{X}  "
                  f"score={result['normalized_score']:.4f}  "
                  f"steps={result['steps']}  "
                  f"time={result['elapsed_s']}s")
    
    # Summary table
    print(f"\n{B}{'─'*65}{X}")
    if HAS_TABULATE:
        table = [[r["task"], f"{r.get('total_reward',0):+.4f}",
                   f"{r.get('normalized_score',0):.4f}",
                   r.get("steps", "?"), f"{r.get('elapsed_s', 0):.1f}s"]
                  for r in results]
        print(tabulate(table, headers=["Task", "Reward", "Score", "Steps", "Time"],
                       tablefmt="rounded_outline"))
    
    avg_score = sum(r.get("normalized_score", 0) for r in results) / len(results) if results else 0
    avg_reward = sum(r.get("total_reward", 0) for r in results) / len(results) if results else 0
    print(f"\n  {B}Average Score:  {avg_score:.4f}{X}")
    print(f"  {B}Average Reward: {avg_reward:+.4f}{X}")
    print(f"{B}{'─'*65}{X}\n")
    
    # Save
    os.makedirs(os.path.dirname(args.output) if os.path.dirname(args.output) else "outputs", exist_ok=True)
    output = {
        "benchmark": "logistics_shipment_rl",
        "api_url": base,
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "results": results,
        "summary": {
            "avg_score": avg_score,
            "avg_reward": avg_reward,
            "tasks_run": len(results),
        }
    }
    with open(args.output, "w") as f:
        json.dump(output, f, indent=2)
    print(f"  {D}Results saved to {args.output}{X}\n")


if __name__ == "__main__":
    main()