feat: teamsbot command refactoring, commcoach session cleanup

Made-with: Cursor
This commit is contained in:
patrick-motsch 2026-03-06 22:55:28 +01:00
parent f4fb4637ea
commit 05f1c98825
3 changed files with 219 additions and 61 deletions

View file

@ -524,6 +524,8 @@ class CommcoachService:
interface.updateSession(sessionId, { interface.updateSession(sessionId, {
"status": CoachingSessionStatus.COMPLETED.value, "status": CoachingSessionStatus.COMPLETED.value,
"endedAt": getIsoTimestamp(), "endedAt": getIsoTimestamp(),
"compressedHistorySummary": None,
"compressedHistoryUpToMessageCount": None,
}) })
return session return session
@ -633,13 +635,15 @@ class CommcoachService:
except Exception: except Exception:
pass pass
# Update session # Update session - clear compressed history so it never leaks into new sessions
sessionUpdates = { sessionUpdates = {
"status": CoachingSessionStatus.COMPLETED.value, "status": CoachingSessionStatus.COMPLETED.value,
"endedAt": getIsoTimestamp(), "endedAt": getIsoTimestamp(),
"summary": summary, "summary": summary,
"durationSeconds": durationSeconds, "durationSeconds": durationSeconds,
"messageCount": len(messages), "messageCount": len(messages),
"compressedHistorySummary": None,
"compressedHistoryUpToMessageCount": None,
} }
if competenceScore is not None: if competenceScore is not None:
sessionUpdates["competenceScore"] = round(competenceScore, 1) sessionUpdates["competenceScore"] = round(competenceScore, 1)

View file

@ -104,6 +104,7 @@ class TeamsbotTranscript(BaseModel):
confidence: float = Field(default=0.0, ge=0.0, le=1.0, description="STT confidence score") 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)") 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") 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") creationDate: Optional[str] = Field(default=None, description="ISO timestamp of record creation")
@ -257,7 +258,10 @@ class TeamsbotConfigUpdateRequest(BaseModel):
class TeamsbotCommand(BaseModel): class TeamsbotCommand(BaseModel):
"""A structured command the AI can issue to control Teams meeting actions.""" """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") params: Optional[Dict[str, Any]] = Field(default=None, description="Action-specific parameters")

View file

@ -668,6 +668,7 @@ class TeamsbotService:
confidence=1.0, confidence=1.0,
language=self.config.language, language=self.config.language,
isFinal=True, isFinal=True,
source="chatHistory",
).model_dump() ).model_dump()
createdTranscript = interface.createTranscript(transcriptData) createdTranscript = interface.createTranscript(transcriptData)
@ -724,6 +725,7 @@ class TeamsbotService:
confidence=1.0, confidence=1.0,
language=self.config.language, language=self.config.language,
isFinal=isFinal, isFinal=isFinal,
source=source,
).model_dump() ).model_dump()
createdTranscript = interface.createTranscript(transcriptData) createdTranscript = interface.createTranscript(transcriptData)
@ -1298,80 +1300,228 @@ class TeamsbotService:
websocket: WebSocket, websocket: WebSocket,
): ):
"""Execute structured commands returned by the AI. """Execute structured commands returned by the AI.
Each command is dispatched to a dedicated handler function."""
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.).
"""
for cmd in commands: for cmd in commands:
action = cmd.action action = cmd.action
params = cmd.params or {} params = cmd.params or {}
logger.info(f"Session {sessionId}: Executing command '{action}' with params {params}") logger.info(f"Session {sessionId}: Executing command '{action}' with params {params}")
try: try:
if action == "toggleTranscript": if action == "toggleTranscript":
enable = params.get("enable", True) await self._cmdToggleTranscript(sessionId, params, websocket)
if websocket: elif action == "toggleChat":
await websocket.send_text(json.dumps({ await self._cmdToggleChat(sessionId, params, websocket)
"type": "botCommand",
"sessionId": sessionId,
"command": "toggleTranscript",
"params": {"enable": enable},
}))
elif action == "sendChat": elif action == "sendChat":
chatText = params.get("text", "") await self._cmdSendChat(sessionId, params, websocket)
if chatText and websocket: elif action == "readChat":
await websocket.send_text(json.dumps({ await self._cmdReadChat(sessionId, params, voiceInterface, websocket)
"type": "sendChatMessage",
"sessionId": sessionId,
"text": chatText,
}))
elif action == "readAloud": elif action == "readAloud":
readText = params.get("text", "") await self._cmdReadAloud(sessionId, params, voiceInterface, websocket)
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",
},
}))
elif action == "changeLanguage": elif action == "changeLanguage":
newLang = params.get("language", "") await self._cmdChangeLanguage(sessionId, params)
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})
elif action in ("toggleMic", "toggleCamera"): elif action in ("toggleMic", "toggleCamera"):
if websocket: await self._cmdToggleMicOrCamera(sessionId, action, params, websocket)
await websocket.send_text(json.dumps({ elif action == "sendMail":
"type": "botCommand", await self._cmdSendMail(sessionId, params)
"sessionId": sessionId, elif action == "storeDocument":
"command": action, await self._cmdStoreDocument(sessionId, params)
"params": params,
}))
else: else:
logger.warning(f"Session {sessionId}: Unknown command '{action}'") logger.warning(f"Session {sessionId}: Unknown command '{action}'")
except Exception as cmdErr: except Exception as cmdErr:
logger.warning(f"Session {sessionId}: Command '{action}' failed: {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) # Context Summarization (for long sessions)
# ========================================================================= # =========================================================================