From 1f5867af219cdb092bbfb993a788bcd043b760c9 Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Sat, 22 Nov 2025 04:11:23 +0200 Subject: [PATCH 1/2] error on conflicts --- dimos/core/blueprints.py | 37 +++++++++ dimos/core/test_blueprints.py | 77 +++++++++++++++++++ dimos/robot/all_blueprints.py | 1 + .../demo_error_on_name_conflicts.py | 36 +++++++++ 4 files changed, 151 insertions(+) create mode 100644 dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py diff --git a/dimos/core/blueprints.py b/dimos/core/blueprints.py index 45aa617571..1954f45721 100644 --- a/dimos/core/blueprints.py +++ b/dimos/core/blueprints.py @@ -110,6 +110,41 @@ def _all_name_types(self) -> set[tuple[str, type]]: def _is_name_unique(self, name: str) -> bool: return sum(1 for n, _ in self._all_name_types if n == name) == 1 + def _verify_no_name_conflicts(self) -> None: + name_to_types = defaultdict(set) + name_to_modules = defaultdict(list) + + for blueprint in self.blueprints: + for conn in blueprint.connections: + connection_name = self.remapping_map.get((blueprint.module, conn.name), conn.name) + name_to_types[connection_name].add(conn.type) + name_to_modules[connection_name].append((blueprint.module, conn.type)) + + conflicts = {} + for conn_name, types in name_to_types.items(): + if len(types) > 1: + modules_by_type = defaultdict(list) + for module, conn_type in name_to_modules[conn_name]: + modules_by_type[conn_type].append(module) + conflicts[conn_name] = modules_by_type + + if not conflicts: + return + + error_lines = ["Blueprint cannot start because there are conflicting connections."] + for name, modules_by_type in conflicts.items(): + type_entries = [] + for conn_type, modules in modules_by_type.items(): + for module in modules: + type_str = f"{conn_type.__module__}.{conn_type.__name__}" + module_str = module.__name__ + type_entries.append((type_str, module_str)) + if len(type_entries) >= 2: + locations = ", ".join(f"{type_} in {module}" for type_, module in type_entries) + error_lines.append(f" - '{name}' has conflicting types. {locations}") + + raise ValueError("\n".join(error_lines)) + def _deploy_all_modules( self, module_coordinator: ModuleCoordinator, global_config: GlobalConfig ) -> None: @@ -209,6 +244,8 @@ def build(self, global_config: GlobalConfig | None = None) -> ModuleCoordinator: global_config = GlobalConfig() global_config = global_config.model_copy(update=self.global_config_overrides) + self._verify_no_name_conflicts() + module_coordinator = ModuleCoordinator(global_config=global_config) module_coordinator.start() diff --git a/dimos/core/test_blueprints.py b/dimos/core/test_blueprints.py index d910e88d7d..373a6b8f6e 100644 --- a/dimos/core/test_blueprints.py +++ b/dimos/core/test_blueprints.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + from dimos.core.blueprints import ( ModuleBlueprint, ModuleBlueprintSet, @@ -185,6 +187,81 @@ def test_build_happy_path() -> None: coordinator.stop() +def test_name_conflicts_are_reported() -> None: + class ModuleA(Module): + shared_data: Out[Data1] = None + + class ModuleB(Module): + shared_data: In[Data2] = None + + blueprint_set = autoconnect(ModuleA.blueprint(), ModuleB.blueprint()) + + try: + blueprint_set._verify_no_name_conflicts() + pytest.fail("Expected ValueError to be raised") + except ValueError as e: + error_message = str(e) + assert "Blueprint cannot start because there are conflicting connections" in error_message + assert "'shared_data' has conflicting types" in error_message + assert "Data1 in ModuleA" in error_message + assert "Data2 in ModuleB" in error_message + + +def test_multiple_name_conflicts_are_reported() -> None: + class Module1(Module): + sensor_data: Out[Data1] = None + control_signal: Out[Data2] = None + + class Module2(Module): + sensor_data: In[Data2] = None + control_signal: In[Data3] = None + + blueprint_set = autoconnect(Module1.blueprint(), Module2.blueprint()) + + try: + blueprint_set._verify_no_name_conflicts() + pytest.fail("Expected ValueError to be raised") + except ValueError as e: + error_message = str(e) + assert "Blueprint cannot start because there are conflicting connections" in error_message + assert "'sensor_data' has conflicting types" in error_message + assert "'control_signal' has conflicting types" in error_message + + +def test_that_remapping_can_resolve_conflicts() -> None: + class Module1(Module): + data: Out[Data1] = None + + class Module2(Module): + data: Out[Data2] = None # Would conflict with Module1.data + + class Module3(Module): + data1: In[Data1] = None + data2: In[Data2] = None + + # Without remapping, should raise conflict error + blueprint_set = autoconnect(Module1.blueprint(), Module2.blueprint(), Module3.blueprint()) + + try: + blueprint_set._verify_no_name_conflicts() + pytest.fail("Expected ValueError due to conflict") + except ValueError as e: + assert "'data' has conflicting types" in str(e) + + # With remapping to resolve the conflict + blueprint_set_remapped = autoconnect( + Module1.blueprint(), Module2.blueprint(), Module3.blueprint() + ).remappings( + [ + (Module1, "data", "data1"), + (Module2, "data", "data2"), + ] + ) + + # Should not raise any exception after remapping + blueprint_set_remapped._verify_no_name_conflicts() + + def test_remapping() -> None: """Test that remapping connections works correctly.""" pubsub.lcm.autoconf() diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index 6121a8bc4d..8a57589cff 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -37,6 +37,7 @@ "demo-google-maps-skill": "dimos.agents2.skills.demo_google_maps_skill:demo_google_maps_skill", "demo-remapping": "dimos.robot.unitree_webrtc.demo_remapping:remapping", "demo-remapping-transport": "dimos.robot.unitree_webrtc.demo_remapping:remapping_and_transport", + "demo-error-on-name-conflicts": "dimos.robot.unitree_webrtc.demo_error_on_name_conflicts:blueprint", } diff --git a/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py b/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py new file mode 100644 index 0000000000..7fb54f4494 --- /dev/null +++ b/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py @@ -0,0 +1,36 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dimos.core.blueprints import autoconnect +from dimos.core.module import Module +from dimos.core.stream import In, Out + + +class Data1: + pass + + +class Data2: + pass + + +class ModuleA(Module): + shared_data: Out[Data1] = None + + +class ModuleB(Module): + shared_data: In[Data2] = None + + +blueprint = autoconnect(ModuleA.blueprint(), ModuleB.blueprint()) From 7e5f3e82876f6a2f63c837c10ba0863c81db72d6 Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Sat, 22 Nov 2025 06:33:10 +0200 Subject: [PATCH 2/2] fix --- .../demo_error_on_name_conflicts.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py b/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py index 7fb54f4494..d0f4093a4f 100644 --- a/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py +++ b/dimos/robot/unitree_webrtc/demo_error_on_name_conflicts.py @@ -13,6 +13,7 @@ # limitations under the License. from dimos.core.blueprints import autoconnect +from dimos.core.core import rpc from dimos.core.module import Module from dimos.core.stream import In, Out @@ -28,9 +29,25 @@ class Data2: class ModuleA(Module): shared_data: Out[Data1] = None + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + class ModuleB(Module): shared_data: In[Data2] = None + @rpc + def start(self) -> None: + super().start() + + @rpc + def stop(self) -> None: + super().stop() + blueprint = autoconnect(ModuleA.blueprint(), ModuleB.blueprint())