Team Ai
Apppublic

jester1177/cloud-native-debug-env

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
environment.py467 linesDownload Raw Back to server
1"""Core environment logic."""2 3import copy4import random5from typing import Any, Dict, List, Optional, Tuple6 7from server.models import (8    Action,9    ActionType,10    ErrorInfo,11    ErrorPhase,12    FileContent,13    FileType,14    Observation,15    TaskDifficulty,16)17from server.simulators.docker_simulator import DockerSimulator18from server.simulators.k8s_simulator import KubernetesSimulator19from server.simulators.workflow_simulator import WorkflowSimulator20from server.tasks.task_registry import TASK_REGISTRY, get_task21 22 23class CICDDebugEnvironment:24    MAX_STEPS = 1025    MAX_HINTS = 326 27    def _resolve_task_id(self, task_id: Optional[object]) -> Optional[str]:28        """Resolve task identifiers from either canonical string IDs or numeric indices.29 30        Supports:31        - canonical ids (e.g. "dockerfile_syntax")32        - numeric values (1-based preferred, 0-based tolerated)33        - numeric strings ("1", "0")34        """35        if task_id is None:36            return None37 38        keys = list(TASK_REGISTRY.keys())39 40        if isinstance(task_id, int):41            if 1 <= task_id <= len(keys):42                return keys[task_id - 1]43            if 0 <= task_id < len(keys):44                return keys[task_id]45            return str(task_id)46 47        if isinstance(task_id, str):48            raw = task_id.strip()49            if raw in TASK_REGISTRY:50                return raw51            if raw.isdigit():52                numeric = int(raw)53                if 1 <= numeric <= len(keys):54                    return keys[numeric - 1]55                if 0 <= numeric < len(keys):56                    return keys[numeric]57                return raw58            lowered = raw.lower()59            for key in keys:60                if key.lower() == lowered:61                    return key62            return raw63 64        return str(task_id)65 66    def _find_workflow_file(self) -> Optional[FileContent]:67        """Return the first workflow file found in current_files."""68        for path, fc in self.current_files.items():69            if path.startswith(".github/workflows/") and path.endswith(".yml"):70                return fc71        return None72 73    def _validation_snapshot(self) -> Dict[str, bool]:74        docker_result = self.docker_sim.validate(self.current_files.get("Dockerfile"), self.current_files)75        workflow_file = self._find_workflow_file()76        workflow_result = self.workflow_sim.validate(workflow_file, self.current_files)77        k8s_result = self.k8s_sim.validate(self.current_files)78        return {79            "docker_build_valid": bool(docker_result.get("build_success", False)),80            "workflow_parse_valid": bool(workflow_result.get("parse_success", False)),81            "k8s_valid": bool(k8s_result.get("valid", True)),82        }83 84    def __init__(self):85        self.docker_sim = DockerSimulator()86        self.workflow_sim = WorkflowSimulator()87        self.k8s_sim = KubernetesSimulator()88 89        self.current_task_id: Optional[str] = None90        self.current_scenario_id: Optional[str] = None91        self.current_difficulty: Optional[TaskDifficulty] = None92        self.current_task = None93 94        self.original_files: Dict[str, FileContent] = {}95        self.current_files: Dict[str, FileContent] = {}96        self.expected_fixes: List[Dict[str, Any]] = []97 98        self.current_error: Optional[ErrorInfo] = None99        self.issues_total = 0100        self.issues_fixed = 0101 102        self.step_count = 0103        self.episode_reward = 0.0104        self.done = False105        self.hints_used = 0106 107        self.trajectory: List[Dict[str, Any]] = []108        self.last_action_success: Optional[bool] = None109        self.last_action_feedback: Optional[str] = None110 111    def reset(self, task_id: Optional[str] = None, scenario_id: Optional[str] = None, seed: Optional[int] = None):112        if seed is not None:113            random.seed(seed)114 115        task_id = self._resolve_task_id(task_id)116 117        if task_id is None:118            task_id = random.choice(list(TASK_REGISTRY.keys()))119        if task_id not in TASK_REGISTRY:120            raise ValueError(f"Unknown task: {task_id}")121 122        self.current_task_id = task_id123        self.current_task = get_task(task_id)124        self.current_difficulty = self.current_task.DIFFICULTY125 126        scenario = self.current_task.load_scenario(scenario_id)127        self.current_scenario_id = scenario["id"]128 129        self.original_files = {130            f["path"]: FileContent(131                path=f["path"],132                content=f["content"],133                file_type=FileType(f["type"]),134                line_count=f["content"].count("\n") + 1,135            )136            for f in scenario["files"]137        }138        self.current_files = copy.deepcopy(self.original_files)139 140        self.current_error = ErrorInfo(141            phase=ErrorPhase(scenario["error"]["phase"]),142            error_message=scenario["error"]["message"],143            exit_code=scenario["error"].get("exit_code"),144            failed_step=scenario["error"].get("failed_step"),145            line_hint=scenario["error"].get("line_hint"),146        )147 148        self.expected_fixes = scenario["expected_fixes"]149        self.issues_total = len(self.expected_fixes)150        self.issues_fixed = 0151 152        self.step_count = 0153        self.episode_reward = 0.0154        self.done = False155        self.hints_used = 0156        self.trajectory = []157        self.last_action_success = None158        self.last_action_feedback = None159 160        return self.get_observation()161 162    def step(self, action: Action) -> Tuple[Observation, float, bool, Dict[str, Any]]:163        if self.done:164            return self.get_observation(), 0.0, True, {"error": "Episode already done"}165 166        self.step_count += 1167 168        if action.action_type == ActionType.REQUEST_HINT:169            reward, feedback = self._handle_hint_request()170        elif action.action_type == ActionType.SUBMIT:171            reward, feedback = self._handle_submit()172        else:173            reward, feedback = self._handle_edit(action)174 175        self.last_action_feedback = feedback176        self.episode_reward += reward177 178        info: Dict[str, Any] = {}179        if self.step_count >= self.MAX_STEPS:180            self.done = True181            info["termination_reason"] = "max_steps"182        elif action.action_type == ActionType.SUBMIT:183            self.done = True184            info["termination_reason"] = "submitted"185        elif self.issues_fixed == self.issues_total:186            self.done = True187            info["termination_reason"] = "all_fixed"188 189        info["issues_fixed"] = self.issues_fixed190        info["issues_total"] = self.issues_total191 192        self.trajectory.append(193            {"step": self.step_count, "action": action.model_dump(), "reward": reward, "done": self.done, "info": info}194        )195 196        return self.get_observation(), reward, self.done, info197 198    def _handle_edit(self, action: Action) -> Tuple[float, str]:199        if not action.edits:200            self.last_action_success = False201            return 0.0, "No edits provided"202 203        before_validation = self._validation_snapshot()204 205        reward = 0.0206        feedbacks: List[str] = []207        applied_count = 0208        for edit in action.edits:209            if edit.file_path not in self.current_files:210                # Allow creating new files (needed for K8s ConfigMap scenarios etc.)211                if action.action_type == ActionType.EDIT_FILE and edit.new_content:212                    ft = FileType.OTHER213                    if edit.file_path.startswith("k8s/") or edit.file_path.endswith(".yaml") or edit.file_path.endswith(".yml"):214                        ft = FileType.KUBERNETES215                    self.current_files[edit.file_path] = FileContent(216                        path=edit.file_path,217                        content=edit.new_content,218                        file_type=ft,219                        line_count=edit.new_content.count("\n") + 1,220                    )221                    feedbacks.append(f"Created new file: {edit.file_path}")222                    applied_count += 1223                    continue224                feedbacks.append(f"File not found: {edit.file_path}")225                continue226 227            file_content = self.current_files[edit.file_path]228            lines = file_content.content.split("\n")229 230            if action.action_type == ActionType.REPLACE_LINE:231                if edit.line_number and 1 <= edit.line_number <= len(lines):232                    lines[edit.line_number - 1] = edit.new_content or ""233                    feedbacks.append(f"Replaced line {edit.line_number} in {edit.file_path}")234                    applied_count += 1235                else:236                    feedbacks.append(f"Invalid line number: {edit.line_number}")237                    continue238            elif action.action_type == ActionType.ADD_LINE:239                insert_at = edit.line_number - 1 if edit.line_number else len(lines)240                insert_at = max(0, min(insert_at, len(lines)))241                lines.insert(insert_at, edit.new_content or "")242                feedbacks.append(f"Added line at {insert_at + 1} in {edit.file_path}")243                applied_count += 1244            elif action.action_type == ActionType.DELETE_LINE:245                if edit.line_number and 1 <= edit.line_number <= len(lines):246                    del lines[edit.line_number - 1]247                    feedbacks.append(f"Deleted line {edit.line_number} in {edit.file_path}")248                    applied_count += 1249                else:250                    feedbacks.append(f"Invalid line number: {edit.line_number}")251                    continue252            elif action.action_type == ActionType.EDIT_FILE:253                if edit.old_content and edit.old_content in file_content.content:254                    new_content = file_content.content.replace(edit.old_content, edit.new_content or "", 1)255                    lines = new_content.split("\n")256                    feedbacks.append(f"Edited content in {edit.file_path}")257                    applied_count += 1258                else:259                    feedbacks.append(f"Content not found in {edit.file_path}")260                    continue261            elif action.action_type == ActionType.ADD_BLOCK:262                insert_at = edit.line_number - 1 if edit.line_number else len(lines)263                insert_at = max(0, min(insert_at, len(lines)))264                block_lines = (edit.new_content or "").split("\n")265                lines[insert_at:insert_at] = block_lines266                feedbacks.append(f"Added block at {insert_at + 1} in {edit.file_path}")267                applied_count += 1268            elif action.action_type == ActionType.DELETE_BLOCK:269                if edit.old_content and edit.old_content in file_content.content:270                    new_content = file_content.content.replace(edit.old_content, "", 1)271                    lines = new_content.split("\n")272                    feedbacks.append(f"Deleted block in {edit.file_path}")273                    applied_count += 1274                else:275                    feedbacks.append(f"Block content not found in {edit.file_path}")276                    continue277            else:278                feedbacks.append(f"Unsupported action type for now: {action.action_type.value}")279                continue280 281            new_content = "\n".join(lines)282            self.current_files[edit.file_path] = FileContent(283                path=file_content.path,284                content=new_content,285                file_type=file_content.file_type,286                line_count=len(lines),287            )288 289        reward += self._check_fix_progress()290 291        after_validation = self._validation_snapshot()292        if not before_validation["docker_build_valid"] and after_validation["docker_build_valid"]:293            reward += 0.1294            feedbacks.append("Docker build validity improved")295        if not before_validation["workflow_parse_valid"] and after_validation["workflow_parse_valid"]:296            reward += 0.1297            feedbacks.append("Workflow parse validity improved")298        if not before_validation["k8s_valid"] and after_validation["k8s_valid"]:299            reward += 0.1300            feedbacks.append("Kubernetes manifest validity improved")301 302        if applied_count == 0:303            self.last_action_success = False304            return max(-0.02, reward - 0.02), "; ".join(feedbacks) or "No edit applied"305 306        self.last_action_success = True307        return max(0.0, reward), "; ".join(feedbacks)308 309    def _check_fix_progress(self) -> float:310        fixes_applied = 0311        for fix in self.expected_fixes:312            file_path = fix["file"]313            if file_path not in self.current_files:314                # For "contains" checks on missing files, the fix is not applied315                # For "not_contains" checks on missing files, consider it fixed316                if fix["type"] == "not_contains":317                    fixes_applied += 1318                continue319            current_content = self.current_files[file_path].content320            if fix["type"] == "contains" and fix["expected"] in current_content:321                fixes_applied += 1322            if fix["type"] == "not_contains" and fix["expected"] not in current_content:323                fixes_applied += 1324            if fix["type"] == "line_equals":325                lines = current_content.split("\n")326                line_num = int(fix.get("line", 0))327                if 1 <= line_num <= len(lines):328                    if lines[line_num - 1].strip() == str(fix["expected"]).strip():329                        fixes_applied += 1330 331        new_fixed = fixes_applied - self.issues_fixed332        if new_fixed > 0:333            self.issues_fixed = fixes_applied334            return 0.3 * new_fixed335        return 0.0336 337    def _handle_submit(self) -> Tuple[float, str]:338        docker_result = self.docker_sim.validate(self.current_files.get("Dockerfile"), self.current_files)339        workflow_file = self._find_workflow_file()340        workflow_result = self.workflow_sim.validate(workflow_file, self.current_files)341        k8s_result = self.k8s_sim.validate(self.current_files)342 343        has_k8s = any(fc.file_type == FileType.KUBERNETES for fc in self.current_files.values())344        has_docker = "Dockerfile" in self.current_files345        has_workflow = workflow_file is not None346 347        reward = 0.0348        parts: List[str] = []349 350        # Determine weight distribution based on what file types are present351        if has_docker and has_workflow and has_k8s:352            # Full stack: Docker 20%, Workflow 30%, K8s 30%, fix progress 20%353            docker_w, wf_w, k8s_w = 0.20, 0.30, 0.30354        elif has_docker and has_workflow:355            docker_w, wf_w, k8s_w = 0.50, 0.50, 0.0356        elif has_docker and has_k8s:357            docker_w, wf_w, k8s_w = 0.40, 0.0, 0.40358        elif has_workflow and has_k8s:359            docker_w, wf_w, k8s_w = 0.0, 0.40, 0.40360        elif has_k8s:361            docker_w, wf_w, k8s_w = 0.0, 0.0, 0.80362        elif has_docker:363            docker_w, wf_w, k8s_w = 0.50, 0.0, 0.0364        else:365            docker_w, wf_w, k8s_w = 0.0, 0.50, 0.0366 367        # Docker validation368        if has_docker:369            if docker_result.get("build_success"):370                reward += docker_w * 0.6371                parts.append("Docker build: PASS")372            else:373                parts.append(f"Docker build: FAIL - {docker_result.get('error', 'unknown')}")374 375            if docker_result.get("run_success"):376                reward += docker_w * 0.4377                parts.append("Docker run: PASS")378            else:379                parts.append(f"Docker run: FAIL - {docker_result.get('run_error', 'unknown')}")380 381        # Workflow validation382        if has_workflow:383            if workflow_result["parse_success"]:384                reward += wf_w * 0.4385                parts.append("Workflow parse: PASS")386            else:387                parts.append(f"Workflow parse: FAIL - {workflow_result.get('error', 'unknown')}")388 389            if workflow_result["execution_success"]:390                reward += wf_w * 0.6391                parts.append("Workflow execution: PASS")392            else:393                parts.append(f"Workflow execution: FAIL - {workflow_result.get('exec_error', 'unknown')}")394 395        # Kubernetes validation396        if has_k8s:397            if k8s_result["valid"]:398                reward += k8s_w * 0.4399                parts.append("K8s manifests: VALID")400            else:401                k8s_errors = k8s_result.get("errors", [])402                parts.append(f"K8s manifests: INVALID - {'; '.join(k8s_errors[:2])}")403 404            pod_status = k8s_result.get("pod_status", "N/A")405            if pod_status == "Running":406                reward += k8s_w * 0.3407                parts.append(f"K8s pod status: {pod_status}")408            else:409                parts.append(f"K8s pod status: {pod_status}")410 411            svc_status = k8s_result.get("service_status", "N/A")412            if "active" in svc_status.lower() or svc_status == "N/A":413                reward += k8s_w * 0.3414                parts.append(f"K8s service: {svc_status}")415            else:416                parts.append(f"K8s service: {svc_status}")417 418        self.last_action_success = reward >= 0.6419        return reward, "; ".join(parts)420 421    def _handle_hint_request(self) -> Tuple[float, str]:422        if self.hints_used >= self.MAX_HINTS:423            self.last_action_success = False424            return 0.0, "No hints remaining"425 426        self.hints_used += 1427        for fix in self.expected_fixes:428            file_path = fix["file"]429            if file_path not in self.current_files:430                continue431            current_content = self.current_files[file_path].content432            is_fixed = False433            if fix["type"] == "contains":434                is_fixed = fix["expected"] in current_content435            elif fix["type"] == "not_contains":436                is_fixed = fix["expected"] not in current_content437            if not is_fixed:438                hint = fix.get("hint", f"Check {file_path}")439                self.last_action_success = True440                return -0.05, f"Hint ({self.hints_used}/{self.MAX_HINTS}): {hint}"441 442        self.last_action_success = True443        return 0.0, "All known issues appear to be fixed"444 445    def get_observation(self) -> Observation:446        return Observation(447            task_id=self.current_task_id or "unknown",448            task_description=self.current_task.DESCRIPTION if self.current_task else "",449            difficulty=self.current_difficulty or TaskDifficulty.EASY,450            files=list(self.current_files.values()),451            error=self.current_error or ErrorInfo(452                phase=ErrorPhase.DOCKER_BUILD,453                error_message="No error",454            ),455            available_secrets=self.current_task.AVAILABLE_SECRETS if self.current_task else [],456            runner_os="ubuntu-latest",457            step_number=self.step_count,458            max_steps=self.MAX_STEPS,459            hints_used=self.hints_used,460            hints_available=self.MAX_HINTS - self.hints_used,461            last_action_success=self.last_action_success,462            last_action_feedback=self.last_action_feedback,463            issues_found=self.issues_fixed,464            issues_fixed=self.issues_fixed,465            total_issues=max(1, self.issues_total),466        )467