DEV Community

Cover image for I saved my team 20 hours by automating support emails with this AI tool šŸ¤”
Sunil Kumar Dash for Composio

Posted on

I saved my team 20 hours by automating support emails with this AI tool šŸ¤”

TL;DR

At Composio, we have been receiving a lot of emails lately regarding tech support, feature requests, collaboration, and other related matters. Managing them was getting very tough.

So, I built an AI tool to route emails to the right person and Slack channel based on the content of the email.

Hereā€™s how I did it.

  • Set up keywords to filter email content, the respective email address, and the Slack channel.
  • Set up an event listener and use Gmail integration to poll incoming emails.
  • Add a Slack integration.
  • Use an AI agent to process and route email contents to emails and Slack channels. For example, issues related to bug fixes get routed to the dev-team channel.

Email GIF


Composio šŸ‘‘ - Comprehensive AI Tools and Integrations platform

Hereā€™s a quick introduction about us.

Composio is an open-source tooling infrastructure for building robust and reliable AI applications. We provide over 100+ tools and integrations across industry verticals from CRM, HRM, and Sales to Productivity, Dev, and Social Media. With any large language model, you can integrate third-party services like GitHub, Slack, Gmail, Doscord, etc, for free.

Guy struggling at the beach

Please help us with a star. šŸ„¹

It would help us to create more articles like this šŸ’–

Star the Composio repository ā­


Project Description

As mentioned, this project finds relevant emails and routes them to respective Slack channels and email IDs.

Here is the overall workflow.

  • Connect Services: Configure Slack and Gmail integration.
  • Configure Keywords: Add keywords that the AI tool will use to filter and categorize emails.
  • Email Processing: The AI tool will poll Gmail inbox repeatedly and filter required emails.
  • Intelligent Routing: The bot analyzes the email contents and routes them to appropriate Slack channels and Email IDs.

Gmail support bot


Tech Stack

To complete the project, you will need the following.

  • Front End: React, Vite, Tailwind.
  • Back End: FastAPI, Pydantic.
  • Authentication: Firebase.
  • AI agents: Composio, CrewAI, and OpenAI.

Quick Description

  • Composio: A toolkit for integrating apps with AI agents.
  • CrewAI: An open-source framework for building collaborative multiple AI bot systems.
  • React + Vite: A combination of React for building UIs and Vite for fast development and build tooling.
  • FastAPI: Python framework for building REST APIs faster.

Prerequisites

For LLM, we will use the OpenAI's GPT-4o.

To get an OpenAI API key, visit their website and create an API key. You might also need some credits.

OpenAI API key

You may also use Google's Gemini models.


Letā€™s get started! šŸ”„

The project has three parts.

  1. Firebase authentication.
  2. Building the AI bot using Composio and CrewAI
  3. Configuring the back end using FastAPI.
  4. Building the front end using React and Vite.

To quickly get started, clone this repository.

Go to the backend directory and run the setup script. This will create a virtual environment and download the necessary libraries.

(Note: Grant permission -> chmod +x setup if you cannot execute it.sh) You'll then be prompted to log in to Composio, link Gmail, and access the Slack workspace.

Add API keys inĀ .env file.

This is the setup file.




#!/bin/bash

# Create a virtual environment
echo "Creating virtual environment..."
python3 -m venv ~/.venvs/gmail_support_bot

# Activate the virtual environment
echo "Activating virtual environment..."
source ~/.venvs/gmail_support_bot/bin/activate

# Install libraries from requirements.txt 
echo "Installing libraries from requirements.txt..."
pip install -r requirements.txt

# Login to your account
echo "Login to your Composio account"
composio login

# Add Gmail tool
echo "Add Gmail tool"
composio add gmail

# Add Slackbot tool
echo "Add Slakbot tool"
composio add slackbot

# Copy env backup to the .env file
if [ -f ".env.example" ]; then
    echo "Copying .env.example to .env..."
    cp .env.example .env
else
    echo "No .env.example file found. Creating a new .env file..."
    touch .env
fi

# Prompt user to fill the .env file
echo "Please fill in the .env file with the necessary environment variables."

echo "Setup completed successfully!"


Enter fullscreen mode Exit fullscreen mode

This will create a Python virtual environment and install libraries fromĀ requirements.txt. You will also be prompted to log in to Composio. This will redirect you to the Composio login page.

Create an account on Composio and paste the displayed key into the terminal to log in to your Composio account.

Composio login

You will then be redirected to the Google Authentication page to add the Gmail and Google Sheet integrations.

Composio Athentication

Once you complete integration, you can visit the composio dashboard and monitor your integrations.

Composio dashboard

Execute the setup script.



  cd backend && ./setup.sh


Enter fullscreen mode Exit fullscreen mode

Firebase Authentication

This is optional, you may skip this section.

We will use Firebase for user authentication and authorization.

So, import the libraries and authenticate using a service account. The account credentials are stored in the JSON file.



import firebase_admin
from firebase_admin import credentials, auth, firestore
from pathlib import Path
import os

cred = credentials.Certificate(f"{Path.cwd()}/firebase/support-bot-49f93-94ae307979d3.json")
firebase_admin.initialize_app(cred)


Enter fullscreen mode Exit fullscreen mode

Intitialize the Firebase admin with required credentials.

Once authenticated, we can initialize a Firestore client:



db = firestore.client()


Enter fullscreen mode Exit fullscreen mode

This allows us to query and manipulate documents in Firestore collections.

Define utility functions



def get_user_by_username(username):
    users_ref = db.collection('users')
    query = users_ref.where('uid', '==', username).limit(1)
    docs = query.get()

    for doc in docs:
        return doc.to_dict()

    return False


Enter fullscreen mode Exit fullscreen mode

The function fetches a user document from Firestore by querying the users collection using the uid field.



def update_row(uid, new_row):
    users_ref = db.collection('users')
    query = users_ref.where('uid', '==', uid).limit(1)
    docs = query.get()

    for doc in docs:
        try:
            doc.reference.update({'sheetsConfig.row': str(new_row)})
            return True
        except Exception as e:
            print(f"Error updating user row: {e}")
            return False

    print(f"User with uid {uid} not found")
    return False


Enter fullscreen mode Exit fullscreen mode

The above function updates a specific field, sheetsConfig.row, in the userā€™s document.



def update_spreadsheet_id(username: str, spreadsheet_id: str):
    users_ref = db.collection('users')
    query = users_ref.where('username', '==', username).limit(1)
    docs = query.get()

    for doc in docs:
        try:
            doc.reference.update(
                {'sheetsConfig.spreadsheet_id': spreadsheet_id})
            print(f"Successfully updated spreadsheet_id for user {username}")
            return True
        except Exception as e:
            print(f"Error updating spreadsheet_id for user {username}: {e}")
            return False

    print(f"User {username} not found")
    return False


Enter fullscreen mode Exit fullscreen mode

Similar to the row update, this function updates the sheetsConfig.spreadsheet_id field in the userā€™s Firestore document:


Building the AI Bot

Letā€™s now build the AI bot.

Define prompts

First, we will define two prompts in the prompt.py file.



prompt1 = f"""
    1. Send an automatic reply to the sender of the email with the following message:
        "Thank you for your email. We have received it and will get back to you shortly"
        using GMAIL_REPLY_TO_THREAD action & if any attachments are present, use GMAIL_SEND_EMAIL send that too.
    2. Check if the email subject (subject) or body (messageText) in the payload contains any of the keywords specified in this dictionary: {[{'slackChannel': 'dev-channel', 'email': 'hrishikeshvastrad14@gmail.com', 'keywords': 'bugs, errors, issues'}, {'slackChannel': 'growth-channel', 'email': 'mskarthikugalawat@gmail.com', 'keywords': 'Collaboration, partnership, sponser'}, {'slackChannel': 'hrishikesh-channel', 'email': 'hrishikesh@gmail.com', 'keywords': 'bill'}]}.
    3. If a keyword match is found:
        a. Check if the original email contains any attachments.
        b. If attachments are present, use the GMAIL_GET_ATTACHMENT action to download them.
        c. Send the email payload to the corresponding email address and slack channel. If attachments are present, include the downloaded attachments.
        message: 'Forwarded email: subject & body.'
    Payload: {payload}
    """
prompt2 = f"""
    1. Check if the email subject (subject) or body (messageText) in the payload contains any of the keywords specified in this dictionary: {keywords}.
    2. If a keyword match is found:
        a. Check if the original email contains any attachments.
        b. If attachments are present, use the GMAIL_GET_ATTACHMENT action to download them.
        c. Send the email payload to the corresponding email address and slack channel. If attachments are present, include the downloaded attachments.
        message: 'Forwarded email: subject & body.'
    Payload: {payload}
    """


Enter fullscreen mode Exit fullscreen mode

Hereā€™s what each prompt means to do.

  • Prompt 1: (for Emails without attachments)
    • This prompt instructs the system to automatically reply to an email using a specified message, check for keywords in the email's subject or body, and forward the email (with any attachments) to a designated Slack channel and email address if a match is found.
  • Prompt 2: (for emails with attachments)
    • This prompt focuses on checking the email for specific keywords in the subject or body and, if a match is found, forwards the email and any attachments to the corresponding Slack channel and email address.

Next, in the agent.py, we will define the agent and the workflow.

Import the libraries and load the environment variables.



import os
import re
import glob
import json
from composio.client.collections import TriggerEventData
from composio_crewai import Action, ComposioToolSet
from crewai import Agent, Crew, Task, Process
from crewai_tools.tools.base_tool import BaseTool
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from typing import Any, Dict
import requests
from firebase.init import update_row
from firebase.init import db
# get_user_by_username
from pathlib import Path
from prompts import prompt1,prompt2

load_dotenv()


Enter fullscreen mode Exit fullscreen mode

Now, create instances of OpenAI and Composio toolset.



lm = ChatOpenAI(model="gpt-4o")

# Trigger instance
composio_toolset1 = ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"))


Enter fullscreen mode Exit fullscreen mode

Initialize the event listener, with G



listener = composio_toolset1.create_trigger_listener()


Enter fullscreen mode Exit fullscreen mode

Next define a callback function with the event listener decorator.



@listener.callback(filters={"trigger_name": "GMAIL_NEW_GMAIL_MESSAGE"})
def callback_new_message(event: TriggerEventData) -> None:
...


Enter fullscreen mode Exit fullscreen mode

Inside the decorator, the trigger name is set to GMAIL_NEW_GMAIL_MESSAGE , so whenever there is a new email event, the trigger will forward the email content to the callback message.

Now, this callback function receives the event data and pre-processes the email contents. Based on the content, the bot will route the emails to appropriate email IDs and Slack channels.

First, from the payload, extract the senderā€™s email.



listener.callback(filters={"trigger_name": "GMAIL_NEW_GMAIL_MESSAGE"})
def callback_new_message(event: TriggerEventData) -> None:
    print("Received new email")
    payload = event.payload
    sender_email = payload['sender']
    sender_email = sender_email.strip()


Enter fullscreen mode Exit fullscreen mode

Now, extract the user name from the email using the UID.



@listener.callback(filters={"trigger_name": "GMAIL_NEW_GMAIL_MESSAGE"})
def callback_new_message(event: TriggerEventData) -> None:
    print("Received new email")
    payload = event.payload
    sender_email = payload['sender']
    sender_email = sender_email.strip()

    def get_user_by_username(username):
        users_ref = db.collection('users')
        query = users_ref.where('username', '==', username).limit(1)
        docs = query.get()

        for doc in docs:
            return doc.to_dict()

        return False

    user = get_user_by_username(event.metadata.connection.clientUniqueUserId)
    uid = user['uid']
    keywords = user['keywords']
    user_email = user['email']


Enter fullscreen mode Exit fullscreen mode

Now, define the Composio toolset with required actions.



@listener.callback(filters={"trigger_name": "GMAIL_NEW_GMAIL_MESSAGE"})
def callback_new_message(event: TriggerEventData) -> None:
    print("Received new email")
    payload = event.payload
    sender_email = payload['sender']
    sender_email = sender_email.strip()

    def get_user_by_username(username):
        users_ref = db.collection('users')
        query = users_ref.where('username', '==', username).limit(1)
        docs = query.get()

        for doc in docs:
            return doc.to_dict()

        return False

    user = get_user_by_username(event.metadata.connection.clientUniqueUserId)
    uid = user['uid']
    keywords = user['keywords']
    user_email = user['email']

    # Tools
    composio_toolset = ComposioToolSet(
        api_key=os.environ.get("COMPOSIO_API_KEY"),
        output_dir=Path.cwd() / "attachments",
        entity_id=event.metadata.connection.clientUniqueUserId)
    tools = composio_toolset.get_actions(actions=[Action.GMAIL_SEND_EMAIL, 
                                                  Action.GMAIL_GET_ATTACHMENT, 
                                                  Action.GMAIL_REPLY_TO_THREAD, 
                                                  Action.SLACKBOT_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL])


Enter fullscreen mode Exit fullscreen mode

Here are the actions we will be using.

  • GMAIL_SEND_EMAIL: For sending emails to user IDs.
  • GMAIL_GET_ATTACHMENT: For extracting attachments from emails.
  • GMAIL_REPLY_TO_THREAD: For replying to an email thread.
  • SLACKBOT_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL: For sending message in a Slack channel.

Now, define the CrewAI agent.



# Agent
    email_assistant = Agent(
        role="Email Assistant",
        goal="Process incoming emails, send auto-replies, and forward emails based on keywords",
        backstory="You're an AI assistant that handles incoming emails, sends automatic responses, and forwards emails to appropriate recipients based on content, including attachments.",
        verbose=True,
        llm=llm,
        tools=tools,
        allow_delegation=False,
    )


Enter fullscreen mode Exit fullscreen mode

The agent is given a specific role, goal, and backstory. These add additional contexts to the LLM before task completion. It also has the LLM and tools.

Now, define the task and the Crew..



task_description = prompt1 if user_email != sender_email else prompt2

process_new_email = Task(
        description=task_description,
        agent=email_assistant,
        expected_output="Summary of email processing, including confirmation of auto-reply sent, whether the email was forwarded & message sent to slack based on keyword matching, and if any attachments were included in the forwarded email.",
    )

email_processing_crew = Crew(
        agents=[email_assistant],
        tasks=[process_new_email],
        verbose=1,
        process=Process.sequential,
    )
result = email_processing_crew.kickoff()
return result


Enter fullscreen mode Exit fullscreen mode

This is what is happening in the above code block.

  • A description, expected output, and agent define the task. The associated email agent will accomplish the task. A crew is defined with the agents and tasks. This crew is the orchestrator responsible for managing the agentic workflow.
  • Finally, the crew run is executed, and the result is returned.


print("Email trigger listener activated!")
listener.listen()


Enter fullscreen mode Exit fullscreen mode

This will start the event listener.

Run this script, and you will have the active event listener ready to fetch emails and orchestrate the workflow.


FastAPI Backend

Now, we will define our API endpoints in the main.py file.

Import libraries and modules and create the FasAPI app.



from fastapi import FastAPI, HTTPException, Request, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from firebase.init import auth
from composio_config import createNewEntity, isEntityConnected, enable_gmail_trigger
import logging
from initialise_agent import initialise

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI()


Enter fullscreen mode Exit fullscreen mode

Add origins and middleware.



origins = [
    "http://localhost",
    "http://localhost:5173",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


Enter fullscreen mode Exit fullscreen mode

Define Pydantic models.



# Pydantic models
class UserData(BaseModel):
    username: str
    appType: str

class NewEntityData(BaseModel):
    username: str
    appType: str
    redirectUrl: str

class EnableTriggerData(BaseModel):
    username: str

class InitialiseAgentData(BaseModel):
    username: str


Enter fullscreen mode Exit fullscreen mode

Define the following endpoints.



@app.post("/newentity")
async def handle_request(user_data: NewEntityData,
                         decoded_token: dict = Depends(verify_token)):
    user_id = decoded_token['uid']
    username = user_data.username
    appType = user_data.appType
    redirectUrl = user_data.redirectUrl
    res = createNewEntity(username, appType, redirectUrl)
    return res

@app.post("/enabletrigger")
async def handle_request(user_data: EnableTriggerData,
                         decoded_token: dict = Depends(verify_token)):
    user_id = decoded_token['uid']
    username = user_data.username
    res = enable_gmail_trigger(username)
    return res

@app.post("/checkconnection")
async def handle_request(user_data: UserData,
                         decoded_token: dict = Depends(verify_token)):
    user_id = decoded_token['uid']
    username = user_data.username
    appType = user_data.appType
    res = isEntityConnected(username, appType)
    return res

@app.post("/initialiseagent")
async def handle_request(user_data: InitialiseAgentData,
                         decoded_token: dict = Depends(verify_token)):
    username = user_data.username
    res = initialise(username)
    return res

@app.get("/")
async def handle_request():
    return "ok"


Enter fullscreen mode Exit fullscreen mode

Here are the descriptions of each endpoint.

  1. `POST /new entity: Creates a new entity with the provided username, appType, and redirectUrl, authenticated using the decoded token.
  2. POST /enabletrigger: Enables a Gmail trigger for the specified username based on the provided data and authentication token.
  3. *`POST /check connection *: This function checks if the entity for a given username and appType is connected and authenticated using the decoded token.
  4. POST /initialiseagent: Initializes an agent for the provided username, authenticated using the decoded token.
  5. GET /: This is a simple health check that returns "okay" to confirm that the service is running.

Finally, define the Unicorn server.



if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

# Start the server (if running locally)
# Run the following command in your terminal: uvicorn main:app --reload


Enter fullscreen mode Exit fullscreen mode

Start the Uvicorn server by running the script using.



python main.py


Enter fullscreen mode Exit fullscreen mode

This will start your server at the port 8000.


Front end with React and Vite

For brevity, we will not go into the deep.

Letā€™s at the important pages.

Home Page

This is going to be our home page.



import Hero from "../components/Hero";
import Benefits from "../components/Benefits";
import FAQ from "../components/FAQ";
import Working from "../components/Working";
import ActionButton from "../components/ActionButton";
const Home = () => {
    return <section className="bg-white dark:bg-gray-900 mt-12">
        <div className="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 lg:px-12">
            <Hero />
            <Benefits />
            <Working />
            <FAQ />
            <div className="mt-20">
                <ActionButton displayName={"Get started"} link={"#"} />
            </div>
        </div>
    </section>
}

export default Home;


Enter fullscreen mode Exit fullscreen mode

This will create a simple home page like the following image.

Gmail Support bot homepage

You can check out the codes for the checking page here.

Agent Setting page

On this page, users can add their Slack and Gmail accounts and configure keywords with their respective Slack channels and Gmail IDs.

Define the App and Entrypoint

Here is the app.jsx file.



import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "./config/firebase";
import Navbar from "./components/Navbar";
import Home from "./pages/Home";
import Footer from "./components/Footer";
import ScrollToTop from "./components/ScrollToTop";
import { useState, useEffect } from "react";
import Login from "./pages/Login";
import Settings from "./pages/Settings";
import NotFound from "./pages/NotFound";
import SkeletonLoader from "./components/SkeletonLoader";
import { SnackbarProvider } from 'notistack'

const ProtectedRoute = ({ user, children }) => {
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  return children;
};

const App = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user);
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  if (loading) {
    return <SkeletonLoader />
  }

  return (
    <BrowserRouter>
      <SnackbarProvider autoHideDuration={3000} preventDuplicate={true} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}>
        <Navbar user={user} />
        <ScrollToTop />
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/Settings" element={
            <ProtectedRoute user={user}>
              <Settings user={user} />
            </ProtectedRoute>
          } />
          <Route path="/" element={<Home />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
        <Footer />
      </SnackbarProvider>
    </BrowserRouter>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode
  • Firebase Authentication: It uses onAuthStateChanged to track the userā€™s authentication status and conditionally render protected routes (like Settings) based on whether the user is logged in.
  • Routing and Navigation: The app uses react-router-dom to handle different routes (like /login, /settings, and /), with protected routes redirecting unauthenticated users to the login page.
  • Loading and UI Components: A skeleton loader is displayed while Firebase determines the userā€™s authentication state, and global notifications are managed with not stack.

Now, the main.jsx as the entrypoint to the app.



import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import ResponsiveMessage from './components/ResponsiveMessage.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <ResponsiveMessage />
    <App />
  </StrictMode>,
)


Enter fullscreen mode Exit fullscreen mode

Once you are done with everything, launch the application.

Running the App

Finally, run the application using the followingĀ npmĀ command.



npm run dev


Enter fullscreen mode Exit fullscreen mode

This will start up the front-end server on the localhost:5345.

You can now visit the app, configure the agent, and see it in action.

Gmail Support app in action


Next Steps

In this article, you built a complete AI tool that handles the support emails and routes them to respective email IDs and Slack channels.

If you liked the article, explore and star the Composio repository for more AI use cases.

Ā 
Ā 

star the repo
Star the Composio repository ā­

Ā 
Ā 

Thank you for reading the article!

Top comments (1)

Collapse
 
anna_lapushner profile image
anna lapushner

This is so awesome! This is what we are learning in my online MIT Course: Data Science and Machine Learning! Specifically we are covering precision and recall, SVMs, Random Forest and Decision Trees. Congratulations on improving your team's efficiency!