TASK 5: Backend abort handling (cancel previous processMessage tasks) TASK 6: Compression thresholds lowered (25->15, 15->10) TASK 7: Combine pending user messages into single prompt TASK 8: Document handling with pre-AI-call intent detection TASK 9: Granular status events during AI processing Made-with: Cursor
478 lines
19 KiB
Python
478 lines
19 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 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": "<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."""
|
|
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 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
|
|
|
|
Gespräch:
|
|
{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: 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}}"""
|