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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ To run a blueprint directly from Python:
autoconnect(module_a(), module_b(), module_c()).build().loop()
```

Expose as a module-level variable for `dimos run` to find it. Add to the registry by running `pytest dimos/robot/test_all_blueprints_generation.py`.
For in-repo DimOS blueprints, expose a module-level variable for `dimos run` to find it and add it to the built-in registry by running `pytest dimos/robot/test_all_blueprints_generation.py`. External packages should not edit `all_blueprints.py`; expose runnable blueprints through installed Python package entry points in the `dimos.blueprints` group.

### GlobalConfig

Expand Down Expand Up @@ -366,13 +366,13 @@ Code style rules:

## `all_blueprints.py` is auto-generated

`dimos/robot/all_blueprints.py` is generated by `test_all_blueprints_generation.py`. After adding or renaming blueprints:
`dimos/robot/all_blueprints.py` is generated by `test_all_blueprints_generation.py` for in-repo built-in blueprints and modules. After adding or renaming built-in blueprints:

```bash
pytest dimos/robot/test_all_blueprints_generation.py
```

CI asserts the file is current — if it's stale, CI fails.
CI asserts the file is current — if it's stale, CI fails. Externally packaged blueprints are discovered from installed `dimos.blueprints` entry points and do not require regenerating this file.

---

Expand Down
23 changes: 23 additions & 0 deletions dimos/core/coordination/test_module_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from dimos.core.module import Module
from dimos.core.stream import In, Out
from dimos.msgs.sensor_msgs.Image import Image
import dimos.robot.get_all_blueprints as resolver
from dimos.spec.utils import Spec

# Disable Rerun for tests (prevents viewer spawn and gRPC flush errors)
Expand Down Expand Up @@ -94,6 +95,10 @@ class TargetModule(Module):
remapped_data: In[Data1]


class ExternalNameLoadModule(Module):
pass


# ModuleRef / RPC tests
class CalculatorSpec(Spec, Protocol):
@rpc
Expand Down Expand Up @@ -790,3 +795,21 @@ def test_list_module_names(dynamic_coordinator) -> None:
dynamic_coordinator.load_module(ModuleA)
dynamic_coordinator.load_module(ModuleC)
assert set(dynamic_coordinator.list_module_names()) == {"ModuleA", "ModuleC"}


def test_load_blueprint_by_name_uses_shared_resolver(
monkeypatch: pytest.MonkeyPatch, mocker
) -> None:
expected_blueprint = ExternalNameLoadModule.blueprint()

def fake_get_by_name(name: str):
assert name == "my-test-stack.demo"
return expected_blueprint

coordinator = ModuleCoordinator()
load_blueprint = mocker.patch.object(ModuleCoordinator, "load_blueprint")
monkeypatch.setattr(resolver, "get_by_name", fake_get_by_name)

coordinator.load_blueprint_by_name("my-test-stack.demo")

load_blueprint.assert_called_once_with(expected_blueprint)
3 changes: 2 additions & 1 deletion dimos/core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
ClassVar,
Literal,
Protocol,
TypeGuard,
get_args,
get_origin,
get_type_hints,
Expand Down Expand Up @@ -813,7 +814,7 @@ def connect_stream(self, input_name: str, remote_stream: RemoteOut[T]): # type:
ModuleSpec = tuple[type[ModuleBase], GlobalConfig, dict[str, Any]]


def is_module_type(value: Any) -> bool:
def is_module_type(value: object) -> TypeGuard[type[Module]]:
try:
return inspect.isclass(value) and issubclass(value, Module)
except Exception:
Expand Down
12 changes: 12 additions & 0 deletions dimos/porcelain/test_dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ def test_resolve_string_name():
assert bp is not None


def test_resolve_external_string_name_uses_shared_resolver(monkeypatch):
expected = StressTestModule.blueprint()

def fake_get_by_name(name: str):
assert name == "my-test-stack.demo"
return expected

monkeypatch.setattr("dimos.porcelain.dimos.get_by_name", fake_get_by_name)

assert _resolve_target("my-test-stack.demo") is expected


def test_resolve_unknown_string():
with pytest.raises(ValueError, match="Unknown"):
_resolve_target("nonexistent-blueprint-xyz")
Expand Down
19 changes: 18 additions & 1 deletion dimos/robot/cli/dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,10 +609,27 @@ def show_config(ctx: typer.Context) -> None:
def list_blueprints() -> None:
"""List all available blueprints."""
from dimos.robot.all_blueprints import all_blueprints
from dimos.robot.external_blueprints import (
ExternalBlueprintError,
list_external_blueprint_names,
)

blueprints = [name for name in all_blueprints.keys() if not name.startswith("demo-")]
typer.echo("Built-in blueprints:")
for blueprint_name in sorted(blueprints):
typer.echo(blueprint_name)
typer.echo(f" {blueprint_name}")

try:
external_blueprints = list_external_blueprint_names()
except ExternalBlueprintError as exc:
typer.echo(typer.style(str(exc), fg=typer.colors.RED), err=True)
raise typer.Exit(1) from exc

if external_blueprints:
typer.echo("")
typer.echo("External blueprints:")
for blueprint_name in external_blueprints:
typer.echo(f" {blueprint_name}")


@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
Expand Down
73 changes: 72 additions & 1 deletion dimos/robot/cli/test_dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
from typing import Literal

import pytest
from typer.testing import CliRunner

from dimos.core.coordination.blueprints import autoconnect
from dimos.core.module import Module, ModuleConfig
from dimos.robot.cli.dimos import _normalize_simulation_argv, arg_help
from dimos.robot import external_blueprints as external
from dimos.robot.cli.dimos import _normalize_simulation_argv, arg_help, main


@pytest.mark.parametrize(
Expand Down Expand Up @@ -144,3 +146,72 @@ class TestModule(Module):
" * testmodule.spam: str (default: eggs)",
"",
]


def test_list_blueprints_groups_builtin_and_external(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
external,
"list_external_blueprint_names",
lambda: ["my-test-stack.demo", "my-test-stack.keyboard-teleop"],
)

result = CliRunner().invoke(main, ["list"])

assert result.exit_code == 0
assert "Built-in blueprints:" in result.output
assert " unitree-go2" in result.output
assert "demo-agent" not in result.output
assert "External blueprints:" in result.output
assert " my-test-stack.demo" in result.output
assert " my-test-stack.keyboard-teleop" in result.output


def test_list_blueprints_without_external_names(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(external, "list_external_blueprint_names", lambda: [])

result = CliRunner().invoke(main, ["list"])

assert result.exit_code == 0
assert "Built-in blueprints:" in result.output
assert "External blueprints:" not in result.output


def test_list_blueprints_reports_external_discovery_errors(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def raise_error() -> list[str]:
raise external.ExternalBlueprintError("external metadata is invalid")

monkeypatch.setattr(external, "list_external_blueprint_names", raise_error)

result = CliRunner().invoke(main, ["list"])

assert result.exit_code == 1
assert "external metadata is invalid" in result.output


def test_run_reports_external_resolution_errors(monkeypatch: pytest.MonkeyPatch) -> None:
def raise_error(name: str):
raise external.ExternalBlueprintError(
"Failed to load external blueprint "
f"{name!r} from entry point 'my_test_stack.missing:demo_blueprint': "
"ModuleNotFoundError: No module named 'my_test_stack.missing'"
)

monkeypatch.setattr(
"dimos.robot.get_all_blueprints.resolve_external_blueprint_by_name",
raise_error,
)

result = CliRunner().invoke(main, ["run", "my-test-stack.demo"])

assert result.exit_code == 1
assert "Failed to load external blueprint 'my-test-stack.demo'" in result.output
assert "my_test_stack.missing:demo_blueprint" in result.output


def test_run_reports_unknown_bare_blueprint() -> None:
result = CliRunner().invoke(main, ["run", "missing-bare-blueprint"])

assert result.exit_code == 1
assert "Unknown blueprint or module: missing-bare-blueprint" in result.output
Loading
Loading