Part 6 of 7 · 3 min read
Exercises
Try each exercise before opening its hint.
Easy
Easy 1: Write the request (15 minutes)
Write by hand the messages list for a three-turn conversation with a system prompt "answer in one sentence". Then add the fourth user message. Count the messages sent on each of the four calls.
Done when: your counts are 2, 4, 6 and 8.
Hint
the system prompt counts as a message in this style.
Easy 2: Prove statelessness (15 minutes)
Using FakeLLM, make two separate calls: introduce yourself in the first and ask your name in the second. Then do it with a Conversation.
Done when: the first approach fails and the second succeeds, and you can explain why.
Easy 3: Read the stop reason (15 minutes)
Call create with max_tokens=2 and print the stop reason. Explain in two sentences what could go wrong if your app ignored it when asking for JSON.
Done when: you show the max_tokens result and name the failure (truncated, unparseable JSON).
Medium
Medium 1: Ordering rules (45 minutes)
Extend Conversation with a validate(messages) function that checks a list from outside: only known roles, a system message only first, user and assistant alternating, last message from the user. Write 8 tests, each with one specific violation.
Done when: eight tests pass and each error message names the position of the problem.
Hint
return a list of problems rather than raising on the first.
Medium 2: Pinned messages (60 minutes)
Add a pin option: messages marked as pinned (for example the user's stated preferences) are never trimmed. Test that pinned messages survive when the budget is tight and that you raise a clear error when pinned messages alone exceed the budget.
Done when: both behaviours are tested.
Hint
when trimming, skip pinned messages, but keep pairs consistent.
Medium 3: Summarise instead of drop (60 minutes)
Write compact(conversation, llm, keep_last=2) that replaces everything except the last two exchanges with a single summary message produced by a function you pass in. Check that the size falls, the newest two exchanges are intact, and the roles still alternate.
Done when: the three checks are tests, using a fake summariser.
Hint
put the summary in a user message such as "Summary so far: ..." followed by a short assistant acknowledgement, so roles alternate.
Hard
Hard 1: Provider adapter (90 minutes)
Write functions to_openai_style(conv) and to_system_field_style(conv) that produce two request shapes: system as the first message, and system as a separate field with only user and assistant messages in the list. Write a round-trip test that converts each back into a Conversation and compares.
Done when: both directions are tested and no information is lost.
Hard 2: A guard that is hard to evade (120 minutes)
Extend guard_output to catch a secret in simple disguises: different case, spaces between letters (M A N G O), and hyphenated (M-A-N-G-O). Write tests for each disguise and for a harmless reply that must pass. Then write down one disguise your guard still misses and why that matters.
Done when: disguise tests pass, there is no false block on harmless text, and the limitation is documented.
Hint
normalise by removing everything that is not a letter or digit before comparing.
Hard 3: Budget planner (120 minutes)
Write a function that, given the context limit, a maximum output size, the system prompt and the history, decides what to trim so that input plus output fits, and returns both the trimmed request and a report (dropped messages, tokens before and after). Property-test it: for 200 random histories, the result always fits, always keeps the system prompt, and always ends with the newest user message.
Done when: the property test passes for all 200 cases.
Hint
use random.Random(seed) in the test so failures are reproducible. The fixed trim already refuses an impossible budget; decide what your planner does in that case.