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:
parent
92d9a2a0d5
commit
12b0d3d36e
6 changed files with 212 additions and 168 deletions
|
|
@ -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,
|
||||
ttsTask = asyncio.create_task(
|
||||
_generateAndEmitTts(sessionId, speechContent, self.currentUser, self.mandateId, self.instanceId, interface)
|
||||
)
|
||||
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 _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,
|
||||
ttsTask = asyncio.create_task(
|
||||
_generateAndEmitTts(sessionId, speechContent, self.currentUser, self.mandateId, self.instanceId, interface)
|
||||
)
|
||||
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 _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):
|
||||
|
|
|
|||
|
|
@ -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}"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue