├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .env.example ├── agent.py ├── composio_config.py ├── firebase │ ├── __pycache__ │ │ └── init.cpython-312.pyc │ └── init.py ├── initialize_sheet_agent.py ├── main.py ├── requirements.txt └── setup.sh ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.css ├── App.jsx ├── assets │ ├── brain.svg │ └── react.svg ├── components │ ├── ActionButton.jsx │ ├── AddAgent.jsx │ ├── Avatar.jsx │ ├── BenefitCard.jsx │ ├── Benefits.jsx │ ├── FAQ.jsx │ ├── Footer.jsx │ ├── Hero.jsx │ ├── LogoComponent.jsx │ ├── Navbar.jsx │ ├── ResponsiveMessage.jsx │ ├── ScrollToTop.jsx │ ├── SettingsAttribute.jsx │ ├── SkeletonLoader.jsx │ ├── SmallButton.jsx │ ├── Working.jsx │ └── WorkingFlow.jsx ├── config │ └── firebase.js ├── index.css ├── main.jsx ├── pages │ ├── Agent.jsx │ ├── Dashboard.jsx │ ├── Home.jsx │ ├── Login.jsx │ ├── NotFound.jsx │ └── Settings.jsx └── utils │ └── authUtils.js ├── tailwind.config.js ├── vercel.json └── vite.config.js /.env.example: -------------------------------------------------------------------------------- 1 | VITE_FIREBASE_API_KEY= 2 | VITE_FIREBASE_AUTH_DOMAIN= 3 | VITE_FIREBASE_PROJECT_ID= 4 | VITE_FIREBASE_STORAGE_BUCKET= 5 | VITE_FIREBASE_MESSAGING_SENDER_ID= 6 | VITE_FIREBASE_APP_ID= 7 | VITE_FIREBASE_MEASUREMENT_ID= 8 | VITE_BACKEND_URL= 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .env 11 | backend/firebase/genius-57d8d-firebase-adminsdk-ue7u9-90656332c6.json 12 | backend/attachments 13 | 14 | backend/__pycache__ 15 | backend/genius-57d8d-firebase-adminsdk-ue7u9-90656332c6.json 16 | 17 | node_modules 18 | dist 19 | dist-ssr 20 | *.local 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | !.vscode/extensions.json 25 | .idea 26 | .DS_Store 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw? 32 | 33 | backend/.env 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Abhishek Patil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡️GmailGenius: Supercharge your Gmail 2 | *Automatically processes new emails, extracts data from attachments, and organizes everything in a spreadsheet!* 3 | 4 | ## Why GmailGenius? 5 | 6 | I developed GmailGenius because managing invoices felt like trying to find a needle in a haystack—while blindfolded! I was constantly losing track of due dates and getting hit with late fees, which turned my financial life into a chaotic mess. So, I thought, why not create an AI buddy to handle the email chaos? With GmailGenius, users can connect their Gmail, specify keywords, and let the AI scan for invoices, extracting all the important details straight into Google Sheets. I built the AI agents using CrewAI, which allowed me to create specialized agents tailored for this task. The real magic comes from [Composio (https://app.composio.dev)’s powerful [Gmail](https://app.composio.dev/app/gmail) and [Google sheets](https://app.composio.dev/app/googlesheets) tools, which made integrating these features a breeze—just plug and play! Thanks to Composio, I could focus on creating a seamless user experience while my AI genius does the heavy lifting. Now you can say goodbye to late fees and hello to a more organized inbox! 7 | 8 | #### Checkout how others are using GmailGenius: 9 | [![Open in Dev.to](https://img.shields.io/badge/Open%20in-Dev.to-green?logo=dev.to&style=for-the-badge)](https://dev.to/composiodev/i-built-an-ai-tool-to-handle-my-moms-invoices-and-saved-her-20-hours-of-work-44h1) 10 | [![Open in Reddit](https://img.shields.io/badge/Open%20in-Reddit-blue?logo=reddit&style=for-the-badge)](https://www.reddit.com/r/selfhosted/comments/1f7f8f4/i_built_an_ai_tool_to_handle_my_moms_invoices_and/) 11 | 12 | ## Demo 13 | ### Check it out on Replit 14 | [![Open in Replit](https://img.shields.io/badge/Open%20in-Replit-blue?logo=replit&style=for-the-badge)](https://replit.com/@abishkpatil/gmail-assistant-fb) 15 | 16 | ### Live Demo ([Live Link](https://gmail-assistant-six.vercel.app/)) 17 | [![gmailgenius-demo](https://github.com/user-attachments/assets/abb24495-d242-42f3-8cff-599182f735f4)](https://drive.google.com/file/d/1_CWZ3yNK4pxe8Ey1bnQq4C6H_lEHDICb/preview) 18 | 19 | ## Description 20 | GmailGenius simplifies the process of finding relevant emails, downloading attachments, and extracting key data. Here's how it works: 21 | 22 | 1. **Sign up on GmailGenius** and link your Gmail account and Google Sheet 23 | 2. **Enter keywords** you want the AI agent to look for in your email 24 | 3. **GmailGenius finds emails and attachments** from Gmail that match your keyword criteria 25 | 4. **Useful information from the attachments is extracted and stored** in your linked Google Sheet. 26 | 27 | ### Under the hood, the AI agent divides the task into multiple steps and executes them: 28 | 29 | Screenshot 2024-09-02 at 12 53 53 AM 30 | 31 | 1. **Retrieves emails from Gmail** that match the keyword/phrase criteria. 32 | 2. **Downloads** the relevant attachments. 33 | 3. **Extracts useful attributes** from the email body & attachments. 34 | 4. **Stores** the extracted data in the linked Google Sheet. 35 | 36 | ## Tech Stack 37 | - Frontend: ReactJS, Vite, TailwindCSS 38 | - Backend: Python, FastAPI 39 | - AI Agent: CrewAI, Composio, OpenAI 40 | - Composio tools: [Gmail](https://app.composio.dev/app/gmail), [Google Sheets](https://app.composio.dev/app/googlesheets) 41 | 42 | ## Run Locally 43 | ### Setup tutorial 44 | [![gmailgenius-demo](https://github.com/user-attachments/assets/abb24495-d242-42f3-8cff-599182f735f4)](https://drive.google.com/file/d/1kC9oVSUatqQ6Tcs3u6CTsVsmczzG-F6k/preview) 45 | 46 | Clone the project 47 | 48 | ```bash 49 | git clone https://github.com/ComposioHQ/cookbook.git 50 | ``` 51 | 52 | Go to the project directory 53 | 54 | ```bash 55 | cd gmail-assistant/gmail-assistant-firebase 56 | ``` 57 | 58 | ### Backend 59 | 60 | Go to backend dir & run setup script, this will create a virtual environment & download necessary libraries (Note: if you're unable to execute then grant permisson -> chmod +x setup.sh) 61 | You'll then be prompted to login to **Composio**, link **Gmail** & **Google Sheets**. 62 | Add API keys in **.env file** 63 | 64 | ```bash 65 | cd backend && ./setup.sh 66 | ``` 67 | 68 | Start the server 69 | 70 | ```bash 71 | python main.py 72 | ``` 73 | 74 | Start the agent 75 | 76 | ```bash 77 | python agent.py 78 | ``` 79 | 80 | ### Frontend 81 | 82 | Install dependencies 83 | 84 | ```bash 85 | npm install 86 | ``` 87 | 88 | Start the server 89 | 90 | ```bash 91 | npm run dev 92 | ``` 93 | 94 | ### Composio Login 95 | If you're prompted to login & enter API key, run the below command to login 96 | 97 | ```bash 98 | composio login 99 | ``` 100 | 101 | You'll be redirected to composio website, login, get the API key and paste it 102 | 103 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | MODEL= 3 | NANO_API_KEY= 4 | NANO_URL= 5 | FRONTEND_URL= 6 | -------------------------------------------------------------------------------- /backend/agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import glob 4 | import json 5 | from composio.client.collections import TriggerEventData 6 | from composio_crewai import Action, ComposioToolSet 7 | from crewai import Agent, Crew, Task, Process 8 | from crewai_tools.tools.base_tool import BaseTool 9 | from langchain_openai import ChatOpenAI 10 | from dotenv import load_dotenv 11 | from typing import Any, Dict 12 | import requests 13 | from firebase.init import update_row 14 | from firebase.init import db 15 | # get_user_by_username 16 | from pathlib import Path 17 | 18 | load_dotenv() 19 | 20 | llm = ChatOpenAI(model="gpt-4o") 21 | 22 | 23 | class incrementCounter(BaseTool): 24 | name: str = "Increment Counter" 25 | description: str = "This tool increments the counter value for a user" 26 | 27 | def _run(self, uid: str, current_row: str) -> bool: 28 | new_row = int(current_row) + 1 29 | success = update_row(uid, str(new_row)) 30 | if success: 31 | return f"Counter incremented. New row value: {new_row}" 32 | else: 33 | return "Failed to increment counter" 34 | 35 | 36 | # # Get the attachment that was recently downloaded 37 | # def get_recent_attachment() -> str: 38 | # pdf_files = glob.glob(os.path.join("/Users/abhishekpatil/.composio/output/", "*.pdf")) # modify path as per need 39 | # if not pdf_files: 40 | # return None 41 | 42 | # most_recent_pdf = max(pdf_files, key=os.path.getctime) 43 | # return most_recent_pdf 44 | 45 | 46 | # Extract useful attributes from attachment 47 | class extractorTool(BaseTool): 48 | name: str = "ExtractorTool" 49 | description: str = "This tool extracts useful attributes from pdf document/attachments" 50 | 51 | def _run(self, file_path: str) -> Dict[str, Any]: 52 | url = os.environ.get("NANO_URL") 53 | FilePath = {'file': open(file_path, 'rb')} 54 | response = requests.post(url, 55 | auth=requests.auth.HTTPBasicAuth( 56 | os.environ.get("NANO_API_KEY"), ''), 57 | files=FilePath) 58 | return json.loads(response.text)["result"][0]["prediction"] 59 | 60 | 61 | # Trigger instance 62 | composio_toolset1 = ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY")) 63 | listener = composio_toolset1.create_trigger_listener() 64 | 65 | 66 | @listener.callback(filters={"trigger_name": "GMAIL_NEW_GMAIL_MESSAGE"}) 67 | def callback_new_message(event: TriggerEventData) -> None: 68 | print("Received new email") 69 | payload = event.payload 70 | 71 | def get_user_by_username(username): 72 | users_ref = db.collection('users') 73 | query = users_ref.where('username', '==', username).limit(1) 74 | docs = query.get() 75 | 76 | for doc in docs: 77 | return doc.to_dict() 78 | 79 | return False 80 | 81 | user = get_user_by_username(event.metadata.connection.clientUniqueUserId) 82 | keywords = user['sheetsConfig']['keywords'] 83 | attributes = user['sheetsConfig']['attributes'] 84 | sheetId = user['sheetsConfig']['spreadsheet_id'] 85 | sheetName = "Sheet1" 86 | row = user['sheetsConfig']['row'] 87 | uid = user['uid'] 88 | # Tools 89 | composio_toolset = ComposioToolSet( 90 | api_key=os.environ.get("COMPOSIO_API_KEY"), 91 | output_dir=Path.cwd() / "attachments", 92 | entity_id=event.metadata.connection.clientUniqueUserId) 93 | tools = composio_toolset.get_actions(actions=[ 94 | Action.GMAIL_GET_ATTACHMENT, Action.GOOGLESHEETS_BATCH_UPDATE 95 | ]) + [extractorTool(), incrementCounter()] 96 | 97 | # Agent 98 | google_assistant = Agent( 99 | role="Google Assistant", 100 | goal= 101 | "Process emails, check for keywords, download attachments, extract attributes, and store in Google Sheets", 102 | backstory= 103 | "You're an AI assistant that processes emails, extracts information from attachments, and stores data in Google Sheets.", 104 | verbose=True, 105 | llm=llm, 106 | tools=tools, 107 | allow_delegation=False, 108 | ) 109 | 110 | process_new_email = Task( 111 | description=f""" 112 | 1. Check if the email subject (subject) or body (messageText) in the payload contains any of these keywords: {keywords}, Payload: {payload}. 113 | 2. If keywords match, download the attachment using the GMAIL_GET_ATTACHMENT action. 114 | 3. Use the Extractor_tool, pass the file path of the downloaded attachment to extract content from it 115 | 4. From the extracted content, identify the following attributes: {attributes}. 116 | 5. Store the identified attributes in Google Sheet with ID {sheetId}, sheet name {sheetName}, in cell A{row}. Just add the attribute value and not the attribute name. Do not use includeValuesInResponse attribute. 117 | 6. Increment the row counter, if and only if you updated the Google Sheet otherwise do not increment the row counter. Use the IncrementCounter tool, pass the uid value: {uid} and current row value: {row} as arguments. 118 | """, 119 | agent=google_assistant, 120 | expected_output= 121 | "Summary of email processing, including whether keywords matched, attachment was processed, and data was stored in the Google Sheet.", 122 | ) 123 | 124 | email_processing_crew = Crew( 125 | agents=[google_assistant], 126 | tasks=[process_new_email], 127 | verbose=1, 128 | process=Process.sequential, 129 | ) 130 | result = email_processing_crew.kickoff() 131 | return result 132 | 133 | 134 | print("Email trigger listener activated!") 135 | listener.listen() 136 | -------------------------------------------------------------------------------- /backend/composio_config.py: -------------------------------------------------------------------------------- 1 | from composio import ComposioToolSet, App, Composio, Action 2 | from composio.client.exceptions import NoItemsFound 3 | from firebase.init import db 4 | import os 5 | from dotenv import load_dotenv 6 | load_dotenv() 7 | 8 | def update_gmail_trigger_status(ent_id: str) -> bool: 9 | users_ref = db.collection('users') 10 | query = users_ref.where('username', '==', ent_id).limit(1) 11 | docs = query.get() 12 | 13 | for doc in docs: 14 | try: 15 | doc.reference.update({'gmailTriggerEnabled': True}) 16 | return True 17 | except Exception as e: 18 | print(f"Error updating trigger status: {e}") 19 | return False 20 | 21 | print(f"User with entity id {ent_id} not found") 22 | return False 23 | 24 | 25 | def enable_gmail_trigger(ent_id: str): 26 | client = Composio() 27 | try: 28 | entity = client.get_entity(id=ent_id) 29 | trigger_config = {'userId': 'me', 'interval': 1, 'labelIds': 'INBOX'} 30 | trigger_name = 'GMAIL_NEW_GMAIL_MESSAGE' 31 | 32 | entity.enable_trigger(app=App.GMAIL, 33 | trigger_name=trigger_name, 34 | config=trigger_config) 35 | 36 | response = { 37 | "status": "success", 38 | "message": f"Trigger {trigger_name} enabled for {ent_id} on Gmail" 39 | } 40 | 41 | if update_gmail_trigger_status(ent_id): 42 | return response 43 | else: 44 | return { 45 | "status": 46 | "partial_success", 47 | "message": 48 | f"Trigger enabled, but failed to update user status for {ent_id}" 49 | } 50 | 51 | except Exception as e: 52 | return { 53 | "status": "error", 54 | "message": f"Failed to enable trigger: {str(e)}" 55 | } 56 | 57 | 58 | def isEntityConnected(ent_id: str, appType: str): 59 | toolset = ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"), 60 | entity_id=ent_id) 61 | entity = toolset.get_entity() 62 | app_enum = getattr(App, appType) 63 | try: 64 | entity.get_connection(app=app_enum) 65 | response = { 66 | "authenticated": "yes", 67 | "message": f"User {ent_id} is authenticated with {appType}", 68 | } 69 | return response 70 | except NoItemsFound as e: 71 | response = { 72 | "authenticated": "no", 73 | "message": f"User {ent_id} is not authenticated with {appType}", 74 | } 75 | return response 76 | 77 | 78 | def createNewEntity(ent_id: str, appType: str, redirectUrl: str): 79 | toolset = ComposioToolSet(api_key=os.environ.get("COMPOSIO_API_KEY"), 80 | entity_id=ent_id) 81 | entity = toolset.get_entity() 82 | app_enum = getattr(App, appType) 83 | try: 84 | entity.get_connection(app=app_enum) 85 | response = { 86 | "authenticated": "yes", 87 | "message": 88 | f"User {ent_id} is already authenticated with {appType}", 89 | "url": "" 90 | } 91 | return response 92 | 93 | except NoItemsFound as e: 94 | # Create a request to initiate connection 95 | request = entity.initiate_connection( 96 | app_enum, 97 | redirect_url=redirectUrl) 98 | response = { 99 | "authenticated": "no", 100 | "message": 101 | f"User {ent_id} is not yet authenticated with {appType}. Please authenticate.", 102 | "url": request.redirectUrl 103 | } 104 | return response 105 | # Poll until the connection is active 106 | connected_account = request.wait_until_active(client=toolset.client, 107 | timeout=100) 108 | 109 | -------------------------------------------------------------------------------- /backend/firebase/__pycache__/init.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishekpatil4/GmailGenius/208363fec5e7d75da21a3a35114adf0a68299d45/backend/firebase/__pycache__/init.cpython-312.pyc -------------------------------------------------------------------------------- /backend/firebase/init.py: -------------------------------------------------------------------------------- 1 | import firebase_admin 2 | from firebase_admin import credentials, auth, firestore 3 | from pathlib import Path 4 | import os 5 | from dotenv import load_dotenv 6 | 7 | load_dotenv() 8 | 9 | cred = credentials.Certificate(f"{Path.cwd()}/firebase/genius-57d8d-firebase-adminsdk-ue7u9-90656332c6.json") 10 | creds = { 11 | "type": os.environ.get("type"), 12 | "project_id": os.environ.get("project_id"), 13 | "private_key_id": os.environ.get("private_key_id"), 14 | "private_key": os.environ.get("private_key"), 15 | "client_email": os.environ.get("client_email"), 16 | "client_id": os.environ.get("client_id"), 17 | "auth_uri": os.environ.get("auth_uri"), 18 | "token_uri": os.environ.get("token_uri"), 19 | "auth_provider_x509_cert_url": 20 | os.environ.get("auth_provider_x509_cert_url"), 21 | "client_x509_cert_url": os.environ.get("client_x509_cert_url"), 22 | } 23 | # cred = credentials.Certificate(creds) 24 | firebase_admin.initialize_app(cred) 25 | 26 | db = firestore.client() 27 | 28 | # def get_user_by_username(username): 29 | # users_ref = db.collection('users') 30 | # query = users_ref.where('username', '==', username).limit(1) 31 | # docs = query.get() 32 | 33 | # for doc in docs: 34 | # return doc.to_dict() 35 | 36 | # return False 37 | 38 | 39 | def get_user_by_username(username): 40 | users_ref = db.collection('users') 41 | query = users_ref.where('uid', '==', username).limit(1) 42 | docs = query.get() 43 | 44 | for doc in docs: 45 | return doc.to_dict() 46 | 47 | return False 48 | 49 | 50 | def update_row(uid, new_row): 51 | users_ref = db.collection('users') 52 | query = users_ref.where('uid', '==', uid).limit(1) 53 | docs = query.get() 54 | 55 | for doc in docs: 56 | try: 57 | doc.reference.update({'sheetsConfig.row': str(new_row)}) 58 | return True 59 | except Exception as e: 60 | print(f"Error updating user row: {e}") 61 | return False 62 | 63 | print(f"User with uid {uid} not found") 64 | return False 65 | 66 | 67 | def update_spreadsheet_id(username: str, spreadsheet_id: str): 68 | users_ref = db.collection('users') 69 | query = users_ref.where('username', '==', username).limit(1) 70 | docs = query.get() 71 | 72 | for doc in docs: 73 | try: 74 | doc.reference.update( 75 | {'sheetsConfig.spreadsheet_id': spreadsheet_id}) 76 | print(f"Successfully updated spreadsheet_id for user {username}") 77 | return True 78 | except Exception as e: 79 | print(f"Error updating spreadsheet_id for user {username}: {e}") 80 | return False 81 | 82 | print(f"User {username} not found") 83 | return False 84 | -------------------------------------------------------------------------------- /backend/initialize_sheet_agent.py: -------------------------------------------------------------------------------- 1 | import json 2 | from composio_crewai import Action, ComposioToolSet 3 | from crewai import Agent, Crew, Task, Process 4 | from langchain_openai import ChatOpenAI 5 | from dotenv import load_dotenv 6 | from crewai_tools.tools.base_tool import BaseTool 7 | from firebase.init import db 8 | import os 9 | 10 | load_dotenv() 11 | llm = ChatOpenAI(model="gpt-4o") 12 | from firebase.init import update_spreadsheet_id 13 | # get_user_by_username 14 | 15 | 16 | def createSheet(entityId: str): 17 | username = entityId 18 | 19 | def get_user_by_username(username): 20 | users_ref = db.collection('users') 21 | query = users_ref.where('username', '==', username).limit(1) 22 | docs = query.get() 23 | 24 | for doc in docs: 25 | return doc.to_dict() 26 | 27 | return False 28 | 29 | user = get_user_by_username(entityId) 30 | keywords = user['sheetsConfig']['keywords'] 31 | attributes = user['sheetsConfig']['attributes'] 32 | sheetTitle = user['sheetsConfig']['sheetTitle'] 33 | email = user['email'] 34 | composio_toolset = ComposioToolSet( 35 | api_key=os.environ.get("COMPOSIO_API_KEY"), entity_id=entityId) 36 | 37 | class UpdateSpreadsheetId(BaseTool): 38 | name: str = "Update Spreadsheet ID in the database" 39 | description: str = "This tool updates the spreadsheet_id for a user in the database" 40 | 41 | def _run(self, username: str, spreadsheet_id: str) -> str: 42 | success = update_spreadsheet_id(username, spreadsheet_id) 43 | if success: 44 | return "Spreadsheet ID updated successfully" 45 | else: 46 | return "Failed to update Spreadsheet ID" 47 | 48 | update_spreadsheet_id_tool = UpdateSpreadsheetId() 49 | 50 | google_tools = composio_toolset.get_actions(actions=[ 51 | Action.GOOGLESHEETS_CREATE_GOOGLE_SHEET1, 52 | Action.GOOGLESHEETS_BATCH_UPDATE, 53 | Action.GMAIL_SEND_EMAIL 54 | ]) 55 | google_tools.append(update_spreadsheet_id_tool) 56 | 57 | google_assistant = Agent( 58 | role="Gmail Assistant", 59 | goal= """ 60 | 1. Create a Google Sheet and update it with provided values 61 | 2. Update the spreadsheet_id in the database. 62 | 3. Send an email to the user with specified subject and body. 63 | """, 64 | backstory= 65 | "You're an AI assistant that handles Google Sheets & Gmail operations using Google APIs.", 66 | verbose=True, 67 | llm=llm, 68 | tools=google_tools, 69 | allow_delegation=False, 70 | ) 71 | 72 | create_google_sheet = Task( 73 | description=f""" 74 | 1. Create a new Google Sheet titled '{sheetTitle}', batch update it with the following values starting in row 1 ie from A1: {attributes} (if more than one, then update in the next column ex: A2, A3...), 75 | 2. Do not use includeValuesInResponse attribute. 76 | 3. Update the spreadsheet_id in the database for user with username: {username} 77 | 4. Send an email with recipient_email as: {email}, with subject {keywords} and body with dummy values for the following attributes: {attributes}. dont specify anything else apart from recipient_email, subject, body while sending email. 78 | """, 79 | agent=google_assistant, 80 | expected_output= 81 | "Create sheet, store values, update spreadsheet_id in the database, send an email to the user", 82 | ) 83 | 84 | gmail_processing_crew = Crew( 85 | agents=[google_assistant], 86 | tasks=[create_google_sheet], 87 | verbose=1, 88 | process=Process.sequential, 89 | ) 90 | return gmail_processing_crew.kickoff() 91 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException, Request, Depends 2 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 3 | from pydantic import BaseModel 4 | from fastapi.middleware.cors import CORSMiddleware 5 | from firebase.init import auth 6 | from composio_config import createNewEntity, isEntityConnected, enable_gmail_trigger 7 | import logging 8 | from initialize_sheet_agent import createSheet 9 | 10 | logging.basicConfig(level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | app = FastAPI() 14 | 15 | app.add_middleware( 16 | CORSMiddleware, 17 | allow_origins=["*" 18 | ], # Allows all origins, can be a list of specific origins 19 | allow_credentials=True, 20 | allow_methods=["*"], # Allows all methods (GET, POST, etc.) 21 | allow_headers=["*"], # Allows all headers 22 | ) 23 | 24 | 25 | def verify_token(auth_credentials: HTTPAuthorizationCredentials = Depends( 26 | HTTPBearer())): 27 | token = auth_credentials.credentials 28 | try: 29 | decoded_token = auth.verify_id_token(token) 30 | return decoded_token 31 | except Exception: 32 | raise HTTPException(status_code=401, detail="Invalid or expired token") 33 | 34 | 35 | # Pydantic model for the request body 36 | class UserData(BaseModel): 37 | username: str 38 | appType: str 39 | 40 | class NewEntityData(BaseModel): 41 | username: str 42 | appType: str 43 | redirectUrl: str 44 | 45 | @app.post("/newentity") 46 | async def handle_request(user_data: NewEntityData, 47 | decoded_token: dict = Depends(verify_token)): 48 | user_id = decoded_token['uid'] 49 | username = user_data.username 50 | appType = user_data.appType 51 | redirectUrl = user_data.redirectUrl 52 | res = createNewEntity(username, appType, redirectUrl) 53 | return res 54 | 55 | 56 | class EnableTriggerData(BaseModel): 57 | username: str 58 | 59 | 60 | @app.post("/enabletrigger") 61 | async def handle_request(user_data: EnableTriggerData, 62 | decoded_token: dict = Depends(verify_token)): 63 | user_id = decoded_token['uid'] 64 | username = user_data.username 65 | res = enable_gmail_trigger(username) 66 | return res 67 | 68 | 69 | @app.post("/checkconnection") 70 | async def handle_request(user_data: UserData, 71 | decoded_token: dict = Depends(verify_token)): 72 | user_id = decoded_token['uid'] 73 | username = user_data.username 74 | appType = user_data.appType 75 | res = isEntityConnected(username, appType) 76 | return res 77 | 78 | 79 | class UserData(BaseModel): 80 | username: str 81 | 82 | 83 | @app.post("/createsheet") 84 | async def handle_request(user_data: UserData, 85 | decoded_token: dict = Depends(verify_token)): 86 | username = user_data.username 87 | res = createSheet(username) 88 | return res 89 | 90 | 91 | @app.get("/") 92 | async def handle_request(): 93 | return "ok" 94 | 95 | 96 | if __name__ == "__main__": 97 | import uvicorn 98 | uvicorn.run(app, host="0.0.0.0", port=8000) 99 | 100 | # Start the server (if running locally) 101 | # Run the following command in your terminal: uvicorn main:app --reload 102 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | crewai 2 | composio-crewai 3 | langchain-openai 4 | python-dotenv 5 | crewai_tools 6 | fastapi 7 | uvicorn 8 | langchain_google_genai 9 | 10 | -------------------------------------------------------------------------------- /backend/setup.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | # Create a virtual environment 5 | echo "Creating virtual environment..." 6 | python3 -m venv ~/.venvs/gmail_agent 7 | 8 | # Activate the virtual environment 9 | echo "Activating virtual environment..." 10 | source ~/.venvs/gmail_agent/bin/activate 11 | 12 | # Install libraries from requirements.txt 13 | echo "Installing libraries from requirements.txt..." 14 | pip install -r requirements.txt 15 | 16 | # Login to your account 17 | echo "Login to your Composio acount" 18 | composio login 19 | 20 | # Add calendar tool 21 | echo "Add Gmail tools. Finish the flow" 22 | composio add gmail 23 | composio add googlesheets 24 | 25 | # Copy env backup to .env file 26 | if [ -f ".env.example" ]; then 27 | echo "Copying .env.example to .env..." 28 | cp .env.example .env 29 | else 30 | echo "No .env.example file found. Creating a new .env file..." 31 | touch .env 32 | fi 33 | 34 | # Prompt user to fill the .env file 35 | echo "Please fill in the .env file with the necessary environment variables." 36 | 37 | echo "Setup completed successfully!" 38 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | GmailGenius 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gmailgenius", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.5", 14 | "firebase": "^10.13.0", 15 | "flowbite": "^2.5.1", 16 | "flowbite-react": "^0.10.1", 17 | "lucide-react": "^0.436.0", 18 | "notistack": "^3.0.1", 19 | "react": "^18.3.1", 20 | "react-dom": "^18.3.1", 21 | "react-loader-spinner": "^6.1.6" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.9.0", 25 | "@types/react": "^18.3.3", 26 | "@types/react-dom": "^18.3.0", 27 | "@vitejs/plugin-react": "^4.3.1", 28 | "autoprefixer": "^10.4.20", 29 | "eslint": "^9.9.0", 30 | "eslint-plugin-react": "^7.35.0", 31 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 32 | "eslint-plugin-react-refresh": "^0.4.9", 33 | "globals": "^15.9.0", 34 | "postcss": "^8.4.42", 35 | "react-router-dom": "^6.26.1", 36 | "tailwindcss": "^3.4.10", 37 | "vite": "^5.4.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; 2 | import { onAuthStateChanged } from "firebase/auth"; 3 | import { auth } from "./config/firebase"; 4 | import Navbar from "./components/Navbar"; 5 | import Home from "./pages/Home"; 6 | import Footer from "./components/Footer"; 7 | // import Dashboard from "./pages/Dashboard"; 8 | import ScrollToTop from "./components/ScrollToTop"; 9 | import { useState, useEffect } from "react"; 10 | import Login from "./pages/Login"; 11 | import Settings from "./pages/Settings"; 12 | import Agent from "./pages/Agent"; 13 | import NotFound from "./pages/NotFound"; 14 | import SkeletonLoader from "./components/SkeletonLoader"; 15 | import { SnackbarProvider } from 'notistack' 16 | 17 | const ProtectedRoute = ({ user, children }) => { 18 | if (!user) { 19 | return ; 20 | } 21 | return children; 22 | }; 23 | 24 | const App = () => { 25 | const [user, setUser] = useState(null); 26 | const [loading, setLoading] = useState(true); 27 | 28 | useEffect(() => { 29 | const unsubscribe = onAuthStateChanged(auth, (user) => { 30 | setUser(user); 31 | setLoading(false); 32 | }); 33 | 34 | return () => unsubscribe(); 35 | }, []); 36 | 37 | if (loading) { 38 | return 39 | } 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | } /> 48 | {/* 50 | 51 | 52 | } /> */} 53 | 55 | 56 | 57 | } /> 58 | {/* 62 | 63 | 64 | } 65 | /> */} 66 | } /> 67 | } /> 68 | 69 |