manikandan-n-07 commited on
Commit
d0e61c2
Β·
1 Parent(s): 864a7e4

Phase 2 validation

Browse files
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 python:3.10-slim
4
 
5
- # Set working directory
6
  WORKDIR /app
7
 
8
- # Install system dependencies (git for building packages if needed)
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
- # Expose the API port (standard for HF Spaces or custom)
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
- # This calls the main function in server/app.py which starts uvicorn
30
- CMD ["python", "server/app.py", "--host", "0.0.0.0", "--port", "8000"]
 
 
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
- module = None
46
- # Try full path first (as it will be in the platform)
47
  try:
48
  module = importlib.import_module(module_path)
49
- except ImportError:
50
- # Local fallback: try without 'drone_env.' prefix if we're in the project root
51
- if module_path.startswith("drone_env."):
52
- alt_path = module_path.replace("drone_env.", "", 1)
53
- try:
54
- module = importlib.import_module(alt_path)
55
- except ImportError as eb:
56
- print(f" ❌ Module search failed (tried {module_path} and {alt_path}): {eb}")
 
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
- "drone_env.graders.easy:grade_easy": {
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
- "drone_env.graders.medium:grade_medium": {
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
- "drone_env.graders.hard:grade_hard": {
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
- "drone_env.graders.easy:grade_easy": grade_easy,
7
- "drone_env.graders.medium:grade_medium": grade_medium,
8
- "drone_env.graders.hard:grade_hard": grade_hard,
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
- - Environment variables: HF_TOKEN, API_BASE_URL, MODEL_NAME
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
- # Local Project Imports
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
- # Load .env for local development convenience
25
- def load_dotenv(path: Path):
26
- if path.exists():
27
- for line in path.read_text().splitlines():
 
 
 
 
 
 
 
 
 
28
  line = line.strip()
29
- if not line or line.startswith("#") or "=" not in line: continue
30
- k, v = line.split("=", 1)
31
- os.environ[k.strip()] = v.strip().strip('"').strip("'")
32
- load_dotenv(ROOT_DIR / ".env")
33
 
34
- # Use the established Drone Environment
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("IMAGE_NAME") or os.getenv("LOCAL_IMAGE_NAME")
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
- TASK_MAP = {
48
- "easy": "drone_env.graders.easy:grade_easy",
49
- "medium": "drone_env.graders.medium:grade_medium",
50
- "hard": "drone_env.graders.hard:grade_hard",
51
- "easy_delivery": "drone_env.graders.easy:grade_easy",
52
- "medium_delivery": "drone_env.graders.medium:grade_medium",
53
- "hard_delivery": "drone_env.graders.hard:grade_hard",
54
- }
55
-
56
- # Accepts "easy", "medium", "hard" or the full grader path
57
- RAW_TASK = os.getenv("TASK_NAME", "easy")
58
- TASK_NAME = TASK_MAP.get(RAW_TASK, RAW_TASK)
59
-
60
- BENCHMARK = os.getenv("DRONE_BENCHMARK", "drone_env_v1")
61
-
62
- MAX_STEPS = 60 # Default fallback
63
- TEMPERATURE = 0.0
64
- MAX_TOKENS = 50
65
- SUCCESS_SCORE_THRESHOLD = 0.5
66
-
67
- SYSTEM_PROMPT = textwrap.dedent(
68
- """
69
- You are a drone navigation AI. Your goal is to deliver packages to targets.
70
- Each turn, you receive the drone's position, battery, target, and distance.
71
- Valid Actions: UP, DOWN, LEFT, RIGHT, WAIT.
72
- Respond with exactly one action name in uppercase. No quotes.
73
- """
74
- ).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- done_val = str(done).lower()
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 get_model_action(client: OpenAI, obs: DroneObservation) -> str:
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
- max_tokens=MAX_TOKENS,
118
- stream=False,
119
  )
120
- text = (completion.choices[0].message.content or "").strip().upper()
121
- if text not in ["UP", "DOWN", "LEFT", "RIGHT", "WAIT"]:
122
- return "WAIT"
123
- return text
 
 
 
 
 
124
  except Exception as exc:
125
- print(f"[DEBUG] Model request failed: {exc}", file=sys.stderr, flush=True)
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
- history: List[str] = []
138
  rewards: List[float] = []
139
  steps_taken = 0
140
- score = 0.010
141
  success = False
142
 
143
- log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
144
 
145
  try:
146
- # Initial reset
147
- obs = env.reset(DroneAction(task_name=TASK_NAME))
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, max_limit + 1):
153
  if obs.done:
154
  break
155
 
156
- action_str = get_model_action(client, obs)
157
-
158
- # Step environment
159
  obs = env.step(DroneAction(direction=action_str))
160
-
161
  reward = float(obs.reward_last)
162
  done = bool(obs.done)
163
- error = None
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
- # Final score (already normalized by the environment's grader [0.01, 0.99])
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: drone_env.graders.easy:grade_easy | medium | hard",
18
  )
19
 
20
 
@@ -43,7 +43,7 @@ class DroneObservation(BaseModel):
43
 
44
  class DroneState(BaseModel):
45
  episode_id: str = ""
46
- task_name: str = "drone_env.graders.easy:grade_easy"
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: drone_env.graders.easy:grade_easy
10
  - id: medium_delivery
11
- grader: drone_env.graders.medium:grade_medium
12
  - id: hard_delivery
13
- grader: drone_env.graders.hard:grade_hard
 
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
- from drone_env.models import DroneAction, DroneObservation, DroneState
26
- from drone_env.server.grid_world_environment import DroneDeliveryEnvironment
27
- from drone_env.core.tasks import TASK_CONFIG
28
- from drone_env.graders import GRADERS
 
 
 
 
 
 
 
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.1",
90
  "tasks": [
91
- {"id": "easy_delivery", "grader": "drone_env.graders.easy:grade_easy"},
92
- {"id": "medium_delivery", "grader": "drone_env.graders.medium:grade_medium"},
93
- {"id": "hard_delivery", "grader": "drone_env.graders.hard:grade_hard"},
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
- "easy_delivery": "drone_env.graders.easy:grade_easy",
148
- "medium_delivery": "drone_env.graders.medium:grade_medium",
149
- "hard_delivery": "drone_env.graders.hard:grade_hard",
 
 
 
 
 
 
 
 
 
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 "drone_env.graders.easy:grade_easy"
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 "drone_env.graders.easy:grade_easy"
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
- from drone_env.graders import GRADERS
 
 
 
 
 
 
 
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["drone_env.graders.easy:grade_easy"]
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": "drone_env.graders.easy:grade_easy",
55
- "medium_delivery": "drone_env.graders.medium:grade_medium",
56
- "hard_delivery": "drone_env.graders.hard:grade_hard",
 
 
 
 
57
  }
58
- task = "drone_env.graders.easy:grade_easy"
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="drone_env.graders.easy: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="drone_env.graders.medium: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="drone_env.graders.hard:grade_hard">
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 = 'drone_env.graders.easy:grade_easy';
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
- 'drone_env.graders.easy:grade_easy': 'drone_env.graders.medium:grade_medium',
605
- 'drone_env.graders.medium:grade_medium': 'drone_env.graders.hard:grade_hard',
606
- 'drone_env.graders.hard:grade_hard': 'drone_env.graders.easy:grade_easy'
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 = ['drone_env.graders.easy:grade_easy', 'drone_env.graders.medium:grade_medium', 'drone_env.graders.hard:grade_hard'];
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 = `