chatbot1-5 Exercise 2: What the User Sees and What Is Stored ============================================================== ex2.py (save in the project root): import asyncio from bot.core import Bot from bot.guardrails import GuardConfig from bot.persona import Persona from bot.providers import FakeProvider PERSONA = Persona("Rowan", "a library assistant.", "brief", "library questions", "I can only help with library questions.") async def main(): cfg = GuardConfig(max_input_chars=2000, blocked_phrases=("password",)) leak = f"Sure, here you go: {PERSONA.canary}" cases = [ ("normal reply", "We open at nine."), ("leaks the marker", leak), ("mentions a password", "The admin Password is hunter2"), ("empty reply", ""), ] for label, scripted in cases: fake = FakeProvider([scripted]) bot = Bot(fake, persona=PERSONA, guard=cfg) answer = await bot.handle("s", "tell me something") stored = [m["content"] for _, m in bot.store.messages("s")][-1] print(f"{label:<20} user sees: {answer!r:42} stored: {stored == answer} flags: {[f for _, f in bot.events]}") asyncio.run(main()) Run with: py ex2.py Output: normal reply user sees: 'We open at nine.' stored: True flags: [] leaks the marker user sees: "Sorry, I can't share that." stored: True flags: ['canary_leak'] mentions a password user sees: "Sorry, I can't help with that." stored: True flags: ['blocked_phrase'] empty reply user sees: "Sorry, I don't have an answer for that." stored: True flags: ['empty_output'] Why the stored text must be the checked text: the stored history is sent back to the model on later turns. If the leaked marker (or a blocked phrase) were stored, the next request would contain it again, so the guard would have cleaned the user's screen while the model's own context still held the leak, and the history exported with /export would contain it too. Storing the replacement means a bad reply is contained to the one turn. WHY THIS WORKS AS AN ANSWER --------------------------- Each case uses a fresh bot with a fake provider scripted to return one specific reply. The normal reply passes through unchanged and produces no flags. The other three are replaced by the fixed apology texts and record a flag each (canary_leak, blocked_phrase, empty_output). The column "stored: True" shows that what was saved equals what the user saw. The blocked word matched although the reply used a capital P, because the comparison lowercases both sides. That also shows why substring blocklists are crude: they would match "password" inside harmless text, and they miss "p4ssw0rd".