From c7d1642f61d6c9bb4fe1c0b1f73101ec7e02ebd0 Mon Sep 17 00:00:00 2001 From: patrick-motsch Date: Mon, 16 Feb 2026 09:02:43 +0100 Subject: [PATCH 1/2] fix(teamsbot): improve AI prompt - less floskel, stricter response rules, no repetition, language switching support Co-authored-by: Cursor --- modules/services/serviceAi/mainServiceAi.py | 37 +++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/modules/services/serviceAi/mainServiceAi.py b/modules/services/serviceAi/mainServiceAi.py index a57054a4..dd4f000e 100644 --- a/modules/services/serviceAi/mainServiceAi.py +++ b/modules/services/serviceAi/mainServiceAi.py @@ -331,24 +331,33 @@ class AiService: basePrompt = f"""Du bist "{botName}", ein AI-Teilnehmer in einem Microsoft Teams Meeting. Analysiere das folgende Transkript und entscheide, ob du antworten sollst. -WICHTIG: Das Transkript kann in verschiedenen Sprachen sein (Deutsch, Englisch, etc.) weil die Spracherkennung die Sprache nicht immer korrekt erkennt. Antworte immer in der Sprache, in der du angesprochen wirst. +SPRACHE: Das Transkript kann in verschiedenen Sprachen sein. Antworte immer in der Sprache des letzten Sprechers der dich angesprochen hat. Wenn jemand sagt "let's talk German" oder "sprich deutsch", wechsle die Sprache entsprechend. -REGEL 1 (HOECHSTE PRIORITAET - IMMER ANTWORTEN): -Wenn dein Name "{botName}" oder eine aehnliche Anrede (Shelly, shelly, SHELLY, etc.) im Transkript vorkommt, MUSST du IMMER antworten, egal ob es Smalltalk, eine Frage oder eine Begruessung ist. Du bist ein freundlicher Meeting-Teilnehmer. +WANN ANTWORTEN: -REGEL 2 (ANTWORTEN wenn sinnvoll): -- Eine explizite Frage an die Runde gestellt wird, die du sachlich beantworten kannst -- Du einen wertvollen Beitrag zur Diskussion hast +REGEL 1 (HOECHSTE PRIORITAET - NUR wenn direkt angesprochen): +Antworte NUR wenn dein Name "{botName}" (oder Varianten wie Shelly, shelly) DIREKT im aktuellsten Transkript-Segment vorkommt. +Beispiele wo du antworten MUSST: "Shelly, was denkst du?", "Hey Shelly", "Shelly please introduce yourself" +Beispiele wo du NICHT antworten darfst: Jemand spricht ueber ein Thema ohne dich zu adressieren. -REGEL 3 (NICHT ANTWORTEN): -- Die Teilnehmer normal miteinander sprechen OHNE deinen Namen zu nennen -- Die Diskussion keinen Input von dir benoetigt und du nicht angesprochen wirst +REGEL 2 (NUR bei direkter Frage an dich): +Wenn jemand eine Frage DIREKT AN DICH stellt (mit deinem Namen), beantworte sie. +Antworte NICHT auf allgemeine Fragen in der Runde, die nicht an dich gerichtet sind. -ANTWORT-STIL: -- Halte dich kurz und praezise (max 2-3 Saetze) -- Sei freundlich, professionell und natuerlich -- Bei Begruessung: Antworte herzlich zurueck -- Bei Fragen: Beantworte sachlich und konkret""" +REGEL 3 (NICHT ANTWORTEN - sehr wichtig): +- Wenn Teilnehmer miteinander sprechen ohne dich zu adressieren: NICHT antworten +- Wenn die Konversation nicht an dich gerichtet ist: NICHT antworten +- Wenn du bereits auf dieselbe Frage geantwortet hast: NICHT nochmal antworten +- Wenn du nicht sicher bist ob du gemeint bist: NICHT antworten +- Im Zweifel: shouldRespond = false + +ANTWORT-STIL (wenn du antwortest): +- Direkt und konkret antworten, KEINE Floskeln +- NICHT mit "Hallo [Name]" anfangen wenn du bereits begruessst hast +- NICHT "Ich bin {botName} und ich bin hier um zu helfen" wiederholen +- NICHT frueheres wiederholen das du schon gesagt hast +- Max 1-2 Saetze, praezise auf den Punkt +- Sieh dir an was du (markiert als [YOU]) bereits gesagt hast und wiederhole es NICHT""" # Append user-configured instructions if provided if userSystemPrompt and userSystemPrompt.strip(): From 4186ec6188b28662ba698d5825d7877ab5497f1e Mon Sep 17 00:00:00 2001 From: patrick-motsch Date: Mon, 16 Feb 2026 09:29:03 +0100 Subject: [PATCH 2/2] feat(teamsbot): stop command detection, session context for AI, context summarization for long sessions Co-authored-by: Cursor --- .../features/teamsbot/datamodelTeamsbot.py | 2 + .../features/teamsbot/routeFeatureTeamsbot.py | 1 + modules/features/teamsbot/service.py | 92 ++++++++++++++++++- 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/modules/features/teamsbot/datamodelTeamsbot.py b/modules/features/teamsbot/datamodelTeamsbot.py index b5523b5a..e4803097 100644 --- a/modules/features/teamsbot/datamodelTeamsbot.py +++ b/modules/features/teamsbot/datamodelTeamsbot.py @@ -78,6 +78,7 @@ class TeamsbotSession(BaseModel): startedByUserId: str = Field(description="User ID who started the session") bridgeSessionId: Optional[str] = Field(default=None, description="Session ID on the .NET Media Bridge") meetingChatId: Optional[str] = Field(default=None, description="Teams meeting chat ID for Graph API messages") + sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge provided by the user for this session") summary: Optional[str] = Field(default=None, description="AI-generated meeting summary (after session ends)") errorMessage: Optional[str] = Field(default=None, description="Error message if status is ERROR") transcriptSegmentCount: int = Field(default=0, description="Number of transcript segments in this session") @@ -200,6 +201,7 @@ class TeamsbotStartSessionRequest(BaseModel): backgroundImageUrl: Optional[str] = Field(default=None, description="Override background image for this session") connectionId: Optional[str] = Field(default=None, description="Microsoft connection ID for Graph API access") joinMode: Optional[TeamsbotJoinMode] = Field(default=None, description="How the bot joins: systemBot, anonymous, or userAccount. Defaults to systemBot if credentials configured, else anonymous.") + sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge to provide to the bot for this session (e.g. meeting agenda, documents, background info)") class TeamsbotSessionResponse(BaseModel): diff --git a/modules/features/teamsbot/routeFeatureTeamsbot.py b/modules/features/teamsbot/routeFeatureTeamsbot.py index 70aba37f..575094bf 100644 --- a/modules/features/teamsbot/routeFeatureTeamsbot.py +++ b/modules/features/teamsbot/routeFeatureTeamsbot.py @@ -178,6 +178,7 @@ async def startSession( meetingLink=cleanMeetingUrl, botName=body.botName or config.botName, backgroundImageUrl=body.backgroundImageUrl or config.backgroundImageUrl, + sessionContext=body.sessionContext, status=TeamsbotSessionStatus.PENDING, startedByUserId=str(context.user.id), ).model_dump() diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py index 6cd1fa1d..8b246f1f 100644 --- a/modules/features/teamsbot/service.py +++ b/modules/features/teamsbot/service.py @@ -77,6 +77,8 @@ class TeamsbotService: # State self._lastAiCallTime: float = 0.0 self._contextBuffer: List[Dict[str, Any]] = [] + self._sessionContext: Optional[str] = None # User-provided background context + self._contextSummary: Optional[str] = None # AI-generated summary of long context # ========================================================================= # Session Lifecycle @@ -210,6 +212,13 @@ class TeamsbotService: interface = interfaceDb.getInterface(self.currentUser, self.mandateId, self.instanceId) voiceInterface = getVoiceInterface(self.currentUser, self.mandateId) + # Load session context (user-provided background knowledge) + session = interface.getSession(sessionId) + if session: + self._sessionContext = session.get("sessionContext") + if self._sessionContext: + logger.info(f"Session {sessionId}: Loaded session context ({len(self._sessionContext)} chars)") + logger.info(f"[WS-DEBUG] WebSocket handler started for session {sessionId}") try: @@ -323,6 +332,23 @@ class TeamsbotService: if not text: return + # Check for STOP command: " stop" or " STOP" + # This immediately stops the bot from speaking and clears the audio queue. + botNameLower = self.config.botName.lower() + textLower = text.lower() + if botNameLower in textLower and "stop" in textLower: + logger.info(f"Session {sessionId}: STOP command detected: [{speaker}] {text[:60]}") + if websocket: + try: + await websocket.send_text(json.dumps({ + "type": "stopAudio", + "sessionId": sessionId, + })) + except Exception as stopErr: + logger.warning(f"Failed to send stop command: {stopErr}") + # Don't trigger AI analysis for stop commands + return + # Filter out the bot's own speech from AI triggering. # The bot hears itself via captions — these should be stored in the # transcript for the record, but must NOT trigger AI analysis (feedback loop). @@ -351,6 +377,10 @@ class TeamsbotService: # Keep only last N segments maxSegments = self.config.contextWindowSegments if len(self._contextBuffer) > maxSegments: + # When buffer overflows, summarize the older half to preserve context + # without losing information. The summary replaces the old segments. + if not self._contextSummary and len(self._contextBuffer) > maxSegments * 1.5: + asyncio.create_task(self._summarizeContextBuffer(sessionId)) self._contextBuffer = self._contextBuffer[-maxSegments:] # Emit SSE event for live transcript @@ -473,7 +503,17 @@ class TeamsbotService: else: contextLines.append(f"[{speaker}]: {text}") - transcriptContext = f"BOT_NAME:{self.config.botName}\n" + "\n".join(contextLines) + # Include session context if provided by the user at session start + sessionContextStr = "" + if self._sessionContext: + sessionContextStr = f"\nSESSION_CONTEXT (background knowledge provided by the user):\n{self._sessionContext}\n" + + # Include summary of earlier conversation if available + summaryStr = "" + if self._contextSummary: + summaryStr = f"\nEARLIER_CONVERSATION_SUMMARY:\n{self._contextSummary}\n" + + transcriptContext = f"BOT_NAME:{self.config.botName}{sessionContextStr}{summaryStr}\nRECENT_TRANSCRIPT:\n" + "\n".join(contextLines) # Call SPEECH_TEAMS try: @@ -633,6 +673,56 @@ class TeamsbotService: logger.error(f"SPEECH_TEAMS analysis failed for session {sessionId}: {type(e).__name__}: {e}", exc_info=True) await _emitSessionEvent(sessionId, "error", {"message": f"AI analysis failed: {type(e).__name__}: {str(e)}"}) + # ========================================================================= + # Context Summarization (for long sessions) + # ========================================================================= + + async def _summarizeContextBuffer(self, sessionId: str): + """Summarize the older part of the context buffer to preserve information + without exceeding the context window. This runs in the background.""" + try: + if self._contextSummary: + return # Already summarized recently + + # Take the older half of the buffer for summarization + halfPoint = len(self._contextBuffer) // 2 + oldSegments = self._contextBuffer[:halfPoint] + + if len(oldSegments) < 10: + return # Not enough to summarize + + # Build text to summarize + lines = [] + for seg in oldSegments: + speaker = seg.get("speaker", "Unknown") + text = seg.get("text", "") + lines.append(f"[{speaker}]: {text}") + textToSummarize = "\n".join(lines) + + from modules.services.serviceAi.mainServiceAi import AiService + from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum + + serviceContext = _ServiceContext(self.currentUser, self.mandateId, self.instanceId) + aiService = AiService(serviceCenter=serviceContext) + await aiService.ensureAiObjectsInitialized() + + request = AiCallRequest( + prompt="Fasse das folgende Meeting-Transkript in 3-5 Saetzen zusammen. Nenne die wichtigsten Themen, Entscheidungen und offene Fragen. Antworte NUR mit der Zusammenfassung, keine Erklaerungen.", + context=textToSummarize, + options=AiCallOptions( + operationType=OperationTypeEnum.DATA_ANALYSE, + priority=PriorityEnum.SPEED, + ) + ) + + response = await aiService.callAi(request) + if response and response.errorCount == 0: + self._contextSummary = response.content.strip() + logger.info(f"Session {sessionId}: Context summarized ({len(oldSegments)} segments -> {len(self._contextSummary)} chars)") + + except Exception as e: + logger.warning(f"Context summarization failed for session {sessionId}: {e}") + # ========================================================================= # Meeting Summary # =========================================================================