This is an old revision of the document!


Lab 9: Do What I Say: LLM Control for Pupper

Downloading the llm_lab starter folder

For this lab, use the starter archive provided on OCW:

Download llm_lab.zip

The original repository contains the ROS2/Pupper files. The archive `llm_lab.zip` adds a simplified sandbox for testing the command pipeline before running it on the real robot.

Download `llm_lab.zip` and extract it in the root folder of the repository:

cd ~/lab_9_fall_2025
unzip llm_lab.zip
ls llm_lab

After extraction, the repository should contain:

lab_9_fall_2025/
└── llm_lab/
    ├── commands_config.json
    ├── llm_prompt.txt
    ├── command_parser.py
    ├── karel_pupper.py
    ├── karel_commander.py
    ├── mock_llm.py
    ├── run_karel_test.py
    ├── run_mock_pipeline.py
    ├── README.md
    └── outputs/

The folder `llm_lab` is used first for local testing. After the local pipeline works, students must connect the command parser to a real LLM and then use the validated commands to control the real Pupper robot.

Environment setup for ROS2 simulation

This lab uses a ROS2 simulator before commands are tested on the real Pupper robot.

The recommended environment is:

Ubuntu 24.04 / WSL2
ROS2 Jazzy
Python 3 virtual environment
Ollama or another approved local/instructor-provided LLM

The archive for this lab contains both the LLM sandbox and a minimal ROS2 simulator package:

lab_9_fall_2025/
├── llm_lab/
│   ├── command_parser.py
│   ├── karel_commander.py
│   ├── karel_pupper.py
│   ├── ros2_pupper_bridge.py
│   ├── real_llm.py
│   ├── run_sim_llm_pipeline.py
│   ├── run_voice_sim_pipeline.py
│   └── ...
└── src/
    └── llm_pupper_sim/
        ├── package.xml
        ├── setup.py
        ├── launch/
        │   └── pupper_sim.launch.py
        └── llm_pupper_sim/
            ├── pupper_sim_node.py
            └── pupper_marker_node.py

The ROS2 simulator package subscribes to:

/cmd_vel
/pupper/high_level_command

and publishes:

/pupper/sim_pose
/pupper/marker

The LLM must never publish directly to these ROS2 topics.

The safe structure is:

LLM output
    -> sanitize_commands()
    -> KarelPupper API
    -> ros2_pupper_bridge.py
    -> ROS2 topics
    -> simulator

Installing ROS2 Jazzy on Ubuntu 24.04

If ROS2 is not installed, install ROS2 Jazzy first.

Check the Ubuntu version:

lsb_release -a

For Ubuntu 24.04, use ROS2 Jazzy.

Enable the required repositories:

sudo apt update
sudo apt install software-properties-common curl -y
sudo add-apt-repository universe -y
sudo apt update

Add the ROS2 apt source:

export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F'"' '{print $4}')
 
curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo $VERSION_CODENAME)_all.deb"
 
sudo dpkg -i /tmp/ros2-apt-source.deb
sudo apt update 

Install ROS2 Jazzy and development tools:

sudo apt install ros-jazzy-desktop python3-argcomplete ros-dev-tools -y

Source ROS2:

source /opt/ros/jazzy/setup.bash

Optional: add ROS2 to `.bashrc`:

echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc
source ~/.bashrc

Check that ROS2 works:

ros2 --help

Building the simulator package

Go to the root folder of the lab:

cd ~/lab_9_fall_2025

or, if the lab is stored inside the Windows filesystem from WSL:

cd /mnt/c/Users/<your_user>/Documents/sumer_school/llm_lab/lab_9_fall_2025

Build the simulator package:

source /opt/ros/jazzy/setup.bash
colcon build --packages-select llm_pupper_sim
source install/setup.bash

A successful build should create:

build/
install/
log/

and the local setup file:

install/setup.bash

Python environment for the LLM lab

Ubuntu 24 may block global `pip install` commands because the system Python environment is externally managed.

Use a virtual environment instead.

From the `llm_lab` folder:

cd ~/lab_9_fall_2025/llm_lab
 
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
 
python -m pip install --upgrade pip
python -m pip install requests sounddevice scipy faster-whisper 

The option `–system-site-packages` is important because it allows the virtual environment to see ROS2 Python packages such as `rclpy`.

Check that both ROS2 and the lab dependencies are visible:

python -c "import rclpy; print('rclpy OK')"
python -c "import requests; print('requests OK')"

Local LLM access

The lab can use a local LLM through Ollama.

If Ollama runs inside the same Ubuntu / WSL environment, the default URL is:

http://localhost:11434/api/generate

If Ollama runs on Windows and the ROS2 pipeline runs in WSL, expose Ollama from Windows and call it from WSL.

In Windows PowerShell, start Ollama with:

$env:OLLAMA_HOST="0.0.0.0:11434"
ollama serve

If Windows Firewall blocks access from WSL, add an inbound rule for port 11434.

In WSL, find the Windows host IP:

WIN_HOST=$(ip route | awk '/default/ {print $3}')
echo $WIN_HOST

Test access to Ollama:

curl http://$WIN_HOST:11434/api/tags

Then set the LLM URL:

export OLLAMA_URL="http://$WIN_HOST:11434/api/generate"
export OLLAMA_MODEL="llama3.2:3b"

If Ollama runs directly in WSL, use:

export OLLAMA_URL="http://localhost:11434/api/generate"
export OLLAMA_MODEL="llama3.2:3b"

Lab idea

In this lab, you will build a natural language command pipeline for Pupper.

The final pipeline is:

human command
    -> real LLM parser
    -> safety filter
    -> KarelPupper API
    -> real Pupper action

The LLM must not execute Python code directly. It is only allowed to return commands from a fixed list.

Allowed commands:

MOVE_FORWARD
MOVE_BACKWARD
TURN_LEFT
TURN_RIGHT
SIT
STAND
WAVE
STOP

The safety filter checks the LLM output before any robot command is executed.

Step 1 - Test the local KarelPupper API

Go to the lab folder:

cd ~/lab_9_fall_2025/llm_lab

Run:

python3 run_karel_test.py

Expected output:

[MOCK PUPPER] stand
[MOCK PUPPER] move forward
[MOCK PUPPER] turn left
[MOCK PUPPER] wave
[MOCK PUPPER] sit
[MOCK PUPPER] stop

This confirms that the high-level Pupper API works in mock mode.

Step 2 - Inspect the allowed commands

Open:

nano commands_config.json

This file contains the commands that the robot is allowed to execute.

The safety rule is:

  • if a command is in the allowed list, it may be executed;
  • if a command is not in the allowed list, it must be rejected.

Step 3 - Inspect the safety parser

Open:

nano command_parser.py

Find the function:

sanitize_commands(raw_commands)

This function validates the output generated by the LLM.

The LLM output is never trusted directly. It must always pass through this safety layer before reaching the robot.

Step 4 - Run the baseline mock pipeline

Run:

python3 run_mock_pipeline.py

Try commands such as:

Please move forward and then turn left.
Can you say hello?
Go back and stop.
Run into the wall.

The mock pipeline uses a simple local parser. This is only the baseline. It is used to understand the command flow before connecting a real LLM.

Exercise 1 - Connect a real LLM

Your first task is to replace the mock parser with a real LLM call.

The LLM must receive the user command and return only commands from the allowed list.

The LLM may be:

  • a local LLM running on your computer;
  • an instructor-provided API;
  • another LLM option approved by the instructor.

You are not required to use a paid personal API key.

The important requirement is that the LLM must transform natural language into robot commands.

Open the prompt file:

nano llm_prompt.txt

The prompt must force the model to follow these rules:

  • return only commands from the allowed list;
  • return one command per line;
  • do not explain;
  • do not invent new commands;
  • return `STOP` for unsafe or impossible requests.

Allowed commands:

MOVE_FORWARD
MOVE_BACKWARD
TURN_LEFT
TURN_RIGHT
SIT
STAND
WAVE
STOP

You must implement or adapt a real LLM call so that a command such as:

Please move forward and then turn left.

returns:

MOVE_FORWARD
TURN_LEFT

You can create a new file for the real LLM parser, for example:

nano real_llm.py

The function should have this structure:

def real_llm_response(user_text):
    """
    Sends user_text to a real LLM and returns the raw model output.
    The model output must contain only commands, one per line.
    """
    # TODO: call a real LLM here
    return raw_llm_output

If you use a local LLM, the function may call a local server such as Ollama.

Example structure:

def real_llm_response(user_text):
    prompt = read_prompt_from_file("llm_prompt.txt")
 
```
# TODO: send prompt + user_text to a local LLM
# Example target: a local LLM server running on your computer
 
return raw_llm_output
```

If you use an instructor-provided API, the function may call that API instead.

After receiving the LLM output, pass it through the safety parser before executing it.

The required pipeline is:

user text
    -> real LLM
    -> raw command output
    -> sanitize_commands()
    -> KarelPupper API

Do not execute code generated by the LLM.

The LLM is allowed to output only text commands. The program decides whether those commands are valid.

Free local LLM option

A paid API key is not required for this lab.

A free option is to run a local LLM on your computer and call it from Python.

One possible setup is:

student command
    -> local LLM
    -> command text
    -> sanitize_commands()
    -> KarelPupper API

Example local LLM flow:

1. Start a local LLM server.
2. Load a small instruction-following model.
3. Send the contents of llm_prompt.txt plus the user command.
4. Receive the model output.
5. Validate the output with sanitize_commands().

Example Python structure:

import requests
from pathlib import Path
 
ROOT = Path(**file**).resolve().parent
PROMPT_PATH = ROOT / "llm_prompt.txt"
 
def real_llm_response(user_text):
prompt = PROMPT_PATH.read_text(encoding="utf-8")
 
```
response = requests.post(
    "http://localhost:11434/api/generate",
    json={
        "model": "llama3.2:3b",
        "prompt": prompt + "\\n\\nUser: " + user_text + "\\nCommands:",
        "stream": False,
        "options": {
            "temperature": 0
        }
    },
    timeout=60
)
 
response.raise_for_status()
return response.json()["response"].strip()
```

This is only an example. The exact local model may be changed by the instructor.

Even when using a local LLM, the output must still pass through:

sanitize_commands()

The local LLM is not allowed to execute code or send commands directly to the robot.

Exercise 2 - Compare mock parser and real LLM parser

Test at least 10 natural language commands.

Your tests must include:

  • simple commands;
  • multi-step commands;
  • polite commands;
  • ambiguous commands;
  • unsafe commands.

Use the mock parser first, then the real LLM parser.

Example table:

User command Mock parser output Real LLM output Expected output Correct?
Please go forward MOVE_FORWARD MOVE_FORWARD
Turn left and sit TURN_LEFT, SIT TURN_LEFT, SIT
Can you say hello? WAVE WAVE
Run into the wall STOP STOP

Write a short conclusion explaining where the real LLM performs better than the simple mock parser.

Before Exercise 3 - How speech-to-text works

Before controlling Pupper using voice, you need to transform spoken audio into text.

This step is called speech-to-text or audio transcription.

The idea is simple:

spoken command
    -> audio recording
    -> speech-to-text model
    -> transcribed text

For example:

Audio: "Please move forward and then turn left."

should become:

Please move forward and then turn left.

After that, the transcribed text is sent to the LLM command parser, just like a normal typed command.

The full voice pipeline becomes:

microphone
    -> audio file
    -> speech-to-text
    -> transcribed text
    -> LLM command parser
    -> sanitize_commands()
    -> KarelPupper API

Important: the speech-to-text result is only text. It must not be executed directly. It must still go through the LLM parser and the safety filter.

Speech-to-text options

There are several ways to implement speech-to-text.

You are not required to use a paid transcription API.

The recommended order is:

1. Try a local speech-to-text option.
2. If available, use an instructor-provided API.
3. Use another approved transcription tool if needed.

Option 1 - Local speech-to-text model

A local speech-to-text model runs on your own computer.

Advantages:

  • does not require a paid API key;
  • can work offline after installation;
  • is suitable for a lab environment;
  • avoids sending audio to an external service.

Disadvantages:

  • may be slower on weak laptops;
  • may require additional installation;
  • transcription quality depends on the model and microphone.

The pipeline is:

microphone
    -> audio recording
    -> local speech-to-text model
    -> transcribed text
    -> LLM command parser

Example structure:

def speech_to_text(audio_path):
    """
    Receives the path to an audio file.
    Returns the transcribed text.
    """
    # TODO: run a local speech-to-text model here
    return transcribed_text

Option 2 - Instructor-provided transcription API

If the instructor provides an API key or a shared transcription service, you may use it.

Advantages:

  • easier to integrate;
  • usually good transcription quality;
  • less local setup.

Disadvantages:

  • requires internet access;
  • may have usage limits;
  • should not use students' personal paid keys unless explicitly allowed.

Example structure:

def speech_to_text(audio_path):
    with open(audio_path, "rb") as audio_file:
        # send audio_file to the instructor-provided transcription API
        # receive transcription
        pass
 
```
return transcription_text
```

Option 3 - Realtime speech-to-text

Realtime speech-to-text processes microphone input continuously.

This is more advanced and should not be the first implementation.

For this lab, start with short audio recordings of 3-5 seconds. After that works, realtime transcription can be added later.

For this lab, use the simple recording-based approach:

1. Record 3-5 seconds of audio.
2. Save the audio as a .wav file.
3. Transcribe the .wav file using a local model or instructor-provided service.
4. Receive the transcription.
5. Send the transcription to the LLM command parser.
6. Validate the LLM output using sanitize_commands().
7. Execute only validated commands.

The voice command pipeline should look like this:

audio_path = record_audio()
text = speech_to_text(audio_path)
 
raw_llm_output = real_llm_response(text)
commands = sanitize_commands(raw_llm_output.splitlines())
 
execute_commands(robot, commands) 

Exercise 3 - Add voice input

Using the speech-to-text approach explained above, extend the pipeline so that the user can speak commands instead of typing them.

Start with short recorded commands of 3-5 seconds. Do not begin with realtime transcription.

The required pipeline is:

microphone
    -> audio recording
    -> speech-to-text
    -> transcribed text
    -> real LLM parser
    -> sanitize_commands()
    -> KarelPupper API

Test the voice pipeline in mock mode first.

Example spoken commands:

Stand up.
Move forward.
Turn left.
Turn right.
Sit down.
Say hello.
Stop.
Run into the wall.

For each test, record:

  • the spoken command;
  • the transcription;
  • the LLM output;
  • the validated commands;
  • the mock robot output.

Example table:

Spoken command Transcription LLM output Validated commands Mock output
Move forward Move forward MOVE_FORWARD MOVE_FORWARD [MOCK PUPPER] move forward
Turn left and sit Turn left and sit TURN_LEFT, SIT TURN_LEFT, SIT [MOCK PUPPER] turn left / sit
Run into the wall Run into the wall STOP STOP [MOCK PUPPER] stop

Common speech-to-text problems

Problem 1 - The microphone is not detected

Check that your system can access the microphone.

Try recording a short audio file first, without using the LLM.

Problem 2 - The transcription is wrong

Speak clearly and use short commands.

Bad input:

Can you maybe, like, go there and do the thing?

Better input:

Move forward and turn left.

Problem 3 - The LLM receives bad text

If the transcription is wrong, the LLM may produce the wrong command.

That is why the report must include the transcription, not only the final robot action.

Problem 4 - The transcription contains extra words

This is normal. The LLM parser should still extract the intended command.

Example:

Please, can you move forward a little bit?

Expected command:

MOVE_FORWARD

Problem 5 - The voice command is unsafe

If the user says something unsafe, the final validated command should be:

STOP

Example:

Run into the wall.

Expected output:

STOP

Exercise 4 - Test the pipeline in the ROS2 simulator

Before running commands on the real Pupper robot, test the validated command pipeline in simulation.

The simulator step is important because it lets you check whether the command generated by the LLM is correctly mapped to a robot action before using the physical robot.

The simulated pipeline is:

voice or text command
    -> speech-to-text, if voice is used
    -> real LLM parser
    -> sanitize_commands()
    -> KarelPupper API
    -> ROS2 simulator / RViz / Gazebo

The LLM must still not publish directly to ROS2 topics. The output of the LLM must always pass through:

sanitize_commands()

Only validated commands may be sent to the robot API.

4.1 Start the simulator

Open a terminal and source the ROS2 workspace used by the Pupper repository.

Example:

cd ~/lab_9_fall_2025
source install/setup.bash

or, if the workspace is in another folder:

source ~/ros2_ws/install/setup.bash

Then start the simulator using the launch file provided by the repository.

Example structure:

ros2 launch <pupper_simulation_package> <simulation_launch_file>.py

Use the exact simulation launch command provided by the instructor or by the repository.

Depending on the repository, the simulator may use RViz, Gazebo, or both.

4.2 Connect KarelPupper to the simulated robot

The starter version of `KarelPupper` runs in mock mode and only prints actions.

For the simulator, you must connect the high-level KarelPupper functions to the simulated robot interface.

Open:

nano llm_lab/karel_pupper.py

Find the high-level methods:

def move_forward(self):
    ...
 
def turn_left(self):
...
 
def turn_right(self):
...
 
def sit(self):
...
 
def stand(self):
...
 
def wave(self):
...
 
def stop(self):
... 

Modify the implementation so that these functions send commands to the simulated Pupper robot using the existing ROS2/Pupper interface.

Do not send raw LLM output directly to ROS2.

The correct structure is:

LLM output
    -> sanitize_commands()
    -> MOVE_FORWARD / TURN_LEFT / SIT / ...
    -> KarelPupper method
    -> ROS2 simulator command

4.3 Test text commands in simulation

First test the simulator with text input.

Use at least the following commands:

Stand up.
Move forward.
Turn left.
Turn right.
Sit down.
Stop.

For each command, record:

  • the input text;
  • the LLM output;
  • the validated command list;
  • the simulator behavior.

Example table:

Input command LLM output Validated commands Simulator behavior
Stand up STAND STAND Simulated robot stands
Move forward MOVE_FORWARD MOVE_FORWARD Simulated robot moves forward
Turn left TURN_LEFT TURN_LEFT Simulated robot turns left
Stop STOP STOP Simulated robot stops

4.4 Test voice commands in simulation

After text commands work, test the same pipeline using voice input.

The voice simulator pipeline is:

microphone
    -> speech-to-text
    -> transcribed text
    -> real LLM parser
    -> sanitize_commands()
    -> KarelPupper API
    -> ROS2 simulator / RViz / Gazebo

Test at least 5 spoken commands:

Stand up.
Move forward.
Turn left.
Turn right and sit.
Stop.

For each spoken command, record:

  • the spoken command;
  • the transcription;
  • the LLM output;
  • the validated commands;
  • the simulator behavior.

Example table:

Spoken command Transcription LLM output Validated commands Simulator behavior
Move forward Move forward MOVE_FORWARD MOVE_FORWARD Simulated robot moves forward
Turn left and sit Turn left and sit TURN_LEFT, SIT TURN_LEFT, SIT Simulated robot turns left, then sits
Stop Stop STOP STOP Simulated robot stops

Exercise 5 - Connect the validated commands to the real Pupper robot

After the text pipeline, voice pipeline and simulator pipeline work, connect the validated command pipeline to the real Pupper robot.

This step must be done under instructor supervision.

Before running anything on the real robot:

  • test the command in mock mode;
  • test the command in the simulator;
  • make sure the emergency stop is available;
  • place the robot in a safe open area;
  • use only high-level KarelPupper commands;
  • do not bypass the safety parser.

The real robot pipeline must be:

human text or voice command
    -> speech-to-text, if voice is used
    -> real LLM
    -> sanitize_commands()
    -> KarelPupper command
    -> real Pupper action

The LLM is not allowed to control motors directly. It may only select one or more commands from the allowed command list.

Modify the real robot backend in:

nano llm_lab/karel_pupper.py

The same high-level methods should be used:

robot.stand()
robot.move_forward()
robot.turn_left()
robot.turn_right()
robot.sit()
robot.wave()
robot.stop()

The implementation behind these methods may use ROS2, an existing Pupper API, or another interface provided by the repository.

Exercise 6 - Demonstrate the full voice-to-Pupper pipeline

Demonstrate at least 5 commands on the real Pupper robot using voice input.

Your demonstration must include:

  • one single-step command;
  • one two-step command;
  • one command that makes the robot stop;
  • one command that is rejected or converted to `STOP`;
  • one command chosen by you.

Example spoken commands:

Stand up.
Move forward.
Turn left and sit.
Say hello.
Stop.
Run into the wall.

For each demonstrated command, record:

  • the spoken command;
  • the transcription;
  • the LLM output;
  • the validated command list;
  • the observed robot behavior.

Example table:

Spoken command Transcription LLM output Validated commands Real robot behavior
Stand up Stand up STAND STAND Robot stands
Move forward Move forward MOVE_FORWARD MOVE_FORWARD Robot moves forward
Turn left and sit Turn left and sit TURN_LEFT, SIT TURN_LEFT, SIT Robot turns left, then sits
Run into the wall Run into the wall STOP STOP Robot stops / does not execute unsafe movement

Deliverables

Submit:

  • the final LLM prompt;
  • the code or function used to call the real LLM;
  • a short note specifying which LLM option you used: local LLM, instructor-provided API, or another approved option;
  • the code or function used for speech-to-text;
  • a short note specifying which speech-to-text option you used: local model, instructor-provided API, or another approved option;
  • the code section that connects validated commands to the simulator;
  • the code section that connects validated commands to the real Pupper robot;
  • a comparison table between the mock parser and the real LLM parser;
  • a table with at least 5 voice commands, transcriptions, LLM outputs and validated commands;
  • proof that the command pipeline was tested in the ROS2 simulator;
  • proof of running at least 5 voice commands on the real Pupper robot;
  • a short explanation of the safety filter;
  • a short explanation of why simulation is used before the real robot;
  • a short explanation of why the robot should not execute raw LLM output directly.

What to remember

A language model should not be allowed to control a robot directly.

The safe design is:

voice or text input
    -> transcription, if voice is used
    -> LLM output
    -> validation
    -> allowed command
    -> high-level robot API

The LLM can interpret natural language, but the program must decide what is safe to execute.

rasb/lab/09.1782724228.txt.gz · Last modified: 2026/06/29 12:10 by vlad.radulescu2901
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0