platform-core/modules/features/teamsbot/serviceCommands.py
ValueOn AG 4a60086c80
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 15s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped
cp adapted to 2026 poweron
2026-06-09 09:53:31 +02:00

305 lines
11 KiB
Python

# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""
Teamsbot Service — AI command execution logic.
Extracted from service.py. All functions accept `service` (a TeamsbotService
instance) as the first parameter so the class can delegate to them.
"""
import logging
import json
import asyncio
from datetime import datetime, timezone
from typing import List
from fastapi import WebSocket
from modules.shared.timeUtils import getUtcTimestamp
from .datamodelTeamsbot import (
TeamsbotTranscript,
TeamsbotCommand,
)
logger = logging.getLogger(__name__)
async def _executeCommands(
service,
sessionId: str,
commands: List[TeamsbotCommand],
voiceInterface,
websocket: WebSocket,
):
"""Execute structured commands returned by the AI."""
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":
await _cmdToggleTranscript(service, sessionId, params, websocket)
elif action == "toggleChat":
await _cmdToggleChat(service, sessionId, params, websocket)
elif action == "sendChat":
await _cmdSendChat(service, sessionId, params, websocket)
elif action == "readChat":
await _cmdReadChat(service, sessionId, params, voiceInterface, websocket)
elif action == "readAloud":
await _cmdReadAloud(service, sessionId, params, voiceInterface, websocket)
elif action == "changeLanguage":
await _cmdChangeLanguage(service, sessionId, params)
elif action in ("toggleMic", "toggleCamera"):
await _cmdToggleMicOrCamera(service, sessionId, action, params, websocket)
elif action == "sendMail":
await _cmdSendMail(service, sessionId, params)
elif action == "storeDocument":
await _cmdStoreDocument(service, 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(service, 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(service, 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(service, sessionId: str, params: dict, websocket: WebSocket):
"""Send a message to the meeting chat and record it in transcript/SSE."""
from .service import _emitSessionEvent
chatText = params.get("text", "")
if not chatText:
return
if websocket:
await websocket.send_text(json.dumps({
"type": "sendChatMessage",
"sessionId": sessionId,
"text": chatText,
}))
logger.info(f"Chat command sent for session {sessionId}")
from . import interfaceFeatureTeamsbot as interfaceDb
interface = interfaceDb.getInterface(service.currentUser, service.mandateId, service.instanceId)
transcriptData = TeamsbotTranscript(
sessionId=sessionId,
speaker=service.config.botName,
text=chatText,
timestamp=getUtcTimestamp(),
confidence=1.0,
language=service.config.language,
isFinal=True,
source="chat",
).model_dump()
createdTranscript = interface.createTranscript(transcriptData)
import time
service._contextBuffer.append({
"speaker": service.config.botName,
"text": chatText,
"timestamp": getUtcTimestamp(),
"source": "chat",
})
service._lastTranscriptSpeaker = service.config.botName
service._lastTranscriptText = chatText
service._lastTranscriptId = createdTranscript.get("id")
service._lastBotResponseText = chatText.strip().lower()
service._lastBotResponseTs = time.time()
await _emitSessionEvent(sessionId, "transcript", {
"id": createdTranscript.get("id"),
"speaker": service.config.botName,
"text": chatText,
"confidence": 1.0,
"timestamp": getUtcTimestamp(),
"isContinuation": False,
"source": "chat",
"speakerResolvedFromHint": False,
})
async def _cmdReadChat(
service,
sessionId: str,
params: dict,
voiceInterface,
websocket: WebSocket,
):
"""Read chat messages (from DB) with optional fromdatetime/todatetime, then speak or send to chat."""
from .service import _speakTextChunked
from .serviceConversation import _summarizeForVoice
from . import interfaceFeatureTeamsbot as interfaceDb
interface = interfaceDb.getInterface(service.currentUser, service.mandateId, service.instanceId)
transcripts = interface.getTranscripts(sessionId)
fromDtRaw = params.get("fromdatetime") or params.get("fromDateTime")
toDtRaw = params.get("todatetime") or params.get("toDateTime")
fromTs = datetime.fromisoformat(fromDtRaw).replace(tzinfo=timezone.utc).timestamp() if fromDtRaw else None
toTs = datetime.fromisoformat(toDtRaw).replace(tzinfo=timezone.utc).timestamp() if toDtRaw else None
chatOnly = [t for t in transcripts if t.get("source") in ("chat", "chatHistory")]
if fromTs is not None:
chatOnly = [t for t in chatOnly if (t.get("timestamp") or 0) >= fromTs]
if toTs is not None:
chatOnly = [t for t in chatOnly if (t.get("timestamp") or 0) <= toTs]
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:
spokenSummary = await _summarizeForVoice(service, sessionId, summary[:2000])
cancelHook = service._makeAnswerCancelHook()
async with service._meetingTtsLock:
await _speakTextChunked(
websocket=websocket,
voiceInterface=voiceInterface,
sessionId=sessionId,
voiceText=spokenSummary,
languageCode=service.config.language,
voiceName=service.config.voiceId,
isCancelled=cancelHook,
)
async def _cmdReadAloud(
service,
sessionId: str,
params: dict,
voiceInterface,
websocket: WebSocket,
):
"""Read text aloud via TTS and play in meeting."""
from .service import _speakTextChunked, _voiceFriendlyMeetingText
readText = params.get("text", "")
if readText and voiceInterface and websocket:
cancelHook = service._makeAnswerCancelHook()
async with service._meetingTtsLock:
await _speakTextChunked(
websocket=websocket,
voiceInterface=voiceInterface,
sessionId=sessionId,
voiceText=_voiceFriendlyMeetingText(readText),
languageCode=service.config.language,
voiceName=service.config.voiceId,
isCancelled=cancelHook,
)
async def _cmdChangeLanguage(service, sessionId: str, params: dict):
"""Change bot language."""
from .service import _emitSessionEvent
newLang = params.get("language", "")
if newLang:
service.config = service.config.model_copy(update={"language": newLang})
logger.info(f"Session {sessionId}: Language changed to '{newLang}'")
await _emitSessionEvent(sessionId, "languageChanged", {"language": newLang})
async def _cmdToggleMicOrCamera(
service,
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(service, 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=service.currentUser,
mandateId=service.mandateId,
featureInstanceId=service.instanceId,
)
messaging = getService("messaging", ctx)
success = messaging.sendEmailDirect(
recipient=recipient,
subject=subject,
message=message,
userId=str(service.currentUser.id) if service.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(service, 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=service.currentUser,
mandateId=service.mandateId,
featureInstanceId=service.instanceId,
)
sharepoint = getService("sharepoint", ctx)
if not sharepoint.setAccessTokenFromConnection(service.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}")