Spaces:
Sleeping
Sleeping
| """ | |
| baseline.py — LLM-powered baseline using the OpenAI API. | |
| This script satisfies the hackathon requirement: | |
| "Uses the OpenAI API client to run a model against the environment. | |
| Reads API credentials from environment variables (OPENAI_API_KEY). | |
| Produces a reproducible baseline score on all 3 tasks." | |
| ══════════════════════════════════════════════════════════════ | |
| HOW TO SET YOUR OPENAI API KEY | |
| ══════════════════════════════════════════════════════════════ | |
| Option A — terminal (recommended, key is never in your code): | |
| export OPENAI_API_KEY="sk-proj-xxxxxxxxxxxxxxxxxxxx" | |
| python baseline.py | |
| Option B — .env file (create a file called .env in this folder): | |
| OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxx | |
| Then run: python baseline.py | |
| Option C — Windows Command Prompt: | |
| set OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxx | |
| python baseline.py | |
| NEVER paste your API key directly into this file and commit it to git. | |
| ══════════════════════════════════════════════════════════════ | |
| USAGE | |
| ══════════════════════════════════════════════════════════════ | |
| python baseline.py # all 3 tasks, gpt-4o-mini | |
| python baseline.py --task easy # single task | |
| python baseline.py --model gpt-4o # use a different model | |
| python baseline.py --seeds 5 # average over 5 seeds | |
| python baseline.py --task hard --seeds 3 --model gpt-4o-mini | |
| Output is also written to baseline_scores.json for CI / reproducibility. | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import argparse | |
| import random | |
| # --- Load .env file if present (optional convenience) ------- | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| except ImportError: | |
| pass # python-dotenv not installed — that is fine, use export instead | |
| # -- OpenAI client ---------------------------- | |
| try: | |
| from openai import OpenAI | |
| except ImportError: | |
| print("ERROR: openai package not installed.") | |
| print("Run: pip install openai") | |
| sys.exit(1) | |
| # -- Environment imports (dual-import for local + Docker) | |
| sys.path.insert(0, os.path.dirname(__file__)) | |
| try: | |
| from server.traffic_environment import TrafficEnvironment | |
| from models import TrafficAction, ActionType | |
| except ImportError: | |
| from traffic_env.server.traffic_environment import TrafficEnvironment | |
| from traffic_env.models import TrafficAction, ActionType | |
| # ---------------------------- | |
| # SYSTEM PROMPT | |
| # This is the instruction we give the LLM at the start of | |
| # every conversation. It tells it what it is and how to respond. | |
| # ---------------------------- | |
| SYSTEM_PROMPT = """You are an adaptive traffic signal controller agent. | |
| At each step you receive sensor data from traffic intersections and must | |
| choose one action to keep traffic flowing smoothly. | |
| ALWAYS respond with ONLY a valid JSON object. No explanation, no markdown, | |
| no code blocks. Just the raw JSON. | |
| Response format: | |
| {"action_type": "extend_green", "intersection_id": 0} | |
| Rules: | |
| - action_type must be exactly "extend_green" OR "next_phase" | |
| - intersection_id must be an integer (0-indexed) | |
| - extend_green = hold current green phase 5 extra seconds | |
| - next_phase = switch to next signal phase immediately | |
| Strategy hints: | |
| - If a lane has queue_length > 8, it is critically congested | |
| - extend_green when the current green lanes are heavily loaded | |
| - next_phase when a different direction has higher queue lengths | |
| - Always target the intersection with the most total waiting cars""" | |
| # ---------------------------- | |
| # OBSERVATION → PROMPT CONVERTER | |
| # Turns a TrafficObservation Pydantic object into a plain | |
| # English string the LLM can read and reason about. | |
| # ---------------------------- | |
| def obs_to_prompt(obs) -> str: | |
| """Convert observation to a compact human-readable string for the LLM.""" | |
| lines = [] | |
| for inter in obs.intersections: | |
| lane_details = " | ".join( | |
| f"{l.lane_id}: queue={l.queue_length} wait={l.avg_wait_time:.1f}s" | |
| for l in inter.lanes | |
| ) | |
| lines.append( | |
| f"Intersection {inter.intersection_id} " | |
| f"[phase={inter.current_phase}, elapsed={inter.phase_elapsed:.0f}s]: " | |
| f"{lane_details}" | |
| ) | |
| lines.append("") | |
| lines.append(f"Total vehicles waiting : {obs.total_waiting_vehicles}") | |
| lines.append(f"Fleet avg wait time : {obs.total_avg_wait:.1f}s") | |
| lines.append(f"Vehicles cleared (last): {obs.throughput_last_step}") | |
| return "\n".join(lines) | |
| # ---------------------------- | |
| # LLM POLICY | |
| # Calls the OpenAI API with the current observation and | |
| # parses the action from the JSON response. | |
| # ---------------------------- | |
| def llm_policy(obs, n_intersections: int, client: OpenAI, model: str) -> TrafficAction: | |
| """ | |
| Ask the LLM what action to take given the current observation. | |
| Falls back to a random action if the LLM response cannot be parsed. | |
| """ | |
| prompt = obs_to_prompt(obs) | |
| try: | |
| response = client.chat.completions.create( | |
| model=model, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| temperature=0.0, # 0.0 = deterministic, same input → same output | |
| max_tokens=60, # We only need a small JSON object | |
| ) | |
| raw_text = response.choices[0].message.content.strip() | |
| # Strip markdown code fences if the model adds them despite instructions | |
| raw_text = raw_text.replace("```json", "").replace("```", "").strip() | |
| data = json.loads(raw_text) | |
| return TrafficAction( | |
| action_type=ActionType(data["action_type"]), | |
| intersection_id=int(data.get("intersection_id", 0)), | |
| ) | |
| except Exception as e: | |
| # If the LLM gives a bad response, fall back to random | |
| # This prevents a single bad response from crashing the whole run | |
| print(f" [WARN] LLM parse failed ({e}), using random fallback") | |
| return TrafficAction( | |
| action_type=random.choice(list(ActionType)), | |
| intersection_id=random.randint(0, n_intersections - 1), | |
| ) | |
| # ---------------------------- | |
| # EPISODE RUNNER | |
| # ---------------------------- | |
| def run_episode(task_level: str, client: OpenAI, model: str, seed: int) -> dict: | |
| """Run one complete episode and return metrics.""" | |
| env = TrafficEnvironment(task_level=task_level) | |
| obs = env.reset(seed=seed) | |
| n = len(obs.intersections) | |
| step = 0 | |
| while not obs.done: | |
| action = llm_policy(obs, n, client, model) | |
| obs = env.step(action) | |
| step += 1 | |
| if step % 20 == 0: | |
| print(f" step {step:3d} | waiting={obs.total_waiting_vehicles:3d} " | |
| f"| reward={obs.reward:+.3f}") | |
| return { | |
| "cumulative_reward": env.state.cumulative_reward, | |
| "cumulative_throughput": env.state.cumulative_throughput, | |
| "steps": step, | |
| } | |
| # --------------------------- | |
| # SCORING | |
| # Normalise agent score against known random/oracle baselines. | |
| # These constants were measured empirically over 20 seeds. | |
| # ---------------------------- | |
| # (random_baseline, oracle_baseline) per task | |
| BASELINES = { | |
| "easy": (-0.12, 0.68), | |
| "medium": (-0.18, 0.58), | |
| "hard": (-0.22, 0.46), | |
| } | |
| def compute_score(task_level: str, avg_reward: float) -> float: | |
| lo, hi = BASELINES[task_level] | |
| raw = (avg_reward - lo) / (hi - lo + 1e-9) | |
| return round(min(max(raw, 0.0), 1.0), 4) | |
| def score_task(task_level: str, client: OpenAI, model: str, n_seeds: int) -> float: | |
| rewards = [] | |
| for seed in range(n_seeds): | |
| print(f" seed {seed}:") | |
| result = run_episode(task_level, client, model, seed=seed) | |
| rewards.append(result["cumulative_reward"]) | |
| print(f" → cumulative_reward={result['cumulative_reward']:.4f}") | |
| avg = sum(rewards) / len(rewards) | |
| return compute_score(task_level, avg) | |
| # ---------------------------- | |
| # MAIN | |
| # --------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Run LLM agent baseline against Traffic Flow environment" | |
| ) | |
| parser.add_argument( | |
| "--task", choices=["easy", "medium", "hard", "all"], default="all", | |
| help="Which task(s) to evaluate" | |
| ) | |
| parser.add_argument( | |
| "--model", default="gpt-4o-mini", | |
| help="OpenAI model name (default: gpt-4o-mini)" | |
| ) | |
| parser.add_argument( | |
| "--seeds", type=int, default=3, | |
| help="Number of seeds to average over (default: 3)" | |
| ) | |
| args = parser.parse_args() | |
| # -- API key check --------------------------- | |
| api_key = os.environ.get("OPENAI_API_KEY") | |
| if not api_key: | |
| print("\nERROR: OPENAI_API_KEY environment variable is not set.") | |
| print("\nHow to fix:") | |
| print(" export OPENAI_API_KEY='sk-proj-xxxxxxxxxxxxxxxxxxxx'") | |
| print(" python baseline.py") | |
| sys.exit(1) | |
| client = OpenAI(api_key=api_key) | |
| tasks = ["easy", "medium", "hard"] if args.task == "all" else [args.task] | |
| results = {} | |
| print(f"\n{'='*60}") | |
| print(f" Smart City Traffic Flow — LLM Baseline Evaluation") | |
| print(f" Model : {args.model}") | |
| print(f" Seeds : {args.seeds}") | |
| print(f"{'='*60}\n") | |
| for task in tasks: | |
| print(f"Task: {task.upper()}") | |
| print("-" * 40) | |
| score = score_task(task, client, args.model, args.seeds) | |
| results[task] = score | |
| bar = "█" * int(score * 30) | |
| print(f"\n SCORE = {score:.4f} |{bar}\n") | |
| # --- Summary ---------------------------- | |
| print(f"{'='*60}") | |
| print(" FINAL SCORES") | |
| print(f"{'='*60}") | |
| for task, score in results.items(): | |
| bar = "█" * int(score * 30) | |
| print(f" {task:<8} {score:.4f} |{bar}") | |
| print(f"{'='*60}\n") | |
| # ---- Save to JSON ---------------------------- | |
| output = { | |
| "model": args.model, | |
| "seeds": args.seeds, | |
| "scores": results, | |
| } | |
| with open("baseline_scores.json", "w") as f: | |
| json.dump(output, f, indent=2) | |
| print("Scores saved to baseline_scores.json") | |
| if __name__ == "__main__": | |
| main() | |