Skip to content

Commit 0d0c1d0

Browse files
authored
Merge pull request #71 from nasa-jpl/hotfix/tiktoken-rust-dep
- Fix critical tiktoken dependency issue that prevented ROSA from running in Docker containers due to missing Rust compiler - Improve keyboard interrupt (Ctrl+C) handling to allow users to gracefully cancel long-running operations without crashing the agent - Enhance the turtle agent demo with new high-level drawing tools for creating complex shapes like rectangles, circles, polylines, and arcs - Improve documentation and parameter descriptions for turtle control tools to reduce agent confusion and improve drawing accuracy - Fix X11 forwarding configuration in demo script to support multiple operating systems and environments
2 parents db554ad + 87ca5b8 commit 0d0c1d0

File tree

13 files changed

+994
-176
lines changed

13 files changed

+994
-176
lines changed

Dockerfile

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@ RUN apt-get update && apt-get install -y \
1111
locales \
1212
xvfb \
1313
python3.9 \
14-
python3-pip
14+
python3-pip \
15+
curl \
16+
build-essential
1517

18+
# Install Rust (required for building tiktoken)
19+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
20+
ENV PATH="/root/.cargo/bin:${PATH}"
21+
22+
# Cleanup disabled for development builds
1623
# RUN apt-get clean && rm -rf /var/lib/apt/lists/*
17-
RUN python3 -m pip install -U python-dotenv catkin_tools
24+
# Upgrade pip first, then install packages
25+
RUN python3.9 -m pip install --upgrade pip
26+
RUN python3.9 -m pip install --break-system-packages python-dotenv catkin_tools
1827
RUN rosdep update && \
1928
echo "source /opt/ros/noetic/setup.bash" >> /root/.bashrc && \
2029
echo "alias start='catkin build && source devel/setup.bash && roslaunch turtle_agent agent.launch'" >> /root/.bashrc && \
@@ -25,9 +34,9 @@ WORKDIR /app/
2534

2635
# Modify the RUN command to use ARG
2736
RUN /bin/bash -c 'if [ "$DEVELOPMENT" = "true" ]; then \
28-
python3.9 -m pip install --user -e .; \
37+
python3.9 -m pip install --break-system-packages --ignore-installed --user -e .; \
2938
else \
30-
python3.9 -m pip install -U jpl-rosa>=1.0.7; \
39+
python3.9 -m pip install --break-system-packages --ignore-installed -U jpl-rosa>=1.0.8; \
3140
fi'
3241

3342
CMD ["/bin/bash", "-c", "source /opt/ros/noetic/setup.bash && \
@@ -39,5 +48,5 @@ CMD ["/bin/bash", "-c", "source /opt/ros/noetic/setup.bash && \
3948
xvfb-run -a -s \"-screen 0 1920x1080x24\" rosrun turtlesim turtlesim_node & \
4049
fi && \
4150
sleep 5 && \
42-
echo \"Run \\`start\\` to build and launch the ROSA-TurtleSim demo.\" && \
51+
echo \"Run \\`start streaming:=true\\` to build and launch the ROSA-TurtleSim demo.\" && \
4352
/bin/bash"]

demo.sh

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,35 @@ HEADLESS=${HEADLESS:-false}
2626
DEVELOPMENT=${DEVELOPMENT:-false}
2727

2828
# Enable X11 forwarding based on OS
29+
echo "Enabling X11 forwarding..."
2930
case "$(uname)" in
30-
Linux*|Darwin*)
31-
echo "Enabling X11 forwarding..."
32-
# If running under WSL, use :0 for DISPLAY
33-
if grep -q "WSL" /proc/version; then
34-
export DISPLAY=:0
35-
else
36-
export DISPLAY=host.docker.internal:0
31+
Linux*)
32+
export DISPLAY=${DISPLAY:-:0}
33+
xhost +local:docker &>/dev/null || echo "Warning: xhost command failed"
34+
# Verify X11 is working
35+
if ! xset q &>/dev/null; then
36+
echo "Error: X11 forwarding is not working. Please check your X11 server."
37+
exit 1
38+
fi
39+
;;
40+
Darwin*)
41+
# Keep XQuartz's DISPLAY or default to :0
42+
export DISPLAY=${DISPLAY:-:0}
43+
xhost +local:docker &>/dev/null || true
44+
45+
# Check if XQuartz is running and properly configured
46+
if ! pgrep -xq "Xquartz" && ! pgrep -xq "X11"; then
47+
echo "Error: XQuartz is not running. Please start XQuartz and try again."
48+
exit 1
49+
fi
50+
51+
# Warn if network connections are disabled
52+
if ! defaults read org.xquartz.X11 nolisten_tcp 2>/dev/null | grep -q 0; then
53+
echo "Warning: XQuartz may not allow network connections."
54+
echo "Enable in: XQuartz Preferences > Security > 'Allow connections from network clients'"
3755
fi
38-
xhost +
3956
;;
4057
MINGW*|CYGWIN*|MSYS*)
41-
echo "Enabling X11 forwarding for Windows..."
4258
export DISPLAY=host.docker.internal:0
4359
;;
4460
*)
@@ -47,29 +63,45 @@ case "$(uname)" in
4763
;;
4864
esac
4965

50-
# Check if X11 forwarding is working
51-
if ! xset q &>/dev/null; then
52-
echo "Error: X11 forwarding is not working. Please check your X11 server and try again."
53-
exit 1
54-
fi
55-
5666
# Build and run the Docker container
5767
CONTAINER_NAME="rosa-turtlesim-demo"
68+
69+
# Detect platform for Apple Silicon
70+
PLATFORM_ARG=""
71+
if [ "$(uname -m)" = "arm64" ]; then
72+
PLATFORM_ARG="--platform linux/amd64"
73+
fi
74+
5875
echo "Building the $CONTAINER_NAME Docker image..."
59-
docker build --build-arg DEVELOPMENT=$DEVELOPMENT -t $CONTAINER_NAME -f Dockerfile . || { echo "Error: Docker build failed"; exit 1; }
76+
docker build $PLATFORM_ARG --build-arg DEVELOPMENT=$DEVELOPMENT -t $CONTAINER_NAME -f Dockerfile . || {
77+
echo "Error: Docker build failed"
78+
exit 1
79+
}
6080

6181
echo "Running the Docker container..."
62-
docker run -it --rm --name $CONTAINER_NAME \
63-
-e DISPLAY=$DISPLAY \
64-
-e HEADLESS=$HEADLESS \
65-
-e DEVELOPMENT=$DEVELOPMENT \
66-
-v /tmp/.X11-unix:/tmp/.X11-unix \
67-
-v "$PWD/src":/app/src \
68-
-v "$PWD/tests":/app/tests \
69-
--network host \
70-
$CONTAINER_NAME
82+
if [ "$(uname)" = "Darwin" ]; then
83+
# macOS: Use host.docker.internal for X11
84+
docker run -it --rm --init --name $CONTAINER_NAME \
85+
-e DISPLAY=host.docker.internal:0 \
86+
-e HEADLESS=$HEADLESS \
87+
-e DEVELOPMENT=$DEVELOPMENT \
88+
-v "$PWD/src":/app/src \
89+
-v "$PWD/tests":/app/tests \
90+
$CONTAINER_NAME
91+
else
92+
# Linux/WSL: Use unix socket
93+
docker run -it --rm --init --name $CONTAINER_NAME \
94+
-e DISPLAY=$DISPLAY \
95+
-e HEADLESS=$HEADLESS \
96+
-e DEVELOPMENT=$DEVELOPMENT \
97+
-v /tmp/.X11-unix:/tmp/.X11-unix \
98+
-v "$PWD/src":/app/src \
99+
-v "$PWD/tests":/app/tests \
100+
--network host \
101+
$CONTAINER_NAME
102+
fi
71103

72104
# Disable X11 forwarding
73-
xhost -
105+
xhost -local:docker &>/dev/null || true
74106

75107
exit 0

src/rosa/prompts.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,36 @@ def __str__(self):
6969
"interact with the robotic system you are integrated with. Your responses should be grounded in real-time "
7070
"information whenever possible using the tools available to you.",
7171
),
72+
(
73+
"system",
74+
"CRITICAL - TOOL USAGE REQUIREMENT: When a user asks you to perform an action involving ROS nodes, topics, "
75+
"or services, you MUST IMMEDIATELY use your tools to check what is available before responding. "
76+
"DO NOT say things like 'I don't see any nodes' or 'the system isn't running' or 'I can't control the robot' "
77+
"without FIRST calling the appropriate tool (like rosnode_list, rostopic_list, etc.) to verify the actual "
78+
"current state. Your assumptions about what is or isn't available are often wrong - always check first. "
79+
"If you claim something isn't available without using a tool to verify, you are making an error.",
80+
),
81+
(
82+
"system",
83+
"CRITICAL - SEQUENTIAL TOOL EXECUTION: You MUST call tools ONE AT A TIME and wait for each to complete before calling the next. "
84+
"NEVER call multiple tools in parallel in a single response. This is especially critical for drawing/movement commands. "
85+
"When you need to execute multiple operations (like drawing multiple shapes), you must: "
86+
"1. Call the FIRST tool and stop "
87+
"2. Wait for the result "
88+
"3. Then call the NEXT tool and stop "
89+
"4. Repeat until all operations complete "
90+
"Even if operations seem independent, you MUST execute them sequentially. Do not batch tool calls together.",
91+
),
92+
(
93+
"system",
94+
"WORKFLOW FOR ACTION REQUESTS: When a user asks you to perform a robotic action (move, draw, control, etc.), "
95+
"follow this workflow: "
96+
"1. FIRST: Call rosnode_list() and rostopic_list() WITHOUT any parameters to see what's available. "
97+
" Do NOT pass 'namespace' parameter unless working with a specific non-root namespace. "
98+
"2. SECOND: If relevant nodes/topics exist, proceed with the action immediately. "
99+
"3. THIRD: Only if the tools show nothing is available should you explain that to the user. "
100+
"Do NOT skip step 1. Do NOT describe what you 'would do if the system were running' - check if it IS running first.",
101+
),
72102
(
73103
"system",
74104
"When asked to provide names of topics or nodes, first retrieve a list of available names using the "
@@ -92,8 +122,10 @@ def __str__(self):
92122
),
93123
(
94124
"system",
95-
"You must use your math tools to perform calculations. Failing to do this may result in a catastrophic "
96-
"failure of the system. You must never perform calculations manually or assume you know the correct answer. ",
125+
"You must use your math tools to perform calculations, especially for angles, distances, coordinates, and "
126+
"geometric computations. Failing to do this may result in incorrect commands or system failures. You must "
127+
"never perform calculations manually in your reasoning - always use the provided calculation tools to ensure "
128+
"accuracy. This is critical for robotics operations where precision matters.",
97129
),
98130
(
99131
"system",

src/rosa/rosa.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class ROSA:
4747
accumulate_chat_history (bool): Whether to accumulate chat history. Defaults to True.
4848
show_token_usage (bool): Whether to show token usage. Does not work when streaming is enabled. Defaults to False.
4949
streaming (bool): Whether to stream the output of the agent. Defaults to True.
50+
max_iterations (int): Maximum number of iterations for the agent executor. Defaults to 100.
51+
return_intermediate_steps (bool): Whether to return intermediate steps in the agent's execution.
52+
Setting to True increases memory usage but provides detailed execution traces. Defaults to False.
5053
5154
Attributes:
5255
chat_history (list): A list of messages representing the chat history.
@@ -75,6 +78,8 @@ def __init__(
7578
accumulate_chat_history: bool = True,
7679
show_token_usage: bool = False,
7780
streaming: bool = True,
81+
max_iterations: int = 100,
82+
return_intermediate_steps: bool = False,
7883
):
7984
self.__chat_history = []
8085
self.__ros_version = ros_version
@@ -84,6 +89,8 @@ def __init__(
8489
self.__blacklist = blacklist if blacklist else []
8590
self.__accumulate_chat_history = accumulate_chat_history
8691
self.__streaming = streaming
92+
self.__max_iterations = max_iterations
93+
self.__return_intermediate_steps = return_intermediate_steps
8794
self.__tools = self._get_tools(
8895
ros_version, packages=tool_packages, tools=tools, blacklist=self.__blacklist
8996
)
@@ -129,6 +136,9 @@ def invoke(self, query: str) -> str:
129136
{"input": query, "chat_history": self.__chat_history}
130137
)
131138
self._print_usage(cb)
139+
except KeyboardInterrupt:
140+
# Re-raise KeyboardInterrupt so it can be handled upstream
141+
raise
132142
except Exception as e:
133143
return f"An error occurred: {str(e)}"
134144

@@ -215,6 +225,9 @@ async def astream(self, query: str) -> AsyncIterable[Dict[str, Any]]:
215225

216226
if final_output:
217227
self._record_chat_history(query, final_output)
228+
except KeyboardInterrupt:
229+
# Re-raise KeyboardInterrupt so it can be handled upstream
230+
yield {"type": "error", "content": "Operation interrupted by user"}
218231
except Exception as e:
219232
yield {"type": "error", "content": f"An error occurred: {e}"}
220233

@@ -225,6 +238,9 @@ def _get_executor(self, verbose: bool) -> AgentExecutor:
225238
tools=self.__tools.get_tools(),
226239
stream_runnable=self.__streaming,
227240
verbose=verbose,
241+
max_iterations=self.__max_iterations,
242+
handle_parsing_errors=True,
243+
return_intermediate_steps=self.__return_intermediate_steps,
228244
)
229245
return executor
230246

src/rosa/tools/calculation.py

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,13 @@ def acos(x_values: List[float]) -> List[dict]:
238238

239239
@tool
240240
def atan(x_values: List[float]) -> List[dict]:
241-
"""Performs arctangent on the input values x.
241+
"""
242+
Calculate arctangent (inverse tangent) of input values. Returns angle in radians.
243+
Use this to find an angle from a slope (rise/run).
244+
245+
For finding angle from point A to point B, use atan2 instead (it's better).
246+
247+
Example: atan(1) = π/4 ≈ 0.785 radians = 45 degrees
242248
243249
:arg x_values: A list of values x (e.g. [x1, x2, ...])
244250
"""
@@ -317,24 +323,111 @@ def count_lines(text: str) -> int:
317323
@tool
318324
def degrees_to_radians(degrees: List[float]):
319325
"""
320-
Convert degrees to radians.
326+
Convert degrees to radians. Use this for angle conversions.
321327
322328
:param degrees: A list of one or more degrees to convert to radians.
323329
"""
324330
rads = {}
325331
for degree in degrees:
326-
rads[degree] = f"{degree * (3.14159 / 180)} radians."
332+
rads[degree] = degree * (math.pi / 180)
327333
return rads
328334

329335

330336
@tool
331337
def radians_to_degrees(radians: List[float]):
332338
"""
333-
Convert radians to degrees.
339+
Convert radians to degrees. Use this for angle conversions.
334340
335341
:param radians: A list of one or more radians to convert to degrees.
336342
"""
337343
degs = {}
338344
for radian in radians:
339-
degs[radian] = f"{radian * (180 / 3.14159)} degrees."
345+
degs[radian] = radian * (180 / math.pi)
340346
return degs
347+
348+
349+
@tool
350+
def sqrt(x_values: List[float]) -> List[dict]:
351+
"""
352+
Calculate the square root of input values. Essential for distance calculations.
353+
354+
:arg x_values: A list of values x (e.g. [x1, x2, ...])
355+
"""
356+
results = []
357+
for x in x_values:
358+
if x < 0:
359+
result = {f"sqrt({x})": "undefined (negative number)"}
360+
else:
361+
result = {f"sqrt({x})": math.sqrt(x)}
362+
results.append(result)
363+
return results
364+
365+
366+
@tool
367+
def atan2(pairs: List[tuple]) -> List[dict]:
368+
"""
369+
Calculate the angle (in radians) from the positive x-axis to the point (x, y).
370+
This is the MOST IMPORTANT tool for calculating angles between two points.
371+
372+
To find the angle from point (x1, y1) to point (x2, y2):
373+
Use atan2(y2-y1, x2-x1)
374+
375+
Example: angle from (1, 1) to (3, 4) = atan2(4-1, 3-1) = atan2(3, 2) ≈ 0.98 radians
376+
377+
:arg pairs: A list of tuples containing (y, x) values in that order, e.g., [(y1, x1), (y2, x2), ...]
378+
"""
379+
results = []
380+
for y, x in pairs:
381+
result = {f"atan2({y}, {x})": math.atan2(y, x)}
382+
results.append(result)
383+
return results
384+
385+
386+
@tool
387+
def distance_between_points(point_pairs: List[tuple]) -> List[dict]:
388+
"""
389+
Calculate the straight-line distance between two points using the Pythagorean theorem.
390+
Formula: sqrt((x2-x1)^2 + (y2-y1)^2)
391+
392+
This is essential for determining how far the turtle needs to move.
393+
394+
:arg point_pairs: A list of tuples, each containing ((x1, y1), (x2, y2))
395+
Example: [((0, 0), (3, 4))] calculates distance from (0,0) to (3,4) = 5.0
396+
"""
397+
results = []
398+
for (x1, y1), (x2, y2) in point_pairs:
399+
dx = x2 - x1
400+
dy = y2 - y1
401+
dist = math.sqrt(dx**2 + dy**2)
402+
result = {f"distance from ({x1},{y1}) to ({x2},{y2})": dist}
403+
results.append(result)
404+
return results
405+
406+
407+
@tool
408+
def calculate_line_angle_and_distance(point_pairs: List[tuple]) -> List[dict]:
409+
"""
410+
Calculate BOTH the angle (in radians) and distance needed to draw a line from point A to point B.
411+
This is a high-level helper that combines atan2 and distance calculations.
412+
413+
Use this when planning to draw a line between two specific coordinates.
414+
The angle returned is relative to the positive x-axis (right = 0, up = π/2).
415+
416+
:arg point_pairs: A list of tuples, each containing ((x1, y1), (x2, y2))
417+
Example: [((2, 3), (5, 7))] returns angle and distance from (2,3) to (5,7)
418+
"""
419+
results = []
420+
for (x1, y1), (x2, y2) in point_pairs:
421+
dx = x2 - x1
422+
dy = y2 - y1
423+
angle = math.atan2(dy, dx)
424+
distance = math.sqrt(dx**2 + dy**2)
425+
result = {
426+
f"line from ({x1},{y1}) to ({x2},{y2})": {
427+
"angle_radians": angle,
428+
"angle_degrees": angle * (180 / math.pi),
429+
"distance": distance,
430+
}
431+
}
432+
results.append(result)
433+
return results

0 commit comments

Comments
 (0)