sileod commited on
Commit
5a10f7f
·
verified ·
1 Parent(s): a0a20b5

Upload folder using huggingface_hub

Browse files
Files changed (11) hide show
  1. Dockerfile +22 -0
  2. README.md +80 -5
  3. __init__.py +10 -0
  4. client.py +45 -0
  5. models.py +28 -0
  6. openenv.yaml +11 -0
  7. pyproject.toml +26 -0
  8. server/__init__.py +5 -0
  9. server/app.py +35 -0
  10. server/reasoning_core_environment.py +210 -0
  11. uv.lock +0 -0
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ARG BASE_IMAGE=ghcr.io/huggingface/openenv-base:latest
2
+ FROM ${BASE_IMAGE} AS builder
3
+
4
+ WORKDIR /app/env
5
+ COPY . /app/env
6
+
7
+ RUN if ! command -v uv >/dev/null 2>&1; then \
8
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
9
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
10
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
11
+ fi
12
+ RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-editable
13
+
14
+ FROM ${BASE_IMAGE}
15
+ WORKDIR /app/env
16
+ COPY --from=builder /app/env /app/env
17
+ ENV PATH="/app/env/.venv/bin:$PATH"
18
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
19
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
20
+ CMD curl -f http://localhost:8000/health || exit 1
21
+ ENV ENABLE_WEB_INTERFACE=true
22
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,10 +1,85 @@
1
  ---
2
- title: Reasoning Core Openenv
3
- emoji: 🏃
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: docker
7
  pinned: false
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Reasoning Core Environment Server
3
+ emoji: 🧠
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
+ - agent-environment
13
+ - reasoning
14
+ - reinforcement-learning
15
+ - evaluation
16
+ - symbolic-reasoning
17
  ---
18
 
19
+ # Reasoning Core Environment
20
+
21
+ An [OpenEnv](https://github.com/huggingface/openenv) environment for formally
22
+ verifiable symbolic reasoning across logic, mathematics, planning, syntax, and
23
+ related procedural domains.
24
+
25
+ Tasks come from
26
+ [`reasoning-core/formal-reasoning-env`](https://huggingface.co/datasets/reasoning-core/formal-reasoning-env)
27
+ and are scored by the task-specific evaluators in
28
+ [`reasoning-core`](https://github.com/sileod/reasoning_core).
29
+
30
+ ## Use The Hosted Environment
31
+
32
+ ```python
33
+ from reasoning_core_env import ReasoningCoreAction, ReasoningCoreEnv
34
+
35
+ with ReasoningCoreEnv(
36
+ base_url="https://sileod-reasoning-core-openenv.hf.space"
37
+ ) as env:
38
+ result = env.reset(split="train", seed=42, size=1000)
39
+ print(result.observation.prompt)
40
+
41
+ result = env.step(ReasoningCoreAction(answer="<answer>...</answer>"))
42
+ print(result.reward)
43
+ ```
44
+
45
+ Each episode has one action:
46
+
47
+ 1. `reset()` returns a symbolic reasoning prompt.
48
+ 2. `step(ReasoningCoreAction(answer=...))` scores the answer and ends the episode.
49
+
50
+ Plain answers and answers wrapped in `<answer>...</answer>` are accepted. Rewards
51
+ are task-specific scores in the range 0 to 1.
52
+
53
+ Set `RC_DISABLE_HF_DATASET=1` to skip Hub loading and generate tasks
54
+ procedurally at runtime.
55
+
56
+ ## Local Development
57
+
58
+ ```bash
59
+ uv sync
60
+ uv run openenv validate
61
+ uv run openenv build -t reasoning-core-openenv
62
+ ```
63
+
64
+ Run without Docker:
65
+
66
+ ```bash
67
+ uv run server
68
+ ```
69
+
70
+ The service exposes the interactive UI at `/web`, API documentation at `/docs`,
71
+ health information at `/health`, and the persistent environment API at `/ws`.
72
+
73
+ ## Citation
74
+
75
+ If you use this environment, cite the Reasoning Core paper:
76
+
77
+ ```bibtex
78
+ @article{reasoningcore2026,
79
+ title={Reasoning Core: A Scalable Procedural Data Generation Suite for Symbolic Pre-training and Post-Training},
80
+ author={Lacombe, Valentin and Quesnel, Valentin and Sileo, Damien},
81
+ journal={arXiv preprint arXiv:2603.02208},
82
+ year={2026},
83
+ url={https://arxiv.org/abs/2603.02208}
84
+ }
85
+ ```
__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reasoning Core environment for OpenEnv."""
2
+
3
+ from .client import ReasoningCoreEnv
4
+ from .models import ReasoningCoreAction, ReasoningCoreObservation
5
+
6
+ __all__ = [
7
+ "ReasoningCoreAction",
8
+ "ReasoningCoreEnv",
9
+ "ReasoningCoreObservation",
10
+ ]
client.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Client for the Reasoning Core OpenEnv server."""
2
+
3
+ from typing import Any
4
+
5
+ from openenv.core import EnvClient
6
+ from openenv.core.client_types import StepResult
7
+ from openenv.core.env_server.types import State
8
+
9
+ from .models import ReasoningCoreAction, ReasoningCoreObservation
10
+
11
+
12
+ class ReasoningCoreEnv(
13
+ EnvClient[ReasoningCoreAction, ReasoningCoreObservation, State]
14
+ ):
15
+ """HTTP/WebSocket client for Reasoning Core."""
16
+
17
+ def _step_payload(self, action: ReasoningCoreAction) -> dict[str, Any]:
18
+ return {"answer": action.answer}
19
+
20
+ def _parse_result(
21
+ self,
22
+ payload: dict[str, Any],
23
+ ) -> StepResult[ReasoningCoreObservation]:
24
+ obs_data = payload.get("observation", {})
25
+ observation = ReasoningCoreObservation(
26
+ prompt=obs_data.get("prompt"),
27
+ score=obs_data.get("score"),
28
+ correct_answer=obs_data.get("correct_answer"),
29
+ task_name=obs_data.get("task_name"),
30
+ dataset_metadata=obs_data.get("dataset_metadata"),
31
+ done=payload.get("done", False),
32
+ reward=payload.get("reward"),
33
+ metadata=obs_data.get("metadata", {}),
34
+ )
35
+ return StepResult(
36
+ observation=observation,
37
+ reward=payload.get("reward"),
38
+ done=payload.get("done", False),
39
+ )
40
+
41
+ def _parse_state(self, payload: dict[str, Any]) -> State:
42
+ return State(
43
+ episode_id=payload.get("episode_id"),
44
+ step_count=payload.get("step_count", 0),
45
+ )
models.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Action and observation models for the Reasoning Core environment."""
2
+
3
+ from typing import Any
4
+
5
+ from openenv.core.env_server.types import Action, Observation
6
+ from pydantic import Field
7
+
8
+
9
+ class ReasoningCoreAction(Action):
10
+ """Submit an answer to the current symbolic reasoning problem."""
11
+
12
+ answer: str = Field(..., description="The final answer to the current problem")
13
+
14
+
15
+ class ReasoningCoreObservation(Observation):
16
+ """A symbolic reasoning problem or its scored result."""
17
+
18
+ prompt: str | None = Field(default=None, description="Problem to solve")
19
+ score: float | None = Field(default=None, description="Answer score from 0 to 1")
20
+ correct_answer: str | None = Field(
21
+ default=None,
22
+ description="Reference answer, revealed after submission",
23
+ )
24
+ task_name: str | None = Field(default=None, description="Reasoning task family")
25
+ dataset_metadata: dict[str, Any] | None = Field(
26
+ default=None,
27
+ description="Metadata associated with the dataset example",
28
+ )
openenv.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: reasoning_core
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+ variables:
8
+ MAX_CONCURRENT_ENVS: "16"
9
+ RC_DATASET_SIZE: "1000"
10
+ RC_HF_DATASET: reasoning-core/formal-reasoning-env
11
+ RC_SEED: "42"
pyproject.toml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=75", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "openenv-reasoning-core"
7
+ version = "0.1.0"
8
+ description = "Formally verifiable symbolic reasoning tasks for OpenEnv"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "datasets>=3.0",
12
+ "easydict>=1.13",
13
+ "openenv[core]>=0.3.1",
14
+ "reasoning-core>=0.4.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=8.0"]
19
+
20
+ [project.scripts]
21
+ server = "reasoning_core_env.server.app:main"
22
+
23
+ [tool.setuptools]
24
+ include-package-data = true
25
+ packages = ["reasoning_core_env", "reasoning_core_env.server"]
26
+ package-dir = { "reasoning_core_env" = ".", "reasoning_core_env.server" = "server" }
server/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Server components for the Reasoning Core environment."""
2
+
3
+ from .reasoning_core_environment import ReasoningCoreEnvironment
4
+
5
+ __all__ = ["ReasoningCoreEnvironment"]
server/app.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI application for the Reasoning Core environment."""
2
+
3
+ import os
4
+
5
+ from openenv.core.env_server.http_server import create_app
6
+
7
+ try:
8
+ from ..models import ReasoningCoreAction, ReasoningCoreObservation
9
+ from .reasoning_core_environment import ReasoningCoreEnvironment
10
+ except ImportError:
11
+ from models import ReasoningCoreAction, ReasoningCoreObservation
12
+ from server.reasoning_core_environment import ReasoningCoreEnvironment
13
+
14
+
15
+ def create_reasoning_core_environment() -> ReasoningCoreEnvironment:
16
+ return ReasoningCoreEnvironment()
17
+
18
+
19
+ app = create_app(
20
+ create_reasoning_core_environment,
21
+ ReasoningCoreAction,
22
+ ReasoningCoreObservation,
23
+ env_name="reasoning_core",
24
+ max_concurrent_envs=int(os.getenv("MAX_CONCURRENT_ENVS", "16")),
25
+ )
26
+
27
+
28
+ def main(host: str = "0.0.0.0", port: int = 8000) -> None:
29
+ import uvicorn
30
+
31
+ uvicorn.run(app, host=host, port=port)
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()
server/reasoning_core_environment.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Single-step OpenEnv environment backed by reasoning-core scorers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import random
8
+ import re
9
+ from itertools import islice
10
+ from typing import Any
11
+ from uuid import uuid4
12
+
13
+ import numpy as np
14
+ from easydict import EasyDict as edict
15
+ from openenv.core.env_server.interfaces import Environment
16
+ from openenv.core.env_server.types import State
17
+ from reasoning_core import get_task, list_tasks, score_answer
18
+
19
+ try:
20
+ from ..models import ReasoningCoreAction, ReasoningCoreObservation
21
+ except ImportError:
22
+ from models import ReasoningCoreAction, ReasoningCoreObservation
23
+
24
+
25
+ DEFAULT_DATASET = os.getenv(
26
+ "RC_HF_DATASET",
27
+ "reasoning-core/formal-reasoning-env",
28
+ )
29
+ DEFAULT_SIZE = int(os.getenv("RC_DATASET_SIZE", "1000"))
30
+ DEFAULT_SEED = int(os.getenv("RC_SEED", "42"))
31
+ DISABLE_HF_DATASET = os.getenv("RC_DISABLE_HF_DATASET", "").lower() in {
32
+ "1",
33
+ "true",
34
+ "yes",
35
+ }
36
+ XML_ANSWER_PATTERN = re.compile(
37
+ r"<answer>(.*?)</answer>",
38
+ flags=re.IGNORECASE | re.DOTALL,
39
+ )
40
+
41
+
42
+ def _metadata_dict(value: Any) -> dict[str, Any]:
43
+ if isinstance(value, dict):
44
+ return value
45
+ if isinstance(value, str) and value.strip():
46
+ try:
47
+ parsed = json.loads(value)
48
+ except json.JSONDecodeError:
49
+ return {"raw_metadata": value}
50
+ return parsed if isinstance(parsed, dict) else {}
51
+ return {}
52
+
53
+
54
+ def _task_name(entry: dict[str, Any]) -> str | None:
55
+ metadata = _metadata_dict(entry.get("metadata"))
56
+ value = metadata.get("_task") or entry.get("task") or metadata.get("task")
57
+ return str(value) if value is not None else None
58
+
59
+
60
+ def _normalize_entry(entry: dict[str, Any], index: int) -> dict[str, Any] | None:
61
+ task_name = _task_name(entry)
62
+ if task_name not in set(list_tasks()):
63
+ return None
64
+ return {
65
+ "id": str(entry.get("id", index)),
66
+ "prompt": str(entry["prompt"]),
67
+ "answer": str(entry["answer"]),
68
+ "metadata": {"task": task_name, **_metadata_dict(entry.get("metadata"))},
69
+ }
70
+
71
+
72
+ def _load_hub_entries(
73
+ dataset_name: str,
74
+ split: str,
75
+ seed: int,
76
+ size: int,
77
+ ) -> list[dict[str, Any]]:
78
+ from datasets import get_dataset_split_names, load_dataset
79
+
80
+ split_names = get_dataset_split_names(dataset_name)
81
+ source_split = split
82
+ if source_split not in split_names:
83
+ source_split = next(
84
+ (name for name in ("test", "validation", "eval", "dev") if name in split_names),
85
+ "train",
86
+ )
87
+
88
+ stream = load_dataset(dataset_name, split=source_split, streaming=True)
89
+ stream = stream.shuffle(seed=seed, buffer_size=max(size * 4, 1000))
90
+ entries: list[dict[str, Any]] = []
91
+ for index, row in enumerate(islice(stream, size * 4)):
92
+ normalized = _normalize_entry(dict(row), index)
93
+ if normalized is not None:
94
+ entries.append(normalized)
95
+ if len(entries) >= size:
96
+ break
97
+ if not entries:
98
+ raise RuntimeError(f"No supported tasks found in {dataset_name}:{source_split}")
99
+ return entries
100
+
101
+
102
+ def _generate_entries(seed: int, size: int) -> list[dict[str, Any]]:
103
+ task_names = sorted(list_tasks())
104
+ entries: list[dict[str, Any]] = []
105
+ for index in range(size):
106
+ random.seed(seed + index)
107
+ np.random.seed(seed + index)
108
+ task_name = task_names[index % len(task_names)]
109
+ example = get_task(task_name).generate_example()
110
+ entries.append(
111
+ {
112
+ "id": f"{task_name}-{index}",
113
+ "prompt": str(example.prompt),
114
+ "answer": str(example.answer),
115
+ "metadata": {"task": task_name, **dict(example.metadata)},
116
+ }
117
+ )
118
+ return entries
119
+
120
+
121
+ def _extract_answer(answer: str) -> str:
122
+ match = XML_ANSWER_PATTERN.search(answer)
123
+ return match.group(1).strip() if match else answer.strip()
124
+
125
+
126
+ class ReasoningCoreEnvironment(Environment):
127
+ """Formally scored symbolic reasoning tasks from reasoning-core."""
128
+
129
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
130
+
131
+ def __init__(self):
132
+ self._state = State(episode_id=str(uuid4()), step_count=0)
133
+ self._entries: list[dict[str, Any]] = []
134
+ self._entry_index = 0
135
+ self._current_entry: dict[str, Any] | None = None
136
+ self._configuration: tuple[str, str, int, int] | None = None
137
+
138
+ def _configure(
139
+ self,
140
+ dataset_name: str,
141
+ split: str,
142
+ seed: int,
143
+ size: int,
144
+ ) -> None:
145
+ configuration = (dataset_name, split, seed, size)
146
+ if configuration == self._configuration:
147
+ return
148
+ if DISABLE_HF_DATASET:
149
+ self._entries = _generate_entries(seed, size)
150
+ self._configuration = configuration
151
+ self._entry_index = 0
152
+ return
153
+ try:
154
+ self._entries = _load_hub_entries(dataset_name, split, seed, size)
155
+ except Exception as exc:
156
+ print(f"Dataset loading failed ({exc}); using procedural tasks.")
157
+ self._entries = _generate_entries(seed, size)
158
+ self._configuration = configuration
159
+ self._entry_index = 0
160
+
161
+ def reset(
162
+ self,
163
+ dataset_name: str = DEFAULT_DATASET,
164
+ split: str = "train",
165
+ seed: int = DEFAULT_SEED,
166
+ size: int = DEFAULT_SIZE,
167
+ episode_id: str | None = None,
168
+ ) -> ReasoningCoreObservation:
169
+ if size <= 0:
170
+ raise ValueError("size must be positive")
171
+ self._configure(dataset_name, split, seed, size)
172
+ self._current_entry = self._entries[self._entry_index % len(self._entries)]
173
+ self._entry_index += 1
174
+ self._state = State(
175
+ episode_id=episode_id or str(uuid4()),
176
+ step_count=0,
177
+ )
178
+ return ReasoningCoreObservation(
179
+ prompt=self._current_entry["prompt"],
180
+ score=None,
181
+ correct_answer=None,
182
+ task_name=_task_name(self._current_entry),
183
+ dataset_metadata=None,
184
+ done=False,
185
+ reward=0.0,
186
+ )
187
+
188
+ def step(self, action: ReasoningCoreAction) -> ReasoningCoreObservation:
189
+ self._state.step_count += 1
190
+ if self._current_entry is None:
191
+ raise RuntimeError("Call reset() before step().")
192
+
193
+ entry = edict(
194
+ answer=self._current_entry["answer"],
195
+ metadata=self._current_entry["metadata"],
196
+ )
197
+ score = float(score_answer(_extract_answer(action.answer), entry))
198
+ return ReasoningCoreObservation(
199
+ prompt=None,
200
+ score=score,
201
+ correct_answer=self._current_entry["answer"],
202
+ task_name=_task_name(self._current_entry),
203
+ dataset_metadata=self._current_entry["metadata"],
204
+ done=True,
205
+ reward=score,
206
+ )
207
+
208
+ @property
209
+ def state(self) -> State:
210
+ return self._state
uv.lock ADDED
The diff for this file is too large to render. See raw diff