commcoach: email subject uses context title instead of UUID, AI generates HTML directly

Made-with: Cursor
This commit is contained in:
patrick-motsch 2026-03-07 01:28:51 +01:00
parent 364e431749
commit 740d2a0b49
2 changed files with 59 additions and 22 deletions

View file

@ -86,6 +86,27 @@ def cleanupSessionEvents(sessionId: str):
CHUNK_WORD_SIZE = 4
CHUNK_DELAY_SECONDS = 0.05
def _wrapEmailHtml(contentHtml: str) -> str:
"""Wrap AI-generated HTML content in a styled email shell."""
return f"""<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
<body style="margin:0;padding:0;background-color:#f4f4f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<div style="background-color:#f4f4f7;padding:32px 16px">
<div style="max-width:600px;margin:0 auto;background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
<div style="background:linear-gradient(135deg,#2563eb,#1e40af);padding:28px 32px">
<h1 style="margin:0;color:#ffffff;font-size:20px;font-weight:600">Coaching-Session Zusammenfassung</h1>
<p style="margin:6px 0 0;color:rgba(255,255,255,0.8);font-size:13px">PowerOn CommCoach</p>
</div>
<div style="padding:28px 32px;color:#374151;font-size:15px;line-height:1.65">{contentHtml}</div>
<div style="padding:18px 32px;background:#f9fafb;border-top:1px solid #e5e7eb;text-align:center">
<p style="margin:0;color:#9ca3af;font-size:12px">Diese Zusammenfassung wurde automatisch erstellt.</p>
</div>
</div>
</div>
</body>
</html>"""
DOC_INTENT_MAX_DOCS = 3
DOC_CONTENT_MAX_CHARS = 3000
@ -634,14 +655,21 @@ class CommcoachService:
})
return session
# Generate summary
# Generate summary (AI returns JSON with summary + emailHtml)
summary = None
emailHtml = None
try:
summaryPrompt = aiPrompts.buildSummaryPrompt(messages, context.get("title", "Coaching"))
summaryResponse = await self._callAi("Du bist ein präziser Zusammenfasser.", summaryPrompt)
summary = summaryResponse.content.strip() if summaryResponse and summaryResponse.errorCount == 0 else None
summaryResponse = await self._callAi("Du bist ein präziser Zusammenfasser. Antworte NUR als JSON.", summaryPrompt)
if summaryResponse and summaryResponse.errorCount == 0 and summaryResponse.content:
parsed = aiPrompts.parseJsonResponse(summaryResponse.content.strip(), None)
if isinstance(parsed, dict):
summary = parsed.get("summary") or parsed.get("text")
emailHtml = parsed.get("emailHtml")
else:
summary = summaryResponse.content.strip()
except Exception as e:
logger.warning(f"Summary generation failed: {e}")
summary = None
keyTopics = None
if summary:
@ -782,7 +810,8 @@ class CommcoachService:
# Send email summary
if summary:
await self._sendSessionEmail(session, summary, interface)
contextTitle = context.get("title", "Coaching") if context else "Coaching"
await self._sendSessionEmail(session, summary, emailHtml, contextTitle, interface)
await emitSessionEvent(sessionId, "sessionState", {
"status": "completed",
@ -833,8 +862,8 @@ class CommcoachService:
except Exception as e:
logger.warning(f"Failed to update streak: {e}")
async def _sendSessionEmail(self, session: Dict[str, Any], summary: str, interface):
"""Send session summary via email if enabled."""
async def _sendSessionEmail(self, session: Dict[str, Any], summary: str, emailHtml: str, contextTitle: str, interface):
"""Send session summary via email if enabled. Uses AI-generated HTML directly."""
try:
profile = interface.getProfile(self.userId, self.instanceId)
if profile and not profile.get("emailSummaryEnabled", True):
@ -849,13 +878,10 @@ class CommcoachService:
return
messaging = getMessagingInterface()
subject = f"Coaching-Session Zusammenfassung: {session.get('contextId', 'Session')}"
htmlMessage = f"""
<h2>Coaching-Session Zusammenfassung</h2>
<p>{summary.replace(chr(10), '<br>')}</p>
<hr>
<p><small>Diese Zusammenfassung wurde automatisch erstellt.</small></p>
"""
subject = f"Coaching-Session Zusammenfassung: {contextTitle}"
contentHtml = emailHtml if emailHtml else f"<p>{summary}</p>"
htmlMessage = _wrapEmailHtml(contentHtml)
messaging.send("email", user.email, subject, htmlMessage)
interface.updateSession(session.get("id"), {"emailSent": True})

View file

@ -260,24 +260,35 @@ Fuer ein NEUES Dokument: {"title": "...", "content": "...Inhalt..."}"""
def buildSummaryPrompt(messages: List[Dict[str, Any]], contextTitle: str) -> str:
"""Build a prompt to generate a session summary."""
"""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 kompakte Zusammenfassung dieser Coaching-Session zum Thema "{contextTitle}".
return f"""Erstelle eine Zusammenfassung dieser Coaching-Session zum Thema "{contextTitle}".
Struktur:
1. **Kernthema**: Was wurde besprochen (1-2 Sätze)
2. **Erkenntnisse**: Was wurde erkannt/gelernt (Stichpunkte)
3. **Nächste Schritte**: Konkrete Aufgaben für den Benutzer (Stichpunkte)
4. **Fortschritt**: Einschätzung des Fortschritts
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": "<div>...</div>"
}}
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 <h3> fuer Abschnitte (color: #1e40af; margin: 20px 0 8px; font-size: 16px)
- Verwende <ul>/<li> fuer Stichpunkte (margin: 4px 0; line-height: 1.6)
- Verwende <strong> fuer Hervorhebungen
- Verwende <p> fuer Fliesstext (color: #374151; line-height: 1.65; font-size: 15px)
- Verwende <hr style="border:none;border-top:1px solid #e5e7eb;margin:20px 0"> als Trenner
Fuer "summary": Kompakter Plaintext ohne HTML/Markdown. Abschnitte mit Zeilenumbruechen trennen.
Gespräch:
{conversation}
Antworte auf Deutsch, sachlich und kompakt."""
Antworte auf Deutsch, sachlich und kompakt. NUR JSON, keine Erklaerungen."""
def buildScoringPrompt(messages: List[Dict[str, Any]], contextCategory: str) -> str: