LLM + RoboticsBuild a Robot Brain · Week 7 · Lesson 11

Capstone: Robo-Helper

Ten lessons of pieces: moving, sensing, braking, a stop-neuron, honest testing, route planning, a word model, a command reader and a safety layer. Today they click together into one robot that understands you, drives around a surprise, and tells you honestly how good it is.

Time3–4 hours (plus the stretch)
You'll needPython 3 and your finished Lessons 1–6, 8, 9 and 10
New wordssystem, integration, pipeline, test set, failure analysis, confidence
Project pieceThe full robot, tested

Start here

A robot is not one clever idea. It's lots of small ideas passing messages to each other, and the hard part is making the hand-offs work, then measuring the whole thing honestly.

The problem

You have all the parts, but they're scattered across ten files. Your A* planner doesn't know what "put the blue block in the bin" means. Your command reader doesn't know where the bin is. Your stop-neuron has never seen the room. And nobody has checked what happens when a cat sits down in the middle of the planned route. Today you write the conductor that makes the orchestra play together.

The idea in one picture

Think of a restaurant. The waiter takes your order and writes it on a ticket (understanding). The head chef checks the ticket: is that dish on the menu? (safety). The cooks plan who does what (route planning) and cook carefully, watching for spills (sensing and braking). Then a food critic eats ten dishes and writes "9 out of 10 were good, the soup was cold" (evaluation). No single person is the restaurant. The restaurant is how they work together.

Words to know

System
Several parts working together to do a job none of them can do alone.
Integration
Joining parts so the output of one becomes the input of the next.
Pipeline
A chain of steps: text → action → route → driving → result.
Test set
A fixed list of tasks, with the right answers, that you run the whole robot on to score it.
Failure analysis
Looking closely at each failed test to find out why it failed.
Confidence
How sure a model is. A model that always gives an answer, even when unsure, has no way to say "I don't know".

The full lesson: everything below shows how the pieces connect, then you write the last two functions and test the whole robot.

1. The problem

Here's the job. Someone types "put the blue block in the bin". A friendly cat (a "surprise") is sitting at square (4, 1), and it isn't on the map. The robot starts at (1, 1). It has to:

  1. understand the words,
  2. check that the request is allowed and possible,
  3. plan a route to the blue block at (6, 3),
  4. drive it at a safe speed, notice the cat, stop, and plan a new route,
  5. pick up the block, plan a route to the bin at (7, 6), drive there and drop it,
  6. say what it did.

And then you need to know: does it work on other commands too? One good demo proves nothing. A test set proves a lot more.

2. First principles

Step 1: draw the pipeline

The top of your homework file has the whole plan in five lines. Each arrow is a hand-off between lessons:

command text --(L9 parser / L10 safety checks)--> action
action       --(L6 A* planner)-->                  route
route        --(L1 moving, L2 sensing, L4/L5 stop detector, L3 safe speed)--> driving
result       --(L5 evaluation habits)-->           success rate on a test set

The trick to building a system is agreeing on the shape of each message. Lesson 9 outputs {"intent": ..., "object": ...}. Lesson 10 checks exactly that shape. Lesson 6 takes two (x, y) squares and returns a list of squares. Because every piece agreed on its inputs and outputs, you can snap them together.

Step 2: what each earlier lesson does inside Robo-Helper

Open lesson11_robohelper.py and look at the imports and at __init__. Most of it is written for you. Here's every earlier piece, and exactly where it's used:

LessonWhat's usedWhere, and what for
world.pyROOM, OBJECTS, ROBOT_START, find_bin, renderThe map, the three blocks, the start (1, 1), the bin's square (7, 6), and drawing the room.
L1 Movingdirections as (dx, dy), move, manhattandrive_to turns each step of the route back into "N/S/E/W" using L1's direction changes. move runs inside L2's sensor and L6's neighbors. manhattan is A*'s guess of the distance left.
L2 Sensorsdistance_to_wallBefore each step: how many free squares are ahead? Times SQUARE (0.5 m), plus one square, gives the free distance in metres.
L3 LearningSTOP_DATA, fit_line, max_safe_speedIn __init__, fit_line learns the braking line (w ≈ 0.81, b ≈ 0.07). In drive_to, max_safe_speed picks the fastest speed that can still stop in the free distance.
L4 Neuronmake_obstacle_data, train_neuron, should_stop__init__ makes 450 examples and trains the stop-neuron. In drive_to, when a surprise is one square ahead, should_stop decides whether to brake.
L5 Evaluationsafest_thresholdPicks the stop-neuron's threshold so it catches at least 97% of real obstacles. The trained robot gets 0.17, well below 0.5, because a missed stop is worse than a false alarm. (You'll use L5's habits again in evaluate().)
L6 Pathsastar_pathdrive_to plans with A*, passing blocked=self.known_blocked so the second plan avoids the cat.
L7 Q-learningnothingA side quest. Robo-Helper has a map, so A* is the better tool here. Q-learning shines when there's no map.
L8 WordstokenizeUsed inside L9 to split commands into words.
L9 CommandsTRAIN, train_intent_model, parse_command__init__ trains the word-counter. self.parse(text) turns words into an action.
L10 Safetyvalidate_actionYour job, in run_command: every action is checked before the robot moves.

So if Lesson 11 breaks, the bug might be in an earlier file. That's normal in real systems. check.py 11 tells you if an earlier lesson isn't finished yet.

Step 3: follow the cat by hand

Let's trace the drive to the blue block. A* (L6) plans the shortest route from (1, 1) to (6, 3), 7 steps:

(1,1) → (2,1) → (3,1) → (4,1) → (5,1) → (5,2) → (5,3) → (6,3)

The robot steps to (2, 1), then to (3, 1). The next square is (4, 1), and the cat is there. drive_to works out how fast it's going:

direction                  = E   (from (3,1) to (4,1): dx = +1)
distance_to_wall((3,1),E)  = 5 free squares          (L2)
free distance              = 5 × 0.5 + 0.5 = 3.0 m
max_safe_speed: needs  0.81 × speed + 0.07 + 0.2 ≤ 3.0
                       speed ≤ 3.37, but the top speed tried is 3.00
                → speed = 3.0 m/s                     (L3)

Now the stop-neuron (L4) sees something 0.5 m ahead, closing at 3.0 m/s. The trained weights are about −5.17 for distance and 4.92 for speed, with bias 5.33:

z = −5.17 × 0.5 + 4.92 × 3.0 + 5.33 = −2.58 + 14.77 + 5.33 = 17.52
sigmoid(17.52) ≈ 1.00   ≥ 0.17 (L5's threshold)   → STOP

The robot logs "Something is in the way at (4, 1)! Stopping and finding another route.", adds (4, 1) to known_blocked, and asks A* again from where it is, (3, 1):

(3,1) → (2,1) → (2,2) → (2,3) → (2,4) → (3,4) → (4,4) → (4,3) → (5,3) → (6,3)

That's 9 more steps (it has to back up, because (3, 2) is a wall). Then from the block to the bin: (7,3) → (7,4) → (7,5) → (7,6), 4 steps. Total: 2 + 9 + 4 = 15 moves instead of 11. Slower, but nobody stepped on the cat.

Predict first: what if known_blocked was never updated after stopping?

A* would plan the exact same route through (4, 1) again. The robot would stop again, plan again, stop again... drive_to has max_replans=5 for this reason: after 5 tries it logs "Too many surprises, giving up." and returns False. Remembering what you've seen is what turns a stop into a replan.

Step 4: one demo is not a test

Lesson 5's big lesson was: test on many examples, including ones that should fail. TEST_COMMANDS has 12 commands, each with what a correct robot should do:

Each test runs on a fresh robot. Why? If test 1 bins the red block, test 6 ("where is the red block") would get a different answer. Each test must start from the same state, or your score depends on the order of the tests.

Step 5: the honest result

The finished robot scores 83%, 10 out of 12. The two failures:

FAILED: ('find the green block', 'The green block is in the bin.')
FAILED: ('make me a sandwich', 'Stopping.')

The first one is scary. Asked where the green block was, the robot threw it in the bin. Lesson 9's classifier scored "put" just above "find" (a gap of only 0.09), because "the green block" appears in lots of "put" examples. If the test had only checked "is the right block in the bin?", this would have passed. Checking that nothing else changed is what caught it.

The second one:

The robot was asked for a sandwich and decided it was a "stop" command. Why? Let's do Lesson 9's maths by hand. None of the four words "make me a sandwich" appear in the "stop" examples, and only "me" appears anywhere (twice, in "find"). For a word an intent has never seen, Lesson 9 gives it a probability of 1 ÷ (words in that intent + vocabulary size). The vocabulary has 52 words:

"stop" has seen 15 words in total:   unseen word → 1 / (15 + 52) = 1/67
"put"  has seen 61 words in total:   unseen word → 1 / (61 + 52) = 1/113

The "stop" examples are short, so "stop" has the fewest words, so it gives the biggest share to words it has never seen. Four strange words in a row, and "stop" wins. The real scores:

stop  −18.27     find  −18.77     go  −19.70     put  −20.13

The deeper problem: predict_intent always returns the highest score. It has no way to say "I don't know". That's the stretch goal of this capstone.

Predict first: "go to the moon" is also nonsense. Why does it pass (get refused)?

The classifier picks "go" (the word "go" helps a lot). But extract_object finds no colour, so the object is None, and Lesson 10's rule says "go" needs an object. Refused, for the right result, but by luck rather than by understanding. It's worth knowing why a test passes, not just that it does.

3. Play with it

Interactive: run Robo-Helper

The same pipeline as the Python, step for step: Lesson 9's word-counter (trained on the same 34 sentences), Lesson 10's checks, Lesson 6's A*, Lesson 2's sensor, Lesson 3's braking and Lesson 4's stop-neuron with the weights and 0.17 threshold that the Python trains. Click a floor square to add or remove a surprise (orange). The robot remembers what it's done until you reset.


Top intent
–
Margin to 2nd
–
Moves
–
Test set
–

Try: "make me a sandwich" (look at the margin), "find the green block" (look at the top intent!), then "where is the blue block" after binning it. Put a surprise on (5, 3) as well as (4, 1) and watch the robot work harder. Box the robot in completely and see what it says.

4. Homework

Open project/homework/lesson11_robohelper.py. __init__, parse and drive_to are written for you. Read drive_to line by line first: you should recognise every function it calls. Then write two functions.

4.1 run_command(self, text, parser=None)

Turn text into an action, check it, then do it. Return a dict {"ok": bool, "action": action_or_None, "message": str}. The docstring lists six steps. In plain words:

  1. Parse the text (with parser if one was given, otherwise self.parse).
  2. Check it with validate_action(action, set(self.objects)). If it fails, return ok False and the message "Sorry, I can't do that: <reason>".
  3. "stop": nothing to drive. Message "Stopping."
  4. "find": just answer. It's in the bin, or you're holding it, or say its square.
  5. "go": self.drive_to(...) the object's square.
  6. "put": drive to the block, hold it, drive to the bin, drop it, update everything.

Example: on a fresh robot, run_command("where is the blue block") → {"ok": True, "action": {"intent": "find", "object": "blue block"}, "message": "The blue block is at (6, 3)."}

Hint 1

(parser or self.parse)(text) picks the parser in one line: or returns the first thing that isn't None. After validating, pull out intent and obj, then handle each intent with an if that returns. Flat is easier than nested.

Hint 2

Order "find"'s checks as the docstring says: binned first, then holding, then position. f-strings print a tuple like (6, 3) exactly as the test expects: f"The {obj} is at {self.objects[obj]}.". For "go", drive_to returns True or False: use it as ok.

Hint 3
put:
  already in self.binned?            → ok True, "The X is already in the bin."
  not drive_to(objects[X])?          → ok False, "I couldn't reach the X."
  holding = X
  bin_pos = find_bin(self.room)
  not drive_to(bin_pos)?             → ok False, "I'm holding the X but can't reach the bin."
  binned.add(X);  objects[X] = bin_pos;  holding = None
  → ok True, "The X is in the bin."

4.2 evaluate(commands=TEST_COMMANDS, surprises=((4, 1),), seed=0)

Run each test command on a fresh RoboHelper(surprises=surprises, seed=seed). Decide pass or fail. Return (pass_rate, failures), where failures is a list of (command, message).

Example: if 10 of 12 pass, return (10/12, [("find the green block", "The green block is in the bin."), ("make me a sandwich", "Stopping.")]), and 10/12 prints as 83%.

Hint 1

Each entry in commands is a triple: for text, expect, binned in commands:.

Hint 2

For "ok" tests you need two things: result["ok"] is true, and robot.binned is exactly the set binned. Two sets are equal with ==. (Why not "every listed block is in the bin", binned <= robot.binned? Because then a robot that bins everything would pass.)

Hint 3
for each (text, expect, binned):
    robot = fresh RoboHelper;  result = robot.run_command(text)
    if expect == "ok":  good = result["ok"] and binned <= robot.binned
    else:               good = not result["ok"]
    count it, or append (text, result["message"]) to failures
return passed / len(commands), failures

Run it

cd project/homework
python3 lesson11_robohelper.py

What you should see (real output from the solution):

##########
#Rr......#
#..##....#
#..#..b..#
#....##..#
#g.......#
#.#....B.#
##########
> where is the blue block
  The blue block is at (6, 3).
> put the blue block in the bin
  The blue block is in the bin.
> where is the blue block
  The blue block is in the bin.
  log: ['Something is in the way at (4, 1)! Stopping and finding another route.']
##########
#.r......#
#..##....#
#..#.....#
#....##..#
#g.......#
#.#....R.#
##########

Test set: 83% passed
  FAILED: ('find the green block', 'The green block is in the bin.')
  FAILED: ('make me a sandwich', 'Stopping.')

The first map is the start: R at (1, 1). The last map shows the robot parked on the bin, and the blue block gone from (6, 3). The log line is the cat.

Check it

cd project
python3 check.py 11

Five checks: put works, find works, refuses safely, replans around surprises and evaluate. The last one needs at least 80%. A red ✗ shows a hint such as "with a surprise at (4, 1) the robot should log a stop and still finish" or "test pass rate should be at least 80% (got 75%)". If you see a yellow … saying "can't load yet, an earlier lesson isn't finished", run python3 check.py all to find which one.

Stretch: teach the robot to say "I don't know"

This is the real capstone. Professionals spend most of their time here: finding why a system fails, and fixing it without breaking something else.

  1. Diagnose. Print intent_scores (from Lesson 9) for all 12 test commands, sorted, and the gap between the top two. You already know the sandwich's gap is 0.50. Which passing commands also have a tiny gap?
  2. Fix it, idea A: a confidence margin. If the top score beats the second by less than some number, don't guess: return an action that Lesson 10's validate_action will refuse.
  3. Fix it, idea B: an "unknown" intent. Add a few training examples of things the robot can't do ("tell me a joke", "play some music"...) labelled "unknown". "unknown" isn't in ALLOWED_INTENTS, so your safety layer refuses it for free.
  4. Re-measure. Run evaluate() again. Did the score go up? Did anything that used to pass now fail?
Hint 1: where to plug it in

You don't need to edit Lesson 9. run_command takes a parser argument, and __init__ builds self.intent_model. Either write a new parse function that uses intent_scores(self.intent_model, text), or train the model on TRAIN plus your extra examples. (Import intent_scores from lesson09_commands.)

Hint 2: choosing the margin

Sort the scores from high to low and subtract the second from the first. Look at the gaps of the commands you want to accept ("stop" is 2.25, "halt now" is 1.90, "put the red block in the bin" is 4.74) and the one you want to refuse (sandwich, 0.50). Pick a number in between. Don't pick it by staring at the test set alone, though, or you're cheating on the exam (Lesson 5!). Try it on a few new commands of your own too.

Hint 3: the surprise you'll find

With a margin of about 0.6, the sandwich is refused and the score climbs to 92%. "find the green block" still fails, but look how: its gap is only 0.09, so now the robot refuses instead of binning the block. A refusal is a much safer failure than doing the wrong thing. To make it pass, the classifier has to get "find" right. What would a few more "find" examples ("where's the X", "locate the X") do? Re-run evaluate() after each change.

Write down your before and after numbers, and keep them. They go straight into your project README in the coming lessons and Lesson 14.

5. Real-world challenge: be Robo-Helper

You'll run the whole pipeline with your own body, in a real room, with a family member as the user. You need squared paper, a pencil, a cushion, and three small objects (your "blocks").

  1. Map (L1). Draw a room in your home on squared paper, one square per floor tile or per big step. Mark walls and furniture as #, a box or basket as the bin B, and where the three objects sit.
  2. Write the rules (L10). Allowed: put / go / find / stop, and only your three objects. Anything else: refuse politely.
  3. Write a test set (L5). Ask your family member to write 10 commands on slips of paper before you start. Ask for some normal, some odd, and at least two impossible ones.
  4. Run each command. Plan the route on paper first (L6: count the steps). Then walk it, slowly. Meanwhile your helper may quietly drop the cushion on your route (the cat). If it's one step ahead: stop, mark it on the map as blocked, re-plan on paper, carry on.
  5. Score yourself. For each command record: understood correctly? refused when it should be? how many steps planned, how many walked? Work out your pass rate.
  6. Failure analysis. For every failure, write why in one line. Did you ever guess what an odd command meant instead of refusing?

What it teaches: each part is easy alone. Doing them in order, under surprises, while keeping honest score, is the actual skill. And humans also find it hard to say "I don't know" instead of guessing.

6. Your Robo-Helper so far

This is it: the full robot, tested. RoboHelper understands, checks, plans, senses, brakes, replans, and reports a score with a list of failures. Every earlier lesson (except the Q-learning side quest) is inside it.

Next comes inference engineering: week 8 measures how fast and expensive each brain is, week 9 makes brains faster and cheaper, week 10 plugs in a world-class model, and week 11 races every brain on the same tasks with sim.py, which runs this robot. In week 12, Lesson 12 learns from people's preferences, and Lesson 14 takes run_command, your evaluate() score and your failure list, and turn them into a project README, a demo recording, and (if you like) a real wheeled robot.

7. Common mistakes

8. Check yourself

Why does every test in evaluate() use a fresh robot?

So each test starts from the same state. Otherwise the result of one test depends on what the earlier tests did, and changing the order would change the score.

Predict: on one robot, you run "put the red block in the bin" twice. What does the second call say?

"The red block is already in the bin." with ok True. The robot doesn't move.

Why is the stop threshold 0.17 and not 0.5?

Lesson 5's safest_threshold picks the highest threshold that still catches at least 97% of real obstacles. Missing a real obstacle is much worse than braking for nothing, so it accepts more false alarms.

Why does "make me a sandwich" become "stop"?

The classifier always picks the top score, and "stop" has the fewest training words (15), so it gives the biggest probability to words it has never seen (1/67 versus 1/113 for "put"). Four unknown words make "stop" win. There's no "I don't know" option.

The robot scored 83%. Is it 83% good at real life?

No. It's 83% on 12 hand-picked commands, in one room, with one surprise. Real people type far stranger things. And as the stretch shows, a test can pass while the robot does the wrong thing. A score is only as honest as the test set behind it.

9. Go deeper (optional)