# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ CommCoach AI Service. Handles system prompts, diagnostic question generation, session summarization, and scoring. """ import logging import json from typing import Optional, Dict, Any, List, Tuple logger = logging.getLogger(__name__) # Compression thresholds — lowered for voice fragment pattern (multiple user msgs per turn) COMPRESSION_MESSAGE_THRESHOLD = 15 COMPRESSION_RECENT_COUNT = 10 COMPRESSION_MAX_MESSAGES_FETCH = 80 def buildResumeGreetingPrompt(messages: List[Dict[str, Any]], contextTitle: str) -> str: """Build prompt for AI to generate a follow-up greeting when user returns to session.""" recent = messages[-6:] if len(messages) > 6 else messages conversation = "" 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}" zurück. Bisheriger Verlauf: {conversation} 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 Begrüssung, keine Erklärungen.""" def buildEarlierConversationSummaryPrompt(messages: List[Dict[str, Any]]) -> str: """Build prompt to summarize older messages for long-session compression.""" conversation = "" for msg in messages: role = "Benutzer" if msg.get("role") == "user" else "Coach" conversation += f"\n{role}: {msg.get('content', '')}" 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. Gespräch: {conversation}""" def prepareMessagesForPrompt( messages: List[Dict[str, Any]], compressedSummary: Optional[str], compressedUpToCount: Optional[int], ) -> Tuple[Optional[str], List[Dict[str, Any]]]: """ Prepare message history for the coaching prompt. Returns (earlierSummary, recentMessages). If messages <= THRESHOLD: (None, messages). If messages > THRESHOLD: (summary or None, last RECENT_COUNT messages). Cached summary is reused when compressedUpToCount >= len(toSummarize). """ if len(messages) <= COMPRESSION_MESSAGE_THRESHOLD: return None, messages toSummarizeCount = len(messages) - COMPRESSION_RECENT_COUNT if toSummarizeCount <= 0: return None, messages toSummarize = messages[:toSummarizeCount] recent = messages[-COMPRESSION_RECENT_COUNT:] try: upTo = int(compressedUpToCount) if compressedUpToCount is not None else None except (TypeError, ValueError): upTo = None if compressedSummary and upTo is not None and upTo >= toSummarizeCount: return compressedSummary, recent return None, messages def buildCoachingSystemPrompt( context: Dict[str, Any], previousMessages: List[Dict[str, Any]], tasks: List[Dict[str, Any]], previousSessionSummaries: Optional[List[Dict[str, Any]]] = None, earlierSummary: Optional[str] = None, rollingOverview: Optional[str] = None, retrievedSession: Optional[Dict[str, Any]] = None, retrievedByTopic: Optional[List[Dict[str, Any]]] = None, persona: Optional[Dict[str, Any]] = None, documentSummaries: Optional[List[str]] = None, referencedDocumentContents: Optional[List[Dict[str, Any]]] = None, ) -> str: """Build the system prompt for a coaching session, including context history, tasks, and session continuity.""" contextTitle = context.get("title", "General Coaching") contextCategory = context.get("category", "custom") contextDescription = context.get("description", "") goalsRaw = context.get("goals") insightsRaw = context.get("insights") goals = _parseJsonField(goalsRaw, []) insights = _parseJsonField(insightsRaw, []) openTasks = [t for t in tasks if t.get("status") in ("open", "inProgress")] doneTasks = [t for t in tasks if t.get("status") == "done"] summaries = previousSessionSummaries or [] if persona and persona.get("key") != "coach": if persona.get("systemPromptOverride"): prompt = persona["systemPromptOverride"] else: personaLabel = persona.get("label", "Gesprächspartner") personaDescription = persona.get("description", "") personaGender = persona.get("gender", "") 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 für dein Verhalten: - Du BIST {personaLabel}. Du bist NICHT der Coach. Sprich IMMER direkt als diese Person. - Beschreibe KEINE Szenarien. Beginne SOFORT mit dem Dialog in deiner Rolle. - 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 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 Gespräch).""" else: 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 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 nächste Schritte vor (als Tasks) - Kommuniziere empathisch, klar und auf Augenhöhe Roleplay: - Wenn der Benutzer dich bittet, eine bestimmte Person zu spielen (z.B. einen kritischen Kunden, einen Vorgesetzten, einen Mitarbeiter), dann wechsle SOFORT in diese Rolle. - Beschreibe KEIN Szenario. Sprich direkt ALS diese Person. Beginne sofort mit dem Dialog in der Rolle. - Bleibe in der Rolle, bis der Benutzer explizit sagt, dass das Roleplay beendet ist oder Feedback möchte. - Reagiere authentisch, emotional und realistisch wie die beschriebene Person. Kommunikationsstil: - Duze den Benutzer - Sei direkt aber wertschätzend - Verwende keine Emojis - Antworte in der Sprache des Benutzers - 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": Dokumente (Zusammenfassungen, Checklisten, Übungen, Protokolle). Erstelle ein Dokument wenn: der Benutzer explizit darum bittet, du strukturierte Inhalte (Listen, Pläne, Checklisten) lieferst, oder Material zum Aufbewahren sinnvoll ist. Jedes Dokument: {"title": "...", "content": "Markdown-Inhalt"}. Wenn keine: 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}" if goals: goalTexts = [g.get("text", g) if isinstance(g, dict) else str(g) for g in goals] prompt += f"\n\nZiele des Benutzers:\n" + "\n".join(f"- {g}" for g in goalTexts) if insights: insightTexts = [i.get("text", i) if isinstance(i, dict) else str(i) for i in insights[-5:]] prompt += f"\n\nBisherige Erkenntnisse:\n" + "\n".join(f"- {i}" for i in insightTexts) if rollingOverview: prompt += f"\n\nGesamtüberblick bisheriger Sessions:\n{rollingOverview[:600]}" if summaries: prompt += "\n\nBisherige Sessions (Zusammenfassungen):" for s in summaries[-5:]: summary = s.get("summary", s.get("text", "")) dateStr = s.get("date", "") prefix = f"[{dateStr}] " if dateStr else "" if summary: prompt += f"\n- {prefix}{summary[:350]}" if retrievedSession: dateStr = "" startedAt = retrievedSession.get("startedAt") or retrievedSession.get("createdAt") if startedAt: try: from datetime import datetime dt = datetime.fromisoformat(str(startedAt).replace("Z", "+00:00")) dateStr = dt.strftime("%d.%m.%Y") except Exception: pass prompt += f"\n\nVom Benutzer angefragte Session ({dateStr}):" prompt += f"\n{retrievedSession.get('summary', '')[:500]}" if retrievedByTopic: prompt += "\n\nRelevante Sessions zum angefragten Thema:" for s in retrievedByTopic[:3]: summary = s.get("summary", "") dateStr = s.get("date", "") if summary: prompt += f"\n- [{dateStr}] {summary[:300]}" if openTasks: prompt += "\n\nOffene Aufgaben:" for t in openTasks: prompt += f"\n- [{t.get('status')}] {t.get('title')}" if doneTasks: prompt += f"\n\nAbgeschlossene Aufgaben: {len(doneTasks)}" if earlierSummary: prompt += f"\n\nÄlterer Gesprächsverlauf (zusammengefasst):\n{earlierSummary[:800]}" if documentSummaries: prompt += "\n\nRelevante Dokumente zum Kontext:" for docSummary in documentSummaries[:5]: prompt += f"\n- {docSummary[:300]}" if referencedDocumentContents: prompt += "\n\nReferenzierte Dokumente (vollstaendiger Inhalt):" for doc in referencedDocumentContents[:3]: prompt += f"\n\n=== {doc.get('title', 'Dokument')} (id: {doc.get('id', '')}) ===\n{doc.get('content', '')[:3000]}" prompt += """ Du kannst bestehende Dokumente aendern oder neue erstellen. Fuer UPDATE eines bestehenden Dokuments: {"id": "", "title": "...", "content": "...neuer vollstaendiger Inhalt..."} Fuer ein NEUES Dokument: {"title": "...", "content": "...Inhalt..."}""" if previousMessages: prompt += "\n\nVorige Nachrichten dieser Session (Kontext):" for msg in previousMessages[-12:]: role = msg.get("role", "user") content = msg.get("content", "")[:400] prompt += f"\n[{role}]: {content}" return prompt def buildSummaryPrompt(messages: List[Dict[str, Any]], contextTitle: str) -> str: """Build a prompt to generate a session summary as JSON with plain text and styled HTML email.""" conversation = "" for msg in messages: role = "Benutzer" if msg.get("role") == "user" else "Coach" conversation += f"\n{role}: {msg.get('content', '')}" return f"""Erstelle eine Zusammenfassung dieser Coaching-Session zum Thema "{contextTitle}". Antworte AUSSCHLIESSLICH als JSON mit zwei Feldern: {{ "summary": "Kompakte Zusammenfassung als Plaintext (fuer Anzeige in der App). Struktur: 1. Kernthema, 2. Erkenntnisse, 3. Naechste Schritte, 4. Fortschritt.", "emailHtml": "
...
" }} Fuer "emailHtml": Erstelle ein professionell formatiertes HTML-Fragment (KEIN vollstaendiges HTML-Dokument, nur der Inhalt-Block). Verwende inline CSS fuer schoene Darstellung in E-Mail-Clients: - Verwende

fuer Abschnitte (color: #1e40af; margin: 20px 0 8px; font-size: 16px) - Verwende