backend-lehrer (11 files): - llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6) - messenger_api.py (840 → 5), print_generator.py (824 → 5) - unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4) - llm_gateway/routes/edu_search_seeds.py (710 → 4) klausur-service (12 files): - ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4) - legal_corpus_api.py (790 → 4), page_crop.py (758 → 3) - mail/ai_service.py (747 → 4), github_crawler.py (767 → 3) - trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4) - dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4) website (6 pages): - audit-checklist (867 → 8), content (806 → 6) - screen-flow (790 → 4), scraper (789 → 5) - zeugnisse (776 → 5), modules (745 → 4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
197 lines
6.0 KiB
Python
197 lines
6.0 KiB
Python
"""
|
|
AI Email Analysis Service — Barrel Re-export
|
|
|
|
Split into:
|
|
- mail/ai_sender.py — Sender classification (domain + LLM)
|
|
- mail/ai_deadline.py — Deadline extraction (regex + LLM)
|
|
- mail/ai_category.py — Category classification + response suggestions
|
|
|
|
The AIEmailService class and get_ai_email_service() are defined here
|
|
to maintain the original public API.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, List, Tuple
|
|
from datetime import datetime
|
|
|
|
import httpx
|
|
|
|
from .models import (
|
|
EmailCategory,
|
|
SenderType,
|
|
TaskPriority,
|
|
SenderClassification,
|
|
DeadlineExtraction,
|
|
EmailAnalysisResult,
|
|
ResponseSuggestion,
|
|
get_priority_from_sender_type,
|
|
)
|
|
from .mail_db import update_email_ai_analysis
|
|
from .ai_sender import classify_sender, LLM_GATEWAY_URL
|
|
from .ai_deadline import extract_deadlines
|
|
from .ai_category import classify_category, suggest_response
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AIEmailService:
|
|
"""
|
|
AI-powered email analysis service.
|
|
|
|
Features:
|
|
- Domain-based sender classification (fast, no LLM)
|
|
- LLM-based sender classification (fallback)
|
|
- Deadline extraction using regex + LLM
|
|
- Category classification
|
|
- Response suggestions
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._http_client = None
|
|
|
|
async def get_http_client(self) -> httpx.AsyncClient:
|
|
"""Get or create HTTP client for LLM gateway."""
|
|
if self._http_client is None:
|
|
self._http_client = httpx.AsyncClient(timeout=30.0)
|
|
return self._http_client
|
|
|
|
async def classify_sender(
|
|
self,
|
|
sender_email: str,
|
|
sender_name: Optional[str] = None,
|
|
subject: Optional[str] = None,
|
|
body_preview: Optional[str] = None,
|
|
) -> SenderClassification:
|
|
"""Classify the sender of an email."""
|
|
client = await self.get_http_client()
|
|
return await classify_sender(
|
|
client, sender_email, sender_name, subject, body_preview
|
|
)
|
|
|
|
async def extract_deadlines(
|
|
self,
|
|
subject: str,
|
|
body_text: str,
|
|
) -> List[DeadlineExtraction]:
|
|
"""Extract deadlines from email content."""
|
|
client = await self.get_http_client()
|
|
return await extract_deadlines(client, subject, body_text)
|
|
|
|
async def classify_category(
|
|
self,
|
|
subject: str,
|
|
body_preview: str,
|
|
sender_type: SenderType,
|
|
) -> Tuple[EmailCategory, float]:
|
|
"""Classify email into a category."""
|
|
client = await self.get_http_client()
|
|
return await classify_category(client, subject, body_preview, sender_type)
|
|
|
|
async def analyze_email(
|
|
self,
|
|
email_id: str,
|
|
sender_email: str,
|
|
sender_name: Optional[str],
|
|
subject: str,
|
|
body_text: Optional[str],
|
|
body_preview: Optional[str],
|
|
) -> EmailAnalysisResult:
|
|
"""Run full analysis pipeline on an email."""
|
|
# 1. Classify sender
|
|
sender_classification = await self.classify_sender(
|
|
sender_email, sender_name, subject, body_preview
|
|
)
|
|
|
|
# 2. Extract deadlines
|
|
deadlines = await self.extract_deadlines(subject, body_text or "")
|
|
|
|
# 3. Classify category
|
|
category, category_confidence = await self.classify_category(
|
|
subject, body_preview or "", sender_classification.sender_type
|
|
)
|
|
|
|
# 4. Determine priority
|
|
suggested_priority = get_priority_from_sender_type(sender_classification.sender_type)
|
|
|
|
# Upgrade priority if deadlines are found
|
|
if deadlines:
|
|
nearest_deadline = min(d.deadline_date for d in deadlines)
|
|
days_until = (nearest_deadline - datetime.now()).days
|
|
|
|
if days_until <= 1:
|
|
suggested_priority = TaskPriority.URGENT
|
|
elif days_until <= 3:
|
|
suggested_priority = TaskPriority.HIGH
|
|
elif days_until <= 7:
|
|
suggested_priority = max(suggested_priority, TaskPriority.MEDIUM)
|
|
|
|
# 5. Summary (optional)
|
|
summary = None
|
|
|
|
# 6. Determine if task should be auto-created
|
|
auto_create_task = (
|
|
len(deadlines) > 0 or
|
|
sender_classification.sender_type in [
|
|
SenderType.KULTUSMINISTERIUM,
|
|
SenderType.LANDESSCHULBEHOERDE,
|
|
SenderType.RLSB,
|
|
]
|
|
)
|
|
|
|
# 7. Store analysis in database
|
|
await update_email_ai_analysis(
|
|
email_id=email_id,
|
|
category=category.value,
|
|
sender_type=sender_classification.sender_type.value,
|
|
sender_authority_name=sender_classification.authority_name,
|
|
detected_deadlines=[
|
|
{
|
|
"date": d.deadline_date.isoformat(),
|
|
"description": d.description,
|
|
"is_firm": d.is_firm,
|
|
}
|
|
for d in deadlines
|
|
],
|
|
suggested_priority=suggested_priority.value,
|
|
ai_summary=summary,
|
|
)
|
|
|
|
return EmailAnalysisResult(
|
|
email_id=email_id,
|
|
category=category,
|
|
category_confidence=category_confidence,
|
|
sender_classification=sender_classification,
|
|
deadlines=deadlines,
|
|
suggested_priority=suggested_priority,
|
|
summary=summary,
|
|
suggested_actions=[],
|
|
auto_create_task=auto_create_task,
|
|
)
|
|
|
|
async def suggest_response(
|
|
self,
|
|
subject: str,
|
|
body_text: str,
|
|
sender_type: SenderType,
|
|
category: EmailCategory,
|
|
) -> List[ResponseSuggestion]:
|
|
"""Generate response suggestions for an email."""
|
|
client = await self.get_http_client()
|
|
return await suggest_response(
|
|
client, subject, body_text, sender_type, category
|
|
)
|
|
|
|
|
|
# Global instance
|
|
_ai_service: Optional[AIEmailService] = None
|
|
|
|
|
|
def get_ai_email_service() -> AIEmailService:
|
|
"""Get or create the global AIEmailService instance."""
|
|
global _ai_service
|
|
|
|
if _ai_service is None:
|
|
_ai_service = AIEmailService()
|
|
|
|
return _ai_service
|