Alle 9 Fixes sind implementiert. Hier die Zusammenfassung:

Fix 1 -- Opening-Prompt: processSessionOpening in serviceCommcoach.py prüft jetzt ob es die erste Session ist (isFirstSession) und gibt der AI einen expliziten Prompt, der das Erfinden von Kontext verbietet.
Fix 2 -- Stabiler Transcript: onresult in CommcoachCoachingView.tsx nutzt jetzt processedResultIndexRef um nur neue Results zu verarbeiten. Finalisierte Teile werden stabil akkumuliert, kein Flackern mehr.
Fix 3 -- Hintergrundgeräusche-Timeout: Neuer silenceTimerRef mit 5s Timeout. Wenn nach onspeechstart kein Text kommt, wird isUserSpeaking automatisch zurückgesetzt.
Fix 4 -- Stop-Button: "Stop" Button erscheint im Session-Header wenn TTS läuft (via isTtsPlaying State, synchronisiert per 200ms Interval mit isTtsPlayingRef).
Fix 5 -- Weitersprechen-Button: lastTtsAudioRef speichert das zuletzt gespielte Audio. stopTts setzt wasInterrupted = true. "Weitersprechen" Button erscheint nach Unterbrechung und spielt das Audio erneut ab.
Fix 6 -- Paralleles TTS: Neue _generateAndEmitTts() Hilfsfunktion. In processMessage und processSessionOpening wird TTS als asyncio.create_task parallel zu _emitChunkedResponse gestartet.
Fix 7 -- JSON-Response: Die AI antwortet jetzt als JSON mit text, speech, documents. Neuer Prompt-Block wird in buildCoachingSystemPrompt angehängt. _parseAiJsonResponse() und _saveGeneratedDocument() im Backend. processMessage und processSessionOpening nutzen die neue Struktur.
Fix 8 -- Loading-States: Neuer actionLoading State in useCommcoach. Alle async Funktionen setzen setActionLoading('key') vor dem Await und null im finally. Buttons zeigen Loading-Text und werden disabled.
Fix 9 -- Umlaute: Alle deutschen Strings in allen CommCoach-Dateien (Backend + Frontend) korrigiert: ae->ä, oe->ö, ue->ü.
This commit is contained in:
patrick-motsch 2026-03-04 22:53:41 +01:00
parent 92d9a2a0d5
commit 12b0d3d36e
6 changed files with 212 additions and 168 deletions

View file

@ -87,6 +87,74 @@ CHUNK_WORD_SIZE = 4
CHUNK_DELAY_SECONDS = 0.05 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): async def _emitChunkedResponse(sessionId: str, createdMsg: Dict[str, Any], fullText: str):
"""Emit response as messageChunk events for progressive display, then the full message.""" """Emit response as messageChunk events for progressive display, then the full message."""
msgId = createdMsg.get("id") msgId = createdMsg.get("id")
@ -199,7 +267,7 @@ class CommcoachService:
try: try:
summaryPrompt = aiPrompts.buildEarlierConversationSummaryPrompt(toSummarize) summaryPrompt = aiPrompts.buildEarlierConversationSummaryPrompt(toSummarize)
summaryResponse = await self._callAi( 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: if summaryResponse and summaryResponse.errorCount == 0 and summaryResponse.content:
earlierSummary = summaryResponse.content.strip() earlierSummary = summaryResponse.content.strip()
@ -236,7 +304,7 @@ class CommcoachService:
) )
if retrievalResult.get("intent") == RetrievalIntent.SUMMARIZE_ALL: 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 # Call AI
await emitSessionEvent(sessionId, "status", {"label": "Coach denkt nach..."}) await emitSessionEvent(sessionId, "status", {"label": "Coach denkt nach..."})
@ -248,47 +316,38 @@ class CommcoachService:
await emitSessionEvent(sessionId, "error", {"message": f"AI error: {str(e)}"}) await emitSessionEvent(sessionId, "error", {"message": f"AI error: {str(e)}"})
return createdUserMsg 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( assistantMsg = CoachingMessage(
sessionId=sessionId, sessionId=sessionId,
contextId=contextId, contextId=contextId,
userId=self.userId, userId=self.userId,
role=CoachingMessageRole.ASSISTANT, role=CoachingMessageRole.ASSISTANT,
content=responseText, content=textContent,
contentType=CoachingMessageContentType.TEXT, contentType=CoachingMessageContentType.TEXT,
).model_dump() ).model_dump()
createdAssistantMsg = interface.createMessage(assistantMsg) createdAssistantMsg = interface.createMessage(assistantMsg)
# Update session message count
messages = interface.getMessages(sessionId) messages = interface.getMessages(sessionId)
interface.updateSession(sessionId, {"messageCount": len(messages)}) interface.updateSession(sessionId, {"messageCount": len(messages)})
await _emitChunkedResponse(sessionId, createdAssistantMsg, responseText) ttsTask = asyncio.create_task(
_generateAndEmitTts(sessionId, speechContent, self.currentUser, self.mandateId, self.instanceId, interface)
if responseText: )
try: await _emitChunkedResponse(sessionId, createdAssistantMsg, textContent)
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface await ttsTask
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}")
await emitSessionEvent(sessionId, "complete", {}) await emitSessionEvent(sessionId, "complete", {})
return createdAssistantMsg return createdAssistantMsg
@ -326,11 +385,15 @@ class CommcoachService:
documentSummaries=documentSummaries, documentSummaries=documentSummaries,
) )
isFirstSession = not previousSessionSummaries or len(previousSessionSummaries) == 0
if persona and persona.get("key") != "coach": if persona and persona.get("key") != "coach":
personaLabel = persona.get("label", "Gespraechspartner") personaLabel = persona.get("label", "Gesprächspartner")
openingUserPrompt = f"Beginne das Gespraech in deiner Rolle als {personaLabel}. Stelle dich kurz vor und eroeffne die Situation gemaess deiner Rollenbeschreibung." 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: 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: try:
aiResponse = await self._callAi(systemPrompt, openingUserPrompt) aiResponse = await self._callAi(systemPrompt, openingUserPrompt)
@ -340,46 +403,41 @@ class CommcoachService:
await emitSessionEvent(sessionId, "complete", {}) await emitSessionEvent(sessionId, "complete", {})
return {} return {}
openingContent = ( responseRaw = (
aiResponse.content.strip() aiResponse.content.strip()
if aiResponse and aiResponse.errorCount == 0 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( assistantMsg = CoachingMessage(
sessionId=sessionId, sessionId=sessionId,
contextId=contextId, contextId=contextId,
userId=self.userId, userId=self.userId,
role=CoachingMessageRole.ASSISTANT, role=CoachingMessageRole.ASSISTANT,
content=openingContent, content=textContent,
contentType=CoachingMessageContentType.TEXT, contentType=CoachingMessageContentType.TEXT,
).model_dump() ).model_dump()
createdMsg = interface.createMessage(assistantMsg) createdMsg = interface.createMessage(assistantMsg)
interface.updateSession(sessionId, {"messageCount": 1}) interface.updateSession(sessionId, {"messageCount": 1})
await _emitChunkedResponse(sessionId, createdMsg, openingContent) ttsTask = asyncio.create_task(
if openingContent: _generateAndEmitTts(sessionId, speechContent, self.currentUser, self.mandateId, self.instanceId, interface)
try: )
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface await _emitChunkedResponse(sessionId, createdMsg, textContent)
import base64 await ttsTask
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}")
await emitSessionEvent(sessionId, "complete", {}) await emitSessionEvent(sessionId, "complete", {})
logger.info(f"CommCoach session opening completed: {sessionId}") logger.info(f"CommCoach session opening completed: {sessionId}")
@ -425,36 +483,7 @@ class CommcoachService:
await emitSessionEvent(sessionId, "error", {"message": msg, "detail": sttError}) await emitSessionEvent(sessionId, "error", {"message": msg, "detail": sttError})
return {} return {}
# Process through normal pipeline
result = await self.processMessage(sessionId, contextId, transcribedText, interface) 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 return result
async def completeSession(self, sessionId: str, interface) -> Dict[str, Any]: async def completeSession(self, sessionId: str, interface) -> Dict[str, Any]:
@ -484,7 +513,7 @@ class CommcoachService:
# Generate summary # Generate summary
try: try:
summaryPrompt = aiPrompts.buildSummaryPrompt(messages, context.get("title", "Coaching")) 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 summary = summaryResponse.content.strip() if summaryResponse and summaryResponse.errorCount == 0 else None
except Exception as e: except Exception as e:
logger.warning(f"Summary generation failed: {e}") logger.warning(f"Summary generation failed: {e}")
@ -507,7 +536,7 @@ class CommcoachService:
# Extract tasks # Extract tasks
try: try:
taskPrompt = aiPrompts.buildTaskExtractionPrompt(messages) 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: if taskResponse and taskResponse.errorCount == 0:
extractedTasks = aiPrompts.parseJsonResponse(taskResponse.content, []) extractedTasks = aiPrompts.parseJsonResponse(taskResponse.content, [])
if isinstance(extractedTasks, list): if isinstance(extractedTasks, list):

View file

@ -24,16 +24,16 @@ def buildResumeGreetingPrompt(messages: List[Dict[str, Any]], contextTitle: str)
for msg in recent: for msg in recent:
role = "Benutzer" if msg.get("role") == "user" else "Coach" role = "Benutzer" if msg.get("role") == "user" else "Coach"
conversation += f"\n{role}: {msg.get('content', '')[:200]}" 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: Bisheriger Verlauf:
{conversation} {conversation}
Erstelle eine kurze, freundliche Begruesssung fuer den Wiedereinstieg (2-3 Saetze): Erstelle eine kurze, freundliche Begrüssung für den Wiedereinstieg (2-3 tze):
- Begruesse den User zurueck - Begrüsse den User zurück
- Fasse in einem Satz zusammen, worum es zuletzt ging - Fasse in einem Satz zusammen, worum es zuletzt ging
- Lade ein, dort weiterzumachen oder eine neue Frage zu stellen - 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: 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" role = "Benutzer" if msg.get("role") == "user" else "Coach"
conversation += f"\n{role}: {msg.get('content', '')}" conversation += f"\n{role}: {msg.get('content', '')}"
return f"""Fasse das folgende Coaching-Gespraech in 4-6 Saetzen zusammen. return f"""Fasse das folgende Coaching-Gespräch in 4-6 Sätzen zusammen.
Behalte: Kernthemen, wichtige Erkenntnisse, erwaehnte Aufgaben, emotionale Wendepunkte, Fortschritte. Behalte: Kernthemen, wichtige Erkenntnisse, erwähnte Aufgaben, emotionale Wendepunkte, Fortschritte.
Entferne Wiederholungen und Fuelltext. Entferne Wiederholungen und Fülltext.
Antworte NUR mit der Zusammenfassung, keine Erklaerungen. Antworte NUR mit der Zusammenfassung, keine Erklärungen.
Gespraech: Gespräch:
{conversation}""" {conversation}"""
@ -115,46 +115,61 @@ def buildCoachingSystemPrompt(
if persona.get("systemPromptOverride"): if persona.get("systemPromptOverride"):
prompt = persona["systemPromptOverride"] prompt = persona["systemPromptOverride"]
else: else:
personaLabel = persona.get("label", "Gespraechspartner") personaLabel = persona.get("label", "Gesprächspartner")
personaDescription = persona.get("description", "") personaDescription = persona.get("description", "")
personaGender = persona.get("gender", "") 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}). prompt = f"""Du spielst die Rolle von "{personaLabel}"{genderHint} in einem Roleplay-Szenario zum Thema: "{contextTitle}" (Kategorie: {contextCategory}).
Rollenbeschreibung: {personaDescription} Rollenbeschreibung: {personaDescription}
WICHTIG fuer dein Verhalten: WICHTIG für dein Verhalten:
- Bleibe KONSEQUENT in deiner Rolle. Du bist NICHT der Coach, du bist {personaLabel}. - Bleibe KONSEQUENT in deiner Rolle. Du bist NICHT der Coach, du bist {personaLabel}.
- Reagiere authentisch und emotional gemaess deiner Rollenbeschreibung. - Reagiere authentisch und emotional gemäss deiner Rollenbeschreibung.
- Verwende eine Sprache und Tonalitaet, die zu deiner Rolle passt. - Verwende eine Sprache und Tonalität, die zu deiner Rolle passt.
- Der Benutzer uebt ein Gespraech mit dir. Gib ihm realistische Reaktionen. - 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 gut kommuniziert, zeige das durch angemessene positive Reaktionen.
- Wenn der Benutzer schlecht kommuniziert, eskaliere entsprechend deiner Rolle. - Wenn der Benutzer schlecht kommuniziert, eskaliere entsprechend deiner Rolle.
Kommunikationsstil: Kommunikationsstil:
- Sprich natuerlich, wie die beschriebene Person sprechen wuerde. - Sprich natürlich, wie die beschriebene Person sprechen würde.
- Verwende keine Emojis. - Verwende keine Emojis.
- Antworte in der Sprache des Benutzers. - Antworte in der Sprache des Benutzers.
- Halte Antworten realistisch kurz (wie in einem echten Gespraech, 2-4 Saetze). - Halte Antworten realistisch kurz (wie in einem echten Gespräch)."""
- WICHTIG: Schreibe reinen Redetext ohne jegliche Formatierung. Kein Markdown, keine Sternchen, keine Hashes, keine Aufzaehlungszeichen, keine Backticks. Deine Antworten werden direkt vorgelesen."""
else: 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: Deine Rolle:
- Stelle gezielte diagnostische Rueckfragen, um das Problem/Thema besser zu verstehen - Stelle gezielte diagnostische Rückfragen, um das Problem/Thema besser zu verstehen
- Gib konkrete, praxisnahe Tipps und Uebungen - Gib konkrete, praxisnahe Tipps und Übungen
- Baue auf fruehere Sessions auf (Kontext-Kontinuitaet) - Baue auf frühere Sessions auf (Kontext-Kontinuität)
- Erkenne Fortschritte und benenne sie - Erkenne Fortschritte und benenne sie
- Schlage am Ende der Session konkrete naechste Schritte vor (als Tasks) - Schlage am Ende der Session konkrete nächste Schritte vor (als Tasks)
- Kommuniziere empathisch, klar und auf Augenhoehe - Kommuniziere empathisch, klar und auf Augenhöhe
Kommunikationsstil: Kommunikationsstil:
- Duze den Benutzer - Duze den Benutzer
- Sei direkt aber wertschaetzend - Sei direkt aber wertschätzend
- Verwende keine Emojis - Verwende keine Emojis
- Antworte in der Sprache des Benutzers - Antworte in der Sprache des Benutzers
- Halte Antworten fokussiert (max 3-4 Absaetze) - Halte Antworten fokussiert (max 3-4 Absätze)"""
- WICHTIG: Schreibe reinen Redetext ohne jegliche Formatierung. Kein Markdown, keine Sternchen, keine Hashes, keine Aufzaehlungszeichen, keine Backticks. Deine Antworten werden direkt vorgelesen."""
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: if contextDescription:
prompt += f"\n\nKontext-Beschreibung: {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) prompt += f"\n\nBisherige Erkenntnisse:\n" + "\n".join(f"- {i}" for i in insightTexts)
if rollingOverview: if rollingOverview:
prompt += f"\n\nGesamtueberblick bisheriger Sessions:\n{rollingOverview[:600]}" prompt += f"\n\nGesamtüberblick bisheriger Sessions:\n{rollingOverview[:600]}"
if summaries: if summaries:
prompt += "\n\nBisherige Sessions (Zusammenfassungen):" prompt += "\n\nBisherige Sessions (Zusammenfassungen):"
@ -209,7 +224,7 @@ Kommunikationsstil:
prompt += f"\n\nAbgeschlossene Aufgaben: {len(doneTasks)}" prompt += f"\n\nAbgeschlossene Aufgaben: {len(doneTasks)}"
if earlierSummary: if earlierSummary:
prompt += f"\n\nAelterer Gespraechsverlauf (zusammengefasst):\n{earlierSummary[:800]}" prompt += f"\n\nÄlterer Gesprächsverlauf (zusammengefasst):\n{earlierSummary[:800]}"
if documentSummaries: if documentSummaries:
prompt += "\n\nRelevante Dokumente zum Kontext:" 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}". return f"""Erstelle eine kompakte Zusammenfassung dieser Coaching-Session zum Thema "{contextTitle}".
Struktur: 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) 2. **Erkenntnisse**: Was wurde erkannt/gelernt (Stichpunkte)
3. **Naechste Schritte**: Konkrete Aufgaben fuer den Benutzer (Stichpunkte) 3. **Nächste Schritte**: Konkrete Aufgaben r den Benutzer (Stichpunkte)
4. **Fortschritt**: Einschaetzung des Fortschritts 4. **Fortschritt**: Einschätzung des Fortschritts
Gespraech: Gespräch:
{conversation} {conversation}
Antworte auf Deutsch, sachlich und kompakt.""" Antworte auf Deutsch, sachlich und kompakt."""
@ -258,21 +273,21 @@ def buildScoringPrompt(messages: List[Dict[str, Any]], contextCategory: str) ->
Kategorie: {contextCategory} Kategorie: {contextCategory}
Bewerte folgende Dimensionen auf einer Skala von 0-100: Bewerte folgende Dimensionen auf einer Skala von 0-100:
- empathy: Einfuehlungsvermoegen - empathy: Einfühlungsvermögen
- clarity: Klarheit der Kommunikation - clarity: Klarheit der Kommunikation
- assertiveness: Durchsetzungsfaehigkeit - assertiveness: Durchsetzungsfähigkeit
- listening: Zuhoerfaehigkeit - listening: Zuhörfähigkeit
- selfReflection: Selbstreflexion - selfReflection: Selbstreflexion
Antworte AUSSCHLIESSLICH als JSON-Array: 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": "..."}} {{"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}""" {conversation}"""
@ -284,7 +299,7 @@ Antworte AUSSCHLIESSLICH als JSON-Array von Strings:
Zusammenfassung: {summary[:500]} 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( def buildFullContextSummaryPrompt(
@ -315,15 +330,15 @@ def buildFullContextSummaryPrompt(
return f"""Erstelle eine kompakte Gesamtzusammenfassung aller Coaching-Sessions zum Thema "{contextTitle}". return f"""Erstelle eine kompakte Gesamtzusammenfassung aller Coaching-Sessions zum Thema "{contextTitle}".
Struktur: 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 2. **Entwicklung**: Wie hat sich das Thema/thematische Schwerpunkte entwickelt
3. **Offene Punkte**: Was steht noch aus 3. **Offene Punkte**: Was steht noch aus
4. **Empfehlung**: Kurzer naechster Fokus 4. **Empfehlung**: Kurzer nächster Fokus
Inhalt: Inhalt:
{combined[:6000]} {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: 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]}") parts.append(f"- {dateStr}: {summary[:300]}")
combined = "\n".join(parts) 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. Behalte: Kernthemen, Fortschritte, wichtige Erkenntnisse, offene Punkte.
Entferne Wiederholungen. Entferne Wiederholungen.
@ -356,15 +371,15 @@ def buildInsightPrompt(messages: List[Dict[str, Any]], summary: Optional[str] =
summarySection = f"\nZusammenfassung: {summary[:500]}" if summary else "" summarySection = f"\nZusammenfassung: {summary[:500]}" if summary else ""
return f"""Generiere 1-3 kurze Coaching-Insights aus dieser Session. 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: Antworte AUSSCHLIESSLICH als JSON-Array:
[{{"text": "Erkenntnis in einem Satz"}}] [{{"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} {summarySection}
Gespraech: Gespräch:
{conversation}""" {conversation}"""
@ -376,7 +391,7 @@ def buildTaskExtractionPrompt(messages: List[Dict[str, Any]]) -> str:
role = "Benutzer" if msg.get("role") == "user" else "Coach" role = "Benutzer" if msg.get("role") == "user" else "Coach"
conversation += f"\n{role}: {msg.get('content', '')}" 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. Nur Aufgaben, die der Benutzer selbst umsetzen soll.
Antworte AUSSCHLIESSLICH als JSON-Array: Antworte AUSSCHLIESSLICH als JSON-Array:
@ -387,7 +402,7 @@ Antworte AUSSCHLIESSLICH als JSON-Array:
priority: "low", "medium", oder "high" priority: "low", "medium", oder "high"
Maximal 3 Aufgaben. Wenn keine klar erkennbar: leeres Array []. Maximal 3 Aufgaben. Wenn keine klar erkennbar: leeres Array [].
Gespraech: Gespräch:
{conversation}""" {conversation}"""

View file

@ -112,7 +112,7 @@ def buildSessionMarkdown(session: Dict[str, Any], messages: List[Dict[str, Any]]
lines += ["", "## Zusammenfassung", "", summary] lines += ["", "## Zusammenfassung", "", summary]
if messages: if messages:
lines += ["", "## Gespraechsverlauf", ""] lines += ["", "## Gesprächsverlauf", ""]
for msg in messages: for msg in messages:
role = "Du" if msg.get("role") == "user" else "Coach" role = "Du" if msg.get("role") == "user" else "Coach"
content = msg.get("content", "") content = msg.get("content", "")
@ -228,7 +228,7 @@ def _buildPdfContent(context, sessions, tasks, scores, isDossier=True, messages=
sections.append({ sections.append({
"id": "chat", "id": "chat",
"content_type": "heading", "content_type": "heading",
"elements": [{"text": "Gespraechsverlauf", "level": 2}], "elements": [{"text": "Gesprächsverlauf", "level": 2}],
}) })
sections.append({ sections.append({
"id": "chat_content", "id": "chat_content",

View file

@ -53,7 +53,7 @@ BADGE_DEFINITIONS: Dict[str, Dict[str, Any]] = {
}, },
"high_score": { "high_score": {
"label": "Bestleistung", "label": "Bestleistung",
"description": "Durchschnittsscore ueber 80 in einer Session", "description": "Durchschnittsscore über 80 in einer Session",
"icon": "medal", "icon": "medal",
}, },
"multi_context": { "multi_context": {

View file

@ -21,18 +21,18 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
{ {
"key": "critical_cfo_f", "key": "critical_cfo_f",
"label": "Kritische CFO", "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. " "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", "gender": "f",
"category": "builtin", "category": "builtin",
}, },
{ {
"key": "difficult_employee_m", "key": "difficult_employee_m",
"label": "Schwieriger Mitarbeiter", "label": "Schwieriger Mitarbeiter",
"description": "Thomas Huber, langjaeheriger Mitarbeiter der sich uebergangen fuehlt. Defensiv, emotional, nimmt Kritik persoenlich. " "description": "Thomas Huber, langjähriger Mitarbeiter der sich übergangen fühlt. Defensiv, emotional, nimmt Kritik persönlich. "
"Verweist staendig auf seine Erfahrung und fruehhere Verdienste. Reagiert mit Widerstand auf Veraenderungen. " "Verweist ständig auf seine Erfahrung und frühere Verdienste. Reagiert mit Widerstand auf Veränderungen. "
"Braucht das Gefuehl, gehoert und wertgeschaetzt zu werden, bevor er sich oeffnet.", "Braucht das Gefühl, gehört und wertgeschätzt zu werden, bevor er sich öffnet.",
"gender": "m", "gender": "m",
"category": "builtin", "category": "builtin",
}, },
@ -41,7 +41,7 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
"label": "Unsichere neue Mitarbeiterin", "label": "Unsichere neue Mitarbeiterin",
"description": "Lisa Brunner, seit drei Wochen im Team. Fachlich kompetent aber unsicher in der neuen Umgebung. " "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 " "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", "gender": "f",
"category": "builtin", "category": "builtin",
}, },
@ -49,33 +49,33 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
"key": "board_member_m", "key": "board_member_m",
"label": "Verwaltungsrat", "label": "Verwaltungsrat",
"description": "Dr. Peter Keller, erfahrener Verwaltungsrat. Formell, strategisch denkend, zeitlich unter Druck. " "description": "Dr. Peter Keller, erfahrener Verwaltungsrat. Formell, strategisch denkend, zeitlich unter Druck. "
"Erwartet praegnante Praesentationen auf den Punkt. Unterbricht bei zu vielen Details. " "Erwartet prägnante Präsentationen auf den Punkt. Unterbricht bei zu vielen Details. "
"Interessiert sich fuer das grosse Bild, Risiken und strategische Implikationen. Ungeduldig bei Smalltalk.", "Interessiert sich für das grosse Bild, Risiken und strategische Implikationen. Ungeduldig bei Smalltalk.",
"gender": "m", "gender": "m",
"category": "builtin", "category": "builtin",
}, },
{ {
"key": "angry_customer_f", "key": "angry_customer_f",
"label": "Aufgebrachte Kundin", "label": "Aufgebrachte Kundin",
"description": "Maria Rossi, Geschaeftskunde die wuetend ist wegen einer fehlerhaften Lieferung. Emotional, laut, " "description": "Maria Rossi, Geschäftskunde die wütend ist wegen einer fehlerhaften Lieferung. Emotional, laut, "
"droht mit Vertragsaufloesung. Will sofortige Loesungen, keine Erklaerungen oder Entschuldigungen. " "droht mit Vertragsauflösung. Will sofortige Lösungen, keine Erklärungen oder Entschuldigungen. "
"Kann beruhigt werden durch empathisches Zuhoeren und konkrete Sofortmassnahmen.", "Kann beruhigt werden durch empathisches Zuhören und konkrete Sofortmassnahmen.",
"gender": "f", "gender": "f",
"category": "builtin", "category": "builtin",
}, },
{ {
"key": "resistant_manager_m", "key": "resistant_manager_m",
"label": "Widerstaendiger Abteilungsleiter", "label": "Widerständiger Abteilungsleiter",
"description": "Martin Weber, Abteilungsleiter seit 15 Jahren. Blockiert systematisch Veraenderungsprojekte mit " "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'. " "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", "gender": "m",
"category": "builtin", "category": "builtin",
}, },
{ {
"key": "ambitious_colleague_f", "key": "ambitious_colleague_f",
"label": "Ehrgeizige Kollegin", "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 " "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.", "Sichtbarkeit beim Management. Kann kooperativ werden, wenn man ihr Win-Win-Szenarien aufzeigt.",
"gender": "f", "gender": "f",
@ -83,20 +83,20 @@ BUILTIN_PERSONAS: List[Dict[str, Any]] = [
}, },
{ {
"key": "partner_supportive_f", "key": "partner_supportive_f",
"label": "Verstaendnisvolle Lebenspartnerin", "label": "Verständnisvolle Lebenspartnerin",
"description": "Claudia, deine Lebenspartnerin. Grundsaetzlich unterstuetzend, aber zunehmend besorgt ueber deine " "description": "Claudia, deine Lebenspartnerin. Grundsätzlich unterstützend, aber zunehmend besorgt über deine "
"Work-Life-Balance. Moechte ueber Arbeitsbelastung sprechen und gemeinsame Zeit einfordern. " "Work-Life-Balance. Möchte über Arbeitsbelastung sprechen und gemeinsame Zeit einfordern. "
"Reagiert emotional auf Abweisung, ist aber offen fuer kompromissorientierte Gespraeche. " "Reagiert emotional auf Abweisung, ist aber offen für kompromissorientierte Gespräche. "
"Wuenscht sich, dass du mehr von deinen Gefuehlen teilst.", "Wünscht sich, dass du mehr von deinen Gefühlen teilst.",
"gender": "f", "gender": "f",
"category": "builtin", "category": "builtin",
}, },
{ {
"key": "partner_critical_m", "key": "partner_critical_m",
"label": "Kritischer Lebenspartner", "label": "Kritischer Lebenspartner",
"description": "Michael, dein Lebenspartner. Frustriert ueber deine haeufige Abwesenheit und staendiges Arbeiten. " "description": "Michael, dein Lebenspartner. Frustriert über deine häufige Abwesenheit und ständiges Arbeiten. "
"Drueckt Enttaeuschung offen aus, manchmal mit Sarkasmus. Fuehlt sich vernachlaessigt und " "Drückt Enttäuschung offen aus, manchmal mit Sarkasmus. Fühlt sich vernachlässigt und "
"hinterfragt deine Prioritaeten. Braucht das Gefuehl, dass die Beziehung dir genauso wichtig ist " "hinterfragt deine Prioritäten. Braucht das Gefühl, dass die Beziehung dir genauso wichtig ist "
"wie die Karriere. Reagiert positiv auf ehrliche Selbstreflexion.", "wie die Karriere. Reagiert positiv auf ehrliche Selbstreflexion.",
"gender": "m", "gender": "m",
"category": "builtin", "category": "builtin",

View file

@ -65,14 +65,14 @@ class TestBuildCoachingSystemPrompt:
def test_promptLanguageIsGerman(self): def test_promptLanguageIsGerman(self):
context = {"title": "Test", "category": "custom"} context = {"title": "Test", "category": "custom"}
prompt = buildCoachingSystemPrompt(context, [], []) 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): def test_withEarlierSummary(self):
context = {"title": "Test", "category": "custom"} context = {"title": "Test", "category": "custom"}
messages = [{"role": "user", "content": "Recent question"}] messages = [{"role": "user", "content": "Recent question"}]
earlierSummary = "User discussed delegation. Coach suggested practice." earlierSummary = "User discussed delegation. Coach suggested practice."
prompt = buildCoachingSystemPrompt(context, messages, [], earlierSummary=earlierSummary) prompt = buildCoachingSystemPrompt(context, messages, [], earlierSummary=earlierSummary)
assert "Aelterer Gespraechsverlauf" in prompt assert "Älterer Gesprächsverlauf" in prompt
assert "delegation" in prompt.lower() assert "delegation" in prompt.lower()
assert "Recent question" in prompt assert "Recent question" in prompt
@ -81,7 +81,7 @@ class TestBuildCoachingSystemPrompt:
prompt = buildCoachingSystemPrompt( prompt = buildCoachingSystemPrompt(
context, [], [], rollingOverview="User arbeitet an Delegation. Fortschritt sichtbar." context, [], [], rollingOverview="User arbeitet an Delegation. Fortschritt sichtbar."
) )
assert "Gesamtueberblick" in prompt assert "Gesamtüberblick" in prompt
assert "Delegation" in prompt assert "Delegation" in prompt
def test_withRetrievedSession(self): def test_withRetrievedSession(self):