Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
23cf04d
Merge changes from branch feat/debug_3dpipe
Juliaj Jan 8, 2026
7142aa5
Add redesign proposal for api usability
Juliaj Jan 8, 2026
f4abec9
Code refactoring to improve api usability
Juliaj Jan 11, 2026
ae39909
Revert changes in rai_sim and bump up the version
Juliaj Jan 11, 2026
d23c255
Update manipulation demo
Juliaj Jan 12, 2026
a03ed63
Merge branch 'main' into jj/feat/3dpipe_and_usability
Juliaj Jan 12, 2026
09674aa
Refector code to use ROS2ServiceError and ROS2ParameterError
Juliaj Jan 12, 2026
1918e6b
Handle exception in detection and segment service
Juliaj Jan 12, 2026
31fc1ba
Update document
Juliaj Jan 12, 2026
e4e1925
Fix the failing tests
Juliaj Jan 12, 2026
0d55ea9
Fix the link in the docs
Juliaj Jan 12, 2026
5d200fe
Refactor service_utils and topics_utils to use ROS2 APIs and add more…
Juliaj Jan 13, 2026
4af6d1b
Add a test for PARAMETER_BYTE_ARRAY
Juliaj Jan 13, 2026
2dc2e95
Remove redundant doc
Juliaj Jan 13, 2026
8e34696
Fix the doc link
Juliaj Jan 13, 2026
2febfad
Merge branch 'main' into jj/feat/3dpipe_and_usability
Juliaj Jan 14, 2026
344f266
Support enable_legacy_service_names to reduce migration cost
Juliaj Jan 21, 2026
cd02864
Set timeout for services and tools
Juliaj Jan 21, 2026
6ce5eca
Switch to use encoding-aware depth conversion
Juliaj Jan 21, 2026
b170cd0
Address review feedback from coderabbitai
Juliaj Jan 21, 2026
f4bc59b
Consolidate the manipulation demo agent creation code
Juliaj Jan 22, 2026
55089f0
Address coderabbitai minor comments
Juliaj Jan 22, 2026
244b095
Address some of coderabbitai nitpicking comments
Juliaj Jan 22, 2026
7f5e01e
Increase test coverage and fix flakey tests
Juliaj Jan 22, 2026
7a21c08
Fix unit test failure (segfault) on Humble
Juliaj Jan 22, 2026
a1aa532
Simplify get_object_gripping_points description and enable streamlit …
Juliaj Jan 22, 2026
04cf7ee
Adjust tool name and descripton to match the v1 tool behavior for man…
Juliaj Jan 22, 2026
853b719
Update README and add warning if both tools are registered
Juliaj Jan 22, 2026
0311f6e
Fix segfault from test_topic_tools.py on humble
Juliaj Jan 22, 2026
d2a5f21
Update follow-ups.md
Juliaj Jan 22, 2026
3bb3d12
Add migration guide
Juliaj Jan 22, 2026
c77b065
Merge branch 'main' into jj/feat/3dpipe_and_usability
Juliaj Jan 23, 2026
bed82a3
Remove unused PerceptionAlgorithmError
Juliaj Jan 23, 2026
03eb766
Address review feedback to remove unused exceptions and clean up migr…
Juliaj Jan 23, 2026
5cf4bf5
Version bump
Juliaj Jan 23, 2026
64d74fd
Merge remote-tracking branch 'origin/main' into jj/feat/3dpipe_and_us…
Juliaj Jan 23, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/poetry-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
shell: bash
run: |
source setup_shell.sh
pytest -m "not billable" --cov=./src/rai_core --cov-report=xml --junitxml=junit.xml -o junit_family=legacy
pytest -m "not billable and not manual" --cov=./src/rai_core --cov-report=xml --junitxml=junit.xml -o junit_family=legacy
- name: Upload coverage to Codecov
if: ${{ matrix.ros_distro == 'jazzy' }}
Expand Down
23 changes: 23 additions & 0 deletions docs/API_documentation/connectors/ROS_2_Connectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,29 @@ agent = ReActAgent(
agent.subscribe_source("/from_human", hri_connector)
```

## ROS2 Utilities

RAI provides utilities for error handling and parameter access when working with ROS2 connectors:

- `ROS2ServiceError`: Exception raised for service-related errors (e.g., service unavailable, timeout). Includes service name, timeout duration, and diagnostic suggestions.

- `ROS2ParameterError`: Exception raised for parameter-related errors (e.g., missing parameter, type mismatch). Includes parameter name, expected type/value, and suggestions for resolution.

- `get_param_value()`: Utility function for simplified ROS2 parameter access with automatic type conversion and default value support. Reduces boilerplate compared to direct ROS2 parameter API usage.

```python
from rai.communication.ros2 import ROS2ServiceError, ROS2ParameterError, get_param_value

# Access parameters with defaults
service_name = get_param_value(node, "/my_service/name", default="/default_service")

# Handle service errors
try:
response = connector.service_call(...)
except ROS2ServiceError as e:
print(f"Service {e.service_name} unavailable: {e.suggestion}")
```

## See Also

- [Connectors Overview](./overview.md)
Expand Down
242 changes: 242 additions & 0 deletions docs/api_design_considerations.md

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions docs/demos/manipulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,24 @@ manipulation techniques.
ros2 launch examples/manipulation-demo.launch.py game_launcher:=demo_assets/manipulation/RAIManipulationDemo/RAIManipulationDemo.GameLauncher
```

By default, perception services register both new model-agnostic service names (`/detection`, `/segmentation`) and legacy names (`/grounding_dino_classify`, `/grounded_sam_segment`) for backward compatibility. To disable legacy names, add the launch argument:

```shell
ros2 launch examples/manipulation-demo.launch.py game_launcher:=... enable_legacy_service_names:=false
```

2. Run cmd app

```shell
python examples/manipulation-demo.py
```

Note: `manipulation-demo.py` uses the new service names (`/detection`, `/segmentation`). Legacy examples like `manipulation-demo-v1.py` use legacy service names and require `enable_legacy_service_names:=true` (default).

2. Interact with the robot arm using natural language commands. For example:

```
"Place every apple on top of the cube"
"Place each apple on top of a cube"
"Build a tower from cubes"
"Arrange objects in a line"
"Put two boxes closer to each other. Move only one box."
Expand Down Expand Up @@ -134,7 +142,7 @@ manipulation techniques.

The manipulation demo utilizes several components:

1. Vision processing using Grounded SAM 2 and Grounding DINO for object detection and segmentation.
1. Vision processing using Grounded SAM 2 and Grounding DINO for object detection and segmentation via ROS2 services (`/detection`, `/segmentation`).
2. RAI agent to process the request and plan the manipulation sequence.
3. Robot arm control for executing the planned movements.

Expand All @@ -144,6 +152,21 @@ The main logic of the demo is implemented in the `create_agent` function, which
examples/manipulation-demo.py
```

### Service Names and Backward Compatibility

Perception services support both new model-agnostic service names and legacy names for backward compatibility:

- New service names (default): `/detection`, `/segmentation`
- Legacy service names: `/grounding_dino_classify`, `/grounded_sam_segment`

By default, both sets of names are registered. To disable legacy names (for new applications only), launch with:

```shell
ros2 launch examples/manipulation-demo.launch.py game_launcher:=... enable_legacy_service_names:=false
```

Existing applications using legacy service names will continue to work with the default configuration.

## Known Limitations

- `Grounding DINO` can't distinguish colors.
Expand Down
324 changes: 324 additions & 0 deletions docs/extensions/rethinking_usability.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/embodiments/manipulation_embodiment.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"Always follow the specified payload limit; do not exceed 3 kg.",
"Before starting the task, by looking at the image, determine if the task can be completed. If yes - proceed without asking. Otherwise report.",
"Cubes are 5cm tall. When stacking, add 5cm to the z coordinate for each additional cube.",
"When placing objects 'on top' of other objects (different object types), you must add a height offset to the target object's z-coordinate. The height offset should be approximately 5cm (0.05m) for most objects. Never use the target object's centroid z-coordinate directly - always add the height offset. For stacking tasks (same object type), follow the stacking rule instead.",
"Remember to not put objects on top of other objects, unless specifically instructed otherwise",
"Try to consider the typical size of objects when placing them (width, height, length)",
"Always leave at least a 5cm margin (x and y) between objects unless specifically instructed otherwise",
Expand Down
48 changes: 43 additions & 5 deletions examples/manipulation-demo-streamlit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
from pathlib import Path

import streamlit as st
Expand All @@ -36,6 +38,8 @@
)
from rai_sim.simulation_bridge import SceneConfig

logger = logging.getLogger(__name__)


def launch_description():
launch_moveit = IncludeLaunchDescription(
Expand All @@ -62,6 +66,9 @@ def launch_description():
"/launch/openset.launch.py",
]
),
launch_arguments={
"enable_legacy_service_names": "true", # Enable both legacy and new service names for v1/v2 compatibility
}.items(),
)

return LaunchDescription(
Expand All @@ -74,16 +81,32 @@ def launch_description():


@st.cache_resource
def initialize_graph():
agent, camera_tool = create_agent()
def initialize_graph(version: str):
agent, camera_tool = create_agent(version=version)
return agent, camera_tool


@st.cache_resource
def initialize_o3de(scenario_path: str, o3de_config_path: str):
def initialize_o3de(
scenario_path: str, o3de_config_path: str, agent_version: str = "v2"
):
simulation_config = O3DExROS2SimulationConfig.load_config(
config_path=Path(o3de_config_path)
)

# Adjust required services based on agent version
# v1 uses legacy service names, v2 uses new service names
if agent_version == "v2":
# Replace legacy service names with new ones for v2
services = simulation_config.required_robotic_ros2_interfaces["services"]
services = [
"/detection" if s == "/grounding_dino_classify" else s for s in services
]
services = [
"/segmentation" if s == "/grounded_sam_segment" else s for s in services
]
simulation_config.required_robotic_ros2_interfaces["services"] = services

scene_config = SceneConfig.load_base_config(Path(scenario_path))
scenario = Scenario(
task=None,
Expand Down Expand Up @@ -143,6 +166,9 @@ def main(o3de_config_path: str):
# Layout selection in sidebar
st.sidebar.header("Configuration")

# Agent version from environment variable
agent_version = os.getenv("AGENT_VERSION", "v2")

# Get available scenarios for layout selection
levels = ["medium", "hard", "very_hard"]
scenarios: list[Scenario] = get_scenarios(levels=levels)
Expand Down Expand Up @@ -260,19 +286,31 @@ def main(o3de_config_path: str):
else:
st.sidebar.warning("Demo not initialized yet")

# Clear conversation history button - resets cached tool results
# This forces the agent to query object positions fresh for each new task
# instead of using cached ToolMessage results from previous interactions
if st.sidebar.button(
"Clear Conversation History", help="Clear all previous tool calls and messages"
):
st.session_state["messages"] = [
AIMessage(content="Hi! I am a robotic arm. What can I do for you?")
]
st.rerun()

if "o3de" not in st.session_state:
selected_scenario_path = get_scenario_path(scenarios, scenario)
if selected_scenario_path is None:
st.error(f"Could not find scenario: {scenario}")
st.stop()
o3de, initial_scenario = initialize_o3de(
selected_scenario_path, o3de_config_path
selected_scenario_path, o3de_config_path, agent_version=agent_version
)
st.session_state["o3de"] = o3de
st.session_state["current_scenario"] = initial_scenario

if "graph" not in st.session_state:
graph, camera_tool = initialize_graph()
logger.info(f"Initializing agent version: {agent_version}")
graph, camera_tool = initialize_graph(version=agent_version)
st.session_state["graph"] = graph
st.session_state["camera_tool"] = camera_tool

Expand Down
37 changes: 37 additions & 0 deletions examples/manipulation-demo-v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright (C) 2025 Robotec.AI
#
# 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.


import logging
from typing import List

from langchain_core.messages import BaseMessage, HumanMessage
from manipulation_common import create_agent

logger = logging.getLogger(__name__)


def main():
agent, _ = create_agent(version="v1")
messages: List[BaseMessage] = []

while True:
prompt = input("Enter a prompt: ")
messages.append(HumanMessage(content=prompt))
output = agent.invoke({"messages": messages})
output["messages"][-1].pretty_print()


if __name__ == "__main__":
main()
13 changes: 13 additions & 0 deletions examples/manipulation-demo.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ def generate_launch_description():
description="Path to the game launcher executable",
)

# Declare argument for legacy service names (forwarded to openset.launch.py)
enable_legacy_arg = DeclareLaunchArgument(
"enable_legacy_service_names",
default_value="true",
description="Enable legacy service names for backward compatibility (default: true)",
)

launch_game_launcher = ExecuteProcess(
cmd=[
LaunchConfiguration("game_launcher"),
Expand Down Expand Up @@ -64,11 +71,17 @@ def generate_launch_description():
"/launch/openset.launch.py",
]
),
launch_arguments={
"enable_legacy_service_names": LaunchConfiguration(
"enable_legacy_service_names"
),
}.items(),
)

return LaunchDescription(
[
game_launcher_arg,
enable_legacy_arg,
launch_game_launcher,
launch_openset,
launch_moveit,
Expand Down
46 changes: 38 additions & 8 deletions examples/manipulation-demo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2024 Robotec.AI
# Copyright (C) 2025 Robotec.AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,23 +14,53 @@


import logging
import signal
from typing import List

import rclpy
from langchain_core.messages import BaseMessage, HumanMessage
from manipulation_common import create_agent

logger = logging.getLogger(__name__)


def create_agent():
"""Create and configure the manipulation agent (v2).

This is a thin wrapper around manipulation_common.create_agent(version="v2").
See manipulation_common.py for documentation on parameter configuration.
"""
from manipulation_common import create_agent as _create_agent

agent, _ = _create_agent(version="v2")
return agent


def main():
agent, _ = create_agent()
agent = create_agent()
messages: List[BaseMessage] = []

while True:
prompt = input("Enter a prompt: ")
messages.append(HumanMessage(content=prompt))
output = agent.invoke({"messages": messages})
output["messages"][-1].pretty_print()
def cleanup(signum, frame):
"""Cleanup handler for graceful shutdown."""
logger.info("Shutting down...")
if rclpy.ok():
rclpy.shutdown()
exit(0)

signal.signal(signal.SIGINT, cleanup)

try:
while True:
prompt = input("Enter a prompt: ")
messages.append(HumanMessage(content=prompt))
output = agent.invoke({"messages": messages})
output["messages"][-1].pretty_print()
except KeyboardInterrupt:
cleanup(None, None)
except Exception as e:
logger.error(f"Error: {e}")
if rclpy.ok():
rclpy.shutdown()
raise


if __name__ == "__main__":
Expand Down
Loading
Loading