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