Skip to content

Commit 367069a

Browse files
authored
Merge pull request #717 - Update G1/Go2 skills to work in blueprint and remove some Robot interfaces
Update G1/Go2 skills and remove some Robot interfaces * Convert `UnitreeSkillContainer` to work with `autoconnect`. I removed the dynamic skills because they don't work across RPC. So I converted to using a single skill called `execute_sport_command`. * Also updated `UnitreeG1SkillContainer`. * Replaced the tests for the above skill. * Remove `UnitreeRobot` and `GpsRobot`. * Convert `SkillContainer` classes to `Module`: `GpsNavSkillContainer`, `GoogleMapsSkillContainer`. Also add demos for them. * Add some missing types for some previous code I wrote. * Added support for declaring RPC connections like this: ```python # new rpc_calls: list[str] = [ "SpatialMemory.tag_location", "SpatialMemory.query_tagged_location", ... ] ``` instead of this: ```python # old @rpc def set_SpatialMemory_tag_location(self, callable: RpcCall) -> None: self._tag_location = callable self._tag_location.set_rpc(self.rpc) @rpc def set_SpatialMemory_query_tagged_location(self, callable: RpcCall) -> None: self._query_tagged_location = callable self._query_tagged_location.set_rpc(self.rpc) ``` * I've also changed the code to support connecting to different Module classes based on the interfaces they implement rather than just the concrete classes. * For example, `NavigationSkillContainer` needs to talk to a navigation class to tell it to move to a specific point. Previously it only supported linking to `BehaviorTreeNavigator.set_goal`. But now both `BehaviorTreeNavigator` and `ROSNav` implement the `NavigationInterface` ABC. So you can say `rpc_calls = ["NavigationInterface.set_goal"]` and you get back either `BehaviorTreeNavigator.set_goal` or `ROSNav.set_goal`, based on which is deployed in the blueprint. If, by chance, both are deployed, then it triggers an error because it's ambiguous, and we can't know which one it's supposed to connect to. Former-commit-id: 8d41052
2 parents eec30de + 4242400 commit 367069a

63 files changed

Lines changed: 1052 additions & 1045 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ repos:
1919
- --use-current-year
2020

2121
- repo: https://github.com/astral-sh/ruff-pre-commit
22-
rev: v0.14.1
22+
rev: v0.14.3
2323
hooks:
2424
- id: ruff-format
2525
stages: [pre-commit]

bin/mypy-strict

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ run_mypy() {
3434
main() {
3535
local user_email="none"
3636
local after_date=""
37+
local in_this_branch=""
3738

3839
# Parse arguments
3940
while [[ $# -gt 0 ]]; do
@@ -74,6 +75,10 @@ main() {
7475
esac
7576
shift 2
7677
;;
78+
--in-this-branch)
79+
in_this_branch=true
80+
shift 1
81+
;;
7782
*)
7883
echo "Error: Unknown argument '$1'" >&2
7984
exit 1
@@ -92,6 +97,10 @@ main() {
9297
pipeline="$pipeline | ./bin/filter-errors-for-user '$user_email'"
9398
fi
9499

100+
if [[ "$in_this_branch" ]]; then
101+
pipeline="$pipeline | grep -Ff <(git diff --name-only dev..HEAD) -"
102+
fi
103+
95104
eval "$pipeline"
96105
}
97106

dimos/agents2/agent.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@
3232
from dimos.agents2.system_prompt import get_system_prompt
3333
from dimos.core import DimosCluster, rpc
3434
from dimos.protocol.skill.coordinator import (
35-
SkillContainer,
3635
SkillCoordinator,
3736
SkillState,
3837
SkillStateDict,
3938
)
39+
from dimos.protocol.skill.skill import SkillContainer
4040
from dimos.protocol.skill.type import Output
4141
from dimos.utils.logging_config import setup_logger
4242

@@ -270,7 +270,6 @@ def _get_state() -> str:
270270
# we are getting tools from the coordinator on each turn
271271
# since this allows for skillcontainers to dynamically provide new skills
272272
tools = self.get_tools()
273-
print("Available tools:", [tool.name for tool in tools])
274273
self._llm = self._llm.bind_tools(tools)
275274

276275
# publish to /agent topic for observability
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"responses": [
3+
{
4+
"content": "",
5+
"tool_calls": [
6+
{
7+
"name": "execute_sport_command",
8+
"args": {
9+
"args": [
10+
"FrontPounce"
11+
]
12+
},
13+
"id": "call_Ukj6bCAnHQLj28RHRp697blZ",
14+
"type": "tool_call"
15+
}
16+
]
17+
},
18+
{
19+
"content": "",
20+
"tool_calls": [
21+
{
22+
"name": "speak",
23+
"args": {
24+
"args": [
25+
"I have successfully performed a front pounce."
26+
]
27+
},
28+
"id": "call_FR9DtqEvJ9zSY85qVD2UFrll",
29+
"type": "tool_call"
30+
}
31+
]
32+
},
33+
{
34+
"content": "I have successfully performed a front pounce.",
35+
"tool_calls": []
36+
}
37+
]
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"responses": [
3+
{
4+
"content": "",
5+
"tool_calls": [
6+
{
7+
"name": "execute_sport_command",
8+
"args": {
9+
"args": [
10+
"FingerHeart"
11+
]
12+
},
13+
"id": "call_VFp6x9F00FBmiiUiemFWewop",
14+
"type": "tool_call"
15+
}
16+
]
17+
},
18+
{
19+
"content": "",
20+
"tool_calls": [
21+
{
22+
"name": "speak",
23+
"args": {
24+
"args": [
25+
"Here's a gesture to show you some love!"
26+
]
27+
},
28+
"id": "call_WUUmBJ95s9PtVx8YQsmlJ4EU",
29+
"type": "tool_call"
30+
}
31+
]
32+
},
33+
{
34+
"content": "Just did a finger heart gesture to show my affection!",
35+
"tool_calls": []
36+
}
37+
]
38+
}

dimos/agents2/skills/conftest.py

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,13 @@
1515
from functools import partial
1616

1717
import pytest
18-
import reactivex as rx
1918
from reactivex.scheduler import ThreadPoolScheduler
2019

2120
from dimos.agents2.skills.google_maps_skill_container import GoogleMapsSkillContainer
2221
from dimos.agents2.skills.gps_nav_skill import GpsNavSkillContainer
2322
from dimos.agents2.skills.navigation import NavigationSkillContainer
2423
from dimos.agents2.system_prompt import get_system_prompt
25-
from dimos.mapping.types import LatLon
26-
from dimos.msgs.sensor_msgs import Image
27-
from dimos.robot.robot import GpsRobot
28-
from dimos.utils.data import get_data
24+
from dimos.robot.unitree_webrtc.unitree_skill_container import UnitreeSkillContainer
2925

3026
system_prompt = get_system_prompt()
3127

@@ -45,31 +41,6 @@ def cleanup_threadpool_scheduler(monkeypatch):
4541
threadpool.scheduler = ThreadPoolScheduler(max_workers=threadpool.get_max_workers())
4642

4743

48-
# TODO: Delete
49-
@pytest.fixture
50-
def fake_robot(mocker):
51-
return mocker.MagicMock()
52-
53-
54-
# TODO: Delete
55-
@pytest.fixture
56-
def fake_gps_robot(mocker):
57-
return mocker.Mock(spec=GpsRobot)
58-
59-
60-
@pytest.fixture
61-
def fake_video_stream():
62-
image_path = get_data("chair-image.png")
63-
image = Image.from_file(str(image_path))
64-
return rx.of(image)
65-
66-
67-
# TODO: Delete
68-
@pytest.fixture
69-
def fake_gps_position_stream():
70-
return rx.of(LatLon(lat=37.783, lon=-122.413))
71-
72-
7344
@pytest.fixture
7445
def navigation_skill_container(mocker):
7546
container = NavigationSkillContainer()
@@ -81,22 +52,35 @@ def navigation_skill_container(mocker):
8152

8253

8354
@pytest.fixture
84-
def gps_nav_skill_container(fake_gps_robot, fake_gps_position_stream):
85-
container = GpsNavSkillContainer(fake_gps_robot, fake_gps_position_stream)
55+
def gps_nav_skill_container(mocker):
56+
container = GpsNavSkillContainer()
57+
container.gps_location.connection = mocker.MagicMock()
58+
container.gps_goal = mocker.MagicMock()
8659
container.start()
8760
yield container
8861
container.stop()
8962

9063

9164
@pytest.fixture
92-
def google_maps_skill_container(fake_gps_robot, fake_gps_position_stream, mocker):
93-
container = GoogleMapsSkillContainer(fake_gps_robot, fake_gps_position_stream)
65+
def google_maps_skill_container(mocker):
66+
container = GoogleMapsSkillContainer()
67+
container.gps_location.connection = mocker.MagicMock()
9468
container.start()
9569
container._client = mocker.MagicMock()
9670
yield container
9771
container.stop()
9872

9973

74+
@pytest.fixture
75+
def unitree_skills(mocker):
76+
container = UnitreeSkillContainer()
77+
container._move = mocker.Mock()
78+
container._publish_request = mocker.Mock()
79+
container.start()
80+
yield container
81+
container.stop()
82+
83+
10084
@pytest.fixture
10185
def create_navigation_agent(navigation_skill_container, create_fake_agent):
10286
return partial(
@@ -122,3 +106,12 @@ def create_google_maps_agent(
122106
system_prompt=system_prompt,
123107
skill_containers=[gps_nav_skill_container, google_maps_skill_container],
124108
)
109+
110+
111+
@pytest.fixture
112+
def create_unitree_skills_agent(unitree_skills, create_fake_agent):
113+
return partial(
114+
create_fake_agent,
115+
system_prompt=system_prompt,
116+
skill_containers=[unitree_skills],
117+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Dimensional Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from dotenv import load_dotenv
17+
18+
from dimos.agents2.agent import llm_agent
19+
from dimos.agents2.cli.human import human_input
20+
from dimos.agents2.skills.demo_robot import demo_robot
21+
from dimos.agents2.skills.google_maps_skill_container import google_maps_skill
22+
from dimos.agents2.system_prompt import get_system_prompt
23+
from dimos.core.blueprints import autoconnect
24+
25+
load_dotenv()
26+
27+
28+
demo_google_maps_skill = autoconnect(
29+
demo_robot(),
30+
google_maps_skill(),
31+
human_input(),
32+
llm_agent(system_prompt=get_system_prompt()),
33+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Dimensional Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from dotenv import load_dotenv
17+
18+
from dimos.agents2.agent import llm_agent
19+
from dimos.agents2.cli.human import human_input
20+
from dimos.agents2.skills.demo_robot import demo_robot
21+
from dimos.agents2.skills.gps_nav_skill import gps_nav_skill
22+
from dimos.agents2.system_prompt import get_system_prompt
23+
from dimos.core.blueprints import autoconnect
24+
25+
load_dotenv()
26+
27+
28+
demo_gps_nav_skill = autoconnect(
29+
demo_robot(),
30+
gps_nav_skill(),
31+
human_input(),
32+
llm_agent(system_prompt=get_system_prompt()),
33+
)

dimos/agents2/skills/demo_robot.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Dimensional Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from reactivex import interval
17+
18+
from dimos.core.module import Module
19+
from dimos.core.stream import Out
20+
from dimos.mapping.types import LatLon
21+
22+
23+
class DemoRobot(Module):
24+
gps_location: Out[LatLon] = None
25+
26+
def start(self) -> None:
27+
super().start()
28+
self._disposables.add(interval(1.0).subscribe(lambda _: self._publish_gps_location()))
29+
30+
def stop(self) -> None:
31+
super().stop()
32+
33+
def _publish_gps_location(self) -> None:
34+
self.gps_location.publish(LatLon(lat=37.78092426217621, lon=-122.40682866540769))
35+
36+
37+
demo_robot = DemoRobot.blueprint
38+
39+
40+
__all__ = ["DemoRobot", "demo_robot"]

0 commit comments

Comments
 (0)