Yet another holiday season has arrived. It’s indeed the most wonderful time of the year. It’s also that time of the year when working professionals set the same old out-of-office reply to every email they get. Well, the problem with automating email responses this way is that it gives the same flavourless replies to all the emails – both relevant and irrelevant ones. This is when even adults start wishing Santa would gift them an email workflow optimisation solution or an AI email assistant that gives smart replies. Well, this year, Santa has come dressed as CrewAI! In this blog, we’ll learn about automating email responses by building an agentic AI system with CrewAI to reply to your out-of-office emails smartly and ensure smart email management.
First, let’s try to understand the context of our problem statement.
This screenshot captures the essence of the problem. If you look closely, you will find emails where my direct intervention is required, and then you will find emails with subscribed newsletters and calendar notifications that do not require any reply.
The existing ‘Vacation Responder’ responds to all the messages with no capability to change the name of the recipient or the contents of the mail based on who it is responding to. Also, it responds to irrelevant emails, which include newsletters, verification code emails, OTP emails, etc.
This is where the CrewAI framework comes to the rescue for email response management. With CrewAI, you can quickly build an email responder agent system, with some simple coding. Relieved? So, let’s build an agentic AI system for automating email responses with CrewAI and bring a layer of optimisation to your email workflow.
Also Read: Automating Email Sorting and Labelling with CrewAI
Before we jump to the code for automating email responses in Gmail, you need to enable the Gmail API and generate the OAuth 2.0 credentials. This will give your email responder agentic system access to your emails. Here’s how to get this done.
Visit the Google Cloud console and log in with your email address. First-time users will need to create an account.
Then select “New Project” in the dropdown, give it a name, and click Create. This project will have the required API-related configurations. While adding the new project, choose your organisation name as the location, as we have selected analyticsvidhya.com.
Click the Navigation Menu from the console’s Dashboard and head to Explore and Enable APIs under the Getting Started section.
On the left-hand side of the screen, select Library, and search for “Gmail API”. Enable it for the project you created.
Next, set up the OAuth consent screen under APIs & Services. Then click Configure Consent Screen.
Choose the type (e.g., External for apps used by anyone). We will chose Internal since we are using it for our own email ID. Then click Create.
Then, name your app and add the User support email and Developer contact information. Here’s where you should add your work email ID. Once done, click on SAVE AND CONTINUE at the bottom of the screen.
Now, we need to define the scopes in the consent screen setup. Scopes, in the context of Google Console, dictate what the API can access. For email-related tasks, you’ll need the following: ‘https://www.googleapis.com/auth/gmail.modify‘. This scope will allow the email responder system to send and modify emails in your Gmail account. Click on ADD OR REMOVE SCOPES and then select the scope mentioned above.
Then click on Update. You can see that the scope has been added. Press SAVE AND CONTINUE.
Now go through the summary, and then click BACK TO DASHBOARD.
Now choose Credentials under APIs & Services and click CREATE CREDENTIALS.
Then select OAuth client ID.
For local development, we will choose the Desktop App option, and then press CREATE.
Now download the JSON file and save it locally at your preferred location.
And that concludes our use of Google Search Console.
To enable CrewAI agents to perform web searches and information retrieval from the internet, it will need the API to the SerperDev Tool. The SerperDev Tool is a Python utility that interfaces with the Serper API, a cost-effective and rapid Google Search API. It enables developers to programmatically retrieve and process Google Search results, including answer boxes, knowledge graphs, and organic listings.
Let’s go through the steps to get the API.
Now, let’s jump to the Python code and build our AI email assistant for automated email responses.
We will begin by importing the relevant libraries to build an agentic system for automating email responses.
# Importing necessary libraries
import os # Provides functions to interact with the operating system
# Importing modules from the CrewAI framework
# CrewAI is a framework for managing agents, tasks, processes, and tools.
from crewai import Agent, Task, Crew, Process # Manage agents, tasks, and processes
from crewai_tools import SerperDevTool # Tool from CrewAI for connecting to Google search
# Importing modules for Google OAuth2 authentication and API interaction
from google.auth.transport.requests import Request # To handle token refresh requests
from google.oauth2.credentials import Credentials # To manage OAuth2 credentials
from google_auth_oauthlib.flow import InstalledAppFlow # To handle OAuth2 login flows
from googleapiclient.discovery import build # To create service objects for Google APIs
# Importing modules for email creation
import base64 # To encode email messages in base64
from email.mime.text import MIMEText # To create MIME-compliant email messages
Now, let’s set the SCOPES variable that defines permissions for the Gmail API. ‘gmail_modify’ allows reading, sending, and modifying emails, excluding permanent deletion, ensuring limited access.
# Gmail API setup
SCOPES = ['https://www.googleapis.com/auth/gmail.modify']
Next, we create the get_gmail_service function that authenticates and connects to the Gmail API. It checks for saved credentials in token.json, refreshing them if expired. If unavailable, it initiates a new login flow using credentials.json. It saves valid credentials for reuse, and returns a Gmail API service object for email operations.
# Function to authenticate and connect gmail API
def get_gmail_service():
creds = None
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
with open('token.json', 'w') as token:
token.write(creds.to_json())
return build('gmail', 'v1', credentials=creds)
Then, we create the get_unread_emails function to retrieve unread emails from the Gmail inbox. It uses the Gmail API service object to list messages with the labels ‘INBOX’ and ‘UNREAD’. The results are executed as a query, and the function returns a list of messages or an empty list if none exist.
# Function to retrieve unread emails
def get_unread_emails(service):
results = service.users().messages().list(userId='me', labelIds=['INBOX', 'UNREAD']).execute()
return results.get('messages', [])
Next, we create the get_email_content function to retrieve and parse email details from Gmail using the message ID. It fetches the full email, extracts the subject and sender from headers, and decodes the body. It supports multi-part emails (extracting plain text) and single-part emails by decoding the Base64-encoded content. The function returns a dictionary containing the email’s subject, sender, and body, ensuring comprehensive handling of different email formats.
def get_email_content(service, msg_id):
message = service.users().messages().get(userId='me', id=msg_id, format='full').execute()
payload = message['payload']
headers = payload['headers']
subject = next(header['value'] for header in headers if header['name'] == 'Subject')
sender = next(header['value'] for header in headers if header['name'] == 'From')
body = ''
if 'parts' in payload:
for part in payload['parts']:
if part['mimeType'] == 'text/plain':
body = base64.urlsafe_b64decode(part['body']['data']).decode('utf-8')
break
else:
body = base64.urlsafe_b64decode(payload['body']['data']).decode('utf-8')
return {'subject': subject, 'sender': sender, 'body': body}
Now, we come to the most important piece of code. The part that helps us filter out irrelevant emails based on certain keywords in the body of the mail or the sender. For me, I do not want my agentic system to respond to emails that are- “subscribed newsletters”, “marketing emails”, “automated reports”, “calendar notifications”, “verification code emails”, “OTP emails”, “HRMS”, “emails containing the terms like ‘do not reply’, ‘no-reply’, ‘accepted your invitation’, ‘rejected your invitation’, etc.”
So, we will create the filter_emails function that identifies emails to ignore based on predefined criteria. It uses two lists: ignore_keywords for phrases like “newsletter,” “OTP,” or “marketing” that indicate irrelevant content, and ignore_senders for sender patterns like “noreply” or “calendar-notification.” The function checks if the sender matches any ignored patterns or if the subject or body contains any ignored keywords. It converts text(sender’s email id + body of the email) to lowercase for consistent, case-insensitive comparisons. If an email matches any criteria, the function returns True to filter it out; otherwise, it returns False.
# Function to filter out emails
def filter_emails(email_content, sender, subject):
ignore_keywords = [
"newsletter", "marketing", "automated report", "calendar notifications", "verification code", "otp", "Join with Google Meet",
"HRMS", "do not reply", "no-reply", "accepted your invitation", "rejected your invitation", "Accepted:"
]
ignore_senders = [
"[email protected]", "calendar-notification", "noreply", "no-reply", "calendar-server.bounces.google.com"
]
# Check sender
if any(ignore in sender.lower() for ignore in ignore_senders):
return True
# Check subject and body for keywords
if any(keyword in subject.lower() or keyword in email_content.lower() for keyword in ignore_keywords):
return True
return False
Next, we create the send_reply function that replies to an email using the Gmail API. To maintain the context of the conversation, we will create a message with the specified recipient (to), body, and thread ID. The message is Base64-encoded and sent via the API. This function ensures replies are linked to the original thread for seamless communication.
def send_reply(service, to, subject, body, thread_id):
message = MIMEText(body)
message['to'] = to
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
return service.users().messages().send(
userId='me',
body={'raw': raw_message, 'threadId': thread_id}
).execute()
Now, we will build the three agents required to execute the task: the email_analyzer, response_drafter, and proofreader agents. I have added the relevant prompts to each agent as per my preference. I recommend you go through the backstory and goal accordingly.
# Define agents
email_analyzer = Agent(
role="Email Analyzer",
goal="Analyze incoming emails and determine appropriate responses to relevant emails",
backstory="You're an expert at understanding email content and context. "
"With this understanding, you determine whether to reply to a particular email or not. "
"You need to ignore emails that are newsletters, marketing emails, Google document comments, google document notifications,"
"calendar notifications, HRMS mails, automated reports, etc.",
verbose=True,
tools=[SerperDevTool()],
allow_delegation=False
)
response_drafter = Agent(
role="Response Drafter",
goal="Draft appropriate responses to emails",
backstory="You're skilled at crafting professional and contextually appropriate email responses."
"Do not Generate responses to emails that are unread newsletters, marketing emails,"
"googl document comments(from: [email protected]), calendar notifications, HRMS mails, automated reports, etc."
"Make the responses crisp. Ensure it is known to people that the I am celebrating the Holidays and will be able to send any required documents once I am join back on 3rd January 2025."
"YOUR NAME is my name and this is the name to be used in the sign-off for each mail you generate response for."
"In the salutation use the recipient's name after 'Hi'",
verbose=True
)
proofreader = Agent(
role="Proofreader",
goal="Ensure email responses are error-free and polished",
backstory="You have a keen eye for detail and excellent grammar skills. Ensure the response is crisp and to the point"
"Also discard email replies generated for emails that are newsletters, marketing emails, googl document comments, calendar notifications, HRMS mails, automated reports, etc.",
verbose=True
)
Now we will define three different tasks for the 3 agents we have created. The description will reflect the directions written in the backstory of their respective agents.
# Define task for email_analyzer agent
def analyze_email(email_content):
return Task(
description=f"Analyze the content and context of the following email:\n\n{email_content}\n"
f"Determine if this email requires a response. Ignore newsletters, marketing emails, "
f"Google document comments (from: [email protected]), Google document notifications, calendar invites, "
f"HRMS mails, automated reports, etc.",
expected_output="A detailed analysis of the email content and context, including whether it requires a response",
agent=email_analyzer
)
# Define task for response_drafter agent
def draft_response(analysis):
return Task(
description=(
f"Draft a professional and contextually appropriate response to the following email analysis:\n\n{analysis}\n\n"
f"Ensure the response is crisp and engaging. Avoid generating email responses for newsletters, marketing emails, "
f"Google document comments (from: [email protected]), Google document notifications, "
f"calendar invites, HRMS mails, and automated reports. "
f"Additionally, include a note indicating that the sender is celebrating the holidays and will be able to provide "
f"any required documents or assistance after returning."
f"YOUR NAME is the name of the sender and this should be used in sign-off for each mail you generate response for."
f"In the salutation use the recipient's name after 'Hi'"
),
expected_output="A well-crafted email response based on the analysis",
agent=response_drafter
)
# Define task for proofreader agent
def proofread_response(draft):
return Task(
description=f"Review and refine the following drafted email response:\n\n{draft}",
expected_output="A polished, error-free email response",
agent=proofreader
)
Next, we create the process_email function to process an email by first applying filter_emails to ignore irrelevant messages. If the email passes the filter, a crew instance manages the sequential execution of tasks: analyzing the email, drafting a response, and proofreading it. The result is evaluated to check if a response is required. If not, it returns None; otherwise, it returns the processed output. This function automates email handling efficiently with clear decision-making at each step.
# Process_email to include filtering logic
def process_email(email_content, sender, subject):
if filter_emails(email_content, sender, subject):
print(f"Filtered out email from: {sender}")
return None
crew = Crew(
agents=[email_analyzer, response_drafter, proofreader],
tasks=[
analyze_email(email_content),
draft_response(""),
proofread_response("")
],
process=Process.sequential,
verbose=True
)
result = crew.kickoff()
# Check if the result is a CrewOutput object
if hasattr(result, 'result'):
final_output = result.result
else:
final_output = str(result)
# Now check if a response is required
if "requires response: false" in final_output.lower():
return None
return final_output
And now we come to our final function for this code. We will create the run_email_replier function which automates email management for CrewAI agents. It fetches unread emails, analyzes their content, and responds if needed. It does this by retrieving detailed email information (body, sender, subject), processing it with process_email to filter irrelevant messages, and determining if a response is required. If so, it sends a reply while maintaining the email thread; otherwise, it skips the email. This streamlined process efficiently handles email triage, ensuring only relevant emails receive attention and automating responses where necessary.
# Run_email_replier to pass sender and subject to process_email
def run_email_replier():
service = get_gmail_service()
unread_emails = get_unread_emails(service)
for email in unread_emails:
email_content = get_email_content(service, email['id'])
response = process_email(email_content['body'], email_content['sender'], email_content['subject'])
if response:
send_reply(service, email_content['sender'], email_content['subject'], response, email['threadId'])
print(f"Replied to email: {email_content['subject']}")
else:
print(f"Skipped email: {email_content['subject']}")
Finally, we set the environment variables for API keys (OPENAI_API_KEY, SERPER_API_KEY that you saved) required for email processing. It executes the run_email_replier function to automate email management using CrewAI agents, including analyzing, filtering, and replying to unread emails. The if __name__ == “__main__”: block ensures the process runs only when executed directly.
# sets environment variables
if __name__ == "__main__":
# Set up environment variables
os.environ['OPENAI_API_KEY'] = 'Your API Key'
os.environ['SERPER_API_KEY'] = 'Your API Key'
# Run the email replier
run_email_replier()
And that’s our email response management agent in full action!
Let’s have a look at the emails. As you can see, for emails where my direct intervention was required, it has generated a customized email as per the context.
And for emails such as newsletters and notifications from HRMs, where replies are not required, it has not given any reply.
And there you have it! A fully functional autonomous agentic system for automating email responses for out-of-office replies This way you can use AI agents for email workflow optimization. If you are satisfied with the responses you can set up a task scheduler to automatically run this code at specific times during the day. When the code is run it will automatically reply to the relevant unread emails.
Also Read: Build LLM Agents on the Fly Without Code With CrewAI
Automating email responses with Agents(Crew AI, Langchain, AutoGen) can transform how we manage replies to out-of-office emails. Moreover, setting up such a system offers a glimpse into a more hopeful future for workplace efficiency. As AI continues to evolve, tools like CrewAI will empower us to maintain seamless communication without compromise, paving the way for a future where technology enhances both productivity and personal well-being. The possibilities are bright, and the future is promising!
A. CrewAI is an open-source Python framework designed to support the development and management of multi-agent AI systems. Using crewAI, you can build LLM backed AI agents that can autonomously make decisions within an environment based on the variables present.
A. Yes! You can use crewAI for email workflow optimisation by automating email responses.
A. For automating email responses in Gmail, you can use as many agents as you like. It depends on your agentic system structure. It is recommended that you build one agent per task.
A. crewAI can perform various tasks, including sorting and writing emails, planning projects, generating articles, scheduling and posting social media content, and more.
A. To automate email sorting using crewAI, you simply need to define the agents with a descriptive backstory within the Agent function, define tasks for each agent using Task functionality, and then create a Crew to enable different agents to collaborate with each other.