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 @@ -264,6 +265,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 @@ -283,6 +285,7 @@ class MySkillContainer(Module):
"""
return f"Moving at {x} m/s for {duration}s"


my_skill_container = MySkillContainer.blueprint
```

Expand All @@ -304,13 +307,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 @@ -253,6 +253,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 @@ -271,13 +272,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
2 changes: 1 addition & 1 deletion dimos/agents/harness/runtime/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from dimos.constants import HARNESS_STATE_DIR

JSON: TypeAlias = "str | int | float | bool | None | list[JSON] | dict[str, JSON]"
JSON: TypeAlias = "str | int | float | bool | list[JSON] | dict[str, JSON] | None"

HARNESS_EVENT_METHOD = "dimos/harness_event"
HARNESS_STATUS_METHOD = "dimos/harness_status"
Expand Down
18 changes: 12 additions & 6 deletions dimos/agents/harness/runtime/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,10 @@ def facts(inventory: Inventory) -> list[Fact]:
[
(
f"stream:{stream.module}.{stream.name}",
f"{stream.module}.{stream.name} {stream.direction} {stream.type} "
f"channel={stream.channel}",
(
f"{stream.module}.{stream.name} {stream.direction} {stream.type} "
f"channel={stream.channel}"
),
)
for stream in inventory.runtime.streams
],
Expand All @@ -462,17 +464,21 @@ def module_text(detail: ModuleDetail) -> str:
lines += ["", detail.doc.strip()]
lines += [
"",
f"RPCs ({len(detail.rpcs)}; call as app.get_module({detail.module!r}).RPC(...); "
"lifecycle RPCs omitted; add rpc=NAME for the full documentation):",
(
f"RPCs ({len(detail.rpcs)}; call as app.get_module({detail.module!r}).RPC(...); "
"lifecycle RPCs omitted; add rpc=NAME for the full documentation):"
),
]
for rpc in detail.rpcs:
lines.append(f" {rpc.signature}")
if rpc.doc:
lines.append(f" {rpc.doc}")
lines += [
"",
f"Streams ({len(detail.streams)}; a module joins one with a port of the same name "
"and type):",
(
f"Streams ({len(detail.streams)}; a module joins one with a port of the same name "
"and type):"
),
]
lines += [
f" {stream.module}.{stream.name} {stream.direction} {stream.type} "
Expand Down
5 changes: 3 additions & 2 deletions dimos/agents/harness/runtime/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,9 @@ def mcp_url(system: subprocess.Popen[bytes], port: int) -> str:
url = f"http://127.0.0.1:{port}/mcp"
client = Client(url)
_until(
lambda: _try(lambda: client.call_raw("echo", {"text": "up"}, {})["content"][0]["text"])
== "up",
lambda: (
_try(lambda: client.call_raw("echo", {"text": "up"}, {})["content"][0]["text"]) == "up"
),
"McpServer",
60.0,
)
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 @@ -32,10 +32,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 @@ -81,8 +81,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
12 changes: 5 additions & 7 deletions dimos/cli/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,8 @@ def _store(key: str) -> str:


def _load() -> str | None:
if kr := _keyring():
if key := kr.get_password(_KEYRING_SERVICE, _KEYRING_USER):
return cast("str", key)
if (kr := _keyring()) and (key := kr.get_password(_KEYRING_SERVICE, _KEYRING_USER)):
return cast("str", key)
try:
return CREDENTIALS_PATH.read_text().strip() or None
except OSError:
Expand All @@ -90,10 +89,9 @@ def _load() -> str | None:

def _forget() -> bool:
found = False
if kr := _keyring():
if kr.get_password(_KEYRING_SERVICE, _KEYRING_USER):
kr.delete_password(_KEYRING_SERVICE, _KEYRING_USER)
found = True
if (kr := _keyring()) and kr.get_password(_KEYRING_SERVICE, _KEYRING_USER):
kr.delete_password(_KEYRING_SERVICE, _KEYRING_USER)
found = True
if CREDENTIALS_PATH.exists():
CREDENTIALS_PATH.unlink()
found = True
Expand Down
2 changes: 1 addition & 1 deletion dimos/cli/commands/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,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
2 changes: 1 addition & 1 deletion dimos/cli/human/test_humancli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def fake_make_transport(name: str, *args: Any, **kwargs: Any) -> FakeTransport:
return transports[name]

monkeypatch.setattr(humancli, "make_transport", fake_make_transport)
monkeypatch.setattr(tool_stream, "subscribe", lambda cb: (lambda: None))
monkeypatch.setattr(tool_stream, "subscribe", lambda cb: lambda: None)

app = HumanCLIApp()
async with app.run_test(size=(120, 40)) as pilot:
Expand Down
21 changes: 9 additions & 12 deletions dimos/cli/test_dimos.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,18 @@
from pytest_mock import MockerFixture
from typer.testing import CliRunner

import dimos.cli.commands.info as info
import dimos.cli.commands.lifecycle as lifecycle
from dimos.cli.commands import info, lifecycle
from dimos.cli.commands.lifecycle import _with_relay_bridge
from dimos.cli.dimos import main, normalize_argv
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
import dimos.core.daemon as core_daemon
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 @@ -126,7 +123,7 @@ def test_show_config_redacts_secrets_including_nested_settings(mocker: MockerFix
def test_rerun_bridge_resolves_its_settings_like_a_blueprint_module(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import dimos.visualization.rerun.bridge as bridge
from dimos.visualization.rerun import bridge

recorded: dict[str, Any] = {}
monkeypatch.setattr(bridge, "run_bridge", lambda **config: recorded.update(config))
Expand All @@ -151,7 +148,7 @@ def test_rerun_bridge_resolves_its_settings_like_a_blueprint_module(
def test_rerun_bridge_applies_global_options_given_after_the_command(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import dimos.visualization.rerun.bridge as bridge
from dimos.visualization.rerun import bridge

seen: list[str] = []
monkeypatch.setattr(bridge, "run_bridge", lambda **config: seen.append(global_config.transport))
Expand Down Expand Up @@ -201,7 +198,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
20 changes: 10 additions & 10 deletions dimos/cloud/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,16 @@ def upload(
else:
artifact = path
size = artifact.stat().st_size
spec = dict(
filename=artifact.name,
size=size,
sha256=_sha256(artifact),
kind=kind,
content_encoding=self.codec_id if compress else None,
robot_id=robot_id,
manifest=manifest,
part_size=part_size,
)
spec = {
"filename": artifact.name,
"size": size,
"sha256": _sha256(artifact),
"kind": kind,
"content_encoding": self.codec_id if compress else None,
"robot_id": robot_id,
"manifest": manifest,
"part_size": part_size,
}
create = self.api.create(**spec)
if create["state"] == "complete":
# already uploaded: (re)send the preview, so a failed send is repaired
Expand Down
8 changes: 5 additions & 3 deletions dimos/cloud/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,11 @@ def test_recordings_discovery(
monkeypatch.setattr(
Path,
"stat",
lambda self, *a, **k: (_ for _ in ()).throw(FileNotFoundError(self))
if self.name == "ghost.db"
else real(self, *a, **k),
lambda self, *a, **k: (
(_ for _ in ()).throw(FileNotFoundError(self))
if self.name == "ghost.db"
else real(self, *a, **k)
),
)
assert cd.recordings() == [db] and cd.recordings(10**9) == [db] and cd.recordings(1) == []

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
Loading
Loading