Skip to content
Closed
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
15 changes: 10 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ from dimos.agents.mcp.mcp_client import McpClient
from dimos.agents.mcp.mcp_server import McpServer

unitree_go2_agentic = autoconnect(
unitree_go2_spatial, # robot stack
McpServer.blueprint(), # HTTP MCP server — exposes all @skill methods on port 9990
McpClient.blueprint(), # LLM agent — fetches tools from McpServer
_common_agentic, # skill containers
unitree_go2_spatial, # robot stack
McpServer.blueprint(), # HTTP MCP server — exposes all @skill methods on port 9990
McpClient.blueprint(), # LLM agent — fetches tools from McpServer
_common_agentic, # skill containers
)
```

Expand Down Expand Up @@ -159,6 +159,7 @@ from dimos.core.stream import In, Out
from dimos.core.core import rpc
from dimos.msgs.sensor_msgs import Image


class MyModule(Module):
color_image: In[Image]
processed: Out[Image]
Expand Down Expand Up @@ -263,6 +264,7 @@ from dimos.agents.annotation import skill
from dimos.core.core import rpc
from dimos.core.module import Module


class MySkillContainer(Module):
@rpc
def start(self) -> None:
Expand All @@ -282,6 +284,7 @@ class MySkillContainer(Module):
"""
return f"Moving at {x} m/s for {duration}s"


my_skill_container = MySkillContainer.blueprint
```

Expand All @@ -303,13 +306,15 @@ To call methods on another module, declare a `Spec` Protocol and annotate an att
from typing import Protocol
from dimos.spec.utils import Spec


class NavigatorSpec(Spec, Protocol):
def set_goal(self, goal: PoseStamped) -> bool: ...
def cancel_goal(self) -> bool: ...


# my_skill_container.py
class MySkillContainer(Module):
_navigator: NavigatorSpec # injected by blueprint at build time
_navigator: NavigatorSpec # injected by blueprint at build time

@skill
def go_to(self, x: float, y: float) -> str:
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ from dimos.core.stream import In, Out
from dimos.msgs.geometry_msgs import Twist
from dimos.msgs.sensor_msgs import Image, ImageFormat


class RobotConnection(Module):
cmd_vel: In[Twist]
color_image: Out[Image]
Expand All @@ -273,13 +274,15 @@ class RobotConnection(Module):
self.color_image.publish(img)
time.sleep(0.2)


class Listener(Module):
color_image: In[Image]

@rpc
def start(self):
self.color_image.subscribe(lambda img: print(f"image {img.width}x{img.height}"))


if __name__ == "__main__":
autoconnect(
RobotConnection.blueprint(),
Expand Down
5 changes: 1 addition & 4 deletions dimos/agents/mcp/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ def _make_rpc_calls(
rpc_calls: dict[str, MagicMock] = {}
for skill in skills:
mock_call = MagicMock()
if skill.func_name in call_results:
mock_call.return_value = call_results[skill.func_name]
else:
mock_call.return_value = None
mock_call.return_value = call_results.get(skill.func_name, None)
rpc_calls[skill.func_name] = mock_call
return rpc_calls

Expand Down
44 changes: 23 additions & 21 deletions dimos/agents/mcp/test_tool_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,31 +121,33 @@ def _read_sse_notifications(
deadline = time.monotonic() + timeout
# A scalar timeout is requests' read timeout: the SSE stream stays open
# across the whole request, an idle read (no bytes for `timeout`s) trips it.
with requests.Session() as session:
with session.get(
with (
requests.Session() as session,
session.get(
url,
headers={"Accept": "text/event-stream"},
stream=True,
timeout=timeout,
) as response:
assert response.headers["content-type"].startswith("text/event-stream")
for raw in response.iter_lines():
if time.monotonic() > deadline:
break
line = raw.decode("utf-8", "replace")
if not line or not line.startswith("data: "):
continue
try:
data = json.loads(line[6:])
except json.JSONDecodeError:
continue
if data.get("method") not in _NOTIFICATION_METHODS:
continue
if tool_name is not None and _frame_tool_name(data) != tool_name:
continue
collected.append(data)
if len(collected) >= expected:
return collected
) as response,
):
assert response.headers["content-type"].startswith("text/event-stream")
for raw in response.iter_lines():
if time.monotonic() > deadline:
break
line = raw.decode("utf-8", "replace")
if not line or not line.startswith("data: "):
continue
try:
data = json.loads(line[6:])
except json.JSONDecodeError:
continue
if data.get("method") not in _NOTIFICATION_METHODS:
continue
if tool_name is not None and _frame_tool_name(data) != tool_name:
continue
collected.append(data)
if len(collected) >= expected:
return collected
return collected


Expand Down
3 changes: 1 addition & 2 deletions dimos/agents/testing/vlm_stream_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ def _on_image(self, image: Image) -> None:
now = time.time()
if self._last_image_wall_ts is not None:
gap = now - self._last_image_wall_ts
if gap > self._max_gap_seen_s:
self._max_gap_seen_s = gap
self._max_gap_seen_s = max(self._max_gap_seen_s, gap)
self._last_image_wall_ts = now
self._latest_image_wall_ts = now
self._latest_image = image
Expand Down
1 change: 0 additions & 1 deletion dimos/cli/agentspy/agentspy.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def __init__(self, topic: str = "/agent", max_messages: int = 1000) -> None:
self.transport = make_transport(self.topic)
self.transport.start()
self.callbacks: list[callable] = [] # type: ignore[valid-type]
pass

def start(self) -> None:
"""Start monitoring messages."""
Expand Down
2 changes: 1 addition & 1 deletion dimos/cli/dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ def list_blueprints() -> None:
list_external_blueprint_names,
)

blueprints = [name for name in all_blueprints.keys() if not name.startswith("demo-")]
blueprints = [name for name in all_blueprints if not name.startswith("demo-")]
typer.echo("Built-in blueprints:")
for blueprint_name in sorted(blueprints):
typer.echo(f" {blueprint_name}")
Expand Down
14 changes: 6 additions & 8 deletions dimos/cli/test_dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,14 @@
_with_relay_bridge,
main,
)
import dimos.cli.spy.run_spy as run_spy
import dimos.core.coordination.module_coordinator as module_coordinator
import dimos.core.coordination.process_lifecycle as process_lifecycle
from dimos.cli.spy import run_spy
from dimos.core import run_registry
from dimos.core.coordination import module_coordinator, process_lifecycle
from dimos.core.global_config import global_config
from dimos.core.module import Module, ModuleConfig
import dimos.core.run_registry as run_registry
from dimos.robot import external_blueprints as external
import dimos.robot.get_all_blueprints as get_all_blueprints
from dimos.robot import external_blueprints as external, get_all_blueprints
from dimos.utils import logging_config
import dimos.utils.cache as cache_utils
import dimos.utils.logging_config as logging_config


class RunConfigA(ModuleConfig):
Expand Down Expand Up @@ -135,7 +133,7 @@ def test_list_blueprints_groups_builtin_and_external(monkeypatch: pytest.MonkeyP


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

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

Expand Down
14 changes: 9 additions & 5 deletions dimos/codebase_checks/test_get_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,20 @@ def test_no_get_logger():
violations = find_get_logger_usages()
if violations:
report_lines = [
f"Found {len(violations)} forbidden use(s) of `logging.getLogger`. "
"Use `setup_logger` instead:",
(
f"Found {len(violations)} forbidden use(s) of `logging.getLogger`. "
"Use `setup_logger` instead:"
),
"",
" from dimos.utils.logging_config import setup_logger",
"",
" logger = setup_logger()",
"",
"If the usage is legitimate (e.g. standalone script, logging "
"infrastructure, or third-party logger suppression), add it to the "
"WHITELIST in dimos/codebase_checks/test_get_logger.py.",
(
"If the usage is legitimate (e.g. standalone script, logging "
"infrastructure, or third-party logger suppression), add it to the "
"WHITELIST in dimos/codebase_checks/test_get_logger.py."
),
"",
]
for path, lineno, text in violations:
Expand Down
8 changes: 5 additions & 3 deletions dimos/codebase_checks/test_no_sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,11 @@ def test_no_section_markers():
violations = find_section_markers()
if violations:
report_lines = [
f"Found {len(violations)} section marker(s). "
"If a file is too complicated to be understood without sections, "
'then the sections should be files. We don\'t need "subfiles".',
(
f"Found {len(violations)} section marker(s). "
"If a file is too complicated to be understood without sections, "
'then the sections should be files. We don\'t need "subfiles".'
),
"",
]
for path, lineno, text in violations:
Expand Down
7 changes: 5 additions & 2 deletions dimos/control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Tasks output commands in one of three modes:
```python
from dimos.control.task import ControlTask, ResourceClaim, JointCommandOutput, ControlMode


class PIDController:
def __init__(self, joints: list[str], priority: int = 10):
self._name = "pid_controller"
Expand Down Expand Up @@ -181,8 +182,10 @@ class PIDController:
# PID
self._integral = [i + e * state.dt for i, e in zip(self._integral, error)]
derivative = [(e - le) / state.dt for e, le in zip(error, self._last_error)]
output = [self.Kp*e + self.Ki*i + self.Kd*d
for e, i, d in zip(error, self._integral, derivative)]
output = [
self.Kp * e + self.Ki * i + self.Kd * d
for e, i, d in zip(error, self._integral, derivative)
]
self._last_error = error

return JointCommandOutput(
Expand Down
14 changes: 9 additions & 5 deletions dimos/control/benchmarking/tuning.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,11 +634,15 @@ class default 0.05). A profile-supplied ``min_speed_floor > 0``

caveats.extend(
[
f"Valid only for surface={provenance.surface!r}, "
f"mode={provenance.mode!r}, {provenance.sim_or_hw}. Re-run "
f"characterization on any surface or gait-mode change.",
f"Plant fitted from {provenance.characterization_session_dir or 'n/a'} "
f"on {provenance.date} (git {provenance.git_sha}).",
(
f"Valid only for surface={provenance.surface!r}, "
f"mode={provenance.mode!r}, {provenance.sim_or_hw}. Re-run "
f"characterization on any surface or gait-mode change."
),
(
f"Plant fitted from {provenance.characterization_session_dir or 'n/a'} "
f"on {provenance.date} (git {provenance.git_sha})."
),
]
)
valid_for_tuning = provenance.sim_or_hw == "hw"
Expand Down
6 changes: 3 additions & 3 deletions dimos/control/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class TaskConfig:

name: str
type: str = "trajectory"
joint_names: list[str] = field(default_factory=lambda: [])
joint_names: list[str] = field(default_factory=list)
priority: int = 10
auto_start: bool = False
params: dict[str, Any] = field(default_factory=dict)
Expand All @@ -104,8 +104,8 @@ class ControlCoordinatorConfig(ModuleConfig):
publish_robot_joint_states: bool = False
joint_state_frame_id: str = "coordinator"
log_ticks: bool = False
hardware: list[HardwareComponent] = field(default_factory=lambda: [])
tasks: list[TaskConfig] = field(default_factory=lambda: [])
hardware: list[HardwareComponent] = field(default_factory=list)
tasks: list[TaskConfig] = field(default_factory=list)


class ControlCoordinator(Module):
Expand Down
2 changes: 1 addition & 1 deletion dimos/control/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from typing import TYPE_CHECKING, Protocol, runtime_checkable

from dimos.control.components import JointName
from dimos.hardware.manipulators.spec import ControlMode as ControlMode
from dimos.hardware.manipulators.spec import ControlMode
from dimos.hardware.whole_body.spec import IMUState

if TYPE_CHECKING:
Expand Down
20 changes: 10 additions & 10 deletions dimos/control/tasks/g1_groot_wbc_task/g1_groot_wbc_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,16 +813,16 @@ def create_task(cfg: Any, hardware: Any) -> G1GrootWBCTask:
)

model_dir = Path(params.model_path)
kwargs: dict[str, Any] = dict(
balance_onnx=model_dir / "balance.onnx",
walk_onnx=model_dir / "walk.onnx",
joint_names=cfg.joint_names,
all_joint_names=hw.joint_names,
priority=cfg.priority,
auto_arm=params.auto_arm,
auto_dry_run=params.auto_dry_run,
default_ramp_seconds=params.default_ramp_seconds,
)
kwargs: dict[str, Any] = {
"balance_onnx": model_dir / "balance.onnx",
"walk_onnx": model_dir / "walk.onnx",
"joint_names": cfg.joint_names,
"all_joint_names": hw.joint_names,
"priority": cfg.priority,
"auto_arm": params.auto_arm,
"auto_dry_run": params.auto_dry_run,
"default_ramp_seconds": params.default_ramp_seconds,
}
if params.decimation is not None:
kwargs["decimation"] = params.decimation
return G1GrootWBCTask(
Expand Down
11 changes: 5 additions & 6 deletions dimos/control/tasks/path_follower_task/path_follower_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,7 @@ def _step_path_following(self) -> tuple[float, float, float]:
pos = np.array([self._current_odom.position.x, self._current_odom.position.y])

closest = self._windowed_closest(pos)
if closest > self._max_progress_idx:
self._max_progress_idx = closest
self._max_progress_idx = max(self._max_progress_idx, closest)

# Arrival is only valid AFTER we've traversed enough of the path.
# Otherwise closed paths (goal==start) would arrive on tick 1.
Expand Down Expand Up @@ -426,11 +425,11 @@ def configure(
lookahead_min: float | None = None,
lookahead_max: float | None = None,
lookahead_speed_scale: float | None = None,
max_yaw_rate: float | None | object = _UNSET,
max_yaw_rate: float | object | None = _UNSET,
forward_only: bool | None = None,
ff_config: FeedforwardGainConfig | None | object = _UNSET,
velocity_profile_config: VelocityProfileConfig | None | object = _UNSET,
external_profile_cap: PathSpeedCapProtocol | None | object = _UNSET,
ff_config: FeedforwardGainConfig | object | None = _UNSET,
velocity_profile_config: VelocityProfileConfig | object | None = _UNSET,
external_profile_cap: PathSpeedCapProtocol | object | None = _UNSET,
**ignored: Any,
) -> bool:
"""Override per-run knobs before start_path. ``ff_config``,
Expand Down
4 changes: 1 addition & 3 deletions dimos/control/test_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,9 +849,7 @@ def test_higher_priority_wins(self):
if values is None:
continue
for i, joint in enumerate(output.joint_names):
if joint not in winners:
winners[joint] = (claim.priority, values[i], output.mode, task.name)
elif claim.priority > winners[joint][0]:
if joint not in winners or claim.priority > winners[joint][0]:
winners[joint] = (claim.priority, values[i], output.mode, task.name)

assert winners["j1"][3] == "high_priority"
Expand Down
7 changes: 1 addition & 6 deletions dimos/core/coordination/blueprint_config/merging.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,7 @@ def merge_cli(
index = 0
while index < len(tokens):
token = tokens[index]
if (
token == "-o"
or token.startswith("-o=")
or token == "--option"
or token.startswith("--option=")
):
if token == "-o" or token.startswith(("-o=", "--option=")) or token == "--option":
raise BlueprintConfigError(
"The legacy -o/--option syntax was removed. "
"Use a blueprint option directly, for example "
Expand Down
Loading
Loading