""" AI Email - Category Classification and Response Suggestions Rule-based and LLM-based email category classification, plus response suggestion generation. Extracted from ai_service.py to keep files under 500 LOC. """ import os import logging from typing import Optional, List, Tuple import httpx from .models import ( EmailCategory, SenderType, ResponseSuggestion, ) logger = logging.getLogger(__name__) # LLM Gateway configuration LLM_GATEWAY_URL = os.getenv("LLM_GATEWAY_URL", "http://localhost:8090") async def classify_category( http_client: httpx.AsyncClient, subject: str, body_preview: str, sender_type: SenderType, ) -> Tuple[EmailCategory, float]: """ Classify email into a category. Rule-based classification first, falls back to LLM. """ category, confidence = _classify_category_rules(subject, body_preview, sender_type) if confidence > 0.7: return category, confidence return await _classify_category_llm(http_client, subject, body_preview) def _classify_category_rules( subject: str, body_preview: str, sender_type: SenderType, ) -> Tuple[EmailCategory, float]: """Rule-based category classification.""" text = f"{subject} {body_preview}".lower() category_keywords = { EmailCategory.DIENSTLICH: [ "dienstlich", "dienstanweisung", "erlass", "verordnung", "bescheid", "verfuegung", "ministerium", "behoerde" ], EmailCategory.PERSONAL: [ "personalrat", "stellenausschreibung", "versetzung", "beurteilung", "dienstzeugnis", "krankmeldung", "elternzeit" ], EmailCategory.FINANZEN: [ "budget", "haushalt", "etat", "abrechnung", "rechnung", "erstattung", "zuschuss", "foerdermittel" ], EmailCategory.ELTERN: [ "elternbrief", "elternabend", "schulkonferenz", "elternvertreter", "elternbeirat" ], EmailCategory.SCHUELER: [ "schueler", "schuelerin", "zeugnis", "klasse", "unterricht", "pruefung", "klassenfahrt", "schulpflicht" ], EmailCategory.FORTBILDUNG: [ "fortbildung", "seminar", "workshop", "schulung", "weiterbildung", "nlq", "didaktik" ], EmailCategory.VERANSTALTUNG: [ "einladung", "veranstaltung", "termin", "konferenz", "sitzung", "tagung", "feier" ], EmailCategory.SICHERHEIT: [ "sicherheit", "notfall", "brandschutz", "evakuierung", "hygiene", "corona", "infektionsschutz" ], EmailCategory.TECHNIK: [ "it", "software", "computer", "netzwerk", "login", "passwort", "digitalisierung", "iserv" ], EmailCategory.NEWSLETTER: [ "newsletter", "rundschreiben", "info-mail", "mitteilung" ], EmailCategory.WERBUNG: [ "angebot", "rabatt", "aktion", "werbung", "abonnement" ], } best_category = EmailCategory.SONSTIGES best_score = 0.0 for category, keywords in category_keywords.items(): score = sum(1 for kw in keywords if kw in text) if score > best_score: best_score = score best_category = category if sender_type in [SenderType.KULTUSMINISTERIUM, SenderType.LANDESSCHULBEHOERDE, SenderType.RLSB]: if best_category == EmailCategory.SONSTIGES: best_category = EmailCategory.DIENSTLICH best_score = 2 confidence = min(0.9, 0.4 + (best_score * 0.15)) return best_category, confidence async def _classify_category_llm( client: httpx.AsyncClient, subject: str, body_preview: str, ) -> Tuple[EmailCategory, float]: """LLM-based category classification.""" try: categories = ", ".join([c.value for c in EmailCategory]) prompt = f"""Klassifiziere diese E-Mail in EINE Kategorie: Betreff: {subject} Inhalt: {body_preview[:500]} Kategorien: {categories} Antworte NUR mit dem Kategorienamen und einer Konfidenz (0.0-1.0): Format: kategorie|konfidenz """ response = await client.post( f"{LLM_GATEWAY_URL}/api/v1/inference", json={ "prompt": prompt, "playbook": "mail_analysis", "max_tokens": 50, }, ) if response.status_code == 200: data = response.json() result = data.get("response", "sonstiges|0.5") parts = result.strip().split("|") if len(parts) >= 2: category_str = parts[0].strip().lower() confidence = float(parts[1].strip()) try: category = EmailCategory(category_str) return category, min(max(confidence, 0.0), 1.0) except ValueError: pass except Exception as e: logger.warning(f"LLM category classification failed: {e}") return EmailCategory.SONSTIGES, 0.5 async def suggest_response( http_client: httpx.AsyncClient, subject: str, body_text: str, sender_type: SenderType, category: EmailCategory, ) -> List[ResponseSuggestion]: """Generate response suggestions for an email.""" suggestions = [] if sender_type in [SenderType.KULTUSMINISTERIUM, SenderType.LANDESSCHULBEHOERDE, SenderType.RLSB]: suggestions.append(ResponseSuggestion( template_type="acknowledgment", subject=f"Re: {subject}", body="""Sehr geehrte Damen und Herren, vielen Dank fuer Ihre Nachricht. Ich bestaetige den Eingang und werde die Angelegenheit fristgerecht bearbeiten. Mit freundlichen Gruessen""", confidence=0.8, )) if category == EmailCategory.ELTERN: suggestions.append(ResponseSuggestion( template_type="parent_response", subject=f"Re: {subject}", body="""Liebe Eltern, vielen Dank fuer Ihre Nachricht. [Ihre Antwort hier] Mit freundlichen Gruessen""", confidence=0.7, )) try: llm_suggestion = await _generate_response_llm(http_client, subject, body_text[:500], sender_type) if llm_suggestion: suggestions.append(llm_suggestion) except Exception as e: logger.warning(f"LLM response generation failed: {e}") return suggestions async def _generate_response_llm( client: httpx.AsyncClient, subject: str, body_preview: str, sender_type: SenderType, ) -> Optional[ResponseSuggestion]: """Generate a response suggestion using LLM.""" try: sender_desc = { SenderType.KULTUSMINISTERIUM: "dem Kultusministerium", SenderType.LANDESSCHULBEHOERDE: "der Landesschulbehoerde", SenderType.RLSB: "dem RLSB", SenderType.ELTERNVERTRETER: "einem Elternvertreter", }.get(sender_type, "einem Absender") prompt = f"""Du bist eine Schulleiterin in Niedersachsen. Formuliere eine professionelle, kurze Antwort auf diese E-Mail von {sender_desc}: Betreff: {subject} Inhalt: {body_preview} Die Antwort sollte: - Hoeflich und formell sein - Den Eingang bestaetigen - Eine konkrete naechste Aktion nennen oder um Klaerung bitten Antworte NUR mit dem Antworttext (ohne Betreffzeile, ohne "Betreff:"). """ response = await client.post( f"{LLM_GATEWAY_URL}/api/v1/inference", json={ "prompt": prompt, "playbook": "mail_analysis", "max_tokens": 300, }, ) if response.status_code == 200: data = response.json() body = data.get("response", "").strip() if body: return ResponseSuggestion( template_type="ai_generated", subject=f"Re: {subject}", body=body, confidence=0.6, ) except Exception as e: logger.warning(f"LLM response generation failed: {e}") return None