// lesson: messages-format

The Messages Format

By the end of this course you will have a chatbot running in your terminal โ€” your API key, your machine, a real model on the other end. Each lesson adds one piece to that app, and each challenge unit-tests the one function you just wrote. The full app runs locally; the graded challenges never touch the network.

Every major LLM API speaks the same basic shape: a list of messages, where each message is a dict with a role and a content string.

[
    {"role": "system", "content": "You are a concise assistant."},
    {"role": "user", "content": "What is a context window?"},
    {"role": "assistant", "content": "The maximum amount of text a model..."},
]

The three roles matter:

  • system โ€” instructions from you, the developer: persona, tone, rules. The user never types this; your app supplies it.
  • user โ€” what the human typed.
  • assistant โ€” what the model said back. On the next call you send it right back in, which is how the model "remembers" the conversation. The API itself is stateless โ€” the message list is the memory.

Let's make one real call. Create a project folder, install the SDK, and set your key (this course shows the Anthropic SDK; the OpenAI equivalent appears in the next lesson and everything in the course works with either):

mkdir chatbot && cd chatbot
python -m venv .venv && source .venv/bin/activate
pip install anthropic
export ANTHROPIC_API_KEY=sk-ant-...

Now the first cut of chatbot.py:

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system="You are a concise assistant.",
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(response.content[0].text)

Run it: python chatbot.py. That's a real model answering you.

max_tokens is required, and it's easy to confuse with the context window you'll meet in the memory lesson โ€” they cap different things. max_tokens limits how long this one reply can be; the model stops generating, possibly mid-sentence, once it hits that count. The context window is the model's hard limit on total input size across the whole conversation. Set max_tokens too low and long answers get truncated; that's independent of how much history you've accumulated.

One wrinkle worth noticing: Anthropic's API takes the system prompt as a separate system= parameter, while OpenAI's API takes it as a {"role": "system", ...} message at the front of the list. We'll keep the system message in our history list (the portable, OpenAI-style shape) and let a small adapter function split it out for whichever API we call. That adapter arrives in the next lesson.

First, the piece we can test today: every conversation in our app starts from a helper that builds the initial history. Add this to chatbot.py:

def make_conversation(system_prompt):
    if system_prompt is None or not system_prompt.strip():
        return []
    return [{"role": "system", "content": system_prompt.strip()}]

Small, but it pins down real decisions: a blank or missing prompt means an empty history (no junk system message), stray whitespace gets stripped, and โ€” importantly โ€” every call returns a fresh list. If two conversations shared one list, mutating one chat's history would corrupt the other's. That kind of aliasing bug โ€” handing every caller the same mutable object instead of a fresh one โ€” is a Python classic; the tests below check for it.

โ€บ Start the Conversation

10 pts

Implement make_conversation(system_prompt):

  • If system_prompt is None, empty, or only whitespace, return [].
  • Otherwise return a new list containing exactly one message: {"role": "system", "content": <system_prompt with surrounding whitespace stripped>}.
  • Every call must return a fresh list โ€” mutating one returned list must not affect another.

Log in to submit a solution and earn points.