Skip to content

Commit b39070c

Browse files
authored
refactor(tools): remove repeated ros2 tools (#364)
1 parent 7aac651 commit b39070c

File tree

6 files changed

+168
-330
lines changed

6 files changed

+168
-330
lines changed

src/rai/rai/apps/high_level_api.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,7 @@
1818
from langchain_core.messages import BaseMessage, HumanMessage
1919

2020
from rai.agents.conversational_agent import create_conversational_agent
21-
from rai.tools.ros.cli import (
22-
Ros2ActionTool,
23-
Ros2InterfaceTool,
24-
Ros2ServiceTool,
25-
Ros2TopicTool,
26-
)
21+
from rai.tools.ros.cli import ros2_action, ros2_interface, ros2_service, ros2_topic
2722
from rai.utils.model_initialization import get_llm_model
2823

2924

@@ -37,10 +32,10 @@ class ROS2Agent(Agent):
3732
def __init__(self):
3833
super().__init__()
3934
self.tools = [
40-
Ros2TopicTool(),
41-
Ros2InterfaceTool(),
42-
Ros2ServiceTool(),
43-
Ros2ActionTool(),
35+
ros2_topic,
36+
ros2_interface,
37+
ros2_service,
38+
ros2_action,
4439
]
4540
self.agent = create_conversational_agent(
4641
self.llm, self.tools, "You are a ROS2 expert."

src/rai/rai/tools/debugging_assistant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from rai.agents.conversational_agent import create_conversational_agent
1919
from rai.agents.integrations.streamlit import get_streamlit_cb, streamlit_invoke
20-
from rai.tools.ros.debugging import (
20+
from rai.tools.ros.cli import (
2121
ros2_action,
2222
ros2_interface,
2323
ros2_node,

src/rai/rai/tools/ros/__init__.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@
1313
# limitations under the License.
1414

1515

16-
from .cli import Ros2InterfaceTool, Ros2ServiceTool, Ros2TopicTool
16+
from .cli import (
17+
ros2_action,
18+
ros2_interface,
19+
ros2_node,
20+
ros2_param,
21+
ros2_service,
22+
ros2_topic,
23+
)
1724
from .native import Ros2BaseInput, Ros2BaseTool
1825
from .tools import (
1926
AddDescribedWaypointToDatabaseTool,
@@ -22,9 +29,12 @@
2229
)
2330

2431
__all__ = [
25-
"Ros2TopicTool",
26-
"Ros2InterfaceTool",
27-
"Ros2ServiceTool",
32+
"ros2_action",
33+
"ros2_interface",
34+
"ros2_node",
35+
"ros2_topic",
36+
"ros2_param",
37+
"ros2_service",
2838
"Ros2BaseTool",
2939
"Ros2BaseInput",
3040
"AddDescribedWaypointToDatabaseTool",

src/rai/rai/tools/ros/cli.py

Lines changed: 147 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -12,156 +12,160 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from subprocess import PIPE, Popen
16+
from threading import Timer
17+
from typing import List, Literal, Optional
1518

16-
import subprocess
17-
from typing import Type
19+
from langchain_core.tools import tool
1820

19-
from langchain.tools import BaseTool
20-
from pydantic import BaseModel, Field
21+
FORBIDDEN_CHARACTERS = ["&", ";", "|", "&&", "||", "(", ")", "<", ">", ">>", "<<"]
2122

2223

23-
class Ros2TopicToolInput(BaseModel):
24-
"""Input for the ros2_topic tool."""
24+
def run_with_timeout(cmd: List[str], timeout_sec: int):
25+
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
26+
timer = Timer(timeout_sec, proc.kill)
27+
try:
28+
timer.start()
29+
stdout, stderr = proc.communicate()
30+
return stdout, stderr
31+
finally:
32+
timer.cancel()
2533

26-
command: str = Field(..., description="The command to run")
2734

28-
29-
class Ros2TopicTool(BaseTool):
30-
"""Tool for interacting with ROS2 topics."""
31-
32-
name: str = "Ros2TopicTool"
33-
description: str = """
34-
usage: ros2 topic [-h] [--include-hidden-topics] Call `ros2 topic <command> -h` for more detailed usage. ...
35-
36-
Various topic related sub-commands
37-
38-
options:
39-
-h, --help show this help message and exit
40-
--include-hidden-topics
41-
Consider hidden topics as well
42-
43-
Commands:
44-
bw Display bandwidth used by topic
45-
delay Display delay of topic from timestamp in header
46-
echo Output messages from a topic
47-
find Output a list of available topics of a given type
48-
hz Print the average publishing rate to screen
49-
info Print information about a topic
50-
list Output a list of available topics
51-
pub Publish a message to a topic
52-
type Print a topic's type
53-
54-
Call `ros2 topic <command> -h` for more detailed usage.
55-
"""
56-
args_schema: Type[Ros2TopicToolInput] = Ros2TopicToolInput
57-
58-
def _run(self, command: str):
59-
"""Executes the specified ROS2 topic command."""
60-
result = subprocess.run(
61-
f"ros2 topic {command}", shell=True, capture_output=True, timeout=2
35+
def run_command(cmd: List[str], timeout: int = 5):
36+
# Validate command safety by checking for shell operators
37+
# Block potentially dangerous characters
38+
if any(char in " ".join(cmd) for char in FORBIDDEN_CHARACTERS):
39+
raise ValueError(
40+
"Command is not safe to run. The command contains forbidden characters."
6241
)
63-
return result
64-
65-
66-
class Ros2InterafaceToolInput(BaseModel):
67-
"""Input for the ros2_interface tool."""
68-
69-
command: str = Field(..., description="The command to run")
70-
71-
72-
class Ros2InterfaceTool(BaseTool):
73-
74-
name: str = "Ros2InterfaceTool"
75-
76-
description: str = """
77-
usage: ros2 interface [-h] Call `ros2 interface <command> -h` for more detailed usage. ...
78-
79-
Show information about ROS interfaces
80-
81-
options:
82-
-h, --help show this help message and exit
83-
84-
Commands:
85-
list List all interface types available
86-
package Output a list of available interface types within one package
87-
packages Output a list of packages that provide interfaces
88-
proto Output an interface prototype
89-
show Output the interface definition
90-
91-
Call `ros2 interface <command> -h` for more detailed usage.
42+
stdout, stderr = run_with_timeout(cmd, timeout)
43+
output = {}
44+
if stdout:
45+
output["stdout"] = stdout.decode("utf-8")
46+
else:
47+
output["stdout"] = "Command returned no stdout output"
48+
if stderr:
49+
output["stderr"] = stderr.decode("utf-8")
50+
else:
51+
output["stderr"] = "Command returned no stderr output"
52+
return str(output)
53+
54+
55+
@tool
56+
def ros2_action(
57+
command: Literal["info", "list", "type", "send_goal"],
58+
arguments: Optional[List[str]] = None,
59+
timeout: int = 5,
60+
):
61+
"""Run a ROS2 action command
62+
Args:
63+
command: The action command to run (info/list/type)
64+
arguments: Additional arguments for the command as a list of strings
65+
timeout: Command timeout in seconds
9266
"""
93-
94-
args_schema: Type[Ros2InterafaceToolInput] = Ros2InterafaceToolInput
95-
96-
def _run(self, command: str):
97-
command = f"ros2 interface {command}"
98-
result = subprocess.run(command, shell=True, capture_output=True, timeout=2)
99-
return result
100-
101-
102-
class Ros2ServiceToolInput(BaseModel):
103-
"""Input for the ros2_service tool."""
104-
105-
command: str = Field(..., description="The command to run")
106-
107-
108-
class Ros2ServiceTool(BaseTool):
109-
name: str = "Ros2ServiceTool"
110-
111-
description: str = """
112-
usage: ros2 service [-h] [--include-hidden-services] Call `ros2 service <command> -h` for more detailed usage. ...
113-
114-
Various service related sub-commands
115-
116-
options:
117-
-h, --help show this help message and exit
118-
--include-hidden-services
119-
Consider hidden services as well
120-
121-
Commands:
122-
call Call a service
123-
find Output a list of available services of a given type
124-
list Output a list of available services
125-
type Output a service's type
67+
cmd = ["ros2", "action", command]
68+
if arguments:
69+
cmd.extend(arguments)
70+
return run_command(cmd, timeout)
71+
72+
73+
@tool
74+
def ros2_service(
75+
command: Literal["call", "find", "info", "list", "type"],
76+
arguments: Optional[List[str]] = None,
77+
timeout: int = 5,
78+
):
79+
"""Run a ROS2 service command
80+
Args:
81+
command: The service command to run
82+
arguments: Additional arguments for the command as a list of strings
83+
timeout: Command timeout in seconds
12684
"""
127-
128-
args_schema: Type[Ros2ServiceToolInput] = Ros2ServiceToolInput
129-
130-
def _run(self, command: str):
131-
command = f"ros2 service {command}"
132-
result = subprocess.run(command, shell=True, capture_output=True, timeout=2)
133-
return result
134-
135-
136-
class Ros2ActionToolInput(BaseModel):
137-
"""Input for the ros2_action tool."""
138-
139-
command: str = Field(..., description="The command to run")
140-
141-
142-
class Ros2ActionTool(BaseTool):
143-
name: str = "Ros2ActionTool"
144-
145-
description: str = """
146-
usage: ros2 action [-h] Call `ros2 action <command> -h` for more detailed usage. ...
147-
148-
Various action related sub-commands
149-
150-
options:
151-
-h, --help show this help message and exit
152-
153-
Commands:
154-
info Print information about an action
155-
list Output a list of action names
156-
send_goal Send an action goal
157-
type Print a action's type
158-
159-
Call `ros2 action <command> -h` for more detailed usage.
85+
cmd = ["ros2", "service", command]
86+
if arguments:
87+
cmd.extend(arguments)
88+
return run_command(cmd, timeout)
89+
90+
91+
@tool
92+
def ros2_node(
93+
command: Literal["info", "list"],
94+
arguments: Optional[List[str]] = None,
95+
timeout: int = 5,
96+
):
97+
"""Run a ROS2 node command
98+
Args:
99+
command: The node command to run
100+
arguments: Additional arguments for the command as a list of strings
101+
timeout: Command timeout in seconds
160102
"""
161-
162-
args_schema: Type[Ros2ActionToolInput] = Ros2ActionToolInput
163-
164-
def _run(self, command: str):
165-
command = f"ros2 action {command}"
166-
result = subprocess.run(command, shell=True, capture_output=True)
167-
return result
103+
cmd = ["ros2", "node", command]
104+
if arguments:
105+
cmd.extend(arguments)
106+
return run_command(cmd, timeout)
107+
108+
109+
@tool
110+
def ros2_param(
111+
command: Literal["delete", "describe", "dump", "get", "list", "set"],
112+
arguments: Optional[List[str]] = None,
113+
timeout: int = 5,
114+
):
115+
"""Run a ROS2 parameter command
116+
Args:
117+
command: The parameter command to run
118+
arguments: Additional arguments for the command as a list of strings
119+
timeout: Command timeout in seconds
120+
"""
121+
cmd = ["ros2", "param", command]
122+
if arguments:
123+
cmd.extend(arguments)
124+
return run_command(cmd, timeout)
125+
126+
127+
@tool
128+
def ros2_interface(
129+
command: Literal["list", "package", "packages", "proto", "show"],
130+
arguments: Optional[List[str]] = None,
131+
timeout: int = 5,
132+
):
133+
"""Run a ROS2 interface command
134+
Args:
135+
command: The interface command to run
136+
arguments: Additional arguments for the command as a list of strings
137+
timeout: Command timeout in seconds
138+
"""
139+
cmd = ["ros2", "interface", command]
140+
if arguments:
141+
cmd.extend(arguments)
142+
return run_command(cmd, timeout)
143+
144+
145+
@tool
146+
def ros2_topic(
147+
command: Literal[
148+
"bw", "delay", "echo", "find", "hz", "info", "list", "pub", "type"
149+
],
150+
arguments: Optional[List[str]] = None,
151+
timeout: int = 5,
152+
):
153+
"""Run a ROS2 topic command
154+
Args:
155+
command: The topic command to run:
156+
- bw: Display bandwidth used by topic
157+
- delay: Display delay of topic from timestamp in header
158+
- echo: Output messages from a topic
159+
- find: Output a list of available topics of a given type
160+
- hz: Print the average publishing rate to screen
161+
- info: Print information about a topic
162+
- list: Output a list of available topics
163+
- pub: Publish a message to a topic
164+
- type: Print a topic's type
165+
arguments: Additional arguments for the command as a list of strings
166+
timeout: Command timeout in seconds
167+
"""
168+
cmd = ["ros2", "topic", command]
169+
if arguments:
170+
cmd.extend(arguments)
171+
return run_command(cmd, timeout)

0 commit comments

Comments
 (0)