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)