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>
615 lines
24 KiB
Python
615 lines
24 KiB
Python
"""
|
|
Communication Service - KI-gestützte Lehrer-Eltern-Kommunikation.
|
|
|
|
Unterstützt Lehrkräfte bei der Erstellung professioneller, rechtlich fundierter
|
|
Kommunikation mit Eltern. Basiert auf den Prinzipien der gewaltfreien Kommunikation
|
|
(GFK nach Marshall Rosenberg) und deutschen Schulgesetzen.
|
|
|
|
Die rechtlichen Referenzen werden dynamisch aus der Datenbank geladen
|
|
(edu_search_documents Tabelle), nicht mehr hardcoded.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from typing import Optional, List, Dict, Any
|
|
from enum import Enum, auto
|
|
from dataclasses import dataclass
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Legal Crawler API URL (für dynamische Rechtsinhalte)
|
|
LEGAL_CRAWLER_API_URL = os.getenv(
|
|
"LEGAL_CRAWLER_API_URL",
|
|
"http://localhost:8000/v1/legal-crawler"
|
|
)
|
|
|
|
|
|
class CommunicationType(str, Enum):
|
|
"""Arten von Eltern-Kommunikation."""
|
|
GENERAL_INFO = "general_info" # Allgemeine Information
|
|
BEHAVIOR = "behavior" # Verhalten/Disziplin
|
|
ACADEMIC = "academic" # Schulleistungen
|
|
ATTENDANCE = "attendance" # Anwesenheit/Fehlzeiten
|
|
MEETING_INVITE = "meeting_invite" # Einladung zum Gespräch
|
|
POSITIVE_FEEDBACK = "positive_feedback" # Positives Feedback
|
|
CONCERN = "concern" # Bedenken äußern
|
|
CONFLICT = "conflict" # Konfliktlösung
|
|
SPECIAL_NEEDS = "special_needs" # Förderbedarf
|
|
|
|
|
|
class CommunicationTone(str, Enum):
|
|
"""Tonalität der Kommunikation."""
|
|
FORMAL = "formal" # Sehr förmlich
|
|
PROFESSIONAL = "professional" # Professionell-freundlich
|
|
WARM = "warm" # Warmherzig
|
|
CONCERNED = "concerned" # Besorgt
|
|
APPRECIATIVE = "appreciative" # Wertschätzend
|
|
|
|
|
|
@dataclass
|
|
class LegalReference:
|
|
"""Rechtliche Referenz für Kommunikation."""
|
|
law: str # z.B. "SchulG NRW"
|
|
paragraph: str # z.B. "§ 42"
|
|
title: str # z.B. "Pflichten der Eltern"
|
|
summary: str # Kurzzusammenfassung
|
|
relevance: str # Warum relevant für diesen Fall
|
|
|
|
|
|
@dataclass
|
|
class GFKPrinciple:
|
|
"""Prinzip der Gewaltfreien Kommunikation."""
|
|
principle: str # z.B. "Beobachtung"
|
|
description: str # Erklärung
|
|
example: str # Beispiel im Kontext
|
|
|
|
|
|
# Fallback Rechtliche Grundlagen (nur verwendet wenn DB leer)
|
|
# Die primäre Quelle sind gecrawlte Dokumente in der edu_search_documents Tabelle
|
|
FALLBACK_LEGAL_REFERENCES: Dict[str, Dict[str, LegalReference]] = {
|
|
"DEFAULT": {
|
|
"elternpflichten": LegalReference(
|
|
law="Landesschulgesetz",
|
|
paragraph="(je nach Bundesland)",
|
|
title="Pflichten der Eltern",
|
|
summary="Eltern haben die Pflicht, die schulische Entwicklung zu unterstützen.",
|
|
relevance="Grundlage für Kooperationsaufforderungen"
|
|
),
|
|
"schulpflicht": LegalReference(
|
|
law="Landesschulgesetz",
|
|
paragraph="(je nach Bundesland)",
|
|
title="Schulpflicht",
|
|
summary="Kinder sind schulpflichtig. Eltern sind verantwortlich für regelmäßigen Schulbesuch.",
|
|
relevance="Bei Fehlzeiten und Anwesenheitsproblemen"
|
|
),
|
|
}
|
|
}
|
|
|
|
|
|
async def fetch_legal_references_from_db(state: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Lädt rechtliche Referenzen aus der Datenbank (via Legal Crawler API).
|
|
|
|
Args:
|
|
state: Bundesland-Kürzel (z.B. "NRW", "BY", "NW")
|
|
|
|
Returns:
|
|
Liste von Rechtsdokumenten mit Paragraphen
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(
|
|
f"{LEGAL_CRAWLER_API_URL}/references/{state}"
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return data.get("documents", [])
|
|
else:
|
|
logger.warning(f"Legal API returned {response.status_code} for state {state}")
|
|
return []
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden rechtlicher Referenzen für {state}: {e}")
|
|
return []
|
|
|
|
|
|
def parse_db_references_to_legal_refs(
|
|
db_docs: List[Dict[str, Any]],
|
|
topic: str
|
|
) -> List[LegalReference]:
|
|
"""
|
|
Konvertiert DB-Dokumente in LegalReference-Objekte.
|
|
|
|
Filtert nach relevanten Paragraphen basierend auf dem Topic.
|
|
"""
|
|
references = []
|
|
|
|
# Topic zu relevanten Paragraph-Nummern mapping
|
|
topic_keywords = {
|
|
"elternpflichten": ["42", "76", "85", "eltern", "pflicht"],
|
|
"schulpflicht": ["41", "35", "schulpflicht", "pflicht"],
|
|
"ordnungsmassnahmen": ["53", "ordnung", "erzieh", "maßnahm"],
|
|
"datenschutz": ["120", "daten", "schutz"],
|
|
"foerderung": ["2", "förder", "bildung", "auftrag"],
|
|
}
|
|
|
|
keywords = topic_keywords.get(topic, ["eltern"])
|
|
|
|
for doc in db_docs:
|
|
law_name = doc.get("law_name", doc.get("title", "Schulgesetz"))
|
|
paragraphs = doc.get("paragraphs", [])
|
|
|
|
if not paragraphs:
|
|
# Wenn keine Paragraphen extrahiert, allgemeine Referenz erstellen
|
|
references.append(LegalReference(
|
|
law=law_name,
|
|
paragraph="(siehe Gesetzestext)",
|
|
title=doc.get("title", "Schulgesetz"),
|
|
summary=f"Rechtliche Grundlage aus {law_name}",
|
|
relevance=f"Relevant für {topic}"
|
|
))
|
|
continue
|
|
|
|
# Relevante Paragraphen finden
|
|
for para in paragraphs[:10]: # Max 10 Paragraphen prüfen
|
|
para_nr = para.get("nr", "")
|
|
para_title = para.get("title", "")
|
|
|
|
# Prüfen ob Paragraph relevant ist
|
|
is_relevant = False
|
|
for keyword in keywords:
|
|
if keyword.lower() in para_nr.lower() or keyword.lower() in para_title.lower():
|
|
is_relevant = True
|
|
break
|
|
|
|
if is_relevant:
|
|
references.append(LegalReference(
|
|
law=law_name,
|
|
paragraph=para_nr,
|
|
title=para_title[:100],
|
|
summary=f"{para_title[:150]}",
|
|
relevance=f"Relevant für {topic}"
|
|
))
|
|
|
|
return references
|
|
|
|
# GFK-Prinzipien
|
|
GFK_PRINCIPLES = [
|
|
GFKPrinciple(
|
|
principle="Beobachtung",
|
|
description="Konkrete Handlungen beschreiben ohne Bewertung oder Interpretation",
|
|
example="'Ich habe bemerkt, dass Max in den letzten zwei Wochen dreimal ohne Hausaufgaben kam.' statt 'Max ist faul.'"
|
|
),
|
|
GFKPrinciple(
|
|
principle="Gefühle",
|
|
description="Eigene Gefühle ausdrücken (Ich-Botschaften)",
|
|
example="'Ich mache mir Sorgen...' statt 'Sie müssen endlich...'"
|
|
),
|
|
GFKPrinciple(
|
|
principle="Bedürfnisse",
|
|
description="Dahinterliegende Bedürfnisse benennen",
|
|
example="'Mir ist wichtig, dass Max sein Potential entfalten kann.' statt 'Sie müssen mehr kontrollieren.'"
|
|
),
|
|
GFKPrinciple(
|
|
principle="Bitten",
|
|
description="Konkrete, erfüllbare Bitten formulieren",
|
|
example="'Wären Sie bereit, täglich die Hausaufgaben zu prüfen?' statt 'Tun Sie endlich etwas!'"
|
|
),
|
|
]
|
|
|
|
|
|
# Kommunikationsvorlagen
|
|
COMMUNICATION_TEMPLATES: Dict[CommunicationType, Dict[str, str]] = {
|
|
CommunicationType.GENERAL_INFO: {
|
|
"subject": "Information: {topic}",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich möchte Sie über folgendes informieren:",
|
|
"closing": "Bei Fragen stehe ich Ihnen gerne zur Verfügung.\n\nMit freundlichen Grüßen",
|
|
},
|
|
CommunicationType.BEHAVIOR: {
|
|
"subject": "Gesprächswunsch: {student_name}",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich wende mich heute an Sie, da mir das Wohlergehen von {student_name} sehr am Herzen liegt.",
|
|
"closing": "Ich bin überzeugt, dass wir gemeinsam eine gute Lösung finden können. Ich würde mich über ein Gespräch freuen.\n\nMit freundlichen Grüßen",
|
|
},
|
|
CommunicationType.ACADEMIC: {
|
|
"subject": "Schulische Entwicklung: {student_name}",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich möchte Sie über die schulische Entwicklung von {student_name} informieren.",
|
|
"closing": "Ich würde mich freuen, wenn wir gemeinsam überlegen könnten, wie wir {student_name} optimal unterstützen können.\n\nMit freundlichen Grüßen",
|
|
},
|
|
CommunicationType.ATTENDANCE: {
|
|
"subject": "Fehlzeiten: {student_name}",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich wende mich an Sie bezüglich der Anwesenheit von {student_name}.",
|
|
"closing": "Gemäß {legal_reference} sind regelmäßige Fehlzeiten meldepflichtig. Ich bin sicher, dass wir gemeinsam eine Lösung finden.\n\nMit freundlichen Grüßen",
|
|
},
|
|
CommunicationType.MEETING_INVITE: {
|
|
"subject": "Einladung zum Elterngespräch",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich würde mich freuen, Sie zu einem persönlichen Gespräch einzuladen.",
|
|
"closing": "Bitte teilen Sie mir mit, ob einer der vorgeschlagenen Termine für Sie passt, oder nennen Sie mir einen Alternativtermin.\n\nMit freundlichen Grüßen",
|
|
},
|
|
CommunicationType.POSITIVE_FEEDBACK: {
|
|
"subject": "Positive Rückmeldung: {student_name}",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich freue mich, Ihnen heute eine erfreuliche Nachricht mitteilen zu können.",
|
|
"closing": "Ich freue mich, {student_name} auf diesem positiven Weg weiter begleiten zu dürfen.\n\nMit herzlichen Grüßen",
|
|
},
|
|
CommunicationType.CONCERN: {
|
|
"subject": "Gemeinsame Sorge: {student_name}",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich wende mich heute an Sie, weil mir etwas aufgefallen ist, das ich gerne mit Ihnen besprechen würde.",
|
|
"closing": "Ich bin überzeugt, dass wir im Sinne von {student_name} gemeinsam eine gute Lösung finden werden.\n\nMit freundlichen Grüßen",
|
|
},
|
|
CommunicationType.CONFLICT: {
|
|
"subject": "Bitte um ein klärendes Gespräch",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich möchte das Gespräch mit Ihnen suchen, da mir eine konstruktive Zusammenarbeit sehr wichtig ist.",
|
|
"closing": "Mir liegt eine gute Kooperation zum Wohl von {student_name} am Herzen. Ich bin überzeugt, dass wir im Dialog eine für alle Seiten gute Lösung finden können.\n\nMit freundlichen Grüßen",
|
|
},
|
|
CommunicationType.SPECIAL_NEEDS: {
|
|
"subject": "Förderung: {student_name}",
|
|
"opening": "Sehr geehrte/r {parent_name},\n\nich möchte mit Ihnen über die individuelle Förderung von {student_name} sprechen.",
|
|
"closing": "Gemäß dem Bildungsauftrag ({legal_reference}) ist es uns ein besonderes Anliegen, jedes Kind optimal zu fördern. Lassen Sie uns gemeinsam überlegen, wie wir {student_name} bestmöglich unterstützen können.\n\nMit freundlichen Grüßen",
|
|
},
|
|
}
|
|
|
|
|
|
class CommunicationService:
|
|
"""
|
|
Service zur Unterstützung von Lehrer-Eltern-Kommunikation.
|
|
|
|
Generiert professionelle, rechtlich fundierte und empathische Nachrichten
|
|
basierend auf den Prinzipien der gewaltfreien Kommunikation.
|
|
|
|
Rechtliche Referenzen werden dynamisch aus der DB geladen (via Legal Crawler API).
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.fallback_references = FALLBACK_LEGAL_REFERENCES
|
|
self.gfk_principles = GFK_PRINCIPLES
|
|
self.templates = COMMUNICATION_TEMPLATES
|
|
# Cache für DB-Referenzen (um wiederholte API-Calls zu vermeiden)
|
|
self._cached_references: Dict[str, List[LegalReference]] = {}
|
|
|
|
async def get_legal_references_async(
|
|
self,
|
|
state: str,
|
|
topic: str
|
|
) -> List[LegalReference]:
|
|
"""
|
|
Gibt relevante rechtliche Referenzen für ein Bundesland und Thema zurück.
|
|
Lädt aus DB via Legal Crawler API.
|
|
|
|
Args:
|
|
state: Bundesland-Kürzel (z.B. "NRW", "BY", "NW")
|
|
topic: Themenbereich (z.B. "elternpflichten", "schulpflicht")
|
|
|
|
Returns:
|
|
Liste relevanter LegalReference-Objekte
|
|
"""
|
|
cache_key = f"{state}:{topic}"
|
|
|
|
# Cache prüfen
|
|
if cache_key in self._cached_references:
|
|
return self._cached_references[cache_key]
|
|
|
|
# Aus DB laden
|
|
db_docs = await fetch_legal_references_from_db(state)
|
|
|
|
if db_docs:
|
|
# DB-Dokumente in LegalReference konvertieren
|
|
references = parse_db_references_to_legal_refs(db_docs, topic)
|
|
if references:
|
|
self._cached_references[cache_key] = references
|
|
return references
|
|
|
|
# Fallback wenn DB leer
|
|
logger.info(f"Keine DB-Referenzen für {state}/{topic}, nutze Fallback")
|
|
return self._get_fallback_references(state, topic)
|
|
|
|
def get_legal_references(
|
|
self,
|
|
state: str,
|
|
topic: str
|
|
) -> List[LegalReference]:
|
|
"""
|
|
Synchrone Methode für Rückwärtskompatibilität.
|
|
Nutzt nur Fallback-Referenzen (für non-async Kontexte).
|
|
|
|
Für dynamische DB-Referenzen bitte get_legal_references_async() verwenden.
|
|
"""
|
|
return self._get_fallback_references(state, topic)
|
|
|
|
def _get_fallback_references(
|
|
self,
|
|
state: str,
|
|
topic: str
|
|
) -> List[LegalReference]:
|
|
"""Gibt Fallback-Referenzen zurück."""
|
|
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]:
|
|
"""
|
|
Gibt GFK-Leitlinien für einen Kommunikationstyp zurück.
|
|
"""
|
|
return self.gfk_principles
|
|
|
|
def get_template(
|
|
self,
|
|
comm_type: CommunicationType
|
|
) -> Dict[str, str]:
|
|
"""
|
|
Gibt die Vorlage für einen Kommunikationstyp zurück.
|
|
"""
|
|
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 für die KI-gestützte Nachrichtengenerierung.
|
|
|
|
Args:
|
|
comm_type: Art der Kommunikation
|
|
state: Bundesland für rechtliche Referenzen
|
|
tone: Gewünschte Tonalität
|
|
|
|
Returns:
|
|
System-Prompt für LLM
|
|
"""
|
|
# Rechtliche Referenzen sammeln
|
|
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"
|
|
|
|
# Tonalität beschreiben
|
|
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: "Drücke aufrichtige Sorge und Empathie aus.",
|
|
CommunicationTone.APPRECIATIVE: "Betone Wertschätzung und positives Feedback.",
|
|
}
|
|
tone_desc = tone_descriptions.get(tone, tone_descriptions[CommunicationTone.PROFESSIONAL])
|
|
|
|
system_prompt = f"""Du bist ein erfahrener Kommunikationsberater für Lehrkräfte 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. GEFÜHLE: Drücke Gefühle als Ich-Botschaften aus
|
|
Beispiel: "Ich mache mir Sorgen..." statt "Sie müssen..."
|
|
|
|
3. BEDÜRFNISSE: Benenne dahinterliegende Bedürfnisse
|
|
Beispiel: "Mir ist wichtig, dass..." statt "Sie sollten..."
|
|
|
|
4. BITTEN: Formuliere konkrete, erfüllbare Bitten
|
|
Beispiel: "Wären Sie bereit, ...?" statt "Tun Sie endlich...!"
|
|
|
|
WICHTIGE REGELN:
|
|
- Immer die Würde aller Beteiligten wahren
|
|
- Keine Schuldzuweisungen oder Vorwürfe
|
|
- Lösungsorientiert statt problemfokussiert
|
|
- Auf Augenhöhe kommunizieren
|
|
- Kooperation statt Konfrontation
|
|
- Deutsche Sprache, förmliche Anrede (Sie)
|
|
- Sachlich, aber empathisch
|
|
{legal_context}
|
|
|
|
TONALITÄT:
|
|
{tone_desc}
|
|
|
|
FORMAT:
|
|
- Verfasse den Brief als vollständigen, versandfertigen Text
|
|
- Beginne mit der Anrede
|
|
- Strukturiere den Inhalt klar und verständlich
|
|
- Schließe mit einer freundlichen Grußformel
|
|
- Die Signatur (Name der Lehrkraft) wird später hinzugefügt
|
|
|
|
WICHTIG: Der Brief soll professionell und rechtlich einwandfrei sein, aber gleichzeitig
|
|
menschlich und einladend wirken. Ziel ist immer eine konstruktive Zusammenarbeit."""
|
|
|
|
return system_prompt
|
|
|
|
def build_user_prompt(
|
|
self,
|
|
comm_type: CommunicationType,
|
|
context: Dict[str, Any]
|
|
) -> str:
|
|
"""
|
|
Erstellt den User-Prompt aus dem Kontext.
|
|
|
|
Args:
|
|
comm_type: Art der Kommunikation
|
|
context: Kontextinformationen (student_name, parent_name, situation, etc.)
|
|
|
|
Returns:
|
|
User-Prompt für LLM
|
|
"""
|
|
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 Elterngespräch",
|
|
CommunicationType.POSITIVE_FEEDBACK: "positives Feedback",
|
|
CommunicationType.CONCERN: "eine Sorge oder ein Anliegen",
|
|
CommunicationType.CONFLICT: "eine konflikthafte Situation",
|
|
CommunicationType.SPECIAL_NEEDS: "Förderbedarf oder besondere Unterstützung",
|
|
}
|
|
type_desc = type_descriptions.get(comm_type, "ein Anliegen")
|
|
|
|
user_prompt = f"""Schreibe einen Elternbrief zu folgendem Anlass: {type_desc}
|
|
|
|
Schülername: {student_name}
|
|
Elternname: {parent_name}
|
|
|
|
Situation:
|
|
{situation}
|
|
"""
|
|
|
|
if additional_info:
|
|
user_prompt += f"\nZusätzliche 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)
|
|
- Verständnis und Sorge ausdrücken (Gefühle)
|
|
- Das gemeinsame Ziel betonen (Bedürfnisse)
|
|
- Einen konstruktiven Vorschlag machen (Bitte)
|
|
"""
|
|
|
|
return user_prompt
|
|
|
|
def validate_communication(self, text: str) -> Dict[str, Any]:
|
|
"""
|
|
Validiert eine generierte Kommunikation auf GFK-Konformität.
|
|
|
|
Args:
|
|
text: Der zu prüfende Text
|
|
|
|
Returns:
|
|
Validierungsergebnis mit Verbesserungsvorschlägen
|
|
"""
|
|
issues = []
|
|
suggestions = []
|
|
|
|
# Prüfe auf problematische Formulierungen
|
|
problematic_patterns = [
|
|
("Sie müssen", "Vorschlag: 'Wären Sie bereit, ...' oder 'Ich bitte Sie, ...'"),
|
|
("Sie sollten", "Vorschlag: 'Ich würde mir wünschen, ...'"),
|
|
("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"),
|
|
("unverschämt", "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)
|
|
|
|
# Prüfe auf positive Elemente
|
|
positive_elements = []
|
|
positive_patterns = [
|
|
("Ich habe bemerkt", "Gute Beobachtung"),
|
|
("Ich möchte", "Gute Ich-Botschaft"),
|
|
("gemeinsam", "Gute Kooperationsorientierung"),
|
|
("wichtig", "Gutes Bedürfnis-Statement"),
|
|
("freuen", "Positive Tonalität"),
|
|
("Wären 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]]:
|
|
"""Gibt alle verfügbaren Kommunikationstypen zurück."""
|
|
return [
|
|
{"value": ct.value, "label": self._get_type_label(ct)}
|
|
for ct in CommunicationType
|
|
]
|
|
|
|
def _get_type_label(self, ct: CommunicationType) -> str:
|
|
"""Gibt das deutsche Label für einen Kommunikationstyp zurück."""
|
|
labels = {
|
|
CommunicationType.GENERAL_INFO: "Allgemeine Information",
|
|
CommunicationType.BEHAVIOR: "Verhalten/Disziplin",
|
|
CommunicationType.ACADEMIC: "Schulleistungen",
|
|
CommunicationType.ATTENDANCE: "Fehlzeiten",
|
|
CommunicationType.MEETING_INVITE: "Einladung zum Gespräch",
|
|
CommunicationType.POSITIVE_FEEDBACK: "Positives Feedback",
|
|
CommunicationType.CONCERN: "Bedenken äußern",
|
|
CommunicationType.CONFLICT: "Konfliktlösung",
|
|
CommunicationType.SPECIAL_NEEDS: "Förderbedarf",
|
|
}
|
|
return labels.get(ct, ct.value)
|
|
|
|
def get_all_tones(self) -> List[Dict[str, str]]:
|
|
"""Gibt alle verfügbaren Tonalitäten zurück."""
|
|
labels = {
|
|
CommunicationTone.FORMAL: "Sehr förmlich",
|
|
CommunicationTone.PROFESSIONAL: "Professionell-freundlich",
|
|
CommunicationTone.WARM: "Warmherzig",
|
|
CommunicationTone.CONCERNED: "Besorgt",
|
|
CommunicationTone.APPRECIATIVE: "Wertschätzend",
|
|
}
|
|
return [
|
|
{"value": t.value, "label": labels.get(t, t.value)}
|
|
for t in CommunicationTone
|
|
]
|
|
|
|
def get_states(self) -> List[Dict[str, str]]:
|
|
"""Gibt alle verfügbaren Bundesländer zurück."""
|
|
return [
|
|
{"value": "NRW", "label": "Nordrhein-Westfalen"},
|
|
{"value": "BY", "label": "Bayern"},
|
|
{"value": "BW", "label": "Baden-Württemberg"},
|
|
{"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": "Thüringen"},
|
|
{"value": "HH", "label": "Hamburg"},
|
|
{"value": "HB", "label": "Bremen"},
|
|
{"value": "SL", "label": "Saarland"},
|
|
]
|
|
|
|
|
|
# Singleton-Instanz
|
|
_communication_service: Optional[CommunicationService] = None
|
|
|
|
|
|
def get_communication_service() -> CommunicationService:
|
|
"""Gibt die Singleton-Instanz des CommunicationService zurück."""
|
|
global _communication_service
|
|
if _communication_service is None:
|
|
_communication_service = CommunicationService()
|
|
return _communication_service
|