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:
parent
3777839a5c
commit
e01c6dcb95
2 changed files with 121 additions and 11 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue