Commit Β·
d0e61c2
1
Parent(s): 864a7e4
Phase 2 validation
Browse files- Dockerfile +13 -11
- check_graders.py +10 -19
- core/tasks.py +3 -3
- data/memory.json +0 -0
- graders.py +57 -0
- graders/__init__.py +3 -3
- inference.py +109 -100
- models.py +2 -2
- openenv.yaml +3 -3
- server/app.py +32 -14
- server/grid_world_environment.py +17 -6
- server/static/index.html +4 -4
- server/static/script.js +5 -5
Dockerfile
CHANGED
|
@@ -1,11 +1,9 @@
|
|
| 1 |
-
# Use official lightweight Python image
|
| 2 |
# REBUILD_TIMESTAMP: 2026-04-07 23:30 (Phase2 Fix: /health=healthy, /metadata, /schema, /mcp, httpx)
|
| 3 |
-
FROM
|
| 4 |
|
| 5 |
-
# Set working directory
|
| 6 |
WORKDIR /app
|
| 7 |
|
| 8 |
-
# Install system dependencies
|
| 9 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
git \
|
| 11 |
&& rm -rf /var/lib/apt/lists/*
|
|
@@ -14,17 +12,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 14 |
COPY . .
|
| 15 |
|
| 16 |
# Install the package and its dependencies
|
| 17 |
-
# pip install -e . uses the pyproject.toml in the current directory
|
| 18 |
RUN pip install --no-cache-dir -e .
|
| 19 |
RUN pip install --no-cache-dir uvicorn fastapi
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
EXPOSE 8000
|
| 23 |
-
|
| 24 |
-
# Set environment variables for better logging
|
| 25 |
ENV PYTHONUNBUFFERED=1
|
| 26 |
ENV HF_HOME=/tmp/.cache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# Command to run the FastAPI server
|
| 29 |
-
#
|
| 30 |
-
CMD ["
|
|
|
|
|
|
|
| 1 |
# REBUILD_TIMESTAMP: 2026-04-07 23:30 (Phase2 Fix: /health=healthy, /metadata, /schema, /mcp, httpx)
|
| 2 |
+
FROM ghcr.io/meta-pytorch/openenv-base:latest
|
| 3 |
|
|
|
|
| 4 |
WORKDIR /app
|
| 5 |
|
| 6 |
+
# Install system dependencies
|
| 7 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 8 |
git \
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
| 12 |
COPY . .
|
| 13 |
|
| 14 |
# Install the package and its dependencies
|
|
|
|
| 15 |
RUN pip install --no-cache-dir -e .
|
| 16 |
RUN pip install --no-cache-dir uvicorn fastapi
|
| 17 |
|
| 18 |
+
# Set environment variables for better logging and robustness
|
|
|
|
|
|
|
|
|
|
| 19 |
ENV PYTHONUNBUFFERED=1
|
| 20 |
ENV HF_HOME=/tmp/.cache
|
| 21 |
+
ENV PYTHONPATH="/app:$PYTHONPATH"
|
| 22 |
+
|
| 23 |
+
# Expose the API port
|
| 24 |
+
EXPOSE 8000
|
| 25 |
+
|
| 26 |
+
# Health check
|
| 27 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 28 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 29 |
|
| 30 |
# Command to run the FastAPI server
|
| 31 |
+
# Running from /app ensures server.app:app resolves correctly
|
| 32 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
check_graders.py
CHANGED
|
@@ -42,28 +42,19 @@ def check_graders():
|
|
| 42 |
|
| 43 |
module_path, func_name = grader_str.split(":")
|
| 44 |
|
| 45 |
-
|
| 46 |
-
# Try full path first (as it will be in the platform)
|
| 47 |
try:
|
| 48 |
module = importlib.import_module(module_path)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
| 57 |
|
| 58 |
-
if module:
|
| 59 |
-
func = getattr(module, func_name, None)
|
| 60 |
-
if func and callable(func):
|
| 61 |
-
print(f" β
SUCCESS: Found {func_name} in {module.__name__}")
|
| 62 |
-
valid_count += 1
|
| 63 |
-
else:
|
| 64 |
-
print(f" β {func_name} not found or not callable in {module.__name__}")
|
| 65 |
-
else:
|
| 66 |
-
pass # Already printed error
|
| 67 |
except Exception as e:
|
| 68 |
print(f" β Error: {e}")
|
| 69 |
|
|
|
|
| 42 |
|
| 43 |
module_path, func_name = grader_str.split(":")
|
| 44 |
|
| 45 |
+
# Simple direct import (no fallback, must be robust)
|
|
|
|
| 46 |
try:
|
| 47 |
module = importlib.import_module(module_path)
|
| 48 |
+
if module:
|
| 49 |
+
func = getattr(module, func_name, None)
|
| 50 |
+
if func and callable(func):
|
| 51 |
+
print(f" β
SUCCESS: Found {func_name} in {module.__name__}")
|
| 52 |
+
valid_count += 1
|
| 53 |
+
else:
|
| 54 |
+
print(f" β {func_name} not found or not callable in {module.__name__}")
|
| 55 |
+
except ImportError as e:
|
| 56 |
+
print(f" β Module import failed: {e}")
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
except Exception as e:
|
| 59 |
print(f" β Error: {e}")
|
| 60 |
|
core/tasks.py
CHANGED
|
@@ -4,7 +4,7 @@ Mission configurations for Drone Delivery missions.
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
TASK_CONFIG = {
|
| 7 |
-
"
|
| 8 |
"width": 10,
|
| 9 |
"height": 10,
|
| 10 |
"n_buildings": 4,
|
|
@@ -24,7 +24,7 @@ TASK_CONFIG = {
|
|
| 24 |
"r_wall": 0.10,
|
| 25 |
"r_blocked": 0.10,
|
| 26 |
},
|
| 27 |
-
"
|
| 28 |
"width": 14,
|
| 29 |
"height": 14,
|
| 30 |
"n_buildings": 8,
|
|
@@ -44,7 +44,7 @@ TASK_CONFIG = {
|
|
| 44 |
"r_wall": 0.15,
|
| 45 |
"r_blocked": 0.15,
|
| 46 |
},
|
| 47 |
-
"
|
| 48 |
"width": 18,
|
| 49 |
"height": 18,
|
| 50 |
"n_buildings": 12,
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
TASK_CONFIG = {
|
| 7 |
+
"graders:grade_easy": {
|
| 8 |
"width": 10,
|
| 9 |
"height": 10,
|
| 10 |
"n_buildings": 4,
|
|
|
|
| 24 |
"r_wall": 0.10,
|
| 25 |
"r_blocked": 0.10,
|
| 26 |
},
|
| 27 |
+
"graders:grade_medium": {
|
| 28 |
"width": 14,
|
| 29 |
"height": 14,
|
| 30 |
"n_buildings": 8,
|
|
|
|
| 44 |
"r_wall": 0.15,
|
| 45 |
"r_blocked": 0.15,
|
| 46 |
},
|
| 47 |
+
"graders:grade_hard": {
|
| 48 |
"width": 18,
|
| 49 |
"height": 18,
|
| 50 |
"n_buildings": 12,
|
data/memory.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
graders.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unified graders for Drone Delivery OpenEnv.
|
| 3 |
+
All grading functions are consolidated here for reliable importing during validation.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
def _get_attr(state, key, default=0):
|
| 7 |
+
"""Safely get an attribute from a state object or dict."""
|
| 8 |
+
if isinstance(state, dict):
|
| 9 |
+
return state.get(key, default)
|
| 10 |
+
return getattr(state, key, default)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def compute_grade(state, max_steps: float) -> float:
|
| 14 |
+
"""
|
| 15 |
+
Unified grade calculation:
|
| 16 |
+
- 80% weighted by deliveries completed.
|
| 17 |
+
- 20% weighted by efficiency (remaining battery and steps).
|
| 18 |
+
"""
|
| 19 |
+
deliveries_total = _get_attr(state, "deliveries_total", 0)
|
| 20 |
+
deliveries_done = _get_attr(state, "deliveries_done", 0)
|
| 21 |
+
battery = _get_attr(state, "battery", 1.0)
|
| 22 |
+
step_count = _get_attr(state, "step_count", 0)
|
| 23 |
+
|
| 24 |
+
if deliveries_total == 0:
|
| 25 |
+
return 0.5
|
| 26 |
+
|
| 27 |
+
delivery_ratio = deliveries_done / deliveries_total
|
| 28 |
+
efficiency = float(battery) * 0.5 + (1.0 - (float(step_count) / float(max_steps))) * 0.5
|
| 29 |
+
efficiency = max(0.0, min(1.0, efficiency))
|
| 30 |
+
|
| 31 |
+
score = (delivery_ratio * 0.8) + (efficiency * 0.2)
|
| 32 |
+
if deliveries_done < deliveries_total:
|
| 33 |
+
score = min(score, 0.49)
|
| 34 |
+
|
| 35 |
+
return max(0.01, min(0.99, float(score)))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def grade_easy(state) -> float:
|
| 39 |
+
"""Grader for easy_delivery task (10x10 grid, 1 delivery, 60 max steps)."""
|
| 40 |
+
return compute_grade(state, 60.0)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def grade_medium(state) -> float:
|
| 44 |
+
"""Grader for medium_delivery task (14x14 grid, 3 deliveries, 100 max steps)."""
|
| 45 |
+
return compute_grade(state, 100.0)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def grade_hard(state) -> float:
|
| 49 |
+
"""Grader for hard_delivery task (18x18 grid, 5 deliveries, 160 max steps)."""
|
| 50 |
+
return compute_grade(state, 160.0)
|
| 51 |
+
|
| 52 |
+
# --- Standardization Mapping ---
|
| 53 |
+
GRADERS = {
|
| 54 |
+
"graders:grade_easy": grade_easy,
|
| 55 |
+
"graders:grade_medium": grade_medium,
|
| 56 |
+
"graders:grade_hard": grade_hard,
|
| 57 |
+
}
|
graders/__init__.py
CHANGED
|
@@ -3,9 +3,9 @@ from .medium import grade_medium
|
|
| 3 |
from .hard import grade_hard
|
| 4 |
|
| 5 |
GRADERS = {
|
| 6 |
-
"
|
| 7 |
-
"
|
| 8 |
-
"
|
| 9 |
}
|
| 10 |
|
| 11 |
__all__ = ["grade_easy", "grade_medium", "grade_hard", "GRADERS"]
|
|
|
|
| 3 |
from .hard import grade_hard
|
| 4 |
|
| 5 |
GRADERS = {
|
| 6 |
+
"graders:grade_easy": grade_easy,
|
| 7 |
+
"graders:grade_medium": grade_medium,
|
| 8 |
+
"graders:grade_hard": grade_hard,
|
| 9 |
}
|
| 10 |
|
| 11 |
__all__ = ["grade_easy", "grade_medium", "grade_hard", "GRADERS"]
|
inference.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
SkyRelic Drone Delivery: Standardized Inference Script
|
| 3 |
===================================================
|
| 4 |
MANDATORY
|
| 5 |
-
-
|
| 6 |
- STDOUT FORMAT: [START], [STEP], [END]
|
| 7 |
- Participants must use OpenAI Client for all LLM calls.
|
| 8 |
"""
|
|
@@ -10,101 +10,103 @@ MANDATORY
|
|
| 10 |
import asyncio
|
| 11 |
import os
|
| 12 |
import textwrap
|
|
|
|
| 13 |
import sys
|
| 14 |
from typing import List, Optional
|
| 15 |
from pathlib import Path
|
| 16 |
-
|
| 17 |
from openai import OpenAI
|
| 18 |
|
| 19 |
-
#
|
| 20 |
ROOT_DIR = Path(__file__).parent
|
| 21 |
if str(ROOT_DIR) not in sys.path:
|
| 22 |
sys.path.insert(0, str(ROOT_DIR))
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
line = line.strip()
|
| 29 |
-
if not line or line.startswith("#") or "=" not in line:
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
-
from drone_env.server.grid_world_environment import DroneDeliveryEnvironment
|
| 36 |
-
from drone_env.models import DroneAction, DroneObservation
|
| 37 |
|
| 38 |
# --- Configuration ------------------------------------------------------------
|
| 39 |
-
IMAGE_NAME = os.getenv("
|
| 40 |
-
# Use a placeholder if no key is found to prevent initialization crash during local tests
|
| 41 |
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY") or "EMPTY_KEY"
|
| 42 |
-
|
| 43 |
-
# Defaults are set only for API_BASE_URL and MODEL_NAME as per requirements
|
| 44 |
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 45 |
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-7B-Instruct"
|
| 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 |
# --- Logging Helpers ---------------------------------------------------------
|
|
|
|
| 77 |
def log_start(task: str, env: str, model: str) -> None:
|
| 78 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 79 |
|
| 80 |
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 81 |
error_val = error if error else "null"
|
| 82 |
-
|
| 83 |
-
# Format reward to 2 decimal places as per requirement
|
| 84 |
-
print(
|
| 85 |
-
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
|
| 86 |
-
flush=True,
|
| 87 |
-
)
|
| 88 |
|
| 89 |
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 90 |
-
# Format rewards to 2 decimal places as per requirement
|
| 91 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 92 |
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 93 |
|
| 94 |
# --- Agent Logic -------------------------------------------------------------
|
| 95 |
-
def build_user_prompt(obs: DroneObservation) -> str:
|
| 96 |
-
return textwrap.dedent(
|
| 97 |
-
f"""
|
| 98 |
-
Pos: ({obs.drone_x}, {obs.drone_y})
|
| 99 |
-
Battery: {obs.battery:.2f}
|
| 100 |
-
Target: {obs.current_target}
|
| 101 |
-
Distance: {obs.distance_to_target:.1f}
|
| 102 |
-
Status: {obs.message}
|
| 103 |
-
Available Actions: UP, DOWN, LEFT, RIGHT, WAIT
|
| 104 |
-
"""
|
| 105 |
-
).strip()
|
| 106 |
|
| 107 |
-
def
|
| 108 |
user_prompt = build_user_prompt(obs)
|
| 109 |
try:
|
| 110 |
completion = client.chat.completions.create(
|
|
@@ -114,70 +116,77 @@ def get_model_action(client: OpenAI, obs: DroneObservation) -> str:
|
|
| 114 |
{"role": "user", "content": user_prompt},
|
| 115 |
],
|
| 116 |
temperature=TEMPERATURE,
|
| 117 |
-
|
| 118 |
-
stream=False,
|
| 119 |
)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
except Exception as exc:
|
| 125 |
-
print(f"[DEBUG] Model request failed: {exc}",
|
| 126 |
-
return "WAIT"
|
| 127 |
|
| 128 |
# --- Run Loop ----------------------------------------------------------------
|
| 129 |
-
async def main() -> None:
|
| 130 |
-
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 131 |
-
|
| 132 |
-
# Initialize environment locally
|
| 133 |
-
# Note: the sample template uses docker integration, but for local
|
| 134 |
-
# hackathon development, we initialize the DroneDeliveryEnvironment directly.
|
| 135 |
-
env = DroneDeliveryEnvironment()
|
| 136 |
|
| 137 |
-
|
| 138 |
rewards: List[float] = []
|
| 139 |
steps_taken = 0
|
| 140 |
-
score = 0.
|
| 141 |
success = False
|
| 142 |
|
| 143 |
-
log_start(task=
|
| 144 |
|
| 145 |
try:
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
# Max steps from environment or local constant
|
| 150 |
-
max_limit = int(obs.max_steps) if obs.max_steps else MAX_STEPS
|
| 151 |
|
| 152 |
-
for step in range(1,
|
| 153 |
if obs.done:
|
| 154 |
break
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
obs = env.step(DroneAction(direction=action_str))
|
| 160 |
-
|
| 161 |
reward = float(obs.reward_last)
|
| 162 |
done = bool(obs.done)
|
| 163 |
-
|
| 164 |
-
|
| 165 |
rewards.append(reward)
|
| 166 |
steps_taken = step
|
| 167 |
-
|
| 168 |
-
log_step(step=step, action=action_str, reward=reward, done=done, error=error)
|
| 169 |
|
| 170 |
if done:
|
| 171 |
break
|
| 172 |
|
| 173 |
-
|
| 174 |
-
score = float(obs.score) if obs.score is not None else 0.010
|
| 175 |
success = (obs.deliveries_done == obs.deliveries_total) and obs.deliveries_total > 0
|
| 176 |
|
|
|
|
|
|
|
|
|
|
| 177 |
finally:
|
| 178 |
-
# Mandatory close and log emission
|
| 179 |
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
|
|
|
|
|
|
| 181 |
|
| 182 |
if __name__ == "__main__":
|
| 183 |
asyncio.run(main())
|
|
|
|
| 2 |
SkyRelic Drone Delivery: Standardized Inference Script
|
| 3 |
===================================================
|
| 4 |
MANDATORY
|
| 5 |
+
- All 3 tasks (easy, medium, hard) are executed sequentially to satisfy Phase 2 validation.
|
| 6 |
- STDOUT FORMAT: [START], [STEP], [END]
|
| 7 |
- Participants must use OpenAI Client for all LLM calls.
|
| 8 |
"""
|
|
|
|
| 10 |
import asyncio
|
| 11 |
import os
|
| 12 |
import textwrap
|
| 13 |
+
import json
|
| 14 |
import sys
|
| 15 |
from typing import List, Optional
|
| 16 |
from pathlib import Path
|
| 17 |
+
from pydantic import BaseModel, Field
|
| 18 |
from openai import OpenAI
|
| 19 |
|
| 20 |
+
# Unified Imports - Canonical Package Paths with local fallbacks
|
| 21 |
ROOT_DIR = Path(__file__).parent
|
| 22 |
if str(ROOT_DIR) not in sys.path:
|
| 23 |
sys.path.insert(0, str(ROOT_DIR))
|
| 24 |
|
| 25 |
+
try:
|
| 26 |
+
from models import DroneAction, DroneObservation
|
| 27 |
+
from server.grid_world_environment import DroneDeliveryEnvironment
|
| 28 |
+
except ImportError:
|
| 29 |
+
# Use fallback if not found in root
|
| 30 |
+
from drone_env.models import DroneAction, DroneObservation
|
| 31 |
+
from drone_env.server.grid_world_environment import DroneDeliveryEnvironment
|
| 32 |
+
|
| 33 |
+
# --- Load .env file for local development ---
|
| 34 |
+
def load_dotenv():
|
| 35 |
+
env_path = Path(__file__).parent / ".env"
|
| 36 |
+
if env_path.exists():
|
| 37 |
+
for line in env_path.read_text().splitlines():
|
| 38 |
line = line.strip()
|
| 39 |
+
if not line or line.startswith("#") or "=" not in line:
|
| 40 |
+
continue
|
| 41 |
+
key, value = line.split("=", 1)
|
| 42 |
+
os.environ[key.strip()] = value.strip().strip('"').strip("'")
|
| 43 |
|
| 44 |
+
load_dotenv()
|
|
|
|
|
|
|
| 45 |
|
| 46 |
# --- Configuration ------------------------------------------------------------
|
| 47 |
+
IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME")
|
|
|
|
| 48 |
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY") or "EMPTY_KEY"
|
|
|
|
|
|
|
| 49 |
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 50 |
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-7B-Instruct"
|
| 51 |
|
| 52 |
+
BENCHMARK = "drone_env"
|
| 53 |
+
ALL_TASKS = ["easy_delivery", "medium_delivery", "hard_delivery"]
|
| 54 |
+
TEMPERATURE = 0.7
|
| 55 |
+
|
| 56 |
+
# --- Structured output schema ------------------------------------------------
|
| 57 |
+
|
| 58 |
+
class NavigationAction(BaseModel):
|
| 59 |
+
"""Structured navigation action output from the LLM."""
|
| 60 |
+
reasoning: str = Field(description="Brief explanation of why this direction was chosen")
|
| 61 |
+
direction: str = Field(description="Movement direction: UP, DOWN, LEFT, RIGHT, or WAIT")
|
| 62 |
+
|
| 63 |
+
# --- Prompts ------------------------------------------------------------------
|
| 64 |
+
|
| 65 |
+
SYSTEM_PROMPT = textwrap.dedent("""
|
| 66 |
+
You are a drone navigation AI. Your goal is to deliver packages to targets in a grid world.
|
| 67 |
+
|
| 68 |
+
Grid Mechanics:
|
| 69 |
+
- (x, y) coordinates: x increases right, y increases down.
|
| 70 |
+
- UP: y decreases
|
| 71 |
+
- DOWN: y increases
|
| 72 |
+
- LEFT: x decreases
|
| 73 |
+
- RIGHT: x increases
|
| 74 |
+
|
| 75 |
+
Constraints:
|
| 76 |
+
- Avoid buildings and obstacles.
|
| 77 |
+
- Battery drains per move.
|
| 78 |
+
|
| 79 |
+
You MUST respond with a valid JSON object:
|
| 80 |
+
{"reasoning": "<brief explanation>", "direction": "UP|DOWN|LEFT|RIGHT|WAIT"}
|
| 81 |
+
""").strip()
|
| 82 |
+
|
| 83 |
+
def build_user_prompt(obs: DroneObservation) -> str:
|
| 84 |
+
return textwrap.dedent(f"""
|
| 85 |
+
Pos: ({obs.drone_x}, {obs.drone_y})
|
| 86 |
+
Battery: {obs.battery:.2f}
|
| 87 |
+
Target: {obs.current_target}
|
| 88 |
+
Distance: {obs.distance_to_target:.1f}
|
| 89 |
+
Status: {obs.message}
|
| 90 |
+
|
| 91 |
+
Plan your next move to reach the target efficiently.
|
| 92 |
+
""").strip()
|
| 93 |
|
| 94 |
# --- Logging Helpers ---------------------------------------------------------
|
| 95 |
+
|
| 96 |
def log_start(task: str, env: str, model: str) -> None:
|
| 97 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 98 |
|
| 99 |
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 100 |
error_val = error if error else "null"
|
| 101 |
+
print(f"[STEP] step={step} action={action} reward={reward:.2f} done={str(done).lower()} error={error_val}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
|
|
|
| 104 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 105 |
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 106 |
|
| 107 |
# --- Agent Logic -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
def get_action(client: OpenAI, obs: DroneObservation) -> NavigationAction:
|
| 110 |
user_prompt = build_user_prompt(obs)
|
| 111 |
try:
|
| 112 |
completion = client.chat.completions.create(
|
|
|
|
| 116 |
{"role": "user", "content": user_prompt},
|
| 117 |
],
|
| 118 |
temperature=TEMPERATURE,
|
| 119 |
+
response_format={"type": "json_object"},
|
|
|
|
| 120 |
)
|
| 121 |
+
raw = completion.choices[0].message.content or "{}"
|
| 122 |
+
data = json.loads(raw)
|
| 123 |
+
action = NavigationAction(
|
| 124 |
+
reasoning=data.get("reasoning", ""),
|
| 125 |
+
direction=data.get("direction", "WAIT").upper(),
|
| 126 |
+
)
|
| 127 |
+
if action.direction not in ["UP", "DOWN", "LEFT", "RIGHT", "WAIT"]:
|
| 128 |
+
action.direction = "WAIT"
|
| 129 |
+
return action
|
| 130 |
except Exception as exc:
|
| 131 |
+
print(f"[DEBUG] Model request failed: {exc}", flush=True)
|
| 132 |
+
return NavigationAction(reasoning="fallback", direction="WAIT")
|
| 133 |
|
| 134 |
# --- Run Loop ----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
+
async def run_task(task_id: str, env: DroneDeliveryEnvironment, client: OpenAI) -> float:
|
| 137 |
rewards: List[float] = []
|
| 138 |
steps_taken = 0
|
| 139 |
+
score = 0.01
|
| 140 |
success = False
|
| 141 |
|
| 142 |
+
log_start(task=task_id, env=BENCHMARK, model=MODEL_NAME)
|
| 143 |
|
| 144 |
try:
|
| 145 |
+
obs = env.reset(DroneAction(task_name=task_id))
|
| 146 |
+
max_steps = int(obs.max_steps) if obs.max_steps else 60
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
for step in range(1, max_steps + 1):
|
| 149 |
if obs.done:
|
| 150 |
break
|
| 151 |
|
| 152 |
+
nav_action = get_action(client, obs)
|
| 153 |
+
action_str = nav_action.direction
|
| 154 |
+
|
| 155 |
obs = env.step(DroneAction(direction=action_str))
|
| 156 |
+
|
| 157 |
reward = float(obs.reward_last)
|
| 158 |
done = bool(obs.done)
|
| 159 |
+
|
|
|
|
| 160 |
rewards.append(reward)
|
| 161 |
steps_taken = step
|
| 162 |
+
log_step(step=step, action=action_str, reward=reward, done=done, error=None)
|
|
|
|
| 163 |
|
| 164 |
if done:
|
| 165 |
break
|
| 166 |
|
| 167 |
+
score = float(obs.score) if obs.score is not None else 0.01
|
|
|
|
| 168 |
success = (obs.deliveries_done == obs.deliveries_total) and obs.deliveries_total > 0
|
| 169 |
|
| 170 |
+
except Exception as e:
|
| 171 |
+
print(f"[DEBUG] run_task({task_id}) error: {e}", flush=True)
|
| 172 |
+
|
| 173 |
finally:
|
|
|
|
| 174 |
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 175 |
+
print(f"\n{'='*50}", flush=True)
|
| 176 |
+
print(f" Task : {task_id}", flush=True)
|
| 177 |
+
print(f" Total Steps : {steps_taken}", flush=True)
|
| 178 |
+
print(f" Final Score : {score:.3f}", flush=True)
|
| 179 |
+
print(f" Success : {success}", flush=True)
|
| 180 |
+
print(f"{'='*50}\n", flush=True)
|
| 181 |
+
|
| 182 |
+
return score
|
| 183 |
+
|
| 184 |
+
async def main() -> None:
|
| 185 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 186 |
+
env = DroneDeliveryEnvironment()
|
| 187 |
|
| 188 |
+
for task_id in ALL_TASKS:
|
| 189 |
+
await run_task(task_id, env, client)
|
| 190 |
|
| 191 |
if __name__ == "__main__":
|
| 192 |
asyncio.run(main())
|
models.py
CHANGED
|
@@ -14,7 +14,7 @@ class DroneAction(BaseModel):
|
|
| 14 |
)
|
| 15 |
task_name: Optional[str] = Field(
|
| 16 |
default=None,
|
| 17 |
-
description="Task to load on reset:
|
| 18 |
)
|
| 19 |
|
| 20 |
|
|
@@ -43,7 +43,7 @@ class DroneObservation(BaseModel):
|
|
| 43 |
|
| 44 |
class DroneState(BaseModel):
|
| 45 |
episode_id: str = ""
|
| 46 |
-
task_name: str = "
|
| 47 |
step_count: int = 0
|
| 48 |
done: bool = False
|
| 49 |
reward_total: float = 0.0
|
|
|
|
| 14 |
)
|
| 15 |
task_name: Optional[str] = Field(
|
| 16 |
default=None,
|
| 17 |
+
description="Task to load on reset: graders:grade_easy | medium | hard",
|
| 18 |
)
|
| 19 |
|
| 20 |
|
|
|
|
| 43 |
|
| 44 |
class DroneState(BaseModel):
|
| 45 |
episode_id: str = ""
|
| 46 |
+
task_name: str = "graders:grade_easy"
|
| 47 |
step_count: int = 0
|
| 48 |
done: bool = False
|
| 49 |
reward_total: float = 0.0
|
openenv.yaml
CHANGED
|
@@ -6,8 +6,8 @@ app: server.app:app
|
|
| 6 |
port: 8000
|
| 7 |
tasks:
|
| 8 |
- id: easy_delivery
|
| 9 |
-
grader:
|
| 10 |
- id: medium_delivery
|
| 11 |
-
grader:
|
| 12 |
- id: hard_delivery
|
| 13 |
-
grader:
|
|
|
|
| 6 |
port: 8000
|
| 7 |
tasks:
|
| 8 |
- id: easy_delivery
|
| 9 |
+
grader: graders:grade_easy
|
| 10 |
- id: medium_delivery
|
| 11 |
+
grader: graders:grade_medium
|
| 12 |
- id: hard_delivery
|
| 13 |
+
grader: graders:grade_hard
|
server/app.py
CHANGED
|
@@ -21,13 +21,22 @@ BASE_DIR = Path(__file__).parent.parent
|
|
| 21 |
if str(BASE_DIR) not in sys.path:
|
| 22 |
sys.path.insert(0, str(BASE_DIR))
|
| 23 |
|
| 24 |
-
# Unified Imports - Canonical Package Paths
|
| 25 |
-
|
| 26 |
-
from drone_env.
|
| 27 |
-
from drone_env.
|
| 28 |
-
from drone_env.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
from drone_env.rl.trainer import PathLearner, get_action_from_policy
|
| 30 |
|
|
|
|
|
|
|
| 31 |
app = FastAPI(
|
| 32 |
title="Drone Delivery OpenEnv",
|
| 33 |
description="Real-world drone delivery RL environment.",
|
|
@@ -86,11 +95,11 @@ async def metadata():
|
|
| 86 |
return {
|
| 87 |
"name": "drone_env",
|
| 88 |
"description": "SkyRelic Drone Delivery reinforcement-learning environment with easy/medium/hard delivery tasks on a grid world.",
|
| 89 |
-
"version": "0.2.
|
| 90 |
"tasks": [
|
| 91 |
-
{"id": "easy_delivery",
|
| 92 |
-
{"id": "medium_delivery", "grader": "
|
| 93 |
-
{"id": "hard_delivery",
|
| 94 |
],
|
| 95 |
}
|
| 96 |
|
|
@@ -144,9 +153,18 @@ async def get_state():
|
|
| 144 |
|
| 145 |
# Task ID β grader key mapping (matches openenv.yaml task ids)
|
| 146 |
TASK_ID_TO_GRADER = {
|
| 147 |
-
|
| 148 |
-
"
|
| 149 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
@app.get("/grade/{task_name}")
|
|
@@ -207,7 +225,7 @@ async def get_terminal_logs():
|
|
| 207 |
@app.get("/rewards")
|
| 208 |
async def get_rewards():
|
| 209 |
"""Return the reward configuration for the current task."""
|
| 210 |
-
task_name = _env.state.task_name or "
|
| 211 |
config = TASK_CONFIG.get(task_name, {})
|
| 212 |
# Filter only reward keys
|
| 213 |
rewards = {k: v for k, v in config.items() if k.startswith("r_")}
|
|
@@ -248,7 +266,7 @@ async def get_memory_logs():
|
|
| 248 |
|
| 249 |
@app.post("/predict")
|
| 250 |
async def predict(obs: DroneObservation):
|
| 251 |
-
task_name = _env.state.task_name or "
|
| 252 |
action_str = get_action_from_policy(obs, task_name)
|
| 253 |
return {"direction": action_str}
|
| 254 |
|
|
|
|
| 21 |
if str(BASE_DIR) not in sys.path:
|
| 22 |
sys.path.insert(0, str(BASE_DIR))
|
| 23 |
|
| 24 |
+
# Unified Imports - Canonical Package Paths with local fallbacks
|
| 25 |
+
try:
|
| 26 |
+
from drone_env.models import DroneAction, DroneObservation, DroneState
|
| 27 |
+
from drone_env.server.grid_world_environment import DroneDeliveryEnvironment
|
| 28 |
+
from drone_env.core.tasks import TASK_CONFIG
|
| 29 |
+
import graders as graders_mod
|
| 30 |
+
except ImportError:
|
| 31 |
+
from models import DroneAction, DroneObservation, DroneState
|
| 32 |
+
from server.grid_world_environment import DroneDeliveryEnvironment
|
| 33 |
+
from core.tasks import TASK_CONFIG
|
| 34 |
+
import graders as graders_mod
|
| 35 |
+
|
| 36 |
from drone_env.rl.trainer import PathLearner, get_action_from_policy
|
| 37 |
|
| 38 |
+
from graders import GRADERS
|
| 39 |
+
|
| 40 |
app = FastAPI(
|
| 41 |
title="Drone Delivery OpenEnv",
|
| 42 |
description="Real-world drone delivery RL environment.",
|
|
|
|
| 95 |
return {
|
| 96 |
"name": "drone_env",
|
| 97 |
"description": "SkyRelic Drone Delivery reinforcement-learning environment with easy/medium/hard delivery tasks on a grid world.",
|
| 98 |
+
"version": "0.2.2",
|
| 99 |
"tasks": [
|
| 100 |
+
{"id": "easy_delivery", "grader": "graders:grade_easy"},
|
| 101 |
+
{"id": "medium_delivery", "grader": "graders:grade_medium"},
|
| 102 |
+
{"id": "hard_delivery", "grader": "graders:grade_hard"},
|
| 103 |
],
|
| 104 |
}
|
| 105 |
|
|
|
|
| 153 |
|
| 154 |
# Task ID β grader key mapping (matches openenv.yaml task ids)
|
| 155 |
TASK_ID_TO_GRADER = {
|
| 156 |
+
# Short IDs
|
| 157 |
+
"easy_delivery": "graders:grade_easy",
|
| 158 |
+
"medium_delivery": "graders:grade_medium",
|
| 159 |
+
"hard_delivery": "graders:grade_hard",
|
| 160 |
+
# Standardized keys (as IDs)
|
| 161 |
+
"graders:grade_easy": "graders:grade_easy",
|
| 162 |
+
"graders:grade_medium": "graders:grade_medium",
|
| 163 |
+
"graders:grade_hard": "graders:grade_hard",
|
| 164 |
+
# Legacy support for old cached browser sessions
|
| 165 |
+
"drone_env.graders.easy:grade_easy": "graders:grade_easy",
|
| 166 |
+
"drone_env.graders.medium:grade_medium": "graders:grade_medium",
|
| 167 |
+
"drone_env.graders.hard:grade_hard": "graders:grade_hard",
|
| 168 |
}
|
| 169 |
|
| 170 |
@app.get("/grade/{task_name}")
|
|
|
|
| 225 |
@app.get("/rewards")
|
| 226 |
async def get_rewards():
|
| 227 |
"""Return the reward configuration for the current task."""
|
| 228 |
+
task_name = _env.state.task_name or "graders:grade_easy"
|
| 229 |
config = TASK_CONFIG.get(task_name, {})
|
| 230 |
# Filter only reward keys
|
| 231 |
rewards = {k: v for k, v in config.items() if k.startswith("r_")}
|
|
|
|
| 266 |
|
| 267 |
@app.post("/predict")
|
| 268 |
async def predict(obs: DroneObservation):
|
| 269 |
+
task_name = _env.state.task_name or "graders:grade_easy"
|
| 270 |
action_str = get_action_from_policy(obs, task_name)
|
| 271 |
return {"direction": action_str}
|
| 272 |
|
server/grid_world_environment.py
CHANGED
|
@@ -24,7 +24,14 @@ from drone_env.core.tasks import TASK_CONFIG
|
|
| 24 |
from drone_env.core.drone import compute_next_pos, drain_battery
|
| 25 |
from drone_env.core.obstacles import check_move
|
| 26 |
from drone_env.core.state_manager import new_episode_state
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
from drone_env.rl.trainer import record_episode
|
| 29 |
|
| 30 |
|
|
@@ -33,7 +40,7 @@ class DroneDeliveryEnvironment(Environment):
|
|
| 33 |
def __init__(self):
|
| 34 |
super().__init__()
|
| 35 |
self._state = DroneState()
|
| 36 |
-
self._cfg = TASK_CONFIG["
|
| 37 |
self._grid: List[List[str]] = []
|
| 38 |
self._deliveries: List[Tuple[int, int]] = []
|
| 39 |
self._delivered: List[bool] = []
|
|
@@ -51,11 +58,15 @@ class DroneDeliveryEnvironment(Environment):
|
|
| 51 |
|
| 52 |
# Map short task IDs (from openenv.yaml) to full grader keys
|
| 53 |
_TASK_ID_MAP = {
|
| 54 |
-
"easy_delivery": "
|
| 55 |
-
"medium_delivery": "
|
| 56 |
-
"hard_delivery": "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
-
task = "
|
| 59 |
if action and action.task_name:
|
| 60 |
name = action.task_name
|
| 61 |
if name in _TASK_ID_MAP:
|
|
|
|
| 24 |
from drone_env.core.drone import compute_next_pos, drain_battery
|
| 25 |
from drone_env.core.obstacles import check_move
|
| 26 |
from drone_env.core.state_manager import new_episode_state
|
| 27 |
+
try:
|
| 28 |
+
from drone_env.graders import GRADERS
|
| 29 |
+
except ImportError:
|
| 30 |
+
try:
|
| 31 |
+
from graders import GRADERS
|
| 32 |
+
except ImportError:
|
| 33 |
+
# Final fallback
|
| 34 |
+
GRADERS = {}
|
| 35 |
from drone_env.rl.trainer import record_episode
|
| 36 |
|
| 37 |
|
|
|
|
| 40 |
def __init__(self):
|
| 41 |
super().__init__()
|
| 42 |
self._state = DroneState()
|
| 43 |
+
self._cfg = TASK_CONFIG["graders:grade_easy"]
|
| 44 |
self._grid: List[List[str]] = []
|
| 45 |
self._deliveries: List[Tuple[int, int]] = []
|
| 46 |
self._delivered: List[bool] = []
|
|
|
|
| 58 |
|
| 59 |
# Map short task IDs (from openenv.yaml) to full grader keys
|
| 60 |
_TASK_ID_MAP = {
|
| 61 |
+
"easy_delivery": "graders:grade_easy",
|
| 62 |
+
"medium_delivery": "graders:grade_medium",
|
| 63 |
+
"hard_delivery": "graders:grade_hard",
|
| 64 |
+
# Legacy redirects
|
| 65 |
+
"drone_env.graders.easy:grade_easy": "graders:grade_easy",
|
| 66 |
+
"drone_env.graders.medium:grade_medium": "graders:grade_medium",
|
| 67 |
+
"drone_env.graders.hard:grade_hard": "graders:grade_hard",
|
| 68 |
}
|
| 69 |
+
task = "graders:grade_easy"
|
| 70 |
if action and action.task_name:
|
| 71 |
name = action.task_name
|
| 72 |
if name in _TASK_ID_MAP:
|
server/static/index.html
CHANGED
|
@@ -1230,17 +1230,17 @@
|
|
| 1230 |
<div class="panel-inner">
|
| 1231 |
<div class="panel-title">Mission Select</div>
|
| 1232 |
<div class="task-group" id="taskGroup">
|
| 1233 |
-
<button class="task-btn active" data-task="
|
| 1234 |
<span class="dot"></span>
|
| 1235 |
Easy Delivery
|
| 1236 |
<span class="task-tag">10Γ10</span>
|
| 1237 |
</button>
|
| 1238 |
-
<button class="task-btn" data-task="
|
| 1239 |
<span class="dot"></span>
|
| 1240 |
Medium Delivery
|
| 1241 |
<span class="task-tag">14Γ14</span>
|
| 1242 |
</button>
|
| 1243 |
-
<button class="task-btn" data-task="
|
| 1244 |
<span class="dot"></span>
|
| 1245 |
Hard Delivery
|
| 1246 |
<span class="task-tag">18Γ18</span>
|
|
@@ -1718,7 +1718,7 @@
|
|
| 1718 |
}
|
| 1719 |
</style>
|
| 1720 |
|
| 1721 |
-
<script src="/static/script.js"></script>
|
| 1722 |
</body>
|
| 1723 |
|
| 1724 |
</html>
|
|
|
|
| 1230 |
<div class="panel-inner">
|
| 1231 |
<div class="panel-title">Mission Select</div>
|
| 1232 |
<div class="task-group" id="taskGroup">
|
| 1233 |
+
<button class="task-btn active" data-task="graders:grade_easy">
|
| 1234 |
<span class="dot"></span>
|
| 1235 |
Easy Delivery
|
| 1236 |
<span class="task-tag">10Γ10</span>
|
| 1237 |
</button>
|
| 1238 |
+
<button class="task-btn" data-task="graders:grade_medium">
|
| 1239 |
<span class="dot"></span>
|
| 1240 |
Medium Delivery
|
| 1241 |
<span class="task-tag">14Γ14</span>
|
| 1242 |
</button>
|
| 1243 |
+
<button class="task-btn" data-task="graders:grade_hard">
|
| 1244 |
<span class="dot"></span>
|
| 1245 |
Hard Delivery
|
| 1246 |
<span class="task-tag">18Γ18</span>
|
|
|
|
| 1718 |
}
|
| 1719 |
</style>
|
| 1720 |
|
| 1721 |
+
<script src="/static/script.js?v=2.2"></script>
|
| 1722 |
</body>
|
| 1723 |
|
| 1724 |
</html>
|
server/static/script.js
CHANGED
|
@@ -17,7 +17,7 @@ const EMOJI = {
|
|
| 17 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
// STATE
|
| 19 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
-
let currentTask = '
|
| 21 |
let autoTimer = null;
|
| 22 |
let logTimer = null;
|
| 23 |
let obs = null;
|
|
@@ -601,9 +601,9 @@ function startNextTask() {
|
|
| 601 |
closeCompletionModal();
|
| 602 |
|
| 603 |
const sequence = {
|
| 604 |
-
'
|
| 605 |
-
'
|
| 606 |
-
'
|
| 607 |
};
|
| 608 |
|
| 609 |
const nextTask = sequence[currentTask] || 'drone_env.graders.easy:grade_easy';
|
|
@@ -627,7 +627,7 @@ async function updateMissionLegend() {
|
|
| 627 |
if (!container || !data.tasks) return;
|
| 628 |
|
| 629 |
// Ensure tasks are sorted Easy, Medium, Hard
|
| 630 |
-
const order = ['
|
| 631 |
const tasks = data.tasks.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
|
| 632 |
|
| 633 |
container.innerHTML = `
|
|
|
|
| 17 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
// STATE
|
| 19 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
let currentTask = 'graders:grade_easy';
|
| 21 |
let autoTimer = null;
|
| 22 |
let logTimer = null;
|
| 23 |
let obs = null;
|
|
|
|
| 601 |
closeCompletionModal();
|
| 602 |
|
| 603 |
const sequence = {
|
| 604 |
+
'graders:grade_easy': 'graders:grade_medium',
|
| 605 |
+
'graders:grade_medium': 'graders:grade_hard',
|
| 606 |
+
'graders:grade_hard': 'graders:grade_easy'
|
| 607 |
};
|
| 608 |
|
| 609 |
const nextTask = sequence[currentTask] || 'drone_env.graders.easy:grade_easy';
|
|
|
|
| 627 |
if (!container || !data.tasks) return;
|
| 628 |
|
| 629 |
// Ensure tasks are sorted Easy, Medium, Hard
|
| 630 |
+
const order = ['graders:grade_easy', 'graders:grade_medium', 'graders:grade_hard'];
|
| 631 |
const tasks = data.tasks.sort((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
|
| 632 |
|
| 633 |
container.innerHTML = `
|