Chapter 07
Week 2 Day 2 - Orchestration
Week 2 Day 2 - Orchestration
Our first Agentic Framework project!!
Part 1: Email Setup
Part 2: Orchestrating by code
Part 3: Orchestrating by LLMs
- 3a: via Tools
- 3b: via Handoffs
Part 1: Email Setup
Setting up to send emails from your SMTP server
STEP 1: Determining your SMTP Server
Either google or ask ChatGPT / Claude for the SMTP server for your email. Here are some common ones. Some email providers might not have SMTP servers enabled (eg Microsoft 365 for work/school).
Google: smtp.gmail.com
Outlook.com / Hotmail / Live: smtp-mail.outlook.com
Microsoft 365: smtp.office365.com
iCloud Mail: smtp.mail.me.com
Add to your .env file:
EMAIL_SMTP_SERVER=xxxx
STEP 2: Obtain an app specific password
Google how to do this for your email provider. For gmail, you need to have 2-step verification on. Then visit this page:
https://myaccount.google.com/apppasswords
Give it any name; copy the password and add it to your .env file, removing the spaces that it adds. (It should be 16 characters, no spaces.)
EMAIL_APP_PASSWORD=xxxx
STEP 3: Add in your email address:
EMAIL_ADDRESS=xxx
Remember to Save the .env file!
from dotenv import load_dotenv
import requests
from agents import Agent, Runner, trace, function_tool, ModelSettings
from agents.extensions.visualization import draw_graph
from openai.types.responses import ResponseTextDeltaEvent
import os
import asyncio
import smtplib
from email.message import EmailMessage
load_dotenv(override=True)
MODEL_NAME = "gpt-5.4-mini"EMAIL_ADDRESS = os.getenv("EMAIL_ADDRESS")
EMAIL_SMTP_SERVER = os.getenv("EMAIL_SMTP_SERVER")
EMAIL_APP_PASSWORD = os.getenv("EMAIL_APP_PASSWORD")
if EMAIL_ADDRESS:
print("Email address is set")
else:
print("Email address is not set")
if EMAIL_SMTP_SERVER:
print("SMTP server is set")
else:
print("SMTP server is not set")
if EMAIL_APP_PASSWORD:
print("App password is set")
else:
print("App password is not set")
USE_EMAIL = EMAIL_ADDRESS and EMAIL_SMTP_SERVER and EMAIL_APP_PASSWORD
if USE_EMAIL:
print("Email is set up and we will try using it")
else:
print("Email is not set up; we will send push notifications instead")# Here we go
def send_email(subject, text_body, html_body):
msg = EmailMessage()
msg["From"] = EMAIL_ADDRESS
msg["To"] = EMAIL_ADDRESS
msg["Subject"] = subject
msg.set_content(text_body)
msg.add_alternative(html_body, subtype="html")
with smtplib.SMTP(EMAIL_SMTP_SERVER, 587) as server:
server.starttls()
server.login(EMAIL_ADDRESS, EMAIL_APP_PASSWORD)
server.send_message(msg)send_email("Testing testing 123", "Fingers crossed..", "<html><body><strong>Fingers</strong> crossed..</body></html>")If this didn't work, then uncomment the below so that we don't use emails
# USE_EMAIL = FalseOur fallback strategy - send a push
pushover_user = os.getenv("PUSHOVER_USER")
pushover_token = os.getenv("PUSHOVER_TOKEN")
pushover_url = "https://api.pushover.net/1/messages.json"
if pushover_user:
if pushover_user.startswith("u"):
print("Pushover user found and looks good")
else:
print("Pushover user found but doesn't start with u")
else:
print("Pushover user not found")
if pushover_token:
if pushover_token.startswith("a"):
print("Pushover token found and looks good")
else:
print("Pushover token found but doesn't start with a")
else:
print("Pushover token not found")def push(message):
print(f"Push: {message}")
payload = {"user": pushover_user, "token": pushover_token, "message": message}
requests.post(pushover_url, data=payload)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}")OK Now everything should work!
send_message("Big news", "Communications are a go!", "<html><body>Communications are a <strong>go!</strong></body></html>")Agent Orchestration
There are 2 models for Agent Orchestration; by code and by LLMs.
By code: more predictable and deterministic.
By LLMs: more powerful.
An excellent write-up is here:
https://openai.github.io/openai-agents-python/multi_agent/
We will start with by Code.
Part 2: Orchestrating by Code
intro = """
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 emails.
"""
instructions1 = intro + "Your email style is professional, serious, with gravitas and credibility."
instructions2 = intro + "Your email style is witty, engaging, and humorous."
instructions3 = intro + "Your email style is concise, to the point, in the style of a busy senior executive."sales_agent1 = Agent(name="Professional Sales Agent", instructions=instructions1, model=MODEL_NAME)
sales_agent2 = Agent(name="Humorous Sales Agent", instructions=instructions2, model=MODEL_NAME)
sales_agent3 = Agent(name="Executive Sales Agent", instructions=instructions3, model=MODEL_NAME)
result = Runner.run_streamed(sales_agent1, input="Write a cold sales email")
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
print(event.data.delta, end="", flush=True)message = "Write a cold sales email"
with trace("Parallel cold emails"):
results = await asyncio.gather(
Runner.run(sales_agent1, message),
Runner.run(sales_agent2, message),
Runner.run(sales_agent3, message),
)
outputs = [result.final_output for result in results]
for output in outputs:
print(output + "\n\n")decision = """
You pick the best cold sales email from the given options.
Imagine you are a customer and pick the one you are most likely to respond to.
Do not give an explanation; reply with the selected email only.
"""
sales_picker = Agent(name="Sales_picker", instructions=decision, model=MODEL_NAME)message = "Write a cold sales email"
with trace("Sales selection workflow"):
results = await asyncio.gather(
Runner.run(sales_agent1, message),
Runner.run(sales_agent2, message),
Runner.run(sales_agent3, message),
)
outputs = [result.final_output for result in results]
emails = "Cold sales emails:\n\n" + "\n\nEmail:\n\n".join(outputs)
best = await Runner.run(sales_picker, emails)
print(f"Best sales email:\n{best.final_output}")Now go and check out the trace:
Now we will add a tool to the mix.
@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"This has automatically been converted into a tool, with the boilerplate json created
send_email_tool.params_json_schemadecision = """
You pick the best cold sales email from the given options.
Imagine you are a customer and pick the one you are most likely to respond to.
Then use your tool to send the email.
"""
require_tool = ModelSettings(tool_choice="required")
sales_sender = Agent(name="Sales Sender", instructions=decision, model=MODEL_NAME, tools=[send_email_tool], model_settings=require_tool)message = "Write a cold sales email"
with trace("Sales selection workflow with sending"):
results = await asyncio.gather(
Runner.run(sales_agent1, message),
Runner.run(sales_agent2, message),
Runner.run(sales_agent3, message),
)
outputs = [result.final_output for result in results]
emails = "Cold sales emails:\n\n" + "\n\nEmail:\n\n".join(outputs)
response = await Runner.run(sales_sender, emails)
print(f"Final response:\n{response.final_output}")Did that work?!
See the traces for more! This is a great way to debug. Smaller models might require more time and experimentation.
Part 3: Orchestrating by LLMs
3a: via Tools
The simplest way to have 1 Agent choose to invoke another is by treating it as a tool call.
The OpenAI Agents SDK gives a very simple way to do this.
This works best when the flow is:
Agent A -> Agent B -> Agent A
And for the classic "Planning Agent" situation.
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_email_writer_1", tool_description=description)
tool1So now we can gather all the tools together:
A tool for each of our 3 email-writing agents
And a tool for our function to send emails
tool1 = sales_agent1.as_tool(tool_name="sales_email_writer_1", tool_description=description)
tool2 = sales_agent2.as_tool(tool_name="sales_email_writer_2", tool_description=description)
tool3 = sales_agent3.as_tool(tool_name="sales_email_writer_3", tool_description=description)
tools = [tool1, tool2, tool3, send_email_tool]
toolsAnd now it's time for our Sales Manager - our planning agent
instructions = """
You are a Sales Manager at ComplAI. Your goal is to find the single best cold sales email using the sales_writer tools.
"""
task = """
Follow these steps:
1. Generate Drafts: Use each of the three sales_email_writer 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=MODEL_NAME)draw_graph(sales_manager)with trace("Sales manager"):
result = await Runner.run(sales_manager, task)Remember to check the trace
https://platform.openai.com/traces
And then check your email!! Also look in your Junk / Spam folder - after all, this is basically a spam message..
Part 3: Orchestrating by LLMs
3a: via Handoffs
I am not a fan of handoffs. They seem very unreliable. They're not used consistently by other frameworks.
Behind the scenes, OpenAI Agents SDK has implemented these with Tools anyway.
Handoffs represent a way an agent can delegate to an agent, passing control to it
Handoffs and Agents-as-tools are similar:
In both cases, an Agent can collaborate with another Agent
With tools, control passes back
A -> B -> A
With handoffs, control passes across
A -> B
instructions = """
You are a Sales Manager at ComplAI. You get your sales team to draft emails, then send them all to a sales picker.
"""
task = """
Follow these steps:
1. Generate Drafts: Use each of the three sales_email_writer 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. Handoff to the sales sender to choose and send the best email.
"""
tools = [tool1, tool2, tool3]
handoffs = [sales_sender]
sales_manager = Agent(name="Sales Manager", instructions=instructions, tools=tools, handoffs=handoffs, model=MODEL_NAME)draw_graph(sales_manager)with trace("Sales manager"):
result = await Runner.run(sales_manager, task)Remember to check the trace
https://platform.openai.com/traces
And then check your email!!
Note that handoffs can be unrealiable and a little bit frustrating. I needed to force the tool use otherwise this didn't work. If you don't get reliable behavior, try iterating on the prompts - or use a larger model. And enjoy the process; this is what Agentic AI is all about!
