503 lines
20 KiB
Python
503 lines
20 KiB
Python
# 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 Chat-Antworten (text/speech) fokussiert. Dokumente duerfen ausfuehrlich sein."""
|
||
|
||
prompt += """
|
||
|
||
Handlungsprinzip:
|
||
- Wenn der Benutzer dich bittet, etwas zu erstellen (Dokument, Präsentation, Checkliste, Plan), dann TU ES SOFORT. Frage NICHT nochmals nach Bestätigung.
|
||
- Verwende alle verfügbaren Informationen aus dem Chat-Verlauf, den Dokumenten und dem Kontext.
|
||
- Wenn der Benutzer sagt "erstelle", "mach", "schreib", dann liefere das fertige Ergebnis — keine Aufzählung von Punkten, die du "gleich umsetzen wirst".
|
||
- Dir wird automatisch relevanter Kontext aus früheren Sessions bereitgestellt (Relevant Knowledge). Nutze diesen für Kontinuität und Bezugnahme auf frühere Gespräche.
|
||
|
||
Antwortformat:
|
||
- Antworte direkt als Freitext (KEIN JSON). Markdown-Formatierung ist erlaubt.
|
||
- Halte Antworten gesprächig und kurz (2-6 Sätze im Normalfall), wie in einem echten Coaching-Gespräch.
|
||
- Bei komplexen Themen oder wenn der Benutzer Details anfragt, darf die Antwort ausführlicher sein.
|
||
- Dein Text wird sowohl angezeigt als auch vorgelesen – schreibe daher natürlich und gut sprechbar.
|
||
|
||
Tool-Nutzung:
|
||
- Du hast Zugriff auf Tools (Dateien lesen, Web-Suche, Datenquellen abfragen) wenn der Benutzer Dateien/Quellen angehängt hat oder Recherche benötigt.
|
||
- Nutze Tools NUR wenn nötig. Für normales Coaching-Gespräch: antworte direkt ohne Tools.
|
||
- Wenn du ein Tool nutzt, erkläre kurz was du tust."""
|
||
|
||
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 und Mandantenwissen zum angefragten Thema:"
|
||
for s in retrievedByTopic[:5]:
|
||
summary = s.get("summary", s.get("content", ""))
|
||
if not summary:
|
||
continue
|
||
dateStr = s.get("date", "")
|
||
if s.get("source") == "rag":
|
||
label = s.get("ragSourceLabel") or "Mandantenwissen"
|
||
prompt += f"\n- [Wissen: {label}] {summary[:320]}"
|
||
else:
|
||
prefix = f"[{dateStr}] " if dateStr else ""
|
||
prompt += f"\n- {prefix}{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": "<doc-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 plus structured email content."""
|
||
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 im folgenden Format:
|
||
|
||
{{
|
||
"summary": "Kompakte Plaintext-Zusammenfassung fuer die App. Struktur: Kernthema, Erkenntnisse, Naechste Schritte, Fortschritt.",
|
||
"email": {{
|
||
"headline": "Kurze, professionelle Titelzeile fuer die E-Mail",
|
||
"intro": "1-2 Saetze, die den Kern der Session auf den Punkt bringen",
|
||
"coreTopic": "Das zentrale Thema in einem praezisen Satz",
|
||
"insights": ["Erkenntnis 1", "Erkenntnis 2"],
|
||
"nextSteps": ["Naechster Schritt 1", "Naechster Schritt 2"],
|
||
"progress": ["Fortschritt 1", "Fortschritt 2"]
|
||
}}
|
||
}}
|
||
|
||
Regeln:
|
||
- KEIN HTML erzeugen.
|
||
- "summary" ist reiner Plaintext ohne Markdown.
|
||
- "headline" kurz und professionell.
|
||
- "intro" in natuerlichem Business-Deutsch.
|
||
- "insights", "nextSteps" und "progress" jeweils als kurze Stichpunkte.
|
||
- Maximal 4 Eintraege pro Liste.
|
||
- Wenn eine Liste leer ist, gib [] zurueck.
|
||
|
||
Gespräch:
|
||
{conversation}
|
||
|
||
Antworte auf Deutsch, sachlich, klar und kompakt. NUR JSON, keine Erklaerungen."""
|
||
|
||
|
||
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: Einfühlungsvermögen
|
||
- clarity: Klarheit der Kommunikation
|
||
- assertiveness: Durchsetzungsfähigkeit
|
||
- listening: Zuhörfähigkeit
|
||
- selfReflection: Selbstreflexion
|
||
|
||
Antworte AUSSCHLIESSLICH als JSON-Array:
|
||
[
|
||
{{"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 Gesprächsverlauf.
|
||
|
||
Gespräch:
|
||
{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-Gespräch, 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. **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 nächster Fokus
|
||
|
||
Inhalt:
|
||
{combined[:6000]}
|
||
|
||
Antworte auf Deutsch, sachlich, 4-6 Absätze."""
|
||
|
||
|
||
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 Sätzen 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 prägende Erkenntnis oder ein Aha-Moment des Benutzers.
|
||
|
||
Antworte AUSSCHLIESSLICH als JSON-Array:
|
||
[{{"text": "Erkenntnis in einem Satz"}}]
|
||
|
||
Nur echte Erkenntnisse, keine Banalitäten. Wenn keine klaren Insights: leeres Array [].
|
||
{summarySection}
|
||
|
||
Gespräch:
|
||
{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/nächste Schritte aus diesem Coaching-Gespräch.
|
||
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 [].
|
||
|
||
Gespräch:
|
||
{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:]
|
||
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
|
||
|
||
_parseAiJsonSafe = parseJsonResponse
|
||
|
||
|
||
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
|
||
|
||
|
||
def buildDocumentIntentPrompt(userInput: str, docCatalog: List[Dict[str, Any]]) -> str:
|
||
"""Build a lightweight prompt for the pre-AI-call that identifies referenced documents."""
|
||
catalogStr = ""
|
||
for doc in docCatalog:
|
||
catalogStr += f'\n- id: {doc.get("id", "")}, title: "{doc.get("title", "")}", summary: "{doc.get("summary", "")[:100]}"'
|
||
|
||
return f"""Analysiere den User-Input und die Dokumentliste.
|
||
Welche Dokumente referenziert der User? Was soll damit passieren?
|
||
|
||
User-Input: "{userInput}"
|
||
|
||
Verfuegbare Dokumente:{catalogStr}
|
||
|
||
Antworte NUR als JSON:
|
||
{{"read": ["doc-id-1"], "update": ["doc-id-2"], "create": ["Titel fuer neues Dokument"], "noDocumentAction": true/false}}
|
||
|
||
- "read": IDs von Dokumenten deren Inhalt der User lesen/besprechen will
|
||
- "update": IDs von Dokumenten die geaendert/angepasst werden sollen
|
||
- "create": Titel fuer neue Dokumente die erstellt werden sollen
|
||
- "noDocumentAction": true wenn kein Dokument-Bezug erkannt wurde
|
||
|
||
Wenn kein Dokument-Bezug: {{"read": [], "update": [], "create": [], "noDocumentAction": true}}"""
|