# 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 (Teamsbot-style) COMPRESSION_MESSAGE_THRESHOLD = 25 COMPRESSION_RECENT_COUNT = 15 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}" zurueck. Bisheriger Verlauf: {conversation} Erstelle eine kurze, freundliche Begruesssung fuer den Wiedereinstieg (2-3 Saetze): - Begruesse den User zurueck - 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.""" 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-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. Gespraech: {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, ) -> 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 [] prompt = f"""Du bist ein erfahrener Kommunikations-Coach fuer Fuehrungskraefte. 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) - Erkenne Fortschritte und benenne sie - Schlage am Ende der Session konkrete naechste Schritte vor (als Tasks) - Kommuniziere empathisch, klar und auf Augenhoehe Kommunikationsstil: - Duze den Benutzer - Sei direkt aber wertschaetzend - 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.""" 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\nGesamtueberblick 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\nAelterer Gespraechsverlauf (zusammengefasst):\n{earlierSummary[:800]}" 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.""" 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 kompakte Zusammenfassung dieser Coaching-Session zum Thema "{contextTitle}". Struktur: 1. **Kernthema**: Was wurde besprochen (1-2 Saetze) 2. **Erkenntnisse**: Was wurde erkannt/gelernt (Stichpunkte) 3. **Naechste Schritte**: Konkrete Aufgaben fuer den Benutzer (Stichpunkte) 4. **Fortschritt**: Einschaetzung des Fortschritts Gespraech: {conversation} Antworte auf Deutsch, sachlich und kompakt.""" def buildScoringPrompt(messages: List[Dict[str, Any]], contextCategory: str) -> str: """Build a prompt to evaluate competence dimensions after a session.""" conversation = "" for msg in messages: role = "Benutzer" if msg.get("role") == "user" else "Coach" conversation += f"\n{role}: {msg.get('content', '')}" return f"""Bewerte die Kommunikationskompetenz des Benutzers basierend auf dieser Coaching-Session. Kategorie: {contextCategory} Bewerte folgende Dimensionen auf einer Skala von 0-100: - empathy: Einfuehlungsvermoegen - clarity: Klarheit der Kommunikation - assertiveness: Durchsetzungsfaehigkeit - listening: Zuhoerfaehigkeit - selfReflection: Selbstreflexion Antworte AUSSCHLIESSLICH als JSON-Array: [ {{"dimension": "empathy", "score": 65, "trend": "improving", "evidence": "Zeigt zunehmendes Verstaendnis..."}}, {{"dimension": "clarity", "score": 70, "trend": "stable", "evidence": "..."}} ] Trend: "improving", "stable", oder "declining" basierend auf dem Gespraechsverlauf. Gespraech: {conversation}""" def buildKeyTopicsExtractionPrompt(summary: str, messages: List[Dict[str, Any]]) -> str: """Extract 2-5 key topics from session for indexing.""" return f"""Extrahiere 2-5 Kernthemen aus dieser Coaching-Session. Antworte AUSSCHLIESSLICH als JSON-Array von Strings: ["Thema 1", "Thema 2", "Thema 3"] Zusammenfassung: {summary[:500]} Nur konkrete Themen (z.B. Delegation, Feedback-Gespraech, Konflikt mit Vorgesetztem).""" def buildFullContextSummaryPrompt( sessionSummaries: List[Dict[str, Any]], currentSessionSummary: Optional[str], currentSessionMessages: List[Dict[str, Any]], contextTitle: str, ) -> str: """Build prompt for full context summary (summarize_all intent).""" parts = [] for s in sessionSummaries: dateStr = s.get("date", "") summary = s.get("summary", "") if summary: parts.append(f"Session {dateStr}: {summary}") if currentSessionSummary: parts.append(f"Aktuelle Session (zusammengefasst): {currentSessionSummary}") recent = "\n".join( f"{m.get('role','user')}: {m.get('content','')[:200]}" for m in currentSessionMessages[-10:] ) if recent: parts.append(f"Aktuelle Session (letzte Nachrichten):\n{recent}") combined = "\n\n".join(parts) return f"""Erstelle eine kompakte Gesamtzusammenfassung aller Coaching-Sessions zum Thema "{contextTitle}". Struktur: 1. **Gesamtueberblick**: Was wurde ueber 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 Inhalt: {combined[:6000]} Antworte auf Deutsch, sachlich, 4-6 Absaetze.""" def buildRollingOverviewPrompt(sessionSummaries: List[Dict[str, Any]], contextTitle: str) -> str: """Build prompt for rolling overview (compress many sessions).""" parts = [] for s in sessionSummaries: dateStr = s.get("date", "") summary = s.get("summary", "") if summary: 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. Behalte: Kernthemen, Fortschritte, wichtige Erkenntnisse, offene Punkte. Entferne Wiederholungen. Sessions: {combined} Antworte NUR mit der Zusammenfassung.""" def buildInsightPrompt(messages: List[Dict[str, Any]], summary: Optional[str] = None) -> str: """Build a prompt to generate coaching insights from a completed session.""" conversation = "" for msg in messages[-15:]: role = "Benutzer" if msg.get("role") == "user" else "Coach" conversation += f"\n{role}: {msg.get('content', '')[:300]}" 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. Antworte AUSSCHLIESSLICH als JSON-Array: [{{"text": "Erkenntnis in einem Satz"}}] Nur echte Erkenntnisse, keine Banalitaeten. Wenn keine klaren Insights: leeres Array []. {summarySection} Gespraech: {conversation}""" def buildTaskExtractionPrompt(messages: List[Dict[str, Any]]) -> str: """Build a prompt to extract actionable tasks from a session.""" recentForTasks = messages[-25:] if len(messages) > 25 else messages conversation = "" for msg in recentForTasks: 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. Nur Aufgaben, die der Benutzer selbst umsetzen soll. Antworte AUSSCHLIESSLICH als JSON-Array: [ {{"title": "Aufgabentitel", "description": "Kurze Beschreibung", "priority": "medium"}} ] priority: "low", "medium", oder "high" Maximal 3 Aufgaben. Wenn keine klar erkennbar: leeres Array []. Gespraech: {conversation}""" def parseJsonResponse(responseText: str, fallback: Any = None) -> Any: """Parse a JSON response from AI, handling markdown code blocks.""" text = responseText.strip() if text.startswith("```"): lines = text.split("\n") lines = lines[1:] # remove opening ```json if lines and lines[-1].strip() == "```": lines = lines[:-1] text = "\n".join(lines) try: return json.loads(text) except json.JSONDecodeError: logger.warning(f"Failed to parse AI JSON response: {text[:200]}") return fallback def _parseJsonField(value: Optional[str], fallback: Any = None) -> Any: if not value: return fallback if isinstance(value, (list, dict)): return value try: return json.loads(value) except (json.JSONDecodeError, TypeError): return fallback