Table of Contents

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:

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:

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:

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:

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:

Disadvantages:

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:

Disadvantages:

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:

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 checks 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

The LLM must 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 ROS2 simulator

Open a terminal and 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

Source ROS2 and the local workspace:

source /opt/ros/jazzy/setup.bash
source install/setup.bash

Start the simulator:

ros2 launch llm_pupper_sim pupper_sim.launch.py

The simulator should print messages similar to:

Pupper ROS2 simulator started.
Listening on /cmd_vel and /pupper/high_level_command.
Pupper RViz marker node started.
pose x=0.00, y=0.00, theta=0.00, state=STAND

Leave this terminal open.

4.2 Check the simulator topics

Open a second terminal.

Go to the same lab folder and source the environment:

cd ~/lab_9_fall_2025
 
source /opt/ros/jazzy/setup.bash
source install/setup.bash 

List the ROS2 topics:

ros2 topic list

You should see:

/cmd_vel
/pupper/high_level_command
/pupper/sim_pose
/pupper/marker

Manually test forward movement:

ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"

Let it run for a few seconds, then stop it with `Ctrl+C`.

In the simulator terminal, the value of `pose x` should change.

Stop the simulated robot:

ros2 topic pub --once /pupper/high_level_command std_msgs/msg/String "{data: 'STOP'}"

Manually test turning:

ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.8}}"

Let it run for a few seconds, then stop it with `Ctrl+C`.

In the simulator terminal, the value of `theta` should change.

4.3 Connect KarelPupper to the ROS2 simulator

The archive already contains a ROS2 bridge:

llm_lab/ros2_pupper_bridge.py

and a simulator-aware version of:

llm_lab/karel_pupper.py

The connection is:

KarelPupper(mode="sim")
    -> ros2_pupper_bridge.py
    -> /cmd_vel
    -> /pupper/high_level_command
    -> ROS2 simulator

The command mapping is:

MOVE_FORWARD  -> /cmd_vel linear.x = 0.20
MOVE_BACKWARD -> /cmd_vel linear.x = -0.20
TURN_LEFT     -> /cmd_vel angular.z = 0.80
TURN_RIGHT    -> /cmd_vel angular.z = -0.80
STOP          -> zero velocity + STOP high-level command
SIT           -> /pupper/high_level_command
STAND         -> /pupper/high_level_command
WAVE          -> /pupper/high_level_command

Do not modify the LLM so that it publishes directly to ROS2.

The correct structure is:

LLM output
    -> sanitize_commands()
    -> allowed command
    -> KarelPupper method
    -> ROS2 simulator command

4.4 Test text commands in simulation

Keep the simulator running in the first terminal.

In the second terminal, go to the lab folder:

cd ~/lab_9_fall_2025
 
source /opt/ros/jazzy/setup.bash
source install/setup.bash
 
cd llm_lab
source .venv/bin/activate 

If the LLM runs through Ollama on Windows, set:

WIN_HOST=$(ip route | awk '/default/ {print $3}')
export OLLAMA_URL="http://$WIN_HOST:11434/api/generate"
export OLLAMA_MODEL="llama3.2:3b"

If the LLM runs through Ollama inside WSL, set:

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

Run the text-to-simulator pipeline:

python run_sim_llm_pipeline.py

Test at least the following commands:

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

For each command, record:

Example table:

Input command LLM output Validated commands Simulator behavior
Stand up STAND STAND Simulator receives STAND high-level command
Move forward MOVE_FORWARD MOVE_FORWARD `pose x` changes in the ROS2 simulator
Turn left TURN_LEFT TURN_LEFT `theta` changes in the ROS2 simulator
Stop STOP STOP Simulator receives STOP
Run into the wall STOP STOP Unsafe command is rejected and converted to STOP

4.5 Test voice commands in simulation

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

The voice simulator pipeline is:

microphone or recorded audio file
    -> speech-to-text
    -> transcribed text
    -> real LLM parser
    -> sanitize_commands()
    -> KarelPupper API
    -> ROS2 simulator

Run:

python run_voice_sim_pipeline.py

Test at least 5 spoken commands:

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

For each spoken command, record:

Example table:

Spoken command Transcription LLM output Validated commands Simulator behavior
Move forward Move forward MOVE_FORWARD MOVE_FORWARD `pose x` changes in the ROS2 simulator
Turn left and sit Turn left and sit TURN_LEFT, SIT TURN_LEFT, SIT `theta` changes, then simulator receives SIT
Stop Stop STOP STOP Simulator receives STOP
Run into the wall Run into the wall STOP STOP Unsafe command is rejected and converted to STOP

If the microphone is not available inside WSL, record a short `.wav` file on Windows and process that file from WSL.

The pipeline is still valid:

recorded voice command
    -> .wav file
    -> speech-to-text
    -> transcribed text
    -> real LLM parser
    -> sanitize_commands()
    -> ROS2 simulator

4.6 Safety check before the real robot

Before moving to the real robot, verify that:

Only after the simulator pipeline works should you continue to the real Pupper robot.

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:

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:

Example spoken commands:

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

For each demonstrated command, record:

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

Common setup problems

Problem 1 - `ros2: command not found`

ROS2 is not sourced or not installed.

Check:

ls /opt/ros

For Ubuntu 24.04, source Jazzy:

source /opt/ros/jazzy/setup.bash

Then source the local workspace:

source install/setup.bash

Problem 2 - `externally-managed-environment` when using pip

Ubuntu 24 protects the system Python environment.

Use a virtual environment:

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

Do not install lab packages globally into the system Python.

Problem 3 - WSL cannot connect to Ollama running on Windows

If Ollama runs on Windows and the pipeline runs in WSL, Ollama must be accessible from WSL.

In Windows PowerShell:

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

If WSL still cannot connect, allow inbound TCP traffic on port 11434 in Windows Firewall.

In WSL:

WIN_HOST=$(ip route | awk '/default/ {print $3}')
curl http://$WIN_HOST:11434/api/tags

Then set:

export OLLAMA_URL="http://$WIN_HOST:11434/api/generate"

Problem 4 - The simulator does not move

First check that the ROS2 topics exist:

ros2 topic list

Then publish a manual velocity command:

ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"

If `pose x` changes in the simulator terminal, the simulator works.

Problem 5 - Microphone does not work in WSL

Some WSL installations do not expose the microphone correctly.

If microphone recording fails, record a short `.wav` file on Windows and process the audio file from WSL.

This still satisfies the voice pipeline requirement because the command starts as spoken audio.

Deliverables

Submit:

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.