Talk to the head.From your own code.
Get the head onto your PC.
New in step 1: the head lives on your own PC.
The head is the trained model you met at the end of lesson 0. It runs on your own computer, for free.
Two quick questions from last time. Guessing first helps it stick, even when it feels harder.
Pause here and do these, then press Done. You need both programs: Python runs your agent.py, and Ollama runs the head.
Install Python 3
Get it from python.org. Python is the language your agent is written in, and it runs agent.py.
Install Ollama
Get it from ollama.com. It's the program that keeps models and runs them, so it runs the head.
Check it:
ollama --versionollama version is 0.40.14
Your number will differ.
Open a terminal
A terminal is a window where you type commands instead of clicking. On Windows, press Start, type PowerShell and press Enter. On a Mac, open the Terminal app.
Check that Python is installed
Type this in the terminal and press Enter:
python --versionpython --version- Asks Python to print its version. A number like
Python 3.12.1means it is installed. On some Macs the command ispython3.
Python 3.12.2
Your number may differ. On our PC py --version prints the same line.
If red text says 'python' is not recognized
: type py instead of python from here on, or reinstall Python and tick Add python.exe to PATH
. (We saw that wording when we typed a made-up command on our PC.)
Download the head
In the terminal, run this (about 5 GB, once):
ollama pull qwen3:8bollama pull- Asks Ollama to download a model and keep it. The jar above is what you end up with.
qwen3:8b- The model's name.
qwen3is the model family, and8bmeans about 8 billion parameters: the tuned numbers from lesson 0, like volume knobs the training turned.
The first time, each line below fills up like a progress bar, and the last line is success:
pulling manifest pulling a3de86cd1c13: 100% ▕████████████████▏ 5.2 GB pulling ae370d884f10: 100% ▕████████████████▏ 1.7 KB pulling d18a5cc71b84: 100% ▕████████████████▏ 11 KB pulling cff3f395ef37: 100% ▕████████████████▏ 120 B pulling 05a61d37b084: 100% ▕████████████████▏ 487 B verifying sha256 digest writing manifest success
Then check it is there:
ollama listNAME ID SIZE MODIFIED qwen3:8b 500a1f067a9f 5.2 GB 2 days ago
If a command can't reach Ollama, or localhost:11434 won't load: Ollama isn't running. Start it from the Start menu, or run ollama serve in another terminal and leave that window open.
Eight billion numbers on your hard drive. My mother's photo library is bigger, and she still can't find the one photo.Prof. Graves
Start with an empty file.
New in step 2: an empty agent.py, and the command that runs it.
Make a file called agent.py in any text editor, leave it empty, and save it in a folder. In the terminal, move into that folder with cd and the folder's path, then run it:
python agent.py- text editor
- A program for writing plain text. Notepad on Windows works. On a Mac, TextEdit works if you choose Format, then Make Plain Text.
cd- Short for
change directory
: it moves the terminal into a folder. Typecd, a space, then the folder's path. For examplecd Desktop\agentfor a folder called agent on your Desktop (on a Mac,cd Desktop/agent). The commandpython agent.pyonly finds the file from inside its folder. agent.py- Your program.
.pyis the ending for Python files. Save it in a folder you can reach withcd. python agent.pypythonstarts Python, and the name after it is the file to run. Python reads the file from the top, one line at a time. An empty file has no lines, so it finishes at once and prints nothing.
Nothing happens, which is correct. Over the next lessons this file becomes your agent, a few lines at a time.
The most honest program you'll ever write. It promises nothing and delivers exactly that.Prof. Graves
Write the note.
New in step 3: the note, a dictionary that holds your prompt.
Put this in agent.py. It is the whole file for now:
body = {
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Why is the sky blue? One sentence."}],
"stream": False,
"think": False,
}
The note, line by line
body = { ... }- A Python dictionary (like a JavaScript object): curly braces holding
"name": valuepairs, separated by commas. The names are called keys.=stores the whole thing under the namebody, the body of the message you will send. "model": "qwen3:8b"- Which head answers: the name you pulled in Get the head onto your PC. The
8bmeans about 8 billion tuned numbers. Pull a different model later and put its name here. "messages": [ ... ]- The conversation so far, as a list: square brackets with items separated by commas. Right now it holds one message.
{"role": "user", "content": "..."}- Each message is its own small dictionary.
rolesays who is talking, anduseris you.contentis the text itself, your prompt. Lesson 2 adds a message with the rolesystem, and lesson 3 adds the head's own replies with the roleassistant. "stream": False- Asks for the whole answer in one piece. With
True, Ollama sends it in small pieces while the head is still writing. The fold-out below shows both. "think": False- qwen3 can write private notes to itself before it answers.
Falseswitches them off. The fold-out below shows a real set of notes. False- Python's word for no, with a capital F (and
Truefor yes). When the dictionary is turned into text to travel, it becomes lowercasefalse, because that is how JSON spells it. JSON is a plain-text format for sending data between programs, the one the note travels in. ,after the last pair- Python allows a comma after the last item. It is only there so you can add another line later without touching this one.
Try it: your question inside the note
Type your own question in the box. The part after the next, Build the trip, explains json.dumps: it turns the dictionary into one line of text. This is exactly that line, with your question highlighted inside it:
Nothing happens yet. A note in your pocket answers nothing, as my mother's postman can confirm.Prof. Graves
Two switches in the note: stream and think. See the difference
"stream": True or False
Same question, two settings. These are the real pieces of one recorded answer (a real run on our PC, qwen3:8b). Each colour band is one piece, in the order it arrived. The pace is just for show.
"stream": True"stream": False"think": True adds notes
With "think": True the reply gets an extra field, "thinking". These are the first two sentences of the notes from a real run on our PC. They run 724 characters in all, for an answer of one sentence:
Okay, the user is asking why the sky is blue in one sentence. Let me recall the scientific explanation.
The head reads and writes text in tokens. A token is a piece of text, often part of a word.
Slide it under the door.
New in step 4: the note goes to Ollama, which hands it to the head and sends back its reply.
Ollama is the program at the door: it keeps the head and runs it. It listens at http://localhost:11434: localhost means this very computer, and 11434 is the number of Ollama's door. Your code slides the note under that door, and Ollama hands it to the head. Here you watch one trip. The next part, Build the trip, writes the code that makes it.
These buttons replay answers we recorded on our PC (qwen3:8b). Your own agent.py sends the question written in its body, and from Keep asking on, whatever you type. They were recorded with "stream": True, so the answer comes in pieces.
Build the trip.
New in step 5: the lines that pack the note, send it to Ollama and read the reply.
You just watched the trip. Now we write the lines that make it, in three small changes, each shown as your whole agent.py. New lines are highlighted.
import json
body = {
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Why is the sky blue? One sentence."}],
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
print(note)
import json- Loads Python's JSON tools. JSON is a plain-text format for sending data between programs, and the note travels in it. The
jsontools turn dictionaries into it and back. URL = "http://localhost:11434/api/chat"- The address.
localhostis your own computer,11434is the door number Ollama listens on, and/api/chatis its chat counter. json.dumps(body)- Turns the dictionary into one line of text, the line you saw in the live note.
Falsebecomesfalseon the way. .encode()- Turns that text into bytes, the raw numbers a connection carries (one byte per plain letter). The result is stored as
note. From here on:bodyis what the note says, andnoteis the same note packed into bytes for the trip. print(note)- Shows the note. The
bbefore the quote means bytes. This is exactly what will travel.
Run python agent.py. Ollama is not needed yet. It prints the note:
b'{"model": "qwen3:8b", "messages": [{"role": "user", "content": "Why is the sky blue? One sentence."}], "stream": false, "think": false}'
import json
import urllib.request
body = {
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Why is the sky blue? One sentence."}],
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
with urllib.request.urlopen(request) as response:
answer = json.load(response)
print(answer)
import urllib.requesturllibis Python's own way of talking to web addresses, so there is nothing extra to install.urllib.requestsends requests.urllib.request.Request(URL, note, {...})- Packs the parcel: the address, the contents, and a header. We store it as
request. Two different things share that word:urllib.requestis Python's web toolbox, andrequestis the name we chose for our parcel. The header"Content-Type": "application/json"is a label on the parcel saying the contents are JSON. Nothing is sent yet. Because it has contents, it is what web people call a POST: a request that carries something, not one that only asks for a page. with urllib.request.urlopen(request) as response:- Sends the request and waits for the reply.
with ... asnames the replyresponseand closes the connection when the indented lines end. Python groups lines by their indent: four spaces further right meansinside
. answer = json.load(response)- Reads the reply's text and turns the JSON back into a Python dictionary, stored as
answer. print(answer)- Replaces
print(note): now it shows the whole reply dictionary instead of the note.
import json
import urllib.error
import urllib.request
body = {
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Why is the sky blue? One sentence."}],
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(request) as response:
answer = json.load(response)
except urllib.error.URLError as error:
raise SystemExit(f"Could not reach Ollama ({error}). "
"If it is not running, open another terminal and run: ollama serve")
print(answer)import urllib.errorurllib.errorholds the errors that can happen while sending.try:andexcept- Python runs the lines indented under
try(indented means four spaces further right: they belong to it). If one of them fails, it jumps to theexceptblock instead of crashing. The two lines that send the request moved four spaces right: they are inside thetrynow. except urllib.error.URLError as error:- Runs only if the sending failed, for example when Ollama is not running.
as errornames the problem. raise SystemExit(f"...{error}...")- Stops the program and prints the message. The
fbefore the quote makes it an f-string: whatever is inside{ }is replaced by its value.
Run python agent.py with Ollama running. The first run can take a few seconds while the head wakes up.
{'model': 'qwen3:8b', 'created_at': '2026-11-10T17:26:19.5300478Z', 'message': {'role': 'assistant', 'content': "The sky appears blue because molecules in the Earth's atmosphere scatter shorter wavelengths of light, like blue, more effectively than longer wavelengths."}, 'done': True, 'done_reason': 'stop', 'total_duration': 3803095500, ...}
That's the answer, buried in packaging: which model, when, how long it took. It's another real run on our PC, shortened. Each run words the answer a little differently.
You asked for one sentence and it sent the paperwork too. My mother ran an office like that for thirty years.Prof. Graves
Keep only the words.
New in step 6: take only the head's words out of the reply.
The words live under "message", then "content". Watch where they sit inside answer.
answer = {
'model': 'qwen3:8b',
'created_at': '2026-11-10T17:26:19.5300478Z',
'message': {
'role': 'assistant',
'content': "The sky appears blue because molecules in the Earth's atmosphere scatter shorter wavelengths of light, like blue, more effectively than longer wavelengths.",
},
'done': True,
'done_reason': 'stop',
'total_duration': 3803095500,
...
}
Change the last line, print(answer). Your whole agent.py: New lines are highlighted.
import json
import urllib.error
import urllib.request
body = {
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Why is the sky blue? One sentence."}],
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(request) as response:
answer = json.load(response)
except urllib.error.URLError as error:
raise SystemExit(f"Could not reach Ollama ({error}). "
"If it is not running, open another terminal and run: ollama serve")
print(answer["message"]["content"])
answer- The dictionary you got back from
json.loadin Build the trip. answer["message"]- Square brackets with a key inside look that key up and give back what is stored under it. Here that is another dictionary, the one in yellow above.
answer["message"]["content"]- Chain a second lookup onto the first. It reads left to right: first the inner dictionary, then the
"content"inside it. That is the sentence, in green above. print(...)- Shows that one value instead of the whole dictionary. Python prints strings with single quotes inside a dictionary and without quotes on their own.
The sky appears blue because molecules in the Earth's atmosphere scatter shorter wavelengths of light, like blue, more effectively than longer wavelengths.
Yours will be worded differently. The head guesses, and guesses vary from run to run, exactly like the tiny one you built in lesson 0.
Keep asking.
New in step 7: a loop that asks you again and again until you type quit.
One question per run makes your program a vending machine. Put everything below the imports inside a loop that asks you each time and stops when you type quit. Here is the whole of agent.py at the end of lesson 1: Everything that moved into the loop is four spaces further right. New lines are highlighted.
import json
import urllib.error
import urllib.request
while True:
prompt = input("You: ")
if prompt == "quit":
break
body = {
"model": "qwen3:8b",
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"think": False,
}
URL = "http://localhost:11434/api/chat"
note = json.dumps(body).encode()
request = urllib.request.Request(URL, note, {"Content-Type": "application/json"})
try:
with urllib.request.urlopen(request) as response:
answer = json.load(response)
except urllib.error.URLError as error:
raise SystemExit(f"Could not reach Ollama ({error}). "
"If it is not running, open another terminal and run: ollama serve")
print(answer["message"]["content"])It forgets because every pass of the loop builds a brand-new messages list holding only your latest line. Lesson 3 fixes that.
while True:- Repeats the indented lines under it for as long as the condition is true.
Trueis always true, so the loop never ends by itself. The colon opens the block. - the indentation
- Python has no curly braces for blocks. The lines indented by four spaces are the body of the loop. That is why every line from Build the trip moved to the right when it went inside.
prompt = input("You: ")inputprints the text in brackets, waits until you type a line and press Enter, and hands back what you typed. It is stored inprompt.if prompt == "quit":==asks whether two values are equal. The indented line under theifonly runs when they are.break- Jumps out of the loop at once. Nothing is left after the loop, so the program ends.
"content": prompt- The one change inside the note: the fixed sky question became the variable
prompt, so every trip round the loop sends whatever you just typed. - the rest
- The same lines as in Build the trip and Keep only the words, now inside the loop, so they run once per question.
You: Say hello in three words. Hello, how are you? You: quit
Asked for three words, it gave four. Lesson 9 is about catching it making things up.Prof. Graves
That's lesson 1.
Your file asks the head a question and prints the answer, as many times as you like. That's the first piece of your agent.
Try telling it your name, then asking for it. It won't know. Every message starts from zero, and fixing that is lesson 3.
- the head
- The trained model, qwen3:8b: a big file of tuned numbers that runs on your PC.
- Ollama
- The program on your PC that keeps models and runs them.
- terminal
- A window where you type commands instead of clicking.
- prompt
- The text you send to the head.
- dictionary
- A Python container of named values: curly braces holding
"name": valuepairs. - list
- Square brackets holding items in order. The messages go in one.
- JSON
- The text format the note travels in.
json.dumpsturns a dictionary into it,json.loadturns it back. - stream
True: the answer arrives in pieces while the head writes.False: all at once.- think
True: qwen3 writes private notes before it answers, and they come back in their own field.- token
- A piece of text, often part of a word. The head reads and writes text in tokens. (See the fold-out in Write the note.)
- localhost
- The address of your own computer. Ollama's door on it is number 11434.
- loop (
while True) - Repeats the lines under it until a
break. Typing quit triggers the break. input()- Prints a question, waits for you to type a line, and hands back what you typed.