changes
This commit is contained in:
@@ -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
46
app/prompt_templates.py
Normal 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
62
app/static/js/confetti.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user