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}")
|
@router.websocket("/{instanceId}/bot/ws/{sessionId}")
|
||||||
async def botWebsocket(
|
async def botWebsocket(
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
|
|
@ -494,10 +599,9 @@ async def botWebsocket(
|
||||||
Gateway sends:
|
Gateway sends:
|
||||||
- playAudio: TTS audio for the bot to play in the meeting
|
- 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()
|
await websocket.accept()
|
||||||
logger.info(f"Browser Bot WebSocket connected: session={sessionId}, instance={instanceId}")
|
logger.info(f"Browser Bot WebSocket ACCEPTED: session={sessionId}, instance={instanceId}")
|
||||||
|
|
||||||
# TODO: Validate bot API key from headers/query params
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = _getInstanceConfig(instanceId)
|
config = _getInstanceConfig(instanceId)
|
||||||
|
|
|
||||||
|
|
@ -206,21 +206,25 @@ class TeamsbotService:
|
||||||
interface = interfaceDb.getInterface(self.currentUser, self.mandateId, self.instanceId)
|
interface = interfaceDb.getInterface(self.currentUser, self.mandateId, self.instanceId)
|
||||||
voiceInterface = getVoiceInterface(self.currentUser, self.mandateId)
|
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:
|
try:
|
||||||
|
msgCount = 0
|
||||||
while True:
|
while True:
|
||||||
data = await websocket.receive()
|
data = await websocket.receive()
|
||||||
|
msgCount += 1
|
||||||
|
|
||||||
if "text" not in data:
|
if "text" not in data:
|
||||||
|
logger.debug(f"[WS-DEBUG] session={sessionId} msg #{msgCount}: non-text data received (keys: {list(data.keys())})")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
message = json.loads(data["text"])
|
message = json.loads(data["text"])
|
||||||
msgType = message.get("type")
|
msgType = message.get("type")
|
||||||
|
logger.info(f"[WS-DEBUG] session={sessionId} msg #{msgCount}: type={msgType}")
|
||||||
|
|
||||||
if msgType == "transcript":
|
if msgType == "transcript":
|
||||||
# Process transcript from captions
|
|
||||||
transcript = message.get("transcript", {})
|
transcript = message.get("transcript", {})
|
||||||
|
logger.info(f"[WS-DEBUG] Transcript received: speaker={transcript.get('speaker')}, text={transcript.get('text', '')[:60]}...")
|
||||||
await self._processTranscript(
|
await self._processTranscript(
|
||||||
sessionId=sessionId,
|
sessionId=sessionId,
|
||||||
speaker=transcript.get("speaker", "Unknown"),
|
speaker=transcript.get("speaker", "Unknown"),
|
||||||
|
|
@ -232,9 +236,9 @@ class TeamsbotService:
|
||||||
)
|
)
|
||||||
|
|
||||||
elif msgType == "status":
|
elif msgType == "status":
|
||||||
# Handle status updates from bot
|
|
||||||
status = message.get("status")
|
status = message.get("status")
|
||||||
errorMessage = message.get("message")
|
errorMessage = message.get("message")
|
||||||
|
logger.info(f"[WS-DEBUG] Status received: status={status}, message={errorMessage}")
|
||||||
await self._handleBotStatus(sessionId, status, errorMessage, interface)
|
await self._handleBotStatus(sessionId, status, errorMessage, interface)
|
||||||
|
|
||||||
elif msgType == "ping":
|
elif msgType == "ping":
|
||||||
|
|
@ -242,9 +246,9 @@ class TeamsbotService:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if "disconnect" not in str(e).lower():
|
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(
|
async def _handleBotStatus(
|
||||||
self,
|
self,
|
||||||
|
|
@ -488,13 +492,15 @@ class TeamsbotService:
|
||||||
|
|
||||||
if ttsResult and isinstance(ttsResult, dict):
|
if ttsResult and isinstance(ttsResult, dict):
|
||||||
audioContent = ttsResult.get("audioContent")
|
audioContent = ttsResult.get("audioContent")
|
||||||
if audioContent:
|
if audioContent and websocket:
|
||||||
# Send TTS audio to bridge
|
# Send TTS audio to bot via WebSocket
|
||||||
await websocket.send_text(json.dumps({
|
await websocket.send_text(json.dumps({
|
||||||
"type": "tts_audio",
|
"type": "tts_audio",
|
||||||
"data": base64.b64encode(audioContent if isinstance(audioContent, bytes) else audioContent.encode()).decode(),
|
"data": base64.b64encode(audioContent if isinstance(audioContent, bytes) else audioContent.encode()).decode(),
|
||||||
"format": "mp3",
|
"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:
|
except Exception as ttsErr:
|
||||||
logger.warning(f"TTS failed for session {sessionId}: {ttsErr}")
|
logger.warning(f"TTS failed for session {sessionId}: {ttsErr}")
|
||||||
responseType = TeamsbotResponseType.CHAT # Fallback to chat only
|
responseType = TeamsbotResponseType.CHAT # Fallback to chat only
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue