nemo-evaluator-launcher 0.1.41__py3-none-any.whl → 0.1.67__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. nemo_evaluator_launcher/api/functional.py +55 -5
  2. nemo_evaluator_launcher/api/types.py +21 -14
  3. nemo_evaluator_launcher/cli/ls_task.py +280 -0
  4. nemo_evaluator_launcher/cli/ls_tasks.py +208 -55
  5. nemo_evaluator_launcher/cli/main.py +17 -2
  6. nemo_evaluator_launcher/cli/run.py +43 -52
  7. nemo_evaluator_launcher/common/container_metadata/__init__.py +61 -0
  8. nemo_evaluator_launcher/common/container_metadata/intermediate_repr.py +530 -0
  9. nemo_evaluator_launcher/common/container_metadata/loading.py +1126 -0
  10. nemo_evaluator_launcher/common/container_metadata/registries.py +824 -0
  11. nemo_evaluator_launcher/common/container_metadata/utils.py +63 -0
  12. nemo_evaluator_launcher/common/helpers.py +44 -28
  13. nemo_evaluator_launcher/common/mapping.py +166 -177
  14. nemo_evaluator_launcher/common/printing_utils.py +18 -12
  15. nemo_evaluator_launcher/configs/deployment/nim.yaml +3 -1
  16. nemo_evaluator_launcher/executors/lepton/executor.py +26 -8
  17. nemo_evaluator_launcher/executors/local/executor.py +6 -2
  18. nemo_evaluator_launcher/executors/slurm/executor.py +270 -22
  19. nemo_evaluator_launcher/package_info.py +1 -1
  20. nemo_evaluator_launcher/resources/all_tasks_irs.yaml +17016 -0
  21. nemo_evaluator_launcher/resources/mapping.toml +62 -354
  22. {nemo_evaluator_launcher-0.1.41.dist-info → nemo_evaluator_launcher-0.1.67.dist-info}/METADATA +2 -1
  23. {nemo_evaluator_launcher-0.1.41.dist-info → nemo_evaluator_launcher-0.1.67.dist-info}/RECORD +27 -20
  24. {nemo_evaluator_launcher-0.1.41.dist-info → nemo_evaluator_launcher-0.1.67.dist-info}/WHEEL +0 -0
  25. {nemo_evaluator_launcher-0.1.41.dist-info → nemo_evaluator_launcher-0.1.67.dist-info}/entry_points.txt +0 -0
  26. {nemo_evaluator_launcher-0.1.41.dist-info → nemo_evaluator_launcher-0.1.67.dist-info}/licenses/LICENSE +0 -0
  27. {nemo_evaluator_launcher-0.1.41.dist-info → nemo_evaluator_launcher-0.1.67.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,13 @@ from dataclasses import dataclass
18
18
 
19
19
  from simple_parsing import field
20
20
 
21
+ from nemo_evaluator_launcher.common.printing_utils import (
22
+ bold,
23
+ cyan,
24
+ grey,
25
+ magenta,
26
+ )
27
+
21
28
 
22
29
  @dataclass
23
30
  class Cmd:
@@ -28,20 +35,101 @@ class Cmd:
28
35
  action="store_true",
29
36
  help="Print output as JSON instead of table format",
30
37
  )
38
+ from_container: str = field(
39
+ default="",
40
+ help="Load tasks from container image (e.g., nvcr.io/nvidia/eval-factory/simple-evals:25.10). "
41
+ "If provided, extracts framework.yml from container and lists tasks on-the-fly instead of using mapping.toml",
42
+ )
31
43
 
32
44
  def execute(self) -> None:
33
45
  # Import heavy dependencies only when needed
34
46
  import json
35
47
 
36
- from nemo_evaluator_launcher.api.functional import get_tasks_list
48
+ if self.from_container:
49
+ # Load tasks from container
50
+ from nemo_evaluator_launcher.common.container_metadata import (
51
+ load_tasks_from_container,
52
+ )
53
+
54
+ try:
55
+ tasks = load_tasks_from_container(self.from_container)
56
+ except ValueError as e:
57
+ from nemo_evaluator_launcher.common.logging_utils import logger
58
+
59
+ logger.error(
60
+ "Failed to load tasks from container",
61
+ container=self.from_container,
62
+ error=str(e),
63
+ )
64
+ return
65
+ except Exception as e:
66
+ from nemo_evaluator_launcher.common.logging_utils import logger
67
+
68
+ logger.error(
69
+ "Failed to load tasks from container",
70
+ container=self.from_container,
71
+ error=str(e),
72
+ exc_info=True,
73
+ )
74
+ return
75
+
76
+ if not tasks:
77
+ from nemo_evaluator_launcher.common.logging_utils import logger
78
+
79
+ logger.error(
80
+ "No tasks found in container",
81
+ container=self.from_container,
82
+ )
83
+ return
37
84
 
38
- # TODO(dfridman): modify `get_tasks_list` to return a list of dicts in the first place
39
- data = get_tasks_list()
40
- headers = ["task", "endpoint_type", "harness", "container"]
85
+ # Convert TaskIntermediateRepresentation to format expected by get_tasks_list()
86
+ # Build data structure matching get_tasks_list() output format
87
+ data = []
88
+ for task in tasks:
89
+ # Extract endpoint types from defaults
90
+ endpoint_types = (
91
+ task.defaults.get("target", {})
92
+ .get("api_endpoint", {})
93
+ .get("type", "chat")
94
+ )
95
+ if isinstance(endpoint_types, str):
96
+ endpoint_types = [endpoint_types]
97
+
98
+ data.append(
99
+ [
100
+ task.name, # task
101
+ ",".join(endpoint_types)
102
+ if isinstance(endpoint_types, list)
103
+ else endpoint_types, # endpoint_type
104
+ task.harness, # harness
105
+ task.container, # container
106
+ getattr(task, "container_arch", "") or "", # arch
107
+ task.description, # description
108
+ ]
109
+ )
110
+ else:
111
+ # Default behavior: load from mapping.toml via get_tasks_list()
112
+ from nemo_evaluator_launcher.api.functional import get_tasks_list
113
+
114
+ # TODO(dfridman): modify `get_tasks_list` to return a list of dicts in the first place
115
+ data = get_tasks_list()
116
+
117
+ headers = [
118
+ "task",
119
+ "endpoint_type",
120
+ "harness",
121
+ "container",
122
+ "arch",
123
+ "description",
124
+ ]
41
125
  supported_benchmarks = []
42
126
  for task_data in data:
43
- assert len(task_data) == len(headers)
44
- supported_benchmarks.append(dict(zip(headers, task_data)))
127
+ if len(task_data) < len(headers):
128
+ raise ValueError(
129
+ f"Invalid task row shape: expected at least {len(headers)} columns, got {len(task_data)}"
130
+ )
131
+ # Backwards/forwards compat: allow extra columns and ignore them.
132
+ supported_benchmarks.append(dict(zip(headers, task_data[: len(headers)])))
45
133
 
46
134
  if self.json:
47
135
  print(json.dumps({"tasks": supported_benchmarks}, indent=2))
@@ -49,11 +137,55 @@ class Cmd:
49
137
  self._print_table(supported_benchmarks)
50
138
 
51
139
  def _print_table(self, tasks: list[dict]) -> None:
52
- """Print tasks grouped by harness and container in table format."""
140
+ """Print tasks grouped by harness and container in table format with colorized output."""
53
141
  if not tasks:
54
142
  print("No tasks found.")
55
143
  return
56
144
 
145
+ def _truncate(s: str, max_len: int) -> str:
146
+ s = s or ""
147
+ if max_len <= 0:
148
+ return ""
149
+ if len(s) <= max_len:
150
+ return s
151
+ if max_len <= 3:
152
+ return s[:max_len]
153
+ return s[: max_len - 3] + "..."
154
+
155
+ def _infer_arch(container: str, container_tasks: list[dict]) -> str:
156
+ # Prefer explicit arch from task IRs.
157
+ for t in container_tasks:
158
+ a = (t.get("arch") or "").strip()
159
+ if a:
160
+ return a
161
+
162
+ # Heuristic fallback: look for common suffixes in tag.
163
+ c = (container or "").lower()
164
+ if "arm64" in c or "aarch64" in c:
165
+ return "arm"
166
+ if "amd64" in c or "x86_64" in c:
167
+ return "amd"
168
+ return "unknown"
169
+
170
+ def _infer_registry(container: str) -> str:
171
+ try:
172
+ from nemo_evaluator_launcher.common.container_metadata.utils import (
173
+ parse_container_image,
174
+ )
175
+
176
+ registry_type, _registry_url, _repo, _ref = parse_container_image(
177
+ container
178
+ )
179
+ return str(registry_type)
180
+ except Exception:
181
+ # Best-effort fallback for unknown formats.
182
+ c = (container or "").lower()
183
+ if "nvcr.io/" in c or c.startswith("nvcr.io"):
184
+ return "nvcr"
185
+ if "gitlab" in c:
186
+ return "gitlab"
187
+ return ""
188
+
57
189
  # Group tasks by harness and container
58
190
  grouped = defaultdict(lambda: defaultdict(list))
59
191
  for task in tasks:
@@ -70,67 +202,88 @@ class Cmd:
70
202
  if j > 0:
71
203
  print() # Spacing between containers
72
204
 
73
- # Prepare task table first to get column widths
74
- task_headers = ["task", "endpoint_type"]
75
205
  rows = []
76
206
  for task in container_tasks:
77
- rows.append([task["task"], task["endpoint_type"]])
78
-
79
- # Sort tasks alphabetically for better readability
80
- rows.sort(key=lambda x: x[0])
81
-
82
- # Calculate column widths with some padding
83
- widths = [
84
- max(len(task_headers[i]), max(len(str(row[i])) for row in rows)) + 2
85
- for i in range(len(task_headers))
86
- ]
87
-
88
- # Calculate minimum table width based on task columns
89
- min_table_width = sum(widths) + len(widths) + 1
207
+ rows.append(
208
+ {
209
+ "task": str(task.get("task", "")),
210
+ "endpoint": str(task.get("endpoint_type", "")),
211
+ "description": str(task.get("description", "")),
212
+ }
213
+ )
214
+ rows.sort(key=lambda r: r["task"].lower())
90
215
 
91
216
  # Calculate required width for header content
92
217
  harness_line = f"harness: {harness}"
93
218
  container_line = f"container: {container}"
219
+ arch_line = f"arch: {_infer_arch(container, container_tasks)}"
220
+ registry_line = f"registry: {_infer_registry(container)}"
94
221
  header_content_width = (
95
- max(len(harness_line), len(container_line)) + 4
222
+ max(
223
+ len(harness_line),
224
+ len(container_line),
225
+ len(arch_line),
226
+ len(registry_line),
227
+ )
228
+ + 4
96
229
  ) # +4 for "| " and " |"
97
230
 
98
- # Use the larger of the two widths
99
- table_width = max(min_table_width, header_content_width)
231
+ # Limit separator width to prevent overflow on small terminals
232
+ # Use terminal width if available, otherwise cap at 120 characters
233
+ import shutil
100
234
 
101
- # Print combined header with harness and container info
102
- print("=" * table_width)
103
- print(f"{harness_line}")
104
- print(f"{container_line}")
235
+ try:
236
+ terminal_width = shutil.get_terminal_size().columns
237
+ separator_width = min(terminal_width - 2, 160) # -2 safety margin
238
+ except Exception:
239
+ # Fallback if terminal size can't be determined
240
+ separator_width = 120
105
241
 
106
- # Adjust column widths to fill the full table width
107
- available_width = table_width
108
- # Give more space to the first column (task names can be long)
109
- adjusted_widths = [
110
- max(
111
- widths[0], available_width * 2 // 3
112
- ), # 2/3 of available width for task
113
- 0, # Will be calculated as remainder
114
- ]
115
- adjusted_widths[1] = (
116
- available_width - adjusted_widths[0]
117
- ) # Remainder for endpoint_type
242
+ separator_width = max(separator_width, min(header_content_width, 160))
243
+
244
+ # Table columns (keep compact and stable).
245
+ col_task = 36
246
+ col_endpoint = 14
247
+ sep = " "
248
+ fixed = col_task + col_endpoint + len(sep) * 2
249
+ col_desc = max(20, separator_width - fixed)
250
+
251
+ # Print combined header with harness and container info - colorized
252
+ # Keys: magenta, Values: cyan (matching logging utils)
253
+ print(bold("=" * separator_width))
254
+ print(f"{magenta('harness:')} {cyan(str(harness))}")
255
+ print(f"{magenta('container:')} {cyan(str(container))}")
256
+ arch = _infer_arch(container, container_tasks)
257
+ registry = _infer_registry(container)
258
+ print(f"{magenta('arch:')} {cyan(str(arch))}")
259
+ if registry:
260
+ print(f"{magenta('registry:')} {cyan(str(registry))}")
118
261
 
119
262
  # Print task table header separator
120
- print(" " * table_width)
121
- header_row = f"{task_headers[0]:<{adjusted_widths[0]}}{task_headers[1]:<{adjusted_widths[1]}}"
122
- print(header_row)
123
- print("-" * table_width)
124
-
125
- # Print task rows
126
- for row in rows:
127
- data_row = f"{str(row[0]):<{adjusted_widths[0]}}{str(row[1]):<{adjusted_widths[1]}}"
128
- print(data_row)
129
-
130
- print("-" * table_width)
131
- # Show task count
263
+ print()
264
+ print(
265
+ bold(
266
+ f"{'task':<{col_task}}{sep}"
267
+ f"{'endpoint':<{col_endpoint}}{sep}"
268
+ f"{'description':<{col_desc}}"
269
+ )
270
+ )
271
+ print(bold("-" * separator_width))
272
+
273
+ # Print task rows - use grey for task descriptions
274
+ for r in rows:
275
+ line = (
276
+ f"{_truncate(r['task'], col_task):<{col_task}}{sep}"
277
+ f"{_truncate(r['endpoint'], col_endpoint):<{col_endpoint}}{sep}"
278
+ f"{_truncate(r['description'], col_desc):<{col_desc}}"
279
+ )
280
+ print(grey(line))
281
+
282
+ print(bold("-" * separator_width))
283
+ # Show task count - grey for count text
132
284
  task_count = len(rows)
133
- print(f" {task_count} task{'s' if task_count != 1 else ''} available")
134
- print("=" * table_width)
285
+ task_word = "task" if task_count == 1 else "tasks"
286
+ print(f" {grey(f'{task_count} {task_word} available')}")
287
+ print(bold("=" * separator_width))
135
288
 
136
289
  print()
@@ -24,6 +24,7 @@ import nemo_evaluator_launcher.cli.info as info
24
24
  import nemo_evaluator_launcher.cli.kill as kill
25
25
  import nemo_evaluator_launcher.cli.logs as logs
26
26
  import nemo_evaluator_launcher.cli.ls_runs as ls_runs
27
+ import nemo_evaluator_launcher.cli.ls_task as ls_task
27
28
  import nemo_evaluator_launcher.cli.ls_tasks as ls_tasks
28
29
  import nemo_evaluator_launcher.cli.run as run
29
30
  import nemo_evaluator_launcher.cli.status as status
@@ -49,6 +50,7 @@ def is_verbose_enabled(args) -> bool:
49
50
  "tasks_alias",
50
51
  "tasks",
51
52
  "runs",
53
+ "task",
52
54
  "export",
53
55
  ]
54
56
  for subcmd in subcommands:
@@ -159,6 +161,14 @@ def create_parser() -> ArgumentParser:
159
161
  )
160
162
  ls_runs_parser.add_arguments(ls_runs.Cmd, dest="runs")
161
163
 
164
+ # ls task (task details)
165
+ ls_task_parser = ls_sub.add_parser(
166
+ "task",
167
+ help="Show task details",
168
+ description="Show detailed information about a specific task",
169
+ )
170
+ ls_task_parser.add_arguments(ls_task.Cmd, dest="task")
171
+
162
172
  # Export subcommand
163
173
  export_parser = subparsers.add_parser(
164
174
  "export",
@@ -220,12 +230,17 @@ def main() -> None:
220
230
  args.kill.execute()
221
231
  elif args.command == "ls":
222
232
  # Dispatch nested ls subcommands
223
- if args.ls_command is None or args.ls_command == "tasks":
224
- # Default to tasks when no subcommand specified
233
+ if args.ls_command == "tasks":
234
+ # When explicitly "ls tasks", use args.tasks (has correct from_container)
235
+ args.tasks.execute()
236
+ elif args.ls_command is None:
237
+ # When just "ls" (no subcommand), use args.tasks_alias
225
238
  if hasattr(args, "tasks_alias"):
226
239
  args.tasks_alias.execute()
227
240
  else:
228
241
  args.tasks.execute()
242
+ elif args.ls_command == "task":
243
+ args.task.execute()
229
244
  elif args.ls_command == "runs":
230
245
  args.runs.execute()
231
246
  elif args.command == "export":
@@ -27,7 +27,6 @@ from nemo_evaluator_launcher.common.printing_utils import (
27
27
  green,
28
28
  magenta,
29
29
  red,
30
- yellow,
31
30
  )
32
31
 
33
32
 
@@ -42,20 +41,6 @@ class Cmd:
42
41
  "help": "Full path to config file. Uses Hydra by default (--config-mode=hydra). Use --config-mode=raw to load directly (bypasses Hydra)."
43
42
  },
44
43
  )
45
- config_name: str = field(
46
- default="default",
47
- alias=["-c", "--config-name"],
48
- metadata={
49
- "help": "Config name to use. Consult `nemo_evaluator_launcher.configs`"
50
- },
51
- )
52
- config_dir: str | None = field(
53
- default=None,
54
- alias=["-d", "--config-dir"],
55
- metadata={
56
- "help": "Path to user config directory. If provided, searches here first, then falls back to internal configs."
57
- },
58
- )
59
44
  config_mode: Literal["hydra", "raw"] = field(
60
45
  default="hydra",
61
46
  alias=["--config-mode"],
@@ -77,6 +62,15 @@ class Cmd:
77
62
  alias=["-n", "--dry-run"],
78
63
  metadata={"help": "Do not run the evaluation, just print the config."},
79
64
  )
65
+ tasks: list[str] = field(
66
+ default_factory=list,
67
+ action="append",
68
+ nargs="?",
69
+ alias=["-t"],
70
+ metadata={
71
+ "help": "Run only specific tasks from the config. Example: -t ifeval -t gsm8k"
72
+ },
73
+ )
80
74
  config_output: str | None = field(
81
75
  default=None,
82
76
  alias=["--config-output"],
@@ -85,12 +79,31 @@ class Cmd:
85
79
  },
86
80
  )
87
81
 
82
+ def _parse_requested_tasks(self) -> list[str]:
83
+ """Parse -t arguments into a list of task names.
84
+
85
+ Handles None values that can be appended when using nargs="?" with action="append".
86
+ """
87
+ requested_tasks = []
88
+ for task_arg in self.tasks:
89
+ # Skip None or empty values (can happen with nargs="?")
90
+ if not task_arg:
91
+ continue
92
+ task_name = task_arg.strip()
93
+ if task_name and task_name not in requested_tasks:
94
+ requested_tasks.append(task_name)
95
+ return requested_tasks
96
+
88
97
  def execute(self) -> None:
89
98
  # Import heavy dependencies only when needed
90
99
  import yaml
91
100
  from omegaconf import OmegaConf
92
101
 
93
- from nemo_evaluator_launcher.api.functional import RunConfig, run_eval
102
+ from nemo_evaluator_launcher.api.functional import (
103
+ RunConfig,
104
+ filter_tasks,
105
+ run_eval,
106
+ )
94
107
 
95
108
  # Validate config_mode value
96
109
  if self.config_mode not in ["hydra", "raw"]:
@@ -104,17 +117,12 @@ class Cmd:
104
117
  "--config-mode=raw requires --config to be specified. Raw mode loads config files directly."
105
118
  )
106
119
 
120
+ # Parse requested tasks if -t is specified
121
+ requested_tasks = self._parse_requested_tasks() if self.tasks else None
122
+
107
123
  # Load configuration either from Hydra or directly from a config file
108
124
  if self.config_mode == "raw" and self.config:
109
125
  # Validate that raw config loading is not used with other config options
110
- if self.config_name != "default":
111
- raise ValueError(
112
- "Cannot use --config-mode=raw with --config-name. Raw mode only works with --config."
113
- )
114
- if self.config_dir is not None:
115
- raise ValueError(
116
- "Cannot use --config-mode=raw with --config-dir. Raw mode only works with --config."
117
- )
118
126
  if self.override:
119
127
  raise ValueError(
120
128
  "Cannot use --config-mode=raw with --override. Raw mode only works with --config."
@@ -127,26 +135,21 @@ class Cmd:
127
135
  # Create RunConfig from the loaded data
128
136
  config = OmegaConf.create(config_dict)
129
137
  else:
130
- # Handle --config parameter: split path into config_dir and config_name for Hydra
131
- if self.config:
132
- if self.config_name != "default":
133
- raise ValueError("Cannot use --config with --config-name")
134
- if self.config_dir is not None:
135
- raise ValueError("Cannot use --config with --config-dir")
136
- config_path = pathlib.Path(self.config)
137
- config_dir = str(config_path.parent)
138
- config_name = str(config_path.stem)
139
- else:
140
- config_dir = self.config_dir
141
- config_name = self.config_name
142
-
143
138
  # Load the complete Hydra configuration
144
139
  config = RunConfig.from_hydra(
145
- config_dir=config_dir,
146
- config_name=config_name,
140
+ config=self.config,
147
141
  hydra_overrides=self.override,
148
142
  )
149
143
 
144
+ # Apply task filtering if -t is specified
145
+ if requested_tasks:
146
+ config = filter_tasks(config, requested_tasks)
147
+ logger.info(
148
+ "Running filtered tasks",
149
+ count=len(config.evaluation.tasks),
150
+ tasks=[t.name for t in config.evaluation.tasks],
151
+ )
152
+
150
153
  try:
151
154
  invocation_id = run_eval(config, self.dry_run)
152
155
  except Exception as e:
@@ -243,16 +246,4 @@ class Cmd:
243
246
  )
244
247
  )
245
248
 
246
- # Warn if both config_dir and config_name are provided (and config_name is not default)
247
- if (
248
- self.config is None
249
- and self.config_dir is not None
250
- and self.config_name != "default"
251
- ):
252
- joint_path = pathlib.Path(self.config_dir) / f"{self.config_name}.yaml"
253
- print(
254
- yellow(
255
- f"Warning: Using --config-dir and --config-name together is deprecated. "
256
- f"Please use --config {joint_path} instead."
257
- )
258
- )
249
+ # Done.
@@ -0,0 +1,61 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ """Container metadata management: registries, intermediate representations, and loading."""
17
+
18
+ from nemo_evaluator_launcher.common.container_metadata.intermediate_repr import (
19
+ HarnessIntermediateRepresentation,
20
+ TaskIntermediateRepresentation,
21
+ load_harnesses_and_tasks_from_tasks_file,
22
+ load_tasks_from_tasks_file,
23
+ )
24
+ from nemo_evaluator_launcher.common.container_metadata.registries import (
25
+ DockerRegistryHandler,
26
+ create_authenticator,
27
+ )
28
+ from nemo_evaluator_launcher.common.container_metadata.utils import (
29
+ parse_container_image,
30
+ )
31
+
32
+ __all__ = [
33
+ "DockerRegistryHandler",
34
+ "create_authenticator",
35
+ "HarnessIntermediateRepresentation",
36
+ "TaskIntermediateRepresentation",
37
+ "load_harnesses_and_tasks_from_tasks_file",
38
+ "load_tasks_from_tasks_file",
39
+ "parse_container_image",
40
+ ]
41
+
42
+ # Optional imports:
43
+ # `loading` pulls in `nemo_evaluator` (and deps like `pydantic`). Keep IR-only
44
+ # workflows (e.g., docs autogen) usable without requiring the full stack.
45
+ try:
46
+ from nemo_evaluator_launcher.common.container_metadata.loading import ( # noqa: F401
47
+ extract_framework_yml,
48
+ load_tasks_from_container,
49
+ parse_framework_to_irs,
50
+ )
51
+
52
+ __all__.extend(
53
+ [
54
+ "extract_framework_yml",
55
+ "load_tasks_from_container",
56
+ "parse_framework_to_irs",
57
+ ]
58
+ )
59
+ except ModuleNotFoundError:
60
+ # Allow importing this package for IR-only workflows (docs autogen, etc.)
61
+ pass