diff --git a/docs/source/environments/agent_world_model.md b/docs/source/environments/agent_world_model.md index 03d1c92aee..cf504eca4d 100644 --- a/docs/source/environments/agent_world_model.md +++ b/docs/source/environments/agent_world_model.md @@ -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 diff --git a/docs/source/environments/carla.md b/docs/source/environments/carla.md index 2b9a5fab60..2095e69245 100644 --- a/docs/source/environments/carla.md +++ b/docs/source/environments/carla.md @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. diff --git a/docs/source/environments/git.md b/docs/source/environments/git.md index 1b386c672f..df4018b232 100644 --- a/docs/source/environments/git.md +++ b/docs/source/environments/git.md @@ -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 diff --git a/docs/source/environments/opencode.md b/docs/source/environments/opencode.md index b936ea3e46..ca7cd64bf6 100644 --- a/docs/source/environments/opencode.md +++ b/docs/source/environments/opencode.md @@ -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) diff --git a/docs/source/environments/repl.md b/docs/source/environments/repl.md index 0407205da3..3da73b317e 100644 --- a/docs/source/environments/repl.md +++ b/docs/source/environments/repl.md @@ -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. diff --git a/scripts/sync_env_docs.py b/scripts/sync_env_docs.py index c222bddc22..3f2dd9129a 100644 --- a/scripts/sync_env_docs.py +++ b/scripts/sync_env_docs.py @@ -19,6 +19,7 @@ import argparse import os +import posixpath import re import sys @@ -26,8 +27,11 @@ 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"\[([^\]]+)\]\(([^)]+)\)") # --------------------------------------------------------------------------- @@ -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//``.""" + 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//README.md`` but break once the + README is inlined at ``docs/source/environments/.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. @@ -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"\n{content}" @@ -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() @@ -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) diff --git a/tests/scripts/test_sync_env_docs.py b/tests/scripts/test_sync_env_docs.py new file mode 100644 index 0000000000..ebe37adfe4 --- /dev/null +++ b/tests/scripts/test_sync_env_docs.py @@ -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