CoolFace
Apppublic

vasiuuu/DGX_AI

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
environment.py516 linesDownload Raw Back to codeforge
1from __future__ import annotations2 3import logging4import re5import uuid6from pathlib import Path7from typing import Any8 9from openenv.core.env_server.interfaces import Environment10 11from codeforge.audit.ledger import AuditLedger12from codeforge.grader import compute_reward13from codeforge.grounder import ground14from codeforge.interrogator.interrogator import Interrogator15from codeforge.kb.cluster import build_clusters16from codeforge.kb.indexer import SkillsIndex17from codeforge.models import AuditEntry, CodeForgeAction, CodeForgeActionType, CodeForgeObservation18from codeforge.observation import build_observation19from codeforge.ralph.loop import run_loop20from codeforge.ralph.models import LoopConfig21from codeforge.ralph.synthesizer import StubSynthesizer, Synthesizer22from codeforge.sandbox.sandbox import run_sandbox23from codeforge.shaping import citation_shaping_bonus24from codeforge.tasks import Task, get_task25 26_log = logging.getLogger(__name__)27_DEFAULT_CORPUS = Path(__file__).resolve().parent / "kb" / "skills_corpus.jsonl"28 29# ---------------------------------------------------------------------------30# Filename validation (SYSTEM_DESIGN §14.2, §14.3)31# ---------------------------------------------------------------------------32_FILENAME_RE = re.compile(r"^[a-z][a-z0-9_]*\.py$")33_FORBIDDEN_FILENAMES = frozenset({34    "conftest.py", "pytest.ini", "setup.cfg", "pyproject.toml", "tox.ini",35})36_MAX_FILES = 1037_MAX_FILE_SIZE = 50 * 1024  # 50 KB38_MAX_TOTAL_SIZE = 200 * 1024  # 200 KB39 40 41def _validate_files(files: dict[str, str]) -> str | None:42    """Return an error message if *files* violates submission constraints, else None."""43    if not files:44        return "files dict is empty"45    if len(files) > _MAX_FILES:46        return f"too many files ({len(files)} > {_MAX_FILES})"47    total_size = 048    for name, content in files.items():49        if name in _FORBIDDEN_FILENAMES:50            return f"filename '{name}' is not allowed"51        if not _FILENAME_RE.match(name):52            return f"filename '{name}' must match [a-z][a-z0-9_]*.py"53        size = len(content.encode("utf-8"))54        if size > _MAX_FILE_SIZE:55            return f"file '{name}' exceeds {_MAX_FILE_SIZE} bytes"56        total_size += size57    if total_size > _MAX_TOTAL_SIZE:58        return f"total size ({total_size}) exceeds {_MAX_TOTAL_SIZE} bytes"59    return None60 61 62# ---------------------------------------------------------------------------63# Valid action types (for fast membership check)64# ---------------------------------------------------------------------------65_VALID_ACTION_TYPES = frozenset(member.value for member in CodeForgeActionType)66 67 68# ---------------------------------------------------------------------------69# Environment70# ---------------------------------------------------------------------------71 72 73class CodeForgeEnvironment(Environment):  # type: ignore[type-arg]74    """OpenEnv-compliant RL environment with all 6 CodeForge actions.75 76    Implements SYSTEM_DESIGN §4.9, §5.2, §17.77    """78 79    SUPPORTS_CONCURRENT_SESSIONS = True80 81    def __init__(82        self,83        *,84        corpus_path: Path | None = None,85        synthesizer: Synthesizer | None = None,86    ) -> None:87        super().__init__()88        self._corpus_path = corpus_path or _DEFAULT_CORPUS89        self._synthesizer = synthesizer90        self._index: SkillsIndex | None = None91        self._task: Task | None = None92        self._episode_id: str = ""93        self._budget_remaining: int = 094        self._current_files: dict[str, str] = {}95        self._previous_score: float = 0.096        self._is_done: bool = False97 98        # Per-step state99        self._last_citations: tuple[dict[str, object], ...] = ()100        self._last_grounding: dict[str, object] | None = None101        self._last_reward: float = 0.0102        self._last_cluster_hits: tuple[str, ...] = ()103        self._last_interrogation_questions: tuple[str, ...] = ()104        self._last_ralph_run_id: str | None = None105        self._last_ralph_iterations: tuple[dict[str, object], ...] = ()106 107        # Brier/quality tracking for audit entries108        self._last_brier_penalty: float | None = None109        self._last_quality: float = 0.0110 111        # Episode-level accumulators112        self._all_episode_citations: list[dict[str, object]] = []113        self._all_episode_cluster_hits: list[str] = []114        self._ledger: AuditLedger | None = None115        self._step_index: int = 0116 117    # ------------------------------------------------------------------118    # Index management119    # ------------------------------------------------------------------120 121    def _ensure_index(self) -> SkillsIndex:122        if self._index is None:123            if not self._corpus_path.is_file():124                msg = (125                    f"corpus not found: {self._corpus_path}. "126                    f"Run the skills scraper first."127                )128                raise FileNotFoundError(msg)129            idx = SkillsIndex(corpus_path=self._corpus_path)130            idx.build()131            # Build and attach clusters132            import json133            nodes: list[dict[str, Any]] = []134            with self._corpus_path.open(encoding="utf-8") as f:135                for line in f:136                    line = line.strip()137                    if line:138                        nodes.append(json.loads(line))139            manifest = build_clusters(nodes)140            idx.attach_cluster_manifest(manifest)141            self._index = idx142        return self._index143 144    # ------------------------------------------------------------------145    # OpenEnv interface146    # ------------------------------------------------------------------147 148    def reset(149        self,150        seed: int | None = None,151        episode_id: str | None = None,152        **kwargs: Any,153    ) -> CodeForgeObservation:154        task_level: str = kwargs.get("task_level", "easy")155        task = get_task(task_level)156        self._task = task157        self._episode_id = episode_id or uuid.uuid4().hex[:12]158        self._budget_remaining = task.max_budget159        self._current_files = dict(task.initial_files)160        self._previous_score = 0.0161        self._is_done = False162 163        # Reset per-step164        self._last_citations = ()165        self._last_grounding = None166        self._last_reward = 0.0167        self._last_cluster_hits = ()168        self._last_interrogation_questions = ()169        self._last_ralph_run_id = None170        self._last_ralph_iterations = ()171 172        # Reset episode accumulators173        self._all_episode_citations = []174        self._all_episode_cluster_hits = []175        self._ledger = AuditLedger()176        self._step_index = 0177 178        _log.info(179            "reset id=%s task=%s budget=%s",180            self._episode_id, task.task_id, task.max_budget,181        )182        return self._build_obs()183 184    def step(185        self,186        action: CodeForgeAction,187        timeout_s: float | None = None,188        **kwargs: Any,189    ) -> CodeForgeObservation:190        # --- Pre-check: no active episode --------------------------------191        if self._task is None:192            return self._error_obs("No active episode — call reset() first")193 194        # --- Pre-check: episode already done -----------------------------195        if self._is_done:196            return self._build_obs()197 198        # --- Pre-check: valid action_type --------------------------------199        action_type_str = str(action.action_type)200        if action_type_str not in _VALID_ACTION_TYPES:201            return self._error_obs(f"Unknown action_type: {action_type_str!r}")202 203        # --- Budget check (variable cost) --------------------------------204        cost = self._action_cost(action)205        if cost > self._budget_remaining:206            return self._error_obs(207                f"Insufficient budget: need {cost}, have {self._budget_remaining}"208            )209        self._budget_remaining -= cost210 211        # --- Clear per-step state ----------------------------------------212        self._last_reward = 0.0213        self._last_citations = ()214        self._last_grounding = None215        self._last_cluster_hits = ()216        self._last_interrogation_questions = ()217        self._last_ralph_run_id = None218        self._last_ralph_iterations = ()219        error: str | None = None220 221        # --- Route to handler --------------------------------------------222        try:223            if action_type_str == CodeForgeActionType.QUERY_KB:224                error = self._handle_query_kb(action)225            elif action_type_str == CodeForgeActionType.QUERY_CLUSTER:226                error = self._handle_query_cluster(action)227            elif action_type_str == CodeForgeActionType.INTERROGATE:228                error = self._handle_interrogate(action)229            elif action_type_str == CodeForgeActionType.SUBMIT:230                error = self._handle_submit(action)231            elif action_type_str == CodeForgeActionType.RUN_RALPH:232                error = self._handle_run_ralph(action)233            elif action_type_str == CodeForgeActionType.GET_AUDIT:234                error = self._handle_get_audit(action)235        except Exception as exc:236            _log.exception("handler error: %s", exc)237            error = f"Internal error: {exc}"238 239        # --- Append audit entry ------------------------------------------240        assert self._ledger is not None241        _cited: list[str] = []242        _cite: dict[str, object]243        for _cite in self._last_citations:244            _cited.append(str(_cite.get("node_id", "")))245        cited_ids: tuple[str, ...] = tuple(_cited)246        self._ledger.append(247            AuditEntry(248                step_index=self._step_index,249                action_type=action_type_str,250                cited_skill_ids=cited_ids,251                cited_clusters=self._last_cluster_hits,252                grounding_report=(253                    self._last_grounding if self._last_grounding else None254                ),255                reward=self._last_reward,256                brier_penalty=(257                    self._last_brier_penalty258                    if action_type_str == CodeForgeActionType.SUBMIT259                    else None260                ),261                confidence_declared=(262                    action.confidence263                    if action_type_str == CodeForgeActionType.SUBMIT264                    else None265                ),266                quality=(267                    self._last_quality268                    if action_type_str == CodeForgeActionType.SUBMIT269                    else self._previous_score270                ),271            ),272        )273        self._step_index += 1274 275        # --- Check budget exhaustion -------------------------------------276        if self._budget_remaining <= 0:277            self._is_done = True278 279        return self._build_obs(error=error)280 281    @property282    def state(self) -> CodeForgeObservation:283        if self._task is None:284            return self._error_obs("No active episode — call reset() first")285        return self._build_obs()286 287    # ------------------------------------------------------------------288    # Cost computation289    # ------------------------------------------------------------------290 291    @staticmethod292    def _action_cost(action: CodeForgeAction) -> int:293        """Variable-cost budget accounting (SYSTEM_DESIGN §17.2)."""294        if str(action.action_type) == CodeForgeActionType.GET_AUDIT:295            return 0296        if str(action.action_type) == CodeForgeActionType.RUN_RALPH:297            return action.max_iters298        return 1299 300    # ------------------------------------------------------------------301    # Action handlers (each returns an error string or None)302    # ------------------------------------------------------------------303 304    def _handle_query_kb(self, action: CodeForgeAction) -> str | None:305        try:306            idx = self._ensure_index()307        except FileNotFoundError as e:308            _log.warning("query_kb: no corpus: %s", e)309            self._last_citations = ()310            return None311        tags = set(action.required_tags) if action.required_tags else None312        results = idx.search(313            action.claim or "", top_k=action.top_k, required_tags=tags,314        )315        self._last_citations = tuple(316            {317                "node_id": r.node_id,318                "skill_name": r.skill_name,319                "section_path": list(r.section_path),320                "section_body": r.section_body,321                "score": r.score,322                "rank": r.rank,323            }324            for r in results325        )326        self._all_episode_citations.extend(self._last_citations)327        return None328 329    def _handle_query_cluster(self, action: CodeForgeAction) -> str | None:330        try:331            idx = self._ensure_index()332        except FileNotFoundError as e:333            _log.warning("query_cluster: no corpus: %s", e)334            self._last_cluster_hits = ()335            return None336        label = action.cluster_label or ""337        results = idx.nodes_in_cluster(label)338        if not results:339            self._last_cluster_hits = ()340            return None341        self._last_cluster_hits = tuple(r.node_id for r in results)342        self._all_episode_cluster_hits.extend(self._last_cluster_hits)343        return None344 345    def _handle_interrogate(self, action: CodeForgeAction) -> str | None:346        idx: SkillsIndex | None347        try:348            idx = self._ensure_index()349        except FileNotFoundError:350            idx = None351        interrogator = Interrogator(idx)352        assert self._task is not None353        result = interrogator.generate(self._task.brief)354        self._last_interrogation_questions = result.questions355        return None356 357    def _handle_submit(self, action: CodeForgeAction) -> str | None:358        if action.files is None:359            return "files required for submit"360        file_err = _validate_files(action.files)361        if file_err is not None:362            return file_err363 364        self._current_files = dict(action.files)365        assert self._task is not None366 367        # Merge hidden correctness tests into sandbox files (agent cannot see these)368        sandbox_files = dict(action.files)369        if self._task.hidden_tests:370            sandbox_files.update(self._task.hidden_tests)371 372        # Run sandbox373        try:374            sandbox_result = run_sandbox(375                files=sandbox_files,376                tools=self._task.tools,377                timeout_per_tool=30.0,378            )379            sandbox_score = sandbox_result.composite_score380        except Exception as e:381            _log.exception("sandbox error: %s", e)382            sandbox_score = 0.0383 384        # Run grounder (pass local module names so they're not penalized)385        local_modules = frozenset(386            f.removesuffix(".py") for f in action.files if f.endswith(".py")387        )388        concatenated = "\n".join(action.files.values())389        grounding_report = ground(concatenated, local_modules=local_modules)390        self._last_grounding = grounding_report.model_dump()391 392        # Compute reward with Brier calibration393        quality = 0.6 * sandbox_score + 0.4 * grounding_report.groundedness394        effective_conf = action.confidence if action.confidence is not None else 0.5395        brier_penalty: float | None = min((effective_conf - quality) ** 2, 0.5)396        self._last_brier_penalty = brier_penalty397        self._last_quality = quality398 399        reward = compute_reward(400            sandbox_score=sandbox_score,401            groundedness=grounding_report.groundedness,402            confidence=action.confidence,403        )404 405        # Apply citation shaping bonus only on successful submits (§4.8.4)406        if reward > 0:407            shaping = citation_shaping_bonus(408                submit_files=action.files,409                prior_citations=self._all_episode_citations,410                prior_cluster_hits=self._all_episode_cluster_hits,411            )412            reward = round(min(1.0, reward + shaping), 3)413 414        self._last_reward = reward415        self._previous_score = reward416 417        # Check target score418        if reward >= self._task.target_score:419            self._is_done = True420 421        return None422 423    def _handle_run_ralph(self, action: CodeForgeAction) -> str | None:424        assert self._task is not None425        try:426            idx = self._ensure_index()427        except FileNotFoundError as e:428            return f"corpus not available: {e}"429 430        config = LoopConfig(431            max_iters=action.max_iters,432            target_score=self._task.target_score,433            tools=self._task.tools,434        )435        synthesizer = self._synthesizer or StubSynthesizer()436        result = run_loop(437            spec=self._task.brief,438            initial_files=self._current_files,439            index=idx,440            synthesizer=synthesizer,441            config=config,442        )443 444        self._last_ralph_run_id = result.run_id445        self._last_ralph_iterations = tuple(446            it.model_dump() for it in result.iterations447        )448        self._current_files = dict(result.final_files)449 450        # Compute ralph reward (SYSTEM_DESIGN §4.8.5)451        concatenated = "\n".join(result.final_files.values())452        grounding_report = ground(concatenated)453        self._last_grounding = grounding_report.model_dump()454 455        wasted = sum(456            1 for it in result.iterations if it.reason in ("score_regressed", "score_plateau")457        )458        base = compute_reward(459            sandbox_score=result.final_score,460            groundedness=grounding_report.groundedness,461            confidence=0.75,462        )463        waste_penalty = wasted * 0.05464        ralph_reward = round(max(0.0, min(1.0, base - waste_penalty)), 3)465 466        self._last_reward = ralph_reward467        self._previous_score = ralph_reward468        return None469 470    def _handle_get_audit(self, action: CodeForgeAction) -> str | None:471        # Audit data is populated in _build_obs via cumulative_audit_summary472        return None473 474    # ------------------------------------------------------------------475    # Observation helpers476    # ------------------------------------------------------------------477 478    def _build_obs(self, *, error: str | None = None) -> CodeForgeObservation:479        assert self._task is not None480        audit_summary: dict[str, object] | None = None481        if self._ledger is not None:482            audit_summary = self._ledger.serialize()483        return build_observation(484            episode_id=self._episode_id,485            task=self._task,486            current_files=self._current_files,487            budget_remaining=self._budget_remaining,488            previous_score=self._previous_score,489            last_citations=self._last_citations,490            last_grounding=self._last_grounding,491            is_done=self._is_done,492            last_reward=self._last_reward,493            last_cluster_hits=self._last_cluster_hits,494            last_interrogation_questions=self._last_interrogation_questions,495            last_ralph_run_id=self._last_ralph_run_id,496            last_ralph_iterations=self._last_ralph_iterations,497            cumulative_audit_summary=audit_summary,498            error=error,499        )500 501    def _error_obs(self, msg: str) -> CodeForgeObservation:502        """Return an error observation without modifying episode state."""503        if self._task is None:504            # No task set — use a dummy task for the observation structure505            dummy = get_task("easy")506            return build_observation(507                episode_id=self._episode_id or "none",508                task=dummy,509                current_files=self._current_files,510                budget_remaining=self._budget_remaining,511                previous_score=self._previous_score,512                is_done=self._is_done,513                error=msg,514            )515        return self._build_obs(error=msg)516