feat: teamsbot command refactoring, commcoach session cleanup
Made-with: Cursor
This commit is contained in:
parent
f4fb4637ea
commit
05f1c98825
3 changed files with 219 additions and 61 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue