Skip to content
Closed
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
54 changes: 51 additions & 3 deletions nemo_gym/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ def _bool_flag(name: str, hydra_key: str, flag_help: str) -> Flag:
translate_to_hydra=lambda args: [f"+config_paths=[{','.join(args.config)}]"] if args.config else [],
)

# Shared flag: select an environment by name (run-by-name). Resolved to its config via the registry
# in `_env_run` rather than statically, then merged with any --config into a single +config_paths.
ENV = Flag(
register=lambda p: p.add_argument(
"--env",
metavar="NAME",
help="Environment to run, by name (see `gym list environments`); combined with --config / --model-* for the model.",
),
)

# Shared flag: select the storage backend. Reused by `dataset upload` and `dataset download`.
STORAGE = Flag(
register=lambda p: p.add_argument(
Expand Down Expand Up @@ -256,6 +266,34 @@ def _eval_run(args: argparse.Namespace, overrides: list[str]) -> None:
dispatch(target, overrides)


def _env_run(args: argparse.Namespace, overrides: list[str]) -> None:
# Run an environment by name: resolve --env to its config via the registry and merge it with any
# --config files into a single +config_paths token (the env references its model server, so pair
# it with --config / --model-* for the model). Other overrides (model flags, passthrough) flow
# through unchanged.
config_paths: list[str] = []
if getattr(args, "env", None):
from nemo_gym.registry import EnvironmentNotFoundError, resolve_environment_config_paths

try:
config_paths += resolve_environment_config_paths(args.env)
except EnvironmentNotFoundError as error:
raise SystemExit(f"error: {error}")
# Merge --env's resolved config(s) with any +config_paths the router already emitted — from
# --config or from an asset-selector name (e.g. `gym env run mcqa`) — into a single token,
# preserving order. Pulling paths out of the existing token (rather than re-reading args.config)
# means asset-selector resolution is not discarded.
rest: list[str] = []
for token in overrides:
if token.startswith("+config_paths=[") and token.endswith("]"):
config_paths += [path for path in token[len("+config_paths=[") : -1].split(",") if path]
else:
rest.append(token)

merged = ([f"+config_paths=[{','.join(config_paths)}]"] if config_paths else []) + rest
dispatch("nemo_gym.cli.env:run", merged)


def _env_test(args: argparse.Namespace, overrides: list[str]) -> None:
# Run a single server's tests if +entrypoint was passed. No need to check for
# --resource-server because it is translated to +entrypoint in the flag definition.
Expand Down Expand Up @@ -409,9 +447,19 @@ def _dataset_download(args: argparse.Namespace, overrides: list[str]) -> None:
flags=(RESOURCE_SERVER,),
),
"env run": Command(
target="nemo_gym.cli.env:run",
summary="Start the servers.",
flags=(CONFIG, BENCHMARK, RESOURCE_SERVER_CONFIG, MODEL_TYPE, SEARCH_DIR, MODEL, MODEL_URL, MODEL_API_KEY),
target=_env_run,
summary="Start the servers (by --env name and/or --config).",
flags=(
ENV,
CONFIG,
BENCHMARK,
RESOURCE_SERVER_CONFIG,
MODEL_TYPE,
SEARCH_DIR,
MODEL,
MODEL_URL,
MODEL_API_KEY,
),
),
"env status": Command(target="nemo_gym.cli.env:status", summary="Print the server status.", flags=(JSON,)),
"eval prepare": Command(
Expand Down
59 changes: 59 additions & 0 deletions tests/unit_tests/test_cli_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,62 @@ def test_list_environments_dispatches(self, monkeypatch: MonkeyPatch) -> None:
target, overrides = _dispatch_for(monkeypatch, ["list", "environments"])
assert target == "nemo_gym.cli.env:list_environments"
assert overrides == []


class TestEnvRunByName:
def _patch_resolve(self, monkeypatch: MonkeyPatch) -> None:
import nemo_gym.registry

monkeypatch.setattr(
nemo_gym.registry,
"resolve_environment_config_paths",
lambda name, *a, **k: [f"environments/{name}/config.yaml"],
)

def test_env_name_resolves_to_config_path(self, monkeypatch: MonkeyPatch) -> None:
self._patch_resolve(monkeypatch)
target, overrides = _dispatch_for(monkeypatch, ["env", "run", "--env", "alpha"])
assert target == "nemo_gym.cli.env:run"
assert overrides == ["+config_paths=[environments/alpha/config.yaml]"]

def test_env_and_config_merge_into_one_config_paths(self, monkeypatch: MonkeyPatch) -> None:
self._patch_resolve(monkeypatch)
_, overrides = _dispatch_for(monkeypatch, ["env", "run", "--env", "alpha", "--config", "model.yaml"])
config_paths = [o for o in overrides if o.startswith("+config_paths=")]
assert config_paths == ["+config_paths=[environments/alpha/config.yaml,model.yaml]"]

def test_model_flags_pass_through(self, monkeypatch: MonkeyPatch) -> None:
self._patch_resolve(monkeypatch)
_, overrides = _dispatch_for(monkeypatch, ["env", "run", "--env", "alpha", "--model", "gpt"])
assert "+config_paths=[environments/alpha/config.yaml]" in overrides
assert "+policy_model_name=gpt" in overrides

def test_config_only_is_unchanged(self, monkeypatch: MonkeyPatch) -> None:
target, overrides = _dispatch_for(monkeypatch, ["env", "run", "--config", "a.yaml"])
assert target == "nemo_gym.cli.env:run"
assert overrides == ["+config_paths=[a.yaml]"]

def test_existing_config_paths_token_is_preserved(self, monkeypatch: MonkeyPatch) -> None:
# An asset-selector name (or passthrough) reaches _env_run as a +config_paths token with no
# --env/--config; it must survive rather than be stripped.
target, overrides = _dispatch_for(monkeypatch, ["env", "run", "+config_paths=[foo.yaml]"])
assert target == "nemo_gym.cli.env:run"
assert overrides == ["+config_paths=[foo.yaml]"]

def test_env_merges_with_existing_config_paths_token(self, monkeypatch: MonkeyPatch) -> None:
self._patch_resolve(monkeypatch)
_, overrides = _dispatch_for(monkeypatch, ["env", "run", "--env", "alpha", "+config_paths=[foo.yaml]"])
assert overrides == ["+config_paths=[environments/alpha/config.yaml,foo.yaml]"]

def test_unknown_env_exits_cleanly(self, monkeypatch: MonkeyPatch) -> None:
import nemo_gym.registry
from nemo_gym.registry import EnvironmentNotFoundError

def boom(name, *a, **k):
raise EnvironmentNotFoundError(f"No environment named '{name}'")

monkeypatch.setattr(nemo_gym.registry, "resolve_environment_config_paths", boom)
monkeypatch.setattr(cli_main, "dispatch", lambda target, overrides: None)
monkeypatch.setattr(sys, "argv", ["gym", "env", "run", "--env", "nope"])
with pytest.raises(SystemExit):
main()
Loading