Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/environments/agent_world_model.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ A background cleanup daemon automatically kills sessions idle longer than `MAX_I

## Full Agent Interaction Example

See [`examples/agent_world_model/example_usage.py`](../../examples/agent_world_model/example_usage.py) for a complete example of an LLM-powered agent that:
See [`examples/agent_world_model/example_usage.py`](https://github.com/huggingface/OpenEnv/blob/main/examples/agent_world_model/example_usage.py) for a complete example of an LLM-powered agent that:

1. Discovers available tools via `list_tools`
2. Iteratively calls tools to accomplish the task
Expand Down
14 changes: 7 additions & 7 deletions docs/source/environments/carla.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,11 @@ result = await env.reset(scenario_config={

## Examples

The [`examples/carla_env/`](../../examples/carla_env/) directory contains inference scripts. All connect to `http://localhost:8000` by default — pass `--base-url https://sergiopaniego-carla-env.hf.space` for the live Space.
The [`examples/carla_env/`](https://github.com/huggingface/OpenEnv/tree/main/examples/carla_env) directory contains inference scripts. All connect to `http://localhost:8000` by default — pass `--base-url https://sergiopaniego-carla-env.hf.space` for the live Space.

### Trolley Problems

**[trolley_problems.py](../../examples/carla_env/trolley_problems.py)** — LLM evaluation across all trolley scenarios.
**[trolley_problems.py](https://github.com/huggingface/OpenEnv/blob/main/examples/carla_env/trolley_problems.py)** — LLM evaluation across all trolley scenarios.

```bash
uv run python trolley_problems.py --model qwen3-235b --scenario classic-3v1
Expand All @@ -168,7 +168,7 @@ Available keys: `equal-1v1`, `saves-3v0`, `deadzone-3v1`, `classic-3v1`, `classi

### Maze Navigation

**[maze_navigation.py](../../examples/carla_env/maze_navigation.py)** — LLM navigation with rolling action history.
**[maze_navigation.py](https://github.com/huggingface/OpenEnv/blob/main/examples/carla_env/maze_navigation.py)** — LLM navigation with rolling action history.

```bash
uv run python maze_navigation.py --model qwen3-235b --scenario maze-1
Expand All @@ -177,7 +177,7 @@ uv run python maze_navigation.py --model gpt-5.2 --scenario maze-1 --save-images

### Free-Roam Navigation

**[free_roam_navigation.py](../../examples/carla_env/free_roam_navigation.py)** — LLM navigation in open traffic.
**[free_roam_navigation.py](https://github.com/huggingface/OpenEnv/blob/main/examples/carla_env/free_roam_navigation.py)** — LLM navigation in open traffic.

```bash
uv run python free_roam_navigation.py --model qwen3-235b
Expand All @@ -186,7 +186,7 @@ uv run python free_roam_navigation.py --model qwen3-235b --scenario free-roam-tr

### Autopilot Baseline (No LLM)

**[autopilot_navigation.py](../../examples/carla_env/autopilot_navigation.py)** — CARLA's built-in navigation agent.
**[autopilot_navigation.py](https://github.com/huggingface/OpenEnv/blob/main/examples/carla_env/autopilot_navigation.py)** — CARLA's built-in navigation agent.

```bash
uv run python autopilot_navigation.py --scenario maze-1
Expand All @@ -195,7 +195,7 @@ uv run python autopilot_navigation.py --scenario free-roam-default --behavior ca

### Rubric Reward Demo (No LLM)

**[rubric_autopilot_example.py](../../examples/carla_env/rubric_autopilot_example.py)** — Raw vs rubric rewards side-by-side.
**[rubric_autopilot_example.py](https://github.com/huggingface/OpenEnv/blob/main/examples/carla_env/rubric_autopilot_example.py)** — Raw vs rubric rewards side-by-side.

```bash
uv run python rubric_autopilot_example.py --scenario free-roam-default
Expand All @@ -222,7 +222,7 @@ Hugging Face models use [Inference Providers](https://huggingface.co/docs/infere

## Rubrics for RL Training

The environment includes rubrics following the [OpenEnv rubric system](../../rfcs/004-rubrics.md). Rubrics are automatically selected based on the scenario type and populate `obs.rubric_reward` alongside the raw `obs.reward` on each step.
The environment includes rubrics following the [OpenEnv rubric system](https://github.com/huggingface/OpenEnv/blob/main/rfcs/004-rubrics.md). Rubrics are automatically selected based on the scenario type and populate `obs.rubric_reward` alongside the raw `obs.reward` on each step.

**CarlaTrolleyRubric** — For trolley/action-bias scenarios. Returns 0.0 on intermediate steps, then the terminal reward at episode end. Supports temporal discounting (`gamma`) for credit assignment.

Expand Down
2 changes: 1 addition & 1 deletion docs/source/environments/git.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ docker run \

Then in your training code, environments automatically reset to the configured state.

See [`examples/local_git_env.py`](../../../examples/local_git_env.py) for complete working example.
See [`examples/local_git_env.py`](https://github.com/huggingface/OpenEnv/blob/main/examples/local_git_env.py) for complete working example.

## Project Structure

Expand Down
2 changes: 1 addition & 1 deletion docs/source/environments/opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,4 @@ opencode_env/
- [OpenEnv docs](https://huggingface.co/docs/openenv)
- [OpenCode CLI](https://opencode.ai/docs/cli/)
- [E2B Python SDK](https://e2b.dev/docs)
- [HF Inference Providers logprob matrix](../../../DOCS/HF/hf_inference_providers_logprobs.md)
- [HF Inference Providers logprob matrix](https://github.com/huggingface/OpenEnv/blob/main/DOCS/HF/hf_inference_providers_logprobs.md)
4 changes: 2 additions & 2 deletions docs/source/environments/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,8 @@ These prompts were updated to reflect the actual helper surface the environment

## Examples

- [`examples/repl_with_llm.py`](../../examples/repl_with_llm.py)
- [`examples/repl_oolong_simple.py`](../../examples/repl_oolong_simple.py)
- [`examples/repl_with_llm.py`](https://github.com/huggingface/OpenEnv/blob/main/examples/repl_with_llm.py)
- [`examples/repl_oolong_simple.py`](https://github.com/huggingface/OpenEnv/blob/main/examples/repl_oolong_simple.py)

Default hosted model in the examples is currently `Qwen/Qwen3.5-9B`, but real hosted inference still depends on provider availability and token access.

Expand Down
97 changes: 92 additions & 5 deletions scripts/sync_env_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@

import argparse
import os
import posixpath
import re
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ENVS_DIR = os.path.join(ROOT, "envs")
DOCS_ENVS_DIR = os.path.join(ROOT, "docs", "source", "environments")
GITHUB_RAW_BASE = "https://raw.githubusercontent.com/huggingface/OpenEnv/main"
GITHUB_BLOB_BASE = "https://github.com/huggingface/OpenEnv/blob/main"
GITHUB_TREE_BASE = "https://github.com/huggingface/OpenEnv/tree/main"

SKIP_DIRS = {"README.md"}
_MD_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -97,6 +101,78 @@ def _strip_frontmatter(text):
return text


def _is_absolute_or_external_url(url):
"""Return True if *url* should be left untouched (http, mailto, anchors, site-root)."""
url = url.strip()
if not url:
return True
if url.startswith(("#", "/", "http://", "https://", "mailto:")):
return True
if "://" in url:
return True
return False


def repo_relpath_from_readme_link(env_dir, url):
"""Resolve a README-relative link to a repo-root POSIX path, or None.

Extra ``../`` segments that walk out of the repository (a common README
mistake) are stripped so ``envs/git_env/../../../examples/foo.py`` still
maps to ``examples/foo.py``.
"""
path = url.split()[0].strip()
path, hash_sep, anchor = path.partition("#")
if not path or _is_absolute_or_external_url(path):
return None
joined = posixpath.normpath(posixpath.join("envs", env_dir, path))
while joined.startswith("../"):
joined = joined[3:]
if joined in {".", ""} or joined.startswith(".."):
return None
if hash_sep:
return f"{joined}#{anchor}"
return joined


def _link_escapes_env_dir(env_dir, repo_relpath):
"""True when the resolved path is outside ``envs/<env_dir>/``."""
path_only = repo_relpath.split("#", 1)[0]
prefix = f"envs/{env_dir}"
return path_only != prefix and not path_only.startswith(prefix + "/")


def rewrite_markdown_links(content, env_dir):
"""Rewrite README-relative markdown links that leave the env directory.

Those links are valid from ``envs/<env>/README.md`` but break once the
README is inlined at ``docs/source/environments/<slug>.md``. Convert them
to GitHub blob/tree URLs, matching the convention already used by some
env READMEs. See https://github.com/huggingface/OpenEnv/issues/1095
"""

def repl(match):
text, url = match.group(1), match.group(2)
raw_url = url.split()[0]
repo_rel = repo_relpath_from_readme_link(env_dir, raw_url)
if repo_rel is None or not _link_escapes_env_dir(env_dir, repo_rel):
return match.group(0)
path_only, hash_sep, anchor = repo_rel.partition("#")
abs_fs = os.path.join(ROOT, *path_only.split("/"))
is_image = match.start() > 0 and content[match.start() - 1] == "!"
url_path = raw_url.split("#", 1)[0]
if is_image:
new_url = f"{GITHUB_RAW_BASE}/{path_only}"
elif url_path.endswith("/") or os.path.isdir(abs_fs):
new_url = f"{GITHUB_TREE_BASE}/{path_only}"
else:
new_url = f"{GITHUB_BLOB_BASE}/{path_only}"
if hash_sep:
new_url = f"{new_url}#{anchor}"
return f"[{text}]({new_url})"

return _MD_LINK_RE.sub(repl, content)


def generate_stub(env_dir):
"""Return the doc stub content for an environment.

Expand All @@ -111,7 +187,8 @@ def generate_stub(env_dir):
# Rewrite relative assets/ paths to absolute GitHub raw URLs so images
# render correctly when the README is inlined into the doc-builder site.
base_url = f"{GITHUB_RAW_BASE}/envs/{env_dir}"
content = re.sub(r'(src=["\'])assets/', rf'\1{base_url}/assets/', content)
content = re.sub(r'(src=["\'])assets/', rf"\1{base_url}/assets/", content)
content = rewrite_markdown_links(content, env_dir)
return f"<!-- openenv-source: {env_dir} -->\n{content}"


Expand Down Expand Up @@ -241,9 +318,15 @@ def main():
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--check", action="store_true", help="Check sync status (CI mode)")
group.add_argument("--fix", action="store_true", help="Fix missing, stale, and orphaned stubs")
group.add_argument("--dry-run", action="store_true", help="Preview --fix without writing")
group.add_argument(
"--check", action="store_true", help="Check sync status (CI mode)"
)
group.add_argument(
"--fix", action="store_true", help="Fix missing, stale, and orphaned stubs"
)
group.add_argument(
"--dry-run", action="store_true", help="Preview --fix without writing"
)
args = parser.parse_args()

env_dirs = get_env_dirs()
Expand All @@ -264,7 +347,11 @@ def main():
print("✅ Everything is already in sync.")
return

print("Fixing documentation...\n" if not args.dry_run else "Dry run — no files will be modified:\n")
print(
"Fixing documentation...\n"
if not args.dry_run
else "Dry run — no files will be modified:\n"
)
run_fix(missing, orphaned, stale, dry_run=args.dry_run)


Expand Down
85 changes: 85 additions & 0 deletions tests/scripts/test_sync_env_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Tests for scripts/sync_env_docs.py README inlining and link rewriting."""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

SCRIPT_PATH = Path(__file__).resolve().parents[2] / "scripts" / "sync_env_docs.py"
SPEC = importlib.util.spec_from_file_location("sync_env_docs", SCRIPT_PATH)
assert SPEC is not None
sync_env_docs = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules["sync_env_docs"] = sync_env_docs
SPEC.loader.exec_module(sync_env_docs)

repo_relpath_from_readme_link = sync_env_docs.repo_relpath_from_readme_link
rewrite_markdown_links = sync_env_docs.rewrite_markdown_links
generate_stub = sync_env_docs.generate_stub
GITHUB_BLOB_BASE = sync_env_docs.GITHUB_BLOB_BASE
GITHUB_TREE_BASE = sync_env_docs.GITHUB_TREE_BASE


def test_repo_relpath_strips_extra_parent_segments() -> None:
# git_env README uses ../../../examples/... which walks out of the repo.
rel = repo_relpath_from_readme_link("git_env", "../../../examples/local_git_env.py")
assert rel == "examples/local_git_env.py"


def test_repo_relpath_from_env_readme() -> None:
rel = repo_relpath_from_readme_link(
"agent_world_model_env", "../../examples/agent_world_model/example_usage.py"
)
assert rel == "examples/agent_world_model/example_usage.py"


def test_rewrite_escapes_to_github_blob() -> None:
src = "See [example](../../examples/repl_with_llm.py) please."
out = rewrite_markdown_links(src, "repl_env")
assert out == f"See [example]({GITHUB_BLOB_BASE}/examples/repl_with_llm.py) please."


def test_rewrite_directory_link_uses_tree() -> None:
src = "The [`examples/carla_env/`](../../examples/carla_env/) directory."
out = rewrite_markdown_links(src, "carla_env")
assert f"]({GITHUB_TREE_BASE}/examples/carla_env/)" in out or (
f"]({GITHUB_TREE_BASE}/examples/carla_env)" in out
)


def test_rewrite_leaves_in_env_and_absolute_links() -> None:
src = (
"See [local](server/app.py) and "
"[docs](https://github.com/huggingface/OpenEnv/blob/main/examples/x.py) "
"and [section](#overview)."
)
assert rewrite_markdown_links(src, "git_env") == src


def test_generate_stub_rewrites_git_env_example_link() -> None:
stub = generate_stub("git_env")
assert f"]({GITHUB_BLOB_BASE}/examples/local_git_env.py)" in stub
assert "](../../../examples/local_git_env.py)" not in stub


def test_generate_stub_rewrites_agent_world_model_example_link() -> None:
stub = generate_stub("agent_world_model_env")
assert f"]({GITHUB_BLOB_BASE}/examples/agent_world_model/example_usage.py)" in stub
assert "](../../examples/agent_world_model/example_usage.py)" not in stub


def test_generate_stub_rewrites_carla_and_rfc_links() -> None:
stub = generate_stub("carla_env")
assert f"]({GITHUB_TREE_BASE}/examples/carla_env)" in stub or (
f"]({GITHUB_TREE_BASE}/examples/carla_env/)" in stub
)
assert f"]({GITHUB_BLOB_BASE}/rfcs/004-rubrics.md)" in stub
assert "](../../examples/carla_env/)" not in stub
assert "](../../rfcs/004-rubrics.md)" not in stub


def test_generate_stub_rewrites_opencode_dead_relative_link() -> None:
stub = generate_stub("opencode_env")
assert "](../../../DOCS/HF/hf_inference_providers_logprobs.md)" not in stub
assert f"]({GITHUB_BLOB_BASE}/DOCS/HF/hf_inference_providers_logprobs.md)" in stub