Spaces:
Sleeping
Sleeping
Commit ·
cda147c
0
Parent(s):
Replace repo with updated config-debug files
Browse files- .dockerignore +5 -0
- .gitattributes +0 -0
- .gitignore +2 -0
- Dockerfile +14 -0
- README.md +226 -0
- __init__.py +3 -0
- client.py +25 -0
- inference.py +217 -0
- openenv.yaml +62 -0
- pyproject.toml +22 -0
- requirements.txt +8 -0
- server/Dockerfile +14 -0
- server/__init__.py +0 -0
- server/app.py +176 -0
- server/config_debug_environment.py +145 -0
- server/env.py +362 -0
- server/graders/__init__.py +0 -0
- server/graders/compose_grader.py +109 -0
- server/graders/dockerfile_grader.py +107 -0
- server/graders/github_actions_grader.py +164 -0
- server/graders/grader_api.py +60 -0
- server/graders/json_grader.py +48 -0
- server/graders/k8s_grader.py +64 -0
- server/graders/nginx_grader.py +49 -0
- server/graders/yaml_grader.py +51 -0
- server/models.py +41 -0
- server/requirements.txt +8 -0
- server/tasks/__init__.py +0 -0
- server/tasks/task1_json.py +31 -0
- server/tasks/task2_yaml.py +43 -0
- server/tasks/task3_dockerfile.py +47 -0
- server/tasks/task4_compose.py +99 -0
- server/tasks/task5_k8s.py +66 -0
- server/tasks/task6_github_actions.py +124 -0
- server/tasks/task7_nginx.py +57 -0
- server/tasks/task_registry.py +53 -0
- test_env.py +21 -0
- test_k8s_multistep.py +88 -0
- test_nginx_multistep.py +72 -0
- uv.lock +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.git
|
| 4 |
+
test_*.py
|
| 5 |
+
validate-submission.sh
|
.gitattributes
ADDED
|
File without changes
|
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
RUN useradd -m -u 1000 user
|
| 9 |
+
COPY --chown=user . .
|
| 10 |
+
USER user
|
| 11 |
+
|
| 12 |
+
EXPOSE 7860
|
| 13 |
+
|
| 14 |
+
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ConfigDebugEnv
|
| 3 |
+
colorFrom: red
|
| 4 |
+
colorTo: yellow
|
| 5 |
+
sdk: docker
|
| 6 |
+
app_port: 7860
|
| 7 |
+
tags:
|
| 8 |
+
- openenv
|
| 9 |
+
- devops
|
| 10 |
+
- configuration
|
| 11 |
+
- debugging
|
| 12 |
+
- reinforcement-learning
|
| 13 |
+
- multi-task-rl
|
| 14 |
+
pinned: false
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
# ConfigDebugEnv: Multi-Task RL Environment for Configuration Debugging
|
| 18 |
+
|
| 19 |
+
> **Challenge**: Train AI agents to autonomously debug broken configuration files across 7 real-world DevOps formats. This is a **multi-step sequential decision-making problem** requiring iterative reasoning and progressive understanding.
|
| 20 |
+
|
| 21 |
+
## Why This Matters
|
| 22 |
+
|
| 23 |
+
**The Problem**: Configuration errors cause ~40% of production incidents and are among the hardest to debug manually because they require domain expertise across multiple technologies (JSON, YAML, Docker, Kubernetes, nginx, etc.).
|
| 24 |
+
|
| 25 |
+
**Why RL?**: Static rule-based fixes fail because:
|
| 26 |
+
- Bugs interact (fixing one reveals another)
|
| 27 |
+
- Context matters (nginx syntax differs from Kubernetes)
|
| 28 |
+
- Validation is semantic (type checker would pass some errors)
|
| 29 |
+
|
| 30 |
+
**ConfigDebugEnv's Solution**: Train agents with **partial rewards** to discover multi-step fixes:
|
| 31 |
+
- Agent attempts fix → gets 0.4 reward + error guidance
|
| 32 |
+
- Agent iterates → gets 0.7 reward + next error guidance
|
| 33 |
+
- Agent completes → gets 1.0 reward + advances to next task
|
| 34 |
+
|
| 35 |
+
This mirrors real-world debugging where solutions emerge through iteration, not revelation.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## Environment Design
|
| 40 |
+
|
| 41 |
+
### Action Space
|
| 42 |
+
```
|
| 43 |
+
ConfigDebugAction.fixed_config: str # Corrected configuration
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Observation Space
|
| 47 |
+
```
|
| 48 |
+
{
|
| 49 |
+
"broken_config": str, # Current broken config
|
| 50 |
+
"file_type": str, # Format: json, yaml, dockerfile, etc
|
| 51 |
+
"error_message": str, # Specific guidance (e.g., "replicas must be int")
|
| 52 |
+
"task_id": str, # Current task ID
|
| 53 |
+
"task_description": str, # Human-readable task
|
| 54 |
+
"difficulty": str, # easy, medium, hard, very_hard
|
| 55 |
+
"num_bugs": int, # Total bugs in this config
|
| 56 |
+
"bugs_found_so_far": int, # Bugs fixed in this attempt
|
| 57 |
+
"previous_reward": float, # Last reward: [0.0, 1.0]
|
| 58 |
+
}
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### Reward Structure (Sequential Decision Making)
|
| 62 |
+
|
| 63 |
+
Each task has **3 progressive levels**:
|
| 64 |
+
|
| 65 |
+
| Level | Example (Kubernetes) | Reward | Guides Next Attempt |
|
| 66 |
+
|-------|----------------------|--------|---------------------|
|
| 67 |
+
| L1 | Fix replicas type | 0.4 | "containerPort must be int" |
|
| 68 |
+
| L2 | Fix port type too | 0.7 | "cpu must include unit (m)" |
|
| 69 |
+
| L3 | Fix all bugs | 1.0 | ✅ Task complete, advance |
|
| 70 |
+
|
| 71 |
+
**Why this works**: Agents learn to read error messages, make targeted fixes, and build on partial success—exactly like human debugging.
|
| 72 |
+
|
| 73 |
+
**⚠️ Important**: Hard tasks (Kubernetes, Nginx) **require sequential reasoning and cannot be solved in a single step**. Perfect first-attempt solutions will not occur; agents must iterate based on error guidance.
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## The 7 Tasks (Progressive Difficulty)
|
| 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 |
+
|
| 93 |
+
## Multi-Task Learning
|
| 94 |
+
|
| 95 |
+
The environment progresses through all 7 tasks sequentially:
|
| 96 |
+
1. Agent learns from task1 → task2 → ... → task7
|
| 97 |
+
2. Each task builds on previous knowledge
|
| 98 |
+
3. Harder tasks should show better reasoning (agents see more diverse error types)
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## API Endpoints
|
| 103 |
+
|
| 104 |
+
### Core OpenEnv Endpoints
|
| 105 |
+
- `POST /reset` - Reset environment to task1
|
| 106 |
+
- `POST /step` - Submit action (fixed config) → get reward + next observation
|
| 107 |
+
- `GET /observation` - Get current observation
|
| 108 |
+
- `GET /state` - Get full environment state
|
| 109 |
+
|
| 110 |
+
### Utility Endpoints
|
| 111 |
+
- `GET /metadata` - Environment specification (auto-generated by OpenEnv)
|
| 112 |
+
- `GET /info` - Service info
|
| 113 |
+
- `GET /health` - Health check
|
| 114 |
+
- `GET /tasks` - List all tasks with metadata
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
## Example Session
|
| 119 |
+
|
| 120 |
+
```python
|
| 121 |
+
from server.config_debug_environment import ConfigDebugEnvironment
|
| 122 |
+
from server.models import ConfigDebugAction
|
| 123 |
+
|
| 124 |
+
env = ConfigDebugEnvironment()
|
| 125 |
+
obs = env.reset() # Start at task1_json
|
| 126 |
+
|
| 127 |
+
# Attempt 1: Agent tries initial fix
|
| 128 |
+
action = ConfigDebugAction(fixed_config='{"key": "value"}')
|
| 129 |
+
obs = env.step(action)
|
| 130 |
+
print(f"Reward: {obs.reward}") # → 0.4 (partial credit)
|
| 131 |
+
print(f"Error: {obs.error_message}") # → Guides next attempt
|
| 132 |
+
|
| 133 |
+
# Attempt 2: Agent learns and improves
|
| 134 |
+
action = ConfigDebugAction(fixed_config='{"key": "value", "number": 42}')
|
| 135 |
+
obs = env.step(action)
|
| 136 |
+
print(f"Reward: {obs.reward}") # → 0.7
|
| 137 |
+
print(f"Error: {obs.error_message}") # → Final hint
|
| 138 |
+
|
| 139 |
+
# Attempt 3: Agent completes
|
| 140 |
+
action = ConfigDebugAction(fixed_config='{"key": "value", "number": 42, "enabled": true}')
|
| 141 |
+
obs = env.step(action)
|
| 142 |
+
print(f"Reward: {obs.reward}") # → 1.0 ✅
|
| 143 |
+
print(f"Done: {obs.done}") # → False (more tasks remain)
|
| 144 |
+
obs = obs.task_id # → task2_yaml
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## Key Strengths
|
| 150 |
+
|
| 151 |
+
✅ **True RL Problem**: Partial rewards with sequential decision-making
|
| 152 |
+
✅ **Multi-Step Reasoning**: Errors cascade; fixes must be iterative
|
| 153 |
+
✅ **Domain-Diverse**: 7 config formats = varied error types
|
| 154 |
+
✅ **Scalable Difficulty**: Easy tasks build foundation for hard tasks
|
| 155 |
+
✅ **Real-World Relevance**: Configuration bugs are a major DevOps pain point
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## Technical Stack
|
| 160 |
+
|
| 161 |
+
- **Framework**: OpenEnv (FastAPI-based)
|
| 162 |
+
- **Language**: Python 3.12+
|
| 163 |
+
- **Graders**: Domain-specific validators (YAML, JSON, Docker, K8s, nginx)
|
| 164 |
+
- **Deployment**: Docker + Hugging Face Spaces
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## Getting Started
|
| 169 |
+
|
| 170 |
+
```bash
|
| 171 |
+
# Install dependencies
|
| 172 |
+
pip install -r requirements.txt
|
| 173 |
+
|
| 174 |
+
# Run tests
|
| 175 |
+
python test_env.py
|
| 176 |
+
|
| 177 |
+
# Start server
|
| 178 |
+
uvicorn server.app:app --reload
|
| 179 |
+
|
| 180 |
+
# Access at http://localhost:8000
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
---
|
| 184 |
+
|
| 185 |
+
## Evaluation Criteria
|
| 186 |
+
|
| 187 |
+
- ✅ **Correct Task Progression** - All 7 tasks with proper graders
|
| 188 |
+
- ✅ **Multi-Step Learning** - Agents show iterative improvement
|
| 189 |
+
- ✅ **Error Guidance** - Clear feedback directs next fix
|
| 190 |
+
- ✅ **Reward Semantics** - Partial credit enables intermediate learning
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
## For Judges
|
| 195 |
+
|
| 196 |
+
This project demonstrates **advanced RL environment design**:
|
| 197 |
+
|
| 198 |
+
1. **Problem Formulation**: Configuration debugging is inherently sequential and iterative
|
| 199 |
+
2. **Reward Engineering**: Partial rewards guide exploration; not just binary pass/fail
|
| 200 |
+
3. **Task Curriculum**: 7 tasks from easy→hard with consistent structure
|
| 201 |
+
4. **Error Pedagogy**: Error messages teach agents what to fix next
|
| 202 |
+
5. **Scalability**: Framework generalizes to any text-based debugging task
|
| 203 |
+
|
| 204 |
+
ConfigDebugEnv isn't just a benchmark—it's a **learning tool** that teaches agents to think like human debuggers.
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
**Built for**: Meta PyTorch OpenEnv Hackathon x SST
|
| 209 |
+
**Version**: 1.0.0
|
| 210 |
+
- GET /docs - Swagger API docs
|
| 211 |
+
|
| 212 |
+
## Setup
|
| 213 |
+
|
| 214 |
+
Run locally:
|
| 215 |
+
|
| 216 |
+
pip install fastapi uvicorn pydantic pyyaml httpx openai gradio
|
| 217 |
+
uvicorn server.env:app --host 0.0.0.0 --port 7860
|
| 218 |
+
|
| 219 |
+
Run with Docker:
|
| 220 |
+
|
| 221 |
+
docker build -t config-debug-env .
|
| 222 |
+
docker run -p 7860:7860 config-debug-env
|
| 223 |
+
|
| 224 |
+
## Baseline Results
|
| 225 |
+
|
| 226 |
+
Qwen/Qwen2.5-72B-Instruct: 7/7 tasks, score = 1.000
|
__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
|
| 2 |
+
from server.config_debug_environment import ConfigDebugEnvironment
|
| 3 |
+
from client import ConfigDebugEnv
|
client.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Client for ConfigDebugEnv - connects to the environment server."""
|
| 2 |
+
from typing import Dict, Any
|
| 3 |
+
|
| 4 |
+
from openenv.core.env_client import EnvClient
|
| 5 |
+
from openenv.core.client_types import StepResult
|
| 6 |
+
from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ConfigDebugEnv(EnvClient[ConfigDebugAction, ConfigDebugObservation, ConfigDebugState]):
|
| 10 |
+
"""Typed client for the ConfigDebugEnv environment."""
|
| 11 |
+
|
| 12 |
+
def _step_payload(self, action: ConfigDebugAction) -> Dict[str, Any]:
|
| 13 |
+
return {"fixed_config": action.fixed_config}
|
| 14 |
+
|
| 15 |
+
def _parse_result(self, payload: Dict[str, Any]) -> StepResult[ConfigDebugObservation]:
|
| 16 |
+
obs_data = payload.get("observation", {})
|
| 17 |
+
obs = ConfigDebugObservation(**obs_data)
|
| 18 |
+
return StepResult(
|
| 19 |
+
observation=obs,
|
| 20 |
+
reward=payload.get("reward"),
|
| 21 |
+
done=payload.get("done", False),
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
def _parse_state(self, payload: Dict[str, Any]) -> ConfigDebugState:
|
| 25 |
+
return ConfigDebugState(**payload)
|
inference.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inference Script — ConfigDebugEnv
|
| 3 |
+
===================================
|
| 4 |
+
MANDATORY
|
| 5 |
+
- Before submitting, ensure the following variables are defined in your environment configuration:
|
| 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 |
+
import asyncio
|
| 20 |
+
import os
|
| 21 |
+
import textwrap
|
| 22 |
+
from typing import List, Optional
|
| 23 |
+
|
| 24 |
+
from openai import OpenAI
|
| 25 |
+
|
| 26 |
+
# Constants (not env-dependent)
|
| 27 |
+
IMAGE_NAME = None # Will be read at runtime
|
| 28 |
+
TASK_NAME = "config-debug" # Default
|
| 29 |
+
BENCHMARK = "config_debug_env" # Default
|
| 30 |
+
MAX_STEPS = 35 # 7 tasks × 5 steps each
|
| 31 |
+
TEMPERATURE = 0.1
|
| 32 |
+
MAX_TOKENS = 2000
|
| 33 |
+
SUCCESS_SCORE_THRESHOLD = 0.5 # normalized score in [0, 1]
|
| 34 |
+
MAX_TOTAL_REWARD = 7.0 # 7 tasks, 1.0 max per task
|
| 35 |
+
|
| 36 |
+
SYSTEM_PROMPT = textwrap.dedent(
|
| 37 |
+
"""
|
| 38 |
+
You are an expert DevOps engineer specializing in configuration file debugging.
|
| 39 |
+
You will be given a broken configuration file and must fix ALL bugs in it.
|
| 40 |
+
Return ONLY the fixed configuration file content.
|
| 41 |
+
No explanations, no markdown formatting, no code blocks. Just the raw fixed configuration.
|
| 42 |
+
"""
|
| 43 |
+
).strip()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 47 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 51 |
+
error_val = error if error else "null"
|
| 52 |
+
done_val = str(done).lower()
|
| 53 |
+
print(
|
| 54 |
+
f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}",
|
| 55 |
+
flush=True,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 60 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 61 |
+
print(
|
| 62 |
+
f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}",
|
| 63 |
+
flush=True,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def strip_code_blocks(text: str) -> str:
|
| 68 |
+
"""Remove markdown code blocks if LLM wraps output in them."""
|
| 69 |
+
text = text.strip()
|
| 70 |
+
if text.startswith("```"):
|
| 71 |
+
lines = text.split("\n")
|
| 72 |
+
if lines[-1].strip() == "```":
|
| 73 |
+
lines = lines[1:-1]
|
| 74 |
+
else:
|
| 75 |
+
lines = lines[1:]
|
| 76 |
+
text = "\n".join(lines)
|
| 77 |
+
return text
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def build_user_prompt(obs: dict, step: int, history: List[str]) -> str:
|
| 81 |
+
history_block = "\n".join(history[-4:]) if history else "None"
|
| 82 |
+
return textwrap.dedent(
|
| 83 |
+
f"""
|
| 84 |
+
Fix the following broken {obs['file_type']} configuration file.
|
| 85 |
+
|
| 86 |
+
Task: {obs['task_description']}
|
| 87 |
+
Difficulty: {obs['difficulty']}
|
| 88 |
+
Number of bugs to find: {obs['num_bugs']}
|
| 89 |
+
Bugs fixed so far: {obs['bugs_found_so_far']}
|
| 90 |
+
Error message: {obs['error_message']}
|
| 91 |
+
Step: {step}
|
| 92 |
+
|
| 93 |
+
Previous attempts:
|
| 94 |
+
{history_block}
|
| 95 |
+
|
| 96 |
+
Broken configuration:
|
| 97 |
+
```
|
| 98 |
+
{obs['broken_config']}
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
Return ONLY the fixed configuration file content.
|
| 102 |
+
"""
|
| 103 |
+
).strip()
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def get_model_message(client: OpenAI, obs: dict, step: int, history: List[str], model_name: str) -> str:
|
| 107 |
+
user_prompt = build_user_prompt(obs, step, history)
|
| 108 |
+
try:
|
| 109 |
+
completion = client.chat.completions.create(
|
| 110 |
+
model=model_name,
|
| 111 |
+
messages=[
|
| 112 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 113 |
+
{"role": "user", "content": user_prompt},
|
| 114 |
+
],
|
| 115 |
+
temperature=TEMPERATURE,
|
| 116 |
+
max_tokens=MAX_TOKENS,
|
| 117 |
+
stream=False,
|
| 118 |
+
)
|
| 119 |
+
text = (completion.choices[0].message.content or "").strip()
|
| 120 |
+
return strip_code_blocks(text) if text else ""
|
| 121 |
+
except Exception as exc:
|
| 122 |
+
print(f"[DEBUG] Model request failed: {exc}", flush=True)
|
| 123 |
+
return ""
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
async def main() -> None:
|
| 127 |
+
# Re-read env vars at runtime (evaluator may inject after module import)
|
| 128 |
+
api_key = os.getenv("API_KEY") or os.getenv("HF_TOKEN")
|
| 129 |
+
api_base_url = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 130 |
+
model_name = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
|
| 131 |
+
task_name = os.getenv("CONFIG_DEBUG_TASK", "config-debug")
|
| 132 |
+
benchmark = os.getenv("CONFIG_DEBUG_BENCHMARK", "config_debug_env")
|
| 133 |
+
|
| 134 |
+
client = OpenAI(base_url=api_base_url, api_key=api_key)
|
| 135 |
+
|
| 136 |
+
# Connect to the environment via HTTP
|
| 137 |
+
import httpx
|
| 138 |
+
import sys
|
| 139 |
+
|
| 140 |
+
class HTTPEnvClient:
|
| 141 |
+
"""Simple HTTP client that mimics the OpenEnv SDK interface."""
|
| 142 |
+
|
| 143 |
+
def __init__(self, base_url: str):
|
| 144 |
+
self.base_url = base_url.rstrip("/")
|
| 145 |
+
self.http = httpx.AsyncClient(timeout=60.0)
|
| 146 |
+
|
| 147 |
+
async def reset(self):
|
| 148 |
+
resp = await self.http.post(f"{self.base_url}/reset")
|
| 149 |
+
resp.raise_for_status()
|
| 150 |
+
return resp.json()
|
| 151 |
+
|
| 152 |
+
async def step(self, action_data: dict):
|
| 153 |
+
resp = await self.http.post(f"{self.base_url}/step", json=action_data)
|
| 154 |
+
resp.raise_for_status()
|
| 155 |
+
return resp.json()
|
| 156 |
+
|
| 157 |
+
async def close(self):
|
| 158 |
+
await self.http.aclose()
|
| 159 |
+
|
| 160 |
+
env_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:7860"
|
| 161 |
+
env = HTTPEnvClient(env_url)
|
| 162 |
+
|
| 163 |
+
history: List[str] = []
|
| 164 |
+
rewards: List[float] = []
|
| 165 |
+
steps_taken = 0
|
| 166 |
+
score = 0.0
|
| 167 |
+
success = False
|
| 168 |
+
|
| 169 |
+
log_start(task=task_name, env=benchmark, model=model_name)
|
| 170 |
+
|
| 171 |
+
try:
|
| 172 |
+
result = await env.reset()
|
| 173 |
+
obs = result["observation"]
|
| 174 |
+
state = result["state"]
|
| 175 |
+
|
| 176 |
+
step_num = 0
|
| 177 |
+
while not state["is_done"]:
|
| 178 |
+
step_num += 1
|
| 179 |
+
|
| 180 |
+
fixed_config = get_model_message(client, obs, step_num, history, model_name)
|
| 181 |
+
|
| 182 |
+
step_result = await env.step({"fixed_config": fixed_config})
|
| 183 |
+
obs = step_result["observation"]
|
| 184 |
+
state = step_result["state"]
|
| 185 |
+
reward = step_result.get("reward", 0.0)
|
| 186 |
+
done = state["is_done"]
|
| 187 |
+
info = step_result.get("info", {})
|
| 188 |
+
error = info.get("error_message") if info.get("error_message") != "All checks passed!" else None
|
| 189 |
+
|
| 190 |
+
rewards.append(reward)
|
| 191 |
+
steps_taken = step_num
|
| 192 |
+
|
| 193 |
+
action_summary = f"fix({info.get('task_id', 'unknown')})"
|
| 194 |
+
log_step(step=step_num, action=action_summary, reward=reward, done=done, error=error)
|
| 195 |
+
|
| 196 |
+
history.append(f"Step {step_num}: {action_summary} -> reward {reward:+.2f}")
|
| 197 |
+
|
| 198 |
+
if done:
|
| 199 |
+
break
|
| 200 |
+
|
| 201 |
+
score = sum(rewards) / MAX_TOTAL_REWARD if MAX_TOTAL_REWARD > 0 else 0.0
|
| 202 |
+
score = min(max(score, 0.0), 1.0)
|
| 203 |
+
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 204 |
+
|
| 205 |
+
finally:
|
| 206 |
+
try:
|
| 207 |
+
await env.close()
|
| 208 |
+
except Exception as e:
|
| 209 |
+
print(f"[DEBUG] env.close() error: {e}", flush=True)
|
| 210 |
+
log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
try:
|
| 215 |
+
asyncio.run(main())
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f"[END] success=false error={str(e)}", flush=True)
|
openenv.yaml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: config-debug-env
|
| 3 |
+
type: space
|
| 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
|
| 13 |
+
- debugging
|
| 14 |
+
- infrastructure
|
| 15 |
+
|
| 16 |
+
action_model: server.models:ConfigDebugAction
|
| 17 |
+
observation_model: server.models:ConfigDebugObservation
|
| 18 |
+
state_model: server.models:ConfigDebugState
|
| 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:grade_task1
|
| 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:grade_task2
|
| 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:grade_task3
|
| 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:grade_task4
|
| 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:grade_task5
|
| 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:grade_task6
|
| 56 |
+
has_grader: true
|
| 57 |
+
- id: task7_nginx
|
| 58 |
+
name: "Nginx Config Debug"
|
| 59 |
+
difficulty: very_hard
|
| 60 |
+
num_bugs: 3
|
| 61 |
+
grader: server.graders.grader_api:grade_task7
|
| 62 |
+
has_grader: true
|
pyproject.toml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "config-debug-env"
|
| 3 |
+
version = "1.0.0"
|
| 4 |
+
description = "An RL environment for debugging broken configuration files"
|
| 5 |
+
requires-python = ">=3.10"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"fastapi",
|
| 8 |
+
"uvicorn",
|
| 9 |
+
"pydantic",
|
| 10 |
+
"pyyaml",
|
| 11 |
+
"httpx",
|
| 12 |
+
"openai",
|
| 13 |
+
"gradio",
|
| 14 |
+
"openenv-core>=0.2.0",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
[project.scripts]
|
| 18 |
+
server = "server.app:main"
|
| 19 |
+
|
| 20 |
+
[build-system]
|
| 21 |
+
requires = ["setuptools"]
|
| 22 |
+
build-backend = "setuptools.backends._legacy:_Backend"
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pydantic
|
| 4 |
+
pyyaml
|
| 5 |
+
httpx
|
| 6 |
+
openai
|
| 7 |
+
gradio
|
| 8 |
+
openenv-core>=0.2.0
|
server/Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
RUN useradd -m -u 1000 user
|
| 9 |
+
COPY --chown=user . .
|
| 10 |
+
USER user
|
| 11 |
+
|
| 12 |
+
EXPOSE 7860
|
| 13 |
+
|
| 14 |
+
CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
server/__init__.py
ADDED
|
File without changes
|
server/app.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI application for ConfigDebugEnv.
|
| 2 |
+
|
| 3 |
+
Uses OpenEnv's create_fastapi_app() for standard framework compatibility
|
| 4 |
+
(WebSocket sessions, standard endpoints, grader discovery).
|
| 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
|
| 11 |
+
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, # factory / class — called per session
|
| 18 |
+
ConfigDebugAction, # action model (inherits Action)
|
| 19 |
+
ConfigDebugObservation, # observation model (inherits Observation)
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ---- Custom endpoints ----
|
| 26 |
+
|
| 27 |
+
@app.get("/info")
|
| 28 |
+
def info():
|
| 29 |
+
return {"name": "ConfigDebugEnv", "version": "1.0.0", "status": "running"}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@app.get("/health")
|
| 33 |
+
def health():
|
| 34 |
+
return {"status": "healthy"}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@app.get("/tasks")
|
| 41 |
+
def tasks():
|
| 42 |
+
return {
|
| 43 |
+
"tasks": [
|
| 44 |
+
{
|
| 45 |
+
"id": tid,
|
| 46 |
+
"name": get_task(tid).description,
|
| 47 |
+
"difficulty": get_task(tid).difficulty,
|
| 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 |
+
],
|
| 54 |
+
"total_tasks": len(TASK_ORDER),
|
| 55 |
+
"tasks_with_graders": len(TASK_ORDER),
|
| 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 = "█" * int(state.progress_ratio * 10) + "░" * (10 - int(state.progress_ratio * 10))
|
| 68 |
+
|
| 69 |
+
return f"""
|
| 70 |
+
Task Progress: {len(state.tasks_completed)+1}/7
|
| 71 |
+
Progress: {progress_bar} ({int(state.progress_ratio*100)}%)
|
| 72 |
+
Total Reward: {state.total_reward:.2f}
|
| 73 |
+
|
| 74 |
+
Current Task: {state.current_task_id}
|
| 75 |
+
Difficulty: {state.current_difficulty}
|
| 76 |
+
|
| 77 |
+
Bugs Found: {state.bugs_found_so_far}
|
| 78 |
+
Error: {state.current_error_message or 'None'}
|
| 79 |
+
|
| 80 |
+
Completed: {', '.join(state.tasks_completed) if state.tasks_completed else 'None'}
|
| 81 |
+
Remaining: {', '.join(state.tasks_remaining[:3]) if state.tasks_remaining else 'None'}
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def ui_get_state():
|
| 86 |
+
"""Get current environment state (inspectable state)."""
|
| 87 |
+
return format_state(_ui_env)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def ui_reset():
|
| 91 |
+
_ui_env.reset()
|
| 92 |
+
obs = _ui_env._build_observation()
|
| 93 |
+
return (
|
| 94 |
+
f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
|
| 95 |
+
obs.task_description,
|
| 96 |
+
obs.broken_config,
|
| 97 |
+
obs.error_message,
|
| 98 |
+
format_state(_ui_env),
|
| 99 |
+
"Environment reset. Submit a fixed config to begin.",
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def ui_step(fixed_config):
|
| 104 |
+
if _ui_env._done:
|
| 105 |
+
return (
|
| 106 |
+
"All tasks completed!",
|
| 107 |
+
"",
|
| 108 |
+
"",
|
| 109 |
+
"Episode done. Click Reset to start again.",
|
| 110 |
+
format_state(_ui_env),
|
| 111 |
+
f"Final score: {_ui_env.total_reward:.1f} / {len(TASK_ORDER)}.0",
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
action = ConfigDebugAction(fixed_config=fixed_config)
|
| 115 |
+
obs = _ui_env.step(action)
|
| 116 |
+
|
| 117 |
+
history = f"Reward: {obs.reward:.2f} | Bugs found: {obs.bugs_found_so_far}/{obs.num_bugs}\nFeedback: {obs.error_message}"
|
| 118 |
+
return (
|
| 119 |
+
f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
|
| 120 |
+
obs.task_description,
|
| 121 |
+
obs.broken_config,
|
| 122 |
+
obs.error_message,
|
| 123 |
+
format_state(_ui_env),
|
| 124 |
+
history,
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
with gr.Blocks(title="ConfigDebugEnv") as demo:
|
| 129 |
+
gr.Markdown("# ConfigDebugEnv")
|
| 130 |
+
gr.Markdown("An RL environment for debugging broken config files across 7 real-world formats.")
|
| 131 |
+
|
| 132 |
+
with gr.Row():
|
| 133 |
+
with gr.Column(scale=1):
|
| 134 |
+
gr.Markdown("### Agent Interface")
|
| 135 |
+
task_info = gr.Textbox(label="Current Task", interactive=False)
|
| 136 |
+
task_desc = gr.Textbox(label="Task Description", interactive=False, lines=2)
|
| 137 |
+
broken_config = gr.Textbox(label="Broken Config", interactive=False, lines=10)
|
| 138 |
+
error_msg = gr.Textbox(label="Error Message", interactive=False, lines=2)
|
| 139 |
+
|
| 140 |
+
gr.Markdown("### Take Action")
|
| 141 |
+
fixed_config_input = gr.Textbox(label="Your Fixed Config", placeholder="Paste your fixed configuration here...", lines=10)
|
| 142 |
+
with gr.Row():
|
| 143 |
+
reset_btn = gr.Button("Reset Environment", variant="secondary")
|
| 144 |
+
step_btn = gr.Button("Step", variant="primary")
|
| 145 |
+
state_btn = gr.Button("Get State", variant="secondary")
|
| 146 |
+
|
| 147 |
+
with gr.Column(scale=1):
|
| 148 |
+
gr.Markdown("### State Observer")
|
| 149 |
+
state_display = gr.Textbox(label="Current State (with RL Signals)", interactive=False, lines=14)
|
| 150 |
+
history_display = gr.Textbox(label="Action History / Reward", interactive=False, lines=4)
|
| 151 |
+
|
| 152 |
+
reset_btn.click(
|
| 153 |
+
fn=ui_reset,
|
| 154 |
+
outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
|
| 155 |
+
)
|
| 156 |
+
step_btn.click(
|
| 157 |
+
fn=ui_step,
|
| 158 |
+
inputs=[fixed_config_input],
|
| 159 |
+
outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
|
| 160 |
+
)
|
| 161 |
+
state_btn.click(
|
| 162 |
+
fn=ui_get_state,
|
| 163 |
+
outputs=[state_display],
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
app = gr.mount_gradio_app(app, demo, path="/")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def main(host: str = "0.0.0.0", port: int = 7860):
|
| 171 |
+
import uvicorn
|
| 172 |
+
uvicorn.run(app, host=host, port=port)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
main()
|
server/config_debug_environment.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ConfigDebugEnvironment - OpenEnv-compatible environment class.
|
| 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
|
| 8 |
+
|
| 9 |
+
from openenv.core.env_server import Environment
|
| 10 |
+
from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
|
| 11 |
+
from server.tasks.task_registry import get_task, TASK_ORDER
|
| 12 |
+
|
| 13 |
+
MAX_STEPS_PER_TASK = 5
|
| 14 |
+
|
| 15 |
+
|
| 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
|
| 24 |
+
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self._init_episode()
|
| 27 |
+
|
| 28 |
+
def _init_episode(self):
|
| 29 |
+
self.task_ids = list(TASK_ORDER)
|
| 30 |
+
self.current_task_index = 0
|
| 31 |
+
self.current_step = 0
|
| 32 |
+
self.total_reward = 0.0
|
| 33 |
+
self._done = False
|
| 34 |
+
self.tasks_completed: list = []
|
| 35 |
+
self.bugs_found_so_far = 0
|
| 36 |
+
self.previous_reward = 0.0
|
| 37 |
+
self.current_error_message: Optional[str] = None
|
| 38 |
+
self.current_broken_config: Optional[str] = None
|
| 39 |
+
self._episode_id = str(uuid4())
|
| 40 |
+
self._global_step = 0
|
| 41 |
+
|
| 42 |
+
# ---- OpenEnv interface methods ----
|
| 43 |
+
|
| 44 |
+
def reset(self, seed: Optional[int] = None, episode_id: Optional[str] = None, **kwargs: Any) -> ConfigDebugObservation:
|
| 45 |
+
"""Reset environment to initial state (task 1)."""
|
| 46 |
+
self._init_episode()
|
| 47 |
+
if episode_id:
|
| 48 |
+
self._episode_id = episode_id
|
| 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
|
| 60 |
+
reward, error_message, bugs_fixed = task.grader(action.fixed_config)
|
| 61 |
+
reward = max(0.0, min(1.0, reward))
|
| 62 |
+
|
| 63 |
+
self.current_step += 1
|
| 64 |
+
self._global_step += 1
|
| 65 |
+
self.bugs_found_so_far = len(bugs_fixed)
|
| 66 |
+
self.previous_reward = round(reward, 4)
|
| 67 |
+
self.current_error_message = error_message
|
| 68 |
+
|
| 69 |
+
# Check if task is complete
|
| 70 |
+
task_done = reward >= 0.99 or self.current_step >= MAX_STEPS_PER_TASK
|
| 71 |
+
|
| 72 |
+
if task_done:
|
| 73 |
+
self.total_reward += reward
|
| 74 |
+
self.tasks_completed.append(task_id)
|
| 75 |
+
self.current_task_index += 1
|
| 76 |
+
self.current_step = 0
|
| 77 |
+
self.bugs_found_so_far = 0
|
| 78 |
+
self.current_error_message = None
|
| 79 |
+
self.current_broken_config = None
|
| 80 |
+
if self.current_task_index >= len(self.task_ids):
|
| 81 |
+
self._done = True
|
| 82 |
+
else:
|
| 83 |
+
self.current_broken_config = action.fixed_config
|
| 84 |
+
|
| 85 |
+
obs = self._build_observation()
|
| 86 |
+
obs.done = self._done
|
| 87 |
+
obs.reward = round(reward, 4)
|
| 88 |
+
return obs
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def state(self) -> ConfigDebugState:
|
| 92 |
+
"""Return current environment state with enhanced RL signals."""
|
| 93 |
+
tasks_remaining = self.task_ids[self.current_task_index:]
|
| 94 |
+
if self._done:
|
| 95 |
+
tasks_remaining = []
|
| 96 |
+
|
| 97 |
+
total_tasks = len(self.task_ids)
|
| 98 |
+
completed_tasks = len(self.tasks_completed)
|
| 99 |
+
progress_ratio = completed_tasks / total_tasks if total_tasks > 0 else 0.0
|
| 100 |
+
|
| 101 |
+
current_task = get_task(self._current_task_id())
|
| 102 |
+
|
| 103 |
+
return ConfigDebugState(
|
| 104 |
+
episode_id=self._episode_id,
|
| 105 |
+
step_count=self._global_step,
|
| 106 |
+
current_task_id=self._current_task_id(),
|
| 107 |
+
current_step=self.current_step,
|
| 108 |
+
max_steps=MAX_STEPS_PER_TASK,
|
| 109 |
+
total_reward=round(self.total_reward, 4),
|
| 110 |
+
is_done=self._done,
|
| 111 |
+
tasks_completed=list(self.tasks_completed),
|
| 112 |
+
tasks_remaining=tasks_remaining,
|
| 113 |
+
# Enhanced RL signals
|
| 114 |
+
bugs_found_so_far=self.bugs_found_so_far,
|
| 115 |
+
current_error_message=self.current_error_message,
|
| 116 |
+
progress_ratio=round(progress_ratio, 2),
|
| 117 |
+
current_difficulty=current_task.difficulty,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# ---- Internal helpers ----
|
| 121 |
+
|
| 122 |
+
def _current_task_id(self) -> str:
|
| 123 |
+
if self.current_task_index < len(self.task_ids):
|
| 124 |
+
return self.task_ids[self.current_task_index]
|
| 125 |
+
return self.task_ids[-1]
|
| 126 |
+
|
| 127 |
+
def _build_observation(self) -> ConfigDebugObservation:
|
| 128 |
+
task_id = self._current_task_id()
|
| 129 |
+
task = get_task(task_id)
|
| 130 |
+
broken = self.current_broken_config if self.current_broken_config is not None else task.broken_config
|
| 131 |
+
error = self.current_error_message if self.current_error_message is not None else task.error_message
|
| 132 |
+
|
| 133 |
+
return ConfigDebugObservation(
|
| 134 |
+
broken_config=broken,
|
| 135 |
+
file_type=task.file_type,
|
| 136 |
+
error_message=error,
|
| 137 |
+
task_id=task.task_id,
|
| 138 |
+
task_description=task.description,
|
| 139 |
+
difficulty=task.difficulty,
|
| 140 |
+
num_bugs=task.num_bugs,
|
| 141 |
+
bugs_found_so_far=self.bugs_found_so_far,
|
| 142 |
+
previous_reward=self.previous_reward,
|
| 143 |
+
done=self._done,
|
| 144 |
+
reward=self.previous_reward,
|
| 145 |
+
)
|
server/env.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from typing import Optional
|
| 3 |
+
import json
|
| 4 |
+
import gradio as gr
|
| 5 |
+
|
| 6 |
+
from server.models import ConfigDebugAction, ConfigDebugObservation, ConfigDebugState
|
| 7 |
+
from server.tasks.task_registry import get_task, get_all_task_ids, TASK_ORDER
|
| 8 |
+
|
| 9 |
+
app = FastAPI(title="ConfigDebugEnv", version="1.0.0")
|
| 10 |
+
|
| 11 |
+
# --- Environment State ---
|
| 12 |
+
MAX_STEPS_PER_TASK = 5
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class EnvironmentState:
|
| 16 |
+
"""Mutable environment state that persists across requests."""
|
| 17 |
+
|
| 18 |
+
def __init__(self):
|
| 19 |
+
self.reset_state()
|
| 20 |
+
|
| 21 |
+
def reset_state(self):
|
| 22 |
+
self.task_ids = list(TASK_ORDER)
|
| 23 |
+
self.current_task_index = 0
|
| 24 |
+
self.current_step = 0
|
| 25 |
+
self.total_reward = 0.0
|
| 26 |
+
self.is_done = False
|
| 27 |
+
self.tasks_completed = []
|
| 28 |
+
self.bugs_found_so_far = 0
|
| 29 |
+
self.previous_reward = 0.0
|
| 30 |
+
self.current_error_message: Optional[str] = None
|
| 31 |
+
self.current_broken_config: Optional[str] = None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
env_state = EnvironmentState()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _get_current_task_id() -> str:
|
| 38 |
+
if env_state.current_task_index < len(env_state.task_ids):
|
| 39 |
+
return env_state.task_ids[env_state.current_task_index]
|
| 40 |
+
return env_state.task_ids[-1]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _build_observation() -> ConfigDebugObservation:
|
| 44 |
+
task_id = _get_current_task_id()
|
| 45 |
+
task = get_task(task_id)
|
| 46 |
+
|
| 47 |
+
broken_config = (
|
| 48 |
+
env_state.current_broken_config
|
| 49 |
+
if env_state.current_broken_config is not None
|
| 50 |
+
else task.broken_config
|
| 51 |
+
)
|
| 52 |
+
error_message = (
|
| 53 |
+
env_state.current_error_message
|
| 54 |
+
if env_state.current_error_message is not None
|
| 55 |
+
else task.error_message
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
return ConfigDebugObservation(
|
| 59 |
+
broken_config=broken_config,
|
| 60 |
+
file_type=task.file_type,
|
| 61 |
+
error_message=error_message,
|
| 62 |
+
task_id=task.task_id,
|
| 63 |
+
task_description=task.description,
|
| 64 |
+
difficulty=task.difficulty,
|
| 65 |
+
num_bugs=task.num_bugs,
|
| 66 |
+
bugs_found_so_far=env_state.bugs_found_so_far,
|
| 67 |
+
previous_reward=env_state.previous_reward,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _build_state() -> ConfigDebugState:
|
| 72 |
+
task_id = _get_current_task_id()
|
| 73 |
+
tasks_remaining = env_state.task_ids[env_state.current_task_index:]
|
| 74 |
+
if env_state.is_done:
|
| 75 |
+
tasks_remaining = []
|
| 76 |
+
|
| 77 |
+
return ConfigDebugState(
|
| 78 |
+
current_task_id=task_id,
|
| 79 |
+
current_step=env_state.current_step,
|
| 80 |
+
max_steps=MAX_STEPS_PER_TASK,
|
| 81 |
+
total_reward=round(env_state.total_reward, 4),
|
| 82 |
+
is_done=env_state.is_done,
|
| 83 |
+
tasks_completed=list(env_state.tasks_completed),
|
| 84 |
+
tasks_remaining=tasks_remaining,
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 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."""
|
| 104 |
+
env_state.reset_state()
|
| 105 |
+
if task_id and task_id in env_state.task_ids:
|
| 106 |
+
env_state.current_task_index = env_state.task_ids.index(task_id)
|
| 107 |
+
|
| 108 |
+
observation = _build_observation()
|
| 109 |
+
state = _build_state()
|
| 110 |
+
|
| 111 |
+
return {
|
| 112 |
+
"observation": observation.model_dump(),
|
| 113 |
+
"state": state.model_dump(),
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
@app.post("/step")
|
| 118 |
+
def step(action: ConfigDebugAction):
|
| 119 |
+
"""
|
| 120 |
+
Take a step in the environment.
|
| 121 |
+
The agent submits a fixed config, the grader evaluates it.
|
| 122 |
+
"""
|
| 123 |
+
if env_state.is_done:
|
| 124 |
+
raise HTTPException(
|
| 125 |
+
status_code=400,
|
| 126 |
+
detail="Environment is done. Call /reset to start a new episode.",
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
task_id = _get_current_task_id()
|
| 130 |
+
task = get_task(task_id)
|
| 131 |
+
|
| 132 |
+
# Run the grader
|
| 133 |
+
reward, error_message, bugs_fixed = task.grader(action.fixed_config)
|
| 134 |
+
reward = max(0.01, min(0.99, reward))
|
| 135 |
+
|
| 136 |
+
env_state.current_step += 1
|
| 137 |
+
env_state.bugs_found_so_far = len(bugs_fixed)
|
| 138 |
+
env_state.previous_reward = round(reward, 4)
|
| 139 |
+
|
| 140 |
+
# Update error message for feedback
|
| 141 |
+
env_state.current_error_message = error_message
|
| 142 |
+
|
| 143 |
+
# Check if task is complete (perfect score or max steps reached)
|
| 144 |
+
task_done = reward >= 0.99 or env_state.current_step >= MAX_STEPS_PER_TASK
|
| 145 |
+
|
| 146 |
+
task_reward = reward # Reward for this step
|
| 147 |
+
|
| 148 |
+
if task_done:
|
| 149 |
+
# Record the best reward for this task
|
| 150 |
+
env_state.total_reward += reward
|
| 151 |
+
env_state.tasks_completed.append(task_id)
|
| 152 |
+
env_state.current_task_index += 1
|
| 153 |
+
|
| 154 |
+
# Reset per-task state
|
| 155 |
+
env_state.current_step = 0
|
| 156 |
+
env_state.bugs_found_so_far = 0
|
| 157 |
+
env_state.current_error_message = None
|
| 158 |
+
env_state.current_broken_config = None
|
| 159 |
+
|
| 160 |
+
# Check if all tasks are done
|
| 161 |
+
if env_state.current_task_index >= len(env_state.task_ids):
|
| 162 |
+
env_state.is_done = True
|
| 163 |
+
else:
|
| 164 |
+
# If the agent submitted something, use it as the new "broken" config
|
| 165 |
+
# so the agent can iterate
|
| 166 |
+
env_state.current_broken_config = action.fixed_config
|
| 167 |
+
|
| 168 |
+
observation = _build_observation()
|
| 169 |
+
state = _build_state()
|
| 170 |
+
|
| 171 |
+
return {
|
| 172 |
+
"observation": observation.model_dump(),
|
| 173 |
+
"reward": round(task_reward, 4),
|
| 174 |
+
"done": env_state.is_done,
|
| 175 |
+
"state": state.model_dump(),
|
| 176 |
+
"info": {
|
| 177 |
+
"task_id": task_id,
|
| 178 |
+
"bugs_fixed": bugs_fixed,
|
| 179 |
+
"error_message": error_message,
|
| 180 |
+
"task_done": task_done,
|
| 181 |
+
},
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@app.get("/state")
|
| 186 |
+
def state():
|
| 187 |
+
"""Return current environment state."""
|
| 188 |
+
return _build_state().model_dump()
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
@app.get("/observation")
|
| 192 |
+
def observation():
|
| 193 |
+
"""Return current observation."""
|
| 194 |
+
if env_state.is_done:
|
| 195 |
+
raise HTTPException(
|
| 196 |
+
status_code=400,
|
| 197 |
+
detail="Environment is done. Call /reset to start a new episode.",
|
| 198 |
+
)
|
| 199 |
+
return _build_observation().model_dump()
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@app.get("/metadata")
|
| 204 |
+
def metadata():
|
| 205 |
+
return {
|
| 206 |
+
"env_name": "config_debug_env",
|
| 207 |
+
"version": "1.0.0",
|
| 208 |
+
"description": "Config file debugging environment",
|
| 209 |
+
"tasks": [
|
| 210 |
+
{"id": "task1_json", "name": "JSON Config Debug", "difficulty": "easy", "num_bugs": 2, "has_grader": True, "grader": "server.graders.json_grader:grade_task1"},
|
| 211 |
+
{"id": "task2_yaml", "name": "YAML Config Debug", "difficulty": "easy", "num_bugs": 2, "has_grader": True, "grader": "server.graders.yaml_grader:grade_task2"},
|
| 212 |
+
{"id": "task3_dockerfile", "name": "Dockerfile Debug", "difficulty": "medium", "num_bugs": 3, "has_grader": True, "grader": "server.graders.dockerfile_grader:grade_task3"},
|
| 213 |
+
{"id": "task4_compose", "name": "Docker Compose Debug", "difficulty": "medium", "num_bugs": 4, "has_grader": True, "grader": "server.graders.compose_grader:grade_task4"},
|
| 214 |
+
{"id": "task5_k8s", "name": "Kubernetes Config Debug", "difficulty": "hard", "num_bugs": 5, "has_grader": True, "grader": "server.graders.k8s_grader:grade_task5"},
|
| 215 |
+
{"id": "task6_github_actions", "name": "GitHub Actions Debug", "difficulty": "hard", "num_bugs": 5, "has_grader": True, "grader": "server.graders.github_actions_grader:grade_task6"},
|
| 216 |
+
{"id": "task7_nginx", "name": "Nginx Config Debug", "difficulty": "very_hard", "num_bugs": 6, "has_grader": True, "grader": "server.graders.nginx_grader:grade_task7"},
|
| 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 |
+
],
|
| 239 |
+
"total_tasks": len(TASK_ORDER),
|
| 240 |
+
"tasks_with_graders": len(TASK_ORDER),
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
@app.get("/schema")
|
| 244 |
+
def schema():
|
| 245 |
+
return {
|
| 246 |
+
"action": ConfigDebugAction.model_json_schema(),
|
| 247 |
+
"observation": ConfigDebugObservation.model_json_schema(),
|
| 248 |
+
"state": ConfigDebugState.model_json_schema(),
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
# --- Gradio Web UI ---
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def ui_reset():
|
| 255 |
+
env_state.reset_state()
|
| 256 |
+
obs = _build_observation()
|
| 257 |
+
st = _build_state()
|
| 258 |
+
return (
|
| 259 |
+
f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
|
| 260 |
+
obs.task_description,
|
| 261 |
+
obs.broken_config,
|
| 262 |
+
obs.error_message,
|
| 263 |
+
json.dumps(st.model_dump(), indent=2),
|
| 264 |
+
"Environment reset. Submit a fixed config to begin.",
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def ui_step(fixed_config):
|
| 269 |
+
if env_state.is_done:
|
| 270 |
+
st = _build_state()
|
| 271 |
+
return (
|
| 272 |
+
"All tasks completed!",
|
| 273 |
+
"",
|
| 274 |
+
"",
|
| 275 |
+
"Episode done. Click Reset to start again.",
|
| 276 |
+
json.dumps(st.model_dump(), indent=2),
|
| 277 |
+
f"Final score: {env_state.total_reward:.1f} / {len(TASK_ORDER)}.0",
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
task_id = _get_current_task_id()
|
| 281 |
+
task = get_task(task_id)
|
| 282 |
+
reward, error_message, bugs_fixed = task.grader(fixed_config)
|
| 283 |
+
reward = max(0.01, min(0.99, reward))
|
| 284 |
+
|
| 285 |
+
env_state.current_step += 1
|
| 286 |
+
env_state.bugs_found_so_far = len(bugs_fixed)
|
| 287 |
+
env_state.previous_reward = round(reward, 4)
|
| 288 |
+
env_state.current_error_message = error_message
|
| 289 |
+
|
| 290 |
+
task_done = reward >= 0.99 or env_state.current_step >= MAX_STEPS_PER_TASK
|
| 291 |
+
|
| 292 |
+
if task_done:
|
| 293 |
+
env_state.total_reward += reward
|
| 294 |
+
env_state.tasks_completed.append(task_id)
|
| 295 |
+
env_state.current_task_index += 1
|
| 296 |
+
env_state.current_step = 0
|
| 297 |
+
env_state.bugs_found_so_far = 0
|
| 298 |
+
env_state.current_error_message = None
|
| 299 |
+
env_state.current_broken_config = None
|
| 300 |
+
if env_state.current_task_index >= len(env_state.task_ids):
|
| 301 |
+
env_state.is_done = True
|
| 302 |
+
else:
|
| 303 |
+
env_state.current_broken_config = fixed_config
|
| 304 |
+
|
| 305 |
+
obs = _build_observation()
|
| 306 |
+
st = _build_state()
|
| 307 |
+
history = f"Reward: {reward:.2f} | Bugs fixed: {bugs_fixed} | Task done: {task_done}\nFeedback: {error_message}"
|
| 308 |
+
|
| 309 |
+
return (
|
| 310 |
+
f"Task: {obs.task_id} | Difficulty: {obs.difficulty} | Bugs: {obs.num_bugs}",
|
| 311 |
+
obs.task_description,
|
| 312 |
+
obs.broken_config,
|
| 313 |
+
obs.error_message,
|
| 314 |
+
json.dumps(st.model_dump(), indent=2),
|
| 315 |
+
history,
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def ui_get_state():
|
| 320 |
+
st = _build_state()
|
| 321 |
+
return json.dumps(st.model_dump(), indent=2)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
with gr.Blocks(title="ConfigDebugEnv", theme=gr.themes.Soft()) as demo:
|
| 325 |
+
gr.Markdown("# ConfigDebugEnv")
|
| 326 |
+
gr.Markdown("An RL environment for debugging broken config files across 7 real-world formats: JSON, YAML, Dockerfile, docker-compose, Kubernetes, GitHub Actions, nginx.")
|
| 327 |
+
|
| 328 |
+
with gr.Row():
|
| 329 |
+
with gr.Column(scale=1):
|
| 330 |
+
gr.Markdown("### HumanAgent Interface")
|
| 331 |
+
task_info = gr.Textbox(label="Current Task", interactive=False)
|
| 332 |
+
task_desc = gr.Textbox(label="Task Description", interactive=False, lines=2)
|
| 333 |
+
broken_config = gr.Textbox(label="Broken Config", interactive=False, lines=10)
|
| 334 |
+
error_msg = gr.Textbox(label="Error Message", interactive=False, lines=2)
|
| 335 |
+
|
| 336 |
+
gr.Markdown("### Take Action")
|
| 337 |
+
fixed_config_input = gr.Textbox(label="Your Fixed Config", placeholder="Paste your fixed configuration here...", lines=10)
|
| 338 |
+
with gr.Row():
|
| 339 |
+
reset_btn = gr.Button("Reset Environment", variant="secondary")
|
| 340 |
+
step_btn = gr.Button("Step", variant="primary")
|
| 341 |
+
state_btn = gr.Button("Get State", variant="secondary")
|
| 342 |
+
|
| 343 |
+
with gr.Column(scale=1):
|
| 344 |
+
gr.Markdown("### State Observer")
|
| 345 |
+
state_display = gr.Textbox(label="Current State", interactive=False, lines=12)
|
| 346 |
+
history_display = gr.Textbox(label="Action History / Reward", interactive=False, lines=4)
|
| 347 |
+
|
| 348 |
+
reset_btn.click(
|
| 349 |
+
fn=ui_reset,
|
| 350 |
+
outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
|
| 351 |
+
)
|
| 352 |
+
step_btn.click(
|
| 353 |
+
fn=ui_step,
|
| 354 |
+
inputs=[fixed_config_input],
|
| 355 |
+
outputs=[task_info, task_desc, broken_config, error_msg, state_display, history_display],
|
| 356 |
+
)
|
| 357 |
+
state_btn.click(
|
| 358 |
+
fn=ui_get_state,
|
| 359 |
+
outputs=[state_display],
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
app = gr.mount_gradio_app(app, demo, path="/")
|
server/graders/__init__.py
ADDED
|
File without changes
|
server/graders/compose_grader.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
from typing import Tuple, List
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def grade_task4(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
+
"""
|
| 7 |
+
Grade Task 4: Broken docker-compose.yml (4 bugs)
|
| 8 |
+
Bug 1 (Syntax): Wrong indentation on volumes key
|
| 9 |
+
Bug 2 (Semantic): Service references non-existent network name
|
| 10 |
+
Bug 3 (Runtime): Port mapping "8080:80" should be "8080:3000"
|
| 11 |
+
Bug 4 (Integration): depends_on references "postgres" but service is defined as "db"
|
| 12 |
+
|
| 13 |
+
Returns: (reward, error_message, bugs_fixed_list)
|
| 14 |
+
"""
|
| 15 |
+
bugs_fixed = []
|
| 16 |
+
total_bugs = 4
|
| 17 |
+
error_messages = []
|
| 18 |
+
|
| 19 |
+
# Bug 1: Check if YAML parses (syntax layer)
|
| 20 |
+
try:
|
| 21 |
+
config = yaml.safe_load(submitted_config)
|
| 22 |
+
if not isinstance(config, dict):
|
| 23 |
+
error_messages.append("docker-compose config is not a valid mapping")
|
| 24 |
+
return 0.0, "; ".join(error_messages), bugs_fixed
|
| 25 |
+
bugs_fixed.append("syntax_valid_yaml")
|
| 26 |
+
except yaml.YAMLError as e:
|
| 27 |
+
error_messages.append(f"YAML parse error: {str(e)}")
|
| 28 |
+
return 0.0, "; ".join(error_messages), bugs_fixed
|
| 29 |
+
|
| 30 |
+
services = config.get("services", {})
|
| 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())
|
| 37 |
+
defined_networks = set(config.get("networks", {}).keys()) if isinstance(config.get("networks"), dict) else set()
|
| 38 |
+
|
| 39 |
+
# Bug 4: Check depends_on references valid service names
|
| 40 |
+
all_deps_valid = True
|
| 41 |
+
for svc_name, svc_config in services.items():
|
| 42 |
+
if not isinstance(svc_config, dict):
|
| 43 |
+
continue
|
| 44 |
+
deps = svc_config.get("depends_on", [])
|
| 45 |
+
if isinstance(deps, list):
|
| 46 |
+
for dep in deps:
|
| 47 |
+
if dep not in defined_service_names:
|
| 48 |
+
all_deps_valid = False
|
| 49 |
+
error_messages.append(
|
| 50 |
+
f"Service '{svc_name}' depends_on '{dep}', "
|
| 51 |
+
f"but no service named '{dep}' is defined. "
|
| 52 |
+
f"Defined services: {sorted(defined_service_names)}"
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
if all_deps_valid:
|
| 56 |
+
bugs_fixed.append("depends_on_valid")
|
| 57 |
+
|
| 58 |
+
# Bug 2: Check all referenced networks exist in top-level networks
|
| 59 |
+
all_nets_valid = True
|
| 60 |
+
for svc_name, svc_config in services.items():
|
| 61 |
+
if not isinstance(svc_config, dict):
|
| 62 |
+
continue
|
| 63 |
+
svc_networks = svc_config.get("networks", [])
|
| 64 |
+
if isinstance(svc_networks, list):
|
| 65 |
+
for net in svc_networks:
|
| 66 |
+
if net not in defined_networks:
|
| 67 |
+
all_nets_valid = False
|
| 68 |
+
error_messages.append(
|
| 69 |
+
f"Service '{svc_name}' references network '{net}', "
|
| 70 |
+
f"but it's not defined in top-level networks. "
|
| 71 |
+
f"Defined networks: {sorted(defined_networks)}"
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
if all_nets_valid:
|
| 75 |
+
bugs_fixed.append("networks_valid")
|
| 76 |
+
|
| 77 |
+
# Bug 3: Check port mapping - web service should map to 3000
|
| 78 |
+
web_svc = services.get("web", {})
|
| 79 |
+
if isinstance(web_svc, dict):
|
| 80 |
+
ports = web_svc.get("ports", [])
|
| 81 |
+
port_correct = False
|
| 82 |
+
for port_mapping in ports:
|
| 83 |
+
port_str = str(port_mapping)
|
| 84 |
+
# Check the container port (right side) is 3000
|
| 85 |
+
if ":3000" in port_str:
|
| 86 |
+
port_correct = True
|
| 87 |
+
break
|
| 88 |
+
if port_correct:
|
| 89 |
+
bugs_fixed.append("port_mapping_correct")
|
| 90 |
+
else:
|
| 91 |
+
error_messages.append(
|
| 92 |
+
"Web service port mapping is incorrect. "
|
| 93 |
+
"The application runs on port 3000, so the container port should be 3000."
|
| 94 |
+
)
|
| 95 |
+
else:
|
| 96 |
+
error_messages.append("Web service is missing or misconfigured")
|
| 97 |
+
|
| 98 |
+
# Calculate reward
|
| 99 |
+
reward = len(bugs_fixed) / total_bugs
|
| 100 |
+
|
| 101 |
+
# Bonus for parsing
|
| 102 |
+
if len(bugs_fixed) >= 1:
|
| 103 |
+
reward = min(1.0, reward + 0.1)
|
| 104 |
+
|
| 105 |
+
if len(bugs_fixed) == total_bugs:
|
| 106 |
+
reward = 1.0
|
| 107 |
+
|
| 108 |
+
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 109 |
+
return reward, error_msg, bugs_fixed
|
server/graders/dockerfile_grader.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from typing import Tuple, List
|
| 3 |
+
|
| 4 |
+
ALLOWED_BASE_IMAGES = [
|
| 5 |
+
"python:3.9-slim", "python:3.10-slim", "python:3.11-slim",
|
| 6 |
+
"python:3.9", "python:3.10", "python:3.11",
|
| 7 |
+
"python:3.9-alpine", "python:3.10-alpine", "python:3.11-alpine",
|
| 8 |
+
"python:3.9-slim-bullseye", "python:3.9-slim-bookworm",
|
| 9 |
+
"node:18", "node:20", "node:18-slim", "node:20-slim",
|
| 10 |
+
"ubuntu:22.04", "ubuntu:20.04", "debian:bullseye-slim",
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def grade_task3(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 15 |
+
"""
|
| 16 |
+
Grade Task 3: Broken Dockerfile (3 bugs)
|
| 17 |
+
Bug 1 (Syntax): RUN command with wrong escape character (^ instead of \\)
|
| 18 |
+
Bug 2 (Semantic): Wrong base image tag (python:3.9-slm instead of python:3.9-slim)
|
| 19 |
+
Bug 3 (Runtime): Missing EXPOSE directive
|
| 20 |
+
|
| 21 |
+
Returns: (reward, error_message, bugs_fixed_list)
|
| 22 |
+
"""
|
| 23 |
+
bugs_fixed = []
|
| 24 |
+
total_bugs = 3
|
| 25 |
+
error_messages = []
|
| 26 |
+
|
| 27 |
+
lines = submitted_config.strip().split("\n")
|
| 28 |
+
|
| 29 |
+
# Bug 2: Check base image is valid
|
| 30 |
+
from_lines = [l for l in lines if l.strip().upper().startswith("FROM")]
|
| 31 |
+
if from_lines:
|
| 32 |
+
from_image = from_lines[0].strip().split()[1] if len(from_lines[0].strip().split()) > 1 else ""
|
| 33 |
+
if from_image in ALLOWED_BASE_IMAGES:
|
| 34 |
+
bugs_fixed.append("valid_base_image")
|
| 35 |
+
else:
|
| 36 |
+
error_messages.append(
|
| 37 |
+
f"Base image '{from_image}' is not a recognized valid image. "
|
| 38 |
+
f"Did you mean 'python:3.9-slim'?"
|
| 39 |
+
)
|
| 40 |
+
else:
|
| 41 |
+
error_messages.append("Missing FROM instruction in Dockerfile")
|
| 42 |
+
|
| 43 |
+
# Bug 1: Check RUN syntax - no ^ for line continuation
|
| 44 |
+
run_blocks = []
|
| 45 |
+
in_run = False
|
| 46 |
+
current_run = []
|
| 47 |
+
for line in lines:
|
| 48 |
+
stripped = line.strip()
|
| 49 |
+
if stripped.upper().startswith("RUN "):
|
| 50 |
+
in_run = True
|
| 51 |
+
current_run = [line]
|
| 52 |
+
elif in_run:
|
| 53 |
+
if current_run[-1].rstrip().endswith("\\"):
|
| 54 |
+
current_run.append(line)
|
| 55 |
+
else:
|
| 56 |
+
run_blocks.append("\n".join(current_run))
|
| 57 |
+
in_run = False
|
| 58 |
+
current_run = []
|
| 59 |
+
if stripped.upper().startswith("RUN "):
|
| 60 |
+
in_run = True
|
| 61 |
+
current_run = [line]
|
| 62 |
+
if not in_run and current_run:
|
| 63 |
+
run_blocks.append("\n".join(current_run))
|
| 64 |
+
current_run = []
|
| 65 |
+
if current_run:
|
| 66 |
+
run_blocks.append("\n".join(current_run))
|
| 67 |
+
|
| 68 |
+
has_caret = any("^" in line and not line.strip().startswith("#") for line in lines)
|
| 69 |
+
# Check if multi-line RUN uses proper backslash
|
| 70 |
+
has_proper_continuation = any(
|
| 71 |
+
line.rstrip().endswith("\\") for line in lines
|
| 72 |
+
if not line.strip().startswith("#")
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
if not has_caret:
|
| 76 |
+
bugs_fixed.append("run_syntax_correct")
|
| 77 |
+
else:
|
| 78 |
+
error_messages.append(
|
| 79 |
+
"RUN command uses '^' for line continuation. "
|
| 80 |
+
"Dockerfiles use '\\' for multi-line commands."
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Bug 3: Check EXPOSE directive present
|
| 84 |
+
expose_lines = [l for l in lines if l.strip().upper().startswith("EXPOSE")]
|
| 85 |
+
if expose_lines:
|
| 86 |
+
bugs_fixed.append("expose_present")
|
| 87 |
+
else:
|
| 88 |
+
error_messages.append(
|
| 89 |
+
"Missing EXPOSE directive. The application port should be exposed."
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Calculate reward
|
| 93 |
+
reward = len(bugs_fixed) / total_bugs
|
| 94 |
+
|
| 95 |
+
# Bonus for having a parseable Dockerfile structure
|
| 96 |
+
has_from = any(l.strip().upper().startswith("FROM") for l in lines)
|
| 97 |
+
has_cmd_or_entrypoint = any(
|
| 98 |
+
l.strip().upper().startswith(("CMD", "ENTRYPOINT")) for l in lines
|
| 99 |
+
)
|
| 100 |
+
if has_from and has_cmd_or_entrypoint:
|
| 101 |
+
reward = min(1.0, reward + 0.1)
|
| 102 |
+
|
| 103 |
+
if len(bugs_fixed) == total_bugs:
|
| 104 |
+
reward = 1.0
|
| 105 |
+
|
| 106 |
+
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 107 |
+
return reward, error_msg, bugs_fixed
|
server/graders/github_actions_grader.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
from typing import Tuple, List
|
| 3 |
+
|
| 4 |
+
VALID_RUNNERS = [
|
| 5 |
+
"ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04", "ubuntu-24.04",
|
| 6 |
+
"macos-latest", "macos-14", "macos-13", "macos-12",
|
| 7 |
+
"windows-latest", "windows-2022", "windows-2019",
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
VALID_CHECKOUT_PARAMS = [
|
| 11 |
+
"repository", "ref", "token", "ssh-key", "ssh-known-hosts", "ssh-strict",
|
| 12 |
+
"persist-credentials", "path", "clean", "filter", "sparse-checkout",
|
| 13 |
+
"sparse-checkout-cone-mode", "fetch-depth", "fetch-tags",
|
| 14 |
+
"show-progress", "lfs", "submodules", "set-safe-directory",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def grade_task6(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 19 |
+
"""
|
| 20 |
+
Grade Task 6: Broken GitHub Actions Workflow (5 bugs)
|
| 21 |
+
Bug 1 (Syntax): Missing colon after 'on' trigger
|
| 22 |
+
Bug 2 (Semantic): runs-on uses "ubuntu-latets" (typo)
|
| 23 |
+
Bug 3 (Semantic): actions/checkout with invalid parameter name "fetch-deph"
|
| 24 |
+
Bug 4 (Runtime): env var references secrets but not defined in env block (acceptable)
|
| 25 |
+
Bug 5 (Integration): Upload artifact name "build-output" vs download "build-artifact"
|
| 26 |
+
|
| 27 |
+
Returns: (reward, error_message, bugs_fixed_list)
|
| 28 |
+
"""
|
| 29 |
+
bugs_fixed = []
|
| 30 |
+
total_bugs = 5
|
| 31 |
+
error_messages = []
|
| 32 |
+
|
| 33 |
+
# Bug 1: Check if YAML parses (syntax layer - the missing colon after 'on')
|
| 34 |
+
try:
|
| 35 |
+
config = yaml.safe_load(submitted_config)
|
| 36 |
+
if not isinstance(config, dict):
|
| 37 |
+
error_messages.append("GitHub Actions workflow is not a valid mapping")
|
| 38 |
+
return 0.0, "; ".join(error_messages), bugs_fixed
|
| 39 |
+
bugs_fixed.append("syntax_valid_yaml")
|
| 40 |
+
except yaml.YAMLError as e:
|
| 41 |
+
error_messages.append(f"YAML parse error: {str(e)}")
|
| 42 |
+
return 0.0, "; ".join(error_messages), bugs_fixed
|
| 43 |
+
|
| 44 |
+
jobs = config.get("jobs", {})
|
| 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
|
| 51 |
+
all_runners_valid = True
|
| 52 |
+
for job_name, job_config in jobs.items():
|
| 53 |
+
if not isinstance(job_config, dict):
|
| 54 |
+
continue
|
| 55 |
+
runner = job_config.get("runs-on", "")
|
| 56 |
+
if runner and runner not in VALID_RUNNERS:
|
| 57 |
+
all_runners_valid = False
|
| 58 |
+
error_messages.append(
|
| 59 |
+
f"Job '{job_name}' uses runner '{runner}' which is not valid. "
|
| 60 |
+
f"Did you mean 'ubuntu-latest'?"
|
| 61 |
+
)
|
| 62 |
+
if all_runners_valid:
|
| 63 |
+
bugs_fixed.append("valid_runners")
|
| 64 |
+
|
| 65 |
+
# Bug 3: Check checkout action parameters
|
| 66 |
+
checkout_params_valid = True
|
| 67 |
+
for job_name, job_config in jobs.items():
|
| 68 |
+
if not isinstance(job_config, dict):
|
| 69 |
+
continue
|
| 70 |
+
steps = job_config.get("steps", [])
|
| 71 |
+
if not isinstance(steps, list):
|
| 72 |
+
continue
|
| 73 |
+
for step in steps:
|
| 74 |
+
if not isinstance(step, dict):
|
| 75 |
+
continue
|
| 76 |
+
uses = step.get("uses", "")
|
| 77 |
+
if "actions/checkout" in str(uses):
|
| 78 |
+
with_params = step.get("with", {})
|
| 79 |
+
if isinstance(with_params, dict):
|
| 80 |
+
for param_name in with_params.keys():
|
| 81 |
+
if param_name not in VALID_CHECKOUT_PARAMS:
|
| 82 |
+
checkout_params_valid = False
|
| 83 |
+
error_messages.append(
|
| 84 |
+
f"actions/checkout parameter '{param_name}' is invalid. "
|
| 85 |
+
f"Did you mean 'fetch-depth'?"
|
| 86 |
+
)
|
| 87 |
+
if checkout_params_valid:
|
| 88 |
+
bugs_fixed.append("checkout_params_valid")
|
| 89 |
+
|
| 90 |
+
# Bug 4: Check that env vars used with secrets are defined in env block
|
| 91 |
+
# This is more of a best-practice check - we'll check the env block exists on steps using secrets
|
| 92 |
+
env_usage_valid = True
|
| 93 |
+
for job_name, job_config in jobs.items():
|
| 94 |
+
if not isinstance(job_config, dict):
|
| 95 |
+
continue
|
| 96 |
+
steps = job_config.get("steps", [])
|
| 97 |
+
if not isinstance(steps, list):
|
| 98 |
+
continue
|
| 99 |
+
for step in steps:
|
| 100 |
+
if not isinstance(step, dict):
|
| 101 |
+
continue
|
| 102 |
+
step_env = step.get("env", {})
|
| 103 |
+
run_cmd = str(step.get("run", ""))
|
| 104 |
+
# Check if run uses env vars that aren't defined
|
| 105 |
+
if "${{" in run_cmd and "secrets." in run_cmd:
|
| 106 |
+
if not step_env:
|
| 107 |
+
env_usage_valid = False
|
| 108 |
+
error_messages.append(
|
| 109 |
+
f"Step '{step.get('name', 'unnamed')}' references secrets "
|
| 110 |
+
f"in run command but doesn't define them in env block."
|
| 111 |
+
)
|
| 112 |
+
if env_usage_valid:
|
| 113 |
+
bugs_fixed.append("env_vars_defined")
|
| 114 |
+
|
| 115 |
+
# Bug 5: Check artifact name consistency between upload and download
|
| 116 |
+
upload_names = set()
|
| 117 |
+
download_names = set()
|
| 118 |
+
for job_name, job_config in jobs.items():
|
| 119 |
+
if not isinstance(job_config, dict):
|
| 120 |
+
continue
|
| 121 |
+
steps = job_config.get("steps", [])
|
| 122 |
+
if not isinstance(steps, list):
|
| 123 |
+
continue
|
| 124 |
+
for step in steps:
|
| 125 |
+
if not isinstance(step, dict):
|
| 126 |
+
continue
|
| 127 |
+
uses = str(step.get("uses", ""))
|
| 128 |
+
with_params = step.get("with", {})
|
| 129 |
+
if not isinstance(with_params, dict):
|
| 130 |
+
continue
|
| 131 |
+
if "upload-artifact" in uses:
|
| 132 |
+
name = with_params.get("name", "")
|
| 133 |
+
if name:
|
| 134 |
+
upload_names.add(name)
|
| 135 |
+
elif "download-artifact" in uses:
|
| 136 |
+
name = with_params.get("name", "")
|
| 137 |
+
if name:
|
| 138 |
+
download_names.add(name)
|
| 139 |
+
|
| 140 |
+
if download_names and upload_names:
|
| 141 |
+
if download_names.issubset(upload_names):
|
| 142 |
+
bugs_fixed.append("artifact_names_match")
|
| 143 |
+
else:
|
| 144 |
+
mismatched = download_names - upload_names
|
| 145 |
+
error_messages.append(
|
| 146 |
+
f"Artifact name mismatch: download references {sorted(mismatched)} "
|
| 147 |
+
f"but upload defines {sorted(upload_names)}. Names must match."
|
| 148 |
+
)
|
| 149 |
+
elif not download_names and not upload_names:
|
| 150 |
+
bugs_fixed.append("artifact_names_match")
|
| 151 |
+
else:
|
| 152 |
+
error_messages.append("Artifact upload/download configuration is incomplete")
|
| 153 |
+
|
| 154 |
+
# Calculate reward
|
| 155 |
+
reward = len(bugs_fixed) / total_bugs
|
| 156 |
+
|
| 157 |
+
if len(bugs_fixed) >= 1:
|
| 158 |
+
reward = min(1.0, reward + 0.1)
|
| 159 |
+
|
| 160 |
+
if len(bugs_fixed) == total_bugs:
|
| 161 |
+
reward = 1.0
|
| 162 |
+
|
| 163 |
+
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 164 |
+
return reward, error_msg, bugs_fixed
|
server/graders/grader_api.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Grader API wrapper layer for OpenEnv validator compatibility.
|
| 2 |
+
|
| 3 |
+
OpenEnv validator expects graders to:
|
| 4 |
+
1. Be callable
|
| 5 |
+
2. Return float only (not tuple)
|
| 6 |
+
3. Return values in [0.0, 1.0] range
|
| 7 |
+
|
| 8 |
+
Our graders return (reward, error_msg, bugs_fixed).
|
| 9 |
+
This wrapper extracts just the float reward.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from server.graders.json_grader import grade_task1 as _g1
|
| 13 |
+
from server.graders.yaml_grader import grade_task2 as _g2
|
| 14 |
+
from server.graders.dockerfile_grader import grade_task3 as _g3
|
| 15 |
+
from server.graders.compose_grader import grade_task4 as _g4
|
| 16 |
+
from server.graders.k8s_grader import grade_task5 as _g5
|
| 17 |
+
from server.graders.github_actions_grader import grade_task6 as _g6
|
| 18 |
+
from server.graders.nginx_grader import grade_task7 as _g7
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _extract(result):
|
| 22 |
+
"""Extract float reward from grader result tuple or return as-is if already float."""
|
| 23 |
+
if isinstance(result, tuple):
|
| 24 |
+
return float(result[0])
|
| 25 |
+
return float(result)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def grade_task1(x):
|
| 29 |
+
"""Task 1 (JSON) grader wrapper."""
|
| 30 |
+
return _extract(_g1(x))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def grade_task2(x):
|
| 34 |
+
"""Task 2 (YAML) grader wrapper."""
|
| 35 |
+
return _extract(_g2(x))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def grade_task3(x):
|
| 39 |
+
"""Task 3 (Dockerfile) grader wrapper."""
|
| 40 |
+
return _extract(_g3(x))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def grade_task4(x):
|
| 44 |
+
"""Task 4 (Docker Compose) grader wrapper."""
|
| 45 |
+
return _extract(_g4(x))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def grade_task5(x):
|
| 49 |
+
"""Task 5 (Kubernetes) grader wrapper."""
|
| 50 |
+
return _extract(_g5(x))
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def grade_task6(x):
|
| 54 |
+
"""Task 6 (GitHub Actions) grader wrapper."""
|
| 55 |
+
return _extract(_g6(x))
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def grade_task7(x):
|
| 59 |
+
"""Task 7 (Nginx) grader wrapper."""
|
| 60 |
+
return _extract(_g7(x))
|
server/graders/json_grader.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import Tuple, List
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def grade_task1(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
+
"""
|
| 7 |
+
Grade Task 1: Broken JSON Config (2 bugs)
|
| 8 |
+
Bug 1 (Syntax): Missing comma between key-value pairs
|
| 9 |
+
Bug 2 (Semantic): Wrong data type for "port" field (string instead of int)
|
| 10 |
+
|
| 11 |
+
Returns: (reward, error_message, bugs_fixed_list)
|
| 12 |
+
"""
|
| 13 |
+
bugs_fixed = []
|
| 14 |
+
total_bugs = 2
|
| 15 |
+
error_messages = []
|
| 16 |
+
|
| 17 |
+
# Bug 1: Check if JSON parses (syntax layer)
|
| 18 |
+
try:
|
| 19 |
+
config = json.loads(submitted_config)
|
| 20 |
+
bugs_fixed.append("syntax_valid_json")
|
| 21 |
+
except json.JSONDecodeError as e:
|
| 22 |
+
error_messages.append(f"JSON parse error: {str(e)}")
|
| 23 |
+
reward = len(bugs_fixed) / total_bugs
|
| 24 |
+
return reward, "; ".join(error_messages), bugs_fixed
|
| 25 |
+
|
| 26 |
+
# Bug 2: Check port is integer (semantic layer)
|
| 27 |
+
if "port" in config:
|
| 28 |
+
if isinstance(config["port"], int):
|
| 29 |
+
bugs_fixed.append("port_is_integer")
|
| 30 |
+
else:
|
| 31 |
+
error_messages.append(
|
| 32 |
+
f"Field 'port' should be integer, got {type(config['port']).__name__}"
|
| 33 |
+
)
|
| 34 |
+
else:
|
| 35 |
+
error_messages.append("Missing required field: 'port'")
|
| 36 |
+
|
| 37 |
+
# Calculate reward
|
| 38 |
+
reward = len(bugs_fixed) / total_bugs
|
| 39 |
+
|
| 40 |
+
# Bonus for full parse
|
| 41 |
+
if len(bugs_fixed) >= 1:
|
| 42 |
+
reward = min(1.0, reward + 0.1)
|
| 43 |
+
|
| 44 |
+
if len(bugs_fixed) == total_bugs:
|
| 45 |
+
reward = 1.0
|
| 46 |
+
|
| 47 |
+
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 48 |
+
return reward, error_msg, bugs_fixed
|
server/graders/k8s_grader.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
from typing import Tuple, List
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def grade_task5(fixed_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
+
"""
|
| 7 |
+
Grade Task 5: Multi-step Kubernetes Deployment YAML debugging.
|
| 8 |
+
|
| 9 |
+
Bug 1: replicas = "three" (string, should be integer) → 0.4 reward
|
| 10 |
+
Bug 2: containerPort = "80" (string, should be integer) → +0.3 reward
|
| 11 |
+
Bug 3: cpu = "500" (missing 'm' suffix, should be "500m") → +0.3 reward
|
| 12 |
+
|
| 13 |
+
Returns: (reward, error_message, bugs_fixed_list)
|
| 14 |
+
"""
|
| 15 |
+
reward = 0.0
|
| 16 |
+
errors = []
|
| 17 |
+
fixed = []
|
| 18 |
+
|
| 19 |
+
# Parse YAML
|
| 20 |
+
try:
|
| 21 |
+
config = yaml.safe_load(fixed_config)
|
| 22 |
+
except Exception as e:
|
| 23 |
+
return 0.0, f"Invalid YAML format: {str(e)}", ["yaml"]
|
| 24 |
+
|
| 25 |
+
# --- STEP 1: replicas fix (0.0 → 0.4) ---
|
| 26 |
+
try:
|
| 27 |
+
replicas = config["spec"]["replicas"]
|
| 28 |
+
if isinstance(replicas, int):
|
| 29 |
+
reward += 0.4
|
| 30 |
+
fixed.append("replicas")
|
| 31 |
+
else:
|
| 32 |
+
errors.append("replicas must be an integer (not string)")
|
| 33 |
+
except Exception:
|
| 34 |
+
errors.append("missing or invalid replicas field")
|
| 35 |
+
|
| 36 |
+
# --- STEP 2: containerPort fix (0.4 → 0.7) ---
|
| 37 |
+
try:
|
| 38 |
+
port = config["spec"]["template"]["spec"]["containers"][0]["ports"][0]["containerPort"]
|
| 39 |
+
if isinstance(port, int):
|
| 40 |
+
reward += 0.3
|
| 41 |
+
fixed.append("port")
|
| 42 |
+
else:
|
| 43 |
+
errors.append("containerPort must be integer (not string)")
|
| 44 |
+
except Exception:
|
| 45 |
+
errors.append("missing or invalid containerPort")
|
| 46 |
+
|
| 47 |
+
# --- STEP 3: cpu resource unit fix (0.7 → 1.0) ---
|
| 48 |
+
try:
|
| 49 |
+
cpu = config["spec"]["template"]["spec"]["containers"][0]["resources"]["limits"]["cpu"]
|
| 50 |
+
if isinstance(cpu, str) and cpu.endswith("m"):
|
| 51 |
+
reward += 0.3
|
| 52 |
+
fixed.append("cpu")
|
| 53 |
+
else:
|
| 54 |
+
errors.append("cpu must be in millicores format (e.g., 500m, not plain integer)")
|
| 55 |
+
except Exception:
|
| 56 |
+
errors.append("missing or invalid cpu limit")
|
| 57 |
+
|
| 58 |
+
reward = round(min(reward, 1.0), 2)
|
| 59 |
+
|
| 60 |
+
if reward == 1.0:
|
| 61 |
+
return 1.0, "Deployment config is fully valid", fixed
|
| 62 |
+
|
| 63 |
+
error_msg = " ; ".join(errors) if errors else "Configuration has issues"
|
| 64 |
+
return reward, error_msg, fixed
|
server/graders/nginx_grader.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Tuple, List
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def grade_task7(fixed_config: str) -> Tuple[float, str, List[str]]:
|
| 5 |
+
"""
|
| 6 |
+
Grade Task 7: Multi-step nginx config debugging.
|
| 7 |
+
|
| 8 |
+
Bug 1: Missing semicolons in listen and error_log directives → 0.3 reward
|
| 9 |
+
Bug 2: Missing http:// prefix in proxy_pass directive → +0.3 reward
|
| 10 |
+
Bug 3: Improper routing with missing API endpoint headers → +0.4 reward
|
| 11 |
+
|
| 12 |
+
Returns: (reward, error_message, bugs_fixed_list)
|
| 13 |
+
"""
|
| 14 |
+
reward = 0.0
|
| 15 |
+
errors = []
|
| 16 |
+
fixed = []
|
| 17 |
+
|
| 18 |
+
config = fixed_config.strip()
|
| 19 |
+
|
| 20 |
+
# --- STEP 1: Syntax checks (semicolons) ---
|
| 21 |
+
if "listen 80;" in config and "error_log logs/error.log;" in config:
|
| 22 |
+
reward += 0.3
|
| 23 |
+
fixed.append("syntax")
|
| 24 |
+
else:
|
| 25 |
+
errors.append("Missing semicolon in listen or error_log directives")
|
| 26 |
+
|
| 27 |
+
# --- STEP 2: Directive correctness (proxy_pass protocol) ---
|
| 28 |
+
if "proxy_pass http://localhost:3000;" in config:
|
| 29 |
+
reward += 0.3
|
| 30 |
+
fixed.append("proxy_pass")
|
| 31 |
+
else:
|
| 32 |
+
errors.append("proxy_pass for / must include http:// protocol prefix (e.g., http://localhost:3000;)")
|
| 33 |
+
|
| 34 |
+
# --- STEP 3: Routing logic (API endpoint with headers) ---
|
| 35 |
+
if ("location /api/" in config or "location /api {" in config):
|
| 36 |
+
if "proxy_set_header Host" in config and "proxy_set_header X-Real-IP" in config:
|
| 37 |
+
reward += 0.4
|
| 38 |
+
fixed.append("routing")
|
| 39 |
+
else:
|
| 40 |
+
errors.append("API routing missing required proxy headers (Host and X-Real-IP)")
|
| 41 |
+
else:
|
| 42 |
+
errors.append("Improper API route configuration (use 'location /api/' with headers)")
|
| 43 |
+
|
| 44 |
+
reward = round(min(reward, 1.0), 2)
|
| 45 |
+
|
| 46 |
+
if reward == 1.0:
|
| 47 |
+
return 1.0, "Nginx config fully valid", fixed
|
| 48 |
+
|
| 49 |
+
return reward, " ; ".join(errors), fixed
|
server/graders/yaml_grader.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
from typing import Tuple, List
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def grade_task2(submitted_config: str) -> Tuple[float, str, List[str]]:
|
| 6 |
+
"""
|
| 7 |
+
Grade Task 2: Broken YAML Config (2 bugs)
|
| 8 |
+
Bug 1 (Syntax): Wrong indentation on nested key (3 spaces instead of 2)
|
| 9 |
+
Bug 2 (Semantic): Missing required field "version"
|
| 10 |
+
|
| 11 |
+
Returns: (reward, error_message, bugs_fixed_list)
|
| 12 |
+
"""
|
| 13 |
+
bugs_fixed = []
|
| 14 |
+
total_bugs = 2
|
| 15 |
+
error_messages = []
|
| 16 |
+
|
| 17 |
+
# Bug 1: Check if YAML parses (syntax layer)
|
| 18 |
+
try:
|
| 19 |
+
config = yaml.safe_load(submitted_config)
|
| 20 |
+
if isinstance(config, dict):
|
| 21 |
+
bugs_fixed.append("syntax_valid_yaml")
|
| 22 |
+
else:
|
| 23 |
+
error_messages.append("YAML parsed but result is not a mapping/dictionary")
|
| 24 |
+
reward = len(bugs_fixed) / total_bugs
|
| 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 |
+
reward = len(bugs_fixed) / total_bugs
|
| 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 |
+
# Bonus for full parse
|
| 44 |
+
if len(bugs_fixed) >= 1:
|
| 45 |
+
reward = min(1.0, reward + 0.1)
|
| 46 |
+
|
| 47 |
+
if len(bugs_fixed) == total_bugs:
|
| 48 |
+
reward = 1.0
|
| 49 |
+
|
| 50 |
+
error_msg = "; ".join(error_messages) if error_messages else "All checks passed!"
|
| 51 |
+
return reward, error_msg, bugs_fixed
|
server/models.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
from openenv.core.env_server import Action, Observation, State
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class ConfigDebugAction(Action):
|
| 6 |
+
"""Action: the agent submits a fixed configuration file"""
|
| 7 |
+
fixed_config: str # The corrected configuration file content
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ConfigDebugObservation(Observation):
|
| 11 |
+
"""Observation: what the agent sees.
|
| 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 = ""
|
| 18 |
+
task_description: str = ""
|
| 19 |
+
difficulty: str = ""
|
| 20 |
+
num_bugs: int = 0
|
| 21 |
+
bugs_found_so_far: int = 0
|
| 22 |
+
previous_reward: float = 0.0
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ConfigDebugState(State):
|
| 26 |
+
"""Full environment state.
|
| 27 |
+
Inherits episode_id: Optional[str], step_count: int from State.
|
| 28 |
+
"""
|
| 29 |
+
current_task_id: str = ""
|
| 30 |
+
current_step: int = 0
|
| 31 |
+
max_steps: int = 5
|
| 32 |
+
total_reward: float = 0.0
|
| 33 |
+
is_done: bool = False
|
| 34 |
+
tasks_completed: List[str] = []
|
| 35 |
+
tasks_remaining: List[str] = []
|
| 36 |
+
|
| 37 |
+
# Enhanced state fields for better RL signal
|
| 38 |
+
bugs_found_so_far: int = 0
|
| 39 |
+
current_error_message: Optional[str] = None
|
| 40 |
+
progress_ratio: float = 0.0
|
| 41 |
+
current_difficulty: Optional[str] = None
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pydantic
|
| 4 |
+
pyyaml
|
| 5 |
+
httpx
|
| 6 |
+
openai
|
| 7 |
+
gradio
|
| 8 |
+
openenv-core>=0.2.0
|
server/tasks/__init__.py
ADDED
|
File without changes
|
server/tasks/task1_json.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TASK_ID = "task1_json"
|
| 2 |
+
DIFFICULTY = "easy"
|
| 3 |
+
FILE_TYPE = "json"
|
| 4 |
+
NUM_BUGS = 2
|
| 5 |
+
|
| 6 |
+
DESCRIPTION = (
|
| 7 |
+
"A simple application configuration file in JSON format. "
|
| 8 |
+
"It should define the app name, port (as integer), host, and debug mode."
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
# The broken config (what the agent sees)
|
| 12 |
+
# Bug 1 (Syntax): Missing comma after "my-service"
|
| 13 |
+
# Bug 2 (Semantic): port is string "8080" instead of integer 8080
|
| 14 |
+
BROKEN_CONFIG = """{
|
| 15 |
+
"app_name": "my-service"
|
| 16 |
+
"port": "8080",
|
| 17 |
+
"host": "0.0.0.0",
|
| 18 |
+
"debug": true
|
| 19 |
+
}"""
|
| 20 |
+
|
| 21 |
+
ERROR_MESSAGE = (
|
| 22 |
+
"JSON parse error: Expecting ',' delimiter: line 3 column 5 (char 33). "
|
| 23 |
+
"Additionally, there may be type issues with some fields."
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
GROUND_TRUTH = """{
|
| 27 |
+
"app_name": "my-service",
|
| 28 |
+
"port": 8080,
|
| 29 |
+
"host": "0.0.0.0",
|
| 30 |
+
"debug": true
|
| 31 |
+
}"""
|
server/tasks/task2_yaml.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TASK_ID = "task2_yaml"
|
| 2 |
+
DIFFICULTY = "easy"
|
| 3 |
+
FILE_TYPE = "yaml"
|
| 4 |
+
NUM_BUGS = 2
|
| 5 |
+
|
| 6 |
+
DESCRIPTION = (
|
| 7 |
+
"A service configuration file in YAML format. "
|
| 8 |
+
"It should define the service name, version, port, host, database settings, "
|
| 9 |
+
"and logging configuration with proper indentation and all required fields."
|
| 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 (Semantic): Missing required field "version"
|
| 15 |
+
BROKEN_CONFIG = """service:
|
| 16 |
+
name: my-service
|
| 17 |
+
port: 8080
|
| 18 |
+
host: 0.0.0.0
|
| 19 |
+
database:
|
| 20 |
+
host: localhost
|
| 21 |
+
port: 5432
|
| 22 |
+
name: mydb
|
| 23 |
+
logging:
|
| 24 |
+
level: info
|
| 25 |
+
format: json"""
|
| 26 |
+
|
| 27 |
+
ERROR_MESSAGE = (
|
| 28 |
+
"YAML parse error: mapping values are not allowed in this context. "
|
| 29 |
+
"Additionally, the configuration may be missing required fields."
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
GROUND_TRUTH = """service:
|
| 33 |
+
name: my-service
|
| 34 |
+
version: "1.0.0"
|
| 35 |
+
port: 8080
|
| 36 |
+
host: 0.0.0.0
|
| 37 |
+
database:
|
| 38 |
+
host: localhost
|
| 39 |
+
port: 5432
|
| 40 |
+
name: mydb
|
| 41 |
+
logging:
|
| 42 |
+
level: info
|
| 43 |
+
format: json"""
|
server/tasks/task3_dockerfile.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TASK_ID = "task3_dockerfile"
|
| 2 |
+
DIFFICULTY = "medium"
|
| 3 |
+
FILE_TYPE = "dockerfile"
|
| 4 |
+
NUM_BUGS = 3
|
| 5 |
+
|
| 6 |
+
DESCRIPTION = (
|
| 7 |
+
"A Dockerfile for a Python web application. It should use the python:3.9-slim "
|
| 8 |
+
"base image, install dependencies from requirements.txt, copy the application code, "
|
| 9 |
+
"expose port 8000, and run the application with uvicorn."
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
# Bug 1 (Syntax): Wrong escape character for multi-line RUN (using ^ instead of \)
|
| 13 |
+
# Bug 2 (Semantic): Wrong base image tag (python:3.9-slm instead of python:3.9-slim)
|
| 14 |
+
# Bug 3 (Runtime): Missing EXPOSE directive for the application port
|
| 15 |
+
BROKEN_CONFIG = """FROM python:3.9-slm
|
| 16 |
+
|
| 17 |
+
WORKDIR /app
|
| 18 |
+
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
|
| 21 |
+
RUN pip install --no-cache-dir -r requirements.txt ^
|
| 22 |
+
&& pip install uvicorn
|
| 23 |
+
|
| 24 |
+
COPY . .
|
| 25 |
+
|
| 26 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]"""
|
| 27 |
+
|
| 28 |
+
ERROR_MESSAGE = (
|
| 29 |
+
"Dockerfile build error: failed to resolve base image 'python:3.9-slm'. "
|
| 30 |
+
"Additionally, the RUN command may have syntax issues with multi-line continuation, "
|
| 31 |
+
"and the container may not be accessible on the expected port."
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
GROUND_TRUTH = """FROM python:3.9-slim
|
| 35 |
+
|
| 36 |
+
WORKDIR /app
|
| 37 |
+
|
| 38 |
+
COPY requirements.txt .
|
| 39 |
+
|
| 40 |
+
RUN pip install --no-cache-dir -r requirements.txt \\
|
| 41 |
+
&& pip install uvicorn
|
| 42 |
+
|
| 43 |
+
COPY . .
|
| 44 |
+
|
| 45 |
+
EXPOSE 8000
|
| 46 |
+
|
| 47 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]"""
|
server/tasks/task4_compose.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TASK_ID = "task4_compose"
|
| 2 |
+
DIFFICULTY = "medium"
|
| 3 |
+
FILE_TYPE = "docker-compose"
|
| 4 |
+
NUM_BUGS = 4
|
| 5 |
+
|
| 6 |
+
DESCRIPTION = (
|
| 7 |
+
"A docker-compose.yml file for a multi-service application with a web frontend "
|
| 8 |
+
"(Node.js app running on port 3000), a PostgreSQL database, and a Redis cache. "
|
| 9 |
+
"Services should be connected via a custom network and the web service should "
|
| 10 |
+
"depend on the database and cache services."
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
# Bug 1 (Syntax): Wrong indentation on volumes key
|
| 14 |
+
# Bug 2 (Semantic): Service references non-existent network name
|
| 15 |
+
# Bug 3 (Runtime): Port mapping "8080:80" should be "8080:3000" (app runs on 3000)
|
| 16 |
+
# Bug 4 (Integration): depends_on references "postgres" but service is defined as "db"
|
| 17 |
+
BROKEN_CONFIG = """version: "3.8"
|
| 18 |
+
|
| 19 |
+
services:
|
| 20 |
+
web:
|
| 21 |
+
build: ./app
|
| 22 |
+
ports:
|
| 23 |
+
- "8080:80"
|
| 24 |
+
environment:
|
| 25 |
+
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
|
| 26 |
+
- REDIS_URL=redis://cache:6379
|
| 27 |
+
depends_on:
|
| 28 |
+
- postgres
|
| 29 |
+
- cache
|
| 30 |
+
networks:
|
| 31 |
+
- frontend-net
|
| 32 |
+
|
| 33 |
+
db:
|
| 34 |
+
image: postgres:15
|
| 35 |
+
environment:
|
| 36 |
+
- POSTGRES_USER=user
|
| 37 |
+
- POSTGRES_PASSWORD=pass
|
| 38 |
+
- POSTGRES_DB=mydb
|
| 39 |
+
volumes:
|
| 40 |
+
- db-data:/var/lib/postgresql/data
|
| 41 |
+
networks:
|
| 42 |
+
- backend-net
|
| 43 |
+
|
| 44 |
+
cache:
|
| 45 |
+
image: redis:7-alpine
|
| 46 |
+
networks:
|
| 47 |
+
- backend-net
|
| 48 |
+
|
| 49 |
+
networks:
|
| 50 |
+
app-network:
|
| 51 |
+
driver: bridge
|
| 52 |
+
|
| 53 |
+
volumes:
|
| 54 |
+
db-data:"""
|
| 55 |
+
|
| 56 |
+
ERROR_MESSAGE = (
|
| 57 |
+
"docker-compose config error: yaml parse error near 'volumes' key. "
|
| 58 |
+
"Additionally, there are issues with service references, network names, "
|
| 59 |
+
"and port mappings that need to be fixed."
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
GROUND_TRUTH = """version: "3.8"
|
| 63 |
+
|
| 64 |
+
services:
|
| 65 |
+
web:
|
| 66 |
+
build: ./app
|
| 67 |
+
ports:
|
| 68 |
+
- "8080:3000"
|
| 69 |
+
environment:
|
| 70 |
+
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
|
| 71 |
+
- REDIS_URL=redis://cache:6379
|
| 72 |
+
depends_on:
|
| 73 |
+
- db
|
| 74 |
+
- cache
|
| 75 |
+
networks:
|
| 76 |
+
- app-network
|
| 77 |
+
|
| 78 |
+
db:
|
| 79 |
+
image: postgres:15
|
| 80 |
+
environment:
|
| 81 |
+
- POSTGRES_USER=user
|
| 82 |
+
- POSTGRES_PASSWORD=pass
|
| 83 |
+
- POSTGRES_DB=mydb
|
| 84 |
+
volumes:
|
| 85 |
+
- db-data:/var/lib/postgresql/data
|
| 86 |
+
networks:
|
| 87 |
+
- app-network
|
| 88 |
+
|
| 89 |
+
cache:
|
| 90 |
+
image: redis:7-alpine
|
| 91 |
+
networks:
|
| 92 |
+
- app-network
|
| 93 |
+
|
| 94 |
+
networks:
|
| 95 |
+
app-network:
|
| 96 |
+
driver: bridge
|
| 97 |
+
|
| 98 |
+
volumes:
|
| 99 |
+
db-data:"""
|
server/tasks/task5_k8s.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TASK_ID = "task5_k8s"
|
| 2 |
+
DIFFICULTY = "hard"
|
| 3 |
+
FILE_TYPE = "kubernetes"
|
| 4 |
+
NUM_BUGS = 3
|
| 5 |
+
|
| 6 |
+
DESCRIPTION = (
|
| 7 |
+
"A Kubernetes Deployment manifest with multi-step bugs. Requires fixing replicas type, "
|
| 8 |
+
"containerPort type, and CPU resource units. Each fix provides intermediate feedback."
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
# Bug 1 (Type): replicas = "three" (string, should be integer)
|
| 12 |
+
# Bug 2 (Type): containerPort = "80" (string, should be integer)
|
| 13 |
+
# Bug 3 (Domain): cpu = "500" (missing 'm' suffix, should be "500m")
|
| 14 |
+
BROKEN_CONFIG = """apiVersion: apps/v1
|
| 15 |
+
kind: Deployment
|
| 16 |
+
metadata:
|
| 17 |
+
name: my-app
|
| 18 |
+
spec:
|
| 19 |
+
replicas: "three"
|
| 20 |
+
selector:
|
| 21 |
+
matchLabels:
|
| 22 |
+
app: my-app
|
| 23 |
+
template:
|
| 24 |
+
metadata:
|
| 25 |
+
labels:
|
| 26 |
+
app: my-app
|
| 27 |
+
spec:
|
| 28 |
+
containers:
|
| 29 |
+
- name: my-container
|
| 30 |
+
image: nginx
|
| 31 |
+
ports:
|
| 32 |
+
- containerPort: "80"
|
| 33 |
+
resources:
|
| 34 |
+
limits:
|
| 35 |
+
cpu: "500"
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
ERROR_MESSAGE = (
|
| 39 |
+
"Kubernetes manifest has type and unit errors: replicas must be integer, "
|
| 40 |
+
"containerPort must be integer, and cpu must include millicores unit (e.g., 500m)."
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
GROUND_TRUTH = """apiVersion: apps/v1
|
| 44 |
+
kind: Deployment
|
| 45 |
+
metadata:
|
| 46 |
+
name: my-app
|
| 47 |
+
spec:
|
| 48 |
+
replicas: 3
|
| 49 |
+
selector:
|
| 50 |
+
matchLabels:
|
| 51 |
+
app: my-app
|
| 52 |
+
template:
|
| 53 |
+
metadata:
|
| 54 |
+
labels:
|
| 55 |
+
app: my-app
|
| 56 |
+
spec:
|
| 57 |
+
containers:
|
| 58 |
+
- name: my-container
|
| 59 |
+
image: nginx
|
| 60 |
+
ports:
|
| 61 |
+
- containerPort: 80
|
| 62 |
+
resources:
|
| 63 |
+
limits:
|
| 64 |
+
cpu: "500m"
|
| 65 |
+
"""
|
| 66 |
+
|
server/tasks/task6_github_actions.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TASK_ID = "task6_github_actions"
|
| 2 |
+
DIFFICULTY = "hard"
|
| 3 |
+
FILE_TYPE = "github-actions"
|
| 4 |
+
NUM_BUGS = 5
|
| 5 |
+
|
| 6 |
+
DESCRIPTION = (
|
| 7 |
+
"A GitHub Actions workflow that builds a Node.js application on push to main, "
|
| 8 |
+
"runs tests, uploads a build artifact, and deploys it. It should use ubuntu-latest "
|
| 9 |
+
"runner, checkout the code, setup Node.js, install deps, build, upload artifact as "
|
| 10 |
+
"'build-output', and download the same artifact in the deploy job."
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
# Bug 1 (Syntax): Missing colon after "on" trigger value (push should be on:\n push:)
|
| 14 |
+
# Bug 2 (Semantic): runs-on uses "ubuntu-latets" (typo, should be "ubuntu-latest")
|
| 15 |
+
# Bug 3 (Semantic): Step uses actions/checkout@v2 but with has invalid parameter name
|
| 16 |
+
# Bug 4 (Runtime): Environment variable referenced but not defined in env block
|
| 17 |
+
# Bug 5 (Integration): Build step artifact name "build-output" but deploy references "build-artifact"
|
| 18 |
+
BROKEN_CONFIG = """name: CI/CD Pipeline
|
| 19 |
+
|
| 20 |
+
on
|
| 21 |
+
push:
|
| 22 |
+
branches: [main]
|
| 23 |
+
pull_request:
|
| 24 |
+
branches: [main]
|
| 25 |
+
|
| 26 |
+
jobs:
|
| 27 |
+
build:
|
| 28 |
+
runs-on: ubuntu-latets
|
| 29 |
+
steps:
|
| 30 |
+
- uses: actions/checkout@v4
|
| 31 |
+
with:
|
| 32 |
+
fetch-deph: 0
|
| 33 |
+
|
| 34 |
+
- name: Setup Node.js
|
| 35 |
+
uses: actions/setup-node@v4
|
| 36 |
+
with:
|
| 37 |
+
node-version: '18'
|
| 38 |
+
|
| 39 |
+
- name: Install dependencies
|
| 40 |
+
run: npm ci
|
| 41 |
+
|
| 42 |
+
- name: Run tests
|
| 43 |
+
run: npm test
|
| 44 |
+
env:
|
| 45 |
+
API_ENDPOINT: ${{ secrets.API_KEY }}
|
| 46 |
+
|
| 47 |
+
- name: Build
|
| 48 |
+
run: npm run build
|
| 49 |
+
|
| 50 |
+
- name: Upload artifact
|
| 51 |
+
uses: actions/upload-artifact@v4
|
| 52 |
+
with:
|
| 53 |
+
name: build-output
|
| 54 |
+
path: dist/
|
| 55 |
+
|
| 56 |
+
deploy:
|
| 57 |
+
needs: build
|
| 58 |
+
runs-on: ubuntu-latest
|
| 59 |
+
steps:
|
| 60 |
+
- name: Download artifact
|
| 61 |
+
uses: actions/download-artifact@v4
|
| 62 |
+
with:
|
| 63 |
+
name: build-artifact
|
| 64 |
+
|
| 65 |
+
- name: Deploy
|
| 66 |
+
run: echo "Deploying..."
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
ERROR_MESSAGE = (
|
| 70 |
+
"GitHub Actions workflow error: YAML parse error near 'on' key. "
|
| 71 |
+
"Additionally, the runner name may have a typo, an action parameter name is invalid, "
|
| 72 |
+
"environment variables may be misconfigured, and artifact names between jobs may not match."
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
GROUND_TRUTH = """name: CI/CD Pipeline
|
| 76 |
+
|
| 77 |
+
on:
|
| 78 |
+
push:
|
| 79 |
+
branches: [main]
|
| 80 |
+
pull_request:
|
| 81 |
+
branches: [main]
|
| 82 |
+
|
| 83 |
+
jobs:
|
| 84 |
+
build:
|
| 85 |
+
runs-on: ubuntu-latest
|
| 86 |
+
steps:
|
| 87 |
+
- uses: actions/checkout@v4
|
| 88 |
+
with:
|
| 89 |
+
fetch-depth: 0
|
| 90 |
+
|
| 91 |
+
- name: Setup Node.js
|
| 92 |
+
uses: actions/setup-node@v4
|
| 93 |
+
with:
|
| 94 |
+
node-version: '18'
|
| 95 |
+
|
| 96 |
+
- name: Install dependencies
|
| 97 |
+
run: npm ci
|
| 98 |
+
|
| 99 |
+
- name: Run tests
|
| 100 |
+
run: npm test
|
| 101 |
+
env:
|
| 102 |
+
API_ENDPOINT: ${{ secrets.API_KEY }}
|
| 103 |
+
|
| 104 |
+
- name: Build
|
| 105 |
+
run: npm run build
|
| 106 |
+
|
| 107 |
+
- name: Upload artifact
|
| 108 |
+
uses: actions/upload-artifact@v4
|
| 109 |
+
with:
|
| 110 |
+
name: build-output
|
| 111 |
+
path: dist/
|
| 112 |
+
|
| 113 |
+
deploy:
|
| 114 |
+
needs: build
|
| 115 |
+
runs-on: ubuntu-latest
|
| 116 |
+
steps:
|
| 117 |
+
- name: Download artifact
|
| 118 |
+
uses: actions/download-artifact@v4
|
| 119 |
+
with:
|
| 120 |
+
name: build-output
|
| 121 |
+
|
| 122 |
+
- name: Deploy
|
| 123 |
+
run: echo "Deploying..."
|
| 124 |
+
"""
|
server/tasks/task7_nginx.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TASK_ID = "task7_nginx"
|
| 2 |
+
DIFFICULTY = "very_hard"
|
| 3 |
+
FILE_TYPE = "nginx"
|
| 4 |
+
NUM_BUGS = 3
|
| 5 |
+
|
| 6 |
+
DESCRIPTION = (
|
| 7 |
+
"An nginx reverse proxy configuration with multi-step bugs. "
|
| 8 |
+
"Requires fixing syntax (semicolons), directives (protocol), and routing logic (headers)."
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
# Bug 1 (Syntax): Missing semicolons in listen and error_log directives
|
| 12 |
+
# Bug 2 (Directive): proxy_pass missing http:// protocol prefix
|
| 13 |
+
# Bug 3 (Logic): Improper routing with missing headers for API endpoint
|
| 14 |
+
BROKEN_CONFIG = """events {}
|
| 15 |
+
|
| 16 |
+
http {
|
| 17 |
+
server {
|
| 18 |
+
listen 80
|
| 19 |
+
|
| 20 |
+
location / {
|
| 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
|
| 31 |
+
}
|
| 32 |
+
}"""
|
| 33 |
+
|
| 34 |
+
ERROR_MESSAGE = (
|
| 35 |
+
"nginx configuration has syntax, directive, and routing errors: "
|
| 36 |
+
"missing semicolons, missing http:// prefix in proxy_pass, and improper API routing."
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
GROUND_TRUTH = """events {}
|
| 40 |
+
|
| 41 |
+
http {
|
| 42 |
+
server {
|
| 43 |
+
listen 80;
|
| 44 |
+
|
| 45 |
+
location / {
|
| 46 |
+
proxy_pass http://localhost:3000;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
location /api/ {
|
| 50 |
+
proxy_pass http://localhost:5000;
|
| 51 |
+
proxy_set_header Host $host;
|
| 52 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
error_log logs/error.log;
|
| 56 |
+
}
|
| 57 |
+
}"""
|
server/tasks/task_registry.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
from server.graders.json_grader import grade_task1
|
| 6 |
+
from server.graders.yaml_grader import grade_task2
|
| 7 |
+
from server.graders.dockerfile_grader import grade_task3
|
| 8 |
+
from server.graders.compose_grader import grade_task4
|
| 9 |
+
from server.graders.k8s_grader import grade_task5
|
| 10 |
+
from server.graders.github_actions_grader import grade_task6
|
| 11 |
+
from server.graders.nginx_grader import grade_task7
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TaskInfo:
|
| 15 |
+
def __init__(self, module, grader_func: Callable):
|
| 16 |
+
self.task_id: str = module.TASK_ID
|
| 17 |
+
self.difficulty: str = module.DIFFICULTY
|
| 18 |
+
self.file_type: str = module.FILE_TYPE
|
| 19 |
+
self.num_bugs: int = module.NUM_BUGS
|
| 20 |
+
self.description: str = module.DESCRIPTION
|
| 21 |
+
self.broken_config: str = module.BROKEN_CONFIG
|
| 22 |
+
self.error_message: str = module.ERROR_MESSAGE
|
| 23 |
+
self.ground_truth: str = module.GROUND_TRUTH
|
| 24 |
+
self.grader: Callable[[str], Tuple[float, str, List[str]]] = grader_func
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
TASK_ORDER = [
|
| 28 |
+
"task1_json",
|
| 29 |
+
"task2_yaml",
|
| 30 |
+
"task3_dockerfile",
|
| 31 |
+
"task4_compose",
|
| 32 |
+
"task5_k8s",
|
| 33 |
+
"task6_github_actions",
|
| 34 |
+
"task7_nginx",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
TASK_REGISTRY: Dict[str, TaskInfo] = {
|
| 38 |
+
"task1_json": TaskInfo(task1_json, grade_task1),
|
| 39 |
+
"task2_yaml": TaskInfo(task2_yaml, grade_task2),
|
| 40 |
+
"task3_dockerfile": TaskInfo(task3_dockerfile, grade_task3),
|
| 41 |
+
"task4_compose": TaskInfo(task4_compose, grade_task4),
|
| 42 |
+
"task5_k8s": TaskInfo(task5_k8s, grade_task5),
|
| 43 |
+
"task6_github_actions": TaskInfo(task6_github_actions, grade_task6),
|
| 44 |
+
"task7_nginx": TaskInfo(task7_nginx, grade_task7),
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def get_task(task_id: str) -> TaskInfo:
|
| 49 |
+
return TASK_REGISTRY[task_id]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_all_task_ids() -> List[str]:
|
| 53 |
+
return list(TASK_ORDER)
|
test_env.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from server.config_debug_environment import ConfigDebugEnvironment
|
| 2 |
+
from server.models import ConfigDebugAction
|
| 3 |
+
from server.tasks.task_registry import get_task
|
| 4 |
+
|
| 5 |
+
env = ConfigDebugEnvironment()
|
| 6 |
+
|
| 7 |
+
# ONLY ONE RESET
|
| 8 |
+
obs = env.reset()
|
| 9 |
+
|
| 10 |
+
for i in range(3):
|
| 11 |
+
print(f"\n--- Step {i+1} ---")
|
| 12 |
+
|
| 13 |
+
task = get_task(obs.task_id)
|
| 14 |
+
correct_output = task.ground_truth
|
| 15 |
+
|
| 16 |
+
action = ConfigDebugAction(fixed_config=correct_output)
|
| 17 |
+
obs = env.step(action)
|
| 18 |
+
|
| 19 |
+
print("Task ID:", obs.task_id)
|
| 20 |
+
print("Reward:", obs.reward)
|
| 21 |
+
print("Done:", obs.done)
|
test_k8s_multistep.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from server.graders.k8s_grader import grade_task5
|
| 2 |
+
|
| 3 |
+
# Test 1: Only fix replicas
|
| 4 |
+
config1 = """apiVersion: apps/v1
|
| 5 |
+
kind: Deployment
|
| 6 |
+
metadata:
|
| 7 |
+
name: my-app
|
| 8 |
+
spec:
|
| 9 |
+
replicas: 3
|
| 10 |
+
selector:
|
| 11 |
+
matchLabels:
|
| 12 |
+
app: my-app
|
| 13 |
+
template:
|
| 14 |
+
metadata:
|
| 15 |
+
labels:
|
| 16 |
+
app: my-app
|
| 17 |
+
spec:
|
| 18 |
+
containers:
|
| 19 |
+
- name: my-container
|
| 20 |
+
image: nginx
|
| 21 |
+
ports:
|
| 22 |
+
- containerPort: "80"
|
| 23 |
+
resources:
|
| 24 |
+
limits:
|
| 25 |
+
cpu: "500"
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
reward1, msg1, fixed1 = grade_task5(config1)
|
| 29 |
+
print(f'Step 1 (replicas fixed): Reward={reward1}, Fixed={fixed1}')
|
| 30 |
+
print(f' Error: {msg1}\n')
|
| 31 |
+
|
| 32 |
+
# Test 2: Fix replicas + port
|
| 33 |
+
config2 = """apiVersion: apps/v1
|
| 34 |
+
kind: Deployment
|
| 35 |
+
metadata:
|
| 36 |
+
name: my-app
|
| 37 |
+
spec:
|
| 38 |
+
replicas: 3
|
| 39 |
+
selector:
|
| 40 |
+
matchLabels:
|
| 41 |
+
app: my-app
|
| 42 |
+
template:
|
| 43 |
+
metadata:
|
| 44 |
+
labels:
|
| 45 |
+
app: my-app
|
| 46 |
+
spec:
|
| 47 |
+
containers:
|
| 48 |
+
- name: my-container
|
| 49 |
+
image: nginx
|
| 50 |
+
ports:
|
| 51 |
+
- containerPort: 80
|
| 52 |
+
resources:
|
| 53 |
+
limits:
|
| 54 |
+
cpu: "500"
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
reward2, msg2, fixed2 = grade_task5(config2)
|
| 58 |
+
print(f'Step 2 (replicas + port fixed): Reward={reward2}, Fixed={fixed2}')
|
| 59 |
+
print(f' Error: {msg2}\n')
|
| 60 |
+
|
| 61 |
+
# Test 3: Fix everything
|
| 62 |
+
config3 = """apiVersion: apps/v1
|
| 63 |
+
kind: Deployment
|
| 64 |
+
metadata:
|
| 65 |
+
name: my-app
|
| 66 |
+
spec:
|
| 67 |
+
replicas: 3
|
| 68 |
+
selector:
|
| 69 |
+
matchLabels:
|
| 70 |
+
app: my-app
|
| 71 |
+
template:
|
| 72 |
+
metadata:
|
| 73 |
+
labels:
|
| 74 |
+
app: my-app
|
| 75 |
+
spec:
|
| 76 |
+
containers:
|
| 77 |
+
- name: my-container
|
| 78 |
+
image: nginx
|
| 79 |
+
ports:
|
| 80 |
+
- containerPort: 80
|
| 81 |
+
resources:
|
| 82 |
+
limits:
|
| 83 |
+
cpu: "500m"
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
reward3, msg3, fixed3 = grade_task5(config3)
|
| 87 |
+
print(f'Step 3 (all fixed): Reward={reward3}, Fixed={fixed3}')
|
| 88 |
+
print(f' Message: {msg3}')
|
test_nginx_multistep.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from server.graders.nginx_grader import grade_task7
|
| 2 |
+
|
| 3 |
+
# Test 1: Only fix syntax (listen and error_log semicolons)
|
| 4 |
+
config1 = """events {}
|
| 5 |
+
|
| 6 |
+
http {
|
| 7 |
+
server {
|
| 8 |
+
listen 80;
|
| 9 |
+
|
| 10 |
+
location / {
|
| 11 |
+
proxy_pass localhost:3000
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
location /api {
|
| 15 |
+
proxy_pass http://localhost:5000
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
error_log logs/error.log;
|
| 19 |
+
}
|
| 20 |
+
}"""
|
| 21 |
+
|
| 22 |
+
reward1, msg1, fixed1 = grade_task7(config1)
|
| 23 |
+
print(f'Step 1 (syntax fixed): Reward={reward1}, Fixed={fixed1}')
|
| 24 |
+
print(f' Error: {msg1}\n')
|
| 25 |
+
|
| 26 |
+
# Test 2: Fix syntax + proxy_pass (add http://)
|
| 27 |
+
config2 = """events {}
|
| 28 |
+
|
| 29 |
+
http {
|
| 30 |
+
server {
|
| 31 |
+
listen 80;
|
| 32 |
+
|
| 33 |
+
location / {
|
| 34 |
+
proxy_pass http://localhost:3000;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
location /api {
|
| 38 |
+
proxy_pass http://localhost:5000
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
error_log logs/error.log;
|
| 42 |
+
}
|
| 43 |
+
}"""
|
| 44 |
+
|
| 45 |
+
reward2, msg2, fixed2 = grade_task7(config2)
|
| 46 |
+
print(f'Step 2 (syntax + proxy_pass fixed): Reward={reward2}, Fixed={fixed2}')
|
| 47 |
+
print(f' Error: {msg2}\n')
|
| 48 |
+
|
| 49 |
+
# Test 3: Fix everything (add /api/ and headers)
|
| 50 |
+
config3 = """events {}
|
| 51 |
+
|
| 52 |
+
http {
|
| 53 |
+
server {
|
| 54 |
+
listen 80;
|
| 55 |
+
|
| 56 |
+
location / {
|
| 57 |
+
proxy_pass http://localhost:3000;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
location /api/ {
|
| 61 |
+
proxy_pass http://localhost:5000;
|
| 62 |
+
proxy_set_header Host $host;
|
| 63 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
error_log logs/error.log;
|
| 67 |
+
}
|
| 68 |
+
}"""
|
| 69 |
+
|
| 70 |
+
reward3, msg3, fixed3 = grade_task7(config3)
|
| 71 |
+
print(f'Step 3 (all fixed): Reward={reward3}, Fixed={fixed3}')
|
| 72 |
+
print(f' Message: {msg3}')
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|