algorithm-nexus 0.2.1__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.
@@ -0,0 +1,158 @@
1
+ # Copyright IBM Corp. 2026
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Virtual environment management for benchmark validation."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+ try:
15
+ from rich.console import Console
16
+ except ImportError:
17
+ print(
18
+ "Error: CLI dependencies are not installed.\n"
19
+ "Please install them with: pip install algorithm-nexus[cli]",
20
+ file=sys.stderr,
21
+ )
22
+ sys.exit(1)
23
+
24
+ console = Console()
25
+
26
+
27
+ def create_temp_venv(system_site_packages: bool = True) -> Path:
28
+ """Create a temporary virtual environment using uv.
29
+
30
+ Args:
31
+ system_site_packages: Whether to give access to system site packages (default: True)
32
+
33
+ Returns:
34
+ Path to the created virtual environment
35
+
36
+ Raises:
37
+ subprocess.CalledProcessError: If venv creation fails
38
+ RuntimeError: If uv is not available
39
+ """
40
+
41
+ # Create temporary directory
42
+ temp_dir = tempfile.mkdtemp(prefix="nexus-validate-")
43
+ venv_path = Path(temp_dir) / "venv"
44
+
45
+ try:
46
+ console.print(f" Creating virtual environment with uv: {venv_path}")
47
+ cmd = ["uv", "venv", str(venv_path)]
48
+ if system_site_packages:
49
+ cmd.append("--system-site-packages")
50
+ subprocess.run( # noqa: S603
51
+ cmd,
52
+ check=True,
53
+ capture_output=True,
54
+ text=True,
55
+ )
56
+
57
+ return venv_path
58
+
59
+ except subprocess.CalledProcessError as e:
60
+ # Clean up on failure
61
+ shutil.rmtree(temp_dir, ignore_errors=True)
62
+ console.print(f"[red]Error creating venv:[/red] {e.stderr}")
63
+ raise
64
+
65
+
66
+ def install_packages(
67
+ venv_path: Path,
68
+ requirements: list[str],
69
+ verbose: bool = False,
70
+ ) -> bool:
71
+ """Install packages in the virtual environment using uv.
72
+
73
+ Args:
74
+ venv_path: Path to the virtual environment
75
+ requirements: List of package requirement specifiers
76
+ verbose: Whether to show installation output
77
+
78
+ Returns:
79
+ True if installation succeeded, False otherwise
80
+
81
+ Raises:
82
+ RuntimeError: If uv is not available
83
+ """
84
+ if not requirements:
85
+ return True
86
+
87
+ python_path = venv_path / "bin" / "python"
88
+
89
+ try:
90
+ console.print(f" Installing packages with uv: {', '.join(requirements)}")
91
+ subprocess.run( # noqa: S603
92
+ # Forcing installing ado-core for extra safety. However,
93
+ # ado-core is most probably already in the requirements of the benchmark
94
+ # package.
95
+ [ # noqa: S607
96
+ "uv",
97
+ "pip",
98
+ "install",
99
+ "--python",
100
+ str(python_path),
101
+ "ado-core",
102
+ *requirements,
103
+ ],
104
+ check=True,
105
+ capture_output=True,
106
+ text=True,
107
+ )
108
+
109
+ return True
110
+
111
+ except subprocess.CalledProcessError as e:
112
+ console.print(f"[red]Error installing packages:[/red] {e.stderr}")
113
+ return False
114
+
115
+
116
+ def run_in_venv(
117
+ venv_path: Path,
118
+ command: list[str],
119
+ capture_output: bool = True,
120
+ ) -> subprocess.CompletedProcess:
121
+ """Execute a command in the virtual environment context.
122
+
123
+ Args:
124
+ venv_path: Path to the virtual environment
125
+ command: Command to execute (without python prefix)
126
+ capture_output: Whether to capture stdout/stderr
127
+
128
+ Returns:
129
+ CompletedProcess instance with execution results
130
+ """
131
+ python_path = venv_path / "bin" / "python"
132
+ full_command = [str(python_path), *command]
133
+
134
+ return subprocess.run( # noqa: S603
135
+ full_command,
136
+ capture_output=capture_output,
137
+ text=True,
138
+ check=False,
139
+ )
140
+
141
+
142
+ def cleanup_venv(venv_path: Path) -> None:
143
+ """Remove the temporary virtual environment.
144
+
145
+ Args:
146
+ venv_path: Path to the virtual environment to remove
147
+ """
148
+ try:
149
+ # Remove the parent temp directory (which contains the venv)
150
+ temp_dir = venv_path.parent
151
+ shutil.rmtree(temp_dir, ignore_errors=True)
152
+ except Exception as e:
153
+ console.print(
154
+ f"[yellow]Warning:[/yellow] Failed to clean up venv at {venv_path}: {e}"
155
+ )
156
+
157
+
158
+ # Made with Bob
@@ -0,0 +1,408 @@
1
+ # Copyright IBM Corp. 2026
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Pydantic models for Nexus package YAML validation."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ import sys
10
+ from typing import Annotated, Literal
11
+
12
+ from pydantic import AfterValidator, computed_field
13
+
14
+ try:
15
+ from ado.schema.property import Property
16
+ from ado.schema.property_value import PropertyValue
17
+ from ado.schema.reference import ExperimentReference
18
+ from pydantic import BaseModel, ConfigDict, Field
19
+ except ImportError:
20
+ print(
21
+ "Error: CLI dependencies are not installed.\n"
22
+ "Please install them with: pip install algorithm-nexus[cli]",
23
+ file=sys.stderr,
24
+ )
25
+ sys.exit(1)
26
+
27
+
28
+ def validate_hf_model_id(v: str) -> str:
29
+ """Validate HuggingFace model ID format and constraints.
30
+ https://huggingface.co/docs/hub/en/security-sso-okta-scim#step-5-assign-users-or-groups
31
+
32
+ Rules:
33
+ - Only alphanumeric characters, dashes, dots, and underscores are accepted
34
+ - Double dashes (--) are forbidden
35
+ - Cannot start or end with a dash
36
+ - Digit-only names are not accepted (must contain at least one letter)
37
+ - Format: org-name/model-name where both follow the same rules except length
38
+ - Maximum length is 42 for org-name and 96 for model-name, minimum length is 2 for both
39
+ """
40
+ # Combined regex pattern that validates the entire model ID:
41
+ # - org-name: 2-42 chars, alphanumeric + dashes + dots + underscores, no double dashes, must have letter
42
+ # - model-name: 2-96 chars, same rules as org-name
43
+ # - Separated by exactly one slash
44
+ # Split validation into org and model parts to ensure each has at least one letter
45
+ pattern = re.compile(
46
+ r"^"
47
+ r"(?=(?:[a-zA-Z0-9._-]*[a-zA-Z][a-zA-Z0-9._-]*)/)" # org must contain at least one letter
48
+ r"(?!.*--)" # no double dashes in org
49
+ r"[a-zA-Z0-9][a-zA-Z0-9._-]{0,40}[a-zA-Z0-9]" # org-name: 2-42 chars
50
+ r"/" # separator
51
+ r"(?=(?:[a-zA-Z0-9._-]*[a-zA-Z][a-zA-Z0-9._-]*)$)" # model must contain at least one letter
52
+ r"(?!.*--)" # no double dashes in model
53
+ r"[a-zA-Z0-9][a-zA-Z0-9._-]{0,94}[a-zA-Z0-9]" # model-name: 2-96 chars
54
+ r"$"
55
+ )
56
+
57
+ if not pattern.match(v):
58
+ msg = "Model ID must be in format 'org-name/model-name' where org-name is 2-42 characters and model-name is 2-96 characters. Both must start and end with alphanumeric, contain only alphanumeric, dashes, dots, and underscores, not have double dashes, and contain at least one letter"
59
+ raise ValueError(msg)
60
+
61
+ return v
62
+
63
+
64
+ class BenchmarkPackage(BaseModel):
65
+ """Benchmark package registration in nexus.yaml.
66
+
67
+ Registers a benchmark package and the experiments it exposes.
68
+ Each package must follow the ADO custom experiment format.
69
+ """
70
+
71
+ model_config = ConfigDict(extra="forbid")
72
+
73
+ requirement_specifier: Annotated[
74
+ str,
75
+ Field(
76
+ min_length=1,
77
+ description="Python package requirement target for the benchmark package. May be a Python package name, a URL to a Python package or source repository, or a local path to a Python package within ./packages in the Nexus repository root.",
78
+ ),
79
+ ]
80
+ experiments: Annotated[
81
+ list[str],
82
+ Field(
83
+ min_length=1,
84
+ description="Experiment identifiers exposed by that benchmark package and made available to models in the Nexus package.",
85
+ ),
86
+ ]
87
+
88
+
89
+ class NexusPackageInfo(BaseModel):
90
+ """Package-level configuration."""
91
+
92
+ model_config = ConfigDict(extra="forbid")
93
+
94
+ name: Annotated[str, Field(min_length=1, description="Python package name")]
95
+ benchmark_packages: Annotated[
96
+ list[BenchmarkPackage] | None,
97
+ Field(
98
+ description="List of benchmark packages available to models in this package",
99
+ ),
100
+ ] = None
101
+
102
+
103
+ class VLLMPlugins(BaseModel):
104
+ """vLLM plugins configuration."""
105
+
106
+ model_config = ConfigDict(extra="forbid")
107
+
108
+ general: Annotated[
109
+ str | None,
110
+ Field(description="General vLLM plugin that loads the model class"),
111
+ ] = None
112
+ io_processors: Annotated[
113
+ list[str] | None,
114
+ Field(
115
+ min_length=1,
116
+ description="List of vLLM IO processor plugins supported by this model",
117
+ ),
118
+ ] = None
119
+
120
+
121
+ class VLLMConfig(BaseModel):
122
+ """vLLM serving configuration.
123
+
124
+ Should only be defined for models that require additional vLLM plugins
125
+ and belong to a Nexus Package targeting the product or candidate distribution variants.
126
+ """
127
+
128
+ model_config = ConfigDict(extra="forbid")
129
+
130
+ enabled: Annotated[
131
+ Literal[True],
132
+ Field(description="Whether vLLM serving is enabled for this model"),
133
+ ]
134
+ plugins: Annotated[
135
+ VLLMPlugins | None,
136
+ Field(description="vLLM plugins configuration"),
137
+ ] = None
138
+
139
+
140
+ class ModelInfo(BaseModel):
141
+ """Model-level configuration."""
142
+
143
+ model_config = ConfigDict(extra="forbid")
144
+
145
+ id: Annotated[
146
+ str,
147
+ Field(
148
+ min_length=5,
149
+ max_length=139,
150
+ description="Hugging Face model repository identifier",
151
+ ),
152
+ AfterValidator(validate_hf_model_id),
153
+ ]
154
+
155
+ owner: Annotated[
156
+ str | None,
157
+ Field(
158
+ # Validates the owner field against the GitHub username rules:
159
+ # https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/iam-configuration-reference/username-considerations-for-external-authentication
160
+ # - Only contains dashes and alphanumeric characters
161
+ # - Does not start or end with a dash
162
+ # - Does not contain consecutive dashes
163
+ # - Has a maximum length of 39 characters
164
+ pattern=r"^[a-zA-Z0-9]([a-zA-Z0-9]|-[a-zA-Z0-9]){0,38}$",
165
+ description="Model owner GitHub identifier. If omitted, ownership defaults to the Nexus package owner.",
166
+ ),
167
+ ] = None
168
+
169
+ vllm: Annotated[
170
+ VLLMConfig | None,
171
+ Field(
172
+ description="vLLM serving configuration. Only required for models that need additional vLLM plugins and belong to a Nexus Package targeting the product or candidate distribution variants.",
173
+ ),
174
+ ] = None
175
+
176
+
177
+ class AlgorithmNexusModelConfig(BaseModel):
178
+ """Root model.yaml structure."""
179
+
180
+ model_config = ConfigDict(extra="forbid")
181
+
182
+ model: Annotated[ModelInfo, Field(description="Model configuration")]
183
+
184
+
185
+ class AlgorithmNexusPackageConfig(BaseModel):
186
+ """Root nexus.yaml structure."""
187
+
188
+ model_config = ConfigDict(extra="forbid")
189
+
190
+ package: Annotated[
191
+ NexusPackageInfo, Field(description="Package-level configuration")
192
+ ]
193
+
194
+
195
+ class ValidationResult(BaseModel):
196
+ """Result of validating a benchmark instance."""
197
+
198
+ success: Annotated[bool, Field(description="Whether validation succeeded")]
199
+ instance_path: Annotated[str, Field(description="Path to the benchmark instance")]
200
+ errors: Annotated[list[str], Field(description="List of validation errors")]
201
+ warnings: Annotated[list[str], Field(description="List of validation warnings")]
202
+
203
+ @computed_field # type: ignore[prop-decorator]
204
+ @property
205
+ def status(self) -> str:
206
+ """Computed status based on success field."""
207
+ return "success" if self.success else "failed"
208
+
209
+
210
+ class BenchmarkExecutionResult(BaseModel):
211
+ """Result of executing a benchmark instance."""
212
+
213
+ instance_path: Annotated[str, Field(description="Path to the benchmark instance")]
214
+ status: Annotated[
215
+ Literal["success", "failed", "started", "unknown"],
216
+ Field(description="Execution status"),
217
+ ] = "unknown"
218
+ message: Annotated[
219
+ str, Field(description="Status message or error description")
220
+ ] = ""
221
+ space_id: Annotated[str | None, Field(description="Created discovery space ID")] = (
222
+ None
223
+ )
224
+ operation_id: Annotated[str | None, Field(description="Created operation ID")] = (
225
+ None
226
+ )
227
+ ray_job_id: Annotated[
228
+ str | None, Field(description="Ray job ID for remote execution")
229
+ ] = None
230
+
231
+
232
+ class ValidationReport(BaseModel):
233
+ """Aggregated result of validating all benchmark instances."""
234
+
235
+ instances: Annotated[
236
+ list[dict], Field(description="Per-instance validation result dicts")
237
+ ]
238
+ successful: Annotated[int, Field(description="Number of instances that passed")]
239
+ failed: Annotated[int, Field(description="Number of instances that failed")]
240
+ total: Annotated[
241
+ int,
242
+ Field(
243
+ description="Total number of instances discovered (may exceed successful + failed under --fail-fast)"
244
+ ),
245
+ ]
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # Logical Benchmark models
250
+ # ---------------------------------------------------------------------------
251
+
252
+
253
+ class FieldMapping(BaseModel):
254
+ """1-to-1 mapping of a benchmark property to an experiment property."""
255
+
256
+ model_config = ConfigDict(extra="forbid")
257
+
258
+ benchmark: Annotated[
259
+ Property,
260
+ Field(description="Canonical benchmark property."),
261
+ ]
262
+ experiment: Annotated[Property, Field(description="Experiment property.")]
263
+
264
+
265
+ class CategoricalValueMapping(BaseModel):
266
+ """Maps a categorical benchmark property value to a set of experiment property predicates."""
267
+
268
+ model_config = ConfigDict(extra="forbid")
269
+
270
+ categoricalValue: Annotated[
271
+ PropertyValue,
272
+ Field(description="The benchmark categorical value being mapped."),
273
+ ]
274
+ predicate: Annotated[
275
+ list[Property],
276
+ Field(
277
+ min_length=1,
278
+ description="Experiment property constraints that identify this categorical value.",
279
+ ),
280
+ ]
281
+
282
+
283
+ class MetricIdentifier(BaseModel):
284
+ """A reference to a canonical benchmark metric by its identifier string."""
285
+
286
+ model_config = ConfigDict(extra="forbid")
287
+
288
+ identifier: Annotated[
289
+ str,
290
+ Field(min_length=1, description="Metric identifier."),
291
+ ]
292
+
293
+
294
+ class MetricMapping(BaseModel):
295
+ """Maps a canonical benchmark metric name to an experiment metric name."""
296
+
297
+ model_config = ConfigDict(extra="forbid")
298
+
299
+ benchmark: Annotated[
300
+ MetricIdentifier,
301
+ Field(description="Canonical benchmark metric identifier."),
302
+ ]
303
+
304
+ experiment: Annotated[
305
+ MetricIdentifier,
306
+ Field(description="Experiment metric identifier."),
307
+ ]
308
+
309
+
310
+ class LogicalBenchmarkDefinition(BaseModel):
311
+ """Logical benchmark definition — the abstract problem description."""
312
+
313
+ model_config = ConfigDict(extra="forbid")
314
+
315
+ benchmarkIdentifier: Annotated[
316
+ str,
317
+ Field(
318
+ min_length=1, description="Canonical identifier for this logical benchmark."
319
+ ),
320
+ ]
321
+ description: Annotated[
322
+ str,
323
+ Field(
324
+ min_length=1,
325
+ description="Human-readable description of the abstract problem being evaluated.",
326
+ ),
327
+ ]
328
+ target: Annotated[
329
+ Property,
330
+ Field(
331
+ description="The property that identifies the quantity being benchmarked."
332
+ ),
333
+ ]
334
+ properties: Annotated[
335
+ list[Property],
336
+ Field(
337
+ min_length=1,
338
+ description="The properties on which this benchmark is evaluated.",
339
+ ),
340
+ ]
341
+ metrics: Annotated[
342
+ list[str] | None,
343
+ Field(description="Canonical metric names for this benchmark."),
344
+ ] = None
345
+ owner: Annotated[
346
+ str | None,
347
+ Field(
348
+ description="Team or individual responsible for maintaining this definition."
349
+ ),
350
+ ] = None
351
+
352
+
353
+ class BenchmarkBinding(BaseModel):
354
+ """Binding that maps an experiment's properties and metrics to a logical benchmark."""
355
+
356
+ model_config = ConfigDict(extra="forbid")
357
+
358
+ benchmarkIdentifier: Annotated[
359
+ str,
360
+ Field(
361
+ min_length=1,
362
+ description="The id of the logical benchmark this experiment targets.",
363
+ ),
364
+ ]
365
+ experiment: Annotated[
366
+ ExperimentReference,
367
+ Field(description="The ado ExperimentReference object."),
368
+ ]
369
+ targetMapping: Annotated[
370
+ str,
371
+ Field(
372
+ min_length=1,
373
+ description="The experiment property name that carries the benchmark target.",
374
+ ),
375
+ ]
376
+ staticFilters: Annotated[
377
+ list[PropertyValue] | None,
378
+ Field(
379
+ description="Static experiment property filters implicit in this logical benchmark."
380
+ ),
381
+ ] = None
382
+ propertyMapping: Annotated[
383
+ list[FieldMapping | CategoricalValueMapping] | None,
384
+ Field(
385
+ description="Maps experiment internal properties to canonical benchmark properties."
386
+ ),
387
+ ] = None
388
+ metricMapping: Annotated[
389
+ list[MetricMapping] | None,
390
+ Field(
391
+ description="Translates per-experiment metric names to canonical benchmark metric names."
392
+ ),
393
+ ] = None
394
+
395
+
396
+ class LogicalBenchmarkConfig(BaseModel):
397
+ """Root model for a logical benchmark YAML file (logicalBenchmark + optional bindings)."""
398
+
399
+ model_config = ConfigDict(extra="forbid")
400
+
401
+ logicalBenchmark: Annotated[
402
+ LogicalBenchmarkDefinition,
403
+ Field(description="The logical benchmark definition."),
404
+ ]
405
+ bindings: Annotated[
406
+ list[BenchmarkBinding] | None,
407
+ Field(description="Benchmark bindings declared in this file."),
408
+ ] = None