From cc86b144ac45dc23da9734fa846c94455cbbfcc8 Mon Sep 17 00:00:00 2001 From: patrick-motsch Date: Sun, 15 Feb 2026 22:52:02 +0100 Subject: [PATCH] fix(teamsbot): filter bot's own captions from AI trigger, mark bot responses in context to prevent repetition Co-authored-by: Cursor --- modules/features/teamsbot/service.py | 44 ++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py index 47a036fc..ad52e0c5 100644 --- a/modules/features/teamsbot/service.py +++ b/modules/features/teamsbot/service.py @@ -307,6 +307,11 @@ class TeamsbotService: if not text: 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). + isBotSpeaker = self._isBotSpeaker(speaker) + # Store transcript segment transcriptData = TeamsbotTranscript( sessionId=sessionId, @@ -346,6 +351,11 @@ class TeamsbotService: count = session.get("transcriptSegmentCount", 0) + 1 interface.updateSession(sessionId, {"transcriptSegmentCount": count}) + # Skip AI analysis for bot's own speech (prevents feedback loop) + if isBotSpeaker: + logger.debug(f"Session {sessionId}: Skipping AI trigger for bot's own speech: [{speaker}] {text[:60]}...") + return + # Check if AI analysis should be triggered (only for final transcripts) if not isFinal: return @@ -364,6 +374,32 @@ class TeamsbotService: logger.info(f"Session {sessionId}: Triggering AI analysis (buffer: {len(self._contextBuffer)} segments)") await self._analyzeAndRespond(sessionId, interface, voiceInterface, websocket, createdTranscript) + def _isBotSpeaker(self, speaker: str) -> bool: + """Check if a transcript speaker is the bot itself. + + Teams captions show the bot as e.g. "Shelly Miller (Unverified)" or + "Nyla Larsson" depending on auth/anonymous join. We match against: + - The configured bot name (e.g. "Shelly Miller") + - The bot account display name if authenticated + """ + if not speaker: + return False + + speakerLower = speaker.lower().strip() + + # Match against configured bot name + botName = self.config.botName.lower().strip() + if botName and botName in speakerLower: + return True + + # Match against bot account email prefix (e.g. "nyla.larsson" from "nyla.larsson@poweron.swiss") + if self.config.botAccountEmail: + emailPrefix = self.config.botAccountEmail.split("@")[0].lower().replace(".", " ") + if emailPrefix in speakerLower: + return True + + return False + def _shouldTriggerAnalysis(self, transcriptText: str) -> bool: """ Decide whether to trigger AI analysis based on the latest transcript. @@ -405,12 +441,16 @@ class TeamsbotService: """Run SPEECH_TEAMS AI analysis and respond if needed.""" self._lastAiCallTime = time.time() - # Build transcript context from buffer + # Build transcript context from buffer. + # Mark bot's own utterances so the AI knows what it already said. contextLines = [] for segment in self._contextBuffer: speaker = segment.get("speaker", "Unknown") text = segment.get("text", "") - contextLines.append(f"[{speaker}]: {text}") + if self._isBotSpeaker(speaker): + contextLines.append(f"[YOU ({self.config.botName})]: {text}") + else: + contextLines.append(f"[{speaker}]: {text}") transcriptContext = f"BOT_NAME:{self.config.botName}\n" + "\n".join(contextLines)