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 <cursoragent@cursor.com>
This commit is contained in:
patrick-motsch 2026-02-15 11:05:58 +01:00
parent 3777839a5c
commit e01c6dcb95
2 changed files with 121 additions and 11 deletions

View file

@ -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)

View file

@ -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