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>
289 lines
10 KiB
Python
289 lines
10 KiB
Python
"""
|
|
RelevanceProfile Model.
|
|
|
|
Definiert das Relevanzprofil eines Nutzers für die Alerts-Filterung.
|
|
Lernt über Zeit durch Feedback.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
import uuid
|
|
|
|
|
|
@dataclass
|
|
class PriorityItem:
|
|
"""Ein Prioritäts-Thema im Profil."""
|
|
label: str # z.B. "Inklusion", "Datenschutz Schule"
|
|
weight: float = 0.5 # 0.0 - 1.0, höher = wichtiger
|
|
keywords: list = field(default_factory=list) # Zusätzliche Keywords
|
|
description: Optional[str] = None # Kontext für LLM
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"label": self.label,
|
|
"weight": self.weight,
|
|
"keywords": self.keywords,
|
|
"description": self.description,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "PriorityItem":
|
|
return cls(**data)
|
|
|
|
|
|
@dataclass
|
|
class RelevanceProfile:
|
|
"""
|
|
Nutzerprofil für Relevanz-Scoring.
|
|
|
|
Das Profil wird verwendet, um Alerts auf Relevanz zu prüfen.
|
|
Es enthält:
|
|
- Prioritäten: Themen die wichtig sind (mit Gewichtung)
|
|
- Ausschlüsse: Themen die ignoriert werden sollen
|
|
- Positive Beispiele: URLs/Titel die relevant waren
|
|
- Negative Beispiele: URLs/Titel die irrelevant waren
|
|
- Policies: Zusätzliche Regeln (z.B. nur deutsche Quellen)
|
|
"""
|
|
|
|
# Identifikation
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
user_id: Optional[str] = None # Falls benutzerspezifisch
|
|
|
|
# Relevanz-Kriterien
|
|
priorities: list = field(default_factory=list) # List[PriorityItem]
|
|
exclusions: list = field(default_factory=list) # Keywords zum Ausschließen
|
|
|
|
# Beispiele für Few-Shot Learning
|
|
positive_examples: list = field(default_factory=list) # Relevante Alerts
|
|
negative_examples: list = field(default_factory=list) # Irrelevante Alerts
|
|
|
|
# Policies
|
|
policies: dict = field(default_factory=dict)
|
|
|
|
# Metadaten
|
|
created_at: datetime = field(default_factory=datetime.utcnow)
|
|
updated_at: datetime = field(default_factory=datetime.utcnow)
|
|
|
|
# Statistiken
|
|
total_scored: int = 0
|
|
total_kept: int = 0
|
|
total_dropped: int = 0
|
|
accuracy_estimate: Optional[float] = None # Geschätzte Genauigkeit
|
|
|
|
def add_priority(self, label: str, weight: float = 0.5, **kwargs) -> None:
|
|
"""Füge ein Prioritäts-Thema hinzu."""
|
|
self.priorities.append(PriorityItem(
|
|
label=label,
|
|
weight=weight,
|
|
**kwargs
|
|
))
|
|
self.updated_at = datetime.utcnow()
|
|
|
|
def add_exclusion(self, keyword: str) -> None:
|
|
"""Füge ein Ausschluss-Keyword hinzu."""
|
|
if keyword not in self.exclusions:
|
|
self.exclusions.append(keyword)
|
|
self.updated_at = datetime.utcnow()
|
|
|
|
def add_positive_example(self, title: str, url: str, reason: str = "") -> None:
|
|
"""Füge ein positives Beispiel hinzu (für Few-Shot Learning)."""
|
|
self.positive_examples.append({
|
|
"title": title,
|
|
"url": url,
|
|
"reason": reason,
|
|
"added_at": datetime.utcnow().isoformat(),
|
|
})
|
|
# Begrenze auf letzte 20 Beispiele
|
|
self.positive_examples = self.positive_examples[-20:]
|
|
self.updated_at = datetime.utcnow()
|
|
|
|
def add_negative_example(self, title: str, url: str, reason: str = "") -> None:
|
|
"""Füge ein negatives Beispiel hinzu."""
|
|
self.negative_examples.append({
|
|
"title": title,
|
|
"url": url,
|
|
"reason": reason,
|
|
"added_at": datetime.utcnow().isoformat(),
|
|
})
|
|
# Begrenze auf letzte 20 Beispiele
|
|
self.negative_examples = self.negative_examples[-20:]
|
|
self.updated_at = datetime.utcnow()
|
|
|
|
def update_from_feedback(self, alert_title: str, alert_url: str,
|
|
is_relevant: bool, reason: str = "") -> None:
|
|
"""
|
|
Aktualisiere Profil basierend auf Nutzer-Feedback.
|
|
|
|
Args:
|
|
alert_title: Titel des Alerts
|
|
alert_url: URL des Alerts
|
|
is_relevant: True wenn der Nutzer den Alert als relevant markiert hat
|
|
reason: Optional - Grund für die Entscheidung
|
|
"""
|
|
if is_relevant:
|
|
self.add_positive_example(alert_title, alert_url, reason)
|
|
self.total_kept += 1
|
|
else:
|
|
self.add_negative_example(alert_title, alert_url, reason)
|
|
self.total_dropped += 1
|
|
|
|
self.total_scored += 1
|
|
|
|
# Aktualisiere Accuracy-Schätzung (vereinfacht)
|
|
if self.total_scored > 10:
|
|
# Hier könnte eine komplexere Berechnung erfolgen
|
|
# basierend auf Vergleich von Vorhersage vs. tatsächlichem Feedback
|
|
pass
|
|
|
|
def get_prompt_context(self) -> str:
|
|
"""
|
|
Generiere Kontext für LLM-Prompt.
|
|
|
|
Dieser Text wird in den System-Prompt des Relevanz-Scorers eingefügt.
|
|
"""
|
|
lines = ["## Relevanzprofil des Nutzers\n"]
|
|
|
|
# Prioritäten
|
|
if self.priorities:
|
|
lines.append("### Prioritäten (Themen von Interesse):")
|
|
for p in self.priorities:
|
|
if isinstance(p, dict):
|
|
p = PriorityItem.from_dict(p)
|
|
weight_label = "Sehr wichtig" if p.weight > 0.7 else "Wichtig" if p.weight > 0.4 else "Interessant"
|
|
lines.append(f"- **{p.label}** ({weight_label})")
|
|
if p.description:
|
|
lines.append(f" {p.description}")
|
|
if p.keywords:
|
|
lines.append(f" Keywords: {', '.join(p.keywords)}")
|
|
lines.append("")
|
|
|
|
# Ausschlüsse
|
|
if self.exclusions:
|
|
lines.append("### Ausschlüsse (ignorieren):")
|
|
lines.append(f"Themen mit diesen Keywords: {', '.join(self.exclusions)}")
|
|
lines.append("")
|
|
|
|
# Positive Beispiele
|
|
if self.positive_examples:
|
|
lines.append("### Beispiele für relevante Alerts:")
|
|
for ex in self.positive_examples[-5:]: # Letzte 5
|
|
lines.append(f"- \"{ex['title']}\"")
|
|
if ex.get("reason"):
|
|
lines.append(f" Grund: {ex['reason']}")
|
|
lines.append("")
|
|
|
|
# Negative Beispiele
|
|
if self.negative_examples:
|
|
lines.append("### Beispiele für irrelevante Alerts:")
|
|
for ex in self.negative_examples[-5:]: # Letzte 5
|
|
lines.append(f"- \"{ex['title']}\"")
|
|
if ex.get("reason"):
|
|
lines.append(f" Grund: {ex['reason']}")
|
|
lines.append("")
|
|
|
|
# Policies
|
|
if self.policies:
|
|
lines.append("### Zusätzliche Regeln:")
|
|
for key, value in self.policies.items():
|
|
lines.append(f"- {key}: {value}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Konvertiere zu Dictionary."""
|
|
return {
|
|
"id": self.id,
|
|
"user_id": self.user_id,
|
|
"priorities": [p.to_dict() if isinstance(p, PriorityItem) else p
|
|
for p in self.priorities],
|
|
"exclusions": self.exclusions,
|
|
"positive_examples": self.positive_examples,
|
|
"negative_examples": self.negative_examples,
|
|
"policies": self.policies,
|
|
"created_at": self.created_at.isoformat(),
|
|
"updated_at": self.updated_at.isoformat(),
|
|
"total_scored": self.total_scored,
|
|
"total_kept": self.total_kept,
|
|
"total_dropped": self.total_dropped,
|
|
"accuracy_estimate": self.accuracy_estimate,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "RelevanceProfile":
|
|
"""Erstelle RelevanceProfile aus Dictionary."""
|
|
# Parse Timestamps
|
|
for field_name in ["created_at", "updated_at"]:
|
|
if field_name in data and isinstance(data[field_name], str):
|
|
data[field_name] = datetime.fromisoformat(data[field_name])
|
|
|
|
# Parse Priorities
|
|
if "priorities" in data:
|
|
data["priorities"] = [
|
|
PriorityItem.from_dict(p) if isinstance(p, dict) else p
|
|
for p in data["priorities"]
|
|
]
|
|
|
|
return cls(**data)
|
|
|
|
@classmethod
|
|
def create_default_education_profile(cls) -> "RelevanceProfile":
|
|
"""
|
|
Erstelle ein Standard-Profil für Bildungsthemen.
|
|
|
|
Dieses Profil ist für Lehrkräfte/Schulpersonal optimiert.
|
|
"""
|
|
profile = cls()
|
|
|
|
# Bildungs-relevante Prioritäten
|
|
profile.add_priority(
|
|
"Inklusion",
|
|
weight=0.9,
|
|
keywords=["inklusiv", "Förderbedarf", "Behinderung", "Barrierefreiheit"],
|
|
description="Inklusive Bildung, Förderschulen, Nachteilsausgleich"
|
|
)
|
|
profile.add_priority(
|
|
"Datenschutz Schule",
|
|
weight=0.85,
|
|
keywords=["DSGVO", "Schülerfotos", "Einwilligung", "personenbezogene Daten"],
|
|
description="DSGVO in Schulen, Datenschutz bei Klassenfotos"
|
|
)
|
|
profile.add_priority(
|
|
"Schulrecht Bayern",
|
|
weight=0.8,
|
|
keywords=["BayEUG", "Schulordnung", "Kultusministerium", "Bayern"],
|
|
description="Bayerisches Schulrecht, Verordnungen"
|
|
)
|
|
profile.add_priority(
|
|
"Digitalisierung Schule",
|
|
weight=0.7,
|
|
keywords=["DigitalPakt", "Tablet-Klasse", "Lernplattform"],
|
|
description="Digitale Medien im Unterricht"
|
|
)
|
|
profile.add_priority(
|
|
"Elternarbeit",
|
|
weight=0.6,
|
|
keywords=["Elternbeirat", "Elternabend", "Kommunikation"],
|
|
description="Zusammenarbeit mit Eltern"
|
|
)
|
|
|
|
# Standard-Ausschlüsse
|
|
profile.exclusions = [
|
|
"Stellenanzeige",
|
|
"Praktikum gesucht",
|
|
"Werbung",
|
|
"Pressemitteilung", # Oft generisch
|
|
]
|
|
|
|
# Policies
|
|
profile.policies = {
|
|
"prefer_german_sources": True,
|
|
"max_age_days": 30, # Ältere Alerts ignorieren
|
|
"min_content_length": 100, # Sehr kurze Snippets ignorieren
|
|
}
|
|
|
|
return profile
|
|
|
|
def __repr__(self) -> str:
|
|
return f"RelevanceProfile(id={self.id[:8]}, priorities={len(self.priorities)}, examples={len(self.positive_examples) + len(self.negative_examples)})"
|