A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
342 lines
10 KiB
Python
342 lines
10 KiB
Python
"""
|
|
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]
|