Skip to content

Commit e7c7692

Browse files
committed
Update TurtleSim demo. Minor changes to ROSA base class.
1 parent 958b1c9 commit e7c7692

File tree

11 files changed

+347
-199
lines changed

11 files changed

+347
-199
lines changed

Dockerfile

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ RUN echo "export ROSLAUNCH_SSH_UNKNOWN=1" >> /root/.bashrc
3838
COPY . /app/
3939
WORKDIR /app/
4040

41+
# Uncomment this line to test with local ROSA package
42+
# RUN python3.9 -m pip install --user -e .
43+
4144
# Run roscore in the background, then run `rosrun turtlesim turtlesim_node` in a new terminal, finally run main.py in a new terminal
4245
CMD /bin/bash -c 'source /opt/ros/noetic/setup.bash && \
4346
roscore & \

README.md

+79-23
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,98 @@
11
# ROS Agent (ROSA)
22

33
ROSA is an AI agent that can be used to interact with ROS (Robot Operating System) and perform various tasks.
4-
It is built using the Langchain framework and the [ROS](https://www.ros.org/) framework.
4+
It is built using [Langchain](https://python.langchain.com/v0.2/docs/introduction/) and the
5+
[ROS](https://www.ros.org/) framework.
56

67
## Installation
78

9+
Requirements:
10+
- Python 3.9 or higher
11+
- ROS Noetic (or higher)
12+
13+
**Note:** ROS Noetic uses Python3.8, but LangChain requires Python3.9 or higher. To use ROSA with ROS Noetic,
14+
you will need to create a virtual environment with Python3.9 or higher and install ROSA in that environment.
15+
816
Use pip to install ROSA:
917

1018
```bash
11-
pip install jpl-rosa
19+
pip3 install jpl-rosa
1220
```
1321

14-
**Important:** ROS Noetic runs on Python 3.8, but LangChain is only available for Python >= 3.9. So you will
15-
need to install Python3.9 separately, and run ROSA outside the ROS environment. This restriction is not true
16-
for ROS2 variants.
22+
# TurtleSim Demo
23+
We have included a demo that uses ROSA to control the TurtleSim robot in simulation. To run the demo, you will need
24+
to have Docker installed on your machine.
1725

26+
## Setup
1827

19-
# TurtleSim Demo
20-
We have included a demo that uses ROSA to control the TurtleSim simulator.
28+
1. Clone this repository
29+
2. Configure the LLM in `src/turtle_agent/scripts/llm.py`
30+
3. Run the demo script: `./demo.sh`
31+
4. Start ROSA in the new Docker session: `catkin build && source devel/setup.bash && roslaunch turtle_agent agent`
32+
5. Run example queries: `examples`
2133

22-
## Configure your LLM
23-
You will need to configure your LLM by setting the environment variables found in `.env`. You will also need
24-
to ensure the correct LLM is configured in the `src/turtle_agent/turtle_agent.py` file, specifically in the
25-
`get_llm()` function.
2634

27-
After that is configured properly, you can run the demo using the following command:
35+
# Adapting ROSA for Your Robot
2836

29-
```bash
30-
./demo.sh
31-
```
37+
ROSA is designed to be easily adaptable to different robots and environments. To adapt ROSA for your robot, you will
38+
can either (1) create a new class that inherits from the `ROSA` class, or (2) create a new instance of the `ROSA` class
39+
and pass in the necessary parameters. The first option is recommended if you need to make significant changes to the
40+
agent's behavior, while the second option is recommended if you want to use the agent with minimal changes.
3241

33-
The above command will start Docker and launch the turtlesim node. To start ROSA, you can run the following command
34-
the new Docker session:
42+
In either case, ROSA is adapted by providing it with a new set of tools and/or prompts. The tools are used to interact
43+
with the robot and the ROS environment, while the prompts are used to guide the agents behavior.
3544

36-
```bash
37-
catkin build && source devel/setup.bash && roslaunch turtle_agent agent
38-
```
45+
## Adding Tools
46+
There are two methods for adding tools to ROSA:
47+
1. Pass in a list of @tool functions using the `tools` parameter.
48+
2. Pass in a list of Python packages containing @tool functions using the `tool_packages` parameter.
3949

40-
## Example Queries
41-
After launching the agent, you can get a list of example queries by typing `examples` in the terminal.
42-
You can then run any of the example queries by typing the query number (e.g. 2) and pressing enter.
50+
The first method is recommended if you have a small number of tools, while the second method is recommended if you have
51+
a large number of tools or if you want to organize your tools into separate packages.
52+
53+
**Hint:** check `src/turtle_agent/scripts/turtle_agent.py` for examples on how to use both methods.
54+
55+
## Adding Prompts
56+
To add prompts to ROSA, you need to create a new instance of the `RobotSystemPrompts` class and pass it to the `ROSA`
57+
constructor using the `prompts` parameter. The `RobotSystemPrompts` class contains the following attributes:
58+
59+
- `embodiment_and_persona`: Gives the agent a sense of identity and helps it understand its role.
60+
- `about_your_operators`: Provides information about the operators who interact with the robot, which can help the agent
61+
understand the context of the interaction.
62+
- `critical_instructions`: Provides critical instructions that the agent should follow to ensure the safety and
63+
well-being of the robot and its operators.
64+
- `constraints_and_guardrails`: Gives the robot a sense of its limitations and informs its decision-making process.
65+
- `about_your_environment`: Provides information about the physical and digital environment in which the robot operates.
66+
- `about_your_capabilities`: Describes what the robot can and cannot do, which can help the agent understand its
67+
limitations.
68+
- `nuance_and_assumptions`: Provides information about the nuances and assumptions that the agent should consider when
69+
interacting with the robot.
70+
- `mission_and_objectives`: Describes the mission and objectives of the robot, which can help the agent understand its
71+
purpose and goals.
72+
- `environment_variables`: Provides information about the environment variables that the agent should consider when
73+
interacting with the robot. e.g. $ROS_MASTER_URI, or $ROS_IP.
74+
75+
## Example
76+
Here is a quick and easy example showing how to add new tools and prompts to ROSA:
77+
```python
78+
from langchain.agents import tool
79+
from rosa import ROSA, RobotSystemPrompts
80+
81+
@tool
82+
def move_forward(distance: float) -> str:
83+
"""
84+
Move the robot forward by the specified distance.
85+
86+
:param distance: The distance to move the robot forward.
87+
"""
88+
# Your code here ...
89+
return f"Moving forward by {distance} units."
90+
91+
prompts = RobotSystemPrompts(
92+
embodiment_and_persona="You are a cool robot that can move forward."
93+
)
94+
95+
llm = get_your_llm_here()
96+
rosa = ROSA(ros_version=1, llm=llm, tools=[move_forward])
97+
rosa.invoke("Move forward by 2 units.")
98+
```

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
setup(
2424
name="jpl-rosa",
25-
version="1.0.0",
25+
version="1.0.1",
2626
license="Apache 2.0",
2727
description="ROSA: the Robot Operating System Agent",
2828
long_description=long_description,

src/rosa/prompts.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616

1717

1818
class RobotSystemPrompts:
19-
def __init__(self, embodiment_and_persona: Optional[str], about_your_operators: Optional[str],
20-
critical_instructions: Optional[str], constraints_and_guardrails: Optional[str],
21-
about_your_environment: Optional[str], about_your_capabilities: Optional[str],
22-
nuance_and_assumptions: Optional[str], mission_and_objectives: Optional[str],
23-
environment_variables: Optional[dict] = None):
19+
def __init__(
20+
self,
21+
embodiment_and_persona: Optional[str] = None,
22+
about_your_operators: Optional[str] = None,
23+
critical_instructions: Optional[str] = None,
24+
constraints_and_guardrails: Optional[str] = None,
25+
about_your_environment: Optional[str] = None,
26+
about_your_capabilities: Optional[str] = None,
27+
nuance_and_assumptions: Optional[str] = None,
28+
mission_and_objectives: Optional[str] = None,
29+
environment_variables: Optional[dict] = None
30+
):
2431
self.embodiment = embodiment_and_persona
2532
self.about_your_operators = about_your_operators
2633
self.critical_instructions = critical_instructions
@@ -31,7 +38,6 @@ def __init__(self, embodiment_and_persona: Optional[str], about_your_operators:
3138
self.mission_and_objectives = mission_and_objectives
3239
self.environment_variables = environment_variables
3340

34-
3541
def as_message(self) -> tuple:
3642
"""Return the robot prompts as a tuple of strings for use with OpenAI tools."""
3743
return "system", str(self)

src/rosa/rosa.py

+25-25
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
1516
from langchain.agents import AgentExecutor
1617
from langchain.agents.format_scratchpad.openai_tools import format_to_openai_tool_messages
1718
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
@@ -21,9 +22,7 @@
2122
from langchain_openai import AzureChatOpenAI, ChatOpenAI
2223
from langchain_community.callbacks import get_openai_callback
2324
from rich import print
24-
from rich.console import Console
2525
from typing import Literal, Union, Optional
26-
from rich.markdown import Markdown
2726

2827
try:
2928
from .prompts import system_prompts, RobotSystemPrompts
@@ -39,11 +38,10 @@ class ROSA:
3938
4039
Args:
4140
ros_version: The version of ROS that the agent will interact with. This can be either 1 or 2.
42-
llm: The language model to use for generating responses. This can be either an instance of AzureChatOpenAI
43-
or ChatOpenAI.
44-
robot_tools: A list of ROS tools to use with the agent. This can be a list of ROS tools from the ROSATools class.
45-
robot_prompts: A list of prompts to use with the agent. This can be a list of prompts from the RobotSystemPrompts
46-
class.
41+
llm: The language model to use for generating responses. This can be either an instance of AzureChatOpenAI or ChatOpenAI.
42+
tools: A list of LangChain tool functions to use with the agent.
43+
tool_packages: A list of Python packages that contain LangChain tool functions to use with the agent.
44+
robot_prompts: A list of prompts to use with the agent. This can be a list of prompts from the RobotSystemPrompts class.
4745
verbose: A boolean flag that indicates whether to print verbose output.
4846
blacklist: A list of ROS tools to exclude from the agent. This can be a list of ROS tools from the ROSATools class.
4947
accumulate_chat_history: A boolean flag that indicates whether to accumulate chat history.
@@ -54,8 +52,9 @@ def __init__(
5452
self,
5553
ros_version: Literal[1, 2],
5654
llm: Union[AzureChatOpenAI, ChatOpenAI],
57-
robot_tools: Optional[list] = None,
58-
robot_prompts: Optional[RobotSystemPrompts] = None,
55+
tools: Optional[list] = None,
56+
tool_packages: Optional[list] = None,
57+
prompts: Optional[RobotSystemPrompts] = None,
5958
verbose: bool = False,
6059
blacklist: Optional[list] = None,
6160
accumulate_chat_history: bool = True,
@@ -69,17 +68,16 @@ def __init__(
6968
self.__show_token_usage = show_token_usage
7069
self.__blacklist = blacklist if blacklist else []
7170
self.__accumulate_chat_history = accumulate_chat_history
72-
self.__tools = self.__get_tools(ros_version, robot_tools, self.__blacklist)
73-
self.__prompts = self.__get_prompts(robot_prompts)
71+
self.__tools = self.__get_tools(ros_version, packages=tool_packages, tools=tools, blacklist=self.__blacklist)
72+
self.__prompts = self.__get_prompts(prompts)
7473
self.__llm_with_tools = llm.bind_tools(self.__tools.get_tools())
7574
self.__agent = self.__get_agent()
7675
self.__executor = self.__get_executor(verbose=verbose)
7776

78-
def clear_chat_history(self):
79-
pass
80-
81-
def clear_screen(self):
82-
pass
77+
def clear_chat(self):
78+
"""Clear the chat history."""
79+
self.__chat_history = []
80+
os.system("clear")
8381

8482
def invoke(self, query: str) -> str:
8583
"""Invoke the agent with a user query."""
@@ -119,17 +117,19 @@ def __get_executor(self, verbose: bool):
119117

120118
def __get_agent(self):
121119
agent = ({
122-
"input": lambda x: x["input"],
123-
"agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
124-
"chat_history": lambda x: x["chat_history"],
125-
} | self.__prompts | self.__llm_with_tools | OpenAIToolsAgentOutputParser())
120+
"input": lambda x: x["input"],
121+
"agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
122+
"chat_history": lambda x: x["chat_history"],
123+
} | self.__prompts | self.__llm_with_tools | OpenAIToolsAgentOutputParser())
126124
return agent
127125

128-
def __get_tools(self, ros_version: Literal[1, 2], robot_tools: Optional[list], blacklist: Optional[list]):
129-
tools = ROSATools(ros_version, blacklist=blacklist)
130-
if robot_tools:
131-
tools.add(robot_tools, blacklist=blacklist)
132-
return tools
126+
def __get_tools(self, ros_version: Literal[1, 2], packages: Optional[list], tools: Optional[list], blacklist: Optional[list]):
127+
rosa_tools = ROSATools(ros_version, blacklist=blacklist)
128+
if tools:
129+
rosa_tools.add_tools(tools)
130+
if packages:
131+
rosa_tools.add_packages(packages, blacklist=blacklist)
132+
return rosa_tools
133133

134134
def __get_prompts(self, robot_prompts: Optional[RobotSystemPrompts] = None):
135135
prompts = system_prompts

src/rosa/tools/__init__.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class ROSATools:
5454
def __init__(self, ros_version: Literal[1, 2], blacklist: Optional[List[str]] = None):
5555
self.__tools: list = []
5656
self.__ros_version = ros_version
57+
self.__blacklist = blacklist
5758

5859
# Add the default tools
5960
from . import calculation, log, ros1, ros2, system
@@ -79,6 +80,13 @@ def __init__(self, ros_version: Literal[1, 2], blacklist: Optional[List[str]] =
7980
def get_tools(self) -> List[Tool]:
8081
return self.__tools
8182

83+
def __add_tool(self, tool):
84+
if hasattr(tool, 'name') and hasattr(tool, 'func'):
85+
if self.__blacklist and 'blacklist' in tool.func.__code__.co_varnames:
86+
# Inject the blacklist into the tool function
87+
tool.func = inject_blacklist(self.__blacklist)(tool.func)
88+
self.__tools.append(tool)
89+
8290
def __iterative_add(self, package, blacklist: Optional[List[str]] = None):
8391
"""
8492
Iterate through a package and add each @tool to the tools list.
@@ -89,17 +97,22 @@ def __iterative_add(self, package, blacklist: Optional[List[str]] = None):
8997
for tool_name in dir(package):
9098
if not tool_name.startswith("_"):
9199
t = getattr(package, tool_name)
92-
if hasattr(t, 'name') and hasattr(t, 'func'):
93-
if blacklist and 'blacklist' in t.func.__code__.co_varnames:
94-
# Inject the blacklist into the tool function
95-
t.func = inject_blacklist(blacklist)(t.func)
96-
self.__tools.append(t)
100+
self.__add_tool(t)
97101

98-
def add(self, tool_packages: List, blacklist: Optional[List[str]] = None):
102+
def add_packages(self, tool_packages: List, blacklist: Optional[List[str]] = None):
99103
"""
100-
Add a list of tools to the Tools object.
104+
Add a list of tools to the Tools object by iterating through each package.
101105
102106
:param tool_packages: A list of tool packages to add to the Tools object.
103107
"""
104108
for pkg in tool_packages:
105109
self.__iterative_add(pkg, blacklist=blacklist)
110+
111+
def add_tools(self, tools: list):
112+
"""
113+
Add a single tool to the Tools object.
114+
115+
:param tools: A list of tools to add
116+
"""
117+
for tool in tools:
118+
self.__add_tool(tool)

src/turtle_agent/requirements.txt

-1
This file was deleted.

src/turtle_agent/scripts/llm.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright (c) 2024. Jet Propulsion Laboratory. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import dotenv
16+
import os
17+
from azure.identity import ClientSecretCredential, get_bearer_token_provider
18+
from langchain_openai import AzureChatOpenAI
19+
20+
21+
def get_llm():
22+
"""A helper function to get the LLM instance."""
23+
dotenv.load_dotenv(dotenv.find_dotenv())
24+
25+
APIM_SUBSCRIPTION_KEY = os.getenv("APIM_SUBSCRIPTION_KEY")
26+
default_headers = {}
27+
if APIM_SUBSCRIPTION_KEY != None:
28+
# only set this if the APIM API requires a subscription...
29+
default_headers["Ocp-Apim-Subscription-Key"] = APIM_SUBSCRIPTION_KEY
30+
31+
# Set up authority and credentials for Azure authentication
32+
credential = ClientSecretCredential(
33+
tenant_id=os.getenv("AZURE_TENANT_ID"),
34+
client_id=os.getenv("AZURE_CLIENT_ID"),
35+
client_secret=os.getenv("AZURE_CLIENT_SECRET"),
36+
authority="https://login.microsoftonline.com",
37+
)
38+
39+
token_provider = get_bearer_token_provider(
40+
credential, "https://cognitiveservices.azure.com/.default"
41+
)
42+
43+
llm = AzureChatOpenAI(
44+
azure_deployment=os.getenv("DEPLOYMENT_ID"),
45+
azure_ad_token_provider=token_provider,
46+
openai_api_type="azure_ad",
47+
api_version=os.getenv("API_VERSION"),
48+
azure_endpoint=os.getenv("API_ENDPOINT"),
49+
default_headers=default_headers
50+
)
51+
52+
return llm

0 commit comments

Comments
 (0)