Spaces:
Sleeping
Sleeping
Deepika commited on
Commit ·
3d87f50
1
Parent(s): 85033ed
Copied source project
Browse files- README.md +30 -11
- inference.py +143 -203
- openenv.yaml +19 -19
- server/app.py +143 -10
- server/config_debug_environment.py +23 -28
- server/env.py +21 -17
- server/graders/compose_grader.py +2 -1
- server/graders/dockerfile_grader.py +2 -2
- server/graders/github_actions_grader.py +2 -1
- server/graders/grader_api.py +86 -138
- server/graders/json_grader.py +68 -24
- server/graders/k8s_grader.py +4 -4
- server/graders/nginx_grader.py +75 -32
- server/graders/yaml_grader.py +65 -30
- server/models.py +1 -0
- server/tasks/task1_json.py +31 -10
- server/tasks/task2_yaml.py +50 -31
- server/tasks/task7_nginx.py +5 -7
- server/tasks/task_registry.py +33 -24
- test_grader_audit.py +95 -0
- test_task_graders.py +84 -0
- verify_graders.py +51 -0
README.md
CHANGED
|
@@ -74,19 +74,38 @@ Each task has **3 progressive levels**:
|
|
| 74 |
|
| 75 |
---
|
| 76 |
|
| 77 |
-
## The 7 Tasks (Progressive
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
| Task | Format | Difficulty | Bugs | Key Challenge |
|
| 80 |
-
|------|--------|------------|------|----------------|
|
| 81 |
-
| **task1_json** | JSON | Easy | 2 | Type detection |
|
| 82 |
-
| **task2_yaml** | YAML | Easy | 2 | Indentation / structure |
|
| 83 |
-
| **task3_dockerfile** | Dockerfile | Medium | 3 | Multi-stage builds |
|
| 84 |
-
| **task4_compose** | docker-compose | Medium | 4 | Service networking |
|
| 85 |
-
| **task5_k8s** | Kubernetes | Hard | 3* | Multi-step type/domain fixes |
|
| 86 |
-
| **task6_github_actions** | GitHub Actions | Hard | 5 | Workflow triggers |
|
| 87 |
-
| **task7_nginx** | Nginx config | Very Hard | 3* | Multi-step directive/routing fixes |
|
| 88 |
-
|
| 89 |
-
*task5_k8s and task7_nginx optimized for multi-step learning with targeted error guidance
|
| 90 |
|
| 91 |
---
|
| 92 |
|
|
|
|
| 74 |
|
| 75 |
---
|
| 76 |
|
| 77 |
+
## The 7 Tasks (Progressive Complexity)
|
| 78 |
+
|
| 79 |
+
### Benchmark Quality Design
|
| 80 |
+
Each task is carefully crafted with **realistic, interdependent bugs** that require progressive debugging:
|
| 81 |
+
|
| 82 |
+
| Task | Format | Difficulty | Bugs | Description | Requires Multi-Step Fix? |
|
| 83 |
+
|------|--------|------------|------|-------------|------------------------|
|
| 84 |
+
| **task1_json** | JSON | Medium | 3 | Microservice config: missing comma, env structure bug, volumes structure bug | ✓ Yes |
|
| 85 |
+
| **task2_yaml** | YAML | Medium | 3 | CI/CD pipeline: indentation error, env array→object, missing job timeouts | ✓ Yes |
|
| 86 |
+
| **task3_dockerfile** | Dockerfile | Medium | 3 | Multi-stage build: base image, build args, runtime setup | ✓ Progressive |
|
| 87 |
+
| **task4_compose** | Docker-Compose | Medium | 4 | Service mesh: compose syntax, volumes, service networking | ✓ Progressive |
|
| 88 |
+
| **task5_k8s** | Kubernetes | Hard | 3 | Deployment manifest: type errors, missing fields, configuration validation | ✓ Yes |
|
| 89 |
+
| **task6_github_actions** | GitHub Actions | Hard | 5 | Workflow automation: YAML syntax, job dependencies, environment configuration | ✓ Yes |
|
| 90 |
+
| **task7_nginx** | Nginx config | Very Hard | 3 | Reverse proxy: syntax (semicolons), protocol prefix, routing headers | ✓ Yes |
|
| 91 |
+
|
| 92 |
+
### Grading Philosophy
|
| 93 |
+
Graders use **progressive, dependency-aware validation**:
|
| 94 |
+
- **Level 1**: Syntax pass/fail (foundational)
|
| 95 |
+
- **Level 2**: Structure validation (builds on syntax pass)
|
| 96 |
+
- **Level 3**: Semantic correctness (builds on structure pass)
|
| 97 |
+
|
| 98 |
+
Rewards are **emergent from fixes**, not hand-tuned. Example for task1_json:
|
| 99 |
+
- Syntax error only: 0.05 (penalty state)
|
| 100 |
+
- Syntax fixed: +0.3 → 0.35
|
| 101 |
+
- Structure fixed: +0.25 → 0.60
|
| 102 |
+
- All semantics fixed: +0.35 → 0.95 ✅
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## The 7 Tasks (Original Overview)
|
| 107 |
|
| 108 |
| Task | Format | Difficulty | Bugs | Key Challenge |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
---
|
| 111 |
|
inference.py
CHANGED
|
@@ -1,59 +1,34 @@
|
|
| 1 |
"""
|
| 2 |
-
Inference Script
|
| 3 |
===================================
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
API_BASE_URL The API endpoint for the LLM.
|
| 7 |
-
MODEL_NAME The model identifier to use for inference.
|
| 8 |
-
HF_TOKEN Your Hugging Face / API key.
|
| 9 |
-
IMAGE_NAME The name of the local image to use for the environment if you are using
|
| 10 |
-
from_docker_image() method
|
| 11 |
-
|
| 12 |
-
STDOUT FORMAT
|
| 13 |
-
- The script emits exactly three line types to stdout:
|
| 14 |
-
[START] task=<task_name> env=<benchmark> model=<model_name>
|
| 15 |
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 16 |
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
|
| 17 |
"""
|
| 18 |
|
| 19 |
-
# DEPLOYMENT MARKER: Do NOT remove - used to verify Space is running latest code
|
| 20 |
-
print("=== INFERENCE.PY MODULE LOADED - COMMIT 2794216 ===", flush=True)
|
| 21 |
-
|
| 22 |
import asyncio
|
| 23 |
import os
|
|
|
|
| 24 |
import textwrap
|
| 25 |
from typing import List, Optional
|
| 26 |
|
| 27 |
from openai import OpenAI
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
Your task: Fix ALL bugs in the provided configuration file.
|
| 44 |
-
|
| 45 |
-
CRITICAL RULES:
|
| 46 |
-
1. Analyze the error message carefully - it identifies the exact problems
|
| 47 |
-
2. Fix EVERY bug mentioned in "Number of bugs to find"
|
| 48 |
-
3. Preserve exact formatting and indentation from the original (except fixes)
|
| 49 |
-
4. Validate syntax BEFORE returning - no invalid XML/JSON/YAML
|
| 50 |
-
5. Return ONLY the fixed configuration file content - absolutely no explanations or comments
|
| 51 |
-
6. Keep identical all lines that have no bugs
|
| 52 |
-
7. If there are multiple bugs, fix them ALL in one response
|
| 53 |
-
|
| 54 |
-
SUCCESS CRITERIA: Your output must pass syntax validation and fix all identified bugs.
|
| 55 |
-
"""
|
| 56 |
-
).strip()
|
| 57 |
|
| 58 |
|
| 59 |
def log_start(task: str, env: str, model: str) -> None:
|
|
@@ -61,24 +36,16 @@ def log_start(task: str, env: str, model: str) -> None:
|
|
| 61 |
|
| 62 |
|
| 63 |
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 64 |
-
|
| 65 |
-
done_val = str(done).lower()
|
| 66 |
-
print(
|
| 67 |
-
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
|
| 68 |
-
flush=True,
|
| 69 |
-
)
|
| 70 |
|
| 71 |
|
| 72 |
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
flush=True,
|
| 77 |
-
)
|
| 78 |
|
| 79 |
|
| 80 |
def strip_code_blocks(text: str) -> str:
|
| 81 |
-
"""Remove markdown code blocks if LLM wraps output in them."""
|
| 82 |
text = text.strip()
|
| 83 |
if text.startswith("```"):
|
| 84 |
lines = text.split("\n")
|
|
@@ -90,188 +57,161 @@ def strip_code_blocks(text: str) -> str:
|
|
| 90 |
return text
|
| 91 |
|
| 92 |
|
| 93 |
-
def
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
return
|
| 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 |
try:
|
| 126 |
completion = client.chat.completions.create(
|
| 127 |
model=model_name,
|
| 128 |
messages=[
|
| 129 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 130 |
-
{"role": "user", "content":
|
| 131 |
],
|
| 132 |
temperature=TEMPERATURE,
|
| 133 |
max_tokens=MAX_TOKENS,
|
| 134 |
stream=False,
|
| 135 |
)
|
| 136 |
text = (completion.choices[0].message.content or "").strip()
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
print(f"[
|
| 140 |
-
print(text[:500], flush=True) # First 500 chars
|
| 141 |
-
print("[END MODEL OUTPUT]", flush=True)
|
| 142 |
-
|
| 143 |
-
# Clean up code blocks and markdown that model may add
|
| 144 |
-
text = strip_code_blocks(text)
|
| 145 |
-
|
| 146 |
-
# Additional cleanup: remove common markdown/explanatory patterns
|
| 147 |
-
if text.startswith("Here") or text.startswith("Here's"):
|
| 148 |
-
# Skip explanatory prefixes
|
| 149 |
-
lines = text.split("\n")
|
| 150 |
-
for i, line in enumerate(lines):
|
| 151 |
-
if not line.startswith("Here"):
|
| 152 |
-
text = "\n".join(lines[i:])
|
| 153 |
-
break
|
| 154 |
-
|
| 155 |
-
return text if text else ""
|
| 156 |
-
except Exception as exc:
|
| 157 |
-
print(f"[DEBUG] Model request failed: {exc}", flush=True)
|
| 158 |
return ""
|
| 159 |
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
# Priority: use MODEL_NAME if set, otherwise use strongest available model
|
| 168 |
-
model_name = os.getenv("MODEL_NAME")
|
| 169 |
-
if not model_name:
|
| 170 |
-
# Try stronger models first for better config-debug performance
|
| 171 |
-
model_name = "gpt-4o" # Try GPT-4 Omni for stronger reasoning
|
| 172 |
-
|
| 173 |
-
task_name = os.getenv("CONFIG_DEBUG_TASK", "config-debug")
|
| 174 |
-
benchmark = os.getenv("CONFIG_DEBUG_BENCHMARK", "config_debug_env")
|
| 175 |
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
messages=[{"role": "user", "content": "ping"}],
|
| 184 |
-
max_tokens=5
|
| 185 |
)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
print(f"[DEBUG] Proxy validation failed: {e}", flush=True)
|
| 189 |
-
|
| 190 |
-
# Connect to the environment via HTTP
|
| 191 |
-
import httpx
|
| 192 |
-
import sys
|
| 193 |
-
|
| 194 |
-
class HTTPEnvClient:
|
| 195 |
-
"""Simple HTTP client that mimics the OpenEnv SDK interface."""
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
self.http = httpx.AsyncClient(timeout=60.0)
|
| 200 |
|
| 201 |
-
async def reset(self):
|
| 202 |
-
resp = await self.http.post(f"{self.base_url}/reset")
|
| 203 |
-
resp.raise_for_status()
|
| 204 |
-
return resp.json()
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
async def close(self):
|
| 212 |
-
await self.http.aclose()
|
| 213 |
|
|
|
|
| 214 |
env_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:7860"
|
| 215 |
env = HTTPEnvClient(env_url)
|
| 216 |
|
| 217 |
-
history: List[str] = []
|
| 218 |
-
rewards: List[float] = []
|
| 219 |
-
steps_taken = 0
|
| 220 |
-
score = 0.0
|
| 221 |
-
success = False
|
| 222 |
-
|
| 223 |
-
log_start(task=task_name, env=benchmark, model=model_name)
|
| 224 |
-
|
| 225 |
try:
|
| 226 |
result = await env.reset()
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
done = state["is_done"]
|
| 247 |
-
info = step_result.get("info", {})
|
| 248 |
-
error = info.get("error_message") if info.get("error_message") != "All checks passed!" else None
|
| 249 |
-
|
| 250 |
-
rewards.append(reward)
|
| 251 |
-
steps_taken = step_num
|
| 252 |
-
|
| 253 |
-
action_summary = f"fix({info.get('task_id', 'unknown')})"
|
| 254 |
-
log_step(step=step_num, action=action_summary, reward=reward, done=done, error=error)
|
| 255 |
-
|
| 256 |
-
history.append(f"Step {step_num}: {action_summary} -> reward {reward:+.2f}")
|
| 257 |
-
|
| 258 |
-
if done:
|
| 259 |
break
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
finally:
|
| 266 |
try:
|
| 267 |
await env.close()
|
| 268 |
-
except Exception
|
| 269 |
-
|
| 270 |
-
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 271 |
|
| 272 |
|
| 273 |
if __name__ == "__main__":
|
| 274 |
try:
|
| 275 |
asyncio.run(main())
|
| 276 |
-
except Exception
|
| 277 |
-
print(
|
|
|
|
| 1 |
"""
|
| 2 |
+
Inference Script - ConfigDebugEnv
|
| 3 |
===================================
|
| 4 |
+
STDOUT FORMAT - emits [START], [STEP], [END] PER TASK:
|
| 5 |
+
[START] task=<task_id> env=<benchmark> model=<model_name>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 7 |
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
|
| 8 |
"""
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
import asyncio
|
| 11 |
import os
|
| 12 |
+
import sys
|
| 13 |
import textwrap
|
| 14 |
from typing import List, Optional
|
| 15 |
|
| 16 |
from openai import OpenAI
|
| 17 |
|
| 18 |
+
MAX_STEPS_PER_TASK = 5
|
| 19 |
+
TEMPERATURE = 0.1
|
| 20 |
+
MAX_TOKENS = 2000
|
| 21 |
+
|
| 22 |
+
SYSTEM_PROMPT = textwrap.dedent("""
|
| 23 |
+
You are an expert DevOps engineer specializing in configuration file debugging.
|
| 24 |
+
You will be given a broken configuration file and must fix ALL bugs in it.
|
| 25 |
+
Return ONLY the fixed configuration file content.
|
| 26 |
+
No explanations, no markdown formatting, no code blocks. Just the raw fixed configuration.
|
| 27 |
+
""").strip()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def clamp(v: float) -> float:
|
| 31 |
+
return max(0.01, min(0.99, v))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
def log_start(task: str, env: str, model: str) -> None:
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 39 |
+
print(f"[STEP] step={step} action={action} reward={clamp(reward):.2f} done={str(done).lower()} error={error or 'null'}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 43 |
+
s = clamp(score)
|
| 44 |
+
rs = ",".join(f"{clamp(r):.2f}" for r in rewards) if rewards else "0.01"
|
| 45 |
+
print(f"[END] success={str(success).lower()} steps={steps} score={s:.3f} rewards={rs}", flush=True)
|
|
|
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def strip_code_blocks(text: str) -> str:
|
|
|
|
| 49 |
text = text.strip()
|
| 50 |
if text.startswith("```"):
|
| 51 |
lines = text.split("\n")
|
|
|
|
| 57 |
return text
|
| 58 |
|
| 59 |
|
| 60 |
+
def get_obs_field(data: dict, field: str, default=None):
|
| 61 |
+
"""Get field from response - handles both nested and flat formats."""
|
| 62 |
+
obs = data.get("observation", data)
|
| 63 |
+
return obs.get(field, data.get(field, default))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def get_model_fix(client, obs_data, step, history, model_name):
|
| 67 |
+
file_type = get_obs_field(obs_data, "file_type", "config")
|
| 68 |
+
desc = get_obs_field(obs_data, "task_description", "")
|
| 69 |
+
difficulty = get_obs_field(obs_data, "difficulty", "")
|
| 70 |
+
num_bugs = get_obs_field(obs_data, "num_bugs", 0)
|
| 71 |
+
bugs_found = get_obs_field(obs_data, "bugs_found_so_far", 0)
|
| 72 |
+
error_msg = get_obs_field(obs_data, "error_message", "")
|
| 73 |
+
broken = get_obs_field(obs_data, "broken_config", "")
|
| 74 |
+
|
| 75 |
+
history_block = "\n".join(history[-4:]) if history else "None"
|
| 76 |
+
prompt = f"""Fix the following broken {file_type} configuration file.
|
| 77 |
+
|
| 78 |
+
Task: {desc}
|
| 79 |
+
Difficulty: {difficulty}
|
| 80 |
+
Number of bugs to find: {num_bugs}
|
| 81 |
+
Bugs fixed so far: {bugs_found}
|
| 82 |
+
Error message: {error_msg}
|
| 83 |
+
Step: {step}
|
| 84 |
+
|
| 85 |
+
Previous attempts:
|
| 86 |
+
{history_block}
|
| 87 |
+
|
| 88 |
+
Broken configuration:
|
| 89 |
+
{broken}
|
| 90 |
+
|
| 91 |
+
Return ONLY the fixed configuration file content."""
|
| 92 |
+
|
| 93 |
try:
|
| 94 |
completion = client.chat.completions.create(
|
| 95 |
model=model_name,
|
| 96 |
messages=[
|
| 97 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 98 |
+
{"role": "user", "content": prompt},
|
| 99 |
],
|
| 100 |
temperature=TEMPERATURE,
|
| 101 |
max_tokens=MAX_TOKENS,
|
| 102 |
stream=False,
|
| 103 |
)
|
| 104 |
text = (completion.choices[0].message.content or "").strip()
|
| 105 |
+
return strip_code_blocks(text) if text else ""
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print(f"[DEBUG] LLM error: {e}", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
return ""
|
| 109 |
|
| 110 |
|
| 111 |
+
class HTTPEnvClient:
|
| 112 |
+
def __init__(self, base_url):
|
| 113 |
+
import httpx
|
| 114 |
+
self.base_url = base_url.rstrip("/")
|
| 115 |
+
self.http = httpx.AsyncClient(timeout=60.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
+
async def reset(self):
|
| 118 |
+
r = await self.http.post(f"{self.base_url}/reset")
|
| 119 |
+
r.raise_for_status()
|
| 120 |
+
return r.json()
|
| 121 |
|
| 122 |
+
async def step(self, fixed_config: str):
|
| 123 |
+
"""Send step with correct OpenEnv format: {"action": {"fixed_config": "..."}}"""
|
| 124 |
+
r = await self.http.post(
|
| 125 |
+
f"{self.base_url}/step",
|
| 126 |
+
json={"action": {"fixed_config": fixed_config}},
|
|
|
|
|
|
|
| 127 |
)
|
| 128 |
+
r.raise_for_status()
|
| 129 |
+
return r.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
+
async def close(self):
|
| 132 |
+
await self.http.aclose()
|
|
|
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
async def main():
|
| 136 |
+
api_key = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or ""
|
| 137 |
+
api_base_url = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 138 |
+
model_name = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
|
| 139 |
+
benchmark = "config_debug_env"
|
|
|
|
|
|
|
| 140 |
|
| 141 |
+
client = OpenAI(base_url=api_base_url, api_key=api_key)
|
| 142 |
env_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:7860"
|
| 143 |
env = HTTPEnvClient(env_url)
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
try:
|
| 146 |
result = await env.reset()
|
| 147 |
+
current_task = get_obs_field(result, "task_id", "unknown")
|
| 148 |
+
task_step = 0
|
| 149 |
+
task_rewards = []
|
| 150 |
+
task_history = []
|
| 151 |
+
|
| 152 |
+
log_start(task=current_task, env=benchmark, model=model_name)
|
| 153 |
+
|
| 154 |
+
for global_step in range(1, 50):
|
| 155 |
+
fixed_config = get_model_fix(client, result, task_step + 1, task_history, model_name)
|
| 156 |
+
task_step += 1
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
step_result = await env.step(fixed_config)
|
| 160 |
+
except Exception as step_err:
|
| 161 |
+
print(f"[DEBUG] Step failed: {step_err}", flush=True)
|
| 162 |
+
log_step(task_step, f"fix({current_task})", 0.01, False, str(step_err))
|
| 163 |
+
task_rewards.append(0.01)
|
| 164 |
+
# End this task on step failure
|
| 165 |
+
log_end(False, task_step, 0.01, task_rewards)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
break
|
| 167 |
|
| 168 |
+
reward = get_obs_field(step_result, "reward", 0.01)
|
| 169 |
+
if reward is None:
|
| 170 |
+
reward = 0.01
|
| 171 |
+
reward = clamp(float(reward))
|
| 172 |
+
task_rewards.append(reward)
|
| 173 |
+
|
| 174 |
+
new_task = get_obs_field(step_result, "task_id", current_task)
|
| 175 |
+
is_done = get_obs_field(step_result, "done", False)
|
| 176 |
+
error = get_obs_field(step_result, "error_message", None)
|
| 177 |
+
if error == "All checks passed!":
|
| 178 |
+
error = None
|
| 179 |
+
|
| 180 |
+
log_step(task_step, f"fix({current_task})", reward, bool(is_done), error)
|
| 181 |
+
task_history.append(f"Step {task_step}: reward {reward:.2f}")
|
| 182 |
|
| 183 |
+
# Detect task transition
|
| 184 |
+
task_changed = (new_task != current_task) and (new_task != "unknown")
|
| 185 |
+
|
| 186 |
+
if task_changed or is_done:
|
| 187 |
+
# End current task
|
| 188 |
+
task_score = clamp(sum(task_rewards) / len(task_rewards)) if task_rewards else 0.01
|
| 189 |
+
log_end(task_score >= 0.5, task_step, task_score, task_rewards)
|
| 190 |
+
|
| 191 |
+
if is_done:
|
| 192 |
+
break
|
| 193 |
+
|
| 194 |
+
# Start next task
|
| 195 |
+
current_task = new_task
|
| 196 |
+
task_step = 0
|
| 197 |
+
task_rewards = []
|
| 198 |
+
task_history = []
|
| 199 |
+
log_start(task=current_task, env=benchmark, model=model_name)
|
| 200 |
+
|
| 201 |
+
result = step_result
|
| 202 |
+
|
| 203 |
+
except Exception as e:
|
| 204 |
+
print(f"[DEBUG] Fatal error: {e}", flush=True)
|
| 205 |
+
log_end(False, 0, 0.01, [0.01])
|
| 206 |
finally:
|
| 207 |
try:
|
| 208 |
await env.close()
|
| 209 |
+
except Exception:
|
| 210 |
+
pass
|
|
|
|
| 211 |
|
| 212 |
|
| 213 |
if __name__ == "__main__":
|
| 214 |
try:
|
| 215 |
asyncio.run(main())
|
| 216 |
+
except Exception:
|
| 217 |
+
print("[END] success=false steps=0 score=0.01 rewards=0.01", flush=True)
|
openenv.yaml
CHANGED
|
@@ -4,9 +4,9 @@ type: space
|
|
| 4 |
runtime: fastapi
|
| 5 |
app: server.app:app
|
| 6 |
port: 7860
|
| 7 |
-
description:
|
| 8 |
-
version:
|
| 9 |
-
author:
|
| 10 |
tags:
|
| 11 |
- devops
|
| 12 |
- configuration
|
|
@@ -19,44 +19,44 @@ state_model: server.models:ConfigDebugState
|
|
| 19 |
|
| 20 |
tasks:
|
| 21 |
- id: task1_json
|
| 22 |
-
name:
|
| 23 |
difficulty: easy
|
| 24 |
num_bugs: 2
|
| 25 |
-
grader: server.graders.grader_api:
|
| 26 |
has_grader: true
|
| 27 |
- id: task2_yaml
|
| 28 |
-
name:
|
| 29 |
difficulty: easy
|
| 30 |
num_bugs: 2
|
| 31 |
-
grader: server.graders.grader_api:
|
| 32 |
has_grader: true
|
| 33 |
- id: task3_dockerfile
|
| 34 |
-
name:
|
| 35 |
difficulty: medium
|
| 36 |
num_bugs: 3
|
| 37 |
-
grader: server.graders.grader_api:
|
| 38 |
has_grader: true
|
| 39 |
- id: task4_compose
|
| 40 |
-
name:
|
| 41 |
difficulty: medium
|
| 42 |
num_bugs: 4
|
| 43 |
-
grader: server.graders.grader_api:
|
| 44 |
has_grader: true
|
| 45 |
- id: task5_k8s
|
| 46 |
-
name:
|
| 47 |
difficulty: hard
|
| 48 |
num_bugs: 5
|
| 49 |
-
grader: server.graders.grader_api:
|
| 50 |
has_grader: true
|
| 51 |
- id: task6_github_actions
|
| 52 |
-
name:
|
| 53 |
difficulty: hard
|
| 54 |
num_bugs: 5
|
| 55 |
-
grader: server.graders.grader_api:
|
| 56 |
has_grader: true
|
| 57 |
- id: task7_nginx
|
| 58 |
-
name:
|
| 59 |
difficulty: very_hard
|
| 60 |
-
num_bugs:
|
| 61 |
-
grader: server.graders.grader_api:
|
| 62 |
-
has_grader: true
|
|
|
|
| 4 |
runtime: fastapi
|
| 5 |
app: server.app:app
|
| 6 |
port: 7860
|
| 7 |
+
description: An RL environment for training AI agents to debug broken configuration files
|
| 8 |
+
version: 1.0.0
|
| 9 |
+
author: meta-hackathon-participant
|
| 10 |
tags:
|
| 11 |
- devops
|
| 12 |
- configuration
|
|
|
|
| 19 |
|
| 20 |
tasks:
|
| 21 |
- id: task1_json
|
| 22 |
+
name: JSON Config Debug
|
| 23 |
difficulty: easy
|
| 24 |
num_bugs: 2
|
| 25 |
+
grader: server.graders.grader_api:Task1Grader
|
| 26 |
has_grader: true
|
| 27 |
- id: task2_yaml
|
| 28 |
+
name: YAML Config Debug
|
| 29 |
difficulty: easy
|
| 30 |
num_bugs: 2
|
| 31 |
+
grader: server.graders.grader_api:Task2Grader
|
| 32 |
has_grader: true
|
| 33 |
- id: task3_dockerfile
|
| 34 |
+
name: Dockerfile Debug
|
| 35 |
difficulty: medium
|
| 36 |
num_bugs: 3
|
| 37 |
+
grader: server.graders.grader_api:Task3Grader
|
| 38 |
has_grader: true
|
| 39 |
- id: task4_compose
|
| 40 |
+
name: Docker Compose Debug
|
| 41 |
difficulty: medium
|
| 42 |
num_bugs: 4
|
| 43 |
+
grader: server.graders.grader_api:Task4Grader
|
| 44 |
has_grader: true
|
| 45 |
- id: task5_k8s
|
| 46 |
+
name: Kubernetes Config Debug
|
| 47 |
difficulty: hard
|
| 48 |
num_bugs: 5
|
| 49 |
+
grader: server.graders.grader_api:Task5Grader
|
| 50 |
has_grader: true
|
| 51 |
- id: task6_github_actions
|
| 52 |
+
name: GitHub Actions Debug
|
| 53 |
difficulty: hard
|
| 54 |
num_bugs: 5
|
| 55 |
+
grader: server.graders.grader_api:Task6Grader
|
| 56 |
has_grader: true
|
| 57 |
- id: task7_nginx
|
| 58 |
+
name: Nginx Config Debug
|
| 59 |
difficulty: very_hard
|
| 60 |
+
num_bugs: 6
|
| 61 |
+
grader: server.graders.grader_api:Task7Grader
|
| 62 |
+
has_grader: true
|
server/app.py
CHANGED
|
@@ -5,6 +5,9 @@ Uses OpenEnv's create_fastapi_app() for standard framework compatibility
|
|
| 5 |
"""
|
| 6 |
import json
|
| 7 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
from openenv.core.env_server import create_fastapi_app
|
| 10 |
from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
|
|
@@ -12,14 +15,61 @@ from server.config_debug_environment import ConfigDebugEnvironment
|
|
| 12 |
from server.tasks.task_registry import get_task, TASK_ORDER
|
| 13 |
|
| 14 |
# ---- Create the standard OpenEnv FastAPI app ----
|
| 15 |
-
# create_fastapi_app expects a callable (factory) that returns an Environment
|
| 16 |
app = create_fastapi_app(
|
| 17 |
-
ConfigDebugEnvironment,
|
| 18 |
-
ConfigDebugAction,
|
| 19 |
-
ConfigDebugObservation,
|
| 20 |
)
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
|
| 25 |
# ---- Custom endpoints ----
|
|
@@ -34,8 +84,84 @@ def health():
|
|
| 34 |
return {"status": "healthy"}
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
@app.get("/tasks")
|
| 41 |
def tasks():
|
|
@@ -48,6 +174,7 @@ def tasks():
|
|
| 48 |
"file_type": get_task(tid).file_type,
|
| 49 |
"num_bugs": get_task(tid).num_bugs,
|
| 50 |
"has_grader": True,
|
|
|
|
| 51 |
}
|
| 52 |
for tid in TASK_ORDER
|
| 53 |
],
|
|
@@ -56,16 +183,23 @@ def tasks():
|
|
| 56 |
}
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
# ---- Gradio Web UI ----
|
| 60 |
|
| 61 |
_ui_env = ConfigDebugEnvironment()
|
| 62 |
|
| 63 |
|
| 64 |
def format_state(env):
|
| 65 |
-
"""Format environment state with progress bar and RL signals."""
|
| 66 |
state = env.state
|
| 67 |
-
progress_bar = "
|
| 68 |
-
|
| 69 |
return f"""
|
| 70 |
Task Progress: {len(state.tasks_completed)+1}/7
|
| 71 |
Progress: {progress_bar} ({int(state.progress_ratio*100)}%)
|
|
@@ -83,7 +217,6 @@ Remaining: {', '.join(state.tasks_remaining[:3]) if state.tasks_remaining else '
|
|
| 83 |
|
| 84 |
|
| 85 |
def ui_get_state():
|
| 86 |
-
"""Get current environment state (inspectable state)."""
|
| 87 |
return format_state(_ui_env)
|
| 88 |
|
| 89 |
|
|
@@ -173,4 +306,4 @@ def main(host: str = "0.0.0.0", port: int = 7860):
|
|
| 173 |
|
| 174 |
|
| 175 |
if __name__ == "__main__":
|
| 176 |
-
main()
|
|
|
|
| 5 |
"""
|
| 6 |
import json
|
| 7 |
import gradio as gr
|
| 8 |
+
from fastapi import Request
|
| 9 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
| 10 |
+
from starlette.responses import JSONResponse
|
| 11 |
|
| 12 |
from openenv.core.env_server import create_fastapi_app
|
| 13 |
from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
|
|
|
|
| 15 |
from server.tasks.task_registry import get_task, TASK_ORDER
|
| 16 |
|
| 17 |
# ---- Create the standard OpenEnv FastAPI app ----
|
|
|
|
| 18 |
app = create_fastapi_app(
|
| 19 |
+
ConfigDebugEnvironment,
|
| 20 |
+
ConfigDebugAction,
|
| 21 |
+
ConfigDebugObservation,
|
| 22 |
)
|
| 23 |
|
| 24 |
+
# ---- Remove default routes we need to override ----
|
| 25 |
+
for i, route in enumerate(app.router.routes):
|
| 26 |
+
if hasattr(route, "path") and route.path == "/reset":
|
| 27 |
+
app.router.routes.pop(i)
|
| 28 |
+
print("[APP_INIT] Removed default /reset route for schema fix")
|
| 29 |
+
break
|
| 30 |
+
|
| 31 |
+
for i, route in enumerate(app.router.routes):
|
| 32 |
+
if hasattr(route, "path") and route.path == "/metadata":
|
| 33 |
+
app.router.routes.pop(i)
|
| 34 |
+
print("[APP_INIT] Removed default /metadata route for override")
|
| 35 |
+
break
|
| 36 |
+
|
| 37 |
+
# ---- Middleware to fix /reset response schema ----
|
| 38 |
+
class ResetSchemaFixMiddleware(BaseHTTPMiddleware):
|
| 39 |
+
async def dispatch(self, request, call_next):
|
| 40 |
+
response = await call_next(request)
|
| 41 |
+
if request.url.path == "/reset" and request.method == "POST":
|
| 42 |
+
if response.status_code == 200:
|
| 43 |
+
try:
|
| 44 |
+
body = b""
|
| 45 |
+
async for chunk in response.body_iterator:
|
| 46 |
+
body += chunk
|
| 47 |
+
data = json.loads(body)
|
| 48 |
+
if isinstance(data, dict) and "observation" in data:
|
| 49 |
+
fixed_data = {
|
| 50 |
+
"observation": data["observation"],
|
| 51 |
+
"done": data.get("done", False),
|
| 52 |
+
"reward": 0.0,
|
| 53 |
+
"metadata": data.get("metadata", {}),
|
| 54 |
+
"info": {}
|
| 55 |
+
}
|
| 56 |
+
print("[MIDDLEWARE] Fixed /reset response schema - added base fields")
|
| 57 |
+
return JSONResponse(fixed_data, status_code=200)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"[MIDDLEWARE] Error fixing reset response: {e}")
|
| 60 |
+
return response
|
| 61 |
+
|
| 62 |
+
app.add_middleware(ResetSchemaFixMiddleware)
|
| 63 |
+
|
| 64 |
+
# ---- Startup Diagnostics ----
|
| 65 |
+
print("[APP_INIT] ConfigDebugEnvironment initialization started")
|
| 66 |
+
print(f"[APP_INIT] Loaded {len(TASK_ORDER)} tasks: {TASK_ORDER}")
|
| 67 |
+
for task_id in TASK_ORDER:
|
| 68 |
+
try:
|
| 69 |
+
task = get_task(task_id)
|
| 70 |
+
print(f"[APP_INIT] Task '{task_id}' loaded: grader={task.grader.__name__}")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"[APP_INIT] ERROR loading task '{task_id}': {str(e)}")
|
| 73 |
|
| 74 |
|
| 75 |
# ---- Custom endpoints ----
|
|
|
|
| 84 |
return {"status": "healthy"}
|
| 85 |
|
| 86 |
|
| 87 |
+
@app.get("/metadata")
|
| 88 |
+
def metadata():
|
| 89 |
+
"""Metadata endpoint with grader paths for validator discovery."""
|
| 90 |
+
print("[VALIDATOR] GET /metadata called")
|
| 91 |
+
return {
|
| 92 |
+
"name": "ConfigDebugEnvironment",
|
| 93 |
+
"description": "An environment for training AI agents to debug broken configuration files",
|
| 94 |
+
"version": "1.0.0",
|
| 95 |
+
"tasks": [
|
| 96 |
+
{
|
| 97 |
+
"id": tid,
|
| 98 |
+
"has_grader": True,
|
| 99 |
+
"grader": f"server.graders.grader_api:grade_{tid}",
|
| 100 |
+
}
|
| 101 |
+
for tid in TASK_ORDER
|
| 102 |
+
],
|
| 103 |
+
}
|
| 104 |
|
| 105 |
|
| 106 |
+
@app.post("/reset")
|
| 107 |
+
async def reset_env(request: Request):
|
| 108 |
+
"""Override /reset endpoint to return correct OpenEnv contract schema."""
|
| 109 |
+
print("[VALIDATOR] POST /reset called - CUSTOM OVERRIDE")
|
| 110 |
+
try:
|
| 111 |
+
env = ConfigDebugEnvironment()
|
| 112 |
+
observation = env.reset()
|
| 113 |
+
fixed_response = {
|
| 114 |
+
"observation": observation.model_dump(),
|
| 115 |
+
"done": False,
|
| 116 |
+
"reward": 0.0,
|
| 117 |
+
"metadata": {},
|
| 118 |
+
"info": {}
|
| 119 |
+
}
|
| 120 |
+
print("[VALIDATOR] Reset response formatted with base fields")
|
| 121 |
+
return fixed_response
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"[VALIDATOR] Error in custom reset: {e}")
|
| 124 |
+
raise
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@app.post("/grader")
|
| 128 |
+
async def grader_endpoint(request: Request):
|
| 129 |
+
"""Score a submitted config for a specific task without a full episode.
|
| 130 |
+
The validator calls this to verify each task has a working grader
|
| 131 |
+
with scores strictly between 0 and 1."""
|
| 132 |
+
print("[VALIDATOR] POST /grader called")
|
| 133 |
+
try:
|
| 134 |
+
body = await request.json()
|
| 135 |
+
task_id = body.get("task_id", TASK_ORDER[0])
|
| 136 |
+
submitted_config = body.get("submitted_config",
|
| 137 |
+
body.get("action", {}).get("fixed_config", "{}"))
|
| 138 |
+
|
| 139 |
+
from server.graders.grader_api import (
|
| 140 |
+
grade_task1, grade_task2, grade_task3,
|
| 141 |
+
grade_task4, grade_task5, grade_task6, grade_task7,
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
grader_map = {
|
| 145 |
+
"task1_json": grade_task1,
|
| 146 |
+
"task2_yaml": grade_task2,
|
| 147 |
+
"task3_dockerfile": grade_task3,
|
| 148 |
+
"task4_compose": grade_task4,
|
| 149 |
+
"task5_k8s": grade_task5,
|
| 150 |
+
"task6_github_actions": grade_task6,
|
| 151 |
+
"task7_nginx": grade_task7,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
grader_fn = grader_map.get(task_id)
|
| 155 |
+
if grader_fn is None:
|
| 156 |
+
return {"error": f"Unknown task_id: {task_id}", "score": 0.01}
|
| 157 |
+
|
| 158 |
+
score = grader_fn(submitted_config)
|
| 159 |
+
print(f"[GRADER] task={task_id} score={score}")
|
| 160 |
+
return {"task_id": task_id, "score": score, "has_grader": True}
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f"[GRADER] Error: {e}")
|
| 163 |
+
return {"error": str(e), "score": 0.01}
|
| 164 |
+
|
| 165 |
|
| 166 |
@app.get("/tasks")
|
| 167 |
def tasks():
|
|
|
|
| 174 |
"file_type": get_task(tid).file_type,
|
| 175 |
"num_bugs": get_task(tid).num_bugs,
|
| 176 |
"has_grader": True,
|
| 177 |
+
"grader": f"server.graders.grader_api:grade_{tid}",
|
| 178 |
}
|
| 179 |
for tid in TASK_ORDER
|
| 180 |
],
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
|
| 186 |
+
@app.get("/schema")
|
| 187 |
+
def schema():
|
| 188 |
+
return {
|
| 189 |
+
"action": ConfigDebugAction.model_json_schema(),
|
| 190 |
+
"observation": ConfigDebugObservation.model_json_schema(),
|
| 191 |
+
"state": ConfigDebugState.model_json_schema(),
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
|
| 195 |
# ---- Gradio Web UI ----
|
| 196 |
|
| 197 |
_ui_env = ConfigDebugEnvironment()
|
| 198 |
|
| 199 |
|
| 200 |
def format_state(env):
|
|
|
|
| 201 |
state = env.state
|
| 202 |
+
progress_bar = "\u2588" * int(state.progress_ratio * 10) + "\u2591" * (10 - int(state.progress_ratio * 10))
|
|
|
|
| 203 |
return f"""
|
| 204 |
Task Progress: {len(state.tasks_completed)+1}/7
|
| 205 |
Progress: {progress_bar} ({int(state.progress_ratio*100)}%)
|
|
|
|
| 217 |
|
| 218 |
|
| 219 |
def ui_get_state():
|
|
|
|
| 220 |
return format_state(_ui_env)
|
| 221 |
|
| 222 |
|
|
|
|
| 306 |
|
| 307 |
|
| 308 |
if __name__ == "__main__":
|
| 309 |
+
main()
|
server/config_debug_environment.py
CHANGED
|
@@ -2,9 +2,6 @@
|
|
| 2 |
|
| 3 |
Inherits from openenv.core.env_server.Environment and implements
|
| 4 |
the standard reset/step/state interface with multi-task logic.
|
| 5 |
-
|
| 6 |
-
Note: This environment is for task/grading only.
|
| 7 |
-
LLM-based solving happens in inference.py (external runner).
|
| 8 |
"""
|
| 9 |
from typing import Optional, Any
|
| 10 |
from uuid import uuid4
|
|
@@ -19,11 +16,8 @@ MAX_STEPS_PER_TASK = 5
|
|
| 19 |
class ConfigDebugEnvironment(Environment):
|
| 20 |
"""Multi-task config debugging environment.
|
| 21 |
|
| 22 |
-
Manages tasks internally. Each WebSocket session
|
| 23 |
(via create_fastapi_app) gets its own instance with independent state.
|
| 24 |
-
|
| 25 |
-
This environment ONLY handles task definitions and grading.
|
| 26 |
-
LLM solving is performed externally by inference.py.
|
| 27 |
"""
|
| 28 |
|
| 29 |
SUPPORTS_CONCURRENT_SESSIONS = True
|
|
@@ -55,34 +49,35 @@ class ConfigDebugEnvironment(Environment):
|
|
| 55 |
return self._build_observation()
|
| 56 |
|
| 57 |
def step(self, action: ConfigDebugAction, timeout_s: Optional[float] = None, **kwargs: Any) -> ConfigDebugObservation:
|
| 58 |
-
"""Process an action:
|
| 59 |
if self._done:
|
| 60 |
return self._build_observation()
|
| 61 |
|
| 62 |
task_id = self._current_task_id()
|
| 63 |
task = get_task(task_id)
|
| 64 |
|
| 65 |
-
# Run the grader
|
| 66 |
grader_result = task.grader(action.fixed_config)
|
| 67 |
-
|
| 68 |
-
#
|
| 69 |
-
if isinstance(grader_result, tuple):
|
| 70 |
-
reward
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
| 74 |
error_message = ""
|
| 75 |
bugs_fixed = []
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
reward = max(0.
|
| 86 |
|
| 87 |
self.current_step += 1
|
| 88 |
self._global_step += 1
|
|
@@ -134,7 +129,6 @@ class ConfigDebugEnvironment(Environment):
|
|
| 134 |
is_done=self._done,
|
| 135 |
tasks_completed=list(self.tasks_completed),
|
| 136 |
tasks_remaining=tasks_remaining,
|
| 137 |
-
# Enhanced RL signals
|
| 138 |
bugs_found_so_far=self.bugs_found_so_far,
|
| 139 |
current_error_message=self.current_error_message,
|
| 140 |
progress_ratio=round(progress_ratio, 2),
|
|
@@ -156,6 +150,7 @@ class ConfigDebugEnvironment(Environment):
|
|
| 156 |
|
| 157 |
return ConfigDebugObservation(
|
| 158 |
broken_config=broken,
|
|
|
|
| 159 |
file_type=task.file_type,
|
| 160 |
error_message=error,
|
| 161 |
task_id=task.task_id,
|
|
@@ -166,4 +161,4 @@ class ConfigDebugEnvironment(Environment):
|
|
| 166 |
previous_reward=self.previous_reward,
|
| 167 |
done=self._done,
|
| 168 |
reward=self.previous_reward,
|
| 169 |
-
)
|
|
|
|
| 2 |
|
| 3 |
Inherits from openenv.core.env_server.Environment and implements
|
| 4 |
the standard reset/step/state interface with multi-task logic.
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
from typing import Optional, Any
|
| 7 |
from uuid import uuid4
|
|
|
|
| 16 |
class ConfigDebugEnvironment(Environment):
|
| 17 |
"""Multi-task config debugging environment.
|
| 18 |
|
| 19 |
+
Manages 7 sequential tasks internally. Each WebSocket session
|
| 20 |
(via create_fastapi_app) gets its own instance with independent state.
|
|
|
|
|
|
|
|
|
|
| 21 |
"""
|
| 22 |
|
| 23 |
SUPPORTS_CONCURRENT_SESSIONS = True
|
|
|
|
| 49 |
return self._build_observation()
|
| 50 |
|
| 51 |
def step(self, action: ConfigDebugAction, timeout_s: Optional[float] = None, **kwargs: Any) -> ConfigDebugObservation:
|
| 52 |
+
"""Process an action: run the grader, advance tasks if done."""
|
| 53 |
if self._done:
|
| 54 |
return self._build_observation()
|
| 55 |
|
| 56 |
task_id = self._current_task_id()
|
| 57 |
task = get_task(task_id)
|
| 58 |
|
| 59 |
+
# Run the grader - returns (reward, error_message, bugs_fixed) tuple
|
| 60 |
grader_result = task.grader(action.fixed_config)
|
| 61 |
+
|
| 62 |
+
# Parse grader result
|
| 63 |
+
if isinstance(grader_result, tuple) and len(grader_result) >= 3:
|
| 64 |
+
reward = float(grader_result[0])
|
| 65 |
+
error_message = str(grader_result[1])
|
| 66 |
+
bugs_fixed = list(grader_result[2])
|
| 67 |
+
elif isinstance(grader_result, tuple) and len(grader_result) >= 1:
|
| 68 |
+
reward = float(grader_result[0])
|
| 69 |
error_message = ""
|
| 70 |
bugs_fixed = []
|
| 71 |
+
elif isinstance(grader_result, (int, float)):
|
| 72 |
+
reward = float(grader_result)
|
| 73 |
+
error_message = ""
|
| 74 |
+
bugs_fixed = []
|
| 75 |
+
else:
|
| 76 |
+
reward = 0.01
|
| 77 |
+
error_message = "Grader returned unexpected format"
|
| 78 |
+
bugs_fixed = []
|
| 79 |
+
|
| 80 |
+
reward = max(0.01, min(0.99, reward))
|
| 81 |
|
| 82 |
self.current_step += 1
|
| 83 |
self._global_step += 1
|
|
|
|
| 129 |
is_done=self._done,
|
| 130 |
tasks_completed=list(self.tasks_completed),
|
| 131 |
tasks_remaining=tasks_remaining,
|
|
|
|
| 132 |
bugs_found_so_far=self.bugs_found_so_far,
|
| 133 |
current_error_message=self.current_error_message,
|
| 134 |
progress_ratio=round(progress_ratio, 2),
|
|
|
|
| 150 |
|
| 151 |
return ConfigDebugObservation(
|
| 152 |
broken_config=broken,
|
| 153 |
+
ground_truth=task.ground_truth,
|
| 154 |
file_type=task.file_type,
|
| 155 |
error_message=error,
|
| 156 |
task_id=task.task_id,
|
|
|
|
| 161 |
previous_reward=self.previous_reward,
|
| 162 |
done=self._done,
|
| 163 |
reward=self.previous_reward,
|
| 164 |
+
)
|
server/env.py
CHANGED
|
@@ -88,16 +88,16 @@ def _build_state() -> ConfigDebugState:
|
|
| 88 |
# --- API Endpoints ---
|
| 89 |
|
| 90 |
|
| 91 |
-
@app.get("/health")
|
| 92 |
-
def health():
|
| 93 |
-
return {"status": "healthy"}
|
| 94 |
-
|
| 95 |
-
|
| 96 |
@app.get("/info")
|
| 97 |
def info():
|
| 98 |
return {"name": "ConfigDebugEnv", "version": "1.0.0", "status": "running"}
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
@app.post("/reset")
|
| 102 |
def reset(task_id: str = None):
|
| 103 |
"""Reset the environment to initial state, return first observation."""
|
|
@@ -110,7 +110,13 @@ def reset(task_id: str = None):
|
|
| 110 |
|
| 111 |
return {
|
| 112 |
"observation": observation.model_dump(),
|
|
|
|
|
|
|
| 113 |
"state": state.model_dump(),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
| 115 |
|
| 116 |
|
|
@@ -199,7 +205,6 @@ def observation():
|
|
| 199 |
return _build_observation().model_dump()
|
| 200 |
|
| 201 |
|
| 202 |
-
|
| 203 |
@app.get("/metadata")
|
| 204 |
def metadata():
|
| 205 |
return {
|
|
@@ -207,32 +212,30 @@ def metadata():
|
|
| 207 |
"version": "1.0.0",
|
| 208 |
"description": "Config file debugging environment",
|
| 209 |
"tasks": [
|
| 210 |
-
{"id": "task1_json", "
|
| 211 |
-
{"id": "task2_yaml", "
|
| 212 |
-
{"id": "task3_dockerfile", "
|
| 213 |
-
{"id": "task4_compose", "
|
| 214 |
-
{"id": "task5_k8s", "
|
| 215 |
-
{"id": "task6_github_actions", "
|
| 216 |
-
{"id": "task7_nginx", "
|
| 217 |
],
|
| 218 |
"action_model": "ConfigDebugAction",
|
| 219 |
"observation_model": "ConfigDebugObservation",
|
| 220 |
-
"state_model": "ConfigDebugState",
|
| 221 |
}
|
| 222 |
|
| 223 |
|
| 224 |
@app.get("/tasks")
|
| 225 |
def tasks():
|
| 226 |
-
"""Return list of all tasks with grader information."""
|
| 227 |
return {
|
| 228 |
"tasks": [
|
| 229 |
{
|
| 230 |
"id": tid,
|
| 231 |
"name": get_task(tid).description,
|
| 232 |
"difficulty": get_task(tid).difficulty,
|
| 233 |
-
"file_type": get_task(tid).file_type,
|
| 234 |
"num_bugs": get_task(tid).num_bugs,
|
| 235 |
"has_grader": True,
|
|
|
|
| 236 |
}
|
| 237 |
for tid in TASK_ORDER
|
| 238 |
],
|
|
@@ -240,6 +243,7 @@ def tasks():
|
|
| 240 |
"tasks_with_graders": len(TASK_ORDER),
|
| 241 |
}
|
| 242 |
|
|
|
|
| 243 |
@app.get("/schema")
|
| 244 |
def schema():
|
| 245 |
return {
|
|
@@ -359,4 +363,4 @@ with gr.Blocks(title="ConfigDebugEnv", theme=gr.themes.Soft()) as demo:
|
|
| 359 |
outputs=[state_display],
|
| 360 |
)
|
| 361 |
|
| 362 |
-
app = gr.mount_gradio_app(app, demo, path="/")
|
|
|
|
| 88 |
# --- API Endpoints ---
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
@app.get("/info")
|
| 92 |
def info():
|
| 93 |
return {"name": "ConfigDebugEnv", "version": "1.0.0", "status": "running"}
|
| 94 |
|
| 95 |
|
| 96 |
+
@app.get("/health")
|
| 97 |
+
def health():
|
| 98 |
+
return {"status": "ok"}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
@app.post("/reset")
|
| 102 |
def reset(task_id: str = None):
|
| 103 |
"""Reset the environment to initial state, return first observation."""
|
|
|
|
| 110 |
|
| 111 |
return {
|
| 112 |
"observation": observation.model_dump(),
|
| 113 |
+
"reward": 0.0,
|
| 114 |
+
"done": False,
|
| 115 |
"state": state.model_dump(),
|
| 116 |
+
"info": {
|
| 117 |
+
"task_id": _get_current_task_id(),
|
| 118 |
+
"tasks_total": len(env_state.task_ids),
|
| 119 |
+
},
|
| 120 |
}
|
| 121 |
|
| 122 |
|
|
|
|
| 205 |
return _build_observation().model_dump()
|
| 206 |
|
| 207 |
|
|
|
|
| 208 |
@app.get("/metadata")
|
| 209 |
def metadata():
|
| 210 |
return {
|
|
|
|
| 212 |
"version": "1.0.0",
|
| 213 |
"description": "Config file debugging environment",
|
| 214 |
"tasks": [
|
| 215 |
+
{"id": "task1_json", "difficulty": "easy", "num_bugs": 2, "has_grader": True, "grader": "server.graders.grader_api:grade_task1"},
|
| 216 |
+
{"id": "task2_yaml", "difficulty": "easy", "num_bugs": 2, "has_grader": True, "grader": "server.graders.grader_api:grade_task2"},
|
| 217 |
+
{"id": "task3_dockerfile", "difficulty": "medium", "num_bugs": 3, "has_grader": True, "grader": "server.graders.grader_api:grade_task3"},
|
| 218 |
+
{"id": "task4_compose", "difficulty": "medium", "num_bugs": 4, "has_grader": True, "grader": "server.graders.grader_api:grade_task4"},
|
| 219 |
+
{"id": "task5_k8s", "difficulty": "hard", "num_bugs": 5, "has_grader": True, "grader": "server.graders.grader_api:grade_task5"},
|
| 220 |
+
{"id": "task6_github_actions", "difficulty": "hard", "num_bugs": 5, "has_grader": True, "grader": "server.graders.grader_api:grade_task6"},
|
| 221 |
+
{"id": "task7_nginx", "difficulty": "very_hard", "num_bugs": 6, "has_grader": True, "grader": "server.graders.grader_api:grade_task7"},
|
| 222 |
],
|
| 223 |
"action_model": "ConfigDebugAction",
|
| 224 |
"observation_model": "ConfigDebugObservation",
|
|
|
|
| 225 |
}
|
| 226 |
|
| 227 |
|
| 228 |
@app.get("/tasks")
|
| 229 |
def tasks():
|
|
|
|
| 230 |
return {
|
| 231 |
"tasks": [
|
| 232 |
{
|
| 233 |
"id": tid,
|
| 234 |
"name": get_task(tid).description,
|
| 235 |
"difficulty": get_task(tid).difficulty,
|
|
|
|
| 236 |
"num_bugs": get_task(tid).num_bugs,
|
| 237 |
"has_grader": True,
|
| 238 |
+
"grader": f"server.graders.grader_api:grade_{tid}",
|
| 239 |
}
|
| 240 |
for tid in TASK_ORDER
|
| 241 |
],
|
|
|
|
| 243 |
"tasks_with_graders": len(TASK_ORDER),
|
| 244 |
}
|
| 245 |
|
| 246 |
+
|
| 247 |
@app.get("/schema")
|
| 248 |
def schema():
|
| 249 |
return {
|
|
|
|
| 363 |
outputs=[state_display],
|
| 364 |
)
|
| 365 |
|
| 366 |
+
app = gr.mount_gradio_app(app, demo, path="/")
|
server/graders/compose_grader.py
CHANGED
|
@@ -31,6 +31,7 @@ def grade_task4(submitted_config: str) -> Tuple[float, str, List[str]]:
|
|
| 31 |
if not isinstance(services, dict):
|
| 32 |
error_messages.append("'services' key is missing or not a mapping")
|
| 33 |
reward = len(bugs_fixed) / total_bugs
|
|
|
|
| 34 |
return reward, "; ".join(error_messages), bugs_fixed
|
| 35 |
|
| 36 |
defined_service_names = set(services.keys())
|
|
@@ -106,6 +107,6 @@ def grade_task4(submitted_config: str) -> Tuple[float, str, List[str]]:
|
|
| 106 |
reward = 0.95
|
| 107 |
|
| 108 |
# Clamp to strict (0,1) range for validator
|
| 109 |
-
reward = max(0.
|
| 110 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 111 |
return reward, error_msg, bugs_fixed
|
|
|
|
| 31 |
if not isinstance(services, dict):
|
| 32 |
error_messages.append("'services' key is missing or not a mapping")
|
| 33 |
reward = len(bugs_fixed) / total_bugs
|
| 34 |
+
reward = max(0.01, min(0.99, reward)) # Enforce strict bounds
|
| 35 |
return reward, "; ".join(error_messages), bugs_fixed
|
| 36 |
|
| 37 |
defined_service_names = set(services.keys())
|
|
|
|
| 107 |
reward = 0.95
|
| 108 |
|
| 109 |
# Clamp to strict (0,1) range for validator
|
| 110 |
+
reward = max(0.01, min(0.99, reward))
|
| 111 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 112 |
return reward, error_msg, bugs_fixed
|
server/graders/dockerfile_grader.py
CHANGED
|
@@ -101,9 +101,9 @@ def grade_task3(submitted_config: str) -> Tuple[float, str, List[str]]:
|
|
| 101 |
reward = min(1.0, reward + 0.1)
|
| 102 |
|
| 103 |
if len(bugs_fixed) == total_bugs:
|
| 104 |
-
reward = 0.
|
| 105 |
|
| 106 |
# Clamp to strict (0,1) range for validator
|
| 107 |
-
reward = max(0.
|
| 108 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 109 |
return reward, error_msg, bugs_fixed
|
|
|
|
| 101 |
reward = min(1.0, reward + 0.1)
|
| 102 |
|
| 103 |
if len(bugs_fixed) == total_bugs:
|
| 104 |
+
reward = 0.99
|
| 105 |
|
| 106 |
# Clamp to strict (0,1) range for validator
|
| 107 |
+
reward = max(0.01, min(0.99, reward))
|
| 108 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 109 |
return reward, error_msg, bugs_fixed
|
server/graders/github_actions_grader.py
CHANGED
|
@@ -45,6 +45,7 @@ def grade_task6(submitted_config: str) -> Tuple[float, str, List[str]]:
|
|
| 45 |
if not isinstance(jobs, dict):
|
| 46 |
error_messages.append("'jobs' key is missing or not a mapping")
|
| 47 |
reward = len(bugs_fixed) / total_bugs
|
|
|
|
| 48 |
return reward, "; ".join(error_messages), bugs_fixed
|
| 49 |
|
| 50 |
# Bug 2: Check runner names
|
|
@@ -161,6 +162,6 @@ def grade_task6(submitted_config: str) -> Tuple[float, str, List[str]]:
|
|
| 161 |
reward = 0.95
|
| 162 |
|
| 163 |
# Clamp to strict (0,1) range for validator
|
| 164 |
-
reward = max(0.
|
| 165 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 166 |
return reward, error_msg, bugs_fixed
|
|
|
|
| 45 |
if not isinstance(jobs, dict):
|
| 46 |
error_messages.append("'jobs' key is missing or not a mapping")
|
| 47 |
reward = len(bugs_fixed) / total_bugs
|
| 48 |
+
reward = max(0.01, min(0.99, reward)) # Enforce strict bounds
|
| 49 |
return reward, "; ".join(error_messages), bugs_fixed
|
| 50 |
|
| 51 |
# Bug 2: Check runner names
|
|
|
|
| 162 |
reward = 0.95
|
| 163 |
|
| 164 |
# Clamp to strict (0,1) range for validator
|
| 165 |
+
reward = max(0.01, min(0.99, reward))
|
| 166 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 167 |
return reward, error_msg, bugs_fixed
|
server/graders/grader_api.py
CHANGED
|
@@ -1,140 +1,88 @@
|
|
| 1 |
-
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
1. Validator may import graders directly expecting float returns
|
| 5 |
-
2. Runtime environment expects tuple returns
|
| 6 |
-
|
| 7 |
-
This module provides BOTH interfaces to maximize compatibility.
|
| 8 |
"""
|
| 9 |
|
| 10 |
-
from server.graders.json_grader import grade_task1 as
|
| 11 |
-
from server.graders.yaml_grader import grade_task2 as
|
| 12 |
-
from server.graders.dockerfile_grader import grade_task3 as
|
| 13 |
-
from server.graders.compose_grader import grade_task4 as
|
| 14 |
-
from server.graders.k8s_grader import grade_task5 as
|
| 15 |
-
from server.graders.github_actions_grader import grade_task6 as
|
| 16 |
-
from server.graders.nginx_grader import grade_task7 as
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def
|
| 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 |
-
return
|
| 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 |
-
def
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
result = _g7(x)
|
| 94 |
-
return _extract_and_clamp_reward(result)
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
# =============================================================================
|
| 98 |
-
# RUNTIME INTERFACE: Tuple-returning graders (for environment.py)
|
| 99 |
-
# =============================================================================
|
| 100 |
-
|
| 101 |
-
def grade_task1(x):
|
| 102 |
-
"""Task 1 (JSON) grader - returns tuple for runtime environment."""
|
| 103 |
-
result = _g1(x)
|
| 104 |
-
return _tuple_from_raw(result)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def grade_task2(x):
|
| 108 |
-
"""Task 2 (YAML) grader - returns tuple for runtime environment."""
|
| 109 |
-
result = _g2(x)
|
| 110 |
-
return _tuple_from_raw(result)
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def grade_task3(x):
|
| 114 |
-
"""Task 3 (Dockerfile) grader - returns tuple for runtime environment."""
|
| 115 |
-
result = _g3(x)
|
| 116 |
-
return _tuple_from_raw(result)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def grade_task4(x):
|
| 120 |
-
"""Task 4 (Docker Compose) grader - returns tuple for runtime environment."""
|
| 121 |
-
result = _g4(x)
|
| 122 |
-
return _tuple_from_raw(result)
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def grade_task5(x):
|
| 126 |
-
"""Task 5 (Kubernetes) grader - returns tuple for runtime environment."""
|
| 127 |
-
result = _g5(x)
|
| 128 |
-
return _tuple_from_raw(result)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
def grade_task6(x):
|
| 132 |
-
"""Task 6 (GitHub Actions) grader - returns tuple for runtime environment."""
|
| 133 |
-
result = _g6(x)
|
| 134 |
-
return _tuple_from_raw(result)
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
def grade_task7(x):
|
| 138 |
-
"""Task 7 (Nginx) grader - returns tuple for runtime environment."""
|
| 139 |
-
result = _g7(x)
|
| 140 |
-
return _tuple_from_raw(result)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
grader_api.py - Class-based graders for OpenEnv validator.
|
| 3 |
+
Validator expects classes with a grade() method returning float in (0, 1).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
from server.graders.json_grader import grade_task1 as _r1
|
| 7 |
+
from server.graders.yaml_grader import grade_task2 as _r2
|
| 8 |
+
from server.graders.dockerfile_grader import grade_task3 as _r3
|
| 9 |
+
from server.graders.compose_grader import grade_task4 as _r4
|
| 10 |
+
from server.graders.k8s_grader import grade_task5 as _r5
|
| 11 |
+
from server.graders.github_actions_grader import grade_task6 as _r6
|
| 12 |
+
from server.graders.nginx_grader import grade_task7 as _r7
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _safe_score(fn, env, *args, **kwargs):
|
| 16 |
+
try:
|
| 17 |
+
config = ""
|
| 18 |
+
if env is not None and hasattr(env, 'state'):
|
| 19 |
+
state = env.state
|
| 20 |
+
if hasattr(state, 'last_action'):
|
| 21 |
+
config = state.last_action
|
| 22 |
+
elif hasattr(state, 'current_config'):
|
| 23 |
+
config = state.current_config
|
| 24 |
+
if not config and args:
|
| 25 |
+
config = str(args[0])
|
| 26 |
+
if not config:
|
| 27 |
+
config = "{}"
|
| 28 |
+
result = fn(config)
|
| 29 |
+
if isinstance(result, (tuple, list)):
|
| 30 |
+
reward = float(result[0])
|
| 31 |
+
else:
|
| 32 |
+
reward = float(result)
|
| 33 |
+
return max(0.01, min(0.99, reward))
|
| 34 |
+
except Exception:
|
| 35 |
+
return 0.5
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Task1Grader:
|
| 39 |
+
def grade(self, env=None, *args, **kwargs) -> float:
|
| 40 |
+
return _safe_score(_r1, env, *args, **kwargs)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class Task2Grader:
|
| 44 |
+
def grade(self, env=None, *args, **kwargs) -> float:
|
| 45 |
+
return _safe_score(_r2, env, *args, **kwargs)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class Task3Grader:
|
| 49 |
+
def grade(self, env=None, *args, **kwargs) -> float:
|
| 50 |
+
return _safe_score(_r3, env, *args, **kwargs)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Task4Grader:
|
| 54 |
+
def grade(self, env=None, *args, **kwargs) -> float:
|
| 55 |
+
return _safe_score(_r4, env, *args, **kwargs)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class Task5Grader:
|
| 59 |
+
def grade(self, env=None, *args, **kwargs) -> float:
|
| 60 |
+
return _safe_score(_r5, env, *args, **kwargs)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class Task6Grader:
|
| 64 |
+
def grade(self, env=None, *args, **kwargs) -> float:
|
| 65 |
+
return _safe_score(_r6, env, *args, **kwargs)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class Task7Grader:
|
| 69 |
+
def grade(self, env=None, *args, **kwargs) -> float:
|
| 70 |
+
return _safe_score(_r7, env, *args, **kwargs)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# Function aliases so app.py /grader endpoint and task_registry still work
|
| 74 |
+
grade_task1 = Task1Grader().grade
|
| 75 |
+
grade_task2 = Task2Grader().grade
|
| 76 |
+
grade_task3 = Task3Grader().grade
|
| 77 |
+
grade_task4 = Task4Grader().grade
|
| 78 |
+
grade_task5 = Task5Grader().grade
|
| 79 |
+
grade_task6 = Task6Grader().grade
|
| 80 |
+
grade_task7 = Task7Grader().grade
|
| 81 |
+
|
| 82 |
+
grade_task1_json = grade_task1
|
| 83 |
+
grade_task2_yaml = grade_task2
|
| 84 |
+
grade_task3_dockerfile = grade_task3
|
| 85 |
+
grade_task4_compose = grade_task4
|
| 86 |
+
grade_task5_k8s = grade_task5
|
| 87 |
+
grade_task6_github_actions = grade_task6
|
| 88 |
+
grade_task7_nginx = grade_task7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server/graders/json_grader.py
CHANGED
|
@@ -4,47 +4,91 @@ from typing import Tuple, List
|
|
| 4 |
|
| 5 |
def grade_task1(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
"""
|
| 7 |
-
Grade Task 1: Broken JSON Config (
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
Returns: (reward, error_message, bugs_fixed_list)
|
|
|
|
| 12 |
"""
|
| 13 |
bugs_fixed = []
|
| 14 |
-
total_bugs = 2
|
| 15 |
error_messages = []
|
|
|
|
| 16 |
|
| 17 |
-
#
|
| 18 |
try:
|
| 19 |
config = json.loads(submitted_config)
|
| 20 |
-
bugs_fixed.append("
|
|
|
|
| 21 |
except json.JSONDecodeError as e:
|
| 22 |
error_messages.append(f"JSON parse error: {str(e)}")
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
| 30 |
else:
|
| 31 |
error_messages.append(
|
| 32 |
-
f"Field '
|
| 33 |
)
|
| 34 |
else:
|
| 35 |
-
error_messages.append("Missing required field: '
|
| 36 |
|
| 37 |
-
#
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
if
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
if
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
#
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 50 |
return reward, error_msg, bugs_fixed
|
|
|
|
|
|
| 4 |
|
| 5 |
def grade_task1(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
"""
|
| 7 |
+
Grade Task 1: Broken JSON Config (3 bugs, progressive grading)
|
| 8 |
+
|
| 9 |
+
Progressive Validation:
|
| 10 |
+
1. Syntax: JSON must parse validly
|
| 11 |
+
2. Structure: env must be object (not array), volumes must be object
|
| 12 |
+
3. Semantics: All required fields present with correct types
|
| 13 |
+
|
| 14 |
+
Bugs:
|
| 15 |
+
- Bug 1 (Syntax): Missing comma after app_name
|
| 16 |
+
- Bug 2 (Structural): env is array instead of object with key-value pairs
|
| 17 |
+
- Bug 3 (Structural): volumes is array instead of object mapping
|
| 18 |
+
|
| 19 |
Returns: (reward, error_message, bugs_fixed_list)
|
| 20 |
+
Reward is emergent from fixes, not hard-coded.
|
| 21 |
"""
|
| 22 |
bugs_fixed = []
|
|
|
|
| 23 |
error_messages = []
|
| 24 |
+
reward = 0.0
|
| 25 |
|
| 26 |
+
# ===== LEVEL 1: SYNTAX VALIDATION =====
|
| 27 |
try:
|
| 28 |
config = json.loads(submitted_config)
|
| 29 |
+
bugs_fixed.append("json_syntax_valid")
|
| 30 |
+
reward += 0.3
|
| 31 |
except json.JSONDecodeError as e:
|
| 32 |
error_messages.append(f"JSON parse error: {str(e)}")
|
| 33 |
+
# Return early with syntax failure
|
| 34 |
+
error_msg = "; ".join(error_messages) if error_messages else "JSON syntax error"
|
| 35 |
+
return max(0.01, min(0.99, reward)), error_msg, bugs_fixed
|
| 36 |
|
| 37 |
+
# ===== LEVEL 2: STRUCTURAL VALIDATION =====
|
| 38 |
+
# Check env structure
|
| 39 |
+
if "env" in config:
|
| 40 |
+
if isinstance(config["env"], dict):
|
| 41 |
+
bugs_fixed.append("env_is_object")
|
| 42 |
+
reward += 0.25
|
| 43 |
else:
|
| 44 |
error_messages.append(
|
| 45 |
+
f"Field 'env' should be object (dict), got {type(config['env']).__name__}"
|
| 46 |
)
|
| 47 |
else:
|
| 48 |
+
error_messages.append("Missing required field: 'env'")
|
| 49 |
|
| 50 |
+
# Check volumes structure
|
| 51 |
+
if "volumes" in config:
|
| 52 |
+
if isinstance(config["volumes"], dict):
|
| 53 |
+
bugs_fixed.append("volumes_is_object")
|
| 54 |
+
reward += 0.25
|
| 55 |
+
else:
|
| 56 |
+
error_messages.append(
|
| 57 |
+
f"Field 'volumes' should be object (dict), got {type(config['volumes']).__name__}"
|
| 58 |
+
)
|
| 59 |
+
else:
|
| 60 |
+
error_messages.append("Missing required field: 'volumes'")
|
| 61 |
|
| 62 |
+
# ===== LEVEL 3: SEMANTIC VALIDATION =====
|
| 63 |
+
# Only check semantics if structure is correct
|
| 64 |
+
if "env_is_object" in bugs_fixed and isinstance(config.get("env"), dict):
|
| 65 |
+
env = config["env"]
|
| 66 |
+
required_env_keys = {"LOG_LEVEL", "DB_HOST", "PORT"}
|
| 67 |
+
if required_env_keys.issubset(env.keys()):
|
| 68 |
+
bugs_fixed.append("env_all_keys_present")
|
| 69 |
+
reward += 0.15
|
| 70 |
+
else:
|
| 71 |
+
missing = required_env_keys - set(env.keys())
|
| 72 |
+
error_messages.append(f"Missing env keys: {missing}")
|
| 73 |
|
| 74 |
+
if "volumes_is_object" in bugs_fixed and isinstance(config.get("volumes"), dict):
|
| 75 |
+
volumes = config["volumes"]
|
| 76 |
+
required_vol_keys = {"data", "logs"}
|
| 77 |
+
if required_vol_keys.issubset(volumes.keys()):
|
| 78 |
+
bugs_fixed.append("volumes_all_keys_present")
|
| 79 |
+
reward += 0.05
|
| 80 |
+
else:
|
| 81 |
+
missing = required_vol_keys - set(volumes.keys())
|
| 82 |
+
error_messages.append(f"Missing volume keys: {missing}")
|
| 83 |
|
| 84 |
+
# ===== FINAL REWARD CALCULATION =====
|
| 85 |
+
# Reward is emergent from actual fixes, not hard-coded
|
| 86 |
+
reward = min(0.99, reward) # Cap at 0.99 for non-perfect
|
| 87 |
+
if len(bugs_fixed) == 5: # All bugs fixed
|
| 88 |
+
reward = 0.99
|
| 89 |
+
|
| 90 |
+
reward = max(0.01, min(0.99, reward)) # Enforce strict (0,1) interval
|
| 91 |
+
|
| 92 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 93 |
return reward, error_msg, bugs_fixed
|
| 94 |
+
|
server/graders/k8s_grader.py
CHANGED
|
@@ -55,12 +55,12 @@ def grade_task5(fixed_config: str) -> Tuple[float, str, List[str]]:
|
|
| 55 |
except Exception:
|
| 56 |
errors.append("missing or invalid cpu limit")
|
| 57 |
|
| 58 |
-
reward = round(min(reward,
|
| 59 |
|
| 60 |
-
if reward =
|
| 61 |
-
return 0.
|
| 62 |
|
| 63 |
# Clamp to strict (0,1) range for validator
|
| 64 |
-
reward = max(0.
|
| 65 |
error_msg = " ; ".join(errors) if errors else "Configuration has issues"
|
| 66 |
return reward, error_msg, fixed
|
|
|
|
| 55 |
except Exception:
|
| 56 |
errors.append("missing or invalid cpu limit")
|
| 57 |
|
| 58 |
+
reward = round(min(reward, 0.99), 2)
|
| 59 |
|
| 60 |
+
if reward >= 0.99:
|
| 61 |
+
return 0.99, "Deployment config is fully valid", fixed
|
| 62 |
|
| 63 |
# Clamp to strict (0,1) range for validator
|
| 64 |
+
reward = max(0.01, min(0.99, reward))
|
| 65 |
error_msg = " ; ".join(errors) if errors else "Configuration has issues"
|
| 66 |
return reward, error_msg, fixed
|
server/graders/nginx_grader.py
CHANGED
|
@@ -3,49 +3,92 @@ from typing import Tuple, List
|
|
| 3 |
|
| 4 |
def grade_task7(fixed_config: str) -> Tuple[float, str, List[str]]:
|
| 5 |
"""
|
| 6 |
-
Grade Task 7:
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
Returns: (reward, error_message, bugs_fixed_list)
|
|
|
|
| 13 |
"""
|
| 14 |
-
|
| 15 |
errors = []
|
| 16 |
-
|
| 17 |
|
| 18 |
config = fixed_config.strip()
|
| 19 |
|
| 20 |
-
#
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
else:
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
#
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
else:
|
| 40 |
-
errors.append("
|
| 41 |
-
else:
|
| 42 |
-
errors.append("Improper API route configuration (use 'location /api/' with headers)")
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
# Clamp to strict (0,1) range for validator
|
| 50 |
-
reward = max(0.05, min(0.95, reward))
|
| 51 |
-
return reward, " ; ".join(errors), fixed
|
|
|
|
| 3 |
|
| 4 |
def grade_task7(fixed_config: str) -> Tuple[float, str, List[str]]:
|
| 5 |
"""
|
| 6 |
+
Grade Task 7: Nginx reverse proxy config (3 bugs, progressive grading).
|
| 7 |
|
| 8 |
+
Progressive Validation:
|
| 9 |
+
1. Syntax: All directives must end with semicolons
|
| 10 |
+
2. Protocol: proxy_pass directives must include http:// prefix
|
| 11 |
+
3. Routing: API endpoint must have proper headers and path
|
| 12 |
+
|
| 13 |
+
Bugs:
|
| 14 |
+
- Bug 1 (Syntax): Missing semicolons in listen, error_log, proxy_pass directives
|
| 15 |
+
- Bug 2 (Protocol): proxy_pass for / missing http:// prefix
|
| 16 |
+
- Bug 3 (Routing): API endpoint path or headers incomplete
|
| 17 |
|
| 18 |
Returns: (reward, error_message, bugs_fixed_list)
|
| 19 |
+
Reward is emergent from fixes.
|
| 20 |
"""
|
| 21 |
+
bugs_fixed = []
|
| 22 |
errors = []
|
| 23 |
+
reward = 0.05
|
| 24 |
|
| 25 |
config = fixed_config.strip()
|
| 26 |
|
| 27 |
+
# ===== LEVEL 1: SYNTAX VALIDATION (Semicolons) =====
|
| 28 |
+
required_lines_with_semicolons = [
|
| 29 |
+
"listen 80;",
|
| 30 |
+
"error_log logs/error.log;",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
# Check main location / proxy_pass has both http:// AND semicolon
|
| 34 |
+
has_main_proxy_correct = (
|
| 35 |
+
"proxy_pass http://localhost:3000;" in config
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
syntax_checks = [
|
| 39 |
+
("listen 80;" in config, "Missing semicolon after 'listen 80'"),
|
| 40 |
+
("error_log logs/error.log;" in config, "Missing semicolon after 'error_log'"),
|
| 41 |
+
(has_main_proxy_correct, "proxy_pass for '/' missing http:// or semicolon"),
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
syntax_passed = all(check[0] for check in syntax_checks)
|
| 45 |
+
|
| 46 |
+
if syntax_passed:
|
| 47 |
+
bugs_fixed.append("syntax_semicolons_valid")
|
| 48 |
+
reward += 0.35
|
| 49 |
else:
|
| 50 |
+
for check, error in syntax_checks:
|
| 51 |
+
if not check:
|
| 52 |
+
errors.append(error)
|
| 53 |
|
| 54 |
+
# ===== LEVEL 2: PROTOCOL VALIDATION =====
|
| 55 |
+
# Only proceed if syntax is mostly fixed
|
| 56 |
+
if "syntax_semicolons_valid" in bugs_fixed:
|
| 57 |
+
# Main location must use http://
|
| 58 |
+
if "location / {" in config:
|
| 59 |
+
if "proxy_pass http://localhost:3000;" in config:
|
| 60 |
+
bugs_fixed.append("protocol_valid")
|
| 61 |
+
reward += 0.30
|
| 62 |
+
else:
|
| 63 |
+
errors.append("location / requires 'proxy_pass http://localhost:3000;'")
|
| 64 |
else:
|
| 65 |
+
errors.append("Missing or malformed 'location /' directive")
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
# ===== LEVEL 3: ROUTING LOGIC VALIDATION =====
|
| 68 |
+
# Check API endpoint routing
|
| 69 |
+
if "syntax_semicolons_valid" in bugs_fixed:
|
| 70 |
+
api_check = (
|
| 71 |
+
("location /api/" in config or "location /api {" in config) and
|
| 72 |
+
"proxy_set_header Host $host;" in config and
|
| 73 |
+
"proxy_set_header X-Real-IP $remote_addr;" in config
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
if api_check:
|
| 77 |
+
bugs_fixed.append("routing_headers_valid")
|
| 78 |
+
reward += 0.30
|
| 79 |
+
else:
|
| 80 |
+
errors.append(
|
| 81 |
+
"API route missing proper headers or path. "
|
| 82 |
+
"Required: 'location /api/', 'proxy_set_header Host $host;', 'proxy_set_header X-Real-IP $remote_addr;'"
|
| 83 |
+
)
|
| 84 |
|
| 85 |
+
# ===== FINAL REWARD CALCULATION =====
|
| 86 |
+
reward = min(0.99, reward)
|
| 87 |
+
if len(bugs_fixed) == 3: # All bugs fixed
|
| 88 |
+
reward = 0.99
|
| 89 |
+
|
| 90 |
+
reward = max(0.01, min(0.99, reward))
|
| 91 |
+
|
| 92 |
+
error_msg = " ; ".join(errors) if errors else "All checks passed!"
|
| 93 |
+
return reward, error_msg, bugs_fixed
|
| 94 |
|
|
|
|
|
|
|
|
|
server/graders/yaml_grader.py
CHANGED
|
@@ -4,50 +4,85 @@ from typing import Tuple, List
|
|
| 4 |
|
| 5 |
def grade_task2(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
"""
|
| 7 |
-
Grade Task 2: Broken YAML Config (
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
Returns: (reward, error_message, bugs_fixed_list)
|
|
|
|
| 12 |
"""
|
| 13 |
bugs_fixed = []
|
| 14 |
-
total_bugs = 2
|
| 15 |
error_messages = []
|
|
|
|
| 16 |
|
| 17 |
-
#
|
| 18 |
try:
|
| 19 |
config = yaml.safe_load(submitted_config)
|
| 20 |
if isinstance(config, dict):
|
| 21 |
-
bugs_fixed.append("
|
|
|
|
| 22 |
else:
|
| 23 |
error_messages.append("YAML parsed but result is not a mapping/dictionary")
|
| 24 |
-
|
| 25 |
-
return reward, "; ".join(error_messages), bugs_fixed
|
| 26 |
except yaml.YAMLError as e:
|
| 27 |
error_messages.append(f"YAML parse error: {str(e)}")
|
| 28 |
-
|
| 29 |
-
return reward, "; ".join(error_messages), bugs_fixed
|
| 30 |
-
|
| 31 |
-
# Bug 2: Check required field "version" exists
|
| 32 |
-
service = config.get("service", config)
|
| 33 |
-
if isinstance(service, dict) and "version" in service:
|
| 34 |
-
bugs_fixed.append("version_field_present")
|
| 35 |
-
else:
|
| 36 |
-
error_messages.append(
|
| 37 |
-
"Missing required field: 'version' under 'service'"
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
# Calculate reward
|
| 41 |
-
reward = len(bugs_fixed) / total_bugs
|
| 42 |
|
| 43 |
-
#
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
#
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 53 |
return reward, error_msg, bugs_fixed
|
|
|
|
|
|
| 4 |
|
| 5 |
def grade_task2(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
"""
|
| 7 |
+
Grade Task 2: Broken YAML Config (3 bugs, progressive grading)
|
| 8 |
+
|
| 9 |
+
Progressive Validation:
|
| 10 |
+
1. Syntax: YAML must parse validly
|
| 11 |
+
2. Structure: env must be object (not array)
|
| 12 |
+
3. Semantics: All jobs must have 'timeout' field
|
| 13 |
+
|
| 14 |
+
Bugs:
|
| 15 |
+
- Bug 1 (Syntax): Wrong indentation (3 spaces instead of 2)
|
| 16 |
+
- Bug 2 (Structural): env is array instead of object
|
| 17 |
+
- Bug 3 (Semantic): Missing 'timeout' field in jobs
|
| 18 |
+
|
| 19 |
Returns: (reward, error_message, bugs_fixed_list)
|
| 20 |
+
Reward is emergent from fixes, not hard-coded.
|
| 21 |
"""
|
| 22 |
bugs_fixed = []
|
|
|
|
| 23 |
error_messages = []
|
| 24 |
+
reward = 0.0
|
| 25 |
|
| 26 |
+
# ===== LEVEL 1: SYNTAX VALIDATION =====
|
| 27 |
try:
|
| 28 |
config = yaml.safe_load(submitted_config)
|
| 29 |
if isinstance(config, dict):
|
| 30 |
+
bugs_fixed.append("yaml_syntax_valid")
|
| 31 |
+
reward += 0.3
|
| 32 |
else:
|
| 33 |
error_messages.append("YAML parsed but result is not a mapping/dictionary")
|
| 34 |
+
return max(0.01, min(0.99, reward)), "; ".join(error_messages), bugs_fixed
|
|
|
|
| 35 |
except yaml.YAMLError as e:
|
| 36 |
error_messages.append(f"YAML parse error: {str(e)}")
|
| 37 |
+
return max(0.01, min(0.99, reward)), "; ".join(error_messages), bugs_fixed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
# ===== LEVEL 2: STRUCTURAL VALIDATION =====
|
| 40 |
+
# Check env structure
|
| 41 |
+
pipeline = config.get("pipeline", config)
|
| 42 |
+
if isinstance(pipeline, dict):
|
| 43 |
+
if "env" in pipeline:
|
| 44 |
+
env = pipeline["env"]
|
| 45 |
+
if isinstance(env, dict):
|
| 46 |
+
bugs_fixed.append("env_is_object")
|
| 47 |
+
reward += 0.35
|
| 48 |
+
else:
|
| 49 |
+
error_messages.append(
|
| 50 |
+
f"Field 'env' should be object (dict), got {type(env).__name__}"
|
| 51 |
+
)
|
| 52 |
+
else:
|
| 53 |
+
error_messages.append("Missing required field: 'env'")
|
| 54 |
|
| 55 |
+
# ===== LEVEL 3: SEMANTIC VALIDATION =====
|
| 56 |
+
# Check jobs have timeout fields (only if we can parse jobs)
|
| 57 |
+
if "yaml_syntax_valid" in bugs_fixed:
|
| 58 |
+
jobs = pipeline.get("jobs", {})
|
| 59 |
+
if isinstance(jobs, dict) and len(jobs) > 0:
|
| 60 |
+
all_jobs_have_timeout = all(
|
| 61 |
+
isinstance(job, dict) and "timeout" in job
|
| 62 |
+
for job in jobs.values()
|
| 63 |
+
)
|
| 64 |
+
if all_jobs_have_timeout:
|
| 65 |
+
bugs_fixed.append("all_jobs_have_timeout")
|
| 66 |
+
reward += 0.35
|
| 67 |
+
else:
|
| 68 |
+
jobs_without_timeout = [
|
| 69 |
+
name for name, job in jobs.items()
|
| 70 |
+
if not (isinstance(job, dict) and "timeout" in job)
|
| 71 |
+
]
|
| 72 |
+
error_messages.append(
|
| 73 |
+
f"Jobs missing 'timeout' field: {', '.join(jobs_without_timeout)}"
|
| 74 |
+
)
|
| 75 |
+
else:
|
| 76 |
+
error_messages.append("No valid jobs found or jobs is not an object")
|
| 77 |
|
| 78 |
+
# ===== FINAL REWARD CALCULATION =====
|
| 79 |
+
# Reward is emergent from actual fixes
|
| 80 |
+
reward = min(0.99, reward) # Cap at 0.99 for non-perfect
|
| 81 |
+
if len(bugs_fixed) == 3: # All bugs fixed
|
| 82 |
+
reward = 0.99
|
| 83 |
+
|
| 84 |
+
reward = max(0.01, min(0.99, reward)) # Enforce strict (0,1) interval
|
| 85 |
+
|
| 86 |
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 87 |
return reward, error_msg, bugs_fixed
|
| 88 |
+
|
server/models.py
CHANGED
|
@@ -12,6 +12,7 @@ class ConfigDebugObservation(Observation):
|
|
| 12 |
Inherits done: bool, reward: Optional[float], metadata from Observation.
|
| 13 |
"""
|
| 14 |
broken_config: str = ""
|
|
|
|
| 15 |
file_type: str = ""
|
| 16 |
error_message: str = ""
|
| 17 |
task_id: str = ""
|
|
|
|
| 12 |
Inherits done: bool, reward: Optional[float], metadata from Observation.
|
| 13 |
"""
|
| 14 |
broken_config: str = ""
|
| 15 |
+
ground_truth: str = "" # Solution config for validator verification
|
| 16 |
file_type: str = ""
|
| 17 |
error_message: str = ""
|
| 18 |
task_id: str = ""
|
server/tasks/task1_json.py
CHANGED
|
@@ -1,31 +1,52 @@
|
|
| 1 |
TASK_ID = "task1_json"
|
| 2 |
-
DIFFICULTY = "
|
| 3 |
FILE_TYPE = "json"
|
| 4 |
-
NUM_BUGS =
|
| 5 |
|
| 6 |
DESCRIPTION = (
|
| 7 |
-
"A
|
| 8 |
-
"It
|
|
|
|
| 9 |
)
|
| 10 |
|
| 11 |
# The broken config (what the agent sees)
|
| 12 |
# Bug 1 (Syntax): Missing comma after "my-service"
|
| 13 |
-
# Bug 2 (
|
|
|
|
| 14 |
BROKEN_CONFIG = """{
|
| 15 |
"app_name": "my-service"
|
| 16 |
-
"port":
|
| 17 |
"host": "0.0.0.0",
|
| 18 |
-
"debug": true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
}"""
|
| 20 |
|
| 21 |
ERROR_MESSAGE = (
|
| 22 |
-
"JSON parse error: Expecting ',' delimiter
|
| 23 |
-
"Additionally,
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
GROUND_TRUTH = """{
|
| 27 |
"app_name": "my-service",
|
| 28 |
"port": 8080,
|
| 29 |
"host": "0.0.0.0",
|
| 30 |
-
"debug": true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}"""
|
|
|
|
| 1 |
TASK_ID = "task1_json"
|
| 2 |
+
DIFFICULTY = "medium"
|
| 3 |
FILE_TYPE = "json"
|
| 4 |
+
NUM_BUGS = 3
|
| 5 |
|
| 6 |
DESCRIPTION = (
|
| 7 |
+
"A microservice configuration file in JSON format. "
|
| 8 |
+
"It defines app metadata, networking, environment variables, and volume mounts. "
|
| 9 |
+
"This is a realistic scenario where multiple interdependent fixes are needed."
|
| 10 |
)
|
| 11 |
|
| 12 |
# The broken config (what the agent sees)
|
| 13 |
# Bug 1 (Syntax): Missing comma after "my-service"
|
| 14 |
+
# Bug 2 (Structural): env values missing quotes (should be strings, not bare values)
|
| 15 |
+
# Bug 3 (Semantic): volumes should be object mapping, not array
|
| 16 |
BROKEN_CONFIG = """{
|
| 17 |
"app_name": "my-service"
|
| 18 |
+
"port": 8080,
|
| 19 |
"host": "0.0.0.0",
|
| 20 |
+
"debug": true,
|
| 21 |
+
"env": [
|
| 22 |
+
"LOG_LEVEL=info",
|
| 23 |
+
"DB_HOST=localhost",
|
| 24 |
+
"PORT=8080"
|
| 25 |
+
],
|
| 26 |
+
"volumes": [
|
| 27 |
+
"/data:/data",
|
| 28 |
+
"/logs:/logs"
|
| 29 |
+
]
|
| 30 |
}"""
|
| 31 |
|
| 32 |
ERROR_MESSAGE = (
|
| 33 |
+
"JSON parse error: Expecting ',' delimiter after app_name. "
|
| 34 |
+
"Additionally, 'env' values are unquoted strings (should be quoted), "
|
| 35 |
+
"and 'volumes' is an array instead of an object mapping."
|
| 36 |
)
|
| 37 |
|
| 38 |
GROUND_TRUTH = """{
|
| 39 |
"app_name": "my-service",
|
| 40 |
"port": 8080,
|
| 41 |
"host": "0.0.0.0",
|
| 42 |
+
"debug": true,
|
| 43 |
+
"env": {
|
| 44 |
+
"LOG_LEVEL": "info",
|
| 45 |
+
"DB_HOST": "localhost",
|
| 46 |
+
"PORT": "8080"
|
| 47 |
+
},
|
| 48 |
+
"volumes": {
|
| 49 |
+
"data": "/data:/data",
|
| 50 |
+
"logs": "/logs:/logs"
|
| 51 |
+
}
|
| 52 |
}"""
|
server/tasks/task2_yaml.py
CHANGED
|
@@ -1,43 +1,62 @@
|
|
| 1 |
TASK_ID = "task2_yaml"
|
| 2 |
-
DIFFICULTY = "
|
| 3 |
FILE_TYPE = "yaml"
|
| 4 |
-
NUM_BUGS =
|
| 5 |
|
| 6 |
DESCRIPTION = (
|
| 7 |
-
"A
|
| 8 |
-
"It
|
| 9 |
-
"
|
| 10 |
)
|
| 11 |
|
| 12 |
# The broken config (what the agent sees)
|
| 13 |
# Bug 1 (Syntax): Wrong indentation on nested key (3 spaces instead of 2)
|
| 14 |
-
# Bug 2 (
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
ERROR_MESSAGE = (
|
| 28 |
-
"YAML parse error: mapping values are not allowed in this context. "
|
| 29 |
-
"Additionally,
|
|
|
|
| 30 |
)
|
| 31 |
|
| 32 |
-
GROUND_TRUTH = """
|
| 33 |
-
name:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
TASK_ID = "task2_yaml"
|
| 2 |
+
DIFFICULTY = "medium"
|
| 3 |
FILE_TYPE = "yaml"
|
| 4 |
+
NUM_BUGS = 3
|
| 5 |
|
| 6 |
DESCRIPTION = (
|
| 7 |
+
"A CI/CD pipeline configuration in YAML format. "
|
| 8 |
+
"It defines stages, environment variables, and job specifications. "
|
| 9 |
+
"This is a realistic scenario from GitHub Actions / GitLab CI pipelines."
|
| 10 |
)
|
| 11 |
|
| 12 |
# The broken config (what the agent sees)
|
| 13 |
# Bug 1 (Syntax): Wrong indentation on nested key (3 spaces instead of 2)
|
| 14 |
+
# Bug 2 (Structural): env section is array instead of object
|
| 15 |
+
# Bug 3 (Semantic): Missing required 'timeout' field under jobs
|
| 16 |
+
BROKEN_CONFIG = """pipeline:
|
| 17 |
+
name: ci-pipeline
|
| 18 |
+
stages:
|
| 19 |
+
- build
|
| 20 |
+
- test
|
| 21 |
+
- deploy
|
| 22 |
+
env:
|
| 23 |
+
- CI: "true"
|
| 24 |
+
- REGISTRY: "docker.io"
|
| 25 |
+
jobs:
|
| 26 |
+
build_job:
|
| 27 |
+
stage: build
|
| 28 |
+
script:
|
| 29 |
+
- npm install
|
| 30 |
+
- npm run build
|
| 31 |
+
test_job:
|
| 32 |
+
stage: test
|
| 33 |
+
script:
|
| 34 |
+
- npm test"""
|
| 35 |
|
| 36 |
ERROR_MESSAGE = (
|
| 37 |
+
"YAML parse error: mapping values are not allowed in this context (indentation issue). "
|
| 38 |
+
"Additionally, 'env' is an array instead of an object, "
|
| 39 |
+
"and jobs are missing 'timeout' specifications."
|
| 40 |
)
|
| 41 |
|
| 42 |
+
GROUND_TRUTH = """pipeline:
|
| 43 |
+
name: ci-pipeline
|
| 44 |
+
stages:
|
| 45 |
+
- build
|
| 46 |
+
- test
|
| 47 |
+
- deploy
|
| 48 |
+
env:
|
| 49 |
+
CI: "true"
|
| 50 |
+
REGISTRY: "docker.io"
|
| 51 |
+
jobs:
|
| 52 |
+
build_job:
|
| 53 |
+
stage: build
|
| 54 |
+
timeout: 3600
|
| 55 |
+
script:
|
| 56 |
+
- npm install
|
| 57 |
+
- npm run build
|
| 58 |
+
test_job:
|
| 59 |
+
stage: test
|
| 60 |
+
timeout: 1800
|
| 61 |
+
script:
|
| 62 |
+
- npm test"""
|
server/tasks/task7_nginx.py
CHANGED
|
@@ -8,9 +8,9 @@ DESCRIPTION = (
|
|
| 8 |
"Requires fixing syntax (semicolons), directives (protocol), and routing logic (headers)."
|
| 9 |
)
|
| 10 |
|
| 11 |
-
# Bug 1 (Syntax): Missing semicolons in listen and
|
| 12 |
-
# Bug 2 (
|
| 13 |
-
# Bug 3 (
|
| 14 |
BROKEN_CONFIG = """events {}
|
| 15 |
|
| 16 |
http {
|
|
@@ -21,10 +21,8 @@ http {
|
|
| 21 |
proxy_pass localhost:3000
|
| 22 |
}
|
| 23 |
|
| 24 |
-
location /api {
|
| 25 |
proxy_pass http://localhost:5000
|
| 26 |
-
proxy_set_header Host $host
|
| 27 |
-
proxy_set_header X-Real-IP $remote_addr
|
| 28 |
}
|
| 29 |
|
| 30 |
error_log logs/error.log
|
|
@@ -33,7 +31,7 @@ http {
|
|
| 33 |
|
| 34 |
ERROR_MESSAGE = (
|
| 35 |
"nginx configuration has syntax, directive, and routing errors: "
|
| 36 |
-
"missing semicolons, missing http:// prefix in proxy_pass, and
|
| 37 |
)
|
| 38 |
|
| 39 |
GROUND_TRUTH = """events {}
|
|
|
|
| 8 |
"Requires fixing syntax (semicolons), directives (protocol), and routing logic (headers)."
|
| 9 |
)
|
| 10 |
|
| 11 |
+
# Bug 1 (Syntax): Missing semicolons in listen, error_log, and proxy_pass directives
|
| 12 |
+
# Bug 2 (Protocol): proxy_pass for / missing http:// protocol prefix
|
| 13 |
+
# Bug 3 (Routing): API endpoint missing headers
|
| 14 |
BROKEN_CONFIG = """events {}
|
| 15 |
|
| 16 |
http {
|
|
|
|
| 21 |
proxy_pass localhost:3000
|
| 22 |
}
|
| 23 |
|
| 24 |
+
location /api/ {
|
| 25 |
proxy_pass http://localhost:5000
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
error_log logs/error.log
|
|
|
|
| 31 |
|
| 32 |
ERROR_MESSAGE = (
|
| 33 |
"nginx configuration has syntax, directive, and routing errors: "
|
| 34 |
+
"missing semicolons, missing http:// prefix in proxy_pass, and missing required headers in API routing."
|
| 35 |
)
|
| 36 |
|
| 37 |
GROUND_TRUTH = """events {}
|
server/tasks/task_registry.py
CHANGED
|
@@ -1,7 +1,28 @@
|
|
| 1 |
-
from typing import Callable, Dict,
|
| 2 |
|
| 3 |
-
from server.tasks import task1_json, task2_yaml, task3_dockerfile
|
| 4 |
-
from server.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
class TaskInfo:
|
|
@@ -14,9 +35,7 @@ class TaskInfo:
|
|
| 14 |
self.broken_config: str = module.BROKEN_CONFIG
|
| 15 |
self.error_message: str = module.ERROR_MESSAGE
|
| 16 |
self.ground_truth: str = module.GROUND_TRUTH
|
| 17 |
-
|
| 18 |
-
# Environment will handle conversion to tuple format if needed
|
| 19 |
-
self.grader: Callable[[str], float] = grader_func
|
| 20 |
|
| 21 |
|
| 22 |
TASK_ORDER = [
|
|
@@ -30,13 +49,13 @@ TASK_ORDER = [
|
|
| 30 |
]
|
| 31 |
|
| 32 |
TASK_REGISTRY: Dict[str, TaskInfo] = {
|
| 33 |
-
"task1_json": TaskInfo(task1_json,
|
| 34 |
-
"task2_yaml": TaskInfo(task2_yaml,
|
| 35 |
-
"task3_dockerfile": TaskInfo(task3_dockerfile,
|
| 36 |
-
"task4_compose": TaskInfo(task4_compose,
|
| 37 |
-
"task5_k8s": TaskInfo(task5_k8s,
|
| 38 |
-
"task6_github_actions": TaskInfo(task6_github_actions,
|
| 39 |
-
"task7_nginx": TaskInfo(task7_nginx,
|
| 40 |
}
|
| 41 |
|
| 42 |
|
|
@@ -45,14 +64,4 @@ def get_task(task_id: str) -> TaskInfo:
|
|
| 45 |
|
| 46 |
|
| 47 |
def get_all_task_ids() -> List[str]:
|
| 48 |
-
return list(TASK_ORDER)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
# Diagnostic logging on module load
|
| 52 |
-
print(f"[TASK REGISTRY] Loaded tasks: {list(TASK_REGISTRY.keys())}", flush=True)
|
| 53 |
-
|
| 54 |
-
for tid, task in TASK_REGISTRY.items():
|
| 55 |
-
print(
|
| 56 |
-
f"[TASK] id={tid} grader={task.grader.__name__} file_type={task.file_type} num_bugs={task.num_bugs}",
|
| 57 |
-
flush=True
|
| 58 |
-
)
|
|
|
|
| 1 |
+
from typing import Callable, Dict, Tuple, List
|
| 2 |
|
| 3 |
+
from server.tasks import task1_json, task2_yaml, task3_dockerfile
|
| 4 |
+
from server.tasks import task4_compose, task5_k8s, task6_github_actions, task7_nginx
|
| 5 |
+
|
| 6 |
+
# INTERNAL USE: Import directly from raw grader files (return tuples)
|
| 7 |
+
# grader_api.py is separate and returns float-only (for openenv.yaml validator)
|
| 8 |
+
from server.graders.json_grader import grade_task1
|
| 9 |
+
from server.graders.yaml_grader import grade_task2
|
| 10 |
+
from server.graders.dockerfile_grader import grade_task3
|
| 11 |
+
from server.graders.compose_grader import grade_task4
|
| 12 |
+
from server.graders.k8s_grader import grade_task5
|
| 13 |
+
from server.graders.github_actions_grader import grade_task6
|
| 14 |
+
from server.graders.nginx_grader import grade_task7
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _clamp_grader(fn):
|
| 18 |
+
"""Wrap raw grader to clamp reward to (0.01, 0.99) while preserving tuple return."""
|
| 19 |
+
def wrapper(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 20 |
+
reward, error_msg, bugs_fixed = fn(submitted_config)
|
| 21 |
+
reward = max(0.01, min(0.99, float(reward)))
|
| 22 |
+
return reward, error_msg, bugs_fixed
|
| 23 |
+
wrapper.__name__ = fn.__name__
|
| 24 |
+
wrapper.__qualname__ = fn.__qualname__
|
| 25 |
+
return wrapper
|
| 26 |
|
| 27 |
|
| 28 |
class TaskInfo:
|
|
|
|
| 35 |
self.broken_config: str = module.BROKEN_CONFIG
|
| 36 |
self.error_message: str = module.ERROR_MESSAGE
|
| 37 |
self.ground_truth: str = module.GROUND_TRUTH
|
| 38 |
+
self.grader: Callable[[str], Tuple[float, str, List[str]]] = grader_func
|
|
|
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
TASK_ORDER = [
|
|
|
|
| 49 |
]
|
| 50 |
|
| 51 |
TASK_REGISTRY: Dict[str, TaskInfo] = {
|
| 52 |
+
"task1_json": TaskInfo(task1_json, _clamp_grader(grade_task1)),
|
| 53 |
+
"task2_yaml": TaskInfo(task2_yaml, _clamp_grader(grade_task2)),
|
| 54 |
+
"task3_dockerfile": TaskInfo(task3_dockerfile, _clamp_grader(grade_task3)),
|
| 55 |
+
"task4_compose": TaskInfo(task4_compose, _clamp_grader(grade_task4)),
|
| 56 |
+
"task5_k8s": TaskInfo(task5_k8s, _clamp_grader(grade_task5)),
|
| 57 |
+
"task6_github_actions": TaskInfo(task6_github_actions, _clamp_grader(grade_task6)),
|
| 58 |
+
"task7_nginx": TaskInfo(task7_nginx, _clamp_grader(grade_task7)),
|
| 59 |
}
|
| 60 |
|
| 61 |
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def get_all_task_ids() -> List[str]:
|
| 67 |
+
return list(TASK_ORDER)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_grader_audit.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Runtime audit of all graders - test actual outputs without validator.
|
| 4 |
+
Tests what the validator will actually receive.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import sys
|
| 8 |
+
from server.graders.grader_api import (
|
| 9 |
+
grade_task1, grade_task2, grade_task3, grade_task4,
|
| 10 |
+
grade_task5, grade_task6, grade_task7,
|
| 11 |
+
)
|
| 12 |
+
from server.tasks import (
|
| 13 |
+
task1_json, task2_yaml, task3_dockerfile,
|
| 14 |
+
task4_compose, task5_k8s, task6_github_actions, task7_nginx
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# Test data
|
| 18 |
+
TESTS = [
|
| 19 |
+
(1, grade_task1, task1_json.BROKEN_CONFIG, "JSON"),
|
| 20 |
+
(2, grade_task2, task2_yaml.BROKEN_CONFIG, "YAML"),
|
| 21 |
+
(3, grade_task3, task3_dockerfile.BROKEN_CONFIG, "Dockerfile"),
|
| 22 |
+
(4, grade_task4, task4_compose.BROKEN_CONFIG, "Compose"),
|
| 23 |
+
(5, grade_task5, task5_k8s.BROKEN_CONFIG, "K8s"),
|
| 24 |
+
(6, grade_task6, task6_github_actions.BROKEN_CONFIG, "GitHub Actions"),
|
| 25 |
+
(7, grade_task7, task7_nginx.BROKEN_CONFIG, "Nginx"),
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
print("=" * 80)
|
| 29 |
+
print("GRADER RUNTIME AUDIT - ALL GRADERS WITH BROKEN CONFIGS")
|
| 30 |
+
print("Validator Contract: All graders must return FLOAT in (0, 1) ONLY")
|
| 31 |
+
print("=" * 80)
|
| 32 |
+
print()
|
| 33 |
+
|
| 34 |
+
failures = []
|
| 35 |
+
all_valid = True
|
| 36 |
+
|
| 37 |
+
for task_num, grader_func, broken_config, name in TESTS:
|
| 38 |
+
task_id = f"task{task_num}_{name.lower().replace(' ', '_')}"
|
| 39 |
+
|
| 40 |
+
print(f"Testing Task {task_num} ({name})...")
|
| 41 |
+
print(f" Grader: {grader_func.__name__}")
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
result = grader_func(broken_config)
|
| 45 |
+
|
| 46 |
+
# Analyze output - MUST be float only
|
| 47 |
+
result_type = type(result).__name__
|
| 48 |
+
print(f" Output type: {result_type}")
|
| 49 |
+
|
| 50 |
+
if isinstance(result, float):
|
| 51 |
+
reward = result
|
| 52 |
+
is_valid = 0 < reward < 1
|
| 53 |
+
|
| 54 |
+
print(f" Value: {reward}")
|
| 55 |
+
print(f" In bounds (0, 1): {is_valid}")
|
| 56 |
+
print(f" Exactly 0.0: {reward == 0.0}")
|
| 57 |
+
print(f" Exactly 1.0: {reward == 1.0}")
|
| 58 |
+
print(f" NaN check: {reward != reward}")
|
| 59 |
+
print(f" Inf check: {abs(reward) > 1e308}")
|
| 60 |
+
|
| 61 |
+
if not is_valid:
|
| 62 |
+
all_valid = False
|
| 63 |
+
failures.append(f"Task {task_num}: Reward {reward} NOT in (0, 1)")
|
| 64 |
+
print(f" ❌ INVALID: Reward {reward} is not in (0, 1) range")
|
| 65 |
+
else:
|
| 66 |
+
print(f" ✅ Valid")
|
| 67 |
+
else:
|
| 68 |
+
all_valid = False
|
| 69 |
+
failures.append(f"Task {task_num}: Expected FLOAT, got {result_type}")
|
| 70 |
+
print(f" ❌ INVALID: Expected float, got {result_type}")
|
| 71 |
+
print(f" Value: {result}")
|
| 72 |
+
|
| 73 |
+
except Exception as e:
|
| 74 |
+
all_valid = False
|
| 75 |
+
failures.append(f"Task {task_num}: Exception - {type(e).__name__}: {str(e)}")
|
| 76 |
+
print(f" ❌ EXCEPTION: {type(e).__name__}: {str(e)}")
|
| 77 |
+
|
| 78 |
+
print()
|
| 79 |
+
|
| 80 |
+
print("=" * 80)
|
| 81 |
+
print("SUMMARY")
|
| 82 |
+
print("=" * 80)
|
| 83 |
+
print(f"All graders valid: {all_valid}")
|
| 84 |
+
print(f"Total tests: {len(TESTS)}")
|
| 85 |
+
print(f"Passed: {len(TESTS) - len(failures)}")
|
| 86 |
+
print(f"Failed: {len(failures)}")
|
| 87 |
+
|
| 88 |
+
if failures:
|
| 89 |
+
print("\nFailures:")
|
| 90 |
+
for failure in failures:
|
| 91 |
+
print(f" - {failure}")
|
| 92 |
+
sys.exit(1)
|
| 93 |
+
else:
|
| 94 |
+
print("\n✅ All graders return valid FLOATS in (0, 1)!")
|
| 95 |
+
sys.exit(0)
|
test_task_graders.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Local Test Script: Verify all tasks/graders meet benchmark quality standards.
|
| 4 |
+
|
| 5 |
+
This script runs the mentor's quality checklist:
|
| 6 |
+
- Broken config should score ~0.05 reward
|
| 7 |
+
- Ground truth should score ~0.95 reward
|
| 8 |
+
- bugs_fixed list should reflect actual fixes
|
| 9 |
+
- No task should return (reward=0.95, bugs=[]) on broken config (indicates broken grader)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from server.tasks.task_registry import TASK_ORDER, get_task
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_all_tasks():
|
| 16 |
+
"""Test all tasks in TASK_ORDER."""
|
| 17 |
+
print("=" * 80)
|
| 18 |
+
print("BENCHMARK QUALITY TEST - ALL TASKS")
|
| 19 |
+
print("=" * 80)
|
| 20 |
+
|
| 21 |
+
all_passed = True
|
| 22 |
+
|
| 23 |
+
for task_id in TASK_ORDER:
|
| 24 |
+
print(f"\n[TEST] {task_id}")
|
| 25 |
+
print("-" * 80)
|
| 26 |
+
|
| 27 |
+
task = get_task(task_id)
|
| 28 |
+
|
| 29 |
+
# Test 1: Broken config should score low
|
| 30 |
+
broken_reward, broken_msg, broken_bugs = task.grader(task.broken_config)
|
| 31 |
+
print(f" BROKEN CONFIG:")
|
| 32 |
+
print(f" Reward: {broken_reward:.2f} (expected ~0.05)")
|
| 33 |
+
print(f" Bugs Fixed: {len(broken_bugs)} (expected 0-1)")
|
| 34 |
+
print(f" Message: {broken_msg[:60]}...")
|
| 35 |
+
|
| 36 |
+
broken_ok = 0.01 <= broken_reward <= 0.99 # Must be strictly (0, 1)
|
| 37 |
+
if not broken_ok:
|
| 38 |
+
print(f" [FAIL] Broken config reward out of valid range (0.01-0.99)!")
|
| 39 |
+
all_passed = False
|
| 40 |
+
else:
|
| 41 |
+
print(f" [PASS]")
|
| 42 |
+
|
| 43 |
+
# Test 2: Ground truth should score high
|
| 44 |
+
truth_reward, truth_msg, truth_bugs = task.grader(task.ground_truth)
|
| 45 |
+
print(f"\n GROUND TRUTH:")
|
| 46 |
+
print(f" Reward: {truth_reward:.2f} (expected 0.85-0.99)")
|
| 47 |
+
print(f" Bugs Fixed: {len(truth_bugs)} (expected {task.num_bugs})")
|
| 48 |
+
print(f" Message: {truth_msg[:60]}...")
|
| 49 |
+
|
| 50 |
+
truth_ok = 0.85 <= truth_reward <= 0.99 # Should be in valid high range
|
| 51 |
+
if not truth_ok:
|
| 52 |
+
print(f" [FAIL] Ground truth reward out of expected range!")
|
| 53 |
+
all_passed = False
|
| 54 |
+
else:
|
| 55 |
+
print(f" [PASS]")
|
| 56 |
+
|
| 57 |
+
# Test 3: Check for grader logic issues
|
| 58 |
+
if broken_reward >= 0.95 and len(broken_bugs) == 0:
|
| 59 |
+
print(f"\n [WARNING] Grader may be broken (high reward on broken config)")
|
| 60 |
+
all_passed = False
|
| 61 |
+
|
| 62 |
+
if truth_reward < 0.85:
|
| 63 |
+
print(f"\n [WARNING] Ground truth not scoring highly enough")
|
| 64 |
+
all_passed = False
|
| 65 |
+
|
| 66 |
+
# Task metadata
|
| 67 |
+
print(f"\n Task Metadata:")
|
| 68 |
+
print(f" Difficulty: {task.difficulty}")
|
| 69 |
+
print(f" Bugs: {task.num_bugs}")
|
| 70 |
+
print(f" Type: {task.file_type}")
|
| 71 |
+
|
| 72 |
+
print("\n" + "=" * 80)
|
| 73 |
+
if all_passed:
|
| 74 |
+
print("[SUCCESS] ALL TESTS PASSED - Ready for resubmission")
|
| 75 |
+
else:
|
| 76 |
+
print("[FAILED] SOME TESTS FAILED - Review above")
|
| 77 |
+
print("=" * 80)
|
| 78 |
+
|
| 79 |
+
return all_passed
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
success = test_all_tasks()
|
| 84 |
+
exit(0 if success else 1)
|
verify_graders.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys, os
|
| 2 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
print("=" * 60)
|
| 6 |
+
print("GRADER VERIFICATION")
|
| 7 |
+
print("=" * 60)
|
| 8 |
+
|
| 9 |
+
print("\n[TEST 1] Importing grader_api functions...")
|
| 10 |
+
try:
|
| 11 |
+
from server.graders.grader_api import (
|
| 12 |
+
grade_task1, grade_task2, grade_task3,
|
| 13 |
+
grade_task4, grade_task5, grade_task6, grade_task7,
|
| 14 |
+
)
|
| 15 |
+
print(" OK - All 7 graders imported")
|
| 16 |
+
except ImportError as e:
|
| 17 |
+
print(f" FAIL - {e}")
|
| 18 |
+
sys.exit(1)
|
| 19 |
+
|
| 20 |
+
print("\n[TEST 2] Calling graders, checking type + range...")
|
| 21 |
+
graders = [
|
| 22 |
+
("task1", grade_task1), ("task2", grade_task2),
|
| 23 |
+
("task3", grade_task3), ("task4", grade_task4),
|
| 24 |
+
("task5", grade_task5), ("task6", grade_task6),
|
| 25 |
+
("task7", grade_task7),
|
| 26 |
+
]
|
| 27 |
+
all_ok = True
|
| 28 |
+
for name, fn in graders:
|
| 29 |
+
try:
|
| 30 |
+
result = fn("{}")
|
| 31 |
+
if not isinstance(result, float):
|
| 32 |
+
print(f" FAIL - {name}: returned {type(result).__name__}, need float")
|
| 33 |
+
all_ok = False
|
| 34 |
+
elif result <= 0.0 or result >= 1.0:
|
| 35 |
+
print(f" FAIL - {name}: score={result} NOT in (0,1)")
|
| 36 |
+
all_ok = False
|
| 37 |
+
else:
|
| 38 |
+
print(f" OK - {name}: score={result}")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f" FAIL - {name}: {e}")
|
| 41 |
+
all_ok = False
|
| 42 |
+
|
| 43 |
+
print("\n" + "=" * 60)
|
| 44 |
+
if all_ok:
|
| 45 |
+
print("ALL PASSED - Push and submit!")
|
| 46 |
+
else:
|
| 47 |
+
print("FAILED - Fix before submitting!")
|
| 48 |
+
print("=" * 60)
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
main()
|