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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ We can now run this model using the plugboard CLI with the command:
plugboard process run my-model.yaml
```

Process fields can be overridden at run time with repeated `--param` / `-p` flags. Use
`process.default.parameter.<name>` for process parameters or
`component.<name>.<arg|initial_value|parameter>.<field>` for component fields. Values are parsed
as YAML:

```shell
plugboard process run my-model.yaml --param process.default.parameter.scale=2.0 -p component.a.arg.enabled=true
```

## 📖 Documentation

For more information including a detailed API reference and step-by-step usage examples, refer to the [documentation site](https://docs.plugboard.dev). We recommend diving into the [tutorials](https://docs.plugboard.dev/latest/examples/tutorials/hello-world/) for a step-by-step guide to getting started.
Expand Down
6 changes: 6 additions & 0 deletions docs/examples/tutorials/hello-world.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ We can now run this model using the plugboard CLI with the command:
plugboard process run model.yaml
```

You can override process parameters or component fields at run time without editing the YAML:

```shell
plugboard process run model.yaml --param process.default.parameter.scale=2.0 -p component.a.arg.iters=10
```

You should see that an output `.txt` file has been created, showing the the model as run successfully. Congratulations - you have built and run your first Plugboard model!

In the following tutorials we will build up some more complex components and processes to demonstrate the power of the framework.
11 changes: 11 additions & 0 deletions docs/examples/tutorials/more-complex-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ Now we can supply the `scale` value when we create the [`Process`][plugboard.pro
1. Supply a dictionary of common parameter values to the `Process` here. They are accessible from within all components.
2. If you need to override a parameter on a specific component, you can supply it via the component-level `parameters` argument.

When running a YAML-defined model from the CLI you can override process parameters or component
arguments, initial values, and parameters without editing the file. Repeat `--param` / `-p` as
`name=value` pairs, where `name` is
`process.default.parameter.<name>` or
`component.<name>.<arg|initial_value|parameter>.<field>`. Values are parsed as YAML, so numbers,
booleans, lists and mappings keep their natural types:

```shell
plugboard process run parameters.yaml --param process.default.parameter.scale=2.0 -p component.scale.arg.factor=2
```

## Next steps

You've now learned how to build up complex model layouts in Plugboard. In the next tutorial we'll show how powerful a Plugboard model can be as we start to include different types of [`Component`][plugboard.component.Component].
6 changes: 6 additions & 0 deletions plugboard-schemas/plugboard_schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
Status,
)
from .tune import (
BaseFieldSpec,
CategoricalParameterSpec,
Direction,
FloatParameterSpec,
Expand All @@ -50,13 +51,16 @@
TuneArgsDict,
TuneArgsSpec,
TuneSpec,
override_parameter,
parse_parameter_name,
)


__version__ = version(__package__)


__all__ = [
"BaseFieldSpec",
"CategoricalParameterSpec",
"ComponentSpec",
"ComponentArgsDict",
Expand Down Expand Up @@ -100,4 +104,6 @@
"validate_input_events",
"validate_no_unresolved_cycles",
"validate_process",
"override_parameter",
"parse_parameter_name",
]
68 changes: 68 additions & 0 deletions plugboard-schemas/plugboard_schemas/tune.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydantic import Field, PositiveInt, ValidationInfo, field_validator, model_validator

from ._common import PlugboardBaseModel
from .process import ProcessSpec


class OptunaSpec(PlugboardBaseModel):
Expand Down Expand Up @@ -77,6 +78,73 @@ def full_name(self) -> str:
)


def parse_parameter_name(name: str) -> BaseFieldSpec:
"""Parse a tunable parameter's fully-qualified name.

Args:
name: A parameter name such as ``component.my_component.arg.scale``.

Returns:
The corresponding field specification.

Raises:
ValueError: If the name does not identify an overridable parameter.
"""
try:
object_type, object_name, field_type, field_name = name.split(".", maxsplit=3)
except ValueError as e:
raise ValueError(
"Parameter name must have the format "
"'component.<name>.<arg|initial_value|parameter>.<field>' or "
"'process.default.parameter.<field>'."
) from e

if not object_name or not field_name:
raise ValueError("Parameter names must include an object name and field name.")
if object_type not in {"component", "process"}:
raise ValueError(f"Unknown object type {object_type!r} for parameter override.")
if object_type == "process" and object_name != "default":
raise ValueError("Process parameter names must use 'process.default'.")
if (object_type == "component" and field_type not in {"arg", "initial_value", "parameter"}) or (
object_type == "process" and field_type != "parameter"
):
raise ValueError(f"Parameter name {name!r} does not identify an overridable parameter.")
return BaseFieldSpec(
object_type=object_type,
object_name=None if object_type == "process" else object_name,
field_type=field_type,
field_name=field_name,
)


def override_parameter(process: ProcessSpec, param: BaseFieldSpec, value: _t.Any) -> None:
"""Override a parameter or initial value in a process specification.

Args:
process: The process specification to update.
param: The field to override.
value: The replacement value.

Raises:
ValueError: If the target component is not present in the process.
"""
if param.object_type == "component":
try:
component = next(c for c in process.args.components if c.args.name == param.object_name)
except StopIteration:
raise ValueError(f"Component {param.object_name} not found in process.")
if param.field_type == "arg":
setattr(component.args, param.field_name, value)
elif param.field_type == "initial_value":
component.args.initial_values[param.field_name] = value
elif param.field_type == "parameter":
component.args.parameters[param.field_name] = value
elif param.object_type == "process":
process.args.parameters[param.field_name] = value
else:
raise ValueError(f"Unknown object type {param.object_type} for parameter override.")


class ObjectiveSpec(BaseFieldSpec):
"""Specification for an objective field."""

Expand Down
10 changes: 5 additions & 5 deletions plugboard/cli/ai/skills/run-process-scenario/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ Create or update a YAML config for the requested scenario, then run it with `plu

1. Make sure a YAML config exists for the model. If the model only exists in Python, create the YAML first by using the `create-yaml-config` skill at `../create-yaml-config/SKILL.md`.
2. Ask for any missing scenario inputs before running anything.
3. Update the YAML config with the exact parameter values the user requested.
3. Prefer overriding process parameters or component fields on the CLI with repeated `--param` / `-p` `name=value` flags when only values change for a scenario. Use `process.default.parameter.<name>` or `component.<name>.<arg|initial_value|parameter>.<field>`. Edit the YAML when component structure, connectors, or defaults need to change.
4. Preserve a clear component structure that matches the real-world model. Do not collapse multiple entities into one component just to make the config shorter.
5. Validate the updated YAML against `plugboard_schemas.ConfigSpec` before running it.
6. Run:
5. Validate the YAML against `plugboard_schemas.ConfigSpec` before running it.
6. Run, for example:

```sh
plugboard process run path/to/model.yaml
plugboard process run path/to/model.yaml --param process.default.parameter.scale=2.0 -p component.simulation.arg.max_iters=10
```

7. Report what configuration was used, what validation was performed, what command was run, and the key outputs or generated artifacts.
7. Report what configuration was used, what validation was performed, what command was run (including any `--param` overrides), and the key outputs or generated artifacts.
8. If the scenario required new parameters, confirm that the final config remains YAML-friendly and reusable.

## Output
Expand Down
84 changes: 83 additions & 1 deletion plugboard/cli/process/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@

from plugboard.diagram import MermaidDiagram
from plugboard.process import Process, ProcessBuilder
from plugboard.schemas import ConfigSpec, validate_process
from plugboard.schemas import (
BaseFieldSpec,
ConfigSpec,
override_parameter,
parse_parameter_name,
validate_process,
)
from plugboard.tune import Tuner
from plugboard.utils import add_sys_path, run_coro_sync

Expand All @@ -34,6 +40,63 @@ def _read_yaml(path: Path) -> ConfigSpec:
return ConfigSpec.model_validate(data)


def _parse_param_override(param: str) -> tuple[BaseFieldSpec, _t.Any]:
"""Parse a single ``name=value`` parameter override.

Values are decoded as YAML scalars/collections so that numbers, booleans,
nulls, lists and mappings keep their natural types. Plain strings are left
as strings. Use quotes around a value if YAML would otherwise coerce it
(for example ``name='"yes"'``).
"""
if "=" not in param:
raise typer.BadParameter(
f"Invalid parameter override {param!r}. Expected format: name=value.",
param_hint="--param",
)
key, _, raw_value = param.partition("=")
if not key:
raise typer.BadParameter(
f"Invalid parameter override {param!r}. Parameter name must not be empty.",
param_hint="--param",
)
if raw_value == "":
value = ""
else:
try:
value = msgspec.yaml.decode(raw_value.encode())
except msgspec.DecodeError as e:
raise typer.BadParameter(
f"Could not parse value for parameter {key!r}: {raw_value!r}.",
param_hint="--param",
) from e
try:
field = (
BaseFieldSpec(
object_type="process",
object_name=None,
field_type="parameter",
field_name=key,
)
if "." not in key
else parse_parameter_name(key)
)
except ValueError as e:
raise typer.BadParameter(
f"Invalid parameter name {key!r}: {e}",
param_hint="--param",
) from e
return field, value


def _apply_param_overrides(config: ConfigSpec, params: list[str] | None) -> None:
"""Apply CLI parameter overrides to the process configuration in place."""
if not params:
return
for param in params:
field, value = _parse_param_override(param)
override_parameter(config.plugboard.process, field, value)


def _build_process(config: ConfigSpec) -> Process:
process = ProcessBuilder.build(config.plugboard.process)
return process
Expand Down Expand Up @@ -102,6 +165,19 @@ def run(
),
),
] = None,
param: Annotated[
_t.Optional[list[str]],
typer.Option(
"--param",
"-p",
help=(
"Override a process or component field as name=value. Repeatable. "
"Use component.<name>.<arg|initial_value|parameter>.<field> or "
"process.default.parameter.<field>; a bare name targets a process parameter. "
"Values are parsed as YAML."
),
),
] = None,
) -> None:
"""Run a Plugboard process."""
config_spec = _read_yaml(config)
Expand All @@ -116,6 +192,12 @@ def run(
process_type # type: ignore[arg-type]
)

try:
_apply_param_overrides(config_spec, param)
except typer.BadParameter as e:
stderr.print(f"[red]{e}[/red]")
raise typer.Exit(2) from e

with Progress(
SpinnerColumn("arrow3"),
TextColumn("[progress.description]{task.description}"),
Expand Down
6 changes: 6 additions & 0 deletions plugboard/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
DEFAULT_STATE_BACKEND_CLS_PATH,
ENTITY_ID_REGEX,
RAY_STATE_BACKEND_CLS_PATH,
BaseFieldSpec,
CategoricalParameterSpec,
ComponentArgsDict,
ComponentArgsSpec,
Expand Down Expand Up @@ -46,6 +47,8 @@
TuneArgsDict,
TuneArgsSpec,
TuneSpec,
override_parameter,
parse_parameter_name,
simple_cycles,
validate_all_inputs_connected,
validate_input_events,
Expand All @@ -55,6 +58,7 @@


__all__ = [
"BaseFieldSpec",
"CategoricalParameterSpec",
"ComponentSpec",
"ComponentArgsDict",
Expand Down Expand Up @@ -96,4 +100,6 @@
"validate_input_events",
"validate_no_unresolved_cycles",
"validate_process",
"override_parameter",
"parse_parameter_name",
]
26 changes: 2 additions & 24 deletions plugboard/tune/tune.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ParameterSpec,
ProcessSpec,
Resource,
override_parameter,
)
from plugboard.utils import DI, run_coro_sync
from plugboard.utils.dependencies import depends_on_optional
Expand Down Expand Up @@ -174,29 +175,6 @@ def _build_parameter(
**parameter.model_dump(exclude={"type"})
)

@staticmethod
def _override_parameter(
process: ProcessSpec, param: ParameterSpec, value: _t.Any
) -> None: # pragma: no cover
if param.object_type == "component":
try:
component = next(
c for c in process.args.components if c.args.name == param.object_name
)
except StopIteration:
raise ValueError(f"Component {param.object_name} not found in process.")
if param.field_type == "arg":
setattr(component.args, param.field_name, value)
elif param.field_type == "initial_value":
component.args.initial_values[param.field_name] = value
elif param.field_type == "parameter":
component.args.parameters[param.field_name] = value
elif param.object_type == "process":
if param.field_type == "parameter":
process.args.parameters[param.field_name] = value
else:
raise ValueError(f"Unknown object type {param.object_type} for parameter override.")

@staticmethod
def _get_objective(process: Process, objective: ObjectiveSpec) -> _t.Any: # pragma: no cover
if objective.object_type != "component":
Expand Down Expand Up @@ -280,7 +258,7 @@ def fn(config: dict[str, _t.Any]) -> dict[str, _t.Any]: # pragma: no cover
# Custom search spaces may include intermediate parameters not in the Tuner
self._logger.warning("Parameter from config not found in Tuner", param=name)
continue
self._override_parameter(spec, self._parameters_dict[name], value)
override_parameter(spec, self._parameters_dict[name], value)

process = ProcessBuilder.build(spec)
result = {}
Expand Down
Loading
Loading