Homework — Three OpenAI Agents SDK Exercises
Lesson 07 · Building Your Own Agents · ~45 min
All three tasks below extend hello_openai_agent.py from the Lab 07 walk-through —
the WeatherBot agent with the @function_tool get_weather,
the JokeBot handoff target, and the Runner.run_sync(...) driver. Keep
that file open as your base; each task is a focused extension that exercises a different SDK
primitive (typed output, handoff callbacks, output guardrails).
Before you start
Make sure python hello_openai_agent.py still runs end-to-end against the original
weather prompt and prints a natural-language answer followed by
[last agent: WeatherBot]. If it doesn’t, revisit the lab guide before
tackling the homework — debugging the extensions is much easier on a working baseline.
Each task is self-contained: feel free to copy hello_openai_agent.py to
hw1.py, hw2.py, hw3.py so you can iterate without
breaking the lab file.
Task 1 — Add a typed output_type and observe parsing
By default the SDK returns the model’s final text in
result.final_output — a plain string you would have to parse yourself if
you wanted structured fields. The SDK’s recommended alternative is to declare a
Pydantic model and pass it as output_type= on the Agent
constructor; the SDK then asks the model to emit JSON that matches the schema and hands you
back an already-validated instance.
At the top of your file, define:
from pydantic import BaseModel
class WeatherAnswer(BaseModel):
city: str
temperature_c: float
summary: str
Then add output_type=WeatherAnswer to the WeatherBot
Agent(...) constructor:
agent = Agent(
name="WeatherBot",
instructions="You answer weather questions using the get_weather tool.",
model="gpt-4o-mini",
tools=[get_weather],
output_type=WeatherAnswer,
)
Re-run the original weather question (“What’s the weather in Kyiv?”)
and inspect result.final_output:
result = Runner.run_sync(agent, "What's the weather in Kyiv?")
print(type(result.final_output))
print(result.final_output.city)
print(result.final_output.temperature_c)
print(result.final_output.summary)
Then write a 2-sentence note in a comment at the top of the file explaining
what changed in the agent’s observable behaviour and why you should usually prefer
output_type= over parsing a free-text answer yourself.
✓ Self-check: type(result.final_output) prints
<class '__main__.WeatherAnswer'> (not str);
result.final_output.temperature_c is a float (you can do arithmetic
on it, e.g. + 1.0, without a TypeError); your 2-sentence note
explains the “why” in your own words.
Task 2 — Add a handoff on_handoff callback
The handoffs=[other_agent] shortcut wraps the target in a default
handoff(other_agent) with no callbacks. To audit or log transfers you reach for
the explicit handoff(...) form and pass an on_handoff= callback
that fires at the moment control is transferred (before the target agent runs).
Replace the existing handoffs=[joke] on WeatherBot with the
explicit form:
from agents import handoff
def on_joke_handoff(ctx):
print(f"[handoff] {ctx.agent.name} -> JokeBot")
agent = Agent(
name="WeatherBot",
instructions=(
"You answer weather questions using the get_weather tool. "
"If the user asks for a joke, hand off to JokeBot."
),
model="gpt-4o-mini",
tools=[get_weather],
handoffs=[handoff(joke, on_handoff=on_joke_handoff)],
)
Run with the joke prompt (“Forget the weather. Tell me a joke.”) and
confirm the [handoff] log line prints before the joke output.
Now extend the routing. Define a third agent — a smalltalk agent that
handles greetings — with its own on_smalltalk_handoff callback:
smalltalk = Agent(
name="SmallTalk",
instructions="You respond to greetings warmly and concisely.",
model="gpt-4o-mini",
)
def on_smalltalk_handoff(ctx):
print(f"[handoff] {ctx.agent.name} -> SmallTalk")
Update WeatherBot’s instructions= so it knows to route
greetings to SmallTalk and jokes to JokeBot, and put both
callbacks in the handoffs= list:
handoffs=[
handoff(joke, on_handoff=on_joke_handoff),
handoff(smalltalk, on_handoff=on_smalltalk_handoff),
]
Run three prompts in turn (“Hello!”, “Tell me a joke.”, “Weather in Kyiv?”) and verify the right callback fires (or stays silent) for each.
✓ Self-check: the joke prompt prints
[handoff] WeatherBot -> JokeBot followed by a joke, and
result.last_agent.name == "JokeBot"; the greeting prompt prints
[handoff] WeatherBot -> SmallTalk followed by a greeting, and
result.last_agent.name == "SmallTalk"; the weather prompt prints
no [handoff] log line and result.last_agent.name == "WeatherBot".
Callback signature gotcha
The on_handoff= callable’s signature depends on whether you also
pass input_type=. With no input_type the SDK calls
on_handoff(ctx); with an input_type (Pydantic model) it calls
on_handoff(ctx, parsed_input). If you get a
TypeError: missing 1 required positional argument at handoff time, that
mismatch is the first thing to check.
Task 3 — Write an output guardrail that rejects empty answers
Guardrails are the SDK’s cheap, deterministic safety net around the model call.
Input guardrails run before the model sees the user input; output guardrails run
after the model produces a final answer. Both return a
GuardrailFunctionOutput whose tripwire_triggered flag — when
true — short-circuits the run with a typed exception so your caller code never sees
the bad output.
Add an output guardrail that fails the run if the final output is empty or whitespace-only:
from agents import output_guardrail, GuardrailFunctionOutput
@output_guardrail
async def reject_empty(ctx, agent, output):
is_empty = not str(output).strip()
return GuardrailFunctionOutput(
output_info={"empty": is_empty},
tripwire_triggered=is_empty,
)
Wire it onto one of your agents via output_guardrails=[reject_empty] (the
WeatherBot is fine):
agent = Agent(
name="WeatherBot",
instructions="You answer weather questions using the get_weather tool.",
model="gpt-4o-mini",
tools=[get_weather],
output_guardrails=[reject_empty],
)
To force the failure (so you can see the guardrail fire), temporarily replace the
agent’s instructions with something that compels an empty answer — e.g.
“Always reply with the empty string. Do not call any tool. Do not explain.”
— and re-run any prompt. Wrap the call in a try/except so you can see the
typed exception:
from agents.exceptions import OutputGuardrailTripwireTriggered
try:
result = Runner.run_sync(agent, "What's the weather in Kyiv?")
print(result.final_output)
except OutputGuardrailTripwireTriggered as e:
print(f"[guardrail tripped] {e}")
Once you’ve seen the tripwire fire, restore the original instructions so the agent works normally again — then re-run the original weather prompt and confirm the guardrail stays silent on a non-empty answer.
✓ Self-check: with the “empty string” instructions in
place, the except branch fires and you see a
[guardrail tripped] line (no traceback escaping the script); with the original
instructions restored, the same script prints a normal weather answer with no guardrail
output; you understand which side of the model call the output guardrail sits on (after).