[
  {
    "path": ".env_example",
    "content": "# OpenAI Model\nMODEL=openai/gpt-4o-mini\nOPENAI_API_KEY=your_openai_api_key\n\n# Gmail credentials\nEMAIL_ADDRESS=your_email_address@gmail.com\nAPP_PASSWORD=your_app_password\n\n# Slack Webhook URL\nSLACK_WEBHOOK_URL=your_slack_webhook_url"
  },
  {
    "path": ".gitignore",
    "content": "# Environment variables\n.env\n.env.*\n\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# Virtual Environment\nvenv/\n.venv/\nenv/\nENV/\n\n# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n\n# Project specific\noutput\n*.log\n\n# OS specific\n.DS_Store\nThumbs.db\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Tony Kipkemboi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Gmail Automation with CrewAI 📧✨\n\n[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCApiD66gf36M9hZanbjgNaw?style=social)](https://www.youtube.com/@tonykipkemboi)\n[![GitHub followers](https://img.shields.io/github/followers/tonykipkemboi?style=social)](https://github.com/tonykipkemboi)\n[![Twitter Follow](https://img.shields.io/twitter/follow/tonykipkemboi?style=social)](https://twitter.com/tonykipkemboi)\n[![LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-blue?style=flat&logo=linkedin)](https://www.linkedin.com/in/tonykipkemboi/)\n\nGmail Automation with CrewAI is an intelligent email management system that uses AI agents to categorize, organize, respond to, and clean up your Gmail inbox automatically.\n\n![Gmail Automation](./assets/gmail-automation.jpg)\n\n## ✨ Features\n\n![Stars](https://img.shields.io/github/stars/tonykipkemboi/crewai-gmail-automation?style=social)\n![Last Commit](https://img.shields.io/github/last-commit/tonykipkemboi/crewai-gmail-automation) \n![Status](https://img.shields.io/badge/Status-Active-brightgreen)\n\n- **📋 Email Categorization**: Automatically categorizes emails into specific types (newsletters, promotions, personal, etc.)\n- **🔔 Priority Assignment**: Assigns priority levels (HIGH, MEDIUM, LOW) based on content and sender with strict classification rules\n- **🏷️ Smart Organization**: Applies Gmail labels and stars based on categories and priorities\n- **💬 Automated Responses**: Generates draft responses for important emails that need replies\n- **📱 Slack Notifications**: Sends creative notifications for high-priority emails\n- **🧹 Intelligent Cleanup**: Safely deletes low-priority emails based on age and category\n- **🎬 YouTube Content Protection**: Special handling for YouTube-related emails\n- **🗑️ Trash Management**: Automatically empties trash to free up storage space\n- **🧵 Thread Awareness**: Recognizes and properly handles email threads\n\n\n## 🚀 Installation\n\n```bash\n# Clone the repository\ngit clone https://github.com/tonykipkemboi/crewai-gmail-automation.git\ncd crewai-gmail-automation\n\n# Create and activate a virtual environment\npython -m venv .venv\nsource .venv/bin/activate  # On Windows: .venv\\Scripts\\activate\n\n# Install dependencies\ncrewai install\n```\n\n## ⚙️ Configuration\n\n1. Create a `.env` file in the root directory with the following variables:\n\n```\n# Choose your LLM provider\n# OpenAI (Recommended)\nMODEL=openai/gpt-4o-mini\nOPENAI_API_KEY=your_openai_api_key\n\n# Or Gemini\n# MODEL=gemini/gemini-2.0-flash\n# GEMINI_API_KEY=your_gemini_api_key\n\n# Or Ollama  (Note: May have compatibility issues with tool calling)\n# Download the model from https://ollama.com/library\n# MODEL=ollama/llama3-groq-tool-use # use ones that have tool calling capabilities\n\n# Gmail credentials\nEMAIL_ADDRESS=your_email@gmail.com\nAPP_PASSWORD=your_app_password\n\n# Optional: Slack notifications\nSLACK_WEBHOOK_URL=your_slack_webhook_url\n```\n\n<details>\n<summary><b>🔑 How to create a Gmail App Password</b></summary>\n\n1. Go to your Google Account settings at [myaccount.google.com](https://myaccount.google.com/)\n2. Select **Security** from the left navigation panel\n3. Under \"Signing in to Google,\" find and select **2-Step Verification** (enable it if not already enabled)\n4. Scroll to the bottom and find **App passwords**\n5. Select **Mail** from the \"Select app\" dropdown\n6. Select **Other (Custom name)** from the \"Select device\" dropdown\n7. Enter `Gmail CrewAI` as the name\n8. Click **Generate**\n9. Copy the 16-character password that appears (spaces will be removed automatically)\n10. Paste this password in your `.env` file as the `APP_PASSWORD` value\n11. Click **Done**\n\n**Note**: App passwords can only be created if you have 2-Step Verification enabled on your Google account.\n</details>\n\n<details>\n<summary><b>🔗 How to create a Slack Webhook URL</b></summary>\n\n1. Go to [api.slack.com/apps](https://api.slack.com/apps)\n2. Click **Create New App**\n3. Select **From scratch**\n4. Enter `Gmail Notifications` as the app name\n5. Select your workspace and click **Create App**\n6. In the left sidebar, find and click on **Incoming Webhooks**\n7. Toggle the switch to **Activate Incoming Webhooks**\n8. Click **Add New Webhook to Workspace**\n9. Select the channel where you want to receive notifications\n10. Click **Allow**\n11. Find the **Webhook URL** section and copy the URL that begins with `https://hooks.slack.com/services/`\n12. Paste this URL in your `.env` file as the `SLACK_WEBHOOK_URL` value\n\n**Customizing your Slack app (optional):**\n1. Go to **Basic Information** in the left sidebar\n2. Scroll down to **Display Information**\n3. Add an app icon and description\n4. Click **Save Changes**\n\n**Note**: You need admin permissions or the ability to install apps in your Slack workspace.\n</details>\n\n## 📧 How It Works\n\nThis application uses the IMAP (Internet Message Access Protocol) to securely connect to your Gmail account and manage your emails. Here's how it works:\n\n<details>\n<summary><b>🔄 IMAP Connection Process</b></summary>\n\n1. **Secure Connection**: The application establishes a secure SSL connection to Gmail's IMAP server (`imap.gmail.com`).\n\n2. **Authentication**: It authenticates using your email address and app password (not your regular Google password).\n\n3. **Mailbox Access**: Once authenticated, it can access your inbox and other mailboxes to:\n   - Read unread emails\n   - Apply labels\n   - Move emails to trash\n   - Save draft responses\n\n4. **Safe Disconnection**: After each operation, the connection is properly closed to maintain security.\n\nIMAP allows the application to work with your emails while they remain on Google's servers, unlike POP3 which would download them to your device. This means you can still access all emails through the regular Gmail interface.\n\n**Security Note**: Your credentials are only stored locally in your `.env` file and are never shared with any external services.\n</details>\n\n## 🔍 Usage\n\nRun the application with:\n\n```bash\ncrewai run\n```\n\nYou'll be prompted to enter the number of emails to process (default is 5).\n\nThe application will:\n1. 📥 Fetch your unread emails\n2. 🔎 Categorize them by type and priority\n3. ⭐ Apply appropriate labels and stars\n4. ✏️ Generate draft responses for important emails\n5. 🔔 Send Slack notifications for high-priority items\n6. 🗑️ Clean up low-priority emails based on age\n7. 🧹 Empty the trash to free up storage space\n\n\n## 🌟 Special Features\n\n- **📅 Smart Deletion Rules**: \n  - Promotions older than 2 days are automatically deleted\n  - Newsletters older than 7 days (unless HIGH priority) are deleted\n  - Shutterfly emails are always deleted regardless of age\n  - Receipts and important documents are archived instead of deleted\n\n- **🎬 YouTube Protection**: All YouTube-related emails are preserved and marked as READ_ONLY (you'll respond directly on YouTube)\n\n- **✍️ Smart Response Generation**: Responses are tailored to the email context and include proper formatting\n\n- **💡 Creative Slack Notifications**: Fun, attention-grabbing notifications for important emails\n\n- **🧵 Thread Handling**: Properly tracks and manages email threads to maintain conversation context\n\n## 👥 Contributing\n\nContributions are welcome! Please feel free to submit a [Pull Request](https://github.com/tonykipkemboi/crewai-gmail-automation/pulls).\n\n## 📄 License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## 📚 References\n\n- [CrewAI](https://github.com/crewAIInc/crewAI/)\n- [IMAP Protocol for Gmail](https://support.google.com/mail/answer/7126229)\n- [Slack API](https://api.slack.com/messaging/webhooks)\n- [Ollama](https://ollama.com/library)\n- [Gemini](https://ai.google.com/gemini-api)\n- [OpenAI](https://openai.com/api/)\n\n"
  },
  {
    "path": "knowledge/user_preference.txt",
    "content": "User name is John Doe.\nUser is an AI Engineer.\nUser is interested in AI Agents.\nUser is based in San Francisco, California.\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"gmail_crew_ai\"\nversion = \"0.1.0\"\ndescription = \"gmail-crew-ai using crewAI\"\nauthors = [{ name = \"Your Name\", email = \"you@example.com\" }]\nrequires-python = \">=3.10,<3.13\"\ndependencies = [\n    \"bs4>=0.0.2\",\n    \"crewai[tools]>=0.102.0,<1.0.0\",\n]\n\n[project.scripts]\ngmail_crew_ai = \"gmail_crew_ai.main:run\"\nrun_crew = \"gmail_crew_ai.main:run\"\ntrain = \"gmail_crew_ai.main:train\"\nreplay = \"gmail_crew_ai.main:replay\"\ntest = \"gmail_crew_ai.main:test\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.crewai]\ntype = \"crew\"\n"
  },
  {
    "path": "src/gmail_crew_ai/__init__.py",
    "content": ""
  },
  {
    "path": "src/gmail_crew_ai/config/agents.yaml",
    "content": "categorizer:\n  role: >\n    Email Categorizer and Prioritizer\n  goal: >\n    Analyze and categorize unread emails based on urgency, importance, and required actions, \n    ensuring critical communications are identified and properly prioritized.\n  backstory: >\n    You are an expert email manager with years of experience in handling high-volume inboxes. \n    You excel at quickly identifying urgent matters, understanding email context, and determining \n    appropriate priority levels. You're particularly skilled at recognizing the true importance \n    of messages beyond just their subject lines.\n  verbose: true\n  memory: true\n\norganizer:\n  role: >\n    Email Organization Specialist\n  goal: >\n    Apply Gmail's organizational features (stars, labels, priority markers) consistently and \n    effectively based on email categorization, maintaining a well-structured and easily navigable inbox.\n  backstory: >\n    You are a Gmail power user and productivity expert who understands how to leverage Gmail's \n    organizational features to their fullest potential. You've helped numerous executives maintain \n    pristine inboxes and have developed a systematic approach to email organization that ensures \n    important messages are easily findable and properly highlighted.\n  verbose: true\n  memory: true\n\nresponse_generator:\n  role: >\n    Email Response Draft Generator\n  goal: >\n    Create professional, context-aware email responses that effectively address the sender's needs \n    while maintaining appropriate tone and including all necessary information.\n  backstory: >\n    You are a skilled professional writer with extensive experience in business communication. \n    You excel at crafting responses that are clear, concise, and appropriate for the context. \n    You understand the importance of maintaining professional relationships through effective \n    email communication and know when to include additional resources like calendar links for meetings.\n  verbose: true\n  memory: true\n\nnotifier:\n  role: >\n    Email Notification Specialist\n  goal: >\n    Monitor important emails and send timely notifications to Slack for high and medium priority messages.\n  backstory: >\n    You are a vigilant notification specialist who ensures that important communications are never missed. \n    You understand which emails require immediate attention and can summarize complex information into \n    clear, actionable notifications. Your role is to bridge the gap between email and Slack, ensuring \n    that urgent matters are promptly brought to attention.\n  verbose: true\n  memory: true\n\ncleaner:\n  role: >\n    Email Cleanup Specialist\n  goal: >\n    Identify and delete low-priority emails that are cluttering the inbox while ensuring \n    no important communications are lost.\n  backstory: >\n    You are a meticulous digital organizer who specializes in email management. You have a \n    keen eye for distinguishing between emails that need to be preserved and those that can \n    be safely removed. You understand the importance of maintaining a clean inbox while being \n    extremely careful not to delete anything of value.\n  verbose: true\n  memory: true\n\ntrash_cleaner:\n  role: \"Email Trash Cleanup Specialist\"\n  goal: \"Empty the Gmail trash folder to free up storage space\"\n  backstory: \"You are an expert at managing email storage efficiently. Your job is to help users free up space by emptying the trash folder when needed.\""
  },
  {
    "path": "src/gmail_crew_ai/config/tasks.yaml",
    "content": "categorization_task:\n  description: >\n    First, read the fetched emails from the file at 'output/fetched_emails.json' using the `FileReadTool`.\n    \n    For each email, analyze the content and categorize as follows:\n\n    1. Category (choose one):\n       - NEWSLETTERS\n       - PROMOTIONS\n       - PERSONAL\n       - GITHUB\n       - YOUTUBE\n       - RECEIPTS_INVOICES\n       - OTHER\n\n    2. Priority (choose one):\n      IMPORTANT - BE VERY STRICT WITH PRIORITY LEVELS:\n      - HIGH: ONLY for urgent matters requiring immediate attention or response\n      - MEDIUM: ONLY for important but not urgent communications from real people\n         - DO NOT mark promotional content, newsletters, or automated notifications as MEDIUM\n         - DO NOT mark Shutterfly or other promotional emails as MEDIUM - they should be LOW\n         - Only actual communication from real people can be MEDIUM\n      - LOW\n\n    3. Required Action (choose one):\n       - REPLY\n       - READ_ONLY\n       - TASK\n       - IGNORE\n\n    IMPORTANT YOUTUBE EMAIL RULES:\n    - ANY email from youtube.com domains MUST be categorized as YOUTUBE\n    - ANY email containing \"YouTube\" in the subject or sender MUST be categorized as YOUTUBE\n    - YouTube emails about comments should be HIGH priority but require READ_ONLY action\n      (the user will reply directly on YouTube, not via email)\n    \n    IMPORTANT FORMAT INSTRUCTIONS:\n    Your final answer must be a valid JSON object with these exact fields(example without the quotes):\n      \"email_id\": \"The email's unique identifier\",\n      \"subject\": \"The email's subject line\",\n      \"sender\": \"The email sender\",\n      \"category\": \"The category you assigned\",\n      \"priority\": \"The priority you assigned\",\n      \"required_action\": \"The action needed\",\n      \"date\": \"The email date in YYYY-MM-DD format\",\n      \"age_days\": \"The age of the email in days\"\n    \n    Do not include any explanations, thoughts, or additional text outside the JSON object.\n    Do not use markdown formatting or code blocks.\n    Just return the raw JSON object.\n  expected_output: >\n    A JSON object with email_id, subject, category, priority, and required_action fields.\n  agent: categorizer\n  output_file: output/categorization_report.json\n\norganization_task:\n  description: >\n    First, read the categorization report from 'output/categorization_report.json' using the `FileReadTool`.\n    \n    For each email, organize it using Gmail's priority features with the 'organize_email' tool.\n\n    Use the email content, email domain, and categorization report to determine the correct labels.\n    \n    Simplified Label Rules:\n    \n    1. For YOUTUBE emails:\n       - Add \"YOUTUBE\" label\n       - Star the email\n       - If HIGH priority, also add \"URGENT\" label\n    \n    2. For NEWSLETTERS emails:\n       - Add \"NEWSLETTERS\" label\n       - No newsletter is HIGH or MEDIUM priority\n       - No newsletter requires action\n    \n    3. For other emails:\n       - Add a label matching the category (e.g., \"PERSONAL\", \"GITHUB\")\n       - If HIGH priority, add \"URGENT\" label and star the email\n       - If MEDIUM priority, add \"ACTION_NEEDED\" label\n    \n    Your final answer should be a JSON object with:\n    - email_id: The email's ID\n    - subject: The email's subject\n    - applied_labels: List of labels applied\n    - starred: Whether the email was starred (true/false)\n    - result: \"Success\" or error message\n  expected_output: >\n    A detailed report of how each email was organized, including:\n    - The email ID and subject\n    - The labels that were applied\n    - Whether the email was starred\n    - The result of the organization attempt\n\n    The report should include special handling for YOUTUBE and NEWSLETTERS emails to ensure they receive\n    the appropriate labels and priority indicators.\n  agent: organizer\n  context: [categorization_task]\n  output_file: output/organization_report.json\n\nresponse_task:\n  description: >\n    Based on the categorization report, generate responses ONLY for emails that require action.\n    \n    Only generate responses for:\n    - PERSONAL emails with HIGH or MEDIUM priority\n    - DO NOT generate responses for ANY YouTube emails - the user will respond directly on YouTube instead\n    \n    \n    For YouTube comments:\n    - Thank them for their comment\n    - Answer their question if possible\n    - End with \"Best regards, Tony Kipkemboi\"\n    \n    IMPORTANT: For the recipient field, extract the username from the comment.\n    For example, if the comment is from \"@username commented on your video\",\n    use \"username@gmail.com\" as the recipient.\n    \n    Use the 'save_email_draft' tool with:\n    - subject: Add \"Re: \" to the original subject\n    - body: Your response\n    - recipient: The extracted username + \"@gmail.com\" or use \"user@example.com\" if you can't extract it\n    \n    Your final answer should be a simple list of emails you responded to.\n  expected_output: >\n    A draft email saved to the drafts folder for each email that requires a response, or \n    \"No response needed\" for emails to be deleted or ignored. Each draft should follow the \n    specified structure and formatting guidelines. The should be in correct Markdown format without\n    any additional text or formatting such as \"```\" or \"```md\".\n  agent: response_generator\n  context: [categorization_task, organization_task]\n  output_file: output/response_report.json\n\nnotification_task:\n  description: >\n    Based on the categorization report, send Slack notifications for HIGH priority emails using the 'SlackNotificationTool'.\n    \n    For each HIGH priority email:\n    1. Create a brief summary of the email content\n    2. Identify any action needed\n    3. Create an attention-grabbing headline\n    4. Add an emoji-filled intro\n    \n    Use the 'SlackNotificationTool' with:\n    - subject: The email subject\n    - sender: The email sender\n    - category: The email category (YOUTUBE, PERSONAL, etc.)\n    - priority: HIGH\n    - summary: Your brief summary\n    - action_needed: What action is needed\n    - headline: Your custom headline\n    - intro: Your custom intro with emojis\n    \n    Your final answer should list all notifications sent.\n  expected_output: >\n    A report of which emails had Slack notifications sent, including their subjects, priorities, and the creative elements used.\n    The report should be in correct Markdown format without any additional text or formatting such as \"```\" or \"```md\".\n  agent: notifier\n  context: [categorization_task]\n  output_file: output/notification_report.json\n\ncleanup_task:\n  description: >\n    Based on the categorization report, identify LOW priority emails that are safe to delete.\n    \n    EMAILS TO ARCHIVE INSTEAD OF DELETE:\n    - Receipts and Invoices (archive for tax purposes)\n    - Confirmation emails for purchases\n    - Travel-related emails with booking information\n    - Anything that might be needed for reference later\n    \n    For these emails, use the 'GmailArchiveTool' instead of deleting them.\n    \n    NEVER delete:\n    - ANY email marked as HIGH or MEDIUM priority\n    - ANY email from a personal contact\n    - ANY email related to YouTube\n    - ANY email less than 5 days old\n    \n    ALWAYS delete:\n    - ANY email with \"Shutterfly\" in the sender or subject, regardless of categorization\n    - ANY email categorized as PROMOTIONS and older than 2 days\n    - ANY email categorized as NEWSLETTERS and older than 7 days, unless HIGH priority\n    \n    The emails already have their age calculated in the \"age_days\" field.\n    An email is less than 5 days old if age_days < 5.\n    \n    For each email:\n    1. Check if it meets the deletion criteria (LOW priority AND not from criteria above)\n    2. For emails you decide to delete, document:\n       - The email subject and sender\n       - The reason for deletion\n       - Use the 'GmailDeleteTool' with the email_id and reason\n\n    AFTER processing all emails for deletion:\n    3. IMPORTANT: Use the 'empty_gmail_trash' tool to permanently empty the trash folder\n       and free up storage space in the Gmail account.\n\n    WORKFLOW:\n    1. First, analyze each email one at a time\n    2. For emails that should be deleted, use delete_email tool with ONLY the required fields\n    3. After processing all emails, use empty_gmail_trash tool\n    4. Then compile your final report\n\n    Your final answer should provide a detailed report containing:\n    - DELETED EMAILS: For each deleted email, include email_id, subject, sender, category, priority, age, and reason for deletion\n    - PRESERVED EMAILS: For each preserved email, include email_id, subject, sender, category, priority, age, and reason for preservation\n    - TRASH STATUS: Information about whether the trash was emptied successfully and how many messages were permanently removed\n    Also include information about whether the trash was emptied successfully.\n\n    IMPORTANT: When using the 'delete_email' tool, ONLY provide the email_id and reason \n    as a simple JSON object. For example:\n    \n      \"email_id\": \"123\", \n      \"reason\": \"Low priority promotional email\"\n    \n  expected_output: >\n    A comprehensive deletion report with three sections:\n    1. DELETED EMAILS: Full details of each deleted email including subject, sender, id, category, priority, age and deletion reason\n    2. PRESERVED EMAILS: Full details of each preserved email including the reason for preservation\n    3. TRASH STATUS: Results of the trash emptying operation including number of messages permanently removed\n    The report should be in JSON format for easy parsing and tracking.\n  agent: cleaner\n  context: [categorization_task, organization_task]\n  output_file: output/cleanup_report.json\n"
  },
  {
    "path": "src/gmail_crew_ai/crew.py",
    "content": "from crewai import Agent, Crew, Process, Task, LLM\nfrom crewai.project import CrewBase, agent, crew, task, before_kickoff\nfrom crewai_tools import FileReadTool\nimport json\nimport os\nfrom typing import List, Dict, Any, Callable\nfrom pydantic import SkipValidation\nfrom datetime import date, datetime\n\nfrom gmail_crew_ai.tools.gmail_tools import GetUnreadEmailsTool, SaveDraftTool, GmailOrganizeTool, GmailDeleteTool, EmptyTrashTool\nfrom gmail_crew_ai.tools.slack_tool import SlackNotificationTool\nfrom gmail_crew_ai.tools.date_tools import DateCalculationTool\nfrom gmail_crew_ai.models import CategorizedEmail, OrganizedEmail, EmailResponse, SlackNotification, EmailCleanupInfo, SimpleCategorizedEmail, EmailDetails\n\n@CrewBase\nclass GmailCrewAi():\n\t\"\"\"Crew that processes emails.\"\"\"\n\tagents_config = 'config/agents.yaml'\n\ttasks_config = 'config/tasks.yaml'\n\n\t@before_kickoff\n\tdef fetch_emails(self, inputs: Dict[str, Any]) -> Dict[str, Any]:\n\t\t\"\"\"Fetch emails before starting the crew and calculate ages.\"\"\"\n\t\tprint(\"Fetching emails before starting the crew...\")\n\t\t\n\t\t# Get the email limit from inputs\n\t\temail_limit = inputs.get('email_limit', 5)\n\t\tprint(f\"Fetching {email_limit} emails...\")\n\t\t\n\t\t# Create the output directory if it doesn't exist\n\t\tos.makedirs(\"output\", exist_ok=True)\n\t\t\n\t\t# Use the GetUnreadEmailsTool directly\n\t\temail_tool = GetUnreadEmailsTool()\n\t\temail_tuples = email_tool._run(limit=email_limit)\n\t\t\n\t\t# Convert email tuples to EmailDetails objects with pre-calculated ages\n\t\temails = []\n\t\ttoday = date.today()\n\t\tfor email_tuple in email_tuples:\n\t\t\temail_detail = EmailDetails.from_email_tuple(email_tuple)\n\t\t\t\n\t\t\t# Calculate age if date is available\n\t\t\tif email_detail.date:\n\t\t\t\ttry:\n\t\t\t\t\temail_date_obj = datetime.strptime(email_detail.date, \"%Y-%m-%d\").date()\n\t\t\t\t\temail_detail.age_days = (today - email_date_obj).days\n\t\t\t\t\tprint(f\"Email date: {email_detail.date}, age: {email_detail.age_days} days\")\n\t\t\t\texcept Exception as e:\n\t\t\t\t\tprint(f\"Error calculating age for email date {email_detail.date}: {e}\")\n\t\t\t\t\temail_detail.age_days = None\n\t\t\t\n\t\t\temails.append(email_detail.dict())\n\t\t\n\t\t# Save emails to file\n\t\twith open('output/fetched_emails.json', 'w') as f:\n\t\t\tjson.dump(emails, f, indent=2)\n\t\t\n\t\tprint(f\"Fetched and saved {len(emails)} emails to output/fetched_emails.json\")\n\t\t\n\t\treturn inputs\n\t\n\tllm = LLM(\n\t\tmodel=\"openai/gpt-4o-mini\",\n\t\tapi_key=os.getenv(\"OPENAI_API_KEY\"),\n\t)\n\n\t@agent\n\tdef categorizer(self) -> Agent:\n\t\t\"\"\"The email categorizer agent.\"\"\"\n\t\treturn Agent(\n\t\t\tconfig=self.agents_config['categorizer'],\n\t\t\ttools=[FileReadTool()],\n\t\t\tllm=self.llm,\n\t\t)\n\n\t@agent\n\tdef organizer(self) -> Agent:\n\t\t\"\"\"The email organization agent.\"\"\"\n\t\treturn Agent(\n\t\t\tconfig=self.agents_config['organizer'],\n\t\t\ttools=[GmailOrganizeTool(), FileReadTool()],\n\t\t\tllm=self.llm,\n\t\t)\n\t\t\n\t@agent\n\tdef response_generator(self) -> Agent:\n\t\t\"\"\"The email response generator agent.\"\"\"\n\t\treturn Agent(\n\t\t\tconfig=self.agents_config['response_generator'],\n\t\t\ttools=[SaveDraftTool()],\n\t\t\tllm=self.llm,\n\t\t)\n\t\n\t@agent\n\tdef notifier(self) -> Agent:\n\t\t\"\"\"The email notification agent.\"\"\"\n\t\treturn Agent(\n\t\t\tconfig=self.agents_config['notifier'],\n\t\t\ttools=[SlackNotificationTool()],\n\t\t\tllm=self.llm,\n\t\t)\n\n\t@agent\n\tdef cleaner(self) -> Agent:\n\t\t\"\"\"The email cleanup agent.\"\"\"\n\t\treturn Agent(\n\t\t\tconfig=self.agents_config['cleaner'],\n\t\t\ttools=[GmailDeleteTool(), EmptyTrashTool()],\n\t\t\tllm=self.llm,\n\t\t)\n\n\t@task\n\tdef categorization_task(self) -> Task:\n\t\t\"\"\"The email categorization task.\"\"\"\n\t\treturn Task(\n\t\t\tconfig=self.tasks_config['categorization_task'],\n\t\t\toutput_pydantic=SimpleCategorizedEmail\n\t\t)\n\t\n\t@task\n\tdef organization_task(self) -> Task:\n\t\t\"\"\"The email organization task.\"\"\"\n\t\treturn Task(\n\t\t\tconfig=self.tasks_config['organization_task'],\n\t\t\toutput_pydantic=OrganizedEmail,\n\t\t)\n\n\t@task\n\tdef response_task(self) -> Task:\n\t\t\"\"\"The email response task.\"\"\"\n\t\treturn Task(\n\t\t\tconfig=self.tasks_config['response_task'],\n\t\t\toutput_pydantic=EmailResponse,\n\t\t)\n\t\n\t@task\n\tdef notification_task(self) -> Task:\n\t\t\"\"\"The email notification task.\"\"\"\n\t\treturn Task(\n\t\t\tconfig=self.tasks_config['notification_task'],\n\t\t\toutput_pydantic=SlackNotification,\n\t\t)\n\n\t@task\n\tdef cleanup_task(self) -> Task:\n\t\t\"\"\"The email cleanup task.\"\"\"\n\t\treturn Task(\n\t\t\tconfig=self.tasks_config['cleanup_task'],\n\t\t\toutput_pydantic=EmailCleanupInfo,\n\t\t)\n\n\t@crew\n\tdef crew(self) -> Crew:\n\t\t\"\"\"Creates the email processing crew.\"\"\"\n\t\treturn Crew(\n\t\t\tagents=self.agents,\n\t\t\ttasks=self.tasks,\n\t\t\tprocess=Process.sequential,\n\t\t\tverbose=True\n\t\t)\n\n\tdef _debug_callback(self, event_type, payload):\n\t\t\"\"\"Debug callback for crew events.\"\"\"\n\t\tif event_type == \"task_start\":\n\t\t\tprint(f\"DEBUG: Starting task: {payload.get('task_name')}\")\n\t\telif event_type == \"task_end\":\n\t\t\tprint(f\"DEBUG: Finished task: {payload.get('task_name')}\")\n\t\t\tprint(f\"DEBUG: Task output type: {type(payload.get('output'))}\")\n\t\t\t\n\t\t\t# Add more detailed output inspection\n\t\t\toutput = payload.get('output')\n\t\t\tif output:\n\t\t\t\tif isinstance(output, dict):\n\t\t\t\t\tprint(f\"DEBUG: Output keys: {output.keys()}\")\n\t\t\t\t\tfor key, value in output.items():\n\t\t\t\t\t\tprint(f\"DEBUG: {key}: {value[:100] if isinstance(value, str) and len(value) > 100 else value}\")\n\t\t\t\telif isinstance(output, list):\n\t\t\t\t\tprint(f\"DEBUG: Output list length: {len(output)}\")\n\t\t\t\t\tif output and len(output) > 0:\n\t\t\t\t\t\tprint(f\"DEBUG: First item type: {type(output[0])}\")\n\t\t\t\t\t\tif isinstance(output[0], dict):\n\t\t\t\t\t\t\tprint(f\"DEBUG: First item keys: {output[0].keys()}\")\n\t\t\t\telse:\n\t\t\t\t\tprint(f\"DEBUG: Output: {str(output)[:200]}...\")\n\t\telif event_type == \"agent_start\":\n\t\t\tprint(f\"DEBUG: Agent starting: {payload.get('agent_name')}\")\n\t\telif event_type == \"agent_end\":\n\t\t\tprint(f\"DEBUG: Agent finished: {payload.get('agent_name')}\")\n\t\telif event_type == \"error\":\n\t\t\tprint(f\"DEBUG: Error: {payload.get('error')}\")\n\n\tdef _validate_categorization_output(self, output):\n\t\t\"\"\"Validate the categorization output before writing to file.\"\"\"\n\t\tprint(f\"DEBUG: Validating categorization output: {output}\")\n\t\t\n\t\t# If output is empty or invalid, provide a default\n\t\tif not output:\n\t\t\tprint(\"WARNING: Empty categorization output, providing default\")\n\t\t\treturn {\n\t\t\t\t\"email_id\": \"\",\n\t\t\t\t\"subject\": \"\",\n\t\t\t\t\"category\": \"\",\n\t\t\t\t\"priority\": \"\",\n\t\t\t\t\"required_action\": \"\"\n\t\t\t}\n\t\t\n\t\t# If output is a string (which might happen if the LLM returns JSON as a string)\n\t\tif isinstance(output, str):\n\t\t\ttry:\n\t\t\t\t# Try to parse it as JSON\n\t\t\t\timport json\n\t\t\t\t# First, check if the string starts with \"my best complete final answer\"\n\t\t\t\tif \"my best complete final answer\" in output.lower():\n\t\t\t\t\t# Extract the JSON part\n\t\t\t\t\tjson_start = output.find(\"{\")\n\t\t\t\t\tjson_end = output.rfind(\"}\") + 1\n\t\t\t\t\tif json_start >= 0 and json_end > json_start:\n\t\t\t\t\t\tjson_str = output[json_start:json_end]\n\t\t\t\t\t\tparsed = json.loads(json_str)\n\t\t\t\t\t\tprint(\"DEBUG: Successfully extracted and parsed JSON from answer\")\n\t\t\t\t\t\treturn parsed\n\t\t\t\t\n\t\t\t\t# Try to parse the whole string as JSON\n\t\t\t\tparsed = json.loads(output)\n\t\t\t\tprint(\"DEBUG: Successfully parsed string output as JSON\")\n\t\t\t\treturn parsed\n\t\t\texcept Exception as e:\n\t\t\t\tprint(f\"WARNING: Output is a string but not valid JSON: {e}\")\n\t\t\t\t# Try to extract anything that looks like JSON\n\t\t\t\timport re\n\t\t\t\tjson_pattern = r'\\{.*\\}'\n\t\t\t\tmatch = re.search(json_pattern, output, re.DOTALL)\n\t\t\t\tif match:\n\t\t\t\t\ttry:\n\t\t\t\t\t\tjson_str = match.group(0)\n\t\t\t\t\t\tparsed = json.loads(json_str)\n\t\t\t\t\t\tprint(\"DEBUG: Successfully extracted and parsed JSON using regex\")\n\t\t\t\t\t\treturn parsed\n\t\t\t\t\texcept:\n\t\t\t\t\t\tprint(\"WARNING: Failed to parse extracted JSON\")\n\t\t\n\t\t# If output is already a dict, make sure it has the required fields\n\t\tif isinstance(output, dict):\n\t\t\trequired_fields = [\"email_id\", \"subject\", \"category\", \"priority\", \"required_action\"]\n\t\t\tmissing_fields = [field for field in required_fields if field not in output]\n\t\t\t\n\t\t\tif missing_fields:\n\t\t\t\tprint(f\"WARNING: Output missing required fields: {missing_fields}\")\n\t\t\t\t# Add missing fields with empty values\n\t\t\t\tfor field in missing_fields:\n\t\t\t\t\toutput[field] = \"\"\n\t\t\t\n\t\t\t# Check if the values match the expected format\n\t\t\tif output.get(\"email_id\") == \"12345\" and output.get(\"subject\") == \"Urgent Task Update\":\n\t\t\t\tprint(\"WARNING: Output contains placeholder values, trying to fix\")\n\t\t\t\t# Try to get the real email ID from the fetched emails\n\t\t\t\ttry:\n\t\t\t\t\twith open(\"output/fetched_emails.json\", \"r\") as f:\n\t\t\t\t\t\tfetched_emails = json.load(f)\n\t\t\t\t\t\tif fetched_emails and len(fetched_emails) > 0:\n\t\t\t\t\t\t\treal_email = fetched_emails[0]\n\t\t\t\t\t\t\toutput[\"email_id\"] = real_email.get(\"email_id\", \"\")\n\t\t\t\t\t\t\toutput[\"subject\"] = real_email.get(\"subject\", \"\")\n\t\t\t\texcept Exception as e:\n\t\t\t\t\tprint(f\"WARNING: Failed to fix placeholder values: {e}\")\n\t\t\n\t\treturn output\n"
  },
  {
    "path": "src/gmail_crew_ai/main.py",
    "content": "#!/usr/bin/env python\nimport sys\nimport warnings\nfrom dotenv import load_dotenv\n\n# Remove or comment out these debug lines\n# import litellm\n# litellm._turn_on_debug()\n\n# Add this line to suppress the warning\nwarnings.filterwarnings(\"ignore\", message=\".*not a Python type.*\")\n# Keep your existing warning filter\nwarnings.filterwarnings(\"ignore\", category=SyntaxWarning, module=\"pysbd\")\n\nfrom gmail_crew_ai.crew import GmailCrewAi\n\ndef run():\n    \"\"\"Run the Gmail Crew AI.\"\"\"\n    try:\n        # Load environment variables\n        load_dotenv()\n        \n        # Get user input for number of emails to process\n        try:\n            email_limit = input(\"How many emails would you like to process? (default: 5): \")\n            if email_limit.strip() == \"\":\n                email_limit = 5\n            else:\n                email_limit = int(email_limit)\n                if email_limit <= 0:\n                    print(\"Number must be positive. Using default of 5.\")\n                    email_limit = 5\n        except ValueError:\n            print(\"Invalid input. Using default of 5 emails.\")\n            email_limit = 5\n        \n        print(f\"Processing {email_limit} emails...\")\n        \n        # Create and run the crew with the specified email limit\n        result = GmailCrewAi().crew().kickoff(inputs={'email_limit': email_limit})\n        \n        # Check if result is empty or None\n        if not result:\n            print(\"\\nNo emails were processed. Inbox might be empty.\")\n            return 0\n            \n        # Print the result in a clean way\n        if result:\n            print(\"\\nCrew execution completed successfully! 🎉\")\n            print(\"Results have been saved to the output directory.\")\n            return 0  # Return success code\n        else:\n            print(\"\\nCrew execution completed but no results were returned.\")\n            return 0  # Still consider this a success\n    except Exception as e:\n        print(f\"\\nError: {e}\")\n        return 1  # Return error code\n\nif __name__ == \"__main__\":\n    sys.exit(run())  # Use the return value as the exit code\n"
  },
  {
    "path": "src/gmail_crew_ai/models.py",
    "content": "from pydantic import BaseModel, Field, SkipValidation\nfrom typing import List, Optional, Dict, Literal, Callable, Any\nfrom datetime import datetime\n\nclass EmailDetails(BaseModel):\n    \"\"\"Model for email details.\"\"\"\n    email_id: Optional[str] = Field(None, description=\"Email ID\")\n    subject: Optional[str] = Field(None, description=\"Email subject\")\n    sender: Optional[str] = Field(None, description=\"Email sender\")\n    body: Optional[str] = Field(None, description=\"Email body\")\n    date: Optional[str] = Field(None, description=\"Email date (YYYY-MM-DD)\")\n    age_days: Optional[int] = Field(None, description=\"Age of the email in days from today\")\n    thread_info: Optional[Dict[str, Any]] = Field(None, description=\"Thread information\")\n    is_part_of_thread: Optional[bool] = Field(False, description=\"Whether this email is part of a thread\")\n    thread_size: Optional[int] = Field(1, description=\"Number of emails in this thread\")\n    thread_position: Optional[int] = Field(1, description=\"Position of this email in the thread (1 = first)\")\n\n    @classmethod\n    def from_email_tuple(cls, email_tuple):\n        \"\"\"Create an EmailDetails from an email tuple.\"\"\"\n        if not email_tuple or len(email_tuple) < 5:\n            return cls(email_id=None, subject=None)\n        \n        subject, sender, body, email_id, thread_info = email_tuple\n        \n        # Extract date from thread_info\n        date = \"\"\n        if isinstance(thread_info, dict) and 'date' in thread_info:\n            date = thread_info['date']\n            \n        return cls(\n            email_id=email_id,\n            subject=subject,\n            sender=sender,\n            body=body,\n            date=date,\n            thread_info=thread_info\n        )\n\n# Define the valid categories, priorities, and actions as type aliases\nEmailCategoryType = Literal[\"NEWSLETTERS\", \"PROMOTIONS\", \"PERSONAL\", \"GITHUB\", \n                           \"SPONSORSHIPS\", \"RECRUITMENT\", \"COLD_EMAIL\", \n                           \"EVENT_INVITATIONS\", \"RECEIPTS_INVOICES\", \"YOUTUBE\", \"SOCIALS\"]\n\nEmailPriorityType = Literal[\"HIGH\", \"MEDIUM\", \"LOW\"]\n\nEmailActionType = Literal[\"REPLY\", \"READ_ONLY\", \"TASK\", \"IGNORE\"]\n\nclass CategorizedEmail(BaseModel):\n    \"\"\"Model for categorized email information.\"\"\"\n    email_id: str = Field(..., description=\"Unique identifier for the email\")\n    subject: str = Field(..., description=\"Email subject line\")\n    sender: str = Field(..., description=\"Email sender (name and address)\")\n    date: str = Field(..., description=\"Email date in YYYY-MM-DD format\")\n    category: EmailCategoryType = Field(..., description=\"Category of the email\")\n    priority: EmailPriorityType = Field(..., description=\"Priority level of the email\")\n    required_action: EmailActionType = Field(..., description=\"Required action for the email\")\n    reason: str = Field(..., description=\"Reason for the categorization\")\n    due_date: Optional[str] = Field(None, description=\"Due date for action if applicable\")\n    thread_info: Optional[Dict] = Field(default=None, description=\"Thread information for replies\")\n\nclass OrganizedEmail(BaseModel):\n    \"\"\"Model for organized email information.\"\"\"\n    email_id: str = Field(..., description=\"Unique identifier for the email\")\n    subject: str = Field(..., description=\"Email subject line\")\n    applied_labels: List[str] = Field(default_factory=list, description=\"Labels applied to the email\")\n    starred: bool = Field(default=False, description=\"Whether the email was starred\")\n    result: str = Field(..., description=\"Result of the organization attempt\")\n\nclass EmailResponse(BaseModel):\n    \"\"\"Model for email response information.\"\"\"\n    email_id: str = Field(..., description=\"Unique identifier for the email\")\n    subject: str = Field(..., description=\"Email subject line\")\n    recipient: str = Field(..., description=\"Email recipient\")\n    response_summary: str = Field(..., description=\"Summary of the response\")\n    response_needed: bool = Field(..., description=\"Whether a response was needed\")\n    draft_saved: bool = Field(default=False, description=\"Whether a draft was saved\")\n\nclass SlackNotification(BaseModel):\n    \"\"\"Model for Slack notification information.\"\"\"\n    email_id: str = Field(..., description=\"Unique identifier for the email\")\n    subject: str = Field(..., description=\"Email subject line\")\n    sender: str = Field(..., description=\"Email sender\")\n    category: EmailCategoryType = Field(..., description=\"Category of the email\")\n    priority: EmailPriorityType = Field(..., description=\"Priority level of the email\")\n    summary: str = Field(..., description=\"Brief summary of the email content\")\n    action_needed: Optional[str] = Field(None, description=\"Action needed, if any\")\n    headline: str = Field(..., description=\"Custom headline for the notification\")\n    intro: str = Field(..., description=\"Custom intro phrase for the notification\")\n    action_header: Optional[str] = Field(None, description=\"Custom header for the action section\")\n    notification_sent: bool = Field(default=False, description=\"Whether notification was sent\")\n\nclass EmailCleanupInfo(BaseModel):\n    \"\"\"Model for email cleanup information.\"\"\"\n    email_id: str = Field(..., description=\"Unique identifier for the email\")\n    subject: str = Field(..., description=\"Email subject line\")\n    sender: str = Field(..., description=\"Email sender\")\n    age_days: int = Field(..., description=\"Age of the email in days\")\n    deleted: bool = Field(..., description=\"Whether the email was deleted\")\n    reason: str = Field(..., description=\"Reason for deletion or preservation\")\n\nclass SimpleCategorizedEmail(BaseModel):\n    \"\"\"Simplified model for debugging.\"\"\"\n    email_id: Optional[str] = Field(None, description=\"Unique identifier for the email\")\n    subject: Optional[str] = Field(None, description=\"Email subject line\")\n    sender: Optional[str] = Field(None, description=\"Email sender address\")\n    category: Optional[str] = Field(None, description=\"Category of the email\")\n    priority: Optional[str] = Field(None, description=\"Priority level of the email\")\n    required_action: Optional[str] = Field(None, description=\"Required action for the email\")\n    date: Optional[str] = Field(None, description=\"Date the email was received (YYYY-MM-DD)\")\n    age_days: Optional[int] = Field(None, description=\"Age of the email in days from today\")\n\n    # Update the from_email_tuple method to include date\n    @classmethod\n    def from_email_tuple(cls, email_tuple):\n        \"\"\"Create a SimpleCategorizedEmail from an email tuple.\"\"\"\n        if not email_tuple or len(email_tuple) < 5:\n            return cls(email_id=None, subject=None)\n        \n        subject, sender, body, email_id, thread_info = email_tuple\n        \n        # Extract date from body or thread_info\n        date = \"\"\n        if isinstance(thread_info, dict) and 'date' in thread_info:\n            date = thread_info['date']\n        elif body and body.startswith(\"EMAIL DATE:\"):\n            date_line = body.split(\"\\n\")[0]\n            date = date_line.replace(\"EMAIL DATE:\", \"\").strip()\n        \n        return cls(\n            email_id=email_id,\n            subject=subject,\n            sender=sender,\n            date=date\n        )\n"
  },
  {
    "path": "src/gmail_crew_ai/tools/__init__.py",
    "content": ""
  },
  {
    "path": "src/gmail_crew_ai/tools/date_tools.py",
    "content": "from datetime import datetime, date\nfrom typing import Optional\nfrom pydantic import BaseModel, Field\nfrom crewai.tools import BaseTool\nimport time\n\nclass DateCalculationSchema(BaseModel):\n    \"\"\"Schema for DateCalculationTool input.\"\"\"\n    email_date: str = Field(..., description=\"Email date in ISO format (YYYY-MM-DD)\")\n    # Remove reference_date from schema to prevent agent from using it\n    # reference_date: Optional[str] = Field(None, description=\"Reference date in ISO format (YYYY-MM-DD). Defaults to today.\")\n\nclass DateCalculationTool(BaseTool):\n    \"\"\"Tool to calculate the age of an email in days.\"\"\"\n    name: str = \"calculate_email_age\"\n    description: str = \"Calculate how many days old an email is compared to today's date\"\n    args_schema: type[BaseModel] = DateCalculationSchema\n\n    def _run(self, email_date: str, reference_date: Optional[str] = None) -> str:\n        \"\"\"Calculate the age of an email in days compared to today.\n        \n        Args:\n            email_date: The date of the email in YYYY-MM-DD format\n            \n        Returns:\n            A string with the email age information\n        \"\"\"\n        try:\n            # Parse the email date\n            email_date_obj = datetime.strptime(email_date, \"%Y-%m-%d\").date()\n            \n            # Always use today's date - ignore any provided reference_date\n            today = date.today()\n            \n            # Calculate the age in days\n            age_days = (today - email_date_obj).days\n            \n            # Create the response\n            response = f\"Email age: {age_days} days from today ({today})\\n\"\n            response += f\"Email date: {email_date_obj}\\n\"\n            response += f\"- Less than 5 days old: {'Yes' if age_days < 5 else 'No'}\\n\"\n            response += f\"- Older than 7 days: {'Yes' if age_days > 7 else 'No'}\\n\"\n            response += f\"- Older than 10 days: {'Yes' if age_days > 10 else 'No'}\\n\"\n            response += f\"- Older than 14 days: {'Yes' if age_days > 14 else 'No'}\\n\"\n            response += f\"- Older than 30 days: {'Yes' if age_days > 30 else 'No'}\\n\"\n            \n            return response\n            \n        except Exception as e:\n            return f\"Error calculating email age: {str(e)}\" "
  },
  {
    "path": "src/gmail_crew_ai/tools/gmail_tools.py",
    "content": "import imaplib\nimport email\nfrom email.header import decode_header\nfrom typing import List, Tuple, Literal, Optional, Type, Dict, Any\nimport re\nfrom bs4 import BeautifulSoup\nfrom crewai.tools import BaseTool\nimport os\nfrom pydantic import BaseModel, Field\nfrom crewai.tools import tool\nimport time\nfrom email.mime.multipart import MIMEMultipart\nfrom email.mime.text import MIMEText\nimport base64\n\ndef decode_header_safe(header):\n    \"\"\"\n    Safely decode email headers that might contain encoded words or non-ASCII characters.\n    \"\"\"\n    if not header:\n        return \"\"\n    \n    try:\n        decoded_parts = []\n        for decoded_str, charset in decode_header(header):\n            if isinstance(decoded_str, bytes):\n                if charset:\n                    decoded_parts.append(decoded_str.decode(charset or 'utf-8', errors='replace'))\n                else:\n                    decoded_parts.append(decoded_str.decode('utf-8', errors='replace'))\n            else:\n                decoded_parts.append(str(decoded_str))\n        return ' '.join(decoded_parts)\n    except Exception as e:\n        # Fallback to raw header if decoding fails\n        return str(header)\n\ndef clean_email_body(email_body: str) -> str:\n    \"\"\"\n    Clean the email body by removing HTML tags and excessive whitespace.\n    \"\"\"\n    try:\n        soup = BeautifulSoup(email_body, \"html.parser\")\n        text = soup.get_text(separator=\" \")  # Get text with spaces instead of <br/>\n    except Exception as e:\n        print(f\"Error parsing HTML: {e}\")\n        text = email_body  # Fallback to raw body if parsing fails\n\n    # Remove excessive whitespace and newlines\n    text = re.sub(r'\\s+', ' ', text).strip()\n    return text\n\nclass GmailToolBase(BaseTool):\n    \"\"\"Base class for Gmail tools, handling connection and credentials.\"\"\"\n    \n    class Config:\n        arbitrary_types_allowed = True\n\n    email_address: Optional[str] = Field(None, description=\"Gmail email address\")\n    app_password: Optional[str] = Field(None, description=\"Gmail app password\")\n\n    def __init__(self, description: str = \"\"):\n        super().__init__(description=description)\n        self.email_address = os.environ.get(\"EMAIL_ADDRESS\")\n        self.app_password = os.environ.get(\"APP_PASSWORD\")\n\n        if not self.email_address or not self.app_password:\n            raise ValueError(\"EMAIL_ADDRESS and APP_PASSWORD must be set in the environment.\")\n\n    def _connect(self):\n        \"\"\"Connect to Gmail.\"\"\"\n        try:\n            print(f\"Connecting to Gmail with email: {self.email_address[:3]}...{self.email_address[-8:]}\")\n            mail = imaplib.IMAP4_SSL(\"imap.gmail.com\")\n            mail.login(self.email_address, self.app_password)\n            print(\"Successfully logged in to Gmail\")\n            return mail\n        except Exception as e:\n            print(f\"Error connecting to Gmail: {e}\")\n            raise e\n\n    def _disconnect(self, mail):\n        \"\"\"Disconnect from Gmail.\"\"\"\n        try:\n            mail.close()\n            mail.logout()\n            print(\"Successfully disconnected from Gmail\")\n        except:\n            pass\n\n    def _get_thread_messages(self, mail: imaplib.IMAP4_SSL, msg) -> List[str]:\n        \"\"\"Get all messages in the thread by following References and In-Reply-To headers.\"\"\"\n        thread_messages = []\n        \n        # Get message IDs from References and In-Reply-To headers\n        references = msg.get(\"References\", \"\").split()\n        in_reply_to = msg.get(\"In-Reply-To\", \"\").split()\n        message_ids = list(set(references + in_reply_to))  # Remove duplicates\n        \n        if message_ids:\n            # Search for messages with these Message-IDs\n            search_criteria = ' OR '.join(f'HEADER MESSAGE-ID \"{mid}\"' for mid in message_ids)\n            result, data = mail.search(None, search_criteria)\n            \n            if result == \"OK\":\n                thread_ids = data[0].split()\n                for thread_id in thread_ids:\n                    result, msg_data = mail.fetch(thread_id, \"(RFC822)\")\n                    if result == \"OK\":\n                        thread_msg = email.message_from_bytes(msg_data[0][1])\n                        # Extract body from thread message\n                        thread_body = self._extract_body(thread_msg)\n                        thread_messages.append(thread_body)\n        \n        return thread_messages\n\n    def _extract_body(self, msg) -> str:\n        \"\"\"Extract body from an email message.\"\"\"\n        body = \"\"\n        if msg.is_multipart():\n            for part in msg.walk():\n                content_type = part.get_content_type()\n                content_disposition = str(part.get(\"Content-Disposition\"))\n\n                try:\n                    email_body = part.get_payload(decode=True).decode()\n                except:\n                    email_body = \"\"\n\n                if content_type == \"text/plain\" and \"attachment\" not in content_disposition:\n                    body += email_body\n                elif content_type == \"text/html\" and \"attachment\" not in content_disposition:\n                    body += clean_email_body(email_body)\n        else:\n            try:\n                body = clean_email_body(msg.get_payload(decode=True).decode())\n            except Exception as e:\n                body = f\"Error decoding body: {e}\"\n        return body\n\nclass GetUnreadEmailsSchema(BaseModel):\n    \"\"\"Schema for GetUnreadEmailsTool input.\"\"\"\n    limit: Optional[int] = Field(\n        default=5,\n        description=\"Maximum number of unread emails to retrieve. Defaults to 5.\",\n        ge=1  # Ensures the limit is greater than or equal to 1\n    )\n\nclass GetUnreadEmailsTool(GmailToolBase):\n    \"\"\"Tool to get unread emails from Gmail.\"\"\"\n    name: str = \"get_unread_emails\"\n    description: str = \"Gets unread emails from Gmail\"\n    args_schema: Type[BaseModel] = GetUnreadEmailsSchema\n    \n    def _run(self, limit: Optional[int] = 5) -> List[Tuple[str, str, str, str, Dict]]:\n        mail = self._connect()\n        try:\n            print(\"DEBUG: Connecting to Gmail...\")\n            mail.select(\"INBOX\")\n            result, data = mail.search(None, 'UNSEEN')\n            \n            print(f\"DEBUG: Search result: {result}\")\n            \n            if result != \"OK\":\n                print(\"DEBUG: Error searching for unseen emails\")\n                return []\n            \n            email_ids = data[0].split()\n            print(f\"DEBUG: Found {len(email_ids)} unread emails\")\n            \n            if not email_ids:\n                print(\"DEBUG: No unread emails found.\")\n                return []\n            \n            email_ids = list(reversed(email_ids))\n            email_ids = email_ids[:limit]\n            print(f\"DEBUG: Processing {len(email_ids)} emails\")\n            \n            emails = []\n            for i, email_id in enumerate(email_ids):\n                print(f\"DEBUG: Processing email {i+1}/{len(email_ids)}\")\n                result, msg_data = mail.fetch(email_id, \"(RFC822)\")\n                if result != \"OK\":\n                    print(f\"Error fetching email {email_id}:\", result)\n                    continue\n\n                raw_email = msg_data[0][1]\n                msg = email.message_from_bytes(raw_email)\n\n                # Decode headers properly (handles encoded characters)\n                subject = decode_header_safe(msg[\"Subject\"])\n                sender = decode_header_safe(msg[\"From\"])\n                \n                # Extract and standardize the date\n                date_str = msg.get(\"Date\", \"\")\n                received_date = self._parse_email_date(date_str)\n                \n                # Get the current message body\n                current_body = self._extract_body(msg)\n                \n                # Get thread messages\n                thread_messages = self._get_thread_messages(mail, msg)\n                \n                # Combine current message with thread history\n                full_body = \"\\n\\n--- Previous Messages ---\\n\".join([current_body] + thread_messages)\n\n                # Get thread metadata\n                thread_info = {\n                    'message_id': msg.get('Message-ID', ''),\n                    'in_reply_to': msg.get('In-Reply-To', ''),\n                    'references': msg.get('References', ''),\n                    'date': received_date,  # Use standardized date\n                    'raw_date': date_str,   # Keep original date string\n                    'email_id': email_id.decode('utf-8')\n                }\n\n                # Add a clear date indicator in the body for easier extraction\n                full_body = f\"EMAIL DATE: {received_date}\\n\\n{full_body}\"\n                \n                # Print the structure of what we're appending\n                print(f\"DEBUG: Email tuple structure: subject={subject}, sender={sender}, body_length={len(full_body)}, email_id={email_id.decode('utf-8')}, thread_info_keys={thread_info.keys()}\")\n                \n                emails.append((subject, sender, full_body, email_id.decode('utf-8'), thread_info))\n            \n            print(f\"DEBUG: Returning {len(emails)} email tuples\")\n            return emails\n        except Exception as e:\n            print(f\"DEBUG: Exception in GetUnreadEmailsTool: {e}\")\n            import traceback\n            traceback.print_exc()\n            return []\n        finally:\n            self._disconnect(mail)\n\n    def _parse_email_date(self, date_str: str) -> str:\n        \"\"\"\n        Parse email date string into a standardized format.\n        Returns ISO format date string (YYYY-MM-DD) or empty string if parsing fails.\n        \"\"\"\n        if not date_str:\n            return \"\"\n        \n        try:\n            # Try various date formats commonly found in emails\n            # Remove timezone name if present (like 'EDT', 'PST')\n            date_str = re.sub(r'\\s+\\([A-Z]{3,4}\\)', '', date_str)\n            \n            # Parse with email.utils\n            parsed_date = email.utils.parsedate_to_datetime(date_str)\n            if parsed_date:\n                return parsed_date.strftime(\"%Y-%m-%d\")\n        except Exception as e:\n            print(f\"Error parsing date '{date_str}': {e}\")\n        \n        return \"\"\n\nclass SaveDraftSchema(BaseModel):\n    \"\"\"Schema for SaveDraftTool input.\"\"\"\n    subject: str = Field(..., description=\"Email subject\")\n    body: str = Field(..., description=\"Email body content\")\n    recipient: str = Field(..., description=\"Recipient email address\")\n    thread_info: Optional[Dict[str, Any]] = Field(None, description=\"Thread information for replies\")\n\nclass SaveDraftTool(BaseTool):\n    \"\"\"Tool to save an email as a draft using IMAP.\"\"\"\n    name: str = \"save_email_draft\"\n    description: str = \"Saves an email as a draft in Gmail\"\n    args_schema: Type[BaseModel] = SaveDraftSchema\n\n    def _format_body(self, body: str) -> str:\n        \"\"\"Format the email body with signature.\"\"\"\n        # Replace [Your name] or [Your Name] with Tony Kipkemboi\n        body = re.sub(r'\\[Your [Nn]ame\\]', 'Tony Kipkemboi', body)\n        \n        # If no placeholder was found, append the signature\n        if '[Your' not in body and '[your' not in body:\n            body = f\"{body}\\n\\nBest regards,\\nTony Kipkemboi\"\n        \n        return body\n\n    def _connect(self):\n        \"\"\"Connect to Gmail using IMAP.\"\"\"\n        # Get email credentials from environment\n        email_address = os.environ.get('EMAIL_ADDRESS')\n        app_password = os.environ.get('APP_PASSWORD')\n        \n        if not email_address or not app_password:\n            raise ValueError(\"EMAIL_ADDRESS or APP_PASSWORD environment variables not set\")\n        \n        # Connect to Gmail's IMAP server\n        mail = imaplib.IMAP4_SSL('imap.gmail.com')\n        print(f\"Connecting to Gmail with email: {email_address[:3]}...{email_address[-10:]}\")\n        mail.login(email_address, app_password)\n        return mail, email_address\n\n    def _disconnect(self, mail):\n        \"\"\"Disconnect from Gmail.\"\"\"\n        try:\n            mail.logout()\n        except:\n            pass\n\n    def _check_drafts_folder(self, mail):\n        \"\"\"Check available mailboxes to find the drafts folder.\"\"\"\n        print(\"Checking available mailboxes...\")\n        result, mailboxes = mail.list()\n        if result == 'OK':\n            drafts_folders = []\n            for mailbox in mailboxes:\n                if b'Drafts' in mailbox or b'Draft' in mailbox:\n                    drafts_folders.append(mailbox.decode())\n                    print(f\"Found drafts folder: {mailbox.decode()}\")\n            return drafts_folders\n        return []\n\n    def _verify_draft_saved(self, mail, subject, recipient):\n        \"\"\"Verify if the draft was actually saved by searching for it.\"\"\"\n        try:\n            # Try different drafts folder names\n            drafts_folders = [\n                '\"[Gmail]/Drafts\"', \n                'Drafts',\n                'DRAFTS',\n                '\"[Google Mail]/Drafts\"',\n                '[Gmail]/Drafts'\n            ]\n            \n            for folder in drafts_folders:\n                try:\n                    print(f\"Checking folder: {folder}\")\n                    result, _ = mail.select(folder, readonly=True)\n                    if result != 'OK':\n                        continue\n                        \n                    # Search for drafts with this subject\n                    search_criteria = f'SUBJECT \"{subject}\"'\n                    result, data = mail.search(None, search_criteria)\n                    \n                    if result == 'OK' and data[0]:\n                        draft_count = len(data[0].split())\n                        print(f\"Found {draft_count} drafts matching subject '{subject}' in folder {folder}\")\n                        return True, folder\n                    else:\n                        print(f\"No drafts found matching subject '{subject}' in folder {folder}\")\n                except Exception as e:\n                    print(f\"Error checking folder {folder}: {e}\")\n                    continue\n                    \n            return False, None\n        except Exception as e:\n            print(f\"Error verifying draft: {e}\")\n            return False, None\n\n    def _run(self, subject: str, body: str, recipient: str, thread_info: Optional[Dict[str, Any]] = None) -> str:\n        try:\n            mail, email_address = self._connect()\n            \n            # Check available drafts folders\n            drafts_folders = self._check_drafts_folder(mail)\n            print(f\"Available drafts folders: {drafts_folders}\")\n            \n            # Try with quoted folder name first\n            drafts_folder = '\"[Gmail]/Drafts\"'\n            print(f\"Selecting drafts folder: {drafts_folder}\")\n            result, _ = mail.select(drafts_folder)\n            \n            # If that fails, try without quotes\n            if result != 'OK':\n                drafts_folder = '[Gmail]/Drafts'\n                print(f\"First attempt failed. Trying: {drafts_folder}\")\n                result, _ = mail.select(drafts_folder)\n                \n            # If that also fails, try just 'Drafts'\n            if result != 'OK':\n                drafts_folder = 'Drafts'\n                print(f\"Second attempt failed. Trying: {drafts_folder}\")\n                result, _ = mail.select(drafts_folder)\n                \n            if result != 'OK':\n                return f\"Error: Could not select drafts folder. Available folders: {drafts_folders}\"\n                \n            print(f\"Successfully selected drafts folder: {drafts_folder}\")\n            \n            # Format body and add signature\n            body_with_signature = self._format_body(body)\n            \n            # Create the email message\n            message = email.message.EmailMessage()\n            message[\"From\"] = email_address\n            message[\"To\"] = recipient\n            message[\"Subject\"] = subject\n            message.set_content(body_with_signature)\n            print(f\"Created message with subject: {subject}\")\n\n            # Add thread headers if this is a reply\n            if thread_info:\n                # References header should include all previous message IDs\n                references = []\n                if thread_info.get('references'):\n                    references.extend(thread_info['references'].split())\n                if thread_info.get('message_id'):\n                    references.append(thread_info['message_id'])\n                \n                if references:\n                    message[\"References\"] = \" \".join(references)\n                \n                # In-Reply-To should point to the immediate parent message\n                if thread_info.get('message_id'):\n                    message[\"In-Reply-To\"] = thread_info['message_id']\n\n                # Make sure subject has \"Re: \" prefix\n                if not subject.lower().startswith('re:'):\n                    message[\"Subject\"] = f\"Re: {subject}\"\n                    \n                print(f\"Added thread information for reply\")\n\n            # Save to drafts\n            print(f\"Attempting to save draft to {drafts_folder}...\")\n            date = imaplib.Time2Internaldate(time.time())\n            result, data = mail.append(drafts_folder, '\\\\Draft', date, message.as_bytes())\n            \n            if result != 'OK':\n                return f\"Error saving draft: {result}, {data}\"\n                \n            print(f\"Draft save attempt result: {result}\")\n            \n            # Verify the draft was actually saved\n            verified, folder = self._verify_draft_saved(mail, subject, recipient)\n            \n            if verified:\n                return f\"VERIFIED: Draft email saved with subject: '{subject}' in folder {folder}\"\n            else:\n                # Try Gmail's API approach as a fallback\n                try:\n                    # Try saving directly to All Mail and flagging as draft\n                    result, data = mail.append('[Gmail]/All Mail', '\\\\Draft', date, message.as_bytes())\n                    if result == 'OK':\n                        return f\"Draft saved to All Mail with subject: '{subject}' (flagged as draft)\"\n                    else:\n                        return f\"WARNING: Draft save attempt returned {result}, but verification failed. Please check your Gmail Drafts folder.\"\n                except Exception as e:\n                    return f\"WARNING: Draft may not have been saved properly: {str(e)}\"\n\n        except Exception as e:\n            return f\"Error saving draft: {str(e)}\"\n        finally:\n            self._disconnect(mail)\n\nclass GmailOrganizeSchema(BaseModel):\n    \"\"\"Schema for GmailOrganizeTool input.\"\"\"\n    email_id: str = Field(..., description=\"Email ID to organize\")\n    category: str = Field(..., description=\"Category assigned by agent (Urgent/Response Needed/etc)\")\n    priority: str = Field(..., description=\"Priority level (High/Medium/Low)\")\n    should_star: bool = Field(default=False, description=\"Whether to star the email\")\n    labels: List[str] = Field(default_list=[], description=\"Labels to apply\")\n\nclass GmailOrganizeTool(GmailToolBase):\n    \"\"\"Tool to organize emails based on agent categorization.\"\"\"\n    name: str = \"organize_email\"\n    description: str = \"Organizes emails using Gmail's priority features based on category and priority\"\n    args_schema: Type[BaseModel] = GmailOrganizeSchema\n\n    def _run(self, email_id: str, category: str, priority: str, should_star: bool = False, labels: List[str] = None) -> str:\n        \"\"\"Organize an email with the specified parameters.\"\"\"\n        if labels is None:\n            # Provide a default empty list to avoid validation errors\n            labels = []\n        \n        print(f\"Organizing email {email_id} with category {category}, priority {priority}, star={should_star}, labels={labels}\")\n        \n        mail = self._connect()\n        try:\n            # Select inbox to ensure we can access the email\n            mail.select(\"INBOX\")\n            \n            # Apply organization based on category and priority\n            if category == \"Urgent Response Needed\" and priority == \"High\":\n                # Star the email\n                if should_star:\n                    mail.store(email_id, '+FLAGS', '\\\\Flagged')\n                \n                # Mark as important\n                mail.store(email_id, '+FLAGS', '\\\\Important')\n                \n                # Apply URGENT label if it doesn't exist\n                if \"URGENT\" not in labels:\n                    labels.append(\"URGENT\")\n\n            # Apply all specified labels\n            for label in labels:\n                try:\n                    # Create label if it doesn't exist\n                    mail.create(label)\n                except:\n                    pass  # Label might already exist\n                \n                # Apply label\n                mail.store(email_id, '+X-GM-LABELS', label)\n\n            return f\"Email organized: Starred={should_star}, Labels={labels}\"\n\n        except Exception as e:\n            return f\"Error organizing email: {e}\"\n        finally:\n            self._disconnect(mail)\n\nclass GmailDeleteSchema(BaseModel):\n    \"\"\"Schema for GmailDeleteTool input.\"\"\"\n    email_id: str = Field(..., description=\"Email ID to delete\")\n    reason: str = Field(..., description=\"Reason for deletion\")\n\nclass GmailDeleteTool(BaseTool):\n    \"\"\"Tool to delete an email using IMAP.\"\"\"\n    name: str = \"delete_email\"\n    description: str = \"Deletes an email from Gmail\"\n    \n    def _run(self, email_id: str, reason: str) -> str:\n        \"\"\"\n        Delete an email by ID.\n        Parameters:\n            email_id: The email ID to delete\n            reason: The reason for deletion (for logging)\n        \"\"\"\n        try:\n            # Validate inputs - Add this validation\n            if not email_id or not isinstance(email_id, str):\n                return f\"Error: Invalid email_id format: {email_id}\"\n            \n            if not reason or not isinstance(reason, str):\n                return f\"Error: Invalid reason format: {reason}\"\n                \n            mail = self._connect()\n            try:\n                mail.select(\"INBOX\")\n                \n                # First verify the email exists and get its details for logging\n                result, data = mail.fetch(email_id, \"(RFC822)\")\n                if result != \"OK\" or not data or data[0] is None:\n                    return f\"Error: Email with ID {email_id} not found\"\n                    \n                msg = email.message_from_bytes(data[0][1])\n                subject = decode_header_safe(msg[\"Subject\"])\n                sender = decode_header_safe(msg[\"From\"])\n                \n                # Move to Trash\n                mail.store(email_id, '+X-GM-LABELS', '\\\\Trash')\n                mail.store(email_id, '-X-GM-LABELS', '\\\\Inbox')\n                \n                return f\"Email deleted: '{subject}' from {sender}. Reason: {reason}\"\n            except Exception as e:\n                return f\"Error deleting email: {e}\"\n            finally:\n                self._disconnect(mail)\n\n        except Exception as e:\n            return f\"Error deleting email: {str(e)}\"\n\nclass EmptyTrashTool(BaseTool):\n    \"\"\"Tool to empty Gmail trash.\"\"\"\n    name: str = \"empty_gmail_trash\"\n    description: str = \"Empties the Gmail trash folder to free up space\"\n\n    def _connect(self):\n        \"\"\"Connect to Gmail using IMAP.\"\"\"\n        # Get email credentials from environment\n        email_address = os.environ.get('EMAIL_ADDRESS')\n        app_password = os.environ.get('APP_PASSWORD')\n        \n        if not email_address or not app_password:\n            raise ValueError(\"EMAIL_ADDRESS or APP_PASSWORD environment variables not set\")\n        \n        # Connect to Gmail's IMAP server\n        mail = imaplib.IMAP4_SSL('imap.gmail.com')\n        print(f\"Connecting to Gmail with email: {email_address[:3]}...{email_address[-10:]}\")\n        mail.login(email_address, app_password)\n        return mail\n\n    def _disconnect(self, mail):\n        \"\"\"Disconnect from Gmail.\"\"\"\n        try:\n            mail.logout()\n        except:\n            pass\n    \n    def _run(self) -> str:\n        \"\"\"Empty the Gmail trash folder.\"\"\"\n        try:\n            mail = self._connect()\n            \n            # Try different trash folder names (Gmail can have different naming conventions)\n            trash_folders = [\n                '\"[Gmail]/Trash\"',\n                '[Gmail]/Trash',\n                'Trash',\n                '\"[Google Mail]/Trash\"',\n                '[Google Mail]/Trash'\n            ]\n            \n            success = False\n            trash_folder_used = None\n            \n            for folder in trash_folders:\n                try:\n                    print(f\"Attempting to select trash folder: {folder}\")\n                    result, data = mail.select(folder)\n                    \n                    if result == 'OK':\n                        trash_folder_used = folder\n                        print(f\"Successfully selected trash folder: {folder}\")\n                        \n                        # Search for all messages in trash\n                        result, data = mail.search(None, 'ALL')\n                        \n                        if result == 'OK':\n                            email_ids = data[0].split()\n                            count = len(email_ids)\n                            \n                            if count == 0:\n                                print(\"No messages found in trash.\")\n                                return \"Trash is already empty. No messages to delete.\"\n                            \n                            print(f\"Found {count} messages in trash.\")\n                            \n                            # Delete all messages in trash\n                            for email_id in email_ids:\n                                mail.store(email_id, '+FLAGS', '\\\\Deleted')\n                            \n                            # Permanently remove messages marked for deletion\n                            mail.expunge()\n                            success = True\n                            break\n                        \n                except Exception as e:\n                    print(f\"Error accessing trash folder {folder}: {e}\")\n                    continue\n            \n            if success:\n                return f\"Successfully emptied Gmail trash folder ({trash_folder_used}). Deleted {count} messages.\"\n            else:\n                return \"Could not empty trash. No trash folder found or accessible.\"\n\n        except Exception as e:\n            return f\"Error emptying trash: {str(e)}\"\n        finally:\n            self._disconnect(mail)"
  },
  {
    "path": "src/gmail_crew_ai/tools/slack_tool.py",
    "content": "import os\nimport json\nimport requests\nfrom typing import List, Dict, Optional, Type\nfrom pydantic import BaseModel, Field\nfrom crewai.tools import BaseTool\n\nclass SlackNotificationSchema(BaseModel):\n    \"\"\"Schema for SlackNotificationTool input.\"\"\"\n    subject: str = Field(..., description=\"Email subject\")\n    sender: str = Field(..., description=\"Email sender\")\n    category: str = Field(..., description=\"Email category\")\n    priority: str = Field(..., description=\"Email priority\")\n    summary: str = Field(..., description=\"Brief summary of the email content\")\n    action_needed: Optional[str] = Field(None, description=\"Action needed, if any\")\n    headline: Optional[str] = Field(None, description=\"Custom headline for the notification\")\n    intro: Optional[str] = Field(None, description=\"Custom intro phrase for the notification\")\n    action_header: Optional[str] = Field(None, description=\"Custom header for the action section\")\n\nclass SlackNotificationTool(BaseTool):\n    \"\"\"Tool to send notifications to Slack.\"\"\"\n    name: str = \"slack_notification\"\n    description: str = \"Sends notifications about important emails to Slack\"\n    args_schema: Type[BaseModel] = SlackNotificationSchema\n    \n    class Config:\n        arbitrary_types_allowed = True\n    \n    def __init__(self):\n        super().__init__()\n        self._webhook_url = os.environ.get(\"SLACK_WEBHOOK_URL\")\n        if not self._webhook_url:\n            raise ValueError(\"SLACK_WEBHOOK_URL must be set in the environment.\")\n\n    def _run(self, subject: str, sender: str, category: str, \n             priority: str, summary: str, action_needed: Optional[str] = None,\n             headline: Optional[str] = None, intro: Optional[str] = None,\n             action_header: Optional[str] = None) -> str:\n        \"\"\"Send a notification to Slack.\"\"\"\n        \n        # Format the message\n        blocks = [\n            {\n                \"type\": \"header\",\n                \"text\": {\n                    \"type\": \"plain_text\",\n                    \"text\": headline or f\"Important Email: {subject}\"\n                }\n            }\n        ]\n        \n        # Add intro if provided\n        if intro:\n            blocks.append({\n                \"type\": \"section\",\n                \"text\": {\n                    \"type\": \"mrkdwn\",\n                    \"text\": f\"*{intro}*\"\n                }\n            })\n        \n        # Add email details\n        blocks.append({\n            \"type\": \"section\",\n            \"fields\": [\n                {\"type\": \"mrkdwn\", \"text\": f\"*From:*\\n{sender}\"},\n                {\"type\": \"mrkdwn\", \"text\": f\"*Category:*\\n{category}\"},\n                {\"type\": \"mrkdwn\", \"text\": f\"*Priority:*\\n{priority}\"},\n            ]\n        })\n        \n        # Add summary\n        blocks.append({\n            \"type\": \"section\",\n            \"text\": {\n                \"type\": \"mrkdwn\",\n                \"text\": f\"*Summary:*\\n{summary}\"\n            }\n        })\n        \n        # Add action needed if provided\n        if action_needed:\n            blocks.append({\n                \"type\": \"section\",\n                \"text\": {\n                    \"type\": \"mrkdwn\",\n                    \"text\": f\"*{action_header or 'Action Needed:'}*\\n{action_needed}\"\n                }\n            })\n        \n        # Add divider\n        blocks.append({\"type\": \"divider\"})\n        \n        # Prepare the payload\n        payload = {\n            \"blocks\": blocks\n        }\n        \n        # Send the notification\n        try:\n            response = requests.post(\n                self._webhook_url,\n                data=json.dumps(payload),\n                headers={\"Content-Type\": \"application/json\"}\n            )\n            response.raise_for_status()\n            return f\"Slack notification sent successfully for email: {subject}\"\n        except Exception as e:\n            return f\"Error sending Slack notification: {e}\" "
  }
]