A guide to build OpenAI Agents SDK Multi-agent sales team
Imagine having an AI-powered sales team that can automatically identify leads, gather crucial information, and craft personalized emails, all while you focus on strategic decisions. This is now easily achievable with the OpenAI Agents SDK.
This article will guide you through building such a system, step by step. We'll explore how to create a multi-agent sales team that leverages the power of AI to automate lead generation and personalize outreach, significantly enhancing your sales process.
Key Insights: AI Sales Team in a Nutshell
The result? A streamlined sales process that saves time, reduces manual effort, and increases the chances of engaging potential clients effectively.
Why Should You Care About AI in Sales?
Why is automating sales outreach with AI agents a worthwhile endeavor? Because it directly addresses critical challenges in modern sales:
Ultimately, an AI sales team can lead to a more efficient, personalized, and scalable sales process, resulting in better lead engagement and potentially higher conversion rates.
Let's dive into building your AI sales team!
Defining the Sales Context
Before we introduce our AI agents, we need to establish a shared understanding of the information they'll be working with. This is where the SalesContext comes in. We define a data structure to hold all relevant information about a sales lead.
from dataclasses import dataclass
from typing import Any, Dict, Optional
@dataclass
class SalesContext:
name: Optional[str]
linkedin_url: Optional[str]
profile_data: Optional[Dict[str, Any]]
email_draft: Optional[str]
This SalesContext dataclass will be used throughout our system to store:
By using this shared context, all our agents will have access to the same information, ensuring seamless collaboration.
Assembling Your AI Sales Team: Defining the Agents
Now, let's introduce the agents that will form our AI sales team. We'll define three key roles:
Let's define these agents using the OpenAI Agents SDK. We'll start by importing necessary components and defining instructions for each agent.
from agents import Agent, RunContextWrapper, RunResult, Runner, handoff
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
# Instructions for each agent (defined separately for clarity, not shown here for brevity but will be detailed later)
SALES_TEAM_LEAD_INSTRUCTIONS = """..."""
SALES_DEVELOPMENT_REP_INSTRUCTIONS = """..."""
COLD_EMAIL_SPECIALIST_INSTRUCTIONS = """..."""
async def on_handoff_callback(ctx: RunContextWrapper[SalesContext]):
print("\\\\n Handoff just happened")
# AGENTS
sales_team_lead = Agent[SalesContext](
name="Sales Team Lead",
instructions=prompt_with_handoff_instructions(SALES_TEAM_LEAD_INSTRUCTIONS),
model="gpt-4o", # Using gpt-4o model for better performance
)
sales_development_rep = Agent[SalesContext](
name="Sales Development Rep",
instructions=prompt_with_handoff_instructions(SALES_DEVELOPMENT_REP_INSTRUCTIONS),
tools=[extract_linkedin_profile], # We'll define this tool later
model="gpt-4o",
)
cold_email_specialist = Agent[SalesContext](
name="Cold Email Specialist",
instructions=prompt_with_handoff_instructions(COLD_EMAIL_SPECIALIST_INSTRUCTIONS),
tools=[generate_email], # We'll define this tool later
model="gpt-4o",
)
In this code:
Let's look at the instructions we provide to each agent in more detail.
Sales Team Lead Instructions (SALES_TEAM_LEAD_INSTRUCTIONS)
SALES_TEAM_LEAD_INSTRUCTIONS = """
You are the Sales Team Lead responsible for managing the sales workflow.
Your job is to:
1. Take in lead information (name and LinkedIn URL)
2. Decide which team member to assign tasks to
3. Coordinate the overall process
If there's no user profile outside of name and linkedin url, you should first instruct the Sales Development Rep to extract the LinkedIn profile.
After the user's info has been populated, instruct the Cold Email Specialist agent to draft a personalized email.
"""
The Sales Team Lead is instructed to manage the sales process, take lead information, delegate tasks, and coordinate the workflow. It's also instructed to initiate LinkedIn profile extraction if needed and then delegate email drafting.
Sales Development Rep Instructions (SALES_DEVELOPMENT_REP_INSTRUCTIONS)
SALES_DEVELOPMENT_REP_INSTRUCTIONS = """
You are a Sales Development Rep responsible for researching leads.
Your job is to extract LinkedIn profile information.
Use the extract_linkedin_profile tool to get profile data
You do not do anything else other than the tool given to you.
Once you're done with your job, you should ping your supervisor agent using a tool.
"""
The SDR is clearly instructed to focus solely on extracting LinkedIn profile information using the extract_linkedin_profile tool and nothing else.
Cold Email Specialist Instructions (COLD_EMAIL_SPECIALIST_INSTRUCTIONS)
COLD_EMAIL_SPECIALIST_INSTRUCTIONS = """
You are a Cold Email Specialist responsible for drafting highly personalized, effective outreach emails.
Once you're done with your job, you should ping your supervisor agent using a tool
"""
The Cold Email Specialist's role is to draft personalized outreach emails. Like the SDR, it's instructed to notify its supervisor (Sales Team Lead) upon task completion.
Equipping Your Team: Defining the Tools
Our agents are defined, but they need tools to perform their tasks. We'll define two essential tools:
Let's define the extract_linkedin_profile tool first. This tool will use a web scraping service (scraperapi.com) to fetch the LinkedIn profile page and then parse the markdown content using another function (parse_linkedin_profile) which leverages OpenAI to extract structured data.
import os
from typing import Any, Dict
from agents import RunContextWrapper, function_tool
from dotenv import load_dotenv
import requests
from agent_tools.utils.linkedin import parse_linkedin_profile # Assuming this utility function exists in agent_tools/utils/linkedin.py
from models.sales import SalesContext # Importing SalesContext
load_dotenv()
scraper_api_key = os.environ.get("SCRAPER_API_KEY") # Ensure you have SCRAPER_API_KEY in your .env file
@function_tool
def extract_linkedin_profile(
wrapper: RunContextWrapper[SalesContext], linkedin_url: str
) -> Dict[str, Any]:
"""Extract profile data from a LinkedIn URL"""
print("Start scraping & extracting LinkedIn")
payload = {
"api_key": scraper_api_key,
"url": linkedin_url,
"output_format": "markdown",
}
response = requests.get(
"<https://api.scraperapi.com/>", # Using scraperapi for web scraping
params=payload,
)
page_markdown = response.text
# Extract the user's profile from the response using parse_linkedin_profile utility
profile_data = parse_linkedin_profile(page_markdown)
# Update the context with the extracted profile data
wrapper.context["profile_data"] = profile_data
print("Finished LinkedIn extration.")
return profile_data
In this code:
Now, let's look at the generate_email tool, which the Cold Email Specialist will use. This tool will leverage OpenAI's API to generate a personalized email based on the extracted LinkedIn profile data.
import os
from agents import RunContextWrapper, function_tool
from dotenv import load_dotenv
from openai import OpenAI
from models.sales import SalesContext # Importing SalesContext
load_dotenv()
openai_api_key = os.environ.get("OPENAI_API_KEY") # Ensure you have OPENAI_API_KEY in your .env file
client = OpenAI(api_key=openai_api_key)
@function_tool
def generate_email(
wrapper: RunContextWrapper[SalesContext],
) -> str:
"""Generate a personalized outbound sales email based on LinkedIn profile data"""
if not wrapper.context.get("profile_data"):
return "Error: No LinkedIn profile data available. Please extract profile data first."
system_prompt = "You're an expert at writing personalized outbound sales emails. Write a concise, persuasive email that connects with"
prompt_details = f"""
RECIPIENT INFORMATION:
{wrapper.context["name"]}
{wrapper.context["profile_data"]}
EMAIL DETAILS:
- Sender Name: AI Agent Company
- Sender Company: Company of Agents
Guidelines:
- Keep the email concise (1 paragraph)
- Write like Josh Braun (shine a light on a problem that the prospect might not know)
- No jargons, no hard pitch, just pique interest
"""
response = client.responses.create(
model="gpt-4o-mini", # Using gpt-4o-mini for email generation
input=[
{
"role": "system",
"content": [{"type": "input_text", "text": system_prompt}],
},
{
"role": "user",
"content": [{"type": "input_text", "text": prompt_details}],
},
],
text={"format": {"type": "text"}},
reasoning={},
tools=[],
temperature=0.7,
max_output_tokens=2048,
top_p=1,
store=True,
)
generated_email = response.output_text
# Update the context with the generated email
wrapper.context["email_draft"] = generated_email # Storing the generated email in the shared context
return generated_email
In this generate_email tool:
Finally, let's look at the parse_linkedin_profile utility function used within extract_linkedin_profile. This function takes the markdown content of a LinkedIn profile and uses OpenAI to extract structured data based on a predefined JSON schema.
import logging
import os
import json
from openai import OpenAI
from schemas.linkedin_schema import LINKEDIN_PROFILE_SCHEMA # Assuming this schema is defined in schemas/linkedin_schema.py
# Initialize OpenAI client
openai_api_key = os.environ.get("OPENAI_API_KEY") # Ensure you have OPENAI_API_KEY in your .env file
client = OpenAI(api_key=openai_api_key)
def parse_linkedin_profile(markdown_content: str):
"""Extract structured data from LinkedIn profile HTML content using OpenAI API"""
logging.info("Starting LinkedIn profile structured output extraction")
response = client.responses.create(
model="gpt-4o-mini", # Using gpt-4o-mini for cost-effectiveness
input=[
{
"role": "system",
"content": [
{
"type": "input_text",
"text": "You're an expert at looking at a person's LinkedIn page and extract out relevant information.",
}
],
},
{
"role": "user",
"content": [{"type": "input_text", "text": markdown_content}],
},
],
text={
"format": {
"type": "json_schema",
"name": "linkedin_profile",
"strict": True,
"schema": LINKEDIN_PROFILE_SCHEMA, # Using the predefined JSON schema
}
},
reasoning={},
tools=[],
temperature=0.5,
top_p=1,
)
return json.loads(response.output_text)
In parse_linkedin_profile:
The LINKEDIN_PROFILE_SCHEMA itself is a JSON schema that defines the structure of the LinkedIn profile data we want to extract. Here's an example of what it might look like:
LINKEDIN_PROFILE_SCHEMA = {
"type": "object",
"properties": {
"current_role": {
"type": "string",
"description": "The current job title of the person on LinkedIn.",
},
"company": {
"type": "string",
"description": "The name of the company where the person is currently employed.",
},
"industry": {
"type": "string",
"description": "The industry the person works in.",
},
"experience": {
"type": "array",
"description": "A list of previous job positions held by the person.",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The job title of the position held.",
},
"company": {
"type": "string",
"description": "The name of the company where the person was previously employed.",
},
"duration": {
"type": "string",
"description": "The time range the position was held.",
},
},
"required": ["title", "company", "duration"],
"additionalProperties": False,
},
},
"education": {
"type": "array",
"description": "A list of schools and degrees the person has.",
"items": {
"type": "object",
"properties": {
"school": {
"type": "string",
"description": "The name of the school the person attended.",
},
"degree": {
"type": "string",
"description": "The degree the person earned.",
},
"date_range": {
"type": "string",
"description": "The time range the degree was earned.",
},
},
},
},
"interests": {
"type": "array",
"description": "A list of professional interests mentioned on the profile.",
"items": {
"type": "string",
"description": "A professional interest or topic",
},
},
"recent_activity": {
"type": "string",
"description": "Brief description of recent activity on LinkedIn, if visible.",
},
},
"required": [
"current_role",
"company",
"industry",
"experience",
"education",
"interests",
"recent_activity",
],
"additionalProperties": False,
}
This schema ensures that the parse_linkedin_profile function extracts data in a consistent and structured format, which is crucial for the subsequent email generation step.
Orchestrating the Workflow: Agent Handoffs
Now that we have our agents and tools defined, we need to orchestrate how they work together. This is where agent handoffs come in. We'll define handoffs for the Sales Team Lead to delegate tasks to the SDR and Cold Email Specialist.
sales_team_lead.handoffs = [
handoff(agent=sales_development_rep, on_handoff=on_handoff_callback),
handoff(agent=cold_email_specialist, on_handoff=on_handoff_callback),
]
sales_development_rep.handoffs = [
handoff(agent=sales_team_lead, on_handoff=on_handoff_callback) # SDR can hand back to team lead
]
cold_email_specialist.handoffs = [
handoff(agent=sales_team_lead, on_handoff=on_handoff_callback) # Email specialist can hand back to team lead
]
Here, we define the handoffs attribute for each agent:
The handoff() function specifies the target agent and an optional on_handoff_callback function, which we use to print a message whenever a handoff occurs, helping us track the workflow.
Putting It All Together: Running the Multi-Agent System
With agents, tools, and handoffs defined, we can now create functions to run our multi-agent system. Let's start with a function to process a single sales lead:
async def process_sales_lead(lead: dict) -> RunResult:
"""Process a sales lead through the multi-agent workflow"""
name = lead["name"]
linkedin_url = lead["linkedin_url"]
context: SalesContext = SalesContext( # Initialize SalesContext object
name=name,
linkedin_url=linkedin_url,
profile_data=None,
email_draft=None,
)
print(f"\\\\n Processing lead: {name} ({linkedin_url})")
final_result = await Runner.run(
starting_agent=sales_team_lead, # Start with the Sales Team Lead
input=f"We have a new lead: {name} ({linkedin_url}). Please coordinate the process to research this lead and create a personalized outreach email.",
context=context, # Pass the SalesContext
max_turns=15, # Limit the number of turns to prevent infinite loops
)
return final_result
In process_sales_lead:
To process multiple leads in parallel, we can use the run_dict_tasks_in_parallel utility function:
from data.sales_leads import leads # Assuming leads are defined in data/sales_leads.py
from miscs.run_parallel_agents import run_dict_tasks_in_parallel # Assuming this utility function exists in miscs/run_parallel_agents.py
import asyncio
def display_lead_result(lead: dict, final_result: RunResult):
print("Final results:")
print(f"""
Input: {final_result.input}
Final message from agent: {final_result.final_output}
Last agent: {final_result.last_agent.name}
""")
async def process_multiple_leads_in_parallel():
"""Process a list of predefined leads in parallel"""
print("\\\\n===== Sales Outreach Multi-Agent System =====")
results = await run_dict_tasks_in_parallel(
process_function=process_sales_lead,
input_dicts=leads, # Using a list of leads defined in data/sales_leads.py
show_progress=True,
result_handler=display_lead_result, # Function to display results for each lead
)
return results
def main():
"""Main entry point for the application"""
print("Starting Sales Outreach Multi-Agent System...")
asyncio.run(process_multiple_leads_in_parallel())
if __name__ == "__main__":
main()
In process_multiple_leads_in_parallel:
The main function simply calls process_multiple_leads_in_parallel using asyncio.run to start the asynchronous execution.
To run this system, you would need to:
You should see output in your terminal showing the progress of lead processing, agent handoffs, and final results for each lead. You can also check the OpenAI traces dashboard (platform.openai.com/traces) to see detailed execution traces and timings for each agent and tool call.
Conclusion
Important Considerations:
The Future of AI in Sales:
This example is just the beginning. AI agents have the potential to revolutionize sales in many ways, including: