# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Teamsbot Service — AI command execution logic. Extracted from service.py. All functions accept `service` (a TeamsbotService instance) as the first parameter so the class can delegate to them. """ import logging import json import asyncio from datetime import datetime, timezone from typing import List from fastapi import WebSocket from modules.shared.timeUtils import getUtcTimestamp from .datamodelTeamsbot import ( TeamsbotTranscript, TeamsbotCommand, ) logger = logging.getLogger(__name__) async def _executeCommands( service, sessionId: str, commands: List[TeamsbotCommand], voiceInterface, websocket: WebSocket, ): """Execute structured commands returned by the AI.""" for cmd in commands: action = cmd.action params = cmd.params or {} logger.info(f"Session {sessionId}: Executing command '{action}' with params {params}") try: if action == "toggleTranscript": await _cmdToggleTranscript(service, sessionId, params, websocket) elif action == "toggleChat": await _cmdToggleChat(service, sessionId, params, websocket) elif action == "sendChat": await _cmdSendChat(service, sessionId, params, websocket) elif action == "readChat": await _cmdReadChat(service, sessionId, params, voiceInterface, websocket) elif action == "readAloud": await _cmdReadAloud(service, sessionId, params, voiceInterface, websocket) elif action == "changeLanguage": await _cmdChangeLanguage(service, sessionId, params) elif action in ("toggleMic", "toggleCamera"): await _cmdToggleMicOrCamera(service, sessionId, action, params, websocket) elif action == "sendMail": await _cmdSendMail(service, sessionId, params) elif action == "storeDocument": await _cmdStoreDocument(service, sessionId, params) else: logger.warning(f"Session {sessionId}: Unknown command '{action}'") except Exception as cmdErr: logger.warning(f"Session {sessionId}: Command '{action}' failed: {cmdErr}") async def _cmdToggleTranscript(service, sessionId: str, params: dict, websocket: WebSocket): """Caption on/off - toggle Teams live transcript capture.""" enable = params.get("enable", True) if websocket: await websocket.send_text(json.dumps({ "type": "botCommand", "sessionId": sessionId, "command": "toggleTranscript", "params": {"enable": enable}, })) async def _cmdToggleChat(service, sessionId: str, params: dict, websocket: WebSocket): """Chat on/off - enable/disable meeting chat monitoring.""" enable = params.get("enable", True) if websocket: await websocket.send_text(json.dumps({ "type": "botCommand", "sessionId": sessionId, "command": "toggleChat", "params": {"enable": enable}, })) async def _cmdSendChat(service, sessionId: str, params: dict, websocket: WebSocket): """Send a message to the meeting chat and record it in transcript/SSE.""" from .service import _emitSessionEvent chatText = params.get("text", "") if not chatText: return if websocket: await websocket.send_text(json.dumps({ "type": "sendChatMessage", "sessionId": sessionId, "text": chatText, })) logger.info(f"Chat command sent for session {sessionId}") from . import interfaceFeatureTeamsbot as interfaceDb interface = interfaceDb.getInterface(service.currentUser, service.mandateId, service.instanceId) transcriptData = TeamsbotTranscript( sessionId=sessionId, speaker=service.config.botName, text=chatText, timestamp=getUtcTimestamp(), confidence=1.0, language=service.config.language, isFinal=True, source="chat", ).model_dump() createdTranscript = interface.createTranscript(transcriptData) import time service._contextBuffer.append({ "speaker": service.config.botName, "text": chatText, "timestamp": getUtcTimestamp(), "source": "chat", }) service._lastTranscriptSpeaker = service.config.botName service._lastTranscriptText = chatText service._lastTranscriptId = createdTranscript.get("id") service._lastBotResponseText = chatText.strip().lower() service._lastBotResponseTs = time.time() await _emitSessionEvent(sessionId, "transcript", { "id": createdTranscript.get("id"), "speaker": service.config.botName, "text": chatText, "confidence": 1.0, "timestamp": getUtcTimestamp(), "isContinuation": False, "source": "chat", "speakerResolvedFromHint": False, }) async def _cmdReadChat( service, sessionId: str, params: dict, voiceInterface, websocket: WebSocket, ): """Read chat messages (from DB) with optional fromdatetime/todatetime, then speak or send to chat.""" from .service import _speakTextChunked from .serviceConversation import _summarizeForVoice from . import interfaceFeatureTeamsbot as interfaceDb interface = interfaceDb.getInterface(service.currentUser, service.mandateId, service.instanceId) transcripts = interface.getTranscripts(sessionId) fromDtRaw = params.get("fromdatetime") or params.get("fromDateTime") toDtRaw = params.get("todatetime") or params.get("toDateTime") fromTs = datetime.fromisoformat(fromDtRaw).replace(tzinfo=timezone.utc).timestamp() if fromDtRaw else None toTs = datetime.fromisoformat(toDtRaw).replace(tzinfo=timezone.utc).timestamp() if toDtRaw else None chatOnly = [t for t in transcripts if t.get("source") in ("chat", "chatHistory")] if fromTs is not None: chatOnly = [t for t in chatOnly if (t.get("timestamp") or 0) >= fromTs] if toTs is not None: chatOnly = [t for t in chatOnly if (t.get("timestamp") or 0) <= toTs] summary = "\n".join(f"[{t.get('speaker', '?')}]: {t.get('text', '')}" for t in chatOnly[-20:]) if not summary: summary = "Keine Chat-Nachrichten im angegebenen Zeitraum." if voiceInterface and websocket: spokenSummary = await _summarizeForVoice(service, sessionId, summary[:2000]) cancelHook = service._makeAnswerCancelHook() async with service._meetingTtsLock: await _speakTextChunked( websocket=websocket, voiceInterface=voiceInterface, sessionId=sessionId, voiceText=spokenSummary, languageCode=service.config.language, voiceName=service.config.voiceId, isCancelled=cancelHook, ) async def _cmdReadAloud( service, sessionId: str, params: dict, voiceInterface, websocket: WebSocket, ): """Read text aloud via TTS and play in meeting.""" from .service import _speakTextChunked, _voiceFriendlyMeetingText readText = params.get("text", "") if readText and voiceInterface and websocket: cancelHook = service._makeAnswerCancelHook() async with service._meetingTtsLock: await _speakTextChunked( websocket=websocket, voiceInterface=voiceInterface, sessionId=sessionId, voiceText=_voiceFriendlyMeetingText(readText), languageCode=service.config.language, voiceName=service.config.voiceId, isCancelled=cancelHook, ) async def _cmdChangeLanguage(service, sessionId: str, params: dict): """Change bot language.""" from .service import _emitSessionEvent newLang = params.get("language", "") if newLang: service.config = service.config.model_copy(update={"language": newLang}) logger.info(f"Session {sessionId}: Language changed to '{newLang}'") await _emitSessionEvent(sessionId, "languageChanged", {"language": newLang}) async def _cmdToggleMicOrCamera( service, sessionId: str, action: str, params: dict, websocket: WebSocket, ): """Toggle mic or camera in the meeting.""" if websocket: await websocket.send_text(json.dumps({ "type": "botCommand", "sessionId": sessionId, "command": action, "params": params, })) async def _cmdSendMail(service, sessionId: str, params: dict): """Send email via Service Center MessagingService.""" recipient = params.get("recipient") or params.get("to", "") subject = params.get("subject", "") message = params.get("message") or params.get("body", "") if not recipient or not subject: logger.warning(f"Session {sessionId}: sendMail requires recipient and subject") return try: from modules.serviceCenter import ServiceCenterContext, getService ctx = ServiceCenterContext( user=service.currentUser, mandateId=service.mandateId, featureInstanceId=service.instanceId, ) messaging = getService("messaging", ctx) success = messaging.sendEmailDirect( recipient=recipient, subject=subject, message=message, userId=str(service.currentUser.id) if service.currentUser else None, ) if success: logger.info(f"Session {sessionId}: Email sent to {recipient}") else: logger.warning(f"Session {sessionId}: Email send failed for {recipient}") except Exception as e: logger.warning(f"Session {sessionId}: sendMail failed: {e}") async def _cmdStoreDocument(service, sessionId: str, params: dict): """Store document via Service Center SharepointService.""" sitePath = params.get("sitePath") or params.get("site", "") folderPath = params.get("folderPath") or params.get("folder", "") fileName = params.get("fileName", "document.txt") content = params.get("content", "") if isinstance(content, str): content = content.encode("utf-8") if not sitePath or not folderPath: logger.warning(f"Session {sessionId}: storeDocument requires sitePath and folderPath") return try: from modules.serviceCenter import ServiceCenterContext, getService ctx = ServiceCenterContext( user=service.currentUser, mandateId=service.mandateId, featureInstanceId=service.instanceId, ) sharepoint = getService("sharepoint", ctx) if not sharepoint.setAccessTokenFromConnection(service.currentUser): logger.warning(f"Session {sessionId}: SharePoint connection not configured") return site = await sharepoint.getSiteByStandardPath(sitePath) if not site: logger.warning(f"Session {sessionId}: SharePoint site not found: {sitePath}") return result = await sharepoint.uploadFile( siteId=site["id"], folderPath=folderPath, fileName=fileName, content=content, ) if "error" in result: logger.warning(f"Session {sessionId}: storeDocument failed: {result['error']}") else: logger.info(f"Session {sessionId}: Document stored: {fileName}") except Exception as e: logger.warning(f"Session {sessionId}: storeDocument failed: {e}")