Chapter 08
Week 2 Day 3
Week 2 Day 3
Now we get to more detail:
-
Different models
-
Structured Outputs
-
Guardrails
from dotenv import load_dotenv
from openai import AsyncOpenAI
from agents import Agent, Runner, trace, function_tool, OpenAIChatCompletionsModel, output_guardrail, GuardrailFunctionOutput
import os
from pydantic import BaseModel, Fieldload_dotenv(override=True)openai_api_key = os.getenv('OPENAI_API_KEY')
google_api_key = os.getenv('GOOGLE_API_KEY')
openrouter_api_key = os.getenv('OPENROUTER_API_KEY')
groq_api_key = os.getenv('GROQ_API_KEY')
if openai_api_key:
print(f"OpenAI API Key exists and begins {openai_api_key[:8]}")
else:
print("OpenAI API Key not set")
if google_api_key:
print(f"Google API Key exists and begins {google_api_key[:2]}")
else:
print("Google API Key not set (and this is optional)")
if openrouter_api_key:
print(f"OpenRouter API Key exists and begins {openrouter_api_key[:6]}")
else:
print("OpenRouter API Key not set (and this is optional)")
if groq_api_key:
print(f"Groq API Key exists and begins {groq_api_key[:4]}")
else:
print("Groq API Key not set (and this is optional)")instructions = """
You are a sales agent working for ComplAI,
a company that provides a SaaS tool for ensuring SOC2 compliance and preparing for audits, powered by AI.
You write compelling sales emails that are likely to get a response.
"""It's easy to use any models with OpenAI compatible endpoints in 3 steps:
STEP 1: Find the OpenAI compatible base URL (see Guide 9 in the guides folder)
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
GROQ_BASE_URL = "https://api.groq.com/openai/v1"STEP 2: Create a python client library instance (the async version)
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
openrouter_client = AsyncOpenAI(base_url=OPENROUTER_BASE_URL, api_key=openrouter_api_key)
groq_client = AsyncOpenAI(base_url=GROQ_BASE_URL, api_key=groq_api_key)STEP 3: Create a model object
gemini_model = OpenAIChatCompletionsModel(model="gemini-3.1-flash-lite", openai_client=gemini_client)
kimi_model = OpenAIChatCompletionsModel(model="moonshotai/kimi-k2.6", openai_client=openrouter_client)
oss_model = OpenAIChatCompletionsModel(model="openai/gpt-oss-120b", openai_client=groq_client)sales_agent1 = Agent(name="Gemini Sales Agent", instructions=instructions, model=gemini_model)
sales_agent2 = Agent(name="Kimi Sales Agent", instructions=instructions, model=kimi_model)
sales_agent3 = Agent(name="GPT-OSS Sales Agent",instructions=instructions, model=oss_model)description = "Use this tool to write a sales email. In the input, just instruct it to write a sales email."
tool1 = sales_agent1.as_tool(tool_name="sales_agent1", tool_description=description)
tool2 = sales_agent2.as_tool(tool_name="sales_agent2", tool_description=description)
tool3 = sales_agent3.as_tool(tool_name="sales_agent3", tool_description=description)from messenger import send_email, push
USE_EMAIL = True
def send_message(subject, text_body, html_body):
if USE_EMAIL:
send_email(subject, text_body, html_body)
else:
push(f"Subject: {subject}\n\n{text_body}")send_message("Yet another test", "Hooray!", "<html><body><h1>Hooray!</h1></body></html>")@function_tool
def send_email_tool(subject: str, text_body: str, html_body: str) -> str:
"""
Send out an email with the given subject and body to all sales prospects
Args:
subject: The subject of the email
text_body: The body of the email as plain text
html_body: The HTML body of the email
"""
send_message(subject, text_body, html_body)
return "Email sent successfully"tools = [tool1, tool2, tool3, send_email_tool]instructions = """
You are a Sales Manager at ComplAI. Your goal is to find the single best cold sales email using the sales_agent tools.
"""
task = """
Follow these steps:
1. Generate Drafts: Use each of the three sales_agent tools to generate different email drafts.
Just instruct each to write a sales email; no further details are needed.
Do not proceed until all three drafts are ready, one from each tool.
2. Evaluate and Select: Review the drafts and choose the single best email using your judgment of which one is most effective.
3. Use your tool to send the best email (and only the best email) to the user. Only send 1 email.
"""
sales_manager = Agent(name="Sales Manager", instructions=instructions, tools=tools, model="gpt-5.4-mini")with trace("Sales Manager across different models"):
result = await Runner.run(sales_manager, task)
print(result.final_output)Check out the trace
Part 2: Structured Outputs
An LLM produces text in natural language. But we can have it instead produce a "python object".
This is accomplished using the usual trickery: clever prompts & json!
- We specify a Python object
- In the System prompt, the LLM is instructed to respond in JSON and follow a Schema which represents the Python object
- The LLM outputs JSON, and the framework populates a Python object based on it
When we specify the Python object, we create a subclass of BaseModel, which is part of the Pydantic framework.
Pydantic is a framework that easily allows defining a JSON schema and mapping between Python and json.
NOTES:
- There is something about the way this is done that IS really clever - if you're interested, look up "constrained decoding".
- Not all providers support Structured Outputs.
class EmailReview(BaseModel):
is_professional: bool = Field(description="Whether the email is professional and appropriate")
number_of_sentences: int = Field(description="The number of sentences in the body of the email, not including the greeting and signature")
contains_placeholders: bool = Field(description="Whether the email contains placeholders for personalization")EmailReview.model_json_schema()email = """
Hi [first_name],
I'm hitting you up to see if you'd like to buy our product. It's really great. You'll miss out if you don't buy it.
Laters.
Ed
"""checker = Agent(name="Checker", instructions="You review potential sales emails", model="gpt-5.4-mini", output_type=EmailReview)
result = await Runner.run(checker, email)review = result.final_output
reviewreview.is_professionalPart 3: Guardrails
Guardrails are extremely important in AgenticAI. Put simply, they are controls that you code either in logic or with another LLM call, to prevent undesirable behavior.
For me, the Guardrails impementation in OpenAI Agents SDK feels a bit like "framework voodoo". I suspect their motivation was to show framework-level controls to address this important topic.
But it's simple and clean to implement guardrails explicitly, as separate Runner.run() calls, or checks in your tool implementations.
Regardless - let's take a look at the framework tooling.
@output_guardrail
async def email_guardrail(ctx, agent, message):
result = await Runner.run(checker, message, context=ctx.context)
review = result.final_output
is_problem = review.contains_placeholders or not review.is_professional
return GuardrailFunctionOutput(output_info={"review": review},tripwire_triggered=is_problem)cowboy_instructions = instructions + "\nSpeak like a cowboy"
sales_agent_cowboy = Agent(name="Cowboy", instructions=cowboy_instructions, model=gemini_model, output_guardrails=[email_guardrail])result = await Runner.run(sales_agent_cowboy, "Write a cold sales email")
result.final_outputCheck out the trace:
On the other hand..
To state the obvious, this is simpler and will work in any framework
simple_cowboy = Agent(name="Simple Cowboy", instructions=cowboy_instructions, model=gemini_model)
result = await Runner.run(simple_cowboy, "Write a cold sales email")
email = result.final_output
print(email)result = await Runner.run(checker, email)
review = result.final_output
if not review.is_professional or review.contains_placeholders:
print("The email is not professional or has placeholders and will not be sent")
else:
print("Email is good")Check out the trace:
OPTIONAL EXTRA: Sandbox Agents
This example will only work on Windows + WSL2, or Mac, or Linux
https://openai.github.io/openai-agents-python/sandbox_agents/
This is an execution harness - a runtime - "a persistent workspace where it can search large document sets, edit files, run commands, generate artifacts, and pick work back up from saved sandbox state."
You have to set up:
- Manifest: the workspace
- Capabilities: what it can do
- SandboxRunConfig: where it runs
from pathlib import Path
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig, SandboxPathGrant
from agents.sandbox.capabilities import Capabilities
from agents.sandbox.entries import LocalDir
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClientCODE_DIR = Path("code").resolve()
OUTPUT_DIR = Path("output").resolve()
if not OUTPUT_DIR.exists():
OUTPUT_DIR.mkdir()CODE_DIRinstructions = f"""
You are a software engineer that fixes bugs.
Review files in the sandbox code directory.
Write the fixed version of the file to this host output directory:
{OUTPUT_DIR}
Use full file paths when writing output.
Respond with a summary of what you did.
"""manifest = Manifest(entries={"code": LocalDir(src=CODE_DIR)}, extra_path_grants=[SandboxPathGrant(path=str(OUTPUT_DIR))])
capabilities = Capabilities.default()
capabilitiesrun_config = RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()), workflow_name="Sandbox coding example")agent = SandboxAgent(name="Engineer", instructions=instructions, model="gpt-5.4-mini", default_manifest=manifest, capabilities=capabilities)result = await Runner.run(agent, "Fix the bug in the code", run_config=run_config)
print(result.final_output)OPTIONAL EXTRA: MCP Teaser!
from agents.mcp import MCPServerStreamableHttptask = """
In the new SandboxAgents feature in the OpenAI Agents SDK as of May 2026, what is the role of the Manifest object?
Always be accurate. If you don't know the answer, say so.
"""agent = Agent(name="Expert", instructions="Answer the question", model="gpt-4o-mini")
result = await Runner.run(agent, task)
print(result.final_output)params = {"url": "https://mcp.context7.com/mcp", "timeout": 60}
async with MCPServerStreamableHttp(name="Context7", params=params) as server:
agent = Agent(name="Expert", instructions="Use Context7 to answer the question", mcp_servers=[server], model="gpt-4o-mini")
result = await Runner.run(agent, task)
print(result.final_output)And see the traces:
