How to Create an AI Agent to Manage Your Email Inbox and Reply to Your Cold Email: Code Included

Read Time:
minutes

Introduction

Imagine you are a service based company or a freelancer who constantly wants to make connection and increase your network for better opportunities or a job seeker who want to get more visibility among companies for better opportunities then the best way to get more opportunities is by sending cold emails to companies or organizations.

šŸ’”You can get all of the code shown in this blog from here.

What is cold email?

Cold email is a marketing strategy where a person or organization sends emails to potential clients or customers who havenā€™t expressed prior interest in their product or service. The main goal of Ā cold emailing is to initiate contact and generate interest of leads.

To get more replies and interest from your leads, you need to first research about the different domains and different leads in those domains. You canā€™t send a same email to everyone because if you are selling any service then you must target different domains differently to make more impact and your leads can relate to your service. It is also very important with additional emails to encourage them to take action after sending your first cold email.

Which workflow we are automating?

There are many services to manage your cold email workflow for example smartlead and instantly but these software does require some manual setup. Also you have to reply to every lead manually even if it is a small question which can be answered by any bot.

Even if you create a basic bot using any scripting language for this flow which can be triggered for every email message then still it wonā€™t be that much impactful for your leads because as we discussed above, we need to give personal touch to every conversation because in cold email, you are interacting with lead to get an opportunity and to make a good impression you have to handle every email carefully and thatā€™s why you canā€™t have a basic bot to handle this job for you because it canā€™t talk in a way that you talk generally in your email conversations.

Now imagine you have written a very good email which increased the reply rate for you and now you are getting so many replies and as a good CEO or freelancer you want to answer every email to make the best impression šŸ˜Ž but it is taking too much time for you and you canā€™t focus on other things in company rather than replying to your emails. But what if i tell you that you can have a custom autonomous AI agent who can talk like you, reply to your emails and also classify your emails for you, sounds great right? šŸ‘€

In this blog, we are going to automate this exact email replying workflow for rohan who is CEO and founder of Ionio so we will create a autonomous AI agent completely from scratch which can reply to his emails in his tone and also classify his emails based on conversation so letā€™s start šŸš€!

ā€

Workflow of our agent

Letā€™s take a look at workflow of our agent šŸ” !

The agent will take the campaign id and the csv file of lead emails whom you want to reply. Once that is provided, it will first classify the emails based on the conversation and if the email is positive or needs reply then it will construct a reply for that. Generally we donā€™t need to reply for negative emails or blank emails so this classification will help us to find the category of email.

We will require 4 tools for this workflow:

  • Email Classifier Tool: It will be used to categorize emails based on the email conversation
  • Company Search Tool: It will be used to get the information about any person or organization using apollo API
  • Email Writer Tool: It will be used to construct a email reply for given message.
  • Email Sender Tool: It will be used to send the email reply to given lead using smartlead API

This is how the flow will look like:

ā€

Prerequisites

Now there are some prerequisites which we need to setup so letā€™s take a look at them!

  1. First of all, we are going to use SMARTLEAD as our cold email management software so i encourage you to first create an account on smartlead and create one campaign. After creating a campaign, add your leads as a csv file and then you can follow the remaining steps provided on the SMARTLEAD dashboard to start your campaign.
  2. We will be using smartlead API to get the conversation history and also we will use it to reply back to lead. Get your API key from smartlead by going to settings ā†’ Your profile ā†’ smartlead API Key and copy your api key. If you are using any other cold email software, then read their API documentations and follow accordingly.
  3. We are going to use GPT-4 as our LLM model for agent so get your openai API key from openai dashboard.
  4. We are also going to use apollo API to get the information about lead and itā€™s organization. we will talk about it more later in the blog

ā€

Letā€™s setup our knowledge base

Once you have successfully created a campaign on smartlead and you are getting replies from your leads then itā€™s time to create our knowledge base because to make an agent reply in your tone, we need to feed your past emails and replies to it and then using vector embeddings we can filter out top matching results for current reply and generate our custom reply using LLM(Large language model).

Basically to use this agent, you will have to provide 2 CSVs to our agent:

  • Past emails and replies between you and leads
  • FAQs about you and your organization

We will get the email and reply CSV from smartlead and from that CSV, we will generate our FAQs.

Letā€™s take a look at how you can generate these 2 CSVs!

Getting past emails and replies

You can get the past emails and replies from smartlead by going to your campaign ā†’ inbox. In inbox, filter the leads who have replied to your email from sequence status filter.

After that click on ā€œDownload as CSVā€ button to get these leads.

Now we canā€™t directly create embeddings of this csv because it contains full email threads as a plain text and we have to format it to make it limited to single message and single reply so letā€™s write some code for it šŸ§‘ā€šŸ’»

First install the required dependencies


pip install requests html2text openai

Load your api keys from environment variables


secret = os['SMARTLEAD_API_KEY']
openai_secret = os["OPENAI_KEY"]
apollo_secret = os['APOLLO_API_KEY']

Import the required dependencies


import requests
import csv
import json
import html2text
# Initialize openai client
from openai import OpenAI as OpenAIPython
client = OpenAIPython(
    api_key=openai_secret,
)

To get the message history for any lead, you will need a campaign_id of the campaign for which you want to fetch the conversation. You can get the campaign_id from ā€œList all Campaignsā€ endpoint of smartlead API:


# Get all the campaigns
response = requests.get(f"https://server.smartlead.ai/api/v1/campaigns?api_key={secret}")
response = response.json()
print(response)

From the given array of objects, you can get the campaign id of your campaign.

Once you got the campaign id, itā€™s time to create our knowledge base.

We have all the lead information which we downloaded from smartlead in ā€œLead_Data.csvā€ file and after formatting each reply, we will store it in a new csv file called ā€œfinal.csvā€ and it will have only 2 columns called "Message and Reply"


with open('Lead_Data.csv', mode='r') as file:
    # Create a CSV reader object
    csv_reader = csv.reader(file)
    # Skip the header row
    next(csv_reader)
    with open('final.csv', mode='w', newline='') as final_csv:
      fieldnames = ['Message', 'Reply']
      writer = csv.DictWriter(final_csv, fieldnames=fieldnames)
      writer.writeheader()
      # Iterate over each row in the CSV file
      for row in csv_reader:
		      # Get the lead id from smartlead API
          lead_info = requests.get(f"https://server.smartlead.ai/api/v1/leads/?api_key={secret}&email={row[0]}")
          lead_info = lead_info.json()
          # Get message history from smartlead API
          message_history = requests.get(f"https://server.smartlead.ai/api/v1/campaigns/{campaign_id}/leads/{lead_info['id']}/message-history?api_key={secret}")
          message_history = message_history.json()
          # traverse message history array
          for index, message in enumerate(message_history["history"]):
            if(message["type"] == "REPLY" and index != len(message_history["history"]) - 1):
	            # The conversation you will get from smartlead will be in html so we need to convert it into plain text
              plain_text = html2text.html2text(message["email_body"])
							# Prompt for LLM to format the email thread 
              prompt = f"""
                  Email Thread:
                  ---
                  {plain_text}
                  ---
                  You have given a email thread as a plain text and you have to return the latest email from it

                  You have to follow these steps to do it:
                  Step-1: Get the text which don't starts with '>' because every other text which starts with '>' is a old message in thread
                  (It will be at the starting of text and older messages will be at below this latest message)

                  The email thread format typically look like this:
                  ---
                  Hey john,
                  I am interested
                  Thanks,
                  shivam
                  > sentence 1 ....
                  > sentence 2 ....
                  > ...
                  >> sentence from older emails ...
                  >> sentence from older emails ....
                  >> ....
                  ---
                  For above example, the email message content will be:
                  Hey john,
                  I am interested
                  Thanks,
                  Shivam

                  Note: Please note that the above message content was just an example and your respond must be related to given plain text

                  Step-2: Once you got the latest email then get the message content from it and remove any extra blank space or unnecessary content from email
                  After these steps, Return the email message content in response

                  RESPONSE (Don't return anything except email message):
                """
              email_content = client.chat.completions.create(
                  model="gpt-4",
                  messages=[
                      {"role": "user", "content": prompt}
                  ]
              )
              email_content = email_content.choices[0].message.content
              reply = html2text.html2text(message_history["history"][index+1]["email_body"])
              # Add the formatted data into new csv file
              writer.writerow({'Message': email_content, 'Reply': reply})

Your final csv file will look like this:

Creating FAQs

Now letā€™s create our 2nd knowledge base which is FAQs about you and your organization

To create faqs, we will get all the replies from the ā€œfinal.csvā€ file which we just created and generate some quetions and answers about you and your organization from those replies so that our agent can use this information as a reference. You can also use your own FAQs instead of generating them from emails.

Here we will use textsplitter from Langchain because we canā€™t pass this whole csv content at once in prompt so we will convert the csv content in chunks and generate FAQs for that specific chunk and at last we will combine every chunk faqs in single array of json.

Letā€™s first install langchain module


pip install langchain

Now letā€™s write logic for extracting FAQs from our csv file


from langchain.prompts import PromptTemplate
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.summarize import load_summarize_chain
llm = OpenAI(temperature=0,api_key=openai_secret)
def load_csv(file_path):
    # Create a list to hold dictionaries
    data_list = []

    # Open the CSV file and read its content
    with open(file_path, 'r') as csv_file:
        csv_reader = csv.DictReader(csv_file)

        # For each row, append it as a dictionary to the list
        for row in csv_reader:
            data_list.append(row)

    return data_list

def extract_faq(text_data):
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=30,
        length_function = len,
        is_separator_regex=False)

    texts = text_splitter.split_text(text_data)
    docs = text_splitter.create_documents(texts)
    print(docs)
    map_prompt = """
    PAST EMAILS:
    {text}
    ----

    You are a smart AI assistant, above is some past emails from Rohan (Founder and CEO of Ionio),
    your goal is to learn & extract common FAQ about Rohan and Ionio
    (include both question & answer, return results in JSON)

    Response MUST be like this:
    Question: Question 1
    Answer: Answer 1

    Question: Question 2
    Answer: Answer 2
    """
    map_prompt_template = PromptTemplate(template=map_prompt, input_variables=["text"])

    combine_prompt = """
    The following are set of FAQs about Rohan (Founder and CEO of Ionio):
    {text}
    Take every question and answer and combine them into a final array of faq,
    include both question & answer in json format
    Response should be atleast 3000 characters long and ask incrementally better questions and give better answers

    Every json object will have these 2 fields:
    Question:
    Answer:

    NOTE: IGNORE THE UNTERMINATED STRINGS AND DON'T ADD THEM IN ARRAY BUT ADD ALL OTHER COMPLETE QUESTIONS IN ARRAY
    Make sure the response array is parsable to json otherwise the code will break

    array of FAQ:
    """
    combine_prompt_template = PromptTemplate(template=combine_prompt, input_variables=["text"])
		# we will use map_reduce chain here
    summary_chain = load_summarize_chain(llm=llm,
                                        chain_type='map_reduce',
                                        map_prompt=map_prompt_template,
                                        combine_prompt=combine_prompt_template,
                                        verbose=True,

                                        )

    output = summary_chain.run(docs)
    print("--------- OUTPUT -------------")
    print(output)
    # faqs = json.loads(output)
    
# Function to save json object in csv file
def save_json_to_csv(data, file_name):
    with open(file_name, mode='w', newline='', encoding='utf-8') as file:
        # Get the keys (column names) from the first dictionary in the list
        fieldnames = data[0].keys()

        # Create a CSV dict writer object
        writer = csv.DictWriter(file, fieldnames=fieldnames)

        # Write the header row
        writer.writeheader()

        # Write the data rows
        for entry in data:
            writer.writerow(entry)


# Print or save the JSON data
past_emails = load_csv("final.csv")

# Extracting Rohan's replies
replies = [entry["Reply"] for entry in past_emails]
replies_string = json.dumps(replies)
extract_faq(replies_string)

But there is one problem ā˜¹ļø, for some reason i was not getting the full array of json as a response as you can see in the below screenshot:

It could have happened because of the langchain system prompts or the context window problem and i tried to make it work with different chunk sizes and llms but it didnā€™t work. So instead of wasting time on this i just passed all the faqs from different chunks and passed them to chatgpt and it gave me json file of FAQs.

Take this json data and store it in file called ā€œfaq.csvā€


# Convert json to csv
with open("faq.json", "r") as file:
    faqs = json.load(file)

save_json_to_csv(faqs, "faq.csv")

Now we have both CSVs ready, itā€™s time to create our agent šŸ¤–

ā€

Creating our agent

Now we have all the things ready which we want to create our agent so letā€™s start creating our agent by importing required dependencies


# Import dependencies
import requests
import csv
import json
import html2text
import ast
from openai import OpenAI as OpenAIPython
from langchain.llms import OpenAI
from langchain.schema import SystemMessage
from langchain.prompts import MessagesPlaceholder
from langchain.memory import ConversationBufferWindowMemory, ConversationSummaryBufferMemory
from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI
from langchain.pydantic_v1 import BaseModel, Field
from typing import Type, List
from langchain.tools import BaseTool
OpenAI_LLM = OpenAI(temperature=0.6,api_key=openai_secret)
ChatOpenAI_LLM = ChatOpenAI(temperature=0, model="gpt-4",api_key=openai_secret)
client = OpenAIPython(
    # This is the default and can be omitted
    api_key=openai_secret,
)
from datetime import datetime
current_datetime = datetime.utcnow()

Now letā€™s create our custom tools!

Email Categorization Tool

Once we get the email conversation from smartlead, the first task of agent is to categorize this email based on the email conversation and then decide whether to reply to this email or not. We will use gpt-4 as our LLM model here to categorize these emails.

This tool will take only 1 input parameter:

  • Conversation: Past email conversation between rohan and lead as an array of json objects where each object contains sender and message field

We will categorize these emails in total 8 categories but you can customize these categories according to your needs and use case. Letā€™s take a look at prompt which we are going to pass to our LLM.


prompt = f"""
        Email Conversation History:
        ---
        {conversation}
        ---
        You have given an array of conversation between Rohan Sawant and a client
        Your goal is to categorize this email based on the conversation history from the given categories:

        1. Meeting_Ready_Lead: they have shown positive intent and are interested in getting on a call
        2. Power: If theyā€™re interested and we want to push for a call
        3. Question: If they have any question regarding anything
        4. Unsubscribe: They want to unsubscribe themselves from our email list
        5. OOO: They are out of office
        6. No_Longer_Works: They no longer works in the company
        7. Not_Interested: They are not interested
        8. Info: these are emails that don't fit into any of the above categories.

        Note: Your final response MUST BE the category name ONLY

        RESPONSE:
      """

Letā€™s write code for it!


# Input class for tool so that it can follow strict input parameter schema
class CategorizeEmailInput(BaseModel):
    conversation: str = Field(description="Email conversation array")

class CategorizeEmailTool(BaseTool):
		# Provide proper name and description for your tool
    name = "email_categorizer_tool"
    description = "use this tool when have email conversation history and you want to categorize this email"
    args_schema: Type[BaseModel] = CategorizeEmailInput

    def _run(self, conversation: str):
      message = client.chat.completions.create(
          model="gpt-4",
          messages=[
              {"role": "user", "content": prompt}
          ]
      )
      category = message.choices[0].message.content
      return category

    def _arun(self, url: str):
        raise NotImplementedError(
            "categorise_email does not support async")

Company Search Tool

To give more personal touch to every email, it is good to search about lead and itā€™s organization first before booking a meet with them and if you mention it in your email reply then it can make a good impact which shows that the person sending an email is real person who searched about your organization and is interested to work with you šŸ˜

To search about any organization, we will use apollo API which will give us information about any organization from the lead email.

This tool will take 2 parameters as input:

  • Email: Email of lead
  • Category: Category of email conversation (we are passing this because we want this tool to run only after categorization is done)

So letā€™s create our tool!


class CompanySearchToolInput(BaseModel):
    email: str = Field(description="Email of sender")
    category: str = Field(description="Category of email")

class CompanySearchTool(BaseTool):
    name = "company_search_tool"
    description = "use this tool when you want to get information about any company"
    args_schema: Type[BaseModel] = CompanySearchToolInput

    def _run(self, email: str, category: str):
        data = {
            "api_key":apollo_secret,
            "email":email
        }
        response = requests.post(f"https://api.apollo.io/v1/people/match",data=data)
        response = response.json()
        return response["person"]["organization"]["short_description"]

    def _arun(self, url: str):
        raise NotImplementedError(
            "categorise_email does not support async")

Email Writer Tool

Now itā€™s time to create the core tool of whole workflow because the main goal of this agent is to mimic the tone of rohan and reply in a way in which rohan replies to his cold emails. So we will have to provide all the required information and a detailed prompt to this tool so that we can get more better results.

The basic idea of achieving this will be like this:

We can write a code for this entire architecture where first we have to store our CSV knowledge base in a vector database and then based on the user input we can perform semantic search on the message and FAQs. Once we got all the similar email and reply pairs, we can pass this to LLM and it will construct an email response for you which mimics the tone of rohan.

For this blog, i am going to use relevance platform where we can create our custom AI tools and host them. The main advantage of relevance is it comes with pre built vector database where you can store your data and then use that data in your tools and you donā€™t need to worry about any vector embedding or semantic search process.

First of all, goto relevance dashboard and add the ā€œfinal.csvā€ which contains message and reply pairs for your past emails:

When it asks for ā€œwhat content should be added to knowledge?ā€ then only select message because we only want to vectorize the messages and we are going to perform semantic search on these messages with given lead message for which we have to generate a reply

Once you have created a table for message and reply pairs, its time to add our ā€œFAQ.csvā€ file in this knowledge base. This time we will vectorize both question and answer.

Now we have uploaded both CSVs, itā€™s time to create our tool

Follow these steps to create your tool:

  • Go to tool section from navbar and click on ā€œcreate new toolā€ and select ā€œstart from scratchā€.
  • Add your knowledge base in knowledge section.
  • Specify your user inputs in input section. We are going to use 4 inputs here:
    • Client Email: The email text for which we have to construct a email reply
    • Sender Name: The name of sender
    • Conversation History: Conversation history between you and sender
    • Company Description: Description about leadā€™s organization
  • At last, select your LLM and add the below prompt in prompt section:

You are the email inbox manager for Rohan Sawant who is Founder and CEO of ionio. He have sent an cold emails to some people and they have replied to rohan and now its your job to help draft email response for rohan that mimic the past reply. Rohan is 35 year old millenial so please talk in his tone only.

PAST EXAMPLES:
"""
{{knowledge.final_csv}}
"""

Use this data as your knowledge base:
"""
{{knowledge.faq_csv}}
"""

Use this conversation history as a context:
{{conversation_history}}

Here is the new email for which you have to generate a reply:
{{client_email}}

Here is the description about organization of sender:
{{company_description}}

Sender Name: 
{{sender}}

While generating reply YOU MUST FOLLOW these instructions:

- Try to reply in a way rohan replied in his past emails
- Don't repeat anything which client said and add everything in this reply only, don't postpone anything to next email.
- Make the reply short and easy to understand without adding any unnecessary information
- At the end of email body, take something from sender's organization description which rohan might like if he was replying to this email and add it before last line of email as a 10-15 words sentence which starts with " PS, I just checked your website and i loved "

Now we are ready to use this tool šŸš€!

Fill the required input values and click on ā€œrun stepā€ and we can see that i am getting the reply which looks exactly how rohan talks in his emails šŸ‘€!

Now itā€™s time to integrate this tool in our agent so click on ā€œAPIā€ section and click on ā€œDeployā€ and it will deploy this tool for you for free and you can trigger this tool by making the POST request to given API endpoint.

This email writer tool will take the same 4 parameters which we used as an input in the tool which we created on relevance because we are going to pass these 4 parameters as a input to that tool.

Here is the code for email writer tool:


class EmailWriterToolInput(BaseModel):
    latest_reply: str = Field(description="Latest reply from the prospect")
    conversation_history: str = Field(description="Array of conversation history")
    sender: str = Field(description="Name of sender")
    company_info: str = Field(description="Information about sender's company")

class EmailWriterTool(BaseTool):
    name = "email_writer_tool"
    description = "use this tool when you have given a email and you have to construct a reply for it"
    args_schema: Type[BaseModel] = EmailWriterToolInput

    def _run(self, latest_reply: str, conversation_history: str, sender: str,company_info: str):
        # making api call to relevance tool
        headers = {
            "Content-Type": "application/json"
        }
        data = {
            "params": {
                "client_email": latest_reply,
                "sender":sender,
                "conversation_history":conversation_history,
                "company_description":company_info
            },
            "project": "Your_Project_Id"
        }

        res = requests.post("https://api-xxxxx.tryrelevance.com/latest/studios/xxxxxx-xxxx-xxxx-xxxxxx/trigger_limited",data=json.dumps(data),headers=headers)
        res = res.json()
        return res["output"]["answer"]

    def _arun(self, url: str):
        raise NotImplementedError(
            "email writer tool does not support async")


Email Sender Tool

Letā€™s create our last tool which is email sender tool (we can trigger this function with agent too but instead of passing so many parameters to agent, we can just call it after agent gives us reply) which will help us to send emails back to leads using smartlead API. we will require some parameters mentioned on their API documentation page to send an email.


def EmailSenderTool(campaign_id,email_stats_id,email_body,reply_message_id,reply_email_body,email):
  url = f"https://server.smartlead.ai/api/v1/campaigns/{campaign_id}/reply-email-thread"
  data = {
    "email_stats_id": email_stats_id,
    "email_body": email_body,
    "reply_message_id": reply_message_id,
    "reply_email_time": current_datetime.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
    "reply_email_body": reply_email_body,
    "cc": email,
  }
  response = requests.post(url,data=data)
  response = response.json()
  print("Email sent to lead!")

Now letā€™s initialize our agent by providing a system prompt, memory and list of tools to it!


# Creating agent
system_message = SystemMessage(
    content="""
    You are an email inbox assistant of an Rohan sawant who is founder and CEO of Ionio,
    Which provides AI-solutions to technical and non-technical organizations
    Rohan have sent a cold email to some leads and you have provided a conversation history between rohan sawant and the lead

    Follow these steps while generating email reply:
    Step-1: First categorize the email based on given conversation history and get the category of email.
    Step-2: check the sender of the last message and if the sender is not rohan sawant then goto step-3
    If the sender of last message is rohan sawant then you don't need to construct a reply

    Step-3: Once you get the category, follow these conditions while constructing a reply email:
    1. If category is "Meeting_Ready_Lead" or "Power", ONLY THEN search about company using lead's email and then construct the reply email
    2. For all the other categories, DON'T construct a reply

    Your final response MUST BE in json with these keys:
    reply: Constructed email reply for positive email (leave it blank if no reply constructed or the last sender is rohan sawant)
    category: Category of given email based on email conversation history

    RESPONSE(Don't return anything except the json object):
    """
)
agent_kwargs = {
    "system_message": system_message,
}
# Memory
memory = ConversationBufferWindowMemory(
    memory_key='memory',
    k=1,
    return_messages=True
)
# Tools
tools = [
    CategorizeEmailTool(),
    CompanySearchTool(),
    EmailWriterTool()
]
# Initializing agent
agent = initialize_agent(
    tools,
    llm=ChatOpenAI_LLM,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True,
    agent_kwargs=agent_kwargs,
    memory=memory,
    handle_parsing_errors=True
)

Now we are ready to use this agent and we will take 2 things from user before running this agent:

  • Campaign ID: Campaign id of the campaign for which you want to run this agent
  • Lead CSV: The CSV file of leads whom you want to reply or categorize their replies (follow the same procedure which we discussed while building our email/reply knowledge base to get the csv file)

Once we get the CSV file of leads, we will traverse every lead conversation one by one and run agent for every lead and we will reply to them if needed.


# Run the agent for your leads
campaign_id = input("Enter campaign id:")
with open('Campaign_Leads.csv', mode='r') as file:
  # Create a CSV reader object
  csv_reader = csv.reader(file)
  for index,row in enumerate(csv_reader):
    if index == 0:
      continue
    # Get lead ID
    lead_info = requests.get(f"https://server.smartlead.ai/api/v1/leads/?api_key={secret}&email={row[1]}")
    lead_info = lead_info.json()
    # Get conversation history of lead
    message_history = requests.get(f"https://server.smartlead.ai/api/v1/campaigns/{campaign_id}/leads/{lead_info['id']}/message-history?api_key={secret}")
    message_history = message_history.json()
    message_history = message_history["history"]
    conversation_history = []
    # Format every message in conversation history
    for message in message_history:
      plain_text = html2text.html2text(message["email_body"])
      prompt = f"""
        Email Thread:
        ---
        {plain_text}
        ---
        You have given a email thread as a plain text and you have to return the latest email from it

        You have to follow these steps to do it:
        Step-1: Get the text which don't starts with '>' because every other text which starts with '>' is a old message in thread
        (It will be at the starting of text and older messages will be at below this latest message)

        The email thread format typically look like this:
        ---
        Hey john,
        I am interested
        Thanks,
        shivam
        > sentence 1 ....
        > sentence 2 ....
        > ...
        >> sentence from older emails ...
        >> sentence from older emails ....
        >> ....
        ---
        For above example, the email message content will be:
        Hey john,
        I am interested
        Thanks,
        Shivam

        Note: Please note that the above message content was just an example and your respond must be related to given plain text

        Step-2: Once you got the latest email then get the message content from it and remove any extra blank space or unnecessary content from email
        After these steps, Return the email message content in response

        RESPONSE (Don't return anything except email message):
      """
      email_content = client.chat.completions.create(
          model="gpt-4",
          messages=[
              {"role": "user", "content": prompt}
          ]
      )
      email_content = email_content.choices[0].message.content
      convo = {
          "sender": "rohan sawant" if message["type"] == "SENT" else row[0],
          "message": email_content
      }
      conversation_history.append(convo)
    
    # Prompt for our agent
    prompt = f"""
      Email conversation history:
      ---
      {conversation_history}
      ---
      Lead Name: {row[0]}
      Lead Email: {row[1]}

      Sender of last message: {conversation_history[len(conversation_history) - 1]["sender"]}
      """
    response = agent({"input": prompt})
    response = json.loads(response["output"])
    history = conversation_history[len(conversation_history)-1]
	  # If there is reply which needs to be send then use email sender tool to send email
    if response['reply'] != "":
    	EmailSenderTool(campaign_id=campaign_id,email_stats_id=history["stats_id"],email_body=response["output"],reply_message_id=history["message_id"],reply_email_body=history["email_body"],email=row[1])

After running the above code, you will see that agent is running for every lead one by one and replying to leads if needed.

It is also able to categorize different type of emails

And we have successfully automated the cold email workflow using our agent šŸŽ‰

šŸ’”You can get all of the code shown in this blog from here.

ā€

Challenges

Letā€™s discuss the challenges which i faced while making this agent šŸ‘€

Filtering Leads

The first issue i faced was related to filtering the leads because there are thousands of leads in every campaign and you canā€™t just check every lead one by one and check if they have replied to your email or not and then run the agent because it will take so many API requests and you might face rate limit error.

So instead of checking every lead, i decided to get only those leads who have replied to my first cold email and in smartlead you can easily get it using filters provided on the platform and you can easily export then in CSV file. If you are using any other cold email software then there must be similar option to filter out leads based on their reply status.

Once you got the leads who have replied to your email, you can then get the conversation history between you and lead and check the sender of last message and if it is lead then you need to reply to that message otherwise you have already replied to their message then you donā€™t need to run agent (If you want to send a follow up message then you can run that agent with some changes in prompt for followups).

Formatting The Email Data

The email conversation history which i was getting from smartlead API was not properly formatted because i was getting the email thread as html and even after converting that html into plain text it was looking like this:

To get only the latest message from long email thread, I used openai to format it properly and then i was able to get the latest message from this long email thread without any extra space or information.

To build the conversation history array, I traversed the array coming from smartlead API and for every element in that array, I formatted it with openai and then added it into our conversation history array.

Generating Reply in Human Tone

The main aim of agent was to generate replies in any humanā€™s tone and to achieve that i first tried with only the email/reply knowledge base and FAQs in prompt but still it was not perfect. There were still some issues like:

  • It was repeating the question or content from leadā€™s message ( For example, if lead says they are interested then agent was saying ā€œI am glad that you are interestedā€ and it looks bot generated text)
  • The message was not concise and descriptive but it was just looking like a long paragraph (Rohanā€™s previous replies were short and concise so we need to match that pattern)
  • Agent donā€™t have context of full conversation because it only have last message

To solve this issue, i added some conditions in LLM prompt which LLM should follow and then i got better results and also i then added conversation history as a input parameter to our email writer tool.

To give more human touch to our email response, I then decided to search about lead and their organization. So first i decided to scrape their website but not every website allows scraping so you might not get much information so i used apollo API to get information about lead and their organization and then on every email reply i added one extra line which tells them what i liked about their organization or website to make it look like more natural and human.

It was looking like this before adding above conditions:

This is how it looks after applying all the changes:

You can also search about leadā€™s latest achievements or news from internet and add that too and also you donā€™t need to add it in response everytime so you can tell the agent when you need to add this line.

ā€

Conclusion

The main goal was this agent to handle every lead reply very carefully because as i said before, the reply rate for some emails might not be high so you need to handle every lead very carefully and donā€™t make them feel like you are just spamming on every lead and you have made some sort of bot script which replies to your emails because then they might lose interest.

AI agents are next big thing in AI-ML field and many companies are using them to automate their workflows because if you hire a inbox manager or email writer for your companyā€™s cold emails then it might cost you at least $50k a year. As we saw in blog, itā€™s very easy to setup an autonomous agent for your organization using langchain and python knowledge.

Want to Automate Your Workflows?

So, whether you are a small team looking for clients, a job seeker looking for better opportunities, a freelancer looking for clients or a large organization seeking for more clients then cold emails are one of the best ways to get more connections and AI agents can automate this work for you.

If you are looking to build custom AI agents to automate your workflows then kindly book a call with us and we will be happy to convert your ideas into reality and make your life easy.

Thanks for reading šŸ˜„.

Book an AI consultation

Looking to build AI solutions? Let's chat.
ā€
Schedule your consultation today - this not a sales call, feel free to come prepared with your technical queries.
ā€
You'll be meeting Rohan Sawant, the Founder.
 Company
Book a Call

Let us help you.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Behind the Blog šŸ‘€
Shivam Danawale
Writer

Shivam is an AI Researcher & Full Stack Engineer at Ionio.

Rohan Sawant
Editor

Rohan is the Founder & CEO of Ionio. I make everyone write all these nice articles... šŸ„µ