From e01c6dcb95467a53a2f1717cd40e8ac719cf4247 Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Sun, 15 Feb 2026 11:05:58 +0100
Subject: [PATCH] feat(teamsbot): HTTP fallback for transcripts, debug logging
for WebSocket
- HTTP POST endpoints: /bot/transcript/{sessionId} and /bot/status/{sessionId}
- Fallback for when Azure App Service blocks WebSocket upgrade
- Debug logging [WS-DEBUG] in WebSocket handler for message tracking
- Handle websocket=None in AI response pipeline (HTTP mode)
Co-authored-by: Cursor
---
.../features/teamsbot/routeFeatureTeamsbot.py | 112 +++++++++++++++++-
modules/features/teamsbot/service.py | 20 ++--
2 files changed, 121 insertions(+), 11 deletions(-)
diff --git a/modules/features/teamsbot/routeFeatureTeamsbot.py b/modules/features/teamsbot/routeFeatureTeamsbot.py
index 2abd2cf7..0fbd46cd 100644
--- a/modules/features/teamsbot/routeFeatureTeamsbot.py
+++ b/modules/features/teamsbot/routeFeatureTeamsbot.py
@@ -475,9 +475,114 @@ async def testVoice(
# =========================================================================
-# Browser Bot Communication Endpoints
+# Browser Bot Communication Endpoints (HTTP Fallback + WebSocket)
# =========================================================================
+@router.post("/{instanceId}/bot/transcript/{sessionId}")
+async def postTranscript(
+ request: Request,
+ instanceId: str,
+ sessionId: str,
+):
+ """
+ HTTP POST fallback for transcript delivery when WebSocket is unavailable.
+ Used by the Browser Bot when Azure/proxy blocks WebSocket connections.
+ """
+ body = await request.json()
+ transcript = body.get("transcript", {})
+ speaker = transcript.get("speaker", "Unknown")
+ text = transcript.get("text", "")
+ isFinal = transcript.get("isFinal", True)
+
+ if not text.strip():
+ return {"success": True, "message": "Empty transcript ignored"}
+
+ try:
+ config = _getInstanceConfig(instanceId)
+
+ # Load original user context from session
+ from modules.datamodels.datamodelUam import User
+
+ systemUser = User(id="system", username="system", email="system@poweron.swiss")
+ sessionInterface = interfaceDb.getInterface(systemUser, featureInstanceId=instanceId)
+ session = sessionInterface.getSession(sessionId)
+ mandateId = session.get("mandateId") if session else None
+ startedByUserId = session.get("startedByUserId") if session else None
+
+ rootInterface = getRootInterface()
+ originalUser = rootInterface.getUser(startedByUserId) if startedByUserId else None
+ if not originalUser:
+ originalUser = systemUser
+
+ # Process transcript through the service pipeline
+ from .service import TeamsbotService
+ from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
+
+ service = TeamsbotService(originalUser, mandateId, instanceId, config)
+ interface = interfaceDb.getInterface(originalUser, mandateId=mandateId, featureInstanceId=instanceId)
+ voiceInterface = getVoiceInterface(originalUser, mandateId)
+
+ await service._processTranscript(
+ sessionId=sessionId,
+ speaker=speaker,
+ text=text,
+ isFinal=isFinal,
+ interface=interface,
+ voiceInterface=voiceInterface,
+ websocket=None, # No WebSocket in HTTP mode
+ )
+
+ logger.info(f"HTTP transcript received: session={sessionId}, speaker={speaker}, text={text[:50]}...")
+ return {"success": True}
+
+ except Exception as e:
+ logger.error(f"HTTP transcript error: session={sessionId}, error={e}")
+ return {"success": False, "error": str(e)}
+
+
+@router.post("/{instanceId}/bot/status/{sessionId}")
+async def postBotStatus(
+ request: Request,
+ instanceId: str,
+ sessionId: str,
+):
+ """
+ HTTP POST fallback for bot status updates when WebSocket is unavailable.
+ """
+ body = await request.json()
+ status = body.get("status", "")
+ message = body.get("message")
+
+ try:
+ config = _getInstanceConfig(instanceId)
+
+ from modules.datamodels.datamodelUam import User
+
+ systemUser = User(id="system", username="system", email="system@poweron.swiss")
+ sessionInterface = interfaceDb.getInterface(systemUser, featureInstanceId=instanceId)
+ session = sessionInterface.getSession(sessionId)
+ mandateId = session.get("mandateId") if session else None
+ startedByUserId = session.get("startedByUserId") if session else None
+
+ rootInterface = getRootInterface()
+ originalUser = rootInterface.getUser(startedByUserId) if startedByUserId else None
+ if not originalUser:
+ originalUser = systemUser
+
+ from .service import TeamsbotService
+ service = TeamsbotService(originalUser, mandateId, instanceId, config)
+
+ interface = interfaceDb.getInterface(originalUser, mandateId=mandateId, featureInstanceId=instanceId)
+ await service._handleBotStatus(sessionId, status, message, interface)
+
+ logger.info(f"HTTP status received: session={sessionId}, status={status}")
+ return {"success": True}
+
+ except Exception as e:
+ logger.error(f"HTTP status error: session={sessionId}, error={e}")
+ return {"success": False, "error": str(e)}
+
+
@router.websocket("/{instanceId}/bot/ws/{sessionId}")
async def botWebsocket(
websocket: WebSocket,
@@ -494,10 +599,9 @@ async def botWebsocket(
Gateway sends:
- playAudio: TTS audio for the bot to play in the meeting
"""
+ logger.info(f"Browser Bot WebSocket INCOMING: session={sessionId}, instance={instanceId}")
await websocket.accept()
- logger.info(f"Browser Bot WebSocket connected: session={sessionId}, instance={instanceId}")
-
- # TODO: Validate bot API key from headers/query params
+ logger.info(f"Browser Bot WebSocket ACCEPTED: session={sessionId}, instance={instanceId}")
try:
config = _getInstanceConfig(instanceId)
diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py
index 1241cb44..aedb2f88 100644
--- a/modules/features/teamsbot/service.py
+++ b/modules/features/teamsbot/service.py
@@ -206,21 +206,25 @@ class TeamsbotService:
interface = interfaceDb.getInterface(self.currentUser, self.mandateId, self.instanceId)
voiceInterface = getVoiceInterface(self.currentUser, self.mandateId)
- logger.info(f"Browser Bot WebSocket connected for session {sessionId}")
+ logger.info(f"[WS-DEBUG] WebSocket handler started for session {sessionId}")
try:
+ msgCount = 0
while True:
data = await websocket.receive()
+ msgCount += 1
if "text" not in data:
+ logger.debug(f"[WS-DEBUG] session={sessionId} msg #{msgCount}: non-text data received (keys: {list(data.keys())})")
continue
message = json.loads(data["text"])
msgType = message.get("type")
+ logger.info(f"[WS-DEBUG] session={sessionId} msg #{msgCount}: type={msgType}")
if msgType == "transcript":
- # Process transcript from captions
transcript = message.get("transcript", {})
+ logger.info(f"[WS-DEBUG] Transcript received: speaker={transcript.get('speaker')}, text={transcript.get('text', '')[:60]}...")
await self._processTranscript(
sessionId=sessionId,
speaker=transcript.get("speaker", "Unknown"),
@@ -232,9 +236,9 @@ class TeamsbotService:
)
elif msgType == "status":
- # Handle status updates from bot
status = message.get("status")
errorMessage = message.get("message")
+ logger.info(f"[WS-DEBUG] Status received: status={status}, message={errorMessage}")
await self._handleBotStatus(sessionId, status, errorMessage, interface)
elif msgType == "ping":
@@ -242,9 +246,9 @@ class TeamsbotService:
except Exception as e:
if "disconnect" not in str(e).lower():
- logger.error(f"Browser Bot WebSocket error for session {sessionId}: {e}")
+ logger.error(f"[WS-DEBUG] WebSocket error for session {sessionId}: {type(e).__name__}: {e}")
- logger.info(f"Browser Bot WebSocket disconnected for session {sessionId}")
+ logger.info(f"[WS-DEBUG] WebSocket handler ended for session {sessionId} after {msgCount} messages")
async def _handleBotStatus(
self,
@@ -488,13 +492,15 @@ class TeamsbotService:
if ttsResult and isinstance(ttsResult, dict):
audioContent = ttsResult.get("audioContent")
- if audioContent:
- # Send TTS audio to bridge
+ if audioContent and websocket:
+ # Send TTS audio to bot via WebSocket
await websocket.send_text(json.dumps({
"type": "tts_audio",
"data": base64.b64encode(audioContent if isinstance(audioContent, bytes) else audioContent.encode()).decode(),
"format": "mp3",
}))
+ elif audioContent and not websocket:
+ logger.info(f"TTS audio generated for session {sessionId} (HTTP mode - no WebSocket for playback)")
except Exception as ttsErr:
logger.warning(f"TTS failed for session {sessionId}: {ttsErr}")
responseType = TeamsbotResponseType.CHAT # Fallback to chat only