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:
"""