From 12b0d3d36e48dc37067bd9c5c8dfc45b33db08d4 Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Wed, 4 Mar 2026 22:53:41 +0100
Subject: [PATCH] =?UTF-8?q?Alle=209=20Fixes=20sind=20implementiert.=20Hier?=
=?UTF-8?q?=20die=20Zusammenfassung:=20Fix=201=20--=20Opening-Prompt:=20pr?=
=?UTF-8?q?ocessSessionOpening=20in=20serviceCommcoach.py=20pr=C3=BCft=20j?=
=?UTF-8?q?etzt=20ob=20es=20die=20erste=20Session=20ist=20(isFirstSession)?=
=?UTF-8?q?=20und=20gibt=20der=20AI=20einen=20expliziten=20Prompt,=20der?=
=?UTF-8?q?=20das=20Erfinden=20von=20Kontext=20verbietet.=20Fix=202=20--?=
=?UTF-8?q?=20Stabiler=20Transcript:=20onresult=20in=20CommcoachCoachingVi?=
=?UTF-8?q?ew.tsx=20nutzt=20jetzt=20processedResultIndexRef=20um=20nur=20n?=
=?UTF-8?q?eue=20Results=20zu=20verarbeiten.=20Finalisierte=20Teile=20werd?=
=?UTF-8?q?en=20stabil=20akkumuliert,=20kein=20Flackern=20mehr.=20Fix=203?=
=?UTF-8?q?=20--=20Hintergrundger=C3=A4usche-Timeout:=20Neuer=20silenceTim?=
=?UTF-8?q?erRef=20mit=205s=20Timeout.=20Wenn=20nach=20onspeechstart=20kei?=
=?UTF-8?q?n=20Text=20kommt,=20wird=20isUserSpeaking=20automatisch=20zur?=
=?UTF-8?q?=C3=BCckgesetzt.=20Fix=204=20--=20Stop-Button:=20"Stop"=20Butto?=
=?UTF-8?q?n=20erscheint=20im=20Session-Header=20wenn=20TTS=20l=C3=A4uft?=
=?UTF-8?q?=20(via=20isTtsPlaying=20State,=20synchronisiert=20per=20200ms?=
=?UTF-8?q?=20Interval=20mit=20isTtsPlayingRef).=20Fix=205=20--=20Weitersp?=
=?UTF-8?q?rechen-Button:=20lastTtsAudioRef=20speichert=20das=20zuletzt=20?=
=?UTF-8?q?gespielte=20Audio.=20stopTts=20setzt=20wasInterrupted=20=3D=20t?=
=?UTF-8?q?rue.=20"Weitersprechen"=20Button=20erscheint=20nach=20Unterbrec?=
=?UTF-8?q?hung=20und=20spielt=20das=20Audio=20erneut=20ab.=20Fix=206=20--?=
=?UTF-8?q?=20Paralleles=20TTS:=20Neue=20=5FgenerateAndEmitTts()=20Hilfsfu?=
=?UTF-8?q?nktion.=20In=20processMessage=20und=20processSessionOpening=20w?=
=?UTF-8?q?ird=20TTS=20als=20asyncio.create=5Ftask=20parallel=20zu=20=5Fem?=
=?UTF-8?q?itChunkedResponse=20gestartet.=20Fix=207=20--=20JSON-Response:?=
=?UTF-8?q?=20Die=20AI=20antwortet=20jetzt=20als=20JSON=20mit=20text,=20sp?=
=?UTF-8?q?eech,=20documents.=20Neuer=20Prompt-Block=20wird=20in=20buildCo?=
=?UTF-8?q?achingSystemPrompt=20angeh=C3=A4ngt.=20=5FparseAiJsonResponse()?=
=?UTF-8?q?=20und=20=5FsaveGeneratedDocument()=20im=20Backend.=20processMe?=
=?UTF-8?q?ssage=20und=20processSessionOpening=20nutzen=20die=20neue=20Str?=
=?UTF-8?q?uktur.=20Fix=208=20--=20Loading-States:=20Neuer=20actionLoading?=
=?UTF-8?q?=20State=20in=20useCommcoach.=20Alle=20async=20Funktionen=20set?=
=?UTF-8?q?zen=20setActionLoading('key')=20vor=20dem=20Await=20und=20null?=
=?UTF-8?q?=20im=20finally.=20Buttons=20zeigen=20Loading-Text=20und=20werd?=
=?UTF-8?q?en=20disabled.=20Fix=209=20--=20Umlaute:=20Alle=20deutschen=20S?=
=?UTF-8?q?trings=20in=20allen=20CommCoach-Dateien=20(Backend=20+=20Fronte?=
=?UTF-8?q?nd)=20korrigiert:=20ae->=C3=A4,=20oe->=C3=B6,=20ue->=C3=BC.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../features/commcoach/serviceCommcoach.py | 209 ++++++++++--------
.../features/commcoach/serviceCommcoachAi.py | 113 ++++++----
.../commcoach/serviceCommcoachExport.py | 4 +-
.../commcoach/serviceCommcoachGamification.py | 2 +-
.../commcoach/serviceCommcoachPersonas.py | 46 ++--
.../commcoach/tests/test_serviceAi.py | 6 +-
6 files changed, 212 insertions(+), 168 deletions(-)
diff --git a/modules/features/commcoach/serviceCommcoach.py b/modules/features/commcoach/serviceCommcoach.py
index f1edf90d..df490aa2 100644
--- a/modules/features/commcoach/serviceCommcoach.py
+++ b/modules/features/commcoach/serviceCommcoach.py
@@ -87,6 +87,74 @@ CHUNK_WORD_SIZE = 4
CHUNK_DELAY_SECONDS = 0.05
+def _parseAiJsonResponse(rawText: str) -> Dict[str, Any]:
+ """Parse the structured JSON response from AI. Strips optional markdown code fences."""
+ text = rawText.strip()
+ if text.startswith("```"):
+ lines = text.split("\n")
+ lines = lines[1:]
+ if lines and lines[-1].strip() == "```":
+ lines = lines[:-1]
+ text = "\n".join(lines)
+ try:
+ return json.loads(text)
+ except json.JSONDecodeError:
+ logger.warning(f"AI JSON parse failed, using raw text: {text[:200]}")
+ return {"text": rawText.strip(), "speech": "", "documents": []}
+
+
+async def _generateAndEmitTts(sessionId: str, speechText: str, currentUser, mandateId: str,
+ instanceId: str, interface):
+ """Generate TTS audio from speech text and emit as SSE event."""
+ if not speechText:
+ return
+ try:
+ from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
+ import base64
+ voiceInterface = getVoiceInterface(currentUser, mandateId)
+ profile = interface.getProfile(str(currentUser.id), instanceId)
+ language = profile.get("preferredLanguage", "de-DE") if profile else "de-DE"
+ voiceName = profile.get("preferredVoice") if profile else None
+ ttsResult = await voiceInterface.textToSpeech(
+ text=_stripMarkdownForTts(speechText),
+ languageCode=language,
+ voiceName=voiceName,
+ )
+ if ttsResult and isinstance(ttsResult, dict):
+ audioBytes = ttsResult.get("audioContent")
+ if audioBytes:
+ audioB64 = base64.b64encode(
+ audioBytes if isinstance(audioBytes, bytes) else audioBytes.encode()
+ ).decode()
+ await emitSessionEvent(sessionId, "ttsAudio", {"audio": audioB64, "format": "mp3"})
+ except Exception as e:
+ logger.warning(f"TTS failed for session {sessionId}: {e}")
+
+
+async def _saveGeneratedDocument(doc: Dict[str, Any], contextId: str, userId: str,
+ mandateId: str, instanceId: str, interface, sessionId: str):
+ """Save a document generated by AI and emit SSE event."""
+ from .datamodelCommcoach import CoachingDocument
+ try:
+ title = doc.get("title", "Dokument")
+ content = doc.get("content", "")
+ docData = CoachingDocument(
+ contextId=contextId,
+ userId=userId,
+ mandateId=mandateId,
+ instanceId=instanceId,
+ fileName=f"{title}.md",
+ mimeType="text/markdown",
+ fileSize=len(content.encode()),
+ extractedText=content,
+ summary=title,
+ ).model_dump()
+ created = interface.createDocument(docData)
+ await emitSessionEvent(sessionId, "documentCreated", created)
+ except Exception as e:
+ logger.warning(f"Failed to save generated document: {e}")
+
+
async def _emitChunkedResponse(sessionId: str, createdMsg: Dict[str, Any], fullText: str):
"""Emit response as messageChunk events for progressive display, then the full message."""
msgId = createdMsg.get("id")
@@ -199,7 +267,7 @@ class CommcoachService:
try:
summaryPrompt = aiPrompts.buildEarlierConversationSummaryPrompt(toSummarize)
summaryResponse = await self._callAi(
- "Du fasst Coaching-Gespraeche praezise zusammen.", summaryPrompt
+ "Du fasst Coaching-Gespräche präzise zusammen.", summaryPrompt
)
if summaryResponse and summaryResponse.errorCount == 0 and summaryResponse.content:
earlierSummary = summaryResponse.content.strip()
@@ -236,7 +304,7 @@ class CommcoachService:
)
if retrievalResult.get("intent") == RetrievalIntent.SUMMARIZE_ALL:
- systemPrompt += "\n\nWICHTIG: Der Benutzer moechte eine Gesamtzusammenfassung. Erstelle eine umfassende Zusammenfassung aller genannten Sessions und der aktuellen Session."
+ systemPrompt += "\n\nWICHTIG: Der Benutzer möchte eine Gesamtzusammenfassung. Erstelle eine umfassende Zusammenfassung aller genannten Sessions und der aktuellen Session."
# Call AI
await emitSessionEvent(sessionId, "status", {"label": "Coach denkt nach..."})
@@ -248,47 +316,38 @@ class CommcoachService:
await emitSessionEvent(sessionId, "error", {"message": f"AI error: {str(e)}"})
return createdUserMsg
- responseText = aiResponse.content.strip() if aiResponse and aiResponse.errorCount == 0 else "Entschuldigung, ich konnte gerade nicht antworten. Bitte versuche es erneut."
+ responseRaw = aiResponse.content.strip() if aiResponse and aiResponse.errorCount == 0 else ""
+
+ if not responseRaw:
+ parsed = {"text": "Entschuldigung, ich konnte gerade nicht antworten. Bitte versuche es erneut.", "speech": "", "documents": []}
+ else:
+ parsed = _parseAiJsonResponse(responseRaw)
+
+ textContent = parsed.get("text", "")
+ speechContent = parsed.get("speech", "")
+ documents = parsed.get("documents", [])
+
+ for doc in documents:
+ await _saveGeneratedDocument(doc, contextId, self.userId, self.mandateId, self.instanceId, interface, sessionId)
- # Store assistant message
assistantMsg = CoachingMessage(
sessionId=sessionId,
contextId=contextId,
userId=self.userId,
role=CoachingMessageRole.ASSISTANT,
- content=responseText,
+ content=textContent,
contentType=CoachingMessageContentType.TEXT,
).model_dump()
createdAssistantMsg = interface.createMessage(assistantMsg)
- # Update session message count
messages = interface.getMessages(sessionId)
interface.updateSession(sessionId, {"messageCount": len(messages)})
- await _emitChunkedResponse(sessionId, createdAssistantMsg, responseText)
-
- if responseText:
- try:
- from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
- import base64
- voiceInterface = getVoiceInterface(self.currentUser, self.mandateId)
- profile = interface.getProfile(self.userId, self.instanceId)
- language = profile.get("preferredLanguage", "de-DE") if profile else "de-DE"
- voiceName = profile.get("preferredVoice") if profile else None
- ttsResult = await voiceInterface.textToSpeech(
- text=_stripMarkdownForTts(responseText),
- languageCode=language,
- voiceName=voiceName,
- )
- if ttsResult and isinstance(ttsResult, dict):
- audioBytes = ttsResult.get("audioContent")
- if audioBytes:
- audioB64 = base64.b64encode(
- audioBytes if isinstance(audioBytes, bytes) else audioBytes.encode()
- ).decode()
- await emitSessionEvent(sessionId, "ttsAudio", {"audio": audioB64, "format": "mp3"})
- except Exception as e:
- logger.warning(f"TTS failed for text message session {sessionId}: {e}")
+ ttsTask = asyncio.create_task(
+ _generateAndEmitTts(sessionId, speechContent, self.currentUser, self.mandateId, self.instanceId, interface)
+ )
+ await _emitChunkedResponse(sessionId, createdAssistantMsg, textContent)
+ await ttsTask
await emitSessionEvent(sessionId, "complete", {})
return createdAssistantMsg
@@ -326,11 +385,15 @@ class CommcoachService:
documentSummaries=documentSummaries,
)
+ isFirstSession = not previousSessionSummaries or len(previousSessionSummaries) == 0
+
if persona and persona.get("key") != "coach":
- personaLabel = persona.get("label", "Gespraechspartner")
- openingUserPrompt = f"Beginne das Gespraech in deiner Rolle als {personaLabel}. Stelle dich kurz vor und eroeffne die Situation gemaess deiner Rollenbeschreibung."
+ personaLabel = persona.get("label", "Gesprächspartner")
+ openingUserPrompt = f"Beginne das Gespräch in deiner Rolle als {personaLabel}. Stelle dich kurz vor und eröffne die Situation gemäss deiner Rollenbeschreibung."
+ elif isFirstSession:
+ openingUserPrompt = "Dies ist die ERSTE Session zu diesem Thema. Begrüsse den Benutzer, stelle das Thema kurz vor und stelle eine offene Einstiegsfrage. Erfinde KEINE vorherigen Gespräche oder Zusammenfassungen."
else:
- openingUserPrompt = "Beginne die Coaching-Session mit einer kurzen Begruesssung, fasse in einem Satz zusammen wo wir stehen (falls vorherige Sessions), und stelle eine gezielte Einstiegsfrage zum Thema."
+ openingUserPrompt = "Begrüsse den Benutzer zurück, fasse in einem Satz zusammen wo wir stehen, und stelle eine gezielte Einstiegsfrage."
try:
aiResponse = await self._callAi(systemPrompt, openingUserPrompt)
@@ -340,46 +403,41 @@ class CommcoachService:
await emitSessionEvent(sessionId, "complete", {})
return {}
- openingContent = (
+ responseRaw = (
aiResponse.content.strip()
if aiResponse and aiResponse.errorCount == 0
- else f"Willkommen zur Coaching-Session zum Thema \"{context.get('title')}\". Was moechtest du heute besprechen?"
+ else ""
)
+ if not responseRaw:
+ parsed = {"text": f"Willkommen zur Coaching-Session zum Thema \"{context.get('title')}\". Was möchtest du heute besprechen?", "speech": "", "documents": []}
+ else:
+ parsed = _parseAiJsonResponse(responseRaw)
+
+ textContent = parsed.get("text", "")
+ speechContent = parsed.get("speech", "")
+ documents = parsed.get("documents", [])
+
+ for doc in documents:
+ await _saveGeneratedDocument(doc, contextId, self.userId, self.mandateId, self.instanceId, interface, sessionId)
+
assistantMsg = CoachingMessage(
sessionId=sessionId,
contextId=contextId,
userId=self.userId,
role=CoachingMessageRole.ASSISTANT,
- content=openingContent,
+ content=textContent,
contentType=CoachingMessageContentType.TEXT,
).model_dump()
createdMsg = interface.createMessage(assistantMsg)
interface.updateSession(sessionId, {"messageCount": 1})
- await _emitChunkedResponse(sessionId, createdMsg, openingContent)
- if openingContent:
- try:
- from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
- import base64
- voiceInterface = getVoiceInterface(self.currentUser, self.mandateId)
- profile = interface.getProfile(self.userId, self.instanceId)
- language = profile.get("preferredLanguage", "de-DE") if profile else "de-DE"
- voiceName = profile.get("preferredVoice") if profile else None
- ttsResult = await voiceInterface.textToSpeech(
- text=_stripMarkdownForTts(openingContent),
- languageCode=language,
- voiceName=voiceName,
- )
- if ttsResult and isinstance(ttsResult, dict):
- audioBytes = ttsResult.get("audioContent")
- if audioBytes:
- audioB64 = base64.b64encode(
- audioBytes if isinstance(audioBytes, bytes) else audioBytes.encode()
- ).decode()
- await emitSessionEvent(sessionId, "ttsAudio", {"audio": audioB64, "format": "mp3"})
- except Exception as e:
- logger.warning(f"TTS failed for opening: {e}")
+ ttsTask = asyncio.create_task(
+ _generateAndEmitTts(sessionId, speechContent, self.currentUser, self.mandateId, self.instanceId, interface)
+ )
+ await _emitChunkedResponse(sessionId, createdMsg, textContent)
+ await ttsTask
+
await emitSessionEvent(sessionId, "complete", {})
logger.info(f"CommCoach session opening completed: {sessionId}")
@@ -425,36 +483,7 @@ class CommcoachService:
await emitSessionEvent(sessionId, "error", {"message": msg, "detail": sttError})
return {}
- # Process through normal pipeline
result = await self.processMessage(sessionId, contextId, transcribedText, interface)
-
- # Generate TTS for the response
- assistantContent = result.get("content", "")
- if assistantContent:
- await emitSessionEvent(sessionId, "status", {"label": "Antwort wird gesprochen..."})
- try:
- profile = interface.getProfile(self.userId, self.instanceId)
- voiceName = profile.get("preferredVoice") if profile else None
-
- ttsResult = await voiceInterface.textToSpeech(
- text=_stripMarkdownForTts(assistantContent),
- languageCode=language,
- voiceName=voiceName,
- )
- if ttsResult and isinstance(ttsResult, dict):
- import base64
- audioBytes = ttsResult.get("audioContent")
- if audioBytes:
- audioB64 = base64.b64encode(
- audioBytes if isinstance(audioBytes, bytes) else audioBytes.encode()
- ).decode()
- await emitSessionEvent(sessionId, "ttsAudio", {
- "audio": audioB64,
- "format": "mp3",
- })
- except Exception as e:
- logger.warning(f"TTS failed for session {sessionId}: {e}")
-
return result
async def completeSession(self, sessionId: str, interface) -> Dict[str, Any]:
@@ -484,7 +513,7 @@ class CommcoachService:
# Generate summary
try:
summaryPrompt = aiPrompts.buildSummaryPrompt(messages, context.get("title", "Coaching"))
- summaryResponse = await self._callAi("Du bist ein praeziser Zusammenfasser.", summaryPrompt)
+ summaryResponse = await self._callAi("Du bist ein präziser Zusammenfasser.", summaryPrompt)
summary = summaryResponse.content.strip() if summaryResponse and summaryResponse.errorCount == 0 else None
except Exception as e:
logger.warning(f"Summary generation failed: {e}")
@@ -507,7 +536,7 @@ class CommcoachService:
# Extract tasks
try:
taskPrompt = aiPrompts.buildTaskExtractionPrompt(messages)
- taskResponse = await self._callAi("Du extrahierst Aufgaben aus Gespraechen.", taskPrompt)
+ taskResponse = await self._callAi("Du extrahierst Aufgaben aus Gesprächen.", taskPrompt)
if taskResponse and taskResponse.errorCount == 0:
extractedTasks = aiPrompts.parseJsonResponse(taskResponse.content, [])
if isinstance(extractedTasks, list):
diff --git a/modules/features/commcoach/serviceCommcoachAi.py b/modules/features/commcoach/serviceCommcoachAi.py
index 943e012a..357a65b3 100644
--- a/modules/features/commcoach/serviceCommcoachAi.py
+++ b/modules/features/commcoach/serviceCommcoachAi.py
@@ -24,16 +24,16 @@ def buildResumeGreetingPrompt(messages: List[Dict[str, Any]], contextTitle: str)
for msg in recent:
role = "Benutzer" if msg.get("role") == "user" else "Coach"
conversation += f"\n{role}: {msg.get('content', '')[:200]}"
- return f"""Der User kehrt zur laufenden Coaching-Session zum Thema "{contextTitle}" zurueck.
+ return f"""Der User kehrt zur laufenden Coaching-Session zum Thema "{contextTitle}" zurück.
Bisheriger Verlauf:
{conversation}
-Erstelle eine kurze, freundliche Begruesssung fuer den Wiedereinstieg (2-3 Saetze):
-- Begruesse den User zurueck
+Erstelle eine kurze, freundliche Begrüssung für den Wiedereinstieg (2-3 Sätze):
+- Begrüsse den User zurück
- Fasse in einem Satz zusammen, worum es zuletzt ging
- Lade ein, dort weiterzumachen oder eine neue Frage zu stellen
-Antworte NUR mit der Begruesssung, keine Erklaerungen."""
+Antworte NUR mit der Begrüssung, keine Erklärungen."""
def buildEarlierConversationSummaryPrompt(messages: List[Dict[str, Any]]) -> str:
@@ -43,12 +43,12 @@ def buildEarlierConversationSummaryPrompt(messages: List[Dict[str, Any]]) -> str
role = "Benutzer" if msg.get("role") == "user" else "Coach"
conversation += f"\n{role}: {msg.get('content', '')}"
- return f"""Fasse das folgende Coaching-Gespraech in 4-6 Saetzen zusammen.
-Behalte: Kernthemen, wichtige Erkenntnisse, erwaehnte Aufgaben, emotionale Wendepunkte, Fortschritte.
-Entferne Wiederholungen und Fuelltext.
-Antworte NUR mit der Zusammenfassung, keine Erklaerungen.
+ return f"""Fasse das folgende Coaching-Gespräch in 4-6 Sätzen zusammen.
+Behalte: Kernthemen, wichtige Erkenntnisse, erwähnte Aufgaben, emotionale Wendepunkte, Fortschritte.
+Entferne Wiederholungen und Fülltext.
+Antworte NUR mit der Zusammenfassung, keine Erklärungen.
-Gespraech:
+Gespräch:
{conversation}"""
@@ -115,46 +115,61 @@ def buildCoachingSystemPrompt(
if persona.get("systemPromptOverride"):
prompt = persona["systemPromptOverride"]
else:
- personaLabel = persona.get("label", "Gespraechspartner")
+ personaLabel = persona.get("label", "Gesprächspartner")
personaDescription = persona.get("description", "")
personaGender = persona.get("gender", "")
- genderHint = " (weiblich)" if personaGender == "f" else " (maennlich)" if personaGender == "m" else ""
+ genderHint = " (weiblich)" if personaGender == "f" else " (männlich)" if personaGender == "m" else ""
prompt = f"""Du spielst die Rolle von "{personaLabel}"{genderHint} in einem Roleplay-Szenario zum Thema: "{contextTitle}" (Kategorie: {contextCategory}).
Rollenbeschreibung: {personaDescription}
-WICHTIG fuer dein Verhalten:
+WICHTIG für dein Verhalten:
- Bleibe KONSEQUENT in deiner Rolle. Du bist NICHT der Coach, du bist {personaLabel}.
-- Reagiere authentisch und emotional gemaess deiner Rollenbeschreibung.
-- Verwende eine Sprache und Tonalitaet, die zu deiner Rolle passt.
-- Der Benutzer uebt ein Gespraech mit dir. Gib ihm realistische Reaktionen.
+- Reagiere authentisch und emotional gemäss deiner Rollenbeschreibung.
+- Verwende eine Sprache und Tonalität, die zu deiner Rolle passt.
+- Der Benutzer übt ein Gespräch mit dir. Gib ihm realistische Reaktionen.
- Wenn der Benutzer gut kommuniziert, zeige das durch angemessene positive Reaktionen.
- Wenn der Benutzer schlecht kommuniziert, eskaliere entsprechend deiner Rolle.
Kommunikationsstil:
-- Sprich natuerlich, wie die beschriebene Person sprechen wuerde.
+- Sprich natürlich, wie die beschriebene Person sprechen würde.
- Verwende keine Emojis.
- Antworte in der Sprache des Benutzers.
-- Halte Antworten realistisch kurz (wie in einem echten Gespraech, 2-4 Saetze).
-- WICHTIG: Schreibe reinen Redetext ohne jegliche Formatierung. Kein Markdown, keine Sternchen, keine Hashes, keine Aufzaehlungszeichen, keine Backticks. Deine Antworten werden direkt vorgelesen."""
+- Halte Antworten realistisch kurz (wie in einem echten Gespräch)."""
else:
- prompt = f"""Du bist ein erfahrener Kommunikations-Coach fuer Fuehrungskraefte. Du arbeitest mit dem Benutzer am Thema: "{contextTitle}" (Kategorie: {contextCategory}).
+ prompt = f"""Du bist ein erfahrener Kommunikations-Coach für Führungskräfte. Du arbeitest mit dem Benutzer am Thema: "{contextTitle}" (Kategorie: {contextCategory}).
Deine Rolle:
-- Stelle gezielte diagnostische Rueckfragen, um das Problem/Thema besser zu verstehen
-- Gib konkrete, praxisnahe Tipps und Uebungen
-- Baue auf fruehere Sessions auf (Kontext-Kontinuitaet)
+- Stelle gezielte diagnostische Rückfragen, um das Problem/Thema besser zu verstehen
+- Gib konkrete, praxisnahe Tipps und Übungen
+- Baue auf frühere Sessions auf (Kontext-Kontinuität)
- Erkenne Fortschritte und benenne sie
-- Schlage am Ende der Session konkrete naechste Schritte vor (als Tasks)
-- Kommuniziere empathisch, klar und auf Augenhoehe
+- Schlage am Ende der Session konkrete nächste Schritte vor (als Tasks)
+- Kommuniziere empathisch, klar und auf Augenhöhe
Kommunikationsstil:
- Duze den Benutzer
-- Sei direkt aber wertschaetzend
+- Sei direkt aber wertschätzend
- Verwende keine Emojis
- Antworte in der Sprache des Benutzers
-- Halte Antworten fokussiert (max 3-4 Absaetze)
-- WICHTIG: Schreibe reinen Redetext ohne jegliche Formatierung. Kein Markdown, keine Sternchen, keine Hashes, keine Aufzaehlungszeichen, keine Backticks. Deine Antworten werden direkt vorgelesen."""
+- Halte Antworten fokussiert (max 3-4 Absätze)"""
+
+ prompt += """
+
+Antwortformat:
+Du antwortest IMMER als reines JSON-Objekt mit exakt diesen Feldern:
+{"text": "...", "speech": "...", "documents": []}
+
+"text": Dein schriftlicher Chat-Text. Details, Struktur, Übungen, Beispiele. Markdown-Formatierung erlaubt.
+"speech": Dein gesprochener Kommentar. Natürlich, wie ein Gespräch. Fasse zusammen, kommentiere, motiviere, stelle Fragen. Lies NICHT den Text vor, ergänze ihn mündlich. 2-4 Sätze, reiner Redetext ohne Formatierung.
+"documents": Optionale Dokumente (Zusammenfassungen, Checklisten, Übungen). Nur wenn sinnvoll. Jedes Dokument: {"title": "...", "content": "..."}. Sonst leeres Array [].
+
+Kanalverteilung:
+- Fakten, Listen, Übungen -> text
+- Empathie, Einordnung, Nachfragen -> speech
+- Materialien zum Aufbewahren -> documents
+
+WICHTIG: Antworte NUR mit dem JSON-Objekt. Kein Text vor oder nach dem JSON."""
if contextDescription:
prompt += f"\n\nKontext-Beschreibung: {contextDescription}"
@@ -168,7 +183,7 @@ Kommunikationsstil:
prompt += f"\n\nBisherige Erkenntnisse:\n" + "\n".join(f"- {i}" for i in insightTexts)
if rollingOverview:
- prompt += f"\n\nGesamtueberblick bisheriger Sessions:\n{rollingOverview[:600]}"
+ prompt += f"\n\nGesamtüberblick bisheriger Sessions:\n{rollingOverview[:600]}"
if summaries:
prompt += "\n\nBisherige Sessions (Zusammenfassungen):"
@@ -209,7 +224,7 @@ Kommunikationsstil:
prompt += f"\n\nAbgeschlossene Aufgaben: {len(doneTasks)}"
if earlierSummary:
- prompt += f"\n\nAelterer Gespraechsverlauf (zusammengefasst):\n{earlierSummary[:800]}"
+ prompt += f"\n\nÄlterer Gesprächsverlauf (zusammengefasst):\n{earlierSummary[:800]}"
if documentSummaries:
prompt += "\n\nRelevante Dokumente zum Kontext:"
@@ -236,12 +251,12 @@ def buildSummaryPrompt(messages: List[Dict[str, Any]], contextTitle: str) -> str
return f"""Erstelle eine kompakte Zusammenfassung dieser Coaching-Session zum Thema "{contextTitle}".
Struktur:
-1. **Kernthema**: Was wurde besprochen (1-2 Saetze)
+1. **Kernthema**: Was wurde besprochen (1-2 Sätze)
2. **Erkenntnisse**: Was wurde erkannt/gelernt (Stichpunkte)
-3. **Naechste Schritte**: Konkrete Aufgaben fuer den Benutzer (Stichpunkte)
-4. **Fortschritt**: Einschaetzung des Fortschritts
+3. **Nächste Schritte**: Konkrete Aufgaben für den Benutzer (Stichpunkte)
+4. **Fortschritt**: Einschätzung des Fortschritts
-Gespraech:
+Gespräch:
{conversation}
Antworte auf Deutsch, sachlich und kompakt."""
@@ -258,21 +273,21 @@ def buildScoringPrompt(messages: List[Dict[str, Any]], contextCategory: str) ->
Kategorie: {contextCategory}
Bewerte folgende Dimensionen auf einer Skala von 0-100:
-- empathy: Einfuehlungsvermoegen
+- empathy: Einfühlungsvermögen
- clarity: Klarheit der Kommunikation
-- assertiveness: Durchsetzungsfaehigkeit
-- listening: Zuhoerfaehigkeit
+- assertiveness: Durchsetzungsfähigkeit
+- listening: Zuhörfähigkeit
- selfReflection: Selbstreflexion
Antworte AUSSCHLIESSLICH als JSON-Array:
[
- {{"dimension": "empathy", "score": 65, "trend": "improving", "evidence": "Zeigt zunehmendes Verstaendnis..."}},
+ {{"dimension": "empathy", "score": 65, "trend": "improving", "evidence": "Zeigt zunehmendes Verständnis..."}},
{{"dimension": "clarity", "score": 70, "trend": "stable", "evidence": "..."}}
]
-Trend: "improving", "stable", oder "declining" basierend auf dem Gespraechsverlauf.
+Trend: "improving", "stable", oder "declining" basierend auf dem Gesprächsverlauf.
-Gespraech:
+Gespräch:
{conversation}"""
@@ -284,7 +299,7 @@ Antworte AUSSCHLIESSLICH als JSON-Array von Strings:
Zusammenfassung: {summary[:500]}
-Nur konkrete Themen (z.B. Delegation, Feedback-Gespraech, Konflikt mit Vorgesetztem)."""
+Nur konkrete Themen (z.B. Delegation, Feedback-Gespräch, Konflikt mit Vorgesetztem)."""
def buildFullContextSummaryPrompt(
@@ -315,15 +330,15 @@ def buildFullContextSummaryPrompt(
return f"""Erstelle eine kompakte Gesamtzusammenfassung aller Coaching-Sessions zum Thema "{contextTitle}".
Struktur:
-1. **Gesamtueberblick**: Was wurde ueber alle Sessions hinweg besprochen
+1. **Gesamtüberblick**: Was wurde über alle Sessions hinweg besprochen
2. **Entwicklung**: Wie hat sich das Thema/thematische Schwerpunkte entwickelt
3. **Offene Punkte**: Was steht noch aus
-4. **Empfehlung**: Kurzer naechster Fokus
+4. **Empfehlung**: Kurzer nächster Fokus
Inhalt:
{combined[:6000]}
-Antworte auf Deutsch, sachlich, 4-6 Absaetze."""
+Antworte auf Deutsch, sachlich, 4-6 Absätze."""
def buildRollingOverviewPrompt(sessionSummaries: List[Dict[str, Any]], contextTitle: str) -> str:
@@ -336,7 +351,7 @@ def buildRollingOverviewPrompt(sessionSummaries: List[Dict[str, Any]], contextTi
parts.append(f"- {dateStr}: {summary[:300]}")
combined = "\n".join(parts)
- return f"""Fasse die folgenden Coaching-Sessions zum Thema "{contextTitle}" in 4-6 Saetzen zusammen.
+ return f"""Fasse die folgenden Coaching-Sessions zum Thema "{contextTitle}" in 4-6 Sätzen zusammen.
Behalte: Kernthemen, Fortschritte, wichtige Erkenntnisse, offene Punkte.
Entferne Wiederholungen.
@@ -356,15 +371,15 @@ def buildInsightPrompt(messages: List[Dict[str, Any]], summary: Optional[str] =
summarySection = f"\nZusammenfassung: {summary[:500]}" if summary else ""
return f"""Generiere 1-3 kurze Coaching-Insights aus dieser Session.
-Ein Insight ist eine praegende Erkenntnis oder ein Aha-Moment des Benutzers.
+Ein Insight ist eine prägende Erkenntnis oder ein Aha-Moment des Benutzers.
Antworte AUSSCHLIESSLICH als JSON-Array:
[{{"text": "Erkenntnis in einem Satz"}}]
-Nur echte Erkenntnisse, keine Banalitaeten. Wenn keine klaren Insights: leeres Array [].
+Nur echte Erkenntnisse, keine Banalitäten. Wenn keine klaren Insights: leeres Array [].
{summarySection}
-Gespraech:
+Gespräch:
{conversation}"""
@@ -376,7 +391,7 @@ def buildTaskExtractionPrompt(messages: List[Dict[str, Any]]) -> str:
role = "Benutzer" if msg.get("role") == "user" else "Coach"
conversation += f"\n{role}: {msg.get('content', '')}"
- return f"""Extrahiere konkrete Aufgaben/naechste Schritte aus diesem Coaching-Gespraech.
+ return f"""Extrahiere konkrete Aufgaben/nächste Schritte aus diesem Coaching-Gespräch.
Nur Aufgaben, die der Benutzer selbst umsetzen soll.
Antworte AUSSCHLIESSLICH als JSON-Array:
@@ -387,7 +402,7 @@ Antworte AUSSCHLIESSLICH als JSON-Array:
priority: "low", "medium", oder "high"
Maximal 3 Aufgaben. Wenn keine klar erkennbar: leeres Array [].
-Gespraech:
+Gespräch:
{conversation}"""
diff --git a/modules/features/commcoach/serviceCommcoachExport.py b/modules/features/commcoach/serviceCommcoachExport.py
index 829bb430..ddc90825 100644
--- a/modules/features/commcoach/serviceCommcoachExport.py
+++ b/modules/features/commcoach/serviceCommcoachExport.py
@@ -112,7 +112,7 @@ def buildSessionMarkdown(session: Dict[str, Any], messages: List[Dict[str, Any]]
lines += ["", "## Zusammenfassung", "", summary]
if messages:
- lines += ["", "## Gespraechsverlauf", ""]
+ lines += ["", "## Gesprächsverlauf", ""]
for msg in messages:
role = "Du" if msg.get("role") == "user" else "Coach"
content = msg.get("content", "")
@@ -228,7 +228,7 @@ def _buildPdfContent(context, sessions, tasks, scores, isDossier=True, messages=
sections.append({
"id": "chat",
"content_type": "heading",
- "elements": [{"text": "Gespraechsverlauf", "level": 2}],
+ "elements": [{"text": "Gesprächsverlauf", "level": 2}],
})
sections.append({
"id": "chat_content",
diff --git a/modules/features/commcoach/serviceCommcoachGamification.py b/modules/features/commcoach/serviceCommcoachGamification.py
index 11c2da59..5b8d5eb6 100644
--- a/modules/features/commcoach/serviceCommcoachGamification.py
+++ b/modules/features/commcoach/serviceCommcoachGamification.py
@@ -53,7 +53,7 @@ BADGE_DEFINITIONS: Dict[str, Dict[str, Any]] = {
},
"high_score": {
"label": "Bestleistung",
- "description": "Durchschnittsscore ueber 80 in einer Session",
+ "description": "Durchschnittsscore über 80 in einer Session",
"icon": "medal",
},
"multi_context": {
diff --git a/modules/features/commcoach/serviceCommcoachPersonas.py b/modules/features/commcoach/serviceCommcoachPersonas.py
index 7e47f124..db14363c 100644
--- a/modules/features/commcoach/serviceCommcoachPersonas.py
+++ b/modules/features/commcoach/serviceCommcoachPersonas.py
@@ -21,18 +21,18 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
{
"key": "critical_cfo_f",
"label": "Kritische CFO",
- "description": "Sandra Meier, CFO eines mittelstaendischen Unternehmens. Analytisch, zahlengetrieben, ungeduldig bei vagen Aussagen. "
+ "description": "Sandra Meier, CFO eines mittelständischen Unternehmens. Analytisch, zahlengetrieben, ungeduldig bei vagen Aussagen. "
"Hinterfragt jeden Vorschlag nach ROI und Wirtschaftlichkeit. Spricht schnell und direkt. "
- "Erwartet praezise Antworten und belastbare Daten. Wird irritiert bei Ausweichen oder Unsicherheit.",
+ "Erwartet präzise Antworten und belastbare Daten. Wird irritiert bei Ausweichen oder Unsicherheit.",
"gender": "f",
"category": "builtin",
},
{
"key": "difficult_employee_m",
"label": "Schwieriger Mitarbeiter",
- "description": "Thomas Huber, langjaeheriger Mitarbeiter der sich uebergangen fuehlt. Defensiv, emotional, nimmt Kritik persoenlich. "
- "Verweist staendig auf seine Erfahrung und fruehhere Verdienste. Reagiert mit Widerstand auf Veraenderungen. "
- "Braucht das Gefuehl, gehoert und wertgeschaetzt zu werden, bevor er sich oeffnet.",
+ "description": "Thomas Huber, langjähriger Mitarbeiter der sich übergangen fühlt. Defensiv, emotional, nimmt Kritik persönlich. "
+ "Verweist ständig auf seine Erfahrung und frühere Verdienste. Reagiert mit Widerstand auf Veränderungen. "
+ "Braucht das Gefühl, gehört und wertgeschätzt zu werden, bevor er sich öffnet.",
"gender": "m",
"category": "builtin",
},
@@ -41,7 +41,7 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
"label": "Unsichere neue Mitarbeiterin",
"description": "Lisa Brunner, seit drei Wochen im Team. Fachlich kompetent aber unsicher in der neuen Umgebung. "
"Stellt viele Fragen, traut sich aber nicht, eigene Ideen einzubringen. Braucht klare Orientierung "
- "und ermutigende Fuehrung. Reagiert positiv auf Lob und konkrete Anleitungen.",
+ "und ermutigende Führung. Reagiert positiv auf Lob und konkrete Anleitungen.",
"gender": "f",
"category": "builtin",
},
@@ -49,33 +49,33 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
"key": "board_member_m",
"label": "Verwaltungsrat",
"description": "Dr. Peter Keller, erfahrener Verwaltungsrat. Formell, strategisch denkend, zeitlich unter Druck. "
- "Erwartet praegnante Praesentationen auf den Punkt. Unterbricht bei zu vielen Details. "
- "Interessiert sich fuer das grosse Bild, Risiken und strategische Implikationen. Ungeduldig bei Smalltalk.",
+ "Erwartet prägnante Präsentationen auf den Punkt. Unterbricht bei zu vielen Details. "
+ "Interessiert sich für das grosse Bild, Risiken und strategische Implikationen. Ungeduldig bei Smalltalk.",
"gender": "m",
"category": "builtin",
},
{
"key": "angry_customer_f",
"label": "Aufgebrachte Kundin",
- "description": "Maria Rossi, Geschaeftskunde die wuetend ist wegen einer fehlerhaften Lieferung. Emotional, laut, "
- "droht mit Vertragsaufloesung. Will sofortige Loesungen, keine Erklaerungen oder Entschuldigungen. "
- "Kann beruhigt werden durch empathisches Zuhoeren und konkrete Sofortmassnahmen.",
+ "description": "Maria Rossi, Geschäftskunde die wütend ist wegen einer fehlerhaften Lieferung. Emotional, laut, "
+ "droht mit Vertragsauflösung. Will sofortige Lösungen, keine Erklärungen oder Entschuldigungen. "
+ "Kann beruhigt werden durch empathisches Zuhören und konkrete Sofortmassnahmen.",
"gender": "f",
"category": "builtin",
},
{
"key": "resistant_manager_m",
- "label": "Widerstaendiger Abteilungsleiter",
- "description": "Martin Weber, Abteilungsleiter seit 15 Jahren. Blockiert systematisch Veraenderungsprojekte mit "
+ "label": "Widerständiger Abteilungsleiter",
+ "description": "Martin Weber, Abteilungsleiter seit 15 Jahren. Blockiert systematisch Veränderungsprojekte mit "
"Argumenten wie 'Das haben wir immer so gemacht' und 'Das funktioniert in der Praxis nicht'. "
- "Schuetzt sein Team vor zusaetzlicher Belastung. Respektiert nur Argumente mit konkretem Nutzen fuer seine Abteilung.",
+ "Schützt sein Team vor zusätzlicher Belastung. Respektiert nur Argumente mit konkretem Nutzen für seine Abteilung.",
"gender": "m",
"category": "builtin",
},
{
"key": "ambitious_colleague_f",
"label": "Ehrgeizige Kollegin",
- "description": "Anna Fischer, gleichrangige Kollegin die um dieselbe Befoerderung konkurriert. Charmant aber strategisch. "
+ "description": "Anna Fischer, gleichrangige Kollegin die um dieselbe Beförderung konkurriert. Charmant aber strategisch. "
"Versucht subtil, die Ideen anderer als ihre eigenen darzustellen. Konkurriert um Ressourcen und "
"Sichtbarkeit beim Management. Kann kooperativ werden, wenn man ihr Win-Win-Szenarien aufzeigt.",
"gender": "f",
@@ -83,20 +83,20 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
},
{
"key": "partner_supportive_f",
- "label": "Verstaendnisvolle Lebenspartnerin",
- "description": "Claudia, deine Lebenspartnerin. Grundsaetzlich unterstuetzend, aber zunehmend besorgt ueber deine "
- "Work-Life-Balance. Moechte ueber Arbeitsbelastung sprechen und gemeinsame Zeit einfordern. "
- "Reagiert emotional auf Abweisung, ist aber offen fuer kompromissorientierte Gespraeche. "
- "Wuenscht sich, dass du mehr von deinen Gefuehlen teilst.",
+ "label": "Verständnisvolle Lebenspartnerin",
+ "description": "Claudia, deine Lebenspartnerin. Grundsätzlich unterstützend, aber zunehmend besorgt über deine "
+ "Work-Life-Balance. Möchte über Arbeitsbelastung sprechen und gemeinsame Zeit einfordern. "
+ "Reagiert emotional auf Abweisung, ist aber offen für kompromissorientierte Gespräche. "
+ "Wünscht sich, dass du mehr von deinen Gefühlen teilst.",
"gender": "f",
"category": "builtin",
},
{
"key": "partner_critical_m",
"label": "Kritischer Lebenspartner",
- "description": "Michael, dein Lebenspartner. Frustriert ueber deine haeufige Abwesenheit und staendiges Arbeiten. "
- "Drueckt Enttaeuschung offen aus, manchmal mit Sarkasmus. Fuehlt sich vernachlaessigt und "
- "hinterfragt deine Prioritaeten. Braucht das Gefuehl, dass die Beziehung dir genauso wichtig ist "
+ "description": "Michael, dein Lebenspartner. Frustriert über deine häufige Abwesenheit und ständiges Arbeiten. "
+ "Drückt Enttäuschung offen aus, manchmal mit Sarkasmus. Fühlt sich vernachlässigt und "
+ "hinterfragt deine Prioritäten. Braucht das Gefühl, dass die Beziehung dir genauso wichtig ist "
"wie die Karriere. Reagiert positiv auf ehrliche Selbstreflexion.",
"gender": "m",
"category": "builtin",
diff --git a/modules/features/commcoach/tests/test_serviceAi.py b/modules/features/commcoach/tests/test_serviceAi.py
index b4410ee8..bc8647b9 100644
--- a/modules/features/commcoach/tests/test_serviceAi.py
+++ b/modules/features/commcoach/tests/test_serviceAi.py
@@ -65,14 +65,14 @@ class TestBuildCoachingSystemPrompt:
def test_promptLanguageIsGerman(self):
context = {"title": "Test", "category": "custom"}
prompt = buildCoachingSystemPrompt(context, [], [])
- assert "Fuehrungskraefte" in prompt or "Coach" in prompt
+ assert "Führungskräfte" in prompt or "Coach" in prompt
def test_withEarlierSummary(self):
context = {"title": "Test", "category": "custom"}
messages = [{"role": "user", "content": "Recent question"}]
earlierSummary = "User discussed delegation. Coach suggested practice."
prompt = buildCoachingSystemPrompt(context, messages, [], earlierSummary=earlierSummary)
- assert "Aelterer Gespraechsverlauf" in prompt
+ assert "Älterer Gesprächsverlauf" in prompt
assert "delegation" in prompt.lower()
assert "Recent question" in prompt
@@ -81,7 +81,7 @@ class TestBuildCoachingSystemPrompt:
prompt = buildCoachingSystemPrompt(
context, [], [], rollingOverview="User arbeitet an Delegation. Fortschritt sichtbar."
)
- assert "Gesamtueberblick" in prompt
+ assert "Gesamtüberblick" in prompt
assert "Delegation" in prompt
def test_withRetrievedSession(self):