From 05f1c9882586b81033c8d2a728f506e2f2abbf47 Mon Sep 17 00:00:00 2001
From: patrick-motsch
Date: Fri, 6 Mar 2026 22:55:28 +0100
Subject: [PATCH] feat: teamsbot command refactoring, commcoach session cleanup
Made-with: Cursor
---
.../features/commcoach/serviceCommcoach.py | 6 +-
.../features/teamsbot/datamodelTeamsbot.py | 6 +-
modules/features/teamsbot/service.py | 268 ++++++++++++++----
3 files changed, 219 insertions(+), 61 deletions(-)
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)
# =========================================================================