[split-required] Split remaining 500-680 LOC files (final batch)

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>
This commit is contained in:
Benjamin Admin
2026-04-25 08:56:45 +02:00
parent b4613e26f3
commit 451365a312
115 changed files with 10694 additions and 13839 deletions

View File

@@ -6,6 +6,8 @@ from .inference import InferenceService, get_inference_service
from .playbook_service import PlaybookService
from .pii_detector import PIIDetector, get_pii_detector, PIIType, RedactionResult
from .tool_gateway import ToolGateway, get_tool_gateway, SearchDepth
from .communication_service import CommunicationService, get_communication_service
from .communication_types import CommunicationType, CommunicationTone, LegalReference, GFKPrinciple
__all__ = [
"InferenceService",
@@ -18,4 +20,10 @@ __all__ = [
"ToolGateway",
"get_tool_gateway",
"SearchDepth",
"CommunicationService",
"get_communication_service",
"CommunicationType",
"CommunicationTone",
"LegalReference",
"GFKPrinciple",
]

View File

@@ -1,371 +1,95 @@
"""
Communication Service - KI-gestützte Lehrer-Eltern-Kommunikation.
Communication Service - KI-gestuetzte 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.
Split into:
- communication_types.py: Enums, data classes, templates, legal references
- communication_service.py (this file): CommunicationService class
Die rechtlichen Referenzen werden dynamisch aus der Datenbank geladen
(edu_search_documents Tabelle), nicht mehr hardcoded.
All symbols are re-exported here for backward compatibility.
"""
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"
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,
)
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!'"
),
# 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",
]
# 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",
},
}
logger = logging.getLogger(__name__)
class CommunicationService:
"""
Service zur Unterstützung von Lehrer-Eltern-Kommunikation.
Service zur Unterstuetzung 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
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
"""
"""Gibt relevante rechtliche Referenzen fuer ein Bundesland und Thema zurueck."""
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")
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 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.
"""
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 zurück."""
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]:
"""
Gibt GFK-Leitlinien für einen Kommunikationstyp zurück.
"""
def get_gfk_guidance(self, comm_type: CommunicationType) -> List[GFKPrinciple]:
return self.gfk_principles
def get_template(
self,
comm_type: CommunicationType
) -> Dict[str, str]:
"""
Gibt die Vorlage für einen Kommunikationstyp zurück.
"""
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
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
"""Erstellt den System-Prompt fuer die KI-gestuetzte Nachrichtengenerierung."""
topic_map = {
CommunicationType.ATTENDANCE: "schulpflicht",
CommunicationType.BEHAVIOR: "ordnungsmassnahmen",
@@ -383,17 +107,16 @@ class CommunicationService:
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.",
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])
system_prompt = f"""Du bist ein erfahrener Kommunikationsberater für Lehrkräfte im deutschen Schulsystem.
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):
@@ -401,55 +124,42 @@ 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..."
2. GEFUEHLE: Druecke Gefuehle als Ich-Botschaften aus
Beispiel: "Ich mache mir Sorgen..." statt "Sie muessen..."
3. BEDÜRFNISSE: Benenne dahinterliegende Bedürfnisse
3. BEDUERFNISSE: Benenne dahinterliegende Beduerfnisse
Beispiel: "Mir ist wichtig, dass..." statt "Sie sollten..."
4. BITTEN: Formuliere konkrete, erfüllbare Bitten
Beispiel: "Wären Sie bereit, ...?" statt "Tun Sie endlich...!"
4. BITTEN: Formuliere konkrete, erfuellbare Bitten
Beispiel: "Waeren 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
- Immer die Wuerde aller Beteiligten wahren
- Keine Schuldzuweisungen oder Vorwuerfe
- Loesungsorientiert statt problemfokussiert
- Auf Augenhoehe kommunizieren
- Kooperation statt Konfrontation
- Deutsche Sprache, förmliche Anrede (Sie)
- Deutsche Sprache, foermliche Anrede (Sie)
- Sachlich, aber empathisch
{legal_context}
TONALITÄT:
TONALITAET:
{tone_desc}
FORMAT:
- Verfasse den Brief als vollständigen, versandfertigen Text
- Verfasse den Brief als vollstaendigen, 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
- 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."""
return system_prompt
def build_user_prompt(
self,
comm_type: CommunicationType,
context: Dict[str, Any]
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
"""
"""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", "")
@@ -460,59 +170,48 @@ menschlich und einladend wirken. Ziel ist immer eine konstruktive Zusammenarbeit
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.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: "Förderbedarf oder besondere Unterstützung",
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}
Schülername: {student_name}
Schuelername: {student_name}
Elternname: {parent_name}
Situation:
{situation}
"""
if additional_info:
user_prompt += f"\nZusätzliche Informationen:\n{additional_info}\n"
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)
- Verständnis und Sorge ausdrücken (Gefühle)
- Das gemeinsame Ziel betonen (Bedürfnisse)
- 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-Konformität.
Args:
text: Der zu prüfende Text
Returns:
Validierungsergebnis mit Verbesserungsvorschlägen
"""
"""Validiert eine generierte Kommunikation auf GFK-Konformitaet."""
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, ...'"),
("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"),
("unverschämt", "Vorschlag: Verhalten konkret beschreiben statt bewerten"),
("unverschaemt", "Vorschlag: Verhalten konkret beschreiben statt bewerten"),
("respektlos", "Vorschlag: Verhalten konkret beschreiben statt bewerten"),
]
@@ -521,15 +220,14 @@ Der Brief sollte:
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"),
("Ich moechte", "Gute Ich-Botschaft"),
("gemeinsam", "Gute Kooperationsorientierung"),
("wichtig", "Gutes Bedürfnis-Statement"),
("freuen", "Positive Tonalität"),
("Wären Sie bereit", "Gute Bitte-Formulierung"),
("wichtig", "Gutes Beduerfnis-Statement"),
("freuen", "Positive Tonalitaet"),
("Waeren Sie bereit", "Gute Bitte-Formulierung"),
]
for pattern, feedback in positive_patterns:
@@ -545,47 +243,37 @@ Der Brief sollte:
}
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
]
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.MEETING_INVITE: "Einladung zum Gespraech",
CommunicationType.POSITIVE_FEEDBACK: "Positives Feedback",
CommunicationType.CONCERN: "Bedenken äußern",
CommunicationType.CONFLICT: "Konfliktlösung",
CommunicationType.SPECIAL_NEEDS: "Förderbedarf",
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]]:
"""Gibt alle verfügbaren Tonalitäten zurück."""
labels = {
CommunicationTone.FORMAL: "Sehr förmlich",
CommunicationTone.FORMAL: "Sehr foermlich",
CommunicationTone.PROFESSIONAL: "Professionell-freundlich",
CommunicationTone.WARM: "Warmherzig",
CommunicationTone.CONCERNED: "Besorgt",
CommunicationTone.APPRECIATIVE: "Wertschätzend",
CommunicationTone.APPRECIATIVE: "Wertschaetzend",
}
return [
{"value": t.value, "label": labels.get(t, t.value)}
for t in CommunicationTone
]
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": "BW", "label": "Baden-Wuerttemberg"},
{"value": "NI", "label": "Niedersachsen"},
{"value": "HE", "label": "Hessen"},
{"value": "SN", "label": "Sachsen"},
@@ -595,19 +283,18 @@ Der Brief sollte:
{"value": "BB", "label": "Brandenburg"},
{"value": "MV", "label": "Mecklenburg-Vorpommern"},
{"value": "ST", "label": "Sachsen-Anhalt"},
{"value": "TH", "label": "Thüringen"},
{"value": "TH", "label": "Thueringen"},
{"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."""
"""Gibt die Singleton-Instanz des CommunicationService zurueck."""
global _communication_service
if _communication_service is None:
_communication_service = CommunicationService()

View File

@@ -0,0 +1,209 @@
"""
Communication Types - Enums, data classes, templates, and legal references.
"""
import logging
import os
from typing import Optional, List, Dict, Any
from enum import Enum
from dataclasses import dataclass
import httpx
logger = logging.getLogger(__name__)
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"
BEHAVIOR = "behavior"
ACADEMIC = "academic"
ATTENDANCE = "attendance"
MEETING_INVITE = "meeting_invite"
POSITIVE_FEEDBACK = "positive_feedback"
CONCERN = "concern"
CONFLICT = "conflict"
SPECIAL_NEEDS = "special_needs"
class CommunicationTone(str, Enum):
"""Tonalitaet der Kommunikation."""
FORMAL = "formal"
PROFESSIONAL = "professional"
WARM = "warm"
CONCERNED = "concerned"
APPRECIATIVE = "appreciative"
@dataclass
class LegalReference:
"""Rechtliche Referenz fuer Kommunikation."""
law: str
paragraph: str
title: str
summary: str
relevance: str
@dataclass
class GFKPrinciple:
"""Prinzip der Gewaltfreien Kommunikation."""
principle: str
description: str
example: str
# Fallback Rechtliche Grundlagen (nur verwendet wenn DB leer)
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 unterstuetzen.",
relevance="Grundlage fuer Kooperationsaufforderungen"
),
"schulpflicht": LegalReference(
law="Landesschulgesetz",
paragraph="(je nach Bundesland)",
title="Schulpflicht",
summary="Kinder sind schulpflichtig. Eltern sind verantwortlich fuer regelmaessigen Schulbesuch.",
relevance="Bei Fehlzeiten und Anwesenheitsproblemen"
),
}
}
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="Gefuehle",
description="Eigene Gefuehle ausdruecken (Ich-Botschaften)",
example="'Ich mache mir Sorgen...' statt 'Sie muessen endlich...'"
),
GFKPrinciple(
principle="Beduerfnisse",
description="Dahinterliegende Beduerfnisse benennen",
example="'Mir ist wichtig, dass Max sein Potential entfalten kann.' statt 'Sie muessen mehr kontrollieren.'"
),
GFKPrinciple(
principle="Bitten",
description="Konkrete, erfuellbare Bitten formulieren",
example="'Waeren Sie bereit, taeglich die Hausaufgaben zu pruefen?' statt 'Tun Sie endlich etwas!'"
),
]
COMMUNICATION_TEMPLATES: Dict[CommunicationType, Dict[str, str]] = {
CommunicationType.GENERAL_INFO: {
"subject": "Information: {topic}",
"opening": "Sehr geehrte/r {parent_name},\n\nich moechte Sie ueber folgendes informieren:",
"closing": "Bei Fragen stehe ich Ihnen gerne zur Verfuegung.\n\nMit freundlichen Gruessen",
},
CommunicationType.BEHAVIOR: {
"subject": "Gespraechswunsch: {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 ueberzeugt, dass wir gemeinsam eine gute Loesung finden koennen. Ich wuerde mich ueber ein Gespraech freuen.\n\nMit freundlichen Gruessen",
},
CommunicationType.ACADEMIC: {
"subject": "Schulische Entwicklung: {student_name}",
"opening": "Sehr geehrte/r {parent_name},\n\nich moechte Sie ueber die schulische Entwicklung von {student_name} informieren.",
"closing": "Ich wuerde mich freuen, wenn wir gemeinsam ueberlegen koennten, wie wir {student_name} optimal unterstuetzen koennen.\n\nMit freundlichen Gruessen",
},
CommunicationType.ATTENDANCE: {
"subject": "Fehlzeiten: {student_name}",
"opening": "Sehr geehrte/r {parent_name},\n\nich wende mich an Sie bezueglich der Anwesenheit von {student_name}.",
"closing": "Gemaess {legal_reference} sind regelmaessige Fehlzeiten meldepflichtig. Ich bin sicher, dass wir gemeinsam eine Loesung finden.\n\nMit freundlichen Gruessen",
},
CommunicationType.MEETING_INVITE: {
"subject": "Einladung zum Elterngespraech",
"opening": "Sehr geehrte/r {parent_name},\n\nich wuerde mich freuen, Sie zu einem persoenlichen Gespraech einzuladen.",
"closing": "Bitte teilen Sie mir mit, ob einer der vorgeschlagenen Termine fuer Sie passt, oder nennen Sie mir einen Alternativtermin.\n\nMit freundlichen Gruessen",
},
CommunicationType.POSITIVE_FEEDBACK: {
"subject": "Positive Rueckmeldung: {student_name}",
"opening": "Sehr geehrte/r {parent_name},\n\nich freue mich, Ihnen heute eine erfreuliche Nachricht mitteilen zu koennen.",
"closing": "Ich freue mich, {student_name} auf diesem positiven Weg weiter begleiten zu duerfen.\n\nMit herzlichen Gruessen",
},
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 wuerde.",
"closing": "Ich bin ueberzeugt, dass wir im Sinne von {student_name} gemeinsam eine gute Loesung finden werden.\n\nMit freundlichen Gruessen",
},
CommunicationType.CONFLICT: {
"subject": "Bitte um ein klaerendes Gespraech",
"opening": "Sehr geehrte/r {parent_name},\n\nich moechte das Gespraech 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 ueberzeugt, dass wir im Dialog eine fuer alle Seiten gute Loesung finden koennen.\n\nMit freundlichen Gruessen",
},
CommunicationType.SPECIAL_NEEDS: {
"subject": "Foerderung: {student_name}",
"opening": "Sehr geehrte/r {parent_name},\n\nich moechte mit Ihnen ueber die individuelle Foerderung von {student_name} sprechen.",
"closing": "Gemaess dem Bildungsauftrag ({legal_reference}) ist es uns ein besonderes Anliegen, jedes Kind optimal zu foerdern. Lassen Sie uns gemeinsam ueberlegen, wie wir {student_name} bestmoeglich unterstuetzen koennen.\n\nMit freundlichen Gruessen",
},
}
async def fetch_legal_references_from_db(state: str) -> List[Dict[str, Any]]:
"""Laedt rechtliche Referenzen aus der Datenbank (via Legal Crawler API)."""
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:
return response.json().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 fuer {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."""
references = []
topic_keywords = {
"elternpflichten": ["42", "76", "85", "eltern", "pflicht"],
"schulpflicht": ["41", "35", "schulpflicht", "pflicht"],
"ordnungsmassnahmen": ["53", "ordnung", "erzieh", "massnahm"],
"datenschutz": ["120", "daten", "schutz"],
"foerderung": ["2", "foerder", "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:
references.append(LegalReference(
law=law_name, paragraph="(siehe Gesetzestext)",
title=doc.get("title", "Schulgesetz"),
summary=f"Rechtliche Grundlage aus {law_name}",
relevance=f"Relevant fuer {topic}"
))
continue
for para in paragraphs[:10]:
para_nr = para.get("nr", "")
para_title = para.get("title", "")
is_relevant = any(
kw.lower() in para_nr.lower() or kw.lower() in para_title.lower()
for kw in keywords
)
if is_relevant:
references.append(LegalReference(
law=law_name, paragraph=para_nr,
title=para_title[:100],
summary=f"{para_title[:150]}",
relevance=f"Relevant fuer {topic}"
))
return references