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, {
|
||||
"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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
# =========================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue