├── .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 |
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 |
--------------------------------------------------------------------------------