"""Single-step OpenEnv environment backed by reasoning-core scorers.""" from __future__ import annotations import json import os import re from itertools import islice from typing import Any from uuid import uuid4 from easydict import EasyDict as edict from openenv.core.env_server.interfaces import Environment from openenv.core.env_server.types import State from reasoning_core import list_tasks, score_answer try: from ..models import ReasoningCoreAction, ReasoningCoreObservation except ImportError: from models import ReasoningCoreAction, ReasoningCoreObservation DEFAULT_DATASET = os.getenv( "RC_HF_DATASET", "reasoning-core/formal-reasoning-env", ) DEFAULT_SIZE = int(os.getenv("RC_DATASET_SIZE", "1000")) DEFAULT_SEED = int(os.getenv("RC_SEED", "42")) AVAILABLE_TASKS = frozenset(list_tasks()) XML_ANSWER_PATTERN = re.compile( r"(.*?)", flags=re.IGNORECASE | re.DOTALL, ) def _metadata_dict(value: Any) -> dict[str, Any]: if isinstance(value, dict): return value if isinstance(value, str) and value.strip(): try: parsed = json.loads(value) except json.JSONDecodeError: return {"raw_metadata": value} return parsed if isinstance(parsed, dict) else {} return {} def _task_name(entry: dict[str, Any]) -> str | None: metadata = _metadata_dict(entry.get("metadata")) value = metadata.get("_task") or entry.get("task") or metadata.get("task") return str(value) if value is not None else None def _normalize_entry(entry: dict[str, Any], index: int) -> dict[str, Any] | None: task_name = _task_name(entry) if task_name not in AVAILABLE_TASKS: return None return { "id": str(entry.get("id", index)), "prompt": str(entry["prompt"]), "answer": str(entry["answer"]), "metadata": {"task": task_name, **_metadata_dict(entry.get("metadata"))}, } def _load_hub_entries( dataset_name: str, split: str, seed: int, size: int, ) -> list[dict[str, Any]]: from datasets import get_dataset_split_names, load_dataset split_names = get_dataset_split_names(dataset_name) source_split = split if source_split not in split_names: source_split = next( (name for name in ("test", "validation", "eval", "dev") if name in split_names), "train", ) stream = load_dataset(dataset_name, split=source_split, streaming=True) stream = stream.shuffle(seed=seed, buffer_size=max(size * 4, 1000)) entries: list[dict[str, Any]] = [] for index, row in enumerate(islice(stream, size * 4)): normalized = _normalize_entry(dict(row), index) if normalized is not None: entries.append(normalized) if len(entries) >= size: break if not entries: raise RuntimeError(f"No supported tasks found in {dataset_name}:{source_split}") return entries def _extract_answer(answer: str) -> str: match = XML_ANSWER_PATTERN.search(answer) return match.group(1).strip() if match else answer.strip() class ReasoningCoreEnvironment(Environment): """Formally scored symbolic reasoning tasks from reasoning-core.""" SUPPORTS_CONCURRENT_SESSIONS: bool = True def __init__(self): self._state = State(episode_id=str(uuid4()), step_count=0) self._entries: list[dict[str, Any]] = [] self._entry_index = 0 self._current_entry: dict[str, Any] | None = None self._configuration: tuple[str, str, int, int] | None = None def _configure( self, dataset_name: str, split: str, seed: int, size: int, ) -> None: configuration = (dataset_name, split, seed, size) if configuration == self._configuration: return self._entries = _load_hub_entries(dataset_name, split, seed, size) self._configuration = configuration self._entry_index = 0 def reset( self, dataset_name: str = DEFAULT_DATASET, split: str = "train", seed: int = DEFAULT_SEED, size: int = DEFAULT_SIZE, episode_id: str | None = None, ) -> ReasoningCoreObservation: if size <= 0: raise ValueError("size must be positive") self._configure(dataset_name, split, seed, size) self._current_entry = self._entries[self._entry_index % len(self._entries)] self._entry_index += 1 self._state = State( episode_id=episode_id or str(uuid4()), step_count=0, ) return ReasoningCoreObservation( prompt=self._current_entry["prompt"], score=None, correct_answer=None, task_name=_task_name(self._current_entry), dataset_metadata=None, done=False, reward=0.0, ) def step(self, action: ReasoningCoreAction) -> ReasoningCoreObservation: self._state.step_count += 1 if self._current_entry is None: raise RuntimeError("Call reset() before step().") entry = edict( answer=self._current_entry["answer"], metadata=self._current_entry["metadata"], ) score = float(score_answer(_extract_answer(action.answer), entry)) return ReasoningCoreObservation( prompt=None, score=score, correct_answer=self._current_entry["answer"], task_name=_task_name(self._current_entry), dataset_metadata=self._current_entry["metadata"], done=True, reward=score, ) @property def state(self) -> State: return self._state