Fix: add missing Automation2Workflow/Automation2WorkflowRun imports to interfaceFeatureGraphicalEditor.py (caused scheduler crash on boot) Refactor: gdprDeletion via onUserDelete lifecycle hooks Refactor: i18nBootSync accounting labels via app.py parameter injection Refactor: serviceHub moved to serviceCenter/serviceHub.py Split: teamsbot/service.py, realEstate/main, routeTrustee, routeBilling Cleanup: remove obsolete methodTrustee, serviceExceptions shim Co-authored-by: Cursor <cursoragent@cursor.com>
305 lines
11 KiB
Python
305 lines
11 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# 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,
|
|
mandate_id=service.mandateId,
|
|
feature_instance_id=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,
|
|
mandate_id=service.mandateId,
|
|
feature_instance_id=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}")
|