├── .env ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── demo.mp4 └── logo.png ├── requirements.txt └── src ├── app.py └── utils ├── attachment.py ├── tool.py └── utils.py /.env: -------------------------------------------------------------------------------- 1 | GROQ_API_KEY=your_groq_api_key -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build and Release Folders 2 | bin-debug/ 3 | bin-release/ 4 | [Oo]bj/ 5 | [Bb]in/ 6 | 7 | # Other files and folders 8 | .settings/ 9 | 10 | # Executables 11 | *.swf 12 | *.air 13 | *.ipa 14 | *.apk 15 | 16 | # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` 17 | # should NOT be excluded as they contain compiler settings and other important 18 | # information for Eclipse / Flash Builder. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | InboxHero Logo 4 |

5 | 6 | # InboxHero ✉️ 7 | 8 | **InboxHero** is a smart email prioritizer and Gmail assistant built with **Streamlit**, **Langchain**, and **ChatGroq**. It helps you quickly identify the most important emails in your inbox, detect those that need a reply, and even generate draft responses—all in one sleek, professional workspace. 9 | 10 | --- 11 | 12 | ## Demo Video - InboxHero 🎥 13 | 14 | https://github.com/user-attachments/assets/8e79293d-2659-4a83-bdfc-bbc185120fdf 15 | 16 | --- 17 | 18 | ## Features 🚀 19 | 20 | - **Email Prioritization:** 21 | - Automatically fetches your Gmail inbox and filters out promotional emails. 22 | - Uses a custom ranking prompt with a language model to score emails from 1 (least important) to 10 (extremely important). 23 | 24 | - **Reply Detection & Draft Generation:** 25 | - Detects emails that require a reply and displays them in a dedicated section. 26 | - Offers an interactive "Generate Draft" button to quickly produce draft responses. 27 | 28 | - **Content Summarization:** 29 | - Summarizes the email body using **ChatGroq** and **Langchain**, ensuring a concise and clear overview. 30 | - Cleans and organizes summaries for a crisp, readable display. 31 | 32 | - **Microsoft Attachments Support:** 33 | - Reads and summarizes various Microsoft attachments such as PDFs, DOCX, Excel sheets, and more. 34 | - Presents attachment summaries using beautiful Markdown formatting for a professional look. 35 | 36 | - **Interactive Chat Mode:** 37 | - Engage with your inbox through a conversational chat interface. 38 | - Ask queries and receive real-time insights about your emails. 39 | 40 | - **Customizable Time Frame:** 41 | - Choose from multiple time windows (e.g., 1 Hour, 6 Hours, 24 Hours, etc.) to filter emails based on recency. 42 | 43 | - **Seamless Integration:** 44 | - Powered by **Langchain**🦜 for advanced prompt management and natural language processing. 45 | - Utilizes robust Python libraries like Streamlit, simplegmail, and python-dotenv for a smooth user experience. 46 | 47 | --- 48 | 49 | ## Installation & Setup 🔧 50 | 51 | **Clone the Repository:** 52 | ```bash 53 | git clone https://github.com/zamalali/InboxHero.git 54 | cd InboxHero 55 | ``` 56 | 57 | ### Install Dependencies: 58 | 59 | ```bash 60 | pip install -r requirements.txt 61 | ``` 62 | ## 📌 Get Your LangChain Groq API Key 63 | 64 | To use LangChain with Groq, you need an API key. Follow these steps: 65 | 66 | 1. **Go to the Groq Console**: [Click here to get your API key](https://console.groq.com/playground) 67 | 2. **Sign in or Sign up** if you haven't already. 68 | 3. **Generate an API key** and copy it. 69 | 4. **Set up the key in your environment**: 70 | - If running locally, add it to your `.env` file: 71 | ```ini 72 | GROQ_API_KEY=your_api_key_here 73 | ``` 74 | - If deploying to a cloud service, add it to **your environment variables or repository secrets**. 75 | 76 | ✅ Now, you're all set to use Groq with LangChain! 🚀 77 | 78 | 79 | ## 📌 Get Your Gmail Client Secret JSON File 80 | 81 | To connect to your Gmail account, you need a **Client Secret JSON file**. Follow these steps: 82 | 83 | 1. **Go to Google API Console**: [Follow this guide to download your client secret file](https://stackoverflow.com/questions/52200589/where-to-download-your-client-secret-file-json-file#:~:text=Go%20to%20your%20Google%20API%20Console%20where%20you%27ll,arrow%20on%20the%20farthest%20right%20of%20the%20page%3A) 84 | 2. **Enable the Gmail API** for your Google Cloud project. 85 | 3. **Download the `client_secret.json` file** from the Credentials section. 86 | 4. **Upload the file when you run the streamlit app**. 87 | 88 | ✅ Now, you're ready to authenticate and interact with Gmail in your app! ✉️ 89 | 90 | 91 | ### Contributing 🤝 92 | Contributions are welcome! If you’d like to improve InboxHero or add new features, please fork the repository and submit a pull request. 93 | 94 | ### License 📄 95 | This project is licensed under the Apache 2.0 License. 96 | -------------------------------------------------------------------------------- /assets/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zamalali/InboxHero/9bc875cebe03fdb2b62e05d4e6e9ed938c1a5a6a/assets/demo.mp4 -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zamalali/InboxHero/9bc875cebe03fdb2b62e05d4e6e9ed938c1a5a6a/assets/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit==1.24.0 2 | pandas 3 | python-dateutil 4 | python-dotenv 5 | simplegmail 6 | langchain-groq==0.2.4 7 | langchain==0.3.15 8 | markitdown 9 | Pillow<10 --only-binary=:all: 10 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import re 4 | import json 5 | import html # For escaping HTML special characters 6 | import uuid 7 | from datetime import datetime, timedelta 8 | from dateutil.parser import parse as parse_date 9 | from dateutil.tz import tzlocal, gettz 10 | import streamlit as st 11 | import pandas as pd 12 | import streamlit.components.v1 as components 13 | 14 | from simplegmail import Gmail 15 | from langchain_groq import ChatGroq 16 | from langchain.prompts.chat import ChatPromptTemplate 17 | 18 | # Import our utility functions and GmailChat class (from your other modules) 19 | from utils.utils import generate_and_save_draft 20 | from utils.tool import GmailChat 21 | from dotenv import load_dotenv 22 | # **Import the attachments summarizer module** 23 | from utils.attachment import GmailAttachmentSummarizer 24 | load_dotenv() 25 | # Load any existing environment variables 26 | GROQ_API_KEY = os.getenv("GROQ_API_KEY") 27 | # (Any other secrets can be loaded similarly) 28 | 29 | # -------------------------------------------------- 30 | # Create a session-specific ID (if not already set) 31 | # -------------------------------------------------- 32 | if "session_id" not in st.session_state: 33 | st.session_state.session_id = uuid.uuid4().hex 34 | # Session-specific filename for client secret 35 | client_file = f"client_secret_{st.session_state.session_id}.json" 36 | 37 | # -------------------------------------------------- 38 | # Credentials Upload & Validation (Gmail client secret only) 39 | # -------------------------------------------------- 40 | if "client_secret" not in st.session_state: 41 | with st.form(key="credentials_form"): 42 | st.markdown( 43 | """ 44 | 72 |
73 |

Welcome to InboxHero ✉️

74 |

Please upload your Gmail client secret JSON file below.

75 |

If you don't have one, refer to this guide.

76 |
77 | """, 78 | unsafe_allow_html=True, 79 | ) 80 | uploaded_file = st.file_uploader("Upload Gmail Client Secret JSON", type=["json"]) 81 | submitted = st.form_submit_button("Submit Credentials") 82 | 83 | if submitted: 84 | if not uploaded_file: 85 | st.error("Please upload your Gmail client secret JSON file.") 86 | else: 87 | try: 88 | client_secret_data = json.load(uploaded_file) 89 | # Validate structure: check for "installed" or "web" keys 90 | if "installed" in client_secret_data or "web" in client_secret_data: 91 | st.session_state.client_secret = client_secret_data 92 | # Write the uploaded client secret to a session-specific file 93 | with open(client_file, "w") as f: 94 | json.dump(client_secret_data, f) 95 | st.success("Client secret file uploaded and validated successfully.") 96 | st.experimental_rerun() 97 | else: 98 | st.error("Invalid client secret file format. Please upload a valid Gmail client secret JSON file.") 99 | except Exception as e: 100 | st.error(f"Error processing credentials: {e}") 101 | st.stop() # Halt further execution until valid credentials are provided 102 | 103 | # ------------------------------- 104 | # Session State Initialization 105 | # ------------------------------- 106 | if "result" not in st.session_state: 107 | st.session_state.result = None 108 | if "reply_email_objects" not in st.session_state: 109 | st.session_state.reply_email_objects = None 110 | if "generated_drafts" not in st.session_state: 111 | st.session_state.generated_drafts = {} 112 | if "email_summaries" not in st.session_state: 113 | st.session_state.email_summaries = {} 114 | if "mode" not in st.session_state: 115 | st.session_state.mode = "home" # "home" or "chat" 116 | if "chat_history" not in st.session_state: 117 | st.session_state.chat_history = [] # List of dicts: {"role": "user"/"assistant", "text": ...} 118 | 119 | # ------------------------------------ 120 | # Helper: Format date to CET with AM/PM (no seconds) 121 | # ------------------------------------ 122 | def format_date(date_str): 123 | try: 124 | dt = parse_date(date_str) 125 | cet_tz = gettz("Europe/Berlin") 126 | dt_cet = dt.astimezone(cet_tz) 127 | return dt_cet.strftime("%Y-%m-%d %I:%M%p") 128 | except Exception as e: 129 | st.error(f"Error formatting date: {e}") 130 | return date_str 131 | 132 | # ------------------------------------ 133 | # Helper: Summarize Email Content + Attachments 134 | # ------------------------------------ 135 | def summarize_email(email): 136 | """ 137 | Summarizes the email body using ChatGroq. If the email has attachments, 138 | it calls the GmailAttachmentSummarizer to generate attachment summaries. 139 | The final summary is a combination of the text summary and the attachment summary. 140 | """ 141 | email_body = getattr(email, 'plain', None) or getattr(email, 'snippet', "") 142 | if email_body: 143 | if len(email_body) > 2000: 144 | email_body = email_body[:2000] + "..." 145 | llm = ChatGroq( 146 | model="mixtral-8x7b-32768", 147 | temperature=0.7, 148 | max_tokens=75, 149 | timeout=30, 150 | max_retries=2, 151 | ) 152 | summary_prompt = ChatPromptTemplate.from_messages([ 153 | ( 154 | "system", 155 | ( 156 | "You are Zamal Babar’s professional email assistant. Summarize the following email content concisely in 2-3 lines in English only. " 157 | "Do not use any German language. Return only the summary." 158 | ) 159 | ), 160 | ( 161 | "human", 162 | "Email Content:\n{body}" 163 | ) 164 | ]) 165 | prompt_inputs = {"body": email_body} 166 | try: 167 | messages_formatted = summary_prompt.format_messages(**prompt_inputs) 168 | response_obj = llm.invoke(messages_formatted) 169 | text_summary = response_obj.content.strip() 170 | except Exception as e: 171 | text_summary = f"Error summarizing text: {e}" 172 | else: 173 | text_summary = "" 174 | 175 | if hasattr(email, 'attachments') and email.attachments: 176 | att_summarizer = GmailAttachmentSummarizer(time_frame_hours=4) 177 | att_summaries = [] 178 | for att in email.attachments: 179 | try: 180 | att_summary = att_summarizer.summarize_attachment(att) 181 | att_summaries.append(att_summary) 182 | except Exception as e: 183 | att_summaries.append(f"Error summarizing attachment: {e}") 184 | attachments_summary = " ".join(att_summaries) 185 | combined_summary = f"{text_summary} \n\n Attachments Summary: {attachments_summary}" 186 | full_summary = f"This email contains attachments. {combined_summary}" 187 | else: 188 | full_summary = text_summary 189 | 190 | return full_summary 191 | 192 | # ------------------------------------ 193 | # Email Prioritizer Function 194 | # ------------------------------------ 195 | def email_prioritizer(time_frame_hours: int = 24): 196 | try: 197 | gmail = Gmail(client_secret_file=client_file) 198 | except Exception as e: 199 | st.error(f"Error initializing Gmail: {e}") 200 | return json.dumps({"top_important_emails": [], "reply_needed_emails": []}, indent=4), [] 201 | 202 | time_threshold = datetime.now(tz=tzlocal()) - timedelta(hours=time_frame_hours) 203 | date_query = time_threshold.strftime("%Y/%m/%d") 204 | query = f"in:inbox -category:promotions after:{date_query}" 205 | 206 | try: 207 | messages = gmail.get_messages(query=query) 208 | except Exception as e: 209 | st.error(f"Error fetching messages: {e}") 210 | return json.dumps({"top_important_emails": [], "reply_needed_emails": []}, indent=4), [] 211 | 212 | if not messages: 213 | result = {"top_important_emails": [], "reply_needed_emails": []} 214 | return json.dumps(result, indent=4), [] 215 | 216 | recent_messages = [] 217 | for msg in messages: 218 | try: 219 | msg_dt = parse_date(msg.date) 220 | except Exception as e: 221 | st.error(f"Could not parse date for message with subject '{msg.subject}': {e}") 222 | continue 223 | if msg_dt >= time_threshold: 224 | recent_messages.append(msg) 225 | st.write(f"**Found {len(recent_messages)} messages** from the past {time_frame_hours} hours.") 226 | 227 | if not recent_messages: 228 | result = {"top_important_emails": [], "reply_needed_emails": []} 229 | return json.dumps(result, indent=4), [] 230 | 231 | filtered_messages = [] 232 | marketing_keywords = ["marketing", "discount", "sale", "offer", "deal"] 233 | for msg in recent_messages: 234 | lower_subject = msg.subject.lower() if hasattr(msg, 'subject') else "" 235 | lower_sender = msg.sender.lower() if hasattr(msg, 'sender') else "" 236 | if any(keyword in lower_subject for keyword in marketing_keywords) or any(keyword in lower_sender for keyword in marketing_keywords): 237 | continue 238 | filtered_messages.append(msg) 239 | 240 | if not filtered_messages: 241 | result = {"top_important_emails": [], "reply_needed_emails": []} 242 | return json.dumps(result, indent=4), [] 243 | 244 | llm = ChatGroq( 245 | model="mixtral-8x7b-32768", 246 | temperature=0, 247 | max_tokens=50, 248 | timeout=60, 249 | max_retries=2, 250 | ) 251 | ranking_prompt = ChatPromptTemplate.from_messages([ 252 | ( 253 | "system", 254 | ( 255 | "You are an intelligent email assistant specialized in evaluating email urgency and importance. " 256 | "Score the following email on a scale from 1 to 10, where 10 means extremely important and urgent, and 1 means not important at all. " 257 | "Return only a single numerical score with no additional text." 258 | ) 259 | ), 260 | ( 261 | "human", 262 | "Email subject: {subject}\nEmail received on: {date}\nEmail body: {body}" 263 | ) 264 | ]) 265 | reply_prompt = ChatPromptTemplate.from_messages([ 266 | ( 267 | "system", 268 | ( 269 | "You are an intelligent email assistant that determines whether an email requires a reply. " 270 | "Make sure you consider the email's content, tone, and sender details. Answer 'Yes' only when it is truly necessary. " 271 | "Return only 'Yes' or 'No'." 272 | ) 273 | ), 274 | ( 275 | "human", 276 | "Email subject: {subject}\nEmail sender: {sender}\nEmail received on: {date}\nEmail body: {body}" 277 | ) 278 | ]) 279 | 280 | top_emails_list = [] 281 | reply_emails_list = [] 282 | max_body_length = 500 283 | for email in filtered_messages: 284 | if hasattr(email, 'plain') and email.plain: 285 | email_body = email.plain 286 | else: 287 | email_body = email.snippet if hasattr(email, 'snippet') else "" 288 | if len(email_body) > max_body_length: 289 | email_body = email_body[:max_body_length] + "..." 290 | 291 | if "noreply" in email.sender.lower() or "no-reply" in email.sender.lower(): 292 | reply_needed = False 293 | else: 294 | reply_inputs = {"subject": email.subject, "sender": email.sender, "date": email.date, "body": email_body} 295 | try: 296 | messages_reply = reply_prompt.format_messages(**reply_inputs) 297 | reply_response_obj = llm.invoke(messages_reply) 298 | reply_response_text = reply_response_obj.content.strip().lower() 299 | reply_needed = (reply_response_text == "yes") 300 | except Exception as e: 301 | st.error(f"Error checking reply need for email '{email.subject}': {e}") 302 | reply_needed = False 303 | 304 | if reply_needed: 305 | reply_emails_list.append(email) 306 | else: 307 | rank_inputs = {"subject": email.subject, "date": email.date, "body": email_body} 308 | try: 309 | messages_formatted = ranking_prompt.format_messages(**rank_inputs) 310 | response_obj = llm.invoke(messages_formatted) 311 | response_text = response_obj.content.strip() 312 | score_match = re.search(r'\d+(\.\d+)?', response_text) 313 | score = float(score_match.group()) if score_match else 0.0 314 | except Exception as e: 315 | st.error(f"Error processing email with subject '{email.subject}' for ranking: {e}") 316 | score = 0.0 317 | top_emails_list.append((email, score)) 318 | 319 | time.sleep(1) 320 | 321 | sorted_emails = sorted(top_emails_list, key=lambda x: x[1], reverse=True) 322 | top_emails = sorted_emails[:5] 323 | 324 | top_emails_output = [] 325 | for email, score in top_emails: 326 | email_summary = summarize_email(email) 327 | clean_summary = html.escape(email_summary.replace("\n", " ").strip()) 328 | email_info = { 329 | "Sender": email.sender, 330 | "Summary": clean_summary, 331 | "Date": format_date(email.date), 332 | "Importance Score": score 333 | } 334 | top_emails_output.append(email_info) 335 | 336 | reply_needed_output = [] 337 | for idx, email in enumerate(reply_emails_list, start=1): 338 | summary_key = f"summary_{idx}" 339 | if summary_key not in st.session_state.email_summaries: 340 | st.session_state.email_summaries[summary_key] = summarize_email(email) 341 | email_info = { 342 | "Sender": email.sender, 343 | "Summary": st.session_state.email_summaries[summary_key], 344 | "Date": format_date(email.date), 345 | } 346 | reply_needed_output.append(email_info) 347 | 348 | result = {"top_important_emails": top_emails_output, "reply_needed_emails": reply_needed_output} 349 | return json.dumps(result, indent=4), reply_emails_list 350 | 351 | # ------------------------------------ 352 | # Helper: Render HTML table with custom styling for Top Emails 353 | # ------------------------------------ 354 | def render_table(df: pd.DataFrame) -> str: 355 | html_table = df.to_html(index=False, classes="custom-table", border=0) 356 | style = """ 357 | 376 | """ 377 | return style + html_table 378 | 379 | # ------------------------------------ 380 | # Main App Interface: InboxHero 381 | # ------------------------------------ 382 | st.set_page_config( 383 | page_title="InboxHero", 384 | page_icon="✉️", 385 | layout="wide", 386 | initial_sidebar_state="expanded", 387 | ) 388 | 389 | st.markdown( 390 | """ 391 | 416 | """, 417 | unsafe_allow_html=True, 418 | ) 419 | 420 | # Sidebar Mode Toggle: Chat Inbox vs Home. 421 | if "mode" not in st.session_state: 422 | st.session_state.mode = "home" 423 | if "chat_history" not in st.session_state: 424 | st.session_state.chat_history = [] 425 | 426 | if st.session_state.mode == "home": 427 | st.sidebar.caption("Click below to chat with your inbox") 428 | if st.sidebar.button("Chat Inbox"): 429 | st.session_state.mode = "chat" 430 | else: 431 | if st.sidebar.button("Back to Home"): 432 | st.session_state.mode = "home" 433 | 434 | # Main UI: Chat Mode or Home Mode. 435 | if st.session_state.mode == "chat": 436 | st.markdown("

Chat Inbox

", unsafe_allow_html=True) 437 | chat_query = st.text_input("Enter your query:", key="chat_query", help="Type your query here.", placeholder="Enter your query here...", label_visibility="visible") 438 | if st.button("Send Query"): 439 | chat_instance = GmailChat(time_frame_hours=24) 440 | answer = chat_instance.chat(chat_query) 441 | st.session_state.chat_history.append({"role": "user", "text": chat_query}) 442 | st.session_state.chat_history.append({"role": "assistant", "text": answer}) 443 | st.markdown("

Chat History

", unsafe_allow_html=True) 444 | chat_container = st.container() 445 | for msg in st.session_state.chat_history: 446 | if msg["role"] == "user": 447 | st.markdown(f"

You: {msg['text']}

", unsafe_allow_html=True) 448 | else: 449 | st.markdown(f"

InboxHero: {msg['text']}

", unsafe_allow_html=True) 450 | else: 451 | st.markdown("

InboxHero ✉️

", unsafe_allow_html=True) 452 | st.markdown("

Instant Email Prioritizer

", unsafe_allow_html=True) 453 | st.markdown( 454 | """ 455 |

456 | InboxHero helps you quickly identify the most important emails in your inbox along with those that require a reply. 457 | Use the sidebar to choose a time window (e.g., 1 Hour, 6 Hours, 12 Hours, 24 Hours, 3 Days, 1 Week, or 2 Weeks) to filter your emails. 458 | Then, click Fetch Emails to see a clean, organized list of your top important emails and interactive rows for emails needing a reply. 459 |

460 | """, 461 | unsafe_allow_html=True 462 | ) 463 | 464 | time_option = st.sidebar.selectbox( 465 | "Select Timeframe:", 466 | options=["1 Hour", "6 Hours", "12 Hours", "24 Hours", "3 Days", "1 Week", "2 Weeks"], 467 | index=3, 468 | help="Choose how old emails should be considered." 469 | ) 470 | time_mapping = { 471 | "1 Hour": 1, 472 | "6 Hours": 6, 473 | "12 Hours": 12, 474 | "24 Hours": 24, 475 | "3 Days": 72, 476 | "1 Week": 168, 477 | "2 Weeks": 336 478 | } 479 | time_frame_hours = time_mapping[time_option] 480 | 481 | st.sidebar.markdown("---") 482 | st.sidebar.markdown( 483 | """ 484 | **Instructions:** 485 | - Select the desired timeframe from the dropdown. 486 | - Click **Fetch Emails** to prioritize your inbox. 487 | - The top important emails will be shown in a styled table. 488 | - Emails requiring a reply are listed with interactive rows and a "Generate Draft" button. 489 | """ 490 | ) 491 | 492 | if st.button("Fetch Emails"): 493 | with st.spinner("Fetching and prioritizing emails... Please wait..."): 494 | result_json, reply_email_objects = email_prioritizer(time_frame_hours=time_frame_hours) 495 | st.session_state.result = json.loads(result_json) 496 | st.session_state.reply_email_objects = reply_email_objects 497 | 498 | if st.session_state.result: 499 | result = st.session_state.result 500 | st.markdown("

Top Important Emails

", unsafe_allow_html=True) 501 | if result["top_important_emails"]: 502 | df_top = pd.DataFrame(result["top_important_emails"]) 503 | components.html(render_table(df_top), height=300, scrolling=True) 504 | else: 505 | st.markdown("

No top important emails found.

", unsafe_allow_html=True) 506 | 507 | st.markdown("

Emails Requiring a Reply

", unsafe_allow_html=True) 508 | if st.session_state.reply_email_objects: 509 | for idx, email in enumerate(st.session_state.reply_email_objects, start=1): 510 | with st.container(): 511 | st.markdown( 512 | """ 513 |
514 | """, 515 | unsafe_allow_html=True, 516 | ) 517 | cols = st.columns([3, 5, 3, 2]) 518 | cols[0].markdown(f"Sender: {email.sender}", unsafe_allow_html=True) 519 | summary_key = f"summary_{idx}" 520 | if summary_key not in st.session_state.email_summaries: 521 | st.session_state.email_summaries[summary_key] = summarize_email(email) 522 | cols[1].markdown(f"Summary: {html.escape(st.session_state.email_summaries[summary_key])}", unsafe_allow_html=True) 523 | cols[2].markdown(f"Date: {format_date(email.date)}", unsafe_allow_html=True) 524 | draft_key = f"draft_{idx}" 525 | draft_placeholder = cols[3].empty() 526 | if draft_key in st.session_state.generated_drafts: 527 | draft_placeholder.markdown("Draft Generated", unsafe_allow_html=True) 528 | else: 529 | if draft_placeholder.button("Generate Draft", key=draft_key): 530 | with st.spinner("Generating draft..."): 531 | draft_text = generate_and_save_draft(email) 532 | st.session_state.generated_drafts[draft_key] = draft_text 533 | draft_placeholder.markdown("Draft Generated", unsafe_allow_html=True) 534 | st.markdown("
", unsafe_allow_html=True) 535 | else: 536 | st.markdown("

No emails requiring a reply found.

", unsafe_allow_html=True) 537 | 538 | if st.session_state.generated_drafts: 539 | for key, draft in st.session_state.generated_drafts.items(): 540 | if draft and draft != "loading": 541 | st.success(f"Draft created successfully for {key}!") 542 | elif draft == "loading": 543 | st.info(f"Generating draft for {key}...") 544 | -------------------------------------------------------------------------------- /src/utils/attachment.py: -------------------------------------------------------------------------------- 1 | # gmail_attachment_summarizer.py 2 | 3 | import os 4 | import tempfile 5 | import warnings 6 | from datetime import datetime, timedelta 7 | from dateutil.parser import parse as parse_date 8 | from dateutil.tz import tzlocal 9 | from dotenv import load_dotenv 10 | 11 | from simplegmail import Gmail 12 | from markitdown import MarkItDown 13 | from langchain_groq import ChatGroq 14 | from langchain.prompts.chat import ChatPromptTemplate 15 | 16 | # Suppress resource and deprecation warnings. 17 | warnings.filterwarnings("ignore", category=ResourceWarning) 18 | warnings.filterwarnings("ignore", category=DeprecationWarning) 19 | 20 | # Load environment variables (e.g., GROQ_API_KEY) from GitHub repository secrets. 21 | load_dotenv() 22 | GROQ_API_KEY = os.getenv("GROQ_API_KEY") 23 | 24 | class GmailAttachmentSummarizer: 25 | """ 26 | A class to fetch Gmail emails with attachments and generate concise summaries 27 | of both the email content and each attachment using MarkItDown and ChatGroq. 28 | """ 29 | 30 | def __init__(self, time_frame_hours: int = 4): 31 | """ 32 | Initialize the summarizer with a specified time frame (in hours) and instantiate the clients. 33 | :param time_frame_hours: The number of past hours to search for emails. 34 | """ 35 | self.time_frame_hours = time_frame_hours 36 | self.gmail = Gmail() # Uses your preconfigured Gmail secret file. 37 | self.markdown_converter = MarkItDown() 38 | self.llm = ChatGroq( 39 | model="mixtral-8x7b-32768", 40 | temperature=0, 41 | max_tokens=300, 42 | timeout=60, 43 | max_retries=2, 44 | api_key=GROQ_API_KEY # Now using the repository secret. 45 | ) 46 | 47 | def fetch_emails_with_attachments(self): 48 | """ 49 | Fetch emails from the inbox within the past time frame that contain attachments. 50 | :return: List of email objects with attachments. 51 | """ 52 | time_threshold = datetime.now(tz=tzlocal()) - timedelta(hours=self.time_frame_hours) 53 | date_query = time_threshold.strftime("%Y/%m/%d") 54 | query = f"in:inbox -category:promotions after:{date_query}" 55 | emails = self.gmail.get_messages(query=query) 56 | filtered = [] 57 | for email in emails: 58 | try: 59 | email_date = parse_date(email.date) 60 | except Exception: 61 | continue 62 | if email_date >= time_threshold and hasattr(email, 'attachments') and email.attachments: 63 | filtered.append(email) 64 | return filtered 65 | 66 | def summarize_attachment(self, attachment) -> str: 67 | """ 68 | Downloads an attachment into a temporary file, converts it with MarkItDown, 69 | then uses ChatGroq to produce a concise summary. 70 | :param attachment: An attachment object from an email. 71 | :return: A concise summary string of the attachment's content. 72 | """ 73 | temp_path = None 74 | try: 75 | # Determine file extension from the attachment's filename. 76 | suffix = os.path.splitext(attachment.filename)[1] 77 | 78 | # First, try to get the content directly. 79 | content = attachment.download() 80 | 81 | # If direct download returns nothing, try saving the file. 82 | if not content: 83 | attachment.save() 84 | if os.path.exists(attachment.filename): 85 | with open(attachment.filename, 'rb') as f: 86 | content = f.read() 87 | os.remove(attachment.filename) 88 | else: 89 | return "Attachment content could not be retrieved." 90 | 91 | # Write the content to a temporary file. 92 | with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: 93 | tmp.write(content) 94 | temp_path = tmp.name 95 | 96 | # Convert the temporary file using MarkItDown. 97 | conversion_result = self.markdown_converter.convert(temp_path) 98 | text_content = conversion_result.text_content 99 | if not text_content: 100 | text_content = "No text could be extracted from the attachment." 101 | 102 | except Exception as e: 103 | text_content = f"Error converting attachment: {str(e)}" 104 | finally: 105 | if temp_path and os.path.exists(temp_path): 106 | os.remove(temp_path) 107 | 108 | # Escape curly braces in the text to prevent formatting issues. 109 | safe_text_content = text_content.replace("{", "{{").replace("}", "}}") 110 | 111 | # Build the prompt text with instructions for a single, concise paragraph summary. 112 | prompt_text = ( 113 | "Please provide a concise summary of the following attachment content as a single paragraph. " 114 | "Make it as concise as possible and avoid detailed breakdowns yet give an overview of the attachment. " 115 | "Do not include any bullet points, lists, or detailed breakdowns. " 116 | "Only summarize the overall content.\n\n" 117 | f"{safe_text_content}" 118 | ) 119 | prompt = ChatPromptTemplate.from_messages([ 120 | ("system", "You are a highly accurate and detail-oriented summarization assistant."), 121 | ("human", prompt_text) 122 | ]) 123 | try: 124 | messages_formatted = prompt.format_messages() 125 | response = self.llm.invoke(messages_formatted) 126 | summary = response.content.strip() 127 | except Exception as e: 128 | summary = f"Failed to summarize attachment: {str(e)}" 129 | return summary 130 | 131 | def summarize_email(self, email) -> str: 132 | """ 133 | Returns a summary string for an email including its subject, sender, date, 134 | a snippet from the body, and for each attachment a summarized version. 135 | :param email: An email object. 136 | :return: A string summary of the email. 137 | """ 138 | summary = ( 139 | f"Email Summary:\n" 140 | f"Subject: {email.subject}\n" 141 | f"Sender: {email.sender}\n" 142 | f"Date: {email.date}\n" 143 | ) 144 | body = email.plain if (hasattr(email, 'plain') and email.plain) else email.snippet 145 | snippet = body if len(body) <= 200 else body[:200] + "..." 146 | summary += f"Snippet: {snippet}\n" 147 | 148 | if hasattr(email, 'attachments') and email.attachments: 149 | summary += "Attachments Summaries:\n" 150 | for att in email.attachments: 151 | att_summary = self.summarize_attachment(att) 152 | summary += f" Attachment: {att.filename}\n Summary: {att_summary}\n" 153 | return summary 154 | 155 | def summarize_emails(self) -> str: 156 | """ 157 | Fetches emails with attachments in the past time frame and builds a combined summary. 158 | :return: A combined summary string of all relevant emails. 159 | """ 160 | emails = self.fetch_emails_with_attachments() 161 | if not emails: 162 | return f"No emails with attachments found in the past {self.time_frame_hours} hours." 163 | 164 | summaries = [self.summarize_email(email) for email in emails] 165 | return "\n\n---------------------\n\n".join(summaries) 166 | 167 | 168 | def main(): 169 | # Example usage when running this module directly. 170 | print("=== Email Attachment Summarizer ===") 171 | summarizer = GmailAttachmentSummarizer(time_frame_hours=48) 172 | print("Fetching emails with attachments from the past 48 hours...\n") 173 | final_summary = summarizer.summarize_emails() 174 | print("Final Summary:\n") 175 | print(final_summary) 176 | 177 | 178 | if __name__ == "__main__": 179 | main() 180 | -------------------------------------------------------------------------------- /src/utils/tool.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | import time 5 | from datetime import datetime, timedelta 6 | from dateutil.parser import parse as parse_date 7 | from dateutil.tz import tzlocal 8 | from dotenv import load_dotenv 9 | 10 | from simplegmail import Gmail 11 | from langchain_groq import ChatGroq 12 | from langchain.prompts.chat import ChatPromptTemplate 13 | 14 | # Load environment variables (including GROQ_API_KEY) from repository secrets. 15 | load_dotenv() 16 | GROQ_API_KEY = os.getenv("GROQ_API_KEY") 17 | 18 | class GmailChat: 19 | def __init__(self, time_frame_hours: int = 24): 20 | """ 21 | Initialize with a desired timeframe (in hours) for fetching emails. 22 | """ 23 | self.time_frame_hours = time_frame_hours 24 | 25 | def fetch_emails(self): 26 | """ 27 | Fetches emails from the inbox within the past time_frame_hours. 28 | Returns a list of email objects. 29 | """ 30 | gmail = Gmail() 31 | time_threshold = datetime.now(tz=tzlocal()) - timedelta(hours=self.time_frame_hours) 32 | date_query = time_threshold.strftime("%Y/%m/%d") 33 | query = f"in:inbox -category:promotions after:{date_query}" 34 | emails = gmail.get_messages(query=query) 35 | filtered = [] 36 | for email in emails: 37 | try: 38 | email_date = parse_date(email.date) 39 | except Exception: 40 | continue 41 | if email_date >= time_threshold: 42 | filtered.append(email) 43 | return filtered 44 | 45 | @staticmethod 46 | def summarize_email(email): 47 | """ 48 | Returns a summary string for an email including subject, sender, date, 49 | a snippet from the body, and any attachment filenames if present. 50 | """ 51 | summary = f"Subject: {email.subject}\nSender: {email.sender}\nDate: {email.date}\n" 52 | if hasattr(email, 'plain') and email.plain: 53 | body = email.plain 54 | else: 55 | body = email.snippet 56 | if len(body) > 200: 57 | body = body[:200] + "..." 58 | summary += f"Snippet: {body}\n" 59 | if hasattr(email, 'attachments') and email.attachments: 60 | att_names = [att.filename for att in email.attachments] 61 | summary += f"Attachments: {', '.join(att_names)}\n" 62 | return summary 63 | 64 | def build_emails_summary(self, emails): 65 | """ 66 | Builds and returns a combined summary text for a list of emails. 67 | """ 68 | summaries = [self.summarize_email(email) for email in emails] 69 | return "\n---------------------\n".join(summaries) 70 | 71 | def answer_query(self, query, emails_summary): 72 | """ 73 | Uses ChatGroq to answer the query based on the provided emails summary. 74 | Returns the answer as a string. 75 | """ 76 | llm = ChatGroq( 77 | model="mixtral-8x7b-32768", 78 | temperature=0, 79 | max_tokens=150, 80 | timeout=60, 81 | max_retries=2, 82 | api_key=GROQ_API_KEY # Using the repository secret 83 | ) 84 | prompt = ChatPromptTemplate.from_messages([ 85 | ( 86 | "system", 87 | "You are a helpful assistant that answers queries based on a collection of email summaries. Provide a clear yes/no answer followed by a brief explanation." 88 | ), 89 | ( 90 | "human", 91 | f"User query: {query}\n\nHere are the summaries of my emails from the past {self.time_frame_hours} hours:\n\n{emails_summary}\n\nBased on the above, answer the query." 92 | ) 93 | ]) 94 | messages_formatted = prompt.format_messages() 95 | response = llm.invoke(messages_formatted) 96 | return response.content.strip() 97 | 98 | def chat(self, query): 99 | """ 100 | Fetches emails from the past timeframe, builds summaries, and then answers the query. 101 | Returns the final answer. 102 | """ 103 | emails = self.fetch_emails() 104 | if not emails: 105 | return f"No emails found in the past {self.time_frame_hours} hours." 106 | emails_summary = self.build_emails_summary(emails) 107 | answer = self.answer_query(query, emails_summary) 108 | return answer 109 | 110 | def main(): 111 | print("=== Email Chat Mode ===") 112 | query = input("Enter your query: ") 113 | chat_instance = GmailChat(time_frame_hours=24) 114 | print("Fetching and processing your emails...") 115 | answer = chat_instance.chat(query) 116 | print("\nAnswer:") 117 | print(answer) 118 | 119 | if __name__ == "__main__": 120 | main() 121 | -------------------------------------------------------------------------------- /src/utils/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import logging 4 | import base64 5 | from datetime import datetime, timedelta 6 | from email.mime.text import MIMEText 7 | from dateutil.parser import parse as parse_date 8 | 9 | from simplegmail import Gmail 10 | from simplegmail.query import construct_query 11 | from dotenv import load_dotenv 12 | from langchain_groq import ChatGroq 13 | from langchain.prompts.chat import ChatPromptTemplate 14 | 15 | # Configure logging for better debugging and traceability. 16 | logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") 17 | load_dotenv() # Load environment variables from a .env file if present. 18 | GROQ_API_KEY = os.getenv("GROQ_API_KEY") 19 | 20 | class EmailDraftUtil: 21 | def __init__(self, gmail=None): 22 | """ 23 | Initialize the utility. If no Gmail object is provided, 24 | authenticate using the default client_secret.json. 25 | """ 26 | if gmail is None: 27 | self.gmail = self.authenticate_gmail() 28 | else: 29 | self.gmail = gmail 30 | 31 | def authenticate_gmail(self): 32 | """ 33 | Authenticates the Gmail API using the client_secret.json file. 34 | On the first run, a browser window will open for you to grant permissions. 35 | 36 | Returns: 37 | An authenticated Gmail object. 38 | """ 39 | try: 40 | gmail = Gmail() # Uses client_secret.json by default. 41 | logging.info("Successfully authenticated with Gmail.") 42 | return gmail 43 | except Exception as e: 44 | logging.error("Failed to authenticate with Gmail: %s", e) 45 | raise 46 | 47 | def generate_reply_draft_for_email(self, email): 48 | """ 49 | Uses ChatGroq (via LangChain) to generate a professional reply draft for the provided email. 50 | The prompt instructs the model that it is Zamal Ali Babar’s assistant and must draft a reply 51 | on his behalf based on the email received. The reply must be original, address the sender’s message, 52 | and not simply echo the email content. 53 | 54 | Args: 55 | email: The email object (from SimpleGmail). 56 | 57 | Returns: 58 | A string with the generated reply draft, or None on error. 59 | """ 60 | try: 61 | # Use plain text if available; otherwise, use a snippet. 62 | email_body = getattr(email, 'plain', None) or getattr(email, 'snippet', "") 63 | if len(email_body) > 1000: 64 | email_body = email_body[:1000] + "..." 65 | 66 | # Initialize the ChatGroq LLM with the repository secret. 67 | llm = ChatGroq( 68 | model="mixtral-8x7b-32768", # Adjust model as needed. 69 | temperature=0.7, 70 | max_tokens=300, 71 | timeout=60, 72 | max_retries=2, 73 | api_key=GROQ_API_KEY # Using the repository secret. 74 | ) 75 | 76 | # Revised prompt: instruct the model that it is acting as Zamal Ali Babar's assistant. 77 | draft_prompt = ChatPromptTemplate.from_messages([ 78 | ( 79 | "system", 80 | ( 81 | "You are Zamal Ali Babar's professional email assistant. Your task is to draft a reply email on his behalf " 82 | "based on the email details provided. Do not simply repeat the original email. Instead, craft a personalized, " 83 | "clear, and courteous reply that acknowledges the sender's message, addresses key points, and ends with an appropriate sign-off. " 84 | "Return only the text of the reply email draft." 85 | ) 86 | ), 87 | ( 88 | "human", 89 | ( 90 | "Email Details:\n" 91 | "Subject: {subject}\n" 92 | "From: {sender}\n" 93 | "Date: {date}\n" 94 | "Email Body:\n{body}" 95 | ) 96 | ) 97 | ]) 98 | 99 | prompt_inputs = { 100 | "subject": email.subject, 101 | "sender": email.sender, 102 | "date": email.date, 103 | "body": email_body, 104 | } 105 | 106 | messages_formatted = draft_prompt.format_messages(**prompt_inputs) 107 | response_obj = llm.invoke(messages_formatted) 108 | reply_draft = response_obj.content.strip() 109 | logging.info("Reply draft generated successfully.") 110 | return reply_draft 111 | 112 | except Exception as e: 113 | logging.error("Error generating reply draft for email '%s': %s", email.subject, e) 114 | return None 115 | 116 | def create_draft_reply(self, email, reply_body): 117 | """ 118 | Saves the reply draft as a Gmail draft using the Gmail API. 119 | Builds a MIME message from the reply draft and calls the Gmail API's drafts.create() method. 120 | 121 | Args: 122 | email: The original email object to reply to. 123 | reply_body: The text of the reply draft. 124 | """ 125 | try: 126 | # Prepend "Re:" to the subject if not already present. 127 | reply_subject = email.subject if email.subject.lower().startswith("re:") else f"Re: {email.subject}" 128 | # Determine the sender email address. 129 | sender_email = self.gmail.user_email if hasattr(self.gmail, "user_email") else "me" 130 | 131 | # Create a MIMEText message for the reply. 132 | mime_msg = MIMEText(reply_body) 133 | mime_msg['to'] = email.sender 134 | mime_msg['from'] = sender_email 135 | mime_msg['subject'] = reply_subject 136 | 137 | # Encode the message as base64url. 138 | raw_msg = base64.urlsafe_b64encode(mime_msg.as_bytes()).decode() 139 | 140 | # Build the draft body. 141 | draft_body = { 142 | 'message': { 143 | 'raw': raw_msg 144 | } 145 | } 146 | 147 | # Use the underlying Gmail API to create the draft. 148 | draft = self.gmail.service.users().drafts().create(userId="me", body=draft_body).execute() 149 | logging.info("Draft reply created successfully. Draft ID: %s", draft.get('id')) 150 | except Exception as e: 151 | logging.error("Error creating Gmail draft for email '%s': %s", email.subject, e) 152 | return None 153 | 154 | def generate_and_save_draft(self, email): 155 | """ 156 | Generates a reply draft for the provided email and saves it as a Gmail draft. 157 | 158 | Args: 159 | email: The email object to process. 160 | 161 | Returns: 162 | The generated reply draft text if successful, otherwise None. 163 | """ 164 | reply_draft = self.generate_reply_draft_for_email(email) 165 | if not reply_draft: 166 | logging.error("Failed to generate a reply draft for email: %s", email.subject) 167 | return None 168 | 169 | self.create_draft_reply(email, reply_draft) 170 | return reply_draft 171 | 172 | # --- Module-level convenience functions --- 173 | 174 | def authenticate_gmail(): 175 | """ 176 | Module-level helper to authenticate and return a Gmail object. 177 | """ 178 | ed_util = EmailDraftUtil() 179 | return ed_util.gmail 180 | 181 | def generate_and_save_draft(email): 182 | """ 183 | Module-level helper that accepts an email object, generates a reply draft, 184 | and creates a Gmail draft. 185 | 186 | Args: 187 | email: The email object to process. 188 | 189 | Returns: 190 | The generated reply draft text if successful, otherwise None. 191 | """ 192 | ed_util = EmailDraftUtil() 193 | return ed_util.generate_and_save_draft(email) 194 | --------------------------------------------------------------------------------