""" Importance Mapping für Guided Mode. Konvertiert Relevanz-Scores (0.0-1.0) in 5-stufige Wichtigkeitsstufen: - KRITISCH (90-100%): Sofortiges Handeln erforderlich - DRINGEND (75-90%): Wichtig, bald handeln - WICHTIG (60-75%): Beachtenswert - PRÜFEN (40-60%): Eventuell relevant - INFO (0-40%): Zur Kenntnisnahme Zusätzlich: Generierung von "Warum relevant?"-Erklärungen und nächsten Schritten. """ from typing import Optional, List, Dict, Any from datetime import datetime, timedelta import re from ..db.models import ImportanceLevelEnum, AlertItemDB # Re-export fuer einfacheren Import __all__ = [ 'ImportanceLevelEnum', 'score_to_importance', 'importance_to_label_de', 'importance_to_color', 'extract_deadline', 'generate_why_relevant', 'generate_next_steps', 'enrich_alert_for_guided_mode', 'batch_enrich_alerts', 'filter_by_importance', ] # Standard-Schwellenwerte für Importance-Mapping DEFAULT_THRESHOLDS = { "kritisch": 0.90, "dringend": 0.75, "wichtig": 0.60, "pruefen": 0.40, } def score_to_importance( score: float, thresholds: Dict[str, float] = None ) -> ImportanceLevelEnum: """ Konvertiere Relevanz-Score zu Importance-Level. Args: score: Relevanz-Score (0.0 - 1.0) thresholds: Optionale benutzerdefinierte Schwellenwerte Returns: ImportanceLevelEnum """ if score is None: return ImportanceLevelEnum.INFO thresholds = thresholds or DEFAULT_THRESHOLDS if score >= thresholds.get("kritisch", 0.90): return ImportanceLevelEnum.KRITISCH elif score >= thresholds.get("dringend", 0.75): return ImportanceLevelEnum.DRINGEND elif score >= thresholds.get("wichtig", 0.60): return ImportanceLevelEnum.WICHTIG elif score >= thresholds.get("pruefen", 0.40): return ImportanceLevelEnum.PRUEFEN else: return ImportanceLevelEnum.INFO def importance_to_label_de(importance: ImportanceLevelEnum) -> str: """Deutsches Label für Importance-Level.""" labels = { ImportanceLevelEnum.KRITISCH: "Kritisch", ImportanceLevelEnum.DRINGEND: "Dringend", ImportanceLevelEnum.WICHTIG: "Wichtig", ImportanceLevelEnum.PRUEFEN: "Zu prüfen", ImportanceLevelEnum.INFO: "Info", } return labels.get(importance, "Info") def importance_to_color(importance: ImportanceLevelEnum) -> str: """CSS-Farbe für Importance-Level (Tailwind-kompatibel).""" colors = { ImportanceLevelEnum.KRITISCH: "red", ImportanceLevelEnum.DRINGEND: "orange", ImportanceLevelEnum.WICHTIG: "amber", ImportanceLevelEnum.PRUEFEN: "blue", ImportanceLevelEnum.INFO: "slate", } return colors.get(importance, "slate") def extract_deadline(text: str) -> Optional[datetime]: """ Extrahiere Deadline/Frist aus Text. Sucht nach Mustern wie: - "bis zum 15.03.2026" - "Frist: 1. April" - "Anmeldeschluss: 30.11." """ # Deutsche Datumsformate patterns = [ r"bis\s+(?:zum\s+)?(\d{1,2})\.(\d{1,2})\.(\d{4})", r"Frist[:\s]+(\d{1,2})\.(\d{1,2})\.(\d{4})", r"(?:Anmelde|Bewerbungs)schluss[:\s]+(\d{1,2})\.(\d{1,2})\.?(?:(\d{4}))?", r"endet\s+am\s+(\d{1,2})\.(\d{1,2})\.(\d{4})?", ] for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: day = int(match.group(1)) month = int(match.group(2)) year = int(match.group(3)) if match.group(3) else datetime.now().year try: return datetime(year, month, day) except ValueError: continue return None def generate_why_relevant( alert: AlertItemDB, profile_priorities: List[Dict[str, Any]] = None, matched_keywords: List[str] = None ) -> str: """ Generiere "Warum relevant?"-Erklärung für einen Alert. Args: alert: Der Alert profile_priorities: Prioritäten aus dem User-Profil matched_keywords: Keywords, die gematcht haben Returns: Deutsche Erklärung (1-2 Bulletpoints) """ reasons = [] # Deadline-basierte Relevanz deadline = extract_deadline(f"{alert.title} {alert.snippet}") if deadline: days_until = (deadline - datetime.now()).days if days_until <= 0: reasons.append("Frist abgelaufen oder heute!") elif days_until <= 7: reasons.append(f"Frist endet in {days_until} Tagen") elif days_until <= 30: reasons.append(f"Frist in ca. {days_until} Tagen") # Keyword-basierte Relevanz if matched_keywords and len(matched_keywords) > 0: keywords_str = ", ".join(matched_keywords[:3]) reasons.append(f"Enthält relevante Begriffe: {keywords_str}") # Prioritäten-basierte Relevanz if profile_priorities: for priority in profile_priorities[:2]: label = priority.get("label", "") keywords = priority.get("keywords", []) text_lower = f"{alert.title} {alert.snippet}".lower() for kw in keywords: if kw.lower() in text_lower: reasons.append(f"Passt zu Ihrem Interesse: {label}") break # Score-basierte Relevanz if alert.relevance_score and alert.relevance_score >= 0.8: reasons.append("Hohe Übereinstimmung mit Ihrem Profil") # Fallback if not reasons: reasons.append("Passt zu Ihren ausgewählten Themen") # Formatiere als Bulletpoints return " • ".join(reasons[:2]) def generate_next_steps( alert: AlertItemDB, template_slug: str = None ) -> List[str]: """ Generiere empfohlene nächste Schritte. Basiert auf Template-Typ und Alert-Inhalt. """ steps = [] text = f"{alert.title} {alert.snippet}".lower() # Template-spezifische Schritte if template_slug == "foerderprogramme": if "antrag" in text or "förder" in text: steps.append("Schulträger über Fördermöglichkeit informieren") steps.append("Antragsunterlagen prüfen") if "frist" in text or "deadline" in text: steps.append("Termin in Kalender eintragen") elif template_slug == "datenschutz-recht": if "dsgvo" in text or "datenschutz" in text: steps.append("Datenschutzbeauftragten informieren") steps.append("Prüfen, ob Handlungsbedarf besteht") if "urteil" in text or "gericht" in text: steps.append("Rechtsfolgen für die Schule prüfen") elif template_slug == "it-security": if "cve" in text or "sicherheitslücke" in text: steps.append("Betroffene Systeme prüfen") steps.append("Update/Patch einspielen") if "phishing" in text: steps.append("Kollegium warnen") steps.append("Erkennungsmerkmale kommunizieren") elif template_slug == "abitur-updates": if "abitur" in text or "prüfung" in text: steps.append("Fachschaften informieren") steps.append("Anpassung der Kursplanung prüfen") elif template_slug == "fortbildungen": steps.append("Termin und Ort prüfen") steps.append("Bei Interesse: Anmeldung vornehmen") elif template_slug == "wettbewerbe-projekte": steps.append("Passende Schülergruppe identifizieren") steps.append("Anmeldefrist beachten") # Allgemeine Schritte als Fallback if not steps: steps.append("Quelle öffnen und Details lesen") if "frist" in text or "bis" in text: steps.append("Termin notieren") return steps[:3] # Maximal 3 Schritte def enrich_alert_for_guided_mode( alert: AlertItemDB, profile_priorities: List[Dict[str, Any]] = None, template_slug: str = None, importance_thresholds: Dict[str, float] = None ) -> AlertItemDB: """ Reichere Alert mit Guided-Mode-spezifischen Feldern an. Setzt: - importance_level - why_relevant - next_steps - action_deadline Args: alert: Der Alert profile_priorities: Prioritäten aus dem User-Profil template_slug: Slug des aktiven Templates importance_thresholds: Optionale Schwellenwerte Returns: Der angereicherte Alert """ # Importance Level alert.importance_level = score_to_importance( alert.relevance_score, importance_thresholds ) # Why Relevant alert.why_relevant = generate_why_relevant(alert, profile_priorities) # Next Steps alert.next_steps = generate_next_steps(alert, template_slug) # Action Deadline deadline = extract_deadline(f"{alert.title} {alert.snippet}") if deadline: alert.action_deadline = deadline return alert def batch_enrich_alerts( alerts: List[AlertItemDB], profile_priorities: List[Dict[str, Any]] = None, template_slug: str = None, importance_thresholds: Dict[str, float] = None ) -> List[AlertItemDB]: """ Reichere mehrere Alerts für Guided Mode an. """ return [ enrich_alert_for_guided_mode( alert, profile_priorities, template_slug, importance_thresholds ) for alert in alerts ] def filter_by_importance( alerts: List[AlertItemDB], min_level: ImportanceLevelEnum = ImportanceLevelEnum.INFO, max_count: int = 10 ) -> List[AlertItemDB]: """ Filtere Alerts nach Mindest-Importance und limitiere Anzahl. Sortiert nach Importance (höchste zuerst). """ # Importance-Ranking (höher = wichtiger) importance_rank = { ImportanceLevelEnum.KRITISCH: 5, ImportanceLevelEnum.DRINGEND: 4, ImportanceLevelEnum.WICHTIG: 3, ImportanceLevelEnum.PRUEFEN: 2, ImportanceLevelEnum.INFO: 1, } min_rank = importance_rank.get(min_level, 1) # Filter filtered = [ a for a in alerts if importance_rank.get(a.importance_level, 1) >= min_rank ] # Sortiere nach Importance (absteigend) filtered.sort( key=lambda a: importance_rank.get(a.importance_level, 1), reverse=True ) return filtered[:max_count]