Skip to content

Commit

Permalink
Improve ROS2 tools and add unit tests with CI (#15)
Browse files Browse the repository at this point in the history
* Simplified within_bounds function by removing redundant 'elif' condition. Improved code readability and maintainability. (#13)

* Add unit tests and CI. (#14)

* refactor: better error handling and response parsing for ROS2 tools, add blacklist where applicable.

* feat(ros2): add ros2 topic echo tool.

* chore: bump version to 1.0.4, update CHANGELOG.md

* chore: bump langchain versions.

* feat(tests): add unit tests for most tools and the ROSATools class.

* fix: passing a blacklist into any of the tools no longer overrides the blacklist passed into the ROSA constructor. They are concatenated instead.

* feat(CI): add ci workflow.

* fix: properly filter out blacklisted topics and nodes.

* feat(tests): add ros2 tests.

* feat(ci): update humble jobs.

* feat(tests): add stubs for additional test classes.

* docs: update README

* chore: bump version to 1.0.5

---------

Co-authored-by: Kejun Liu <119113065+dawnkisser@users.noreply.github.com>
  • Loading branch information
RobRoyce and dawnkisser authored Aug 23, 2024
1 parent 261c56a commit fdca5a1
Show file tree
Hide file tree
Showing 22 changed files with 1,723 additions and 123 deletions.
70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: CI Pipeline

on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev

jobs:
test-noetic:
runs-on: ubuntu-latest
container:
image: osrf/ros:noetic-desktop
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libc6 libc6-dev
sudo apt-get install -y python3.9
sudo apt-get install -y python3-pip
python3.9 -m pip install --user -e .
shell: bash

- name: Run tests
run: |
. /opt/ros/noetic/setup.bash
python3.9 -m unittest discover -s tests --verbose
shell: bash
env:
ROS_VERSION: 1

test-humble:
runs-on: ubuntu-latest
container:
image: osrf/ros:humble-desktop
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.10'

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3-pip
python3.10 -m pip install --user -e .
shell: bash

- name: Run tests
run: |
. /opt/ros/humble/setup.bash
python3.10 -m unittest discover -s tests --verbose
shell: bash
env:
ROS_VERSION: 2
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.5]

### Added

* CI pipeline for automated testing
* Unit tests for ROSA tools and utilities

### Changed

* Improvements to various ROS2 tools
* Upgrade dependencies:
* `langchain` to 0.2.14
* `langchain_core` to 0.2.34
* `langchain-openai` to 0.1.22

## [1.0.4] - 2024-08-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ RUN apt-get update && apt-get install -y \
RUN apt-get update && apt-get install -y python3.9
RUN apt-get update && apt-get install -y python3-pip
RUN python3 -m pip install -U python-dotenv catkin_tools
RUN python3.9 -m pip install -U jpl-rosa>=1.0.4
RUN python3.9 -m pip install -U jpl-rosa>=1.0.5

# Configure ROS
RUN rosdep update
Expand Down
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
<h1 align="center">ROSA - Robot Operating System Agent</h1>
<!-- ☝️ Replace with your repo name ☝️ -->
</div>
<pre align="center">ROSA is an AI Agent designed to interact with ROS-based robotics systems using natural language queries.</pre>
<!-- Header block for project -->
<pre align="center">
ROSA is an AI Agent designed to interact with ROS-based robotics systems<br>using natural language queries.
</pre>

<div align="center">

![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nasa-jpl/rosa/publish.yml)
![Static Badge](https://img.shields.io/badge/Python->=3.9-blue)
![Static Badge](https://img.shields.io/badge/ROS_1-Supported-blue)
![Static Badge](https://img.shields.io/badge/ROS_2-Supported-blue)
![Static Badge](https://img.shields.io/badge/ROS_1-Noetic-blue)
![Static Badge](https://img.shields.io/badge/ROS_2-Humble|Iron|Jazzy-blue)
![PyPI - License](https://img.shields.io/pypi/l/jpl-rosa)
[![SLIM](https://img.shields.io/badge/Best%20Practices%20from-SLIM-blue)](https://nasa-ammos.github.io/slim/)

![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nasa-jpl/rosa/ci.yml?branch=main&label=main)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nasa-jpl/rosa/ci.yml?branch=dev&label=dev)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nasa-jpl/rosa/publish.yml?label=publish)
![PyPI - Version](https://img.shields.io/pypi/v/jpl-rosa)
![PyPI - Downloads](https://img.shields.io/pypi/dw/jpl-rosa)
[![SLIM](https://img.shields.io/badge/Best%20Practices%20from-SLIM-blue)](https://nasa-ammos.github.io/slim/)

</div>
<!-- Header block for project -->


ROSA is an AI agent that can be used to interact with ROS1 _and_ ROS2 systems in order to carry out various tasks.
It is built using the open-source [Langchain](https://python.langchain.com/v0.2/docs/introduction/) framework, and can
Expand Down Expand Up @@ -90,9 +99,11 @@ rosa.invoke("Show me a list of topics that have publishers but no subscribers")

## TurtleSim Demo

We have included a demo that uses ROSA to control the TurtleSim robot in simulation. To run the demo, you will need to have Docker installed on your machine.
We have included a demo that uses ROSA to control the TurtleSim robot in simulation. To run the demo, you will need to
have Docker installed on your machine.

The following video shows ROSA reasoning about how to draw a 5-point star, then executing the necessary commands to do so.
The following video shows ROSA reasoning about how to draw a 5-point star, then executing the necessary commands to do
so.

https://github.com/user-attachments/assets/77b97014-6d2e-4123-8d0b-ea0916d93a4e

Expand Down
8 changes: 4 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

setup(
name="jpl-rosa",
version="1.0.4",
version="1.0.5",
license="Apache 2.0",
description="ROSA: the Robot Operating System Agent",
long_description=long_description,
Expand All @@ -49,10 +49,10 @@
install_requires=[
"PyYAML==6.0.1",
"python-dotenv>=1.0.1",
"langchain==0.2.13",
"langchain==0.2.14",
"langchain-community==0.2.12",
"langchain-core==0.2.32",
"langchain-openai==0.1.21",
"langchain-core==0.2.34",
"langchain-openai==0.1.22",
"pydantic",
"pyinputplus",
"azure-identity",
Expand Down
38 changes: 21 additions & 17 deletions src/rosa/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from langchain.agents import Tool


def inject_blacklist(blacklist):
def inject_blacklist(default_blacklist: List[str]):
"""
Inject a blacklist parameter into @tool functions that require it. Required because we do not
want to rely on the LLM to manually use the blacklist, as it may "forget" to do so.
Expand All @@ -32,18 +32,28 @@ def inject_blacklist(blacklist):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if "blacklist" in kwargs:
kwargs["blacklist"] = blacklist
if args and isinstance(args[0], dict):
if "blacklist" in args[0]:
args[0]["blacklist"] = default_blacklist + args[0]["blacklist"]
else:
args[0]["blacklist"] = default_blacklist
else:
params = inspect.signature(func).parameters
if "blacklist" in params:
kwargs["blacklist"] = blacklist
if "blacklist" in kwargs:
kwargs["blacklist"] = default_blacklist + kwargs["blacklist"]
else:
params = inspect.signature(func).parameters
if "blacklist" in params:
kwargs["blacklist"] = default_blacklist
return func(*args, **kwargs)

# Rebuild the signature to include 'blacklist'
sig = inspect.signature(func)
new_params = [
param.replace(default=blacklist) if param.name == "blacklist" else param
(
param.replace(default=default_blacklist)
if param.name == "blacklist"
else param
)
for param in sig.parameters.values()
]
wrapper.__signature__ = sig.replace(parameters=new_params)
Expand All @@ -68,19 +78,13 @@ def __init__(
self.__iterative_add(system)

if self.__ros_version == 1:
try:
from . import ros1
from . import ros1

self.__iterative_add(ros1, blacklist=blacklist)
except Exception as e:
print(e)
self.__iterative_add(ros1, blacklist=blacklist)
elif self.__ros_version == 2:
try:
from . import ros2
from . import ros2

self.__iterative_add(ros2, blacklist=blacklist)
except Exception as e:
print(e)
self.__iterative_add(ros2, blacklist=blacklist)
else:
raise ValueError("Invalid ROS version. Must be either 1 or 2.")

Expand Down
34 changes: 22 additions & 12 deletions src/rosa/tools/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

import os
from typing import Optional
from typing import Optional, Literal

from langchain.agents import tool

Expand All @@ -22,20 +22,28 @@
def read_log(
log_file_directory: str,
log_filename: str,
level_filter: Optional[str],
line_range: tuple = (-200, -1),
level_filter: Optional[
Literal[
"ERROR", "INFO", "DEBUG", "WARNING", "CRITICAL", "FATAL", "TRACE", "DEBUG"
]
] = None,
num_lines: Optional[int] = None,
) -> dict:
"""
Read a log file and return the log lines that match the level filter and line range.
:arg log_file_directory: The directory containing the log file to read (use your tools to get it)
:arg log_filename: The path to the log file to read
:arg level_filter: Only show log lines that contain this level (e.g. "ERROR", "INFO", "DEBUG", etc.)
:arg line_range: A tuple of two integers representing the start and end line numbers to return
:param log_file_directory: The directory containing the log file to read (use your tools to get it)
:param log_filename: The path to the log file to read
:param level_filter: Only show log lines that contain this level (e.g. "ERROR", "INFO", "DEBUG", etc.)
:param num_lines: The number of most recent lines to return from the log file
"""
if num_lines is not None and num_lines < 1:
return {"error": "Invalid `num_lines` argument. It must be a positive integer."}

if not os.path.exists(log_file_directory):
return {
"error": f"The log directory '{log_file_directory}' does not exist. You should first use your tools to get the correct log directory."
"error": f"The log directory '{log_file_directory}' does not exist. You should first use your tools to "
f"get the correct log directory."
}

full_log_path = os.path.join(log_file_directory, log_filename)
Expand All @@ -56,13 +64,15 @@ def read_log(
for i in range(len(log_lines)):
log_lines[i] = f"line {i+1}: " + log_lines[i].strip()

print(f"Reading log file '{log_filename}' lines {line_range[0]} to {line_range[1]}")
log_lines = log_lines[line_range[0] : line_range[1]]
if num_lines is not None:
# Get the most recent num_lines from the log file
log_lines = log_lines[-num_lines:]

# If there are more than 200 lines, return a message to use the line_range argument
if len(log_lines) > 200:
return {
"error": f"The log file '{log_filename}' has more than 200 lines. Please use the `line_range` argument to read a subset of the log file at a time."
"error": f"The log file '{log_filename}' has more than 200 lines. Please use the `num_lines` argument to "
f"read a subset of the log file at a time."
}

if level_filter is not None:
Expand All @@ -72,7 +82,7 @@ def read_log(
"log_filename": log_filename,
"log_file_directory": log_file_directory,
"level_filter": level_filter,
"line_range": line_range,
"requested_num_lines": num_lines,
"total_lines": total_lines,
"lines_returned": len(log_lines),
"lines": log_lines,
Expand Down
Loading

0 comments on commit fdca5a1

Please sign in to comment.