diff --git a/modules/features/commcoach/serviceCommcoach.py b/modules/features/commcoach/serviceCommcoach.py index 5d9b6f29..d6bd1243 100644 --- a/modules/features/commcoach/serviceCommcoach.py +++ b/modules/features/commcoach/serviceCommcoach.py @@ -524,6 +524,8 @@ class CommcoachService: interface.updateSession(sessionId, { "status": CoachingSessionStatus.COMPLETED.value, "endedAt": getIsoTimestamp(), + "compressedHistorySummary": None, + "compressedHistoryUpToMessageCount": None, }) return session @@ -633,13 +635,15 @@ class CommcoachService: except Exception: pass - # Update session + # Update session - clear compressed history so it never leaks into new sessions sessionUpdates = { "status": CoachingSessionStatus.COMPLETED.value, "endedAt": getIsoTimestamp(), "summary": summary, "durationSeconds": durationSeconds, "messageCount": len(messages), + "compressedHistorySummary": None, + "compressedHistoryUpToMessageCount": None, } if competenceScore is not None: sessionUpdates["competenceScore"] = round(competenceScore, 1) diff --git a/modules/features/teamsbot/datamodelTeamsbot.py b/modules/features/teamsbot/datamodelTeamsbot.py index 4d111b73..bc17642f 100644 --- a/modules/features/teamsbot/datamodelTeamsbot.py +++ b/modules/features/teamsbot/datamodelTeamsbot.py @@ -104,6 +104,7 @@ class TeamsbotTranscript(BaseModel): confidence: float = Field(default=0.0, ge=0.0, le=1.0, description="STT confidence score") language: Optional[str] = Field(default=None, description="Detected language code (e.g., de-DE)") isFinal: bool = Field(default=True, description="Whether this is a final or interim result") + source: Optional[str] = Field(default=None, description="Source: caption, audioCapture, chat, chatHistory, speakerHint") creationDate: Optional[str] = Field(default=None, description="ISO timestamp of record creation") @@ -257,7 +258,10 @@ class TeamsbotConfigUpdateRequest(BaseModel): class TeamsbotCommand(BaseModel): """A structured command the AI can issue to control Teams meeting actions.""" - action: str = Field(description="Command action: toggleTranscript, sendChat, readAloud, changeLanguage, toggleMic, toggleCamera") + action: str = Field( + description="Command action: toggleTranscript, toggleChat, sendChat, readChat, readAloud, " + "changeLanguage, toggleMic, toggleCamera, sendMail, storeDocument" + ) params: Optional[Dict[str, Any]] = Field(default=None, description="Action-specific parameters") diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py index 42065c78..787ee1c8 100644 --- a/modules/features/teamsbot/service.py +++ b/modules/features/teamsbot/service.py @@ -668,6 +668,7 @@ class TeamsbotService: confidence=1.0, language=self.config.language, isFinal=True, + source="chatHistory", ).model_dump() createdTranscript = interface.createTranscript(transcriptData) @@ -724,6 +725,7 @@ class TeamsbotService: confidence=1.0, language=self.config.language, isFinal=isFinal, + source=source, ).model_dump() createdTranscript = interface.createTranscript(transcriptData) @@ -1298,80 +1300,228 @@ class TeamsbotService: websocket: WebSocket, ): """Execute structured commands returned by the AI. - - Each command is sent to the browser bot via WebSocket as a - 'botCommand' message. The bot's TeamsActionsService handles - the actual Teams UI interaction (checking state, toggling, etc.). - """ + Each command is dispatched to a dedicated handler function.""" 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": - enable = params.get("enable", True) - if websocket: - await websocket.send_text(json.dumps({ - "type": "botCommand", - "sessionId": sessionId, - "command": "toggleTranscript", - "params": {"enable": enable}, - })) - + await self._cmdToggleTranscript(sessionId, params, websocket) + elif action == "toggleChat": + await self._cmdToggleChat(sessionId, params, websocket) elif action == "sendChat": - chatText = params.get("text", "") - if chatText and websocket: - await websocket.send_text(json.dumps({ - "type": "sendChatMessage", - "sessionId": sessionId, - "text": chatText, - })) - + await self._cmdSendChat(sessionId, params, websocket) + elif action == "readChat": + await self._cmdReadChat(sessionId, params, voiceInterface, websocket) elif action == "readAloud": - readText = params.get("text", "") - if readText and voiceInterface: - ttsResult = await voiceInterface.textToSpeech( - text=readText, - languageCode=self.config.language, - voiceName=self.config.voiceId, - ) - if ttsResult and isinstance(ttsResult, dict): - audioContent = ttsResult.get("audioContent") - if audioContent and websocket: - await websocket.send_text(json.dumps({ - "type": "playAudio", - "sessionId": sessionId, - "audio": { - "data": base64.b64encode( - audioContent if isinstance(audioContent, bytes) else audioContent.encode() - ).decode(), - "format": "mp3", - }, - })) - + await self._cmdReadAloud(sessionId, params, voiceInterface, websocket) elif action == "changeLanguage": - newLang = params.get("language", "") - if newLang: - self.config = self.config.model_copy(update={"language": newLang}) - logger.info(f"Session {sessionId}: Language changed to '{newLang}'") - await _emitSessionEvent(sessionId, "languageChanged", {"language": newLang}) - + await self._cmdChangeLanguage(sessionId, params) elif action in ("toggleMic", "toggleCamera"): - if websocket: - await websocket.send_text(json.dumps({ - "type": "botCommand", - "sessionId": sessionId, - "command": action, - "params": params, - })) - + await self._cmdToggleMicOrCamera(sessionId, action, params, websocket) + elif action == "sendMail": + await self._cmdSendMail(sessionId, params) + elif action == "storeDocument": + await self._cmdStoreDocument(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(self, 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(self, 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(self, sessionId: str, params: dict, websocket: WebSocket): + """Send a message to the meeting chat.""" + chatText = params.get("text", "") + if chatText and websocket: + await websocket.send_text(json.dumps({ + "type": "sendChatMessage", + "sessionId": sessionId, + "text": chatText, + })) + + async def _cmdReadChat( + self, + sessionId: str, + params: dict, + voiceInterface, + websocket: WebSocket, + ): + """Read chat messages (from DB) with optional fromdatetime/todatetime, then speak or send to chat.""" + from . import interfaceFeatureTeamsbot as interfaceDb + interface = interfaceDb.getInterface(self.currentUser, self.mandateId, self.instanceId) + transcripts = interface.getTranscripts(sessionId) + fromDt = params.get("fromdatetime") or params.get("fromDateTime") + toDt = params.get("todatetime") or params.get("toDateTime") + chatOnly = [t for t in transcripts if t.get("source") in ("chat", "chatHistory")] + if fromDt: + chatOnly = [t for t in chatOnly if (t.get("timestamp") or "") >= fromDt] + if toDt: + chatOnly = [t for t in chatOnly if (t.get("timestamp") or "") <= toDt] + 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: + ttsResult = await voiceInterface.textToSpeech( + text=summary[:2000], + languageCode=self.config.language, + voiceName=self.config.voiceId, + ) + if ttsResult and isinstance(ttsResult, dict) and ttsResult.get("audioContent"): + audioContent = ttsResult["audioContent"] + await websocket.send_text(json.dumps({ + "type": "playAudio", + "sessionId": sessionId, + "audio": { + "data": base64.b64encode( + audioContent if isinstance(audioContent, bytes) else audioContent.encode() + ).decode(), + "format": "mp3", + }, + })) + + async def _cmdReadAloud( + self, + sessionId: str, + params: dict, + voiceInterface, + websocket: WebSocket, + ): + """Read text aloud via TTS and play in meeting.""" + readText = params.get("text", "") + if readText and voiceInterface: + ttsResult = await voiceInterface.textToSpeech( + text=readText, + languageCode=self.config.language, + voiceName=self.config.voiceId, + ) + if ttsResult and isinstance(ttsResult, dict): + audioContent = ttsResult.get("audioContent") + if audioContent and websocket: + await websocket.send_text(json.dumps({ + "type": "playAudio", + "sessionId": sessionId, + "audio": { + "data": base64.b64encode( + audioContent if isinstance(audioContent, bytes) else audioContent.encode() + ).decode(), + "format": "mp3", + }, + })) + + async def _cmdChangeLanguage(self, sessionId: str, params: dict): + """Change bot language.""" + newLang = params.get("language", "") + if newLang: + self.config = self.config.model_copy(update={"language": newLang}) + logger.info(f"Session {sessionId}: Language changed to '{newLang}'") + await _emitSessionEvent(sessionId, "languageChanged", {"language": newLang}) + + async def _cmdToggleMicOrCamera( + self, + 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(self, 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=self.currentUser, + mandate_id=self.mandateId, + feature_instance_id=self.instanceId, + ) + messaging = getService("messaging", ctx) + success = messaging.sendEmailDirect( + recipient=recipient, + subject=subject, + message=message, + userId=str(self.currentUser.id) if self.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(self, 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=self.currentUser, + mandate_id=self.mandateId, + feature_instance_id=self.instanceId, + ) + sharepoint = getService("sharepoint", ctx) + if not sharepoint.setAccessTokenFromConnection(self.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}") + # ========================================================================= # Context Summarization (for long sessions) # =========================================================================