This commit is contained in:
2026-04-07 17:42:53 -07:00
parent 433228c90e
commit d5ed3c1b52
7 changed files with 448 additions and 98 deletions

View File

@@ -1,9 +1,11 @@
from typing import List, Dict, Optional
from datetime import datetime, timedelta
import logging
import requests
from app.models import db, Folder, User, ProcessedEmail
from app.imap_service import IMAPService
from app.processed_emails_service import ProcessedEmailsService
from app.prompt_templates import build_destination_prompt
class EmailProcessor:
@@ -159,33 +161,68 @@ class EmailProcessor:
if resp_code != 'OK':
raise Exception(f"Failed to select folder {folder.name}: {content}")
# Process each email in the batch
processed_uids = []
# Get all enabled folders for this user (for AI context)
all_folders = Folder.query.filter_by(user_id=self.user.id, organize_enabled=True).all()
rules = [
{
'name': f.name,
'rule_text': f.rule_text,
'priority': f.priority
}
for f in all_folders
]
# Get email headers for all emails in batch
emails = []
valid_uids = [] # Track which UIDs we successfully fetched headers for
for email_uid in email_uids:
try:
# Get email headers to evaluate rules
headers = self.imap_service.get_email_headers(folder.name, email_uid)
if not headers:
if headers:
emails.append({
'uid': email_uid,
'headers': headers
})
valid_uids.append(email_uid)
else:
self.logger.warning(f"Could not get headers for email {email_uid} in folder {folder.name}")
result['error_count'] += 1
except Exception as e:
self.logger.error(f"Error getting headers for email {email_uid}: {str(e)}")
result['error_count'] += 1
# Skip AI call if no valid emails
if not emails:
return result
# Get destinations from AI
destinations = self.get_email_destinations(emails, rules)
# Process each email based on AI decision
processed_uids = []
for email in emails:
try:
email_uid = email['uid']
destination_folder = destinations.get(email_uid)
if not destination_folder:
self.logger.warning(f"No destination determined for email {email_uid}")
result['error_count'] += 1
continue
# Evaluate rules to determine destination
destination_folder = self._evaluate_rules(headers, folder.rule_text)
if destination_folder and destination_folder != folder.name:
# Move email to destination folder
if self._move_email(email_uid, folder.name, destination_folder):
processed_uids.append(email_uid)
result['processed_count'] += 1
else:
self.logger.error(f"Failed to move email {email_uid} from {folder.name} to {destination_folder}")
result['error_count'] += 1
else:
# Mark as processed (no move needed)
# Skip if destination is same as current folder or INBOX (no move needed)
if destination_folder.lower() == 'inbox' or destination_folder == folder.name:
processed_uids.append(email_uid)
result['processed_count'] += 1
continue
# Move email to destination folder
if self._move_email(email_uid, folder.name, destination_folder):
processed_uids.append(email_uid)
result['processed_count'] += 1
else:
self.logger.error(f"Failed to move email {email_uid} from {folder.name} to {destination_folder}")
result['error_count'] += 1
except Exception as e:
self.logger.error(f"Error processing email {email_uid}: {str(e)}")
@@ -218,40 +255,55 @@ class EmailProcessor:
return result
def _evaluate_rules(self, headers: Dict[str, str], rule_text: Optional[str]) -> Optional[str]:
def get_email_destinations(self, emails: List[Dict], rules: List[Dict]) -> Dict[str, str]:
"""
Evaluate rules against email headers to determine destination folder.
Get destination folders for a batch of emails using AI completion API.
Args:
headers: Email headers (subject, from, to, date)
rule_text: Rule text defining processing criteria
emails: List of email dictionaries containing uid and headers
rules: List of folder rules with name, rule_text, and priority
Returns:
Destination folder name or None if no move is needed
Dictionary mapping email UID to destination folder name
"""
# This is a simplified implementation - in a real app, this would parse
# the rule_text and evaluate it against the headers
# For now, we'll implement a simple rule evaluation
destinations = {}
if not rule_text:
return None
# Get API configuration from environment or config
api_url = self.user.get_setting('OPENAI_BASE_URL', 'http://localhost:1234/v1/completions')
api_key = self.user.get_setting('OPENAI_API_KEY', 'dummy-key')
# Simple example: if rule contains 'move to', extract destination
# This should be replaced with proper rule parsing in production
rule_lower = rule_text.lower()
if 'move to' in rule_lower:
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {api_key}'
}
for email in emails:
try:
start_idx = rule_lower.find('move to') + 7 # length of 'move to'
# Extract folder name (everything after 'move to' until end or punctuation)
destination = rule_text[start_idx:].strip()
# Remove any trailing punctuation
destination = destination.rstrip('.!,;:')
return destination.strip()
except:
pass
# Default: no move needed
return None
# Build prompt using template
prompt = build_destination_prompt(email['headers'], rules)
payload = {
'prompt': prompt,
'temperature': 0.7,
'max_tokens': 50
}
response = requests.post(f'{api_url}', json=payload, headers=headers, timeout=30)
if response.status_code == 200:
# Extract folder name from response (should be just the folder name)
destination = response.text.strip()
destinations[email['uid']] = destination
else:
self.logger.error(f"AI API request failed: {response.status_code} - {response.text}")
# On error, don't assign a destination (will be handled by caller)
except Exception as e:
self.logger.error(f"Error calling AI API for email {email['uid']}: {str(e)}")
# Continue with other emails
continue
return destinations
def _move_email(self, email_uid: str, source_folder: str, destination_folder: str) -> bool:
"""

46
app/prompt_templates.py Normal file
View File

@@ -0,0 +1,46 @@
from typing import List, Dict
def build_destination_prompt(email_headers: Dict[str, str], rules: List[Dict[str, any]]) -> str:
"""
Build a prompt for determining the best destination folder for an email.
Args:
email_headers: Dictionary containing email headers (subject, from, to, date)
rules: List of dictionaries containing folder rules with name, rule_text, and priority
Returns:
Formatted prompt string
"""
# Sort rules by priority (highest first) for consistent ordering
sorted_rules = sorted(rules, key=lambda x: x['priority'], reverse=True)
prompt = '''Determine the best destination folder for the following email based on the user's folder rules and priorities.
Email Details:
- Subject: {subject}
- From: {from_email}
- To: {to_email}
- Date: {date}
Available Folders (listed in priority order - higher priority folders should be preferred):
{folder_rules}
Instructions:
1. Review the email details and all folder rules
2. Select the SINGLE most appropriate destination folder based on the rules and priorities
3. If none of the folders are appropriate, respond with "INBOX" (keep in current location)
4. Respond ONLY with the exact folder name - nothing else
Best destination folder: '''.format(
subject=email_headers.get('subject', 'N/A'),
from_email=email_headers.get('from', 'N/A'),
to_email=email_headers.get('to', 'N/A'),
date=email_headers.get('date', 'N/A'),
folder_rules='\n'.join([
f"- {rule['name']} (Priority: {rule['priority']}): {rule['rule_text']}"
for rule in sorted_rules
])
)
return prompt

62
app/static/js/confetti.js Normal file
View File

@@ -0,0 +1,62 @@
document.addEventListener('DOMContentLoaded', function() {
// Initialize confetti effect on page load
initConfetti();
// Add confetti effect to welcome section button click
const welcomeButton = document.querySelector('.welcome-button');
if (welcomeButton) {
welcomeButton.addEventListener('click', function() {
createConfetti();
});
}
// Add confetti effect to any button with data-confetti attribute
document.querySelectorAll('[data-confetti]').forEach(button => {
button.addEventListener('click', function() {
createConfetti();
});
});
});
function initConfetti() {
// Create a splash confetti effect when the page loads
setTimeout(() => {
createConfetti();
}, 500);
}
function createConfetti() {
const confettiCount = 150;
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff'];
for (let i = 0; i < confettiCount; i++) {
const confetti = document.createElement('div');
confetti.style.position = 'fixed';
confetti.style.width = Math.random() * 10 + 5 + 'px';
confetti.style.height = Math.random() * 10 + 5 + 'px';
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
confetti.style.borderRadius = '50%';
confetti.style.zIndex = '9999';
confetti.style.left = Math.random() * 100 + 'vw';
confetti.style.top = '-10px';
document.body.appendChild(confetti);
// Animate confetti falling
const animationDuration = Math.random() * 3 + 2;
const targetX = (Math.random() - 0.5) * 100;
confetti.animate([
{ transform: 'translate(0, 0) rotate(0deg)', opacity: 1 },
{ transform: `translate(${targetX}vw, 100vh) rotate(${Math.random() * 360}deg)`, opacity: 0 }
], {
duration: animationDuration * 1000,
easing: 'cubic-bezier(0.1, 0.8, 0.2, 1)'
});
// Remove confetti after animation
setTimeout(() => {
confetti.remove();
}, animationDuration * 1000);
}
}