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):