website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
302 lines
12 KiB
Python
302 lines
12 KiB
Python
"""
|
|
Communication Service - KI-gestuetzte Lehrer-Eltern-Kommunikation.
|
|
|
|
Split into:
|
|
- communication_types.py: Enums, data classes, templates, legal references
|
|
- communication_service.py (this file): CommunicationService class
|
|
|
|
All symbols are re-exported here for backward compatibility.
|
|
"""
|
|
import logging
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
from .communication_types import (
|
|
CommunicationType,
|
|
CommunicationTone,
|
|
LegalReference,
|
|
GFKPrinciple,
|
|
FALLBACK_LEGAL_REFERENCES,
|
|
GFK_PRINCIPLES,
|
|
COMMUNICATION_TEMPLATES,
|
|
fetch_legal_references_from_db,
|
|
parse_db_references_to_legal_refs,
|
|
)
|
|
|
|
# Re-export for backward compatibility
|
|
__all__ = [
|
|
"CommunicationType",
|
|
"CommunicationTone",
|
|
"LegalReference",
|
|
"GFKPrinciple",
|
|
"CommunicationService",
|
|
"get_communication_service",
|
|
"fetch_legal_references_from_db",
|
|
"parse_db_references_to_legal_refs",
|
|
]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CommunicationService:
|
|
"""
|
|
Service zur Unterstuetzung von Lehrer-Eltern-Kommunikation.
|
|
|
|
Generiert professionelle, rechtlich fundierte und empathische Nachrichten
|
|
basierend auf den Prinzipien der gewaltfreien Kommunikation.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.fallback_references = FALLBACK_LEGAL_REFERENCES
|
|
self.gfk_principles = GFK_PRINCIPLES
|
|
self.templates = COMMUNICATION_TEMPLATES
|
|
self._cached_references: Dict[str, List[LegalReference]] = {}
|
|
|
|
async def get_legal_references_async(
|
|
self, state: str, topic: str
|
|
) -> List[LegalReference]:
|
|
"""Gibt relevante rechtliche Referenzen fuer ein Bundesland und Thema zurueck."""
|
|
cache_key = f"{state}:{topic}"
|
|
if cache_key in self._cached_references:
|
|
return self._cached_references[cache_key]
|
|
|
|
db_docs = await fetch_legal_references_from_db(state)
|
|
if db_docs:
|
|
references = parse_db_references_to_legal_refs(db_docs, topic)
|
|
if references:
|
|
self._cached_references[cache_key] = references
|
|
return references
|
|
|
|
logger.info(f"Keine DB-Referenzen fuer {state}/{topic}, nutze Fallback")
|
|
return self._get_fallback_references(state, topic)
|
|
|
|
def get_legal_references(self, state: str, topic: str) -> List[LegalReference]:
|
|
"""Synchrone Methode fuer Rueckwaertskompatibilitaet."""
|
|
return self._get_fallback_references(state, topic)
|
|
|
|
def _get_fallback_references(self, state: str, topic: str) -> List[LegalReference]:
|
|
"""Gibt Fallback-Referenzen zurueck."""
|
|
state_refs = self.fallback_references.get("DEFAULT", {})
|
|
if topic in state_refs:
|
|
return [state_refs[topic]]
|
|
return list(state_refs.values())
|
|
|
|
def get_gfk_guidance(self, comm_type: CommunicationType) -> List[GFKPrinciple]:
|
|
return self.gfk_principles
|
|
|
|
def get_template(self, comm_type: CommunicationType) -> Dict[str, str]:
|
|
return self.templates.get(comm_type, self.templates[CommunicationType.GENERAL_INFO])
|
|
|
|
def build_system_prompt(
|
|
self, comm_type: CommunicationType, state: str, tone: CommunicationTone
|
|
) -> str:
|
|
"""Erstellt den System-Prompt fuer die KI-gestuetzte Nachrichtengenerierung."""
|
|
topic_map = {
|
|
CommunicationType.ATTENDANCE: "schulpflicht",
|
|
CommunicationType.BEHAVIOR: "ordnungsmassnahmen",
|
|
CommunicationType.ACADEMIC: "foerderung",
|
|
CommunicationType.SPECIAL_NEEDS: "foerderung",
|
|
CommunicationType.CONCERN: "elternpflichten",
|
|
CommunicationType.CONFLICT: "elternpflichten",
|
|
}
|
|
topic = topic_map.get(comm_type, "elternpflichten")
|
|
legal_refs = self.get_legal_references(state, topic)
|
|
|
|
legal_context = ""
|
|
if legal_refs:
|
|
legal_context = "\n\nRechtliche Grundlagen:\n"
|
|
for ref in legal_refs:
|
|
legal_context += f"- {ref.law} {ref.paragraph} ({ref.title}): {ref.summary}\n"
|
|
|
|
tone_descriptions = {
|
|
CommunicationTone.FORMAL: "Verwende eine sehr formelle, sachliche Sprache.",
|
|
CommunicationTone.PROFESSIONAL: "Verwende eine professionelle, aber freundliche Sprache.",
|
|
CommunicationTone.WARM: "Verwende eine warmherzige, einladende Sprache.",
|
|
CommunicationTone.CONCERNED: "Druecke aufrichtige Sorge und Empathie aus.",
|
|
CommunicationTone.APPRECIATIVE: "Betone Wertschaetzung und positives Feedback.",
|
|
}
|
|
tone_desc = tone_descriptions.get(tone, tone_descriptions[CommunicationTone.PROFESSIONAL])
|
|
|
|
return f"""Du bist ein erfahrener Kommunikationsberater fuer Lehrkraefte im deutschen Schulsystem.
|
|
Deine Aufgabe ist es, professionelle, empathische und rechtlich fundierte Elternbriefe zu verfassen.
|
|
|
|
GRUNDPRINZIPIEN (Gewaltfreie Kommunikation nach Marshall Rosenberg):
|
|
|
|
1. BEOBACHTUNG: Beschreibe konkrete Handlungen ohne Bewertung
|
|
Beispiel: "Ich habe bemerkt, dass..." statt "Das Kind ist..."
|
|
|
|
2. GEFUEHLE: Druecke Gefuehle als Ich-Botschaften aus
|
|
Beispiel: "Ich mache mir Sorgen..." statt "Sie muessen..."
|
|
|
|
3. BEDUERFNISSE: Benenne dahinterliegende Beduerfnisse
|
|
Beispiel: "Mir ist wichtig, dass..." statt "Sie sollten..."
|
|
|
|
4. BITTEN: Formuliere konkrete, erfuellbare Bitten
|
|
Beispiel: "Waeren Sie bereit, ...?" statt "Tun Sie endlich...!"
|
|
|
|
WICHTIGE REGELN:
|
|
- Immer die Wuerde aller Beteiligten wahren
|
|
- Keine Schuldzuweisungen oder Vorwuerfe
|
|
- Loesungsorientiert statt problemfokussiert
|
|
- Auf Augenhoehe kommunizieren
|
|
- Kooperation statt Konfrontation
|
|
- Deutsche Sprache, foermliche Anrede (Sie)
|
|
- Sachlich, aber empathisch
|
|
{legal_context}
|
|
|
|
TONALITAET:
|
|
{tone_desc}
|
|
|
|
FORMAT:
|
|
- Verfasse den Brief als vollstaendigen, versandfertigen Text
|
|
- Beginne mit der Anrede
|
|
- Strukturiere den Inhalt klar und verstaendlich
|
|
- Schliesse mit einer freundlichen Grussformel
|
|
- Die Signatur (Name der Lehrkraft) wird spaeter hinzugefuegt
|
|
|
|
WICHTIG: Der Brief soll professionell und rechtlich einwandfrei sein, aber gleichzeitig
|
|
menschlich und einladend wirken. Ziel ist immer eine konstruktive Zusammenarbeit."""
|
|
|
|
def build_user_prompt(
|
|
self, comm_type: CommunicationType, context: Dict[str, Any]
|
|
) -> str:
|
|
"""Erstellt den User-Prompt aus dem Kontext."""
|
|
student_name = context.get("student_name", "das Kind")
|
|
parent_name = context.get("parent_name", "Frau/Herr")
|
|
situation = context.get("situation", "")
|
|
additional_info = context.get("additional_info", "")
|
|
|
|
type_descriptions = {
|
|
CommunicationType.GENERAL_INFO: "eine allgemeine Information",
|
|
CommunicationType.BEHAVIOR: "ein Verhalten, das besprochen werden sollte",
|
|
CommunicationType.ACADEMIC: "die schulische Entwicklung",
|
|
CommunicationType.ATTENDANCE: "Fehlzeiten oder Anwesenheitsprobleme",
|
|
CommunicationType.MEETING_INVITE: "eine Einladung zum Elterngespraech",
|
|
CommunicationType.POSITIVE_FEEDBACK: "positives Feedback",
|
|
CommunicationType.CONCERN: "eine Sorge oder ein Anliegen",
|
|
CommunicationType.CONFLICT: "eine konflikthafte Situation",
|
|
CommunicationType.SPECIAL_NEEDS: "Foerderbedarf oder besondere Unterstuetzung",
|
|
}
|
|
type_desc = type_descriptions.get(comm_type, "ein Anliegen")
|
|
|
|
user_prompt = f"""Schreibe einen Elternbrief zu folgendem Anlass: {type_desc}
|
|
|
|
Schuelername: {student_name}
|
|
Elternname: {parent_name}
|
|
|
|
Situation:
|
|
{situation}
|
|
"""
|
|
if additional_info:
|
|
user_prompt += f"\nZusaetzliche Informationen:\n{additional_info}\n"
|
|
|
|
user_prompt += """
|
|
Bitte verfasse einen professionellen, empathischen Brief nach den GFK-Prinzipien.
|
|
Der Brief sollte:
|
|
- Die Situation sachlich beschreiben (Beobachtung)
|
|
- Verstaendnis und Sorge ausdruecken (Gefuehle)
|
|
- Das gemeinsame Ziel betonen (Beduerfnisse)
|
|
- Einen konstruktiven Vorschlag machen (Bitte)
|
|
"""
|
|
return user_prompt
|
|
|
|
def validate_communication(self, text: str) -> Dict[str, Any]:
|
|
"""Validiert eine generierte Kommunikation auf GFK-Konformitaet."""
|
|
issues = []
|
|
suggestions = []
|
|
|
|
problematic_patterns = [
|
|
("Sie muessen", "Vorschlag: 'Waeren Sie bereit, ...' oder 'Ich bitte Sie, ...'"),
|
|
("Sie sollten", "Vorschlag: 'Ich wuerde mir wuenschen, ...'"),
|
|
("Das Kind ist", "Vorschlag: 'Ich habe beobachtet, dass ...'"),
|
|
("immer", "Vorsicht bei Verallgemeinerungen - besser konkrete Beispiele"),
|
|
("nie", "Vorsicht bei Verallgemeinerungen - besser konkrete Beispiele"),
|
|
("faul", "Vorschlag: Verhalten konkret beschreiben statt bewerten"),
|
|
("unverschaemt", "Vorschlag: Verhalten konkret beschreiben statt bewerten"),
|
|
("respektlos", "Vorschlag: Verhalten konkret beschreiben statt bewerten"),
|
|
]
|
|
|
|
for pattern, suggestion in problematic_patterns:
|
|
if pattern.lower() in text.lower():
|
|
issues.append(f"Problematische Formulierung gefunden: '{pattern}'")
|
|
suggestions.append(suggestion)
|
|
|
|
positive_elements = []
|
|
positive_patterns = [
|
|
("Ich habe bemerkt", "Gute Beobachtung"),
|
|
("Ich moechte", "Gute Ich-Botschaft"),
|
|
("gemeinsam", "Gute Kooperationsorientierung"),
|
|
("wichtig", "Gutes Beduerfnis-Statement"),
|
|
("freuen", "Positive Tonalitaet"),
|
|
("Waeren Sie bereit", "Gute Bitte-Formulierung"),
|
|
]
|
|
|
|
for pattern, feedback in positive_patterns:
|
|
if pattern.lower() in text.lower():
|
|
positive_elements.append(feedback)
|
|
|
|
return {
|
|
"is_valid": len(issues) == 0,
|
|
"issues": issues,
|
|
"suggestions": suggestions,
|
|
"positive_elements": positive_elements,
|
|
"gfk_score": max(0, 100 - len(issues) * 15 + len(positive_elements) * 10) / 100
|
|
}
|
|
|
|
def get_all_communication_types(self) -> List[Dict[str, str]]:
|
|
return [{"value": ct.value, "label": self._get_type_label(ct)} for ct in CommunicationType]
|
|
|
|
def _get_type_label(self, ct: CommunicationType) -> str:
|
|
labels = {
|
|
CommunicationType.GENERAL_INFO: "Allgemeine Information",
|
|
CommunicationType.BEHAVIOR: "Verhalten/Disziplin",
|
|
CommunicationType.ACADEMIC: "Schulleistungen",
|
|
CommunicationType.ATTENDANCE: "Fehlzeiten",
|
|
CommunicationType.MEETING_INVITE: "Einladung zum Gespraech",
|
|
CommunicationType.POSITIVE_FEEDBACK: "Positives Feedback",
|
|
CommunicationType.CONCERN: "Bedenken aeussern",
|
|
CommunicationType.CONFLICT: "Konfliktloesung",
|
|
CommunicationType.SPECIAL_NEEDS: "Foerderbedarf",
|
|
}
|
|
return labels.get(ct, ct.value)
|
|
|
|
def get_all_tones(self) -> List[Dict[str, str]]:
|
|
labels = {
|
|
CommunicationTone.FORMAL: "Sehr foermlich",
|
|
CommunicationTone.PROFESSIONAL: "Professionell-freundlich",
|
|
CommunicationTone.WARM: "Warmherzig",
|
|
CommunicationTone.CONCERNED: "Besorgt",
|
|
CommunicationTone.APPRECIATIVE: "Wertschaetzend",
|
|
}
|
|
return [{"value": t.value, "label": labels.get(t, t.value)} for t in CommunicationTone]
|
|
|
|
def get_states(self) -> List[Dict[str, str]]:
|
|
return [
|
|
{"value": "NRW", "label": "Nordrhein-Westfalen"},
|
|
{"value": "BY", "label": "Bayern"},
|
|
{"value": "BW", "label": "Baden-Wuerttemberg"},
|
|
{"value": "NI", "label": "Niedersachsen"},
|
|
{"value": "HE", "label": "Hessen"},
|
|
{"value": "SN", "label": "Sachsen"},
|
|
{"value": "RP", "label": "Rheinland-Pfalz"},
|
|
{"value": "SH", "label": "Schleswig-Holstein"},
|
|
{"value": "BE", "label": "Berlin"},
|
|
{"value": "BB", "label": "Brandenburg"},
|
|
{"value": "MV", "label": "Mecklenburg-Vorpommern"},
|
|
{"value": "ST", "label": "Sachsen-Anhalt"},
|
|
{"value": "TH", "label": "Thueringen"},
|
|
{"value": "HH", "label": "Hamburg"},
|
|
{"value": "HB", "label": "Bremen"},
|
|
{"value": "SL", "label": "Saarland"},
|
|
]
|
|
|
|
|
|
_communication_service: Optional[CommunicationService] = None
|
|
|
|
|
|
def get_communication_service() -> CommunicationService:
|
|
"""Gibt die Singleton-Instanz des CommunicationService zurueck."""
|
|
global _communication_service
|
|
if _communication_service is None:
|
|
_communication_service = CommunicationService()
|
|
return _communication_service
|